Starting out with (Doom) Emacs

I’ve been writing OCaml for a bit now, and have decided to try and upgrade my workflow. Part of that project was to be switching from vim to emacs for longer dev sessions. So I get Doom, make sure it’s OCaml stuff is switched on, and…it’s not easy. To wit (warning:complaints, feel free to skip!):

  • The autocompletion is usually unobstrusive, but it insists on trying to complete the “in” in “let/in” to other things!
  • Similarly, I have yet to figure out how the indentation engine guesses things, and fight it a lot. One issue is it seems to want 2 space indents, but the editors line shifting (“>” and “<”) locks to 4 space tab locations, and I can’t find where these settings are documented?
  • In evil mode (the vim-like editing), I can’t send stuff to the repl. I need to switch to emacs bindings, which my fingers hate, to use the repl in emacs.

So, is there some beginner’s guide I’ve overlooked to help build a comfortable environment where I can:

  • Get type information from throughout the project
  • Run a repl
  • Find and fix typing mistakes
  • Write and use expect tests
    ?

My 2c: use vanilla Emacs (+ Merlin). You will be slower at the beginning because of the different keybindings but you will have a more solid foundation on top of which to build your workflow. And if you find out that you cannot get used to Emacs keybindings after a while, maybe Emacs is not for you :slight_smile:

Cheers,
Nicolas

I see your point. For me, that would be a hard choice. Vim (or even vi, if you can find it!) satisfies most of my text editing needs and I want to retain efficiency there. By design though it’s worse at integrating development functionality. On a previous thread here someone suggested Doom, so presumably it works for some folks.

This isn’t emacs specific, it affects all editors. There’s a bunch of issues on GitHub tracking that problem.

Are you using ocamlformat or ocp-indent? That would be the most robust solution. Or only what is natively being done by emacs? The Tuareg-mode in emacs is using something called SMIE which should be pretty good when it comes to indentation. I don’t know the settings that would control 2vs4 spaces though.

When you launch the command using the emacs bindings I believe that it should display for a second or two which command is being executed. Then you can create your own vim-like bindings for that command.

I doubt that it’s possible to recreate the essence of vi/vim “modal” [1] UI in emacs, globally. It’s always going to clash with some emacs infrastructure, especially and unfortunately because lots of that infrastructure was written before abstraction was fashionable :grin: There are emacs extension packages (from the various ELPAs) that provide similar kind of efficiency in a limited, scoped way. Look for example at the hydra package. There are others. Just give yourself an hour or two exploring the output of package-list-packages.

[1] by this I mean things like INSERT and VISUAL mode; emacs has its own notion of mode (major/minor) which is very different, of course.

I’ve used both doom-emacs and doom-nvim (and prior to that I contributed some OCaml support to Spacemacs, and for a while used an entirely hand-rolled Neovim configuration).

If you want a more Vim-like experience you can give doom-nvim a try. It did require quite a lot of tweaking, although I’ve upstreamed most of my tweaks to doom-nvim, but there are some that remain Comparing doom-neovim:main...edwintorok:main · doom-neovim/doom-nvim · GitHub

These days I just use https://helix-editor.com/ though. It requires hardly any tweaking and works with the OCaml LSP server mostly out of the box.
This is the entirety of my Helix configuration, you’ll notice there isn’t anything OCaml specific about it and is only ~41 lines in total, including color theme tweaks.
Then run hx -g fetch and hx -g build, and opam install ocaml-lsp-server and you’re ready to go (although of course Helix is not Vim, in fact its action-verb order is the opposite of Vim’s).

There is a VSCode plugin that works fairly well. And you can install a Neovim plugin in VSCode to use an embedded real Neovim to do the editing (which doesn’t suffer from all the latency issues that the original neovim plugin had).

But it all depends what IDE you’re already comfortable with.
If you’re familiar with Emacs already then it might be worth tweaking your Emacs config further (or improving doom-emacs like I’ve done previously). Or you can try spacemacs.org too, I used that for a few years.

If you’re already familiar with Vim or Neovim then either improving doom-nvim or setting up your new config from scratch is worthwhile (and I used this setup for many years).

If you don’t really mind what IDE to use, just want something that works then VSCode (either the official version or VSCodium) with the OCaml platform plugin might be a good choice.

1 Like

I’d love to try Helix, but these days I’m back in the Debian/Ubuntu world and there’s no package. And in this case flatpak sandbox would be a pita. Any suggestions?


Ian

The AppImage release is not working for you?

There are unofficial Ubuntu packages here Installation

To (old) me, Doom is a 2D 1st person shooter video game from the 90s…

The autocompletion is usually unobstrusive, but it insists on trying to complete the “in” in “let/in” to other things!

i haven’t experienced this for let in but i have for sig end. haven’t figured out what causes it yet.

Similarly, I have yet to figure out how the indentation engine guesses things, and fight it a lot. One issue is it seems to want 2 space indents, but the editors line shifting (“>” and “<”) locks to 4 space tab locations, and I can’t find where these settings are documented?

ive found that its usually easier to search for the variables a mode defines rather than search docs for this kind of thing. doing M-x helpful-variable and typing in tuareg indent returned a few results for me. you can probably use setq to change those values however you like.

that said, i tend to let ocpindent / ocamlformat do those things for me automatically via the format doom module. those tools have their own configuration files per project.

In evil mode (the vim-like editing), I can’t send stuff to the repl. I need to switch to emacs bindings, which my fingers hate, to use the repl in emacs.

fairly sure in vim mode g r is bound to +eval:region by default, which will automatically try to open a repl for the given mode & send the selected region to it. ive done that a bunch of times. g R will send the whole buffer.

  • Get type information from throughout the project
  • Find and fix typing mistakes

this should just work if you have the ocaml lsp in your environment. M-x env will let you search the env for OPAM stuff. the doom binary has a doom env command that lets you save your current env to a file that doom will load.

Write and use expect tests

when i do SPC p T it automatically defaults to dune runtest in my project. my config (below) has some custom functions that let you run all the inline tests in your current buffer via SPC m i f.

good luck!


;; ## OCAML
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(use-package! tuareg
  :config
  (map! :localleader :map tuareg-mode-map
        :desc "find [a]lternate file" "a" #'tuareg-find-alternate-file
        :desc "compile" "c" #'my/dune-build
        :desc "run tests" "t" #'my/dune-runtest
        :desc "promote tests" "p" #'my/dune-promote
        (:prefix ("i". "inline tests")
         :desc "Run tests in [f]ile" "f" #'my/dune-run-inline-tests
         :desc "Promote tests in file" "F" #'my/dune-promote-inline-tests
         :desc "Promote tests in file" "a" #'my/dune-run-all-inline-tests
         :desc "See help" "h" #'my/dune-inline-test-help
         ))

  (add-hook! 'tuareg-mode-hook #'my/disable-evil-continue-comments-h)
  (add-hook! 'tuareg-mode-hook #'my/tuareg-comment-style)

  (define-derived-mode tuareg-mli-mode tuareg-mode "Tuareg Mli")
  (set-tree-sitter-lang! 'tuareg-mli-mode 'ocaml-interface)
  (add-hook! 'tuareg-mli-mode-hook #'tree-sitter!)
  (setq auto-mode-alist (delete '("\\.ml[ip]?\\'" . tuareg-mode) auto-mode-alist))
  (add-to-list 'auto-mode-alist '("\\.ml[p]?\\'" . tuareg-mode))
  (add-to-list 'auto-mode-alist '("\\.mli\\'" . tuareg-mli-mode))
  )

(use-package! utop
  :config
  (setq! utop-command "dune utop --no-print-directory --display quiet . -- -emacs"
         utop-edit-command nil)
  (map! :map utop-mode-map
        :nvm "i" #'my/utop-insert-at-prompt
        :nvmi "C-p" #'utop-history-goto-prev
        :nvmi "C-n" #'utop-history-goto-next
        :i "C-k" #'utop-history-goto-prev
        :i "C-j" #'utop-history-goto-next))

(after! (:and apheleia ocp-indent)
  (add-to-list 'apheleia-formatters '(ocp-indent . ("ocp-indent"))))

(defun my/utop-insert-at-prompt ()
  (interactive)
  (if (< (point) utop-prompt-max)
      (progn (goto-char (point-max))))
  (evil-insert 1))

(defun my/disable-evil-continue-comments-h ()
  (setq-local +evil-want-o/O-to-continue-comments nil)
  (setq-local +default-want-RET-continue-comments nil))

(defun my/tuareg-comment-style ()
  (print "wtf bro")
  (setq-local comment-style 'multi-line)
  (setq-local comment-continue "   "))

(defun my/projectile-compile (cmd)
  (projectile-with-default-dir (projectile-acquire-root)
    (compile cmd)))

;; TODO: make this handle the case where theres more than one .exe
(defun my/dune-inline-test-runner ()
  (let ((path (s-trim (projectile-with-default-dir (projectile-acquire-root)
                        (shell-command-to-string
                         (s-concat "find _build -path '*sandbox' -prune "
                                   "-o -name 'inline_test_runner*.exe' "
                                   "-print" ))))))
    (--if-let (s-match (s-concat "_build/.?*/\\(.?*\\)/"        ; get dir
                                 "\\.\\(.?*\\).inline-tests/"   ; get target
                                 "inline_test_runner_.?*.exe") path)
        (list 'path (car it) 'dir (cadr it) 'target (caddr it))
      nil)))

(defcustom my/dune-inline-test-args "-verbose -no-color -diff-cmd 'diff -u'"
  "arguments that are always passed to the inline test runner"
  :group 'my/dune :type 'string)

;; TODO: make this choose the corresponding test runner based on
;; the directory of the current file (my/projectile-buffer-path)
(defun my/dune-inline-test-cmd (args)
  (let* ((runner (my/dune-inline-test-runner))
         (cmd (format "%s inline-test-runner %s %s %s"
                      (plist-get runner 'path)
                      (plist-get runner 'target)
                      my/dune-inline-test-args
                      args)))
    (format "dune exec -- %s" cmd)))

(defun my/dune-inline-test-help ()
  "runs -help command on inline test runner"
  (interactive)
  (my/projectile-compile (my/dune-inline-test-cmd "-help")))

(defun my/dune-run-inline-tests ()
  "runs inline tests in the current file"
  (interactive)
  (let* ((file (f-filename (buffer-file-name)))
         (cmd (format "-only-test %s" file)))
    (my/projectile-compile (my/dune-inline-test-cmd cmd))))

(defun my/dune-promote-inline-tests ()
  "promotes inline tests in the current file"
  (interactive)
  (let* ((file (f-filename (buffer-file-name)))
         (cmd (format "-in-place -only-test %s" file)))
    (my/projectile-compile (my/dune-inline-test-cmd cmd))))

(defun my/dune-run-all-inline-tests ()
  "runs all inline tests in the project"
  (interactive)
  (my/projectile-compile (my/dune-inline-test-cmd "")))

(defun my/dune-build ()
  "calls dune build" (interactive) (my/projectile-compile "dune build"))

(defun my/dune-runtest ()
  "calls dune runtest" (interactive) (my/projectile-compile "dune runtest"))

(defun my/dune-promote ()
  "calls dune promote" (interactive) (my/projectile-compile "dune promote"))

2 Likes

Thanks mate! It’s nice to have an example of how to configure things – I’m very far from an elisp hacker.