mirror of
https://git.savannah.gnu.org/git/emacs.git
synced 2026-09-10 07:46:51 -04:00
Improve where the replacement is previewed (bug#81583)
* lisp/replace.el (replace-preview--input): New function. (replace-preview-update): Add optional START and END, and preview only the matches between them. (replace-preview-setup): Accept a nil FROM, read from the minibuffer along with the replacement, and add optional BACKWARD. Limit the preview to the part of the buffer that the replacement will cover. (query-replace-read-to): Add optional BACKWARD and pass it on. (query-replace-read-args): Preview the replacement while reading FROM as well, and preview nothing when the caller asks for no highlighting. * lisp/isearch.el (isearch-query-replace): Pass BACKWARD to `query-replace-read-to'. * test/lisp/replace-tests.el (replace-tests--preview): Add START and END. (replace-tests-preview-bounds, replace-tests-preview-input): New tests. * doc/emacs/search.texi (Replace): Document the above.
This commit is contained in:
parent
92acd66831
commit
a8736d5f88
|
|
@ -1573,8 +1573,16 @@ value, the replace commands show a preview of the replacement while you
|
||||||
type it: the matches visible in the window are displayed as they would
|
type it: the matches visible in the window are displayed as they would
|
||||||
look after the replacement. This tells you what back-references like
|
look after the replacement. This tells you what back-references like
|
||||||
@samp{\1} (@pxref{Regexp Replace}) expand to before you commit to the
|
@samp{\1} (@pxref{Regexp Replace}) expand to before you commit to the
|
||||||
replacement. However, replacements that use @samp{\,} or @samp{\#} are
|
replacement. The preview also appears at the prompt that reads the text
|
||||||
not previewed.
|
to replace, as soon as its input holds both halves separated by an
|
||||||
|
arrow, which is what @kbd{M-p} recalls from the history. However,
|
||||||
|
replacements that use @samp{\,} or @samp{\#} are not previewed.
|
||||||
|
|
||||||
|
Only the matches that the command will replace are previewed: those
|
||||||
|
after point, or those before it when replacing backward
|
||||||
|
(@pxref{Query Replace}). The other ones are still highlighted, to show
|
||||||
|
that the buffer has more of them. When the region is active, the
|
||||||
|
preview covers all of it, as the replacement does.
|
||||||
|
|
||||||
The value can be @code{replacement-only}, to display the replacement
|
The value can be @code{replacement-only}, to display the replacement
|
||||||
alone, @code{both}, to display the match next to its replacement,
|
alone, @code{both}, to display the match next to its replacement,
|
||||||
|
|
|
||||||
6
etc/NEWS
6
etc/NEWS
|
|
@ -136,7 +136,11 @@ of the terminal emulator:
|
||||||
** New user option 'query-replace-show-preview'.
|
** New user option 'query-replace-show-preview'.
|
||||||
If non-nil, the replacement commands preview the replacement while you
|
If non-nil, the replacement commands preview the replacement while you
|
||||||
type it: the matches visible in the window are shown as they would look
|
type it: the matches visible in the window are shown as they would look
|
||||||
after the replacement. Replacements that use '\,' or '\#' are not
|
after the replacement. Only the matches that the command will replace
|
||||||
|
are previewed: those after point, or those before it when replacing
|
||||||
|
backward. The preview also appears at the first prompt, as soon as its
|
||||||
|
input holds both halves, as it does after recalling a replacement from
|
||||||
|
the history with 'M-p'. Replacements that use '\,' or '\#' are not
|
||||||
previewed. The preview is off by default.
|
previewed. The preview is off by default.
|
||||||
|
|
||||||
The value can be the symbol 'replacement-only', which shows the
|
The value can be the symbol 'replacement-only', which shows the
|
||||||
|
|
|
||||||
|
|
@ -2450,7 +2450,7 @@ type \\[help-command] at that time."
|
||||||
(isearch--describe-regexp-mode (or delimited isearch-regexp-function) t)
|
(isearch--describe-regexp-mode (or delimited isearch-regexp-function) t)
|
||||||
(if backward " backward" "")
|
(if backward " backward" "")
|
||||||
(if (use-region-p) " in region" ""))
|
(if (use-region-p) " in region" ""))
|
||||||
isearch-regexp (or delimited isearch-regexp-function))
|
isearch-regexp (or delimited isearch-regexp-function) backward)
|
||||||
t isearch-regexp (or delimited isearch-regexp-function) nil nil
|
t isearch-regexp (or delimited isearch-regexp-function) nil nil
|
||||||
(use-region-beginning) (use-region-end)
|
(use-region-beginning) (use-region-end)
|
||||||
backward))
|
backward))
|
||||||
|
|
|
||||||
|
|
@ -436,22 +436,41 @@ replace it. They are combined as `query-replace-show-preview' says."
|
||||||
(funcall query-replace-show-preview match replacement))))))
|
(funcall query-replace-show-preview match replacement))))))
|
||||||
(and (stringp s) s)))
|
(and (stringp s) s)))
|
||||||
|
|
||||||
(defun replace-preview-update (from to regexp-flag delimited-flag case-fold)
|
(defun replace-preview--input (from contents)
|
||||||
|
"Return the pair of strings to preview, or nil if there is none.
|
||||||
|
FROM is the string to search for and CONTENTS is the minibuffer input,
|
||||||
|
which is then the replacement text. FROM nil means that CONTENTS holds
|
||||||
|
both halves, as at the prompt of `query-replace-read-from', which splits
|
||||||
|
them on the `separator' text property; until the input has both of them
|
||||||
|
there is nothing to preview, and nil is returned."
|
||||||
|
(if from
|
||||||
|
(cons from (substring-no-properties contents))
|
||||||
|
(let ((split (query-replace--split-string contents)))
|
||||||
|
(when (consp split)
|
||||||
|
(cons (substring-no-properties (car split)) (cdr split))))))
|
||||||
|
|
||||||
|
(defun replace-preview-update (from to regexp-flag delimited-flag case-fold
|
||||||
|
&optional start end)
|
||||||
"Preview the result of replacing FROM with TO in the current buffer.
|
"Preview the result of replacing FROM with TO in the current buffer.
|
||||||
Each match of FROM visible in the selected window gets an overlay
|
Each match of FROM visible in the selected window gets an overlay
|
||||||
showing the text that `replace-preview--format' returns for it, which
|
showing the text that `replace-preview--format' returns for it, which
|
||||||
depends on `query-replace-show-preview'. Matches for which it returns
|
depends on `query-replace-show-preview'. Matches for which it returns
|
||||||
nil are left alone.
|
nil are left alone.
|
||||||
|
|
||||||
|
START and END limit the previewed portion of the buffer, as in
|
||||||
|
`perform-replace': a visible match outside of them will not be replaced,
|
||||||
|
so previewing it would be misleading. Matches to leave alone for other
|
||||||
|
reasons are still expected to be rejected by `isearch-filter-predicate'.
|
||||||
|
|
||||||
REGEXP-FLAG, DELIMITED-FLAG and CASE-FOLD say how to search for FROM, as
|
REGEXP-FLAG, DELIMITED-FLAG and CASE-FOLD say how to search for FROM, as
|
||||||
in `replace-search'."
|
in `replace-search'."
|
||||||
(replace-preview-cleanup)
|
(replace-preview-cleanup)
|
||||||
(let ((nocasify (not (and case-replace case-fold)))
|
(let ((nocasify (not (and case-replace case-fold)))
|
||||||
(literal (or (not regexp-flag) (eq regexp-flag 'literal)))
|
(literal (or (not regexp-flag) (eq regexp-flag 'literal)))
|
||||||
(limit (window-end nil t)))
|
(limit (min (window-end nil t) (or end (point-max)))))
|
||||||
(save-excursion
|
(save-excursion
|
||||||
(save-match-data
|
(save-match-data
|
||||||
(goto-char (window-start))
|
(goto-char (max (window-start) (or start (point-min))))
|
||||||
(while (and (< (point) limit)
|
(while (and (< (point) limit)
|
||||||
(replace-search from limit regexp-flag delimited-flag
|
(replace-search from limit regexp-flag delimited-flag
|
||||||
case-fold))
|
case-fold))
|
||||||
|
|
@ -473,21 +492,31 @@ in `replace-search'."
|
||||||
(when (and (= beg end) (not (eobp)))
|
(when (and (= beg end) (not (eobp)))
|
||||||
(forward-char 1))))))))
|
(forward-char 1))))))))
|
||||||
|
|
||||||
(defun replace-preview-setup (from regexp-flag delimited-flag)
|
(defun replace-preview-setup (from regexp-flag delimited-flag
|
||||||
|
&optional backward)
|
||||||
"Return a closure that previews the replacement of FROM.
|
"Return a closure that previews the replacement of FROM.
|
||||||
Add it to `minibuffer-setup-hook' while reading the replacement text:
|
Add it to `minibuffer-setup-hook' while reading the replacement text:
|
||||||
on every change it shows, in the original window, how the visible
|
on every change it shows, in the original window, how the visible
|
||||||
matches of FROM would look after the replacement.
|
matches of FROM would look after the replacement.
|
||||||
|
|
||||||
|
FROM nil means that the same minibuffer reads it too, as at the prompt
|
||||||
|
of `query-replace-read-from': then both halves come from the input, and
|
||||||
|
nothing is previewed until it has both of them.
|
||||||
|
|
||||||
REGEXP-FLAG and DELIMITED-FLAG say how to search for FROM, as in
|
REGEXP-FLAG and DELIMITED-FLAG say how to search for FROM, as in
|
||||||
`replace-search'."
|
`replace-search'. BACKWARD non-nil means that the replacement will go
|
||||||
|
from point to the beginning of the buffer instead of to its end."
|
||||||
(if (or (not query-replace-show-preview) (minibufferp))
|
(if (or (not query-replace-show-preview) (minibufferp))
|
||||||
#'ignore
|
#'ignore
|
||||||
(let ((unwind (make-symbol "replace-preview--unwind"))
|
(let ((unwind (make-symbol "replace-preview--unwind"))
|
||||||
(after-change (make-symbol "replace-preview--after-change"))
|
(after-change (make-symbol "replace-preview--after-change"))
|
||||||
(buffer (current-buffer))
|
(buffer (current-buffer))
|
||||||
(case-fold (if (and case-fold-search search-upper-case)
|
;; All of an active region is replaced, and only the
|
||||||
(isearch-no-upper-case-p from regexp-flag)
|
;; filter below keeps the preview inside it.
|
||||||
case-fold-search))
|
(bounds (unless (use-region-p)
|
||||||
|
(if backward
|
||||||
|
(cons (point-min) (point))
|
||||||
|
(cons (point) (point-max)))))
|
||||||
(region-filter (when (use-region-p)
|
(region-filter (when (use-region-p)
|
||||||
(replace--region-filter
|
(replace--region-filter
|
||||||
(funcall region-extract-function 'bounds)))))
|
(funcall region-extract-function 'bounds)))))
|
||||||
|
|
@ -503,16 +532,18 @@ REGEXP-FLAG and DELIMITED-FLAG say how to search for FROM, as in
|
||||||
(replace-preview-cleanup)))))
|
(replace-preview-cleanup)))))
|
||||||
(fset after-change
|
(fset after-change
|
||||||
(lambda (_beg _end _len)
|
(lambda (_beg _end _len)
|
||||||
(let ((to (minibuffer-contents-no-properties)))
|
(let* ((input (replace-preview--input
|
||||||
|
from (minibuffer-contents)))
|
||||||
|
(search (car input))
|
||||||
|
(replacement (cdr input)))
|
||||||
(with-minibuffer-selected-window
|
(with-minibuffer-selected-window
|
||||||
;; The replacement text is typed one character at a
|
;; The replacement text is typed one character at a
|
||||||
;; time, so it's expected to be invalid meanwhile,
|
;; time, so it's expected to be invalid meanwhile,
|
||||||
;; e.g. when it ends with a backslash or refers to a
|
;; e.g. when it ends with a backslash or refers to a
|
||||||
;; group that the regexp doesn't have.
|
;; group that the regexp doesn't have.
|
||||||
(condition-case nil
|
(condition-case nil
|
||||||
(if (and regexp-flag
|
(if (or
|
||||||
(string-match
|
(null input) (equal search "")
|
||||||
query-replace-eval-replacement-regexp to))
|
|
||||||
;; Neither \, nor \# can be previewed, for
|
;; Neither \, nor \# can be previewed, for
|
||||||
;; different reasons. \, is a Lisp expression
|
;; different reasons. \, is a Lisp expression
|
||||||
;; that the user is still typing: evaluating it
|
;; that the user is still typing: evaluating it
|
||||||
|
|
@ -523,9 +554,17 @@ REGEXP-FLAG and DELIMITED-FLAG say how to search for FROM, as in
|
||||||
;; made yet, so the preview would show 0 for
|
;; made yet, so the preview would show 0 for
|
||||||
;; every match where the replacement itself
|
;; every match where the replacement itself
|
||||||
;; will show 0, 1, 2...
|
;; will show 0, 1, 2...
|
||||||
|
(and regexp-flag
|
||||||
|
(string-match
|
||||||
|
query-replace-eval-replacement-regexp
|
||||||
|
replacement)))
|
||||||
(replace-preview-cleanup)
|
(replace-preview-cleanup)
|
||||||
(replace-preview-update from to regexp-flag
|
(replace-preview-update
|
||||||
delimited-flag case-fold))
|
search replacement regexp-flag delimited-flag
|
||||||
|
(if (and case-fold-search search-upper-case)
|
||||||
|
(isearch-no-upper-case-p search regexp-flag)
|
||||||
|
case-fold-search)
|
||||||
|
(car bounds) (cdr bounds)))
|
||||||
(error (replace-preview-cleanup)))))))
|
(error (replace-preview-cleanup)))))))
|
||||||
(lambda ()
|
(lambda ()
|
||||||
(add-hook 'minibuffer-exit-hook unwind nil t)
|
(add-hook 'minibuffer-exit-hook unwind nil t)
|
||||||
|
|
@ -536,17 +575,19 @@ REGEXP-FLAG and DELIMITED-FLAG say how to search for FROM, as in
|
||||||
region-filter)))
|
region-filter)))
|
||||||
(funcall after-change nil nil nil)))))
|
(funcall after-change nil nil nil)))))
|
||||||
|
|
||||||
(defun query-replace-read-to (from prompt regexp-flag &optional delimited-flag)
|
(defun query-replace-read-to (from prompt regexp-flag &optional delimited-flag
|
||||||
|
backward)
|
||||||
"Query and return the TO argument of a `query-replace' operation.
|
"Query and return the TO argument of a `query-replace' operation.
|
||||||
Prompt with PROMPT. REGEXP-FLAG non-nil means the response
|
Prompt with PROMPT. REGEXP-FLAG non-nil means the response
|
||||||
should a regexp.
|
should a regexp.
|
||||||
DELIMITED-FLAG is used to search for the occurrences of FROM when
|
DELIMITED-FLAG and BACKWARD are used to search for the occurrences of
|
||||||
previewing the replacement (see `query-replace-show-preview')."
|
FROM when previewing the replacement (see `query-replace-show-preview')."
|
||||||
(query-replace-compile-replacement
|
(query-replace-compile-replacement
|
||||||
(save-excursion
|
(save-excursion
|
||||||
(let* ((history-add-new-input nil)
|
(let* ((history-add-new-input nil)
|
||||||
(to (minibuffer-with-setup-hook
|
(to (minibuffer-with-setup-hook
|
||||||
(replace-preview-setup from regexp-flag delimited-flag)
|
(replace-preview-setup from regexp-flag delimited-flag
|
||||||
|
backward)
|
||||||
(read-from-minibuffer
|
(read-from-minibuffer
|
||||||
(format "%s %s with: " prompt (query-replace-descr from))
|
(format "%s %s with: " prompt (query-replace-descr from))
|
||||||
nil nil nil
|
nil nil nil
|
||||||
|
|
@ -560,9 +601,17 @@ previewing the replacement (see `query-replace-show-preview')."
|
||||||
(unless noerror
|
(unless noerror
|
||||||
(barf-if-buffer-read-only))
|
(barf-if-buffer-read-only))
|
||||||
(save-mark-and-excursion
|
(save-mark-and-excursion
|
||||||
(let* ((delimited-flag (and current-prefix-arg
|
(let* ((query-replace-show-preview
|
||||||
|
;; No highlighting in this buffer means no preview in it.
|
||||||
|
(and (not no-highlight) query-replace-show-preview))
|
||||||
|
(delimited-flag (and current-prefix-arg
|
||||||
(not (eq current-prefix-arg '-))))
|
(not (eq current-prefix-arg '-))))
|
||||||
|
(backward (and current-prefix-arg (eq current-prefix-arg '-)))
|
||||||
(from (minibuffer-with-setup-hook
|
(from (minibuffer-with-setup-hook
|
||||||
|
;; A nil FROM: this input can hold both halves.
|
||||||
|
(replace-preview-setup nil regexp-flag delimited-flag
|
||||||
|
backward)
|
||||||
|
(minibuffer-with-setup-hook
|
||||||
(minibuffer-lazy-highlight-setup
|
(minibuffer-lazy-highlight-setup
|
||||||
:case-fold case-fold-search
|
:case-fold case-fold-search
|
||||||
:filter (when (use-region-p)
|
:filter (when (use-region-p)
|
||||||
|
|
@ -584,15 +633,15 @@ previewing the replacement (see `query-replace-show-preview')."
|
||||||
(setq isearch-case-fold-search
|
(setq isearch-case-fold-search
|
||||||
(isearch-no-upper-case-p from-string regexp-flag)))
|
(isearch-no-upper-case-p from-string regexp-flag)))
|
||||||
from-string)))
|
from-string)))
|
||||||
(query-replace-read-from prompt regexp-flag)))
|
(query-replace-read-from prompt regexp-flag))))
|
||||||
(to (if (consp from) (prog1 (cdr from) (setq from (car from)))
|
(to (if (consp from) (prog1 (cdr from) (setq from (car from)))
|
||||||
(query-replace-read-to from prompt regexp-flag
|
(query-replace-read-to from prompt regexp-flag
|
||||||
delimited-flag))))
|
delimited-flag backward))))
|
||||||
(list from to
|
(list from to
|
||||||
(or delimited-flag
|
(or delimited-flag
|
||||||
(and (plist-member (text-properties-at 0 from) 'isearch-regexp-function)
|
(and (plist-member (text-properties-at 0 from) 'isearch-regexp-function)
|
||||||
(get-text-property 0 'isearch-regexp-function from)))
|
(get-text-property 0 'isearch-regexp-function from)))
|
||||||
(and current-prefix-arg (eq current-prefix-arg '-))))))
|
backward))))
|
||||||
|
|
||||||
(defun query-replace-read-transpose-from-to ()
|
(defun query-replace-read-transpose-from-to ()
|
||||||
"Transpose the FROM and TO arguments of a `query-replace' operation.
|
"Transpose the FROM and TO arguments of a `query-replace' operation.
|
||||||
|
|
|
||||||
|
|
@ -705,9 +705,11 @@ bound to HIGHLIGHT-LOCUS."
|
||||||
(if (match-string 2) "R" "L"))))
|
(if (match-string 2) "R" "L"))))
|
||||||
(should (equal (buffer-string) after)))))
|
(should (equal (buffer-string) after)))))
|
||||||
|
|
||||||
(defun replace-tests--preview (text from to regexp-flag &optional case-fold)
|
(defun replace-tests--preview (text from to regexp-flag &optional case-fold
|
||||||
|
start end)
|
||||||
"Return the previews of replacing FROM with TO in a buffer holding TEXT.
|
"Return the previews of replacing FROM with TO in a buffer holding TEXT.
|
||||||
Each preview is a list (BEG END STRING).
|
Each preview is a list (BEG END STRING). START and END limit the
|
||||||
|
previewed portion of the buffer.
|
||||||
Unless the caller binds `query-replace-show-preview' to something else,
|
Unless the caller binds `query-replace-show-preview' to something else,
|
||||||
the previews are those of `replacement-only'."
|
the previews are those of `replacement-only'."
|
||||||
(let ((query-replace-show-preview
|
(let ((query-replace-show-preview
|
||||||
|
|
@ -717,7 +719,8 @@ the previews are those of `replacement-only'."
|
||||||
(set-window-buffer (selected-window) (current-buffer))
|
(set-window-buffer (selected-window) (current-buffer))
|
||||||
(unwind-protect
|
(unwind-protect
|
||||||
(progn
|
(progn
|
||||||
(replace-preview-update from to regexp-flag nil case-fold)
|
(replace-preview-update from to regexp-flag nil case-fold
|
||||||
|
start end)
|
||||||
(mapcar (lambda (ov)
|
(mapcar (lambda (ov)
|
||||||
(list (overlay-start ov)
|
(list (overlay-start ov)
|
||||||
(overlay-end ov)
|
(overlay-end ov)
|
||||||
|
|
@ -750,6 +753,26 @@ the previews are those of `replacement-only'."
|
||||||
(should (equal (replace-tests--preview "foo\n" "foo" "" nil)
|
(should (equal (replace-tests--preview "foo\n" "foo" "" nil)
|
||||||
'((1 4 " ")))))
|
'((1 4 " ")))))
|
||||||
|
|
||||||
|
(ert-deftest replace-tests-preview-bounds ()
|
||||||
|
;; Only the matches that would be replaced are previewed.
|
||||||
|
(should (equal (replace-tests--preview "foo foo foo\n" "foo" "bar" nil nil 5)
|
||||||
|
'((5 8 "bar") (9 12 "bar"))))
|
||||||
|
;; When the replacement goes backward, the other side is the valid one.
|
||||||
|
(should (equal (replace-tests--preview "foo foo foo\n" "foo" "bar" nil nil
|
||||||
|
nil 8)
|
||||||
|
'((1 4 "bar") (5 8 "bar")))))
|
||||||
|
|
||||||
|
(ert-deftest replace-tests-preview-input ()
|
||||||
|
(let ((from-to (concat "foo" (propertize " \N{RIGHTWARDS ARROW} "
|
||||||
|
'separator t)
|
||||||
|
"bar")))
|
||||||
|
;; While only the replacement is read, FROM is fixed.
|
||||||
|
(should (equal (replace-preview--input "foo" "bar") '("foo" . "bar")))
|
||||||
|
;; While both halves are read, they are split apart.
|
||||||
|
(should (equal (replace-preview--input nil from-to) '("foo" . "bar")))
|
||||||
|
;; There is nothing to preview until the input has both of them.
|
||||||
|
(should-not (replace-preview--input nil "foo"))))
|
||||||
|
|
||||||
(ert-deftest replace-tests-preview-both ()
|
(ert-deftest replace-tests-preview-both ()
|
||||||
(let ((query-replace-show-preview 'both)
|
(let ((query-replace-show-preview 'both)
|
||||||
(arrow (if (char-displayable-p ?→) "→" "->")))
|
(arrow (if (char-displayable-p ?→) "→" "->")))
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue