Example plugins

Each example is a whole plugin in the approved API, beside the closest idiomatic Emacs code for the same job. They were written first against the prototype, where each ran in the tests, and then rewritten with the verbs; the rewritten versions have not run, because nothing of the API is built yet. The plugins they call (buffers, keys, commands, modes, faces, display, layout, minibuffer, completion, processes) are Tuna’s default plugins as planned; their contracts are listed in Verbs plugins offer.

In none of them does a table, a table key, an owner or a tie key appear. That is the point of the verbs.

Hello: the smallest useful plugin

What it shows: a plugin is a file that runs top to bottom; it imports contracts and calls them with a prefix; it defines a command and binds a key through verbs; and it needs no clean-up code, because both go when it turns off.

Tuna · hello/plugin.ss
(plugin hello
  (import (buffers) (commands) (keys))
  (commands:define (say-hello)
    "Insert a greeting at point."
    (buffers:insert! "Hello, world!"))
  (keys:bind! "C-c h" 'say-hello))
Emacs · hello.el
(defun say-hello ()
  "Insert a greeting at point."
  (interactive)
  (insert "Hello, world!"))
(keymap-global-set "C-c h" #'say-hello)

How it reads:

To use it: put the folder among your plugins, add (use-plugin hello) to init.ss, run reload-config. To remove it: delete the line and reload-config; the command and the binding are gone, as if it had never been on. Text it inserted stays: it is in your buffer, which is your data.

Where Tuna differs. The Emacs file, once loaded, cannot be taken back: unload-feature removes the function but leaves the key bound to a missing command. The Tuna plugin is one line shorter because the interactive form is not needed: every command is a procedure of no arguments that reads what it needs.

A syntax highlighter: a worker and faces

What it shows: heavy work on a worker; one job per buffer, each change replacing the last; faces given to a buffer through a verb, so they go when the plugin goes.

Tuna · highlight/plugin.ss
(plugin highlight
  (import (buffers) (faces))
  (faces:define highlight '(bold #t) "Words worth noticing.")
  (define-worker (find-hello text)
    (post (let loop ([i 0] [acc '()])
            (if (> (+ i 5) (string-length text))
                (reverse acc)
                (loop (+ i 1)
                      (if (string=? (substring text i (+ i 5)) "hello")
                          (cons (list i (+ i 5) 'highlight) acc)
                          acc))))))
  (define (highlight! id)
    (start-job find-hello (buffers:text id)
      (lambda (spans) (buffers:set-faces! id spans))
      for: id))
  (add-hook! buffers:after-change-hook (lambda (id . _) (highlight! id)))
  (for-each highlight! (buffers:list)))
Emacs · my-hello.el
(defface my-hello '((t :weight bold))
  "Words worth noticing.")
(font-lock-add-keywords nil '(("hello" . 'my-hello)))

How it works:

  1. When the plugin starts, the last line starts a scan of every buffer. From then on, its entry in after-change-hook starts one after every edit.
  2. highlight! hands the worker the buffer’s text, a read-only string, so it is shared without a copy. The job is for: id: if you type fast, each new job for the same buffer cancels the previous one, so only the newest text’s spans are ever applied.
  3. find-hello runs on its own thread. It can use string-length, substring and post; if it tried (buffers:text id) the build would refuse it, because a worker cannot reach other plugins.
  4. Its result comes back to on-result on the editor thread: spans ready to apply, each (from to face). buffers:set-faces! replaces this plugin’s faces on the buffer; on a buffer killed meanwhile it does nothing.
  5. When highlight turns off, its faces on every buffer, its hook entry, its face declaration and any running job all go. The next frame is drawn without the colours. The buffers are untouched.

Where Tuna differs. Emacs is shorter because font-lock is a highlighting engine and this plugin is the engine. A Tuna language mode would use a highlighting plugin’s verb in the same way, (highlight:add-keywords! 'scheme-mode '(("hello" . highlight))); this example shows what such a plugin does with a worker. Emacs’s font-lock runs on the main thread, so a slow pattern freezes typing; Tuna’s scan never blocks typing.

Flymake-style diagnostics

What it shows: a checker on a worker; a minor mode tied to the plugin; faces and margin marks on a buffer; a per-buffer value; a mode-line item; a command with a key.

Tuna · flymake/plugin.ss
(plugin flymake
  (export diagnostics)
  (import (buffers) (modes) (faces) (display) (commands) (keys))
  (faces:define flymake-error '(underline (wave "#dc322f")) "Text a checker found an error in.")
  (modes:define-minor flymake-mode "Checks the buffer as you type.")
  (define-option found '() "The current buffer's errors, each (from to line)." list?)
  ;; Each FIXME is an error.
  (define-worker (find-fixmes text)
    (post (let loop ([i 0] [line 0] [acc '()])
            (cond [(> (+ i 5) (string-length text)) (reverse acc)]
                  [(string=? (substring text i (+ i 5)) "FIXME")
                   (loop (+ i 5) line (cons (list i (+ i 5) line) acc))]
                  [else (loop (+ i 1) (if (char=? (string-ref text i) #\newline) (+ line 1) line) acc)]))))
  (define (diagnostics id) (buffers:with id found))
  (define (check! id)
    (start-job find-fixmes (buffers:text id)
      (lambda (errors)
        (modes:enable-minor! id 'flymake-mode)
        (buffers:with id (set-local! found errors))
        (buffers:set-faces! id (map (lambda (d) (list (car d) (cadr d) 'flymake-error)) errors))
        (buffers:set-margin! id (map (lambda (d) (list (caddr d) "!" 'flymake-error)) errors)))
      for: id))
  (add-hook! buffers:after-change-hook
    (lambda (id . _) (unless (buffers:internal? id) (check! id))))
  (display:add-mode-line!
    (lambda (id) (let ([n (length (diagnostics id))]) (and (> n 0) (format "FlyM:~a" n)))))
  (commands:define (flymake-goto-next-error)
    "Move point to the next error."
    (let ([d (find (lambda (d) (> (car d) (buffers:point))) found)])
      (when d (buffers:goto! (car d)))))
  (keys:bind! "C-c ! n" 'flymake-goto-next-error)
  (for-each check! (remp buffers:internal? (buffers:list))))
Emacs · flymake.el, much shortened
(defface flymake-error '((t :underline (:style wave :color "Red1")))
  "Face used for marking error regions.")
(defvar-local flymake--found nil "This buffer's errors.")
(define-minor-mode flymake-mode "Checks the buffer as you type."
  :lighter (:eval (format " FlyM:%d" (length flymake--found)))
  (if flymake-mode
      (add-hook 'after-change-functions #'flymake-after-change nil t)
    (remove-hook 'after-change-functions #'flymake-after-change t)))
(defun my-fixme-backend (report-fn &rest _)
  (let (found)
    (save-excursion
      (goto-char (point-min))
      (while (search-forward "FIXME" nil t)
        (push (flymake-make-diagnostic (current-buffer) (- (point) 5) (point) :error "FIXME")
              found)))
    (funcall report-fn found)))
(add-hook 'flymake-diagnostic-functions #'my-fixme-backend nil t)
(keymap-set flymake-mode-map "C-c ! n" #'flymake-goto-next-error)

How it reads:

Where Tuna differs. Emacs splits flymake into an interface and backends, each backend a hook function; the Tuna plugin here is both. Splitting it the same way, with checkers in their own plugins registering a worker through a verb such as (flymake:add-checker! find-fixmes), is a natural next step, not part of the core. Emacs runs the backend on the main thread or in a subprocess; Tuna on a worker.

A status view, like magit

What it shows: a plugin that shows its output in a buffer of its own, a view, written with no lifetime code at all; and, on the provider’s side, how buffers makes a view go with the plugin that asked for it.

The problem it solves: if the status plugin made its buffer with buffers:create, the buffer would be the user’s and would stay open after the plugin turned off, with keys that do nothing. If buffers closed every buffer a plugin made, your shell would close when the terminal plugin reloaded. So the provider offers two procedures and says, per procedure, whose the thing is.

Tuna · gitstat/plugin.ss
(plugin gitstat
  (import (buffers) (layout) (modes) (faces) (commands) (keys))
  (faces:define git-added '(foreground "green") "Margin mark of a changed file.")
  (modes:define-major git-status-mode "Shows the repository's status.")
  (define (render) "On branch main\nmodified: notes.txt\n")
  (define (changed-file? id) (equal? (buffers:file id) "notes.txt"))
  (commands:define (magit-status)
    "Show the repository's status."
    (let ([view (buffers:view "*git-status*")])
      (modes:set-major! view 'git-status-mode)
      (buffers:with view
        (buffers:erase!)
        (buffers:insert! (render)))
      (for-each (lambda (id) (buffers:set-margin! id '((0 "+" git-added))))
                (filter changed-file? (buffers:list)))
      (layout:show! view)))
  (keys:bind! "g" 'magit-status 'git-status-mode))
Emacs · git-status.el
(define-derived-mode git-status-mode special-mode "Git-Status"
  "Shows the repository's status.")
(keymap-set git-status-mode-map "g" #'magit-status)
(defun magit-status ()
  "Show the repository's status."
  (interactive)
  (with-current-buffer (get-buffer-create "*git-status*")
    (git-status-mode)
    (let ((inhibit-read-only t))
      (erase-buffer)
      (insert (my-render)))
    (pop-to-buffer (current-buffer))))

render stands for running git; changed-file? for reading its output.

What you see: M-x magit-status opens *git-status* in the selected window. Edit and reload gitstat: the view stays open, in its window. Turn gitstat off: the view closes, the window shows what it showed before, and the + in notes.txt’s margin goes. If you had a buffer of your own called *git-status*, it is untouched; the view is *git-status*<2>. After a crash the view comes back still tied to gitstat, because ties are saved.

The provider’s side. The parts of buffers this view uses:

Tuna · buffers/plugin.ss (part)
(define-plain-record buffer
  ([name string?] [text] [point fixnum?] [mark (maybe fixnum?)] [modified? boolean?]))
(define-saved all (pmap) "Every buffer by id." (map-of fixnum? buffer?))
(define-saved views (pmap) "Each view's id -> the name it was asked for." (map-of fixnum? string?))
(define-dynamic current #f "The buffer text procedures act on.")
(define-hook after-change-hook "Called with id, position, deleted text and inserted text.")
(define-hook killed-hook "Called with a killed buffer's id.")
(define-hook kill-query-hook "Called with an id; an entry returning #f refuses the kill.")
(define-table faces "Buffer id -> each owner's list of (from to face).")
(define-table margins "Buffer id -> each owner's list of (line text face).")

;; A plugin's view goes when the plugin that asked for it turns off, and
;; stays across its reloads; a buffer the user made with that name is never taken.
(define (view name)
  (let ([id (or (find (lambda (id) (and (live? id) (string=? (pmap-ref views id) name)))
                      (pmap-keys views))
                (let ([id (generate name)]) (set! views (pmap-set views id name)) id))])
    (on-caller-end (buffers:kill! id))
    id))

(define (set-faces! id spans) (when (live? id) (as-caller (table-set! faces id spans))))
(define (set-margin! id marks) (when (live? id) (as-caller (table-set! margins id marks))))

(define (kill! id)
  (and (live? id)
       (run-hook-until-failure kill-query-hook id)
       (begin
         (set! all (pmap-delete all id))
         (when (pmap-contains? views id)
           (set! views (pmap-delete views id))
           (forget-caller-end (buffers:kill! id)))
         (table-clear! faces id)
         (table-clear! margins id)
         (run-hook killed-hook id)
         #t)))

(define (insert! s)
  (let ([b (rec current)])
    (put! current (buffer-with-point
                    (buffer-with-text b (text-store:insert (buffer-text b) (buffer-point b) s))
                    (+ (buffer-point b) (string-length s))))
    (run-hook after-change-hook current (buffer-point b) "" s)))
Emacs
;; In Emacs all of this is C: buffer objects,
;; get-buffer-create, kill-buffer (which runs
;; kill-buffer-query-functions and kill-buffer-hook,
;; and deletes the buffer's overlays and local variables),
;; and insert.
(get-buffer-create "*git-status*")
(kill-buffer "*git-status*")

What to notice:

A terminal, using the process service

What it shows: your processes owned by a service, not by the plugin that shows them; output followed through a verb, every piece once and in order; restarting work at the top of the file.

Why it is built this way. Processes you start are your work: they keep running when the terminal plugin turns off, reloads or is swapped, and across a clean exit of Tuna. Three facts force the shape. A port (an open pipe) belongs to the run that opened it, and Chez closes ports nobody refers to, so a pipe cannot survive a reload inside Tuna. A worker blocked reading a pipe cannot be cancelled from inside Tuna. And output must not be lost or repeated across a reload. So each process lives in a small helper program that owns its pipes and keeps its output with offsets, and the process service offers follow!, which delivers each piece once.

Tuna · terminal/plugin.ss
(plugin terminal
  (import (buffers) (modes) (keys) (commands) (layout) (processes))
  (modes:define-major term-mode "A shell in a buffer.")
  ;; Output goes at the end, wherever point is.
  (define (follow! id)
    (processes:follow! id
      (lambda (text)
        (buffers:with id
          (buffers:goto! (buffers:size))
          (buffers:insert! text)))))
  (commands:define (shell)
    "Run a shell in the buffer *shell*."
    (let ([id (buffers:create "*shell*")])
      (unless (processes:running? id)
        (modes:set-major! id 'term-mode)
        (processes:start! id "bash -i")
        (follow! id))
      (layout:show! id)))
  (commands:define (term-send-input)
    "Send the current line to the shell."
    (processes:send! buffers:current (string-append (buffers:line) "\n")))
  (keys:bind! "RET" 'term-send-input 'term-mode)
  ;; Shells run on while the terminal is off; follow them again.
  (for-each follow!
    (filter (lambda (id) (and (eq? (modes:major id) 'term-mode) (processes:running? id)))
            (buffers:list))))
Emacs · my-shell.el
(defun my-shell ()
  "Run a shell in the buffer *shell*."
  (interactive)
  (let ((buf (get-buffer-create "*shell*")))
    (unless (get-buffer-process buf)
      (with-current-buffer buf (my-term-mode))
      (make-process :name "shell" :buffer buf :command '("bash" "-i")
                    :filter (lambda (proc s)
                              (with-current-buffer (process-buffer proc)
                                (goto-char (point-max))
                                (insert s)))))
    (pop-to-buffer buf)))

How the pieces fit:

  1. M-x shell makes *shell* with buffers:create (your buffer, which no plugin closes) and asks processes:start! for a process for it.
  2. follow! starts the service’s reader as the terminal’s job. Each piece of output arrives once, in order; the terminal inserts it at the end.
  3. When the terminal’s run ends for any reason, the reader is cancelled and released by the service, because it was started on the terminal’s receipt. The shell keeps running.
  4. The last form runs at every start: for each shell still running, follow it again. The service resumes from the offset it already delivered, so output printed while the terminal was off appears, and nothing is repeated.
  5. Killing *shell* asks “Buffer has a running process; kill it?” (the service’s kill-query-hook entry), then ends the shell, whether or not the terminal is on.

The terminal keeps no saved data: the buffer and its process (kept by the service under the buffer’s id) are the state, as in Emacs. While the terminal is off, a shell buffer shows as Fundamental mode, and its mode returns with the terminal.

The service’s side (part):

Tuna · processes/plugin.ss (part)
(define-plain-record process ([pid fixnum?] [dir string?] [command string?] [delivered fixnum?]))
(define-saved all (pmap)
  "Each process by key: a buffer id for the user's, a name for a plugin's own."
  (map-of (lambda (k p) (and (or (fixnum? k) (string? k)) (process? p)))))

(define (follow! id on-output)
  (as-caller
    (start-job read-output (vector (process-dir (entry id)) (process-delivered (entry id)))
      (lambda (piece)
        (let* ([p (entry id)] [have (process-delivered p)]
               [off (car piece)] [text (cdr piece)] [end (+ off (string-length text))])
          (when (> end have)
            (on-output (substring text (max 0 (- have off)) (string-length text)))
            (set-entry! id (process-with-delivered p end)))))
      for: id)
    (on-end (disconnect! id))))

(define (start-own name command)
  (on-caller-end (processes:stop! name))
  (if (running? name) name (launch! name command)))

(add-hook! buffers:kill-query-hook
  (lambda (id)
    (or (not (running? id)) (not (provided? 'minibuffer))
        (minibuffer:yes-or-no? "Buffer has a running process; kill it? "))))
(add-hook! buffers:killed-hook stop!)
Emacs
;; Emacs's equivalents are built in:
(add-hook 'kill-buffer-query-functions #'process-kill-buffer-query-function)
(set-process-filter proc #'my-filter)
(process-send-string proc "ls\n")

The offset is the service’s saved data, written in the same command as the insert, so nothing is lost or repeated across reloads and crashes. A process a plugin starts for itself is the other kind:

Tuna · lsp/plugin.ss
(plugin lsp
  (export server)
  (import (processes))
  (define server (processes:start-own "lsp" "clangd")))
Emacs
(defvar my-lsp-server (make-process :name "lsp" :command '("clangd")))

start-own ties the process to lsp: the same process across a reload of lsp, ended when lsp turns off.

Where Tuna differs. The Tuna version is longer than Emacs’s because it also sends input and reattaches its shells at start. In exchange the shell outlives the plugin, its reloads and a clean exit, and no output is lost or repeated. Emacs kills a buffer’s process with its plugin’s buffer at exit. The prototype used pipes, not a pseudo-terminal; a real terminal needs one, from a general library in tuna/lib/.

What it shows: fuzzy matching on a worker, each keystroke’s job replacing the last; code shared between a plugin and its worker with include; hooks another plugin offers; private state in plain variables, and what should be drawn handed to the display.

Tuna · cpop/plugin.ss
(plugin cpop
  (import (minibuffer) (completion) (buffers) (display) (commands) (keys))
  (define-worker (match in)
    (include "fuzzy.ss")
    (post (best-matches (vector-ref in 0) (vector-ref in 1) 50)))
  (define items '())
  (define selected 0)
  (define (show!) (display:show-popup! items selected))
  (define (close!) (set! items '()) (display:hide-popup!))
  (define (refresh!)
    (let ([kind (minibuffer:current-prompt)])
      (if kind
          (start-job match (vector (list->vector (completion:complete kind ""))
                                   (buffers:text (minibuffer:prompt-buffer)))
            (lambda (found)
              (when (eq? kind (minibuffer:current-prompt))
                (set! items found)
                (set! selected 0)
                (show!)))
            for: 'match)
          (close!))))
  (define (move! by)
    (unless (null? items)
      (set! selected (modulo (+ selected by) (length items)))
      (show!)))
  (commands:define (cpop-next) "Select the next candidate." (move! 1))
  (commands:define (cpop-prev) "Select the previous candidate." (move! -1))
  (commands:define (cpop-accept)
    "Answer the prompt with the selected candidate."
    (unless (null? items)
      (let ([choice (list-ref items selected)])
        (close!)
        (minibuffer:answer! choice))))
  (add-hook! minibuffer:opened-hook (lambda (kind) (refresh!)))
  (add-hook! minibuffer:closed-hook (lambda (kind) (close!)))
  (add-hook! buffers:after-change-hook
    (lambda (id . _) (when (eqv? id (minibuffer:prompt-buffer)) (refresh!))))
  (keys:bind! "C-n" 'cpop-next 'minibuffer-mode)
  (keys:bind! "C-p" 'cpop-prev 'minibuffer-mode)
  (keys:bind! "TAB" 'cpop-accept 'minibuffer-mode))
Emacs · corfu-like, the registrations only
(add-hook 'completion-at-point-functions #'my-capf nil t)
(keymap-set corfu-map "C-n" #'corfu-next)
(keymap-set corfu-map "C-p" #'corfu-previous)
(keymap-set corfu-map "TAB" #'corfu-insert)
(add-hook 'minibuffer-setup-hook #'my-popup-refresh)
(add-hook 'minibuffer-exit-hook #'my-popup-close)
Tuna · cpop/fuzzy.ss, included by the worker
;; A smaller score is a better match: the span the query's characters cover.
(define (score name q)
  (let ([n (string-length name)] [m (string-length q)])
    (let loop ([i 0] [j 0] [start #f])
      (cond
        [(fx= j m) (fx- i (or start 0))]
        [(fx= i n) #f]
        [(char=? (string-ref name i) (string-ref q j)) (loop (fx+ i 1) (fx+ j 1) (or start i))]
        [else (loop (fx+ i 1) j start)]))))
(define (best-matches names q k)
  (let loop ([i 0] [acc '()])
    (if (fx= i (vector-length names))
        (let ([sorted (list-sort (lambda (a b) (fx< (car a) (car b))) acc)])
          (map cdr (if (> (length sorted) k) (list-head sorted k) sorted)))
        (let ([s (score (vector-ref names i) q)])
          (loop (fx+ i 1) (if s (cons (cons s (vector-ref names i)) acc) acc))))))

How it works: when a prompt opens, or you type in it, refresh! collects every candidate from the completion sources on the editor thread (the worker cannot ask them) and starts match with the candidates and your input, for: 'match, so the previous keystroke’s match is cancelled. The worker includes fuzzy.ss and returns the best 50. The result is applied only if the same prompt is still open: closing a prompt starts no new job, so this check is needed here. The popup’s state is two plain variables; display:show-popup! tells the display what to draw, which also makes the frame due. Turning cpop off removes its keys, hook entries, commands and popup; the prompt it decorated stays open and can be answered. Measured in the prototype: typing stayed at about 0.1 ms per key while each key started a match of 100,000 names, each match taking about 9.5 ms on its worker.

Where Tuna differs. An Emacs completion popup (company, corfu) is a third-party package of thousands of lines, because Emacs has no popup primitive and no worker; its matching runs on the main thread and is cut short when you type. The registrations read the same.

A theme

Tuna · solarized/plugin.ss
(plugin solarized
  (import (faces))
  (faces:set! 'default '(background "#002b36" foreground "#839496"))
  (faces:set! 'highlight '(foreground "#b58900"))
  (faces:set! 'flymake-error '(underline (wave "#dc322f")))
  (faces:set! 'git-added '(foreground "#859900")))
Emacs · solarized-theme.el
(deftheme solarized)
(custom-theme-set-faces 'solarized
  '(default ((t :background "#002b36" :foreground "#839496")))
  '(highlight ((t :foreground "#b58900")))
  '(flymake-error ((t :underline (:style wave :color "#dc322f"))))
  '(git-added ((t :foreground "#859900"))))

A theme is a plugin that gives faces its looks. Turning it off returns each face to the look below it: another theme’s, or the default its plugin declared. The theme writes plain attributes, not the browser’s CSS, so it does not depend on the display.

Where Tuna differs. In Emacs only themes layer face settings; a set-face-attribute elsewhere is permanent. In Tuna every faces:set! is a layer.

The user’s init.ss

The configuration the prototype’s scenario tests used, in the approved API.

Tuna · init.ss
(use-plugin journal-store commands settings keys rope-store buffers)
(use-plugin undo limit: 5000)
(use-plugin modes tree-layout faces minibuffer completion files registers)
(use-plugin highlight processes terminal web)
(set! settings:fill-column 70)
(keys:bind! "C-c n" (lambda () (files:find-file "~/notes.txt")))
(keys:unbind! "C-z")
(redefine (english::helper n) (string-append "Yo, " n))
(advice-add dabbrev:expand filter-return (lambda (s) (string-append s "+i")))
Emacs · init.el
(use-package undo-tree :custom (undo-limit 5000))
(setq-default fill-column 70)
(keymap-global-set "C-c n" (lambda () (interactive) (find-file "~/notes.txt")))
(keymap-global-unset "C-z")
(defun english--helper (n) (concat "Yo, " n))
(advice-add 'dabbrev-expand :filter-return (lambda (s) (concat s "+i")))

How it reads:

A contract with two implementations, and a swap

What it shows: an interface declared apart from any implementation; a second plugin standing in for it; a plugin using the interface without knowing which implementation runs; swapping implementations while Tuna runs; saved data that belongs to the contract and passes to the replacement.

buffers stores each buffer’s text through the (text-store) contract. One implementation might be a plain string, another a rope (a tree of chunks, faster for big files). You choose in init.ss; buffers does not change.

Tuna · rope-store/contract.ss
(contract (text-store)
  (procedure make (s) "A text holding the string s.")
  (procedure insert (t pos s) "The text t with s inserted at pos.")
  (procedure delete (t from to) "The text t without the characters from..to.")
  (procedure kind () "A symbol naming the implementation."))
Tuna · rope-store/plugin.ss
(plugin rope-store
  ;; Texts cross the contract as strings; the rope is this provider's own business.
  (define (chunks s)
    (let loop ([i 0] [acc '()])
      (if (>= i (string-length s))
          (reverse acc)
          (loop (+ i 4096) (cons (substring s i (min (string-length s) (+ i 4096))) acc)))))
  (define (make s) (string-copy s))
  (define (insert t pos s)
    (let ([c (apply string-append (chunks t))])
      (string-append (substring c 0 pos) s (substring c pos (string-length c)))))
  (define (delete t from to) (string-append (substring t 0 from) (substring t to (string-length t))))
  (define (kind) 'rope))
Tuna · string-store/plugin.ss
(plugin string-store
  (implements (text-store))
  (define (make s) (string-copy s))
  (define (insert t pos s) (string-append (substring t 0 pos) s (substring t pos (string-length t))))
  (define (delete t from to) (string-append (substring t 0 from) (substring t to (string-length t))))
  (define (kind) 'string))
Tuna · init.ss, before and after the swap
;; before
(use-plugin rope-store)
;; after: the line replaced, then M-x reload-config
(use-plugin string-store)
Emacs
;; Emacs has one buffer text, in C, and nothing to swap.
;; Its nearest swap is a function variable:
(setq completing-read-function #'ivy-completing-read)

How it reads:

Saved data across a swap. (text-store) keeps no saved data. The minibuffer’s contract does, and declares it whole:

Tuna · minibuffer/contract.ss (part)
(contract (minibuffer)
  (saved history '() "Answers to prompts by kind, newest first."
    (list-of (pair-of symbol? (list-of string?))))
  (procedure read-string (prompt kind) "Ask the user for a string and return it."))
Tuna · ivy/plugin.ss (first lines)
(plugin ivy
  (implements (minibuffer))
  (import (buffers) (layout) (modes) (keys) (commands) (files))
  (define-option height 10 "Lines the prompt shows." fixnum?)
  (define (remember! kind answer)
    (set! history (cons (list kind answer) history))))

ivy uses history by its short name and does not declare it: the build writes the declaration from the contract into every implementation, so the shape cannot drift between them. It is stored as minibuffer:history, under the contract’s name, so after a swap from minibuffer to ivy, ivy starts with the history you built in the minibuffer.

Where Tuna differs. Emacs’s ivy-mode replaces completing-read-function and keeps its own history variables, so switching back and forth splits your history between packages. In Tuna the history is the contract’s.