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 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.
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:
- toggle the deferred state
- defer all tests but the current one
- enable all tests in the current buffer
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 cleverer, 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:
- Search backward for the closest match of of
buster-test-regexp
. Store the
position as start
.
- Find the bracket that closes this function by counting
{
and }
. (When the
number of forward-brackets and backward-brackets are the same, the function is
closed). Store the position as end
.
- If
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:
- Local variables
- The commonly used
cond
and progn
functions
- Optional function arguments
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).
- In the first pass, find the closest "{" and set the parens counter to 1
- In subsequent passes, find the next "{" and "}", and move to the closest one
- For each "{", increment the counter
- For each "}", decrement the counter
- When the counter is 0, return
(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)
- Return the sum of the arguments.
(- &optional NUMBER-OR-MARKER &rest MORE-NUMBERS-OR-MARKERS)
- Return
NUMBER-OR-MARKER
subtracted by remaining arguments
(1+ NUMBER)
- Return
NUMBER
incremented by one
(1- NUMBER)
- Return
NUMBER
decremented by one
(< NUM1 NUM2)
- Return
t
if the first argument is less than the second
(and CONDITIONS...)
-
Evaluate arguments until one of them yields nil, then return
nil. Otherwise return value of last argument
(beginning-of-line &optional N)
- Move point to the beginning of the current line
(cond CLAUSES...)
- Try each clause until one succeeds
(condition-case VAR BODYFORM &rest HANDLERS)
- Catch errors thrown by
BODYFORM
(ignore-errors &rest BODY)
- Execute
BODY
; if an error occurs, return nil. Otherwise, return result of last form in BODY
.
(defun NAME ARGLIST [DOCSTRING] BODY...)
- Define a function
(defvar SYMBOL &optional INITVALUE DOCSTRING)
- Define a variable
(delete-region START END)
- Delete text in the current buffer from
START
to END
(eq OBJ1 OBJ2)
- Return
t
if the arguments are equal
(forward-char &optional N)
- Move point
N
(default one) character(s) ahead
(goto-char POSITION)
- Move point to the exact
POSITION
(if COND THEN ELSE...)
-
Evaluate
THEN
if COND
is
non-nil, ELSE
otherwise
(insert &rest ARGS)
-
Insert text into the current buffer and move point to the end of the
inserted text
(interactive &optional ARGS)
-
Mark function as interactive. Also control how arguments can be passed
to an interactive function (not covered here)
(let VARLIST BODY...)
- Define scoped variables and execute expressions in scope
(not OBJECT)
- Return
t
if OBJECT
is nil
(point)
- Return the current position of point
(point-at-eol &optional N)
- Return the position of point at the end of the current line
(progn BODY...)
-
Eval
BODY
forms sequentially and return value of last
one
(save-excursion &rest BODY)
-
Save point, mark, and current buffer; execute
BODY
;
restore those things
(search-backward-regexp REGEXP &optional BOUND NOERROR COUNT)
-
Search backward from point for match for regular
expression
REGEXP
(search-forward STRING &optional BOUND NOERROR COUNT)
-
Search forward from point for match for
STRING
(search-forward-regexp REGEXP &optional BOUND NOERROR COUNT)
-
Search forward from point for match for regular
expression
REGEXP
(setq [SYM VAL]...)
-
Set each
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.