char/avril

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

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

Charlotte Somexperiment: use native typescript lsp supportc7a9569

main
11.8 KiB207 linesraw
1const vscode = require("vscode");
2const path = require("node:path");
3const { LanguageClient, State } = require("vscode-languageclient/node");
4
5let session;
6let tsApi;
7let paused = false;
8let pending = Promise.resolve();
9
10function enqueue(action) {
11  const result = pending.then(action);
12  pending = result.catch(() => {});
13  return result;
14}
15
16function ownsDocument(uri, folder) {
17  return uri.scheme !== "file" || vscode.workspace.getWorkspaceFolder(uri)?.uri.toString() === folder.uri.toString();
18}
19
20function configureTypeScript() {
21  tsApi?.configurePlugin("avril-typescript", {
22    workspaceRoots: (vscode.workspace.workspaceFolders || []).filter((folder) => folder.uri.scheme === "file").map((folder) => folder.uri.fsPath),
23    liveRoots: session?.client.state === State.Running ? [session.folder.uri.fsPath] : [],
24  });
25}
26
27async function stop() {
28  const previous = session;
29  session = undefined;
30  try { if (previous?.client.needsStop()) await previous.client.stop(); }
31  finally {
32    previous?.state.dispose();
33    previous?.files.dispose();
34    configureTypeScript();
35  }
36}
37
38async function start(automatic = false) {
39  if (!vscode.workspace.isTrusted) throw new Error("The language server requires a trusted workspace.");
40  if (session?.client.state === State.Stopped) await stop();
41  if (session) return;
42  const folder = vscode.workspace.workspaceFolders?.[0];
43  if (!folder || folder.uri.scheme !== "file") throw new Error("Open a local workspace.");
44  const config = vscode.workspace.getConfiguration("avril.lsp", folder.uri);
45  const backend = config.get("backend");
46  if (!backend || !path.isAbsolute(backend)) throw new Error("Set avril.lsp.backend to the absolute native tsc path.");
47  const extension = vscode.extensions.getExtension("vscode.typescript-language-features");
48  if (extension && !tsApi) {
49    tsApi = (await extension.activate()).getAPI(0);
50    if (!tsApi) throw new Error("Cannot configure VS Code's TypeScript plugin API.");
51  }
52  const owns = (uri) => ownsDocument(uri, folder);
53  const args = ["--project=" + folder.uri.fsPath, ...config.get("options", [])];
54  const files = vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(folder, "**/*.{ts,tsx,js,jsx,mts,cts,mjs,cjs,json,conf,lock}"));
55  const client = new LanguageClient("avril", "avril", {
56    command: config.get("executable"),
57    args: ["lsp", "--typescript=" + backend, ...args],
58    options: { cwd: folder.uri.fsPath },
59  }, {
60    workspaceFolder: folder,
61    initializationOptions: { preferences: config.get("preferences", {}) },
62    synchronize: { fileEvents: files },
63    documentSelector: [
64      { scheme: "file", pattern: new vscode.RelativePattern(folder, "**/*.{ts,tsx,js,jsx,mts,cts,mjs,cjs}") },
65      { scheme: "avril-https" },
66      { scheme: "avril-builtin" },
67      { scheme: "avril-http" },
68    ],
69    // A nested workspace folder can belong to a different language service.
70    middleware: {
71      didOpen: (document, next) => owns(document.uri) ? next(document) : undefined,
72      didChange: (event, next) => owns(event.document.uri) ? next(event) : undefined,
73      didClose: (document, next) => owns(document.uri) ? next(document) : undefined,
74      didSave: (document, next) => owns(document.uri) ? next(document) : undefined,
75      provideHover: (document, position, token, next) => owns(document.uri) ? next(document, position, token) : undefined,
76      provideDefinition: (document, position, token, next) => owns(document.uri) ? next(document, position, token) : undefined,
77      provideCompletionItem: (document, position, context, token, next) => owns(document.uri) ? next(document, position, context, token) : undefined,
78      resolveCompletionItem: (item, token, next) => owns(folder.uri) ? next(item, token) : undefined,
79      provideSignatureHelp: (document, position, context, token, next) => owns(document.uri) ? next(document, position, context, token) : undefined,
80      provideReferences: (document, position, options, token, next) => owns(document.uri) ? next(document, position, options, token) : undefined,
81      provideDocumentHighlights: (document, position, token, next) => owns(document.uri) ? next(document, position, token) : undefined,
82      provideRenameEdits: (document, position, newName, token, next) => owns(document.uri) ? next(document, position, newName, token) : undefined,
83      prepareRename: (document, position, token, next) => owns(document.uri) ? next(document, position, token) : undefined,
84      provideDocumentSymbols: (document, token, next) => owns(document.uri) ? next(document, token) : undefined,
85      provideWorkspaceSymbols: (query, token, next) => owns(folder.uri) ? next(query, token) : undefined,
86      provideFoldingRanges: (document, context, token, next) => owns(document.uri) ? next(document, context, token) : undefined,
87      provideSelectionRanges: (document, positions, token, next) => owns(document.uri) ? next(document, positions, token) : undefined,
88      provideDocumentFormattingEdits: (document, options, token, next) => owns(document.uri) ? next(document, options, token) : undefined,
89      provideDocumentRangeFormattingEdits: (document, range, options, token, next) => owns(document.uri) ? next(document, range, options, token) : undefined,
90      provideDocumentSemanticTokens: (document, token, next) => owns(document.uri) ? next(document, token) : undefined,
91      provideDocumentSemanticTokensEdits: (document, previousResultId, token, next) => owns(document.uri) ? next(document, previousResultId, token) : undefined,
92      provideDocumentRangeSemanticTokens: (document, range, token, next) => owns(document.uri) ? next(document, range, token) : undefined,
93      provideInlayHints: (document, range, token, next) => owns(document.uri) ? next(document, range, token) : undefined,
94      prepareTypeHierarchy: (document, position, token, next) => owns(document.uri) ? next(document, position, token) : undefined,
95      prepareCallHierarchy: (document, position, token, next) => owns(document.uri) ? next(document, position, token) : undefined,
96      provideCallHierarchyIncomingCalls: (item, token, next) => owns(item.uri) ? next(item, token) : undefined,
97      provideCallHierarchyOutgoingCalls: (item, token, next) => owns(item.uri) ? next(item, token) : undefined,
98      provideTypeHierarchySupertypes: (item, token, next) => owns(item.uri) ? next(item, token) : undefined,
99      provideTypeHierarchySubtypes: (item, token, next) => owns(item.uri) ? next(item, token) : undefined,
100      provideLinkedEditingRange: (document, position, token, next) => owns(document.uri) ? next(document, position, token) : undefined,
101      provideImplementation: (document, position, token, next) => owns(document.uri) ? next(document, position, token) : undefined,
102      provideTypeDefinition: (document, position, token, next) => owns(document.uri) ? next(document, position, token) : undefined,
103      provideCodeActions: (document, range, context, token, next) => owns(document.uri) ? next(document, range, context, token) : undefined,
104      resolveCodeAction: (item, token, next) => owns(folder.uri) ? next(item, token) : undefined,
105      provideCodeLenses: (document, token, next) => {
106        if (!owns(document.uri)) return undefined;
107        return Promise.resolve(next(document, token)).then((lenses) => {
108          // The language client's converter recognizes its CodeLens subclass.
109          for (const lens of lenses || []) lens.data = { ...lens.data, avrilDocumentUri: document.uri.toString() };
110          return lenses;
111        });
112      },
113      resolveCodeLens: (codeLens, token, next) => {
114        const uri = codeLens.data?.avrilDocumentUri;
115        if (typeof uri !== "string" || !owns(vscode.Uri.parse(uri))) return undefined;
116        return Promise.resolve(next(codeLens, token)).then((resolved) => {
117          if (!resolved?.command) return resolved;
118          if (resolved.command.command !== "editor.action.showReferences") return undefined;
119          resolved.command.command = "avril.showReferences";
120          resolved.data = { ...resolved.data, avrilDocumentUri: uri };
121          return resolved;
122        });
123      },
124      handleDiagnostics: (uri, diagnostics, next) => next(uri, owns(uri) ? diagnostics : []),
125    },
126  });
127  session = { client, files, folder, automatic, state: client.onDidChangeState(configureTypeScript) };
128  try { await client.start(); }
129  catch (error) { await stop(); throw error; }
130}
131
132async function reconcile() {
133  const folder = vscode.workspace.workspaceFolders?.[0];
134  if (session && session.folder.uri.toString() !== folder?.uri.toString()) await stop();
135  configureTypeScript();
136  if (!vscode.workspace.isTrusted || !folder || folder.uri.scheme !== "file") return;
137  const config = vscode.workspace.getConfiguration("avril.lsp", folder.uri);
138  let hasConfig = false;
139  try {
140    const stat = await vscode.workspace.fs.stat(vscode.Uri.joinPath(folder.uri, "avril.conf"));
141    hasConfig = Boolean(stat.type & vscode.FileType.File);
142  } catch (error) {
143    if (error.code !== "FileNotFound") throw error;
144  }
145  const enabled = config.get("autoStart", true) && hasConfig;
146  if (session?.automatic && !enabled) await stop();
147  if (!session && enabled && !paused && config.get("backend")) await start(true);
148}
149
150function activate(context) {
151  const automatic = () => enqueue(reconcile).catch((error) => vscode.window.showErrorMessage(`avril: ${error.message}`));
152  const roots = vscode.workspace.createFileSystemWatcher("**/avril.conf");
153  const rootChanged = (uri) => {
154    const folder = vscode.workspace.workspaceFolders?.[0];
155    if (folder && uri.toString() === vscode.Uri.joinPath(folder.uri, "avril.conf").toString()) return automatic();
156  };
157  context.subscriptions.push(
158    roots,
159    roots.onDidCreate(rootChanged),
160    roots.onDidDelete(rootChanged),
161    vscode.workspace.onDidGrantWorkspaceTrust(automatic),
162    vscode.workspace.onDidChangeWorkspaceFolders(automatic),
163    vscode.workspace.onDidChangeConfiguration((event) => {
164      if (event.affectsConfiguration("avril.lsp.preferences") && session?.client.state === State.Running) {
165        const preferences = vscode.workspace.getConfiguration("avril.lsp", session.folder.uri).get("preferences", {});
166        session.client.sendNotification("workspace/didChangeConfiguration", { settings: { preferences } })
167          .catch((error) => vscode.window.showErrorMessage(`avril: ${error.message}`));
168      }
169      if (event.affectsConfiguration("avril.lsp")) return automatic();
170    }),
171    ...["avril-https", "avril-http", "avril-builtin"].map((scheme) => vscode.workspace.registerTextDocumentContentProvider(scheme, {
172      provideTextDocumentContent(uri, token) {
173        if (!session) throw new Error("Start the avril language server first.");
174        return session.client.sendRequest("avril/readDocument", { uri: uri.toString() }, token);
175      },
176    })),
177    vscode.commands.registerCommand("avril.showReferences", (uri, position, locations) => {
178      const client = session?.client;
179      if (!client) throw new Error("The avril language server is not running.");
180      if (typeof uri !== "string" || !position || !Array.isArray(locations)) {
181        throw new TypeError("Invalid avril references command arguments.");
182      }
183      const converter = client.protocol2CodeConverter;
184      const targetUri = converter.asUri(uri);
185      if (!ownsDocument(targetUri, session.folder)) {
186        throw new Error("The avril references command contains a document outside its workspace.");
187      }
188      return vscode.commands.executeCommand(
189        "editor.action.showReferences",
190        targetUri,
191        converter.asPosition(position),
192        locations.map((location) => converter.asLocation(location)),
193      );
194    }),
195    vscode.commands.registerCommand("avril.stopLanguageServer", () => enqueue(async () => {
196      paused = true;
197      await stop();
198    })),
199    vscode.commands.registerCommand("avril.startLanguageServer", () => enqueue(async () => {
200      paused = false;
201      await start();
202    })),
203  );
204  return automatic();
205}
206
207module.exports = { activate, deactivate: () => enqueue(stop) };