all repos

clerk @ 336ef9125580710f81bc59d0743eb25def0e505b

missing tooling for ledger/hledger

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

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
lsp: textdocument/selectionRange..., 1 month ago
1
package lsp
2
3
import (
4
	"context"
5
	"errors"
6
	"fmt"
7
	"log/slog"
8
	"os"
9
	"sync"
10
11
	"github.com/go-json-experiment/json"
12
	"github.com/pelletier/go-toml/v2"
13
	"go.lsp.dev/protocol"
14
	"go.lsp.dev/uri"
15
16
	"olexsmir.xyz/clerk/internal/analyzer"
17
	"olexsmir.xyz/clerk/internal/settings"
18
	"olexsmir.xyz/clerk/journal"
19
)
20
21
type server struct {
22
	protocol.UnimplementedServer
23
24
	client protocol.Client
25
	log    *slog.Logger
26
27
	version, name string
28
29
	settings settings.Settings
30
	loader   *journal.Loader
31
32
	mu            sync.RWMutex
33
	openDocs      map[uri.URI]docState
34
	diagCancel    context.CancelFunc
35
	dynFileWather bool
36
	configPath    string
37
}
38
39
// parsedFileFor returns the parsed file for path within the analysis, or nil.
40
func parsedFileFor(an *analyzer.Analysis, path string) *journal.ParsedFile {
41
	for _, pf := range an.Files {
42
		if pf.Path == path {
43
			return pf
44
		}
45
	}
46
	return nil
47
}
48
49
// analysisFor returns the cached analysis for an open doc, rebuilds when the doc or a file it inclues changed.
50
func (s *server) analysisFor(u uri.URI) *analyzer.Analysis {
51
	s.mu.RLock()
52
	state, ok := s.openDocs[u]
53
	if !ok {
54
		s.mu.RUnlock()
55
		return nil
56
	}
57
	if !state.dirty {
58
		an := state.analysis
59
		s.mu.RUnlock()
60
		return an
61
	}
62
	text := state.text
63
	version := state.version
64
	s.mu.RUnlock()
65
66
	an := analyzer.Build(s.loader.ResolveBytes(u.Path(), []byte(text)))
67
68
	s.mu.Lock()
69
	state, ok = s.openDocs[u]
70
	if !ok || state.version != version {
71
		// editot or closed while building. doc stays dirty so te request rebuilds
72
		s.mu.Unlock()
73
		return an
74
	}
75
	state.analysis = an
76
	state.dirty = false
77
	state.paths = make(map[string]bool, len(an.Files))
78
	for _, pf := range an.Files {
79
		state.paths[journal.CanonicalPath(pf.Path)] = true
80
	}
81
	s.openDocs[u] = state
82
	s.mu.Unlock()
83
	return an
84
}
85
86
func (s *server) Initialize(ctx context.Context, params *protocol.InitializeParams) (*protocol.InitializeResult, error) {
87
	if w := params.Capabilities.Workspace; w != nil {
88
		if wf := w.DidChangeWatchedFiles; wf != nil {
89
			s.dynFileWather = wf.DynamicRegistration != nil && *wf.DynamicRegistration
90
		}
91
	}
92
93
	if err := s.applySettings(ctx, params.InitializationOptions); err != nil {
94
		return nil, err
95
	}
96
	full := protocol.SemanticTokensOptionsFull(protocol.Boolean(true))
97
	if td := params.Capabilities.TextDocument; td != nil {
98
		if fd, ok := td.SemanticTokens.Requests.Full.(*protocol.ClientSemanticTokensRequestFullDelta); ok && fd.Delta != nil && *fd.Delta {
99
			full = &protocol.SemanticTokensFullDelta{Delta: new(true)}
100
		}
101
	}
102
103
	return &protocol.InitializeResult{
104
		ServerInfo: protocol.ServerInfo{
105
			Name:    s.name,
106
			Version: protocol.NewOptional(s.version),
107
		},
108
		Capabilities: protocol.ServerCapabilities{
109
			DocumentFormattingProvider: &protocol.DocumentFormattingOptions{},
110
			DefinitionProvider:         protocol.Boolean(true),
111
			HoverProvider:              protocol.Boolean(true),
112
			ReferencesProvider:         protocol.Boolean(true),
113
			WorkspaceSymbolProvider:    protocol.Boolean(true),
114
			DocumentSymbolProvider:     protocol.Boolean(true),
115
			FoldingRangeProvider:       protocol.Boolean(true),
116
			SelectionRangeProvider:     protocol.Boolean(true),
117
			RenameProvider: &protocol.RenameOptions{
118
				PrepareProvider: new(true),
119
			},
120
			CompletionProvider: &protocol.CompletionOptions{
121
				TriggerCharacters: []string{":", "@"},
122
			},
123
			TextDocumentSync: &protocol.TextDocumentSyncOptions{
124
				OpenClose: new(true),
125
				Change:    new(protocol.TextDocumentSyncKindFull),
126
			},
127
			SemanticTokensProvider: &protocol.SemanticTokensOptions{
128
				Legend: getSemanticTokensLegend(),
129
				Range:  protocol.Boolean(true),
130
				Full:   full,
131
			},
132
		},
133
	}, nil
134
}
135
136
func (s *server) Initialized(ctx context.Context, params *protocol.InitializedParams) error {
137
	if s.dynFileWather {
138
		go s.registerFileWatchers(context.Background())
139
	}
140
	s.applyConfigFile(ctx)
141
	s.scheduleDiagnostics(ctx)
142
	return nil
143
}
144
145
func (s *server) DidChangeWatchedFiles(ctx context.Context, params *protocol.DidChangeWatchedFilesParams) error {
146
	for _, change := range params.Changes {
147
		u := change.URI
148
		path := u.Path()
149
		if path == "" {
150
			continue
151
		}
152
		if _, isOpen := s.getDocState(u); isOpen {
153
			continue // editor buffer is authoritative for open docs
154
		}
155
		s.loader.InvalidateFile(path)
156
		s.markDependentsDirty(u)
157
	}
158
	s.scheduleDiagnostics(ctx)
159
	return nil
160
}
161
162
func (s *server) DidChangeConfiguration(ctx context.Context, params *protocol.DidChangeConfigurationParams) error {
163
	return s.applySettings(ctx, params.Settings)
164
}
165
166
func (s *server) Shutdown(ctx context.Context) error {
167
	return nil
168
}
169
170
func (s *server) Exit(ctx context.Context) error {
171
	return nil
172
}
173
174
func (s *server) registerFileWatchers(ctx context.Context) {
175
	if s.client == nil {
176
		return
177
	}
178
	watchers := make([]protocol.FileSystemWatcher, 0, len(journal.SupportedExtensions))
179
	for _, ext := range journal.SupportedExtensions {
180
		watchers = append(watchers, protocol.FileSystemWatcher{GlobPattern: protocol.Pattern("**/*" + ext)})
181
	}
182
	options, err := protocol.Marshal(protocol.DidChangeWatchedFilesRegistrationOptions{Watchers: watchers})
183
	if err != nil {
184
		return
185
	}
186
	if err := s.client.RegisterCapability(ctx, &protocol.RegistrationParams{
187
		Registrations: []protocol.Registration{{
188
			ID:              "clerk.watchedFiles",
189
			Method:          protocol.MethodWorkspaceDidChangeWatchedFiles,
190
			RegisterOptions: protocol.LSPAny(options),
191
		}},
192
	}); err != nil {
193
		s.log.Warn("registering file watchers failed", "err", err)
194
	}
195
}
196
197
func (s *server) applySettings(ctx context.Context, v protocol.LSPAny) error {
198
	if len(v) == 0 {
199
		return nil
200
	}
201
	var raw map[string]any
202
	if err := json.Unmarshal(v, &raw); err != nil {
203
		return fmt.Errorf("invalid settings: %w", err)
204
	}
205
	s.mu.Lock()
206
	warns, err := s.settings.ApplyLSP(raw)
207
	s.mu.Unlock()
208
	for _, w := range warns {
209
		s.reportConfigProblem(ctx, protocol.MessageTypeWarning, w)
210
	}
211
	return err
212
}
213
214
func (s *server) applyConfigFile(ctx context.Context) {
215
	data, err := os.ReadFile(s.configPath)
216
	if err != nil {
217
		if errors.Is(err, os.ErrNotExist) {
218
			return
219
		}
220
		s.reportConfigError(ctx, err)
221
		return
222
	}
223
	var raw map[string]any
224
	if err := toml.Unmarshal(data, &raw); err != nil {
225
		s.reportConfigError(ctx, err)
226
		return
227
	}
228
	s.mu.Lock()
229
	warns, err := s.settings.Apply(raw)
230
	s.mu.Unlock()
231
	if err != nil {
232
		s.reportConfigError(ctx, err)
233
	}
234
	for _, w := range warns {
235
		s.reportConfigProblem(ctx, protocol.MessageTypeWarning, w)
236
	}
237
}
238
239
func (s *server) reportConfigError(ctx context.Context, err error) {
240
	s.reportConfigProblem(ctx, protocol.MessageTypeError, "config "+s.configPath+": "+err.Error())
241
}
242
243
func (s *server) reportConfigProblem(ctx context.Context, typ protocol.MessageType, msg string) {
244
	lvl := slog.LevelWarn
245
	if typ == protocol.MessageTypeError {
246
		lvl = slog.LevelError
247
	}
248
	s.log.Log(ctx, lvl, "config", "message", msg)
249
	if s.client == nil {
250
		return
251
	}
252
	if err := s.client.ShowMessage(ctx, &protocol.ShowMessageParams{Type: typ, Message: msg}); err != nil {
253
		s.log.Warn("window/showMessage failed", "err", err)
254
	}
255
}
256
257
func (s *server) semanticHighlightingEnabled() bool {
258
	s.mu.RLock()
259
	defer s.mu.RUnlock()
260
	return s.settings.SemanticHighlighting
261
}
262
263
func (s *server) latinToCyrillicCompletionEnabled() bool {
264
	s.mu.RLock()
265
	defer s.mu.RUnlock()
266
	return s.settings.LatinToCyrillicCompletion
267
}