Toplevel (REPL) Integration¶
neocaml provides integration with the OCaml toplevel (REPL), allowing you to evaluate OCaml code directly from your source buffer. The REPL features:
- Tree-sitter syntax highlighting for input (via
comint-fontify-input-mode) - Persistent input history across sessions
- Clickable error locations (via
compilation-shell-minor-mode) - Quick switching between source and REPL with
C-c C-z - Support for both the standard
ocamltoplevel and utop
To get started, enable neocaml-repl-minor-mode (which adds REPL
keybindings to your OCaml buffers), then press C-c C-z to start
a REPL session:
;; Enable for both .ml and .mli files at once
(add-hook 'neocaml-base-mode-hook #'neocaml-repl-minor-mode)
If you're using use-package you'd probably do something like:
(use-package neocaml
:ensure t
:config
(add-hook 'neocaml-base-mode-hook #'neocaml-repl-minor-mode)
;; other config options...
)
The following keybindings are available when neocaml-repl-minor-mode is active:
Note
C-c C-c is bound to compile in the base mode. When
neocaml-repl-minor-mode is enabled, it is rebound to
neocaml-repl-send-definition.
| Keybinding | Command | Description |
|---|---|---|
C-c C-z |
neocaml-repl-switch-to-repl |
Start OCaml REPL or switch to it if already running |
C-c C-c |
neocaml-repl-send-definition |
Send the current definition to the REPL |
C-c C-r |
neocaml-repl-send-region |
Send the selected region to the REPL |
C-c C-b |
neocaml-repl-send-buffer |
Send the entire buffer to the REPL |
C-c C-l |
neocaml-repl-load-file |
Load the current file into the REPL via #use |
C-c C-p |
neocaml-repl-send-phrase |
Send the current phrase (code up to next ;;) to the REPL |
C-c C-n |
neocaml-repl-send-phrase-and-step |
Send the current phrase, then move to the next one |
C-c C-i |
neocaml-repl-interrupt |
Interrupt the current evaluation in the REPL |
C-c C-k |
neocaml-repl-clear-buffer |
Clear the REPL buffer |
Tip
In the REPL buffer itself, C-c C-z switches back to the source
buffer you came from, so you can quickly bounce between source and REPL.
C-c C-n (neocaml-repl-send-phrase-and-step) is handy for working
through a buffer: it sends the phrase at point and then advances to the
start of the next one, so you can evaluate a file phrase by phrase with
repeated presses.
M-x neocaml-repl-require loads a findlib package into the REPL via
#require (the toplevel needs findlib, e.g. via topfind or utop).
M-x neocaml-repl-restart kills the running toplevel and starts a fresh
one in the same buffer. These, along with the commands above, are also
available from the "OCaml REPL" menu.
The REPL buffer also enables compilation-shell-minor-mode, so
error locations in REPL output are clickable and navigable with
next-error / previous-error.
Per-project REPLs¶
Each project gets its own dedicated REPL. When you start or send to a
REPL from a source file, neocaml derives the project (via project.el,
falling back to the directory containing a dune-project) and routes to
a per-project buffer named after neocaml-repl-buffer-name plus the
project, e.g. *OCaml: myproject*. Files outside any project share the
base *OCaml* buffer.
This means you can have several OCaml projects open at once, each with
its own toplevel, and the send commands always reach the right one. It
also means M-x neocaml-dune-utop (see below) and the regular send
commands target the same per-project REPL.
Input Syntax Highlighting¶
By default, code you type in the REPL is fontified using tree-sitter
via comint-fontify-input-mode, giving you the same syntax highlighting
as in regular .ml buffers. REPL output (errors, warnings, values)
keeps its own highlighting.
To disable this and use only basic REPL font-lock:
(setq neocaml-repl-fontify-input nil)
Tip
You can also get language-aware indentation for REPL input by
leveraging comint-indent-input-line-default, which delegates
indentation to the same indirect buffer used for font-lock:
(add-hook 'neocaml-repl-mode-hook
(lambda ()
(setq-local indent-line-function
#'comint-indent-input-line-default)
(setq-local indent-region-function
#'comint-indent-input-region-default)))
This is experimental -- tree-sitter parsers see the entire comint buffer (prompts, output, and input), so indentation may be approximate for complex multi-line input.
Configuration¶
You can customize the OCaml REPL integration with the following variables:
;; Add extra command-line arguments to the default OCaml toplevel.
;; The default is '("-nopromptcont"), which disables continuation
;; prompts for cleaner multi-line input in comint. Make sure to
;; preserve it when adding your own flags:
(setq neocaml-repl-program-args '("-nopromptcont" "-short-paths" "-color=never"))
;; Change the base REPL buffer name (default: "*OCaml*"). Per-project
;; REPLs derive their name from this, e.g. "*OCaml: myproject*".
(setq neocaml-repl-buffer-name "*OCaml-REPL*")
REPL input history is persisted across sessions automatically.
You can configure this with neocaml-repl-history-file (set to
nil to disable) and neocaml-repl-history-size (default 1000).
Choosing a toplevel¶
neocaml-repl-flavor selects which toplevel to launch:
ocaml(the default) - the standard ocaml toplevel, usingneocaml-repl-program-nameandneocaml-repl-program-args.utop- utop, an improved toplevel with auto-completion, syntax highlighting, and a rich history.dune-utop-dune utopfor the current project, launched viaM-x neocaml-dune-utop, so the project's libraries are available.
Set it globally or per project via .dir-locals.el; the REPL reads it
when it starts (restart an existing one with M-x neocaml-repl-restart
to apply a change). The active flavor is shown in the REPL's mode line,
e.g. OCaml-REPL[utop].
(setq neocaml-repl-flavor 'utop)
Note
Don't pass utop's -emacs flag here. It activates a structured
protocol that the comint-based REPL doesn't speak; plain utop
works fine with neocaml-repl. If you want that protocol, use the
dedicated neocaml-utop backend
described below.
Note
If Emacs can't find utop or ocaml, your shell PATH may not be
inherited. See
Troubleshooting
for the fix.
The utop protocol backend¶
neocaml-utop is an alternative toplevel integration that drives utop
through its native editor protocol (utop -emacs) instead of treating
it as a plain comint stream. It's a separate module from neocaml-repl;
use whichever you prefer. neocaml-repl remains the right choice for the
standard ocaml toplevel, which has no protocol of its own.
In protocol mode utop does no rendering itself and reports structured
information that a raw stream can't, which lets neocaml-utop:
- separate output, results, errors, and warnings without guessing from the text;
- underline the exact sub-expression a parse or type error points at,
back in your source buffer (see the
neocaml-utop-error-faceface), rather than just the line; - answer completion from utop's own engine, wired into
completion-at-pointin the transcript.
The transcript is a comint derivative, so input editing, history (with
optional persistence via neocaml-utop-history-file), and read-only
prompts work as usual.
Enable the source-buffer minor mode to get the keybindings, then press
C-c C-z to start a session:
(add-hook 'neocaml-base-mode-hook #'neocaml-utop-minor-mode)
The keybindings mirror neocaml-repl-minor-mode:
| Keybinding | Command | Description |
|---|---|---|
C-c C-z |
neocaml-utop-switch-to-utop |
Start utop or switch to a running session |
C-c C-c |
neocaml-utop-send-definition |
Send the definition at point |
C-c C-r |
neocaml-utop-send-region |
Send the active region |
C-c C-b |
neocaml-utop-send-buffer |
Send the whole buffer |
C-c C-p |
neocaml-utop-send-phrase |
Send the phrase at point (code up to ;;) |
C-c C-n |
neocaml-utop-send-phrase-and-step |
Send the phrase, then move to the next one |
C-c C-l |
neocaml-utop-load-file |
Load the current file via #use |
C-c C-i |
neocaml-utop-interrupt |
Interrupt the utop process |
C-c C-k |
neocaml-utop-clear-buffer |
Clear the transcript buffer |
Choose between plain utop and dune utop with neocaml-utop-flavor
(utop or dune-utop), set globally or per project via .dir-locals.el;
dune-utop makes the project's own libraries available in the toplevel.
M-x neocaml-utop-require loads a findlib package via #require.
Sending a phrase or definition from a source buffer also shows its
result (or error) two ways, SLIME/CIDER style, while the full output
still goes to the transcript: echoed in the minibuffer, and inline in
the source buffer as a => ... overlay at the end of the evaluated form
(the overlay clears on the next evaluation or when you edit the code).
Toggle either independently:
(setq neocaml-utop-echo-eval-result nil) ; no minibuffer echo
(setq neocaml-utop-inline-eval-result nil) ; no inline => overlay
Region and buffer sends evaluate every ;;-separated phrase in the
selection, not just the first. In the transcript itself, RET submits
the input when it forms a complete phrase (ends with ;;, or is a #
directive) and otherwise inserts a newline, so you can type multi-line
phrases naturally; RET on a blank line submits regardless. Input you
type is fontified with tree-sitter (via comint-fontify-input-mode),
which you can turn off with neocaml-utop-fontify-input. When an
evaluation fails, the underlined span in the source carries the error
message as its tooltip.
Note
neocaml-utop needs utop (it relies on the -emacs protocol), so it
doesn't apply to the standard ocaml toplevel. Pick one backend per
buffer: enable either neocaml-repl-minor-mode or
neocaml-utop-minor-mode, since they share the same keybindings.