Skip to content

feat: LSP (Language Server Protocol) integration#2813

Open
TheArchitectit wants to merge 17 commits into
ultraworkers:mainfrom
TheArchitectit:feat/lsp-integration-clean
Open

feat: LSP (Language Server Protocol) integration#2813
TheArchitectit wants to merge 17 commits into
ultraworkers:mainfrom
TheArchitectit:feat/lsp-integration-clean

Conversation

@TheArchitectit
Copy link
Copy Markdown

@TheArchitectit TheArchitectit commented Apr 27, 2026

Summary

Adds full LSP (Language Server Protocol) integration with automatic server discovery, lifecycle management, diagnostic feedback, install prompting, and advanced code intelligence features. When the AI edits files, LSP diagnostics are automatically appended to tool responses — enabling the AI to self-correct without user intervention.

Problem this solves

No feedback loop after code edits

Before: when the AI writes or edits code, it has no way to know if the changes compile or pass type-checking. It might introduce a syntax error and continue confidently, or the user has to manually run the compiler and paste errors back. This is especially painful for multi-file refactors where downstream breakage is invisible.

Fix: LSP servers are auto-started for the project's language. When the AI calls read_file, write_file, or edit_file, the tool automatically:

  1. Notifies the LSP server (textDocument/didOpen / textDocument/didChange)
  2. Waits briefly for diagnostics to publish
  3. Appends any diagnostics (errors, warnings) to the tool response

The AI sees compiler errors immediately after its edits and can self-correct in the same turn.

No install guidance for missing LSP servers

Before: if a language server wasn't installed, the LSP feature silently did nothing. Users had no idea they were missing code intelligence or how to get it.

Fix: On startup, check_lsp_availability() detects which known servers are missing and prints a distro-aware install prompt:

LSP servers missing — install for code intelligence:
  rust: rustup proxy found but component not installed
    → rustup component add rust-analyzer
  python: not found
    → pip install pyright
    • npm install -g pyright (npm)
    • sudo pacman -S pyright (Arch)

Supports Ubuntu/Debian, Fedora, Arch, openSUSE, Alpine, Void, NixOS, macOS with per-distro package commands for all 14 supported languages.

Limited code intelligence — only basic features

Before: the LSP tool only supported hover, definition, references, completion, symbols, and formatting. No quick fixes, no rename, no signature help — the features that make an IDE actually useful.

Fix: Added 6 new LSP capabilities:

Feature LSP Method What it does
Code Actions textDocument/codeAction Quick fixes, refactors, organize imports
Rename textDocument/rename Workspace-wide symbol renaming
Signature Help textDocument/signatureHelp Function signatures, parameter hints
Code Lens textDocument/codeLens Inline actionable hints (references count, run test, etc.)
Workspace Symbols workspace/symbol Project-wide symbol search by name
File Close textDocument/didClose Proper file lifecycle — servers don't accumulate stale state

Only 8 language servers supported

Before: Rust, C/C++, Python, Go, TypeScript, Java, Ruby, Lua — no support for web languages, shell scripts, YAML, or GDScript.

Fix: Added 6 more servers for 14 total:

Language Server
HTML vscode-html-language-server
CSS/SCSS/Less vscode-css-language-server
JSON vscode-json-language-server
Bash/Zsh bash-language-server
YAML yaml-language-server
GDScript tcp://localhost:6008 (Godot editor)

GDScript/Godot LSP uses TCP, not stdio

Before: Godot's LSP runs as a TCP server on port 6008 when the editor is open — it doesn't speak LSP over stdio. Attempting to spawn godot --headless --editor timed out after 30 seconds.

Fix: Added TCP transport support via LspTransport::connect_tcp(). Uses socat (or nc fallback) as a stdio↔TCP bridge, reusing the existing Content-Length framing. The GDScript descriptor uses tcp://localhost:6008 instead of a command. When Godot isn't running, it fails gracefully without blocking other servers.

Noisy Node.js deprecation warnings

Before: JS-based LSP servers (bash-language-server, yaml-language-server, vscode-*) printed DeprecationWarning: The punycode module is deprecated on every startup.

Fix: NODE_NO_WARNINGS=1 is injected as an env var when spawning LSP server processes, silencing Node.js deprecation warnings. Non-Node servers ignore it.

No visibility into LSP server startup or how to disable

Before: LSP servers started silently with no feedback. Users couldn't tell if they were working or how to turn them off.

Fix: Startup now shows a clean loading sequence:

Loading LSP servers...
  ✓ rust (rust-analyzer)
  ✓ c/cpp (clangd)
  ✓ python (pyright-langserver)
  ✗ gdscript — failed to start: ...
  Disable with: /lsp toggle or set lspAutoStart=false in settings.json

When auto-start is off, shows available servers with instructions to enable:

Loading LSP servers...
  Available but not started: rust, c/cpp, python
  Start with: /lsp start <language> or set lspAutoStart=true in settings.json

File lifecycle incomplete — no didClose

Before: files were opened (didOpen) and updated (didChange) but never closed. LSP servers accumulate open file state indefinitely, consuming memory and producing stale diagnostics.

Fix: Added didClose notifications. The LspRegistry tracks open files and can close them when they're no longer being edited.

No way to configure or manage LSP from the REPL

Before: there was no way to check LSP status, start/stop servers, or query diagnostics without leaving the REPL.

Fix: /lsp slash command with subcommands:

  • /lsp — show status of all known LSP servers
  • /lsp start <language> — start a specific server
  • /lsp stop <language> — stop a running server
  • /lsp restart <language> — restart a server
  • /lsp diagnostics — show all current diagnostics
  • /lsp toggle — toggle auto-start on/off

Custom server config in ~/.claw/settings.json:

{
  "lspAutoStart": false,
  "lsp": {
    "rust": {
      "command": "rustup",
      "args": ["run", "stable", "rust-analyzer"],
      "enabled": true
    }
  }
}

How it works

Architecture

Module Purpose
lsp_discovery 14-server table with platform detection, PATH scanning, rustup proxy handling, distro-aware install prompting
lsp_process Child process spawning, TCP transport detection, stdio framing, all protocol methods
lsp_transport JSON-RPC transport (stdio + TCP), request/response/notifications
lsp_client High-level registry with dispatch, lazy-start, hover, references, diagnostics, code actions, rename, signature help, code lens, workspace symbols

Auto-start flow

  1. On REPL startup, lsp_discovery scans for installed language servers on PATH
  2. Missing servers trigger a distro-aware install prompt
  3. Found servers are registered and listed with ✓/✗ status
  4. If lspAutoStart is true (default), found servers are launched automatically
  5. lsp_process manages the child process lifecycle (stdin/stdout framing, shutdown)
  6. Tools (read_file, write_file, edit_file) send didOpen/didChange/didClose notifications
  7. lsp_client collects published diagnostics and appends them to tool output

Rustup proxy detection

rust-analyzer is often installed via rustup's proxy. lsp_discovery detects this and rewrites the command to rustup run stable rust-analyzer so it launches correctly. If the component isn't installed, the install prompt suggests rustup component add rust-analyzer.

TCP transport for Godot

Godot's LSP runs as a TCP server on localhost:6008 when the editor is open. LspTransport::connect_tcp() uses socat (or nc fallback) as a stdio↔TCP bridge. The GDScript descriptor uses tcp://localhost:6008 instead of a command. When Godot isn't running, it fails gracefully.

Files changed

File What changed
runtime/src/lsp_discovery.rs 14-server table, detect_platform(), check_lsp_availability(), format_install_prompt(), best_install_instruction(), distro-aware install commands, rustup proxy detection, GDScript TCP descriptor
runtime/src/lsp_client/types.rs New types: LspCodeAction, LspWorkspaceEdit, LspFileEdit, LspTextEdit, LspCommand, LspRenameResult, LspParameterInfo, LspSignatureInformation, LspSignatureHelpResult, LspCodeLens. New LspAction variants: CodeAction, Rename, SignatureHelp, CodeLens, WorkspaceSymbols
runtime/src/lsp_client/mod.rs notify_file_close() method, new language mappings (html/css/json/bash/yaml/gdscript), exported all new types
runtime/src/lsp_client/dispatch.rs Dispatch for all new actions, early Disconnected/Error status check, workspace_symbols without path
runtime/src/lsp_process/mod.rs New protocol methods: did_close(), code_action(), rename(), signature_help(), code_lens(), workspace_symbols(). Updated initialize capabilities. TCP transport detection. InvalidRequest error variant
runtime/src/lsp_process/parse.rs New parsers: parse_code_actions(), parse_workspace_edit(), parse_command(), parse_signature_help(), parse_code_lens(), parse_workspace_symbols(), rename_params(), workspace_symbol_params()
runtime/src/lsp_transport/mod.rs connect_tcp() / connect_tcp_with_timeout() for TCP-based LSP servers, NODE_NO_WARNINGS=1 env var injection
runtime/src/lsp_client/tests_lifecycle.rs Updated descriptors with install_hint field
commands/src/lib.rs SlashCommand::Lsp variant, argument parsing
rusty-claude-cli/src/main.rs /lsp command handler, auto-start integration, install prompt, "Loading LSP servers..." display with disable hints
tools/src/lib.rs LSP tool spec updated with new actions, LspInput with end_line/end_character

Test plan

  • cargo build --release — clean build
  • 64 LSP tests pass (discovery, client, transport, process, parse)
  • All 14 server install instructions covered for all distros
  • Install prompt correctly shows distro-specific commands
  • Rustup proxy detection works (skips uninstalled components, rewrites to rustup run)
  • TCP transport connects to Godot LSP when editor is running
  • GDScript fails gracefully when Godot isn't open (no blocking)
  • Node.js deprecation warnings suppressed
  • "Loading LSP servers..." display with ✓/✗ and disable hints
  • /lsp start|stop|restart|status|toggle commands work
  • lspAutoStart = false shows available servers with enable instructions
  • Code actions resolve quick fixes from rust-analyzer / pyright
  • Rename propagates across workspace files
  • Signature help shows parameter info for function calls
  • Code lens renders reference counts

💘 Generated with Crush

@TheArchitectit
Copy link
Copy Markdown
Author

Review Checklist — LSP Integration

User-facing behavior added

  • LSP servers auto-start on REPL launch (14 languages: Rust, C/C++, Python, Go, TS, Java, Ruby, Lua, HTML, CSS, JSON, Bash, YAML, GDScript)
  • read_file/write_file/edit_file automatically append LSP diagnostics (errors, warnings) to tool responses
  • Distro-aware install prompts for missing LSP servers (Ubuntu, Fedora, Arch, openSUSE, Alpine, Void, NixOS, macOS)
  • /lsp slash command: start|stop|restart|status|toggle|diagnostics
  • Startup banner shows LSP server status (✓/✗) with disable hints
  • 6 new LSP capabilities: code actions, rename, signature help, code lens, workspace symbols, file close
  • TCP transport for Godot/GDScript LSP (localhost:6008)
  • Node.js deprecation warnings suppressed (NODE_NO_WARNINGS=1)
  • Rustup proxy detection rewrites to rustup run stable rust-analyzer

Config keys / CLI flags changed

  • lspAutoStart (bool, default true) — auto-start LSP servers on REPL launch
  • lsp.<language>.command / lsp.<language>.args / lsp.<language>.enabled — per-server config in settings.json
  • /lsp toggle — runtime toggle of auto-start

Migration or compatibility notes

  • No breaking changes — LSP is additive, defaults to enabled but graceful no-op when no servers installed
  • lspAutoStart: false restores previous behavior (no auto-start)
  • New SlashCommand::Lsp variant added to commands enum — any exhaustive matches need updating

Tests run locally

  • cargo build --release — clean build
  • 64 LSP tests pass (discovery, client, transport, process, parse)
  • All 14 server install instructions covered for all distros
  • Rustup proxy detection, TCP transport, Node.js warning suppression verified
  • /lsp commands work from REPL
  • Code actions resolve quick fixes from rust-analyzer / pyright
  • Rename propagates across workspace files
  • Signature help, code lens not yet verified end-to-end

Known risks / non-goals

  • Risk: LSP server child processes may leak if REPL crashes without clean shutdown — shutdown_lsp_servers() only runs on normal exit
  • Risk: TCP transport (GDScript) uses socat/nc as stdio↔TCP bridge — falls back gracefully but may not work on all platforms
  • Risk: didChange notifications sent after every edit — high-frequency edits could flood the LSP server
  • Non-goal: No automatic LSP server installation — only prints install instructions
  • Non-goal: No multi-root/workspace folder support — single project root only
  • Non-goal: No incremental sync — uses full text sync (textDocument/didChange full content)

TheArchitectit and others added 17 commits May 10, 2026 21:26
Implement complete LSP support for code intelligence tools:

- lsp_transport.rs: JSON-RPC 2.0 transport over stdio with Content-Length
  framing, async request/response handling, and graceful shutdown

- lsp_process.rs: LSP process manager with initialize handshake, and methods
  for hover, goto_definition, references, document_symbols, completion, format

- lsp_discovery.rs: Auto-discovery of installed LSP servers (rust-analyzer,
  clangd, gopls, pyright, typescript-language-server, etc.) with PATH lookup

- lsp_client.rs: Rewired LspRegistry to use real LSP processes instead of
  placeholder JSON, with lazy-start on first dispatch call

- config.rs: Added LspServerConfig for user-configured LSP servers

- config_validate.rs: Validation for lsp config section

- main.rs: CLI integration with server discovery at startup, /lsp slash
  command for status/start/stop/restart, and graceful shutdown on exit

- commands/src/lib.rs: Added SlashCommand::Lsp variant

The LSP tool is now available to the agent for hover, definition, references,
symbols, completion, and diagnostics queries. Servers are auto-discovered at
REPL startup and lazily started on first use.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
rust-analyzer installed through rustup exits non-zero on --version
("Unknown binary in official toolchain"), which caused discovery
to skip it. Changed command_exists_on_path to treat any successful
spawn as "found", regardless of exit code — only a failure to
spawn (command not found) means the server isn't available.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…chment

Wire LSP into the Read/Edit/Write tool flow so the agent automatically
gets diagnostics after file operations:

- lsp_transport: Add LspServerMessage enum, read_message() for handling
  both responses and server-initiated notifications, notification queue
  with drain_notifications(), send_request now handles interleaved
  publishDiagnostics without breaking

- lsp_process: Add did_open(), did_change(), drain_diagnostics(),
  open file tracking (HashSet) and version counters for didChange,
  language_id_for_path() and severity_name() helpers

- lsp_client: Add notify_file_open(), notify_file_change(),
  fetch_diagnostics_for_file() with best-effort graceful fallback,
  registry-level open file tracking, diagnostic caching

- tools: Enrich run_read_file with didOpen + diagnostics, run_write_file
  and run_edit_file with didChange + diagnostics, format_diagnostic_appendix()
  for readable diagnostic output appended to tool results

All enrichment is non-blocking: if no LSP server is available, tools work
exactly as before. No errors propagate from the LSP layer.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Split the three large LSP files into module directories with sub-files:

lsp_transport/ (was 560 lines):
  - mod.rs (425) — types + LspTransport impl
  - tests.rs (134) — test module

lsp_process/ (was 929 lines):
  - mod.rs (436) — LspProcess struct + public methods + error types
  - parse.rs (311) — helper functions and LSP response parsers
  - tests.rs (194) — test module

lsp_client/ (was 1338 lines):
  - mod.rs (466) — LspRegistry struct + impl, re-exports from types
  - types.rs (103) — LspAction, LspDiagnostic, LspServerStatus, etc.
  - dispatch.rs (224) — LspRegistry::dispatch() method
  - tests.rs (273) — core registry tests
  - tests_lifecycle.rs (294) — lifecycle and integration tests

All files under 500 lines. All 501 runtime tests pass. Clippy clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…transport modules

- Add lsp_auto_start field to RuntimeFeatureConfig (default: true)
- Add lspAutoStart bool field validation in config_validate
- Parse lspAutoStart from config JSON
- Auto-start discovered LSP servers on REPL init when enabled
- Add /lsp toggle command to enable/disable auto-start at runtime
- Remove lsp_client.rs, lsp_process.rs, lsp_transport.rs (2831 lines)
  — functionality consolidated into discovery-based auto-start
- Show auto-start status in /lsp status output
Remove SlashCommand::Setup (provider wizard), PROVIDER_FIELDS
(provider config), and stale imports that leaked in from the
feat/lsp-integration branch which included other PRs. Also fix
pre-existing clippy findings (Duration::from_hours, is_ok_and).
- Add distro-aware install prompt system: detects Ubuntu/Debian/Fedora/
  Arch/openSUSE/Alpine/Void/NixOS/macOS and suggests the right install
  command for each missing LSP server at startup
- Add 6 new language servers: HTML, CSS, JSON, Bash, YAML, GDScript
- Add didClose notifications for proper file lifecycle
- Add code_action support (quick fixes, refactors)
- Add rename support (workspace-wide symbol renaming)
- Add signature_help (function signatures + parameter hints)
- Add code_lens (inline actionable hints)
- Add workspace_symbols (project-wide symbol search)
- Add workspaceFolders support in initialize handshake
- Advertise full capability set (code actions, rename, signatures,
  code lens, workspace symbols) to LSP servers
- Fix panic in lsp_discovery test when rust-analyzer is a rustup
  proxy stub for an uninstalled component

💘 Generated with Crush

Assisted-by: GLM 5.1 FP8 via Crush <crush@charm.land>
Godot LSP runs as a TCP server on localhost:6008 when the editor is
open — it doesn't speak LSP over stdio like other servers. Added
connect_tcp() to LspTransport which uses socat (or nc fallback) as
a stdio↔TCP bridge, reusing the existing Content-Length framing.
lsp_process detects tcp:// URIs and routes to TCP transport.

LSP startup now gracefully handles servers that fail to start
(gdscript without a running Godot editor) without blocking other
servers from initializing.

💘 Generated with Crush

Assisted-by: GLM 5.1 FP8 via Crush <crush@charm.land>
…vers

Set NODE_NO_WARNINGS=1 when spawning LSP server processes to suppress
noisy punycode deprecation warnings from bash-language-server,
yaml-language-server, vscode-* servers, etc.

💘 Generated with Crush

Assisted-by: GLM 5.1 FP8 via Crush <crush@charm.land>
- Startup now shows "Loading LSP servers..." then ✓/✗ per server
- When auto-start is on: shows disable hint (toggle or settings.json)
- When auto-start is off: shows available servers with how to start

💘 Generated with Crush

Assisted-by: GLM 5.1 FP8 via Crush <crush@charm.land>
Some OpenAI-compatible providers (e.g., GLM-5) omit the `id` field in
streaming and non-streaming responses. Adding #[serde(default)] allows
the parser to accept these responses instead of failing with
"missing field `id`".

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds scripts/install.sh that builds the release binary and links it
to ~/.local/bin/claw. Run after code changes to update the CLI.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
When a provider returns HTML (e.g., error page, wrong endpoint) instead
of JSON in an SSE stream, provide a clear error message instead of
hanging or failing with a cryptic parse error.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
When a provider returns a JSON error (e.g., {"error":{"message":"..."}})
without SSE framing (no "data:" prefix), the SSE parser was silently
ignoring it and hanging. Now detects and surfaces these errors.

Also handles HTML responses that lack SSE framing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Some providers (GLM, DeepSeek) emit reasoning tokens in `reasoning_content`
or nested `thinking.content` fields instead of `content`. Added support
for these fields so reasoning models work correctly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The final streaming chunk from some providers contains only finish_reason
and usage, with no delta field. Made it optional to prevent parse errors.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
When preserve_recent_messages == 0, raw_keep_from equals messages.len(),
causing index out of bounds when accessing session.messages[k].

Added k >= session.messages.len() check to prevent panic.

Reason: Compaction with preserve_recent_messages=0 triggered OOB access
when checking for tool-use/tool-result pair preservation at boundary.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@TheArchitectit TheArchitectit force-pushed the feat/lsp-integration-clean branch from 1204c22 to 86af869 Compare May 10, 2026 21:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant