feat: LSP (Language Server Protocol) integration#2813
Open
TheArchitectit wants to merge 17 commits into
Open
Conversation
Author
Review Checklist — LSP IntegrationUser-facing behavior added
Config keys / CLI flags changed
Migration or compatibility notes
Tests run locally
Known risks / non-goals
|
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>
1204c22 to
86af869
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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, oredit_file, the tool automatically:textDocument/didOpen/textDocument/didChange)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: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:
textDocument/codeActiontextDocument/renametextDocument/signatureHelptextDocument/codeLensworkspace/symboltextDocument/didCloseOnly 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:
vscode-html-language-servervscode-css-language-servervscode-json-language-serverbash-language-serveryaml-language-servertcp://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 --editortimed out after 30 seconds.Fix: Added TCP transport support via
LspTransport::connect_tcp(). Usessocat(orncfallback) as a stdio↔TCP bridge, reusing the existing Content-Length framing. The GDScript descriptor usestcp://localhost:6008instead 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 deprecatedon every startup.Fix:
NODE_NO_WARNINGS=1is 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:
When auto-start is off, shows available servers with instructions to enable:
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
didClosenotifications. TheLspRegistrytracks 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:
/lspslash 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/offCustom server config in
~/.claw/settings.json:{ "lspAutoStart": false, "lsp": { "rust": { "command": "rustup", "args": ["run", "stable", "rust-analyzer"], "enabled": true } } }How it works
Architecture
lsp_discoverylsp_processlsp_transportlsp_clientAuto-start flow
lsp_discoveryscans for installed language servers on PATHlspAutoStartis true (default), found servers are launched automaticallylsp_processmanages the child process lifecycle (stdin/stdout framing, shutdown)read_file,write_file,edit_file) senddidOpen/didChange/didClosenotificationslsp_clientcollects published diagnostics and appends them to tool outputRustup proxy detection
rust-analyzeris often installed via rustup's proxy.lsp_discoverydetects this and rewrites the command torustup run stable rust-analyzerso it launches correctly. If the component isn't installed, the install prompt suggestsrustup 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()usessocat(orncfallback) as a stdio↔TCP bridge. The GDScript descriptor usestcp://localhost:6008instead of a command. When Godot isn't running, it fails gracefully.Files changed
runtime/src/lsp_discovery.rsdetect_platform(),check_lsp_availability(),format_install_prompt(),best_install_instruction(), distro-aware install commands, rustup proxy detection, GDScript TCP descriptorruntime/src/lsp_client/types.rsLspCodeAction,LspWorkspaceEdit,LspFileEdit,LspTextEdit,LspCommand,LspRenameResult,LspParameterInfo,LspSignatureInformation,LspSignatureHelpResult,LspCodeLens. NewLspActionvariants:CodeAction,Rename,SignatureHelp,CodeLens,WorkspaceSymbolsruntime/src/lsp_client/mod.rsnotify_file_close()method, new language mappings (html/css/json/bash/yaml/gdscript), exported all new typesruntime/src/lsp_client/dispatch.rsruntime/src/lsp_process/mod.rsdid_close(),code_action(),rename(),signature_help(),code_lens(),workspace_symbols(). Updatedinitializecapabilities. TCP transport detection.InvalidRequesterror variantruntime/src/lsp_process/parse.rsparse_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.rsconnect_tcp()/connect_tcp_with_timeout()for TCP-based LSP servers,NODE_NO_WARNINGS=1env var injectionruntime/src/lsp_client/tests_lifecycle.rsinstall_hintfieldcommands/src/lib.rsSlashCommand::Lspvariant, argument parsingrusty-claude-cli/src/main.rs/lspcommand handler, auto-start integration, install prompt, "Loading LSP servers..." display with disable hintstools/src/lib.rsLSPtool spec updated with new actions,LspInputwithend_line/end_characterTest plan
cargo build --release— clean buildrustup run)/lsp start|stop|restart|status|togglecommands worklspAutoStart = falseshows available servers with enable instructionsrust-analyzer/pyright💘 Generated with Crush