Subject: bug#14979: 24.3; Feature Request: query-replace-backward

From: Juri Linkov <[email-masked]>
Date: Tue, 17 Dec 2013 21:35:32 +0200

> Typing M-- C-M-% is fewer keystrokes than pressing C-r a few times then
> C-M-%.  It is also fewer keystrokes than setting the mark, then moving
> through the document to find a proper starting point and finally pressing
> C-M-%.
>
> The best solution would be if the functions query-replace and
> query-replace-regexp are changed so that they step backwards when
> receiving a negative argument.

The following patch implements replace backward with this UI:

M-- M-%         - replace string backward
M-- C-M-%       - replace regexp backward
M-s w M-- M-%   - replace words backward
M-s _ M-- M-%   - replace symbols backward

=== modified file 'lisp/replace.el'
--- lisp/replace.el	2013-11-30 08:42:28 +0000
+++ lisp/replace.el	2013-12-17 19:34:11 +0000
@@ -226,9 +226,11 @@ (defun query-replace-read-args (prompt r
   (let* ((from (query-replace-read-from prompt regexp-flag))
 	 (to (if (consp from) (prog1 (cdr from) (setq from (car from)))
 	       (query-replace-read-to from prompt regexp-flag))))
-    (list from to current-prefix-arg)))
+    (list from to
+	  (and current-prefix-arg (not (eq current-prefix-arg '-)))
+	  (and current-prefix-arg (eq current-prefix-arg '-)))))
 
-(defun query-replace (from-string to-string &optional delimited start end)
+(defun query-replace (from-string to-string &optional delimited start end backward)
   "Replace some occurrences of FROM-STRING with TO-STRING.
 As each match is found, the user must type a character saying
 what to do with it.  For directions, type \\[help-command] at that time.
@@ -259,7 +261,9 @@ (defun query-replace (from-string to-str
 regexp in `search-whitespace-regexp'.
 
 Third arg DELIMITED (prefix arg if interactive), if non-nil, means replace
-only matches surrounded by word boundaries.
+only matches surrounded by word boundaries.  The negative prefix arg `-'
+means replacing backward.
+
 Fourth and fifth arg START and END specify the region to operate on.
 
 To customize possible responses, change the \"bindings\" in `query-replace-map'."
@@ -267,7 +271,9 @@ (defun query-replace (from-string to-str
    (let ((common
 	  (query-replace-read-args
 	   (concat "Query replace"
-		   (if current-prefix-arg " word" "")
+		   (if current-prefix-arg
+		       (if (eq current-prefix-arg '-) " backward" " word")
+		     "")
 		   (if (and transient-mark-mode mark-active) " in region" ""))
 	   nil)))
      (list (nth 0 common) (nth 1 common) (nth 2 common)
@@ -277,12 +283,13 @@ (defun query-replace (from-string to-str
 	   (if (and transient-mark-mode mark-active)
 	       (region-beginning))
 	   (if (and transient-mark-mode mark-active)
-	       (region-end)))))
-  (perform-replace from-string to-string t nil delimited nil nil start end))
+	       (region-end))
+	   (nth 3 common))))
+  (perform-replace from-string to-string t nil delimited nil nil start end backward))
 
 (define-key esc-map "%" 'query-replace)
 
-(defun query-replace-regexp (regexp to-string &optional delimited start end)
+(defun query-replace-regexp (regexp to-string &optional delimited start end backward)
   "Replace some things after point matching REGEXP with TO-STRING.
 As each match is found, the user must type a character saying
 what to do with it.  For directions, type \\[help-command] at that time.
@@ -313,7 +320,9 @@ (defun query-replace-regexp (regexp to-s
 regexp in `search-whitespace-regexp'.
 
 Third arg DELIMITED (prefix arg if interactive), if non-nil, means replace
-only matches surrounded by word boundaries.
+only matches surrounded by word boundaries.  The negative prefix arg `-'
+means replacing backward.
+
 Fourth and fifth arg START and END specify the region to operate on.
 
 In TO-STRING, `\\&' stands for whatever matched the whole of REGEXP,
@@ -341,7 +350,9 @@ (defun query-replace-regexp (regexp to-s
    (let ((common
 	  (query-replace-read-args
 	   (concat "Query replace"
-		   (if current-prefix-arg " word" "")
+		   (if current-prefix-arg
+		       (if (eq current-prefix-arg '-) " backward" " word")
+		     "")
 		   " regexp"
 		   (if (and transient-mark-mode mark-active) " in region" ""))
 	   t)))
@@ -352,8 +363,9 @@ (defun query-replace-regexp (regexp to-s
 	   (if (and transient-mark-mode mark-active)
 	       (region-beginning))
 	   (if (and transient-mark-mode mark-active)
-	       (region-end)))))
-  (perform-replace regexp to-string t t delimited nil nil start end))
+	       (region-end))
+	   (nth 3 common))))
+  (perform-replace regexp to-string t t delimited nil nil start end backward))
 
 (define-key esc-map [?\C-%] 'query-replace-regexp)
 
@@ -475,7 +487,7 @@ (defun map-query-replace-regexp (regexp
 		to-strings ""))))
     (perform-replace regexp replacements t t nil n nil start end)))
 
-(defun replace-string (from-string to-string &optional delimited start end)
+(defun replace-string (from-string to-string &optional delimited start end backward)
   "Replace occurrences of FROM-STRING with TO-STRING.
 Preserve case in each match if `case-replace' and `case-fold-search'
 are non-nil and FROM-STRING has no uppercase letters.
@@ -491,7 +503,8 @@ (defun replace-string (from-string to-st
 regexp in `search-whitespace-regexp'.
 
 Third arg DELIMITED (prefix arg if interactive), if non-nil, means replace
-only matches surrounded by word boundaries.
+only matches surrounded by word boundaries.  The negative prefix arg `-'
+means replacing backward.
 
 Operates on the region between START and END (if both are nil, from point
 to the end of the buffer).  Interactively, if Transient Mark mode is
@@ -513,7 +526,9 @@ (defun replace-string (from-string to-st
    (let ((common
 	  (query-replace-read-args
 	   (concat "Replace"
-		   (if current-prefix-arg " word" "")
+		   (if current-prefix-arg
+		       (if (eq current-prefix-arg '-) " backward" " word")
+		     "")
 		   " string"
 		   (if (and transient-mark-mode mark-active) " in region" ""))
 	   nil)))
@@ -521,12 +536,13 @@ (defun replace-string (from-string to-st
 	   (if (and transient-mark-mode mark-active)
 	       (region-beginning))
 	   (if (and transient-mark-mode mark-active)
-	       (region-end)))))
-  (perform-replace from-string to-string nil nil delimited nil nil start end))
+	       (region-end))
+	   (nth 3 common))))
+  (perform-replace from-string to-string nil nil delimited nil nil start end backward))
 (put 'replace-string 'interactive-only
      "use `search-forward' and `replace-match' instead.")
 
-(defun replace-regexp (regexp to-string &optional delimited start end)
+(defun replace-regexp (regexp to-string &optional delimited start end backward)
   "Replace things after point matching REGEXP with TO-STRING.
 Preserve case in each match if `case-replace' and `case-fold-search'
 are non-nil and REGEXP has no uppercase letters.
@@ -543,7 +559,9 @@ (defun replace-regexp (regexp to-string
 of the region.  Otherwise, operate from point to the end of the buffer.
 
 Third arg DELIMITED (prefix arg if interactive), if non-nil, means replace
-only matches surrounded by word boundaries.
+only matches surrounded by word boundaries.  The negative prefix arg `-'
+means replacing backward.
+
 Fourth and fifth arg START and END specify the region to operate on.
 
 In TO-STRING, `\\&' stands for whatever matched the whole of REGEXP,
@@ -582,7 +600,9 @@ (defun replace-regexp (regexp to-string
    (let ((common
 	  (query-replace-read-args
 	   (concat "Replace"
-		   (if current-prefix-arg " word" "")
+		   (if current-prefix-arg
+		       (if (eq current-prefix-arg '-) " backward" " word")
+		     "")
 		   " regexp"
 		   (if (and transient-mark-mode mark-active) " in region" ""))
 	   t)))
@@ -590,8 +610,9 @@ (defun replace-regexp (regexp to-string
 	   (if (and transient-mark-mode mark-active)
 	       (region-beginning))
 	   (if (and transient-mark-mode mark-active)
-	       (region-end)))))
-  (perform-replace regexp to-string nil t delimited nil nil start end))
+	       (region-end))
+	   (nth 3 common))))
+  (perform-replace regexp to-string nil t delimited nil nil start end backward))
 (put 'replace-regexp 'interactive-only
      "use `re-search-forward' and `replace-match' instead.")
 
@@ -1847,7 +1868,7 @@ (defun replace-match-data (integers reus
 		  new)))
       (match-data integers reuse t)))
 
-(defun replace-match-maybe-edit (newtext fixedcase literal noedit match-data)
+(defun replace-match-maybe-edit (newtext fixedcase literal noedit match-data backward)
   "Make a replacement with `replace-match', editing `\\?'.
 NEWTEXT, FIXEDCASE, LITERAL are just passed on.  If NOEDIT is true, no
 check for `\\?' is made to save time.  MATCH-DATA is used for the
@@ -1871,6 +1892,9 @@ (defun replace-match-maybe-edit (newtext
 	    noedit nil)))
   (set-match-data match-data)
   (replace-match newtext fixedcase literal)
+  ;; `replace-match' leaves point at the end of the replacement text,
+  ;; so move point to the beginning when replacing backward.
+  (when backward (goto-char (nth 0 match-data)))
   noedit)
 
 (defvar replace-search-function nil
@@ -1886,7 +1910,7 @@ (defvar replace-re-search-function nil
 `re-search-forward'.")
 
 (defun replace-search (search-string limit regexp-flag delimited-flag
-				     case-fold-search)
+				     case-fold-search backward)
   "Search for the next occurrence of SEARCH-STRING to replace."
   ;; Let-bind global isearch-* variables to values used
   ;; to search the next replacement.  These let-bindings
@@ -1905,7 +1929,7 @@ (defun replace-search (search-string lim
 	 (isearch-case-fold-search case-fold-search)
 	 (isearch-adjusted nil)
 	 (isearch-nonincremental t)	; don't use lax word mode
-	 (isearch-forward t)
+	 (isearch-forward (not backward))
 	 (search-function
 	  (or (if regexp-flag
 		  replace-re-search-function
@@ -1917,7 +1941,7 @@ (defvar replace-overlay nil)
 
 (defun replace-highlight (match-beg match-end range-beg range-end
 			  search-string regexp-flag delimited-flag
-			  case-fold-search)
+			  case-fold-search backward)
   (if query-replace-highlight
       (if replace-overlay
 	  (move-overlay replace-overlay match-beg match-end (current-buffer))
@@ -1933,7 +1957,7 @@ (defun replace-highlight (match-beg matc
 	    (isearch-regexp-lax-whitespace
 	     replace-regexp-lax-whitespace)
 	    (isearch-case-fold-search case-fold-search)
-	    (isearch-forward t)
+	    (isearch-forward (not backward))
 	    (isearch-other-end match-beg)
 	    (isearch-error nil))
 	(isearch-lazy-highlight-new-loop range-beg range-end))))
@@ -1949,7 +1973,7 @@ (defun replace-dehighlight ()
 
 (defun perform-replace (from-string replacements
 		        query-flag regexp-flag delimited-flag
-			&optional repeat-count map start end)
+			&optional repeat-count map start end backward)
   "Subroutine of `query-replace'.  Its complexity handles interactive queries.
 Don't use this in your own program unless you want to query and set the mark
 just as `query-replace' does.  Instead, write a simple loop like this:
@@ -2003,10 +2027,15 @@ (defun perform-replace (from-string repl
                      minibuffer-prompt-properties))))
 
     ;; If region is active, in Transient Mark mode, operate on region.
-    (when start
-      (setq limit (copy-marker (max start end)))
-      (goto-char (min start end))
-      (deactivate-mark))
+    (if backward
+	(when end
+	  (setq limit (copy-marker (min start end)))
+	  (goto-char (max start end))
+	  (deactivate-mark))
+      (when start
+	(setq limit (copy-marker (max start end)))
+	(goto-char (min start end))
+	(deactivate-mark)))
 
     ;; If last typed key in previous call of multi-buffer perform-replace
     ;; was `automatic-all', don't ask more questions in next files
@@ -2036,13 +2065,17 @@ (defun perform-replace (from-string repl
     (unwind-protect
 	;; Loop finding occurrences that perhaps should be replaced.
 	(while (and keep-going
-		    (not (or (eobp) (and limit (>= (point) limit))))
+		    (if backward
+			(not (or (bobp) (and limit (<= (point) limit))))
+		      (not (or (eobp) (and limit (>= (point) limit)))))
 		    ;; Use the next match if it is already known;
 		    ;; otherwise, search for a match after moving forward
 		    ;; one char if progress is required.
 		    (setq real-match-data
 			  (cond ((consp match-again)
-				 (goto-char (nth 1 match-again))
+				 (goto-char (if backward
+						(nth 0 match-again)
+					      (nth 1 match-again)))
 				 (replace-match-data
 				  t real-match-data match-again))
 				;; MATCH-AGAIN non-nil means accept an
@@ -2051,22 +2084,26 @@ (defun perform-replace (from-string repl
 				 (and
 				  (replace-search search-string limit
 						  regexp-flag delimited-flag
-						  case-fold-search)
+						  case-fold-search backward)
 				  ;; For speed, use only integers and
 				  ;; reuse the list used last time.
 				  (replace-match-data t real-match-data)))
-				((and (< (1+ (point)) (point-max))
+				((and (if backward
+					  (> (1- (point)) (point-min))
+					(< (1+ (point)) (point-max)))
 				      (or (null limit)
-					  (< (1+ (point)) limit)))
+					  (if backward
+					      (> (1- (point)) limit)
+					    (< (1+ (point)) limit))))
 				 ;; If not accepting adjacent matches,
 				 ;; move one char to the right before
 				 ;; searching again.  Undo the motion
 				 ;; if the search fails.
 				 (let ((opoint (point)))
-				   (forward-char 1)
+				   (forward-char (if backward -1 1))
 				   (if (replace-search search-string limit
 						       regexp-flag delimited-flag
-						       case-fold-search)
+						       case-fold-search backward)
 				       (replace-match-data
 					t real-match-data)
 				     (goto-char opoint)
@@ -2087,7 +2124,9 @@ (defun perform-replace (from-string repl
 	  (setq match-again
 		(and nonempty-match
 		     (or (not regexp-flag)
-			 (and (looking-at search-string)
+			 (and (if backward
+				  (looking-back search-string)
+				(looking-at search-string))
 			      (let ((match (match-data)))
 				(and (/= (nth 0 match) (nth 1 match))
 				     match))))))
@@ -2124,11 +2163,11 @@ (defun perform-replace (from-string repl
 		    (replace-highlight
 		     (nth 0 real-match-data) (nth 1 real-match-data)
 		     start end search-string
-		     regexp-flag delimited-flag case-fold-search))
+		     regexp-flag delimited-flag case-fold-search backward))
 		  (setq noedit
 			(replace-match-maybe-edit
 			 next-replacement nocasify literal
-			 noedit real-match-data)
+			 noedit real-match-data backward)
 			replace-count (1+ replace-count)))
 	      (undo-boundary)
 	      (let (done replaced key def)
@@ -2143,7 +2182,7 @@ (defun perform-replace (from-string repl
 		  (replace-highlight
 		   (match-beginning 0) (match-end 0)
 		   start end search-string
-		   regexp-flag delimited-flag case-fold-search)
+		   regexp-flag delimited-flag case-fold-search backward)
 		  ;; Bind message-log-max so we don't fill up the message log
 		  ;; with a bunch of identical messages.
 		  (let ((message-log-max nil)
@@ -2173,6 +2212,7 @@ (defun perform-replace (from-string repl
 						 (get delimited-flag 'isearch-message-prefix))
 					    "word ") "")
 				    (if regexp-flag "regexp " "")
+				    (if backward "backward " "")
 				    from-string " with "
 				    next-replacement ".\n\n"
 				    (substitute-command-keys
@@ -2201,7 +2241,7 @@ (defun perform-replace (from-string repl
 			     (setq noedit
 				   (replace-match-maybe-edit
 				    next-replacement nocasify literal
-				    noedit real-match-data)
+				    noedit real-match-data backward)
 				   replace-count (1+ replace-count)))
 			 (setq done t replaced t))
 			((eq def 'act-and-exit)
@@ -2209,7 +2249,7 @@ (defun perform-replace (from-string repl
 			     (setq noedit
 				   (replace-match-maybe-edit
 				    next-replacement nocasify literal
-				    noedit real-match-data)
+				    noedit real-match-data backward)
 				   replace-count (1+ replace-count)))
 			 (setq keep-going nil)
 			 (setq done t replaced t))
@@ -2218,7 +2258,7 @@ (defun perform-replace (from-string repl
 			     (setq noedit
 				   (replace-match-maybe-edit
 				    next-replacement nocasify literal
-				    noedit real-match-data)
+				    noedit real-match-data backward)
 				   replace-count (1+ replace-count)
 				   real-match-data (replace-match-data
 						    t real-match-data)
@@ -2228,7 +2268,7 @@ (defun perform-replace (from-string repl
 			     (setq noedit
 				   (replace-match-maybe-edit
 				    next-replacement nocasify literal
-				    noedit real-match-data)
+				    noedit real-match-data backward)
 				   replace-count (1+ replace-count)))
 			 (setq done t query-flag nil replaced t)
 			 (if (eq def 'automatic-all) (setq multi-buffer t)))
@@ -2272,7 +2312,7 @@ (defun perform-replace (from-string repl
 			   (setq noedit
 				 (replace-match-maybe-edit
 				  next-replacement nocasify literal noedit
-				  real-match-data)
+				  real-match-data backward)
 				 replaced t))
 			 (setq done t))

=== modified file 'lisp/isearch.el'
--- lisp/isearch.el	2013-12-16 20:32:15 +0000
+++ lisp/isearch.el	2013-12-17 19:34:05 +0000
@@ -1667,10 +1667,11 @@ (defun re-search-backward-lax-whitespace
     (re-search-backward regexp bound noerror count)))
 
 
-(defun isearch-query-replace (&optional delimited regexp-flag)
+(defun isearch-query-replace (&optional arg regexp-flag)
   "Start `query-replace' with string to replace from last search string.
-The arg DELIMITED (prefix arg if interactive), if non-nil, means replace
-only matches surrounded by word boundaries.  Note that using the prefix arg
+The ARG (prefix arg if interactive), if non-nil, means replace
+only matches surrounded by word boundaries.  The negative prefix
+arg `-' means replacing backward.  Note that using the prefix arg
 is possible only when `isearch-allow-scroll' is non-nil or
 `isearch-allow-prefix' is non-nil, and it doesn't always provide the
 correct matches for `query-replace', so the preferred way to run word
@@ -1688,6 +1689,8 @@ (defun isearch-query-replace (&optional
 	 isearch-lax-whitespace)
 	(replace-regexp-lax-whitespace
 	 isearch-regexp-lax-whitespace)
+	(delimited (and arg (not (eq arg '-))))
+	(backward (and arg (eq arg '-)))
 	;; Set `isearch-recursive-edit' to nil to prevent calling
 	;; `exit-recursive-edit' in `isearch-done' that terminates
 	;; the execution of this command when it is non-nil.
@@ -1696,9 +1699,13 @@ (defun isearch-query-replace (&optional
     (isearch-done nil t)
     (isearch-clean-overlays)
     (if (and isearch-other-end
-	     (< isearch-other-end (point))
+	     (if backward
+		 (> isearch-other-end (point))
+	       (< isearch-other-end (point)))
              (not (and transient-mark-mode mark-active
-                       (< (mark) (point)))))
+                       (if backward
+			   (> (mark) (point))
+			 (< (mark) (point))))))
         (goto-char isearch-other-end))
     (set query-replace-from-history-variable
          (cons isearch-string
@@ -1718,19 +1725,21 @@ (defun isearch-query-replace (&optional
 		      " word"))
 		"")
 	      (if isearch-regexp " regexp" "")
+	      (if backward " backward" "")
 	      (if (and transient-mark-mode mark-active) " in region" ""))
       isearch-regexp)
      t isearch-regexp (or delimited isearch-word) nil nil
      (if (and transient-mark-mode mark-active) (region-beginning))
-     (if (and transient-mark-mode mark-active) (region-end))))
+     (if (and transient-mark-mode mark-active) (region-end))
+     backward))
   (and isearch-recursive-edit (exit-recursive-edit)))
 
-(defun isearch-query-replace-regexp (&optional delimited)
+(defun isearch-query-replace-regexp (&optional arg)
   "Start `query-replace-regexp' with string to replace from last search string.
 See `isearch-query-replace' for more information."
   (interactive
    (list current-prefix-arg))
-  (isearch-query-replace delimited t))
+  (isearch-query-replace arg t))
 
 (defun isearch-occur (regexp &optional nlines)
   "Run `occur' using the last search string as the regexp.