all repos

clerk @ d7328be77cefeea38a3d496d593d4624992542d1

missing tooling for ledger/hledger

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

Oleksandr Smirnov Oleksandr Smirnov
olexsmir@gmail.com
lsp: completion, 2 months 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
	SemanticDirective = iota
68
	SemanticDate
69
	SemanticAccount
70
	SemanticCommodity
71
	SemanticAmount
72
	SemanticStatus
73
	SemanticComment
74
	SemString
75
	SemOperator
76
	SemProperty
77
78
	semTypeCount // used to ensure we dont go out of bound
79
)
80
81
var tokenTypeStrings = []string{
82
	string(protocol.SemanticTokenTypesKeyword),   // directive
83
	string(protocol.SemanticTokenTypesClass),     // date
84
	string(protocol.SemanticTokenTypesNamespace), // account
85
	string(protocol.SemanticTokenTypesType),      // commodity
86
	string(protocol.SemanticTokenTypesNumber),    // amount
87
	string(protocol.SemanticTokenTypesOperator),  // status
88
	string(protocol.SemanticTokenTypesComment),   // comment
89
	string(protocol.SemanticTokenTypesString),    // string
90
	string(protocol.SemanticTokenTypesOperator),  // operator
91
	string(protocol.SemanticTokenTypesProperty),  // property
92
}
93
94
const (
95
	modifierAbstract = 1 << 0 // virtual account
96
	modifierNegative = 1 << 1 // negative amount
97
)
98
99
var modifierStrings = []string{
100
	"abstract", // bit 0
101
	"negative", // bit 1
102
}
103
104
func getSemanticTokensLegend() protocol.SemanticTokensLegend {
105
	return protocol.SemanticTokensLegend{
106
		TokenTypes:     tokenTypeStrings,
107
		TokenModifiers: modifierStrings,
108
	}
109
}
110
111
type semanticToken struct {
112
	line      uint32 // 0-based
113
	col       uint32 // 0-based UTF-16
114
	length    uint32 // UTF-16 code units
115
	tokenType uint32
116
	modifiers uint32
117
}
118
119
func tokenizeForSemantics(content string, j *ast.Journal) []semanticToken {
120
	var raw []rawSpan
121
	emit := func(s token.Span, tokType, mods uint32) {
122
		if s.Start.Offset >= s.End.Offset {
123
			return
124
		}
125
		raw = append(raw, rawSpan{s, tokType, mods})
126
	}
127
	if j == nil || len(j.Errors) > 0 {
128
		semLexerFallback(content, emit)
129
	} else {
130
		for _, e := range j.Entries {
131
			visitEntry(content, e, emit)
132
		}
133
	}
134
	return rawToSemanticTokens(content, raw)
135
}
136
137
// rawSpan is a source span tagged with semantic token
138
type rawSpan struct {
139
	span      token.Span
140
	tok, mods uint32
141
}
142
143
func rawToSemanticTokens(content string, raw []rawSpan) []semanticToken {
144
	if len(raw) == 0 {
145
		return nil
146
	}
147
	slices.SortFunc(raw, func(a, b rawSpan) int { return a.span.Start.Offset - b.span.Start.Offset })
148
	out := make([]semanticToken, len(raw))
149
	line, col, cursor := 0, 0, 0
150
	advance := func(end int) {
151
		for cursor < end {
152
			r, size := utf8.DecodeRuneInString(content[cursor:])
153
			if r == utf8.RuneError && size <= 1 {
154
				break
155
			}
156
			if r == '\r' {
157
				cursor += size
158
				if cursor < len(content) && content[cursor] == '\n' {
159
					cursor++
160
				}
161
				line++
162
				col = 0
163
				continue
164
			}
165
			if r == '\n' {
166
				cursor += size
167
				line++
168
				col = 0
169
				continue
170
			}
171
			cursor += size
172
			col += utf16Units(r)
173
		}
174
	}
175
	for i, t := range raw {
176
		if cursor < t.span.Start.Offset {
177
			advance(t.span.Start.Offset)
178
		}
179
		out[i] = semanticToken{
180
			line:      uint32(line),
181
			col:       uint32(col),
182
			length:    uint32(lsputil.Utf16Len(content, t.span.Start.Offset, t.span.End.Offset)),
183
			tokenType: t.tok,
184
			modifiers: t.mods,
185
		}
186
		advance(t.span.End.Offset)
187
	}
188
	return out
189
}
190
191
func utf16Units(r rune) int {
192
	if r >= 0x10000 && r <= 0x10FFFF {
193
		return 2
194
	}
195
	return 1
196
}
197
198
func visitEntry(content string, e ast.Entry, emit semEmitFn) {
199
	switch e := e.(type) {
200
	case *ast.Transaction:
201
		visitTransaction(content, e, emit)
202
	case *ast.PeriodicTransaction:
203
		visitPeriodicTransaction(content, e, emit)
204
	case *ast.AutomatedTransaction:
205
		visitAutomatedTransaction(content, e, emit)
206
	case *ast.AccountDirective:
207
		emit(directiveKeyword(e.Span, "account"), SemanticDirective, 0)
208
		emit(e.Account.Span, SemanticAccount, 0)
209
		if e.Comment != nil {
210
			emit(e.Comment.Span, SemanticComment, 0)
211
		}
212
	case *ast.CommodityDirective:
213
		emit(directiveKeyword(e.Span, "commodity"), SemanticDirective, 0)
214
		if e.Format.Span.End.Offset > 0 {
215
			semEmitAmount(content, &e.Format, emit)
216
		} else if e.CommoditySpan.Start.Offset > 0 && e.CommoditySpan.End.Offset > 0 {
217
			emit(e.CommoditySpan, SemanticCommodity, 0)
218
		}
219
		if e.Comment != nil {
220
			emit(e.Comment.Span, SemanticComment, 0)
221
		}
222
	case *ast.IncludeDirective:
223
		emitDirective(content, e.Span, len("include"), SemString, e.Comment, emit)
224
	case *ast.PayeeDirective:
225
		emitDirective(content, e.Span, len("payee"), SemProperty, e.Comment, emit)
226
	case *ast.TagDirective:
227
		emitDirective(content, e.Span, len("tag"), SemProperty, e.Comment, emit)
228
	case *ast.AliasDirective:
229
		emit(directiveKeyword(e.Span, "alias"), SemanticDirective, 0)
230
		emit(e.From.Span, SemanticAccount, 0)
231
		if op, ok := betweenSpan(content, e.Span.Start.File, e.From.Span.End.Offset, e.To.Span.Start.Offset); ok {
232
			emit(op, SemOperator, 0)
233
		}
234
		emit(e.To.Span, SemanticAccount, 0)
235
		if e.Comment != nil {
236
			emit(e.Comment.Span, SemanticComment, 0)
237
		}
238
	case *ast.YearDirective:
239
		kwLen := len("year")
240
		if content[e.Span.Start.Offset] == 'Y' {
241
			kwLen = 1
242
		}
243
		emitDirective(content, e.Span, kwLen, SemProperty, e.Comment, emit)
244
	case *ast.DecimalMarkDirective:
245
		emitDirective(content, e.Span, len("decimal-mark"), SemProperty, e.Comment, emit)
246
	case *ast.DefaultCommodityDirective:
247
		emit(directiveKeyword(e.Span, "D"), SemanticDirective, 0)
248
		semEmitAmount(content, &e.Amount, emit)
249
		if e.Comment != nil {
250
			emit(e.Comment.Span, SemanticComment, 0)
251
		}
252
	case *ast.MarketPriceDirective:
253
		emit(directiveKeyword(e.Span, "P"), SemanticDirective, 0)
254
		emit(e.DateTime.Date.Span, SemanticDate, 0)
255
		if e.DateTime.Time != nil {
256
			emit(e.DateTime.Time.Span, SemanticDate, 0)
257
		}
258
		// commodity: text between the date (or time) and the amount
259
		commStart := e.DateTime.Date.Span.End.Offset
260
		if e.DateTime.Time != nil {
261
			commStart = e.DateTime.Time.Span.End.Offset
262
		}
263
		if comm, ok := betweenSpan(content, e.Span.Start.File, commStart, e.Amount.Span.Start.Offset); ok {
264
			emit(comm, SemanticCommodity, 0)
265
		}
266
		semEmitAmount(content, &e.Amount, emit)
267
		if e.Comment != nil {
268
			emit(e.Comment.Span, SemanticComment, 0)
269
		}
270
	case *ast.ConversionDirective:
271
		emit(directiveKeyword(e.Span, "C"), SemanticDirective, 0)
272
		semEmitAmount(content, &e.From, emit)
273
		// = operator: text between the two amounts
274
		if op, ok := betweenSpan(content, e.Span.Start.File, e.From.Span.End.Offset, e.To.Span.Start.Offset); ok {
275
			emit(op, SemOperator, 0)
276
		}
277
		semEmitAmount(content, &e.To, emit)
278
		if e.Comment != nil {
279
			emit(e.Comment.Span, SemanticComment, 0)
280
		}
281
	case *ast.Comment:
282
		emit(e.Span, SemanticComment, 0)
283
	case *ast.CommentBlockDirective:
284
		emit(e.Span, SemanticComment, 0)
285
	case *ast.IgnoredDirective:
286
		emitDirective(content, e.Span, len("N"), SemProperty, e.Comment, emit)
287
	case *ast.ApplyDirective:
288
		emitDirective(content, e.Span, len("apply"), SemProperty, e.Comment, emit)
289
	case *ast.EndDirective:
290
		emitDirective(content, e.Span, len("end"), SemProperty, e.Comment, emit)
291
	case *ast.BlankLine:
292
	}
293
}
294
295
func visitTransaction(content string, t *ast.Transaction, emit semEmitFn) {
296
	emit(t.Date.Span, SemanticDate, 0)
297
	if t.SecondDate != nil {
298
		emit(t.SecondDate.Span, SemanticDate, 0)
299
	}
300
	if t.Status.Value != ast.StatusNone {
301
		emit(t.Status.Span, SemanticStatus, 0)
302
	}
303
	if t.Code != nil {
304
		emit(t.Code.Span, SemString, 0)
305
	}
306
	if t.Payee != nil {
307
		emit(t.Payee.Span, SemProperty, 0)
308
	}
309
	if t.Note != nil {
310
		emit(t.Note.Span, SemProperty, 0)
311
	}
312
	if t.Comment != nil {
313
		emit(t.Comment.Span, SemanticComment, 0)
314
	}
315
	for i := range t.HeaderComments {
316
		emit(t.HeaderComments[i].Span, SemanticComment, 0)
317
	}
318
	for _, p := range t.Postings {
319
		visitPosting(content, p, emit)
320
	}
321
}
322
323
func visitPeriodicTransaction(content string, pt *ast.PeriodicTransaction, emit semEmitFn) {
324
	// ~ operator is at the start of the period span
325
	emit(offsetSpan(pt.Span.Start.File, pt.Span.Start.Offset, pt.Span.Start.Offset+1), SemOperator, 0)
326
327
	// The period span covers the whole expr, including any "from ... to ..." dates
328
	if pt.Period.Span.End.Offset > pt.Period.Span.Start.Offset {
329
		var dates []*ast.Date
330
		if pt.Period.From != nil {
331
			dates = append(dates, pt.Period.From)
332
		}
333
		if pt.Period.To != nil {
334
			dates = append(dates, pt.Period.To)
335
		}
336
		pos := pt.Period.Span.Start.Offset
337
		for _, d := range dates {
338
			if d.Span.Start.Offset > pos {
339
				emit(offsetSpan(pt.Period.Span.Start.File, pos, d.Span.Start.Offset), SemProperty, 0)
340
			}
341
			emit(d.Span, SemanticDate, 0)
342
			pos = d.Span.End.Offset
343
		}
344
		if pos < pt.Period.Span.End.Offset {
345
			emit(offsetSpan(pt.Period.Span.Start.File, pos, pt.Period.Span.End.Offset), SemProperty, 0)
346
		}
347
	}
348
	if pt.Description != nil {
349
		emit(pt.Description.Span, SemProperty, 0)
350
	}
351
	if pt.Comment != nil {
352
		emit(pt.Comment.Span, SemanticComment, 0)
353
	}
354
	for i := range pt.HeaderComments {
355
		emit(pt.HeaderComments[i].Span, SemanticComment, 0)
356
	}
357
	for _, p := range pt.Postings {
358
		visitPosting(content, p, emit)
359
	}
360
}
361
362
func visitAutomatedTransaction(content string, at *ast.AutomatedTransaction, emit semEmitFn) {
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
	if at.Comment != nil {
370
		emit(at.Comment.Span, SemanticComment, 0)
371
	}
372
	for i := range at.HeaderComments {
373
		emit(at.HeaderComments[i].Span, SemanticComment, 0)
374
	}
375
	for _, p := range at.Postings {
376
		visitPosting(content, p, emit)
377
	}
378
}
379
380
func visitPosting(content string, p *ast.Posting, emit semEmitFn) {
381
	if p.Status.Value != ast.StatusNone {
382
		emit(p.Status.Span, SemanticStatus, 0)
383
	}
384
385
	// virtual brackets
386
	if p.Type == ast.PostingVirtualUnbalanced || p.Type == ast.PostingVirtualBalanced {
387
		// opening bracket
388
		for off := p.Span.Start.Offset; off < p.Account.Span.Start.Offset && off < p.Span.End.Offset; off++ {
389
			if content[off] == '(' || content[off] == '[' {
390
				brSpan := token.Span{Start: offsetPos(p.Span.Start.File, off), End: offsetPos(p.Span.Start.File, off+1)}
391
				emit(brSpan, SemOperator, modifierAbstract)
392
				break
393
			}
394
		}
395
		// closing bracket
396
		for off := p.Account.Span.End.Offset; off < p.Span.End.Offset; off++ {
397
			if content[off] == ')' || content[off] == ']' {
398
				brSpan := token.Span{Start: offsetPos(p.Span.Start.File, off), End: offsetPos(p.Span.Start.File, off+1)}
399
				emit(brSpan, SemOperator, modifierAbstract)
400
				break
401
			}
402
		}
403
	}
404
405
	emit(p.Account.Span, SemanticAccount, 0)
406
407
	if p.Amount != nil {
408
		semEmitAmount(content, p.Amount, emit)
409
	}
410
	if p.Cost != nil {
411
		semEmitCost(content, p.Cost, emit)
412
	}
413
	if p.Balance != nil {
414
		semEmitBalanceAssertion(content, p.Balance, emit)
415
	}
416
	if p.Comment != nil {
417
		emit(p.Comment.Span, SemanticComment, 0)
418
	}
419
	for i := range p.Comments {
420
		emit(p.Comments[i].Span, SemanticComment, 0)
421
	}
422
}
423
424
// directiveKeyword returns the span of the leading keyword on a directive line.
425
func directiveKeyword(e token.Span, kw string) token.Span {
426
	return token.Span{Start: e.Start, End: offsetPos(e.Start.File, e.Start.Offset+len(kw))}
427
}
428
429
// directiveValue returns the trimmed span of the text after the keyword end
430
// offset, up to the inline comment or the end of the line.
431
func directiveValue(content string, e token.Span, comment *ast.Comment, kwEnd int) (token.Span, bool) {
432
	end := e.End.Offset
433
	if comment != nil {
434
		end = comment.Span.Start.Offset
435
	}
436
	return betweenSpan(content, e.Start.File, kwEnd, end)
437
}
438
439
func semEmitAmount(content string, a *ast.Amount, emit semEmitFn) {
440
	if a == nil {
441
		return
442
	}
443
	hasCommodity := a.CommoditySpan.Start.Offset > 0 && a.CommoditySpan.End.Offset > 0
444
	if hasCommodity && a.CommodityPos == ast.CommodityBefore {
445
		emit(a.CommoditySpan, SemanticCommodity, 0)
446
		semEmitQuantity(content, a, emit)
447
		return
448
	}
449
	semEmitQuantity(content, a, emit)
450
	if hasCommodity {
451
		emit(a.CommoditySpan, SemanticCommodity, 0)
452
	}
453
}
454
455
func semEmitQuantity(content string, a *ast.Amount, emit semEmitFn) {
456
	qStart, qEnd := quantitySpan(content, a)
457
	if qEnd <= qStart {
458
		return
459
	}
460
	mods := uint32(0)
461
	if a.IsNegative {
462
		mods |= modifierNegative
463
	}
464
	emit(offsetSpan(a.Span.Start.File, qStart, qEnd), SemanticAmount, mods)
465
}
466
467
func quantitySpan(content string, a *ast.Amount) (int, int) {
468
	start, end := a.Span.Start.Offset, a.Span.End.Offset
469
	switch {
470
	case a.Commodity == "":
471
		// bare quantity
472
	case a.CommodityPos == ast.CommodityBefore:
473
		// "$50.00" or "$   50.00": quantity follows the commodity span
474
		start = a.CommoditySpan.End.Offset
475
	default: // CommodityAfter
476
		// "50.00 USD" or "50.00     USD": quantity precedes the commodity
477
		end = a.CommoditySpan.Start.Offset
478
	}
479
	for start < end && (content[start] == ' ' || content[start] == '\t') {
480
		start++
481
	}
482
	for end > start && (content[end-1] == ' ' || content[end-1] == '\t') {
483
		end--
484
	}
485
	return start, end
486
}
487
488
type semEmitFn func(token.Span, uint32, uint32)
489
490
func emitDirective(content string, e token.Span, kwLen int, valType uint32, comment *ast.Comment, emit semEmitFn) {
491
	kwEnd := e.Start.Offset + kwLen
492
	emit(token.Span{Start: e.Start, End: offsetPos(e.Start.File, kwEnd)}, SemanticDirective, 0)
493
	if v, ok := directiveValue(content, e, comment, kwEnd); ok {
494
		emit(v, valType, 0)
495
	}
496
	if comment != nil {
497
		emit(comment.Span, SemanticComment, 0)
498
	}
499
}
500
501
func semEmitCost(content string, c *ast.Cost, emit semEmitFn) {
502
	if c.IsTotal {
503
		emit(token.Span{Start: c.Span.Start, End: offsetPos(c.Span.Start.File, c.Span.Start.Offset+2)}, SemOperator, 0)
504
	} else {
505
		emit(token.Span{Start: c.Span.Start, End: offsetPos(c.Span.Start.File, c.Span.Start.Offset+1)}, SemOperator, 0)
506
	}
507
	semEmitAmount(content, &c.Amount, emit)
508
}
509
510
func semEmitBalanceAssertion(content string, ba *ast.BalanceAssertion, emit semEmitFn) {
511
	// The operator is the run of '=', ':', '*' chars from the span start.
512
	// The ':' of ':=' precedes the '=' token, so back up one offset.
513
	opStart := ba.Span.Start.Offset
514
	if ba.IsAssignment && opStart > 0 {
515
		opStart--
516
	}
517
	opEnd := opStart
518
	for opEnd < ba.Span.End.Offset && (content[opEnd] == '=' || content[opEnd] == ':' || content[opEnd] == '*') {
519
		opEnd++
520
	}
521
	emit(token.Span{Start: offsetPos(ba.Span.Start.File, opStart), End: offsetPos(ba.Span.Start.File, opEnd)}, SemOperator, 0)
522
	semEmitAmount(content, &ba.Amount, emit)
523
	if ba.Cost != nil {
524
		semEmitCost(content, ba.Cost, emit)
525
	}
526
}
527
528
// semLexerFallback produces semantic tokens using only the lexer (for unparseable documents).
529
func semLexerFallback(content string, emit semEmitFn) {
530
	l := lexer.New("", []byte(content))
531
532
	var commentStart, commentEnd int // 0 = not inside a comment line
533
	take := func(span token.Span, tokType uint32, mods uint32) {
534
		emit(span, tokType, mods)
535
	}
536
	for {
537
		tok := l.Next()
538
		if tok.Type == token.EOF {
539
			if commentStart > 0 {
540
				take(token.Span{Start: offsetPos("", commentStart), End: offsetPos("", commentEnd)}, SemanticComment, 0)
541
			}
542
			break
543
		}
544
		if tok.Type == token.NEWLINE {
545
			if commentStart > 0 {
546
				take(token.Span{Start: offsetPos("", commentStart), End: offsetPos("", commentEnd)}, SemanticComment, 0)
547
				commentStart, commentEnd = 0, 0
548
			}
549
			continue
550
		}
551
		if tok.Type == token.WHITESPACE || tok.Type == token.INDENT {
552
			continue
553
		}
554
		tokType := uint32(SemString)
555
		if commentStart > 0 {
556
			tokType = SemanticComment
557
			if tok.Span.End.Offset > commentEnd {
558
				commentEnd = tok.Span.End.Offset
559
			}
560
			continue
561
		}
562
		switch tok.Type {
563
		case token.SEMICOLON, token.HASH, token.PERCENT:
564
			tokType = SemanticComment
565
			commentStart = tok.Span.Start.Offset
566
			commentEnd = tok.Span.End.Offset
567
			continue
568
		case token.STAR:
569
			tokType = SemanticComment // * at col 0 is comment marker
570
			commentStart = tok.Span.Start.Offset
571
			commentEnd = tok.Span.End.Offset
572
			continue
573
		case token.ACCOUNT, token.COMMODITY, token.INCLUDE, token.ALIAS,
574
			token.PAYEE, token.TAG, token.APPLY, token.END, token.COMMENTKW,
575
			token.YEAR, token.DECIMALMARK, token.D, token.P, token.N, token.C:
576
			tokType = SemanticDirective
577
		case token.DATE:
578
			tokType = SemanticDate
579
		case token.INT, token.DECIMAL:
580
			tokType = SemanticAmount
581
		case token.COMMODITYMARK:
582
			tokType = SemanticCommodity
583
		case token.BANG:
584
			tokType = SemanticStatus
585
		case token.AT, token.ATAT, token.EQ, token.EQEQ, token.EQEQEQ, token.EQSTAR:
586
			tokType = SemOperator
587
		}
588
		take(tok.Span, tokType, 0)
589
	}
590
}
591
592
func encodeSemTokens(tokens []semanticToken) []uint32 {
593
	if len(tokens) == 0 {
594
		return nil
595
	}
596
	slices.SortFunc(tokens, func(a, b semanticToken) int {
597
		if a.line != b.line {
598
			return int(a.line) - int(b.line)
599
		}
600
		return int(a.col) - int(b.col)
601
	})
602
	data := make([]uint32, 0, len(tokens)*5)
603
	var prevLine, prevCol uint32
604
	for _, t := range tokens {
605
		var deltaLine, deltaCol uint32
606
		if t.line == prevLine {
607
			deltaLine = 0
608
			deltaCol = t.col - prevCol
609
		} else {
610
			deltaLine = t.line - prevLine
611
			deltaCol = t.col
612
		}
613
		data = append(data, deltaLine, deltaCol, t.length, t.tokenType, t.modifiers)
614
		prevLine = t.line
615
		prevCol = t.col
616
	}
617
	return data
618
}
619
620
// betweenSpan returns the span of the text between two offsets, trimmed of surrounding whitespace.
621
func betweenSpan(content, file string, start, end int) (token.Span, bool) {
622
	for start < end && (content[start] == ' ' || content[start] == '\t') {
623
		start++
624
	}
625
	for end > start && (content[end-1] == ' ' || content[end-1] == '\t' || content[end-1] == '\n' || content[end-1] == '\r') {
626
		end--
627
	}
628
	if end <= start {
629
		return token.Span{}, false
630
	}
631
	return token.Span{Start: offsetPos(file, start), End: offsetPos(file, end)}, true
632
}
633
634
func offsetPos(file string, offset int) token.Pos { return token.Pos{File: file, Offset: offset} }
635
func offsetSpan(file string, start, end int) token.Span {
636
	return token.Span{Start: offsetPos(file, start), End: offsetPos(file, end)}
637
}