all repos

clerk @ 2e3f494

missing tooling for ledger/hledger

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

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
ast: store postings as values instead of pointers, 1 month ago
1
package lsp
2
3
import (
4
	"cmp"
5
	"context"
6
	"slices"
7
	"strings"
8
9
	"go.lsp.dev/protocol"
10
11
	"olexsmir.xyz/clerk/journal/ast"
12
)
13
14
func (s *server) FoldingRange(_ context.Context, params *protocol.FoldingRangeParams) ([]protocol.FoldingRange, error) {
15
	an := s.analysisFor(params.TextDocument.URI)
16
	if an == nil {
17
		return nil, nil
18
	}
19
	for _, pf := range an.Files {
20
		if pf.Path != params.TextDocument.URI.Path() {
21
			continue
22
		}
23
		ranges := foldingRangesFor(pf.Ast.Entries)
24
		slices.SortFunc(ranges, func(a, b protocol.FoldingRange) int {
25
			return cmp.Compare(a.StartLine, b.StartLine)
26
		})
27
		return ranges, nil
28
	}
29
	return nil, nil
30
}
31
32
// foldingRangesFor folds every foldable structure in a journal's top-level entries:
33
// posting blocks, directive sub-blocks, comment runs, comment blocks, and apply/end pairs.
34
func foldingRangesFor(entries []ast.Entry) []protocol.FoldingRange {
35
	ranges := make([]protocol.FoldingRange, 0, len(entries)) // each entry folds to at most one range
36
	var (
37
		comments []*ast.Comment // consecutive comment lines, flushed at the next non-comment entry
38
		applies  []*ast.ApplyDirective
39
	)
40
41
	flush := func() {
42
		ranges = appendFold(ranges, commentRunFold(comments))
43
		comments = nil
44
	}
45
46
	for _, entry := range entries {
47
		if c, ok := entry.(*ast.Comment); ok {
48
			comments = append(comments, c)
49
			continue
50
		}
51
		flush()
52
53
		switch e := entry.(type) {
54
		case *ast.Transaction:
55
			ranges = appendFold(ranges, postingsFold(e.Postings))
56
		case *ast.PeriodicTransaction:
57
			ranges = appendFold(ranges, postingsFold(e.Postings))
58
		case *ast.AutomatedTransaction:
59
			ranges = appendFold(ranges, postingsFold(e.Postings))
60
		case *ast.AccountDirective:
61
			ranges = appendFold(ranges, accountDirectiveFold(e))
62
		case *ast.CommentBlockDirective:
63
			ranges = appendFold(ranges, commentBlockDirectiveFold(e))
64
		case *ast.ApplyDirective:
65
			applies = append(applies, e)
66
		case *ast.EndDirective:
67
			if n := len(applies); n > 0 && applyKeyword(applies[n-1].Expr) == applyKeyword(e.Expr) {
68
				a := applies[n-1]
69
				applies = applies[:n-1]
70
				ranges = appendFold(ranges, applyFold(a, e))
71
			}
72
		}
73
	}
74
	flush()
75
	return ranges
76
}
77
78
// appendFold appends r, skipping nil.
79
func appendFold(ranges []protocol.FoldingRange, r *protocol.FoldingRange) []protocol.FoldingRange {
80
	if r != nil {
81
		ranges = append(ranges, *r)
82
	}
83
	return ranges
84
}
85
86
// postingsFold folds the posting block of a transaction, leaving the header visible.
87
func postingsFold(postings []ast.Posting) *protocol.FoldingRange {
88
	if len(postings) < 2 {
89
		return nil
90
	}
91
	first, last := postings[0], postings[len(postings)-1]
92
	return foldRange(
93
		uint32(first.Span.Start.Line-1),
94
		uint32(last.Span.Start.Line-1),
95
		protocol.FoldingRangeKindRegion,
96
	)
97
}
98
99
// accountDirectiveFold folds the indented subdirective block after an "account" line.
100
func accountDirectiveFold(ad *ast.AccountDirective) *protocol.FoldingRange {
101
	if len(ad.Subdirectives) < 2 {
102
		return nil
103
	}
104
	first, last := ad.Subdirectives[0], ad.Subdirectives[len(ad.Subdirectives)-1]
105
	return foldRange(
106
		uint32(first.NameSpan.Start.Line-1),
107
		uint32(last.NameSpan.Start.Line-1),
108
		protocol.FoldingRangeKindRegion,
109
	)
110
}
111
112
// commentBlockDirectiveFold folds a "comment" ... "end comment" block, leaving the header visible.
113
// The directive span extends past the block, so the end line is derived from the content.
114
func commentBlockDirectiveFold(cb *ast.CommentBlockDirective) *protocol.FoldingRange {
115
	start := uint32(cb.Span.Start.Line - 1)
116
	end := start + uint32(strings.Count(cb.Content, "\n")) + 1 // +1 for the "end comment" line
117
	return foldRange(start, end, protocol.FoldingRangeKindComment)
118
}
119
120
// commentRunFold folds consecutive comment lines into a single range.
121
func commentRunFold(comments []*ast.Comment) *protocol.FoldingRange {
122
	if len(comments) < 2 {
123
		return nil
124
	}
125
	return foldRange(
126
		uint32(comments[0].Span.Start.Line-1),
127
		uint32(comments[len(comments)-1].Span.Start.Line-1),
128
		protocol.FoldingRangeKindComment,
129
	)
130
}
131
132
// applyKeyword returns the first word of an apply/end directive expr ("apply tag work" → "tag").
133
func applyKeyword(expr string) string {
134
	if i := strings.IndexByte(expr, ' '); i >= 0 {
135
		return expr[:i]
136
	}
137
	return expr
138
}
139
140
// applyFold folds an "apply" ... "end" block.
141
func applyFold(a *ast.ApplyDirective, end *ast.EndDirective) *protocol.FoldingRange {
142
	return foldRange(
143
		uint32(a.Span.Start.Line-1),
144
		uint32(end.Span.Start.Line-1),
145
		protocol.FoldingRangeKindRegion,
146
	)
147
}
148
149
// foldRange returns nil when the range is not foldable (single line).
150
func foldRange(startLine, endLine uint32, kind protocol.FoldingRangeKind) *protocol.FoldingRange {
151
	if endLine <= startLine {
152
		return nil
153
	}
154
	return &protocol.FoldingRange{StartLine: startLine, EndLine: endLine, Kind: kind}
155
}