Repository navigation
feat(muya): expose block affiliation in selection-change (PG1) - #4410
Conversation
The `selection-change` payload only carried flat caret/range info, so the desktop Paragraph/Format menu state builder (`createApplicationMenuState`) could not reconstruct block context: no ancestor `affiliation` chain, and the engine blocks expose `blockName` rather than muyajs's `type` / `functionType`. As a result the Paragraph-menu check marks, Loose/Task-list toggles, table / code-fence detection and the in-code Format-menu disable were all dead. Re-derive the legacy `selectionChange` block-context from the @muyajs/core block tree in a focused `selection/affiliation.ts` helper and add three fields to the `selection-change` payload (existing fields untouched): - `affiliation` — shared ancestor PARAGRAPH-type chain (outermost-first), each entry carrying the markdown `type` (`p`, `h1`…`h6`, `ul`, `ol`, `li`, `pre`, `figure`, `blockquote`) plus list context (`listType`, `listItemType`, `isLooseListItem`). - `anchorBlockInfo` / `focusBlockInfo` — per-endpoint content-leaf info: `type` (always `span`) + `functionType` (`codeContent`, `cellContent`, `languageInput`, `paragraphContent`). Flips parity scoreboard PG1: the two `it.fails` PG1 tests are now real passing tests, plus two new tests pin the per-endpoint code-content info and list affiliation context. Desktop adapter consumption is wave 2. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
There was a problem hiding this comment.
Pull request overview
This PR closes parity gap PG1 by extending the muya engine’s selection-change event payload with legacy-compatible block-context information (ancestor affiliation chain + per-endpoint leaf info), enabling downstream desktop menu-state reconstruction in a follow-up PR.
Changes:
- Adds
affiliation,anchorBlockInfo, andfocusBlockInfoto theselection-changepayload. - Introduces a new
selection/affiliation.tshelper to derive legacy-like affiliation/endpoint metadata from the@muyajs/coreblock tree. - Flips PG1 parity tests from
it.failsto passingit()and adds targeted assertions for the new fields.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| packages/muya/src/selection/index.ts | Emits new selection payload fields (affiliation, anchorBlockInfo, focusBlockInfo). |
| packages/muya/src/selection/affiliation.ts | Derives legacy-style affiliation chain + per-endpoint content-leaf info from the block tree. |
| packages/muya/src/selection/tests/paritySelectionChange.spec.ts | Updates PG1 tests to assert the new payload shape and behaviors. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Lists carry `meta.loose`; a loose list item is one inside a loose list. | ||
| const meta = (block as Parent & { meta?: { loose?: boolean } }).meta; | ||
|
|
||
| return Boolean(meta?.loose); | ||
| } |
There was a problem hiding this comment.
Good catch — verified: list-item / task-list-item carry no meta, so meta.loose was always undefined and isLooseListItem was stuck at false for li entries. Fixed in d8cbbc4: li entries now read meta.loose from their enclosing list block (_parentListOf), and a new test asserts isLooseListItem === true on both the ul and li entries of a loose list.
| const listType = LIST_TYPE_BY_NAME[block.blockName]; | ||
| if (listType) { | ||
| if (type === 'li') | ||
| entry.listItemType = listType; | ||
| else | ||
| entry.listType = listType; | ||
| } |
There was a problem hiding this comment.
Correct — both bullet and ordered lists share the list-item block, so mapping list-item → bullet misclassified ordered-list items. Fixed in d8cbbc4: an li's listItemType is now derived from its parent list block (bullet-list/order-list/task-list → bullet/order/task), with a new test asserting an ordered item reports listItemType === 'order'.
…(PG1) Address Copilot review on #4410: - `isLooseListItem` was read from a list-item block's own `meta.loose`, but `list-item` / `task-list-item` carry no `meta` — the loose/tight flag lives on the parent list (`bullet-list` / `order-list` / `task-list`). It was therefore always `false` for `li` entries, so the desktop "Loose list item" state could never enable. - `listItemType` for an `li` was mapped from the item's own `blockName` (`list-item` → `bullet`), but both bullet and ordered lists use the same `list-item` block, so ordered-list items were misclassified as `bullet`. Walk from a list-item up to its enclosing list block and read both the discriminator (`bullet` | `order` | `task`) and `meta.loose` from there. Add tests pinning ordered-list-item classification and loose-list detection on both the list and item entries. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
… 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]>
What
Parity follow-up to #4406 (@muyajs/core migration). Flips parity scoreboard gap PG1 from the failing
it.failsscoreboard (#4407) to real passing tests.The engine
selection-changepayload only carried flat caret/range info (anchor,focus,anchorBlock,anchorPath, …,type,formats). The desktop application-menu state builder (createApplicationMenuState) was written against muyajs's richerselectionChangeshape and needs:affiliationchain to light up the Paragraph-menu check marks and the Loose/Task-list toggles, andtype+functionTypeto setisCodeFences/isCodeContent/isTableand disable the Format menu inside code.The new engine exposes
blockName(codeblock.content,bullet-list, …), not muyajs's markdowntype/functionType, so none of that downstream state could be reconstructed — Paragraph check marks never lit, Loose-list was always disabled, and the Format menu was never disabled in code.How
ENGINE-ONLY, in
packages/muya. A focusedselection/affiliation.tshelper re-derives the legacy block-context from the @muyajs/core block tree, and three fields are added to theselection-changepayload (all existing fields untouched, new ones JSDoc'd):affiliation— shared ancestor PARAGRAPH-type chain (outermost-first), each entry:{ type, blockName, listType?, listItemType?, isLooseListItem? }wheretypeis the markdown block type (p,h1…h6,ul,ol,li,pre,figure,blockquote). Cross-block selections are trimmed to the shared ancestor block instances (parity with muyajs'sstartParents.filter(p => endParents.includes(p))).anchorBlockInfo/focusBlockInfo— per-endpoint content-leaf info{ blockName, type, functionType? }wheretypeis alwaysspanandfunctionTypeiscodeContent/cellContent/languageInput/paragraphContent.Tests
The two PG1
it.failsscoreboard tests are now realit()tests, plus two new tests pin the per-endpoint code-content info and the list affiliation context. Verified locally:lint(0 errors),lint:types,check-circular,test(516 pass / 18 expected-fail remaining for other PGs),test:spec(1347 pass).lint:csshas a pre-existingno-descending-specificityerror ondevelop(no CSS touched here).Desktop adapter is wave 2
The desktop
editor.vueadaptSelectionChangeis intentionally not touched here. A separate wave-2 desktop PR will consume the new fields: mapaffiliation→createApplicationMenuState'saffiliation, andanchorBlockInfo/focusBlockInfo→start.type/start.block.functionType(and the same forend).🤖 Generated with Claude Code