From d58ec516506c83a8d6258b6504c65798a7ea593f Mon Sep 17 00:00:00 2001 From: Kinneyzhang Date: Tue, 25 Aug 2026 22:45:13 +0800 Subject: [PATCH] perf: coalesce viewport resize delivery --- docs/user/ebox-api-reference.en.md | 9 ++- docs/user/ebox-api-reference.zh.md | 8 ++- ebox-viewport.el | 103 +++++++++++++++++++++++++++++ ebox.el | 42 ++---------- tests/ebox-child-range-tests.el | 5 ++ tests/ebox-commit-tests.el | 6 ++ tests/ebox-core-render-tests.el | 5 ++ tests/ebox-package-tests.el | 7 +- tests/ebox-surface-tests.el | 72 +++++++++++++++++--- 9 files changed, 204 insertions(+), 53 deletions(-) create mode 100644 ebox-viewport.el diff --git a/docs/user/ebox-api-reference.en.md b/docs/user/ebox-api-reference.en.md index 39de9c6..e80823a 100644 --- a/docs/user/ebox-api-reference.en.md +++ b/docs/user/ebox-api-reference.en.md @@ -431,6 +431,12 @@ context. For pure rendering, callers may instead bind `ebox-viewport-width` in pixels and `ebox-viewport-height` in lines around the render. Do not mutate a published node tree in place. +Visible mounted buffers follow their displaying window automatically. Ebox +coalesces continuous size events and publishes only the latest sample after +`ebox-viewport-resize-delay` (0.05 seconds by default). +`ebox-viewport-window-width` returns the same display-safe pixel width used by +that controller, so hosts must not install a second resize hook or width rule. + ## 7. Selectors `ebox-selector-parse` delegates to ECSS and returns its structured AST. @@ -516,6 +522,7 @@ The main customization variables are: | Lazy scroll | `ebox-scroll-lazy-prefix-lookahead-lines` 8; `ebox-scroll-lazy-idle-prefetch-lines` 128; `ebox-scroll-lazy-idle-prefetch-slice-lines` 16; `ebox-scroll-lazy-idle-prefetch-delay` 0.15 | | Runtime prewarm | `ebox-runtime-idle-prewarm` t; `ebox-runtime-idle-prewarm-delay` 0.1; `ebox-runtime-idle-prewarm-prefix-resume-delay` 2.0; `ebox-runtime-idle-prewarm-slice-size` 32; `ebox-native-buffer-scroll` t (initial/visible-window handoff only; strict root-owner proof) | | Predicted reflow | `ebox-runtime-idle-reflow-cache-prewarm` t; `ebox-runtime-idle-reflow-cache-prewarm-delay` 0.15 | +| Viewport delivery | `ebox-viewport-resize-delay` 0.05; continuous window events replace one pending sample per mounted buffer | | Reflow GC | `ebox-reflow-cache-prewarm-gc-cons-threshold` `auto`; `ebox-reflow-cache-prewarm-gc-auto-frame-budget` 0.2; `ebox-reflow-cache-prewarm-gc-auto-min-threshold` 64 MiB; `ebox-reflow-cache-prewarm-gc-auto-max-threshold` 1 GiB; `ebox-reflow-cache-prewarm-gc-auto-initial-threshold` 512 MiB; `ebox-reflow-cache-prewarm-gc-auto-target-layouts` 24; `ebox-reflow-cache-prewarm-gc-cons-percentage` 0.1 | | Visual verification | `ebox-visual-check-output-dir` points to the temporary directory used for optional screenshots and reports. | @@ -583,7 +590,7 @@ style-rule functions immediately below are public module-level style APIs. | `ebox-flex`, `ebox-flex-item` | Build flex containers and item metadata. | | `ebox-grid`, `ebox-grid-fr`, `ebox-grid-item` | Build Grid layouts, fractional tracks, and placement metadata. | | `ebox-render`, `ebox-render-to-buffer`, `ebox-display-buffer` | Pure materialization, retained mount, and display wrapper. | -| `ebox-commit`, `ebox-buffer-update-report`, `ebox-rerender-buffer-with-context` | Atomic root commit, report lookup, and viewport-context rerender. | +| `ebox-commit`, `ebox-buffer-update-report`, `ebox-rerender-buffer-with-context`, `ebox-viewport-window-width` | Atomic root commit, report lookup, viewport-context rerender, and the shared display-safe width sampler. | | `ebox-region-ids`, `ebox-region-resolve`, `ebox-region-update` | Region mapping, logical handle lookup, and direct update. | | `ebox-child-range`, `ebox-range-ref-present-p`, `ebox-candidate-begin`, `ebox-candidate-replace`, `ebox-candidate-replace-range-ref`, `ebox-candidate-replace-root`, `ebox-candidate-replace-host-ref`, `ebox-candidate-patch-host-paint` | Persistent material child segments, backend-anchor lookup, subtree replacement, and retained Host paint transactions. | | `ebox-host-ref-bounds`, `ebox-host-ref-position` | Live host-reference bounds and first position. | diff --git a/docs/user/ebox-api-reference.zh.md b/docs/user/ebox-api-reference.zh.md index 634b86f..f0bb7c0 100644 --- a/docs/user/ebox-api-reference.zh.md +++ b/docs/user/ebox-api-reference.zh.md @@ -392,6 +392,11 @@ report;若后续 phase 失败,rollback 最多一次接收同一个 report, render 外部动态绑定 `ebox-viewport-width`(像素)与 `ebox-viewport-height` (行数)。不要原地修改已经发布的 node tree。 +可见的 mounted buffer 会自动跟随展示它的 window。Ebox 合并连续 size event, +并在 `ebox-viewport-resize-delay`(默认 0.05 秒)后只发布最新 sample。 +`ebox-viewport-window-width` 返回 controller 使用的同一个显示安全像素宽度; +宿主不应再安装第二套 resize hook 或宽度算法。 + ## 7. Selector `ebox-selector-parse` 委托 ECSS 并返回结构化 AST; @@ -468,6 +473,7 @@ text scale 和固定 `display` space;它也是布局使用的公共测量原 | Lazy scroll | `ebox-scroll-lazy-prefix-lookahead-lines` 8;`ebox-scroll-lazy-idle-prefetch-lines` 128;`ebox-scroll-lazy-idle-prefetch-slice-lines` 16;`ebox-scroll-lazy-idle-prefetch-delay` 0.15 | | Runtime prewarm | `ebox-runtime-idle-prewarm` t;`ebox-runtime-idle-prewarm-delay` 0.1;`ebox-runtime-idle-prewarm-prefix-resume-delay` 2.0;`ebox-runtime-idle-prewarm-slice-size` 32;`ebox-native-buffer-scroll` t(仅 initial/visible-window handoff;严格 root-owner proof) | | 预测 reflow | `ebox-runtime-idle-reflow-cache-prewarm` t;`ebox-runtime-idle-reflow-cache-prewarm-delay` 0.15 | +| Viewport delivery | `ebox-viewport-resize-delay` 0.05;连续 window event 对每个 mounted buffer 只保留一个最新 sample | | Reflow GC | `ebox-reflow-cache-prewarm-gc-cons-threshold` `auto`;`ebox-reflow-cache-prewarm-gc-auto-frame-budget` 0.2;`ebox-reflow-cache-prewarm-gc-auto-min-threshold` 64 MiB;`ebox-reflow-cache-prewarm-gc-auto-max-threshold` 1 GiB;`ebox-reflow-cache-prewarm-gc-auto-initial-threshold` 512 MiB;`ebox-reflow-cache-prewarm-gc-auto-target-layouts` 24;`ebox-reflow-cache-prewarm-gc-cons-percentage` 0.1 | | Visual verification | `ebox-visual-check-output-dir` 指向可选 screenshot/report 使用的临时目录。 | @@ -531,7 +537,7 @@ style API。 | `ebox-flex`、`ebox-flex-item` | 构建 Flex 容器与 item metadata。 | | `ebox-grid`、`ebox-grid-fr`、`ebox-grid-item` | 构建 Grid、分数轨道和 placement metadata。 | | `ebox-render`、`ebox-render-to-buffer`、`ebox-display-buffer` | 纯 materialization、retained mount 与展示包装。 | -| `ebox-commit`、`ebox-buffer-update-report`、`ebox-rerender-buffer-with-context` | 原子根提交、报告查询和 viewport-context rerender。 | +| `ebox-commit`、`ebox-buffer-update-report`、`ebox-rerender-buffer-with-context`、`ebox-viewport-window-width` | 原子根提交、报告查询、viewport-context rerender 与统一的显示安全宽度采样。 | | `ebox-region-ids`、`ebox-region-resolve`、`ebox-region-update` | Region 映射、logical handle 查询与直接更新。 | | `ebox-child-range`、`ebox-range-ref-present-p`、`ebox-candidate-begin`、`ebox-candidate-replace`、`ebox-candidate-replace-range-ref`、`ebox-candidate-replace-root`、`ebox-candidate-replace-host-ref`、`ebox-candidate-patch-host-paint` | 持久 material child segment、backend anchor 查询、子树替换与 retained Host paint transaction。 | | `ebox-host-ref-bounds`、`ebox-host-ref-position` | Host-reference 的 live bounds 与首位置。 | diff --git a/ebox-viewport.el b/ebox-viewport.el new file mode 100644 index 0000000..1cfb991 --- /dev/null +++ b/ebox-viewport.el @@ -0,0 +1,103 @@ +;;; ebox-viewport.el --- Coalesced Emacs viewport delivery -*- lexical-binding: t; -*- + +;;; Commentary: +;; Emacs owns window events; Ebox owns the mounted viewport. This module is +;; the single bridge between them. Size hooks only sample the latest visible +;; dimensions. One deferred callback per buffer publishes the newest sample, +;; so continuous frame dragging cannot enqueue synchronous layout transactions. + +;;; Code: + +(require 'cl-lib) +(require 'ebox-surface) + +(declare-function ebox--buffer-render-state "ebox-incremental" (buffer)) +(declare-function ebox-rerender-buffer-with-context + "ebox" (buffer viewport-width &optional viewport-height)) + +(defcustom ebox-viewport-resize-delay 0.05 + "Seconds after the latest size event before publishing its viewport. +Continuous size events replace one pending sample for each mounted buffer." + :type 'number + :group 'ebox) + +(defvar ebox--viewport-resize-pending-table (make-hash-table :test #'eq) + "Buffer-keyed latest viewport samples awaiting publication.") + +(defvar ebox--viewport-resize-generation 0 + "Monotonic identity for pending viewport samples.") + +(defun ebox-viewport-window-width (window) + "Return Ebox's display-safe viewport width for live WINDOW." + (and (window-live-p window) + (ebox-surface--window-content-width window))) + +(defun ebox--cancel-buffer-viewport-resize (buffer) + "Cancel BUFFER's pending viewport publication." + (when-let* ((pending (gethash buffer ebox--viewport-resize-pending-table))) + (let ((timer (aref pending 4))) + (when (timerp timer) (cancel-timer timer))) + (remhash buffer ebox--viewport-resize-pending-table)) + nil) + +(defun ebox--viewport-resize-apply (buffer generation) + "Publish BUFFER's pending viewport identified by GENERATION." + (when-let* ((pending (gethash buffer ebox--viewport-resize-pending-table))) + (when (= generation (aref pending 0)) + (remhash buffer ebox--viewport-resize-pending-table) + (let ((window (aref pending 1)) + (width (aref pending 2)) + (height (aref pending 3))) + (when (and (buffer-live-p buffer) + (window-live-p window) + (eq (window-buffer window) buffer) + (ebox-surface-buffer-mounted-p buffer)) + (let ((state (ebox--buffer-render-state buffer))) + (unless (and (= width (or (plist-get state :viewport-width) -1)) + (= height (or (plist-get state :viewport-height) -1))) + (ebox-rerender-buffer-with-context buffer width height)))))))) + +(defun ebox--schedule-buffer-viewport-resize + (buffer window width height) + "Replace BUFFER's pending viewport with WINDOW, WIDTH, and HEIGHT." + (ebox--cancel-buffer-viewport-resize buffer) + (let* ((generation (cl-incf ebox--viewport-resize-generation)) + (pending (vector generation window width height nil))) + (puthash buffer pending ebox--viewport-resize-pending-table) + (aset pending 4 + (run-at-time + (max 0 ebox-viewport-resize-delay) nil + #'ebox--viewport-resize-apply buffer generation))) + buffer) + +(defun ebox--window-size-change (frame) + "Sample latest viewports for mounted Ebox buffers visible in FRAME." + (when (and (frame-live-p frame) (not noninteractive)) + (let ((seen (make-hash-table :test #'eq))) + (dolist (window (window-list frame 'no-minibuf)) + (when (window-live-p window) + (let ((buffer (window-buffer window))) + (unless (gethash buffer seen) + (puthash buffer t seen) + (when (and (buffer-live-p buffer) + (ebox-surface-buffer-mounted-p buffer)) + (let* ((state (ebox--buffer-render-state buffer)) + (width (ebox-viewport-window-width window)) + (height (window-body-height window))) + (when (and width + state + (not (and + (= width + (or (plist-get state :viewport-width) -1)) + (= height + (or (plist-get state :viewport-height) + -1))))) + (ebox--schedule-buffer-viewport-resize + buffer window width height))))))))))) + +(when (boundp 'window-size-change-functions) + (add-hook 'window-size-change-functions #'ebox--window-size-change)) + +(provide 'ebox-viewport) + +;;; ebox-viewport.el ends here diff --git a/ebox.el b/ebox.el index 3a25e57..0677ca0 100644 --- a/ebox.el +++ b/ebox.el @@ -26,7 +26,8 @@ '("ebox-cache.el" "ebox-style.el" "ebox-child-range.el" "ebox-tree.el" "ebox-measure.el" "ebox-fragment.el" "ebox-render-context.el" "ebox-layout.el" "ebox-flex.el" "ebox-grid.el" "ebox-buffer-backend.el" "ebox-incremental.el" - "ebox-native-commit.el" "ebox-surface.el" "ebox-dsl.el" "ebox-selector.el" "ebox.el" + "ebox-native-commit.el" "ebox-surface.el" "ebox-viewport.el" + "ebox-dsl.el" "ebox-selector.el" "ebox.el" "ebox-native-reflow.el") "Active Ebox Lisp sources compiled by `ebox-byte-compile'.") @@ -49,6 +50,7 @@ (require 'ebox-buffer-backend) (require 'ebox-incremental) (require 'ebox-surface) +(require 'ebox-viewport) (require 'ebox-dsl) (require 'ebox-selector) @@ -2742,42 +2744,6 @@ that was rendered off-window cannot miss the native-scroll handoff." (when (boundp 'window-state-change-functions) (add-hook 'window-state-change-functions #'ebox--window-state-change)) -(defvar ebox--window-size-change-in-progress nil - "Non-nil while visible Ebox buffers are synchronizing their viewport.") - -(defun ebox--window-size-change (frame) - "Synchronize visible Ebox buffers after FRAME changes size. - -Ebox stores the containing-block viewport in its mounted surface. A frame -resize must publish the new window content width before the next render; -otherwise responsive Flex/Grid trees keep using the width from mount time. -One mounted surface represents one viewport, so when a buffer is visible in -multiple windows the first window in FRAME supplies the shared dimensions." - (when (and (frame-live-p frame) - (not noninteractive) - (not ebox--window-size-change-in-progress)) - (let ((ebox--window-size-change-in-progress t) - (seen (make-hash-table :test #'eq))) - (dolist (window (window-list frame 'no-minibuf)) - (when (and (window-live-p window) - (not (gethash (window-buffer window) seen))) - (let ((buffer (window-buffer window))) - (puthash buffer t seen) - (when (and (buffer-live-p buffer) - (ebox--buffer-render-state buffer) - (ebox-surface-buffer-mounted-p buffer)) - (let* ((state (ebox--buffer-render-state buffer)) - (width (ebox-surface--window-content-width window)) - (height (window-body-height window))) - (unless (and (= width (or (plist-get state :viewport-width) - -1)) - (= height (or (plist-get state :viewport-height) - -1))) - (ebox-rerender-buffer-with-context buffer width height)))))))))) - -(when (boundp 'window-size-change-functions) - (add-hook 'window-size-change-functions #'ebox--window-size-change)) - (defun ebox--runtime-prewarm-record-viewport-node (job node width-allowed) "Record NODE's direct viewport dependencies in JOB. @@ -3882,6 +3848,7 @@ With prefix ARG, scroll by that many content lines." "Remove all runtime caches owned by BUFFER. Defaults to the current buffer." (let ((buffer (or buffer (current-buffer)))) + (ebox--cancel-buffer-viewport-resize buffer) (ebox--cancel-buffer-runtime-prewarm buffer) (when-let ((root (ebox--buffer-root-node buffer))) (ebox--clear-region-runtime-caches @@ -4716,6 +4683,7 @@ through dirty-set and patch-set execution before falling back to root rerender." ebox-stack ebox-string-pixel-width ebox-update-selector + ebox-viewport-window-width ebox-wheel-scroll-down ebox-wheel-scroll-up) "Stable core Ebox entry points available to applications and tooling. diff --git a/tests/ebox-child-range-tests.el b/tests/ebox-child-range-tests.el index 1134325..8d9f600 100644 --- a/tests/ebox-child-range-tests.el +++ b/tests/ebox-child-range-tests.el @@ -4,6 +4,11 @@ (require 'ebox-child-range) (require 'ebox) +;; This suite locks child-range and Elisp projection semantics. Native frame +;; execution is verified separately and must not replace the named fallback +;; strategies merely because a developer has built the optional module. +(setq ebox-native-reflow-module-path nil) + (defconst ebox-child-range-test--root (expand-file-name ".." (file-name-directory (or load-file-name buffer-file-name))) "Ebox repository root for source audits.") diff --git a/tests/ebox-commit-tests.el b/tests/ebox-commit-tests.el index ad02cb8..466fc40 100644 --- a/tests/ebox-commit-tests.el +++ b/tests/ebox-commit-tests.el @@ -5,6 +5,12 @@ (require 'ebox) (require 'ebox-native-commit) +;; These tests lock the named Elisp projection proofs. Native commit has its +;; own focused contract tests below; disable runtime module discovery here so +;; a locally built optional module cannot silently replace paint/span/mixed +;; plans with `native-frame' and make this suite environment-dependent. +(setq ebox-native-reflow-module-path nil) + (defun ebox-commit-test--buffer-string (buffer) "Return BUFFER's complete propertized contents." (with-current-buffer buffer diff --git a/tests/ebox-core-render-tests.el b/tests/ebox-core-render-tests.el index 3f8c818..5092f36 100644 --- a/tests/ebox-core-render-tests.el +++ b/tests/ebox-core-render-tests.el @@ -10,6 +10,11 @@ (load-file (expand-file-name "../ebox.el" (file-name-directory load-file-name))) +;; Core tests assert named Elisp render/patch strategies. Keep optional local +;; native artifacts from replacing those strategies; native execution has +;; separate module and integration gates. +(setq ebox-native-reflow-module-path nil) + (defvar ebox--scroll-window-initial-lookahead-lines-override nil) (defun ebox-test--reset-runtime-state () diff --git a/tests/ebox-package-tests.el b/tests/ebox-package-tests.el index 4643d8a..ad06f3a 100644 --- a/tests/ebox-package-tests.el +++ b/tests/ebox-package-tests.el @@ -51,7 +51,8 @@ (dolist (feature '(ebox-cache ebox-style ebox-tree ebox-measure ebox-fragment ebox-layout ebox-flex ebox-grid ebox-buffer-backend ebox-incremental - ebox-surface ebox-dsl ebox-selector)) + ebox-surface ebox-viewport ebox-dsl + ebox-selector)) (should (featurep feature)))) (ert-deftest ebox-facade-exposes-native-workflow-without-loading-it () @@ -70,8 +71,8 @@ "ebox-tree.el" "ebox-measure.el" "ebox-fragment.el" "ebox-render-context.el" "ebox-layout.el" "ebox-flex.el" "ebox-grid.el" "ebox-buffer-backend.el" "ebox-incremental.el" - "ebox-native-commit.el" "ebox-surface.el" "ebox-dsl.el" - "ebox-selector.el" "ebox.el" + "ebox-native-commit.el" "ebox-surface.el" "ebox-viewport.el" + "ebox-dsl.el" "ebox-selector.el" "ebox.el" "ebox-native-reflow.el")))) (ert-deftest ebox-surface-depends-only-on-public-tp-api () diff --git a/tests/ebox-surface-tests.el b/tests/ebox-surface-tests.el index 6e05f92..48f9e06 100644 --- a/tests/ebox-surface-tests.el +++ b/tests/ebox-surface-tests.el @@ -171,11 +171,15 @@ (when (get-buffer " *other-window*") (kill-buffer " *other-window*"))))) -(ert-deftest ebox-window-size-change-publishes-visible-viewport () - "A live frame resize updates the mounted surface's containing block." +(ert-deftest ebox-window-size-change-coalesces-to-latest-visible-viewport () + "Continuous frame changes publish only the latest sampled viewport." (let* ((buffer (generate-new-buffer " *ebox-window-size-change*")) (window (selected-window)) (old-buffer (window-buffer window)) + (sampled-height (window-body-height window)) + (sampled-widths (number-sequence 240 430 10)) + scheduled + cancelled calls) (unwind-protect (progn @@ -186,18 +190,58 @@ ((symbol-function 'ebox-surface-buffer-mounted-p) (lambda (_buffer) t)) ((symbol-function 'ebox-surface--window-content-width) - (lambda (_window) 240)) - ((symbol-function 'window-body-height) - (lambda (_window &optional _pixelwise) 20)) + (lambda (_window) (pop sampled-widths))) ((symbol-function 'ebox-rerender-buffer-with-context) (lambda (target width height) - (setq calls (list target width height)))) + (push (list target width height) calls))) + ((symbol-function 'run-at-time) + (lambda (delay _repeat function &rest arguments) + (let ((timer (intern (format "ebox-resize-timer-%d" + (1+ (length scheduled)))))) + (push (list timer delay function arguments) scheduled) + timer))) + ((symbol-function 'timerp) + (lambda (value) + (string-prefix-p "ebox-resize-timer-" + (symbol-name value)))) + ((symbol-function 'cancel-timer) + (lambda (timer) (push timer cancelled))) (noninteractive nil)) - (ebox--window-size-change (selected-frame)) - (should (equal calls (list buffer 240 20))))) + (dotimes (_index 20) + (ebox--window-size-change (selected-frame))) + (should-not calls) + (should (= (length scheduled) 20)) + (should (= (length cancelled) 19)) + (pcase-let ((`(,_timer ,delay ,function ,arguments) + (car scheduled))) + (should (= delay ebox-viewport-resize-delay)) + (apply function arguments)) + (should (equal calls + (list (list buffer 430 sampled-height)))))) (set-window-buffer window old-buffer) + (when (boundp 'ebox--viewport-resize-pending-table) + (clrhash ebox--viewport-resize-pending-table)) (kill-buffer buffer)))) +(ert-deftest ebox-buffer-cleanup-cancels-pending-viewport-resize () + "A dead mounted buffer cannot retain a viewport timer or pending sample." + (let ((buffer (generate-new-buffer " *ebox-viewport-cleanup*")) + (timer 'ebox-test-viewport-cleanup-timer) + cancelled) + (unwind-protect + (progn + (puthash buffer (vector 1 (selected-window) 400 20 timer) + ebox--viewport-resize-pending-table) + (cl-letf (((symbol-function 'timerp) + (lambda (value) (eq value timer))) + ((symbol-function 'cancel-timer) + (lambda (value) (setq cancelled value)))) + (with-current-buffer buffer (ebox--cleanup-current-buffer))) + (should (eq cancelled timer)) + (should-not (gethash buffer ebox--viewport-resize-pending-table))) + (remhash buffer ebox--viewport-resize-pending-table) + (when (buffer-live-p buffer) (kill-buffer buffer))))) + (defun ebox-surface-test--hash-fingerprint (table) "Return a stable content fingerprint for hash TABLE. The fingerprint checks entries rather than only table identity, so a failed @@ -732,10 +776,13 @@ candidate cannot hide mutations by restoring the old hash-table pointer." (ert-deftest ebox-surface-plan-stays-free-of-runtime-state () "The surface plan should not contain markers or runtime handles." + (require 'ebox-native-commit) (let ((original (symbol-function 'tp-surface-result-create)) (original-owned (symbol-function 'tp-surface-result-create-owned)) captured client-state) - (cl-letf (((symbol-function 'tp-surface-result-create) + (cl-letf (((symbol-function 'ebox-native-commit-render) + (lambda (&rest _) nil)) + ((symbol-function 'tp-surface-result-create) (lambda (plan &optional state) (setq captured plan client-state state) @@ -753,7 +800,8 @@ candidate cannot hide mutations by restoring the old hash-table pointer." (let* ((fragments-plan (car (tp-surface-plan-children captured))) (text-plan (car (tp-surface-plan-children fragments-plan))) (plan-text (tp-surface-plan-text text-plan)) - (state-fragments (plist-get client-state :surface-fragments)) + (state-fragments + (ebox-surface--materialized-fragment-ledger client-state)) (owner-position (cl-loop for position from 0 below (length plan-text) when (get-text-property position @@ -1323,7 +1371,9 @@ candidate cannot hide mutations by restoring the old hash-table pointer." (plist-put node :key 'growing-source) node)) (let ((original-render (symbol-function 'ebox-render))) - (cl-letf (((symbol-function 'ebox-render) + (cl-letf (((symbol-function 'ebox-native-commit-render) + (lambda (&rest _) nil)) + ((symbol-function 'ebox-render) (lambda (node) (cond ((equal (plist-get node :key) 'stable-source)