Yeah, I’m certainly open to improving the upstream defaults. Feel free to open issues with concrete suggestions, so we can discuss them in a more focused manner.
This morning I did a bit of a re-org of the font-lock defaults in neocaml, prompted by the discussion here. I haven’t addressed the specific issues we’ve been discussing, but I did notice that I actually hadn’t done some changes I thought I had already done… ![]()
In a nutshell:
- Moved
typefrom level 3 to level 2 andnumberfrom level 2 to level 3 - Split
escape-sequenceout ofstringinto its own feature at level 3 - Split
property(record fields) andlabel(labeled arguments) out ofvariableinto their own features at level 4
New layout:
| Level | Features |
|---|---|
| 1 | comment, definition |
| 2 | keyword, string, type |
| 3 | attribute, builtin, constant, escape-sequence, number |
| 4 | operator, bracket, delimiter, variable, property, label, function |
Now the defaults are actually aligned with the official Emacs suggestions, and the features are more granular and more aligned with what an OCaml programmer might want to font-lock differently.
FYI - Release v0.4.0 · bbatsov/neocaml · GitHub
You’ll need to update your Tree-sitter grammars due to some upstream (breaking) changes in the TS OCaml grammar.
I just reinstalled Prelude v2.1.0. I have (require 'prelude-ocaml) in my ~/.emacs.d/personal/prelude-modules.el.
I have in ~/.emacs.d/personal/init.el:
(use-package ocaml-eglot
:ensure t
:hook
(neocaml-base-mode . ocaml-eglot)
(ocaml-eglot . eglot-ensure))
with neocaml 20260330.1402 and ocaml-eglot 20260317.617
ocaml-eglot doesn’t load.
[eglot] (warning) Wrong type argument: processp, nil
eglot--current-server-or-lose: jsonrpc-error: "No current JSON-RPC connection", (jsonrpc-error-code . -32603), (jsonrpc-error-message . "No current JSON-RPC connection")
and I have these packages in my switch:
$ opam list | grep -E '(lsp)|(json)'
jsonrpc 1.21.0-4.14 Jsonrpc protocol implemenation
lsp 1.21.0-4.14 LSP protocol implementation in OCaml
ocaml-lsp-server 1.21.0-4.14 LSP Server for OCaml
any help? I’m also launching emacs directly, without the systemd emacs server unit.
cc @xvw
I use almost the same use-package for ocaml-eglot + neocaml. Does prelude links ocamllsp as a server ?
(defun neocaml-ocp-indent-setup ()
(setq-local indent-line-function #'ocp-indent-line)
(setq-local indent-region-function #'ocp-indent-region))
(use-package neocaml
:ensure t
:hook
(neocaml-base-mode . neocaml-ocp-indent-setup)
(neocaml-base-mode . outline-minor-mode)
;; FIXME: Change keybindings
;; (neocaml-base-mode . neocaml-repl-minor-mode)
(neocaml-base-mode . neocaml-dune-interaction-mode)
(neocaml-dune-mode . neocaml-dune-interaction-mode)
(neocaml-opam-mode . neocaml-dune-interaction-mode)
(neocaml-opam-mode . flymake-mode)
:config
(setq treesit-font-lock-level 4)
;; Register neocaml modes with Eglot-Managed-Mode-Hook
(with-eval-after-load 'eglot
(add-to-list 'eglot-server-programs
'((neocaml-mode neocaml-interface-mode) . ("ocamllsp")))))
(use-package ocaml-eglot
:ensure t
:after neocaml
:hook
(neocaml-base-mode . ocaml-eglot)
(ocaml-eglot . eglot-ensure)
:config
(setq ocaml-eglot-syntax-checker 'flymake)
(set-face-foreground 'eglot-diagnostic-tag-unnecessary-face "gold3")
(set-face-background 'eglot-highlight-symbol-face "dark slate blue")
(set-face-foreground 'eglot-highlight-symbol-face "plum"))
(use-package opam-switch-mode
:ensure t
:hook
(tuareg-mode . opam-switch-mode)
(neocaml-mode . opam-switch-mode)
(neocaml-interface-mode . opam-switch-mode))
(use-package ocp-indent
:ensure t
:config
(add-hook
'ocaml-eglot-hook
(lambda ()
(ocp-setup-indent)
(setq-local indent-line-function #'ocp-indent-line)
(setq-local indent-region-function #'ocp-ident-region))))
That must be it, because with your settings, my setup works again (I’ve also removed (require 'prelude-ocaml) from my config). Thanks for the help!
The (ocaml-eglot-setup) seems missing here: prelude/modules/prelude-ocaml.el at master · bbatsov/prelude · GitHub (cc @bbatsov)
Hmm, I was under the impression that running the minor mode runs the setup function, so it’s not needed to run it by itself. I’ll have to double check the code.
In general any additional setup should not be needed from the users, but the code in Prelude is slight more involved than the typical config as it also supports lsp-mode. Might be good to eventually make ocaml-eglot work with both Eglot and LSP mode as well, but that’s a conversation for another day.
So, it seems that ocaml-eglot-setup is not needed indeed:
(define-minor-mode ocaml-eglot
"Minor mode for interacting with `ocaml-lsp-server' using `eglot' as a client.
OCaml Eglot provides standard implementations of the various custom-requests
exposed by `ocaml-lsp-server'."
:lighter " OCaml-eglot"
:keymap ocaml-eglot-map
:group 'ocaml-eglot
(if ocaml-eglot (ocaml-eglot-setup) (ocaml-eglot-clean)))
One more thing - at some point we might actually name the mode “ocaml-eglot-mode” it’s clear it’s a minor mode.
It’s not very common to have modes with names that don’t include the word “mode” and this was definitely a small surprise for me at the beginning.
When you get the chance - please, check the fixes I pushed to Prelude and let me know if they work well for you.
I’m now on Prelude dd8899f, with no other configuration than (require 'prelude-ocaml), and I get the same error as I reported.
If I disable the prelude-ocaml module and I use Xavier’s config, it works. Did your Prelude patch require the ocaml-eglot “mode” patch? It’s not available yet in MELPA.
It took me a while, but I think I finally figured out what was going on. I had forgotten to add an entry about ocamllisp in neocaml’s init code, and I guessing most people had either caml-mode or tuareg lying around to provide such an entry (there are two things needed to have a mode play nice with Eglot and neocaml was doing only one of them). I’ve made a couple to Prelude (and neocaml) that should address this. (and updated Prelude to reflect the small update to ocaml-eglot I just did).
sorry… but I’ve updated the packages and prelude, and the default configuration still errors.
neocaml 20260331.1516
ocaml-eglot 20260331.1306
and Prelude 2497ca1.
Ah, that’s totally on me! In a typical fashion I had forgotten a pair of parentheses in the code I committed…
As I was reloading code here and there I didn’t notice the mistake I made, but now I tested with a fresh Prelude instance and I’m 100% sure the problem is solved now.
Sorry for the back and forth, but at least we fixed a tricky bug!
It’s working! it’s working!
Thanks for the fixes ![]()
So I think I’m now a user of neocaml-mode, thanks @bbatsov for your work on this.
But… I tried to play with these levels and colors and I was not going anywhere. First, it’s difficult to precisely target face changes. Second, I don’t think these “official” generic features combined with the font-lock-* face schemes make any sense from an ergonomic point of view. It looks like a programmer’s approach to ergonomics and visual design (in other words, likely unsuitable :–)
Notably:
-
Lexemes you want to distinguish are often lumped into the same feature/face. But in order to make it easier for humans to parse a busy context you want to make distinctions inside a given lexical class (e.g. infix operators and boolean operators, or making distinctions between keywords).
-
Lots of lexemes in these lexical classes are not worth highlighting. Drawing attention to them impedes on the reading flow. In general I’m mostly interested in highlighting the block structure so that’s it’s easier to navigate and clarify the structure of certain busy contexts (e.g. conditionals).
-
Having a dozen of face/colors is unlikely to help, the resulting fruit salad is a random mix of attention seeking lexemes instead of subtly highlighting the code structure. There’s a reason why graphic designers stick to ~2-5 colors and one or two typefaces when they design.
A good example of these problems is the punctuation.delimiter feature of upstream’s (not neocaml) highlights.scm. I don’t want to pay attention to most of this punctuation precisely because it is punctuation; it mostly works without colorizing it. However I do want to colorize -> because it is indicative of block structure and helps with certain contexts (e.g. separates patterns/variables from code, helps delineating arguments in more involved functional type signatures).
Same with lumping all operators in a single feature. To me highlighting named (logical) infix operators makes sense so that you don’t confuse them with variable names, but most other operators can simply remain bland: they stand on their own by the fact that they are operators (vs. alphabetic text). On the other hand I find highlighting boolean binary operators useful because it makes it easier to delineate the clauses in conditionals.
So I rewrote the font-lock features from scratch mostly making the distinctions caml-mode is doing because in my opinion they make sense from the language’s perspective. While I didn’t design them and I’m certainly buyest since I used them for the past 20+ years, I still think I can give a reasonably rational ergonomic argument for each of these (and for each of those not defined).
In turn I map each of the features to a single face named after the feature. This allows users to adapt the font-lock to their emacs generic theme which may not make all the appropriate distinction or whose definition may result in making wrong emphasis. I didn’t bother adding levels because this is design: the result is what you need.
Now @bbatsov what are your thoughts about this? Do you see a way to integrate this in your work or am I doomed to have these ~150 lines of elisp in my init.el forever?
I can see these paths:
-
I just keep my
init.elbusy. That’s ok :–) I still feel that eventually I’m a better place withneocamland these 150 lines of elisp than with a slowly rottingcaml-mode. -
Have an alternative
neocaml-use-alt-font-lockthat replaces the generic stuff with these language aware definitions – which I’m happy to help maintaining. -
Define the generic features in terms of union of subsets of features that include these definitions and add per feature face binding so that I can define my own view on this with a simple
treesit-font-lock-recompute-featuresand a few face redefinitions. -
Simply don’t bother about the generic stuff and use these definitions, perhaps only as a starting point, for a couple of well designed and ergonomic font-lock schemes.
Personally I don’t think that having the generic stuff as a default is good for the OCaml programmer and would rather go with 4. But perhaps it would also be interesting to hear about tuareg’s users on the matter as I don’t know how their highlighting scheme is designed.
Thanks for another round of great feedback!
I’m open to open 2. (having a “caml-mode” inspired alternative font-lock scheme), although this comes with the slight overhead of having to update the font-lock rules in both places (and the likelihood of forgetting to do something), and there’s also the question about how much test coverage we want to do for it.
For options 3. and 4. perhaps you can suggest some code illustrating the ideas, so I can get a better sense for them? Might also be easier to discuss this topic and our options on GitHub, as I find it convenient to have issues there for future reference.
Probably it will be simplest to come up with more granular font-lock features (more of them, but more focused), so people have more flexibility how to combine them, although it might be tricky to come up with granularity that everyone agrees with.
Thanks for your anwser. I’m still tweaking stuff around some label edge cases, I will open something on the repo at some point.
For 3. the idea would be to define the features I suggest as a basis for the mode’s font-lock features and complete them with the complement for those who are not matched by these definitions. Along with the idea that one font-face is defined per feature, the system becomes rich enough to be able to define both the schemes you already have and the one I propose.
For example I have operator-infix, operator-boolean, operator-hash with respective font-faces @ocaml-operator-infix, @ocaml-operator-boolean, @ocaml-operator-hash.
You could define the remaining operators in operator-other with a font face @ocaml-operator-other.
Now in your generic font-lock definitions, at levels that highlights operators you can activate all operator-* features and set their respective faces to @font-lock-operator-face and we recover the generic definitions. While to get my definitions I only activate all operator features except operator-other and map their fonts as I defined here.
But then again from a design perspective I don’t think the generic font-lock/level definitions are any good, which leads to 4. Where you simply define features/feature fonts that make sense for the OCaml language to which you map reasonably generic @font-lock-face by default. The features I propose could be a starting point and perhaps more could be added (or some of my definitions split) for people who want to make even more semantic distinctions. In turn these features could be activated at different levels.