3 files changed,
114 insertions(+),
100 deletions(-)
Author:
Oleksandr Smirnov
olexsmir@gmail.com
Committed at:
2026-08-15 18:59:46 +0300
Authored at:
2026-08-14 12:36:36 +0300
Change ID:
wlyqytwvtrvqzunpsmxqsypmoqrqsklz
Parent:
c2e8832
M
internal/analyzer/analyzer.go
··· 21 21 // account "alias" subdirectives and top-level "alias A = B" directives. 22 22 AccountAliases map[string]string 23 23 24 + // AliasDirectives holds every top-level "alias A = B" directive in entry order. 25 + AliasDirectives []*ast.AliasDirective 26 + 24 27 AccountNames []string // sorted for binary search 25 28 PayeeNames []string // sorted, all payee names from directives + usage 26 29 AccountsByPrefix map[string][]string // "expenses:" -> ["expenses:food", "expenses:taxi"]
M
internal/analyzer/build.go
··· 127 127 // source name resolves to the target account. 128 128 func (a *Analysis) addAliasDirective(ad *ast.AliasDirective) { 129 129 a.AccountAliases[ad.From.String()] = ad.To.String() 130 + a.AliasDirectives = append(a.AliasDirectives, ad) 130 131 } 131 132 132 133 func (a *Analysis) addPayeeDirective(pd *ast.PayeeDirective) {
M
internal/lsp/textdocument_rename.go
··· 68 68 return nil, nil 69 69 } 70 70 71 - changes := renameChanges(an, ref, params.NewName) 71 + changes := renameChanges(an, ref, params.NewName, state.lineIdx) 72 72 if len(changes) == 0 { 73 73 return nil, nil 74 74 } ··· 255 255 return nil 256 256 } 257 257 258 -// renameChanges collects the workspace edits renaming ref to newName 259 -func renameChanges(an *analyzer.Analysis, ref *symbolRef, newName string) map[uri.URI][]protocol.TextEdit { 260 - changes := make(map[uri.URI][]protocol.TextEdit) 261 - for _, pf := range an.Files { 262 - content := string(pf.Src) 263 - // LineIndex is built lazily: most files have no matching edits, and 264 - // each edit needs an O(log n) offset-to-position lookup, not a scan. 265 - var li *lsputil.LineIndex 266 - var edits []protocol.TextEdit 267 - add := func(span token.Span, text string) { 268 - if li == nil { 269 - li = lsputil.NewLineIndex(content) 258 +func renameChanges(an *analyzer.Analysis, ref *symbolRef, newName string, primaryLI *lsputil.LineIndex) map[uri.URI][]protocol.TextEdit { 259 + type fileEdits struct { 260 + li *lsputil.LineIndex 261 + edits []protocol.TextEdit 262 + } 263 + files := make(map[int]*fileEdits) 264 + add := func(fileIdx int, span token.Span, text string) { 265 + fe := files[fileIdx] 266 + if fe == nil { 267 + fe = &fileEdits{} 268 + if fileIdx == 0 { 269 + fe.li = primaryLI 270 + } else { 271 + fe.li = lsputil.NewLineIndex(string(an.Files[fileIdx].Src)) 270 272 } 271 - edits = append(edits, protocol.TextEdit{ 272 - Range: li.SpanRange(span), 273 - NewText: text, 274 - }) 273 + files[fileIdx] = fe 275 274 } 276 - for _, e := range pf.Ast.Entries { 277 - renameEntry(add, ref, newName, content, e) 278 - } 279 - if len(edits) > 0 { 280 - changes[uri.File(pf.Path)] = edits 281 - } 275 + fe.edits = append(fe.edits, protocol.TextEdit{ 276 + Range: fe.li.SpanRange(span), 277 + NewText: text, 278 + }) 279 + } 280 + 281 + switch ref.kind { 282 + case symbolAccount: 283 + renameAccountEdits(an, ref, newName, add) 284 + case symbolCommodity: 285 + renameCommodityEdits(an, ref, newName, add) 286 + case symbolPayee: 287 + renamePayeeEdits(an, ref, newName, add) 288 + case symbolTag: 289 + renameTagEdits(an, ref, newName, add) 290 + } 291 + 292 + changes := make(map[uri.URI][]protocol.TextEdit, len(files)) 293 + for fileIdx, fe := range files { 294 + changes[uri.File(an.Files[fileIdx].Path)] = fe.edits 282 295 } 283 296 sortAndDedup(changes) 284 297 return changes 285 298 } 286 299 287 -func renameEntry(add func(token.Span, string), ref *symbolRef, newName, content string, e ast.Entry) { 288 - switch e := e.(type) { 289 - case *ast.Transaction: 290 - renamePayee(add, ref, newName, e.Payee) 291 - renameCommentTags(add, ref, newName, content, e.Comment) 292 - for _, c := range e.HeaderComments { 293 - renameCommentTags(add, ref, newName, content, c) 294 - } 295 - renamePostings(add, ref, newName, content, e.Postings) 296 - case *ast.PeriodicTransaction: 297 - renameCommentTags(add, ref, newName, content, e.Comment) 298 - for _, c := range e.HeaderComments { 299 - renameCommentTags(add, ref, newName, content, c) 300 - } 301 - renamePostings(add, ref, newName, content, e.Postings) 302 - case *ast.AutomatedTransaction: 303 - renameCommentTags(add, ref, newName, content, e.Comment) 304 - for _, c := range e.HeaderComments { 305 - renameCommentTags(add, ref, newName, content, c) 300 +func renameAccountEdits(an *analyzer.Analysis, ref *symbolRef, newName string, add func(int, token.Span, string)) { 301 + for _, name := range an.AccountNames { 302 + if !accountMatches(name, ref.name) { 303 + continue 306 304 } 307 - renamePostings(add, ref, newName, content, e.Postings) 308 - case *ast.Comment: 309 - renameCommentTags(add, ref, newName, content, e) 310 - case *ast.AccountDirective: 311 - if text, ok := ref.renameTo(symbolAccount, e.Account.String(), newName); ok { 312 - add(e.Account.Span, text) 305 + info := an.Accounts[name] 306 + text := newName + strings.TrimPrefix(name, ref.name) 307 + for _, u := range info.Usages { 308 + add(u.FileIndex, u.Posting.Account.Span, text) 313 309 } 314 - for _, sd := range e.Subdirectives { 315 - if sd.Name == "alias" { 316 - if text, ok := ref.renameTo(symbolAccount, sd.Value, newName); ok { 317 - add(sd.ValueSpan, text) 318 - } 310 + } 311 + for _, info := range an.Accounts { 312 + for _, d := range info.Directives { 313 + fileIdx := fileIndexForEntry(an, d) 314 + if fileIdx < 0 { 315 + continue 319 316 } 320 - } 321 - case *ast.AliasDirective: 322 - if text, ok := ref.renameTo(symbolAccount, e.From.String(), newName); ok { 323 - add(e.From.Span, text) 324 - } 325 - if text, ok := ref.renameTo(symbolAccount, e.To.String(), newName); ok { 326 - add(e.To.Span, text) 327 - } 328 - case *ast.CommodityDirective: 329 - if text, ok := ref.renameTo(symbolCommodity, e.Commodity, newName); ok { 330 - add(e.CommoditySpan, text) 331 - } 332 - case *ast.PayeeDirective: 333 - renamePayee(add, ref, newName, e.Name) 334 - case *ast.TagDirective: 335 - if text, ok := ref.renameTo(symbolTag, e.Name, newName); ok { 336 - if span, ok := tagDirectiveSpan(content, e); ok { 337 - add(span, text) 317 + if accountMatches(d.Account.String(), ref.name) { 318 + add(fileIdx, d.Account.Span, newName+strings.TrimPrefix(d.Account.String(), ref.name)) 319 + } 320 + for _, sd := range d.Subdirectives { 321 + if sd.Name == "alias" && accountMatches(sd.Value, ref.name) { 322 + add(fileIdx, sd.ValueSpan, newName+strings.TrimPrefix(sd.Value, ref.name)) 323 + } 338 324 } 339 325 } 340 326 } 341 -} 342 - 343 -func renamePostings(add func(token.Span, string), ref *symbolRef, newName, content string, postings []*ast.Posting) { 344 - for _, p := range postings { 345 - if text, ok := ref.renameTo(symbolAccount, p.Account.String(), newName); ok { 346 - add(p.Account.Span, text) 327 + for _, ad := range an.AliasDirectives { 328 + fileIdx := fileIndexForEntry(an, ad) 329 + if fileIdx < 0 { 330 + continue 347 331 } 348 - renameCommodity(add, ref, newName, p.Amount) 349 - if p.Cost != nil { 350 - renameCommodity(add, ref, newName, &p.Cost.Amount) 351 - } 352 - if p.Balance != nil { 353 - renameCommodity(add, ref, newName, &p.Balance.Amount) 332 + if accountMatches(ad.From.String(), ref.name) { 333 + add(fileIdx, ad.From.Span, newName+strings.TrimPrefix(ad.From.String(), ref.name)) 354 334 } 355 - renameCommentTags(add, ref, newName, content, p.Comment) 356 - for i := range p.Comments { 357 - renameCommentTags(add, ref, newName, content, &p.Comments[i]) 335 + if accountMatches(ad.To.String(), ref.name) { 336 + add(fileIdx, ad.To.Span, newName+strings.TrimPrefix(ad.To.String(), ref.name)) 358 337 } 359 338 } 360 339 } 361 340 362 -func renameCommodity(add func(token.Span, string), ref *symbolRef, newName string, am *ast.Amount) { 363 - if am == nil { 341 +func renameCommodityEdits(an *analyzer.Analysis, ref *symbolRef, newName string, add func(int, token.Span, string)) { 342 + info := an.Commodities[ref.name] 343 + if info == nil { 364 344 return 365 345 } 366 - if text, ok := ref.renameTo(symbolCommodity, am.Commodity, newName); ok { 367 - add(am.CommoditySpan, text) 346 + for _, d := range info.Directives { 347 + if fileIdx := fileIndexForEntry(an, d); fileIdx >= 0 { 348 + add(fileIdx, d.CommoditySpan, newName) 349 + } 350 + } 351 + for _, u := range info.Usages { 352 + add(u.FileIndex, u.Amount.CommoditySpan, newName) 368 353 } 369 354 } 370 355 371 -func renamePayee(add func(token.Span, string), ref *symbolRef, newName string, p *ast.Payee) { 372 - if p == nil { 356 +func renamePayeeEdits(an *analyzer.Analysis, ref *symbolRef, newName string, add func(int, token.Span, string)) { 357 + info := an.Payees[ref.name] 358 + if info == nil { 373 359 return 374 360 } 375 - if text, ok := ref.renameTo(symbolPayee, p.Name, newName); ok { 376 - add(p.Span, text) 361 + for _, d := range info.Directives { 362 + if d.Name != nil { 363 + if fileIdx := fileIndexForEntry(an, d); fileIdx >= 0 { 364 + add(fileIdx, d.Name.Span, newName) 365 + } 366 + } 367 + } 368 + for _, u := range info.Usage { 369 + add(u.FileIndex, u.Payee.Span, newName) 377 370 } 378 371 } 379 372 380 -func renameCommentTags(add func(token.Span, string), ref *symbolRef, newName, content string, c *ast.Comment) { 381 - if c == nil { 373 +func renameTagEdits(an *analyzer.Analysis, ref *symbolRef, newName string, add func(int, token.Span, string)) { 374 + info := an.Tags[ref.name] 375 + if info == nil { 382 376 return 383 377 } 384 - for i := range c.Tags { 385 - t := &c.Tags[i] 386 - if text, ok := ref.renameTo(symbolTag, t.Key, newName); ok { 387 - add(tagKeySpan(content, t), text) 378 + contents := make(map[int]string) // file index → source, converted once per file 379 + content := func(fileIdx int) string { 380 + s, ok := contents[fileIdx] 381 + if !ok { 382 + s = string(an.Files[fileIdx].Src) 383 + contents[fileIdx] = s 388 384 } 385 + return s 386 + } 387 + for _, d := range info.Directives { 388 + fileIdx := fileIndexForEntry(an, d) 389 + if fileIdx < 0 { 390 + continue 391 + } 392 + if span, ok := tagDirectiveSpan(content(fileIdx), d); ok { 393 + add(fileIdx, span, newName) 394 + } 395 + } 396 + for _, u := range info.Usage { 397 + span := tagKeySpan(content(u.FileIndex), u.Tag) 398 + add(u.FileIndex, span, newName) 389 399 } 390 400 } 391 401