feat(live-preview): add grip-handle drag reorder for list items#132
Open
Harriet-star-glitch wants to merge 3 commits into
Open
feat(live-preview): add grip-handle drag reorder for list items#132Harriet-star-glitch wants to merge 3 commits into
Harriet-star-glitch wants to merge 3 commits into
Conversation
Adds a small grip handle before each list item's bullet/number in live preview. Dragging it (mousedown/mousemove/mouseup, no HTML5 DnD, per project convention) reorders the item among its siblings; nested items move with whatever sub-list they contain, so indentation is preserved automatically since the raw text is relocated verbatim. Closes OpenSpec add-missing-features task 2.4. - packages/core/src/live-preview.ts: grip rendering + drag state machine in buildListDecorations; new pure, unit-testable getListItemRanges()/computeListReorderChange() helpers so the reorder semantics don't depend on simulating pixel-accurate drag coordinates. - packages/core/src/live-preview-table.ts: generalized the table's editing-lock counter (previously table-only) into isStructuralEditing()/acquireStructuralEditingLock()/ releaseStructuralEditingLock(), since list drag needs the same "skip live-preview rebuilds mid-interaction" guarantee the table widget already relied on. - packages/core/test/live-preview-list-drag.test.ts: 12 new tests covering grip rendering (incl. nested lists and cursor-revealed raw markup), the pure reorder computation (down/up/no-op/ nested-indentation-preserving), and a real EditorView round-trip. Manually verified in the electron-demo build with a real mouse-driven drag (Playwright driving the packaged Electron binary): the drop indicator renders during the drag and the document reorders correctly, with no console errors.
|
|
…e offsets view.coordsAtPos() throws "No tile at position N" for an offset outside CM6's currently-measured viewport (virtualized rendering) — reachable simply by dragging a grip toward a target that has scrolled off-screen in a long list, no external interference required. It reproduced reliably in a test that also mutates the document mid-drag (a host setDocument() call while the grip is held), which is the other half of the same underlying issue: the drag's captured item ranges/doc snapshot can go stale while a drag is in progress. - gapIndexAtClientY/showIndicatorAtGap now go through a safeCoordsAtPos wrapper instead of calling view.coordsAtPos() directly, so a measurement failure mid-drag degrades to a coordinate fallback instead of an uncaught exception in the mousemove handler. - onDragEnd now compares the live document against the snapshot taken when this list was last built, and bails out instead of dispatching a change computed from now-invalid offsets — the failure mode without this would be silent corruption (splicing at the wrong position), not just a crash. Added a regression test that mutates the document via setDocument() between mousedown and mouseup and asserts no uncaught exception and no corruption of the externally-set document.
15 tasks
…llel counter Found during a follow-up self-review of floatboatai#132, not from a reported bug. buildListDecorations tracked each grip's drag index with a counter incremented once per list.children entry, while the itemRanges array used to actually perform the reorder is built separately by getListItemRanges, which skips any item lacking `position` info. Real markdown parsed by this pipeline always has position, so the two never observably diverged in this PR's own tests — but a host's custom remarkPlugin injecting a position-less node into a list would desync the counter from itemRanges' indices for every subsequent sibling, sending later grips to reorder the wrong item (or an out-of-range index, silently absorbed by computeListReorderChange's existing bounds check — no crash, no corruption, just a drag that silently does nothing to the wrong item). Deriving the index directly from itemRanges (matched by the item's own `from` offset) instead of a separately-maintained counter makes this class of desync impossible by construction. All 528 tests still pass; typecheck and build unaffected.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary / 摘要
Adds a small grip handle before each list item's bullet/number in live preview; dragging it reorders the item among its siblings (nested sub-lists move with their parent, preserving indentation).
Motivation / 背景与动机
Changes / 变更内容
packages/core:live-preview.ts: grip handle + custom mousedown/mousemove/mouseup drag state machine insidebuildListDecorations(no HTML5 DnD, per CLAUDE.md rule 5). New pure, unit-testablegetListItemRanges()/computeListReorderChange()helpers so the reorder semantics are covered without needing to simulate pixel-accurate drag coordinates in jsdom.live-preview-table.ts: generalized the table widget's editing-lock counter (previously table-only,isTableEditing) intoisStructuralEditing()/acquireStructuralEditingLock()/releaseStructuralEditingLock(), since list drag needs the same "skip live-preview StateField rebuilds mid-interaction" guarantee the table widget already relied on (CLAUDE.md rules 8/12). Internal only — not exported from the package's publicindex.ts.openspec/:add-missing-features/tasks.md— checked off 2.4.Testing / 测试
pnpm testpasses (527/527, including 12 new tests)pnpm build)packages/core/test/live-preview-list-drag.test.ts— grip rendering (top-level list, nested sub-list, and the cursor-revealed-raw-markup case), the pure reorder computation (move down / move up / no-op / out-of-range / preserves nested indentation), and a realEditorViewround-trip applying the computed change.alpha,beta,gamma→beta,gamma,alphafor a drag of item 0 past item 2), and confirmed no console/page errors.Compliance / 合规自检
Checklist / 自检清单
isStructuralEditingetc. stay internal topackages/core/src, not exported fromindex.ts)live-preview-table.ts→ only renamed/generalized the shared lock counter; did not touch any of the 12 Table Widget drag/DOM rules in CLAUDE.mdadd-missing-featuresfeat(electron): i18n for renderer UI driven by language setting #2.4) rather than opening a new proposal