lsp_definition.go 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. package tools
  2. import (
  3. "context"
  4. "encoding/json"
  5. "fmt"
  6. "log/slog"
  7. "strings"
  8. "github.com/sst/opencode/internal/lsp"
  9. "github.com/sst/opencode/internal/lsp/protocol"
  10. )
  11. type DefinitionParams struct {
  12. FilePath string `json:"file_path"`
  13. Line int `json:"line"`
  14. Column int `json:"column"`
  15. }
  16. type definitionTool struct {
  17. lspClients map[string]*lsp.Client
  18. }
  19. const (
  20. DefinitionToolName = "definition"
  21. definitionDescription = `Find the definition of a symbol at a specific position in a file.
  22. WHEN TO USE THIS TOOL:
  23. - Use when you need to find where a symbol is defined
  24. - Helpful for understanding code structure and relationships
  25. - Great for navigating between implementation and interface
  26. HOW TO USE:
  27. - Provide the path to the file containing the symbol
  28. - Specify the line number (1-based) where the symbol appears
  29. - Specify the column number (1-based) where the symbol appears
  30. - Results show the location of the symbol's definition
  31. FEATURES:
  32. - Finds definitions across files in the project
  33. - Works with variables, functions, classes, interfaces, etc.
  34. - Returns file path, line, and column of the definition
  35. LIMITATIONS:
  36. - Requires a functioning LSP server for the file type
  37. - May not work for all symbols depending on LSP capabilities
  38. - Results depend on the accuracy of the LSP server
  39. TIPS:
  40. - Use in conjunction with References tool to understand usage
  41. - Combine with View tool to examine the definition
  42. `
  43. )
  44. func NewDefinitionTool(lspClients map[string]*lsp.Client) BaseTool {
  45. return &definitionTool{
  46. lspClients,
  47. }
  48. }
  49. func (b *definitionTool) Info() ToolInfo {
  50. return ToolInfo{
  51. Name: DefinitionToolName,
  52. Description: definitionDescription,
  53. Parameters: map[string]any{
  54. "file_path": map[string]any{
  55. "type": "string",
  56. "description": "The path to the file containing the symbol",
  57. },
  58. "line": map[string]any{
  59. "type": "integer",
  60. "description": "The line number (1-based) where the symbol appears",
  61. },
  62. "column": map[string]any{
  63. "type": "integer",
  64. "description": "The column number (1-based) where the symbol appears",
  65. },
  66. },
  67. Required: []string{"file_path", "line", "column"},
  68. }
  69. }
  70. func (b *definitionTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error) {
  71. var params DefinitionParams
  72. if err := json.Unmarshal([]byte(call.Input), &params); err != nil {
  73. return NewTextErrorResponse(fmt.Sprintf("error parsing parameters: %s", err)), nil
  74. }
  75. lsps := b.lspClients
  76. if len(lsps) == 0 {
  77. return NewTextResponse("\nLSP clients are still initializing. Definition lookup will be available once they're ready.\n"), nil
  78. }
  79. // Ensure file is open in LSP
  80. notifyLspOpenFile(ctx, params.FilePath, lsps)
  81. // Convert 1-based line/column to 0-based for LSP protocol
  82. line := max(0, params.Line-1)
  83. column := max(0, params.Column-1)
  84. output := getDefinition(ctx, params.FilePath, line, column, lsps)
  85. return NewTextResponse(output), nil
  86. }
  87. func getDefinition(ctx context.Context, filePath string, line, column int, lsps map[string]*lsp.Client) string {
  88. var results []string
  89. slog.Debug(fmt.Sprintf("Looking for definition in %s at line %d, column %d", filePath, line+1, column+1))
  90. slog.Debug(fmt.Sprintf("Available LSP clients: %v", getClientNames(lsps)))
  91. for lspName, client := range lsps {
  92. slog.Debug(fmt.Sprintf("Trying LSP client: %s", lspName))
  93. // Create definition params
  94. uri := fmt.Sprintf("file://%s", filePath)
  95. definitionParams := protocol.DefinitionParams{
  96. TextDocumentPositionParams: protocol.TextDocumentPositionParams{
  97. TextDocument: protocol.TextDocumentIdentifier{
  98. URI: protocol.DocumentUri(uri),
  99. },
  100. Position: protocol.Position{
  101. Line: uint32(line),
  102. Character: uint32(column),
  103. },
  104. },
  105. }
  106. slog.Debug(fmt.Sprintf("Sending definition request with params: %+v", definitionParams))
  107. // Get definition
  108. definition, err := client.Definition(ctx, definitionParams)
  109. if err != nil {
  110. slog.Debug(fmt.Sprintf("Error from %s: %s", lspName, err))
  111. results = append(results, fmt.Sprintf("Error from %s: %s", lspName, err))
  112. continue
  113. }
  114. slog.Debug(fmt.Sprintf("Got definition result type: %T", definition.Value))
  115. // Process the definition result
  116. locations := processDefinitionResult(definition)
  117. slog.Debug(fmt.Sprintf("Processed locations count: %d", len(locations)))
  118. if len(locations) == 0 {
  119. results = append(results, fmt.Sprintf("No definition found by %s", lspName))
  120. continue
  121. }
  122. // Format the locations
  123. for _, loc := range locations {
  124. path := strings.TrimPrefix(string(loc.URI), "file://")
  125. // Convert 0-based line/column to 1-based for display
  126. defLine := loc.Range.Start.Line + 1
  127. defColumn := loc.Range.Start.Character + 1
  128. slog.Debug(fmt.Sprintf("Found definition at %s:%d:%d", path, defLine, defColumn))
  129. results = append(results, fmt.Sprintf("Definition found by %s: %s:%d:%d", lspName, path, defLine, defColumn))
  130. }
  131. }
  132. if len(results) == 0 {
  133. return "No definition found for the symbol at the specified position."
  134. }
  135. return strings.Join(results, "\n")
  136. }
  137. func processDefinitionResult(result protocol.Or_Result_textDocument_definition) []protocol.Location {
  138. var locations []protocol.Location
  139. switch v := result.Value.(type) {
  140. case protocol.Location:
  141. locations = append(locations, v)
  142. case []protocol.Location:
  143. locations = append(locations, v...)
  144. case []protocol.DefinitionLink:
  145. for _, link := range v {
  146. locations = append(locations, protocol.Location{
  147. URI: link.TargetURI,
  148. Range: link.TargetRange,
  149. })
  150. }
  151. case protocol.Or_Definition:
  152. switch d := v.Value.(type) {
  153. case protocol.Location:
  154. locations = append(locations, d)
  155. case []protocol.Location:
  156. locations = append(locations, d...)
  157. }
  158. }
  159. return locations
  160. }
  161. // Helper function to get LSP client names for debugging
  162. func getClientNames(lsps map[string]*lsp.Client) []string {
  163. names := make([]string, 0, len(lsps))
  164. for name := range lsps {
  165. names = append(names, name)
  166. }
  167. return names
  168. }