Emacs Plugins in OCaml: Putting It All Together (part 4)
Published: Last updated:· Tags: ecaml emacs ocaml · Category: ecaml-getting-started
This post is part 4 of a series (prev). The full code is available on GitHub.
In previous parts, we:
- figured out how to compile OCaml code into an Emacs plugin
- wrote a brainfuck interpreter library
- explored features of the Ecaml library
Now, it’s time to put everything together. Our final plugin will provide us a way to execute brainfuck code in a buffer without leaving Emacs, and with the benefit of the type safety and performance of OCaml.
Table of Contents
1 Setup
First, we need to modify our jbuild
file from part 1.
1: (jbuild_version 1) 2: 3: (executables 4: ((names (plugin)) 5: (libraries (bf_lib ecaml)) 6: (preprocess (pps (ppx_here))))) 7: 8: (rule (copy plugin.exe ecaml-bf.so)) 9: 10: (alias 11: ((name plugin) 12: (deps (ecaml-bf.so)))) 13: 14: (alias 15: ((name runtest) 16: (deps ((alias plugin))) 17: (action (run emacs -Q -L . --batch --eval "(require 'ecaml-bf)"))))
Only the executables
rule was modified, adding bf_lib
(our interpreter
library) to the list of required libraries and adding a preprocess
rule for
ppx_here
, which provides the [%here]
syntax we’ll be using with defun
.
2 Plugin
2.1 Basic function
To begin with, we’re just going to allow Emacs to evaluate brainfuck code
through our interpreter, non-interactively (i.e., from Lisp code). We’ll
define a function named bf-eval
that just wraps our library code:
1: open Ecaml 2: 3: let eval_program program input = 4: let program = Value.to_utf8_bytes_exn program in 5: let input = Value.to_utf8_bytes_exn input in 6: Bf_lib.Program.run' ~program ~input 7: |> Value.of_utf8_bytes 8: ;; 9: 10: let () = 11: defun [%here] (Symbol.intern "bf-eval") 12: ~docstring:"evaluate [program], a brainfuck program, given [input]" 13: ~args:[ Symbol.intern "program" 14: ; Symbol.intern "input" ] 15: (function 16: | [| program; input |] -> eval_program program input 17: | _ -> invalid_arg "wrong arity") 18: ;;
I split up the definition into two parts. eval_program
wraps
Bf_lib.Program.run'
and does the work of converting Emacs values (in this
case, strings) into OCaml values and vice versa.
The second part calls defun
to register the function with Emacs, providing
a docstring and argument names and checking the number of arguments passed.
We can try running it:
jbuilder build @plugin emacs -Q -L _build/default/src --batch --eval "(require 'ecaml-bf)" \ --eval '(print (bf-eval ",+." "A"))' # B
The program ,+.
simply inputs a byte, increments it, and prints the result,
so it transforms "A"
into "B"
.
If we load the plugin into a normal (non-batch mode) Emacs, we wouldn’t be able to call the function as an M-x command, since we haven’t marked it as an interactive command yet.
2.2 Interacting with Emacs
Next, let’s write a function that evaluates the brainfuck code in the current
buffer. bf-eval-buffer
will be an interactive command, so it can be called
via M-x bf-eval-buffer and will prompt the user for a
string to serve as the input to the program.
20: let eval_current_buffer input = 21: let program = 22: Current_buffer.contents () 23: |> Text.to_utf8_bytes 24: in 25: let input = Value.to_utf8_bytes_exn input in 26: Bf_lib.Program.run' ~program ~input 27: ;; 28: 29: let () = 30: defun [%here] (Symbol.intern "bf-eval-buffer") 31: ~docstring:"evaluate the current buffer as brainfuck code" 32: ~interactive:"sInput: " 33: ~args:[ Symbol.intern "input" ] 34: (function 35: | [| input |] -> 36: let output = eval_current_buffer input in 37: messagef "Output: %s" output; 38: Value.nil 39: | _ -> invalid_arg "wrong arity") 40: ;;
The argument to interactive
, "sInput: "
, causes the command to prompt the
user for a string as its first argument (the minibuffer will contain the
prompt Input:
).
2.3 Major mode and key bindings
Next, we’ll define a major mode for this new feature, as well as a key binding so that users can easily access the command without using M-x.
42: let mode_name = Symbol.intern "bf-mode" 43: 44: let () = 45: (* define [bf-mode] as a major mode *) 46: let mode = 47: Major_mode.define_derived_mode ~parent:Major_mode.fundamental 48: [%here] 49: ~change_command:mode_name 50: ~docstring:"Major mode for interacting with brainfuck code." 51: ~initialize:ignore 52: ~mode_line:"brainfuck" 53: in 54: (* bind [C-x C-e] to [bf-eval-buffer] *) 55: let keymap = Major_mode.keymap mode in 56: Keymap.define_key keymap (Key_sequence.create_exn "C-x C-e") 57: (Command (Command.of_value_exn (Value.intern "bf-eval-buffer"))) 58: ;;
Our major mode doesn’t need anything to be initialized, so we simply pass
ignore
to that argument. change_command
determines the name of the mode,
i.e., the name of the command that sets the major mode, in this case,
bf-mode
. docstring
provides the documentation that is displayed by
describe-mode
(C-h m), while mode_line
sets the
display name of the mode that appears, unsurprisingly, in the mode line.
Additionally, we bind the sequence C-x C-e to
bf-eval-buffer
in the keymap of our major mode, so that when the mode is
activated, pressing that key sequence will trigger the command
bf-eval=buffer
.
2.4 Automatically starting bf-mode
Ecaml’s Auto_mode_alist
module provides a clean interface for manipulating
the auto-mode-alist
variable, which determines how Emacs decides which
major mode to use when you open a file, like starting c-mode
when you open
a file named foo.c
. We’ll register bf-mode
for *.b
files:
60: let () = 61: (* automatically start [bf-mode] upon opening a [*.b] file *) 62: Auto_mode_alist.add Auto_mode_alist.Entry.( 63: [ { delete_suffix_and_recur = false 64: ; filename_match = Regexp.of_pattern "\\.b\\'" 65: ; function_ = Some mode_name } ]) 66: ;;
The delete_suffix_and_recur
field allows an entry to defer to another mode
when the file may have more than one extension.1 filename_match
specifies the regular expression that determines which filenames activate the
major mode. Finally, function_
specifies what mode to activate when the
file name matches filename_match
2.
4 Hooray!!!
jbuilder build @plugin
emacs -Q -L _build/default/src --eval "(require 'ecaml-bf)"
Try it out for yourself!
All of the code’s on GitHub. Bug reports and patches welcome.
Footnotes:
For example, you could have an entry that is activated by opening a file
with a .gz
suffix, such as foo.c.gz
. It might decompress the file and then
recursively activate the correct major mode based on the remainder of the
filename.
It actually doesn’t have to be a mode change command; it can just be any function.