Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 166 additions & 18 deletions src/utils/exerciseLspServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,9 @@ 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 serverLineCount = totalLines;

let prev = "";

Expand Down Expand Up @@ -351,57 +354,201 @@ 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];

// Increase probabilities around things that look like jsdoc, where we've had problems in the past
const isAt = curr === "@";

// 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();
if (mutationRoll < (isDelimiter ? standardProb * 3 : standardProb)) {
const mutationType = prng.random();
const serverCharacter = Math.max(0, character + characterDelta);
// Delimiter characters have increased probability of single character deletion
if (mutationType < (isDelimiter ? 2 / 20 : 5 / 20)) {
// Insert "."
documentVersion++;
await notify("textDocument/didChange", {
textDocument: {
uri: openFileUri,
version: documentVersion,
},
contentChanges: [
{
range: {
start: { line, character: serverCharacter },
end: { line, character: serverCharacter },
},
text: ".",
},
],
});
characterDelta++;
}
else if (mutationType < (isDelimiter ? 4 / 20 : 10 / 20)) {
// 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: serverCharacter },
end: { line, character: serverCharacter },
},
text: randomChar,
},
],
});
characterDelta++;
}
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,
},
contentChanges: [
{
range: {
start: { line, character: serverCharacter },
end: { line, character: serverCharacter + 1 },
},
text: "",
},
],
});
characterDelta--;
}
}
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") {
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;
}
}
else {
// Reset file to original contents
documentVersion++;
await notify("textDocument/didChange", {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should try to bit somewhat smarter about deletion in order to get closer to scenarios where someone is halfway through typing/editing code? e.g. delete the rest of the line, delete ending parentheses/braces, etc.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea! I'll add a case for deleting the rest of the line, and I'll increase the probability of deletion if the character is a brace/dot/similar punctuation

textDocument: {
uri: openFileUri,
version: documentVersion,
},
contentChanges: [
{
text: openFileContents,
},
],
});
characterDelta = 0;
serverLineCount = totalLines;
}
}

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))) {
const standardProb = 0.001;
// 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) {
Expand All @@ -413,7 +560,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],
Expand All @@ -423,20 +570,20 @@ 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)
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,
Expand All @@ -446,7 +593,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,
},
Expand All @@ -465,7 +612,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],
Expand All @@ -479,7 +626,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,
Expand All @@ -492,7 +639,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,
Expand All @@ -513,8 +660,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",
},
Expand All @@ -524,6 +671,7 @@ async function exerciseLspServerWorker(testDir: string, lspServerPath: string, r

line++;
character = 0;
characterDelta = 0;
if (curr === "\r" && next === "\n") {
i++;
}
Expand Down