add full El LSP — completions, hover, go-to-def, diagnostics, VSCode extension
This commit is contained in:
+31
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Launch Extension (Extension Development Host)",
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"args": [
|
||||
"--extensionDevelopmentPath=${workspaceFolder}"
|
||||
],
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/**/*.js"
|
||||
],
|
||||
"preLaunchTask": "npm: install",
|
||||
"env": {
|
||||
"EL_LSP_LOG": "1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Attach to el-lsp process",
|
||||
"type": "node",
|
||||
"request": "attach",
|
||||
"port": 6009,
|
||||
"restart": true,
|
||||
"timeout": 10000,
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/**/*.js"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "install",
|
||||
"label": "npm: install",
|
||||
"detail": "Install vscode-languageclient dependency",
|
||||
"group": "build",
|
||||
"presentation": {
|
||||
"reveal": "silent"
|
||||
},
|
||||
"problemMatcher": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -1,20 +1,29 @@
|
||||
{
|
||||
"name": "el-language",
|
||||
"displayName": "El Language",
|
||||
"description": "El language support — syntax highlighting, completions, hover, go-to-definition",
|
||||
"version": "0.1.0",
|
||||
"description": "El language support — syntax highlighting, completions, hover, go-to-definition, and diagnostics",
|
||||
"version": "1.0.0",
|
||||
"publisher": "neuron-technologies",
|
||||
"license": "MIT",
|
||||
"icon": "icon.png",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/neuron-technologies/foundation"
|
||||
},
|
||||
"engines": {
|
||||
"vscode": "^1.75.0"
|
||||
},
|
||||
"categories": [
|
||||
"Programming Languages"
|
||||
"Programming Languages",
|
||||
"Linters",
|
||||
"Other"
|
||||
],
|
||||
"keywords": [
|
||||
"el",
|
||||
"el-lang",
|
||||
"language-server"
|
||||
"language-server",
|
||||
"lsp",
|
||||
"neuron"
|
||||
],
|
||||
"activationEvents": [
|
||||
"onLanguage:el"
|
||||
@@ -23,13 +32,8 @@
|
||||
"languages": [
|
||||
{
|
||||
"id": "el",
|
||||
"aliases": [
|
||||
"El",
|
||||
"el"
|
||||
],
|
||||
"extensions": [
|
||||
".el"
|
||||
],
|
||||
"aliases": ["El", "el-lang"],
|
||||
"extensions": [".el", ".elh"],
|
||||
"configuration": "./language-configuration.json"
|
||||
}
|
||||
],
|
||||
@@ -39,6 +43,29 @@
|
||||
"scopeName": "source.el",
|
||||
"path": "./syntaxes/el.tmGrammar.json"
|
||||
}
|
||||
],
|
||||
"configuration": {
|
||||
"type": "object",
|
||||
"title": "El Language",
|
||||
"properties": {
|
||||
"el.lspPath": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "Absolute path to the el-lsp binary. Leave empty to use the binary bundled with the extension (../dist/el-lsp)."
|
||||
},
|
||||
"el.trace.server": {
|
||||
"type": "string",
|
||||
"enum": ["off", "messages", "verbose"],
|
||||
"default": "off",
|
||||
"description": "Trace LSP messages between VSCode and el-lsp (visible in Output → El LSP Trace)."
|
||||
}
|
||||
}
|
||||
},
|
||||
"commands": [
|
||||
{
|
||||
"command": "el.restartServer",
|
||||
"title": "El: Restart Language Server"
|
||||
}
|
||||
]
|
||||
},
|
||||
"main": "./extension.js",
|
||||
@@ -46,10 +73,12 @@
|
||||
"vscode-languageclient": "^8.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/vscode": "^1.75.0"
|
||||
"@types/vscode": "^1.75.0",
|
||||
"@vscode/vsce": "^2.22.0"
|
||||
},
|
||||
"scripts": {
|
||||
"vscode:prepublish": "echo 'no build step required'",
|
||||
"package": "vsce package"
|
||||
"vscode:prepublish": "echo 'no transpile step required'",
|
||||
"package": "vsce package",
|
||||
"install-dev": "npm install && code --install-extension el-language-1.0.0.vsix 2>/dev/null || true"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,11 +7,16 @@
|
||||
{ "include": "#comments" },
|
||||
{ "include": "#strings" },
|
||||
{ "include": "#function-definition" },
|
||||
{ "include": "#keywords" },
|
||||
{ "include": "#builtin-functions" },
|
||||
{ "include": "#keywords-control" },
|
||||
{ "include": "#keywords-declaration" },
|
||||
{ "include": "#keywords-other" },
|
||||
{ "include": "#types" },
|
||||
{ "include": "#type-annotations" },
|
||||
{ "include": "#constants" },
|
||||
{ "include": "#numbers" },
|
||||
{ "include": "#operators" },
|
||||
{ "include": "#range-operator" },
|
||||
{ "include": "#function-call" },
|
||||
{ "include": "#identifiers" }
|
||||
],
|
||||
@@ -29,44 +34,68 @@
|
||||
"patterns": [
|
||||
{
|
||||
"name": "constant.character.escape.el",
|
||||
"match": "\\\\[nrt\\\\\"']"
|
||||
"match": "\\\\[nrt\\\\\"'0]"
|
||||
},
|
||||
{
|
||||
"name": "constant.character.escape.hex.el",
|
||||
"match": "\\\\x[0-9a-fA-F]{2}"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
"function-definition": {
|
||||
"comment": "Highlight 'fn name(' — the name gets entity.name.function scope",
|
||||
"match": "\\bfn\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\(",
|
||||
"comment": "fn keyword + function name — name gets entity.name.function",
|
||||
"match": "\\b(fn)\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*(?=\\()",
|
||||
"captures": {
|
||||
"0": { "name": "meta.function.el" },
|
||||
"1": { "name": "entity.name.function.el" }
|
||||
"1": { "name": "keyword.declaration.function.el" },
|
||||
"2": { "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"
|
||||
}
|
||||
]
|
||||
"builtin-functions": {
|
||||
"comment": "Well-known El builtins get support.function scope for distinct colour",
|
||||
"name": "support.function.builtin.el",
|
||||
"match": "\\b(println|print|readline|str_eq|str_len|str_concat|str_slice|str_contains|str_starts_with|str_ends_with|str_replace|str_to_upper|str_to_lower|str_trim|str_lstrip|str_rstrip|str_index_of|str_last_index_of|str_split|str_split_lines|str_split_n|str_join|str_char_at|str_char_code|str_pad_left|str_pad_right|str_repeat|str_reverse|str_strip_prefix|str_strip_suffix|str_strip_chars|str_count|str_count_lines|str_find_chars|str_upper|str_lower|int_to_str|str_to_int|float_to_str|str_to_float|int_to_float|float_to_int|format_float|decimal_round|bool_to_str|el_abs|el_max|el_min|math_sqrt|math_log|math_ln|math_sin|math_cos|math_pi|el_list_empty|el_list_append|el_list_len|el_list_get|el_list_clone|list_push|list_push_front|list_join|list_range|el_map_get|el_map_set|el_get_field|state_set|state_get|state_del|state_keys|json_get|json_get_string|json_get_int|json_get_float|json_get_bool|json_get_raw|json_set|json_parse|json_stringify|json_array_len|json_array_get|json_array_get_string|fs_read|fs_write|fs_list|fs_exists|fs_mkdir|http_get|http_post|http_post_json|http_get_with_headers|http_post_with_headers|http_serve|http_serve_v2|http_response|url_encode|url_decode|time_now|time_now_utc|sleep_secs|sleep_ms|time_format|time_add|time_diff|now|unix_seconds|unix_millis|uuid_new|uuid_v4|env|args|exit_program|exec_command|exec_capture|sha256_hex|hmac_sha256_hex|base64_encode|base64_decode|base64url_encode|base64url_decode|llm_call|llm_call_system|llm_call_agentic|llm_vision|llm_models|llm_register_tool|engram_node|engram_get_node|engram_strengthen|engram_forget|engram_search|engram_connect|engram_activate|engram_save|engram_load|engram_node_count|engram_edge_count|engram_neighbors|engram_search_json|engram_stats_json|dharma_connect|dharma_send|dharma_activate|dharma_emit|dharma_field|dharma_peers|native_list_empty|native_list_append|native_list_len|native_list_get|native_list_clone|native_string_chars|native_int_to_str)\\b(?=\\s*\\()"
|
||||
},
|
||||
|
||||
"keywords-control": {
|
||||
"name": "keyword.control.el",
|
||||
"match": "\\b(if|else|while|for|in|return|match|break|continue)\\b"
|
||||
},
|
||||
|
||||
"keywords-declaration": {
|
||||
"name": "keyword.declaration.el",
|
||||
"match": "\\b(fn|let|type|enum|import|from|as|extern)\\b"
|
||||
},
|
||||
|
||||
"keywords-other": {
|
||||
"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"
|
||||
"comment": "Built-in El primitive types",
|
||||
"name": "support.type.primitive.el",
|
||||
"match": "\\b(String|Int|Float|Bool|Void|Any|Instant|Duration|Map|List)\\b"
|
||||
},
|
||||
|
||||
"type-annotations": {
|
||||
"comment": "Highlight type annotations after : in let and fn params",
|
||||
"patterns": [
|
||||
{
|
||||
"match": ":\\s*([A-Z][a-zA-Z0-9_<>, ]*)(?=\\s*[,)={\\]]|\\s*->)",
|
||||
"captures": {
|
||||
"1": { "name": "support.type.el" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"match": "->\\s*([A-Z][a-zA-Z0-9_<>, ]*)",
|
||||
"captures": {
|
||||
"0": { "name": "keyword.operator.arrow.el" },
|
||||
"1": { "name": "support.type.return.el" }
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
"constants": {
|
||||
@@ -74,6 +103,10 @@
|
||||
{
|
||||
"name": "constant.language.boolean.el",
|
||||
"match": "\\b(true|false)\\b"
|
||||
},
|
||||
{
|
||||
"name": "constant.language.null.el",
|
||||
"match": "\\bnull\\b"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -82,24 +115,33 @@
|
||||
"patterns": [
|
||||
{
|
||||
"name": "constant.numeric.float.el",
|
||||
"match": "\\b[0-9]+\\.[0-9]+\\b"
|
||||
"match": "-?\\b[0-9]+\\.[0-9]+\\b"
|
||||
},
|
||||
{
|
||||
"name": "constant.numeric.integer.el",
|
||||
"match": "\\b[0-9]+\\b"
|
||||
"match": "-?\\b[0-9]+\\b"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
"range-operator": {
|
||||
"name": "keyword.operator.range.el",
|
||||
"match": "\\.\\."
|
||||
},
|
||||
|
||||
"operators": {
|
||||
"patterns": [
|
||||
{
|
||||
"name": "keyword.operator.comparison.el",
|
||||
"match": "(==|!=|<=|>=|<|>)"
|
||||
"match": "(==|!=|<=|>=)"
|
||||
},
|
||||
{
|
||||
"name": "keyword.operator.comparison.el",
|
||||
"match": "(?<![<>-])([<>])(?![>=])"
|
||||
},
|
||||
{
|
||||
"name": "keyword.operator.logical.el",
|
||||
"match": "(&&|\\|\\||!)"
|
||||
"match": "(&&|\\|\\||!(?!=))"
|
||||
},
|
||||
{
|
||||
"name": "keyword.operator.assignment.el",
|
||||
@@ -117,7 +159,7 @@
|
||||
},
|
||||
|
||||
"function-call": {
|
||||
"comment": "Highlight function calls: name( — name gets entity.name.function scope",
|
||||
"comment": "Function calls: identifier immediately followed by (",
|
||||
"match": "\\b([a-zA-Z_][a-zA-Z0-9_]*)\\s*(?=\\()",
|
||||
"captures": {
|
||||
"1": { "name": "entity.name.function.call.el" }
|
||||
@@ -125,7 +167,7 @@
|
||||
},
|
||||
|
||||
"identifiers": {
|
||||
"comment": "General identifiers — lower priority than other rules",
|
||||
"comment": "Catch-all for remaining identifiers",
|
||||
"name": "variable.other.el",
|
||||
"match": "\\b[a-zA-Z_][a-zA-Z0-9_]*\\b"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user