# rhun extension protocol 1.1

Protocol 1.1 extends [version 1](../v1/v1.md). Existing messages keep their v1 meaning and framing.
The first line remains `hello 1 FEATURES...`; `RHUN_PROTOCOL=1.1` advertises the host's maximum
protocol revision. An extension sends new requests only after rhun agrees to their feature in
the `0 features` event before `ready`. A host that does not know a feature ignores its name.
Feature agreement examines the complete bounded hello line, including names after unknown
features. Decimal fields must fit their documented bounds without wrapping. A request whose
body length cannot be parsed keeps its v1 meaning: `error ID syntax`, only its line is dropped,
no body is read and the stream continues (three syntax errors in a row still stop the
extension). A reply whose body length cannot be parsed ends the extension stream; a bounded body
on a stray provider reply is drained before the next message is parsed.

## Command identity

The host accepts `OWNER/NAME.command` in user key bindings and command lookup. The command
palette shows this identity beside its title and shortcut, and searches it as well as the title.
An exact qualified query selects that command alone, before fuzzy matching.

The shorter `NAME.command` alias remains available when exactly one installed, non-removed
extension has that extension name. An ambiguous alias does not invoke either command, even
when only one owner currently registers that local command. A disabled extension still counts
toward ambiguity, so disabling one does not reroute a saved shortcut to another publisher.
The same rule applies to a short extension name in host controls: use
`OWNER/NAME` when more than one publisher uses the name.

Names in `0 run NAME.command DOC` events stay unchanged. Runtime `register` names are local
to the extension. Another publisher's equal extension or command name does not prevent
registration; an extension cannot register over its own manifest or runtime command.

## Feature: `commands`

An extension lists `commands` in `hello 1 ...`. When the host agrees, it may send:

| Request | Reply | Effect |
| --- | --- | --- |
| `ID unregister NAME` | `ok ID` | removes the caller's runtime command, its default shortcut, and its palette entry |

`NAME` has the same grammar as `register`: 1 to 40 lowercase ASCII letters, digits or underscores.
It names the local command, without the extension prefix. Missing commands and manifest
commands return `error ID refused not a runtime command of this extension`. Malformed names
return `syntax`. Without agreed `commands`, the request returns `unknown` and changes nothing.

Removing one command preserves the caller's other commands and all other extensions' commands.
The same name can be registered again after removal. Existing user key-binding configuration is
kept, so its binding returns if that runtime command is registered again. An open command palette
closes when its command storage changes; other menus resolve their saved command names again
when clicked.

## Feature: `workspace_watch`

Extensions can subscribe to coalesced filesystem invalidations for the current workspace.
Declare `project = read` (or `write`) in `[needs]`, request `workspace_watch` in `hello 1`,
and wait for feature agreement before subscribing. This feature grants no additional file access.

| Request | Reply | Effect |
| --- | --- | --- |
| `ID watch-workspace` | `ok ID HANDLE` | subscribes the current runtime to workspace invalidations |
| `ID unwatch-workspace HANDLE` | `ok ID` | removes that runtime's subscription |

The host sends `0 watch HANDLE changed` when it observes a possible change, and
`0 watch HANDLE rescan` when the extension should recover its view of the workspace.
Both events require reconciliation with disk. Events can combine multiple changes or repeat;
they do not enumerate operations, preserve every intermediate state, or identify changed paths.
Hidden directories are included. Extensions must not infer deletions from an incomplete scan.

Handles are positive integers through 2147483647, owned by one runtime. Repeated subscription
returns its existing handle. Another runtime cannot remove it. Stop, process exit and workspace
change retire subscriptions; subscribe again for the new runtime or workspace. Stale handles
are never reassigned. Missing permission or workspace, exhausted capacity and invalid requests
return an error without creating a subscription.

The host shares directory watches across subscribers and preserves its editor watches when a
subscription ends. It bounds subscriptions to 128 and tree coverage to 512 directories, 32768
entries and 4095-byte paths, with at most 4096 entries per directory. Incomplete coverage uses
recovery invalidations, including a two-second periodic rescan signal. A subscriber must bound
its own scanning and preserve its previous view when reconciliation fails.

Go exposes `WatchWorkspaceContext`, `UnwatchWorkspaceContext` and `ParseWorkspaceWatch`;
Rust exposes `watch_workspace`, `unwatch_workspace` and `parse_workspace_watch`;
C exposes `rx_watch_workspace`, `rx_unwatch_workspace` and `rx_parse_workspace_watch`.

## Single-provider selection

Formatting, rename, document symbols and signature help use a per-language provider preference. These are host
settings, so extensions keep their existing v1 provider declarations and request/reply messages.
When several providers match and no default exists, the host asks the user and saves the
qualified `owner/name` choice. Definitions, references and code actions keep their combined
results from matching providers.

A request waits up to five seconds for the selected extension to become ready, including time
waiting for a process slot. The host rechecks the document, selection, path, language, manifest
patterns and negotiated feature before sending it. An unavailable preference is an actionable
error, with no fallback. Explicit actions may start an idle or crashed extension; a user-stopped
or disabled extension stays stopped until the user enables or restarts it.

Headless controls for testing are `provider-set KIND LANGUAGE OWNER/NAME`,
`provider-set KIND LANGUAGE -` to clear, and `print-providers`. Kinds are `format`, `rename` and
`symbols`, and `signature`. The control command takes single-word languages; the INI file also accepts language
names containing spaces.

Automatic signature triggers do not show a chooser or an unavailable-provider error. Without a
saved preference they use a sole eligible provider only when it is running; competing idle or
starting providers make the choice ambiguous too. A shown or pending signature session keeps
its original provider for both triggers and updates, even if the saved preference changes.
Explicit Show Signature Help closes that session and resolves the current choice afresh.

## Managed saves

The optional host setting `[files] format_on_save = true` uses the existing native format
request and workspace-edit reply before writing. It adds no wire feature. The default is false;
`format_on_save_timeout` defaults to 2,000 milliseconds and accepts 250 through 30,000.

Automatic saves use the saved formatter or a sole eligible provider, including cold activation
within the deadline. They never show a chooser or restart a user-stopped provider. If formatting
is unavailable, ambiguous, fails or times out, the current buffer is saved with a warning.
The document's ID, destination and version are rechecked before applying a reply. A stale reply
is discarded and the latest buffer is saved. A formatter's edit can only target the existing
document being saved; a foreign group rejects the entire edit without opening tabs.

Save cancellation sends `0 cancel ID` for an outstanding format request and performs no write.
Closing or retargeting the document cancels its old save, and late replies are ignored. Save
Without Formatting cancels the request and writes the current text. Save-before-close and
Save All continue only after the actual file write succeeds.

## Capacity and shared scheduling

The host can run all 128 installed extension records concurrently. The earlier 16-process
limit no longer holds matching providers idle behind extensions that stay running. The editor
reserves enough event watches for their input and output pipes plus its other services. On
Linux and macOS it raises its descriptor soft limit toward 1,024 when the inherited hard limit
permits, preserving larger inherited limits. A restricted hard limit can still constrain starts.

Parsing has a shared soft budget per 16 ms window: 4 ms of work, 2,000 message steps or 1 MiB
consumed, whichever comes first. A framed header and its completed body each count as a message
step. Existing per-extension message, byte, time and flood limits still apply. A complete message
handler is atomic and may exceed the soft time/byte budget; subsequent work waits. Shared
throttling alone does not count as flooding. Suspended programs resume in rotating order, with
both their own and the shared deadlines respected.

Stop also cancels queued restarts, an idle activation and queued commands, so it cannot start later without an
explicit restart. `print-ext-budget` reports cumulative windows/yields and the highest message
and byte totals seen in a shared window for headless checks.

## Text snapshots and document events

Before answering `text DOC`, the host sends pending document events through the returned
version. An extension that replaces its cache with the reply can discard queued edit or resync
events at that version or earlier. Later edit batches begin after the snapshot, so they can be
applied without repeating changes already included in the text. Repeated text reads do not
repeat a resync event for the same version.

## Feature: `save-actions`

A provider declares `[provides] save_actions = PATTERNS`, `documents = write` in `[needs]`,
and `save-actions` in `hello 1 ...`. A matching document activates it and receives the same
opened and edits stream as other language providers. The host agrees to the feature before
ready. Protocol 1.1's complete manifest schema is [v1.1.json](https://github.com/rhun-editor/extension-protocol/blob/main/v1.1/v1.1.json); the frozen v1 schema
remains unchanged.

The opt-in `[files] code_actions_on_save` setting lists source kinds separated by spaces.
Each kind is `source` or a hierarchy such as `source.organizeImports` or `source.fixAll.eslint`.
Segments contain ASCII letters, digits, underscores or hyphens. A kind is at most 128 bytes,
with at most 32 kinds and 4,096 bytes in the setting. Empty disables it. An ancestor covers its
listed descendants, so `source source.fixAll` runs `source` once. Invalid changes preserve the
previous valid setting and show a warning. `code_actions_on_save_timeout` defaults to 2,000
milliseconds and accepts 250 through 30,000. This is one deadline for the whole source phase,
including activation. Source actions precede formatting, which has its own deadline.

Eligible matching providers run in qualified `owner/name` order for each configured kind.
Disabled, user-stopped, failed and unnegotiated providers are skipped. No matching provider is
quiet. Errors or a timeout skip the remaining source phase and continue to formatting with a
warning. A user edit makes the remaining automatic transforms stale; the current text is saved.
Save Without Formatting bypasses all automatic save participants.

| Direction | Request | Successful reply |
| --- | --- | --- |
| Host to extension | `ID save-actions DOC VERSION KIND` | `ok ID LEN` followed by a workspace edit, or `ok ID 0` |
| Extension to host | `ID save-apply SAVE_REQUEST DOC VERSION LEN` followed by a workspace edit | `ok ID NEW_VERSION` |

`SAVE_REQUEST` is the host's active source request ID. Request IDs are 1 through 2147483647,
DOC is positive, and VERSION is nonnegative. The ordinary workspace-edit limits apply.
A returned or intermediate edit may only target the existing saving document. Any foreign
group rejects the whole edit before opening files. Empty edits do not advance its version.
Intermediate edits require agreed `save-actions`, documents write authority, the same peer,
request and document, and the live version last credited by that save. A wrong or retired
invocation returns `refused`; an obsolete version returns `stale`. Ordinary `apply` does not
advance the save's credited version. After intermediate edits, the final reply uses their
credited version for its workspace edit, or an empty edit when there is nothing left to apply.

The host revokes intermediate authority before sending `0 cancel SAVE_REQUEST`, saving,
retargeting or closing the document. Late intermediate edits are rejected and late final replies
are ignored. Extensions must observe cancellation and bound their work. Go's typed handler
receives a context cancelled directly by the input reader; Rust and C handlers are serial, so
custom asynchronous dispatchers must use the cancel event's request ID to retire their work.

## Feature: `snippets`

This feature adds native editable fields to prepared UTF-8 text. It uses the existing
`complete` provider; no new manifest property is needed. A completion's INSERT contains expanded
text, and EXTRA may contain records with no following body:

```
field GROUP START END PARENT 0
```

START and END are half-open byte offsets into INSERT, on UTF-8 character boundaries. GROUP is
0 through 2147483647. The first occurrence of a positive group is its primary editable field;
other occurrences are linked mirrors and must initially contain identical bytes. Group 0 is
an optional single final selection, empty for a caret. Without it, the final caret is INSERT's end. Positive
groups are visited in increasing numerical order, with 0 last. Empty prepared text stays empty,
even when the completion has a nonempty label. Existing cursor metadata remains a fallback for
hosts that do not agree to snippets. Invalid field metadata is ignored as a whole by completion
acceptance, leaving plain INSERT and its ordinary cursor behavior.

PARENT is 0 for a root, or the one-based index of an earlier field record that is a different
positive group's primary. That primary must contain this occurrence. Mirrors cannot contain
child fields. Overlapping nonempty ranges, and empty ranges strictly inside nonempty ranges,
must follow their declared ancestor chain. Coextensive parent and child ranges are allowed;
unrelated empty fields may coincide at boundaries. Same-group occurrences cannot overlap or
nest. Each occurrence's group depends on its containing primary ancestor groups; cycles in
this group dependency graph are invalid even when the record-parent graph is acyclic.

The final marker is non-owning: it may overlap or cross other fields, creates no group
propagation dependency, and cannot be another record's parent. Its own PARENT, when nonzero,
must contain the whole final selection. Advancing past the positive groups selects this range
and finishes the innermost session. An initially nonempty final range follows edits to its
content without absorbing adjacent fields. If deletion collapses the range, later typing in
the same occurrence restores the range; typing in a different occurrence keeps its boundary.
Initially empty final markers retain their rightward caret affinity.

A primary group can offer literal values using a separate EXTRA record, before or after fields:

```
choice GROUP COUNT LEN
```

The LEN-byte body contains exactly COUNT options. Each option is `OPTION_LEN` and LF followed
by exactly OPTION_LEN UTF-8 bytes. Options can be empty, multiline, or contain NUL. GROUP must
name a positive primary group, with only one choice record per group. COUNT is 1 through 256,
and each option is at most 1 MiB. The first option must equal the initial primary bytes. Choice
records and fields are validated together. One bad choice invalidates all new snippet metadata.
Choices remain native after the provider stops. Freeform field editing remains available.
The host displays all choices in declared order. Enter or a click replaces the entire active
primary and keeps the field active; Tab also advances once. Shift+Tab closes without applying
and moves to the previous field. Escape closes choices before ending field editing. Choosing
the same bytes does not create an undo record. The Choose Value command reopens the list.
Explicit provider completion from an exactly selected active primary collapses to its end;
ordinary provider request position and replacement-range rules then apply.

Limits are 512 field records, 256 positive groups, nesting depth 32, and 4 MiB for an explicit
prepared record including framing. Existing completion-list limits still apply. A completion
on an older host keeps its plain text; that host skips unknown EXTRA records using their declared body lengths.
Extensions must check agreement before using the explicit insertion request:

```
ID snippet DOC VERSION L1 C1 L2 C2 LEN
```

The LEN-byte body is one prepared completion-item record, without a list header:

```
0 0 0 0 snippet 0 INSERT_LEN 0 EXTRA_LEN
```

Exactly INSERT_LEN text bytes and EXTRA_LEN field and choice record bytes follow the header. No label,
detail, documentation, extra edit, or unknown record is allowed in this explicit body. DOC is
a positive existing document ID, VERSION is nonnegative and exact, and the range uses positive
one-based lines and byte columns. Documents write authority and a writable document are required.
The host validates the entire request before replacing the range. Invalid explicit metadata
rejects the request without edits. Success replies `ok ID NEW_VERSION`, selects the first
positive field, and starts a session without switching to an inactive target tab. Fieldless
text inserts normally; final-only metadata selects its range and finishes immediately.

Tab and Shift+Tab move through fields, and Escape ends the session. A completion popup takes
its keys first. Accepted sessions are independent of the provider process. Inactive tabs retain
their session. Undo and redo, reloading, readonly state, non-user document mutations, or moving
the selection outside the active primary end the session. Returning the selection cannot revive
it. Each user edit and its mirror updates undo together.
Replacing or editing through a parent field retires affected nested fields. Retiring a primary
retires its whole group; external mirror text remains as ordinary text. Retiring only a mirror
leaves its primary and other mirrors live. The final marker survives field retirement.

Inserting a prepared snippet within the active primary pauses the enclosing session. Inner
field numbers are local. Tab after the inner final field returns to its final caret and resumes
the enclosing field; the next Tab continues the enclosing field order. Shift+Tab at the first
inner field stays there. Escape ends every enclosing level. Fieldless and final-only insertions
retain enclosing links without adding a navigation level. Ordinary completions also retain
those links when all their edits fit inside the active primary. Invalid completion field data
is discarded without discarding valid enclosing links. An additional edit outside that primary
ends the old session before accepting the completion. The same applies when a new positive-field
session would exceed 32 levels, 512 total occurrences, 256 total groups, or 4 MiB of retained choice payload across the stack.
The new snippet still inserts and starts independently. Malformed explicit insertion leaves
the existing stack unchanged.

Mirrors propagate in group dependency order. A nonempty replacement preserves its original
left and right boundary positions, so a surviving empty field at the old end stays after the
new text. For originally coincident empty occurrences, record order determines side affinity.
The host preflights the complete propagation, including ancestors, before mirror writes. If
the operation would insert more than 4 MiB through mirroring across all enclosing levels, it ends the session and retains
the primary user input without making a partial mirror batch. Template parsing, variables
and transforms are separate from this prepared-text wire representation.

### Read-only insertion context

After agreeing to `snippets`, an extension with documents read authority can request:

```
ID snippet-context DOC VERSION L1 C1 L2 C2
```

There is no request body. DOC must name an open document, VERSION must match exactly, and
the ordered range uses positive one-based line and byte columns at UTF-8 boundaries. All
numbers are unsigned decimal within signed 64-bit range; ID is 1 through 2147483647. The
request has exactly these eight fields. Readonly documents and inactive tabs are allowed.
It does not move the cursor, change the selection, edit the document, or grant write authority.

Success is `ok ID VERSION LEN` followed by exactly LEN bytes. The body begins:

```
context 1 LINE COL TABWIDTH SPACES CRLF SELECTED_LEN LINE_LEN PATH_LEN WORKSPACE_LEN INDENT_LEN COMMENT_LEN
```

LF ends the header. Exactly six raw UTF-8 strings follow in the listed order, without separators:

| String | Meaning |
| --- | --- |
| Selected text | The exact requested range, independent of the editor's current selection |
| Current line | The range start's line, excluding its newline |
| Path | The document's absolute display path, or empty for an untitled document |
| Workspace | The current project root, or empty without a project |
| Indentation | Leading spaces and tabs up to the range start, stopping at the first other byte |
| Line comment | The grammar's line-comment token, or empty when unavailable |

LINE and COL repeat L1 and C1. TABWIDTH is the target document's effective width, 1 through 16.
SPACES and CRLF are 0 or 1. These settings include the target document's option overrides.
The six strings may contain NUL; selected text may contain line breaks. Body framing uses
byte lengths throughout. The header is at most 256 bytes excluding LF. The complete body,
including header and strings, is at most 4 MiB. The host rejects larger snapshots with `toobig`
and invalid UTF-8 with `refused`, without truncation. Standard `syntax`, `needs`, `unknown`,
`nodoc`, `badpos`, and `stale CURRENTVERSION` errors also apply.

Pending document events precede a successful context reply. This partial snapshot does not
replace a whole-document text cache. An extension can prepare a snippet using these values
and insert it using the same version and range; an intervening edit produces `stale`.
The context contains no clipboard contents, environment values, clock or random data.


### Declared initial mirror variants

The additive `snippet-variants` feature requires `snippets` as well. With both agreed, a
prepared program's EXTRA may include `variant FIELD 0` followed by LF, with no body.
FIELD is the one-based field-record index, 1 through 512. Declarations may precede or
follow fields and choices. At most 512 distinct declarations are allowed. Each must name
an existing positive-group mirror whose initial bytes differ from its primary. Primary
fields, final selections, equal copies, duplicate declarations and absent indices are invalid.
Decimal indices accept leading zeroes; the final body-length token must be exactly `0`.
All ordinary field, UTF-8, ancestry, graph, choice and encoded-size constraints still apply.
An unequal mirror without a declaration is invalid. Choice option zero still matches the primary.

A variant changes only initial validation. After an edit to its group, the native editor copies
the primary value to ordinary mirrors using the existing undo and navigation behavior.
Declarations do not execute transforms or retain a dependency on the extension process.

Explicit insertion requires agreement and rejects invalid metadata atomically. For completion,
invalid or unnegotiated metadata is discarded together while the insertion text remains usable.
Older hosts skip the unknown zero-body record and reject the unequal field bytes, producing
the same plain-text fallback. Extension authors must check both features before emitting variants.
Plain prepared-snippet SDK validation remains strict; use the program API for declarations.

### Compiled field transforms

The additive `snippet-transforms` feature requires `snippets`. When both are agreed,
EXTRA may contain one `transforms 0 LEN` header followed by LF and exactly LEN ASCII
bytes. The reserved token is exactly `0`. The body is canonical RFC 4648 standard
Base64 with required padding. Whitespace, line breaks, the URL alphabet, misplaced
padding and nonzero unused bits are invalid. The record may precede or follow fields,
choices and variants. A second record is invalid, including an empty binding payload.

The decoded body is an SNTX program. Each binding names an existing one-based field
index, 1 through 512, in a positive group. Primaries and mirrors may both have transforms;
final selections cannot. Initial bytes still obey ordinary equality or explicit variant
rules. The SDK encoders sort bindings by field index without changing caller arrays.
The host owns validated programs independently of the extension process.

On Tab or Shift+Tab, every alive transformed occurrence in the group being left uses
its own current value. Typing still copies the primary value to ordinary mirrors.
Initial activation and cancellation do not run transforms. Shift+Tab at the first group
runs the group and reselects it; revisiting a group runs its transforms again. Outputs
are prepared together, including enclosing mirror propagation, then applied as one undo
group before selecting the destination. Failure preserves document text and selection
and ends the live session. The batch has a shared 16,777,216 work-unit budget, 50 ms
deadline and 4 MiB inserted-byte cap. Output comparison, case conversion, matching and
indentation share the budget. Undo and redo restore the complete accepted change.

The first output line keeps its whitespace. CRLF, CR and LF become LF. Each later line,
including a trailing empty line, receives the original insertion line's leading ASCII
spaces and tabs, then normalization with the document's current tab width and spaces
setting. Completion captures this prefix at its main edit before additional edits.
The captured prefix, choices and compiled programs share the existing 4 MiB aggregate
metadata budget across nested sessions.

All existing encoded-body limits include the Base64 expansion: `4 * ceil(raw_bytes / 3)`
plus the record header. Explicit insertion remains at most 4 MiB and completion lists at
most 1 MiB. SDK encoders reject an invalid explicit-sized program before emitting it.
Invalid completion metadata falls back to insertion text as a whole. A valid program
whose completion item exceeds the completion budget is omitted and marks the list
incomplete. Malformed outer completion or EXTRA framing follows ordinary list rejection.
Unnegotiated transforms reject explicit insertion and discard completion snippet metadata.
Extension authors must check both features before sending this record.

#### Portable binary programs

All integers below are unsigned little-endian 32-bit words. There is no alignment,
terminator, implicit padding, or trailing data. Program version is 1 and Unicode version
is `0x00100000` (Unicode 16.0.0). All reserved words and unused operands must be zero.
Lengths and offsets are in bytes. Validators reject an invalid program before executing it.
Use SDK compilers or import validated compiled programs rather than constructing bytes
from untrusted integers.

SNTX begins with eight words: magic `0x58544e53` (`SNTX`), version, Unicode version,
binding count, total length, and three reserved words. The count is 0 through 512 and
total length is at most 4 MiB. Each 24-byte record contains field index, absolute regex
offset, regex length, absolute replacement offset, replacement length, reserved zero.
Field indices strictly increase. Blobs start immediately after the record table and
partition the rest of the payload: regex then replacement for each binding, without gaps.
A zero-count payload is exactly the 32-byte header. SDKs omit the record for no bindings.

RNRX begins with twelve words: magic `0x58524e52` (`RNRX`), version, Unicode version,
flags, capture count, instruction count, class count, range count, repeat count, entry
instruction index, total length, reserved zero. Flags are `g=1`, `i=2`, `m=4`, `s=8`,
`u=16`, `y=32`, `d=64`; other bits are invalid. Consumption uses Unicode scalars in
both modes, while case equivalence distinguishes `i` and `iu`. Captures are at most 64,
instructions 1 through 8192, classes at most 256, ranges at most 8192 and repeats at most
256. The whole regex is at most 256 KiB. Entry and all branch targets are zero-based
instruction indices. Arrays follow the header in the order below.

Each instruction is four words: opcode, X, Y, Z. A consuming instruction falls through
to the next instruction, which must exist. Unused operands are zero.

| Opcode | Meaning and operands |
| --- | --- |
| 0 | Match/return, no operands. |
| 1 | Match scalar X. |
| 2 | Match dot, no operands. |
| 3 | Match class X. |
| 4 | Assert X: 1 start, 2 end, 3 word boundary, 4 non-boundary. |
| 5 | Save capture slot X; slots 2 through `2*(captures+1)-1`. |
| 6 | Clear Y capture slots starting at X; X and Y even, X at least 2, Y positive. |
| 7 | Split to X then Y, without fallthrough. |
| 8 | Jump to X. |
| 9 | Match capture X, 1 through capture count. |
| 10 | Initialize repeat X, then fall through. |
| 11 | Test repeat X, using body target Y and exit target Z. |
| 12 | Advance repeat X and jump to Y. |
| 13 | Call assertion at X, continue at Y. Z bit 0 negates, bit 1 searches backward. |
| 14 | Assert kind X (1 through 4), with explicit local `i`/`m` bits in Y (mask `0x06`), and Z zero. |
| 15 | Match capture X (1 through capture count), with explicit local `i` bit in Y (`0` or `2`), and Z zero. |

Instructions 14 and 15 fall through to a following instruction, which must exist.
Their Y operand replaces only the indicated flag bits for that operation; other global
flags retain their values. Local flags never persist into later instructions or branches.

Class records are `(first_range, range_count)` pairs that partition the range array in
class order, including empty classes. Ranges are inclusive `(low_scalar, high_scalar)`
pairs, sorted and disjoint within each class, with adjacent ranges merged. Scalars exclude
surrogates; no range may cross the surrogate gap. Repeat records have four words:
minimum, maximum, flags, reserved zero. Counts are at most `0x7fffffff`; maximum may
instead be `0xffffffff` for unbounded. Maximum is otherwise at least minimum. Repeat
flag bit 0 selects lazy order; other bits are invalid. Matching uses bounded backtracking,
rollback and assertion storage. Storage exhaustion fails the whole batch.

RNRF begins with eight words: magic `0x46524e52` (`RNRF`), version, Unicode version,
record count, UTF-8 blob length, total length, and two reserved words. At most 8192
24-byte records follow, then exactly the blob. Total length is at most 4 MiB. Records
contain opcode, capture index, A offset, A length, B offset, B length. Offsets are relative
to the blob. Used strings, including empty ones, partition the blob in record order,
A then B. Unused offset/length pairs are zero. Capture indices are at most `0x7fffffff`;
absent captures resolve as empty. Literal records require index zero.

| Opcode | Result | Strings used |
| --- | --- | --- |
| 0 | Literal A | A |
| 1 | Capture | none |
| 2, 3, 4 | Uppercase, lowercase, capitalize capture | none |
| 5, 6, 7, 8 | Camel, Pascal, snake, kebab case capture | none |
| 9 | A when capture is nonempty | A |
| 10 | Capture when nonempty, otherwise A | A |
| 11 | A when capture is nonempty, otherwise B | A, B |

Transforms preserve unmatched input around replacements. Global zero-width matches
advance the next search by one scalar without dropping intervening text. Sticky matching
starts at the current search offset. If there is no match and the format has a nonempty
else branch, the whole format resolves once with absent captures and replaces the input.
Otherwise a no-match transform preserves the input. Case conversion uses pinned Unicode
full mappings and contextual final sigma. Word case formats use Unicode letters and
ASCII digits.

## Feature: `command-args`

Commands learn where they were invoked from and receive a body. With `command-args` agreed,
every invocation of one of the extension's commands is

```
0 run EXT.NAME DOC FROM LEN
```

followed by exactly LEN body bytes. `EXT.NAME` and `DOC` are v1's (the command's full name and
the document it is about, 0 without one); the two trailing fields are always present, LEN is 0
without a body. Without the feature the extension receives v1's `0 run EXT.NAME DOC` and no body.

| FROM | Invoked by | DOC | Body |
| --- | --- | --- | --- |
| `palette` | the command palette, a headless `cmd`, a menu listing all commands | the active document | empty |
| `key` | a key binding | the active document | the binding's ARGS (at most 4096 bytes) |
| `menu` | a menu item, a tab-strip or view button | the target's document when it has one, else the active one | `TARGET` LF, then the item's args |
| `ref` | a command reference (code lens, tree item, status item, notification button, inlay hint part) | the reference's document (a lens, a hint), else the active one | the reference's ARGS, verbatim |

A command invoked while its extension starts is held with its FROM, document and body (at most
65536 bytes) and delivered after `ready` and the catch-up; a later invocation replaces it.

### Command references

Inside any body, the extra record

```
command NAME ARGS_LEN
```

followed by exactly ARGS_LEN bytes names one of the extension's own commands, manifest or
runtime-registered, by its local name NAME (1 to 40 of `[a-z0-9_]`, without the extension
prefix). ARGS are opaque, at most 8192 bytes, and come back verbatim as the body of
`0 run EXT.NAME DOC ref LEN` when the user activates whatever carries the reference. A record
with a bad name or more than 8192 bytes of ARGS is a syntax error of the message that carries it.
A reference whose command no longer exists when it is used shows "This command is no longer
available" and sends nothing. Code lens, tree items, status items, notification buttons, inlay
hint parts and menu-like lists use this record.

Headless controls: `cmd-args NAME ARGS...` invokes a command as a reference would (FROM `ref`,
ARGS as the body) and `cmd-ref EXT NAME [ARGS]` parses the record `command NAME LEN` + ARGS for
extension EXT and invokes it.

### Running commands: `run-command` and `call`

| Direction | Request | Reply |
| --- | --- | --- |
| extension to rhun | `ID run-command NAME LEN` + ARGS | `ok ID LEN` + RESULT, or `error ID CODE TEXT` |
| rhun to the command's owner | `ID call EXT.NAME DOC CALLER LEN` + ARGS | `ok ID LEN` + RESULT, or `error ID CODE TEXT` |

NAME is a rhun built-in (`NAME` or `rhun.NAME`) or another extension's command (`OWNER/EXT.NAME`,
or `EXT.NAME` when unambiguous, the rule of "Command identity"). An extension's own commands are
`refused its own command` (call the handler directly). ARGS are at most 65536 bytes.

- Built-ins: v1 section 8a's list with its `needs`; the result is always empty (`ok ID 0`).
  `goto_line` (`LINE` or `LINE COL`, one-based; it moves the cursor without the palette), `find`,
  `replace` (the find field's text), `find_in_files`, `quick_open` and `command_palette` (the
  palette field's text) take ARGS: one line of UTF-8 without control characters, at most 1024
  bytes. ARGS for another built-in are `syntax`. `goto_line`, `quick_open`, `command_palette` and
  `find_in_files` while the palette or a prompt is open are `busy a prompt is open`.
- Another extension's command runs only when its owner declares `NAME.callable = yes` in
  `[command-options]` and agreed `command-args`; otherwise `refused not callable`. rhun starts an
  idle owner as a command invocation would, then sends it `call`: DOC is the active document (or
  0), CALLER the caller's `OWNER/NAME`. The owner's `ok` (its RESULT, at most 1048576 bytes; more is
  `toobig`) or `error` (its code and text) goes back to the caller unchanged. rhun waits 10 seconds
  including the start; then the caller gets `error ID cancelled timeout` and the owner
  `0 cancel ID`. An owner that stops first answers `cancelled`; a caller that stops has its calls
  cancelled. A caller has at most 8 calls waiting (`busy 8 calls outstanding`). There is no user
  prompt: a callable command runs with its owner's `[needs]`, and the owner sees CALLER.
- Other errors: `refused no such command`, `refused not a command an extension may run` (v1's
  text), `refused the extension is turned off`, `needs documents=write`, `needs terminal=yes`,
  `needs terminal=run` (the task commands).

Without the agreed feature `run-command` returns `unknown`.

### `[command-options]`

Per command of the manifest, `NAME.FIELD = VALUE`; all optional:

```ini
[command-options]
copy_relative.icon = copy           # icon for menu items and tab-strip or view buttons
copy_relative.when = projectOpen    # enablement: false hides it from the palette and menus; its keys fall through
copy_relative.palette = no          # not listed in the palette (menus and keys still run it)
copy_relative.callable = yes        # other extensions may run-command it
```

A bad line or a clause that does not compile drops that one entry and is listed on the
extension's page; it never fails the manifest. The palette evaluates `when` with the focus it had
before it opened.

The extension's page has a **Contributions** block: its commands (each with the binding in
force), its bindings (`active`, `overrides built-in TITLE`, `shadowed by your [keys]`, by
`built-in` or by `OWNER/NAME`, or `refused: WHY`), menu items, status items, settings, grammars
and `[language.NAME]` keys, file icon themes, declared views, and the dropped entries. Headless:
`print-ext-page-contributions EXT` prints it as `head TEXT` and `row plain|warning|error TEXT --
DETAIL` lines.

## Keys

An extension binds keys in its manifest's `[keys]` section; the user's config file takes the same
grammar in its `[keys]` section.

```ini
[keys]
# KEYS = COMMAND [ARGS] [when CLAUSE]: KEYS one stroke or two separated by a space; a leading
# linux: / mac: / windows: limits it to that system; ARGS run to " when " (they cannot contain it)
ctrl+k ctrl+c = rhun.toggle_comment when editorFocus && !editorReadonly
ctrl+alt+l = insert_log console.log when resourceLang == JavaScript
mac:ctrl+shift+r = reveal
```

A stroke is modifiers and a key joined by `+` (`ctrl`, `shift`, `alt`, `super`; `cmd` and `meta`
are `super`; on macOS Command arrives as `ctrl`), the key a character or a key name (`Enter`,
`Tab`, `Escape`, `Backspace`, `Delete`, `Up`, `Down`, `Left`, `Right`, `Home`, `End`, `PageUp`,
`PageDown`, `Insert`, `Space`, `F1`-`F12`, or an X keysym name such as `KP_Add`). A key containing
`=` is written with its name (`ctrl+equal`), since the INI line splits at the first `=`. COMMAND in
a manifest is `NAME` (its own command, manifest or runtime-registered), `rhun.NAME` (a built-in)
or `EXT.NAME` / `OWNER/EXT.NAME` (another extension's); in the user's `[keys]` it is any command's
name, and `none` removes every binding of exactly those strokes. ARGS are the body of
`0 run EXT.NAME DOC key LEN` (at most 4096 bytes); built-ins ignore them.

Precedence: the user's `[keys]` > an extension's `[keys]` > rhun's built-in bindings > an
extension's `[commands]` default keys (v1, single strokes, which still never take a built-in's
key). Among bindings of the same rank the earlier one wins. A key goes to the best binding whose
first stroke matches, whose clause holds and whose command is enabled (`[command-options]`
`when`). When the best match starts a chord, rhun waits for the second stroke, showing
`Ctrl+K — waiting for the next key` in the status bar: the second stroke runs the chord; Escape
ends the wait quietly; another key ends it with the toast `Ctrl+K Ctrl+X is not bound`; a click, a
focus change or 5 seconds end it too. When a single-stroke binding of the same rank shares the
first stroke, the status bar says `… or pause for TITLE` and a 1-second pause runs that one. Chord
second strokes and extension bindings also match the key on the first (Latin) keyboard layout, so
`ctrl+k c` works on a Cyrillic layout. Select Color Theme is `ctrl+k ctrl+t`.

Checks, for extensions' bindings: a chord's first stroke needs ctrl, alt or super; a typing key
(a character, Enter, Tab, Backspace, Delete, Escape, the arrows, Home, End, PageUp, PageDown,
Insert) without ctrl, alt or super is accepted only when its clause has, as a top-level `&&` term,
`!textInputFocus`, `viewFocus` or `view == …`, so no extension takes typing away from a text
field; a command that does not exist is refused (until a runtime command of that name is
registered). Refused bindings stay listed with the reason. Keymaps rebind keys to commands; they
do not give modal editing (operators, counts, a kill ring), and an extension cannot take typing
keys while text has the keys.

The palette command **Keyboard Shortcuts** lists every binding with its source, clause and what
shadows it; Enter runs its command. Headless controls: `print-keys [EXT]` prints a line per
binding (`STROKES COMMAND source=user|builtin|ext:OWNER/NAME|default:OWNER/NAME rank=R [args=A]
[when=W] state=active|shadowed:SOURCE|refused:command|when|typing|prefix|strokes`), `print-chord`
prints `chord=none` or `chord=STROKE fallback=COMMAND ms=N`, and `key-latin LATIN COMBO` presses
COMBO as a layout that is not Latin reports it, with LATIN on the first layout.

## When clauses

Menu items, key bindings, status items and view actions take a `when` clause: an expression over
context keys that decides whether they apply. A clause is at most 512 bytes, compiles to at most
64 operations and nests `!` and parentheses at most 8 levels deep.

```abnf
clause   = ws or ws
or       = and *( ws "||" ws and )
and      = unary *( ws "&&" ws unary )
unary    = "!" ws unary / "(" ws or ws ")" / "true" / "false" / test
test     = key [ ws tail ] / quoted ws ( "in" / "not" 1*SP "in" ) ws list
tail     = cmp ws value / "=~" ws value / "in" ws list / "not" 1*SP "in" ws list
cmp      = "==" / "!=" / "<=" / ">=" / "<" / ">"
value    = quoted / bare                     ; always a literal, never a key
list     = key / quoted                      ; space-separated words
quoted   = "'" *( %x20-26 / %x28-7E / UTF8-2-4 ) "'" / DQUOTE *( %x20-21 / %x23-7E / UTF8-2-4 ) DQUOTE
bare     = 1*( ALPHA / DIGIT / "_" / "-" / "." / "/" / ":" / "*" / "?" / "+" / "#" / "@" / "$" )
key      = builtin / config-key / ext-key
builtin  = ALPHA *( ALPHA / DIGIT )          ; a name of the table below
config-key = "config." word "." word         ; word = 1*( %x61-7A / DIGIT / "_" )
ext-key  = [ owner "/" ] name "." local / [ owner "/" ] name ".setting." word
local    = %x61-7A *( ALPHA / DIGIT / "_" )  ; at most 40 bytes
ws       = *( SP / HTAB )
```

Every key's value is a byte string; an absent key is empty. A bare key (and the result of the
whole clause) is true when its value is non-empty and not `false` or `0`; built-in booleans are
`true` or `false`. `==` and `!=` compare bytes exactly. `<`, `<=`, `>`, `>=` compare signed
decimal integers and are false when either side is not one. `=~` matches the whole value against
a glob: `*` any run, `?` one UTF-8 character, every other byte itself, case-sensitively (there are
no regular expressions). `X in L` is true when X's value (or the quoted literal) equals one of L's
space-separated words; `not in` negates it. `!` applies to the test or parenthesized group that
follows it (`!key == v` is `!(key == v)`), comparisons bind tighter than `&&`, which binds
tighter than `||`. Quotes have no escapes.

A clause that does not compile is reported with the byte offset of the error, for example
`unknown key resourceExtt at byte 0`; the item, binding or status item it guards is dropped and
the problem is listed on the extension's page. A typo in a built-in key is an error; an unknown
key of another extension evaluates empty.

Extension keys: an extension sets `KEY` (feature `context-keys` below, or `[context]` defaults);
everyone reads it as `NAME.KEY`, or `OWNER/NAME.KEY` when two installed extensions share NAME
(an ambiguous `NAME` is a compile error). In an extension's own manifest `NAME.KEY` always means
its own key. `NAME.setting.KEY` is the extension's `[settings]` value KEY, as `0 setting` sends it,
and is read-only. `config.SECTION.KEY` is a rhun setting as written in its config file
(`config.editor.word_wrap == true`, `config.editor.line_height == 1.5`).

Built-in keys. Inside a menu, the `resource*` and `editor*` keys describe the menu's target.

| Key | Value |
| --- | --- |
| `editorFocus` | the active tab is a text document and has the keys |
| `textInputFocus` | the editor, the palette or a prompt, the find bar, the settings page or the commit message has the keys |
| `paletteFocus`, `findFocus`, `explorerFocus`, `agentsFocus`, `terminalFocus`, `scmInputFocus`, `settingsFocus` | that part has the keys |
| `findVisible`, `completionVisible`, `signatureVisible`, `snippetActive` | the find bar, the completion list, signature help show; a snippet session is live |
| `viewFocus`, `view`, `viewItem` | an extension view has the keys; its id; the focused or targeted tree item's context value |
| `activeTab` | `doc`, `image`, `settings`, `git`, `extensions`, `view`, or empty |
| `activeTabPinned`, `tabCount` | boolean; a number |
| `resourceLang` | the grammar's name with spaces as `_` (`Visual_Basic`); empty without one |
| `resourceExt`, `resourceFilename`, `resourceDirname`, `resourcePath` | `.rs` (the last suffix, with its dot), `main.rs`, its folder, the absolute path; empty for an untitled document |
| `resourceScheme` | `file` or `untitled` |
| `resourceInProject` | the path is inside the project folder |
| `editorReadonly`, `editorDirty`, `editorHasSelection`, `editorHasMultipleSelections` | of the active (or target) document |
| `editorProviders` | the provider kinds declared for it, words of `complete signature definition references symbols actions rename format` |
| `vimMode` | `normal`, `insert`, `visual`, `vline` while vim mode is on, else empty |
| `sidebarVisible`, `agentsVisible`, `terminalVisible` | that panel shows |
| `projectOpen`, `projectPath` | a project folder is open; its path |
| `gitRepo`, `gitBranch`, `gitChanges` | git runs in a work tree; its branch; the number of changed files |
| `isLinux`, `isMac`, `isWindows`, `isWayland` | the system rhun runs on |
| `remoteName`, `remoteHost` | in a remote window: `ssh`, `docker`, `podman` or `wsl`, and the host or container; empty in a local window |
| `targetKind` | in a menu: `editor`, `file`, `folder`, `root`, `tab`, `terminal`, `extension`, `view`, `item`; else empty |
| `explorerResourceIsFolder`, `explorerResourceIsRoot`, `tabKind` | of an explorer or tab menu's target |
| `debugState`, `inDebugMode`, `debugType` | the debugger's state (`inactive`, `false` and empty while no session runs) |
| `gitTrusted` | git may run in the project folder (`false` while the trust pause holds) |
| `scmProvider`, `scmResourceGroup`, `scmResourceStatus` | in a source control row's menu: `git` or the provider's `OWNER/NAME`; the row's group; its status letter (see Source control) |

The test vectors `tests/data/when-vectors.txt` (rhun) and `v1.1/when-vectors.txt` (extension
protocol repository) list clauses with their results in a fixed context and errors with their
offsets. Headless controls: `when CLAUSE` prints `when=1`, `when=0` or `when error at N: TEXT`;
`print-context [KEY]` prints every built-in key, or any one key a clause can name, as `KEY=VALUE`.

## Feature: `context-keys`

An extension lists `context-keys` in `hello 1 ...` to set keys of its own for when clauses.

| Request | Reply | Effect |
| --- | --- | --- |
| `ID set-context KEY VALUE` | `ok ID` | sets the extension's key KEY |

KEY is `local` above: a lowercase letter, then ASCII letters, digits and underscores, at most 40
bytes. VALUE is the rest of the line: one line of UTF-8 without control characters, at most 256
bytes (more is `toobig 256 bytes`); `-` removes the value set at run time, so the `[context]`
default applies again. An extension has at most 64 keys, its `[context]` defaults included (one
more is `toobig 64 keys`). A bad KEY or VALUE is `syntax`. Values set at run time are removed when
the program stops. A change takes effect at the next frame. Without the agreed feature the
request returns `unknown`.

The manifest section `[context]` gives defaults, valid before the program starts and after it
stops: `KEY = VALUE` lines with the same KEY and VALUE rules.

```ini
[context]
mode = normal
enabled = true
```

## Menus

An extension adds items to rhun's menus in its manifest; they need no feature (without
`command-args` a clicked item sends v1's `0 run EXT.NAME DOC` and no target).

```ini
[menu.explorer]
# ITEM.FIELD: command (default: ITEM), group, order, when, args, submenu
copy_relative.command = copy_relative
copy_relative.group = copy
copy_relative.order = 10
copy_relative.when = !explorerResourceIsRoot
run_make.command = make_targets
run_make.when = resourceFilename == Makefile
more.submenu = tools                # this item opens [submenu.tools]
more.group = 9_tools

[submenu.tools]
title = Demo Tools
lint.command = lint_file
lint.when = resourceExt in '.js .ts'
```

Locations: `editor` (the editor's context menu), `explorer` (an explorer row's menu and the menu
of its empty area), `tab` (a tab's menu), `terminal` (the terminal's menu), `extension` (its
title-bar button and its row in the extensions dropdown: the items replace the list of all its
commands, followed by Settings and details), and, drawn by views and the tab strip, `editor_title`,
`view_title`, `view_item`, `view_item_inline`; on source control rows `history_commit`,
`history_file`, `scm_resource` and `scm_group`, and in a version control panel's header
`scm_title` (targets under Source control below). ITEM is 1 to 40 of `[a-z0-9_]`. `command` names a
command as `[keys]` does (`NAME`, `rhun.NAME`, `EXT.NAME`); `args` are bytes added to the body;
`order` is a whole number (default 0); `when` is a clause evaluated for the menu's target (inside a
menu the `resource*` and `editor*` keys describe the target, and `targetKind`,
`explorerResourceIsFolder`, `explorerResourceIsRoot` and `tabKind` are set). An item shows when its
command exists, its `[command-options]` `when` holds and its own clause holds.

Order: rhun's own items first, then the extensions' groups sorted by name (an item without `group`
is in a group named after its extension), a line between groups; within a group by `order`, then
the extensions' order, then the item's name. Labels are the commands' titles, at most 60 bytes. A
submenu row (its title and `›`) opens its items in place of the menu; a submenu inside a submenu is
refused. Caps: 16 items per extension per location, 32 extensions' items in one menu. A bad line,
an unknown location, a clause that does not compile, a nested submenu or a 17th item drops that
entry and is listed on the extension's page; the manifest still loads.

A clicked item sends `0 run EXT.NAME DOC menu LEN` with the body `TARGET` LF and the item's `args`.
DOC is the target's document when it has one, else the active document. TARGET is one line:

| Location | TARGET |
| --- | --- |
| editor | `editor VERSION L1 C1 L2 C2`: the document's version and the selection (one-based line and byte column of its start and end) after the right click placed the cursor |
| explorer | `file PATH`, `folder PATH`, or `root PATH` for the empty area (the project folder) |
| tab | `tab doc DOC PATH` (`-` for an untitled document), `tab image PATH`, `tab view VIEW`, `tab settings`, `tab git`, `tab extensions` |
| terminal | `terminal N`, N the one-based terminal tab |
| extension | `extension` |
| editor_title | as `tab`, for the active tab |
| view_title | `view VIEW` |
| view_item, view_item_inline | `item VIEW ID`, ID the tree row's id (the rest of the line) |

`editor_title` items are icon buttons at the end of the tab strip, before which the tabs scroll:
at most 3, in menu order, then `…` opening the others as a menu. A button's icon is its command's
`[command-options]` `icon` (a built-in name or `ext:NAME`; a dot without one), hovering shows the
command's title, a click runs it at once. `view_title` and `view_item_inline` items are drawn the
same way in a view's header and on a tree row; `view_item` items make a tree row's context menu.
For these the key `view` is the view (`OWNER/NAME.KEY`) and `viewItem` the row's context value.

A command that no longer exists when its item is clicked shows "This command is no longer
available". Headless controls: `print-menu` lists an extension's rows as `LABEL [OWNER/NAME]` and
submenu rows with a trailing ` >`; `menu-pick LABEL` clicks a row; `print-contrib-problems EXT` lists the
dropped entries as `problem=[SECTION] KEY: TEXT`; `print-title-buttons` lists the tab strip's
buttons as `button EXT.NAME at=X,Y,W,H` and `more at=X,Y,W,H`; `view-item-menu VIEW ID [CONTEXT]`
opens a tree row's menu at the mouse.

## Feature: `status-items`

Status bar items: up to 8 per extension (manifest and runtime together; v1's `status` text is not
counted), left or right of the status bar, with text, icons, a tooltip, a color, a `when` clause and
a command a click runs.

| Request | Reply | Effect |
| --- | --- | --- |
| `ID status-item ITEM LEN` + body | `ok ID` | adds or replaces item ITEM (1 to 40 of `[a-z0-9_]`) |
| `ID status-remove ITEM` | `ok ID` | removes it; a manifest item is hidden until the program stops; an unknown ITEM changes nothing |

```
ALIGN PRIORITY TEXT_LEN TOOLTIP_LEN EXTRA_LEN LF
<TEXT><TOOLTIP><EXTRA>
```

ALIGN is `left` or `right`; PRIORITY −1000 to 1000, higher further left on both sides (ties: the
extensions' order, then the item's name); TEXT one line of UTF-8 without control characters, at
most 120 bytes; TOOLTIP at most 500 bytes, may hold line breaks. The three lengths must use the
body exactly. EXTRA holds records, each `NAME ARGS... LEN` + LEN bytes, unknown ones skipped:
`command NAME ARGS_LEN` + ARGS (a command reference), `color NAME 0`, `background NAME 0` (`error`
or `warning`), `when LEN` + the clause. Colors are `fg`, `muted` (the default), `accent`, `error`,
`warning`, `success`. A bad body or record is `syntax` and changes nothing; a ninth item is
`toobig 8 items`. Without the agreed feature both requests return `unknown`.

In TEXT, `$(NAME)` is an icon: a built-in name or `ext:NAME` from the manifest's `[icons]`; any other
`$(...)` stays text; at most 4 icons are drawn. Built-in icons: `chevron-right chevron-down
chevron-up folder file close search spark dot plus minus check terminal back user arrow-up
arrow-down refresh sync branch discard file-plus folder-plus sliders play stop debug bug warning
error info bell gear edit trash eye pin lock copy filter list beaker cloud more extensions`.

Together the items take at most 40 % of the status bar; the lowest priorities are dropped first,
and one item's content is cut at 240 logical pixels. Right items stand left of the updater's item,
the lowest priority rightmost; left items follow the cursor position, the highest priority
leftmost, as far as the file's items leave room. Hovering an item shows its tooltip; a click runs
its command with FROM `ref` and the reference's ARGS (an item without a command does nothing).
Runtime items are removed when the program stops.

Manifest items show while the extension is installed and enabled, running or not; their command
must be one of the extension's own commands, and a click starts the program:

```ini
[status]
# ITEM.FIELD: text tooltip command args align (left|right) priority color background when
tasks.text = $(play) Tasks
tasks.tooltip = Run a task of this project
tasks.command = run_task
tasks.align = left
tasks.priority = 20
tasks.when = projectOpen

[icons]
# NAME = PNG in the extension's folder, used as ext:NAME in status items, menus and buttons
logo = icons/logo.png
```

An `[icons]` NAME is 1 to 40 of `[a-z0-9_-]`, at most 32 per extension; the PNG at most 65536
bytes and 256 pixels a side. A PNG whose pixels are all grey is drawn as a mask in the text's
color, any other as it is. v1's `status` text is the item `_v1` (right, priority 0, muted, no
command) with or without the feature. Headless control: `print-status` prints a line per item:
`side=left|right prio=N owner=OWNER/NAME item=ITEM text=TEXT tooltip=TOOLTIP command=NAME shown=0|1
at=X,Y,W,H`.

## Language configuration

How the editor types, indents and comments a language. The same keys are read from a grammar (a
`.syn` file: built-in, `~/.config/rhun/syntax/`, an extension's `syntax/`) and from a manifest
`[language.NAME]` section for a grammar that exists (NAME is the grammar's name, `_` for a space:
`[language.Plain_Text]`). The grammar keys `comment`, `toggle_comment`, `block`, `ident` (extra
word characters) and `colon_indent` (`:$` in `indent_open`) keep their meaning and may be set in
`[language.NAME]` too.

| Key | Value | Without it (the behaviour before these keys) |
| --- | --- | --- |
| `brackets` | two-byte pairs Match brackets outlines, at most 16 | `() [] {}` |
| `autoclose` | OPEN CLOSE token pairs, space separated, each 1 to 8 bytes, at most 16 pairs | `( ) [ ] { } " " ' ' `` ` `` ` `` |
| `autoclose_code` | OPEN tokens that close only outside strings and comments | `" ' `` ` `` |
| `surround` | one-byte OPEN CLOSE pairs typing OPEN wraps a selection in | the one-byte `autoclose` pairs |
| `indent_open` | tokens opening a block; `^T` only as a line's first code token, `T$` only as its last | `{ ( [` |
| `indent_close` | tokens closing one | `} ) ]` |
| `indent_middle` | tokens closing and reopening one (`else elif`) | none |
| `indent_dedent_after` | first words after which the next line is one level less (`return pass`) | none |
| `continue` | line markers Enter continues (`/// //! - >`) | none |
| `doc_block` | `START MIDDLE END` (`/** * */`) | none |
| `fold` | `brackets` or `indent` | `brackets` |
| `fold_markers` | `START END` (`#region #endregion`), after an optional line comment token | none |

A token is at most 14 bytes; lists hold at most 32. A bad value is a warning in a grammar and a
problem on the extension's page; that key keeps its default.

**Tokens.** A line's code tokens are its words (runs of word bytes: letters, digits, `_`, every byte
of a multibyte character, and `ident`) and punctuation (the configured tokens, longest first, else
one byte), leaving out strings and comments; words compare exactly (ignoring case in a
`case = insensitive` grammar).

**Enter**, at each cursor (L the cursor's line, *before* and *after* its code tokens left and right
of the cursor):

1. In a line starting, after its indentation, with a `continue` marker T followed by a blank or the
   line's end, the new line gets the indentation and `T `; a line holding only T loses T instead
   (the list ends there). After `doc_block`'s START or MIDDLE inside a comment the new line gets
   ` MIDDLE ` (`MIDDLE ` under a MIDDLE line); Enter between START and END puts END on its own
   line.
2. *net* is +1 per `indent_open` token in *before* and −1 per `indent_close` token, a leading
   closing token not counted and a leading middle one counted +1.
3. The new line has L's indentation and one unit more when *net* > 0, one unit less when L's first
   word is in `indent_dedent_after`.
4. When *net* > 0 and *after* starts with a closing or middle token, that rest goes to a line of
   its own at L's indentation (`{|}`), unless both are brackets that are not partners (`{|]`).

**Auto-outdent.** When a line becomes its indentation and one closing or middle token (a word
followed by a character that is not a word byte, or punctuation the typed character completes, or
Enter after such a line), its indentation is what Enter after the previous non-blank line gives,
and that line's block is deeper, the line loses one unit, in the same undo step as the character.
A line aligned by hand is left alone.

**Auto-close** (Settings > Editor > Auto-close brackets) inserts CLOSE when the typed character
completes an OPEN, before a blank, a closer or the line's end; a quote pair (OPEN = CLOSE) not
next to a word byte or the same quote; `autoclose_code` pairs not inside strings and comments.
Typing a CLOSE that is next types over it; Backspace between an empty pair deletes both. **Toggle
Comment** uses `block` when the language has no line comment (HTML, XML).

**Merging**, per key: a user grammar's own value; else the first enabled extension, in discovery
order, whose `[language.NAME]` sets it; else the grammar's; else the default. An extension setting
a key another one sets is listed on both pages. Folding ranges come from a folding provider, else
indentation; `fold_markers` regions (START's line to END's, nested) are ranges too. Built-in
grammars set these keys for Lua, Python, Ruby, Crystal, Shell, Fish, Elixir, Julia, MATLAB, Vim
script, Markdown (`continue = - * >`), Rust and C# (`///`), and C-like doc comments.

```ini
[language.Lua]
continue = ---
indent_middle = else elseif
```

Headless control: `print-langconf` prints the active document's keys, a line each:
`KEY=VALUE source=default|grammar|user|ext:OWNER/NAME`.

## File icon themes

An extension ships explorer icons as `icon-themes/NAME.icons` (INI, at most 256 KB) and the PNGs
it names, in paths relative to that file:

```ini
title = Minimal
# files without an entry: this theme's file icon (default) or rhun's own icons (rhun)
fallback = default
[icons]
# NAME = PNG [COLOR] | rhun:ICON [COLOR]; with a COLOR (red orange yellow green blue purple pink
# cyan grey white accent muted) a PNG's alpha is drawn as a mask in that color of the theme
file = png/file.png muted
folder = png/folder.png accent
folder_open = png/folder-open.png accent
rust = png/rust.png
cargo = rhun:config orange
[files]
Cargo.toml = cargo
[extensions]
rs = rust
d.ts = typescript_def
[languages]
Rust = rust
[folders]
src = folder_src
[folders_open]
src = folder_src_open
```

A file's icon: `[files]` (its whole name, ignoring case), then `[extensions]` (the longest that
ends the name after a dot, ignoring case), then `[languages]` (the grammar rhun gives the name),
then `file`; with `fallback = rhun` a miss before `file` uses rhun's icon. A folder's:
`[folders]` (closed) or `[folders_open]` (open), then `folder` or `folder_open` (`folder` when
there is no `folder_open`), then rhun's folder. A PNG is at most 65536 bytes and 256 pixels a
side (32 or 64 square is best); one that cannot be read shows rhun's icon. At most 512 icons and
4096 mapping lines. SVG and VS Code icon-theme JSON are not read.

The theme's id is `OWNER/NAME/THEME` (THEME the file's name without `.icons`). The setting
`[ui] file_icons` is `rhun` (the default), `none` (no file icons) or such an id; a missing or
turned-off theme draws rhun's icons and keeps the setting. The command **Select File Icon Theme**
lists rhun, None and every theme, previews the selected one in the explorer while it is open,
and saves the choice. Headless control: `print-icon NAME [folder|folder-open]` prints
`icon=rhun:ICON color=COLOR`, `icon=theme:OWNER/NAME/THEME:ICON` or `icon=none`.

## Feature: `tests`

A test controller publishes a tree of test items; rhun keeps the tree, shows it in Testing and in
the editor gutter, starts runs the user asks for, and stores their results. The controller runs
its tests, usually by asking rhun to start a program as the user (`test-exec`).

### Manifest

```ini
[provides]
# the files this controller has tests in: it gets opened and edits for them as a provider does,
# and starts when one opens, when Testing is shown, or when a test command runs
tests = *_test.go *.rs
# run kinds it offers besides run: debug, coverage
tests.profiles = run debug coverage

[needs]
documents = read
project = read
# programs rhun may start for this extension, with the user's environment and access, during a
# test run the user started (test-exec): bare names, space separated, at most 16 of 64 bytes
tests = go cargo
```

The extension lists `tests` in `hello 1 ...` and sends nothing of this section before rhun agrees
in `0 features`. Without agreement every request below answers `error ID unknown`.

### Items

An item has an `ID` (1 to 512 bytes, no LF, CR or NUL; unique within the extension and stable
across sessions), a kind, an optional range in a file, a label and flags. One body shape carries
items everywhere:

```
PARENT_LEN N LF PARENT
N items: KIND L1 C1 L2 C2 FLAGS ID_LEN LABEL_LEN PATH_LEN EXTRA_LEN LF ID LABEL PATH EXTRA
```

- `PARENT` empty: the controller's roots. Otherwise an existing item's id.
- `KIND`: `group` (module, package, crate, folder), `file`, `suite`, `test`.
- `L1 C1 L2 C2`: all `0` for no range; otherwise one-based lines and byte columns with `L1 C1`
  at or before `L2 C2`, in `PATH`. A range needs a path.
- `FLAGS`: `-`, or a comma list of `lazy` (the item has children rhun has not been given; it asks
  with `tests-resolve` when the user expands it) and `nodebug` (no Debug for it).
- `LABEL`: one line, at most 200 bytes. `PATH`: absolute (`/…`, `C:/…`), or `#DOC` for an open
  document's file, or empty.
- `EXTRA`: extra records `NAME ARGS… LEN` LF and LEN bytes; unknown names are skipped.
  `desc LEN`: dim text after the label (one line, 200 bytes). `error LEN`: a discovery problem
  shown on the row (1000 bytes).
- At most 10,000 items and 4 MiB per body, 50,000 items per extension, 100,000 in all (`toobig`).
- A body applies whole or not at all: a malformed item is `syntax`, a bad range `badpos`, an
  unknown parent `refused unknown parent`, an existing item placed below itself `refused`.
- Ranges are lines of the file as the controller read it: when its document is open, rhun moves
  them, and the places of failure messages and coverage lines, through the user's edits since they
  came, until the controller sends them again.

### Messages

| Direction | Message | Reply | Effect |
| --- | --- | --- | --- |
| rhun → ext | `ID tests-resolve LEN` + PARENT id (empty: roots) | `ok ID LEN` + items body for that PARENT, or `error ID CODE TEXT` | sent for the roots once the controller agreed `tests`, and for a `lazy` item when it is expanded. The answer replaces PARENT's children and clears `lazy`; an error's TEXT shows on the row. rhun waits 30 s, then sends `0 cancel ID` and shows "no answer in 30 s" |
| ext → rhun | `ID test-items MODE LEN` + items body | `ok ID` | `MODE` `replace`: PARENT's children become exactly these, in this order (an item that keeps its id keeps its results, expansion and children; the others go with their descendants); `add`: adds or updates these, the others stay. An existing id elsewhere in the tree moves under PARENT |
| ext → rhun | `ID test-remove N LEN` + N ids, each ending in LF | `ok ID` | removes them and their descendants; unknown ids are ignored |
| rhun → ext | `ID test-run RUN PROFILE N LEN` + N ids, each ending in LF | `ok ID 0`, or `error ID busy TEXT` / `refused TEXT` | starts run `RUN` (unique for the session) of those items and their descendants; `N` 0: everything. `PROFILE` is `run`, `debug` or `coverage` (only those the manifest offers). An error ends the run errored and TEXT is shown |
| ext → rhun | `ID test-update RUN LEN` + results body | `ok ID` | results and output (below). `error ID stale run over` once the run ended; `refused` for another extension's run |
| ext → rhun | `ID test-end RUN` | `ok ID` | the run is over |
| rhun → ext | `0 test-cancel RUN` | | the user cancelled: stop and send `test-end` |
| ext → rhun | `ID test-coverage RUN LEN` + coverage body | `ok ID` | line coverage found by this run; any number of messages, merged |
| ext → rhun | `ID test-exec RUN LEN` + command body | `ok ID EXEC` | rhun starts a program for this run (below) |
| rhun → ext | `0 test-out EXEC STREAM LEN` + bytes | | `STREAM` `out` or `err`: what the process wrote, as read (at most 64 KiB an event, not split at lines) |
| rhun → ext | `0 test-exit EXEC CODE` | | the process ended: `CODE` its exit status, `signal N`, or `killed` (rhun stopped it: Cancel, the run ending, the extension stopping) |

Runs are concurrent: at most 4 running per extension and 8 in all; more wait, queued, until a
run ends. Two runs may include the same item; the latest report wins. After Cancel rhun sends
`test-cancel`, stops the run's processes after 2 s (SIGTERM to the process group, SIGKILL 2 s
later) and ends the run itself after 5 s. Items still queued or running when a run ends go back to
the result they had before it. When the extension stops, its runs end errored ("the extension
stopped") and their processes stop. rhun's own run ids for requests count with the other
requests it sends (v1 section 12).

### Results body

At most 1 MiB and 2,000 records:

```
N LF
N records: STATE MS ITEM_LEN EXTRA_LEN LF ITEM EXTRA
```

- `STATE`: `queued`, `started`, `passed`, `failed`, `errored`, `skipped`, or `-` (output only).
- `MS`: the duration in milliseconds, or `-`.
- `ITEM` empty means the run itself (only `-` and `errored`, for a problem that belongs to no
  item such as a build that failed). A record for an unknown item is dropped and counted.
- Extra records, in order: `message LEN` + text (8,000 bytes kept; may hold LF) starts a new
  message of this record's item (16 per item per run); `location L1 C1 L2 C2 LEN` + PATH: where
  the current message points; `expected LEN` and `actual LEN` + bytes (64 KiB each): the current
  message's values (rhun shows a diff); `output LEN` + bytes: appended to the item's output (to the
  run's for an empty ITEM).
- rhun keeps 256 KiB of output per item per run and 4 MiB per run, then notes "[output cut]".
  All runs together keep at most 32 MiB of messages, values and output; the oldest finished runs
  are dropped first (their items keep state, duration and the first message line). Ten finished
  runs are kept.
- The whole body is checked before anything applies (`syntax`, `toobig`).

### Coverage body

At most 4 MiB a message, 20,000 files and 1,000,000 ranges per run, 1,048,576 lines per file:

```
N LF
N files: PATH_LEN M LF PATH, then M records: L1 L2 COUNT LF
```

Lines `L1` to `L2`, one-based and inclusive, as the file was when the run started; `COUNT` hits
(0: not run). Per line rhun keeps "some hit" and "some miss": covered, partly covered (both) or
missed. The first coverage message of a new run replaces older coverage.

### Running programs as the user: `test-exec`

Command body, at most 256 KiB, no NUL anywhere:

```
PROGRAM_LEN CWD_LEN ARGC ENVC LF PROGRAM CWD
ARGC arguments: LEN LF bytes
ENVC variables: LEN LF NAME=VALUE
```

rhun checks, in this order, and answers with the first failure:

| Check | Error |
| --- | --- |
| the folder is not one a `rhun://` link opened and git is paused for | `refused folder not trusted` |
| the body parses; at most 1,000 arguments of 32 KiB and 64 variables named `[A-Z_][A-Z0-9_]*` | `syntax`, `toobig` |
| `PROGRAM` is a bare name (`[A-Za-z0-9._+-]`, 64 bytes) | `refused the program must be a bare name` |
| it is a word of `[needs] tests` | `needs tests=NAME` |
| the run has fewer than 4 live processes and all runs fewer than 8 | `busy` |
| the user allows this extension to run programs as the user in this project: asked once per extension and project, showing the command (the request waits for the answer; one question at a time) | `cancelled` when declined, when the run ends or the extension stops; `busy a question is open` |
| it is found on the user's PATH (the login shell's environment when rhun was not started from a terminal) | `nofile NAME not found` |
| `CWD` (empty: the project) is an existing folder inside the project after resolving links | `refused the folder is not inside the project` |

The program gets the user's environment plus the variables, the folder, input from the null device,
and a process group of its own (a job object on Windows); it is not confined. rhun reads its
output only while the extension's outgoing queue holds less than 256 KiB, so a chatty program
waits rather than growing memory. When rhun quits, test processes are killed.

Headless controls for testing: `cmd testing`, `cmd test_run_all` (and the other `test_*`
commands), `test-select OWNER/NAME [ID]`, `test-expand`/`test-collapse OWNER/NAME [ID]`,
`test-action run|debug|coverage|cancel`, `wait-tests`, `wait-test-items N`, `print-tests [all]`,
`print-test-result OWNER/NAME ID`, `print-test-runs`, `print-coverage PATH`, `print-testing` (the
Testing section's rows), `print-gutter` (the active file's test glyphs and coverage), `test-gutter
LINE [right]`, `test-action goto|failure|diff|output`.

rhun shows the results as well as the tree: a glyph at the first line of each `test` and `suite` with
a range in an open file (its state; a click runs it), each failure message with a location as a
problem with an error mark (its first line and the first lines of its expected and actual values),
and a line-number tint per line of a coverage run's files.

## Terminals and tasks

An extension can run programs in terminal tabs of rhun's panel (feature `terminals`) and publish
tasks that rhun lists, runs, and turns into problems (feature `tasks`). The programs run as the
user, outside the extension's sandbox. Each run stays visible as a tab.

### Manifest: `terminal`

```ini
[needs]
# suggest (= v1's yes): propose a line for your terminal, which runs when you press Enter
# run: also run programs in terminal tabs, outside the limits above, once allowed per project
# input: also type into the terminals it started; output: also read what they show
terminal = run
```

Each level includes the ones before it. The install dialog and the page show them in the warning
color: run is "run programs in terminal tabs, without the limits above, after you allow it in a
project", input adds "and type into them", output adds "and read what they show, including what
you type there". A 1.0 host reads `run`, `input` and `output` as `no`. With `run`, v1's `cmd` also
takes `run_task`, `run_build_task`, `run_test_task`, `rerun_last_task`, `restart_task`,
`stop_task` and `show_task` (below `run`: `needs terminal=run`).

**The approval.** The first program an extension starts in a project (`terminal-open`,
`terminal-restart`, `task-start`, `task-run`, or the user picking one of its tasks) opens a
dialog: "Let TITLE run programs in PROJECT?" with the exact command (shell quoting, the middle
elided past 96 bytes, "and N tasks it depends on" when it has dependencies) and its folder.
**Allow** runs it and remembers the extension and project: later runs do not ask. Cancel answers
`cancelled` and remembers nothing. One question at a time (`busy` meanwhile); `cancel ID` closes
an open question (`cancelled`). The extension's page lists "Runs programs as you in:" with
**Forget**. The approvals live in `extensions.ini` (`allow_run = /abs/project` lines, at most 64
per extension, `-` without a project), where the extension cannot write. `test-exec` and the
debugger's adapters ask the same question and share the approval.

### Definitions

`terminal-open`, `tasks-set` and `task-run` carry definitions: a header line, its byte fields,
then optional records.

```
TASK_ID FORM LABEL_LEN CMD_LEN CWD_LEN EXTRA_LEN LF <LABEL><CMD><CWD><EXTRA>
```

- `TASK_ID`: 1 to 64 bytes of `[a-z0-9_.-]` starting with a letter or digit, unique in a list;
  `-` in `terminal-open`.
- `FORM` `shell`: `CMD` is 1 to 4096 bytes of plain text run as `/bin/sh -c CMD` (Linux, macOS)
  or `powershell.exe -NoLogo -NoProfile -Command CMD` (Windows); empty only in `terminal-open`,
  where it starts the user's interactive shell. `FORM` `argv`: 1 to 64 arguments, each followed by
  a NUL, 32768 bytes in all; the first is the program, looked up on the program's PATH when it
  has no slash.
- `LABEL`: 1 to 128 bytes of plain text (v1's `term` rule): the tab's title and the lists' label.
- `CWD`: empty is the project folder (the home folder without one), a relative path is inside the
  project, an absolute one is used as given; it must exist.
- `EXTRA`: records `NAME ARGS LEN LF` + `LEN` bytes, in any order; unknown names are skipped.

| Record | Meaning |
| --- | --- |
| `detail LEN` | one line, at most 200 bytes, beside the label in lists (default: the command) |
| `env NAME LEN` | sets NAME (`[A-Za-z_][A-Za-z0-9_]*`, 64 bytes) to the bytes; 64 records, 32768 bytes in all |
| `unset NAME 0` | removes NAME from the environment |
| `reveal show\|focus\|silent 0` | on start: open the panel at the tab (default), also give it the keyboard, or only add the tab |
| `output 0` | send `terminal-output` for it (needs `terminal = output`) |
| `location local 0` | where it runs; any other value is `syntax`, so a future remote task never runs locally |
| `group build\|test 0` | tasks only: its group |
| `default 0` | tasks only: its group's default (the first such wins) |
| `depends sequence\|parallel LEN` | tasks only: 1 to 16 ids of the same published list, separated by spaces |
| `matcher NAME 0`, `matcher - LEN` | tasks only: a built-in or inline matcher (below); at most 4 |
| `ready LEN` | tasks only: 1 to 8 patterns, one per line: a background task, done for its dependents when a line matches |
| `busy LEN` | tasks only: 1 to 8 patterns: a new cycle began (needs `ready`): its problems clear |

The environment is the user's: rhun's own when rhun was started from a terminal, else the one the
user's login shell resolves (`$SHELL -l -i`, once, at most 5 s). It gets `TERM=xterm-256color`,
`COLORTERM=truecolor` and `TERM_PROGRAM=rhun`, then the `env` and `unset` records. The extension's
own reduced environment is never used. Each program leads a process group of its own (a job object on Windows),
signalled and killed as one.

### Feature: `terminals`

**TERM** is a positive decimal, unique for the session and never reused; a task keeps its TERM
across runs while its tab is open. An extension addresses only terminals it opened or that run its
tasks, in its current runtime; the user's own shells have no TERM. Any other is `noterm`.

| Request | Reply | Needs | Effect |
| --- | --- | --- | --- |
| `ID terminal-open LEN` + definition | `ok ID TERM` | run | asks the approval if needed (the reply waits); starts the program in a new tab |
| `ID terminal-stop TERM` | `ok ID` | run | Ctrl+C into it (SIGINT to its foreground group; `CTRL_C_EVENT` on Windows); the whole tree killed 2 s later if anything is left |
| `ID terminal-restart TERM` | `ok ID` | run | stop, wait for the end, run again in the same tab |
| `ID terminal-close TERM` | `ok ID` | run | SIGHUP and SIGTERM to its group (SIGKILL 2 s later), the tab closes |
| `ID terminal-show TERM show\|focus` | `ok ID` | run | opens the panel at the tab; `focus` gives it the keyboard |
| `ID terminal-input TERM LEN` + bytes | `ok ID` | input | the bytes as typed (LF is Enter); at most 64 KiB; `busy` while 1 MiB waits |
| `ID terminal-screen TERM` | `ok ID LEN` + body | output | `ROWS COLS CURSOR_ROW CURSOR_COL` LF, then the visible rows (trailing blanks dropped) |

| Event | When |
| --- | --- |
| `0 terminal-started TERM PID TASK_ID` | the program started (`-` for a terminal); again at each restart |
| `0 terminal-ended TERM REASON CODE ERRORS WARNINGS` | it ended: `exit`, `stopped` or `failed` (could not start; `CODE` `-`). `CODE` 0-255 or 128 + signal (Linux, macOS), the 32-bit code (Windows); the problems the run made |
| `0 terminal-output TERM DROPPED LEN` + bytes | finished lines (LF-terminated, plain UTF-8, no escapes) of a terminal opened with `output`; `DROPPED` lines left out since the last event |
| `0 terminal-closed TERM` | the tab closed; the TERM is gone |

A program ends with its leader. What the leader left in its group gets SIGHUP and SIGTERM then,
and SIGKILL 2 s later. That happens even when it let go of the terminal (Windows: the job ends).
When its extension stops (not an idle stop) its programs are stopped and their tabs stay. When rhun
quits every group gets SIGTERM, then SIGKILL after 300 ms. The tab of an ended program keeps its
output and a footer: `── LABEL exited with code N after X.Y s, 2 errors ──`.

### Feature: `tasks`

rhun agrees `tasks` only with `terminals`. A task's terminal is a TERM, so the terminal verbs work
on it. All verbs need `terminal = run`.

| Request | Reply | Effect |
| --- | --- | --- |
| `ID tasks-set LEN` + `N` LF + N definitions | `ok ID` | replaces the extension's published tasks, checked whole first: ids unique, `depends` naming ids of the list, no cycles, patterns valid |
| `ID task-start TASK_ID` | `ok ID TERM` | runs a published task after its dependencies (approval as above); a running task is left running and its TERM returned; `notask` |
| `ID task-run LEN` + definition | `ok ID TERM` | runs a task that is not published (its `depends` may name published ones); `busy` while a task with its id runs |

| Event | When |
| --- | --- |
| `0 task-ready TERM ERRORS WARNINGS` | a background task's line matched `ready` |
| `0 task-busy TERM` | a background task's line matched `busy` |

Dependencies: `sequence a b` runs a, then b, then the task; `parallel a b` runs them together (8 at
once at most). A dependency is done at exit 0 or, for a background task, when it is ready. A
non-zero exit, a failure to start or a stop fails its dependents, whose tabs say
`── not run: build failed ──`. A dependency already running is not started again: a ready
background task counts as done, a running one is waited for. Each runs in its own tab, reused when
the task runs again. Published tasks stay listed across a crash and an idle stop. They go when the
user stops, disables or removes the extension, and when the project changes.

rhun's commands: **Run Task** (every published task: recently run first, then build, test, the
rest), **Run Build Task** (ctrl+shift+B: the build group's default, else a list of the group),
**Run Test Task**, **Re-run Last Task**, **Restart Task**, **Stop Task**, **Show Task**, **Clear
Task Problems**. While tasks run the status bar shows the newest one's label (`+N` for the others);
a click is Show Task.

### Problem patterns and matchers

A **pattern** is one line of UTF-8, 1 to 512 bytes, matched against a whole line as the terminal
shows it: escapes applied, wrapped rows joined, carriage returns applied, trailing blanks dropped,
4096 bytes at most.

- Fields `{file}` `{line}` `{col}` `{endline}` `{endcol}` `{severity}` `{code}` `{message}` and
  `{_}` (any text, 0 or more characters); `{{` and `}}` are braces; any other `{...}` is `syntax`.
  Each field but `{_}` appears at most once.
- A run of spaces matches one or more spaces or tabs; every other character matches itself.
- Numbers are 1 to 9 digits. `{file}` does not start with a blank and holds no `://`.
  `{severity}` is, in any case, `error`, `fatal error`, `fatal`, `warning`, `warn`, `info`, `note`,
  `hint` or `help`; other text fails the line. Fields are lazy and backtrack; a field that ends the
  pattern takes the rest. At most 10000 steps per line and pattern.

A **matcher** (inline: at most 4096 bytes) is lines:

```
severity error            optional, for patterns without {severity}
columns chars             optional: chars (default) or bytes, how {col} counts
1 {file}:{line}:{col}: {severity}: {message}
1 {file}:{line}: {severity}: {message}
2 PATTERN                 steps 1 to 4, each with 1 to 8 alternatives
loop                      optional: the last step repeats
```

Step 1 is tried on every line; after step k matches, step k+1 must match the next line, else the
attempt drops and that line is tried as step 1. The last step makes a problem when `{file}` and
`{message}` were captured (a later step's field wins). With `loop` the next lines are tried against
the last step until one fails. A file is absolute, `~/`, or relative to the task's folder (else the
project's). Without `{col}` the column is the line's first non-blank. Without an end the problem
covers the word at the column, else one character. Messages are cut at 200 bytes, codes at 40.

| Built-in | Lines |
| --- | --- |
| `go` | `columns bytes`, `1 {file}:{line}:{col}: {message}`, `1 vet: {file}:{line}:{col}: {message}`, `1  {file}:{line}: {message}` (go test's indented failure) |
| `gcc` | `columns bytes`, `1 {file}:{line}:{col}: {severity}: {message}`, `1 {file}:{line}: {severity}: {message}` (gcc, clang) |
| `rustc` | `1 {severity}[{code}]: {message}`, `1 {severity}: {message}`, `2  --> {file}:{line}:{col}` (rustc, cargo) |
| `tsc` | `1 {file}({line},{col}): {severity} {code}: {message}`, `1 {file}:{line}:{col} - {severity} {code}: {message}` |
| `msbuild` | `1 {file}({line},{col}): {severity} {code}: {message}`, `1 {file}({line},{col},{endline},{endcol}): {severity} {code}: {message}` |

Problems go to rhun's Problems store (the Problems tab, ctrl+shift+m, and marks on open files) with
the task's label as their source. A run replaces its task's problems (a background task's at each
`busy` line); they survive the extension restarting.

### Errors and limits

`syntax` names the definition, the record and why (`syntax task 3: depends: no task "lint"`);
`toobig`; `busy` (a question is open, every tab is taken, the input backlog); `cancelled`;
`needs terminal=run|input|output`; `noterm`; `notask`.

| What | Limit |
| --- | --- |
| `tasks-set` | 1000 tasks, 1 MiB per extension, 4 MiB of definitions in all |
| terminal tabs (shells too) | 32; a new tab replaces the oldest ended task or extension tab, else `busy` |
| runs waiting or running | 32, a dependency chain 8 deep |
| problems | 2000 per run, 10000 kept in all; more are counted in the footer |
| line matching | 64 KiB waiting per tab, 2 ms per tick; lines past that are counted in the footer (`N lines were not checked for problems`) |
| `terminal-output` | 64 KiB per event, 256 KiB/s per extension, 1 MiB/s in all, none while its queue holds 512 KiB |

Headless controls for testing: `term-run [--cwd D] [--env N=V] [--unset N] [--title T] [--task]
[--silent] [--matcher NAME] [--matcher-file PATH] [--sh] -- ARGV`, `term-stop`, `term-restart`,
`term-close`, `term-select TERM`, `wait-terminal TERM running|waiting|exited|stopped|failed|ended
[RUNS]`, `print-terminals`, `print-tasks`, `print-task-status`, `print-terminal-records`,
`print-uproc`, `run-approve OWNER/NAME COMMAND`, `print-approve`, `print-allowed`,
`print-pattern`, `print-problems`, `print-problems-tab`.

## Retained views

Views whose content rhun keeps: the extension describes a tree (later a table, log, form or
Markdown document) once and changes it with patches; rhun draws it in the theme, keeps the user's
expansion, selection and scroll, and reports what the user does. They live in **places**: the
sidebar (sections stacked under the Explorer), the bottom panel (tabs after the terminal
sessions; Ctrl+J `toggle_panel` shows or hides it without starting a shell for a view tab) and
the side column (tabs beside the Agents panel; `toggle_side`).

### Manifest: `[views]`

```ini
[views]
# KEY.FIELD = VALUE; KEY is [a-z0-9_]{1,40}; at most 16 views
todo.title = TODO
# sidebar | panel | side
todo.place = sidebar
# tree; log with view-log; form with view-form; table with view-table; doc with view-doc;
# picture with views
todo.kind = tree
# yes: expanded (sidebar) or the chosen tab (panel, side) the first time it appears; default no
todo.open = yes
```

A view without a title or with an unknown place or kind is left out; the extension still loads.
A declared view takes its place when the extension is installed, running or not. Showing it (a
sidebar section expanded, a panel or side tab chosen) starts an extension that has not run, as
invoking one of its commands does. While the extension is stopped, crashed or failed the view
shows why, with a Start button. Its place item is named `OWNER/NAME.KEY`.

### Feature: `view-tree`

**Instances.** Right after the catch-up that follows `ready`, rhun opens one instance per
declared view of a supported kind, in manifest order:

```
0 view VIEW open KIND PLACE NAME
0 view VIEW visible 0|1
```

`VIEW` is a number from the same sequence as section 12a's views (never reused in a session);
`PLACE` is `sidebar`, `panel` or `side`; `NAME` is the manifest key. When the extension stops its
instances end without events; the next run opens new ones.

**Node ids** are chosen by the extension: 1 to 1000 bytes, any byte but LF and NUL, unique within
the instance, stable across patches. They are the identity of expansion, selection and events and
travel in bodies, never as fields. The root is the empty id.

**Revisions.** REV is 0 when an instance opens; each applied patch adds 1. Every user event names
the REV the user acted on.

#### Requests

| Request | Reply | Effect |
| --- | --- | --- |
| `ID vpatch VIEW LEN` + patch | `ok ID REV` | applies the patch whole, or nothing of it |
| `ID vshow NAME FOCUS` | `ok ID VIEW` | opens the declared view's place on it (the sidebar expands its section, the panel or side column chooses its tab); FOCUS `focus` also gives it the keyboard, `-` does not. `refused` for an unknown NAME or a view without an instance |
| `ID vreveal VIEW HOW LEN` + node id | `ok ID` | expands the node's ancestors and scrolls it into view; HOW is `-` or a comma list of `select` (the cursor and selection on it), `focus` (also the keyboard to the view) and `expand` (the node itself). `badpos` for an unknown node |

#### Requests from rhun

| Request | Answer |
| --- | --- |
| `ID vkids VIEW REV LEN` + node id | `ok ID LEN` + a patch (normally one `kids` op for that node), applied as `vpatch` (REV + 1); or `error ID CODE TEXT` |
| `ID vmore VIEW REV HAVE LEN` + parent id | the same, normally one `add` op; HAVE is how many children rhun has |

These are host requests with a body (v1's trailing LEN rule). A node with KIDS `closed` or `open`
whose children no patch has given is asked for when it is expanded and on screen; a "Load more"
row (`kids`/`add` with MORE 1) asks for more when it comes on screen, once at a time per parent.
At most 4 requests wait per view; others are asked when a row is drawn again. A collapse, the
node's removal or the end of the instance sends `0 cancel ID`, and a late answer is read and
dropped. An error reply, a patch that cannot apply, or 15 s without an answer turn the row into
an error row with Retry. An answer that gives no children for the node leaves it with none.

Errors: `noview` (not one of the extension's instances), `syntax op N` (the first unreadable
op, from 1), `badpos OP ID` (an op names a node the content does not have at that point, or a
new child id that is already elsewhere in the tree, or one listed twice), `toobig nodes` (more
than 100000 nodes in the instance, 400000 in all, or more than 16 MB of node bytes in the instance
or 64 MB in all as the patch would leave them: bytes of nodes it replaces or removes count out),
`unknown` without the feature.

#### Patch body

```
N LF
then N ops, each:  NAME ARGS... LEN LF <LEN bytes>
```

Every op is checked against the content as the ops before it leave it; when one fails nothing
changes and REV stays. An op may name nodes created earlier in the same patch, never nodes
removed earlier in it. An op rhun does not know is skipped.

| Op | Effect |
| --- | --- |
| `title LEN` + text | the view's title (section header, tab), at most 80 bytes, one line; empty: the manifest's |
| `badge N 0` | the count beside the title; 0 hides it |
| `message LEN` + text | a muted line above the content, at most 1000 bytes; empty clears it |
| `state STATE TEXT_LEN LEN` + TEXT + KEYS | `ready`, `loading`, `empty` or `error`; TEXT (at most 1000 bytes) shows centered for `empty` and `error`; KEYS, the rest, are up to 3 action keys |
| `busy 0\|1 0` | a thin bar at the top of the view, a spinner in its header |
| `action KEY ICON WHERE LEN` + TITLE | defines or replaces action KEY (`[a-z0-9_]{1,40}`): ICON a built-in icon name or `-`; WHERE `title` (a button in the view's header, in definition order), `node` (offered to nodes' `inline` and `menu` records), `none` (removes it); TITLE (at most 200 bytes) its tooltip or menu label. At most 32 (`toobig actions`) |
| `select-mode single\|multi 0` | multi: Ctrl+click and Ctrl+Space toggle a node, Shift+click and Shift+arrows extend from the anchor |
| `kids PARENT_LEN N MORE LEN` + PARENT + N node records | replaces PARENT's children (PARENT empty: the root). A child whose id already was PARENT's child keeps its expansion and its own children; the old children not listed go with their subtrees. The records must take the op's bytes exactly |
| `add PARENT_LEN N MORE LEN` + PARENT + N node records | appends children with new ids |
| `set LEN` + one node record | replaces an existing node's label, kind of children and extra records; its expansion stays the user's |
| `del LEN` + id | removes the node and its subtree (not the root) |
| `expand LEN` + id, `collapse LEN` + id | sets its expansion |

Until the first `state` or root `kids` the view shows "Loading…" after 300 ms. A root `kids`
makes it `ready`, whatever state it had (`loading`, `empty`, `error`).

**Node record:** `ID_LEN LABEL_LEN EXTRA_LEN KIDS` LF, then the id, the label (one line, at most
1000 bytes) and EXTRA_LEN bytes of extra records. KIDS is `leaf`, `closed` (collapsed when it
first appears) or `open` (expanded when it first appears). Extra records are `NAME ARGS... LEN`
LF + LEN bytes, in any order; unknown ones are skipped:

| Record | Meaning |
| --- | --- |
| `desc LEN` | muted text after the label, cut first when space runs out |
| `tip LEN` | the tooltip, plain text, at most 4000 bytes |
| `icon SPEC COLOR 0` | SPEC: a built-in name (`file folder folder-open search check close plus minus dot refresh branch pin puzzle more terminal spark user arrow-up arrow-down discard gear`) or `file:NAME` (the Explorer's icon and color for that file name); COLOR a color name or `-` |
| `color NAME 0` | the label's color |
| `hl FROM TO 0` | label bytes [FROM, TO) in the accent color (zero-based); at most 16 |
| `badge N 0` | a count at the row's right |
| `check STATE 0` | a checkbox, 0 or 1 |
| `open LINE COL LEN` + PATH | a click (on a leaf) or Enter opens the file there, as v1's `open` does, with its permission checks; PATH is absolute or `#DOC` |
| `command NAME ARGS_LEN` + ARGS | a click (on a leaf) or Enter runs the command reference ("Command references") |
| `inline LEN` + keys | up to 4 action keys (space separated) shown as icon buttons at the row's right while it is hovered or has the cursor (they hide its badge) |
| `menu LEN` + keys | up to 16 action keys in its context menu (right click, Shift+F10 or the Menu key) |
| `ctx LEN` | its context value (`viewItem`), `[A-Za-z0-9_.-]`, at most 100 bytes |
| `data LEN` | bytes given back as ARGS when a manifest menu command runs for the node, at most 4096 |

Color names: `fg muted accent error warning success add mod del c1` … `c8`.

A node with KIDS `closed` or `open` whose children no patch has given shows "Loading…" under it
when expanded.

#### Events

All are `0 view VIEW WHAT ...`; a node id is the body.

| Event | When |
| --- | --- |
| `open KIND PLACE NAME` | the instance exists; before any other event of VIEW |
| `visible 0\|1` | it came on or went off screen (collapsed, hidden, its tab not chosen, its panel closed); at most once a frame |
| `focus 0\|1` | the keyboard entered or left it |
| `select REV N LEN` + N ids, each ending in LF | the selection changed (by the user, or moved off a removed node to its next sibling, else the previous one, else its parent) |
| `activate REV LEN` + id | Enter or a double click on a node, or a click on a leaf, when it has no `open` or `command` record (rhun acts on those itself) |
| `expand REV LEN` + id, `collapse REV LEN` + id | the user expanded or collapsed it |
| `check REV STATE LEN` + id | the user toggled its checkbox (STATE 0 or 1); rhun shows the new state at once, a later `set` wins |
| `action REV KEY LEN` + id | an action was invoked on a node (inline button, context menu), or with an empty id from the header |
| `discarded REV WHAT LEN` + id | a user action WHAT (`action`) on a node removed meanwhile (a menu open while a patch removed its node) was dropped; REV is the content the user acted on |

Keys while a tree has the keyboard: Up/Down, Page Up/Down, Home/End, Left (collapse, else to the
parent), Right (expand, else to the first child), Enter (activate), Space (toggle the checkbox,
else activate), Shift+F10 (menu), typing jumps to the next label starting with what was typed,
Escape gives the keyboard back to the editor. The context keys `viewFocus` and `view`
(`OWNER/NAME.KEY`) follow the keyboard.

Headless controls: `print-sidebar`, `print-panel-tabs`, `print-side-tabs` (the places' items:
`NAME title=T at=X,Y size=WxH badge=N [collapsed] [hidden] [visible] [chosen] [focus]`),
`print-views` (also the retained instances: `view=ID kind=K place=P name=N rev=R state=S busy=B
title=T badge=N visible=V nodes=N [message=M] [progress=P|-]`), `print-view NAME|VIEW` (`at=X,Y size=WxH
scroll=S rowh=H`, then a line per row: `row I y=Y d=DEPTH open|closed|leaf|loading|more|error
[cur] [check=0|1] id=ID label=L [desc=D] [badge=N] [icon=I] [hl=F-T,...]`), `view-show NAME`
and `view-focus NAME` (NAME a place item's name or a declared view's key), `places-demo` (a
built-in section and panel tab drawn with the same widget). `print-panel-tabs` adds `tab NAME x=X
w=W` after an item whose tab is drawn in the panel's strip.

A view's keyboard goes back to the editor when its column or panel is hidden (`toggle_side`,
`toggle_sidebar`, `toggle_panel`). An instance that opens in a place that already has the
keyboard hears `focus 1` after `open` and `visible`.

### Feature: `view-log`

With `view-log` (and `view-tree`, which carries the instances, patches and events) a declared view
may be `kind = log`: append-only lines with a severity, drawn in the editor's monospaced face and
kept in a ring of the last 10000 lines and 4 MB. A log is `ready` once given lines.

| Op | Effect |
| --- | --- |
| `log N LEN` + N lines | appends N lines, each `SEV TEXT` LF; SEV `trace`, `debug`, `info`, `warn`, `error` or `-` (none). TEXT keeps SGR color sequences (`ESC [ ... m`), which color it; other escape sequences and control characters are dropped and a tab becomes spaces to the next multiple of 4 |
| `clear 0` | removes every line |

`title`, `badge`, `message`, `state`, `busy` and `action` work as for a tree; tree ops (`kids`,
`add`, `set`, `del`, `expand`, `collapse`, `select-mode`) on a log, and `log`/`clear` on a tree,
are `noview an op for another kind of view`.

The log follows its end while the last line is in view (End returns to following, scrolling up
leaves it). The header has Find, Clear and Follow buttons before the extension's title actions.
Clear removes the lines and sends `0 view VIEW clear`. Keys: Up/Down (the cursor line), Shift
extends the selection, Page Up/Down, Home, End, Ctrl+F find (Enter or F3 the next line holding
the text, Shift+Enter the previous one, Escape closes the field), Ctrl+C copies the selected
lines without colors.

Headless: `print-log NAME|VIEW` (`lines=N dropped=D follow=0|1 cur=C anchor=A match=M find=TEXT`,
then `line K SEV TEXT` per line in view, K counted from the first line ever appended).

### Feature: `view-form`

With `view-form` (and `view-tree`) a declared view may be `kind = form` (or a `vtab form`): rows of
fields the user fills in. The form is ready once given fields.

| Op | Effect |
| --- | --- |
| `form N LEN` + N field records | the whole form: fields of a listed KEY keep what the user typed (by the rule below), the others go; the order is the listed one |
| `field LEN` + one field record | adds or updates that KEY |

**Field record:** `KEY TYPE ROW WEIGHT LABEL_LEN EXTRA_LEN` LF, then the label and EXTRA_LEN bytes
of records. KEY is 1 to 64 of `[A-Za-z0-9_.-]`; TYPE `text`, `area` (several lines), `number`,
`check`, `select` (a dropdown of options), `button` or `label` (static text); fields with the same
ROW (1-200) share a line in order, each taking WEIGHT (1-12) shares of it. The label (at most 200
bytes, one line) shows above an input, right of a check, on a button. Records:

| Record | Meaning |
| --- | --- |
| `value SEQ LEN` | the value (a check's `yes` or `no`; a select's option); SEQ `-` or a number, see below. At most 64 KB, an area's 1 MB |
| `placeholder LEN` | shown in an empty input |
| `options LEN` | a select's options, one a line |
| `lines N 0` | an area's height in lines (default 4) |
| `range MIN MAX STEP 0` | a number's Up and Down step by STEP within MIN and MAX (decimals allowed) |
| `msg SEVERITY LEN` | a line under the field in the severity's color (`info`, `warning`, `error`) |
| `disabled 0`, `primary 0`, `mono 0`, `password 0` | greyed out and not editable; an accented button; (reserved: the code face, masking) |

**Ownership.** The user owns a field's value: each field counts the user's edits (its sequence, 0
when the field is made), sent with each change. A `value SEQ` record replaces the value only when
SEQ is the field's sequence (the extension saw the last edit; `-` is 0), and at the field's
creation; otherwise the value part is dropped because the user's newer edit is on its way. The
caret and selection stay the user's. Messages, options and the other records always apply.

Events: `change REV KEY SEQ LEN` + value (the user edited it; SEQ its sequence now), `press REV
KEY` (a button: a click, Enter or Space), `submit REV KEY` (Enter in a text or number field,
Ctrl+Enter in an area). Keys: Tab and Shift+Tab move between the fields that take the keyboard; a
text field's editing keys; Up and Down step a number; Space toggles a check and presses a button;
Enter, Space or Down open a select's options; Escape gives the keyboard back. A paste goes into the
focused field. Errors: `syntax op N` for a record rhun cannot read, `toobig form` past 200 fields
or a value past its cap. Headless: `print-form NAME|VIEW` (`field KEY type=T row=R seq=S on=0|1
[focus] [disabled] at=X,Y size=WxH [msg=SEV:TEXT] value=V` per field in display order).

### Feature: `view-table`

With `view-table` (and `view-tree`) a declared view may be `kind = table` (or a `vtab table`): a
tree with a header row of columns. Every tree op and node record applies; these are added:

| Op or record | Effect |
| --- | --- |
| `columns N LEN` + N column records (op) | the columns (1-16), replacing the last ones; a column the user widened keeps its width while the count stays |
| `sorted COL DIR 0` (op) | the header's arrow: COL from 1 (`-` none), DIR `asc`, `desc` or `-`; the extension sorted the rows itself |
| `cell LEN` + text (node record) | the next column's text, from the second column on |

**Column record:** `ALIGN WIDTH SORT TITLE_LEN` LF + the title: ALIGN `left` or `right`, WIDTH a
minimum in characters (0-200; 0 takes a share of the width the others leave), SORT `sort` (a click
on its header sorts by it) or `-`; the title is one line of at most 200 bytes. Column 1 shows the
node's indent, chevron, icon, label and badge; later columns its `cell` records in order, cut with
"…" (the pointer over a cut cell shows the whole text as its tooltip). The dividers between headers
drag to resize.

**Sorting.** A click on a sortable header sorts by that column, and again the other way. When no
node has more children to page (no `MORE`), rhun sorts each node's children itself: stable, by
value when both cells read as numbers (`[-+]digits[.digits]`), else by bytes with ASCII letters
folded; the order stays across later patches until the extension sends `sorted`. With paged
children anywhere, rhun shows the arrow and sends `sort REV COL asc|desc` instead: resend the rows
with `kids` (and `sorted`). Errors: `syntax op N` for a column record rhun cannot read; `noview`
for `columns` or `sorted` on another kind of view. Headless: `print-view` starts with `columns=
TITLE:ALIGN:MIN:SORT:X:W|… sorted=COL asc|desc [owner]`, and each row adds `cells=A|B|…`.

### Feature: `view-doc`

With `view-doc` (and `view-tree`) a declared view may be `kind = doc` (or a `vtab doc`): Markdown
rendered natively (the subset below). It is ready once given a document.

| Op | Effect |
| --- | --- |
| `doc LEN` + Markdown | the document, replacing the last one (at most 1 MB: `toobig doc`) |
| `base LEN` + folder | an absolute folder (`/…` or `D:/…`) relative links name |
| `follow DOC 0` | while document DOC (its v1 number) is the active one, the block holding the editor's top line stays at the view's top, and a double click on a block moves the editor to its line; `0` stops |

**Markdown:** ATX and setext headings, paragraphs (soft wrap, hard breaks), bullet and ordered lists
nested by indentation, task items (drawn, read-only), block quotes, fenced and indented code
(colored with rhun's grammars by the info string's first word), thematic breaks, pipe tables,
reference definitions, hidden `<!-- -->`; emphasis (oblique), strong, strikethrough, code spans,
inline, reference and auto links, bare http(s) URLs, images, backslash escapes and the entities
`&amp; &lt; &gt; &quot; &apos; &nbsp; &#N; &#xN;`. Other HTML shows as text.

**Images:** `img:IMG` (a `view-images` handle of the extension) or a path relative to the base (a
PNG, JPEG or GIF of at most 8 MB and 4096 px a side, 32 MB decoded per document), on lines of
their own, scaled down to the width and never up past the window's scale. Anything else, remote
images included, shows its alt text as a link: rhun never fetches it.

**Links:** `rhun://` opens in rhun, `http://` and `https://` in the user's browser, a path relative
to the base naming a file (`#fragment` and `?query` left out, `%XX` decoded) in a tab, `#slug` scrolls
to the heading with that slug (GitHub's rule); anything else is the event `link REV LEN` + target.
Keys: arrows, Page Up, Page Down, Space (Shift: up), Home and End scroll; Tab and Shift+Tab move
between links, Enter follows one. Dragging selects text (a double click a word), Ctrl+C copies it,
Ctrl+A selects it all. A rich-document body for other features (the notebooks' `rich`
outputs) holds only `doc` and `base` records, after an optional count line. Headless:
`print-docview NAME|VIEW` (`doc scroll=S height=H follow=N line=E focus=L`, then the layout's
`block`, `item` and `link` lines).

### Feature: `markdown`

With `markdown` agreed, rhun draws an extension's node tooltips (`tip` records) and notification
messages as Markdown (the subset `view-doc` renders; tooltips at most 70 columns wide and half the
window high, messages six lines high). In messages, links rhun follows itself work and others are
ignored; a tooltip closes when the pointer leaves its row or presses, so links in tooltips are
shown but not followed.
A message's text can be selected by dragging and copied (Copy with the pointer over the card).
The language-features design adopts it for hover bodies, completion documentation and signature
help. Headless, `print-tip` still shows the source, and `print-notes` marks such a card `markdown`.

### Views in tabs of their own: `vtab`

| Request | Reply | Effect |
| --- | --- | --- |
| `ID vtab KIND TITLE` | `ok ID VIEW` | a retained view of KIND (`tree`, or `log` with `view-log`) in an editor tab of its own, shown at once; TITLE is the rest of the line, at most 80 bytes. `unknown` for a kind not agreed; `toobig 8 views` past 8 tabs and section 12a views together |
| `ID unview VIEW` | `ok ID` | closes such a tab without an event (as for section 12a's views) |

The instance hears `0 view VIEW open KIND tab -` and `visible`, is filled with `vpatch` like a
declared view, and ends with `0 view VIEW closed` when the user closes its tab (Ctrl+W, its close
button). It ends with the extension.

### Picture views

A declared view of `kind = picture` (with `views` and `view-tree` agreed) is a section 12a
picture in a sidebar section, a panel tab or a side tab: the instance opens as `0 view VIEW open
picture PLACE NAME`, then takes 12a's `draw VIEW` and sends 12a's events (`size`, `metrics`,
pointer events) as a tab view does; `title`, `badge`, `message` and `busy` patches apply, other
ops are `noview`. It does not count among the extension's 8 tab views.

### Feature: `view-keys`

With `view-keys` (and `views`) a picture (a tab view, a picture view, a custom editor's view)
takes the keyboard when pressed and sends `0 view VIEW focus 0|1`. While it has the keyboard:

| Event | When |
| --- | --- |
| `key MODS NAME` | a key no binding takes; MODS `-` or `ctrl+shift+alt+super` in that order as `[keys]` writes them; NAME `Return`, `Tab`, `Escape`, `Backspace`, `Delete`, `Up`, `Down`, `Left`, `Right`, `Home`, `End`, `PageUp`, `PageDown`, `Insert`, `Space`, `F1`-`F12`, or the character (letters lowercase) |
| `text LEN` + UTF-8 | a typed character (without Ctrl, Alt or Super), after its `key` |
| `paste LEN` + text | the paste command (Ctrl+V) |
| `selectall` | the select-all command (Ctrl+A) |

rhun's bound commands keep their keys, except that copy and cut ask the picture: `ID vcopy VIEW
copy|cut` (a host request) answered `ok ID LEN` + text puts the text on the clipboard (`ok ID 0`
leaves it; an answer after 1 s is dropped). Pointer events `down`, `up`, `click`, `drag` and
`wheel` gain a last MODS field, and `dblclick X Y MODS` follows the second press of a double
click.

### Feature: `view-images`

| Request | Reply | Effect |
| --- | --- | --- |
| `ID vimage LEN` + bytes | `ok ID IMG` | a PNG, JPEG or GIF decoded once (at most 8 MB and 4096 pixels a side; anything else is `syntax`) |
| `ID unimage IMG` | `ok ID` | lets it go (`noview no such image`) |

A picture draws one with the shape `image X Y W H IMG`, scaled into the box. Images belong to the
extension and go when it stops; at most 64 and 128 MB decoded per extension (`toobig 64 images or
128 MB`). `view-images` needs `views`.

## Notifications and progress

### Feature: `notify`

| Request | Reply | Effect |
| --- | --- | --- |
| `ID notify SEVERITY LEN` + body | `ok ID NOTE` | a card at the window's bottom right, above the status bar; SEVERITY `info`, `warning` or `error` |
| `ID notify-set NOTE LEN` + body | `ok ID` | replaces the card's content |
| `ID unnotify NOTE` | `ok ID` | closes it without an event |

Body: `MSG_LEN EXTRA_LEN` LF, then the message (at most 2000 bytes, cut; LF breaks a line, long
lines wrap, 6 lines at most) and EXTRA_LEN bytes of records `NAME ARGS... LEN` LF + bytes, in any
order, unknown ones skipped:

| Record | Meaning |
| --- | --- |
| `detail LEN` | a muted line under the message (with the extension's title), at most 1000 bytes |
| `sticky 0` | it stays until closed |
| `button LEN` + title | a button (at most 3; more are left out), at most 60 bytes |
| `command NAME ARGS_LEN` + ARGS | the command reference of the button before it ("Command references") |

A button with a command reference runs it; one without sends `0 notify NOTE pressed N` (N from
1). Either way the card closes. The close button, a timeout and `clear_notifications` send `0
notify NOTE closed`. Info cards close after 8 s and warnings after 12 s; the pointer over a card
holds it; errors, cards with buttons and sticky ones stay. Newest at the bottom; 4 show, "+N
more" over them. A NOTE that is gone (closed by the user, timed out) is `noview`. 16 cards per
extension (`toobig 16 notifications`). The cards take the pointer before
what is under them, and leave it to menus, the palette and dialogs.

Keyboard: `focus_notifications` gives the keyboard to the newest card: Up/Down between cards,
Left/Right between its buttons, Enter presses the button (on none: closes), Delete closes,
Escape gives the keyboard back. `clear_notifications` closes every card. v1's `toast` is
unchanged.

### Feature: `progress`

| Request | Reply | Effect |
| --- | --- | --- |
| `ID progress WHERE CANCEL LEN` + title | `ok ID P` | WHERE `status` (the status bar: spinner, title, percent; a click opens its card), `note` (a card with a bar), or `view:VIEW` (a bar at the top of one of the extension's own views and a spinner in its header); CANCEL `cancel` (a Cancel button) or `-`. The title is one line, at most 200 bytes |
| `ID progress-set P PERCENT LEN` + message | `ok ID` | PERCENT 0-100, or `-` for indeterminate (a moving bar); the message at most 200 bytes |
| `ID progress-end P` | `ok ID` | removes it |

Nothing shows for progress that ends within 500 ms. Cancel sends `0 progress P cancel`; rhun
shows "Cancelling…" and removes the progress 10 s later unless `progress-end` comes first. A P
that is gone is `noview`. 8 per extension (`toobig 8 progress`). A bad WHERE or CANCEL, a
view not the extension's, or an empty or multi-line title is `syntax`. Without the feature each
of these requests is `unknown`.

Built-in code uses the same (notify.s): `notify_add(ext|0, SEV_*, msg, len)` → id,
`notify_button(id, title, fn(ctx, id), ctx)`, `notify_close(id)`; `progress_begin(ext|0,
NPW_STATUS|NPW_NOTE, title, cancel_fn|0, ctx)` → id, `progress_update(id, percent|-1, msg,
len)`, `progress_end(id)`.

Headless: `print-notes` (a line per card, `note=NOTE sev=S buttons=A|B msg=M [detail=D] [stays]
[at=X,Y size=WxH]` where `at` is present for the 4 drawn, then a line per progress, `progress=P
where=W percent=N|- cancel=0|1|2 shown=0|1 title=T [msg=M]`, cancel 2 while cancelling) and
`note-press N B|close|cancel` (card N among those drawn, 0 the oldest: button B, its close
button, or a progress card's Cancel).

## Quick input

### Feature: `quick-input`

A multi-step quick pick or validated input, drawn as the palette is: a title row (the title, "2/3",
a Back button, title buttons, "K selected"), the field (masked for passwords, a spinner while
busy), a prompt or a message under it, and items (icon, label with the matched characters in
accent, a muted description, a second detail line, item buttons on the active or hovered row,
separators with labels, checkboxes in multi-select). One session shows at a time.

| Request | Reply | Effect |
| --- | --- | --- |
| `ID quick LEN` + step body | `ok ID Q` | opens it; `busy` while one, the palette or a prompt shows |
| `ID quick-set Q LEN` + step body | `ok ID` | changes what its records name, atomically; the rest stays |
| `ID quick-close Q` | `ok ID` | closes it (no `hide`) |

**Step body**: records, all optional (unknown ones are skipped): `title LEN`; `step N M 0` (`0 0`
hides it); `placeholder LEN`; `prompt LEN` (the line under the field); `value SEQ LEN` (the field's
text, set at `quick`, when SEQ is the field's sequence, or after an `accept`: the next step);
`mode pick|input 0`; `flags F… 0`, the flags in effect from now, F among `multi back busy nofilter
match-desc match-detail password` (`-` clears them); `msg SEQ SEVERITY LEN` (`info`, `warning`,
`error`: shown while SEQ is the field's sequence; an error refuses Enter); `buttons N LEN` + N
records `KEY ICON TIP_LEN` LF + tooltip (title buttons, at most 8; ICON a built-in name or `-`);
`items N LEN` + N records `ID_LEN LABEL_LEN EXTRA_LEN KIND` LF + ID, LABEL, EXTRA (KIND `item`, or
`sep`: a separator labelled LABEL), item records `desc LEN`, `detail LEN`, `icon SPEC COLOR 0`,
`picked 0`, `always 0` (shown whatever the filter), `hl FROM TO 0` (highlighted bytes, with
`nofilter`), `button KEY ICON LEN` + tooltip (at most 4); `active LEN` + item id. Items keep their
checks and the active item by id across `quick-set`. rhun filters with the palette's fuzzy match on
the label (with the flags, the description and detail too), keeping the items' order; a separator
shows when an item under it does. Limits: 10000 items, 2 MB, 8 buttons, 4 item buttons (`toobig`).

**Events** (`0 quick Q …`): `value SEQ LEN` + text (the user edited the field; SEQ counts edits
from 1; at most once a frame), `active SEQ LEN` + item id (the active item moved), `accept SEQ N
LEN` + N item ids each ending in LF, then the field's text (Enter or a click on an item: the checked
items in multi-select, else the active one; none in input mode), `back SEQ` (Alt+Left or the Back
button), `button SEQ KEY LEN` + item id (empty for a title button), `hide` (Escape, a click
outside, the palette opening: Q is gone). rhun never closes a session on accept: Enter does nothing
more until a `quick-set` or `quick-close` comes. Keys: Up, Down, Page Up and Page Down move the
active item; Space toggles it in multi-select; the field's editing keys. Headless: `print-quick`
(`quick Q title=T step=N/M mode=pick|input flags=F seq=S waiting=0|1 focus=0|1 [msg=SEV:TEXT]
[prompt=P] at=X,Y size=WxH field=TEXT`, a `row I id=ID label=L [sep] [checked] [active] [at=X,Y]`
line per item shown, `button KEY at=X,Y` per title button, `back at=X,Y`; `quick none` without one).

## Language features

The features of this part complete the language providers of v1 section 12 (12b to 12h). They
keep v1's framing: rhun's requests are `ID VERB ...` answered once by `ok ID LEN` + body or by
`error ID CODE TEXT` (an error is an empty answer unless a feature says otherwise); rhun may send
`0 cancel ID` and drops late answers. Positions are one-based lines and one-based byte columns.

### Manifest keys

`[provides]` takes these keys besides v1's, with v1's file patterns. A key only makes rhun ask
when the extension also agreed the feature named beside it.

| Key | Feature | Asked for |
| --- | --- | --- |
| `hover` | `hover-query` | the hover card |
| `declaration`, `type_definition`, `implementation` | `navigation` | Go to Declaration, Type Definition, Implementation |
| `selection_ranges` | `selection-ranges` | Expand and Shrink Selection |
| `call_hierarchy`, `type_hierarchy` | `hierarchy` | call and type hierarchies |
| `workspace_symbols` | `workspace-symbols` | Go to Symbol in Project, while a matching document is open |
| `range_format`, `type_format` | `partial-format` | Format Selection, formatting as you type |
| `type_format.triggers` | `partial-format` | characters that ask for on-type formatting; the word `newline` stands for Enter |
| `document_links` | `document-links` | links under ctrl+hover |

Like v1's keys, each makes rhun send `opened` and `edits` for the matching files, and the first
matching document to open starts the extension.

### Queries: kinds, time and caps

rhun tracks every request it sends a provider with its kind's limits. **Show** is when rhun
presents what has arrived (and keeps updating as more comes); **Limit** is the hard limit after
which rhun stops waiting: it sends `0 cancel ID`, counts the request expired and treats it as an
empty answer; a progress report (below) moves the limit to now + its base, up to the ceiling.
**Answer** is the cap of one answer or one part: over it, the answer or part is dropped and
counted. **Query** is the cap of a request with all its parts: rhun keeps what it has, answers
the part that crossed it `toobig`, withdraws the request and finishes with what came.

| Kind (request) | Parts | Show | Limit (base / ceiling) | Answer | Query |
| --- | --- | --- | --- | --- | --- |
| `complete` | yes | as answers come | the list's session | 4096 items, 1 MiB | 8192 items, 2 MiB |
| `complete-resolve` | no | on arrival | 1 s | 64 KiB | |
| `signature`, `signatures` | no | on arrival | the help's session | 64 KiB | |
| `hover-query` | no | 1 s or all answered | 3 s | 64 KiB | 256 KiB |
| `definition`, `declaration`, `type-definition` | yes | all answered | 5 s / 30 s | 10000, 1 MiB | 10000, 4 MiB |
| `implementation` | yes | first part | 10 s / 60 s | 10000, 1 MiB | 20000, 8 MiB |
| `references` | yes | first part | 10 s / 120 s | 10000, 1 MiB | 100000, 16 MiB |
| `symbols` | no | on arrival | 5 s | 10000, 1 MiB | |
| `workspace-symbols` | yes | 300 ms | 5 s / 30 s | 10000, 1 MiB | 20000, 4 MiB |
| `selection-ranges` | no | on arrival | 1 s | 64 chains of 64, 64 KiB | |
| `call-hierarchy`, `type-hierarchy` | no | on arrival | 5 s | 64 items, 256 KiB | |
| `calls-in`, `calls-out`, `supertypes`, `subtypes` | yes | first part | 5 s / 30 s | 1000, 1 MiB | 10000, 4 MiB |
| `code-actions` (invoked) | no | all answered | 3 s | 1000, 4 MiB | |
| `code-actions` (auto) | no | all answered | 1 s | 1000, 4 MiB | |
| `code-action-resolve` | no | on arrival | 5 s / 60 s | 4 MiB | |
| `prepare-rename` | no | on arrival | 2 s | 4 KiB | |
| `rename` | no | on arrival | 15 s / 120 s | v1's edit caps | |
| `format`, `range-format` | no | on arrival | 10 s / 30 s | v1's edit caps | |
| `type-format` | no | on arrival | 500 ms | 64 KiB | |
| `document-links` | no | on arrival | 2 s | 10000, 1 MiB | |

rhun waits on at most 64 requests across all extensions (v1 section 7); automatic requests
(hover, the lightbulb, links, resolve, the Outline) are not sent while 48 are out. Request ids
are never reused. A second answer to an id is read and dropped. An answer after rhun withdrew the
request (`0 cancel ID`) or after it expired is read, counted late and dropped: cancellation is
advisory for the extension and authoritative for rhun.

rhun counts, per extension and kind for the session: asked, cold (sent within 5 s of the
program's start), answered, empty (no bytes, or a count of 0), failed (an error reply), expired,
late, dropped (an answer or part that broke its kind's rules or caps; also counted answered),
cancelled (withdrawn by rhun), and the average and slowest time. The extension's page in
Settings > Extensions lists each kind it provides with its file patterns in force and these
numbers; a kind averaging over a second, or failing or expiring on more than 10% of at least 10
requests, is drawn in the warning color. `print-langstats [OWNER/NAME]` prints them for tests,
`print-query` the requests rhun waits on.

### Feature: `query-stream`

An extension that agreed `query-stream` may stream long answers and report progress on rhun's
pending requests to it:

| Request | Reply |
| --- | --- |
| `ID query-part REQ LEN` + body | `ok ID`, or `error ID cancelled`, `toobig`, `syntax`, `refused` |
| `ID query-progress REQ PERCENT LEN` + text | `ok ID`, or `error ID cancelled`, `syntax` |

- `REQ` is the id of one of rhun's pending requests to this extension. A part's body is in the
  grammar of REQ's answer (the count line and that many records); parts append in order, and the
  final `ok REQ LEN` holds the rest (`0` LF when nothing remains). Only kinds with Parts "yes"
  take parts.
- `cancelled`: REQ is unknown, answered, expired or withdrawn; stop working on it. `toobig`: the
  Query cap is reached; rhun keeps what it has and withdraws REQ. `syntax`: this part broke the
  rules or the Answer cap and was dropped (counted); the query goes on. `refused`: REQ's kind
  takes no parts.
- `PERCENT` is `0` to `100`, or `-` for none. The text is one line of at most 200 bytes and may be
  empty (`LEN` 0); a body of more than 4096 bytes ends the extension, as for other capped bodies.
  A report extends REQ's hard limit as the table says.
- Waiting for each reply before sending the next part is the flow control.
- Without the feature agreed, both requests are `error ID unknown` (their bodies are read first).

### Feature: `provider-scope`

```
→ ID provides KEY VALUES
← ok ID | error ID syntax | error ID refused
```

`KEY` is a `[provides]` key (`definition`, `complete.triggers`, `type_format.triggers`, …; an
unknown key is `syntax`). `VALUES`, the rest of the line, replace what rhun uses for KEY until
the process stops: `*` restores the manifest's value, `-` withdraws the kind, anything else is a
space-separated subset of the manifest's own values, each written exactly as there (`refused`
otherwise). Narrowing never widens, and activation and the `opened`/`edits` subscriptions follow
the manifest. Which documents are asked, `editorProviders`, the single-provider chooser and F2's
choice between renaming a symbol and renaming the file follow the values in force. A language
server that registers capabilities dynamically narrows its manifest's union this way.
`print-provides OWNER/NAME` prints the values in force.

### Feature: `complete-items`

Completion items (v1 12b) gain EXTRA records; rhun reads them only from an extension that agreed
`complete-items`, and skips records it does not know as before.

| Record | Meaning |
| --- | --- |
| `resolve EDITS LEN` + data | rhun may ask `complete-resolve` with these bytes (at most 16 KiB). `EDITS` 1: the answer may add edits, so accepting the item waits for it |
| `replace L1 C1 L2 C2 0` | the replace range; the header's range is then the insert range. Both start at the same position (a replace range that starts elsewhere is ignored) |
| `commit LEN` + characters | 1 to 16 printable ASCII characters; typing one while the item is selected accepts it first, then types the character |
| `filter LEN` + text | one line, matched and ranked instead of the label |
| `suffix LEN` + text | one line drawn after the label in the muted color (a signature, a module) |
| `preselect 0` | selected first when it passes the filter |
| `deprecated 0` | drawn struck through |
| `adjust 0` | the lines of INSERT after the first get the insertion line's leading whitespace (not applied to items with snippet fields) |
| `after signature 0`, `after complete 0` | after accepting, rhun asks for signature help, or for a new list |

```
← ID complete-resolve DOC VERSION LEN    + the item's resolve data
→ ok ID LEN   + EXTRA records only: doc LEN, detail LEN, edit L1 C1 L2 C2 LEN, after …
```

rhun resolves the selected item 60 ms after it becomes selected (the first item when the list
shows), one at a time; selecting another item withdraws the resolve still out. `VERSION` is the
version the list was asked at. In the answer, `detail` replaces the item's DETAIL (one line),
`doc` is its documentation, `edit` records join its edits (v1 12b's rules: positions at the
list's version, no overlap with the item's range or other edits), `after` as above. Field, choice
and cursor records (feature `snippets`) are not allowed in a resolve answer: such an answer is
dropped. An item with `resolve 1` accepted before its answer came waits up to 500 ms (its row
shows `…`); the answer, the next key or the timeout accepts it, without the edits that did not
come. Accepting is one undo step.

Enter and Tab insert over the insert range; with `[editor] complete_replace = yes` over the
replace range; shift+Enter uses the other one. `[editor] complete_commit_characters = no` turns
commit characters off. A list may come in parts (`query-stream`): the first part of an answer
replaces what the provider said before in the session, later parts and the final answer add to
it.

Headless control: `print-complete` prints v1's two lines (see v1 section 10). While the list has
items from a provider that agreed `complete-items`, a line between them describes the selected
item: `selected label=L detail=D doc=TEXT resolved=0|1 replace=0|1 commit=CHARS suffix=S
deprecated=0|1 waiting=0|1` (the documentation's line breaks as `\n`). A list from v1 providers
alone prints exactly as v1 says.

### Feature: `signature-list`

With `signature-list` agreed, rhun sends `ID signatures DOC VERSION LINE COL KIND CHAR` (as v1's
`signature`, which it then does not send) and the answer holds every overload:

```
N ACTIVE LF
then N records: FROM TO SIG_LEN DOC_LEN LF <SIG bytes><DOC bytes>
```

`N` is 1 to 32, `ACTIVE` the zero-based overload to show first (another value shows the first),
`FROM TO` the active parameter's bytes in that signature or `-1 -1`, SIG one line, DOC plain text
(Markdown with `markdown` agreed). `ok ID 0` hides the help; a body that breaks the rules hides
it too. With several overloads the help shows `2/3` before the signature, and Up and Down switch
between them while it shows.

### Feature: `hover-query`

```
← ID hover-query DOC VERSION LINE COL KIND -        KIND pointer | invoked
→ ok ID LEN + "L1 C1 L2 C2" LF + text
→ ok ID 0                                           (nothing to say)
```

Key `hover` in `[provides]`. rhun asks every provider that agreed `hover-query` and whose `hover`
patterns match the document when the pointer has rested on text for half a second (`pointer`),
and for Show Hover (ctrl+i) at the cursor (`invoked`). `L1 C1 L2 C2` is the range the text
describes (`0 0 0 0` for the position only); the card stays while the pointer is on that range
or on the card. The text is at most 64 KiB, plain text (Markdown with `markdown` agreed); a body
that breaks the rules is dropped.

The hover card shows, separated by rules: the diagnostics at the position (severity and the
whole message), each provider's text in rank order (the most exact matching `hover` pattern
first: a file name, then `*.ext`, then the rest; then by owner/name; identical texts once), a
decoration's tip, and a v1 `hover` answer. It appears
when every provider answered, or 1 s after the request with what came (later answers are added
until 3 s). It is at most 80 columns or 60% of the window wide and half the window high, the
wheel scrolls it, and it stays while the pointer moves between the word and the card (400 ms
grace). A provider's text is rendered as Markdown when its extension agreed `markdown` and
shown as it is otherwise (a URL in it stays text); the diagnostics are plain. Dragging selects text, a link click
follows it. Code lines do not wrap: a card with a longer one scrolls sideways (Shift+wheel or a
sideways wheel). ctrl+i again focuses it: Up/Down and PageUp/PageDown scroll, Left/Right scroll
sideways, ctrl+a selects all, ctrl+c copies the selection (plain text's indentation as ordinary
spaces), Escape closes it. The completion list hides it. An extension whose `hover-query` request was sent does not get v1's
`0 hover` event for that position; v1 subscribers that did not agree the feature still do.
`hover-at LINE COL` rests the pointer on a position in tests and `print-hover-card` prints the
card (with `sideways=N/MAX` while a line is wider than the card and `selected: TEXT` while text
is selected).

### Feature: `navigation`

```
← ID declaration DOC VERSION LINE COL invoked -
← ID type-definition DOC VERSION LINE COL invoked -
← ID implementation DOC VERSION LINE COL invoked -
→ ok ID LEN + a locations body (v1 12d)
```

Keys `declaration`, `type_definition`, `implementation` in `[provides]`. The commands
`declaration` (Go to Declaration), `type_definition` (Go to Type Definition) and
`implementation` (Go to Implementation, ctrl+F12) ask every matching provider and merge their
answers as Go to Definition does: one location opens, several make a list. Parts are allowed
(`query-stream`).

The peek commands (`peek_definition` alt+F12, `peek_declaration`, `peek_type_definition`,
`peek_implementation`, `peek_references`) ask the same requests and show the locations in the
peek view under the cursor's line instead of moving there. Find All References (shift+F12) puts
v1 `references` answers into the Results panel tab: file rows with counts, then `line:col
preview` rows, growing as parts arrive; F4 and shift+F4 open the next and previous location.
While providers search, the tab's title shows the slowest one's last `query-progress` report;
Cancel Language Query (`cancel_query`), or Escape in the editor while a query or a hierarchy's
roots are still asked for, withdraws the requests still out and keeps what came.
Neither needs a new feature.

### Feature: `selection-ranges`

```
← ID selection-ranges DOC VERSION N LEN     + N lines "LINE COL" (each selection's cursor)
→ ok ID LEN + N groups in order: "M" LF + M lines "L1 C1 L2 C2", innermost first
```

Key `selection_ranges`. `N` and `M` are 1 to 64. Each range of a group must contain the one
before it (an equal one is skipped); a group that breaks this keeps its valid prefix, and an
answer with fewer groups than N gives the missing selections nothing. Expand Selection
(shift+alt+Right) grows each selection to the smallest range of its group that strictly contains
it; Shrink Selection (shift+alt+Left) goes back; any other change forgets the chain. Without a
provider, or without an answer within a second, rhun expands by word, the line's text without
its indentation, the whole line and the document.

### Feature: `hierarchy`

An **item** is a header line and its bytes:

```
KIND L1 C1 L2 C2 S1 SC1 S2 SC2 PATH_LEN NAME_LEN DETAIL_LEN DATA_LEN LF <PATH><NAME><DETAIL><DATA>
```

`KIND` is a symbol kind word (12e), `L1 C1 L2 C2` the whole symbol and `S1 SC1 S2 SC2` its name
(inside it), `PATH` as in 12d's locations, `NAME` one line, `DETAIL` one line or empty, `DATA` up
to 16 KiB that rhun returns verbatim when it asks about the item. A **call** is an item whose
header has one more number `R` (1 to 1000) followed, after the item's bytes, by R lines `L1 C1 L2
C2`: the call sites (for `calls-in` in the caller's file, for `calls-out` in the file of the item
asked about).

```
← ID call-hierarchy DOC VERSION LINE COL invoked -   → "N" LF + N items (the roots, N ≤ 64)
← ID calls-in LEN + DATA                             → "N" LF + N calls
← ID calls-out LEN + DATA                            → "N" LF + N calls
← ID type-hierarchy DOC VERSION LINE COL invoked -   → "N" LF + N items (the roots, N ≤ 64)
← ID supertypes LEN + DATA                           → "N" LF + N items
← ID subtypes LEN + DATA                             → "N" LF + N items
```

Keys `call_hierarchy` and `type_hierarchy`. `calls-in`, `calls-out`, `supertypes` and `subtypes`
go to the provider of the item and may come in parts (`query-stream`). Show Call Hierarchy
(shift+alt+h) asks for the roots at the cursor and shows their incoming calls in the Results
panel; a title action switches to outgoing calls; expanding a row asks about that item. Show Type
Hierarchy does the same with supertypes and subtypes. A call row reads `name  detail  file:line`
with `(3 calls)` when there are several call sites; Enter opens the first.

### Feature: `workspace-symbols`

```
← ID workspace-symbols MAX LEN + the query (one line, at most 256 bytes, may be empty)
→ ok ID LEN + "N" LF + N records: L1 C1 L2 C2 KIND PATH_LEN NAME_LEN DETAIL_LEN LF + PATH + NAME + DETAIL
```

Key `workspace_symbols`: rhun asks every running extension that agreed the feature and declares
the key (its patterns name the files it knows; a document matching them starts it, as for the
other keys). `MAX` is a hint (500). `PATH` as in 12d, `DETAIL` the container (one line, may be
empty). Parts are allowed. Go to Symbol in Project (ctrl+t, or `#` typed first in Go to File)
asks at once with the empty query and again 120 ms after each change, withdrawing the requests
before; the palette filters and ranks the names as it does any list (at most 5000 are kept), and
the older results stay until the new query's first answer. Choosing one opens its file with the
cursor at its start.

### Feature: `prepare-rename`

```
← ID prepare-rename DOC VERSION LINE COL invoked -
→ ok ID LEN + "L1 C1 L2 C2 PLEN" LF + placeholder (one line, at most 200 bytes)
→ ok ID 0                     (nothing to say: the word at the cursor, as without the feature)
→ error ID refused TEXT       (nothing can be renamed there)
```

With the feature agreed by the document's rename provider, F2 asks it before the prompt opens
(2 s): the answer's range is selected and the prompt is filled with its placeholder; a refusal
shows TEXT and neither the prompt nor a file rename follows; an empty answer, another error or
no answer in time opens the prompt with the word at the cursor. The rename itself is v1 12g's.

### Feature: `partial-format`

```
← ID range-format DOC VERSION L1 C1 L2 C2 TAB_WIDTH SPACES TRIM FINAL
← ID type-format DOC VERSION LINE COL CHAR TAB_WIDTH SPACES TRIM FINAL
→ ok ID LEN + a workspace edit for DOC (v1 12g)
```

Keys `range_format`, `type_format`, `type_format.triggers` (characters, space separated; the word
`newline` stands for Enter). `TRIM` and `FINAL` are the document's `trim_trailing_whitespace` and
`final_newline` in force (`yes` or `no`). `CHAR` is the typed character or `newline`; `LINE COL`
the cursor after it. A group naming another document rejects the whole edit, as in managed saves.
Both use the language's formatter choice (`[providers] format.LANG`).

- Format Selection (`format_selection`), and Format Document (shift+alt+f) when there is a
  selection and an extension declares `range_format` for the document, send `range-format` for
  the selection.
- With `[editor] format_on_type = yes` (default no), typing one of the formatter's triggers sends
  `type-format` to the saved or sole running formatter (never a chooser or a cold start). The edit
  applies as its own undo step only if the document is still at VERSION; otherwise, or after
  500 ms, it is dropped.
- With `[editor] format_on_paste = yes` (default no), the lines a paste touched are sent as a
  `range-format`, under the same rules.

### Feature: `code-actions`

```
← ID code-actions DOC VERSION L1 C1 L2 C2 TRIGGER KINDS DIAGS
→ ok ID LEN + "N" LF + N records: KIND TITLE_LEN EDIT_LEN EXTRA_LEN LF + TITLE + EDIT + EXTRA
← ID code-action-resolve DOC VERSION LEN + the action's resolve data
→ ok ID LEN + "EDIT_LEN EXTRA_LEN" LF + EDIT + EXTRA (command records only)
```

A provider that agreed `code-actions` gets these instead of v1's `actions` (and needs no
`actions` feature; the manifest key stays `actions`).

- `TRIGGER` is `invoked` (the user asked) or `auto` (the lightbulb). `KINDS` is `-` or a
  comma-separated list of hierarchical kinds (`quickfix`, `refactor.extract`,
  `source.organizeImports`; save-actions' kind grammar, at most 32). An action matches a kind it
  equals or descends from (`source.organizeImports.go` matches `source.organizeImports`); rhun
  drops answers that match none of KINDS.
- `DIAGS` is `-` or the comma-separated `id`s (feature `file-diagnostics`) of this extension's
  diagnostics intersecting the range, at most 64.
- The range: the selection; without one, when this extension has diagnostics on the cursor's
  line, the diagnostic containing the cursor, else the line's first (a server often fixes only a
  diagnostic it is given, and the fix may lie outside the cursor's range); else the cursor.
- `KIND` in an answer is hierarchical (at most 128 bytes). `EDIT` is a workspace edit (v1 12g) or
  empty. EXTRA records: `preferred 0`; `disabled LEN` + the reason (one line, at most 1000
  bytes); `resolve LEN` + data (at most 16 KiB); `command NAME ARGS_LEN` + ARGS (the command
  reference of `command-args`, ARGS at most 8192); `fixes LEN` + space-separated diagnostic ids.
  An action with no edit, resolve data or command (and not disabled) is left out.
- rhun lists every provider's actions in the palette: quick fixes (preferred first), refactors,
  source actions, others, each with its kind as detail; disabled ones last with their reason as
  detail (choosing one shows the reason). Choosing an action with resolve data and no edit sends
  `code-action-resolve`; then the edit is applied (nothing more happens when it cannot be), then
  the command runs as `0 run EXT.NAME DOC ref LEN` + ARGS (the resolve answer's command, else the
  action's own).
- **The lightbulb**: after the cursor rests 250 ms on a line where this extension has
  diagnostics, rhun sends `code-actions` with TRIGGER `auto` and KINDS `quickfix` (silent, 1 s);
  when an enabled quick fix comes, a bulb shows in the gutter on that line (full when one is
  preferred) and clicking it lists them (`quick_fix`). Lines without diagnostics send nothing;
  `[editor] lightbulb = no` turns it off.
- Commands: `code_actions` (ctrl+., any kind), `quick_fix_auto` (Auto Fix, shift+alt+.:
  `quickfix`; the one enabled preferred quick fix is applied at once, otherwise the quick fixes are
  listed), `organize_imports` (shift+alt+o: `source.organizeImports`) and `fix_all`
  (`source.fixAll`) apply the action at once when exactly one comes, `refactor` (ctrl+shift+r:
  `refactor`) and `source_action` (`source`) list theirs. v1 providers answer only `code_actions`.

### Feature: `file-diagnostics`

```
→ ID file-diagnostics VERSION N TARGET_LEN LEN   + TARGET + N records
   record: L1 C1 L2 C2 SEVERITY MSG_LEN EXTRA_LEN LF + MESSAGE + EXTRA
← ok ID | error ID stale V | needs | syntax | toobig | nodoc | nofile
→ ID file-diagnostics-reset
← ok ID
```

Diagnostics for any file, kept in rhun's Problems store (the Problems panel tab, ctrl+shift+m)
with task problems. Needs `documents = read`.

- `TARGET` is `#DOC` (an open document; `VERSION` its current version, else `stale V` with the
  current one) or an absolute path (`VERSION` `-`). A path must be one the extension could `open`
  (the project, its `project`/`home` read access, its state folder; else `needs`); when it is open
  and unmodified its marks show at once, when it is open with unsaved changes the set is `stale`,
  and a file that is not open keeps its diagnostics until it opens (positions outside its text are
  not shown then). An untitled document takes v1 `diagnostics` (`nofile`).
- The set replaces this extension's diagnostics for that target; `N` 0 clears them. At most 2000
  per target (`toobig`, nothing changes); the store keeps at most 10000 problems in all, past which
  further ones are not kept. A record that breaks the grammar is `syntax` and nothing changes.
- `SEVERITY` as v1 (`error warning info hint`). `MESSAGE` is 1 to 4000 bytes and may hold line
  feeds. EXTRA records: `id LEN` + a token of `[A-Za-z0-9_.:-]`, at most 64 bytes (unique in the
  set; code actions get it in `DIAGS`); `code LEN` (at most 100); `source LEN` (at most 100: the
  problem shows `source(code)`); `href LEN` (http(s), at most 1000); `tag unnecessary 0`, `tag
  deprecated 0`, `related L1 C1 L2 C2 PATH_LEN MSG_LEN` + PATH + MSG (PATH as TARGET; read and
  checked, not shown yet).
- `file-diagnostics-reset` removes every set of this extension; stopping it does too.
- Next Problem (F8) and Previous Problem (shift+F8) move the cursor to the next or previous
  diagnostic of the file and show its hover card.
- `print-problems` prints the store for tests.

### Feature: `document-links`

```
← ID document-links DOC VERSION
→ ok ID LEN + "N" LF + N records: L1 C1 L2 C2 TARGET_LEN TIP_LEN LF + TARGET + TIP
```

Key `document_links`. `TARGET` is an `http`/`https` URL, an absolute path optionally followed by
`:LINE` or `:LINE:COL`, or `#DOC`; `TIP` one line or empty. At most 10000 records and 1 MiB; a
link outside the text is left out. rhun asks every matching provider the first time ctrl (cmd on
macOS; alt when `[editor] multi_cursor_modifier` makes ctrl add carets) is held over a document
version, or at the first such click, and keeps the answers for that version: while the modifier
is held the link under the pointer is underlined, and a click with it opens the link (a URL in
the browser, a path in rhun); on text without a link it goes to the definition. Open Link
(`open_link`) opens the link at the cursor.

## Feature: `notebooks`

An extension reads and writes a notebook format, and may run its cells. rhun never parses a
notebook file: the extension turns the file into cells and back. The first-party `jupyter`
extension does this for `.ipynb`.

```ini
[provides]
# notebooks this extension reads and writes (section 2's patterns)
notebook = *.ipynb
notebook.title = Jupyter Notebook
# it runs cells; "no" for a viewer or converter only
notebook.kernel = yes
```

Opening a file a `notebook` pattern matches shows it in a notebook tab and starts the extension
(a user-stopped or disabled one stays stopped, and the tab says so). The tab is a list of cells,
each a text document of its own with its language's colors; outputs show under code cells. The
user can always look at the file as text instead (the failed tab's Open as Text, or the command
Notebook: Reopen as Text on a notebook without changes).

Identifiers rhun hands out, never reused in a session: `NB` a notebook, `CELL` a cell (stable
while the cell exists), `RUN` a run.

### Bodies

A **notebook body**, in both directions:

```
N META_LEN LF <META>
then N cells:   KIND LANG COUNT SOURCE_LEN META_LEN NOUT LF <SOURCE><META>
                then that cell's NOUT output bodies
```

`KIND` is `code` or `markup`. `LANG` is the cell's language, a grammar name with spaces as `_`
(case does not matter: `python` names the Python grammar), or `-`; markup cells are Markdown and
send `-`. `COUNT` is the execution count, or `-`. SOURCE is the cell's text, LF line ends. The
notebook's META and each cell's META (nbformat's cell id, tags, attachments) are opaque: rhun keeps
them and returns them unchanged. A cell rhun creates has empty META and the language of the
notebook's first code cell that names one. A markup cell that is not being edited shows its
Markdown rendered (relative links name the notebook's folder); Enter or a double click edits its
source, and Esc or running it renders it again.

An **output body**:

```
KIND OUTID KEEP_LEN NITEM LF <KEEP>
then NITEM items:  TYPE W H LEN LF <LEN bytes>
```

`KIND` is `stdout`, `stderr`, `result`, `display` or `error`. `OUTID` is `[a-z0-9_.+/-]{1,64}`
(Jupyter's display id) or `-`. KEEP is opaque and returned on save: the jupyter extension keeps
the output's original JSON there, so every MIME bundle round-trips, including the ones rhun cannot
draw. Items are in order of preference and rhun shows the first one it can draw. `TYPE` is `text`
(UTF-8), `ansi` (UTF-8 with SGR colors: 0/1/2/3/4/7/22-24/27/30-37/39/40-47/49/90-97/100-107 and
38/48 with `;5;N` and `;2;R;G;B` apply, every other escape is left out), `png`, `jpeg`, `gif`,
`rich`, `picture`, or any other token `[a-z0-9_.+/-]{1,64}`, which rhun keeps and returns by
name. `rich` is a rich document: a `doc LEN` record holding Markdown (at most 1 MB, the subset of
section 3.9 of the views design: headings, lists, emphasis, code, links, pipe tables) and at most
one `base LEN` record holding an absolute folder that relative links name, in any order, optionally
after a count line (`1` or `2` LF, as a views patch body has); rhun draws it laid out at the
column's width. `picture` is a 12a picture body, drawn without events. `W H` are the intrinsic
logical size of an image or a picture (`0 0`: one image pixel is one logical pixel; a picture is the
column wide and 240 tall); a `W` alone keeps the image's proportions. Images and pictures are scaled
down to the column, never wider. An item rhun cannot read (a rich document or a picture with a
syntax error) is passed over for the next one. A text item shows its first 400 rows, then "N more lines · Open in Tab",
which opens all of it, escapes left out, in a read-only tab.

### Messages

| Direction | Message | Reply or meaning |
| --- | --- | --- |
| rhun → ext | `ID nb-open NB LEN` + `PATH_LEN DATA_LEN LF <PATH><DATA>` | `ok ID LEN` + notebook body. DATA is the file's bytes (empty when it does not exist yet: a new notebook). `error ID CODE TEXT` shows TEXT in the tab with Open as Text |
| rhun → ext | `ID nb-save NB LEN` + `PATH_LEN LF <PATH>` + notebook body | `ok ID LEN` + the file's bytes, which rhun writes in place of the file (a temporary file renamed over it). An error keeps the notebook modified and shows its text |
| rhun → ext | `0 nb-run NB CELL RUN LANG LEN` + source | run the cell's source; a run is an event, not a request rhun waits on, and runs of one notebook queue in the extension in the order they arrive |
| rhun → ext | `0 nb-interrupt NB` | the user asked to interrupt the running cell |
| rhun → ext | `0 nb-restart NB` | restart the kernel (the user confirmed) |
| rhun → ext | `0 nb-kernel NB` | the kernel button was clicked: offer kernels, for example with `choose` |
| rhun → ext | `0 nb-closed NB` | the tab closed |
| ext → rhun | `ID nb-state NB CELL RUN STATE COUNT` | `ok ID`. STATE `queued`, `running`, `done`, `failed` or `cancelled`; COUNT a number or `-` (left as it was) |
| ext → rhun | `ID nb-output NB CELL RUN OP TARGET LEN` + output body | `ok ID`. OP `append` (TARGET `-`), `replace` (TARGET the index of one of the cell's outputs, or an OUTID anywhere in the notebook), `clear` (TARGET `-`, LEN 0), `clear-wait` (the outputs clear when the next one arrives; LEN 0) |
| ext → rhun | `ID nb-stream NB CELL RUN NAME LEN` + bytes | `ok ID`. Appends to the cell's last output when it is the `NAME` (`stdout` or `stderr`) stream, else starts one (type `ansi`). CR LF is a line feed; a carriage return alone takes the text back to its line's start, as a progress bar expects |
| ext → rhun | `ID nb-kernel-label NB STATE LEN` + label | `ok ID`. The kernel button's text (one line, at most 200 bytes, cut); STATE `idle`, `busy`, `starting`, `dead` or `none` colors its dot |

Running a cell gives it a new RUN and clears its outputs. Every `nb-state`, `nb-output` and
`nb-stream` names the RUN it belongs to: one for a run that is not the cell's latest, a deleted
cell or a closed notebook is answered `error ID stale`, and nothing changes. Outputs and counts
change the notebook, which then counts as modified, as in Jupyter. When the extension stops, its
queued and running cells show as cancelled and the kernel button clears.

Limits: `nb-open`'s answer and `nb-save`'s body at most 64 MB each (that answer has a cap of its
own; every other answer keeps 4 MB); 5,000 cells; a cell's source 4 MB; 1,000 outputs a cell; 32
items an output; an item 16 MB; an `nb-output` body 17 MB and an `nb-stream` body 64 KB (more
stops the extension, as for any body over its cap); outputs in all 64 MB a notebook. A message
past a count or the 64 MB is answered `error ID toobig` and changes nothing. A request rhun waits
on that is not answered within 30 seconds fails as above. `nb-output` and `nb-stream` count
against the 16 ms message window like any other message.

### Kernels that run as the user: `nb-exec`

A kernel runs the user's code with the user's access, outside the extension's sandbox (the
extension itself stays confined). The manifest names the programs:

```ini
[needs]
# kernels rhun may start as the user for this extension's notebooks: names, space separated
kernels = python3 python
```

| Direction | Message | Reply or meaning |
| --- | --- | --- |
| ext → rhun | `ID nb-exec NB LEN` + command body (test-exec's shape: `PROGRAM_LEN CWD_LEN ARGC ENVC LF PROGRAM CWD`, then ARGC arguments and ENVC `NAME=VALUE` variables, each `LEN LF` bytes; at most 256 KiB, 256 arguments of 32 KiB, 64 variables) | `ok ID EXEC` once the program runs. The first time an extension runs a program as the user in a project rhun shows the command and asks (Allow, Cancel); the answer waits for the user |
| ext → rhun | `ID nb-exec-signal EXEC SIGNAL` | `ok ID`. SIGNAL `interrupt` (SIGINT; `refused` on Windows, where a kernel takes Jupyter's interrupt message instead), `terminate` (SIGTERM), `kill`; to the program's process group |
| rhun → ext | `0 nb-exec-exit EXEC CODE` | it ended: CODE its exit status (128 + N after signal N), or `killed` when rhun ended it |

rhun checks, in this order: the feature is agreed (`unknown`); `NB` is the extension's open
notebook (`stale`); at most 4 kernels per extension are running or waiting (`busy`); the body
parses (`syntax`, `toobig`); the program's name, its last path component (`.exe` left out on
Windows), is a word of `[needs] kernels` and a path is absolute (`needs kernels=NAME`); a bare
name is found on the user's PATH and a path exists (`nofile`); the folder (empty: the notebook's
own) exists (`refused`); the user allows it (`refused the user did not allow it`). In a remote
window nb-exec is `refused` in this revision. The program
gets the user's login environment plus the variables, input from the null device, its output and
errors in `kernel-EXEC.log` in the extension's state folder (`$RHUN_STATE`, which the extension can
read), and a process group of its own (a job object on Windows). It ends with its extension, 3 s
after its notebook closes (time for the kernel's own shutdown), and when rhun quits. The extension
talks to the kernel itself, for Jupyter over its loopback sockets (`[needs] network = yes`).
Headless: `print-nb-exec` prints `exec=N nb=N pid=P state=asking|running` per kernel.

### The notebook tab

Jupyter's keyboard. In **edit mode** the selected cell has the keys and every editor command works
in it; Esc goes to command mode (with vim mode on, Esc in vim's normal mode), Shift+Enter runs the
cell and selects the next one (a new one past the last), Ctrl+Enter runs it, Alt+Enter runs it and
inserts a cell below, Up on the first line and Down on the last go to the neighbouring cell. In
**command mode** Enter edits, Up/k and Down/j select, a and b insert a code cell above or below,
d d deletes, z undoes the last cell operation (the last 100 are kept), m and y make the cell
markup or code (its outputs go), Alt+Up and Alt+Down move it, i i interrupts, 0 0 restarts after
asking, o hides or shows its outputs. Every action is also a command named `notebook.*`
(`notebook.run_cell`, `notebook.insert_below`, ...). The toolbar has Run All, Interrupt, Restart,
Clear Outputs and the kernel button; clicking a cell's count column runs it. Save, Save As, Save
All, Revert File, the close and quit questions and the file watcher treat a notebook as one
document: a file changed on disk is read again while the notebook has no changes, else the tab
warns (as it does when the notebook changes while nb-open's answer to a reload is on its way: the
changes stay). A save marks saved what its nb-save body held, so changes made while the answer
was on its way still count. Find searches the selected cell; Find Next and Find Previous go on through the other cells.

When-clause keys: `notebookFocus` (a notebook tab has the keys), `notebookCellEditing` (and its
cell is in edit mode), `notebookCellType` (`code` or `markup`), `notebookKernelState` (the
kernel button's STATE); `activeTab` is `notebook` there.

Headless controls: `print-notebook` prints a line per cell, `cell=I id=CELL kind=code|markup
lang=LANG lines=N count=C state=none|queued|running|done|failed|cancelled outputs=K sel=0|1
editing=0|1 hidden=0|1 at=X,Y` (`at` the top left of its frame on screen, `-1` off screen), then
`dirty=D mode=edit|command undo=N kstate=S kernel=LABEL`; `notebook=opening` or `notebook=failed
error=TEXT` before it is open; `notebook=none` without one. `print-nb-output I J` prints cell I's
output J: `output kind=K id=OUTID items=TYPE,... show=INDEX lines=N bytes=B keep=K` (with
`image=WxH` or `picture=WxH` once drawn), the text of a text item (ESC as `\e`) or a rich
document's layout (the lines of `print-md`), then `<eoo>`. `print-nb-md I` prints cell I's rendered
Markdown: `md cell=I rendered=0|1 at=X,Y` (where it was drawn last) and its layout, or `md=none`;
`print-nb-xy I LINE COL` prints `X Y`, where cell I's text has that line and column on screen.
`nb-mode edit|command`, `nb-select I`, `nb-run I` and `nb-output-tab I J` do what the keys and
buttons do.

### Cells for language providers

An extension that agreed `notebooks` hears the cells of open notebooks as documents, as it hears
tabs: `0 opened DOC VERSION LANG -` (no path), `changed`, `edits`, `closed` when a cell is deleted or
its notebook closes (never `saved`), and its requests (`text`, `replace`, `diagnostics`, completion
and the other language features) take the cell's DOC. Its `[subscribe]` and `[provides]` patterns
match a cell through its language's usual file name: a Python cell is `cell.py`, so `complete =
*.py` gets Python cells and `*` gets every cell. When the order of the cells it knows is new to it,
it hears `0 cell DOC NB CELL INDEX` for each (INDEX among all the notebook's cells, from 0) and then
`0 cells NB DOC...` with them in order: once after their `opened`, and again after cells are
inserted, deleted or moved. While a notebook tab is active its `active` document is the selected
cell. Extensions that did not agree `notebooks` never see cells, and their active document is `0`
while a notebook tab is active.

## Feature: `custom-editors`

An extension owns how a kind of file is shown and edited: rhun opens the file in a tab the
extension draws, a 12a view (feature `views`), and the extension edits it through one of two models.

```ini
[provides]
# the files it edits (section 2's patterns)
editor = *.bin *.hex
editor.title = Hex Editor
# text: rhun keeps the file as a document; owned: the extension keeps it
editor.model = owned
# default: matching files open in it; option: only through Reopen With
editor.priority = option
```

- **text**: the file is an ordinary document of rhun's (its DOC is the editor's `CDOC`), not shown
  as a text tab. The extension hears it like any open document (`opened` and so on, by its path),
  reads it with `text` and changes it with `replace` (`documents = write`). rhun owns its undo, its
  dirty state, Save, Save As, Revert File, reloading when the file changes and the close question.
- **owned**: the extension keeps the file's contents (a binary or a huge file). rhun keeps a
  stand-in with the path, routes Undo, Redo, Save, Save As, Revert File and the close and quit
  questions to the extension, shows its dirty dot and writes the bytes it is given.

A file whose name matches an extension's `editor` patterns with `editor.priority = default` (the
default) opens in that editor; the first such extension wins. A file rhun cannot open as text
(a binary) opens in the first editor whose patterns match it, an `option` one too. **Reopen Editor With...** (the
palette) lists Text and every editor whose patterns match the active file; the choice holds for
files with the same extension from then on, in the config's `[editors]` section (`*.bin =
rhun-editor/hexedit`, or `text`), and the file opens again (a tab with changes asks to save
first). An editor that refuses a file, or whose extension does not run or does not agree
`custom-editors`, closes its tab, and the file opens as text with the reason in a toast.

| Direction | Message | Reply or meaning |
| --- | --- | --- |
| rhun → ext | `ID editor-open CDOC VIEW MODEL SOURCE LEN` + path | `MODEL` `text` or `owned`, `SOURCE` `file`, or `backup` when it opens unsaved changes rhun kept (below: `editor-read` then reads the backup, and the editor is dirty), `VIEW` the 12a view rhun opened for the tab (draw it with `draw`; its events come as for any view). Reply `ok ID 0` when ready; an error closes the tab and opens the file as text with the error's text in a toast. rhun waits 30 s |
| ext → rhun | `ID editor-read CDOC OFFSET MAX` | `ok ID LEN` + up to `MAX` (at most 4 MB) bytes of the file from `OFFSET`; `LEN` 0 at its end. It reads the file the user opened without `project = read` |
| ext → rhun | `ID editor-dirty CDOC 0\|1` | `ok ID`. An owned model's dirty dot and close question follow it (`refused` for a text model, whose state is rhun's) |
| rhun → ext | `ID editor-save CDOC KIND PART` | `KIND` `save`, `saveas` or `backup`. Reply `ok ID LEN` + body: a line `MORE` (`1` when another part follows, else `0`), then at most 4 MB of the file's bytes. rhun asks for `PART`+1 until `MORE` is `0`, then replaces the file at once (a temporary file renamed over it); 1 GB in all. The extension takes a snapshot of its model at part 0. After a Save As rhun sends `0 editor-path CDOC LEN` + the new path |
| rhun → ext | `ID editor-undo CDOC`, `ID editor-redo CDOC`, `ID editor-revert CDOC` | rhun's Undo, Redo and Revert File in an owned model's tab; reply `ok ID 0` |
| rhun → ext | `0 editor-changed CDOC` | an owned model's file changed on disk; a clean model should read it again, a dirty one is the extension's call |
| rhun → ext | `0 editor-closed CDOC` | the tab closed (after the save-or-discard question when it had changes) |

A text model uses only `editor-open` and `editor-closed`; everything else is its document's. A
request rhun waits on that is not answered within 30 seconds fails: an open opens the file as
text, a save keeps the changes. When the extension stops, its editors' tabs close; when its program
starts again at once (it was turned off and on, or rhun retries it without confinement), they
open again (from their backup when their unsaved changes are in one). A save or a backup marks the
model as the file holds it only when no `editor-dirty` came after its first part was asked for. Revert
File reads the file even for an editor that was opened from its backup. The editor's
view is not one of the extension's 8; with `view-keys` agreed a press on it gives it the keyboard,
and keys, typed text, paste and select-all reach it as for any picture.

**Backups of an owned model.** 30 seconds after an owned editor last said `editor-dirty CDOC 1`
(and while rhun waits on nothing of it) rhun asks `editor-save CDOC backup PART` and keeps the
parts in its state folder (`backups/`, 0700), with the file's path beside them; saving or a
clean model removes them. When the extension stops while the editor has unsaved changes in a
backup, the tab stays and says so, with **Restore Unsaved Changes**, which starts the extension
again and opens the backup (`SOURCE backup`). When rhun itself stopped without closing the tab, the
next time the file opens in the editor it opens from its backup, with the changes unsaved.

Headless controls: `print-custom` prints `cdoc=ID ext=OWNER/NAME model=text|owned dirty=D path=P
view=V state=opening|ready|stopped` per editor; `reopen-with OWNER/NAME|text` does what Reopen With
does; `custom-backup` takes the backups that are due in 30 s now; `custom-restore` presses the
active stopped editor's Restore Unsaved Changes; `custom-timeout` prints `timeout=MS`, when rhun
looks at its editors again (a request's wait, a backup falling due; `-1` none).

## Source control

Extensions read rhun's Git data instead of running `git` themselves, write commit messages, give
quick-diff bases for the gutter, and add their own version control panels. The features are
`git-data`, `commit-message`, `quickdiff` and `scm`.

### Manifest

```ini
[needs]
# read the project's Git data through rhun (status, blame, history, files at a revision)
git = read

[provides]
# a commit-message provider (feature commit-message)
commit_message = yes
```

`git = read` is shown before install as "read the project's Git history". `git = none` (the
default) or any other value grants nothing.

### Feature: `git-data`

Every request needs `git = read` (else `needs git=read`) and the agreed feature (else `unknown`).
rhun runs git in the background in the project's work tree, as it does for its own views, so the
trust pause holds: in a folder a `rhun://` link opened, git runs no command until the user trusts
the folder, and every request answers `error ID refused git is paused in this folder`. Outside a
repository the answer is `error ID nofile not in a Git repository`; with Git turned off in
Settings (or no `git` program) it is `error ID refused git is off`.

| Request | Successful reply |
| --- | --- |
| `ID git-repo` | `ok ID LEN` + `ROOT_LEN BRANCH_LEN UPSTREAM_LEN HEAD AHEAD BEHIND STATE` LF `<ROOT><BRANCH><UPSTREAM>` |
| `ID git-status` | `ok ID LEN` + `N` LF, then N entries `XY PATH_LEN` LF `<PATH>` |
| `ID git-blame DOC` | `ok ID LEN` + the blame of the document's current text (below) |
| `ID git-log MAX L1 L2 LEN` + LEN bytes of path | `ok ID LEN` + `N` LF, then N commits `SHA PARENTS TIME AUTHOR_LEN SUBJECT_LEN` LF `<AUTHOR><SUBJECT>` |
| `ID git-show REV LEN` + LEN bytes of path | `ok ID LEN` + the file's bytes at REV |
| `ID git-diff-open REV1 REV2 LEN` + LEN bytes of path | `ok ID`: rhun's read-only diff view of the file from REV1 to REV2 opens |

Strings in bodies are raw bytes framed by their lengths, concatenated without separators; the
next record starts right after the last string.

- `git-repo`: ROOT is the work tree's absolute path. HEAD is `branch`, `detached` or `unborn` (a
  branch without commits). BRANCH is the branch's name, or the commit's first 7 hex digits when
  detached. UPSTREAM is like `origin/main`, empty without one; AHEAD and BEHIND count the commits
  HEAD has that the upstream lacks and the other way round (0 without an upstream). STATE is
  `merging` (MERGE_HEAD exists), `rebasing` (a rebase is in progress), `dirty` (any change) or
  `clean`.
- `git-status`: the entries of `git status --porcelain` in git's order, ignored files left out. X
  and Y are git's two status columns, an unmodified side written `.` (as porcelain v2 does), so
  `.M` is a change not staged, `M.` a staged one, `??` an untracked file, `UU` a conflict. PATH is
  relative to ROOT; an untracked folder is one entry ending in `/`; a rename gives its new path.
- `git-repo` and `git-status` answer from rhun's status. While a status refresh runs or is about to
  (after a save, a commit, a checkout), the answer waits for it.
- `git-blame DOC`: the blame of the open document's current text, unsaved edits included (git
  blame `--contents`, given the text with the file's line ends). The document must have a file in
  the repository (else `nofile`) and at most 100,000 lines and 16 MiB (else `toobig`). The body is
  `NC NR` LF, NC commits `SHA TIME AUTHOR_LEN MAIL_LEN SUMMARY_LEN` LF `<AUTHOR><MAIL><SUMMARY>`,
  then NR runs `SHA L1 L2 ORIG_L1` LF: lines L1 to L2 (one-based, in the document) come from
  commit SHA, where they started at line ORIG_L1. Lines not committed (unsaved, or changed on disk)
  carry the zero SHA (40 or 64 `0`s, as the repository's hashes), whose commit record names
  git's external-file author. TIME is the author time in seconds since 1970; MAIL is without angle
  brackets. A file git cannot blame (not committed yet) answers `nofile`.
- `git-log MAX L1 L2 LEN` + path: at most MAX (1 to 1000) commits, newest first. With `L1 L2` `0 0`
  the commits that touched the file, following renames; with 1 ≤ L1 ≤ L2 the commits that touched
  lines L1 to L2 of the file as it is at HEAD (git log -L); with LEN 0 (no path, L1 L2 `0 0`) the
  history of HEAD. PARENTS are the parents' SHAs joined by `,`, or `-` for a root commit. TIME is
  the author time; AUTHOR the author's name; SUBJECT the first line of the message.
- `git-show REV LEN` + path: the file's stored bytes at REV (no filters or text conversion), at
  most 4 MiB (more is `toobig`). A missing file or revision is `nofile`.
- `git-diff-open REV1 REV2 LEN` + path: REV2 `-` means the file in the work tree. The diff opens in
  a tab named like `main.rs (HEAD~1..HEAD)`; the reply comes at once, the diff when git answers.

REV is 1 to 200 bytes of ASCII letters, digits and `. _ / ~ ^ @ { } + -`, not starting with `-`
(a commit id, `HEAD~2`, a branch, `main@{1}`); anything else is `syntax`. A path is the file's
absolute path inside the work tree or a path relative to ROOT, at most 4095 bytes, without NUL,
CR or LF; a relative path with a `..` segment, or an absolute path outside the work tree, is
`nofile`. Paths with a line break cannot be named.

Limits: 16 requests waiting per extension and 64 in all (more is `busy too many Git requests`);
15 seconds per request (then `cancelled git took longer than 15 seconds`, and git is stopped); a
reply body at most 4 MiB (`toobig`). A request still waiting when the project changes answers
`cancelled the project changed`; a stopped or restarted extension gets no old answers.

The event `0 git changed` follows a status refresh after which the status, the branch, HEAD's
kind, the upstream or the ahead and behind counts differ from the last event, or after HEAD moved
(a commit, a checkout, a reset). It goes to running extensions that agreed `git-data` and have
`git = read`. It carries no data: ask again. An extension that starts later is not told about
changes before it started; it asks at start.

Headless control: `print-git-data` prints `git-data pending=N`, the requests waiting.

### Feature: `commit-message`

A provider declares `commit_message = yes` under `[provides]` and lists `commit-message` in
`hello 1 ...`. It needs no `[needs]` entry: rhun sends it a diff only when the user asks.
Settings > Git > Commit message AI lists each installed provider after the built-in ones, by its
title; the choice is saved as `commit_ai = ext:OWNER/NAME` and kept while the extension is
missing (shown as "OWNER/NAME (not installed)").

When the user clicks Generate, rhun starts the provider if it is idle, builds the bounded diff the
built-in providers get (the staged changes; with nothing staged, what Commit All would take,
untracked files included, through a temporary index; binary contents left out; at most 16,000
bytes, then a line `[Diff truncated to 16000 bytes.]`) and sends

```
ID commit-message LEN
```

followed by LEN body bytes `BRANCH_LEN DIFF_LEN` LF `<BRANCH><DIFF>`. BRANCH is the current branch
(the short commit id when detached). The reply is `ok ID LEN` + the message, or `error ID CODE
TEXT`, whose TEXT the user sees. The message is trimmed of spaces, tabs and line breaks at both
ends and must be 1 to 8192 bytes without control characters other than tab, CR and LF; anything
else is refused and the user's draft stays. The message replaces the draft only if the user did
not edit it meanwhile. Cancel sends `0 cancel ID`. rhun waits 5 seconds for the provider to start
and 180 seconds for the answer; a provider that stops meanwhile, did not agree the feature, or
times out leaves the draft and says so.

Headless control: `print-scm-ext` prints `commit-provider=OWNER/NAME asked=0|1` and the last
source control menu's location and target.

### Feature: `quickdiff`

A quick-diff provider gives the base text a file is compared with for the gutter marks (added,
changed, deleted lines), as rhun does with HEAD for files in its git work tree. It declares
`quickdiff = PATTERNS` under `[provides]` (name patterns as in `[subscribe]`; it hears `opened`
for those files) and lists `quickdiff` in `hello 1 ...`.

```
ID quickdiff DOC VERSION
```

The reply is `ok ID LEN` + the base's bytes (at most 4 MiB; line ends may be CRLF), or `ok ID 0`
for none: the marks are then git's, or none. An error reply changes nothing. rhun hashes the base
and redraws the marks as the document changes, without asking again.

A provider's bases apply to files under a root it registered with `scm-register` (feature `scm`).
Without such a root they apply only to files outside the work tree rhun runs git in (or to every
file while git is off or paused). When several providers match, a registered root wins, else the
first in rhun's order. rhun asks when a document opens, after it is saved, once when the provider
starts (for the documents already open), and when the provider asks:

| Request | Reply | Effect |
| --- | --- | --- |
| `ID quickdiff-refresh LEN` + LEN bytes: an absolute path, or `-` | `ok ID` | rhun asks again for that open file, or for every open file the provider matches |

Without the agreed feature the request returns `unknown`; an empty body is `syntax`.

### Menus on source control rows

A right press on a commit in the history, on a file of a commit, or on a changed file or group in
Source Control or an extension's version control panel opens a menu: rhun's items (or the panel's
actions), then the items extensions add under `[menu.history_commit]`, `[menu.history_file]`,
`[menu.scm_resource]` and `[menu.scm_group]` (see Menus). Its target, the first line of such an
item's body (feature `command-args`, FROM `menu`), is:

| Location | TARGET |
| --- | --- |
| `history_commit` | `commit SHA` |
| `history_file` | `commit SHA PATH` |
| `scm_resource` | `resource GROUP PATH` |
| `scm_group` | `group GROUP` |

GROUP is `merge`, `staged` or `changes` for git (a provider's group id for an `scm` provider);
PATH is relative to the work tree. While the menu is open the context keys `scmProvider` (`git`
or the provider's `OWNER/NAME`), `scmResourceGroup` (GROUP) and `scmResourceStatus` (the
resource's letter, `M A D R C U`) describe its target. `gitTrusted` is `true` once git may run in
the project folder, `false` while it is paused.

### Feature: `scm`

An extension that lists `scm` in `hello 1 ...` may add panels for another version control system
(Mercurial, Fossil, Perforce, …). It registers a repository root and rhun adds a section to the
sidebar for it, titled with its label, which the extension fills: a status line, an optional
message box, up to four buttons, and groups of changed files. rhun colors those files and their
folders in the explorer and the tab titles (git's colors win inside git's work tree), and the
extension's quick-diff bases (feature `quickdiff`) apply to files under its roots. No `[needs]` or
`[provides]` entry is needed; the extension runs its own tool (`run` in `[needs]` for that).

| Request | Reply | Effect |
| --- | --- | --- |
| `ID scm-register LEN` + `ROOT_LEN LABEL_LEN` LF `<ROOT><LABEL>` | `ok ID SCM` | a sidebar section for the repository at ROOT; registering the same root again returns its SCM |
| `ID scm-update SCM LEN` + body | `ok ID` | replaces everything the section shows (below) |
| `ID scm-input SCM` | `ok ID LEN` + text | the message box's text |
| `ID scm-input-set SCM LEN` + text | `ok ID` | puts text in the message box (LEN 0 clears it; at most 65,536 bytes) |

ROOT is an absolute path (trailing slashes are dropped), LABEL 1 to 200 bytes; neither has NUL, CR
or LF. SCM is a number. At most 4 providers per extension and 16 in all (`toobig`); an SCM the
extension did not get is `refused no such provider`. Without the agreed feature the requests are
`unknown`. When the extension stops, its sections go.

The `scm-update` body:

```
STATUS_LEN PLACEHOLDER_LEN NACT NGROUP LF <STATUS><PLACEHOLDER>
NACT buttons:     action ICON NAME ARGS_LEN LABEL_LEN LF <ARGS><LABEL>
NGROUP groups:    group ID LABEL_LEN NACT NRES LF <LABEL>, its NACT actions, its NRES resources
a resource:       res CODE COLOR PATH_LEN NREC LF <PATH>, then NREC records:
                    command NAME ARGS_LEN LF <ARGS>       what a click runs (one, before its actions)
                    action ICON NAME ARGS_LEN LABEL_LEN LF <ARGS><LABEL>    in its menu
                    tip LEN LF <TEXT>                     its tooltip (at most 1000 bytes)
                    any other NAME ... LEN LF <LEN bytes> skipped (later revisions)
```

- STATUS is a line above the box (a branch, a count); PLACEHOLDER, when not empty, shows a message
  box with that placeholder. Both are at most 200 bytes without NUL, CR or LF.
- An action names one of the extension's own commands (NAME as in a command reference, 1 to 40 of
  `[a-z0-9_]`) with ARGS (at most 8192 bytes) and a LABEL (at most 200 bytes; empty: the command's
  title). ICON is a tree icon name (`check`, `refresh`, `plus`, `minus`, `discard`, `close`, …) or
  `-`. Buttons are drawn in a row under the message box; Ctrl+Enter in the box runs the first. A
  group's actions are its menu; a resource's actions are its menu, followed by rhun's Open File. A
  group's or resource's actions that have an ICON are also buttons at the row's right end while it
  is hovered or has the cursor (at most four, the first rightmost; the title is the tooltip).
- Running an action or a click sends `0 run EXT.NAME DOC ref LEN` with ARGS as the body (feature
  `command-args`; without it v1's `0 run EXT.NAME DOC`). A click on a resource without a `command`
  record opens a read-only diff tab, `NAME (LABEL)`, of the file against the base the extension's
  quick-diff provider gave for it (feature `quickdiff`); a file that is not open opens first and
  the diff follows when its base comes. Without a base, or with no changes, the file opens.
- A group has an ID (1 to 64 bytes without spaces, its menus' `GROUP`; two groups with one ID are
  `syntax`), a LABEL and its resources as a tree with the count as a badge; expansion and the
  selection stay across updates. A PATH listed twice in a group shows once (the first).
- CODE is one printable ASCII letter (`M`, `A`, `D`, `?`, `!`, …) shown after the name; COLOR is
  `added`, `modified`, `deleted`, `conflict`, `untracked`, `ignored` or `-` (the theme's colors).
  PATH is relative to ROOT (no NUL, CR or LF, not starting with `/`; ROOT/PATH at most 4095 bytes).
- At most 4 buttons, 64 groups, 8 actions per group or resource, 10,000 resources and a body of
  4 MB. A body that breaks the grammar or a limit is `syntax` and the section keeps what it had.

The menus of a provider's rows have the targets `group GROUP` and `resource GROUP PATH` at
`scm_group` and `scm_resource`, with `scmProvider` the extension's `OWNER/NAME` (see above). Items
under `[menu.scm_title]` are icon buttons in each panel's header and on the branch row of rhun's own
Source Control (at most three, then `…`), with TARGET `scm OWNER/NAME ROOT` (or `scm git ROOT`) and
`targetKind` `scm`; while their clauses are evaluated `scmProvider` is that provider (`git` or
`OWNER/NAME`), so `when = scmProvider == git` puts a button on git's header only.

An exchange (a Mercurial repository with one change):

```
→ hello 1 scm command-args
← 0 features scm command-args
→ 1 scm-register 26
→ 12 9
→ /home/u/repoMercurial
← ok 1 1
→ 2 scm-update 1 147
→ 7 30 1 1
→ defaultMessage (Ctrl+Enter to commit)action check commit 0 6
→ Commitgroup changes 7 0 1
→ Changesres M modified 5 1
→ a.txtcommand diff 5
→ a.txt
← ok 2
```

Headless control: `print-scm-ext` lists each provider (`scm SCM OWNER/NAME ROOT label=… status=…
input=… actions=…`) and its rows; `scm-click SCM GROUP PATH`, `scm-action SCM N` (from 0),
`scm-menu SCM GROUP [PATH]` (then `menu-pick`), `scm-focus SCM` (then `type`), `print-vcs PATH`
(`vcs=LETTER dir=0|1 color=RRGGBB`, as the explorer colors it), `wait-scm SCM [TEXT]` (until the
panel's status line holds TEXT, at most 15 seconds) and `wait-quickdiff` (until no open file waits
for a quick-diff base).

## Feature: `decorations`

An extension that lists `decorations` in `hello 1 ...` and has `documents = read` may style ranges
of open documents. It registers named styles (types), then puts ranges of a type on a document.
No `[provides]` line is needed.

| Request | Reply | Effect |
| --- | --- | --- |
| `ID decoration-type NAME LEN` + body | `ok ID` | registers this extension's type NAME, or replaces its style (ranges already drawn take the new style) |
| `ID decorate DOC VERSION NAME FROM TO N LEN` + body | `ok ID` | replaces this extension's ranges of type NAME on DOC whose first line is in FROM..TO (1-based, inclusive; `0 0`: all of them) with the body's N records |

| Event | When |
| --- | --- |
| `0 visible DOC L1 L2` | the 1-based first and last lines the editor shows of the active document DOC (`0 0 0`: no document); sent when they change, at most every 100 ms, and once to an extension that just started |

NAME is 1 to 40 bytes of `a-z`, `0-9`, `_` and `-`.

### Type body

One `KEY ARGS` per line; blank lines are allowed. An unknown key or bad arguments is
`error ID syntax line N` (the first such line) and nothing changes.

| Line | Effect |
| --- | --- |
| `color COLOR` | the text's color |
| `background COLOR` | behind the range's characters, per row |
| `line-background COLOR` | behind the text area's whole width, on every line the range touches |
| `border WIDTH COLOR` | an outline of 1 to 3 pixels (times the display scale), per row |
| `underline STYLE COLOR` | STYLE `solid`, `double`, `dotted`, `dashed` or `squiggle` |
| `strike COLOR` | a line through the text; COLOR `-` is the text's color |
| `faded` | the text halfway to the background |
| `before COLOR BACKGROUND` | text before the range (each range's BEFORE); BACKGROUND `-` for none |
| `after COLOR BACKGROUND` | text after the range (each range's AFTER) |
| `width CELLS` | before and after text padded or cut to this many cells; `0` natural (0-200) |
| `gutter SHAPE COLOR` | an icon in the gutter's left lane on the range's first line; SHAPE `dot`, `circle`, `ring`, `square`, `diamond`, `triangle`, `bar`, `check`, `cross`, `plus`, `minus`, `arrow` |
| `scrollbar COLOR` | a tick in the vertical scrollbar at the range's first line |
| `grow EDGE` | EDGE `none` (default), `start`, `end` or `both`: whether text typed at that edge joins the range |
| `priority N` | 0-100, default 50: where ranges overlap, the higher one's color wins and is drawn on top; equal priorities go by the extensions' order, then by when the type was first registered |

`COLOR` is `SPEC` or `SPEC|SPEC` (for dark themes `|` for light ones). `SPEC` is a theme color
name as in `.theme` files (`warning`, `keyword`, `git_added`, …), `NAME/PCT` (that color at PCT
percent opacity, 0-100), `#rrggbb` or `#rrggbbaa`. Theme names follow theme changes.

Whole-line backgrounds are drawn first, then backgrounds, the selection, find matches, the text,
strikethroughs, underlines and borders; diagnostics' underlines are drawn over decorations.

BEFORE and AFTER texts are **virtual text**: drawn inside the line, in whole cells of the code
font, but not part of the document (copying, searching and the status bar's column never see
them). An `after` text belongs to the text before its position (typing there lands left of it), a
`before` text to the text after it. At one position the `after` texts come first, then the
`before` ones, each group by priority with the highest nearest the text it belongs to. The caret
at that position is drawn left of the texts when an `after` one is there, otherwise right of them;
a range starting there covers the cells after every text at its start and before every text at its
end. A press on virtual text whose range has a `command` record (section Command references) runs
that command; otherwise the caret goes to the text's position. The pointer resting on it shows the
range's TIP, and no `hover` event is sent for it. On a wrapped line a text goes with the row of the
character after it.

### Ranges

The body holds N records, each a header line and its texts:

```
L1 C1 L2 C2 BEFORE_LEN AFTER_LEN TIP_LEN EXTRA_LEN LF <BEFORE><AFTER><TIP><EXTRA>
```

Positions are v1's (1-based lines, 1-based byte columns; a column one past the line's end is its
line feed). BEFORE and AFTER are one line each without control characters, at most 200 bytes. TIP
(at most 1000 bytes) is shown as a tooltip while the pointer rests on the range (or on its gutter
icon). EXTRA (at most 9000 bytes) holds extra records for later revisions; a host skips what it
does not know.

VERSION may be the document's current version or an older one: rhun keeps each document's last 256
edits and moves positions of an older version onto the current text (a range typed into grows only
at edges its type lets grow; a range whose text was deleted collapses). A version older than that,
or newer than the text, is `stale` with the current version as its text. Ranges then follow edits
as the text does; a range that becomes empty is removed, and an empty range is removed when a
deletion covers it.

Errors: `nodoc`, `readonly` (a diff view), `syntax unknown decoration type` (NAME is not one of
this extension's types), `badpos` (a position outside the text or inside a character, or an end
before its start), `toobig` (see the limits) and `syntax` for a malformed body (more or fewer than
N records, a text that runs past the body). On an error nothing changes.

### Limits

| What | Limit |
| --- | --- |
| types | 64 per extension, 512 in all; a type body 4 KB |
| records | 20,000 per message and per extension and document; 50,000 per document in all |
| a body | 4 MB |
| drawn per frame | 4,096 decoration pieces (a range on one row), 1,024 scrollbar ticks; the rest is not drawn |

When the extension stops, its types and ranges are removed.

A record's texts follow its header line directly, with no line feed after them. An exchange (blame text after
line 2's start):

```
→ hello 1 decorations
← 0 features decorations
→ 1 decoration-type blame 32
→ after muted -
→ grow none
→ width 0
← ok 1
→ 2 decorate 1 3 blame 2 2 1 23
→ 2 1 2 1 0 7 0 0
→ you, 2h
← ok 2
```

## Packs and the install record

No messages: these concern how rhun installs extensions (docs/guide.md, "Managing extensions").

### Manifest: `pack`

```ini
# a pack: other extensions installed with this one (at most 16, OWNER/NAME, space separated). A
# pack needs no program: without run entries it never starts, and its themes and grammars load
pack = rhun-editor/lsp rhun-editor/format rhun-editor/editorconfig rhun-editor/snippets
```

Each name is `[A-Za-z0-9._-]+/[A-Za-z0-9._-]+`, not starting with a dot on either side, and not
the extension itself; a wrong list fails the manifest ("pack in extension.ini must list at most
16 other extensions as owner/name"). With `pack` set, a missing `run.TARGET` or `runtime` is no
longer a failure: such an extension never starts. Installing the pack fetches every member that
is not installed in the same batch and asks once; removing it keeps the members. There are no
dependencies: extensions never call each other, so a dependency would only mean "install this
too", which a pack says. A pack is released like any extension: `NAME-TARGET.tar.gz` for each
target (the same files) and `sha256sums.txt`.

### Install records: `.rhun-install`

rhun writes `.rhun-install` into each folder it installs; it is rhun's, and an extension must
not ship one (rhun overwrites it). Keys: `source = github | archive | folder`, `repository`
and `tag` (github), `path` (archive or folder), `link = yes` (a linked development folder: the
manifest and program are read from `path`), `sha256` (the archive's), `reviewed = yes | no`
(the tag and digest were the extensions index's), `installed` (UTC), `started = yes` after the
version's first `hello`, `kept = yes` for a version Roll back restored. Until `started = yes`, a
version with a previous one kept in `ROOT/.previous/OWNER/NAME` rolls back by itself when its
first start fails (it cannot start, sends no `hello` within 5 s, or exits before `hello`).
Extension discovery skips folders whose names start with a dot, at both levels.

### Headless: `print-ext-page-more`

`print-ext-page` keeps v1's form byte for byte (v1.md section 10), so a published extension's
golden test does not change. `print-ext-page-more` prints the same lines and adds this version's:

- in the list, `updates=N` first when N extensions have an update offered;
- in the details, `badge=letter|icon|unreadable color=#rrggbb|-` after `pinned=` (the badge drawn
  from the manifest's `icon` and `color`: `unreadable` is an icon rhun could not show, and the
  letter stands in);
- at the end of the details, `pack OWNER/NAME=installed|missing …` for a pack's members, then
  `versions update=VERSION|- previous=VERSION|-` (the update offered and the version Roll back
  restores).

<!-- workspace edits and the editor model: workspace-edits, file-operations, selections, reveal,
diff, linked-editing -->

## Feature: `workspace-edits`

With `workspace-edits` agreed, every workspace edit the extension sends (the `apply` request, a
rename answer, a code action's EDIT) is a **transaction**: checked whole, then made everywhere
or nowhere, and undone and redone as one step from any tab it touched. Format and save-actions
answers keep their rule (one existing document, `refused` otherwise).

The body is a superset of 12g, so a v1 body is a valid one:

```text
N LF
then N entries; a text group is 12g's:
  PATH_LEN PRE M LF <PATH>, then M edits: L1 C1 L2 C2 LEN LF <LEN bytes>
then extra records up to the end of the body, each NAME ARGS... LEN LF <LEN bytes>:
  label LEN       one line naming the change in the confirmation list, toasts and Undo;
                  at most 200 bytes (longer is cut)
  confirm 0       show the confirmation list even when the extension may write
  (other names)   skipped
```

`PRE` of a text group is one of:

- a version: an open document at that version (`stale` otherwise; `nodoc` when it is closed);
- `-`: the file on disk as it is; an open document only while it has no unsaved changes;
- `fnv1a64:HEX`: 16 lowercase hexadecimal digits, the 64-bit FNV-1a hash of the file's bytes
  **on disk** (CR included): offset basis `0xcbf29ce484222325`, prime `0x100000001b3`, for each
  byte `h = (h XOR byte) * prime` modulo 2^64. The file must hash to it (`stale` otherwise); an
  open document must then have no unsaved changes. A language server reads closed files itself:
  the hash says which bytes it converted its positions against, without a round trip.

`#DOC` takes a version or `-`. Positions always refer to rhun's text of the file (CR before LF
dropped, v1 section 6).

**Open documents and files on disk.** An open document is edited in its buffer. A file that is
not open is read, edited and written back atomically (a temporary file renamed over it; its mode
and CRLF line endings kept); no tab opens. It must be a regular text file of at most 16 MiB
(`nofile` otherwise). When an edit writes anything to disk (a closed file, or a file operation
of `file-operations`), the open documents it edits that had **no unsaved changes before** are
written too (a plain write: no format-on-save, code actions, trimming or final newline); a
document with unsaved changes is edited in its buffer and stays unsaved. An edit that touches
only open documents writes nothing.

**Permissions.** `documents = write` for any workspace edit (v1). Open documents, untitled ones
included, need nothing more. A disk change (a closed file's group, a create, rename or delete,
and the writes of clean open documents that come with them) must lie inside the project after
symbolic links are resolved, or inside the extension's own state folder (`needs` otherwise), and
needs one of: `project = write` in `[needs]`; the user's standing permission ("Let EXTENSION
change project files from now on", stored per extension and revocable on its page); or the
user's Apply in the **confirmation list**:

```text
Language Server: Rename to 'total' changes 3 files that are not open and renames 1
> Apply
  Apply, and let Language Server change project files from now on
  Preview changes
  M  internal/calc.go                2 edits
  R  util.go → helpers.go
```

Preview changes opens a read-only tab with a unified diff of every file. Escape cancels.

**Reply.** `ok ID` once the edit is made. While the confirmation list is open the reply waits,
as `choose`'s does (`busy` when a prompt or the palette is already open, `cancelled` on Escape);
rhun goes on reading the extension's other requests and checks every precondition again when
the user applies (`stale` when something changed meanwhile). A failed `apply` is answered
`error ID CODE ENTRY TEXT`: `ENTRY` is the 1-based number of the entry at fault, or 0, and TEXT
one sentence. Codes: `syntax`, `needs`, `nofile`, `nodoc`, `stale`, `readonly`, `badpos`,
`toobig`, `refused`, `cancelled`, `busy`, and **`io`**: the system refused a write while the edit
was being made, and rhun put back everything it had done (the edit itself was valid; it may be
sent again). Nothing changes on any error.

**Undo.** Undo (or vim's `u`) in any tab the edit touched undoes all of it: buffers, files on
disk, and with `file-operations` creates, renames and deletes. Redo redoes it. When part of it
changed since (the user typed in one of its documents, a file changed on disk), rhun asks whether
to undo in that file only or in the files that did not change. Extensions see the changes as
ordinary `edits`/`changed` events and disk changes (`workspace_watch`). A crash while an edit is
being written is rolled back at the next start from a journal in rhun's state folder.

**Limits.** 4 MiB body, 1000 entries (file operations included), 100000 edits; a closed file
16 MiB; 64 MiB read and kept by one edit in all (closed files and the originals of overwritten
and deleted files); a recursive delete 10000 entries. Over a limit: `toobig`.

Without the feature, workspace edits keep v1's behaviour exactly (files that are not open open
in tabs, unsaved), and an edit across several documents still undoes as one step.

## Feature: `file-operations`

Requires `workspace-edits` (rhun agrees it only together with it). The N entries of a workspace
edit may then also be file operations, in any mix with text groups:

```text
create PATH_LEN FLAGS LF <PATH>
rename FROM_LEN TO_LEN FLAGS LF <FROM><TO>
delete PATH_LEN FLAGS LF <PATH>
```

Paths are absolute. `FLAGS` is `-` or a comma-separated list: for `create`, `overwrite`
(replace an existing file with an empty one) and `ignore-existing` (skip the entry when the path
exists); for `rename`, `overwrite` (replace an existing file at TO; a folder is never replaced)
and `ignore-existing`; for `delete`, `recursive` (a folder and everything in it; without it only
an empty folder or a file) and `ignore-missing`. `overwrite` wins over `ignore-existing`. An
unknown flag is `syntax`. A `create` path ending in `/` makes a folder. Missing parent folders of
a create or rename target are made.

**Order.** rhun makes the entries in phases, each phase in listed order: creates, text groups,
renames, deletes. Text groups name files as they are after the creates and before the renames;
a text group for a file an earlier `create` makes takes PRE `-` (its text is empty). The SDKs'
normalisers map LSP's ordered `documentChanges` onto these phases.

**Refused** (`refused`): a path that is the source or target of two create, rename or delete
entries; a text group for a deleted path or for a rename target; an entry inside a folder another
entry renames or deletes (text groups excepted); a rename or delete of the project folder or a
folder above it; anything inside `.git`; deleting, or overwriting, an open document with unsaved
changes; a rename to another volume; a target that exists without `overwrite` or
`ignore-existing`; a missing source without `ignore-missing` (`nofile`).

**Tabs follow.** A renamed open file keeps its tab, text, unsaved changes, cursor and scroll.
Extensions see `0 closed OLD` and then `0 opened NEW VERSION LANG PATH` with a new DOC (a
reopened file gets a new id, v1 section 6). A deleted open file's tab closes (`0 closed`).
Undo renames back (tabs follow back) and brings deleted files and their tabs back. A case-only
rename (`util.go` to `Util.go`) works on case-insensitive volumes.

## Feature: `selections`

| Message | Form | Meaning |
| --- | --- | --- |
| event | `0 selections DOC N LEN` + N lines `L1 C1 L2 C2` | replaces `selection` for this extension (same `[subscribe] selection` patterns): every selection, anchor then cursor, **the primary first**; sent at most once per frame when the set changes, except when the set is one caret that moved (`cursor` says that); at catch-up for the active document |
| request | `ID select-many DOC N LEN` + N lines `L1 C1 L2 C2` | `ok ID`; sets the selections, the first becoming primary; overlapping ones merge; N is 1 to 512 (`toobig`); `badpos` as for `select`; a document that is not on screen keeps them |

The lines on screen come with feature `decorations`' `visible` event. The **primary** selection
is the most recently added one; scrolling, the status bar and the `cursor` event follow it. With the feature agreed, `select` collapses to one selection (as
without it), and `insert` inserts the body at every selection, as typing would; `replace` is
unchanged. At most 512 selections; they never overlap.

## Feature: `reveal`

| Request | Reply | Effect |
| --- | --- | --- |
| `ID reveal DOC LINE COL WHERE` | `ok ID` | scrolls the document's view so the position shows: `WHERE` is `top`, `center`, `nearest` (as little as needed) or `center-if-outside`; the cursor does not move and no tab is activated (a background tab keeps the place for when it is shown); with `uris` agreed, DOC may be the URI (without spaces) of an open document: a virtual document's, or a `file:` URI; `nodoc`, `badpos`, `syntax` |
| `ID show-locations LEN` + body | `ok ID` | the body's first line is a title (at most 200 bytes), the rest a 12d locations body; rhun lists them as Find All References does (even one) and choosing one opens it; `busy` while a prompt or the palette is open; 12d's limits |

## Feature: `diff`

| Request | Reply | Effect |
| --- | --- | --- |
| `ID diff LEFT RIGHT FLAGS LEN` + TITLE | `ok ID DOC` | a read-only tab showing a unified line diff from document LEFT to document RIGHT (open documents, `nodoc` otherwise; with `uris` agreed, a side may be a URI without spaces, as below), drawn like the git changes view; TITLE one line, at most 200 bytes; FLAGS `show` (activate it) or `background`; the same pair and title again reuses the tab; it follows both sides as they change (at most every 100 ms) and closes when either closes |
| `ID diff-close DOC` | `ok ID` | closes one of this extension's diff tabs; `nodoc` otherwise |

DOC is the diff tab's own id; it is not a document (no `opened`, `text` or edits). The user may
close it. A side given as a URI (feature `uris`): a `file:` URI names the open document of that
path; any other names a virtual document, which opens without a tab when it is not open (no
`opened`; its provider is asked for the content, and the diff follows it as it arrives) and closes
with the last diff tab that shows it, unless the user opened the URI meanwhile (`toobig` when too
many virtual documents are open). Write a space in a URI as `%20`: fields are split at spaces. A side over 16 MiB shows "too large to compare"; past 1000 differing regions the rest
is one change. At most 32 diff tabs per extension (`toobig`); they close when it stops.

## Feature: `linked-editing`

`[provides] linked = PATTERNS`. rhun asks `ID linked DOC VERSION LINE COL` when the cursor has
rested 150 ms (after a move or an edit) inside a word of a matching document, with one selection
and no linked session; `0 cancel ID` when it moves first; it waits 1 s. One provider: the first
that agreed `linked-editing` and matches. The answer is `ok ID LEN` and:

```text
N LF                        0 for none, else 2 to 64
N lines: L1 C1 L2 C2 LF     ranges at VERSION with equal text, not overlapping, one containing LINE COL
optional extra record: word LEN LF <LEN bytes>
                            the ASCII characters that may be typed in a range besides letters,
                            digits and non-ASCII; default "-_.:"
```

While linked, typing in one range changes all of them (one undo step per edit) and they are
outlined; typing another character, Escape, moving out of the ranges or Undo ends it. A body
that breaks a rule, or comes for an older version or another cursor place, is dropped.

## Lifecycle: `idle = stop` and feature `idle`

An extension that is only needed while it works may let rhun stop it when it is quiet:

```ini
# keep (the default): it runs until rhun quits | stop: rhun stops it after idle.after seconds
idle = stop
# 10 to 86400; 300 (5 minutes) when left out
idle.after = 300
```

"Work" is any message in either direction. rhun stops an `idle = stop` extension once
`idle.after` seconds passed without work, unless it holds something: an open prompt (`choose`,
`ask`, `term`), a request either way that is not answered yet (including secret requests), an
open view, diagnostics marks, status text, a command waiting for it to start, or `busy yes`. The
stop is v1's (`0 bye`, then TERM after a second, KILL after another); its runtime commands are
removed. Its state returns to `idle` (reason `idle`), and it starts again on its next command or
provider request (the request waits for it, as in single-provider selection) and on `lang:`,
`open:` and `contains:` activations for documents opened after the stop. `on = startup` and
`on = project` do not fire again. The Extensions page says "Stops after 5 min without work".

With feature `idle` agreed:

| Request | Reply | Effect |
| --- | --- | --- |
| `ID busy yes` | `ok ID` | it is not stopped for idleness until `busy no` or its next start |
| `ID busy no` | `ok ID` | |

Any other word is `syntax`; without the feature, `unknown`. Use it around work rhun cannot see
(a background index, a long download).

**Starting many.** `on = startup` extensions start at most four a frame; the rest start in the
following frames. The first frame never waits for extensions.

**Cost.** The Extensions page shows, per extension: how long it has run and how long its last
start took until `hello`, CPU time and use now, memory and processes (the program and its
descendants: `/proc` on Linux, `proc_pid_rusage` on macOS, its job on Windows), messages and
bytes each way, and how rhun's requests were answered: slow (completion and signature help after
500 ms, others after half their deadline), dropped (after rhun cancelled the request, gave up on
it after 5 s, or no longer waited for it) and failed (an `error` other than `cancelled`).

## Feature: `secrets`

Passwords and tokens kept for the extension by the system: the macOS login keychain, the
Windows Credential Manager, the Linux Secret Service (gnome-keyring, KWallet 5.97 or later,
KeePassXC). The manifest says so, and the install dialog and the page show it ("store passwords
and tokens in your keychain"):

```ini
[needs]
secrets = yes
```

| Request | Reply |
| --- | --- |
| `ID secret-get KEY` | `ok ID 1 LEN` + the value, or `ok ID 0 0` when nothing is stored |
| `ID secret-set KEY LEN` + value | `ok ID` |
| `ID secret-delete KEY` | `ok ID`, also when nothing was stored |
| `ID secret-keys` | `ok ID LEN` + the keys, each followed by LF |

- KEY is 1 to 64 bytes of `a-z 0-9 _ . -` (else `syntax`). A value is 0 to 8192 arbitrary bytes
  (`toobig a value is at most 8192 bytes`). An extension keeps at most 64 keys
  (`toobig 64 keys` for a new 65th; changing an existing key works).
- Without `secrets = yes`: `needs secrets=yes`. Without the feature: `unknown`.
- Requests are answered in order, when the store answers (an unlock dialog may come first);
  rhun keeps reading the extension's other requests meanwhile. At most 8 secret requests of one
  extension wait at a time (`busy 8 secret requests are waiting`).
- `refused the keychain is locked, or access was cancelled or denied` and
  `refused no keychain is available` are the store's refusals.

Each store keeps the values under the service `rhun extension OWNER/NAME` and the account
KEY (Windows: the credential `rhun extension OWNER/NAME/KEY`, a value over 2560 bytes split into
`KEY`, `KEY#2`, `KEY#3`, `KEY#4`; Linux: items with the attributes `service` and `account` in the
default collection, the plain session algorithm over the session bus). Removing the extension
removes its secrets; the page lists the keys (never the values) with Forget and Forget all.

Where no Secret Service runs (no session bus, a headless server), values are kept in memory until
rhun quits, and the first one stored says "No keyring is running: TITLE's sign-in lasts until
rhun quits". The user setting `[extensions] secrets = system | memory | file` chooses; `file`
keeps them in `$XDG_STATE_HOME/rhun/secrets/OWNER--NAME`, mode 0600 in a 0700 folder, in plain
text: as safe as any file only the user can read.

Rust: `secret_get`, `secret_set`, `secret_delete`, `secret_keys`, `busy`; Go: `SecretGetContext`,
`SecretSetContext`, `SecretDeleteContext`, `SecretKeysContext`, `BusyContext`; C: `rx_secret_get`,
`rx_secret_set`, `rx_secret_delete`, `rx_secret_keys`, `rx_busy`.

## Feature: `uris` (URIs and virtual documents)

Local files stay plain absolute paths on the wire, in both directions; untitled documents stay
`#DOC`/`-`. URIs are for resources that are not local files: read-only documents an extension
provides (a macro's expansion, a file at a git revision, a generated view). Only an extension
that agreed `uris` sees one.

```ini
[provides]
# URI schemes whose documents it provides: the extension's name or NAME-something
schemes = lsp-expand lsp-generated
```

A **resource** field (the PATH of `open`, and of locations and workspace edits where those
features say so) may then hold `SCHEME:REST`: SCHEME is `[a-z][a-z0-9+.-]` of 2 to 32 bytes (so
`C:` stays a drive), REST 1 to 4095 bytes without LF, CR or NUL, compared byte for byte. A `file:`
URI (`file:///p`, `file://localhost/p`, `file:///C:/p`) is percent-decoded to its path; rhun never
sends one. Any other scheme must be provided by an installed extension, else `nofile`; when two
declare one, the first in `owner/name` order provides it. Reserved, never provided: `file
untitled rhun http https mailto data git remote`.

| Direction | Message | Reply / meaning |
| --- | --- | --- |
| extension | `ID open LINE COL URI` | v1's `open`: the document's tab opens (or activates) at once, `ok ID DOC`; the cursor goes to LINE COL once the text is in (0 0: the start) |
| host request | `ID content URI` | `ok ID LEN` + `LANG_LEN TITLE_LEN TEXT_LEN LF` and the three: LANG a grammar name with spaces as `_` (empty: by the URI's last segment, as for a file of that name), TITLE the tab's title (empty: the last segment), TEXT the document; `error ID CODE TEXT` shows in the tab |
| extension | `ID content-changed URI` | `ok ID`; rhun asks again for the open document with that URI and replaces its text, keeping cursor and scroll (`edits`/`changed` go to subscribers) |
| event to the provider | `0 content-closed URI` | its tab closed |
| event | `0 opened DOC VERSION LANG URI` | to `uris` extensions whose `[subscribe] opened` patterns match the URI's last segment (after the last `/`, else after the `:`); never to others |

The provider is the scheme's, not the extension that opened the URI. A provider that is not
running starts, and is asked once it said `hello`. A virtual document is read-only (`replace`,
`insert` and edits on it are `readonly`), never saved, never in the session; `text` and `range`
work on its DOC. Limits: the text is at most 4 MiB of UTF-8 (otherwise, or for an answer that does
not parse, the tab says why); rhun waits 5 s for the answer (then `0 cancel ID` and the reason in
the tab); 64 open per provider, 256 in all (`toobig`). Reload File asks the provider again.
Removing or turning off the provider closes its documents; a stopped one leaves them open.

Rust: `Feature::Uris`, `Handler::content` returning `Content { lang, title, text }`,
`Handler::content_closed`, `Ext::open_uri`, `Ext::content_changed`, `valid_uri`; Go:
`ContentProvider`, `Content`, `OpenURIContext`, `ContentChangedContext`, `ParseContentClosed`,
`ValidURI`; C: `rx_handlers.content` with `rx_content`, `rx_handlers.content_closed`,
`rx_content_changed`, `rx_uri_valid`.

## Feature: `urls` (opening URLs, link callbacks)

An extension asks rhun to open a web page (a sign-in page, documentation, an issue) and gets
links back from the browser. The user always sees what is opened.

| Direction | Message | Reply / meaning |
| --- | --- | --- |
| extension | `ID open-url LEN` + URL | `ok ID opened`, `ok ID copied` (the user copied the link instead), or `error ID cancelled ...` |
| extension | `ID external-uri LEN` + URI | `ok ID LEN` + the URI the user's browser can use for it |
| extension | `ID link` | `ok ID LEN` + `rhun://ext/OWNER/NAME/TOKEN` |
| event | `0 link ID LEN` + the link | a link made by request ID reached rhun |

- **open-url.** URL is `http://` or `https://`, 1 to 8192 bytes without spaces or control
  characters (else `syntax`). rhun asks "TITLE wants to open HOST in your browser: URL" with
  Open, Copy Link, Cancel and Always open links from TITLE (Escape cancels); Open and Always open
  it in the default browser. Always is kept in the extension's section of `extensions.ini`
  (`open_urls = yes`) and its page shows a switch to take it back; such an extension's URLs open
  at once. While another prompt or the palette is open the request is `busy`.
- **external-uri.** For an `http(s)` URL (a loopback redirect `http://127.0.0.1:PORT/...`) or a
  `rhun:` link, else `syntax`. On this computer the URI comes back unchanged; where the extension
  runs on another host, the reply is the address that reaches it from the user's browser (a
  forwarded port). Pass every URL the browser must reach through it.
- **link.** TOKEN is 32 lowercase hex digits from the system's random source (else `refused no
  random source`); the link is one-shot and valid for ten minutes, and an extension has at most 4
  pending (`toobig 4 links are pending`). Append a path and query, for instance as an OAuth
  redirect URI: `rhun://ext/OWNER/NAME/TOKEN/callback`. When the link is opened (the system hands
  `rhun://` links to rhun, as for `rhun://file/`), the extension that asked gets `0 link ID LEN`
  and the whole link as it arrived, with the path and query; the token is spent. The scheme and
  `ext` match in any case, the token exactly. A link nobody expects (unknown, spent, expired)
  says "This link is not expected by any extension (it may have expired)". An `idle = stop`
  extension is not stopped while a link of it is pending; when its program exits, its links go.
- **Instances.** On macOS the running rhun receives the link. On Linux the system starts
  `rhun URL`; that rhun offers the link to the rhun that made it and quits before opening a
  window. While a link is pending, the rhun that made it listens on `DIR/link-PID` (`DIR` is
  `$XDG_RUNTIME_DIR/rhun`, else `/tmp/rhun-UID`, mode 0700 and owned by the user: rhun uses no
  other) for one line `link URL`, answered `yes` (taken) or `no`. On Windows a link reaches only
  the rhun that made it: use a loopback redirect there.

**Signing in.** OAuth's authorization code flow with PKCE (RFC 7636): listen on
`127.0.0.1:0`, pass `http://127.0.0.1:PORT/callback` through `external-uri`, open the provider's
authorize URL (with `state`, `code_challenge` and `code_challenge_method=S256`) with `open-url`,
read the redirect's `code` and `state`, exchange the code with the verifier, and keep the token
with `secret-set`. The listener and the token exchange need `[needs] network = yes`. A provider
that cannot redirect to a loopback address takes a `link` as its redirect URI instead.

Rust: `Feature::Urls`, `Ext::open_url` (`UrlOpened`), `Ext::external_uri`, `Ext::link`,
`parse_link`, and `auth::{Pkce, Loopback, random_hex, percent_encode, parse_query}`; Go:
`OpenURLContext`, `ExternalURIContext`, `LinkContext`, `ParseLink`, `NewPKCE`, `ListenLoopback`,
`RandomHex`; C: `rx_open_url`, `rx_external_uri`, `rx_link`, `rx_parse_link`,
`rx_handlers.link`, and with `RHUNEXT_AUTH` `rx_pkce_new`, `rx_loopback_listen`,
`rx_loopback_wait`, `rx_query_get`, `rx_random_hex`.

## Feature: `files` (finding files and searching their text)

An extension with `[needs] project = read` (or `write`) lists the workspace's files by glob and
searches their text through rhun, the same way on every system and with nothing installed. Where
git runs (git on, the project trusted), `git -C FOLDER ls-files --cached --others
--exclude-standard` says which files a folder has, so its repository's `.gitignore`, `info/exclude`
and the global excludes apply exactly as git applies them; for a folder in no repository, or when
the request asks for ignored files too, rhun walks the folder. The `exclude` setting's names (`.git node_modules target build ...`)
are left out unless ignored files are asked for; `.git` is always left out. Unsaved editor text is
searched instead of the file on disk.

| Request | Answer |
| --- | --- |
| `ID find-files FOLDER MAX FLAGS LEN` + globs | `0 found ID LEN` batches, then `ok ID COUNT STATUS` |
| `ID search-text FOLDER MAX FLAGS LEN` + `PATTERN_LEN GLOBS_LEN` LF, the pattern, the globs | `0 found ID LEN` batches, then `ok ID MATCHES FILES SKIPPED STATUS` |

- `FOLDER` is a folder id (`1` is the primary folder; see `workspace-folders`) or `0` for every
  folder, else `nofile no such folder`. `MAX` is 1 to 100000 paths, or 1 to 20000 matches
  (`toobig`); `0` is `syntax`.
- `FLAGS`, decimal bits: 1 ignored files too (a walk without `.gitignore` and the `exclude`
  setting), 2 skip hidden names (a path segment starting with `.`), 4 follow links to folders (a
  link back to a folder on its own way is not followed), 8 list folders too (`find-files` only,
  with a trailing `/`); for `search-text` also 16 ignore case (Unicode simple case folding), 32
  whole words (letters, digits, `_`, and any non-ASCII character count as word characters), 64
  regex. Other bits are `syntax`.
- **Globs**: zero or more lines `+GLOB` (include) or `-GLOB` (exclude); no include line means
  every file. At most 64 lines of 1024 bytes (`toobig`). A glob is relative to the folder root
  with `/` between segments: `*` any run within a segment, `**` any number of segments (`src/**`
  takes the folder `src` too), `?` one character, `[abc]`, `[a-z]`, `[!a-z]` one byte of a set,
  `{a,b}` alternatives (32 for a line in all, no nesting). A glob without `/` matches the base name
  at any depth (`*.rs`, `Makefile`). An exclude matches a path when it matches the path or a folder
  on its way (`-node_modules`, `-target/**`). macOS and Windows ignore ASCII case, Linux does not.
  A line that is neither `+GLOB` nor `-GLOB`, or an unclosed `[` or `{`, is `syntax`.
- **Pattern**: without 64, 1 to 4096 bytes of UTF-8 without LF (`toobig`, `syntax`). With 64, an
  RNRX program ("Portable binary programs" above; at most 256 KiB), validated before the run
  (`syntax`), matched line by line (without the line's LF and a CR before it), its own `i` flag
  instead of 16, every match reported (`g` is ignored); a line its matcher cannot finish within its
  work budget has no matches. Empty matches are reported, and the next one starts a character on.

Batches (`0 found ID LEN`): `N` LF, then `N` records, at most 2000 of them and 256 KiB in all.

```
find-files:   FOLDER PATH_LEN LF <PATH>
search-text:  FOLDER DOC VERSION PATH_LEN M LF <PATH>, then M matches:
              LINE COL END PRE TEXT_LEN LF <TEXT>
```

PATH is relative to its folder, `/`-separated on every system. `COL`..`END` is the half-open byte
range of the match on `LINE` (one-based, as in section 6). `TEXT` is the line, or for a line over
512 bytes up to 512 bytes of it starting at most 128 bytes before the match, cut at characters;
`PRE` is the column of TEXT's first byte. `DOC VERSION` are `0 0` for text read from disk, else the
open document whose current text was searched. A file with many matches can come as several
groups. Files over 8 MiB and files with a NUL in their first 8 KiB are skipped and counted in
`SKIPPED`. Order: folder order, then git's order or the walk's.

`STATUS` is `complete`, `limit` (MAX reached) or `partial` (a folder could not be read entirely);
everything listed is valid. One run per extension at a time (a second is `busy`), 4 run at once in
all and 16 wait (`busy` past that). `ID2 cancel ID` ends a run with `error ID cancelled` and no
further batches. A run waits while the extension has more than 256 KiB to read, so a slow reader is
never stopped for not reading; runs share a few milliseconds of each frame, so typing never waits.
An `idle = stop` extension is not stopped while a run of it goes on.

Rust: `Ext::find_files`, `Ext::search_text` with `files::FileQuery`, `files::Pattern` and the
batches through a callback that returns false to cancel, `files::Finished`; Go:
`FindFilesContext`, `SearchTextContext`, `FileQuery`, `Pattern`, `CompileSearchRegex` (a regex
source to the RNRX program), `ParseFoundPaths`, `ParseFoundFiles`; C: `rx_find_files`,
`rx_search_text` with `rx_file_query` and a batch callback. The checker walks its project folder
and answers RNRX searches with no matches.

## Feature: `scoped-settings` (settings per workspace, folder and language)

An extension's own `[settings]` (v1 section 2) can say that a workspace or a folder may set a key,
and that a key may differ per language:

```ini
[settings]
style.type = choice
style.options = gofmt goimports
# user (the default: only ~/.config/rhun/extensions.ini) | workspace | folder
style.scope = folder
# yes: style@LANG values apply (LANG a grammar name with _ for spaces, as in opened)
style.languages = yes
```

Values live in `[owner/name]` sections as `setting.KEY = VALUE` and `setting.KEY@LANG = VALUE`:
the user's in `~/.config/rhun/extensions.ini` (v1's file), the workspace's in the primary folder's
`.rhun/extensions.ini`, another folder's in its own `.rhun/extensions.ini`. A project file sets only
keys whose scope reaches it (`workspace` or `folder` for the workspace's file, `folder` for a
folder's) and `@LANG` values only for keys with `languages = yes`; other lines, sections of other
extensions and values that do not fit the type are ignored (a number is clamped). A folder that is
not trusted (a `rhun://` link opened it and the user has not trusted it) contributes nothing. So a
cloned repository changes nothing an extension did not open to it: never give `workspace` or
`folder` scope to a key that names a program, a path or arguments (the checker warns).

- `0 setting KEY VALUE` (v1) carries the **window value**: the workspace's, then the user's, then
  the default (values without a language). For a `user` key it is exactly v1's value.
- **`ID setting-get KEY FOLDER LANG`** → `ok ID LEN` + the value. `FOLDER` is a folder id (see
  `workspace-folders`; `1` is the primary folder) or `0` for the window; `LANG` is a grammar name
  with spaces as `_`, or `-`. The first value found of: folder@LANG, folder, workspace@LANG,
  workspace, user@LANG, user, default, skipping the scopes the key does not reach and `@LANG`
  values unless `KEY.languages = yes`. Errors: `refused not a setting of this extension`, `nofile no
  such folder`, `syntax`.
- **Event `0 setting-changed KEY`**: a stored value of KEY changed at some scope (a project file
  edited on disk, the user's value on the page), after `0 setting` when the window value changed
  too; at most once per key for each change. The extension asks again for the folders and
  languages it cares about. Project files are read when the project opens and checked once a
  second.

The details page shows the window value, and under a key the workspace sets "This workspace sets
VALUE (.rhun/extensions.ini)" with Open.

Rust: `Ext::setting_get`, `Feature::ScopedSettings`; Go: `SettingGetContext`,
`ParseSettingChanged`; C: `rx_setting_get`.

## Feature: `workspace-folders` (several folders in a window)

A window has zero or more workspace folders, 16 at most; the first, the primary, is what
`0 ready PROJECT` names, what a relative `open` path is relative to, and what `RHUN_PROJECT` holds.
Folders do not nest, so a file is in one folder at most. `project = read|write` covers every folder
(the sandbox has a rule for each), and find-files and search-text go through all of them.

**Event `0 folders LEN`** + body, after `0 features` and before `0 ready`, and again (the whole
list) whenever the folders change for a program that keeps running:

```
N LF
N records: FOLDER LOCATION FLAGS NAME_LEN PATH_LEN LF <NAME><PATH>
```

`FOLDER` is a positive id that stays the same for the rhun session (removing a path and adding it
again gives a new one). `LOCATION` is `local` (a reader treats another word as a folder it cannot
reach). `FLAGS`, decimal bits: 1 the primary, 2 trusted (opened by hand, or a repository the user
trusted after a `rhun://` link opened it). `NAME` is the display name, unique in the window (a second
`src` is `src 2`); `PATH` is absolute, native separators, no trailing one. `N` is 0 in a window
without a project.

**`ID folder-of PATH`** → `ok ID FOLDER`, `0` when the path is in no folder; PATH is the rest of the
line (with `uris` a `file:` URI too); links are resolved when the path as written is in none.

When only other folders change, a running extension with `project = read|write` restarts (its
sandbox and catch-up describe the new set); one without project access gets `0 folders` and keeps
running. A new primary folder restarts every extension, as in v1. Manifests may start an extension
when a folder has a file: `on = contains:Cargo.toml contains:go.mod contains:*.cabal` (a name or a
relative path up to three levels at a folder's top, or `*.EXT` at its top level), checked when the
folders change.

Folders added to a project are remembered with its primary folder. A workspace file,
`NAME.rhun-workspace`, names its folders in `[folders]` (`path = ` lines, relative to the file or
absolute) and holds workspace settings for extensions in `[owner/name]` sections (see
`scoped-settings`); opening it opens its folders, and with it the primary folder's
`.rhun/extensions.ini` counts as that folder's own.

Rust: `Feature::WorkspaceFolders`, `parse_folders`, `Ext::folder_of`; Go: `ParseFolders`,
`FolderOfContext`; C: `rx_parse_folders`, `rx_folder_of`. The checker sends its project as the one
folder.


## Feature: `diagnostic-tags`

With `diagnostic-tags` agreed, a diagnostics line's SEVERITY (v1 section 6b) may carry tags:
`SEVERITY+TAG[+TAG]`, for example `hint+unnecessary` or `warning+deprecated`. TAG `unnecessary`
draws the range's text faded (halfway to the background; a `hint` then draws no underline),
`deprecated` draws a line through it. Unknown tags are ignored, so a later tag can be sent to any
host that agreed the feature. Without the feature the v1 grammar holds exactly: `hint+unnecessary`
is a `syntax` error.

`info` diagnostics use the theme color `info` (the accent unless the theme sets it) and `hint`
diagnostics `hint` (muted), for the underline and the dot in the gutter's left lane.

## Presentation providers

Folding ranges, semantic tokens, inlay hints, code lens, colors and highlights are asked for by
rhun, about the **active document only**, without the user doing anything. Each is a feature
listed in `hello 1 ...` plus a `[provides]` key whose patterns match the document:

| Feature | `[provides]` key | Providers asked |
| --- | --- | --- |
| `folding-ranges` | `folding` | one: the language's (section Single-provider selection; never a chooser) |
| `semantic-tokens` | `semantic_tokens` | one, as above |
| `document-highlights` | `highlights` | one, as above |
| `inlay-hints` | `inlay_hints` | every matching one; answers are merged |
| `code-lens` | `code_lens` | every matching one |
| `document-colors` | `colors` | every matching one |

When rhun asks:

| Feature | Asked | After edits |
| --- | --- | --- |
| folding ranges | when the document becomes active, and after edits | 500 ms after the last edit |
| semantic tokens | as above | 150 ms |
| inlay hints, colors | as above, and when a scroll leaves the lines last asked about | 250 / 500 ms; 100 ms after a scroll |
| code lens | as above | 500 ms; 100 ms after a scroll |

At most one request per document, feature and provider is out at a time: a newer one withdraws
the older with `0 cancel ID` (v1.1 Capacity and shared scheduling), and requests about a document
that is no longer active are withdrawn. These requests count against the 64 a host waits on and
leave the last 16 to the user's own requests. An answer that does not come within the kind's
time (30 s for folding ranges and semantic tokens, 10 s for hints, lens and colors, 5 s for
highlights) is withdrawn the same way. An error answer changes nothing; an answer that breaks its
grammar or caps is dropped whole (`query-stats` counts it).

The requests carry the document's VERSION. Lines are 1-based. An answer about an older version
is moved onto the current text through the edits made since (as for `decorate`); when those edits
are no longer kept the answer is dropped and the next request asks again.

### Feature: `folding-ranges`

| Request (rhun) | Answer |
| --- | --- |
| `ID folding-ranges DOC VERSION` | `ok ID LEN` + body |

```
N LF
START END KIND TEXT_LEN LF <TEXT>      (N times)
```

A range lets the lines START+1..END hide behind line START, which shows the collapsed text: TEXT
when there is one (one line without control characters, at most 80 bytes), otherwise ` ⋯ `. KIND
is `region`, `comment`, `imports` or `-` (an unknown kind counts as `-`). A range with START not
before END is left out. At most 50,000 ranges; more, or a malformed body, drops the answer.

Once a provider has answered for a document, its ranges replace rhun's indentation ranges there:
the fold lane's chevrons, Fold, Fold All and Fold Level N use them. Ranges move with edits until
the next answer replaces them; when the provider stops, the document goes back to indentation.
Folds already made stay.

```
← 7 folding-ranges 3 12
→ ok 7 30
→ 2
→ 1 9 - 0
→ 3 5 region 6
→ consts
```

### Feature: `semantic-tokens`

| Message | Answer |
| --- | --- |
| `ID semantic-legend LEGEND LEN` + body (extension) | `ok ID` |
| `ID semantic-tokens DOC VERSION PREV` (rhun) | `ok ID LEN` + `RESULT LEGEND full N` LF and 5N ints, or `RESULT LEGEND delta K` LF and K edits |
| `ID semantic-tokens-range DOC VERSION L1 L2` (rhun, for a document over 200,000 lines or 8 MB) | `ok ID LEN` + `RESULT LEGEND range N` LF and 5N ints |

**Legends.** LEGEND names a legend (`[A-Za-z0-9_.-]`, 1 to 64 bytes; a language server's, say).
Its body is one line of token types and one of modifiers, names separated by spaces, each name
1 to 64 bytes of `[A-Za-z0-9_.-]` and optionally `=EFFECT`. An extension has at most 8 legends of
at most 256 types and 64 modifiers, a body at most 16 KB (`toobig`); sending a legend again
replaces it. A bad name or effect is `syntax` and changes nothing.

**Answers.** RESULT is the answer's id (`[A-Za-z0-9_.-]`, 1 to 64 bytes) or `-`; rhun sends it
back as PREV with its next request for that document (`-` when it has none), and the provider may
then answer with a delta against it. The ints are decimal, separated by one space or line feed, in
LSP's layout with byte columns: per token `DLINE DCOL LEN TYPE MODS`, DLINE the lines after the
previous token's (the first counted from line 1), DCOL the columns after the previous token's start
on the same line (from the line's start on a new line), TYPE an index into the legend's types and
MODS a bit set of its modifiers (64 bits: bit i is the legend's modifier i). A delta's edits are `START DEL N` LF and N ints: at index START of
the previous result's ints, DEL ints are replaced by these N (edits sorted, not overlapping,
indices into the previous array). A range answer holds the tokens of lines L1..L2 and has no
delta. At most 200,000 tokens and 10,000 edits; a malformed answer is dropped. A delta rhun cannot
apply (it has no such result, an edit runs past the array) drops the document's tokens, and rhun
asks again with `-`.

Tokens are single-line: one running past its line's end is cut there, one starting inside a
character is left out. Where tokens overlap the later one wins. An answer about an older version
becomes the base for the next delta, but its positions are not used: the request the edit
scheduled brings positions for the current text, and meanwhile the tokens drawn move with the edits
(text typed inside a token joins it).

**Classes.** Each token is drawn in a syntax class: the type's EFFECT when it has one, otherwise
rhun's table, otherwise the syntax class stays.

| Token types | Class |
| --- | --- |
| namespace type class enum interface struct typeParameter concept typeAlias union | `type` |
| builtinType | `builtin` |
| parameter variable property event | `variable` |
| enumMember const static boolean label | `constant` |
| function method | `function` |
| macro | `preproc` |
| decorator attribute builtinAttribute derive | `attribute` |
| keyword modifier selfKeyword selfTypeKeyword lifetime | `keyword` |
| comment, string, number | `comment`, `string`, `number` |
| regexp escapeSequence formatSpecifier | `escape` |
| operator arithmetic bitwise comparison logical | `operator` |
| bracket brace parenthesis punctuation angle colon comma dot semicolon | `punctuation` |
| anything else (`unknown`, `generic`, `unresolvedReference` …) | the syntax class stays |

EFFECT is a class name (`text keyword type function string number comment constant operator
punctuation preproc variable builtin attribute tag heading inserted deleted escape link`), a style
(`strike`, `underline`, `faded`) or `-` (nothing). A type with a style keeps the syntax class. A
modifier's class effect replaces the type's (the first such modifier in legend order wins); styles
add up. Without an EFFECT, `deprecated` strikes the text and `defaultLibrary` turns `function`,
`type` and `variable` into `builtin`; other modifiers do nothing. Strikes and underlines take the
token's color, faded text is halfway to the background. `semantic_highlighting = false` under
`[editor]` turns tokens off: rhun neither asks nor draws them.

```
→ 1 semantic-legend ra 53
→ function variable macro
→ declaration mutable=underline
← ok 1
← 4 semantic-tokens 3 12 -
→ ok 4 32
→ r1 ra full 2
→ 0 3 4 0 1 1 8 1 1 2
```

### Feature: `inlay-hints`

| Request (rhun) | Answer |
| --- | --- |
| `ID inlay-hints DOC VERSION L1 L2` | `ok ID LEN` + body |

L1..L2 are the lines asked about: those on screen and one screen more each way. Every provider
whose `inlay_hints` patterns match is asked; an answer replaces that provider's hints on those
lines (others keep theirs).

```
N LF
LINE COL SIDE NPARTS EXTRA_LEN LF                        (N times)
  LABEL_LEN TIP_LEN PEXTRA_LEN LF <LABEL><TIP><PEXTRA>   (NPARTS times)
  <EXTRA>
```

A hint sits at LINE COL (v1 positions; a column one past the line's end is its line feed). SIDE
`after` keeps it after the text before it (an inferred type), `before` keeps it before the text
after it (a parameter name); see the virtual text rules under Feature `decorations`. Its label is
its parts' LABELs side by side (1 to 8 parts, at most 120 bytes in all, one line without control
characters), drawn in the theme's `inlay` color on `inlay_bg`. TIP (at most 4000 bytes) is shown
while the pointer rests on the part. PEXTRA (at most 9000 bytes) holds the part's extra records:
`command NAME LEN` (section Command references) runs when the part is clicked; `goto LINE COL LEN`
LF and LEN bytes of an absolute path (none: this document) opens that place instead. EXTRA (at most
64 KB) holds up to 16 records `edit L1 C1 L2 C2 LEN` LF and LEN bytes of text: a double click on
the hint makes these replacements in its document as one undo step, moved onto the current text
when it changed since the answer. Records a host does not know are skipped.

At most 5,000 hints an answer and 50,000 a document; more, a malformed hint, or a label over its
limits drops the answer. `inlay_hints = false` under `[editor]` turns hints off: rhun neither asks
nor draws them.

```
← 5 inlay-hints 3 12 1 40
→ ok 5 28
→ 1
→ 2 10 after 1 0
→ 5 0 0
→ : i32
```

### Feature: `code-lens`

| Request (rhun) | Answer |
| --- | --- |
| `ID code-lens DOC VERSION L1 L2` | `ok ID LEN` + body |

Every provider whose `code_lens` patterns match is asked; an answer holds **every** lens of the
document and replaces all of that provider's. L1..L2 are the lines around the view: a provider
fills in at least their titles (resolving a lens can be slow) and may leave others empty.

```
N LF
LINE TITLE_LEN EXTRA_LEN LF <TITLE><EXTRA>     (N times)
```

A line with lenses gets a row above it, its lenses' titles side by side in the small interface
font at the line's indentation, ` | ` between them, eight at most, in the theme's `codelens` color.
An empty TITLE is a lens not resolved yet: its row is kept and it is drawn `…`; rhun asks again
when a scroll leaves the lines asked about. TITLE is one line without control characters, at most
200 bytes. EXTRA (at most 9000 bytes) holds `command NAME LEN` (section Command references), run
when the title is clicked, and `tip LEN` LF and LEN bytes of text, shown while the pointer rests on
it. A lens stays with its line as the text is edited. The caret steps over lens rows; when lenses
come or go above the view's top line the text stays where it was (at the document's top, the first
line's lens row shows). At most 10,000 lenses an answer; more, or a malformed lens, drops the
answer. `code_lens = false` under `[editor]` turns lenses off.

```
← 6 code-lens 3 12 1 40
→ ok 6 26
→ 1
→ 1 3 14
→ Runcommand run 0
```

### Feature: `document-highlights`

| Request (rhun) | Answer |
| --- | --- |
| `ID highlights DOC VERSION LINE COL` | `ok ID LEN` + body |

```
N LF
L1 C1 L2 C2 KIND LF                            (N times)
```

rhun asks the language's provider (`[provides] highlights`) when the caret has rested for 250 ms
inside a word or at its end with nothing selected; LINE COL is the caret. The answer's ranges
(KIND `text`, `read` or `write`) are drawn behind the text in the theme's `highlight` color,
`write` ones in `highlight_write`. They go when the caret moves out of every range, and with any
edit. At most 5,000 ranges; more, or a malformed line, drops the answer. Without a provider, and
with `highlight_occurrences` on (the default), rhun marks the word's whole-word occurrences on the
400 lines each side of the caret itself.

```
← 9 highlights 3 12 2 9
→ ok 9 32
→ 2
→ 2 9 2 10 write
→ 3 13 3 14 read
```

### Feature: `document-colors`

| Request (rhun) | Answer |
| --- | --- |
| `ID colors DOC VERSION L1 L2` | `ok ID LEN` + `N LF` and N lines `L1 C1 L2 C2 RRGGBBAA` |
| `ID color-edit DOC VERSION L1 C1 L2 C2 RRGGBBAA` | `ok ID LEN` + a workspace edit |

Every provider whose `colors` patterns match is asked about the lines around the view (L1..L2)
when the document becomes active, 500 ms after edits and when a scroll leaves those lines; an
answer replaces that provider's colors. Each color gets a swatch: a two-cell inlay before its
range in that color (its alpha over the editor's background). A click on a swatch asks for a new
color in the palette (`#rrggbb` or `#rrggbbaa`, the current one filled in), then sends
`color-edit` with the color's range and the new color to the provider that found it; the answer
is a workspace edit (v1 section 12g, or 1.1's grammar with `workspace-edits` agreed) for the
document at that VERSION, made as one undo step. RRGGBBAA is eight hex digits. At most 10,000
colors an answer; more, or a malformed line, drops the answer. `color_swatches = false` under
`[editor]` turns swatches off: rhun neither asks nor draws them.

```
← 4 colors 3 12 1 40
→ ok 4 20
→ 1
→ 1 9 1 17 ff0000ff
← 5 color-edit 3 12 1 9 1 17 0000ffff
→ ok 5 30
→ 1
→ 2 12 1
→ #31 9 1 17 8
→ 0x0000ff
```

### Feature: `inline-complete`

| Request (rhun) | Answer |
| --- | --- |
| `ID inline-complete DOC VERSION LINE COL KIND` | `ok ID LEN` + body |

rhun asks the language's provider (`[provides] inline_complete`) after a typing pause
(`inline_complete_delay`, 300 ms by default; KIND `typed`, only with `inline_complete` on) or when
the user runs Trigger Inline Completion (alt+\, KIND `invoked`). LINE COL is the caret.

```
N LF
L1 C1 L2 C2 TEXT_LEN EXTRA_LEN LF <TEXT><EXTRA>    (N times, at most 8)
```

The first item that fits shows: its range holds the caret, the document's text from L1 C1 to the
caret is TEXT's start, and its text from the caret to L2 C2 is the end of the rest; the **ghost**
is what is left between them, drawn in the theme's `ghost` color after the caret (its first line)
and as rows below the caret's line (the others). TEXT is at most 64 KB and 50 lines; an answer
about an older version, or for another caret, is not used. EXTRA holds `command NAME LEN`
(section Command references), run after the whole item is taken, and `edit L1 C1 L2 C2 LEN`
records, made with it when the text is still the version answered.

Tab takes the whole item (after the completion list and a snippet's fields have had Tab): TEXT
replaces its range, as one undo step. Ctrl+Right takes the ghost's next word. Typing the
characters the ghost starts with keeps the rest; any other edit, a caret move or Escape dismisses
it. A newer request withdraws the older.

```
← 8 inline-complete 3 14 2 15 invoked
→ ok 8 23
→ 1
→ 2 15 2 15 7 0
→  // one
```

## Agents, proposals and tools

rhun reads and reviews; it drives no agent itself. Extensions can add agent sessions to the agents
panel (`agents`), propose edits the user reviews file by file (`edit-review`), declare tools other
extensions call through rhun (`tools`), and take prompts for their sessions (`agent-prompts`). The
first-party `rhun-editor/mcp` extension exposes the tools to agents in a terminal over MCP.

### Feature: `agents`

The manifest says `[provides] agents = yes` (and optionally `agents.title = Gemini CLI`, at most
80 bytes; the default is the extension's title). Without it `agent-sessions` is refused.

| Direction | Message | Answer |
| --- | --- | --- |
| extension → rhun | `ID agent-sessions LEN` + rows | `ok ID` |
| extension → rhun | `ID agent-changed SID TOTAL FIRST` | `ok ID`; `refused no such session` |
| rhun → extension | `ID agent-transcript SID FROM` | `ok ID LEN` + messages, or `error ID CODE TEXT` |

`agent-sessions` replaces all of the extension's rows. The body is `N` LF and N rows, each
`UPDATED STATE SID_LEN TITLE_LEN MODEL_LEN CWD_LEN` LF followed by the four strings without
separators. `UPDATED` is Unix seconds of the session's last change; `STATE` is `idle`, `working`,
`waiting` (for the user) or `error`; `SID` is 1 to 128 bytes of visible ASCII (0x21 to 0x7e),
unique in the list; `TITLE` and `MODEL` are at most 4096 bytes each (rhun shows the title's first
line, cut to 200 bytes, and the model cut to 120); `CWD` is an absolute path without NUL or LF, or
empty. At most 1000 rows and 1 MB (`toobig`); anything malformed is `syntax` and changes nothing.
Rows keep their place by session id: a row whose id is still listed keeps the messages rhun has.

A row is shown when its `CWD` is empty, the project, a folder inside it, or one of the
repository's checkouts (a linked worktree's row shows its name as a badge). Rows sort with the
built-in sessions by `UPDATED`; a row shows the extension's badge and the source title, the model,
the age, and `working`, `waiting for you` or `error`. Rows go when the extension stops. Settings >
Agents has a switch per source; switched off it is `[agents] OWNER/NAME = false` in the config.

When the user opens a session, rhun sends `agent-transcript SID FROM` with `FROM` 0 the first time,
later the first message it lacks. The answer is `TOTAL N` LF and N messages from index FROM, each
`ROLE NAME_LEN TEXT_LEN MODEL_LEN` LF followed by the three strings. `ROLE` is `user`, `assistant`,
`tool` (NAME the tool, TEXT a summary such as a command or a path), `result` (a tool's output,
shown as an excerpt), `error` or `info`. MODEL labels an assistant message (shown where it
changes). A text is cut at 70 KB; NAME and MODEL may be 4096 bytes; at most 2000 messages and
4 MB an answer. The answer replaces rhun's copy from FROM on; when FROM + N is less than TOTAL rhun
asks again from there. `agent-changed SID TOTAL FIRST` says the session has TOTAL messages and the
first changed one is FIRST: while the session is open rhun asks from FIRST, one request at a time.
An answer that does not come within 30 seconds is given up (`0 cancel ID`).

### Feature: `edit-review`

| Direction | Message | Answer |
| --- | --- | --- |
| extension → rhun | `ID propose LEN` + head + workspace edit | `ok ID PROPOSAL` at once |
| extension → rhun | `ID unpropose PROPOSAL` | `ok ID`; `refused no such proposal` |
| rhun → extension | `0 proposal PROPOSAL INDEX accepted` (or `rejected`, `stale`, `partial A N`) | |
| rhun → extension | `0 proposal PROPOSAL done ACCEPTED REJECTED` | |

`propose` needs `documents = read` (and `project` or `home` read for files that are not open: the
user's decision is the edit, as with a completion item). The head is `LABEL_LEN SID_LEN` LF, the
label (its first line shown, cut to 200 bytes) and an optional session id (as above, or empty),
followed by a workspace edit exactly as in v1 section 12g. rhun checks it as `apply` would: an
open document at its version (`-` only while it has no unsaved changes), a file that is not open
an existing text file of at most 4 MB the extension may read (CR LF read as LF), every range inside
its text and on character boundaries, no two edits overlapping, no file twice. Errors are those
of `apply` (`syntax`, `needs file=read`, `nofile`, `nodoc`, `stale`, `readonly`, `badpos`, `toobig`);
an edit that changes nothing is `refused`. At most 8 of the extension's proposals wait for the user
(`toobig`); the body is at most 4 MB plus the head.

rhun opens a Review tab: the label, the source, the files with their added and removed lines, and
each run of changed lines as a hunk with three lines of context. The user accepts or rejects a
file, a hunk, or everything (buttons on the title, file and hunk lines; keys `a` and `r` for the
file under the cursor, `A` and `R` for the hunk, `]` `[` and `}` `{` to move between files and
hunks, Ctrl+Enter to accept everything). Accepting replaces the hunks' lines through the ordinary
workspace-edit path, as one undo step per decision, unsaved; a file that is not open opens in a
tab. When a file's text is not what the proposal and the earlier decisions left (the user typed,
the file changed on disk), its open hunks are `stale` and nothing is applied. Closing the tab
rejects what is left.

Each file, once all its hunks are decided, gets one event: `accepted` (all hunks), `rejected`
(none, by the user), `stale` (none, changed meanwhile) or `partial A N` (A of its N hunks). INDEX
is the file's place in the workspace edit, from 1 (files the edit leaves unchanged get no event).
When every file is decided, `done ACCEPTED REJECTED` counts files accepted or partial against
files rejected or stale. `unpropose` closes the tab without further events (files accepted stay
changed). Headless: `print-review`, `review accept|reject FILE|all [HUNK]`.

### Feature: `tools`

A manifest declares tools:

```ini
[tools]
# NAME = Title | one-line description for the model
search_docs = Search Docs | Searches the project's documentation index
search_docs.input = query=TEXT (one line); limit=N (optional, at most 50)
search_docs.readonly = yes
```

NAME is 1 to 40 of `[a-z0-9_]`; at most 64 tools; title, description and input are cut to 1000
bytes. A tool that is not `readonly = yes` asks the user before each call.

| Direction | Message | Answer |
| --- | --- | --- |
| extension → rhun | `ID tools` | `ok ID LEN` + `N` LF and N records |
| extension → rhun | `ID tool-call TOOL LEN` + input | when done: `ok ID LEN` + output, or `error ID ...` |
| rhun → owner | `ID tool NAME CALLER LEN` + input | `ok ID LEN` + output, or `error ID CODE TEXT` |

A `tools` record is `TOOL FLAGS TITLE_LEN DESC_LEN INPUT_LEN` LF and the three texts; TOOL is
`rhun.NAME` or `OWNER/NAME.TOOL`; FLAGS is `ro` or `rw`. The list holds rhun's tools and those of
every installed extension that is neither turned off nor failed.

`tool-call` answers when the tool is done; the input and output are opaque to rhun for an
extension's tool (at most 4 MB each). rhun sends the owner `tool` with the tool's local NAME and
the CALLER as `OWNER/NAME`, starting the owner first when it is idle (a call that waits more than
10 seconds for the start fails `busy`); the owner's answer, or its error code and text, goes back
to the caller as it came. A call to a tool that changes anything waits for the user: a dialog
names the caller, the tool and the input's first line, with Allow once (the default),
Allow for this session (this caller and this tool, until rhun quits) and Deny (`error ID refused
denied by the user`). One dialog shows at a time; calls queue, at most 256 in all (`busy`). The
caller may withdraw a call with `cancel ID` (answered `error ID cancelled`; an owner already asked
gets `0 cancel`). When the owner stops, its calls fail `cancelled the tool's extension stopped`.
Errors of the call itself: `refused no such tool`, `refused the tool's extension is not running`,
`refused the tool's extension does not take tool calls` (it did not agree `tools`).

rhun's own tools run with the caller's `[needs]`. Their input is `KEY VALUE` lines (the value to the
end of the line), and `KEY LEN` LF and LEN bytes for `text` and `edit`:

| Tool | Input | Output | Needs |
| --- | --- | --- | --- |
| `rhun.editors` | | `DOC DIRTY LANG PATH` per open document (DIRTY 0 or 1, LANG as in `opened`, PATH `-` for an untitled one), then `active DOC` (0 for none) | documents read |
| `rhun.selection` | | `DOC L1 C1 L2 C2` LF and the selected text, of the active document (`nodoc` without one) | documents read |
| `rhun.read` | `path P` (absolute or in the project) or `doc DOC`; `from L`, `to L` (lines, optional) | the text: an open document's unsaved text, or the file's | documents read; a file not open: `project`/`home` read |
| `rhun.diagnostics` | `path P` (optional) | `PATH L1 C1 L2 C2 SEVERITY OWNER/NAME MESSAGE` per mark on an open document | documents read |
| `rhun.open` | `path P`, `line L`, `col C` | `DOC` | a file not open: `project`/`home` read |
| `rhun.propose` | `edit LEN` and a workspace edit, or `path P` and `text LEN` with the file's new text; `label TEXT` (optional) | when the user decided every file: `accepted A rejected R` | documents read |
| `rhun.run` | `command TEXT` | `ok` once Enter put it in the terminal; `cancelled` otherwise | terminal |

`rhun.propose` and `rhun.run` need no dialog: the Review tab and rhun's prompt are the user's
confirmation (`rhun.run` shows the command in the prompt as `term` does; `busy` while a prompt is
open). Headless: `print-tools` (the list, then each call in flight and its state) and
`tool-confirm once|session|deny`.

An extension may listen on a Unix socket inside its own state folder whatever `network` says: on
macOS the sandbox allows binding and accepting there, and on Linux such a socket is reachable
from the user's other programs (the MCP bridge relies on this).

### Feature: `agent-prompts`

With `agents` agreed too, the extension's sessions become interactive: the open session shows a
prompt box under its messages, Stop while its state is `working`, and the cards the extension asks.

| Direction | Message | Answer |
| --- | --- | --- |
| rhun → extension | `ID agent-prompt SID LEN` + text | `ok ID LEN` + session id, or `error ID CODE TEXT` |
| rhun → extension | `0 agent-stop SID` | |
| extension → rhun | `ID agent-ask SID LEN` + prompt line and option lines | `ok ID N` when the user picks option N (from 1); `error ID cancelled` |

Enter in the box sends its text (Shift+Enter is a new line; at most 64 KB; one prompt at a time);
Enter or Tab in the panel moves to the box, Escape leaves it. The answer to `agent-prompt` names the
session: the extension then lists or changes it as usual (`agent-sessions`, `agent-changed`), and
its messages show the prompt. An error, or no answer within 30 seconds, puts the text back in the
box and shows the reason (a refusal that comes after the user opened another session only shows).
The list's header has `+ TITLE` for each running extension that agreed `agents` and
`agent-prompts`, listed sessions and is switched on in Settings > Agents: it opens an empty session
with the box, and its first prompt is sent with `SID` `-`. The answer's session id opens once the
extension lists it (unless the user opened another session meanwhile). Stop sends `0 agent-stop SID` and nothing else: the extension says what changed.

A card is a prompt line (cut to 1000 bytes) and 1 to 16 options, one per line (each cut to 200
bytes; the body's last LF is optional), at most 8 KB. It shows under the session's messages with
its options as buttons; keys 1 to 9 pick while the panel has the focus, and the arrow keys move
among all of them (a card taller than its room scrolls to the chosen one) and Enter picks it. A card on a session that
already has one replaces it (the earlier is answered `cancelled`); the extension may withdraw it
with `cancel ID`; it is cancelled when its session's row goes or the extension stops. Errors:
`syntax a prompt line and 1 to 16 options, none empty`, `toobig more than 16 options`,
`refused no such session`. Headless: `agent-new OWNER/NAME`, `agent-prompt TEXT` (the text in the
box, then Enter), `agent-stop`, `agent-card N`, `print-agent-input`.


## Remote windows: `where` and the host

A remote window (`rhun --remote ssh:HOST PATH`, Connect to Host, Attach to Container, Connect to
WSL) has its project, files, terminals, git and extensions on another machine, a Linux x86-64
host. The protocol does not change there: paths stay plain absolute paths of the host, and the
pipe an extension reads and writes is relayed byte for byte.

An extension runs where the files are unless its manifest says otherwise:

```ini
where = local          # run on this computer even in a remote window (default: workspace)
```

- `where = workspace` (the default) runs it on the host. rhun copies its folder once into the
  host's `~/.cache/rhun-remote/extensions/OWNER/NAME/VERSION-HASH`; when the installed folder holds
  only this computer's build, its release's `NAME-linux-x86_64.tar.gz` (checked against the
  release's `sha256sums.txt`) is fetched instead, so an extension that runs on remote hosts ships a
  `run.linux-x86_64` build in its release, or is a script (`runtime` and `run`, the runtime found in
  the host's login `PATH`) or a WebAssembly module. Its environment is that of section 4 with the
  host's paths: `HOME`, `RHUN_STATE` and the caches in `~/.local/state/rhun/extensions/OWNER--NAME`
  on the host, `RHUN_PROJECT` the host's folder, `RHUN_TARGET=linux-x86_64`, and `RHUN_REMOTE` the
  window's remote (`ssh:HOST`, `docker:NAME`, `podman:NAME`, `wsl:DISTRO`). The host holds it to
  its `[needs]` with Landlock as a Linux computer would; when its kernel cannot, the extension's page
  says why, prefixed "on the host:". A folder with a link leading outside it, or with folders
  nested more than 16 levels deep, is not copied (the extension fails with the reason).
- `where = local` runs it on this computer, as in a local window. In a remote window it may not ask
  for `project` or `home` in `[needs]` (the manifest is refused there): the host's paths it sees
  are names only, and it reads documents through the protocol. It suits a spell checker, or a bridge
  to a service with this computer's sign-in. A local window ignores `where`.

When clauses read `remoteName` (`ssh`, `docker`, `podman`, `wsl`, or empty in a local window) and
`remoteHost` (the host or container). An extension that serves a page on the host's loopback
(`http://127.0.0.1:PORT/`) passes its URL through `external-uri` (feature `urls`) before
`open-url`: in a remote window rhun forwards the port and answers with the URL that reaches it from
this computer.

## WebAssembly extensions: `run.wasm32-wasip1`

An extension may be one WebAssembly module instead of a program per system. Compiled for
`wasm32-wasip1` (WASI preview 1), the same file runs on Linux, macOS and Windows, and rhun runs it
itself: no runtime to install, nothing else on `PATH`. The protocol is unchanged: the module reads
rhun's messages on its standard input and writes its own on its standard output.

```ini
run.wasm32-wasip1 = bin/NAME.wasm
```

- **Which program runs.** A `run.TARGET` for this system (or `runtime` with `run`) wins; the
  module runs when the manifest names no program here. A manifest may name both: native builds
  where it has them, the module everywhere else. A missing module fails the extension with the
  reason "the WebAssembly module named by run.wasm32-wasip1 is missing".
- **How it runs.** rhun starts its own executable as `rhun --wasm-host [--dir PATH | --dir-ro
  PATH]... FILE.wasm ARGS...`, a separate process like any extension program: one that spins,
  traps or exits does not hold or take the editor with it, and rhun stops it as it stops any
  program. The module's `_start` runs (a WASI command); `args` are the manifest's after the
  module's path. Its environment is the extension environment of v1 section 4.
- **Files.** The module sees, as WASI preopened directories named by their absolute paths, its
  state folder (`$RHUN_STATE`, read and write), its own folder (read only: the module's path,
  its first argument, says where it is), the project with `project = read` (read only) or
  `project = write`, and the home folder with `home = read` (read only). On Windows the names
  are `/C:/Users/me/proj`; the environment has `PWD=/`, so `C:/Users/me/proj/a.txt` (as
  `$RHUN_PROJECT` spells it) resolves there in every runtime. A path is resolved inside its
  folder and then by its real path: an absolute path elsewhere, a `..` that climbs out, or a
  symbolic link inside that leads out is refused (`ENOTCAPABLE`), and so is writing to a
  read-only folder. Standard libraries (Rust `std::fs`, Go `os`, wasi-libc) find the right
  directory for an absolute path themselves.
- **Confinement.** The module can reach nothing but these folders and its standard streams: WASI
  preview 1 has no sockets and no processes, so `network` and `run` have nothing to grant. This
  holds on every system, Windows included, so a WebAssembly extension is always shown as held to
  its permissions (`confined=1`); where the kernel sandbox exists (v1 section 5) it holds the host
  process as well.
- **WASI preview 1 functions.** Implemented: `args_*`, `environ_*`, `clock_res_get`,
  `clock_time_get` (realtime and monotonic; the CPU clocks read monotonic), `random_get`,
  `proc_exit`, `sched_yield`, `poll_oneoff` (clock subscriptions, and reads of the standard input,
  which waits; other descriptors are always ready), `fd_read`, `fd_write`, `fd_pread`,
  `fd_pwrite`, `fd_seek`, `fd_tell`, `fd_close`, `fd_sync`, `fd_datasync`, `fd_fdstat_get`,
  `fd_fdstat_set_flags` (append, non-blocking: a non-blocking standard input answers `EAGAIN`
  when nothing waits), `fd_filestat_get`, `fd_filestat_set_size`, `fd_prestat_get`,
  `fd_prestat_dir_name`, `fd_readdir`, `path_open`, `path_filestat_get`,
  `path_create_directory`, `path_remove_directory`, `path_unlink_file`, `path_rename`;
  `fd_advise`, `fd_allocate` and `fd_fdstat_set_rights` succeed doing nothing. Every other WASI
  function (sockets, links, symlinks, times, `fd_renumber`, `proc_raise`) answers `ENOSYS`,
  and so does a function WASI does not define when its result is an `i32` or nothing (any other
  signature does not link), so a module importing more than it uses still runs.
- **WebAssembly.** WebAssembly 2.0 without SIMD: the MVP, sign extension, non-trapping
  float-to-int conversions, multi-value, bulk memory and reference types, which is what current
  Rust, Go and clang emit for `wasm32-wasip1` (no `simd128` target feature). rhun interprets the
  module after validating it; the official core test suite passes on every system.
- **Limits.** Linear memory up to 256 MB (`memory.grow` past it answers -1), a value stack of
  16 MB and 100000 nested calls (deeper recursion traps). A trap ends the program with status
  134 and `rhun --wasm-host: trap: MESSAGE` on standard error (the extension's log); `proc_exit`
  ends it with its status; a module rhun cannot load ends with status 1 and the reason.
- **Speed.** Startup is a few milliseconds (a 2.4 MB Go module about 30), a message round trip
  is close to native, and computation runs about 15 to 40 times slower than a native build: keep
  heavy work in native targets where it matters.

Building (the SDKs need nothing else):

| Language | Command | Note |
| --- | --- | --- |
| Rust | `rustup target add wasm32-wasip1`, then `cargo build --release --target wasm32-wasip1` | rhun-ext reads rhun's input without threads on this target |
| Go | `GOOS=wasip1 GOARCH=wasm go build -o bin/NAME.wasm .` | rhunext makes the standard input non-blocking, so goroutines run while it waits |
| C, C++ | `clang --target=wasm32-wasip1 --sysroot=WASI_SYSROOT -O2 main.c -o bin/NAME.wasm` | rhunext.h as is; a sysroot from wasi-sdk or Homebrew's `wasi-libc` |

`rhun --wasm-host` can be run by hand to try a module: it speaks on its standard streams, so a
recorded exchange can be piped in.

## Debuggers: `[debugger.TYPE]` and feature `debug`

rhun is a Debug Adapter Protocol client: an extension declares debuggers, rhun starts their
adapters (after the user's approval) and speaks DAP to them itself. No DAP crosses the extension
wire. Users write launch configurations in `PROJECT/.rhun/launch.ini` (docs/guide.md
"Debugging").

### Manifest

`[needs] debug = yes` lets the extension's adapters run as the user, outside any sandbox, after
one approval per extension and project (the same approval as the tasks' programs; Forget on the
extension page). Without it the extension's debuggers are not offered.

```ini
[needs]
debug = yes

[debugger.go]
title = Go (Delve)
languages = Go
program = dlv
paths = ~/go/bin
args = dap --listen=127.0.0.1:${port}
transport = tcp
attr.mode = choice debug test exec
attr.program = path
attr.args = list
attr.env = map
attr.stopOnEntry = bool
```

| Key | Value |
| --- | --- |
| `title` | shown in the picker and the console |
| `languages` | grammar names, space separated |
| `program` | the adapter: names tried in order on the login PATH and in `paths`, or a path inside the extension's folder (with a `/`); `program.TARGET` for one system |
| `paths` | extra folders to search; `~` is the home |
| `args` | words (`"a b"` keeps its space); `args.TARGET` for one system |
| `transport` | `stdio` (the default: DAP on the adapter's standard input and output) or `tcp`: rhun picks a free loopback port for `${port}`, starts the adapter and connects to it every 100 ms for 10 s; the adapter's standard output is the program's output |
| `requests` | `launch attach` (the default) |
| `install` | a command shown when `program` is not found |
| `resolve` | `yes`: rhun asks the extension before every launch (debug-resolve, below) |
| `templates` | launch.ini fragments in the extension's folder, space separated, offered by Debug: Add Configuration (64 KB each; their sections' `type` is this debugger's) |
| `attr.NAME` | the type of a launch attribute: `string` (the default), `path` (relative to the project, `~` the home), `int`, `bool`, `list` (words, repeated lines append), `map` (`NAME.KEY = value` lines), `choice A B ...` |

`TYPE` is `[a-z0-9-]{1,32}`. Variables in `program` and `args`: `${port}` (tcp), `${extension}`
(its folder), `${state}` (its state folder), `${project}`, `${config:KEY}` and
`${config:KEY|DEFAULT}` (a value of the configuration being launched, for example
`program = ${config:python|python3}`). Limits: 8 debuggers per extension and 64 in all, 128
`attr.*` per debugger, `args` 4 KB. An extension that only declares debuggers needs no program of
its own: rhun never starts it.

A launch configuration's keys become the launch arguments with their declared types; `type`,
`request` and the section's name go in too (as `type`, `request`, `name`). `server = HOST:PORT`
in a configuration connects to an adapter that runs already (HOST `localhost` or IPv4) and starts
nothing.

### Configuration bodies

A configuration travels as a flat list of typed leaves:

```text
N LF
KIND PATH_LEN VALUE_LEN LF <PATH bytes><VALUE bytes>      (N times, nothing between them)
<extra records>
```

- `KIND`: `s` string, `n` integer (decimal without leading zeros or `-0`, |v| < 2^53), `b` bool (`true` or `false`), `z` null
  (VALUE_LEN 0).
- `PATH`: segments separated by `.`; a decimal segment under an array is an index (`args.0`,
  `args.1`), the others are object keys (`env.LOG_LEVEL`).
- Extra records follow the leaves: `context DOC LINE COL LEN` + LEN bytes of the active
  document's path (DOC 0 without one). A record's last field is its byte count, so unknown
  records are skipped.
- Limits: 4096 leaves, 1 MB, PATH 256 bytes and 16 segments, VALUE 64 KB. A path used both as an
  object and an array, a value and a container, or a bad KIND or value is `syntax`.

### Feature `debug`

| Direction | Message | Reply |
| --- | --- | --- |
| ext → rhun | `ID debug-start NAME_LEN LEN` + NAME + configuration body | `ok ID SESSION` once the adapter started |
| ext → rhun | `ID debug-stop SESSION` | `ok ID` once Stop was asked |
| rhun → ext | `0 debug-ended SESSION CODE` | (event) a session this extension started is over: the program's exit code, `-` when unknown |

`debug-start` starts a session named NAME (1 to 256 bytes) of one of the extension's own
debuggers: the body's `type` leaf; `request` (`launch` or `attach`) and `noDebug` (true: run
without debugging) are optional and the other leaves are the arguments as they are. rhun does
not resolve variables or apply `attr.*` types to them.

| Error | When |
| --- | --- |
| `unknown` | `debug` was not agreed |
| `needs` | no `debug = yes` |
| `syntax` | the body does not parse |
| `refused not within 10 s of one of your commands` | it must follow one of the extension's commands (a test run with the `debug` profile counts too): one session per command |
| `refused the configuration's type is not one of your debuggers` | |
| `cancelled` | the user declined the approval |
| `busy` | a dialog is open, or a launch is being prepared (its extension resolves it, or its before task runs) |
| `toobig 8 sessions` | 8 sessions run already |
| `refused TEXT` | the adapter could not be started, with the reason |

`debug-stop` stops a session this extension started, as the toolbar's Stop does (terminate,
then disconnect, then the adapter is killed); its `debug-ended` follows when it is over. A session
already over is `ok` too. Errors: `unknown` and `needs` as above, `syntax` (SESSION not a number),
`refused not a session you started` (the user's sessions, another extension's, or one rhun no
longer knows).

SDKs: Rust `Ext::debug_start(name, &DebugConfig)`, `Ext::debug_stop(session)` and
`DebugEnded::parse(&Event)`; Go `ext.DebugStart(ctx, name, DebugConfig)`,
`ext.DebugStop(ctx, session)` and `ParseDebugEnded(ev)`; C `rx_debug_start`, `rx_debug_stop` and
`rx_parse_debug_ended`, with `rx_debug_config_set`/`_encode`/`_parse`.

### Host request `debug-resolve`

A debugger with `resolve = yes` in its section shows its extension the configuration before
every launch the user starts (not before its own debug-start sessions):

`ID debug-resolve TYPE REQUEST NODEBUG LEN` + configuration body, with variables replaced, types
applied and `type`, `request` and `name` among the leaves, and a `context` record (the active
document, the cursor, its path).

| Answer | Meaning |
| --- | --- |
| `ok ID 0` | launch the configuration unchanged |
| `ok ID LEN` + configuration body | the complete resolved configuration replaces it (its `type` must be one of the extension's debuggers) |
| `error ID cancelled TEXT` | abandon the launch quietly (the user dismissed the extension's question) |
| `error ID refused TEXT` | abandon it and show TEXT ("no Go package in this folder") |

- The extension may use `choose` and `ask` while it resolves. An idle extension is started first
  (5 s for its hello). rhun waits 60 s; the console says "Preparing the launch" meanwhile, and
  Stop withdraws the request with `0 cancel ID`.
- A crash, a timeout, or an answer that does not parse abandons the launch with that reason.
  `resolve = yes` on an extension that did not agree `debug` fails every launch.
- The approval comes after the answer, with the adapter's command built from the resolved
  configuration (`${config:KEY}`).

SDKs: Rust `Handler::resolve_debug(&DebugResolve) -> Resolved` (`Unchanged`, `Config`, `Cancel`,
`Refuse`); Go `DebugResolver.ResolveDebug` (nil to launch unchanged, `ErrDebugCancelled`, any other
error refuses; `req.Context` ends with `0 cancel`); C the `debug_resolve` handler (0, 1 with `out`,
-1 with `why`, -2). `rhun-ext-check` asks every `resolve = yes` debugger once per template section.
