perf: retain local text updates beside independent scroll regions
This commit is contained in:
parent
dfa097e988
commit
21cf79cdfa
@ -7903,6 +7903,12 @@ suppresses the ordinary fallback when any fact is missing."
|
|||||||
:retain-external-owner-suffix-p t)))
|
:retain-external-owner-suffix-p t)))
|
||||||
(when (and geometry-proof paint-valid-p disjoint-p
|
(when (and geometry-proof paint-valid-p disjoint-p
|
||||||
allocation-disjoint-p cascade-safe-p
|
allocation-disjoint-p cascade-safe-p
|
||||||
|
;; The single-owner span proof can retain unrelated scroll
|
||||||
|
;; subtrees. Its geometry partition does not prove that this
|
||||||
|
;; mixed transaction's paint owners leave scroll caches intact.
|
||||||
|
(null (plist-get old-state :scroll-region-ids))
|
||||||
|
(null (ebox-incremental--hash-keys
|
||||||
|
(plist-get prepared :scroll-state-table)))
|
||||||
(not (plist-get prepared :scroll-state-transaction))
|
(not (plist-get prepared :scroll-state-transaction))
|
||||||
(not (plist-get prepared :scroll-patch-fast-p))
|
(not (plist-get prepared :scroll-patch-fast-p))
|
||||||
root-id
|
root-id
|
||||||
@ -9282,6 +9288,39 @@ the surface renderer and role/overflow validators remain the final proof."
|
|||||||
(plist-get slot :start-pixel)))
|
(plist-get slot :start-pixel)))
|
||||||
slots))))))
|
slots))))))
|
||||||
|
|
||||||
|
(defun ebox-incremental--span-owner-disjoint-from-scroll-p
|
||||||
|
(buffer old-state prepared candidate-state owner-id)
|
||||||
|
"Return non-nil when OWNER-ID cannot change any retained scroll content.
|
||||||
|
The active scroll set and each scroll box identity must survive preparation.
|
||||||
|
An ancestor or descendant scroll box still requires the ordinary renderer to
|
||||||
|
regenerate its cached lines; only separate branches may keep those lines."
|
||||||
|
(let* ((regions (plist-get old-state :scroll-region-ids))
|
||||||
|
(next-table (plist-get prepared :scroll-state-table)))
|
||||||
|
(and (= (length regions)
|
||||||
|
(if (hash-table-p next-table) (hash-table-count next-table) 0))
|
||||||
|
(cl-every
|
||||||
|
(lambda (region)
|
||||||
|
(let* ((old-scroll (gethash region ebox--scroll-global-state))
|
||||||
|
(next-scroll (and next-table (gethash region next-table)))
|
||||||
|
(old-box (plist-get old-scroll :box))
|
||||||
|
(next-box (plist-get next-scroll :box))
|
||||||
|
(scroll-id (plist-get old-box :node-id)))
|
||||||
|
(and scroll-id next-box
|
||||||
|
(eq (plist-get old-scroll :buffer) buffer)
|
||||||
|
(equal scroll-id (plist-get next-box :node-id))
|
||||||
|
(eq old-box (gethash scroll-id (plist-get old-state :node-table)))
|
||||||
|
(eq old-box (gethash region (plist-get old-state :region-box-table)))
|
||||||
|
(eq next-box (gethash scroll-id (plist-get candidate-state :node-table)))
|
||||||
|
(eq next-box (gethash region (plist-get candidate-state :region-box-table)))
|
||||||
|
(not (equal scroll-id owner-id))
|
||||||
|
(not (ebox--runtime-ancestor-id-p buffer scroll-id owner-id))
|
||||||
|
(not (ebox--runtime-ancestor-id-p buffer owner-id scroll-id))
|
||||||
|
(let ((ebox-incremental--buffer-render-state-override
|
||||||
|
(cons buffer candidate-state)))
|
||||||
|
(and (not (ebox--runtime-ancestor-id-p buffer scroll-id owner-id))
|
||||||
|
(not (ebox--runtime-ancestor-id-p buffer owner-id scroll-id)))))))
|
||||||
|
regions))))
|
||||||
|
|
||||||
(defun ebox-incremental--single-span-patch-projection-proof
|
(defun ebox-incremental--single-span-patch-projection-proof
|
||||||
(buffer old-state prepared candidate-state owner-plan
|
(buffer old-state prepared candidate-state owner-plan
|
||||||
&optional path-copied-p)
|
&optional path-copied-p)
|
||||||
@ -9411,10 +9450,22 @@ metadata; this predicate only authorizes the local attempt."
|
|||||||
local-typography-p)
|
local-typography-p)
|
||||||
(ebox-incremental--cascade-local-owner-proof-p
|
(ebox-incremental--cascade-local-owner-proof-p
|
||||||
old-state candidate-state (list dirty))))
|
old-state candidate-state (list dirty))))
|
||||||
(null (plist-get old-state :scroll-region-ids))
|
(ebox-incremental--span-owner-disjoint-from-scroll-p
|
||||||
(or (null (plist-get candidate-state :scroll-region-ids))
|
buffer old-state prepared candidate-state owner-id)
|
||||||
(null (ebox-incremental--hash-keys
|
;; Admit the new scroll coexistence case only for an exact
|
||||||
(plist-get candidate-state :scroll-state-table))))
|
;; content extent. Length or width changes can alter an
|
||||||
|
;; ancestor's line filler outside the local owner's spans.
|
||||||
|
(or (null (plist-get old-state :scroll-region-ids))
|
||||||
|
(let ((before (plist-get (plist-get dirty :old-signature)
|
||||||
|
:content))
|
||||||
|
(after (plist-get (plist-get dirty :new-signature)
|
||||||
|
:content)))
|
||||||
|
(and (stringp before) (stringp after)
|
||||||
|
(not (cl-intersection
|
||||||
|
changed-keys ebox--typography-style-signature-keys))
|
||||||
|
(= (length before) (length after))
|
||||||
|
(= (ebox--string-pixel-width before)
|
||||||
|
(ebox--string-pixel-width after)))))
|
||||||
;; The ancestor allocation proof is tied to the published
|
;; The ancestor allocation proof is tied to the published
|
||||||
;; viewport slot. A declarative commit does not alter that
|
;; viewport slot. A declarative commit does not alter that
|
||||||
;; context; final footprint validation still rejects a miss.
|
;; context; final footprint validation still rejects a miss.
|
||||||
@ -9465,6 +9516,10 @@ metadata; this predicate only authorizes the local attempt."
|
|||||||
:ancestor-slot-signature ancestor-slot-signature
|
:ancestor-slot-signature ancestor-slot-signature
|
||||||
:variable-content-p
|
:variable-content-p
|
||||||
(and snapshot
|
(and snapshot
|
||||||
|
;; With retained scroll siblings, the final rendered
|
||||||
|
;; footprint must remain exact even when raw content has
|
||||||
|
;; equal width under the current buffer's default font.
|
||||||
|
(null (plist-get old-state :scroll-region-ids))
|
||||||
(memq :content changed-keys)
|
(memq :content changed-keys)
|
||||||
(stringp
|
(stringp
|
||||||
(plist-get (plist-get dirty :new-signature) :content))
|
(plist-get (plist-get dirty :new-signature) :content))
|
||||||
|
|||||||
@ -5630,10 +5630,17 @@ published runtime and must be consumed without clearing those shared nodes."
|
|||||||
context projection state
|
context projection state
|
||||||
(ebox-surface--paint-fragments
|
(ebox-surface--paint-fragments
|
||||||
previous-state state dirty-set old-text)))
|
previous-state state dirty-set old-text)))
|
||||||
(unless (memq projection-kind
|
(cond
|
||||||
'(scroll-patch formatting-context-reflow
|
((memq projection-kind '(span-patch owner-scoped))
|
||||||
mixed-owner-reflow))
|
;; A local owner render visits only its own regions. Keep the
|
||||||
(plist-put state :region-box-table (make-hash-table :test 'equal)))
|
;; complete candidate registry, including disjoint retained scroll
|
||||||
|
;; boxes, in a private table until the transaction is accepted.
|
||||||
|
(plist-put state :region-box-table
|
||||||
|
(copy-hash-table (plist-get state :region-box-table))))
|
||||||
|
((not (memq projection-kind
|
||||||
|
'(scroll-patch formatting-context-reflow
|
||||||
|
mixed-owner-reflow)))
|
||||||
|
(plist-put state :region-box-table (make-hash-table :test 'equal))))
|
||||||
(let* ((_initial-native
|
(let* ((_initial-native
|
||||||
(when initial-native-p
|
(when initial-native-p
|
||||||
;; Initial mount has no incremental planner. Surface owns
|
;; Initial mount has no incremental planner. Surface owns
|
||||||
|
|||||||
@ -40,6 +40,218 @@
|
|||||||
"Return one stable declarative root containing CONTENT."
|
"Return one stable declarative root containing CONTENT."
|
||||||
(ebox-test-box :key 'root (ebox-test-text content)))
|
(ebox-test-box :key 'root (ebox-test-text content)))
|
||||||
|
|
||||||
|
(defun ebox-commit-test--scroll-sibling-root (content &optional nested)
|
||||||
|
"Return CONTENT beside an active scroll box, or inside it when NESTED."
|
||||||
|
(let ((label (ebox-test-text content :key 'label :source-identity 'label)))
|
||||||
|
(ebox-test-column :key 'root :width '(120)
|
||||||
|
(unless nested label)
|
||||||
|
(ebox-test-box :key 'scroll :id "scroll" :height 2 :width '(80)
|
||||||
|
:overflow 'scroll
|
||||||
|
(ebox-test-column
|
||||||
|
(when nested label)
|
||||||
|
(ebox-test-text "line-a\nline-b\nline-c\nline-d"
|
||||||
|
:key 'scroll-lines :source-identity 'scroll-lines
|
||||||
|
:color "#123456"))))))
|
||||||
|
|
||||||
|
(defun ebox-commit-test--replace-scroll-sibling-label (buffer content)
|
||||||
|
"Commit CONTENT into BUFFER's stable label host."
|
||||||
|
(let ((candidate (ebox-candidate-begin buffer)))
|
||||||
|
(ebox-candidate-replace-host-ref
|
||||||
|
candidate 'label
|
||||||
|
(ebox-test-text content :key 'label :source-identity 'label))
|
||||||
|
(ebox-commit buffer candidate)))
|
||||||
|
|
||||||
|
(ert-deftest ebox-commit-local-content-retains-disjoint-scroll ()
|
||||||
|
"An unrelated scroll box keeps its caches and registry across a local edit."
|
||||||
|
(with-temp-buffer
|
||||||
|
(ebox-render-to-buffer
|
||||||
|
(current-buffer) (ebox-commit-test--scroll-sibling-root "old"))
|
||||||
|
(let* ((before (ebox--buffer-render-state (current-buffer)))
|
||||||
|
(scroll-id (car (plist-get before :scroll-region-ids)))
|
||||||
|
(old-scroll (gethash scroll-id ebox--scroll-global-state))
|
||||||
|
(raw (plist-get old-scroll :content-lines))
|
||||||
|
(rendered (plist-get old-scroll :rendered-content-lines))
|
||||||
|
(region-count (hash-table-count (plist-get before :region-box-table)))
|
||||||
|
(root-render (symbol-function 'ebox-surface--render-candidate))
|
||||||
|
(root-renders 0)
|
||||||
|
report)
|
||||||
|
(should scroll-id)
|
||||||
|
(cl-letf (((symbol-function 'ebox-surface--render-candidate)
|
||||||
|
(lambda (state)
|
||||||
|
(cl-incf root-renders)
|
||||||
|
(funcall root-render state))))
|
||||||
|
(setq report
|
||||||
|
(ebox-commit-test--replace-scroll-sibling-label
|
||||||
|
(current-buffer) "new")))
|
||||||
|
(should (memq (plist-get report :projection-kind) '(span-patch owner-scoped)))
|
||||||
|
(should (zerop root-renders))
|
||||||
|
(should-not (plist-get report :tp-full-root))
|
||||||
|
(let* ((after (ebox--buffer-render-state (current-buffer)))
|
||||||
|
(scroll (gethash scroll-id ebox--scroll-global-state)))
|
||||||
|
(should (= region-count (hash-table-count (plist-get after :region-box-table))))
|
||||||
|
(should (eq (plist-get scroll :box) (gethash scroll-id ebox--region-box-table)))
|
||||||
|
(should (eq raw (plist-get scroll :content-lines)))
|
||||||
|
(should (eq rendered (plist-get scroll :rendered-content-lines))))
|
||||||
|
(should (= 1 (ebox--scroll-region-by scroll-id 1)))
|
||||||
|
(should (string-match-p "new" (buffer-string)))
|
||||||
|
(should (string-match-p "line-c" (buffer-string)))
|
||||||
|
(should (= -1 (ebox--scroll-region-by scroll-id -1)))
|
||||||
|
(should (string-match-p "new" (buffer-string)))
|
||||||
|
(should (string-match-p "line-a" (buffer-string))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-commit-disjoint-scroll-rolls-back-with-local-content ()
|
||||||
|
"Late rejection restores both the local content and the scroll registry."
|
||||||
|
(with-temp-buffer
|
||||||
|
(ebox-render-to-buffer
|
||||||
|
(current-buffer) (ebox-commit-test--scroll-sibling-root "old"))
|
||||||
|
(let* ((state (ebox--buffer-render-state (current-buffer)))
|
||||||
|
(scroll-id (car (plist-get state :scroll-region-ids)))
|
||||||
|
(scroll (gethash scroll-id ebox--scroll-global-state))
|
||||||
|
(box (gethash scroll-id ebox--region-box-table))
|
||||||
|
(before (buffer-string)))
|
||||||
|
(cl-letf (((symbol-function 'accept-change-group)
|
||||||
|
(lambda (_) (error "Reject local content publication"))))
|
||||||
|
(should-error
|
||||||
|
(ebox-commit-test--replace-scroll-sibling-label (current-buffer) "new")))
|
||||||
|
(should (eq state (ebox--buffer-render-state (current-buffer))))
|
||||||
|
(should (eq scroll (gethash scroll-id ebox--scroll-global-state)))
|
||||||
|
(should (eq box (gethash scroll-id ebox--region-box-table)))
|
||||||
|
(should (equal-including-properties before (buffer-string)))
|
||||||
|
(should (= 1 (ebox--scroll-region-by scroll-id 1)))
|
||||||
|
(should (string-match-p "old" (buffer-string)))
|
||||||
|
(ebox-commit-test--replace-scroll-sibling-label (current-buffer) "new")
|
||||||
|
(should (= -1 (ebox--scroll-region-by scroll-id -1)))
|
||||||
|
(should (string-match-p "new" (buffer-string))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-commit-local-content-inside-scroll-keeps-full-cache-update ()
|
||||||
|
"A scrolling ancestor still needs its retained content regenerated."
|
||||||
|
(with-temp-buffer
|
||||||
|
(ebox-render-to-buffer
|
||||||
|
(current-buffer) (ebox-commit-test--scroll-sibling-root "old" t))
|
||||||
|
(let* ((state (ebox--buffer-render-state (current-buffer)))
|
||||||
|
(scroll-id (car (plist-get state :scroll-region-ids)))
|
||||||
|
(report (ebox-commit-test--replace-scroll-sibling-label
|
||||||
|
(current-buffer) "new")))
|
||||||
|
(should-not (memq (plist-get report :projection-kind) '(span-patch owner-scoped)))
|
||||||
|
(should (= 1 (ebox--scroll-region-by scroll-id 1)))
|
||||||
|
(should (= -1 (ebox--scroll-region-by scroll-id -1)))
|
||||||
|
(should (string-match-p "new" (buffer-string)))
|
||||||
|
(should-not (string-match-p "old" (buffer-string))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-commit-disjoint-scroll-proof-rejects-incomplete-or-overlapping-state ()
|
||||||
|
"Missing mappings, changed membership and either ancestor direction reject."
|
||||||
|
(with-temp-buffer
|
||||||
|
(ebox-render-to-buffer
|
||||||
|
(current-buffer) (ebox-commit-test--scroll-sibling-root "old"))
|
||||||
|
(let* ((state (ebox--buffer-render-state (current-buffer)))
|
||||||
|
(owner (plist-get (ebox--host-ref-node (current-buffer) 'label) :node-id))
|
||||||
|
(root (plist-get (plist-get state :root-node) :node-id))
|
||||||
|
(region (car (plist-get state :scroll-region-ids)))
|
||||||
|
(scroll (gethash region ebox--scroll-global-state))
|
||||||
|
(scroll-id (plist-get (plist-get scroll :box) :node-id))
|
||||||
|
(table (make-hash-table :test 'equal))
|
||||||
|
(prepared (list :scroll-state-table table))
|
||||||
|
(candidate (copy-sequence state)))
|
||||||
|
(puthash region scroll table)
|
||||||
|
(should (ebox-incremental--span-owner-disjoint-from-scroll-p
|
||||||
|
(current-buffer) state prepared candidate owner))
|
||||||
|
(should-not (ebox-incremental--span-owner-disjoint-from-scroll-p
|
||||||
|
(current-buffer) state prepared candidate root))
|
||||||
|
(should-not (ebox-incremental--span-owner-disjoint-from-scroll-p
|
||||||
|
(current-buffer) state prepared candidate scroll-id))
|
||||||
|
(remhash region table)
|
||||||
|
(should-not (ebox-incremental--span-owner-disjoint-from-scroll-p
|
||||||
|
(current-buffer) state prepared candidate owner))
|
||||||
|
(puthash 'different-region scroll table)
|
||||||
|
(should-not (ebox-incremental--span-owner-disjoint-from-scroll-p
|
||||||
|
(current-buffer) state prepared candidate owner))
|
||||||
|
(clrhash table)
|
||||||
|
(puthash region scroll table)
|
||||||
|
(let ((regions (copy-hash-table (plist-get state :region-box-table))))
|
||||||
|
(remhash region regions)
|
||||||
|
(plist-put candidate :region-box-table regions)
|
||||||
|
(should-not (ebox-incremental--span-owner-disjoint-from-scroll-p
|
||||||
|
(current-buffer) state prepared candidate owner)))
|
||||||
|
(plist-put candidate :region-box-table (plist-get state :region-box-table))
|
||||||
|
(let ((parents (copy-hash-table (plist-get state :parent-table))))
|
||||||
|
(puthash scroll-id owner parents)
|
||||||
|
(plist-put candidate :parent-table parents)
|
||||||
|
(should-not (ebox-incremental--span-owner-disjoint-from-scroll-p
|
||||||
|
(current-buffer) state prepared candidate owner))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-commit-content-shift-preserves-later-scroll-targets ()
|
||||||
|
"Longer and shorter local text retain the later scroll widget's targets."
|
||||||
|
(with-temp-buffer
|
||||||
|
(ebox-render-to-buffer
|
||||||
|
(current-buffer) (ebox-commit-test--scroll-sibling-root "old"))
|
||||||
|
(let ((scroll-id (car (plist-get (ebox--buffer-render-state (current-buffer))
|
||||||
|
:scroll-region-ids))))
|
||||||
|
(dolist (value '("longer" "x"))
|
||||||
|
(ebox-commit-test--replace-scroll-sibling-label (current-buffer) value)
|
||||||
|
(should (= 1 (ebox--scroll-region-by scroll-id 1)))
|
||||||
|
(should (string-match-p (concat "\\`" value) (buffer-string)))
|
||||||
|
(should (string-match-p "line-c" (buffer-string)))
|
||||||
|
(should (= -1 (ebox--scroll-region-by scroll-id -1)))
|
||||||
|
(should (string-match-p (concat "\\`" value) (buffer-string)))
|
||||||
|
(should (string-match-p "line-a" (buffer-string)))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-commit-mixed-content-and-scroll-paint-refreshes-cache ()
|
||||||
|
"An unrelated label edit cannot authorize stale paint in a scroll cache."
|
||||||
|
(with-temp-buffer
|
||||||
|
(ebox-render-to-buffer
|
||||||
|
(current-buffer) (ebox-commit-test--scroll-sibling-root "old"))
|
||||||
|
(let* ((scroll-id (car (plist-get (ebox--buffer-render-state (current-buffer))
|
||||||
|
:scroll-region-ids)))
|
||||||
|
(candidate (ebox-candidate-begin (current-buffer))))
|
||||||
|
(ebox-candidate-replace-host-ref
|
||||||
|
candidate 'label (ebox-test-text "new" :key 'label :source-identity 'label))
|
||||||
|
(ebox-candidate-replace-host-ref
|
||||||
|
candidate 'scroll-lines
|
||||||
|
(ebox-test-text "line-a\nline-b\nline-c\nline-d"
|
||||||
|
:key 'scroll-lines :source-identity 'scroll-lines
|
||||||
|
:color "#654321"))
|
||||||
|
(let ((report (ebox-commit (current-buffer) candidate)))
|
||||||
|
(should-not (eq (plist-get report :projection-kind) 'mixed-owner-reflow)))
|
||||||
|
(dotimes (_ 2)
|
||||||
|
(goto-char (point-min))
|
||||||
|
(search-forward "line-a")
|
||||||
|
(should (equal "#654321"
|
||||||
|
(ebox-commit-test--face-value
|
||||||
|
(get-text-property (1- (point)) 'face) :foreground)))
|
||||||
|
(should (= 1 (ebox--scroll-region-by scroll-id 1)))
|
||||||
|
(should (= -1 (ebox--scroll-region-by scroll-id -1)))))))
|
||||||
|
|
||||||
|
(ert-deftest ebox-commit-disjoint-scroll-validates-rendered-font-extent ()
|
||||||
|
"Equal raw widths do not authorize changed rendered glyph widths."
|
||||||
|
(let ((measure (symbol-function 'ebox--string-pixel-width)))
|
||||||
|
;; Model an unchanged proportional font: raw WWW/iii both measure three
|
||||||
|
;; units, but styled iii is narrower. The final painted line must still
|
||||||
|
;; include exactly the filler required by its 120px parent allocation.
|
||||||
|
(cl-letf (((symbol-function 'ebox--string-pixel-width)
|
||||||
|
(lambda (string)
|
||||||
|
(let ((width (funcall measure string)) (start 0))
|
||||||
|
(while (string-match "iii" string start)
|
||||||
|
(when (get-text-property (match-beginning 0) 'face string)
|
||||||
|
(cl-decf width))
|
||||||
|
(setq start (match-end 0)))
|
||||||
|
width))))
|
||||||
|
(with-temp-buffer
|
||||||
|
(ebox-render-to-buffer
|
||||||
|
(current-buffer)
|
||||||
|
(ebox-test-column :key 'root :width '(120)
|
||||||
|
(ebox-test-text "WWW" :font-size 16 :key 'label :source-identity 'label)
|
||||||
|
(ebox-test-box :height 1 :overflow 'scroll
|
||||||
|
(ebox-test-text "first\nsecond"))))
|
||||||
|
(let ((candidate (ebox-candidate-begin (current-buffer))))
|
||||||
|
(ebox-candidate-replace-host-ref
|
||||||
|
candidate 'label
|
||||||
|
(ebox-test-text "iii" :font-size 16 :key 'label :source-identity 'label))
|
||||||
|
(ebox-commit (current-buffer) candidate))
|
||||||
|
(goto-char (point-min))
|
||||||
|
(should (looking-at "iii"))
|
||||||
|
(should (= 120 (ebox--string-pixel-width
|
||||||
|
(buffer-substring (point) (line-end-position)))))))))
|
||||||
|
|
||||||
(defun ebox-commit-test--hash-facts (table &optional values)
|
(defun ebox-commit-test--hash-facts (table &optional values)
|
||||||
"Return sorted TABLE keys, or key/value pairs when VALUES is non-nil."
|
"Return sorted TABLE keys, or key/value pairs when VALUES is non-nil."
|
||||||
(let (facts)
|
(let (facts)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user