all repos

clerk @ c027d7c5c0081deeabfaa5147338fbc821391093

missing tooling for ledger/hledger
13 files changed, 377 insertions(+), 19 deletions(-)
lsp: go to definition
Author: Oleksandr Smirnov olexsmir@gmail.com
Committed at: 2026-08-12 13:28:05 +0300
Authored at: 2026-08-10 15:32:27 +0300
Change ID: uyumnxzlyyymzulzqzoplnnunyonslty
Parent: 907ec02
M internal/analyzer/analyzer.go
···
        63
        63
         }

      
        64
        64
         

      
        65
        65
         type PayeeInfo struct {

      
        66
        
        -	Directives []*ast.Payee

      
        
        66
        +	Directives []*ast.PayeeDirective

      
        67
        67
         	Usage      []PayeeUsage

      
        68
        68
         	UsedCount  int

      
        69
        69
         	LastUsed   ast.Date

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

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

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

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

      
        
        128
        +			if d.Name != nil {

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

      
        
        130
        +			}

      
        129
        131
         		}

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

      
        131
        133
         		for _, u := range info.Usage {

      ···
        166
        168
         		case *ast.CommodityDirective:

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

      
        168
        170
         		case *ast.PayeeDirective:

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

      
        
        171
        +			if d.Name != nil {

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

      
        
        173
        +			}

      
        170
        174
         		case *ast.TagDirective:

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

      
        172
        176
         		case *ast.IncludeDirective:

      
M internal/analyzer/build.go
···
        122
        122
         }

      
        123
        123
         

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

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

      
        
        125
        +	if pd.Name == nil {

      
        
        126
        +		return

      
        
        127
        +	}

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

      
        126
        129
         	if !ok {

      
        127
        130
         		info = &PayeeInfo{}

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

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

      
        129
        132
         	}

      
        130
        
        -	info.Directives = append(info.Directives, &ast.Payee{

      
        131
        
        -		Name: pd.Name,

      
        132
        
        -		Span: pd.Span,

      
        133
        
        -	})

      
        
        133
        +	info.Directives = append(info.Directives, pd)

      
        134
        134
         }

      
        135
        135
         

      
        136
        136
         func (a *Analysis) addTagDirective(td *ast.TagDirective) {

      
M internal/lsp/server.go
···
        71
        71
         		},

      
        72
        72
         		Capabilities: protocol.ServerCapabilities{

      
        73
        73
         			DocumentFormattingProvider: &protocol.DocumentFormattingOptions{},

      
        
        74
        +			DefinitionProvider:         protocol.Boolean(true),

      
        74
        75
         			CompletionProvider: &protocol.CompletionOptions{

      
        75
        76
         				TriggerCharacters: []string{":", "@"},

      
        76
        77
         			},

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

      
        
        2
        +include sub.journal

      
        
        3
        +

      
        
        4
        +2024-01-16 Another^ Store

      
        
        5
        +    expenses:fo^od  100.00 EUR

      
        
        6
        +    assets:ca^sh

      
        
        7
        +

      
        
        8
        +-- sub.journal --

      
        
        9
        +account expenses:food

      
        
        10
        +

      
        
        11
        +2024-01-15 Grocery Store

      
        
        12
        +    expenses:food  $50

      
        
        13
        +    assets:cash

      
        
        14
        +

      
        
        15
        +-- expect --

      
        
        16
        +2:18 in.journal 2:11-2:24

      
        
        17
        +3:15 sub.journal 0:8-0:21

      
        
        18
        +4:13 sub.journal 4:4-4:15

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

      
        
        2
        +account ex^penses:food

      
        
        3
        +account assets:checking

      
        
        4
        +account untracked

      
        
        5
        +    no^te ignore

      
        
        6
        +

      
        
        7
        +commodity ^$

      
        
        8
        +    format 1,000.00

      
        
        9
        +

      
        
        10
        +payee Grocery^ Store

      
        
        11
        +

      
        
        12
        +^2024-01-15 Grocery^ Store ; no^te

      
        
        13
        +    ^expenses:food^  $5^0

      
        
        14
        +    expenses:food  ^$50

      
        
        15
        +    assets:checking  = ^$30

      
        
        16
        +    assets:cash  100.00 E^UR

      
        
        17
        +

      
        
        18
        +2024-01-16 Another^ Store

      
        
        19
        +    expenses:food  $50

      
        
        20
        +    assets:cash  100.00 E^UR

      
        
        21
        +    assets:checking  $30 @ ^$1.5

      
        
        22
        +

      
        
        23
        +2024-01-17 продукти

      
        
        24
        +    expenses:food  $10

      
        
        25
        +    assets:cash

      
        
        26
        +

      
        
        27
        +2024-01-18 прод^укти

      
        
        28
        +    expenses:food  $20

      
        
        29
        +    assets:ca^sh

      
        
        30
        +

      
        
        31
        +-- expect --

      
        
        32
        +0:10 in.journal 0:8-0:21

      
        
        33
        +3:6 <none>

      
        
        34
        +5:10 in.journal 5:10-5:11

      
        
        35
        +8:13 in.journal 8:6-8:19

      
        
        36
        +10:0 <none>

      
        
        37
        +10:18 in.journal 8:6-8:19

      
        
        38
        +10:29 <none>

      
        
        39
        +11:4 in.journal 0:8-0:21

      
        
        40
        +11:17 in.journal 0:8-0:21

      
        
        41
        +11:21 <none>

      
        
        42
        +12:19 in.journal 5:10-5:11

      
        
        43
        +13:23 in.journal 5:10-5:11

      
        
        44
        +14:25 in.journal 14:24-14:27

      
        
        45
        +16:18 in.journal 16:11-16:24

      
        
        46
        +18:25 in.journal 14:24-14:27

      
        
        47
        +19:27 in.journal 5:10-5:11

      
        
        48
        +25:15 in.journal 21:11-21:19

      
        
        49
        +27:13 in.journal 14:4-14:15

      
A internal/lsp/textdocument_definition.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/analyzer"

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

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

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

      
        
        14
        +)

      
        
        15
        +

      
        
        16
        +func (s *server) Definition(_ context.Context, params *protocol.DefinitionParams) (protocol.DefinitionResult, error) {

      
        
        17
        +	state, ok := s.getDocState(params.TextDocument.URI)

      
        
        18
        +	if !ok {

      
        
        19
        +		return nil, nil

      
        
        20
        +	}

      
        
        21
        +

      
        
        22
        +	an := s.analysis()

      
        
        23
        +	cursor := lsputil.Offset(state.text, int(params.Position.Line), int(params.Position.Character))

      
        
        24
        +	return findDefinitionUnderCursor(an, params.TextDocument.URI.Path(), state.text, cursor), nil

      
        
        25
        +}

      
        
        26
        +

      
        
        27
        +func findDefinitionUnderCursor(an *analyzer.Analysis, docPath, content string, cursor int) protocol.LocationSlice {

      
        
        28
        +	for _, pf := range an.Files {

      
        
        29
        +		if pf.Path != docPath {

      
        
        30
        +			continue

      
        
        31
        +		}

      
        
        32
        +		for _, entry := range pf.Ast.Entries {

      
        
        33
        +			if loc := definitionInEntry(an, content, entry, cursor); loc != nil {

      
        
        34
        +				return protocol.LocationSlice{*loc}

      
        
        35
        +			}

      
        
        36
        +		}

      
        
        37
        +		return nil

      
        
        38
        +	}

      
        
        39
        +	return nil

      
        
        40
        +}

      
        
        41
        +

      
        
        42
        +func definitionInEntry(an *analyzer.Analysis, content string, e ast.Entry, cursor int) *protocol.Location {

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

      
        
        44
        +	case *ast.Transaction:

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

      
        
        46
        +			return findPayeeDefinition(an, e.Payee.Name)

      
        
        47
        +		}

      
        
        48
        +		return definitionInPostings(an, content, e.Postings, cursor)

      
        
        49
        +	case *ast.PeriodicTransaction:

      
        
        50
        +		return definitionInPostings(an, content, e.Postings, cursor)

      
        
        51
        +	case *ast.AutomatedTransaction:

      
        
        52
        +		return definitionInPostings(an, content, e.Postings, cursor)

      
        
        53
        +	case *ast.AccountDirective:

      
        
        54
        +		if spanContains(content, e.Account.Span, cursor) {

      
        
        55
        +			return findAccountDefinition(an, e.Account.String())

      
        
        56
        +		}

      
        
        57
        +	case *ast.CommodityDirective:

      
        
        58
        +		if spanContains(content, e.CommoditySpan, cursor) {

      
        
        59
        +			return findCommodityDefinition(an, e.Commodity)

      
        
        60
        +		}

      
        
        61
        +	case *ast.PayeeDirective:

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

      
        
        63
        +			return findPayeeDefinition(an, e.Name.Name)

      
        
        64
        +		}

      
        
        65
        +	}

      
        
        66
        +	return nil

      
        
        67
        +}

      
        
        68
        +

      
        
        69
        +func definitionInPostings(an *analyzer.Analysis, content string, postings []*ast.Posting, cursor int) *protocol.Location {

      
        
        70
        +	for _, p := range postings {

      
        
        71
        +		if spanContains(content, p.Account.Span, cursor) {

      
        
        72
        +			return findAccountDefinition(an, p.Account.String())

      
        
        73
        +		}

      
        
        74
        +		if loc := commodityDefinition(an, content, p.Amount, cursor); loc != nil {

      
        
        75
        +			return loc

      
        
        76
        +		}

      
        
        77
        +		if p.Cost != nil {

      
        
        78
        +			if loc := commodityDefinition(an, content, &p.Cost.Amount, cursor); loc != nil {

      
        
        79
        +				return loc

      
        
        80
        +			}

      
        
        81
        +		}

      
        
        82
        +		if p.Balance != nil {

      
        
        83
        +			if loc := commodityDefinition(an, content, &p.Balance.Amount, cursor); loc != nil {

      
        
        84
        +				return loc

      
        
        85
        +			}

      
        
        86
        +		}

      
        
        87
        +	}

      
        
        88
        +	return nil

      
        
        89
        +}

      
        
        90
        +

      
        
        91
        +func commodityDefinition(an *analyzer.Analysis, content string, am *ast.Amount, cursor int) *protocol.Location {

      
        
        92
        +	if am == nil || am.Commodity == "" || !spanContains(content, am.CommoditySpan, cursor) {

      
        
        93
        +		return nil

      
        
        94
        +	}

      
        
        95
        +	return findCommodityDefinition(an, am.Commodity)

      
        
        96
        +}

      
        
        97
        +

      
        
        98
        +func findAccountDefinition(an *analyzer.Analysis, name string) *protocol.Location {

      
        
        99
        +	info := an.Accounts[name]

      
        
        100
        +	if info == nil {

      
        
        101
        +		return nil

      
        
        102
        +	}

      
        
        103
        +	if len(info.Directives) > 0 {

      
        
        104
        +		return locationForDirective(an, info.Directives[0], info.Directives[0].Account.Span)

      
        
        105
        +	}

      
        
        106
        +	if len(info.Usages) > 0 {

      
        
        107
        +		u := info.Usages[0]

      
        
        108
        +		return locationFor(an, u.FileIndex, u.Posting.Account.Span)

      
        
        109
        +	}

      
        
        110
        +	return nil

      
        
        111
        +}

      
        
        112
        +

      
        
        113
        +func findCommodityDefinition(an *analyzer.Analysis, symbol string) *protocol.Location {

      
        
        114
        +	info := an.Commodities[symbol]

      
        
        115
        +	if info == nil {

      
        
        116
        +		return nil

      
        
        117
        +	}

      
        
        118
        +	if len(info.Directives) > 0 {

      
        
        119
        +		return locationForDirective(an, info.Directives[0], info.Directives[0].CommoditySpan)

      
        
        120
        +	}

      
        
        121
        +	if len(info.Usages) > 0 {

      
        
        122
        +		u := info.Usages[0]

      
        
        123
        +		return locationFor(an, u.FileIndex, u.Amount.CommoditySpan)

      
        
        124
        +	}

      
        
        125
        +	return nil

      
        
        126
        +}

      
        
        127
        +

      
        
        128
        +func findPayeeDefinition(an *analyzer.Analysis, name string) *protocol.Location {

      
        
        129
        +	info := an.Payees[name]

      
        
        130
        +	if info == nil {

      
        
        131
        +		return nil

      
        
        132
        +	}

      
        
        133
        +	if len(info.Directives) > 0 {

      
        
        134
        +		d := info.Directives[0]

      
        
        135
        +		if d.Name == nil {

      
        
        136
        +			return nil

      
        
        137
        +		}

      
        
        138
        +		return locationForDirective(an, d, d.Name.Span)

      
        
        139
        +	}

      
        
        140
        +	if len(info.Usage) > 0 {

      
        
        141
        +		u := info.Usage[0]

      
        
        142
        +		return locationFor(an, u.FileIndex, u.Payee.Span)

      
        
        143
        +	}

      
        
        144
        +	return nil

      
        
        145
        +}

      
        
        146
        +

      
        
        147
        +func locationForDirective(a *analyzer.Analysis, d ast.Entry, span token.Span) *protocol.Location {

      
        
        148
        +	for i, pf := range a.Files {

      
        
        149
        +		if slices.Contains(pf.Ast.Entries, d) {

      
        
        150
        +			return locationFor(a, i, span)

      
        
        151
        +		}

      
        
        152
        +	}

      
        
        153
        +	return nil

      
        
        154
        +}

      
        
        155
        +

      
        
        156
        +func locationFor(a *analyzer.Analysis, fileIdx int, span token.Span) *protocol.Location {

      
        
        157
        +	pf := a.Files[fileIdx]

      
        
        158
        +	return &protocol.Location{

      
        
        159
        +		URI:   uri.File(pf.Path),

      
        
        160
        +		Range: spanToProtocolRange(string(pf.Src), span),

      
        
        161
        +	}

      
        
        162
        +}

      
        
        163
        +

      
        
        164
        +func spanToProtocolRange(content string, span token.Span) protocol.Range {

      
        
        165
        +	return protocol.Range{

      
        
        166
        +		Start: lsputil.Position(content, span.Start.Offset),

      
        
        167
        +		End:   lsputil.Position(content, spanEndClamped(content, span.End.Offset)),

      
        
        168
        +	}

      
        
        169
        +}

      
        
        170
        +

      
        
        171
        +func spanContains(content string, span token.Span, offset int) bool {

      
        
        172
        +	if span.End.Offset <= span.Start.Offset {

      
        
        173
        +		return false

      
        
        174
        +	}

      
        
        175
        +	end := spanEndClamped(content, span.End.Offset)

      
        
        176
        +	return span.Start.Offset <= offset && offset <= end

      
        
        177
        +}

      
        
        178
        +

      
        
        179
        +func spanEndClamped(content string, end int) int {

      
        
        180
        +	for end > 0 {

      
        
        181
        +		switch content[end-1] {

      
        
        182
        +		case ' ', '\t', '\r', '\n':

      
        
        183
        +			end--

      
        
        184
        +		default:

      
        
        185
        +			return end

      
        
        186
        +		}

      
        
        187
        +	}

      
        
        188
        +	return end

      
        
        189
        +}

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

      
        
        2
        +

      
        
        3
        +import (

      
        
        4
        +	"fmt"

      
        
        5
        +	"os"

      
        
        6
        +	"path/filepath"

      
        
        7
        +	"strings"

      
        
        8
        +	"testing"

      
        
        9
        +

      
        
        10
        +	"go.lsp.dev/protocol"

      
        
        11
        +	"go.lsp.dev/uri"

      
        
        12
        +

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

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

      
        
        15
        +)

      
        
        16
        +

      
        
        17
        +func TestDefinition_DocumentNotFound(t *testing.T) {

      
        
        18
        +	srv := NewServer("test")

      
        
        19
        +	res, err := srv.server.Definition(t.Context(), &protocol.DefinitionParams{

      
        
        20
        +		TextDocumentPositionParams: protocol.TextDocumentPositionParams{

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

      
        
        22
        +			Position:     protocol.Position{Line: 0, Character: 0},

      
        
        23
        +		},

      
        
        24
        +	})

      
        
        25
        +	if err != nil {

      
        
        26
        +		t.Fatal(err)

      
        
        27
        +	}

      
        
        28
        +	if res != nil {

      
        
        29
        +		t.Errorf("got %v, want nil", res)

      
        
        30
        +	}

      
        
        31
        +}

      
        
        32
        +

      
        
        33
        +// Golden

      
        
        34
        +

      
        
        35
        +func TestDefinitionTxtar(t *testing.T) {

      
        
        36
        +	tests := []string{

      
        
        37
        +		"definition-journal",

      
        
        38
        +		"definition-include",

      
        
        39
        +	}

      
        
        40
        +	for _, tt := range tests {

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

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

      
        
        43
        +			dir := t.TempDir()

      
        
        44
        +			for _, f := range ar.Files {

      
        
        45
        +				if f.Name != "expect" {

      
        
        46
        +					if err := os.WriteFile(filepath.Join(dir, f.Name), f.Data, 0o644); err != nil {

      
        
        47
        +						t.Fatal(err)

      
        
        48
        +					}

      
        
        49
        +				}

      
        
        50
        +			}

      
        
        51
        +

      
        
        52
        +			content, cursors := stripCursors(string(ar.Get("in.journal")))

      
        
        53
        +			u := uri.File(filepath.Join(dir, "in.journal"))

      
        
        54
        +

      
        
        55
        +			srv := NewServer("test")

      
        
        56
        +			srv.server.openDoc(u, content, 1, "journal")

      
        
        57
        +

      
        
        58
        +			var b strings.Builder

      
        
        59
        +			for _, c := range cursors {

      
        
        60
        +				pos := lsputil.Position(content, c)

      
        
        61
        +				res, err := srv.server.Definition(t.Context(), &protocol.DefinitionParams{

      
        
        62
        +					TextDocumentPositionParams: protocol.TextDocumentPositionParams{

      
        
        63
        +						TextDocument: protocol.TextDocumentIdentifier{URI: u},

      
        
        64
        +						Position:     pos,

      
        
        65
        +					},

      
        
        66
        +				})

      
        
        67
        +				if err != nil {

      
        
        68
        +					t.Fatal(err)

      
        
        69
        +				}

      
        
        70
        +				locs, _ := res.(protocol.LocationSlice)

      
        
        71
        +				if len(locs) == 0 {

      
        
        72
        +					fmt.Fprintf(&b, "%d:%d <none>\n", pos.Line, pos.Character)

      
        
        73
        +					continue

      
        
        74
        +				}

      
        
        75
        +				r := locs[0].Range

      
        
        76
        +				fmt.Fprintf(&b, "%d:%d %s %d:%d-%d:%d\n", pos.Line, pos.Character,

      
        
        77
        +					filepath.Base(locs[0].URI.Path()),

      
        
        78
        +					r.Start.Line, r.Start.Character, r.End.Line, r.End.Character)

      
        
        79
        +			}

      
        
        80
        +			golden.Assert(t, ar, b.String())

      
        
        81
        +		})

      
        
        82
        +	}

      
        
        83
        +}

      
        
        84
        +

      
        
        85
        +func stripCursors(content string) (string, []int) {

      
        
        86
        +	var cursors []int

      
        
        87
        +	for {

      
        
        88
        +		i := strings.Index(content, "^")

      
        
        89
        +		if i < 0 {

      
        
        90
        +			return content, cursors

      
        
        91
        +		}

      
        
        92
        +		cursors = append(cursors, i)

      
        
        93
        +		content = content[:i] + content[i+1:]

      
        
        94
        +	}

      
        
        95
        +}

      
M journal/ast/directives.go
···
        21
        21
         func (CommodityDirective) entryNode() {}

      
        22
        22
         

      
        23
        23
         type PayeeDirective struct {

      
        24
        
        -	Name    string

      
        
        24
        +	Name    *Payee

      
        25
        25
         	Comment *Comment

      
        26
        26
         	Span    token.Span

      
        27
        27
         }

      
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
        
        -		indent(b, depth+1)

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

      
        
        60
        +		if e.Name != nil {

      
        
        61
        +			indent(b, depth+1)

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

      
        
        63
        +		}

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

      
        63
        65
         	case *TagDirective:

      
        64
        66
         		indent(b, depth)

      
M journal/parser/parser.go
···
        465
        465
         	p.expect(token.PAYEE)

      
        466
        466
         	p.skipWhitespace()

      
        467
        467
         

      
        468
        
        -	name := ""

      
        
        468
        +	var name *ast.Payee

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

      
        470
        
        -		name = p.parsePayee().Name

      
        
        470
        +		name = p.parsePayee()

      
        471
        471
         	}

      
        472
        472
         

      
        473
        473
         	comment := p.parseOptInlineComment()

      
M journal/parser/testdata/payee_directive.txtar
···
        6
        6
         -- expect --

      
        7
        7
         Journal

      
        8
        8
           PayeeDirective j:1:1-2:1

      
        9
        
        -    Name: "grocery store"

      
        
        9
        +    Name: "grocery store" j:1:7-2:0

      
        10
        10
           PayeeDirective j:2:1-3:1

      
        11
        
        -    Name: "grocery store 3"

      
        
        11
        +    Name: "grocery store 3" j:2:7-3:0

      
        12
        12
           PayeeDirective j:3:1-4:1

      
        13
        
        -    Name: "grocery store 2"

      
        
        13
        +    Name: "grocery store 2" j:3:7-4:0

      
        14
        14
           PayeeDirective j:4:1-5:1

      
        15
        
        -    Name: "grocery store 1"

      
        
        15
        +    Name: "grocery store 1" j:4:7-5:0

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

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

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

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

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

      
        58
        58
         	p.writeInlineComment(pd.Comment)

      
        59
        59
         }

      
        60
        60