From 5ea989fb74ab1c286fbdeadc9d5a2860361d4f95 Mon Sep 17 00:00:00 2001 From: adamdottv <2363879+adamdottv@users.noreply.github.com> Date: Wed, 14 May 2025 14:40:45 -0500 Subject: feat: docSymbols and workspaceSymbols tools --- internal/llm/agent/tools.go | 4 + internal/llm/tools/definition.go | 198 ------------------- internal/llm/tools/diagnostics.go | 296 ---------------------------- internal/llm/tools/lsp_definition.go | 198 +++++++++++++++++++ internal/llm/tools/lsp_diagnostics.go | 296 ++++++++++++++++++++++++++++ internal/llm/tools/lsp_doc_symbols.go | 204 +++++++++++++++++++ internal/llm/tools/lsp_references.go | 161 +++++++++++++++ internal/llm/tools/lsp_workspace_symbols.go | 162 +++++++++++++++ internal/llm/tools/references.go | 161 --------------- 9 files changed, 1025 insertions(+), 655 deletions(-) delete mode 100644 internal/llm/tools/definition.go delete mode 100644 internal/llm/tools/diagnostics.go create mode 100644 internal/llm/tools/lsp_definition.go create mode 100644 internal/llm/tools/lsp_diagnostics.go create mode 100644 internal/llm/tools/lsp_doc_symbols.go create mode 100644 internal/llm/tools/lsp_references.go create mode 100644 internal/llm/tools/lsp_workspace_symbols.go delete mode 100644 internal/llm/tools/references.go diff --git a/internal/llm/agent/tools.go b/internal/llm/agent/tools.go index b27ecc245..a0e835ff0 100644 --- a/internal/llm/agent/tools.go +++ b/internal/llm/agent/tools.go @@ -35,6 +35,8 @@ func PrimaryAgentTools( tools.NewDiagnosticsTool(lspClients), tools.NewDefinitionTool(lspClients), tools.NewReferencesTool(lspClients), + tools.NewDocSymbolsTool(lspClients), + tools.NewWorkspaceSymbolsTool(lspClients), NewAgentTool(sessions, messages, lspClients), }, mcpTools..., ) @@ -48,5 +50,7 @@ func TaskAgentTools(lspClients map[string]*lsp.Client) []tools.BaseTool { tools.NewViewTool(lspClients), tools.NewDefinitionTool(lspClients), tools.NewReferencesTool(lspClients), + tools.NewDocSymbolsTool(lspClients), + tools.NewWorkspaceSymbolsTool(lspClients), } } diff --git a/internal/llm/tools/definition.go b/internal/llm/tools/definition.go deleted file mode 100644 index e7dc50425..000000000 --- a/internal/llm/tools/definition.go +++ /dev/null @@ -1,198 +0,0 @@ -package tools - -import ( - "context" - "encoding/json" - "fmt" - "log/slog" - "strings" - - "github.com/sst/opencode/internal/lsp" - "github.com/sst/opencode/internal/lsp/protocol" -) - -type DefinitionParams struct { - FilePath string `json:"file_path"` - Line int `json:"line"` - Column int `json:"column"` -} - -type definitionTool struct { - lspClients map[string]*lsp.Client -} - -const ( - DefinitionToolName = "definition" - definitionDescription = `Find the definition of a symbol at a specific position in a file. -WHEN TO USE THIS TOOL: -- Use when you need to find where a symbol is defined -- Helpful for understanding code structure and relationships -- Great for navigating between implementation and interface - -HOW TO USE: -- Provide the path to the file containing the symbol -- Specify the line number (1-based) where the symbol appears -- Specify the column number (1-based) where the symbol appears -- Results show the location of the symbol's definition - -FEATURES: -- Finds definitions across files in the project -- Works with variables, functions, classes, interfaces, etc. -- Returns file path, line, and column of the definition - -LIMITATIONS: -- Requires a functioning LSP server for the file type -- May not work for all symbols depending on LSP capabilities -- Results depend on the accuracy of the LSP server - -TIPS: -- Use in conjunction with References tool to understand usage -- Combine with View tool to examine the definition -` -) - -func NewDefinitionTool(lspClients map[string]*lsp.Client) BaseTool { - return &definitionTool{ - lspClients, - } -} - -func (b *definitionTool) Info() ToolInfo { - return ToolInfo{ - Name: DefinitionToolName, - Description: definitionDescription, - Parameters: map[string]any{ - "file_path": map[string]any{ - "type": "string", - "description": "The path to the file containing the symbol", - }, - "line": map[string]any{ - "type": "integer", - "description": "The line number (1-based) where the symbol appears", - }, - "column": map[string]any{ - "type": "integer", - "description": "The column number (1-based) where the symbol appears", - }, - }, - Required: []string{"file_path", "line", "column"}, - } -} - -func (b *definitionTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error) { - var params DefinitionParams - if err := json.Unmarshal([]byte(call.Input), ¶ms); err != nil { - return NewTextErrorResponse(fmt.Sprintf("error parsing parameters: %s", err)), nil - } - - lsps := b.lspClients - - if len(lsps) == 0 { - return NewTextResponse("\nLSP clients are still initializing. Definition lookup will be available once they're ready.\n"), nil - } - - // Ensure file is open in LSP - notifyLspOpenFile(ctx, params.FilePath, lsps) - - // Convert 1-based line/column to 0-based for LSP protocol - line := max(0, params.Line-1) - column := max(0, params.Column-1) - - output := getDefinition(ctx, params.FilePath, line, column, lsps) - - return NewTextResponse(output), nil -} - -func getDefinition(ctx context.Context, filePath string, line, column int, lsps map[string]*lsp.Client) string { - var results []string - - slog.Debug(fmt.Sprintf("Looking for definition in %s at line %d, column %d", filePath, line+1, column+1)) - slog.Debug(fmt.Sprintf("Available LSP clients: %v", getClientNames(lsps))) - - for lspName, client := range lsps { - slog.Debug(fmt.Sprintf("Trying LSP client: %s", lspName)) - // Create definition params - uri := fmt.Sprintf("file://%s", filePath) - definitionParams := protocol.DefinitionParams{ - TextDocumentPositionParams: protocol.TextDocumentPositionParams{ - TextDocument: protocol.TextDocumentIdentifier{ - URI: protocol.DocumentUri(uri), - }, - Position: protocol.Position{ - Line: uint32(line), - Character: uint32(column), - }, - }, - } - slog.Debug(fmt.Sprintf("Sending definition request with params: %+v", definitionParams)) - - // Get definition - definition, err := client.Definition(ctx, definitionParams) - if err != nil { - slog.Debug(fmt.Sprintf("Error from %s: %s", lspName, err)) - results = append(results, fmt.Sprintf("Error from %s: %s", lspName, err)) - continue - } - slog.Debug(fmt.Sprintf("Got definition result type: %T", definition.Value)) - - // Process the definition result - locations := processDefinitionResult(definition) - slog.Debug(fmt.Sprintf("Processed locations count: %d", len(locations))) - if len(locations) == 0 { - results = append(results, fmt.Sprintf("No definition found by %s", lspName)) - continue - } - - // Format the locations - for _, loc := range locations { - path := strings.TrimPrefix(string(loc.URI), "file://") - // Convert 0-based line/column to 1-based for display - defLine := loc.Range.Start.Line + 1 - defColumn := loc.Range.Start.Character + 1 - slog.Debug(fmt.Sprintf("Found definition at %s:%d:%d", path, defLine, defColumn)) - results = append(results, fmt.Sprintf("Definition found by %s: %s:%d:%d", lspName, path, defLine, defColumn)) - } - } - - if len(results) == 0 { - return "No definition found for the symbol at the specified position." - } - - return strings.Join(results, "\n") -} - -func processDefinitionResult(result protocol.Or_Result_textDocument_definition) []protocol.Location { - var locations []protocol.Location - - switch v := result.Value.(type) { - case protocol.Location: - locations = append(locations, v) - case []protocol.Location: - locations = append(locations, v...) - case []protocol.DefinitionLink: - for _, link := range v { - locations = append(locations, protocol.Location{ - URI: link.TargetURI, - Range: link.TargetRange, - }) - } - case protocol.Or_Definition: - switch d := v.Value.(type) { - case protocol.Location: - locations = append(locations, d) - case []protocol.Location: - locations = append(locations, d...) - } - } - - return locations -} - -// Helper function to get LSP client names for debugging -func getClientNames(lsps map[string]*lsp.Client) []string { - names := make([]string, 0, len(lsps)) - for name := range lsps { - names = append(names, name) - } - return names -} diff --git a/internal/llm/tools/diagnostics.go b/internal/llm/tools/diagnostics.go deleted file mode 100644 index a1ed33b6a..000000000 --- a/internal/llm/tools/diagnostics.go +++ /dev/null @@ -1,296 +0,0 @@ -package tools - -import ( - "context" - "encoding/json" - "fmt" - "maps" - "sort" - "strings" - "time" - - "github.com/sst/opencode/internal/lsp" - "github.com/sst/opencode/internal/lsp/protocol" -) - -type DiagnosticsParams struct { - FilePath string `json:"file_path"` -} -type diagnosticsTool struct { - lspClients map[string]*lsp.Client -} - -const ( - DiagnosticsToolName = "diagnostics" - diagnosticsDescription = `Get diagnostics for a file and/or project. -WHEN TO USE THIS TOOL: -- Use when you need to check for errors or warnings in your code -- Helpful for debugging and ensuring code quality -- Good for getting a quick overview of issues in a file or project -HOW TO USE: -- Provide a path to a file to get diagnostics for that file -- Leave the path empty to get diagnostics for the entire project -- Results are displayed in a structured format with severity levels -FEATURES: -- Displays errors, warnings, and hints -- Groups diagnostics by severity -- Provides detailed information about each diagnostic -LIMITATIONS: -- Results are limited to the diagnostics provided by the LSP clients -- May not cover all possible issues in the code -- Does not provide suggestions for fixing issues -TIPS: -- Use in conjunction with other tools for a comprehensive code review -- Combine with the LSP client for real-time diagnostics -` -) - -func NewDiagnosticsTool(lspClients map[string]*lsp.Client) BaseTool { - return &diagnosticsTool{ - lspClients, - } -} - -func (b *diagnosticsTool) Info() ToolInfo { - return ToolInfo{ - Name: DiagnosticsToolName, - Description: diagnosticsDescription, - Parameters: map[string]any{ - "file_path": map[string]any{ - "type": "string", - "description": "The path to the file to get diagnostics for (leave w empty for project diagnostics)", - }, - }, - Required: []string{}, - } -} - -func (b *diagnosticsTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error) { - var params DiagnosticsParams - if err := json.Unmarshal([]byte(call.Input), ¶ms); err != nil { - return NewTextErrorResponse(fmt.Sprintf("error parsing parameters: %s", err)), nil - } - - lsps := b.lspClients - - if len(lsps) == 0 { - // Return a more helpful message when LSP clients aren't ready yet - return NewTextResponse("\n\nLSP clients are still initializing. Diagnostics will be available once they're ready.\n\n"), nil - } - - if params.FilePath != "" { - notifyLspOpenFile(ctx, params.FilePath, lsps) - waitForLspDiagnostics(ctx, params.FilePath, lsps) - } - - output := getDiagnostics(params.FilePath, lsps) - - return NewTextResponse(output), nil -} - -func notifyLspOpenFile(ctx context.Context, filePath string, lsps map[string]*lsp.Client) { - for _, client := range lsps { - err := client.OpenFile(ctx, filePath) - if err != nil { - continue - } - } -} - -func waitForLspDiagnostics(ctx context.Context, filePath string, lsps map[string]*lsp.Client) { - if len(lsps) == 0 { - return - } - - diagChan := make(chan struct{}, 1) - - for _, client := range lsps { - originalDiags := make(map[protocol.DocumentUri][]protocol.Diagnostic) - maps.Copy(originalDiags, client.GetDiagnostics()) - - handler := func(params json.RawMessage) { - lsp.HandleDiagnostics(client, params) - var diagParams protocol.PublishDiagnosticsParams - if err := json.Unmarshal(params, &diagParams); err != nil { - return - } - - if diagParams.URI.Path() == filePath || hasDiagnosticsChanged(client.GetDiagnostics(), originalDiags) { - select { - case diagChan <- struct{}{}: - default: - } - } - } - - client.RegisterNotificationHandler("textDocument/publishDiagnostics", handler) - - if client.IsFileOpen(filePath) { - err := client.NotifyChange(ctx, filePath) - if err != nil { - continue - } - } else { - err := client.OpenFile(ctx, filePath) - if err != nil { - continue - } - } - } - - select { - case <-diagChan: - case <-time.After(5 * time.Second): - case <-ctx.Done(): - } -} - -func hasDiagnosticsChanged(current, original map[protocol.DocumentUri][]protocol.Diagnostic) bool { - for uri, diags := range current { - origDiags, exists := original[uri] - if !exists || len(diags) != len(origDiags) { - return true - } - } - return false -} - -func getDiagnostics(filePath string, lsps map[string]*lsp.Client) string { - fileDiagnostics := []string{} - projectDiagnostics := []string{} - - formatDiagnostic := func(pth string, diagnostic protocol.Diagnostic, source string) string { - severity := "Info" - switch diagnostic.Severity { - case protocol.SeverityError: - severity = "Error" - case protocol.SeverityWarning: - severity = "Warn" - case protocol.SeverityHint: - severity = "Hint" - } - - location := fmt.Sprintf("%s:%d:%d", pth, diagnostic.Range.Start.Line+1, diagnostic.Range.Start.Character+1) - - sourceInfo := "" - if diagnostic.Source != "" { - sourceInfo = diagnostic.Source - } else if source != "" { - sourceInfo = source - } - - codeInfo := "" - if diagnostic.Code != nil { - codeInfo = fmt.Sprintf("[%v]", diagnostic.Code) - } - - tagsInfo := "" - if len(diagnostic.Tags) > 0 { - tags := []string{} - for _, tag := range diagnostic.Tags { - switch tag { - case protocol.Unnecessary: - tags = append(tags, "unnecessary") - case protocol.Deprecated: - tags = append(tags, "deprecated") - } - } - if len(tags) > 0 { - tagsInfo = fmt.Sprintf(" (%s)", strings.Join(tags, ", ")) - } - } - - return fmt.Sprintf("%s: %s [%s]%s%s %s", - severity, - location, - sourceInfo, - codeInfo, - tagsInfo, - diagnostic.Message) - } - - for lspName, client := range lsps { - diagnostics := client.GetDiagnostics() - if len(diagnostics) > 0 { - for location, diags := range diagnostics { - isCurrentFile := location.Path() == filePath - - for _, diag := range diags { - formattedDiag := formatDiagnostic(location.Path(), diag, lspName) - - if isCurrentFile { - fileDiagnostics = append(fileDiagnostics, formattedDiag) - } else { - projectDiagnostics = append(projectDiagnostics, formattedDiag) - } - } - } - } - } - - sort.Slice(fileDiagnostics, func(i, j int) bool { - iIsError := strings.HasPrefix(fileDiagnostics[i], "Error") - jIsError := strings.HasPrefix(fileDiagnostics[j], "Error") - if iIsError != jIsError { - return iIsError // Errors come first - } - return fileDiagnostics[i] < fileDiagnostics[j] // Then alphabetically - }) - - sort.Slice(projectDiagnostics, func(i, j int) bool { - iIsError := strings.HasPrefix(projectDiagnostics[i], "Error") - jIsError := strings.HasPrefix(projectDiagnostics[j], "Error") - if iIsError != jIsError { - return iIsError - } - return projectDiagnostics[i] < projectDiagnostics[j] - }) - - output := "" - - if len(fileDiagnostics) > 0 { - output += "\n\n" - if len(fileDiagnostics) > 10 { - output += strings.Join(fileDiagnostics[:10], "\n") - output += fmt.Sprintf("\n... and %d more diagnostics", len(fileDiagnostics)-10) - } else { - output += strings.Join(fileDiagnostics, "\n") - } - output += "\n\n" - } - - if len(projectDiagnostics) > 0 { - output += "\n\n" - if len(projectDiagnostics) > 10 { - output += strings.Join(projectDiagnostics[:10], "\n") - output += fmt.Sprintf("\n... and %d more diagnostics", len(projectDiagnostics)-10) - } else { - output += strings.Join(projectDiagnostics, "\n") - } - output += "\n\n" - } - - if len(fileDiagnostics) > 0 || len(projectDiagnostics) > 0 { - fileErrors := countSeverity(fileDiagnostics, "Error") - fileWarnings := countSeverity(fileDiagnostics, "Warn") - projectErrors := countSeverity(projectDiagnostics, "Error") - projectWarnings := countSeverity(projectDiagnostics, "Warn") - - output += "\n\n" - output += fmt.Sprintf("Current file: %d errors, %d warnings\n", fileErrors, fileWarnings) - output += fmt.Sprintf("Project: %d errors, %d warnings\n", projectErrors, projectWarnings) - output += "\n" - } - - return output -} - -func countSeverity(diagnostics []string, severity string) int { - count := 0 - for _, diag := range diagnostics { - if strings.HasPrefix(diag, severity) { - count++ - } - } - return count -} diff --git a/internal/llm/tools/lsp_definition.go b/internal/llm/tools/lsp_definition.go new file mode 100644 index 000000000..e7dc50425 --- /dev/null +++ b/internal/llm/tools/lsp_definition.go @@ -0,0 +1,198 @@ +package tools + +import ( + "context" + "encoding/json" + "fmt" + "log/slog" + "strings" + + "github.com/sst/opencode/internal/lsp" + "github.com/sst/opencode/internal/lsp/protocol" +) + +type DefinitionParams struct { + FilePath string `json:"file_path"` + Line int `json:"line"` + Column int `json:"column"` +} + +type definitionTool struct { + lspClients map[string]*lsp.Client +} + +const ( + DefinitionToolName = "definition" + definitionDescription = `Find the definition of a symbol at a specific position in a file. +WHEN TO USE THIS TOOL: +- Use when you need to find where a symbol is defined +- Helpful for understanding code structure and relationships +- Great for navigating between implementation and interface + +HOW TO USE: +- Provide the path to the file containing the symbol +- Specify the line number (1-based) where the symbol appears +- Specify the column number (1-based) where the symbol appears +- Results show the location of the symbol's definition + +FEATURES: +- Finds definitions across files in the project +- Works with variables, functions, classes, interfaces, etc. +- Returns file path, line, and column of the definition + +LIMITATIONS: +- Requires a functioning LSP server for the file type +- May not work for all symbols depending on LSP capabilities +- Results depend on the accuracy of the LSP server + +TIPS: +- Use in conjunction with References tool to understand usage +- Combine with View tool to examine the definition +` +) + +func NewDefinitionTool(lspClients map[string]*lsp.Client) BaseTool { + return &definitionTool{ + lspClients, + } +} + +func (b *definitionTool) Info() ToolInfo { + return ToolInfo{ + Name: DefinitionToolName, + Description: definitionDescription, + Parameters: map[string]any{ + "file_path": map[string]any{ + "type": "string", + "description": "The path to the file containing the symbol", + }, + "line": map[string]any{ + "type": "integer", + "description": "The line number (1-based) where the symbol appears", + }, + "column": map[string]any{ + "type": "integer", + "description": "The column number (1-based) where the symbol appears", + }, + }, + Required: []string{"file_path", "line", "column"}, + } +} + +func (b *definitionTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error) { + var params DefinitionParams + if err := json.Unmarshal([]byte(call.Input), ¶ms); err != nil { + return NewTextErrorResponse(fmt.Sprintf("error parsing parameters: %s", err)), nil + } + + lsps := b.lspClients + + if len(lsps) == 0 { + return NewTextResponse("\nLSP clients are still initializing. Definition lookup will be available once they're ready.\n"), nil + } + + // Ensure file is open in LSP + notifyLspOpenFile(ctx, params.FilePath, lsps) + + // Convert 1-based line/column to 0-based for LSP protocol + line := max(0, params.Line-1) + column := max(0, params.Column-1) + + output := getDefinition(ctx, params.FilePath, line, column, lsps) + + return NewTextResponse(output), nil +} + +func getDefinition(ctx context.Context, filePath string, line, column int, lsps map[string]*lsp.Client) string { + var results []string + + slog.Debug(fmt.Sprintf("Looking for definition in %s at line %d, column %d", filePath, line+1, column+1)) + slog.Debug(fmt.Sprintf("Available LSP clients: %v", getClientNames(lsps))) + + for lspName, client := range lsps { + slog.Debug(fmt.Sprintf("Trying LSP client: %s", lspName)) + // Create definition params + uri := fmt.Sprintf("file://%s", filePath) + definitionParams := protocol.DefinitionParams{ + TextDocumentPositionParams: protocol.TextDocumentPositionParams{ + TextDocument: protocol.TextDocumentIdentifier{ + URI: protocol.DocumentUri(uri), + }, + Position: protocol.Position{ + Line: uint32(line), + Character: uint32(column), + }, + }, + } + slog.Debug(fmt.Sprintf("Sending definition request with params: %+v", definitionParams)) + + // Get definition + definition, err := client.Definition(ctx, definitionParams) + if err != nil { + slog.Debug(fmt.Sprintf("Error from %s: %s", lspName, err)) + results = append(results, fmt.Sprintf("Error from %s: %s", lspName, err)) + continue + } + slog.Debug(fmt.Sprintf("Got definition result type: %T", definition.Value)) + + // Process the definition result + locations := processDefinitionResult(definition) + slog.Debug(fmt.Sprintf("Processed locations count: %d", len(locations))) + if len(locations) == 0 { + results = append(results, fmt.Sprintf("No definition found by %s", lspName)) + continue + } + + // Format the locations + for _, loc := range locations { + path := strings.TrimPrefix(string(loc.URI), "file://") + // Convert 0-based line/column to 1-based for display + defLine := loc.Range.Start.Line + 1 + defColumn := loc.Range.Start.Character + 1 + slog.Debug(fmt.Sprintf("Found definition at %s:%d:%d", path, defLine, defColumn)) + results = append(results, fmt.Sprintf("Definition found by %s: %s:%d:%d", lspName, path, defLine, defColumn)) + } + } + + if len(results) == 0 { + return "No definition found for the symbol at the specified position." + } + + return strings.Join(results, "\n") +} + +func processDefinitionResult(result protocol.Or_Result_textDocument_definition) []protocol.Location { + var locations []protocol.Location + + switch v := result.Value.(type) { + case protocol.Location: + locations = append(locations, v) + case []protocol.Location: + locations = append(locations, v...) + case []protocol.DefinitionLink: + for _, link := range v { + locations = append(locations, protocol.Location{ + URI: link.TargetURI, + Range: link.TargetRange, + }) + } + case protocol.Or_Definition: + switch d := v.Value.(type) { + case protocol.Location: + locations = append(locations, d) + case []protocol.Location: + locations = append(locations, d...) + } + } + + return locations +} + +// Helper function to get LSP client names for debugging +func getClientNames(lsps map[string]*lsp.Client) []string { + names := make([]string, 0, len(lsps)) + for name := range lsps { + names = append(names, name) + } + return names +} diff --git a/internal/llm/tools/lsp_diagnostics.go b/internal/llm/tools/lsp_diagnostics.go new file mode 100644 index 000000000..a1ed33b6a --- /dev/null +++ b/internal/llm/tools/lsp_diagnostics.go @@ -0,0 +1,296 @@ +package tools + +import ( + "context" + "encoding/json" + "fmt" + "maps" + "sort" + "strings" + "time" + + "github.com/sst/opencode/internal/lsp" + "github.com/sst/opencode/internal/lsp/protocol" +) + +type DiagnosticsParams struct { + FilePath string `json:"file_path"` +} +type diagnosticsTool struct { + lspClients map[string]*lsp.Client +} + +const ( + DiagnosticsToolName = "diagnostics" + diagnosticsDescription = `Get diagnostics for a file and/or project. +WHEN TO USE THIS TOOL: +- Use when you need to check for errors or warnings in your code +- Helpful for debugging and ensuring code quality +- Good for getting a quick overview of issues in a file or project +HOW TO USE: +- Provide a path to a file to get diagnostics for that file +- Leave the path empty to get diagnostics for the entire project +- Results are displayed in a structured format with severity levels +FEATURES: +- Displays errors, warnings, and hints +- Groups diagnostics by severity +- Provides detailed information about each diagnostic +LIMITATIONS: +- Results are limited to the diagnostics provided by the LSP clients +- May not cover all possible issues in the code +- Does not provide suggestions for fixing issues +TIPS: +- Use in conjunction with other tools for a comprehensive code review +- Combine with the LSP client for real-time diagnostics +` +) + +func NewDiagnosticsTool(lspClients map[string]*lsp.Client) BaseTool { + return &diagnosticsTool{ + lspClients, + } +} + +func (b *diagnosticsTool) Info() ToolInfo { + return ToolInfo{ + Name: DiagnosticsToolName, + Description: diagnosticsDescription, + Parameters: map[string]any{ + "file_path": map[string]any{ + "type": "string", + "description": "The path to the file to get diagnostics for (leave w empty for project diagnostics)", + }, + }, + Required: []string{}, + } +} + +func (b *diagnosticsTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error) { + var params DiagnosticsParams + if err := json.Unmarshal([]byte(call.Input), ¶ms); err != nil { + return NewTextErrorResponse(fmt.Sprintf("error parsing parameters: %s", err)), nil + } + + lsps := b.lspClients + + if len(lsps) == 0 { + // Return a more helpful message when LSP clients aren't ready yet + return NewTextResponse("\n\nLSP clients are still initializing. Diagnostics will be available once they're ready.\n\n"), nil + } + + if params.FilePath != "" { + notifyLspOpenFile(ctx, params.FilePath, lsps) + waitForLspDiagnostics(ctx, params.FilePath, lsps) + } + + output := getDiagnostics(params.FilePath, lsps) + + return NewTextResponse(output), nil +} + +func notifyLspOpenFile(ctx context.Context, filePath string, lsps map[string]*lsp.Client) { + for _, client := range lsps { + err := client.OpenFile(ctx, filePath) + if err != nil { + continue + } + } +} + +func waitForLspDiagnostics(ctx context.Context, filePath string, lsps map[string]*lsp.Client) { + if len(lsps) == 0 { + return + } + + diagChan := make(chan struct{}, 1) + + for _, client := range lsps { + originalDiags := make(map[protocol.DocumentUri][]protocol.Diagnostic) + maps.Copy(originalDiags, client.GetDiagnostics()) + + handler := func(params json.RawMessage) { + lsp.HandleDiagnostics(client, params) + var diagParams protocol.PublishDiagnosticsParams + if err := json.Unmarshal(params, &diagParams); err != nil { + return + } + + if diagParams.URI.Path() == filePath || hasDiagnosticsChanged(client.GetDiagnostics(), originalDiags) { + select { + case diagChan <- struct{}{}: + default: + } + } + } + + client.RegisterNotificationHandler("textDocument/publishDiagnostics", handler) + + if client.IsFileOpen(filePath) { + err := client.NotifyChange(ctx, filePath) + if err != nil { + continue + } + } else { + err := client.OpenFile(ctx, filePath) + if err != nil { + continue + } + } + } + + select { + case <-diagChan: + case <-time.After(5 * time.Second): + case <-ctx.Done(): + } +} + +func hasDiagnosticsChanged(current, original map[protocol.DocumentUri][]protocol.Diagnostic) bool { + for uri, diags := range current { + origDiags, exists := original[uri] + if !exists || len(diags) != len(origDiags) { + return true + } + } + return false +} + +func getDiagnostics(filePath string, lsps map[string]*lsp.Client) string { + fileDiagnostics := []string{} + projectDiagnostics := []string{} + + formatDiagnostic := func(pth string, diagnostic protocol.Diagnostic, source string) string { + severity := "Info" + switch diagnostic.Severity { + case protocol.SeverityError: + severity = "Error" + case protocol.SeverityWarning: + severity = "Warn" + case protocol.SeverityHint: + severity = "Hint" + } + + location := fmt.Sprintf("%s:%d:%d", pth, diagnostic.Range.Start.Line+1, diagnostic.Range.Start.Character+1) + + sourceInfo := "" + if diagnostic.Source != "" { + sourceInfo = diagnostic.Source + } else if source != "" { + sourceInfo = source + } + + codeInfo := "" + if diagnostic.Code != nil { + codeInfo = fmt.Sprintf("[%v]", diagnostic.Code) + } + + tagsInfo := "" + if len(diagnostic.Tags) > 0 { + tags := []string{} + for _, tag := range diagnostic.Tags { + switch tag { + case protocol.Unnecessary: + tags = append(tags, "unnecessary") + case protocol.Deprecated: + tags = append(tags, "deprecated") + } + } + if len(tags) > 0 { + tagsInfo = fmt.Sprintf(" (%s)", strings.Join(tags, ", ")) + } + } + + return fmt.Sprintf("%s: %s [%s]%s%s %s", + severity, + location, + sourceInfo, + codeInfo, + tagsInfo, + diagnostic.Message) + } + + for lspName, client := range lsps { + diagnostics := client.GetDiagnostics() + if len(diagnostics) > 0 { + for location, diags := range diagnostics { + isCurrentFile := location.Path() == filePath + + for _, diag := range diags { + formattedDiag := formatDiagnostic(location.Path(), diag, lspName) + + if isCurrentFile { + fileDiagnostics = append(fileDiagnostics, formattedDiag) + } else { + projectDiagnostics = append(projectDiagnostics, formattedDiag) + } + } + } + } + } + + sort.Slice(fileDiagnostics, func(i, j int) bool { + iIsError := strings.HasPrefix(fileDiagnostics[i], "Error") + jIsError := strings.HasPrefix(fileDiagnostics[j], "Error") + if iIsError != jIsError { + return iIsError // Errors come first + } + return fileDiagnostics[i] < fileDiagnostics[j] // Then alphabetically + }) + + sort.Slice(projectDiagnostics, func(i, j int) bool { + iIsError := strings.HasPrefix(projectDiagnostics[i], "Error") + jIsError := strings.HasPrefix(projectDiagnostics[j], "Error") + if iIsError != jIsError { + return iIsError + } + return projectDiagnostics[i] < projectDiagnostics[j] + }) + + output := "" + + if len(fileDiagnostics) > 0 { + output += "\n\n" + if len(fileDiagnostics) > 10 { + output += strings.Join(fileDiagnostics[:10], "\n") + output += fmt.Sprintf("\n... and %d more diagnostics", len(fileDiagnostics)-10) + } else { + output += strings.Join(fileDiagnostics, "\n") + } + output += "\n\n" + } + + if len(projectDiagnostics) > 0 { + output += "\n\n" + if len(projectDiagnostics) > 10 { + output += strings.Join(projectDiagnostics[:10], "\n") + output += fmt.Sprintf("\n... and %d more diagnostics", len(projectDiagnostics)-10) + } else { + output += strings.Join(projectDiagnostics, "\n") + } + output += "\n\n" + } + + if len(fileDiagnostics) > 0 || len(projectDiagnostics) > 0 { + fileErrors := countSeverity(fileDiagnostics, "Error") + fileWarnings := countSeverity(fileDiagnostics, "Warn") + projectErrors := countSeverity(projectDiagnostics, "Error") + projectWarnings := countSeverity(projectDiagnostics, "Warn") + + output += "\n\n" + output += fmt.Sprintf("Current file: %d errors, %d warnings\n", fileErrors, fileWarnings) + output += fmt.Sprintf("Project: %d errors, %d warnings\n", projectErrors, projectWarnings) + output += "\n" + } + + return output +} + +func countSeverity(diagnostics []string, severity string) int { + count := 0 + for _, diag := range diagnostics { + if strings.HasPrefix(diag, severity) { + count++ + } + } + return count +} diff --git a/internal/llm/tools/lsp_doc_symbols.go b/internal/llm/tools/lsp_doc_symbols.go new file mode 100644 index 000000000..243cb1918 --- /dev/null +++ b/internal/llm/tools/lsp_doc_symbols.go @@ -0,0 +1,204 @@ +package tools + +import ( + "context" + "encoding/json" + "fmt" + "strings" + + "github.com/sst/opencode/internal/lsp" + "github.com/sst/opencode/internal/lsp/protocol" +) + +type DocSymbolsParams struct { + FilePath string `json:"file_path"` +} + +type docSymbolsTool struct { + lspClients map[string]*lsp.Client +} + +const ( + DocSymbolsToolName = "docSymbols" + docSymbolsDescription = `Get document symbols for a file. +WHEN TO USE THIS TOOL: +- Use when you need to understand the structure of a file +- Helpful for finding classes, functions, methods, and variables in a file +- Great for getting an overview of a file's organization + +HOW TO USE: +- Provide the path to the file to get symbols for +- Results show all symbols defined in the file with their kind and location + +FEATURES: +- Lists all symbols in a hierarchical structure +- Shows symbol types (function, class, variable, etc.) +- Provides location information for each symbol +- Organizes symbols by their scope and relationship + +LIMITATIONS: +- Requires a functioning LSP server for the file type +- Results depend on the accuracy of the LSP server +- May not work for all file types + +TIPS: +- Use to quickly understand the structure of a large file +- Combine with Definition and References tools for deeper code exploration +` +) + +func NewDocSymbolsTool(lspClients map[string]*lsp.Client) BaseTool { + return &docSymbolsTool{ + lspClients, + } +} + +func (b *docSymbolsTool) Info() ToolInfo { + return ToolInfo{ + Name: DocSymbolsToolName, + Description: docSymbolsDescription, + Parameters: map[string]any{ + "file_path": map[string]any{ + "type": "string", + "description": "The path to the file to get symbols for", + }, + }, + Required: []string{"file_path"}, + } +} + +func (b *docSymbolsTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error) { + var params DocSymbolsParams + if err := json.Unmarshal([]byte(call.Input), ¶ms); err != nil { + return NewTextErrorResponse(fmt.Sprintf("error parsing parameters: %s", err)), nil + } + + lsps := b.lspClients + + if len(lsps) == 0 { + return NewTextResponse("\nLSP clients are still initializing. Document symbols lookup will be available once they're ready.\n"), nil + } + + // Ensure file is open in LSP + notifyLspOpenFile(ctx, params.FilePath, lsps) + + output := getDocumentSymbols(ctx, params.FilePath, lsps) + + return NewTextResponse(output), nil +} + +func getDocumentSymbols(ctx context.Context, filePath string, lsps map[string]*lsp.Client) string { + var results []string + + for lspName, client := range lsps { + // Create document symbol params + uri := fmt.Sprintf("file://%s", filePath) + symbolParams := protocol.DocumentSymbolParams{ + TextDocument: protocol.TextDocumentIdentifier{ + URI: protocol.DocumentUri(uri), + }, + } + + // Get document symbols + symbolResult, err := client.DocumentSymbol(ctx, symbolParams) + if err != nil { + results = append(results, fmt.Sprintf("Error from %s: %s", lspName, err)) + continue + } + + // Process the symbol result + symbols := processDocumentSymbolResult(symbolResult) + if len(symbols) == 0 { + results = append(results, fmt.Sprintf("No symbols found by %s", lspName)) + continue + } + + // Format the symbols + results = append(results, fmt.Sprintf("Symbols found by %s:", lspName)) + for _, symbol := range symbols { + results = append(results, formatSymbol(symbol, 1)) + } + } + + if len(results) == 0 { + return "No symbols found in the specified file." + } + + return strings.Join(results, "\n") +} + +func processDocumentSymbolResult(result protocol.Or_Result_textDocument_documentSymbol) []SymbolInfo { + var symbols []SymbolInfo + + switch v := result.Value.(type) { + case []protocol.SymbolInformation: + for _, si := range v { + symbols = append(symbols, SymbolInfo{ + Name: si.Name, + Kind: symbolKindToString(si.Kind), + Location: locationToString(si.Location), + Children: nil, + }) + } + case []protocol.DocumentSymbol: + for _, ds := range v { + symbols = append(symbols, documentSymbolToSymbolInfo(ds)) + } + } + + return symbols +} + +// SymbolInfo represents a symbol in a document +type SymbolInfo struct { + Name string + Kind string + Location string + Children []SymbolInfo +} + +func documentSymbolToSymbolInfo(symbol protocol.DocumentSymbol) SymbolInfo { + info := SymbolInfo{ + Name: symbol.Name, + Kind: symbolKindToString(symbol.Kind), + Location: fmt.Sprintf("Line %d-%d", + symbol.Range.Start.Line+1, + symbol.Range.End.Line+1), + Children: []SymbolInfo{}, + } + + for _, child := range symbol.Children { + info.Children = append(info.Children, documentSymbolToSymbolInfo(child)) + } + + return info +} + +func locationToString(location protocol.Location) string { + return fmt.Sprintf("Line %d-%d", + location.Range.Start.Line+1, + location.Range.End.Line+1) +} + +func symbolKindToString(kind protocol.SymbolKind) string { + if kindStr, ok := protocol.TableKindMap[kind]; ok { + return kindStr + } + return "Unknown" +} + +func formatSymbol(symbol SymbolInfo, level int) string { + indent := strings.Repeat(" ", level) + result := fmt.Sprintf("%s- %s (%s) %s", indent, symbol.Name, symbol.Kind, symbol.Location) + + var childResults []string + for _, child := range symbol.Children { + childResults = append(childResults, formatSymbol(child, level+1)) + } + + if len(childResults) > 0 { + return result + "\n" + strings.Join(childResults, "\n") + } + + return result +} \ No newline at end of file diff --git a/internal/llm/tools/lsp_references.go b/internal/llm/tools/lsp_references.go new file mode 100644 index 000000000..6e0090f4e --- /dev/null +++ b/internal/llm/tools/lsp_references.go @@ -0,0 +1,161 @@ +package tools + +import ( + "context" + "encoding/json" + "fmt" + "strings" + + "github.com/sst/opencode/internal/lsp" + "github.com/sst/opencode/internal/lsp/protocol" +) + +type ReferencesParams struct { + FilePath string `json:"file_path"` + Line int `json:"line"` + Column int `json:"column"` + IncludeDeclaration bool `json:"include_declaration"` +} + +type referencesTool struct { + lspClients map[string]*lsp.Client +} + +const ( + ReferencesToolName = "references" + referencesDescription = `Find all references to a symbol at a specific position in a file. +WHEN TO USE THIS TOOL: +- Use when you need to find all places where a symbol is used +- Helpful for understanding code usage and dependencies +- Great for refactoring and impact analysis + +HOW TO USE: +- Provide the path to the file containing the symbol +- Specify the line number (1-based) where the symbol appears +- Specify the column number (1-based) where the symbol appears +- Optionally set include_declaration to include the declaration in results +- Results show all locations where the symbol is referenced + +FEATURES: +- Finds references across files in the project +- Works with variables, functions, classes, interfaces, etc. +- Returns file paths, lines, and columns of all references + +LIMITATIONS: +- Requires a functioning LSP server for the file type +- May not find all references depending on LSP capabilities +- Results depend on the accuracy of the LSP server + +TIPS: +- Use in conjunction with Definition tool to understand symbol origins +- Combine with View tool to examine the references +` +) + +func NewReferencesTool(lspClients map[string]*lsp.Client) BaseTool { + return &referencesTool{ + lspClients, + } +} + +func (b *referencesTool) Info() ToolInfo { + return ToolInfo{ + Name: ReferencesToolName, + Description: referencesDescription, + Parameters: map[string]any{ + "file_path": map[string]any{ + "type": "string", + "description": "The path to the file containing the symbol", + }, + "line": map[string]any{ + "type": "integer", + "description": "The line number (1-based) where the symbol appears", + }, + "column": map[string]any{ + "type": "integer", + "description": "The column number (1-based) where the symbol appears", + }, + "include_declaration": map[string]any{ + "type": "boolean", + "description": "Whether to include the declaration in the results", + }, + }, + Required: []string{"file_path", "line", "column"}, + } +} + +func (b *referencesTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error) { + var params ReferencesParams + if err := json.Unmarshal([]byte(call.Input), ¶ms); err != nil { + return NewTextErrorResponse(fmt.Sprintf("error parsing parameters: %s", err)), nil + } + + lsps := b.lspClients + + if len(lsps) == 0 { + return NewTextResponse("\nLSP clients are still initializing. References lookup will be available once they're ready.\n"), nil + } + + // Ensure file is open in LSP + notifyLspOpenFile(ctx, params.FilePath, lsps) + + // Convert 1-based line/column to 0-based for LSP protocol + line := max(0, params.Line-1) + column := max(0, params.Column-1) + + output := getReferences(ctx, params.FilePath, line, column, params.IncludeDeclaration, lsps) + + return NewTextResponse(output), nil +} + +func getReferences(ctx context.Context, filePath string, line, column int, includeDeclaration bool, lsps map[string]*lsp.Client) string { + var results []string + + for lspName, client := range lsps { + // Create references params + uri := fmt.Sprintf("file://%s", filePath) + referencesParams := protocol.ReferenceParams{ + TextDocumentPositionParams: protocol.TextDocumentPositionParams{ + TextDocument: protocol.TextDocumentIdentifier{ + URI: protocol.DocumentUri(uri), + }, + Position: protocol.Position{ + Line: uint32(line), + Character: uint32(column), + }, + }, + Context: protocol.ReferenceContext{ + IncludeDeclaration: includeDeclaration, + }, + } + + // Get references + references, err := client.References(ctx, referencesParams) + if err != nil { + results = append(results, fmt.Sprintf("Error from %s: %s", lspName, err)) + continue + } + + if len(references) == 0 { + results = append(results, fmt.Sprintf("No references found by %s", lspName)) + continue + } + + // Format the locations + results = append(results, fmt.Sprintf("References found by %s:", lspName)) + for _, loc := range references { + path := strings.TrimPrefix(string(loc.URI), "file://") + // Convert 0-based line/column to 1-based for display + refLine := loc.Range.Start.Line + 1 + refColumn := loc.Range.Start.Character + 1 + results = append(results, fmt.Sprintf(" %s:%d:%d", path, refLine, refColumn)) + } + } + + if len(results) == 0 { + return "No references found for the symbol at the specified position." + } + + return strings.Join(results, "\n") +} + diff --git a/internal/llm/tools/lsp_workspace_symbols.go b/internal/llm/tools/lsp_workspace_symbols.go new file mode 100644 index 000000000..24ca577ea --- /dev/null +++ b/internal/llm/tools/lsp_workspace_symbols.go @@ -0,0 +1,162 @@ +package tools + +import ( + "context" + "encoding/json" + "fmt" + "strings" + + "github.com/sst/opencode/internal/lsp" + "github.com/sst/opencode/internal/lsp/protocol" +) + +type WorkspaceSymbolsParams struct { + Query string `json:"query"` +} + +type workspaceSymbolsTool struct { + lspClients map[string]*lsp.Client +} + +const ( + WorkspaceSymbolsToolName = "workspaceSymbols" + workspaceSymbolsDescription = `Find symbols across the workspace matching a query. +WHEN TO USE THIS TOOL: +- Use when you need to find symbols across multiple files +- Helpful for locating classes, functions, or variables in a project +- Great for exploring large codebases + +HOW TO USE: +- Provide a query string to search for symbols +- Results show matching symbols from across the workspace + +FEATURES: +- Searches across all files in the workspace +- Shows symbol types (function, class, variable, etc.) +- Provides location information for each symbol +- Works with partial matches and fuzzy search (depending on LSP server) + +LIMITATIONS: +- Requires a functioning LSP server for the file types +- Results depend on the accuracy of the LSP server +- Query capabilities vary by language server +- May not work for all file types + +TIPS: +- Use specific queries to narrow down results +- Combine with DocSymbols tool for detailed file exploration +- Use with Definition tool to jump to symbol definitions +` +) + +func NewWorkspaceSymbolsTool(lspClients map[string]*lsp.Client) BaseTool { + return &workspaceSymbolsTool{ + lspClients, + } +} + +func (b *workspaceSymbolsTool) Info() ToolInfo { + return ToolInfo{ + Name: WorkspaceSymbolsToolName, + Description: workspaceSymbolsDescription, + Parameters: map[string]any{ + "query": map[string]any{ + "type": "string", + "description": "The query string to search for symbols", + }, + }, + Required: []string{"query"}, + } +} + +func (b *workspaceSymbolsTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error) { + var params WorkspaceSymbolsParams + if err := json.Unmarshal([]byte(call.Input), ¶ms); err != nil { + return NewTextErrorResponse(fmt.Sprintf("error parsing parameters: %s", err)), nil + } + + lsps := b.lspClients + + if len(lsps) == 0 { + return NewTextResponse("\nLSP clients are still initializing. Workspace symbols lookup will be available once they're ready.\n"), nil + } + + output := getWorkspaceSymbols(ctx, params.Query, lsps) + + return NewTextResponse(output), nil +} + +func getWorkspaceSymbols(ctx context.Context, query string, lsps map[string]*lsp.Client) string { + var results []string + + for lspName, client := range lsps { + // Create workspace symbol params + symbolParams := protocol.WorkspaceSymbolParams{ + Query: query, + } + + // Get workspace symbols + symbolResult, err := client.Symbol(ctx, symbolParams) + if err != nil { + results = append(results, fmt.Sprintf("Error from %s: %s", lspName, err)) + continue + } + + // Process the symbol result + symbols := processWorkspaceSymbolResult(symbolResult) + if len(symbols) == 0 { + results = append(results, fmt.Sprintf("No symbols found by %s for query '%s'", lspName, query)) + continue + } + + // Format the symbols + results = append(results, fmt.Sprintf("Symbols found by %s for query '%s':", lspName, query)) + for _, symbol := range symbols { + results = append(results, fmt.Sprintf(" %s (%s) - %s", symbol.Name, symbol.Kind, symbol.Location)) + } + } + + if len(results) == 0 { + return fmt.Sprintf("No symbols found matching query '%s'.", query) + } + + return strings.Join(results, "\n") +} + +func processWorkspaceSymbolResult(result protocol.Or_Result_workspace_symbol) []SymbolInfo { + var symbols []SymbolInfo + + switch v := result.Value.(type) { + case []protocol.SymbolInformation: + for _, si := range v { + symbols = append(symbols, SymbolInfo{ + Name: si.Name, + Kind: symbolKindToString(si.Kind), + Location: formatWorkspaceLocation(si.Location), + Children: nil, + }) + } + case []protocol.WorkspaceSymbol: + for _, ws := range v { + location := "Unknown location" + if ws.Location.Value != nil { + if loc, ok := ws.Location.Value.(protocol.Location); ok { + location = formatWorkspaceLocation(loc) + } + } + symbols = append(symbols, SymbolInfo{ + Name: ws.Name, + Kind: symbolKindToString(ws.Kind), + Location: location, + Children: nil, + }) + } + } + + return symbols +} + +func formatWorkspaceLocation(location protocol.Location) string { + path := strings.TrimPrefix(string(location.URI), "file://") + return fmt.Sprintf("%s:%d:%d", path, location.Range.Start.Line+1, location.Range.Start.Character+1) +} \ No newline at end of file diff --git a/internal/llm/tools/references.go b/internal/llm/tools/references.go deleted file mode 100644 index 6e0090f4e..000000000 --- a/internal/llm/tools/references.go +++ /dev/null @@ -1,161 +0,0 @@ -package tools - -import ( - "context" - "encoding/json" - "fmt" - "strings" - - "github.com/sst/opencode/internal/lsp" - "github.com/sst/opencode/internal/lsp/protocol" -) - -type ReferencesParams struct { - FilePath string `json:"file_path"` - Line int `json:"line"` - Column int `json:"column"` - IncludeDeclaration bool `json:"include_declaration"` -} - -type referencesTool struct { - lspClients map[string]*lsp.Client -} - -const ( - ReferencesToolName = "references" - referencesDescription = `Find all references to a symbol at a specific position in a file. -WHEN TO USE THIS TOOL: -- Use when you need to find all places where a symbol is used -- Helpful for understanding code usage and dependencies -- Great for refactoring and impact analysis - -HOW TO USE: -- Provide the path to the file containing the symbol -- Specify the line number (1-based) where the symbol appears -- Specify the column number (1-based) where the symbol appears -- Optionally set include_declaration to include the declaration in results -- Results show all locations where the symbol is referenced - -FEATURES: -- Finds references across files in the project -- Works with variables, functions, classes, interfaces, etc. -- Returns file paths, lines, and columns of all references - -LIMITATIONS: -- Requires a functioning LSP server for the file type -- May not find all references depending on LSP capabilities -- Results depend on the accuracy of the LSP server - -TIPS: -- Use in conjunction with Definition tool to understand symbol origins -- Combine with View tool to examine the references -` -) - -func NewReferencesTool(lspClients map[string]*lsp.Client) BaseTool { - return &referencesTool{ - lspClients, - } -} - -func (b *referencesTool) Info() ToolInfo { - return ToolInfo{ - Name: ReferencesToolName, - Description: referencesDescription, - Parameters: map[string]any{ - "file_path": map[string]any{ - "type": "string", - "description": "The path to the file containing the symbol", - }, - "line": map[string]any{ - "type": "integer", - "description": "The line number (1-based) where the symbol appears", - }, - "column": map[string]any{ - "type": "integer", - "description": "The column number (1-based) where the symbol appears", - }, - "include_declaration": map[string]any{ - "type": "boolean", - "description": "Whether to include the declaration in the results", - }, - }, - Required: []string{"file_path", "line", "column"}, - } -} - -func (b *referencesTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error) { - var params ReferencesParams - if err := json.Unmarshal([]byte(call.Input), ¶ms); err != nil { - return NewTextErrorResponse(fmt.Sprintf("error parsing parameters: %s", err)), nil - } - - lsps := b.lspClients - - if len(lsps) == 0 { - return NewTextResponse("\nLSP clients are still initializing. References lookup will be available once they're ready.\n"), nil - } - - // Ensure file is open in LSP - notifyLspOpenFile(ctx, params.FilePath, lsps) - - // Convert 1-based line/column to 0-based for LSP protocol - line := max(0, params.Line-1) - column := max(0, params.Column-1) - - output := getReferences(ctx, params.FilePath, line, column, params.IncludeDeclaration, lsps) - - return NewTextResponse(output), nil -} - -func getReferences(ctx context.Context, filePath string, line, column int, includeDeclaration bool, lsps map[string]*lsp.Client) string { - var results []string - - for lspName, client := range lsps { - // Create references params - uri := fmt.Sprintf("file://%s", filePath) - referencesParams := protocol.ReferenceParams{ - TextDocumentPositionParams: protocol.TextDocumentPositionParams{ - TextDocument: protocol.TextDocumentIdentifier{ - URI: protocol.DocumentUri(uri), - }, - Position: protocol.Position{ - Line: uint32(line), - Character: uint32(column), - }, - }, - Context: protocol.ReferenceContext{ - IncludeDeclaration: includeDeclaration, - }, - } - - // Get references - references, err := client.References(ctx, referencesParams) - if err != nil { - results = append(results, fmt.Sprintf("Error from %s: %s", lspName, err)) - continue - } - - if len(references) == 0 { - results = append(results, fmt.Sprintf("No references found by %s", lspName)) - continue - } - - // Format the locations - results = append(results, fmt.Sprintf("References found by %s:", lspName)) - for _, loc := range references { - path := strings.TrimPrefix(string(loc.URI), "file://") - // Convert 0-based line/column to 1-based for display - refLine := loc.Range.Start.Line + 1 - refColumn := loc.Range.Start.Character + 1 - results = append(results, fmt.Sprintf(" %s:%d:%d", path, refLine, refColumn)) - } - } - - if len(results) == 0 { - return "No references found for the symbol at the specified position." - } - - return strings.Join(results, "\n") -} - -- cgit v1.2.3