all repos

clerk @ e12aa47

missing tooling for ledger/hledger

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

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
ast: inline leaf value+span wrapper structs, 1 month ago
1
package lsp
2
3
import (
4
	"context"
5
	"slices"
6
	"strconv"
7
	"unicode/utf8"
8
9
	"go.lsp.dev/protocol"
10
	"go.lsp.dev/uri"
11
12
	"olexsmir.xyz/clerk/internal/lsp/lsputil"
13
	"olexsmir.xyz/clerk/journal/ast"
14
	"olexsmir.xyz/clerk/journal/lexer"
15
	"olexsmir.xyz/clerk/journal/token"
16
)
17
18
func (s *server) SemanticTokensFull(ctx context.Context, params *protocol.SemanticTokensParams) (*protocol.SemanticTokens, error) {
19
	if !s.semanticHighlightingEnabled() {
20
		return &protocol.SemanticTokens{}, nil
21
	}
22
	return s.semanticTokensFullResult(params.TextDocument.URI), nil
23
}
24
25
func (s *server) SemanticTokensFullDelta(ctx context.Context, params *protocol.SemanticTokensDeltaParams) (protocol.SemanticTokensDeltaResult, error) {
26
	if !s.semanticHighlightingEnabled() {
27
		return &protocol.SemanticTokens{}, nil
28
	}
29
30
	u := params.TextDocument.URI
31
	s.mu.RLock()
32
	st, ok := s.openDocs[u]
33
	s.mu.RUnlock()
34
	if !ok || st.sem == nil || st.sem.gen == 0 || params.PreviousResultID != st.sem.resultID() {
35
		return s.semanticTokensFullResult(u), nil
36
	}
37
38
	data, ok := s.semanticTokensData(u)
39
	if !ok {
40
		return &protocol.SemanticTokens{}, nil
41
	}
42
	edits := semanticTokensEdits(st.sem.baseline, data)
43
	if len(edits) == 0 {
44
		return &protocol.SemanticTokensDelta{ResultID: new(st.sem.resultID()), Edits: []protocol.SemanticTokensEdit{}}, nil
45
	}
46
47
	rid, ok := s.storeSemResult(u, data)
48
	if !ok {
49
		return &protocol.SemanticTokens{Data: data}, nil
50
	}
51
	return &protocol.SemanticTokensDelta{ResultID: &rid, Edits: edits}, nil
52
}
53
54
func (s *server) SemanticTokensRange(ctx context.Context, params *protocol.SemanticTokensRangeParams) (*protocol.SemanticTokens, error) {
55
	if !s.semanticHighlightingEnabled() {
56
		return &protocol.SemanticTokens{}, nil
57
	}
58
59
	tokens, ok := s.tokensForDoc(params.TextDocument.URI)
60
	if !ok {
61
		return &protocol.SemanticTokens{}, nil
62
	}
63
	start := int(params.Range.Start.Line)
64
	end := int(params.Range.End.Line)
65
	var filtered []semanticToken
66
	for _, t := range tokens {
67
		if t.line >= uint32(start) && t.line <= uint32(end) {
68
			filtered = append(filtered, t)
69
		}
70
	}
71
	return &protocol.SemanticTokens{Data: encodeSemTokens(filtered)}, nil
72
}
73
74
func (s *server) semanticTokensFullResult(u uri.URI) *protocol.SemanticTokens {
75
	data, ok := s.semanticTokensData(u)
76
	if !ok {
77
		return &protocol.SemanticTokens{}
78
	}
79
	res := &protocol.SemanticTokens{Data: data}
80
	if rid, ok := s.storeSemResult(u, data); ok {
81
		res.ResultID = &rid
82
	}
83
	return res
84
}
85
86
func (s *server) semanticTokensData(u uri.URI) ([]uint32, bool) {
87
	tokens, ok := s.tokensForDoc(u)
88
	if !ok {
89
		return nil, false
90
	}
91
	return encodeSemTokens(tokens), true
92
}
93
94
type semCache struct {
95
	entries  []semEntry // tokens per entry of the current text
96
	baseline []uint32   // encoded data of the last response; diff baseline for the next delta request
97
	gen      uint64     // increments per response; 0 before the first
98
	pending  *semEdit   // single edit to apply incrementally, nil when none
99
}
100
101
type semEdit struct {
102
	start, oldEnd, newEnd int
103
	deltaLine             int
104
}
105
106
func (c *semCache) resultID() string { return strconv.FormatUint(c.gen, 10) }
107
func (c *semCache) nextResultID() string {
108
	c.gen++
109
	return c.resultID()
110
}
111
112
func (s *server) storeSemResult(u uri.URI, data []uint32) (string, bool) {
113
	s.mu.Lock()
114
	defer s.mu.Unlock()
115
	st, ok := s.openDocs[u]
116
	if !ok {
117
		return "", false
118
	}
119
	if st.sem == nil {
120
		st.sem = &semCache{}
121
	}
122
	rid := st.sem.nextResultID()
123
	st.sem.baseline = data
124
	s.openDocs[u] = st
125
	return rid, true
126
}
127
128
func (s *server) tokensForDoc(doc uri.URI) ([]semanticToken, bool) {
129
	s.mu.RLock()
130
	st, ok := s.openDocs[doc]
131
	s.mu.RUnlock()
132
	if !ok {
133
		return nil, false
134
	}
135
	if st.sem != nil && st.sem.pending == nil {
136
		return flattenSemTokens(st.sem.entries), true
137
	}
138
	rj := s.loader.ResolveBytes(doc.Path(), []byte(st.text))
139
	j := rj.Occurrences[0].Ast
140
141
	var tokens []semanticToken
142
	var entries []semEntry
143
	good := st.sem != nil && st.sem.pending != nil
144
	if good {
145
		entries, good = incrementalTokens(st.text, j, st.sem.entries, *st.sem.pending)
146
	}
147
	if !good {
148
		tokens, entries = computeSemTokens(st.text, j)
149
	} else {
150
		tokens = flattenSemTokens(entries)
151
	}
152
	s.mu.Lock()
153
	defer s.mu.Unlock()
154
	cur, ok := s.openDocs[doc]
155
	if !ok {
156
		return nil, false
157
	}
158
	if cur.text == st.text { // unchanged during tokenization
159
		if cur.sem == nil {
160
			cur.sem = &semCache{}
161
		}
162
		cur.sem.entries = entries
163
		cur.sem.pending = nil
164
		s.openDocs[doc] = cur
165
	}
166
	return tokens, true
167
}
168
169
const (
170
	semDirective uint32 = iota
171
	semDate
172
	semAccount
173
	semCommodity
174
	semAmount
175
	semStatus
176
	semComment
177
	semString
178
	semOperator
179
	semProperty
180
)
181
182
var tokenTypeStrings = []string{
183
	string(protocol.SemanticTokenTypesKeyword),   // directive
184
	string(protocol.SemanticTokenTypesClass),     // date
185
	string(protocol.SemanticTokenTypesNamespace), // account
186
	string(protocol.SemanticTokenTypesType),      // commodity
187
	string(protocol.SemanticTokenTypesNumber),    // amount
188
	string(protocol.SemanticTokenTypesOperator),  // status
189
	string(protocol.SemanticTokenTypesComment),   // comment
190
	string(protocol.SemanticTokenTypesString),    // string
191
	string(protocol.SemanticTokenTypesOperator),  // operator
192
	string(protocol.SemanticTokenTypesProperty),  // property
193
}
194
195
const (
196
	modifierAbstract = 1 << 0 // virtual account
197
	modifierNegative = 1 << 1 // negative amount
198
)
199
200
var modifierStrings = []string{
201
	"abstract", // bit 0
202
	"negative", // bit 1
203
}
204
205
func getSemanticTokensLegend() protocol.SemanticTokensLegend {
206
	return protocol.SemanticTokensLegend{
207
		TokenTypes:     tokenTypeStrings,
208
		TokenModifiers: modifierStrings,
209
	}
210
}
211
212
type semanticToken struct {
213
	line, col uint32 // 0-based
214
	length    uint32
215
	tokenType uint32
216
	modifiers uint32
217
}
218
219
type semEntry struct {
220
	start, end int
221
	tokens     []semanticToken
222
}
223
224
func tokenizeForSemantics(content string, j *ast.Journal) []semanticToken {
225
	var raw []rawSpan
226
	emit := func(s token.Span, tokType, mods uint32) {
227
		if s.Start.Offset >= s.End.Offset {
228
			return
229
		}
230
		raw = append(raw, rawSpan{s, tokType, mods})
231
	}
232
	for _, e := range j.Entries {
233
		visitEntry(content, e, emit)
234
	}
235
	if len(j.Errors) > 0 {
236
		// parser recovers per line; lexer fills the unparsed regions, keeping ast tokens where the parser succeeded
237
		semLexerFallback(content, raw, emit)
238
	}
239
	return rawToSemanticTokens(content, raw)
240
}
241
242
// rawSpan is a source span tagged with semantic token
243
type rawSpan struct {
244
	span      token.Span
245
	tok, mods uint32
246
}
247
248
func computeSemTokens(content string, j *ast.Journal) ([]semanticToken, []semEntry) {
249
	if len(j.Errors) > 0 {
250
		return tokenizeForSemantics(content, j), nil
251
	}
252
	entries := buildSemTokens(content, j)
253
	return flattenSemTokens(entries), entries
254
}
255
256
func buildSemTokens(content string, j *ast.Journal) []semEntry {
257
	entries := make([]semEntry, 0, len(j.Entries))
258
	for _, e := range j.Entries {
259
		if _, isBlank := e.(*ast.BlankLine); isBlank {
260
			continue
261
		}
262
		sp := entrySpan(e)
263
		entries = append(entries, semEntry{start: sp.Start.Offset, end: sp.End.Offset, tokens: tokensForEntry(content, e)})
264
	}
265
	return entries
266
}
267
268
func flattenSemTokens(entries []semEntry) []semanticToken {
269
	var flat []semanticToken
270
	for _, en := range entries {
271
		flat = append(flat, en.tokens...)
272
	}
273
	return flat
274
}
275
276
func tokensForEntry(content string, e ast.Entry) []semanticToken {
277
	sp := entrySpan(e)
278
	var eraw []rawSpan
279
	visitEntry(content, e, func(tok token.Span, tokKind, modifier uint32) {
280
		if tok.Start.Offset >= tok.End.Offset {
281
			return
282
		}
283
		eraw = append(eraw, rawSpan{tok, tokKind, modifier})
284
	})
285
	if len(eraw) == 0 {
286
		return nil
287
	}
288
	return rawToSemanticTokensFrom(content, eraw, sp.Start.Line-1, 0, sp.Start.Offset)
289
}
290
291
func shiftLines(toks []semanticToken, delta int) []semanticToken {
292
	if delta == 0 {
293
		return toks
294
	}
295
	out := make([]semanticToken, len(toks))
296
	for i, t := range toks {
297
		t.line = uint32(int(t.line) + delta)
298
		out[i] = t
299
	}
300
	return out
301
}
302
303
func incrementalTokens(content string, j *ast.Journal, old []semEntry, e semEdit) ([]semEntry, bool) {
304
	if len(j.Errors) > 0 {
305
		return nil, false
306
	}
307
	var nb, na int
308
	for _, en := range j.Entries {
309
		if _, isBlank := en.(*ast.BlankLine); isBlank {
310
			continue
311
		}
312
		switch sp := entrySpan(en); {
313
		case sp.End.Offset <= e.start:
314
			nb++
315
		case sp.Start.Offset >= e.newEnd:
316
			na++
317
		}
318
	}
319
	var ob, oa int
320
	for _, en := range old {
321
		switch {
322
		case en.end <= e.start:
323
			ob++
324
		case en.start >= e.oldEnd:
325
			oa++
326
		}
327
	}
328
	if nb != ob || na != oa {
329
		return nil, false
330
	}
331
332
	out := make([]semEntry, 0, len(old)+1)
333
	iBefore, iTail := 0, len(old)-oa
334
	for _, en := range j.Entries {
335
		if _, isBlank := en.(*ast.BlankLine); isBlank {
336
			continue
337
		}
338
		sp := entrySpan(en)
339
		var toks []semanticToken
340
		switch {
341
		case sp.End.Offset <= e.start:
342
			toks = old[iBefore].tokens
343
			iBefore++
344
		case sp.Start.Offset >= e.newEnd:
345
			toks = shiftLines(old[iTail].tokens, e.deltaLine)
346
			iTail++
347
		default:
348
			toks = tokensForEntry(content, en)
349
		}
350
		out = append(out, semEntry{start: sp.Start.Offset, end: sp.End.Offset, tokens: toks})
351
	}
352
	return out, true
353
}
354
355
func rawToSemanticTokens(content string, raw []rawSpan) []semanticToken {
356
	return rawToSemanticTokensFrom(content, raw, 0, 0, 0)
357
}
358
359
func rawToSemanticTokensFrom(content string, raw []rawSpan, line, col, cursor int) []semanticToken {
360
	if len(raw) == 0 {
361
		return nil
362
	}
363
	slices.SortFunc(raw, func(a, b rawSpan) int { return a.span.Start.Offset - b.span.Start.Offset })
364
	out := make([]semanticToken, len(raw))
365
	advance := func(end int) {
366
		for cursor < end {
367
			r, size := utf8.DecodeRuneInString(content[cursor:])
368
			if r == utf8.RuneError && size <= 1 {
369
				break
370
			}
371
			if r == '\r' {
372
				cursor += size
373
				if cursor < len(content) && content[cursor] == '\n' {
374
					cursor++
375
				}
376
				line++
377
				col = 0
378
				continue
379
			}
380
			if r == '\n' {
381
				cursor += size
382
				line++
383
				col = 0
384
				continue
385
			}
386
			cursor += size
387
			col += utf16Units(r)
388
		}
389
	}
390
	for i, t := range raw {
391
		if cursor < t.span.Start.Offset {
392
			advance(t.span.Start.Offset)
393
		}
394
		out[i] = semanticToken{
395
			line:      uint32(line),
396
			col:       uint32(col),
397
			length:    uint32(lsputil.Utf16Len(content, t.span.Start.Offset, t.span.End.Offset)),
398
			tokenType: t.tok,
399
			modifiers: t.mods,
400
		}
401
		advance(t.span.End.Offset)
402
	}
403
	return out
404
}
405
406
func utf16Units(r rune) int {
407
	if r >= 0x10000 && r <= 0x10FFFF {
408
		return 2
409
	}
410
	return 1
411
}
412
413
func visitEntry(content string, e ast.Entry, emit semEmitFunc) {
414
	switch e := e.(type) {
415
	case *ast.Transaction:
416
		visitTransaction(content, e, emit)
417
	case *ast.PeriodicTransaction:
418
		visitPeriodicTransaction(content, e, emit)
419
	case *ast.AutomatedTransaction:
420
		visitAutomatedTransaction(content, e, emit)
421
	case *ast.AccountDirective:
422
		emit(directiveKeyword(e.Span, "account"), semDirective, 0)
423
		emit(e.Account.Span, semAccount, 0)
424
		for _, sd := range e.Subdirectives {
425
			if sd.Kind == ast.SubdirectiveComment {
426
				emitComment(sd.Comment, emit)
427
				continue
428
			}
429
			emit(sd.NameSpan, semDirective, 0)
430
			switch sd.Kind {
431
			case ast.SubdirectiveAlias:
432
				emit(sd.ValueSpan, semAccount, 0)
433
			case ast.SubdirectiveType, ast.SubdirectiveNote:
434
				emit(sd.ValueSpan, semProperty, 0)
435
			}
436
			emitComment(sd.Comment, emit)
437
		}
438
		emitComment(e.Comment, emit)
439
	case *ast.CommodityDirective:
440
		emit(directiveKeyword(e.Span, "commodity"), semDirective, 0)
441
		if e.FormatSub != nil {
442
			if e.FormatSub.KeywordSpan.End.Offset > 0 {
443
				emit(e.FormatSub.KeywordSpan, semDirective, 0)
444
			}
445
			semEmitAmount(content, &e.FormatSub.Amount, emit)
446
			emitComment(e.FormatSub.Comment, emit)
447
		} else if e.CommoditySpan.Start.Offset > 0 && e.CommoditySpan.End.Offset > 0 {
448
			emit(e.CommoditySpan, semCommodity, 0)
449
		}
450
		emitBlockComments(e.BlockComments, emit)
451
		emitComment(e.Comment, emit)
452
	case *ast.IncludeDirective:
453
		emitDirective(content, e.Span, len("include"), semString, e.Comment, emit)
454
	case *ast.PayeeDirective:
455
		emitDirective(content, e.Span, len("payee"), semProperty, e.Comment, emit)
456
	case *ast.TagDirective:
457
		emitDirective(content, e.Span, len("tag"), semProperty, e.Comment, emit)
458
	case *ast.AliasDirective:
459
		emit(directiveKeyword(e.Span, "alias"), semDirective, 0)
460
		emit(e.From.Span, semAccount, 0)
461
		if op, ok := betweenSpan(content, e.Span.File, e.From.Span.End.Offset, e.To.Span.Start.Offset); ok {
462
			emit(op, semOperator, 0)
463
		}
464
		emit(e.To.Span, semAccount, 0)
465
		emitComment(e.Comment, emit)
466
	case *ast.YearDirective:
467
		kwLen := len("year")
468
		if content[e.Span.Start.Offset] == 'Y' {
469
			kwLen = 1
470
		}
471
		emitDirective(content, e.Span, kwLen, semProperty, e.Comment, emit)
472
	case *ast.DecimalMarkDirective:
473
		emitDirective(content, e.Span, len("decimal-mark"), semProperty, e.Comment, emit)
474
	case *ast.DefaultCommodityDirective:
475
		emit(directiveKeyword(e.Span, "D"), semDirective, 0)
476
		semEmitAmount(content, &e.Amount, emit)
477
		emitComment(e.Comment, emit)
478
	case *ast.MarketPriceDirective:
479
		emit(directiveKeyword(e.Span, "P"), semDirective, 0)
480
		emit(e.DateTime.Date.Span, semDate, 0)
481
		if e.DateTime.Time != nil {
482
			emit(e.DateTime.Time.Span, semDate, 0)
483
		}
484
		// commodity: text between the date (or time) and the amount
485
		commStart := e.DateTime.Date.Span.End.Offset
486
		if e.DateTime.Time != nil {
487
			commStart = e.DateTime.Time.Span.End.Offset
488
		}
489
		if comm, ok := betweenSpan(content, e.Span.File, commStart, e.Amount.Span.Start.Offset); ok {
490
			emit(comm, semCommodity, 0)
491
		}
492
		semEmitAmount(content, &e.Amount, emit)
493
		emitComment(e.Comment, emit)
494
	case *ast.ConversionDirective:
495
		emit(directiveKeyword(e.Span, "C"), semDirective, 0)
496
		semEmitAmount(content, &e.From, emit)
497
		// = operator: text between the two amounts
498
		if op, ok := betweenSpan(content, e.Span.File, e.From.Span.End.Offset, e.To.Span.Start.Offset); ok {
499
			emit(op, semOperator, 0)
500
		}
501
		semEmitAmount(content, &e.To, emit)
502
		emitComment(e.Comment, emit)
503
	case *ast.Comment:
504
		emitComment(e, emit)
505
	case *ast.CommentBlockDirective:
506
		emit(e.Span, semComment, 0)
507
	case *ast.IgnoredDirective:
508
		emitDirective(content, e.Span, len("N"), semProperty, e.Comment, emit)
509
	case *ast.ApplyDirective:
510
		emitDirective(content, e.Span, len("apply"), semProperty, e.Comment, emit)
511
	case *ast.EndDirective:
512
		emitDirective(content, e.Span, len("end"), semProperty, e.Comment, emit)
513
	case *ast.BlankLine:
514
	}
515
}
516
517
func visitTransaction(content string, t *ast.Transaction, emit semEmitFunc) {
518
	emit(t.Date.Span, semDate, 0)
519
	if t.SecondDate != nil {
520
		emit(t.SecondDate.Span, semDate, 0)
521
	}
522
	if t.Status != ast.StatusNone {
523
		emit(t.StatusSpan, semStatus, 0)
524
	}
525
	if t.Code != "" {
526
		emit(t.CodeSpan, semString, 0)
527
	}
528
	if t.Payee != "" {
529
		emit(t.PayeeSpan, semProperty, 0)
530
	}
531
	if t.Note != "" {
532
		emit(t.NoteSpan, semProperty, 0)
533
	}
534
	emitComment(t.Comment, emit)
535
	for i := range t.HeaderComments {
536
		emitComment(t.HeaderComments[i], emit)
537
	}
538
	for _, p := range t.Postings {
539
		visitPosting(content, p, emit)
540
	}
541
}
542
543
func visitPeriodicTransaction(content string, pt *ast.PeriodicTransaction, emit semEmitFunc) {
544
	// ~ operator is at the start of the period span
545
	emit(offsetSpan(pt.Span.File, pt.Span.Start.Offset, pt.Span.Start.Offset+1), semOperator, 0)
546
547
	// The period span covers the whole expr, including any "from ... to ..." dates
548
	if pt.Period.Span.End.Offset > pt.Period.Span.Start.Offset {
549
		var dates []*ast.Date
550
		if pt.Period.From != nil {
551
			dates = append(dates, pt.Period.From)
552
		}
553
		if pt.Period.To != nil {
554
			dates = append(dates, pt.Period.To)
555
		}
556
		pos := pt.Period.Span.Start.Offset
557
		for _, d := range dates {
558
			if d.Span.Start.Offset > pos {
559
				emit(offsetSpan(pt.Period.Span.File, pos, d.Span.Start.Offset), semProperty, 0)
560
			}
561
			emit(d.Span, semDate, 0)
562
			pos = d.Span.End.Offset
563
		}
564
		if pos < pt.Period.Span.End.Offset {
565
			emit(offsetSpan(pt.Period.Span.File, pos, pt.Period.Span.End.Offset), semProperty, 0)
566
		}
567
	}
568
	if pt.Description != "" {
569
		emit(pt.DescriptionSpan, semProperty, 0)
570
	}
571
	emitComment(pt.Comment, emit)
572
	for i := range pt.HeaderComments {
573
		emitComment(pt.HeaderComments[i], emit)
574
	}
575
	for _, p := range pt.Postings {
576
		visitPosting(content, p, emit)
577
	}
578
}
579
580
func visitAutomatedTransaction(content string, at *ast.AutomatedTransaction, emit semEmitFunc) {
581
	// = operator is at the start of the expression span
582
	emit(offsetSpan(at.Span.File, at.Span.Start.Offset, at.Span.Start.Offset+1), semOperator, 0)
583
584
	if at.Expr != "" {
585
		emit(at.ExprSpan, semString, 0)
586
	}
587
	emitComment(at.Comment, emit)
588
	for i := range at.HeaderComments {
589
		emitComment(at.HeaderComments[i], emit)
590
	}
591
	for _, p := range at.Postings {
592
		visitPosting(content, p, emit)
593
	}
594
}
595
596
func visitPosting(content string, p ast.Posting, emit semEmitFunc) {
597
	if p.Status != ast.StatusNone {
598
		emit(p.StatusSpan, semStatus, 0)
599
	}
600
601
	// virtual brackets
602
	if p.Type == ast.PostingVirtualUnbalanced || p.Type == ast.PostingVirtualBalanced {
603
		// opening bracket
604
		for off := p.Span.Start.Offset; off < p.Account.Span.Start.Offset && off < p.Span.End.Offset; off++ {
605
			if content[off] == '(' || content[off] == '[' {
606
				emit(offsetSpan(p.Span.File, off, off+1), semOperator, modifierAbstract)
607
				break
608
			}
609
		}
610
		// closing bracket
611
		for off := p.Account.Span.End.Offset; off < p.Span.End.Offset; off++ {
612
			if content[off] == ')' || content[off] == ']' {
613
				emit(offsetSpan(p.Span.File, off, off+1), semOperator, modifierAbstract)
614
				break
615
			}
616
		}
617
	}
618
619
	emit(p.Account.Span, semAccount, 0)
620
621
	if p.Amount != nil {
622
		semEmitAmount(content, p.Amount, emit)
623
	}
624
	if p.Cost != nil {
625
		semEmitCost(content, p.Cost, emit)
626
	}
627
	if p.Balance != nil {
628
		semEmitBalanceAssertion(content, p.Balance, emit)
629
	}
630
	emitComment(p.Comment, emit)
631
	for i := range p.Comments {
632
		emitComment(&p.Comments[i], emit)
633
	}
634
}
635
636
// directiveKeyword returns the span of the leading keyword on a directive line.
637
func directiveKeyword(e token.Span, kw string) token.Span {
638
	return token.Span{File: e.File, Start: e.Start, End: token.Pos{Offset: e.Start.Offset + len(kw)}}
639
}
640
641
// directiveValue returns the trimmed span of the text after the keyword end
642
// offset, up to the inline comment or the end of the line.
643
func directiveValue(content string, e token.Span, comment *ast.Comment, kwEnd int) (token.Span, bool) {
644
	end := e.End.Offset
645
	if comment != nil {
646
		end = comment.Span.Start.Offset
647
	}
648
	return betweenSpan(content, e.File, kwEnd, end)
649
}
650
651
func semEmitAmount(content string, a *ast.Amount, emit semEmitFunc) {
652
	if a == nil {
653
		return
654
	}
655
	hasCommodity := a.CommoditySpan.Start.Offset > 0 && a.CommoditySpan.End.Offset > 0
656
	if hasCommodity && a.CommodityPos == ast.CommodityBefore {
657
		emit(a.CommoditySpan, semCommodity, 0)
658
		semEmitQuantity(content, a, emit)
659
		return
660
	}
661
	semEmitQuantity(content, a, emit)
662
	if hasCommodity {
663
		emit(a.CommoditySpan, semCommodity, 0)
664
	}
665
}
666
667
func semEmitQuantity(content string, a *ast.Amount, emit semEmitFunc) {
668
	qStart, qEnd := quantitySpan(content, a)
669
	if qEnd <= qStart {
670
		return
671
	}
672
	mods := uint32(0)
673
	if a.IsNegative {
674
		mods |= modifierNegative
675
	}
676
	emit(offsetSpan(a.Span.File, qStart, qEnd), semAmount, mods)
677
}
678
679
func quantitySpan(content string, a *ast.Amount) (int, int) {
680
	start, end := a.Span.Start.Offset, a.Span.End.Offset
681
	switch {
682
	case a.Commodity == "":
683
		// bare quantity
684
	case a.CommodityPos == ast.CommodityBefore:
685
		// "$50.00" or "$   50.00": quantity follows the commodity span
686
		start = a.CommoditySpan.End.Offset
687
	default: // CommodityAfter
688
		// "50.00 USD" or "50.00     USD": quantity precedes the commodity
689
		end = a.CommoditySpan.Start.Offset
690
	}
691
	for start < end && (content[start] == ' ' || content[start] == '\t') {
692
		start++
693
	}
694
	for end > start && (content[end-1] == ' ' || content[end-1] == '\t') {
695
		end--
696
	}
697
	return start, end
698
}
699
700
type semEmitFunc func(tok token.Span, tokKind, modifier uint32)
701
702
func emitBlockComments(cs []*ast.Comment, emit semEmitFunc) {
703
	for _, c := range cs {
704
		emitComment(c, emit)
705
	}
706
}
707
708
func emitComment(c *ast.Comment, emit semEmitFunc) {
709
	if c == nil {
710
		return
711
	}
712
	if len(c.Tags) == 0 {
713
		emit(c.Span, semComment, 0)
714
		return
715
	}
716
	pos := c.Span.Start.Offset
717
	for _, t := range c.Tags {
718
		if t.Span.Start.Offset > pos {
719
			emit(offsetSpan(c.Span.File, pos, t.Span.Start.Offset), semComment, 0)
720
		}
721
		emit(t.Span, semProperty, 0)
722
		pos = t.Span.End.Offset
723
	}
724
	if pos < c.Span.End.Offset {
725
		emit(offsetSpan(c.Span.File, pos, c.Span.End.Offset), semComment, 0)
726
	}
727
}
728
729
func emitDirective(content string, e token.Span, kwLen int, valType uint32, comment *ast.Comment, emit semEmitFunc) {
730
	kwEnd := e.Start.Offset + kwLen
731
	emit(token.Span{File: e.File, Start: e.Start, End: token.Pos{Offset: kwEnd}}, semDirective, 0)
732
	if v, ok := directiveValue(content, e, comment, kwEnd); ok {
733
		emit(v, valType, 0)
734
	}
735
	emitComment(comment, emit)
736
}
737
738
func semEmitCost(content string, c *ast.Cost, emit semEmitFunc) {
739
	if c.IsTotal {
740
		emit(token.Span{File: c.Span.File, Start: c.Span.Start, End: token.Pos{Offset: c.Span.Start.Offset + 2}}, semOperator, 0)
741
	} else {
742
		emit(token.Span{File: c.Span.File, Start: c.Span.Start, End: token.Pos{Offset: c.Span.Start.Offset + 1}}, semOperator, 0)
743
	}
744
	semEmitAmount(content, &c.Amount, emit)
745
}
746
747
func semEmitBalanceAssertion(content string, ba *ast.BalanceAssertion, emit semEmitFunc) {
748
	// The operator is the run of '=', ':', '*' chars from the span start.
749
	// The ':' of ':=' precedes the '=' token, so back up one offset.
750
	opStart := ba.Span.Start.Offset
751
	if ba.IsAssignment && opStart > 0 {
752
		opStart--
753
	}
754
	opEnd := opStart
755
	for opEnd < ba.Span.End.Offset && (content[opEnd] == '=' || content[opEnd] == ':' || content[opEnd] == '*') {
756
		opEnd++
757
	}
758
	emit(token.Span{File: ba.Span.File, Start: token.Pos{Offset: opStart}, End: token.Pos{Offset: opEnd}}, semOperator, 0)
759
	semEmitAmount(content, &ba.Amount, emit)
760
	if ba.Cost != nil {
761
		semEmitCost(content, ba.Cost, emit)
762
	}
763
}
764
765
func semLexerFallback(content string, base []rawSpan, emit semEmitFunc) {
766
	l := lexer.New("", []byte(content))
767
768
	var commentStart, commentEnd int // 0 = not inside a comment line
769
	lineStart := true                // the next significant token starts a line
770
	skipLine := false                // the line starts with an unclassifiable token; emit nothing
771
	i := 0                           // next base span to compare against
772
773
	take := func(span token.Span, tokType uint32, mods uint32) {
774
		for i < len(base) && base[i].span.End.Offset <= span.Start.Offset {
775
			i++
776
		}
777
		if i < len(base) && base[i].span.Start.Offset < span.End.Offset {
778
			return // overlaps an AST token; AST wins
779
		}
780
		emit(span, tokType, mods)
781
	}
782
783
	for {
784
		tok := l.Next()
785
		if tok.Type == token.EOF {
786
			if commentStart > 0 {
787
				take(token.Span{Start: token.Pos{Offset: commentStart}, End: token.Pos{Offset: commentEnd}}, semComment, 0)
788
			}
789
			break
790
		}
791
		if tok.Type == token.NEWLINE {
792
			if commentStart > 0 {
793
				take(token.Span{Start: token.Pos{Offset: commentStart}, End: token.Pos{Offset: commentEnd}}, semComment, 0)
794
				commentStart, commentEnd = 0, 0
795
			}
796
			lineStart, skipLine = true, false
797
			continue
798
		}
799
		if tok.Type == token.WHITESPACE || tok.Type == token.INDENT {
800
			continue
801
		}
802
		if lineStart {
803
			lineStart = false
804
			if !isLineStartToken(tok.Type) {
805
				skipLine = true
806
			}
807
		}
808
		if skipLine {
809
			continue
810
		}
811
		tokType := semProperty
812
		if commentStart > 0 {
813
			tokType = semComment
814
			if tok.Span.End.Offset > commentEnd {
815
				commentEnd = tok.Span.End.Offset
816
			}
817
			continue
818
		}
819
820
		switch tok.Type {
821
		case token.SEMICOLON, token.HASH, token.PERCENT, token.STAR:
822
			tokType = semComment
823
			commentStart = tok.Span.Start.Offset
824
			commentEnd = tok.Span.End.Offset
825
			continue
826
		case token.STRING:
827
			tokType = semString
828
		case token.DATE:
829
			tokType = semDate
830
		case token.INT, token.DECIMAL:
831
			tokType = semAmount
832
		case token.COMMODITYMARK:
833
			tokType = semCommodity
834
		case token.BANG:
835
			tokType = semStatus
836
		case token.AT, token.ATAT, token.EQ, token.EQEQ, token.EQEQEQ, token.EQSTAR:
837
			tokType = semOperator
838
		case token.COMMENTKW, token.ACCOUNT, token.COMMODITY, token.INCLUDE,
839
			token.ALIAS, token.PAYEE, token.TAG, token.APPLY, token.END,
840
			token.YEAR, token.DECIMALMARK, token.D, token.P, token.N, token.C:
841
			tokType = semDirective
842
		}
843
		take(tok.Span, tokType, 0)
844
	}
845
}
846
847
func isLineStartToken(t token.Type) bool {
848
	switch t {
849
	case token.DATE, token.TILDE, token.EQ, token.BANG, token.AT,
850
		token.SEMICOLON, token.HASH, token.PERCENT, token.STAR,
851
		token.COMMENTKW, token.ACCOUNT, token.COMMODITY, token.INCLUDE,
852
		token.ALIAS, token.PAYEE, token.TAG, token.APPLY, token.END,
853
		token.YEAR, token.DECIMALMARK, token.D, token.P, token.N, token.C:
854
		return true
855
	}
856
	return false
857
}
858
859
// semanticTokensEdits returns the single edit turning old into new, or nil when
860
// identical. Relative delta encoding keeps the common prefix and suffix unchanged.
861
func semanticTokensEdits(old, new []uint32) []protocol.SemanticTokensEdit {
862
	p := 0
863
	for p < len(old) && p < len(new) && old[p] == new[p] {
864
		p++
865
	}
866
	s := 0
867
	for s < len(old)-p && s < len(new)-p && old[len(old)-1-s] == new[len(new)-1-s] {
868
		s++
869
	}
870
	delCount := len(old) - p - s
871
	ins := new[p : len(new)-s]
872
	if delCount == 0 && len(ins) == 0 {
873
		return nil
874
	}
875
	return []protocol.SemanticTokensEdit{{
876
		Start:       uint32(p),
877
		DeleteCount: uint32(delCount),
878
		Data:        ins,
879
	}}
880
}
881
882
// encodeSemTokens encodes tokens into LSP delta form. Input must be sorted by
883
// line and column; [rawToSemanticTokens] produces such order.
884
func encodeSemTokens(tokens []semanticToken) []uint32 {
885
	if len(tokens) == 0 {
886
		return nil
887
	}
888
	data := make([]uint32, 0, len(tokens)*5)
889
	var prevLine, prevCol uint32
890
	for _, t := range tokens {
891
		var deltaLine, deltaCol uint32
892
		if t.line == prevLine {
893
			deltaLine = 0
894
			deltaCol = t.col - prevCol
895
		} else {
896
			deltaLine = t.line - prevLine
897
			deltaCol = t.col
898
		}
899
		data = append(data, deltaLine, deltaCol, t.length, t.tokenType, t.modifiers)
900
		prevLine = t.line
901
		prevCol = t.col
902
	}
903
	return data
904
}
905
906
// betweenSpan returns the span of the text between two offsets, trimmed of surrounding whitespace.
907
func betweenSpan(content, file string, start, end int) (token.Span, bool) {
908
	for start < end && (content[start] == ' ' || content[start] == '\t') {
909
		start++
910
	}
911
	for end > start && (content[end-1] == ' ' || content[end-1] == '\t' || content[end-1] == '\n' || content[end-1] == '\r') {
912
		end--
913
	}
914
	if end <= start {
915
		return token.Span{}, false
916
	}
917
	return offsetSpan(file, start, end), true
918
}
919
920
func offsetSpan(file string, start, end int) token.Span {
921
	return token.Span{File: file, Start: token.Pos{Offset: start}, End: token.Pos{Offset: end}}
922
}