summaryrefslogtreecommitdiffstats
path: root/emacs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--emacs.d/lisp/smex.el360
1 files changed, 360 insertions, 0 deletions
diff --git a/emacs.d/lisp/smex.el b/emacs.d/lisp/smex.el
new file mode 100644
index 0000000..e2f7ab0
--- /dev/null
+++ b/emacs.d/lisp/smex.el
@@ -0,0 +1,360 @@
+;;; smex.el --- A smart M-x enhancement for Emacs.
+;; (C) 2009 Cornelius Mika
+;; Licensed under the same terms as Emacs.
+
+;;; Quick Start:
+;; (require 'smex)
+;; Run this after all your other Emacs packages have been loaded:
+;; (smex-initialize)
+;;
+;; Bind the following commands:
+;; smex, smex-major-mode-commands, smex-update-and-run
+;;
+;; For a detailed introduction see:
+;; http://github.com/nonsequitur/smex/blob/master/README.markdown
+
+(require 'ido)
+;; Needed for `union'.
+(require 'cl)
+
+(defcustom smex-save-file "~/smex.save"
+ "File in which the smex state is saved between Emacs sessions.
+Variables stored are: `smex-data', `smex-history'.
+Must be set before initializing Smex."
+ :type 'string
+ :group 'smex)
+
+(defcustom smex-history-length 7
+ "Determines on how many recently executed commands
+Smex should keep a record.
+Must be set before initializing Smex."
+ :type 'integer
+ :group 'smex)
+
+(defvar smex-cache)
+(defvar smex-ido-cache)
+(defvar smex-data)
+(defvar smex-history)
+
+;;--------------------------------------------------------------------------------
+;; Smex Interface
+
+(defun smex (&optional commands)
+ (interactive)
+ (unless commands (setq commands smex-ido-cache))
+ (let ((chosen (intern (smex-completing-read commands))))
+ (unwind-protect
+ (call-interactively chosen)
+ (smex-rank chosen)
+ (smex-show-key-advice chosen))))
+
+(defun smex-major-mode-commands ()
+ "Like `smex', but limited to commands that are relevant to the active major mode."
+ (interactive)
+ (let ((commands (union (extract-commands-from-keymap major-mode)
+ (extract-commands-from-features major-mode))))
+ (setq commands (smex-sort-according-to-cache commands))
+ (setq commands (mapcar (lambda (command) (symbol-name command)) commands))
+ (smex commands)))
+
+(defun smex-completing-read (choices)
+ (let ((ido-completion-map ido-completion-map)
+ (ido-setup-hook (cons 'smex-prepare-ido-bindings ido-setup-hook))
+ (ido-enable-prefix nil)
+ (ido-enable-flex-matching t)
+ (ido-max-prospects 10))
+ (ido-completing-read "smex " choices)))
+
+(defun smex-prepare-ido-bindings ()
+ (define-key ido-completion-map (kbd "C-h f") 'smex-describe-function)
+ (define-key ido-completion-map (kbd "C-a") 'move-beginning-of-line))
+
+;;--------------------------------------------------------------------------------
+;; Cache and Maintenance
+
+(defun smex-rebuild-cache ()
+ (interactive)
+ (setq smex-cache nil)
+
+ ;; Build up list 'new-commands' and later put it at the end of 'smex-cache'.
+ ;; This speeds up sorting.
+ (let (new-commands)
+ (mapatoms (lambda (symbol)
+ (when (commandp symbol)
+ (let ((known-command (assq symbol smex-data)))
+ (if known-command
+ (setq smex-cache (cons known-command smex-cache))
+ (setq new-commands (cons (list symbol) new-commands)))))))
+ (if (eq (length smex-cache) 0)
+ (setq smex-cache new-commands)
+ (setcdr (last smex-cache) new-commands)))
+
+ (setq smex-cache (sort smex-cache 'smex-sorting-rules))
+ (smex-restore-history)
+ (setq smex-ido-cache (smex-convert-for-ido smex-cache)))
+
+(defun smex-convert-for-ido (command-items)
+ (mapcar (lambda (command-item) (symbol-name (car command-item))) command-items))
+
+(defun smex-restore-history ()
+ "Rearranges `smex-cache' according to `smex-history'"
+ (if (> (length smex-history) smex-history-length)
+ (setcdr (nthcdr (- smex-history-length 1) smex-history) nil))
+ (mapc (lambda (command)
+ (unless (eq command (caar smex-cache))
+ (let ((command-cell-position (smex-detect-position smex-cache (lambda (cell)
+ (eq command (caar cell))))))
+ (if command-cell-position
+ (let ((command-cell (smex-remove-nth-cell command-cell-position smex-cache)))
+ (setcdr command-cell smex-cache)
+ (setq smex-cache command-cell))))))
+ (reverse smex-history)))
+
+(defun smex-update ()
+ (interactive)
+ (smex-save-history)
+ (smex-rebuild-cache))
+
+(defun smex-update-and-run ()
+ (interactive)
+ (smex-update)
+ (smex))
+
+(defun smex-auto-update (&optional idle-time)
+ "Update Smex when Emacs has been idle for IDLE-TIME."
+ (unless idle-time (setq idle-time 60))
+ (run-with-idle-timer idle-time t 'smex-update))
+
+(defun smex-initialize ()
+ (interactive)
+ (unless ido-mode (smex-initialize-ido))
+ (let ((save-file (expand-file-name smex-save-file)))
+ (if (file-readable-p save-file)
+ (with-temp-buffer
+ (insert-file-contents save-file)
+ (setq smex-history (read (current-buffer))
+ smex-data (read (current-buffer))))
+ (setq smex-history nil smex-data nil))
+ (smex-rebuild-cache)
+ (add-hook 'kill-emacs-hook 'smex-save-to-file)))
+
+(defun smex-initialize-ido ()
+ "Sets up a minimal Ido environment for `ido-completing-read'."
+ (ido-init-completion-maps)
+ (add-hook 'minibuffer-setup-hook 'ido-minibuffer-setup))
+
+(defun smex-save-history ()
+ "Updates `smex-history'"
+ (setq smex-history nil)
+ (let ((cell smex-cache))
+ (dotimes (i smex-history-length)
+ (setq smex-history (cons (caar cell) smex-history))
+ (setq cell (cdr cell))))
+ (setq smex-history (nreverse smex-history)))
+
+(defun smex-save-to-file ()
+ (interactive)
+ (smex-save-history)
+ (with-temp-file (expand-file-name smex-save-file)
+ (ido-pp 'smex-history)
+ (ido-pp 'smex-data)))
+
+;;--------------------------------------------------------------------------------
+;; Ranking
+
+(defun smex-sorting-rules (command-item other-command-item)
+ "Returns true if COMMAND-ITEM should sort before OTHER-COMMAND-ITEM."
+ (let ((count (cdr command-item))
+ (name (car command-item))
+ (other-count (cdr other-command-item))
+ (other-name (car other-command-item)))
+ (if count
+ (not (and other-count
+ (or (< count other-count)
+ (and (= count other-count)
+ (string< other-name name)))))
+ (not (or other-count
+ (string< other-name name))))))
+
+(defun smex-rank (command)
+ (let ((command-item (assq command smex-cache)))
+ ;; TODO: Should we first update the cache and
+ ;; then try again if command-item is nil?
+ (when command-item
+ (smex-update-count command-item)
+
+ ;; Don't touch the cache order if the chosen command
+ ;; has just been execucted previously.
+ (unless (eq command-item (car smex-cache))
+ (let (command-cell
+ (pos (smex-detect-position smex-cache (lambda (cell)
+ (eq command-item (car cell))))))
+ ;; Remove the just executed command.
+ (setq command-cell (smex-remove-nth-cell pos smex-cache))
+ ;; And put it on top of the cache.
+ (setcdr command-cell smex-cache)
+ (setq smex-cache command-cell)
+
+ ;; Repeat the same for the ido cache. Should this be DRYed?
+ (setq command-cell (smex-remove-nth-cell pos smex-ido-cache))
+ (setcdr command-cell smex-ido-cache)
+ (setq smex-ido-cache command-cell)
+
+ ;; Now put the last history item back to its normal place.
+ (smex-sort-item-at smex-history-length))))))
+
+(defun smex-update-count (command-item)
+ (let ((count (cdr command-item)))
+ (setcdr command-item
+ (if count
+ (1+ count)
+ ;; Else: Command has just been executed for the first time.
+ ;; Add it to `smex-data'.
+ (if smex-data
+ (setcdr (last smex-data) (list command-item))
+ (setq smex-data (list command-item)))
+ 1))))
+
+(defun smex-sort-item-at (n)
+ "Sorts item at position N in `smex-cache'."
+ (let* ((command-cell (nthcdr n smex-cache))
+ (command-item (car command-cell))
+ (command-count (cdr command-item)))
+ (let ((insert-at (smex-detect-position command-cell (lambda (cell)
+ (smex-sorting-rules command-item (car cell))))))
+ ;; TODO: Should we handle the case of 'insert-at' being nil?
+ ;; This will never happen in practice.
+ (when (> insert-at 1)
+ (setq command-cell (smex-remove-nth-cell n smex-cache))
+ ;; smex-cache just got shorter by one element, so subtract '1' from insert-at.
+ (setq insert-at (+ n (- insert-at 1)))
+ (smex-insert-cell command-cell insert-at smex-cache)
+
+ ;; Repeat the same for the ido cache. DRY?
+ (setq command-cell (smex-remove-nth-cell n smex-ido-cache))
+ (smex-insert-cell command-cell insert-at smex-ido-cache)))))
+
+(defun smex-detect-position (cell function)
+ "Detects, relatively to CELL, the position of the cell
+on which FUNCTION returns true.
+Only checks cells after CELL, starting with the cell right after CELL.
+Returns nil when reaching the end of the list."
+ (let ((pos 1))
+ (catch 'break
+ (while t
+ (setq cell (cdr cell))
+ (if (not cell)
+ (throw 'break nil)
+ (if (funcall function cell) (throw 'break pos))
+ (setq pos (1+ pos)))))))
+
+(defun smex-remove-nth-cell (n list)
+ "Removes and returns the Nth cell in LIST."
+ (let* ((previous-cell (nthcdr (- n 1) list))
+ (result (cdr previous-cell)))
+ (setcdr previous-cell (cdr result))
+ result))
+
+(defun smex-insert-cell (new-cell n list)
+ "Inserts cell at position N in LIST."
+ (let* ((cell (nthcdr (- n 1) list))
+ (next-cell (cdr cell)))
+ (setcdr (setcdr cell new-cell) next-cell)))
+
+;;--------------------------------------------------------------------------------
+;; Help and Reference
+
+(defun smex-describe-function ()
+ (interactive)
+ (describe-function (intern (car ido-matches)))
+ (pop-to-buffer "*Help*"))
+
+(defvar smex-old-message nil
+ "A temporary storage used by `smex-show-key-advice'")
+
+(defun smex-show-key-advice (command)
+ "Shows the keybinding for command, if available. Like `execute-extended-command'."
+ (let ((advice (smex-key-advice command)))
+ (when advice
+ (if (current-message)
+ (progn
+ (run-at-time 2 nil (lambda (advice)
+ (setq smex-old-message (current-message))
+ (smex-unlogged-message advice)) advice)
+
+ (run-at-time 4.5 nil (lambda (advice)
+ (if (equal (current-message) advice)
+ (smex-unlogged-message smex-old-message))) advice))
+ (smex-unlogged-message advice)))))
+
+(defun smex-key-advice (command)
+ (let ((keys (where-is-internal command)))
+ (if keys
+ (format "You can run the command `%s' with %s"
+ command
+ (mapconcat 'key-description keys ", ")))))
+
+(defun smex-unlogged-message (string)
+ "Bypasses logging in *Messages*"
+ (let (message-log-max)
+ (message "%s" string)))
+
+(defun extract-commands-from-keymap (mode)
+ (let ((keymap (intern (concat (symbol-name mode) "-map"))))
+ (if keymap
+ (parse-keymap (symbol-value keymap)))))
+
+(defun parse-keymap (map)
+ (let (commands)
+ (map-keymap (lambda (binding element)
+ (if (listp element)
+ (if (eq 'keymap (car element))
+ (parse-keymap element))
+ (setq commands (cons element commands)))) map)
+ commands))
+
+(defun extract-commands-from-features (mode)
+ (let ((library-path (symbol-file mode))
+ (mode-name (symbol-name mode))
+ commands)
+ (string-match "\\(.+?\\)\\(-mode\\)?$" mode-name)
+ ;; 'lisp-mode' -> 'lisp'
+ (setq mode-name (match-string 1 mode-name))
+ (dolist (feature load-history)
+ (let ((feature-path (car feature)))
+ (when (and feature-path (or (equal feature-path library-path)
+ (string-match (regexp-quote mode-name)
+ (file-name-nondirectory feature-path))))
+ (dolist (item (cdr feature))
+ (if (and (listp item) (eq 'defun (car item)))
+ (let ((function (cdr item)))
+ (when (commandp function)
+ (setq commands (append commands (list function))))))))))
+ commands))
+
+(defun smex-sort-according-to-cache (list)
+ (let (sorted)
+ (dolist (command-item smex-cache)
+ (let ((command (car command-item)))
+ (when (memq command list)
+ (setq sorted (cons command sorted))
+ (setq list (delq command list)))))
+ (nreverse (append list sorted))))
+
+(defun smex-show-unbound-commands ()
+ "Shows unbound commands in a new buffer,
+sorted by frequency of use."
+ (interactive)
+ (setq smex-data (sort smex-data 'smex-sorting-rules))
+ (let ((unbound-commands (delq nil
+ (mapcar (lambda (command-item)
+ (unless (where-is-internal (car command-item))
+ command-item))
+ smex-data))))
+ (view-buffer-other-window "*Smex: Unbound Commands*")
+ (toggle-read-only nil)
+ (erase-buffer)
+ (ido-pp 'unbound-commands)
+ (beginning-of-buffer)))
+
+(provide 'smex) \ No newline at end of file