Subject: Re: bug#29360: 26.0; Add full-buffer choice for `isearch-lazy-highlight'

From: Juri Linkov <[email-masked]>
Date: Thu, 25 Oct 2018 02:11:25 +0300

> Absolutely.  Like with any similar interaction in
> Emacs the delay before starting should be a user
> option.  Users are different, and user machines etc.
> are different.

You are right, with a small delay lazy-highlighting is responsive.

In the attached patch I also added a separate variable that you asked for
programmatic use, changed text to "currently visible on the screen" and
mentioned lazy-highlight-cleanup.

The patch is extensively tested for various scenarios:

diff --git a/lisp/isearch.el b/lisp/isearch.el
index 1e785a44c5..dae85208cb 100644
--- a/lisp/isearch.el
+++ b/lisp/isearch.el
@@ -304,9 +304,9 @@ isearch-fail
 
 (defcustom isearch-lazy-highlight t
   "Controls the lazy-highlighting during incremental search.
-When non-nil, all text in the buffer matching the current search
-string is highlighted lazily (see `lazy-highlight-initial-delay'
-and `lazy-highlight-interval').
+When non-nil, all text currently visible on the screen
+matching the current search string is highlighted lazily
+(see `lazy-highlight-initial-delay' and `lazy-highlight-interval').
 
 When multiple windows display the current buffer, the
 highlighting is displayed only on the selected window, unless
@@ -316,6 +316,17 @@ isearch-lazy-highlight
   :group 'lazy-highlight
   :group 'isearch)
 
+(defcustom isearch-lazy-highlight-buffer nil
+  "Controls the lazy-highlighting of the full buffer.
+When non-nil, all text in the buffer matching the current search
+string is highlighted lazily (see `lazy-highlight-initial-delay',
+`lazy-highlight-interval' and `lazy-highlight-buffer-max-at-a-time').
+This is useful when `lazy-highlight-cleanup' is customized to nil
+and doesn't remove full-buffer highlighting after a search."
+  :type 'boolean
+  :group 'lazy-highlight
+  :group 'isearch)
+
 ;;; Lazy highlight customization.
 
 (defgroup lazy-highlight nil
@@ -351,6 +362,15 @@ lazy-highlight-max-at-a-time
 		 (integer :tag "Some"))
   :group 'lazy-highlight)
 
+(defcustom lazy-highlight-buffer-max-at-a-time 20
+  "Maximum matches to highlight at a time (for `isearch-lazy-highlight-buffer').
+Larger values may reduce Isearch's responsiveness to user input;
+smaller values make matches highlight slowly.
+A value of nil means highlight all matches shown in the buffer."
+  :type '(choice (const :tag "All" nil)
+		 (integer :tag "Some"))
+  :group 'lazy-highlight)
+
 (defface lazy-highlight
   '((((class color) (min-colors 88) (background light))
      (:background "paleturquoise"))
@@ -3178,6 +3198,7 @@ isearch-lazy-highlight-window
 (defvar isearch-lazy-highlight-window-group nil)
 (defvar isearch-lazy-highlight-window-start nil)
 (defvar isearch-lazy-highlight-window-end nil)
+(defvar isearch-lazy-highlight-buffer-p nil)
 (defvar isearch-lazy-highlight-case-fold-search nil)
 (defvar isearch-lazy-highlight-regexp nil)
 (defvar isearch-lazy-highlight-lax-whitespace nil)
@@ -3226,10 +3247,12 @@ isearch-lazy-highlight-new-loop
 			  isearch-lax-whitespace))
 		 (not (eq isearch-lazy-highlight-regexp-lax-whitespace
 			  isearch-regexp-lax-whitespace))
-                 (not (= (window-group-start)
-                         isearch-lazy-highlight-window-start))
-                 (not (= (window-group-end) ; Window may have been split/joined.
-			 isearch-lazy-highlight-window-end))
+		 (not (or isearch-lazy-highlight-buffer-p
+			  (= (window-group-start)
+			     isearch-lazy-highlight-window-start)))
+		 (not (or isearch-lazy-highlight-buffer-p
+			  (= (window-group-end) ; Window may have been split/joined.
+			     isearch-lazy-highlight-window-end)))
 		 (not (eq isearch-forward
 			  isearch-lazy-highlight-forward))
 		 ;; In case we are recovering from an error.
@@ -3247,6 +3270,7 @@ isearch-lazy-highlight-new-loop
           isearch-lazy-highlight-window-group (selected-window-group)
 	  isearch-lazy-highlight-window-start (window-group-start)
 	  isearch-lazy-highlight-window-end   (window-group-end)
+	  isearch-lazy-highlight-buffer-p     isearch-lazy-highlight-buffer
 	  ;; Start lazy-highlighting at the beginning of the found
 	  ;; match (`isearch-other-end').  If no match, use point.
 	  ;; One of the next two variables (depending on search direction)
@@ -3264,12 +3288,22 @@ isearch-lazy-highlight-new-loop
 	  isearch-lazy-highlight-regexp-lax-whitespace isearch-regexp-lax-whitespace
 	  isearch-lazy-highlight-regexp-function  isearch-regexp-function
 	  isearch-lazy-highlight-forward      isearch-forward)
+    ;; Extend start/end to match whole string at point
+    (if isearch-lazy-highlight-forward
+        (setq isearch-lazy-highlight-start
+	      (min (+ isearch-lazy-highlight-start
+		      (1- (length isearch-lazy-highlight-last-string)))
+		   (point-max)))
+      (setq isearch-lazy-highlight-end
+	    (max (- isearch-lazy-highlight-end
+		    (1- (length isearch-lazy-highlight-last-string)))
+		 (point-min))))
     (unless (equal isearch-string "")
       (setq isearch-lazy-highlight-timer
             (run-with-idle-timer lazy-highlight-initial-delay nil
                                  'isearch-lazy-highlight-start)))))
 
-(defun isearch-lazy-highlight-search ()
+(defun isearch-lazy-highlight-search (string bound)
   "Search ahead for the next or previous match, for lazy highlighting.
 Attempt to do the search exactly the way the pending Isearch would."
   (condition-case nil
@@ -3283,24 +3317,10 @@ isearch-lazy-highlight-search
 	    (isearch-forward isearch-lazy-highlight-forward)
 	    (search-invisible nil)	; don't match invisible text
 	    (retry t)
-	    (success nil)
-	    (bound (if isearch-lazy-highlight-forward
-		       (min (or isearch-lazy-highlight-end-limit (point-max))
-			    (if isearch-lazy-highlight-wrapped
-				(+ isearch-lazy-highlight-start
-				   ;; Extend bound to match whole string at point
-				   (1- (length isearch-lazy-highlight-last-string)))
-			      (window-group-end)))
-		     (max (or isearch-lazy-highlight-start-limit (point-min))
-			  (if isearch-lazy-highlight-wrapped
-			      (- isearch-lazy-highlight-end
-				 ;; Extend bound to match whole string at point
-				 (1- (length isearch-lazy-highlight-last-string)))
-			    (window-group-start))))))
+	    (success nil))
 	;; Use a loop like in `isearch-search'.
 	(while retry
-	  (setq success (isearch-search-string
-			 isearch-lazy-highlight-last-string bound t))
+	  (setq success (isearch-search-string string bound t))
 	  ;; Clear RETRY unless the search predicate says
 	  ;; to skip this search hit.
 	  (if (or (not success)
@@ -3312,6 +3332,20 @@ isearch-lazy-highlight-search
 	success)
     (error nil)))
 
+(defvar isearch-lazy-highlight-match-function #'isearch-lazy-highlight-match
+  "Function that highlights the found match.
+The function accepts two arguments: the beginning and the end of the match.")
+
+(defun isearch-lazy-highlight-match (mb me)
+  (let ((ov (make-overlay mb me)))
+    (push ov isearch-lazy-highlight-overlays)
+    ;; 1000 is higher than ediff's 100+,
+    ;; but lower than isearch main overlay's 1001
+    (overlay-put ov 'priority 1000)
+    (overlay-put ov 'face 'lazy-highlight)
+    (unless (eq isearch-lazy-highlight 'all-windows)
+      (overlay-put ov 'window (selected-window)))))
+
 (defun isearch-lazy-highlight-start ()
   "Start a new lazy-highlight updating loop."
   (lazy-highlight-cleanup t) ;remove old overlays
@@ -3321,19 +3355,32 @@ isearch-lazy-highlight-update
   "Update highlighting of other matches for current search."
   (let ((max lazy-highlight-max-at-a-time)
         (looping t)
-        nomore)
+        nomore window-start window-end)
     (with-local-quit
       (save-selected-window
 	(if (and (window-live-p isearch-lazy-highlight-window)
 		 (not (memq (selected-window) isearch-lazy-highlight-window-group)))
 	    (select-window isearch-lazy-highlight-window))
+	(setq window-start (window-group-start))
+	(setq window-end (window-group-end))
 	(save-excursion
 	  (save-match-data
 	    (goto-char (if isearch-lazy-highlight-forward
 			   isearch-lazy-highlight-end
 			 isearch-lazy-highlight-start))
 	    (while looping
-	      (let ((found (isearch-lazy-highlight-search)))
+	      (let* ((bound (if isearch-lazy-highlight-forward
+		                (min (or isearch-lazy-highlight-end-limit (point-max))
+			             (if isearch-lazy-highlight-wrapped
+				         isearch-lazy-highlight-start
+			               window-end))
+		              (max (or isearch-lazy-highlight-start-limit (point-min))
+			           (if isearch-lazy-highlight-wrapped
+			               isearch-lazy-highlight-end
+			             window-start))))
+		     (found (isearch-lazy-highlight-search
+			     isearch-lazy-highlight-last-string
+			     bound)))
 		(when max
 		  (setq max (1- max))
 		  (if (<= max 0)
@@ -3345,24 +3392,16 @@ isearch-lazy-highlight-update
 			  (if isearch-lazy-highlight-forward
 			      (if (= mb (if isearch-lazy-highlight-wrapped
 					    isearch-lazy-highlight-start
-					  (window-group-end)))
+					  window-end))
 				  (setq found nil)
 				(forward-char 1))
 			    (if (= mb (if isearch-lazy-highlight-wrapped
 					  isearch-lazy-highlight-end
-					(window-group-start)))
+					window-start))
 				(setq found nil)
 			      (forward-char -1)))
-
 			;; non-zero-length match
-			(let ((ov (make-overlay mb me)))
-			  (push ov isearch-lazy-highlight-overlays)
-			  ;; 1000 is higher than ediff's 100+,
-			  ;; but lower than isearch main overlay's 1001
-			  (overlay-put ov 'priority 1000)
-			  (overlay-put ov 'face 'lazy-highlight)
-			  (unless (eq isearch-lazy-highlight 'all-windows)
-                            (overlay-put ov 'window (selected-window)))))
+			(funcall isearch-lazy-highlight-match-function mb me))
 		      ;; Remember the current position of point for
 		      ;; the next call of `isearch-lazy-highlight-update'
 		      ;; when `lazy-highlight-max-at-a-time' is too small.
@@ -3378,17 +3417,83 @@ isearch-lazy-highlight-update
 		      (setq isearch-lazy-highlight-wrapped t)
 		      (if isearch-lazy-highlight-forward
 			  (progn
-			    (setq isearch-lazy-highlight-end (window-group-start))
+			    (setq isearch-lazy-highlight-end window-start)
 			    (goto-char (max (or isearch-lazy-highlight-start-limit (point-min))
-					    (window-group-start))))
-			(setq isearch-lazy-highlight-start (window-group-end))
+					    window-start)))
+			(setq isearch-lazy-highlight-start window-end)
 			(goto-char (min (or isearch-lazy-highlight-end-limit (point-max))
-					(window-group-end))))))))
-	    (unless nomore
+					window-end)))))))
+	    (if nomore
+		(when isearch-lazy-highlight-buffer-p
+		  (if isearch-lazy-highlight-forward
+		      (setq isearch-lazy-highlight-end (point-min))
+		    (setq isearch-lazy-highlight-start (point-max)))
+		  (run-at-time lazy-highlight-interval nil
+			       'isearch-lazy-highlight-buffer-update))
 	      (setq isearch-lazy-highlight-timer
 		    (run-at-time lazy-highlight-interval nil
 				 'isearch-lazy-highlight-update)))))))))
 
+(defun isearch-lazy-highlight-buffer-update ()
+  "Update highlighting of other matches in the whole buffer."
+  (let ((max lazy-highlight-buffer-max-at-a-time)
+        (looping t)
+        nomore window-start window-end)
+    (with-local-quit
+      (save-selected-window
+	(if (and (window-live-p isearch-lazy-highlight-window)
+		 (not (memq (selected-window) isearch-lazy-highlight-window-group)))
+	    (select-window isearch-lazy-highlight-window))
+	(setq window-start (window-group-start))
+	(setq window-end (window-group-end))
+	(save-excursion
+	  (save-match-data
+	    (goto-char (if isearch-lazy-highlight-forward
+			   isearch-lazy-highlight-end
+			 isearch-lazy-highlight-start))
+	    (while looping
+	      (let* ((bound (if isearch-lazy-highlight-forward
+				(or isearch-lazy-highlight-end-limit (point-max))
+			      (or isearch-lazy-highlight-start-limit (point-min))))
+		     (found (isearch-lazy-highlight-search
+			     isearch-lazy-highlight-last-string
+			     bound)))
+		(when max
+		  (setq max (1- max))
+		  (if (<= max 0)
+		      (setq looping nil)))
+		(if found
+		    (let ((mb (match-beginning 0))
+			  (me (match-end 0)))
+		      (if (= mb me)	;zero-length match
+			  (if isearch-lazy-highlight-forward
+			      (if (= mb (point-max))
+				  (setq found nil)
+				(forward-char 1))
+			    (if (= mb (point-min))
+				(setq found nil)
+			      (forward-char -1)))
+			;; Already highlighted by isearch-lazy-highlight-update
+			(unless (or (and (>= mb window-start) (<= me window-end))
+                                    (not isearch-lazy-highlight-buffer-p))
+			  ;; non-zero-length match
+			  (funcall isearch-lazy-highlight-match-function mb me)))
+		      ;; Remember the current position of point for
+		      ;; the next call of `isearch-lazy-highlight-update'
+		      ;; when `lazy-highlight-buffer-max-at-a-time' is too small.
+		      (if isearch-lazy-highlight-forward
+			  (setq isearch-lazy-highlight-end (point))
+			(setq isearch-lazy-highlight-start (point)))))
+
+		;; not found or zero-length match at the search bound
+		(if (not found)
+		    (setq looping nil
+			  nomore  t))))
+	    (unless nomore
+	      (setq isearch-lazy-highlight-timer
+		    (run-at-time lazy-highlight-interval nil
+				 'isearch-lazy-highlight-buffer-update)))))))))
+
 (defun isearch-resume (string regexp word forward message case-fold)
   "Resume an incremental search.
 STRING is the string or regexp searched for.