| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243 |
- /**
- * Model-facing `lsp` tool over `ctx.lsp`. One read-only tool with four operations
- * (`goToDefinition`/`findReferences`/`goToImplementation`/`hover`); it converts one-based UTF-16
- * cursor coordinates to the seam's zero-based positions, requires the session workspace with no
- * fallback, caps and renders results, and attaches a configurable timeout budget for
- * `dsh-timeout-policy` to enforce. It runtime-injects only `tools`, `lsp`, and `systemPrompt` and
- * imports no provider.
- *
- * Namespace plugin (named exports, no default export).
- * @module @deepseek-ai/dsh-tool-lsp
- */
- import type { Context } from 'cordis'
- import z from 'schemastery'
- import { defineTool } from '@deepseek-ai/dsh-tools'
- import { assertNever } from '@deepseek-ai/dsh-llm'
- import { LspError } from '@deepseek-ai/dsh-lsp'
- import type {} from '@deepseek-ai/dsh-lsp'
- import type {} from '@deepseek-ai/dsh-system-prompt'
- import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout'
- import {
- DEFAULT_MAX_LOCATIONS,
- DEFAULT_MAX_RESULT_CHARS,
- formatHover,
- formatLocations,
- LSP_OPERATIONS,
- parseLspArgs,
- presentLspCall,
- } from './render.ts'
- import { sessionCwd } from './session-cwd.ts'
- export {
- DEFAULT_MAX_LOCATIONS,
- DEFAULT_MAX_RESULT_CHARS,
- formatHover,
- formatLocations,
- LSP_OPERATIONS,
- parseLspArgs,
- presentLspCall,
- renderUri,
- } from './render.ts'
- export { sessionCwd } from './session-cwd.ts'
- /** Cordis plugin name for loader diagnostics. */
- export const name = 'tool-lsp'
- /** Services required by this plugin. */
- export const inject = ['tools', 'lsp', 'systemPrompt']
- /** Default tool-call timeout budget (ms), covering the queued open/query/close lifecycle. */
- export const DEFAULT_LSP_TOOL_TIMEOUT_MS = 60_000
- /** The stable system-prompt guidance positioning LSP as a precision aid. */
- export const LSP_PROMPT_TEXT =
- 'Use search/read for ordinary navigation. Use lsp when textual matches are ambiguous or before a change requires precise definitions, implementations, or references. Positions are one-based line and character (UTF-16) at the cursor; an off-symbol position may return no results. findReferences always includes the declaration.'
- /** Plugin configuration: result caps and the timeout budget. */
- export interface Config {
- /** Largest number of rendered locations before an omission marker (default 100). */
- maxLocations?: number
- /** Largest complete rendered result in characters, including truncation metadata (default 16000). */
- maxResultChars?: number
- /** Tool-call timeout budget in ms (default 60000). */
- timeoutMs?: number
- }
- export const Config: z<Config> = z.object({
- maxLocations: z.number().default(DEFAULT_MAX_LOCATIONS),
- maxResultChars: z.number().default(DEFAULT_MAX_RESULT_CHARS),
- timeoutMs: z.number().max(MAX_TIMER_DELAY_MS).default(DEFAULT_LSP_TOOL_TIMEOUT_MS),
- })
- type ResolvedConfig = Required<Config>
- const LSP_POSITION_OUTPUT_SCHEMA = {
- type: 'object',
- additionalProperties: false,
- properties: {
- line: { type: 'integer', required: true },
- character: { type: 'integer', required: true },
- },
- } as const
- const LSP_RANGE_OUTPUT_SCHEMA = {
- type: 'object',
- additionalProperties: false,
- properties: {
- start: { ...LSP_POSITION_OUTPUT_SCHEMA, required: true },
- end: { ...LSP_POSITION_OUTPUT_SCHEMA, required: true },
- },
- } as const
- /**
- * Register the `lsp` tool and its system-prompt guidance.
- * @param ctx - the plugin context (must inject `tools`, `lsp`, `systemPrompt`).
- * @param config - the resolved plugin configuration.
- */
- export function apply(ctx: Context, config: Config): void {
- const resolved = config as ResolvedConfig
- assertPositiveInteger('maxLocations', resolved.maxLocations)
- assertPositiveInteger('maxResultChars', resolved.maxResultChars)
- assertTimer('timeoutMs', resolved.timeoutMs)
- ctx.systemPrompt.section({ name: 'tool:lsp', order: 112, text: LSP_PROMPT_TEXT })
- ctx.tools.register(defineTool({
- name: 'lsp',
- description:
- 'Query a language server for precise code navigation. operation is one of goToDefinition, findReferences, goToImplementation, hover. line and character are one-based UTF-16 cursor coordinates. findReferences includes the declaration.',
- parameters: {
- operation: {
- type: 'string',
- required: true,
- enum: [...LSP_OPERATIONS],
- description: 'goToDefinition, findReferences, goToImplementation, or hover.',
- },
- file_path: { type: 'string', required: true, description: 'The source file to query, relative to the workspace or absolute.' },
- line: { type: 'number', required: true, description: 'One-based line of the cursor.' },
- character: { type: 'number', required: true, description: 'One-based UTF-16 column of the cursor.' },
- },
- output: {
- schema: {
- oneOf: [
- {
- type: 'object',
- additionalProperties: false,
- properties: {
- kind: { type: 'string', required: true, const: 'locations' },
- locations: {
- type: 'array',
- required: true,
- items: {
- type: 'object',
- additionalProperties: false,
- properties: {
- uri: { type: 'string', required: true },
- range: { ...LSP_RANGE_OUTPUT_SCHEMA, required: true },
- },
- },
- },
- resolvedWorkspaceRoot: { type: 'string', required: true },
- },
- },
- {
- type: 'object',
- additionalProperties: false,
- properties: {
- kind: { type: 'string', required: true, const: 'hover' },
- hover: {
- required: true,
- oneOf: [
- { type: 'null' },
- {
- type: 'object',
- additionalProperties: false,
- properties: {
- contents: { type: 'string', required: true },
- range: LSP_RANGE_OUTPUT_SCHEMA,
- },
- },
- ],
- },
- },
- },
- ],
- },
- render: (_args, value) => {
- switch (value.kind) {
- case 'locations':
- return [{ type: 'text', text: formatLocations(value.locations, value.resolvedWorkspaceRoot, resolved.maxLocations, resolved.maxResultChars) }]
- case 'hover':
- return [{ type: 'text', text: formatHover(value.hover, resolved.maxResultChars) }]
- /* v8 ignore next -- exhaustive over the output schema's closed union; unreachable. */
- default:
- return assertNever(value, 'tool-lsp output')
- }
- },
- },
- timeoutMs: resolved.timeoutMs,
- async execute(args, exec) {
- const input = parseLspArgs(args)
- const workspaceRoot = sessionCwd(exec)
- if (workspaceRoot === undefined) {
- throw new LspError('the lsp tool requires a session workspace cwd', 'LSP_WORKSPACE_REQUIRED')
- }
- const result = await ctx.lsp.query({
- operation: input.operation,
- filePath: input.filePath,
- position: input.position,
- workspaceRoot,
- }, exec.signal)
- switch (result.kind) {
- case 'locations':
- return {
- kind: 'locations' as const,
- locations: result.locations.map(location => ({
- uri: location.uri,
- range: {
- start: { line: location.range.start.line, character: location.range.start.character },
- end: { line: location.range.end.line, character: location.range.end.character },
- },
- })),
- resolvedWorkspaceRoot: result.resolvedWorkspaceRoot,
- }
- case 'hover':
- return {
- kind: 'hover' as const,
- hover: result.hover === null
- ? null
- : {
- contents: result.hover.contents,
- ...result.hover.range === undefined
- ? {}
- : {
- range: {
- start: { line: result.hover.range.start.line, character: result.hover.range.start.character },
- end: { line: result.hover.range.end.line, character: result.hover.range.end.character },
- },
- },
- },
- }
- /* v8 ignore next -- exhaustive over the closed LspQueryResult union; unreachable. */
- default:
- return assertNever(result, 'tool-lsp result')
- }
- },
- presentCall: presentLspCall,
- }))
- }
- /** Reject a non-positive-integer config value at load, so misconfiguration fails loud. */
- function assertPositiveInteger(name: string, value: number): void {
- if (!Number.isInteger(value) || value < 1) {
- throw new Error(`tool-lsp: ${name} must be a positive integer`)
- }
- }
- /** Reject a timer value Node would clamp instead of scheduling as configured. */
- function assertTimer(name: string, value: number): void {
- if (!Number.isInteger(value) || value < 1 || value > MAX_TIMER_DELAY_MS) {
- throw new Error(`tool-lsp: ${name} must be a positive integer no greater than ${MAX_TIMER_DELAY_MS}`)
- }
- }
|