>From 87ad2a45293e2fa1796a39f368d9ec16c0c4c070 Mon Sep 17 00:00:00 2001 From: Augusto Stoffel Date: Sat, 8 Jan 2022 11:08:46 +0100 Subject: [PATCH] Display lazy highlight and match count in when reading regexps * lisp/isearch.el (lazy-count-update-hook): New hook allowing to display the lazy count in special ways. (isearch-edit-string): Add lazy highlight and count of matching text. (isearch-lazy-highlight-new-loop, isearch-lazy-highlight-buffer-update): Use `isearch-lazy-count-display-function' instead of hardcoded call to `isearch-message'. (minibuffer-lazy-count-format, minibuffer-lazy-highlight-transform, minibuffer-lazy-highlight--overlay, minibuffer-lazy-highlight--count, minibuffer-lazy-highlight--after-change, minibuffer-lazy-highlight--exit, minibuffer-lazy-highlight-setup): Variables and functions implementing the lazy highlight functionality while reading from minibuffer. * lisp/replace.el (query-replace-read-from): Add lazy highlighting. (replace--region-filter): New function, extracted from 'perform-replace'. --- lisp/isearch.el | 84 +++++++++++++++++++++++++++++++++++++++++++++---- lisp/replace.el | 58 ++++++++++++++++++++++++---------- 2 files changed, 120 insertions(+), 22 deletions(-) diff --git a/lisp/isearch.el b/lisp/isearch.el index 8970216398..6076cc4b5e 100644 --- a/lisp/isearch.el +++ b/lisp/isearch.el @@ -1812,6 +1812,8 @@ isearch-edit-string (minibuffer-history-symbol) ;; Search string might have meta information on text properties. (minibuffer-allow-text-properties t)) + (when isearch-lazy-highlight + (add-hook 'minibuffer-setup-hook #'minibuffer-lazy-highlight-setup)) (setq isearch-new-string (read-from-minibuffer (isearch-message-prefix nil isearch-nonincremental) @@ -3990,6 +3992,8 @@ isearch-lazy-highlight-error (defvar isearch-lazy-count-current nil) (defvar isearch-lazy-count-total nil) (defvar isearch-lazy-count-hash (make-hash-table)) +(defvar lazy-count-update-hook nil + "Hook run after new lazy count results are computed.") (defun lazy-highlight-cleanup (&optional force procrastinate) "Stop lazy highlighting and remove extra highlighting from current buffer. @@ -4048,7 +4052,7 @@ isearch-lazy-highlight-new-loop isearch-lazy-highlight-window-end)))))) ;; something important did indeed change (lazy-highlight-cleanup t (not (equal isearch-string ""))) ;stop old timer - (when (and isearch-lazy-count isearch-mode (null isearch-message-function)) + (when isearch-lazy-count (when (or (equal isearch-string "") ;; Check if this place was reached by a condition above ;; other than changed window boundaries (that shouldn't @@ -4067,7 +4071,10 @@ isearch-lazy-highlight-new-loop (setq isearch-lazy-count-current nil isearch-lazy-count-total nil) ;; Delay updating the message if possible, to avoid flicker - (when (string-equal isearch-string "") (isearch-message)))) + (when (string-equal isearch-string "") + (when (and isearch-mode (null isearch-message-function)) + (isearch-message)) + (run-hooks 'lazy-count-update-hook)))) (setq isearch-lazy-highlight-window-start-changed nil) (setq isearch-lazy-highlight-window-end-changed nil) (setq isearch-lazy-highlight-error isearch-error) @@ -4120,13 +4127,15 @@ isearch-lazy-highlight-new-loop 'isearch-lazy-highlight-start)))) ;; Update the current match number only in isearch-mode and ;; unless isearch-mode is used specially with isearch-message-function - (when (and isearch-lazy-count isearch-mode (null isearch-message-function)) + (when isearch-lazy-count ;; Update isearch-lazy-count-current only when it was already set ;; at the end of isearch-lazy-highlight-buffer-update (when isearch-lazy-count-current (setq isearch-lazy-count-current (gethash (point) isearch-lazy-count-hash 0)) - (isearch-message)))) + (when (and isearch-mode (null isearch-message-function)) + (isearch-message)) + (run-hooks 'lazy-count-update-hook)))) (defun isearch-lazy-highlight-search (string bound) "Search ahead for the next or previous match, for lazy highlighting. @@ -4327,16 +4336,79 @@ isearch-lazy-highlight-buffer-update (setq looping nil nomore t)))) (if nomore - (when (and isearch-lazy-count isearch-mode (null isearch-message-function)) + (when isearch-lazy-count (unless isearch-lazy-count-total (setq isearch-lazy-count-total 0)) (setq isearch-lazy-count-current (gethash opoint isearch-lazy-count-hash 0)) - (isearch-message)) + (when (and isearch-mode (null isearch-message-function)) + (isearch-message)) + (run-hooks 'lazy-count-update-hook)) (setq isearch-lazy-highlight-timer (run-at-time lazy-highlight-interval nil 'isearch-lazy-highlight-buffer-update))))))))) +;; Reading from minibuffer with lazy highlight and match count + +(defcustom minibuffer-lazy-count-format "%s " + "Format of the total number of matches for the prompt prefix." + :type '(choice (const :tag "Don't display a count" nil) + (string :tag "Display match count" "%s ")) + :group 'lazy-count + :version "29.1") + +(defvar minibuffer-lazy-highlight-transform #'identity + "Function to transform minibuffer text into a `isearch-string' for highlighting.") + +(defvar minibuffer-lazy-highlight--overlay nil + "Overlay for minibuffer prompt updates.") + +(defun minibuffer-lazy-highlight--count () + "Display total match count in the minibuffer prompt." + (when minibuffer-lazy-highlight--overlay + (overlay-put minibuffer-lazy-highlight--overlay + 'after-string + (and isearch-lazy-count-total + (not isearch-error) + (format minibuffer-lazy-count-format + isearch-lazy-count-total))))) + +(defun minibuffer-lazy-highlight--after-change (_beg _end _len) + "Update lazy highlight state in minibuffer selected window." + (when isearch-lazy-highlight + (let ((inhibit-redisplay t) ;; Avoid cursor flickering + (string (minibuffer-contents))) + (with-minibuffer-selected-window + (setq isearch-string (funcall minibuffer-lazy-highlight-transform string)) + (isearch-lazy-highlight-new-loop))))) + +(defun minibuffer-lazy-highlight--exit () + "Unwind changes from `minibuffer-lazy-highlight-setup'." + (remove-hook 'after-change-functions + #'minibuffer-lazy-highlight--after-change) + (remove-hook 'lazy-count-update-hook #'minibuffer-lazy-highlight--count) + (remove-hook 'minibuffer-exit-hook #'minibuffer-lazy-highlight--exit) + (setq minibuffer-lazy-highlight--overlay nil) + (lazy-highlight-cleanup)) + +(defun minibuffer-lazy-highlight-setup () + "Set up minibuffer for lazy highlight of matches in the original window. + +This function is intended to be added to `minibuffer-setup-hook'. +Note that several other isearch variables influence the lazy +highlighting, including `isearch-regexp', +`isearch-lazy-highlight' and `isearch-lazy-count'." + (remove-hook 'minibuffer-setup-hook #'minibuffer-lazy-highlight-setup) + (add-hook 'after-change-functions + #'minibuffer-lazy-highlight--after-change) + (add-hook 'lazy-count-update-hook #'minibuffer-lazy-highlight--count) + (add-hook 'minibuffer-exit-hook #'minibuffer-lazy-highlight--exit) + (setq minibuffer-lazy-highlight--overlay + (and minibuffer-lazy-count-format + (make-overlay (point-min) (point-min) (current-buffer) t))) + (minibuffer-lazy-highlight--after-change nil nil nil)) + + (defun isearch-resume (string regexp word forward message case-fold) "Resume an incremental search. STRING is the string or regexp searched for. diff --git a/lisp/replace.el b/lisp/replace.el index 06be597855..bd5a694110 100644 --- a/lisp/replace.el +++ b/lisp/replace.el @@ -254,11 +254,31 @@ query-replace-read-from (query-replace-descr (cdar query-replace-defaults))))) (t (format-prompt prompt nil)))) + ;; Variables controlling lazy highlighting while reading + ;; FROM regexp, which is set up below. + (isearch-lazy-highlight query-replace-lazy-highlight) + (isearch-regexp regexp-flag) + (isearch-regexp-function nil) + (isearch-case-fold-search case-fold-search) ;; FIXME: the case-folding rule here is complicated... + (minibuffer-lazy-highlight-transform + (lambda (string) + (let ((from (query-replace--split-string string))) + (if (consp from) (car from) from)))) (from ;; The save-excursion here is in case the user marks and copies ;; a region in order to specify the minibuffer input. ;; That should not clobber the region for the query-replace itself. (save-excursion + (when query-replace-lazy-highlight + (add-hook 'minibuffer-setup-hook #'minibuffer-lazy-highlight-setup) + (when (use-region-p) + (letrec ((region-filter (replace--region-filter + (funcall region-extract-function 'bounds))) + (cleanup (lambda () + (remove-function isearch-filter-predicate region-filter) + (remove-hook 'minibuffer-exit-hook cleanup)))) + (add-function :after-while isearch-filter-predicate region-filter) + (add-hook 'minibuffer-exit-hook cleanup)))) (minibuffer-with-setup-hook (lambda () (setq-local text-property-default-nonsticky @@ -2773,6 +2793,26 @@ replace--push-stack ,search-str ,next-replace) ,stack)) +(defun replace--region-filter (bounds) + "Return a function that decides if a region is inside BOUNDS. +BOUNDS is a list of cons cells of the form (START . END). The +returned function takes as argument two buffer positions, START +and END." + (let ((region-bounds + (mapcar (lambda (position) + (cons (copy-marker (car position)) + (copy-marker (cdr position)))) + bounds))) + (lambda (start end) + (delq nil (mapcar + (lambda (bounds) + (and + (>= start (car bounds)) + (<= start (cdr bounds)) + (>= end (car bounds)) + (<= end (cdr bounds)))) + region-bounds))))) + (defun perform-replace (from-string replacements query-flag regexp-flag delimited-flag &optional repeat-count map start end backward region-noncontiguous-p) @@ -2857,22 +2897,8 @@ perform-replace ;; Unless a single contiguous chunk is selected, operate on multiple chunks. (when region-noncontiguous-p - (let ((region-bounds - (mapcar (lambda (position) - (cons (copy-marker (car position)) - (copy-marker (cdr position)))) - (funcall region-extract-function 'bounds)))) - (setq region-filter - (lambda (start end) - (delq nil (mapcar - (lambda (bounds) - (and - (>= start (car bounds)) - (<= start (cdr bounds)) - (>= end (car bounds)) - (<= end (cdr bounds)))) - region-bounds)))) - (add-function :after-while isearch-filter-predicate region-filter))) + (setq region-filter (replace--region-filter + (funcall region-extract-function 'bounds)))) ;; If region is active, in Transient Mark mode, operate on region. (if backward -- 2.35.1