perf: retain mount state across mixed geometry and paint updates

This commit is contained in:
Kinneyzhang 2026-09-07 05:03:36 +08:00
parent 789e0b2f17
commit 71154f5f78
2 changed files with 527 additions and 83 deletions

View File

@ -1805,6 +1805,13 @@ the projection roots a second time."
(ebox-surface--native-full-frame-p state projection-kind)))
(previous-owned-ranges
(plist-get state :previous-surface-owned-ranges))
(retained-owned-ranges
(and (eq projection-kind 'mixed-owner-reflow)
(eq (plist-get (plist-get state :mixed-owner-proof)
:geometry-kind)
'span-patch)
(not (ebox-surface--mixed-range-splice-p state))
(plist-get state :span-patch-retained-owned-ranges)))
(node-objects
(or (and scroll-fast-p
(plist-get state :surface-node-object-table))
@ -1818,6 +1825,7 @@ the projection roots a second time."
;; Paint contributions belong to this one publication. Keep them out of
;; the transferred client state so later paint cannot replay old layers.
(cl-remf state :paint-property-contributions)
(cl-remf state :span-patch-retained-owned-ranges)
(plist-put state :surface-node-object-table node-objects)
(plist-put state :region-surface-object-table region-objects)
(unless (and scroll-fast-p
@ -1831,11 +1839,12 @@ the projection roots a second time."
(or (and native-retained-p
(ebox-surface--native-patch-result
context projection state output node-objects region-objects))
(and (null property-contributions)
(and (or (null property-contributions) retained-owned-ranges)
(memq projection-kind
'(span-patch owner-scoped mixed-owner-reflow))
(ebox-surface--span-coordinate-result
context projection state output previous-owned-ranges))
context projection state output previous-owned-ranges
property-contributions retained-owned-ranges))
(let* ((range-splice-p
(and (eq projection-kind 'mixed-owner-reflow)
(ebox-surface--mixed-range-splice-p state)))
@ -1849,9 +1858,15 @@ the projection roots a second time."
(or (and (null property-contributions)
(memq projection-kind
'(span-patch owner-scoped mixed-owner-reflow))
(ebox-surface--coordinate-commit
context state rendered owned-ranges (nth 3 prepared)
previous-owned-ranges))
(when-let* ((batch
(ebox-surface--coordinate-batch
state rendered previous-owned-ranges)))
(tp-commit-batch-result-create
context batch
:mount-specs
(ebox-surface--commit-mount-specs
(nth 3 prepared) owned-ranges)
:client-state state)))
(if (and (not range-splice-p)
(or full-surface-p native-retained-p
property-contributions))
@ -1965,13 +1980,68 @@ content projection order; fragment and surface records follow unchanged."
(ebox-surface--commit-mount-specs mount-records owned-ranges)
:client-state state))))))
(defun ebox-surface--coordinate-commit
(context state rendered owned-ranges mount-records previous-owned-ranges)
"Return STATE's exact proven-owner coordinate commit, or nil.
PREVIOUS-OWNED-RANGES identify the committed owner intervals. The mixed
(defun ebox-surface--patch-property-contributions
(patches contributions extent)
"Return fresh PATCHES carrying completely covered CONTRIBUTIONS, or nil.
CONTRIBUTIONS use target coordinates bounded by EXTENT. Preserve their order
inside every patch; a gap rejects the batch rather than dropping paint."
(when (proper-list-p contributions)
(catch 'uncovered
(let* ((targets (vconcat (mapcar #'copy-sequence patches)))
(count (length targets))
(layers (make-vector count nil)))
(dolist (contribution contributions)
(let ((start (plist-get contribution :start))
(end (plist-get contribution :end))
(props (plist-get contribution :props)))
;; Validate the original interval before clipping. On a proof
;; miss the retained-content path reports invalid inputs through
;; TP's ordinary contribution validator.
(unless (and (integerp start) (integerp end)
(<= 0 start end extent)
(ebox-style--valid-plist-p props)
(cl-loop for (key _value) on props by #'cddr
always (symbolp key)))
(throw 'uncovered nil))
(when (< start end)
(let ((low 0) (high count) (position start))
;; Patches are ordered and disjoint. Locate the first one
;; once per layer, then visit only its actual intersections.
(while (< low high)
(let ((middle (/ (+ low high) 2)))
(if (<= (plist-get (aref targets middle) :new-end) start)
(setq low (1+ middle))
(setq high middle))))
(while (< position end)
(when (>= low count) (throw 'uncovered nil))
(let* ((patch (aref targets low))
(patch-start (plist-get patch :new-start))
(next (min end (plist-get patch :new-end))))
(when (> patch-start position) (throw 'uncovered nil))
(when (< position next)
(push (list :start (- position patch-start)
:end (- next patch-start) :props props)
(aref layers low)))
(setq position next low (1+ low))))))))
(dotimes (index count)
(when (aref layers index)
(aset targets index
(plist-put (aref targets index) :property-contributions
(nreverse (aref layers index))))))
(append targets nil)))))
(defun ebox-surface--coordinate-batch
(state rendered previous-owned-ranges
&optional property-contributions retained-origin)
"Return STATE's exact proven-owner coordinate batch, or nil.
RENDERED is the candidate text. PREVIOUS-OWNED-RANGES identify the committed
owner intervals. The mixed
projection has already produced exact coordinate patches and final text; this
function composes those retained facts into TP's standard validated commit
batch without asking TP to rediscover the same diff from a full plan."
batch without asking TP to rediscover the same diff from a full plan.
PROPERTY-CONTRIBUTIONS add ordered target paint. RETAINED-ORIGIN is the live
surface origin when an exact ownership proof permits indexed owner lookup.
This proof step does not register objects in the active prepare context."
(let* ((proof (plist-get state :mixed-owner-proof))
(owner-ids
(or (plist-get proof :owner-ids)
@ -2006,19 +2076,32 @@ batch without asking TP to rediscover the same diff from a full plan."
:new-start (plist-get patch :new-start)
:new-end (plist-get patch :new-end)))
coordinate-patches))
(dolist (range previous-owned-ranges)
(when (gethash (plist-get range :object) owner-set)
(when-let* ((new-start
(ebox-surface--rebase-coordinate
(plist-get range :start) coordinate-patches))
(new-end
(ebox-surface--rebase-coordinate
(plist-get range :end) coordinate-patches)))
(push (list :old-start (plist-get range :start)
:old-end (plist-get range :end)
:new-start (car new-start)
:new-end (car new-end))
intervals))))
(if retained-origin
;; The strict retained-property witness preserves the complete
;; mount projection. Read just the changed owners from TP's
;; existing index, including baseline-only paint removals.
(maphash
(lambda (object _present)
(dolist (mount (tp-object-mounts object))
(let ((start (- (plist-get mount :start) retained-origin))
(end (- (plist-get mount :end) retained-origin)))
(push (list :old-start start :old-end end
:new-start start :new-end end)
intervals))))
owner-set)
(dolist (range previous-owned-ranges)
(when (gethash (plist-get range :object) owner-set)
(when-let* ((new-start
(ebox-surface--rebase-coordinate
(plist-get range :start) coordinate-patches))
(new-end
(ebox-surface--rebase-coordinate
(plist-get range :end) coordinate-patches)))
(push (list :old-start (plist-get range :start)
:old-end (plist-get range :end)
:new-start (car new-start)
:new-end (car new-end))
intervals)))))
(setq intervals
(sort intervals
(lambda (left right)
@ -2077,56 +2160,103 @@ batch without asking TP to rediscover the same diff from a full plan."
:patches patches
:coordinate-patches intervals)
(tp-surface-error nil)))))
(when (and batch property-contributions)
(setq batch
(when-let* ((layered
(ebox-surface--patch-property-contributions
patches property-contributions target-extent)))
;; The bare batch above proves geometry. Composition
;; runs outside its proof-miss catch: a merger can signal
;; any condition, including `tp-surface-error', and must
;; preserve ordinary transaction failure and rollback.
(tp-commit-batch-create
:base-revision base-revision
:target-revision (1+ base-revision)
:base-extent source-extent :target-extent target-extent
:patches layered :coordinate-patches intervals))))
(when batch
(prog1
(cl-remf state :content-base-extent)
batch))))))
(defun ebox-surface--span-coordinate-result
(context projection state rendered previous-owned-ranges
&optional property-contributions retained-owned-ranges)
"Return a direct span commit from retained coordinate metadata, or nil.
CONTEXT and PROJECTION supply retained object identities. STATE and RENDERED
hold the candidate coordinates and text; PREVIOUS-OWNED-RANGES is the committed
ownership snapshot. PROPERTY-CONTRIBUTIONS are ordered candidate paint layers.
RETAINED-OWNED-RANGES witnesses unchanged ownership when it is that snapshot."
(let* ((patches (plist-get state :content-coordinate-patches))
(surface-root (plist-get projection :surface-root))
(retained-origin
(when (and surface-root patches retained-owned-ranges
(eq retained-owned-ranges previous-owned-ranges)
(equal (plist-get state :content-base-extent)
(length rendered))
(cl-every
(lambda (patch)
(and (= (plist-get patch :old-start)
(plist-get patch :new-start))
(= (plist-get patch :old-end)
(plist-get patch :new-end))))
patches))
(let ((mounts (tp-object-mounts surface-root)))
(when (and (= (length mounts) 1)
(= (- (plist-get (car mounts) :end)
(plist-get (car mounts) :start))
(length rendered)))
(plist-get (car mounts) :start))))))
(when-let* ((fragments
(and surface-root patches
(or (plist-get state :mixed-owner-fragment-data)
(plist-get state :span-patch-fragment-data))))
(rebased
(if retained-origin
(cons t previous-owned-ranges)
(and (null property-contributions) previous-owned-ranges
(ebox-surface--rebase-owned-ranges
previous-owned-ranges patches (length rendered)))))
(batch
(ebox-surface--coordinate-batch
state rendered previous-owned-ranges
property-contributions retained-origin))
(fragment-root
(tp-object-ensure context surface-root
ebox-surface--fragments-key 'ebox/fragments))
(text-leaf
(tp-object-ensure context fragment-root
ebox-surface--text-key 'ebox/text)))
(let* ((owned-ranges (cdr rebased))
(rendered-length (length rendered))
(mount-records
(unless retained-origin
(list (list :object text-leaf :start 0 :end rendered-length
:props nil :tags nil :leaf t)
(list :object fragment-root :start 0 :end rendered-length
:props nil :tags nil :leaf nil)
(list :object surface-root :start 0 :end rendered-length
:props nil :tags nil :leaf nil))))
(result
(if retained-origin
(tp-commit-batch-result-create
context batch :client-state state :reuse-mount-projection t)
(tp-commit-batch-result-create
context batch
:mount-specs
(ebox-surface--commit-mount-specs mount-records owned-ranges)
:client-state state)
(cl-remf state :content-base-extent))))))))
(defun ebox-surface--span-coordinate-result
(context projection state rendered previous-owned-ranges)
"Return a direct span commit from retained coordinate metadata, or nil."
(when-let* ((fragments
(or (plist-get state :mixed-owner-fragment-data)
(plist-get state :span-patch-fragment-data)))
(patches (plist-get state :content-coordinate-patches))
(rebased
(and previous-owned-ranges
(ebox-surface--rebase-owned-ranges
previous-owned-ranges patches (length rendered))))
(surface-root (plist-get projection :surface-root))
(fragment-root
(tp-object-ensure context surface-root
ebox-surface--fragments-key 'ebox/fragments))
(text-leaf
(tp-object-ensure context fragment-root
ebox-surface--text-key 'ebox/text)))
(let* ((owned-ranges (cdr rebased))
(rendered-length (length rendered))
(mount-records
(list (list :object text-leaf :start 0 :end rendered-length
:props nil :tags nil :leaf t)
(list :object fragment-root :start 0 :end rendered-length
:props nil :tags nil :leaf nil)
(list :object surface-root :start 0 :end rendered-length
:props nil :tags nil :leaf nil)))
(result
(ebox-surface--coordinate-commit
context state rendered owned-ranges mount-records
previous-owned-ranges)))
(when result
(plist-put state :surface-owned-ranges
(ebox-surface--snapshot-owned-ranges owned-ranges))
(plist-put state :surface-fragments fragments)
(cl-remf state :previous-surface-owned-ranges)
(cl-remf state :mixed-owner-fragment-data)
(cl-remf state :mixed-owner-content-p)
(cl-remf state :span-patch-fragment-data)
(cl-remf state :span-patch-content-p)
result))))
:client-state state))))
(when result
(plist-put state :surface-owned-ranges
(if retained-origin
owned-ranges
(ebox-surface--snapshot-owned-ranges owned-ranges)))
(plist-put state :surface-fragments fragments)
(cl-remf state :previous-surface-owned-ranges)
(cl-remf state :mixed-owner-fragment-data)
(cl-remf state :mixed-owner-content-p)
(cl-remf state :span-patch-fragment-data)
(cl-remf state :span-patch-content-p)
result)))))
(defun ebox-surface--mounted-object-for-node (state node-id)
"Return NODE-ID's nearest live retained object with mounts in STATE."
@ -3926,7 +4056,10 @@ Proof mismatches return nil; unexpected rendering errors propagate."
output old-fragments
(plist-get state :content-coordinate-patches)))))
(plist-put state :span-patch-fragment-data fragments)
(plist-put state :span-patch-content-p t))
(plist-put state :span-patch-content-p t)
(when (and retained-properties-p (not coordinate-shift-p))
(plist-put state :span-patch-retained-owned-ranges
(plist-get previous-state :surface-owned-ranges))))
output)))))))
(defun ebox-surface--formatting-context-reflow-lines
@ -5617,6 +5750,7 @@ but cannot introduce or remove a viewport expression or node identity."
(ebox--new-buffer-render-state root))
(copy-sequence state-overrides))))
(cl-remf state :paint-property-contributions)
(cl-remf state :span-patch-retained-owned-ranges)
(plist-put state :root-node root)
(when (plist-get state :native-sync-confirmed-p)
(ebox-native-commit-attach-confirmed-base previous-state state))

View File

@ -948,6 +948,23 @@
(ebox-child-range-test--assert-current-snapshot
(current-buffer) ancestor))))))))
(defun ebox-child-range-test--mixed-followup-candidate (buffer selected &optional title)
"Return BUFFER's title and row-paint candidate for SELECTED, using TITLE."
(let ((candidate (ebox-candidate-begin buffer)))
(ebox-candidate-replace-host-ref
candidate 'title
(ebox-test-text (propertize (or title (format "title-%d" selected))
'face '(:weight bold))
:key 'title :source-identity 'title))
(dolist (row '(1 2))
(let ((ref (intern (format "row-%d" row))))
(ebox-candidate-replace-host-ref
candidate ref
(ebox-test-box :key ref :source-identity ref
:color (if (= row selected) "#123456" "#654321")
(ebox-test-text (format "row-%d" row))))))
candidate))
(ert-deftest ebox-child-range-retained-kind-replacement-allows-mixed-followups ()
"Later title and row paint edits remain local beside disjoint scrolling."
(with-temp-buffer
@ -960,6 +977,12 @@
(current-buffer)
(ebox-child-range-test--replace-snapshot-detail (current-buffer) t))
(let* ((state (ebox--buffer-render-state (current-buffer)))
(surface ebox-surface--buffer-surface)
(mounts (tp--surface-mounts surface))
(mount-ids (mapcar #'tp--surface-mount-id mounts))
(mount-index (tp--surface-mount-index surface))
(index (tp--surface-index surface))
(ranges (plist-get state :surface-owned-ranges))
(scroll-id (car (plist-get state :scroll-region-ids)))
(scroll (gethash scroll-id ebox--scroll-global-state))
(raw (plist-get scroll :content-lines))
@ -972,22 +995,12 @@
(should scroll-id)
(should (= (length (plist-get state :scroll-region-ids)) 1))
(dolist (selected '(2 1 2))
(let* ((candidate (ebox-candidate-begin (current-buffer)))
(let* ((candidate
(ebox-child-range-test--mixed-followup-candidate
(current-buffer) selected))
(root-renders 0)
(observer (lambda (&rest _arguments) (cl-incf root-renders)))
report)
(ebox-candidate-replace-host-ref
candidate 'title
(ebox-test-text (propertize (format "title-%d" selected)
'face '(:weight bold))
:key 'title :source-identity 'title))
(dolist (row '(1 2))
(let ((ref (intern (format "row-%d" row))))
(ebox-candidate-replace-host-ref
candidate ref
(ebox-test-box :key ref :source-identity ref
:color (if (= row selected) "#123456" "#654321")
(ebox-test-text (format "row-%d" row))))))
(advice-add 'ebox-surface--render-candidate :before observer)
(unwind-protect
(setq report (ebox-commit (current-buffer) candidate))
@ -1001,11 +1014,308 @@
selected root-renders report))
(should (eq (plist-get report :projection-kind) 'mixed-owner-reflow))
(should (zerop root-renders))
(should-not (plist-get report :tp-full-root)))
(should-not (plist-get report :tp-full-root))
(should (plist-get (tp-surface-report surface) :commit-batch))
(should (plist-get (tp-surface-report surface) :retained-mount-state))
(should (eq mounts (tp--surface-mounts surface)))
(should (equal mount-ids
(mapcar #'tp--surface-mount-id
(tp--surface-mounts surface))))
(should (eq mount-index (tp--surface-mount-index surface)))
(should (eq index (tp--surface-index surface)))
(should (eq ranges
(plist-get (ebox--buffer-render-state (current-buffer))
:surface-owned-ranges))))
(let ((next-scroll (gethash scroll-id ebox--scroll-global-state)))
(should (eq raw (plist-get next-scroll :content-lines)))
(should (eq rendered (plist-get next-scroll :rendered-content-lines)))))))))
(ert-deftest ebox-child-range-mixed-followup-publish-failure-restores-and-retries ()
"A failed public mixed commit restores the old generation before retry."
(with-temp-buffer
(ebox-render-to-buffer
(current-buffer) (ebox-child-range-test--snapshot-replacement-root))
(ebox--ensure-layout-snapshot-details
(current-buffer)
(plist-get (ebox--host-ref-node (current-buffer) 'detail) :node-id))
(ebox-commit
(current-buffer)
(ebox-child-range-test--replace-snapshot-detail (current-buffer) t))
(let* ((state (ebox--buffer-render-state (current-buffer)))
(surface ebox-surface--buffer-surface)
(revision (tp-surface-revision surface))
(previous-report (tp-surface-report surface))
(before (buffer-string))
(mounts (tp--surface-mounts surface))
(mount-ids (mapcar #'tp--surface-mount-id mounts))
(mount-index (tp--surface-mount-index surface))
(index (tp--surface-index surface))
(ranges (plist-get state :surface-owned-ranges))
(scroll-id (car (plist-get state :scroll-region-ids)))
(scroll (gethash scroll-id ebox--scroll-global-state))
trace)
(should-error
(ebox-commit
(current-buffer)
(ebox-child-range-test--mixed-followup-candidate (current-buffer) 2)
(lambda (_report) (push 'publish trace) (error "reject mixed publication"))
(lambda (_report) (push 'rollback trace))))
(should (equal trace '(rollback publish)))
(should (eq state (ebox--buffer-render-state (current-buffer))))
(should (eq state (tp-surface-client-state surface)))
(should (= revision (tp-surface-revision surface)))
(should (eq ranges (plist-get state :surface-owned-ranges)))
(should (eq mounts (tp--surface-mounts surface)))
(should (equal mount-ids (mapcar #'tp--surface-mount-id mounts)))
(should (eq mount-index (tp--surface-mount-index surface)))
(should (eq index (tp--surface-index surface)))
(should (eq scroll (gethash scroll-id ebox--scroll-global-state)))
(should (equal-including-properties before (buffer-string)))
(should (equal previous-report (tp-surface-report surface)))
(let ((report
(ebox-commit
(current-buffer)
(ebox-child-range-test--mixed-followup-candidate
(current-buffer) 2))))
(ebox-child-range-test--assert-full-render-equivalent (current-buffer))
(should (= (1+ revision) (tp-surface-revision surface)))
(should (eq (plist-get report :projection-kind) 'mixed-owner-reflow))
(should-not (plist-get report :tp-full-root))
(should (plist-get (tp-surface-report surface) :commit-batch))
(should (plist-get (tp-surface-report surface) :retained-mount-state))
(should (eq mounts (tp--surface-mounts surface)))
(should (equal mount-ids (mapcar #'tp--surface-mount-id mounts)))
(should (eq mount-index (tp--surface-mount-index surface)))
(should (eq index (tp--surface-index surface)))
(should (eq (plist-get scroll :content-lines)
(plist-get (gethash scroll-id ebox--scroll-global-state)
:content-lines)))))))
(ert-deftest ebox-child-range-mixed-followup-removes-face-without-contribution ()
"A paint owner with no new face still publishes its baseline-only removal."
(with-temp-buffer
(ebox-render-to-buffer
(current-buffer)
(ebox-test-column :width '(200)
(ebox-test-column :bgcolor "#DDE7EF"
(ebox-test-text (propertize "title-1" 'face '(:weight bold))
:key 'title :source-identity 'title))
(ebox-test-box :key 'row-1 :source-identity 'row-1 :color "#123456"
(ebox-test-text (propertize "row-1" 'face '(:slant italic))))))
(let* ((surface ebox-surface--buffer-surface)
(mounts (tp--surface-mounts surface))
(mount-index (tp--surface-mount-index surface))
(candidate (ebox-candidate-begin (current-buffer)))
(observed nil) (contributions 'not-observed)
(observer
(lambda (_context _projection state _output &optional kind)
(when (eq kind 'mixed-owner-reflow)
(setq observed t
contributions (plist-get state :paint-property-contributions)))))
report)
(ebox-candidate-replace-host-ref
candidate 'title
(ebox-test-text (propertize "title-2" 'face '(:weight bold))
:key 'title :source-identity 'title))
(ebox-candidate-replace-host-ref
candidate 'row-1
(ebox-test-box :key 'row-1 :source-identity 'row-1
(ebox-test-text (propertize "row-1" 'face '(:slant italic)))))
(advice-add 'ebox-surface--projection-result :before observer)
(unwind-protect
(setq report (ebox-commit (current-buffer) candidate))
(advice-remove 'ebox-surface--projection-result observer))
(ebox-child-range-test--assert-full-render-equivalent (current-buffer))
(should observed)
(should-not contributions)
(should (eq (plist-get report :projection-kind) 'mixed-owner-reflow))
(should (equal (get-text-property (string-match "row-1" (buffer-string))
'face (buffer-string))
'(:slant italic)))
(should (plist-get (tp-surface-report surface) :commit-batch))
(should (plist-get (tp-surface-report surface) :retained-mount-state))
(should (eq mounts (tp--surface-mounts surface)))
(should (eq mount-index (tp--surface-mount-index surface))))))
(ert-deftest ebox-child-range-mixed-followup-coordinate-change-keeps-fallback ()
"A longer title cannot claim the stable-coordinate mount-reuse proof."
(with-temp-buffer
(ebox-render-to-buffer
(current-buffer) (ebox-child-range-test--snapshot-replacement-root))
(ebox-commit
(current-buffer)
(ebox-child-range-test--replace-snapshot-detail (current-buffer) t))
(let ((extent (buffer-size)))
(ebox-commit
(current-buffer)
(ebox-child-range-test--mixed-followup-candidate
(current-buffer) 2 "title-2 with a longer extent"))
(should-not (= extent (buffer-size))))
(ebox-child-range-test--assert-full-render-equivalent (current-buffer))
(should-not (plist-get (tp-surface-report ebox-surface--buffer-surface)
:retained-mount-state))))
(ert-deftest ebox-child-range-mixed-followup-patch-layers-require-complete-coverage ()
"Ordered paint may cross adjacent patches, but cannot disappear into a gap."
(let* ((baseline (propertize "abcdefgh" 'face 'bold 'help-echo "base"))
(patches
(mapcar (lambda (range)
(list :old-start (car range) :old-end (cdr range)
:new-start (car range) :new-end (cdr range)
:replacement (substring baseline (car range) (cdr range))))
'((0 . 2) (2 . 4) (6 . 8))))
(before (copy-tree patches))
(layers '((:start 0 :end 4 :props (face italic))
(:start 1 :end 3 :props (face nil help-echo nil))
(:start 6 :end 8 :props (face (:foreground "red")))))
(mapped (ebox-surface--patch-property-contributions patches layers 8))
(batch (tp-commit-batch-create
:base-revision 1 :target-revision 2 :base-extent 8 :target-extent 8
:patches mapped))
(stored (tp-commit-batch-patches batch))
(actual (concat (plist-get (nth 0 stored) :replacement)
(plist-get (nth 1 stored) :replacement)
(substring baseline 4 6)
(plist-get (nth 2 stored) :replacement))))
(should (equal-including-properties
actual (tp--compose-relative-property-contributions baseline layers)))
(should (equal-including-properties before patches))
(should-not (eq (car patches) (car mapped)))
(should-not
(ebox-surface--patch-property-contributions
patches '((:start 1 :end 7 :props (face italic))) 8))
(dolist (invalid '((:start -1 :end 1 :props (face bold))
(:start 0 :end 9 :props (face bold))
(:start 0 :end 1 :props (face))))
(should-not
(ebox-surface--patch-property-contributions patches (list invalid) 8)))
(should
(ebox-surface--patch-property-contributions
patches '((:start 5 :end 5 :props (face bold))) 8))))
(ert-deftest ebox-child-range-mixed-followup-proof-misses-preserve-fallback ()
"Missing ownership or paint coverage returns to exact retained-content work."
(dolist (miss '(ownership coverage))
(with-temp-buffer
(ebox-render-to-buffer
(current-buffer) (ebox-child-range-test--snapshot-replacement-root))
(ebox-commit
(current-buffer)
(ebox-child-range-test--replace-snapshot-detail (current-buffer) t))
(let* ((function (if (eq miss 'ownership)
'ebox-surface--span-patch-output
'ebox-surface--patch-property-contributions))
observed
(observer
(lambda (original &rest arguments)
(setq observed t)
(when (eq miss 'ownership)
(prog1 (apply original arguments)
(cl-remf (nth 2 arguments) :span-patch-retained-owned-ranges))))))
(advice-add function :around observer)
(unwind-protect
(ebox-commit
(current-buffer)
(ebox-child-range-test--mixed-followup-candidate (current-buffer) 2))
(advice-remove function observer))
(should observed)
(ebox-child-range-test--assert-full-render-equivalent (current-buffer))
(should-not (plist-get (tp-surface-report ebox-surface--buffer-surface)
:retained-mount-state))))))
(ert-deftest ebox-child-range-mixed-followup-merger-errors-escape-proof-fallback ()
"Even a constructor-like merger error propagates once and preserves state."
(with-temp-buffer
(ebox-render-to-buffer
(current-buffer) (ebox-child-range-test--snapshot-replacement-root))
(ebox-commit
(current-buffer)
(ebox-child-range-test--replace-snapshot-detail (current-buffer) t))
(let* ((surface ebox-surface--buffer-surface)
(state (ebox--buffer-render-state (current-buffer)))
(revision (tp-surface-revision surface))
(mounts (tp--surface-mounts surface))
(index (tp--surface-mount-index surface))
(before (buffer-string))
(merge-calls 0) (contributed-batches 0) contributed-face
(observer
(lambda (original &rest arguments)
(let* ((patches (mapcar #'copy-sequence (plist-get arguments :patches)))
(patch (cl-find-if
(lambda (entry) (plist-get entry :property-contributions))
patches)))
(when patch
;; Exception-boundary fixture: TP merges only over a present
;; baseline property. Keep Ebox's actual generated layers,
;; adding a baseline only to a detached constructor argument.
;; Inline row faces would change the fixture's geometry proof.
(let* ((layer (car (plist-get patch :property-contributions)))
(start (plist-get layer :start))
(replacement (copy-sequence (plist-get patch :replacement))))
(should (< start (plist-get layer :end)))
(setq contributed-face (plist-get (plist-get layer :props) 'face))
(should contributed-face)
(put-text-property start (1+ start) 'face '(:slant italic) replacement)
(plist-put patch :replacement replacement)
(setq arguments (plist-put (copy-sequence arguments) :patches patches))
(cl-incf contributed-batches)))
(apply original arguments)))))
(let ((tp--property-policies (copy-hash-table tp--property-policies))
(tp--property-policy-order (copy-sequence tp--property-policy-order)))
(tp-define-property-policy
(tp-text-property-id 'face)
:projector (tp-property-policy-projector
(tp-property-policy (tp-text-property-id 'face)))
:merge (lambda (old new)
(should (equal old '(:slant italic)))
(should (equal new contributed-face))
(cl-incf merge-calls)
(signal 'tp-surface-error '(:commit-patch merger-failure))))
(advice-add 'tp-commit-batch-create :around observer)
(unwind-protect
(let ((condition
(should-error
(ebox-commit
(current-buffer)
(ebox-child-range-test--mixed-followup-candidate
(current-buffer) 2))
:type 'tp-surface-error)))
(should (equal (seq-take condition 3)
'(tp-surface-error :commit-patch merger-failure))))
(advice-remove 'tp-commit-batch-create observer)))
(should (= contributed-batches 1))
(should (= merge-calls 1))
(should (= revision (tp-surface-revision surface)))
(should (eq state (tp-surface-client-state surface)))
(should (eq state (ebox--buffer-render-state (current-buffer))))
(should (eq mounts (tp--surface-mounts surface)))
(should (eq index (tp--surface-mount-index surface)))
(should (equal-including-properties before (buffer-string)))
(ebox-commit
(current-buffer)
(ebox-child-range-test--mixed-followup-candidate (current-buffer) 2))
(ebox-child-range-test--assert-full-render-equivalent (current-buffer))
(should (plist-get (tp-surface-report surface) :retained-mount-state)))))
(ert-deftest ebox-child-range-mixed-followup-role-change-keeps-fallback ()
"Replacing text with a new box role cannot reuse the old mount topology."
(with-temp-buffer
(ebox-render-to-buffer
(current-buffer) (ebox-child-range-test--snapshot-replacement-root))
(ebox-commit
(current-buffer)
(ebox-child-range-test--replace-snapshot-detail (current-buffer) t))
(let ((candidate
(ebox-child-range-test--mixed-followup-candidate (current-buffer) 2)))
(ebox-candidate-replace-host-ref
candidate 'title
(ebox-test-box :key 'title :source-identity 'title
(ebox-test-text (propertize "title-2" 'face '(:weight bold)))))
(ebox-commit (current-buffer) candidate))
(ebox-child-range-test--assert-full-render-equivalent (current-buffer))
(should-not (plist-get (tp-surface-report ebox-surface--buffer-surface)
:retained-mount-state))))
(ert-deftest ebox-child-range-does-not-reuse-disappeared-key-positionally ()
"A new keyed item must not inherit a removed peer's runtime identity."
(let ((buffer (generate-new-buffer " *ebox-range-key-reentry*")))