9.2 KiB
Query-Based Position Resolution
Summary
Add a query parameter to position-based tools (lsp_hover, lsp_definition, lsp_references, lsp_rename, lsp_signature_help, lsp_code_actions) that resolves a symbol name to a file position, eliminating the need for the LLM to know exact line/column numbers when it just wants to query a known symbol.
Motivation
Today, to hover over a function named handleRequest, the LLM must:
- Know or look up the exact line and column where
handleRequestis defined or used - Call
lsp_hoverwithpath,line,character
This is friction. The LLM often knows the symbol name but not the exact position. It has to call lsp_symbols first to find the position, then call the actual tool — two round-trips.
The lsp-pi competitor solves this with a query parameter: pass query: "handleRequest" and it resolves the position via document symbols before making the LSP request. This is a significant UX improvement.
Goals
- Optional
queryparameter on all position-based tools - Symbol name resolution — find the symbol's position in the file via document symbols (LSP) or tree-sitter
- Fallback chain — try LSP document symbols first, then tree-sitter symbol extraction
- Exact > partial — exact name match wins over substring match
- Transparent — when
queryis used, include the resolved position in the output so the LLM knows where it landed
Non-goals
- Workspace-wide symbol search via query — this is file-scoped resolution. Use
lsp_symbolswith aqueryfor workspace search. - Fuzzy matching — exact and substring only. Fuzzy introduces ambiguity.
- Replacing
line/character— query is an alternative, not a replacement. When both are provided,line/charactertake precedence.
Design
Parameter changes
Add to all position-based tool schemas:
query: Type.Optional(Type.String({
description: "Symbol name to find in the file. Alternative to line/character — resolves the symbol's position automatically."
})),
Tools that gain the query parameter:
lsp_hoverlsp_definitionlsp_referenceslsp_renamelsp_signature_help(new tool)lsp_code_actions(new tool)
Resolution logic
Create a shared resolver in src/shared/resolve-position.ts:
export interface ResolvedPosition {
line: number; // 0-indexed (LSP convention)
character: number; // 0-indexed
symbolName: string; // the matched symbol name
source: "lsp" | "tree-sitter";
}
export async function resolveSymbolPosition(
filePath: string,
query: string,
manager: LspManager,
treeSitter?: TreeSitterManager | null,
): Promise<ResolvedPosition | null> {
// 1. Try LSP document symbols
const client = manager.getRunningClient(manager.getLanguageId(filePath) ?? "");
if (client) {
const uri = manager.getFileUri(filePath);
try {
const symbols = await client.sendRequest<DocumentSymbol[]>(
"textDocument/documentSymbol",
{ textDocument: { uri } }
);
const match = findSymbolPosition(symbols, query);
if (match) return { ...match, source: "lsp" };
} catch { /* fall through */ }
}
// 2. Try tree-sitter
if (treeSitter) {
const absPath = manager.resolvePath(filePath);
const content = await readFile(absPath, "utf-8");
const tree = await treeSitter.parse(absPath, content);
if (tree) {
const match = findSymbolInTree(tree, query);
if (match) return { ...match, source: "tree-sitter" };
}
}
return null;
}
Symbol matching (findSymbolPosition)
Walks the document symbol tree (recursive for children) and matches:
- Exact match —
symbol.name === query(case-sensitive) - Case-insensitive exact —
symbol.name.toLowerCase() === query.toLowerCase() - Substring match —
symbol.name.toLowerCase().includes(query.toLowerCase())
Returns the first match in priority order. Uses selectionRange.start (preferred) or range.start for position.
Tree-sitter symbol matching (findSymbolInTree)
Uses the existing extractSymbols function from symbol-extractor.ts:
- Extract all symbols from the tree
- Match using the same priority chain as above
- Return the symbol's start position
Tool integration pattern
Each tool's execute function gains a preamble:
async execute(_toolCallId, params) {
const filePath = params.path.replace(/^@/, "");
let line = params.line;
let character = params.character;
let resolvedFrom: string | undefined;
// Resolve position from query if line/character not provided
if ((line === undefined || character === undefined) && params.query) {
const resolved = await resolveSymbolPosition(filePath, params.query, manager, treeSitter);
if (resolved) {
line = resolved.line + 1; // convert to 1-indexed
character = resolved.character + 1;
resolvedFrom = `Resolved "${params.query}" → ${line}:${character} [${resolved.source}]`;
} else {
return {
content: [{ type: "text", text: `Could not find symbol "${params.query}" in ${filePath}` }],
details: { hasResult: false },
};
}
}
if (line === undefined || character === undefined) {
return {
content: [{ type: "text", text: "Either line/character or query is required." }],
details: { hasResult: false },
};
}
// ... existing tool logic using line/character ...
// Prepend resolved position to output if query was used
if (resolvedFrom) {
const existingText = result.content[0]?.text ?? "";
result.content[0] = { type: "text", text: `${resolvedFrom}\n\n${existingText}` };
}
return result;
}
Validation rules
- If both
line/characterandqueryare provided: useline/character, ignorequery - If neither is provided: return error "Either line/character or query is required."
- If
queryis provided but doesn't match any symbol: return error with the file's top-level symbol names as a hint lineandcharacterremain individually required whenqueryis not used (can't provide justlinewithoutcharacter)
Parameter schema update
The line and character parameters change from required to optional:
// Before
line: Type.Number({ description: "Line number (1-indexed)" }),
character: Type.Number({ description: "Column number (1-indexed)" }),
// After
line: Type.Optional(Type.Number({ description: "Line number (1-indexed). Required unless query is provided." })),
character: Type.Optional(Type.Number({ description: "Column number (1-indexed). Required unless query is provided." })),
query: Type.Optional(Type.String({ description: "Symbol name to find in the file. Alternative to line/character." })),
renderCall updates
When query is used instead of line/character:
lsp_hover src/handler.ts query="handleRequest"
When line/character is used (unchanged):
lsp_hover src/handler.ts:42:10
System prompt update
Add a guideline:
Position-based LSP tools (lsp_hover, lsp_definition, lsp_references, lsp_rename, lsp_signature_help, lsp_code_actions) accept either line/character or a query parameter. Use query when you know the symbol name but not the exact position — it resolves the position automatically via document symbols.
File layout
src/shared/
└── resolve-position.ts # New: shared position resolver
src/tools/
├── hover.ts # Modified: add query param
├── definition.ts # Modified: add query param
├── references.ts # Modified: add query param
├── rename.ts # Modified: add query param
├── signature-help.ts # New tool: includes query param
└── code-actions.ts # New tool: includes query param
Examples
Hover by symbol name
lsp_hover path="src/handler.ts" query="handleRequest"
→ Resolved "handleRequest" → 42:10 [lsp]
→ function handleRequest(req: Request): Promise<Response>
Find references by name
lsp_references path="src/types.ts" query="UserConfig"
→ Resolved "UserConfig" → 15:14 [tree-sitter]
→ 12 references in 5 files
Rename by name
lsp_rename path="src/utils.ts" query="formatDate" newName="formatDateTime"
→ Resolved "formatDate" → 8:17 [lsp]
→ Rename "formatDateTime": 23 edit(s) across 7 file(s)
Open questions
- Ambiguity — what if a file has multiple symbols with the same name (e.g., overloaded methods, or a variable and a type with the same name)? Current design returns the first exact match. Should we return all matches and let the LLM pick? Or is first-match good enough?
- Nested symbols —
findSymbolPositionwalks children recursively, soquery="render"could match a top-levelrenderfunction or arendermethod inside a class. Should class-qualified names be supported (e.g.,query="MyComponent.render")? - Performance — requesting document symbols adds a round-trip before the actual request. For LSP, this is typically fast (<50ms). For tree-sitter, it's a file parse. Acceptable for the ergonomic benefit.
lsp_completions— should completions also support query? Less clear — completions are about a cursor position in code, not a symbol name. Leaving out for now.