Writing Emacs Apps in JavaScript

David DeSimone
6 min readJan 10, 2021

--

I’ve always been an emacs fanatic. I just love how much control I have over every aspect of the editor. However, a lot of aspects of emacs are showing their age. Writing and packaging a simple app as a beginner means learning elisp, learning the emacs API, learning about package repositories, ELPA vs MELPA… Just writing all that out is draining my motivation. Let’s see if we can make all that simpler using emacs-ng, a new fork that will allow us to write emacs applications in JavaScript, by integrating the Deno Runtime fully into the editor.

I remember after graduating college I discovered Sublime Text, and being immediately impressed by it’s fuzzy search. Press control-p and start typing, and it finds your files like magic. Let’s see if we can use our new tools to write something similar for emacs. First, we will need the core of the feature, which is a fuzzy search algorithm. We will use Forrest Smith’s fuzzy search, which they kindly placed in the public domain. The basic idea is that we will recursively attempt to identify a loose match our pattern across the string. We then assign potential matches a score based on factors like how many unmatched letters we have, if the letters we found in the string were in the same order as our pattern, etc. The API is very simple:

Setting up the basics

First things first, we will want to create our module. This is a basic emacs-ng module:

We will load it by adding this to our init.el located in our emacs home directory (normally $HOME/.emacs.d/init.el). fuzzy-impl.js is a file containing the Fuzzy Search algorithm linked above. Since we are working locally, we specify that the file is in the local directory.

(eval-js "import './mod-fuzzy.js'")

Our file is actually an ES6 module, so we can use import/export syntax. emacs-ng actually embeds the full Deno Runtime. Deno’s APIs are similar to node, except they are async by default, however for this, we will explicitly use the synchronous version (we will revisit this later). Lets use https://deno.land/std@0.83.0/fs/mod.ts, which gives us a function to walk a directory.

It would be nice if we could see our results. We can add a call to lisp.print(fuzzySearch('...')); and the editor will print our results for us once this file is evaluated using our import statement from above ( (eval-js “import ‘./mod-fuzzy.js'") ) . Calling our function with input “javac” gives the following results:

This is great, but we would like this function to be callable while browsing files. That is where the special lisp object comes in.

Interacting with the editor

We have full control of the editor while in JavaScript. In elisp, you would define a function with a special form called defun Instead, we will call the defun function on a special global objected called lisp in JavsScript. That object is the bridge between JavaScript and the editor. Let’s start by defining our function so we can call it by holding down Alt and pressing x (referred to in Emacs as “M-x”, or “Meta-x”) and then typing “fuzzy-search”:

To break this down, defun is defining a lisp function with the name “fuzzy-search”. The interactive: true is what tells lisp we want to call this function “interactively” by using Alt-x. args: "MInput >> " is saying that our input will be a string (Denoted by special code “M”), and that we should prompt the user with message Input>> .

We aren’t just limited to defun, we should be able to call ANY lisp function, even one’s defined in custom user packages, via the lisp object. We only need to translate from kabab case to snake case, i.e.

(my-custom-function 1 2 3) -> lisp.my_custom_function(1, 2, 3)

Showing off what we can do

So far, everything we have done in JavaScript we could have done in easily elisp. To improve our module, we can use Deno’s async filewalker, or use it’s filewatcher and maintain a cache files in the current directory. For the sake of experiment, let’s do something that emacs CAN’T currently do — multithreaded scripting using WebWorkers.

To create a WebWorker, we write the following:

mod-fuzzy-worker.js is a module that we will create that contains our WebWorker code.

This will spawn WebWorker, which is executing in parallel on another thread, something elisp has traditionally struggled with. In order to communicate between the two, we will need to pass messages. Lets start by moving our fuzzy search code over to the WebWorker, and adding an interface for sending messages:

WebWorkers communicate by passing messages to one another via the functions onmessage and postMessage Now we can add a little specific elisp code to get the results, and dump them into part of the user’s screen. Check out the 0.1.0 version on github. The finished product includes more advanced features like text highlighting, and opening the results in another frame.

We highlight the characters that matched in green. Pressing enter will allow us to open the file

This approach keeps the UI interactive even when we are dealing with a large number of files, or file paths that are very long. There are a lot of ways to improve this — use a filewatcher instead of walking the directory every time, allow for more customization of parameters, etc. We have something that’s just enough that someone might find it useful, so let’s get it out there for people to try.

Now that we have our app, how can we share it? Packages in emacs are normally shared via ELPA (or MELPA), but we can try another way — using Deno’s package repository. Deno’s workflow is very integrated with Github releases and is pretty easy to use in my opinion. It is described here.

Sharing this package is easy now. Just have your users add this line to their init.el:

(eval-js “import ‘https://deno.land/x/fuzzy_search@0.1.0/mod-fuzzy.js'")

This file will be cached upon download, so it’s only downloaded once. Deno allows you to not include the specific version, which will cause you to download the latest version, but that will throw a warning. It’s strongly recommended that you specify a version.

We’re just scratching the surface here- Deno has a lot of functionality not covered, including native TypeScript support. Hopefully you found this useful, and will considering checking out emacs-ng in the future.

--

--

David DeSimone
David DeSimone

Written by David DeSimone

Rust/C/C++ Systems Engineer, Game Engine Developer

No responses yet