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