From d623ea3e00f07688a4b1cff8dd758c3bea45519c Mon Sep 17 00:00:00 2001 From: Augusto Stoffel 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. -\\ -\\[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