gnu-emacs-sources
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

irepeat.el - repeat through history data FAST


From: Kevin A. Burton
Subject: irepeat.el - repeat through history data FAST
Date: 22 Mar 2002 00:02:34 -0800
User-agent: Gnus/5.0808 (Gnus v5.8.8) Emacs/21.1.50

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1


OK...

I have been playing with this for a few months.  Took me about an hour to write
and then I kept tweaking it.

It is now at a point where I feel others might benefit from playing with this.

I would REALLY like to get feedback - especially when compared to the standard
Emacs completion mechanism.

Basically the concept here is to give you access to a LRU sorted history list
(command-history, file-name-history, buffer-list, semantic tokens) and work with
them.

Anyway... let me know what you think...

Kevin

;;; irepeat.el --- repeat through history data FAST

;; $Id: irepeat.el,v 1.12 2002/03/22 19:58:36 burton Exp $

;; Copyright (C) 2000-2003 Free Software Foundation, Inc.
;; Copyright (C) 2000-2003 Kevin A. Burton (address@hidden)

;; Author: Kevin A. Burton (address@hidden)
;; Maintainer: Kevin A. Burton (address@hidden)
;; Location: http://relativity.yi.org
;; Keywords: 
;; Version: 1.0.0

;; This file is [not yet] 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 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:

;; (disclaimer: This package requires GNU Emacs 21)

;; This is an 'intelligent repeat' package for Emacs. Basically it is modeled
;; after the reverse-i-search functionality in bash.
;;
;; Essentially this package allows you to *QUICKLY* go over a list of items and
;; select the best one (think of it as an isearch for lists).  In most
;; situations this is much faster than Emacs programmed completion.  Usually
;; this can be done in 2 or 3 key strokes, much faster than Emacs programmed
;; completion.  irepeat can handle huge lists and quickly jump to the best entry
;; in the list.
;;
;; There are implementation of irepeat for buffer switching, complex command
;; reptition, etc.  Most of these emulate the standard Emacs functions but use
;; irepeat instead.
;;
;; An added advantage is that since most are history lists, the more you access
;; a function or buffer, the higher it is on the list and the quicker you can
;; access it.  This basically provides a "first in last out" stack of data at
;; your fingertips.
;;
;; This package also has some cool features that you get for free.  For example
;; if you use irepeat-switch-to-buffer as a filter.  You can just enter a file
;; extension '.el' to narrow this list down to all Emacs Lisp files.

;;; Installation:

;; Installation is easy.  Just add to your load-path an do a (require 'irepeat).
;; The key bindings for irepeat functions such as `irepeat-switch-to-buffer'
;; should be done by the user as in most situations people may want to remap
;; Emacs standard key bindings.  For example I rebind 'C-x b' as
;; `irepeat-switch-to-buffer'.
;; 
;; Also I would highly recommend setting `history-length' to something longer
;; than the default, 30 is just too short.  For irepeat a value of 100 or higher
;; is recommended.  I run with a `history-length' of 250.
;;
;; NOTE: If you enjoy this software, please consider a donation to the EFF
;; (http://www.eff.org)

;;; Code:

;;; NOTES:
;;

;;; TODO:

;;; HIGH PRIORITY:

;; MAJOR BUG:  IF THIS is in this command history:
;;  (browse-url 
"http://s0b.bluestreak.com/ix.e?ht&s=38885&u=http%3A//adserver.usermagnet.com/cgi-bin/advertpro/banners.fpl%3Fregion%3D7%26bust%3D379951%26keyword%3DNULL&x=Insert_Click_Track_URL_Here";
 t)
;; 
;;   we get: apply: Not enough arguments for format string
;;
;;   We need to escape this here!

;; MAJOR NEW FEATURE:
;;
;; - Ability to display a graphical preview window as a buffer.  this would
;; include all the options around the match and highlight them with an overlay.

;; - BUG: when appending to the command-history, we need to prune the history so
;; that it isn't greater than history-length.
;;
;; - can we put the history-length var in the irepeat customization buffer?

;;; LOW PRIORITY:
;;
;; - REF: optionally put the user in a (read-string) prompt before evaluating
;;   the match so that values can be changed.  Could this be done with a control
;;   enter?
;;
;; - if we have a match... the arrows up/down/etc should go through the history
;;   as well.  This should search from index backwards or forwards for the next
;;   match.
;;
;; - SPEED IMPROVEMENT... if I type in an extra character.  We don't always have
;; to search FORWARD.  We can search from the current index on.  Since the
;; current request didn't match any of the previous entries it clearly will not
;; match a second time.  Note that this ISN'T the case if we hit the 'backspace'
;; key... we have to start from 0 again.

;;; RFE: could we support backward-kill-word like normal completion??
;;
;;  THIS COULD BECOME THE ESCAPE KEY

;;; RFE: we should support a type of plugin style operation for the 'tab' key.
;;
;;   1. expand the current match.  (I THINK WE SHOULD GET RID OF THIS) matches
;;   that are too long will fold over into two lines :(
;;
;;   2. cycle through all matches.
;;
;;   3. show a completion style buffer ALA programmed completion.

;;; RFE: ability to narrow down the types of objects that irepeat-semantic-jump
;;; can jump to (functions, variables, imports, etc)

;; RFE: when use faces is off... we should just MERGE faces.  this way we can
;; boldify certain regions.

;; RFE: ability to show ALL matches in an associated buffer (like completion).
;; This list would get smaller as the user typed in a more complex command.
;;
;;    - we would need to ONLY do this when the current text is greater than X
;;      items.
;;
;;    - should be disabled by default due to performance issues.
;;
;;    - should support shortening text (aka filename history)
;;
;;    - make the necessary selections bold.
;;
;;    - ability to run C-? to show the list.
;;
;;    - I think we whould MANUALLY have to invoke this...
;; 

;; how does completing-help hook into completion?  Maybe we can use a similar
;; mechanism within irepeat

;; RFE: should we always default to the first item in the list?

;; RFE: the standard Emacs completion allows the user to enter a '?'.  Maybe we
;; should just reserve this key sequence for us.

;; RFE: is there a way that I can provide addition 'help' or 'hints' when
;; completing.  For example with buffer completion it would be nice to include
;; the parent directory.
;;
;;     Example:
;;
;;     (irepeat-switch-to-buffer): [2/218] 'irepeat.el': irep  /home/burton/el
;;
;;     The cursor would be at 'irep' and we would put a font-lock-comment-face
;;     on /home/burton/el so that it doesn't visually interfere with our input.
;;
;;     We would provide a function named irepeat-get-hint which would return nil
;;     if there is no hint for the given object.
;;
;;     BUG THE MAIN PROBLEM IS: how do we put text AFTER the cursor in a
;;     read-event

;; RFE: maybe irepeat should use try-completion so that we can expand a prompt.
;; So for example if we have jde-and.el and jde-docindex.el and we type 'j tab'
;; we can expand to 'jde-'

;; RFE: could we maybe cache to-string operations on lists?  This way we can
;; quickly go ove ra list multiple times without having to call to-string a few
;; hundred times.

(require 'font-lock)
(require 'semantic)

(defcustom irepeat-command-prompt "%s [%i/%i] '%s': %s"
  "Prompt used for interactively prompting for a irepeat."
  :group 'irepeat
  :type 'string)

(defcustom irepeat-quit-message "%s: quit"
  "Message format used when quiting an irepeat."
  :group 'irepeat
  :type 'string)

(defcustom irepeat-use-faces t
  "If non-nil, use faces to highlight irepeat matches, etc."
  :group 'irepeat
  :type 'boolean)

(defcustom irepeat-sit-after-selection t
  "Display the match and sit for a short time after a selection has been made."
  :group 'irepeat
  :type 'boolean)

(defcustom irepeat-expand-match-after-selection t
  "Expand the match after a selection has been made."
  :group 'irepeat
  :type 'boolean)

(defface irepeat-match-face '((t (:inherit font-lock-constant-face)))
  "Face used for the last available match."
  :group 'irepeat)

(defface irepeat-hit-face '((t (:bold t :inherit font-lock-constant-face)))
  "Face used for the last available match."
  :group 'irepeat)

(defface irepeat-blink-line-face '((t (:background "DarkBlue")))
  "Face used for the last available match."
  :group 'irepeat)

(defcustom irepeat-semantic-token-display-function 
'semantic-uml-concise-prototype-nonterminal
  "*Semantic function to use for displaying tokens in the methods buffer.  Some
useful functions are found in `semantic-token->text-functions'.  This
functionality also allows the user to display tokens as UML.  To enable this
functionality set this value to either
`semantic-uml-concise-prototype-nonterminal',
`semantic-uml-prototype-nonterminal', or `semantic-uml-abbreviate-nonterminal'."
  :group 'irepeat
  :type semantic-token->text-custom-list
  :initialize 'custom-initialize-default)

(defvar irepeat-blink-line-overlay nil "Overlay used for blinking lines.")

(defun irepeat-complex-command()
  "Repeat a comlex command interactively.  See `repeat-complex-command' for more
information."
  (interactive)
  
  (irepeat "(irepeat-complex-command):" command-history
           (lambda(match)
             (setq command-history (cons match command-history))
               
             (eval match))))

(defun irepeat-switch-to-buffer()
  "Run `switch-to-buffer' but prompt for the buffer name with irepeat."
  (interactive)

  (let((buffer-list '())
       (prompt-prefix "(irepeat-switch-to-buffer):"))

    ;;go through the buffer list and make sure we don't include any hidden 
buffer
    (dolist(buffer (buffer-list (selected-frame)))

      ;;it is faster if we just did a substring..  We could use regexp (and I
      ;;think it would be more readable) but this will be slower.
      (when (not (string-equal " " (substring (buffer-name buffer) 0 1)))
        (add-to-list 'buffer-list buffer t)))

    ;;TODO: maybe we should ue other-buffer here... ???

    ;;make sure the current buffer isn't in the list.
    (setq buffer-list (delete (current-buffer) buffer-list))
    
    (let((first-element (cons 0 (nth 0 buffer-list))))
    
      (irepeat prompt-prefix buffer-list
               (lambda(match)
                 
                 (switch-to-buffer match)) nil first-element))))

(defun irepeat-semantic-jump()
  "Jump to a semantic token in the current buffer with irepeat."
  (interactive)

    (let((prompt-prefix "(irepeat-semantic-jump):")
         (nonterminal-list '())
         (nonterminal nil)
         (irepeat-sit-after-selection nil))

      ;;if this buffer isn't bovinated go ahead and do it... this only happens
      ;;the first time we run ig.
      (semantic-bovinate-toplevel) 
      
      (save-excursion

        (beginning-of-buffer)
        
        (while (setq nonterminal (semantic-find-nonterminal-by-overlay-next 
(point)))

          (when nonterminal

            ;;debug message...

            ;;add this nonterminal to the list as a string...
            (add-to-list 'nonterminal-list nonterminal)
            
            ;;goto the end of the nonterminal for better performance.
            
            (goto-char (semantic-token-start nonterminal)))))

      ;;call into irepeat.
      (irepeat prompt-prefix nonterminal-list
               (lambda(match)
                 
                 (goto-char (semantic-token-start match))
                 
                 (recenter)
                 
                 ;;temporarily place an overlay across the whole line and then
                 ;;remove it.
                 (irepeat-blink-line)) t)))

(defun irepeat-find-file()
  "Use the `file-name-history' to find a file."
  (interactive)
  
  (let((prompt-prefix "(irepeat-find-file): ")
       (first-element (cons 0 (nth 0 file-name-history))))
  
    (irepeat prompt-prefix file-name-history (lambda(match)
                                               (find-file match)) nil 
first-element)))

(defun irepeat(irepeat-prompt-prefix irepeat-list success-function &optional
irepeat-no-faces first-element)
  "Use irepeat on the given `irepeat-list' and return the match or nil if no
match was found.  The `irepeat-prompt-prefix' is used as a prefix when
prompting.  When irepeat is successful we call `success-function' with one
argument, the match found.  When `irepeat-no-faces' is true, we do not use the
'face property for irepeat prompts.  If `first-element' is specified, this is
the first element of the `irepeat-list' that we should use as the default input.
This can be used with `irepeat-switch-to-buffer' etc to provide default input."

  ;;define irepeat completion variables.  These variable names are used within
  ;;this function and some utiity functions to determine how completion works.
  ;;
  ;; `irepeat-query' : the text the user has currently entered.  For example if
  ;; you want to look for a buffer named 'irepeat.el' and you have typed 'ir'
  ;; this value will be set to 'ir'.

  ;; `irepeat-event' : the current character (event) you have just entered.

  ;; `irepeat-current-match' : the current element within irepeat-list that is
  ;; matching (or nil)

  ;; `irepeat-keep-matching' : if non-nil, we should keep matching on a branch
  ;; of the while loop. If nil, we should stop (probably because we need to
  ;; exit).

  ;; `irepeat-index' : if a match has been found based on the current query,
  ;; irepeat-index is set to the current element index within the given list.

  ;; `irepeat-skip-guess' : when we are reading events, will skip over guessing
  ;; another match. 
  
  (let((irepeat-query "")
       (irepeat-event nil)
       (irepeat-current-match nil)
       (irepeat-keep-matching t)
       (irepeat-index -1)
       (irepeat-skip-guess nil))

    (setq irepeat-query "")
    
    (when first-element
      (setq irepeat-index (car first-element))
      (setq irepeat-current-match (cdr first-element)))
    
    ;; `irepeat-match-result' : if non-nil we have found a match for the current
    ;; event that is about to be entered.
    
    (while (and irepeat-keep-matching
                (let(irepeat-match-result)
                  
                  (setq irepeat-event (irepeat-read-event irepeat-current-match 
irepeat-query))
                  
                  (if (not (integerp irepeat-event))
                      (progn
                        
                        ;;protect against non character events here - handle
                        ;;other types of events

                        ;;if we hit a backspace... remove some from the 
                        (if (equal irepeat-event 'backspace)
                            (irepeat-handle-backspace)
                          (if (equal irepeat-event 'tab)
                              (irepeat-handle-tab)
                            (if (equal irepeat-event 'return)
                                (irepeat-handle-return)
                              ;;if we go up or down... allow us to walk up the 
list.
                              (if (or (equal irepeat-event 'up)
                                      (equal irepeat-event 'down))
                                  (irepeat-handle-up-or-down)
                                (if (equal irepeat-event 'escape)
                                    (irepeat-handle-escape)

                                  ;; else... stop by default.
                                  
                                  (setq irepeat-keep-matching nil)
                                  (setq irepeat-current-match nil)
                                  (setq irepeat-match-result nil)))))))
                    
                    ;;make sure this is an acceptable character. if it isn't,
                    ;;we should return
                    (if (or (<= irepeat-event 31)
                            (>= irepeat-event 127))
                        (progn
                          
                          (setq irepeat-match-result nil)
                          (setq irepeat-keep-matching nil)
                          (setq irepeat-current-match nil))
                      
                      ;;else we can keep going.
                      
                      (setq irepeat-query (concat irepeat-query (char-to-string 
irepeat-event)))
                      
                      ;;also make sure that the irepeat-event is not a control 
character. AKA ^H
                      (when (char-valid-p irepeat-event)
                        (setq irepeat-match-result t)))
                    
                    irepeat-match-result)))

      (if irepeat-skip-guess
          ;;skil this round.
          (setq irepeat-skip-guess nil)
        
        (let((guessed-match (irepeat-guess-match irepeat-query)))

          (setq irepeat-index (car guessed-match))
          (when irepeat-index
            (setq irepeat-index (1- irepeat-index)))

          (setq irepeat-current-match (cdr guessed-match)))))

    ;;ok... handle a successful match or a quit.
    (if irepeat-current-match
        (funcall success-function irepeat-current-match)
      (irepeat-message-without-log irepeat-quit-message irepeat-prompt-prefix))
    
    irepeat-current-match))

(defun irepeat-read-event(match prompt)
  "Read an event from the minibuffer.  This is used to setup additional features
we need (such as text echoing)."

  (let((cursor-in-echo-area t))

    ;;display the message with font properties in the minibuffer.
    (irepeat-message-without-log (irepeat-make-prompt match prompt))

    (setq irepeat-event (read-event))))

(defun irepeat-make-prompt(match prompt)
  "Return a prompt with faces attached.  The `match' param can be a string or a
symbol representing a match - AKA a lisp expression.  The `prompt' param is the
text the user has currently typed."

  (let(result match-string hit-face)

    (if match
        (setq match-string (irepeat-to-string match))
      (setq match-string ""))

    (when irepeat-use-faces

      ;;determine the face to use

      ;;FIXME: we should use our own custom face list here.  This should be
      ;;merged from the SOURCE property 'face, its attributes poped out, and a
      ;;new list created with (bold . t)_
      
      (if irepeat-no-faces
          (setq hit-face 'bold)

        ;;decorate the match.
        (put-text-property 0 (length match-string) 'face 'irepeat-match-face 
match-string)

        (setq hit-face 'irepeat-hit-face))

      ;;decorate the hit
      (when (> (length match-string) 0)
        (if (string-match (regexp-quote prompt) match-string)
            (put-text-property (match-beginning 0) (match-end 0) 'face hit-face 
match-string))))

    (when (null irepeat-index)
      (setq irepeat-index -1))
    
    (setq result (format irepeat-command-prompt
                         irepeat-prompt-prefix
                         irepeat-index
                         (length irepeat-list)
                         match-string
                         prompt))
    
    result))

(defun irepeat-selected(match prompt)
  "When a match is found update the prompt.  This basically expands the prompt
and displays it for a set amount of time."

  (when irepeat-sit-after-selection

    (if irepeat-expand-match-after-selection
        (irepeat-message-without-log (irepeat-make-prompt match 
(irepeat-to-string match)))
      (irepeat-message-without-log (irepeat-make-prompt match prompt)))
    
    (sit-for .3)
    
    (irepeat-message-without-log nil)))
  
(defun irepeat-guess-match(prompt)
  "Search for the matching prompt within `irepeat-list'.  Return the the match."

  ;;if the prompt is "" we obviously don't have a match.
  (if (string-equal "" prompt)
      nil
    ;;ok... test for a match.
    (let((not-found t)
         (index 0) 
         (current nil)
         (result nil))

      (while (and not-found
                  (< index (length irepeat-list)))

        (setq current (nth index irepeat-list))
        
        (when (string-match (regexp-quote prompt) (irepeat-to-string current))

          (setq not-found nil)

          (setq result current))

        (setq index (1+ index)))

      (when (not result)
        (setq index -1))
      
      ;;return a cons cell with the index and result.
      (cons index result))))

(defun irepeat-to-string(object)
  "Convert the given match to a string.  For example if this is an object, use
  `prin1-to-string'."

  (if (stringp object)
      ;;if this is already a string, we don't need to do anything.
      (substring object 0 (length object))
    (if (semantic-token-p object)
        (funcall irepeat-semantic-token-display-function object nil t)
      (if (bufferp object)
          (let((buffer-name (buffer-name object)))
            
            ;;we can't use the real buffername because we need to set text
            ;;properties on it.  Return a copy.
            
            (irepeat-to-string buffer-name))
        (prin1-to-string object)))))

(defun irepeat-blink-line()
  "Blink the currently selected line."
  (interactive)

  (let((overlay (make-overlay (point-at-bol) (1+ (point-at-eol)) 
(current-buffer))))

    (overlay-put overlay 'face 'irepeat-blink-line-face)

    (overlay-put overlay 'priority 1)

    (setq irepeat-blink-line-overlay overlay)
    
    ;;delete this overlay...
    (add-hook 'pre-command-hook 'irepeat-unblink-line)))

(defun irepeat-unblink-line()

  (delete-overlay irepeat-blink-line-overlay)

  (setq irepeat-blink-line-overlay nil)
  
  (remove-hook 'pre-command-hook 'irepeat-unblink-line))

(defun irepeat-message-without-log(&rest args)
  "Like `message' but don't log it on the message log. All arguments ARGS are
transfered to function `message'.  Note that we could potentially use other
minibuffer function other than `message' but it appears that these don't
preserve text properties."

  (let ((message-log-max nil))
    (apply 'message args)))

;;; KEY HANDLING FUNCTIONS

(defun irepeat-handle-backspace()
  "Handle the backspace character on `read-event'"

  (when (> (length irepeat-query) 0)
    (setq irepeat-query (substring irepeat-query 0 (1- (length 
irepeat-query)))))
  
  (setq irepeat-match-result t))

(defun irepeat-handle-tab()
  "Handle the tab character on `read-event'"

  ;;expand the irepeat-query based on the irepeat-current-match
  ;;(when irepeat-current-match
    ;;(setq irepeat-query (irepeat-to-string irepeat-current-match)))

  (setq irepeat-match-result t))

(defun irepeat-handle-return()
  "Handle the return character on `read-event'"

  (when irepeat-current-match
    (irepeat-selected irepeat-current-match irepeat-query))
  
  ;;if we hit return... stop.
  (setq irepeat-skip-guess t)
  (setq irepeat-keep-matching nil)
  (setq irepeat-match-result t))

(defun irepeat-handle-escape()
  "Handle the return character on `read-event'"

  (setq irepeat-index -1)
  (setq irepeat-match-result t)
  (setq irepeat-keep-matching t)
  (setq irepeat-current-match nil))

(defun irepeat-handle-up-or-down()
  "Handle the return character on `read-event'"

  ;;make sure to start at zero

  (let((not-found t)
       (dont-search nil))

    (if (and (equal irepeat-event 'down)
             (equal irepeat-index 0))
        (progn

          ;;when we are at the beginning of the list (0) obviously we can't go
          ;;down any farther so set the index to -1 and the match to nil.

          (setq dont-search t)
          (setq irepeat-current-match nil)
          (setq irepeat-index -1))
      (if (and (equal irepeat-event 'up)
               (equal irepeat-index nil))
          (setq irepeat-index 0)))

    (while (and not-found
                (not dont-search)
                (>= irepeat-index -1)
                (< irepeat-index (length irepeat-list)))

      ;;figure out how to change the irepeat-index
      (if (equal irepeat-event 'up)
          (setq irepeat-index (1+ irepeat-index))
        (setq irepeat-index (1- irepeat-index)))

      (setq current (nth irepeat-index irepeat-list))
      
      (when (string-match (regexp-quote irepeat-query) (irepeat-to-string 
current))
        
        (setq irepeat-current-match current)
        
        (setq not-found nil)))

    (when not-found
      ;;if we searched throught the whole list, and didn't find anything, set
      ;;the current match to nothing. (makes sense)
      (setq irepeat-current-match nil)))

  (setq irepeat-skip-guess t)
  
  (setq irepeat-keep-matching t))
  
;;; key bindings.
(global-set-key [?\C-r] 'irepeat-complex-command)
(global-set-key "\C-cj" 'irepeat-semantic-jump)
(global-set-key "\C-xb" 'irepeat-switch-to-buffer)

(provide 'irepeat)

;;; irepeat.el ends here

- -- 
Kevin A. Burton ( address@hidden, address@hidden, address@hidden )
             Location - San Francisco, CA, Cell - 415.595.9965
        Jabber - address@hidden,  Web - http://relativity.yi.org/

These are dangerous days.
To say what you feel is to dig your own grave.
Remember what I told you.
If they hated me, they will hate you.
    - Sinead O'Connor

-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.0.6 (GNU/Linux)
Comment: Get my public key at: http://relativity.yi.org/pgpkey.txt

iD8DBQE8muUaAwM6xb2dfE0RAmm6AJ9Dc8uCpLqE2q2mKt/oxxV/WUK8KgCfVWOS
NmRZ0p4VhnchqksM5b63f4I=
=0FfM
-----END PGP SIGNATURE-----



reply via email to

[Prev in Thread] Current Thread [Next in Thread]