Extending M-x in Emacs

« previous entry | next entry »
Apr. 27th, 2010 | 11:26 pm

Over 6 months ago, I revealed an Emacs Lisp version of `M-x'. I was pleased to have a few Emacs users who greeted a Lisp replacement for this core primitive with excitement.

What's the benefit of having a Lisp version? Well, it's easier to change than the C version. The syntax is easier and there is no compilation step.

Here's a modification I made to the original. It's a small change, but makes for a good example. It introduces a different representation of the prefix argument in the Minibuffer.

Typically, hitting `C-u' before `M-x' will show "C-u M-x" in the Minibuffer. However, if you issue more than one `C-u', then it just becomes a numeric argument and becomes "16 M-x", "64 M-x", "256 M-x" and so on. The small patch inserted below will show "C-u C-u M-x", "C-u C-u C-u M-x", "C-u C-u C-u C-u M-x" and so on, respectively. There's a good argument for the original behavior, since the powers of 4 are not that memorable to everyone. Letting Emacs handle the arithmetic and show it is helpful. However, visual feedback for what you're typing is a good interface design rule as well. As you can tell I'm agnostic, but like how this small change only takes a small bit of change to the code.

--- m-x.el	2009/08/25 00:03:17	1.1
+++ m-x.el	2009/08/25 00:07:12	1.1.1.1
@@ -22,8 +22,11 @@
 	       (cond ((eq prefixarg '-)
 		      "- ")
 		     ((and (consp prefixarg)
-			   (= (car prefixarg) 4))
-		      "C-u ")
+			   (zerop (% (car prefixarg) 4)))
+		      (apply 'concat
+			     (make-list (truncate
+					 (log (car prefixarg) 4))
+					"C-u ")))
 		     ((and (consp prefixarg)
 			   (integerp (car prefixarg)))
 		      (format "%d " (car prefixarg)))

Emacs knows the difference between a prefix argument with `C-u' and a numeric argument with `M-4' or `C-4'. So, `M-1 M-6 M-x' does not show "C-u C-u M-x" in the Minibuffer.

Here's the complete version of the code.

;; Based on Fexecute_extended_command in keyboard.c of Emacs.
;; Aaron S. Hawley <aaron.s.hawley(at)gmail.com> 2009-08-24
(defun execute-extended-command (prefixarg)
  "Read function name, then read its arguments and call it.

To pass a numeric argument to the command you are invoking with, specify
the numeric argument to this command.

Noninteractively, the argument PREFIXARG is the prefix argument to
give to the command you invoke, if it asks for an argument."
  (interactive "P")
  ;; The call to completing-read wil start and cancel the hourglass,
  ;; but if the hourglass was already scheduled, this means that no
  ;; hourglass will be shown for the actual M-x command itself.
  ;; So we restart it if it is already scheduled.  Note that checking
  ;; hourglass_shown_p is not enough,  normally the hourglass is not shown,
  ;; just scheduled to be shown.
  (let* ((hstarted (and (symbolp window-system)
                        (eq void-text-area-pointer 'hourglass)))
         (saved-keys (this-command-keys-vector)) ;; ?\M-x
         (buf (concat 
               (cond ((eq prefixarg '-)
                      "- ")
                     ((and (consp prefixarg)
                           (zerop (% (car prefixarg) 4)))
                      (apply 'concat
                             (make-list (truncate
                                         (log (car prefixarg) 4))
                                        "C-u ")))
                     ((and (consp prefixarg)
                           (integerp (car prefixarg)))
                      (format "%d " (car prefixarg)))
                     ((integerp prefixarg)
                      (format "%d " prefixarg))
                     (t ""))
               "M-x "))
         (function (completing-read buf obarray 'commandp t nil
                                    'extended-command-history)))
    (if hstarted (setq void-text-area-pointer 'hourglass))
    (if (and (stringp function)
             (= (length function) 0))
        (error "No command name given")
      ;; Set this_command_keys to the concatenation of saved-keys and
      ;; function, followed by a RET.
      (setq saved-keys (vconcat saved-keys
                                function
                                [return]))
      (setq function (intern function)))
    (setq prefix-arg prefixarg)
    (setq this-command function)
    (command-execute function 'record saved-keys)
    ;; If enabled, show which key runs this command.
    (if (and (not (null suggest-key-bindings))
             (null executing-kbd-macro))
        ;; If the command has a key binding, print it now.
        (let ((bindings (where-is-internal function
                                           overriding-local-map t))
              (waited))
          ;; But first wait, and skip the message if there is input.
          (when (and (not (null bindings))
                     (not (and (vectorp bindings)
                               (eq (aref bindings 0)
                                   'mouse-movement))))
              ;; If this command displayed something in the echo area;
              ;; wait a few seconds, then display our suggestion message.
              (if (null (current-message))
                  (setq waited (sit-for 0))
                (if (numberp suggest-key-bindings)
                    (setq waited (sit-for suggest-key-bindings))
                  (setq waited (sit-for 2))))
              (when (and (not (null waited))
                         (not (consp unread-command-events)))
                (with-temp-message (current-message)
                  (let ((binding (key-description bindings)))
                    (message "You can run the command `%s' with %s"
                             function binding)
                    (if (numberp suggest-key-bindings)
                        (setq waited (sit-for suggest-key-bindings))
                      (setq waited (sit-for 2)))))))))))

(provide 'm-x)

Consider putting the above in a file called m-x.el, then put the file in you load-path and add the following to your .emacs file.

(load "m-x")

In the next installment, I will show how to change `M-x' so that it notices a function name at point and offers it as the default command.

Link | Leave a comment | Add to Memories | Share

Comments {2}

emacs-devel?

from: yrk
date: Apr. 28th, 2010 05:22 am (UTC)
Link

6 months seems like a good trial period. Has this been through emacs-devel?

Reply | Thread

Aaron S. Hawley

Re: emacs-devel?

from: aaronhawley
date: Apr. 28th, 2010 11:20 am (UTC)
Link

No, not yet. I have two unrelated patches to the bug tracker, but they haven't cleared, yet. I'll probably send these bits at some point.

Reply | Parent | Thread