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
.
Warning
ocaml-eglot
is experimental and at an early stage
of development. While we're very happy to collect user feedback.
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.
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
switch.
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.
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)
+ (ocaml-eglot . (lambda () (flycheck-eglot-mode 1)))
+ :config
+ (setq ocaml-eglot-syntax-checker 'flycheck))
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")))))
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)
The default calculation for the window containing the jump result is smart: if the target is in the same file, the command uses the same window; if the target is in another file, the command opens a new window. Auxiliary functions for controlling the placement of a result are provided:
ocaml-eglot-find-definition-in-new-window
ocaml-eglot-find-declaration-in-new-window
ocaml-eglot-find-definition-in-current-window
ocaml-eglot-find-declaration-in-current-window
The default behavior can also be configured using the
ocaml-eglot-open-window-strategy
variable.
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-definition
ocaml-eglot-find-identifier-declaration
And as with commands that locate the identifier below the cursor, there are variations for controlling the window to jump to:
ocaml-eglot-find-identifier-definition-in-new-window
ocaml-eglot-find-identifier-definition-in-current-window
ocaml-eglot-find-identifier-declaration-in-new-window
ocaml-eglot-find-identifier-declaration-in-current-window
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-window
ocaml-eglot-find-type-definition-in-current-window
ocaml-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
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 expression
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-search
searches 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-definition
searches for a value definition by its type or polarity -
ocaml-eglot-search-declaration
searches for a value declaration by its type or polarity
And as with commands that locate the identifier below the cursor, there are variations for controlling the window to jump to:
ocaml-eglot-search-definition-in-current-window
ocaml-eglot-search-definition-in-new-window
ocaml-eglot-search-declaration-in-current-window
ocaml-eglot-search-declaration-in-new-window
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 |