Browse Source

feat: docSymbols and workspaceSymbols tools

adamdottv 9 months ago
parent
commit
5ea989fb74

+ 4 - 0
internal/llm/agent/tools.go

@@ -35,6 +35,8 @@ func PrimaryAgentTools(
 			tools.NewDiagnosticsTool(lspClients),
 			tools.NewDiagnosticsTool(lspClients),
 			tools.NewDefinitionTool(lspClients),
 			tools.NewDefinitionTool(lspClients),
 			tools.NewReferencesTool(lspClients),
 			tools.NewReferencesTool(lspClients),
+			tools.NewDocSymbolsTool(lspClients),
+			tools.NewWorkspaceSymbolsTool(lspClients),
 			NewAgentTool(sessions, messages, lspClients),
 			NewAgentTool(sessions, messages, lspClients),
 		}, mcpTools...,
 		}, mcpTools...,
 	)
 	)
@@ -48,5 +50,7 @@ func TaskAgentTools(lspClients map[string]*lsp.Client) []tools.BaseTool {
 		tools.NewViewTool(lspClients),
 		tools.NewViewTool(lspClients),
 		tools.NewDefinitionTool(lspClients),
 		tools.NewDefinitionTool(lspClients),
 		tools.NewReferencesTool(lspClients),
 		tools.NewReferencesTool(lspClients),
+		tools.NewDocSymbolsTool(lspClients),
+		tools.NewWorkspaceSymbolsTool(lspClients),
 	}
 	}
 }
 }

+ 0 - 0
internal/llm/tools/definition.go → internal/llm/tools/lsp_definition.go


+ 0 - 0
internal/llm/tools/diagnostics.go → internal/llm/tools/lsp_diagnostics.go


+ 204 - 0
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), &params); 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
+}

+ 0 - 0
internal/llm/tools/references.go → internal/llm/tools/lsp_references.go


+ 162 - 0
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), &params); 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)
+}