ocaml-eglot is a lightweight
Emacs minor mode designed
to enhance the experience of writing OCaml code by leveraging the
Eglot
Language Server
Protocol (LSP)
client. This tool specifically caters to the OCaml ecosystem by
implementing canonical custom requests and commands exposed by the
ocaml-lsp-server.
Important
ocaml-eglot is an alternative mode to
merlin. It uses
ocaml-lsp-server (instead of
ocamlmerlin) as the language server. So, if you decide to use
ocaml-eglot, merlin is no longer needed.
ocaml-eglot bridges the gap between generic LSP support and the
specific needs of OCaml developers. Its tight coupling with Eglot
ensures a lightweight experience without sacrificing the advanced
features made available by ocaml-lsp-server. Its aim is to offer a
user experience as close as possible to that offered by the Emacs mode
Merlin.
Table of Contents
- OCaml-eglot
- Installation
- Features
- Browsing errors
- Type Enclosings
- Jump to definition/declaration
- Find identifier definition/declaration
- Jump to type definition of an expression
- Find occurrences
- Renaming
- Infer Interface
- Find Alternate file
- Get Documentation
- Construct Expression
- Destruct (or case-analysis)
- Source Browsing
- Search for values
- Opening up build artefacts
- Comparison of Merlin and OCaml-eglot commands
ocaml-eglot is distributed as a MELPA
package. ocaml-eglot is only an
interface between eglot (available out of the box since emacs >= 29.1) and Emacs, a major mode dedicated to OCaml editing must be
installed (e.g. caml-mode or
tuareg). Then, for example, you can use
use-package
to install ocaml-eglot. You will also need
https://ocaml.org/p/ocaml-lsp-server/latest in the current
opam switch. (If you are using
dune pkg to managed your dependencies, see Usage with dune pkg)
Here's an example with Tuareg already installed:
(use-package ocaml-eglot
:ensure t
:after tuareg
:hook
(tuareg-mode . ocaml-eglot)
(ocaml-eglot . eglot-ensure))ocaml-eglot is a minor mode which is grafted onto eglot (provided
by default in Emacs since version 29.1). Since eglot is itself
based on several popular packages in the Emacs ecosystem (such as
xref, flymake imenu etc.), you can configure it highly using
these modes. What's more, reading the Eglot
manual
is recommended for fine-tuning!
Eglot provides a hook to format the buffer on saving:
(use-package ocaml-eglot
:ensure t
:after tuareg
:hook
(tuareg-mode . ocaml-eglot)
- (ocaml-eglot . eglot-ensure))
+ (ocaml-eglot . eglot-ensure)
+ (ocaml-eglot . (lambda ()
+ (add-hook #'before-save-hook #'eglot-format nil t))))Eglot introduces a lot of visual noise (which can greatly alter the
user experience, especially when you're from merlin). One way of
reducing this visual obtrusion is to disable type annotations (eldoc)
and inlay-hints:
(use-package ocaml-eglot
:ensure t
:after tuareg
:hook
(tuareg-mode . ocaml-eglot)
- (ocaml-eglot . eglot-ensure))
+ (ocaml-eglot . eglot-ensure)
+ (eglot-managed-mode . (lambda ()
+ (eldoc-mode -1)
+ (eglot-inlay-hints-mode -1))))You can find more customisation options in the Eglot manual.
By default, flymake is configured to display error diagnostics on a
single line only, which, in OCaml, is rather irritating! Fortunately,
since version 1.4.0 (available via elpa), it is possible to
configure how diagnostics are displayed. Here is a minimal
configuration that displays the full diagnostic message in the echo
area:
(use-package flymake
:ensure t
:pin gnu
:config
(setq flymake-diagnostic-format-alist
'((t . (origin code message)))))Out of the box, eglot uses Flymake as a syntax checker. However, it
is possible to use flycheck, via the
flycheck-eglot
package. If you have flycheck-eglot installed, you can change your
configuration in this way:
+ (use-package flycheck-eglot
+ :ensure t
+ :after (flycheck eglot))
(use-package ocaml-eglot
:ensure t
:after tuareg
:hook
(tuareg-mode . ocaml-eglot)
- (ocaml-eglot . eglot-ensure))
+ (ocaml-eglot . eglot-ensure)
+ (eglot-managed-mode . (lambda () (flycheck-eglot-mode 1)))
+ :config
+ (setq ocaml-eglot-syntax-checker 'flycheck))You can find more information about flycheck-eglot on its
README.
OCaml-lsp-server can use .merlin as a configuration template (rather
than configuring via dune). This requires the
dot-merlin-reader
package to be installed in the switch being used, and then eglot can be
configured in this way:
(use-package ocaml-eglot
:ensure t
:after tuareg
:hook
(tuareg-mode . ocaml-eglot)
- (ocaml-eglot . eglot-ensure))
+ (ocaml-eglot . eglot-ensure)
+ :config
+ (with-eval-after-load 'eglot
+ (add-to-list 'eglot-server-programs
+ '(tuareg-mode . ("ocamllsp" "--fallback-read-dot-merlin")))))The definition/declaration search uses a dedicated backend
xref. By
default, the window behavior differs from merlin.el. If you are used
to the old behavior from merlin.el and want that, you can apply this
configuration:
(add-to-list
'display-buffer-alist
'((category . xref-jump)
(display-buffer-reuse-window
display-buffer-use-some-window)))Here is a recommended minimal configuration to take full advantage of
ocaml-eglot via tuareg (and a few additional modes):
;; Configure Flymake for verbose diagnostics
(use-package flymake
:ensure t
:pin gnu
:config
(setq flymake-diagnostic-format-alist
'((t . (origin code message)))))
;; Configure Tuareg
(use-package tuareg
:ensure t
:mode (("\\.ocamlinit\\'" . tuareg-mode)))
;; Configure OCaml-eglot
(use-package ocaml-eglot
:ensure t
:after tuareg
:hook
(tuareg-mode . ocaml-eglot)
(ocaml-eglot . eglot-ensure)
(ocaml-eglot . (lambda () (add-hook #'before-save-hook #'eglot-format nil t)))
:config
(setq ocaml-eglot-syntax-checker 'flymake))
;; Additional modes configuration
(use-package dune
:ensure t)
(use-package opam-switch-mode
:ensure t
:hook
(tuareg-mode . opam-switch-mode))
(use-package ocp-indent
:ensure t
:config
(add-hook 'ocaml-eglot-hook 'ocp-setup-indent))Using this configuration should provide a pleasant OCaml development experience in Emacs!
If you are using dune for package management on the latest
nightly bulid or version 3.21 or later, we
recommend using direnv via envrc.el, in conjunction with the
dune tools env command to get dune-managed dev tools in the environment.
Assuming you have installed and configured
envrc.el,
then for each dune-managed project:
-
Install
ocamllspin your dune workspace with$ dune tools install ocamllsp
-
Configure the
.envrcfile to putdune-managed dev tools in your path:$ echo 'eval $(dune tools env)' >> .envrc $ direnv allow
Here is the list of commands offered by ocaml-eglot, together with
their key binding (you'll find more detailed descriptions and
illustrations of each command in the next section.).
Important
This section only covers features specific to ocaml-eglot,
however, Eglot offers a large number of out of the box features,
via LSP (like completion, xref, flymake backend, imenu). To find out
more, please consult its user
manual.
| Command | Default Binding |
|---|---|
ocaml-eglot-error-next |
C-c C-x |
ocaml-eglot-error-prev |
|
ocaml-eglot-find-definition |
C-c C-l |
ocaml-eglot-find-identifier-definition |
|
ocaml-eglot-find-declaration |
C-c C-i |
ocaml-eglot-find-identifier-declaration |
|
ocaml-eglot-find-type-definition |
|
ocaml-eglot-find-type-definition-in-new-window |
|
ocaml-eglot-find-type-definition-in-current-window |
|
ocaml-eglot-infer-interface |
|
ocaml-eglot-alternate-file |
C-c C-a |
ocaml-eglot-hole-next |
|
ocaml-eglot-hole-prev |
|
ocaml-eglot-jump |
|
ocaml-eglot-phrase-next |
C-c C-p |
ocaml-eglot-phrase-prev |
C-c C-n |
ocaml-eglot-search |
|
ocaml-eglot-search-definition |
|
ocaml-eglot-search-declaration |
|
ocaml-eglot-document |
C-c C-d |
ocaml-eglot-document-identifier |
|
ocaml-eglot-construct |
C-c \ |
ocaml-eglot-destruct |
C-c | |
ocaml-eglot-type-expression |
|
ocaml-eglot-type-enclosing |
C-c C-t |
ocaml-eglot-occurences |
|
ocaml-eglot-rename |
Eglot relies on Flymake for error diagnosis. OCaml-eglot offers two functions for quickly navigating through errors:
ocaml-eglot-error-next(C-c C-x): jump to the next errorocaml-eglot-error-prev(C-c C-c): jump to the previous error
In ocaml-eglot one can display the type of the expression below the cursor and
navigate the enclosing nodes while increasing or decreasing verbosity:
ocaml-eglot-type-enclosing(C-c C-t): display the type of the selection and start a "type enclosing" session.
During a "type enclosing" session the following commands are available:
ocaml-eglot-type-enclosing-increase-verbosity(C-c C-t or C-→): to increase the verbosity of the type observedocaml-eglot-type-enclosing-decrease-verbosity(C-←): to decrease verbosity of the type observedocaml-eglot-type-enclosing-grow(C-↑): to grow the expressionocaml-eglot-type-enclosing-shrink(C-↓): to shrink the expressionocaml-eglot-type-enclosing-copy(C-w): to copy the type expression to the kill-ring (clipboard)
You can also enter an expression in the mini-buffer for which you want to display the type:
ocaml-eglot-type-expression(C-u C-c C-t)
ocaml-eglot provides a shortcut to quickly jump to the definition or
declaration of an identifier:
-
ocaml-eglot-find-definition(C-c C-l): jump to definition (the implementation) -
ocaml-eglot-find-declaration(C-c C-i): jump to declaration (the signature)
In LSP, the terminology between definition and declaration can
be confusing:
definition: corresponds to the implementationdeclaration: corresponds to the signature
It is also possible to directly enter the name of an identifier (definition or declaration) using the following commands:
ocaml-eglot-find-identifier-definitionocaml-eglot-find-identifier-declaration
You can also jump to the type definition of the expression at point.
Auxiliary functions for controlling the placement of a result are provided:
ocaml-eglot-find-type-definition-in-new-windowocaml-eglot-find-type-definition-in-current-window
ocaml-eglot-occurences returns all occurrences of the
identifier under the cursor. To find all occurrences in the entire
project, it requires an index. This index can be created by running
dune build @ocaml-index --watch when developing. Requires OCaml
5.2 and Dune 3.16.0. See the
announcement.
Use ocaml-eglot-rename to rename the symbol under the
cursor. Starting with OCaml 5.3 it is possible to rename a symbol
across multiple files after building an up-to-date index with dune build @ocaml-index.
Used to infer the type of an interface file. If the buffer is not empty, a prompt will ask for confirmation to overwrite the buffer contents:
ocaml-eglot-infer-interface: infer the interface for the current implementation file
OCaml-eglot allows you to quickly switch from the implementation file
to the interface file and vice versa. If the interface file does not
exist, a prompt can be used to generate it (using type inference,
based on ocaml-eglot-infer-interface):
ocaml-eglot-alternate-file(C-c C-a): switch from the implementation file to the interface file and vice versa
Although the Hover primitive in the LSP protocol can be used to
conveniently display the documentation of a value, it is also possible to query for it
explicitly:
ocaml-eglot-document(C-c C-d): documents the expression below the cursor.ocaml-eglot-document-identifier: enables you to enter an identifier (present in the environment) and return its documentation.
Enables you to navigate between typed-holes (_) in a document and
interactively substitute them:
ocaml-eglot-hole-next: jump to the next holeocaml-eglot-hole-prev: jump to the previous holeocaml-eglot-construct: open up a list of valid substitutions to fill the hole
If the ocaml-eglot-construct (C-c \) command
is prefixed by an argument, i.e.: C-u M-x ocaml-eglot-construct, the
command will also search for valid candidates in the current
environment:
Destruct, ocaml-eglot-destruct (C-c |) is a
powerful feature that allows one to generate and manipulate pattern
matching expressions. It behaves differently depending on the cursor’s
context:
- on an expression: it replaces it by a pattern matching over it’s constructors
- on a wildcard pattern: it will refine it if possible
- on a pattern of a non-exhaustive matching: it will make the pattern matching exhaustive by adding missing cases
OCaml-eglot allows you to navigate semantically in a buffer, passing
from an expression to the parent let, the parent module, the
parent fun and the parent match expression. It is also possible to
navigate between pattern matching cases:
ocaml-eglot-jump: jumps to the referenced expressionocaml-eglot-phrase-prev(C-c C-p): jump to the beginning of the previous phraseocaml-eglot-phrase-next(C-c C-n): jump to the beginning of the next phrase
Search for values using a by polarity query or a type expression. A
polarity query prefixes the function arguments with - and the return
with +. For example, to search for a function of this type: int -> string. Search for -int +string. Searching by polarity does not
support type parameters. A search by type (modulo isomorphisms) uses a
query closer to what you would write to describe a type. For example,
to find the function int_of_string_opt, search for string -> int option:
ocaml-eglot-searchsearches for a value by its type or polarity to included in the current buffer (the search type is defined by the input query)
Alternatively, you can search for a definition or declaration:
-
ocaml-eglot-search-definitionsearches for a value definition by its type or polarity -
ocaml-eglot-search-declarationsearches for a value declaration by its type or polarity
Used to hook the opening of a compilation artefact with
ocamlobjinfo:
merlin |
ocaml-eglot |
Note |
|---|---|---|
merlin-error-check |
— | The functionality is supported by eglot diagnostics (via LSP). |
merlin-error-next |
ocaml-eglot-error-next |
|
merlin-error-prev |
ocaml-eglot-error-prev |
|
merlin-type-enclosing |
ocaml-eglot-type-enclosing |
|
merlin-type-expr |
ocaml-eglot-type-expression |
|
merlin-locate |
ocaml-eglot-find-declaration |
|
| — | ocaml-eglot-find-definition |
Available in Merlin by configuration |
| ❌ | ocaml-eglot-find-type-definition |
|
merlin-locate-ident |
ocaml-eglot-find-identifier-definition, ocaml-eglot-find-identifier-declaration |
|
merlin-occurences |
ocaml-eglot-occurences |
|
merlin-project-occurences |
— | Handle by ocaml-eglot-occurences (if ocaml-version >= 5.2 and need an index, dune build @ocaml-index) |
merlin-iedit-occurrences |
ocaml-eglot-rename |
|
merlin-document |
ocaml-eglot-document |
also ocaml-eglot-document-identifier |
merlin-phrase-next |
ocaml-eglot-phrase-next |
|
merlin-phrase-prev |
ocaml-eglot-phrase-prev |
|
merlin-switch-to-ml |
ocaml-eglot-alternate-file |
|
merlin-switch-to-mli |
ocaml-eglot-alternate-file |
|
| ❌ | ocaml-eglot-infer-interface |
It was supported by Tuareg (and a bit ad-hoc) |
merlin-jump |
ocaml-eglot-jump |
|
merlin-destruct |
ocaml-eglot-destruct |
|
merlin-construct |
ocaml-eglot-construct |
|
merlin-next-hole |
ocaml-eglot-hole-next |
|
merlin-previous-hole |
ocaml-eglot-hole-prev |
|
merlin-toggle-view-errors |
— | An eglot configuration |