# El LSP — Language Server for El Full Language Server Protocol implementation for the El programming language. ## Features | Feature | Status | |---------|--------| | Syntax highlighting | Full TextMate grammar | | Completions | Builtins (130+), user-defined fns, keywords, types | | Hover | Signatures + descriptions for all builtins and user fns | | Go-to-definition | Jump to `fn name(` in the open document | | Diagnostics | Unclosed braces/parens/brackets, unterminated strings | | Document sync | Full (re-sends entire document on every change) | ## Building ### Prerequisites - The `elc` compiler binary at `dist/platform/elc` (built from the repo root) - `cc` (clang or gcc), `libcurl`, `pthreads` ### Build ```bash # From the el repo root: ./tools/lsp/build.sh # Or with a custom elc path: ELC=/path/to/elc ./tools/lsp/build.sh ``` Output: `tools/lsp/dist/el-lsp` ### Install system-wide ```bash sudo cp tools/lsp/dist/el-lsp /usr/local/bin/el-lsp ``` ## VSCode Extension ### Development install (recommended) 1. Build the binary first (see above). 2. Install the npm dependency: ```bash cd tools/lsp/vscode-extension npm install ``` 3. Open `tools/lsp/vscode-extension/` in VSCode. 4. Press **F5** — this launches the Extension Development Host. 5. Open any `.el` file in the dev host window. ### Package as .vsix ```bash npm install -g @vscode/vsce cd tools/lsp/vscode-extension vsce package # Produces: el-language-1.0.0.vsix ``` Install the .vsix: ```bash code --install-extension el-language-1.0.0.vsix ``` ### Configuration | Setting | Default | Description | |---------|---------|-------------| | `el.lspPath` | (bundled) | Path to `el-lsp` binary. Empty = use `../dist/el-lsp`. | | `el.trace.server` | `off` | LSP message tracing. Set to `verbose` to see all messages. | ## Neovim / other editors Any editor that supports LSP can use `el-lsp`. Example configuration for Neovim with `nvim-lspconfig`: ```lua local lspconfig = require('lspconfig') local configs = require('lspconfig.configs') if not configs.el then configs.el = { default_config = { cmd = { 'el-lsp' }, filetypes = { 'el' }, root_dir = lspconfig.util.root_pattern('.git', '*.el'), settings = {}, }, } end lspconfig.el.setup({}) ``` Add to `ftdetect/el.vim`: ```vim au BufRead,BufNewFile *.el set filetype=el ``` ## Wire protocol El LSP communicates over stdin/stdout using standard LSP framing: ``` Content-Length: \r\n \r\n ``` The `__read_n(n: Int) -> String` primitive in `el_runtime.c` reads exactly `n` bytes from stdin — needed because `readline()` stops at `\n` and LSP JSON bodies are not newline-terminated. `__print_raw(s: String)` writes with `fwrite + fflush` to preserve embedded `\r\n` in headers. ## Smoke test After building, verify the server responds to `initialize`: ```bash python3 - << 'PY' import subprocess, json def frame(obj): body = json.dumps(obj).encode() return f"Content-Length: {len(body)}\r\n\r\n".encode() + body def read_response(proc): hdr = b"" while not hdr.endswith(b"\r\n\r\n"): hdr += proc.stdout.read(1) cl = int([l for l in hdr.decode().split("\r\n") if "Content-Length" in l][0].split(": ")[1]) return json.loads(proc.stdout.read(cl)) proc = subprocess.Popen(["./dist/el-lsp"], stdin=subprocess.PIPE, stdout=subprocess.PIPE) proc.stdin.write(frame({"jsonrpc":"2.0","id":1,"method":"initialize","params":{"capabilities":{}}})) proc.stdin.flush() r = read_response(proc) print("Server name:", r["result"]["serverInfo"]["name"]) print("Capabilities:", list(r["result"]["capabilities"].keys())) proc.stdin.write(frame({"jsonrpc":"2.0","method":"exit","params":{}})) proc.stdin.flush() proc.wait() print("OK") PY ``` ## Architecture ``` el-lsp.el lsp_read_message() reads header bytes one-by-one, then __read_n(body_len) lsp_write_message() __print_raw("Content-Length: N\r\n\r\n" + json) lsp_dispatch() routes method string to handler lsp_builtin_catalog() [String] of "name|signature|description" entries lsp_extract_fns() scan source for "fn name(" patterns lsp_word_at() expand identifier under cursor lsp_compute_diagnostics() scan for unclosed brackets + unterminated strings ``` ## Files ``` tools/lsp/ el-lsp.el LSP server source (El language) build.sh Build script README.md This file dist/ el-lsp Compiled binary (after build) el-lsp.c Generated C (after build) vscode-extension/ extension.js Extension entry point package.json Extension manifest language-configuration.json Bracket matching, comment config syntaxes/ el.tmGrammar.json TextMate syntax grammar .vscode/ launch.json F5 debug configuration tasks.json Pre-launch npm install task ``` ## Runtime additions Two new primitives added to `el-compiler/runtime/`: ### `__read_n(n: Int) -> String` Reads exactly `n` bytes from stdin using `fread`. Returns `""` on EOF. Required for reading JSON-RPC message bodies. ### `__print_raw(s: String) -> Void` Writes a string to stdout using `fwrite + fflush`. Preserves embedded `\r\n` bytes exactly. Required for LSP Content-Length headers. Both are declared in `el_runtime.h` and registered in the `builtin_arity` table in `el-compiler/src/codegen.el`.