How to start with Emacs to work on OCaml codebases

after using vim for a long time I want to give Emacs a try – to work on OCaml codebases.

I find the GitHub - ocaml/tuareg: Emacs OCaml mode README targeted at people who already know emacs, so I am lost.

Also the $ opam install tuareg output works fine but puzzles me a bit, because it refers a fixed switch while I would expect emacs to use the current switch:

=> If you have not yet done so, please add the following line to ~/.emacs.d/init.el or ~/.emacs:
       (load "/Users/mro/.opam/4.10.2/share/emacs/site-lisp/tuareg-site-file")
=> You should consider installing "merlin" (completion, displaying types,...)
   or "caml-mode" (displaying types).  See https://github.com/ocaml/tuareg
   for customization tips.

And installing “merlin” is a bit vague – could it point to a helpful URL of merlin?

1 Like

See GitHub - ocaml/merlin: Context sensitive completion for OCaml in Vim and Emacs .

Have a look at OCaml - Merlin | An editor service that provides advanced IDE features for OCaml. I’m just discovering.

1 Like

Since you’re coming from vim, I would recommend starting with Doom Emacs. It’s a configuration framework that comes with evil-mode, out-of-the-box support for OCaml (and many other languages) and nice non-archaic defaults. You can choose merlin or ocaml-lsp, and lsp-mode or eglot, with a single-line configuration change.

2 Likes

This is how I setup my Emacs (using caml-mode because I don’t use Tuareg).

Begin by installing merlin, ocp-indent, caml-mode, etc, using opam:

opam install merlin ocp-indent caml-mode

Then add the following bits of code to your Emacs init file.
The first bit of code will add the emacs/site-lisp directory of the current opam switch to the Emacs load path:

(let ((opam-share (ignore-errors (car (process-lines "opam" "var" "share")))))
  (when (and opam-share (file-directory-p opam-share))
    (add-to-list 'load-path (expand-file-name "emacs/site-lisp" opam-share))))

Load the respective packages in your Emacs init file:

(require 'caml)
(require 'caml-font)
(require 'merlin)
(require 'ocp-indent)

And finally activate the relevant modes automatically when editing Caml code:

;; automatically activate caml-mode when eidting .ml, .mli, .mly, .mll
(add-to-list 'auto-mode-alist '("\\.ml[iyl]?$" . caml-mode))

;; automatically activate merlin-mode when editing caml code
(add-hook 'caml-mode-hook 'merlin-mode t)

Cheers,
Nicolas

1 Like

There is also spacemacs, which as doom, has a one-line configuration for OCaml.

Can be slow compared to vanilla emacs at startup if you have a lot documents opened. I do not know how it compares to doom.

1 Like

Are there any advantages of using Caml-mode over Tuareg? I’m an emacs only user and I’ve been using Tuareg since I first started learning OCaml

AFAIR there may have been in the past, but no longer. There’s a discussion about it here.

2 Likes

Doom is supposed to be more lightweight, and uses a declarative (and many people say nicer) configuration system that allows it to lazy load only what’s needed. It’s been a long time since I last tried (and got scared away from) Spacemacs though, so this is just based on what I hear from others. In terms of functionality I think they’re very similar.

A bit off-topic, but if you are planning to use Emacs for real-life development, I recommend using vanilla Emacs with as little customization as possible. This works great on both Unix and Windows, is very fast, and if you ever find yourself without your init file (eg when ssh-ing into an unknown machine) it isn’t a problem because you won’t depend on fancy extension packages.

Just my 2c.

Cheers,
Nicolas

5 Likes

indeed i found customization often overrated, a nuisance to manage machines and leading to idiosyncrasies. Or as the fish shell puts it: “Configurability is the root of all evil”.

I aim for an unambitioned setup and also look at others (nano, micro). I want it available on unixes, have syntax highlighting for OCaml and be able to pipe marked text through a command.

I like the command execution of vim, e.g. :!wc -c % and :%!wc -c

I think the particulars of your editor configuration will likely vary from person to person, so I’d warn against generalising too much from a single data point. Personally, I do most of my main development from a single machine, so I invest heavily in customising my Emacs to maximise my own efficiency - in the rare case I’m editing on a foreign machine, I can either use tramp or spawn a light VI process and rely on my fading VI muscle memory. For people who develop on remote machines more frequently, then maybe a lighter configuration might be a better option.

For example, here’s a very useful function that I find myself using quite a bit:

(defun init-ocaml/open-dune-file ()
           "Opens the nearest enclosing dune-file."
           (interactive)
           (find-file (let ((path-components (-filter (lambda (s) (not (string-empty-p s)))
                                                      (split-string (file-name-directory (buffer-file-name)) "/")))
                            dune-file)
                        (while (and (not dune-file) (> (length path-components) 1))
                          (let ((path
                                 (string-join (list "/" (string-join path-components "/") "/"))))
                            (setq dune-file (seq-find (lambda (s) (string-equal s "dune")) (directory-files ".")))
                            (unless dune-file
                              (setq path-components (-drop-last 1 path-components)))))
                        (when dune-file
                          (setq dune-file
                                (string-join (list "/" (string-join path-components "/") "/" dune-file))))
                        dune-file)))
         (bind-key (kbd "C-c C-q") #'init-ocaml/open-dune-file tuareg-mode-map)

Which allows me to jump to the nearest enclosing dune file for the current buffer by just pressing C-c C-q - it’s especially useful when I quickly need to add or remove dependencies for a dune file, and has probably cumulatively saved me at least an hour of unnecassary keypresses.

Naturally, Emacs can do that - it’s just M-| (shell-command-on-region) with vanilla bindings. (prefix with C-u to output the result back into the same buffer).

I think you should be able to simplify that quite a bit by using (locate-dominating-file buffer-file-name "dune")

1 Like

Thank you. I had no idea that there was a function to do that already! You learn something new every day.

(defun init-ocaml/open-dune-file ()
           "Opens the nearest enclosing dune-file."
           (interactive)
           (find-file (string-join (list (locate-dominating-file buffer-file-name "dune") "/dune"))))
         (bind-key (kbd "C-c C-q") #'init-ocaml/open-dune-file tuareg-mode-map)

I think it’s a recent addition. I use it to guess a compilation command from the current buffer.

2 Likes

Thanks for all the advice, epecially the options to keep it simple.

I think I go on with neovim according this recipe https://www.rockyourcode.com/setup-ocaml-with-neovim/

I can’t get used to pressing multiple keys at once and CTRL and ALT being so frequent. (German keyboard, Mac, remote mosh terminals)

It’s possible that what I’m about to suggest is … not useful to you, but just in case:

If you don’t need LSP/Merlin, then might I suggest running Emacs locally on your Mac, and using tramp-mode to access remote filesystems ? I’ve been doing that pretty much exclusively for … 30+ years (before TRAMP, ange-ftp) and it works great. You get a local GUI Emacs, with all the responsiveness, a single editor experience across many machines you might access, etc. You can M-x compile, etc, no problemo (TRAMP figures out it needs to ssh to the remote machine to run the compile command).

I know some people have mentioned that LSP/Merlin don’t seem to work over SSH, and since I don’t use 'em, I don’t know whether that’s fixable. But I kinda suspect it is, since magit-mode (Emacs git mode) works remotely no problemo.

having a local ssh-agent and using ControlMaster channels also makes this work really sweetly.

But OTOH, if you don’t like “living on the control key”, this might not be a useful suggestion.

1 Like

That’s partly why I suggested Doom, which comes with vim bindings (evil-mode) out of the box. Not that it would be too hard to set up on a barebones config either. But Doom also comes with a space-triggered menu (ala Spacemacs) for all the non-vim commands, so you don’t ever have to touch Ctrl or Alt.

I use Emacs as an IDE, not a text editor. For that purpose I use vim, which is always going to boot faster, and for the most part the key bindings work the same in both.

Now I’m curious about TRAMP though…

It’s the bee’s knees. Seriously! I never fire up editors on remote machines except for editing things under sudo. The only limitation is that it’s too complicated to set up the edit-server stuff so you can run emacsclient on the remote machine and have the file show up in your local emacs. I’ve read instructions on how to do that, but … too damn complicated. Someday I’ll look into how chromeos does it from inside the linux container, and try to replicate that.

2 Likes

Here’s my personal Emacs config for OCaml programming (it’s based on use-package, but this can converted to the plain old config format easily):

;;;; OCaml support

;; major mode for editing OCaml code
;; it also features basic toplevel integration
(use-package tuareg
  :ensure t
  :mode (("\\.ocamlinit\\'" . tuareg-mode)))

;; major mode for editing Dune files
(use-package dune
  :ensure t)

;; Merlin provides a lot of IDE-like features for OCaml editors
;; e.g. code completion, go to definition, show type of expression, etc
(use-package merlin
  :ensure t
  :config
  (add-hook 'tuareg-mode-hook #'merlin-mode)
  (add-hook 'merlin-mode-hook #'company-mode)
  ;; we're using flycheck instead
  (setq merlin-error-after-save nil))

;; eldoc integration for Merlin
(use-package merlin-eldoc
  :ensure t
  :hook ((tuareg-mode) . merlin-eldoc-setup))

;; Code Linting
;; This uses Merlin internally
(use-package flycheck-ocaml
  :ensure t
  :config
  (flycheck-ocaml-setup))

;; utop integration
(use-package utop
  :ensure t
  :config
  (add-hook 'tuareg-mode-hook #'utop-minor-mode))

I’d say you can start with just tuareg, dune and merlin initially and include additional packages down the road. I’ve preferred to use merlin-mode directly over using OCamlLISP as the LSP server is implemented in terms of Merlin anyways, and I like the simplicity of merlin-mode.

6 Likes