Skip to content

Gopiandcode/gopcaml-mode

Repository files navigation

NOTE: THIS IS A MIRROR

Please submit issues, feature requests and PRs to https://gitlab.com/gopiandcode/gopcaml-mode

Gopcaml Ocaml Emacs Major Mode

The ultimate ocaml editing mode.

Features

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

ast-code-navigation

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

ast-code-transform

  • Fixed move-to-defun, move-to-end-defun -C-M-a, C-M-e

move-to-defun

  • Jump to type hole - TAB

jump-to-type-hole

  • Automatic let binding expansion (i.e adds in automatically if defining let inside a let)

automatic-let-bindings

  • Mark exp - C-M-SPC

mark-sexp

  • Move to nearest parameter - C-c C-p

move-to-param

  • Move to nearest let def - C-c C-o

move-to-let-def

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

extract-expression

Installation

Gopcaml mode is implemented using a mixture of ocaml and elisp.

Make sure your emacs is compiled with dynamic modules support (you may need to build emacs from source with the --with-modules option).

Note: If you get an error about ELF headers this means that your emacs doesn't support dynamic modules - you'll need to build emacs from source (takes ~5 minutes usually).

  • Install the project via opam:
opam install gopcaml-mode
  • install merlin, ocp-indent and tuareg mode
  • load the project 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)
         (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))
	     ))

Enjoy your ultimate editing experience.

Extras

  • For some additional features that aren't included in the main release, see the extras folder

Development

If you want to tinker with this project/extend it/build your own version, see below:

Project Structure

The core project laid out as follows:

├── gopcaml.ml
├── gopcaml_state.ml
├── ast_zipper.ml
├── ast_analysis.ml
├── ast_transformer.ml
├── gopcaml-mode.el
├── gopcaml-multiple-cursors.el
└── gopcaml-smartparens.el

The purpose of each file is defined as follows (in the order in which you'd probably want to look at them):

  • gopcaml.ml

    • defines the main entrypoint for the module
    • this is where all the functions bindings to emacs are setup
  • gopcaml_state.ml

    • defines functions to parse and track a copy of the AST for use in other components
  • ast_zipper.ml

    • defines a huet-style scarred zipper for the OCaml AST.
    • the zipper operates in a lazy fashion - i.e the AST is only expanded into the zipper type when the user expicitly requests it
  • ast_analysis.ml

    • contains functions that perform analysis over the AST (i.e things like finding the free variables in an expression, etc.)
    • ast_transformer.ml should be moved into here at some point
  • gopcaml-mode.el

    • main elisp plugin file
    • takes the functions exported by gopcaml.ml and provides wrappers to make them more robust
  • gopcaml-*.el

    • optional features that are loaded in when the required packages are also loaded
    • allows for better compatibility with other emacs packages (i.e for example, disabling ast-movement when at the start of a parens so smartparens can work )

Architecture

  • There are two main interesting components to gopcaml-mode
  • Tracking OCaml Ast
    • in order to work, gopcaml mode needs to have a copy of the ocaml ast that (typically*) needs to be up to date with the buffer contents
    • to achieve this while maintaining a fluid user experience this is achieved through to measures:
      • invalidating on changes:
        • when any change is made to the buffer, the state is invalidiated (see gopcaml_state.ml/State/DirtyRegion/update)
        • if the user runs a command that requires the ast and the ast is invalidated, then we try and rebuild the ast (see gopcaml_state.ml/State/Validated/of_state.ml).
      • periodic rebuilding:
        • when gopcaml-mode is started, an idle timer is setup to periodically check if the AST is out of date and rebuild it when the user doesn't perform any changes for a while
        • this just means that we can perform AST reconstruction during idle time, and reduces the cost of moving after changes *sometimes we don't care if the ast is out of date/we're doing analysis during a time when we know the ast will not be constructable (i.e for example if implementing a function to check whether we are writing text inside a letbinding (see gopcaml_state.ml/inside_let_def)) - in this case we can try and retrieve an old copy of the state (see gopcaml_state.ml/State/Validated/of_state_immediate)
  • Zipper-mode
    • zipper-mode is the terminology given to the transient mode that is entered when the user performs strucutural movement.
    • when a structural command is run for the first time, we retrieve the ast and create a zipper and store it in a buffer-local-varaible (see gopcaml_state.ml/build_zipper_enclosing_point)
    • all subsequent movement commands retrieve the zipper from this variable and use it to move the emacs cursor and the overlay highlighting the selected item
    • transformation operations also use the zipper to update the buffer, but have to take extra care to ensure that they also update the state of the zipper to reflect the changes in the ast (as the zipper, unlike the ast isn't periodically updated) (see ast_zipper.ml/move_(left|right|up|down))
    • when any command that isn't a structural editing one is pressed, the transient mode ends, and the zipper variable is cleared.
    • Note: the fact that the zipper is in a separate variable from the ast deliberately means that the zipper may become desynchronized from the ast - for example, if we perform an AST transformation using the zipper, then the original ast will not be up to date. This is mainly just to avoid unnecassary work - rather than writing transformation functions twice for the ast and zipper, we write them once for the zipper (taking sure to ensure that the meta-information stored in the zipper is kept up to date), and then let the automatic rebuilding functionality handle updating the original ast.

Setting up the development environment

Being an emacs plugin, the development environment setup is tailored for emacs.

(add-to-list
	'command-switch-alist
	(cons "gopdev"  (lambda (__) nil)))
	
(if (member "-gopdev" command-line-args) (setq gopcaml-dev-mode t))

(if (or (not (boundp 'gopcaml-dev-mode)) (not gopcaml-dev-mode))
   ... ;; run normal gopcaml initialization code (i.e from the install instructions)
)
  • Now launch emacs passing the flag -gopdev and open any file inside the project directory.
  • When prompted press y or ! to setup the development variables for the file.
  • Now this instance of emacs will use your local branch to load gopcaml-mode (It's quite nice developing in this way, as any changes you make will be reflected in your editor, and can quickly be tried out**.

Note: My typical development setup is to have a command prompt open in the background and execute dune build && emacs -gopdev ./<some-file>.ml. I make some changes, use merlin to ensure there are no issues, exit and press up on my terminal to reload the prior command and press enter.

Note*: The reason for the complicated setup is that gopcaml-mode uses dynamic modules to call out to ocaml mode from emacs, and dynamic modules can only be loaded into an emacs instance once - thus each time you make a change, you'll need to restart emacs.