all repos

clerk @ 2963f4c54119979c248426c02e700c05715604fb

missing tooling for ledger/hledger

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

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