all repos

clerk @ 23d0a0ddf451ff6ac76c6dd2ffda2cd7d4fe34fc

missing tooling for ledger/hledger

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

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