all repos

clerk @ 368f35af2811d8688f0ff80ac6f71b873203be00

missing tooling for ledger/hledger
63 files changed, 2011 insertions(+), 241 deletions(-)
lsp: semantic highlights
Author: Oleksandr Smirnov olexsmir@gmail.com
Committed at: 2026-07-31 16:41:01 +0300
Authored at: 2026-07-29 20:09:13 +0300
Change ID: ykpklkqsnuurvtvwqlktxzrotokxqqvp
Parent: 1f3a173
jump to
M internal/cli/cmd_lsp.go
A internal/lsp/lsputil/utf16.go
A internal/lsp/lsputil/utf16_test.go
M internal/lsp/server.go
A internal/lsp/testdata/semantic-directives.txtar
A internal/lsp/testdata/semantic-empty.txtar
A internal/lsp/testdata/semantic-journal.txtar
A internal/lsp/testdata/semantic-unparseable.txtar
M internal/lsp/textdocument_format.go
A internal/lsp/textdocument_semantic.go
A internal/lsp/textdocument_semantic_test.go
M internal/lsp/textdocument_sync.go
M journal/ast/ast.go
M journal/ast/directives.go
M journal/ast/dump.go
M journal/ast/entries.go
M journal/parser/parser.go
M journal/parser/testdata/C_directive.txtar
M journal/parser/testdata/D_directive.txtar
M journal/parser/testdata/P_directive.txtar
M journal/parser/testdata/P_directive_with_time.txtar
M journal/parser/testdata/account_with_spaces.txtar
M journal/parser/testdata/automated_transaction.txtar
M journal/parser/testdata/bad_between_good.txtar
M journal/parser/testdata/bad_posting_at_end.txtar
M journal/parser/testdata/bad_then_next_transaction.txtar
M journal/parser/testdata/comment_between_bad_postings.txtar
M journal/parser/testdata/commodity_quantity_after.txtar
M journal/parser/testdata/commodity_quantity_first.txtar
M journal/parser/testdata/commodity_with_subdirectives.txtar
M journal/parser/testdata/comodity_directive.txtar
M journal/parser/testdata/comodity_directive_no_space.txtar
M journal/parser/testdata/comodity_directive_word.txtar
M journal/parser/testdata/digit_group_marks.txtar
M journal/parser/testdata/directive_prefixes.txtar
M journal/parser/testdata/illegal_between_transactions.txtar
M journal/parser/testdata/multiple_bad_postings.txtar
M journal/parser/testdata/nested_apply_tag_directives.txtar
M journal/parser/testdata/period_transaction_expressions.txtar
M journal/parser/testdata/quoted_payee_names.txtar
M journal/parser/testdata/recovery_after_bad_posting.txtar
M journal/parser/testdata/special_chars_in_description.txtar
M journal/parser/testdata/three_bad_postings.txtar
M journal/parser/testdata/transaction_in_ukrainian.txtar
M journal/parser/testdata/transaction_with_balance_assertion.txtar
M journal/parser/testdata/transaction_with_code.txtar
M journal/parser/testdata/transaction_with_cost_and_assertion.txtar
M journal/parser/testdata/transaction_with_costs.txtar
M journal/parser/testdata/transaction_with_digit_payees.txtar
M journal/parser/testdata/transaction_with_header_comment.txtar
M journal/parser/testdata/transaction_with_inline_comment.txtar
M journal/parser/testdata/transaction_with_note.txtar
M journal/parser/testdata/transaction_with_posting.txtar
M journal/parser/testdata/transaction_with_posting_amounts.txtar
M journal/parser/testdata/transaction_with_quoted_commodity.txtar
M journal/parser/testdata/transaction_with_spaced_account_name.txtar
M journal/parser/testdata/transaction_with_strange_commodity_symbols.txtar
M journal/parser/testdata/transaction_with_tabs.txtar
M journal/parser/testdata/transaction_with_trailing_indent.txtar
M journal/parser/testdata/transaction_with_unicode_commodity_symbols.txtar
M journal/parser/testdata/transaction_with_virtual_accounts.txtar
M journal/parser/testdata/virtual_postings_with_statuses.txtar
M journal/printer/transaction.go
M internal/cli/cmd_lsp.go
···
        5
        5
         	"os"

      
        6
        6
         

      
        7
        7
         	"github.com/urfave/cli/v3"

      
        
        8
        +

      
        8
        9
         	"olexsmir.xyz/clerk/internal/lsp"

      
        9
        10
         )

      
        10
        11
         

      
A internal/lsp/lsputil/utf16.go
···
        
        1
        +package lsputil

      
        
        2
        +

      
        
        3
        +import "unicode/utf8"

      
        
        4
        +

      
        
        5
        +// Utf16Col returns the UTF-16 code unit column (0-based) for a byte offset

      
        
        6
        +// within the given line content (without newline).

      
        
        7
        +func Utf16Col(line string, byteOffset int) int {

      
        
        8
        +	if byteOffset <= 0 {

      
        
        9
        +		return 0

      
        
        10
        +	}

      
        
        11
        +	if byteOffset > len(line) {

      
        
        12
        +		byteOffset = len(line)

      
        
        13
        +	}

      
        
        14
        +	col := 0

      
        
        15
        +	for i := 0; i < byteOffset; {

      
        
        16
        +		r, size := utf8.DecodeRuneInString(line[i:])

      
        
        17
        +		if r == utf8.RuneError && size <= 1 {

      
        
        18
        +			break

      
        
        19
        +		}

      
        
        20
        +		col += utf16Len(r)

      
        
        21
        +		i += size

      
        
        22
        +	}

      
        
        23
        +	return col

      
        
        24
        +}

      
        
        25
        +

      
        
        26
        +// Utf16Len returns the UTF-16 code unit length of content[offset:end].

      
        
        27
        +func Utf16Len(content string, offset, end int) int {

      
        
        28
        +	if offset < 0 {

      
        
        29
        +		offset = 0

      
        
        30
        +	}

      
        
        31
        +	if end > len(content) {

      
        
        32
        +		end = len(content)

      
        
        33
        +	}

      
        
        34
        +	if offset >= end {

      
        
        35
        +		return 0

      
        
        36
        +	}

      
        
        37
        +	n := 0

      
        
        38
        +	for i := offset; i < end; {

      
        
        39
        +		r, size := utf8.DecodeRuneInString(content[i:])

      
        
        40
        +		if r == utf8.RuneError && size <= 1 {

      
        
        41
        +			break

      
        
        42
        +		}

      
        
        43
        +		n += utf16Len(r)

      
        
        44
        +		i += size

      
        
        45
        +	}

      
        
        46
        +	return n

      
        
        47
        +}

      
        
        48
        +

      
        
        49
        +// LineCol converts a byte offset in content to 0-based line number and

      
        
        50
        +// UTF-16 code unit column.

      
        
        51
        +func LineCol(content string, offset int) (line int, col int) {

      
        
        52
        +	if offset <= 0 {

      
        
        53
        +		return 0, 0

      
        
        54
        +	}

      
        
        55
        +	if offset > len(content) {

      
        
        56
        +		offset = len(content)

      
        
        57
        +	}

      
        
        58
        +	lineStart := 0

      
        
        59
        +	lineNum := 0

      
        
        60
        +	for i := 0; i < len(content); {

      
        
        61
        +		r, size := utf8.DecodeRuneInString(content[i:])

      
        
        62
        +		if r == utf8.RuneError && size <= 1 {

      
        
        63
        +			break

      
        
        64
        +		}

      
        
        65
        +		if r == '\n' {

      
        
        66
        +			if offset < i+size {

      
        
        67
        +				col = Utf16Col(content[lineStart:offset], offset-lineStart)

      
        
        68
        +				return lineNum, col

      
        
        69
        +			}

      
        
        70
        +			lineStart = i + size

      
        
        71
        +			lineNum++

      
        
        72
        +			i += size

      
        
        73
        +			continue

      
        
        74
        +		}

      
        
        75
        +		if r == '\r' {

      
        
        76
        +			// skip \r\n

      
        
        77
        +			if i+1 < len(content) && content[i+1] == '\n' {

      
        
        78
        +				size++

      
        
        79
        +			}

      
        
        80
        +			if offset < i+size {

      
        
        81
        +				col = Utf16Col(content[lineStart:offset], offset-lineStart)

      
        
        82
        +				return lineNum, col

      
        
        83
        +			}

      
        
        84
        +			lineStart = i + size

      
        
        85
        +			lineNum++

      
        
        86
        +			i += size

      
        
        87
        +			continue

      
        
        88
        +		}

      
        
        89
        +		if i+size > offset {

      
        
        90
        +			col = Utf16Col(content[lineStart:offset], offset-lineStart)

      
        
        91
        +			return lineNum, col

      
        
        92
        +		}

      
        
        93
        +		i += size

      
        
        94
        +	}

      
        
        95
        +	col = Utf16Col(content[lineStart:], len(content)-lineStart)

      
        
        96
        +	return lineNum, col

      
        
        97
        +}

      
        
        98
        +

      
        
        99
        +// utf16Len returns the number of UTF-16 code units for a rune.

      
        
        100
        +func utf16Len(r rune) int {

      
        
        101
        +	if r >= 0x10000 && r <= 0x10FFFF {

      
        
        102
        +		return 2

      
        
        103
        +	}

      
        
        104
        +	return 1

      
        
        105
        +}

      
A internal/lsp/lsputil/utf16_test.go
···
        
        1
        +package lsputil

      
        
        2
        +

      
        
        3
        +import "testing"

      
        
        4
        +

      
        
        5
        +func TestUtf16Col_Basic(t *testing.T) {

      
        
        6
        +	col := Utf16Col("hello", 3)

      
        
        7
        +	if col != 3 {

      
        
        8
        +		t.Errorf("Utf16Col for ASCII = %d, want 3", col)

      
        
        9
        +	}

      
        
        10
        +}

      
        
        11
        +

      
        
        12
        +func TestUtf16Col_Cyrillic(t *testing.T) {

      
        
        13
        +	// "Привет" is 6 Cyrillic chars = 12 bytes, 6 UTF-16 code units

      
        
        14
        +	line := "Привет"

      
        
        15
        +	col := Utf16Col(line, len(line))

      
        
        16
        +	if col != 6 {

      
        
        17
        +		t.Errorf("Utf16Col for 'Привет' = %d, want 6", col)

      
        
        18
        +	}

      
        
        19
        +}

      
        
        20
        +

      
        
        21
        +func TestUtf16Len_ASCII(t *testing.T) {

      
        
        22
        +	l := Utf16Len("hello world", 0, 5)

      
        
        23
        +	if l != 5 {

      
        
        24
        +		t.Errorf("Utf16Len = %d, want 5", l)

      
        
        25
        +	}

      
        
        26
        +}

      
        
        27
        +

      
        
        28
        +func TestUtf16Len_Cyrillic(t *testing.T) {

      
        
        29
        +	// "Привет" = 6 UTF-16 code units

      
        
        30
        +	l := Utf16Len("Привет мир", 0, 12)

      
        
        31
        +	if l != 6 {

      
        
        32
        +		t.Errorf("Utf16Len for 'Привет' = %d, want 6", l)

      
        
        33
        +	}

      
        
        34
        +}

      
        
        35
        +

      
        
        36
        +func TestLineCol_Basic(t *testing.T) {

      
        
        37
        +	line, col := LineCol("hello\nworld", 11)

      
        
        38
        +	if line != 1 || col != 5 {

      
        
        39
        +		t.Errorf("LineCol = (%d,%d), want (1,5)", line, col)

      
        
        40
        +	}

      
        
        41
        +}

      
        
        42
        +

      
        
        43
        +func TestLineCol_FirstLine(t *testing.T) {

      
        
        44
        +	line, col := LineCol("hello world", 5)

      
        
        45
        +	if line != 0 || col != 5 {

      
        
        46
        +		t.Errorf("LineCol = (%d,%d), want (0,5)", line, col)

      
        
        47
        +	}

      
        
        48
        +}

      
        
        49
        +

      
        
        50
        +func TestLineCol_LineStart(t *testing.T) {

      
        
        51
        +	// offset at the first char of line 1 must be (1,0), not the tail of line 0

      
        
        52
        +	line, col := LineCol("ab\ncd", 3)

      
        
        53
        +	if line != 1 || col != 0 {

      
        
        54
        +		t.Errorf("LineCol(3) = (%d,%d), want (1,0)", line, col)

      
        
        55
        +	}

      
        
        56
        +	// newline char itself belongs to the line it ends

      
        
        57
        +	line, col = LineCol("ab\ncd", 2)

      
        
        58
        +	if line != 0 || col != 2 {

      
        
        59
        +		t.Errorf("LineCol(2) = (%d,%d), want (0,2)", line, col)

      
        
        60
        +	}

      
        
        61
        +}

      
        
        62
        +

      
        
        63
        +func TestLineCol_BlankLine(t *testing.T) {

      
        
        64
        +	// "a\n\nb": line 1 is blank; offset 3 is the start of line 2

      
        
        65
        +	line, col := LineCol("a\n\nb", 3)

      
        
        66
        +	if line != 2 || col != 0 {

      
        
        67
        +		t.Errorf("LineCol(3) = (%d,%d), want (2,0)", line, col)

      
        
        68
        +	}

      
        
        69
        +	line, col = LineCol("a\n\nb", 2)

      
        
        70
        +	if line != 1 || col != 0 {

      
        
        71
        +		t.Errorf("LineCol(2) = (%d,%d), want (1,0)", line, col)

      
        
        72
        +	}

      
        
        73
        +}

      
        
        74
        +

      
        
        75
        +func TestLineCol_CRLF(t *testing.T) {

      
        
        76
        +	line, col := LineCol("ab\r\ncd", 4)

      
        
        77
        +	if line != 1 || col != 0 {

      
        
        78
        +		t.Errorf("LineCol(4) = (%d,%d), want (1,0)", line, col)

      
        
        79
        +	}

      
        
        80
        +	line, col = LineCol("ab\r\ncd", 2)

      
        
        81
        +	if line != 0 || col != 2 {

      
        
        82
        +		t.Errorf("LineCol(2) = (%d,%d), want (0,2)", line, col)

      
        
        83
        +	}

      
        
        84
        +}

      
M internal/lsp/server.go
···
        11
        11
         	"olexsmir.xyz/clerk/internal/analyzer"

      
        12
        12
         	"olexsmir.xyz/clerk/internal/linter"

      
        13
        13
         	"olexsmir.xyz/clerk/journal"

      
        
        14
        +	"olexsmir.xyz/clerk/journal/ast"

      
        14
        15
         	"olexsmir.xyz/clerk/journal/printer"

      
        15
        16
         )

      
        16
        17
         

      ···
        38
        39
         	text       string

      
        39
        40
         	version    int32

      
        40
        41
         	languageID protocol.LanguageKind

      
        
        42
        +	journal    *ast.Journal // cached parse; nil on parse failure

      
        41
        43
         }

      
        42
        44
         

      
        43
        45
         func (s *server) Initialize(ctx context.Context, params *protocol.InitializeParams) (*protocol.InitializeResult, error) {

      ···
        51
        53
         			TextDocumentSync: &protocol.TextDocumentSyncOptions{

      
        52
        54
         				OpenClose: new(true),

      
        53
        55
         				Change:    new(protocol.TextDocumentSyncKindFull),

      
        
        56
        +			},

      
        
        57
        +			SemanticTokensProvider: &protocol.SemanticTokensOptions{

      
        
        58
        +				Legend: getSemanticTokensLegend(),

      
        
        59
        +				Range:  protocol.Boolean(true),

      
        
        60
        +				Full:   protocol.Boolean(true),

      
        54
        61
         			},

      
        55
        62
         		},

      
        56
        63
         	}, nil

      
A internal/lsp/testdata/semantic-directives.txtar
···
        
        1
        +-- in.journal --

      
        
        2
        +account expenses:food

      
        
        3
        +account expenses:food  ; note

      
        
        4
        +

      
        
        5
        +alias old = new

      
        
        6
        +alias old = new  ; alias note

      
        
        7
        +

      
        
        8
        +payee Some Store

      
        
        9
        +payee Some Store  ; payee note

      
        
        10
        +

      
        
        11
        +tag foo

      
        
        12
        +tag foo  ; tag note

      
        
        13
        +

      
        
        14
        +year 2024

      
        
        15
        +year 2024  ; year note

      
        
        16
        +

      
        
        17
        +Y 2024

      
        
        18
        +Y 2024  ; Y note

      
        
        19
        +

      
        
        20
        +apply account expenses

      
        
        21
        +end apply

      
        
        22
        +apply account expenses  ; apply note

      
        
        23
        +end apply  ; end note

      
        
        24
        +

      
        
        25
        +apply tag hashtag

      
        
        26
        +end apply

      
        
        27
        +apply tag hashtag  ; apply tag note

      
        
        28
        +end apply  ; end note

      
        
        29
        +

      
        
        30
        +apply fixed CAD $0.90

      
        
        31
        +end apply

      
        
        32
        +apply fixed CAD $0.90  ; apply fixed note

      
        
        33
        +end apply  ; end note

      
        
        34
        +

      
        
        35
        +N ignored

      
        
        36
        +N ignored  ; N note

      
        
        37
        +N $

      
        
        38
        +N $  ; N dollar note

      
        
        39
        +

      
        
        40
        +D $1,000.00

      
        
        41
        +D $1,000.00  ; D note

      
        
        42
        +

      
        
        43
        +P 2024-01-15 USD 1.05

      
        
        44
        +P 2024-01-15 USD 1.05  ; P note

      
        
        45
        +P 2024-01-01 12:00:00 USD 40.50

      
        
        46
        +P 2024-01-01 12:00:00 USD 40.50  ; P time note

      
        
        47
        +

      
        
        48
        +C 100 USD = 90 EUR

      
        
        49
        +C 100 USD = 90 EUR  ; C note

      
        
        50
        +

      
        
        51
        +decimal-mark .

      
        
        52
        +decimal-mark .  ; decimal note

      
        
        53
        +

      
        
        54
        +include foo.journal

      
        
        55
        +include foo.journal  ; include note

      
        
        56
        +

      
        
        57
        +commodity USD

      
        
        58
        +commodity USD  ; commodity note

      
        
        59
        +

      
        
        60
        +commodity INR

      
        
        61
        +  format INR 1,00,00,000.00

      
        
        62
        +commodity INR  ; commodity INR note

      
        
        63
        +  format INR 1,00,00,000.00

      
        
        64
        +

      
        
        65
        +commodity 1,000.00 UAH

      
        
        66
        +commodity 1,000.00 UAH  ; commodity amount note

      
        
        67
        +commodity UAH

      
        
        68
        +  format 1 000.00 UAH

      
        
        69
        +commodity UAH  ; commodity UAH note

      
        
        70
        +  format 1 000.00 UAH

      
        
        71
        +

      
        
        72
        +-- expect --

      
        
        73
        +0:0+7 keyword

      
        
        74
        +0:8+13 namespace

      
        
        75
        +1:0+7 keyword

      
        
        76
        +1:8+13 namespace

      
        
        77
        +1:23+6 comment

      
        
        78
        +3:0+5 keyword

      
        
        79
        +3:6+3 namespace

      
        
        80
        +3:10+1 operator

      
        
        81
        +3:12+3 namespace

      
        
        82
        +4:0+5 keyword

      
        
        83
        +4:6+3 namespace

      
        
        84
        +4:10+1 operator

      
        
        85
        +4:12+3 namespace

      
        
        86
        +4:17+12 comment

      
        
        87
        +6:0+5 keyword

      
        
        88
        +6:6+10 property

      
        
        89
        +7:0+5 keyword

      
        
        90
        +7:6+10 property

      
        
        91
        +7:18+12 comment

      
        
        92
        +9:0+3 keyword

      
        
        93
        +9:4+3 property

      
        
        94
        +10:0+3 keyword

      
        
        95
        +10:4+3 property

      
        
        96
        +10:9+10 comment

      
        
        97
        +12:0+4 keyword

      
        
        98
        +12:5+4 property

      
        
        99
        +13:0+4 keyword

      
        
        100
        +13:5+4 property

      
        
        101
        +13:11+11 comment

      
        
        102
        +15:0+1 keyword

      
        
        103
        +15:2+4 property

      
        
        104
        +16:0+1 keyword

      
        
        105
        +16:2+4 property

      
        
        106
        +16:8+8 comment

      
        
        107
        +18:0+5 keyword

      
        
        108
        +18:6+16 property

      
        
        109
        +19:0+3 keyword

      
        
        110
        +19:4+5 property

      
        
        111
        +20:0+5 keyword

      
        
        112
        +20:6+16 property

      
        
        113
        +20:24+12 comment

      
        
        114
        +21:0+3 keyword

      
        
        115
        +21:4+5 property

      
        
        116
        +21:11+10 comment

      
        
        117
        +23:0+5 keyword

      
        
        118
        +23:6+11 property

      
        
        119
        +24:0+3 keyword

      
        
        120
        +24:4+5 property

      
        
        121
        +25:0+5 keyword

      
        
        122
        +25:6+11 property

      
        
        123
        +25:19+16 comment

      
        
        124
        +26:0+3 keyword

      
        
        125
        +26:4+5 property

      
        
        126
        +26:11+10 comment

      
        
        127
        +28:0+5 keyword

      
        
        128
        +28:6+15 property

      
        
        129
        +29:0+3 keyword

      
        
        130
        +29:4+5 property

      
        
        131
        +30:0+5 keyword

      
        
        132
        +30:6+15 property

      
        
        133
        +30:23+18 comment

      
        
        134
        +31:0+3 keyword

      
        
        135
        +31:4+5 property

      
        
        136
        +31:11+10 comment

      
        
        137
        +33:0+1 keyword

      
        
        138
        +33:2+7 property

      
        
        139
        +34:0+1 keyword

      
        
        140
        +34:2+7 property

      
        
        141
        +34:11+8 comment

      
        
        142
        +35:0+1 keyword

      
        
        143
        +35:2+1 property

      
        
        144
        +36:0+1 keyword

      
        
        145
        +36:2+1 property

      
        
        146
        +36:5+15 comment

      
        
        147
        +38:0+1 keyword

      
        
        148
        +38:2+1 type

      
        
        149
        +38:3+8 number

      
        
        150
        +39:0+1 keyword

      
        
        151
        +39:2+1 type

      
        
        152
        +39:3+8 number

      
        
        153
        +39:13+8 comment

      
        
        154
        +41:0+1 keyword

      
        
        155
        +41:2+10 class

      
        
        156
        +41:13+3 type

      
        
        157
        +41:17+4 number

      
        
        158
        +42:0+1 keyword

      
        
        159
        +42:2+10 class

      
        
        160
        +42:13+3 type

      
        
        161
        +42:17+4 number

      
        
        162
        +42:23+8 comment

      
        
        163
        +43:0+1 keyword

      
        
        164
        +43:2+10 class

      
        
        165
        +43:13+8 class

      
        
        166
        +43:22+3 type

      
        
        167
        +43:26+5 number

      
        
        168
        +44:0+1 keyword

      
        
        169
        +44:2+10 class

      
        
        170
        +44:13+8 class

      
        
        171
        +44:22+3 type

      
        
        172
        +44:26+5 number

      
        
        173
        +44:33+13 comment

      
        
        174
        +46:0+1 keyword

      
        
        175
        +46:2+3 number

      
        
        176
        +46:6+3 type

      
        
        177
        +46:10+1 operator

      
        
        178
        +46:12+2 number

      
        
        179
        +46:15+3 type

      
        
        180
        +47:0+1 keyword

      
        
        181
        +47:2+3 number

      
        
        182
        +47:6+3 type

      
        
        183
        +47:10+1 operator

      
        
        184
        +47:12+2 number

      
        
        185
        +47:15+3 type

      
        
        186
        +47:20+8 comment

      
        
        187
        +49:0+12 keyword

      
        
        188
        +49:13+1 property

      
        
        189
        +50:0+12 keyword

      
        
        190
        +50:13+1 property

      
        
        191
        +50:16+14 comment

      
        
        192
        +52:0+7 keyword

      
        
        193
        +52:8+11 string

      
        
        194
        +53:0+7 keyword

      
        
        195
        +53:8+11 string

      
        
        196
        +53:21+14 comment

      
        
        197
        +55:0+9 keyword

      
        
        198
        +55:10+3 type

      
        
        199
        +56:0+9 keyword

      
        
        200
        +56:10+3 type

      
        
        201
        +56:15+16 comment

      
        
        202
        +58:0+9 keyword

      
        
        203
        +59:9+3 type

      
        
        204
        +59:13+14 number

      
        
        205
        +60:0+9 keyword

      
        
        206
        +60:15+20 comment

      
        
        207
        +61:9+3 type

      
        
        208
        +61:13+14 number

      
        
        209
        +63:0+9 keyword

      
        
        210
        +63:10+8 number

      
        
        211
        +63:19+3 type

      
        
        212
        +64:0+9 keyword

      
        
        213
        +64:10+8 number

      
        
        214
        +64:19+3 type

      
        
        215
        +64:24+23 comment

      
        
        216
        +65:0+9 keyword

      
        
        217
        +66:9+8 number

      
        
        218
        +66:18+3 type

      
        
        219
        +67:0+9 keyword

      
        
        220
        +67:15+20 comment

      
        
        221
        +68:9+8 number

      
        
        222
        +68:18+3 type

      
A internal/lsp/testdata/semantic-empty.txtar
···
        
        1
        +-- in.journal --

      
        
        2
        +-- expect --

      
A internal/lsp/testdata/semantic-journal.txtar
···
        
        1
        +-- in.journal --

      
        
        2
        +; a comment

      
        
        3
        +;

      
        
        4
        +# hash comment

      
        
        5
        +% percent comment

      
        
        6
        +comment

      
        
        7
        +block text

      
        
        8
        +end comment

      
        
        9
        +comment tag:hidden

      
        
        10
        +stuff

      
        
        11
        +end

      
        
        12
        +

      
        
        13
        +* 2024-01-15 comment

      
        
        14
        +2024-01-15 real

      
        
        15
        +

      
        
        16
        +2024/01/01=2024/01/02 groceries

      
        
        17
        +  ! assets:cash  $10

      
        
        18
        +  * (income:gift)  $-10

      
        
        19
        +

      
        
        20
        +2024.01.01 "quoted payee"

      
        
        21
        +	expenses:food  10.00 "EUR"

      
        
        22
        +	assets:checking

      
        
        23
        +

      
        
        24
        +2024-01-01 groceries

      
        
        25
        +  expenses:food  1E-6 BTC

      
        
        26
        +  expenses:supplies  EUR 1E3

      
        
        27
        +  expenses:rest  -2.5e-2 $

      
        
        28
        +  expenses:meals  1 000.00 UAH

      
        
        29
        +  expenses:gifts  1'000.00 USD

      
        
        30
        +  expenses:donuts  -2_000.00 USD

      
        
        31
        +  expenses:food  €10.00

      
        
        32
        +  expenses:food  £5.00

      
        
        33
        +  expenses:food  ₹700.00

      
        
        34
        +  expenses:account name  20.00

      
        
        35
        +  assets:cash

      
        
        36
        +

      
        
        37
        +2024-01-15 t

      
        
        38
        +    a  $10

      
        
        39
        +    b  $-10

      
        
        40
        +

      
        
        41
        +2024-01-15 * (CODE) Cafe | lunch ; weekly treat

      
        
        42
        +    ; header note

      
        
        43
        +    expenses:food:cafe    $12.50

      
        
        44
        +    expenses:food         $-2.50  ; discount

      
        
        45
        +    assets:cash

      
        
        46
        +    [virtual:budget]      $20

      
        
        47
        +    (virtual:unbalanced)  $5

      
        
        48
        +    assets:checking       50 = 100

      
        
        49
        +    assets:savings        10 == 100

      
        
        50
        +    assets:brokerage      5 := 100

      
        
        51
        +    ; continuation comment

      
        
        52
        +

      
        
        53
        +2024-01-16 ! todo  ; inline note

      
        
        54
        +    expenses:rent

      
        
        55
        +    assets:checking  $800

      
        
        56
        +

      
        
        57
        +2024-01-17 * done

      
        
        58
        +    a  $1 = $100

      
        
        59
        +    b  $2 == $100

      
        
        60
        +    c  $3 =* $100

      
        
        61
        +    d  $4 ==* $100

      
        
        62
        +    e  $5 := $100

      
        
        63
        +

      
        
        64
        +2024-01-18 test

      
        
        65
        +  a  10 @ 5

      
        
        66
        +  b  10 @@ 50

      
        
        67
        +  c  50 USD

      
        
        68
        +  d  10 @ 5 = 100

      
        
        69
        +  e  10 === 10

      
        
        70
        +  f  10 @@ 5 =* 100

      
        
        71
        +  g  10 = 100 @ 5

      
        
        72
        +

      
        
        73
        +2024-01-19 payee | note

      
        
        74
        +2024-01-20 (CODE) payee

      
        
        75
        +

      
        
        76
        +2020-02-12 * asdf

      
        
        77
        +  assets

      
        
        78
        +  expenses  ; comment text

      
        
        79
        +

      
        
        80
        +2020-02-14 * more

      
        
        81
        +

      
        
        82
        +2024-01-15 午餐

      
        
        83
        +; Привет мир

      
        
        84
        +2024-01-15 Такси

      
        
        85
        +

      
        
        86
        +~ monthly

      
        
        87
        +  a  $10

      
        
        88
        +

      
        
        89
        +~ monthly from 2023-04-15 to 2023-06-16

      
        
        90
        +  expenses:utilities  $400

      
        
        91
        +  assets:bank:checking

      
        
        92
        +

      
        
        93
        +~ monthly  Next year blah blah ; period note

      
        
        94
        +  expenses:food  $100

      
        
        95
        +  assets:checking

      
        
        96
        +

      
        
        97
        += expr

      
        
        98
        +  a  $10

      
        
        99
        +

      
        
        100
        += ^income

      
        
        101
        +  (liabilities:tax)  *.33

      
        
        102
        +

      
        
        103
        += expenses:gifts

      
        
        104
        +  budget:gifts  *-1

      
        
        105
        +  assets:budget  *1

      
        
        106
        +

      
        
        107
        += income:salary

      
        
        108
        +    budget:savings  (amount * 0.5)

      
        
        109
        +

      
        
        110
        += ^gifts  ; automated note

      
        
        111
        +  ; automated header

      
        
        112
        +  expenses:gifts  $25.50

      
        
        113
        +  assets:cash

      
        
        114
        +

      
        
        115
        +2024-01-21 cost with commodity

      
        
        116
        +  a  10 @ 5 EUR

      
        
        117
        +  b  10 @@ 5 USD

      
        
        118
        +  c  $-5.00 @ 0.9 BTC

      
        
        119
        +

      
        
        120
        +-- expect --

      
        
        121
        +0:0+12 comment

      
        
        122
        +1:0+2 comment

      
        
        123
        +2:0+15 comment

      
        
        124
        +3:0+18 comment

      
        
        125
        +4:0+31 comment

      
        
        126
        +7:0+29 comment

      
        
        127
        +11:0+21 comment

      
        
        128
        +12:0+10 class

      
        
        129
        +12:11+4 property

      
        
        130
        +14:0+10 class

      
        
        131
        +14:11+10 class

      
        
        132
        +14:22+9 property

      
        
        133
        +15:2+2 operator

      
        
        134
        +15:4+11 namespace

      
        
        135
        +15:17+1 type

      
        
        136
        +15:18+2 number

      
        
        137
        +16:2+2 operator

      
        
        138
        +16:4+1 operator abstract

      
        
        139
        +16:5+11 namespace

      
        
        140
        +16:16+1 operator abstract

      
        
        141
        +16:19+1 type

      
        
        142
        +16:20+3 number negative

      
        
        143
        +18:0+10 class

      
        
        144
        +18:11+14 property

      
        
        145
        +19:1+13 namespace

      
        
        146
        +19:16+5 number

      
        
        147
        +19:22+5 type

      
        
        148
        +20:1+15 namespace

      
        
        149
        +22:0+10 class

      
        
        150
        +22:11+9 property

      
        
        151
        +23:2+13 namespace

      
        
        152
        +23:17+4 number

      
        
        153
        +23:22+3 type

      
        
        154
        +24:2+17 namespace

      
        
        155
        +24:21+3 type

      
        
        156
        +24:25+3 number

      
        
        157
        +25:2+13 namespace

      
        
        158
        +25:17+7 number negative

      
        
        159
        +25:25+1 type

      
        
        160
        +26:2+14 namespace

      
        
        161
        +26:18+8 number

      
        
        162
        +26:27+3 type

      
        
        163
        +27:2+14 namespace

      
        
        164
        +27:18+8 number

      
        
        165
        +27:27+3 type

      
        
        166
        +28:2+15 namespace

      
        
        167
        +28:19+9 number negative

      
        
        168
        +28:29+3 type

      
        
        169
        +29:2+13 namespace

      
        
        170
        +29:17+1 type

      
        
        171
        +29:18+5 number

      
        
        172
        +30:2+13 namespace

      
        
        173
        +30:17+1 type

      
        
        174
        +30:18+4 number

      
        
        175
        +31:2+13 namespace

      
        
        176
        +31:17+1 type

      
        
        177
        +31:18+6 number

      
        
        178
        +32:2+21 namespace

      
        
        179
        +32:25+5 number

      
        
        180
        +33:2+11 namespace

      
        
        181
        +35:0+10 class

      
        
        182
        +35:11+1 property

      
        
        183
        +36:4+1 namespace

      
        
        184
        +36:7+1 type

      
        
        185
        +36:8+2 number

      
        
        186
        +37:4+1 namespace

      
        
        187
        +37:7+1 type

      
        
        188
        +37:8+3 number negative

      
        
        189
        +39:0+10 class

      
        
        190
        +39:11+2 operator

      
        
        191
        +39:13+6 string

      
        
        192
        +39:20+4 property

      
        
        193
        +39:27+6 property

      
        
        194
        +39:33+14 comment

      
        
        195
        +40:4+14 comment

      
        
        196
        +41:4+18 namespace

      
        
        197
        +41:26+1 type

      
        
        198
        +41:27+5 number

      
        
        199
        +42:4+13 namespace

      
        
        200
        +42:26+1 type

      
        
        201
        +42:27+5 number negative

      
        
        202
        +42:34+10 comment

      
        
        203
        +43:4+11 namespace

      
        
        204
        +44:4+1 operator abstract

      
        
        205
        +44:5+14 namespace

      
        
        206
        +44:19+1 operator abstract

      
        
        207
        +44:26+1 type

      
        
        208
        +44:27+2 number

      
        
        209
        +45:4+1 operator abstract

      
        
        210
        +45:5+18 namespace

      
        
        211
        +45:23+1 operator abstract

      
        
        212
        +45:26+1 type

      
        
        213
        +45:27+1 number

      
        
        214
        +46:4+15 namespace

      
        
        215
        +46:26+2 number

      
        
        216
        +46:29+1 operator

      
        
        217
        +46:31+3 number

      
        
        218
        +47:4+14 namespace

      
        
        219
        +47:26+2 number

      
        
        220
        +47:29+2 operator

      
        
        221
        +47:32+3 number

      
        
        222
        +48:4+16 namespace

      
        
        223
        +48:26+1 number

      
        
        224
        +48:28+2 operator

      
        
        225
        +48:31+3 number

      
        
        226
        +49:4+23 comment

      
        
        227
        +51:0+10 class

      
        
        228
        +51:11+2 operator

      
        
        229
        +51:13+4 property

      
        
        230
        +51:19+13 comment

      
        
        231
        +52:4+13 namespace

      
        
        232
        +53:4+15 namespace

      
        
        233
        +53:21+1 type

      
        
        234
        +53:22+3 number

      
        
        235
        +55:0+10 class

      
        
        236
        +55:11+2 operator

      
        
        237
        +55:13+4 property

      
        
        238
        +56:4+1 namespace

      
        
        239
        +56:7+1 type

      
        
        240
        +56:8+1 number

      
        
        241
        +56:10+1 operator

      
        
        242
        +56:12+1 type

      
        
        243
        +56:13+3 number

      
        
        244
        +57:4+1 namespace

      
        
        245
        +57:7+1 type

      
        
        246
        +57:8+1 number

      
        
        247
        +57:10+2 operator

      
        
        248
        +57:13+1 type

      
        
        249
        +57:14+3 number

      
        
        250
        +58:4+1 namespace

      
        
        251
        +58:7+1 type

      
        
        252
        +58:8+1 number

      
        
        253
        +58:10+2 operator

      
        
        254
        +58:13+1 type

      
        
        255
        +58:14+3 number

      
        
        256
        +59:4+1 namespace

      
        
        257
        +59:7+1 type

      
        
        258
        +59:8+1 number

      
        
        259
        +59:10+3 operator

      
        
        260
        +59:14+1 type

      
        
        261
        +59:15+3 number

      
        
        262
        +60:4+1 namespace

      
        
        263
        +60:7+1 type

      
        
        264
        +60:8+1 number

      
        
        265
        +60:10+2 operator

      
        
        266
        +60:13+1 type

      
        
        267
        +60:14+3 number

      
        
        268
        +62:0+10 class

      
        
        269
        +62:11+4 property

      
        
        270
        +63:2+1 namespace

      
        
        271
        +63:5+2 number

      
        
        272
        +63:8+1 operator

      
        
        273
        +63:10+1 number

      
        
        274
        +64:2+1 namespace

      
        
        275
        +64:5+2 number

      
        
        276
        +64:8+2 operator

      
        
        277
        +64:11+2 number

      
        
        278
        +65:2+1 namespace

      
        
        279
        +65:5+2 number

      
        
        280
        +65:8+3 type

      
        
        281
        +66:2+1 namespace

      
        
        282
        +66:5+2 number

      
        
        283
        +66:8+1 operator

      
        
        284
        +66:10+1 number

      
        
        285
        +66:12+1 operator

      
        
        286
        +66:14+3 number

      
        
        287
        +67:2+1 namespace

      
        
        288
        +67:5+2 number

      
        
        289
        +67:8+3 operator

      
        
        290
        +67:12+2 number

      
        
        291
        +68:2+1 namespace

      
        
        292
        +68:5+2 number

      
        
        293
        +68:8+2 operator

      
        
        294
        +68:11+1 number

      
        
        295
        +68:13+2 operator

      
        
        296
        +68:16+3 number

      
        
        297
        +69:2+1 namespace

      
        
        298
        +69:5+2 number

      
        
        299
        +69:8+1 operator

      
        
        300
        +69:10+3 number

      
        
        301
        +69:14+1 operator

      
        
        302
        +69:16+1 number

      
        
        303
        +71:0+10 class

      
        
        304
        +71:11+5 property

      
        
        305
        +71:19+4 property

      
        
        306
        +72:0+10 class

      
        
        307
        +72:11+6 string

      
        
        308
        +72:18+5 property

      
        
        309
        +74:0+10 class

      
        
        310
        +74:11+2 operator

      
        
        311
        +74:13+4 property

      
        
        312
        +75:2+6 namespace

      
        
        313
        +76:2+8 namespace

      
        
        314
        +76:12+14 comment

      
        
        315
        +78:0+10 class

      
        
        316
        +78:11+2 operator

      
        
        317
        +78:13+4 property

      
        
        318
        +80:0+10 class

      
        
        319
        +80:11+2 property

      
        
        320
        +81:0+13 comment

      
        
        321
        +82:0+10 class

      
        
        322
        +82:11+5 property

      
        
        323
        +84:0+1 operator

      
        
        324
        +84:2+7 property

      
        
        325
        +85:2+1 namespace

      
        
        326
        +85:5+1 type

      
        
        327
        +85:6+2 number

      
        
        328
        +87:0+1 operator

      
        
        329
        +87:2+13 property

      
        
        330
        +87:15+10 class

      
        
        331
        +87:25+4 property

      
        
        332
        +87:29+10 class

      
        
        333
        +88:2+18 namespace

      
        
        334
        +88:22+1 type

      
        
        335
        +88:23+3 number

      
        
        336
        +89:2+20 namespace

      
        
        337
        +91:0+1 operator

      
        
        338
        +91:2+7 property

      
        
        339
        +91:11+19 property

      
        
        340
        +91:31+13 comment

      
        
        341
        +92:2+13 namespace

      
        
        342
        +92:17+1 type

      
        
        343
        +92:18+3 number

      
        
        344
        +93:2+15 namespace

      
        
        345
        +95:0+1 operator

      
        
        346
        +95:2+4 string

      
        
        347
        +96:2+1 namespace

      
        
        348
        +96:5+1 type

      
        
        349
        +96:6+2 number

      
        
        350
        +98:0+1 operator

      
        
        351
        +98:2+7 string

      
        
        352
        +99:2+1 operator abstract

      
        
        353
        +99:3+15 namespace

      
        
        354
        +99:18+1 operator abstract

      
        
        355
        +99:22+3 number

      
        
        356
        +101:0+1 operator

      
        
        357
        +101:2+14 string

      
        
        358
        +102:2+12 namespace

      
        
        359
        +102:17+2 number negative

      
        
        360
        +103:2+13 namespace

      
        
        361
        +103:18+1 number

      
        
        362
        +105:0+1 operator

      
        
        363
        +105:2+13 string

      
        
        364
        +106:4+14 namespace

      
        
        365
        +106:20+14 number

      
        
        366
        +108:0+1 operator

      
        
        367
        +108:2+8 string

      
        
        368
        +108:10+16 comment

      
        
        369
        +109:2+19 comment

      
        
        370
        +110:2+14 namespace

      
        
        371
        +110:18+1 type

      
        
        372
        +110:19+5 number

      
        
        373
        +111:2+11 namespace

      
        
        374
        +113:0+10 class

      
        
        375
        +113:11+19 property

      
        
        376
        +114:2+1 namespace

      
        
        377
        +114:5+2 number

      
        
        378
        +114:8+1 operator

      
        
        379
        +114:10+1 number

      
        
        380
        +114:12+3 type

      
        
        381
        +115:2+1 namespace

      
        
        382
        +115:5+2 number

      
        
        383
        +115:8+2 operator

      
        
        384
        +115:11+1 number

      
        
        385
        +115:13+3 type

      
        
        386
        +116:2+1 namespace

      
        
        387
        +116:5+1 type

      
        
        388
        +116:6+5 number negative

      
        
        389
        +116:12+1 operator

      
        
        390
        +116:14+3 number

      
        
        391
        +116:18+3 type

      
A internal/lsp/testdata/semantic-unparseable.txtar
···
        
        1
        +-- in.journal --

      
        
        2
        +garbage 123 @@$

      
        
        3
        +; note after garbage

      
        
        4
        +# hash note

      
        
        5
        +% percent note

      
        
        6
        +* star note

      
        
        7
        +2024-01-15 ! (CODE) payee

      
        
        8
        +  expenses:food  $10.50 @ 2 USD

      
        
        9
        +  assets:cash  !!

      
        
        10
        += expr

      
        
        11
        +~ monthly

      
        
        12
        +account foo

      
        
        13
        +N $

      
        
        14
        +P 2024-01-15 USD 1.05

      
        
        15
        +garbage $$$$ 123

      
        
        16
        +

      
        
        17
        +-- expect --

      
        
        18
        +0:0+7 string

      
        
        19
        +0:8+7 string

      
        
        20
        +1:0+20 comment

      
        
        21
        +2:0+11 comment

      
        
        22
        +3:0+14 comment

      
        
        23
        +4:0+11 comment

      
        
        24
        +5:0+10 class

      
        
        25
        +5:11+1 operator

      
        
        26
        +5:13+6 string

      
        
        27
        +5:20+5 string

      
        
        28
        +6:2+8 string

      
        
        29
        +6:10+1 string

      
        
        30
        +6:11+4 string

      
        
        31
        +6:17+1 type

      
        
        32
        +6:18+5 number

      
        
        33
        +6:24+1 operator

      
        
        34
        +6:26+1 number

      
        
        35
        +6:28+3 type

      
        
        36
        +7:2+6 string

      
        
        37
        +7:8+1 string

      
        
        38
        +7:9+4 string

      
        
        39
        +7:15+2 string

      
        
        40
        +8:0+1 operator

      
        
        41
        +8:2+4 string

      
        
        42
        +9:0+1 string

      
        
        43
        +9:2+7 string

      
        
        44
        +10:0+7 keyword

      
        
        45
        +10:8+3 string

      
        
        46
        +11:0+1 keyword

      
        
        47
        +11:2+1 type

      
        
        48
        +12:0+1 keyword

      
        
        49
        +12:2+10 class

      
        
        50
        +12:13+3 type

      
        
        51
        +12:17+4 number

      
        
        52
        +13:0+7 string

      
        
        53
        +13:8+8 string

      
M internal/lsp/textdocument_format.go
···
        6
        6
         	"strings"

      
        7
        7
         

      
        8
        8
         	"go.lsp.dev/protocol"

      
        
        9
        +

      
        9
        10
         	"olexsmir.xyz/clerk/journal/lexer"

      
        10
        11
         	"olexsmir.xyz/clerk/journal/parser"

      
        11
        12
         )

      
A internal/lsp/textdocument_semantic.go
···
        
        1
        +package lsp

      
        
        2
        +

      
        
        3
        +import (

      
        
        4
        +	"context"

      
        
        5
        +	"slices"

      
        
        6
        +

      
        
        7
        +	"go.lsp.dev/protocol"

      
        
        8
        +	"go.lsp.dev/uri"

      
        
        9
        +

      
        
        10
        +	"olexsmir.xyz/clerk/internal/lsp/lsputil"

      
        
        11
        +	"olexsmir.xyz/clerk/journal/ast"

      
        
        12
        +	"olexsmir.xyz/clerk/journal/lexer"

      
        
        13
        +	"olexsmir.xyz/clerk/journal/token"

      
        
        14
        +)

      
        
        15
        +

      
        
        16
        +func (s *server) SemanticTokensFull(ctx context.Context, params *protocol.SemanticTokensParams) (*protocol.SemanticTokens, error) {

      
        
        17
        +	tokens, ok := s.tokensForDoc(params.TextDocument.URI)

      
        
        18
        +	if !ok {

      
        
        19
        +		return &protocol.SemanticTokens{}, nil

      
        
        20
        +	}

      
        
        21
        +	return &protocol.SemanticTokens{Data: encodeSemTokens(tokens)}, nil

      
        
        22
        +}

      
        
        23
        +

      
        
        24
        +func (s *server) SemanticTokensRange(ctx context.Context, params *protocol.SemanticTokensRangeParams) (*protocol.SemanticTokens, error) {

      
        
        25
        +	tokens, ok := s.tokensForDoc(params.TextDocument.URI)

      
        
        26
        +	if !ok {

      
        
        27
        +		return &protocol.SemanticTokens{}, nil

      
        
        28
        +	}

      
        
        29
        +	start := int(params.Range.Start.Line)

      
        
        30
        +	end := int(params.Range.End.Line)

      
        
        31
        +	var filtered []semanticToken

      
        
        32
        +	for _, t := range tokens {

      
        
        33
        +		if t.line >= uint32(start) && t.line <= uint32(end) {

      
        
        34
        +			filtered = append(filtered, t)

      
        
        35
        +		}

      
        
        36
        +	}

      
        
        37
        +	return &protocol.SemanticTokens{Data: encodeSemTokens(filtered)}, nil

      
        
        38
        +}

      
        
        39
        +

      
        
        40
        +func (s *server) tokensForDoc(doc uri.URI) ([]semanticToken, bool) {

      
        
        41
        +	state, ok := s.getDocState(doc)

      
        
        42
        +	if !ok {

      
        
        43
        +		return nil, false

      
        
        44
        +	}

      
        
        45
        +	return tokenizeForSemantics(state.text, state.journal), true

      
        
        46
        +}

      
        
        47
        +

      
        
        48
        +// Implementation

      
        
        49
        +

      
        
        50
        +const (

      
        
        51
        +	SemanticDirective = iota

      
        
        52
        +	SemanticDate

      
        
        53
        +	SemanticAccount

      
        
        54
        +	SemanticCommodity

      
        
        55
        +	SemanticAmount

      
        
        56
        +	SemanticStatus

      
        
        57
        +	SemanticComment

      
        
        58
        +	SemString

      
        
        59
        +	SemOperator

      
        
        60
        +	SemProperty

      
        
        61
        +

      
        
        62
        +	semTypeCount // used to ensure we dont go out of bound

      
        
        63
        +)

      
        
        64
        +

      
        
        65
        +var tokenTypeStrings = []string{

      
        
        66
        +	string(protocol.SemanticTokenTypesKeyword),   // directive

      
        
        67
        +	string(protocol.SemanticTokenTypesClass),     // date

      
        
        68
        +	string(protocol.SemanticTokenTypesNamespace), // account

      
        
        69
        +	string(protocol.SemanticTokenTypesType),      // commodity

      
        
        70
        +	string(protocol.SemanticTokenTypesNumber),    // amount

      
        
        71
        +	string(protocol.SemanticTokenTypesOperator),  // status

      
        
        72
        +	string(protocol.SemanticTokenTypesComment),   // comment

      
        
        73
        +	string(protocol.SemanticTokenTypesString),    // string

      
        
        74
        +	string(protocol.SemanticTokenTypesOperator),  // operator

      
        
        75
        +	string(protocol.SemanticTokenTypesProperty),  // property

      
        
        76
        +}

      
        
        77
        +

      
        
        78
        +const (

      
        
        79
        +	modifierAbstract = 1 << 0 // virtual account

      
        
        80
        +	modifierNegative = 1 << 1 // negative amount

      
        
        81
        +)

      
        
        82
        +

      
        
        83
        +var modifierStrings = []string{

      
        
        84
        +	"abstract", // bit 0

      
        
        85
        +	"negative", // bit 1

      
        
        86
        +}

      
        
        87
        +

      
        
        88
        +func getSemanticTokensLegend() protocol.SemanticTokensLegend {

      
        
        89
        +	return protocol.SemanticTokensLegend{

      
        
        90
        +		TokenTypes:     tokenTypeStrings,

      
        
        91
        +		TokenModifiers: modifierStrings,

      
        
        92
        +	}

      
        
        93
        +}

      
        
        94
        +

      
        
        95
        +type semanticToken struct {

      
        
        96
        +	line      uint32 // 0-based

      
        
        97
        +	col       uint32 // 0-based UTF-16

      
        
        98
        +	length    uint32 // UTF-16 code units

      
        
        99
        +	tokenType uint32

      
        
        100
        +	modifiers uint32

      
        
        101
        +}

      
        
        102
        +

      
        
        103
        +func tokenizeForSemantics(content string, j *ast.Journal) []semanticToken {

      
        
        104
        +	if j == nil || len(j.Errors) > 0 {

      
        
        105
        +		return semLexerFallback(content)

      
        
        106
        +	}

      
        
        107
        +	return visitEntries(content, j)

      
        
        108
        +}

      
        
        109
        +

      
        
        110
        +// visitEntries walks the AST and emits semantic tokens.

      
        
        111
        +func visitEntries(content string, j *ast.Journal) []semanticToken {

      
        
        112
        +	var out []semanticToken

      
        
        113
        +	emit := func(s token.Span, tokType uint32, mods uint32) {

      
        
        114
        +		if s.Start.Offset >= s.End.Offset {

      
        
        115
        +			return

      
        
        116
        +		}

      
        
        117
        +		out = append(out, semanticTokenAt(content, s, tokType, mods))

      
        
        118
        +	}

      
        
        119
        +	for _, e := range j.Entries {

      
        
        120
        +		visitEntry(content, e, emit)

      
        
        121
        +	}

      
        
        122
        +	return out

      
        
        123
        +}

      
        
        124
        +

      
        
        125
        +// semanticTokenAt converts a source span into a semantic token.

      
        
        126
        +func semanticTokenAt(content string, s token.Span, tokType, mods uint32) semanticToken {

      
        
        127
        +	line, col := lsputil.LineCol(content, s.Start.Offset)

      
        
        128
        +	length := lsputil.Utf16Len(content, s.Start.Offset, s.End.Offset)

      
        
        129
        +	return semanticToken{

      
        
        130
        +		line:      uint32(line),

      
        
        131
        +		col:       uint32(col),

      
        
        132
        +		length:    uint32(length),

      
        
        133
        +		tokenType: tokType,

      
        
        134
        +		modifiers: mods,

      
        
        135
        +	}

      
        
        136
        +}

      
        
        137
        +

      
        
        138
        +func visitEntry(content string, e ast.Entry, emit semEmitFn) {

      
        
        139
        +	switch e := e.(type) {

      
        
        140
        +	case *ast.Transaction:

      
        
        141
        +		visitTransaction(content, e, emit)

      
        
        142
        +	case *ast.PeriodicTransaction:

      
        
        143
        +		visitPeriodicTransaction(content, e, emit)

      
        
        144
        +	case *ast.AutomatedTransaction:

      
        
        145
        +		visitAutomatedTransaction(content, e, emit)

      
        
        146
        +	case *ast.AccountDirective:

      
        
        147
        +		emit(directiveKeyword(e.Span, "account"), SemanticDirective, 0)

      
        
        148
        +		emit(e.Account.Span, SemanticAccount, 0)

      
        
        149
        +		if e.Comment != nil {

      
        
        150
        +			emit(e.Comment.Span, SemanticComment, 0)

      
        
        151
        +		}

      
        
        152
        +	case *ast.CommodityDirective:

      
        
        153
        +		emit(directiveKeyword(e.Span, "commodity"), SemanticDirective, 0)

      
        
        154
        +		if e.Format.Span.End.Offset > 0 {

      
        
        155
        +			semEmitAmount(content, &e.Format, emit)

      
        
        156
        +		} else if e.CommoditySpan.Start.Offset > 0 && e.CommoditySpan.End.Offset > 0 {

      
        
        157
        +			emit(e.CommoditySpan, SemanticCommodity, 0)

      
        
        158
        +		}

      
        
        159
        +		if e.Comment != nil {

      
        
        160
        +			emit(e.Comment.Span, SemanticComment, 0)

      
        
        161
        +		}

      
        
        162
        +	case *ast.IncludeDirective:

      
        
        163
        +		emitDirective(content, e.Span, len("include"), SemString, e.Comment, emit)

      
        
        164
        +	case *ast.PayeeDirective:

      
        
        165
        +		emitDirective(content, e.Span, len("payee"), SemProperty, e.Comment, emit)

      
        
        166
        +	case *ast.TagDirective:

      
        
        167
        +		emitDirective(content, e.Span, len("tag"), SemProperty, e.Comment, emit)

      
        
        168
        +	case *ast.AliasDirective:

      
        
        169
        +		emit(directiveKeyword(e.Span, "alias"), SemanticDirective, 0)

      
        
        170
        +		emit(e.From.Span, SemanticAccount, 0)

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

      
        
        172
        +			emit(op, SemOperator, 0)

      
        
        173
        +		}

      
        
        174
        +		emit(e.To.Span, SemanticAccount, 0)

      
        
        175
        +		if e.Comment != nil {

      
        
        176
        +			emit(e.Comment.Span, SemanticComment, 0)

      
        
        177
        +		}

      
        
        178
        +	case *ast.YearDirective:

      
        
        179
        +		kwLen := len("year")

      
        
        180
        +		if content[e.Span.Start.Offset] == 'Y' {

      
        
        181
        +			kwLen = 1

      
        
        182
        +		}

      
        
        183
        +		emitDirective(content, e.Span, kwLen, SemProperty, e.Comment, emit)

      
        
        184
        +	case *ast.DecimalMarkDirective:

      
        
        185
        +		emitDirective(content, e.Span, len("decimal-mark"), SemProperty, e.Comment, emit)

      
        
        186
        +	case *ast.DefaultCommodityDirective:

      
        
        187
        +		emit(directiveKeyword(e.Span, "D"), SemanticDirective, 0)

      
        
        188
        +		semEmitAmount(content, &e.Amount, emit)

      
        
        189
        +		if e.Comment != nil {

      
        
        190
        +			emit(e.Comment.Span, SemanticComment, 0)

      
        
        191
        +		}

      
        
        192
        +	case *ast.MarketPriceDirective:

      
        
        193
        +		emit(directiveKeyword(e.Span, "P"), SemanticDirective, 0)

      
        
        194
        +		emit(e.DateTime.Date.Span, SemanticDate, 0)

      
        
        195
        +		if e.DateTime.Time != nil {

      
        
        196
        +			emit(e.DateTime.Time.Span, SemanticDate, 0)

      
        
        197
        +		}

      
        
        198
        +		// commodity: text between the date (or time) and the amount

      
        
        199
        +		commStart := e.DateTime.Date.Span.End.Offset

      
        
        200
        +		if e.DateTime.Time != nil {

      
        
        201
        +			commStart = e.DateTime.Time.Span.End.Offset

      
        
        202
        +		}

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

      
        
        204
        +			emit(comm, SemanticCommodity, 0)

      
        
        205
        +		}

      
        
        206
        +		semEmitAmount(content, &e.Amount, emit)

      
        
        207
        +		if e.Comment != nil {

      
        
        208
        +			emit(e.Comment.Span, SemanticComment, 0)

      
        
        209
        +		}

      
        
        210
        +	case *ast.ConversionDirective:

      
        
        211
        +		emit(directiveKeyword(e.Span, "C"), SemanticDirective, 0)

      
        
        212
        +		semEmitAmount(content, &e.From, emit)

      
        
        213
        +		// = operator: text between the two amounts

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

      
        
        215
        +			emit(op, SemOperator, 0)

      
        
        216
        +		}

      
        
        217
        +		semEmitAmount(content, &e.To, emit)

      
        
        218
        +		if e.Comment != nil {

      
        
        219
        +			emit(e.Comment.Span, SemanticComment, 0)

      
        
        220
        +		}

      
        
        221
        +	case *ast.Comment:

      
        
        222
        +		emit(e.Span, SemanticComment, 0)

      
        
        223
        +	case *ast.CommentBlockDirective:

      
        
        224
        +		emit(e.Span, SemanticComment, 0)

      
        
        225
        +	case *ast.IgnoredDirective:

      
        
        226
        +		emitDirective(content, e.Span, len("N"), SemProperty, e.Comment, emit)

      
        
        227
        +	case *ast.ApplyDirective:

      
        
        228
        +		emitDirective(content, e.Span, len("apply"), SemProperty, e.Comment, emit)

      
        
        229
        +	case *ast.EndDirective:

      
        
        230
        +		emitDirective(content, e.Span, len("end"), SemProperty, e.Comment, emit)

      
        
        231
        +	case *ast.BlankLine:

      
        
        232
        +	}

      
        
        233
        +}

      
        
        234
        +

      
        
        235
        +func visitTransaction(content string, t *ast.Transaction, emit semEmitFn) {

      
        
        236
        +	emit(t.Date.Span, SemanticDate, 0)

      
        
        237
        +	if t.SecondDate != nil {

      
        
        238
        +		emit(t.SecondDate.Span, SemanticDate, 0)

      
        
        239
        +	}

      
        
        240
        +	if t.Status.Value != ast.StatusNone {

      
        
        241
        +		emit(t.Status.Span, SemanticStatus, 0)

      
        
        242
        +	}

      
        
        243
        +	if t.Code != nil {

      
        
        244
        +		emit(t.Code.Span, SemString, 0)

      
        
        245
        +	}

      
        
        246
        +	if t.Payee != nil {

      
        
        247
        +		emit(t.Payee.Span, SemProperty, 0)

      
        
        248
        +	}

      
        
        249
        +	if t.Note != nil {

      
        
        250
        +		emit(t.Note.Span, SemProperty, 0)

      
        
        251
        +	}

      
        
        252
        +	if t.Comment != nil {

      
        
        253
        +		emit(t.Comment.Span, SemanticComment, 0)

      
        
        254
        +	}

      
        
        255
        +	for i := range t.HeaderComments {

      
        
        256
        +		emit(t.HeaderComments[i].Span, SemanticComment, 0)

      
        
        257
        +	}

      
        
        258
        +	for _, p := range t.Postings {

      
        
        259
        +		visitPosting(content, p, emit)

      
        
        260
        +	}

      
        
        261
        +}

      
        
        262
        +

      
        
        263
        +func visitPeriodicTransaction(content string, pt *ast.PeriodicTransaction, emit semEmitFn) {

      
        
        264
        +	// ~ operator is at the start of the period span

      
        
        265
        +	emit(offsetSpan(pt.Span.Start.File, pt.Span.Start.Offset, pt.Span.Start.Offset+1), SemOperator, 0)

      
        
        266
        +

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

      
        
        268
        +	if pt.Period.Span.End.Offset > pt.Period.Span.Start.Offset {

      
        
        269
        +		var dates []*ast.Date

      
        
        270
        +		if pt.Period.From != nil {

      
        
        271
        +			dates = append(dates, pt.Period.From)

      
        
        272
        +		}

      
        
        273
        +		if pt.Period.To != nil {

      
        
        274
        +			dates = append(dates, pt.Period.To)

      
        
        275
        +		}

      
        
        276
        +		pos := pt.Period.Span.Start.Offset

      
        
        277
        +		for _, d := range dates {

      
        
        278
        +			if d.Span.Start.Offset > pos {

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

      
        
        280
        +			}

      
        
        281
        +			emit(d.Span, SemanticDate, 0)

      
        
        282
        +			pos = d.Span.End.Offset

      
        
        283
        +		}

      
        
        284
        +		if pos < pt.Period.Span.End.Offset {

      
        
        285
        +			emit(offsetSpan(pt.Period.Span.Start.File, pos, pt.Period.Span.End.Offset), SemProperty, 0)

      
        
        286
        +		}

      
        
        287
        +	}

      
        
        288
        +	if pt.Description != nil {

      
        
        289
        +		emit(pt.Description.Span, SemProperty, 0)

      
        
        290
        +	}

      
        
        291
        +	if pt.Comment != nil {

      
        
        292
        +		emit(pt.Comment.Span, SemanticComment, 0)

      
        
        293
        +	}

      
        
        294
        +	for i := range pt.HeaderComments {

      
        
        295
        +		emit(pt.HeaderComments[i].Span, SemanticComment, 0)

      
        
        296
        +	}

      
        
        297
        +	for _, p := range pt.Postings {

      
        
        298
        +		visitPosting(content, p, emit)

      
        
        299
        +	}

      
        
        300
        +}

      
        
        301
        +

      
        
        302
        +func visitAutomatedTransaction(content string, at *ast.AutomatedTransaction, emit semEmitFn) {

      
        
        303
        +	// = operator is at the start of the expression span

      
        
        304
        +	emit(offsetSpan(at.Span.Start.File, at.Span.Start.Offset, at.Span.Start.Offset+1), SemOperator, 0)

      
        
        305
        +

      
        
        306
        +	if at.Expr.Value != "" {

      
        
        307
        +		emit(at.Expr.Span, SemString, 0)

      
        
        308
        +	}

      
        
        309
        +	if at.Comment != nil {

      
        
        310
        +		emit(at.Comment.Span, SemanticComment, 0)

      
        
        311
        +	}

      
        
        312
        +	for i := range at.HeaderComments {

      
        
        313
        +		emit(at.HeaderComments[i].Span, SemanticComment, 0)

      
        
        314
        +	}

      
        
        315
        +	for _, p := range at.Postings {

      
        
        316
        +		visitPosting(content, p, emit)

      
        
        317
        +	}

      
        
        318
        +}

      
        
        319
        +

      
        
        320
        +func visitPosting(content string, p *ast.Posting, emit semEmitFn) {

      
        
        321
        +	if p.Status.Value != ast.StatusNone {

      
        
        322
        +		emit(p.Status.Span, SemanticStatus, 0)

      
        
        323
        +	}

      
        
        324
        +

      
        
        325
        +	// virtual brackets

      
        
        326
        +	if p.Type == ast.PostingVirtualUnbalanced || p.Type == ast.PostingVirtualBalanced {

      
        
        327
        +		// opening bracket

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

      
        
        329
        +			if content[off] == '(' || content[off] == '[' {

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

      
        
        331
        +				emit(brSpan, SemOperator, modifierAbstract)

      
        
        332
        +				break

      
        
        333
        +			}

      
        
        334
        +		}

      
        
        335
        +		// closing bracket

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

      
        
        337
        +			if content[off] == ')' || content[off] == ']' {

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

      
        
        339
        +				emit(brSpan, SemOperator, modifierAbstract)

      
        
        340
        +				break

      
        
        341
        +			}

      
        
        342
        +		}

      
        
        343
        +	}

      
        
        344
        +

      
        
        345
        +	emit(p.Account.Span, SemanticAccount, 0)

      
        
        346
        +

      
        
        347
        +	if p.Amount != nil {

      
        
        348
        +		semEmitAmount(content, p.Amount, emit)

      
        
        349
        +	}

      
        
        350
        +	if p.Cost != nil {

      
        
        351
        +		semEmitCost(content, p.Cost, emit)

      
        
        352
        +	}

      
        
        353
        +	if p.Balance != nil {

      
        
        354
        +		semEmitBalanceAssertion(content, p.Balance, emit)

      
        
        355
        +	}

      
        
        356
        +	if p.Comment != nil {

      
        
        357
        +		emit(p.Comment.Span, SemanticComment, 0)

      
        
        358
        +	}

      
        
        359
        +	for i := range p.Comments {

      
        
        360
        +		emit(p.Comments[i].Span, SemanticComment, 0)

      
        
        361
        +	}

      
        
        362
        +}

      
        
        363
        +

      
        
        364
        +// directiveKeyword returns the span of the leading keyword on a directive line.

      
        
        365
        +func directiveKeyword(e token.Span, kw string) token.Span {

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

      
        
        367
        +}

      
        
        368
        +

      
        
        369
        +// directiveValue returns the trimmed span of the text after the keyword end

      
        
        370
        +// offset, up to the inline comment or the end of the line.

      
        
        371
        +func directiveValue(content string, e token.Span, comment *ast.Comment, kwEnd int) (token.Span, bool) {

      
        
        372
        +	end := e.End.Offset

      
        
        373
        +	if comment != nil {

      
        
        374
        +		end = comment.Span.Start.Offset

      
        
        375
        +	}

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

      
        
        377
        +}

      
        
        378
        +

      
        
        379
        +func semEmitAmount(content string, a *ast.Amount, emit semEmitFn) {

      
        
        380
        +	if a == nil {

      
        
        381
        +		return

      
        
        382
        +	}

      
        
        383
        +	hasCommodity := a.CommoditySpan.Start.Offset > 0 && a.CommoditySpan.End.Offset > 0

      
        
        384
        +	if hasCommodity && a.CommodityPos == ast.CommodityBefore {

      
        
        385
        +		emit(a.CommoditySpan, SemanticCommodity, 0)

      
        
        386
        +		semEmitQuantity(content, a, emit)

      
        
        387
        +		return

      
        
        388
        +	}

      
        
        389
        +	semEmitQuantity(content, a, emit)

      
        
        390
        +	if hasCommodity {

      
        
        391
        +		emit(a.CommoditySpan, SemanticCommodity, 0)

      
        
        392
        +	}

      
        
        393
        +}

      
        
        394
        +

      
        
        395
        +func semEmitQuantity(content string, a *ast.Amount, emit semEmitFn) {

      
        
        396
        +	qStart, qEnd := quantitySpan(content, a)

      
        
        397
        +	if qEnd <= qStart {

      
        
        398
        +		return

      
        
        399
        +	}

      
        
        400
        +	mods := uint32(0)

      
        
        401
        +	if a.IsNegative {

      
        
        402
        +		mods |= modifierNegative

      
        
        403
        +	}

      
        
        404
        +	emit(offsetSpan(a.Span.Start.File, qStart, qEnd), SemanticAmount, mods)

      
        
        405
        +}

      
        
        406
        +

      
        
        407
        +func quantitySpan(content string, a *ast.Amount) (int, int) {

      
        
        408
        +	start, end := a.Span.Start.Offset, a.Span.End.Offset

      
        
        409
        +	switch {

      
        
        410
        +	case a.Commodity == "":

      
        
        411
        +		// bare quantity

      
        
        412
        +	case a.CommodityPos == ast.CommodityBefore:

      
        
        413
        +		// "$50.00" or "$   50.00": quantity follows the commodity span

      
        
        414
        +		start = a.CommoditySpan.End.Offset

      
        
        415
        +	default: // CommodityAfter

      
        
        416
        +		// "50.00 USD" or "50.00     USD": quantity precedes the commodity

      
        
        417
        +		end = a.CommoditySpan.Start.Offset

      
        
        418
        +	}

      
        
        419
        +	for start < end && (content[start] == ' ' || content[start] == '\t') {

      
        
        420
        +		start++

      
        
        421
        +	}

      
        
        422
        +	for end > start && (content[end-1] == ' ' || content[end-1] == '\t') {

      
        
        423
        +		end--

      
        
        424
        +	}

      
        
        425
        +	return start, end

      
        
        426
        +}

      
        
        427
        +

      
        
        428
        +type semEmitFn func(token.Span, uint32, uint32)

      
        
        429
        +

      
        
        430
        +func emitDirective(content string, e token.Span, kwLen int, valType uint32, comment *ast.Comment, emit semEmitFn) {

      
        
        431
        +	kwEnd := e.Start.Offset + kwLen

      
        
        432
        +	emit(token.Span{Start: e.Start, End: offsetPos(e.Start.File, kwEnd)}, SemanticDirective, 0)

      
        
        433
        +	if v, ok := directiveValue(content, e, comment, kwEnd); ok {

      
        
        434
        +		emit(v, valType, 0)

      
        
        435
        +	}

      
        
        436
        +	if comment != nil {

      
        
        437
        +		emit(comment.Span, SemanticComment, 0)

      
        
        438
        +	}

      
        
        439
        +}

      
        
        440
        +

      
        
        441
        +func semEmitCost(content string, c *ast.Cost, emit semEmitFn) {

      
        
        442
        +	if c.IsTotal {

      
        
        443
        +		emit(token.Span{Start: c.Span.Start, End: offsetPos(c.Span.Start.File, c.Span.Start.Offset+2)}, SemOperator, 0)

      
        
        444
        +	} else {

      
        
        445
        +		emit(token.Span{Start: c.Span.Start, End: offsetPos(c.Span.Start.File, c.Span.Start.Offset+1)}, SemOperator, 0)

      
        
        446
        +	}

      
        
        447
        +	semEmitAmount(content, &c.Amount, emit)

      
        
        448
        +}

      
        
        449
        +

      
        
        450
        +func semEmitBalanceAssertion(content string, ba *ast.BalanceAssertion, emit semEmitFn) {

      
        
        451
        +	// The operator is the run of '=', ':', '*' chars from the span start.

      
        
        452
        +	// The ':' of ':=' precedes the '=' token, so back up one offset.

      
        
        453
        +	opStart := ba.Span.Start.Offset

      
        
        454
        +	if ba.IsAssignment && opStart > 0 {

      
        
        455
        +		opStart--

      
        
        456
        +	}

      
        
        457
        +	opEnd := opStart

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

      
        
        459
        +		opEnd++

      
        
        460
        +	}

      
        
        461
        +	emit(token.Span{Start: offsetPos(ba.Span.Start.File, opStart), End: offsetPos(ba.Span.Start.File, opEnd)}, SemOperator, 0)

      
        
        462
        +	semEmitAmount(content, &ba.Amount, emit)

      
        
        463
        +	if ba.Cost != nil {

      
        
        464
        +		semEmitCost(content, ba.Cost, emit)

      
        
        465
        +	}

      
        
        466
        +}

      
        
        467
        +

      
        
        468
        +// semLexerFallback produces semantic tokens using only the lexer (for unparseable documents).

      
        
        469
        +func semLexerFallback(content string) []semanticToken {

      
        
        470
        +	l := lexer.New("", []byte(content))

      
        
        471
        +	var out []semanticToken

      
        
        472
        +

      
        
        473
        +	var commentStart, commentEnd int // 0 = not inside a comment line

      
        
        474
        +	take := func(span token.Span, tokType uint32, mods uint32) {

      
        
        475
        +		out = append(out, semanticTokenAt(content, span, tokType, mods))

      
        
        476
        +	}

      
        
        477
        +	for {

      
        
        478
        +		tok := l.Next()

      
        
        479
        +		if tok.Type == token.EOF {

      
        
        480
        +			if commentStart > 0 {

      
        
        481
        +				take(token.Span{Start: offsetPos("", commentStart), End: offsetPos("", commentEnd)}, SemanticComment, 0)

      
        
        482
        +			}

      
        
        483
        +			break

      
        
        484
        +		}

      
        
        485
        +		if tok.Type == token.NEWLINE {

      
        
        486
        +			if commentStart > 0 {

      
        
        487
        +				take(token.Span{Start: offsetPos("", commentStart), End: offsetPos("", commentEnd)}, SemanticComment, 0)

      
        
        488
        +				commentStart, commentEnd = 0, 0

      
        
        489
        +			}

      
        
        490
        +			continue

      
        
        491
        +		}

      
        
        492
        +		if tok.Type == token.WHITESPACE || tok.Type == token.INDENT {

      
        
        493
        +			continue

      
        
        494
        +		}

      
        
        495
        +		tokType := uint32(SemString)

      
        
        496
        +		if commentStart > 0 {

      
        
        497
        +			tokType = SemanticComment

      
        
        498
        +			if tok.Span.End.Offset > commentEnd {

      
        
        499
        +				commentEnd = tok.Span.End.Offset

      
        
        500
        +			}

      
        
        501
        +			continue

      
        
        502
        +		}

      
        
        503
        +		switch tok.Type {

      
        
        504
        +		case token.SEMICOLON, token.HASH, token.PERCENT:

      
        
        505
        +			tokType = SemanticComment

      
        
        506
        +			commentStart = tok.Span.Start.Offset

      
        
        507
        +			commentEnd = tok.Span.End.Offset

      
        
        508
        +			continue

      
        
        509
        +		case token.STAR:

      
        
        510
        +			tokType = SemanticComment // * at col 0 is comment marker

      
        
        511
        +			commentStart = tok.Span.Start.Offset

      
        
        512
        +			commentEnd = tok.Span.End.Offset

      
        
        513
        +			continue

      
        
        514
        +		case token.ACCOUNT, token.COMMODITY, token.INCLUDE, token.ALIAS,

      
        
        515
        +			token.PAYEE, token.TAG, token.APPLY, token.END, token.COMMENTKW,

      
        
        516
        +			token.YEAR, token.DECIMALMARK, token.D, token.P, token.N, token.C:

      
        
        517
        +			tokType = SemanticDirective

      
        
        518
        +		case token.DATE:

      
        
        519
        +			tokType = SemanticDate

      
        
        520
        +		case token.INT, token.DECIMAL:

      
        
        521
        +			tokType = SemanticAmount

      
        
        522
        +		case token.COMMODITYMARK:

      
        
        523
        +			tokType = SemanticCommodity

      
        
        524
        +		case token.BANG:

      
        
        525
        +			tokType = SemanticStatus

      
        
        526
        +		case token.AT, token.ATAT, token.EQ, token.EQEQ, token.EQEQEQ, token.EQSTAR:

      
        
        527
        +			tokType = SemOperator

      
        
        528
        +		}

      
        
        529
        +		take(tok.Span, tokType, 0)

      
        
        530
        +	}

      
        
        531
        +	return out

      
        
        532
        +}

      
        
        533
        +

      
        
        534
        +func encodeSemTokens(tokens []semanticToken) []uint32 {

      
        
        535
        +	if len(tokens) == 0 {

      
        
        536
        +		return nil

      
        
        537
        +	}

      
        
        538
        +	slices.SortFunc(tokens, func(a, b semanticToken) int {

      
        
        539
        +		if a.line != b.line {

      
        
        540
        +			return int(a.line) - int(b.line)

      
        
        541
        +		}

      
        
        542
        +		return int(a.col) - int(b.col)

      
        
        543
        +	})

      
        
        544
        +	data := make([]uint32, 0, len(tokens)*5)

      
        
        545
        +	var prevLine, prevCol uint32

      
        
        546
        +	for _, t := range tokens {

      
        
        547
        +		var deltaLine, deltaCol uint32

      
        
        548
        +		if t.line == prevLine {

      
        
        549
        +			deltaLine = 0

      
        
        550
        +			deltaCol = t.col - prevCol

      
        
        551
        +		} else {

      
        
        552
        +			deltaLine = t.line - prevLine

      
        
        553
        +			deltaCol = t.col

      
        
        554
        +		}

      
        
        555
        +		data = append(data, deltaLine, deltaCol, t.length, t.tokenType, t.modifiers)

      
        
        556
        +		prevLine = t.line

      
        
        557
        +		prevCol = t.col

      
        
        558
        +	}

      
        
        559
        +	return data

      
        
        560
        +}

      
        
        561
        +

      
        
        562
        +// betweenSpan returns the span of the text between two offsets, trimmed of surrounding whitespace.

      
        
        563
        +func betweenSpan(content, file string, start, end int) (token.Span, bool) {

      
        
        564
        +	for start < end && (content[start] == ' ' || content[start] == '\t') {

      
        
        565
        +		start++

      
        
        566
        +	}

      
        
        567
        +	for end > start && (content[end-1] == ' ' || content[end-1] == '\t' || content[end-1] == '\n' || content[end-1] == '\r') {

      
        
        568
        +		end--

      
        
        569
        +	}

      
        
        570
        +	if end <= start {

      
        
        571
        +		return token.Span{}, false

      
        
        572
        +	}

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

      
        
        574
        +}

      
        
        575
        +

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

      
        
        577
        +func offsetSpan(file string, start, end int) token.Span {

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

      
        
        579
        +}

      
A internal/lsp/textdocument_semantic_test.go
···
        
        1
        +package lsp

      
        
        2
        +

      
        
        3
        +import (

      
        
        4
        +	"context"

      
        
        5
        +	"fmt"

      
        
        6
        +	"slices"

      
        
        7
        +	"strings"

      
        
        8
        +	"testing"

      
        
        9
        +

      
        
        10
        +	"go.lsp.dev/protocol"

      
        
        11
        +	"go.lsp.dev/uri"

      
        
        12
        +

      
        
        13
        +	"olexsmir.xyz/clerk/internal/testutil/golden"

      
        
        14
        +)

      
        
        15
        +

      
        
        16
        +func TestSemanticTokens_Legend(t *testing.T) {

      
        
        17
        +	if len(tokenTypeStrings) != semTypeCount {

      
        
        18
        +		t.Fatalf("tokenTypeStrings has %d entries, want %d (one per TokenType constant)",

      
        
        19
        +			len(tokenTypeStrings), semTypeCount)

      
        
        20
        +	}

      
        
        21
        +	legend := getSemanticTokensLegend()

      
        
        22
        +	if !slices.Equal(legend.TokenTypes, tokenTypeStrings) {

      
        
        23
        +		t.Errorf("legend.TokenTypes = %v, want %v", legend.TokenTypes, tokenTypeStrings)

      
        
        24
        +	}

      
        
        25
        +	if !slices.Equal(legend.TokenModifiers, modifierStrings) {

      
        
        26
        +		t.Errorf("legend.TokenModifiers = %v, want %v", legend.TokenModifiers, modifierStrings)

      
        
        27
        +	}

      
        
        28
        +}

      
        
        29
        +

      
        
        30
        +func TestSemanticTokensEncode(t *testing.T) {

      
        
        31
        +	tests := []struct {

      
        
        32
        +		name   string

      
        
        33
        +		tokens []semanticToken

      
        
        34
        +		want   []uint32

      
        
        35
        +	}{

      
        
        36
        +		{"nil", nil, nil},

      
        
        37
        +		{"empty", []semanticToken{}, nil},

      
        
        38
        +		{"single", []semanticToken{

      
        
        39
        +			{line: 0, col: 0, length: 4, tokenType: SemanticDate},

      
        
        40
        +		}, []uint32{0, 0, 4, SemanticDate, 0}},

      
        
        41
        +		{"line 0 col 0", []semanticToken{

      
        
        42
        +			{line: 0, col: 0, length: 1, tokenType: SemanticDirective},

      
        
        43
        +		}, []uint32{0, 0, 1, SemanticDirective, 0}},

      
        
        44
        +		{"multiple", []semanticToken{

      
        
        45
        +			{line: 0, col: 0, length: 10, tokenType: SemanticDate},

      
        
        46
        +			{line: 0, col: 11, length: 5, tokenType: SemString},

      
        
        47
        +			{line: 1, col: 4, length: 10, tokenType: SemanticAccount},

      
        
        48
        +		}, []uint32{

      
        
        49
        +			0, 0, 10, SemanticDate, 0,

      
        
        50
        +			0, 11, 5, SemString, 0,

      
        
        51
        +			1, 4, 10, SemanticAccount, 0,

      
        
        52
        +		}},

      
        
        53
        +		// the wire format requires non-negative deltas; unsorted input must

      
        
        54
        +		// be sorted first (commodity at col 36 comes after amount at col 32)

      
        
        55
        +		{"unsorted input", []semanticToken{

      
        
        56
        +			{line: 0, col: 0, length: 10, tokenType: SemanticDate},

      
        
        57
        +			{line: 0, col: 36, length: 3, tokenType: SemanticCommodity},

      
        
        58
        +			{line: 0, col: 32, length: 2, tokenType: SemanticAmount},

      
        
        59
        +			{line: 1, col: 4, length: 6, tokenType: SemanticAccount},

      
        
        60
        +		}, []uint32{

      
        
        61
        +			0, 0, 10, SemanticDate, 0,

      
        
        62
        +			0, 32, 2, SemanticAmount, 0,

      
        
        63
        +			0, 4, 3, SemanticCommodity, 0,

      
        
        64
        +			1, 4, 6, SemanticAccount, 0,

      
        
        65
        +		}},

      
        
        66
        +	}

      
        
        67
        +	for _, tt := range tests {

      
        
        68
        +		t.Run(tt.name, func(t *testing.T) {

      
        
        69
        +			if got := encodeSemTokens(tt.tokens); !slices.Equal(got, tt.want) {

      
        
        70
        +				t.Errorf("encodeSemTokens() = %v, want %v", got, tt.want)

      
        
        71
        +			}

      
        
        72
        +		})

      
        
        73
        +	}

      
        
        74
        +}

      
        
        75
        +

      
        
        76
        +func TestSemanticTokens_Server_SimpleTransaction(t *testing.T) {

      
        
        77
        +	content := "2024-01-15 test\n    expenses:food  $50\n    assets:cash\n"

      
        
        78
        +

      
        
        79
        +	srv := NewServer("test")

      
        
        80
        +	srv.server.openDoc(uri.URI("file:///test.journal"), content, 1, "journal")

      
        
        81
        +

      
        
        82
        +	result, err := srv.server.SemanticTokensFull(t.Context(), &protocol.SemanticTokensParams{

      
        
        83
        +		TextDocument: protocol.TextDocumentIdentifier{URI: uri.URI("file:///test.journal")},

      
        
        84
        +	})

      
        
        85
        +	if err != nil {

      
        
        86
        +		t.Fatal(err)

      
        
        87
        +	}

      
        
        88
        +	if result == nil {

      
        
        89
        +		t.Fatal("result is nil")

      
        
        90
        +	}

      
        
        91
        +	if len(result.Data) == 0 {

      
        
        92
        +		t.Fatal("expected non-empty token data")

      
        
        93
        +	}

      
        
        94
        +	if len(result.Data)%5 != 0 {

      
        
        95
        +		t.Fatalf("token data length %d is not a multiple of 5", len(result.Data))

      
        
        96
        +	}

      
        
        97
        +	// first token is the transaction date at line 0, col 0: deltas are 0, 0

      
        
        98
        +	if result.Data[0] != 0 || result.Data[1] != 0 {

      
        
        99
        +		t.Errorf("first token deltas = %d,%d, want 0,0", result.Data[0], result.Data[1])

      
        
        100
        +	}

      
        
        101
        +}

      
        
        102
        +

      
        
        103
        +func TestSemanticTokens_Server_EmptyDocument(t *testing.T) {

      
        
        104
        +	srv := NewServer("test")

      
        
        105
        +	srv.server.openDoc(uri.URI("file:///empty.journal"), "", 1, "journal")

      
        
        106
        +

      
        
        107
        +	result, err := srv.server.SemanticTokensFull(t.Context(), &protocol.SemanticTokensParams{

      
        
        108
        +		TextDocument: protocol.TextDocumentIdentifier{URI: uri.URI("file:///empty.journal")},

      
        
        109
        +	})

      
        
        110
        +	if err != nil {

      
        
        111
        +		t.Fatal(err)

      
        
        112
        +	}

      
        
        113
        +	if len(result.Data) != 0 {

      
        
        114
        +		t.Errorf("expected empty data for empty doc, got %d values", len(result.Data))

      
        
        115
        +	}

      
        
        116
        +}

      
        
        117
        +

      
        
        118
        +func TestSemanticTokens_Server_DocumentNotFound(t *testing.T) {

      
        
        119
        +	result, err := NewServer("test").server.SemanticTokensFull(t.Context(), &protocol.SemanticTokensParams{

      
        
        120
        +		TextDocument: protocol.TextDocumentIdentifier{URI: uri.URI("file:///unknown.journal")},

      
        
        121
        +	})

      
        
        122
        +	if err != nil {

      
        
        123
        +		t.Fatal(err)

      
        
        124
        +	}

      
        
        125
        +	if result == nil {

      
        
        126
        +		t.Fatal("result is nil")

      
        
        127
        +	}

      
        
        128
        +	if result.Data != nil {

      
        
        129
        +		t.Errorf("expected nil Data for unknown doc, got %v", result.Data)

      
        
        130
        +	}

      
        
        131
        +}

      
        
        132
        +

      
        
        133
        +func TestSemanticTokens_Server_Range(t *testing.T) {

      
        
        134
        +	content := "2024-01-15 test\n    expenses:food  $50\n2024-01-16 other\n    expenses:drinks  $20\n"

      
        
        135
        +	srv := NewServer("test")

      
        
        136
        +	srv.server.openDoc(uri.URI("file:///test.journal"), content, 1, "journal")

      
        
        137
        +

      
        
        138
        +	params := &protocol.SemanticTokensRangeParams{

      
        
        139
        +		TextDocument: protocol.TextDocumentIdentifier{URI: uri.URI("file:///test.journal")},

      
        
        140
        +		Range: protocol.Range{

      
        
        141
        +			Start: protocol.Position{Line: 0, Character: 0},

      
        
        142
        +			End:   protocol.Position{Line: 1, Character: 50},

      
        
        143
        +		},

      
        
        144
        +	}

      
        
        145
        +	result, err := srv.server.SemanticTokensRange(context.Background(), params)

      
        
        146
        +	if err != nil {

      
        
        147
        +		t.Fatal(err)

      
        
        148
        +	}

      
        
        149
        +	if result == nil || len(result.Data) == 0 {

      
        
        150
        +		t.Fatal("expected non-empty tokens for range")

      
        
        151
        +	}

      
        
        152
        +

      
        
        153
        +	// decode and assert every token is inside the requested line range

      
        
        154
        +	line, col := 0, 0

      
        
        155
        +	for i := 0; i+4 < len(result.Data); i += 5 {

      
        
        156
        +		dl, dc := result.Data[i], result.Data[i+1]

      
        
        157
        +		if dl > 0 {

      
        
        158
        +			col = 0

      
        
        159
        +		}

      
        
        160
        +		line += int(dl)

      
        
        161
        +		col += int(dc)

      
        
        162
        +		if line > 1 {

      
        
        163
        +			t.Fatalf("token at line %d outside requested range [0,1]", line)

      
        
        164
        +		}

      
        
        165
        +	}

      
        
        166
        +}

      
        
        167
        +

      
        
        168
        +// Golden

      
        
        169
        +

      
        
        170
        +func TestSemanticTokensTxtar(t *testing.T) {

      
        
        171
        +	tests := []string{

      
        
        172
        +		"semantic-empty",

      
        
        173
        +		"semantic-journal",

      
        
        174
        +		"semantic-directives",

      
        
        175
        +		"semantic-unparseable",

      
        
        176
        +	}

      
        
        177
        +

      
        
        178
        +	for _, tt := range tests {

      
        
        179
        +		ar := golden.Read(t, tt)

      
        
        180
        +

      
        
        181
        +		t.Run(tt+"_golden", func(t *testing.T) {

      
        
        182
        +			toks := renderSemanticTokens(tokSem(string(ar.Get("in.journal"))))

      
        
        183
        +			golden.Assert(t, ar, toks)

      
        
        184
        +		})

      
        
        185
        +

      
        
        186
        +		t.Run(tt+"_no-overlap", func(t *testing.T) {

      
        
        187
        +			toks := tokSem(string(ar.Get("in.journal")))

      
        
        188
        +			slices.SortFunc(toks, func(a, b semanticToken) int {

      
        
        189
        +				if a.line != b.line {

      
        
        190
        +					return int(a.line) - int(b.line)

      
        
        191
        +				}

      
        
        192
        +				return int(a.col) - int(b.col)

      
        
        193
        +			})

      
        
        194
        +

      
        
        195
        +			for i := 1; i < len(toks); i++ {

      
        
        196
        +				prev, cur := toks[i-1], toks[i]

      
        
        197
        +				if prev.line != cur.line {

      
        
        198
        +					continue

      
        
        199
        +				}

      
        
        200
        +				if cur.col < prev.col+prev.length {

      
        
        201
        +					t.Errorf("%s: overlapping tokens on line %d: %s@%d+%d then %s@%d+%d",

      
        
        202
        +						tt, prev.line, tokenTypeStrings[prev.tokenType], prev.col, prev.length,

      
        
        203
        +						tokenTypeStrings[cur.tokenType], cur.col, cur.length)

      
        
        204
        +				}

      
        
        205
        +			}

      
        
        206
        +		})

      
        
        207
        +	}

      
        
        208
        +}

      
        
        209
        +

      
        
        210
        +func renderSemanticTokens(tokens []semanticToken) string {

      
        
        211
        +	slices.SortFunc(tokens, func(a, b semanticToken) int {

      
        
        212
        +		if a.line != b.line {

      
        
        213
        +			return int(a.line) - int(b.line)

      
        
        214
        +		}

      
        
        215
        +		return int(a.col) - int(b.col)

      
        
        216
        +	})

      
        
        217
        +	var b strings.Builder

      
        
        218
        +	for _, tok := range tokens {

      
        
        219
        +		fmt.Fprintf(&b, "%d:%d+%d %s", tok.line, tok.col, tok.length, tokenTypeStrings[tok.tokenType])

      
        
        220
        +		for i, m := range modifierStrings {

      
        
        221
        +			if tok.modifiers&(1<<uint(i)) != 0 {

      
        
        222
        +				b.WriteByte(' ')

      
        
        223
        +				b.WriteString(m)

      
        
        224
        +			}

      
        
        225
        +		}

      
        
        226
        +		b.WriteByte('\n')

      
        
        227
        +	}

      
        
        228
        +	return b.String()

      
        
        229
        +}

      
        
        230
        +

      
        
        231
        +func tokSem(content string) []semanticToken {

      
        
        232
        +	return tokenizeForSemantics(content, parseJournalStr(content))

      
        
        233
        +}

      
M internal/lsp/textdocument_sync.go
···
        5
        5
         

      
        6
        6
         	"go.lsp.dev/protocol"

      
        7
        7
         	"go.lsp.dev/uri"

      
        
        8
        +

      
        
        9
        +	"olexsmir.xyz/clerk/journal/ast"

      
        
        10
        +	"olexsmir.xyz/clerk/journal/lexer"

      
        
        11
        +	"olexsmir.xyz/clerk/journal/parser"

      
        8
        12
         )

      
        9
        13
         

      
        10
        14
         func (s *server) DidOpen(ctx context.Context, params *protocol.DidOpenTextDocumentParams) error {

      ···
        32
        36
         // Document management

      
        33
        37
         

      
        34
        38
         func (s *server) openDoc(u uri.URI, text string, version int32, langID protocol.LanguageKind) {

      
        
        39
        +	j := parseJournalStr(text)

      
        35
        40
         	s.mu.Lock()

      
        36
        41
         	s.openDocs[u] = docState{

      
        37
        42
         		text:       text,

      
        38
        43
         		version:    version,

      
        39
        44
         		languageID: langID,

      
        
        45
        +		journal:    j,

      
        40
        46
         	}

      
        41
        47
         	s.mu.Unlock()

      
        42
        48
         }

      ···
        53
        59
         		switch ev := ch.(type) {

      
        54
        60
         		case *protocol.TextDocumentContentChangeWholeDocument:

      
        55
        61
         			state.text = ev.Text

      
        
        62
        +			state.journal = parseJournalStr(ev.Text)

      
        56
        63
         		case *protocol.TextDocumentContentChangePartial:

      
        57
        64
         			_ = ev // TODO: incremental edit support

      
        58
        65
         		}

      ···
        72
        79
         	s.mu.Unlock()

      
        73
        80
         	return state.text, ok

      
        74
        81
         }

      
        
        82
        +

      
        
        83
        +func (s *server) getDocState(u uri.URI) (docState, bool) {

      
        
        84
        +	s.mu.Lock()

      
        
        85
        +	state, ok := s.openDocs[u]

      
        
        86
        +	s.mu.Unlock()

      
        
        87
        +	return state, ok

      
        
        88
        +}

      
        
        89
        +

      
        
        90
        +// parseJournalStr parses a raw journal string into an AST.

      
        
        91
        +func parseJournalStr(content string) *ast.Journal {

      
        
        92
        +	l := lexer.New("", []byte(content))

      
        
        93
        +	p := parser.New(l)

      
        
        94
        +	return p.ParseJournal()

      
        
        95
        +}

      
M journal/ast/ast.go
···
        77
        77
         	Span  token.Span

      
        78
        78
         }

      
        79
        79
         

      
        
        80
        +type Code struct {

      
        
        81
        +	Value string

      
        
        82
        +	Span  token.Span

      
        
        83
        +}

      
        
        84
        +

      
        
        85
        +type Note struct {

      
        
        86
        +	Value string

      
        
        87
        +	Span  token.Span

      
        
        88
        +}

      
        
        89
        +

      
        
        90
        +type Description struct {

      
        
        91
        +	Value string

      
        
        92
        +	Span  token.Span

      
        
        93
        +}

      
        
        94
        +

      
        
        95
        +type Expr struct {

      
        
        96
        +	Value string

      
        
        97
        +	Span  token.Span

      
        
        98
        +}

      
        
        99
        +

      
        80
        100
         type Payee struct {

      
        81
        101
         	Name string

      
        82
        102
         	Span token.Span

      
M journal/ast/directives.go
···
        11
        11
         func (AccountDirective) entryNode() {}

      
        12
        12
         

      
        13
        13
         type CommodityDirective struct {

      
        14
        
        -	Commodity string

      
        15
        
        -	Format    Amount   // optional format hint: "1,000.00 UAH"

      
        16
        
        -	Comment   *Comment // optional inline comment

      
        17
        
        -	Span      token.Span

      
        
        14
        +	Commodity     string

      
        
        15
        +	CommoditySpan token.Span // span of the commodity token

      
        
        16
        +	Format        Amount     // optional format hint: "1,000.00 UAH"

      
        
        17
        +	Comment       *Comment   // optional inline comment

      
        
        18
        +	Span          token.Span

      
        18
        19
         }

      
        19
        20
         

      
        20
        21
         func (CommodityDirective) entryNode() {}

      
M journal/ast/dump.go
···
        134
        134
         	}

      
        135
        135
         	if t.Code != nil {

      
        136
        136
         		indent(b, depth+1)

      
        137
        
        -		fmt.Fprintf(b, "Code: %q\n", *t.Code)

      
        
        137
        +		fmt.Fprintf(b, "Code: %q %s\n", t.Code.Value, t.Code.Span)

      
        138
        138
         	}

      
        139
        139
         	if t.Payee != nil {

      
        140
        140
         		indent(b, depth+1)

      ···
        142
        142
         	}

      
        143
        143
         	if t.Note != nil {

      
        144
        144
         		indent(b, depth+1)

      
        145
        
        -		fmt.Fprintf(b, "Note: %q\n", *t.Note)

      
        
        145
        +		fmt.Fprintf(b, "Note: %q %s\n", t.Note.Value, t.Note.Span)

      
        146
        146
         	}

      
        147
        147
         	dumpOptComment(b, t.Comment, depth+1)

      
        148
        148
         	if len(t.HeaderComments) > 0 {

      ···
        161
        161
         	indent(b, depth)

      
        162
        162
         	fmt.Fprintf(b, "AutomatedTransaction %s\n", t.Span)

      
        163
        163
         	indent(b, depth+1)

      
        164
        
        -	fmt.Fprintf(b, "Expr: %q\n", t.Expr)

      
        
        164
        +	fmt.Fprintf(b, "Expr: %q %s\n", t.Expr.Value, t.Expr.Span)

      
        165
        165
         	dumpOptComment(b, t.Comment, depth+1)

      
        166
        166
         	if len(t.HeaderComments) > 0 {

      
        167
        167
         		indent(b, depth+1)

      ···
        194
        194
         	}

      
        195
        195
         	if t.Code != nil {

      
        196
        196
         		indent(b, depth+1)

      
        197
        
        -		fmt.Fprintf(b, "Code: %q\n", *t.Code)

      
        
        197
        +		fmt.Fprintf(b, "Code: %q %s\n", t.Code.Value, t.Code.Span)

      
        198
        198
         	}

      
        199
        199
         	if t.Description != nil {

      
        200
        200
         		indent(b, depth+1)

      
        201
        
        -		fmt.Fprintf(b, "Description: %q\n", *t.Description)

      
        
        201
        +		fmt.Fprintf(b, "Description: %q %s\n", t.Description.Value, t.Description.Span)

      
        202
        202
         	}

      
        203
        203
         	dumpOptComment(b, t.Comment, depth+1)

      
        204
        204
         	if len(t.HeaderComments) > 0 {

      ···
        251
        251
         	indent(b, depth+1)

      
        252
        252
         	fmt.Fprintf(b, "Quantity: %s\n", a.Quantity.String())

      
        253
        253
         	indent(b, depth+1)

      
        254
        
        -	fmt.Fprintf(b, "Commodity: %q\n", a.Commodity)

      
        
        254
        +	fmt.Fprintf(b, "Commodity: %q", a.Commodity)

      
        
        255
        +	if a.Commodity != "" {

      
        
        256
        +		fmt.Fprintf(b, " %s", a.CommoditySpan)

      
        
        257
        +	}

      
        
        258
        +	fmt.Fprintf(b, "\n")

      
        255
        259
         	indent(b, depth+1)

      
        256
        260
         	fmt.Fprintf(b, "CommodityPos: %s\n", a.CommodityPos)

      
        257
        261
         	indent(b, depth+1)

      ···
        317
        321
         	indent(b, depth)

      
        318
        322
         	fmt.Fprintf(b, "CommodityDirective %s\n", c.Span)

      
        319
        323
         	indent(b, depth+1)

      
        320
        
        -	fmt.Fprintf(b, "Commodity: %q\n", c.Commodity)

      
        
        324
        +	fmt.Fprintf(b, "Commodity: %q %s\n", c.Commodity, c.CommoditySpan)

      
        321
        325
         	if c.Format.QuantityFmt.Decimal != 0 {

      
        322
        326
         		dumpAmount(b, &c.Format, depth+1)

      
        323
        327
         	}

      
M journal/ast/entries.go
···
        13
        13
         	Date           Date

      
        14
        14
         	SecondDate     *Date      // optional =2026-05-18 date

      
        15
        15
         	Status         Status     // optional */! status

      
        16
        
        -	Code           *string    // optional (123) code

      
        
        16
        +	Code           *Code      // optional (123) code

      
        17
        17
         	Payee          *Payee     // optional payee

      
        18
        
        -	Note           *string    // part after |

      
        
        18
        +	Note           *Note      // part after |

      
        19
        19
         	Comment        *Comment   // inline ; on header line

      
        20
        20
         	HeaderComments []*Comment // indented ; lines before first posting

      
        21
        21
         	Postings       []*Posting

      ···
        34
        34
         func (Period) entryNode() {}

      
        35
        35
         

      
        36
        36
         type PeriodicTransaction struct {

      
        37
        
        -	Period         Period   // period-expr

      
        38
        
        -	Status         Status   // optional */! status

      
        39
        
        -	Code           *string  // optional (123) code

      
        40
        
        -	Description    *string  // optional description

      
        41
        
        -	Comment        *Comment // optional inline comment

      
        
        37
        +	Period         Period       // period-expr

      
        
        38
        +	Status         Status       // optional */! status

      
        
        39
        +	Code           *Code        // optional (123) code

      
        
        40
        +	Description    *Description // optional description

      
        
        41
        +	Comment        *Comment     // optional inline comment

      
        42
        42
         	HeaderComments []*Comment

      
        43
        43
         	Postings       []*Posting

      
        44
        44
         	Span           token.Span

      ···
        47
        47
         func (PeriodicTransaction) entryNode() {}

      
        48
        48
         

      
        49
        49
         type AutomatedTransaction struct {

      
        50
        
        -	Expr           string

      
        
        50
        +	Expr           Expr

      
        51
        51
         	Postings       []*Posting

      
        52
        52
         	Comment        *Comment   // inline ; on header line

      
        53
        53
         	HeaderComments []*Comment // indented ; lines before first posting

      ···
        90
        90
         }

      
        91
        91
         

      
        92
        92
         type Amount struct {

      
        93
        
        -	IsNegative   bool

      
        94
        
        -	Quantity     decimal.Decimal

      
        95
        
        -	QuantityFmt  QuantityFormat

      
        96
        
        -	Commodity    string

      
        97
        
        -	CommodityPos CommodityPos // Before | After

      
        98
        
        -	HasSpace     bool         // "$10" vs "$ 10"

      
        99
        
        -	IsExpr       bool         // e.g: *-1

      
        100
        
        -	Expr         string       // expression text e.g. "amount * -1". set only if IsExpr is true

      
        101
        
        -	Span         token.Span

      
        
        93
        +	IsNegative    bool

      
        
        94
        +	Quantity      decimal.Decimal

      
        
        95
        +	QuantityFmt   QuantityFormat

      
        
        96
        +	Commodity     string

      
        
        97
        +	CommodityPos  CommodityPos // Before | After

      
        
        98
        +	CommoditySpan token.Span   // span of the commodity within the amount

      
        
        99
        +	HasSpace      bool         // "$10" vs "$ 10"

      
        
        100
        +	IsExpr        bool         // e.g: *-1

      
        
        101
        +	Expr          string       // expression text e.g. "amount * -1". set only if IsExpr is true

      
        
        102
        +	Span          token.Span

      
        102
        103
         }

      
        103
        104
         

      
        104
        105
         type Cost struct {

      
M journal/parser/parser.go
···
        140
        140
         

      
        141
        141
         	// optional code

      
        142
        142
         	if p.got(token.LPAREN) {

      
        
        143
        +		cs := p.cur.Span

      
        143
        144
         		p.advance()

      
        144
        145
         		var code strings.Builder

      
        145
        146
         		for p.cur.Type != token.RPAREN {

      
        146
        147
         			_, _ = code.WriteString(p.cur.Literal)

      
        147
        148
         			p.advance()

      
        148
        149
         		}

      
        149
        
        -		tx.Code = new(code.String())

      
        
        150
        +		rp := p.cur.Span

      
        150
        151
         		p.advance()

      
        
        152
        +		tx.Code = &ast.Code{

      
        
        153
        +			Value: code.String(),

      
        
        154
        +			Span:  token.Span{Start: cs.Start, End: rp.End},

      
        
        155
        +		}

      
        151
        156
         		p.skipWhitespace()

      
        152
        157
         	}

      
        153
        158
         

      ···
        163
        168
         		if p.got(token.PIPE) {

      
        164
        169
         			p.advance()

      
        165
        170
         			if p.got(token.TEXT) {

      
        
        171
        +				sn := p.cur.Span

      
        166
        172
         				n := p.cur.Literal

      
        167
        173
         				p.advance()

      
        168
        
        -				tx.Note = &n

      
        
        174
        +				tx.Note = &ast.Note{Value: n, Span: p.span(sn)}

      
        169
        175
         			}

      
        170
        176
         		}

      
        171
        177
         	}

      ···
        225
        231
         

      
        226
        232
         	pt.Period = p.parsePeriod()

      
        227
        233
         

      
        228
        
        -	if desc := p.parseOptPeriodicDescription(); desc != "" {

      
        229
        
        -		pt.Description = &desc

      
        
        234
        +	if desc, dspan := p.parseOptPeriodicDescription(); desc != "" {

      
        
        235
        +		pt.Description = &ast.Description{Value: desc, Span: dspan}

      
        230
        236
         	}

      
        231
        237
         

      
        232
        238
         	comment := p.parseOptInlineComment()

      ···
        257
        263
         

      
        258
        264
         	at := &ast.AutomatedTransaction{}

      
        259
        265
         

      
        260
        
        -	at.Expr = p.parseDirectiveExpr()

      
        
        266
        +	// expression

      
        
        267
        +	sd := p.cur.Span

      
        
        268
        +	expr := p.parseDirectiveExpr()

      
        
        269
        +	at.Expr = ast.Expr{Value: expr, Span: p.span(sd)}

      
        261
        270
         	at.Comment = p.parseOptInlineComment()

      
        262
        271
         	p.expectNewline()

      
        263
        272
         

      ···
        312
        321
         			dateStr = after[:end]

      
        313
        322
         		}

      
        314
        323
         		if d := parseSimpleDate(dateStr); d.Year > 0 {

      
        
        324
        +			fromOff := strings.Index(str, dateStr)

      
        
        325
        +			d.Span = periodDateSpan(period, str, dateStr, fromOff)

      
        315
        326
         			period.From = &d

      
        316
        327
         			rest := after

      
        317
        328
         			if end >= 0 {

      ···
        322
        333
         					toAfter = toAfter[:toEnd]

      
        323
        334
         				}

      
        324
        335
         				if d := parseSimpleDate(toAfter); d.Year > 0 {

      
        
        336
        +					d.Span = periodDateSpan(period, str, toAfter, fromOff+len(dateStr))

      
        325
        337
         					period.To = &d

      
        326
        338
         				}

      
        327
        339
         			}

      ···
        330
        342
         	return period

      
        331
        343
         }

      
        332
        344
         

      
        
        345
        +// periodDateSpan returns the source span of dateStr, which occurs in the

      
        
        346
        +// period text at or after searchFrom. The period span and text cover the same

      
        
        347
        +// bytes, so offsets line up 1:1.

      
        
        348
        +func periodDateSpan(period ast.Period, text, dateStr string, searchFrom int) token.Span {

      
        
        349
        +	off := strings.Index(text[searchFrom:], dateStr)

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

      
        
        351
        +	return token.Span{

      
        
        352
        +		Start: token.Pos{File: period.Span.Start.File, Offset: abs},

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

      
        
        354
        +	}

      
        
        355
        +}

      
        
        356
        +

      
        333
        357
         func (p *Parser) parseComment() *ast.Comment {

      
        334
        358
         	s := p.cur.Span

      
        335
        359
         	marker := p.cur.Literal[0]

      ···
        381
        405
         	p.skipWhitespace()

      
        382
        406
         

      
        383
        407
         	var commodity string

      
        
        408
        +	var commoditySpan token.Span

      
        384
        409
         	var format *ast.Amount

      
        385
        410
         

      
        386
        411
         	switch p.cur.Type {

      
        387
        412
         	case token.COMMODITYMARK, token.TEXT, token.STRING:

      
        
        413
        +		cs := p.cur.Span

      
        388
        414
         		commodity = p.cur.Literal

      
        389
        415
         		if p.got(token.STRING) {

      
        390
        416
         			commodity = unquote(commodity)

      
        391
        417
         		}

      
        392
        418
         		p.advance()

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

      
        393
        420
         		hadSpace := p.got(token.WHITESPACE)

      
        394
        421
         		p.skipWhitespace()

      
        395
        422
         		if p.got(token.INT) || p.got(token.DECIMAL) || p.got(token.TEXT) {

      
        396
        423
         			format = p.parseAmount()

      
        397
        424
         			format.Commodity = commodity

      
        
        425
        +			format.CommoditySpan = commoditySpan

      
        398
        426
         			format.CommodityPos = ast.CommodityBefore

      
        399
        427
         			format.HasSpace = hadSpace

      
        400
        428
         		}

      
        401
        429
         	case token.INT, token.DECIMAL:

      
        402
        430
         		format = p.parseAmount()

      
        403
        431
         		commodity = format.Commodity

      
        
        432
        +		commoditySpan = format.CommoditySpan

      
        404
        433
         	default:

      
        405
        434
         		p.errorf("expected commodity name or amount, got %s", p.cur.Type)

      
        406
        435
         	}

      ···
        428
        457
         	}

      
        429
        458
         

      
        430
        459
         	cd := &ast.CommodityDirective{

      
        431
        
        -		Commodity: commodity,

      
        432
        
        -		Comment:   comment,

      
        433
        
        -		Span:      p.span(s),

      
        
        460
        +		Commodity:     commodity,

      
        
        461
        +		CommoditySpan: commoditySpan,

      
        
        462
        +		Comment:       comment,

      
        
        463
        +		Span:          p.span(s),

      
        434
        464
         	}

      
        435
        465
         	if format != nil {

      
        436
        466
         		cd.Format = *format

      ···
        798
        828
         	s := p.cur.Span

      
        799
        829
         	amt := &ast.Amount{

      
        800
        830
         		QuantityFmt: ast.QuantityFormat{Decimal: '.'},

      
        801
        
        -		Span:        p.span(s),

      
        802
        831
         	}

      
        
        832
        +	defer func() {

      
        
        833
        +		// The span covers from the first token to the start of the next unconsumed token.

      
        
        834
        +		// Since parseQuantityInto (and possible commodity consumption) advanced past the last

      
        
        835
        +		// amount token, p.cur points to the next token after the amount — which is the correct end.

      
        
        836
        +		amt.Span = p.span(s)

      
        
        837
        +	}()

      
        803
        838
         

      
        804
        839
         	// commodity before quantity: $10.00, eur 10.00

      
        805
        840
         	if p.got(token.COMMODITYMARK) || p.got(token.TEXT) || p.got(token.STRING) {

      
        
        841
        +		cs := p.cur.Span

      
        806
        842
         		amt.Commodity = unquote(p.cur.Literal)

      
        807
        843
         		amt.CommodityPos = ast.CommodityBefore

      
        808
        844
         		p.advance()

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

      
        809
        846
         		if p.got(token.WHITESPACE) {

      
        810
        847
         			amt.HasSpace = true

      
        811
        848
         			p.skipWhitespace()

      ···
        830
        867
         

      
        831
        868
         		// commodity before quantity: -$120, -eur 120:

      
        832
        869
         		if p.got(token.COMMODITYMARK) || p.got(token.TEXT) || p.got(token.STRING) {

      
        
        870
        +			cs := p.cur.Span

      
        833
        871
         			amt.Commodity = unquote(p.cur.Literal)

      
        834
        872
         			amt.CommodityPos = ast.CommodityBefore

      
        835
        873
         			p.advance()

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

      
        836
        875
         			if p.got(token.WHITESPACE) {

      
        837
        876
         				amt.HasSpace = true

      
        838
        877
         				p.skipWhitespace()

      ···
        847
        886
         			case token.WHITESPACE:

      
        848
        887
         				p.skipWhitespace()

      
        849
        888
         				if p.got(token.COMMODITYMARK) || p.got(token.TEXT) || p.got(token.STRING) {

      
        
        889
        +					cs := p.cur.Span

      
        850
        890
         					amt.HasSpace = true

      
        851
        891
         					amt.Commodity = unquote(p.cur.Literal)

      
        852
        892
         					amt.CommodityPos = ast.CommodityAfter

      
        853
        893
         					p.advance()

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

      
        854
        895
         				}

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

      
        
        897
        +				cs := p.cur.Span

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

      
        857
        899
         				amt.CommodityPos = ast.CommodityAfter

      
        858
        900
         				p.advance()

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

      
        859
        902
         			}

      
        860
        903
         		}

      
        861
        904
         	}

      ···
        1168
        1211
         	}

      
        1169
        1212
         }

      
        1170
        1213
         

      
        1171
        
        -func (p *Parser) parseOptPeriodicDescription() string {

      
        
        1214
        +func (p *Parser) parseOptPeriodicDescription() (string, token.Span) {

      
        1172
        1215
         	if p.cur.Type != token.WHITESPACE || len(p.cur.Literal) < 2 {

      
        1173
        
        -		return ""

      
        
        1216
        +		return "", token.Span{}

      
        1174
        1217
         	}

      
        1175
        1218
         

      
        1176
        1219
         	p.skipWhitespace()

      
        1177
        1220
         

      
        1178
        1221
         	if p.cur.Type != token.TEXT {

      
        1179
        
        -		return ""

      
        
        1222
        +		return "", token.Span{}

      
        1180
        1223
         	}

      
        1181
        1224
         

      
        1182
        
        -	return p.parseDescription()

      
        
        1225
        +	s := p.cur.Span

      
        
        1226
        +	desc := p.parseDescription()

      
        
        1227
        +	return desc, p.span(s)

      
        1183
        1228
         }

      
        1184
        1229
         

      
        1185
        1230
         func (p *Parser) parseDescription() string {

      
M journal/parser/testdata/C_directive.txtar
···
        4
        4
         Journal

      
        5
        5
           ConversionDirective j:1:1-2:1

      
        6
        6
             From:

      
        7
        
        -      Amount j:1:3-1:3

      
        
        7
        +      Amount j:1:3-1:8

      
        8
        8
                 Quantity: 1

      
        9
        
        -        Commodity: "s"

      
        
        9
        +        Commodity: "s" j:1:7-1:8

      
        10
        10
                 CommodityPos: After

      
        11
        11
                 HasSpace: false

      
        12
        12
                 Precision: 2

      
        13
        13
                 Decimal: "."

      
        14
        14
             To:

      
        15
        
        -      Amount j:1:11-1:11

      
        
        15
        +      Amount j:1:11-2:0

      
        16
        16
                 Quantity: 100

      
        17
        
        -        Commodity: "c"

      
        
        17
        +        Commodity: "c" j:1:14-2:0

      
        18
        18
                 CommodityPos: After

      
        19
        19
                 HasSpace: false

      
        20
        20
                 Precision: 0

      
M journal/parser/testdata/D_directive.txtar
···
        4
        4
         -- expect --

      
        5
        5
         Journal

      
        6
        6
           DefaultCommodityDirective j:1:1-2:1

      
        7
        
        -    Amount j:1:3-1:3

      
        
        7
        +    Amount j:1:3-2:0

      
        8
        8
               Quantity: 1

      
        9
        
        -      Commodity: "$"

      
        
        9
        +      Commodity: "$" j:1:3-1:4

      
        10
        10
               CommodityPos: Before

      
        11
        11
               HasSpace: false

      
        12
        12
               Precision: 2

      
        13
        13
               Decimal: "."

      
        14
        14
           DefaultCommodityDirective j:2:1-3:1

      
        15
        
        -    Amount j:2:3-2:3

      
        
        15
        +    Amount j:2:3-3:0

      
        16
        16
               Quantity: 10

      
        17
        
        -      Commodity: "UAH"

      
        
        17
        +      Commodity: "UAH" j:2:6-3:0

      
        18
        18
               CommodityPos: After

      
        19
        19
               HasSpace: true

      
        20
        20
               Precision: 0

      
M journal/parser/testdata/P_directive.txtar
···
        5
        5
           MarketPriceDirective j:1:1-2:1

      
        6
        6
             Date: 2024/01/01

      
        7
        7
             Commodity: "USD"

      
        8
        
        -    Amount j:1:18-1:18

      
        
        8
        +    Amount j:1:18-2:0

      
        9
        9
               Quantity: 40.5

      
        10
        
        -      Commodity: "UAH"

      
        
        10
        +      Commodity: "UAH" j:1:24-2:0

      
        11
        11
               CommodityPos: After

      
        12
        12
               HasSpace: true

      
        13
        13
               Precision: 2

      
M journal/parser/testdata/P_directive_with_time.txtar
···
        6
        6
             Date: 2024-01-01

      
        7
        7
             Time: 12:00:00

      
        8
        8
             Commodity: "USD"

      
        9
        
        -    Amount j:1:27-1:27

      
        
        9
        +    Amount j:1:27-2:0

      
        10
        10
               Quantity: 40.5

      
        11
        
        -      Commodity: "UAH"

      
        
        11
        +      Commodity: "UAH" j:1:33-2:0

      
        12
        12
               CommodityPos: After

      
        13
        13
               HasSpace: true

      
        14
        14
               Precision: 2

      
M journal/parser/testdata/account_with_spaces.txtar
···
        11
        11
               Account j:2:2-2:23

      
        12
        12
                 SubAccount: "expenses" j:2:2-2:10

      
        13
        13
                 SubAccount: "account name" j:2:11-2:23

      
        14
        
        -      Amount j:2:25-2:25

      
        
        14
        +      Amount j:2:25-3:0

      
        15
        15
                 Quantity: 20

      
        16
        16
                 Commodity: ""

      
        17
        17
                 CommodityPos: Before

      
M journal/parser/testdata/automated_transaction.txtar
···
        11
        11
         -- expect --

      
        12
        12
         Journal

      
        13
        13
           AutomatedTransaction j:1:1-4:0

      
        14
        
        -    Expr: "^income"

      
        
        14
        +    Expr: "^income" j:1:3-2:0

      
        15
        15
             Posting j:2:1-4:0

      
        16
        16
               Type: unbalanced virtual

      
        17
        17
               Account j:2:6-2:21

      
        18
        18
                 SubAccount: "liabilities" j:2:6-2:17

      
        19
        19
                 SubAccount: "tax" j:2:18-2:21

      
        20
        
        -      Amount j:2:25-2:25

      
        
        20
        +      Amount j:2:25-3:0

      
        21
        21
                 Quantity: 0.33

      
        22
        22
                 Commodity: ""

      
        23
        23
                 CommodityPos: Before

      ···
        27
        27
                 Decimal: "."

      
        28
        28
           BlankLine j:4:0-4:1

      
        29
        29
           AutomatedTransaction j:4:1-8:0

      
        30
        
        -    Expr: "expenses:gifts"

      
        
        30
        +    Expr: "expenses:gifts" j:4:3-5:0

      
        31
        31
             Posting j:5:1-6:1

      
        32
        32
               Account j:5:5-5:17

      
        33
        33
                 SubAccount: "budget" j:5:5-5:11

      
        34
        34
                 SubAccount: "gifts" j:5:12-5:17

      
        35
        
        -      Amount j:5:20-5:20

      
        
        35
        +      Amount j:5:20-6:0

      
        36
        36
                 Quantity: -1

      
        37
        37
                 Commodity: ""

      
        38
        38
                 CommodityPos: Before

      ···
        44
        44
               Account j:6:5-6:18

      
        45
        45
                 SubAccount: "assets" j:6:5-6:11

      
        46
        46
                 SubAccount: "budget" j:6:12-6:18

      
        47
        
        -      Amount j:6:21-6:21

      
        
        47
        +      Amount j:6:21-7:0

      
        48
        48
                 Quantity: 1

      
        49
        49
                 Commodity: ""

      
        50
        50
                 CommodityPos: Before

      ···
        54
        54
                 Decimal: "."

      
        55
        55
           BlankLine j:8:0-8:1

      
        56
        56
           AutomatedTransaction j:8:1-10:1

      
        57
        
        -    Expr: "income:salary"

      
        
        57
        +    Expr: "income:salary" j:8:3-9:0

      
        58
        58
             Posting j:9:1-10:1

      
        59
        59
               Account j:9:5-9:19

      
        60
        60
                 SubAccount: "budget" j:9:5-9:11

      
M journal/parser/testdata/bad_between_good.txtar
···
        12
        12
               Account j:2:5-2:18

      
        13
        13
                 SubAccount: "expenses" j:2:5-2:13

      
        14
        14
                 SubAccount: "food" j:2:14-2:18

      
        15
        
        -      Amount j:2:20-2:20

      
        
        15
        +      Amount j:2:20-3:0

      
        16
        16
                 Quantity: 10

      
        17
        
        -        Commodity: "$"

      
        
        17
        +        Commodity: "$" j:2:20-2:21

      
        18
        18
                 CommodityPos: Before

      
        19
        19
                 HasSpace: false

      
        20
        20
                 Precision: 0

      ···
        23
        23
               Account j:4:5-4:16

      
        24
        24
                 SubAccount: "assets" j:4:5-4:11

      
        25
        25
                 SubAccount: "cash" j:4:12-4:16

      
        26
        
        -      Amount j:4:18-4:18

      
        
        26
        +      Amount j:4:18-5:0

      
        27
        27
                 Quantity: 5

      
        28
        
        -        Commodity: "$"

      
        
        28
        +        Commodity: "$" j:4:18-4:19

      
        29
        29
                 CommodityPos: Before

      
        30
        30
                 HasSpace: false

      
        31
        31
                 Precision: 0

      
M journal/parser/testdata/bad_posting_at_end.txtar
···
        15
        15
               Account j:2:5-2:18

      
        16
        16
                 SubAccount: "expenses" j:2:5-2:13

      
        17
        17
                 SubAccount: "food" j:2:14-2:18

      
        18
        
        -      Amount j:2:20-2:20

      
        
        18
        +      Amount j:2:20-3:0

      
        19
        19
                 Quantity: 10

      
        20
        
        -        Commodity: "$"

      
        
        20
        +        Commodity: "$" j:2:20-2:21

      
        21
        21
                 CommodityPos: Before

      
        22
        22
                 HasSpace: false

      
        23
        23
                 Precision: 0

      ···
        30
        30
               Account j:6:5-6:18

      
        31
        31
                 SubAccount: "income" j:6:5-6:11

      
        32
        32
                 SubAccount: "salary" j:6:12-6:18

      
        33
        
        -      Amount j:6:20-6:20

      
        
        33
        +      Amount j:6:20-7:0

      
        34
        34
                 Quantity: 1000

      
        35
        
        -        Commodity: "$"

      
        
        35
        +        Commodity: "$" j:6:20-6:21

      
        36
        36
                 CommodityPos: Before

      
        37
        37
                 HasSpace: false

      
        38
        38
                 Precision: 0

      
M journal/parser/testdata/bad_then_next_transaction.txtar
···
        15
        15
               Account j:2:5-2:18

      
        16
        16
                 SubAccount: "expenses" j:2:5-2:13

      
        17
        17
                 SubAccount: "food" j:2:14-2:18

      
        18
        
        -      Amount j:2:20-2:20

      
        
        18
        +      Amount j:2:20-3:0

      
        19
        19
                 Quantity: 10

      
        20
        
        -        Commodity: "$"

      
        
        20
        +        Commodity: "$" j:2:20-2:21

      
        21
        21
                 CommodityPos: Before

      
        22
        22
                 HasSpace: false

      
        23
        23
                 Precision: 0

      ···
        30
        30
               Account j:6:5-6:18

      
        31
        31
                 SubAccount: "income" j:6:5-6:11

      
        32
        32
                 SubAccount: "salary" j:6:12-6:18

      
        33
        
        -      Amount j:6:20-6:20

      
        
        33
        +      Amount j:6:20-7:0

      
        34
        34
                 Quantity: 1000

      
        35
        
        -        Commodity: "$"

      
        
        35
        +        Commodity: "$" j:6:20-6:21

      
        36
        36
                 CommodityPos: Before

      
        37
        37
                 HasSpace: false

      
        38
        38
                 Precision: 0

      
M journal/parser/testdata/comment_between_bad_postings.txtar
···
        14
        14
               Account j:2:5-2:18

      
        15
        15
                 SubAccount: "expenses" j:2:5-2:13

      
        16
        16
                 SubAccount: "food" j:2:14-2:18

      
        17
        
        -      Amount j:2:20-2:20

      
        
        17
        +      Amount j:2:20-3:0

      
        18
        18
                 Quantity: 10

      
        19
        
        -        Commodity: "$"

      
        
        19
        +        Commodity: "$" j:2:20-2:21

      
        20
        20
                 CommodityPos: Before

      
        21
        21
                 HasSpace: false

      
        22
        22
                 Precision: 0

      
M journal/parser/testdata/commodity_quantity_after.txtar
···
        3
        3
         -- expect --

      
        4
        4
         Journal

      
        5
        5
           CommodityDirective j:1:1-2:1

      
        6
        
        -    Commodity: "UAH"

      
        7
        
        -    Amount j:1:15-1:15

      
        
        6
        +    Commodity: "UAH" j:1:11-1:14

      
        
        7
        +    Amount j:1:15-2:0

      
        8
        8
               Quantity: 1000

      
        9
        
        -      Commodity: "UAH"

      
        
        9
        +      Commodity: "UAH" j:1:11-1:14

      
        10
        10
               CommodityPos: Before

      
        11
        11
               HasSpace: true

      
        12
        12
               Precision: 2

      
M journal/parser/testdata/commodity_quantity_first.txtar
···
        3
        3
         -- expect --

      
        4
        4
         Journal

      
        5
        5
           CommodityDirective j:1:1-2:1

      
        6
        
        -    Commodity: "UAH"

      
        7
        
        -    Amount j:1:11-1:11

      
        
        6
        +    Commodity: "UAH" j:1:20-2:0

      
        
        7
        +    Amount j:1:11-2:0

      
        8
        8
               Quantity: 1000

      
        9
        
        -      Commodity: "UAH"

      
        
        9
        +      Commodity: "UAH" j:1:20-2:0

      
        10
        10
               CommodityPos: After

      
        11
        11
               HasSpace: true

      
        12
        12
               Precision: 2

      
M journal/parser/testdata/commodity_with_subdirectives.txtar
···
        5
        5
         -- expect --

      
        6
        6
         Journal

      
        7
        7
           CommodityDirective j:1:1-4:1

      
        8
        
        -    Commodity: "UAH"

      
        
        8
        +    Commodity: "UAH" j:1:11-2:0

      
M journal/parser/testdata/comodity_directive.txtar
···
        3
        3
         -- expect --

      
        4
        4
         Journal

      
        5
        5
           CommodityDirective j:1:1-2:1

      
        6
        
        -    Commodity: "$"

      
        
        6
        +    Commodity: "$" j:1:11-2:0

      
M journal/parser/testdata/comodity_directive_no_space.txtar
···
        3
        3
         -- expect --

      
        4
        4
         Journal

      
        5
        5
           CommodityDirective j:1:1-2:1

      
        6
        
        -    Commodity: "$"

      
        7
        
        -    Amount j:1:12-1:12

      
        
        6
        +    Commodity: "$" j:1:11-1:12

      
        
        7
        +    Amount j:1:12-2:0

      
        8
        8
               Quantity: 1000

      
        9
        
        -      Commodity: "$"

      
        
        9
        +      Commodity: "$" j:1:11-1:12

      
        10
        10
               CommodityPos: Before

      
        11
        11
               HasSpace: false

      
        12
        12
               Precision: 2

      
M journal/parser/testdata/comodity_directive_word.txtar
···
        3
        3
         -- expect --

      
        4
        4
         Journal

      
        5
        5
           CommodityDirective j:1:1-2:1

      
        6
        
        -    Commodity: "UAH"

      
        
        6
        +    Commodity: "UAH" j:1:11-2:0

      
M journal/parser/testdata/digit_group_marks.txtar
···
        12
        12
               Account j:2:3-2:16

      
        13
        13
                 SubAccount: "expenses" j:2:3-2:11

      
        14
        14
                 SubAccount: "food" j:2:12-2:16

      
        15
        
        -      Amount j:2:18-2:18

      
        
        15
        +      Amount j:2:18-3:0

      
        16
        16
                 Quantity: 1000

      
        17
        
        -        Commodity: "UAH"

      
        
        17
        +        Commodity: "UAH" j:2:27-3:0

      
        18
        18
                 CommodityPos: After

      
        19
        19
                 HasSpace: true

      
        20
        20
                 Precision: 2

      ···
        24
        24
               Account j:3:3-3:20

      
        25
        25
                 SubAccount: "expenses" j:3:3-3:11

      
        26
        26
                 SubAccount: "supplies" j:3:12-3:20

      
        27
        
        -      Amount j:3:22-3:22

      
        
        27
        +      Amount j:3:22-4:0

      
        28
        28
                 Quantity: 1000

      
        29
        
        -        Commodity: "USD"

      
        
        29
        +        Commodity: "USD" j:3:31-4:0

      
        30
        30
                 CommodityPos: After

      
        31
        31
                 HasSpace: true

      
        32
        32
                 Precision: 2

      ···
        36
        36
               Account j:4:3-4:18

      
        37
        37
                 SubAccount: "assets" j:4:3-4:9

      
        38
        38
                 SubAccount: "checking" j:4:10-4:18

      
        39
        
        -      Amount j:4:20-4:20

      
        
        39
        +      Amount j:4:20-5:0

      
        40
        40
                 Quantity: -2000

      
        41
        
        -        Commodity: "USD"

      
        
        41
        +        Commodity: "USD" j:4:30-5:0

      
        42
        42
                 CommodityPos: After

      
        43
        43
                 HasSpace: true

      
        44
        44
                 Precision: 2

      
M journal/parser/testdata/directive_prefixes.txtar
···
        8
        8
               SubAccount: "expenses" j:1:10-1:18

      
        9
        9
               SubAccount: "food" j:1:19-2:0

      
        10
        10
           CommodityDirective j:2:2-3:1

      
        11
        
        -    Commodity: "USD"

      
        
        11
        +    Commodity: "USD" j:2:12-3:0

      
M journal/parser/testdata/illegal_between_transactions.txtar
···
        15
        15
               Account j:2:5-2:18

      
        16
        16
                 SubAccount: "expenses" j:2:5-2:13

      
        17
        17
                 SubAccount: "food" j:2:14-2:18

      
        18
        
        -      Amount j:2:20-2:20

      
        
        18
        +      Amount j:2:20-3:0

      
        19
        19
                 Quantity: 10

      
        20
        
        -        Commodity: "$"

      
        
        20
        +        Commodity: "$" j:2:20-2:21

      
        21
        21
                 CommodityPos: Before

      
        22
        22
                 HasSpace: false

      
        23
        23
                 Precision: 0

      ···
        34
        34
               Account j:6:5-6:18

      
        35
        35
                 SubAccount: "income" j:6:5-6:11

      
        36
        36
                 SubAccount: "salary" j:6:12-6:18

      
        37
        
        -      Amount j:6:20-6:20

      
        
        37
        +      Amount j:6:20-7:0

      
        38
        38
                 Quantity: 1000

      
        39
        
        -        Commodity: "$"

      
        
        39
        +        Commodity: "$" j:6:20-6:21

      
        40
        40
                 CommodityPos: Before

      
        41
        41
                 HasSpace: false

      
        42
        42
                 Precision: 0

      
M journal/parser/testdata/multiple_bad_postings.txtar
···
        13
        13
               Account j:2:5-2:18

      
        14
        14
                 SubAccount: "expenses" j:2:5-2:13

      
        15
        15
                 SubAccount: "food" j:2:14-2:18

      
        16
        
        -      Amount j:2:20-2:20

      
        
        16
        +      Amount j:2:20-3:0

      
        17
        17
                 Quantity: 10

      
        18
        
        -        Commodity: "$"

      
        
        18
        +        Commodity: "$" j:2:20-2:21

      
        19
        19
                 CommodityPos: Before

      
        20
        20
                 HasSpace: false

      
        21
        21
                 Precision: 0

      
M journal/parser/testdata/nested_apply_tag_directives.txtar
···
        19
        19
               Account j:4:3-4:17

      
        20
        20
                 SubAccount: "expenses" j:4:3-4:11

      
        21
        21
                 SubAccount: "books" j:4:12-4:17

      
        22
        
        -      Amount j:4:19-4:19

      
        
        22
        +      Amount j:4:19-5:0

      
        23
        23
                 Quantity: 20

      
        24
        
        -        Commodity: "$"

      
        
        24
        +        Commodity: "$" j:4:19-4:20

      
        25
        25
                 CommodityPos: Before

      
        26
        26
                 HasSpace: false

      
        27
        27
                 Precision: 2

      
M journal/parser/testdata/period_transaction_expressions.txtar
···
        29
        29
               Account j:3:5-3:18

      
        30
        30
                 SubAccount: "expenses" j:3:5-3:13

      
        31
        31
                 SubAccount: "rent" j:3:14-3:18

      
        32
        
        -      Amount j:3:28-3:28

      
        
        32
        +      Amount j:3:28-4:0

      
        33
        33
                 Quantity: 2000

      
        34
        
        -        Commodity: "$"

      
        
        34
        +        Commodity: "$" j:3:28-3:29

      
        35
        35
                 CommodityPos: Before

      
        36
        36
                 HasSpace: false

      
        37
        37
                 Precision: 0

      ···
        52
        52
               Account j:7:5-7:23

      
        53
        53
                 SubAccount: "expenses" j:7:5-7:13

      
        54
        54
                 SubAccount: "utilities" j:7:14-7:23

      
        55
        
        -      Amount j:7:33-7:33

      
        
        55
        +      Amount j:7:33-8:0

      
        56
        56
                 Quantity: 400

      
        57
        
        -        Commodity: "$"

      
        
        57
        +        Commodity: "$" j:7:33-7:34

      
        58
        58
                 CommodityPos: Before

      
        59
        59
                 HasSpace: false

      
        60
        60
                 Precision: 0

      ···
        69
        69
           PeriodicTransaction j:10:1-14:0

      
        70
        70
             Period: "every 2 months"

      
        71
        71
             Status: "*"

      
        72
        
        -    Description: "in 2023, we will review"

      
        
        72
        +    Description: "in 2023, we will review" j:10:19-11:0

      
        73
        73
             Posting j:11:1-12:1

      
        74
        74
               Account j:11:5-11:23

      
        75
        75
                 SubAccount: "expenses" j:11:5-11:13

      
        76
        76
                 SubAccount: "utilities" j:11:14-11:23

      
        77
        
        -      Amount j:11:33-11:33

      
        
        77
        +      Amount j:11:33-12:0

      
        78
        78
                 Quantity: 400

      
        79
        
        -        Commodity: "$"

      
        
        79
        +        Commodity: "$" j:11:33-11:34

      
        80
        80
                 CommodityPos: Before

      
        81
        81
                 HasSpace: false

      
        82
        82
                 Precision: 0

      ···
        91
        91
           PeriodicTransaction j:14:1-18:0

      
        92
        92
             Period: "monthly"

      
        93
        93
             Status: "*"

      
        94
        
        -    Description: "Next year blah blah"

      
        
        94
        +    Description: "Next year blah blah" j:14:12-15:0

      
        95
        95
             Posting j:15:1-16:1

      
        96
        96
               Account j:15:5-15:18

      
        97
        97
                 SubAccount: "expenses" j:15:5-15:13

      
        98
        98
                 SubAccount: "food" j:15:14-15:18

      
        99
        
        -      Amount j:15:20-15:20

      
        
        99
        +      Amount j:15:20-16:0

      
        100
        100
                 Quantity: 100

      
        101
        
        -        Commodity: "$"

      
        
        101
        +        Commodity: "$" j:15:20-15:21

      
        102
        102
                 CommodityPos: Before

      
        103
        103
                 HasSpace: false

      
        104
        104
                 Precision: 0

      ···
        119
        119
               Account j:19:5-19:18

      
        120
        120
                 SubAccount: "expenses" j:19:5-19:13

      
        121
        121
                 SubAccount: "food" j:19:14-19:18

      
        122
        
        -      Amount j:19:20-19:20

      
        
        122
        +      Amount j:19:20-20:0

      
        123
        123
                 Quantity: 100

      
        124
        
        -        Commodity: "$"

      
        
        124
        +        Commodity: "$" j:19:20-19:21

      
        125
        125
                 CommodityPos: Before

      
        126
        126
                 HasSpace: false

      
        127
        127
                 Precision: 0

      
M journal/parser/testdata/quoted_payee_names.txtar
···
        11
        11
               Account j:2:3-2:16

      
        12
        12
                 SubAccount: "expenses" j:2:3-2:11

      
        13
        13
                 SubAccount: "food" j:2:12-2:16

      
        14
        
        -      Amount j:2:18-2:18

      
        
        14
        +      Amount j:2:18-3:0

      
        15
        15
                 Quantity: 10

      
        16
        
        -        Commodity: "$"

      
        
        16
        +        Commodity: "$" j:2:18-2:19

      
        17
        17
                 CommodityPos: Before

      
        18
        18
                 HasSpace: false

      
        19
        19
                 Precision: 0

      
M journal/parser/testdata/recovery_after_bad_posting.txtar
···
        24
        24
               Account j:6:5-6:18

      
        25
        25
                 SubAccount: "income" j:6:5-6:11

      
        26
        26
                 SubAccount: "salary" j:6:12-6:18

      
        27
        
        -      Amount j:6:20-6:20

      
        
        27
        +      Amount j:6:20-7:0

      
        28
        28
                 Quantity: 1000

      
        29
        
        -        Commodity: "$"

      
        
        29
        +        Commodity: "$" j:6:20-6:21

      
        30
        30
                 CommodityPos: Before

      
        31
        31
                 HasSpace: false

      
        32
        32
                 Precision: 0

      
M journal/parser/testdata/special_chars_in_description.txtar
···
        22
        22
           Transaction j:2:1-3:1

      
        23
        23
             Date: 2024/01/01

      
        24
        24
             Payee: "groceries" j:2:12-2:21

      
        25
        
        -    Note: "1 + 2"

      
        
        25
        +    Note: "1 + 2" j:2:24-3:0

      
        26
        26
           Transaction j:3:1-4:1

      
        27
        27
             Date: 2024/01/01

      
        28
        28
             Payee: "groceries" j:3:12-3:21

      
        29
        
        -    Note: "1 * 2"

      
        
        29
        +    Note: "1 * 2" j:3:24-4:0

      
        30
        30
           Transaction j:4:1-5:1

      
        31
        31
             Date: 2024/01/01

      
        32
        32
             Payee: "groceries" j:4:12-4:21

      
        33
        
        -    Note: "1 ! 2"

      
        
        33
        +    Note: "1 ! 2" j:4:24-5:0

      
        34
        34
           Transaction j:5:1-6:1

      
        35
        35
             Date: 2024/01/01

      
        36
        36
             Payee: "groceries" j:5:12-5:21

      
        37
        
        -    Note: "1 # 2"

      
        
        37
        +    Note: "1 # 2" j:5:24-6:0

      
        38
        38
           Transaction j:6:1-7:1

      
        39
        39
             Date: 2024/01/01

      
        40
        40
             Payee: "groceries" j:6:12-6:21

      
        41
        
        -    Note: "1 % 2"

      
        
        41
        +    Note: "1 % 2" j:6:24-7:0

      
        42
        42
           Transaction j:7:1-8:1

      
        43
        43
             Date: 2024/01/01

      
        44
        44
             Payee: "groceries" j:7:12-7:21

      
        45
        
        -    Note: "1 ^ 2"

      
        
        45
        +    Note: "1 ^ 2" j:7:24-8:0

      
        46
        46
           Transaction j:8:1-9:1

      
        47
        47
             Date: 2024/01/01

      
        48
        48
             Payee: "groceries" j:8:12-8:21

      
        49
        
        -    Note: "1 & 2"

      
        
        49
        +    Note: "1 & 2" j:8:24-9:0

      
        50
        50
           Transaction j:9:1-10:1

      
        51
        51
             Date: 2024/01/01

      
        52
        52
             Payee: "groceries" j:9:12-9:21

      
        53
        
        -    Note: "1 ( 2"

      
        
        53
        +    Note: "1 ( 2" j:9:24-10:0

      
        54
        54
           Transaction j:10:1-11:1

      
        55
        55
             Date: 2024/01/01

      
        56
        56
             Payee: "groceries" j:10:12-10:21

      
        57
        
        -    Note: "1 ) 2"

      
        
        57
        +    Note: "1 ) 2" j:10:24-11:0

      
        58
        58
           Transaction j:11:1-12:1

      
        59
        59
             Date: 2024/01/01

      
        60
        60
             Payee: "groceries" j:11:12-11:21

      
        61
        
        -    Note: "1 [ 2"

      
        
        61
        +    Note: "1 [ 2" j:11:24-12:0

      
        62
        62
           Transaction j:12:1-13:1

      
        63
        63
             Date: 2024/01/01

      
        64
        64
             Payee: "groceries" j:12:12-12:21

      
        65
        
        -    Note: "1 ] 2"

      
        
        65
        +    Note: "1 ] 2" j:12:24-13:0

      
        66
        66
           Transaction j:13:1-14:1

      
        67
        67
             Date: 2024/01/01

      
        68
        68
             Payee: "groceries" j:13:12-13:21

      
        69
        
        -    Note: "1 { 2"

      
        
        69
        +    Note: "1 { 2" j:13:24-14:0

      
        70
        70
           Transaction j:14:1-15:1

      
        71
        71
             Date: 2024/01/01

      
        72
        72
             Payee: "groceries" j:14:12-14:21

      
        73
        
        -    Note: "1 } 2"

      
        
        73
        +    Note: "1 } 2" j:14:24-15:0

      
        74
        74
           Transaction j:15:1-18:1

      
        75
        75
             Date: 2026-06-07

      
        76
        76
             State: "*"

      
        77
        77
             Payee: "payee !one" j:15:14-15:24

      
        78
        
        -    Note: "something *important*"

      
        
        78
        +    Note: "something *important*" j:15:27-16:0

      
        79
        79
             Posting j:16:1-17:1

      
        80
        80
               Account j:16:5-16:18

      
        81
        81
                 SubAccount: "expenses" j:16:5-16:13

      
        82
        82
                 SubAccount: "food" j:16:14-16:18

      
        83
        
        -      Amount j:16:20-16:20

      
        
        83
        +      Amount j:16:20-17:0

      
        84
        84
                 Quantity: 40

      
        85
        85
                 Commodity: ""

      
        86
        86
                 CommodityPos: Before

      
M journal/parser/testdata/three_bad_postings.txtar
···
        14
        14
               Account j:2:5-2:18

      
        15
        15
                 SubAccount: "expenses" j:2:5-2:13

      
        16
        16
                 SubAccount: "food" j:2:14-2:18

      
        17
        
        -      Amount j:2:20-2:20

      
        
        17
        +      Amount j:2:20-3:0

      
        18
        18
                 Quantity: 10

      
        19
        
        -        Commodity: "$"

      
        
        19
        +        Commodity: "$" j:2:20-2:21

      
        20
        20
                 CommodityPos: Before

      
        21
        21
                 HasSpace: false

      
        22
        22
                 Precision: 0

      
M journal/parser/testdata/transaction_in_ukrainian.txtar
···
        11
        11
               Account j:2:3-2:14

      
        12
        12
                 SubAccount: "витрати" j:2:3-2:10

      
        13
        13
                 SubAccount: "їжа" j:2:11-2:14

      
        14
        
        -      Amount j:2:16-2:16

      
        
        14
        +      Amount j:2:16-3:0

      
        15
        15
                 Quantity: 350

      
        16
        
        -        Commodity: "UAH"

      
        
        16
        +        Commodity: "UAH" j:2:20-3:0

      
        17
        17
                 CommodityPos: After

      
        18
        18
                 HasSpace: true

      
        19
        19
                 Precision: 0

      
M journal/parser/testdata/transaction_with_balance_assertion.txtar
···
        19
        19
               Account j:2:2-2:15

      
        20
        20
                 SubAccount: "expenses" j:2:2-2:10

      
        21
        21
                 SubAccount: "food" j:2:11-2:15

      
        22
        
        -      Amount j:2:17-2:17

      
        
        22
        +      Amount j:2:17-2:23

      
        23
        23
                 Quantity: 10

      
        24
        
        -        Commodity: "$"

      
        
        24
        +        Commodity: "$" j:2:17-2:18

      
        25
        25
                 CommodityPos: Before

      
        26
        26
                 HasSpace: false

      
        27
        27
                 Precision: 2

      ···
        29
        29
               BalanceAssertion j:2:24-3:0

      
        30
        30
                 IsStrict: false

      
        31
        31
                 IsInclusive: false

      
        32
        
        -        Amount j:2:26-2:26

      
        
        32
        +        Amount j:2:26-3:0

      
        33
        33
                   Quantity: 100

      
        34
        
        -          Commodity: "$"

      
        
        34
        +          Commodity: "$" j:2:26-2:27

      
        35
        35
                   CommodityPos: Before

      
        36
        36
                   HasSpace: false

      
        37
        37
                   Precision: 2

      ···
        49
        49
               Account j:6:2-6:15

      
        50
        50
                 SubAccount: "expenses" j:6:2-6:10

      
        51
        51
                 SubAccount: "food" j:6:11-6:15

      
        52
        
        -      Amount j:6:17-6:17

      
        
        52
        +      Amount j:6:17-6:23

      
        53
        53
                 Quantity: 10

      
        54
        
        -        Commodity: "$"

      
        
        54
        +        Commodity: "$" j:6:17-6:18

      
        55
        55
                 CommodityPos: Before

      
        56
        56
                 HasSpace: false

      
        57
        57
                 Precision: 2

      ···
        59
        59
               BalanceAssertion j:6:24-7:0

      
        60
        60
                 IsStrict: true

      
        61
        61
                 IsInclusive: false

      
        62
        
        -        Amount j:6:27-6:27

      
        
        62
        +        Amount j:6:27-7:0

      
        63
        63
                   Quantity: 100

      
        64
        
        -          Commodity: "$"

      
        
        64
        +          Commodity: "$" j:6:27-6:28

      
        65
        65
                   CommodityPos: Before

      
        66
        66
                   HasSpace: false

      
        67
        67
                   Precision: 2

      ···
        74
        74
               BalanceAssertion j:7:19-8:0

      
        75
        75
                 IsStrict: true

      
        76
        76
                 IsInclusive: false

      
        77
        
        -        Amount j:7:22-7:22

      
        
        77
        +        Amount j:7:22-8:0

      
        78
        78
                   Quantity: 0

      
        79
        
        -          Commodity: "$"

      
        
        79
        +          Commodity: "$" j:7:22-7:23

      
        80
        80
                   CommodityPos: Before

      
        81
        81
                   HasSpace: false

      
        82
        82
                   Precision: 2

      ···
        89
        89
               Account j:10:2-10:15

      
        90
        90
                 SubAccount: "expenses" j:10:2-10:10

      
        91
        91
                 SubAccount: "food" j:10:11-10:15

      
        92
        
        -      Amount j:10:17-10:17

      
        
        92
        +      Amount j:10:17-11:0

      
        93
        93
                 Quantity: 10

      
        94
        
        -        Commodity: "$"

      
        
        94
        +        Commodity: "$" j:10:17-10:18

      
        95
        95
                 CommodityPos: Before

      
        96
        96
                 HasSpace: false

      
        97
        97
                 Precision: 2

      ···
        104
        104
               BalanceAssertion j:11:19-12:0

      
        105
        105
                 IsStrict: false

      
        106
        106
                 IsInclusive: false

      
        107
        
        -        Amount j:11:21-11:21

      
        
        107
        +        Amount j:11:21-12:0

      
        108
        108
                   Quantity: 0

      
        109
        
        -          Commodity: "$"

      
        
        109
        +          Commodity: "$" j:11:21-11:22

      
        110
        110
                   CommodityPos: Before

      
        111
        111
                   HasSpace: false

      
        112
        112
                   Precision: 2

      
M journal/parser/testdata/transaction_with_code.txtar
···
        11
        11
               Account j:2:5-2:18

      
        12
        12
                 SubAccount: "expenses" j:2:5-2:13

      
        13
        13
                 SubAccount: "food" j:2:14-2:18

      
        14
        
        -      Amount j:2:20-2:20

      
        
        14
        +      Amount j:2:20-3:0

      
        15
        15
                 Quantity: 10

      
        16
        
        -        Commodity: "$"

      
        
        16
        +        Commodity: "$" j:2:20-2:21

      
        17
        17
                 CommodityPos: Before

      
        18
        18
                 HasSpace: false

      
        19
        19
                 Precision: 2

      
M journal/parser/testdata/transaction_with_cost_and_assertion.txtar
···
        11
        11
               Account j:2:2-2:14

      
        12
        12
                 SubAccount: "expenses" j:2:2-2:10

      
        13
        13
                 SubAccount: "atm" j:2:11-2:14

      
        14
        
        -      Amount j:2:16-2:16

      
        
        14
        +      Amount j:2:16-2:25

      
        15
        15
                 Quantity: 20

      
        16
        
        -        Commodity: "UAH"

      
        
        16
        +        Commodity: "UAH" j:2:16-2:19

      
        17
        17
                 CommodityPos: Before

      
        18
        18
                 HasSpace: true

      
        19
        19
                 Precision: 2

      
        20
        20
                 Decimal: "."

      
        21
        21
               Cost(unit) j:2:26-2:33

      
        22
        
        -        Amount j:2:28-2:28

      
        
        22
        +        Amount j:2:28-2:33

      
        23
        23
                   Quantity: 1

      
        24
        
        -          Commodity: "USD"

      
        
        24
        +          Commodity: "USD" j:2:30-2:33

      
        25
        25
                   CommodityPos: After

      
        26
        26
                   HasSpace: true

      
        27
        27
                   Precision: 0

      ···
        29
        29
               BalanceAssertion j:2:34-3:0

      
        30
        30
                 IsStrict: false

      
        31
        31
                 IsInclusive: false

      
        32
        
        -        Amount j:2:36-2:36

      
        
        32
        +        Amount j:2:36-3:0

      
        33
        33
                   Quantity: 0

      
        34
        
        -          Commodity: "UAH"

      
        
        34
        +          Commodity: "UAH" j:2:38-3:0

      
        35
        35
                   CommodityPos: After

      
        36
        36
                   HasSpace: true

      
        37
        37
                   Precision: 0

      
M journal/parser/testdata/transaction_with_costs.txtar
···
        23
        23
               Account j:2:2-2:14

      
        24
        24
                 SubAccount: "expenses" j:2:2-2:10

      
        25
        25
                 SubAccount: "atm" j:2:11-2:14

      
        26
        
        -      Amount j:2:16-2:16

      
        
        26
        +      Amount j:2:16-2:25

      
        27
        27
                 Quantity: 20

      
        28
        
        -        Commodity: "UAH"

      
        
        28
        +        Commodity: "UAH" j:2:22-2:25

      
        29
        29
                 CommodityPos: After

      
        30
        30
                 HasSpace: true

      
        31
        31
                 Precision: 2

      
        32
        32
                 Decimal: "."

      
        33
        33
               Cost(unit) j:2:26-3:0

      
        34
        
        -        Amount j:2:28-2:28

      
        
        34
        +        Amount j:2:28-3:0

      
        35
        35
                   Quantity: 1

      
        36
        
        -          Commodity: "USD"

      
        
        36
        +          Commodity: "USD" j:2:30-3:0

      
        37
        37
                   CommodityPos: After

      
        38
        38
                   HasSpace: true

      
        39
        39
                   Precision: 0

      ···
        51
        51
               Account j:6:2-6:14

      
        52
        52
                 SubAccount: "expenses" j:6:2-6:10

      
        53
        53
                 SubAccount: "atm" j:6:11-6:14

      
        54
        
        -      Amount j:6:16-6:16

      
        
        54
        +      Amount j:6:16-6:25

      
        55
        55
                 Quantity: 20

      
        56
        
        -        Commodity: "UAH"

      
        
        56
        +        Commodity: "UAH" j:6:22-6:25

      
        57
        57
                 CommodityPos: After

      
        58
        58
                 HasSpace: true

      
        59
        59
                 Precision: 2

      
        60
        60
                 Decimal: "."

      
        61
        61
               Cost(total) j:6:26-7:0

      
        62
        
        -        Amount j:6:29-6:29

      
        
        62
        +        Amount j:6:29-7:0

      
        63
        63
                   Quantity: 1

      
        64
        
        -          Commodity: "USD"

      
        
        64
        +          Commodity: "USD" j:6:31-7:0

      
        65
        65
                   CommodityPos: After

      
        66
        66
                   HasSpace: true

      
        67
        67
                   Precision: 0

      ···
        79
        79
               Account j:10:2-10:14

      
        80
        80
                 SubAccount: "expenses" j:10:2-10:10

      
        81
        81
                 SubAccount: "atm" j:10:11-10:14

      
        82
        
        -      Amount j:10:16-10:16

      
        
        82
        +      Amount j:10:16-10:25

      
        83
        83
                 Quantity: 20

      
        84
        
        -        Commodity: "UAH"

      
        
        84
        +        Commodity: "UAH" j:10:22-10:25

      
        85
        85
                 CommodityPos: After

      
        86
        86
                 HasSpace: true

      
        87
        87
                 Precision: 2

      
        88
        88
                 Decimal: "."

      
        89
        89
               Cost(unit) j:10:26-10:33

      
        90
        
        -        Amount j:10:28-10:28

      
        
        90
        +        Amount j:10:28-10:33

      
        91
        91
                   Quantity: 1

      
        92
        
        -          Commodity: "USD"

      
        
        92
        +          Commodity: "USD" j:10:30-10:33

      
        93
        93
                   CommodityPos: After

      
        94
        94
                   HasSpace: true

      
        95
        95
                   Precision: 0

      ···
        97
        97
               BalanceAssertion j:10:34-11:0

      
        98
        98
                 IsStrict: false

      
        99
        99
                 IsInclusive: false

      
        100
        
        -        Amount j:10:36-10:36

      
        
        100
        +        Amount j:10:36-10:45

      
        101
        101
                   Quantity: 20

      
        102
        
        -          Commodity: "UAH"

      
        
        102
        +          Commodity: "UAH" j:10:42-10:45

      
        103
        103
                   CommodityPos: After

      
        104
        104
                   HasSpace: true

      
        105
        105
                   Precision: 2

      
        106
        106
                   Decimal: "."

      
        107
        107
                 Cost(unit) j:10:46-11:0

      
        108
        
        -          Amount j:10:48-10:48

      
        
        108
        +          Amount j:10:48-11:0

      
        109
        109
                     Quantity: 1

      
        110
        
        -            Commodity: "USD"

      
        
        110
        +            Commodity: "USD" j:10:50-11:0

      
        111
        111
                     CommodityPos: After

      
        112
        112
                     HasSpace: true

      
        113
        113
                     Precision: 0

      ···
        125
        125
               Account j:14:5-14:16

      
        126
        126
                 SubAccount: "assets" j:14:5-14:11

      
        127
        127
                 SubAccount: "cash" j:14:12-14:16

      
        128
        
        -      Amount j:14:18-14:18

      
        
        128
        +      Amount j:14:18-14:25

      
        129
        129
                 Quantity: -20

      
        130
        
        -        Commodity: "EUR"

      
        
        130
        +        Commodity: "EUR" j:14:22-14:25

      
        131
        131
                 CommodityPos: After

      
        132
        132
                 HasSpace: true

      
        133
        133
                 Precision: 0

      
        134
        134
                 Decimal: "."

      
        135
        135
               Cost(unit) j:14:26-15:0

      
        136
        
        -        Amount j:14:28-14:28

      
        
        136
        +        Amount j:14:28-15:0

      
        137
        137
                   Quantity: 7.53

      
        138
        
        -          Commodity: "HRK"

      
        
        138
        +          Commodity: "HRK" j:14:33-15:0

      
        139
        139
                   CommodityPos: After

      
        140
        140
                   HasSpace: true

      
        141
        141
                   Precision: 2

      ···
        144
        144
               Account j:15:5-15:16

      
        145
        145
                 SubAccount: "assets" j:15:5-15:11

      
        146
        146
                 SubAccount: "cash" j:15:12-15:16

      
        147
        
        -      Amount j:15:18-15:18

      
        
        147
        +      Amount j:15:18-16:0

      
        148
        148
                 Quantity: 150.6

      
        149
        
        -        Commodity: "HRK"

      
        
        149
        +        Commodity: "HRK" j:15:25-16:0

      
        150
        150
                 CommodityPos: After

      
        151
        151
                 HasSpace: true

      
        152
        152
                 Precision: 2

      
M journal/parser/testdata/transaction_with_digit_payees.txtar
···
        15
        15
             Posting j:2:1-3:1

      
        16
        16
               Account j:2:5-2:45

      
        17
        17
                 SubAccount: "af0628973ff35bd62ddb048fa41dd8d83c1c46fe" j:2:5-2:45

      
        18
        
        -      Amount j:2:52-2:52

      
        
        18
        +      Amount j:2:52-3:0

      
        19
        19
                 Quantity: 474.31

      
        20
        
        -        Commodity: "$"

      
        
        20
        +        Commodity: "$" j:2:52-2:53

      
        21
        21
                 CommodityPos: Before

      
        22
        22
                 HasSpace: false

      
        23
        23
                 Precision: 2

      ···
        34
        34
             Posting j:6:1-7:1

      
        35
        35
               Account j:6:5-6:45

      
        36
        36
                 SubAccount: "0ecbb1b15e2cf3e515cc0f8533e5bb0fb2326728" j:6:5-6:45

      
        37
        
        -      Amount j:6:53-6:53

      
        
        37
        +      Amount j:6:53-7:0

      
        38
        38
                 Quantity: 14.91

      
        39
        
        -        Commodity: "$"

      
        
        39
        +        Commodity: "$" j:6:53-6:54

      
        40
        40
                 CommodityPos: Before

      
        41
        41
                 HasSpace: false

      
        42
        42
                 Precision: 2

      
M journal/parser/testdata/transaction_with_header_comment.txtar
···
        16
        16
               Account j:3:2-3:15

      
        17
        17
                 SubAccount: "expenses" j:3:2-3:10

      
        18
        18
                 SubAccount: "food" j:3:11-3:15

      
        19
        
        -      Amount j:3:17-3:17

      
        
        19
        +      Amount j:3:17-4:0

      
        20
        20
                 Quantity: 10

      
        21
        
        -        Commodity: "$"

      
        
        21
        +        Commodity: "$" j:3:17-3:18

      
        22
        22
                 CommodityPos: Before

      
        23
        23
                 HasSpace: false

      
        24
        24
                 Precision: 2

      
M journal/parser/testdata/transaction_with_inline_comment.txtar
···
        11
        11
               Account j:2:2-2:15

      
        12
        12
                 SubAccount: "Expenses" j:2:2-2:10

      
        13
        13
                 SubAccount: "Good" j:2:11-2:15

      
        14
        
        -      Amount j:2:17-2:17

      
        
        14
        +      Amount j:2:17-2:23

      
        15
        15
                 Quantity: 10

      
        16
        
        -        Commodity: "$"

      
        
        16
        +        Commodity: "$" j:2:17-2:18

      
        17
        17
                 CommodityPos: Before

      
        18
        18
                 HasSpace: false

      
        19
        19
                 Precision: 2

      
M journal/parser/testdata/transaction_with_note.txtar
···
        5
        5
           Transaction j:1:1-2:1

      
        6
        6
             Date: 2024-01-01

      
        7
        7
             Payee: "groceries" j:1:12-1:21

      
        8
        
        -    Note: "eating out"

      
        
        8
        +    Note: "eating out" j:1:24-2:0

      
M journal/parser/testdata/transaction_with_posting.txtar
···
        11
        11
               Account j:2:5-2:18

      
        12
        12
                 SubAccount: "expenses" j:2:5-2:13

      
        13
        13
                 SubAccount: "food" j:2:14-2:18

      
        14
        
        -      Amount j:2:20-2:20

      
        
        14
        +      Amount j:2:20-3:0

      
        15
        15
                 Quantity: 10

      
        16
        
        -        Commodity: "$"

      
        
        16
        +        Commodity: "$" j:2:20-2:21

      
        17
        17
                 CommodityPos: Before

      
        18
        18
                 HasSpace: false

      
        19
        19
                 Precision: 2

      
M journal/parser/testdata/transaction_with_posting_amounts.txtar
···
        11
        11
               Account j:2:5-2:18

      
        12
        12
                 SubAccount: "expenses" j:2:5-2:13

      
        13
        13
                 SubAccount: "food" j:2:14-2:18

      
        14
        
        -      Amount j:2:20-2:20

      
        
        14
        +      Amount j:2:20-3:0

      
        15
        15
                 Quantity: 10

      
        16
        
        -        Commodity: "UAH"

      
        
        16
        +        Commodity: "UAH" j:2:26-3:0

      
        17
        17
                 CommodityPos: After

      
        18
        18
                 HasSpace: true

      
        19
        19
                 Precision: 2

      ···
        22
        22
               Account j:3:5-3:20

      
        23
        23
                 SubAccount: "assets" j:3:5-3:11

      
        24
        24
                 SubAccount: "checking" j:3:12-3:20

      
        25
        
        -      Amount j:3:22-3:22

      
        
        25
        +      Amount j:3:22-4:0

      
        26
        26
                 Quantity: -10

      
        27
        
        -        Commodity: "UAH"

      
        
        27
        +        Commodity: "UAH" j:3:29-4:0

      
        28
        28
                 CommodityPos: After

      
        29
        29
                 HasSpace: true

      
        30
        30
                 Precision: 2

      
M journal/parser/testdata/transaction_with_quoted_commodity.txtar
···
        11
        11
               Account j:2:5-2:18

      
        12
        12
                 SubAccount: "expenses" j:2:5-2:13

      
        13
        13
                 SubAccount: "food" j:2:14-2:18

      
        14
        
        -      Amount j:2:20-2:20

      
        
        14
        +      Amount j:2:20-3:0

      
        15
        15
                 Quantity: 10

      
        16
        
        -        Commodity: "EUR"

      
        
        16
        +        Commodity: "EUR" j:2:26-3:0

      
        17
        17
                 CommodityPos: After

      
        18
        18
                 HasSpace: true

      
        19
        19
                 Precision: 2

      
M journal/parser/testdata/transaction_with_spaced_account_name.txtar
···
        10
        10
             Posting j:2:1-3:1

      
        11
        11
               Account j:2:5-2:11

      
        12
        12
                 SubAccount: "assets" j:2:5-2:11

      
        13
        
        -      Amount j:2:31-2:31

      
        
        13
        +      Amount j:2:31-2:34

      
        14
        14
                 Quantity: 21

      
        15
        15
                 Commodity: ""

      
        16
        16
                 CommodityPos: Before

      ···
        20
        20
               BalanceAssertion j:2:34-3:0

      
        21
        21
                 IsStrict: false

      
        22
        22
                 IsInclusive: false

      
        23
        
        -        Amount j:2:36-2:36

      
        
        23
        +        Amount j:2:36-3:0

      
        24
        24
                   Quantity: 21

      
        25
        25
                   Commodity: ""

      
        26
        26
                   CommodityPos: Before

      
M journal/parser/testdata/transaction_with_strange_commodity_symbols.txtar
···
        17
        17
             Posting j:3:1-4:1

      
        18
        18
               Account j:3:3-3:7

      
        19
        19
                 SubAccount: "asdf" j:3:3-3:7

      
        20
        
        -      Amount j:3:9-3:9

      
        
        20
        +      Amount j:3:9-4:0

      
        21
        21
                 Quantity: 123

      
        22
        
        -        Commodity: "$€£"

      
        
        22
        +        Commodity: "$€£" j:3:13-4:0

      
        23
        23
                 CommodityPos: After

      
        24
        24
                 HasSpace: true

      
        25
        25
                 Precision: 0

      ···
        34
        34
             Posting j:7:1-8:1

      
        35
        35
               Account j:7:3-7:7

      
        36
        36
                 SubAccount: "asdf" j:7:3-7:7

      
        37
        
        -      Amount j:7:9-7:9

      
        
        37
        +      Amount j:7:9-8:0

      
        38
        38
                 Quantity: 123

      
        39
        
        -        Commodity: "bytes"

      
        
        39
        +        Commodity: "bytes" j:7:13-8:0

      
        40
        40
                 CommodityPos: After

      
        41
        41
                 HasSpace: true

      
        42
        42
                 Precision: 0

      
M journal/parser/testdata/transaction_with_tabs.txtar
···
        11
        11
               Account j:2:2-2:15

      
        12
        12
                 SubAccount: "expenses" j:2:2-2:10

      
        13
        13
                 SubAccount: "food" j:2:11-2:15

      
        14
        
        -      Amount j:2:17-2:17

      
        
        14
        +      Amount j:2:17-3:0

      
        15
        15
                 Quantity: 10

      
        16
        
        -        Commodity: "$"

      
        
        16
        +        Commodity: "$" j:2:17-2:18

      
        17
        17
                 CommodityPos: Before

      
        18
        18
                 HasSpace: false

      
        19
        19
                 Precision: 2

      
M journal/parser/testdata/transaction_with_trailing_indent.txtar
···
        16
        16
                 SubAccount: "expenses" j:3:5-3:13

      
        17
        17
                 SubAccount: "personal" j:3:14-3:22

      
        18
        18
                 SubAccount: "tax" j:3:23-3:26

      
        19
        
        -      Amount j:3:40-3:40

      
        
        19
        +      Amount j:3:40-4:0

      
        20
        20
                 Quantity: 1250

      
        21
        
        -        Commodity: "$"

      
        
        21
        +        Commodity: "$" j:3:40-3:41

      
        22
        22
                 CommodityPos: Before

      
        23
        23
                 HasSpace: false

      
        24
        24
                 Precision: 0

      ···
        28
        28
                 SubAccount: "assets" j:4:5-4:11

      
        29
        29
                 SubAccount: "bank" j:4:12-4:16

      
        30
        30
                 SubAccount: "checking" j:4:17-4:25

      
        31
        
        -      Amount j:4:40-4:40

      
        
        31
        +      Amount j:4:40-5:0

      
        32
        32
                 Quantity: -1250

      
        33
        
        -        Commodity: "$"

      
        
        33
        +        Commodity: "$" j:4:40-4:41

      
        34
        34
                 CommodityPos: Before

      
        35
        35
                 HasSpace: false

      
        36
        36
                 Precision: 0

      
M journal/parser/testdata/transaction_with_unicode_commodity_symbols.txtar
···
        13
        13
               Account j:2:5-2:18

      
        14
        14
                 SubAccount: "expenses" j:2:5-2:13

      
        15
        15
                 SubAccount: "food" j:2:14-2:18

      
        16
        
        -      Amount j:2:20-2:20

      
        
        16
        +      Amount j:2:20-3:0

      
        17
        17
                 Quantity: 10

      
        18
        
        -        Commodity: "€"

      
        
        18
        +        Commodity: "€" j:2:20-2:21

      
        19
        19
                 CommodityPos: Before

      
        20
        20
                 HasSpace: false

      
        21
        21
                 Precision: 2

      ···
        24
        24
               Account j:3:5-3:18

      
        25
        25
                 SubAccount: "expenses" j:3:5-3:13

      
        26
        26
                 SubAccount: "food" j:3:14-3:18

      
        27
        
        -      Amount j:3:20-3:20

      
        
        27
        +      Amount j:3:20-4:0

      
        28
        28
                 Quantity: 5

      
        29
        
        -        Commodity: "£"

      
        
        29
        +        Commodity: "£" j:3:20-3:21

      
        30
        30
                 CommodityPos: Before

      
        31
        31
                 HasSpace: false

      
        32
        32
                 Precision: 2

      ···
        35
        35
               Account j:4:5-4:18

      
        36
        36
                 SubAccount: "expenses" j:4:5-4:13

      
        37
        37
                 SubAccount: "food" j:4:14-4:18

      
        38
        
        -      Amount j:4:20-4:20

      
        
        38
        +      Amount j:4:20-5:0

      
        39
        39
                 Quantity: 700

      
        40
        
        -        Commodity: "₹"

      
        
        40
        +        Commodity: "₹" j:4:20-4:21

      
        41
        41
                 CommodityPos: Before

      
        42
        42
                 HasSpace: false

      
        43
        43
                 Precision: 2

      
M journal/parser/testdata/transaction_with_virtual_accounts.txtar
···
        13
        13
               Account j:2:3-2:18

      
        14
        14
                 SubAccount: "virtual" j:2:3-2:10

      
        15
        15
                 SubAccount: "account" j:2:11-2:18

      
        16
        
        -      Amount j:2:21-2:21

      
        
        16
        +      Amount j:2:21-3:0

      
        17
        17
                 Quantity: 1

      
        18
        
        -        Commodity: "PESO"

      
        
        18
        +        Commodity: "PESO" j:2:23-3:0

      
        19
        19
                 CommodityPos: After

      
        20
        20
                 HasSpace: true

      
        21
        21
                 Precision: 0

      ···
        25
        25
               Account j:3:3-3:17

      
        26
        26
                 SubAccount: "something" j:3:3-3:12

      
        27
        27
                 SubAccount: "else" j:3:13-3:17

      
        28
        
        -      Amount j:3:21-3:21

      
        
        28
        +      Amount j:3:21-4:0

      
        29
        29
                 Quantity: 5

      
        30
        
        -        Commodity: "PESO"

      
        
        30
        +        Commodity: "PESO" j:3:23-4:0

      
        31
        31
                 CommodityPos: After

      
        32
        32
                 HasSpace: true

      
        33
        33
                 Precision: 0

      
M journal/parser/testdata/virtual_postings_with_statuses.txtar
···
        21
        21
               Account j:2:6-2:17

      
        22
        22
                 SubAccount: "assets" j:2:6-2:12

      
        23
        23
                 SubAccount: "cash" j:2:13-2:17

      
        24
        
        -      Amount j:2:20-2:20

      
        
        24
        +      Amount j:2:20-3:0

      
        25
        25
                 Quantity: 10

      
        26
        
        -        Commodity: "$"

      
        
        26
        +        Commodity: "$" j:2:20-2:21

      
        27
        27
                 CommodityPos: Before

      
        28
        28
                 HasSpace: false

      
        29
        29
                 Precision: 0

      ···
        33
        33
               Account j:3:4-3:15

      
        34
        34
                 SubAccount: "income" j:3:4-3:10

      
        35
        35
                 SubAccount: "gift" j:3:11-3:15

      
        36
        
        -      Amount j:3:18-3:18

      
        
        36
        +      Amount j:3:18-4:0

      
        37
        37
                 Quantity: -10

      
        38
        
        -        Commodity: "$"

      
        
        38
        +        Commodity: "$" j:3:18-3:19

      
        39
        39
                 CommodityPos: Before

      
        40
        40
                 HasSpace: false

      
        41
        41
                 Precision: 0

      ···
        50
        50
               Account j:6:8-6:19

      
        51
        51
                 SubAccount: "assets" j:6:8-6:14

      
        52
        52
                 SubAccount: "cash" j:6:15-6:19

      
        53
        
        -      Amount j:6:22-6:22

      
        
        53
        +      Amount j:6:22-7:0

      
        54
        54
                 Quantity: 10

      
        55
        
        -        Commodity: "$"

      
        
        55
        +        Commodity: "$" j:6:22-6:23

      
        56
        56
                 CommodityPos: Before

      
        57
        57
                 HasSpace: false

      
        58
        58
                 Precision: 0

      ···
        62
        62
               Account j:7:6-7:17

      
        63
        63
                 SubAccount: "income" j:7:6-7:12

      
        64
        64
                 SubAccount: "gift" j:7:13-7:17

      
        65
        
        -      Amount j:7:20-7:20

      
        
        65
        +      Amount j:7:20-8:0

      
        66
        66
                 Quantity: -10

      
        67
        
        -        Commodity: "$"

      
        
        67
        +        Commodity: "$" j:7:20-7:21

      
        68
        68
                 CommodityPos: Before

      
        69
        69
                 HasSpace: false

      
        70
        70
                 Precision: 0

      ···
        79
        79
               Account j:10:10-10:21

      
        80
        80
                 SubAccount: "assets" j:10:10-10:16

      
        81
        81
                 SubAccount: "cash" j:10:17-10:21

      
        82
        
        -      Amount j:10:24-10:24

      
        
        82
        +      Amount j:10:24-11:0

      
        83
        83
                 Quantity: 10

      
        84
        
        -        Commodity: "$"

      
        
        84
        +        Commodity: "$" j:10:24-10:25

      
        85
        85
                 CommodityPos: Before

      
        86
        86
                 HasSpace: false

      
        87
        87
                 Precision: 0

      ···
        92
        92
               Account j:11:8-11:19

      
        93
        93
                 SubAccount: "income" j:11:8-11:14

      
        94
        94
                 SubAccount: "gift" j:11:15-11:19

      
        95
        
        -      Amount j:11:22-11:22

      
        
        95
        +      Amount j:11:22-12:0

      
        96
        96
                 Quantity: -10

      
        97
        
        -        Commodity: "$"

      
        
        97
        +        Commodity: "$" j:11:22-11:23

      
        98
        98
                 CommodityPos: Before

      
        99
        99
                 HasSpace: false

      
        100
        100
                 Precision: 0

      
M journal/printer/transaction.go
···
        21
        21
         	}

      
        22
        22
         

      
        23
        23
         	// code

      
        24
        
        -	if t.Code != nil && *t.Code != "" {

      
        
        24
        +	if t.Code != nil && t.Code.Value != "" {

      
        25
        25
         		p.buf.WriteString(" (")

      
        26
        
        -		p.buf.WriteString(*t.Code)

      
        
        26
        +		p.buf.WriteString(t.Code.Value)

      
        27
        27
         		p.buf.WriteByte(')')

      
        28
        28
         	}

      
        29
        29
         

      ···
        34
        34
         	}

      
        35
        35
         

      
        36
        36
         	// note

      
        37
        
        -	if t.Note != nil && *t.Note != "" {

      
        
        37
        +	if t.Note != nil && t.Note.Value != "" {

      
        38
        38
         		p.buf.WriteString(" | ")

      
        39
        
        -		p.buf.WriteString(*t.Note)

      
        
        39
        +		p.buf.WriteString(t.Note.Value)

      
        40
        40
         	}

      
        41
        41
         

      
        42
        42
         	p.writeComment(t.Comment)

      ···
        69
        69
         	}

      
        70
        70
         

      
        71
        71
         	// code

      
        72
        
        -	if t.Code != nil && *t.Code != "" {

      
        
        72
        +	if t.Code != nil && t.Code.Value != "" {

      
        73
        73
         		p.buf.WriteString(" (")

      
        74
        
        -		p.buf.WriteString(*t.Code)

      
        
        74
        +		p.buf.WriteString(t.Code.Value)

      
        75
        75
         		p.buf.WriteByte(')')

      
        76
        76
         	}

      
        77
        77
         

      
        78
        78
         	// description

      
        79
        
        -	if t.Description != nil && *t.Description != "" {

      
        
        79
        +	if t.Description != nil && t.Description.Value != "" {

      
        80
        80
         		p.buf.WriteByte(' ')

      
        81
        
        -		p.buf.WriteString(*t.Description)

      
        
        81
        +		p.buf.WriteString(t.Description.Value)

      
        82
        82
         	}

      
        83
        83
         

      
        84
        84
         	// comment

      ···
        100
        100
         	p.buf.WriteByte('=')

      
        101
        101
         

      
        102
        102
         	// expression

      
        103
        
        -	if t.Expr != "" {

      
        
        103
        +	if t.Expr.Value != "" {

      
        104
        104
         		p.buf.WriteByte(' ')

      
        105
        
        -		p.buf.WriteString(t.Expr)

      
        
        105
        +		p.buf.WriteString(t.Expr.Value)

      
        106
        106
         	}

      
        107
        107
         

      
        108
        108
         	// comment