all repos

clerk @ 122b3849cfb7f6cd41be7fcceaeced24df42dae9

missing tooling for ledger/hledger

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

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
lsp: implement textDocument/semanticTokens/delta, 1 month ago
1
package lsp
2
3
import (
4
	"context"
5
	"log/slog"
6
	"sync"
7
8
	"go.lsp.dev/protocol"
9
	"go.lsp.dev/uri"
10
11
	"olexsmir.xyz/clerk/internal/analyzer"
12
	"olexsmir.xyz/clerk/internal/linter"
13
	"olexsmir.xyz/clerk/journal"
14
	"olexsmir.xyz/clerk/journal/printer"
15
)
16
17
type server struct {
18
	protocol.UnimplementedServer
19
20
	client protocol.Client
21
	log    *slog.Logger
22
23
	version, name string
24
25
	linter  *linter.Linter
26
	loader  *journal.Loader
27
	printer *printer.Config
28
29
	mu            sync.RWMutex
30
	config        Config
31
	openDocs      map[uri.URI]docState
32
	diagCancel    context.CancelFunc
33
	dynFileWather bool
34
}
35
36
// analysisFor returns the cached analysis for an open doc, rebuilds when the doc or a file it inclues changed.
37
func (s *server) analysisFor(u uri.URI) *analyzer.Analysis {
38
	s.mu.RLock()
39
	state, ok := s.openDocs[u]
40
	if !ok {
41
		s.mu.RUnlock()
42
		return nil
43
	}
44
	if !state.dirty {
45
		an := state.analysis
46
		s.mu.RUnlock()
47
		return an
48
	}
49
	text := state.text
50
	version := state.version
51
	s.mu.RUnlock()
52
53
	an := analyzer.Build(s.loader.ResolveBytes(u.Path(), []byte(text)))
54
55
	s.mu.Lock()
56
	state, ok = s.openDocs[u]
57
	if !ok || state.version != version {
58
		// editot or closed while building. doc stays dirty so te request rebuilds
59
		s.mu.Unlock()
60
		return an
61
	}
62
	state.analysis = an
63
	state.dirty = false
64
	state.paths = make(map[string]bool, len(an.Files))
65
	for _, pf := range an.Files {
66
		state.paths[journal.CanonicalPath(pf.Path)] = true
67
	}
68
	s.openDocs[u] = state
69
	s.mu.Unlock()
70
	return an
71
}
72
73
func (s *server) Initialize(ctx context.Context, params *protocol.InitializeParams) (*protocol.InitializeResult, error) {
74
	if w := params.Capabilities.Workspace; w != nil {
75
		if wf := w.DidChangeWatchedFiles; wf != nil {
76
			s.dynFileWather = wf.DynamicRegistration != nil && *wf.DynamicRegistration
77
		}
78
	}
79
80
	s.applySettings(params.InitializationOptions)
81
	full := protocol.SemanticTokensOptionsFull(protocol.Boolean(true))
82
	if td := params.Capabilities.TextDocument; td != nil {
83
		if fd, ok := td.SemanticTokens.Requests.Full.(*protocol.ClientSemanticTokensRequestFullDelta); ok && fd.Delta != nil && *fd.Delta {
84
			full = &protocol.SemanticTokensFullDelta{Delta: new(true)}
85
		}
86
	}
87
88
	return &protocol.InitializeResult{
89
		ServerInfo: protocol.ServerInfo{
90
			Name:    s.name,
91
			Version: protocol.NewOptional(s.version),
92
		},
93
		Capabilities: protocol.ServerCapabilities{
94
			DocumentFormattingProvider: &protocol.DocumentFormattingOptions{},
95
			DefinitionProvider:         protocol.Boolean(true),
96
			HoverProvider:              protocol.Boolean(true),
97
			ReferencesProvider:         protocol.Boolean(true),
98
			WorkspaceSymbolProvider:    protocol.Boolean(true),
99
			DocumentSymbolProvider:     protocol.Boolean(true),
100
			RenameProvider: &protocol.RenameOptions{
101
				PrepareProvider: new(true),
102
			},
103
			CompletionProvider: &protocol.CompletionOptions{
104
				TriggerCharacters: []string{":", "@"},
105
			},
106
			TextDocumentSync: &protocol.TextDocumentSyncOptions{
107
				OpenClose: new(true),
108
				Change:    new(protocol.TextDocumentSyncKindFull),
109
			},
110
			SemanticTokensProvider: &protocol.SemanticTokensOptions{
111
				Legend: getSemanticTokensLegend(),
112
				Range:  protocol.Boolean(true),
113
				Full:   full,
114
			},
115
		},
116
	}, nil
117
}
118
119
func (s *server) Initialized(ctx context.Context, params *protocol.InitializedParams) error {
120
	if s.dynFileWather {
121
		go s.registerFileWatchers(context.Background())
122
	}
123
	s.scheduleDiagnostics(ctx)
124
	return nil
125
}
126
127
func (s *server) DidChangeWatchedFiles(ctx context.Context, params *protocol.DidChangeWatchedFilesParams) error {
128
	for _, change := range params.Changes {
129
		u := change.URI
130
		path := u.Path()
131
		if path == "" {
132
			continue
133
		}
134
		if _, isOpen := s.getDocState(u); isOpen {
135
			continue // editor buffer is authoritative for open docs
136
		}
137
		s.loader.InvalidateFile(path)
138
		s.markDependentsDirty(u)
139
	}
140
	s.scheduleDiagnostics(ctx)
141
	return nil
142
}
143
144
func (s *server) DidChangeConfiguration(ctx context.Context, params *protocol.DidChangeConfigurationParams) error {
145
	s.applySettings(params.Settings)
146
	return nil
147
}
148
149
func (s *server) Shutdown(ctx context.Context) error {
150
	return nil
151
}
152
153
func (s *server) Exit(ctx context.Context) error {
154
	return nil
155
}
156
157
func (s *server) registerFileWatchers(ctx context.Context) {
158
	if s.client == nil {
159
		return
160
	}
161
	watchers := make([]protocol.FileSystemWatcher, 0, len(journal.SupportedExtensions))
162
	for _, ext := range journal.SupportedExtensions {
163
		watchers = append(watchers, protocol.FileSystemWatcher{GlobPattern: protocol.Pattern("**/*" + ext)})
164
	}
165
	options, err := protocol.Marshal(protocol.DidChangeWatchedFilesRegistrationOptions{Watchers: watchers})
166
	if err != nil {
167
		return
168
	}
169
	if err := s.client.RegisterCapability(ctx, &protocol.RegistrationParams{
170
		Registrations: []protocol.Registration{{
171
			ID:              "clerk.watchedFiles",
172
			Method:          protocol.MethodWorkspaceDidChangeWatchedFiles,
173
			RegisterOptions: protocol.LSPAny(options),
174
		}},
175
	}); err != nil {
176
		s.log.Warn("registering file watchers failed", "err", err)
177
	}
178
}
179
180
func (s *server) applySettings(v protocol.LSPAny) {
181
	s.mu.Lock()
182
	if err := s.config.merge(v); err != nil {
183
		s.log.Error("failed to merge config", "err", err)
184
	}
185
	s.mu.Unlock()
186
}