diff --git a/docs/users/features/lsp.md b/docs/users/features/lsp.md index efae7830f..8cab231a6 100644 --- a/docs/users/features/lsp.md +++ b/docs/users/features/lsp.md @@ -148,6 +148,8 @@ For servers that use TCP or Unix socket transport: Qwen Code exposes LSP functionality through the unified `lsp` tool. Here are the available operations: +For location-based operations (`goToDefinition`, `findReferences`, `hover`, `goToImplementation`, and `prepareCallHierarchy`), you can either provide an exact `filePath` + `line` + `character`, or provide `symbolName` and let Qwen Code resolve the symbol position through workspace symbol search first. + ### Code Navigation #### Go to Definition @@ -157,9 +159,8 @@ Find where a symbol is defined. ``` Operation: goToDefinition Parameters: - - filePath: Path to the file - - line: Line number (1-based) - - character: Column number (1-based) + - filePath + line + character: Exact source position (1-based), or + - symbolName: Symbol name to resolve automatically ``` #### Find References @@ -169,9 +170,8 @@ Find all references to a symbol. ``` Operation: findReferences Parameters: - - filePath: Path to the file - - line: Line number (1-based) - - character: Column number (1-based) + - filePath + line + character: Exact source position (1-based), or + - symbolName: Symbol name to resolve automatically - includeDeclaration: Include the declaration itself (optional) ``` @@ -182,9 +182,8 @@ Find implementations of an interface or abstract method. ``` Operation: goToImplementation Parameters: - - filePath: Path to the file - - line: Line number (1-based) - - character: Column number (1-based) + - filePath + line + character: Exact source position (1-based), or + - symbolName: Symbol name to resolve automatically ``` ### Symbol Information @@ -196,9 +195,8 @@ Get documentation and type information for a symbol. ``` Operation: hover Parameters: - - filePath: Path to the file - - line: Line number (1-based) - - character: Column number (1-based) + - filePath + line + character: Exact source position (1-based), or + - symbolName: Symbol name to resolve automatically ``` #### Document Symbols @@ -231,9 +229,8 @@ Get the call hierarchy item at a position. ``` Operation: prepareCallHierarchy Parameters: - - filePath: Path to the file - - line: Line number (1-based) - - character: Column number (1-based) + - filePath + line + character: Exact source position (1-based), or + - symbolName: Symbol name to resolve automatically ``` #### Incoming Calls diff --git a/packages/core/src/core/client.test.ts b/packages/core/src/core/client.test.ts index 5c8c6c2c3..4745801a7 100644 --- a/packages/core/src/core/client.test.ts +++ b/packages/core/src/core/client.test.ts @@ -340,7 +340,6 @@ describe('Gemini Client (client.ts)', () => { getUserMemory: vi.fn().mockReturnValue(''), getSystemPrompt: vi.fn().mockReturnValue(undefined), getAppendSystemPrompt: vi.fn().mockReturnValue(undefined), - isLspEnabled: vi.fn().mockReturnValue(false), getFullContext: vi.fn().mockReturnValue(false), getSessionId: vi.fn().mockReturnValue('test-session-id'), getProxy: vi.fn().mockReturnValue(undefined), @@ -2919,7 +2918,6 @@ Other open files: '', 'test-model', 'Be extra concise.', - { lspEnabled: false }, ); }); diff --git a/packages/core/src/core/client.ts b/packages/core/src/core/client.ts index 1cdb9325d..797f61190 100644 --- a/packages/core/src/core/client.ts +++ b/packages/core/src/core/client.ts @@ -267,7 +267,6 @@ export class GeminiClient { userMemory, this.config.getModel(), appendSystemPrompt, - { lspEnabled: this.config.isLspEnabled() }, ); } diff --git a/packages/core/src/core/prompts.ts b/packages/core/src/core/prompts.ts index f21d3c027..7c221b58a 100644 --- a/packages/core/src/core/prompts.ts +++ b/packages/core/src/core/prompts.ts @@ -116,7 +116,6 @@ export function getCoreSystemPrompt( userMemory?: string, model?: string, appendInstruction?: string, - options?: { lspEnabled?: boolean }, ): string { // if QWEN_SYSTEM_MD is set (and not 0|false), override system prompt from file // default path is .qwen/system.md but can be modified via custom path in QWEN_SYSTEM_MD @@ -141,36 +140,11 @@ export function getCoreSystemPrompt( } } - if (options?.lspEnabled) { - debugLogger.info( - 'LSP is enabled — injecting LSP priority instruction into system prompt', - ); - } - const basePrompt = systemMdEnabled ? fs.readFileSync(systemMdPath, 'utf8') : ` You are Qwen Code, an interactive CLI agent developed by Alibaba Group, specializing in software engineering tasks. Your primary goal is to help users safely and efficiently, adhering strictly to the following instructions and utilizing your available tools. -${ - options?.lspEnabled - ? ` -# LSP Code Intelligence (IMPORTANT) -A Language Server Protocol (LSP) tool is available and connected to a running language server. You MUST use the '${ToolNames.LSP}' tool as your FIRST choice for ALL code intelligence queries: -- Finding definitions → use lsp with operation "goToDefinition" -- Finding references → use lsp with operation "findReferences" -- Hover/type info → use lsp with operation "hover" -- Listing symbols in a file → use lsp with operation "documentSymbol" -- Searching symbols in workspace → use lsp with operation "workspaceSymbol" -- Finding implementations → use lsp with operation "goToImplementation" -- Call hierarchy → use lsp with operation "prepareCallHierarchy", then "incomingCalls" or "outgoingCalls" -- Checking errors/warnings → use lsp with operation "diagnostics" or "workspaceDiagnostics" -- Code actions/quick fixes → use lsp with operation "codeActions" - -Do NOT use '${ToolNames.GREP}' or '${ToolNames.READ_FILE}' for these queries. Only fall back to grep if LSP returns no results. -` - : '' -} # Core Mandates - **Conventions:** Rigorously adhere to existing project conventions when reading or modifying code. Analyze surrounding code, tests, and configuration first. diff --git a/packages/core/src/tools/grep.ts b/packages/core/src/tools/grep.ts index 25bb1be1b..53500022f 100644 --- a/packages/core/src/tools/grep.ts +++ b/packages/core/src/tools/grep.ts @@ -564,13 +564,10 @@ export class GrepTool extends BaseDeclarativeTool { static readonly Name = ToolNames.GREP; constructor(private readonly config: Config) { - const lspNote = config.isLspEnabled() - ? '\n - IMPORTANT: An LSP tool is available. For code intelligence queries (finding definitions, references, implementations, symbols, hover info, diagnostics, call hierarchy), use the "lsp" tool instead of Grep. Grep should only be used for text pattern searches, NOT for code navigation or symbol lookups.\n' - : ''; super( GrepTool.Name, ToolDisplayNames.GREP, - `A powerful search tool for finding patterns in files\n\n Usage:\n - ALWAYS use Grep for search tasks. NEVER invoke \`grep\` or \`rg\` as a Bash command. The Grep tool has been optimized for correct permissions and access.${lspNote}\n - Supports full regex syntax (e.g., "log.*Error", "function\\s+\\w+")\n - Filter files with glob parameter (e.g., "*.js", "**/*.tsx")\n - Case-insensitive by default\n - Use Agent tool for open-ended searches requiring multiple rounds\n`, + 'A powerful search tool for finding patterns in files\n\n Usage:\n - ALWAYS use Grep for search tasks. NEVER invoke `grep` or `rg` as a Bash command. The Grep tool has been optimized for correct permissions and access.\n - Supports full regex syntax (e.g., "log.*Error", "function\\s+\\w+")\n - Filter files with glob parameter (e.g., "*.js", "**/*.tsx")\n - Case-insensitive by default\n - Use Agent tool for open-ended searches requiring multiple rounds\n', Kind.Search, { properties: { diff --git a/packages/core/src/tools/lsp.test.ts b/packages/core/src/tools/lsp.test.ts index b30eb67e8..aaee78a5d 100644 --- a/packages/core/src/tools/lsp.test.ts +++ b/packages/core/src/tools/lsp.test.ts @@ -5,6 +5,8 @@ */ import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest'; +import fs from 'node:fs'; +import os from 'node:os'; import path from 'node:path'; import { pathToFileURL } from 'node:url'; import type { Config } from '../config/config.js'; @@ -82,6 +84,19 @@ const createMockConfig = (client?: LspClient, enabled = true): Config => const createTool = (client?: LspClient, enabled = true) => new LspTool(createMockConfig(client, enabled)); +const createToolWithRoot = (root: string, client?: LspClient, enabled = true) => + new LspTool({ + getLspClient: () => client, + isLspEnabled: () => enabled, + getProjectRoot: () => root, + getWorkspaceContext: () => ({ + getDirectories: () => [root], + }), + getFileService: () => ({ + shouldIgnoreFile: () => false, + }), + } as unknown as Config); + describe('LspTool', () => { describe('validateToolParams', () => { let tool: LspTool; @@ -367,6 +382,85 @@ describe('LspTool', () => { expect(result.llmContent).toContain('No definitions found'); }); + + it('returns the resolved symbol location when definitions are empty', async () => { + const client = createMockClient(); + const tool = createTool(client); + const filePath = resolvePath('src', 'calculator.py'); + const symbol: LspSymbolInformation = { + name: 'SimpleCalculator', + kind: 'Class', + location: createLocation(filePath, 21, 6), + serverName: 'pylsp', + }; + (client.workspaceSymbols as Mock).mockResolvedValue([symbol]); + (client.definitions as Mock).mockResolvedValue([]); + + const invocation = tool.build({ + operation: 'goToDefinition', + symbolName: 'SimpleCalculator', + }); + const result = await invocation.execute(abortSignal); + + expect(client.definitions).toHaveBeenCalledWith( + expect.objectContaining({ + uri: toUri(filePath), + range: expect.objectContaining({ + start: { line: 21, character: 6 }, + }), + }), + 'pylsp', + 20, + ); + expect(result.llmContent).toContain('Definitions for'); + expect(result.returnDisplay).toContain( + '1. src/calculator.py:22:7 [pylsp]', + ); + }); + + it('matches document symbols that include a language-specific signature', async () => { + const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'lsp-tool-')); + try { + const javaDir = path.join(tempRoot, 'src', 'main', 'java', 'com'); + fs.mkdirSync(javaDir, { recursive: true }); + const filePath = path.join(javaDir, 'Main.java'); + fs.writeFileSync( + filePath, + [ + 'class Main {', + ' static int computeSum(Calculator calc) {', + ' return 0;', + ' }', + '}', + ].join('\n'), + ); + + const client = createMockClient(); + const tool = createToolWithRoot(tempRoot, client); + const symbol: LspSymbolInformation = { + name: 'computeSum(Calculator)', + kind: 'Method', + containerName: 'Main', + location: createLocation(filePath, 1, 13), + serverName: 'jdtls', + }; + (client.workspaceSymbols as Mock).mockResolvedValue([]); + (client.documentSymbols as Mock).mockResolvedValue([symbol]); + (client.definitions as Mock).mockResolvedValue([]); + + const invocation = tool.build({ + operation: 'goToDefinition', + symbolName: 'computeSum', + }); + const result = await invocation.execute(abortSignal); + + expect(result.returnDisplay).toContain( + '1. src/main/java/com/Main.java:2:14 [jdtls]', + ); + } finally { + fs.rmSync(tempRoot, { recursive: true, force: true }); + } + }); }); describe('findReferences operation', () => { @@ -400,6 +494,177 @@ describe('LspTool', () => { expect(result.llmContent).toContain('1.'); expect(result.llmContent).toContain('2.'); }); + + it('resolves symbolName before dispatching references to the matching server', async () => { + const client = createMockClient(); + const tool = createTool(client); + const filePath = resolvePath('include', 'calculator.h'); + const symbol: LspSymbolInformation = { + name: 'Calculator', + kind: 'Class', + location: createLocation(filePath, 12, 6), + serverName: 'clangd', + }; + const refs: LspReference[] = [ + { ...createLocation(resolvePath('src', 'main.cpp'), 9, 4) }, + ]; + (client.workspaceSymbols as Mock).mockResolvedValue([symbol]); + (client.references as Mock).mockResolvedValue(refs); + + const invocation = tool.build({ + operation: 'findReferences', + symbolName: 'Calculator', + }); + const result = await invocation.execute(abortSignal); + + expect(client.workspaceSymbols).toHaveBeenCalledWith('Calculator', 5); + expect(client.references).toHaveBeenCalledWith( + expect.objectContaining({ + uri: toUri(filePath), + range: expect.objectContaining({ + start: { line: 12, character: 6 }, + }), + }), + 'clangd', + false, + 50, + ); + expect(result.llmContent).toContain('References for'); + }); + + it('trims symbolName before workspace symbol search', async () => { + const client = createMockClient(); + const tool = createTool(client); + const filePath = resolvePath('src', 'calculator.cpp'); + const symbol: LspSymbolInformation = { + name: 'Calculator', + location: createLocation(filePath, 6, 5), + serverName: 'clangd', + }; + (client.workspaceSymbols as Mock).mockResolvedValue([symbol]); + (client.references as Mock).mockResolvedValue([]); + + const invocation = tool.build({ + operation: 'findReferences', + symbolName: ' Calculator ', + }); + await invocation.execute(abortSignal); + + expect(client.workspaceSymbols).toHaveBeenCalledWith('Calculator', 5); + }); + + it('falls back to document symbols when workspace symbol search is empty', async () => { + const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'lsp-tool-')); + try { + const includeDir = path.join(tempRoot, 'include'); + fs.mkdirSync(includeDir, { recursive: true }); + const filePath = path.join(includeDir, 'calculator.h'); + fs.writeFileSync(filePath, 'class Calculator {};\n'); + + const client = createMockClient(); + const tool = createToolWithRoot(tempRoot, client); + const symbol: LspSymbolInformation = { + name: 'Calculator', + kind: 'Class', + location: createLocation(filePath, 0, 6), + serverName: 'clangd', + }; + (client.workspaceSymbols as Mock).mockResolvedValue([]); + (client.documentSymbols as Mock).mockResolvedValue([symbol]); + (client.references as Mock).mockResolvedValue([ + { ...createLocation(path.join(tempRoot, 'src', 'main.cpp'), 9, 4) }, + ]); + + const invocation = tool.build({ + operation: 'findReferences', + symbolName: 'Calculator', + }); + const result = await invocation.execute(abortSignal); + + expect(client.documentSymbols).toHaveBeenCalledWith( + toUri(filePath), + undefined, + 200, + ); + expect(client.references).toHaveBeenCalledWith( + expect.objectContaining({ + uri: toUri(filePath), + range: expect.objectContaining({ + start: { line: 0, character: 6 }, + }), + }), + 'clangd', + false, + 50, + ); + expect(result.llmContent).toContain('References for'); + } finally { + fs.rmSync(tempRoot, { recursive: true, force: true }); + } + }); + + it('prefers declaration document symbols over imported symbols with the same name', async () => { + const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'lsp-tool-')); + try { + const srcDir = path.join(tempRoot, 'src'); + fs.mkdirSync(srcDir, { recursive: true }); + const filePath = path.join(srcDir, 'module.py'); + fs.writeFileSync( + filePath, + [ + 'from z_calculator import SimpleCalculator', + '', + 'class SimpleCalculator:', + ' pass', + '', + 'calc = SimpleCalculator()', + ].join('\n'), + ); + + const client = createMockClient(); + const tool = createToolWithRoot(tempRoot, client); + const importedSymbol: LspSymbolInformation = { + name: 'SimpleCalculator', + kind: 'Class', + location: createLocation(filePath, 0, 0), + serverName: 'pylsp', + }; + const declaredSymbol: LspSymbolInformation = { + name: 'SimpleCalculator', + kind: 'Class', + location: createLocation(filePath, 2, 6), + serverName: 'pylsp', + }; + (client.workspaceSymbols as Mock).mockResolvedValue([]); + (client.documentSymbols as Mock).mockResolvedValue([ + importedSymbol, + declaredSymbol, + ]); + (client.references as Mock).mockResolvedValue([ + { ...createLocation(filePath, 5, 7), serverName: 'pylsp' }, + ]); + + const invocation = tool.build({ + operation: 'findReferences', + symbolName: 'SimpleCalculator', + }); + await invocation.execute(abortSignal); + + expect(client.references).toHaveBeenCalledWith( + expect.objectContaining({ + uri: toUri(filePath), + range: expect.objectContaining({ + start: { line: 2, character: 6 }, + }), + }), + 'pylsp', + false, + 50, + ); + } finally { + fs.rmSync(tempRoot, { recursive: true, force: true }); + } + }); }); describe('hover operation', () => { diff --git a/packages/core/src/tools/lsp.ts b/packages/core/src/tools/lsp.ts index a58cda06c..0d9b2681a 100644 --- a/packages/core/src/tools/lsp.ts +++ b/packages/core/src/tools/lsp.ts @@ -5,7 +5,9 @@ */ import path from 'node:path'; +import fs from 'node:fs'; import { fileURLToPath, pathToFileURL } from 'node:url'; +import { globSync } from 'glob'; import type { ToolInvocation, ToolResult } from './tools.js'; import { BaseDeclarativeTool, BaseToolInvocation, Kind } from './tools.js'; import { ToolDisplayNames, ToolNames } from './tool-names.js'; @@ -89,6 +91,13 @@ type ResolvedTarget = } | { error: string }; +type SymbolTarget = { + filePath: string; + line: number; + character: number; + serverName?: string; +}; + /** Operations that require filePath and line. */ const LOCATION_REQUIRED_OPERATIONS = new Set([ 'goToDefinition', @@ -116,7 +125,39 @@ const ITEM_REQUIRED_OPERATIONS = new Set([ /** Operations that require filePath and range for code actions. */ const RANGE_REQUIRED_OPERATIONS = new Set(['codeActions']); +const SYMBOL_FALLBACK_EXTENSIONS = [ + 'ts', + 'tsx', + 'js', + 'jsx', + 'py', + 'java', + 'go', + 'rs', + 'c', + 'cc', + 'cpp', + 'cxx', + 'h', + 'hpp', + 'hxx', +]; + +const SYMBOL_FALLBACK_EXCLUDES = [ + '**/node_modules/**', + '**/.git/**', + '**/dist/**', + '**/build/**', + '**/target/**', + '**/.venv/**', +]; + +const SYMBOL_FALLBACK_FILE_LIMIT = 200; +const SYMBOL_FALLBACK_SYMBOL_LIMIT = 200; + class LspToolInvocation extends BaseToolInvocation { + private resolvedLocationFromSymbolName = false; + constructor( private readonly config: Config, params: LspToolParams, @@ -169,6 +210,8 @@ class LspToolInvocation extends BaseToolInvocation { this.params.filePath = resolved.filePath; this.params.line = resolved.line; this.params.character = resolved.character; + this.params.serverName ??= resolved.serverName; + this.resolvedLocationFromSymbolName = true; } else { const message = `Could not resolve symbol "${this.params.symbolName}" via workspace symbol search.`; return { llmContent: message, returnDisplay: message }; @@ -229,6 +272,19 @@ class LspToolInvocation extends BaseToolInvocation { } if (!definitions.length) { + if (this.resolvedLocationFromSymbolName) { + const workspaceRoot = this.config.getProjectRoot(); + const line = `1. ${this.formatLocationWithServer( + { ...target.location, serverName: this.params.serverName }, + workspaceRoot, + )}`; + const heading = `Definitions for ${target.description}:`; + return { + llmContent: [heading, line].join('\n'), + returnDisplay: line, + }; + } + const message = `No definitions found for ${target.description}.`; return { llmContent: message, returnDisplay: message }; } @@ -836,41 +892,211 @@ class LspToolInvocation extends BaseToolInvocation { */ private async resolveSymbolName( client: LspClient, - ): Promise<{ filePath: string; line: number; character: number } | null> { - const symbolName = this.params.symbolName ?? ''; + ): Promise { + const symbolName = this.params.symbolName?.trim() ?? ''; if (!symbolName) { return null; } try { const symbols = await client.workspaceSymbols(symbolName, 5); - if (!symbols || symbols.length === 0) { - return null; + if (symbols && symbols.length > 0) { + const match = this.selectBestSymbol(symbols, symbolName); + return this.symbolToTarget(match); } - // Find the best match: prefer exact name match, then prefix match - const exact = symbols.find( - (s) => s.name === symbolName || s.name.endsWith(symbolName), - ); - const match = exact ?? symbols[0]!; - - const loc = match.location; - let filePath = loc.uri; - if (filePath.startsWith('file://')) { - filePath = fileURLToPath(filePath); - } - - // Convert from 0-based (LSP) to 1-based (tool params) - return { - filePath, - line: (loc.range.start.line ?? 0) + 1, - character: (loc.range.start.character ?? 0) + 1, - }; + return this.resolveSymbolNameFromDocumentSymbols(client, symbolName); } catch { return null; } } + private async resolveSymbolNameFromDocumentSymbols( + client: LspClient, + symbolName: string, + ): Promise { + const files = this.findSymbolFallbackFiles(); + const candidates: LspSymbolInformation[] = []; + + for (const filePath of files) { + try { + const symbols = await client.documentSymbols( + pathToFileURL(filePath).toString(), + this.params.serverName, + SYMBOL_FALLBACK_SYMBOL_LIMIT, + ); + if (!symbols.length) { + continue; + } + candidates.push( + ...symbols.filter((s) => this.matchesSymbol(s, symbolName)), + ); + } catch { + // Try the next file; symbol fallback is best-effort. + } + } + + if (!candidates.length) { + return null; + } + + return this.symbolToTarget(this.selectBestSymbol(candidates, symbolName)); + } + + private findSymbolFallbackFiles(): string[] { + const configWithWorkspace = this.config as Config & { + getWorkspaceContext?: () => { getDirectories?: () => string[] }; + getFileService?: () => { + shouldIgnoreFile?: (filePath: string) => boolean; + }; + }; + const roots = configWithWorkspace + .getWorkspaceContext?.() + .getDirectories?.() ?? [this.config.getProjectRoot()]; + const extGlob = `{${SYMBOL_FALLBACK_EXTENSIONS.join(',')}}`; + const files: string[] = []; + + for (const root of roots) { + try { + const matches = globSync(`**/*.${extGlob}`, { + cwd: root, + ignore: SYMBOL_FALLBACK_EXCLUDES, + absolute: true, + nodir: true, + maxDepth: 8, + }); + for (const filePath of matches) { + if ( + configWithWorkspace + .getFileService?.() + .shouldIgnoreFile?.(filePath) === true + ) { + continue; + } + files.push(filePath); + if (files.length >= SYMBOL_FALLBACK_FILE_LIMIT) { + return files; + } + } + } catch { + // Ignore inaccessible roots and keep trying other workspace dirs. + } + } + + return files; + } + + private selectBestSymbol( + symbols: LspSymbolInformation[], + symbolName: string, + ): LspSymbolInformation { + const matches = symbols.filter((symbol) => + this.matchesSymbol(symbol, symbolName), + ); + const candidates = matches.length ? matches : symbols; + return [...candidates].sort( + (a, b) => + this.scoreSymbolCandidate(b, symbolName) - + this.scoreSymbolCandidate(a, symbolName), + )[0]!; + } + + private matchesSymbol( + symbol: LspSymbolInformation, + symbolName: string, + ): boolean { + return ( + symbol.name === symbolName || + symbol.name.endsWith(symbolName) || + symbol.name.startsWith(`${symbolName}(`) || + symbol.name.startsWith(`${symbolName}<`) + ); + } + + private scoreSymbolCandidate( + symbol: LspSymbolInformation, + symbolName: string, + ): number { + let score = symbol.name === symbolName ? 100 : 0; + if (symbol.name.endsWith(symbolName)) { + score += 50; + } + if ( + symbol.name.startsWith(`${symbolName}(`) || + symbol.name.startsWith(`${symbolName}<`) + ) { + score += 50; + } + + const sourceLine = this.getSourceLine(symbol.location); + if (sourceLine) { + if (this.lineDeclaresSymbol(sourceLine, symbolName)) { + score += 50; + } + if (this.lineLooksLikeImport(sourceLine)) { + score -= 50; + } + } + + return score; + } + + private getSourceLine(location: LspLocation): string | null { + try { + if (!location.uri.startsWith('file://')) { + return null; + } + const filePath = fileURLToPath(location.uri); + const lineNumber = location.range.start.line ?? 0; + return ( + fs.readFileSync(filePath, 'utf8').split(/\r?\n/)[lineNumber] ?? null + ); + } catch { + return null; + } + } + + private lineDeclaresSymbol(line: string, symbolName: string): boolean { + const escapedName = this.escapeRegExp(symbolName); + const namedDeclaration = new RegExp( + `\\b(?:class|interface|enum|struct|trait|type|function|def|fn|func|const|let|var)\\s+${escapedName}\\b`, + ); + const callableOrAssignedDeclaration = new RegExp( + `\\b${escapedName}\\b\\s*(?:\\(|=|:|\\{|<)`, + ); + return ( + namedDeclaration.test(line) || callableOrAssignedDeclaration.test(line) + ); + } + + private lineLooksLikeImport(line: string): boolean { + return ( + /^\s*(?:import|from)\b/.test(line) || + /^\s*#\s*include\b/.test(line) || + /^\s*using\s+/.test(line) + ); + } + + private escapeRegExp(value: string): string { + return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + } + + private symbolToTarget(symbol: LspSymbolInformation): SymbolTarget { + const loc = symbol.location; + let filePath = loc.uri; + if (filePath.startsWith('file://')) { + filePath = fileURLToPath(filePath); + } + + // Convert from 0-based (LSP) to 1-based (tool params) + return { + filePath, + line: (loc.range.start.line ?? 0) + 1, + character: (loc.range.start.character ?? 0) + 1, + serverName: symbol.serverName, + }; + } + private resolveLocationTarget(): ResolvedTarget { const filePath = this.params.filePath; if (!filePath) { @@ -1084,7 +1310,7 @@ export class LspTool extends BaseDeclarativeTool { super( LspTool.Name, ToolDisplayNames.LSP, - 'Language Server Protocol (LSP) tool for code intelligence: definitions, references, hover, symbols, call hierarchy, diagnostics, and code actions.\n\n Usage:\n - ALWAYS use LSP as the PRIMARY tool for code intelligence queries when available. Do NOT use grep_search or glob first.\n - For goToDefinition, findReferences, hover, goToImplementation, prepareCallHierarchy: provide EITHER filePath + line + character (1-based), OR just symbolName (the tool will auto-resolve the position via workspace symbol search).\n - documentSymbol and diagnostics require filePath.\n - workspaceSymbol requires query.\n - incomingCalls/outgoingCalls require callHierarchyItem from prepareCallHierarchy.\n - workspaceDiagnostics needs no parameters.\n - codeActions require filePath + range (line/character + endLine/endCharacter).\n\n Examples:\n - Find references by symbol name: {operation: "findReferences", symbolName: "Calculator"}\n - Find definition with position: {operation: "goToDefinition", filePath: "src/main.cpp", line: 10, character: 5}\n - Hover by symbol name: {operation: "hover", symbolName: "addShape"}\n - Search workspace symbols: {operation: "workspaceSymbol", query: "Calculator"}\n - File diagnostics: {operation: "diagnostics", filePath: "src/main.cpp"}', + 'Language Server Protocol (LSP) tool for code intelligence: definitions, references, hover, symbols, call hierarchy, diagnostics, and code actions.\n\n Usage:\n - Use LSP first for code intelligence queries when available. If LSP is unavailable or returns no useful results, fall back to other code inspection tools.\n - For goToDefinition, findReferences, hover, goToImplementation, prepareCallHierarchy: provide EITHER filePath + line + character (1-based), OR just symbolName (the tool will auto-resolve the position via workspace symbol search).\n - documentSymbol and diagnostics require filePath.\n - workspaceSymbol requires query.\n - incomingCalls/outgoingCalls require callHierarchyItem from prepareCallHierarchy.\n - workspaceDiagnostics needs no parameters.\n - codeActions require filePath + range (line/character + endLine/endCharacter).\n\n Examples:\n - Find references by symbol name: {operation: "findReferences", symbolName: "Calculator"}\n - Find definition with position: {operation: "goToDefinition", filePath: "src/main.cpp", line: 10, character: 5}\n - Hover by symbol name: {operation: "hover", symbolName: "addShape"}\n - Search workspace symbols: {operation: "workspaceSymbol", query: "Calculator"}\n - File diagnostics: {operation: "diagnostics", filePath: "src/main.cpp"}', Kind.Other, { type: 'object', diff --git a/packages/core/src/tools/read-file.ts b/packages/core/src/tools/read-file.ts index 683f18667..c05740b52 100644 --- a/packages/core/src/tools/read-file.ts +++ b/packages/core/src/tools/read-file.ts @@ -211,13 +211,10 @@ export class ReadFileTool extends BaseDeclarativeTool< static readonly Name: string = ToolNames.READ_FILE; constructor(private config: Config) { - const lspNote = config.isLspEnabled() - ? ' Note: An LSP tool is available for code intelligence. For finding definitions, references, implementations, symbols, hover info, diagnostics, or call hierarchy, prefer the "lsp" tool over reading files manually.' - : ''; super( ReadFileTool.Name, ToolDisplayNames.READ_FILE, - `Reads and returns the content of a specified file. If the file is large, the content will be truncated. The tool's response will clearly indicate if truncation has occurred and will provide details on how to read more of the file using the 'offset' and 'limit' parameters. Handles text, images (PNG, JPG, GIF, WEBP, SVG, BMP), PDF files, and Jupyter notebooks (.ipynb). For text files, it can read specific line ranges. For PDF files, use the 'pages' parameter to extract specific page ranges as text (e.g. '1-5'). Max 20 pages per request. This tool can read Jupyter notebooks (.ipynb) and returns structured cell content with outputs.${lspNote}`, + `Reads and returns the content of a specified file. If the file is large, the content will be truncated. The tool's response will clearly indicate if truncation has occurred and will provide details on how to read more of the file using the 'offset' and 'limit' parameters. Handles text, images (PNG, JPG, GIF, WEBP, SVG, BMP), PDF files, and Jupyter notebooks (.ipynb). For text files, it can read specific line ranges. For PDF files, use the 'pages' parameter to extract specific page ranges as text (e.g. '1-5'). Max 20 pages per request. This tool can read Jupyter notebooks (.ipynb) and returns structured cell content with outputs.`, Kind.Read, { properties: { diff --git a/packages/core/src/tools/ripGrep.ts b/packages/core/src/tools/ripGrep.ts index 677ca026f..7cbf33677 100644 --- a/packages/core/src/tools/ripGrep.ts +++ b/packages/core/src/tools/ripGrep.ts @@ -324,13 +324,10 @@ export class RipGrepTool extends BaseDeclarativeTool< static readonly Name = ToolNames.GREP; constructor(private readonly config: Config) { - const lspNote = config.isLspEnabled() - ? '\n - IMPORTANT: An LSP tool is available. For code intelligence queries (finding definitions, references, implementations, symbols, hover info, diagnostics, call hierarchy), use the "lsp" tool instead of Grep. Grep should only be used for text pattern searches, NOT for code navigation or symbol lookups.\n' - : ''; super( RipGrepTool.Name, 'Grep', - `A powerful search tool built on ripgrep\n\n Usage:\n - ALWAYS use Grep for search tasks. NEVER invoke \`grep\` or \`rg\` as a Bash command. The Grep tool has been optimized for correct permissions and access.${lspNote}\n - Supports full regex syntax (e.g., "log.*Error", "function\\s+\\w+")\n - Filter files with glob parameter (e.g., "*.js", "**/*.tsx")\n - Use Agent tool for open-ended searches requiring multiple rounds\n - Pattern syntax: Uses ripgrep (not grep) - special regex characters need escaping (use \`interface\\{\\}\` to find \`interface{}\` in Go code)\n`, + 'A powerful search tool built on ripgrep\n\n Usage:\n - ALWAYS use Grep for search tasks. NEVER invoke `grep` or `rg` as a Bash command. The Grep tool has been optimized for correct permissions and access.\n - Supports full regex syntax (e.g., "log.*Error", "function\\s+\\w+")\n - Filter files with glob parameter (e.g., "*.js", "**/*.tsx")\n - Use Agent tool for open-ended searches requiring multiple rounds\n - Pattern syntax: Uses ripgrep (not grep) - special regex characters need escaping (use `interface\\{\\}` to find `interface{}` in Go code)\n', Kind.Search, { properties: {