The magic of defadvice

May 16, 2014

I recently did some house-cleaning on my emacs configuration. What used to be a huge mess of git submodules and manually added files is now very neatly managed with the help of Cask and Pallet. But that’s not what this post is about. While cleaning up all the emacs packages that I’ve accumulated over the past three years, I realized that I’d installed Helm with the express intention of fiddling around with it, but had never actually gotten around to doing so.

At work I usually work on a codebase that has several tens of thousands of files in it. In order to navigate them easily, I have a cscope.files generated in the root of my workspace that lists all the files in the codebase that are of interest to me. I also have an elisp function that uses ido-completing-read to help me filter through those files very easily. However, the completion menu appears in the minibuffer, which restricts visibility to about five files at a time. I also wasn’t very impressed with the sluggish performance of ido when it came to filtering through that whole list by pattern matching. So I started playing around with porting that function to use helm-for-files instead, and that’s when I came across helm-cmd-t.

At face value, the plugin was perfect for my usecase. The plugin already has a list that it uses for determing the typo of repo and the list of files in that repo. It looks like this:

(defvar helm-cmd-t-repo-types
  `(("git"         ".git"           "cd %d && git --no-pager ls-files --full-name")
    ("hg"          ".hg"            "cd %d && hg manifest")
    ("bzr"         ".bzr"           "cd %d && bzr ls --versioned")
    ("dir-locals"  ".dir-locals.el" helm-cmd-t-get-find)
    (""            ""               helm-cmd-t-get-find)))

To test it out, I decided to run it on a git repo first. Upon running M-x helm-cmd-t, I expected to see a wonderous filterable list of all the files in my repo. However, instead of being greeted with said magical list, I was instead shown an ominous

The system cannot find the path specified

After stepping through the functions in helm-cmd-t.el, I figured out what was causing the issue.

  1. The root of my “repo” was being resolved to a path in my home directory, "C:/Users/aparulekar/repos/work".
  2. Emacs was helpfully shortening that path to "~/repos/work".
  3. The plugin was running a shell command cd ~/repos/work && git --no-pager ls-files --full-name.
  4. Windows was not happy about cd ~/repos/work since it has no idea what that ~ means.

A simple solution was to call expand-file-name on the repo’s root in helm-cmd-t.el, so I immediately raised an issue in the project’s github repo. There I explained the problem I was facing and the solution to it. Expecting a resolution to take several days (if ever, since not a lot of developers seem to care about/have time for supporting windows, e.g. Cask) I decided to hack a temporary workaround.

The first solution that came into mind was to provide a function to helm-cmd-t-repo-types instead of a command string. This, however, resulted in some very ugly-looking code:

(defun helm-cmd-t-insert-listing-windows (repo-root cmd)
  (shell-command (format-spec cmd (format-spec-make ?d (expand-file-name repo-root))) t))

;; Windows needs special handling
(when (eq system-type 'windows-nt)
  (setq helm-cmd-t-repo-types
    '(("git" ".git" (lambda (repo-root)
         (helm-cmd-t-insert-listing-windows repo-root
            "cd %d && git --no-pager ls-files --full-name")))
      ("hg" ".hg" (lambda (repo-root)
         (helm-cmd-t-insert-listing-windows repo-root
            "cd %d && hg manifest")))
      ("bzr" ".bzr" (lambda (repo-root)
         (helm-cmd-t-insert-listing-windows repo-root
            "cd %d && bzr ls --versioned")))
      ("dir-locals" ".dir-locals.el" helm-cmd-t-get-find)
      ("" "" helm-cmd-t-get-find)
    )))

This code worked perfectly, but it was rather ugly looking. Thankfully I then remembered reading about being able to enhance the functionality of elisp functions without completely redefining them. A bit of searching led me to Advising Functions, resulting in the beautiful snippet below:

(defadvice helm-cmd-t-root-data (after adv-expand-filename)
  "expand the repo-root returned"
  (setq ad-return-value
    `(,(car ad-return-value) . ,(expand-file-name (cdr ad-return-value)))))

(ad-activate 'helm-cmd-t-root-data)

This, boys and girls, is the beauty of elisp.

And for implementing my original plan of using helm-cmd-t with my generated cscope.files, I had to add an entry to helm-cmd-t-repo-types. That was just a simple matter of cd %d && cat cscope.files. Of course, I still had to put in some special logic for handling my cscope.files on windows since it does not come with a cat binary. Instead, Windows has a command called type which essentially does the same thing.

(if (eq system-type 'windows-nt)
    (add-to-list 'helm-cmd-t-repo-types '("cscope" "cscope.files" "cd %d && type cscope.files"))
  (add-to-list 'helm-cmd-t-repo-types '("cscope" "cscope.files" "cd %d && cat cscope.files")))

(Note, the developers of helm-cmd-t were actually very helpful. Within an hour of my posting the issue, one of the devs asked for some clarification. Five minutes after my providing an explanation, a fix was committed, making my elegant solution elegantly useless. But I did get to learn something very useful, so there’s that.)