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