on main help sign in
Improve LSP diagnostics handling for file operations
3aef4deKujtim Hoxha
internal/llm/tools/diagnostics.go+30−6
@@ -53,6 +53,7 @@ func (b *diagnosticsTool) Run(ctx context.Context, call ToolCall) (ToolResponse,
53
53
54 if params.FilePath != "" {
54 if params.FilePath != "" {
55 notifyLspOpenFile(ctx, params.FilePath, lsps)
55 notifyLspOpenFile(ctx, params.FilePath, lsps)
56+ waitForLspDiagnostics(ctx, params.FilePath, lsps)
56 }
57 }
57
58
58 output := appendDiagnostics(params.FilePath, lsps)
59 output := appendDiagnostics(params.FilePath, lsps)
@@ -61,6 +62,22 @@ func (b *diagnosticsTool) Run(ctx context.Context, call ToolCall) (ToolResponse,
61}
62}
62
63
63func notifyLspOpenFile(ctx context.Context, filePath string, lsps map[string]*lsp.Client) {
64func notifyLspOpenFile(ctx context.Context, filePath string, lsps map[string]*lsp.Client) {
65+ for _, client := range lsps {
66+ // Open the file
67+ err := client.OpenFile(ctx, filePath)
68+ if err != nil {
69+ // If there's an error opening the file, continue to the next client
70+ continue
71+ }
72+ }
73+}
74+
75+// waitForLspDiagnostics opens a file in LSP clients and waits for diagnostics to be published
76+func waitForLspDiagnostics(ctx context.Context, filePath string, lsps map[string]*lsp.Client) {
77+ if len(lsps) == 0 {
78+ return
79+ }
80+
64 // Create a channel to receive diagnostic notifications
81 // Create a channel to receive diagnostic notifications
65 diagChan := make(chan struct{}, 1)
82 diagChan := make(chan struct{}, 1)
66
83
@@ -92,11 +109,18 @@ func notifyLspOpenFile(ctx context.Context, filePath string, lsps map[string]*ls
92 // Register our temporary handler
109 // Register our temporary handler
93 client.RegisterNotificationHandler("textDocument/publishDiagnostics", handler)
110 client.RegisterNotificationHandler("textDocument/publishDiagnostics", handler)
94
111
112+ // Notify change if the file is already open
113+ if client.IsFileOpen(filePath) {
114+ err := client.NotifyChange(ctx, filePath)
115+ if err != nil {
116+ continue
117+ }
118+ } else {
95− // Open the file
119+ // Open the file if it's not already open
96− err := client.OpenFile(ctx, filePath)
120+ err := client.OpenFile(ctx, filePath)
97− if err != nil {
121+ if err != nil {
98− // If there's an error opening the file, continue to the next client
99− continue
122+ continue
123+ }
100 }
124 }
101 }
125 }
102
126
@@ -104,7 +128,7 @@ func notifyLspOpenFile(ctx context.Context, filePath string, lsps map[string]*ls
104 select {
128 select {
105 case <-diagChan:
129 case <-diagChan:
106 // Diagnostics received
130 // Diagnostics received
107− case <-time.After(10 * time.Second):
131+ case <-time.After(5 * time.Second):
108 // Timeout after 5 seconds - this is a fallback in case no diagnostics are published
132 // Timeout after 5 seconds - this is a fallback in case no diagnostics are published
109 case <-ctx.Done():
133 case <-ctx.Done():
110 // Context cancelled
134 // Context cancelled
internal/llm/tools/edit.go+6−5
@@ -74,7 +74,6 @@ func (e *editTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error)
74 params.FilePath = filepath.Join(wd, params.FilePath)
74 params.FilePath = filepath.Join(wd, params.FilePath)
75 }
75 }
76
76
77− notifyLspOpenFile(ctx, params.FilePath, e.lspClients)
78 if params.OldString == "" {
77 if params.OldString == "" {
79 result, err := createNewFile(params.FilePath, params.NewString)
78 result, err := createNewFile(params.FilePath, params.NewString)
80 if err != nil {
79 if err != nil {
@@ -96,6 +95,8 @@ func (e *editTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error)
96 return NewTextErrorResponse(fmt.Sprintf("error replacing content: %s", err)), nil
95 return NewTextErrorResponse(fmt.Sprintf("error replacing content: %s", err)), nil
97 }
96 }
98
97
98+ // Wait for LSP diagnostics after editing the file
99+ waitForLspDiagnostics(ctx, params.FilePath, e.lspClients)
99 result = fmt.Sprintf("<result>\n%s\n</result>\n", result)
100 result = fmt.Sprintf("<result>\n%s\n</result>\n", result)
100 result += appendDiagnostics(params.FilePath, e.lspClients)
101 result += appendDiagnostics(params.FilePath, e.lspClients)
101 return NewTextResponse(result), nil
102 return NewTextResponse(result), nil
@@ -303,23 +304,23 @@ func GenerateDiff(oldContent, newContent string) string {
303 diffs = dmp.DiffCharsToLines(diffs, dmpStrings)
304 diffs = dmp.DiffCharsToLines(diffs, dmpStrings)
304 diffs = dmp.DiffCleanupSemantic(diffs)
305 diffs = dmp.DiffCleanupSemantic(diffs)
305 buff := strings.Builder{}
306 buff := strings.Builder{}
306−
307+
307 // Add a header to make the diff more readable
308 // Add a header to make the diff more readable
308 buff.WriteString("Changes:\n")
309 buff.WriteString("Changes:\n")
309−
310+
310 for _, diff := range diffs {
311 for _, diff := range diffs {
311 text := diff.Text
312 text := diff.Text
312
313
313 switch diff.Type {
314 switch diff.Type {
314 case diffmatchpatch.DiffInsert:
315 case diffmatchpatch.DiffInsert:
315− for _, line := range strings.Split(text, "\n") {
316+ for line := range strings.SplitSeq(text, "\n") {
316 if line == "" {
317 if line == "" {
317 continue
318 continue
318 }
319 }
319 _, _ = buff.WriteString("+ " + line + "\n")
320 _, _ = buff.WriteString("+ " + line + "\n")
320 }
321 }
321 case diffmatchpatch.DiffDelete:
322 case diffmatchpatch.DiffDelete:
322− for _, line := range strings.Split(text, "\n") {
323+ for line := range strings.SplitSeq(text, "\n") {
323 if line == "" {
324 if line == "" {
324 continue
325 continue
325 }
326 }
internal/llm/tools/write.go+2−1
@@ -100,7 +100,6 @@ func (w *writeTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error
100 return NewTextErrorResponse(fmt.Sprintf("Failed to create parent directories: %s", err)), nil
100 return NewTextErrorResponse(fmt.Sprintf("Failed to create parent directories: %s", err)), nil
101 }
101 }
102
102
103− notifyLspOpenFile(ctx, filePath, w.lspClients)
104 // Get old content for diff if file exists
103 // Get old content for diff if file exists
105 oldContent := ""
104 oldContent := ""
106 if fileInfo != nil && !fileInfo.IsDir() {
105 if fileInfo != nil && !fileInfo.IsDir() {
@@ -135,6 +134,8 @@ func (w *writeTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error
135 // Record the file write
134 // Record the file write
136 recordFileWrite(filePath)
135 recordFileWrite(filePath)
137 recordFileRead(filePath)
136 recordFileRead(filePath)
137+ // Wait for LSP diagnostics after writing the file
138+ waitForLspDiagnostics(ctx, filePath, w.lspClients)
138
139
139 result := fmt.Sprintf("File successfully written: %s", filePath)
140 result := fmt.Sprintf("File successfully written: %s", filePath)
140 result = fmt.Sprintf("<result>\n%s\n</result>", result)
141 result = fmt.Sprintf("<result>\n%s\n</result>", result)