diff --git a/internal/lsp/client.go b/internal/lsp/client.go index 0fb73577f62c138abf435a789996a86dbc328993..d8a97a429ea2f3a60e600731c6343a52c51e992b 100644 --- a/internal/lsp/client.go +++ b/internal/lsp/client.go @@ -8,13 +8,13 @@ import ( "maps" "os" "path/filepath" - "strings" "sync" "sync/atomic" "time" "github.com/charmbracelet/crush/internal/config" "github.com/charmbracelet/crush/internal/csync" + "github.com/charmbracelet/crush/internal/fsext" "github.com/charmbracelet/crush/internal/home" powernap "github.com/charmbracelet/x/powernap/pkg/lsp" "github.com/charmbracelet/x/powernap/pkg/lsp/protocol" @@ -35,7 +35,7 @@ type Client struct { debug bool // Working directory this LSP is scoped to. - workDir string + cwd string // File types this LSP server handles (e.g., .go, .rs, .py) fileTypes []string @@ -66,7 +66,14 @@ type Client struct { } // New creates a new LSP client using the powernap implementation. -func New(ctx context.Context, name string, cfg config.LSPConfig, resolver config.VariableResolver, debug bool) (*Client, error) { +func New( + ctx context.Context, + name string, + cfg config.LSPConfig, + resolver config.VariableResolver, + cwd string, + debug bool, +) (*Client, error) { client := &Client{ name: name, fileTypes: cfg.FileTypes, @@ -76,6 +83,7 @@ func New(ctx context.Context, name string, cfg config.LSPConfig, resolver config ctx: ctx, debug: debug, resolver: resolver, + cwd: cwd, } client.serverState.Store(StateStarting) @@ -134,13 +142,7 @@ func (c *Client) Close(ctx context.Context) error { // createPowernapClient creates a new powernap client with the current configuration. func (c *Client) createPowernapClient() error { - workDir, err := os.Getwd() - if err != nil { - return fmt.Errorf("failed to get working directory: %w", err) - } - - rootURI := string(protocol.URIFromPath(workDir)) - c.workDir = workDir + rootURI := string(protocol.URIFromPath(c.cwd)) command, err := c.resolver.ResolveValue(c.config.Command) if err != nil { @@ -157,7 +159,7 @@ func (c *Client) createPowernapClient() error { WorkspaceFolders: []protocol.WorkspaceFolder{ { URI: rootURI, - Name: filepath.Base(workDir), + Name: filepath.Base(c.cwd), }, }, } @@ -321,15 +323,8 @@ type OpenFileInfo struct { // HandlesFile checks if this LSP client handles the given file based on its // extension and whether it's within the working directory. func (c *Client) HandlesFile(path string) bool { - // Check if file is within working directory. - absPath, err := filepath.Abs(path) - if err != nil { - slog.Debug("Cannot resolve path", "name", c.name, "file", path, "error", err) - return false - } - relPath, err := filepath.Rel(c.workDir, absPath) - if err != nil || strings.HasPrefix(relPath, "..") { - slog.Debug("File outside workspace", "name", c.name, "file", path, "workDir", c.workDir) + if !fsext.HasPrefix(path, c.cwd) { + slog.Debug("File outside workspace", "name", c.name, "file", path, "workDir", c.cwd) return false } return handlesFiletype(c.name, c.fileTypes, path) @@ -472,31 +467,6 @@ func (c *Client) OpenFileOnDemand(ctx context.Context, filepath string) error { return c.OpenFile(ctx, filepath) } -// GetDiagnosticsForFile ensures a file is open and returns its diagnostics. -func (c *Client) GetDiagnosticsForFile(ctx context.Context, filepath string) ([]protocol.Diagnostic, error) { - documentURI := protocol.URIFromPath(filepath) - - // Make sure the file is open - if !c.IsFileOpen(filepath) { - if err := c.OpenFile(ctx, filepath); err != nil { - return nil, fmt.Errorf("failed to open file for diagnostics: %w", err) - } - - // Give the LSP server a moment to process the file - time.Sleep(100 * time.Millisecond) - } - - // Get diagnostics - diagnostics, _ := c.diagnostics.Get(documentURI) - - return diagnostics, nil -} - -// ClearDiagnosticsForURI removes diagnostics for a specific URI from the cache. -func (c *Client) ClearDiagnosticsForURI(uri protocol.DocumentURI) { - c.diagnostics.Del(uri) -} - // RegisterNotificationHandler registers a notification handler. func (c *Client) RegisterNotificationHandler(method string, handler transport.NotificationHandler) { c.client.RegisterNotificationHandler(method, handler) @@ -507,11 +477,6 @@ func (c *Client) RegisterServerRequestHandler(method string, handler transport.H c.client.RegisterHandler(method, handler) } -// DidChangeWatchedFiles sends a workspace/didChangeWatchedFiles notification to the server. -func (c *Client) DidChangeWatchedFiles(ctx context.Context, params protocol.DidChangeWatchedFilesParams) error { - return c.client.NotifyDidChangeWatchedFiles(ctx, params.Changes) -} - // openKeyConfigFiles opens important configuration files that help initialize the server. func (c *Client) openKeyConfigFiles(ctx context.Context) { wd, err := os.Getwd() diff --git a/internal/lsp/client_test.go b/internal/lsp/client_test.go index 1de51997f973909a616dc9b07283622b7839a3cb..e444354800b6e41ad26a1d8f0d8ffb097a1f060a 100644 --- a/internal/lsp/client_test.go +++ b/internal/lsp/client_test.go @@ -23,7 +23,7 @@ func TestClient(t *testing.T) { // but we can still test the basic structure client, err := New(ctx, "test", cfg, config.NewEnvironmentVariableResolver(env.NewFromMap(map[string]string{ "THE_CMD": "echo", - })), false) + })), ".", false) if err != nil { // Expected to fail with echo command, skip the rest t.Skipf("Powernap client creation failed as expected with dummy command: %v", err) diff --git a/internal/lsp/manager.go b/internal/lsp/manager.go index fae462557df045d0f4822acb3e770d6057091f6c..88f9d72972350106c9ffc52d85434b0f20ec33aa 100644 --- a/internal/lsp/manager.go +++ b/internal/lsp/manager.go @@ -65,8 +65,8 @@ func NewManager(cfg *config.Config) *Manager { } // Clients returns the map of LSP clients. -func (m *Manager) Clients() *csync.Map[string, *Client] { - return m.clients +func (s *Manager) Clients() *csync.Map[string, *Client] { + return s.clients } // SetCallback sets a callback that is invoked when a new LSP @@ -79,13 +79,17 @@ func (s *Manager) SetCallback(cb func(name string, client *Client)) { // Start starts an LSP server that can handle the given file path. // If an appropriate LSP is already running, this is a no-op. -func (s *Manager) Start(ctx context.Context, filePath string) { +func (s *Manager) Start(ctx context.Context, path string) { + if !fsext.HasPrefix(path, s.cfg.WorkingDir()) { + return + } + s.mu.Lock() defer s.mu.Unlock() var wg sync.WaitGroup for name, server := range s.manager.GetServers() { - if !handles(server, filePath, s.cfg.WorkingDir()) { + if !handles(server, path, s.cfg.WorkingDir()) { continue } wg.Go(func() { @@ -150,7 +154,14 @@ func (s *Manager) startServer(ctx context.Context, name string, server *powernap return } } - client, err := New(ctx, name, cfg, s.cfg.Resolver(), s.cfg.Options.DebugLSP) + client, err := New( + ctx, + name, + cfg, + s.cfg.Resolver(), + s.cfg.WorkingDir(), + s.cfg.Options.DebugLSP, + ) if err != nil { slog.Error("Failed to create LSP client", "name", name, "error", err) return