This year I started consolidating all the major modes by using lsp-mode and apheleia for all the programming language I use. The switch from OCaml’s merlin-mode was surprisingly painless.

Let’s start with the default major modes that provide syntax highlighting and a few other things. I have tuareg for OCaml, dune-format to format dune files and reason-mode for ReasonML.

(use-package tuareg
  :ensure t
  :custom
  (tuareg-opam-insinuate t)
  :config)

(use-package dune-format
  :ensure t)

(use-package reason-mode
  :ensure t)

My lsp-mode setup is the same for every language so I get a consistent experience. I am using ocamllsp as the LSP server implementation, this is the one that the official VS Code extension uses as well.

(defun my-lsp-fix-buffer ()
  "Formats buffer and organizes imports."
  (interactive)
  (lsp-organize-imports)
  (lsp-format-buffer))

(use-package lsp-mode
  :ensure t
  :after flycheck
  :commands lsp
  :bind (("C-c l n" . flycheck-next-error)
         ("C-c l d" . lsp-find-definition)
         ("C-c l r" . lsp-find-references)
         ("C-c l h" . lsp-describe-thing-at-point)
         ("C-c l i" . lsp-find-implementation)
         ("C-c l R" . lsp-rename)
         ("C-c l o" . my-lsp-fix-buffer))
  :hook ((tuareg-mode . lsp)
         (caml-mode . lsp)
         (reason-mode . lsp)
         (before-save . lsp-organize-imports))
  :custom
  (lsp-lens-enable t)
  (lsp-log-io nil)
  (lsp-headerline-breadcrumb-enable nil)
  :config
  (lsp-enable-which-key-integration t)
  (lsp-register-client
   (make-lsp-client
    :new-connection (lsp-stdio-connection
                     '("opam" "exec" "--" "ocamllsp"))
    :major-modes '(caml-mode tuareg-mode reason-mode)
    :server-id 'ocamllsp)))

For my formatting needs I use the outstanding apheleia package. This package is great, especially for slow formatters. With ocamlformat you won’t get all the benefits because it’s quite fast.

Apheleia runs the formatting on a buffer in the background and patches the current buffer asynchronously. The cursor remains at the same location in the code, which makes it easy to keep track of what is going on.

(use-package apheleia
  :ensure t
  :hook
  (caml-mode . apheleia-mode)
  (tuareg-mode . apheleia-mode)
  (reason-mode . apheleia-mode)
  :config
  (setf (alist-get 'ocamlformat apheleia-formatters)
        '("opam" "exec" "--" "ocamlformat" "--impl" "-"))
  (setf (alist-get 'refmt apheleia-formatters)
        '("opam" "exec" "--" "refmt"))
  (setf (alist-get 'tuareg-mode apheleia-mode-alist)
        '(ocamlformat))
  (setf (alist-get 'reason-mode apheleia-mode-alist)
        '(refmt)))

I don’t use lsp-ui, lsp-mode on its own does the trick combined with apheleia.