Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions packages/dora/src/providers/lsp/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -235,6 +252,41 @@ export class LSPProvider implements Provider {
}
}

private async handleReferences(args: unknown): Promise<ToolResult> {
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<ToolResult> {
const status = await this.manager!.status()

Expand Down
28 changes: 28 additions & 0 deletions packages/dora/test/lsp-provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
})

Expand Down Expand Up @@ -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', () => {
Expand Down
116 changes: 116 additions & 0 deletions specs/003-lsp-find-references/spec.md
Original file line number Diff line number Diff line change
@@ -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)