add El LSP skeleton — language server, VSCode extension, syntax highlighting

Implements a Language Server Protocol server for El files over stdin/stdout
JSON-RPC. Provides completions (builtins + keywords + document functions),
hover documentation, go-to-definition, and full document sync.

Also adds a VSCode extension that launches the binary as a child process and
a TextMate grammar for .el syntax highlighting.

NOTE: el-lsp.el calls __read_n(n: Int) -> String, a seed primitive not yet
in el_runtime.c. Build.sh documents the required C implementation; the seed
agent must add it before the binary will link.
This commit is contained in:
Will Anderson
2026-05-03 15:52:10 -05:00
parent 9d0e1f64d4
commit cefff5b891
6 changed files with 1245 additions and 0 deletions
+97
View File
@@ -0,0 +1,97 @@
// tools/lsp/vscode-extension/extension.js — El Language VSCode extension
//
// Launches el-lsp as a child process (stdin/stdout Language Server Protocol)
// and connects VSCode's language client to it.
//
// Prerequisites:
// - el-lsp binary built at ../dist/el-lsp (run tools/lsp/build.sh first)
// - npm install (installs vscode-languageclient)
//
// 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
//
// To package as a .vsix:
// npm install -g @vscode/vsce
// cd tools/lsp/vscode-extension && vsce package
'use strict';
const path = require('path');
const { workspace, window, ExtensionContext } = require('vscode');
const {
LanguageClient,
TransportKind,
} = require('vscode-languageclient/node');
let client;
/**
* activate — called when a .el file is first opened.
* Resolves the el-lsp binary path and starts the language client.
*
* @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.
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.
const serverOptions = {
run: {
command: serverPath,
transport: TransportKind.stdio,
},
debug: {
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.
},
};
// ClientOptions: handle all files with language id "el".
const clientOptions = {
documentSelector: [
{ scheme: 'file', language: 'el' },
{ scheme: 'untitled', language: 'el' },
],
synchronize: {
// Re-send full document on save (matches textDocumentSync: 1).
fileEvents: workspace.createFileSystemWatcher('**/*.el'),
},
// Output channel for LSP messages (visible in Output → El Language Server).
outputChannelName: 'El Language Server',
};
client = new LanguageClient(
'el-language-server',
'El Language Server',
serverOptions,
clientOptions
);
// Start the client (and the server as its child process).
client.start();
// Notify user on activation.
window.showInformationMessage('El Language Server started.');
}
/**
* deactivate — called when the extension is unloaded.
* Stops the language client (sends LSP shutdown + exit).
*/
function deactivate() {
if (!client) {
return undefined;
}
return client.stop();
}
module.exports = { activate, deactivate };
@@ -0,0 +1,26 @@
{
"comments": {
"lineComment": "//"
},
"brackets": [
["{", "}"],
["[", "]"],
["(", ")"]
],
"autoClosingPairs": [
{ "open": "{", "close": "}" },
{ "open": "[", "close": "]" },
{ "open": "(", "close": ")" },
{ "open": "\"", "close": "\"", "notIn": ["string"] }
],
"surroundingPairs": [
["{", "}"],
["[", "]"],
["(", ")"],
["\"", "\""]
],
"indentationRules": {
"increaseIndentPattern": "\\{[^}]*$",
"decreaseIndentPattern": "^\\s*}"
}
}
+55
View File
@@ -0,0 +1,55 @@
{
"name": "el-language",
"displayName": "El Language",
"description": "El language support — syntax highlighting, completions, hover, go-to-definition",
"version": "0.1.0",
"publisher": "neuron-technologies",
"license": "MIT",
"engines": {
"vscode": "^1.75.0"
},
"categories": [
"Programming Languages"
],
"keywords": [
"el",
"el-lang",
"language-server"
],
"activationEvents": [
"onLanguage:el"
],
"contributes": {
"languages": [
{
"id": "el",
"aliases": [
"El",
"el"
],
"extensions": [
".el"
],
"configuration": "./language-configuration.json"
}
],
"grammars": [
{
"language": "el",
"scopeName": "source.el",
"path": "./syntaxes/el.tmGrammar.json"
}
]
},
"main": "./extension.js",
"dependencies": {
"vscode-languageclient": "^8.1.0"
},
"devDependencies": {
"@types/vscode": "^1.75.0"
},
"scripts": {
"vscode:prepublish": "echo 'no build step required'",
"package": "vsce package"
}
}
@@ -0,0 +1,133 @@
{
"$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
"name": "El",
"scopeName": "source.el",
"fileTypes": ["el"],
"patterns": [
{ "include": "#comments" },
{ "include": "#strings" },
{ "include": "#function-definition" },
{ "include": "#keywords" },
{ "include": "#types" },
{ "include": "#constants" },
{ "include": "#numbers" },
{ "include": "#operators" },
{ "include": "#function-call" },
{ "include": "#identifiers" }
],
"repository": {
"comments": {
"name": "comment.line.double-slash.el",
"match": "//.*$"
},
"strings": {
"name": "string.quoted.double.el",
"begin": "\"",
"end": "\"",
"patterns": [
{
"name": "constant.character.escape.el",
"match": "\\\\[nrt\\\\\"']"
}
]
},
"function-definition": {
"comment": "Highlight 'fn name(' — the name gets entity.name.function scope",
"match": "\\bfn\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\(",
"captures": {
"0": { "name": "meta.function.el" },
"1": { "name": "entity.name.function.el" }
}
},
"keywords": {
"patterns": [
{
"comment": "Control-flow keywords",
"name": "keyword.control.el",
"match": "\\b(if|else|while|for|in|return|match)\\b"
},
{
"comment": "Declaration keywords",
"name": "keyword.declaration.el",
"match": "\\b(fn|let|type|enum|import|from|as)\\b"
},
{
"comment": "Reserved future keywords",
"name": "keyword.other.el",
"match": "\\b(cgi|vessel|activate|where|sealed|with|test|seed|assert|protocol|impl|retry|times|fallback|reason|parallel|trace|requires|deploy|to|via|target|manager|engine|accessor)\\b"
}
]
},
"types": {
"comment": "Built-in El types",
"name": "support.type.el",
"match": "\\b(String|Int|Float|Bool|Void|Any|Map|List)\\b"
},
"constants": {
"patterns": [
{
"name": "constant.language.boolean.el",
"match": "\\b(true|false)\\b"
}
]
},
"numbers": {
"patterns": [
{
"name": "constant.numeric.float.el",
"match": "\\b[0-9]+\\.[0-9]+\\b"
},
{
"name": "constant.numeric.integer.el",
"match": "\\b[0-9]+\\b"
}
]
},
"operators": {
"patterns": [
{
"name": "keyword.operator.comparison.el",
"match": "(==|!=|<=|>=|<|>)"
},
{
"name": "keyword.operator.logical.el",
"match": "(&&|\\|\\||!)"
},
{
"name": "keyword.operator.assignment.el",
"match": "(?<![=!<>])=(?!=)"
},
{
"name": "keyword.operator.arithmetic.el",
"match": "[+\\-*/%]"
},
{
"name": "keyword.operator.arrow.el",
"match": "->"
}
]
},
"function-call": {
"comment": "Highlight function calls: name( — name gets entity.name.function scope",
"match": "\\b([a-zA-Z_][a-zA-Z0-9_]*)\\s*(?=\\()",
"captures": {
"1": { "name": "entity.name.function.call.el" }
}
},
"identifiers": {
"comment": "General identifiers — lower priority than other rules",
"name": "variable.other.el",
"match": "\\b[a-zA-Z_][a-zA-Z0-9_]*\\b"
}
}
}