all repos

clerk @ 1b9e6e1f8922a34b89b80ea1c5b55997326c3aa5

missing tooling for ledger/hledger

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

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
lsp: comply with lsp3.18 lifecycle, 29 days 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
	lineIdx    *lsputil.LineIndex // cached line index for the text
59
60
	dirty bool           // true while the cached analysis may not reflect the current text
61
	cache *analysisCache // nil until first build
62
	sem   *semCache      // nil until first tokenize
63
}
64
65
type analysisCache struct {
66
	analysis *analyzer.Analysis
67
	fileIdx  map[string]*lsputil.LineIndex
68
	paths    map[string]bool
69
}
70
71
func newAnalysisCache(an *analyzer.Analysis) *analysisCache {
72
	paths := make(map[string]bool, len(an.Files))
73
	for _, pf := range an.Files {
74
		paths[journal.CanonicalPath(pf.Path)] = true
75
	}
76
	return &analysisCache{analysis: an, paths: paths, fileIdx: buildFileIdx(an)}
77
}
78
79
func buildFileIdx(an *analyzer.Analysis) map[string]*lsputil.LineIndex {
80
	idx := make(map[string]*lsputil.LineIndex, len(an.Files))
81
	for _, pf := range an.Files {
82
		idx[pf.Path] = lsputil.NewLineIndex(string(pf.Src))
83
	}
84
	return idx
85
}
86
87
func (s *server) openDoc(u uri.URI, text string, version int32, langID protocol.LanguageKind) {
88
	s.mu.Lock()
89
	s.openDocs[u] = docState{
90
		text:       text,
91
		version:    version,
92
		languageID: langID,
93
		lineIdx:    lsputil.NewLineIndex(text),
94
		dirty:      true,
95
	}
96
	s.mu.Unlock()
97
}
98
99
func (s *server) updateDoc(u uri.URI, version int32, changes []protocol.TextDocumentContentChangeEvent) {
100
	s.mu.Lock()
101
	state, ok := s.openDocs[u]
102
	if !ok {
103
		s.mu.Unlock()
104
		return
105
	}
106
	state.version = version
107
108
	incremental := len(changes) == 1
109
	var edit semEdit
110
	for _, ch := range changes {
111
		switch ev := ch.(type) {
112
		case *protocol.TextDocumentContentChangeWholeDocument:
113
			incremental = false
114
			state.text = ev.Text
115
		case *protocol.TextDocumentContentChangePartial:
116
			if state.sem == nil || state.sem.pending != nil {
117
				incremental = false
118
			}
119
			if state.lineIdx == nil {
120
				state.lineIdx = lsputil.NewLineIndex(state.text)
121
			}
122
			start := state.lineIdx.Offset(int(ev.Range.Start.Line), int(ev.Range.Start.Character))
123
			end := state.lineIdx.Offset(int(ev.Range.End.Line), int(ev.Range.End.Character))
124
			removed := state.text[start:end]
125
			state.text = state.text[:start] + ev.Text + state.text[end:]
126
			if incremental {
127
				edit = semEdit{
128
					start:     start,
129
					oldEnd:    end,
130
					newEnd:    start + len(ev.Text),
131
					deltaLine: strings.Count(ev.Text, "\n") - strings.Count(removed, "\n"),
132
				}
133
			}
134
		}
135
		state.lineIdx = lsputil.NewLineIndex(state.text)
136
	}
137
138
	state.cache = nil
139
	state.dirty = true
140
	if incremental {
141
		state.sem.pending = &edit
142
	} else {
143
		state.sem = nil
144
	}
145
	s.openDocs[u] = state
146
	s.mu.Unlock()
147
148
	s.markDependentsDirty(u)
149
}
150
151
// markDependentsDirty dirties every open doc whose cached analysis includes u,
152
// e.g. after u is edited or closed.
153
func (s *server) markDependentsDirty(u uri.URI) {
154
	canon := journal.CanonicalPath(u.Path())
155
	s.mu.Lock()
156
	for du, dstate := range s.openDocs {
157
		if dstate.cache == nil {
158
			continue
159
		}
160
		if dstate.cache.paths[canon] {
161
			dstate.dirty = true
162
			s.openDocs[du] = dstate
163
		}
164
	}
165
	s.mu.Unlock()
166
}
167
168
func (s *server) getDocState(u uri.URI) (docState, bool) {
169
	s.mu.RLock()
170
	state, ok := s.openDocs[u]
171
	s.mu.RUnlock()
172
	return state, ok
173
}
174
175
func (s *server) closeDoc(u uri.URI) {
176
	s.mu.Lock()
177
	delete(s.openDocs, u)
178
	s.mu.Unlock()
179
}
180
181
func parseJournalStr(content string) *ast.Journal {
182
	l := lexer.New("", []byte(content))
183
	p := parser.New(l)
184
	return p.ParseJournal()
185
}