Skip to content

fix(muya): insertParagraph immediate-block anchoring in nested blocks (PG13) - #4408

Merged
Jocs merged 1 commit into
developfrom
fix/muya-insert-paragraph-anchor
Jun 8, 2026
Merged

Jocs merged 1 commit into
developfrom
fix/muya-insert-paragraph-anchor

Conversation

@Jocs

@Jocs Jocs commented Jun 8, 2026

Copy link
Copy Markdown
Member

Summary

Follow-up to #4406 (desktop migration to @muyajs/core). Flips parity scoreboard gap PG13 from #4407.

The new engine's insertParagraph(location, text) always resolved _outmostBlockAtCursor(), so when the cursor sat inside a nested list item / blockquote the new paragraph was inserted adjacent to the outermost container instead of as an inner sibling next to the immediate block. The legacy packages/muyajs insertParagraph(location, text, outMost=false) took a 3rd outMost flag and, when false (the context-menu "Insert Paragraph Before/After" path), anchored at the immediate parent block (getAnchor/getParent).

Change

  • insertParagraph(location, text, outMost = false) — re-adds the third outMost flag.
    • outMost === false (default): anchor to the immediate block-level parent of the active content leaf (_immediateBlockAtCursor() → content.parent), so a new paragraph stays as an inner sibling inside the list item / blockquote.
    • outMost === true: keep the existing outermost-container behaviour used by "Create Paragraph Below" (editor.vue already calls insertParagraph('after', '', true)).
  • Top-level paragraphs are unaffected — the immediate parent and the outermost block coincide there.

Tests

  • parityInsertParagraphNested.spec.ts (PG13 ×2): it.fails → it, now passing (nested list stays nested; blockquote sibling stays inside the quote).
  • blockEditing.spec.ts: added a regression locking the outMost=true path (cursor in a blockquote + outMost=true inserts after the whole quote at document root).
  • Full muya suite green: lint (0 errors), lint:types, check-circular, test (515 pass / 18 expected-fail), test:spec (1347 pass). lint:css has a pre-existing failure on develop unrelated to this change (separate branch handles it).

Wave-2 desktop note

This is engine-only (packages/muya); desktop editor.vue is untouched. For full parity, the wave-2 desktop context-menu "Insert Paragraph Before/After" path (handleInsertParagraph → insertParagraph(location)) must pass the non-outmost anchor — i.e. call insertParagraph(location, '', false) (or rely on the new false default). The "Create Paragraph Below" caller must keep passing true.

Closes parity gap PG13.

🤖 Generated with Claude Code

… (PG13)

Restore the legacy immediate-anchor capability for `insertParagraph`. The
engine method always resolved `_outmostBlockAtCursor()`, so inside a nested
list item / blockquote the new paragraph landed adjacent to the OUTERMOST
container instead of as an inner sibling next to the immediate block.

Add a third `outMost` flag (default `false`): when false, anchor to the
IMMEDIATE block-level parent of the active content leaf (mirrors the legacy
`getAnchor`/`getParent` path used by the context-menu "Insert Paragraph
Before/After" action); when true, keep the outermost-container behaviour used
by "Create Paragraph Below" (which passes `outMost=true` explicitly). Top-level
paragraphs are unaffected since the immediate parent and outermost block
coincide.

Flips parity scoreboard PG13 (parityInsertParagraphNested.spec.ts, 2 specs
`it.fails` -> `it`) and adds a blockEditing.spec.ts regression locking the
`outMost=true` path.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Copilot AI review requested due to automatic review settings June 8, 2026 17:26

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR restores legacy insertParagraph anchoring behavior in @muyajs/core when the cursor is inside nested structures (e.g., list items, blockquotes), closing parity gap PG13 from the migration follow-ups.

Changes:

  • Re-introduces insertParagraph(location, text, outMost=false) to choose between immediate-block anchoring (default) and outermost-container anchoring (outMost=true).
  • Adds _immediateBlockAtCursor() to support the new default anchoring mode.
  • Updates/extends Vitest coverage by flipping the PG13 parity spec to passing and adding a regression test for the outMost=true behavior.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
packages/muya/src/muya.ts Adds immediate-vs-outmost anchoring option to insertParagraph.
packages/muya/src/tests/parityInsertParagraphNested.spec.ts Flips PG13 from expected-fail to passing for nested list/blockquote insertion.
packages/muya/src/tests/blockEditing.spec.ts Adds regression coverage for outMost=true anchoring in nested structures.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/muya/src/muya.ts
Comment on lines +478 to +482
private _immediateBlockAtCursor(): Parent | null {
const content = this.editor.activeContentBlock ?? this.editor.selection.anchorBlock;

return content?.parent ?? null;
}
@Jocs
Jocs merged commit 267d422 into develop Jun 8, 2026
8 checks passed
Jocs added a commit that referenced this pull request Jun 8, 2026
… heading-link, source cursor, saved indicator) (#4415)

* fix(desktop): map copyAsRich to the engine copyAsRich method (PG9)

The legacy "Copy as Rich Text" command was remapped to copyAsHtml, which
blanks text/html and puts the HTML source into text/plain, so pasting into
Word/email yielded raw HTML markup as literal text. #4411 added a real
Muya.copyAsRich() that writes rendered HTML to text/html and plain text to
text/plain; point COPY_PASTE_METHOD_MAP.copyAsRich at it.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* feat(desktop): wire heading-copy-link to copyGithubSlug (PG11)

#4414 made the engine attach a hover-to-copy affordance to every heading and
emit heading-copy-link { key } (key == the heading's stable slug) on click.
Re-subscribe in editor.vue and forward the key to editorStore.copyGithubSlug,
which copies `#<githubSlug>` to the clipboard — restoring the heading-anchor
copy affordance that was a documented gap after the @muyajs/core migration.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* fix(desktop): match exported-TOC anchors to engine heading ids (PG8)

editor.vue now exports via @muyajs/core (#4406), and #4412 injects
github-compatible heading ids onto exported headings (deduped in document
order with a `-N` suffix). getHtmlToc still slugged via the legacy muyajs
Slugger, so `href="#slug"` targets no longer matched the injected ids and TOC
/ [TOC] links were dead. Swap to @muyajs/core's generateGithubSlug and
replicate the engine's whole-document `-N` dedup so the anchors resolve.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* fix(desktop): consume selection-change affiliation for menu state (PG1)

adaptSelectionChange hardcoded affiliation:[] and copied changes.type
('Caret'/'Range') into start.type/end.type, so createApplicationMenuState's
`start.type === 'span'` and functionType guards never fired — the native
Paragraph-menu check marks, loose/task-list toggles, table/code-fence
detection, and Format-disable-in-code all went dead after the @muyajs/core
migration.

#4410 added an `affiliation` chain (outermost-first) plus per-endpoint
`anchorBlockInfo`/`focusBlockInfo` (`type: 'span'` + `functionType`) to the
selection-change payload. Map them onto the legacy shape:
  - start/end `.type` and `.block.functionType` from the leaf block info,
  - affiliation passed through, with a derived `functionType` surfaced on
    `pre`/`figure` containers so table / code-fence detection lights up.

Also fix the consumer's loose-list read: the engine affiliation entry carries
`isLooseListItem` on the list block directly, not via a `children` chain.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* fix(desktop): restore saved indicator on undo-to-disk (PG15)

makeSyntheticHistory minted an ever-incrementing editSeq id on every
json-change (including undo/redo), so after edit-then-undo-to-saved the id
never matched lastSavedHistoryId and the tab stayed marked dirty even when its
content matched disk.

Derive the synthetic id from the engine undo-stack DEPTH instead — a stable
position marker that returns to its saved value when an edit is undone back to
the baseline. Seed lastSavedHistoryId to 0 (the engine's post-setContent
baseline depth, since setContent clears history) so a freshly-loaded,
never-saved document clears its dirty indicator when undone back to disk
content, mirroring the legacy history-index behaviour.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* feat(muya): add setCursorByOffset for source-mode cursor restore (PG2)

The source-code -> WYSIWYG handoff carries only a CodeMirror {line, ch} index
cursor; @muyajs/core had no index-offset -> block-path conversion, so the
WYSIWYG caret was lost. Add Muya#setCursorByOffset, reproducing the legacy
muyajs approach: inject sentinel strings into the current markdown at the
line/ch offsets, rebuild the tree (sentinels embed as literal text), find the
content blocks they landed in, then rebuild the clean document and set the
cursor by the resolved block paths + offsets. Both setContent calls run
synchronously so no intermediate paint occurs, and the method is a no-op for
stale/unresolvable cursors.

Engine helper only — desktop wiring lands in a separate commit.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* fix(desktop): restore WYSIWYG caret after source-mode edit (PG2)

handleFileChange dropped the saved muyaIndexCursor on the source-code ->
WYSIWYG handoff, so the caret was lost. Consume the new engine
Muya#setCursorByOffset: when the tab has no key-based cursor but carries a
CodeMirror {line, ch} index cursor, map it onto a block-key cursor so the
caret lands where the source-mode cursor was. Restore the per-tab engine
history afterwards (setCursorByOffset re-runs setContent internally, which
clears history).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* test(desktop): flip parity e2e for PG1/PG2/PG15, defer PG14

Remove the `test.fail()` markers on the PG1 (Paragraph-menu check mark), PG2
(source-mode caret restore), and PG15 (saved indicator on undo-to-disk) parity
e2e tests now that the desktop wiring lands — all three genuinely pass.

PG14 (first undo after source mode reverts the bulk edit in one step) stays
`test.fail()`: recording the source-mode change as a single undo boundary needs
a general whole-document json1 diff through Editor.updateContents' pick/drop
walker, which only handles specific op shapes and would risk corrupting the
document. Deferred with an explanatory note here and in handleFileChange.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* docs(test): reconcile parity scoreboard after wave-2 (14/15 fixed)

Update the Status column to the true post-merge state: all seven Wave-1 engine
PRs (#4408-#4414) plus the Wave-2 desktop wiring close PG1-PG13 and PG15. Fix
the "Gaps remaining" count (11 -> 1), add the PG2 setCursorByOffset engine spec,
and document why PG14 is accept-deferred (single-undo-boundary across the
source-mode handoff needs a whole-document json1 diff the op walker can't safely
apply).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

* fix: address Copilot review on parity wave-2

- editor.vue adaptSelectionChange: restore start/end block.text from the live
  anchorBlock/focusBlock so SELECTION_CHANGE can still slice the selected text
  (search prefill); the previous {functionType}-only block dropped it.
- editor.vue isIndexCursor: validate both line AND ch are numbers (factored out
  isIndexPosition) so a missing ch no longer silently clamps to column 0.
- muya setCursorByOffset: snapshot getHistory() and restore it after the
  internal setContent rebuild so the public API is caret-only and does not clear
  the undo stack; document the behaviour and cover it with a test.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
@Jocs
Jocs deleted the fix/muya-insert-paragraph-anchor branch June 10, 2026 07:06
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.

2 participants