As a long-time passionate Emacs user, I've been curious about Lisp in general and Emacs Lisp in particular for quite some time. Until recently I had not written any Lisp apart from my .emacs.d setup, despite having read both An introduction to programming in Emacs Lisp and The Little Schemer last summer. A year later, I have finally written some Lisp, and I thought I'd share the code as an introduction to others out there curious about Lisp and extending Emacs.
A word of warning: The code ahead is written by an absolute beginner in Lisp, and may violate any number of best-practices and idioms, but it is somewhat working code. If you spot something that's wrong or something you just don't like, please tell me what and why so I can improve.
This article is intended to help you get started with 1) Lisp, and 2) extending Emacs using Emacs Lisp (elisp). My hope is that it will help you go from being unable to even read Lisp to have a basic knowledge of how to use elisp to extend Emacs. If you are curious about the code we will develop, check out the full code-listing at the end of this article.
The task I set out to solve was to make Emacs slightly more intelligent when working with tests written in Buster.JS, which is a test framework for JavaScript I'm working on with August Lilleaas. In particular I wanted Emacs to help me with Buster's concept of deferred tests. Given a test like this:
buster.testCase("Some object", {
"should do something nice": function () {
// ...
}
});
You can "comment out" the name to defer its execution:
buster.testCase("Some object", {
"//should do something nice": function () {
// ...
}
});
When a test is deferred, it will still appear in the test report so you don't forget about it, but it will not run (perhaps because you want to isolate some other test, don't know how to pass it yet, or whatever).
Using simple key-bindings, I want Emacs to help me:
Turns out, this isn't particularly hard, and it will introduce us to some core Emacs Lisp concepts. In this article, we will cover some navigational issues and toggling the deferred state. In a later article, I will cover the remaining two issues on the list.
Like so much else in Emacs, I am going to base my extension on regular expression searches. The code that follows has obvious defeciencies, as I learn more I intend make it clever-er, but for now, this will do. The core of the solution is this expression:
(defvar buster-test-regexp
"^\s+\"\.+\s\.+\":\s?fun"
"Regular expression that finds the beginning of a test function")
The regular expression targets a quoted string (double quotes only) that contains at least one space, and that is followed by a colon and the word "fun". Hopefully this will be specific enough to target a line where a test starts, given that Buster does not mandate the test start with "test" or include any other special words.
The expression above creates a documented global variable
buster-test-regexp. It does so using a list that is also a function
call. Lists are important in Lisp. After all, it does take its name from
"List processor". The expression is processed as follows:
(function arg1 arg2 ...)
The list is space-separated. The first symbol is the function name, and the following ones are arguments. Emacs Lisp comes with a bunch of built-in functions, and you can look them up by pressing C-h f <function-name>.
If you look up defvar you will find that it defines the
variable named by the first argument with the optional initial value
provided by the second argument. Finally there is the optional
documentation string provided by the last argument. You can evaluate
this particular expression by placing the cursor right after the closing
parenthesis and hitting C-x C-e
(eval-last-sexp). The documentation string can be looked up
with C-h v buster-test-regexp <Enter>.
In order to toggle the deferred switch for tests, we need to know where a test starts. We can break this task up into several sub-tasks. The simplest case is to locate the exact point where the test begins, assuming it begins somewhere on the current line:
(defun buster-test-name-pos ()
"Return the position where the test name starts on the current line"
(save-excursion
(beginning-of-line)
(search-forward "\"" (point-at-eol))
(1- (point))))
This example exposes a very central concept in elisp along with a few new functions. Let's tackle the concept of "point" first.
Imagine a buffer (in Emacs, a buffer is a space of editable text, usually the contents of a file) as a string of characters on a single line. This string would be indexed from 1 (the very first character) to N (the very last character). Line breaks count as a single character. Point is the position of the cursor in this string of characters.
Assume a file with 3 lines, each with 10 characters:
0123456789
0123456789
0123456789
If you place the cursor after the last character on the third line and
press M-: (point) <Enter>
(that's eval-expression with an expression that calls the
point function), you will see "33" displayed in the
minibuffer. Count yourself, starting from 1, including line-breaks
(i.e. 11 characters per line).
One of the things that surprised me the most when reading up on elisp is the fact that many functions actually have side-effects. The most common side-effect is moving point (that is, relocating the cursor).
You will often need to use functions that move point to look around the
buffer, but without physically moving the cursor. That's
where save-excursion comes in. This function works as a
transaction wrapper for point (and others), making sure they are reset
after the body is executed. Its signature is:
(save-excursion &rest BODY)
This means that you can pass any number of expressions
to save-excursion, and it will execute each one of them,
then restore point and return the value of the last expression.
Let us look at the three expressions passed
to save-excursion in the example above:
(save-excursion
(beginning-of-line)
(search-forward "\"" (point-at-eol))
(1- (point)))
The first expression is a call to the
function beginning-of-line, which moves point to the
beginning of the current line. The second expression is a call
to search-forward. Its two arguments are the string
"\"" (an escaped quote) and the result of calling
the function point-at-eol. point-at-eol
returns the position of point at the end of the line, but does not
actually move point. This second argument to search-forward
is a bound, i.e. we only want to find the first quote on the
current line.
The search-forward function moves point to just after the
match. The third expression takes advantage of this by returning the
position of point minus one - in other words, the position before
the leading quote.
To summarize: this piece of code returns the position of the first quote on the current line, without moving point. It was originally seen in this context:
(defun buster-test-name-pos ()
"Return the position where the test name starts on the current line"
(save-excursion
(beginning-of-line)
(search-forward "\"" (point-at-eol))
(1- (point))))
The defun function defines a function. Its name is
buster-test-name-pos. Note the leading "buster-", which it
shares with the regular expression from before. This is just a very
lo-fi way of namespacing our code. The function takes no arguments (thus
the empty list as the second argument), has a documentation string, and
one expression (the call to save-excursion, which itself
has three expressions) that makes up its body.
In summary, the buster-test-name-pos function assumes that
the current line holds the start of a test, and returns the position of
the leading quote.
If we have one function that assumes that the current line contains the start of a test, we will also need a function to verify that assumption:
(defun buster-beginning-of-test-curr-linep ()
"Return t if a test starts on the current line"
(save-excursion
(beginning-of-line)
(search-forward-regexp buster-test-regexp (point-at-eol) t)))
Note the trailing p in the name. It stands for predicate, and
is a common way of naming functions that return booleans in Emacs. This
function calls beginning-of-line and
search-forward-regexp, both of which move point, so the
body is yet again wrapped in a call to save-excursion.
The search-forward-regexp function works just
like search-forward, except with regular expressions. This
time we also pass it a third argument - t (true). If
you look up the function with C-h f search-forward-regexp,
you will find that this argument indicates that we don't want the
function to throw an error if no result is found. Instead, it will just
return nil, which works like false.
It is time to put our two functions to work and try them out
interactively in an Emacs buffer. Whenever you call an Emacs
Lisp function through a key binding (e.g. C-f,
forward-char) or by hitting
M-x func-name <Enter>, you are calling it
interactively, which is different from regular function calls. Functions
have to explicitly declare themselves interactive for this to work. If
you type M-x buster- and hit tab, you will note that none of
our two functions show up, because they are not interactive.
Let us make an interactive function that moves to the beginning of the test name if point is currently at a line where a test is declared:
(defun buster-goto-beginning-of-test ()
"Move point to the beginning of the current test function.
Does nothing if point is not currently on a line where a test is declared."
(interactive)
(if (buster-beginning-of-test-curr-linep)
(goto-char (buster-test-name-pos))))
This function is declared interactive by calling
the interactive function. It can also take some arguments
to control how it receives input, but we will leave that for later.
The function then calls the if special form (a
function implemented in C in elisp core) with two arguments: the test,
which is a call to buster-beginning-of-test-curr-linep, and the
expression to evaluate if the test returns t. The
expression is a call to goto-char with the position
returned by buster-test-name-pos as argument. There is no
call to save-excursion, because we actually mean to move
point this time.
Place the following code in your Emacs' scratch buffer:
(defvar buster-test-regexp
"^\s+\"\.+\s\.+\":\s?fun"
"Regular expression that finds the beginning of a test function")
(defun buster-test-name-pos ()
"Return the position where the test name starts on the current line"
(save-excursion
(beginning-of-line)
(search-forward "\"" (point-at-eol))
(1- (point))))
(defun buster-beginning-of-test-curr-linep ()
"Return t if a test starts on the current line"
(save-excursion
(beginning-of-line)
(search-forward-regexp buster-test-regexp (point-at-eol) t)))
(defun buster-goto-beginning-of-test ()
"Move point to the beginning of the current test function.
Does nothing if point is not currently on a line where a test is declared."
(interactive)
(if (buster-beginning-of-test-curr-linep)
(goto-char (buster-test-name-pos))))
;; buster.el key-binding
(global-set-key (kbd "C-c t") 'buster-goto-beginning-of-test)
Mark it all and hit M-x eval-region <Enter>. Then place the following into a buffer:
buster.testCase("Graph", {
setUp: function () {
this.graph = capillary.graph.create();
this.formatter = capillary.formatters.ascii.bindGraph(this.graph);
},
"should emit dot for commit": function () {
var commit = { seqId: 0, id: "1234567", message: "Ok" };
var listener = this.spy();
this.graph.on("graph:dot", listener);
this.graph.graphBranch(C.branch.create([commit]));
var args = listener.args[0];
assert.calledOnce(listener);
assert.calledWith(listener, [0, 0]);
}
});
Place your cursor anywhere on the line with the test name, hit C-c t and watch the cursor move to the beginning of the test name. Magic! If you try it on any other line, the function will do nothing and fail silently.
Being able to move to where the test name starts from within the same line is nice, but not very useful. We will improve it by recognizing which test currently "has focus" (i.e. point is inside it).
We don't have a parser at hand, and ideally we would like to process a minimal part of the buffer to determine what test we are in. That way our utility will stay fast and usable.
To figure out the start position of the current test, we will:
buster-test-regexp. Store the position
as start.
{
and }. (When the number of forward-brackets and
backward-brackets are the same, the function is closed). Store the
position as end.
start < point < end, then start is
the starting position of the current test. Otherwise, we are not
currently inside a test.
Implementing this algorithm will introduce use to a few things:
cond and progn functions
Local variables are defined with the let function. It takes
two arguments - a list of variables and their (optional) initial
value, and one or more "body" expressions. The variables are only
defined within the body. The following snippet initializes our three
variables. curr holds the current position (retrieved by
calling the point function), and start-pos
and end-pos are both set to 0 initially.
(let ((curr (point))
(start-pos 0)
(end-pos 0))
BODY...)
To find the start position, we search backwards for the closest match
for buster-test-regexp and call point. We
use setq assigns a new value to an existing variable.
(search-backward-regexp buster-test-regexp)
(setq start-pos (point))
Finding the end position is a little more work, we will defer it for now by delegating to another function. It involves counting brackets and moving forward in the buffer until we find the closing one.
When all the variables are set, we check if the current position is inside the test we found. If so we return the start position. Otherwise, we return the current position.
(defun buster-beginning-of-test-pos ()
"Return the start position of the current test"
(let ((curr (point))
(start-pos 0)
(end-pos 0))
(save-excursion
(search-backward-regexp buster-test-regexp)
(setq start-pos (point))
(setq end-pos (buster-goto-eoblock))
(if (and (< start-pos curr)
(< curr end-pos))
start-pos curr))))
The function above calls buster-goto-eoblock,
which moves point to the closing bracket for the current
block. Let's take a stab that function now. The algorithm we
will implement is once again very manual and straight-forward
(and I'm sure smarter people than me has better ways to do
this).
(point)Iterative problems like this can be solved fairly elegantly using recursion. We will make the function accept an optional argument which provides the parens counter. If it is not set, we initialize it according to the description above.
(defun buster-goto-eoblock (&optional open-paren-pairs-count)
"Move point to the end of the next block"
(if (null open-paren-pairs-count)
(progn
(search-forward "{")
(setq open-paren-pairs-count 1))))
Every argument that follows the &optional keyword is
optional. This piece of code introduces the progn
function. To understand it, consider the if special form's
signature, seen below.
(if COND THEN ELSE...)
if can take multiple "else" expressions, but only one
"then" expression. We can work around this by using
the progn function, which simply evaluates all its operands
and returns the result of the last one. progn works pretty
much exactly like JavaScript's comma operator.
To complete the buster-goto-eoblock function, we need a way to
find the position of the next "{" or "}". We will use a small helper to
search forward to a character and return its position.
(defun buster-find-next-pos (char)
"Return the position at the next occurrence of `char`"
(save-excursion
(if (not (search-forward char nil t)) (end-of-buffer))
(point)))
Once again we make use of the save-excursion function to
search forward without actually moving point. Note that if we can't find
a character, we move to the end of the buffer. This isn't entirely
ideal, but it avoids leaving point in the same position, possibly
causing us to recursively look for the same characer in the same spot
indefinately. With the buster-find-next-pos function in place,
we can complete the rest of buster-goto-eoblock.
(cond
((eq 0 open-paren-pairs-count) (point))
(t ...))
The cond function takes multiple lists. It moves through
each list and evaluates the first expression in them until it finds one
that returns a non-nil value. When it does, it evaluates
the following expressions in that list and returns the value of the last
one. The first case in our cond expression is the case
where the number of brackets is 0, meaning that we are done, so we
return point.
If we are not done, we find the next "{" and "}" and move to the closest
one. This can be considered the default case, so our modifier is simply
the value t (true).
(cond
((eq 0 open-paren-pairs-count) (point))
(t (let ((open (buster-find-next-pos "{"))
(close (buster-find-next-pos "}"))))))
We use let once more to define local variables holding the
position of the two next brackets. Next up, we figure out which one is
closest using a new call to cond.
(let ((open (buster-find-next-pos "{"))
(close (buster-find-next-pos "}")))
(cond
((< open close)
(goto-char open)
(buster-goto-eoblock (1+ open-paren-pairs-count)))
((< close open)
(goto-char close)
(buster-goto-eoblock (1- open-paren-pairs-count)))))
After moving to the closest bracket, we
call buster-goto-eoblock recursively, passing in the adjusted
parens count - incremented if the "{" was closest, decremented if "}"
was closest. Note that in addition to passing the parens counter to the
recursive call, the function relies on point moving, meaning that it
cannot use save-restriction. We could have worked around
this by passing the position to go from too, but it seemed more
appropriate to move point (given my limited experience with built-in
Emacs Lisp functions).
Putting all the pieces together results in an 18 line Lisp function, the longest one I have ever written (in my very short Lisp adventure).
(defun buster-goto-eoblock (&optional open-paren-pairs-count)
"Move point to the end of the next block"
(if (not open-paren-pairs-count)
(progn
(search-forward "{")
(setq open-paren-pairs-count 1)))
(cond
((eq 0 open-paren-pairs-count) (point))
(t (let ((open (buster-find-next-pos "{"))
(close (buster-find-next-pos "}")))
(cond
((< open close)
(goto-char open)
(buster-goto-eoblock (1+ open-paren-pairs-count)))
((< close open)
(goto-char close)
(buster-goto-eoblock (1- open-paren-pairs-count))))))))
Now that we can move to the beginning of a test from anywhere inside it, we can update our interactive function from before so it does something useful.
If the cursor is not currently on a line where a test starts, we call
our new function to move to the position (the goto-char
function moves point to the position specified by its argument). Finally
we move to the exact position where the quoted test name starts.
The buster-beginning-of-test-pos function will throw an
error if called before all tests. The reason is our call
to search-backward-regexp, which throws an error if no
result is found. Not having any test to move to isn't a big problem, and
we can silently fail that case in the interactive function.
(defun buster-goto-beginning-of-test ()
"Move point to the beginning of the current test function.
Does nothing if point is not currently inside a test function."
(interactive)
(condtion-case nil
(progn
(if (not (buster-beginning-of-test-curr-linep))
(goto-char (buster-beginning-of-test-pos)))
(goto-char (buster-test-name-pos)))
(error nil)))
The updated function conveniently introduces
the condition-case function, which is more or less
elisp's try-catch. Its first argument is a variable that
will have the error bound to it if one is thrown. We don't need it. The
second argument is the expression to catch errors from. Any other
arguments are error handlers. In the example above, we only have one,
for the "error" type, which does nothing.
Note again the use of the progn function to have two
expressions evaluated where Emacs expects one.
Update: Rolando Pereira emailed me to inform me of the
function ignore-errors, which is convenient in cases like
the above, where we really only want to ignore the error and rather
return nil. It's docstring is "Execute BODY; if an error
occurs, return nil. Otherwise, return result of last form in BODY."
Using this function gives us a slightly shorter result:
(defun buster-goto-beginning-of-test ()
"Move point to the beginning of the current test function.
Does nothing if point is not currently inside a test function."
(interactive)
(ignore-errors
(if (not (buster-beginning-of-test-curr-linep))
(goto-char (buster-beginning-of-test-pos)))
(goto-char (buster-test-name-pos))))
To try out our new Emacs functionality, put the following code in a buffer, mark it all and evaluate it using M-x eval-region <Enter>.
(defvar buster-test-regexp
"^\s+\"\.+\s\.+\":\s?fun"
"Regular expression that finds the beginning of a test function")
(defun buster-find-next-pos (char)
"Return the position at the next occurrence of `char`"
(save-excursion
(search-forward char nil t)
(point)))
(defun buster-test-name-pos ()
"Return the position where the test name starts on the current line"
(save-excursion
(beginning-of-line)
(search-forward "\"" (point-at-eol))
(1- (point))))
(defun buster-beginning-of-test-curr-linep ()
"Return t if a test starts on the current line"
(save-excursion
(beginning-of-line)
(search-forward-regexp buster-test-regexp (point-at-eol) t)))
(defun buster-goto-eoblock (&optional open-paren-pairs-count)
"Move point to the end of the next block"
(if (null open-paren-pairs-count)
(progn
(search-forward "{")
(setq open-paren-pairs-count 1)))
(cond
((eq 0 open-paren-pairs-count) (point))
(t (let ((open (buster-find-next-pos "{"))
(close (buster-find-next-pos "}")))
(cond
((< open close)
(goto-char open)
(buster-goto-eoblock (1+ open-paren-pairs-count)))
((< close open)
(goto-char close)
(buster-goto-eoblock (1- open-paren-pairs-count))))))))
(defun buster-beginning-of-test-pos ()
"Return the start position of the current test"
(let ((curr (point))
(start-pos 0)
(end-pos 0))
(save-excursion
(search-backward-regexp buster-test-regexp)
(setq start-pos (point))
(setq end-pos (buster-goto-eoblock))
(if (and (< start-pos curr)
(< curr end-pos))
start-pos curr))))
(defun buster-goto-beginning-of-test ()
"Move point to the beginning of the current test function.
Does nothing if point is not currently inside a test function."
(interactive)
(ignore-errors
(if (not (buster-beginning-of-test-curr-linep))
(goto-char (buster-beginning-of-test-pos)))
(goto-char (buster-test-name-pos))))
;; buster.el key bindings
(global-set-key (kbd "C-c t") 'buster-goto-beginning-of-test)
Once again we can try this out on the following JavaScript. Place the cursor somewhere in the body of the test function and hit C-c t <Enter>. The cursor should move to the beginning of the quoted test name.
buster.testCase("Graph", {
setUp: function () {
this.graph = capillary.graph.create();
this.formatter = capillary.formatters.ascii.bindGraph(this.graph);
},
"should emit dot for commit": function () {
var commit = { seqId: 0, id: "1234567", message: "Ok" };
var listener = this.spy();
this.graph.on("graph:dot", listener);
this.graph.graphBranch(C.branch.create([commit]));
var args = listener.args[0];
assert.calledOnce(listener);
assert.calledWith(listener, [0, 0]);
}
});
With the navigation in place, we can finally attempt the initial problem - disabling tests by adding two slashes in front of their name. To do this, we move to the beginning of the current test, move past the quote character, and insert the string "//". Simple enough.
(defun buster-disable-test ()
"Disables a single test by using the 'comment out' feature of
Buster.JS xUnit style tests. Finds test to disable using
buster-goto-beginning-of-test"
(interactive)
(save-excursion
(buster-goto-beginning-of-test)
(forward-char)
(insert "//")))
(global-set-key (kbd "C-c C-d") 'buster-disable-test)
To avoid actually moving the cursor, we wrap the function body in a call
to save-excursion. Evaluate the code above and place the
cursor inside the test from before, and hit C-c C-d. The test
name should have the two slashes added.
What happens if we call the new buster-disable-test
function when the cursor is not inside a test? As you might remember,
buster-goto-beginning-of-test will simply leave point at
the current position if it is not inside a test. This means that we need
to make sure we are inside a test before doing anything else.
(defun buster-disable-test ()
"Disables a single test by using the 'comment out' feature of
Buster.JS xUnit style tests. Finds test to disable using
buster-goto-beginning-of-test"
(interactive)
(save-excursion
(buster-goto-beginning-of-test)
(if (buster-beginning-of-test-curr-linep)
(progn (forward-char)
(insert "//")))))
This avoids adding "//" in random places. If we are not inside a test, the function will simply do nothing. The last thing to consider is what to do if attempting to disable an already disabled test. As is, the function will just keep adding "//". We can fix this by searching for existing slashes before adding new ones.
In this last version of the function, we will rely on the fact that
search-forward returns point (i.e. non-nil) after finding a
match. If we pass t as the third argument, it will return
nil if no match is found. We will only add new slashes if
the search fails.
(defun buster-disable-test ()
"Disables a single test by using the 'comment out' feature of
Buster.JS xUnit style tests. Finds test to disable using
buster-goto-beginning-of-test"
(interactive)
(save-excursion
(buster-goto-beginning-of-test)
(if (buster-beginning-of-test-curr-linep)
(progn
(forward-char)
(if (not (search-forward "//" (+ (point) 2) t))
(insert "//"))))))
The search for the slashes is bounded by (+ (point) 2). In
other words, we only look for them directly after the quote. This final
version can be called any number of times but will only ever add two
slashes to the test name.
When a test is disabled, we need a way to enable it again.
buster-enable-test is simpler than its couterpart. It will
move to the start of the current test, search for the slashes in the
test name and remove them if found. Otherwise it does nothing.
(defun buster-enable-test ()
"Ensables a single test by removing the 'comment' inserted by
buster-disable-test"
(interactive)
(save-excursion
(buster-goto-beginning-of-test)
(if (search-forward "\"//" (+ (point) 3) t)
(delete-region (- (point) 2) (point)))))
(global-set-key (kbd "C-c C-e") 'buster-enable-test)
With both buster-disable-test
and buster-enable-test in place, we have reached our
initial goal. If you made it all the way down here, then thanks for your
attention. Hopefully you learned a little about both Emacs and Lisp.
To summarize, we have seen basic Lisp syntax in use, and we have succesfully built several functions on our own, using multiple built-in Emacs Lisp functions. Below you will find a list of all the functions we used along with a short description of each. Note that many of these accept optional arguments that we have not discussed. You can find usage examples throughout the article, and more information in Emacs by typing M-h f func-name <Enter> (many of the explanations below were lifted right from the Emacs documentation).
(+ &rest NUMBERS-OR-MARKERS)(- &optional NUMBER-OR-MARKER &rest MORE-NUMBERS-OR-MARKERS)NUMBER-OR-MARKER subtracted by remaining arguments(1+ NUMBER)NUMBER incremented by one(1- NUMBER)NUMBER decremented by one(< NUM1 NUM2)t if the first argument is less than the second(and CONDITIONS...)(beginning-of-line &optional N)(cond CLAUSES...)(condition-case VAR BODYFORM &rest HANDLERS)BODYFORM(ignore-errors &rest BODY)BODY; if an error occurs, return nil. Otherwise, return result of last form in BODY.(defun NAME ARGLIST [DOCSTRING] BODY...)(defvar SYMBOL &optional INITVALUE DOCSTRING)(delete-region START END)START to END(eq OBJ1 OBJ2)t if the arguments are equal(forward-char &optional N)N (default one) character(s) ahead(goto-char POSITION)POSITION(if COND THEN ELSE...)THEN if COND is
non-nil, ELSE otherwise
(insert &rest ARGS)(interactive &optional ARGS)(let VARLIST BODY...)(not OBJECT)t if OBJECT is nil(point)(point-at-eol &optional N)(progn BODY...)BODY forms sequentially and return value of last
one
(save-excursion &rest BODY)BODY;
restore those things
(search-backward-regexp REGEXP &optional BOUND NOERROR COUNT)REGEXP
(search-forward STRING &optional BOUND NOERROR COUNT)STRING
(search-forward-regexp REGEXP &optional BOUND NOERROR COUNT)REGEXP
(setq [SYM VAL]...)SYM to the value of its VAL.
In an upcoming article, I will show how to write interactive functions that take arguments, operate on regions and other segments of the current buffer.
(defvar buster-test-regexp
"^\s+\"\.+\s\.+\":\s?fun"
"Regular expression that finds the beginning of a test function")
(defun buster-find-next-pos (char)
"Return the position at the next occurrence of `char`"
(save-excursion
(search-forward char nil t)
(point)))
(defun buster-test-name-pos ()
"Return the position where the test name starts on the current line"
(save-excursion
(beginning-of-line)
(search-forward "\"" (point-at-eol))
(1- (point))))
(defun buster-beginning-of-test-curr-linep ()
"Return t if a test starts on the current line"
(save-excursion
(beginning-of-line)
(search-forward-regexp buster-test-regexp (point-at-eol) t)))
(defun buster-goto-eoblock (&optional open-paren-pairs-count)
"Move point to the end of the next block"
(if (null open-paren-pairs-count)
(progn
(search-forward "{")
(setq open-paren-pairs-count 1)))
(cond
((eq 0 open-paren-pairs-count) (point))
(t (let ((open (buster-find-next-pos "{"))
(close (buster-find-next-pos "}")))
(cond
((< open close)
(goto-char open)
(buster-goto-eoblock (1+ open-paren-pairs-count)))
((< close open)
(goto-char close)
(buster-goto-eoblock (1- open-paren-pairs-count)))))))
(defun buster-beginning-of-test-pos ()
"Return the start position of the current test"
(let ((curr (point))
(start-pos 0)
(end-pos 0))
(save-excursion
(search-backward-regexp buster-test-regexp)
(setq start-pos (point))
(setq end-pos (buster-goto-eoblock))
(if (and (< start-pos curr)
(< curr end-pos))
start-pos curr))))
(defun buster-goto-beginning-of-test ()
"Move point to the beginning of the current test function.
Does nothing if point is not currently inside a test function."
(interactive)
(ignore-errors
(if (not (buster-beginning-of-test-curr-linep))
(goto-char (buster-beginning-of-test-pos)))
(goto-char (buster-test-name-pos))))
(defun buster-disable-test ()
"Disables a single test by using the 'comment out' feature of
Buster.JS xUnit style tests. Finds test to disable using
buster-goto-beginning-of-test"
(interactive)
(save-excursion
(buster-goto-beginning-of-test)
(if (buster-beginning-of-test-curr-linep)
(progn
(forward-char)
(if (not (search-forward "//" (+ (point) 2) t))
(insert "//"))))))
(defun buster-enable-test ()
"Ensables a single test by removing the 'comment' inserted by
buster-disable-test"
(interactive)
(save-excursion
(buster-goto-beginning-of-test)
(if (search-forward "\"//" (+ (point) 3) t)
(delete-region (- (point) 2) (point)))))
;; buster.el key bindings
(global-set-key (kbd "C-c C-d") 'buster-disable-test)
(global-set-key (kbd "C-c C-e") 'buster-enable-test)
(global-set-key (kbd "C-c t") 'buster-goto-beginning-of-test)
Big thanks to Ala'a Mohammad for cleaning up/improving several aspects of this article.
Comments