Port upstream 4946: progressively mount large documents#161
Open
Renakoni wants to merge 4 commits into
Open
Conversation
…footnote consumer gate
…, cover delete/tab exits
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.
Ports upstream marktext#4946 (progressive mount, the second half of the marktext#4887 large-file freeze fix) into the vendored Muya, plus the one app-side consumer adaptation. Companion to #159 (the algorithmic half); the two are independent to merge.
What it does
Opening a large document still spends its time synchronously building every block's DOM before anything paints.
ScrollPagenow mounts an over-viewport prefix synchronously (budgeted by state weight, 512 nodes) and appends the rest in scheduled, time-budgeted chunks (4 ms JS budget per task): the document is visible and editable at once and the remainder mounts in the background. On phone-class CPUs this matters more than anywhere else — the upstream desktop measurement went from 25 s blocking to interactive in 9.6 s on a 2.1 MB document, and mobile parse/mount costs are a multiple of that.The contracts (as reviewed upstream, seven Codex rounds, plus this repo’s parallel review)
getTOCcollects from the JSON state (complete before any block mounts);ITocItemcarries the heading's top-levelindex; slugs key off state nodes via WeakMap,headingCopyLinkresolves the same node.ensureMountedThrough), never a full flush.jsonState.flush()) and resumes at the live child count.updateContentspre-mounts every touched top-level block plus its successor from the pre-dispatch state, and on-demand materialization is suspended during the pick/drop walk (a residual miss falls back to the existing wholesale rebuild)._appendFromStateclones each slice before constructing blocks.setContentclears the selection and active block sofocus()cannot deep-mount to a stale caret.json-change— a same-frame edit enters the undo stack instead of being silently dropped;redo()with a pending edit invalidates quietly instead of popping an emptied stack; a first unflushed edit is undoable immediately.Muya.destroy()/updateStatecancel an in-flight mount explicitly; detached hosts mount to completion.lastContentInDescendantforces the remainder; markdown serialization reads the state and is unaffected. Hosts can trackmuya-mount-progress/muya-mount-complete.TreeNode.resolveNextContentInContext). The resolver advances from the mounted boundary itself — it asks for the first UNMOUNTED index (children.length) each round, never the receiver's own index + 1: the context walk has already scanned every mounted successor (including mounted empty containers), andensureMountedThroughearly-returns for any already-mounted index, so a receiver-relative request at the frontier would misread the pending tail as EOF. It keeps extending until a content appears or the mount stops progressing. Only a genuine document end falls through to the original behavior.muya-footnote-toollistener (newEventCenter.hasListeners) skip the preparation entirely. This host installs no FootnoteTool, so a footnote tap on Android now costs nothing.headingCopyLinkresolves path and state at the same revision. The block path reflects same-frame tree mutations immediately whilerawStateupdates on the rAF flush;_activate()drains the batch before indexing, so a click landing in that one-frame window cannot slug the wrong node.App-side adaptations
Two app features resolve top-level blocks by index against the mounted DOM; both now materialize their target first (a no-op once mounted):
selectHeadingcallseditor.ensureMountedThrough(item.index)before resolving, mirroring the upstream desktop TOC click.children[topBlockIndex]; beyond the prefix the offer would be silently discarded (and large documents are exactly where resume matters). Per this repo's review: the automatic open-time probe must never materialize blocks (a stored near-end index would synchronously deep-mount the document and reintroduce the freeze), so the offer validates a not-yet-mounted index against the logical top-level count (jsonState.rawState.length) — the hash match already proved the document identical, and a pending-tail target is far past the min-distance threshold by construction. Materialization happens only on the user's tap (activateResume). Genuinely out-of-range indices still discard.caretFollowneeds nothing: it follows the active block, which is always mounted. Serialization, autosave, and share flows read the JSON state and are unaffected.Verification
chunkedMount.spec.ts(31 contract tests — every review finding is a repro, incl. a below-frontier click inert while pending and appending after completion; a gutter click never flushing; frontier navigation for paragraph ArrowDown, table Enter/ArrowDown, and code-block Shift+Enter; resolution continuing across an empty container (unmounted AND already-mounted at the frontier), table Tab and Delete-forward merge reaching the pending successor; the footnote map covering a tail definition and skipping without a listener; copy-link slugging correctly under an unflushed structural edit) and thesetCursorByOffsetpartial-mount repro pass; app-gate suite 1475/1475;tsc --noEmitclean.ensureMountedThrough(2)) and resolved correctly. Resume probe tests (red-green verified against the previous code): a pending-tail target is offered without anyensureMountedThroughcall; an index beyond the logical document discards without mounting; activation materializes through the target.documentOutline+resumePositionsuites 44/44.pnpm typecheck,pnpm lint,pnpm buildclean;node_modules/@muyajs/coreresynced.mobile-editor-resumespec re-run in full: 14/14, including the previously flaky image spec, now clean.