all repos

clerk @ 1925839

missing tooling for ledger/hledger

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

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
add support of subdirectives and it's highlights, 1 month ago
1
package lsp
2
3
import (
4
	"context"
5
	"slices"
6
	"unicode/utf8"
7
8
	"go.lsp.dev/protocol"
9
	"go.lsp.dev/uri"
10
11
	"olexsmir.xyz/clerk/internal/lsp/lsputil"
12
	"olexsmir.xyz/clerk/journal/ast"
13
	"olexsmir.xyz/clerk/journal/lexer"
14
	"olexsmir.xyz/clerk/journal/token"
15
)
16
17
func (s *server) SemanticTokensFull(ctx context.Context, params *protocol.SemanticTokensParams) (*protocol.SemanticTokens, error) {
18
	if !s.semanticHighlightingEnabled() {
19
		return &protocol.SemanticTokens{}, nil
20
	}
21
22
	tokens, ok := s.tokensForDoc(params.TextDocument.URI)
23
	if !ok {
24
		return &protocol.SemanticTokens{}, nil
25
	}
26
	return &protocol.SemanticTokens{Data: encodeSemTokens(tokens)}, nil
27
}
28
29
func (s *server) SemanticTokensRange(ctx context.Context, params *protocol.SemanticTokensRangeParams) (*protocol.SemanticTokens, error) {
30
	if !s.semanticHighlightingEnabled() {
31
		return &protocol.SemanticTokens{}, nil
32
	}
33
34
	tokens, ok := s.tokensForDoc(params.TextDocument.URI)
35
	if !ok {
36
		return &protocol.SemanticTokens{}, nil
37
	}
38
	start := int(params.Range.Start.Line)
39
	end := int(params.Range.End.Line)
40
	var filtered []semanticToken
41
	for _, t := range tokens {
42
		if t.line >= uint32(start) && t.line <= uint32(end) {
43
			filtered = append(filtered, t)
44
		}
45
	}
46
	return &protocol.SemanticTokens{Data: encodeSemTokens(filtered)}, nil
47
}
48
49
func (s *server) tokensForDoc(doc uri.URI) ([]semanticToken, bool) {
50
	s.mu.Lock()
51
	defer s.mu.Unlock()
52
	st, ok := s.openDocs[doc]
53
	if !ok {
54
		return nil, false
55
	}
56
	if st.semTokens != nil {
57
		return st.semTokens, true
58
	}
59
	st.semTokens = tokenizeForSemantics(st.text, parseJournalStr(st.text))
60
	s.openDocs[doc] = st
61
	return st.semTokens, true
62
}
63
64
// Implementation
65
66
const (
67
	semDirective uint32 = iota
68
	semDate
69
	semAccount
70
	semCommodity
71
	semAmount
72
	semStatus
73
	semComment
74
	semString
75
	semOperator
76
	semProperty
77
)
78
79
var tokenTypeStrings = []string{
80
	string(protocol.SemanticTokenTypesKeyword),   // directive
81
	string(protocol.SemanticTokenTypesClass),     // date
82
	string(protocol.SemanticTokenTypesNamespace), // account
83
	string(protocol.SemanticTokenTypesType),      // commodity
84
	string(protocol.SemanticTokenTypesNumber),    // amount
85
	string(protocol.SemanticTokenTypesOperator),  // status
86
	string(protocol.SemanticTokenTypesComment),   // comment
87
	string(protocol.SemanticTokenTypesString),    // string
88
	string(protocol.SemanticTokenTypesOperator),  // operator
89
	string(protocol.SemanticTokenTypesProperty),  // property
90
}
91
92
const (
93
	modifierAbstract = 1 << 0 // virtual account
94
	modifierNegative = 1 << 1 // negative amount
95
)
96
97
var modifierStrings = []string{
98
	"abstract", // bit 0
99
	"negative", // bit 1
100
}
101
102
func getSemanticTokensLegend() protocol.SemanticTokensLegend {
103
	return protocol.SemanticTokensLegend{
104
		TokenTypes:     tokenTypeStrings,
105
		TokenModifiers: modifierStrings,
106
	}
107
}
108
109
type semanticToken struct {
110
	line, col uint32 // 0-based
111
	length    uint32
112
	tokenType uint32
113
	modifiers uint32
114
}
115
116
func tokenizeForSemantics(content string, j *ast.Journal) []semanticToken {
117
	var raw []rawSpan
118
	emit := func(s token.Span, tokType, mods uint32) {
119
		if s.Start.Offset >= s.End.Offset {
120
			return
121
		}
122
		raw = append(raw, rawSpan{s, tokType, mods})
123
	}
124
	for _, e := range j.Entries {
125
		visitEntry(content, e, emit)
126
	}
127
	if len(j.Errors) > 0 {
128
		// parser recovers per line; lexer fills the unparsed regions, keeping ast tokens where the parser succeeded
129
		semLexerFallback(content, raw, emit)
130
	}
131
	return rawToSemanticTokens(content, raw)
132
}
133
134
// rawSpan is a source span tagged with semantic token
135
type rawSpan struct {
136
	span      token.Span
137
	tok, mods uint32
138
}
139
140
func rawToSemanticTokens(content string, raw []rawSpan) []semanticToken {
141
	if len(raw) == 0 {
142
		return nil
143
	}
144
	slices.SortFunc(raw, func(a, b rawSpan) int { return a.span.Start.Offset - b.span.Start.Offset })
145
	out := make([]semanticToken, len(raw))
146
	line, col, cursor := 0, 0, 0
147
	advance := func(end int) {
148
		for cursor < end {
149
			r, size := utf8.DecodeRuneInString(content[cursor:])
150
			if r == utf8.RuneError && size <= 1 {
151
				break
152
			}
153
			if r == '\r' {
154
				cursor += size
155
				if cursor < len(content) && content[cursor] == '\n' {
156
					cursor++
157
				}
158
				line++
159
				col = 0
160
				continue
161
			}
162
			if r == '\n' {
163
				cursor += size
164
				line++
165
				col = 0
166
				continue
167
			}
168
			cursor += size
169
			col += utf16Units(r)
170
		}
171
	}
172
	for i, t := range raw {
173
		if cursor < t.span.Start.Offset {
174
			advance(t.span.Start.Offset)
175
		}
176
		out[i] = semanticToken{
177
			line:      uint32(line),
178
			col:       uint32(col),
179
			length:    uint32(lsputil.Utf16Len(content, t.span.Start.Offset, t.span.End.Offset)),
180
			tokenType: t.tok,
181
			modifiers: t.mods,
182
		}
183
		advance(t.span.End.Offset)
184
	}
185
	return out
186
}
187
188
func utf16Units(r rune) int {
189
	if r >= 0x10000 && r <= 0x10FFFF {
190
		return 2
191
	}
192
	return 1
193
}
194
195
func visitEntry(content string, e ast.Entry, emit semEmitFunc) {
196
	switch e := e.(type) {
197
	case *ast.Transaction:
198
		visitTransaction(content, e, emit)
199
	case *ast.PeriodicTransaction:
200
		visitPeriodicTransaction(content, e, emit)
201
	case *ast.AutomatedTransaction:
202
		visitAutomatedTransaction(content, e, emit)
203
	case *ast.AccountDirective:
204
		emit(directiveKeyword(e.Span, "account"), semDirective, 0)
205
		emit(e.Account.Span, semAccount, 0)
206
		for _, sd := range e.Subdirectives {
207
			if sd.Name == "" {
208
				emitComment(sd.Comment, emit)
209
				continue
210
			}
211
			emit(sd.NameSpan, semDirective, 0)
212
			switch sd.Name {
213
			case "alias":
214
				emit(sd.ValueSpan, semAccount, 0)
215
			case "type", "note":
216
				emit(sd.ValueSpan, semProperty, 0)
217
			}
218
			emitComment(sd.Comment, emit)
219
		}
220
		emitComment(e.Comment, emit)
221
	case *ast.CommodityDirective:
222
		emit(directiveKeyword(e.Span, "commodity"), semDirective, 0)
223
		if e.FormatSub != nil {
224
			if e.FormatSub.KeywordSpan.End.Offset > 0 {
225
				emit(e.FormatSub.KeywordSpan, semDirective, 0)
226
			}
227
			semEmitAmount(content, &e.FormatSub.Amount, emit)
228
			emitComment(e.FormatSub.Comment, emit)
229
		} else if e.CommoditySpan.Start.Offset > 0 && e.CommoditySpan.End.Offset > 0 {
230
			emit(e.CommoditySpan, semCommodity, 0)
231
		}
232
		emitBlockComments(e.BlockComments, emit)
233
		emitComment(e.Comment, emit)
234
	case *ast.IncludeDirective:
235
		emitDirective(content, e.Span, len("include"), semString, e.Comment, emit)
236
	case *ast.PayeeDirective:
237
		emitDirective(content, e.Span, len("payee"), semProperty, e.Comment, emit)
238
	case *ast.TagDirective:
239
		emitDirective(content, e.Span, len("tag"), semProperty, e.Comment, emit)
240
	case *ast.AliasDirective:
241
		emit(directiveKeyword(e.Span, "alias"), semDirective, 0)
242
		emit(e.From.Span, semAccount, 0)
243
		if op, ok := betweenSpan(content, e.Span.Start.File, e.From.Span.End.Offset, e.To.Span.Start.Offset); ok {
244
			emit(op, semOperator, 0)
245
		}
246
		emit(e.To.Span, semAccount, 0)
247
		emitComment(e.Comment, emit)
248
	case *ast.YearDirective:
249
		kwLen := len("year")
250
		if content[e.Span.Start.Offset] == 'Y' {
251
			kwLen = 1
252
		}
253
		emitDirective(content, e.Span, kwLen, semProperty, e.Comment, emit)
254
	case *ast.DecimalMarkDirective:
255
		emitDirective(content, e.Span, len("decimal-mark"), semProperty, e.Comment, emit)
256
	case *ast.DefaultCommodityDirective:
257
		emit(directiveKeyword(e.Span, "D"), semDirective, 0)
258
		semEmitAmount(content, &e.Amount, emit)
259
		emitComment(e.Comment, emit)
260
	case *ast.MarketPriceDirective:
261
		emit(directiveKeyword(e.Span, "P"), semDirective, 0)
262
		emit(e.DateTime.Date.Span, semDate, 0)
263
		if e.DateTime.Time != nil {
264
			emit(e.DateTime.Time.Span, semDate, 0)
265
		}
266
		// commodity: text between the date (or time) and the amount
267
		commStart := e.DateTime.Date.Span.End.Offset
268
		if e.DateTime.Time != nil {
269
			commStart = e.DateTime.Time.Span.End.Offset
270
		}
271
		if comm, ok := betweenSpan(content, e.Span.Start.File, commStart, e.Amount.Span.Start.Offset); ok {
272
			emit(comm, semCommodity, 0)
273
		}
274
		semEmitAmount(content, &e.Amount, emit)
275
		emitComment(e.Comment, emit)
276
	case *ast.ConversionDirective:
277
		emit(directiveKeyword(e.Span, "C"), semDirective, 0)
278
		semEmitAmount(content, &e.From, emit)
279
		// = operator: text between the two amounts
280
		if op, ok := betweenSpan(content, e.Span.Start.File, e.From.Span.End.Offset, e.To.Span.Start.Offset); ok {
281
			emit(op, semOperator, 0)
282
		}
283
		semEmitAmount(content, &e.To, emit)
284
		emitComment(e.Comment, emit)
285
	case *ast.Comment:
286
		emitComment(e, emit)
287
	case *ast.CommentBlockDirective:
288
		emit(e.Span, semComment, 0)
289
	case *ast.IgnoredDirective:
290
		emitDirective(content, e.Span, len("N"), semProperty, e.Comment, emit)
291
	case *ast.ApplyDirective:
292
		emitDirective(content, e.Span, len("apply"), semProperty, e.Comment, emit)
293
	case *ast.EndDirective:
294
		emitDirective(content, e.Span, len("end"), semProperty, e.Comment, emit)
295
	case *ast.BlankLine:
296
	}
297
}
298
299
func visitTransaction(content string, t *ast.Transaction, emit semEmitFunc) {
300
	emit(t.Date.Span, semDate, 0)
301
	if t.SecondDate != nil {
302
		emit(t.SecondDate.Span, semDate, 0)
303
	}
304
	if t.Status.Value != ast.StatusNone {
305
		emit(t.Status.Span, semStatus, 0)
306
	}
307
	if t.Code != nil {
308
		emit(t.Code.Span, semString, 0)
309
	}
310
	if t.Payee != nil {
311
		emit(t.Payee.Span, semProperty, 0)
312
	}
313
	if t.Note != nil {
314
		emit(t.Note.Span, semProperty, 0)
315
	}
316
	emitComment(t.Comment, emit)
317
	for i := range t.HeaderComments {
318
		emitComment(t.HeaderComments[i], emit)
319
	}
320
	for _, p := range t.Postings {
321
		visitPosting(content, p, emit)
322
	}
323
}
324
325
func visitPeriodicTransaction(content string, pt *ast.PeriodicTransaction, emit semEmitFunc) {
326
	// ~ operator is at the start of the period span
327
	emit(offsetSpan(pt.Span.Start.File, pt.Span.Start.Offset, pt.Span.Start.Offset+1), semOperator, 0)
328
329
	// The period span covers the whole expr, including any "from ... to ..." dates
330
	if pt.Period.Span.End.Offset > pt.Period.Span.Start.Offset {
331
		var dates []*ast.Date
332
		if pt.Period.From != nil {
333
			dates = append(dates, pt.Period.From)
334
		}
335
		if pt.Period.To != nil {
336
			dates = append(dates, pt.Period.To)
337
		}
338
		pos := pt.Period.Span.Start.Offset
339
		for _, d := range dates {
340
			if d.Span.Start.Offset > pos {
341
				emit(offsetSpan(pt.Period.Span.Start.File, pos, d.Span.Start.Offset), semProperty, 0)
342
			}
343
			emit(d.Span, semDate, 0)
344
			pos = d.Span.End.Offset
345
		}
346
		if pos < pt.Period.Span.End.Offset {
347
			emit(offsetSpan(pt.Period.Span.Start.File, pos, pt.Period.Span.End.Offset), semProperty, 0)
348
		}
349
	}
350
	if pt.Description != nil {
351
		emit(pt.Description.Span, semProperty, 0)
352
	}
353
	emitComment(pt.Comment, emit)
354
	for i := range pt.HeaderComments {
355
		emitComment(pt.HeaderComments[i], emit)
356
	}
357
	for _, p := range pt.Postings {
358
		visitPosting(content, p, emit)
359
	}
360
}
361
362
func visitAutomatedTransaction(content string, at *ast.AutomatedTransaction, emit semEmitFunc) {
363
	// = operator is at the start of the expression span
364
	emit(offsetSpan(at.Span.Start.File, at.Span.Start.Offset, at.Span.Start.Offset+1), semOperator, 0)
365
366
	if at.Expr.Value != "" {
367
		emit(at.Expr.Span, semString, 0)
368
	}
369
	emitComment(at.Comment, emit)
370
	for i := range at.HeaderComments {
371
		emitComment(at.HeaderComments[i], emit)
372
	}
373
	for _, p := range at.Postings {
374
		visitPosting(content, p, emit)
375
	}
376
}
377
378
func visitPosting(content string, p *ast.Posting, emit semEmitFunc) {
379
	if p.Status.Value != ast.StatusNone {
380
		emit(p.Status.Span, semStatus, 0)
381
	}
382
383
	// virtual brackets
384
	if p.Type == ast.PostingVirtualUnbalanced || p.Type == ast.PostingVirtualBalanced {
385
		// opening bracket
386
		for off := p.Span.Start.Offset; off < p.Account.Span.Start.Offset && off < p.Span.End.Offset; off++ {
387
			if content[off] == '(' || content[off] == '[' {
388
				brSpan := token.Span{Start: offsetPos(p.Span.Start.File, off), End: offsetPos(p.Span.Start.File, off+1)}
389
				emit(brSpan, semOperator, modifierAbstract)
390
				break
391
			}
392
		}
393
		// closing bracket
394
		for off := p.Account.Span.End.Offset; off < p.Span.End.Offset; off++ {
395
			if content[off] == ')' || content[off] == ']' {
396
				brSpan := token.Span{Start: offsetPos(p.Span.Start.File, off), End: offsetPos(p.Span.Start.File, off+1)}
397
				emit(brSpan, semOperator, modifierAbstract)
398
				break
399
			}
400
		}
401
	}
402
403
	emit(p.Account.Span, semAccount, 0)
404
405
	if p.Amount != nil {
406
		semEmitAmount(content, p.Amount, emit)
407
	}
408
	if p.Cost != nil {
409
		semEmitCost(content, p.Cost, emit)
410
	}
411
	if p.Balance != nil {
412
		semEmitBalanceAssertion(content, p.Balance, emit)
413
	}
414
	emitComment(p.Comment, emit)
415
	for i := range p.Comments {
416
		emitComment(&p.Comments[i], emit)
417
	}
418
}
419
420
// directiveKeyword returns the span of the leading keyword on a directive line.
421
func directiveKeyword(e token.Span, kw string) token.Span {
422
	return token.Span{Start: e.Start, End: offsetPos(e.Start.File, e.Start.Offset+len(kw))}
423
}
424
425
// directiveValue returns the trimmed span of the text after the keyword end
426
// offset, up to the inline comment or the end of the line.
427
func directiveValue(content string, e token.Span, comment *ast.Comment, kwEnd int) (token.Span, bool) {
428
	end := e.End.Offset
429
	if comment != nil {
430
		end = comment.Span.Start.Offset
431
	}
432
	return betweenSpan(content, e.Start.File, kwEnd, end)
433
}
434
435
func semEmitAmount(content string, a *ast.Amount, emit semEmitFunc) {
436
	if a == nil {
437
		return
438
	}
439
	hasCommodity := a.CommoditySpan.Start.Offset > 0 && a.CommoditySpan.End.Offset > 0
440
	if hasCommodity && a.CommodityPos == ast.CommodityBefore {
441
		emit(a.CommoditySpan, semCommodity, 0)
442
		semEmitQuantity(content, a, emit)
443
		return
444
	}
445
	semEmitQuantity(content, a, emit)
446
	if hasCommodity {
447
		emit(a.CommoditySpan, semCommodity, 0)
448
	}
449
}
450
451
func semEmitQuantity(content string, a *ast.Amount, emit semEmitFunc) {
452
	qStart, qEnd := quantitySpan(content, a)
453
	if qEnd <= qStart {
454
		return
455
	}
456
	mods := uint32(0)
457
	if a.IsNegative {
458
		mods |= modifierNegative
459
	}
460
	emit(offsetSpan(a.Span.Start.File, qStart, qEnd), semAmount, mods)
461
}
462
463
func quantitySpan(content string, a *ast.Amount) (int, int) {
464
	start, end := a.Span.Start.Offset, a.Span.End.Offset
465
	switch {
466
	case a.Commodity == "":
467
		// bare quantity
468
	case a.CommodityPos == ast.CommodityBefore:
469
		// "$50.00" or "$   50.00": quantity follows the commodity span
470
		start = a.CommoditySpan.End.Offset
471
	default: // CommodityAfter
472
		// "50.00 USD" or "50.00     USD": quantity precedes the commodity
473
		end = a.CommoditySpan.Start.Offset
474
	}
475
	for start < end && (content[start] == ' ' || content[start] == '\t') {
476
		start++
477
	}
478
	for end > start && (content[end-1] == ' ' || content[end-1] == '\t') {
479
		end--
480
	}
481
	return start, end
482
}
483
484
type semEmitFunc func(tok token.Span, tokKind, modifier uint32)
485
486
func emitBlockComments(cs []*ast.Comment, emit semEmitFunc) {
487
	for _, c := range cs {
488
		emitComment(c, emit)
489
	}
490
}
491
492
func emitComment(c *ast.Comment, emit semEmitFunc) {
493
	if c == nil {
494
		return
495
	}
496
	if len(c.Tags) == 0 {
497
		emit(c.Span, semComment, 0)
498
		return
499
	}
500
	pos := c.Span.Start.Offset
501
	for _, t := range c.Tags {
502
		if t.Span.Start.Offset > pos {
503
			emit(offsetSpan(c.Span.Start.File, pos, t.Span.Start.Offset), semComment, 0)
504
		}
505
		emit(t.Span, semProperty, 0)
506
		pos = t.Span.End.Offset
507
	}
508
	if pos < c.Span.End.Offset {
509
		emit(offsetSpan(c.Span.Start.File, pos, c.Span.End.Offset), semComment, 0)
510
	}
511
}
512
513
func emitDirective(content string, e token.Span, kwLen int, valType uint32, comment *ast.Comment, emit semEmitFunc) {
514
	kwEnd := e.Start.Offset + kwLen
515
	emit(token.Span{Start: e.Start, End: offsetPos(e.Start.File, kwEnd)}, semDirective, 0)
516
	if v, ok := directiveValue(content, e, comment, kwEnd); ok {
517
		emit(v, valType, 0)
518
	}
519
	emitComment(comment, emit)
520
}
521
522
func semEmitCost(content string, c *ast.Cost, emit semEmitFunc) {
523
	if c.IsTotal {
524
		emit(token.Span{Start: c.Span.Start, End: offsetPos(c.Span.Start.File, c.Span.Start.Offset+2)}, semOperator, 0)
525
	} else {
526
		emit(token.Span{Start: c.Span.Start, End: offsetPos(c.Span.Start.File, c.Span.Start.Offset+1)}, semOperator, 0)
527
	}
528
	semEmitAmount(content, &c.Amount, emit)
529
}
530
531
func semEmitBalanceAssertion(content string, ba *ast.BalanceAssertion, emit semEmitFunc) {
532
	// The operator is the run of '=', ':', '*' chars from the span start.
533
	// The ':' of ':=' precedes the '=' token, so back up one offset.
534
	opStart := ba.Span.Start.Offset
535
	if ba.IsAssignment && opStart > 0 {
536
		opStart--
537
	}
538
	opEnd := opStart
539
	for opEnd < ba.Span.End.Offset && (content[opEnd] == '=' || content[opEnd] == ':' || content[opEnd] == '*') {
540
		opEnd++
541
	}
542
	emit(token.Span{Start: offsetPos(ba.Span.Start.File, opStart), End: offsetPos(ba.Span.Start.File, opEnd)}, semOperator, 0)
543
	semEmitAmount(content, &ba.Amount, emit)
544
	if ba.Cost != nil {
545
		semEmitCost(content, ba.Cost, emit)
546
	}
547
}
548
549
func semLexerFallback(content string, base []rawSpan, emit semEmitFunc) {
550
	l := lexer.New("", []byte(content))
551
552
	var commentStart, commentEnd int // 0 = not inside a comment line
553
	lineStart := true                // the next significant token starts a line
554
	skipLine := false                // the line starts with an unclassifiable token; emit nothing
555
	i := 0                           // next base span to compare against
556
557
	take := func(span token.Span, tokType uint32, mods uint32) {
558
		for i < len(base) && base[i].span.End.Offset <= span.Start.Offset {
559
			i++
560
		}
561
		if i < len(base) && base[i].span.Start.Offset < span.End.Offset {
562
			return // overlaps an AST token; AST wins
563
		}
564
		emit(span, tokType, mods)
565
	}
566
567
	for {
568
		tok := l.Next()
569
		if tok.Type == token.EOF {
570
			if commentStart > 0 {
571
				take(token.Span{Start: offsetPos("", commentStart), End: offsetPos("", commentEnd)}, semComment, 0)
572
			}
573
			break
574
		}
575
		if tok.Type == token.NEWLINE {
576
			if commentStart > 0 {
577
				take(token.Span{Start: offsetPos("", commentStart), End: offsetPos("", commentEnd)}, semComment, 0)
578
				commentStart, commentEnd = 0, 0
579
			}
580
			lineStart, skipLine = true, false
581
			continue
582
		}
583
		if tok.Type == token.WHITESPACE || tok.Type == token.INDENT {
584
			continue
585
		}
586
		if lineStart {
587
			lineStart = false
588
			if !isLineStartToken(tok.Type) {
589
				skipLine = true
590
			}
591
		}
592
		if skipLine {
593
			continue
594
		}
595
		tokType := semProperty
596
		if commentStart > 0 {
597
			tokType = semComment
598
			if tok.Span.End.Offset > commentEnd {
599
				commentEnd = tok.Span.End.Offset
600
			}
601
			continue
602
		}
603
604
		switch tok.Type {
605
		case token.SEMICOLON, token.HASH, token.PERCENT, token.STAR:
606
			tokType = semComment
607
			commentStart = tok.Span.Start.Offset
608
			commentEnd = tok.Span.End.Offset
609
			continue
610
		case token.STRING:
611
			tokType = semString
612
		case token.DATE:
613
			tokType = semDate
614
		case token.INT, token.DECIMAL:
615
			tokType = semAmount
616
		case token.COMMODITYMARK:
617
			tokType = semCommodity
618
		case token.BANG:
619
			tokType = semStatus
620
		case token.AT, token.ATAT, token.EQ, token.EQEQ, token.EQEQEQ, token.EQSTAR:
621
			tokType = semOperator
622
		case token.COMMENTKW, token.ACCOUNT, token.COMMODITY, token.INCLUDE,
623
			token.ALIAS, token.PAYEE, token.TAG, token.APPLY, token.END,
624
			token.YEAR, token.DECIMALMARK, token.D, token.P, token.N, token.C:
625
			tokType = semDirective
626
		}
627
		take(tok.Span, tokType, 0)
628
	}
629
}
630
631
func isLineStartToken(t token.Type) bool {
632
	switch t {
633
	case token.DATE, token.TILDE, token.EQ, token.BANG, token.AT,
634
		token.SEMICOLON, token.HASH, token.PERCENT, token.STAR,
635
		token.COMMENTKW, token.ACCOUNT, token.COMMODITY, token.INCLUDE,
636
		token.ALIAS, token.PAYEE, token.TAG, token.APPLY, token.END,
637
		token.YEAR, token.DECIMALMARK, token.D, token.P, token.N, token.C:
638
		return true
639
	}
640
	return false
641
}
642
643
// encodeSemTokens encodes tokens into LSP delta form. Input must be sorted by
644
// line and column; [rawToSemanticTokens] produces such order.
645
func encodeSemTokens(tokens []semanticToken) []uint32 {
646
	if len(tokens) == 0 {
647
		return nil
648
	}
649
	data := make([]uint32, 0, len(tokens)*5)
650
	var prevLine, prevCol uint32
651
	for _, t := range tokens {
652
		var deltaLine, deltaCol uint32
653
		if t.line == prevLine {
654
			deltaLine = 0
655
			deltaCol = t.col - prevCol
656
		} else {
657
			deltaLine = t.line - prevLine
658
			deltaCol = t.col
659
		}
660
		data = append(data, deltaLine, deltaCol, t.length, t.tokenType, t.modifiers)
661
		prevLine = t.line
662
		prevCol = t.col
663
	}
664
	return data
665
}
666
667
// betweenSpan returns the span of the text between two offsets, trimmed of surrounding whitespace.
668
func betweenSpan(content, file string, start, end int) (token.Span, bool) {
669
	for start < end && (content[start] == ' ' || content[start] == '\t') {
670
		start++
671
	}
672
	for end > start && (content[end-1] == ' ' || content[end-1] == '\t' || content[end-1] == '\n' || content[end-1] == '\r') {
673
		end--
674
	}
675
	if end <= start {
676
		return token.Span{}, false
677
	}
678
	return token.Span{Start: offsetPos(file, start), End: offsetPos(file, end)}, true
679
}
680
681
func offsetPos(file string, offset int) token.Pos { return token.Pos{File: file, Offset: offset} }
682
func offsetSpan(file string, start, end int) token.Span {
683
	return token.Span{Start: offsetPos(file, start), End: offsetPos(file, end)}
684
}