all repos

clerk @ 2f348cb

missing tooling for ledger/hledger

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

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