all repos

clerk @ ed6cfe6

missing tooling for ledger/hledger

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

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
bump go to 1.27, 1 month ago
1
package lsp
2
3
import (
4
	"context"
5
	"sort"
6
	"strings"
7
8
	"go.lsp.dev/protocol"
9
10
	"olexsmir.xyz/clerk/internal/analyzer"
11
	"olexsmir.xyz/clerk/internal/lsp/fuzzy"
12
	"olexsmir.xyz/clerk/journal/ast"
13
)
14
15
func (s *server) Symbols(_ context.Context, params *protocol.WorkspaceSymbolParams) (protocol.WorkspaceSymbolResult, error) {
16
	if params.Query == "" {
17
		return nil, nil
18
	}
19
20
	s.mu.RLock()
21
	paths := make([]string, 0, len(s.openDocs))
22
	for u := range s.openDocs {
23
		paths = append(paths, u.Path())
24
	}
25
	s.mu.RUnlock()
26
	if len(paths) == 0 {
27
		return nil, nil
28
	}
29
30
	an := analyzer.Build(s.loader.ResolveFiles(paths))
31
	symbols := searchSymbols(an, params.Query)
32
	if len(symbols) == 0 {
33
		return nil, nil
34
	}
35
	return protocol.WorkspaceSymbolSlice(symbols), nil
36
}
37
38
const maxSymbolResults = 100
39
40
type scoredSymbol struct {
41
	kind     symbolKind
42
	name     string
43
	score    float64
44
	tnxEntry ast.Entry // set if kind == symbolTransaction, used for location resolution
45
}
46
47
func searchSymbols(an *analyzer.Analysis, query string) []protocol.WorkspaceSymbol {
48
	matcher := fuzzy.Compile(query)
49
	scored := make([]scoredSymbol, 0)
50
	add := func(s scoredSymbol) {
51
		if score := matcher.Score(s.name); score > 0 {
52
			s.score = score
53
			scored = append(scored, s)
54
		}
55
	}
56
	for _, name := range an.AccountNames {
57
		add(scoredSymbol{kind: symbolAccount, name: name})
58
	}
59
	for name := range an.Commodities {
60
		add(scoredSymbol{kind: symbolCommodity, name: name})
61
	}
62
	for _, name := range an.PayeeNames {
63
		add(scoredSymbol{kind: symbolPayee, name: name})
64
	}
65
	for _, name := range an.TagNames {
66
		add(scoredSymbol{kind: symbolTag, name: name})
67
	}
68
	for _, tx := range an.Transactions {
69
		add(scoredSymbol{kind: symbolTransaction, name: transactionName(tx), tnxEntry: tx})
70
	}
71
	for _, tx := range an.PeriodicTransactions {
72
		add(scoredSymbol{kind: symbolTransaction, name: transactionName(tx), tnxEntry: tx})
73
	}
74
	for _, tx := range an.AutomatedTransactions {
75
		add(scoredSymbol{kind: symbolTransaction, name: transactionName(tx), tnxEntry: tx})
76
	}
77
78
	sortScoredSymbols(scored)
79
	if len(scored) > maxSymbolResults {
80
		scored = scored[:maxSymbolResults]
81
	}
82
83
	symbols := make([]protocol.WorkspaceSymbol, 0, len(scored))
84
	for _, s := range scored {
85
		loc := definitionLocation(an, s)
86
		if loc == nil {
87
			continue
88
		}
89
		symbols = append(symbols, protocol.WorkspaceSymbol{
90
			Name:     s.name,
91
			Kind:     s.kind.ToProtocol(),
92
			Location: loc,
93
		})
94
	}
95
	return symbols
96
}
97
98
func definitionLocation(an *analyzer.Analysis, s scoredSymbol) *protocol.Location {
99
	switch s.kind {
100
	case symbolAccount:
101
		return findAccountDefinition(an, s.name)
102
	case symbolTransaction:
103
		return findTransactionDefinition(an, s.tnxEntry)
104
	case symbolCommodity:
105
		return findCommodityDefinition(an, s.name)
106
	case symbolPayee:
107
		return findPayeeDefinition(an, s.name)
108
	case symbolTag:
109
		return findTagDefinition(an, s.name)
110
	}
111
	return nil
112
}
113
114
func transactionName(e ast.Entry) string {
115
	var b strings.Builder
116
	switch e := e.(type) {
117
	case *ast.Transaction:
118
		b.Grow(24)
119
		b.WriteString(e.Date.String())
120
		if s := e.Status.Value.String(); s != "" {
121
			b.WriteByte(' ')
122
			b.WriteString(s)
123
		}
124
		if e.Payee != nil {
125
			b.WriteByte(' ')
126
			b.WriteString(e.Payee.Name)
127
		}
128
		if e.Note != nil {
129
			b.WriteString(" | ")
130
			b.WriteString(e.Note.Value)
131
		}
132
	case *ast.PeriodicTransaction:
133
		b.WriteByte('~')
134
		if s := e.Status.Value.String(); s != "" {
135
			b.WriteByte(' ')
136
			b.WriteString(s)
137
		}
138
		b.WriteByte(' ')
139
		b.WriteString(e.Period.Raw)
140
		if e.Description != nil {
141
			b.WriteString(" | ")
142
			b.WriteString(e.Description.Value)
143
		}
144
	case *ast.AutomatedTransaction:
145
		b.WriteByte('=')
146
		b.WriteByte(' ')
147
		b.WriteString(e.Expr.Value)
148
	}
149
	return b.String()
150
}
151
152
func sortScoredSymbols(scored []scoredSymbol) {
153
	sort.Slice(scored, func(i, j int) bool {
154
		if scored[i].score != scored[j].score {
155
			return scored[i].score > scored[j].score
156
		}
157
		if len(scored[i].name) != len(scored[j].name) {
158
			return len(scored[i].name) < len(scored[j].name)
159
		}
160
		return scored[i].name < scored[j].name
161
	})
162
}