Subject: Re: bug#76313: New function `replace-region`

From: Stefan Monnier <[email-masked]>
Date: Sun, 16 Feb 2025 23:36:57 -0500

> So I suggest the patch below.  Comments?  Objections?

Here's a better implementation, in C.
One advantage is that it runs the `after/before-change-functions` only once.
Also, I made it take an optional `inherit` argument so it can do the
same as `insert-and-inherit` (as is used in `minibuffer--replace`).
This version does not move point to the end of the new text.

        Stefan
diff --git a/src/editfns.c b/src/editfns.c
index f9258392146..c86d0b890a2 100644
--- a/src/editfns.c
+++ b/src/editfns.c
@@ -2654,6 +2654,22 @@ DEFUN ("delete-and-extract-region", Fdelete_and_extract_region,
     return empty_unibyte_string;
   return del_range_1 (XFIXNUM (start), XFIXNUM (end), 1, 1);
 }
+
+DEFUN ("replace-region", Freplace_region, Sreplace_region, 3, 4, 0,
+       doc: /* Replace region FROM..TO with STRING.
+Does not move point, like `delete-region' but unlike `insert'.
+If optional arg INHERIT, then text properties are inherited from
+context like `insert-and-inherit'.  */)
+  (Lisp_Object from, Lisp_Object to, Lisp_Object string, Lisp_Object inherit)
+{
+  validate_region (&from, &to);
+  CHECK_STRING (string);
+  /* SET_PT (XFIXNUM (to)); */
+  replace_range (XFIXNUM (from), XFIXNUM (to), string,
+		 /* FIXME: Should we update search regs?  */
+		 true, !NILP (inherit), true, false, false);
+  return Qnil;
+}
 
 /* Alist of buffers in which labeled restrictions are used.  The car
    of each list element is a buffer, the cdr is a list of triplets
@@ -4929,6 +4945,7 @@ syms_of_editfns (void)
   defsubr (&Stranslate_region_internal);
   defsubr (&Sdelete_region);
   defsubr (&Sdelete_and_extract_region);
+  defsubr (&Sreplace_region);
   defsubr (&Swiden);
   defsubr (&Snarrow_to_region);
   defsubr (&Sinternal__labeled_narrow_to_region);
diff --git a/test/src/editfns-tests.el b/test/src/editfns-tests.el
index 8d4e7bc48fa..1b0fe30f8ce 100644
--- a/test/src/editfns-tests.el
+++ b/test/src/editfns-tests.el
@@ -305,6 +305,29 @@ replace-buffer-contents-bug31837
   (should (equal (buffer-substring-no-properties (point-min) (point-max))
                  (concat (string (char-from-name "SMILE")) "1234"))))
 
+(ert-deftest editfns-tests--replace-region ()
+  (with-temp-buffer
+    (insert "here is some text")
+    (let ((m5n (copy-marker (+ (point-min) 5)))
+          (m5a (copy-marker (+ (point-min) 5) t))
+          (m6n (copy-marker (+ (point-min) 6)))
+          (m6a (copy-marker (+ (point-min) 6) t))
+          (m7n (copy-marker (+ (point-min) 7)))
+          (m7a (copy-marker (+ (point-min) 7) t)))
+      (replace-region (+ (point-min) 5) (+ (point-min) 7) "was")
+      (should (equal (buffer-string) "here was some text"))
+      (should (equal (point) (point-max)))
+      ;; Markers before the replaced text stay before.
+      (should (= m5n (+ (point-min) 5)))
+      (should (= m5a (+ (point-min) 5)))
+      ;; Markers in the replaced text can end up at either end, depending
+      ;; on whether they're advance-after-insert or not.
+      (should (= m6n (+ (point-min) 5)))
+      (should (<= (+ (point-min) 5) m6a (+ (point-min) 8)))
+      ;; Markers after the replaced text stay after.
+      (should (= m7n (+ (point-min) 8)))
+      (should (= m7a (+ (point-min) 8))))))
+
 (ert-deftest delete-region-undo-markers-1 ()
   "Make sure we don't end up with freed markers reachable from Lisp."
   ;; https://debbugs.gnu.org/cgi/bugreport.cgi?bug=30931#40