on main help sign in
fix(lsp): files outside cwd (#2180)

closes #1401 Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>

commitd6643a64aca64744d4156126835d8bbfccce2f73 authorCarlos Alexandro Becker <caarlos0@users.noreply.github.com> dateTue 10 Feb 2026, 15:47 (8 months ago) parent48157c6
3 files changed+32−56
internal/lsp/client.go+15−50 internal/lsp/client_test.go+1−1 internal/lsp/manager.go+16−5
internal/lsp/client.go+15−50
@@ -8,13 +8,13 @@ import (
88 "maps"
99 "os"
1010 "path/filepath"
11− "strings"
1211 "sync"
1312 "sync/atomic"
1413 "time"
1514
1615 "github.com/charmbracelet/crush/internal/config"
1716 "github.com/charmbracelet/crush/internal/csync"
17+ "github.com/charmbracelet/crush/internal/fsext"
1818 "github.com/charmbracelet/crush/internal/home"
1919 powernap "github.com/charmbracelet/x/powernap/pkg/lsp"
2020 "github.com/charmbracelet/x/powernap/pkg/lsp/protocol"
@@ -35,7 +35,7 @@ type Client struct {
3535 debug bool
3636
3737 // Working directory this LSP is scoped to.
38− workDir string
38+ cwd string
3939
4040 // File types this LSP server handles (e.g., .go, .rs, .py)
4141 fileTypes []string
@@ -66,7 +66,14 @@ type Client struct {
6666}
6767
6868// New creates a new LSP client using the powernap implementation.
69−func New(ctx context.Context, name string, cfg config.LSPConfig, resolver config.VariableResolver, debug bool) (*Client, error) {
69+func New(
70+ ctx context.Context,
71+ name string,
72+ cfg config.LSPConfig,
73+ resolver config.VariableResolver,
74+ cwd string,
75+ debug bool,
76+) (*Client, error) {
7077 client := &Client{
7178 name: name,
7279 fileTypes: cfg.FileTypes,
@@ -76,6 +83,7 @@ func New(ctx context.Context, name string, cfg config.LSPConfig, resolver config
7683 ctx: ctx,
7784 debug: debug,
7885 resolver: resolver,
86+ cwd: cwd,
7987 }
8088 client.serverState.Store(StateStarting)
8189
@@ -134,13 +142,7 @@ func (c *Client) Close(ctx context.Context) error {
134142
135143// createPowernapClient creates a new powernap client with the current configuration.
136144func (c *Client) createPowernapClient() error {
137− workDir, err := os.Getwd()
138− if err != nil {
139− return fmt.Errorf("failed to get working directory: %w", err)
140− }
141−
142− rootURI := string(protocol.URIFromPath(workDir))
143− c.workDir = workDir
145+ rootURI := string(protocol.URIFromPath(c.cwd))
144146
145147 command, err := c.resolver.ResolveValue(c.config.Command)
146148 if err != nil {
@@ -157,7 +159,7 @@ func (c *Client) createPowernapClient() error {
157159 WorkspaceFolders: []protocol.WorkspaceFolder{
158160 {
159161 URI: rootURI,
160− Name: filepath.Base(workDir),
162+ Name: filepath.Base(c.cwd),
161163 },
162164 },
163165 }
@@ -321,15 +323,8 @@ type OpenFileInfo struct {
321323// HandlesFile checks if this LSP client handles the given file based on its
322324// extension and whether it's within the working directory.
323325func (c *Client) HandlesFile(path string) bool {
324− // Check if file is within working directory.
325− absPath, err := filepath.Abs(path)
326− if err != nil {
327− slog.Debug("Cannot resolve path", "name", c.name, "file", path, "error", err)
328− return false
329− }
330− relPath, err := filepath.Rel(c.workDir, absPath)
331− if err != nil || strings.HasPrefix(relPath, "..") {
332− slog.Debug("File outside workspace", "name", c.name, "file", path, "workDir", c.workDir)
326+ if !fsext.HasPrefix(path, c.cwd) {
327+ slog.Debug("File outside workspace", "name", c.name, "file", path, "workDir", c.cwd)
333328 return false
334329 }
335330 return handlesFiletype(c.name, c.fileTypes, path)
@@ -472,31 +467,6 @@ func (c *Client) OpenFileOnDemand(ctx context.Context, filepath string) error {
472467 return c.OpenFile(ctx, filepath)
473468}
474469
475−// GetDiagnosticsForFile ensures a file is open and returns its diagnostics.
476−func (c *Client) GetDiagnosticsForFile(ctx context.Context, filepath string) ([]protocol.Diagnostic, error) {
477− documentURI := protocol.URIFromPath(filepath)
478−
479− // Make sure the file is open
480− if !c.IsFileOpen(filepath) {
481− if err := c.OpenFile(ctx, filepath); err != nil {
482− return nil, fmt.Errorf("failed to open file for diagnostics: %w", err)
483− }
484−
485− // Give the LSP server a moment to process the file
486− time.Sleep(100 * time.Millisecond)
487− }
488−
489− // Get diagnostics
490− diagnostics, _ := c.diagnostics.Get(documentURI)
491−
492− return diagnostics, nil
493−}
494−
495−// ClearDiagnosticsForURI removes diagnostics for a specific URI from the cache.
496−func (c *Client) ClearDiagnosticsForURI(uri protocol.DocumentURI) {
497− c.diagnostics.Del(uri)
498−}
499−
500470// RegisterNotificationHandler registers a notification handler.
501471func (c *Client) RegisterNotificationHandler(method string, handler transport.NotificationHandler) {
502472 c.client.RegisterNotificationHandler(method, handler)
@@ -507,11 +477,6 @@ func (c *Client) RegisterServerRequestHandler(method string, handler transport.H
507477 c.client.RegisterHandler(method, handler)
508478}
509479
510−// DidChangeWatchedFiles sends a workspace/didChangeWatchedFiles notification to the server.
511−func (c *Client) DidChangeWatchedFiles(ctx context.Context, params protocol.DidChangeWatchedFilesParams) error {
512− return c.client.NotifyDidChangeWatchedFiles(ctx, params.Changes)
513−}
514−
515480// openKeyConfigFiles opens important configuration files that help initialize the server.
516481func (c *Client) openKeyConfigFiles(ctx context.Context) {
517482 wd, err := os.Getwd()
internal/lsp/client_test.go+1−1
@@ -23,7 +23,7 @@ func TestClient(t *testing.T) {
2323 // but we can still test the basic structure
2424 client, err := New(ctx, "test", cfg, config.NewEnvironmentVariableResolver(env.NewFromMap(map[string]string{
2525 "THE_CMD": "echo",
26− })), false)
26+ })), ".", false)
2727 if err != nil {
2828 // Expected to fail with echo command, skip the rest
2929 t.Skipf("Powernap client creation failed as expected with dummy command: %v", err)
internal/lsp/manager.go+16−5
@@ -65,8 +65,8 @@ func NewManager(cfg *config.Config) *Manager {
6565}
6666
6767// Clients returns the map of LSP clients.
68−func (m *Manager) Clients() *csync.Map[string, *Client] {
69− return m.clients
68+func (s *Manager) Clients() *csync.Map[string, *Client] {
69+ return s.clients
7070}
7171
7272// 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)) {
7979
8080// Start starts an LSP server that can handle the given file path.
8181// If an appropriate LSP is already running, this is a no-op.
82−func (s *Manager) Start(ctx context.Context, filePath string) {
82+func (s *Manager) Start(ctx context.Context, path string) {
83+ if !fsext.HasPrefix(path, s.cfg.WorkingDir()) {
84+ return
85+ }
86+
8387 s.mu.Lock()
8488 defer s.mu.Unlock()
8589
8690 var wg sync.WaitGroup
8791 for name, server := range s.manager.GetServers() {
88− if !handles(server, filePath, s.cfg.WorkingDir()) {
92+ if !handles(server, path, s.cfg.WorkingDir()) {
8993 continue
9094 }
9195 wg.Go(func() {
@@ -150,7 +154,14 @@ func (s *Manager) startServer(ctx context.Context, name string, server *powernap
150154 return
151155 }
152156 }
153− client, err := New(ctx, name, cfg, s.cfg.Resolver(), s.cfg.Options.DebugLSP)
157+ client, err := New(
158+ ctx,
159+ name,
160+ cfg,
161+ s.cfg.Resolver(),
162+ s.cfg.WorkingDir(),
163+ s.cfg.Options.DebugLSP,
164+ )
154165 if err != nil {
155166 slog.Error("Failed to create LSP client", "name", name, "error", err)
156167 return