Evaluating JavaScript in a Node.js REPL from an Emacs Buffer
Written by J. David Smith
Published on 01 June 2014
For my internship at IBM, we're going to be doing a lot of work on Node.js. This is awesome: Node is a great platform. However, I very quickly discovered that the state of Emacs ↔ Node.js integration is dilapidated at best (as far as I can tell, at least).
A Survey of Existing Tools
One of the first tools I came across was the swank-js
/ slime-js
combination. However, when I (after a bit of pain) got both setup, slime
promptly died when I tried to evaluate the no-op function: (function() {})()
.
Many pages describing how to work with Node in Emacs seem woefully out of
date. However, I did eventually find nodejs-repl
via package.el
. This
worked great right out of the box! However, it was missing what I consider a
killer feature: evaluating code straight from the buffer.
Buffer Evaluation: Harder than it Sounds
Most of the languages I use that have a REPL are Lisps, which makes choosing
what code to run in the REPL when I mash C-x C-e
pretty
straightforward. The only notable exceptions are Python (which I haven't used
much outside of Django since I started using Emacs) and JavaScript (which I
haven't used an Emacs REPL for before). Thankfully, while the problem is
actually quite difficult, a collection of functions from js2-mode
, which I
use for development, made it much easier.
The first thing I did was try to figure out how to evaluate things via Emacs Lisp. Thus, I began with this simple function:
(defun nodejs-repl-eval-region (start end)
"Evaluate the region specified by `START' and `END'."
(let ((proc (get-process nodejs-repl-process-name)))
(comint-simple-send proc (buffer-substring-no-properties start end))))
It worked! Even better, it put the contents of the region in the REPL so that it was clear exactly what had been evaluated! Whole-buffer evaluation was similarly trivial:
(defun nodejs-repl-eval-buffer (&optional buffer)
"Evaluate the current buffer or the one given as `BUFFER'.
`BUFFER' should be a string or buffer."
(interactive)
(let ((buffer (or buffer (current-buffer))))
(with-current-buffer buffer
(nodejs-repl-eval-region (point-min) (point-max)))))
I knew I wasn't going to be happy with just region evaluation, though, so I
began hunting for a straightforward way to extract meaning from a js2-mode
buffer.
js2-mode
: Mooz is my Savior
Mooz has implemented JavaScript parsing in Emacs Lisp for his extension
js2-mode
. What this means is that I can use his tools to extract meaningful
and complete segments of code from a JS document intelligently. I
experimented for a while in an Emacs Lisp buffer. In short order, it became
clear that the fundamental unit I'd be working with was a node. Each node
is a segment of code not unlike symbols in a BNF. He's implemented many
different kinds of nodes, but the ones I'm mostly interested in are
statement and function nodes. My first stab at function evaluation looked
like this:
(defun nodejs-repl-eval-function ()
(interactive)
(let ((fn (js2-mode-function-at-point (point))))
(when fn
(let ((beg (js2-node-abs-pos fn))
(end (js2-node-abs-end fn)))
(nodejs-repl-eval-region beg end)))))
This worked surprisingly well! However, it only let me evaluate functions that the point currently resided in. For that reason, I implemented a simple reverse-searching function:
(defun nodejs-repl–find-current-or-prev-node (pos &optional include-comments)
"Locate the first node before `POS'. Return a node or nil.
If `INCLUDE-COMMENTS' is set to t, then comments are considered
valid nodes. This is stupid, don't do it."
(let ((node (js2-node-at-point pos (not include-comments))))
(if (or (null node)
(js2-ast-root-p node))
(unless (= 0 pos)
(nodejs-repl–find-current-or-prev-node (1- pos) include-comments))
node)))
This searches backwards one character at a time to find the closest node. Note that it does not find the closest function node, only the closest node. It'd be pretty straightforward to incorporate a predicate function to make it match only functions or statements or what-have-you, but I haven't felt the need for that yet.
My current implementation of function evaluation looks like this:
(defun nodejs-repl-eval-function ()
"Evaluate the current or previous function."
(interactive)
(let* ((fn-above-node (lambda (node)
(js2-mode-function-at-point (js2-node-abs-pos node))))
(fn (funcall fn-above-node
(nodejs-repl–find-current-or-prev-node
(point) (lambda (node)
(not (null (funcall fn-above-node node))))))))
(unless (null fn)
(nodejs-repl-eval-node fn))))
You Know What I Meant!
My next step was to implement statement evaluation, but I'll leave that off of here for now. If you're really interested, you can check out the full source.
The final step in my rather short adventure through buffevaluation-land was a
*-dwim
function. DWIM is Emacs shorthand for Do What I Mean. It's seen
throughout the environment in function names such as comment-dwim
. Of
course, figuring out what the user means is not feasible – so we guess. The
heuristic I used for my function was pretty simple:
- If a region is active, evaluate it
- If the point is at the end of the line, try to evaluate the statement on that line (works with multiline statements thanks to Mooz's awesome work)
- Otherwise, evaluate the first statement or function found
This is succinctly represent-able using cond
:
(defun nodejs-repl-eval-dwim ()
"Heuristic evaluation of JS code in a NodeJS repl.
Evaluates the region, if active, or the first statement found at
or prior to the point.
If the point is at the end of a line, evaluation is done from one
character prior. In many cases, this will be a semicolon and will
change what is evaluated to the statement on the current line."
(interactive)
(cond
((use-region-p) (nodejs-repl-eval-region (region-beginning) (region-end)))
((= (line-end-position) (point)) (nodejs-repl-eval-first-stmt (1- (point))))
(t (nodejs-repl-eval-first-stmt (point)))))
The Beauty of the Emacs Development Process
This whole adventure took a bit less than 2 hours, all told. Keep in mind that, while I consider myself a decent Emacs user, I am by no means an ELisp hacker. Previously, the extent of my ELisp has been one-off advice functions for my .emacs.d. Being a competent Lisper, using ELisp has always been pretty straightforward, but I did not imagine that this project would end up being so simple.
The whole reason it ended up being easy is because the structure of Emacs
makes it very easy to experiment with new functionality. The built-in Emacs
Lisp REPL had me speeding through iterations of my evaluation functions, and
the ability to jump to functions by name with a single key-chord was
invaluable. This would not have been possible if I had been unable to read
the context from the sources of comint-mode
, nodejs-repl
and
js2-mode
. Even if I had just been forced to grep
through the codebases
instead of being able to jump straight to functions, it would have taken
longer and been much less enjoyable.
The beautiful part of this process is really how it enables one to stand on the shoulders of those who came before. I accomplished more than I had expected in far, far less time than I had anticipated because I was able to read and re-use the code written by my fellows and precursors. I am thoroughly happy with my results and have been using this code to speed up prototyping of Node.js code. The entire source code can be found here.