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://email@example.com/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
defun Instead, we will call the
defun function on a special global objected called
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
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
To create a WebWorker, we write the following:
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
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.
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://firstname.lastname@example.org/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.