all repos

clerk @ 6573acb

missing tooling for ledger/hledger
23 files changed, 192 insertions(+), 204 deletions(-)
ast: inline leaf value+span wrapper structs
Author: Oleksandr Smirnov olexsmir@gmail.com
Committed at: 2026-09-08 14:48:57 +0300
Authored at: 2026-09-08 14:21:06 +0300
Change ID: vxsokqnqlsmwvqzruvuknyvrkzqpmrno
Parent: c958816
M internal/analyzer/analyzer.go
···
        3
        3
         import (

      
        4
        4
         	"olexsmir.xyz/clerk/journal"

      
        5
        5
         	"olexsmir.xyz/clerk/journal/ast"

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

      
        6
        7
         )

      
        7
        8
         

      
        8
        9
         type Analysis struct {

      ···
        68
        69
         

      
        69
        70
         type PayeeUsage struct {

      
        70
        71
         	FileIndex int

      
        71
        
        -	Payee     *ast.Payee

      
        
        72
        +	Name      string

      
        
        73
        +	Span      token.Span

      
        72
        74
         }

      
        73
        75
         

      
        74
        76
         type TagInfo struct {

      
M internal/analyzer/analyzer_test.go
···
        137
        137
         		fmt.Fprintf(w, "    directives: %d\n", len(info.Directives))

      
        138
        138
         		fmt.Fprintf(w, "    used: %d\n", info.UsedCount)

      
        139
        139
         		for _, d := range info.Directives {

      
        140
        
        -			if d.Name != nil {

      
        141
        
        -				fmt.Fprintf(w, "      payee %s\n", d.Name.Name)

      
        
        140
        +			if d.Name != "" {

      
        
        141
        +				fmt.Fprintf(w, "      payee %s\n", d.Name)

      
        142
        142
         			}

      
        143
        143
         		}

      
        144
        144
         		fmt.Fprintf(w, "    usages: %d\n", len(info.Usage))

      
        145
        145
         		for _, u := range info.Usage {

      
        146
        
        -			fmt.Fprintf(w, "      file %d: %s\n", u.FileIndex, u.Payee.Name)

      
        
        146
        +			fmt.Fprintf(w, "      file %d: %s\n", u.FileIndex, u.Name)

      
        147
        147
         		}

      
        148
        148
         		if info.LastUsed.Year != 0 {

      
        149
        149
         			fmt.Fprintf(w, "    last-used: %d-%02d-%02d\n", info.LastUsed.Year, info.LastUsed.Month, info.LastUsed.Day)

      ···
        180
        180
         		case *ast.CommodityDirective:

      
        181
        181
         			fmt.Fprintf(w, "  commodity %s\n", d.Commodity)

      
        182
        182
         		case *ast.PayeeDirective:

      
        183
        
        -			if d.Name != nil {

      
        184
        
        -				fmt.Fprintf(w, "  payee %s\n", d.Name.Name)

      
        
        183
        +			if d.Name != "" {

      
        
        184
        +				fmt.Fprintf(w, "  payee %s\n", d.Name)

      
        185
        185
         			}

      
        186
        186
         		case *ast.TagDirective:

      
        187
        187
         			fmt.Fprintf(w, "  tag %s\n", d.Name)

      
M internal/analyzer/build.go
···
        6
        6
         

      
        7
        7
         	"olexsmir.xyz/clerk/journal"

      
        8
        8
         	"olexsmir.xyz/clerk/journal/ast"

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

      
        9
        10
         )

      
        10
        11
         

      
        11
        12
         // Build constructs [Analysis] from a flat resolved journal view.

      ···
        45
        46
         	var b strings.Builder

      
        46
        47
         	b.WriteString(tx.Date.String())

      
        47
        48
         	b.WriteByte('|')

      
        48
        
        -	if tx.Payee != nil {

      
        49
        
        -		b.WriteString(tx.Payee.Name)

      
        
        49
        +	if tx.Payee != "" {

      
        
        50
        +		b.WriteString(tx.Payee)

      
        50
        51
         	}

      
        51
        52
         	b.WriteByte('|')

      
        52
        53
         	for i := range tx.Postings {

      ···
        61
        62
         	templates := make(map[string][]PostingTemplate)

      
        62
        63
         	for _, tx := range a.Transactions {

      
        63
        64
         		payee := ""

      
        64
        
        -		if tx.Payee != nil {

      
        65
        
        -			payee = tx.Payee.Name

      
        
        65
        +		if tx.Payee != "" {

      
        
        66
        +			payee = tx.Payee

      
        66
        67
         		}

      
        67
        68
         		if payee == "" {

      
        68
        69
         			continue

      ···
        99
        100
         	case *ast.Comment:

      
        100
        101
         		a.addCommentTags(fileIndex, nil, e)

      
        101
        102
         	case *ast.Transaction:

      
        102
        
        -		a.addPayee(fileIndex, e.Payee)

      
        
        103
        +		a.addPayee(fileIndex, e.Payee, e.PayeeSpan)

      
        103
        104
         		a.Transactions = append(a.Transactions, e)

      
        104
        105
         		a.TransactionsCountByDate[e.Date.String()]++

      
        105
        106
         

      ···
        163
        164
         }

      
        164
        165
         

      
        165
        166
         func (a *Analysis) addPayeeDirective(pd *ast.PayeeDirective) {

      
        166
        
        -	if pd.Name == nil {

      
        
        167
        +	if pd.Name == "" {

      
        167
        168
         		return

      
        168
        169
         	}

      
        169
        
        -	info, ok := a.Payees[pd.Name.Name]

      
        
        170
        +	info, ok := a.Payees[pd.Name]

      
        170
        171
         	if !ok {

      
        171
        172
         		info = &PayeeInfo{}

      
        172
        
        -		a.Payees[pd.Name.Name] = info

      
        
        173
        +		a.Payees[pd.Name] = info

      
        173
        174
         	}

      
        174
        175
         	info.Directives = append(info.Directives, pd)

      
        175
        176
         }

      ···
        214
        215
         	info.Directives = append(info.Directives, cd)

      
        215
        216
         }

      
        216
        217
         

      
        217
        
        -func (a *Analysis) addPayee(fileIndex int, payee *ast.Payee) {

      
        218
        
        -	if payee == nil {

      
        
        218
        +func (a *Analysis) addPayee(fileIndex int, name string, span token.Span) {

      
        
        219
        +	if name == "" {

      
        219
        220
         		return

      
        220
        221
         	}

      
        221
        
        -	info, ok := a.Payees[payee.Name]

      
        
        222
        +	info, ok := a.Payees[name]

      
        222
        223
         	if !ok {

      
        223
        224
         		info = &PayeeInfo{}

      
        224
        
        -		a.Payees[payee.Name] = info

      
        
        225
        +		a.Payees[name] = info

      
        225
        226
         	}

      
        226
        227
         	info.Usage = append(info.Usage, PayeeUsage{

      
        227
        228
         		FileIndex: fileIndex,

      
        228
        
        -		Payee:     payee,

      
        
        229
        +		Name:      name,

      
        
        230
        +		Span:      span,

      
        229
        231
         	})

      
        230
        232
         	info.UsedCount++

      
        231
        233
         }

      
M internal/linter/rule_missing_payee.go
···
        11
        11
         func (m *MissingPayee) CheckJournal(an *analyzer.Analysis) []Find {

      
        12
        12
         	var finds []Find

      
        13
        13
         	for _, txn := range an.Transactions {

      
        14
        
        -		if txn.Payee == nil {

      
        
        14
        +		if txn.Payee == "" {

      
        15
        15
         			finds = append(finds, Find{

      
        16
        16
         				Code:    m.ID(),

      
        17
        17
         				Message: "transaction has no payee",

      
M internal/linter/rule_missing_status.go
···
        14
        14
         func (m *MissingStatus) CheckJournal(an *analyzer.Analysis) []Find {

      
        15
        15
         	var finds []Find

      
        16
        16
         	for _, txn := range an.Transactions {

      
        17
        
        -		if txn.Status.Value == ast.StatusNone {

      
        
        17
        +		if txn.Status == ast.StatusNone {

      
        18
        18
         			finds = append(finds, Find{

      
        19
        19
         				Code:    m.ID(),

      
        20
        20
         				Message: "transaction has no status",

      
        21
        
        -				Span:    txn.Status.Span,

      
        
        21
        +				Span:    txn.StatusSpan,

      
        22
        22
         			})

      
        23
        23
         		}

      
        24
        24
         	}

      
M internal/linter/rule_undeclared_payee.go
···
        20
        20
         		for _, usage := range info.Usage {

      
        21
        21
         			finds = append(finds, Find{

      
        22
        22
         				Code:    u.ID(),

      
        23
        
        -				Span:    usage.Payee.Span,

      
        
        23
        +				Span:    usage.Span,

      
        24
        24
         				Message: "undeclared payee: " + name,

      
        25
        25
         			})

      
        26
        26
         		}

      
M internal/lsp/textdocument_definition.go
···
        85
        85
         	case *ast.PeriodicTransaction:

      
        86
        86
         		span = e.Period.Span

      
        87
        87
         	case *ast.AutomatedTransaction:

      
        88
        
        -		span = e.Expr.Span

      
        
        88
        +		span = e.ExprSpan

      
        89
        89
         	default:

      
        90
        90
         		return nil

      
        91
        91
         	}

      ···
        114
        114
         	}

      
        115
        115
         	if len(info.Directives) > 0 {

      
        116
        116
         		d := info.Directives[0]

      
        117
        
        -		if d.Name == nil {

      
        
        117
        +		if d.Name == "" {

      
        118
        118
         			return nil

      
        119
        119
         		}

      
        120
        
        -		return locationForDirective(an, d, d.Name.Span)

      
        
        120
        +		return locationForDirective(an, d, d.NameSpan)

      
        121
        121
         	}

      
        122
        122
         	if len(info.Usage) > 0 {

      
        123
        123
         		u := info.Usage[0]

      
        124
        
        -		return locationFor(an, u.FileIndex, u.Payee.Span)

      
        
        124
        +		return locationFor(an, u.FileIndex, u.Span)

      
        125
        125
         	}

      
        126
        126
         	return nil

      
        127
        127
         }

      
M internal/lsp/textdocument_document_symbol.go
···
        37
        37
         	case *ast.PeriodicTransaction:

      
        38
        38
         		kind, name, sel, whole = symbolTransaction, transactionName(e), e.Period.Span, e.Span

      
        39
        39
         	case *ast.AutomatedTransaction:

      
        40
        
        -		kind, name, sel, whole = symbolTransaction, transactionName(e), e.Expr.Span, e.Span

      
        
        40
        +		kind, name, sel, whole = symbolTransaction, transactionName(e), e.ExprSpan, e.Span

      
        41
        41
         	case *ast.AccountDirective:

      
        42
        42
         		kind, name, sel, whole = symbolAccount, e.Account.String(), e.Account.Span, e.Span

      
        43
        43
         	case *ast.CommodityDirective:

      
        44
        44
         		kind, name, sel, whole = symbolCommodity, e.Commodity, e.CommoditySpan, e.Span

      
        45
        45
         	case *ast.PayeeDirective:

      
        46
        
        -		if e.Name == nil {

      
        
        46
        +		if e.Name == "" {

      
        47
        47
         			return protocol.DocumentSymbol{}, false

      
        48
        48
         		}

      
        49
        
        -		kind, name, sel, whole = symbolPayee, e.Name.Name, e.Name.Span, e.Span

      
        
        49
        +		kind, name, sel, whole = symbolPayee, e.Name, e.NameSpan, e.Span

      
        50
        50
         	case *ast.TagDirective:

      
        51
        51
         		sp, ok := tagDirectiveSpan(string(src), e)

      
        52
        52
         		if !ok {

      
M internal/lsp/textdocument_hover.go
···
        74
        74
         func hoverInEntry(content string, e ast.Entry, cursor int) *hoverElement {

      
        75
        75
         	switch e := e.(type) {

      
        76
        76
         	case *ast.Transaction:

      
        77
        
        -		if e.Payee != nil && spanContains(content, e.Payee.Span, cursor) {

      
        78
        
        -			return &hoverElement{kind: hoverPayee, span: e.Payee.Span, name: e.Payee.Name}

      
        
        77
        +		if e.Payee != "" && spanContains(content, e.PayeeSpan, cursor) {

      
        
        78
        +			return &hoverElement{kind: hoverPayee, span: e.PayeeSpan, name: e.Payee}

      
        79
        79
         		}

      
        80
        80
         		if spanContains(content, e.Date.Span, cursor) {

      
        81
        81
         			return &hoverElement{kind: hoverDate, span: e.Date.Span, tx: e}

      ···
        132
        132
         			return &hoverElement{kind: hoverCommodity, span: e.CommoditySpan, name: e.Commodity}

      
        133
        133
         		}

      
        134
        134
         	case *ast.PayeeDirective:

      
        135
        
        -		if e.Name != nil && spanContains(content, e.Name.Span, cursor) {

      
        136
        
        -			return &hoverElement{kind: hoverPayee, span: e.Name.Span, name: e.Name.Name}

      
        
        135
        +		if e.Name != "" && spanContains(content, e.NameSpan, cursor) {

      
        
        136
        +			return &hoverElement{kind: hoverPayee, span: e.NameSpan, name: e.Name}

      
        137
        137
         		}

      
        138
        138
         	case *ast.TagDirective:

      
        139
        139
         		if e.Name != "" {

      ···
        316
        316
         func buildDateHover(an *analyzer.Analysis, tx *ast.Transaction) string {

      
        317
        317
         	var sb strings.Builder

      
        318
        318
         	fmt.Fprintf(&sb, "**Date:** %04d-%02d-%02d", tx.Date.Year, tx.Date.Month, tx.Date.Day)

      
        319
        
        -	if tx.Payee != nil {

      
        320
        
        -		fmt.Fprintf(&sb, "\n\n**Payee:** %s", tx.Payee.Name)

      
        
        319
        +	if tx.Payee != "" {

      
        
        320
        +		fmt.Fprintf(&sb, "\n\n**Payee:** %s", tx.Payee)

      
        321
        321
         	}

      
        322
        322
         	fmt.Fprintf(&sb, "\n\n**Transactions:** %d", an.CountTransactionsOnDate(tx.Date))

      
        323
        323
         	fmt.Fprintf(&sb, "\n\n**Postings:** %d", len(tx.Postings))

      
M internal/lsp/textdocument_references.go
···
        83
        83
         	}

      
        84
        84
         	var locations []protocol.Location

      
        85
        85
         	for _, u := range info.Usage {

      
        86
        
        -		appendLocation(&locations, locationFor(an, u.FileIndex, u.Payee.Span))

      
        
        86
        +		appendLocation(&locations, locationFor(an, u.FileIndex, u.Span))

      
        87
        87
         	}

      
        88
        88
         	if includeDeclaration {

      
        89
        89
         		for _, d := range info.Directives {

      
        90
        
        -			if d.Name != nil {

      
        91
        
        -				appendLocation(&locations, locationForDirective(an, d, d.Name.Span))

      
        
        90
        +			if d.Name != "" {

      
        
        91
        +				appendLocation(&locations, locationForDirective(an, d, d.NameSpan))

      
        92
        92
         			}

      
        93
        93
         		}

      
        94
        94
         	}

      
M internal/lsp/textdocument_rename.go
···
        94
        94
         func symbolInEntry(content string, e ast.Entry, cursor int) *symbolRef {

      
        95
        95
         	switch e := e.(type) {

      
        96
        96
         	case *ast.Transaction:

      
        97
        
        -		if e.Payee != nil && spanContains(content, e.Payee.Span, cursor) {

      
        98
        
        -			return &symbolRef{symbolPayee, e.Payee.Name, e.Payee.Span}

      
        
        97
        +		if e.Payee != "" && spanContains(content, e.PayeeSpan, cursor) {

      
        
        98
        +			return &symbolRef{symbolPayee, e.Payee, e.PayeeSpan}

      
        99
        99
         		}

      
        100
        100
         		if ref := tagRefInComment(content, e.Comment, cursor); ref != nil {

      
        101
        101
         			return ref

      ···
        149
        149
         			return &symbolRef{symbolCommodity, e.Commodity, e.CommoditySpan}

      
        150
        150
         		}

      
        151
        151
         	case *ast.PayeeDirective:

      
        152
        
        -		if e.Name != nil && spanContains(content, e.Name.Span, cursor) {

      
        153
        
        -			return &symbolRef{symbolPayee, e.Name.Name, e.Name.Span}

      
        
        152
        +		if e.Name != "" && spanContains(content, e.NameSpan, cursor) {

      
        
        153
        +			return &symbolRef{symbolPayee, e.Name, e.NameSpan}

      
        154
        154
         		}

      
        155
        155
         	case *ast.IncludeDirective:

      
        156
        156
         		if spanContains(content, e.Span, cursor) {

      ···
        353
        353
         		return

      
        354
        354
         	}

      
        355
        355
         	for _, d := range info.Directives {

      
        356
        
        -		if d.Name != nil {

      
        
        356
        +		if d.Name != "" {

      
        357
        357
         			if fileIdx := fileIndexForEntry(an, d); fileIdx >= 0 {

      
        358
        
        -				add(fileIdx, d.Name.Span, newName)

      
        
        358
        +				add(fileIdx, d.NameSpan, newName)

      
        359
        359
         			}

      
        360
        360
         		}

      
        361
        361
         	}

      
        362
        362
         	for _, u := range info.Usage {

      
        363
        
        -		add(u.FileIndex, u.Payee.Span, newName)

      
        
        363
        +		add(u.FileIndex, u.Span, newName)

      
        364
        364
         	}

      
        365
        365
         }

      
        366
        366
         

      
M internal/lsp/textdocument_selection_range.go
···
        70
        70
         	case *ast.CommodityDirective:

      
        71
        71
         		return commodityDirectiveSelection(content, t, li, cursor, parent)

      
        72
        72
         	case *ast.PayeeDirective:

      
        73
        
        -		if t.Name != nil {

      
        74
        
        -			if sel, ok := selectionForSpan(content, li, t.Name.Span, cursor, parent); ok {

      
        
        73
        +		if t.Name != "" {

      
        
        74
        +			if sel, ok := selectionForSpan(content, li, t.NameSpan, cursor, parent); ok {

      
        75
        75
         				return sel

      
        76
        76
         			}

      
        77
        77
         		}

      ···
        127
        127
         	if t.SecondDate != nil {

      
        128
        128
         		sps = append(sps, t.SecondDate.Span)

      
        129
        129
         	}

      
        130
        
        -	if t.Status.Value != ast.StatusNone {

      
        131
        
        -		sps = append(sps, t.Status.Span)

      
        
        130
        +	if t.Status != ast.StatusNone {

      
        
        131
        +		sps = append(sps, t.StatusSpan)

      
        132
        132
         	}

      
        133
        
        -	if t.Code != nil {

      
        134
        
        -		sps = append(sps, t.Code.Span)

      
        
        133
        +	if t.Code != "" {

      
        
        134
        +		sps = append(sps, t.CodeSpan)

      
        135
        135
         	}

      
        136
        
        -	if t.Payee != nil {

      
        137
        
        -		sps = append(sps, t.Payee.Span)

      
        
        136
        +	if t.Payee != "" {

      
        
        137
        +		sps = append(sps, t.PayeeSpan)

      
        138
        138
         	}

      
        139
        
        -	if t.Note != nil {

      
        140
        
        -		sps = append(sps, t.Note.Span)

      
        
        139
        +	if t.Note != "" {

      
        
        140
        +		sps = append(sps, t.NoteSpan)

      
        141
        141
         	}

      
        142
        142
         	for _, sp := range sps {

      
        143
        143
         		if sel, ok := selectionForSpan(content, li, sp, cursor, parent); ok {

      ···
        178
        178
         		}

      
        179
        179
         		return sel

      
        180
        180
         	}

      
        181
        
        -	if pt.Description != nil {

      
        182
        
        -		if sel, ok := selectionForSpan(content, li, pt.Description.Span, cursor, parent); ok {

      
        
        181
        +	if pt.Description != "" {

      
        
        182
        +		if sel, ok := selectionForSpan(content, li, pt.DescriptionSpan, cursor, parent); ok {

      
        183
        183
         			return sel

      
        184
        184
         		}

      
        185
        185
         	}

      ···
        187
        187
         }

      
        188
        188
         

      
        189
        189
         func automatedSelection(content string, at *ast.AutomatedTransaction, li *lsputil.LineIndex, cursor int, parent protocol.SelectionRange) protocol.SelectionRange {

      
        190
        
        -	if sel, ok := selectionForSpan(content, li, at.Expr.Span, cursor, parent); ok {

      
        
        190
        +	if sel, ok := selectionForSpan(content, li, at.ExprSpan, cursor, parent); ok {

      
        191
        191
         		return sel

      
        192
        192
         	}

      
        193
        193
         	return commentsAndPostingsSelection(content, at.Comment, at.HeaderComments, at.Postings, li, cursor, parent)

      ···
        238
        238
         }

      
        239
        239
         

      
        240
        240
         func postingSelection(content string, p ast.Posting, li *lsputil.LineIndex, cursor int, parent protocol.SelectionRange) protocol.SelectionRange {

      
        241
        
        -	if p.Status.Value != ast.StatusNone {

      
        242
        
        -		if sel, ok := selectionForSpan(content, li, p.Status.Span, cursor, parent); ok {

      
        
        241
        +	if p.Status != ast.StatusNone {

      
        
        242
        +		if sel, ok := selectionForSpan(content, li, p.StatusSpan, cursor, parent); ok {

      
        243
        243
         			return sel

      
        244
        244
         		}

      
        245
        245
         	}

      
M internal/lsp/textdocument_semantic_tokens.go
···
        519
        519
         	if t.SecondDate != nil {

      
        520
        520
         		emit(t.SecondDate.Span, semDate, 0)

      
        521
        521
         	}

      
        522
        
        -	if t.Status.Value != ast.StatusNone {

      
        523
        
        -		emit(t.Status.Span, semStatus, 0)

      
        
        522
        +	if t.Status != ast.StatusNone {

      
        
        523
        +		emit(t.StatusSpan, semStatus, 0)

      
        524
        524
         	}

      
        525
        
        -	if t.Code != nil {

      
        526
        
        -		emit(t.Code.Span, semString, 0)

      
        
        525
        +	if t.Code != "" {

      
        
        526
        +		emit(t.CodeSpan, semString, 0)

      
        527
        527
         	}

      
        528
        
        -	if t.Payee != nil {

      
        529
        
        -		emit(t.Payee.Span, semProperty, 0)

      
        
        528
        +	if t.Payee != "" {

      
        
        529
        +		emit(t.PayeeSpan, semProperty, 0)

      
        530
        530
         	}

      
        531
        
        -	if t.Note != nil {

      
        532
        
        -		emit(t.Note.Span, semProperty, 0)

      
        
        531
        +	if t.Note != "" {

      
        
        532
        +		emit(t.NoteSpan, semProperty, 0)

      
        533
        533
         	}

      
        534
        534
         	emitComment(t.Comment, emit)

      
        535
        535
         	for i := range t.HeaderComments {

      ···
        565
        565
         			emit(offsetSpan(pt.Period.Span.File, pos, pt.Period.Span.End.Offset), semProperty, 0)

      
        566
        566
         		}

      
        567
        567
         	}

      
        568
        
        -	if pt.Description != nil {

      
        569
        
        -		emit(pt.Description.Span, semProperty, 0)

      
        
        568
        +	if pt.Description != "" {

      
        
        569
        +		emit(pt.DescriptionSpan, semProperty, 0)

      
        570
        570
         	}

      
        571
        571
         	emitComment(pt.Comment, emit)

      
        572
        572
         	for i := range pt.HeaderComments {

      ···
        581
        581
         	// = operator is at the start of the expression span

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

      
        583
        583
         

      
        584
        
        -	if at.Expr.Value != "" {

      
        585
        
        -		emit(at.Expr.Span, semString, 0)

      
        
        584
        +	if at.Expr != "" {

      
        
        585
        +		emit(at.ExprSpan, semString, 0)

      
        586
        586
         	}

      
        587
        587
         	emitComment(at.Comment, emit)

      
        588
        588
         	for i := range at.HeaderComments {

      ···
        594
        594
         }

      
        595
        595
         

      
        596
        596
         func visitPosting(content string, p ast.Posting, emit semEmitFunc) {

      
        597
        
        -	if p.Status.Value != ast.StatusNone {

      
        598
        
        -		emit(p.Status.Span, semStatus, 0)

      
        
        597
        +	if p.Status != ast.StatusNone {

      
        
        598
        +		emit(p.StatusSpan, semStatus, 0)

      
        599
        599
         	}

      
        600
        600
         

      
        601
        601
         	// virtual brackets

      
M internal/lsp/workspace_symbol.go
···
        117
        117
         	case *ast.Transaction:

      
        118
        118
         		b.Grow(24)

      
        119
        119
         		b.WriteString(e.Date.String())

      
        120
        
        -		if s := e.Status.Value.String(); s != "" {

      
        
        120
        +		if s := e.Status.String(); s != "" {

      
        121
        121
         			b.WriteByte(' ')

      
        122
        122
         			b.WriteString(s)

      
        123
        123
         		}

      
        124
        
        -		if e.Payee != nil {

      
        
        124
        +		if e.Payee != "" {

      
        125
        125
         			b.WriteByte(' ')

      
        126
        
        -			b.WriteString(e.Payee.Name)

      
        
        126
        +			b.WriteString(e.Payee)

      
        127
        127
         		}

      
        128
        
        -		if e.Note != nil {

      
        
        128
        +		if e.Note != "" {

      
        129
        129
         			b.WriteString(" | ")

      
        130
        
        -			b.WriteString(e.Note.Value)

      
        
        130
        +			b.WriteString(e.Note)

      
        131
        131
         		}

      
        132
        132
         	case *ast.PeriodicTransaction:

      
        133
        133
         		b.WriteByte('~')

      
        134
        
        -		if s := e.Status.Value.String(); s != "" {

      
        
        134
        +		if s := e.Status.String(); s != "" {

      
        135
        135
         			b.WriteByte(' ')

      
        136
        136
         			b.WriteString(s)

      
        137
        137
         		}

      
        138
        138
         		b.WriteByte(' ')

      
        139
        139
         		b.WriteString(e.Period.Raw)

      
        140
        
        -		if e.Description != nil {

      
        
        140
        +		if e.Description != "" {

      
        141
        141
         			b.WriteString(" | ")

      
        142
        
        -			b.WriteString(e.Description.Value)

      
        
        142
        +			b.WriteString(e.Description)

      
        143
        143
         		}

      
        144
        144
         	case *ast.AutomatedTransaction:

      
        145
        145
         		b.WriteByte('=')

      
        146
        146
         		b.WriteByte(' ')

      
        147
        
        -		b.WriteString(e.Expr.Value)

      
        
        147
        +		b.WriteString(e.Expr)

      
        148
        148
         	}

      
        149
        149
         	return b.String()

      
        150
        150
         }

      
M journal/ast/ast.go
···
        119
        119
         	StatusPending                   // ! pending

      
        120
        120
         )

      
        121
        121
         

      
        122
        
        -type Status struct {

      
        123
        
        -	Value StatusType

      
        124
        
        -	Span  token.Span

      
        125
        
        -}

      
        126
        
        -

      
        127
        
        -type Code struct {

      
        128
        
        -	Value string

      
        129
        
        -	Span  token.Span

      
        130
        
        -}

      
        131
        
        -

      
        132
        
        -type Note struct {

      
        133
        
        -	Value string

      
        134
        
        -	Span  token.Span

      
        135
        
        -}

      
        136
        
        -

      
        137
        
        -type Description struct {

      
        138
        
        -	Value string

      
        139
        
        -	Span  token.Span

      
        140
        
        -}

      
        141
        
        -

      
        142
        
        -type Expr struct {

      
        143
        
        -	Value string

      
        144
        
        -	Span  token.Span

      
        145
        
        -}

      
        146
        
        -

      
        147
        
        -type Payee struct {

      
        148
        
        -	Name string

      
        149
        
        -	Span token.Span

      
        150
        
        -}

      
        151
        
        -

      
        152
        122
         type SubAccount struct {

      
        153
        123
         	Name string

      
        154
        124
         	Span token.Span

      
M journal/ast/directives.go
···
        61
        61
         }

      
        62
        62
         

      
        63
        63
         type PayeeDirective struct {

      
        64
        
        -	Name    *Payee

      
        65
        
        -	Comment *Comment

      
        66
        
        -	Span    token.Span

      
        
        64
        +	Name     string

      
        
        65
        +	NameSpan token.Span

      
        
        66
        +	Comment  *Comment

      
        
        67
        +	Span     token.Span

      
        67
        68
         }

      
        68
        69
         

      
        69
        70
         func (PayeeDirective) entryNode() {}

      
M journal/ast/dump.go
···
        57
        57
         	case *PayeeDirective:

      
        58
        58
         		indent(b, depth)

      
        59
        59
         		fmt.Fprintf(b, "PayeeDirective %s\n", e.Span)

      
        60
        
        -		if e.Name != nil {

      
        
        60
        +		if e.Name != "" {

      
        61
        61
         			indent(b, depth+1)

      
        62
        
        -			fmt.Fprintf(b, "Name: %q %s\n", e.Name.Name, e.Name.Span)

      
        
        62
        +			fmt.Fprintf(b, "Name: %q %s\n", e.Name, e.NameSpan)

      
        63
        63
         		}

      
        64
        64
         		dumpOptComment(b, e.Comment, depth+1)

      
        65
        65
         	case *TagDirective:

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

      
        131
        131
         		fmt.Fprintf(b, "SecondDate: %s\n", dumpDate(*t.SecondDate))

      
        132
        132
         	}

      
        133
        
        -	if t.Status.Value != StatusNone {

      
        
        133
        +	if t.Status != StatusNone {

      
        134
        134
         		indent(b, depth+1)

      
        135
        
        -		fmt.Fprintf(b, "State: %q\n", t.Status.Value)

      
        
        135
        +		fmt.Fprintf(b, "State: %q\n", t.Status)

      
        136
        136
         	}

      
        137
        
        -	if t.Code != nil {

      
        
        137
        +	if t.Code != "" {

      
        138
        138
         		indent(b, depth+1)

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

      
        
        139
        +		fmt.Fprintf(b, "Code: %q %s\n", t.Code, t.CodeSpan)

      
        140
        140
         	}

      
        141
        
        -	if t.Payee != nil {

      
        
        141
        +	if t.Payee != "" {

      
        142
        142
         		indent(b, depth+1)

      
        143
        
        -		fmt.Fprintf(b, "Payee: %q %s\n", t.Payee.Name, t.Payee.Span)

      
        
        143
        +		fmt.Fprintf(b, "Payee: %q %s\n", t.Payee, t.PayeeSpan)

      
        144
        144
         	}

      
        145
        
        -	if t.Note != nil {

      
        
        145
        +	if t.Note != "" {

      
        146
        146
         		indent(b, depth+1)

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

      
        
        147
        +		fmt.Fprintf(b, "Note: %q %s\n", t.Note, t.NoteSpan)

      
        148
        148
         	}

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

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

      ···
        163
        163
         	indent(b, depth)

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

      
        165
        165
         	indent(b, depth+1)

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

      
        
        166
        +	fmt.Fprintf(b, "Expr: %q %s\n", t.Expr, t.ExprSpan)

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

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

      
        169
        169
         		indent(b, depth+1)

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

      
        191
        191
         		fmt.Fprintf(b, "To: %s\n", dumpDate(*t.Period.To))

      
        192
        192
         	}

      
        193
        
        -	if t.Status.Value != StatusNone {

      
        
        193
        +	if t.Status != StatusNone {

      
        194
        194
         		indent(b, depth+1)

      
        195
        
        -		fmt.Fprintf(b, "Status: %q\n", t.Status.Value)

      
        
        195
        +		fmt.Fprintf(b, "Status: %q\n", t.Status)

      
        196
        196
         	}

      
        197
        
        -	if t.Code != nil {

      
        
        197
        +	if t.Code != "" {

      
        198
        198
         		indent(b, depth+1)

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

      
        
        199
        +		fmt.Fprintf(b, "Code: %q %s\n", t.Code, t.CodeSpan)

      
        200
        200
         	}

      
        201
        
        -	if t.Description != nil {

      
        
        201
        +	if t.Description != "" {

      
        202
        202
         		indent(b, depth+1)

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

      
        
        203
        +		fmt.Fprintf(b, "Description: %q %s\n", t.Description, t.DescriptionSpan)

      
        204
        204
         	}

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

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

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

      
        223
        223
         		fmt.Fprintf(b, "Type: %s\n", p.Type)

      
        224
        224
         	}

      
        225
        
        -	if p.Status.Value != StatusNone {

      
        
        225
        +	if p.Status != StatusNone {

      
        226
        226
         		indent(b, depth+1)

      
        227
        
        -		fmt.Fprintf(b, "Status: %q\n", p.Status.Value)

      
        
        227
        +		fmt.Fprintf(b, "Status: %q\n", p.Status)

      
        228
        228
         	}

      
        229
        229
         	dumpAccount(b, p.Account, depth+1)

      
        230
        230
         	if p.Amount != nil {

      
M journal/ast/entries.go
···
        12
        12
         type Transaction struct {

      
        13
        13
         	Date           Date

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

      
        15
        
        -	Status         Status     // optional */! status

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

      
        17
        
        -	Payee          *Payee     // optional payee

      
        18
        
        -	Note           *Note      // part after |

      
        
        15
        +	Status         StatusType // optional */! status

      
        
        16
        +	StatusSpan     token.Span

      
        
        17
        +	Code           string // optional (123) code

      
        
        18
        +	CodeSpan       token.Span

      
        
        19
        +	Payee          string // optional payee

      
        
        20
        +	PayeeSpan      token.Span

      
        
        21
        +	Note           string // part after |

      
        
        22
        +	NoteSpan       token.Span

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

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

      
        21
        25
         	Postings       []Posting

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

      
        35
        39
         

      
        36
        40
         type PeriodicTransaction struct {

      
        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
        
        -	HeaderComments []*Comment

      
        43
        
        -	Postings       []Posting

      
        44
        
        -	Span           token.Span

      
        
        41
        +	Period          Period // period-expr

      
        
        42
        +	Status          StatusType

      
        
        43
        +	StatusSpan      token.Span

      
        
        44
        +	Code            string

      
        
        45
        +	CodeSpan        token.Span

      
        
        46
        +	Description     string

      
        
        47
        +	DescriptionSpan token.Span

      
        
        48
        +	Comment         *Comment // optional inline comment

      
        
        49
        +	HeaderComments  []*Comment

      
        
        50
        +	Postings        []Posting

      
        
        51
        +	Span            token.Span

      
        45
        52
         }

      
        46
        53
         

      
        47
        54
         func (PeriodicTransaction) entryNode() {}

      
        48
        55
         

      
        49
        56
         type AutomatedTransaction struct {

      
        50
        
        -	Expr           Expr

      
        
        57
        +	Expr           string

      
        
        58
        +	ExprSpan       token.Span

      
        51
        59
         	Postings       []Posting

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

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

      ···
        78
        86
         }

      
        79
        87
         

      
        80
        88
         type Posting struct {

      
        81
        
        -	Type     PostingType

      
        82
        
        -	Status   Status

      
        83
        
        -	Account  Account

      
        84
        
        -	Amount   *Amount // nil == auto-balancing

      
        85
        
        -	Cost     *Cost   // @ @@

      
        86
        
        -	Balance  *BalanceAssertion

      
        87
        
        -	Comment  *Comment

      
        88
        
        -	Comments []Comment // continuation comment lines

      
        89
        
        -	Span     token.Span

      
        
        89
        +	Type       PostingType

      
        
        90
        +	Status     StatusType

      
        
        91
        +	StatusSpan token.Span

      
        
        92
        +	Account    Account

      
        
        93
        +	Amount     *Amount // nil == auto-balancing

      
        
        94
        +	Cost       *Cost   // @ @@

      
        
        95
        +	Balance    *BalanceAssertion

      
        
        96
        +	Comment    *Comment

      
        
        97
        +	Comments   []Comment // continuation comment lines

      
        
        98
        +	Span       token.Span

      
        90
        99
         }

      
        91
        100
         

      
        92
        101
         type Amount struct {

      
M journal/loader_test.go
···
        74
        74
         		return "account " + e.Account.String()

      
        75
        75
         	case *ast.Transaction:

      
        76
        76
         		s := fmt.Sprintf("%04d-%02d-%02d", e.Date.Year, e.Date.Month, e.Date.Day)

      
        77
        
        -		if e.Payee != nil {

      
        78
        
        -			s += " " + e.Payee.Name

      
        
        77
        +		if e.Payee != "" {

      
        
        78
        +			s += " " + e.Payee

      
        79
        79
         		}

      
        80
        80
         		return s

      
        81
        81
         	default:

      
M journal/parser/parser.go
···
        129
        129
         	p.skipWhitespace()

      
        130
        130
         

      
        131
        131
         	// optional status

      
        132
        
        -	tx.Status = p.parseStatus()

      
        
        132
        +	tx.Status, tx.StatusSpan = p.parseStatus()

      
        133
        133
         

      
        134
        134
         	// optional code - the lexer emits "(CODE)" as a single TEXT token; split it here

      
        135
        135
         	if p.got(token.TEXT) {

      
        136
        136
         		if lit := p.cur.Literal; len(lit) >= 2 && lit[0] == '(' && lit[len(lit)-1] == ')' {

      
        137
        
        -			tx.Code = &ast.Code{Value: lit[1 : len(lit)-1], Span: p.cur.Span}

      
        
        137
        +			tx.Code = lit[1 : len(lit)-1]

      
        
        138
        +			tx.CodeSpan = p.cur.Span

      
        138
        139
         			p.advance()

      
        139
        140
         			p.skipWhitespace()

      
        140
        141
         		}

      ···
        142
        143
         

      
        143
        144
         	// optional payee | note

      
        144
        145
         	if p.got(token.TEXT) || p.got(token.STRING) {

      
        145
        
        -		tx.Payee = p.parsePayee()

      
        
        146
        +		tx.Payee, tx.PayeeSpan = p.parsePayee()

      
        146
        147
         

      
        147
        148
         		// check for | separator

      
        148
        149
         		p.skipWhitespace()

      ···
        153
        154
         				sn := p.cur.Span

      
        154
        155
         				n := p.cur.Literal

      
        155
        156
         				p.advance()

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

      
        
        157
        +				tx.Note = n

      
        
        158
        +				tx.NoteSpan = p.span(sn)

      
        157
        159
         			}

      
        158
        160
         		}

      
        159
        161
         	}

      ···
        174
        176
         	return s

      
        175
        177
         }

      
        176
        178
         

      
        177
        
        -func (p *Parser) parsePayee() *ast.Payee {

      
        
        179
        +func (p *Parser) parsePayee() (string, token.Span) {

      
        178
        180
         	s := p.cur.Span

      
        179
        181
         

      
        180
        182
         	if p.got(token.STRING) {

      
        181
        183
         		name := unquote(p.cur.Literal)

      
        182
        184
         		p.advance()

      
        183
        
        -		return &ast.Payee{Name: name, Span: p.span(s)}

      
        
        185
        +		return name, p.span(s)

      
        184
        186
         	}

      
        185
        187
         

      
        186
        188
         	// keep spaces/tags between text tokens; stop before trailing whitespace

      ···
        189
        191
         		_, _ = name.WriteString(p.cur.Literal)

      
        190
        192
         		p.advance()

      
        191
        193
         	}

      
        192
        
        -	return &ast.Payee{Name: unquote(name.String()), Span: p.span(s)}

      
        
        194
        +	return unquote(name.String()), p.span(s)

      
        193
        195
         }

      
        194
        196
         

      
        195
        197
         func isPayeeWord(t token.Type) bool {

      ···
        210
        212
         	pt.Period = p.parsePeriod()

      
        211
        213
         

      
        212
        214
         	if desc, dspan := p.parseOptPeriodicDescription(); desc != "" {

      
        213
        
        -		pt.Description = &ast.Description{Value: desc, Span: dspan}

      
        
        215
        +		pt.Description = desc

      
        
        216
        +		pt.DescriptionSpan = dspan

      
        214
        217
         	}

      
        215
        218
         

      
        216
        219
         	comment := p.parseOptInlineComment()

      ···
        233
        236
         	// expression

      
        234
        237
         	sd := p.cur.Span

      
        235
        238
         	expr := p.parseDirectiveExpr()

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

      
        
        239
        +	at.Expr = expr

      
        
        240
        +	at.ExprSpan = p.span(sd)

      
        237
        241
         	at.Comment = p.parseOptInlineComment()

      
        238
        242
         	p.expectNewline()

      
        239
        243
         

      ···
        541
        545
         	p.expect(token.PAYEE)

      
        542
        546
         	p.skipWhitespace()

      
        543
        547
         

      
        544
        
        -	var name *ast.Payee

      
        
        548
        +	name, nameSpan := "", token.Span{}

      
        545
        549
         	if p.got(token.TEXT) || p.got(token.STRING) || p.got(token.COMMODITYMARK) {

      
        546
        
        -		name = p.parsePayee()

      
        
        550
        +		name, nameSpan = p.parsePayee()

      
        547
        551
         	}

      
        548
        552
         

      
        549
        553
         	comment := p.parseOptInlineComment()

      
        550
        554
         	p.expectNewline()

      
        551
        555
         

      
        552
        556
         	return &ast.PayeeDirective{

      
        553
        
        -		Name:    name,

      
        554
        
        -		Comment: comment,

      
        555
        
        -		Span:    p.span(s),

      
        
        557
        +		Name:     name,

      
        
        558
        +		NameSpan: nameSpan,

      
        
        559
        +		Comment:  comment,

      
        
        560
        +		Span:     p.span(s),

      
        556
        561
         	}

      
        557
        562
         }

      
        558
        563
         

      ···
        821
        826
         	}

      
        822
        827
         }

      
        823
        828
         

      
        824
        
        -func (p *Parser) parseStatus() ast.Status {

      
        
        829
        +func (p *Parser) parseStatus() (ast.StatusType, token.Span) {

      
        825
        830
         	s := p.cur.Span

      
        826
        
        -	st := ast.Status{}

      
        
        831
        +	st := ast.StatusNone

      
        827
        832
         	switch p.cur.Type {

      
        828
        833
         	case token.STAR:

      
        829
        
        -		st.Value = ast.StatusCleared

      
        
        834
        +		st = ast.StatusCleared

      
        830
        835
         	case token.BANG:

      
        831
        
        -		st.Value = ast.StatusPending

      
        
        836
        +		st = ast.StatusPending

      
        832
        837
         	}

      
        833
        
        -	if st.Value != ast.StatusNone {

      
        
        838
        +	if st != ast.StatusNone {

      
        834
        839
         		p.advance()

      
        835
        840
         		p.skipWhitespace()

      
        836
        841
         	}

      
        837
        
        -	st.Span = p.span(s)

      
        838
        
        -	return st

      
        
        842
        +	return st, p.span(s)

      
        839
        843
         }

      
        840
        844
         

      
        841
        845
         func (p *Parser) isAmountStart() bool {

      ···
        946
        950
         	}

      
        947
        951
         

      
        948
        952
         	// optional status, outside of brackets, '! (account)'

      
        949
        
        -	posting.Status = p.parseStatus()

      
        
        953
        +	posting.Status, posting.StatusSpan = p.parseStatus()

      
        950
        954
         

      
        951
        955
         	// detect virtual posting brackets

      
        952
        956
         	switch p.cur.Type {

      ···
        960
        964
         

      
        961
        965
         	// optional status, inside of brackets, '(* account)'

      
        962
        966
         	if p.got(token.STAR) || p.got(token.BANG) {

      
        963
        
        -		posting.Status = p.parseStatus()

      
        
        967
        +		posting.Status, posting.StatusSpan = p.parseStatus()

      
        964
        968
         	}

      
        965
        969
         

      
        966
        970
         	// validate, must be account text

      
M journal/printer/directives.go
···
        97
        97
         

      
        98
        98
         func (p *printer) writePayeeDirective(pd *ast.PayeeDirective) {

      
        99
        99
         	p.buf.WriteString("payee ")

      
        100
        
        -	p.buf.WriteString(quoteString(pd.Name.Name))

      
        
        100
        +	p.buf.WriteString(quoteString(pd.Name))

      
        101
        101
         	p.writeInlineComment(pd.Comment)

      
        102
        102
         }

      
        103
        103
         

      
M journal/printer/transaction.go
···
        15
        15
         	}

      
        16
        16
         

      
        17
        17
         	// status

      
        18
        
        -	if t.Status.Value != ast.StatusNone {

      
        
        18
        +	if t.Status != ast.StatusNone {

      
        19
        19
         		p.buf.WriteByte(' ')

      
        20
        
        -		p.buf.WriteString(t.Status.Value.String())

      
        
        20
        +		p.buf.WriteString(t.Status.String())

      
        21
        21
         	}

      
        22
        22
         

      
        23
        23
         	// code

      
        24
        
        -	if t.Code != nil && t.Code.Value != "" {

      
        
        24
        +	if t.Code != "" {

      
        25
        25
         		p.buf.WriteString(" (")

      
        26
        
        -		p.buf.WriteString(t.Code.Value)

      
        
        26
        +		p.buf.WriteString(t.Code)

      
        27
        27
         		p.buf.WriteByte(')')

      
        28
        28
         	}

      
        29
        29
         

      
        30
        30
         	// payee

      
        31
        
        -	if t.Payee != nil && t.Payee.Name != "" {

      
        
        31
        +	if t.Payee != "" {

      
        32
        32
         		p.buf.WriteByte(' ')

      
        33
        
        -		p.buf.WriteString(t.Payee.Name)

      
        
        33
        +		p.buf.WriteString(t.Payee)

      
        34
        34
         	}

      
        35
        35
         

      
        36
        36
         	// note

      
        37
        
        -	if t.Note != nil && t.Note.Value != "" {

      
        
        37
        +	if t.Note != "" {

      
        38
        38
         		p.buf.WriteString(" | ")

      
        39
        
        -		p.buf.WriteString(t.Note.Value)

      
        
        39
        +		p.buf.WriteString(t.Note)

      
        40
        40
         	}

      
        41
        41
         

      
        42
        42
         	p.writeComment(t.Comment)

      ···
        63
        63
         	}

      
        64
        64
         

      
        65
        65
         	// status

      
        66
        
        -	if t.Status.Value != ast.StatusNone {

      
        
        66
        +	if t.Status != ast.StatusNone {

      
        67
        67
         		p.buf.WriteByte(' ')

      
        68
        
        -		p.buf.WriteString(t.Status.Value.String())

      
        
        68
        +		p.buf.WriteString(t.Status.String())

      
        69
        69
         	}

      
        70
        70
         

      
        71
        71
         	// code

      
        72
        
        -	if t.Code != nil && t.Code.Value != "" {

      
        
        72
        +	if t.Code != "" {

      
        73
        73
         		p.buf.WriteString(" (")

      
        74
        
        -		p.buf.WriteString(t.Code.Value)

      
        
        74
        +		p.buf.WriteString(t.Code)

      
        75
        75
         		p.buf.WriteByte(')')

      
        76
        76
         	}

      
        77
        77
         

      
        78
        78
         	// description

      
        79
        
        -	if t.Description != nil && t.Description.Value != "" {

      
        
        79
        +	if t.Description != "" {

      
        80
        80
         		p.buf.WriteByte(' ')

      
        81
        
        -		p.buf.WriteString(t.Description.Value)

      
        
        81
        +		p.buf.WriteString(t.Description)

      
        82
        82
         	}

      
        83
        83
         

      
        84
        84
         	// comment

      ···
        100
        100
         	p.buf.WriteByte('=')

      
        101
        101
         

      
        102
        102
         	// expression

      
        103
        
        -	if t.Expr.Value != "" {

      
        
        103
        +	if t.Expr != "" {

      
        104
        104
         		p.buf.WriteByte(' ')

      
        105
        
        -		p.buf.WriteString(t.Expr.Value)

      
        
        105
        +		p.buf.WriteString(t.Expr)

      
        106
        106
         	}

      
        107
        107
         

      
        108
        108
         	// comment

      
M journal/printer/transaction_postings.go
···
        62
        62
         func (p *printer) writePostingLine(pt ast.Posting, maxAcct int) {

      
        63
        63
         	p.buf.WriteString(p.indent)

      
        64
        64
         

      
        65
        
        -	if pt.Status.Value != ast.StatusNone {

      
        66
        
        -		p.buf.WriteString(pt.Status.Value.String())

      
        
        65
        +	if pt.Status != ast.StatusNone {

      
        
        66
        +		p.buf.WriteString(pt.Status.String())

      
        67
        67
         		p.buf.WriteByte(' ')

      
        68
        68
         	}

      
        69
        69
         

      ···
        82
        82
         			p.buf.WriteString("  ")

      
        83
        83
         		case AlignRight:

      
        84
        84
         			current := len(p.indent) + len(acct)

      
        85
        
        -			if pt.Status.Value != ast.StatusNone {

      
        
        85
        +			if pt.Status != ast.StatusNone {

      
        86
        86
         				current++

      
        87
        87
         			}

      
        88
        88
         			if pad := p.cfg.AlignColumn - current; pad > 0 {

      ···
        91
        91
         			p.buf.WriteByte('\v')

      
        92
        92
         		case AlignTab:

      
        93
        93
         			current := len(p.indent) + len(acct)

      
        94
        
        -			if pt.Status.Value != ast.StatusNone {

      
        
        94
        +			if pt.Status != ast.StatusNone {

      
        95
        95
         				current++

      
        96
        96
         			}

      
        97
        97
         			if pad := maxAcct + len(p.indent) - current; pad > 0 {