Latest Posts (20 found)
(think) 2 weeks ago

Batppuccin: My Take on Catppuccin for Emacs

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… For a very long time my go-to Emacs themes were Zenburn and Solarized – 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. 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 Tokyo Night and Catppuccin , and they’ve been my daily drivers since then. Eventually, I ended up creating my own Emacs ports of both. I’ve already published emacs-tokyo-themes , and I’ll write more about that one down the road. Today is all about Catppuccin. (and by this I totally mean Batppuccin!) There’s already an official Catppuccin theme for Emacs , and it works. So why build another one? A few reasons. The official port registers a single 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 workflow – theme-switching packages like 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. Beyond the architecture, there are style guide issues. is set to the default text color, making variables invisible. All levels use the same blue, so org-mode headings are flat. forces green on all unstyled code. Several faces still ship with magenta placeholder colors. And there’s no support for popular packages like vertico, marginalia, transient, flycheck, or cider. 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. 1 Batppuccin is my opinionated take on Catppuccin for Emacs. The name is a play on my last name (Batsov) + Catppuccin. 2 I guess you can think of this as ’s Catppuccin… or perhaps Batman’s Catppuccin? The key differences from the official port: Four proper themes. , , , and are all separate themes that work with out of the box. No special reload dance needed. Faithful to the style guide. 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. Broad face coverage. 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. Clean architecture. Shared infrastructure in , thin wrapper files for each flavor, color override mechanism, configurable heading scaling. The same pattern I use in zenburn-emacs and emacs-tokyo-night-theme . I didn’t really re-invent anything here - I just created a theme in a way I’m comfortable with. I’m not going to bother with screenshots here – it looks like Catppuccin, because it is 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. The easiest way to install it right now: Replace with , , or for the other flavors. You can also switch interactively with . 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. 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. 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. 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 open an issue or send a PR. 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. That’s all from me, folks! Keep hacking! The official port uses Catppuccin’s Whiskers template tool to generate the Elisp from a template, which is cool for keeping ports in sync across editors but means the generated code doesn’t follow Emacs conventions.  ↩︎ Naming is hard, but it should also be fun! Also – I’m a huge fan of Batman.  ↩︎ The official port uses Catppuccin’s Whiskers template tool to generate the Elisp from a template, which is cool for keeping ports in sync across editors but means the generated code doesn’t follow Emacs conventions.  ↩︎ Naming is hard, but it should also be fun! Also – I’m a huge fan of Batman.  ↩︎

0 views
(think) 2 weeks ago

fsharp-ts-mode: A Modern Emacs Mode for F#

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. Meet fsharp-ts-mode . 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. The problem is that Emacs has never been particularly popular with F# programmers – or .NET programmers in general. The existing fsharp-mode 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. I’ll be honest – I spent quite a bit of time trying to come up with a clever name. 1 Some candidates that didn’t make the cut: In the end none of my fun ideas stuck, so I went with the boring-but-obvious . Sometimes the straightforward choice is the right one. At least nobody will have trouble finding it. 2 I modeled directly after , 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 ( with tree-sitter input highlighting), the same build system interaction pattern (minor mode wrapping CLI commands). This also meant I could get the basics in place really quickly. Having already solved problems like trailing comment indentation, hybrid navigation, and with qualified names in neocaml, porting those solutions to F# was mostly mechanical. The initial release covers all the essentials: If you’re currently using , switching is straightforward: The main thing doesn’t have yet is automatic LSP server installation (the package does this for ). You’ll need to install FsAutoComplete yourself: After that, is all you need. See the migration guide in the README for a detailed comparison. Working with the ionide/tree-sitter-fsharp grammar surfaced some interesting challenges compared to the OCaml grammar: 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: The parser can’t tell that is the body of or that belongs to the branch – it produces ERROR nodes everywhere, and 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. OCaml’s tree-sitter-ocaml-interface grammar inherits from the base grammar, so you can share queries freely. F#’s and grammars are independent with different node types and field names for equivalent concepts. For instance, a binding is in the grammar but in the grammar. Type names use a field in one grammar but not the other. Even some keyword tokens ( , , ) that work fine as query matches in fail at runtime in . This forced me to split font-lock rules into shared and grammar-specific sets – more code, more testing, more edge cases. F# script ( ) files without a declaration can mix bindings with bare expressions like . The grammar doesn’t expect a declaration after a bare expression at the top level, so it chains everything into nested nodes: Each subsequent ends up one level deeper, causing progressive indentation. I worked around this with a heuristic that detects declarations whose ancestor chain leads back to through these misparented nodes and forces them to column 0. Shebangs ( ) required a different trick – excluding the first line from the parser’s range entirely via . I’ve filed issues upstream for the grammar pain points – hopefully they’ll improve over time. 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 open an issue – will collect the environment details for you. The package can currently be installed only from GitHub (via or manually). I’ve filed a PR with MELPA and I hope it will get merged soon. I really need to take a break from building Tree-sitter major modes at this point. Between , , , and now , I’ve spent a lot of time staring at tree-sitter node types and indent rules. 3 It’s been fun, but I think I’ve earned a vacation from . 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! That’s all from me, folks! Keep hacking! Way more time than I needed to actually implement the mode.  ↩︎ Many people pointed out they thought was some package for neovim. Go figure why!  ↩︎ I’ve also been helping a bit with erlang-ts-mode recently.  ↩︎ fsharpe-mode (fsharp(evolved/enhanced)-mode) Fa Dièse (French for F sharp – because after spending time with OCaml you start thinking in French, apparently) fluoride (a play on Ionide , the popular F# IDE extension) Syntax highlighting via Tree-sitter with 4 customizable levels, supporting , , and files Indentation via Tree-sitter indent rules Imenu with fully-qualified names (e.g., ) Navigation – , , F# Interactive (REPL) integration with tree-sitter highlighting for input dotnet CLI integration – build, test, run, clean, format, restore, with watch mode support .NET API documentation lookup at point ( ) Eglot integration for FsAutoComplete Compilation error parsing for output Shift region left/right, auto-detect indent offset, prettify symbols, outline mode, and more Way more time than I needed to actually implement the mode.  ↩︎ Many people pointed out they thought was some package for neovim. Go figure why!  ↩︎ I’ve also been helping a bit with erlang-ts-mode recently.  ↩︎

0 views
(think) 3 weeks ago

Neocaml 0.6: Opam, Dune, and More

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 and ), 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. 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. I was wrong. Of course, OCaml files don’t exist in isolation. They live alongside Opam files that describe packages and Dune files that configure builds. And as I was poking around the Tree-sitter ecosystem, I discovered that there were already grammars for both Opam and Dune 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. So here we are with neocaml 0.6, which is quite a bit bigger than I expected originally. Note: 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 , 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. Let me walk you through what’s new. The new activates automatically for and files. It provides: The flymake backend registers automatically when is found in your PATH, but you need to enable yourself: Flycheck users get support out of the box via Flycheck’s built-in checker — no extra configuration needed. This bridges some of the gap with Tuareg , which also bundles an Opam major mode ( ). The Tree-sitter-based approach gives us more accurate highlighting, and the flymake integration is a nice bonus on top. handles , , and files — all three use the same s-expression syntax and share a single Tree-sitter grammar. You get: This removes the need to install the separate dune package (the standalone maintained by the Dune developers) from MELPA. If you prefer to keep using it, that’s fine too — neocaml’s README has instructions for overriding the entries. Beyond editing Dune files, I wanted a simple way to run Dune commands from any neocaml buffer. is a minor mode that provides keybindings (under ) and a “Dune” menu for common operations: All commands run via Emacs’s , so you get error navigation, clickable source locations, and the full interface for free. With a prefix argument ( ), build, test, and fmt run in watch mode ( ), automatically rebuilding when files change. The command is special — it launches through , so you get the full REPL integration (send region, send definition, etc.) with your project’s libraries preloaded. This mode is completely independent from — it doesn’t care which major mode you’re using. You can enable it in OCaml buffers like this: 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 flattens field-value pairs in a way that makes indentation less precise than it could be, and neither grammar supports variable interpolation ( ) yet. These are very solvable problems and I expect the grammars to improve over time. 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 ocaml-eglot . Down the road there might be support for OCamllex ( ) or Menhir ( ) 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. 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 GitHub . You can find the full list of changes in the changelog . As usual — update from MELPA , kick the tires, and let me know what you think. That’s all I have for you today! Keep hacking! Tree-sitter-based font-lock (field names, strings, operators, version constraints, filter expressions, etc.) Indentation (lists, sections, option braces) Imenu for navigating variables and sections A flymake backend that runs on the current buffer, giving you inline diagnostics for missing fields, deprecated constructs, and syntax errors Font-lock for stanza names, field names, action keywords, strings, module names, library names, operators, and brackets Indentation with 1-space offset Imenu for stanza navigation Defun navigation and support

0 views
(think) 4 weeks ago

One Year with the HHKB: A Mini Review

The keyboard is the most important tool for a programmer. Choose wisely. I’m a keyboard nerd. I’ve owned several great keyboards over the years, starting with the legendary Das Keyboard 3 Ultimate (blank keys and Cherry MX Blue switches – my co-workers loved me), then moving through the Das Keyboard 4 , the excellent KUL ES-87 , and eventually landing on what I considered my dream keyboard: the Leopold FC660C . The Leopold FC660C – my daily driver for almost a decade. The Leopold was a revelation. It’s where I discovered Topre switches – that glorious electrostatic capacitive feel that’s somewhere between membrane and mechanical, yet somehow better than both. After years of clacking away on Cherry MX Blues (much to the dismay of my wife and everyone within a 10-meter radius), the smooth, thocky Topre experience felt like coming home. The compact 65% layout was the cherry on top – small enough to save desk space, but with dedicated arrow keys and a few essential extras. I used the Leopold daily for almost a decade, from 2016 all the way to early 2025. That’s quite a run. So why change something that was working so well? Two words: wireless Topre . I wanted to cut the cord, and if you want a wireless keyboard with Topre switches your options are… well, pretty much just the HHKB (Happy Hacking Keyboard) Hybrid Type S . The HHKB Hybrid Type S – the object of today’s review. Not to mention I’d been exposed to the HHKB hype for as long as I can remember. The keyboard has an almost cult-like following among programmers, especially in the Unix and Lisp communities. I’m honestly not sure why I went for the Leopold instead of the HHKB back in 2016 – the HHKB was definitely on my radar even then – but in hindsight the Leopold served me incredibly well. When I finally pulled the trigger on the HHKB Hybrid Type S in early 2025, I had sky-high expectations. I got it in early 2025, so now I’ve had it for over a year. I deliberately avoided writing about it earlier – I think it’s important to live with a piece of hardware for a good while before passing judgment, especially when there’s an adjustment period involved. So let’s dig in. Looks. The HHKB is a handsome keyboard. The minimalist design, the clean lines, the elegant keycap legends – it’s a looker. I’d say it edges out the Leopold slightly in the aesthetics department, though the battery housing bump on the back is a bit of an eyesore. A minor quibble, though. Weight. It’s impressively light and portable. Some people complain this makes it feel “cheap” since the body is essentially all plastic, but I appreciate being able to toss it in a bag without thinking twice. 1 Keycaps and switches. The Topre experience is excellent, as expected. The keycaps are high quality PBT and the switches feel more or less identical to what I had on the Leopold. If you already know you love Topre, you’ll love the HHKB’s typing feel. The keys on the HHKB Type S are a bit quieter and lighter to press than those of the Leopold, but the difference is not big. Control key placement. This is probably the one aspect of the HHKB’s unconventional layout that I actually love. Control sits right where Caps Lock is on a standard keyboard – exactly where it belongs. On every other keyboard I’ve ever owned, the first thing I’d do is remap Caps Lock to Control anyway, so it’s nice to have a keyboard that gets this right out of the box. Wireless. Being able to pair with multiple devices via Bluetooth and switch between them is genuinely nice. No more cable clutter on the desk. That said, the wireless implementation comes with some significant caveats – more on that below. The layout. For a keyboard that markets itself as a “programmer’s keyboard,” some of the layout decisions are baffling. The tilde/backtick key is in a terrible position (top right corner, miles away from where your fingers expect it). For someone who lives in the terminal, that’s a real problem. I remapped it to the Escape key position almost immediately, since I don’t particularly care where Escape lives – I use a dual-mapping on Caps Lock (Control when held, Escape when tapped via Karabiner Elements ). The backslash placement is also awkward, and the Alt/Option keys are unnecessarily tiny even though there’s plenty of space to make them bigger. There’s no right Control key despite ample room for one (I compensate with a similar hold/tap mapping on the Return key). And the lack of dedicated arrow keys – while manageable when programming – is genuinely annoying in applications that make heavy use of them (browsers, document editors, Slack, etc.). I’ve mostly gotten used to using Fn+key combos for arrows, but I still miss the Leopold’s dedicated arrow keys on a regular basis. The firmware. For such an expensive and supposedly premium product, the firmware feels primitive. You get basic key remapping and a few DIP switches, but it’s nothing compared to the power and flexibility of QMK or VIA that you’ll find on many keyboards at half the price. HHKB recently released firmware 2.0 with some interesting updates, but I haven’t had a chance to try it yet. In the meantime, Karabiner Elements does the heavy lifting for me – but I shouldn’t have to rely on third-party software to make a $300+ keyboard work the way I want. Battery life. It’s mediocre at best, and the HHKB uses disposable AA batteries rather than a built-in rechargeable battery. In 2025. For a premium wireless keyboard. I’ll let that sink in. The sleep/wake behavior. This is my single biggest complaint and the thing that still drives me up the wall a year later. To save battery, the keyboard goes to sleep after 30 minutes of inactivity – that’s perfectly reasonable. What’s not reasonable is that pressing a key doesn’t wake it up. You have to press the power button to bring it back to life. Every. Single. Time. I still don’t understand why it can’t auto-wake like virtually every other wireless keyboard on the market. You come back from a coffee break, start typing, and… nothing. Then you remember, reach for the power button, wait a second for it to reconnect, and then you can start typing. It’s a small thing, but it’s also extremely annoying. The Das Keyboard 4 – where it all started for me (well, almost). Big, loud, and proud. So, is the HHKB the real deal, or is it mostly hype? After a year of daily use, I’d say it’s… a bit of both. It’s a good keyboard – the typing feel is fantastic (because Topre), it looks great on a desk, and the wireless capability is genuinely useful despite its rough edges. But I wouldn’t say it offers much over other Topre keyboards. The layout quirks, the primitive firmware, the battery situation, and that maddening sleep/wake behavior all hold it back from being the definitive keyboard it’s often made out to be. The fundamental problem is that there are so few Topre keyboards on the market that our options are extremely limited. For me it came down to either the HHKB or the Realforce R3 TKL . The Realforce has a better, more conventional layout for sure, but I didn’t love the aesthetics – it felt too big for what it offered, and visually it didn’t do much for me. 2 Despite its shortcomings, the HHKB has grown on me. I don’t think my typing experience has actually improved compared to the Leopold, but my desk certainly looks a bit nicer and I always smile when I look down at it. Sometimes that’s enough. I hope this won’t be the last Topre keyboard from HHKB, and that down the road they’ll release a version that addresses some of my frustrations. But I won’t be upset if I end up typing on my current HHKB for a very long time. If you have an HHKB or any other Topre keyboard, I’d love to hear about your experience in the comments. What do you love? What drives you crazy? Have you found clever workarounds for the layout quirks? And if you’re still on Cherry MX Blues… well, your co-workers would like a word with you. That’s all I have for you today. Keep typing! Not that I carry it around much.  ↩︎ If they tweak it a bit in the future I’ll definitely get one, though.  ↩︎ Not that I carry it around much.  ↩︎ If they tweak it a bit in the future I’ll definitely get one, though.  ↩︎

0 views
(think) 1 months ago

Emacs and Vim in the Age of AI

It’s tough to make predictions, especially about the future. – Yogi Berra I’ve been an Emacs fanatic for over 20 years. I’ve built and maintained some of the most popular Emacs packages, contributed to Emacs itself, and spent countless hours tweaking my configuration. Emacs isn’t just my editor – it’s my passion, and my happy place. Over the past year I’ve also been spending a lot of time with Vim and Neovim, relearning them from scratch and having a blast contrasting how the two communities approach similar problems. It’s been a fun and refreshing experience. 1 And lately, like everyone else in our industry, I’ve been playing with AI tools – Claude Code in particular – watching the impact of AI on the broader programming landscape, and pondering what it all means for the future of programming. Naturally, I keep coming back to the same question: what happens to my beloved Emacs and its “arch nemesis” Vim in this brave new world? I think the answer is more nuanced than either “they’re doomed” or “nothing changes”. Predicting the future is obviously hard work, but it’s so fun to speculate on it. My reasoning is that every major industry shift presents plenty of risks and opportunities for those involved in it, so I want to spend a bit of time ruminating over the risks and opportunities for Emacs and Vim. VS Code is already the dominant editor by a wide margin, and it’s going to get first-class integrations with every major AI tool – Copilot (obviously), Codex, Claude, Gemini, you name it. Microsoft has every incentive to make VS Code the best possible host for AI-assisted development, and the resources to do it. On top of that, purpose-built AI editors like Cursor , Windsurf , and others are attracting serious investment and talent. These aren’t adding AI to an existing editor as an afterthought – they’re building the entire experience around AI workflows. They offer integrated context management, inline diffs, multi-file editing, and agent loops that feel native rather than bolted on. Every developer who switches to one of these tools is a developer who isn’t learning Emacs or Vim keybindings, isn’t writing Elisp, and isn’t contributing to our ecosystems. The gravity well is real. I never tried Cursor and Windsurf simply because they are essentially forks of VS Code and I can’t stand VS Code. I tried it several times over the years and I never felt productive in it for a variety of reasons. Part of the case for Emacs and Vim has always been that they make you faster at writing and editing code. The keybindings, the macros, the extensibility – all of it is in service of making the human more efficient at the mechanical act of coding. But if AI is writing most of your code, how much does mechanical editing speed matter? When you’re reviewing and steering AI-generated diffs rather than typing code character by character, the bottleneck shifts from “how fast can I edit” to “how well can I specify intent and evaluate output.” That’s a fundamentally different skill, and it’s not clear that Emacs or Vim have an inherent advantage there. The learning curve argument gets harder to justify too. “Spend six months learning Emacs and you’ll be 10x faster” is a tough sell when a junior developer with Cursor can scaffold an entire application in an afternoon. 2 VS Code has Microsoft. Cursor has venture capital. Emacs has… a small group of volunteers and the FSF. Vim had Bram, and now has a community of maintainers. Neovim has a small but dedicated core team. This has always been the case, of course, but AI amplifies the gap. Building deep AI integrations requires keeping up with fast-moving APIs, models, and paradigms. Well-funded teams can dedicate engineers to this full-time. Volunteer-driven projects move at the pace of people’s spare time and enthusiasm. Let’s go all the way: what if programming as we know it is fully automated within the next decade? If AI agents can take a specification and produce working, tested, deployed software without human intervention, we won’t need coding editors at all. Not Emacs, not Vim, not VS Code, not Cursor. The entire category becomes irrelevant. I don’t think this is likely in the near term, but it’s worth acknowledging as a possibility. The trajectory of AI capabilities has surprised even the optimists (and I was initially an AI skeptic, but the rapid advancements last year eventually changed my mind). Here’s the thing almost nobody is talking about: Emacs and Vim have always suffered from the obscurity of their extension languages. Emacs Lisp is a 1980s Lisp dialect that most programmers have never seen before. VimScript is… VimScript. Even Lua, which Neovim adopted specifically because it’s more approachable, is niche enough that most developers haven’t written a line of it. This has been the single biggest bottleneck for both ecosystems. Not the editors themselves – they’re incredibly powerful – but the fact that customizing them requires learning an unfamiliar language, and most people never make it past copying snippets from blog posts and READMEs. I felt incredibly overwhelmed by Elisp and VimScript when I was learning Emacs and Vim for the first time, and I imagine I wasn’t the only one. I started to feel very productive in Emacs only after putting in quite a lot of time to actually learn Elisp properly. (never bothered to do the same for VimScript, though, and admittedly I’m not too eager to master Lua either) AI changes this overnight. You can now describe what you want in plain English and get working Elisp, VimScript, or Lua. “Write me an Emacs function that reformats the current paragraph to 72 columns and adds a prefix” – done. “Configure lazy.nvim to set up LSP with these keybindings” – done. The extension language barrier, which has been the biggest obstacle to adoption for decades, is suddenly much lower. After 20+ years in the Emacs community, I often have the feeling that a relatively small group – maybe 50 to 100 people – is driving most of the meaningful progress. The same names show up in MELPA, on the mailing lists, and in bug reports. This isn’t a criticism of those people (I’m proud to be among them), but it’s a structural weakness. A community that depends on so few contributors is fragile. And it’s not just Elisp and VimScript. The C internals of both Emacs and Vim (and Neovim’s C core) are maintained by an even smaller group. Finding people who are both willing and able to hack on decades-old C codebases is genuinely hard, and it’s only getting harder as fewer developers learn C at all. AI tools can help here in two ways. First, they lower the barrier for new contributors – someone who understands the concept of what they want to build can now get AI assistance with the implementation in an unfamiliar language. Second, they help existing maintainers move faster. I’ve personally found that AI is excellent at generating test scaffolding, writing documentation, and handling the tedious parts of package maintenance that slow everything down. The Emacs and Neovim communities aren’t sitting idle. There are already impressive AI integrations: 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. Here’s an irony that deserves more attention: many of the most powerful AI coding tools are terminal-native. Claude Code, Aider, and various Copilot CLI tools all run in the terminal. And what lives in the terminal? Emacs and Vim. 3 Running Claude Code in an Emacs buffer or a Neovim terminal split is a perfectly natural workflow. You get the AI agent in one pane and your editor in another, with all your keybindings and tools intact. There’s no context switching to a different application – it’s all in the same environment. This is actually an advantage over GUI-based AI editors, where the AI integration is tightly coupled to the editor’s own interface. With terminal-native tools, you get to choose your own editor and your own AI tool, and they compose naturally. Emacs’s “editor as operating system” philosophy is uniquely well-suited to AI integration. It’s not just a code editor – it’s a mail client (Gnus, mu4e), a note-taking system (Org mode), a Git interface (Magit), a terminal emulator, a file manager, an RSS reader, and much more. AI can be integrated at every one of these layers. Imagine an AI assistant that can read your org-mode agenda, draft email replies in mu4e, help you write commit messages in Magit, and refactor code in your source buffers – all within the same environment, sharing context. No other editor architecture makes this kind of deep, cross-domain integration as natural as Emacs does. Admittedly, I’ve 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 ) Still, I’m only one Emacs user and many are probably using it in a more holistic manner. One of the most underappreciated benefits of AI for Emacs and Vim users is mundane: troubleshooting. Both editors have notoriously steep learning curves and opaque error messages. “Wrong type argument: stringp, nil” has driven more people away from Emacs than any competitor ever did. AI tools are remarkably good at explaining cryptic error messages, diagnosing configuration issues, and suggesting fixes. They can read your init file and spot the problem. They can explain what a piece of Elisp does. They can help you understand why your keybinding isn’t working. This dramatically flattens the learning curve – not by making the editor simpler, but by giving every user access to a patient, knowledgeable guide. I don’t really need any AI assistance to troubleshoot anything in my Emacs setup, but it’s been handy occasionally in Neovim-land, where my knowledge is relatively modest by comparison. There’s at least one documented case of someone returning to Emacs after years away , specifically because Claude Code made it painless to fix configuration issues. They’d left for IntelliJ because the configuration burden got too annoying – and came back once AI removed that barrier. “Happy f*cking days I’m home again,” as they put it. If AI can bring back lapsed Emacs users, that’s a good thing in my book. Let’s revisit the doomsday scenario. Say programming is fully automated and nobody writes code anymore. Does Emacs die? Not necessarily. Emacs is already used for far more than programming. People use Org mode to manage their entire lives – tasks, notes, calendars, journals, time tracking, even academic papers. Emacs is a capable writing environment for prose, with excellent support for LaTeX, Markdown, AsciiDoc, and plain text. You can read email, browse the web, manage files, and yes, play Tetris. Vim, similarly, is a text editing paradigm as much as a program. Vim keybindings have colonized every text input in the computing world – VS Code, IntelliJ, browsers, shells, even Emacs (via Evil mode). Even if the Vim program fades, the Vim idea is immortal. 4 And who knows – maybe there’ll be a market for artisanal, hand-crafted software one day. “Locally sourced, free-range code, written by a human in Emacs.” I’d buy that t-shirt. And I’m fairly certain those artisan programmers won’t be using VS Code. So even in the most extreme scenario, both editors have a life beyond code. A diminished one, perhaps, but a life nonetheless. I think what’s actually happening is more interesting than “editors die” or “editors are fine.” The role of the editor is shifting. For decades, the editor was where you wrote code. Increasingly, it’s becoming where you review, steer, and refine code that AI writes. The skills that matter are shifting from typing speed and editing gymnastics to specification clarity, code reading, and architectural judgment. In this world, the editor that wins isn’t the one with the best code completion – it’s the one that gives you the most control over your workflow. And that has always been Emacs and Vim’s core value proposition. The question is whether the communities can adapt fast enough. The tools are there. The architecture is there. The philosophy is right. What’s needed is people – more contributors, more plugin authors, more documentation writers, more voices in the conversation. AI can help bridge the gap, but it can’t replace genuine community engagement. Not everyone in the Emacs and Vim communities is enthusiastic about AI, and the objections go beyond mere technophobia. There are legitimate ethical concerns that are going to be debated for a long time: Energy consumption. Training and running large language models requires enormous amounts of compute and electricity. For communities that have long valued efficiency and minimalism – Emacs users who pride themselves on running a 40-year-old editor, Vim users who boast about their sub-second startup times – the environmental cost of AI is hard to ignore. Copyright and training data. LLMs are trained on vast corpora of code and text, and the legality and ethics of that training remain contested. Some developers are uncomfortable using tools that may have learned from copyrighted code without explicit consent. This concern hits close to home for open-source communities that care deeply about licensing. Job displacement. If AI makes developers significantly more productive, fewer developers might be needed. This is an uncomfortable thought for any programming community, and it’s especially pointed for editors whose identity is built around empowering human programmers. These concerns are already producing concrete action. The Vim community recently saw the creation of EVi , a fork of Vim whose entire raison d’etre is to provide a text editor free from AI integration. Whether you agree with the premise or not, the fact that people are forking established editors over this tells you how strongly some community members feel. I don’t think these concerns should stop anyone from exploring AI tools, but they’re real and worth taking seriously. I expect to see plenty of spirited debate about this on emacs-devel and the Neovim issue tracker in the years ahead. The future ain’t what it used to be. – Yogi Berra I won’t pretend I’m not worried. The AI wave is moving fast, the incumbents have massive advantages in funding and mindshare, and the very nature of programming is shifting under our feet. It’s entirely possible that Emacs and Vim will gradually fade into niche obscurity, used only by a handful of diehards who refuse to move on. But I’ve been hearing that Emacs is dying for 20 years, and it’s still here. The community is small but passionate, the editor is more capable than ever, and the architecture is genuinely well-suited to the AI era. Vim’s situation is similar – the core idea is so powerful that it keeps finding new expression (Neovim being the latest and most vigorous incarnation). The editors that survive won’t be the ones with the flashiest AI features. They’ll be the ones whose users care enough to keep building, adapting, and sharing. That’s always been the real engine of open-source software, and no amount of AI changes that. So if you’re an Emacs or Vim user: don’t panic, but don’t be complacent either. Learn the new AI tools (if you’re not fundamentally opposed to them, that is). Pimp your setup and make it awesome. Write about your workflows. Help newcomers. The best way to ensure your editor survives the AI age is to make it thrive in it. Maybe the future ain’t what it used to be – but that’s not necessarily a bad thing. That’s all I have for you today. Keep hacking! If you’re curious about my Vim adventures, I wrote about them in Learning Vim in 3 Steps .  ↩︎ 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.  ↩︎ At least some of the time. Admittedly I usually use Emacs in GUI mode, but I always use (Neo)vim in the terminal.  ↩︎ Even Claude Code has vim mode.  ↩︎ gptel – a versatile LLM client that supports multiple backends (Claude, GPT, Gemini, local models) ellama – an Emacs interface for interacting with LLMs via llama.cpp and Ollama aider.el – Emacs integration for Aider , the popular AI pair programming tool copilot.el – GitHub Copilot integration (I happen to be the current maintainer of the project) elysium – an AI-powered coding assistant with inline diff application agent-shell – a native Emacs buffer for interacting with LLM agents (Claude Code, Gemini CLI, etc.) via the Agent Client Protocol avante.nvim – a Cursor-like AI coding experience inside Neovim codecompanion.nvim – a Copilot Chat replacement supporting multiple LLM providers copilot.lua – native Copilot integration for Neovim gp.nvim – ChatGPT-like sessions in Neovim with support for multiple providers Energy consumption. Training and running large language models requires enormous amounts of compute and electricity. For communities that have long valued efficiency and minimalism – Emacs users who pride themselves on running a 40-year-old editor, Vim users who boast about their sub-second startup times – the environmental cost of AI is hard to ignore. Copyright and training data. LLMs are trained on vast corpora of code and text, and the legality and ethics of that training remain contested. Some developers are uncomfortable using tools that may have learned from copyrighted code without explicit consent. This concern hits close to home for open-source communities that care deeply about licensing. Job displacement. If AI makes developers significantly more productive, fewer developers might be needed. This is an uncomfortable thought for any programming community, and it’s especially pointed for editors whose identity is built around empowering human programmers. If you’re curious about my Vim adventures, I wrote about them in Learning Vim in 3 Steps .  ↩︎ 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.  ↩︎ At least some of the time. Admittedly I usually use Emacs in GUI mode, but I always use (Neo)vim in the terminal.  ↩︎ Even Claude Code has vim mode.  ↩︎

0 views
(think) 1 months ago

Learning OCaml: String Interpolation

Most programming languages I’ve used have some form of string interpolation. Ruby has , Python has f-strings, JavaScript has template literals, even Haskell has a few popular interpolation libraries. It’s one of those small conveniences you don’t think about until it’s gone. OCaml doesn’t have built-in string interpolation. And here’s the funny thing – I didn’t even notice when I was first learning the language. Looking back at my first impressions article, I complained about the comment syntax, the semicolons in lists, the lack of list comprehensions, and a dozen other things – but never once about string interpolation. I was happily concatenating strings with and using without giving it a second thought. I only started thinking about this while working on my PPX article and going through the catalog of popular PPX libraries. That’s when I stumbled upon and thought “wait, why doesn’t OCaml have interpolation?” The short answer: OCaml has no way to generically convert a value to a string. There’s no universal method, no typeclass, no runtime reflection that would let the language figure out how to stringify an arbitrary expression inside a string literal. In Ruby, every object responds to . In Python, everything has . These languages can interpolate anything because there’s always a fallback conversion available at runtime. OCaml’s type information is erased at compile time, so the compiler would need to know at compile time which conversion function to call for each interpolated expression – and the language has no mechanism for that. 1 OCaml does have , which is actually quite nice and type-safe: The format string is statically checked by the compiler – if you pass an where expects a string, you get a compile-time error, not a runtime crash. That’s genuinely better than what most dynamically typed languages offer. But it’s not interpolation – the values aren’t inline in the string, and for complex expressions it gets unwieldy fast. There’s also plain string concatenation with : This works, but it’s ugly and error-prone for anything beyond trivial cases. ppx_string is a Jane Street PPX that adds string interpolation to OCaml at compile time. The basic usage is straightforward: For non-string types, you specify the module whose function should be used: The suffix tells the PPX to call on , and calls on . Note that , , etc. are conventions from Jane Street’s / libraries – OCaml’s uses , and so on, which won’t work with the syntax. This is another reason really only makes sense within the Jane Street ecosystem. Any module that exposes a function works here – including your own: You can also use arbitrary expressions inside the interpolation braces: Though at that point you might be better off with a binding or for readability. A few practical things worth knowing: Honestly? Probably not as much as you think. I’ve been writing OCaml for a while now without it, and it rarely bothers me. Here’s why: That said, when you do need to build a lot of human-readable strings – error messages, log output, CLI formatting – interpolation is genuinely nicer than . If you’re in the Jane Street ecosystem, there’s no reason not to use . The lack of string interpolation in OCaml is one of those things that sounds worse than it actually is. In practice, and cover the vast majority of use cases, and the code you write with them is arguably clearer about types than magical interpolation would be. It’s also a nice example of OCaml’s general philosophy: keep the language core small, provide solid primitives ( , ), and let the PPX ecosystem fill in the syntactic sugar for those who want it. The same pattern plays out with for printing, for monadic syntax, and many other conveniences. Will OCaml ever get built-in string interpolation? Maybe. There have been discussions on the forums over the years, and the language did absorb binding operators ( , ) from the PPX world. But I wouldn’t hold my breath – and honestly, I’m not sure I’d even notice if it landed. That’s all I have for you today. Keep hacking! This is the same fundamental problem that makes printing data structures harder than in dynamically typed languages.  ↩︎ You need the stanza in your dune file: String values interpolate directly, everything else needs a conversion suffix. Unlike Ruby where is called implicitly, requires you to be explicit about non-string types. This is annoying at first, but it’s consistent with OCaml’s philosophy of being explicit about types. It’s a Jane Street library. If you’re already in the Jane Street ecosystem ( , , etc.), adding is trivial. If you’re not, pulling in a Jane Street dependency just for string interpolation might feel heavy. In that case, is honestly fine. It doesn’t work with the module. If you’re building strings for pretty-printing, you’ll still want or . is for building plain strings, not format strings. Nested interpolation doesn’t work – you can’t nest inside another . Keep it simple. is good. It’s type-safe, it’s concise enough for most cases, and it’s available everywhere without extra dependencies. Most string building in OCaml happens through . If you’re writing pretty-printers (which you will be, thanks to ), you’re using , not string concatenation or interpolation. OCaml code tends to be more compute-heavy than string-heavy. Compared to, say, a Rails app or a shell script, the typical OCaml program just doesn’t build that many ad-hoc strings. This is the same fundamental problem that makes printing data structures harder than in dynamically typed languages.  ↩︎

0 views
(think) 1 months ago

Learning OCaml: PPX for Mere Mortals

When I started learning OCaml I kept running into code like this: My first reaction was “what the hell is ?” Coming from languages like Ruby and Clojure, where metaprogramming is either built into the runtime (reflection) or baked into the language itself (macros), OCaml’s approach felt alien. There’s no runtime reflection, no macro system in the Lisp sense – just this mysterious syntax that somehow generates code at compile time. That mystery is PPX (PreProcessor eXtensions), and once you understand it, a huge chunk of the OCaml ecosystem suddenly makes a lot more sense. This article is my attempt to demystify PPX for people like me – developers who want to use PPX effectively without necessarily becoming PPX authors themselves. OCaml is a statically typed language with no runtime reflection. That means you can’t do things like “iterate over all fields of a record at runtime” or “automatically serialize any type to JSON.” The type information simply isn’t available at runtime – it’s erased during compilation. One of my biggest frustrations as a newcomer was not being able to just print arbitrary data for debugging – there’s no generic or that works on any type. That frustration was probably my first real interaction with PPX. PPX solves this by generating code at compile time . When the OCaml compiler parses your source code, it builds an Abstract Syntax Tree (AST) – a tree data structure that represents the syntactic structure of your program. PPX rewriters are programs that receive this AST, transform it, and return a modified AST back to the compiler. The compiler then continues as if you had written the generated code by hand. In practical terms, this means that when you write: The PPX rewriter generates something like this behind the scenes: You get a pretty-printer for free, derived from the type definition. No boilerplate, no manual work, and it stays in sync with your type automatically. If you’ve used Rust’s or Haskell’s , the idea is very similar. The syntax is different, but the motivation is identical – generating repetitive code from type definitions. If you’re coming from Rust, you might wonder why OCaml doesn’t just have a built-in macro system like . It’s a fair question, and the answer says a lot about OCaml’s design philosophy. OCaml has always favored a small, stable language core . The compiler is famously lean and fast, and the language team is conservative about adding complexity to the specification. A full macro system baked into the compiler would be a significant undertaking – it would need to be designed, specified, maintained, and kept compatible across versions, forever. Instead, OCaml took a more minimal approach: the compiler provides just two things – extension points and attributes – as syntactic hooks in the AST. Everything else lives in the ecosystem. The actual PPX rewriters are ordinary OCaml programs that happen to transform ASTs. The ppxlib framework that ties it all together is a regular library, not part of the compiler. This has some real advantages: The trade-offs are real, though. Rust’s proc macros are more tightly integrated – you get better error messages pointing at macro-generated code, better IDE support for macro expansions, and the macro system is a documented, stable part of the language. With PPX, you’re sometimes left staring at cryptic type errors in generated code and reaching for to figure out what went wrong. That said, OCaml’s approach feels very OCaml – pragmatic, minimal, and trusting the ecosystem to build what’s needed on top of a simple foundation. And in practice, it works remarkably well. PPX wasn’t OCaml’s first metaprogramming system. Before PPX, there was Camlp4 (and its fork Camlp5 ) – a powerful but complex preprocessor that maintained its own parser, separate from the compiler’s parser. Camlp4 could extend OCaml’s syntax in arbitrary ways, which sounds great in theory but was a maintenance nightmare in practice. Every OCaml release risked breaking Camlp4, and code using Camlp4 extensions often couldn’t be processed by standard tools like editors and documentation generators. OCaml 4.02 (2014) introduced extension points and attributes directly into the language grammar – syntactic hooks specifically designed for preprocessor extensions. This was a much simpler and more maintainable approach: PPX rewriters use the compiler’s own AST, the syntax is valid OCaml (so tools can still parse your code), and the whole thing is conceptually just “AST in, AST out.” Camlp4 was officially retired in 2019. Today, the PPX ecosystem is built on ppxlib , a unified framework that provides a stable API across OCaml versions and handles all the plumbing for PPX authors. Before diving into specific libraries, let’s decode the bracket soup. PPX uses two syntactic mechanisms built into OCaml: Extension nodes are placeholders that a PPX rewriter must replace with generated code (compilation fails if no PPX handles them): Attributes attach metadata to existing code. Unlike extension nodes, the compiler silently ignores attributes that no PPX handles: The one you’ll see most often is on type declarations. The distinction between , , and is about scope – one for the innermost node, two for the enclosing declaration, three for the whole module-level. Tip: Don’t worry about memorizing all of this upfront. In practice, you’ll mostly use and occasionally or – and the specific PPX library’s documentation will tell you exactly which syntax to use. To use a PPX library in your project, you add it to the stanza in your file: That’s it. List all the PPX rewriters you need after , and Dune takes care of the rest (it even combines them into a single binary for performance). For plugins specifically, you use dotted names like . Let’s look at the PPX libraries that cover probably 90% of real-world use cases. ppx_deriving is the community’s general-purpose deriving framework. It comes with several built-in plugins: is the one you’ll reach for first – it’s essentially the answer to “how do I just print this thing?” that every OCaml newcomer asks sooner or later. The most commonly used plugins: A neat convention: if your type is named (as is idiomatic in OCaml), the generated functions drop the type name suffix – you get , , , instead of , , etc. You can also customize behavior per field with attributes: And you can derive for anonymous types inline: ppx_deriving_yojson generates JSON serialization and deserialization functions using the Yojson library: You can use or if you only need one direction. This is incredibly useful in practice – writing JSON serializers by hand for complex types is tedious and error-prone. If you’re using Jane Street’s Core library, you’ll encounter S-expression serialization everywhere. ( Tip: Jane Street bundles most of their PPXs into a single ppx_jane package, so you can add just to your instead of listing each one individually.) ppx_sexp_conv generates converters between OCaml types and S-expressions: The attributes here are quite handy – provides a default value during deserialization, and means the field is represented as a present/absent atom rather than . Two more Jane Street PPXs that you’ll see a lot in Core-based codebases. ppx_fields_conv generates first-class accessors and iterators for record fields: ppx_variants_conv does something similar for variant types – generating constructors as functions, fold/iter over all variants, and more. These Jane Street PPXs let you write tests directly in your source files: ppx_expect is particularly nice – it captures printed output and compares it against expected output: If the output doesn’t match, the test fails and you can run to automatically update the expected output in your source file. It’s a very productive workflow for testing functions that produce output. ppx_let provides syntactic sugar for working with monads and other “container” types: How does know which to call? It looks for a module in scope that provides the underlying and functions. In practice, you’ll typically open a module that defines before using : Note: Since OCaml 4.08, the language has built-in binding operators ( , , , ) that cover the basic use cases of without needing a preprocessor. If you’re not using Jane Street’s ecosystem, binding operators are probably the simpler choice. still offers extra features like , , and optimized though. ppx_blob is beautifully simple – it embeds a file’s contents as a string at compile time: No more worrying about file paths at runtime or packaging data files with your binary. The file contents become part of your compiled program. One thing that’s always bugged me about OCaml is the lack of string interpolation. ppx_string fills that gap: The suffix tells the PPX to convert the value using . You can use any module that provides a function. Most OCaml developers will never need to write a PPX, but understanding the basics helps demystify the whole system. Let’s build a very simple one. Say we want an extension that converts a string literal to uppercase at compile time. Here’s the complete implementation using ppxlib : The dune file: The key pieces are: For more complex PPXs (especially derivers), you’ll also want to use Metaquot ( ), which lets you write AST-constructing code using actual OCaml syntax instead of manual AST builder calls: The ppxlib documentation has excellent tutorials if you want to go deeper. One practical tip: when something goes wrong with PPX-generated code and you’re staring at a confusing type error, you can inspect what the PPX actually generated: Seeing the expanded code often makes the error immediately obvious. Most of the introductory PPX content out there was written around 2018-2019, so it’s worth noting how things have evolved since then. The big story has been ppxlib’s consolidation of the ecosystem . Back in 2019, some PPX rewriters still used the older (OMP) library, creating fragmentation. By 2021, nearly all PPXs had migrated to ppxlib , effectively ending the split. Today ppxlib is the way to write PPX rewriters – there’s no real alternative to consider. The transition hasn’t always been smooth, though. In 2025, ppxlib 0.36.0 bumped its internal AST to match OCaml 5.2, which changed how functions are represented in the parse tree. This broke many downstream PPXs and temporarily split the opam universe between packages that worked with the new version and those that didn’t. The community worked through it with proactive patching, but it highlighted an ongoing tension in the PPX world: ppxlib shields you from most compiler changes, but major AST overhauls still ripple through the ecosystem. On the API side, ppxlib is gradually deprecating its copy of in favor of , with plans to remove entirely in a future 1.0.0 release. If you’re writing a new PPX today, use exclusively. Meanwhile, OCaml 4.08’s built-in binding operators ( , , etc.) have reduced the need for in projects that don’t use Jane Street’s ecosystem. It’s a nice example of the language absorbing a pattern that PPX pioneered. Perhaps one day we’ll see more of this (e.g. native string interpolation). This article covers a lot of ground, but the PPX topic is pretty deep and complex, so depending on how far you want to go you might want to read more on it. Here are some of the best resources I’ve found on PPX: I was amused to see whitequark’s name pop up while I was doing research for this article – we collaborated quite a bit back in the day on her Ruby parser project, which was instrumental to RuboCop . Seems you can find (former) Rubyists in pretty much every language community. This article turned out to be a beast! I’ve wanted to write something on the subject for quite a while now, but I’ve kept postponing it because I was too lazy to do all the necessary research. I’ll feel quite relieved to put it behind me! PPX might look intimidating at first – all those brackets and symbols can feel like line noise. But the core idea is simple: PPX generates boilerplate code from your type definitions at compile time. You annotate your types with what you want ( , , , , etc.), and the PPX rewriter produces the code you’d otherwise have to write by hand. For day-to-day OCaml programming, you really only need to know: The “writing your own PPX” part is there for when you need it, but honestly most OCaml developers get by just fine using the existing ecosystem. That’s all I have for you today. Keep hacking! The ecosystem can evolve independently. ppxlib can ship new features, fix bugs, and improve APIs without waiting for a compiler release. Compare this to Rust, where changes to the proc macro system require the full RFC process and a compiler update. Tooling stays simple. Because and are valid OCaml syntax, every tool – editors, formatters, documentation generators – can parse PPX-annotated code without knowing anything about the specific PPX. The code is always syntactically valid OCaml, even before preprocessing. The compiler stays lean. No macro expander, no hygiene system, no special compilation phases – just a hook that says “here, transform this AST before I type-check it.” – registers an extension with a name, the context where it can appear (expressions, patterns, types, etc.), the expected payload pattern, and an expansion function. – a pattern-matching DSL for destructuring AST nodes. Here matches a string literal and captures its value. – helpers for constructing AST nodes. builds a string literal expression. – registers the rule with ppxlib’s driver. Preprocessors and PPXs – the official OCaml documentation on metaprogramming. A solid reference, though it assumes some comfort with the compiler internals. An Introduction to OCaml PPX Ecosystem – Nathan Rebours’ 2019 deep dive for Tarides. This is the most thorough tutorial on writing PPX rewriters I’ve seen. Some API details have changed since 2019 (notably the → shift), but the concepts and approach are still excellent. ppxlib Quick Introduction – ppxlib’s own getting-started guide. The best place to begin if you want to write your own PPX. A Guide to PreProcessor eXtensions – OCamlverse’s reference page with a comprehensive list of available PPX libraries. A Guide to Extension Points in OCaml – Whitequark’s original 2014 guide that introduced many developers to PPX. Historically interesting as a snapshot of the early PPX days. on type declarations to generate useful functions How to add PPX libraries to your dune file with Which PPX libraries exist for common tasks (serialization, testing, pretty-printing)

0 views
(think) 1 months ago

Building Emacs Major Modes with TreeSitter: Lessons Learned

Over the past year I’ve been spending a lot of time building TreeSitter-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 battle scars to write about the experience. This post distills the key lessons for anyone thinking about writing a TreeSitter-based major mode, or curious about what it’s actually like. Before TreeSitter, Emacs font-locking was done with regular expressions and indentation was handled by ad-hoc engines (SMIE, custom indent functions, or pure regex heuristics). This works, but it has well-known problems: Regex-based font-locking is fragile. Regexes can’t parse nested structures, so they either under-match (missing valid code) or over-match (highlighting inside strings and comments). Every edge case is another regex, and the patterns become increasingly unreadable over time. Indentation engines are complex. SMIE (the generic indentation engine for non-TreeSitter 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. TreeSitter changes the game because you get a full, incremental, error-tolerant syntax tree for free. Font-locking becomes “match this AST pattern, apply this face”: And indentation becomes “if the parent node is X, indent by Y”: The rules are declarative, composable, and much easier to reason about than regex chains. In practice, ’s entire font-lock and indentation logic fits in about 350 lines of Elisp. The equivalent in tuareg is spread across thousands of lines. That’s the real selling point: simpler, more maintainable code that handles more edge cases correctly . That said, TreeSitter in Emacs is not a silver bullet. Here’s what I ran into. TreeSitter grammars are written by different authors with different philosophies. The tree-sitter-ocaml grammar provides a rich, detailed AST with named fields. The tree-sitter-clojure grammar, by contrast, deliberately keeps things minimal – it only models syntax, not semantics, because Clojure’s macro system makes static semantic analysis unreliable. 1 This means font-locking forms in Clojure requires predicate matching on symbol text, while in OCaml you can directly match nodes with named fields. To illustrate: here’s how you’d fontify a function definition in OCaml, where the grammar gives you rich named fields: And here’s the equivalent in Clojure, where the grammar only gives you lists of symbols and you need predicate matching: You can’t learn “how to write TreeSitter queries” generically – you need to learn each grammar individually. The best tool for this is (to visualize the full parse tree) and (to see the node at point). Use them constantly. You’re dependent on someone else providing the grammar, and quality is all over the map. The OCaml grammar is mature and well-maintained – it’s hosted under the official tree-sitter GitHub org. The Clojure grammar is small and stable by design. But not every language is so lucky. asciidoc-mode uses a third-party AsciiDoc grammar that employs a dual-parser architecture – one parser for block-level structure (headings, lists, code blocks) and another for inline formatting (bold, italic, links). This is the same approach used by Emacs’s built-in , and it makes sense for markup languages where block and inline syntax are largely independent. The problem is that the two parsers run independently on the same text, and they can disagree . The inline parser misinterprets and list markers as emphasis delimiters, creating spurious bold spans that swallow subsequent inline content. The workaround is to use on all block-level font-lock rules so they win over the incorrect inline faces: This doesn’t fix inline elements consumed by the spurious emphasis – that requires an upstream grammar fix. When you hit grammar-level issues like this, you either fix them yourself (which means diving into the grammar’s JavaScript source and C toolchain) or you live with workarounds. Either way, it’s a reminder that your mode is only as good as the grammar underneath it. Getting the font-locking right in was probably the most challenging part of all three projects, precisely because of these grammar quirks. I also ran into a subtle behavior: the default font-lock mode ( ) skips an entire captured range if any position within it already has a face. So if you capture a parent node like and a child was already fontified, the whole thing gets skipped silently. The fix is to capture specific child nodes instead: These issues took a lot of trial and error to diagnose. The lesson: budget extra time for font-locking when working with less mature grammars . Grammars evolve, and breaking changes happen. switched from the stable grammar to the experimental branch because the stable version had metadata nodes as children of other nodes, which caused and to behave incorrectly. The experimental grammar makes metadata standalone nodes, fixing the navigation issues but requiring all queries to be updated. pins to v0.24.0 of the OCaml grammar. If you don’t pin versions, a grammar update can silently break your font-locking or indentation. The takeaway: always pin your grammar version , and include a mechanism to detect outdated grammars. tests a query that changed between versions to detect incompatible grammars at startup. Users shouldn’t have to manually clone repos and compile C code to use your mode. Both and include grammar recipes: On first use, the mode checks and offers to install missing grammars via . This works, but requires a C compiler and Git on the user’s machine, which is not ideal. 2 The TreeSitter support in Emacs has been improving steadily, but each version has its quirks: Emacs 29 introduced TreeSitter support but lacked several APIs. For instance, (used for structured navigation) doesn’t exist – you need a fallback: Emacs 30 added , sentence navigation, and better indentation support. But it also had a bug in offsets ( #77848 ) that broke embedded parsers, and another in that required to disable its TreeSitter-aware version. Emacs 31 has a bug in where an off-by-one error causes to leave ` *)` behind on multi-line OCaml comments. I had to skip the affected test with a version check: The lesson: test your mode against multiple Emacs versions , and be prepared to write version-specific workarounds. CI that runs against Emacs 29, 30, and snapshot is essential. Most TreeSitter grammars ship with query files for syntax highlighting ( ) and indentation ( ). Editors like Neovim and Helix use these directly. Emacs doesn’t – you have to manually translate the patterns into and calls in Elisp. This is tedious and error-prone. For example, here’s a rule from the OCaml grammar’s : And here’s the Elisp equivalent you’d write for Emacs: The query syntax is nearly identical, but you have to wrap everything in calls, map upstream capture names ( ) to Emacs face names ( ), assign features, and manage behavior. You end up maintaining a parallel set of queries that can drift from upstream. Emacs 31 will introduce which will make it possible to use files for font-locking, which should help significantly. But for now, you’re hand-coding everything. When a face isn’t being applied where you expect: TreeSitter modes define four levels of font-locking via , and the default level in Emacs is 3. It’s tempting to pile everything into levels 1–3 so users see maximum highlighting out of the box, but resist the urge. When every token on the screen has a different color, code starts looking like a Christmas tree and the important things – keywords, definitions, types – stop standing out. Less is more here. Here’s how distributes features across levels: And follows the same philosophy: 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 to 4. Better yet, they can use to cherry-pick individual features regardless of level: This gives users fine-grained control without requiring mode authors to anticipate every preference. Indentation issues are harder to diagnose because they depend on tree structure, rule ordering, and anchor resolution: Remember that rule order matters for indentation too – the first matching rule wins. A typical set of rules reads top to bottom from most specific to most general: Watch out for the empty-line problem : when the cursor is on a blank line, TreeSitter has no node at point. The indentation engine falls back to the root node as the parent, which typically matches the top-level rule and gives column 0. In neocaml I solved this with a rule that looks at the previous line’s last token to decide indentation: This is the single most important piece of advice. Font-lock and indentation are easy to break accidentally, and manual testing doesn’t scale. Both projects use Buttercup (a BDD testing framework for Emacs) with custom test macros. Font-lock tests insert code into a buffer, run , and assert that specific character ranges have the expected face: Indentation tests insert code, run , and assert the result matches the expected indentation: Integration tests load real source files and verify that both font-locking and indentation survive on the full file. This catches interactions between rules that unit tests miss. has 200+ automated tests and has even more. Investing in test infrastructure early pays off enormously – I can refactor indentation rules with confidence because the suite catches regressions immediately. When I became the maintainer of clojure-mode many years ago, I really struggled with making changes. There were no font-lock or indentation tests, so every change was a leap of faith – you’d fix one thing and break three others without knowing until someone filed a bug report. I spent years working on a testing approach I was happy with, alongside many great contributors, and the return on investment was massive. The same approach – almost the same test macros – carried over directly to when we built the TreeSitter version. And later I reused the pattern again in and . One investment in testing infrastructure, four projects benefiting from it. I know that automated tests, for whatever reason, never gained much traction in the Emacs community. Many popular packages have no tests at all. I hope stories like this convince you that investing in tests is really important and pays off – not just for the project where you write them, but for every project you build after. This one is specific to but applies broadly: compiling TreeSitter queries at runtime is expensive. If you’re building queries dynamically (e.g. with called at mode init time), consider pre-compiling them as values. This made a noticeable difference in ’s startup time. The Emacs community has settled on a suffix convention for TreeSitter-based modes: , , , and so on. This makes sense when both a legacy mode and a TreeSitter mode coexist in Emacs core – users need to choose between them. But I think the convention is being applied too broadly, and I’m afraid the resulting name fragmentation will haunt the community for years. For new packages that don’t have a legacy counterpart, the suffix is unnecessary. I named my packages (not ) and (not ) because there was no prior or to disambiguate from. The infix is an implementation detail that shouldn’t leak into the user-facing name. Will we rename everything again when TreeSitter becomes the default and the non-TS variants are removed? Be bolder with naming. If you’re building something new, give it a name that makes sense on its own merits, not one that encodes the parsing technology in the package name. I think the full transition to TreeSitter 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 TreeSitter isn’t just a mechanical translation – you need to understand the grammar, rewrite font-lock and indentation rules, handle version compatibility, and build a new test suite. That’s a lot of work. Interestingly, this might be one area where agentic coding tools can genuinely help. The structure of TreeSitter-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 , it could probably scaffold a reasonable new mode fairly quickly. The hard parts – debugging grammar quirks, handling edge cases, getting indentation just right – would still need human attention, but the boilerplate could be automated. Still, knowing the Emacs community, I wouldn’t be surprised if a full migration never actually completes. Many old-school modes work perfectly fine, their maintainers have no interest in TreeSitter, 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. TreeSitter is genuinely great for building Emacs major modes. The code is simpler, the results are more accurate, and incremental parsing means everything stays fast even on large files. I wouldn’t go back to regex-based font-locking willingly. But it’s not magical. Grammars are inconsistent across languages, the Emacs APIs are still maturing, you can’t reuse files (yet), and you’ll hit version-specific bugs that require tedious workarounds. The testing story is better than with regex modes – tree structures are more predictable than regex matches – but you still need a solid test suite to avoid regressions. If you’re thinking about writing a TreeSitter-based major mode, do it. The ecosystem needs more of them, and the experience of working with syntax trees instead of regexes is genuinely enjoyable. Just go in with realistic expectations, pin your grammar versions, test against multiple Emacs releases, and build your test suite early. Anyways, I wish there was an article like this one when I was starting out with and , so there you have it. I hope that the lessons I’ve learned along the way will help build better modes with TreeSitter down the road. That’s all I have for you today. Keep hacking! See the excellent scope discussion in the tree-sitter-clojure repo for the rationale.  ↩︎ There’s ongoing discussion in the Emacs community about distributing pre-compiled grammar binaries, but nothing concrete yet.  ↩︎ Regex-based font-locking is fragile. Regexes can’t parse nested structures, so they either under-match (missing valid code) or over-match (highlighting inside strings and comments). Every edge case is another regex, and the patterns become increasingly unreadable over time. Indentation engines are complex. SMIE (the generic indentation engine for non-TreeSitter 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. Use to verify the node type at point matches your query. Set to to see which rules are firing. 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 . Remember that rule order matters . Without , 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. Set to – this logs which rule matched for each line, what anchor was computed, and the final column. Use to understand the parent chain. The key question is always: “what is the parent node, and which rule matches it?” Remember that rule order matters for indentation too – the first matching rule wins. A typical set of rules reads top to bottom from most specific to most general: Watch out for the empty-line problem : when the cursor is on a blank line, TreeSitter has no node at point. The indentation engine falls back to the root node as the parent, which typically matches the top-level rule and gives column 0. In neocaml I solved this with a rule that looks at the previous line’s last token to decide indentation: See the excellent scope discussion in the tree-sitter-clojure repo for the rationale.  ↩︎ There’s ongoing discussion in the Emacs community about distributing pre-compiled grammar binaries, but nothing concrete yet.  ↩︎

0 views
(think) 1 months ago

Learning Vim in 3 Steps

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. Start with – it ships with Vim and takes about 30 minutes. It’ll teach you enough to survive: moving around, editing text, saving, quitting. The essentials. Once you’re past that, I strongly recommend Practical Vim by Drew Neil. This book changed the way I think about Vim. I had known the basics of Vim for over 20 years, but the Vim editing model never really clicked for me until I read it. The key insight is that Vim has a grammar – operators (verbs) combine with motions (nouns) to form commands. (delete) + (word) = . (change) + (inside quotes) = . Once you internalize this composable language, you stop memorizing individual commands and start thinking in Vim . The book is structured as 121 self-contained tips rather than a linear tutorial, which makes it great for dipping in and out. You could also just read cover to cover – Vim’s built-in documentation is excellent. But let’s be honest, few people have that kind of patience. Other resources worth checking out: Resist the temptation to grab a massive Neovim distribution like LazyVim on day one. You’ll find it overwhelming if you don’t understand the basics and don’t know how the Vim/Neovim plugin ecosystem works. It’s like trying to drive a race car before you’ve learned how a clutch works. Instead, start with a minimal configuration and grow it gradually. I wrote about this in detail in Build your .vimrc from Scratch – the short version is that modern Vim and Neovim ship with excellent defaults and you can get surprisingly far with a handful of settings. I’m a tinkerer by nature. I like to understand how my tools operate at their fundamental level, and I always take that approach when learning something new. Building your config piece by piece means you understand every line in it, and when something breaks you know exactly where to look. I’m only half joking. Peter Norvig’s famous essay Teach Yourself Programming in Ten Years makes the case that mastering any complex skill requires sustained, deliberate practice over a long period – not a weekend crash course. The same applies to Vim. Grow your configuration one setting at a time. Learn Vimscript (or Lua if you’re on Neovim). Read other people’s configs. Maybe write a small plugin. Every month you’ll discover some built-in feature or clever trick that makes you wonder how you ever lived without it. One of the reasons I chose Emacs over Vim back in the day was that I really hated Vimscript – it was a terrible language to write anything in. These days the situation is much better: Vim9 Script is a significant improvement, and Neovim’s switch to Lua makes building configs and plugins genuinely enjoyable. Mastering an editor like Vim is a lifelong journey. Then again, the way things are going with LLM-assisted coding, maybe you should think long and hard about whether you want to commit your life to learning an editor when half the industry is “programming” without one. But that’s a rant for another day. If this bulletproof plan doesn’t work out for you, there’s always Emacs. Over 20 years in and I’m still learning new things – these days mostly how to make the best of evil-mode so I can have the best of both worlds. As I like to say: The road to Emacs mastery is paved with a lifetime of invocations. That’s all I have for you today. Keep hacking! Just kidding – everyone asks me about learning Emacs. But here we are.  ↩︎ Advent of Vim – a playlist of short video tutorials covering basic Vim topics. Great for visual learners who prefer bite-sized lessons. ThePrimeagen’s Vim Fundamentals – if you prefer video content and a more energetic teaching style. vim-be-good – a Neovim plugin that gamifies Vim practice. Good for building muscle memory. Just kidding – everyone asks me about learning Emacs. But here we are.  ↩︎

0 views
(think) 1 months ago

How to Vim: Auto-save on Activity

Coming from Emacs, one of the things I missed most in Vim was auto-saving. I’ve been using my own super-save Emacs package for ages – it saves your buffers automatically when you switch between them, when Emacs loses focus, and on a handful of other common actions. After years of using it I’ve completely forgotten that exists. Naturally, I wanted something similar in Vim. Vim’s autocommands make it straightforward to set up basic auto-saving. Here’s what I ended up with: This saves the current buffer when Vim loses focus (you switch to another window) and when you leave Insert mode. A few things to note: You can extend this with more events if you like: Adding catches edits made in Normal mode (like , , or paste commands), so you’re covered even when you never enter Insert mode. works reliably in GUI Vim and most modern terminal emulators, but it may not fire in all terminal setups (especially inside tmux without additional configuration). One more point in favor of using Ghostty and not bothering with terminal multiplexers. The same autocommands work in Neovim. You can put the equivalent in your : Neovim also has ( ) which automatically saves before certain commands like , , and . It’s not a full auto-save solution, but it’s worth knowing about. There are several plugins that take auto-saving further, notably vim-auto-save for Vim and auto-save.nvim for Neovim. Most of these plugins rely on – an event that fires after the cursor has been idle for milliseconds. The problem is that is a global setting that also controls how often swap files are written, and other plugins depend on it too. Setting it to a very low value (say, 200ms) for snappy auto-saves can cause side effects – swap file churn, plugin conflicts, and in Neovim specifically, can behave inconsistently when timers are running. For what it’s worth, I think idle-timer-based auto-saving is overkill in Vim’s context. The simple autocommand approach covers the important cases, and anything more aggressive starts fighting against Vim’s grain. I’ve never been fond of the idle-timer approach to begin with, and that’s part of the reason why I created for Emacs. I like the predictability of triggering save by doing some action. Simplicity is the ultimate sophistication. – Leonardo da Vinci Here’s the thing I’ve come to appreciate about Vim: saving manually isn’t nearly as painful as it is in Emacs. In Emacs, is a two-chord sequence that you type thousands of times a day – annoying enough that auto-save felt like a necessity. In Vim, you’re already in Normal mode most of the time, so a quick mapping like: gives you a fast, single-keystroke save (assuming your leader is Space, which it should be). It’s explicit, predictable, and takes almost no effort. As always, I’ve learned quite a bit about Vim by looking into this simple topic. That’s probably the main reason I still bother to write such tutorial articles – they make me reinforce the knowledge I’ve just obtained and make ponder more than usual about the trade-offs between different ways to approach certain problems. I still use the autocommand approach myself – old habits die hard – but I have to admit that gets the job done just fine. Sometimes the simplest solution really is the best one. That’s all I have for you today. Keep hacking! instead of – it only writes when the buffer has actually changed, avoiding unnecessary disk writes. – suppresses errors for unnamed buffers and read-only files that can’t be saved.

0 views
(think) 1 months ago

How to Vim: To the Terminal and Back

Sooner or later every Vim user needs to drop to a shell – to run tests, check git status, or just poke around. Vim gives you two very different ways to do this: the old-school suspend and the newer command. Let’s look at both. Pressing in Vim sends a signal that suspends the entire Vim process and drops you back to your shell. When you’re done, type to bring Vim back exactly where you left it. You can also use or from command mode if you prefer. Vim 8.1 (released in May 2018) introduced – a built-in terminal emulator that runs inside a Vim window. This was a pretty big deal at the time, as I’ll explain in a moment. The basics are simple: In Neovim the key mapping to exit terminal mode is the same ( ), but you can also set up a more ergonomic alternative like by adding to your config. One of the most useful aspects of is running a specific command: The expands to the current filename, which makes this a quick way to test whatever you’re working on without leaving Vim. The output stays in a buffer you can scroll through and even yank from – handy when you need to copy an error message. You might be wondering how compares to the classic command. The main difference is that blocks Vim until the command finishes and then shows the output in a temporary screen – you have to press Enter to get back. runs the command in a split window, so you can keep editing while it runs and the output stays around for you to review. For quick one-off commands like or , bang commands are fine. For anything with longer-running output – tests, build commands, interactive REPLs – is the better choice. The story of is intertwined with the story of Neovim. When Neovim was forked from Vim in early 2014, one of its key goals was to add features that Vim had resisted for years – async job control and a built-in terminal emulator among them. Neovim shipped its terminal emulator (via libvterm) in 2015, a full three years before Vim followed suit. It’s fair to say that Neovim’s existence put pressure on Vim to modernize. Bram Moolenaar himself acknowledged that “Neovim did create some pressure to add a way to handle asynchronous jobs.” Vim 8.0 (2016) added async job support, and Vim 8.1 (2018) brought the terminal emulator. Competition is a wonderful thing. Here’s the honest truth: I rarely use . Not in Vim, and not the equivalent in Emacs either ( , , etc.). I much prefer switching to a proper terminal emulator – these days that’s Ghostty for me – where I get my full shell experience with all the niceties of a dedicated terminal (proper scrollback, tabs, splits, ligatures, the works). I typically have Vim in one tab/split and a shell in another, and I switch between them with a keystroke. I get that I might be in the minority here. Many people love having everything inside their editor, and I understand the appeal – fewer context switches, everything in one place. If that’s your style, is a perfectly solid option. But if you’re already comfortable with a good terminal emulator, don’t feel pressured to move your shell workflow into Vim just because you can. That’s all I have for you today. Keep hacking! Dead simple – no configuration, works everywhere. You get your real shell with your full environment, aliases, and all. Zero overhead – Vim stays in memory, ready to resume instantly. You can’t see Vim and the shell at the same time. Easy to forget you have a suspended Vim session (check with ). Doesn’t work in GUIs like gVim or in terminals that don’t support job control. – opens a terminal in a horizontal split – opens it in a vertical split – switches from Terminal mode back to Normal mode (so you can scroll, yank text, etc.)

0 views
(think) 1 months ago

How to Vim: Build your .vimrc from Scratch

People often think that getting started with Vim means spending hours crafting an elaborate with dozens of plugins. In reality, modern Vim (9+) and Neovim ship with remarkably sane defaults, and you can get very far with a configuration that’s just a few lines long – or even no configuration at all. If you launch Vim 9 without a file, it automatically loads – a built-in configuration that provides a solid foundation. Here’s what you get for free: That’s actually a pretty reasonable editing experience out of the box! You can read the full details with . Neovim goes even further with its defaults – it enables (copies indentation from the previous line), (highlights all search matches), (makes Tab smarter at the start of a line), (reloads files changed outside the editor), always shows the statusline, and sets the command history to 10000 entries, among many other things. If you’re on Neovim, the out-of-the-box experience is excellent. See for the full list. Here’s something that trips up a lot of people: the moment you create a file – even an empty one – Vim stops loading entirely. That means you lose all those nice defaults. The fix is simple. Start your with: This loads the defaults first, and then your own settings override or extend them as needed. This gotcha only applies to Vim. Neovim’s defaults are always active regardless of whether you have an or . Here’s a minimal that builds on the defaults and adds a few things most people want: That’s five settings on top of the defaults. You might not even need all of them – already handles the fundamentals. For Neovim, you don’t need the line – all the equivalents are already active. You also get , , and for free, so the only settings left to add are the ones that are genuinely personal preference: One of the most underappreciated aspects of Vim is how much built-in support it ships for programming languages. When is active (which it is via or Neovim’s defaults), you automatically get: This means that when you open a Python file, Vim already knows to use 4-space indentation. Open a Ruby file and it switches to 2 spaces. Open a Makefile and it uses tabs. All without a single plugin or line of configuration. You can check what’s available with for syntax files or for filetype plugins. The list is impressively long. At some point you’ll probably want more than the bare minimum. Here are a few things worth considering as your next steps: And when you eventually want more plugins, you probably won’t need many. A fuzzy finder, maybe a Git integration, and perhaps a completion engine will cover most needs. But that’s a topic for another day. The key takeaway is this: don’t overthink your . Start with the defaults, add only what you actually need, and resist the urge to copy someone else’s 500-line configuration. A small, well-understood configuration beats a large, cargo-culted one every time. That’s part of the reason why when I started to re-learn Vim I’ve opted to slowly build a Vim 9 configuration from scratch, instead of jumping to something like Neovim + Kickstart.nvim or LazyVim right away. Less is more. Understanding the foundations of your editor matters. 1 Right now my is just 100 lines and I don’t foresee it becoming much bigger in the long run. If you want to see just how far you can go without plugins, I highly recommend the Thoughtbot talk How to Do 90% of What Plugins Do (With Just Vim) . It’s a great demonstration of Vim’s built-in capabilities for file finding, auto-completion, tag navigation, and more. That’s all I have for you today. Keep hacking! I guess this sounds strange coming from the author of Emacs Prelude, right?  ↩︎ – syntax highlighting – filetype detection, language-specific plugins, and automatic indentation – incremental search (results appear as you type) – keeps 5 lines of context around the cursor – shows instead of hiding truncated lines – mouse support in all modes remapped to (text formatting) instead of the mostly useless Ex mode And several other quality-of-life improvements Syntax highlighting for hundreds of languages – Vim ships with around 770+ syntax definitions Language-specific indentation rules for over 420 file types Filetype plugins that set sensible options per language (e.g., , , ) A colorscheme – Vim ships with several built-in options (try followed by Tab to see them). Recent Vim builds even bundle Catppuccin – a beautiful pastel theme that I’m quite fond of. Another favorite of mine is Tokyo Night , which you’ll need to install as a plugin. Neovim’s default colorscheme has also been quite good since 0.10. Persistent undo – lets you undo changes even after closing and reopening a file. A game changer. Clipboard integration – makes yank and paste use the system clipboard by default. vim-unimpaired – if you’re on classic Vim (not Neovim), I think Tim Pope’s vim-unimpaired is essential. It adds a consistent set of / mappings for navigating quickfix lists, buffers, adding blank lines, and much more. Neovim 0.11+ has adopted many of these as built-in defaults, but on Vim there’s no substitute. I guess this sounds strange coming from the author of Emacs Prelude, right?  ↩︎

0 views
(think) 1 months ago

Adding Empty Lines in Vim: Redux

A long time ago I wrote about adding empty lines in Emacs on my other blog, Emacs Redux. Now it’s time for the Vim version of this. Adding a blank line above or below the cursor is one of those tiny operations you do constantly, and Vim gives you several ways to do it – each with different trade-offs. Most Vim users reach for (open line below) or (open line above). These work, but they drop you into Insert mode. If you just want a blank line and want to stay in Normal mode, you need or . This gets the job done, but has a couple of annoyances: On the bright side, / accept a count – inserts 5 blank lines below. A lesser-known approach that avoids the issues above: The here is Vim’s black hole register, which is always empty. Since inserts register contents linewise, putting “nothing” results in a clean blank line. No trailing whitespace, no register pollution, and you stay in Normal mode. (using the expression register) works the same way, but is shorter and easier to remember. The downside? It’s verbose to type interactively, and the cursor still moves to the new line. This is the gold standard: They also accept a count – adds 3 blank lines below. In Neovim 0.11+ , these are built-in default mappings. In regular Vim, you need Tim Pope’s vim-unimpaired plugin. Note that the two implementations differ slightly – Neovim uses which preserves the cursor position exactly, while vim-unimpaired uses with mark jumps, which keeps you on the same line but resets the column to 0. If you’re on plain Vim without plugins, you can add equivalent mappings to your : The approach is the cleanest – no side effects on registers, marks, or undo granularity. In Insert mode your options are simpler: is handy when you want to add a line without breaking your Insert mode flow. My recommendation? Use and . Like most Vim users, I relied on and for years, but once I discovered vim-unimpaired (and later the equivalent built-in mappings in Neovim 0.11) I never looked back. More broadly, I’m a huge fan of the uniform / convention that vim-unimpaired pioneered – / for quickfix, / for buffers, / for blank lines, and so on. It’s a consistent, mnemonic system that’s easy to internalize, and I’m glad Neovim has been adopting it as built-in defaults. If you’re on Vim, do yourself a favor and install vim-unimpaired . That’s all I have for you today. Keep hacking! The cursor moves to the new blank line. Auto-indent may leave trailing whitespace on the “blank” line. It pollutes the repeat register (it records an insert operation). – adds a blank line below the current line – adds a blank line above the current line – adds a blank line below – adds a blank line above – creates a new line below (or splits the line if the cursor is in the middle of text). – executes as a one-shot Normal mode command, opening a line below. You end up in Insert mode on the new line. – same thing, but opens a line above.

0 views
(think) 1 months ago

Supercharging Claude Code with the Right (CLI) Tools

I’ve been using Claude Code quite a bit lately, and I got curious – what if I asked it directly which tools would make it more productive? Not the usual suspects like , or , but tools it wishes it had access to, tools that would genuinely extend its capabilities. So I did exactly that. I asked Claude Code: “What are the most valuable CLI tools I could install for you, outside of the ones you already have?” The answer was surprisingly thoughtful and insightful, so I figured I’d share it here along with my own commentary. Here are 10 tools, ranked by how useful they’d be for an AI coding assistant. Note: I write all my blog posts old-school, but this time around I took the liberty to just extend with my comments the output generated by Claude Code. Note also that the post includes some installation instructions that are macOS-specific. That’s what I got from Claude on my local machine (a Mac mini), and I felt it didn’t make much sense to tweak them given how many combinations of operating systems and package managers exist. This was Claude’s number one pick, and I can see why. ast-grep does structural code search and refactoring using AST patterns. Instead of fumbling with regex to find “all calls to function X with 3 arguments”, you write patterns that look like actual code: This is the kind of thing where regex is fragile and error-prone, but AST matching just works. Supports 20+ languages via tree-sitter . A structural diff tool that understands syntax. difftastic compares files by AST nodes rather than lines, so it won’t flag whitespace changes or reformatting as meaningful diffs. This makes reviewing AI-generated changes much clearer – and let’s be honest, reviewing changes is half the job when working with an AI assistant. AI assistants generate a lot of shell commands, and shell scripting is notoriously full of pitfalls (unquoted variables, vs. , POSIX compatibility…). ShellCheck catches these before they blow up. Given that shell bugs can be destructive (e.g., expanding to ), having a safety net here is valuable. A modern replacement with sane regex syntax – no more escaping nightmares. Uses standard PCRE-style regex and has a string-literal mode ( ) for replacing code strings full of metacharacters. Simple, but it eliminates a whole class of errors when generating substitution commands. Sloc Cloc and Code – a fast code counter that gives you an instant overview of a codebase: languages, lines of code, complexity estimates. Understanding the shape of a project before diving in is genuinely useful context for an AI assistant, and this is hard to replicate by manually scanning files. Note: I was under the impression that cloc is a better tool, but perhaps I was mistaken. 1 for YAML (and JSON, TOML, XML). Modern projects are drowning in YAML – GitHub Actions workflows, Kubernetes manifests, Docker Compose files. yq can programmatically query and update YAML while preserving comments and formatting, which is much more reliable than text-based editing that can break indentation. Structural search and replace that works across languages without needing a full parser. Complements ast-grep for simpler pattern matching – it understands delimiters (braces, parens, quotes) but doesn’t need tree-sitter grammar support. Great for quick refactoring across less common languages or config files. Note: I was happy to see that was written in OCaml, but when I installed it I got a warning that the project was deprecated and doesn’t support OCaml 5, so I’m not sure about its future. A command-line benchmarking tool that runs commands multiple times and gives you proper statistical analysis. When you ask an AI to optimize something, it’s nice to have real numbers. The flag produces results ready for a PR description. A file watcher that executes commands when files change. Useful for setting up persistent feedback loops – rerun tests on save, rebuild docs when markdown changes, restart a dev server after config edits. One command instead of cobbling together something with and shell scripts. A syntax-highlighting pager for and friends. Provides word-level diff highlighting, so when only a variable name changes in a long line, you see exactly that. Mostly benefits the human reviewing the AI’s work, but that’s arguably where it matters most. If you only install one tool from this list, make it . It’s the biggest capability gap – an AI assistant limited to regex-based search and replace is like a carpenter limited to a hand saw. Everything else is nice to have, but structural code understanding is a genuine superpower. You can install everything at once if you’re feeling adventurous: I’m not ashamed to admit that I had never heard of some of the tools (e.g. , and ), and I had only one of them installed ( ). 2 It’s never too late to learn something new! By the way, keep in mind that depending on the programming languages that you’re using there are other language specific tools that you can benefit from, so make sure to ask your favorite AI coding tool about those. That’s all I have for you today. Keep hacking! I asked Claude about this as well and it told me that it prefers because it’s written in Go (as opposed to Perl) and therefore it’s much faster than .  ↩ Of course, I didn’t really have it installed - I only thought I did, otherwise Claude wouldn’t have suggested it. (I switch between computers and my setup on all of them is not exactly the same)  ↩ I asked Claude about this as well and it told me that it prefers because it’s written in Go (as opposed to Perl) and therefore it’s much faster than .  ↩ Of course, I didn’t really have it installed - I only thought I did, otherwise Claude wouldn’t have suggested it. (I switch between computers and my setup on all of them is not exactly the same)  ↩

1 views
(think) 1 months ago

How to Vim: Many Ways to Paste

Most Vim users know and – paste after and before the cursor. Simple enough. But did you know that Vim actually has around a dozen paste commands, each with subtly different behavior? I certainly didn’t when I started using Vim, and I was surprised when I discovered the full picture. Let’s take a tour of all the ways to paste in Vim, starting with Normal mode and then moving to Insert mode. One important thing to understand first – it’s all about the register type . Vim registers don’t just store text, they also track how that text was yanked or deleted. There are three register types (see ): This is something that trips up many Vim newcomers – the same command can behave quite differently depending on the register type! With that in mind, here’s the complete family of paste commands in Normal mode: The “Direction” column above reflects both cases – for characterwise text it’s “after/before the cursor”, for linewise text it’s “below/above the current line”. How to pick the right paste command? Here are a few things to keep in mind: All Normal mode paste commands accept a count (e.g., pastes three times) and a register prefix (e.g., pastes from register ). In Insert mode things get interesting. All paste commands start with , but the follow-up keystrokes determine how the text gets inserted: Let me unpack this a bit: Note: Plain can be a minor security concern when pasting from the system clipboard ( or registers), since control characters in the clipboard will be interpreted. When in doubt, use instead. And that’s a wrap! Admittedly, even I didn’t know some of those ways to paste before doing the research for this article. I’ve been using Vim quite a bit in the past year and I’m still amazed how many ways to paste are there! If you want to learn more, check out , , , and in Vim’s built-in help. There’s always more to discover! That’s all I have for you today. Keep hacking! You can also use and with a register, e.g. to paste from register with adjusted indentation.  ↩ Characterwise (e.g., ): inserts text to the right of the cursor, to the left. Linewise (e.g., , ): inserts text on a new line below, on a new line above. The cursor position within the line doesn’t matter. Blockwise (e.g., selection): text is inserted as a rectangular block starting at the cursor column. The difference between / and / is all about where your cursor ends up. With the cursor lands on the last character of the pasted text, while with it moves just past the pasted text. This makes handy when you want to paste something and continue editing right after it. and are incredibly useful when pasting code – they automatically adjust the indentation of the pasted text to match the current line. No more pasting followed by to fix indentation! 1 and are the most niche – they only matter when pasting blockwise selections, where they avoid adding trailing whitespace to pad shorter lines. is the most common one – you press and then a register name (e.g., , , for the system clipboard). The text is inserted as if you typed it, which means and auto-indentation apply. This can be surprising if your pasted code gets reformatted unexpectedly. inserts the text literally – special characters like backspace won’t be interpreted. However, and auto-indent still apply. is the “raw paste” – no interpretation, no formatting, no auto-indent. What you see in the register is what you get. This is the one I’d recommend for pasting code in Insert mode. is like , but it adjusts the indentation to match the current context. Think of it as the Insert mode equivalent of . You can also use and with a register, e.g. to paste from register with adjusted indentation.  ↩

0 views
(think) 2 months ago

Neocaml 0.1: Ready for Action

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 - recently landed in MELPA , which means installing it is now as easy as: That’s quite the journey from “a fun experimental project” to a proper Emacs package! You might be wondering what’s wrong with the existing options. The short answer - nothing is wrong per se, but offers a different set of trade-offs: Of course, is the youngest of the bunch and it doesn’t yet match Tuareg’s feature completeness. But for many OCaml workflows it’s already more than sufficient, especially when combined with LSP support. I’ve started the project mostly because I thought that the existing Emacs tooling for OCaml was somewhat behind the times - e.g. both and have features that are no longer needed in the era of . Let me now walk you through the highlights of version 0.1. The current feature-set is relatively modest, but all the essential functionality one would expect from an Emacs major mode is there. leverages TreeSitter for syntax highlighting, which is both more accurate and more performant than the traditional regex-based approaches used by and . The font-locking supports 4 customizable intensity levels (controlled via , default 3), so you can pick the amount of color that suits your taste. Both (source) and (interface) files get their own major modes with dedicated highlighting rules. Indentation has always been tricky for OCaml modes, and I won’t pretend it’s perfect yet, but ’s TreeSitter-based indentation engine is already quite usable. It also supports cycle-indent functionality, so hitting repeatedly will cycle through plausible indentation levels - a nice quality-of-life feature when the indentation rules can’t fully determine the “right” indent. If you prefer, you can still delegate indentation to external tools like or even Tuareg’s indentation functions. Still, I think most people will be quite satisfied with the built-in indentation logic. provides proper structural navigation commands ( , , ) powered by TreeSitter, plus integration definitions in a buffer has never been easier. The older modes provide very similar functionality as well, of course, but the use of TreeSitter in makes such commands more reliable and robust. No OCaml mode would be complete without REPL (toplevel) integration. provides all the essentials: The default REPL is , but you can easily switch to via . 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. works great with Eglot and , automatically setting the appropriate language IDs for both and files. Pair with ocaml-eglot and you get a pretty solid OCaml development experience. The creation of LSP really simplified the lives of a major mode authors like me, as now many of the features that were historically major mode specific are provided by LSP clients out-of-the-box. That’s also another reason why you probably want to leaner major mode like . But, wait, there’s more! There’s still plenty of work to do: If you’re following me, you probably know that I’m passionate about both Emacs and OCaml. I hope that will be my way to contribute to the awesome OCaml community. I’m not sure how quickly things will move, but I’m committed to making the best OCaml editing experience on Emacs. Time will tell how far I’ll get! If you’re an OCaml programmer using Emacs, I’d love for you to take for a spin. Install it from MELPA, kick the tires, and let me know what you think. Bug reports, feature requests, and pull requests are all most welcome on GitHub ! That’s all from me, folks! Keep hacking! 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. 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. aims to be a modern, lean alternative that fully embraces TreeSitter. The codebase is small, well-documented, and easy to hack on. If you’re running Emacs 29+ (and especially Emacs 30), TreeSitter is the future and is built entirely around it. - Start or switch to the OCaml REPL - Send the current definition - Send the selected region - Send the entire buffer - Send a phrase (code until ) to quickly switch between and files Prettify-symbols support for common OCaml operators Automatic installation of the required TreeSitter grammars via Compatibility with Merlin for those who prefer it over LSP Support for additional OCaml file types (e.g. ) Improvements to structured navigation using newer Emacs TreeSitter APIs Improvements to the test suite Addressing feedback from real-world OCaml users Actually writing some fun OCaml code with

0 views
(think) 2 months ago

A Fresh Look for the Blog: Switching to Jekyll’s Chirpy Theme

It’s been over 4 years since I last updated the look of my blog - in late 2021 I switched to the popular at the time “Minimal Mistakes” theme . It served we well, but like so many Jekyll themes it got abandoned in recent years, so I’ve been shopping recently for a good actively-maintained alternative. After some research I’ve decided on Chirpy as the best option for my use-case, but I’ve been putting off the actual switch for quite a while now. Today it hit me that this was the perfect task for an AI agent like Claude Code , so I’ve decided to just offload the migration to it. The whole process was really smooth - I started with the very simple prompt “Switch my blog’s theme to Chirpy” and pointed Claude to Chirpy’s docs. After less than 1 hour and fairly few follow-up requests to address some small issues (most related to deploying on GitHub Pages ), my updated blog was up and running. As a bonus Claude also: Good stuff! Obviously I could have the done the migration manually, as I’ve always done in the past, but these days I’m quite bored doing mechanical work that doesn’t require much thinking. This was the main reason why I procrastinated on this subject for so long and I think that such boring tasks for me are the most appealing use-cases for AI agents like Claude. If we spent less time on the things that we don’t enjoying doing, this obviously leaves us with more time to spend on the things that we’re actually passionate about. I’ll never use AI to write blog posts, but I’m perfectly fine with delegating the maintenance tasks around my various blogs to tools like Claude. There are many AI uses that I detest (news, blog posts, videos, etc), but I think in programming there are plenty of good use-cases for AI. Perhaps this modest article will inspire you to finish some programming-related task that you’ve been putting off for a very long time? That’s all I have for you today. I hope you’ll enjoy the new look of my blog. Keep hacking! Updated inconsistent tags in my articles (e.g. and ) Updated a reference to the blog’s theme in the colophon Provided a GitHub Action for the deployment of the blog that allows me to run Jekyll 4 directly

0 views
(think) 3 months ago

Tips on Using Mastodon

I’ve started using Mastodon over 3 years ago, in the aftermath of Twitter’s meltdown. At first I didn’t like it much, as there were a few things about it I found frustrating: Eventually I realized the simplest things you can do to have a nice Mastodon experience is to pick some reasonably popular instance (so you known it’s stable and won’t disappear after a while) and some decent third-party client. I never read the local timeline, so in the end I don’t think it matters much which instance you’re end up using. If you don’t want to waste time researching instances just go with the “default” mastodon.social instance. You can certainly do a lot worse, and of course, you can always move your account to another Mastodon instance down the road. When it comes to clients, I’ve played with quite a few and enjoyed the most the following: I’ve used Mona for about a year before switching to Ivory. Mona’s a good app overall, but I experienced some stability issues with it and I felt the UI was a bit cluttered. That’s why eventually I moved to Ivory. (plus I like their logo better) Both Mona and Ivory are proprietary apps and require a subscription to have access to all their features, but these are money well spent IMO. I really like desktop apps, so Mona and Ivory made my interactions with Mastodon way more pleasant (and frequent). Not that the webapps are bad, I just don’t like having a ton of pinned windows in my browser. One more thing - embrace hashtags in Mastodon! Twitter had made those popular in its early days, but eventually moved away from them. They are quite alive in Mastodon and I use them extensively to follow topics that are important to me. (e.g. posts about various programming languages) So, it’s a good to use them in your posts, so other people can discover your post via the hashtags. That’s all from me on the topic for now. I hope you’ve learned something useful. In the Fediverse we trust! You had to pick some instance to join (or host your own), so I spent quite a bit of time looking into various instances The default web UI was not my cup of tea - I found it to be complex and unwieldy. This was probably the main reason I didn’t enjoy using Mastodon at first. The interaction metrics (boosts, likes, etc) were often out-of-sync in different clients. That was improved in recent versions of Mastodon, but it was definitely surprising to me coming from Twitter. Mona (native app for all Apple devices) Ivory (same as Mona, but with a simpler user interface) Phanpy (simple and sleek web client)

0 views
(think) 3 months ago

How to Vim: Navigating Prose in Style

I don’t know about you, but I’m not using Vim solely for programming. I also write documentation in it, plus most of my blog posts (like this one). When dealing with prose (regular text), it’s good to know a couple of essential Vim motions: Vim’s check for beginning/end of sentence is not very precise, but it mostly gets the job done. And because paragraphs are just blocks of text surrounded by blanks lines that’s handy in programming contexts as well. The forward sentence motion positions the cursor on the first character in the next sentence, or on the line after the paragraph (if the sentence is the last in a paragraph). The backward sentence operates similarly - it goes to the first character in the previous (or current) sentence. The paragraph motions will take you to the empty lines before or after a paragraph. Due to the simple definition of a paragraph in Vim, those are quite reliable. I guess in the world of motions like the ones provided by and you might be wondering if learning the rudimentary motions is worth it all. In my experience it’s never a bad idea to be able to use someone else’s setup, and the built-in functionality is naturally the smallest common denominator. That’s all I have for you today. Keep hacking! and allow you to move backward/forward in sentences and allow you to move backward/forward in paragraphs

1 views
(think) 3 months ago

How to Vim: Alternative Approach to Find and Replace

The classic way to do “find and replace” in Vim is pretty well known: This will replace all instances of in the current buffer (that’s what the is about) with . The flag means you’ll get prompted for confirmation for every replacement. Not bad, right? Still, often you need to replace just a few instances of something, so the above might be a bit too much typing. Imagine you’re dealing with the following text: If you want to replace the instances with the fastest way to do this would be something like: Pretty sweet and quite interactive in my opinion. It also allows you easily skip matches you don’t want to replace. And there are a few other tricks you can keep in mind: So, there you have it - another way to do “find and replace” in Vim! Keep hacking! - this will take you to the beginning of - this will replace with - this will take you to the next match and repeat the last edit you did You can use to select the word under the cursor and start a search with it If you’re searching for something more complex (e.g. it has multiple words) you can use instead of . means the next search match.

0 views