diff --git a/packages/dora/src/providers/lsp/index.ts b/packages/dora/src/providers/lsp/index.ts index 027bed1..a0b3a25 100644 --- a/packages/dora/src/providers/lsp/index.ts +++ b/packages/dora/src/providers/lsp/index.ts @@ -54,6 +54,21 @@ const LSP_TOOLS: ToolDefinition[] = [ file: z.string().describe('Path to the file to analyze'), }), }, + { + name: 'lsp_references', + description: + 'Find all references to a symbol at a specific position in a file. ' + + 'Returns locations where the symbol is used across the workspace.', + inputSchema: z.object({ + file: z.string().describe('Path to the file'), + line: z.number().describe('Line number (0-indexed)'), + character: z.number().describe('Character position (0-indexed)'), + include_declaration: z + .boolean() + .optional() + .describe('Include the symbol\'s declaration in results (default: false)'), + }), + }, { name: 'lsp_status', description: @@ -119,6 +134,8 @@ export class LSPProvider implements Provider { return await this.handleWorkspaceSymbol(args) case 'lsp_document_symbol': return await this.handleDocumentSymbol(args) + case 'lsp_references': + return await this.handleReferences(args) case 'lsp_status': return await this.handleStatus() default: @@ -235,6 +252,41 @@ export class LSPProvider implements Provider { } } + private async handleReferences(args: unknown): Promise { + const parsed = z + .object({ + file: z.string(), + line: z.number(), + character: z.number(), + include_declaration: z.boolean().optional(), + }) + .parse(args) + + const filePath = path.isAbsolute(parsed.file) + ? parsed.file + : path.join(this.config.projectPath, parsed.file) + + await this.manager!.touchFile(filePath, true) + const references = await this.manager!.references({ + file: filePath, + line: parsed.line, + character: parsed.character, + includeDeclaration: parsed.include_declaration ?? false, + }) + + if (!references.length) { + return { + content: [ + { type: 'text', text: 'No references found at this position' }, + ], + } + } + + return { + content: [{ type: 'text', text: JSON.stringify(references, null, 2) }], + } + } + private async handleStatus(): Promise { const status = await this.manager!.status() diff --git a/packages/dora/test/lsp-provider.test.ts b/packages/dora/test/lsp-provider.test.ts index d21e207..42e2c9f 100644 --- a/packages/dora/test/lsp-provider.test.ts +++ b/packages/dora/test/lsp-provider.test.ts @@ -48,6 +48,7 @@ describe('LSPProvider', () => { expect(toolNames).toContain('lsp_hover') expect(toolNames).toContain('lsp_workspace_symbol') expect(toolNames).toContain('lsp_document_symbol') + expect(toolNames).toContain('lsp_references') expect(toolNames).toContain('lsp_status') }) @@ -76,6 +77,33 @@ describe('LSPProvider', () => { expect(result.isError).toBe(true) expect(result.content[0]!.text).toContain('Unknown tool') }) + + test('lsp_references returns error when not connected', async () => { + provider = new LSPProvider({ projectPath: process.cwd() }) + + const result = await provider.callTool('lsp_references', { + file: 'test.ts', + line: 0, + character: 0, + }) + expect(result.isError).toBe(true) + expect(result.content[0]!.text).toContain('not connected') + }) + + test('lsp_references returns no references for non-symbol position', async () => { + provider = new LSPProvider({ projectPath: process.cwd() }) + await provider.connect() + + // Call on a position that doesn't have a symbol (no LSP servers connected) + const result = await provider.callTool('lsp_references', { + file: 'test.ts', + line: 0, + character: 0, + }) + // Without connected LSP servers, should return "No references found" + expect(result.isError).toBeFalsy() + expect(result.content[0]!.text).toContain('No references found') + }) }) describe('createLSPProvider', () => { diff --git a/specs/003-lsp-find-references/spec.md b/specs/003-lsp-find-references/spec.md new file mode 100644 index 0000000..6815aea --- /dev/null +++ b/specs/003-lsp-find-references/spec.md @@ -0,0 +1,116 @@ +# Specification: LSP Find References Tool + +**Spec Number**: 003 +**Feature Name**: lsp-find-references +**Created**: 2025-12-19 +**Status**: Draft + +## Summary + +Add an `lsp_references` MCP tool to the `@pleaseai/dora` package that finds all usages/references of a symbol at a given position in a source file using the Language Server Protocol. + +## Problem Statement + +Developers using AI coding assistants need to understand how symbols (functions, classes, variables) are used across a codebase. Currently, dora provides: +- `lsp_workspace_symbol` - Search by name +- `lsp_document_symbol` - Get file structure + +However, there's no way to find all references to a specific symbol at a cursor position, which is essential for: +- Understanding impact of changes +- Refactoring safely +- Navigating codebases +- Learning how APIs are used + +## Solution + +Add `lsp_references` tool that: +1. Takes a file path and cursor position (line, character) +2. Optionally includes the symbol's declaration in results +3. Returns all locations where the symbol is referenced + +## Functional Requirements + +### FR-1: Tool Definition +- **Name**: `lsp_references` +- **Input Parameters**: + - `file` (string, required): Path to the file (relative or absolute) + - `line` (number, required): Line number (0-indexed) + - `character` (number, required): Character position (0-indexed) + - `include_declaration` (boolean, optional, default: false): Include the symbol's declaration + +### FR-2: Output Format +Returns JSON array of Location objects: +```json +[ + { + "uri": "file:///path/to/file.ts", + "range": { + "start": { "line": 10, "character": 5 }, + "end": { "line": 10, "character": 15 } + } + } +] +``` + +### FR-3: Empty Result Handling +When no references are found, return user-friendly message: +``` +No references found at this position +``` + +### FR-4: Path Resolution +- Support both relative and absolute paths +- Relative paths resolved from project root (config.projectPath) + +## Non-Functional Requirements + +### NFR-1: Consistency +Follow existing LSP tool patterns in `packages/dora/src/providers/lsp/index.ts`: +- Zod schema validation +- Path normalization +- Error handling with isError flag +- JSON stringified output + +### NFR-2: Performance +- Leverage existing LSPManager connection pooling +- No additional overhead beyond LSP protocol + +## Implementation Notes + +### Existing Infrastructure +- `LSPManager.references()` already implemented in `@pleaseai/code-lsp` +- Returns `Location[]` from all connected language servers +- Handles multi-server aggregation internally + +### Integration Points +1. Add tool definition to `LSP_TOOLS` array +2. Add case to `callTool` switch +3. Implement `handleReferences` method +4. Add unit test + +## Test Cases + +### TC-1: Tool Listing +Verify `lsp_references` appears in listTools() output + +### TC-2: Not Connected Error +Calling tool before connect() returns error with "not connected" + +### TC-3: Unknown Position +Calling tool on non-symbol position returns "No references found" + +## Out of Scope + +- Go to Definition (separate feature) +- Find Implementations (separate feature) +- Code completion (separate feature) +- Symbol renaming (separate feature) + +## Acceptance Criteria + +1. `lsp_references` tool available via MCP +2. Returns correct reference locations from LSP servers +3. Handles empty results gracefully +4. Handles relative and absolute paths +5. Unit tests pass +6. Documentation updated (CLAUDE.md)