A Language Server Protocol (LSP) implementation for Gerbil Scheme, providing IDE features through any LSP-compatible editor. Primary integration is with Emacs via Eglot.
| Feature | LSP Method | Description |
|---|---|---|
| Diagnostics | textDocument/publishDiagnostics |
Compilation errors via gxc and parse error detection |
| Completion | textDocument/completion |
Symbols from current file, workspace, and Gerbil keywords |
| Hover | textDocument/hover |
Symbol info with kind, signature, and source location |
| Go to Definition | textDocument/definition |
Jump to symbol definition across workspace |
| Find References | textDocument/references |
Locate all occurrences of a symbol |
| Document Symbols | textDocument/documentSymbol |
Outline view of definitions in current file |
| Workspace Symbols | workspace/symbol |
Search definitions across all indexed files |
| Rename | textDocument/rename |
Rename a symbol across all open documents |
| Formatting | textDocument/formatting |
Format via Gambit's pretty-print |
| Signature Help | textDocument/signatureHelp |
Function signatures while typing arguments |
The analysis engine recognizes these Gerbil definition forms:
def,define,defn,def*-- functions and variablesdefstruct-- struct typesdefclass-- class typesdefmethod-- methodsdefrule,defrules,defsyntax-- macrosdefvalues-- multiple value bindingsdefconst-- constantsdeferror-class-- error types
Import resolution supports:
- Standard library modules:
:std/text/json,:std/sugar, etc. - Relative imports:
./foo,../bar - Package modules:
:mypackage/module - Complex import forms:
only-in,except-in,rename-in,prefix-in
- Gerbil Scheme v0.18+ with
gxpkg - A C compiler (gcc/clang) for linking the executable
- OpenSSL development libraries (typically already installed)
- Emacs 29.1+ with Eglot (for Emacs integration)
git clone https://github.com/ober/gerbil-lsp.git
cd gerbil-lsp
make buildOn macOS with Homebrew, the Makefile automatically locates OpenSSL. If linking fails with library 'ssl' not found, set the library path manually:
LIBRARY_PATH="$(brew --prefix openssl@3)/lib" make buildThe compiled binary is placed at .gerbil/bin/gerbil-lsp.
Copy the binary to your PATH or Gerbil bin directory:
make install # copies to ~/.gerbil/bin/gerbil-lspAdd to your Emacs init file (~/.emacs.d/init.el or ~/.emacs):
;; Point to where you cloned gerbil-lsp
(add-to-list 'load-path "/path/to/gerbil-lsp/emacs")
(require 'gerbil-lsp)(add-hook 'gerbil-mode-hook #'eglot-ensure)If gerbil-lsp is not on your PATH:
(setq gerbil-lsp-server-path "/path/to/gerbil-lsp/.gerbil/bin/gerbil-lsp")(setq gerbil-lsp-log-level "debug") ;; debug | info | warn | errorOpen any .ss file in gerbil-mode and run M-x eglot. The LSP server starts automatically and provides:
- Diagnostics -- errors appear as underlines and in the minibuffer
- Completion -- trigger with
(,:,/,.or invoke viaC-M-i - Hover --
M-x eldocor hover with mouse - Go to Definition --
M-. - Find References --
M-? - Rename --
M-x eglot-rename - Format --
M-x eglot-format-buffer - Document Symbols --
M-x imenu
gerbil-lsp [options]
Options:
--stdio Use stdio transport (default)
--log-level <level> Log level: debug, info, warn, error (default: info)
--version Print version and exit
-h, --help Display help
The server communicates via JSON-RPC 2.0 over stdin/stdout using Content-Length framing (standard LSP transport). All log output goes to stderr to keep the transport channel clean.
lsp/
├── main.ss Entry point, CLI parsing, handler registration
├── server.ss JSON-RPC dispatch loop (read -> dispatch -> respond)
├── transport.ss stdio Content-Length framing
├── jsonrpc.ss JSON-RPC 2.0 message codec
├── types.ss LSP protocol type constructors
├── capabilities.ss Server capability declaration
├── state.ss Global state (documents, symbol index, module cache)
│
├── util/
│ ├── log.ss Logging to stderr
│ └── position.ss Line/column and range utilities
│
├── analysis/
│ ├── document.ss Document text buffer tracking
│ ├── parser.ss S-expression parser with position info
│ ├── symbols.ss Symbol extraction from parsed forms
│ ├── module.ss Module resolution (imports/exports)
│ ├── index.ss Workspace-wide symbol index
│ └── completion-data.ss Completion candidate generation
│
└── handlers/
├── lifecycle.ss initialize, shutdown, exit
├── sync.ss didOpen, didChange, didClose, didSave
├── diagnostics.ss Compile errors via gxc
├── completion.ss textDocument/completion
├── hover.ss textDocument/hover
├── definition.ss textDocument/definition
├── references.ss textDocument/references
├── symbols.ss documentSymbol + workspace/symbol
├── rename.ss textDocument/rename
├── formatting.ss textDocument/formatting
└── signature.ss textDocument/signatureHelp
- Transport reads LSP messages from stdin (Content-Length framing)
- JSON-RPC layer parses the JSON and classifies as request or notification
- Server dispatches to the registered handler by method name
- Handlers use the analysis layer to inspect documents and symbols
- Server serializes the response and writes it back via transport
The server maintains global state in lsp/state.ss:
| State | Type | Description |
|---|---|---|
*documents* |
uri -> document |
Open document text buffers |
*symbol-index* |
uri -> sym-info list |
Extracted symbols per file |
*module-cache* |
module-path -> exports |
Cached module export lists |
*workspace-root* |
string |
Workspace root directory |
Documents are re-analyzed on every change (full text sync). The symbol index is updated incrementally as files are opened and modified.
The server uses these Gerbil standard library modules:
| Module | Purpose |
|---|---|
:std/text/json |
JSON serialization |
:std/format |
String formatting |
:std/sugar |
when-let, with-catch, etc. |
:std/iter |
for, for/collect, for-each |
:std/error |
Exception types |
:std/cli/getopt |
CLI argument parsing |
:std/misc/process |
Spawning gxc for diagnostics |
:std/misc/ports |
File reading utilities |
:std/misc/string |
String utilities |
:std/misc/path |
Path manipulation |
On file open and save, the server runs two levels of checking:
- Parse-level: Attempts to read the file as S-expressions using Gambit's
read. Reports syntax errors with position info. - Compile-level: Runs
gxc -Son the file and parses its error output into structured diagnostics with file, line, column, and message.
Completion candidates come from three sources, filtered by the prefix at the cursor:
- Local symbols -- definitions extracted from the current file
- Workspace symbols -- definitions from all indexed
.ssfiles - Keywords -- 75+ Gerbil special forms and keywords (def, lambda, let, if, cond, match, import, export, etc.)
Trigger characters: (, :, /, .
When hovering over a symbol, the server:
- Identifies the symbol at the cursor position using word-boundary detection
- Searches local file symbols, then workspace-wide definitions
- Returns a markdown code block showing the signature and kind
The formatter reads each top-level S-expression and outputs it through Gambit's pretty-print. This handles indentation and line wrapping but does not preserve comments (a known limitation of read-based formatting).
While primary integration is with Emacs/Eglot, gerbil-lsp implements standard LSP over stdio and should work with any LSP client:
local lspconfig = require('lspconfig')
local configs = require('lspconfig.configs')
configs.gerbil_lsp = {
default_config = {
cmd = { 'gerbil-lsp', '--stdio' },
filetypes = { 'gerbil', 'scheme' },
root_dir = lspconfig.util.root_pattern('gerbil.pkg', '.git'),
},
}
lspconfig.gerbil_lsp.setup{}Create a .vscode/settings.json or use a generic LSP client extension configured with:
{
"command": "gerbil-lsp",
"args": ["--stdio"],
"languages": ["scheme"]
}make clean
make buildRun with verbose logging to see all JSON-RPC messages:
gerbil-lsp --stdio --log-level debug 2>lsp-debug.logSend raw LSP messages via stdin:
INIT='{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"processId":1,"rootUri":"file:///tmp","capabilities":{}}}'
printf "Content-Length: %d\r\n\r\n%s" "${#INIT}" "$INIT" | gerbil-lsp --stdio 2>/dev/nullgerbil-lsp/
├── gerbil.pkg Package definition (package: lsp)
├── build.ss Build script listing all modules
├── Makefile Build/clean/install targets
├── lsp/ All source code (23 modules)
├── emacs/ Emacs integration
└── test/ Test files (placeholder)
- Full document sync only -- the entire document text is sent on each change (no incremental sync yet)
- Formatting strips comments --
pretty-printoperates on S-expressions afterread, which discards comments - No incremental indexing -- workspace symbols are only indexed from open documents, not scanned on startup
- Diagnostics require saved files --
gxccompilation runs on the filesystem copy, not the editor buffer - Rename is limited to open documents -- closed files in the workspace are not updated
MIT