Skip to content

fix(splitOnHeading): await async note creation + include heading in replace search - #139

Open
Milofax wants to merge 2 commits into
lynchjames:mainfrom
Milofax:fix/split-on-heading-race-condition
Open

fix(splitOnHeading): await async note creation + include heading in replace search#139
Milofax wants to merge 2 commits into
lynchjames:mainfrom
Milofax:fix/split-on-heading-race-condition

Conversation

@Milofax

@Milofax Milofax commented Apr 28, 2026

Copy link
Copy Markdown

Summary

When running Split note by headings - H{1,2,3} on a note with multiple matching headings, the content of the heading sections remains in the original note while also being copied into the new per-heading notes — i.e. content is duplicated instead of moved.

According to the README, the expected behaviour is that the heading sections are moved out of the original (replaced by links to the new notes). The bug surfaces consistently when Refactored Note Template is configured (extra async calls widen the race window), but the underlying race exists unconditionally.

Root cause

splitOnHeading() iterated over sections with forEach, not awaiting the async createNoteWithFirstLineAsFileName() calls:

headingNotes.forEach((hn, i) =>
  this.createNoteWithFirstLineAsFileName(dedupedFileNames[i], hn, mdView, doc, 'replace-headings', true)
);

Each invocation performs a read–modify–write on the document via doc.setValue(doc.getValue().replace(originalContent, contentToInsert)) (src/doc.ts, replaceContent() for mode === 'replace-headings'). Running the calls concurrently means each one reads the document at roughly the same time and the writes overwrite each other — only the last write survives, so all other sections' content is left untouched in the original note.

Fix

Replace the parallel forEach with a sequential for + await so each replaceContent sees the document state produced by the previous one:

for (let i = 0; i < headingNotes.length; i++) {
  await this.createNoteWithFirstLineAsFileName(dedupedFileNames[i], headingNotes[i], mdView, doc, 'replace-headings', true);
}

No API changes, no behaviour change for users beyond the fix.

Test plan

  • Built the plugin (npm run build) and verified the compiled main.js now contains a sequential yield-based loop instead of a parallel forEach.
  • Manual smoke test in a vault with a note containing 3+ H2 sections + a non-empty Refactored Note Template: trigger Split note by headings - H2 → confirm the original note now contains only the link to each new note (not the original section bodies) and each new note contains exactly its respective section.
  • Same smoke test with empty Refactored Note Template — confirm still works.

🤖 Generated with Claude Code

Milofax and others added 2 commits April 28, 2026 16:31
splitOnHeading() iterated over heading sections with forEach without
awaiting createNoteWithFirstLineAsFileName(). Each invocation performs a
read-modify-write on the document via:

  doc.setValue(doc.getValue().replace(originalContent, contentToInsert))

When all calls fired concurrently, each read the document state at
roughly the same time and the writes overwrote each other — only the
last write survived. The result: content of all other heading sections
remained in the original note, leading to duplication (content present
both in the new per-heading notes and in the original).

Replacing forEach with a for loop and awaiting each call serialises the
read-modify-write cycle, so each replacement sees the document state
produced by the previous one.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…t wrong-line replacement

When excludeFirstLineInNote is enabled (default in many setups), noteContent()
returns the section body without the heading line. Passing only that body as
the search string to replaceContent (which does
doc.setValue(doc.getValue().replace(originalContent, link))) causes
String.prototype.replace to find the first occurrence of the body text
anywhere in the document — including inside a subsequent heading line.

Concrete failure observed in user vault with a German short-text note:

  Source:
    ## Test1
    Test 1

    ## Test 2
    Test 2

  After 'Split note by headings - H2', the second iteration searched for
  "Test 2" and matched the heading "## Test 2" before the body, producing:

    ## Test1
    [[Test1]]
    ## [[Test 2]]   <-- heading corrupted, body left in place
    Test 2

For 'replace-headings' mode, build the search string from the full
selectedContent (heading + body) so the replace anchors on the entire section
block. Other modes are unaffected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Milofax Milofax changed the title fix(splitOnHeading): await async note creation to prevent race condition fix(splitOnHeading): await async note creation + include heading in replace search Apr 28, 2026
@Milofax

Milofax commented Apr 28, 2026

Copy link
Copy Markdown
Author

Added a second commit that fixes a related issue surfaced once the race condition is gone:

When excludeFirstLineInNote is enabled, noteContent() returns the section body without the heading line. Passing that as the search string to replaceContent() caused String.prototype.replace to match the body text inside a later heading line (e.g. searching for "Test 2" matched ## Test 2 before the body), corrupting that heading instead of replacing the intended section.

The fix: for replace-headings mode, build the search string from selectedContent (heading + body) so the replace anchors on the full section block. Other modes are unaffected.

🤖 Generated with Claude Code

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.

1 participant