all repos

clerk @ c027d7c5c0081deeabfaa5147338fbc821391093

missing tooling for ledger/hledger

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

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
lsp: go to definition, 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
	for _, pf := range an.Files {
29
		if pf.Path != docPath {
30
			continue
31
		}
32
		for _, entry := range pf.Ast.Entries {
33
			if loc := definitionInEntry(an, content, entry, cursor); loc != nil {
34
				return protocol.LocationSlice{*loc}
35
			}
36
		}
37
		return nil
38
	}
39
	return nil
40
}
41
42
func definitionInEntry(an *analyzer.Analysis, content string, e ast.Entry, cursor int) *protocol.Location {
43
	switch e := e.(type) {
44
	case *ast.Transaction:
45
		if e.Payee != nil && spanContains(content, e.Payee.Span, cursor) {
46
			return findPayeeDefinition(an, e.Payee.Name)
47
		}
48
		return definitionInPostings(an, content, e.Postings, cursor)
49
	case *ast.PeriodicTransaction:
50
		return definitionInPostings(an, content, e.Postings, cursor)
51
	case *ast.AutomatedTransaction:
52
		return definitionInPostings(an, content, e.Postings, cursor)
53
	case *ast.AccountDirective:
54
		if spanContains(content, e.Account.Span, cursor) {
55
			return findAccountDefinition(an, e.Account.String())
56
		}
57
	case *ast.CommodityDirective:
58
		if spanContains(content, e.CommoditySpan, cursor) {
59
			return findCommodityDefinition(an, e.Commodity)
60
		}
61
	case *ast.PayeeDirective:
62
		if e.Name != nil && spanContains(content, e.Name.Span, cursor) {
63
			return findPayeeDefinition(an, e.Name.Name)
64
		}
65
	}
66
	return nil
67
}
68
69
func definitionInPostings(an *analyzer.Analysis, content string, postings []*ast.Posting, cursor int) *protocol.Location {
70
	for _, p := range postings {
71
		if spanContains(content, p.Account.Span, cursor) {
72
			return findAccountDefinition(an, p.Account.String())
73
		}
74
		if loc := commodityDefinition(an, content, p.Amount, cursor); loc != nil {
75
			return loc
76
		}
77
		if p.Cost != nil {
78
			if loc := commodityDefinition(an, content, &p.Cost.Amount, cursor); loc != nil {
79
				return loc
80
			}
81
		}
82
		if p.Balance != nil {
83
			if loc := commodityDefinition(an, content, &p.Balance.Amount, cursor); loc != nil {
84
				return loc
85
			}
86
		}
87
	}
88
	return nil
89
}
90
91
func commodityDefinition(an *analyzer.Analysis, content string, am *ast.Amount, cursor int) *protocol.Location {
92
	if am == nil || am.Commodity == "" || !spanContains(content, am.CommoditySpan, cursor) {
93
		return nil
94
	}
95
	return findCommodityDefinition(an, am.Commodity)
96
}
97
98
func findAccountDefinition(an *analyzer.Analysis, name string) *protocol.Location {
99
	info := an.Accounts[name]
100
	if info == nil {
101
		return nil
102
	}
103
	if len(info.Directives) > 0 {
104
		return locationForDirective(an, info.Directives[0], info.Directives[0].Account.Span)
105
	}
106
	if len(info.Usages) > 0 {
107
		u := info.Usages[0]
108
		return locationFor(an, u.FileIndex, u.Posting.Account.Span)
109
	}
110
	return nil
111
}
112
113
func findCommodityDefinition(an *analyzer.Analysis, symbol string) *protocol.Location {
114
	info := an.Commodities[symbol]
115
	if info == nil {
116
		return nil
117
	}
118
	if len(info.Directives) > 0 {
119
		return locationForDirective(an, info.Directives[0], info.Directives[0].CommoditySpan)
120
	}
121
	if len(info.Usages) > 0 {
122
		u := info.Usages[0]
123
		return locationFor(an, u.FileIndex, u.Amount.CommoditySpan)
124
	}
125
	return nil
126
}
127
128
func findPayeeDefinition(an *analyzer.Analysis, name string) *protocol.Location {
129
	info := an.Payees[name]
130
	if info == nil {
131
		return nil
132
	}
133
	if len(info.Directives) > 0 {
134
		d := info.Directives[0]
135
		if d.Name == nil {
136
			return nil
137
		}
138
		return locationForDirective(an, d, d.Name.Span)
139
	}
140
	if len(info.Usage) > 0 {
141
		u := info.Usage[0]
142
		return locationFor(an, u.FileIndex, u.Payee.Span)
143
	}
144
	return nil
145
}
146
147
func locationForDirective(a *analyzer.Analysis, d ast.Entry, span token.Span) *protocol.Location {
148
	for i, pf := range a.Files {
149
		if slices.Contains(pf.Ast.Entries, d) {
150
			return locationFor(a, i, span)
151
		}
152
	}
153
	return nil
154
}
155
156
func locationFor(a *analyzer.Analysis, fileIdx int, span token.Span) *protocol.Location {
157
	pf := a.Files[fileIdx]
158
	return &protocol.Location{
159
		URI:   uri.File(pf.Path),
160
		Range: spanToProtocolRange(string(pf.Src), span),
161
	}
162
}
163
164
func spanToProtocolRange(content string, span token.Span) protocol.Range {
165
	return protocol.Range{
166
		Start: lsputil.Position(content, span.Start.Offset),
167
		End:   lsputil.Position(content, spanEndClamped(content, span.End.Offset)),
168
	}
169
}
170
171
func spanContains(content string, span token.Span, offset int) bool {
172
	if span.End.Offset <= span.Start.Offset {
173
		return false
174
	}
175
	end := spanEndClamped(content, span.End.Offset)
176
	return span.Start.Offset <= offset && offset <= end
177
}
178
179
func spanEndClamped(content string, end int) int {
180
	for end > 0 {
181
		switch content[end-1] {
182
		case ' ', '\t', '\r', '\n':
183
			end--
184
		default:
185
			return end
186
		}
187
	}
188
	return end
189
}