From 9de47983663640695f9ee6d9a018f515ec2da40f 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 | 247 +++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 202 insertions(+), 45 deletions(-) diff --git a/lisp/isearch.el b/lisp/isearch.el index 9f3cfd70fb..b8228087ec 100644 --- a/lisp/isearch.el +++ b/lisp/isearch.el @@ -821,15 +821,30 @@ 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 "\C-r" '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.") @@ -1518,6 +1533,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. @@ -1781,43 +1811,153 @@ with-isearch-suspended (isearch-abort) ;; outside of let to restore outside global values ))) +;;; Edit search string in the minibuffer + +;; WIP delete this? (defvar minibuffer-history-symbol) ;; from external package gmhist.el -(defun isearch-edit-string () +(defcustom isearch-from-minibuffer nil + "If non-nil, control Isearch from the minibuffer." + :type 'boolean) + +(defvar isearch-edit--quitting-commands + '(isearch-query-replace + isearch-query-replace-regexp + isearch-highlight-regexp + isearch-highlight-lines-matching-regexp)) + +(defvar isearch-edit--buffer-commands + '(isearch-beginning-of-buffer + isearch-end-of-buffer + isearch-occur + isearch-repeat-backward + isearch-repeat-forward + isearch-toggle-case-fold + isearch-toggle-char-fold + isearch-toggle-invisible + isearch-toggle-lax-whitespace + isearch-toggle-regexp + isearch-toggle-symbol + isearch-toggle-word + isearch-query-replace + isearch-query-replace-regexp + isearch-highlight-regexp + isearch-highlight-lines-matching-regexp)) + +(defvar isearch-edit--no-search-commands + '(next-history-element + previous-history-element)) + +(defun isearch-edit--pre-command-hook () + (when (member this-command isearch-edit--quitting-commands) + (run-with-timer 0 nil 'command-execute this-command) + (exit-minibuffer)) + (when (member this-command isearch-edit--buffer-commands) + (setq inhibit-redisplay t) + (select-window (minibuffer-selected-window)) + (when (and (local-variable-p 'pre-command-hook) + (member 'isearch-pre-command-hook pre-command-hook)) + (isearch-pre-command-hook)))) + +(defun isearch-edit--post-command-hook () + "Hook to run from the minibuffer to update the Isearch state." + (setq inhibit-redisplay nil) + (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)) + (inhibit-redisplay t)) + (with-minibuffer-selected-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 (member this-command isearch-edit--no-search-commands) + (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))))) + +(defvar-local isearch-edit--prompt-overlay nil + "Overlay to display the Isearch status in `isearch-edit-string'.") + +(defvar-local isearch-edit--minibuffer nil + "Minibuffer currently editing the search string. +Local to the search buffer, and non-nil only during an +`isearch-edit-string' session.") + +(defun isearch-edit-string (&optional persist) "Edit the search string in the minibuffer. + +When PERSIST is nil, exiting the minibuffer or repeating the +search resumes Isearch with the edited string. When PERSIST 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 + (minibuffer-with-setup-hook + (lambda () + (setq-local tool-bar-map isearch-tool-bar-map) + (add-hook 'pre-command-hook 'isearch-edit--pre-command-hook nil t) + (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 + (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)))) + (read-from-minibuffer + "I-search: " + nil + (if persist + 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 'regexp-search-ring 'search-ring) + (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))) + (when (and persist isearch-mode) (isearch-done))) + (quit (if (and persist isearch-mode) (isearch-cancel) (signal 'quit nil))))) (defun isearch-nonincremental-exit-minibuffer () (interactive) @@ -1879,13 +2019,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)) @@ -2075,6 +2210,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 +2220,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 @@ -3094,7 +3233,13 @@ isearch-post-command-hook (goto-char isearch-pre-move-point)) (isearch-search-and-update))) (setq isearch-pre-move-point nil)) - (force-mode-line-update)) + (when (and isearch-from-minibuffer (not (minibufferp))) + (if isearch-edit--minibuffer + (progn + (select-window (minibuffer-window)) + (isearch-edit--post-command-hook)) + (run-with-idle-timer 0 nil 'isearch-edit-string t))) + (force-mode-line-update)) (defun isearch-quote-char (&optional count) "Quote special characters for incremental search. @@ -3267,6 +3412,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 +3439,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.31.1