on main help sign in
fix(lsp): use csync for lsp clients (#1073)
d66dfa2Carlos Alexandro Becker
internal/app/app.go+4−12
@@ -6,12 +6,12 @@ import (
6 "errors"
6 "errors"
7 "fmt"
7 "fmt"
8 "log/slog"
8 "log/slog"
9− "maps"
10 "sync"
9 "sync"
11 "time"
10 "time"
12
11
13 tea "github.com/charmbracelet/bubbletea/v2"
12 tea "github.com/charmbracelet/bubbletea/v2"
14 "github.com/charmbracelet/crush/internal/config"
13 "github.com/charmbracelet/crush/internal/config"
14+ "github.com/charmbracelet/crush/internal/csync"
15 "github.com/charmbracelet/crush/internal/db"
15 "github.com/charmbracelet/crush/internal/db"
16 "github.com/charmbracelet/crush/internal/format"
16 "github.com/charmbracelet/crush/internal/format"
17 "github.com/charmbracelet/crush/internal/history"
17 "github.com/charmbracelet/crush/internal/history"
@@ -33,9 +33,7 @@ type App struct {
33
33
34 CoderAgent agent.Service
34 CoderAgent agent.Service
35
35
36− LSPClients map[string]*lsp.Client
36+ LSPClients *csync.Map[string, *lsp.Client]
37−
38− clientsMutex sync.RWMutex
39
37
40 config *config.Config
38 config *config.Config
41
39
@@ -66,7 +64,7 @@ func New(ctx context.Context, conn *sql.DB, cfg *config.Config) (*App, error) {
66 Messages: messages,
64 Messages: messages,
67 History: files,
65 History: files,
68 Permissions: permission.NewPermissionService(cfg.WorkingDir(), skipPermissionsRequests, allowedTools),
66 Permissions: permission.NewPermissionService(cfg.WorkingDir(), skipPermissionsRequests, allowedTools),
69− LSPClients: make(map[string]*lsp.Client),
67+ LSPClients: csync.NewMap[string, *lsp.Client](),
70
68
71 globalCtx: ctx,
69 globalCtx: ctx,
72
70
@@ -324,14 +322,8 @@ func (app *App) Shutdown() {
324 app.CoderAgent.CancelAll()
322 app.CoderAgent.CancelAll()
325 }
323 }
326
324
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−
333 // Shutdown all LSP clients.
325 // Shutdown all LSP clients.
334− for name, client := range clients {
326+ for name, client := range app.LSPClients.Seq2() {
335 shutdownCtx, cancel := context.WithTimeout(app.globalCtx, 5*time.Second)
327 shutdownCtx, cancel := context.WithTimeout(app.globalCtx, 5*time.Second)
336 if err := client.Close(shutdownCtx); err != nil {
328 if err := client.Close(shutdownCtx); err != nil {
337 slog.Error("Failed to shutdown LSP client", "name", name, "error", err)
329 slog.Error("Failed to shutdown LSP client", "name", name, "error", err)
internal/app/lsp.go+1−3
@@ -76,7 +76,5 @@ func (app *App) createAndStartLSPClient(ctx context.Context, name string, config
76 slog.Info("LSP client initialized", "name", name)
76 slog.Info("LSP client initialized", "name", name)
77
77
78 // Add to map with mutex protection before starting goroutine
78 // Add to map with mutex protection before starting goroutine
79− app.clientsMutex.Lock()
80− app.LSPClients[name] = lspClient
79+ app.LSPClients.Set(name, lspClient)
81− app.clientsMutex.Unlock()
82}
80}
internal/csync/maps.go+4−4
@@ -70,10 +70,10 @@ func (m *Map[K, V]) GetOrSet(key K, fn func() V) V {
70
70
71// Take gets an item and then deletes it.
71// Take gets an item and then deletes it.
72func (m *Map[K, V]) Take(key K) (V, bool) {
72func (m *Map[K, V]) Take(key K) (V, bool) {
73+ m.mu.Lock()
74+ defer m.mu.Unlock()
73− v, ok := m.Get(key)
75+ v, ok := m.inner[key]
74− if ok {
76+ delete(m.inner, key)
75− m.Del(key)
76− }
77 return v, ok
77 return v, ok
78}
78}
79
79
internal/csync/versionedmap.go+20−4
@@ -1,34 +1,50 @@
1package csync
1package csync
2
2
3import (
3import (
4+ "iter"
4 "sync/atomic"
5 "sync/atomic"
5)
6)
6
7
7// NewVersionedMap creates a new versioned, thread-safe map.
8// NewVersionedMap creates a new versioned, thread-safe map.
8func NewVersionedMap[K comparable, V any]() *VersionedMap[K, V] {
9func NewVersionedMap[K comparable, V any]() *VersionedMap[K, V] {
9 return &VersionedMap[K, V]{
10 return &VersionedMap[K, V]{
10− Map: NewMap[K, V](),
11+ m: NewMap[K, V](),
11 }
12 }
12}
13}
13
14
14// VersionedMap is a thread-safe map that keeps track of its version.
15// VersionedMap is a thread-safe map that keeps track of its version.
15type VersionedMap[K comparable, V any] struct {
16type VersionedMap[K comparable, V any] struct {
16− *Map[K, V]
17+ m *Map[K, V]
17 v atomic.Uint64
18 v atomic.Uint64
18}
19}
19
20
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+
20// Set sets the value for the specified key in the map and increments the version.
26// Set sets the value for the specified key in the map and increments the version.
21func (m *VersionedMap[K, V]) Set(key K, value V) {
27func (m *VersionedMap[K, V]) Set(key K, value V) {
22− m.Map.Set(key, value)
28+ m.m.Set(key, value)
23 m.v.Add(1)
29 m.v.Add(1)
24}
30}
25
31
26// Del deletes the specified key from the map and increments the version.
32// Del deletes the specified key from the map and increments the version.
27func (m *VersionedMap[K, V]) Del(key K) {
33func (m *VersionedMap[K, V]) Del(key K) {
28− m.Map.Del(key)
34+ m.m.Del(key)
29 m.v.Add(1)
35 m.v.Add(1)
30}
36}
31
37
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+
32// Version returns the current version of the map.
48// Version returns the current version of the map.
33func (m *VersionedMap[K, V]) Version() uint64 {
49func (m *VersionedMap[K, V]) Version() uint64 {
34 return m.v.Load()
50 return m.v.Load()
internal/llm/agent/agent.go+3−4
@@ -83,8 +83,7 @@ type agent struct {
83 summarizeProviderID string
83 summarizeProviderID string
84
84
85 activeRequests *csync.Map[string, context.CancelFunc]
85 activeRequests *csync.Map[string, context.CancelFunc]
86−
87− promptQueue *csync.Map[string, []string]
86+ promptQueue *csync.Map[string, []string]
88}
87}
89
88
90var agentPromptMap = map[string]prompt.PromptID{
89var agentPromptMap = map[string]prompt.PromptID{
@@ -100,7 +99,7 @@ func NewAgent(
100 sessions session.Service,
99 sessions session.Service,
101 messages message.Service,
100 messages message.Service,
102 history history.Service,
101 history history.Service,
103− lspClients map[string]*lsp.Client,
102+ lspClients *csync.Map[string, *lsp.Client],
104) (Service, error) {
103) (Service, error) {
105 cfg := config.Get()
104 cfg := config.Get()
106
105
@@ -204,7 +203,7 @@ func NewAgent(
204 withCoderTools := func(t []tools.BaseTool) []tools.BaseTool {
203 withCoderTools := func(t []tools.BaseTool) []tools.BaseTool {
205 if agentCfg.ID == "coder" {
204 if agentCfg.ID == "coder" {
206 t = append(t, mcpTools...)
205 t = append(t, mcpTools...)
207− if len(lspClients) > 0 {
206+ if lspClients.Len() > 0 {
208 t = append(t, tools.NewDiagnosticsTool(lspClients))
207 t = append(t, tools.NewDiagnosticsTool(lspClients))
209 }
208 }
210 }
209 }
internal/llm/tools/diagnostics.go+10−10
@@ -9,6 +9,7 @@ import (
9 "strings"
9 "strings"
10 "time"
10 "time"
11
11
12+ "github.com/charmbracelet/crush/internal/csync"
12 "github.com/charmbracelet/crush/internal/lsp"
13 "github.com/charmbracelet/crush/internal/lsp"
13 "github.com/charmbracelet/x/powernap/pkg/lsp/protocol"
14 "github.com/charmbracelet/x/powernap/pkg/lsp/protocol"
14)
15)
@@ -18,7 +19,7 @@ type DiagnosticsParams struct {
18}
19}
19
20
20type diagnosticsTool struct {
21type diagnosticsTool struct {
21− lspClients map[string]*lsp.Client
22+ lspClients *csync.Map[string, *lsp.Client]
22}
23}
23
24
24const (
25const (
@@ -46,7 +47,7 @@ TIPS:
46`
47`
47)
48)
48
49
49−func NewDiagnosticsTool(lspClients map[string]*lsp.Client) BaseTool {
50+func NewDiagnosticsTool(lspClients *csync.Map[string, *lsp.Client]) BaseTool {
50 return &diagnosticsTool{
51 return &diagnosticsTool{
51 lspClients,
52 lspClients,
52 }
53 }
@@ -76,20 +77,19 @@ func (b *diagnosticsTool) Run(ctx context.Context, call ToolCall) (ToolResponse,
76 return NewTextErrorResponse(fmt.Sprintf("error parsing parameters: %s", err)), nil
77 return NewTextErrorResponse(fmt.Sprintf("error parsing parameters: %s", err)), nil
77 }
78 }
78
79
79− lsps := b.lspClients
80+ if b.lspClients.Len() == 0 {
80− if len(lsps) == 0 {
81 return NewTextErrorResponse("no LSP clients available"), nil
81 return NewTextErrorResponse("no LSP clients available"), nil
82 }
82 }
83− notifyLSPs(ctx, lsps, params.FilePath)
83+ notifyLSPs(ctx, b.lspClients, params.FilePath)
84− output := getDiagnostics(params.FilePath, lsps)
84+ output := getDiagnostics(params.FilePath, b.lspClients)
85 return NewTextResponse(output), nil
85 return NewTextResponse(output), nil
86}
86}
87
87
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) {
89 if filepath == "" {
89 if filepath == "" {
90 return
90 return
91 }
91 }
92− for _, client := range lsps {
92+ for client := range lsps.Seq() {
93 if !client.HandlesFile(filepath) {
93 if !client.HandlesFile(filepath) {
94 continue
94 continue
95 }
95 }
@@ -99,11 +99,11 @@ func notifyLSPs(ctx context.Context, lsps map[string]*lsp.Client, filepath strin
99 }
99 }
100}
100}
101
101
102−func getDiagnostics(filePath string, lsps map[string]*lsp.Client) string {
102+func getDiagnostics(filePath string, lsps *csync.Map[string, *lsp.Client]) string {
103 fileDiagnostics := []string{}
103 fileDiagnostics := []string{}
104 projectDiagnostics := []string{}
104 projectDiagnostics := []string{}
105
105
106− for lspName, client := range lsps {
106+ for lspName, client := range lsps.Seq2() {
107 for location, diags := range client.GetDiagnostics() {
107 for location, diags := range client.GetDiagnostics() {
108 path, err := location.Path()
108 path, err := location.Path()
109 if err != nil {
109 if err != nil {
internal/llm/tools/edit.go+3−2
@@ -10,6 +10,7 @@ import (
10 "strings"
10 "strings"
11 "time"
11 "time"
12
12
13+ "github.com/charmbracelet/crush/internal/csync"
13 "github.com/charmbracelet/crush/internal/diff"
14 "github.com/charmbracelet/crush/internal/diff"
14 "github.com/charmbracelet/crush/internal/fsext"
15 "github.com/charmbracelet/crush/internal/fsext"
15 "github.com/charmbracelet/crush/internal/history"
16 "github.com/charmbracelet/crush/internal/history"
@@ -39,7 +40,7 @@ type EditResponseMetadata struct {
39}
40}
40
41
41type editTool struct {
42type editTool struct {
42− lspClients map[string]*lsp.Client
43+ lspClients *csync.Map[string, *lsp.Client]
43 permissions permission.Service
44 permissions permission.Service
44 files history.Service
45 files history.Service
45 workingDir string
46 workingDir string
@@ -104,7 +105,7 @@ WINDOWS NOTES:
104Remember: 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.`
105Remember: 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.`
105)
106)
106
107
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 {
108 return &editTool{
109 return &editTool{
109 lspClients: lspClients,
110 lspClients: lspClients,
110 permissions: permissions,
111 permissions: permissions,
internal/llm/tools/multiedit.go+3−2
@@ -10,6 +10,7 @@ import (
10 "strings"
10 "strings"
11 "time"
11 "time"
12
12
13+ "github.com/charmbracelet/crush/internal/csync"
13 "github.com/charmbracelet/crush/internal/diff"
14 "github.com/charmbracelet/crush/internal/diff"
14 "github.com/charmbracelet/crush/internal/fsext"
15 "github.com/charmbracelet/crush/internal/fsext"
15 "github.com/charmbracelet/crush/internal/history"
16 "github.com/charmbracelet/crush/internal/history"
@@ -43,7 +44,7 @@ type MultiEditResponseMetadata struct {
43}
44}
44
45
45type multiEditTool struct {
46type multiEditTool struct {
46− lspClients map[string]*lsp.Client
47+ lspClients *csync.Map[string, *lsp.Client]
47 permissions permission.Service
48 permissions permission.Service
48 files history.Service
49 files history.Service
49 workingDir string
50 workingDir string
@@ -95,7 +96,7 @@ If you want to create a new file, use:
95- Subsequent edits: normal edit operations on the created content`
96- Subsequent edits: normal edit operations on the created content`
96)
97)
97
98
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 {
99 return &multiEditTool{
100 return &multiEditTool{
100 lspClients: lspClients,
101 lspClients: lspClients,
101 permissions: permissions,
102 permissions: permissions,
internal/llm/tools/view.go+3−2
@@ -11,6 +11,7 @@ import (
11 "strings"
11 "strings"
12 "unicode/utf8"
12 "unicode/utf8"
13
13
14+ "github.com/charmbracelet/crush/internal/csync"
14 "github.com/charmbracelet/crush/internal/lsp"
15 "github.com/charmbracelet/crush/internal/lsp"
15 "github.com/charmbracelet/crush/internal/permission"
16 "github.com/charmbracelet/crush/internal/permission"
16)
17)
@@ -28,7 +29,7 @@ type ViewPermissionsParams struct {
28}
29}
29
30
30type viewTool struct {
31type viewTool struct {
31− lspClients map[string]*lsp.Client
32+ lspClients *csync.Map[string, *lsp.Client]
32 workingDir string
33 workingDir string
33 permissions permission.Service
34 permissions permission.Service
34}
35}
@@ -81,7 +82,7 @@ TIPS:
81- When viewing large files, use the offset parameter to read specific sections`
82- When viewing large files, use the offset parameter to read specific sections`
82)
83)
83
84
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 {
85 return &viewTool{
86 return &viewTool{
86 lspClients: lspClients,
87 lspClients: lspClients,
87 workingDir: workingDir,
88 workingDir: workingDir,
internal/llm/tools/write.go+3−2
@@ -10,6 +10,7 @@ import (
10 "strings"
10 "strings"
11 "time"
11 "time"
12
12
13+ "github.com/charmbracelet/crush/internal/csync"
13 "github.com/charmbracelet/crush/internal/diff"
14 "github.com/charmbracelet/crush/internal/diff"
14 "github.com/charmbracelet/crush/internal/fsext"
15 "github.com/charmbracelet/crush/internal/fsext"
15 "github.com/charmbracelet/crush/internal/history"
16 "github.com/charmbracelet/crush/internal/history"
@@ -30,7 +31,7 @@ type WritePermissionsParams struct {
30}
31}
31
32
32type writeTool struct {
33type writeTool struct {
33− lspClients map[string]*lsp.Client
34+ lspClients *csync.Map[string, *lsp.Client]
34 permissions permission.Service
35 permissions permission.Service
35 files history.Service
36 files history.Service
36 workingDir string
37 workingDir string
@@ -78,7 +79,7 @@ TIPS:
78- Always include descriptive comments when making changes to existing code`
79- Always include descriptive comments when making changes to existing code`
79)
80)
80
81
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 {
82 return &writeTool{
83 return &writeTool{
83 lspClients: lspClients,
84 lspClients: lspClients,
84 permissions: permissions,
85 permissions: permissions,
internal/tui/components/chat/header/header.go+4−3
@@ -6,6 +6,7 @@ import (
6
6
7 tea "github.com/charmbracelet/bubbletea/v2"
7 tea "github.com/charmbracelet/bubbletea/v2"
8 "github.com/charmbracelet/crush/internal/config"
8 "github.com/charmbracelet/crush/internal/config"
9+ "github.com/charmbracelet/crush/internal/csync"
9 "github.com/charmbracelet/crush/internal/fsext"
10 "github.com/charmbracelet/crush/internal/fsext"
10 "github.com/charmbracelet/crush/internal/lsp"
11 "github.com/charmbracelet/crush/internal/lsp"
11 "github.com/charmbracelet/crush/internal/pubsub"
12 "github.com/charmbracelet/crush/internal/pubsub"
@@ -28,11 +29,11 @@ type Header interface {
28type header struct {
29type header struct {
29 width int
30 width int
30 session session.Session
31 session session.Session
31− lspClients map[string]*lsp.Client
32+ lspClients *csync.Map[string, *lsp.Client]
32 detailsOpen bool
33 detailsOpen bool
33}
34}
34
35
35−func New(lspClients map[string]*lsp.Client) Header {
36+func New(lspClients *csync.Map[string, *lsp.Client]) Header {
36 return &header{
37 return &header{
37 lspClients: lspClients,
38 lspClients: lspClients,
38 width: 0,
39 width: 0,
@@ -104,7 +105,7 @@ func (h *header) details(availWidth int) string {
104 var parts []string
105 var parts []string
105
106
106 errorCount := 0
107 errorCount := 0
107− for _, l := range h.lspClients {
108+ for l := range h.lspClients.Seq() {
108 for _, diagnostics := range l.GetDiagnostics() {
109 for _, diagnostics := range l.GetDiagnostics() {
109 for _, diagnostic := range diagnostics {
110 for _, diagnostic := range diagnostics {
110 if diagnostic.Severity == protocol.SeverityError {
111 if diagnostic.Severity == protocol.SeverityError {
internal/tui/components/chat/sidebar/sidebar.go+2−2
@@ -69,13 +69,13 @@ type sidebarCmp struct {
69 session session.Session
69 session session.Session
70 logo string
70 logo string
71 cwd string
71 cwd string
72− lspClients map[string]*lsp.Client
72+ lspClients *csync.Map[string, *lsp.Client]
73 compactMode bool
73 compactMode bool
74 history history.Service
74 history history.Service
75 files *csync.Map[string, SessionFile]
75 files *csync.Map[string, SessionFile]
76}
76}
77
77
78−func New(history history.Service, lspClients map[string]*lsp.Client, compact bool) Sidebar {
78+func New(history history.Service, lspClients *csync.Map[string, *lsp.Client], compact bool) Sidebar {
79 return &sidebarCmp{
79 return &sidebarCmp{
80 lspClients: lspClients,
80 lspClients: lspClients,
81 history: history,
81 history: history,
internal/tui/components/lsp/lsp.go+4−3
@@ -6,6 +6,7 @@ import (
6
6
7 "github.com/charmbracelet/crush/internal/app"
7 "github.com/charmbracelet/crush/internal/app"
8 "github.com/charmbracelet/crush/internal/config"
8 "github.com/charmbracelet/crush/internal/config"
9+ "github.com/charmbracelet/crush/internal/csync"
9 "github.com/charmbracelet/crush/internal/lsp"
10 "github.com/charmbracelet/crush/internal/lsp"
10 "github.com/charmbracelet/crush/internal/tui/components/core"
11 "github.com/charmbracelet/crush/internal/tui/components/core"
11 "github.com/charmbracelet/crush/internal/tui/styles"
12 "github.com/charmbracelet/crush/internal/tui/styles"
@@ -22,7 +23,7 @@ type RenderOptions struct {
22}
23}
23
24
24// RenderLSPList renders a list of LSP status items with the given options.
25// RenderLSPList renders a list of LSP status items with the given options.
25−func RenderLSPList(lspClients map[string]*lsp.Client, opts RenderOptions) []string {
26+func RenderLSPList(lspClients *csync.Map[string, *lsp.Client], opts RenderOptions) []string {
26 t := styles.CurrentTheme()
27 t := styles.CurrentTheme()
27 lspList := []string{}
28 lspList := []string{}
28
29
@@ -91,7 +92,7 @@ func RenderLSPList(lspClients map[string]*lsp.Client, opts RenderOptions) []stri
91 protocol.SeverityHint: 0,
92 protocol.SeverityHint: 0,
92 protocol.SeverityInformation: 0,
93 protocol.SeverityInformation: 0,
93 }
94 }
94− if client, ok := lspClients[l.Name]; ok {
95+ if client, ok := lspClients.Get(l.Name); ok {
95 for _, diagnostics := range client.GetDiagnostics() {
96 for _, diagnostics := range client.GetDiagnostics() {
96 for _, diagnostic := range diagnostics {
97 for _, diagnostic := range diagnostics {
97 if severity, ok := lspErrs[diagnostic.Severity]; ok {
98 if severity, ok := lspErrs[diagnostic.Severity]; ok {
@@ -134,7 +135,7 @@ func RenderLSPList(lspClients map[string]*lsp.Client, opts RenderOptions) []stri
134}
135}
135
136
136// RenderLSPBlock renders a complete LSP block with optional truncation indicator.
137// RenderLSPBlock renders a complete LSP block with optional truncation indicator.
137−func RenderLSPBlock(lspClients map[string]*lsp.Client, opts RenderOptions, showTruncationIndicator bool) string {
138+func RenderLSPBlock(lspClients *csync.Map[string, *lsp.Client], opts RenderOptions, showTruncationIndicator bool) string {
138 t := styles.CurrentTheme()
139 t := styles.CurrentTheme()
139 lspList := RenderLSPList(lspClients, opts)
140 lspList := RenderLSPList(lspClients, opts)
140
141