Feature: Language Server Protocol (LSP) Client Support
Status: Proposal
Author: Ferrite Core Team
Version: 0.1
Roadmap: v0.2.8 — ROADMAP.md
Ferrite is a powerful editor with syntax highlighting, code folding, and a custom text editor — but it currently lacks semantic code intelligence. Developers working in Rust, Python, TypeScript, and other languages must switch to VS Code or a full IDE to get autocomplete, inline diagnostics, and go-to-definition. LSP support would close this gap without sacrificing Ferrite's lightweight, offline-first nature.
- Add opt-in LSP client support for any language that has a language server.
- Keep the experience graceful — Ferrite works perfectly without any language server installed.
- Stay fully offline — no network calls, all processing is local.
- Minimize performance impact on the egui render loop.
- Bundling language servers inside Ferrite's binary.
- Building a language server (we are a client only).
- Full IDE-level debugger integration (out of scope for this version).
| As a… | I want… | So that… |
|---|---|---|
| Rust developer | Inline error squiggles | I can catch compile errors without leaving Ferrite |
| Python developer | Autocomplete suggestions | I can write code faster |
| Developer | Go-to-definition | I can navigate large codebases without a separate IDE |
| Casual user | LSP to be invisible and opt-in | My markdown editing experience is unaffected |
- Auto-detect language server based on file extension (e.g.
.rs→ rust-analyzer,.py→ pylsp). - Spawn language server as a child process via stdio on workspace open.
- Graceful fallback if no language server is installed — show a dismissable notification with install instructions.
- Proper server lifecycle — start, restart on crash, shutdown on workspace close.
- Inline diagnostics — error/warning squiggles under affected text with hover tooltip showing message.
- Autocomplete popup — trigger on typing or Ctrl+Space, navigable with arrow keys.
- Hover documentation — show docs/type info on cursor hover with configurable delay.
- Go to Definition — F12 or Ctrl+Click to jump to symbol definition.
- Incremental document sync — send only changed text ranges to the server, not the full document on every keystroke.
- LSP toggle per workspace — opt-in, off by default.
- Per-language server path override in settings (for users with custom installs or multiple versions).
- Status bar indicator — LSP state: ⬤ rust-analyzer ready / ⟳ indexing... / ✗ not found.
- Diagnostic count in status bar — e.g. "2 errors, 1 warning".
lsp-types = "0.97" # LSP message type definitions
lsp-client = "0.1" # JSON-RPC client transport (or equivalent)
tokio = { version = "1", features = ["full"] } # Async runtime for background threadNote: Verify lsp-client availability and API; alternatives include tower-lsp (client side) or a minimal JSON-RPC + stdio wrapper.
┌─────────────────────────────────────┐
│ Ferrite egui UI │
│ (renders diagnostics, completions) │
└────────────────┬────────────────────┘
│ mpsc channel (results)
│ mpsc channel (requests)
┌────────────────▼────────────────────┐
│ LSP Manager (tokio) │
│ - Spawns/manages server processes │
│ - Sends/receives JSON-RPC │
│ - ctx.request_repaint() on update │
└────────────────┬────────────────────┘
│ stdio pipes
┌────────────────▼────────────────────┐
│ Language Server Process │
│ (rust-analyzer, pylsp, tsserver…) │
└─────────────────────────────────────┘
struct LspState {
diagnostics: HashMap<PathBuf, Vec<Diagnostic>>,
completions: Option<Vec<CompletionItem>>,
hover_info: Option<Hover>,
server_status: HashMap<String, ServerStatus>,
}- Diagnostics — keyed by file path; UI reads and draws squiggles from this map.
- Completions / Hover — optional; cleared or updated when new responses arrive; request IDs used to ignore stale results.
- Server status — per-workspace or per-language; drives status bar text and “not found” notifications.
- No blocking the render loop — LSP communication runs on a dedicated background thread (or tokio task); egui never blocks waiting for a server response.
- Debounced autocomplete — e.g. 150ms after last keystroke to avoid flooding the server.
- Request cancellation — stale completions from a previous cursor position must not be rendered; cancel in-flight requests when cursor or document changes.
- RAM — overhead is in the language server process, not Ferrite itself.
| Phase | Scope |
|---|---|
| Phase 1 | Infrastructure only — server spawn/shutdown, stdio transport, status bar indicator, opt-in toggle, graceful “not found” notification. |
| Phase 2 | Inline diagnostics (highest user value, lowest UI complexity) — squiggles, hover tooltip, incremental sync, diagnostic count in status bar. |
| Phase 3 | Hover documentation and go-to-definition. |
| Phase 4 | Autocomplete popup (highest UI complexity) — debounce, cancellation, arrow-key navigation. |
MVP focus: Phase 1–2. Diagnostics alone are a major quality-of-life win and validate the architecture before investing in the more complex autocomplete UI.
- With LSP toggled on, egui frame rate stays ≥ 60 fps on a mid-range machine.
- Language server detected and started within ~500 ms of workspace open.
- Zero LSP-related crashes when no language server is installed.
- Community feedback: feature request (e.g. GitHub issues) addressed.
- New module:
src/lsp/(orsrc/editor/lsp/) containing:manager.rs— spawn process, stdio, JSON-RPC send/receive, lifecycle.transport.rs— stdio read/write, message framing (Content-Length + JSON).state.rs—LspState, merging diagnostics/completions/hover from manager.detection.rs— map file extension → server command (e.g.rust-analyzer,pylsp).
- UI: Status bar and diagnostic rendering in existing UI modules (e.g.
src/ui/or central panel); squiggles drawn in the editor widget (e.g. in or beside the existing editor painting insrc/editor/). - Settings: New LSP section in
src/config/settings.rs(or equivalent) for per-workspace toggle and per-language server path overrides. - App integration:
AppState(or equivalent) holdsLspState; workspace open/close and tab switch trigger LSP init/shutdown and document sync.
- Main thread: egui UI, editor, user input. Only reads from
LspStateand sends requests via a channel (e.g.mpsc::Sender<LspRequest>). - Background thread/task: Runs tokio (or a dedicated thread) that:
- Receives requests (open doc, didChange, completion, hover, definition, etc.).
- Talks to the language server over stdio.
- Pushes results (diagnostics, completions, hover, definition location) back to main thread via channel; main thread updates
LspStateand callsctx.request_repaint()so the next frame shows new data.
- On open: send
textDocument/didOpenwith full content (and URI, languageId). - On edit: send
textDocument/didChangewith incremental updates (e.g.TextDocumentContentChangeEventwithrange+text) so we don’t send the full buffer on every keystroke. - Map Ferrite’s buffer/rope positions to LSP line/character (0-based) in the payloads.
- Server sends
textDocument/publishDiagnostics(notification). - Manager receives it, parses, and sends parsed diagnostics to main thread.
- Main thread updates
LspState.diagnosticsfor that file and requests repaint. - Editor painting: for each visible line, check diagnostics for that line/range and draw underlines (e.g. red for error, yellow for warning) and store hover text for the tooltip on hover.
- If the server binary is not found (e.g.
which rust-analyzeror configured path fails):- Do not spawn; set
ServerStatus::NotFound. - Show a one-time dismissable notification: “Language server for Rust not found. Install rust-analyzer to enable diagnostics and completions.” with a link or short install instructions.
- Status bar shows “✗ not found” and LSP toggle remains available so the user can disable the prompt.
- Do not spawn; set
- Language Server Protocol Specification
- lsp-types crate — LSP data structures for Rust
- Ferrite Editor Architecture and FerriteEditor for integration points