8 files changed,
61 insertions(+),
60 deletions(-)
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