Sextant - A Common Lisp Language Server Protocol (LSP) implementation
Find a file
Glenn Thompson 8b2612639f dap: make the debugger actually work — fix stack frame lines, breakpoints, and pagination
The DAP layer was structurally complete but had three gaps that prevented
it from functioning. All three are now closed:

- Stack frames report real source lines instead of 0. A new
  code-location-to-line helper uses sb-di:code-location-toplevel-form-offset
  and debug-source-start-positions to map code locations to 1-based line
  numbers by counting newlines in the source file.

- Line and function breakpoints now halt execution. Breakpoints are
  installed via sb-int:encapsulate on the function enclosing the requested
  line (resolved through the source index). The wrapper calls dap-stop
  with :breakpoint reason when the debugger is active. setBreakpoints and
  setFunctionBreakpoints sync encapsulations live; install-pending-breakpoints
  handles breakpoints set before launch/attach. The stopped callback now
  dispatches on reason (:breakpoint, :step, :entry, :exception) and sends
  the correct DAP stopped event.

- stackTrace respects startFrame and levels for pagination per the DAP
  spec, and totalFrames reports the full frame count.

Bump version to 0.2.0.
2026-08-16 09:09:39 +03:00
docs Update DAP design: firm decisions on architecture 2026-02-19 17:50:57 +03:00
src dap: make the debugger actually work — fix stack frame lines, breakpoints, and pagination 2026-08-16 09:09:39 +03: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 dap: make the debugger actually work — fix stack frame lines, breakpoints, and pagination 2026-08-16 09:09:39 +03: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