all repos

clerk @ 2a7dd08673ac58db36d74563bdcc5b017c1b5682

missing tooling for ledger/hledger

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

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
lsp: rename, 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
	info := an.Accounts[name]
53
	if info == nil {
54
		return nil
55
	}
56
	if len(info.Directives) > 0 {
57
		return locationForDirective(an, info.Directives[0], info.Directives[0].Account.Span)
58
	}
59
	if len(info.Usages) > 0 {
60
		u := info.Usages[0]
61
		return locationFor(an, u.FileIndex, u.Posting.Account.Span)
62
	}
63
	return nil
64
}
65
66
func findCommodityDefinition(an *analyzer.Analysis, symbol string) *protocol.Location {
67
	info := an.Commodities[symbol]
68
	if info == nil {
69
		return nil
70
	}
71
	if len(info.Directives) > 0 {
72
		return locationForDirective(an, info.Directives[0], info.Directives[0].CommoditySpan)
73
	}
74
	if len(info.Usages) > 0 {
75
		u := info.Usages[0]
76
		return locationFor(an, u.FileIndex, u.Amount.CommoditySpan)
77
	}
78
	return nil
79
}
80
81
func findPayeeDefinition(an *analyzer.Analysis, name string) *protocol.Location {
82
	info := an.Payees[name]
83
	if info == nil {
84
		return nil
85
	}
86
	if len(info.Directives) > 0 {
87
		d := info.Directives[0]
88
		if d.Name == nil {
89
			return nil
90
		}
91
		return locationForDirective(an, d, d.Name.Span)
92
	}
93
	if len(info.Usage) > 0 {
94
		u := info.Usage[0]
95
		return locationFor(an, u.FileIndex, u.Payee.Span)
96
	}
97
	return nil
98
}
99
100
func locationForDirective(a *analyzer.Analysis, d ast.Entry, span token.Span) *protocol.Location {
101
	for i, pf := range a.Files {
102
		if slices.Contains(pf.Ast.Entries, d) {
103
			return locationFor(a, i, span)
104
		}
105
	}
106
	return nil
107
}
108
109
func locationFor(a *analyzer.Analysis, fileIdx int, span token.Span) *protocol.Location {
110
	pf := a.Files[fileIdx]
111
	return &protocol.Location{
112
		URI:   uri.File(pf.Path),
113
		Range: spanToProtocolRange(string(pf.Src), span),
114
	}
115
}
116
117
func spanToProtocolRange(content string, span token.Span) protocol.Range {
118
	return protocol.Range{
119
		Start: lsputil.Position(content, span.Start.Offset),
120
		End:   lsputil.Position(content, spanEndClamped(content, span.End.Offset)),
121
	}
122
}
123
124
func spanContains(content string, span token.Span, offset int) bool {
125
	if span.End.Offset <= span.Start.Offset {
126
		return false
127
	}
128
	end := spanEndClamped(content, span.End.Offset)
129
	return span.Start.Offset <= offset && offset <= end
130
}
131
132
func spanEndClamped(content string, end int) int {
133
	for end > 0 {
134
		switch content[end-1] {
135
		case ' ', '\t', '\r', '\n':
136
			end--
137
		default:
138
			return end
139
		}
140
	}
141
	return end
142
}