Skip to content

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" name
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-u to the top of a struct or record, then C-M-d back into a sibling.
  • C-M-SPC (mark-sexp) -- select exactly one expression, then act on it. Combined with C-M-u first, it selects a whole bracketed group.
  • C-M-k (kill-sexp) -- delete an expression without fiddling with the closing delimiter. With mark-sexp and C-M-t it covers most cut/move edits.
  • C-M-a / C-M-e -- jump to the start/end of the current definition; great before mark-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.