gopcaml-mode

[MIRROR] Ultimate Ocaml Editing Mode

GPL-3.0 License

Stars
21

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-based code transformation -C-M-N, C-M-P, C-M-F, C-M-B
  • Fixed move-to-defun, move-to-end-defun -C-M-a, C-M-e
  • Jump to type hole - TAB
  • Automatic let binding expansion (i.e adds in automatically if defining let inside a let)
  • Mark exp - C-M-SPC
  • Move to nearest parameter - C-c C-p
  • Move to nearest let def - C-c C-o
  • Extract expression into letdef - C-c C-e

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.