2026-07-16 12:05:35 +08:00
/**
* Model-facing `lsp` tool over `ctx.lsp`. One read-only tool with four operations
2026-07-21 13:29:40 +08:00
* (`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.
2026-07-16 12:05:35 +08:00
*
* 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'
2026-07-21 19:15:18 +08:00
import { assertNever } from '@deepseek-ai/dsh-llm'
2026-07-16 12:05:35 +08:00
import { LspError } from '@deepseek-ai/dsh-lsp'
import type { } from '@deepseek-ai/dsh-lsp'
import type { } from '@deepseek-ai/dsh-system-prompt'
2026-07-21 13:29:40 +08:00
import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout'
2026-07-16 12:05:35 +08:00
import {
DEFAULT_MAX_LOCATIONS ,
2026-07-21 13:29:40 +08:00
DEFAULT_MAX_RESULT_CHARS ,
2026-07-16 12:05:35 +08:00
formatHover ,
formatLocations ,
LSP_OPERATIONS ,
parseLspArgs ,
presentLspCall ,
} from './render.ts'
import { sessionCwd } from './session-cwd.ts'
export {
DEFAULT_MAX_LOCATIONS ,
2026-07-21 13:29:40 +08:00
DEFAULT_MAX_RESULT_CHARS ,
2026-07-16 12:05:35 +08:00
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 =
2026-07-21 13:29:40 +08:00
'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.'
2026-07-16 12:05:35 +08:00
/** Plugin configuration: result caps and the timeout budget. */
export interface Config {
/** Largest number of rendered locations before an omission marker (default 100). */
maxLocations? : number
2026-07-21 13:29:40 +08:00
/** Largest complete rendered result in characters, including truncation metadata (default 16000). */
maxResultChars? : number
2026-07-16 12:05:35 +08:00
/** Tool-call timeout budget in ms (default 60000). */
timeoutMs? : number
}
export const Config : z < Config > = z . object ( {
maxLocations : z.number ( ) . default ( DEFAULT_MAX_LOCATIONS ) ,
2026-07-21 13:29:40 +08:00
maxResultChars : z.number ( ) . default ( DEFAULT_MAX_RESULT_CHARS ) ,
timeoutMs : z.number ( ) . max ( MAX_TIMER_DELAY_MS ) . default ( DEFAULT_LSP_TOOL_TIMEOUT_MS ) ,
2026-07-16 12:05:35 +08:00
} )
type ResolvedConfig = Required < Config >
2026-07-21 19:15:18 +08:00
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
2026-07-16 12:05:35 +08:00
/**
* 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 )
2026-07-21 13:29:40 +08:00
assertPositiveInteger ( 'maxResultChars' , resolved . maxResultChars )
assertTimer ( 'timeoutMs' , resolved . timeoutMs )
2026-07-16 12:05:35 +08:00
ctx . systemPrompt . section ( { name : 'tool:lsp' , order : 112 , text : LSP_PROMPT_TEXT } )
ctx . tools . register ( defineTool ( {
name : 'lsp' ,
description :
2026-07-21 13:29:40 +08:00
'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.' ,
2026-07-16 12:05:35 +08:00
parameters : {
operation : {
type : 'string' ,
required : true ,
enum : [ . . . LSP_OPERATIONS ] ,
2026-07-21 13:29:40 +08:00
description : 'goToDefinition, findReferences, goToImplementation, or hover.' ,
2026-07-16 12:05:35 +08:00
} ,
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.' } ,
} ,
2026-07-21 19:15:18 +08:00
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' )
}
} ,
} ,
2026-07-16 12:05:35 +08:00
timeoutMs : resolved.timeoutMs ,
2026-07-21 19:15:18 +08:00
async execute ( args , exec ) {
2026-07-16 12:05:35 +08:00
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' :
2026-07-21 19:15:18 +08:00
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 ,
}
2026-07-16 12:05:35 +08:00
case 'hover' :
2026-07-21 19:15:18 +08:00
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 } ,
} ,
} ,
} ,
}
2026-07-21 13:29:40 +08:00
/* v8 ignore next -- exhaustive over the closed LspQueryResult union; unreachable. */
default :
return assertNever ( result , 'tool-lsp result' )
2026-07-16 12:05:35 +08:00
}
} ,
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 ` )
}
}
2026-07-21 13:29:40 +08:00
/** 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 } ` )
}
}