Helix Editor

Because I’m some kind of masochist, I frequently swap between editors when writing code. For the most part, that means swapping between established editors that I know and already have configured on my system because I feel like using a graphical editor today versus a terminal-based editor yesterday, etc. On rare occasions, though, I find new editors that involve a whole new level of configuration and learning that make me question my life choices.

On the whole, my two most-used editors are Neovim and VS Code. However, I recently spent a little time seeing if anything new and exciting was on the horizon in the world of text editing. I had known about Fleet for a while but had thus far been unimpressed with the current beta builds. Similarly, I’m really looking forward to Cosmic Edit, but it’s currently far too early for someone like me to be using. I recently tried Zed, but I personally found it to be less compelling than other graphical editors with an uncomfortable focus on AI. Then I found Helix.

Background

The idea behind Helix is to take a modal, terminal-based editor (e.g. Vim) and rebuild it from the ground up in Rust. However, the goal is not to just remake Vim in Rust, but to also learn from projects like Vim and Neovim, fixing issues along the way, shrinking the codebase, and making for a much more usable experience out of the box.

Python code in Helix.

Editing

If you’re familiar with Vim, Neovim, etc. then you’ll almost feel immediately at home with Helix. Many of the keys and motions from Vim carry over fluently, and you’ll be rolling through code in no time. The editor itself is also extremely snappy given that it’s written in Rust… and uses basically no system resources (though your accompanying LSPs still will… more on that later.)

While the majority of Vim’s motions are the same, a few key ones are different. Helix has a focus on selecting text and then making changes to it. In some ways this makes sense because when using w or b to move between words in Normal mode, they’re automatically selected. It’s then just a matter of pressing d to delete them. It’s intuitive in a vacuum, but it still feels odd since it’s the opposite of the behavior one would have in Vim where deleting a word would be a matter of pressing d for the command then w to indicate what to delete.

This is even more apparent when wanting to do something like deleting all of the content between double quotes. In Vim, I would do this in Normal mode by pressing d to indicate a deletion command, i to tell it that I want to delete everything in something else, and then " to indicate what the “something else” is. In Helix, I would instead press m to indicate my desire to match, i to say that I want to match in something, and then " for what the “something” is. This will highlight all of the content inside of the double quotes, and then I would press d to delete it. I really don’t think that it’s better or worse… it’s just different, and that takes some time to adjust to.

The one keymap that really bothered me was moving to the beginning or end of a line. In Vim I would do this with $ to move to the end of a line and 0 to move to the beginning. It’s not exactly the most intuitive, but I got used to it. Helix uses… Home and End. This was a bit appalling to me since it seems antithetical to everything modal editors stand for in that it’s forcing me to take my hands away from their normal Home Row position to instead hit some weird keys that don’t even exist on half the keyboards I use without some modifiers. I ultimately ended up remapping them to Ctrl + s for the start of a line and Ctrl + e for the end of a line. More on remapping later.

At the end of the day, most of the motions are the same as Vim to the degree that navigation won’t be particularly impaired… but there will be some uncomfortable moments of stopping to think for a split second about what to hit.

Commands

Similar to Vim, Helix has a lot of commands available through a few different means. Users can either type a : and start typing commands or press a leader key (default Space) and access different commands that way. These can be intimidating to learn, but with time they come naturally. Helix jump starts this by providing on-screen guidance. Typing : for commands, for example, will show this:

Typing more of a given command will further refine what’s shown. Things like Neovim will do something similar in that you can Tab-complete commands, but having the options available on-screen is a nice change.

Similarly, pressing the leader will give the same type of options:

This pattern repeats for various menus. Typing m as previously mentioned to enter “match” mode will also show the various options, of which i is one. While it’s easy to ignore once you learn some menus, it’s still nice to have available to quickly see the options for those times when I think, “There should be an easier way to accomplish this…”

File Management

As astute readers may have noticed from the leader menu, files and buffers can be managed and swapped between with something akin to Telescope. It looks exactly like what anyone who has used Telescope would expect:

Along with this for swapping buffers, open buffers are visible as tabs along the top of the screen as well. In Normal mode, g n can be used to go to the next one while g p can be used to go to the previous one. While this is okay for a few buffers, I find it quickly becomes cumbersome when working with more open files. In Neovim, for example, I have the following keymap to allow me to swap between buffers with my leader and the buffer number:

nnoremap <silent><leader>1 <cmd>lua require("bufferline").go_to_buffer(1, true)<cr>
nnoremap <silent><leader>2 <cmd>lua require("bufferline").go_to_buffer(2, true)<cr>
nnoremap <silent><leader>3 <cmd>lua require("bufferline").go_to_buffer(3, true)<cr>
nnoremap <silent><leader>4 <cmd>lua require("bufferline").go_to_buffer(4, true)<cr>
nnoremap <silent><leader>5 <cmd>lua require("bufferline").go_to_buffer(5, true)<cr>
nnoremap <silent><leader>6 <cmd>lua require("bufferline").go_to_buffer(6, true)<cr>
nnoremap <silent><leader>7 <cmd>lua require("bufferline").go_to_buffer(7, true)<cr>
nnoremap <silent><leader>8 <cmd>lua require("bufferline").go_to_buffer(8, true)<cr>
nnoremap <silent><leader>9 <cmd>lua require("bufferline").go_to_buffer(9, true)<cr>

This works great with bufferline, especially with the following janky embedded Lua to use ordinal numbers:

lua << EOF
require("bufferline").setup{
    \ options = {
        numbers = "ordinal",
        diagnostic = "coc",
        separator_style = "slant" or "padded_slant",
        show_tab_indicators = true,
        show_buffer_close_icons = true,
        show_slow_icon = false,
    },
\ }
EOF

As someone who isn’t used to using a telescope-esque motion for buffer navigation, this was a bit of a deterrent. The bigger issue, though, was that nothing akin to a file tree exists for Helix. In Neovim, I use nerdtree for this. While I don’t keep it open all the time, I have a nice little shortcut in Ctrl + t to toggle it off and on when needed. While I frequently know the name of the file I want to open, sometimes I just really need to look at the directory structure, and for that nerdtree is amazing. I’ve also found it useful for creating new directories and some other very basic file management tasks that didn’t require me to swap to a different terminal tab. Sadly, Helix doesn’t seem to have anything like this at the moment.

Language Server Protocols

Helix comes with LSP support out of the box, though each LSP will need to be managed and configured on its own. There’s nothing like Conquer of Completion that I use with Neovim to do things on easy mode.

That being said, the Helix team has some great documentation on what langauge servers they support out of the box. With this information, I basically got everything I needed up and running without too many problems. While most of them just do what I want without additional configuration, I wanted to get a little more in depth for Python since it’s what I write the most for work and have the most stringent requirements for. I ended up just stealing my configuration from a comment in a Reddit post.

Java was a bit of a unique challenge in that I simply couldn’t get the LSP to work despite following the directions provided at the previous link. After more web searches than I care to think about, I finally found a post with someone having a similar problem that let me know about logs I could access. Simply typing the command for :log-open shows the editor log and that told me that Java 17 was required for the LSP to work. I had Java 11 set as my current version via SDKMAN because that’s what I needed for work I was doing, but swapping to 17 immediately corrected the problem. Since Gradle manages the Java version used for my projects, this was less of an annoyance than I expected.

Custom Language Configuration

For the most part everything has worked “out of the box” with Helix once I had an LSP properly configured. The one pain point I had was with Groovy. It’s a language that hasn’t exactly maintained popularity, and I’ve never really been able to get things properly working with it even in “easy” editors like VS Code. When opening a file in Helix, however, it had zero syntax highlighting. I tried various methods for getting syntax highlighting working, but none of them worked for Groovy itself. In the end, I added the following to my languages.toml file and got passable syntax highlighting by telling Helix that Groovy code was Java:

[[language]]
name = "java"
scope = "source.java"
injection-regex = "java"
file-types = ["java", "groovy", "Jenkinsfile", "jenkinsfile"]
roots = ["pom.xml", "build.gradle", ]
indent = { tab-width = 4, unit = "    " }
language-servers = [ "jdtls" ]

Even better, though, is that in the process of researching this I found that proper Groovy support was just merged last week. It should be available in a future release!

Unfortunately, I ran into the same issue with the DSL for Puppet, and .pp files were opened with zero syntax highlighting. I spent a decent bit of time trying to figure out how to shoehorn the Puppet treesitter definition into Helix without any luck. I ultimately gave on this since I would also need not just highlighting but also the linting provided by the official extension for VS Code.

Customization

On the whole, nearly every aspect of Helix can be customized, just as one would expect from an editor like this. There are two major configuration files. The first is a config.toml file that just controls the basics of the editor. This is where you can do things like set rulers, the line number style, the cursor type based on mode, etc. This file is what I used to remap the behavior for moving the cursor to the beginning or end of a line, as I mentioned earlier in the post:

[keys.normal]
"C-s" = "goto_line_start"
"C-e" = "goto_line_end"

My full config is available as a Gist. For anything language-specific, there’s a languages.toml file which controls those. This is how you can set things like formatters (e.g. Black for Python), linters, and whatever else you need for a given language to augment an existing LSP. I have a separate Gist for that.

In real operating systems, those exist in ~/.config/helix/. So I have no idea where they would live in Windows.

Overall

On the whole, I really like the Helix project, and I’m excited to see where it goes. While there are some things I’d love to see added (my kingdom for something akin to nerdtree) and the lack of support — at least at the moment — for plugins limits the functionality, I think they’re off to a terrific start. I do question some of the decisions for keymappings that conflict with Vim simply because Vim is, quite literally, everywhere, and as someone who knows Vim quite well it’s hard to discard those motions. For the time being, I’m still ultimately more drawn to using Neovim, but I’ll certainly be keeping an eye on Helix and testing out each new release.

One response

  1. […] I’ve talked about plenty of times before, I tend to use a lot of different text editors when writing code. Lately, I’ve been using VS Code for most of my development […]