all repos

clerk @ d41beb6

missing tooling for ledger/hledger

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

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
lsp: refactor: drop cfgMu, 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
			BaseSymbolInformation: protocol.BaseSymbolInformation{
91
				Name: s.name,
92
				Kind: s.kind.ToProtocol(),
93
			},
94
			Location: loc,
95
		})
96
	}
97
	return symbols
98
}
99
100
func definitionLocation(an *analyzer.Analysis, s scoredSymbol) *protocol.Location {
101
	switch s.kind {
102
	case symbolAccount:
103
		return findAccountDefinition(an, s.name)
104
	case symbolTransaction:
105
		return findTransactionDefinition(an, s.tnxEntry)
106
	case symbolCommodity:
107
		return findCommodityDefinition(an, s.name)
108
	case symbolPayee:
109
		return findPayeeDefinition(an, s.name)
110
	case symbolTag:
111
		return findTagDefinition(an, s.name)
112
	}
113
	return nil
114
}
115
116
func transactionName(e ast.Entry) string {
117
	var b strings.Builder
118
	switch e := e.(type) {
119
	case *ast.Transaction:
120
		b.Grow(24)
121
		b.WriteString(e.Date.String())
122
		if s := e.Status.Value.String(); s != "" {
123
			b.WriteByte(' ')
124
			b.WriteString(s)
125
		}
126
		if e.Payee != nil {
127
			b.WriteByte(' ')
128
			b.WriteString(e.Payee.Name)
129
		}
130
		if e.Note != nil {
131
			b.WriteString(" | ")
132
			b.WriteString(e.Note.Value)
133
		}
134
	case *ast.PeriodicTransaction:
135
		b.WriteByte('~')
136
		if s := e.Status.Value.String(); s != "" {
137
			b.WriteByte(' ')
138
			b.WriteString(s)
139
		}
140
		b.WriteByte(' ')
141
		b.WriteString(e.Period.Raw)
142
		if e.Description != nil {
143
			b.WriteString(" | ")
144
			b.WriteString(e.Description.Value)
145
		}
146
	case *ast.AutomatedTransaction:
147
		b.WriteByte('=')
148
		b.WriteByte(' ')
149
		b.WriteString(e.Expr.Value)
150
	}
151
	return b.String()
152
}
153
154
func sortScoredSymbols(scored []scoredSymbol) {
155
	sort.Slice(scored, func(i, j int) bool {
156
		if scored[i].score != scored[j].score {
157
			return scored[i].score > scored[j].score
158
		}
159
		if len(scored[i].name) != len(scored[j].name) {
160
			return len(scored[i].name) < len(scored[j].name)
161
		}
162
		return scored[i].name < scored[j].name
163
	})
164
}