const vscode = require("vscode"); const path = require("node:path"); const { LanguageClient, State } = require("vscode-languageclient/node"); let session; let tsApi; let paused = false; let pending = Promise.resolve(); function enqueue(action) { const result = pending.then(action); pending = result.catch(() => {}); return result; } function ownsDocument(uri, folder) { return uri.scheme !== "file" || vscode.workspace.getWorkspaceFolder(uri)?.uri.toString() === folder.uri.toString(); } function configureTypeScript() { tsApi?.configurePlugin("avril-typescript", { workspaceRoots: (vscode.workspace.workspaceFolders || []).filter((folder) => folder.uri.scheme === "file").map((folder) => folder.uri.fsPath), liveRoots: session?.client.state === State.Running ? [session.folder.uri.fsPath] : [], }); } async function stop() { const previous = session; session = undefined; try { if (previous?.client.needsStop()) await previous.client.stop(); } finally { previous?.state.dispose(); previous?.files.dispose(); configureTypeScript(); } } async function start(automatic = false) { if (!vscode.workspace.isTrusted) throw new Error("The language server requires a trusted workspace."); if (session?.client.state === State.Stopped) await stop(); if (session) return; const folder = vscode.workspace.workspaceFolders?.[0]; if (!folder || folder.uri.scheme !== "file") throw new Error("Open a local workspace."); const config = vscode.workspace.getConfiguration("avril.lsp", folder.uri); const backend = config.get("backend"); if (!backend || !path.isAbsolute(backend)) throw new Error("Set avril.lsp.backend to the absolute native tsc path."); const extension = vscode.extensions.getExtension("vscode.typescript-language-features"); if (extension && !tsApi) { tsApi = (await extension.activate()).getAPI(0); if (!tsApi) throw new Error("Cannot configure VS Code's TypeScript plugin API."); } const owns = (uri) => ownsDocument(uri, folder); const args = ["--project=" + folder.uri.fsPath, ...config.get("options", [])]; const files = vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(folder, "**/*.{ts,tsx,js,jsx,mts,cts,mjs,cjs,json,conf,lock}")); const client = new LanguageClient("avril", "avril", { command: config.get("executable"), args: ["lsp", "--typescript=" + backend, ...args], options: { cwd: folder.uri.fsPath }, }, { workspaceFolder: folder, initializationOptions: { preferences: config.get("preferences", {}) }, synchronize: { fileEvents: files }, documentSelector: [ { scheme: "file", pattern: new vscode.RelativePattern(folder, "**/*.{ts,tsx,js,jsx,mts,cts,mjs,cjs}") }, { scheme: "avril-https" }, { scheme: "avril-builtin" }, { scheme: "avril-http" }, ], // A nested workspace folder can belong to a different language service. middleware: { didOpen: (document, next) => owns(document.uri) ? next(document) : undefined, didChange: (event, next) => owns(event.document.uri) ? next(event) : undefined, didClose: (document, next) => owns(document.uri) ? next(document) : undefined, didSave: (document, next) => owns(document.uri) ? next(document) : undefined, provideHover: (document, position, token, next) => owns(document.uri) ? next(document, position, token) : undefined, provideDefinition: (document, position, token, next) => owns(document.uri) ? next(document, position, token) : undefined, provideCompletionItem: (document, position, context, token, next) => owns(document.uri) ? next(document, position, context, token) : undefined, resolveCompletionItem: (item, token, next) => owns(folder.uri) ? next(item, token) : undefined, provideSignatureHelp: (document, position, context, token, next) => owns(document.uri) ? next(document, position, context, token) : undefined, provideReferences: (document, position, options, token, next) => owns(document.uri) ? next(document, position, options, token) : undefined, provideDocumentHighlights: (document, position, token, next) => owns(document.uri) ? next(document, position, token) : undefined, provideRenameEdits: (document, position, newName, token, next) => owns(document.uri) ? next(document, position, newName, token) : undefined, prepareRename: (document, position, token, next) => owns(document.uri) ? next(document, position, token) : undefined, provideDocumentSymbols: (document, token, next) => owns(document.uri) ? next(document, token) : undefined, provideWorkspaceSymbols: (query, token, next) => owns(folder.uri) ? next(query, token) : undefined, provideFoldingRanges: (document, context, token, next) => owns(document.uri) ? next(document, context, token) : undefined, provideSelectionRanges: (document, positions, token, next) => owns(document.uri) ? next(document, positions, token) : undefined, provideDocumentFormattingEdits: (document, options, token, next) => owns(document.uri) ? next(document, options, token) : undefined, provideDocumentRangeFormattingEdits: (document, range, options, token, next) => owns(document.uri) ? next(document, range, options, token) : undefined, provideDocumentSemanticTokens: (document, token, next) => owns(document.uri) ? next(document, token) : undefined, provideDocumentSemanticTokensEdits: (document, previousResultId, token, next) => owns(document.uri) ? next(document, previousResultId, token) : undefined, provideDocumentRangeSemanticTokens: (document, range, token, next) => owns(document.uri) ? next(document, range, token) : undefined, provideInlayHints: (document, range, token, next) => owns(document.uri) ? next(document, range, token) : undefined, prepareTypeHierarchy: (document, position, token, next) => owns(document.uri) ? next(document, position, token) : undefined, prepareCallHierarchy: (document, position, token, next) => owns(document.uri) ? next(document, position, token) : undefined, provideCallHierarchyIncomingCalls: (item, token, next) => owns(item.uri) ? next(item, token) : undefined, provideCallHierarchyOutgoingCalls: (item, token, next) => owns(item.uri) ? next(item, token) : undefined, provideTypeHierarchySupertypes: (item, token, next) => owns(item.uri) ? next(item, token) : undefined, provideTypeHierarchySubtypes: (item, token, next) => owns(item.uri) ? next(item, token) : undefined, provideLinkedEditingRange: (document, position, token, next) => owns(document.uri) ? next(document, position, token) : undefined, provideImplementation: (document, position, token, next) => owns(document.uri) ? next(document, position, token) : undefined, provideTypeDefinition: (document, position, token, next) => owns(document.uri) ? next(document, position, token) : undefined, provideCodeActions: (document, range, context, token, next) => owns(document.uri) ? next(document, range, context, token) : undefined, resolveCodeAction: (item, token, next) => owns(folder.uri) ? next(item, token) : undefined, provideCodeLenses: (document, token, next) => { if (!owns(document.uri)) return undefined; return Promise.resolve(next(document, token)).then((lenses) => { // The language client's converter recognizes its CodeLens subclass. for (const lens of lenses || []) lens.data = { ...lens.data, avrilDocumentUri: document.uri.toString() }; return lenses; }); }, resolveCodeLens: (codeLens, token, next) => { const uri = codeLens.data?.avrilDocumentUri; if (typeof uri !== "string" || !owns(vscode.Uri.parse(uri))) return undefined; return Promise.resolve(next(codeLens, token)).then((resolved) => { if (!resolved?.command) return resolved; if (resolved.command.command !== "editor.action.showReferences") return undefined; resolved.command.command = "avril.showReferences"; resolved.data = { ...resolved.data, avrilDocumentUri: uri }; return resolved; }); }, handleDiagnostics: (uri, diagnostics, next) => next(uri, owns(uri) ? diagnostics : []), }, }); session = { client, files, folder, automatic, state: client.onDidChangeState(configureTypeScript) }; try { await client.start(); } catch (error) { await stop(); throw error; } } async function reconcile() { const folder = vscode.workspace.workspaceFolders?.[0]; if (session && session.folder.uri.toString() !== folder?.uri.toString()) await stop(); configureTypeScript(); if (!vscode.workspace.isTrusted || !folder || folder.uri.scheme !== "file") return; const config = vscode.workspace.getConfiguration("avril.lsp", folder.uri); let hasConfig = false; try { const stat = await vscode.workspace.fs.stat(vscode.Uri.joinPath(folder.uri, "avril.conf")); hasConfig = Boolean(stat.type & vscode.FileType.File); } catch (error) { if (error.code !== "FileNotFound") throw error; } const enabled = config.get("autoStart", true) && hasConfig; if (session?.automatic && !enabled) await stop(); if (!session && enabled && !paused && config.get("backend")) await start(true); } function activate(context) { const automatic = () => enqueue(reconcile).catch((error) => vscode.window.showErrorMessage(`avril: ${error.message}`)); const roots = vscode.workspace.createFileSystemWatcher("**/avril.conf"); const rootChanged = (uri) => { const folder = vscode.workspace.workspaceFolders?.[0]; if (folder && uri.toString() === vscode.Uri.joinPath(folder.uri, "avril.conf").toString()) return automatic(); }; context.subscriptions.push( roots, roots.onDidCreate(rootChanged), roots.onDidDelete(rootChanged), vscode.workspace.onDidGrantWorkspaceTrust(automatic), vscode.workspace.onDidChangeWorkspaceFolders(automatic), vscode.workspace.onDidChangeConfiguration((event) => { if (event.affectsConfiguration("avril.lsp.preferences") && session?.client.state === State.Running) { const preferences = vscode.workspace.getConfiguration("avril.lsp", session.folder.uri).get("preferences", {}); session.client.sendNotification("workspace/didChangeConfiguration", { settings: { preferences } }) .catch((error) => vscode.window.showErrorMessage(`avril: ${error.message}`)); } if (event.affectsConfiguration("avril.lsp")) return automatic(); }), ...["avril-https", "avril-http", "avril-builtin"].map((scheme) => vscode.workspace.registerTextDocumentContentProvider(scheme, { provideTextDocumentContent(uri, token) { if (!session) throw new Error("Start the avril language server first."); return session.client.sendRequest("avril/readDocument", { uri: uri.toString() }, token); }, })), vscode.commands.registerCommand("avril.showReferences", (uri, position, locations) => { const client = session?.client; if (!client) throw new Error("The avril language server is not running."); if (typeof uri !== "string" || !position || !Array.isArray(locations)) { throw new TypeError("Invalid avril references command arguments."); } const converter = client.protocol2CodeConverter; const targetUri = converter.asUri(uri); if (!ownsDocument(targetUri, session.folder)) { throw new Error("The avril references command contains a document outside its workspace."); } return vscode.commands.executeCommand( "editor.action.showReferences", targetUri, converter.asPosition(position), locations.map((location) => converter.asLocation(location)), ); }), vscode.commands.registerCommand("avril.stopLanguageServer", () => enqueue(async () => { paused = true; await stop(); })), vscode.commands.registerCommand("avril.startLanguageServer", () => enqueue(async () => { paused = false; await start(); })), ); return automatic(); } module.exports = { activate, deactivate: () => enqueue(stop) };