1package lsp
2
3import (
4 "bufio"
5 "context"
6 "encoding/json"
7 "fmt"
8 "io"
9 "log/slog"
10 "maps"
11 "os"
12 "os/exec"
13 "path/filepath"
14 "slices"
15 "strings"
16 "sync"
17 "sync/atomic"
18 "time"
19
20 "github.com/charmbracelet/crush/internal/config"
21 "github.com/charmbracelet/crush/internal/log"
22 "github.com/charmbracelet/crush/internal/lsp/protocol"
23)
24
25type Client struct {
26 Cmd *exec.Cmd
27 stdin io.WriteCloser
28 stdout *bufio.Reader
29 stderr io.ReadCloser
30
31 // Client name for identification
32 name string
33
34 // File types this LSP server handles (e.g., .go, .rs, .py)
35 fileTypes []string
36
37 // Diagnostic change callback
38 onDiagnosticsChanged func(name string, count int)
39
40 // Request ID counter
41 nextID atomic.Int32
42
43 // Response handlers
44 handlers map[int32]chan *Message
45 handlersMu sync.RWMutex
46
47 // Request tracking for cleanup
48 pendingRequests map[int32]time.Time
49 pendingRequestsMu sync.RWMutex
50
51 // Server request handlers
52 serverRequestHandlers map[string]ServerRequestHandler
53 serverHandlersMu sync.RWMutex
54
55 // Notification handlers
56 notificationHandlers map[string]NotificationHandler
57 notificationMu sync.RWMutex
58
59 // Diagnostic cache
60 diagnostics map[protocol.DocumentURI][]protocol.Diagnostic
61 diagnosticsMu sync.RWMutex
62
63 // Files are currently opened by the LSP
64 openFiles map[string]*OpenFileInfo
65 openFilesMu sync.RWMutex
66
67 // Server state
68 serverState atomic.Value
69
70 // Shutdown tracking
71 shutdownOnce sync.Once
72 shutdownChan chan struct{}
73 stderrDone chan struct{}
74 messageHandlerDone chan struct{}
75}
76
77// NewClient creates a new LSP client.
78func NewClient(ctx context.Context, name string, config config.LSPConfig) (*Client, error) {
79 cmd := exec.CommandContext(ctx, config.Command, config.Args...)
80
81 // Copy env
82 cmd.Env = slices.Concat(os.Environ(), config.ResolvedEnv())
83
84 stdin, err := cmd.StdinPipe()
85 if err != nil {
86 return nil, fmt.Errorf("failed to create stdin pipe: %w", err)
87 }
88
89 stdout, err := cmd.StdoutPipe()
90 if err != nil {
91 return nil, fmt.Errorf("failed to create stdout pipe: %w", err)
92 }
93
94 stderr, err := cmd.StderrPipe()
95 if err != nil {
96 return nil, fmt.Errorf("failed to create stderr pipe: %w", err)
97 }
98
99 client := &Client{
100 Cmd: cmd,
101 name: name,
102 fileTypes: config.FileTypes,
103 stdin: stdin,
104 stdout: bufio.NewReader(stdout),
105 stderr: stderr,
106 handlers: make(map[int32]chan *Message),
107 pendingRequests: make(map[int32]time.Time),
108 notificationHandlers: make(map[string]NotificationHandler),
109 serverRequestHandlers: make(map[string]ServerRequestHandler),
110 diagnostics: make(map[protocol.DocumentURI][]protocol.Diagnostic),
111 openFiles: make(map[string]*OpenFileInfo),
112 shutdownChan: make(chan struct{}),
113 stderrDone: make(chan struct{}),
114 messageHandlerDone: make(chan struct{}),
115 }
116
117 // Initialize server state
118 client.serverState.Store(StateStarting)
119
120 // Start the LSP server process
121 if err := cmd.Start(); err != nil {
122 return nil, fmt.Errorf("failed to start LSP server: %w", err)
123 }
124
125 // Handle stderr in a separate goroutine
126 go func() {
127 defer close(client.stderrDone)
128 scanner := bufio.NewScanner(stderr)
129 for scanner.Scan() {
130 select {
131 case <-client.shutdownChan:
132 return
133 default:
134 slog.Error("LSP Server", "err", scanner.Text())
135 }
136 }
137 if err := scanner.Err(); err != nil {
138 slog.Error("Error reading", "err", err)
139 }
140 }()
141
142 // Start message handling loop
143 go func() {
144 defer close(client.messageHandlerDone)
145 defer log.RecoverPanic("LSP-message-handler", func() {
146 slog.Error("LSP message handler crashed, LSP functionality may be impaired")
147 })
148 client.handleMessages()
149 }()
150
151 return client, nil
152}
153
154func (c *Client) RegisterNotificationHandler(method string, handler NotificationHandler) {
155 c.notificationMu.Lock()
156 defer c.notificationMu.Unlock()
157 c.notificationHandlers[method] = handler
158}
159
160func (c *Client) RegisterServerRequestHandler(method string, handler ServerRequestHandler) {
161 c.serverHandlersMu.Lock()
162 defer c.serverHandlersMu.Unlock()
163 c.serverRequestHandlers[method] = handler
164}
165
166func (c *Client) InitializeLSPClient(ctx context.Context, workspaceDir string) (*protocol.InitializeResult, error) {
167 initParams := &protocol.InitializeParams{
168 WorkspaceFoldersInitializeParams: protocol.WorkspaceFoldersInitializeParams{
169 WorkspaceFolders: []protocol.WorkspaceFolder{
170 {
171 URI: protocol.URI(protocol.URIFromPath(workspaceDir)),
172 Name: workspaceDir,
173 },
174 },
175 },
176
177 XInitializeParams: protocol.XInitializeParams{
178 ProcessID: int32(os.Getpid()),
179 ClientInfo: &protocol.ClientInfo{
180 Name: "mcp-language-server",
181 Version: "0.1.0",
182 },
183 RootPath: workspaceDir,
184 RootURI: protocol.URIFromPath(workspaceDir),
185 Capabilities: protocol.ClientCapabilities{
186 Workspace: protocol.WorkspaceClientCapabilities{
187 Configuration: true,
188 DidChangeConfiguration: protocol.DidChangeConfigurationClientCapabilities{
189 DynamicRegistration: true,
190 },
191 DidChangeWatchedFiles: protocol.DidChangeWatchedFilesClientCapabilities{
192 DynamicRegistration: true,
193 RelativePatternSupport: true,
194 },
195 },
196 TextDocument: protocol.TextDocumentClientCapabilities{
197 Synchronization: &protocol.TextDocumentSyncClientCapabilities{
198 DynamicRegistration: true,
199 DidSave: true,
200 },
201 Completion: protocol.CompletionClientCapabilities{
202 CompletionItem: protocol.ClientCompletionItemOptions{},
203 },
204 CodeLens: &protocol.CodeLensClientCapabilities{
205 DynamicRegistration: true,
206 },
207 DocumentSymbol: protocol.DocumentSymbolClientCapabilities{},
208 CodeAction: protocol.CodeActionClientCapabilities{
209 CodeActionLiteralSupport: protocol.ClientCodeActionLiteralOptions{
210 CodeActionKind: protocol.ClientCodeActionKindOptions{
211 ValueSet: []protocol.CodeActionKind{},
212 },
213 },
214 },
215 PublishDiagnostics: protocol.PublishDiagnosticsClientCapabilities{
216 VersionSupport: true,
217 },
218 SemanticTokens: protocol.SemanticTokensClientCapabilities{
219 Requests: protocol.ClientSemanticTokensRequestOptions{
220 Range: &protocol.Or_ClientSemanticTokensRequestOptions_range{},
221 Full: &protocol.Or_ClientSemanticTokensRequestOptions_full{},
222 },
223 TokenTypes: []string{},
224 TokenModifiers: []string{},
225 Formats: []protocol.TokenFormat{},
226 },
227 },
228 Window: protocol.WindowClientCapabilities{},
229 },
230 InitializationOptions: map[string]any{
231 "codelenses": map[string]bool{
232 "generate": true,
233 "regenerate_cgo": true,
234 "test": true,
235 "tidy": true,
236 "upgrade_dependency": true,
237 "vendor": true,
238 "vulncheck": false,
239 },
240 },
241 },
242 }
243
244 var result protocol.InitializeResult
245 if err := c.Call(ctx, "initialize", initParams, &result); err != nil {
246 return nil, fmt.Errorf("initialize failed: %w", err)
247 }
248
249 if err := c.Notify(ctx, "initialized", struct{}{}); err != nil {
250 return nil, fmt.Errorf("initialized notification failed: %w", err)
251 }
252
253 // Register handlers
254 c.RegisterServerRequestHandler("workspace/applyEdit", HandleApplyEdit)
255 c.RegisterServerRequestHandler("workspace/configuration", HandleWorkspaceConfiguration)
256 c.RegisterServerRequestHandler("client/registerCapability", HandleRegisterCapability)
257 c.RegisterNotificationHandler("window/showMessage", HandleServerMessage)
258 c.RegisterNotificationHandler("textDocument/publishDiagnostics",
259 func(params json.RawMessage) { HandleDiagnostics(c, params) })
260
261 // Notify the LSP server
262 err := c.Initialized(ctx, protocol.InitializedParams{})
263 if err != nil {
264 return nil, fmt.Errorf("initialization failed: %w", err)
265 }
266
267 return &result, nil
268}
269
270func (c *Client) Close() error {
271 // Try graceful shutdown first
272 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
273 defer cancel()
274
275 // Attempt to close all files
276 c.CloseAllFiles(ctx)
277
278 // Do full shutdown following LSP spec
279 if err := c.Shutdown(ctx); err != nil {
280 slog.Warn("LSP shutdown failed during close", "name", c.name, "error", err)
281 }
282
283 // Close stdin to signal the server
284 if c.stdin != nil {
285 if err := c.stdin.Close(); err != nil {
286 slog.Warn("Failed to close stdin", "name", c.name, "error", err)
287 }
288 }
289
290 // Terminate the process if still running
291 if c.Cmd != nil && c.Cmd.Process != nil {
292 // Use a channel to handle the Wait with timeout
293 done := make(chan error, 1)
294 go func() {
295 done <- c.Cmd.Wait()
296 }()
297
298 // Wait for process to exit with timeout
299 select {
300 case err := <-done:
301 if err != nil {
302 slog.Debug("LSP process exited with error", "name", c.name, "error", err)
303 }
304 return nil
305 case <-time.After(2 * time.Second):
306 // If we timeout, try to kill the process
307 if err := c.Cmd.Process.Kill(); err != nil {
308 return fmt.Errorf("failed to kill process: %w", err)
309 }
310 return fmt.Errorf("process killed after timeout")
311 }
312 }
313
314 return nil
315}
316
317type ServerState int
318
319const (
320 StateStarting ServerState = iota
321 StateReady
322 StateError
323)
324
325// GetServerState returns the current state of the LSP server
326func (c *Client) GetServerState() ServerState {
327 if val := c.serverState.Load(); val != nil {
328 return val.(ServerState)
329 }
330 return StateStarting
331}
332
333// SetServerState sets the current state of the LSP server
334func (c *Client) SetServerState(state ServerState) {
335 c.serverState.Store(state)
336}
337
338// GetName returns the name of the LSP client
339func (c *Client) GetName() string {
340 return c.name
341}
342
343// SetDiagnosticsCallback sets the callback function for diagnostic changes
344func (c *Client) SetDiagnosticsCallback(callback func(name string, count int)) {
345 c.onDiagnosticsChanged = callback
346}
347
348// WaitForServerReady waits for the server to be ready by polling the server
349// with a simple request until it responds successfully or times out
350func (c *Client) WaitForServerReady(ctx context.Context) error {
351 cfg := config.Get()
352
353 // Set initial state
354 c.SetServerState(StateStarting)
355
356 // Create a context with timeout
357 ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
358 defer cancel()
359
360 // Try to ping the server with a simple request
361 ticker := time.NewTicker(500 * time.Millisecond)
362 defer ticker.Stop()
363
364 if cfg.Options.DebugLSP {
365 slog.Debug("Waiting for LSP server to be ready...")
366 }
367
368 // Determine server type for specialized initialization
369 serverType := c.detectServerType()
370
371 // For TypeScript-like servers, we need to open some key files first
372 if serverType == ServerTypeTypeScript {
373 if cfg.Options.DebugLSP {
374 slog.Debug("TypeScript-like server detected, opening key configuration files")
375 }
376 c.openKeyConfigFiles(ctx)
377 }
378
379 for {
380 select {
381 case <-ctx.Done():
382 c.SetServerState(StateError)
383 return fmt.Errorf("timeout waiting for LSP server to be ready")
384 case <-ticker.C:
385 // Try a simple workspace/symbol request to check if server is ready
386 var symbols []protocol.WorkspaceSymbol
387 err := c.Call(ctx, "workspace/symbol", protocol.WorkspaceSymbolParams{
388 Query: "",
389 }, &symbols)
390 if err == nil {
391 // Server responded successfully
392 c.SetServerState(StateReady)
393 if cfg.Options.DebugLSP {
394 slog.Debug("LSP server is ready")
395 }
396 return nil
397 }
398
399 if cfg.Options.DebugLSP {
400 slog.Debug("LSP server not ready yet", "error", err, "serverType", serverType)
401 }
402 }
403 }
404}
405
406// ServerType represents the type of LSP server
407type ServerType int
408
409const (
410 ServerTypeUnknown ServerType = iota
411 ServerTypeGo
412 ServerTypeTypeScript
413 ServerTypeRust
414 ServerTypePython
415 ServerTypeGeneric
416)
417
418// detectServerType tries to determine what type of LSP server we're dealing with
419func (c *Client) detectServerType() ServerType {
420 if c.Cmd == nil {
421 return ServerTypeUnknown
422 }
423
424 cmdPath := strings.ToLower(c.Cmd.Path)
425
426 switch {
427 case strings.Contains(cmdPath, "gopls"):
428 return ServerTypeGo
429 case strings.Contains(cmdPath, "typescript") || strings.Contains(cmdPath, "vtsls") || strings.Contains(cmdPath, "tsserver"):
430 return ServerTypeTypeScript
431 case strings.Contains(cmdPath, "rust-analyzer"):
432 return ServerTypeRust
433 case strings.Contains(cmdPath, "pyright") || strings.Contains(cmdPath, "pylsp") || strings.Contains(cmdPath, "python"):
434 return ServerTypePython
435 default:
436 return ServerTypeGeneric
437 }
438}
439
440// openKeyConfigFiles opens important configuration files that help initialize the server
441func (c *Client) openKeyConfigFiles(ctx context.Context) {
442 workDir := config.Get().WorkingDir()
443 serverType := c.detectServerType()
444
445 var filesToOpen []string
446
447 switch serverType {
448 case ServerTypeTypeScript:
449 // TypeScript servers need these config files to properly initialize
450 filesToOpen = []string{
451 filepath.Join(workDir, "tsconfig.json"),
452 filepath.Join(workDir, "package.json"),
453 filepath.Join(workDir, "jsconfig.json"),
454 }
455
456 // Also find and open a few TypeScript files to help the server initialize
457 c.openTypeScriptFiles(ctx, workDir)
458 case ServerTypeGo:
459 filesToOpen = []string{
460 filepath.Join(workDir, "go.mod"),
461 filepath.Join(workDir, "go.sum"),
462 }
463 case ServerTypeRust:
464 filesToOpen = []string{
465 filepath.Join(workDir, "Cargo.toml"),
466 filepath.Join(workDir, "Cargo.lock"),
467 }
468 }
469
470 // Try to open each file, ignoring errors if they don't exist
471 for _, file := range filesToOpen {
472 if _, err := os.Stat(file); err == nil {
473 // File exists, try to open it
474 if err := c.OpenFile(ctx, file); err != nil {
475 slog.Debug("Failed to open key config file", "file", file, "error", err)
476 } else {
477 slog.Debug("Opened key config file for initialization", "file", file)
478 }
479 }
480 }
481}
482
483// openTypeScriptFiles finds and opens TypeScript files to help initialize the server
484func (c *Client) openTypeScriptFiles(ctx context.Context, workDir string) {
485 cfg := config.Get()
486 filesOpened := 0
487 maxFilesToOpen := 5 // Limit to a reasonable number of files
488
489 // Find and open TypeScript files
490 err := filepath.WalkDir(workDir, func(path string, d os.DirEntry, err error) error {
491 if err != nil {
492 return err
493 }
494
495 // Skip directories and non-TypeScript files
496 if d.IsDir() {
497 // Skip common directories to avoid wasting time
498 if shouldSkipDir(path) {
499 return filepath.SkipDir
500 }
501 return nil
502 }
503
504 // Check if we've opened enough files
505 if filesOpened >= maxFilesToOpen {
506 return filepath.SkipAll
507 }
508
509 // Check file extension
510 ext := filepath.Ext(path)
511 if ext == ".ts" || ext == ".tsx" || ext == ".js" || ext == ".jsx" {
512 // Try to open the file
513 if err := c.OpenFile(ctx, path); err == nil {
514 filesOpened++
515 if cfg.Options.DebugLSP {
516 slog.Debug("Opened TypeScript file for initialization", "file", path)
517 }
518 }
519 }
520
521 return nil
522 })
523
524 if err != nil && cfg.Options.DebugLSP {
525 slog.Debug("Error walking directory for TypeScript files", "error", err)
526 }
527
528 if cfg.Options.DebugLSP {
529 slog.Debug("Opened TypeScript files for initialization", "count", filesOpened)
530 }
531}
532
533// shouldSkipDir returns true if the directory should be skipped during file search
534func shouldSkipDir(path string) bool {
535 dirName := filepath.Base(path)
536
537 // Skip hidden directories
538 if strings.HasPrefix(dirName, ".") {
539 return true
540 }
541
542 // Skip common directories that won't contain relevant source files
543 skipDirs := map[string]bool{
544 "node_modules": true,
545 "dist": true,
546 "build": true,
547 "coverage": true,
548 "vendor": true,
549 "target": true,
550 }
551
552 return skipDirs[dirName]
553}
554
555type OpenFileInfo struct {
556 Version int32
557 URI protocol.DocumentURI
558}
559
560// HandlesFile checks if this LSP client handles the given file based on its
561// extension.
562func (c *Client) HandlesFile(path string) bool {
563 // If no file types are specified, handle all files (backward compatibility)
564 if len(c.fileTypes) == 0 {
565 return true
566 }
567
568 name := strings.ToLower(filepath.Base(path))
569 for _, filetpe := range c.fileTypes {
570 suffix := strings.ToLower(filetpe)
571 if !strings.HasPrefix(suffix, ".") {
572 suffix = "." + suffix
573 }
574 if strings.HasSuffix(name, suffix) {
575 slog.Debug("handles file", "name", c.name, "file", name, "filetype", filetpe)
576 return true
577 }
578 }
579 slog.Debug("doesn't handle file", "name", c.name, "file", name)
580 return false
581}
582
583func (c *Client) OpenFile(ctx context.Context, filepath string) error {
584 if !c.HandlesFile(filepath) {
585 return nil
586 }
587
588 uri := string(protocol.URIFromPath(filepath))
589
590 c.openFilesMu.Lock()
591 if _, exists := c.openFiles[uri]; exists {
592 c.openFilesMu.Unlock()
593 return nil // Already open
594 }
595 c.openFilesMu.Unlock()
596
597 // Skip files that do not exist or cannot be read
598 content, err := os.ReadFile(filepath)
599 if err != nil {
600 return fmt.Errorf("error reading file: %w", err)
601 }
602
603 params := protocol.DidOpenTextDocumentParams{
604 TextDocument: protocol.TextDocumentItem{
605 URI: protocol.DocumentURI(uri),
606 LanguageID: DetectLanguageID(uri),
607 Version: 1,
608 Text: string(content),
609 },
610 }
611
612 if err := c.Notify(ctx, "textDocument/didOpen", params); err != nil {
613 return err
614 }
615
616 c.openFilesMu.Lock()
617 c.openFiles[uri] = &OpenFileInfo{
618 Version: 1,
619 URI: protocol.DocumentURI(uri),
620 }
621 c.openFilesMu.Unlock()
622
623 return nil
624}
625
626func (c *Client) NotifyChange(ctx context.Context, filepath string) error {
627 uri := string(protocol.URIFromPath(filepath))
628
629 content, err := os.ReadFile(filepath)
630 if err != nil {
631 return fmt.Errorf("error reading file: %w", err)
632 }
633
634 c.openFilesMu.Lock()
635 fileInfo, isOpen := c.openFiles[uri]
636 if !isOpen {
637 c.openFilesMu.Unlock()
638 return fmt.Errorf("cannot notify change for unopened file: %s", filepath)
639 }
640
641 // Increment version
642 fileInfo.Version++
643 version := fileInfo.Version
644 c.openFilesMu.Unlock()
645
646 params := protocol.DidChangeTextDocumentParams{
647 TextDocument: protocol.VersionedTextDocumentIdentifier{
648 TextDocumentIdentifier: protocol.TextDocumentIdentifier{
649 URI: protocol.DocumentURI(uri),
650 },
651 Version: version,
652 },
653 ContentChanges: []protocol.TextDocumentContentChangeEvent{
654 {
655 Value: protocol.TextDocumentContentChangeWholeDocument{
656 Text: string(content),
657 },
658 },
659 },
660 }
661
662 return c.Notify(ctx, "textDocument/didChange", params)
663}
664
665func (c *Client) CloseFile(ctx context.Context, filepath string) error {
666 cfg := config.Get()
667 uri := string(protocol.URIFromPath(filepath))
668
669 c.openFilesMu.Lock()
670 if _, exists := c.openFiles[uri]; !exists {
671 c.openFilesMu.Unlock()
672 return nil // Already closed
673 }
674 c.openFilesMu.Unlock()
675
676 params := protocol.DidCloseTextDocumentParams{
677 TextDocument: protocol.TextDocumentIdentifier{
678 URI: protocol.DocumentURI(uri),
679 },
680 }
681
682 if cfg.Options.DebugLSP {
683 slog.Debug("Closing file", "file", filepath)
684 }
685 if err := c.Notify(ctx, "textDocument/didClose", params); err != nil {
686 return err
687 }
688
689 c.openFilesMu.Lock()
690 delete(c.openFiles, uri)
691 c.openFilesMu.Unlock()
692
693 return nil
694}
695
696func (c *Client) IsFileOpen(filepath string) bool {
697 uri := string(protocol.URIFromPath(filepath))
698 c.openFilesMu.RLock()
699 defer c.openFilesMu.RUnlock()
700 _, exists := c.openFiles[uri]
701 return exists
702}
703
704// CloseAllFiles closes all currently open files
705func (c *Client) CloseAllFiles(ctx context.Context) {
706 cfg := config.Get()
707 c.openFilesMu.Lock()
708 filesToClose := make([]string, 0, len(c.openFiles))
709
710 // First collect all URIs that need to be closed
711 for uri := range c.openFiles {
712 // Convert URI back to file path using proper URI handling
713 filePath, err := protocol.DocumentURI(uri).Path()
714 if err != nil {
715 slog.Error("Failed to convert URI to path for file closing", "uri", uri, "error", err)
716 continue
717 }
718 filesToClose = append(filesToClose, filePath)
719 }
720 c.openFilesMu.Unlock()
721
722 // Then close them all
723 for _, filePath := range filesToClose {
724 err := c.CloseFile(ctx, filePath)
725 if err != nil && cfg.Options.DebugLSP {
726 slog.Warn("Error closing file", "file", filePath, "error", err)
727 }
728 }
729
730 if cfg.Options.DebugLSP {
731 slog.Debug("Closed all files", "files", filesToClose)
732 }
733}
734
735func (c *Client) GetFileDiagnostics(uri protocol.DocumentURI) []protocol.Diagnostic {
736 c.diagnosticsMu.RLock()
737 defer c.diagnosticsMu.RUnlock()
738
739 return c.diagnostics[uri]
740}
741
742// GetDiagnostics returns all diagnostics for all files
743func (c *Client) GetDiagnostics() map[protocol.DocumentURI][]protocol.Diagnostic {
744 c.diagnosticsMu.RLock()
745 defer c.diagnosticsMu.RUnlock()
746
747 return maps.Clone(c.diagnostics)
748}
749
750// OpenFileOnDemand opens a file only if it's not already open
751// This is used for lazy-loading files when they're actually needed
752func (c *Client) OpenFileOnDemand(ctx context.Context, filepath string) error {
753 // Check if the file is already open
754 if c.IsFileOpen(filepath) {
755 return nil
756 }
757
758 // Open the file
759 return c.OpenFile(ctx, filepath)
760}
761
762// GetDiagnosticsForFile ensures a file is open and returns its diagnostics
763// This is useful for on-demand diagnostics when using lazy loading
764func (c *Client) GetDiagnosticsForFile(ctx context.Context, filepath string) ([]protocol.Diagnostic, error) {
765 documentURI := protocol.URIFromPath(filepath)
766
767 // Make sure the file is open
768 if !c.IsFileOpen(filepath) {
769 if err := c.OpenFile(ctx, filepath); err != nil {
770 return nil, fmt.Errorf("failed to open file for diagnostics: %w", err)
771 }
772
773 // Give the LSP server a moment to process the file
774 time.Sleep(100 * time.Millisecond)
775 }
776
777 // Get diagnostics
778 c.diagnosticsMu.RLock()
779 diagnostics := c.diagnostics[documentURI]
780 c.diagnosticsMu.RUnlock()
781
782 return diagnostics, nil
783}
784
785// ClearDiagnosticsForURI removes diagnostics for a specific URI from the cache
786func (c *Client) ClearDiagnosticsForURI(uri protocol.DocumentURI) {
787 c.diagnosticsMu.Lock()
788 defer c.diagnosticsMu.Unlock()
789 delete(c.diagnostics, uri)
790}