all repos

clerk @ a2ef079

missing tooling for ledger/hledger

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

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