all repos

clerk @ 6573acb6af84cdef8bdde568c92ebc1509eb600f

missing tooling for ledger/hledger

clerk/internal/lsp/textdocument_sync.go (view raw)

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
lsp: incremental semantic tokens update, 1 month ago
1
package lsp
2
3
import (
4
	"context"
5
	"strings"
6
7
	"go.lsp.dev/protocol"
8
	"go.lsp.dev/uri"
9
10
	"olexsmir.xyz/clerk/internal/analyzer"
11
	"olexsmir.xyz/clerk/internal/lsp/lsputil"
12
	"olexsmir.xyz/clerk/journal"
13
	"olexsmir.xyz/clerk/journal/ast"
14
	"olexsmir.xyz/clerk/journal/lexer"
15
	"olexsmir.xyz/clerk/journal/parser"
16
)
17
18
func (s *server) DidOpen(ctx context.Context, params *protocol.DidOpenTextDocumentParams) error {
19
	s.openDoc(params.TextDocument.URI, params.TextDocument.Text, params.TextDocument.Version, params.TextDocument.LanguageID)
20
	s.scheduleDiagnostics(ctx)
21
	return nil
22
}
23
24
func (s *server) DidChange(ctx context.Context, params *protocol.DidChangeTextDocumentParams) error {
25
	s.updateDoc(params.TextDocument.URI, params.TextDocument.Version, params.ContentChanges)
26
	s.scheduleDiagnostics(ctx)
27
	return nil
28
}
29
30
func (s *server) DidClose(ctx context.Context, params *protocol.DidCloseTextDocumentParams) error {
31
	u := params.TextDocument.URI
32
	s.closeDoc(u)
33
	s.loader.Evict(u.Path())
34
35
	// clear closed doc's diagnostics; dependents rebuild from disk since the buffer text is gone.
36
	s.markDependentsDirty(u)
37
	if err := s.client.PublishDiagnostics(ctx, &protocol.PublishDiagnosticsParams{
38
		URI:         u,
39
		Diagnostics: []protocol.Diagnostic{},
40
	}); err != nil {
41
		s.log.Warn("clear diagnostics failed", "uri", u, "err", err)
42
	}
43
44
	s.scheduleDiagnostics(ctx)
45
	return nil
46
}
47
48
func (s *server) DidSave(ctx context.Context, params *protocol.DidSaveTextDocumentParams) error {
49
	return nil
50
}
51
52
// Document management
53
54
type docState struct {
55
	text       string
56
	version    int32
57
	languageID protocol.LanguageKind
58
	paths      map[string]bool    // canonical paths of every file in the cached analysis
59
	lineIdx    *lsputil.LineIndex // cached line index for the text
60
61
	analysis *analyzer.Analysis // cached analysis, nil until first build
62
	sem      *semCache          // semantic token cache, nil until first tokenize
63
	dirty    bool               // true while the cached analysis may not reflect the current text
64
}
65
66
func (s *server) openDoc(u uri.URI, text string, version int32, langID protocol.LanguageKind) {
67
	s.mu.Lock()
68
	s.openDocs[u] = docState{
69
		text:       text,
70
		version:    version,
71
		languageID: langID,
72
		lineIdx:    lsputil.NewLineIndex(text),
73
		dirty:      true,
74
	}
75
	s.mu.Unlock()
76
}
77
78
func (s *server) updateDoc(u uri.URI, version int32, changes []protocol.TextDocumentContentChangeEvent) {
79
	s.mu.Lock()
80
	state, ok := s.openDocs[u]
81
	if !ok {
82
		s.mu.Unlock()
83
		return
84
	}
85
	state.version = version
86
87
	incremental := len(changes) == 1
88
	var edit semEdit
89
	for _, ch := range changes {
90
		switch ev := ch.(type) {
91
		case *protocol.TextDocumentContentChangeWholeDocument:
92
			incremental = false
93
			state.text = ev.Text
94
		case *protocol.TextDocumentContentChangePartial:
95
			if state.sem == nil || state.sem.pending != nil {
96
				incremental = false
97
			}
98
			if state.lineIdx == nil {
99
				state.lineIdx = lsputil.NewLineIndex(state.text)
100
			}
101
			start := state.lineIdx.Offset(int(ev.Range.Start.Line), int(ev.Range.Start.Character))
102
			end := state.lineIdx.Offset(int(ev.Range.End.Line), int(ev.Range.End.Character))
103
			removed := state.text[start:end]
104
			state.text = state.text[:start] + ev.Text + state.text[end:]
105
			if incremental {
106
				edit = semEdit{
107
					start:     start,
108
					oldEnd:    end,
109
					newEnd:    start + len(ev.Text),
110
					deltaLine: strings.Count(ev.Text, "\n") - strings.Count(removed, "\n"),
111
				}
112
			}
113
		}
114
		state.lineIdx = lsputil.NewLineIndex(state.text)
115
	}
116
117
	state.analysis = nil
118
	state.dirty = true
119
	if incremental {
120
		state.sem.pending = &edit
121
	} else {
122
		state.sem = nil
123
	}
124
	s.openDocs[u] = state
125
	s.mu.Unlock()
126
127
	s.markDependentsDirty(u)
128
}
129
130
// markDependentsDirty dirties every open doc whose cached analysis includes u,
131
// e.g. after u is edited or closed.
132
func (s *server) markDependentsDirty(u uri.URI) {
133
	canon := journal.CanonicalPath(u.Path())
134
	s.mu.Lock()
135
	for du, dstate := range s.openDocs {
136
		if dstate.paths[canon] {
137
			dstate.dirty = true
138
			s.openDocs[du] = dstate
139
		}
140
	}
141
	s.mu.Unlock()
142
}
143
144
func (s *server) getDocState(u uri.URI) (docState, bool) {
145
	s.mu.RLock()
146
	state, ok := s.openDocs[u]
147
	s.mu.RUnlock()
148
	return state, ok
149
}
150
151
func (s *server) closeDoc(u uri.URI) {
152
	s.mu.Lock()
153
	delete(s.openDocs, u)
154
	s.mu.Unlock()
155
}
156
157
func parseJournalStr(content string) *ast.Journal {
158
	l := lexer.New("", []byte(content))
159
	p := parser.New(l)
160
	return p.ParseJournal()
161
}