bug#81583: 32.0.50; Optional preview of the replacement text while typing it
Rahul Martim Juliato <[email protected]>
| Newsgroups | gmane.emacs.bugs |
|---|---|
| Message-ID | <[email protected]> |
Juri Linkov <[email protected]> writes: >> Personally, I'd prefer to keep the same "topic" in a single bug thread, >> if you're okay with committing 'bug#81583' in several commits. > > This would be nice. > >> Would you mind listing the features you had in mind? > > 1. We could add a special value 'both' of the new option > 'query-replace-show-preview' to customize the display of > both strings side by side (match → replacement). > Hi Juri! Thanks for your patience. Here's an almost final scratch of what I'm working on. Before I go ahead with tests and manual entries, I'd like to share it with you for reviewing code, feature expectations, grammar, doc length and so on. In summary 'query-replace-show-preview' can now be: - nil (default current behavior) - t (replaces are shown "as you type") - both (shows match→replacement as you type) - fn (a function users can customize receiving match and replace and come with their own ideas) Users can also set 'query-replace-preview-separator' to: - nil (automatic default, will use → or -> if not available) - "" (a custom string in case of wanting another symbol or adding spaces around, like " → ") Regarding faces, following this scheme: [match]→[replace] 1 2 3 The provided faces are: 1 -> query-replace-preview-match 2 -> query-replace-preview-separator 3 -> query-replace-preview Please find attached the diff and some screenshots. > 2. Another feature that could be added is to have a variable > that contains a string to display for zero-length matches. > Like a vertical bar for empty rectangular region after 'C-x SPC'. This will be next and hopefully easier :) Thanks, -- Rahul Martim Juliato
replace-preview.diff
(application/octet-stream, 7.2 KB)
lisp/replace.el | 121 +++++++++++++++++++++++++++++++++++++++++++++++++-------
1 file changed, 106 insertions(+), 15 deletions(-)
diff --git a/lisp/replace.el b/lisp/replace.el
index 4bdb80dc531..2952544609b 100644
--- a/lisp/replace.el
+++ b/lisp/replace.el
@@ -116,11 +116,53 @@ query-replace-show-replacement
(defcustom query-replace-show-preview nil
"Non-nil means show preview of the result of replacement while you type it.
-The matches that are visible in the window are shown as they would look after
-the replacement, using the `query-replace-preview' face. This tells
-you what back-references like \\1 expand to before you commit to the
-edit. However, replacements that use \\, or \\# are not previewed."
- :type 'boolean
+The matches visible in the window are shown as they would look after the
+replacement. This tells you what back-references like \\1 expand to
+before you commit to the edit. However, replacements that use \\, or
+\\# are not previewed.
+
+If the value is t, each match is shown as the replacement text, using
+the `query-replace-preview' face.
+
+If the value is `both', the match and the replacement are shown side by
+side, separated by `query-replace-preview-separator-string', with the
+faces `query-replace-preview-match', `query-replace-preview-separator'
+and `query-replace-preview'.
+
+The value can also be a function of two arguments, the matched text and
+the replacement text, that returns the fstring to display in place of the
+match, or nil to leave that match alone. Both arguments can carry text
+properties from the buffer. The replacement already has back-references
+expanded and the case adapted. The returned string is displayed as is,
+with no face added to it, so it is up to the function to propertize it.
+The function is called for every match visible in the window on every
+keystroke, so it should be fast and free of side effects; it is called
+while the replacement is still being typed, hence with a replacement
+that is not final, and if it signals an error no preview is shown until
+the next keystroke. The match data is available to the function, and
+must not be modified. For example, this shows the match and the
+replacement side by side, with no face:
+
+ (lambda (match replacement)
+ (concat \"[\" match \"]~>{\" replacement \"}\"))
+
+while this shows only the replacement, in the `success' face:
+
+ (lambda (_match replacement)
+ (propertize replacement \\='face \\='success))"
+ :type '(choice (const :tag "No preview" nil)
+ (const :tag "Show the replacement" t)
+ (const :tag "Show the match and the replacement" both)
+ (function :tag "Function"))
+ :group 'matching
+ :version "32.1")
+
+(defcustom query-replace-preview-separator-string nil
+ "String shown between the match and the replacement in the preview.
+If nil, use \"→\" when it is displayable on the selected frame, and
+\"->\" otherwise. Used when `query-replace-show-preview' is `both'."
+ :type '(choice (const :tag "Automatic" nil)
+ (string :tag "String"))
:group 'matching
:version "32.1")
@@ -169,6 +211,21 @@ query-replace-preview
:group 'matching
:version "32.1")
+(defface query-replace-preview-match
+ '((t (:inherit lazy-highlight :strike-through t)))
+ "Face for the matched text in the preview.
+Used for the left half of the preview when `query-replace-show-preview'
+is `both'."
+ :group 'matching
+ :version "32.1")
+
+(defface query-replace-preview-separator
+ '((t (:inherit shadow)))
+ "Face for `query-replace-preview-separator-string' in the preview.
+Used when `query-replace-show-preview' is `both'."
+ :group 'matching
+ :version "32.1")
+
(defvar replace-count 0
"Number of replacements done so far.
See `replace-regexp'.")
@@ -372,12 +429,45 @@ replace-preview-cleanup
(mapc #'delete-overlay replace-preview-overlays)
(setq replace-preview-overlays nil))
+(defun query-replace-preview-separator ()
+ "Return the string to show between the match and the replacement.
+Use `query-replace-preview-separator-string' when it is non-nil,
+otherwise \"→\" when displayable on the selected frame, or \"->\"."
+ (or query-replace-preview-separator-string
+ (if (char-displayable-p ?→) "→" "->")))
+
+(defun replace-preview--propertize (text face)
+ "Return a copy of TEXT with all its text properties replaced by FACE.
+Original properties are dropped because TEXT can come from the buffer,
+and a `display' or `invisible' property inside an overlay string would
+show something other than the preview."
+ (setq text (substring-no-properties text))
+ (add-face-text-property 0 (length text) face nil text)
+ text)
+
+(defun replace-preview--format (match replacement)
+ "Return the text to show in place of MATCH, or nil for no preview.
+MATCH is the matched text and REPLACEMENT is the text that would replace
+it, both of which can carry text properties from the buffer. How they
+are combined is decided by `query-replace-show-preview'."
+ (pcase query-replace-show-preview
+ ('both (concat (replace-preview--propertize match 'query-replace-preview-match)
+ (replace-preview--propertize (query-replace-preview-separator)
+ 'query-replace-preview-separator)
+ (replace-preview--propertize replacement 'query-replace-preview)))
+ ((and (pred functionp) fun) (funcall fun match replacement))
+ (_ (replace-preview--propertize replacement 'query-replace-preview))))
+
+
(defun replace-preview-update (from to regexp-flag delimited-flag case-fold)
"Preview the result of replacing FROM with TO in the current buffer.
-Each match of FROM visible in the selected window is displayed as the
-text it would be replaced with, using the `query-replace-preview' face.
-REGEXP-FLAG, DELIMITED-FLAG and CASE-FOLD say how to search for FROM,
-as in `replace-search'."
+Each match of FROM visible in the selected window gets an overlay
+showing the text that `replace-preview--format' returns for it, which
+depends on `query-replace-show-preview'. Matches for which it returns
+nil are left alone.
+
+REGEXP-FLAG, DELIMITED-FLAG and CASE-FOLD say how to search for FROM, as
+in `replace-search'."
(replace-preview-cleanup)
(let ((nocasify (not (and case-replace case-fold)))
(literal (or (not regexp-flag) (eq regexp-flag 'literal)))
@@ -390,13 +480,14 @@ replace-preview-update
case-fold))
(let* ((beg (match-beginning 0))
(end (match-end 0))
- (text (propertize (match-substitute-replacement
- to nocasify literal)
- 'face 'query-replace-preview)))
- (when (funcall isearch-filter-predicate beg end)
+ (text (replace-preview--format
+ (buffer-substring beg end)
+ (match-substitute-replacement to nocasify literal))))
+ (when (and text (funcall isearch-filter-predicate beg end))
(let ((ov (make-overlay beg end)))
- ;; A zero-length overlay displays nothing, so for an
- ;; empty match show the replacement next to it instead.
+ ;; FIXME: A zero-length overlay displays nothing, so for
+ ;; an empty match show the replacement next to it
+ ;; instead.
(if (= beg end)
(overlay-put ov 'before-string text)
(overlay-put ov 'display text))
both-option-dark.png
(image/png, 135.9 KB) - not displayed
both-option-light.png
(image/png, 132.2 KB) - not displayed