Saturday, October 27, 2012

When using Emacs I’m often switching buffers or opening files, so I’m always on the lookout for ways to make those operations more pleasant.

A while ago I tried Anything.el. Anything is like Quicksilver for Emacs. It’s powerful but I found it confusing to set up. Emacs-Fu had a blog post about configuring anything.el, and I used that for my setup.

For the most part, I use it instead of find-file:

screenshot of helm-for-files

When I saw that Helm was the successor to Anything, I decided to switch to Helm and simplify my setup. I was pleasantly surprised by how easy Helm was to set up compared to Anything. Here’s my setup, binding Alt+T (inspired by Textmate) to invoke helm-for-files:

(require 'helm-files)
(setq helm-idle-delay 0.1)
(setq helm-input-idle-delay 0.1)
(setq helm-locate-command "locate-with-mdfind %.0s %s")
(loop for ext in '("\\.swf$" "\\.elc$" "\\.pyc$")
      do (add-to-list 'helm-boring-file-regexp-list ext))
(define-key global-map [(alt t)] 'helm-for-files))

This uses the default helm-for-files setup, which lists, in order:

  1. ffap (find file at point)
  2. buffers (names of all open buffers)
  3. recentf (recently opened files)
  4. bookmarks (which I don’t use)
  5. files-in-current-dir (like find-file)
  6. locate (files in other directories)

However, I don’t want locate for all files on my disk; I want my home directory, minus a few subdirectories that contain files I never open. For this, I use a shell script to filter the results. On a Mac, use mdfind -name instead of locate; it’s updated in real time instead of nightly. I use a more complicated script that also handles patterns like foo/bar.txt:

# Note: updated 2014-03-28 to handle patterns like foo/bar.txt
suffix=$(basename "$@")

if [ "$@" = "$suffix" ]; then
    # If there's no folder then it should be a substring

mdfind -onlyin $HOME "kMDItemFSName = '$pattern'cd" \
    | fgrep -i "$@" \
    | grep -v "$HOME/Library/" \
    | grep -v "$HOME/Pictures/" \
    | grep -v "$HOME/Music/" \
    | sed -e "s|^$HOME|~|"

(Note: if you want to use mdfind to search contents and not only filenames, take a look at helm-source-mac-spotlight.)

More thoughts:

  • Ideally, I don’t want to have to think about whether I’ve already opened a file or not. In practice I do treat switching buffers and opening files differently. I use Alt+J and Alt+K for switching buffers in the same directory (see this blog post). I sometimes use ido-find-file (bound to Alt+O) for opening a file in the same directory, but I’m increasingly using helm-for-files even in the same directory.
  • Since mdfind is so fast with an SSD, I reduced the initial delay from 300ms to 100ms. That made it feel much better. You may need to experiment with the delay setting.
  • The same file can be in the buffers list, the recent files list, the current directory list, and the locate list. I’d like to see each file only once. I’m sure helm can filter these out but I don’t know how.
  • It'd be nice to not have it print out the full path every time, but I haven't figured out how to do that.

There are so many amazing things Helm can do; I’m only using it for this one feature, and it’s been great.

Update: [2014-03-28] I updated the "locate" script I use to allow finding patterns like foo/bar.txt. Normally, mdfind won't find these because it only sees bar.txt.

Update: [2014-05-27] Also see LocateFilesAnywhere on EmacsWiki if you're looking for Emacs packages that let you find files from anywhere, not only the current directory. I'm no longer using the above setup.


11 comments: wrote at Monday, October 29, 2012 at 2:28:00 PM PDT

thanks for the recommendation! helm definitely looks like a worthy successor to ido, anything, and others. i'm now using it for finding files/switching buffers, like you, and for minibuffer history isearch. my only tweak beyond your settings was to add helm-c-source-find-files to helm-for-files-preferred-list so i could type (partial) paths occasionally, since the locate source doesn't seem to handle those well.

looking forward to diving in more!

Jeremy Ong wrote at Friday, February 1, 2013 at 11:16:00 PM PST

I've been using ido for a long time but I'll give this a shot

gogleyedboggle wrote at Friday, February 8, 2013 at 7:12:00 AM PST

Dont understand how you integrate your bash script with helm - where doyou put the bash script and how do you refer to it ?

Amit wrote at Friday, February 8, 2013 at 1:53:00 PM PST

Hi gogleyedboggle,

You can put the bash script anywhere but you need to set helm-c-locate-command to point to it.

n wrote at Saturday, March 16, 2013 at 2:43:00 PM PDT

How did you know about mdfind? Is there anywhere I can learn about these mac specific unix commands?

Amit wrote at Saturday, March 16, 2013 at 3:53:00 PM PDT

n: good question; I don't remember! I was probably browsing some Mac helpful hints site when I found it. I also occasionally look in /usr/bin to see what's there that I'm not familiar with.

PuercoPop wrote at Thursday, March 6, 2014 at 12:17:00 PM PST

You don't need the bash script at all you can configure Emacs to use mfind (spotlight) as its locate command:
(setq locate-command "mdfind")

You can

Amit wrote at Thursday, March 6, 2014 at 12:24:00 PM PST

Hi PuercoPop, I use the bash script because I want to modify the results of mdfind. You can see in my script, I filter out certain folders.

exuberance wrote at Saturday, June 11, 2016 at 7:11:00 PM PDT

Thanks for the posting.

Here's a varation on your script that let's one search for N terms in the complete path. It still requires that the first term appear in the basename of the file.

for bit in "$@" ; do
grep_query="$grep_query | grep '$bit'"
echo $grep_query >> /tmp/foo1
mdfind -onlyin $HOME "$mdfind_query" \
| eval "$grep_query" \
| grep -v "$HOME/Library/" \
| grep -v "$HOME/Pictures/" \
| grep -v "$HOME/Music/" \
| sed -e "s|^$HOME|~|"

Amit wrote at Sunday, June 12, 2016 at 4:24:00 PM PDT

Thanks exuberance! That looks useful.

Kari Argillander wrote at Sunday, November 27, 2016 at 3:43:00 PM PST

Thanks. I do also this
grep_query="$grep_query | grep -i '$bit'"
It will be faster to type when theres no case sensitive grep