;;; kill-ring-search.el --- incremental search for the kill ring
;;
;; Copyright (C) 2006,2007 Nikolaj Schumacher
;;
;; Author: Nikolaj Schumacher <bugs * nschum de>
;; Version: 1.1
;; Keywords: convenience, matching
;; URL: http://nschum.de/src/emacs/kill-ring-search/kill-ring-search.el
;; Compatibility: GNU Emacs 21.x, GNU Emacs 22.x
;;
;; This file is NOT part of GNU Emacs.
;;
;; This program 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
;; of the License, or (at your option) any later version.
;;
;; This program 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 this program; if not, write to the Free Software
;; Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
;;
;;; Commentary:
;;
;; To install, add the following to your .emacs file:
;; (autoload 'kill-ring-search "kill-ring-search"
;;  "Search the kill ring in the minibuffer."
;;  (interactive))
;; (global-set-key "\M-\C-y" 'kill-ring-search)
;;
;; Just call kill-ring-search and enter your search.
;; M-y and C-y work as usual.  You can also use C-r like in a shell.
;; C-v, M-v, C-n and C-p will scroll the view.
;;
;;; Change Log:
;;
;; 2007-05-15 (1.1)
;;    Added compatibility to icomplete-mode.
;;    Added scrolling support.
;;
;; 2006-10-12 (1.0)
;;    Initial release.
;;
;;; Code:

(require 'cl)

(defvar kill-ring-search-keymap
  (let ((map (copy-keymap minibuffer-local-map)))
    (define-key map "\C-r" 'kill-ring-search-prev)
    (define-key map "\M-y" 'kill-ring-search-prev)
    (define-key map "\C-y" 'exit-minibuffer)
    (define-key map "\C-v" 'kill-ring-scroll-up-page)
    (define-key map "\M-v" 'kill-ring-scroll-down-page)
    (define-key map "\C-n" 'kill-ring-scroll-up)
    (define-key map "\C-p" 'kill-ring-scroll-down)
    map)
  "*Keymap used inside the minibuffer by `kill-ring-search'.")

(defvar kill-ring-case-fold nil
  "*Non-nil if `kill-ring-search' should ignore case.")

(defvar kill-ring-search-pos nil
  "The remaining parts of the kill-ring to be searched by `search-kill-ring'.")

(defvar kill-ring-search-string nil
  "The current string searched for by `search-kill-ring'.")

(defvar kill-ring-search-calling-buffer nil
  "The buffer from which the current `search-kill-ring' originated.")

(defvar kill-ring-search-eoinput nil
  "Point where minibuffer input ends and completion info begins.")
(make-variable-buffer-local 'kill-ring-search-eoinput)

(defvar kill-ring-scroll-pos nil)

(defvar kill-ring-auto-scroll-pos nil)

(defun kill-ring-search-pre-command ()
  "Remove the current `kill-ring-search' match before minibuffer input."
  (delete-region kill-ring-search-eoinput (point-max)))

(defsubst kill-ring-search-substring (search-string string)
  "Search SEARCH-STRING in STRING while honoring `kill-ring-case-fold'."
  (if case-fold-search
      (search (downcase search-string) (downcase string))
    (search search-string string)))

(defun get-next-match (search-list)
  "Search SEARCH-LIST for a match on `kill-ring-search-string'."
  (let ((ring search-list)
        (res nil))
    (while (and ring (null res))
      (if (kill-ring-search-substring kill-ring-search-string (car ring))
          (setq res (car ring))
        (setq ring (cdr ring))))
    ring))

(defun kill-ring-chop-newline (text)
  "Chop off trailing newline in TEXT if any."
  (let ((last (1- (length text))))
    (if (and (>= last 0)
             (eq ?\n (elt text last)))
        (substring text 0 last)
      text)))

(defun kill-ring-search-create-highlighted-match
  (string search-string max-lines first-line)
  "Return a copy of STRING that highlights the the `kill-ring-search'.
If FIRST-LINE is set, start with that line, otherwise start with a line so
that SEARCH-STRING is visible."
  (if string
      (let ((lines (split-string string "^")))
        (when (equal (car lines) "")
          (pop lines))
        (dotimes (i (or first-line 0))
          (pop lines))
        (let* ((display-start (cons nil nil))
               (display-tail display-start)
               (no-match-yet (null first-line)))
          (while (and lines (or (>= (decf max-lines) 0)
                                no-match-yet))
            (let* ((line (pop lines))
                   (pos (kill-ring-search-substring search-string line)))
              (when pos
                (add-text-properties pos (+ pos (length search-string))
                                     '(face highlight) line)
                (setq no-match-yet nil))
              (setcdr display-tail (setq display-tail (cons line nil)))
              (when (<= max-lines 0)
                ;; pop off the beginning, so we don't produce too many lines
                (pop display-start))))
          (setq kill-ring-auto-scroll-pos
                (- (or first-line 0)
                   (if (< max-lines -1) (+ 1 max-lines) 0)))
          (kill-ring-chop-newline (apply 'concat display-start))))
    "NO MATCH"))

(defun kill-ring-search-post-command ()
  "Display the current `kill-ring-search' match after minibuffer input occured."
  (let ((contents (buffer-substring (minibuffer-prompt-end) (point-max))))
    (setq kill-ring-search-eoinput (point-max))
    (save-excursion
      (goto-char (point-max))
      (setq kill-ring-search-string contents)
      (let ((match (get-next-match kill-ring-search-pos)))
        (unless match
          ;; reset, if nothing was found
          (setq kill-ring-scroll-pos nil)
          (setq kill-ring-search-pos kill-ring)
          (setq match (get-next-match kill-ring-search-pos)))
        (insert "\n" (kill-ring-search-create-highlighted-match
                      (car match)
                      kill-ring-search-string
                      (- (floor (kill-ring-search-max-minibuffer-size)) 2)
                      kill-ring-scroll-pos))))))

(defun kill-ring-search-max-minibuffer-size ()
  "Return the maximum size the minibuffer can get."
  (if resize-mini-windows
      (cond ((floatp max-mini-window-height)
             (* (frame-height)
                max-mini-window-height))
            ((integerp max-mini-window-height)
             max-mini-window-height)
            (t 1))
    1))

(defun kill-ring-search-minibuffer-setup ()
  "Set up the minibuffer for `kill-ring-search' completions."
  (add-hook 'post-command-hook 'kill-ring-search-post-command nil t)
  (add-hook 'pre-command-hook 'kill-ring-search-pre-command nil t)
  (with-current-buffer kill-ring-search-calling-buffer
    (remove-hook 'minibuffer-setup-hook 'kill-ring-search-minibuffer-setup))
  (setq kill-ring-search-calling-buffer nil))

;;;###autoload
(defun kill-ring-search ()
  "Search the kill ring in the minibuffer."
  (interactive)
  (let ((minibuffer-local-completion-map kill-ring-search-keymap)
        (iswitchb-require-match t))
    (setq kill-ring-search-eoinput (point-max))
    (setq kill-ring-scroll-pos nil)
    (setq kill-ring-search-calling-buffer (current-buffer))
    (setq kill-ring-search-pos kill-ring)
    (setq kill-ring-search-string "")
    (let ((minibuffer-setup-hook)
          (ido-enable-replace-completing-read nil))
      (add-hook 'minibuffer-setup-hook 'kill-ring-search-minibuffer-setup)
      (completing-read "Kill ring search: " '(("dummy" . 1)) nil nil nil nil)
      (let ((result (car-safe (get-next-match kill-ring-search-pos))))
        (unless result (error "No match"))
        (insert result))
      (setq kill-ring-search-pos kill-ring)
      (setq kill-ring-search-string ""))))

(defun kill-ring-scroll-up (&optional arg)
  (interactive "p")
  (setq kill-ring-scroll-pos
        (max 0 (+ arg (or kill-ring-scroll-pos kill-ring-auto-scroll-pos 0)))))

(defun kill-ring-scroll-down (&optional arg)
  (interactive "p")
  (kill-ring-scroll-up (- arg)))

(defun kill-ring-scroll-up-page (&optional arg)
  (interactive "p")
  (kill-ring-scroll-up (- (window-height) 1 next-screen-context-lines)))

(defun kill-ring-scroll-down-page (&optional arg)
  (interactive "p")
  (kill-ring-scroll-down (- (window-height) 1 next-screen-context-lines)))

(defun kill-ring-search-prev ()
  "Return the previous match also matching the current `kill-ring-search'"
  (interactive)
  (let ((new (get-next-match (cdr (get-next-match kill-ring-search-pos)))))
    (if new
        (progn
          (setq kill-ring-search-pos new)
          (setq kill-ring-scroll-pos 0))
      (beep))))

(provide 'kill-ring-search)

;;; kill-ring-search.el ends here