Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Language Server

The flint-lsp crate implements the Flint language server. It reuses the compiler’s frontend crates to provide real-time editor feedback without duplicating language logic.

For user-facing feature documentation and editor setup, see Language Server in the Tools section.

Architecture

The language server is a single-threaded, synchronous event loop that reads JSON-RPC messages from stdin and writes responses to stdout. It does not use an async runtime.

Editor                           flint lsp
  │                                │
  │  ── initialize ──────────────► │
  │  ◄── capabilities ──────────── │
  │  ── initialized ─────────────► │
  │                                │
  │  ── didOpen ─────────────────► │ lex + parse + check
  │  ◄── publishDiagnostics ───── │
  │                                │
  │  ── completion ──────────────► │ index lookup
  │  ◄── completionList ────────── │
  │                                │
  │  ── hover ───────────────────► │ index lookup
  │  ◄── hover content ─────────── │
  │                                │
  │  ── shutdown ────────────────► │
  │  ── exit ────────────────────► │

Crate Dependencies

flint-lsp depends on the compiler’s frontend crates but not on flint-cli:

  • flint-lexer for tokenization
  • flint-parser for CST/AST construction and recovery
  • flint-syntax for AST types, token kinds, spans, and LineIndex
  • flint-check for semantic analysis and the SymbolTable
  • flint-diagnostics for diagnostic types and severity mapping
  • flint-driver for workspace resolution and the canonical formatter
  • flint-manifest for flint.toml loading

Module Layout

ModuleResponsibility
transportContent-Length framed JSON-RPC over stdio
protocolLSP data types (positions, ranges, capabilities, requests)
sessionEvent loop, request dispatch, state management
documentsOpen document store with automatic lex/parse on change
indexWorkspace-wide symbol index built from AST walks
completionContext-aware completions (keywords, imports, locals, members)
diagnosticsSyntax and semantic diagnostic mapping
hoverFunction signatures and doc comments
definitionCross-file go-to-definition
referencesCross-file find-references
renameCross-file rename
signature_helpParameter info at call sites
symbolsDocument and workspace symbol providers
formattingDocument formatting via the flint fmt engine
actionsCode actions (fix hints, add/remove imports)
semantic_tokensToken classification for rich highlighting
inlay_hintsType annotations and parameter name hints
highlightsDocument-level identifier highlighting
foldingFolding ranges for blocks and declarations
locationByte offset to LSP position conversion utilities

Document Model

The server keeps an in-memory store of open documents. Each document holds:

  • The current source text
  • A LineIndex for position conversion
  • The latest LexedModule (tokens and trivia)
  • The latest ParsedModule (CST, AST, recovery status, parse issues)

On every didOpen and didChange, the server re-lexes, re-parses, rebuilds the symbol index, and publishes diagnostics. Incremental document sync is used (the editor sends only the changed range, which the server splices into the existing text before re-parsing).

Diagnostic Pipeline

Diagnostics flow through two lanes:

  1. Syntax lane (immediate): parse issues from the current document are mapped to LSP diagnostics and published right away.

  2. Analysis lane (on change): the server runs the full compiler pipeline in-process on all open documents:

    • flint_check::Checker::check() for semantic errors (unresolved symbols, duplicate definitions, invalid signatures, missing entry points)
    • flint_lower::Lowerer::lower() to produce HIR and FIR
    • flint_lower::TypeChecker::check() for deep type-level errors (mutability violations, ownership and move errors, definite initialization, return-path completeness, exhaustive match, closure capture validity, MCU recursion policy)

    The lowering and type-checking pass only runs when semantic checking succeeds, since it depends on a well-formed checked package.

Both lanes run synchronously after each document change. There is no background thread or debounce timer in the current implementation. Because the compiler is a set of library crates called in-process, every diagnostic the compiler can produce is available in the editor with no translation layer or subprocess overhead.

Symbol Index

The workspace index maintains a per-document index of:

  • Top-level symbols (functions, structs, enums, traits, constants, statics, aliases) with their spans, visibility, rendered signatures, doc comments, parameters, and children (fields, variants, methods)
  • Imports with their paths and bound names
  • Impl method associations (which type each method belongs to)

The index is rebuilt from the AST on every document change. It powers completion, hover, go-to-definition, find-references, rename, document symbols, workspace symbols, semantic tokens, and inlay hints.

Completion Strategy

Completion is context-sensitive. The server checks the cursor position and dispatches to different strategies:

  1. Inside use items: complete module paths from the known package list
  2. After ::: complete enum variants
  3. Inside { } after a type name: complete struct fields
  4. After .: complete struct fields, impl methods, trait methods, and imported module symbols. The receiver’s type is resolved from explicit annotations, construct expressions (Point { ... }), function call return types, or self inside impl blocks.
  5. Default: keywords, top-level symbols, imported names, local bindings (parameters, let bindings, for-loop variables), and public symbols from other documents