emacs-devel
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[WIP PATCH] Controlling Isearch from the minibuffer


From: Augusto Stoffel
Subject: [WIP PATCH] Controlling Isearch from the minibuffer
Date: Sat, 08 May 2021 12:13:52 +0200
User-agent: Gnus/5.13 (Gnus v5.13) Emacs/27.2 (gnu/linux)

I've attached a draft implementation of a minibuffer-controlled mode for
Isearch, as described for instance in
https://lists.gnu.org/archive/html/emacs-devel/2020-01/msg00447.html
To enable the feature, set `isearch-from-minibuffer' to t.

The basic trade-off is that this makes it easier to edit the search
string, and harder to quit the search.  It also drops some of the
eccentricities of regular Isearch.  For instance, DEL deletes and C-g
quits.

I'm sharing this preliminary version because two important questions can
already be answered:

- Does the approach taken here seem sufficiently robust?  Note in
  particular the `with-isearch-window' macro, which is now needed around
  several functions, as well as the somewhat hacky `run-with-idle-timer'
  call inside the `isearch-mode' function.

- Are the slightly backwards incompatible keybinding changes in
  `isearch-edit-string' acceptable?

If any of these answers is no, then I would provide a package for the
same feature.  But I think the feature is interesting enough to be built
in isearch.el.  Moreover, it would benefit from being official because
many third-party extensions to Isearch will need to take into account
the possibility that the search is being controlled remotely from a
minibuffer.

Some further remarks:

- The minibuffer-controlled mode is supposed to depend on the proposed
  `isearch-buffer-local' feature.  This will make the hack used to
  deactivate the `overriding-terminal-local-map' unnecessary.

- It seems necessary to let-bind `inhibit-redisplay' to nil in
  `with-isearch-window' in order to avoid flicker in the cursor.  This
  seems related to the recent thread "Temporarily select-window, without
  updating mode-line face and cursor fill?" in this list.  Any better
  solutions?

- I don't like the `with-isearch-window-quitting-edit' macro, but I
  don't see a different way of achieving the necessary effect.

- I don't use/know of all Isearch features, so let me know if you spot
  some incompatibility.

What do you think?

>From d623ea3e00f07688a4b1cff8dd758c3bea45519c Mon Sep 17 00:00:00 2001
From: Augusto Stoffel <arstoffel@gmail.com>
Date: Sat, 8 May 2021 10:24:20 +0200
Subject: [PATCH] Control Isearch from minibuffer (draft)

---
 lisp/isearch.el | 272 ++++++++++++++++++++++++++++++++++++++----------
 1 file changed, 215 insertions(+), 57 deletions(-)

diff --git a/lisp/isearch.el b/lisp/isearch.el
index 9f3cfd70fb..c2a46f7690 100644
--- a/lisp/isearch.el
+++ b/lisp/isearch.el
@@ -821,15 +821,32 @@ isearch-tool-bar-map
             :image '(isearch-tool-bar-image "left-arrow")))
     map))
 
+;; WIP for debugging
+(makunbound 'minibuffer-local-isearch-map)
+
 (defvar minibuffer-local-isearch-map
   (let ((map (make-sparse-keymap)))
     (set-keymap-parent map minibuffer-local-map)
-    (define-key map "\r"    'exit-minibuffer)
+    (define-key map "\C-j" 'newline)
     (define-key map "\M-\t" 'isearch-complete-edit)
-    (define-key map "\C-s"  'isearch-forward-exit-minibuffer)
-    (define-key map "\C-r"  'isearch-reverse-exit-minibuffer)
-    (define-key map "\C-f"  'isearch-yank-char-in-minibuffer)
-    (define-key map [right] 'isearch-yank-char-in-minibuffer)
+    (define-key map "\C-s" 'isearch-repeat-forward)
+    (define-key map [down] 'isearch-repeat-forward)
+    (define-key map "\C-r" 'isearch-repeat-backward)
+    (define-key map [up] 'isearch-repeat-backward)
+    (define-key map "\M-%" 'isearch-query-replace)
+    (define-key map [?\C-\M-%] 'isearch-query-replace-regexp)
+    (define-key map "\M-<" 'isearch-beginning-of-buffer)
+    (define-key map "\M->" 'isearch-end-of-buffer)
+    (define-key map "\M-s'" 'isearch-toggle-char-fold)
+    (define-key map "\M-s " 'isearch-toggle-lax-whitespace)
+    (define-key map "\M-s_" 'isearch-toggle-symbol)
+    (define-key map "\M-sc" 'isearch-toggle-case-fold)
+    (define-key map "\M-shr" 'isearch-highlight-regexp)
+    (define-key map "\M-shl" 'isearch-highlight-lines-matching-regexp)
+    (define-key map "\M-si" 'isearch-toggle-invisible)
+    (define-key map "\M-so" 'isearch-occur)
+    (define-key map "\M-sr" 'isearch-toggle-regexp)
+    (define-key map "\M-sw" 'isearch-toggle-word)
     map)
   "Keymap for editing Isearch strings in the minibuffer.")
 
@@ -980,6 +997,32 @@ search-map
 (define-key search-map    "." 'isearch-forward-symbol-at-point)
 (define-key search-map "\M-." 'isearch-forward-thing-at-point)
 
+;;; Minibuffer-based interface
+
+(defcustom isearch-from-minibuffer nil
+  "If non-nil, control Isearch from the minibuffer."
+  :type 'boolean)
+
+(defmacro with-isearch-window (&rest body)
+  "Execute BODY in the Isearch window."
+  `(if (and (minibufferp)
+            (not (eq isearch--current-buffer (current-buffer))))
+       (let ((inhibit-redisplay t))
+         (with-minibuffer-selected-window
+         ,@body))
+     ,@body))
+
+(defmacro with-isearch-window-quitting-edit (&rest body)
+  "Execute BODY in the Isearch window.
+
+Like `with-isearch-window', but quit editing the search string
+first if applicable.  In this case, control is returned to the
+caller of `isearch-edit-string'.  This must be used if BODY exits
+the search and uses the minibuffer."
+  `(if (and (minibufferp) (not (eq isearch--current-buffer (current-buffer))))
+       (throw 'isearch-edit--continue (lambda () ,@body))
+     ,@body))
+
 ;; Entry points to isearch-mode.
 
 (defun isearch-forward (&optional regexp-p no-recursive-edit)
@@ -1316,9 +1359,16 @@ isearch-mode
   ;; isearch-mode can be made modal (in the sense of not returning to
   ;; the calling function until searching is completed) by entering
   ;; a recursive-edit and exiting it when done isearching.
-  (if recursive-edit
+  (if (and isearch-from-minibuffer (not (minibufferp)))
+      (if recursive-edit
+          (isearch-edit-string t)
+        ;; WIP: This timer hack is used e.g. for
+        ;; `isearch-forward-symbol-at-point' as well as third party
+        ;; packages which prepare a special isearch state.
+        (run-with-idle-timer 0 nil #'isearch-edit-string t))
+    (when recursive-edit
       (let ((isearch-recursive-edit t))
-       (recursive-edit)))
+        (recursive-edit))))
   isearch-success)
 
 
@@ -1518,6 +1568,21 @@ isearch-update-from-string-properties
     (setq isearch-regexp-function
          (get-text-property 0 'isearch-regexp-function string))))
 
+(defun isearch-set-string (string &optional properties)
+  "Set the current search string.
+
+Return STRING.  If PROPERTIES is non-nil, also update the search
+mode from the text properties of STRING."
+  (when properties (isearch-update-from-string-properties string))
+  (when isearch-edit--minibuffer
+    (with-current-buffer isearch-edit--minibuffer
+      (let ((inhibit-modification-hooks t))
+        (delete-minibuffer-contents)
+        (insert string))
+      (end-of-buffer)))
+  (setq isearch-message (mapconcat 'isearch-text-char-description string "")
+        isearch-string string))
+
 
 ;; The search status structure and stack.
 
@@ -1783,41 +1848,111 @@ with-isearch-suspended
 
 (defvar minibuffer-history-symbol) ;; from external package gmhist.el
 
-(defun isearch-edit-string ()
+(defun isearch-edit--post-command-hook ()
+  "Hook to run from the minibuffer to update the Isearch state."
+  (set-text-properties (minibuffer-prompt-end) (point-max) nil)
+  (when-let ((fail-pos (isearch-fail-pos)))
+    (add-text-properties (+ (minibuffer-prompt-end) fail-pos)
+                         (point-max)
+                         '(face isearch-fail)))
+  (when isearch-error
+    (isearch--momentary-message isearch-error)))
+
+(defun isearch-edit--after-change (_ _ _)
+  "Hook to run from the minibuffer to update the Isearch state."
+  (let ((string (minibuffer-contents)))
+    (with-isearch-window
+     (setq isearch-string (substring-no-properties string))
+     (isearch-update-from-string-properties string)
+     ;; Backtrack to barrier and search, unless the `this-command'
+     ;; is special or the search regexp is invalid.
+     (if (or (and (symbolp this-command)
+                  (get this-command 'isearch-edit-string--no-search))
+             (and isearch-regexp
+                  (condition-case err
+                      (prog1 nil (string-match-p isearch-string ""))
+                    (invalid-regexp
+                     (prog1 t (isearch--momentary-message (cadr err)))))))
+         (isearch-update)
+       (goto-char isearch-barrier)
+       (setq isearch-adjusted t isearch-success t)
+       (isearch-search-and-update)))))
+
+(put 'next-history-element 'isearch-edit-string--no-search t)
+(put 'previous-history-element 'isearch-edit-string--no-search t)
+
+(defvar-local isearch-edit--prompt-overlay nil
+  "Overlay to display the Isearch status in `isearch-edit-string'.")
+
+(defvar-local isearch-edit--minibuffer nil
+  "Pointer to the minibuffer controlling the search.
+Local to the search buffer.  Non-nil only during an
+`isearch-edit-string' session.")
+
+(defun isearch-edit-string (&optional exit)
   "Edit the search string in the minibuffer.
+
+When EXIT is nil, exiting the minibuffer or repeating the search
+resumes Isearch with the edited string.  When EXIT is non-nil,
+exiting the minibuffer also ends the search.
+
 The following additional command keys are active while editing.
-\\<minibuffer-local-isearch-map>
-\\[exit-minibuffer] to resume incremental searching with the edited string.
-\\[isearch-forward-exit-minibuffer] to resume isearching forward.
-\\[isearch-reverse-exit-minibuffer] to resume isearching backward.
-\\[isearch-complete-edit] to complete the search string using the search ring."
+\\{minibuffer-local-isearch-map}"
   (interactive)
-  (with-isearch-suspended
-   (let* ((message-log-max nil)
-         ;; Don't add a new search string to the search ring here
-         ;; in `read-from-minibuffer'. It should be added only
-         ;; by `isearch-update-ring' called from `isearch-done'.
-         (history-add-new-input nil)
-         ;; Binding minibuffer-history-symbol to nil is a work-around
-         ;; for some incompatibility with gmhist.
-         (minibuffer-history-symbol)
-         ;; Search string might have meta information on text properties.
-         (minibuffer-allow-text-properties t))
-     (setq isearch-new-string
-          (read-from-minibuffer
-           (isearch-message-prefix nil isearch-nonincremental)
-           (cons isearch-string (1+ (or (isearch-fail-pos)
-                                        (length isearch-string))))
-           minibuffer-local-isearch-map nil
-           (if isearch-regexp
-               (cons 'regexp-search-ring
-                     (1+ (or regexp-search-ring-yank-pointer -1)))
-             (cons 'search-ring
-                   (1+ (or search-ring-yank-pointer -1))))
-           nil t)
-          isearch-new-message
-          (mapconcat 'isearch-text-char-description
-                     isearch-new-string "")))))
+  (condition-case nil
+      (funcall
+       (catch 'isearch-edit--continue
+         (let (;; WIP: This is a hack that can be removed when isearch
+               ;; local mode is available.
+               (overriding-terminal-local-map nil)
+               ;; We need to set `inhibit-redisplay' in `with-isearch-window' 
to
+               ;; avoid flicker.  As a side effect, window-start/end in
+               ;; `isearch-lazy-highlight-update' will have incorrect values,
+               ;; so we need to lazy-highlight the whole buffer.
+               (lazy-highlight-buffer (not (null isearch-lazy-highlight))))
+           (minibuffer-with-setup-hook
+               (lambda ()
+                 (add-hook 'after-change-functions 'isearch-edit--after-change 
nil t)
+                 (add-hook 'post-command-hook 'isearch-edit--post-command-hook 
nil t)
+                 (setq-local isearch-edit--prompt-overlay
+                             (make-overlay (point-min) (point-min) 
(current-buffer) t t))
+                 (let ((inhibit-modification-hooks t)
+                       (mb (current-buffer))
+                       (buf (window-buffer (minibuffer-selected-window))))
+                   (insert (buffer-local-value 'isearch-string buf))
+                   (with-current-buffer buf
+                     (setq-local isearch-edit--minibuffer mb)
+                     (isearch-message)))
+                 (when isearch-error (isearch--momentary-message 
isearch-error)))
+             (unwind-protect
+                 (read-from-minibuffer
+                  "I-search: "
+                  nil
+                  (if exit
+                      minibuffer-local-isearch-map
+                    (let ((map (make-sparse-keymap)))
+                      (set-keymap-parent map minibuffer-local-isearch-map)
+                      (define-key map
+                        [remap isearch-repeat-forward] 
'isearch-forward-exit-minibuffer)
+                      (define-key map
+                        [remap isearch-repeat-backward] 
'isearch-reverse-exit-minibuffer)
+                      map))
+                  nil
+                  (if isearch-regexp
+                      (cons 'regexp-search-ring
+                            (1+ (or regexp-search-ring-yank-pointer -1)))
+                    (cons 'search-ring
+                          (1+ (or search-ring-yank-pointer -1))))
+                  (thread-last isearch-forward-thing-at-point
+                               ;;  WIP: The above variable can be renamed
+                    (mapcar 'thing-at-point)
+                    (delq nil)
+                    (delete-dups)
+                    (mapcar (if isearch-regexp 'regexp-quote 'identity)))
+                  t)
+               (setq-local isearch-edit--minibuffer nil)))
+(if (and exit isearch-mode) 'isearch-done 'ignore))))
+    (quit (if (and exit isearch-mode) (isearch-cancel) (signal 'quit nil)))))
 
 (defun isearch-nonincremental-exit-minibuffer ()
   (interactive)
@@ -1879,13 +2014,8 @@ isearch-repeat
          ;; If search string is empty, use last one.
          (if (null (if isearch-regexp regexp-search-ring search-ring))
              (setq isearch-error "No previous search string")
-           (setq isearch-string
-                 (car (if isearch-regexp regexp-search-ring search-ring))
-                 isearch-message
-                 (mapconcat 'isearch-text-char-description
-                            isearch-string "")
-                 isearch-case-fold-search isearch-last-case-fold-search)
-           ;; After taking the last element, adjust ring to previous one.
+            (isearch-set-string (car (if isearch-regexp regexp-search-ring 
search-ring)) t)
+            ;; After taking the last element, adjust ring to previous one.
            (isearch-ring-adjust1 nil))
        ;; If already have what to search for, repeat it.
        (unless (or isearch-success (null isearch-wrap-pause))
@@ -1955,6 +2085,7 @@ isearch-repeat-forward
 search string.  To find the absolute occurrence from the beginning
 of the buffer, type \\[isearch-beginning-of-buffer] with a numeric argument."
   (interactive "P")
+(with-isearch-window
   (if arg
       (let ((count (prefix-numeric-value arg)))
         (cond ((< count 0)
@@ -1968,6 +2099,7 @@ isearch-repeat-forward
                  (when (not isearch-forward) (setq count (1+ count))))
                (isearch-repeat 'forward count))))
     (isearch-repeat 'forward)))
+)
 
 (defun isearch-repeat-backward (&optional arg)
   "Repeat incremental search backwards.
@@ -1978,6 +2110,7 @@ isearch-repeat-backward
 search string.  To find the absolute occurrence from the end
 of the buffer, type \\[isearch-end-of-buffer] with a numeric argument."
   (interactive "P")
+(with-isearch-window
   (if arg
       (let ((count (prefix-numeric-value arg)))
         (cond ((< count 0)
@@ -1991,6 +2124,7 @@ isearch-repeat-backward
                  (when isearch-forward (setq count (1+ count))))
                (isearch-repeat 'backward count))))
     (isearch-repeat 'backward)))
+)
 
 (defun isearch-beginning-of-buffer (&optional arg)
   "Go to the first occurrence of the current search string.
@@ -2000,6 +2134,7 @@ isearch-beginning-of-buffer
 the beginning of the buffer.  To find the next relative occurrence forwards,
 type \\[isearch-repeat-forward] with a numeric argument."
   (interactive "p")
+(with-isearch-window
   (if (and arg (< arg 0))
       (isearch-end-of-buffer (abs arg))
     ;; For the case when the match is at bobp,
@@ -2007,6 +2142,7 @@ isearch-beginning-of-buffer
     (setq isearch-just-started t)
     (goto-char (point-min))
     (isearch-repeat 'forward arg)))
+)
 
 (defun isearch-end-of-buffer (&optional arg)
   "Go to the last occurrence of the current search string.
@@ -2016,11 +2152,13 @@ isearch-end-of-buffer
 the end of the buffer.  To find the next relative occurrence backwards,
 type \\[isearch-repeat-backward] with a numeric argument."
   (interactive "p")
+(with-isearch-window
   (if (and arg (< arg 0))
       (isearch-beginning-of-buffer (abs arg))
     (setq isearch-just-started t)
     (goto-char (point-max))
     (isearch-repeat 'backward arg)))
+)
 
 
 ;;; Toggles for `isearch-regexp-function' and `search-default-mode'.
@@ -2040,6 +2178,7 @@ isearch-define-mode-toggle
          ,(format "Toggle %s searching on or off.%s" mode
                   (if docstring (concat "\n" docstring) ""))
          (interactive)
+(with-isearch-window
          (unless isearch-mode (isearch-mode t))
          ,@(when function
              `((setq isearch-regexp-function
@@ -2049,6 +2188,7 @@ isearch-define-mode-toggle
          ,@body
          (setq isearch-success t isearch-adjusted t)
          (isearch-update))
+)
        (define-key isearch-mode-map ,key #',command-name)
        ,@(when (and function (symbolp function))
            `((put ',function 'isearch-message-prefix ,(format "%s " mode))
@@ -2075,6 +2215,9 @@ isearch-message-properties
 (defun isearch--momentary-message (string &optional seconds)
   "Print STRING at the end of the isearch prompt for 1 second.
 The optional argument SECONDS overrides the number of seconds."
+  (if isearch-edit--minibuffer
+      (message (propertize (concat " [" string "]")
+                           'face 'minibuffer-prompt))
   (let ((message-log-max nil))
     (message "%s%s%s"
              (isearch-message-prefix nil isearch-nonincremental)
@@ -2082,6 +2225,7 @@ isearch--momentary-message
              (apply #'propertize (format " [%s]" string)
                     isearch-message-properties)))
   (sit-for (or seconds 1)))
+)
 
 (isearch-define-mode-toggle lax-whitespace " " nil
   "In ordinary search, toggles the value of the variable
@@ -2311,6 +2455,7 @@ isearch-query-replace
 replacements from Isearch is `M-s w ... M-%'."
   (interactive
    (list current-prefix-arg))
+(with-isearch-window-quitting-edit
   (barf-if-buffer-read-only)
   (if regexp-flag (setq isearch-regexp t))
   (let ((case-fold-search isearch-case-fold-search)
@@ -2359,6 +2504,7 @@ isearch-query-replace
      (if (use-region-p) (region-end))
      backward))
   (and isearch-recursive-edit (exit-recursive-edit)))
+)
 
 (defun isearch-query-replace-regexp (&optional arg)
   "Start `query-replace-regexp' with string to replace from last search string.
@@ -2379,6 +2525,7 @@ isearch-occur
 for a literal string, REGEXP is constructed by quoting all the special
 characters in that string."
   (interactive
+(with-isearch-window
    (let* ((perform-collect (consp current-prefix-arg))
          (regexp (cond
                   ((functionp isearch-regexp-function)
@@ -2404,6 +2551,8 @@ isearch-occur
             ;; Otherwise normal occur takes numerical prefix argument.
             (when current-prefix-arg
               (prefix-numeric-value current-prefix-arg))))))
+)
+(with-isearch-window
   (let ((case-fold-search isearch-case-fold-search)
        ;; Set `search-upper-case' to nil to not call
        ;; `isearch-no-upper-case-p' in `occur-1'.
@@ -2421,6 +2570,7 @@ isearch-occur
             regexp)
           nlines
           (if (use-region-p) (region-bounds)))))
+)
 
 (declare-function hi-lock-read-face-name "hi-lock" ())
 
@@ -2461,18 +2611,22 @@ isearch-highlight-regexp
 The arguments passed to `highlight-regexp' are the regexp from
 the last search and the face from `hi-lock-read-face-name'."
   (interactive)
+(with-isearch-window-quitting-edit
   (isearch--highlight-regexp-or-lines
    #'(lambda (regexp face lighter)
        (highlight-regexp regexp face nil lighter))))
+)
 
 (defun isearch-highlight-lines-matching-regexp ()
   "Exit Isearch mode and call `highlight-lines-matching-regexp'.
 The arguments passed to `highlight-lines-matching-regexp' are the
 regexp from the last search and the face from `hi-lock-read-face-name'."
   (interactive)
+(with-isearch-window-quitting-edit
   (isearch--highlight-regexp-or-lines
    #'(lambda (regexp face _lighter)
        (highlight-lines-matching-regexp regexp face))))
+)
 
 
 (defun isearch-delete-char ()
@@ -3227,7 +3381,7 @@ isearch-complete1
                   (all-completions isearch-string ring))))
            t)
        (and completion
-            (setq isearch-string completion))))
+         (isearch-set-string completion))))
      (t
       (message "No completion") ; waits a second if in minibuffer
       nil))))
@@ -3238,22 +3392,14 @@ isearch-complete
 If there is no completion possible, say so and continue searching."
   (interactive)
   (if (isearch-complete1)
-      (progn (setq isearch-message
-                  (mapconcat 'isearch-text-char-description
-                             isearch-string ""))
-            (isearch-edit-string))
-    ;; else
+      (isearch-edit-string)
     (sit-for 1)
     (isearch-update)))
 
 (defun isearch-complete-edit ()
   "Same as `isearch-complete' except in the minibuffer."
   (interactive)
-  (setq isearch-string (field-string))
-  (if (isearch-complete1)
-      (progn
-       (delete-field)
-       (insert isearch-string))))
+  (with-isearch-window (isearch-complete1)))
 
 
 ;; Message string
@@ -3267,6 +3413,17 @@ isearch-message
   ;; circumstances are when follow-mode is active, the search string
   ;; spans two (or several) windows, and the message about to be
   ;; displayed will cause the echo area to expand.
+  (if isearch-from-minibuffer
+      (when-let ((mb isearch-edit--minibuffer)
+                 (ov (buffer-local-value 'isearch-edit--prompt-overlay mb)))
+        (overlay-put ov
+                     'before-string
+                     (concat
+                      (when isearch-lazy-count
+                        (format "%-6s" (isearch-lazy-count-format)))
+                      (capitalize
+                       (isearch--describe-regexp-mode
+                        isearch-regexp-function)))))
   (let ((cursor-in-echo-area ellipsis)
        (m isearch-message)
        (fail-pos (isearch-fail-pos t)))
@@ -3283,6 +3440,7 @@ isearch-message
             m
             (isearch-message-suffix c-q-hack)))
     (if c-q-hack m (let ((message-log-max nil)) (message "%s" m)))))
+)
 
 (defun isearch--describe-regexp-mode (regexp-function &optional space-before)
   "Make a string for describing REGEXP-FUNCTION.
-- 
2.30.2


reply via email to

[Prev in Thread] Current Thread [Next in Thread]