December 10, 2011

Neat Emacs Trick #1

Avdi Grimm has a post on using a shortcut ec script for emacsclient here. I used something like that for a few months, but got quickly frustrated with one peculiar quirk of my development setup.

I do the vast majority of my development in ruby, in a terminal window, and almost all of it is TDD using minitest/unit. Now, when I get a test failure, it looks something like this:

$ ruby -I. test_foo.rb 
Run options: --seed 41149

# Running tests:


Finished tests in 0.177699s, 5.6275 tests/s, 5.6275 assertions/s.

  1) Failure:
test_frobnication(TestFoo) [test_foo.rb:7]:
Expected: 23
  Actual: 42

1 tests, 1 assertions, 1 failures, 0 errors, 0 skips

The bit I’m most interested in is this line:

test_frobnication(TestFoo) [test_foo.rb:7]:

Specifically, if I want to take a look at the code around this failing assertion, this is the bit of information I want:


Now, I don’t know if it’s a quirk of my terminal or not, but when I double-click the filename, it selects the string test_foo.rb:7. The same sort of thing happens with exception backtraces, too. The workflow I want is: double-click the filename to copy, type “e c <space>”, middle-click to paste, hit enter. However, that pesky :7 screws everything up. It’s not part of the filename, so I’ve got to edit it out of the command before telling emacsclient to do its thing. This breaks the flow, because I actually have to think in the middle of the sequence.

Doing that also throws away useful information. What if, instead of just opening the file, I could have emacs navigate to the line as well? That would save me two manual operations and save me a little bit of mental effort.

Fortunately, emacsclient doesn’t just open files, it can also remotely run arbitrary elisp expressions. By a bit of trial and error, I found this set of calls would make emacs do what I want, given a separated filename filename and line number linenum:

(let ((buf (find-file filename)))
  (goto-line linenum buf)
  (select-frame-set-input-focus (window-frame (get-buffer-window buf))))

The only question then is, how best to pass this to emacs? Given that I was learning Haskell when I first tackled this, I thought it would be a suitable challenge for my new-found monad-wrangling skills for my first attempt to be called ec.hs:

#!/usr/bin/env runhaskell

import System.Environment (getArgs)
import System.Cmd (rawSystem)

splitIdentifier :: String -> (String, String)
splitIdentifier str =
        cbrk = break (== ':')
        (filename, rest) = cbrk str
        linenum = case rest of
                    ':':stmt -> fst $ cbrk stmt
                    otherwise -> "1"
      (filename, linenum)

buildCommand :: (String, String) -> (String, [String])
buildCommand (filename, linenum) =
    ("emacsclient", ["-ne", lisp])
      lisp = "(let ((buf (find-file \"" ++ filename ++ "\")))" ++
             "(goto-line " ++ linenum ++ " buf)" ++
             "(select-frame-set-input-focus (window-frame (get-buffer-window buf))))"

run (exe, args) = rawSystem exe args

main = do
     identifier:_ <- getArgs
     exitCode <- run $ buildCommand $ splitIdentifier identifier
     putStrLn ""

To say that I am unimpressed with the verbosity of this code would be an understatement. I’d be happy for this to be golfed into oblivion. Nevertheless, it works.

Here’s a ruby version of the same:

#!/usr/bin/env ruby

filename, linenum = *ARGV.shift.split(":")
fail "Filename required" unless filename
linenum ||= 1

system "emacsclient", "-ne", <<-LISP
(let ((buf (find-file "#{filename}")))
  (goto-line #{linenum} buf)
  (select-frame-set-input-focus (window-frame (get-buffer-window buf))))

Naturally there’s a problem with both of these: filenames can contain colon characters, and both of these scripts will break if they’re given filenames like that. I’m happy enough with that; I don’t think I’ve ever seen a colon in a directory name, and I’ve certainly never written a ruby file with a colon in its name either.

There is, of course, another way to achieve all this, and that’s to make emacs do as much of the work as possible. Stick these in your init.el:

(defun backtrace-find-file (filename linenum)
  (let ((buf (find-file filename)))
    (goto-line linenum buf)
      (window-frame (get-buffer-window buf)))))

(defun backtrace-visit (ref)
  (let* ((matches (split-string ref ":"))
	 (filename (car matches))
	 (linenum (string-to-number (or (cadr matches) "1"))))
    (backtrace-find-file filename linenum)))

Now all we have to do is have emacsclient call backtrace-visit with the correct argument:


emacsclient -ne "(backtrace-visit \"${1}\")"

This is probably the simplest thing that could possibly work.