Subject: Re: bug#62505: 29.0.60; Switching between query-replace and query-replace-regexp should be easy, like isearch

From: Juri Linkov <[email-masked]>
Date: Fri, 12 Sep 2025 19:07:10 +0300

>>> isearch-forward and isearch-forward-regexp are bound to C-s and C-M-s.
>>> After an isearch-forward is started, it can be switched to
>>> isearch-forward-regexp by typing M-s r.
>>>
>>> query-replace and query-replace-regexp are bound to M-% and C-M-%.
>>> After a query-replace is started, there's no way to switch it to
>>> query-replace-regexp.
>>
>> It's not clear whether you want to switch modes when the replacement
>> process is already underway, or when reading query-replace arguments
>> in the minibuffer.  In Isearch mode 'M-s r' switches modes when the
>> search process is in progress, not when reading the search string
>> in the minibuffer.
>
> I think both should work.

It's straightforward to implement this for the case when replacement
is already started like in the active Isearch.  However, switching
the replacement type while reading arguments would be too messy.
There is such logic in 'query-replace-read-from':

              (if regexp-flag
                  (read-regexp ...)
                (read-from-minibuffer ...))

Changing the value of 'regexp-flag' while the minibuffer is active will require
canceling the current minibuffer and restarting 'query-replace' from scratch.
It's easier to do the same for the user manually with just 'C-g C-M-% RET'
that reuses the last search→replace pair in case when the user mistyped
M-% instead of C-M-%.

> True that 'M-s r' doesn't work if you first start editing the search
> string with 'M-e'.  Maybe Isearch would also benefit from having that
> work?  With C-s and C-M-s bindings, which work both while searching and
> while editing the search string in the minibuffer?

There is ambiguity in using C-s in the minibuffer since it can mean both:
- continue the current regexp search like 'C-M-s C-s C-s ...' does;
- switch to non-regexp mode.
It's easier for the user to use just 'M-s r' to toggle regexp search
while in Isearch mode.

Also using M-% and C-M-% is problematic in the minibuffer
since both keys can be used for replacing text in the minibuffer.

However, how to implement toggling the regexp mode while using query-replace
is clear and is implemented in this patch:

diff --git a/lisp/replace.el b/lisp/replace.el
index 8227056e012..dae2434a90f 100644
--- a/lisp/replace.el
+++ b/lisp/replace.el
@@ -2495,6 +2496,7 @@ query-replace-map
     (define-key map "?" 'help)
     (define-key map "\C-g" 'quit)
     (define-key map "\C-]" 'quit)
+    (define-key map "\M-r" 'toggle-regexp)
     (define-key map "\C-v" 'scroll-up)
     (define-key map "\M-v" 'scroll-down)
     (define-key map [next] 'scroll-up)
@@ -2887,21 +2889,24 @@ perform-replace
          ;; Data for the next match.  If a cons, it has the same format as
          ;; (match-data); otherwise it is t if a match is possible at point.
          (match-again t)
-         (message
-          (if query-flag
-              (apply #'propertize
-                     (concat "Query replacing "
-                             (if backward "backward " "")
-                             (if delimited-flag
-                                 (or (and (symbolp delimited-flag)
-                                          (get delimited-flag
-                                               'isearch-message-prefix))
-                                     "word ") "")
-                             (if regexp-flag "regexp " "")
-                             "%s with %s: "
-                             (substitute-command-keys
-                              "(\\<query-replace-map>\\[help] for help) "))
-                     minibuffer-prompt-properties))))
+         (message-function
+          (lambda ()
+            (if query-flag
+                (apply #'propertize
+                       (concat "Query replacing "
+                               (if backward "backward " "")
+                               (if delimited-flag
+                                   (or (and (symbolp delimited-flag)
+                                            (get delimited-flag
+                                                 'isearch-message-prefix))
+                                       "word ")
+                                 "")
+                               (if regexp-flag "regexp " "")
+                               "%s with %s: "
+                               (substitute-command-keys
+                                "(\\<query-replace-map>\\[help] for help) "))
+                       minibuffer-prompt-properties))))
+         (message (funcall message-function)))
 
     ;; Unless a single contiguous chunk is selected, operate on multiple chunks.
     (when region-noncontiguous-p
@@ -3330,6 +3335,22 @@ perform-replace
 			 (replace-dehighlight)
 			 (save-excursion (recursive-edit))
 			 (setq replaced t))
+                        ((eq def 'toggle-regexp)
+                         (setq regexp-flag (not regexp-flag)
+                               literal (not regexp-flag)
+                               message (funcall message-function)
+                               match-again
+                               (save-excursion
+                                 (goto-char (nth 0 real-match-data))
+                                 (and (if regexp-flag
+                                          (if backward
+                                              (looking-back search-string nil)
+                                            (looking-at search-string))
+                                        (if backward
+                                            (looking-back (regexp-quote search-string) nil)
+                                          (looking-at (regexp-quote search-string))))
+                                      (match-data)))
+                               done t))
                         ((commandp def t)
                          (call-interactively def))
 			;; Note: we do not need to treat `exit-prefix'