Back to Blog

6 Months of Rift

June 29, 2026
4 min read
By TJ Raklovits
ProgrammingRust
Screenshot of the Rift editor demonstrating annotations
Click to enlarge

I have been a vim/neovim user for most of my programming life, which at this point means I've been programming for about 15 years and using vim or neovim for 10 of those, and in that time I have built custom vim configs in vimscript and then in lua when that became the thing to do, played with and tried practically every plugin in the ecosystem that seemed worth trying, used big vim distros like spacevim and lazy.nvim and astronvim for a while before inevitably getting frustrated with some opinion they had that wasn't my opinion and going back to something I'd assembled myself, and I have evangelized about vim for years to basically anyone who would sit still long enough to listen to me: coworkers, friends, people at meetups. I love it, genuinely, I think modal editing is the right way to interact with text and I've never really wanted to go back to anything else, but neovim is enormous and has hundreds of features that, while certainly useful at times, I mostly never use, and it has gaps that need to be filled with plugins which have their own opinions about how things should work and their own bugs and their own maintainers who may or may not still be around. At some point my editor started to feel less like mine and more like someone else's editor that I happened to have put a few hundred lines of lua on. I wanted something simpler, something that only has what I actually use, designed not to be an extension of everyone's thought about what an editor should be but specifically mine, something that works the way I work and has paradigms that make sense to me and where I can build the features I want without having to also carry around the hundred features I don't. I'd been reading Acme and abont with interest for the same reason, editor concepts that had different dissatisfactions.

So in December 2025 I started writing rift, first in Zig because I wanted to learn Zig and had been looking for an excuse to, and an editor felt like the right kind of project for that: big enough to be a real project with real problems but scoped enough that it wasn't completely insane to try. I got a terminal backend working, a gap buffer, basic key handling and mode switching, some rudimentary rendering, and then ran into Windows terminal compatibility issues that I didn't really want to wrestle with in Zig specifically, and also at some point I noticed the project was starting to feel more like a vehicle for learning Zig than like actually building the thing I wanted to build, so I scrapped it and started over in Rust. The Rust version had basically the same structure as the Zig version: a Key type, a Mode enum, a gap buffer, some rendering, which meant it came together quickly since I'd already worked through most of the problems once, and by December 31st, ten days after the first commit, I had treesitter in.

I don't really like syntax highlighting, I think it makes you a worse programmer over time to need the colors to read the code, because it means the colors are doing cognitive work that your eyes and brain should be doing on their own, and I feel much the same way about LSP and things like inline type hints, unfortunately all of it is really awfully convenient in a way I find annoying, because I think it papers over a kind of understanding you should be building from the code itself, but I always end up turning it all on anyway because it's so frustratingly useful that forgoing it starts to feel like an unnecessarily principled stand I'm making at my own expense. I put treesitter in early because other people couldn't stand looking at code with me. If you've ever tried to share a screen or pair program in an editor with no syntax highlighting you know exactly the look you get the moment you open a file, and I was getting that look a lot.

The first several months were fast and messy in the way early projects are when you're learning what you actually want as you go and don't really have a spec beyond "an editor that works the way I think about things." I replaced the gap buffer with a rope on a piece table at some point because the gap buffer was making other things harder in ways I kept running into, and the rope has been fine since. I also wrote the regex engine from scratch, it's called monster-regex and it's a separate crate, the reasons were partly that I wanted to understand how search worked all the way down, which is just a thing I find interesting and worthwhile even when it's impractical, and partly that I had specific behavior in mind that no existing engine would give me cleanly. The search format is pattern/flags, where flags are single characters appended after the slash rather than passed as a separate argument, so /foo\ze bar/i matches "foo" in "foo bar" with the match end set just before the space, \zs and \ze are vim-style match boundary markers that let you position the reported match independently of what the pattern actually consumes, which is useful for operations where you want to select exactly the right range without capturing groups. Smartcase is the default rather than an opt-in, meaning all-lowercase patterns search case-insensitively and patterns with any uppercase go case-sensitive automatically, with explicit i and c flags to override either direction. There are extended character classes that don't exist in most engines: \l and \L for lowercase and non-lowercase, \h for head-of-word characters, \p for punctuation, \zs and \ze as mentioned, and position anchors like \%# for the current cursor position and \%5l for line five specifically, most of which are straight from vim's regex dialect and which I use constantly without really thinking about it. Under the hood there are two engines, a backtracking engine that supports lookarounds and backreferences and can be exponential on adversarial input, and a PikeVM-based linear engine that's O(n) guaranteed and runs when you don't need the extra features. I spent a while optimizing it after the initial implementation: there were several rounds of linear engine work, profiling passes, and eventually some unsafe in the hot path, the commit messages go "Faster linear engine," "Even faster searching," "faster faster faster," which is about how it felt at the time. It's competitive with the regex crate now on real workloads, which I didn't expect to get to but here we are. It has some unfinished corners I know about and haven't gotten to yet, but I understand every part of it, which was the point.

The annotation framework is probably the thing I'm most satisfied with out of everything I built, and also the thing I'm most glad I took the time to design carefully before building. An annotation is a span of text with a kind, namespaced, so things like lsp.diagnostic or ui.selection, a generic JSON-like payload, and an owner that determines authority over the annotation. They're stored in an interval tree so range queries are O(log n), they have per-endpoint gravity so each end of the span can be configured to track forward or backward as text is inserted around it, and they're snapshotted into the undo history so that when you undo a change the annotation state rolls back alongside the text, which matters for things like diagnostic positions not drifting as you edit. I built the whole thing because I wanted LSP diagnostics that would stay attached to the right code as you worked around them rather than drifting to wrong positions or disappearing, but it turned out to be the right abstraction for a lot of things I hadn't fully anticipated: search highlights, file explorer metadata, plugin-authored virtual text, selection highlights, all of it runs through the same system now, which makes things considerably more coherent than they would have been if I'd built special cases for each one. LSP itself landed May 5th and the commit message was just "lsp yes yes," which is about as much ceremony as it deserved, and I only use it for Rust and Python.

In April I wrote "LETS GO SCROLLBACK" as a commit message, all caps, when terminal scrollback finally worked, because the terminal emulator had existed for a while already and technically ran programs fine, you could open a terminal pane and run things, but you couldn't scroll up to see previous output, which meant it was useless for anything where you cared about what had just happened, which is most things. Daemon mode came in mid-June and is probably the feature I'm most likely to keep using long-term, the editor runs as a headless process on whatever remote machine you want, you connect over an SSH tunnel from your local terminal, and you can disconnect and reconnect without losing any state, with essentially zero startup time on reconnect since the process is already running.

When I first started having others use it they would run into bugs I found fairly amusing, because in my own day-to-day usage I had never hit those paths and so they'd just been sitting there broken the whole time without my knowing. My roommate started filing issues in February his first was titled "Cannot search the word Coding," which turned out to be a smartcase bug where the capital C was being handled incorrectly, and I fixed it the same day, and by March he was filing faster than I was closing, things like vertical split breaks and line wrap behaving wrong on file open and missing normal mode commands like o and O, a lot of stuff I genuinely hadn't thought about because I just don't work the way he works and so the paths he was walking were ones I'd never touched. He filed "Add ability to scroll up and down in terminal" on April 7th and the LETS GO SCROLLBACK commit is April 13th, so that one came quickly, and on April 9th he filed three issues in a row, add f for find, allow nG to jump to a specific line number, add D to delete to end of line, and all three landed the same day, which I think is a reasonable illustration of what the project looked like at that point, just a lot of obvious vim things absent because they weren't in my own workflow.

After a while he started sending code rather than just filing issues, and his first contributions were things he needed for his own work: Alt key modifier support because he had keybindings in mind that required Alt and simply implemented it rather than filing a request, SQL syntax highlighting because he works in SQL and it didn't exist yet, a TypeScript syntax fix because TypeScript highlighting was broken in a way that was bothering him (I don't use syntax highlighting with TS, so I never noticed). He also sent a PR to fix plugin loading when running directly from the .exe on Windows that didn't end up merging for reasons I can't fully reconstruct now, and opened something that was half PR, half question about how to properly expose terminal escape handling to the plugin API, which was useful even without becoming a direct code contribution. My roommate also wrote plugins on top of all of this, rift has a Lua plugin API and he was using it, which is the most reliable way to find out whether an API is actually usable. He still has two open issues on the tracker from that period, .riftrc config file support and code folding, neither of which I am planning on doing so far (sorry!).

The development workflow is very worktree-heavy. The repo is a bare git repo and every branch I'm actively working on is its own directory on disk, so rather than switching branches you just cd into a different directory, and right now there are around 60 live worktrees with names like feat/annotations and fixes/search-highlight and registers and treesitter and so on, each one a physical place on disk I can navigate to without stashing anything or losing whatever state I was in, which also means features can sit in an in-progress state indefinitely without blocking other work because they're just directories.

It's at version 0.6.1 now, somewhere around 40 releases since the first one, and it has LSP with diagnostics and go-to-definition and find-references and hover and rename and code actions, treesitter syntax highlighting for around 18 languages, a Lua plugin system, a ranger-style file explorer, splits both vertical and horizontal, a terminal emulator with scrollback, daemon and remote connection mode, an undo tree you can navigate visually, the annotation framework, and the custom regex engine. Visual mode has a SelectionSet abstraction where you can bank multiple independent regions and then operate on all of them at once with the usual operators, delete and change and yank and so on, which I think is a more interesting design than standard vim visual mode and I'm genuinely happy with how it turned out, and most of it was written in a single long day in June after I'd been turning the design over in my head for months without sitting down to do it. Still missing macros, marks, and a jump list, and I have a MASSIVE 1075 line file called BACKLOG.md of all of the stuff I eventually want to get to. It's such a fun project to work on, and 99% of the time I can cargo run to edit the code as I build new features, which is truly exciting.

I use it for everything, all my code, all my notes, writing this in it right now.

Share this post