all repos

clerk @ 23d0a0d

missing tooling for ledger/hledger
31 files changed, 98 insertions(+), 157 deletions(-)
fix typos and grammar
Author: Oleksandr Smirnov olexsmir@gmail.com
Committed at: 2026-09-29 19:57:41 +0300
Authored at: 2026-09-26 20:44:18 +0300
Change ID: kkryxnumzpuorvvzsvqlpnsuzuupmvvv
Parent: 25c8f80
jump to
M internal/analyzer/build.go
M internal/decimal/decimal.go
M internal/linter/rule_duplicated_transaction.go
M internal/lsp/lifecycle.go
M internal/lsp/lsp.go
M internal/lsp/lsputil/lineindex.go
M internal/lsp/lsputil/utf16.go
M internal/lsp/server.go
M internal/lsp/textdocument_completion.go
M internal/lsp/textdocument_definition.go
M internal/lsp/textdocument_hover.go
M internal/lsp/textdocument_rename.go
M internal/lsp/textdocument_rename_test.go
M internal/lsp/textdocument_semantic_tokens.go
M internal/lsp/textdocument_semantic_tokens_test.go
M internal/settings/settings.go
M journal/journal_test.go
M journal/lexer/lexer.go
M journal/lexer/lexer_test.go
M journal/lexer/testdata/transaction,_accounts_with_uppercase_latters.txtar → journal/lexer/testdata/transaction,_accounts_with_uppercase_letters.txtar
M journal/loader.go
M journal/parser/parser.go
M journal/parser/parser_test.go
M journal/parser/testdata/comodity_directive.txtar → journal/parser/testdata/commodity_directive.txtar
M journal/parser/testdata/comodity_directive_no_space.txtar → journal/parser/testdata/commodity_directive_no_space.txtar
M journal/parser/testdata/comodity_directive_word.txtar → journal/parser/testdata/commodity_directive_word.txtar
M journal/parser/testdata/inclue_directive.txtar → journal/parser/testdata/include_directive.txtar
M journal/parser/testdata/inclue_directive_digit_path.txtar → journal/parser/testdata/include_directive_digit_path.txtar
M journal/parser/testdata/inclue_directive_with_comment.txtar → journal/parser/testdata/include_directive_with_comment.txtar
M journal/parser/testdata/transaction_clearerd.txtar → journal/parser/testdata/transaction_cleared.txtar
M journal/token/token.go
M internal/analyzer/build.go
···
        57
        57
         	return b.String()

      
        58
        58
         }

      
        59
        59
         

      
        60
        
        -// PayeeTemplates returns  the last Transactions's postings per payee name.

      
        
        60
        +// PayeeTemplates returns the last transaction's postings per payee name.

      
        61
        61
         func (a *Analysis) PayeeTemplates() map[string][]PostingTemplate {

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

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

      
M internal/decimal/decimal.go
···
        165
        165
         	return sb.String()

      
        166
        166
         }

      
        167
        167
         

      
        168
        
        -// WriteFixed writes a string representation with exactly places digits

      
        169
        
        -// after the decimal point directly into sb. Pads with zeros or truncates

      
        170
        
        -// as needed. decSep and thousandsSep control formatting; zero values mean

      
        171
        
        -// no custom separator.

      
        
        168
        +// WriteFixed is [Decimal.StringFixed] writing into sb instead of returning a string.

      
        172
        169
         func (d Decimal) WriteFixed(sb *strings.Builder, places int, decSep, thousandsSep byte) {

      
        173
        170
         	if d.IsZero() {

      
        174
        171
         		sb.WriteByte('0')

      
M internal/linter/rule_duplicated_transaction.go
···
        8
        8
         

      
        9
        9
         const DuplicatedTransactionID = "duplicated-transaction"

      
        10
        10
         

      
        11
        
        -// DuplicatedTransaction flags idnetical transactions.

      
        
        11
        +// DuplicatedTransaction flags identical transactions.

      
        12
        12
         type DuplicatedTransaction struct{}

      
        13
        13
         

      
        14
        14
         func (DuplicatedTransaction) ID() RuleID { return DuplicatedTransactionID }

      
M internal/lsp/lifecycle.go
···
        44
        44
         

      
        45
        45
         // lifecycle gates the connection on the initialize request: before it, only

      
        46
        46
         // initialize and exit are served; everything else answers ServerNotInitialized

      
        47
        
        -// and notifications are dropped. Every later message is served — like gopls,

      
        
        47
        +// and notifications are dropped. Every later message is served: like gopls,

      
        48
        48
         // the server trusts its client, so shutdown and exit leave subsequent requests

      
        49
        49
         // to fail naturally. The handlers themselves enforce initialize-once and the

      
        50
        50
         // exit code.

      
M internal/lsp/lsp.go
···
        88
        88
         	return os.OpenFile(filepath.Join(dir, "lsp.log"), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)

      
        89
        89
         }

      
        90
        90
         

      
        91
        
        -// lspCodec mirros go.lsp.dev/protocol wire codec, so Run can install the lifecycle guard.

      
        
        91
        +// lspCodec mirrors go.lsp.dev/protocol wire codec, so Run can install the lifecycle guard.

      
        92
        92
         type lspCodec struct{}

      
        93
        93
         

      
        94
        94
         func (lspCodec) Marshal(v any) ([]byte, error) {

      
M internal/lsp/lsputil/lineindex.go
···
        16
        16
         	starts  []int // byte offset of each line's first byte; starts[0] == 0

      
        17
        17
         }

      
        18
        18
         

      
        19
        
        -// NewLineIndex builds the line-start table for content.

      
        20
        19
         func NewLineIndex(content string) *LineIndex {

      
        21
        20
         	starts := make([]int, 1, len(content)/20+1)

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

      ···
        80
        79
         	for off < lineEnd && units < col {

      
        81
        80
         		r, size := utf8.DecodeRuneInString(l.content[off:lineEnd])

      
        82
        81
         		off += size

      
        83
        
        -		units += utf16Len(r)

      
        
        82
        +		units += Utf16LenRune(r)

      
        84
        83
         	}

      
        85
        84
         	return off

      
        86
        85
         }

      
M internal/lsp/lsputil/utf16.go
···
        32
        32
         		if r == utf8.RuneError && size <= 1 {

      
        33
        33
         			break

      
        34
        34
         		}

      
        35
        
        -		col += utf16Len(r)

      
        
        35
        +		col += Utf16LenRune(r)

      
        36
        36
         		i += size

      
        37
        37
         	}

      
        38
        38
         	return col

      ···
        55
        55
         		if r == utf8.RuneError && size <= 1 {

      
        56
        56
         			break

      
        57
        57
         		}

      
        58
        
        -		col += utf16Len(r)

      
        
        58
        +		col += Utf16LenRune(r)

      
        59
        59
         		i += size

      
        60
        60
         	}

      
        61
        61
         	return col

      ···
        88
        88
         		if r == utf8.RuneError && size <= 1 {

      
        89
        89
         			break

      
        90
        90
         		}

      
        91
        
        -		n += utf16Len(r)

      
        
        91
        +		n += Utf16LenRune(r)

      
        92
        92
         		i += size

      
        93
        93
         	}

      
        94
        94
         	return n

      ···
        126
        126
         	for off < lineEnd && units < col {

      
        127
        127
         		r, size := utf8.DecodeRuneInString(content[off:])

      
        128
        128
         		off += size

      
        129
        
        -		units += utf16Len(r)

      
        
        129
        +		units += Utf16LenRune(r)

      
        130
        130
         	}

      
        131
        131
         	return off

      
        132
        132
         }

      ···
        188
        188
         	return protocol.Position{Line: uint32(line), Character: uint32(col)}

      
        189
        189
         }

      
        190
        190
         

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

      
        192
        
        -func utf16Len(r rune) int {

      
        
        191
        +// Utf16LenRune returns the number of UTF-16 code units for a rune.

      
        
        192
        +func Utf16LenRune(r rune) int {

      
        193
        193
         	if r >= 0x10000 && r <= 0x10FFFF {

      
        194
        194
         		return 2

      
        195
        195
         	}

      
M internal/lsp/server.go
···
        27
        27
         	conn   jsonrpc2.Conn // set in Run; Exit closes it to end the session

      
        28
        28
         

      
        29
        29
         	stateMu       sync.Mutex

      
        30
        
        -	state         serverState // [serverState]

      
        
        30
        +	state         serverState

      
        31
        31
         	version, name string

      
        32
        32
         

      
        33
        33
         	settings settings.Settings

      
        34
        34
         	loader   *journal.Loader

      
        35
        35
         

      
        36
        
        -	mu            sync.RWMutex

      
        37
        
        -	openDocs      map[uri.URI]docState

      
        38
        
        -	diagCancel    context.CancelFunc

      
        39
        
        -	dynFileWather bool

      
        40
        
        -	configPath    string

      
        
        36
        +	mu             sync.RWMutex

      
        
        37
        +	openDocs       map[uri.URI]docState

      
        
        38
        +	diagCancel     context.CancelFunc

      
        
        39
        +	dynFileWatcher bool

      
        
        40
        +	configPath     string

      
        41
        41
         }

      
        42
        42
         

      
        43
        
        -// parsedFileFor returns the parsed file for path within the analysis, or nil.

      
        44
        43
         func parsedFileFor(an *analyzer.Analysis, path string) *journal.ParsedFile {

      
        45
        44
         	for _, pf := range an.Files {

      
        46
        45
         		if pf.Path == path {

      ···
        86
        85
         func (s *server) Initialize(ctx context.Context, params *protocol.InitializeParams) (*protocol.InitializeResult, error) {

      
        87
        86
         	if w := params.Capabilities.Workspace; w != nil {

      
        88
        87
         		if wf := w.DidChangeWatchedFiles; wf != nil {

      
        89
        
        -			s.dynFileWather = wf.DynamicRegistration != nil && *wf.DynamicRegistration

      
        
        88
        +			s.dynFileWatcher = wf.DynamicRegistration != nil && *wf.DynamicRegistration

      
        90
        89
         		}

      
        91
        90
         	}

      
        92
        91
         

      ···
        150
        149
         }

      
        151
        150
         

      
        152
        151
         func (s *server) Initialized(ctx context.Context, params *protocol.InitializedParams) error {

      
        153
        
        -	if s.dynFileWather {

      
        
        152
        +	if s.dynFileWatcher {

      
        154
        153
         		go s.registerFileWatchers(context.Background())

      
        155
        154
         	}

      
        156
        155
         	s.applyConfigFile(ctx)

      ···
        188
        187
         	return nil

      
        189
        188
         }

      
        190
        189
         

      
        191
        
        -// Exit records the exit code the LSP spec prescribes — 0 after a shutdown

      
        192
        
        -// request, 1 otherwise — and closes the connection to end the session. The

      
        193
        
        -// close runs in its own goroutine: called from inside the exit-notification

      
        194
        
        -// handler it cannot block on the connection draining (see [jsonrpc2.Conn.Close]).

      
        195
        
        -// Exit honors an exit that never saw initialize, and is safe to call

      
        196
        
        -// concurrently from the exit notification and watchParent: the stateMu guard

      
        197
        
        -// below makes the transition a once-only, and an exit that lost the race to

      
        198
        
        -// another exit is a no-op.

      
        199
        190
         func (s *server) Exit(context.Context) error {

      
        200
        191
         	s.stateMu.Lock()

      
        201
        192
         	if s.state < stateExited {

      ···
        260
        251
         		return

      
        261
        252
         	}

      
        262
        253
         	var raw map[string]any

      
        263
        
        -	if err := toml.Unmarshal(data, &raw); err != nil {

      
        264
        
        -		s.reportConfigError(ctx, err)

      
        
        254
        +	if uerr := toml.Unmarshal(data, &raw); uerr != nil {

      
        
        255
        +		s.reportConfigError(ctx, uerr)

      
        265
        256
         		return

      
        266
        257
         	}

      
        267
        258
         	s.mu.Lock()

      
M internal/lsp/textdocument_completion.go
···
        136
        136
         }

      
        137
        137
         

      
        138
        138
         func cmplHeaderCtx(content string, cursor int, toks []token.Token) (cmplCtx, int) {

      
        139
        
        -	// cursor inside date token

      
        140
        139
         	if cursor >= toks[0].Span.Start.Offset && cursor <= toks[0].Span.End.Offset {

      
        141
        140
         		return cmplDate, toks[0].Span.Start.Offset

      
        142
        141
         	}

      ···
        195
        194
         	return cmplNone, cursor

      
        196
        195
         }

      
        197
        196
         

      
        198
        
        -// cmplDirectiveContext classifies a directive line. keyword completion before the keyword ends, symbol completion in the value field after

      
        
        197
        +// cmplDirectiveContext classifies a directive line: keyword completion up to the end of

      
        
        198
        +// the keyword, symbol completion in the value field after it.

      
        199
        199
         func cmplDirectiveContext(cursor, lineStart int, toks []token.Token) (cmplCtx, int) {

      
        200
        200
         	kwEnd := toks[0].Span.End.Offset

      
        201
        201
         	if cursor <= kwEnd {

      ···
        290
        290
         	return cmplCommodity, cursor

      
        291
        291
         }

      
        292
        292
         

      
        293
        
        -// commentStart completes tag names before ':' of the current tag and tag values after it

      
        
        293
        +// cmplTagContext completes tag names before the ':' of the current tag, and tag values after it.

      
        294
        294
         func cmplTagContext(content string, commentStart, cursor int) (cmplCtx, int) {

      
        295
        295
         	prefix := content[commentStart:cursor]

      
        296
        296
         	segStart := commentStart

      ···
        310
        310
         	return cmplTagName, keyStart

      
        311
        311
         }

      
        312
        312
         

      
        313
        
        -// tagKeyAt returns the key of tag whose value region starts at start

      
        
        313
        +// tagKeyAt returns the key of the tag whose value region starts at start.

      
        314
        314
         func tagKeyAt(content string, start int) (string, bool) {

      
        315
        315
         	lineStart, _ := lineBounds(content, start)

      
        316
        316
         	segStart := lineStart

      ···
        376
        376
         	rank         int   // lower sorts first among equal scores; 0 except for date completions

      
        377
        377
         }

      
        378
        378
         

      
        379
        
        -// cmplItems ranks candidates for the content against typed pattern

      
        
        379
        +// cmplItems ranks candidates for the content against the typed pattern.

      
        380
        380
         func cmplItems(

      
        381
        381
         	a *analyzer.Analysis,

      
        382
        382
         	ctx cmplCtx,

      ···
        579
        579
         	return int64(era)*146097 + int64(doe) - 719468

      
        580
        580
         }

      
        581
        581
         

      
        582
        
        -// dateStyle returns reparator and yesr-ness of the most recent history date.

      
        583
        
        -// Defualts to '-'/true when history is empty.

      
        
        582
        +// dateStyle returns the separator and year-ness of the most recent history date.

      
        
        583
        +// Defaults to '-' with a year when history is empty.

      
        584
        584
         func dateStyle(history []string) (sep byte, hasYear bool) {

      
        585
        585
         	sep, hasYear = '-', true

      
        586
        586
         	if n := len(history); n > 0 {

      
M internal/lsp/textdocument_definition.go
···
        271
        271
         	return entries[idx]

      
        272
        272
         }

      
        273
        273
         

      
        274
        
        -// entrySpan returns the span covering an entry.

      
        275
        274
         func entrySpan(e ast.Entry) token.Span {

      
        276
        275
         	switch e := e.(type) {

      
        277
        276
         	case *ast.BlankLine:

      ···
        319
        318
         }

      
        320
        319
         

      
        321
        320
         func spanEndClamped(content string, end int) int {

      
        322
        
        -	for end > 0 {

      
        323
        
        -		switch content[end-1] {

      
        324
        
        -		case ' ', '\t', '\r', '\n':

      
        325
        
        -			end--

      
        326
        
        -		default:

      
        327
        
        -			return end

      
        328
        
        -		}

      
        
        321
        +	for end > 0 && isSpanSpace(content[end-1]) {

      
        
        322
        +		end--

      
        329
        323
         	}

      
        330
        324
         	return end

      
        331
        325
         }

      
M internal/lsp/textdocument_hover.go
···
        80
        80
         		if spanContains(content, e.Date.Span, cursor) {

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

      
        82
        82
         		}

      
        83
        
        -		if el := hoverTagInComment(content, e.Comment, cursor); el != nil {

      
        84
        
        -			return el

      
        85
        
        -		}

      
        86
        
        -		for _, c := range e.HeaderComments {

      
        87
        
        -			if el := hoverTagInComment(content, c, cursor); el != nil {

      
        88
        
        -				return el

      
        89
        
        -			}

      
        90
        
        -		}

      
        91
        
        -		return hoverInPostings(content, e.Postings, cursor)

      
        
        83
        +		return hoverInTransaction(content, e.Comment, e.HeaderComments, e.Postings, cursor)

      
        92
        84
         	case *ast.PeriodicTransaction:

      
        93
        
        -		if el := hoverTagInComment(content, e.Comment, cursor); el != nil {

      
        94
        
        -			return el

      
        95
        
        -		}

      
        96
        
        -		for _, c := range e.HeaderComments {

      
        97
        
        -			if el := hoverTagInComment(content, c, cursor); el != nil {

      
        98
        
        -				return el

      
        99
        
        -			}

      
        100
        
        -		}

      
        101
        
        -		return hoverInPostings(content, e.Postings, cursor)

      
        
        85
        +		return hoverInTransaction(content, e.Comment, e.HeaderComments, e.Postings, cursor)

      
        102
        86
         	case *ast.AutomatedTransaction:

      
        103
        
        -		if el := hoverTagInComment(content, e.Comment, cursor); el != nil {

      
        104
        
        -			return el

      
        105
        
        -		}

      
        106
        
        -		for _, c := range e.HeaderComments {

      
        107
        
        -			if el := hoverTagInComment(content, c, cursor); el != nil {

      
        108
        
        -				return el

      
        109
        
        -			}

      
        110
        
        -		}

      
        111
        
        -		return hoverInPostings(content, e.Postings, cursor)

      
        
        87
        +		return hoverInTransaction(content, e.Comment, e.HeaderComments, e.Postings, cursor)

      
        112
        88
         	case *ast.Comment:

      
        113
        89
         		return hoverTagInComment(content, e, cursor)

      
        114
        90
         	case *ast.AccountDirective:

      ···
        143
        119
         		}

      
        144
        120
         	}

      
        145
        121
         	return nil

      
        
        122
        +}

      
        
        123
        +

      
        
        124
        +func hoverInTransaction(content string, comment *ast.Comment, header []*ast.Comment, postings []ast.Posting, cursor int) *hoverElement {

      
        
        125
        +	if el := hoverTagInComment(content, comment, cursor); el != nil {

      
        
        126
        +		return el

      
        
        127
        +	}

      
        
        128
        +	for _, c := range header {

      
        
        129
        +		if el := hoverTagInComment(content, c, cursor); el != nil {

      
        
        130
        +			return el

      
        
        131
        +		}

      
        
        132
        +	}

      
        
        133
        +	return hoverInPostings(content, postings, cursor)

      
        146
        134
         }

      
        147
        135
         

      
        148
        136
         func hoverInPostings(content string, postings []ast.Posting, cursor int) *hoverElement {

      
M internal/lsp/textdocument_rename.go
···
        97
        97
         		if e.Payee != "" && spanContains(content, e.PayeeSpan, cursor) {

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

      
        99
        99
         		}

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

      
        101
        
        -			return ref

      
        102
        
        -		}

      
        103
        
        -		for _, c := range e.HeaderComments {

      
        104
        
        -			if ref := tagRefInComment(content, c, cursor); ref != nil {

      
        105
        
        -				return ref

      
        106
        
        -			}

      
        107
        
        -		}

      
        108
        
        -		return symbolInPostings(content, e.Postings, cursor)

      
        
        100
        +		return symbolInTransaction(content, e.Comment, e.HeaderComments, e.Postings, cursor)

      
        109
        101
         	case *ast.PeriodicTransaction:

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

      
        111
        
        -			return ref

      
        112
        
        -		}

      
        113
        
        -		for _, c := range e.HeaderComments {

      
        114
        
        -			if ref := tagRefInComment(content, c, cursor); ref != nil {

      
        115
        
        -				return ref

      
        116
        
        -			}

      
        117
        
        -		}

      
        118
        
        -		return symbolInPostings(content, e.Postings, cursor)

      
        
        102
        +		return symbolInTransaction(content, e.Comment, e.HeaderComments, e.Postings, cursor)

      
        119
        103
         	case *ast.AutomatedTransaction:

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

      
        121
        
        -			return ref

      
        122
        
        -		}

      
        123
        
        -		for _, c := range e.HeaderComments {

      
        124
        
        -			if ref := tagRefInComment(content, c, cursor); ref != nil {

      
        125
        
        -				return ref

      
        126
        
        -			}

      
        127
        
        -		}

      
        128
        
        -		return symbolInPostings(content, e.Postings, cursor)

      
        
        104
        +		return symbolInTransaction(content, e.Comment, e.HeaderComments, e.Postings, cursor)

      
        129
        105
         	case *ast.Comment:

      
        130
        106
         		return tagRefInComment(content, e, cursor)

      
        131
        107
         	case *ast.AccountDirective:

      ···
        217
        193
         		return nil

      
        218
        194
         	}

      
        219
        195
         	return &symbolRef{symbolCommodity, am.Commodity, am.CommoditySpan}

      
        
        196
        +}

      
        
        197
        +

      
        
        198
        +func symbolInTransaction(content string, comment *ast.Comment, header []*ast.Comment, postings []ast.Posting, cursor int) *symbolRef {

      
        
        199
        +	if ref := tagRefInComment(content, comment, cursor); ref != nil {

      
        
        200
        +		return ref

      
        
        201
        +	}

      
        
        202
        +	for _, c := range header {

      
        
        203
        +		if ref := tagRefInComment(content, c, cursor); ref != nil {

      
        
        204
        +			return ref

      
        
        205
        +		}

      
        
        206
        +	}

      
        
        207
        +	return symbolInPostings(content, postings, cursor)

      
        220
        208
         }

      
        221
        209
         

      
        222
        210
         func symbolInPostings(content string, postings []ast.Posting, cursor int) *symbolRef {

      ···
        433
        421
         		changes[u] = dedup

      
        434
        422
         	}

      
        435
        423
         }

      
        436
        
        -

      
        437
        
        -// Validation

      
        438
        424
         

      
        439
        425
         func validateAccountName(name string) error   { return validateRenameName(name, "account", ";") }

      
        440
        426
         func validateCommodityName(name string) error { return validateRenameName(name, "commodity", ";") }

      
M internal/lsp/textdocument_rename_test.go
···
        177
        177
         	}

      
        178
        178
         }

      
        179
        179
         

      
        180
        
        -// txtarHarness runs per-cursor lsp requests againt golden tests

      
        
        180
        +// txtarHarness runs per-cursor lsp requests against golden tests

      
        181
        181
         type txtarHarness struct {

      
        182
        182
         	srv     *server

      
        183
        183
         	uri     uri.URI

      
M internal/lsp/textdocument_semantic_tokens.go
···
        239
        239
         	return rawToSemanticTokens(content, raw)

      
        240
        240
         }

      
        241
        241
         

      
        242
        
        -// rawSpan is a source span tagged with semantic token

      
        
        242
        +// rawSpan is a source span carrying a semantic token type and modifiers

      
        243
        243
         type rawSpan struct {

      
        244
        244
         	span      token.Span

      
        245
        245
         	tok, mods uint32

      ···
        384
        384
         				continue

      
        385
        385
         			}

      
        386
        386
         			cursor += size

      
        387
        
        -			col += utf16Units(r)

      
        
        387
        +			col += lsputil.Utf16LenRune(r)

      
        388
        388
         		}

      
        389
        389
         	}

      
        390
        390
         	for i, t := range raw {

      ···
        403
        403
         	return out

      
        404
        404
         }

      
        405
        405
         

      
        406
        
        -func utf16Units(r rune) int {

      
        407
        
        -	if r >= 0x10000 && r <= 0x10FFFF {

      
        408
        
        -		return 2

      
        409
        
        -	}

      
        410
        
        -	return 1

      
        411
        
        -}

      
        412
        
        -

      
        413
        406
         func visitEntry(content string, e ast.Entry, emit semEmitFunc) {

      
        414
        407
         	switch e := e.(type) {

      
        415
        408
         	case *ast.Transaction:

      ···
        481
        474
         		if e.DateTime.Time != nil {

      
        482
        475
         			emit(e.DateTime.Time.Span, semDate, 0)

      
        483
        476
         		}

      
        484
        
        -		// commodity: text between the date (or time) and the amount

      
        485
        477
         		commStart := e.DateTime.Date.Span.End.Offset

      
        486
        478
         		if e.DateTime.Time != nil {

      
        487
        479
         			commStart = e.DateTime.Time.Span.End.Offset

      ···
        494
        486
         	case *ast.ConversionDirective:

      
        495
        487
         		emit(directiveKeyword(e.Span, "C"), semDirective, 0)

      
        496
        488
         		semEmitAmount(content, &e.From, emit)

      
        497
        
        -		// = operator: text between the two amounts

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

      
        499
        490
         			emit(op, semOperator, 0)

      
        500
        491
         		}

      ···
        598
        589
         		emit(p.StatusSpan, semStatus, 0)

      
        599
        590
         	}

      
        600
        591
         

      
        601
        
        -	// virtual brackets

      
        602
        592
         	if p.Type == ast.PostingVirtualUnbalanced || p.Type == ast.PostingVirtualBalanced {

      
        603
        
        -		// opening bracket

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

      
        605
        594
         			if content[off] == '(' || content[off] == '[' {

      
        606
        595
         				emit(offsetSpan(p.Span.File, off, off+1), semOperator, modifierAbstract)

      
M internal/lsp/textdocument_semantic_tokens_test.go
···
        329
        329
         			ar := golden.Read(t, tt)

      
        330
        330
         			in, edited := ar.Get("in.journal"), ar.Get("edited.journal")

      
        331
        331
         

      
        332
        
        -			// golden: the server's incremental result equals a full tokenization

      
        333
        332
         			golden.Assert(t, ar, renderSemanticTokens(serverSemTokensAfterEdit(t, newServer(t).server, string(in), string(edited))))

      
        334
        333
         

      
        335
        
        -			// engage: the incremental path actually runs, not a silent full rebuild

      
        
        334
        +			// the incremental path has to actually run, not silently rebuild

      
        336
        335
         			ev := partialChangeFromDiff(string(in), string(edited))[0].(*protocol.TextDocumentContentChangePartial)

      
        337
        336
         			start, oldEnd, newEnd, delta := editRegion(string(in), ev)

      
        338
        337
         			_, entries := computeSemTokens(string(in), parseJournalStr(string(in)))

      ···
        361
        360
         	}

      
        362
        361
         }

      
        363
        362
         

      
        364
        
        -// BenchmarkSemanticTokensDelta measures the cost of one dela response after an edit.

      
        
        363
        +// BenchmarkSemanticTokensDelta measures the cost of one delta response after an edit.

      
        365
        364
         func BenchmarkSemanticTokensDelta(b *testing.B) {

      
        366
        365
         	content := openJournal(b, "../../journal/testdata/journals/actual-1ktxns-100accts.journal")

      
        367
        366
         	prev := encodeSemTokens(tokenizeForSemantics(content, parseJournalStr(content)))

      
M internal/settings/settings.go
···
        54
        54
         	return s, warns, err

      
        55
        55
         }

      
        56
        56
         

      
        57
        
        -// Apply merges raw setting from a config file into Settings object.

      
        
        57
        +// Apply merges settings from a config file into s.

      
        58
        58
         func (s *Settings) Apply(raw map[string]any) ([]string, error) {

      
        59
        59
         	return applyMap(raw, s.applyFileField)

      
        60
        60
         }

      ···
        70
        70
         	}

      
        71
        71
         }

      
        72
        72
         

      
        73
        
        -// ApplyLSP merges raw settings from lsp server config into Settings object.

      
        
        73
        +// ApplyLSP merges settings from the LSP server configuration into s.

      
        74
        74
         func (s *Settings) ApplyLSP(raw map[string]any) ([]string, error) {

      
        75
        75
         	return applyMap(raw, s.applyLSPField)

      
        76
        76
         }

      
M journal/journal_test.go
···
        14
        14
         

      
        15
        15
         var tests = map[string]test{

      
        16
        16
         	"actual-1ktxns-100accts.journal":            {desc: "hledger: stress test: 1000 transactions, 100 accounts, number-only account names"},

      
        17
        
        -	"actual-accounttypes.journal":               {desc: "hledger: account type annotations (type:A, type:L) via comments"}, // todo: tags are not supported yet

      
        
        17
        +	"actual-accounttypes.journal":               {desc: "hledger: account type annotations (type:A, type:L) via comments"}, // TODO: tags are not supported yet

      
        18
        18
         	"actual-alias.journal":                      {desc: "hledger: account alias directives for renaming"},

      
        19
        19
         	"actual-borrowing.journal":                  {desc: "hledger: borrowing/lending example with liabilities"},

      
        20
        20
         	"actual-business.journal":                   {desc: "hledger: simple business transactions with commodities"},

      
        21
        21
         	"actual-goal-budget.journal":                {desc: "hledger: goal budget using periodic transactions"},

      
        22
        22
         	"actual-i18n-en.journal":                    {desc: "hledger: internationalization with account types in English"},

      
        23
        
        -	"actual-ledger-baseline-opt-lots-basis.dat": {err: true, desc: "ledger: G/S prefixes"}, // no lot support yet

      
        
        23
        +	"actual-ledger-baseline-opt-lots-basis.dat": {err: true, desc: "ledger: G/S prefixes"}, // TODO: no lot support yet

      
        24
        24
         	"actual-ledger-input-divzero.dat":           {desc: "ledger: fuzz corpus, designed to cause divide-by-zero"},

      
        25
        25
         	"actual-ledger-input-parsing.dat":           {desc: "ledger: fuzz corpus, tests EOF without newline"},

      
        26
        26
         	"actual-ledger-input-sample.dat":            {desc: "ledger: fuzz corpus, default commodity directive"},

      
        27
        27
         	"actual-ledger-input-standard.dat":          {desc: "ledger: fuzz corpus, standard ledger format"},

      
        28
        28
         	"actual-ledger-input-transfer.dat":          {desc: "ledger: fuzz corpus, byte quantity (non-monetary)"},

      
        29
        
        -	"actual-ledger-input-wow.dat":               {err: true, desc: "ledger-cli: fuzz corpus, World of Warcraft currency (1G=100s)"}, // no lot support ye

      
        
        29
        +	"actual-ledger-input-wow.dat":               {err: true, desc: "ledger-cli: fuzz corpus, World of Warcraft currency (1G=100s)"}, // TOOD: no lot support yet

      
        30
        30
         	"actual-multicurrency.journal":              {desc: "hledger: multi-currency transactions with HRK/EUR"},

      
        31
        31
         	"actual-personal.journal":                   {desc: "hledger: simple personal finance example"},

      
        32
        32
         	"actual-quickstart.journal":                 {desc: "hledger: quickstart guide with commodity directive"},

      
M journal/lexer/lexer.go
···
        120
        120
         		return tok

      
        121
        121
         	case l.ch == ';' || l.ch == '#' || l.ch == '%':

      
        122
        122
         		l.mode = modeComment

      
        123
        
        -		return l.lexSingle(token.SEMICOLON) // todo: ??

      
        
        123
        +		return l.lexSingle(token.SEMICOLON)

      
        124
        124
         	case l.ch == '*': // * at col 0 == comment

      
        125
        125
         		l.mode = modeComment

      
        126
        126
         		return l.lexSingle(token.STAR)

      ···
        237
        237
         	case '"', '\'':

      
        238
        238
         		return l.lexString()

      
        239
        239
         	default: // description / payee

      
        240
        
        -		if l.isDate() { // secondsry date after =

      
        
        240
        +		if l.isDate() { // secondary date after =

      
        241
        241
         			return l.lexDate()

      
        242
        242
         		}

      
        243
        243
         		return l.lexText()

      ···
        482
        482
         	return token.Token{Type: token.TEXT, Literal: lit, Span: l.span(s)}

      
        483
        483
         }

      
        484
        484
         

      
        485
        
        -// lexAccountNameDirective reads accout name in directive context.

      
        
        485
        +// lexAccountNameDirective reads an account name in directive context.

      
        486
        486
         // stops at any whitespace, supports multi-word names("Taxi Fare").

      
        487
        487
         func (l *Lexer) lexAccountNameDirective() token.Token {

      
        488
        488
         	s := l.save()

      ···
        574
        574
         	}

      
        575
        575
         	lit := l.lit(s)

      
        576
        576
         	kind := l.keyword(lit)

      
        577
        
        -	if kind == token.ILLEGAL { // todo: report an error ??

      
        
        577
        +	if kind == token.ILLEGAL {

      
        578
        578
         		kind = token.TEXT

      
        579
        579
         	} else {

      
        580
        580
         		l.mode = modeDirective

      
M journal/lexer/lexer_test.go
···
        30
        30
         		"transaction with code",

      
        31
        31
         		"transaction with unicode commodity symbols",

      
        32
        32
         		"transaction with virtual accounts",

      
        33
        
        -		"transaction, accounts with uppercase latters",

      
        
        33
        +		"transaction, accounts with uppercase letters",

      
        34
        34
         	}

      
        35
        35
         	for _, tt := range tests {

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

      ···
        82
        82
         		for range maxTokens {

      
        83
        83
         			tok := l.Next()

      
        84
        84
         

      
        85
        
        -			// Monotonic span

      
        86
        85
         			if tok.Span.Start.Offset < prevEnd {

      
        87
        86
         				t.Fatalf("non-monotonic span: prevEnd=%d current=%s %d",

      
        88
        87
         					prevEnd, tok.Type, tok.Span.Start.Offset)

      
        89
        88
         			}

      
        90
        89
         

      
        91
        
        -			// Token type in range (no garbage from memory corruption)

      
        92
        90
         			if tok.Type < 0 || tok.Type > maxKnownTokenType {

      
        93
        91
         				t.Fatalf("token type out of range: %d", tok.Type)

      
        94
        92
         			}

      
        95
        93
         

      
        96
        
        -			// Span in bounds (EOF/NEWLINE sentinels may extend one past input)

      
        
        94
        +			// EOF and NEWLINE sentinels may extend one past the input

      
        97
        95
         			maxEnd := len(data)

      
        98
        96
         			if tok.Type == token.NEWLINE || tok.Type == token.EOF {

      
        99
        97
         				maxEnd = len(data) + 1

      ···
        124
        122
         			t.Fatalf("token consumed beyond input: end=%d len=%d", prevEnd, len(data))

      
        125
        123
         		}

      
        126
        124
         

      
        127
        
        -		// Pass 2: re-lex the same input — token stream must be identical

      
        
        125
        +		// pass 2: re-lex the same input, the token stream must be identical

      
        128
        126
         		l2 := New("j", data)

      
        129
        127
         		for _, expected := range tokens {

      
        130
        128
         			tok := l2.Next()

      
M journal/loader.go
···
        61
        61
         	ast *ast.Journal

      
        62
        62
         }

      
        63
        63
         

      
        64
        
        -// Loader include-aware journal parsing caching.

      
        
        64
        +// Loader parses a journal and its includes, caching both by canonical path.

      
        65
        65
         type Loader struct {

      
        66
        66
         	mu           sync.RWMutex

      
        67
        67
         	contentCache map[string][]byte // canonical path: normalised content

      ···
        164
        164
         	l.mu.Unlock()

      
        165
        165
         }

      
        166
        166
         

      
        167
        
        -// Evict drops a file's cached content.

      
        
        167
        +// Evict drops a file's cached content and every parse cached for it.

      
        168
        168
         func (l *Loader) Evict(fpath string) {

      
        169
        169
         	canon := CanonicalPath(fpath)

      
        170
        170
         	l.mu.Lock()

      ···
        211
        211
         	return bytes.ReplaceAll(content, []byte("\r"), []byte("\n"))

      
        212
        212
         }

      
        213
        213
         

      
        214
        
        -// resolveOccurrence recursively parses one occurrence and its includes

      
        
        214
        +// resolveOccurrence recursively parses one occurrence and its includes.

      
        215
        215
         func (l *Loader) resolveOccurrence(rj *ResolvedJournal, parent *ParsedFile, fpath string, src []byte, defaultYear int, stack []string) {

      
        216
        216
         	// cycle detection uses canonical paths to catch cycles through symlinks.

      
        217
        217
         	canon := CanonicalPath(fpath)

      ···
        316
        316
         	return target, nil

      
        317
        317
         }

      
        318
        318
         

      
        319
        
        -// CanonicalPath resolvea path to it's canonical form: absolute, symlinks evaluated, cleaned.

      
        
        319
        +// CanonicalPath resolves a path to its canonical form: absolute, symlinks evaluated, cleaned.

      
        320
        320
         func CanonicalPath(path string) string {

      
        321
        321
         	abs, err := filepath.Abs(path)

      
        322
        322
         	if err != nil {

      
M journal/parser/parser.go
···
        1198
        1198
         }

      
        1199
        1199
         

      
        1200
        1200
         func (p *Parser) expectNewline() {

      
        1201
        
        -	if p.got(token.NEWLINE) || p.got(token.EOF) {

      
        1202
        
        -		if p.got(token.NEWLINE) {

      
        1203
        
        -			p.advance()

      
        1204
        
        -		}

      
        1205
        
        -		return

      
        
        1201
        +	switch {

      
        
        1202
        +	case p.got(token.NEWLINE):

      
        
        1203
        +		p.advance()

      
        
        1204
        +	case p.got(token.EOF):

      
        
        1205
        +	default:

      
        
        1206
        +		p.errorf("expected %s, got %s", token.NEWLINE, p.cur.Type)

      
        1206
        1207
         	}

      
        1207
        
        -	p.errorf("expected %s, got %s", token.NEWLINE, p.cur.Type)

      
        1208
        1208
         }

      
        1209
        1209
         

      
        1210
        1210
         func (p *Parser) advance() token.Token {

      ···
        1417
        1417
         	return ast.QuantityFormat{Decimal: dec, Thousands: thou, Precision: prec}

      
        1418
        1418
         }

      
        1419
        1419
         

      
        1420
        
        -// parseSimpleDate  parses full YYYY/MM/DD date literal embedded in free text.

      
        
        1420
        +// parseSimpleDate parses full YYYY/MM/DD date literal embedded in free text.

      
        1421
        1421
         func parseSimpleDate(s string) ast.Date {

      
        1422
        1422
         	year, month, day, sep, err := ParseDateLiteral(s)

      
        1423
        1423
         	if err != nil {

      ···
        1471
        1471
         	return 0

      
        1472
        1472
         }

      
        1473
        1473
         

      
        1474
        
        -// parseCommentTags extacts tags from comment text.

      
        
        1474
        +// parseCommentTags extracts tags from comment text.

      
        1475
        1475
         // A tag is a word immediately followed by a ':', with an optional value that ends at a comma or the end of a line.

      
        1476
        1476
         // https://hledger.org/1.52/hledger.html?highlight=tags#tags

      
        1477
        1477
         func parseCommentTags(text string, base token.Span) []ast.Tag {

      
M journal/parser/parser_test.go
···
        28
        28
         		"P directive quoted commodity",

      
        29
        29
         		"N directive",

      
        30
        30
         		"N directive quoted",

      
        31
        
        -		"inclue directive",

      
        32
        
        -		"inclue directive with comment",

      
        33
        
        -		"inclue directive digit path",

      
        
        31
        +		"include directive",

      
        
        32
        +		"include directive with comment",

      
        
        33
        +		"include directive digit path",

      
        34
        34
         		"apply tag directive",

      
        35
        35
         		"apply fixed directive",

      
        36
        36
         		"end apply directive",

      ···
        44
        44
         		"account directive with comment",

      
        45
        45
         		"account with subdirectives",

      
        46
        46
         		"account subdirectives types",

      
        47
        
        -		"comodity directive",

      
        48
        
        -		"comodity directive word",

      
        49
        
        -		"comodity directive no space",

      
        
        47
        +		"commodity directive",

      
        
        48
        +		"commodity directive word",

      
        
        49
        +		"commodity directive no space",

      
        50
        50
         		"commodity quantity first",

      
        51
        51
         		"commodity quantity after",

      
        52
        52
         		"commodity with subdirectives",

      ···
        59
        59
         		"account with spaces",

      
        60
        60
         		"transaction with multiword payee",

      
        61
        61
         		"transaction pending",

      
        62
        
        -		"transaction clearerd",

      
        
        62
        +		"transaction cleared",

      
        63
        63
         		"transaction with note",

      
        64
        64
         		"transaction with comment",

      
        65
        65
         		"transaction with secondary date",

      
M journal/token/token.go
···
        37
        37
         	ILLEGAL Type = iota

      
        38
        38
         	EOF

      
        39
        39
         

      
        40
        
        -	WHITESPACE // singe space or tab

      
        
        40
        +	WHITESPACE // single space or tab

      
        41
        41
         	INDENT     // leading whitespace (posting/subdirective marker)

      
        42
        42
         	NEWLINE    // \n

      
        43
        43