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