Sextant - A Common Lisp Language Server Protocol (LSP) implementation
Find a file
2026-04-19 10:43:58 +01:00
docs Update DAP design: firm decisions on architecture 2026-02-19 17:50:57 +03:00
src Add ANSI-portable source file indexer 2026-04-19 10:43:35 +01:00
.gitignore Add diagnostics/linting via SBCL compile-file 2026-04-09 12:58:12 +01:00
Makefile fix: Change asdf:load-system to ql:quickload so dependencies get pulled in during make 2026-04-07 13:01:30 -04:00
README.org Add diagnostics/linting via SBCL compile-file 2026-04-09 12:58:12 +01:00
sextant.asd Add ANSI-portable source file indexer 2026-04-19 10:43:35 +01:00
test-diagnostics.lisp Add diagnostics/linting via SBCL compile-file 2026-04-09 12:58:12 +01:00
test.lisp Add full LSP feature set: 22 methods implemented 2026-02-19 16:32:46 +03:00
TODO.md fix: Ensure publish-diagnostics sends empty JSON array 2026-04-14 13:58:59 +01:00

Sextant - A Common Lisp Language Server

Sextant

A Language Server Protocol (LSP) implementation for Common Lisp, written in Common Lisp.

Sextant runs as an SBCL process that speaks LSP over stdio. It queries its own running Lisp image for symbol information, giving you a full-featured editing experience in any LSP-capable editor.

Features

Core

  • Hover documentation - Symbol type, arglist, docstring (textDocument/hover)
  • Completions - Symbol name completion across all packages (textDocument/completion)
  • Completion resolve - Lazy-load documentation per completion item (completionItem/resolve)
  • Go to definition - Jump to source via SBCL introspection (textDocument/definition)
  • Find references - Who calls, binds, references, macroexpands (textDocument/references)
  • Signature help - Function arglist display (textDocument/signatureHelp)
  • Diagnostics / Linting - Compile-time warnings, unused variables, type errors, undefined functions (textDocument/publishDiagnostics)

Navigation & Structure

  • Document symbols - Outline of all def* forms (textDocument/documentSymbol)
  • Workspace symbols - Search symbols across all packages (workspace/symbol)
  • Call hierarchy - Incoming calls via sb-introspect:who-calls (callHierarchy/incomingCalls)
  • Folding ranges - Fold top-level forms (textDocument/foldingRange)
  • Selection range - Expand/shrink selection by s-expression (textDocument/selectionRange)

Editing

  • Rename - Rename symbol across current file (textDocument/rename)
  • Formatting - S-expression-aware indentation (textDocument/formatting)
  • Code actions - Add package prefix, export symbol, insert defpackage (textDocument/codeAction)
  • Linked editing range - Edit all occurrences simultaneously (textDocument/linkedEditingRange)

Display

  • Document highlight - Highlight all occurrences of symbol under cursor (textDocument/documentHighlight)
  • Semantic tokens - Rich highlighting: functions, macros, special forms, variables, keywords, classes (textDocument/semanticTokens/full)
  • Inlay hints - Show parameter names at function call sites (textDocument/inlayHint)
  • Code lens - Reference counts above definitions (textDocument/codeLens)

Other

  • Incremental document sync - Efficient partial updates (change: 2)
  • Zero external deps at runtime - Queries the live SBCL image directly
  • No Swank/Slynk required - Self-contained LSP server

Requirements

Building

cd sextant
make

This produces a sextant executable (~16MB compressed).

Emacs Setup (eglot)

;; Add to your init.el or config
(with-eval-after-load 'eglot
  (add-to-list 'eglot-server-programs
               '(lisp-mode . ("/path/to/sextant"))))

;; Auto-start on lisp files (optional)
(add-hook 'lisp-mode-hook 'eglot-ensure)

Or with lsp-mode:

(with-eval-after-load 'lsp-mode
  (lsp-register-client
   (make-lsp-client
    :new-connection (lsp-stdio-connection '("/path/to/sextant"))
    :major-modes '(lisp-mode)
    :server-id 'sextant)))

(add-hook 'lisp-mode-hook #'lsp)

Neovim Setup (LazyVim)

Add to ~/.config/nvim/lua/plugins/sextant.lua:

return {
  {
    "neovim/nvim-lspconfig",
    opts = function(_, opts)
      local configs = require("lspconfig.configs")
      if not configs.sextant then
        local util = require("lspconfig.util")
        configs.sextant = {
          default_config = {
            cmd = { "/path/to/sextant" },
            filetypes = { "lisp" },
            root_dir = util.root_pattern(".git", "*.asd", "*.asdf") or util.path.dirname,
            settings = {},
          },
        }
      end
      require("lspconfig").sextant.setup({})
    end,
  },
}

To enable inlay hints in Neovim:

vim.lsp.inlay_hint.enable(true)

Helix Setup

Add to ~/.config/helix/languages.toml:

[[language]]
name = "lisp"
language-servers = ["sextant"]

[language-server.sextant]
command = "/path/to/sextant"

Architecture

sextant/
  sextant.asd            ; ASDF system definition
  Makefile               ; Build script
  src/
    package.lisp         ; Package definition
    json.lisp            ; Minimal JSON reader/writer (no deps)
    transport.lisp       ; LSP JSON-RPC over stdio
    document.lisp        ; Open file tracking, s-expression utilities
    lisp-introspection.lisp  ; Query running SBCL for docs/completions/defs/refs
    diagnostics.lisp     ; Linting via compile-file + SBCL condition capture
    handlers.lisp        ; LSP method dispatch (all 22 methods)
    server.lisp          ; Main message loop
    main.lisp            ; Entry point

How It Works

Unlike most language servers that do static analysis, Sextant queries a live Common Lisp image. When you hover over format, it calls (documentation 'format 'function) and (sb-introspect:function-lambda-list 'format) in its own SBCL process.

This means:

  • All of CL's 978 standard symbols are available immediately
  • Quicklisp packages can be loaded into the server for their docs too
  • Macros, reader macros, and dynamic features Just Work

For references and call hierarchy, it uses sb-introspect:who-calls, sb-introspect:who-binds, sb-introspect:who-references, and sb-introspect:who-macroexpands.

Diagnostics / Linting

Sextant compiles your buffer using compile-file and captures SBCL's compiler conditions as LSP diagnostics. These appear as inline warnings and errors in your editor.

What it catches:

  • Unused variables - "The variable X is defined but never used"
  • Undefined functions - "undefined function: FOO"
  • Undefined variables - "undefined variable: BAR"
  • Wrong argument count - "called with 3 arguments, but wants exactly 1"
  • Type mismatches - "Constant \"hello\" conflicts with its asserted type NUMBER"
  • Unbalanced parentheses - Reader pre-check catches these before compilation
  • Reader syntax errors - Bad reader macros, malformed strings, etc.

Diagnostics run automatically when you open, edit, or save a file. Edits are debounced (0.5s) so the compiler is not invoked on every keystroke.

Source positions are extracted from SBCL's internal compiler context (sb-c::*compiler-error-context*), with fallback to symbol-name search in the buffer text. This gives accurate positions for most warnings.

Using Sextant

Once connected, Sextant works in the background. Here are the features and how to use them in Emacs (eglot) and other editors:

Hover documentation

Move your cursor over any symbol. Sextant shows its type, arglist, and docstring.

  • Emacs (eglot): M-x eldoc (automatic in the echo area), or C-h . for a dedicated buffer
  • Neovim: K in normal mode
  • Helix: Space k

Completions

Start typing a symbol name (at least 2 characters). Sextant offers completions from all loaded packages.

  • Emacs (eglot): C-M-i or M-TAB (completion-at-point)
  • Neovim: Automatic via nvim-cmp or similar
  • Helix: Automatic

Go to definition

Jump to the source of any function, macro, or generic function.

  • Emacs (eglot): M-. (xref-find-definitions), M-, to jump back
  • Neovim: gd
  • Helix: gd

Signature help

See the arglist of the enclosing function as you type.

  • Emacs (eglot): Automatic in the echo area when inside a function call
  • Neovim: Automatic via LSP
  • Helix: Automatic

Diagnostics

View compiler warnings and errors inline. Sextant underlines problematic code and shows fringe markers.

  • Emacs (eglot/flymake): M-x flymake-show-buffer-diagnostics to list all, M-x flymake-goto-next-error (C-c ! n) and M-x flymake-goto-prev-error (C-c ! p) to navigate
  • Neovim: ]d / [d to navigate, Space e to list
  • Helix: ]d / [d to navigate

Debug Logging

Set SEXTANT_LOG to control the log file location:

SEXTANT_LOG=/tmp/sextant.log sextant

Default: /tmp/sextant.log

License

MIT