all repos

clerk @ 9d057cb160c6849c3dc3c5d0740cc4add015b3f0

missing tooling for ledger/hledger

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

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
lsp: implement textdocument/documentSymbol, 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
33
	cfgMu  sync.RWMutex
34
	config Config
35
}
36
37
// analysisFor returns the cached analysis for an open doc, rebuilds when the doc or a file it inclues changed.
38
func (s *server) analysisFor(u uri.URI) *analyzer.Analysis {
39
	s.mu.Lock()
40
	state, ok := s.openDocs[u]
41
	if !ok {
42
		s.mu.Unlock()
43
		return nil
44
	}
45
	if !state.dirty {
46
		an := state.analysis
47
		s.mu.Unlock()
48
		return an
49
	}
50
	text := state.text
51
	version := state.version
52
	s.mu.Unlock()
53
54
	an := analyzer.Build(s.loader.ResolveBytes(u.Path(), []byte(text)))
55
56
	s.mu.Lock()
57
	state, ok = s.openDocs[u]
58
	if !ok || state.version != version {
59
		// editot or closed while building. doc stays dirty so te request rebuilds
60
		s.mu.Unlock()
61
		return an
62
	}
63
	state.analysis = an
64
	state.dirty = false
65
	state.paths = make(map[string]bool, len(an.Files))
66
	for _, pf := range an.Files {
67
		state.paths[journal.CanonicalPath(pf.Path)] = true
68
	}
69
	s.openDocs[u] = state
70
	s.mu.Unlock()
71
	return an
72
}
73
74
func (s *server) Initialize(ctx context.Context, params *protocol.InitializeParams) (*protocol.InitializeResult, error) {
75
	s.applySettings(params.InitializationOptions)
76
	return &protocol.InitializeResult{
77
		ServerInfo: protocol.ServerInfo{
78
			Name:    s.name,
79
			Version: protocol.NewOptional(s.version),
80
		},
81
		Capabilities: protocol.ServerCapabilities{
82
			DocumentFormattingProvider: &protocol.DocumentFormattingOptions{},
83
			DefinitionProvider:         protocol.Boolean(true),
84
			HoverProvider:              protocol.Boolean(true),
85
			ReferencesProvider:         protocol.Boolean(true),
86
			WorkspaceSymbolProvider:    protocol.Boolean(true),
87
			DocumentSymbolProvider:     protocol.Boolean(true),
88
			RenameProvider: &protocol.RenameOptions{
89
				PrepareProvider: new(true),
90
			},
91
			CompletionProvider: &protocol.CompletionOptions{
92
				TriggerCharacters: []string{":", "@"},
93
			},
94
			TextDocumentSync: &protocol.TextDocumentSyncOptions{
95
				OpenClose: new(true),
96
				Change:    new(protocol.TextDocumentSyncKindFull),
97
			},
98
			SemanticTokensProvider: &protocol.SemanticTokensOptions{
99
				Legend: getSemanticTokensLegend(),
100
				Range:  protocol.Boolean(true),
101
				Full:   protocol.Boolean(true),
102
			},
103
		},
104
	}, nil
105
}
106
107
func (s *server) Initialized(ctx context.Context, params *protocol.InitializedParams) error {
108
	s.scheduleDiagnostics(ctx)
109
	return nil
110
}
111
112
func (s *server) DidChangeConfiguration(ctx context.Context, params *protocol.DidChangeConfigurationParams) error {
113
	s.applySettings(params.Settings)
114
	return nil
115
}
116
117
func (s *server) Shutdown(ctx context.Context) error {
118
	return nil
119
}
120
121
func (s *server) Exit(ctx context.Context) error {
122
	return nil
123
}
124
125
func (s *server) applySettings(v protocol.LSPAny) {
126
	s.cfgMu.Lock()
127
	if err := s.config.merge(v); err != nil {
128
		s.log.Error("failed to merge config", "err", err)
129
	}
130
	s.cfgMu.Unlock()
131
}