all repos

clerk @ 17e6766

missing tooling for ledger/hledger

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

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
lsp: textdocument/selectionRange..., 1 month ago
1
package lsp
2
3
import (
4
	"context"
5
6
	"go.lsp.dev/protocol"
7
8
	"olexsmir.xyz/clerk/internal/lsp/lsputil"
9
	"olexsmir.xyz/clerk/journal"
10
	"olexsmir.xyz/clerk/journal/ast"
11
	"olexsmir.xyz/clerk/journal/token"
12
)
13
14
func (s *server) SelectionRange(_ context.Context, params *protocol.SelectionRangeParams) ([]protocol.SelectionRange, error) {
15
	u := params.TextDocument.URI
16
	state, ok := s.getDocState(u)
17
	if !ok {
18
		return nil, nil
19
	}
20
	an := s.analysisFor(u)
21
	if an == nil {
22
		return nil, nil
23
	}
24
	pf := parsedFileFor(an, u.Path())
25
	if pf == nil {
26
		return nil, nil
27
	}
28
29
	li := state.lineIdx
30
	docSel := protocol.SelectionRange{Range: li.SpanRange(fullDocSpan(pf.Src))}
31
	out := make([]protocol.SelectionRange, len(params.Positions))
32
	for i, pos := range params.Positions {
33
		cursor := li.Offset(int(pos.Line), int(pos.Character))
34
		out[i] = selectionAt(state.text, pf, li, cursor, docSel)
35
	}
36
	return out, nil
37
}
38
39
func fullDocSpan(src []byte) token.Span { return token.Span{End: token.Pos{Offset: len(src)}} }
40
41
func selectionAt(content string, pf *journal.ParsedFile, li *lsputil.LineIndex, cursor int, docSel protocol.SelectionRange) protocol.SelectionRange {
42
	e := entryAt(pf.Ast.Entries, cursor)
43
	if e == nil {
44
		return docSel
45
	}
46
	es := entrySpan(e)
47
	if !spanContains(content, es, cursor) {
48
		return docSel
49
	}
50
	switch c := e.(type) {
51
	case *ast.BlankLine:
52
		return docSel
53
	case *ast.Comment: // the comment span is the entry span itself: no extra level
54
		return commentOrParent(content, c, li, cursor, docSel)
55
	}
56
	parent := protocol.SelectionRange{Range: li.SpanRange(es), Parent: &docSel}
57
	return selectionInEntry(content, e, li, cursor, parent)
58
}
59
60
func selectionInEntry(content string, e ast.Entry, li *lsputil.LineIndex, cursor int, parent protocol.SelectionRange) protocol.SelectionRange {
61
	switch t := e.(type) {
62
	case *ast.Transaction:
63
		return transactionSelection(content, t, li, cursor, parent)
64
	case *ast.PeriodicTransaction:
65
		return periodicSelection(content, t, li, cursor, parent)
66
	case *ast.AutomatedTransaction:
67
		return automatedSelection(content, t, li, cursor, parent)
68
	case *ast.AccountDirective:
69
		return accountDirectiveSelection(content, t, li, cursor, parent)
70
	case *ast.CommodityDirective:
71
		return commodityDirectiveSelection(content, t, li, cursor, parent)
72
	case *ast.PayeeDirective:
73
		if t.Name != nil {
74
			if sel, ok := selectionForSpan(content, li, t.Name.Span, cursor, parent); ok {
75
				return sel
76
			}
77
		}
78
		return commentOrParent(content, t.Comment, li, cursor, parent)
79
	case *ast.TagDirective:
80
		if sp, ok := tagDirectiveSpan(content, t); ok {
81
			if sel, ok := selectionForSpan(content, li, sp, cursor, parent); ok {
82
				return sel
83
			}
84
		}
85
		return commentOrParent(content, t.Comment, li, cursor, parent)
86
	case *ast.AliasDirective:
87
		if sel, ok := accountSelection(content, &t.From, li, cursor, parent); ok {
88
			return sel
89
		}
90
		if sel, ok := accountSelection(content, &t.To, li, cursor, parent); ok {
91
			return sel
92
		}
93
		return commentOrParent(content, t.Comment, li, cursor, parent)
94
	case *ast.DefaultCommodityDirective:
95
		if sel, ok := amountSelection(content, &t.Amount, li, cursor, parent); ok {
96
			return sel
97
		}
98
		return commentOrParent(content, t.Comment, li, cursor, parent)
99
	case *ast.MarketPriceDirective:
100
		if sel, ok := selectionForSpan(content, li, t.DateTime.Date.Span, cursor, parent); ok {
101
			return sel
102
		}
103
		if t.DateTime.Time != nil {
104
			if sel, ok := selectionForSpan(content, li, t.DateTime.Time.Span, cursor, parent); ok {
105
				return sel
106
			}
107
		}
108
		if sel, ok := amountSelection(content, &t.Amount, li, cursor, parent); ok {
109
			return sel
110
		}
111
		return commentOrParent(content, t.Comment, li, cursor, parent)
112
	case *ast.ConversionDirective:
113
		if sel, ok := amountSelection(content, &t.From, li, cursor, parent); ok {
114
			return sel
115
		}
116
		if sel, ok := amountSelection(content, &t.To, li, cursor, parent); ok {
117
			return sel
118
		}
119
		return commentOrParent(content, t.Comment, li, cursor, parent)
120
	}
121
	return parent
122
}
123
124
func transactionSelection(content string, t *ast.Transaction, li *lsputil.LineIndex, cursor int, parent protocol.SelectionRange) protocol.SelectionRange {
125
	var header [6]token.Span // date, second date, status, code, payee, note
126
	sps := append(header[:0], t.Date.Span)
127
	if t.SecondDate != nil {
128
		sps = append(sps, t.SecondDate.Span)
129
	}
130
	if t.Status.Value != ast.StatusNone {
131
		sps = append(sps, t.Status.Span)
132
	}
133
	if t.Code != nil {
134
		sps = append(sps, t.Code.Span)
135
	}
136
	if t.Payee != nil {
137
		sps = append(sps, t.Payee.Span)
138
	}
139
	if t.Note != nil {
140
		sps = append(sps, t.Note.Span)
141
	}
142
	for _, sp := range sps {
143
		if sel, ok := selectionForSpan(content, li, sp, cursor, parent); ok {
144
			return sel
145
		}
146
	}
147
	return commentsAndPostingsSelection(content, t.Comment, t.HeaderComments, t.Postings, li, cursor, parent)
148
}
149
150
// commentsAndPostingsSelection descends into an entry's inline comment, header
151
// comments, then postings; returns parent when none contains the cursor.
152
func commentsAndPostingsSelection(content string, inline *ast.Comment, headers []*ast.Comment, postings []ast.Posting, li *lsputil.LineIndex, cursor int, parent protocol.SelectionRange) protocol.SelectionRange {
153
	if sel, ok := commentSelection(content, inline, li, cursor, parent); ok {
154
		return sel
155
	}
156
	for _, c := range headers {
157
		if sel, ok := commentSelection(content, c, li, cursor, parent); ok {
158
			return sel
159
		}
160
	}
161
	if sel, ok := postingsSelection(content, postings, li, cursor, parent); ok {
162
		return sel
163
	}
164
	return parent
165
}
166
167
func periodicSelection(content string, pt *ast.PeriodicTransaction, li *lsputil.LineIndex, cursor int, parent protocol.SelectionRange) protocol.SelectionRange {
168
	if sel, ok := selectionForSpan(content, li, pt.Period.Span, cursor, parent); ok {
169
		if d := pt.Period.From; d != nil {
170
			if sub, ok := selectionForSpan(content, li, d.Span, cursor, sel); ok {
171
				return sub
172
			}
173
		}
174
		if d := pt.Period.To; d != nil {
175
			if sub, ok := selectionForSpan(content, li, d.Span, cursor, sel); ok {
176
				return sub
177
			}
178
		}
179
		return sel
180
	}
181
	if pt.Description != nil {
182
		if sel, ok := selectionForSpan(content, li, pt.Description.Span, cursor, parent); ok {
183
			return sel
184
		}
185
	}
186
	return commentsAndPostingsSelection(content, pt.Comment, pt.HeaderComments, pt.Postings, li, cursor, parent)
187
}
188
189
func automatedSelection(content string, at *ast.AutomatedTransaction, li *lsputil.LineIndex, cursor int, parent protocol.SelectionRange) protocol.SelectionRange {
190
	if sel, ok := selectionForSpan(content, li, at.Expr.Span, cursor, parent); ok {
191
		return sel
192
	}
193
	return commentsAndPostingsSelection(content, at.Comment, at.HeaderComments, at.Postings, li, cursor, parent)
194
}
195
196
func accountDirectiveSelection(content string, d *ast.AccountDirective, li *lsputil.LineIndex, cursor int, parent protocol.SelectionRange) protocol.SelectionRange {
197
	if sel, ok := accountSelection(content, &d.Account, li, cursor, parent); ok {
198
		return sel
199
	}
200
	for i := range d.Subdirectives {
201
		sd := &d.Subdirectives[i]
202
		if sd.Kind == ast.SubdirectiveComment {
203
			if sel, ok := commentSelection(content, sd.Comment, li, cursor, parent); ok {
204
				return sel
205
			}
206
			continue
207
		}
208
		if sel, ok := selectionForSpan(content, li, sd.ValueSpan, cursor, parent); ok {
209
			return sel
210
		}
211
	}
212
	return commentOrParent(content, d.Comment, li, cursor, parent)
213
}
214
215
func commodityDirectiveSelection(content string, d *ast.CommodityDirective, li *lsputil.LineIndex, cursor int, parent protocol.SelectionRange) protocol.SelectionRange {
216
	if sel, ok := selectionForSpan(content, li, d.CommoditySpan, cursor, parent); ok {
217
		return sel
218
	}
219
	if d.FormatSub != nil {
220
		if sel, ok := amountSelection(content, &d.FormatSub.Amount, li, cursor, parent); ok {
221
			return sel
222
		}
223
	}
224
	return commentOrParent(content, d.Comment, li, cursor, parent)
225
}
226
227
// postingsSelection returns the selection inside the posting containing cursor.
228
func postingsSelection(content string, postings []ast.Posting, li *lsputil.LineIndex, cursor int, parent protocol.SelectionRange) (protocol.SelectionRange, bool) {
229
	for i := range postings {
230
		p := &postings[i]
231
		postingSel, ok := selectionForSpan(content, li, p.Span, cursor, parent)
232
		if !ok {
233
			continue
234
		}
235
		return postingSelection(content, p, li, cursor, postingSel), true
236
	}
237
	return protocol.SelectionRange{}, false
238
}
239
240
func postingSelection(content string, p *ast.Posting, li *lsputil.LineIndex, cursor int, parent protocol.SelectionRange) protocol.SelectionRange {
241
	if p.Status.Value != ast.StatusNone {
242
		if sel, ok := selectionForSpan(content, li, p.Status.Span, cursor, parent); ok {
243
			return sel
244
		}
245
	}
246
	if sel, ok := accountSelection(content, &p.Account, li, cursor, parent); ok {
247
		return sel
248
	}
249
	if sel, ok := amountSelection(content, p.Amount, li, cursor, parent); ok {
250
		return sel
251
	}
252
	if p.Cost != nil {
253
		if sel, ok := selectionForSpan(content, li, p.Cost.Span, cursor, parent); ok {
254
			return sel
255
		}
256
	}
257
	if p.Balance != nil {
258
		if sel, ok := selectionForSpan(content, li, p.Balance.Span, cursor, parent); ok {
259
			return sel
260
		}
261
	}
262
	if sel, ok := commentSelection(content, p.Comment, li, cursor, parent); ok {
263
		return sel
264
	}
265
	for i := range p.Comments {
266
		if sel, ok := commentSelection(content, &p.Comments[i], li, cursor, parent); ok {
267
			return sel
268
		}
269
	}
270
	return parent
271
}
272
273
func accountSelection(content string, a *ast.Account, li *lsputil.LineIndex, cursor int, parent protocol.SelectionRange) (protocol.SelectionRange, bool) {
274
	accountSel, ok := selectionForSpan(content, li, a.Span, cursor, parent)
275
	if !ok {
276
		return protocol.SelectionRange{}, false
277
	}
278
	if len(a.Name) <= 1 {
279
		return accountSel, true
280
	}
281
	for i := range a.Name {
282
		if sel, ok := selectionForSpan(content, li, a.Name[i].Span, cursor, accountSel); ok {
283
			return sel, true
284
		}
285
	}
286
	return accountSel, true
287
}
288
289
func amountSelection(content string, am *ast.Amount, li *lsputil.LineIndex, cursor int, parent protocol.SelectionRange) (protocol.SelectionRange, bool) {
290
	if am == nil {
291
		return protocol.SelectionRange{}, false
292
	}
293
	amountSel, ok := selectionForSpan(content, li, am.Span, cursor, parent)
294
	if !ok {
295
		return protocol.SelectionRange{}, false
296
	}
297
	if sel, ok := selectionForSpan(content, li, am.CommoditySpan, cursor, amountSel); ok {
298
		return sel, true
299
	}
300
	qStart, qEnd := quantitySpan(content, am)
301
	if qEnd > qStart {
302
		if sel, ok := selectionForSpan(content, li, token.Span{Start: token.Pos{Offset: qStart}, End: token.Pos{Offset: qEnd}}, cursor, amountSel); ok {
303
			return sel, true
304
		}
305
	}
306
	return amountSel, true
307
}
308
309
// selectionForSpan selects span nested in parent when span contains cursor.
310
func selectionForSpan(content string, li *lsputil.LineIndex, span token.Span, cursor int, parent protocol.SelectionRange) (protocol.SelectionRange, bool) {
311
	if !spanContains(content, span, cursor) {
312
		return protocol.SelectionRange{}, false
313
	}
314
	return protocol.SelectionRange{Range: li.SpanRange(span), Parent: &parent}, true
315
}
316
317
// commentSelection returns a selection inside comment when the cursor is on it:
318
// the tag key when the cursor is on a tag, else the whole comment line.
319
func commentSelection(content string, c *ast.Comment, li *lsputil.LineIndex, cursor int, parent protocol.SelectionRange) (protocol.SelectionRange, bool) {
320
	if c == nil {
321
		return protocol.SelectionRange{}, false
322
	}
323
	commentSel, ok := selectionForSpan(content, li, c.Span, cursor, parent)
324
	if !ok {
325
		return protocol.SelectionRange{}, false
326
	}
327
	if ref := tagRefInComment(content, c, cursor); ref != nil {
328
		return protocol.SelectionRange{Range: li.SpanRange(ref.span), Parent: &commentSel}, true
329
	}
330
	return commentSel, true
331
}
332
333
// commentOrParent is commentSelection with parent as the fallback.
334
func commentOrParent(content string, c *ast.Comment, li *lsputil.LineIndex, cursor int, parent protocol.SelectionRange) protocol.SelectionRange {
335
	if sel, ok := commentSelection(content, c, li, cursor, parent); ok {
336
		return sel
337
	}
338
	return parent
339
}