all repos

clerk @ 7be0936

missing tooling for ledger/hledger

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

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
lsp: refactor: drop cfgMu, 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
	return &protocol.InitializeResult{
82
		ServerInfo: protocol.ServerInfo{
83
			Name:    s.name,
84
			Version: protocol.NewOptional(s.version),
85
		},
86
		Capabilities: protocol.ServerCapabilities{
87
			DocumentFormattingProvider: &protocol.DocumentFormattingOptions{},
88
			DefinitionProvider:         protocol.Boolean(true),
89
			HoverProvider:              protocol.Boolean(true),
90
			ReferencesProvider:         protocol.Boolean(true),
91
			WorkspaceSymbolProvider:    protocol.Boolean(true),
92
			DocumentSymbolProvider:     protocol.Boolean(true),
93
			RenameProvider: &protocol.RenameOptions{
94
				PrepareProvider: new(true),
95
			},
96
			CompletionProvider: &protocol.CompletionOptions{
97
				TriggerCharacters: []string{":", "@"},
98
			},
99
			TextDocumentSync: &protocol.TextDocumentSyncOptions{
100
				OpenClose: new(true),
101
				Change:    new(protocol.TextDocumentSyncKindFull),
102
			},
103
			SemanticTokensProvider: &protocol.SemanticTokensOptions{
104
				Legend: getSemanticTokensLegend(),
105
				Range:  protocol.Boolean(true),
106
				Full:   protocol.Boolean(true),
107
			},
108
		},
109
	}, nil
110
}
111
112
func (s *server) Initialized(ctx context.Context, params *protocol.InitializedParams) error {
113
	if s.dynFileWather {
114
		go s.registerFileWatchers(context.Background())
115
	}
116
	s.scheduleDiagnostics(ctx)
117
	return nil
118
}
119
120
func (s *server) DidChangeWatchedFiles(ctx context.Context, params *protocol.DidChangeWatchedFilesParams) error {
121
	for _, change := range params.Changes {
122
		u := change.URI
123
		path := u.Path()
124
		if path == "" {
125
			continue
126
		}
127
		if _, isOpen := s.getDocState(u); isOpen {
128
			continue // editor buffer is authoritative for open docs
129
		}
130
		s.loader.InvalidateFile(path)
131
		s.markDependentsDirty(u)
132
	}
133
	s.scheduleDiagnostics(ctx)
134
	return nil
135
}
136
137
func (s *server) DidChangeConfiguration(ctx context.Context, params *protocol.DidChangeConfigurationParams) error {
138
	s.applySettings(params.Settings)
139
	return nil
140
}
141
142
func (s *server) Shutdown(ctx context.Context) error {
143
	return nil
144
}
145
146
func (s *server) Exit(ctx context.Context) error {
147
	return nil
148
}
149
150
func (s *server) registerFileWatchers(ctx context.Context) {
151
	if s.client == nil {
152
		return
153
	}
154
	watchers := make([]protocol.FileSystemWatcher, 0, len(journal.SupportedExtensions))
155
	for _, ext := range journal.SupportedExtensions {
156
		watchers = append(watchers, protocol.FileSystemWatcher{GlobPattern: protocol.Pattern("**/*" + ext)})
157
	}
158
	options, err := protocol.Marshal(protocol.DidChangeWatchedFilesRegistrationOptions{Watchers: watchers})
159
	if err != nil {
160
		return
161
	}
162
	if err := s.client.RegisterCapability(ctx, &protocol.RegistrationParams{
163
		Registrations: []protocol.Registration{{
164
			ID:              "clerk.watchedFiles",
165
			Method:          protocol.MethodWorkspaceDidChangeWatchedFiles,
166
			RegisterOptions: protocol.LSPAny(options),
167
		}},
168
	}); err != nil {
169
		s.log.Warn("registering file watchers failed", "err", err)
170
	}
171
}
172
173
func (s *server) applySettings(v protocol.LSPAny) {
174
	s.mu.Lock()
175
	if err := s.config.merge(v); err != nil {
176
		s.log.Error("failed to merge config", "err", err)
177
	}
178
	s.mu.Unlock()
179
}