Skip to content

Commit a13fc3f

Browse files
committed
Debounde index updates
1 parent 5dc94bb commit a13fc3f

4 files changed

Lines changed: 177 additions & 5 deletions

File tree

internal/lsp/debounce.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package lsp
2+
3+
import (
4+
"sync"
5+
"time"
6+
)
7+
8+
type documentDebouncer struct {
9+
delay time.Duration
10+
refresh func(string)
11+
12+
mu sync.Mutex
13+
timers map[string]*time.Timer
14+
}
15+
16+
func newDocumentDebouncer(delay time.Duration, refresh func(string)) *documentDebouncer {
17+
return &documentDebouncer{
18+
delay: delay,
19+
refresh: refresh,
20+
timers: make(map[string]*time.Timer),
21+
}
22+
}
23+
24+
func (d *documentDebouncer) Schedule(uri string) {
25+
d.mu.Lock()
26+
defer d.mu.Unlock()
27+
28+
if timer, ok := d.timers[uri]; ok {
29+
timer.Stop()
30+
}
31+
32+
var timer *time.Timer
33+
timer = time.AfterFunc(d.delay, func() {
34+
d.fire(uri, timer)
35+
})
36+
37+
d.timers[uri] = timer
38+
}
39+
40+
func (d *documentDebouncer) Stop() {
41+
d.mu.Lock()
42+
defer d.mu.Unlock()
43+
44+
for uri, timer := range d.timers {
45+
timer.Stop()
46+
delete(d.timers, uri)
47+
}
48+
}
49+
50+
func (d *documentDebouncer) fire(uri string, timer *time.Timer) {
51+
d.mu.Lock()
52+
current, ok := d.timers[uri]
53+
if !ok || current != timer {
54+
d.mu.Unlock()
55+
return
56+
}
57+
58+
delete(d.timers, uri)
59+
d.mu.Unlock()
60+
61+
d.refresh(uri)
62+
}

internal/lsp/debounce_test.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package lsp
2+
3+
import (
4+
"testing"
5+
"time"
6+
7+
"github.com/tliron/glsp"
8+
lsp "github.com/tliron/glsp/protocol_3_16"
9+
)
10+
11+
func TestDocumentDebouncerCoalescesRepeatedSchedules(t *testing.T) {
12+
events := make(chan string, 2)
13+
debouncer := newDocumentDebouncer(20*time.Millisecond, func(uri string) {
14+
events <- uri
15+
})
16+
defer debouncer.Stop()
17+
18+
debouncer.Schedule("file:///tmp/lets.yaml")
19+
debouncer.Schedule("file:///tmp/lets.yaml")
20+
21+
select {
22+
case got := <-events:
23+
if got != "file:///tmp/lets.yaml" {
24+
t.Fatalf("refresh uri = %q, want %q", got, "file:///tmp/lets.yaml")
25+
}
26+
case <-time.After(200 * time.Millisecond):
27+
t.Fatal("timed out waiting for debounced refresh")
28+
}
29+
30+
select {
31+
case got := <-events:
32+
t.Fatalf("unexpected extra refresh for %q", got)
33+
case <-time.After(60 * time.Millisecond):
34+
}
35+
}
36+
37+
func TestTextDocumentDidChangeUsesLatestDocumentAfterDebounce(t *testing.T) {
38+
server := &lspServer{
39+
storage: newStorage(),
40+
parser: newParser(logger),
41+
index: newIndex(logger),
42+
log: logger,
43+
}
44+
server.refresh = newDocumentDebouncer(20*time.Millisecond, server.refreshDocument)
45+
defer server.refresh.Stop()
46+
47+
params := &lsp.DidChangeTextDocumentParams{
48+
TextDocument: lsp.VersionedTextDocumentIdentifier{
49+
TextDocumentIdentifier: lsp.TextDocumentIdentifier{URI: "file:///tmp/lets.yaml"},
50+
},
51+
}
52+
53+
params.ContentChanges = []any{
54+
lsp.TextDocumentContentChangeEventWhole{
55+
Text: `commands:
56+
build:
57+
cmd: echo build`,
58+
},
59+
}
60+
61+
if err := server.textDocumentDidChange(&glsp.Context{}, params); err != nil {
62+
t.Fatalf("first textDocumentDidChange() error = %v", err)
63+
}
64+
65+
params.ContentChanges = []any{
66+
lsp.TextDocumentContentChangeEventWhole{
67+
Text: `commands:
68+
release:
69+
cmd: echo release`,
70+
},
71+
}
72+
73+
if err := server.textDocumentDidChange(&glsp.Context{}, params); err != nil {
74+
t.Fatalf("second textDocumentDidChange() error = %v", err)
75+
}
76+
77+
deadline := time.Now().Add(300 * time.Millisecond)
78+
for time.Now().Before(deadline) {
79+
if _, ok := server.index.findCommand("release"); ok {
80+
if _, ok := server.index.findCommand("build"); ok {
81+
t.Fatal("expected stale command to be removed after debounced refresh")
82+
}
83+
84+
return
85+
}
86+
87+
time.Sleep(10 * time.Millisecond)
88+
}
89+
90+
t.Fatal("timed out waiting for debounced document refresh")
91+
}

internal/lsp/handlers.go

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ func (s *lspServer) initialized(context *glsp.Context, params *lsp.InitializedPa
3535
}
3636

3737
func (s *lspServer) shutdown(context *glsp.Context) error {
38+
if s.refresh != nil {
39+
s.refresh.Stop()
40+
}
41+
3842
lsp.SetTraceValue(lsp.TraceValueOff)
3943
return nil
4044
}
@@ -74,11 +78,20 @@ func (s *lspServer) loadMixins(uri string) {
7478
}
7579
}
7680

81+
func (s *lspServer) refreshDocument(uri string) {
82+
doc := s.storage.GetDocument(uri)
83+
if doc == nil {
84+
return
85+
}
86+
87+
s.index.IndexDocument(uri, *doc)
88+
s.loadMixins(uri)
89+
}
90+
7791
func (s *lspServer) textDocumentDidOpen(context *glsp.Context, params *lsp.DidOpenTextDocumentParams) error {
7892
s.storage.AddDocument(params.TextDocument.URI, params.TextDocument.Text)
7993

80-
go s.index.IndexDocument(params.TextDocument.URI, params.TextDocument.Text)
81-
go s.loadMixins(params.TextDocument.URI)
94+
go s.refreshDocument(params.TextDocument.URI)
8295

8396
return nil
8497
}
@@ -88,9 +101,11 @@ func (s *lspServer) textDocumentDidChange(context *glsp.Context, params *lsp.Did
88101
switch c := change.(type) {
89102
case lsp.TextDocumentContentChangeEventWhole:
90103
s.storage.AddDocument(params.TextDocument.URI, c.Text)
91-
92-
go s.index.IndexDocument(params.TextDocument.URI, c.Text)
93-
go s.loadMixins(params.TextDocument.URI)
104+
if s.refresh != nil {
105+
s.refresh.Schedule(params.TextDocument.URI)
106+
} else {
107+
go s.refreshDocument(params.TextDocument.URI)
108+
}
94109
case lsp.TextDocumentContentChangeEvent:
95110
return errors.New("incremental changes not supported")
96111
}

internal/lsp/server.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package lsp
22

33
import (
44
"context"
5+
"time"
56

67
"github.com/lets-cli/lets/internal/env"
78
"github.com/tliron/commonlog"
@@ -11,6 +12,7 @@ import (
1112
)
1213

1314
const lsName = "lets_ls"
15+
const documentRefreshDebounce = 200 * time.Millisecond
1416

1517
var handler lsp.Handler
1618

@@ -20,6 +22,7 @@ type lspServer struct {
2022
storage *storage
2123
parser *parser
2224
index *index
25+
refresh *documentDebouncer
2326
log commonlog.Logger
2427
}
2528

@@ -60,6 +63,7 @@ func Run(ctx context.Context, version string) error {
6063
index: newIndex(logger),
6164
log: logger,
6265
}
66+
lspServer.refresh = newDocumentDebouncer(documentRefreshDebounce, lspServer.refreshDocument)
6367

6468
handler.Initialize = lspServer.initialize
6569
handler.Initialized = lspServer.initialized

0 commit comments

Comments
 (0)