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