From c7051500cbfda75a2a218c323da3715cbf7263cc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Mattias=20Engdeg=C3=A5rd?= <mattiase@acm.org>
Date: Fri, 30 Jun 2023 18:34:10 +0200
Subject: [PATCH] Add duplicate-region-final-position (bug#64185)

* lisp/misc.el (duplicate-region-final-position): New defcustom.
(duplicate-dwim): Use it.
* lisp/rect.el (rectangle--duplicate-right): Add displacement
argument.  Adjust for changes in Emacs 29.
* test/lisp/misc-tests.el (misc--duplicate-dwim):
Extend test.
---
 lisp/misc.el            | 37 +++++++++++++---
 lisp/rect.el            | 25 +++++++----
 test/lisp/misc-tests.el | 97 +++++++++++++++++++++++++++--------------
 3 files changed, 112 insertions(+), 47 deletions(-)

diff --git a/lisp/misc.el b/lisp/misc.el
index 52df33911f7..c50eec9156e 100644
--- a/lisp/misc.el
+++ b/lisp/misc.el
@@ -100,7 +100,18 @@ duplicate-line
       (forward-line duplicate-line-final-position)
       (move-to-column col))))
 
-(declare-function rectangle--duplicate-right "rect" (n))
+(defcustom duplicate-region-final-position 0
+  "Where the region ends up after duplicating a region with `duplicate-dwim'.
+When 0, leave the region in place.
+When 1, put the region around the first copy.
+When -1, put the region around the last copy."
+  :type '(choice (const :tag "Leave region in place" 0)
+                 (const :tag "Put region around first copy" 1)
+                 (const :tag "Put region around last copy" -1))
+  :group 'editing
+  :version "29.1")
+
+(declare-function rectangle--duplicate-right "rect" (n displacement))
 
 ;; `duplicate-dwim' preserves an active region and changes the buffer
 ;; outside of it: disregard the region when immediately undoing the
@@ -113,25 +124,41 @@ duplicate-dwim
 If the region is inactive, duplicate the current line (like `duplicate-line').
 Otherwise, duplicate the region, which remains active afterwards.
 If the region is rectangular, duplicate on its right-hand side.
-Interactively, N is the prefix numeric argument, and defaults to 1."
+Interactively, N is the prefix numeric argument, and defaults to 1.
+The variables `duplicate-line-final-position' and
+`duplicate-region-final-position' control the position of point
+and the region after the duplication."
   (interactive "p")
   (unless n
     (setq n 1))
   (cond
+   ((<= n 0) nil)
    ;; Duplicate rectangle.
    ((bound-and-true-p rectangle-mark-mode)
-    (rectangle--duplicate-right n)
+    (rectangle--duplicate-right n
+                                (if (< duplicate-region-final-position 0)
+                                    n
+                                  duplicate-region-final-position))
     (setq deactivate-mark nil))
 
    ;; Duplicate (contiguous) region.
    ((use-region-p)
     (let* ((beg (region-beginning))
            (end (region-end))
-           (text (buffer-substring beg end)))
+           (text (buffer-substring beg end))
+           (pt (point))
+           (mk (mark)))
       (save-excursion
         (goto-char end)
         (dotimes (_ n)
-          (insert text))))
+          (insert text)))
+      (let* ((displace (if (< duplicate-region-final-position 0)
+                           n
+                         duplicate-region-final-position))
+             (d (* displace (- end beg))))
+        (unless (zerop d)
+          (push-mark (+ mk d))
+          (goto-char (+ pt d)))))
     (setq deactivate-mark nil))
 
    ;; Duplicate line.
diff --git a/lisp/rect.el b/lisp/rect.el
index 5ff821abb3f..8dc188b1de0 100644
--- a/lisp/rect.el
+++ b/lisp/rect.el
@@ -930,8 +930,9 @@ rectangle--unhighlight-for-redisplay
     (mapc #'delete-overlay (nthcdr 5 rol))
     (setcar (cdr rol) nil)))
 
-(defun rectangle--duplicate-right (n)
-  "Duplicate the rectangular region N times on the right-hand side."
+(defun rectangle--duplicate-right (n displacement)
+  "Duplicate the rectangular region N times on the right-hand side.
+Leave the region moved DISPLACEMENT region-wide steps to the right."
   (let ((cols (rectangle--pos-cols (point) (mark))))
     (apply-on-rectangle
      (lambda (startcol endcol)
@@ -940,16 +941,22 @@ rectangle--duplicate-right
          (move-to-column endcol t)
          (dotimes (_ n)
            (insert (cadr lines)))))
-     (region-beginning) (region-end))
-    ;; Recompute the rectangle state; no crutches should be needed now.
-    (let ((p (point))
-          (m (mark)))
+     (min (point) (mark))
+     (max (point) (mark)))
+    ;; Recompute the rectangle state.
+    (let* ((p (point))
+           (m (mark))
+           (point-col (car cols))
+           (mark-col (cdr cols))
+           (d (* displacement (abs (- point-col mark-col)))))
       (rectangle--reset-crutches)
       (goto-char m)
-      (move-to-column (cdr cols) t)
-      (set-mark (point))
+      (move-to-column (+ mark-col d) t)
+      (if (= d 0)
+          (set-mark (point))
+        (push-mark (point)))
       (goto-char p)
-      (move-to-column (car cols) t))))
+      (move-to-column (+ point-col d) t))))
 
 (provide 'rect)
 
diff --git a/test/lisp/misc-tests.el b/test/lisp/misc-tests.el
index ea27ea1653b..b9bafe4bd11 100644
--- a/test/lisp/misc-tests.el
+++ b/test/lisp/misc-tests.el
@@ -24,6 +24,7 @@
 ;;; Code:
 
 (require 'ert)
+(require 'misc)
 
 (defmacro with-misc-test (original result &rest body)
   (declare (indent 2))
@@ -113,40 +114,70 @@ misc--duplicate-line
 (require 'rect)
 
 (ert-deftest misc--duplicate-dwim ()
-  ;; Duplicate a line.
-  (with-temp-buffer
-    (insert "abc\ndefg\nh\n")
-    (goto-char 7)
-    (duplicate-dwim 2)
-    (should (equal (buffer-string) "abc\ndefg\ndefg\ndefg\nh\n"))
-    (should (equal (point) 7)))
+  (let ((duplicate-line-final-position 0)
+        (duplicate-region-final-position 0))
+    ;; Duplicate a line.
+    (dolist (final-pos '(0 -1 1))
+      (ert-info ((prin1-to-string final-pos) :prefix "final-pos: ")
+        (with-temp-buffer
+          (insert "abc\ndefg\nh\n")
+          (goto-char 7)
+          (let ((duplicate-line-final-position final-pos))
+            (duplicate-dwim 3))
+          (should (equal (buffer-string) "abc\ndefg\ndefg\ndefg\ndefg\nh\n"))
+          (let ((delta (* 5 (if (< final-pos 0) 3 final-pos))))
+            (should (equal (point) (+ 7 delta)))))))
+
+    ;; Duplicate a region.
+    (dolist (final-pos '(0 -1 1))
+      (ert-info ((prin1-to-string final-pos) :prefix "final-pos: ")
+        (with-temp-buffer
+          (insert "abCDEFghi")
+          (set-mark 3)
+          (goto-char 7)
+          (transient-mark-mode)
+          (should (use-region-p))
+          (let ((duplicate-region-final-position final-pos))
+            (duplicate-dwim 3))
+          (should (equal (buffer-string) "abCDEFCDEFCDEFCDEFghi"))
+          (should (region-active-p))
+          (let ((delta (* 4 (if (< final-pos 0) 3 final-pos))))
+            (should (equal (point) (+ 7 delta)))
+            (should (equal (mark) (+ 3 delta)))))))
+
+    ;; Duplicate a rectangular region (sparse).
+    (with-temp-buffer
+      (insert "x\n>a\n>bcde\n>fg\nyz\n")
+      (goto-char 4)
+      (rectangle-mark-mode)
+      (goto-char 15)
+      (rectangle-forward-char 1)
+      (duplicate-dwim)
+      (should (equal (buffer-string) "x\n>a  a  \n>bcdbcde\n>fg fg \nyz\n"))
+      (should (equal (point) 24))
+      (should (region-active-p))
+      (should rectangle-mark-mode)
+      (should (equal (mark) 4)))
+
+    ;; Idem (dense).
+    (dolist (final-pos '(0 -1 1))
+      (ert-info ((prin1-to-string final-pos) :prefix "final-pos: ")
+        (with-temp-buffer
+          (insert "aBCd\neFGh\niJKl\n")
+          (goto-char 2)
+          (rectangle-mark-mode)
+          (goto-char 14)
+          (let ((duplicate-region-final-position final-pos))
+            (duplicate-dwim 3))
+          (should (equal (buffer-string)
+                         "aBCBCBCBCd\neFGFGFGFGh\niJKJKJKJKl\n"))
+          (should (region-active-p))
+          (should rectangle-mark-mode)
+          (let ((hdelta (* 2 (if (< final-pos 0) 3 final-pos)))
+                (vdelta 12))
+            (should (equal (point) (+ 14 vdelta hdelta)))
+            (should (equal (mark) (+ 2 hdelta)))))))))
 
-  ;; Duplicate a region.
-  (with-temp-buffer
-    (insert "abc\ndef\n")
-    (set-mark 2)
-    (goto-char 7)
-    (transient-mark-mode)
-    (should (use-region-p))
-    (duplicate-dwim)
-    (should (equal (buffer-string) "abc\ndebc\ndef\n"))
-    (should (equal (point) 7))
-    (should (region-active-p))
-    (should (equal (mark) 2)))
-
-  ;; Duplicate a rectangular region.
-  (with-temp-buffer
-    (insert "x\n>a\n>bcde\n>fg\nyz\n")
-    (goto-char 4)
-    (rectangle-mark-mode)
-    (goto-char 15)
-    (rectangle-forward-char 1)
-    (duplicate-dwim)
-    (should (equal (buffer-string) "x\n>a  a  \n>bcdbcde\n>fg fg \nyz\n"))
-    (should (equal (point) 24))
-    (should (region-active-p))
-    (should rectangle-mark-mode)
-    (should (equal (mark) 4))))
 
 (provide 'misc-tests)
 ;;; misc-tests.el ends here
-- 
2.32.0 (Apple Git-132)

