2026-07-26 02:33:29 +08:00
# LSP 导航
[English ](lsp.md ) | 中文
2026-08-13 11:55:38 +08:00
LSP seam 是一个[能力 seam ](../../.agents/notes/implemented/architecture/2026-07-15-lsp-capability-seam.md ):它在单一 `ctx.lsp` 服务上公开语义代码导航,并拆分到多个包:Service Definition( [dsh-lsp ](../../packages/lsp/lsp ), `ctx.lsp` + 提供方注册表)、通用 Service Provider( [dsh-lsp-stdio ](../../packages/lsp/lsp-stdio ),经过配置的 stdio 语言服务器宿主)和 Consumer([dsh-tool-lsp ](../../packages/lsp/tool-lsp ),即 `lsp` 工具 schema)。LSP 是**一项可选能力**,不属于 agent loop(智能体循环)主干,因此其词汇定义在此而非 [core.md ](core.md ) 中。更换提供方不会改变模型请求导航的方式。
2026-07-26 02:33:29 +08:00
源文件:[`packages/lsp/lsp/src/types.ts` ](../../packages/lsp/lsp/src/types.ts )
## 操作与坐标
seam 与模型恰好公开 4 项语义查询;该联合是闭合的,因此新增一项查询会通过编译强制要求同步修改 seam、提供方和工具。位置与范围采用从零开始的 UTF-16 坐标,与协议一致;面向模型的工具采用从 1 开始的光标约定,并在输入和输出时进行转换。
```ts type-equiv
/**
* The four semantic queries the seam and model expose. A closed union: adding an operation is a
* compile-enforced change across the seam, providers, and the tool. Symbols and call hierarchy are
2026-08-09 15:09:19 +08:00
* not operations here; they need different schemas.
2026-07-26 02:33:29 +08:00
*/
type LspOperation = 'goToDefinition' | 'findReferences' | 'goToImplementation' | 'hover'
` ``
` ``ts type-equiv
/** A zero-based UTF-16 cursor coordinate, matching the LSP wire convention. */
interface LspPosition {
/** Zero-based line. */
readonly line: number
/** Zero-based UTF-16 code-unit offset within the line. */
readonly character: number
}
` ``
` ``ts type-equiv
/** A zero-based UTF-16 half-open range ` [start, end)`. */
interface LspRange {
readonly start: LspPosition
readonly end: LspPosition
}
` ``
## 请求
每个字段都是必填项:` workspaceRoot` 由调用方提供,` languageId` 来自提供方注册而非请求,超时与结果上限由消费方决定。因此没有字段需要由实现提供默认值,也不存在 ` resolve()` 步骤。提供方收到调用方请求和派生的 ` languageId`;后者只用于同步瞬态文档,从不参与选择。
` ``ts type-equiv
/**
* A caller's normalized query. Every field is required: ` workspaceRoot` is caller-supplied,
* ` languageId` comes from the provider registration (not here), and consumers own timeouts and
* result limits — so no field needs implementation defaulting and there is no ` resolve()` step.
*/
interface LspQueryRequest {
/** Which semantic query to run. */
readonly operation: LspOperation
/** The source file to query (relative to ` workspaceRoot` or absolute; the provider canonicalizes). */
readonly filePath: string
/** The zero-based UTF-16 cursor position to query at. */
readonly position: LspPosition
/** The workspace root the provider resolves against and indexes; required, never defaulted. */
readonly workspaceRoot: string
}
` ``
` ``ts type-equiv
/**
* A request as a provider receives it: the caller's {@link LspQueryRequest} plus the ` languageId`
* the seam derived from the provider's extension mapping. The language id only synchronizes the
* transient document; it does not participate in selection.
*/
interface LspProviderQuery extends LspQueryRequest {
/** The LSP language id for ` filePath`, from this provider's extension mapping. */
readonly languageId: string
}
` ``
## 结果
2026-07-29 04:19:18 +08:00
这是一个闭合的可辨识联合:导航操作规范化为 ` locations`, ` hover` 规范化为内容或 ` null`。消费方使用 ` switch` 对 ` kind` 做穷尽处理,因此新增分支会使编译失败,直到完成处理。` findReferences` 始终包含声明;提供方在内部强制保证这一点,因此调用方没有对应 flag。` locations` 变体携带 ` resolvedWorkspaceUri`,即提供方的规范工作区 ` file:` URI。调用方相对化位置 URI 时应使用这一坐标,而不是对可能经过符号链接的请求根目录应用宿主平台路径规则。
2026-07-26 02:33:29 +08:00
` ``ts type-equiv
/** One resolved location: a document URI and the range within it. */
interface LspLocation {
/** The target document URI (` file:` or otherwise), verbatim from the server. */
readonly uri: string
/** The range within the target document. */
readonly range: LspRange
}
` ``
` ``ts type-equiv
/** Normalized hover content, or ` null` for no hover at the position. */
interface LspHover {
/** The normalized hover text (markdown or plaintext, provider-joined). */
readonly contents: string
/** The range the hover applies to, when the server supplied one. */
readonly range?: LspRange
}
` ``
` ``ts type-equiv
/**
* The closed result union. Navigation operations (` goToDefinition`, ` findReferences`,
* ` goToImplementation`) normalize to ` locations`; ` hover` normalizes to content or ` null`.
* Consumers ` switch` on ` kind` to exhaustiveness so a new arm breaks compilation until handled.
*
2026-07-29 04:19:18 +08:00
* The ` locations` variant carries ` resolvedWorkspaceUri`: the provider's canonical ` file:` URI for
* the request's workspace root. A caller that relativizes location URIs MUST use this, not parse the
* request's possibly symlinked process path with host-platform rules; the execution platform may
* differ from the caller's.
2026-07-26 02:33:29 +08:00
*/
type LspQueryResult =
2026-07-29 04:19:18 +08:00
| { readonly kind: 'locations'; readonly locations: readonly LspLocation[]; readonly resolvedWorkspaceUri: string }
2026-07-26 02:33:29 +08:00
| { readonly kind: 'hover'; readonly hover: LspHover | null }
` ``
## 提供方与服务
2026-08-04 17:36:14 +08:00
每个提供方拥有一个稳定的品牌化 ` id`,以及一份互斥的、小写且以点开头的扩展名映射。` registerProvider` 会原子预留 id 和每个扩展名:注册无效或冲突时不发布任何内容;其 disposer 会释放所有保留项。每次查询独立选择提供方,且选择与顺序无关;没有匹配项时抛出 ` LspError` ` LSP_UNAVAILABLE`。该 seam 不公开协议类型、进程或文档控制,也不提供通用 JSON-RPC 逃生口。
2026-07-26 02:33:29 +08:00
` ``ts type-equiv
/**
* A language-server backend registered on ` ctx.lsp`. Each provider owns a stable {@link
* LspProviderId} and an extension-to-language-id map (lowercase, leading-dot keys).
* ` findReferences` always includes declarations — the provider enforces this internally; callers
* get no flag.
*/
interface LspProvider {
/** Stable provider identity, reserved atomically with the extension mappings. */
readonly id: LspProviderId
/** Lowercase leading-dot extension → LSP language id (e.g. ` { '.ts': 'typescript' }`). */
readonly extensionToLanguage: Readonly<Record<string, string>>
/**
* Run one query. The seam has already selected this provider and derived ` languageId`.
* @param request - the resolved provider query (caller request + derived language id).
* @param signal - optional cancellation; the provider stops its own work when it aborts.
* @returns the normalized, closed-union result.
*/
query(request: LspProviderQuery, signal?: AbortSignal): Promise<LspQueryResult>
}
` ``
` ``ts type-equiv
/**
* The LSP capability seam (` ctx.lsp`). Owns provider registration/selection and normalized query
* execution; exposes exactly the four operations and no protocol escape hatch.
*/
interface LspService {
/**
* Register a provider, atomically reserving its id and every normalized extension. Any conflict
* or invalid input publishes nothing and throws ` LspError`; the returned disposer releases all
* reservations. Disposed with the calling fiber.
* @param provider - the backend to register.
* @returns a synchronous disposer releasing the id and all extension reservations.
*/
registerProvider(provider: LspProvider): () => void
/**
* Select a provider by the file's extension and run one query. Selection is per-query and
* order-independent; no match throws ` LspError` ` LSP_UNAVAILABLE`.
* @param request - the normalized query.
* @param signal - optional cancellation forwarded to the selected provider.
* @returns the normalized, closed-union result.
*/
query(request: LspQueryRequest, signal?: AbortSignal): Promise<LspQueryResult>
}
` ``
` LspProviderId` 是该 seam 的品牌化 id(来自 [dsh-brand](../../packages/util/brand) 的 ` Branded<'LspProviderId'>`);` LspError` 扩展 ` HarnessError`,提供 ` LSP_INVALID_PROVIDER`、` LSP_CONFLICT`、` LSP_UNAVAILABLE`、` LSP_DISPOSED`、` LSP_UNSUPPORTED_OPERATION` 和 ` LSP_MALFORMED_RESPONSE` 等稳定错误码,调用方应按错误码路由,而不是解析 ` message`。
2026-08-13 00:19:15 +08:00
<!-- BEGIN GENERATED cordis-surface (gen-cordis-catalog.ts) — do not edit between markers -->
<a id="cordis-surface"></a>
## Cordis API
Generated from source by ` scripts/gen-cordis-catalog.ts` (verified fresh by ` pnpm run verify-cordis-catalog` in doc-sync; regenerate with ` pnpm run gen-cordis-catalog`) — this section is byte-identical in both language sides of the page. Signature blocks use a ` ts cordis-catalog` fence and keep the original source JSDoc; dispatch modes are defined in the [primer](../cordis-primer.md#dispatch-modes), and the framework-inherited ` ctx` API lives in [cordis-api/inherited.md](../cordis-api/inherited.md).
<a id="ctxlsp--lspservice"></a>
### ` ctx.lsp` — ` LspService`
The LSP capability seam (` ctx.lsp`). Owns provider registration/selection and normalized query execution; exposes exactly the four operations and no protocol escape hatch.
` ``ts cordis-catalog
/**
* Register a provider, atomically reserving its id and every normalized extension. Any conflict
* or invalid input publishes nothing and throws ` LspError`; the returned disposer releases all
* reservations. Disposed with the calling fiber.
* @param provider - the backend to register.
* @returns a synchronous disposer releasing the id and all extension reservations.
*/
registerProvider(provider: LspProvider): () => void
/**
* Select a provider by the file's extension and run one query. Selection is per-query and
* order-independent; no match throws ` LspError` ` LSP_UNAVAILABLE`.
* @param request - the normalized query.
* @param signal - optional cancellation forwarded to the selected provider.
* @returns the normalized, closed-union result.
*/
query(request: LspQueryRequest, signal?: AbortSignal): Promise<LspQueryResult>
` ``
Source: [` packages/lsp/lsp/src/types.ts:113`](../../packages/lsp/lsp/src/types.ts)
<!-- END GENERATED cordis-surface -->