Skip to content

Commit d66dfa2

Browse files
authored
fix(lsp): use csync for lsp clients (#1073)
The map was being passed down everywhere, but the locking mechanism only ever lived in `app.go`, which might cause concurrent access issues. This changes it to a `*csync.Map`. Signed-off-by: Carlos Alexandro Becker <[email protected]>
1 parent 016b3b6 commit d66dfa2

File tree

13 files changed

+64
-53
lines changed

13 files changed

+64
-53
lines changed

internal/app/app.go

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ import (
66
"errors"
77
"fmt"
88
"log/slog"
9-
"maps"
109
"sync"
1110
"time"
1211

1312
tea "github.com/charmbracelet/bubbletea/v2"
1413
"github.com/charmbracelet/crush/internal/config"
14+
"github.com/charmbracelet/crush/internal/csync"
1515
"github.com/charmbracelet/crush/internal/db"
1616
"github.com/charmbracelet/crush/internal/format"
1717
"github.com/charmbracelet/crush/internal/history"
@@ -33,9 +33,7 @@ type App struct {
3333

3434
CoderAgent agent.Service
3535

36-
LSPClients map[string]*lsp.Client
37-
38-
clientsMutex sync.RWMutex
36+
LSPClients *csync.Map[string, *lsp.Client]
3937

4038
config *config.Config
4139

@@ -66,7 +64,7 @@ func New(ctx context.Context, conn *sql.DB, cfg *config.Config) (*App, error) {
6664
Messages: messages,
6765
History: files,
6866
Permissions: permission.NewPermissionService(cfg.WorkingDir(), skipPermissionsRequests, allowedTools),
69-
LSPClients: make(map[string]*lsp.Client),
67+
LSPClients: csync.NewMap[string, *lsp.Client](),
7068

7169
globalCtx: ctx,
7270

@@ -324,14 +322,8 @@ func (app *App) Shutdown() {
324322
app.CoderAgent.CancelAll()
325323
}
326324

327-
// Get all LSP clients.
328-
app.clientsMutex.RLock()
329-
clients := make(map[string]*lsp.Client, len(app.LSPClients))
330-
maps.Copy(clients, app.LSPClients)
331-
app.clientsMutex.RUnlock()
332-
333325
// Shutdown all LSP clients.
334-
for name, client := range clients {
326+
for name, client := range app.LSPClients.Seq2() {
335327
shutdownCtx, cancel := context.WithTimeout(app.globalCtx, 5*time.Second)
336328
if err := client.Close(shutdownCtx); err != nil {
337329
slog.Error("Failed to shutdown LSP client", "name", name, "error", err)

internal/app/lsp.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,5 @@ func (app *App) createAndStartLSPClient(ctx context.Context, name string, config
7676
slog.Info("LSP client initialized", "name", name)
7777

7878
// Add to map with mutex protection before starting goroutine
79-
app.clientsMutex.Lock()
80-
app.LSPClients[name] = lspClient
81-
app.clientsMutex.Unlock()
79+
app.LSPClients.Set(name, lspClient)
8280
}

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-
v, ok := m.Get(key)
74-
if ok {
75-
m.Del(key)
76-
}
73+
m.mu.Lock()
74+
defer m.mu.Unlock()
75+
v, ok := m.inner[key]
76+
delete(m.inner, key)
7777
return v, ok
7878
}
7979

internal/csync/versionedmap.go

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,50 @@
11
package csync
22

33
import (
4+
"iter"
45
"sync/atomic"
56
)
67

78
// NewVersionedMap creates a new versioned, thread-safe map.
89
func NewVersionedMap[K comparable, V any]() *VersionedMap[K, V] {
910
return &VersionedMap[K, V]{
10-
Map: NewMap[K, V](),
11+
m: NewMap[K, V](),
1112
}
1213
}
1314

1415
// VersionedMap is a thread-safe map that keeps track of its version.
1516
type VersionedMap[K comparable, V any] struct {
16-
*Map[K, V]
17+
m *Map[K, V]
1718
v atomic.Uint64
1819
}
1920

21+
// Get gets the value for the specified key from the map.
22+
func (m *VersionedMap[K, V]) Get(key K) (V, bool) {
23+
return m.m.Get(key)
24+
}
25+
2026
// Set sets the value for the specified key in the map and increments the version.
2127
func (m *VersionedMap[K, V]) Set(key K, value V) {
22-
m.Map.Set(key, value)
28+
m.m.Set(key, value)
2329
m.v.Add(1)
2430
}
2531

2632
// Del deletes the specified key from the map and increments the version.
2733
func (m *VersionedMap[K, V]) Del(key K) {
28-
m.Map.Del(key)
34+
m.m.Del(key)
2935
m.v.Add(1)
3036
}
3137

38+
// Seq2 returns an iter.Seq2 that yields key-value pairs from the map.
39+
func (m *VersionedMap[K, V]) Seq2() iter.Seq2[K, V] {
40+
return m.m.Seq2()
41+
}
42+
43+
// Len returns the number of items in the map.
44+
func (m *VersionedMap[K, V]) Len() int {
45+
return m.m.Len()
46+
}
47+
3248
// Version returns the current version of the map.
3349
func (m *VersionedMap[K, V]) Version() uint64 {
3450
return m.v.Load()

internal/llm/agent/agent.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,7 @@ type agent struct {
8383
summarizeProviderID string
8484

8585
activeRequests *csync.Map[string, context.CancelFunc]
86-
87-
promptQueue *csync.Map[string, []string]
86+
promptQueue *csync.Map[string, []string]
8887
}
8988

9089
var agentPromptMap = map[string]prompt.PromptID{
@@ -100,7 +99,7 @@ func NewAgent(
10099
sessions session.Service,
101100
messages message.Service,
102101
history history.Service,
103-
lspClients map[string]*lsp.Client,
102+
lspClients *csync.Map[string, *lsp.Client],
104103
) (Service, error) {
105104
cfg := config.Get()
106105

@@ -204,7 +203,7 @@ func NewAgent(
204203
withCoderTools := func(t []tools.BaseTool) []tools.BaseTool {
205204
if agentCfg.ID == "coder" {
206205
t = append(t, mcpTools...)
207-
if len(lspClients) > 0 {
206+
if lspClients.Len() > 0 {
208207
t = append(t, tools.NewDiagnosticsTool(lspClients))
209208
}
210209
}

internal/llm/tools/diagnostics.go

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"strings"
1010
"time"
1111

12+
"github.com/charmbracelet/crush/internal/csync"
1213
"github.com/charmbracelet/crush/internal/lsp"
1314
"github.com/charmbracelet/x/powernap/pkg/lsp/protocol"
1415
)
@@ -18,7 +19,7 @@ type DiagnosticsParams struct {
1819
}
1920

2021
type diagnosticsTool struct {
21-
lspClients map[string]*lsp.Client
22+
lspClients *csync.Map[string, *lsp.Client]
2223
}
2324

2425
const (
@@ -46,7 +47,7 @@ TIPS:
4647
`
4748
)
4849

49-
func NewDiagnosticsTool(lspClients map[string]*lsp.Client) BaseTool {
50+
func NewDiagnosticsTool(lspClients *csync.Map[string, *lsp.Client]) BaseTool {
5051
return &diagnosticsTool{
5152
lspClients,
5253
}
@@ -76,20 +77,19 @@ func (b *diagnosticsTool) Run(ctx context.Context, call ToolCall) (ToolResponse,
7677
return NewTextErrorResponse(fmt.Sprintf("error parsing parameters: %s", err)), nil
7778
}
7879

79-
lsps := b.lspClients
80-
if len(lsps) == 0 {
80+
if b.lspClients.Len() == 0 {
8181
return NewTextErrorResponse("no LSP clients available"), nil
8282
}
83-
notifyLSPs(ctx, lsps, params.FilePath)
84-
output := getDiagnostics(params.FilePath, lsps)
83+
notifyLSPs(ctx, b.lspClients, params.FilePath)
84+
output := getDiagnostics(params.FilePath, b.lspClients)
8585
return NewTextResponse(output), nil
8686
}
8787

88-
func notifyLSPs(ctx context.Context, lsps map[string]*lsp.Client, filepath string) {
88+
func notifyLSPs(ctx context.Context, lsps *csync.Map[string, *lsp.Client], filepath string) {
8989
if filepath == "" {
9090
return
9191
}
92-
for _, client := range lsps {
92+
for client := range lsps.Seq() {
9393
if !client.HandlesFile(filepath) {
9494
continue
9595
}
@@ -99,11 +99,11 @@ func notifyLSPs(ctx context.Context, lsps map[string]*lsp.Client, filepath strin
9999
}
100100
}
101101

102-
func getDiagnostics(filePath string, lsps map[string]*lsp.Client) string {
102+
func getDiagnostics(filePath string, lsps *csync.Map[string, *lsp.Client]) string {
103103
fileDiagnostics := []string{}
104104
projectDiagnostics := []string{}
105105

106-
for lspName, client := range lsps {
106+
for lspName, client := range lsps.Seq2() {
107107
for location, diags := range client.GetDiagnostics() {
108108
path, err := location.Path()
109109
if err != nil {

internal/llm/tools/edit.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"strings"
1111
"time"
1212

13+
"github.com/charmbracelet/crush/internal/csync"
1314
"github.com/charmbracelet/crush/internal/diff"
1415
"github.com/charmbracelet/crush/internal/fsext"
1516
"github.com/charmbracelet/crush/internal/history"
@@ -39,7 +40,7 @@ type EditResponseMetadata struct {
3940
}
4041

4142
type editTool struct {
42-
lspClients map[string]*lsp.Client
43+
lspClients *csync.Map[string, *lsp.Client]
4344
permissions permission.Service
4445
files history.Service
4546
workingDir string
@@ -104,7 +105,7 @@ WINDOWS NOTES:
104105
Remember: when making multiple file edits in a row to the same file, you should prefer to send all edits in a single message with multiple calls to this tool, rather than multiple messages with a single call each.`
105106
)
106107

107-
func NewEditTool(lspClients map[string]*lsp.Client, permissions permission.Service, files history.Service, workingDir string) BaseTool {
108+
func NewEditTool(lspClients *csync.Map[string, *lsp.Client], permissions permission.Service, files history.Service, workingDir string) BaseTool {
108109
return &editTool{
109110
lspClients: lspClients,
110111
permissions: permissions,

internal/llm/tools/multiedit.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"strings"
1111
"time"
1212

13+
"github.com/charmbracelet/crush/internal/csync"
1314
"github.com/charmbracelet/crush/internal/diff"
1415
"github.com/charmbracelet/crush/internal/fsext"
1516
"github.com/charmbracelet/crush/internal/history"
@@ -43,7 +44,7 @@ type MultiEditResponseMetadata struct {
4344
}
4445

4546
type multiEditTool struct {
46-
lspClients map[string]*lsp.Client
47+
lspClients *csync.Map[string, *lsp.Client]
4748
permissions permission.Service
4849
files history.Service
4950
workingDir string
@@ -95,7 +96,7 @@ If you want to create a new file, use:
9596
- Subsequent edits: normal edit operations on the created content`
9697
)
9798

98-
func NewMultiEditTool(lspClients map[string]*lsp.Client, permissions permission.Service, files history.Service, workingDir string) BaseTool {
99+
func NewMultiEditTool(lspClients *csync.Map[string, *lsp.Client], permissions permission.Service, files history.Service, workingDir string) BaseTool {
99100
return &multiEditTool{
100101
lspClients: lspClients,
101102
permissions: permissions,

internal/llm/tools/view.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"strings"
1212
"unicode/utf8"
1313

14+
"github.com/charmbracelet/crush/internal/csync"
1415
"github.com/charmbracelet/crush/internal/lsp"
1516
"github.com/charmbracelet/crush/internal/permission"
1617
)
@@ -28,7 +29,7 @@ type ViewPermissionsParams struct {
2829
}
2930

3031
type viewTool struct {
31-
lspClients map[string]*lsp.Client
32+
lspClients *csync.Map[string, *lsp.Client]
3233
workingDir string
3334
permissions permission.Service
3435
}
@@ -81,7 +82,7 @@ TIPS:
8182
- When viewing large files, use the offset parameter to read specific sections`
8283
)
8384

84-
func NewViewTool(lspClients map[string]*lsp.Client, permissions permission.Service, workingDir string) BaseTool {
85+
func NewViewTool(lspClients *csync.Map[string, *lsp.Client], permissions permission.Service, workingDir string) BaseTool {
8586
return &viewTool{
8687
lspClients: lspClients,
8788
workingDir: workingDir,

internal/llm/tools/write.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"strings"
1111
"time"
1212

13+
"github.com/charmbracelet/crush/internal/csync"
1314
"github.com/charmbracelet/crush/internal/diff"
1415
"github.com/charmbracelet/crush/internal/fsext"
1516
"github.com/charmbracelet/crush/internal/history"
@@ -30,7 +31,7 @@ type WritePermissionsParams struct {
3031
}
3132

3233
type writeTool struct {
33-
lspClients map[string]*lsp.Client
34+
lspClients *csync.Map[string, *lsp.Client]
3435
permissions permission.Service
3536
files history.Service
3637
workingDir string
@@ -78,7 +79,7 @@ TIPS:
7879
- Always include descriptive comments when making changes to existing code`
7980
)
8081

81-
func NewWriteTool(lspClients map[string]*lsp.Client, permissions permission.Service, files history.Service, workingDir string) BaseTool {
82+
func NewWriteTool(lspClients *csync.Map[string, *lsp.Client], permissions permission.Service, files history.Service, workingDir string) BaseTool {
8283
return &writeTool{
8384
lspClients: lspClients,
8485
permissions: permissions,

0 commit comments

Comments
 (0)