Code Navigation¶
neocaml uses tree-sitter to power all structural navigation commands. These are standard Emacs keybindings, but backed by the AST rather than heuristics:
| Keybinding | Command | Description |
|---|---|---|
C-M-a |
beginning-of-defun |
Move to the beginning of the current definition |
C-M-e |
end-of-defun |
Move to the end of the current definition |
C-M-f |
forward-sexp |
Move forward over a balanced expression |
C-M-b |
backward-sexp |
Move backward over a balanced expression |
M-a |
backward-sentence |
Move to the beginning of the current statement (Emacs 30+) |
M-e |
forward-sentence |
Move to the end of the current statement (Emacs 30+) |
C-M-u |
neocaml-backward-up-list |
Move out to the enclosing struct/sig/object, record, array, or paren group |
C-M-d |
down-list |
Move into a parenthesized/bracketed expression (Emacs 30+) |
C-M-n |
forward-list |
Move forward over a parenthesized/bracketed group (Emacs 30+) |
C-M-p |
backward-list |
Move backward over a parenthesized/bracketed group (Emacs 30+) |
C-c C-a |
ff-find-other-file |
Toggle between .ml and .mli (also works for .mll, .mly, .eliom/.eliomi) |
C-c 4 C-a |
ff-find-other-file-other-window |
Same, but in another window |
Note
The built-in backward-up-list only understands syntax-table parens, so on
Emacs 29/30 it can't walk out of a module Foo = struct ... end block.
neocaml ships neocaml-backward-up-list as a tree-sitter-aware replacement
bound to C-M-u. On Emacs 31+ the built-in already consults
treesit-thing-settings, so the command simply delegates to it.
"Definitions" include let bindings, type definitions, module bindings, class
definitions, exceptions, and externals. "Statements" cover the same plus
open, include, and expression items -- essentially any top-level or
block-level construct.
Worked examples¶
In every example below, █ marks the position of point (the cursor). The first
block is the state before the command, the second the state after.
Moving over expressions¶
forward-sexp (C-M-f) moves over the largest balanced expression that
starts at point. At the head of a function application that's the whole call,
not just the function name:
let r = █List.map (fun x -> x + 1) numbers
let r = List.map (fun x -> x + 1) numbers█
Inside a collection (list, array, tuple, record) it steps over one element at a time, which is what you usually want when editing the elements:
let xs = [ █aa; bb; cc ]
let xs = [ aa█; bb; cc ]
backward-sexp (C-M-b) is the mirror image: from the end of the application
above it lands back before List.map.
Diving in and climbing out¶
down-list (C-M-d) moves point just inside the next bracketed group -- handy
for jumping into a record or argument list before editing its contents:
let c = █{ name = "t"; size = 3 }
let c = {█ name = "t"; size = 3 }
neocaml-backward-up-list (C-M-u) does the opposite, and understands
keyword-delimited blocks, not just parens. From inside a struct body it climbs
out to the enclosing module:
module M = struct
let x = 1
let y = █2
end
module M = █struct
let x = 1
let y = 2
end
It works the same way for collections -- from inside a list it lands just before the opening bracket:
let xs = [ 1; █2; 3 ]
let xs = █[ 1; 2; 3 ]
Jumping between definitions¶
beginning-of-defun (C-M-a) and end-of-defun (C-M-e) move by whole
definitions. From anywhere inside a binding, C-M-a lands on its first column:
let greet name =
Printf.printf "hi %s" na█me
█let greet name =
Printf.printf "hi %s" name
Editing with structure¶
The same tree-sitter backing makes the standard structural editing commands reliable. They aren't OCaml-specific, but they stop misbehaving once the AST, rather than the syntax table, decides where an expression ends.
| Keybinding | Command | Description |
|---|---|---|
C-M-h |
mark-defun |
Mark the current definition |
C-M-SPC |
mark-sexp |
Mark the next balanced expression |
M-x neocaml-mark-sentence |
neocaml-mark-sentence |
Mark the current statement (Emacs 30+) |
C-M-k |
kill-sexp |
Kill the balanced expression after point |
C-M-<backspace> |
backward-kill-sexp |
Kill the balanced expression before point |
C-M-t |
transpose-sexps |
Swap the two expressions around point |
M-x delete-pair |
delete-pair |
Remove a matched delimiter pair (unwrap) |
M-x raise-sexp |
raise-sexp |
Replace the enclosing expression with the one at point |
kill-sexp (C-M-k) removes the expression ahead of point in one stroke:
let xs = █(List.rev ys) @@ tl
let xs = █ @@ tl
transpose-sexps (C-M-t) swaps the expressions on either side of point -- a
quick way to reorder function arguments or operands:
let d = sub minuend █subtrahend
let d = sub subtrahend minuend█
delete-pair unwraps a bracketed group. Put point on the opening delimiter and
it deletes both it and its match (neocaml's tree-sitter-aware forward-sexp
finds the correct closing delimiter, even across keyword blocks):
let area = pi *. █(r *. r)
let area = pi *. █r *. r
raise-sexp replaces the enclosing expression with the sub-expression at
point. It's the fastest way to strip a wrapper -- here, dropping a Some around
a call:
let result = process (Some █(find_opt key tbl))
let result = process █(find_opt key tbl)
Note
raise-sexp needs an enclosing bracketed group to raise into. Invoked at the
top level of a binding -- where there's nothing surrounding the expression --
it errors with "Unbalanced parentheses". Reach for it inside a call, list, or
parenthesized expression.
Tip
These commands stay correct around OCaml's trickier lexemes. Character
literals like '"', '(', and ')', and the contents of {|...|} /
{id|...|id} quoted strings, used to confuse the syntax table (a "
opening a string, a ( opening a list); neocaml now propertizes them so
sexp motion, delete-pair, and electric-pair-mode behave.
The commands worth memorizing¶
There are a lot of structural commands; in day-to-day OCaml editing you'll keep coming back to a small subset:
C-M-f/C-M-b-- step over an expression. The everyday workhorses for moving across arguments, list elements, and whole sub-expressions.C-M-u/C-M-d-- climb out of / dive into the enclosing block. Pair them to reposition quickly:C-M-uto the top of astructor record, thenC-M-dback into a sibling.C-M-SPC(mark-sexp) -- select exactly one expression, then act on it. Combined withC-M-ufirst, it selects a whole bracketed group.C-M-k(kill-sexp) -- delete an expression without fiddling with the closing delimiter. Withmark-sexpandC-M-tit covers most cut/move edits.C-M-a/C-M-e-- jump to the start/end of the current definition; great beforemark-defun(C-M-h) to grab the whole thing.
Tip
delete-pair and raise-sexp have no default key bindings but are some of
the handiest refactoring commands. If you unwrap expressions often, bind them
in neocaml-mode-map, for example:
(with-eval-after-load 'neocaml
(define-key neocaml-mode-map (kbd "C-c r") #'raise-sexp)
(define-key neocaml-mode-map (kbd "C-c d") #'delete-pair))
All of these commands are also available from the OCaml menu.