all repos

clerk @ e76a121

missing tooling for ledger/hledger
8 files changed, 61 insertions(+), 60 deletions(-)
remove duplication of token.Pos.File in token.Span
Author: Oleksandr Smirnov olexsmir@gmail.com
Committed at: 2026-08-24 00:13:47 +0300
Authored at: 2026-08-23 23:13:29 +0300
Change ID: zswyqnwwxlrlzsololzmsovnnupoqmot
Parent: 18b35e4
M internal/linter/report.go
···
        24
        24
         	sortFinds(finds)

      
        25
        25
         	for _, find := range finds {

      
        26
        26
         		_, _ = fmt.Fprintf(w, "%s:%d:%d: %s: %s\n",

      
        27
        
        -			formatPath(style, find.Span.Start.File),

      
        
        27
        +			formatPath(style, find.Span.File),

      
        28
        28
         			find.Span.Start.Line, find.Span.Start.Col,

      
        29
        29
         			find.Code, find.Message)

      
        30
        30
         	}

      ···
        48
        48
         			Message:  find.Message,

      
        49
        49
         			Severity: find.Severity.String(),

      
        50
        50
         			Code:     string(find.Code),

      
        51
        
        -			File:     formatPath(style, find.Span.Start.File),

      
        
        51
        +			File:     formatPath(style, find.Span.File),

      
        52
        52
         			Line:     find.Span.Start.Line,

      
        53
        53
         			Column:   find.Span.Start.Col,

      
        54
        54
         		}

      
M internal/lsp/diagnostics.go
···
        93
        93
         	// findings is the dominant allocation in the diagnostics path

      
        94
        94
         	counts := make(map[string]int, len(finds))

      
        95
        95
         	for _, find := range finds {

      
        96
        
        -		if find.Span.Start.File != "" {

      
        97
        
        -			counts[find.Span.Start.File]++

      
        
        96
        +		if find.Span.File != "" {

      
        
        97
        +			counts[find.Span.File]++

      
        98
        98
         		}

      
        99
        99
         	}

      
        100
        100
         	diags := make(map[string][]protocol.Diagnostic, len(counts))

      ···
        102
        102
         		diags[fpath] = make([]protocol.Diagnostic, 0, n)

      
        103
        103
         	}

      
        104
        104
         	for _, find := range finds {

      
        105
        
        -		file := find.Span.Start.File

      
        
        105
        +		file := find.Span.File

      
        106
        106
         		if file == "" {

      
        107
        107
         			continue

      
        108
        108
         		}

      ···
        125
        125
         	seen := make(map[findKey]bool, len(finds))

      
        126
        126
         	dedup := make([]linter.Find, 0, len(finds))

      
        127
        127
         	for _, f := range finds {

      
        128
        
        -		k := findKey{f.Span.Start.File, f.Span.Start.Line, f.Span.Start.Col, f.Code}

      
        
        128
        +		k := findKey{f.Span.File, f.Span.Start.Line, f.Span.Start.Col, f.Code}

      
        129
        129
         		if seen[k] {

      
        130
        130
         			continue

      
        131
        131
         		}

      
M internal/lsp/textdocument_hover.go
···
        342
        342
         	for end > start && (content[end-1] == ' ' || content[end-1] == '\t') {

      
        343
        343
         		end--

      
        344
        344
         	}

      
        345
        
        -	return token.Span{Start: offsetPos(t.Span.Start.File, start), End: offsetPos(t.Span.Start.File, end)}

      
        
        345
        +	return token.Span{File: t.Span.File, Start: token.Pos{Offset: start}, End: token.Pos{Offset: end}}

      
        346
        346
         }

      
M internal/lsp/textdocument_rename.go
···
        390
        390
         	if d.Comment != nil {

      
        391
        391
         		end = d.Comment.Span.Start.Offset

      
        392
        392
         	}

      
        393
        
        -	return betweenSpan(content, d.Span.Start.File, d.Span.Start.Offset+len("tag"), end)

      
        
        393
        +	return betweenSpan(content, d.Span.File, d.Span.Start.Offset+len("tag"), end)

      
        394
        394
         }

      
        395
        395
         

      
        396
        396
         func tagKeySpan(content string, t *ast.Tag) token.Span {

      ···
        404
        404
         	for end > t.Span.Start.Offset && (content[end-1] == ' ' || content[end-1] == '\t') {

      
        405
        405
         		end--

      
        406
        406
         	}

      
        407
        
        -	return token.Span{Start: t.Span.Start, End: offsetPos(t.Span.Start.File, end)}

      
        
        407
        +	return token.Span{File: t.Span.File, Start: t.Span.Start, End: token.Pos{Offset: end}}

      
        408
        408
         }

      
        409
        409
         

      
        410
        410
         func sortAndDedup(changes map[uri.URI][]protocol.TextEdit) {

      
M internal/lsp/textdocument_semantic_tokens.go
···
        306
        306
         	case *ast.AliasDirective:

      
        307
        307
         		emit(directiveKeyword(e.Span, "alias"), semDirective, 0)

      
        308
        308
         		emit(e.From.Span, semAccount, 0)

      
        309
        
        -		if op, ok := betweenSpan(content, e.Span.Start.File, e.From.Span.End.Offset, e.To.Span.Start.Offset); ok {

      
        
        309
        +		if op, ok := betweenSpan(content, e.Span.File, e.From.Span.End.Offset, e.To.Span.Start.Offset); ok {

      
        310
        310
         			emit(op, semOperator, 0)

      
        311
        311
         		}

      
        312
        312
         		emit(e.To.Span, semAccount, 0)

      ···
        334
        334
         		if e.DateTime.Time != nil {

      
        335
        335
         			commStart = e.DateTime.Time.Span.End.Offset

      
        336
        336
         		}

      
        337
        
        -		if comm, ok := betweenSpan(content, e.Span.Start.File, commStart, e.Amount.Span.Start.Offset); ok {

      
        
        337
        +		if comm, ok := betweenSpan(content, e.Span.File, commStart, e.Amount.Span.Start.Offset); ok {

      
        338
        338
         			emit(comm, semCommodity, 0)

      
        339
        339
         		}

      
        340
        340
         		semEmitAmount(content, &e.Amount, emit)

      ···
        343
        343
         		emit(directiveKeyword(e.Span, "C"), semDirective, 0)

      
        344
        344
         		semEmitAmount(content, &e.From, emit)

      
        345
        345
         		// = operator: text between the two amounts

      
        346
        
        -		if op, ok := betweenSpan(content, e.Span.Start.File, e.From.Span.End.Offset, e.To.Span.Start.Offset); ok {

      
        
        346
        +		if op, ok := betweenSpan(content, e.Span.File, e.From.Span.End.Offset, e.To.Span.Start.Offset); ok {

      
        347
        347
         			emit(op, semOperator, 0)

      
        348
        348
         		}

      
        349
        349
         		semEmitAmount(content, &e.To, emit)

      ···
        390
        390
         

      
        391
        391
         func visitPeriodicTransaction(content string, pt *ast.PeriodicTransaction, emit semEmitFunc) {

      
        392
        392
         	// ~ operator is at the start of the period span

      
        393
        
        -	emit(offsetSpan(pt.Span.Start.File, pt.Span.Start.Offset, pt.Span.Start.Offset+1), semOperator, 0)

      
        
        393
        +	emit(offsetSpan(pt.Span.File, pt.Span.Start.Offset, pt.Span.Start.Offset+1), semOperator, 0)

      
        394
        394
         

      
        395
        395
         	// The period span covers the whole expr, including any "from ... to ..." dates

      
        396
        396
         	if pt.Period.Span.End.Offset > pt.Period.Span.Start.Offset {

      ···
        404
        404
         		pos := pt.Period.Span.Start.Offset

      
        405
        405
         		for _, d := range dates {

      
        406
        406
         			if d.Span.Start.Offset > pos {

      
        407
        
        -				emit(offsetSpan(pt.Period.Span.Start.File, pos, d.Span.Start.Offset), semProperty, 0)

      
        
        407
        +				emit(offsetSpan(pt.Period.Span.File, pos, d.Span.Start.Offset), semProperty, 0)

      
        408
        408
         			}

      
        409
        409
         			emit(d.Span, semDate, 0)

      
        410
        410
         			pos = d.Span.End.Offset

      
        411
        411
         		}

      
        412
        412
         		if pos < pt.Period.Span.End.Offset {

      
        413
        
        -			emit(offsetSpan(pt.Period.Span.Start.File, pos, pt.Period.Span.End.Offset), semProperty, 0)

      
        
        413
        +			emit(offsetSpan(pt.Period.Span.File, pos, pt.Period.Span.End.Offset), semProperty, 0)

      
        414
        414
         		}

      
        415
        415
         	}

      
        416
        416
         	if pt.Description != nil {

      ···
        427
        427
         

      
        428
        428
         func visitAutomatedTransaction(content string, at *ast.AutomatedTransaction, emit semEmitFunc) {

      
        429
        429
         	// = operator is at the start of the expression span

      
        430
        
        -	emit(offsetSpan(at.Span.Start.File, at.Span.Start.Offset, at.Span.Start.Offset+1), semOperator, 0)

      
        
        430
        +	emit(offsetSpan(at.Span.File, at.Span.Start.Offset, at.Span.Start.Offset+1), semOperator, 0)

      
        431
        431
         

      
        432
        432
         	if at.Expr.Value != "" {

      
        433
        433
         		emit(at.Expr.Span, semString, 0)

      ···
        451
        451
         		// opening bracket

      
        452
        452
         		for off := p.Span.Start.Offset; off < p.Account.Span.Start.Offset && off < p.Span.End.Offset; off++ {

      
        453
        453
         			if content[off] == '(' || content[off] == '[' {

      
        454
        
        -				brSpan := token.Span{Start: offsetPos(p.Span.Start.File, off), End: offsetPos(p.Span.Start.File, off+1)}

      
        455
        
        -				emit(brSpan, semOperator, modifierAbstract)

      
        
        454
        +				emit(offsetSpan(p.Span.File, off, off+1), semOperator, modifierAbstract)

      
        456
        455
         				break

      
        457
        456
         			}

      
        458
        457
         		}

      
        459
        458
         		// closing bracket

      
        460
        459
         		for off := p.Account.Span.End.Offset; off < p.Span.End.Offset; off++ {

      
        461
        460
         			if content[off] == ')' || content[off] == ']' {

      
        462
        
        -				brSpan := token.Span{Start: offsetPos(p.Span.Start.File, off), End: offsetPos(p.Span.Start.File, off+1)}

      
        463
        
        -				emit(brSpan, semOperator, modifierAbstract)

      
        
        461
        +				emit(offsetSpan(p.Span.File, off, off+1), semOperator, modifierAbstract)

      
        464
        462
         				break

      
        465
        463
         			}

      
        466
        464
         		}

      ···
        485
        483
         

      
        486
        484
         // directiveKeyword returns the span of the leading keyword on a directive line.

      
        487
        485
         func directiveKeyword(e token.Span, kw string) token.Span {

      
        488
        
        -	return token.Span{Start: e.Start, End: offsetPos(e.Start.File, e.Start.Offset+len(kw))}

      
        
        486
        +	return token.Span{File: e.File, Start: e.Start, End: token.Pos{Offset: e.Start.Offset + len(kw)}}

      
        489
        487
         }

      
        490
        488
         

      
        491
        489
         // directiveValue returns the trimmed span of the text after the keyword end

      ···
        495
        493
         	if comment != nil {

      
        496
        494
         		end = comment.Span.Start.Offset

      
        497
        495
         	}

      
        498
        
        -	return betweenSpan(content, e.Start.File, kwEnd, end)

      
        
        496
        +	return betweenSpan(content, e.File, kwEnd, end)

      
        499
        497
         }

      
        500
        498
         

      
        501
        499
         func semEmitAmount(content string, a *ast.Amount, emit semEmitFunc) {

      ···
        523
        521
         	if a.IsNegative {

      
        524
        522
         		mods |= modifierNegative

      
        525
        523
         	}

      
        526
        
        -	emit(offsetSpan(a.Span.Start.File, qStart, qEnd), semAmount, mods)

      
        
        524
        +	emit(offsetSpan(a.Span.File, qStart, qEnd), semAmount, mods)

      
        527
        525
         }

      
        528
        526
         

      
        529
        527
         func quantitySpan(content string, a *ast.Amount) (int, int) {

      ···
        566
        564
         	pos := c.Span.Start.Offset

      
        567
        565
         	for _, t := range c.Tags {

      
        568
        566
         		if t.Span.Start.Offset > pos {

      
        569
        
        -			emit(offsetSpan(c.Span.Start.File, pos, t.Span.Start.Offset), semComment, 0)

      
        
        567
        +			emit(offsetSpan(c.Span.File, pos, t.Span.Start.Offset), semComment, 0)

      
        570
        568
         		}

      
        571
        569
         		emit(t.Span, semProperty, 0)

      
        572
        570
         		pos = t.Span.End.Offset

      
        573
        571
         	}

      
        574
        572
         	if pos < c.Span.End.Offset {

      
        575
        
        -		emit(offsetSpan(c.Span.Start.File, pos, c.Span.End.Offset), semComment, 0)

      
        
        573
        +		emit(offsetSpan(c.Span.File, pos, c.Span.End.Offset), semComment, 0)

      
        576
        574
         	}

      
        577
        575
         }

      
        578
        576
         

      
        579
        577
         func emitDirective(content string, e token.Span, kwLen int, valType uint32, comment *ast.Comment, emit semEmitFunc) {

      
        580
        578
         	kwEnd := e.Start.Offset + kwLen

      
        581
        
        -	emit(token.Span{Start: e.Start, End: offsetPos(e.Start.File, kwEnd)}, semDirective, 0)

      
        
        579
        +	emit(token.Span{File: e.File, Start: e.Start, End: token.Pos{Offset: kwEnd}}, semDirective, 0)

      
        582
        580
         	if v, ok := directiveValue(content, e, comment, kwEnd); ok {

      
        583
        581
         		emit(v, valType, 0)

      
        584
        582
         	}

      ···
        587
        585
         

      
        588
        586
         func semEmitCost(content string, c *ast.Cost, emit semEmitFunc) {

      
        589
        587
         	if c.IsTotal {

      
        590
        
        -		emit(token.Span{Start: c.Span.Start, End: offsetPos(c.Span.Start.File, c.Span.Start.Offset+2)}, semOperator, 0)

      
        
        588
        +		emit(token.Span{File: c.Span.File, Start: c.Span.Start, End: token.Pos{Offset: c.Span.Start.Offset + 2}}, semOperator, 0)

      
        591
        589
         	} else {

      
        592
        
        -		emit(token.Span{Start: c.Span.Start, End: offsetPos(c.Span.Start.File, c.Span.Start.Offset+1)}, semOperator, 0)

      
        
        590
        +		emit(token.Span{File: c.Span.File, Start: c.Span.Start, End: token.Pos{Offset: c.Span.Start.Offset + 1}}, semOperator, 0)

      
        593
        591
         	}

      
        594
        592
         	semEmitAmount(content, &c.Amount, emit)

      
        595
        593
         }

      ···
        605
        603
         	for opEnd < ba.Span.End.Offset && (content[opEnd] == '=' || content[opEnd] == ':' || content[opEnd] == '*') {

      
        606
        604
         		opEnd++

      
        607
        605
         	}

      
        608
        
        -	emit(token.Span{Start: offsetPos(ba.Span.Start.File, opStart), End: offsetPos(ba.Span.Start.File, opEnd)}, semOperator, 0)

      
        
        606
        +	emit(token.Span{File: ba.Span.File, Start: token.Pos{Offset: opStart}, End: token.Pos{Offset: opEnd}}, semOperator, 0)

      
        609
        607
         	semEmitAmount(content, &ba.Amount, emit)

      
        610
        608
         	if ba.Cost != nil {

      
        611
        609
         		semEmitCost(content, ba.Cost, emit)

      ···
        634
        632
         		tok := l.Next()

      
        635
        633
         		if tok.Type == token.EOF {

      
        636
        634
         			if commentStart > 0 {

      
        637
        
        -				take(token.Span{Start: offsetPos("", commentStart), End: offsetPos("", commentEnd)}, semComment, 0)

      
        
        635
        +				take(token.Span{Start: token.Pos{Offset: commentStart}, End: token.Pos{Offset: commentEnd}}, semComment, 0)

      
        638
        636
         			}

      
        639
        637
         			break

      
        640
        638
         		}

      
        641
        639
         		if tok.Type == token.NEWLINE {

      
        642
        640
         			if commentStart > 0 {

      
        643
        
        -				take(token.Span{Start: offsetPos("", commentStart), End: offsetPos("", commentEnd)}, semComment, 0)

      
        
        641
        +				take(token.Span{Start: token.Pos{Offset: commentStart}, End: token.Pos{Offset: commentEnd}}, semComment, 0)

      
        644
        642
         				commentStart, commentEnd = 0, 0

      
        645
        643
         			}

      
        646
        644
         			lineStart, skipLine = true, false

      ···
        764
        762
         	if end <= start {

      
        765
        763
         		return token.Span{}, false

      
        766
        764
         	}

      
        767
        
        -	return token.Span{Start: offsetPos(file, start), End: offsetPos(file, end)}, true

      
        
        765
        +	return offsetSpan(file, start, end), true

      
        768
        766
         }

      
        769
        767
         

      
        770
        
        -func offsetPos(file string, offset int) token.Pos { return token.Pos{File: file, Offset: offset} }

      
        771
        768
         func offsetSpan(file string, start, end int) token.Span {

      
        772
        
        -	return token.Span{Start: offsetPos(file, start), End: offsetPos(file, end)}

      
        
        769
        +	return token.Span{File: file, Start: token.Pos{Offset: start}, End: token.Pos{Offset: end}}

      
        773
        770
         }

      
M journal/lexer/lexer.go
···
        806
        806
         

      
        807
        807
         func (l *Lexer) span(s savedPos) token.Span {

      
        808
        808
         	return token.Span{

      
        809
        
        -		Start: token.Pos{File: l.file, Offset: s.offset, Line: s.line, Col: s.col},

      
        810
        
        -		End:   token.Pos{File: l.file, Offset: l.pos, Line: l.line, Col: l.col},

      
        
        809
        +		File:  l.file,

      
        
        810
        +		Start: token.Pos{Offset: s.offset, Line: s.line, Col: s.col},

      
        
        811
        +		End:   token.Pos{Offset: l.pos, Line: l.line, Col: l.col},

      
        811
        812
         	}

      
        812
        813
         }

      
        813
        814
         

      
M journal/parser/parser.go
···
        320
        320
         	off := strings.Index(text[searchFrom:], dateStr)

      
        321
        321
         	abs := period.Span.Start.Offset + searchFrom + off

      
        322
        322
         	return token.Span{

      
        323
        
        -		Start: token.Pos{File: period.Span.Start.File, Offset: abs},

      
        324
        
        -		End:   token.Pos{File: period.Span.Start.File, Offset: abs + len(dateStr)},

      
        
        323
        +		File:  period.Span.File,

      
        
        324
        +		Start: token.Pos{Offset: abs},

      
        
        325
        +		End:   token.Pos{Offset: abs + len(dateStr)},

      
        325
        326
         	}

      
        326
        327
         }

      
        327
        328
         

      ···
        413
        414
         		cs := p.cur.Span

      
        414
        415
         		commodity = unquote(p.cur.Literal)

      
        415
        416
         		p.advance()

      
        416
        
        -		commoditySpan = token.Span{Start: cs.Start, End: p.cur.Span.Start}

      
        
        417
        +		commoditySpan = token.Span{File: cs.File, Start: cs.Start, End: p.cur.Span.Start}

      
        417
        418
         		hadSpace := p.got(token.WHITESPACE)

      
        418
        419
         		p.skipWhitespace()

      
        419
        420
         		if p.got(token.INT) || p.got(token.DECIMAL) || p.got(token.TEXT) {

      ···
        439
        440
         

      
        440
        441
         	// hledger parity: an inline format amount must include a decimal mark

      
        441
        442
         	if format != nil && format.Amount.QuantityFmt.Decimal == 0 {

      
        442
        
        -		p.errorfAt(format.Amount.Span.Start, "Please include a decimal point or decimal comma in commodity directives, to help us parse correctly. It may be followed by zero or more decimal digits.")

      
        
        443
        +		p.errorfAt(format.Amount.Span, "Please include a decimal point or decimal comma in commodity directives, to help us parse correctly. It may be followed by zero or more decimal digits.")

      
        443
        444
         	}

      
        444
        445
         

      
        445
        446
         	comment := p.parseOptInlineComment()

      ···
        464
        465
         			// and the amount must include a decimal mark; the node is kept either

      
        465
        466
         			// way so the printer can round-trip the input.

      
        466
        467
         			if amt.Commodity != commodity {

      
        467
        
        -				p.errorfAt(amt.Span.Start, "commodity directive symbol %q and format directive symbol %q should be the same", commodity, amt.Commodity)

      
        
        468
        +				p.errorfAt(amt.Span, "commodity directive symbol %q and format directive symbol %q should be the same", commodity, amt.Commodity)

      
        468
        469
         			} else if amt.QuantityFmt.Decimal == 0 {

      
        469
        
        -				p.errorfAt(amt.Span.Start, "Please include a decimal point or decimal comma in commodity directives, to help us parse correctly. It may be followed by zero or more decimal digits.")

      
        
        470
        +				p.errorfAt(amt.Span, "Please include a decimal point or decimal comma in commodity directives, to help us parse correctly. It may be followed by zero or more decimal digits.")

      
        470
        471
         			}

      
        471
        472
         			c := p.parseOptInlineComment()

      
        472
        473
         			p.expectNewline()

      ···
        866
        867
         		amt.Commodity = unquote(p.cur.Literal)

      
        867
        868
         		amt.CommodityPos = ast.CommodityBefore

      
        868
        869
         		p.advance()

      
        869
        
        -		amt.CommoditySpan = token.Span{Start: cs.Start, End: p.cur.Span.Start}

      
        
        870
        +		amt.CommoditySpan = token.Span{File: cs.File, Start: cs.Start, End: p.cur.Span.Start}

      
        870
        871
         		if p.got(token.WHITESPACE) {

      
        871
        872
         			amt.HasSpace = true

      
        872
        873
         			p.skipWhitespace()

      ···
        890
        891
         				amt.Commodity = unquote(p.cur.Literal)

      
        891
        892
         				amt.CommodityPos = ast.CommodityAfter

      
        892
        893
         				p.advance()

      
        893
        
        -				amt.CommoditySpan = token.Span{Start: cs.Start, End: p.cur.Span.Start}

      
        
        894
        +				amt.CommoditySpan = token.Span{File: cs.File, Start: cs.Start, End: p.cur.Span.Start}

      
        894
        895
         			}

      
        895
        896
         		case token.COMMODITYMARK, token.TEXT, token.STRING:

      
        896
        897
         			cs := p.cur.Span

      
        897
        898
         			amt.Commodity = unquote(p.cur.Literal)

      
        898
        899
         			amt.CommodityPos = ast.CommodityAfter

      
        899
        900
         			p.advance()

      
        900
        
        -			amt.CommoditySpan = token.Span{Start: cs.Start, End: p.cur.Span.Start}

      
        
        901
        +			amt.CommoditySpan = token.Span{File: cs.File, Start: cs.Start, End: p.cur.Span.Start}

      
        901
        902
         		}

      
        902
        903
         	}

      
        903
        904
         

      ···
        1156
        1157
         	text := ""

      
        1157
        1158
         	if p.got(token.TEXT) {

      
        1158
        1159
         		text = p.cur.Literal

      
        1159
        
        -		tags = parseCommentTags(text, p.cur.Span.Start)

      
        
        1160
        +		tags = parseCommentTags(text, p.cur.Span)

      
        1160
        1161
         		p.advance()

      
        1161
        1162
         	}

      
        1162
        1163
         

      ···
        1271
        1272
         	})

      
        1272
        1273
         }

      
        1273
        1274
         

      
        1274
        
        -// errorfAt records a parse error pointing at a single position.

      
        1275
        
        -func (p *Parser) errorfAt(pos token.Pos, format string, args ...any) {

      
        
        1275
        +// errorfAt records a parse error pointing at the start of span.

      
        
        1276
        +func (p *Parser) errorfAt(span token.Span, format string, args ...any) {

      
        1276
        1277
         	p.errors = append(p.errors, &ast.ParseError{

      
        1277
        
        -		Span:    token.Span{Start: pos, End: pos},

      
        
        1278
        +		Span:    token.Span{File: span.File, Start: span.Start, End: span.Start},

      
        1278
        1279
         		Message: fmt.Sprintf(format, args...),

      
        1279
        1280
         	})

      
        1280
        1281
         }

      ···
        1337
        1338
         		return "", token.Span{}

      
        1338
        1339
         	}

      
        1339
        1340
         	if single != "" {

      
        1340
        
        -		return single, token.Span{Start: first.Start, End: last.End}

      
        
        1341
        +		return single, token.Span{File: first.File, Start: first.Start, End: last.End}

      
        1341
        1342
         	}

      
        1342
        
        -	return strings.TrimSpace(b.String()), token.Span{Start: first.Start, End: last.End}

      
        
        1343
        +	return strings.TrimSpace(b.String()), token.Span{File: first.File, Start: first.Start, End: last.End}

      
        1343
        1344
         }

      
        1344
        1345
         

      
        1345
        1346
         func (p *Parser) parseTextComment() *ast.Comment {

      ···
        1401
        1402
         }

      
        1402
        1403
         

      
        1403
        1404
         func (p *Parser) span(s token.Span) token.Span {

      
        1404
        
        -	return token.Span{Start: s.Start, End: p.cur.Span.Start}

      
        
        1405
        +	return token.Span{File: s.File, Start: s.Start, End: p.cur.Span.Start}

      
        1405
        1406
         }

      
        1406
        1407
         

      
        1407
        1408
         func normalizeLiteral(lit string, thousands, decimal byte) string {

      ···
        1511
        1512
         // parseCommentTags extacts tags from comment text.

      
        1512
        1513
         // A tag is a word immediately followed by a ':', with an optional value that ends at a comma or the end of a line.

      
        1513
        1514
         // https://hledger.org/1.52/hledger.html?highlight=tags#tags

      
        1514
        
        -func parseCommentTags(text string, base token.Pos) []ast.Tag {

      
        
        1515
        +func parseCommentTags(text string, base token.Span) []ast.Tag {

      
        1515
        1516
         	var tags []ast.Tag

      
        1516
        1517
         	for i := 0; i < len(text); {

      
        1517
        1518
         		colon := strings.IndexByte(text[i:], ':')

      ···
        1544
        1545
         			Key:   key,

      
        1545
        1546
         			Value: value,

      
        1546
        1547
         			Span: token.Span{

      
        1547
        
        -				Start: tagPos(base, text, keyStart),

      
        1548
        
        -				End:   tagPos(base, text, valueEnd),

      
        
        1548
        +				File:  base.File,

      
        
        1549
        +				Start: tagPos(base.Start, text, keyStart),

      
        
        1550
        +				End:   tagPos(base.Start, text, valueEnd),

      
        1549
        1551
         			},

      
        1550
        1552
         		})

      
        1551
        1553
         		i = valueEnd

      ···
        1559
        1561
         

      
        1560
        1562
         func tagPos(base token.Pos, text string, off int) token.Pos {

      
        1561
        1563
         	return token.Pos{

      
        1562
        
        -		File:   base.File,

      
        1563
        1564
         		Offset: base.Offset + off,

      
        1564
        1565
         		Line:   base.Line,

      
        1565
        1566
         		Col:    base.Col + utf8.RuneCountInString(text[:off]),

      
M journal/token/token.go
···
        10
        10
         	Span    Span

      
        11
        11
         }

      
        12
        12
         

      
        13
        
        -type Span struct{ Start, End Pos }

      
        
        13
        +type Span struct {

      
        
        14
        +	File       string // absolute path, "" for unknown

      
        
        15
        +	Start, End Pos

      
        
        16
        +}

      
        14
        17
         

      
        15
        18
         func (s Span) String() string {

      
        16
        
        -	if s.Start.File != "" {

      
        17
        
        -		return fmt.Sprintf("%s:%d:%d-%d:%d", s.Start.File,

      
        
        19
        +	if s.File != "" {

      
        
        20
        +		return fmt.Sprintf("%s:%d:%d-%d:%d", s.File,

      
        18
        21
         			s.Start.Line, s.Start.Col,

      
        19
        22
         			s.End.Line, s.End.Col)

      
        20
        23
         	}

      ···
        24
        27
         }

      
        25
        28
         

      
        26
        29
         type Pos struct {

      
        27
        
        -	File   string // absolute path, "" for unknow

      
        28
        30
         	Offset int

      
        29
        31
         	Line   int

      
        30
        32
         	Col    int