Skip to content

Templater: deterministic snippet rendering (capture + template append)#1017

Merged
chhoumann merged 6 commits intomasterfrom
fix/templater-deterministic-render
Dec 14, 2025
Merged

Templater: deterministic snippet rendering (capture + template append)#1017
chhoumann merged 6 commits intomasterfrom
fix/templater-deterministic-render

Conversation

@chhoumann
Copy link
Copy Markdown
Owner

@chhoumann chhoumann commented Dec 13, 2025

Summary

This PR makes QuickAdd’s Templater integration deterministic by splitting snippet rendering from whole‑file rendering, and by only doing whole‑file renders when QuickAdd truly “owns” the file content.

The two core behavior changes:

  1. Capture into an existing file no longer triggers whole‑file Templater by default.

    • QuickAdd now renders only the inserted snippet via Templater’s parse_template(...) (as it already does for capture payloads in most non-editor flows).
    • This prevents unrelated <% %> elsewhere in the destination note (including fenced code blocks) from executing.
  2. Template append/prepend into an existing file now renders Templater on the template snippet only, not the entire file.

Additionally, this PR reduces race conditions for “trigger on file creation” workflows and makes tp.file.cursor() jumps more reliable by invoking the jump after the file is opened and active.

Motivation / root causes

Several long‑running issues stem from the same architectural problem: QuickAdd sometimes runs whole‑file Templater after a write, which:

  • executes unrelated <% %> anywhere in the destination file (even inside fenced code blocks), and
  • introduces ordering/race problems when Templater is also configured to run on file creation.

Key observed patterns:

What changed

1) Capture: stop whole‑file render by default (safe‑by‑default)

  • CaptureChoiceEngine.run() no longer unconditionally calls overwriteTemplaterOnce(...) after vault.modify(...) for non-editor insertion actions.
  • Added an explicit escape hatch (legacy/advanced):
    • Capture choice setting: “Run Templater on entire destination file after capture”
    • Stored in choice config as templater.afterCapture = "wholeFile" | "none" (default "none").

Rationale: capture formatting already parses the capture payload with Templater where needed; running whole‑file Templater afterward was executing unrelated code elsewhere in the destination note.

2) Template append/prepend: snippet‑only render

  • TemplateEngine.appendToFileWithTemplate(...) now runs templaterParseTemplate(...) on the formatted template snippet before inserting it.
  • Removed the post‑write whole‑file overwriteTemplaterOnce(...) call for append/prepend.

Rationale: appending a template should not execute unrelated <% %> that already exists in the destination file.

3) New-file capture + Templater trigger-on-create: wait before anchoring

When QuickAdd creates an empty file (create-without-template) and the user has Templater’s “trigger on file creation” enabled:

  • QuickAdd waits for the file to “stop changing” (mtime-based settle with grace/quiet periods) before reading/anchoring/inserting.

Rationale: Templater’s on-create handler intentionally runs after a delay and may write folder templates/includes that create the insert-after anchor. Waiting reduces “first run fails, second run works”.

4) Cursor jump reliability

After QuickAdd opens a file (Capture/Template workflows):

  • If the created/modified file is active and Templater’s auto_jump_to_cursor setting is enabled, QuickAdd triggers a jump to the next cursor location.
  • Uses Templater’s API when available (editor_handler.jump_to_next_cursor_location(file, true)), with a guarded command fallback.

Rationale: Templater’s auto-jump returns early if the target file isn’t the active file (timing/focus gated).

5) Centralized utility helpers + locking

In src/utilityObsidian.ts:

  • Added a guarded Templater accessor (getTemplaterPlugin) + trigger-on-create reader.
  • Added waitForFileToStopChanging(...) for delayed external edits (Templater’s ~300ms on-create delay).
  • Made whole-file Templater runs single-flight per file path to prevent QuickAdd overlapping its own renders.
  • Upgraded overwriteTemplaterOnce(...) with:
    • safe guards (md-only, plugin presence),
    • skipIfNoTags (default true) to avoid unnecessary runs,
    • a post-wait to stabilize read-after-render flows.
  • Added openExistingFileTab(app, file, focus) that respects focus and returns the leaf (or null).

Issue impact

Fixes / addresses

Partial mitigation

  • [BUG] Quickadd calling template twice #844: Reduces one major source of duplicated/interleaved execution by removing unnecessary whole-file renders and by preventing QuickAdd overlapping its own whole-file renders per file.
    • Full resolution may require higher-level single-flight (per-choice/per-run) and/or a stronger “Templater completion” handshake (see “Open questions / follow-ups”).

Not addressed

Behavior changes / compatibility

  • Default change: capture into existing files no longer triggers a whole-file Templater render after writing.
    • This is intentional and safe-by-default.
    • Workflows that relied on QuickAdd to execute pre-existing <% %> elsewhere in the destination note can re-enable legacy behavior via the new toggle.

Test plan

  • bun run test
  • bun run build-with-lint

Recommended manual sanity checks (Obsidian):

  1. Capture → existing file with destination note containing <% %> inside a fenced code block → ensure it does NOT execute.
  2. Capture → create file (no template) with Templater trigger-on-create + folder templates that create the insert-after anchor → ensure first run succeeds.
  3. Template choice → append/prepend into existing note containing other <% %> → ensure only inserted snippet is rendered.
  4. Template choice with tp.file.cursor() + auto-jump enabled → ensure cursor jumps when QuickAdd opens the file.

Open questions / follow-ups

  1. Consider replacing the mtime-based settle wait with a stronger completion handshake when available (Templater events / internal pending-set) to handle cases where Templater blocks on prompts before first write.
  2. Decide whether to add a migration for existing capture choices (keep legacy whole-file behavior for existing configs vs. safe-by-default for everyone).
  3. Optional: add an “append mode” escape hatch for Template append/prepend if needed.

Refs: #715, #844, #140, #171, #608, #149, #848

Summary by CodeRabbit

  • New Features

    • Toggle to run Templater on the entire destination file after capture.
    • Option to suppress Templater during Markdown file creation.
    • Open created/captured files with focus control and automatic Templater cursor advancement.
    • Markdown templates are processed before writing.
  • Bug Fixes

    • Safer, more reliable Templater flows with per-file coordination and waiting for templater-on-create actions to finish.
    • Improved handling when opening files/tabs to avoid lost focus.
  • Tests

    • Added tests validating templater parsing and cursor advancement behaviors.

✏️ Tip: You can customize this high-level summary in your review settings.

@vercel
Copy link
Copy Markdown

vercel Bot commented Dec 13, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
quickadd Ready Ready Preview Dec 14, 2025 5:02pm

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Dec 13, 2025

Walkthrough

Templater integration and coordination were added: new templater utilities, per-file suppression and locking, wait-for-change detection, cursor advancement, and a focus-aware workspace-open flow; engines and template handling were updated to pass suppression/focus options and to conditionally run templater actions.

Changes

Cohort / File(s) Change Summary
Engine runtime — templater & file handling
src/engine/CaptureChoiceEngine.ts, src/engine/QuickAddEngine.ts, src/engine/TemplateChoiceEngine.ts, src/engine/TemplateEngine.ts
Added conditional post-capture templater runs (only when afterCapture === "wholeFile"), engines call jumpToNextTemplaterCursorIfPossible after opening files, createFileWithInput gained opts (suppressTemplaterOnCreate), QuickAdd uses withTemplaterFileCreationSuppressed for MD files, and open calls now pass a focus flag to openExistingFileTab.
Utility layer — templater abstractions & workspace
src/utilityObsidian.ts
New TemplaterPluginLike type; getTemplaterPlugin, isTemplaterTriggerOnCreateEnabled, withTemplaterFileCreationSuppressed, waitForTemplaterTriggerOnCreateToComplete, waitForFileToStopChanging, withTemplaterFileLock, jumpToNextTemplaterCursorIfPossible; refactored overwriteTemplaterOnce and templaterParseTemplate; openExistingFileTab(app,file,focus) now returns `WorkspaceLeaf
Type system — capture choice config
src/types/choices/CaptureChoice.ts, src/types/choices/ICaptureChoice.ts
Added `templater?: { afterCapture?: "none"
UI builder
src/gui/ChoiceBuilder/captureChoiceBuilder.ts
Added Behavior toggle to configure this.choice.templater.afterCapture and helper to render the setting.
Tests / Mocks
src/engine/CaptureChoiceEngine.template-property-types.test.ts, src/engine/CaptureChoiceEngine.notice.test.ts, src/engine/TemplateChoiceEngine.notice.test.ts, src/utilityObsidian.templater-binding.test.ts
Extended/made new tests and mocks: added isTemplaterTriggerOnCreateEnabled, jumpToNextTemplaterCursorIfPossible, waitForFileToStopChanging mocks; editor mocks insertOnNewLineAbove/insertOnNewLineBelow; changed openExistingFileTab mock to return null; added tests ensuring templater methods preserve this binding.

Sequence Diagram

sequenceDiagram
    autonumber
    actor User
    participant Engine as Capture/Template Engine
    participant Vault as Vault / FileSystem
    participant Workspace as Workspace / Editor
    participant Templater as Templater Plugin

    User->>Engine: trigger capture/create
    Engine->>Vault: createFileWithInput(path, content, { suppressTemplaterOnCreate? })
    alt suppress templater for MD
        Engine->>Templater: withTemplaterFileCreationSuppressed(...wrap create...)
    end
    Vault-->>Engine: file created (TFile)
    alt templater trigger-on-create enabled & template not pre-parsed
        Engine->>Templater: waitForTemplaterTriggerOnCreateToComplete(file)
        Templater-->>Engine: templater-trigger changes complete
    end
    Engine->>Workspace: openExistingFileTab(file, focus=true)
    Workspace-->>Engine: WorkspaceLeaf or null
    Engine->>Templater: jumpToNextTemplaterCursorIfPossible(file)
    Templater-->>Engine: cursor moved / no-op
    alt choice.templater.afterCapture == "wholeFile"
        Engine->>Templater: overwriteTemplaterOnce(file, opts)
        Templater-->>Engine: templating complete
    end
    Engine-->>User: operation complete
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

  • Focus review on src/utilityObsidian.ts (locking, suppression, wait logic, error handling, API surface).
  • Verify createFileWithInput signature changes propagate to all callers and tests.
  • Confirm callers handle openExistingFileTab returning WorkspaceLeaf | null and the new focus parameter.
  • Check new tests/mocks for correct this binding and for the updated mock return values.

Possibly related PRs

Suggested labels

released

Poem

🐇
I nudged the cursor, held it still,
I hushed the templater's busy quill.
Files settle soft, locks keep the beat,
A tiny hop — templates and cursors meet.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and specifically describes the main change: improving deterministic Templater rendering for capture and template append operations, which is the core focus of the PR.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/templater-deterministic-render

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.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/engine/QuickAddEngine.ts (1)

207-235: noteBodyIsEmpty should probably treat whitespace-only bodies as empty.
This affects when Templater on-create is suppressed for “frontmatter-only” notes.

- const noteBodyIsEmpty = body.length === 0;
+ const noteBodyIsEmpty = body.trim().length === 0;
src/engine/CaptureChoiceEngine.ts (1)

134-167: Reorder applyCapturePropertyVars before overwriteTemplaterOnce in the whole-file templater path.

When afterCapture === "wholeFile", the property variables captured during formatting should be applied to the file before whole-file templater processing. Currently, the order is reversed: overwriteTemplaterOnce() runs first (line 164), then applyCapturePropertyVars() (line 166). This means templater processes the file without seeing the updated frontmatter. Swap these calls so templater can reference the captured property variables:

- await this.app.vault.modify(file, newFileContent);
- if (this.choice.templater?.afterCapture === "wholeFile") {
-   await overwriteTemplaterOnce(this.app, file);
- }
- await this.applyCapturePropertyVars(file);
+ await this.app.vault.modify(file, newFileContent);
+ await this.applyCapturePropertyVars(file);
+ if (this.choice.templater?.afterCapture === "wholeFile") {
+   await overwriteTemplaterOnce(this.app, file);
+ }
🧹 Nitpick comments (3)
src/types/choices/CaptureChoice.ts (1)

38-40: Defaulting is good; consider normalizing afterCapture when templater exists but is partial.
Right now Load() only fills templater when missing; if persisted data has templater: {} then afterCapture remains undefined (effectively “none”, but not explicit). Optional improvement:

 if (!loaded.templater) {
   loaded.templater = { afterCapture: "none" };
+} else if (!loaded.templater.afterCapture) {
+  loaded.templater.afterCapture = "none";
 }

Also applies to: 76-78, 87-89

src/engine/TemplateEngine.ts (1)

260-268: Confirm desired failure mode if templaterParseTemplate() throws.
Right now a Templater exception aborts the entire append/prepend path. If you want resilience similar to “Templater not installed => no-op”, consider catching and logging inside this block (or ensuring templaterParseTemplate never throws).

src/utilityObsidian.ts (1)

296-299: Consider documenting the magic number for run_mode: 4.

The run_mode: 4 value is passed to Templater's parse_template without explanation. Consider adding a comment documenting what this mode represents (e.g., referencing Templater's internal enum if known) to aid future maintainers.

+	// run_mode 4 = DynamicProcessor mode in Templater (parse without file creation side effects)
 	return await parseTemplate(
 		{ target_file: targetFile, run_mode: 4 },
 		templateContent,
 	);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 25dd585 and 741c3ad.

📒 Files selected for processing (9)
  • src/engine/CaptureChoiceEngine.template-property-types.test.ts (1 hunks)
  • src/engine/CaptureChoiceEngine.ts (5 hunks)
  • src/engine/QuickAddEngine.ts (3 hunks)
  • src/engine/TemplateChoiceEngine.ts (2 hunks)
  • src/engine/TemplateEngine.ts (3 hunks)
  • src/gui/ChoiceBuilder/captureChoiceBuilder.ts (2 hunks)
  • src/types/choices/CaptureChoice.ts (3 hunks)
  • src/types/choices/ICaptureChoice.ts (1 hunks)
  • src/utilityObsidian.ts (4 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
src/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Source code lives in src/: core logic under engine/, services/, and utils/; Svelte UI in src/gui; shared types in src/types; settings entry in src/quickAddSettingsTab.ts

Files:

  • src/engine/CaptureChoiceEngine.template-property-types.test.ts
  • src/gui/ChoiceBuilder/captureChoiceBuilder.ts
  • src/engine/QuickAddEngine.ts
  • src/engine/TemplateChoiceEngine.ts
  • src/types/choices/ICaptureChoice.ts
  • src/types/choices/CaptureChoice.ts
  • src/engine/CaptureChoiceEngine.ts
  • src/utilityObsidian.ts
  • src/engine/TemplateEngine.ts
src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.{ts,tsx}: Biome enforces tab indentation (width 2), LF endings, and an 80-character line guide; align editor settings
Use camelCase for variables and functions
Prefer type-only imports in TypeScript files
Route logging through the logger utilities for consistent output
Structure production code so Obsidian dependencies are injected behind interfaces; unit tests target pure logic and swap in adapters or tests/obsidian-stub.ts

Files:

  • src/engine/CaptureChoiceEngine.template-property-types.test.ts
  • src/gui/ChoiceBuilder/captureChoiceBuilder.ts
  • src/engine/QuickAddEngine.ts
  • src/engine/TemplateChoiceEngine.ts
  • src/types/choices/ICaptureChoice.ts
  • src/types/choices/CaptureChoice.ts
  • src/engine/CaptureChoiceEngine.ts
  • src/utilityObsidian.ts
  • src/engine/TemplateEngine.ts
src/**/*.{ts,tsx,svelte}

📄 CodeRabbit inference engine (AGENTS.md)

Use PascalCase for classes and Svelte components

Files:

  • src/engine/CaptureChoiceEngine.template-property-types.test.ts
  • src/gui/ChoiceBuilder/captureChoiceBuilder.ts
  • src/engine/QuickAddEngine.ts
  • src/engine/TemplateChoiceEngine.ts
  • src/types/choices/ICaptureChoice.ts
  • src/types/choices/CaptureChoice.ts
  • src/engine/CaptureChoiceEngine.ts
  • src/utilityObsidian.ts
  • src/engine/TemplateEngine.ts
🧠 Learnings (2)
📚 Learning: 2025-12-09T21:20:52.398Z
Learnt from: CR
Repo: chhoumann/quickadd PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-09T21:20:52.398Z
Learning: Applies to src/**/*.{ts,tsx} : Structure production code so Obsidian dependencies are injected behind interfaces; unit tests target pure logic and swap in adapters or `tests/obsidian-stub.ts`

Applied to files:

  • src/engine/CaptureChoiceEngine.template-property-types.test.ts
  • src/utilityObsidian.ts
📚 Learning: 2025-12-09T21:20:52.398Z
Learnt from: CR
Repo: chhoumann/quickadd PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-09T21:20:52.398Z
Learning: Applies to tests/**/*.{ts,tsx} : Add regression coverage for bug fixes

Applied to files:

  • src/engine/CaptureChoiceEngine.template-property-types.test.ts
🧬 Code graph analysis (4)
src/engine/QuickAddEngine.ts (2)
src/utils/yamlContext.ts (1)
  • findYamlFrontMatterRange (13-18)
src/utilityObsidian.ts (1)
  • withTemplaterFileCreationSuppressed (93-138)
src/engine/TemplateChoiceEngine.ts (1)
src/utilityObsidian.ts (3)
  • openExistingFileTab (760-785)
  • openFile (677-755)
  • jumpToNextTemplaterCursorIfPossible (302-336)
src/utilityObsidian.ts (2)
tests/obsidian-stub.ts (3)
  • TFile (72-78)
  • App (23-51)
  • WorkspaceLeaf (99-104)
src/utils/errorUtils.ts (1)
  • reportError (99-120)
src/engine/TemplateEngine.ts (2)
tests/obsidian-stub.ts (1)
  • TFile (72-78)
src/utilityObsidian.ts (1)
  • templaterParseTemplate (285-300)
🔇 Additional comments (13)
src/gui/ChoiceBuilder/captureChoiceBuilder.ts (1)

66-67: Nice UX for the legacy/advanced escape hatch, and safe initialization.
The toggle correctly guards this.choice.templater and maps boolean UI to the string union.

Also applies to: 77-92

src/types/choices/ICaptureChoice.ts (1)

43-45: Schema extension looks consistent with the implementation + UI.
Optional nesting preserves backwards compatibility.

src/engine/TemplateEngine.ts (1)

169-176: suppressTemplaterOnCreate wiring looks correct.
Delegating the “only suppress when body empty” decision to QuickAddEngine.createFileWithInput() keeps this call site simple.

src/engine/TemplateChoiceEngine.ts (1)

20-25: Focus-aware “open existing tab” + cursor jump is a solid integration step.
The active-file check inside jumpToNextTemplaterCursorIfPossible should keep this safe when focus === false.

Also applies to: 178-190

src/engine/CaptureChoiceEngine.ts (2)

182-190: Open+focus behavior and cursor-jump hook look correct.
Good use of focus ?? true and guarded cursor advancement.


451-471: New-file capture sequencing (suppression + overwrite vs settle) is reasonable.
The “template => overwrite once” vs “no template + trigger-on-create => wait for settle” split matches the stated race-condition mitigation.

src/utilityObsidian.ts (7)

25-45: LGTM!

The TemplaterPluginLike type provides a well-structured interface for the Templater plugin's surface area. Using optional properties throughout enables safe optional chaining access patterns in the consuming code.


83-91: LGTM!

Clean wrapper functions with proper null handling and optional chaining for safe property access.


140-195: LGTM!

Well-designed file stability detection with clear distinction between grace period (no changes observed) and quiet period (after changes stop). The exponential backoff and timeout bounds are reasonable for the use case.


197-229: LGTM!

The lock mechanism correctly serializes concurrent Templater operations on the same file path using promise chaining. The cleanup logic properly ensures only the final chain in the sequence removes the map entry, preventing premature cleanup.


231-283: LGTM!

Solid implementation with defensive guards, proper locking, and a thoughtful rollback mechanism to prevent partial renders from corrupting files. The skipIfNoTags heuristic using includes("<%") is a reasonable performance optimization.


302-336: LGTM!

Well-structured with proper guards and a thoughtful fallback mechanism. The function correctly tries the direct API first and falls back to command execution only when needed and when auto-jump is enabled in Templater's settings.


760-785: Callers already properly updated — no action needed.

Both CaptureChoiceEngine.ts and TemplateChoiceEngine.ts correctly handle the new return type with if (!openExistingTab) null checks, so the breaking change is already accounted for throughout the codebase.

Comment thread src/engine/CaptureChoiceEngine.template-property-types.test.ts Outdated
Comment thread src/utilityObsidian.ts Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/utilityObsidian.ts (1)

822-847: Update docstring: return type no longer matches comment (“true/false”).

The comment says “return false/true”, but the function now returns WorkspaceLeaf | null. Consider updating the JSDoc to avoid misleading downstream usage.

🧹 Nitpick comments (2)
src/engine/QuickAddEngine.ts (1)

206-230: Good: opt-in Templater suppression on create, but consider path/extension canonicalization.

The wrapper approach is clean and keeps default behavior unchanged. Two small hardening points:

  • filePath.toLowerCase().endsWith(".md") is fine, but if Obsidian ever passes non-canonical paths (case/normalization), suppression keys may miss Templater’s internal files_with_pending_templates entries. Consider normalizing to Obsidian’s canonical TFile.path (when available) before calling withTemplaterFileCreationSuppressed.
  • If you already have MARKDOWN_FILE_EXTENSION_REGEX / extension helpers in this file, using a shared helper avoids divergence.
src/utilityObsidian.ts (1)

201-256: waitForFileToStopChanging: good defaults, but ensure callers can distinguish timeout vs “settled”.

Right now it always returns void, even on timeout. If callers need to know whether it actually observed a quiet period, consider returning a boolean (true = settled, false = timed out) so higher-level flows can decide whether to proceed or add a fallback delay.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 741c3ad and 8e95103.

📒 Files selected for processing (4)
  • src/engine/CaptureChoiceEngine.template-property-types.test.ts (1 hunks)
  • src/engine/QuickAddEngine.ts (3 hunks)
  • src/types/choices/CaptureChoice.ts (3 hunks)
  • src/utilityObsidian.ts (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/types/choices/CaptureChoice.ts
  • src/engine/CaptureChoiceEngine.template-property-types.test.ts
🧰 Additional context used
📓 Path-based instructions (3)
src/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Source code lives in src/: core logic under engine/, services/, and utils/; Svelte UI in src/gui; shared types in src/types; settings entry in src/quickAddSettingsTab.ts

Files:

  • src/engine/QuickAddEngine.ts
  • src/utilityObsidian.ts
src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.{ts,tsx}: Biome enforces tab indentation (width 2), LF endings, and an 80-character line guide; align editor settings
Use camelCase for variables and functions
Prefer type-only imports in TypeScript files
Route logging through the logger utilities for consistent output
Structure production code so Obsidian dependencies are injected behind interfaces; unit tests target pure logic and swap in adapters or tests/obsidian-stub.ts

Files:

  • src/engine/QuickAddEngine.ts
  • src/utilityObsidian.ts
src/**/*.{ts,tsx,svelte}

📄 CodeRabbit inference engine (AGENTS.md)

Use PascalCase for classes and Svelte components

Files:

  • src/engine/QuickAddEngine.ts
  • src/utilityObsidian.ts
🧠 Learnings (1)
📚 Learning: 2025-12-09T21:20:52.398Z
Learnt from: CR
Repo: chhoumann/quickadd PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-09T21:20:52.398Z
Learning: Applies to src/**/*.{ts,tsx} : Structure production code so Obsidian dependencies are injected behind interfaces; unit tests target pure logic and swap in adapters or `tests/obsidian-stub.ts`

Applied to files:

  • src/utilityObsidian.ts
🧬 Code graph analysis (1)
src/engine/QuickAddEngine.ts (1)
src/utilityObsidian.ts (1)
  • withTemplaterFileCreationSuppressed (136-199)
🔇 Additional comments (4)
src/engine/QuickAddEngine.ts (1)

5-5: Import addition looks appropriate.

withTemplaterFileCreationSuppressed is used directly and keeps the create flow readable.

src/utilityObsidian.ts (3)

105-199: Suppression coordination looks solid (active counter + teardown single-flight).

The activeTemplaterFileCreationSuppressions guard plus templaterSuppressionTeardownLock should prevent premature/double teardown while concurrent suppressions are in-flight.


258-290: Per-file lock + rollback in overwriteTemplaterOnce is a big reliability win.

Serializing renders per path and rolling back on render failure should reduce interleaving and partial writes substantially.

Also applies to: 292-344


346-363: Verify Templater internal API assumptions are documented or have fallbacks.

This code relies on Templater internals without clear stability guarantees:

  • parse_template({ run_mode: 4 }, ...) assumes run_mode=4 corresponds to "DynamicProcessor"—unclear if this is a stable public API or an undocumented internal value
  • "templater-obsidian:jump-to-next-cursor-location" command ID appears to be a hardcoded internal command
  • editor_handler.jump_to_next_cursor_location(file, true) signature assumes a specific method exists with this behavior

While the code includes type guards (typeof parseTemplate !== "function") and returns templateContent on fallback, Templater's undocumented internal APIs risk silent failures if the plugin updates. Add comments documenting which Templater versions these are tested against, or consider safer alternatives like delegating to Templater's public command interface rather than direct internal method calls.

Comment thread src/utilityObsidian.ts Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/utilityObsidian.ts (1)

822-847: Update test mocks to return correct type for openExistingFileTab.

Production callers in TemplateChoiceEngine.ts and CaptureChoiceEngine.ts correctly handle the new WorkspaceLeaf | null return type. However, test mocks in TemplateChoiceEngine.notice.test.ts (line 83) and CaptureChoiceEngine.notice.test.ts (line 76) return false instead of null, violating the function's type signature. Update these mocks to return null to match the API. Additionally, the JSDoc comment at line 820 in src/utilityObsidian.ts references the old boolean return type and should be updated.

🧹 Nitpick comments (2)
src/utilityObsidian.ts (2)

181-186: Magic number tied to Templater internals.

The 350ms delay depends on Templater's internal ~300ms check interval. This coupling could break if Templater updates its timing. Consider extracting this to a named constant with documentation about its purpose and the Templater version it was tested against.

+// Templater waits ~300ms before checking files_with_pending_templates in its on-create handler.
+// We hold the entry slightly longer to ensure the bypass is observed.
+// Tested with templater-obsidian v2.x - may need adjustment if Templater internals change.
+const TEMPLATER_PENDING_CHECK_BUFFER_MS = 350;
+
 // ...
-				const minHoldMs = 350;
+				const minHoldMs = TEMPLATER_PENDING_CHECK_BUFFER_MS;

346-362: Consider wrapping parse_template call in try-catch.

The parseTemplate call could throw if Templater encounters invalid syntax or other errors. Currently, this would propagate up and potentially break the calling workflow. Consider catching and logging the error while returning the original template content as a fallback.

-	return await parseTemplate(
-		// `run_mode: 4` maps to Templater's internal `RunMode.DynamicProcessor`.
-		{ target_file: targetFile, run_mode: 4 },
-		templateContent,
-	);
+	try {
+		return await parseTemplate(
+			// `run_mode: 4` maps to Templater's internal `RunMode.DynamicProcessor`.
+			{ target_file: targetFile, run_mode: 4 },
+			templateContent,
+		);
+	} catch (err) {
+		log.logWarning(
+			`templaterParseTemplate: failed to parse template – ${(err as Error).message}`,
+		);
+		return templateContent;
+	}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8e95103 and 0d0de87.

📒 Files selected for processing (1)
  • src/utilityObsidian.ts (4 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
src/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Source code lives in src/: core logic under engine/, services/, and utils/; Svelte UI in src/gui; shared types in src/types; settings entry in src/quickAddSettingsTab.ts

Files:

  • src/utilityObsidian.ts
src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.{ts,tsx}: Biome enforces tab indentation (width 2), LF endings, and an 80-character line guide; align editor settings
Use camelCase for variables and functions
Prefer type-only imports in TypeScript files
Route logging through the logger utilities for consistent output
Structure production code so Obsidian dependencies are injected behind interfaces; unit tests target pure logic and swap in adapters or tests/obsidian-stub.ts

Files:

  • src/utilityObsidian.ts
src/**/*.{ts,tsx,svelte}

📄 CodeRabbit inference engine (AGENTS.md)

Use PascalCase for classes and Svelte components

Files:

  • src/utilityObsidian.ts
🧠 Learnings (1)
📚 Learning: 2025-12-09T21:20:52.398Z
Learnt from: CR
Repo: chhoumann/quickadd PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-09T21:20:52.398Z
Learning: Applies to src/**/*.{ts,tsx} : Structure production code so Obsidian dependencies are injected behind interfaces; unit tests target pure logic and swap in adapters or `tests/obsidian-stub.ts`

Applied to files:

  • src/utilityObsidian.ts
🧬 Code graph analysis (1)
src/utilityObsidian.ts (3)
tests/obsidian-stub.ts (3)
  • TFile (72-78)
  • App (23-51)
  • WorkspaceLeaf (99-104)
docs/static/scripts/migrateDataviewToFrontmatter.js (1)
  • content (88-88)
src/utils/errorUtils.ts (1)
  • reportError (99-120)
🔇 Additional comments (6)
src/utilityObsidian.ts (6)

25-45: LGTM! Past review concern addressed.

The TemplaterPluginLike type is now properly exported, resolving the previous concern about TypeScript declaration emit failing when an exported function returns a non-exported type.


83-91: LGTM!

Clean accessor functions for Templater plugin state. The double cast pattern (as unknown as TemplaterPluginLike) is appropriate for duck-typing external plugin APIs where the actual type isn't available.


201-256: LGTM!

The file stability detection logic is well-designed with:

  • Adaptive polling with exponential backoff to reduce I/O
  • Separate handling for "no changes observed" (grace period) vs "changes stopped" (quiet period)
  • Proper error handling with graceful fallback

258-290: LGTM!

The per-file serialization lock is correctly implemented:

  • Promise chaining ensures sequential execution per path
  • Cleanup correctly checks identity before deletion to avoid race conditions
  • Error handling prevents unhandled rejections while still propagating errors from fn()

320-322: Heuristic tag detection may have false positives.

The original.includes("<%") check could match <% appearing in code blocks, inline code, or string literals, not just Templater syntax. This is likely acceptable as a conservative heuristic (better to run Templater unnecessarily than skip when needed), but worth noting if performance becomes a concern.


364-398: LGTM!

The cursor jump implementation correctly:

  • Guards against non-MD files and inactive files
  • Attempts the plugin API first with error handling
  • Falls back to command execution only when the setting is enabled
  • Fails silently as appropriate for a best-effort "if possible" function

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0d0de87 and baef32a.

📒 Files selected for processing (3)
  • src/engine/CaptureChoiceEngine.notice.test.ts (1 hunks)
  • src/engine/TemplateChoiceEngine.notice.test.ts (1 hunks)
  • src/utilityObsidian.ts (4 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
src/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Source code lives in src/: core logic under engine/, services/, and utils/; Svelte UI in src/gui; shared types in src/types; settings entry in src/quickAddSettingsTab.ts

Files:

  • src/engine/CaptureChoiceEngine.notice.test.ts
  • src/engine/TemplateChoiceEngine.notice.test.ts
  • src/utilityObsidian.ts
src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.{ts,tsx}: Biome enforces tab indentation (width 2), LF endings, and an 80-character line guide; align editor settings
Use camelCase for variables and functions
Prefer type-only imports in TypeScript files
Route logging through the logger utilities for consistent output
Structure production code so Obsidian dependencies are injected behind interfaces; unit tests target pure logic and swap in adapters or tests/obsidian-stub.ts

Files:

  • src/engine/CaptureChoiceEngine.notice.test.ts
  • src/engine/TemplateChoiceEngine.notice.test.ts
  • src/utilityObsidian.ts
src/**/*.{ts,tsx,svelte}

📄 CodeRabbit inference engine (AGENTS.md)

Use PascalCase for classes and Svelte components

Files:

  • src/engine/CaptureChoiceEngine.notice.test.ts
  • src/engine/TemplateChoiceEngine.notice.test.ts
  • src/utilityObsidian.ts
🧠 Learnings (2)
📚 Learning: 2025-12-09T21:20:52.398Z
Learnt from: CR
Repo: chhoumann/quickadd PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-09T21:20:52.398Z
Learning: Applies to tests/**/*.{ts,tsx} : Add regression coverage for bug fixes

Applied to files:

  • src/engine/CaptureChoiceEngine.notice.test.ts
  • src/engine/TemplateChoiceEngine.notice.test.ts
📚 Learning: 2025-12-09T21:20:52.398Z
Learnt from: CR
Repo: chhoumann/quickadd PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-09T21:20:52.398Z
Learning: Applies to src/**/*.{ts,tsx} : Structure production code so Obsidian dependencies are injected behind interfaces; unit tests target pure logic and swap in adapters or `tests/obsidian-stub.ts`

Applied to files:

  • src/engine/CaptureChoiceEngine.notice.test.ts
  • src/engine/TemplateChoiceEngine.notice.test.ts
  • src/utilityObsidian.ts
🧬 Code graph analysis (1)
src/utilityObsidian.ts (2)
tests/obsidian-stub.ts (2)
  • App (23-51)
  • WorkspaceLeaf (99-104)
src/utils/errorUtils.ts (1)
  • reportError (99-120)
🔇 Additional comments (11)
src/engine/CaptureChoiceEngine.notice.test.ts (2)

68-81: Mock return type aligns with new openExistingFileTab contract.

Returning null (vs false) matches the updated openExistingFileTab(...): WorkspaceLeaf | null signature and avoids truthiness ambiguity in callers.


83-86: three-way-merge mock: __esModule: true is the right interop fix.

src/utilityObsidian.ts (8)

25-45: Good: explicit TemplaterPluginLike shape keeps integration type-safe.


83-91: Good: getTemplaterPlugin null-guards + isTemplaterTriggerOnCreateEnabled are clean.


206-261: waitForFileToStopChanging polling/backoff strategy is reasonable (grace-period vs quiet-period is a nice touch).


263-295: Per-path single-flight lock is a good fit for Templater “whole file” serialization.


297-349: Rollback-on-failure + lock + settle makes overwriteTemplaterOnce much safer.


824-853: openExistingFileTab(..., focus?): WorkspaceLeaf | null is a nice API improvement (and matches the test mocks).


351-367: templaterParseTemplate: run_mode: 4 correctly maps to Templater's RunMode.DynamicProcessor.
The mapping is accurate and requires no changes.


93-204: Reliance on undocumented Templater internals requires version pinning and testing.

The code depends on files_with_pending_templates (undocumented internal Set) and functions_generator.teardown() (not a documented Templater API). While the "templater:all-templates-executed" event is documented, the overall suppression strategy relies on Templater internals without a stable public API contract. Pin the templater-obsidian version and add integration tests to catch breaking changes in future releases.

src/engine/TemplateChoiceEngine.notice.test.ts (1)

78-85: Mock updated correctly for openExistingFileTab(...): WorkspaceLeaf | null.

Comment thread src/utilityObsidian.ts
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (5)
src/utilityObsidian.templater-binding.test.ts (2)

1-4: Consider moving this test to tests/ and using tests/obsidian-stub.ts instead of importing obsidian.
Importing real obsidian classes in unit tests can be brittle outside Obsidian, and the repo learnings indicate using the stubs + keeping tests under tests/. Based on learnings, use tests/obsidian-stub.ts and relocate this file accordingly.

Also applies to: 5-29, 31-58


14-21: Strengthen the assertions by verifying the arguments too (not just this).
Right now the tests only validate the bound this context; consider also asserting the opts object includes target_file === file and run_mode === 4, and that jump_to_next_cursor_location receives (file, true) to lock in the contract.

Also applies to: 42-49

src/engine/CaptureChoiceEngine.ts (3)

142-160: Avoid variable shadowing of content inside the editor-insert branch.
This block introduces a new const content = ... which shadows the earlier capture content variable in run(). Renaming to renderedCaptureContent (or similar) would reduce confusion.


142-149: Decide whether templaterParseTemplate() failures should fall back to raw insertion.
Currently, if Templater throws while parsing the snippet, the whole capture run will error. If the intended UX is “capture still succeeds but without templater expansion,” wrap parsing in try/catch and insert captureContent on failure (and log/report).


181-190: Cursor-jump call is fine; consider gating it on focus to avoid a no-op call.
If focus === false, jumpToNextTemplaterCursorIfPossible() will early-return because the file won’t be active; you could skip calling it in that case.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between baef32a and 63fe23d.

📒 Files selected for processing (3)
  • src/engine/CaptureChoiceEngine.ts (5 hunks)
  • src/utilityObsidian.templater-binding.test.ts (1 hunks)
  • src/utilityObsidian.ts (4 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
src/**/*.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

Source code lives in src/: core logic under engine/, services/, and utils/; Svelte UI in src/gui; shared types in src/types; settings entry in src/quickAddSettingsTab.ts

Files:

  • src/utilityObsidian.templater-binding.test.ts
  • src/engine/CaptureChoiceEngine.ts
  • src/utilityObsidian.ts
src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

src/**/*.{ts,tsx}: Biome enforces tab indentation (width 2), LF endings, and an 80-character line guide; align editor settings
Use camelCase for variables and functions
Prefer type-only imports in TypeScript files
Route logging through the logger utilities for consistent output
Structure production code so Obsidian dependencies are injected behind interfaces; unit tests target pure logic and swap in adapters or tests/obsidian-stub.ts

Files:

  • src/utilityObsidian.templater-binding.test.ts
  • src/engine/CaptureChoiceEngine.ts
  • src/utilityObsidian.ts
src/**/*.{ts,tsx,svelte}

📄 CodeRabbit inference engine (AGENTS.md)

Use PascalCase for classes and Svelte components

Files:

  • src/utilityObsidian.templater-binding.test.ts
  • src/engine/CaptureChoiceEngine.ts
  • src/utilityObsidian.ts
🧠 Learnings (5)
📚 Learning: 2025-12-09T21:20:52.398Z
Learnt from: CR
Repo: chhoumann/quickadd PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-09T21:20:52.398Z
Learning: Applies to src/**/*.{ts,tsx} : Structure production code so Obsidian dependencies are injected behind interfaces; unit tests target pure logic and swap in adapters or `tests/obsidian-stub.ts`

Applied to files:

  • src/utilityObsidian.templater-binding.test.ts
  • src/utilityObsidian.ts
📚 Learning: 2025-12-09T21:20:52.398Z
Learnt from: CR
Repo: chhoumann/quickadd PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-09T21:20:52.398Z
Learning: Applies to tests/**/*.{ts,tsx} : Add regression coverage for bug fixes

Applied to files:

  • src/utilityObsidian.templater-binding.test.ts
📚 Learning: 2025-12-09T21:20:52.398Z
Learnt from: CR
Repo: chhoumann/quickadd PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-09T21:20:52.398Z
Learning: Applies to tests/**/*.{ts,tsx} : Co-locate specs with their source or group them under `tests/feature-name`

Applied to files:

  • src/utilityObsidian.templater-binding.test.ts
📚 Learning: 2025-12-09T21:20:52.398Z
Learnt from: CR
Repo: chhoumann/quickadd PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-09T21:20:52.398Z
Learning: Applies to tests/**/*.{ts,tsx,js,jsx} : Place tests and stubs in `tests/` directory

Applied to files:

  • src/utilityObsidian.templater-binding.test.ts
📚 Learning: 2025-12-09T21:20:52.398Z
Learnt from: CR
Repo: chhoumann/quickadd PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-09T21:20:52.398Z
Learning: Applies to tests/**/*.test.{ts,tsx} : Use Testing Library helpers for Svelte components

Applied to files:

  • src/utilityObsidian.templater-binding.test.ts
🧬 Code graph analysis (3)
src/utilityObsidian.templater-binding.test.ts (2)
tests/obsidian-stub.ts (2)
  • App (23-51)
  • TFile (72-78)
src/utilityObsidian.ts (2)
  • templaterParseTemplate (389-409)
  • jumpToNextTemplaterCursorIfPossible (411-448)
src/engine/CaptureChoiceEngine.ts (1)
src/utilityObsidian.ts (6)
  • overwriteTemplaterOnce (333-387)
  • openExistingFileTab (873-898)
  • openFile (789-867)
  • jumpToNextTemplaterCursorIfPossible (411-448)
  • isTemplaterTriggerOnCreateEnabled (90-92)
  • waitForTemplaterTriggerOnCreateToComplete (94-127)
src/utilityObsidian.ts (3)
tests/obsidian-stub.ts (3)
  • TFile (72-78)
  • App (23-51)
  • WorkspaceLeaf (99-104)
docs/static/scripts/migrateDataviewToFrontmatter.js (1)
  • content (88-88)
src/utils/errorUtils.ts (1)
  • reportError (99-120)
🔇 Additional comments (9)
src/engine/CaptureChoiceEngine.ts (3)

134-167: Nice: snippet-only Templater parsing for editor insert actions matches the determinism goal.
The templaterParseTemplate() call before appendToCurrentLine / insertOnNewLine* cleanly avoids unintended whole-file side effects.


161-167: Good: whole-file Templater is now explicitly opt-in via templater.afterCapture === "wholeFile".
This makes the side effect controllable and keeps default behavior deterministic.


451-471: Good: suppression + “wait for trigger-on-create completion” reduces on-create races.
Using suppressTemplaterOnCreate when QuickAdd is creating from a template (and otherwise waiting for trigger-on-create) fits the PR’s “avoid interleaving writes” objective.

src/utilityObsidian.ts (6)

25-46: TemplaterPluginLike + getTemplaterPlugin() are a solid boundary for safe integration.
This makes the rest of the codebase much less error-prone around optional plugin presence.

Also applies to: 84-92


94-127: Good fallback strategy in waitForTemplaterTriggerOnCreateToComplete() (Set-driven when available, mtime-driven otherwise).
This is pragmatic for differing Templater internals / versions.


299-331: Per-file single-flight in withTemplaterFileLock() is a good fit for preventing overlapping whole-file renders.
The lock chaining + cleanup looks correct for async concurrency and prevents interleaved overwriteTemplaterOnce() calls per path.

Also applies to: 333-387


411-448: jumpToNextTemplaterCursorIfPossible() behavior and binding look right (API-first with guarded fallback).
Nice use of auto_jump_to_cursor gating + .call(editorHandler, ...) preservation.


869-898: API improvement: openExistingFileTab() returning WorkspaceLeaf | null + optional focusing is clearer than boolean.
This should make call sites (like Capture/Template choice engines) more explicit about what they got back.


141-177: Reconsider the timing assumption: Templater checks files_with_pending_templates on create-file events, not on a ~300ms timer.

The TEMPLATER_PENDING_CHECK_BUFFER_MS = 350 constant assumes Templater checks files_with_pending_templates ~300ms after file creation and is held slightly longer to ensure the bypass is observed. However, Templater's actual behavior is event-driven: it checks files_with_pending_templates immediately when Obsidian emits the create-file event, not after a delay. The 350ms buffer may not be necessary and could mask the real suppression mechanism.

The workspace event templater:all-templates-executed and the functions_generator.teardown() call are documented and correct approaches for signaling and cleanup, but the timing strategy should be re-evaluated to ensure it aligns with Templater's event-driven architecture rather than an assumed timer-based check.

Also applies to: 179-240

Comment thread src/utilityObsidian.ts
@chhoumann chhoumann merged commit 83c0813 into master Dec 14, 2025
4 checks passed
@chhoumann chhoumann deleted the fix/templater-deterministic-render branch December 14, 2025 17:12
@github-actions
Copy link
Copy Markdown

🎉 This PR is included in version 2.9.2 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

@github-actions
Copy link
Copy Markdown

🚀 Release has been published: v2.9.2

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant