diff --git a/lisp/misc.el b/lisp/misc.el index 718750404b7..fad8d545e11 100644 --- a/lisp/misc.el +++ b/lisp/misc.el @@ -105,7 +105,18 @@ Also see the `copy-from-above-command' command." (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 @@ -118,24 +129,40 @@ Also see the `copy-from-above-command' command." 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) - (duplicate--insert-copies n text))) + (duplicate--insert-copies n 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 @@ Ignores `line-move-visual'." (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 @@ Ignores `line-move-visual'." (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 @@ (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. - (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 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 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