Much of an editor is lists that many plugins add to: key bindings, commands, hooks, completion sources, colours on a buffer, mode-line items. If these were ordinary variables, nobody could tell who added what, and turning a plugin off would leave its additions behind, as Emacs’s unload-feature leaves anonymous hook functions. In Tuna every entry carries its owner, so the core removes exactly a plugin’s entries when it goes. That is what makes “as if it had never been on” possible.
An ordinary plugin meets this machinery in two forms only: it calls a provider’s verbs (keys:bind!, buffers:set-faces!), and it adds to hooks. Tables and as-caller are what a provider writes inside its verbs.
Verbs
A verb is a contract procedure or form that says what you mean. Each entry it makes is yours: it goes when your plugin turns off, is made again when it reloads, and is listed under your plugin by describe.
Tuna · a plugin
(commands:define(say-hello)"Insert a greeting at point."(buffers:insert!"Hello, world!"))(keys:bind!"C-c h"'say-hello)(keys:bind!"g"'magit-status'git-status-mode)(keys:unbind!"C-z")(display:add-mode-line!(lambda(id)"FlyM"))(faces:set!'highlight'(foreground "#b58900"))
Emacs
(defun say-hello ()"Insert a greeting at point."(interactive)(insert "Hello, world!"))(keymap-global-set "C-c h"#'say-hello)(keymap-set git-status-mode-map "g"#'magit-status)(keymap-global-unset "C-z")(add-to-list 'mode-line-misc-info'(:eval"FlyM"))(set-face-attribute 'highlightnil:foreground"#b58900")
Where Tuna differs. The lines read the same. The difference is what happens when the plugin goes: Emacs keeps the binding, the mode-line item and the face change; Tuna removes all of them, because each is an entry owned by the plugin that called the verb. The verbs each provider offers are listed in Verbs plugins offer.
Hooks
A hook is a list of procedures others add to, called at a moment the hook’s definer chooses. It is Emacs’s hook, with owners.
(define-hook name "doc")core
Does. Declares a hook. Export it so others can add to it.
Underneath. A table whose entries must be procedures. A hook is not saved data: its entries are made again by the plugins’ files each time they start.
Tuna · buffers/plugin.ss (part)
(define-hook after-change-hook
"Called with id, position, deleted text and inserted text.")
Emacs
(defvar after-change-functions nil"List of functions to call after each text change.")
Does. Adds your entry to the hook. You have one entry per hook unless you name more with name:. depth: orders entries, smaller first (default 0). scope: makes the entry run only where that scope applies, such as one buffer.
Underneath. The entry’s key is its owner (your plugin, the init.ss line, or the session) unless named. That is why evaluating an add-hook! form again with C-M-x, which makes a new procedure, replaces your entry instead of adding a second. depth:, name: and scope: are core keywords, so a misspelling fails at build.
(add-hook 'after-change-functions(lambda(&rest _)(my-highlight)))(add-hook 'after-change-functions#'my-check10)(add-hook 'after-change-functions#'my-on-editnilt); local
Where Tuna differs. Emacs keys an entry by the function itself, so a lambda evaluated twice is added twice, and remove-hook needs the same function object. Tuna keys it by its owner. A local entry in Emacs is the hook variable made buffer-local; in Tuna it is an entry with a scope, which still goes with its owner.
(remove-hook! hook [name: 'n])core
Does. Removes your own entry, or with name: your named one. Used on another owner’s named entry, it hides that entry while you are on (an init.ss line silencing a plugin’s hook entry); when you go, it shows again.
Does. Calls every entry that applies with the arguments: the unscoped entries and those whose scope applies now, in order of depth, then the owner’s place in init.ss, then when each was added.
Underneath. An entry that raises an error is removed and reported (buffers:after-change-hook: the entry of flymake failed and was removed: …), and the others still run. A quit (C-g) is not caught; it passes through.
Tuna
(run-hook after-change-hook id pos "" str)
Emacs
(run-hook-with-args 'after-change-functions beg end len)
Where Tuna differs. In Emacs one failing hook function aborts the rest (or, for some hooks, is removed with a message). Tuna always removes and reports the failing entry and goes on.
(run-hook-until-failure hook arg …) → #t or #fcore
Does. Calls entries in order until one returns #f; returns whether none did. Used to ask permission: buffers:kill! asks its kill-query-hook.
Does. The core’s own hook. When a frame is due, the core runs its entries on the editor thread. A display plugin’s entry reads what each browser tab shows and hands it to that tab’s drawing worker.
Underneath. A frame is due after input, or 33 ms after the last worker result if only results changed anything, and only if core state changed. A plugin’s ordinary variables are not core state, so what a plugin wants drawn (a popup) goes through the display’s verbs, which write core state.
A table is a list of named entries, each owned by its adder. A provider defines its tables and writes them inside its verbs; an ordinary plugin never needs them. A provider’s table layout (its key and value shapes) is private to it, so it can change without breaking anyone.
(define-table name "doc" [check])core
Does. Declares a table. check, if given, is (lambda (key value) …) and every entry must pass it.
Underneath. The table’s contents live in the core, as a persistent map from key to a stack of layers, one per owner. A table is not saved data: its entries are made again by the plugins’ files at each start.
Tuna · keys/plugin.ss (part)
(define-table maps "(mode . key) -> command; mode #f is the global map.")
Emacs
(defvar global-map (make-keymap)"Default global keymap.")
(table-set! table key value)core
Does. Adds the owner’s entry for key, or replaces its earlier one. If another owner has an entry for the same key, the newer covers it while its owner is on; when that owner goes, the older shows again.
Underneath. The owner is where the form is written (the plugin’s run, an init.ss line, the session), or the caller under as-caller. The value may be anything, including a procedure: only the editor thread reads tables.
Errors.keys:maps: k -> v is not a valid entry (the table’s check failed). A change from a run that has already ended is ignored and reported. Must be called on the editor thread.
Tuna
(table-set! maps (cons mode key) command)
Emacs
(define-key keymap key command)
(table-remove! table key)core
Does. Removes the owner’s entry for key. On another owner’s entry, it hides that entry while the remover is on; a hide is itself a layer owned by the remover.
Tuna
(table-remove! maps (cons mode key))
Emacs
(define-key keymap key nilt)
(table-ref table key [default])core
Does. The winning entry’s value for key, or default (#f if not given).
Does. All winning entries, ordered by depth, then by the owner’s place in init.ss, then by when each was added. The order is stable: a plugin that restarts keeps its place.
Does. Removes every owner’s entry for key. Only the plugin that defined the table may call it. It is how a dead buffer’s decorations go without each decorating plugin’s help.
Errors.table-clear!: only buffers, which defines faces, may clear it.
Tuna · buffers/plugin.ss, in kill!
(table-clear! faces id)(table-clear! margins id)
Emacs
;; Emacs does this inside kill-buffer: the buffer's;; overlays and local variables die with it.(kill-buffer buf)
(table-call table key arg …)core
Does. Calls the procedure in key’s entry with the arguments and returns its result.
Underneath. Marks the call as the entry owner’s code, so whatever the entry does is charged to its owner, not to the code calling table-call.
Errors.commands:all: has no entry foo.
Tuna · commands/plugin.ss (part)
(define(run! name . args)(apply table-call all name args))
Emacs
(call-interactively (intern-soft name))
(as-caller body …)core
Who uses it. A provider, inside a contract procedure.
Does. Runs body with every owned change it makes (table entries, hook entries, option layers, timers, jobs, on-end actions) charged to the caller of the contract procedure being run, not to the provider. This is how a verb’s entry becomes its caller’s.
Underneath. Every contract call goes through the caller’s wrapper, which marks the call with the caller. as-caller reads that mark; on-caller-end reads the same one. The caller is the plugin, init.ss line or session whose code contains the call, not whoever happens to be running: a procedure of plugin A, stored by plugin B and later called by B, still charges A. Without as-caller, a table-set! inside keys would always belong to keys.
Errors. Outside a contract call: as-caller: no live caller.
;; No equivalent: keymap-set changes the keymap for;; everyone, and nothing records who called it.(defun keymap-set (keymap key definition)(define-key keymap (key-parse key) definition))
Where Tuna differs. This is the one mechanism Emacs lacks that makes Tuna’s turn-off rule work: a plugin’s contributions are found and removed without the plugin writing any clean-up.