diff --git a/internal/lsp/client.go b/internal/lsp/client.go index 1105f69fcdc5848888a232b758fa7f1a651c4c1e..dfb9694a7f0e3bf0d03d8c91e70aadb6a042a94b 100644 --- a/internal/lsp/client.go +++ b/internal/lsp/client.go @@ -72,7 +72,6 @@ type Client struct { shutdownChan chan struct{} stderrDone chan struct{} messageHandlerDone chan struct{} - healthCheckDone chan struct{} } // NewClient creates a new LSP client. @@ -113,7 +112,6 @@ func NewClient(ctx context.Context, name string, config config.LSPConfig) (*Clie shutdownChan: make(chan struct{}), stderrDone: make(chan struct{}), messageHandlerDone: make(chan struct{}), - healthCheckDone: make(chan struct{}), } // Initialize server state @@ -150,12 +148,6 @@ func NewClient(ctx context.Context, name string, config config.LSPConfig) (*Clie client.handleMessages() }() - // Start health check and cleanup goroutine - go func() { - defer close(client.healthCheckDone) - client.startHealthCheckAndCleanup(ctx) - }() - return client, nil } @@ -390,8 +382,11 @@ func (c *Client) WaitForServerReady(ctx context.Context) error { c.SetServerState(StateError) return fmt.Errorf("timeout waiting for LSP server to be ready") case <-ticker.C: - // Try a ping method appropriate for this server type - err := c.pingServerByType(ctx, serverType) + // Try a simple workspace/symbol request to check if server is ready + var symbols []protocol.WorkspaceSymbol + err := c.Call(ctx, "workspace/symbol", protocol.WorkspaceSymbolParams{ + Query: "", + }, &symbols) if err == nil { // Server responded successfully c.SetServerState(StateReady) @@ -399,8 +394,6 @@ func (c *Client) WaitForServerReady(ctx context.Context) error { slog.Debug("LSP server is ready") } return nil - } else { - slog.Debug("LSP server not ready yet", "error", err, "serverType", serverType) } if cfg.Options.DebugLSP { @@ -487,88 +480,6 @@ func (c *Client) openKeyConfigFiles(ctx context.Context) { } } -// pingServerByType sends a ping request appropriate for the server type -func (c *Client) pingServerByType(ctx context.Context, serverType ServerType) error { - switch serverType { - case ServerTypeTypeScript: - // For TypeScript, try a document symbol request on an open file - return c.pingTypeScriptServer(ctx) - case ServerTypeGo: - // For Go, workspace/symbol works well - return c.pingWithWorkspaceSymbol(ctx) - case ServerTypeRust: - // For Rust, workspace/symbol works well - return c.pingWithWorkspaceSymbol(ctx) - default: - // Default ping method - return c.pingWithWorkspaceSymbol(ctx) - } -} - -// pingTypeScriptServer tries to ping a TypeScript server with appropriate methods -func (c *Client) pingTypeScriptServer(ctx context.Context) error { - // First try workspace/symbol which works for many servers - if err := c.pingWithWorkspaceSymbol(ctx); err == nil { - return nil - } - - // If that fails, try to find an open file and request document symbols - c.openFilesMu.RLock() - defer c.openFilesMu.RUnlock() - - // If we have any open files, try to get document symbols for one - for uri := range c.openFiles { - filePath, err := protocol.DocumentURI(uri).Path() - if err != nil { - slog.Error("Failed to convert URI to path for TypeScript symbol collection", "uri", uri, "error", err) - continue - } - - if strings.HasSuffix(filePath, ".ts") || strings.HasSuffix(filePath, ".js") || - strings.HasSuffix(filePath, ".tsx") || strings.HasSuffix(filePath, ".jsx") { - var symbols []protocol.DocumentSymbol - err := c.Call(ctx, "textDocument/documentSymbol", protocol.DocumentSymbolParams{ - TextDocument: protocol.TextDocumentIdentifier{ - URI: protocol.DocumentURI(uri), - }, - }, &symbols) - if err == nil { - return nil - } - } - } - - // If we have no open TypeScript files, try to find and open one - workDir := config.Get().WorkingDir() - err := filepath.WalkDir(workDir, func(path string, d os.DirEntry, err error) error { - if err != nil { - return err - } - - // Skip directories and non-TypeScript files - if d.IsDir() { - return nil - } - - ext := filepath.Ext(path) - if ext == ".ts" || ext == ".js" || ext == ".tsx" || ext == ".jsx" { - // Found a TypeScript file, try to open it - if err := c.OpenFile(ctx, path); err == nil { - // Successfully opened, stop walking - return filepath.SkipAll - } - } - - return nil - }) - if err != nil { - slog.Debug("Error walking directory for TypeScript files", "error", err) - } - - // Final fallback - just try a generic capability - return c.pingWithServerCapabilities(ctx) -} - // openTypeScriptFiles finds and opens TypeScript files to help initialize the server func (c *Client) openTypeScriptFiles(ctx context.Context, workDir string) { cfg := config.Get() @@ -641,20 +552,6 @@ func shouldSkipDir(path string) bool { return skipDirs[dirName] } -// pingWithWorkspaceSymbol tries a workspace/symbol request -func (c *Client) pingWithWorkspaceSymbol(ctx context.Context) error { - var result []protocol.SymbolInformation - return c.Call(ctx, "workspace/symbol", protocol.WorkspaceSymbolParams{ - Query: "", - }, &result) -} - -// pingWithServerCapabilities tries to get server capabilities -func (c *Client) pingWithServerCapabilities(ctx context.Context) error { - // This is a very lightweight request that should work for most servers - return c.Notify(ctx, "$/cancelRequest", struct{ ID int }{ID: -1}) -} - type OpenFileInfo struct { Version int32 URI protocol.DocumentURI @@ -891,71 +788,3 @@ func (c *Client) ClearDiagnosticsForURI(uri protocol.DocumentURI) { defer c.diagnosticsMu.Unlock() delete(c.diagnostics, uri) } - -// startHealthCheckAndCleanup runs periodic health checks and cleans up stale handlers -func (c *Client) startHealthCheckAndCleanup(ctx context.Context) { - healthTicker := time.NewTicker(30 * time.Second) - cleanupTicker := time.NewTicker(60 * time.Second) - defer healthTicker.Stop() - defer cleanupTicker.Stop() - - for { - select { - case <-ctx.Done(): - return - case <-c.shutdownChan: - return - case <-healthTicker.C: - // Perform health check - if c.GetServerState() == StateReady { - // Try a simple ping to check if server is still responsive - pingCtx, cancel := context.WithTimeout(ctx, 5*time.Second) - err := c.pingServerByType(pingCtx, c.detectServerType()) - cancel() - if err != nil { - slog.Warn("LSP server health check failed", "name", c.name, "error", err) - c.SetServerState(StateError) - } - } - case <-cleanupTicker.C: - // Clean up stale pending requests - c.cleanupStaleHandlers() - } - } -} - -// cleanupStaleHandlers removes handlers for requests that have been pending too long -func (c *Client) cleanupStaleHandlers() { - threshold := time.Now().Add(-5 * time.Minute) - var staleIDs []int32 - - // Find stale requests - c.pendingRequestsMu.RLock() - for id, timestamp := range c.pendingRequests { - if timestamp.Before(threshold) { - staleIDs = append(staleIDs, id) - } - } - c.pendingRequestsMu.RUnlock() - - if len(staleIDs) == 0 { - return - } - - // Clean up stale handlers - c.handlersMu.Lock() - c.pendingRequestsMu.Lock() - for _, id := range staleIDs { - if ch, exists := c.handlers[id]; exists { - close(ch) - delete(c.handlers, id) - } - delete(c.pendingRequests, id) - } - c.pendingRequestsMu.Unlock() - c.handlersMu.Unlock() - - if len(staleIDs) > 0 { - slog.Debug("Cleaned up stale LSP handlers", "count", len(staleIDs), "name", c.name) - } -} diff --git a/internal/lsp/methods.go b/internal/lsp/methods.go index 096a98225b08a503412180745e5567e97848c96e..95ba57c4fdbce97d55eab3045a4747182ee8c403 100644 --- a/internal/lsp/methods.go +++ b/internal/lsp/methods.go @@ -283,7 +283,6 @@ func (c *Client) Shutdown(ctx context.Context) error { go func() { <-c.stderrDone <-c.messageHandlerDone - <-c.healthCheckDone close(done) }()