# Editor
The editor is the flagship panel of the `retro` / `retra`
development environment. It is a Deft app like any other panel —
`packages/editor/src/editor.dft`, launched by the launcher and
reopenable with `[tui/launch "editor"
"packages/editor/src/editor.dft"]` — but it is the largest app in
the tree, and it ships with a layered architecture for behaviour and
extensibility:
- **Engine** (`editor.dft`) — buffers, edit primitives, undo/redo,
selection, scrolling, find/replace, completion, code folding,
rendering, menus. A passive library of `cmd-*` functions invoked
via `apply-cmd`, so every command works equally from a keybinding,
the menu, `tui/eval`, or an RPC call.
- **Driver** — the input layer. All key dispatch and modal UI
(`on-key`, `on-text`, `draw-overlay`, `status`) lives in a
*driver* implementing the `EditorDriver` contract (see
`packages/editor/src/driver.dft`). Three drivers ship by default.
- **Major modes** — per-file-type behaviour: grammar, comment
syntax, lint-on-save / lint-on-idle hooks, folding.
- **Extensions** — third-party packages contributing modes, file
associations, keymaps, menus, commands and hooks (see
[Extensions](#extensions) below).
## Buffers
The editor is multi-buffer: `Ctrl+N` opens a new buffer, `Ctrl+W`
closes the active one, `Ctrl+PageDown` / `Ctrl+PageUp` cycle between
them. Each buffer carries its own cursor, selection, undo history,
scroll position and fold state. Buffers are the unit of mode
selection — opening or switching to a buffer applies the major mode
matched to its file extension.
## Modes
"Modes" in the editor means two different things, and it is worth
keeping them apart:
| Term | What it is | Selected by |
|------|-----------|-------------|
| **Driver** | input behaviour / keybinding style (modal editing vs. chords) | the `editor/driver` config key or the driver menu; same for every buffer |
| **Major mode** | per-file-type behaviour (grammar, lint, folding) | file extension, per buffer, via `mode-for-file` |
### Input drivers
The engine never reads keys itself. Input is delegated to a driver
that implements the `EditorDriver` contract — a small deftype with
`name`, `init`, `on-key`, `on-text`, `draw-overlay`, `status` and
`on-buffer-change` fields. Drivers talk to the engine through a
`ctx` map of stable handles (`get-buf`, `set-buf!`, `apply-cmd`,
`update-state!`, …) and return their own private `driver-state` map
from every event, so modal state lives with the driver.
Three drivers ship in `packages/editor/src/drivers/`:
- **`default`** — a modern chord-based scheme: `Ctrl+S` save,
`Ctrl+Space` completion, `Alt+Up`/`Alt+Down` move line, `Ctrl+F`
find. This is the default (`:editor/driver "default"`).
- **`vi`** — modal editing with `:insert`, `:normal`, `:visual`
and `:visual-line` states, motions, operators, counts, registers,
and `:`/`/`/`?` command lines. The status line shows
`-- NORMAL --`, `-- INSERT --`, `-- VISUAL --`.
- **`emacs`** — Emacs-style bindings (`Ctrl+A`/`Ctrl+E`,
`Ctrl+K`/`Ctrl+U` line editing, `Ctrl+Space` set mark).
Switch at runtime from the driver menu (or `[cmd-set-driver "vi"]`
— it is an `@rpc` function, so other panels can call it). To make
the choice stick for future sessions, set the default in the config
app / registry — it is read at startup:
[config/set "editor/driver" "vi"]
The engine dispatches each key through the active driver's base
keymap, merged with the current major mode's keymap and any
extension keymaps (extension entries win). Chord strings are
modifier-first and lowercase: `"ctrl-s"`, `"ctrl-shift-left"`,
`"alt-d"`, `"enter"`, `"page_up"`.
### Major modes
A major mode is a small map:
%{
name :deft-mode # keyword name
grammar :deft # syntax grammar (:deft :md :zig, nil for none)
comment "#" # comment prefix for this file type
keymap %{ } # mode-specific chords merged over the driver map
on-save "deft-mode-lint" # fn name run after save
on-change "deft-mode-lint-on-idle" # fn name run debounced (800 ms) on edits
fold-ranges fold-ranges-fn # fn buf -> @{ %{ start-row end-row kind label } }
}
Four built-in modes live in `packages/editor/src/modes/`:
| Mode | Files | Grammar | Behaviour |
|------|-------|---------|-----------|
| `deft-mode` | `.dft` | deft | `editor/check` lint on save + debounced on change, structural folding of `def` blocks |
| `markdown-mode` | `.md` | markdown | heading/fence folding (`Alt+1`..`Alt+6` fold to level N), markdown-specific keymap |
| `zig-mode` | `.zig` | zig | no lint, no folds |
| `text-mode` | everything else | none | fallback — no grammar, no folds |
The mode is chosen by file extension (`mode-for-file`), falling
back to `text-mode` for anything unrecognised. Extension-registered
modes and file associations take precedence over the built-in
table, so a package can re-associate `.md` or add its own file
types. Switching modes (`apply-major-mode`) swaps the syntax
grammar and re-runs the mode's hooks.
`on-change` hooks are debounced (800 ms) by the engine and receive
the live buffer; `on-save` runs synchronously after a successful
save. The deft mode uses both to surface syntax diagnostics through
`editor/report-error-at`, which the symbols panel and error
navigation pick up.
## Syntax Highlighting
Highlighting is decoupled from the editor: the `syntax/*` modules
in `packages/stdext/src/syntax/` are standalone renderers that map
a line to a per-character array of hex colours, given a
multi-line state so strings and comments can span lines.
- `deft.syntax.dft`, `markdown.syntax.dft`, `zig.syntax.dft` —
handwritten tokeniser modules, each exporting a `highlight-line`
function.
- `syntax/registry` — the dispatch registry: `[syntax/registry/register lang fn]`
adds a language, `[syntax/registry/dispatch lang line]` renders.
- `syntax_highlight.dft` (in the editor) — the active front end:
`load-syntax-file`, `highlight-line`, `set-theme-colors`,
`set-line-state`/`get-line-state` for multi-line state,
`reset-state`. The editor keeps a per-viewport highlight cache so
unchanged lines are not re-coloured on scroll.
- `tm_highlight.dft` — a generic TextMate engine that loads a
VSCode `.tmLanguage.json` grammar at runtime via `json/parse`
and highlights with it, so third-party language support can be
dropped in without writing a tokeniser.
The editor picks the grammar from the active major mode's `grammar`
field (`select-grammar-for-file`). Theme colours — keywords,
strings, comments, numbers, types, brackets, etc. — are a single
map of hex values applied via `set-theme-colors`, so a theme is
just data:
[hl/set-theme-colors %{
:comment "#6a9955"
:keyword "#c586c0"
:string "#ce9178"
:number "#b5cea8" }]
## Configuration
Editor settings live in the global config registry (defaults from
`packages/editor/config.dft`, user overrides from
`.deft/config.dft` layered on top — edit them in the config app):
| Key | Default | Purpose |
|-----|---------|---------|
| `editor/tab-width` | `4` | Tab / indent width |
| `editor/right-margin` | `80` | Column marker / wrap reference |
| `editor/driver` | `"default"` | Input driver name (`"default"`, `"vi"`, `"emacs"`) — the registry stores the *name*, the editor resolves it to the implementation |
| `editor/bracket-guide` | `true` | Draw bracket-matching guide |
## Extensions
Editor extensions are ordinary Deft packages that declare they
implement the `EditorExtension` deftype (defined in
`stdext/types.dft`). The editor discovers, loads, and merges their
contributions at startup — the editor app reload picks up newly
installed extensions without rebuilding anything.
### The contract
An extension module is a small data + functions module:
implements EditorExtension def name "my-ext" def version "0.1.0" def modes %{ "my-mode" %{ grammar nil comment "" keymap %{ } on-save nil on-change nil fold-ranges nil } } def file-assocs %{ ".myext" "my-mode" } def keymaps %{ "my-map" %{ "ctrl-alt-k" "my-command" } } def menus %{ "My Ext" %{ :order 4 :items @{ %{ label "Do Thing" action "my-command" shortcut "" } } } } def menu-items %{ } def commands @{ "my-command" } def hooks %{ "after-save" "my-on-save" } def init "my-init"
| Field | Contribution |
|-------|--------------|
| `name`, `version` | identity |
| `modes` | `"mode-name"` → major-mode map (fn refs are name strings, resolved against the module's exports) |
| `file-assocs` | `".ext"` → mode-name; consulted before the built-in table |
| `keymaps` | `"name"` → chord → command-name map, merged over the active driver's base keymap |
| `menus` | new top-level menus: `"Title"` → `%{ :order n :items @{ %{ label action shortcut } } }` |
| `menu-items` | items appended to *existing* menus (`"File"` → `@{ ... }`) |
| `commands` | exported fn names exposed as editor commands — the name is both the command name and the def name, so menu actions, keymap chords, and `apply-cmd` all reference it. Each name is also `__refer`-ed into the editor namespace so bare `[my-command]` calls resolve |
| `hooks` | `"after-save"` / `"after-change"` → exported fn name; the fn receives the buffer |
| `init` | optional exported fn invoked once after registration, receiving the editor api map |
The runtime import is conformance-checked against the deftype —
missing contract fields raise a load-time error rather than failing
obscurely later.
### Discovery
Extensions are found in three places, lowest precedence first:
1. `/extensions//` — built-ins shipped with the
editor (`ai`, `templates`).
2. Installed packages — any `pkg/list` entry whose manifest carries
the `editor-extension` tag.
3. `.deft/editor_ext//` — dev extensions in the workspace;
these override everything.
Each source directory is a standard package directory with a
`package.dft` manifest; the manifest's `main` field names the entry
module. Loading mounts the directory under a per-extension name
(`ext-`) and runtime-imports the entry module, which must
`implements EditorExtension`.
### The editor api
`init` (if present) receives the extension api map — the extension's
only sanctioned way into editor internals:
%{
get-buf # fn -> active buffer
update-state! # fn map -> nil (merge into editor state)
apply-cmd # fn name & args -> any (invoke engine cmd-*)
find-enclosing-def # fn lines row -> code string
register-menu # fn "Title" spec -> nil (dynamic menus at init)
source-file # the extension's own source path (for scanning dirs)
}
Everything else — `spawn`, `defcoro`/`await`, `ai/*`, `fs/*`,
`sqlite/*`, stdext — is plain Deft available to any app.
### Hooks
`after-save` and `after-change` hooks receive the buffer and run
unconditionally (extension code owns its own debouncing; the engine
already debounces major-mode `on-change` lints). Multiple
extensions can register the same hook — they run in registration
order, and a failing hook is logged and skipped rather than
aborting the rest.
### Built-in examples
- **`ai`** (`packages/editor/src/extensions/ai/`) — adds a top-level
"AI" menu with `Explain Selected` and `Generate Test` commands;
demonstrates async command handlers (`defcoro` + `spawn`) and the
api (`get-buf`, `find-enclosing-def`).
- **`templates`** (`packages/editor/src/extensions/templates/`) —
adds a "Templates" menu built by scanning its own `templates/`
directory; demonstrates dynamic menu registration via
`register-menu` at init time.
Both are the reference implementation for the contract — write your
extension against `implements EditorExtension`, tag the manifest
`editor-extension`, and `deft pkg install` (or drop it in
`.deft/editor_ext/`) to try it.
## Reference
- [App Platform](app-platform) — how the editor panel runs as its own app/runtime
- [Event Dispatch](event-dispatch) — the widget tree the editor renders into
- `editor/*` stdlib functions (`editor/check`, `editor/format`,
`editor/report-error*`, `editor/semantic`, …) — see the Reference tab