<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en"><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://batsov.com/feeds/Emacs.xml" rel="self" type="application/atom+xml" /><link href="https://batsov.com/" rel="alternate" type="text/html" hreflang="en" /><updated>2026-04-22T07:59:02+03:00</updated><id>https://batsov.com/feeds/Emacs.xml</id><title type="html">(think)</title><subtitle>Bozhidar Batsov&apos;s personal blog</subtitle><entry><title type="html">Batppuccin: My Take on Catppuccin for Emacs</title><link href="https://batsov.com/articles/2026/03/29/batppuccin-my-take-on-catppuccin-for-emacs/" rel="alternate" type="text/html" title="Batppuccin: My Take on Catppuccin for Emacs" /><published>2026-03-29T10:00:00+03:00</published><updated>2026-03-29T10:00:00+03:00</updated><id>https://batsov.com/articles/2026/03/29/batppuccin-my-take-on-catppuccin-for-emacs</id><content type="html" xml:base="https://batsov.com/articles/2026/03/29/batppuccin-my-take-on-catppuccin-for-emacs/"><![CDATA[<p>I promised I’d take a break from building Tree-sitter major modes, and I meant
it. So what better way to relax than to build… color themes? Yeah, I know. My
idea of chilling is weird, but I genuinely enjoy working on random Emacs packages.
Most of the time at least…</p>

<h2 id="some-background">Some Background</h2>

<p>For a very long time my go-to Emacs themes were
<a href="https://github.com/bbatsov/zenburn-emacs">Zenburn</a> and
<a href="https://github.com/bbatsov/solarized-emacs">Solarized</a> – both of which I
maintain popular Emacs ports for. Zenburn was actually one of my very first open
source projects (created way back in 2010, when Emacs 24 was brand new). It served
me well for years.</p>

<p>But at some point I got bored. You know the feeling – you’ve been staring at the
same color palette for so long that you stop seeing it. My experiments with other
editors (Helix, Zed, VS Code) introduced me to
<a href="https://github.com/enkia/tokyo-night-vscode-theme">Tokyo Night</a> and
<a href="https://github.com/catppuccin/catppuccin">Catppuccin</a>, and they’ve been my daily
drivers since then.</p>

<p>Eventually, I ended up creating my own Emacs ports of both. I’ve already published
<a href="https://github.com/bbatsov/tokyo-night-emacs">Tokyo Night for Emacs</a>,
and I’ll write more about that one down the road. Today is all about Catppuccin.
(and by this I totally mean Batppuccin!)</p>

<h2 id="why-another-catppuccin-port">Why Another Catppuccin Port?</h2>

<p>There’s already an <a href="https://github.com/catppuccin/emacs">official Catppuccin theme for
Emacs</a>, and it works. So why build another
one? A few reasons.</p>

<p>The official port registers a single <code class="language-plaintext highlighter-rouge">catppuccin</code> theme and switches between
flavors (Mocha, Macchiato, Frappe, Latte) via a global variable and a reload
function. This is unusual by Emacs standards and breaks the normal <code class="language-plaintext highlighter-rouge">load-theme</code>
workflow – theme-switching packages like <code class="language-plaintext highlighter-rouge">circadian.el</code> need custom glue code to
work with it. It also loads color definitions from an external file in a way that
fails when Emacs hasn’t marked the theme as safe yet, which means some users
can’t load the theme at all.</p>

<p>Beyond the architecture, there are style guide issues.
<code class="language-plaintext highlighter-rouge">font-lock-variable-name-face</code> is set to the default text color, making variables
invisible. All <code class="language-plaintext highlighter-rouge">outline-*</code> levels use the same blue, so org-mode headings are
flat. <code class="language-plaintext highlighter-rouge">org-block</code> forces green on all unstyled code. Several faces still ship
with <code class="language-plaintext highlighter-rouge">#ff00ff</code> magenta placeholder colors. And there’s no support for popular
packages like vertico, marginalia, transient, flycheck, or cider.</p>

<p>I think some of this comes from the official port trying to match the structure of
the Neovim version, which makes sense for their cross-editor tooling but doesn’t
sit well with how Emacs does things.<sup id="fnref:1"><a href="#fn:1" class="footnote" rel="footnote" role="doc-noteref">1</a></sup></p>

<h2 id="meet-batppuccin">Meet Batppuccin</h2>

<p><a href="https://github.com/bbatsov/batppuccin-emacs">Batppuccin</a> is my opinionated take
on Catppuccin for Emacs. The name is a play on my last name (Batsov) + Catppuccin.<sup id="fnref:2"><a href="#fn:2" class="footnote" rel="footnote" role="doc-noteref">2</a></sup>
I guess you can think of this as <code class="language-plaintext highlighter-rouge">@bbatsov</code>’s Catppuccin… or perhaps Batman’s
Catppuccin?</p>

<p>The key differences from the official port:</p>

<p><strong>Four proper themes.</strong> <code class="language-plaintext highlighter-rouge">batppuccin-mocha</code>, <code class="language-plaintext highlighter-rouge">batppuccin-macchiato</code>,
<code class="language-plaintext highlighter-rouge">batppuccin-frappe</code>, and <code class="language-plaintext highlighter-rouge">batppuccin-latte</code> are all separate themes that work
with <code class="language-plaintext highlighter-rouge">load-theme</code> out of the box. No special reload dance needed.</p>

<p><strong>Faithful to the style guide.</strong> Mauve for keywords, green for strings, blue for
functions, peach for constants, sky for operators, yellow for types, overlay2 for
comments, rosewater for the cursor. The rainbow heading cycle (red, peach, yellow,
green, sapphire, lavender) makes org-mode and outline headings actually
distinguishable.</p>

<p><strong>Broad face coverage.</strong> Built-in Emacs faces plus magit, vertico, corfu,
marginalia, embark, orderless, consult, transient, flycheck, cider, company,
doom-modeline, treemacs, web-mode, and more. No placeholder colors.</p>

<p><strong>Clean architecture.</strong> Shared infrastructure in <code class="language-plaintext highlighter-rouge">batppuccin-themes.el</code>, thin
wrapper files for each flavor, color override mechanism, configurable heading
scaling. The same pattern I use in
<a href="https://github.com/bbatsov/zenburn-emacs">Zenburn</a> and
<a href="https://github.com/bbatsov/tokyo-night-emacs">Tokyo Night</a>.</p>

<p>I didn’t really re-invent anything here - I just created a theme in a way I’m
comfortable with.</p>

<blockquote class="prompt-info">
  <p>I’m not going to bother with screenshots here – it looks like Catppuccin,
because it <em>is</em> Catppuccin. There are small visual differences if you know where
to look (headings, variables, a few face tweaks), but most people wouldn’t
notice them side by side. If you’ve seen Catppuccin, you know what to expect.</p>
</blockquote>

<h2 id="installation">Installation</h2>

<p>The easiest way to install it right now:</p>

<div class="language-emacs-lisp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="p">(</span><span class="nb">use-package</span> <span class="nv">batppuccin</span>
  <span class="ss">:vc</span> <span class="p">(</span><span class="ss">:url</span> <span class="s">"https://github.com/bbatsov/batppuccin-emacs"</span> <span class="ss">:rev</span> <span class="ss">:newest</span><span class="p">)</span>
  <span class="ss">:config</span>
  <span class="p">(</span><span class="nv">load-theme</span> <span class="ss">'batppuccin-mocha</span> <span class="no">t</span><span class="p">))</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Replace <code class="language-plaintext highlighter-rouge">mocha</code> with <code class="language-plaintext highlighter-rouge">macchiato</code>, <code class="language-plaintext highlighter-rouge">frappe</code>, or <code class="language-plaintext highlighter-rouge">latte</code> for the other flavors. You
can also switch interactively with <code class="language-plaintext highlighter-rouge">M-x batppuccin-select</code>.</p>

<h2 id="there-can-never-be-enough-theme-ports">There Can Never Be Enough Theme Ports</h2>

<p>I remember when Solarized was the hot new thing and there were something like five
competing Emacs ports of it. People had strong opinions about which one got the
colors right, which one had better org-mode support, which one worked with their
favorite completion framework. And that was fine! Different ports serve different
needs and different tastes.</p>

<p>The same applies here. The official Catppuccin port is perfectly usable for a lot
of people. Batppuccin is for people who want something more idiomatic to
Emacs, with broader face coverage and stricter adherence to the upstream style
guide. Both can coexist happily.</p>

<p>I’ve said many times that for me the best aspect of Emacs is that you can tweak
it infinitely to make it your own, so as far as I’m concerned having a theme
that you’re the only user of is perfectly fine. That being said, I hope
a few of you will appreciate my take on Catppuccin as well.</p>

<h2 id="wrapping-up">Wrapping Up</h2>

<p>This is an early release and there’s plenty of room for improvement. I’m sure
there are faces I’ve missed, colors that could be tweaked, and packages that
deserve better support. If you try it out and something looks off, please <a href="https://github.com/bbatsov/batppuccin-emacs/issues">open
an issue</a> or send a PR.</p>

<p>I’m also curious – what are your favorite Emacs themes these days? Still rocking
Zenburn? Converted to modus-themes? Something else entirely? I’d love to hear
about it.</p>

<p>That’s all from me, folks! Keep hacking!</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1">
      <p>The official port uses Catppuccin’s <a href="https://github.com/catppuccin/toolbox/tree/main/whiskers">Whiskers</a> template tool to generate the Elisp from a <code class="language-plaintext highlighter-rouge">.tera</code> template, which is cool for keeping ports in sync across editors but means the generated code doesn’t follow Emacs conventions. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;&#xfe0e;</a></p>
    </li>
    <li id="fn:2">
      <p>Naming is hard, but it should also be fun! Also – I’m a huge fan of Batman. <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;&#xfe0e;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Bozhidar Batsov</name></author><category term="Emacs" /><category term="Themes" /><summary type="html"><![CDATA[I promised I’d take a break from building Tree-sitter major modes, and I meant it. So what better way to relax than to build… color themes? Yeah, I know. My idea of chilling is weird, but I genuinely enjoy working on random Emacs packages. Most of the time at least…]]></summary></entry><entry><title type="html">fsharp-ts-mode: A Modern Emacs Mode for F#</title><link href="https://batsov.com/articles/2026/03/27/fsharp-ts-mode-a-modern-emacs-mode-for-fsharp/" rel="alternate" type="text/html" title="fsharp-ts-mode: A Modern Emacs Mode for F#" /><published>2026-03-27T17:00:00+02:00</published><updated>2026-03-27T17:00:00+02:00</updated><id>https://batsov.com/articles/2026/03/27/fsharp-ts-mode-a-modern-emacs-mode-for-fsharp</id><content type="html" xml:base="https://batsov.com/articles/2026/03/27/fsharp-ts-mode-a-modern-emacs-mode-for-fsharp/"><![CDATA[<p>I’m pretty much done with the focused development push on
<a href="https://github.com/bbatsov/neocaml">neocaml</a> – it’s reached a point where I’m
genuinely happy using it daily and the remaining work is mostly incremental
polish. So naturally, instead of taking a break I decided it was time to start
another project that’s been living in the back of my head for a while:
a proper Tree-sitter-based F# mode for Emacs.</p>

<p>Meet <a href="https://github.com/bbatsov/fsharp-ts-mode">fsharp-ts-mode</a>.</p>

<h2 id="why-f">Why F#?</h2>

<p>I’ve written before about my fondness for the ML family of languages, and while
OCaml gets most of my attention, last year I developed a soft spot for F#. In some
ways I like it even a bit more than OCaml – the tooling is excellent, the .NET
ecosystem is massive, and computation expressions are one of the most elegant
abstractions I’ve seen in any language. F# manages to feel both practical and
beautiful, which is a rare combination.</p>

<p>The problem is that Emacs has never been particularly popular with F# programmers
– or .NET programmers in general. The existing
<a href="https://github.com/fsharp/emacs-fsharp-mode">fsharp-mode</a> works, but it’s
showing its age: regex-based highlighting, SMIE indentation with quirks, and
some legacy code dating back to the caml-mode days. I needed a good F# mode for
Emacs, and that’s enough of a reason to build one in my book.</p>

<h2 id="the-name">The Name</h2>

<p>I’ll be honest – I spent quite a bit of time trying to come up with
a clever name.<sup id="fnref:1"><a href="#fn:1" class="footnote" rel="footnote" role="doc-noteref">1</a></sup> Some candidates that didn’t make the cut:</p>

<ul>
  <li><strong>fsharpe-mode</strong> (fsharp(evolved/enhanced)-mode)</li>
  <li><strong>Fa Dièse</strong> (French for F sharp – because after spending time with OCaml you
start thinking in French, apparently)</li>
  <li><strong>fluoride</strong> (a play on <a href="https://ionide.io/">Ionide</a>, the popular F# IDE
extension)</li>
</ul>

<p>In the end none of my fun ideas stuck, so I went with the boring-but-obvious
<code class="language-plaintext highlighter-rouge">fsharp-ts-mode</code>. Sometimes the straightforward choice is the right one. At
least nobody will have trouble finding it.<sup id="fnref:2"><a href="#fn:2" class="footnote" rel="footnote" role="doc-noteref">2</a></sup></p>

<h2 id="built-on-neocamls-foundation">Built on neocaml’s Foundation</h2>

<p>I modeled <code class="language-plaintext highlighter-rouge">fsharp-ts-mode</code> directly after <code class="language-plaintext highlighter-rouge">neocaml</code>, and the two packages share
a lot of structural similarities – which shouldn’t be surprising given how much
OCaml and F# have in common. The same architecture (base mode + language-specific
derived modes), the same approach to font-locking (shared + grammar-specific
rules), the same REPL integration pattern (<code class="language-plaintext highlighter-rouge">comint</code> with tree-sitter input
highlighting), the same build system interaction pattern (minor mode wrapping CLI
commands).</p>

<p>This also meant I could get the basics in place really quickly. Having already
solved problems like trailing comment indentation, <code class="language-plaintext highlighter-rouge">forward-sexp</code> hybrid
navigation, and <code class="language-plaintext highlighter-rouge">imenu</code> with qualified names in neocaml, porting those solutions
to F# was mostly mechanical.</p>

<h2 id="whats-in-010">What’s in 0.1.0</h2>

<p>The initial release covers all the essentials:</p>

<ul>
  <li><strong>Syntax highlighting</strong> via Tree-sitter with 4 customizable levels, supporting
<code class="language-plaintext highlighter-rouge">.fs</code>, <code class="language-plaintext highlighter-rouge">.fsx</code>, and <code class="language-plaintext highlighter-rouge">.fsi</code> files</li>
  <li><strong>Indentation</strong> via Tree-sitter indent rules</li>
  <li><strong>Imenu</strong> with fully-qualified names (e.g., <code class="language-plaintext highlighter-rouge">MyModule.myFunc</code>)</li>
  <li><strong>Navigation</strong> – <code class="language-plaintext highlighter-rouge">beginning-of-defun</code>, <code class="language-plaintext highlighter-rouge">end-of-defun</code>, <code class="language-plaintext highlighter-rouge">forward-sexp</code></li>
  <li><strong>F# Interactive (REPL)</strong> integration with tree-sitter highlighting for input</li>
  <li><strong>dotnet CLI integration</strong> – build, test, run, clean, format, restore, with
watch mode support</li>
  <li><strong>.NET API documentation lookup</strong> at point (<code class="language-plaintext highlighter-rouge">C-c C-d</code>)</li>
  <li><strong>Eglot integration</strong> for
<a href="https://github.com/fsharp/FsAutoComplete">FsAutoComplete</a></li>
  <li><strong>Compilation error parsing</strong> for <code class="language-plaintext highlighter-rouge">dotnet build</code> output</li>
  <li>Shift region left/right, auto-detect indent offset, prettify symbols, outline
mode, and more</li>
</ul>

<h3 id="migrating-from-fsharp-mode">Migrating from fsharp-mode</h3>

<p>If you’re currently using <code class="language-plaintext highlighter-rouge">fsharp-mode</code>, switching is straightforward:</p>

<div class="language-emacs-lisp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="p">(</span><span class="nb">use-package</span> <span class="nv">fsharp-ts-mode</span>
  <span class="ss">:vc</span> <span class="p">(</span><span class="ss">:url</span> <span class="s">"https://github.com/bbatsov/fsharp-ts-mode"</span> <span class="ss">:rev</span> <span class="ss">:newest</span><span class="p">))</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The main thing <code class="language-plaintext highlighter-rouge">fsharp-ts-mode</code> doesn’t have yet is automatic LSP server
installation (the <code class="language-plaintext highlighter-rouge">eglot-fsharp</code> package does this for <code class="language-plaintext highlighter-rouge">fsharp-mode</code>). You’ll
need to install FsAutoComplete yourself:</p>

<pre><code class="language-shellsession">$ dotnet tool install -g fsautocomplete
</code></pre>

<p>After that, <code class="language-plaintext highlighter-rouge">(add-hook 'fsharp-ts-mode-hook #'eglot-ensure)</code> is all you need.</p>

<p>See the <a href="https://github.com/bbatsov/fsharp-ts-mode#migrating-from-fsharp-mode">migration
guide</a> in
the README for a detailed comparison.</p>

<h2 id="lessons-learned">Lessons Learned</h2>

<p>Working with the <a href="https://github.com/ionide/tree-sitter-fsharp">ionide/tree-sitter-fsharp</a>
grammar surfaced some interesting challenges compared to the OCaml grammar:</p>

<h3 id="fs-indentation-sensitive-syntax-is-tricky">F#’s indentation-sensitive syntax is tricky</h3>

<p>Unlike OCaml, where indentation is purely cosmetic, F# uses significant
whitespace (the “offside rule”). The tree-sitter grammar needs correct
indentation to parse correctly, which creates a chicken-and-egg problem: you
need a correct parse tree to indent, but you need correct indentation to
parse. For example, if you paste this unindented block:</p>

<div class="language-fsharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="k">let</span> <span class="n">f</span> <span class="n">x</span> <span class="p">=</span>
<span class="k">if</span> <span class="n">x</span> <span class="p">&gt;</span> <span class="mi">0</span> <span class="k">then</span>
<span class="n">x</span> <span class="o">+</span> <span class="mi">1</span>
<span class="k">else</span>
<span class="mi">0</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The parser can’t tell that <code class="language-plaintext highlighter-rouge">if</code> is the body of <code class="language-plaintext highlighter-rouge">f</code> or that <code class="language-plaintext highlighter-rouge">x + 1</code> belongs
to the <code class="language-plaintext highlighter-rouge">then</code> branch – it produces ERROR nodes everywhere, and
<code class="language-plaintext highlighter-rouge">indent-region</code> has nothing useful to work with. But if you’re typing the code
line by line, the parser always has enough context from preceding lines to
indent the current line correctly. This is a fundamental limitation of any
indentation-sensitive grammar.</p>

<h3 id="the-two-grammars-are-more-different-than-youd-expect">The two grammars are more different than you’d expect</h3>

<p>OCaml’s tree-sitter-ocaml-interface grammar inherits from the base grammar, so
you can share queries freely. F#’s <code class="language-plaintext highlighter-rouge">fsharp</code> and <code class="language-plaintext highlighter-rouge">fsharp_signature</code> grammars are
independent with different node types and field names for equivalent concepts.
For instance, a <code class="language-plaintext highlighter-rouge">let</code> binding is <code class="language-plaintext highlighter-rouge">function_or_value_defn</code> in the <code class="language-plaintext highlighter-rouge">.fs</code> grammar
but <code class="language-plaintext highlighter-rouge">value_definition</code> in the <code class="language-plaintext highlighter-rouge">.fsi</code> grammar. Type names use a <code class="language-plaintext highlighter-rouge">type_name:</code>
field in one grammar but not the other. Even some keyword tokens (<code class="language-plaintext highlighter-rouge">of</code>, <code class="language-plaintext highlighter-rouge">open</code>,
<code class="language-plaintext highlighter-rouge">type</code>) that work fine as query matches in <code class="language-plaintext highlighter-rouge">fsharp</code> fail at runtime in
<code class="language-plaintext highlighter-rouge">fsharp_signature</code>.</p>

<p>This forced me to split font-lock rules into shared and grammar-specific sets –
more code, more testing, more edge cases.</p>

<h3 id="script-files-are-weird">Script files are weird</h3>

<p>F# script (<code class="language-plaintext highlighter-rouge">.fsx</code>) files without a <code class="language-plaintext highlighter-rouge">module</code> declaration can mix <code class="language-plaintext highlighter-rouge">let</code> bindings
with bare expressions like <code class="language-plaintext highlighter-rouge">printfn</code>. The grammar doesn’t expect a declaration
after a bare expression at the top level, so it chains everything into nested
<code class="language-plaintext highlighter-rouge">application_expression</code> nodes:</p>

<div class="language-fsharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="k">let</span> <span class="n">x</span> <span class="p">=</span> <span class="mi">1</span>
<span class="n">printfn</span> <span class="s2">"%d"</span> <span class="n">x</span>    <span class="c1">// bare expression</span>
<span class="k">let</span> <span class="n">y</span> <span class="p">=</span> <span class="mi">2</span>         <span class="c1">// grammar nests this under the printfn node</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>Each subsequent <code class="language-plaintext highlighter-rouge">let</code> ends up one level deeper, causing progressive
indentation. I worked around this with a heuristic that detects declarations
whose ancestor chain leads back to <code class="language-plaintext highlighter-rouge">file</code> through these misparented nodes and
forces them to column 0. Shebangs (<code class="language-plaintext highlighter-rouge">#!/usr/bin/env dotnet fsi</code>) required a
different trick – excluding the first line from the parser’s range entirely
via <code class="language-plaintext highlighter-rouge">treesit-parser-set-included-ranges</code>.</p>

<p>I’ve filed issues upstream for the grammar pain points – hopefully they’ll
improve over time.</p>

<h2 id="current-status">Current Status</h2>

<p>Let me be upfront: this is a 0.1.0 release and it’s probably quite buggy. I’ve
tested it against a reasonable set of F# code, but there are certainly
indentation edge cases, font-lock gaps, and interactions I haven’t encountered
yet. If you try it and something looks wrong, please <a href="https://github.com/bbatsov/fsharp-ts-mode/issues">open an
issue</a> – <code class="language-plaintext highlighter-rouge">M-x
fsharp-ts-mode-bug-report-info</code> will collect the environment details for you.</p>

<p>The package can currently be installed only from GitHub (via <code class="language-plaintext highlighter-rouge">package-vc-install</code>
or manually). I’ve filed a <a href="https://github.com/melpa/melpa/pull/9917">PR with
MELPA</a> and I hope it will get merged
soon.</p>

<h2 id="wrapping-up">Wrapping Up</h2>

<p>I really need to take a break from building Tree-sitter major modes at this
point. Between <code class="language-plaintext highlighter-rouge">clojure-ts-mode</code>, <code class="language-plaintext highlighter-rouge">neocaml</code>, <code class="language-plaintext highlighter-rouge">asciidoc-mode</code>, and now
<code class="language-plaintext highlighter-rouge">fsharp-ts-mode</code>, I’ve spent a lot of time staring at tree-sitter node types and
indent rules.<sup id="fnref:3"><a href="#fn:3" class="footnote" rel="footnote" role="doc-noteref">3</a></sup> It’s been fun, but I think I’ve earned a vacation from
<code class="language-plaintext highlighter-rouge">treesit-font-lock-rules</code>.</p>

<p>I really wanted to do something nice for the (admittedly small) F#-on-Emacs
community, and a modern major mode seemed like the most meaningful contribution I
could make. I hope some of you find it useful!</p>

<p>That’s all from me, folks! Keep hacking!</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1">
      <p>Way more time than I needed to actually implement the mode. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;&#xfe0e;</a></p>
    </li>
    <li id="fn:2">
      <p>Many people pointed out they thought <code class="language-plaintext highlighter-rouge">neocaml</code> was some package for neovim. Go figure why! <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;&#xfe0e;</a></p>
    </li>
    <li id="fn:3">
      <p>I’ve also been helping a bit with <a href="https://github.com/erlang/emacs-erlang-ts">erlang-ts-mode</a> recently. <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;&#xfe0e;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Bozhidar Batsov</name></author><category term="F#" /><category term="Emacs" /><summary type="html"><![CDATA[I’m pretty much done with the focused development push on neocaml – it’s reached a point where I’m genuinely happy using it daily and the remaining work is mostly incremental polish. So naturally, instead of taking a break I decided it was time to start another project that’s been living in the back of my head for a while: a proper Tree-sitter-based F# mode for Emacs.]]></summary></entry><entry><title type="html">Neocaml 0.6: Opam, Dune, and More</title><link href="https://batsov.com/articles/2026/03/25/neocaml-0-6-opam-dune-and-more/" rel="alternate" type="text/html" title="Neocaml 0.6: Opam, Dune, and More" /><published>2026-03-25T10:00:00+02:00</published><updated>2026-03-25T10:00:00+02:00</updated><id>https://batsov.com/articles/2026/03/25/neocaml-0-6-opam-dune-and-more</id><content type="html" xml:base="https://batsov.com/articles/2026/03/25/neocaml-0-6-opam-dune-and-more/"><![CDATA[<p>When I released <a href="/articles/2026/02/14/neocaml-0-1-ready-for-action/">neocaml 0.1</a>
last month, I thought I was more or less done with the (main) features for the
foreseeable future. The original scope was deliberately small — a couple of
Tree-sitter-powered OCaml major modes (for <code class="language-plaintext highlighter-rouge">.ml</code> and <code class="language-plaintext highlighter-rouge">.mli</code>), a REPL
integration, and not much else. I was quite happy with how things turned out and
figured the next steps would be mostly polish and bug fixes.</p>

<p>Versions 0.2-0.5 brought polish and bug fixes, but fundamentally the feature
set stayed the same. I was even more convinced a grand 1.0 release was just
around the corner.</p>

<p>I was wrong.</p>

<p>Of course, OCaml files don’t exist in isolation. They live alongside
<a href="https://opam.ocaml.org/">Opam</a> files that describe packages and
<a href="https://dune.readthedocs.io/">Dune</a> files that configure builds. And as I was
poking around the Tree-sitter ecosystem, I discovered that there were already
grammars for both
<a href="https://github.com/tmcgilchrist/tree-sitter-opam">Opam</a> and
<a href="https://github.com/tmcgilchrist/tree-sitter-dune">Dune</a> files. Given how
simple both formats are (Opam is mostly key-value pairs, Dune is
s-expressions), adding support for them turned out to be fairly
straightforward.</p>

<p>So here we are with neocaml 0.6, which is quite a bit bigger than I expected
originally.</p>

<p><strong>Note:</strong> One thing worth mentioning — all the new modes are completely
isolated from the core OCaml modes. They’re separate files with no hard
dependency on <code class="language-plaintext highlighter-rouge">neocaml-mode</code>, loaded only when you open the relevant file
types. I didn’t want to force them upon anyone — for me it’s convenient to get
Opam and Dune support out-of-the-box (given how ubiquitous they are in the OCaml
ecosystem), but I totally get it if someone doesn’t care about this.</p>

<p>Let me walk you through what’s new.</p>

<h2 id="neocaml-opam-mode">neocaml-opam-mode</h2>

<p>The new <code class="language-plaintext highlighter-rouge">neocaml-opam-mode</code> activates automatically for <code class="language-plaintext highlighter-rouge">.opam</code> and <code class="language-plaintext highlighter-rouge">opam</code>
files. It provides:</p>

<ul>
  <li>Tree-sitter-based font-lock (field names, strings, operators, version
constraints, filter expressions, etc.)</li>
  <li>Indentation (lists, sections, option braces)</li>
  <li>Imenu for navigating variables and sections</li>
  <li>A <strong>flymake backend</strong> that runs <code class="language-plaintext highlighter-rouge">opam lint</code> on the current buffer, giving you
inline diagnostics for missing fields, deprecated constructs, and syntax
errors</li>
</ul>

<p>The flymake backend registers automatically when <code class="language-plaintext highlighter-rouge">opam</code> is found in your PATH,
but you need to enable <code class="language-plaintext highlighter-rouge">flymake-mode</code> yourself:</p>

<div class="language-emacs-lisp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="p">(</span><span class="nv">add-hook</span> <span class="ss">'neocaml-opam-mode-hook</span> <span class="nf">#'</span><span class="nv">flymake-mode</span><span class="p">)</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><a href="https://github.com/flycheck/flycheck">Flycheck</a> users get <code class="language-plaintext highlighter-rouge">opam lint</code> support
out of the box via Flycheck’s built-in <code class="language-plaintext highlighter-rouge">opam</code> checker — no extra configuration
needed.</p>

<p>This bridges some of the gap with <a href="https://github.com/ocaml/tuareg">Tuareg</a>,
which also bundles an Opam major mode (<code class="language-plaintext highlighter-rouge">tuareg-opam-mode</code>). The Tree-sitter-based
approach gives us more accurate highlighting, and the flymake integration is a
nice bonus on top.</p>

<h2 id="neocaml-dune-mode">neocaml-dune-mode</h2>

<p><code class="language-plaintext highlighter-rouge">neocaml-dune-mode</code> handles <code class="language-plaintext highlighter-rouge">dune</code>, <code class="language-plaintext highlighter-rouge">dune-project</code>, and <code class="language-plaintext highlighter-rouge">dune-workspace</code> files
— all three use the same s-expression syntax and share a single Tree-sitter
grammar. You get:</p>

<ul>
  <li>Font-lock for stanza names, field names, action keywords, strings, module
names, library names, operators, and brackets</li>
  <li>Indentation with 1-space offset</li>
  <li>Imenu for stanza navigation</li>
  <li>Defun navigation and <code class="language-plaintext highlighter-rouge">which-func</code> support</li>
</ul>

<p>This removes the need to install the separate
<a href="https://melpa.org/#/dune">dune</a> package (the standalone <code class="language-plaintext highlighter-rouge">dune-mode</code> maintained
by the Dune developers) from MELPA. If you prefer to keep using it, that’s fine
too — neocaml’s README has
<a href="https://github.com/bbatsov/neocaml#using-the-legacy-dune-mode">instructions</a>
for overriding the <code class="language-plaintext highlighter-rouge">auto-mode-alist</code> entries.</p>

<h2 id="neocaml-dune-interaction-mode">neocaml-dune-interaction-mode</h2>

<p>Beyond editing Dune files, I wanted a simple way to run Dune commands from any
neocaml buffer. <code class="language-plaintext highlighter-rouge">neocaml-dune-interaction-mode</code> is a minor mode that provides
keybindings (under <code class="language-plaintext highlighter-rouge">C-c C-d</code>) and a “Dune” menu for common operations:</p>

<table>
  <thead>
    <tr>
      <th>Keybinding</th>
      <th>Command</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">C-c C-d b</code></td>
      <td>Build</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">C-c C-d t</code></td>
      <td>Test</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">C-c C-d c</code></td>
      <td>Clean</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">C-c C-d p</code></td>
      <td>Promote</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">C-c C-d f</code></td>
      <td>Format</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">C-c C-d u</code></td>
      <td>Launch utop with project libraries</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">C-c C-d r</code></td>
      <td>Run an executable</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">C-c C-d d</code></td>
      <td>Run any Dune command</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">C-c C-d .</code></td>
      <td>Find the nearest <code class="language-plaintext highlighter-rouge">dune</code> file</td>
    </tr>
  </tbody>
</table>

<p>All commands run via Emacs’s <code class="language-plaintext highlighter-rouge">compile</code>, so you get error navigation, clickable
source locations, and the full <code class="language-plaintext highlighter-rouge">compilation-mode</code> interface for free. With a
prefix argument (<code class="language-plaintext highlighter-rouge">C-u</code>), build, test, and fmt run in <strong>watch mode</strong>
(<code class="language-plaintext highlighter-rouge">--watch</code>), automatically rebuilding when files change.</p>

<p>The <code class="language-plaintext highlighter-rouge">utop</code> command is special — it launches through <code class="language-plaintext highlighter-rouge">neocaml-repl</code>, so you get
the full REPL integration (send region, send definition, etc.) with your
project’s libraries preloaded.</p>

<p>This mode is completely independent from <code class="language-plaintext highlighter-rouge">neocaml-dune-mode</code> — it doesn’t care
which major mode you’re using. You can enable it in OCaml buffers like this:</p>

<div class="language-emacs-lisp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="p">(</span><span class="nv">add-hook</span> <span class="ss">'neocaml-base-mode-hook</span> <span class="nf">#'</span><span class="nv">neocaml-dune-interaction-mode</span><span class="p">)</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<h2 id="rough-edges">Rough Edges</h2>

<p>Both the Opam and Dune Tree-sitter grammars are relatively young and will need
some more work for optimal results. I’ve been filing issues and contributing
patches upstream to improve them — for instance, the Dune grammar currently
<a href="https://github.com/tmcgilchrist/tree-sitter-dune/issues/9">flattens field-value
pairs</a> in a way
that makes indentation less precise than it could be, and neither grammar
supports <a href="https://github.com/tmcgilchrist/tree-sitter-dune/issues/10">variable
interpolation</a>
(<code class="language-plaintext highlighter-rouge">%{...}</code>) yet. These are very solvable problems and I expect the grammars to
improve over time.</p>

<h2 id="whats-next">What’s Next?</h2>

<p>At this point I think I’m (finally!) out of ideas for new functionality. This
time I mean it! Neocaml now covers pretty much everything I ever wanted,
especially when paired with the awesome
<a href="https://github.com/tarides/ocaml-eglot">ocaml-eglot</a>.</p>

<p>Down the road there might be support for OCamllex (<code class="language-plaintext highlighter-rouge">.mll</code>) or Menhir (<code class="language-plaintext highlighter-rouge">.mly</code>)
files, but only if adding them doesn’t bring significant complexity — both are
mixed languages with embedded OCaml code, which makes them fundamentally harder
to support well than the simple Opam and Dune formats.</p>

<p>I hope OCaml programmers will find the new functionality useful. If you’re using neocaml,
I’d love to hear how it’s working for you — bug reports, feature requests, and
general feedback are all welcome on
<a href="https://github.com/bbatsov/neocaml">GitHub</a>. You can find the full list of
changes in the
<a href="https://github.com/bbatsov/neocaml/blob/main/CHANGELOG.md">changelog</a>.</p>

<p>As usual — update from <a href="https://melpa.org/#/neocaml">MELPA</a>, kick the tires,
and let me know what you think.</p>

<p>That’s all I have for you today! Keep hacking!</p>]]></content><author><name>Bozhidar Batsov</name></author><category term="OCaml" /><category term="Emacs" /><summary type="html"><![CDATA[When I released neocaml 0.1 last month, I thought I was more or less done with the (main) features for the foreseeable future. The original scope was deliberately small — a couple of Tree-sitter-powered OCaml major modes (for .ml and .mli), a REPL integration, and not much else. I was quite happy with how things turned out and figured the next steps would be mostly polish and bug fixes.]]></summary></entry><entry><title type="html">Emacs and Vim in the Age of AI</title><link href="https://batsov.com/articles/2026/03/09/emacs-and-vim-in-the-age-of-ai/" rel="alternate" type="text/html" title="Emacs and Vim in the Age of AI" /><published>2026-03-09T10:30:00+02:00</published><updated>2026-03-09T10:30:00+02:00</updated><id>https://batsov.com/articles/2026/03/09/emacs-and-vim-in-the-age-of-ai</id><content type="html" xml:base="https://batsov.com/articles/2026/03/09/emacs-and-vim-in-the-age-of-ai/"><![CDATA[<blockquote>
  <p>It’s tough to make predictions, especially about the future.</p>

  <p>– Yogi Berra</p>
</blockquote>

<p>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.</p>

<p>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.<sup id="fnref:1"><a href="#fn:1" class="footnote" rel="footnote" role="doc-noteref">1</a></sup></p>

<p>And lately, like everyone else in our industry, I’ve been playing with AI tools
– <a href="https://docs.anthropic.com/en/docs/claude-code">Claude Code</a> 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?</p>

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

<!--more-->

<h2 id="the-risks">The Risks</h2>

<blockquote>
  <p>The only thing that is constant is change.</p>

  <p>– Heraclitus</p>
</blockquote>

<p>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.</p>

<p>Let’s start with the challenges.</p>

<h3 id="the-ide-gravity-well">The IDE gravity well</h3>

<p>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.</p>

<p>On top of that, purpose-built AI editors like <a href="https://cursor.com">Cursor</a>,
<a href="https://windsurf.com">Windsurf</a>, 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.</p>

<p>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.</p>

<blockquote class="prompt-info">
  <p>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.</p>
</blockquote>

<h3 id="do-you-even-need-a-power-tool-anymore">Do you even need a “power tool” anymore?</h3>

<p>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.</p>

<p>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.</p>

<p>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.<sup id="fnref:2"><a href="#fn:2" class="footnote" rel="footnote" role="doc-noteref">2</a></sup>
Then again, maybe the question of <em>what</em> exactly you’re editing deserves a
closer look.</p>

<h3 id="the-corporate-backing-asymmetry">The corporate backing asymmetry</h3>

<p>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.</p>

<p>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.</p>

<h3 id="the-doomsday-scenario">The doomsday scenario</h3>

<p>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.</p>

<p>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).</p>

<h2 id="the-opportunities">The Opportunities</h2>

<p>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.</p>

<p>So let’s look at the other side of the coin.</p>

<h3 id="ai-helps-you-help-yourself">AI helps you help yourself</h3>

<p>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.</p>

<p>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.</p>

<p>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.</p>

<blockquote class="prompt-tip">
  <p>There’s at least one documented case of someone
<a href="https://www.reddit.com/r/emacs/comments/1ri5g5e/back_home_at_last_thanks_to_claude_code/">returning to Emacs after years away</a>,
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.</p>
</blockquote>

<h3 id="ai-makes-configuration-and-extension-trivial">AI makes configuration and extension trivial</h3>

<p>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.</p>

<p>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.</p>

<p>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.)</p>

<p>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.</p>

<h3 id="the-same-goes-for-plugin-development">The same goes for plugin development</h3>

<p>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.</p>

<p>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.</p>

<p>AI tools can help here in two ways. First, they lower the barrier for new
contributors – someone who understands the <em>concept</em> of what they want to
build can now get AI assistance with the <em>implementation</em> 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.</p>

<h3 id="ai-integrations-are-already-happening">AI integrations are already happening</h3>

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

<p><strong>Emacs:</strong></p>

<ul>
  <li><a href="https://github.com/karthink/gptel">gptel</a> – a versatile LLM client that
supports multiple backends (Claude, GPT, Gemini, local models)</li>
  <li><a href="https://github.com/s-kostyaev/ellama">ellama</a> – an Emacs interface for
interacting with LLMs via llama.cpp and Ollama</li>
  <li><a href="https://github.com/tninja/aider.el">aider.el</a> – Emacs integration for
<a href="https://aider.chat">Aider</a>, the popular AI pair programming tool</li>
  <li><a href="https://github.com/copilot-emacs/copilot.el">copilot.el</a> – GitHub Copilot
integration (I happen to be the current maintainer of the project)</li>
  <li><a href="https://github.com/lanceberge/elysium">elysium</a> – an AI-powered coding
assistant with inline diff application</li>
  <li><a href="https://github.com/xenodium/agent-shell">agent-shell</a> – a native Emacs
buffer for interacting with LLM agents (Claude Code, Gemini CLI, etc.)
via the <a href="https://agentclientprotocol.org/">Agent Client Protocol</a></li>
  <li><a href="https://eca.dev/">ECA</a> – an editor-agnostic AI coding assistant (built in
Clojure!) that works via a protocol similar to LSP, supporting multiple LLM
providers (and Neovim too)</li>
</ul>

<p><strong>Neovim:</strong></p>

<ul>
  <li><a href="https://github.com/yetone/avante.nvim">avante.nvim</a> – a Cursor-like AI
coding experience inside Neovim</li>
  <li><a href="https://github.com/olimorris/codecompanion.nvim">codecompanion.nvim</a> – a
Copilot Chat replacement supporting multiple LLM providers</li>
  <li><a href="https://github.com/zbirenbaum/copilot.lua">copilot.lua</a> – native Copilot
integration for Neovim</li>
  <li><a href="https://github.com/Robitx/gp.nvim">gp.nvim</a> – ChatGPT-like sessions in
Neovim with support for multiple providers</li>
</ul>

<p>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.</p>

<h3 id="terminal-native-ai-tools-are-a-natural-fit">Terminal-native AI tools are a natural fit</h3>

<p>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.<sup id="fnref:3"><a href="#fn:3" class="footnote" rel="footnote" role="doc-noteref">3</a></sup></p>

<p>Running Claude Code in an Emacs <code class="language-plaintext highlighter-rouge">vterm</code> 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.</p>

<p>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 <em>and</em> your own AI
tool, and they compose naturally.</p>

<blockquote class="prompt-info">
  <p>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 <em>that</em>. 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 <a href="https://agentclientprotocol.org/">Agent Client Protocol</a>
(ACP) enables – it lets editors like Emacs (via
<a href="https://github.com/xenodium/agent-shell">agent-shell</a>) 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.</p>
</blockquote>

<h3 id="emacs-as-an-ai-integration-platform">Emacs as an AI integration platform</h3>

<p>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.</p>

<p>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.</p>

<p>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 <code class="language-plaintext highlighter-rouge">markdown-mode</code>.) Still, I’m only one Emacs user and many are probably
using it in a more holistic manner.</p>

<h3 id="even-in-the-post-coding-apocalypse-emacs-and-vim-survive">Even in the post-coding apocalypse, Emacs and Vim survive</h3>

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

<p>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.</p>

<p>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
<em>program</em> fades, the Vim <em>idea</em> is immortal.<sup id="fnref:4"><a href="#fn:4" class="footnote" rel="footnote" role="doc-noteref">4</a></sup></p>

<p>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.<sup id="fnref:6"><a href="#fn:6" class="footnote" rel="footnote" role="doc-noteref">5</a></sup> “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.</p>

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

<h2 id="the-bigger-picture">The Bigger Picture</h2>

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

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

<p>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.</p>

<p>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.</p>

<h2 id="the-ethical-elephant-in-the-room">The Ethical Elephant in the Room</h2>

<p>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:</p>

<ul>
  <li>
    <p><strong>Energy consumption.</strong> 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.</p>
  </li>
  <li>
    <p><strong>Copyright and training data.</strong> 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.</p>
  </li>
  <li>
    <p><strong>Job displacement.</strong> 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 <em>human</em> programmers.</p>
  </li>
</ul>

<p>These concerns are already producing concrete action. The Vim community recently
saw the creation of <a href="https://codeberg.org/NerdNextDoor/evi">EVi</a>, a fork of Vim
whose entire raison d’etre is to provide a text editor free from AI-assisted
(generated?) code contributions.<sup id="fnref:5"><a href="#fn:5" class="footnote" rel="footnote" role="doc-noteref">6</a></sup> 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.</p>

<p>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.</p>

<h2 id="closing-thoughts">Closing Thoughts</h2>

<blockquote>
  <p>The future ain’t what it used to be.</p>

  <p>– Yogi Berra</p>
</blockquote>

<p>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.</p>

<p>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.</p>

<p>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.</p>

<p>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.</p>

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

<p>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.</p>

<p>That’s all I have for you today. Keep hacking!</p>

<p><strong>P.S.</strong> There’s an interesting <a href="https://news.ycombinator.com/item?id=47319071">Hacker News discussion</a> about this article. Check it out if you want to see what the broader community thinks!</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1">
      <p>If you’re curious about my Vim adventures, I wrote about them in <a href="/articles/2026/02/24/learning-vim-in-3-steps/">Learning Vim in 3 Steps</a>. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;&#xfe0e;</a></p>
    </li>
    <li id="fn:2">
      <p>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. <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;&#xfe0e;</a></p>
    </li>
    <li id="fn:3">
      <p>At least some of the time. Admittedly I usually use Emacs in GUI mode, but I always use (Neo)vim in the terminal. <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;&#xfe0e;</a></p>
    </li>
    <li id="fn:4">
      <p>Even Claude Code has vim mode. <a href="#fnref:4" class="reversefootnote" role="doc-backlink">&#8617;&#xfe0e;</a></p>
    </li>
    <li id="fn:6">
      <p>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. <a href="#fnref:6" class="reversefootnote" role="doc-backlink">&#8617;&#xfe0e;</a></p>
    </li>
    <li id="fn:5">
      <p>See <a href="https://codeberg.org/NerdNextDoor/evi/issues/1">https://codeberg.org/NerdNextDoor/evi/issues/1</a>. <a href="#fnref:5" class="reversefootnote" role="doc-backlink">&#8617;&#xfe0e;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Bozhidar Batsov</name></author><category term="Emacs" /><category term="Vim" /><category term="AI" /><summary type="html"><![CDATA[It’s tough to make predictions, especially about the future. – Yogi Berra]]></summary></entry><entry><title type="html">Building Emacs Major Modes with Tree-sitter: Lessons Learned</title><link href="https://batsov.com/articles/2026/02/27/building-emacs-major-modes-with-treesitter-lessons-learned/" rel="alternate" type="text/html" title="Building Emacs Major Modes with Tree-sitter: Lessons Learned" /><published>2026-02-27T10:00:00+02:00</published><updated>2026-02-27T10:00:00+02:00</updated><id>https://batsov.com/articles/2026/02/27/building-emacs-major-modes-with-treesitter-lessons-learned</id><content type="html" xml:base="https://batsov.com/articles/2026/02/27/building-emacs-major-modes-with-treesitter-lessons-learned/"><![CDATA[<p>Over the past year I’ve been spending a lot of time building <a href="https://tree-sitter.github.io/tree-sitter/">Tree-sitter</a>-powered
major modes for Emacs – <a href="https://github.com/clojure-emacs/clojure-ts-mode">clojure-ts-mode</a>
(as co-maintainer), <a href="https://github.com/bbatsov/neocaml">neocaml</a> (from scratch),
and <a href="https://github.com/bbatsov/asciidoc-mode">asciidoc-mode</a> (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.</p>

<!--more-->

<h2 id="why-tree-sitter">Why Tree-sitter?</h2>

<p>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:</p>

<ul>
  <li>
    <p><strong>Regex-based font-locking is fragile.</strong> 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.</p>
  </li>
  <li>
    <p><strong>Indentation engines are complex.</strong> 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.</p>
  </li>
</ul>

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

<div class="language-emacs-lisp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="c1">;; Highlight let-bound functions: match a let_binding with parameters</span>
<span class="p">(</span><span class="nv">let_binding</span> <span class="nv">pattern:</span> <span class="p">(</span><span class="nv">value_name</span><span class="p">)</span> <span class="nv">@font-lock-function-name-face</span>
             <span class="p">(</span><span class="nv">parameter</span><span class="p">)</span><span class="nb">+</span><span class="p">)</span>
</pre></td></tr></tbody></table></code></pre></div></div>

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

<div class="language-emacs-lisp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="c1">;; Children of a let_binding are indented by neocaml-indent-offset</span>
<span class="p">((</span><span class="nv">parent-is</span> <span class="s">"let_binding"</span><span class="p">)</span> <span class="nv">parent-bol</span> <span class="nv">neocaml-indent-offset</span><span class="p">)</span>
</pre></td></tr></tbody></table></code></pre></div></div>

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

<p>In practice, <code class="language-plaintext highlighter-rouge">neocaml</code>’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: <strong>simpler, more maintainable code that handles more
edge cases correctly</strong>.</p>

<h2 id="challenges">Challenges</h2>

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

<h3 id="every-grammar-is-different">Every grammar is different</h3>

<p>Tree-sitter grammars are written by different authors with different philosophies.
The <a href="https://github.com/tree-sitter/tree-sitter-ocaml">tree-sitter-ocaml</a>
grammar provides a rich, detailed AST with named fields. The
<a href="https://github.com/sogaiu/tree-sitter-clojure">tree-sitter-clojure</a> grammar,
by contrast, deliberately keeps things minimal – it only models syntax, not
semantics, because Clojure’s macro system makes static semantic analysis
unreliable.<sup id="fnref:1"><a href="#fn:1" class="footnote" rel="footnote" role="doc-noteref">1</a></sup> This means font-locking <code class="language-plaintext highlighter-rouge">def</code> forms in Clojure requires
predicate matching on symbol text, while in OCaml you can directly match
<code class="language-plaintext highlighter-rouge">let_binding</code> nodes with named fields.</p>

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

<div class="language-emacs-lisp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="c1">;; OCaml: grammar provides named fields -- direct structural match</span>
<span class="p">(</span><span class="nv">let_binding</span> <span class="nv">pattern:</span> <span class="p">(</span><span class="nv">value_name</span><span class="p">)</span> <span class="nv">@font-lock-function-name-face</span>
             <span class="p">(</span><span class="nv">parameter</span><span class="p">)</span><span class="nb">+</span><span class="p">)</span>
</pre></td></tr></tbody></table></code></pre></div></div>

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

<div class="language-emacs-lisp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="c1">;; Clojure: grammar is syntax-only -- match by symbol text</span>
<span class="p">((</span><span class="nv">list_lit</span> <span class="ss">:anchor</span> <span class="p">(</span><span class="nv">sym_lit</span> <span class="nv">!namespace</span>
                            <span class="nv">name:</span> <span class="p">(</span><span class="nv">sym_name</span><span class="p">)</span> <span class="nv">@font-lock-keyword-face</span><span class="p">))</span>
 <span class="p">(</span><span class="ss">:match</span> <span class="o">,</span><span class="nv">clojure-ts--definition-keyword-regexp</span> <span class="nv">@font-lock-keyword-face</span><span class="p">))</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>You can’t learn “how to write Tree-sitter queries” generically – you need to
learn each grammar individually. The best tool for this is <code class="language-plaintext highlighter-rouge">treesit-explore-mode</code>
(to visualize the full parse tree) and <code class="language-plaintext highlighter-rouge">treesit-inspect-mode</code> (to see the node
at point). Use them constantly.</p>

<h3 id="grammar-quality-varies-wildly">Grammar quality varies wildly</h3>

<p>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 <a href="https://github.com/tree-sitter">tree-sitter</a> GitHub org. The Clojure
grammar is small and stable by design. But not every language is so lucky.</p>

<p><a href="https://github.com/bbatsov/asciidoc-mode">asciidoc-mode</a> uses a
<a href="https://github.com/cathaysia/tree-sitter-asciidoc">third-party AsciiDoc grammar</a>
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 <code class="language-plaintext highlighter-rouge">markdown-ts-mode</code>,
and it makes sense for markup languages where block and inline syntax are largely
independent.</p>

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

<div class="language-emacs-lisp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
</pre></td><td class="rouge-code"><pre><span class="c1">;; Block-level rules use :override t so block-level faces win over</span>
<span class="c1">;; spurious inline emphasis nodes (the inline parser misreads `*'</span>
<span class="c1">;; list markers as emphasis delimiters).</span>
<span class="ss">:language</span> <span class="ss">'asciidoc</span>
<span class="ss">:override</span> <span class="no">t</span>
<span class="ss">:feature</span> <span class="ss">'list</span>
<span class="o">'</span><span class="p">((</span><span class="nv">ordered_list_marker</span><span class="p">)</span> <span class="nv">@font-lock-constant-face</span>
  <span class="p">(</span><span class="nv">unordered_list_marker</span><span class="p">)</span> <span class="nv">@font-lock-constant-face</span><span class="p">)</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>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.</p>

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

<div class="language-emacs-lisp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
</pre></td><td class="rouge-code"><pre><span class="c1">;; BAD: entire node gets skipped if any child is already fontified</span>
<span class="c1">;; (inline_macro) @font-lock-function-call-face</span>

<span class="c1">;; GOOD: capture specific children</span>
<span class="p">(</span><span class="nv">inline_macro</span> <span class="p">(</span><span class="nv">macro_name</span><span class="p">)</span> <span class="nv">@font-lock-function-call-face</span><span class="p">)</span>
<span class="p">(</span><span class="nv">inline_macro</span> <span class="p">(</span><span class="nv">target</span><span class="p">)</span> <span class="nv">@font-lock-string-face</span><span class="p">)</span>
</pre></td></tr></tbody></table></code></pre></div></div>

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

<h3 id="grammar-versions-and-breaking-changes">Grammar versions and breaking changes</h3>

<p>Grammars evolve, and breaking changes happen. <code class="language-plaintext highlighter-rouge">clojure-ts-mode</code> switched from
the stable grammar to the <a href="https://github.com/sogaiu/tree-sitter-clojure/tree/unstable-20250526">experimental
branch</a>
because the stable version had metadata nodes as children of other nodes, which
caused <code class="language-plaintext highlighter-rouge">forward-sexp</code> and <code class="language-plaintext highlighter-rouge">kill-sexp</code> to behave incorrectly. The experimental
grammar makes metadata standalone nodes, fixing the navigation issues but
requiring all queries to be updated.</p>

<p><code class="language-plaintext highlighter-rouge">neocaml</code> pins to
<a href="https://github.com/tree-sitter/tree-sitter-ocaml/tree/v0.24.0">v0.24.0</a> of the
OCaml grammar. If you don’t pin versions, a grammar update can silently break
your font-locking or indentation.</p>

<p>The takeaway: <strong>always pin your grammar version</strong>, and include a mechanism to
detect outdated grammars. <code class="language-plaintext highlighter-rouge">clojure-ts-mode</code> tests a query that changed between
versions to detect incompatible grammars at startup.</p>

<h3 id="grammar-delivery">Grammar delivery</h3>

<p>Users shouldn’t have to manually clone repos and compile C code to use your
mode. Both <code class="language-plaintext highlighter-rouge">neocaml</code> and <code class="language-plaintext highlighter-rouge">clojure-ts-mode</code> include grammar recipes:</p>

<div class="language-emacs-lisp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
</pre></td><td class="rouge-code"><pre><span class="p">(</span><span class="nv">defconst</span> <span class="nv">neocaml-grammar-recipes</span>
  <span class="o">'</span><span class="p">((</span><span class="nv">ocaml</span> <span class="s">"https://github.com/tree-sitter/tree-sitter-ocaml"</span>
           <span class="s">"v0.24.0"</span>
           <span class="s">"grammars/ocaml/src"</span><span class="p">)</span>
    <span class="p">(</span><span class="nv">ocaml-interface</span> <span class="s">"https://github.com/tree-sitter/tree-sitter-ocaml"</span>
                     <span class="s">"v0.24.0"</span>
                     <span class="s">"grammars/interface/src"</span><span class="p">)))</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>On first use, the mode checks <code class="language-plaintext highlighter-rouge">treesit-language-available-p</code> and offers to install
missing grammars via <code class="language-plaintext highlighter-rouge">treesit-install-language-grammar</code>. This works, but requires
a C compiler and Git on the user’s machine, which is not ideal.<sup id="fnref:2"><a href="#fn:2" class="footnote" rel="footnote" role="doc-noteref">2</a></sup></p>

<h3 id="the-emacs-tree-sitter-apis-are-a-moving-target">The Emacs Tree-sitter APIs are a moving target</h3>

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

<p><strong>Emacs 29</strong> introduced Tree-sitter support but lacked several APIs. For instance,
<code class="language-plaintext highlighter-rouge">treesit-thing-settings</code> (used for structured navigation) doesn’t exist – you
need a fallback:</p>

<div class="language-emacs-lisp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="c1">;; Fallback for Emacs 29 (no treesit-thing-settings)</span>
<span class="p">(</span><span class="nb">unless</span> <span class="p">(</span><span class="nb">boundp</span> <span class="ss">'treesit-thing-settings</span><span class="p">)</span>
  <span class="p">(</span><span class="nv">setq-local</span> <span class="nv">forward-sexp-function</span> <span class="nf">#'</span><span class="nv">neocaml-forward-sexp</span><span class="p">))</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><strong>Emacs 30</strong> added <code class="language-plaintext highlighter-rouge">treesit-thing-settings</code>, sentence navigation, and better
indentation support. But it also had a bug in <code class="language-plaintext highlighter-rouge">treesit-range-settings</code> offsets
(<a href="https://debbugs.gnu.org/cgi/bugreport.cgi?bug=77848">#77848</a>) that broke
embedded parsers, and another in <code class="language-plaintext highlighter-rouge">treesit-transpose-sexps</code> that required
<code class="language-plaintext highlighter-rouge">clojure-ts-mode</code> to disable its Tree-sitter-aware version.</p>

<p><strong>Emacs 31</strong> has a bug in <code class="language-plaintext highlighter-rouge">treesit-forward-comment</code> where an off-by-one error
causes <code class="language-plaintext highlighter-rouge">uncomment-region</code> to leave <code class="language-plaintext highlighter-rouge">*)</code> behind on multi-line OCaml comments. I
had to skip the affected test with a version check:</p>

<div class="language-emacs-lisp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="p">(</span><span class="nb">when</span> <span class="p">(</span><span class="nb">&gt;=</span> <span class="nv">emacs-major-version</span> <span class="mi">31</span><span class="p">)</span>
  <span class="p">(</span><span class="nb">signal</span> <span class="ss">'buttercup-pending</span>
          <span class="s">"Emacs 31 treesit-forward-comment bug (off-by-one)"</span><span class="p">))</span>
</pre></td></tr></tbody></table></code></pre></div></div>

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

<h3 id="no-scm-file-support-yet">No .scm file support (yet)</h3>

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

<p>This is tedious and error-prone. For example, here’s a rule from the OCaml
grammar’s <code class="language-plaintext highlighter-rouge">highlights.scm</code>:</p>

<div class="language-scheme highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
</pre></td><td class="rouge-code"><pre><span class="c1">;; upstream .scm (used by Neovim, Helix, etc.)</span>
<span class="p">(</span><span class="nf">constructor_name</span><span class="p">)</span> <span class="nv">@type</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>And here’s the Elisp equivalent you’d write for Emacs:</p>

<div class="language-emacs-lisp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="c1">;; Emacs equivalent -- wrapped in treesit-font-lock-rules</span>
<span class="ss">:language</span> <span class="ss">'ocaml</span>
<span class="ss">:feature</span> <span class="ss">'type</span>
<span class="o">'</span><span class="p">((</span><span class="nv">constructor_name</span><span class="p">)</span> <span class="nv">@font-lock-type-face</span><span class="p">)</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>The query syntax is nearly identical, but you have to wrap everything in
<code class="language-plaintext highlighter-rouge">treesit-font-lock-rules</code> calls, map upstream capture names (<code class="language-plaintext highlighter-rouge">@type</code>) to Emacs
face names (<code class="language-plaintext highlighter-rouge">@font-lock-type-face</code>), assign features, and manage <code class="language-plaintext highlighter-rouge">:override</code>
behavior. You end up maintaining a parallel set of queries that can drift from
upstream. Emacs 31 will introduce
<a href="https://github.com/emacs-mirror/emacs/blob/master/lisp/treesit-x.el#L47"><code class="language-plaintext highlighter-rouge">define-treesit-generic-mode</code></a>
which will make it possible to use <code class="language-plaintext highlighter-rouge">.scm</code> files for font-locking, which should
help significantly. But for now, you’re hand-coding everything.</p>

<h2 id="tips-and-tricks">Tips and tricks</h2>

<h3 id="debugging-font-locking">Debugging font-locking</h3>

<p>When a face isn’t being applied where you expect:</p>

<ol>
  <li>Use <code class="language-plaintext highlighter-rouge">treesit-inspect-mode</code> to verify the node type at point matches your
query.</li>
  <li>Set <code class="language-plaintext highlighter-rouge">treesit--font-lock-verbose</code> to <code class="language-plaintext highlighter-rouge">t</code> to see which rules are firing.</li>
  <li>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
<code class="language-plaintext highlighter-rouge">treesit-font-lock-feature-list</code>.</li>
  <li>Remember that <strong>rule order matters</strong>. Without <code class="language-plaintext highlighter-rouge">:override</code>, 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.</li>
</ol>

<h3 id="use-the-font-lock-levels-wisely">Use the font-lock levels wisely</h3>

<p>Tree-sitter modes define four levels of font-locking via
<code class="language-plaintext highlighter-rouge">treesit-font-lock-feature-list</code>, 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.</p>

<p>Less is more here. Here’s how <code class="language-plaintext highlighter-rouge">neocaml</code> distributes features across levels:</p>

<div class="language-emacs-lisp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="p">(</span><span class="nv">setq-local</span> <span class="nv">treesit-font-lock-feature-list</span>
            <span class="o">'</span><span class="p">((</span><span class="nv">comment</span> <span class="nv">definition</span><span class="p">)</span>
              <span class="p">(</span><span class="kt">keyword</span> <span class="nb">string</span> <span class="nc">number</span><span class="p">)</span>
              <span class="p">(</span><span class="nv">attribute</span> <span class="nv">builtin</span> <span class="nv">constant</span> <span class="k">type</span><span class="p">)</span>
              <span class="p">(</span><span class="nv">operator</span> <span class="nv">bracket</span> <span class="nv">delimiter</span> <span class="nv">variable</span> <span class="k">function</span><span class="p">)))</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>And <code class="language-plaintext highlighter-rouge">clojure-ts-mode</code> follows the same philosophy:</p>

<div class="language-emacs-lisp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="p">(</span><span class="nv">setq-local</span> <span class="nv">treesit-font-lock-feature-list</span>
            <span class="o">'</span><span class="p">((</span><span class="nv">comment</span> <span class="nv">definition</span><span class="p">)</span>
              <span class="p">(</span><span class="kt">keyword</span> <span class="nb">string</span> <span class="nb">char</span> <span class="nc">symbol</span> <span class="nv">builtin</span> <span class="k">type</span><span class="p">)</span>
              <span class="p">(</span><span class="nv">constant</span> <span class="nc">number</span> <span class="k">quote</span> <span class="nv">metadata</span> <span class="nv">doc</span> <span class="nv">regex</span><span class="p">)</span>
              <span class="p">(</span><span class="nv">bracket</span> <span class="nv">deref</span> <span class="k">function</span> <span class="nv">tagged-literals</span><span class="p">)))</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>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 <code class="language-plaintext highlighter-rouge">treesit-font-lock-level</code> to 4. Better
yet, they can use <code class="language-plaintext highlighter-rouge">treesit-font-lock-recompute-features</code> to cherry-pick
individual features regardless of level:</p>

<div class="language-emacs-lisp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
</pre></td><td class="rouge-code"><pre><span class="c1">;; Enable 'function' (level 4) without enabling all of level 4</span>
<span class="p">(</span><span class="nv">treesit-font-lock-recompute-features</span> <span class="o">'</span><span class="p">(</span><span class="k">function</span><span class="p">)</span> <span class="no">nil</span><span class="p">)</span>

<span class="c1">;; Disable 'bracket' even if the user's level would include it</span>
<span class="p">(</span><span class="nv">treesit-font-lock-recompute-features</span> <span class="no">nil</span> <span class="o">'</span><span class="p">(</span><span class="nv">bracket</span><span class="p">))</span>
</pre></td></tr></tbody></table></code></pre></div></div>

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

<h3 id="debugging-indentation">Debugging indentation</h3>

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

<ol>
  <li>Set <code class="language-plaintext highlighter-rouge">treesit--indent-verbose</code> to <code class="language-plaintext highlighter-rouge">t</code> – this logs which rule matched for each
line, what anchor was computed, and the final column.</li>
  <li>Use <code class="language-plaintext highlighter-rouge">treesit-explore-mode</code> to understand the parent chain. The key question
is always: “what is the parent node, and which rule matches it?”</li>
  <li>
    <p>Remember that <strong>rule order matters</strong> for indentation too – the first matching
rule wins. A typical set of rules reads top to bottom from most specific to
most general:</p>

    <div class="language-emacs-lisp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
</pre></td><td class="rouge-code"><pre><span class="c1">;; Closing delimiters align with the opening construct</span>
<span class="p">((</span><span class="nv">node-is</span> <span class="s">")"</span><span class="p">)</span> <span class="nv">parent-bol</span> <span class="mi">0</span><span class="p">)</span>
<span class="p">((</span><span class="nv">node-is</span> <span class="s">"end"</span><span class="p">)</span> <span class="nv">parent-bol</span> <span class="mi">0</span><span class="p">)</span>

<span class="c1">;; then/else clauses align with their enclosing if</span>
<span class="p">((</span><span class="nv">node-is</span> <span class="s">"then_clause"</span><span class="p">)</span> <span class="nv">parent-bol</span> <span class="mi">0</span><span class="p">)</span>
<span class="p">((</span><span class="nv">node-is</span> <span class="s">"else_clause"</span><span class="p">)</span> <span class="nv">parent-bol</span> <span class="mi">0</span><span class="p">)</span>

<span class="c1">;; Bodies inside then/else are indented</span>
<span class="p">((</span><span class="nv">parent-is</span> <span class="s">"then_clause"</span><span class="p">)</span> <span class="nv">parent-bol</span> <span class="nv">neocaml-indent-offset</span><span class="p">)</span>
<span class="p">((</span><span class="nv">parent-is</span> <span class="s">"else_clause"</span><span class="p">)</span> <span class="nv">parent-bol</span> <span class="nv">neocaml-indent-offset</span><span class="p">)</span>
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
  <li>
    <p>Watch out for the <strong>empty-line problem</strong>: when the cursor is on a blank line,
Tree-sitter has no node at point. The indentation engine falls back to the root
<code class="language-plaintext highlighter-rouge">compilation_unit</code> node as the parent, which typically matches the top-level
rule and gives column 0. In neocaml I solved this with a <code class="language-plaintext highlighter-rouge">no-node</code> rule that
looks at the previous line’s last token to decide indentation:</p>

    <div class="language-emacs-lisp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="p">(</span><span class="nv">no-node</span> <span class="nv">prev-line</span> <span class="nv">neocaml--empty-line-offset</span><span class="p">)</span>
</pre></td></tr></tbody></table></code></pre></div>    </div>
  </li>
</ol>

<h3 id="build-a-comprehensive-test-suite">Build a comprehensive test suite</h3>

<p>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
<a href="https://github.com/jorgenschaefer/emacs-buttercup">Buttercup</a> (a BDD testing
framework for Emacs) with custom test macros.</p>

<p><strong>Font-lock tests</strong> insert code into a buffer, run <code class="language-plaintext highlighter-rouge">font-lock-ensure</code>, and assert
that specific character ranges have the expected face:</p>

<div class="language-emacs-lisp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="p">(</span><span class="nv">when-fontifying-it</span> <span class="s">"fontifies let-bound functions"</span>
  <span class="p">(</span><span class="s">"let greet name = ..."</span>
   <span class="p">(</span><span class="mi">5</span> <span class="mi">9</span> <span class="nv">font-lock-function-name-face</span><span class="p">)))</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><strong>Indentation tests</strong> insert code, run <code class="language-plaintext highlighter-rouge">indent-region</code>, and assert the result
matches the expected indentation:</p>

<div class="language-emacs-lisp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
</pre></td><td class="rouge-code"><pre><span class="p">(</span><span class="nv">when-indenting-it</span> <span class="s">"indents a match expression"</span>
  <span class="s">"match x with"</span>
  <span class="s">"| 0 -&gt; \"zero\""</span>
  <span class="s">"| n -&gt; string_of_int n"</span><span class="p">)</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p><strong>Integration tests</strong> load real source files and verify that both font-locking
and indentation survive <code class="language-plaintext highlighter-rouge">indent-region</code> on the full file. This catches
interactions between rules that unit tests miss.</p>

<p><code class="language-plaintext highlighter-rouge">neocaml</code> has 200+ automated tests and <code class="language-plaintext highlighter-rouge">clojure-ts-mode</code> has even more.
Investing in test infrastructure early pays off enormously – I can refactor
indentation rules with confidence because the suite catches regressions
immediately.</p>

<h4 id="a-personal-story-on-testing-roi">A personal story on testing ROI</h4>

<p>When I became the maintainer of
<a href="https://github.com/clojure-emacs/clojure-mode">clojure-mode</a> 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.</p>

<p>The same approach – almost the same test macros – carried over directly to
<code class="language-plaintext highlighter-rouge">clojure-ts-mode</code> when we built the Tree-sitter version. And later I reused the
pattern again in <code class="language-plaintext highlighter-rouge">neocaml</code> and <code class="language-plaintext highlighter-rouge">asciidoc-mode</code>. One investment in testing
infrastructure, four projects benefiting from it.</p>

<p>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.</p>

<h3 id="pre-compile-queries">Pre-compile queries</h3>

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

<h2 id="a-note-on-naming">A note on naming</h2>

<p>The Emacs community has settled on a <code class="language-plaintext highlighter-rouge">-ts-mode</code> suffix convention for
Tree-sitter-based modes: <code class="language-plaintext highlighter-rouge">python-ts-mode</code>, <code class="language-plaintext highlighter-rouge">c-ts-mode</code>, <code class="language-plaintext highlighter-rouge">ruby-ts-mode</code>, 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.</p>

<p>For new packages that don’t have a legacy counterpart, the <code class="language-plaintext highlighter-rouge">-ts-mode</code> suffix is
unnecessary. I named my packages <code class="language-plaintext highlighter-rouge">neocaml</code> (not <code class="language-plaintext highlighter-rouge">ocaml-ts-mode</code>) and
<code class="language-plaintext highlighter-rouge">asciidoc-mode</code> (not <code class="language-plaintext highlighter-rouge">adoc-ts-mode</code>) because there was no prior <code class="language-plaintext highlighter-rouge">neocaml-mode</code>
or <code class="language-plaintext highlighter-rouge">asciidoc-mode</code> to disambiguate from. The <code class="language-plaintext highlighter-rouge">-ts-</code> 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?</p>

<p>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.</p>

<h2 id="the-road-ahead">The road ahead</h2>

<p>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.</p>

<p>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
<code class="language-plaintext highlighter-rouge">clojure-ts-mode</code>, it could probably scaffold a reasonable new mode fairly
quickly. The hard parts – debugging grammar quirks, handling edge cases, getting
indentation <em>just right</em> – would still need human attention, but the boilerplate
could be automated.</p>

<p>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.</p>

<h2 id="closing-thoughts">Closing thoughts</h2>

<p>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.</p>

<p>But it’s not magical. Grammars are inconsistent across languages, the Emacs APIs
are still maturing, you can’t reuse <code class="language-plaintext highlighter-rouge">.scm</code> 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.</p>

<p>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.</p>

<p>Anyways, I wish there was an article like this one when I was starting out
with <code class="language-plaintext highlighter-rouge">clojure-ts-mode</code> and <code class="language-plaintext highlighter-rouge">neocaml</code>, 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.</p>

<p>That’s all I have for you today. Keep hacking!</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1">
      <p>See the excellent <a href="https://github.com/sogaiu/tree-sitter-clojure/blob/master/doc/scope.md">scope discussion</a> in the tree-sitter-clojure repo for the rationale. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;&#xfe0e;</a></p>
    </li>
    <li id="fn:2">
      <p>There’s ongoing discussion in the Emacs community about distributing pre-compiled grammar binaries, but nothing concrete yet. <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;&#xfe0e;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Bozhidar Batsov</name></author><category term="Emacs" /><category term="OCaml" /><category term="Clojure" /><category term="AsciiDoc" /><summary type="html"><![CDATA[Over 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.]]></summary></entry><entry><title type="html">Setting up Emacs for OCaml Development: Neocaml Edition</title><link href="https://batsov.com/articles/2026/02/24/setting-up-emacs-for-ocaml-development-neocaml-edition/" rel="alternate" type="text/html" title="Setting up Emacs for OCaml Development: Neocaml Edition" /><published>2026-02-24T12:00:00+02:00</published><updated>2026-02-24T12:00:00+02:00</updated><id>https://batsov.com/articles/2026/02/24/setting-up-emacs-for-ocaml-development-neocaml-edition</id><content type="html" xml:base="https://batsov.com/articles/2026/02/24/setting-up-emacs-for-ocaml-development-neocaml-edition/"><![CDATA[<p>A few years ago I wrote about <a href="/articles/2022/08/23/setting-up-emacs-for-ocaml-development/">setting up Emacs for OCaml
development</a>.
Back then the recommended stack was <code class="language-plaintext highlighter-rouge">tuareg-mode</code> + <code class="language-plaintext highlighter-rouge">merlin-mode</code>, 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.</p>

<h2 id="the-new-stack">The New Stack</h2>

<p>Here’s what I recommend today:</p>

<ul>
  <li><a href="https://github.com/bbatsov/neocaml">neocaml</a> instead of <code class="language-plaintext highlighter-rouge">tuareg-mode</code></li>
  <li><a href="https://github.com/tarides/ocaml-eglot">ocaml-eglot</a> instead of <code class="language-plaintext highlighter-rouge">merlin-mode</code></li>
  <li><a href="https://www.gnu.org/software/emacs/manual/html_mono/eglot.html">Eglot</a> (built into Emacs 29+) as the LSP client</li>
</ul>

<p>The key shift is from Merlin’s custom protocol to LSP.
<a href="https://github.com/ocaml/ocaml-lsp">ocaml-lsp-server</a> 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. <code class="language-plaintext highlighter-rouge">ocaml-eglot</code> is a lightweight Emacs package by
<a href="https://tarides.com/">Tarides</a> that bridges Eglot with these OCaml-specific
LSP extensions, giving you the full Merlin feature set through a standardized
protocol.</p>

<p>And <code class="language-plaintext highlighter-rouge">neocaml</code> is my own Tree-sitter-powered OCaml major mode – modern, lean,
and built for the LSP era. You can read more about it in the <a href="/articles/2026/02/14/neocaml-0-1-ready-for-action/">0.1 release
announcement</a>.</p>

<h2 id="the-essentials">The Essentials</h2>

<p>First, install the server-side tools:</p>

<div class="language-console highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre><span class="gp">$</span><span class="w"> </span>opam <span class="nb">install </span>ocaml-lsp-server
</pre></td></tr></tbody></table></code></pre></div></div>

<blockquote class="prompt-tip">
  <p>You no longer need to install <code class="language-plaintext highlighter-rouge">merlin</code> separately – <code class="language-plaintext highlighter-rouge">ocaml-lsp-server</code>
vendors it internally.</p>
</blockquote>

<p>Then set up Emacs:</p>

<div class="language-emacs-lisp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
4
5
6
7
8
9
10
11
12
</pre></td><td class="rouge-code"><pre><span class="c1">;; Modern Tree-sitter-powered OCaml major mode</span>
<span class="p">(</span><span class="nb">use-package</span> <span class="nv">neocaml</span>
  <span class="ss">:ensure</span> <span class="no">t</span><span class="p">)</span>

<span class="c1">;; Major mode for editing Dune project files</span>
<span class="p">(</span><span class="nb">use-package</span> <span class="nv">dune</span>
  <span class="ss">:ensure</span> <span class="no">t</span><span class="p">)</span>

<span class="c1">;; OCaml-specific LSP extensions via Eglot</span>
<span class="p">(</span><span class="nb">use-package</span> <span class="nv">ocaml-eglot</span>
  <span class="ss">:ensure</span> <span class="no">t</span>
  <span class="ss">:hook</span> <span class="p">(</span><span class="nv">neocaml-mode</span> <span class="o">.</span> <span class="nv">ocaml-eglot-setup</span><span class="p">))</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>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 <code class="language-plaintext highlighter-rouge">ocaml-lsp-server</code> and you’ll have completion, type information, code
navigation, diagnostics, and all the other goodies you’d expect.</p>

<p>Compare this to the old setup – no more <code class="language-plaintext highlighter-rouge">merlin-mode</code>, <code class="language-plaintext highlighter-rouge">merlin-eldoc</code>,
<code class="language-plaintext highlighter-rouge">flycheck-ocaml</code>, or manual Company configuration. LSP handles all of it
through a single, uniform interface.</p>

<h2 id="the-toplevel">The Toplevel</h2>

<p><code class="language-plaintext highlighter-rouge">neocaml</code> includes built-in REPL integration via <code class="language-plaintext highlighter-rouge">neocaml-repl-minor-mode</code>. The
basics work well:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">C-c C-z</code> – start or switch to the OCaml toplevel</li>
  <li><code class="language-plaintext highlighter-rouge">C-c C-c</code> – send the current definition</li>
  <li><code class="language-plaintext highlighter-rouge">C-c C-r</code> – send the selected region</li>
  <li><code class="language-plaintext highlighter-rouge">C-c C-b</code> – send the entire buffer</li>
</ul>

<p>If you want <code class="language-plaintext highlighter-rouge">utop</code> specifically, you’re still better off using
<a href="https://github.com/ocaml-community/utop">utop.el</a> alongside <code class="language-plaintext highlighter-rouge">neocaml</code>. Its
main advantage is that you get code completion inside the <code class="language-plaintext highlighter-rouge">utop</code> REPL within
Emacs – something <code class="language-plaintext highlighter-rouge">neocaml</code>’s built-in REPL integration doesn’t provide:</p>

<div class="language-emacs-lisp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
2
3
</pre></td><td class="rouge-code"><pre><span class="p">(</span><span class="nb">use-package</span> <span class="nv">utop</span>
  <span class="ss">:ensure</span> <span class="no">t</span>
  <span class="ss">:hook</span> <span class="p">(</span><span class="nv">neocaml-mode</span> <span class="o">.</span> <span class="nv">utop-minor-mode</span><span class="p">))</span>
</pre></td></tr></tbody></table></code></pre></div></div>

<p>This will shadow <code class="language-plaintext highlighter-rouge">neocaml</code>’s REPL keybindings with <code class="language-plaintext highlighter-rouge">utop</code>’s, which is the
intended behavior.</p>

<p>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 <a href="https://discuss.ocaml.org/t/whats-your-development-workflow/10358/7">this OCaml Discuss
thread</a> –
running <code class="language-plaintext highlighter-rouge">dune runtest</code> 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.</p>

<h2 id="give-neocaml-a-try">Give neocaml a Try</h2>

<p>If you’re an OCaml programmer using Emacs, I’d love for you to take
<a href="https://github.com/bbatsov/neocaml">neocaml</a> for a spin. It’s available on
MELPA, so getting started is just an <code class="language-plaintext highlighter-rouge">M-x package-install</code> 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.</p>

<p>That’s all I have for you today. Keep hacking!</p>]]></content><author><name>Bozhidar Batsov</name></author><category term="Emacs" /><category term="OCaml" /><summary type="html"><![CDATA[A 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.]]></summary></entry><entry><title type="html">Learning Vim in 3 Steps</title><link href="https://batsov.com/articles/2026/02/24/learning-vim-in-3-steps/" rel="alternate" type="text/html" title="Learning Vim in 3 Steps" /><published>2026-02-24T11:00:00+02:00</published><updated>2026-02-24T11:00:00+02:00</updated><id>https://batsov.com/articles/2026/02/24/learning-vim-in-3-steps</id><content type="html" xml:base="https://batsov.com/articles/2026/02/24/learning-vim-in-3-steps/"><![CDATA[<p>Every now and then someone asks me how to learn Vim.<sup id="fnref:1"><a href="#fn:1" class="footnote" rel="footnote" role="doc-noteref">1</a></sup> 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.</p>

<h2 id="step-1-learn-the-basics">Step 1: Learn the Basics</h2>

<p>Start with <code class="language-plaintext highlighter-rouge">vimtutor</code> – it ships with Vim and takes about 30 minutes. It’ll
teach you enough to survive: moving around, editing text, saving, quitting. The
essentials.</p>

<p>Once you’re past that, I strongly recommend <a href="https://pragprog.com/titles/dnvim2/practical-vim-second-edition/">Practical
Vim</a> 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 <em>clicked</em> for me
until I read it. The key insight is that Vim has a <strong>grammar</strong> – operators
(verbs) combine with motions (nouns) to form commands. <code class="language-plaintext highlighter-rouge">d</code> (delete) + <code class="language-plaintext highlighter-rouge">w</code>
(word) = <code class="language-plaintext highlighter-rouge">dw</code>. <code class="language-plaintext highlighter-rouge">c</code> (change) + <code class="language-plaintext highlighter-rouge">i"</code> (inside quotes) = <code class="language-plaintext highlighter-rouge">ci"</code>. Once you
internalize this composable language, you stop memorizing individual commands
and start <em>thinking in Vim</em>. The book is structured as 121 self-contained tips
rather than a linear tutorial, which makes it great for dipping in and out.</p>

<blockquote class="prompt-info">
  <p>You could also just read <code class="language-plaintext highlighter-rouge">:help</code> cover to cover – Vim’s built-in
documentation is excellent. But let’s be honest, few people have that kind of
patience.</p>
</blockquote>

<p>Other resources worth checking out:</p>

<ul>
  <li><a href="https://www.youtube.com/playlist?list=PLAgc_JOvkdotxLmxRmcck2AhAF6GlbhlH">Advent of Vim</a>
– a playlist of short video tutorials covering basic Vim topics. Great for
visual learners who prefer bite-sized lessons.</li>
  <li><a href="https://theprimeagen.github.io/vim-fundamentals/">ThePrimeagen’s Vim Fundamentals</a>
– if you prefer video content and a more energetic teaching style.</li>
  <li><a href="https://github.com/ThePrimeagen/vim-be-good">vim-be-good</a> – a Neovim
plugin that gamifies Vim practice. Good for building muscle memory.</li>
</ul>

<h2 id="step-2-start-small">Step 2: Start Small</h2>

<p>Resist the temptation to grab a massive Neovim distribution like
<a href="https://www.lazyvim.org/">LazyVim</a> 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.</p>

<p>Instead, start with a minimal configuration and grow it gradually. I wrote
about this in detail in <a href="/articles/2026/02/22/how-to-vim-build-your-vimrc-from-scratch/">Build your .vimrc from
Scratch</a>
– the short version is that modern Vim and Neovim ship with excellent defaults
and you can get surprisingly far with a handful of settings.</p>

<p>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.</p>

<h2 id="step-3-practice-for-10-years">Step 3: Practice for 10 Years</h2>

<p>I’m only half joking. Peter Norvig’s famous essay <a href="https://norvig.com/21-days.html">Teach Yourself Programming
in Ten Years</a> 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.</p>

<p>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.</p>

<blockquote class="prompt-info">
  <p>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.</p>
</blockquote>

<p>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.</p>

<h2 id="plan-b">Plan B</h2>

<p>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 <a href="https://github.com/emacs-evil/evil">evil-mode</a> so I can have the
best of both worlds. As I like to say:</p>

<blockquote>
  <p>The road to Emacs mastery is paved with a lifetime of <code class="language-plaintext highlighter-rouge">M-x</code> invocations.</p>
</blockquote>

<p>That’s all I have for you today. Keep hacking!</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1">
      <p>Just kidding – everyone asks me about learning Emacs. But here we are. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;&#xfe0e;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Bozhidar Batsov</name></author><category term="Vim" /><category term="Emacs" /><summary type="html"><![CDATA[Every 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. Just kidding – everyone asks me about learning Emacs. But here we are. &#8617;&#xfe0e;]]></summary></entry><entry><title type="html">Neocaml 0.1: Ready for Action</title><link href="https://batsov.com/articles/2026/02/14/neocaml-0-1-ready-for-action/" rel="alternate" type="text/html" title="Neocaml 0.1: Ready for Action" /><published>2026-02-14T18:34:00+02:00</published><updated>2026-02-14T18:34:00+02:00</updated><id>https://batsov.com/articles/2026/02/14/neocaml-0-1-ready-for-action</id><content type="html" xml:base="https://batsov.com/articles/2026/02/14/neocaml-0-1-ready-for-action/"><![CDATA[<p><a href="https://github.com/bbatsov/neocaml">neocaml</a> 0.1 is finally out! Almost a year
after I <a href="/articles/2025/03/14/neocaml-a-new-emacs-package-for-ocaml-programming/">announced the
project</a>,
I’m happy to report that it has matured to the point where I feel comfortable
calling it ready for action. Even better - <code class="language-plaintext highlighter-rouge">neocaml</code> recently landed in
<a href="https://melpa.org/#/neocaml">MELPA</a>, which means installing it is now as easy
as:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><table class="rouge-table"><tbody><tr><td class="rouge-gutter gl"><pre class="lineno">1
</pre></td><td class="rouge-code"><pre>M-x package-install &lt;RET&gt; neocaml &lt;RET&gt;
</pre></td></tr></tbody></table></code></pre></div></div>

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

<h2 id="why-neocaml">Why neocaml?</h2>

<p>You might be wondering what’s wrong with the existing options. The short answer -
nothing is <em>wrong</em> per se, but <code class="language-plaintext highlighter-rouge">neocaml</code> offers a different set of trade-offs:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">caml-mode</code> 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.</li>
  <li><code class="language-plaintext highlighter-rouge">tuareg-mode</code> 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.</li>
  <li><code class="language-plaintext highlighter-rouge">neocaml</code> aims to be a modern, lean alternative that fully embraces
Tree-sitter. The codebase is small, well-documented, and easy to hack on. If
you’re running Emacs 29+ (and especially Emacs 30), Tree-sitter is the future
and <code class="language-plaintext highlighter-rouge">neocaml</code> is built entirely around it.</li>
</ul>

<p>Of course, <code class="language-plaintext highlighter-rouge">neocaml</code> 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.</p>

<p>I’ve started the project mostly because I thought that the existing
Emacs tooling for OCaml was somewhat behind the times - e.g. both
<code class="language-plaintext highlighter-rouge">caml-mode</code> and <code class="language-plaintext highlighter-rouge">tuareg-mode</code> have features that are no longer needed
in the era of <code class="language-plaintext highlighter-rouge">ocamllsp</code>.</p>

<p>Let me now walk you through the highlights of version 0.1.</p>

<h2 id="features">Features</h2>

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

<h3 id="tree-sitter-powered-syntax-highlighting">Tree-sitter-powered Syntax Highlighting</h3>

<p><code class="language-plaintext highlighter-rouge">neocaml</code> leverages Tree-sitter for syntax highlighting, which is both more
accurate and more performant than the traditional regex-based approaches used by
<code class="language-plaintext highlighter-rouge">caml-mode</code> and <code class="language-plaintext highlighter-rouge">tuareg-mode</code>. The font-locking supports 4 customizable
intensity levels (controlled via <code class="language-plaintext highlighter-rouge">treesit-font-lock-level</code>, default 3), so you
can pick the amount of color that suits your taste.</p>

<p>Both <code class="language-plaintext highlighter-rouge">.ml</code> (source) and <code class="language-plaintext highlighter-rouge">.mli</code> (interface) files get their own major modes with
dedicated highlighting rules.</p>

<h3 id="tree-sitter-powered-indentation">Tree-sitter-powered Indentation</h3>

<p>Indentation has always been tricky for OCaml modes, and I won’t pretend it’s
perfect yet, but <code class="language-plaintext highlighter-rouge">neocaml</code>’s Tree-sitter-based indentation engine is already quite
usable. It also supports cycle-indent functionality, so hitting <code class="language-plaintext highlighter-rouge">TAB</code> repeatedly
will cycle through plausible indentation levels - a nice quality-of-life feature
when the indentation rules can’t fully determine the “right” indent.</p>

<p>If you prefer, you can still delegate indentation to external tools like
<code class="language-plaintext highlighter-rouge">ocp-indent</code> or even Tuareg’s indentation functions. Still, I think most people
will be quite satisfied with the built-in indentation logic.</p>

<h3 id="code-navigation-and-imenu">Code Navigation and Imenu</h3>

<p><code class="language-plaintext highlighter-rouge">neocaml</code> provides proper structural navigation commands (<code class="language-plaintext highlighter-rouge">beginning-of-defun</code>,
<code class="language-plaintext highlighter-rouge">end-of-defun</code>, <code class="language-plaintext highlighter-rouge">forward-sexp</code>) powered by Tree-sitter, plus <code class="language-plaintext highlighter-rouge">imenu</code> integration –
navigating definitions in a buffer has never been easier.</p>

<p>The older modes provide very similar functionality as well, of course,
but the use of Tree-sitter in <code class="language-plaintext highlighter-rouge">neocaml</code> makes such commands more reliable and
robust.</p>

<h3 id="repl-integration">REPL Integration</h3>

<p>No OCaml mode would be complete without REPL (toplevel) integration. <code class="language-plaintext highlighter-rouge">neocaml-repl-minor-mode</code>
provides all the essentials:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">C-c C-z</code> - Start or switch to the OCaml REPL</li>
  <li><code class="language-plaintext highlighter-rouge">C-c C-c</code> - Send the current definition</li>
  <li><code class="language-plaintext highlighter-rouge">C-c C-r</code> - Send the selected region</li>
  <li><code class="language-plaintext highlighter-rouge">C-c C-b</code> - Send the entire buffer</li>
  <li><code class="language-plaintext highlighter-rouge">C-c C-p</code> - Send a phrase (code until <code class="language-plaintext highlighter-rouge">;;</code>)</li>
</ul>

<p>The default REPL is <code class="language-plaintext highlighter-rouge">ocaml</code>, but you can easily switch to <code class="language-plaintext highlighter-rouge">utop</code> via
<code class="language-plaintext highlighter-rouge">neocaml-repl-program-name</code>.</p>

<p>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.</p>

<h3 id="lsp-support">LSP Support</h3>

<p><code class="language-plaintext highlighter-rouge">neocaml</code> works great with <a href="https://www.gnu.org/software/emacs/manual/html_mono/eglot.html">Eglot</a> and
<code class="language-plaintext highlighter-rouge">ocamllsp</code>, automatically setting the appropriate language IDs for both <code class="language-plaintext highlighter-rouge">.ml</code> and
<code class="language-plaintext highlighter-rouge">.mli</code> files. Pair <code class="language-plaintext highlighter-rouge">neocaml</code> with
<a href="https://github.com/tarides/ocaml-eglot">ocaml-eglot</a> and you get a pretty
solid OCaml development experience.</p>

<p>The creation of LSP really simplified the lives of 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.</p>

<p>That’s also another reason why you probably want a leaner major mode like <code class="language-plaintext highlighter-rouge">neocaml-mode</code>.</p>

<h3 id="other-goodies">Other Goodies</h3>

<p>But, wait, there’s more!</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">C-c C-a</code> to quickly switch between <code class="language-plaintext highlighter-rouge">.ml</code> and <code class="language-plaintext highlighter-rouge">.mli</code> files</li>
  <li>Prettify-symbols support for common OCaml operators</li>
  <li>Automatic installation of the required Tree-sitter grammars via <code class="language-plaintext highlighter-rouge">M-x neocaml-install-grammars</code></li>
  <li>Compatibility with <a href="https://github.com/ocaml/merlin">Merlin</a> for those who prefer it over LSP</li>
</ul>

<h2 id="the-road-ahead">The Road Ahead</h2>

<p>There’s still plenty of work to do:</p>

<ul>
  <li>Support for additional OCaml file types (e.g. <code class="language-plaintext highlighter-rouge">.mld</code>)</li>
  <li>Improvements to structured navigation using newer Emacs Tree-sitter APIs</li>
  <li>Improvements to the test suite</li>
  <li>Addressing feedback from real-world OCaml users</li>
  <li>Actually writing some fun OCaml code with <code class="language-plaintext highlighter-rouge">neocaml</code></li>
</ul>

<p>If you’re following me, you probably know that I’m passionate about both Emacs
and OCaml. I hope that <code class="language-plaintext highlighter-rouge">neocaml</code> will be my way to contribute to the awesome
OCaml community.</p>

<p>I’m not sure how quickly things will move, but I’m committed to making <code class="language-plaintext highlighter-rouge">neocaml</code>
the best OCaml editing experience on Emacs. Time will tell how far I’ll get!</p>

<h2 id="give-it-a-try">Give it a Try</h2>

<p>If you’re an OCaml programmer using Emacs, I’d love for you to take <code class="language-plaintext highlighter-rouge">neocaml</code> 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
<a href="https://github.com/bbatsov/neocaml">GitHub</a>!</p>

<p>That’s all from me, folks! Keep hacking!</p>]]></content><author><name>Bozhidar Batsov</name></author><category term="OCaml" /><category term="Emacs" /><summary type="html"><![CDATA[neocaml 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:]]></summary></entry><entry><title type="html">Burst-driven Development: My Approach to OSS Projects Maintenance</title><link href="https://batsov.com/articles/2025/11/16/burst-driven-development-my-approach-to-oss-projects-maintenance/" rel="alternate" type="text/html" title="Burst-driven Development: My Approach to OSS Projects Maintenance" /><published>2025-11-16T14:16:00+02:00</published><updated>2025-11-19T00:00:00+02:00</updated><id>https://batsov.com/articles/2025/11/16/burst-driven-development-my-approach-to-oss-projects-maintenance</id><content type="html" xml:base="https://batsov.com/articles/2025/11/16/burst-driven-development-my-approach-to-oss-projects-maintenance/"><![CDATA[<p>I’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 <a href="https://batsov.com/projects">couple of dozen OSS projects</a>
in the realms of Emacs, Clojure and Ruby mostly. Some of them even happen to
have many users!</p>

<p>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.</p>

<p>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.</p>

<p>The time I’ve spent developing OSS projects has taught me that:</p>

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

<p>To illustrate all of the above with some example, let me tell you a bit about
<a href="https://github.com/copilot-emacs/copilot.el/releases/tag/v0.3.0">copilot.el 0.3</a>.
I became the primary maintainer of <code class="language-plaintext highlighter-rouge">copilot.el</code> 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 <code class="language-plaintext highlighter-rouge">copilot.el</code> 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.</p>

<p>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.:</p>

<ul>
  <li>Before Clojure conferences I’d usually focus on improvements in CIDER and nREPL</li>
  <li>Before Ruby conferences I’d focus on RuboCop</li>
</ul>

<p>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…</p>

<p>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!</p>]]></content><author><name>Bozhidar Batsov</name></author><category term="OSS" /><category term="Maintenance" /><category term="Emacs" /><summary type="html"><![CDATA[I’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!]]></summary></entry><entry><title type="html">Rediscovering Vim</title><link href="https://batsov.com/articles/2025/05/20/rediscovering-vim/" rel="alternate" type="text/html" title="Rediscovering Vim" /><published>2025-05-20T18:09:00+03:00</published><updated>2025-05-25T00:00:00+03:00</updated><id>https://batsov.com/articles/2025/05/20/rediscovering-vim</id><content type="html" xml:base="https://batsov.com/articles/2025/05/20/rediscovering-vim/"><![CDATA[<p>Most 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.</p>

<p>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”.</p>

<p>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.<sup id="fnref:1"><a href="#fn:1" class="footnote" rel="footnote" role="doc-noteref">1</a></sup>
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.</p>

<p>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. <code class="language-plaintext highlighter-rouge">:help</code> is your friend!</p>

<p>Initially, I focused on neovim for a couple of reasons:</p>

<ul>
  <li>Lua for the configuration and the plugins</li>
  <li>Built-in support for things like LSP and Tree-sitter</li>
  <li>Many distros and a ton of “modern” plugins</li>
</ul>

<p>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 <code class="language-plaintext highlighter-rouge">.vimrc</code>
and work my way up from there, same as I did with Emacs 20 years ago.</p>

<p>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:</p>

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

<p>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.<sup id="fnref:2"><a href="#fn:2" class="footnote" rel="footnote" role="doc-noteref">2</a></sup></p>

<p>I was also a bit confused by the native Vim 8 package system<sup id="fnref:3"><a href="#fn:3" class="footnote" rel="footnote" role="doc-noteref">3</a></sup>, as it’s little
more than some predefined filesystem locations and structure for the Vim
packages.  I had expected something closer to Emacs’s <code class="language-plaintext highlighter-rouge">package.el</code>. Anyways
<code class="language-plaintext highlighter-rouge">vim-plug</code> is pretty good and there are a ton of other package managers out
there.</p>

<p>Some other pet peeves I have with Vim are:</p>

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

<p>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 <code class="language-plaintext highlighter-rouge">.vimrc</code>. 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.</p>

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

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

<p>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)</p>

<p>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)
<code class="language-plaintext highlighter-rouge">file</code> 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!</p>

<p>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 <code class="language-plaintext highlighter-rouge">evil-mode</code> 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 <code class="language-plaintext highlighter-rouge">evil-mode</code> 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.</p>

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

<ul>
  <li>getting started with Vim</li>
  <li>my Vim configuration</li>
  <li>comparisons between Vim and Emacs today</li>
  <li>some tips and tricks</li>
</ul>

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

<p>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 <code class="language-plaintext highlighter-rouge">.vimrc</code> or <code class="language-plaintext highlighter-rouge">init.lua</code>.</p>

<p>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!</p>

<h2 id="articles-in-the-series">Articles in the Series</h2>

<ul>

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

  
    <li><a href="/articles/2025/05/20/rediscovering-vim/">Rediscovering Vim</a></li>
    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

  
    <li><a href="/articles/2025/05/23/how-to-vim-format-lines/">How to Vim: Format Lines &amp; Paragraphs</a></li>
    <!-- tags if -->

  
    <li><a href="/articles/2025/05/25/how-to-vim-toggle-comments/">How to Vim: Toggle Comments</a></li>
    <!-- tags if -->

  
    <li><a href="/articles/2025/05/31/how-to-vim-jump-around/">How to Vim: Jump Around</a></li>
    <!-- tags if -->

  
    <li><a href="/articles/2025/06/02/how-to-vim-reloading-file-buffers/">How to Vim: Reloading File Buffers</a></li>
    <!-- tags if -->

  
    <li><a href="/articles/2025/06/03/how-to-vim-proper-ways-to-escape/">How to Vim: Proper Ways to Escape</a></li>
    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

  
    <li><a href="/articles/2025/08/27/how-to-vim-take-control-of-split-positioning/">How to Vim: Take Control of Split Positioning</a></li>
    <!-- tags if -->

  
    <li><a href="/articles/2025/08/27/how-to-vim-fixing-typos/">How to Vim: Fixing Typos</a></li>
    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

  
    <li><a href="/articles/2025/12/29/how-to-vim-joining-lines/">How to Vim: Joining Lines</a></li>
    <!-- tags if -->

  
    <li><a href="/articles/2026/01/03/how-to-vim-using-effectively-the-command-history/">How to Vim: Using Effectively the Command History</a></li>
    <!-- tags if -->

  
    <li><a href="/articles/2026/01/04/how-to-vim-insert-thing-at-point-in-command-mode/">How to Vim: Insert Thing at Point in Command Mode</a></li>
    <!-- tags if -->

  
    <li><a href="/articles/2026/01/04/how-to-vim-alternative-approach-to-find-and-replace/">How to Vim: Alternative Approach to Find and Replace</a></li>
    <!-- tags if -->

  
    <li><a href="/articles/2026/01/04/how-to-vim-navigating-prose-in-style/">How to Vim: Navigating Prose in Style</a></li>
    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

  
    <li><a href="/articles/2026/02/15/how-to-vim-many-ways-to-paste/">How to Vim: Many Ways to Paste</a></li>
    <!-- tags if -->

    <!-- tags if -->

  
    <li><a href="/articles/2026/02/22/adding-empty-lines-in-vim-redux/">Adding Empty Lines in Vim: Redux</a></li>
    <!-- tags if -->

  
    <li><a href="/articles/2026/02/22/how-to-vim-build-your-vimrc-from-scratch/">How to Vim: Build your .vimrc from Scratch</a></li>
    <!-- tags if -->

  
    <li><a href="/articles/2026/02/22/how-to-vim-to-the-terminal-and-back/">How to Vim: To the Terminal and Back</a></li>
    <!-- tags if -->

  
    <li><a href="/articles/2026/02/24/how-to-vim-auto-save-on-activity/">How to Vim: Auto-save on Activity</a></li>
    <!-- tags if -->

  
    <li><a href="/articles/2026/02/24/learning-vim-in-3-steps/">Learning Vim in 3 Steps</a></li>
    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

  
    <li><a href="/articles/2026/03/09/emacs-and-vim-in-the-age-of-ai/">Emacs and Vim in the Age of AI</a></li>
    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->

    <!-- tags if -->
 <!-- posts for -->
</ul>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:1">
      <p>I have to admit that I can tolerate vim9 script. <a href="#fnref:1" class="reversefootnote" role="doc-backlink">&#8617;&#xfe0e;</a></p>
    </li>
    <li id="fn:2">
      <p>There was a push in Emacs in recent years to modernize the out-of-the-box experience. <a href="#fnref:2" class="reversefootnote" role="doc-backlink">&#8617;&#xfe0e;</a></p>
    </li>
    <li id="fn:3">
      <p>See <code class="language-plaintext highlighter-rouge">:h packages</code> for more details. In a nutshell it’s a built-in alternative of Pathogen. <a href="#fnref:3" class="reversefootnote" role="doc-backlink">&#8617;&#xfe0e;</a></p>
    </li>
  </ol>
</div>]]></content><author><name>Bozhidar Batsov</name></author><category term="Vim" /><category term="Emacs" /><summary type="html"><![CDATA[Most 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.]]></summary></entry></feed>