changeset 106206:db4fe1337534

Implement Isearch in comint input history. (Bug#3746) * comint.el (comint-mode): Add `comint-history-isearch-setup' to `isearch-mode-hook'. (comint-history-isearch): New defcustom. (comint-history-isearch-backward) (comint-history-isearch-backward-regexp): New commands. (comint-history-isearch-message-overlay): New buffer-local variable. (comint-history-isearch-setup, comint-history-isearch-end) (comint-goto-input, comint-history-isearch-search) (comint-history-isearch-message, comint-history-isearch-wrap) (comint-history-isearch-push-state) (comint-history-isearch-pop-state): New functions.
author Juri Linkov <juri@jurta.org>
date Mon, 23 Nov 2009 20:30:33 +0000
parents f573e64710b2
children 936dfb675acf
files etc/NEWS lisp/ChangeLog lisp/comint.el
diffstat 3 files changed, 216 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- a/etc/NEWS	Mon Nov 23 15:59:07 2009 +0000
+++ b/etc/NEWS	Mon Nov 23 20:30:33 2009 +0000
@@ -281,6 +281,11 @@
 the end of the command.  Its output appears in the buffer `*Async Shell
 Command*'.
 
+*** Isearch searches in the comint/shell input history when the new variable
+`comint-history-isearch' is non-nil.  New commands `comint-history-isearch-backward'
+and `comint-history-isearch-backward-regexp' start Isearch in the input history
+regardless of the value of `comint-history-isearch'.
+
 +++
 *** Autorevert Tail mode now works now for remote files.
 
--- a/lisp/ChangeLog	Mon Nov 23 15:59:07 2009 +0000
+++ b/lisp/ChangeLog	Mon Nov 23 20:30:33 2009 +0000
@@ -1,3 +1,18 @@
+2009-11-23  Juri Linkov  <juri@jurta.org>
+
+	Implement Isearch in comint input history.  (Bug#3746)
+	* comint.el (comint-mode): Add `comint-history-isearch-setup' to
+	`isearch-mode-hook'.
+	(comint-history-isearch): New defcustom.
+	(comint-history-isearch-backward)
+	(comint-history-isearch-backward-regexp): New commands.
+	(comint-history-isearch-message-overlay): New buffer-local variable.
+	(comint-history-isearch-setup, comint-history-isearch-end)
+	(comint-goto-input, comint-history-isearch-search)
+	(comint-history-isearch-message, comint-history-isearch-wrap)
+	(comint-history-isearch-push-state)
+	(comint-history-isearch-pop-state): New functions.
+
 2009-11-23  Michael Albinus  <michael.albinus@gmx.de>
 
 	* net/tramp.el (tramp-shell-prompt-pattern): Use \r for carriage
--- a/lisp/comint.el	Mon Nov 23 15:59:07 2009 +0000
+++ b/lisp/comint.el	Mon Nov 23 20:30:33 2009 +0000
@@ -310,6 +310,7 @@
   :type 'integer
   :group 'comint)
 
+;; FIXME: this should be defcustom
 (defvar comint-input-ring-size 150
   "Size of input history ring.")
 
@@ -446,6 +447,8 @@
     (define-key map [C-up] 	  'comint-previous-input)
     (define-key map [C-down] 	  'comint-next-input)
     (define-key map "\er" 	  'comint-previous-matching-input)
+    ;; FIXME: maybe M-r better to be bound to Isearch comint history?
+    ;; (define-key map "\er" 	  'comint-history-isearch-backward-regexp)
     (define-key map "\es" 	  'comint-next-matching-input)
     (define-key map [?\C-c ?\M-r] 'comint-previous-matching-input-from-input)
     (define-key map [?\C-c ?\M-s] 'comint-next-matching-input-from-input)
@@ -668,6 +671,7 @@
   (make-local-variable 'font-lock-defaults)
   (setq font-lock-defaults '(nil t))
   (add-hook 'change-major-mode-hook 'font-lock-defontify nil t)
+  (add-hook 'isearch-mode-hook 'comint-history-isearch-setup nil t)
   ;; This behavior is not useful in comint buffers, and is annoying
   (set (make-local-variable 'next-line-add-newlines) nil))
 
@@ -1326,6 +1330,198 @@
   (comint-replace-by-expanded-history)
   (self-insert-command arg))
 
+;; Isearch in comint input history
+
+(defcustom comint-history-isearch nil
+  "Non-nil to Isearch in input history only, not in comint buffer output.
+If t, usual Isearch keys like `C-r' and `C-M-r' in comint mode search
+in the input history.
+If `dwim', Isearch keys search in the input history only when initial
+point position is at the comint command line.  When starting Isearch
+from other parts of the comint buffer, they search in the comint buffer.
+If nil, Isearch operates on the whole comint buffer."
+  :type '(choice (const :tag "Don't search in input history" nil)
+		 (const :tag "When point is on command line initially, search history" dwim)
+		 (const :tag "Always search in input history" t))
+  :group 'comint
+  :version "23.2")
+
+(defun comint-history-isearch-backward ()
+  "Search for a string backward in input history using Isearch."
+  (interactive)
+  (let ((comint-history-isearch t))
+    (isearch-backward)))
+
+(defun comint-history-isearch-backward-regexp ()
+  "Search for a regular expression backward in input history using Isearch."
+  (interactive)
+  (let ((comint-history-isearch t))
+    (isearch-backward-regexp)))
+
+(defvar comint-history-isearch-message-overlay nil)
+(make-variable-buffer-local 'comint-history-isearch-message-overlay)
+
+(defun comint-history-isearch-setup ()
+  "Set up a comint for using Isearch to search the input history.
+Intended to be added to `isearch-mode-hook' in `comint-mode'."
+  (when (or (eq comint-history-isearch t)
+	    (and (eq comint-history-isearch 'dwim)
+		 ;; Point is at command line.
+		 (comint-after-pmark-p)))
+    (setq isearch-message-prefix-add "history ")
+    (set (make-local-variable 'isearch-search-fun-function)
+	 'comint-history-isearch-search)
+    (set (make-local-variable 'isearch-message-function)
+	 'comint-history-isearch-message)
+    (set (make-local-variable 'isearch-wrap-function)
+	 'comint-history-isearch-wrap)
+    (set (make-local-variable 'isearch-push-state-function)
+	 'comint-history-isearch-push-state)
+    (add-hook 'isearch-mode-end-hook 'comint-history-isearch-end nil t)))
+
+(defun comint-history-isearch-end ()
+  "Clean up the comint after terminating Isearch in comint."
+  (if comint-history-isearch-message-overlay
+      (delete-overlay comint-history-isearch-message-overlay))
+  (setq isearch-message-prefix-add nil)
+  (setq isearch-search-fun-function nil)
+  (setq isearch-message-function nil)
+  (setq isearch-wrap-function nil)
+  (setq isearch-push-state-function nil)
+  (remove-hook 'isearch-mode-end-hook 'comint-history-isearch-end t))
+
+(defun comint-goto-input (pos)
+  "Put input history item of the absolute history position POS."
+  ;; If leaving the edit line, save partial unfinished input.
+  (if (null comint-input-ring-index)
+      (setq comint-stored-incomplete-input
+	    (funcall comint-get-old-input)))
+  (setq comint-input-ring-index pos)
+  (comint-delete-input)
+  (if (and pos (not (ring-empty-p comint-input-ring)))
+      (insert (ring-ref comint-input-ring pos))
+    ;; Restore partial unfinished input.
+    (when (> (length comint-stored-incomplete-input) 0)
+      (insert comint-stored-incomplete-input))))
+
+(defun comint-history-isearch-search ()
+  "Return the proper search function, for Isearch in input history."
+  (cond
+   (isearch-word
+    (if isearch-forward 'word-search-forward 'word-search-backward))
+   (t
+    (lambda (string bound noerror)
+      (let ((search-fun
+	     ;; Use standard functions to search within comint text
+             (cond
+              (isearch-regexp
+               (if isearch-forward 're-search-forward 're-search-backward))
+              (t
+               (if isearch-forward 'search-forward 'search-backward))))
+	    found)
+	;; Avoid lazy-highlighting matches in the comint prompt when
+	;; searching forward.  Lazy-highlight calls this lambda with the
+	;; bound arg, so skip the comint prompt.
+	(if (and bound isearch-forward (< (point) (comint-line-beginning-position)))
+	    (goto-char (comint-line-beginning-position)))
+        (or
+	 ;; 1. First try searching in the initial comint text
+	 (funcall search-fun string
+		  (if isearch-forward bound (comint-line-beginning-position))
+		  noerror)
+	 ;; 2. If the above search fails, start putting next/prev history
+	 ;; elements in the comint successively, and search the string
+	 ;; in them.  Do this only when bound is nil (i.e. not while
+	 ;; lazy-highlighting search strings in the current comint text).
+	 (unless bound
+	   (condition-case nil
+	       (progn
+		 (while (not found)
+		   (cond (isearch-forward
+			  ;; Signal an error here explicitly, because
+			  ;; `comint-next-input' doesn't signal an error.
+			  (when (null comint-input-ring-index)
+			    (error "End of history; no next item"))
+			  (comint-next-input 1)
+			  (goto-char (comint-line-beginning-position)))
+			 (t
+			  ;; Signal an error here explicitly, because
+			  ;; `comint-previous-input' doesn't signal an error.
+			  (when (eq comint-input-ring-index
+				    (1- (ring-length comint-input-ring)))
+			    (error "Beginning of history; no preceding item"))
+			  (comint-previous-input 1)
+			  (goto-char (point-max))))
+		   (setq isearch-barrier (point) isearch-opoint (point))
+		   ;; After putting the next/prev history element, search
+		   ;; the string in them again, until comint-next-input
+		   ;; or comint-previous-input raises an error at the
+		   ;; beginning/end of history.
+		   (setq found (funcall search-fun string
+					(unless isearch-forward
+					  ;; For backward search, don't search
+					  ;; in the comint prompt
+					  (comint-line-beginning-position))
+					noerror)))
+		 ;; Return point of the new search result
+		 (point))
+	     ;; Return nil on the error "no next/preceding item"
+	     (error nil)))))))))
+
+(defun comint-history-isearch-message (&optional c-q-hack ellipsis)
+  "Display the input history search prompt.
+If there are no search errors, this function displays an overlay with
+the Isearch prompt which replaces the original comint prompt.
+Otherwise, it displays the standard Isearch message returned from
+`isearch-message'."
+  (if (not (and isearch-success (not isearch-error)))
+      ;; Use standard function `isearch-message' when not in comint prompt,
+      ;; or search fails, or has an error (like incomplete regexp).
+      ;; This function displays isearch message in the echo area,
+      ;; so it's possible to see what is wrong in the search string.
+      (isearch-message c-q-hack ellipsis)
+    ;; Otherwise, put the overlay with the standard isearch prompt over
+    ;; the initial comint prompt.
+    (if (overlayp comint-history-isearch-message-overlay)
+	(move-overlay comint-history-isearch-message-overlay
+		      (save-excursion (forward-line 0) (point))
+                      (comint-line-beginning-position))
+      (setq comint-history-isearch-message-overlay
+	    (make-overlay (save-excursion (forward-line 0) (point))
+                          (comint-line-beginning-position)))
+      (overlay-put comint-history-isearch-message-overlay 'evaporate t))
+    (overlay-put comint-history-isearch-message-overlay
+		 'display (isearch-message-prefix c-q-hack ellipsis))
+    ;; And clear any previous isearch message.
+    (message "")))
+
+(defun comint-history-isearch-wrap ()
+  "Wrap the input history search when search fails.
+Move point to the first history element for a forward search,
+or to the last history element for a backward search."
+  (unless isearch-word
+    ;; When `comint-history-isearch-search' fails on reaching the
+    ;; beginning/end of the history, wrap the search to the first/last
+    ;; input history element.
+    (if isearch-forward
+	(comint-goto-input (1- (ring-length comint-input-ring)))
+      (comint-goto-input nil))
+    (setq isearch-success t))
+  (goto-char (if isearch-forward (comint-line-beginning-position) (point-max))))
+
+(defun comint-history-isearch-push-state ()
+  "Save a function restoring the state of input history search.
+Save `comint-input-ring-index' to the additional state parameter
+in the search status stack."
+  `(lambda (cmd)
+     (comint-history-isearch-pop-state cmd ,comint-input-ring-index)))
+
+(defun comint-history-isearch-pop-state (cmd hist-pos)
+  "Restore the input history search state.
+Go to the history element by the absolute history position HIST-POS."
+  (comint-goto-input hist-pos))
+
+
 (defun comint-within-quotes (beg end)
   "Return t if the number of quotes between BEG and END is odd.
 Quotes are single and double."