;;;; isearch.jl -- Emacs style incremental search
;;;  Copyright (C) 1994 John Harper <jsh@ukc.ac.uk>

;;; This file is part of Jade.

;;; Jade is free software; you can redistribute it and/or modify it
;;; under the terms of the GNU General Public License as published by
;;; the Free Software Foundation; either version 2, or (at your option)
;;; any later version.

;;; Jade is distributed in the hope that it will be useful, but
;;; WITHOUT ANY WARRANTY; without even the implied warranty of
;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
;;; GNU General Public License for more details.

;;; You should have received a copy of the GNU General Public License
;;; along with Jade; see the file COPYING.  If not, write to
;;; the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.

(provide 'isearch)

;;; Bugs:
;;; Doesn't un-wrap itself when backtracking

(defvar case-fold-search t
  "Buffer-local variable, when non-nil case of characters is ignored when
searching.")
(make-variable-buffer-local 'case-fold-search)

(defvar isearch-last-match nil
  "The last regexp successfully (i.e. entered with RET) found by isearch.")


;;; Wrappers for some regexp functions to trap errors

(defun isearch-find-next-regexp (pos)
  (setq isearch-re-error nil)
  (error-protect
      (find-next-regexp (car (car isearch-trace)) pos nil case-fold-search)
    (regexp-error
      (setq isearch-re-error t)
      'regexp-error)))

(defun isearch-find-prev-regexp (pos)
  (setq isearch-re-error nil)
  (error-protect
      (find-prev-regexp (car (car isearch-trace)) pos nil case-fold-search)
    (regexp-error
      (setq isearch-re-error t)
      'regexp-error)))

(defun isearch-looking-at ()
  (setq isearch-re-error nil)
  (error-protect
      (looking-at (car (car isearch-trace)) nil nil case-fold-search)
    (regexp-error
      (setq isearch-re-error t)
      'regexp-error)))


;;; I-search stack handling. The variable `isearch-trace' holds a stack of
;;; all strings and match positions in the current search. It looks
;;; something like,
;;;   ((SEARCH-REGEXP [MATCH-POS]... )... )
;;; But although the current string will be on the stack, the current match
;;; position won't -- the cursor position is used to show this.

;; Pops the top match and moves the cursor to it.
(defun isearch-pop-match ()
  (let
      ((item (car isearch-trace)))
    (if (cdr item)
	(progn
	  (goto-char (nth 1 item))
	  (rplaca isearch-trace (cons (car item) (nthcdr 2 item))))
      (goto-char isearch-initial-pos)))
  (setq isearch-failing nil))

;; Pops the top string, then moves to the top match of the next string.
(defun isearch-pop-string ()
  (when (cdr isearch-trace)
    (setq isearch-trace (cdr isearch-trace))
    (isearch-pop-match)))

;; Pushes a match at POS onto the top string
(defun isearch-push-match (pos)
  (let
      ((item (car isearch-trace)))
    (rplaca isearch-trace (cons (car item) (cons pos (cdr item))))))

;; Pushes the current position, pushes the STRING onto the top of the
;; stack, then searches for it
(defun isearch-push-string (string)
  (isearch-push-match (cursor-pos))
  (setq isearch-trace (cons (cons string nil) isearch-trace))
  (unless (isearch-looking-at)
    (let
	((next (if isearch-forwards
		   (isearch-find-next-regexp (next-char))
		 (isearch-find-prev-regexp (prev-char)))))
      (cond
	((posp next)
	  (goto-char next)
	  (setq isearch-failing nil))
	((null next)
	  (setq isearch-failing t)
	  (beep))))))


;; Keymap

(defvar isearch-keymap (make-keylist))
(bind-keys isearch-keymap
  "Ctrl-s"	'isearch-next-forward
  "Ctrl-r"	'isearch-next-backward
  "Ctrl-g"	'isearch-cancel
  "Ctrl-q"	'(progn (setq next-keymap-path nil) (isearch-title))
  "Ctrl-w"	'isearch-yank-word
  "Ctrl-y"	'isearch-yank-line
  "RET"		'isearch-accept
  "ESC"		'isearch-accept
  "BS"		'isearch-rubout)


;; Display the status message, has to be done after each command
(defun isearch-title ()
  (let
      ((msg (concat (if isearch-failing "failing ")
		    (if isearch-wrapped "wrapped ")
		    "I-search: "
		    (car (car isearch-trace))
		    (if isearch-re-error "    *Invalid/incomplete regexp*"))))
    (aset msg 0 (char-upcase (aref msg 0)))
    (message msg)))

;; Accept our current position
(defun isearch-accept ()
  (interactive)
  (set-mark auto-mark isearch-initial-pos isearch-buffer)
  (message "Set auto-mark.")
  (setq isearch-last-match (car (car isearch-trace)))
  (throw 'isearch (cursor-pos)))

;; Cancel the search or if search is failing backtrack to the last match
(defun isearch-cancel ()
  (interactive)
  (if (not (isearch-looking-at))
      (progn
	(while (and (not (isearch-looking-at)) (cdr isearch-trace))
	  (isearch-rubout))
	(isearch-title))
    (goto-char isearch-initial-pos)
    (throw 'isearch nil)))

;; Copy the rest of the current word to the search string
(defun isearch-yank-word ()
  (interactive)
  (when (isearch-looking-at)
    (isearch-push-string (concat (car (car isearch-trace))
				 (copy-area (match-end)
					    (forward-word 1 (match-end))))))
  (isearch-title))

;; Copy the rest of the line to the search string
(defun isearch-yank-line ()
  (interactive)
  (when (isearch-looking-at)
    (isearch-push-string (concat (car (car isearch-trace))
				 (copy-area (match-end) (line-end)))))
  (isearch-title))

;; Backup one match/string
(defun isearch-rubout ()
  (interactive)
  (cond
    ((cdr (car isearch-trace))
      (isearch-pop-match))
    ((cdr isearch-trace)
      (isearch-pop-string))
    (t
      (error "Beginning of I-search")))
  (isearch-title))

;; Add the typed character to the search string
(defun isearch-unbound-key-fun ()
  (let
      ((str (current-event-string)))
    (if (/= (length str) 1)
        (isearch-accept)
      (isearch-push-string (concat (car (car isearch-trace)) str))))
  (isearch-title))

(defun isearch (isearch-forwards)
  (let
      ((isearch-trace (cons (cons "" nil) nil))
       (isearch-initial-pos (cursor-pos))
       (isearch-buffer (current-buffer))
       isearch-failing
       isearch-wrapped
       isearch-re-error
       (esc-means-meta nil)		; want to bind to ESC
       (old-kp keymap-path))
    (setq keymap-path '(isearch-keymap))
    (add-hook 'unbound-key-hook 'isearch-unbound-key-fun)
    (unwind-protect
	(catch 'isearch
	  (isearch-title)
	  (recursive-edit))
      (with-buffer isearch-buffer
	(remove-hook 'unbound-key-hook 'isearch-unbound-key-fun)
	(setq keymap-path old-kp)))))

;;;###autoload
(defun isearch-forward ()
  "Enters the incremental search mode. You are then able to type letters to add
to the regexp being searched for. Special commands are,\n
  `Ctrl-s'     Search forwards for another occurrence
  `Ctrl-r'     Search backwards 
  `Ctrl-g'     If the search is failing, backtrack to the first non-failing
               match, else, cancel the search leaving the cursor at its
               original position.
  `Ctrl-w'     Copy the rest of the current word into the search string
  `Ctrl-y'     Copy the rest of the line to the search string
  `RET'        Exit isearch leaving the cursor at its current position
  `BS'         Retrace your movements one step"
  (interactive)
  (isearch t))

;;;###autoload
(defun isearch-backward ()
  "Similar to `isearch-forward' except the first searching is done in the other
direction."
  (interactive)
  (isearch nil))

(defun isearch-next-forward ()
  (interactive)
  (if (and (equal (car (car isearch-trace)) "") isearch-last-match)
      (progn
	(isearch-push-string isearch-last-match)
	(isearch-title))
    (let
	((next (if (and isearch-failing isearch-forwards)
	           (progn
		     (setq isearch-wrapped t)
		     (buffer-start))
		 (next-char))))
      (setq next (isearch-find-next-regexp next))
      (cond
        ((posp next)
	  (isearch-push-match (cursor-pos))
	  (setq isearch-failing nil)
	  (goto-char next))
	((null next)
	  (setq isearch-failing t)
	  (beep)))
      (setq isearch-forwards t)
      (isearch-title))))

(defun isearch-next-backward ()
  (interactive)
  (if (and (equal (car (car isearch-trace)) "") isearch-last-match)
      (progn
	(isearch-push-string isearch-last-match)
	(isearch-title))
    (let
	((next (if (and isearch-failing (not isearch-forwards))
		   (progn
		     (setq isearch-wrapped t)
		     (buffer-end))
		 (prev-char))))
      (setq next (isearch-find-prev-regexp next))
      (cond
       ((posp next)
	(isearch-push-match (cursor-pos))
	(setq isearch-failing nil)
	(goto-char next))
       ((null next)
	(setq isearch-failing t)
	(beep)))
      (setq isearch-forwards nil)
      (isearch-title))))
