Skip to content

fix(format): resolve current-file/note tokens in one pass to stop rescan loops (#1358)#1369

Open
chhoumann wants to merge 2 commits into
masterfrom
chhoumann/1358-current-file-token-corrupt
Open

fix(format): resolve current-file/note tokens in one pass to stop rescan loops (#1358)#1369
chhoumann wants to merge 2 commits into
masterfrom
chhoumann/1358-current-file-token-corrupt

Conversation

@chhoumann

@chhoumann chhoumann commented Jun 16, 2026

Copy link
Copy Markdown
Owner

Summary

Closes #1358.

Note-derived format tokens — {{linkcurrent}}, {{linksection}}, {{filenamecurrent}}, {{folder}}/{{folder|name}}, {{title}} — resolved as separate sequential regex passes after format(). Each replacement embeds user-controlled text (the active file's basename, the target folder's name, the note title), so a later pass re-scanned an earlier pass's generated output. Two defects when a note/folder/title was literally named like a token:

  • Corruption — a note {{folder}}.md with a {{linkcurrent}} body: the link pass makes [[{{folder}}]], then the folder pass rewrites {{folder}}[[]] (link destroyed). Same for {{title}}.md.
  • HangreplaceCurrentFileNameInString and replaceTitleInString used while (REGEX.test(output)) output = replacer(output, REGEX, value). When the value itself matches the regex (a note {{filenamecurrent}}.md, or a title literally {{title}}), the loop never terminates and freezes the formatter (and Obsidian).

{{linkcurrent}}/{{linksection}} were already hardened against re-scanning each other in #1356; this PR extends that single-pass guarantee to all note-derived tokens so none can re-scan another's output.

Fix

Resolve every note-derived token in one function-replacer passFormatter.replaceCurrentFileTokensInString(input, opts). A single left-to-right String.replace inserts each resolved value literally and never re-scans it, fixing both the corruption and the hang.

  • The six entry points each call it with their token subset; an inactive token category is left literal, preserving per-entry-point behaviour:
    entry point tokens resolved
    CompleteFormatter.formatFileContent links + filename + folder + title
    CompleteFormatter.formatFileName filename + folder (links literal; {{title}} throws earlier)
    CompleteFormatter.formatFolderPath folder
    CompleteFormatter.formatLocationString links + filename + title (folder deliberately literal)
    FileNameDisplayFormatter.format filename + folder
    FormatDisplayFormatter.format links + filename + folder (no title, as before)
  • The individual replacers now delegate to the combined method (centralising the logic and fixing their standalone self-loops); replaceLinkToCurrentFileInString is converted from a while loop to a single pass.

CaptureChoiceFormatter inherits the fix via super.formatFileContent/formatLocationString. Preflight (RequirementCollector) and the syntax suggester never resolve these tokens, so they need no change.

Behaviour preserved

Required/optional throw semantics + byte-identical messages + link-over-filename precedence; {{folder}}/{{title}} never throw; multi-occurrence replacement; $ treated literally; case-insensitive {{FOLDER|NAME}}; the inactive-token-literal contract per entry point. The only intended change is that a token appearing inside a generated value is no longer re-resolved — exactly the bug class being fixed.

Verification

  • Reproduced first in the isolated E2E Obsidian vault via api.format: {{folder}}.md+{{linkcurrent}}[[]]; {{title}}.md+{{linkcurrent}}[[]]; the {{filenamecurrent}} cases hung.
  • After the fix (real app): [[{{folder}}]], [[{{title}}]], {{filenamecurrent}}, [[{{filenamecurrent}}]] — the previously-hanging cases now return. Happy path unchanged (Normal Note[[Normal Note]]).
  • Full suite green (2449 passed), tsc, eslint, and build clean.
  • New regression tests: formatter-token-named-file.test.ts (combined-resolver unit semantics: no-loop, no-corruption, inactive-literal, throw precedence, $/case/multi-occurrence) and production-level guards in completeFormatter.test.ts driving the real formatFileContent/formatFileName so a future revert to sequential passes is caught.

Review

Design reviewed by an ultracode completeness pass + 2 adversarial reviewers (all "sound"); implementation diff reviewed by 3 adversarial reviewers (Codex).

Known, pre-existing limitations (out of scope, not regressions)

  • Nested-template boundary: a parent that {{TEMPLATE:}}-includes a child whose output contains a resolved value literally equal to a token (only possible with a degenerately-named note) is still re-scanned by the parent's pass ([[]]). The old code produced the same result via the child's own pass; fixing it cleanly needs sentinel-protection across the template-composition boundary.
  • Preview formatters: contextual tokens resolve before the macro/template/field/random passes, so a file/folder literally named {{macro:…}}/{{random:n}} is mutated in the preview only (no disk writes; pre-existing position, unchanged here).

Summary by CodeRabbit

  • Bug Fixes
    • Fixed issue #1358: prevented re-scanning/re-writing of generated file links when active file basenames include formatting tokens.
    • Resolved hanging behavior when the active file is literally named {{filenamecurrent}}.
    • Fixed token corruption by switching current-file token resolution to a safer single-pass flow across formatter entry points (including folder/location handling).
  • Tests
    • Added an end-to-end regression suite covering tokenized basenames and link preservation for the formatter entry points.

…can loops (#1358)

Note-derived tokens ({{linkcurrent}}, {{linksection}}, {{filenamecurrent}},
{{folder}}/{{folder|name}}, {{title}}) resolved as separate sequential regex
passes after format(), so a later pass re-scanned an earlier pass's generated
output. When the active file/folder/title was literally named like a token this
corrupted a generated link (a note "{{folder}}.md" with a {{linkcurrent}} body
produced [[]] instead of [[{{folder}}]]) or, via the while-loop replacers, hung
the formatter forever (a note "{{filenamecurrent}}.md", or a title literally
"{{title}}").

Resolve all note-derived tokens in a single function-replacer pass
(replaceCurrentFileTokensInString) so a resolved value is never re-scanned,
generalizing the link-only pass added in #1356. Each of the six entry points
(formatFileContent / formatFileName / formatFolderPath / formatLocationString
plus the two display formatters) calls it with its token subset; inactive
tokens stay literal. The individual replacers delegate to it, and
replaceLinkToCurrentFileInString is now single-pass — fixing their standalone
self-loops too.

Required/optional throw semantics + messages + precedence, multi-occurrence
replacement, $-literal handling, and case-insensitive {{FOLDER|name}} are all
preserved. Adds regression tests for every token-named-file direction (combined
resolver + production entry points).
@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d975675b-937d-4a1f-8f7c-d040167ee222

📥 Commits

Reviewing files that changed from the base of the PR and between 0a6da90 and 47f08f9.

📒 Files selected for processing (1)
  • src/formatters/formatter-token-named-file.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/formatters/formatter-token-named-file.test.ts

📝 Walkthrough

Walkthrough

Introduces a protected replaceCurrentFileTokensInString method in Formatter that resolves all note-derived tokens ({{linkcurrent}}, {{linksection}}, {{filenamecurrent}}, {{folder}}, {{title}}) in a single left-to-right regex pass, preventing re-scan loops and cross-token corruption. All legacy per-token helpers and formatter call sites are updated to delegate to this unified resolver. Regression tests are added covering loop prevention, corruption prevention, required/optional error precedence, and folder edge cases.

Changes

Single-pass current-file token resolver

Layer / File(s) Summary
Unified token resolver and legacy helper delegation
src/formatters/formatter.ts
Adds replaceCurrentFileTokensInString with a combined alternation regex and lazy per-category caching; refactors replaceLinkToCurrentFileInString to a single-pass String.replace; updates replaceCurrentFileLinksInString, replaceCurrentFileNameInString, replaceTargetFolderInString, and replaceTitleInString to delegate to it; removes now-unused regex constant imports.
Call site wiring across all formatters
src/formatters/completeFormatter.ts, src/formatters/fileNameDisplayFormatter.ts, src/formatters/formatDisplayFormatter.ts
Updates formatFileName, formatFileContent, formatFolderPath, formatLocationString, FileNameDisplayFormatter.format(), and FormatDisplayFormatter.format() to call replaceCurrentFileTokensInString with the appropriate option set, replacing prior sequences of separate per-token replacement calls.
Regression tests: unit and end-to-end
src/formatters/formatter-token-named-file.test.ts, src/formatters/completeFormatter.test.ts
Adds StubFormatter exposing the protected resolver for unit testing; covers no-loop, no-corruption, inactive-category, required/optional precedence, folder edge cases, and $-literal handling; adds end-to-end CompleteFormatter tests for token-named-file scenarios.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • chhoumann/quickadd#1356: Adds {{linksection}} support and hardens link-token resolution to a single-pass; this PR extends that single-pass pattern to all note-derived tokens and ensures the full resolver pipeline avoids re-scan loops.
  • chhoumann/quickadd#1328: Introduces the {{FOLDER}}/{{FOLDER|name}} token and replaceTargetFolderInString, which this PR consolidates into the unified replaceCurrentFileTokensInString resolver.

🐇 One pass to rule them all, no loops in sight,
The tokens now march left to right.
{{filenamecurrent}} once spun forever and hung —
Now the regex sings one tidy song, sweetly sung.
No re-scan, no corrupt, no infinite dread,
Just a fresh resolved string and a warm carrot bed! 🥕

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely summarizes the core fix: resolving current-file/note tokens in a single pass to prevent re-scanning loops.
Linked Issues check ✅ Passed The PR fully implements the fix for issue #1358: consolidates all note-derived tokens into a single-pass resolver, adds regression tests for token-named files, and preserves existing semantics.
Out of Scope Changes check ✅ Passed All changes are directly related to resolving issue #1358; no out-of-scope modifications detected.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chhoumann/1358-current-file-token-corrupt

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 16, 2026

Copy link
Copy Markdown

Deploying quickadd with  Cloudflare Pages  Cloudflare Pages

Latest commit: 47f08f9
Status: ✅  Deploy successful!
Preview URL: https://69e9c22b.quickadd.pages.dev
Branch Preview URL: https://chhoumann-1358-current-file.quickadd.pages.dev

View logs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/formatters/formatter-token-named-file.test.ts`:
- Line 113: The constant ALL does not follow TypeScript naming conventions as it
should be in camelCase. Rename the ALL constant declaration to allTokens, and
then update all 10 usages of ALL throughout the file to use the new allTokens
identifier instead. This ensures consistency with TypeScript naming standards
for constants and variables.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1be3f29b-e6dd-46b7-92d8-ecba29120869

📥 Commits

Reviewing files that changed from the base of the PR and between a3e0d88 and 0a6da90.

📒 Files selected for processing (6)
  • src/formatters/completeFormatter.test.ts
  • src/formatters/completeFormatter.ts
  • src/formatters/fileNameDisplayFormatter.ts
  • src/formatters/formatDisplayFormatter.ts
  • src/formatters/formatter-token-named-file.test.ts
  • src/formatters/formatter.ts

Comment thread src/formatters/formatter-token-named-file.test.ts Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Current-file tokens corrupt/loop when a note is literally named like a token (pre-existing rescan class)

1 participant