199 lines
5.4 KiB
Markdown
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`.
|