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
elccompiler binary atdist/platform/elc(built from the repo root) cc(clang or gcc),libcurl,pthreads
Build
# 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
sudo cp tools/lsp/dist/el-lsp /usr/local/bin/el-lsp
VSCode Extension
Development install (recommended)
- Build the binary first (see above).
- Install the npm dependency:
cd tools/lsp/vscode-extension npm install - Open
tools/lsp/vscode-extension/in VSCode. - Press F5 — this launches the Extension Development Host.
- Open any
.elfile in the dev host window.
Package as .vsix
npm install -g @vscode/vsce
cd tools/lsp/vscode-extension
vsce package
# Produces: el-language-1.0.0.vsix
Install the .vsix:
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:
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:
au BufRead,BufNewFile *.el set filetype=el
Wire protocol
El LSP communicates over stdin/stdout using standard LSP framing:
Content-Length: <N>\r\n
\r\n
<N bytes of UTF-8 JSON>
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:
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.