const assert = require("node:assert/strict"); const vscode = require("vscode"); async function waitFor(check) { for (let i = 0; i < 100; i++) { if (await check()) return; await new Promise((resolve) => setTimeout(resolve, 100)); } throw new Error("Editor condition timed out"); } async function run() { const config = vscode.workspace.getConfiguration("avril.lsp"); await config.update("preferences", { referencesCodeLensEnabled: true, referencesCodeLensShowOnAllFunctions: true }, vscode.ConfigurationTarget.Global); await config.update("executable", process.env.AVRIL_LSP_AVRIL, vscode.ConfigurationTarget.Global); await config.update("backend", process.env.AVRIL_LSP_BACKEND, vscode.ConfigurationTarget.Global); const folder = vscode.workspace.workspaceFolders[0].uri; const main = await vscode.workspace.openTextDocument(vscode.Uri.joinPath(folder, "main.tsx")); const at = (document, word, last = true) => { const text = document.getText(); return document.positionAt((last ? text.lastIndexOf(word) : text.indexOf(word)) + 1); }; const hover = () => vscode.commands.executeCommand("vscode.executeHoverProvider", main.uri, at(main, "localValue")); const definitions = (word) => vscode.commands.executeCommand("vscode.executeDefinitionProvider", main.uri, at(main, word, false)); const hasType = async (type) => (await hover()).some((h) => h.contents.some((c) => c.value.includes(type))); try { assert.ok(vscode.extensions.getExtension("vscode.typescript-language-features")); await vscode.extensions.getExtension("avril.avril").activate(); let editor = await vscode.window.showTextDocument(main); await waitFor(() => hasType("localValue: 42")); await waitFor(async () => (await hover()).length === 1); const [local] = await definitions("localValue"); assert.equal(local.targetUri.toString(), vscode.Uri.joinPath(folder, "dep file.ts").toString()); const dep = await vscode.workspace.openTextDocument(local.targetUri); assert.equal(dep.getText(local.targetSelectionRange), "value"); editor.selection = new vscode.Selection(at(main, "localValue", false), at(main, "localValue", false)); await vscode.commands.executeCommand("editor.action.revealDefinition"); await waitFor(() => vscode.window.activeTextEditor?.document.uri.toString() === dep.uri.toString()); assert.equal(vscode.window.activeTextEditor.selection.active.character, local.targetSelectionRange.start.character); console.log("PASS actual editor F12 navigation: original local file and exact selection"); const [alias] = await definitions("answer"); assert.equal(alias.targetUri.toString(), vscode.Uri.joinPath(folder, "data.d.ts").toString()); const declaration = await vscode.workspace.openTextDocument(alias.targetUri); assert.equal(declaration.getText(alias.targetSelectionRange), "answer"); await vscode.window.showTextDocument(declaration); console.log("PASS live avril.conf alias resolves to the original declaration"); editor = await vscode.window.showTextDocument(dep); const edited = '\r\n// 😀 unsaved\r\nexport const value = "edited" as const;\r\n'; await editor.edit((builder) => builder.replace(new vscode.Range(dep.positionAt(0), dep.positionAt(dep.getText().length)), edited)); await waitFor(() => hasType('localValue: "edited"')); const [changed] = await definitions("localValue"); assert.equal(changed.targetSelectionRange.start.line, 2); assert.equal(dep.getText(changed.targetSelectionRange), "value"); await waitFor(() => vscode.languages.getDiagnostics(main.uri).some((d) => d.code === 2322)); console.log("PASS unsaved dependency edits update editor hover, definition ranges and diagnostics"); await vscode.commands.executeCommand("workbench.action.revertAndCloseActiveEditor"); await waitFor(() => hasType("localValue: 42")); editor = await vscode.window.showTextDocument(main); const full = main.getText(); const completions = (position) => vscode.commands.executeCommand("vscode.executeCompletionItemProvider", main.uri, position); const label = (item) => item.label?.label ?? item.label; await editor.edit((builder) => builder.replace( new vscode.Range(main.positionAt(0), main.positionAt(full.length)), full.replace("process.args;", "process.ar;"), )); const partial = main.positionAt(main.getText().indexOf("process.ar;") + "process.ar".length); await waitFor(async () => (await completions(partial))?.items.some((item) => label(item) === "args")); const suggestion = (await completions(partial)).items.find((item) => label(item) === "args"); const edit = suggestion.textEdit; const candidate = suggestion.range ?? edit?.range ?? edit?.replace; const replacementRange = candidate?.replace ?? candidate?.replacing ?? candidate; assert.ok(replacementRange, "Completion has no applicable range"); const range = replacementRange instanceof vscode.Range ? replacementRange : Array.isArray(replacementRange) ? new vscode.Range( new vscode.Position(...replacementRange[0]), new vscode.Position(...replacementRange[1]), ) : new vscode.Range( new vscode.Position(replacementRange.start.line, replacementRange.start.character), new vscode.Position(replacementRange.end.line, replacementRange.end.character), ); const replacement = suggestion.insertText?.value ?? suggestion.insertText ?? edit?.newText ?? "args"; await editor.edit((builder) => builder.replace(range, replacement)); assert.equal(main.getText(), full); await vscode.commands.executeCommand("undo"); await vscode.commands.executeCommand("undo"); assert.equal(main.getText(), full); assert.equal(main.isDirty, false); console.log("PASS applying an avril completion replaces the partial word without saving"); const referencesSource = "function localReferences(value: number): number { return value; }\nlocalReferences(1);"; await editor.edit((builder) => builder.replace( new vscode.Range(main.positionAt(0), main.positionAt(full.length)), referencesSource, )); await vscode.commands.executeCommand("vscode.executeCodeLensProvider", main.uri); const codeLenses = await vscode.commands.executeCommand("vscode.executeCodeLensProvider", main.uri, 1); const codeLens = codeLenses.find((lens) => lens.command?.command === "avril.showReferences" && main.getText(lens.range).includes("localReferences")); assert.ok(codeLens, `Expected a resolved references code lens: ${JSON.stringify(codeLenses)}`); const [lensUri, lensPosition, locations] = codeLens.command.arguments; assert.equal(lensUri, main.uri.toString()); assert.ok(Number.isInteger(lensPosition.line) && Number.isInteger(lensPosition.character)); assert.ok(Array.isArray(locations) && locations.length === 1, JSON.stringify(locations)); await vscode.commands.executeCommand(codeLens.command.command, ...codeLens.command.arguments); console.log("PASS resolved references code lens invokes editor.action.showReferences with editor-native arguments"); await vscode.commands.executeCommand("closeReferenceSearch"); editor = await vscode.window.showTextDocument(main); await vscode.commands.executeCommand("undo"); assert.equal(main.getText(), full); assert.equal(main.isDirty, false); await editor.edit((builder) => builder.replace( new vscode.Range(main.positionAt(0), main.positionAt(full.length)), full + "\nfunction takes(value: number): number { return value; }\ntakes(1);", )); const signaturePosition = main.positionAt(main.getText().lastIndexOf("takes(1)") + "takes(1".length); let lastSignatureHelp; try { await waitFor(async () => { lastSignatureHelp = await vscode.commands.executeCommand("vscode.executeSignatureHelpProvider", main.uri, signaturePosition); return lastSignatureHelp?.signatures.some((signature) => signature.label.includes("takes(value: number): number")); }); } catch (error) { throw new Error(`${error.message}; last signature help: ${JSON.stringify(lastSignatureHelp)}`); } const renamed = await vscode.commands.executeCommand( "vscode.executeDocumentRenameProvider", main.uri, at(main, "localValue", false), "renamedValue", ); const renameEntries = renamed?.entries?.() || []; const renameChanges = renamed?.changes ? Object.values(renamed.changes) : []; assert.ok( [...renameEntries.map(([, edits]) => edits), ...renameChanges].some((edits) => edits.length > 0), "Native rename returned no text edits", ); await vscode.commands.executeCommand("undo"); assert.equal(main.getText(), full); assert.equal(main.isDirty, false); console.log("PASS native signature help and rename providers operate on unsaved documents"); const [builtin] = await definitions("args"); const builtinDocument = await vscode.workspace.openTextDocument(builtin.targetUri); assert.equal(builtin.targetUri.scheme, "avril-builtin"); assert.equal(vscode.workspace.fs.isWritableFileSystem("avril-builtin"), undefined); assert.equal(builtinDocument.getText(builtin.targetSelectionRange), "args"); console.log("PASS builtin navigation selects the real avril declaration"); editor = await vscode.window.showTextDocument(main); const inlineSource = "export const value: number = 42; // ?#% 😀"; const inlineUrl = `data:text/typescript,${encodeURIComponent(inlineSource)}`; await editor.edit((builder) => builder.replace( new vscode.Range(main.positionAt(0), main.positionAt(full.length)), full + `\nimport {value as inlineValue} from ${JSON.stringify(inlineUrl)};\ninlineValue;`, )); let inlineHover; try { await waitFor(async () => { inlineHover = await vscode.commands.executeCommand("vscode.executeHoverProvider", main.uri, at(main, "inlineValue")); return inlineHover.some((h) => h.contents.some((c) => c.value.includes("(alias) const value: number"))); }); } catch (error) { throw new Error(`${error.message}; last inline hover: ${JSON.stringify(inlineHover?.flatMap((h) => h.contents.map((c) => c.value)))}`); } const [inline] = await definitions("inlineValue"); assert.ok(inline.targetUri.toString().startsWith("avril-builtin:/data/")); const inlineDocument = await vscode.workspace.openTextDocument(inline.targetUri); assert.equal(inlineDocument.getText(), inlineSource); assert.equal(inlineDocument.getText(inline.targetSelectionRange), "value"); assert.equal(inlineDocument.languageId, "typescript"); await vscode.window.showTextDocument(inlineDocument); editor = await vscode.window.showTextDocument(main); await vscode.commands.executeCommand("undo"); assert.equal(main.getText(), full); assert.equal(main.isDirty, false); console.log("PASS data URL definition opens the decoded, read-only TypeScript source"); await vscode.commands.executeCommand("avril.stopLanguageServer"); await vscode.window.showTextDocument(main); const hasBuiltinError = () => vscode.languages.getDiagnostics(main.uri).some((d) => d.source === "ts" && (d.code === 2307 || d.code?.value === 2307)); await waitFor(hasBuiltinError); await vscode.commands.executeCommand("avril.startLanguageServer"); await waitFor(async () => (await hover()).length === 1 && await hasType("localValue: 42") && !hasBuiltinError()); console.log("PASS automatic startup and reversible built-in TypeScript suppression"); console.log("AVRIL_EDITOR_TESTS_PASSED"); } finally { await vscode.commands.executeCommand("avril.stopLanguageServer"); } } module.exports = { run };