Sunday, June 19, 2011

Confirm to quit when editing files from a project in Emacs

You have many files loaded in Emacs. You are typing at warp speed. All of a sudden, you attempt to save, but accidentally press the sequence of keys that exits Emacs. You realize this but it's too late. The Emacs window disappears.

To get around this problem, I wrote a function that tries to determine if I am working on a software project. If so, a yes or no confirmation will be issued prior to exiting. If not, no confirmation will be issued prior to exiting. All my projects are located in a parent directory called projects. Hence, it is simple enough to search for the string "projects" in the buffer list. If the string is found, introduce a yes or no confirmation. Add the following code to your .emacs file.

(defun have-projects-buffer ()
  "Search for buffers that come from the projects directory and return t if found else nil."
  (interactive)
  (defvar list (buffer-list))
  (defvar found nil)
  (while list
    (defvar element (car list))                      
    (when element
      (defvar file-name (buffer-file-name element))
      (if file-name
          (progn
            (if (string-match "projects" file-name)
                (setq found t)))))
    (setq list (cdr list)))
  found)

(defun my-quit-hook ()
  "Ask if the user really wants to quit Emacs."
  (interactive)
  (if (have-projects-buffer)
      (y-or-n-p "Really quit Emacs? ")
    t))

(add-hook 'kill-emacs-query-functions 'my-quit-hook)

Thanks Jürgen for the suggestions. Here is a cleaner version.

(defun have-projects-buffer ()
  (delq nil (mapcar (lambda (buf1)
                      (string-match "projects" buf1))
                    (delq nil (mapcar (lambda (buf2)
                                        (buffer-file-name buf2))
                                      (buffer-list))))))


Raimon Grau at puntoblogspot has some different approaches to solving this problem.

Cleaner versions with and without using Common Lisp.

(require 'cl)
(defun project-buffers-p ()
  (some (lambda (buf)
          (let ((file (buffer-file-name buf)))
            (when file
              (string-match "projects" file))))
        (buffer-list)))

(defun project-buffers-p ()
  (not (null (delq nil (mapcar (lambda (buf)
          (let ((file (buffer-file-name buf)))
            (when file
              (string-match "projects" file))))
        (buffer-list))))))

Shorter version suggested at blog.tapoueh.org.

(require 'cl)
(defun project-buffers-p ()
  (loop for b being the buffers
        when (string-match "projects" (or (buffer-file-name b) ""))
        return t))

8 comments:

Jürgen said...

Looks quite imperative. How about this idiomatic elisp:

(defun projects-buffers-p ()
(some (apply-partially 'string-match "projects") (mapcar 'buffer-name (buffer-list))))

tsengf said...

Thanks Jürgen for the optimization. It's a big step in the right direction. I think you want to use buffer-file-name because we want to match a string in the file path. The other problem is that the special Emacs buffers, eg. *Messages*, do not have file names associated with them. Thus, buffer-file-name returns nil for them. String-match on a nil breaks.

Jürgen said...

doing extra checking if the buffer is visiting a file:

(some (lambda (b) (when-let (f (buffer-file-name b)) (string-match "projects" f))) (buffer-list))

tsengf said...

When I try this, I get "Symbol's function definition is void: f" upon exit. Am I missing something? I've added an update to the original post. It feels like it can still be improved.

Raimon Grau said...

A couple of different approaches to avoid accidental shutdowns.

Have fun!

tsengf said...

Thanks Raimon Grau for the new ideas.

jstolle said...

Why not just unassign the key mapping for exiting?

tsengf said...

jstolle, unassigning the key mapping for exit is a solution. I guess the difference is the amount of effort needed to exit Emacs.