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
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.