From d33e88dac5955071e7f6c2934cf0faf3c21598f9 Mon Sep 17 00:00:00 2001 From: Richard Moulton Date: Thu, 30 Oct 2025 15:51:30 +0000 Subject: [PATCH 1/2] Add debouncing to document change handler to fix offset issues Prevents multiple concurrent parse operations during live editing that were causing duplicate definitions with incorrect offsets, resulting in false linter errors about variable name casing. --- extension/server/src/server.ts | 41 +++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/extension/server/src/server.ts b/extension/server/src/server.ts index 8809ab44..d6893d55 100644 --- a/extension/server/src/server.ts +++ b/extension/server/src/server.ts @@ -323,21 +323,36 @@ if (languageToolsEnabled) { if (isLinterEnabled()) Linter.initialise(connection); +// Debounce timers for document changes +const documentChangeTimers: { [uri: string]: NodeJS.Timeout } = {}; + // Always get latest stuff documents.onDidChangeContent(handler => { - parser.getDocs( - handler.document.uri, - handler.document.getText(), - { - withIncludes: true, - ignoreCache: true, - collectReferences: true - } - ).then(cache => { - if (cache) { - Linter.refreshLinterDiagnostics(handler.document, cache); - } - }); + const uri = handler.document.uri; + + // Clear any existing timer for this document + if (documentChangeTimers[uri]) { + clearTimeout(documentChangeTimers[uri]); + } + + // Set a new timer to parse after a short delay (500ms) + documentChangeTimers[uri] = setTimeout(() => { + delete documentChangeTimers[uri]; + + parser.getDocs( + uri, + handler.document.getText(), + { + withIncludes: true, + ignoreCache: true, + collectReferences: true + } + ).then(cache => { + if (cache) { + Linter.refreshLinterDiagnostics(handler.document, cache); + } + }); + }, 500); }); // Make the text document manager listen on the connection From 6b6624ab1185e3f7053ceb6adde533a501689481 Mon Sep 17 00:00:00 2001 From: Richard Moulton Date: Fri, 31 Oct 2025 17:24:39 +0000 Subject: [PATCH 2/2] Enhance debouncing with parse state tracking and validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Improves the offset issue fix by adding parse ID tracking to invalidate stale parse operations, a parsing flag to prevent concurrent parses, error handling, and reduces debounce time from 500ms to 300ms for better responsiveness. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- extension/server/src/server.ts | 56 ++++++++++++++++++++++++++-------- 1 file changed, 43 insertions(+), 13 deletions(-) diff --git a/extension/server/src/server.ts b/extension/server/src/server.ts index d6893d55..b38eea42 100644 --- a/extension/server/src/server.ts +++ b/extension/server/src/server.ts @@ -203,7 +203,7 @@ parser.setIncludeFileFetch(async (stringUri: string, includeString: string) => { // Resolving IFS path from member or streamfile // IFS fetch - + if (cleanString.startsWith(`/`)) { // Path from root validUri = URI.from({ @@ -217,7 +217,7 @@ parser.setIncludeFileFetch(async (stringUri: string, includeString: string) => { // - `${cleanString}.rpgleinc` // - `${cleanString}.rpgle` const possibleFiles = [cleanString, `${cleanString}.rpgleinc`, `${cleanString}.rpgle`]; - + // Path from home directory? const foundStreamfile = await streamfileResolve(stringUri, possibleFiles); @@ -295,7 +295,7 @@ parser.setIncludeFileFetch(async (stringUri: string, includeString: string) => { } } - + fetchingInProgress[includeString] = false; return { @@ -323,21 +323,45 @@ if (languageToolsEnabled) { if (isLinterEnabled()) Linter.initialise(connection); -// Debounce timers for document changes -const documentChangeTimers: { [uri: string]: NodeJS.Timeout } = {}; +// Track parsing state for each document +const documentParseState: { + [uri: string]: { + timer?: NodeJS.Timeout, + parseId: number, + parsing: boolean + } +} = {}; // Always get latest stuff documents.onDidChangeContent(handler => { const uri = handler.document.uri; - // Clear any existing timer for this document - if (documentChangeTimers[uri]) { - clearTimeout(documentChangeTimers[uri]); + // Initialize state if needed + if (!documentParseState[uri]) { + documentParseState[uri] = { parseId: 0, parsing: false }; + } + + const state = documentParseState[uri]; + + // Clear any existing timer + if (state.timer) { + clearTimeout(state.timer); } - // Set a new timer to parse after a short delay (500ms) - documentChangeTimers[uri] = setTimeout(() => { - delete documentChangeTimers[uri]; + // Increment parse ID to invalidate any in-flight parses + state.parseId++; + const currentParseId = state.parseId; + + // Set a new timer to parse after a short delay + state.timer = setTimeout(() => { + delete state.timer; + + // Skip if already parsing this document + if (state.parsing) { + return; + } + + state.parsing = true; parser.getDocs( uri, @@ -348,11 +372,17 @@ documents.onDidChangeContent(handler => { collectReferences: true } ).then(cache => { - if (cache) { + state.parsing = false; + + // Only update diagnostics if this is still the latest parse + if (cache && currentParseId === state.parseId) { Linter.refreshLinterDiagnostics(handler.document, cache); } + }).catch(err => { + state.parsing = false; + console.error(`Error parsing ${uri}:`, err); }); - }, 500); + }, 300); // Reduced to 300ms for better responsiveness }); // Make the text document manager listen on the connection