From cfdd687216799cb5b47f099f1e7cd5dd16b3bdd0 Mon Sep 17 00:00:00 2001 From: Kujtim Hoxha Date: Thu, 3 Apr 2025 15:20:15 +0200 Subject: add initial lsp support --- internal/llm/tools/diagnostics.go | 229 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 229 insertions(+) create mode 100644 internal/llm/tools/diagnostics.go (limited to 'internal/llm/tools/diagnostics.go') diff --git a/internal/llm/tools/diagnostics.go b/internal/llm/tools/diagnostics.go new file mode 100644 index 000000000..dc90e5860 --- /dev/null +++ b/internal/llm/tools/diagnostics.go @@ -0,0 +1,229 @@ +package tools + +import ( + "context" + "encoding/json" + "fmt" + "sort" + "strings" + "time" + + "github.com/kujtimiihoxha/termai/internal/lsp" + "github.com/kujtimiihoxha/termai/internal/lsp/protocol" +) + +type diagnosticsTool struct { + lspClients map[string]*lsp.Client +} + +const ( + DiagnosticsToolName = "diagnostics" +) + +type DiagnosticsParams struct { + FilePath string `json:"file_path"` +} + +func (b *diagnosticsTool) Info() ToolInfo { + return ToolInfo{ + Name: DiagnosticsToolName, + Description: "Get diagnostics for a file and/or project.", + 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 NewTextErrorResponse("no LSP clients available"), nil + } + + if params.FilePath == "" { + notifyLspOpenFile(ctx, params.FilePath, lsps) + } + + output := appendDiagnostics(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 { + // Wait for the file to be opened and diagnostics to be received + // TODO: see if we can do this in a more efficient way + time.Sleep(2 * time.Second) + } + + } +} + +func appendDiagnostics(filePath string, lsps map[string]*lsp.Client) string { + fileDiagnostics := []string{} + projectDiagnostics := []string{} + + // Enhanced format function that includes more diagnostic information + formatDiagnostic := func(pth string, diagnostic protocol.Diagnostic, source string) string { + // Base components + severity := "Info" + switch diagnostic.Severity { + case protocol.SeverityError: + severity = "Error" + case protocol.SeverityWarning: + severity = "Warn" + case protocol.SeverityHint: + severity = "Hint" + } + + // Location information + location := fmt.Sprintf("%s:%d:%d", pth, diagnostic.Range.Start.Line+1, diagnostic.Range.Start.Character+1) + + // Source information (LSP name) + sourceInfo := "" + if diagnostic.Source != "" { + sourceInfo = diagnostic.Source + } else if source != "" { + sourceInfo = source + } + + // Code information + codeInfo := "" + if diagnostic.Code != nil { + codeInfo = fmt.Sprintf("[%v]", diagnostic.Code) + } + + // Tags information + 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, ", ")) + } + } + + // Assemble the full diagnostic message + 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 + + // Group diagnostics by severity for better organization + for _, diag := range diags { + formattedDiag := formatDiagnostic(location.Path(), diag, lspName) + + if isCurrentFile { + fileDiagnostics = append(fileDiagnostics, formattedDiag) + } else { + projectDiagnostics = append(projectDiagnostics, formattedDiag) + } + } + } + } + } + + // Sort diagnostics by severity (errors first) and then by location + 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" + } + + // Add summary counts + 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 +} + +// Helper function to count diagnostics by severity +func countSeverity(diagnostics []string, severity string) int { + count := 0 + for _, diag := range diagnostics { + if strings.HasPrefix(diag, severity) { + count++ + } + } + return count +} + +func NewDiagnosticsTool(lspClients map[string]*lsp.Client) BaseTool { + return &diagnosticsTool{ + lspClients, + } +} -- cgit v1.2.3