diff --git a/ebox-surface.el b/ebox-surface.el index 56de05c..d0f89aa 100644 --- a/ebox-surface.el +++ b/ebox-surface.el @@ -3370,6 +3370,21 @@ old allocated width while the owner changes its natural content length." (setq position (max (1+ position) next))))))) (and valid seen common))) +(defun ebox-surface--owner-needs-ancestor-paint-p (state owner-id) + "Return non-nil if STATE's detached OWNER-ID omits enclosing effects. +Ordinary span publication does not recompose enclosing paint or surface +properties. Its local renderer cannot prove equivalence for such an owner." + (let ((parents (plist-get state :parent-table)) + (nodes (plist-get state :node-table)) + (node-id owner-id) + needed) + (while (and (not needed) (setq node-id (gethash node-id parents))) + (when-let* ((box (ebox-fragment-style-source-node (gethash node-id nodes)))) + (setq needed + (or (ebox-get box :surface-properties) + (ebox-surface--region-face-contributions box '(bt bb) t))))) + (not (null needed)))) + (defun ebox-surface--owner-patch-candidate (buffer state owner-id allocated-width &optional variable-content-p variable-content-max-width @@ -3409,7 +3424,15 @@ old allocated width while the owner changes its natural content length." (or variable-content-p role-owned-lines-p)) (plist-put (copy-sequence node) :width allocated-width) node)))) - (when (and old-snapshot spans node render-node) + (when (and old-snapshot spans node render-node + ;; Mixed projection has a separate paint recomposition step. + ;; Ordinary spans have only the detached owner's rendered + ;; faces; retain the full renderer when enclosing paint would + ;; otherwise disappear from changed text. + (or (not (memq (plist-get state :projection-kind) + '(span-patch owner-scoped))) + (not (ebox-surface--owner-needs-ancestor-paint-p + state owner-id)))) (let* ((rendered (prog1 (ebox-surface--render-candidate-node state render-node) @@ -3543,7 +3566,9 @@ old allocated width while the owner changes its natural content length." (mapcar (lambda (proof) (if (plist-get proof :range-splice-p) - (ebox-surface--range-patch-candidate buffer state proof) + (unless (ebox-surface--owner-needs-ancestor-paint-p + state (plist-get proof :owner-id)) + (ebox-surface--range-patch-candidate buffer state proof)) (ebox-surface--owner-patch-candidate buffer state (plist-get proof :owner-id) (plist-get proof :allocated-width) diff --git a/tests/ebox-commit-tests.el b/tests/ebox-commit-tests.el index 435455b..3db53ce 100644 --- a/tests/ebox-commit-tests.el +++ b/tests/ebox-commit-tests.el @@ -276,6 +276,63 @@ (ebox-commit-test--face-value (get-text-property (1- (point)) 'face) :foreground))))) +(ert-deftest ebox-commit-content-patch-retains-ancestor-paint () + "Changing text keeps the paint supplied by its containing boxes." + (with-temp-buffer + (ebox-render-to-buffer + (current-buffer) + (ebox-test-column :width '(200) :bgcolor "#FFFDF8" + (ebox-test-box :bgcolor "#E0E8E0" + (ebox-test-text "10" :key 'counter :source-identity 'counter)) + (ebox-test-text "untouched"))) + (let ((candidate (ebox-candidate-begin (current-buffer)))) + (ebox-candidate-replace-host-ref + candidate 'counter + (ebox-test-text "20" :key 'counter :source-identity 'counter)) + (ebox-commit (current-buffer) candidate)) + (let* ((state (ebox--buffer-render-state (current-buffer))) + (expected (ebox--render-node (plist-get state :root-node) + (plist-get state :source-index)))) + (should (equal-including-properties expected (buffer-string)))))) + +(ert-deftest ebox-commit-content-patch-retains-ancestor-surface-properties () + "Changing text retains properties supplied by its containing box." + (with-temp-buffer + (ebox-render-to-buffer + (current-buffer) + (ebox-test-column :width '(200) :surface-properties '(help-echo "parent") + (ebox-test-text "10" :key 'counter :source-identity 'counter) + (ebox-test-text "untouched"))) + (let ((candidate (ebox-candidate-begin (current-buffer)))) + (ebox-candidate-replace-host-ref + candidate 'counter + (ebox-test-text "20" :key 'counter :source-identity 'counter)) + (ebox-commit (current-buffer) candidate)) + (let* ((state (ebox--buffer-render-state (current-buffer))) + (expected (ebox--render-node (plist-get state :root-node) + (plist-get state :source-index)))) + (should (equal-including-properties expected (buffer-string)))))) + +(ert-deftest ebox-commit-range-patch-retains-ancestor-paint () + "An equal-line Range replacement retains enclosing paint." + (with-temp-buffer + (ebox-render-to-buffer + (current-buffer) + (ebox-test-column :width '(200) :bgcolor "#FFFDF8" + (ebox-test-box :width '(80) :height 1 + (ebox-test-column + (ebox-test-child-range 'items + (ebox-test-text "10" :key 'old)))) + (ebox-test-text "untouched"))) + (let ((candidate (ebox-candidate-begin (current-buffer)))) + (ebox-candidate-replace-range-ref + candidate 'items (ebox-test-text "20" :key 'new)) + (ebox-commit (current-buffer) candidate)) + (let* ((state (ebox--buffer-render-state (current-buffer))) + (expected (ebox--render-node (plist-get state :root-node) + (plist-get state :source-index)))) + (should (equal-including-properties expected (buffer-string)))))) + (ert-deftest ebox-commit-paint-retains-disjoint-scroll () "Pure sibling paint retains scroll caches and stays painted after scrolling." (with-temp-buffer