all repos

clerk @ 3d2b11f

missing tooling for ledger/hledger

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

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
lsp: cache per-document analyses with include-aware invalidation..., 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
	current    *analyzer.Analysis
32
	diagCancel context.CancelFunc
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
	s.applySettings(params.InitializationOptions)
77
	return &protocol.InitializeResult{
78
		ServerInfo: protocol.ServerInfo{
79
			Name:    s.name,
80
			Version: protocol.NewOptional(s.version),
81
		},
82
		Capabilities: protocol.ServerCapabilities{
83
			DocumentFormattingProvider: &protocol.DocumentFormattingOptions{},
84
			DefinitionProvider:         protocol.Boolean(true),
85
			HoverProvider:              protocol.Boolean(true),
86
			RenameProvider: &protocol.RenameOptions{
87
				PrepareProvider: new(true),
88
			},
89
			CompletionProvider: &protocol.CompletionOptions{
90
				TriggerCharacters: []string{":", "@"},
91
			},
92
			TextDocumentSync: &protocol.TextDocumentSyncOptions{
93
				OpenClose: new(true),
94
				Change:    new(protocol.TextDocumentSyncKindFull),
95
			},
96
			SemanticTokensProvider: &protocol.SemanticTokensOptions{
97
				Legend: getSemanticTokensLegend(),
98
				Range:  protocol.Boolean(true),
99
				Full:   protocol.Boolean(true),
100
			},
101
		},
102
	}, nil
103
}
104
105
func (s *server) Initialized(ctx context.Context, params *protocol.InitializedParams) error {
106
	s.scheduleDiagnostics(ctx)
107
	return nil
108
}
109
110
func (s *server) DidChangeConfiguration(ctx context.Context, params *protocol.DidChangeConfigurationParams) error {
111
	s.applySettings(params.Settings)
112
	return nil
113
}
114
115
func (s *server) Shutdown(ctx context.Context) error {
116
	return nil
117
}
118
119
func (s *server) Exit(ctx context.Context) error {
120
	return nil
121
}
122
123
func (s *server) applySettings(v protocol.LSPAny) {
124
	s.cfgMu.Lock()
125
	if err := s.config.merge(v); err != nil {
126
		s.log.Error("failed to merge config", "err", err)
127
	}
128
	s.cfgMu.Unlock()
129
}