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