on main help sign in
fix(lsp): simplify init/ping, store capabilities (#713)

* fix(lsp): simplify init/ping Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com> * feat(lsp): store server capabilities Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com> * fix(lsp): improve init Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com> * fix(lsp): cancel request id Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com> --------- Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>

commit3b9babbc87819a6ec047c6f9ab11d92d45a6ad5d authorCarlos Alexandro Becker <caarlos0@users.noreply.github.com> dateWed 27 Aug 2025, 14:46 (1 year ago) parente2e99af
4 files changed+153−135
internal/lsp/caps.go+112 internal/lsp/client.go+30−126 internal/lsp/transport.go+10−1 internal/lsp/watcher/watcher.go+1−8
internal/lsp/caps.gonew file+112
@@ -0,0 +1,112 @@
1+package lsp
2+
3+import "github.com/charmbracelet/crush/internal/lsp/protocol"
4+
5+func (c *Client) setCapabilities(caps protocol.ServerCapabilities) {
6+ c.capsMu.Lock()
7+ defer c.capsMu.Unlock()
8+ c.caps = caps
9+ c.capsSet.Store(true)
10+}
11+
12+func (c *Client) getCapabilities() (protocol.ServerCapabilities, bool) {
13+ c.capsMu.RLock()
14+ defer c.capsMu.RUnlock()
15+ return c.caps, c.capsSet.Load()
16+}
17+
18+func (c *Client) IsMethodSupported(method string) bool {
19+ // Always allow core lifecycle and generic methods
20+ switch method {
21+ case "initialize", "shutdown", "exit", "$/cancelRequest":
22+ return true
23+ }
24+
25+ caps, ok := c.getCapabilities()
26+ if !ok {
27+ // caps not set yet, be permissive
28+ return true
29+ }
30+
31+ switch method {
32+ case "textDocument/hover":
33+ return caps.HoverProvider != nil
34+ case "textDocument/definition":
35+ return caps.DefinitionProvider != nil
36+ case "textDocument/references":
37+ return caps.ReferencesProvider != nil
38+ case "textDocument/implementation":
39+ return caps.ImplementationProvider != nil
40+ case "textDocument/typeDefinition":
41+ return caps.TypeDefinitionProvider != nil
42+ case "textDocument/documentColor", "textDocument/colorPresentation":
43+ return caps.ColorProvider != nil
44+ case "textDocument/foldingRange":
45+ return caps.FoldingRangeProvider != nil
46+ case "textDocument/declaration":
47+ return caps.DeclarationProvider != nil
48+ case "textDocument/selectionRange":
49+ return caps.SelectionRangeProvider != nil
50+ case "textDocument/prepareCallHierarchy", "callHierarchy/incomingCalls", "callHierarchy/outgoingCalls":
51+ return caps.CallHierarchyProvider != nil
52+ case "textDocument/semanticTokens/full", "textDocument/semanticTokens/full/delta", "textDocument/semanticTokens/range":
53+ return caps.SemanticTokensProvider != nil
54+ case "textDocument/linkedEditingRange":
55+ return caps.LinkedEditingRangeProvider != nil
56+ case "workspace/willCreateFiles":
57+ return caps.Workspace != nil && caps.Workspace.FileOperations != nil && caps.Workspace.FileOperations.WillCreate != nil
58+ case "workspace/willRenameFiles":
59+ return caps.Workspace != nil && caps.Workspace.FileOperations != nil && caps.Workspace.FileOperations.WillRename != nil
60+ case "workspace/willDeleteFiles":
61+ return caps.Workspace != nil && caps.Workspace.FileOperations != nil && caps.Workspace.FileOperations.WillDelete != nil
62+ case "textDocument/moniker":
63+ return caps.MonikerProvider != nil
64+ case "textDocument/prepareTypeHierarchy", "typeHierarchy/supertypes", "typeHierarchy/subtypes":
65+ return caps.TypeHierarchyProvider != nil
66+ case "textDocument/inlineValue":
67+ return caps.InlineValueProvider != nil
68+ case "textDocument/inlayHint", "inlayHint/resolve":
69+ return caps.InlayHintProvider != nil
70+ case "textDocument/diagnostic", "workspace/diagnostic":
71+ return caps.DiagnosticProvider != nil
72+ case "textDocument/inlineCompletion":
73+ return caps.InlineCompletionProvider != nil
74+ case "workspace/textDocumentContent":
75+ return caps.Workspace != nil && caps.Workspace.TextDocumentContent != nil
76+ case "textDocument/willSaveWaitUntil":
77+ if caps.TextDocumentSync == nil {
78+ return false
79+ }
80+ return true
81+ case "textDocument/completion", "completionItem/resolve":
82+ return caps.CompletionProvider != nil
83+ case "textDocument/signatureHelp":
84+ return caps.SignatureHelpProvider != nil
85+ case "textDocument/documentHighlight":
86+ return caps.DocumentHighlightProvider != nil
87+ case "textDocument/documentSymbol":
88+ return caps.DocumentSymbolProvider != nil
89+ case "textDocument/codeAction", "codeAction/resolve":
90+ return caps.CodeActionProvider != nil
91+ case "workspace/symbol", "workspaceSymbol/resolve":
92+ return caps.WorkspaceSymbolProvider != nil
93+ case "textDocument/codeLens", "codeLens/resolve":
94+ return caps.CodeLensProvider != nil
95+ case "textDocument/documentLink", "documentLink/resolve":
96+ return caps.DocumentLinkProvider != nil
97+ case "textDocument/formatting":
98+ return caps.DocumentFormattingProvider != nil
99+ case "textDocument/rangeFormatting":
100+ return caps.DocumentRangeFormattingProvider != nil
101+ case "textDocument/rangesFormatting":
102+ return caps.DocumentRangeFormattingProvider != nil
103+ case "textDocument/onTypeFormatting":
104+ return caps.DocumentOnTypeFormattingProvider != nil
105+ case "textDocument/rename", "textDocument/prepareRename":
106+ return caps.RenameProvider != nil
107+ case "workspace/executeCommand":
108+ return caps.ExecuteCommandProvider != nil
109+ default:
110+ return true
111+ }
112+}
internal/lsp/client.go+30−126
@@ -62,6 +62,11 @@ type Client struct {
6262
6363 // Server state
6464 serverState atomic.Value
65+
66+ // Server capabilities as returned by initialize
67+ caps protocol.ServerCapabilities
68+ capsMu sync.RWMutex
69+ capsSet atomic.Bool
6570}
6671
6772// NewClient creates a new LSP client.
@@ -143,7 +148,7 @@ func (c *Client) RegisterServerRequestHandler(method string, handler ServerReque
143148}
144149
145150func (c *Client) InitializeLSPClient(ctx context.Context, workspaceDir string) (*protocol.InitializeResult, error) {
146− initParams := &protocol.InitializeParams{
151+ initParams := protocol.ParamInitialize{
147152 WorkspaceFoldersInitializeParams: protocol.WorkspaceFoldersInitializeParams{
148153 WorkspaceFolders: []protocol.WorkspaceFolder{
149154 {
@@ -220,12 +225,14 @@ func (c *Client) InitializeLSPClient(ctx context.Context, workspaceDir string) (
220225 },
221226 }
222227
223− var result protocol.InitializeResult
224− if err := c.Call(ctx, "initialize", initParams, &result); err != nil {
228+ result, err := c.Initialize(ctx, initParams)
229+ if err != nil {
225230 return nil, fmt.Errorf("initialize failed: %w", err)
226231 }
227232
228− if err := c.Notify(ctx, "initialized", struct{}{}); err != nil {
233+ c.setCapabilities(result.Capabilities)
234+
235+ if err := c.Initialized(ctx, protocol.InitializedParams{}); err != nil {
229236 return nil, fmt.Errorf("initialized notification failed: %w", err)
230237 }
231238
@@ -234,14 +241,9 @@ func (c *Client) InitializeLSPClient(ctx context.Context, workspaceDir string) (
234241 c.RegisterServerRequestHandler("workspace/configuration", HandleWorkspaceConfiguration)
235242 c.RegisterServerRequestHandler("client/registerCapability", HandleRegisterCapability)
236243 c.RegisterNotificationHandler("window/showMessage", HandleServerMessage)
237− c.RegisterNotificationHandler("textDocument/publishDiagnostics",
238− func(params json.RawMessage) { HandleDiagnostics(c, params) })
239−
240− // Notify the LSP server
241− err := c.Initialized(ctx, protocol.InitializedParams{})
242− if err != nil {
243− return nil, fmt.Errorf("initialization failed: %w", err)
244− }
244+ c.RegisterNotificationHandler("textDocument/publishDiagnostics", func(params json.RawMessage) {
245+ HandleDiagnostics(c, params)
246+ })
245247
246248 return &result, nil
247249}
@@ -329,16 +331,7 @@ func (c *Client) WaitForServerReady(ctx context.Context) error {
329331 slog.Debug("Waiting for LSP server to be ready...")
330332 }
331333
332− // Determine server type for specialized initialization
333− serverType := c.detectServerType()
334−
335− // For TypeScript-like servers, we need to open some key files first
336− if serverType == ServerTypeTypeScript {
337− if cfg.Options.DebugLSP {
338− slog.Debug("TypeScript-like server detected, opening key configuration files")
339− }
340− c.openKeyConfigFiles(ctx)
341− }
334+ c.openKeyConfigFiles(ctx)
342335
343336 for {
344337 select {
@@ -347,21 +340,19 @@ func (c *Client) WaitForServerReady(ctx context.Context) error {
347340 return fmt.Errorf("timeout waiting for LSP server to be ready")
348341 case <-ticker.C:
349342 // Try a ping method appropriate for this server type
350− err := c.pingServerByType(ctx, serverType)
351− if err == nil {
352− // Server responded successfully
353− c.SetServerState(StateReady)
343+ if err := c.ping(ctx); err != nil {
354344 if cfg.Options.DebugLSP {
355− slog.Debug("LSP server is ready")
345+ slog.Debug("LSP server not ready yet", "error", err, "server", c.name)
356346 }
357− return nil
358− } else {
359− slog.Debug("LSP server not ready yet", "error", err, "serverType", serverType)
347+ continue
360348 }
361349
350+ // Server responded successfully
351+ c.SetServerState(StateReady)
362352 if cfg.Options.DebugLSP {
363− slog.Debug("LSP server not ready yet", "error", err, "serverType", serverType)
353+ slog.Debug("LSP server is ready")
364354 }
355+ return nil
365356 }
366357 }
367358}
@@ -443,86 +434,13 @@ func (c *Client) openKeyConfigFiles(ctx context.Context) {
443434 }
444435}
445436
446−// pingServerByType sends a ping request appropriate for the server type
447−func (c *Client) pingServerByType(ctx context.Context, serverType ServerType) error {
448− switch serverType {
449− case ServerTypeTypeScript:
450− // For TypeScript, try a document symbol request on an open file
451− return c.pingTypeScriptServer(ctx)
452− case ServerTypeGo:
453− // For Go, workspace/symbol works well
454− return c.pingWithWorkspaceSymbol(ctx)
455− case ServerTypeRust:
456− // For Rust, workspace/symbol works well
457− return c.pingWithWorkspaceSymbol(ctx)
458− default:
459− // Default ping method
460− return c.pingWithWorkspaceSymbol(ctx)
461− }
462−}
463−
464−// pingTypeScriptServer tries to ping a TypeScript server with appropriate methods
465−func (c *Client) pingTypeScriptServer(ctx context.Context) error {
466− // First try workspace/symbol which works for many servers
467− if err := c.pingWithWorkspaceSymbol(ctx); err == nil {
468− return nil
469− }
470−
471− // If that fails, try to find an open file and request document symbols
472− c.openFilesMu.RLock()
473− defer c.openFilesMu.RUnlock()
474−
475− // If we have any open files, try to get document symbols for one
476− for uri := range c.openFiles {
477− filePath, err := protocol.DocumentURI(uri).Path()
478− if err != nil {
479− slog.Error("Failed to convert URI to path for TypeScript symbol collection", "uri", uri, "error", err)
480− continue
481− }
482−
483− if strings.HasSuffix(filePath, ".ts") || strings.HasSuffix(filePath, ".js") ||
484− strings.HasSuffix(filePath, ".tsx") || strings.HasSuffix(filePath, ".jsx") {
485− var symbols []protocol.DocumentSymbol
486− err := c.Call(ctx, "textDocument/documentSymbol", protocol.DocumentSymbolParams{
487− TextDocument: protocol.TextDocumentIdentifier{
488− URI: protocol.DocumentURI(uri),
489− },
490− }, &symbols)
491− if err == nil {
492− return nil
493− }
494− }
495− }
496−
497− // If we have no open TypeScript files, try to find and open one
498− workDir := config.Get().WorkingDir()
499− err := filepath.WalkDir(workDir, func(path string, d os.DirEntry, err error) error {
500− if err != nil {
501− return err
502− }
503−
504− // Skip directories and non-TypeScript files
505− if d.IsDir() {
506− return nil
507− }
508−
509− ext := filepath.Ext(path)
510− if ext == ".ts" || ext == ".js" || ext == ".tsx" || ext == ".jsx" {
511− // Found a TypeScript file, try to open it
512− if err := c.OpenFile(ctx, path); err == nil {
513− // Successfully opened, stop walking
514− return filepath.SkipAll
515− }
516− }
517−
437+// ping sends a ping request...
438+func (c *Client) ping(ctx context.Context) error {
439+ if _, err := c.Symbol(ctx, protocol.WorkspaceSymbolParams{}); err == nil {
518440 return nil
519− })
520− if err != nil {
521− slog.Debug("Error walking directory for TypeScript files", "error", err)
522441 }
523−
524− // Final fallback - just try a generic capability
525− return c.pingWithServerCapabilities(ctx)
442+ // This is a very lightweight request that should work for most servers
443+ return c.Notify(ctx, "$/cancelRequest", protocol.CancelParams{ID: "1"})
526444}
527445
528446// openTypeScriptFiles finds and opens TypeScript files to help initialize the server
@@ -597,20 +515,6 @@ func shouldSkipDir(path string) bool {
597515 return skipDirs[dirName]
598516}
599517
600−// pingWithWorkspaceSymbol tries a workspace/symbol request
601−func (c *Client) pingWithWorkspaceSymbol(ctx context.Context) error {
602− var result []protocol.SymbolInformation
603− return c.Call(ctx, "workspace/symbol", protocol.WorkspaceSymbolParams{
604− Query: "",
605− }, &result)
606−}
607−
608−// pingWithServerCapabilities tries to get server capabilities
609−func (c *Client) pingWithServerCapabilities(ctx context.Context) error {
610− // This is a very lightweight request that should work for most servers
611− return c.Notify(ctx, "$/cancelRequest", struct{ ID int }{ID: -1})
612−}
613−
614518type OpenFileInfo struct {
615519 Version int32
616520 URI protocol.DocumentURI
@@ -668,7 +572,7 @@ func (c *Client) OpenFile(ctx context.Context, filepath string) error {
668572 },
669573 }
670574
671− if err := c.Notify(ctx, "textDocument/didOpen", params); err != nil {
575+ if err := c.DidOpen(ctx, params); err != nil {
672576 return err
673577 }
674578
@@ -718,7 +622,7 @@ func (c *Client) NotifyChange(ctx context.Context, filepath string) error {
718622 },
719623 }
720624
721− return c.Notify(ctx, "textDocument/didChange", params)
625+ return c.DidChange(ctx, params)
722626}
723627
724628func (c *Client) CloseFile(ctx context.Context, filepath string) error {
@@ -741,7 +645,7 @@ func (c *Client) CloseFile(ctx context.Context, filepath string) error {
741645 if cfg.Options.DebugLSP {
742646 slog.Debug("Closing file", "file", filepath)
743647 }
744− if err := c.Notify(ctx, "textDocument/didClose", params); err != nil {
648+ if err := c.DidClose(ctx, params); err != nil {
745649 return err
746650 }
747651
internal/lsp/transport.go+10−1
@@ -188,9 +188,12 @@ func (c *Client) handleMessages() {
188188
189189// Call makes a request and waits for the response
190190func (c *Client) Call(ctx context.Context, method string, params any, result any) error {
191− cfg := config.Get()
191+ if !c.IsMethodSupported(method) {
192+ return fmt.Errorf("method not supported by server: %s", method)
193+ }
192194 id := c.nextID.Add(1)
193195
196+ cfg := config.Get()
194197 if cfg.Options.DebugLSP {
195198 slog.Debug("Making call", "method", method, "id", id)
196199 }
@@ -253,6 +256,12 @@ func (c *Client) Call(ctx context.Context, method string, params any, result any
253256// Notify sends a notification (a request without an ID that doesn't expect a response)
254257func (c *Client) Notify(ctx context.Context, method string, params any) error {
255258 cfg := config.Get()
259+ if !c.IsMethodSupported(method) {
260+ if cfg.Options.DebugLSP {
261+ slog.Debug("Skipping notification: method not supported by server", "method", method)
262+ }
263+ return nil
264+ }
256265 if cfg.Options.DebugLSP {
257266 slog.Debug("Sending notification", "method", method)
258267 }
internal/lsp/watcher/watcher.go+1−8
@@ -623,18 +623,11 @@ func (w *WorkspaceWatcher) matchesPattern(path string, pattern protocol.GlobPatt
623623 if basePath == "" {
624624 return false
625625 }
626− // For relative patterns
627− if basePath, err = protocol.DocumentURI(basePath).Path(); err != nil {
628− // XXX: Do we want to return here, or send the error up the stack?
629− slog.Error("Error converting base path to URI", "basePath", basePath, "error", err)
630− }
631−
632− basePath = filepath.ToSlash(basePath)
633626
634627 // Make path relative to basePath for matching
635628 relPath, err := filepath.Rel(basePath, path)
636629 if err != nil {
637− slog.Error("Error getting relative path", "path", path, "basePath", basePath, "error", err)
630+ slog.Error("Error getting relative path", "path", path, "basePath", basePath, "error", err, "server", w.name)
638631 return false
639632 }
640633 relPath = filepath.ToSlash(relPath)