all repos

clerk @ 4b5c1da

missing tooling for ledger/hledger

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

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
add support of subdirectives and it's highlights, 1 month ago
1
package lsp
2
3
import (
4
	"context"
5
	"slices"
6
7
	"go.lsp.dev/protocol"
8
	"go.lsp.dev/uri"
9
10
	"olexsmir.xyz/clerk/internal/analyzer"
11
	"olexsmir.xyz/clerk/internal/lsp/lsputil"
12
	"olexsmir.xyz/clerk/journal/ast"
13
	"olexsmir.xyz/clerk/journal/token"
14
)
15
16
func (s *server) Definition(_ context.Context, params *protocol.DefinitionParams) (protocol.DefinitionResult, error) {
17
	state, ok := s.getDocState(params.TextDocument.URI)
18
	if !ok {
19
		return nil, nil
20
	}
21
22
	an := s.analysis()
23
	cursor := lsputil.Offset(state.text, int(params.Position.Line), int(params.Position.Character))
24
	return findDefinitionUnderCursor(an, params.TextDocument.URI.Path(), state.text, cursor), nil
25
}
26
27
func findDefinitionUnderCursor(an *analyzer.Analysis, docPath, content string, cursor int) protocol.LocationSlice {
28
	ref := findSymbolUnderCursor(an, docPath, content, cursor)
29
	if ref == nil {
30
		return nil
31
	}
32
	loc := resolveSymbol(ref, an)
33
	if loc == nil {
34
		return nil
35
	}
36
	return protocol.LocationSlice{*loc}
37
}
38
39
func resolveSymbol(ref *symbolRef, an *analyzer.Analysis) *protocol.Location {
40
	switch ref.kind {
41
	case symbolAccount:
42
		return findAccountDefinition(an, ref.name)
43
	case symbolCommodity:
44
		return findCommodityDefinition(an, ref.name)
45
	case symbolPayee:
46
		return findPayeeDefinition(an, ref.name)
47
	}
48
	return nil
49
}
50
51
func findAccountDefinition(an *analyzer.Analysis, name string) *protocol.Location {
52
	if canon, ok := an.AccountAliases[name]; ok {
53
		name = canon
54
	}
55
	info := an.Accounts[name]
56
	if info == nil {
57
		return nil
58
	}
59
	if len(info.Directives) > 0 {
60
		return locationForDirective(an, info.Directives[0], info.Directives[0].Account.Span)
61
	}
62
	if len(info.Usages) > 0 {
63
		u := info.Usages[0]
64
		return locationFor(an, u.FileIndex, u.Posting.Account.Span)
65
	}
66
	return nil
67
}
68
69
func findCommodityDefinition(an *analyzer.Analysis, symbol string) *protocol.Location {
70
	info := an.Commodities[symbol]
71
	if info == nil {
72
		return nil
73
	}
74
	if len(info.Directives) > 0 {
75
		return locationForDirective(an, info.Directives[0], info.Directives[0].CommoditySpan)
76
	}
77
	if len(info.Usages) > 0 {
78
		u := info.Usages[0]
79
		return locationFor(an, u.FileIndex, u.Amount.CommoditySpan)
80
	}
81
	return nil
82
}
83
84
func findPayeeDefinition(an *analyzer.Analysis, name string) *protocol.Location {
85
	info := an.Payees[name]
86
	if info == nil {
87
		return nil
88
	}
89
	if len(info.Directives) > 0 {
90
		d := info.Directives[0]
91
		if d.Name == nil {
92
			return nil
93
		}
94
		return locationForDirective(an, d, d.Name.Span)
95
	}
96
	if len(info.Usage) > 0 {
97
		u := info.Usage[0]
98
		return locationFor(an, u.FileIndex, u.Payee.Span)
99
	}
100
	return nil
101
}
102
103
func locationForDirective(a *analyzer.Analysis, d ast.Entry, span token.Span) *protocol.Location {
104
	for i, pf := range a.Files {
105
		if slices.Contains(pf.Ast.Entries, d) {
106
			return locationFor(a, i, span)
107
		}
108
	}
109
	return nil
110
}
111
112
func locationFor(a *analyzer.Analysis, fileIdx int, span token.Span) *protocol.Location {
113
	pf := a.Files[fileIdx]
114
	return &protocol.Location{
115
		URI:   uri.File(pf.Path),
116
		Range: spanToProtocolRange(string(pf.Src), span),
117
	}
118
}
119
120
func spanToProtocolRange(content string, span token.Span) protocol.Range {
121
	return protocol.Range{
122
		Start: lsputil.Position(content, span.Start.Offset),
123
		End:   lsputil.Position(content, spanEndClamped(content, span.End.Offset)),
124
	}
125
}
126
127
func spanContains(content string, span token.Span, offset int) bool {
128
	if span.End.Offset <= span.Start.Offset {
129
		return false
130
	}
131
	end := spanEndClamped(content, span.End.Offset)
132
	return span.Start.Offset <= offset && offset <= end
133
}
134
135
func spanEndClamped(content string, end int) int {
136
	for end > 0 {
137
		switch content[end-1] {
138
		case ' ', '\t', '\r', '\n':
139
			end--
140
		default:
141
			return end
142
		}
143
	}
144
	return end
145
}