Post

Neocaml 0.6: Opam, Dune, and More

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 .ml and .mli), a REPL integration, and not much else. I was quite happy with how things turned out and figured the next steps would be mostly polish and bug fixes.

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 neocaml-mode, 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.

neocaml-opam-mode

The new neocaml-opam-mode activates automatically for .opam and opam files. It provides:

  • 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 opam lint on the current buffer, giving you inline diagnostics for missing fields, deprecated constructs, and syntax errors

The flymake backend registers automatically when opam is found in your PATH, but you need to enable flymake-mode yourself:

1
(add-hook 'neocaml-opam-mode-hook #'flymake-mode)

Flycheck users get opam lint support out of the box via Flycheck’s built-in opam checker — no extra configuration needed.

This bridges some of the gap with Tuareg, which also bundles an Opam major mode (tuareg-opam-mode). The Tree-sitter-based approach gives us more accurate highlighting, and the flymake integration is a nice bonus on top.

neocaml-dune-mode

neocaml-dune-mode handles dune, dune-project, and dune-workspace files — all three use the same s-expression syntax and share a single Tree-sitter grammar. You get:

  • 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 which-func support

This removes the need to install the separate dune package (the standalone dune-mode 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 auto-mode-alist entries.

neocaml-dune-interaction-mode

Beyond editing Dune files, I wanted a simple way to run Dune commands from any neocaml buffer. neocaml-dune-interaction-mode is a minor mode that provides keybindings (under C-c C-d) and a “Dune” menu for common operations:

KeybindingCommand
C-c C-d bBuild
C-c C-d tTest
C-c C-d cClean
C-c C-d pPromote
C-c C-d fFormat
C-c C-d uLaunch utop with project libraries
C-c C-d rRun an executable
C-c C-d dRun any Dune command
C-c C-d .Find the nearest dune file

All commands run via Emacs’s compile, so you get error navigation, clickable source locations, and the full compilation-mode interface for free. With a prefix argument (C-u), build, test, and fmt run in watch mode (--watch), automatically rebuilding when files change.

The utop command is special — it launches through neocaml-repl, so you get the full REPL integration (send region, send definition, etc.) with your project’s libraries preloaded.

This mode is completely independent from neocaml-dune-mode — it doesn’t care which major mode you’re using. You can enable it in OCaml buffers like this:

1
(add-hook 'neocaml-base-mode-hook #'neocaml-dune-interaction-mode)

Rough Edges

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.

What’s Next?

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 (.mll) or Menhir (.mly) 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!

This post is licensed under CC BY 4.0 by the author.