all repos

clerk @ 2f348cb

missing tooling for ledger/hledger

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

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