Introducing Gopcaml mode - structural OCaml editing

Hi all, I am pleased to announce the first release of Gopcaml-mode, a new emacs library that aims to extend the existing OCaml editing experience with structural editing capabilities.

A picture is worth a thousand words, so I’ll cut to the chase, and start with a few demonstrations:

Examples

  • AST-based code navigation - C-M-n, C-M-p, C-M-u, C-M-d, C-M-f, C-M-b

  • AST-based code transformation -C-M-N, C-M-P, C-M-F, C-M-B

  • Mark exp - C-M-SPC

  • Extract expression into letdef - C-c C-e

This is just a small sample of the features - a full listing is provided at the project readme, which can be found at the project page.

Notes

This plugin is quite faithful to the OCaml specification and doesn’t reimplement a separate OCaml parser as some other plugins do - instead I use the Ecaml package (which allows interfacing with Emacs from OCaml code) to allow delegating to the OCaml parser (from Ocaml-compiler-libs) directly.

It’s in the process of being published to opam, and should be available to download soon.

Edit: It has now been merged into the opam-repositories and can be installed via opam:

Installation

  • Install the package from opam
opam install gopcaml-mode
  • load the package in your init.el
 (let ((opam-share (ignore-errors (car (process-lines "opam" "config" "var" "share")))))
	   (when (and opam-share (file-directory-p opam-share))
	     ;; Register Gopcaml mode
	     (add-to-list 'load-path (expand-file-name "emacs/site-lisp" opam-share))
         (autoload 'gopcaml-mode "gopcaml-mode" nil t nil)
	     ;; Automatically start it in OCaml buffers
	     (setq auto-mode-alist
		   (append '(("\\.ml[ily]?$" . gopcaml-mode)
			     ("\\.topml$" . gopcaml-mode))
			   auto-mode-alist))
	     ))
  • (Optional) install multiple-cursors to enable extraction and smartparens for better handling of parenthesis.
20 Likes

This is very well thought out, I love it!

1 Like

Looks very cool. Does it work with buffers that contain syntax errors?

1 Like

Unfortunately at the moment it doesn’t work on syntactically invalid buffers - the vanilla OCaml parser doesn’t support graceful recovery from syntax errors, so the tool will just invalidate it’s local AST when a change is made and then reparse the next time an AST query is run (this isn’t a significant issue as parsing usually just takes in the order of milliseconds to run).

In the future, I plan to add some support for handling syntax errors by migrating over to the merlin implementation of the parser which I’ve heard can handle syntax errors slightly more gracefully.

1 Like

While it’s no real solution, I also haven’t found this to be a major issue as I find that most of the time when I’m moving around (i.e not inserting text), the code is usually syntactically correct - I’ve also bound M-RET to insert a type hole (??) to easily allow movement even when I haven’t completed a function definition, and overall this leads to quite a natural editing experience.

2 Likes

Nice. I’m excited to see a merlin based implementation. I agree that it’s probably not too hard to keep your buffer in a state where the syntax is correct, but it would still be quite nice to lift the extra mental burden.

With an eye towards the future: To provide a good OCaml experience for all editors, it’s nice to provide functionality via the LSP protocol. While the protocol does not cover structural editing at the moment, I’m pretty sure it’s just a matter of time until something pops up. It would be nice to use gopcaml as an OCaml library to provide a structural editing experience for all editors this way.

4 Likes

Very nice. Thanks!
I do a lot of Clojure code and structural editing is a blessing.
I was looking for an OCaml equivalent to paredit (or smartparens now) for quite some time now. And here it is!

I’ve run out of time to edit the original post, so I’ll add an edit as a reply:

I forgot to mention that using this will require that your emacs is compiled with support for dynamic modules (otherwise you’ll get an error about missing ELF headers).

Finally, tuareg and merlin should also be installed and set to autoload, so the init file entry should look like:

 (let ((opam-share (ignore-errors (car (process-lines "opam" "config" "var" "share")))))
	   (when (and opam-share (file-directory-p opam-share))
	     ;; Register Gopcaml mode
	     (add-to-list 'load-path (expand-file-name "emacs/site-lisp" opam-share))
         (autoload 'gopcaml-mode "gopcaml-mode" nil t nil)
         (autoload 'tuareg-mode "tuareg" nil t nil)
         (autoload 'merlin-mode "merlin" "Merlin mode" t)
	     ;; Automatically start it in OCaml buffers
	     (setq auto-mode-alist
		   (append '(("\\.ml[ily]?$" . gopcaml-mode)
			     ("\\.topml$" . gopcaml-mode))
			   auto-mode-alist))
	     ))
1 Like