Friday, December 18, 2009

Emacs: Using Bookmarked Directories

Emacs: Using Bookmarked Directories


You can use bookmarks to mark positions in files by name and jump back to them later.  I don't use file bookmarks often, but if you set a bookmark while you're in a dired buffer it saves the directory location.  I work on projects with thousands of files in hundreds of directories so this is extremely useful to me.  I set bookmarks in the dozen or so directories I use all the time, and a few others in strategic directories that I can start from and navigate down.

This post isn't about how to use bookmarks in general, you can find that elsewhere.  You set a bookmark using "C-x r m", and list your bookmarks using "C-x r l".  In the bookmark list you can rename, delete, etc. the bookmarks.

In Dired

Now that we have Ido acting a bit more sensibly, let's use it to choose a bookmark in dired and switch to that directory (I am assuming here you already have Ido enabled).  I like things in most recently used (MRU) order, so when we pick a bookmark we'll also move it to the top of the bookmark list:

(require 'bookmark)

(defun my-ido-bookmark-jump ()
  "Jump to bookmark using ido"
  (interactive)
  (let ((dir (my-ido-get-bookmark-dir)))
    (when dir
      (find-alternate-file dir))))

(defun my-ido-get-bookmark-dir ()
  "Get the directory of a bookmark."
  (let* ((name (ido-completing-read "Use dir of bookmark: " (bookmark-all-names) nil t))
         (bmk (bookmark-get-bookmark name)))
    (when bmk
      (setq bookmark-alist (delete bmk bookmark-alist))
      (push bmk bookmark-alist)
      (let ((filename (bookmark-get-filename bmk)))
        (if (file-directory-p filename)
            filename
          (file-name-directory filename))))))

(defun my-ido-dired-mode-hook ()
  (define-key dired-mode-map "$" 'my-ido-bookmark-jump))

(add-hook 'dired-mode-hook 'my-ido-dired-mode-hook)

Now in dired you press '$' to choose a bookmarked directory to switch to.  I stole '$' away from dired-hide-subdir, which I never use, because it's a mnemonic device as I'll explain later.

In Ido

That's good for dired, but it would be nice to be able to use bookmarks to switch directories when you are doing a regular ido-find-file with "C-x C-f":

(defun my-ido-use-bookmark-dir ()
  "Get directory of bookmark"
  (interactive)
  (let* ((enable-recursive-minibuffers t)
         (dir (my-ido-get-bookmark-dir)))
    (when dir
      (ido-set-current-directory dir)
      (setq ido-exit 'refresh)
      (exit-minibuffer))))

(define-key ido-file-dir-completion-map (kbd "$") 'my-ido-use-bookmark-dir)

Now when you are opening a file, you can type '$', choose a bookmark, then ido will restart the find-file from that location.

It has a quirk that after ido gets reseated in the new directory you can't navigate up, only down.  I get around this by hitting "C-e", editing the path, then "RET" to go back to Ido.  It's somewhat annoying, but it's only a couple keystrokes, and I don't do it often enough to motivate me to try to fix it.

In Your Shell


Now that you're jumping around the file system in Emacs, you'll start to miss it in your terminal.  Let's have Emacs write the bookmarks out to a file as shell variables:

(setq bookmark-save-flag 1)
(setq bookmark-sort-flag nil)

(defadvice bookmark-write-file (after my-bookmark-to-shell activate)
  "Convert bookmarks to format bash and tcsh (yuck!) can use."
  (let (filename)
    (with-temp-buffer
      (dolist (bmk bookmark-alist)
        (if (listp (caadr bmk))
            (setq filename (cdr (assoc 'filename (cadr bmk))))
          (setq filename (cdr (assoc 'filename (cdr bmk)))))
        (unless (file-directory-p filename)
          (setq filename (file-name-directory filename)))
        (insert (car bmk) "=" filename)
        (delete-char -1)
        (newline))
      (write-file "~/.bashrc_bmk")
      (goto-char (point-min))
      (while (not (eobp))
        (beginning-of-line)
        (insert "set ")
        (forward-line))
      (write-file "~/.cshrc_bmk"))))

I have it writing out in both bash and csh compatible formats because I have to use tcsh at work (SIGH).  Now in your .bashrc put:

bmk_file=~/.bashrc_bmk
if [ -f $bmk_file ]; then
  . $bmk_file
fi
alias bmk_reload='. $bmk_file'
alias bmk_list="sort $bmk_file | awk 'BEGIN { FS = "'"[ =]" }; { printf("%-25s%s\n", $1, $2) }'"'"

or in your .cshrc:

set bmk_file=~/.cshrc_bmk
if ( -f $bmk_file ) source $bmk_file
alias bmk_reload "source $bmk_file"
alias bmk_list "sort $bmk_file | awk 'BEGIN { FS = "'"[ =]" }; { printf("%-25s%s\n", $2, $3) }'"'"

Now if you have a bookmark named "proj_a_inc" you can do things like "cd $proj_a_inc" or "cp $proj_a_inc/foo.h ."  This is why I chose "$" above to easily remember my bookmark key (and it usually isn't in a filename).  The bmk_reload command reloads your bookmarks in case you add one in Emacs, and bmk_list lists your bookmarks and the directories they point to.

4 comments:

Anonymous said...

I'm very excited about your blog! I discovered it through Planet Emacsen and the content so far has been very interesting. Please keep it coming.

Mathias Dahl said...

Nice!

I made a small hack some time ago to make it possible to cd to a bookmarked directory in Eshell:

http://www.emacswiki.org/emacs/EshellBmk

Trombone said...

Thank you. Very good post and very useful.

I have a question. I am using NT emacs with cygwin bash shell. The bookmark does not work in the bash shell.

After some debugging I found that I have to add "#export path" for every line in the .bashrc_bmk file. Would any body tell me why?

I have udpate the function a little to add "export path"

(defadvice bookmark-write-file (after my-bookmark-to-shell activate)
"Convert bookmarks to format bash and tcsh (yuck!) can use."
(let (filename)
(with-temp-buffer
(dolist (bmk bookmark-alist)
(if (listp (caadr bmk))
(setq filename (cdr (assoc 'filename (cadr bmk))))
(setq filename (cdr (assoc 'filename (cdr bmk)))))
(unless (file-directory-p filename)
(setq filename (file-name-directory filename)))
(insert (car bmk) "=" filename)
;;; I add following line
(insert "#export " (car bmk))
(delete-char -1)
(newline))
(write-file "~/.bashrc_bmk")

Anonymous said...

The correct way to add a hook to the ido is during ido-setup. If you
find that your binding keeps getting wiped out then the following might help.

(add-hook 'ido-setup-hook 'ido-my-keys)
(defun ido-my-keys ()
"Add my keybindings for ido."
(define-key ido-file-dir-completion-map (kbd "$") 'my-ido-use-bookmark-dir))