Build your own text editor with Reason—Getting started

Update September 29, 2020

I’ve decided to put this project on hold for a bit, until the Revery team addresses an issue with their ScrollView component. While it’s not a showstopper, the UI wouldn’t really work without it. I’ll be back on this as soon as the issue is addressed!

I’ve been thinking a lot about text editors lately, going so far as to playing with writing my own. In the process of exploring that idea I came across a tutorial written by Pailey Quilts on how to build a text editor in C. It’s a great write-up that thoroughly explains the code behind kilo, a simple terminal editor.

I’ve been writing code in Reason in different projects lately and I’m quickly falling in love with the language. I find the developer experience very pleasing, and I absolutely love the type system. I’ve also been keeping my eyes on Onivim 2, a new text editor that aims to have the usability of VSCode with the utility of Vim—an ambitious project to say the least!

It just so happens that Onivim 2 is built with Reason! More specifically, it’s built with Revery, a GUI framework that came to be because of Onivim 2. And all of this got me thinking; what would a kilo-like editor look like when made with Reason and Revery?

So, here we are. My take on Pailey’s tutorial, except instead of writing a simple terminal editor in C, you’re going to write a simple GUI editor in Reason!

All the code and incremental changes will be published in ~reykjalin/kilo-gui.

Setup

Just like in Pailey’s post, the first thing you’ll want to do is to set up your environment. To build GUIs using Reason and Revery, you’ll need to install Esy. Esy is a package manager and build tool, similar to Node’s npm, but built for Reason and OCaml.

How to install Esy

The first thing you’ll need is Node. The LTS version will do just fine. Node should come with npm, and it’s really npm you need. You’ll use npm to install esy.

npm install -g esy

You may need to run this command as an administrator, in which case you’ll need to add the --unsafe-perm=true flag.

sudo npm install -g esy --unsafe-perm=true

I’d recommend you try running the test project they’ve set up to make sure esy is installed correctly. You can find instructions on how to do this in Esy’s getting started guide.

Creating a Revery project

The simplest way to get started with a new Revery project is to clone the Revery quick start project, and go from there.

git clone https://github.com/revery-ui/revery-quick-start.git

I’ve found that the dependencies tend to be out of date here, and there might be some key differences in the Revery API between the version installed in the quick start vs. the latest version of Revery, so the first thing you’re going to do is update all the project dependencies.

Updating the project dependencies

The current dependencies can be found in package.json in the dependencies and devDependencies objects. The dependencies should look something like this

/* package.json */ "dependencies": { "revery": "revery-ui/revery#8fd380c", "@opam/dune": "2.5.0", "@glennsl/timber": "^1.2.0", "esy-macdylibbundler": "*" }, "devDependencies": { "ocaml": "~4.9.0", "@opam/ocaml-lsp-server": "ocaml/ocaml-lsp:ocaml-lsp-server.opam#04733ed" }

Finding the newest versions for your dependencies can be a bit tricky, so here is how I found the newest versions:

  • Revery‘s latest commit hash is 1531d5f.
  • dune‘s latest version is 2.6.1.
  • OCaml‘s latest version is 4.10.0.
  • ocaml-lsp‘s latest commit hash is b017b14.
  • Timber is already using the latest version.
  • The * in esy-macdylibbundler means the latest version will be installed.

So after we update package.json the dependencies should look something like this.

/* package.json */ "dependencies": { "revery": "revery-ui/revery#1531d5f", "@opam/dune": "2.6.1", "@glennsl/timber": "^1.2.0", "esy-macdylibbundler": "*" }, "devDependencies": { "ocaml": "~4.10.0", "@opam/ocaml-lsp-server": "ocaml/ocaml-lsp:ocaml-lsp-server.opam#b017b14" }

Updating the project metadata

Updating the project metadata to fit your needs is usually a good idea. You should use this chance to do exactly that! You can find this project metadata in package.json.

/* package.json */ { "name": "kilo-gui", "version": "0.1.0", "description": "A simple GUI editor built with Revery", "license": "MIT", "scripts": { "format": "bash -c \"refmt --in-place **/*.re\"", "run": "esy x Kilo" }, "esy": { "build": "dune build -p Kilo", "buildDev": "refmterr dune build -p Kilo", "buildsInSource": "_build" }, "revery-packager": { "bundleName": "Kilo", "bundleId": "com.thorlaksson.kilo", "displayName": "Kilo", "mainExecutable": "Kilo", // ... }

The changes I’ve suggested here mean you need to rename src/App.re to src/Kilo.re, and change some of the dune files to accommodate these project changes. It’s a bit tedious to list all of those here, so I’d recommend you look at the commit where I make these changes instead.

Building the project

The first build after updating your dependencies needs to be completely clean, so before doing anything you should delete the esy.lock directory, which is used to lock the dependency versions in place. If you don’t delete this directory now, esy will not update the dependencies, despite the changes in package.json.

rm -rf esy.lock

Now you can build the project by running esy.

esy

Running esy will install your dependencies and build your project. This first build will take a while since esy will actually build all the dependencies for you. esy caches your built dependencies so subsequent builds will be close to instantaneous. You just need to suffer through this long build time the first time you build your project.

And now you can run the project!

esy run

This command will both build and run the project for you! No need to run 2 commands, yay! 🎉

And you should be greeted with the default quick start app!

The default quick start app for the Revery GUI framework. Displays a window with 3 elements in the center. First element is text that says "Welcome to Revery". Second element is a button that says "Increment". Third element is text that says "Times clicked: 0", and is intended to show the number of times the button has been clicked.

Make room for your own work

The last thing for you to do is remove all the unnecessary code, and start with a clean slate. This is relatively simple; delete src/AnimatedText.re, src/SimpleButton.re, and src/Theme.re, and then modify src/Kilo.re. to remove the unnecessary Style, and reduce the main app component to an empty component.

/* src/Kilo.re */ let main = () => React.empty;

Finally, you need to remove the dependency on Theme.re by changing the win variable to

/* src/Kilo.re */ let win = App.createWindow( app, "Kilo", ~createOptions= WindowCreateOptions.create( ~backgroundColor=Colors.white, ~width=512, ~height=384, (), ), );

Now when you run the program you should see an empty window—a great place to start a new project!

An empty window.

Going forward you’ll want to build and run the app to see the changes in action and make sure you didn’t accidentally break something. Like Pailey says:

It is easy to forget to recompile, and just run ./kilo, and wonder why your changes to kilo.c don’t seem to have any effect. You must recompile in order for changes in kilo.c to be reflected in kilo.

The same can be said here! Make sure to run the app after making changes!

The next post will be up soon!

A new code editor?

I’ve written before about how I’m not happy with the code editors available today. I think they can be improved upon, so I’ve started experimenting with making my own. I don’t think this will ever be the shiny new code editor on the block, but I’m hoping it’ll at least be something I’d prefer over other editors.

I’m currently calling this project Qode, but I might change that further down the line—especially since I’ve already seen other similar projects with the same name.

I’m not sure how far I’ll take this, and might not even make this a huge thing, but I want to outline part of the vision for this editor. I think doing so will help me stay focused on what to work on next, and whether I’m spending too much time digging into something I don’t need to think about.

So, without further ado, let’s get into it.

Graphical interface please!

I dislike the limitations of terminal editors, and as such I want the editor to have a graphical interface (GUI). That said, I also want this interface to be minimalistic. Most of the time it shouldn’t be showing you anything beyond what a terminal editor would show you.

What I mean by this is that most of the time the only thing visible in the editor should be the text editing window itself. No toolbars, no sidebars, no fluff.

That doesn’t mean those won’t be available, mind you! Just that they shouldn’t be visible by default. Instead, you should be able to bring them to focus with a keyboard shortcut (e.g. double-tap tab, hold shift, etc.) or a single mouse-click/hover action.

I want the editor to get out of the way when you’re writing code, and I think modern editors fail when it comes to this, what with all their sidebars and toolbars.

Keyboard driven

I want the GUI to be fully accessible with just a keyboard. Or at the very least, you should be able to control every aspect of the editor with a keyboard.

I’m not really talking about navigating menus with a keyboard (although that should be possible either way), instead I’m thinking of a sort of command mode, similar to Vim, Emacs, and Kakoune. The commands should allow you to do anything you might otherwise do by navigating through a bunch of GUI menus.

Taking Vim as an example here, you enter command mode by typing : while in normal mode. You can then type write and hit the return key, and that’s how you save the file you’re currently working on; by typing :write<ret>.

Mouse driven

Yes! Also fully mouse driven!

I want to be able to fully navigate the editor with only my mouse. Sometimes I like doing that, e.g. when I’m just browsing through the codebase, or messing with some settings.

At other times I’d like to be able to use the keyboard, e.g. when I’m already working on some code and I want to quickly switch to a different file, jump to a definition, etc.

And sometimes I have one hand on the keyboard and the other on my mouse. That should limit me as little as possible.

The whole goal here is to make the editor equally accessible with multiple input methods.

Functionality driven by plugins

This is probably the most difficult part to design and implement. I want to allow plugins to control every aspect of the editor; rebind keys and actions, open/close dialogs, add configuration options, etc.

The editor should have default functionality relevant to the core usage of a code editor, but every aspect of that default functionality should be extendable with plugins. The system should even allow you to completely re-implement how some things work.

The example that’s at the forefront of my mind all the time is rebinding keys. Or rather, changing the input handling. It should be easy for plugins to create modal editing modes, like what Vim and Kakoune do. That means you’re not just rebinding keys, the plugin should be able to change the whole input handling for the editor.

Another example of a plugin would be a frontend for debuggers. Allowing the user to set breakpoints, step through code, etc.

Aside from the ambitious scope of this plugin system I also want plugin support to be programming language agnostic. If you want to write a plugin in Java, go for it. Haskell? No problem. C? You got it boss!

This, of course, essentially means that plugins must be standalone executables or scripts. There’s no other way to make this happen unless you rebuild the editor. As a result, the plugins need to communicate with the editor somehow, and I think I’ll use the json-rpc protocol to achieve this.

Initial goals

What I’ve outlined here is already pretty ambitious, and would take a long time to implement properly. Because of that I want to limit the scope to a sort of minimally usable editor for myself:

  1. Syntax highlighting.
  2. Support for TextMate/VSCode color themes.
  3. Auto-completion via the language server protocol (LSP).
  4. Project overview bar.
  5. Support for basic plugins
    • The benchmark here is to support plugins that change the input handling.

I haven’t yet decided what will be part of the core editor and what will be implemented through plugins. Especially the syntax highlighting and LSP support. I guess that’s something I’ll need to find out along the way!

What’s happening with this now?

Despite the fact that Electron apps work fine most of the time, I dislike the idea of Electron when it’s not required (hint: it almost never is). As such I chose to find a suitable GUI framework that’s closer to being native, and that doesn’t leave me with many options.

I chose to go with C++ and Qt. The Qt framework is mature, looks nice on all platforms, comes with a lot of good stuff, and I’ve worked with Qt before.

At the time of writing I have a working proof of concept available built with QScintilla as the editor widget. Input handling is managed with a plugin, and so is syntax highlighting and TextMate theme support.

Screenshot of TypeScript code being edited with Qode

Right now I’m looking into whether I should continue to use QScintilla, or whether I should use Scintilla or KTextEditor. My main gripe with QScintilla is that it’s based on a relatively outdated version of Scintilla. QScintilla is based on Scintilla v3.10.1, which was released in October 2018. That version of Scintilla is the LTS version, but it’s still several LTS versions behind, the latest one being 3.11.2.

I’m leaning towards using KTextEditor since that gives me a bunch of nice things out of the box, but that will really only work if there’s an easy way to include that in the project.

Once I’ve explored what it’s like to use some of the different editor widgets, and made up my mind as to which one I’ll use, I’ll work on LSP support and re-evaluate how far I want to take this after that.

Here’s to hoping this project isn’t way too ambitious.