add full El LSP — completions, hover, go-to-def, diagnostics, VSCode extension

This commit is contained in:
Will Anderson
2026-05-03 15:59:42 -05:00
parent cefff5b891
commit 9aa0c49d0c
11 changed files with 1381 additions and 930 deletions
+79 -33
View File
@@ -1,91 +1,137 @@
// tools/lsp/vscode-extension/extension.js — El Language VSCode extension
// tools/lsp/vscode-extension/extension.js
//
// Launches el-lsp as a child process (stdin/stdout Language Server Protocol)
// and connects VSCode's language client to it.
// El Language VSCode extension — Language Client
//
// Prerequisites:
// - el-lsp binary built at ../dist/el-lsp (run tools/lsp/build.sh first)
// - npm install (installs vscode-languageclient)
// Launches the el-lsp binary as a child process and connects VSCode's
// Language Client to it over stdin/stdout (JSON-RPC 2.0 + Content-Length).
//
// Install for development:
// 1. Open the vscode-extension/ folder in VSCode
// 2. Press F5 — Extension Development Host launches
// 3. Open any .el file — the LSP activates
// Capabilities provided by el-lsp:
// - textDocumentSync (full)
// - completionProvider — builtins + user fns + keywords
// - hoverProvider — signatures + descriptions
// - definitionProvider — go-to-def for user-defined fns
// - diagnostics — unclosed braces/parens, unterminated strings
//
// To package as a .vsix:
// Setup (development):
// 1. cd tools/lsp && ./build.sh (builds dist/el-lsp)
// 2. cd vscode-extension && npm install (installs vscode-languageclient)
// 3. Open vscode-extension/ in VSCode
// 4. Press F5 (launches Extension Development Host)
// 5. Open any .el file
//
// Package as .vsix:
// npm install -g @vscode/vsce
// cd tools/lsp/vscode-extension && vsce package
'use strict';
const path = require('path');
const fs = require('fs');
const { workspace, window, ExtensionContext } = require('vscode');
const {
LanguageClient,
TransportKind,
} = require('vscode-languageclient/node');
/** @type {LanguageClient | undefined} */
let client;
/**
* activate — called when a .el file is first opened.
* Resolves the el-lsp binary path and starts the language client.
* activate — entry point called when any .el file is first opened.
*
* @param {ExtensionContext} context
*/
function activate(context) {
// Resolve el-lsp binary path.
// Default: ../dist/el-lsp relative to this extension root.
// Override with the "el.lspPath" workspace setting.
// ── Resolve el-lsp binary ────────────────────────────────────────────
const config = workspace.getConfiguration('el');
const defaultLspPath = path.join(context.extensionPath, '..', 'dist', 'el-lsp');
const serverPath = config.get('lspPath', defaultLspPath);
// ServerOptions: run el-lsp as a child process over stdin/stdout.
// Default path: ../dist/el-lsp relative to the extension root.
// Override with the "el.lspPath" workspace/user setting.
const defaultLspPath = path.join(context.extensionPath, '..', 'dist', 'el-lsp');
const serverPath = config.get('lspPath', defaultLspPath);
if (!fs.existsSync(serverPath)) {
window.showErrorMessage(
`El Language Server binary not found at: ${serverPath}\n` +
`Run tools/lsp/build.sh to build it, then reload VSCode.\n` +
`Or set "el.lspPath" in settings to the correct path.`
);
return;
}
// ── Server options (start el-lsp as child process) ───────────────────
/** @type {import('vscode-languageclient/node').ServerOptions} */
const serverOptions = {
run: {
command: serverPath,
command: serverPath,
transport: TransportKind.stdio,
options: { env: { ...process.env } },
},
debug: {
command: serverPath,
command: serverPath,
transport: TransportKind.stdio,
// In debug mode you could add --verbose or redirect stderr to a
// log file for inspection. For now debug == run.
options: {
env: { ...process.env },
// Redirect el-lsp stderr to VSCode's Output panel in debug mode.
},
},
};
// ClientOptions: handle all files with language id "el".
// ── Client options ───────────────────────────────────────────────────
/** @type {import('vscode-languageclient/node').LanguageClientOptions} */
const clientOptions = {
// Activate for all .el files (file:// and untitled: schemes).
documentSelector: [
{ scheme: 'file', language: 'el' },
{ scheme: 'file', language: 'el' },
{ scheme: 'untitled', language: 'el' },
],
synchronize: {
// Re-send full document on save (matches textDocumentSync: 1).
// Fire fileEvents so the client resends on disk changes outside VSCode.
fileEvents: workspace.createFileSystemWatcher('**/*.el'),
},
// Output channel for LSP messages (visible in Output → El Language Server).
outputChannelName: 'El Language Server',
// Trace LSP messages to the Output panel for debugging.
// Set "el.trace.server": "verbose" in settings to enable.
traceOutputChannel: window.createOutputChannel('El LSP Trace'),
};
// ── Create and start client ──────────────────────────────────────────
client = new LanguageClient(
'el-language-server',
'El Language Server',
serverOptions,
clientOptions
clientOptions,
);
// Start the client (and the server as its child process).
client.start();
// Register the client so it is disposed when the extension deactivates.
context.subscriptions.push(client);
// Notify user on activation.
window.showInformationMessage('El Language Server started.');
client.start().then(() => {
// Show a discrete status bar item while the server is active.
const status = window.createStatusBarItem(1);
status.text = '$(symbol-misc) El LSP';
status.tooltip = 'El Language Server is running';
status.command = 'el.restartServer';
status.show();
context.subscriptions.push(status);
}).catch((err) => {
window.showErrorMessage(`El Language Server failed to start: ${err.message}`);
});
// ── Commands ─────────────────────────────────────────────────────────
context.subscriptions.push(
require('vscode').commands.registerCommand('el.restartServer', () => {
if (client) {
client.stop().then(() => client.start());
window.showInformationMessage('El Language Server restarted.');
}
})
);
}
/**
* deactivate — called when the extension is unloaded.
* Stops the language client (sends LSP shutdown + exit).
* Sends LSP shutdown + exit to the server process.
*/
function deactivate() {
if (!client) {