Jekyll2026-03-15T09:30:37+02:00https://batsov.com/feeds/Emacs.xml(think)Bozhidar Batsov's personal blogEmacs and Vim in the Age of AI2026-03-09T10:30:00+02:002026-03-09T10:30:00+02:00https://batsov.com/articles/2026/03/09/emacs-and-vim-in-the-age-of-ai

It’s tough to make predictions, especially about the future.

– Yogi Berra

I’ve been an Emacs fanatic for over 20 years. I’ve built and maintained some of the most popular Emacs packages, contributed to Emacs itself, and spent countless hours tweaking my configuration. Emacs isn’t just my editor – it’s my passion, and my happy place.

Over the past year, I’ve also been spending a lot of time with Vim and Neovim, relearning them from scratch and having a blast contrasting how the two communities approach similar problems. It’s been a fun and refreshing experience.1

And lately, like everyone else in our industry, I’ve been playing with AI tools – Claude Code in particular – watching the impact of AI on the broader programming landscape, and pondering what it all means for the future of programming. Naturally, I keep coming back to the same question: what happens to my beloved Emacs and its “arch nemesis” Vim in this brave new world?

I think the answer is more nuanced than either “they’re doomed” or “nothing changes”. Predicting the future is hard, but speculating is irresistible.

The Risks

The only thing that is constant is change.

– Heraclitus

Things are rarely black and white – usually just some shade of gray. AI is obviously no exception, and I think it’s worth examining both sides honestly before drawing any conclusions.

Let’s start with the challenges.

The IDE gravity well

VS Code is already the dominant editor by a wide margin, and it’s going to get first-class integrations with every major AI tool – Copilot (obviously), Codex, Claude, Gemini, you name it. Microsoft has every incentive to make VS Code the best possible host for AI-assisted development, and the resources to do it.

On top of that, purpose-built AI editors like Cursor, Windsurf, and others are attracting serious investment and talent. These aren’t adding AI to an existing editor as an afterthought – they’re building the entire experience around AI workflows. They offer integrated context management, inline diffs, multi-file editing, and agent loops that feel native rather than bolted on.

Every developer who switches to one of these tools is a developer who isn’t learning Emacs or Vim keybindings, isn’t writing Elisp, and isn’t contributing to our ecosystems. The gravity well is real.

I never tried Cursor and Windsurf simply because they are essentially forks of VS Code and I can’t stand VS Code. I’ve tried it several times over the years and I never felt productive in it for a variety of reasons.

Do you even need a “power tool” anymore?

Part of the case for Emacs and Vim has always been that they make you faster at writing and editing code. The keybindings, the macros, the extensibility – all of it is in service of making the human more efficient at the mechanical act of coding.

But if AI is writing most of your code, how much does mechanical editing speed matter? When you’re reviewing and steering AI-generated diffs rather than typing code character by character, the bottleneck shifts from “how fast can I edit” to “how well can I specify intent and evaluate output.” That’s a fundamentally different skill, and it’s not clear that Emacs or Vim have an inherent advantage there.

The learning curve argument gets harder to justify too. “Spend six months learning Emacs and you’ll be 10x faster” is a tough sell when a junior developer with Cursor can scaffold an entire application in an afternoon.2 Then again, maybe the question of what exactly you’re editing deserves a closer look.

The corporate backing asymmetry

VS Code has Microsoft. Cursor has venture capital. Emacs has… a small group of volunteers and the FSF. Vim had Bram, and now has a community of maintainers. Neovim has a small but dedicated core team.

This has always been the case, of course, but AI amplifies the gap. Building deep AI integrations requires keeping up with fast-moving APIs, models, and paradigms. Well-funded teams can dedicate engineers to this full-time. Volunteer-driven projects move at the pace of people’s spare time and enthusiasm.

The doomsday scenario

Let’s go all the way: what if programming as we know it is fully automated within the next decade? If AI agents can take a specification and produce working, tested, deployed software without human intervention, we won’t need coding editors at all. Not Emacs, not Vim, not VS Code, not Cursor. The entire category becomes irrelevant.

I don’t think this is likely in the near term, but it’s worth acknowledging as a possibility. The trajectory of AI capabilities has surprised even the optimists (and I was initially an AI skeptic, but the rapid advancements last year eventually changed my mind).

The Opportunities

That paints a grim picture, but Emacs and Vim have been written off more times than I can count. Eclipse was going to kill them. IntelliJ was going to kill them. VS Code was going to kill them. Sublime Text, Atom, TextMate – all were supposedly the final nail in the coffin. Most of those “killers” are themselves dead or declining, while Emacs and Vim keep chugging along. There’s a resilience to these editors that’s easy to underestimate.

So let’s look at the other side of the coin.

AI helps you help yourself

One of the most underappreciated benefits of AI for Emacs and Vim users is mundane: troubleshooting. Both editors have notoriously steep learning curves and opaque error messages. “Wrong type argument: stringp, nil” has driven more people away from Emacs than any competitor ever did.

AI tools are remarkably good at explaining cryptic error messages, diagnosing configuration issues, and suggesting fixes. They can read your init file and spot the problem. They can explain what a piece of Elisp does. They can help you understand why your keybinding isn’t working. This dramatically flattens the learning curve – not by making the editor simpler, but by giving every user access to a patient, knowledgeable guide.

I don’t really need any AI assistance to troubleshoot anything in my Emacs setup, but it’s been handy occasionally in Neovim-land, where my knowledge is relatively modest by comparison.

There’s at least one documented case of someone returning to Emacs after years away, specifically because Claude Code made it painless to fix configuration issues. They’d left for IntelliJ because the configuration burden got too annoying – and came back once AI removed that barrier. “Happy f*cking days I’m home again,” as they put it. If AI can bring back lapsed Emacs users, that’s a good thing in my book.

AI makes configuration and extension trivial

Something that doesn’t get nearly enough attention: Emacs and Vim have always suffered from the obscurity of their extension languages. Emacs Lisp is a 1980s Lisp dialect that most programmers have never seen before. VimScript is… VimScript. Even Lua, which Neovim adopted specifically because it’s more approachable, is niche enough that most developers haven’t written a line of it.

This has been the single biggest bottleneck for both ecosystems. Not the editors themselves – they’re incredibly powerful – but the fact that customizing them requires learning an unfamiliar language, and most people never make it past copying snippets from blog posts and READMEs.

I felt incredibly overwhelmed by Elisp and VimScript when I was learning Emacs and Vim for the first time, and I imagine I wasn’t the only one. I started to feel very productive in Emacs only after putting in quite a lot of time to actually learn Elisp properly. (Never bothered to do the same for VimScript, though, and I’m not too eager to master Lua either.)

AI changes this overnight. You can now describe what you want in plain English and get working Elisp, VimScript, or Lua. “Write me an Emacs function that reformats the current paragraph to 72 columns and adds a prefix” – done. “Configure lazy.nvim to set up LSP with these keybindings” – done. The extension language barrier, which has been the biggest obstacle to adoption for decades, is suddenly much lower.

The same goes for plugin development

After 20+ years in the Emacs community, I often have the feeling that a relatively small group – maybe 50 to 100 people – is driving most of the meaningful progress. The same names show up in MELPA, on the mailing lists, and in bug reports. This isn’t a criticism of those people (I’m proud to be among them), but it’s a structural weakness. A community that depends on so few contributors is fragile.

And it’s not just Elisp and VimScript. The C internals of both Emacs and Vim (and Neovim’s C core) are maintained by an even smaller group. Finding people who are both willing and able to hack on decades-old C codebases is genuinely hard, and it’s only getting harder as fewer developers learn C at all.

AI tools can help here in two ways. First, they lower the barrier for new contributors – someone who understands the concept of what they want to build can now get AI assistance with the implementation in an unfamiliar language. Second, they help existing maintainers move faster. I’ve personally found that AI is excellent at generating test scaffolding, writing documentation, and handling the tedious parts of package maintenance that slow everything down.

AI integrations are already happening

The Emacs and Neovim communities aren’t sitting idle. There are already impressive AI integrations:

Emacs:

  • gptel – a versatile LLM client that supports multiple backends (Claude, GPT, Gemini, local models)
  • ellama – an Emacs interface for interacting with LLMs via llama.cpp and Ollama
  • aider.el – Emacs integration for Aider, the popular AI pair programming tool
  • copilot.el – GitHub Copilot integration (I happen to be the current maintainer of the project)
  • elysium – an AI-powered coding assistant with inline diff application
  • agent-shell – a native Emacs buffer for interacting with LLM agents (Claude Code, Gemini CLI, etc.) via the Agent Client Protocol
  • ECA – an editor-agnostic AI coding assistant (built in Clojure!) that works via a protocol similar to LSP, supporting multiple LLM providers (and Neovim too)

Neovim:

  • avante.nvim – a Cursor-like AI coding experience inside Neovim
  • codecompanion.nvim – a Copilot Chat replacement supporting multiple LLM providers
  • copilot.lua – native Copilot integration for Neovim
  • gp.nvim – ChatGPT-like sessions in Neovim with support for multiple providers

And this is just a sample. Building these integrations isn’t as hard as it might seem – the APIs are straightforward, and the extensibility of both editors means you can wire up AI tools in ways that feel native. With AI assistance, creating new integrations becomes even easier. I wouldn’t be surprised if the pace of plugin development accelerates significantly.

Terminal-native AI tools are a natural fit

Funny enough, many of the most powerful AI coding tools are terminal-native. Claude Code, Aider, and various Copilot CLI tools all run in the terminal. And what lives in the terminal? Emacs and Vim.3

Running Claude Code in an Emacs vterm buffer or a Neovim terminal split is a perfectly natural workflow. You get the AI agent in one pane and your editor in another, with all your keybindings and tools intact. There’s no context switching to a different application – it’s all in the same environment.

This is actually an advantage over GUI-based AI editors, where the AI integration is tightly coupled to the editor’s own interface. With terminal-native tools, you get to choose your own editor and your own AI tool, and they compose naturally.

There’s another angle worth considering: if programming is increasingly about writing prompts rather than code, you still benefit from a great text editor for that. Prompts are text, and crafting them well matters. I find it ironic that Claude Code – a tool I otherwise love – doesn’t use readline, so my Emacs keybindings don’t work properly in it, and its vim emulation is fairly poor. I still think using React for CLI apps is a mistake, and I suspect many people would enjoy running Claude Code inside their Emacs or Vim instead. That’s exactly what the Agent Client Protocol (ACP) enables – it lets editors like Emacs (via agent-shell) act as first-class clients for AI agents, giving you proper editing, keybindings, and all the power of your editor while interacting with tools like Claude Code. The best prompt editor might just be the one you’ve been using for decades.

Emacs as an AI integration platform

Emacs’s “editor as operating system” philosophy is uniquely well-suited to AI integration. It’s not just a code editor – it’s a mail client (Gnus, mu4e), a note-taking system (Org mode), a Git interface (Magit), a terminal emulator, a file manager, an RSS reader, and much more.

AI can be integrated at every one of these layers. Imagine an AI assistant that can read your org-mode agenda, draft email replies in mu4e, help you write commit messages in Magit, and refactor code in your source buffers – all within the same environment, sharing context. No other editor architecture makes this kind of deep, cross-domain integration as natural as Emacs does.

I stopped using Emacs as my OS a long time ago, and these days I use it mostly for programming and blogging. (I’m writing this article in Emacs with the help of markdown-mode.) Still, I’m only one Emacs user and many are probably using it in a more holistic manner.

Even in the post-coding apocalypse, Emacs and Vim survive

Let’s revisit the doomsday scenario. Say programming is fully automated and nobody writes code anymore. Does Emacs die?

Not necessarily. Emacs is already used for far more than programming. People use Org mode to manage their entire lives – tasks, notes, calendars, journals, time tracking, even academic papers. Emacs is a capable writing environment for prose, with excellent support for LaTeX, Markdown, AsciiDoc, and plain text. You can read email, browse the web, manage files, and yes, play Tetris.

Vim, similarly, is a text editing paradigm as much as a program. Vim keybindings have colonized every text input in the computing world – VS Code, IntelliJ, browsers, shells, even Emacs (via Evil mode). Even if the Vim program fades, the Vim idea is immortal.4

And who knows – maybe there’ll be a market for artisanal, hand-crafted software one day, the way there’s a market for vinyl records and mechanical watches.5 “Organic, small-batch code, lovingly typed by a human in Emacs – one character at a time.” I’d buy that t-shirt. And I’m fairly certain those artisan programmers won’t be using VS Code.

So even in the most extreme scenario, both editors have a life beyond code. A diminished one, perhaps, but a life nonetheless.

The Bigger Picture

I think what’s actually happening is more interesting than “editors die” or “editors are fine.” The role of the editor is shifting.

For decades, the editor was where you wrote code. Increasingly, it’s becoming where you review, steer, and refine code that AI writes. The skills that matter are shifting from typing speed and editing gymnastics to specification clarity, code reading, and architectural judgment.

In this world, the editor that wins isn’t the one with the best code completion – it’s the one that gives you the most control over your workflow. And that has always been Emacs and Vim’s core value proposition.

The question is whether the communities can adapt fast enough. The tools are there. The architecture is there. The philosophy is right. What’s needed is people – more contributors, more plugin authors, more documentation writers, more voices in the conversation. AI can help bridge the gap, but it can’t replace genuine community engagement.

The Ethical Elephant in the Room

Not everyone in the Emacs and Vim communities is enthusiastic about AI, and the objections go beyond mere technophobia. There are legitimate ethical concerns that are going to be debated for a long time:

  • Energy consumption. Training and running large language models requires enormous amounts of compute and electricity. For communities that have long valued efficiency and minimalism – Emacs users who pride themselves on running a 40-year-old editor, Vim users who boast about their sub-second startup times – the environmental cost of AI is hard to ignore.

  • Copyright and training data. LLMs are trained on vast corpora of code and text, and the legality and ethics of that training remain contested. Some developers are uncomfortable using tools that may have learned from copyrighted code without explicit consent. This concern hits close to home for open-source communities that care deeply about licensing.

  • Job displacement. If AI makes developers significantly more productive, fewer developers might be needed. This is an uncomfortable thought for any programming community, and it’s especially pointed for editors whose identity is built around empowering human programmers.

These concerns are already producing concrete action. The Vim community recently saw the creation of EVi, a fork of Vim whose entire raison d’etre is to provide a text editor free from AI-assisted (generated?) code contributions.6 Whether you agree with the premise or not, the fact that people are forking established editors over this tells you how strongly some community members feel.

I don’t think these concerns should stop anyone from exploring AI tools, but they’re real and worth taking seriously. I expect to see plenty of spirited debate about this on emacs-devel and the Neovim issue tracker in the years ahead.

Closing Thoughts

The future ain’t what it used to be.

– Yogi Berra

I won’t pretend I’m not worried. The AI wave is moving fast, the incumbents have massive advantages in funding and mindshare, and the very nature of programming is shifting under our feet. It’s entirely possible that Emacs and Vim will gradually fade into niche obscurity, used only by a handful of diehards who refuse to move on.

What keeps these editors alive isn’t stubbornness – it’s adaptability. The communities are small but passionate, the editors are more capable than ever, and the architecture is genuinely well-suited to the AI era. Vim’s core idea is so powerful that it keeps finding new expression (Neovim being the most vigorous one). And Emacs? Emacs just keeps absorbing whatever the world throws at it. It always has.

The editors that survive won’t be the ones with the flashiest AI features. They’ll be the ones whose users care enough to keep building, adapting, and sharing. That’s always been the real engine of open-source software, and no amount of AI changes that.

So if you’re an Emacs or Vim user: don’t panic, but don’t be complacent either. Learn the new AI tools (if you’re not fundamentally opposed to them, that is). Pimp your setup and make it awesome. Write about your workflows. Help newcomers. The best way to ensure your editor survives the AI age is to make it thrive in it.

Maybe the future ain’t what it used to be – but that’s not necessarily a bad thing.

This essay covered more ground than I originally intended – lots of thoughts have been rattling around in my head for a while now, and I wanted to get them all out. Programming may be hard, but writing prose remains harder. Still, I hope some of these ideas resonated with you.

That’s all I have for you today. Keep hacking!

P.S. There’s an interesting Hacker News discussion about this article. Check it out if you want to see what the broader community thinks!

  1. If you’re curious about my Vim adventures, I wrote about them in Learning Vim in 3 Steps↩︎

  2. Not to mention you’ll probably have to put in several years in Emacs before you’re actually more productive than you were with your old editor/IDE of choice. ↩︎

  3. At least some of the time. Admittedly I usually use Emacs in GUI mode, but I always use (Neo)vim in the terminal. ↩︎

  4. Even Claude Code has vim mode. ↩︎

  5. I’m a big fan of mechanical watches myself, so I might be biased here. There’s something deeply satisfying about a beautifully crafted mechanism that doesn’t need a battery or an internet connection to work. ↩︎

  6. See https://codeberg.org/NerdNextDoor/evi/issues/1↩︎

]]>
Bozhidar Batsov
Building Emacs Major Modes with Tree-sitter: Lessons Learned2026-02-27T10:00:00+02:002026-02-27T10:00:00+02:00https://batsov.com/articles/2026/02/27/building-emacs-major-modes-with-treesitter-lessons-learnedOver the past year I’ve been spending a lot of time building Tree-sitter-powered major modes for Emacs – clojure-ts-mode (as co-maintainer), neocaml (from scratch), and asciidoc-mode (also from scratch). Between the three projects I’ve accumulated enough knowledge (and battle scars) to write about the experience. This post distills the key lessons for anyone thinking about writing a Tree-sitter-based major mode, or curious about what it’s actually like.

Why Tree-sitter?

Before Tree-sitter, Emacs font-locking was done with regular expressions and indentation was handled by ad-hoc engines (SMIE, custom indent functions, or pure regex heuristics). This works, but it has well-known problems:

  • Regex-based font-locking is fragile. Regexes can’t parse nested structures, so they either under-match (missing valid code) or over-match (highlighting inside strings and comments). Every edge case is another regex, and the patterns become increasingly unreadable over time.

  • Indentation engines are complex. SMIE (the generic indentation engine for non-Tree-sitter modes) requires defining operator precedence grammars for the language, which is hard to get right. Custom indentation functions tend to grow into large, brittle state machines. Tuareg’s indentation code, for example, is thousands of lines long.

Tree-sitter changes the game because you get a full, incremental, error-tolerant syntax tree for free. Font-locking becomes “match this AST pattern, apply this face”:

1
2
3
;; Highlight let-bound functions: match a let_binding with parameters
(let_binding pattern: (value_name) @font-lock-function-name-face
             (parameter)+)

And indentation becomes “if the parent node is X, indent by Y”:

1
2
;; Children of a let_binding are indented by neocaml-indent-offset
((parent-is "let_binding") parent-bol neocaml-indent-offset)

The rules are declarative, composable, and much easier to reason about than regex chains.

In practice, neocaml’s entire font-lock and indentation logic fits in about 350 lines of Elisp. The equivalent in tuareg is spread across thousands of lines. That’s the real selling point: simpler, more maintainable code that handles more edge cases correctly.

Challenges

That said, Tree-sitter in Emacs is not a silver bullet. Here’s what I ran into.

Every grammar is different

Tree-sitter grammars are written by different authors with different philosophies. The tree-sitter-ocaml grammar provides a rich, detailed AST with named fields. The tree-sitter-clojure grammar, by contrast, deliberately keeps things minimal – it only models syntax, not semantics, because Clojure’s macro system makes static semantic analysis unreliable.1 This means font-locking def forms in Clojure requires predicate matching on symbol text, while in OCaml you can directly match let_binding nodes with named fields.

To illustrate: here’s how you’d fontify a function definition in OCaml, where the grammar gives you rich named fields:

1
2
3
;; OCaml: grammar provides named fields -- direct structural match
(let_binding pattern: (value_name) @font-lock-function-name-face
             (parameter)+)

And here’s the equivalent in Clojure, where the grammar only gives you lists of symbols and you need predicate matching:

1
2
3
4
;; Clojure: grammar is syntax-only -- match by symbol text
((list_lit :anchor (sym_lit !namespace
                            name: (sym_name) @font-lock-keyword-face))
 (:match ,clojure-ts--definition-keyword-regexp @font-lock-keyword-face))

You can’t learn “how to write Tree-sitter queries” generically – you need to learn each grammar individually. The best tool for this is treesit-explore-mode (to visualize the full parse tree) and treesit-inspect-mode (to see the node at point). Use them constantly.

Grammar quality varies wildly

You’re dependent on someone else providing the grammar, and quality is all over the map. The OCaml grammar is mature and well-maintained – it’s hosted under the official tree-sitter GitHub org. The Clojure grammar is small and stable by design. But not every language is so lucky.

asciidoc-mode uses a third-party AsciiDoc grammar that employs a dual-parser architecture – one parser for block-level structure (headings, lists, code blocks) and another for inline formatting (bold, italic, links). This is the same approach used by Emacs’s built-in markdown-ts-mode, and it makes sense for markup languages where block and inline syntax are largely independent.

The problem is that the two parsers run independently on the same text, and they can disagree. The inline parser misinterprets * and ** list markers as emphasis delimiters, creating spurious bold spans that swallow subsequent inline content. The workaround is to use :override t on all block-level font-lock rules so they win over the incorrect inline faces:

1
2
3
4
5
6
7
8
;; Block-level rules use :override t so block-level faces win over
;; spurious inline emphasis nodes (the inline parser misreads `*'
;; list markers as emphasis delimiters).
:language 'asciidoc
:override t
:feature 'list
'((ordered_list_marker) @font-lock-constant-face
  (unordered_list_marker) @font-lock-constant-face)

This doesn’t fix inline elements consumed by the spurious emphasis – that requires an upstream grammar fix. When you hit grammar-level issues like this, you either fix them yourself (which means diving into the grammar’s JavaScript source and C toolchain) or you live with workarounds. Either way, it’s a reminder that your mode is only as good as the grammar underneath it.

Getting the font-locking right in asciidoc-mode was probably the most challenging part of all three projects, precisely because of these grammar quirks. I also ran into a subtle treesit behavior: the default font-lock mode (:override nil) skips an entire captured range if any position within it already has a face. So if you capture a parent node like (inline_macro) and a child was already fontified, the whole thing gets skipped silently. The fix is to capture specific child nodes instead:

1
2
3
4
5
6
;; BAD: entire node gets skipped if any child is already fontified
;; (inline_macro) @font-lock-function-call-face

;; GOOD: capture specific children
(inline_macro (macro_name) @font-lock-function-call-face)
(inline_macro (target) @font-lock-string-face)

These issues took a lot of trial and error to diagnose. The lesson: budget extra time for font-locking when working with less mature grammars.

Grammar versions and breaking changes

Grammars evolve, and breaking changes happen. clojure-ts-mode switched from the stable grammar to the experimental branch because the stable version had metadata nodes as children of other nodes, which caused forward-sexp and kill-sexp to behave incorrectly. The experimental grammar makes metadata standalone nodes, fixing the navigation issues but requiring all queries to be updated.

neocaml pins to v0.24.0 of the OCaml grammar. If you don’t pin versions, a grammar update can silently break your font-locking or indentation.

The takeaway: always pin your grammar version, and include a mechanism to detect outdated grammars. clojure-ts-mode tests a query that changed between versions to detect incompatible grammars at startup.

Grammar delivery

Users shouldn’t have to manually clone repos and compile C code to use your mode. Both neocaml and clojure-ts-mode include grammar recipes:

1
2
3
4
5
6
7
(defconst neocaml-grammar-recipes
  '((ocaml "https://github.com/tree-sitter/tree-sitter-ocaml"
           "v0.24.0"
           "grammars/ocaml/src")
    (ocaml-interface "https://github.com/tree-sitter/tree-sitter-ocaml"
                     "v0.24.0"
                     "grammars/interface/src")))

On first use, the mode checks treesit-language-available-p and offers to install missing grammars via treesit-install-language-grammar. This works, but requires a C compiler and Git on the user’s machine, which is not ideal.2

The Emacs Tree-sitter APIs are a moving target

The Tree-sitter support in Emacs has been improving steadily, but each version has its quirks:

Emacs 29 introduced Tree-sitter support but lacked several APIs. For instance, treesit-thing-settings (used for structured navigation) doesn’t exist – you need a fallback:

1
2
3
;; Fallback for Emacs 29 (no treesit-thing-settings)
(unless (boundp 'treesit-thing-settings)
  (setq-local forward-sexp-function #'neocaml-forward-sexp))

Emacs 30 added treesit-thing-settings, sentence navigation, and better indentation support. But it also had a bug in treesit-range-settings offsets (#77848) that broke embedded parsers, and another in treesit-transpose-sexps that required clojure-ts-mode to disable its Tree-sitter-aware version.

Emacs 31 has a bug in treesit-forward-comment where an off-by-one error causes uncomment-region to leave *) behind on multi-line OCaml comments. I had to skip the affected test with a version check:

1
2
3
(when (>= emacs-major-version 31)
  (signal 'buttercup-pending
          "Emacs 31 treesit-forward-comment bug (off-by-one)"))

The lesson: test your mode against multiple Emacs versions, and be prepared to write version-specific workarounds. CI that runs against Emacs 29, 30, and snapshot is essential.

No .scm file support (yet)

Most Tree-sitter grammars ship with .scm query files for syntax highlighting (highlights.scm) and indentation (indents.scm). Editors like Neovim and Helix use these directly. Emacs doesn’t – you have to manually translate the .scm patterns into treesit-font-lock-rules and treesit-simple-indent-rules calls in Elisp.

This is tedious and error-prone. For example, here’s a rule from the OCaml grammar’s highlights.scm:

1
2
;; upstream .scm (used by Neovim, Helix, etc.)
(constructor_name) @type

And here’s the Elisp equivalent you’d write for Emacs:

1
2
3
4
;; Emacs equivalent -- wrapped in treesit-font-lock-rules
:language 'ocaml
:feature 'type
'((constructor_name) @font-lock-type-face)

The query syntax is nearly identical, but you have to wrap everything in treesit-font-lock-rules calls, map upstream capture names (@type) to Emacs face names (@font-lock-type-face), assign features, and manage :override behavior. You end up maintaining a parallel set of queries that can drift from upstream. Emacs 31 will introduce define-treesit-generic-mode which will make it possible to use .scm files for font-locking, which should help significantly. But for now, you’re hand-coding everything.

Tips and tricks

Debugging font-locking

When a face isn’t being applied where you expect:

  1. Use treesit-inspect-mode to verify the node type at point matches your query.
  2. Set treesit--font-lock-verbose to t to see which rules are firing.
  3. Check the font-lock feature level – your rule might be in level 4 while the user has the default level 3. The features are assigned to levels via treesit-font-lock-feature-list.
  4. Remember that rule order matters. Without :override, an earlier rule that already fontified a region will prevent later rules from applying. This can be intentional (e.g. builtin types at level 3 take precedence over generic types) or a source of bugs.

Use the font-lock levels wisely

Tree-sitter modes define four levels of font-locking via treesit-font-lock-feature-list, and the default level in Emacs is 3. It’s tempting to pile everything into levels 1–3 so users see maximum highlighting out of the box, but resist the urge. When every token on the screen has a different color, code starts looking like a Christmas tree and the important things – keywords, definitions, types – stop standing out.

Less is more here. Here’s how neocaml distributes features across levels:

1
2
3
4
5
(setq-local treesit-font-lock-feature-list
            '((comment definition)
              (keyword string number)
              (attribute builtin constant type)
              (operator bracket delimiter variable function)))

And clojure-ts-mode follows the same philosophy:

1
2
3
4
5
(setq-local treesit-font-lock-feature-list
            '((comment definition)
              (keyword string char symbol builtin type)
              (constant number quote metadata doc regex)
              (bracket deref function tagged-literals)))

The pattern is the same: essentials first, progressively more detail at higher levels. This way the default experience (level 3) is clean and readable, and users who want the full rainbow can bump treesit-font-lock-level to 4. Better yet, they can use treesit-font-lock-recompute-features to cherry-pick individual features regardless of level:

1
2
3
4
5
;; Enable 'function' (level 4) without enabling all of level 4
(treesit-font-lock-recompute-features '(function) nil)

;; Disable 'bracket' even if the user's level would include it
(treesit-font-lock-recompute-features nil '(bracket))

This gives users fine-grained control without requiring mode authors to anticipate every preference.

Debugging indentation

Indentation issues are harder to diagnose because they depend on tree structure, rule ordering, and anchor resolution:

  1. Set treesit--indent-verbose to t – this logs which rule matched for each line, what anchor was computed, and the final column.
  2. Use treesit-explore-mode to understand the parent chain. The key question is always: “what is the parent node, and which rule matches it?”
  3. Remember that rule order matters for indentation too – the first matching rule wins. A typical set of rules reads top to bottom from most specific to most general:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
    ;; Closing delimiters align with the opening construct
    ((node-is ")") parent-bol 0)
    ((node-is "end") parent-bol 0)
    
    ;; then/else clauses align with their enclosing if
    ((node-is "then_clause") parent-bol 0)
    ((node-is "else_clause") parent-bol 0)
    
    ;; Bodies inside then/else are indented
    ((parent-is "then_clause") parent-bol neocaml-indent-offset)
    ((parent-is "else_clause") parent-bol neocaml-indent-offset)
    
  4. Watch out for the empty-line problem: when the cursor is on a blank line, Tree-sitter has no node at point. The indentation engine falls back to the root compilation_unit node as the parent, which typically matches the top-level rule and gives column 0. In neocaml I solved this with a no-node rule that looks at the previous line’s last token to decide indentation:

    1
    
    (no-node prev-line neocaml--empty-line-offset)
    

Build a comprehensive test suite

This is the single most important piece of advice. Font-lock and indentation are easy to break accidentally, and manual testing doesn’t scale. Both projects use Buttercup (a BDD testing framework for Emacs) with custom test macros.

Font-lock tests insert code into a buffer, run font-lock-ensure, and assert that specific character ranges have the expected face:

1
2
3
(when-fontifying-it "fontifies let-bound functions"
  ("let greet name = ..."
   (5 9 font-lock-function-name-face)))

Indentation tests insert code, run indent-region, and assert the result matches the expected indentation:

1
2
3
4
(when-indenting-it "indents a match expression"
  "match x with"
  "| 0 -> \"zero\""
  "| n -> string_of_int n")

Integration tests load real source files and verify that both font-locking and indentation survive indent-region on the full file. This catches interactions between rules that unit tests miss.

neocaml has 200+ automated tests and clojure-ts-mode has even more. Investing in test infrastructure early pays off enormously – I can refactor indentation rules with confidence because the suite catches regressions immediately.

A personal story on testing ROI

When I became the maintainer of clojure-mode many years ago, I really struggled with making changes. There were no font-lock or indentation tests, so every change was a leap of faith – you’d fix one thing and break three others without knowing until someone filed a bug report. I spent years working on a testing approach I was happy with, alongside many great contributors, and the return on investment was massive.

The same approach – almost the same test macros – carried over directly to clojure-ts-mode when we built the Tree-sitter version. And later I reused the pattern again in neocaml and asciidoc-mode. One investment in testing infrastructure, four projects benefiting from it.

I know that automated tests, for whatever reason, never gained much traction in the Emacs community. Many popular packages have no tests at all. I hope stories like this convince you that investing in tests is really important and pays off – not just for the project where you write them, but for every project you build after.

Pre-compile queries

This one is specific to clojure-ts-mode but applies broadly: compiling Tree-sitter queries at runtime is expensive. If you’re building queries dynamically (e.g. with treesit-font-lock-rules called at mode init time), consider pre-compiling them as defconst values. This made a noticeable difference in clojure-ts-mode’s startup time.

A note on naming

The Emacs community has settled on a -ts-mode suffix convention for Tree-sitter-based modes: python-ts-mode, c-ts-mode, ruby-ts-mode, and so on. This makes sense when both a legacy mode and a Tree-sitter mode coexist in Emacs core – users need to choose between them. But I think the convention is being applied too broadly, and I’m afraid the resulting name fragmentation will haunt the community for years.

For new packages that don’t have a legacy counterpart, the -ts-mode suffix is unnecessary. I named my packages neocaml (not ocaml-ts-mode) and asciidoc-mode (not adoc-ts-mode) because there was no prior neocaml-mode or asciidoc-mode to disambiguate from. The -ts- infix is an implementation detail that shouldn’t leak into the user-facing name. Will we rename everything again when Tree-sitter becomes the default and the non-TS variants are removed?

Be bolder with naming. If you’re building something new, give it a name that makes sense on its own merits, not one that encodes the parsing technology in the package name.

The road ahead

I think the full transition to Tree-sitter in the Emacs community will take 3–5 years, optimistically. There are hundreds of major modes out there, many maintained by a single person in their spare time. Converting a mode from regex to Tree-sitter isn’t just a mechanical translation – you need to understand the grammar, rewrite font-lock and indentation rules, handle version compatibility, and build a new test suite. That’s a lot of work.

Interestingly, this might be one area where agentic coding tools can genuinely help. The structure of Tree-sitter-based major modes is fairly uniform: grammar recipes, font-lock rules, indentation rules, navigation settings, imenu. If you give an AI agent a grammar and a reference to a high-quality mode like clojure-ts-mode, it could probably scaffold a reasonable new mode fairly quickly. The hard parts – debugging grammar quirks, handling edge cases, getting indentation just right – would still need human attention, but the boilerplate could be automated.

Still, knowing the Emacs community, I wouldn’t be surprised if a full migration never actually completes. Many old-school modes work perfectly fine, their maintainers have no interest in Tree-sitter, and “if it ain’t broke, don’t fix it” is a powerful force. And that’s okay – diversity of approaches is part of what makes Emacs Emacs.

Closing thoughts

Tree-sitter is genuinely great for building Emacs major modes. The code is simpler, the results are more accurate, and incremental parsing means everything stays fast even on large files. I wouldn’t go back to regex-based font-locking willingly.

But it’s not magical. Grammars are inconsistent across languages, the Emacs APIs are still maturing, you can’t reuse .scm files (yet), and you’ll hit version-specific bugs that require tedious workarounds. The testing story is better than with regex modes – tree structures are more predictable than regex matches – but you still need a solid test suite to avoid regressions.

If you’re thinking about writing a Tree-sitter-based major mode, do it. The ecosystem needs more of them, and the experience of working with syntax trees instead of regexes is genuinely enjoyable. Just go in with realistic expectations, pin your grammar versions, test against multiple Emacs releases, and build your test suite early.

Anyways, I wish there was an article like this one when I was starting out with clojure-ts-mode and neocaml, so there you have it. I hope that the lessons I’ve learned along the way will help build better modes with Tree-sitter down the road.

That’s all I have for you today. Keep hacking!

  1. See the excellent scope discussion in the tree-sitter-clojure repo for the rationale. ↩︎

  2. There’s ongoing discussion in the Emacs community about distributing pre-compiled grammar binaries, but nothing concrete yet. ↩︎

]]>
Bozhidar Batsov
Setting up Emacs for OCaml Development: Neocaml Edition2026-02-24T12:00:00+02:002026-02-24T12:00:00+02:00https://batsov.com/articles/2026/02/24/setting-up-emacs-for-ocaml-development-neocaml-editionA few years ago I wrote about setting up Emacs for OCaml development. Back then the recommended stack was tuareg-mode + merlin-mode, with Merlin providing the bulk of the IDE experience. A lot has changed since then – the OCaml tooling has evolved considerably, and I’ve been working on some new tools myself. Time for an update.

The New Stack

Here’s what I recommend today:

The key shift is from Merlin’s custom protocol to LSP. ocaml-lsp-server has matured significantly since my original article – it’s no longer a thin wrapper around Merlin. It now offers project-wide renaming, semantic highlighting, Dune RPC integration, and OCaml-specific extensions like pattern match generation and typed holes. ocaml-eglot is a lightweight Emacs package by Tarides that bridges Eglot with these OCaml-specific LSP extensions, giving you the full Merlin feature set through a standardized protocol.

And neocaml is my own TreeSitter-powered OCaml major mode – modern, lean, and built for the LSP era. You can read more about it in the 0.1 release announcement.

The Essentials

First, install the server-side tools:

1
$ opam install ocaml-lsp-server

You no longer need to install merlin separately – ocaml-lsp-server vendors it internally.

Then set up Emacs:

1
2
3
4
5
6
7
8
9
10
11
12
;; Modern TreeSitter-powered OCaml major mode
(use-package neocaml
  :ensure t)

;; Major mode for editing Dune project files
(use-package dune
  :ensure t)

;; OCaml-specific LSP extensions via Eglot
(use-package ocaml-eglot
  :ensure t
  :hook (neocaml-mode . ocaml-eglot-setup))

That’s it. Eglot ships with Emacs 29+, so there’s nothing extra to install for the LSP client itself. When you open an OCaml file, Eglot will automatically start ocaml-lsp-server and you’ll have completion, type information, code navigation, diagnostics, and all the other goodies you’d expect.

Compare this to the old setup – no more merlin-mode, merlin-eldoc, flycheck-ocaml, or manual Company configuration. LSP handles all of it through a single, uniform interface.

The Toplevel

neocaml includes built-in REPL integration via neocaml-repl-minor-mode. The basics work well:

  • C-c C-z – start or switch to the OCaml toplevel
  • C-c C-c – send the current definition
  • C-c C-r – send the selected region
  • C-c C-b – send the entire buffer

If you want utop specifically, you’re still better off using utop.el alongside neocaml. Its main advantage is that you get code completion inside the utop REPL within Emacs – something neocaml’s built-in REPL integration doesn’t provide:

1
2
3
(use-package utop
  :ensure t
  :hook (neocaml-mode . utop-minor-mode))

This will shadow neocaml’s REPL keybindings with utop’s, which is the intended behavior.

That said, as I’ve grown more comfortable with OCaml I find myself using the toplevel less and less. These days I rely more on a test-driven workflow – write a test, run it, iterate. In particular I’m partial to the workflow described in this OCaml Discuss thread – running dune runtest continuously and writing expect tests for quick feedback. It’s a more structured approach that scales better than REPL-driven development, especially as your projects grow.

Give neocaml a Try

If you’re an OCaml programmer using Emacs, I’d love for you to take neocaml for a spin. It’s available on MELPA, so getting started is just an M-x package-install away. The project is still young and I’m actively working on it – your feedback, bug reports, and pull requests are invaluable. Let me know what works, what doesn’t, and what you’d like to see next.

That’s all I have for you today. Keep hacking!

]]>
Bozhidar Batsov
Learning Vim in 3 Steps2026-02-24T11:00:00+02:002026-02-24T11:00:00+02:00https://batsov.com/articles/2026/02/24/learning-vim-in-3-stepsEvery now and then someone asks me how to learn Vim.1 My answer is always the same: it’s simpler than you think, but it takes longer than you’d like. Here’s my bulletproof 3-step plan.

Step 1: Learn the Basics

Start with vimtutor – it ships with Vim and takes about 30 minutes. It’ll teach you enough to survive: moving around, editing text, saving, quitting. The essentials.

Once you’re past that, I strongly recommend Practical Vim by Drew Neil. This book changed the way I think about Vim. I had known the basics of Vim for over 20 years, but the Vim editing model never really clicked for me until I read it. The key insight is that Vim has a grammar – operators (verbs) combine with motions (nouns) to form commands. d (delete) + w (word) = dw. c (change) + i" (inside quotes) = ci". Once you internalize this composable language, you stop memorizing individual commands and start thinking in Vim. The book is structured as 121 self-contained tips rather than a linear tutorial, which makes it great for dipping in and out.

You could also just read :help cover to cover – Vim’s built-in documentation is excellent. But let’s be honest, few people have that kind of patience.

Other resources worth checking out:

  • Advent of Vim – a playlist of short video tutorials covering basic Vim topics. Great for visual learners who prefer bite-sized lessons.
  • ThePrimeagen’s Vim Fundamentals – if you prefer video content and a more energetic teaching style.
  • vim-be-good – a Neovim plugin that gamifies Vim practice. Good for building muscle memory.

Step 2: Start Small

Resist the temptation to grab a massive Neovim distribution like LazyVim on day one. You’ll find it overwhelming if you don’t understand the basics and don’t know how the Vim/Neovim plugin ecosystem works. It’s like trying to drive a race car before you’ve learned how a clutch works.

Instead, start with a minimal configuration and grow it gradually. I wrote about this in detail in Build your .vimrc from Scratch – the short version is that modern Vim and Neovim ship with excellent defaults and you can get surprisingly far with a handful of settings.

I’m a tinkerer by nature. I like to understand how my tools operate at their fundamental level, and I always take that approach when learning something new. Building your config piece by piece means you understand every line in it, and when something breaks you know exactly where to look.

Step 3: Practice for 10 Years

I’m only half joking. Peter Norvig’s famous essay Teach Yourself Programming in Ten Years makes the case that mastering any complex skill requires sustained, deliberate practice over a long period – not a weekend crash course. The same applies to Vim.

Grow your configuration one setting at a time. Learn Vimscript (or Lua if you’re on Neovim). Read other people’s configs. Maybe write a small plugin. Every month you’ll discover some built-in feature or clever trick that makes you wonder how you ever lived without it.

One of the reasons I chose Emacs over Vim back in the day was that I really hated Vimscript – it was a terrible language to write anything in. These days the situation is much better: Vim9 Script is a significant improvement, and Neovim’s switch to Lua makes building configs and plugins genuinely enjoyable.

Mastering an editor like Vim is a lifelong journey. Then again, the way things are going with LLM-assisted coding, maybe you should think long and hard about whether you want to commit your life to learning an editor when half the industry is “programming” without one. But that’s a rant for another day.

Plan B

If this bulletproof plan doesn’t work out for you, there’s always Emacs. Over 20 years in and I’m still learning new things – these days mostly how to make the best of evil-mode so I can have the best of both worlds. As I like to say:

The road to Emacs mastery is paved with a lifetime of M-x invocations.

That’s all I have for you today. Keep hacking!

  1. Just kidding – everyone asks me about learning Emacs. But here we are. ↩︎

]]>
Bozhidar Batsov
Neocaml 0.1: Ready for Action2026-02-14T18:34:00+02:002026-02-14T18:34:00+02:00https://batsov.com/articles/2026/02/14/neocaml-0-1-ready-for-actionneocaml 0.1 is finally out! Almost a year after I announced the project, I’m happy to report that it has matured to the point where I feel comfortable calling it ready for action. Even better - neocaml recently landed in MELPA, which means installing it is now as easy as:

1
M-x package-install <RET> neocaml <RET>

That’s quite the journey from “a fun experimental project” to a proper Emacs package!

Why neocaml?

You might be wondering what’s wrong with the existing options. The short answer - nothing is wrong per se, but neocaml offers a different set of trade-offs:

  • caml-mode is ancient and barely maintained. It lacks many features that modern Emacs users expect and it probably should have been deprecated a long time ago.
  • tuareg-mode is very powerful, but also very complex. It carries a lot of legacy code and its regex-based font-locking and custom indentation engine show their age. It’s a beast - in both the good and the bad sense of the word.
  • neocaml aims to be a modern, lean alternative that fully embraces TreeSitter. The codebase is small, well-documented, and easy to hack on. If you’re running Emacs 29+ (and especially Emacs 30), TreeSitter is the future and neocaml is built entirely around it.

Of course, neocaml is the youngest of the bunch and it doesn’t yet match Tuareg’s feature completeness. But for many OCaml workflows it’s already more than sufficient, especially when combined with LSP support.

I’ve started the project mostly because I thought that the existing Emacs tooling for OCaml was somewhat behind the times - e.g. both caml-mode and tuareg-mode have features that are no longer needed in the era of ocamllsp.

Let me now walk you through the highlights of version 0.1.

Features

The current feature-set is relatively modest, but all the essential functionality one would expect from an Emacs major mode is there.

TreeSitter-powered Syntax Highlighting

neocaml leverages TreeSitter for syntax highlighting, which is both more accurate and more performant than the traditional regex-based approaches used by caml-mode and tuareg-mode. The font-locking supports 4 customizable intensity levels (controlled via treesit-font-lock-level, default 3), so you can pick the amount of color that suits your taste.

Both .ml (source) and .mli (interface) files get their own major modes with dedicated highlighting rules.

TreeSitter-powered Indentation

Indentation has always been tricky for OCaml modes, and I won’t pretend it’s perfect yet, but neocaml’s TreeSitter-based indentation engine is already quite usable. It also supports cycle-indent functionality, so hitting TAB repeatedly will cycle through plausible indentation levels - a nice quality-of-life feature when the indentation rules can’t fully determine the “right” indent.

If you prefer, you can still delegate indentation to external tools like ocp-indent or even Tuareg’s indentation functions. Still, I think most people will be quite satisfied with the built-in indentation logic.

Code Navigation and Imenu

neocaml provides proper structural navigation commands (beginning-of-defun, end-of-defun, forward-sexp) powered by TreeSitter, plus imenu integration definitions in a buffer has never been easier.

The older modes provide very similar functionality as well, of course, but the use of TreeSitter in neocaml makes such commands more reliable and robust.

REPL Integration

No OCaml mode would be complete without REPL (toplevel) integration. neocaml-repl-minor-mode provides all the essentials:

  • C-c C-z - Start or switch to the OCaml REPL
  • C-c C-c - Send the current definition
  • C-c C-r - Send the selected region
  • C-c C-b - Send the entire buffer
  • C-c C-p - Send a phrase (code until ;;)

The default REPL is ocaml, but you can easily switch to utop via neocaml-repl-program-name.

I’m still on the fence on whether I want to invest time into making the REPL-integration more powerful or keep it as simple as possible. Right now it’s definitely not a big priority for me, but I want to match what the other older OCaml modes offered in that regard.

LSP Support

neocaml works great with Eglot and ocamllsp, automatically setting the appropriate language IDs for both .ml and .mli files. Pair neocaml with ocaml-eglot and you get a pretty solid OCaml development experience.

The creation of LSP really simplified the lives of a major mode authors like me, as now many of the features that were historically major mode specific are provided by LSP clients out-of-the-box.

That’s also another reason why you probably want to leaner major mode like neocaml-mode.

Other Goodies

But, wait, there’s more!

  • C-c C-a to quickly switch between .ml and .mli files
  • Prettify-symbols support for common OCaml operators
  • Automatic installation of the required TreeSitter grammars via M-x neocaml-install-grammars
  • Compatibility with Merlin for those who prefer it over LSP

The Road Ahead

There’s still plenty of work to do:

  • Support for additional OCaml file types (e.g. .mld)
  • Improvements to structured navigation using newer Emacs TreeSitter APIs
  • Improvements to the test suite
  • Addressing feedback from real-world OCaml users
  • Actually writing some fun OCaml code with neocaml

If you’re following me, you probably know that I’m passionate about both Emacs and OCaml. I hope that neocaml will be my way to contribute to the awesome OCaml community.

I’m not sure how quickly things will move, but I’m committed to making neocaml the best OCaml editing experience on Emacs. Time will tell how far I’ll get!

Give it a Try

If you’re an OCaml programmer using Emacs, I’d love for you to take neocaml for a spin. Install it from MELPA, kick the tires, and let me know what you think. Bug reports, feature requests, and pull requests are all most welcome on GitHub!

That’s all from me, folks! Keep hacking!

]]>
Bozhidar Batsov
Burst-driven Development: My Approach to OSS Projects Maintenance2025-11-16T14:16:00+02:002025-11-19T00:00:00+02:00https://batsov.com/articles/2025/11/16/burst-driven-development-my-approach-to-oss-projects-maintenanceI’ve been working on OSS projects for almost 15 years now. Things are simple in the beginning - you’ve got a single project, no users to worry about and all the time and the focus in the world. Things changed quite a bit for me over the years and today I’m the maintainer of a couple of dozen OSS projects in the realms of Emacs, Clojure and Ruby mostly. Some of them even happen to have many users!

People often ask me how I manage to work on so many projects, besides having a day job, that obviously takes up most of my time. My recipe is quite simple and I refer to it as “burst-driven development”. Long ago I’ve realized that it’s totally unsustainable for me to work effectively in parallel on several quite different projects. That’s why I normally keep a closer eye on my bigger projects (e.g. RuboCop, CIDER, Projectile and nREPL), where I try to respond quickly to tickets and PRs, while I typically do (focused) development only on 1-2 projects at a time.

There are often (long) periods when I barely check a project, only to suddenly decide to revisit it and hack vigorously on it for several days or weeks. I guess that’s not ideal for the end users, as some of them might feel that I “undermaintain” some (smaller) projects much of the time, but this approach has worked for me very well for quite a while.

The time I’ve spent developing OSS projects has taught me that:

  • few problems require some immediate action
  • you can’t always have good ideas for how to improve a project
  • sometimes a project is simply mostly done and that’s OK
  • less is more
  • “hammock time” is important

To illustrate all of the above with some example, let me tell you a bit about copilot.el 0.3. I became the primary maintainer of copilot.el about 9 months ago. Initially there were many things about the project that were frustrating to me that I wanted to fix and improve. After a month of relatively focused work I had mostly achieved my initial goals and I’ve put the project on the backburner for a while, although I kept reviewing PRs and thinking about it in the background. Today I remembered I hadn’t done a release there in quite a while and copilot.el 0.3 was born. Tomorrow I might remember about some features in Projectile that have been in the back of my mind for ages and finally implement them. Or not. I don’t have any planned order in which I revisit my projects - I just go wherever my inspiration (or current problems related to the projects) take me.

Back in the day (before the pandemic) I was also practicing a variant of burst-driven development that I was calling “conference-driven development”. I was speaking often at conferences back then and I usually do a lot of focused development on some project leading to a conference where I’d present something about it. E.g.:

  • Before Clojure conferences I’d usually focus on improvements in CIDER and nREPL
  • Before Ruby conferences I’d focus on RuboCop

I hope you get the idea. Even if my talks were not directly related to my OSS projects I liked to announce new releases and exciting new developments at conference talks. Sadly, I’ve barely spoken at any conferences since 2020, so conference-driven development doesn’t happen these days. Perhaps it will make a comeback in the future…

And that’s a wrap. Nothing novel here, but I hope some of you will find it useful to know how do I approach the topic of multi-project maintenance overall. The “job” of the maintainers is sometimes fun, sometimes tiresome and boring, and occasionally it’s quite frustrating. That’s why it’s essential to have a game plan for dealing with it that doesn’t take a heavy toll on you and make you eventually hate the projects that you lovingly developed in the past. Keep hacking!

]]>
Bozhidar Batsov
Rediscovering Vim2025-05-20T18:09:00+03:002025-05-25T00:00:00+03:00https://batsov.com/articles/2025/05/20/rediscovering-vimMost people who know me and follow my open-source work are likely going to be surprised that I’m writing an article about Vim. After all, I’ve been a devout member of the Church of Emacs for 20 years now, and I’ve spent a lot of time preaching the gospel of Emacs and building extensions for Emacs.

I think a lot fewer people know that early on in my career I was using Vim briefly, before switching to Emacs in 2005. In the beginning of this year I felt it was a good time to revisit Vim and (neovim) and see how they’ve evolved compared to my beloved Emacs. It’s never a bad idea to keep an eye on the competition - they are always a great source of “inspiration”.

The main reason why I abandoned Vim back in the day is quite simple - I really dislike Vim script. It was a horrible language back then and I think it’s still a horrible language today.1 I’ll admit that the rise of neovim (and its switch to Lua) was probably the main reason why the thought of revisiting Vim after so many years even crossed my mind. There’s also the fact that it remains the only other editor (besides Emacs), that’s essentially building material for those of us crazy enough to want to build their own editor. Most people don’t care about that, but for me that’s very precious.

Starting with Vim is always intimidating, even if you kind of know the basics. The configuration options are countless and the Vim plugin ecosystem is huge. :help is your friend!

Initially, I focused on neovim for a couple of reasons:

  • Lua for the configuration and the plugins
  • Built-in support for things like LSP and Tree-sitter
  • Many distros and a ton of “modern” plugins

I spent a lot of time with LazyVim (and a bit with Kickstart.nvim) and while I was mostly OK with them, I didn’t like the process of starting with a huge configuration and working from there to make it my own. So, after a bit of soul-searching I’ve decided to start at the very beginning with Vim 9 and a blank .vimrc and work my way up from there, same as I did with Emacs 20 years ago.

For a while I didn’t use any plugins at all, apart from a handful of plugins bundled with Vim. Eventually I had to install a few plugins as Vim is so spartan out-of-the-box, compared to most other editors. I was reminded that you need plugins for basic things like:

  • VCS
  • commenting out stuff
  • surrounding stuff in paired delimiters
  • working with paired delimiters in general (e.g. auto-insert/skip closing quotation marks or brackets)

Frankly, I’m a bit shocked how those things never made their way to Vim proper, but I guess there’s some reason for this. Emacs is often considered to be a super-conservative and slow-moving editor, but it almost feels progressive compared to Vim.2

I was also a bit confused by the native Vim 8 package system3, as it’s little more than some predefined filesystem locations and structure for the Vim packages. I had expected something closer to Emacs’s package.el. Anyways vim-plug is pretty good and there are a ton of other package managers out there.

Some other pet peeves I have with Vim are:

  • seems there’s no way to configure which types of split commands like :help will use (on widescreen displays I always prefer vertical splits)
  • lots of commands that shell out essentially take you out of the editor and you have to press some key (e.g. Enter) to proceed back
  • configuration options with weird names like “wildmenu” - good luck figuring out what this is without consulting the help!

So, what am I doing with Vim these days? I’m working my way through the classic book “Practical Vim” and gradually building up my modest .vimrc. I’m trying to use Vim for as many tasks as I can (e.g. writing this blog article), but admittedly every time I need to do something more complex I switch back to Emacs. Knowing myself and my habit of always diving deeper than it usually makes sense I’ll likely play a bit with the dreaded Vim script and Vim9 script before revisiting neovim eventually.

The “real” Vim seems to be in a pretty weird place right now:

  • Vim9 is here, but few people are interested in using it, as Vim9 won’t be supported on neovim for obvious reasons
  • As neovim has much better LSP support, and Tree-sitter support the majority of the developers using Vim have switched to neovim already
  • Vim’s future seems quite uncertain after the passing of its author and primary maintainer

Still, for me it’s always fun to learn something new and to challenge myself to work in a different manner. I’ve never really bought the claims that Vim’s modal editing model is better than the competition, but I can’t deny it has a certain charm to it and it often forces you to think about achieving the desired end result with as few keystrokes as possible. I also like the concept of TextObjects (e.g. something between parentheses or a tag) and having an uniform language for interacting with them. neovim really takes this to the next level by adding Tree-sitter objects to the mix. (e.g. blocks, functions, classes, etc)

I’m already at the point where I feel pretty comfortable with the basics and I probably know more about Vim than I ever did. Still, I’m definitely not as productive as I want to be and I often have to pause and think how to adjust my work habits to match the Vim way. One of the areas that I struggle the most with is that it’s very hard to work on multiple projects with the same Vim instance. I know that’s not how most people use Vim, but it’s something I’m quite used to and it’s taking some time to adjust. I’m also struggling with the weird (and very central) file plugin and figuring out how to tweak various settings for different file types. I know that’s subjective, but right now Emacs’s notion of major and minor modes seems pretty fantastic to me in comparison. Oh, well… perhaps I’ll see the light one day!

What’s the endgame with Vim for me? I don’t see myself switching to Vim (or neovim) as my primary editor, but I can imagine adopting evil-mode in Emacs if I start seeing some tangible benefits from Vim’s way. Even before I began my experiments with Vim, I’d occasionally use evil-mode in Emacs when I primarily had to read something as opposed to write it - pressing fewer modifier keys is always nice. The other potential benefit from my foray into Vim that I envision is that editors like VS Code and Zed typically have a lot better Vim emulation than Emacs, as there are so much more Vim users out there. Recently my F# exploits forced me to spend a bit of time in VS Code and dealing with its native keybindings was a lot of pain for me.

Down the road I plan to write a couple of follow-up articles on things like:

  • getting started with Vim
  • my Vim configuration
  • comparisons between Vim and Emacs today
  • some tips and tricks

Perhaps I’ll even write a guide to Vim for Emacs users, as I really needed one when I was starting out.

If you have any tips for aspiring Vim users - please share those in the comments. I’m also happy to get some advice from seasoned experts and to peruse their .vimrc or init.lua.

That’s all I have for you today. Now I have to figure out how to commit this article with Fugitive and exit Vim. Wish me luck!

Articles in the Series

  1. I have to admit that I can tolerate vim9 script. ↩︎

  2. There was a push in Emacs in recent years to modernize the out-of-the-box experience. ↩︎

  3. See :h packages for more details. In a nutshell it’s a built-in alternative of Pathogen. ↩︎

]]>
Bozhidar Batsov
Using use-package the right way2025-04-17T15:50:00+03:002025-04-17T15:50:00+03:00https://batsov.com/articles/2025/04/17/using-use-package-the-right-wayI recently wrote that Emacs startup time doesn’t matter and I got quite a lot of heat for it. I totally stand by everything I said there, but I acknowledge that different people have different use-cases and perspectives when it comes to this.

That’s why I’ve decided to share with you the #1 tip to speed up your Emacs - defer the load time of your packages (in other words - load them as late as possible, ideally when you actually need them for the first time). There are many ways to achieve this, but probably the easiest and most popular these days is to use use-package to organize your package configuration.

Unfortunately, using use-package the right way is not very obvious and there’s plenty of incorrect information about it all over the Internet. Here’s classic example of a problematic use-package usage:1

1
2
3
4
5
6
7
8
9
10
(use-package projectile
  :init
  (setq projectile-project-search-path '("~/projects/" "~/work/" "~/playground"))
  :config
  ;; I typically use this keymap prefix on macOS
  (define-key projectile-mode-map (kbd "s-p") 'projectile-command-map)
  ;; On Linux, however, I usually go with another one
  (define-key projectile-mode-map (kbd "C-c C-p") 'projectile-command-map)
  (global-set-key (kbd "C-c p") 'projectile-command-map)
  (projectile-mode +1))

While this is technically speaking correct, the use of :init and :config means that the package will be loaded immediately.

You might be wondering at this point when to use things like :preface, :config and :init and you would be right to. As usual, the best answer is in the Emacs manual, and I’ll try to expand on it below.

Where possible, it is better to avoid :preface, :config and :init. Instead, prefer autoloading keywords such as :bind, :hook, and :mode, as they will take care of setting up autoloads for you without any need for boilerplate code. While the usage of preface in the wild is fairly rare, you’ll see a ton of usage of :init and :config for whatever reasons.2

For example, consider the following declaration:

1
2
3
(use-package foo
  :init
  (add-hook 'some-hook 'foo-mode))

This has two problems. First, it will unconditionally load the package foo on startup, which will make things slower. You can fix this by adding :defer t:

1
2
3
4
(use-package foo
  :defer t
  :init
  (add-hook 'some-hook 'foo-mode))

This is better, as foo is now only loaded when it is actually needed (that is, when the hook some-hook is run).

The second problem is that there is a lot of boilerplate that you have to write. In this case, it might not be so bad, but avoiding that was what use-package was made to allow. The better option in this case is therefore to use :hook, which also implies :defer t. The above is thereby reduced down to:

1
2
(use-package foo
  :hook some-hook)

Now use-package will set up autoloading for you, and your Emacs startup time will not suffer one bit. Nice, ah?

So, let’s return now to our original example and think how we can improve it. Our first instinct is probably to do something like:

1
2
3
4
5
6
7
8
9
10
11
(use-package projectile
  :defer t
  :init
  (setq projectile-project-search-path '("~/projects/" "~/work/" "~/playground"))
  :config
  ;; I typically use this keymap prefix on macOS
  (define-key projectile-mode-map (kbd "s-p") 'projectile-command-map)
  ;; On Linux, however, I usually go with another one
  (define-key projectile-mode-map (kbd "C-c C-p") 'projectile-command-map)
  (global-set-key (kbd "C-c p") 'projectile-command-map)
  (projectile-mode +1))

This is going to be useless, though, as projectile-mode will run at the end of the :config block forcing the package to be loaded. We can make things a bit better if we instruct the mode to be loaded only after Emacs’s initialization has finished:

1
2
3
4
5
6
7
8
9
10
11
(use-package projectile
  :defer t
  :init
  (setq projectile-project-search-path '("~/projects/" "~/work/" "~/playground"))
  :config
  ;; I typically use this keymap prefix on macOS
  (define-key projectile-mode-map (kbd "s-p") 'projectile-command-map)
  ;; On Linux, however, I usually go with another one
  (define-key projectile-mode-map (kbd "C-c C-p") 'projectile-command-map)
  (global-set-key (kbd "C-c p") 'projectile-command-map)
  :hook (after-init . projectile-mode)

Note that we’re using the name after-init instead of after-init-hook, as the hook is actually named. That’s done to spare you some typing, but I understand it might also be a bit confusing. You can enforce the usage of the full hook names like this:

1
(setopt use-package-hook-name-suffix nil)

So, what can we improve next? Ideally we should get rid of :defer, :init and :config:

1
2
3
4
5
6
(use-package projectile
  :custom (projectile-project-search-path '("~/projects/" "~/work/" "~/playground"))
  :bind-keymap (("C-c C-p" . projectile-command-map)
                ("C-c p" . projectile-command-map)
                ("s-p" . projectile-command-map))
  :hook (after-init . projectile-mode)

Much better!

Of course, you can’t always achieve this clean setup, but if you try you’ll get there 90% of the time!

So, to recap:

  • Avoid the use of :init, :config and :preface whenever possible
  • Most of the time you don’t need to use :defer
  • Usually you should aim to activate minor modes only after Emacs’s main initialization has finished (otherwise :defer is pointless)

I’ll add here that less is more, even in Emacs. It’s usually a good idea to review the list of packages in your .init.el every few months and trim it from time to time. I used to be the type of guy who loads 100+ packages in their config, but these days I limit myself only to packages that really improve my workflows.

If use-package still feels like black magic to you I can suggest the following:

  • Macroexpand various use-package blocks in your config to see what’s the generated Emacs Lisp code. Here’s an example you can try:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
(macroexpand-1
'(use-package projectile
  :custom (projectile-project-search-path '("~/projects/" "~/work/" "~/playground"))
  :bind-keymap (("C-c C-p" . projectile-command-map)
                ("C-c p" . projectile-command-map)
                ("s-p" . projectile-command-map))
  :hook (after-init . projectile-mode)))

;; macroexpansion
(progn
  (use-package-ensure-elpa 'projectile '(t) 'nil)
  (defvar use-package--warning78
    #'(lambda (keyword err)
        (let
            ((msg
              (format "%s/%s: %s" 'projectile keyword (error-message-string err))))
          (display-warning 'use-package msg :error))))
  (condition-case-unless-debug err
      (progn
        (let ((custom--inhibit-theme-enable nil))
          (unless (memq 'use-package custom-known-themes)
            (deftheme use-package) (enable-theme 'use-package)
            (setq custom-enabled-themes
                  (remq 'use-package custom-enabled-themes)))
          (custom-theme-set-variables 'use-package
                                      '(projectile-project-search-path
                                        '("~/projects/" "~/work/" "~/playground")
                                        nil nil
                                        "Customized with use-package projectile")))
        (unless (fboundp 'projectile-mode)
          (autoload #'projectile-mode "projectile" nil t))
        (add-hook 'after-init-hook #'projectile-mode)
        (bind-key "C-c C-p"
                  #'(lambda nil (interactive)
                      (use-package-autoload-keymap 'projectile-command-map
                                                   'projectile nil)))
        (bind-key "C-c p"
                  #'(lambda nil (interactive)
                      (use-package-autoload-keymap 'projectile-command-map
                                                   'projectile nil)))
        (bind-key "s-p"
                  #'(lambda nil (interactive)
                      (use-package-autoload-keymap 'projectile-command-map
                                                   'projectile nil))))
    (error (funcall use-package--warning78 :catch err))))

I know this looks a bit intimidating at first, but if you spent a bit of time reading the code you’ll see there’s nothing scary about it.3

  • use-package also comes with profiler, you can set use-package-compute-statistics to t, restart Emacs and call use-package-report to see which packages are taking too much time to set up and what stage they’re at.4

That’s all I have for you today. Feel free to share other use-package tips in the comments!

  1. From my own init.el - after I all I told you I don’t really care about the startup time. :D ↩︎

  2. I’ll have to admit I don’t even remember what :preface does. ↩︎

  3. You might also want to get wild with something like https://github.com/emacsorphanage/macrostep ↩︎

  4. Thanks to Andrey Listopadov for reminding me about this! ↩︎

]]>
Bozhidar Batsov
Emacs Startup Time Doesn’t Matter2025-04-07T10:24:00+03:002025-04-07T10:24:00+03:00https://batsov.com/articles/2025/04/07/emacs-startup-time-does-not-matterEvery now and then I see people discussing one of the following:

  • How editor X has faster startup time than Emacs (in a classic apples to oranges comparison style) and Emacs sucks because it doesn’t start “fast enough”
  • How certain config changes or Elisp hacks optimized by 0.5 seconds someone’s Emacs startup time

Here’s one hot take from me - none of this really matters. Emacs startup time doesn’t matter.1 Why so? Because of how people normally use Emacs, compared to some other editors:

  • If you’re the type of person who uses Emacs in the terminal, you’re likely using emacs --daemon
  • If you’re the type of person who uses Emacs’s GUI - you don’t restart it very often
  • If you’re the type of person who uses both - you’re definitely using emacs --daemon (or you’re missing out a lot if you’re not)

In the end of the day Emacs sessions tend to be “long-lived”. By this I mean that often I restart Emacs only when I restart my computer. I recall M-x uptime often showing 3+ months of uptime. So, why then would I care about micro-optimizations to my startup time?

I think those conversations are mostly driven by users coming from other editors (usually vim), where people have pretty different usage patterns - e.g. they’d work on files in one project, exit their editor, start it again, ad infinitum. For them - startup time probably matters a lot…

But they also care a lot about their shell, terminal emulator, terminal multiplexer (e.g. tmux) and in the world of Emacs none of those are really as important, as Emacs is the unifying interface of everything that we need to use.2

Emacs is different. Emacs is special. Emacs startup time doesn’t matter most of the time. Remember this. M-x forever!

Update: This short article sparked a lively discussion on Reddit.

  1. Unless it’s insanely slow, that is. ↩︎

  2. I’ve noticed most Emacs users really struggle to understand the value of something like tmux, as Emacs is the ultimate window multiplexer for pretty much anything. ↩︎

]]>
Bozhidar Batsov
neocaml: a new Emacs package for OCaml programming2025-03-14T10:01:00+02:002025-03-14T10:01:00+02:00https://batsov.com/articles/2025/03/14/neocaml-a-new-emacs-package-for-ocaml-programmingI wasn’t an early adopter of TreeSitter in Emacs, as usually such big transitions are not smooth and the initial support for TreeSitter in Emacs left much to be desired. Recently, however, Emacs 30 was released with many improvements on that front, and I felt the time was right for me to (try to) embrace TreeSitter.

I’m the type of person who likes to learn by deliberate practice, that’s why I wanted to do some work on TreeSitter-powered major modes. I’ve already been a co-maintainer of clojure-ts-mode for a while now, and I picked up the basics around it, but I didn’t spend much time hacking on it until recently. After spending a bit more time studying the current implementation of clojure-ts-mode and the various Emacs TreeSitter APIs, I decided to start a new experimental project from scratch - neocaml, a TreeSitter-powered package for OCaml development.1

Why did I start a new OCaml package, when there are already a few existing out there? Because caml-mode is ancient (and probably has to be deprecated), and tuareg-mode is a beast. (it’s very powerful, but also very complex) The time seems ripe for a modern, leaner, TreeSitter-powered mode for OCaml.

There have been two other attempts to create TreeSitter-powered major modes for Emacs, but they didn’t get very far:

Looking at the code of both modes, I inferred that the authors were probably knowledgable in OCaml, but not very familiar with Emacs Lisp and Emacs major modes in general. For me it’s the other way around, and that’s what makes this a fun and interesting project for me:

  • I enjoy working on Emacs packages
  • As noted above I want to do more work TreeSitter
  • I really like OCaml and it’s one of my favorite “hobby” languages

One last thing - we really need more Emacs packages with fun names! :D Naming is hard, and I’m notorious “bad” at it!2

They say that third time’s the charm, and I hope that neocaml will get farther than the other ocaml-ts-modes. Time will tell!

I’ve documented the code extensively inline, and in the README you’ll find my development notes detailing some of my decisions, items that need further work and research, etc. If nothing else - I think anyone can learn a bit about how TreeSitter works in Emacs and what are the common challenges that one might face when working with it. To summarize my experience so far:

  • font-locking (syntax highlighting) with TreeSitter is fairly easy
  • structured navigation seems reasonably straight-forward as well
  • the indentation queries are a bit more complicated, but they are definitely not black magic

Fundamentally, the main problem is that we still don’t have easy ways to try out TreeSitter queries in Emacs, so there’s a lot of trial and error involved. (especially when it comes to indentation logic) My other big problem is that most TreeSitter grammars have pretty much no documentation, so one has to learn about their AST format via experimentation (e.g. treesit-explore-mode and treesit-inspect-mode) and reading their bundled queries for font-locking and indentation. As someone who’s used to work with Ruby parser I really miss the docs and tools that come with something like the Ruby parser library.

What’s the state of project right now? Well, neocaml kind of works right now, but the indentation logic needs a lot of polish, and I’ve yet to implement properly structured navigation and some of the newer Emacs TreeSitter APIs (e.g. things). We can always “cheat” a bit with the indentation, by delegating it to another Emacs package like ocp-indent.el or even Tuareg, but I’m hoping to come up with a self-contained TreeSitter implementation in the end of the day.

If you’re feeling adventurous you can easily install the package like this:

1
M-x package-vc-install <RET> https://github.com/bbatsov/neocaml <RET>

In Emacs 30 you can you use-package to both install the package from GitHub and configure it:

1
2
(use-package neocaml
  :vc (:url "https://github.com/bbatsov/neocaml" :rev :newest))

Note: neocaml will auto-install the required TreeSitter grammars the first time one of the provided major modes is activated.

Please refer to the README for usage information, like the various configuration options and interactive commands.

I’m not sure how much time I’ll be able to spend working on neocaml and how far will I be able to push it. Perhaps it will never amount to anything, perhaps it will just be a research platform to bring TreeSitter support to Tuareg. And perhaps it will become a viable simple, yet modern solution for OCaml programming in Emacs. The dream is alive!

Contributions, suggestions and feedback are most welcome. Keep hacking!

  1. I didn’t name it neocaml-mode intentionally - many Emacs packages contain more things than just major modes, so I prefer a more generic naming. ↩︎

  2. On a more serious note - there was never an ocaml-mode, so naming something ocaml-ts-mode is not strictly needed. But I think an actual ocaml-mode should be blessed by the the maintainers of OCaml, hosted in the primary GitHub org, and endorsed as a recommended way to program in OCaml with Emacs. Pretty tall order! ↩︎

]]>
Bozhidar Batsov