Why Emacs is great (Part 1..of probably many)

When it comes to Emacs I had a few false starts… but now I'm a zealot. I love having full control over my development environment, and this is a great example of that control in action.

One of the things I really like about Clojure development in Emacs is Bozhidar's Batsov's excellent Emacs package Cider. It provides just about everything you need for interactive REPL driven development with Clojure.

I specifically make heavy use of cider-test-run-test which is bound to C-t C-t t for me. That function runs the test where I currently have my cursor. In addition, Cider allows you to re-run the last test you ran from anywhere. That's pretty convenient for when a test fails, since you're often not in the test file itself when you put in a fix. You can make your change, evaluate it and then immediately re-run the that test caused you to go make changes in the first place.

Typically I'll bounce back and forth between the code I'm writing and the tests that relate to it, iteratively evaluating the changes as I go and running the appropriate tests. It's how I like to work and working in a language where I have to restart the system to see changes is just painful anymore.

SLIME doesn't seem to have that or at least I haven't found it yet. I didn't look too hard… this sounded like a fun little project and a way to get a little better at extending Emacs.

In Common Lisp projects I use Rove to run my tests and the simplest way to do that is to just put a call to Rove at the bottom of each test package, for example:

(defpackage test-lisp-2/tests/main
  (:use :cl
	:test-lisp-2
	:rove))
(in-package :test-lisp-2/tests/main)

(deftest test-target-1
  (testing "should (= 1 1) to be true"
    (ok (= 1 1))))

(run-suite *package*)

Whenever this file is loaded (even when I (ql:quickload)) the system the tests in this package will be run as a result of that last line. That doesn't actually fit my workflow. I don't want every test in the system to run when I'm just starting things up. I want to load the test system, then iterate on a single test for the most part, and run the full suite only on demand.

After playing around a bit and looking at the source for Rove, I found that, in order to run a single test, I needed to issue a command in the REPL that looked like this:

(rove:run-test 'package-name::test-name)

That should be easy enough to accomplish, I'm just going to need the current package name and the name of the test my cursor is in. I'm also probably going to want to make sure I'm actually in a deftest form.

My first stop was to dig around in the source for Slime and see what it had. Getting the current package is easy enough. Slime has the following function:

(defun slime-current-package ()
  "Return the Common Lisp package in the current context.
If `slime-buffer-package' has a value then return that, otherwise
search for and read an `in-package' form."
  (or slime-buffer-package
      (save-restriction
	(widen)
	(slime-find-buffer-package))))

How did I find this code? The source for all of your lisp packages are available to you within Emacs. I didn't have to Google. I just called M-x find-library, typed in slime, and there was the source.

Searching for current took me to this function pretty quickly.

With (slime-current-package) in hand I can start writing my lisp-test-at-point function:

(defun lisp-test-at-point ()
  (interactive)
  (let ((pkg (slime-current-package)))
    (message (concat "Package: " pkg))))

Not much to look at yet, but I can jump into various Common Lisp files and call my function: M-x lisp-test-at-point. First step complete, I can get the package name and I can iterate on my new Emacs function until I have the behavior I want.

Now for the name of the test.

A test in Rove looks like so:

(deftest test-target-1
  (testing "should (= 1 1) to be true"
    (ok (= 1 1))))

Since we're working with a list this deftest is a form like any other; it's a list. If I can grab the top level list where my cursor is, I'll be able to get the information I need just from those first two list elements: deftest and test-target-1.

Back to slime.el to do some digging, it turns out that (slime-defun-at-point) works on the form deftest as well as defun, and with a little more code we have our test name:

(defun lisp-test-at-point ()
  (interactive)
  (let* ((pkg (slime-current-package))
       (form (read-from-string (slime-defun-at-point)))
       (def-symbol  (caar form))
       (test-name   (cadar form)))
    (message (concat "test: "  (symbol-name test-name)))))

A little explanation here:

Like before, just to check my work I spit out the name of the test into the mini buffer to make sure I'm getting what I expect.

Now I know the package, and I have the entire form of the expression that defines this test. All I have to do is put it all together with a few extra bits:

(defun build-rove-single-test (pkg test-name)
  (let ((pkg-name (substring pkg 1)))
    (car (read-from-string (format "(rove:run-test '%s::%s)"
				 pkg-name
				 (symbol-name test-name))))))

(defun lisp-test-at-point ()
  (interactive)
  (let* ((pkg (slime-current-package))
       (form (read-from-string (slime-defun-at-point)))
       (def-symbol  (caar form))
       (test-name   (cadar form)))
    (if (eq 'deftest def-symbol)
      (progn
	(message (concat "Running test: " (symbol-name test-name)))
	(slime-eval (build-rove-single-test pkg test-name)))
      (message "Not in test."))))

This should all be fairly self explanatory at this point, but a one note:

The package that comes back from (slime-current-package) looks like this:

":package-name"

So there's some string manipulation to get it into the form Rove wants which warranted a helper function.

Slime already has a way to send expressions to the current REPL: slime-eval so that was easy once I had things in the correct format.

It's difficult to overstate the power of Emacs… I had a need and I was able to build it into the editor with very little trouble (I will admit to needing to restart Emacs a few times). In the same way that I iterate on the code I'm working on, I'm iterating on my editor as I go, tuning it exactly to my desired workflow.