# 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