char/avril

(wip) why'd you have to go and make things so complicaaaaated?

git clone https://git.t4t.associates/char/avril

Charlotte Somsupport data urls1c8ffed

main
11.5 KiB196 linesraw
1const assert = require("node:assert/strict");
2const vscode = require("vscode");
3
4async function waitFor(check) {
5  for (let i = 0; i < 100; i++) {
6    if (await check()) return;
7    await new Promise((resolve) => setTimeout(resolve, 100));
8  }
9  throw new Error("Editor condition timed out");
10}
11
12async function run() {
13  const config = vscode.workspace.getConfiguration("avril.lsp");
14  await config.update("preferences", { referencesCodeLensEnabled: true, referencesCodeLensShowOnAllFunctions: true }, vscode.ConfigurationTarget.Global);
15  await config.update("executable", process.env.AVRIL_LSP_AVRIL, vscode.ConfigurationTarget.Global);
16  await config.update("backend", process.env.AVRIL_LSP_BACKEND, vscode.ConfigurationTarget.Global);
17  const folder = vscode.workspace.workspaceFolders[0].uri;
18  const main = await vscode.workspace.openTextDocument(vscode.Uri.joinPath(folder, "main.tsx"));
19  const at = (document, word, last = true) => {
20    const text = document.getText();
21    return document.positionAt((last ? text.lastIndexOf(word) : text.indexOf(word)) + 1);
22  };
23  const hover = () => vscode.commands.executeCommand("vscode.executeHoverProvider", main.uri, at(main, "localValue"));
24  const definitions = (word) => vscode.commands.executeCommand("vscode.executeDefinitionProvider", main.uri, at(main, word, false));
25  const hasType = async (type) => (await hover()).some((h) => h.contents.some((c) => c.value.includes(type)));
26  try {
27    assert.ok(vscode.extensions.getExtension("vscode.typescript-language-features"));
28    await vscode.extensions.getExtension("avril.avril").activate();
29    let editor = await vscode.window.showTextDocument(main);
30    await waitFor(() => hasType("localValue: 42"));
31    await waitFor(async () => (await hover()).length === 1);
32    const [local] = await definitions("localValue");
33    assert.equal(local.targetUri.toString(), vscode.Uri.joinPath(folder, "dep file.ts").toString());
34    const dep = await vscode.workspace.openTextDocument(local.targetUri);
35    assert.equal(dep.getText(local.targetSelectionRange), "value");
36
37    editor.selection = new vscode.Selection(at(main, "localValue", false), at(main, "localValue", false));
38    await vscode.commands.executeCommand("editor.action.revealDefinition");
39    await waitFor(() => vscode.window.activeTextEditor?.document.uri.toString() === dep.uri.toString());
40    assert.equal(vscode.window.activeTextEditor.selection.active.character, local.targetSelectionRange.start.character);
41    console.log("PASS actual editor F12 navigation: original local file and exact selection");
42
43    const [alias] = await definitions("answer");
44    assert.equal(alias.targetUri.toString(), vscode.Uri.joinPath(folder, "data.d.ts").toString());
45    const declaration = await vscode.workspace.openTextDocument(alias.targetUri);
46    assert.equal(declaration.getText(alias.targetSelectionRange), "answer");
47    await vscode.window.showTextDocument(declaration);
48    console.log("PASS live avril.conf alias resolves to the original declaration");
49
50    editor = await vscode.window.showTextDocument(dep);
51    const edited = '\r\n// 😀 unsaved\r\nexport const value = "edited" as const;\r\n';
52    await editor.edit((builder) => builder.replace(new vscode.Range(dep.positionAt(0), dep.positionAt(dep.getText().length)), edited));
53    await waitFor(() => hasType('localValue: "edited"'));
54    const [changed] = await definitions("localValue");
55    assert.equal(changed.targetSelectionRange.start.line, 2);
56    assert.equal(dep.getText(changed.targetSelectionRange), "value");
57    await waitFor(() => vscode.languages.getDiagnostics(main.uri).some((d) => d.code === 2322));
58    console.log("PASS unsaved dependency edits update editor hover, definition ranges and diagnostics");
59    await vscode.commands.executeCommand("workbench.action.revertAndCloseActiveEditor");
60    await waitFor(() => hasType("localValue: 42"));
61
62    editor = await vscode.window.showTextDocument(main);
63    const full = main.getText();
64    const completions = (position) => vscode.commands.executeCommand("vscode.executeCompletionItemProvider", main.uri, position);
65    const label = (item) => item.label?.label ?? item.label;
66    await editor.edit((builder) => builder.replace(
67      new vscode.Range(main.positionAt(0), main.positionAt(full.length)),
68      full.replace("process.args;", "process.ar;"),
69    ));
70    const partial = main.positionAt(main.getText().indexOf("process.ar;") + "process.ar".length);
71    await waitFor(async () => (await completions(partial))?.items.some((item) => label(item) === "args"));
72    const suggestion = (await completions(partial)).items.find((item) => label(item) === "args");
73    const edit = suggestion.textEdit;
74    const candidate = suggestion.range ?? edit?.range ?? edit?.replace;
75    const replacementRange = candidate?.replace ?? candidate?.replacing ?? candidate;
76    assert.ok(replacementRange, "Completion has no applicable range");
77    const range = replacementRange instanceof vscode.Range
78      ? replacementRange
79      : Array.isArray(replacementRange)
80        ? new vscode.Range(
81          new vscode.Position(...replacementRange[0]),
82          new vscode.Position(...replacementRange[1]),
83        )
84        : new vscode.Range(
85          new vscode.Position(replacementRange.start.line, replacementRange.start.character),
86          new vscode.Position(replacementRange.end.line, replacementRange.end.character),
87        );
88    const replacement = suggestion.insertText?.value ?? suggestion.insertText ?? edit?.newText ?? "args";
89    await editor.edit((builder) => builder.replace(range, replacement));
90    assert.equal(main.getText(), full);
91    await vscode.commands.executeCommand("undo");
92    await vscode.commands.executeCommand("undo");
93    assert.equal(main.getText(), full);
94    assert.equal(main.isDirty, false);
95    console.log("PASS applying an avril completion replaces the partial word without saving");
96
97    const referencesSource = "function localReferences(value: number): number { return value; }\nlocalReferences(1);";
98    await editor.edit((builder) => builder.replace(
99      new vscode.Range(main.positionAt(0), main.positionAt(full.length)),
100      referencesSource,
101    ));
102    await vscode.commands.executeCommand("vscode.executeCodeLensProvider", main.uri);
103    const codeLenses = await vscode.commands.executeCommand("vscode.executeCodeLensProvider", main.uri, 1);
104    const codeLens = codeLenses.find((lens) =>
105      lens.command?.command === "avril.showReferences" && main.getText(lens.range).includes("localReferences"));
106    assert.ok(codeLens, `Expected a resolved references code lens: ${JSON.stringify(codeLenses)}`);
107    const [lensUri, lensPosition, locations] = codeLens.command.arguments;
108    assert.equal(lensUri, main.uri.toString());
109    assert.ok(Number.isInteger(lensPosition.line) && Number.isInteger(lensPosition.character));
110    assert.ok(Array.isArray(locations) && locations.length === 1, JSON.stringify(locations));
111    await vscode.commands.executeCommand(codeLens.command.command, ...codeLens.command.arguments);
112    console.log("PASS resolved references code lens invokes editor.action.showReferences with editor-native arguments");
113    await vscode.commands.executeCommand("closeReferenceSearch");
114    editor = await vscode.window.showTextDocument(main);
115    await vscode.commands.executeCommand("undo");
116    assert.equal(main.getText(), full);
117    assert.equal(main.isDirty, false);
118
119    await editor.edit((builder) => builder.replace(
120      new vscode.Range(main.positionAt(0), main.positionAt(full.length)),
121      full + "\nfunction takes(value: number): number { return value; }\ntakes(1);",
122    ));
123    const signaturePosition = main.positionAt(main.getText().lastIndexOf("takes(1)") + "takes(1".length);
124    let lastSignatureHelp;
125    try {
126      await waitFor(async () => {
127        lastSignatureHelp = await vscode.commands.executeCommand("vscode.executeSignatureHelpProvider", main.uri, signaturePosition);
128        return lastSignatureHelp?.signatures.some((signature) => signature.label.includes("takes(value: number): number"));
129      });
130    } catch (error) {
131      throw new Error(`${error.message}; last signature help: ${JSON.stringify(lastSignatureHelp)}`);
132    }
133    const renamed = await vscode.commands.executeCommand(
134      "vscode.executeDocumentRenameProvider", main.uri, at(main, "localValue", false), "renamedValue",
135    );
136    const renameEntries = renamed?.entries?.() || [];
137    const renameChanges = renamed?.changes ? Object.values(renamed.changes) : [];
138    assert.ok(
139      [...renameEntries.map(([, edits]) => edits), ...renameChanges].some((edits) => edits.length > 0),
140      "Native rename returned no text edits",
141    );
142    await vscode.commands.executeCommand("undo");
143    assert.equal(main.getText(), full);
144    assert.equal(main.isDirty, false);
145    console.log("PASS native signature help and rename providers operate on unsaved documents");
146
147    const [builtin] = await definitions("args");
148    const builtinDocument = await vscode.workspace.openTextDocument(builtin.targetUri);
149    assert.equal(builtin.targetUri.scheme, "avril-builtin");
150    assert.equal(vscode.workspace.fs.isWritableFileSystem("avril-builtin"), undefined);
151    assert.equal(builtinDocument.getText(builtin.targetSelectionRange), "args");
152    console.log("PASS builtin navigation selects the real avril declaration");
153
154    editor = await vscode.window.showTextDocument(main);
155    const inlineSource = "export const value: number = 42; // ?#% 😀";
156    const inlineUrl = `data:text/typescript,${encodeURIComponent(inlineSource)}`;
157    await editor.edit((builder) => builder.replace(
158      new vscode.Range(main.positionAt(0), main.positionAt(full.length)),
159      full + `\nimport {value as inlineValue} from ${JSON.stringify(inlineUrl)};\ninlineValue;`,
160    ));
161    let inlineHover;
162    try {
163      await waitFor(async () => {
164        inlineHover = await vscode.commands.executeCommand("vscode.executeHoverProvider", main.uri, at(main, "inlineValue"));
165        return inlineHover.some((h) => h.contents.some((c) => c.value.includes("(alias) const value: number")));
166      });
167    } catch (error) {
168      throw new Error(`${error.message}; last inline hover: ${JSON.stringify(inlineHover?.flatMap((h) => h.contents.map((c) => c.value)))}`);
169    }
170    const [inline] = await definitions("inlineValue");
171    assert.ok(inline.targetUri.toString().startsWith("avril-builtin:/data/"));
172    const inlineDocument = await vscode.workspace.openTextDocument(inline.targetUri);
173    assert.equal(inlineDocument.getText(), inlineSource);
174    assert.equal(inlineDocument.getText(inline.targetSelectionRange), "value");
175    assert.equal(inlineDocument.languageId, "typescript");
176    await vscode.window.showTextDocument(inlineDocument);
177    editor = await vscode.window.showTextDocument(main);
178    await vscode.commands.executeCommand("undo");
179    assert.equal(main.getText(), full);
180    assert.equal(main.isDirty, false);
181    console.log("PASS data URL definition opens the decoded, read-only TypeScript source");
182
183    await vscode.commands.executeCommand("avril.stopLanguageServer");
184    await vscode.window.showTextDocument(main);
185    const hasBuiltinError = () => vscode.languages.getDiagnostics(main.uri).some((d) =>
186      d.source === "ts" && (d.code === 2307 || d.code?.value === 2307));
187    await waitFor(hasBuiltinError);
188    await vscode.commands.executeCommand("avril.startLanguageServer");
189    await waitFor(async () => (await hover()).length === 1 && await hasType("localValue: 42") && !hasBuiltinError());
190    console.log("PASS automatic startup and reversible built-in TypeScript suppression");
191    console.log("AVRIL_EDITOR_TESTS_PASSED");
192  } finally {
193    await vscode.commands.executeCommand("avril.stopLanguageServer");
194  }
195}
196module.exports = { run };