Skip to content

Commit 016b3b6

Browse files
authored
feat(lsp): remove internal watcher (#1062)
* feat(lsp): remove internal watcher It was only ever useful if the user edits files through their editor, but we don't really need it assuming we only care about edits done by Crush itself. Basically, we lose that functionality, but save a bunch of file descriptors and have improved perf. Damn good deal if you ask me. Signed-off-by: Carlos Alexandro Becker <[email protected]> * fix: cleanup Signed-off-by: Carlos Alexandro Becker <[email protected]> * fix: remove rlimit Signed-off-by: Carlos Alexandro Becker <[email protected]> * fix: more cleanup Signed-off-by: Carlos Alexandro Becker <[email protected]> --------- Signed-off-by: Carlos Alexandro Becker <[email protected]>
1 parent f02ce4b commit 016b3b6

18 files changed

+169
-1497
lines changed

go.mod

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,6 @@ require (
114114
github.com/ncruces/julianday v1.0.0 // indirect
115115
github.com/pierrec/lz4/v4 v4.1.22 // indirect
116116
github.com/pmezard/go-difflib v1.0.0 // indirect
117-
github.com/raphamorim/notify v0.9.4
118117
github.com/rivo/uniseg v0.4.7
119118
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
120119
github.com/sethvargo/go-retry v0.3.0 // indirect

go.sum

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -237,8 +237,6 @@ github.com/pressly/goose/v3 v3.25.0 h1:6WeYhMWGRCzpyd89SpODFnCBCKz41KrVbRT58nVjG
237237
github.com/pressly/goose/v3 v3.25.0/go.mod h1:4hC1KrritdCxtuFsqgs1R4AU5bWtTAf+cnWvfhf2DNY=
238238
github.com/qjebbs/go-jsons v0.0.0-20221222033332-a534c5fc1c4c h1:kmzxiX+OB0knCo1V0dkEkdPelzCdAzCURCfmFArn2/A=
239239
github.com/qjebbs/go-jsons v0.0.0-20221222033332-a534c5fc1c4c/go.mod h1:wNJrtinHyC3YSf6giEh4FJN8+yZV7nXBjvmfjhBIcw4=
240-
github.com/raphamorim/notify v0.9.4 h1:JXAGOzeR/cnclKkRCZINKS4EtB47O5TD1N1iCkkarTM=
241-
github.com/raphamorim/notify v0.9.4/go.mod h1:3FXSIPyrunV10GCnLGPrpSxoY/Dxi+saeQb9hf+TDSo=
242240
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
243241
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
244242
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
@@ -370,7 +368,6 @@ golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
370368
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
371369
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
372370
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
373-
golang.org/x/sys v0.0.0-20180926160741-c2ed4eda69e7/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
374371
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
375372
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
376373
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

internal/app/app.go

Lines changed: 0 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,11 @@ import (
77
"fmt"
88
"log/slog"
99
"maps"
10-
"os/exec"
11-
"strings"
1210
"sync"
1311
"time"
1412

1513
tea "github.com/charmbracelet/bubbletea/v2"
1614
"github.com/charmbracelet/crush/internal/config"
17-
"github.com/charmbracelet/crush/internal/csync"
1815
"github.com/charmbracelet/crush/internal/db"
1916
"github.com/charmbracelet/crush/internal/format"
2017
"github.com/charmbracelet/crush/internal/history"
@@ -23,7 +20,6 @@ import (
2320
"github.com/charmbracelet/crush/internal/pubsub"
2421

2522
"github.com/charmbracelet/crush/internal/lsp"
26-
"github.com/charmbracelet/crush/internal/lsp/watcher"
2723
"github.com/charmbracelet/crush/internal/message"
2824
"github.com/charmbracelet/crush/internal/permission"
2925
"github.com/charmbracelet/crush/internal/session"
@@ -41,9 +37,6 @@ type App struct {
4137

4238
clientsMutex sync.RWMutex
4339

44-
watcherCancelFuncs *csync.Slice[context.CancelFunc]
45-
lspWatcherWG sync.WaitGroup
46-
4740
config *config.Config
4841

4942
serviceEventsWG *sync.WaitGroup
@@ -56,16 +49,6 @@ type App struct {
5649
cleanupFuncs []func() error
5750
}
5851

59-
// isGitRepo checks if the current directory is a git repository
60-
func isGitRepo() bool {
61-
bts, err := exec.CommandContext(
62-
context.Background(),
63-
"git", "rev-parse",
64-
"--is-inside-work-tree",
65-
).CombinedOutput()
66-
return err == nil && strings.TrimSpace(string(bts)) == "true"
67-
}
68-
6952
// New initializes a new applcation instance.
7053
func New(ctx context.Context, conn *sql.DB, cfg *config.Config) (*App, error) {
7154
q := db.New(conn)
@@ -89,24 +72,13 @@ func New(ctx context.Context, conn *sql.DB, cfg *config.Config) (*App, error) {
8972

9073
config: cfg,
9174

92-
watcherCancelFuncs: csync.NewSlice[context.CancelFunc](),
93-
9475
events: make(chan tea.Msg, 100),
9576
serviceEventsWG: &sync.WaitGroup{},
9677
tuiWG: &sync.WaitGroup{},
9778
}
9879

9980
app.setupEvents()
10081

101-
// Start the global watcher only if this is a git repository
102-
if isGitRepo() {
103-
if err := watcher.Start(); err != nil {
104-
return nil, fmt.Errorf("app: %w", err)
105-
}
106-
} else {
107-
slog.Warn("Not starting global watcher: not a git repository")
108-
}
109-
11082
// Initialize LSP clients in the background.
11183
app.initLSPClients(ctx)
11284

@@ -352,13 +324,6 @@ func (app *App) Shutdown() {
352324
app.CoderAgent.CancelAll()
353325
}
354326

355-
for cancel := range app.watcherCancelFuncs.Seq() {
356-
cancel()
357-
}
358-
359-
// Wait for all LSP watchers to finish.
360-
app.lspWatcherWG.Wait()
361-
362327
// Get all LSP clients.
363328
app.clientsMutex.RLock()
364329
clients := make(map[string]*lsp.Client, len(app.LSPClients))
@@ -374,9 +339,6 @@ func (app *App) Shutdown() {
374339
cancel()
375340
}
376341

377-
// Shutdown the global watcher
378-
watcher.Shutdown()
379-
380342
// Call call cleanup functions.
381343
for _, cleanup := range app.cleanupFuncs {
382344
if cleanup != nil {

internal/app/lsp.go

Lines changed: 0 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@ import (
66
"time"
77

88
"github.com/charmbracelet/crush/internal/config"
9-
"github.com/charmbracelet/crush/internal/log"
109
"github.com/charmbracelet/crush/internal/lsp"
11-
"github.com/charmbracelet/crush/internal/lsp/watcher"
1210
)
1311

1412
// initLSPClients initializes LSP clients.
@@ -77,64 +75,8 @@ func (app *App) createAndStartLSPClient(ctx context.Context, name string, config
7775

7876
slog.Info("LSP client initialized", "name", name)
7977

80-
// Create a child context that can be canceled when the app is shutting
81-
// down.
82-
watchCtx, cancelFunc := context.WithCancel(ctx)
83-
84-
// Create the workspace watcher.
85-
workspaceWatcher := watcher.New(name, lspClient)
86-
87-
// Store the cancel function to be called during cleanup.
88-
app.watcherCancelFuncs.Append(cancelFunc)
89-
9078
// Add to map with mutex protection before starting goroutine
9179
app.clientsMutex.Lock()
9280
app.LSPClients[name] = lspClient
9381
app.clientsMutex.Unlock()
94-
95-
// Run workspace watcher.
96-
app.lspWatcherWG.Add(1)
97-
go app.runWorkspaceWatcher(watchCtx, name, workspaceWatcher)
98-
}
99-
100-
// runWorkspaceWatcher executes the workspace watcher for an LSP client.
101-
func (app *App) runWorkspaceWatcher(ctx context.Context, name string, workspaceWatcher *watcher.Client) {
102-
defer app.lspWatcherWG.Done()
103-
defer log.RecoverPanic("LSP-"+name, func() {
104-
// Try to restart the client.
105-
app.restartLSPClient(ctx, name)
106-
})
107-
108-
workspaceWatcher.Watch(ctx, app.config.WorkingDir())
109-
slog.Info("Workspace watcher stopped", "client", name)
110-
}
111-
112-
// restartLSPClient attempts to restart a crashed or failed LSP client.
113-
func (app *App) restartLSPClient(ctx context.Context, name string) {
114-
// Get the original configuration.
115-
clientConfig, exists := app.config.LSP[name]
116-
if !exists {
117-
slog.Error("Cannot restart client, configuration not found", "client", name)
118-
return
119-
}
120-
121-
// Clean up the old client if it exists.
122-
app.clientsMutex.Lock()
123-
oldClient, exists := app.LSPClients[name]
124-
if exists {
125-
// Remove from map before potentially slow shutdown.
126-
delete(app.LSPClients, name)
127-
}
128-
app.clientsMutex.Unlock()
129-
130-
if exists && oldClient != nil {
131-
// Try to shut down client gracefully, but don't block on errors.
132-
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
133-
_ = oldClient.Close(shutdownCtx)
134-
cancel()
135-
}
136-
137-
// Create a new client using the shared function.
138-
app.createAndStartLSPClient(ctx, name, clientConfig)
139-
slog.Info("Successfully restarted LSP client", "client", name)
14082
}

internal/csync/maps.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,10 @@ func (m *Map[K, V]) GetOrSet(key K, fn func() V) V {
7070

7171
// Take gets an item and then deletes it.
7272
func (m *Map[K, V]) Take(key K) (V, bool) {
73-
m.mu.Lock()
74-
defer m.mu.Unlock()
75-
v, ok := m.inner[key]
76-
delete(m.inner, key)
73+
v, ok := m.Get(key)
74+
if ok {
75+
m.Del(key)
76+
}
7777
return v, ok
7878
}
7979

internal/csync/versionedmap.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package csync
2+
3+
import (
4+
"sync/atomic"
5+
)
6+
7+
// NewVersionedMap creates a new versioned, thread-safe map.
8+
func NewVersionedMap[K comparable, V any]() *VersionedMap[K, V] {
9+
return &VersionedMap[K, V]{
10+
Map: NewMap[K, V](),
11+
}
12+
}
13+
14+
// VersionedMap is a thread-safe map that keeps track of its version.
15+
type VersionedMap[K comparable, V any] struct {
16+
*Map[K, V]
17+
v atomic.Uint64
18+
}
19+
20+
// Set sets the value for the specified key in the map and increments the version.
21+
func (m *VersionedMap[K, V]) Set(key K, value V) {
22+
m.Map.Set(key, value)
23+
m.v.Add(1)
24+
}
25+
26+
// Del deletes the specified key from the map and increments the version.
27+
func (m *VersionedMap[K, V]) Del(key K) {
28+
m.Map.Del(key)
29+
m.v.Add(1)
30+
}
31+
32+
// Version returns the current version of the map.
33+
func (m *VersionedMap[K, V]) Version() uint64 {
34+
return m.v.Load()
35+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package csync
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
)
8+
9+
func TestVersionedMap_Set(t *testing.T) {
10+
t.Parallel()
11+
12+
vm := NewVersionedMap[string, int]()
13+
require.Equal(t, uint64(0), vm.Version())
14+
15+
vm.Set("key1", 42)
16+
require.Equal(t, uint64(1), vm.Version())
17+
18+
value, ok := vm.Get("key1")
19+
require.True(t, ok)
20+
require.Equal(t, 42, value)
21+
}
22+
23+
func TestVersionedMap_Del(t *testing.T) {
24+
t.Parallel()
25+
26+
vm := NewVersionedMap[string, int]()
27+
vm.Set("key1", 42)
28+
initialVersion := vm.Version()
29+
30+
vm.Del("key1")
31+
require.Equal(t, initialVersion+1, vm.Version())
32+
33+
_, ok := vm.Get("key1")
34+
require.False(t, ok)
35+
}
36+
37+
func TestVersionedMap_VersionIncrement(t *testing.T) {
38+
t.Parallel()
39+
40+
vm := NewVersionedMap[string, int]()
41+
initialVersion := vm.Version()
42+
43+
// Setting a value should increment the version
44+
vm.Set("key1", 42)
45+
require.Equal(t, initialVersion+1, vm.Version())
46+
47+
// Deleting a value should increment the version
48+
vm.Del("key1")
49+
require.Equal(t, initialVersion+2, vm.Version())
50+
51+
// Deleting a non-existent key should still increment the version
52+
vm.Del("nonexistent")
53+
require.Equal(t, initialVersion+3, vm.Version())
54+
}
55+
56+
func TestVersionedMap_ConcurrentAccess(t *testing.T) {
57+
t.Parallel()
58+
59+
vm := NewVersionedMap[int, int]()
60+
const numGoroutines = 100
61+
const numOperations = 100
62+
63+
// Initial version
64+
initialVersion := vm.Version()
65+
66+
// Perform concurrent Set and Del operations
67+
for i := range numGoroutines {
68+
go func(id int) {
69+
for j := range numOperations {
70+
key := id*numOperations + j
71+
vm.Set(key, key*2)
72+
vm.Del(key)
73+
}
74+
}(i)
75+
}
76+
77+
// Wait for operations to complete by checking the version
78+
// This is a simplified check - in a real test you might want to use sync.WaitGroup
79+
expectedMinVersion := initialVersion + uint64(numGoroutines*numOperations*2)
80+
81+
// Allow some time for operations to complete
82+
for vm.Version() < expectedMinVersion {
83+
// Busy wait - in a real test you'd use proper synchronization
84+
}
85+
86+
// Final version should be at least the expected minimum
87+
require.GreaterOrEqual(t, vm.Version(), expectedMinVersion)
88+
require.Equal(t, 0, vm.Len())
89+
}

0 commit comments

Comments
 (0)