Files
el/tools/lsp/README.md

199 lines
5.4 KiB
Markdown

# 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: <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`:
```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`.