Duncan Bayne
Duncan Bayne

Reputation: 3949

Setting mode through a function in Emacs Lisp

I have the following code in my .emacs file, which works as you'd expect:

;; Ruby
(global-font-lock-mode 1)
(autoload 'ruby-mode "ruby-mode" "Ruby editing mode." t)
(setq auto-mode-alist (cons '("\\.rb$" . ruby-mode) auto-mode-alist))
(setq auto-mode-alist (cons '("\\.rsel$" . ruby-mode) auto-mode-alist))
(setq auto-mode-alist (cons '("\\.rhtml$" . html-mode) auto-mode-alist))
(setq auto-mode-alist (cons '("\\.erb$" . html-mode) auto-mode-alist))
(setq auto-mode-alist (cons '("\\.prawn$" . html-mode) auto-mode-alist))
(setq auto-mode-alist (cons '("Rakefile$" . ruby-mode) auto-mode-alist))

However, my attempts to DRY it up a bit fail:

(defun set-mode-for-filename-patterns (mode filename-pattern-list)
  (mapcar
    (lambda (filename-pattern)
      (setq 
        auto-mode-alist 
        (cons '(filename-pattern . mode) auto-mode-alist)))
    filename-pattern-list))

;; Ruby
(global-font-lock-mode 1)
(autoload 'ruby-mode "ruby-mode" "Ruby editing mode." t)
(set-mode-for-filename-patterns 
  ruby-mode
  '("\\.rb$"
    "\\.rsel$"
    "\\.rhtml$"
    "\\.erb$" 
    "\\.prawn$"
    "Rakefile$"
    "Gemfile$"))

... with the following error:

Debugger entered--Lisp error: (void-variable ruby-mode)
  (set-mode-for-filename-patterns ruby-mode (quote ("\\.rb$" "\\.rsel$" "\\.rhtml$" "\\.erb$" "\\.prawn$" "Rakefile$" "Gemfile$")))
  eval-buffer(#<buffer  *load*> nil "/home/duncan/.emacs" nil t)  ; Reading at buffer position 1768
  load-with-code-conversion("/home/duncan/.emacs" "/home/duncan/.emacs" t t)
  load("~/.emacs" t t)
  #[nil "\205\264

I'm a bit confused here ... in particular, I don't understand how ruby-mode is void & so can't be passed to a function, but can be consed into a pair?

Any pointers (heh) would be greatly appreciated.

Upvotes: 2

Views: 577

Answers (3)

Neil Smithline
Neil Smithline

Reputation: 1586

I think set-mode-for-filename-patterns is an interesting function. I'm going to add it to my config but use a more optimized implementation.

The implementations here all add one item to the auto-mode-alist variable for each file suffix. Emacs searches this list every time it finds a file. So the shorter the auto-mode-alist, the faster Emacs will find files.

This version is probably slower at startup but faster when finding files:

(defun set-mode-for-filename-patterns (mode filename-pattern-list)
  (push (cons (regexp-opt filename-pattern-list) mode)
        auto-mode-alist))`

This will work with the the same call:

(set-mode-for-filename-patterns 
   'ruby-mode
   '("\\.rb$"
     "\\.rsel$"
     "\\.rhtml$"
     "\\.erb$" 
     "\\.prawn$"
     "Rakefile$"
     "Gemfile$"))

If you look at the value of auto-mode-alist you'll see that many of the built-in modes use regexps for the same performance reason.

BTW, I advise that you just trust regexp-opt to do the right thing. The regexps it makes are pretty hard on the eye (and brain).

Upvotes: 1

Tyler
Tyler

Reputation: 10032

In the form:

(cons '("\\.rb$" . ruby-mode) ...

ruby-mode is part of a quoted list. That means it is read as a symbol name, not evaluated as a variable. In other words, Emacs sees it as the symbol ruby-mode and accepts it as is.

In the form:

(set-mode-for-filename-patterns 
   ruby-mode
   '("\\.rb$"
     "\\.rsel$"
     ...

ruby-mode is not quoted, and so is read as the argument to a function. Function arguments are evaluated. Which means Emacs reads ruby-mode, recognizes it as a symbol, and tries to evaluate it. Evaluating a symbol means looking for the value that it points to, which in this case doesn't exist.

EDIT:

Your function still doesn't work, there's another problem. You've used a quoted list inside set-mode-for-filename-patterns. This works fine in your original code:

(setq auto-mode-alist (cons '("\\.rb$" . ruby-mode) auto-mode-alist))

as you are in effect manually supplying the value for filename-pattern and mode. Inside your function, you need these symbols to be evaluated, which doesn't happen when they're quoted! The result is that instead of adding each different string from your list to auto-mode-alist, you get the symbol filename-pattern instead.

To fix this, you need to recognize that the '(filename-pattern . mode) is meant to produce a cons cell with the values of filename-pattern and mode. Which we can produce with (cons filename-pattern mode). So the corrected function would be:

(defun set-mode-for-filename-patterns (mode filename-pattern-list)
  (mapcar
    (lambda (filename-pattern)
      (setq 
        auto-mode-alist 
        (cons (cons filename-pattern mode) auto-mode-alist)))
    filename-pattern-list))

And the corrected function call is:

(set-mode-for-filename-patterns 
  'ruby-mode
  '("\\.rb$"
    "\\.rsel$"
    "\\.rhtml$"
    "\\.erb$" 
    "\\.prawn$"
    "Rakefile$"
    "Gemfile$"))

Upvotes: 4

Daimrod
Daimrod

Reputation: 5030

Look here:

(setq auto-mode-alist (cons '("\\.rb$" . ruby-mode) auto-mode-alist))
----------------------------^

This is a quote which means you prevent the evaluation of the next form, thus '("\\.rb$" . ruby-mode) is equivalent to (cons '"hello" 'ruby-mode).

But when you call the function set-mode-for-filename-patterns the arguments are first evaluate then their result is passed to the function. That's why evaluation (set-mode-for-filename-patterns ruby-mode ..) raise an error, because the emacs-lisp interpreter tries to evaluate ruby-mode as a variable, but ruby-mode has no value in this context hence the error (void-variable ruby-mode). What you want here, is to pass the symbol ruby-mode so you have to quote it like this (set-mode-for-filename-patterns 'ruby-mode ...)

Note that you could have set a value to ruby-mode mode with let.

(let ((ruby-mode 'ruby-mode))
  (set-mode-for-filename-patterns ruby-mode ...))

Here when the argument the form (set-...) is evaluated it evaluates ruby-mode and can find a value for it (which is the symbol ruby-mode) and then pass it to the function.

Upvotes: 1

Related Questions