b1563e4e80
- Updated `waitForDiagnostics` to accept exact document-version matches immediately and otherwise wait for a quiescence window before using the latest publish. - Removed the old unversioned-acceptance option and applied the settle-based wait logic through inline and deferred diagnostics fetch paths. - Added an LSP writethrough regression test ensuring stale unversioned diagnostics are ignored in favor of later fresh publishes.
21 KiB
21 KiB
lsp
Query language servers for diagnostics, navigation, symbols, renames, code actions, capabilities, and raw requests.
Source
- Entry:
packages/coding-agent/src/lsp/index.ts - Model-facing prompt:
packages/coding-agent/src/prompts/tools/lsp.md - Key collaborators:
packages/coding-agent/src/lsp/client.ts— client process lifecycle and JSON-RPCpackages/coding-agent/src/lsp/config.ts— config loading, auto-detect, server selectionpackages/coding-agent/src/lsp/lspmux.ts— optionallspmuxcommand wrappingpackages/coding-agent/src/lsp/edits.ts— applyWorkspaceEditand text editspackages/coding-agent/src/lsp/utils.ts— URI conversion, symbol resolution, formatting, glob expansionpackages/coding-agent/src/lsp/types.ts— tool schema and protocol typespackages/coding-agent/src/lsp/clients/index.ts— custom linter client cache/factorypackages/coding-agent/src/lsp/clients/lsp-linter-client.ts— LSP-backed linter adapterpackages/coding-agent/src/lsp/clients/biome-client.ts— Biome CLI diagnostics/formatting adapterpackages/coding-agent/src/lsp/clients/swiftlint-client.ts— SwiftLint CLI diagnostics adapterpackages/coding-agent/src/tools/index.ts— tool registration andlsp.enabledgatingpackages/coding-agent/src/tools/tool-timeouts.ts— timeout defaults and clampingpackages/coding-agent/src/lsp/defaults.json— built-in server definitions for auto-detect
Inputs
| Field | Type | Required | Description |
|---|---|---|---|
action |
string enum | Yes | One of diagnostics, definition, references, hover, symbols, rename, rename_file, code_actions, type_definition, implementation, status, reload, capabilities, request. |
file |
string | No | File path; for diagnostics also a glob; for workspace forms use "*"; for rename_file this is the source path. |
line |
number | No | 1-indexed line number for position-based actions. Defaults to 1 on the single-file action path. |
symbol |
string | No | Substring used to resolve the column on line. Supports name#N occurrence selectors; N is 1-indexed and defaults to 1. |
query |
string | No | Workspace symbol query, code-action selector/filter, or LSP method name for action=request. |
new_name |
string | No | Required for rename and rename_file. |
apply |
boolean | No | For rename/rename_file, apply unless explicitly false. For code_actions, list unless explicitly true. |
timeout |
number | No | Seconds, clamped by clampTimeout("lsp", ...) to 5..60, default 20. |
payload |
string | No | JSON string for action=request; overrides auto-built params. |
Outputs
- Single-shot
AgentToolResult. contentis always one text block:[{ type: "text", text: string }].detailsisLspToolDetails:action,success, optionalserverName, optional originalrequest.- No streaming updates.
- No artifact URIs or background jobs.
- Many validation failures are returned as ordinary text results with
details.success: false; aborts throwToolAbortErrorinstead.
Flow
packages/coding-agent/src/tools/index.tsregisterslsp: LspTool.createIf; session creation also gates it behindsession.enableLsp !== falseandsettings.get("lsp.enabled").LspTool.execute()inpackages/coding-agent/src/lsp/index.tsclampstimeoutwithclampTimeout("lsp", ...), builds anAbortSignal.timeout(...), and combines it with the caller signal.getConfig()loads and cachesLspConfigper cwd, applies idle-timeout config viasetIdleTimeout(), and reuses the cached config on later calls.- Config loading in
packages/coding-agent/src/lsp/config.tsmergesdefaults.jsonwith JSON/YAML overrides from project, project config dirs, user config dirs, plugin roots, and home; if there are no overrides it auto-detects servers from root markers plus executable discovery. - Server routing uses
getServersForFile()/getServerForFile()fromconfig.ts: extension or basename match, then sort primary servers before linters.index.tsfurther filters custom linter clients out of navigation/refactor paths withgetLspServersForFile()/getLspServerForFile(). getOrCreateClient()inclient.tscreates one process percommand:cwd, optionally wraps supported commands withlspmux, spawns the server, starts the background message reader, sendsinitialize, stores server capabilities, then sendsinitialized.- The message reader in
client.tsparses LSP frames, resolves pending requests, cachespublishDiagnostics, tracks$/progresstokens for project-load completion, answersworkspace/configuration, and appliesworkspace/applyEditrequests throughapplyWorkspaceEdit(). - File-scoped actions call
ensureFileOpen()before requests. Column resolution usesresolveSymbolColumn()fromutils.ts: read the target file, pick first non-whitespace whensymbolis omitted, otherwise find the exact or case-insensitive match on the target line and honor#Noccurrence selectors. - Actions dispatch in
LspTool.execute()through dedicated branches: workspace-only branches (status, somediagnostics, workspacesymbols, workspacereload,capabilities,request) run before the single-file switch; all other single-file actions share one client lookup andswitch(action). - Requests go through
sendRequest()inclient.ts, which allocates an incrementing JSON-RPC id, installs abort and timeout handling, sends$/cancelRequeston abort, and rejects on timeout or process exit. - Actions that return edits either preview with
formatWorkspaceEdit()or apply withapplyWorkspaceEdit()fromedits.ts;rename_filealso performs the filesystem rename and then sendsworkspace/didRenameFiles. - Non-abort failures inside the single-file action block are converted to
LSP error: ...; many precondition failures return explicit text without throwing.
Modes / Variants
Routing and workspace scope
file: "*"is only special fordiagnostics,symbols, andreload.statusignoresfile.capabilitieswith omittedfileor"*"inspects all non-custom LSP servers; with a concrete file it scopes to matching non-custom servers.requestwith omittedfileor"*"chooses the first available non-custom LSP server; with a concrete file it chooses that file's primary non-linter server.rename_filesendsworkspace/willRenameFilesandworkspace/didRenameFilesto every non-custom LSP server fromgetLspServers(config), not just one file-scoped server.- Diagnostics are the only tool action that queries both normal LSP servers and custom linter clients (
BiomeClient,SwiftLintClient, orLspLinterClient).
diagnostics
Inputs
- Required:
file, unless using workspace mode withfile: "*". - Optional:
timeout.
Execution
file: "*":runWorkspaceDiagnostics()detects project type from root markers and runs one subprocess command: Rustcargo check --message-format=short, TypeScriptnpx tsc --noEmit, Gogo build ./..., Pythonpyright.- Concrete file or glob:
resolveDiagnosticTargets()treats non-globs as one target, otherwise expands aBun.Globup toMAX_GLOB_DIAGNOSTIC_TARGETS. - Per file, every matching server runs: custom clients call
lint(file); real LSP servers optionally wait for project load, capturediagnosticsVersion,refreshFile(), thenwaitForDiagnostics()for freshpublishDiagnostics(settles on the latest publish; exact-version match accepted immediately). - Results are deduplicated by range+message and severity-sorted.
Output text
- Single target with no issues:
OK. - Single target with issues:
<summary>:\n<grouped diagnostics>. - Batch/glob target: one section per file, plus an initial truncation warning when the glob exceeds the file cap.
- Workspace mode:
Workspace diagnostics (<detected description>):\n<command output>.
definition
Inputs
- Required:
file. - Optional:
line,symbol,timeout.
Execution
- Sends
textDocument/definitionwith{ textDocument, position }. - Accepts
Location,Location[],LocationLink, orLocationLink[];normalizeLocationResult()convertsLocationLinktotargetSelectionRange ?? targetRange. - Waits for project load before the request.
Output text
No definition foundorFound N definition(s):followed byfile:line:coland one context line above/below each location.
type_definition
Same as definition, but sends textDocument/typeDefinition and reports type definition(s).
implementation
Same as definition, but sends textDocument/implementation and reports implementation(s).
references
Inputs
- Required:
file. - Optional:
line,symbol,timeout.
Execution
- Sends
textDocument/referenceswithincludeDeclaration: true. - For project-aware servers, retries up to
REFERENCES_RETRY_COUNTtimes when the only hit is the queried declaration; between retries it waits for project load and sleepsREFERENCES_RETRY_DELAY_MS. - First
REFERENCE_CONTEXT_LIMITreferences include surrounding context; the rest are location-only.
Output text
No references foundorFound N reference(s):with contextual entries first, then... M additional reference(s) shown without contextwhen truncated.
hover
Inputs
- Required:
file. - Optional:
line,symbol,timeout.
Execution
- Sends
textDocument/hover. extractHoverText()flattens strings, markup content, marked-string objects, or arrays into plain text.
Output text
No hover informationor the extracted hover text.
symbols
Inputs
- Workspace mode:
file: "*"or omitted file on the early workspace branch, plus requiredquery. - Document mode: required
file. - Optional:
timeout.
Execution
- Workspace mode sends
workspace/symbolto every non-custom LSP server, post-filters matches withfilterWorkspaceSymbols(), deduplicates withdedupeWorkspaceSymbols(), then truncates toWORKSPACE_SYMBOL_LIMIT. - Document mode sends
textDocument/documentSymbolto the primary server. If the first item hasselectionRange, it formats hierarchicalDocumentSymbols; otherwise it formats flatSymbolInformations.
Output text
- Workspace mode:
Found N symbol(s) matching "query":plus formattedname @ file:line:col, with an omission line when over the limit. - Document mode:
Symbols in <file>:plus hierarchical or flat symbol lines.
rename
Inputs
- Required:
file,new_name. - Optional:
line,symbol,apply,timeout.
Execution
- Waits for project load, sends
textDocument/rename, receives aWorkspaceEdit. apply !== falseapplies edits immediately withapplyWorkspaceEdit().apply === falserenders a preview withformatWorkspaceEdit().
Output text
Rename returned no edits,Applied rename:plus applied change lines, orRename preview:plus summarized edits.
rename_file
Inputs
- Required:
filesource path,new_namedestination path. - Optional:
apply,timeout.
Execution
- Resolves absolute source and destination, rejects identical paths, missing source, existing destination, empty rename set, or directories with more than
MAX_RENAME_PAIRSfiles. enumerateRenamePairs()returns one{oldUri,newUri}pair for a file or walks every regular file in a directory tree.- Sends
workspace/willRenameFileswith{ files: pairs }to every non-custom LSP server; collects returnedWorkspaceEdits and server notes. - Preview mode (
apply === false) only formats those edits. - Apply mode runs each returned
WorkspaceEdit, renames the source path on disk, sendstextDocument/didClosefor every renamed open file, deletes thoseopenFilesentries, then sendsworkspace/didRenameFiles.
Output text
- Preview:
Rename preview: <file-count label> → <dest>plus per-server edit summaries and optional server notes. - Apply:
Renamed <file-count label> → <dest>plus applied edit summaries, filesystem rename line, and optional server notes.
code_actions
Inputs
- Required:
file. - Optional:
line,symbol,query,apply,timeout.
Execution
- Reads cached diagnostics for the open URI from
client.diagnosticsand sendstextDocument/codeActionfor a zero-width range at the resolved position. - When
apply !== true,queryis passed ascontext.only: [query]; this is a server-side kind filter. - When
apply === true,querybecomes a required client-side selector: either a zero-based numeric index or a case-insensitive substring of the action title. - Applying a
CodeActionusesapplyCodeAction(): optionallycodeAction/resolve, thenapplyWorkspaceEdit(edit), then optionalworkspace/executeCommand. - Applying a bare
Commandonly runsworkspace/executeCommand.
Output text
- List mode:
N code action(s):plusindex: [kind] titlelines. - Apply mode success:
Applied "title":plusWorkspace edit:and/orExecuted command(s):sections. - Apply mode miss:
No code action matches "query". Available actions:. - Apply mode with no edit/command:
Action "title" has no workspace edit or command to apply.
status
Inputs
- None.
Execution
- Reads configured servers from cached
LspConfig, notgetActiveClients(). - Calls
detectLspmux()and appends status text whenlspmuxis installed.
Output text
Active language servers: ...orNo language servers configured for this project, optionally followed bylspmux: active (multiplexing enabled)orlspmux: installed but server not running.
reload
Inputs
- Workspace mode:
file: "*"or omittedfile. - Single-file mode: required
file. - Optional:
timeout.
Execution
- Workspace mode reloads every non-custom LSP server.
- Single-file mode reloads the primary server for that file.
reloadServer()triesrust-analyzer/reloadWorkspace, thenworkspace/didChangeConfigurationwith{ settings: {} }; if neither works it kills the process so the next request cold-starts a new client.
Output text
- One line per server:
Reloaded <server>,Restarted <server>, orFailed to reload <server>: ....
capabilities
Inputs
- Optional:
file,timeout.
Execution
- With a concrete
file, inspects matching non-custom servers for that file. - With omitted
fileor"*", inspects every non-custom configured server. - Starts servers as needed and dumps
client.serverCapabilities ?? {}as pretty JSON.
Output text
- Per server:
<server>:followed by indentedcapabilities: { ... }, or<server>: failed to start (...).
request
Inputs
- Required:
querymethod name. - Optional:
file,line,symbol,payload,timeout.
Execution
- Chooses one non-custom server: file-scoped primary server, otherwise the first configured non-custom server.
- Param building precedence:
- If
payloadis present, parse JSON and use it verbatim. - Else if
fileis concrete andlineis present, build{ textDocument: { uri }, position: { line: line - 1, character } }usingresolveSymbolColumn(). - Else if
fileis concrete, build{ textDocument: { uri } }. - Else use
{}.
- If
- Opens the file first when
fileis concrete.
Output text
- Success:
<server> ← <method>:\n<formatted result>, where non-string results areJSON.stringify(..., null, 2)and nullish values becomenull. - Failure:
LSP error from <server> on <method>: ....
Side Effects
- Filesystem
- Reads config files, target files, and root markers.
renameandcode_actionsmay edit/create/delete/rename files viaapplyWorkspaceEdit().rename_filealways renames the source path on disk in apply mode.- Server-initiated
workspace/applyEditrequests also mutate files throughapplyWorkspaceEdit().
- Network
- None directly; communication is local stdio JSON-RPC to subprocesses.
- Subprocesses / native bindings
- Spawns language servers with
ptree.spawn(). - Workspace diagnostics spawns
cargo,npx,go, orpyright. BiomeClientandSwiftLintClientspawn CLI tools.- Optional
lspmuxdetection spawnslspmux status; supported servers may be wrapped throughlspmux client.
- Spawns language servers with
- Session state (transcript, memory, jobs, checkpoints, registries)
- Caches config per cwd in
configCache. - Caches LSP clients per
command:cwd, withpendingRequests,diagnostics,openFiles,serverCapabilities, and project-load state. - Caches custom linter clients by
serverName:cwd. - Updates client
lastActivity; optional idle-timeout cleanup is driven bysetIdleTimeout().
- Caches config per cwd in
- Background work / cancellation
- Every request has an abortable timeout signal.
- Aborting an in-flight LSP request sends
$/cancelRequest. - Background message readers persist for each live client until process exit/shutdown.
Limits & Caps
- Tool timeout clamp: default
20, min5, max60seconds —TOOL_TIMEOUTS.lspinpackages/coding-agent/src/tools/tool-timeouts.ts. - LSP request default timeout inside
sendRequest():30_000ms—DEFAULT_REQUEST_TIMEOUT_MSinpackages/coding-agent/src/lsp/client.ts. - Warmup initialize timeout default:
5_000ms—WARMUP_TIMEOUT_MSinpackages/coding-agent/src/lsp/client.ts. - Project-load wait fallback:
15_000ms—PROJECT_LOAD_TIMEOUT_MSinpackages/coding-agent/src/lsp/client.ts. - Idle-client sweep interval when enabled:
60_000ms—IDLE_CHECK_INTERVAL_MSinpackages/coding-agent/src/lsp/client.ts. - Diagnostic message output cap: first
50messages —DIAGNOSTIC_MESSAGE_LIMITinpackages/coding-agent/src/lsp/index.ts. - Single-file diagnostics wait:
3_000ms—SINGLE_DIAGNOSTICS_WAIT_TIMEOUT_MS. - Batch/glob diagnostics wait per file:
400ms—BATCH_DIAGNOSTICS_WAIT_TIMEOUT_MS. - Glob diagnostic target cap: first
20matches —MAX_GLOB_DIAGNOSTIC_TARGETS. - Workspace symbol cap: first
200entries —WORKSPACE_SYMBOL_LIMIT. - Reference context cap: first
50references include source context —REFERENCE_CONTEXT_LIMIT. - References retry count:
2retries,250msbackoff —REFERENCES_RETRY_COUNT,REFERENCES_RETRY_DELAY_MS. - Directory rename cap:
1_000file pairs —MAX_RENAME_PAIRS. detectLspmux()state cache TTL:5 * 60 * 1000ms; liveness check timeout:1_000ms—STATE_CACHE_TTL_MS,LIVENESS_TIMEOUT_MSinpackages/coding-agent/src/lsp/lspmux.ts.- Workspace diagnostics output cap: first
50lines from the subprocess.
Errors
- Missing or invalid inputs are usually returned as text with
details.success: false, not thrown:- missing
file/query/new_name - invalid JSON in
payload - no matching server
- invalid
rename_filesource/destination conditions
- missing
resolveSymbolColumn()throws explicit errors for missing files, missing symbols, and out-of-bounds#Nselectors; these surface asLSP error: ...or request-specific error text.sendRequest()rejects on timeout withLSP request <method> timed out after <ms>ms.- Client process exit rejects all pending requests with an exit-code/stderr error assembled in
getOrCreateClient(). - Single-file action failures inside the main
trybecomeLSP error: <message>. requesthas its own error envelope:LSP error from <server> on <method>: <message>.- Some server failures are intentionally softened:
- diagnostics continue when one server fails
rename_filesuppressesworkspace/willRenameFiles“method not found” errors and records other server errors as notescode_actionsignorescodeAction/resolvefailures and applies unresolved actions when possible
- Aborts are not converted to text:
ToolAbortErroris rethrown.
Notes
statusreports configured/available servers fromLspConfig, not currently active client processes fromgetActiveClients().getLspServerForFile()excludescreateClientadapters and linter-only servers; navigation/refactor actions never target Biome/SwiftLint custom clients.getServersForFile()matches both file extensions and exact basenames fromfileTypes; config can target names likeDockerfileif present.symbolmatching is exact first, then case-insensitive, and falls back to the Nth occurrence on the specified line only; it never scans other lines.code_actionsusesqueryin two different ways: server-sidecontext.onlyfilter in list mode, client-side title/index selector in apply mode.renameandrename_filedefault to apply. Preview requiresapply: false.requestwithfile: "*"is treated the same as omittedfile: it does not build workspace-specific params.reloaddoes not recreate a client immediately after killing it; the next request triggers reinitialization.workspace/applyEditcan apply edits initiated by the server outside the direct tool action result path.detectLspmux()can be disabled withPI_DISABLE_LSPMUX=1; onlyrust-analyzeris inDEFAULT_SUPPORTED_SERVERS.- Startup LSP warmup (
discoverStartupLspServers(cwd)insdk.ts) is gated onenableLsp && options.hasUI && settings.get("lsp.diagnosticsOnWrite")— print/RPC/ACP/script sessions skip it and letgetOrCreateClient()cold-start servers on demand. Seedocs/sdk.md§ Startup performance. configCacheis per-process and never auto-invalidated; config changes require a fresh process to be observed bygetConfig()callers.