23 files changed,
192 insertions(+),
204 deletions(-)
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
jump to
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/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/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 {