Subject: bug#76018: 31.0.50; wrap-prefix properties from visual-wrap-prefix-mode proliferate

From: Jim Porter <[email-masked]>
Date: Mon, 2 Jun 2025 17:49:07 -0700

On 6/1/2025 10:58 PM, Eli Zaretskii wrote:
> This is more clear, but please use consistent terminology.  In the
> above, you use "list of values" and "list of display specifications"
> to mean the same thing (AFAIU), and "property" to mean "display
> specification".

Good catch. Now done in this patch series. I've also added a patch 
(0002) to improve the wording of the existing documentation, and 
hopefully finished up everything for fixing the original bug.

For patch 0002, I debated changing the various function names to 
'get-display-specification' and 'add-display-specification', but I left 
them as-is for now. If you think we should change them so the wording is 
more consistent, let me know (of course, I'd add aliases from the old 
names to the new ones too).

From 57e44475f8c10286b8b2a157f14edfc28486fa14 Mon Sep 17 00:00:00 2001
From: Jim Porter <[email-masked]>
Date: Wed, 28 May 2025 09:55:58 -0700
Subject: [PATCH 1/4] Don't delete in-place when replacing a display property

When calling 'add-display-text-property' on a region of text that
already contains PROP, we first delete the old display specification
from the region.  If the region's 'display' property is a list of
display specifications, we need to avoid destructively modifying the
list; other regions of text could be using the same list object.  (For a
'display' property that's a vector or a single display spec, this
doesn't matter since we first make a new list in the code.)

In addition, be more careful when working with a display property like
((margin ...) ...).  This is a single display specification, not a list
of display specs.

* lisp/emacs-lisp/subr-x.el (add-display-text-property): Don't delete
in-place for list values.  Handle (margin ...) display specification
type correctly.

* test/lisp/emacs-lisp/subr-x-tests.el
(subr-x-test-add-display-text-property): Update test.
---
 lisp/emacs-lisp/subr-x.el            | 20 +++++++++++----
 test/lisp/emacs-lisp/subr-x-tests.el | 37 ++++++++++++++++++++++------
 2 files changed, 44 insertions(+), 13 deletions(-)

diff --git a/lisp/emacs-lisp/subr-x.el b/lisp/emacs-lisp/subr-x.el
index eaa8119ead7..4d56c67be5b 100644
--- a/lisp/emacs-lisp/subr-x.el
+++ b/lisp/emacs-lisp/subr-x.el
@@ -438,22 +438,32 @@ add-display-text-property
           (put-text-property sub-start sub-end 'display (list prop value)
                              object)
         ;; We have old properties.
-        (let ((vector nil))
+        (let (type)
           ;; Make disp into a list.
           (setq disp
                 (cond
                  ((vectorp disp)
-                  (setq vector t)
+                  (setq type 'vector)
                   (seq-into disp 'list))
-                 ((not (consp (car disp)))
+                 ((or (not (consp (car-safe disp)))
+                      ;; If disp looks like ((margin ...) ...), that's
+                      ;; still a single display specification.
+                      (eq (caar disp) 'margin))
+                  (setq type 'scalar)
                   (list disp))
                  (t
+                  (setq type 'list)
                   disp)))
           ;; Remove any old instances.
           (when-let* ((old (assoc prop disp)))
-            (setq disp (delete old disp)))
+            ;; If the property value was a list, don't modify the
+            ;; original value in place; it could be used by other
+            ;; regions of text.
+            (setq disp (if (eq type 'list)
+                           (remove old disp)
+                         (delete old disp))))
           (setq disp (cons (list prop value) disp))
-          (when vector
+          (when (eq type 'vector)
             (setq disp (seq-into disp 'vector)))
           ;; Finally update the range.
           (put-text-property sub-start sub-end 'display disp object)))
diff --git a/test/lisp/emacs-lisp/subr-x-tests.el b/test/lisp/emacs-lisp/subr-x-tests.el
index f6675637fef..5ffbe64ae40 100644
--- a/test/lisp/emacs-lisp/subr-x-tests.el
+++ b/test/lisp/emacs-lisp/subr-x-tests.el
@@ -696,18 +696,39 @@ subr-x-test-add-display-text-property
     (insert "Foo bar zot gazonk")
     (add-display-text-property 4 8 'height 2.0)
     (add-display-text-property 2 12 'raise 0.5)
-    (should (equal (get-text-property 2 'display) '(raise 0.5)))
-    (should (equal (get-text-property 5 'display)
-                   '((raise 0.5) (height 2.0))))
-    (should (equal (get-text-property 9 'display) '(raise 0.5))))
+    (add-display-text-property 6 10 'height 1.0)
+    (should (equal-including-properties
+             (buffer-string)
+             #("Foo bar zot gazonk"
+               1 3 (display (raise 0.5))
+               3 5 (display ((raise 0.5) (height 2.0)))
+               5 9 (display ((height 1.0) (raise 0.5)))
+               9 11 (display (raise 0.5))))))
   (with-temp-buffer
     (insert "Foo bar zot gazonk")
     (put-text-property 4 8 'display [(height 2.0)])
     (add-display-text-property 2 12 'raise 0.5)
-    (should (equal (get-text-property 2 'display) '(raise 0.5)))
-    (should (equal (get-text-property 5 'display)
-                   [(raise 0.5) (height 2.0)]))
-    (should (equal (get-text-property 9 'display) '(raise 0.5))))
+    (add-display-text-property 6 10 'height 1.0)
+    (should (equal-including-properties
+             (buffer-string)
+             #("Foo bar zot gazonk"
+               1 3 (display (raise 0.5))
+               3 5 (display [(raise 0.5) (height 2.0)])
+               5 7 (display [(height 1.0) (raise 0.5)])
+               7 9 (display ((height 1.0) (raise 0.5)))
+               9 11 (display (raise 0.5))))))
+  (with-temp-buffer
+    (insert "Foo bar zot gazonk")
+    (add-display-text-property 4 8 '(margin nil) "Hi")
+    (add-display-text-property 2 12 'raise 0.5)
+    (add-display-text-property 6 10 '(margin nil) "Bye")
+    (should (equal-including-properties
+             (buffer-string)
+             #("Foo bar zot gazonk"
+               1 3 (display (raise 0.5))
+               3 5 (display ((raise 0.5) ((margin nil) "Hi")))
+               5 9 (display (((margin nil) "Bye") (raise 0.5)))
+               9 11 (display (raise 0.5))))))
   (with-temp-buffer
     (should (equal-including-properties
              (let ((str (copy-sequence "some useless string")))
-- 
2.25.1

From 468c035511626158af82992bcb71b7361d21eb85 Mon Sep 17 00:00:00 2001
From: Jim Porter <[email-masked]>
Date: Sat, 31 May 2025 12:11:01 -0700
Subject: [PATCH 2/4] Improve documentation for display property functions

Specifically, use the term "display specification" more consistently to
distinguish from "display property", which is the full value of the
'display' text property.

* src/xdisp.c (find_display_property): Rename PROP to SPEC.
(Fget_display_property): Rename PROP to SPEC and improve docstring.

* lisp/emacs-lisp/subr-x.el (add-display-text-property): Rename PROP to
SPEC and improve docstring.

* doc/lispref/display.texi (Display Property): Reword documentation to
more-consistently refer to display specifications.
---
 doc/lispref/display.texi  | 50 +++++++++++++++++++--------------------
 lisp/emacs-lisp/subr-x.el | 21 ++++++++--------
 src/xdisp.c               | 24 ++++++++++---------
 3 files changed, 48 insertions(+), 47 deletions(-)

diff --git a/doc/lispref/display.texi b/doc/lispref/display.texi
index 3b48cb93405..f29364bb4ef 100644
--- a/doc/lispref/display.texi
+++ b/doc/lispref/display.texi
@@ -5257,29 +5257,29 @@ Display Property
 specification, or a list or vector containing several display
 specifications.
 
-@defun get-display-property position prop &optional object properties
-This convenience function can be used to get a specific display
-property, no matter whether the @code{display} property is a vector, a
-list or a simple property.  This is like @code{get-text-property}
-(@pxref{Examining Properties}), but works on the @code{display} property
-only.  For properties with a single value (e.g.@: @code{height}, this
-returns the value itself; for properties with a list of values (e.g.@:
-@code{slice}), this returns the list of values.
+@defun get-display-property position spec &optional object properties
+This convenience function can be used to get the value of a specific
+display specification, no matter whether the @code{display} property is
+a vector, a list or a simple property.  This is like
+@code{get-text-property} (@pxref{Examining Properties}), but works on
+the @code{display} property only.  For specifications with a single
+value (e.g.@: @code{height}, this returns the value itself; for
+properties with a list of values (e.g.@: @code{slice}), this returns the
+list of values.
 
 @var{position} is the position in the buffer or string to examine, and
-@var{prop} is the @code{display} property to return.  The optional
-@var{object} argument should be either a string or a buffer, and
-defaults to the current buffer.  If the optional @var{properties}
-argument is non-@code{nil}, it should be a @code{display} property,
-and in that case, @var{position} and @var{object} are ignored.  (This
-can be useful if you've already gotten the @code{display} property
-with @code{get-char-property}, for instance (@pxref{Examining
-Properties}).
+@var{spec} is the @sc{car} of the display specification to return.  The
+optional @var{object} argument should be either a string or a buffer,
+and defaults to the current buffer.  If the optional @var{properties}
+argument is non-@code{nil}, it should be a @code{display} property, and
+in that case, @var{position} and @var{object} are ignored.  (This can be
+useful if you've already gotten the @code{display} property with
+@code{get-char-property}, for instance (@pxref{Examining Properties}).
 @end defun
 
-@defun add-display-text-property start end prop value &optional object
-Add @code{display} property @var{prop} of @var{value} to the text from
-@var{start} to @var{end}.
+@defun add-display-text-property start end spec value &optional object
+Add the display specification @code{(@var{prop} @var{value})} to the
+text from @var{start} to @var{end}.
 
 If any text in the region has a non-@code{nil} @code{display}
 property, those properties are retained.  For instance:
@@ -5290,13 +5290,13 @@ Display Property
 @end lisp
 
 After doing this, the region from 2 to 4 will have the @code{raise}
-@code{display} property, the region from 4 to 8 will have both the
-@code{raise} and @code{height} @code{display} properties, and finally
-the region from 8 to 12 will only have the @code{raise} @code{display}
-property.
+display specification, the region from 4 to 8 will have both the
+@code{raise} and @code{height} display specifications, and finally the
+region from 8 to 12 will only have the @code{raise} display
+specification.
 
-If @var{object} is non-@code{nil}, it should be a string or a buffer.
-If @code{nil}, this defaults to the current buffer.
+@var{object} is either a string or a buffer to add the specification to.
+If omitted, @var{object} defaults to the current buffer.
 @end defun
 
 @cindex display property, unsafe evaluation
diff --git a/lisp/emacs-lisp/subr-x.el b/lisp/emacs-lisp/subr-x.el
index 4d56c67be5b..168e5c0f227 100644
--- a/lisp/emacs-lisp/subr-x.el
+++ b/lisp/emacs-lisp/subr-x.el
@@ -417,14 +417,13 @@ string-glyph-split
     (nreverse result)))
 
 ;;;###autoload
-(defun add-display-text-property (start end prop value
-                                        &optional object)
-  "Add display property PROP with VALUE to the text from START to END.
-If any text in the region has a non-nil `display' property, those
-properties are retained.
-
-If OBJECT is non-nil, it should be a string or a buffer.  If nil,
-this defaults to the current buffer."
+(defun add-display-text-property (start end spec value &optional object)
+  "Add the display specification (SPEC VALUE) to the text from START to END.
+If any text in the region has a non-nil `display' property, the existing
+display specifications are retained.
+
+OBJECT is either a string or a buffer to add the specification to.
+If omitted, OBJECT defaults to the current buffer."
   (let ((sub-start start)
         (sub-end 0)
         disp)
@@ -435,7 +434,7 @@ add-display-text-property
                                                    (min end (point-max)))))
       (if (not (setq disp (get-text-property sub-start 'display object)))
           ;; No old properties in this range.
-          (put-text-property sub-start sub-end 'display (list prop value)
+          (put-text-property sub-start sub-end 'display (list spec value)
                              object)
         ;; We have old properties.
         (let (type)
@@ -455,14 +454,14 @@ add-display-text-property
                   (setq type 'list)
                   disp)))
           ;; Remove any old instances.
-          (when-let* ((old (assoc prop disp)))
+          (when-let* ((old (assoc spec disp)))
             ;; If the property value was a list, don't modify the
             ;; original value in place; it could be used by other
             ;; regions of text.
             (setq disp (if (eq type 'list)
                            (remove old disp)
                          (delete old disp))))
-          (setq disp (cons (list prop value) disp))
+          (setq disp (cons (list spec value) disp))
           (when (eq type 'vector)
             (setq disp (seq-into disp 'vector)))
           ;; Finally update the range.
diff --git a/src/xdisp.c b/src/xdisp.c
index 9f3a684f25e..d6d9a9e61f8 100644
--- a/src/xdisp.c
+++ b/src/xdisp.c
@@ -5570,7 +5570,7 @@ setup_for_ellipsis (struct it *it, int len)
 
 
 static Lisp_Object
-find_display_property (Lisp_Object disp, Lisp_Object prop)
+find_display_property (Lisp_Object disp, Lisp_Object spec)
 {
   Lisp_Object elem;
   if (NILP (disp))
@@ -5583,7 +5583,7 @@ find_display_property (Lisp_Object disp, Lisp_Object prop)
 	  elem = AREF (disp, i);
 	  if (CONSP (elem)
 	      && CONSP (XCDR (elem))
-	      && EQ (XCAR (elem), prop))
+	      && EQ (XCAR (elem), spec))
 	    goto found;
 	}
       return Qnil;
@@ -5597,7 +5597,7 @@ find_display_property (Lisp_Object disp, Lisp_Object prop)
 	  elem = XCAR (disp);
 	  if (CONSP (elem)
 	      && CONSP (XCDR (elem))
-	      && EQ (XCAR (elem), prop))
+	      && EQ (XCAR (elem), spec))
 	    goto found;
 
 	  /* Check that we have a proper list before going to the next
@@ -5612,7 +5612,7 @@ find_display_property (Lisp_Object disp, Lisp_Object prop)
   /* A simple display spec.  */
   else if (CONSP (disp)
 	   && CONSP (XCDR (disp))
-	   && EQ (XCAR (disp), prop))
+	   && EQ (XCAR (disp), spec))
     {
       elem = disp;
       goto found;
@@ -5753,13 +5753,15 @@ display_min_width (struct it *it, ptrdiff_t charpos,
 
 DEFUN ("get-display-property", Fget_display_property,
        Sget_display_property, 2, 4, 0,
-       doc: /* Get the value of the `display' property PROP at POSITION.
-If OBJECT, this should be a buffer or string where the property is
-fetched from.  If omitted, OBJECT defaults to the current buffer.
+       doc: /* Get the value of the display specification SPEC at POSITION.
+SPEC is the car of the display specification to fetch, e.g. `height'.
 
-If PROPERTIES, look for value of PROP in PROPERTIES instead of the
-properties at POSITION.  */)
-  (Lisp_Object position, Lisp_Object prop, Lisp_Object object,
+OBJECT is either a string or a buffer to fetch the specification from.
+If omitted, OBJECT defaults to the current buffer.
+
+If PROPERTIES is non-nil, look for value of SPEC in PROPERTIES instead
+of the properties at POSITION.  */)
+  (Lisp_Object position, Lisp_Object spec, Lisp_Object object,
    Lisp_Object properties)
 {
   if (NILP (properties))
@@ -5767,7 +5769,7 @@ DEFUN ("get-display-property", Fget_display_property,
   else
     CHECK_LIST (properties);
 
-  return find_display_property (properties, prop);
+  return find_display_property (properties, spec);
 }

-- 
2.25.1

From 552a29002ccf634e446944aa1e2258f618ef9c94 Mon Sep 17 00:00:00 2001
From: Jim Porter <[email-masked]>
Date: Wed, 28 May 2025 09:44:34 -0700
Subject: [PATCH 3/4] Clean up text properties in 'visual-wrap-prefix-mode'

Before refontifying a region, remove any text properties we care about
so that we don't end up with stray properties.  Additionally, make sure
to remove all the properties when deactivating the mode.

* lisp/emacs-lisp/subr-x.el (add-remove--display-text-property): New
function, extracted from...
(add-display-text-property): ... here.
(remove-display-text-property): New function.

* lisp/visual-wrap.el (visual-wrap--remove-properties): New function...
(visual-wrap-prefix-function, visual-wrap-prefix-mode): ... call it.

* test/lisp/emacs-lisp/subr-x-tests.el
(subr-x-test-remove-display-text-property): New test.

* test/lisp/visual-wrap-tests.el
(visual-wrap-tests/wrap-prefix-stickiness, visual-wrap-tests/cleanup):
New tests.

* doc/lispref/display.texi (Display Property): Document
'remove-display-text-property'.

* etc/NEWS: Announce 'remove-display-text-property' (bug#76018).
---
 doc/lispref/display.texi             | 38 +++++++++++++++++
 etc/NEWS                             |  6 +++
 lisp/emacs-lisp/subr-x.el            | 64 +++++++++++++++++++---------
 lisp/visual-wrap.el                  | 11 ++++-
 test/lisp/emacs-lisp/subr-x-tests.el | 38 +++++++++++++++++
 test/lisp/visual-wrap-tests.el       | 40 ++++++++++++++++-
 6 files changed, 174 insertions(+), 23 deletions(-)

diff --git a/doc/lispref/display.texi b/doc/lispref/display.texi
index f29364bb4ef..f16b2227f24 100644
--- a/doc/lispref/display.texi
+++ b/doc/lispref/display.texi
@@ -5299,6 +5299,44 @@ Display Property
 If omitted, @var{object} defaults to the current buffer.
 @end defun
 
+@defun remove-display-text-property start end spec &optional object
+Remove the display specification @var{spec} from the text from
+@var{start} to @var{end}.  @var{spec} is the @sc{car} of the display
+specification to remove, e.g.@: @code{height}.
+
+If any text in the region has any other @code{display} properties, those
+properties are retained.  For instance:
+
+@lisp
+@group
+(add-display-text-property 1 8 'raise 0.5)
+(add-display-text-property 4 8 'height 2.0)
+(remove-display-text-property 2 6 'raise)
+@end group
+@end lisp
+
+After doing this, the text will have the following @code{display}
+properties:
+
+@itemize @bullet
+@item
+The region from 1 to 2, only @code{raise}
+
+@item
+The region from 2 to 4, no properties
+
+@item
+The region from 4 to 6, only @code{height}
+
+@item
+The region from 6 to 8, both @code{raise} and @code{height}
+
+@end itemize
+
+@var{object} is either a string or a buffer to remove the specification
+from. If omitted, @var{object} defaults to the current buffer.
+@end defun
+
 @cindex display property, unsafe evaluation
 @cindex security, and display specifications
   Some of the display specifications allow inclusion of Lisp forms,
diff --git a/etc/NEWS b/etc/NEWS
index 01ed372a2da..e79fcd0e3da 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -2353,6 +2353,12 @@ This 'display' property was previously supported only as text property.
 Now overlays can also have this property, with the same effect for the
 text "covered" by the overlay.
 
++++
+** New function 'remove-display-text-property'.
+This function removes a display property from the specified region of
+text, preserving any other display properties already set for that
+region.
+
 +++
 ** New macro 'cond*'.
 The new macro 'cond*' is an alternative to 'cond' and 'pcase'.
diff --git a/lisp/emacs-lisp/subr-x.el b/lisp/emacs-lisp/subr-x.el
index 168e5c0f227..ab5abf9cbed 100644
--- a/lisp/emacs-lisp/subr-x.el
+++ b/lisp/emacs-lisp/subr-x.el
@@ -416,28 +416,25 @@ string-glyph-split
         (setq start (1+ start))))
     (nreverse result)))
 
-;;;###autoload
-(defun add-display-text-property (start end spec value &optional object)
-  "Add the display specification (SPEC VALUE) to the text from START to END.
-If any text in the region has a non-nil `display' property, the existing
-display specifications are retained.
-
-OBJECT is either a string or a buffer to add the specification to.
-If omitted, OBJECT defaults to the current buffer."
+(defun add-remove--display-text-property (start end spec value
+                                                &optional object remove)
   (let ((sub-start start)
         (sub-end 0)
+        (limit (if (stringp object)
+                   (min (length object) end)
+                 (min end (point-max))))
         disp)
     (while (< sub-end end)
       (setq sub-end (next-single-property-change sub-start 'display object
-                                                 (if (stringp object)
-                                                     (min (length object) end)
-                                                   (min end (point-max)))))
+                                                 limit))
       (if (not (setq disp (get-text-property sub-start 'display object)))
           ;; No old properties in this range.
-          (put-text-property sub-start sub-end 'display (list spec value)
-                             object)
+          (unless remove
+            (put-text-property sub-start sub-end 'display (list spec value)
+                               object))
         ;; We have old properties.
-        (let (type)
+        (let ((changed nil)
+              type)
           ;; Make disp into a list.
           (setq disp
                 (cond
@@ -460,14 +457,41 @@ add-display-text-property
             ;; regions of text.
             (setq disp (if (eq type 'list)
                            (remove old disp)
-                         (delete old disp))))
-          (setq disp (cons (list spec value) disp))
-          (when (eq type 'vector)
-            (setq disp (seq-into disp 'vector)))
-          ;; Finally update the range.
-          (put-text-property sub-start sub-end 'display disp object)))
+                         (delete old disp))
+                  changed t))
+          (unless remove
+            (setq disp (cons (list spec value) disp)
+                  changed t))
+          (when changed
+            (if (not disp)
+                (remove-text-properties sub-start sub-end '(display nil) object)
+              (when (eq type 'vector)
+                (setq disp (seq-into disp 'vector)))
+              ;; Finally update the range.
+              (put-text-property sub-start sub-end 'display disp object)))))
       (setq sub-start sub-end))))
 
+;;;###autoload
+(defun add-display-text-property (start end spec value &optional object)
+  "Add the display specification (SPEC VALUE) to the text from START to END.
+If any text in the region has a non-nil `display' property, the existing
+display specifications are retained.
+
+OBJECT is either a string or a buffer to add the specification to.
+If omitted, OBJECT defaults to the current buffer."
+  (add-remove--display-text-property start end spec value object))
+
+;;;###autoload
+(defun remove-display-text-property (start end spec &optional object)
+  "Remove the display specification SPEC from the text from START to END.
+SPEC is the car of the display specification to remove, e.g. `height'.
+If any text in the region has other display specifications, those specs
+are retained.
+
+OBJECT is either a string or a buffer to remove the specification from.
+If omitted, OBJECT defaults to the current buffer."
+  (add-remove--display-text-property start end spec nil object 'remove))
+
 ;;;###autoload
 (defun read-process-name (prompt)
   "Query the user for a process and return the process object."
diff --git a/lisp/visual-wrap.el b/lisp/visual-wrap.el
index 227afe71006..3dcffa7c7e9 100644
--- a/lisp/visual-wrap.el
+++ b/lisp/visual-wrap.el
@@ -226,6 +226,14 @@ visual-wrap-fill-context-prefix
         (propertize prefix 'face face)
       prefix)))
 
+(defun visual-wrap--remove-properties (start end)
+  "Remove visual wrapping text properties from START to END."
+  ;; Remove `min-width' from any prefixes we detected.
+  (remove-display-text-property start end 'min-width)
+  ;; Remove `wrap-prefix' related properties from any lines with
+  ;; prefixes we detected.
+  (remove-text-properties start end '(wrap-prefix nil)))
+
 (defun visual-wrap-prefix-function (beg end)
   "Indent the region between BEG and END with visual filling."
   ;; Any change at the beginning of a line might change its wrap
@@ -238,6 +246,7 @@ visual-wrap-prefix-function
   (goto-char beg)
   (forward-line 0)
   (setq beg (point))
+  (visual-wrap--remove-properties beg end)
   (while (< (point) end)
     ;; Check if the display property at the end of this line is "safe".
     (if (visual-wrap--display-property-safe-p
@@ -283,7 +292,7 @@ visual-wrap-prefix-mode
     (with-silent-modifications
       (save-restriction
         (widen)
-        (remove-text-properties (point-min) (point-max) '(wrap-prefix nil))))))
+        (visual-wrap--remove-properties (point-min) (point-max))))))
 
 ;;;###autoload
 (define-globalized-minor-mode global-visual-wrap-prefix-mode
diff --git a/test/lisp/emacs-lisp/subr-x-tests.el b/test/lisp/emacs-lisp/subr-x-tests.el
index 5ffbe64ae40..e0eecc3e934 100644
--- a/test/lisp/emacs-lisp/subr-x-tests.el
+++ b/test/lisp/emacs-lisp/subr-x-tests.el
@@ -740,6 +740,44 @@ subr-x-test-add-display-text-property
                4 8 (display ((raise 0.5) (height 2.0)))
                8 12 (display (raise 0.5)))))))
 
+(ert-deftest subr-x-test-remove-display-text-property ()
+  (with-temp-buffer
+    (insert "Foo bar zot gazonk")
+    (add-display-text-property 4 12 'height 2.0)
+    (add-display-text-property 2 8 'raise 0.5)
+    (remove-display-text-property 6 10 'height)
+    (should (equal-including-properties
+             (buffer-string)
+             #("Foo bar zot gazonk"
+               1 3 (display (raise 0.5))
+               3 5 (display ((raise 0.5) (height 2.0)))
+               5 7 (display ((raise 0.5)))
+               9 11 (display (height 2.0))))))
+  (with-temp-buffer
+    (insert "Foo bar zot gazonk")
+    (put-text-property 4 12 'display [(height 2.0)])
+    (add-display-text-property 2 8 'raise 0.5)
+    (remove-display-text-property 6 10 'height)
+    (should (equal-including-properties
+             (buffer-string)
+             #("Foo bar zot gazonk"
+               1 3 (display (raise 0.5))
+               3 5 (display [(raise 0.5) (height 2.0)])
+               5 7 (display [(raise 0.5)])
+               9 11 (display [(height 2.0)])))))
+  (with-temp-buffer
+    (should (equal-including-properties
+             (let ((str (copy-sequence "Foo bar zot gazonk")))
+               (add-display-text-property 3 11 'height 2.0 str)
+               (add-display-text-property 1 7 'raise 0.5 str)
+               (remove-display-text-property 5 9 'height str)
+               str)
+             #("Foo bar zot gazonk"
+               1 3 (display (raise 0.5))
+               3 5 (display ((raise 0.5) (height 2.0)))
+               5 7 (display ((raise 0.5)))
+               9 11 (display (height 2.0)))))))
+
 (ert-deftest subr-x-named-let ()
   (let ((funs ()))
     (named-let loop
diff --git a/test/lisp/visual-wrap-tests.el b/test/lisp/visual-wrap-tests.el
index 04977afe207..d057ebef074 100644
--- a/test/lisp/visual-wrap-tests.el
+++ b/test/lisp/visual-wrap-tests.el
@@ -1,6 +1,6 @@
 ;;; visual-wrap-tests.el --- Tests for `visual-wrap-prefix-mode'  -*- lexical-binding: t; -*-
 
-;; Copyright (C) 2024 Free Software Foundation, Inc.
+;; Copyright (C) 2024-2025 Free Software Foundation, Inc.
 
 ;; This file is part of GNU Emacs.
 
@@ -19,7 +19,7 @@
 
 ;;; Commentary:
 
-;; Tets for `visual-wrap-prefix-mode'.
+;; Tests for `visual-wrap-prefix-mode'.
 
 ;;; Code:
 
@@ -117,4 +117,40 @@ visual-wrap-tests/unsafe-display/multiple-2
               0 4 (display ((image :type bmp)))
               4 8 (display ((image :type bmp) (height 1.5))))))))
 
+(ert-deftest visual-wrap-tests/wrap-prefix-stickiness ()
+  "Test that `wrap-prefix' doesn't persist across multiple lines when typing.
+See bug#76018."
+  (with-temp-buffer
+    (insert "* this zoo contains goats")
+    (visual-wrap-prefix-function (point-min) (point-max))
+    (should (equal-including-properties
+             (buffer-string)
+             #("* this zoo contains goats"
+               0  2 ( wrap-prefix (space :align-to (2 . width))
+                      display (min-width ((2 . width))))
+               2 25 ( wrap-prefix (space :align-to (2 . width))))))
+    (let ((start (point)))
+      (insert-and-inherit "\n\nit also contains pandas")
+      (visual-wrap-prefix-function start (point-max)))
+    (should (equal-including-properties
+             (buffer-string)
+             #("* this zoo contains goats\n\nit also contains pandas"
+               0  2 ( wrap-prefix (space :align-to (2 . width))
+                      display (min-width ((2 . width))))
+               2 25 ( wrap-prefix (space :align-to (2 . width))))))))
+
+(ert-deftest visual-wrap-tests/cleanup ()
+  "Test that deactivating `visual-wrap-prefix-mode' cleans up text properties."
+  (with-temp-buffer
+    (insert "* hello\n* hi")
+    (visual-wrap-prefix-function (point-min) (point-max))
+    ;; Make sure we've added the visual-wrapping properties.
+    (should (equal (text-properties-at (point-min))
+                   '( wrap-prefix (space :align-to (2 . width))
+                      display (min-width ((2 . width))))))
+    (visual-wrap-prefix-mode -1)
+    (should (equal-including-properties
+             (buffer-string)
+             "* hello\n* hi"))))
+
 ;; visual-wrap-tests.el ends here
-- 
2.25.1

From b0df37cf0ef2a4555d91de101f9d6a9f35bfb828 Mon Sep 17 00:00:00 2001
From: Jim Porter <[email-masked]>
Date: Wed, 28 May 2025 10:16:02 -0700
Subject: [PATCH 4/4] ; Remove superfluous POSITION argument from
 'visual-wrap--apply-to-line'

* lisp/visual-wrap.el (visual-wrap--apply-to-line): Remove POSITION and
just use point instead.  Update caller.
---
 lisp/visual-wrap.el | 46 ++++++++++++++++++++++-----------------------
 1 file changed, 22 insertions(+), 24 deletions(-)

diff --git a/lisp/visual-wrap.el b/lisp/visual-wrap.el
index 3dcffa7c7e9..24ca8ae7cf4 100644
--- a/lisp/visual-wrap.el
+++ b/lisp/visual-wrap.el
@@ -143,29 +143,27 @@ visual-wrap--adjust-prefix
        (t
         "")))))
 
-(defun visual-wrap--apply-to-line (position)
-  "Apply visual-wrapping properties to the logical line starting at POSITION."
-  (save-excursion
-    (goto-char position)
-    (when-let* ((first-line-prefix (fill-match-adaptive-prefix))
-                (next-line-prefix (visual-wrap--content-prefix
-                                   first-line-prefix position)))
-      (when (numberp next-line-prefix)
-        ;; Set a minimum width for the prefix so it lines up correctly
-        ;; with subsequent lines.  Make sure not to do this past the end
-        ;; of the line though!  (`fill-match-adaptive-prefix' could
-        ;; potentially return a prefix longer than the current line in
-        ;; the buffer.)
-        (add-display-text-property
-         position (min (+ position (length first-line-prefix))
-                       (pos-eol))
-         'min-width `((,next-line-prefix . width))))
-      (setq next-line-prefix (visual-wrap--adjust-prefix next-line-prefix))
-      (put-text-property
-       position (pos-eol) 'wrap-prefix
-       (if (numberp next-line-prefix)
-           `(space :align-to (,next-line-prefix . width))
-         next-line-prefix)))))
+(defun visual-wrap--apply-to-line ()
+  "Apply visual-wrapping properties to the logical line starting at point."
+  (when-let* ((first-line-prefix (fill-match-adaptive-prefix))
+              (next-line-prefix (visual-wrap--content-prefix
+                                 first-line-prefix (point))))
+    (when (numberp next-line-prefix)
+      ;; Set a minimum width for the prefix so it lines up correctly
+      ;; with subsequent lines.  Make sure not to do this past the end
+      ;; of the line though!  (`fill-match-adaptive-prefix' could
+      ;; potentially return a prefix longer than the current line in the
+      ;; buffer.)
+      (add-display-text-property
+       (point) (min (+ (point) (length first-line-prefix))
+                     (pos-eol))
+       'min-width `((,next-line-prefix . width))))
+    (setq next-line-prefix (visual-wrap--adjust-prefix next-line-prefix))
+    (put-text-property
+     (point) (pos-eol) 'wrap-prefix
+     (if (numberp next-line-prefix)
+         `(space :align-to (,next-line-prefix . width))
+       next-line-prefix))))
 
 (defun visual-wrap--content-prefix (prefix position)
   "Get the next-line prefix for the specified first-line PREFIX.
@@ -254,7 +252,7 @@ visual-wrap-prefix-function
         ;; If so, we can apply our visual wrapping properties to this
         ;; line and continue to the next line.
         (progn
-          (visual-wrap--apply-to-line (point))
+          (visual-wrap--apply-to-line)
           (forward-line))
       ;; Otherwise, skip ahead until the end of any unsafe display
       ;; properties.  NOTE: We do this out of an abundance of caution to
-- 
2.25.1