From dc50a8b9ad6278fcafc02a3a76858adde45c8073 Mon Sep 17 00:00:00 2001 From: John Favret <64748847+johnfav03@users.noreply.github.com> Date: Tue, 24 Feb 2026 09:48:37 -0800 Subject: [PATCH 1/5] added single character insertions and deletions --- src/utils/exerciseLspServer.ts | 66 +++++++++++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/src/utils/exerciseLspServer.ts b/src/utils/exerciseLspServer.ts index e2ad398..86579bf 100644 --- a/src/utils/exerciseLspServer.ts +++ b/src/utils/exerciseLspServer.ts @@ -351,6 +351,7 @@ async function exerciseLspServerWorker(testDir: string, lspServerPath: string, r }, 0.5); } + const standardProb = 0.001; for (let i = 0; i < openFileContents.length; i++) { const curr = openFileContents[i]; const next = openFileContents[i + 1]; @@ -358,9 +359,72 @@ async function exerciseLspServerWorker(testDir: string, lspServerPath: string, r // Increase probabilities around things that look like jsdoc, where we've had problems in the past const isAt = curr === "@"; + // Single character mutations (insertion/deletion) + const mutationRoll = prng.random(); + if (mutationRoll < standardProb) { + const mutationType = prng.random(); + if (mutationType < 1 / 3) { + // Insert "." + documentVersion++; + await notify("textDocument/didChange", { + textDocument: { + uri: openFileUri, + version: documentVersion, + }, + contentChanges: [ + { + range: { + start: { line, character }, + end: { line, character }, + }, + text: ".", + }, + ], + }); + } + else if (mutationType < 2 / 3) { + // Insert random character + const randomChar = String.fromCharCode(prng.intBetween(32, 126)); + documentVersion++; + await notify("textDocument/didChange", { + textDocument: { + uri: openFileUri, + version: documentVersion, + }, + contentChanges: [ + { + range: { + start: { line, character }, + end: { line, character }, + }, + text: randomChar, + }, + ], + }); + } + else if (character > 0) { + // Delete previous character + documentVersion++; + await notify("textDocument/didChange", { + textDocument: { + uri: openFileUri, + version: documentVersion, + }, + contentChanges: [ + { + range: { + start: { line, character: character - 1 }, + end: { line, character }, + }, + text: "", + }, + ], + }); + } + } + // Note that this only catches Latin letters - we'll test within tokens of non-Latin characters if (!(/\w/.test(prev) && /\w/.test(curr)) && !(/[ \t]/.test(prev) && /[ \t]/.test(curr))) { - const standardProb = 0.001; // Definition (equivalent to definitionAndBoundSpan) await request("textDocument/definition", { textDocument: { uri: openFileUri }, From 3e3214e4c692ee678d7ce033a7671d0762302737 Mon Sep 17 00:00:00 2001 From: John Favret <64748847+johnfav03@users.noreply.github.com> Date: Tue, 24 Feb 2026 15:59:56 -0800 Subject: [PATCH 2/5] added character delta for pos bounding --- src/utils/exerciseLspServer.ts | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/utils/exerciseLspServer.ts b/src/utils/exerciseLspServer.ts index 86579bf..7d1b109 100644 --- a/src/utils/exerciseLspServer.ts +++ b/src/utils/exerciseLspServer.ts @@ -274,6 +274,8 @@ async function exerciseLspServerWorker(testDir: string, lspServerPath: string, r let line = 0; // LSP uses 0-based lines let character = 0; // LSP uses 0-based characters + let characterDelta = 0; // Net character offset from mutations on the current line + const totalLines = openFileContents.split(/\r\n|\r|\n/).length; let prev = ""; @@ -363,6 +365,7 @@ async function exerciseLspServerWorker(testDir: string, lspServerPath: string, r const mutationRoll = prng.random(); if (mutationRoll < standardProb) { const mutationType = prng.random(); + const serverCharacter = character + characterDelta; if (mutationType < 1 / 3) { // Insert "." documentVersion++; @@ -374,13 +377,14 @@ async function exerciseLspServerWorker(testDir: string, lspServerPath: string, r contentChanges: [ { range: { - start: { line, character }, - end: { line, character }, + start: { line, character: serverCharacter }, + end: { line, character: serverCharacter }, }, text: ".", }, ], }); + characterDelta++; } else if (mutationType < 2 / 3) { // Insert random character @@ -394,15 +398,16 @@ async function exerciseLspServerWorker(testDir: string, lspServerPath: string, r contentChanges: [ { range: { - start: { line, character }, - end: { line, character }, + start: { line, character: serverCharacter }, + end: { line, character: serverCharacter }, }, text: randomChar, }, ], }); + characterDelta++; } - else if (character > 0) { + else if (serverCharacter > 0) { // Delete previous character documentVersion++; await notify("textDocument/didChange", { @@ -413,13 +418,14 @@ async function exerciseLspServerWorker(testDir: string, lspServerPath: string, r contentChanges: [ { range: { - start: { line, character: character - 1 }, - end: { line, character }, + start: { line, character: serverCharacter - 1 }, + end: { line, character: serverCharacter }, }, text: "", }, ], }); + characterDelta--; } } @@ -500,7 +506,7 @@ async function exerciseLspServerWorker(testDir: string, lspServerPath: string, r // Range formatting (equivalent to format with range) await request("textDocument/rangeFormatting", { textDocument: { uri: openFileUri }, - range: { start: { line, character: 0 }, end: { line: line + 10, character: 0 } }, + range: { start: { line, character: 0 }, end: { line: Math.min(line + 10, totalLines - 1), character: 0 } }, options: { tabSize: prng.intBetween(1, 4), insertSpaces: prng.random() < 0.5, @@ -588,6 +594,7 @@ async function exerciseLspServerWorker(testDir: string, lspServerPath: string, r line++; character = 0; + characterDelta = 0; if (curr === "\r" && next === "\n") { i++; } From d3bd3d8645e824666d4e9d7cb6f2f731cca8b98b Mon Sep 17 00:00:00 2001 From: John Favret <64748847+johnfav03@users.noreply.github.com> Date: Tue, 24 Feb 2026 16:12:09 -0800 Subject: [PATCH 3/5] extend use of servercharacter --- src/utils/exerciseLspServer.ts | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/src/utils/exerciseLspServer.ts b/src/utils/exerciseLspServer.ts index 7d1b109..b329db5 100644 --- a/src/utils/exerciseLspServer.ts +++ b/src/utils/exerciseLspServer.ts @@ -429,49 +429,51 @@ async function exerciseLspServerWorker(testDir: string, lspServerPath: string, r } } + const serverCharacter = character + characterDelta; + // Note that this only catches Latin letters - we'll test within tokens of non-Latin characters if (!(/\w/.test(prev) && /\w/.test(curr)) && !(/[ \t]/.test(prev) && /[ \t]/.test(curr))) { // Definition (equivalent to definitionAndBoundSpan) await request("textDocument/definition", { textDocument: { uri: openFileUri }, - position: { line, character }, + position: { line, character: serverCharacter }, }, isAt ? 0.5 : standardProb); // References await request("textDocument/references", { textDocument: { uri: openFileUri }, - position: { line, character }, + position: { line, character: serverCharacter }, context: { includeDeclaration: true }, }, isAt ? 0.5 : 0.00005); // Hover (equivalent to quickinfo) await request("textDocument/hover", { textDocument: { uri: openFileUri }, - position: { line, character }, + position: { line, character: serverCharacter }, }, isAt ? 0.5 : standardProb); // Implementation (equivalent to implementation) await request("textDocument/implementation", { textDocument: { uri: openFileUri }, - position: { line, character }, + position: { line, character: serverCharacter }, }, isAt ? 0.3 : 0.0003); // Type definition (equivalent to typeDefinition) await request("textDocument/typeDefinition", { textDocument: { uri: openFileUri }, - position: { line, character }, + position: { line, character: serverCharacter }, }, isAt ? 0.3 : 0.0003); // Document highlight (equivalent to documentHighlights) await request("textDocument/documentHighlight", { textDocument: { uri: openFileUri }, - position: { line, character }, + position: { line, character: serverCharacter }, }, isAt ? 0.3 : 0.0003); // Call hierarchy (equivalent to prepareCallHierarchy + incoming/outgoing) const callHierarchyItems = await request("textDocument/prepareCallHierarchy", { textDocument: { uri: openFileUri }, - position: { line, character }, + position: { line, character: serverCharacter }, }, isAt ? 0.3 : 0.0002); if (callHierarchyItems && callHierarchyItems.length > 0) { @@ -483,7 +485,7 @@ async function exerciseLspServerWorker(testDir: string, lspServerPath: string, r // Code action for refactors (equivalent to getApplicableRefactors) const refactorActions = await request("textDocument/codeAction", { textDocument: { uri: openFileUri }, - range: { start: { line, character }, end: { line, character } }, + range: { start: { line, character: serverCharacter }, end: { line, character: serverCharacter } }, context: { diagnostics: [], only: [protocol.CodeActionKind.Refactor], @@ -493,14 +495,14 @@ async function exerciseLspServerWorker(testDir: string, lspServerPath: string, r // Rename (equivalent to rename) await request("textDocument/rename", { textDocument: { uri: openFileUri }, - position: { line, character }, + position: { line, character: serverCharacter }, newName: "renamedSymbol", }, isAt ? 0.2 : 0.0002); // Selection range (equivalent to selectionRange) await request("textDocument/selectionRange", { textDocument: { uri: openFileUri }, - positions: [{ line, character }], + positions: [{ line, character: serverCharacter }], }, isAt ? 0.3 : 0.0003); // Range formatting (equivalent to format with range) @@ -516,7 +518,7 @@ async function exerciseLspServerWorker(testDir: string, lspServerPath: string, r // Completions (equivalent to completionInfo) const completionResponse = await request("textDocument/completion", { textDocument: { uri: openFileUri }, - position: { line, character }, + position: { line, character: serverCharacter }, context: { triggerKind: protocol.CompletionTriggerKind.Invoked, }, @@ -535,7 +537,7 @@ async function exerciseLspServerWorker(testDir: string, lspServerPath: string, r if (triggerCharIndex >= 0 && /\w/.test(prev)) { await request("textDocument/completion", { textDocument: { uri: openFileUri }, - position: { line, character }, + position: { line, character: serverCharacter }, context: { triggerKind: protocol.CompletionTriggerKind.TriggerCharacter, triggerCharacter: triggerChars[triggerCharIndex], @@ -549,7 +551,7 @@ async function exerciseLspServerWorker(testDir: string, lspServerPath: string, r // Signature help (equivalent to signatureHelp) await request("textDocument/signatureHelp", { textDocument: { uri: openFileUri }, - position: { line, character }, + position: { line, character: serverCharacter }, context: { triggerCharacter: currisSignatureHelpTrigger ? curr : undefined, triggerKind: currisSignatureHelpTrigger ? protocol.SignatureHelpTriggerKind.TriggerCharacter : protocol.SignatureHelpTriggerKind.Invoked, @@ -562,7 +564,7 @@ async function exerciseLspServerWorker(testDir: string, lspServerPath: string, r if (curr === ";" || curr === "}") { await request("textDocument/onTypeFormatting", { textDocument: { uri: openFileUri }, - position: { line, character }, + position: { line, character: serverCharacter }, ch: curr, options: { tabSize: 4, @@ -583,8 +585,8 @@ async function exerciseLspServerWorker(testDir: string, lspServerPath: string, r contentChanges: [ { range: { - start: { line, character }, - end: { line, character }, + start: { line, character: serverCharacter }, + end: { line, character: serverCharacter }, }, text: " //comment", }, From 9a3f3a9cee9659df49de38c54385e3f2a23b66ed Mon Sep 17 00:00:00 2001 From: John Favret <64748847+johnfav03@users.noreply.github.com> Date: Wed, 25 Feb 2026 10:36:59 -0800 Subject: [PATCH 4/5] improved deletion methods --- src/utils/exerciseLspServer.ts | 45 +++++++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/src/utils/exerciseLspServer.ts b/src/utils/exerciseLspServer.ts index b329db5..217e2ab 100644 --- a/src/utils/exerciseLspServer.ts +++ b/src/utils/exerciseLspServer.ts @@ -362,11 +362,14 @@ async function exerciseLspServerWorker(testDir: string, lspServerPath: string, r const isAt = curr === "@"; // Single character mutations (insertion/deletion) + const delimiters = ",.;:{}[]<>()"; + const isDelimiter = delimiters.includes(curr); const mutationRoll = prng.random(); - if (mutationRoll < standardProb) { + if (mutationRoll < (isDelimiter ? standardProb * 3 : standardProb)) { const mutationType = prng.random(); - const serverCharacter = character + characterDelta; - if (mutationType < 1 / 3) { + const serverCharacter = Math.max(0, character + characterDelta); + // Delimiter characters have increased probability of single character deletion + if (mutationType < (isDelimiter ? 1 / 6 : 1 / 4)) { // Insert "." documentVersion++; await notify("textDocument/didChange", { @@ -386,7 +389,7 @@ async function exerciseLspServerWorker(testDir: string, lspServerPath: string, r }); characterDelta++; } - else if (mutationType < 2 / 3) { + else if (mutationType < (isDelimiter ? 2 / 6 : 2 / 4)) { // Insert random character const randomChar = String.fromCharCode(prng.intBetween(32, 126)); documentVersion++; @@ -407,7 +410,7 @@ async function exerciseLspServerWorker(testDir: string, lspServerPath: string, r }); characterDelta++; } - else if (serverCharacter > 0) { + else if (mutationType < (isDelimiter ? 5 / 6 : 3 / 4) && serverCharacter > 0) { // Delete previous character documentVersion++; await notify("textDocument/didChange", { @@ -427,9 +430,39 @@ async function exerciseLspServerWorker(testDir: string, lspServerPath: string, r }); characterDelta--; } + else { + // Delete rest of line (not including newline) + let endIdx = i; + while (endIdx < openFileContents.length && openFileContents[endIdx] !== "\r" && openFileContents[endIdx] !== "\n") { + endIdx++; + } + const remainingChars = endIdx - i; + // Compute end of line in server coordinates, accounting for prior mutations + const serverEndOfLine = Math.max(0, character + remainingChars + characterDelta); + if (serverEndOfLine > serverCharacter) { + const charsToDelete = serverEndOfLine - serverCharacter; + documentVersion++; + await notify("textDocument/didChange", { + textDocument: { + uri: openFileUri, + version: documentVersion, + }, + contentChanges: [ + { + range: { + start: { line, character: serverCharacter }, + end: { line, character: serverEndOfLine }, + }, + text: "", + }, + ], + }); + characterDelta -= charsToDelete; + } + } } - const serverCharacter = character + characterDelta; + const serverCharacter = Math.max(0, character + characterDelta); // Note that this only catches Latin letters - we'll test within tokens of non-Latin characters if (!(/\w/.test(prev) && /\w/.test(curr)) && !(/[ \t]/.test(prev) && /[ \t]/.test(curr))) { From 3cc215cde135bc9e3f2973acbe80268ad2aa6e9f Mon Sep 17 00:00:00 2001 From: John Favret <64748847+johnfav03@users.noreply.github.com> Date: Wed, 25 Feb 2026 14:10:12 -0800 Subject: [PATCH 5/5] added mutation reset chance --- src/utils/exerciseLspServer.ts | 86 +++++++++++++++++++++++++--------- 1 file changed, 64 insertions(+), 22 deletions(-) diff --git a/src/utils/exerciseLspServer.ts b/src/utils/exerciseLspServer.ts index 217e2ab..f5f2635 100644 --- a/src/utils/exerciseLspServer.ts +++ b/src/utils/exerciseLspServer.ts @@ -276,6 +276,7 @@ async function exerciseLspServerWorker(testDir: string, lspServerPath: string, r let character = 0; // LSP uses 0-based characters let characterDelta = 0; // Net character offset from mutations on the current line const totalLines = openFileContents.split(/\r\n|\r|\n/).length; + let serverLineCount = totalLines; let prev = ""; @@ -361,7 +362,24 @@ async function exerciseLspServerWorker(testDir: string, lspServerPath: string, r // Increase probabilities around things that look like jsdoc, where we've had problems in the past const isAt = curr === "@"; - // Single character mutations (insertion/deletion) + // Skip mutations and requests when line is out of bounds for the server + if (line >= serverLineCount) { + if (curr === "\r" || curr === "\n") { + line++; + character = 0; + characterDelta = 0; + if (curr === "\r" && next === "\n") { + i++; + } + } + else { + character++; + } + prev = curr; + continue; + } + + // Single character mutations (insertion/deletion/reset) const delimiters = ",.;:{}[]<>()"; const isDelimiter = delimiters.includes(curr); const mutationRoll = prng.random(); @@ -369,7 +387,7 @@ async function exerciseLspServerWorker(testDir: string, lspServerPath: string, r const mutationType = prng.random(); const serverCharacter = Math.max(0, character + characterDelta); // Delimiter characters have increased probability of single character deletion - if (mutationType < (isDelimiter ? 1 / 6 : 1 / 4)) { + if (mutationType < (isDelimiter ? 2 / 20 : 5 / 20)) { // Insert "." documentVersion++; await notify("textDocument/didChange", { @@ -389,7 +407,7 @@ async function exerciseLspServerWorker(testDir: string, lspServerPath: string, r }); characterDelta++; } - else if (mutationType < (isDelimiter ? 2 / 6 : 2 / 4)) { + else if (mutationType < (isDelimiter ? 4 / 20 : 10 / 20)) { // Insert random character const randomChar = String.fromCharCode(prng.intBetween(32, 126)); documentVersion++; @@ -410,27 +428,34 @@ async function exerciseLspServerWorker(testDir: string, lspServerPath: string, r }); characterDelta++; } - else if (mutationType < (isDelimiter ? 5 / 6 : 3 / 4) && serverCharacter > 0) { - // Delete previous character - documentVersion++; - await notify("textDocument/didChange", { - textDocument: { - uri: openFileUri, - version: documentVersion, - }, - contentChanges: [ - { - range: { - start: { line, character: serverCharacter - 1 }, - end: { line, character: serverCharacter }, - }, - text: "", + else if (mutationType < (isDelimiter ? 16 / 20 : 15 / 20)) { + // Delete current character, but only if within estimated line content + let lineEndIdx = i; + while (lineEndIdx < openFileContents.length && openFileContents[lineEndIdx] !== "\r" && openFileContents[lineEndIdx] !== "\n") { + lineEndIdx++; + } + const estimatedServerLineLength = Math.max(0, character + (lineEndIdx - i) + characterDelta); + if (serverCharacter + 1 <= estimatedServerLineLength) { + documentVersion++; + await notify("textDocument/didChange", { + textDocument: { + uri: openFileUri, + version: documentVersion, }, - ], - }); - characterDelta--; + contentChanges: [ + { + range: { + start: { line, character: serverCharacter }, + end: { line, character: serverCharacter + 1 }, + }, + text: "", + }, + ], + }); + characterDelta--; + } } - else { + else if (mutationType < (isDelimiter ? 19 / 20 : 19 / 20)) { // Delete rest of line (not including newline) let endIdx = i; while (endIdx < openFileContents.length && openFileContents[endIdx] !== "\r" && openFileContents[endIdx] !== "\n") { @@ -460,6 +485,23 @@ async function exerciseLspServerWorker(testDir: string, lspServerPath: string, r characterDelta -= charsToDelete; } } + else { + // Reset file to original contents + documentVersion++; + await notify("textDocument/didChange", { + textDocument: { + uri: openFileUri, + version: documentVersion, + }, + contentChanges: [ + { + text: openFileContents, + }, + ], + }); + characterDelta = 0; + serverLineCount = totalLines; + } } const serverCharacter = Math.max(0, character + characterDelta);