all repos

clerk @ d12c079

missing tooling for ledger/hledger

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

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
go fix and remove some useless comments, 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
	pf := parsedFileFor(an, params.TextDocument.URI.Path())
20
	if pf == nil {
21
		return nil, nil
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
30
func foldingRangesFor(entries []ast.Entry) []protocol.FoldingRange {
31
	ranges := make([]protocol.FoldingRange, 0, len(entries)) // each entry folds to at most one range
32
	var (
33
		comments []*ast.Comment // consecutive comment lines, flushed at the next non-comment entry
34
		applies  []*ast.ApplyDirective
35
	)
36
37
	flush := func() {
38
		ranges = appendFold(ranges, commentRunFold(comments))
39
		comments = nil
40
	}
41
42
	for _, entry := range entries {
43
		if c, ok := entry.(*ast.Comment); ok {
44
			comments = append(comments, c)
45
			continue
46
		}
47
		flush()
48
49
		switch e := entry.(type) {
50
		case *ast.Transaction:
51
			ranges = appendFold(ranges, postingsFold(e.Postings))
52
		case *ast.PeriodicTransaction:
53
			ranges = appendFold(ranges, postingsFold(e.Postings))
54
		case *ast.AutomatedTransaction:
55
			ranges = appendFold(ranges, postingsFold(e.Postings))
56
		case *ast.AccountDirective:
57
			ranges = appendFold(ranges, accountDirectiveFold(e))
58
		case *ast.CommentBlockDirective:
59
			ranges = appendFold(ranges, commentBlockDirectiveFold(e))
60
		case *ast.ApplyDirective:
61
			applies = append(applies, e)
62
		case *ast.EndDirective:
63
			if n := len(applies); n > 0 && applyKeyword(applies[n-1].Expr) == applyKeyword(e.Expr) {
64
				a := applies[n-1]
65
				applies = applies[:n-1]
66
				ranges = appendFold(ranges, applyFold(a, e))
67
			}
68
		}
69
	}
70
	flush()
71
	return ranges
72
}
73
74
func appendFold(ranges []protocol.FoldingRange, r *protocol.FoldingRange) []protocol.FoldingRange {
75
	if r != nil {
76
		ranges = append(ranges, *r)
77
	}
78
	return ranges
79
}
80
81
func postingsFold(postings []ast.Posting) *protocol.FoldingRange {
82
	if len(postings) < 2 {
83
		return nil
84
	}
85
	first, last := postings[0], postings[len(postings)-1]
86
	return foldRange(
87
		uint32(first.Span.Start.Line-1),
88
		uint32(last.Span.Start.Line-1),
89
		protocol.FoldingRangeKindRegion,
90
	)
91
}
92
93
func accountDirectiveFold(ad *ast.AccountDirective) *protocol.FoldingRange {
94
	if len(ad.Subdirectives) < 2 {
95
		return nil
96
	}
97
	first, last := ad.Subdirectives[0], ad.Subdirectives[len(ad.Subdirectives)-1]
98
	return foldRange(
99
		uint32(first.NameSpan.Start.Line-1),
100
		uint32(last.NameSpan.Start.Line-1),
101
		protocol.FoldingRangeKindRegion,
102
	)
103
}
104
105
func commentBlockDirectiveFold(cb *ast.CommentBlockDirective) *protocol.FoldingRange {
106
	start := uint32(cb.Span.Start.Line - 1)
107
	end := start + uint32(strings.Count(cb.Content, "\n")) + 1 // +1 for the "end comment" line
108
	return foldRange(start, end, protocol.FoldingRangeKindComment)
109
}
110
111
func commentRunFold(comments []*ast.Comment) *protocol.FoldingRange {
112
	if len(comments) < 2 {
113
		return nil
114
	}
115
	return foldRange(
116
		uint32(comments[0].Span.Start.Line-1),
117
		uint32(comments[len(comments)-1].Span.Start.Line-1),
118
		protocol.FoldingRangeKindComment,
119
	)
120
}
121
122
func applyKeyword(expr string) string {
123
	if before, _, ok := strings.Cut(expr, " "); ok {
124
		return before
125
	}
126
	return expr
127
}
128
129
func applyFold(a *ast.ApplyDirective, end *ast.EndDirective) *protocol.FoldingRange {
130
	return foldRange(
131
		uint32(a.Span.Start.Line-1),
132
		uint32(end.Span.Start.Line-1),
133
		protocol.FoldingRangeKindRegion,
134
	)
135
}
136
137
func foldRange(startLine, endLine uint32, kind protocol.FoldingRangeKind) *protocol.FoldingRange {
138
	if endLine <= startLine {
139
		return nil
140
	}
141
	return &protocol.FoldingRange{StartLine: startLine, EndLine: endLine, Kind: kind}
142
}