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