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