Configuration¶
Font-locking¶
neocaml provides 4 levels of font-locking, as is the standard for Tree-sitter modes. The default level in Emacs is 3, and you can change it like this:
;; this font-locks everything neocaml supports
(setq treesit-font-lock-level 4)
The font-lock features available at each level are:
Level 1 (minimal -- comments and definitions):
comment-- comments and doc comments:(* ... *),(** ... *)definition-- let/val/type/method bindings and value patterns:let f x = ...,val x : int
Level 2 (add keywords, strings, types):
keyword-- language keywords and arrows:let,match,fun,if,->, ...string-- strings and characters:"hello",'a',{|raw|}type-- type names, modules, constructors, type punctuation:int,List,Some,'a,->/*in type expressions
Level 3 (default -- full-blown fontification):
attribute-- PPX attributes and extension nodes:[@inline],[@@deriving show],[%test ...]builtin-- builtin identifiers, exceptions, and types:print_endline,Not_found,int,stringconstant-- boolean and unit literals:true,false,()escape-sequence-- escape sequences and format specs in strings:\n,\t,%d,%snumber-- numeric literals:42,3.14,0xFF,0b1010
Level 4 (maximum detail):
Level 4 can get very noisy -- rather than enabling it wholesale, consider cherry-picking individual features from it (see Selecting features below).
operator-- operators:+,::,<-,#(method invocation)bracket-- brackets:(),[],{},[| |]delimiter-- delimiters:,,.,;,:,;;variable-- value names in expressions:x,yinx + yproperty-- record field access:nameinperson.namelabel-- labeled arguments:xin~x:1,yin?y:2function-- function calls:finf x,mapinList.map, pipe targetsx |> f
Selecting features¶
You don't have to use the level system. If you want fine-grained control over
what gets highlighted, you can cherry-pick individual features using
treesit-font-lock-recompute-features:
(defun my-neocaml-font-lock-setup ()
(treesit-font-lock-recompute-features
;; enable these features
'(comment definition keyword string type
attribute builtin constant escape-sequence number
operator variable property label)
;; disable these features
'(bracket delimiter function)))
(add-hook 'neocaml-base-mode-hook #'my-neocaml-font-lock-setup)
This gives you operators and variables without bracket and delimiter noise, for
example. You can also call M-x treesit-font-lock-recompute-features
interactively to toggle features in the current buffer.
Customizing faces¶
The faces used are standard font-lock-*-face faces, so any theme applies
automatically. If you want to tweak how specific syntactic elements look, you
have two options:
Buffer-local remapping (recommended) -- changes apply only to neocaml buffers, leaving other modes unaffected:
;; Use a custom color for type names in OCaml buffers only
(add-hook 'neocaml-base-mode-hook
(lambda ()
(face-remap-add-relative 'font-lock-type-face
:foreground "DarkSeaGreen4")))
You can remap multiple faces in the same hook. Each
face-remap-add-relative call stacks on top of the face's current
definition, so theme settings are preserved as a base. See
Face Remapping
in the Emacs Lisp manual for details.
Global customization -- changes apply everywhere the face is used:
;; Change type names globally (affects all modes)
(custom-set-faces
'(font-lock-type-face ((t (:foreground "DarkSeaGreen4")))))
This is simpler but less precise -- since tree-sitter modes share the same
font-lock-*-face faces, a global change will affect every tree-sitter mode
(and traditional modes) that uses that face.
Adding custom font-lock rules¶
For distinctions that neocaml doesn't make by default (e.g. highlighting block keywords differently from control flow keywords), you can layer additional Tree-sitter font-lock rules via a hook:
(defface my-ocaml-block-keyword-face
'((t :inherit font-lock-keyword-face :weight bold))
"Face for OCaml block-delimiting keywords.")
(defun my-neocaml-block-keywords ()
(setq treesit-font-lock-settings
(append treesit-font-lock-settings
(treesit-font-lock-rules
:language (treesit-parser-language
(car (treesit-parser-list)))
:override t
:feature 'keyword
'(["begin" "end" "struct" "sig" "object"
"do" "done" "fun" "function"]
@my-ocaml-block-keyword-face))))
(treesit-font-lock-recompute-features))
(add-hook 'neocaml-base-mode-hook #'my-neocaml-block-keywords)
The rules use standard Tree-sitter query syntax with :override t to take
precedence over neocaml's built-in rules. You can target any node type the
grammar produces -- use M-x treesit-explore-mode to inspect the syntax tree
and find the right node types to match.
Prettify Symbols¶
You can "prettify" certain symbols (see neocaml-prettify-symbols-alist) by
enabling prettify-symbols-mode via a hook:
;; Enable for both .ml and .mli files at once
(add-hook 'neocaml-base-mode-hook #'prettify-symbols-mode)
Indentation¶
When it comes to indentation you've got several options:
- Using the built-in Tree-sitter indentation
- Supports
letbindings,let...inchains,match/tryexpressions,if/then/else, variant and record types, modules, signatures, loops,fun/functionexpressions, lists, arrays, sequences, and more - It still needs some work, so it might not always behave the way you'd like it to
- Supports
- Use the built-in Emacs function
indent-relativethat simply indents the next line relative to the previous line and allows you manually indent/outdent further. Very simple, but kind of bullet-proof. - Use the indent function of ocp-indent (this requires
ocp-indent.eland theocp-indentbinary) - Use the indent function of tuareg.
You can quickly toggle between tree-sitter indentation and indent-relative
using M-x neocaml-cycle-indent-function (also available from the OCaml menu).
This is handy when the tree-sitter indentation doesn't do what you want for a
particular piece of code.
You can also permanently change the indentation function like this:
;; Use indent-relative (simple, but bullet-proof)
(defun my-neocaml-mode-setup ()
(setq-local indent-line-function 'indent-relative))
(add-hook 'neocaml-base-mode-hook 'my-neocaml-mode-setup)
To use ocp-indent instead:
(defun my-neocaml-mode-setup ()
(setq-local indent-line-function #'ocp-indent-line)
(setq-local indent-region-function #'ocp-indent-region))
(add-hook 'neocaml-base-mode-hook #'my-neocaml-mode-setup)
To use tuareg's SMIE-based indentation:
(defun my-neocaml-mode-setup ()
(setq-local indent-line-function #'tuareg-indent-line))
(add-hook 'neocaml-base-mode-hook #'my-neocaml-mode-setup)
Comments¶
OCaml uses block comments (* ... *) exclusively (no line comments), which
requires some mode-level configuration for Emacs comment commands to work well.
neocaml sets all the necessary variables internally, so everything works out of
the box:
M-;(comment-dwim) -- comments/uncomments regions, inserts inline comments, etc. Works as expected with OCaml's(* ... *)delimiters.M-j(default-indent-new-line) -- inside a comment, inserts a newline and indents the continuation line to align with the comment body text. For example, pressingM-jinside(** doc textproduces a new line indented to column 4 (after(**), keeping the comment open rather than closing and reopening it.M-q(fill-paragraph) -- refills the current comment, wrapping text atfill-columnwith continuation lines properly indented to the body column.
These commands respect both regular comments ((* ... *)) and doc comments
((** ... *)), and work correctly for indented comments.
Code Folding¶
On Emacs 30+, outline-minor-mode works out of the box with neocaml -- it
automatically picks up definition headings from the tree-sitter imenu settings.
Enable it via a hook:
(add-hook 'neocaml-base-mode-hook #'outline-minor-mode)
For tree-sitter-aware code folding (fold any node, not just top-level definitions), see treesit-fold.
Structural Selection¶
expreg provides expand-region-style selection that leverages tree-sitter for language-aware expansion. It walks the AST to grow/shrink the selection to the next syntactic node, which works particularly well with OCaml's deeply nested expressions (match arms, let bindings, module structures, etc.):
(use-package expreg
:ensure t
:bind (("C-=" . expreg-expand)
("C--" . expreg-contract)))