Extension protocol, version 1.1

How a program becomes a rhun extension. Also as Markdown for agents, with its grammar and the manifest's JSON Schema.

Protocol 1.1 extends version 1. 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:

RequestReplyEffect
ID unregister NAMEok IDremoves 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.

RequestReplyEffect
ID watch-workspaceok ID HANDLEsubscribes the current runtime to workspace invalidations
ID unwatch-workspace HANDLEok IDremoves 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; 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.

DirectionRequestSuccessful reply
Host to extensionID save-actions DOC VERSION KINDok ID LEN followed by a workspace edit, or ok ID 0
Extension to hostID save-apply SAVE_REQUEST DOC VERSION LEN followed by a workspace editok 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:

StringMeaning
Selected textThe exact requested range, independent of the editor's current selection
Current lineThe range start's line, excluding its newline
PathThe document's absolute display path, or empty for an untitled document
WorkspaceThe current project root, or empty without a project
IndentationLeading spaces and tabs up to the range start, stopping at the first other byte
Line commentThe 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

  1. 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.

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

OpcodeResultStrings used
0Literal AA
1Capturenone
2, 3, 4Uppercase, lowercase, capitalize capturenone
5, 6, 7, 8Camel, Pascal, snake, kebab case capturenone
9A when capture is nonemptyA
10Capture when nonempty, otherwise AA
11A when capture is nonempty, otherwise BA, 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.

FROMInvoked byDOCBody
palettethe command palette, a headless cmd, a menu listing all commandsthe active documentempty
keya key bindingthe active documentthe binding's ARGS (at most 4096 bytes)
menua menu item, a tab-strip or view buttonthe target's document when it has one, else the active oneTARGET LF, then the item's args
refa command reference (code lens, tree item, status item, notification button, inlay hint part)the reference's document (a lens, a hint), else the active onethe 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

DirectionRequestReply
extension to rhunID run-command NAME LEN + ARGSok ID LEN + RESULT, or error ID CODE TEXT
rhun to the command's ownerID call EXT.NAME DOC CALLER LEN + ARGSok 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:

[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.

[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.

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.

KeyValue
editorFocusthe active tab is a text document and has the keys
textInputFocusthe 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, settingsFocusthat part has the keys
findVisible, completionVisible, signatureVisible, snippetActivethe find bar, the completion list, signature help show; a snippet session is live
viewFocus, view, viewIteman extension view has the keys; its id; the focused or targeted tree item's context value
activeTabdoc, image, settings, git, extensions, view, or empty
activeTabPinned, tabCountboolean; a number
resourceLangthe 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
resourceSchemefile or untitled
resourceInProjectthe path is inside the project folder
editorReadonly, editorDirty, editorHasSelection, editorHasMultipleSelectionsof the active (or target) document
editorProvidersthe provider kinds declared for it, words of complete signature definition references symbols actions rename format
vimModenormal, insert, visual, vline while vim mode is on, else empty
sidebarVisible, agentsVisible, terminalVisiblethat panel shows
projectOpen, projectPatha project folder is open; its path
gitRepo, gitBranch, gitChangesgit runs in a work tree; its branch; the number of changed files
isLinux, isMac, isWindows, isWaylandthe system rhun runs on
remoteName, remoteHostin a remote window: ssh, docker, podman or wsl, and the host or container; empty in a local window
targetKindin a menu: editor, file, folder, root, tab, terminal, extension, view, item; else empty
explorerResourceIsFolder, explorerResourceIsRoot, tabKindof an explorer or tab menu's target
debugState, inDebugMode, debugTypethe debugger's state (inactive, false and empty while no session runs)
gitTrustedgit may run in the project folder (false while the trust pause holds)
scmProvider, scmResourceGroup, scmResourceStatusin 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.

RequestReplyEffect
ID set-context KEY VALUEok IDsets 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.

[context]
mode = normal
enabled = true

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).

[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:

LocationTARGET
editoreditor 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
explorerfile PATH, folder PATH, or root PATH for the empty area (the project folder)
tabtab doc DOC PATH (- for an untitled document), tab image PATH, tab view VIEW, tab settings, tab git, tab extensions
terminalterminal N, N the one-based terminal tab
extensionextension
editor_titleas tab, for the active tab
view_titleview VIEW
view_item, view_item_inlineitem 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.

RequestReplyEffect
ID status-item ITEM LEN + bodyok IDadds or replaces item ITEM (1 to 40 of [a-z0-9_])
ID status-remove ITEMok IDremoves 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:

[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.

KeyValueWithout it (the behaviour before these keys)
bracketstwo-byte pairs Match brackets outlines, at most 16() [] {}
autocloseOPEN CLOSE token pairs, space separated, each 1 to 8 bytes, at most 16 pairs( ) [ ] { } " " ' ' ` ``
autoclose_codeOPEN tokens that close only outside strings and comments" ' ``
surroundone-byte OPEN CLOSE pairs typing OPEN wraps a selection inthe one-byte autoclose pairs
indent_opentokens opening a block; ^T only as a line's first code token, T$ only as its last{ ( [
indent_closetokens closing one} ) ]
indent_middletokens closing and reopening one (else elif)none
indent_dedent_afterfirst words after which the next line is one level less (return pass)none
continueline markers Enter continues (/// //! - >)none
doc_blockSTART MIDDLE END (/** * */)none
foldbrackets or indentbrackets
fold_markersSTART END (#region #endregion), after an optional line comment tokennone

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.

[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:

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

[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

DirectionMessageReplyEffect
rhun → extID tests-resolve LEN + PARENT id (empty: roots)ok ID LEN + items body for that PARENT, or error ID CODE TEXTsent 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 → rhunID test-items MODE LEN + items bodyok IDMODE 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 → rhunID test-remove N LEN + N ids, each ending in LFok IDremoves them and their descendants; unknown ids are ignored
rhun → extID test-run RUN PROFILE N LEN + N ids, each ending in LFok ID 0, or error ID busy TEXT / refused TEXTstarts 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 → rhunID test-update RUN LEN + results bodyok IDresults and output (below). error ID stale run over once the run ended; refused for another extension's run
ext → rhunID test-end RUNok IDthe run is over
rhun → ext0 test-cancel RUNthe user cancelled: stop and send test-end
ext → rhunID test-coverage RUN LEN + coverage bodyok IDline coverage found by this run; any number of messages, merged
ext → rhunID test-exec RUN LEN + command bodyok ID EXECrhun starts a program for this run (below)
rhun → ext0 test-out EXEC STREAM LEN + bytesSTREAM out or err: what the process wrote, as read (at most 64 KiB an event, not split at lines)
rhun → ext0 test-exit EXEC CODEthe 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:

CheckError
the folder is not one a rhun:// link opened and git is paused forrefused 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] testsneeds tests=NAME
the run has fewer than 4 live processes and all runs fewer than 8busy
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 linksrefused 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

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

RequestReplyNeedsEffect
ID terminal-open LEN + definitionok ID TERMrunasks the approval if needed (the reply waits); starts the program in a new tab
ID terminal-stop TERMok IDrunCtrl+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 TERMok IDrunstop, wait for the end, run again in the same tab
ID terminal-close TERMok IDrunSIGHUP and SIGTERM to its group (SIGKILL 2 s later), the tab closes
ID terminal-show TERM show|focusok IDrunopens the panel at the tab; focus gives it the keyboard
ID terminal-input TERM LEN + bytesok IDinputthe bytes as typed (LF is Enter); at most 64 KiB; busy while 1 MiB waits
ID terminal-screen TERMok ID LEN + bodyoutputROWS COLS CURSOR_ROW CURSOR_COL LF, then the visible rows (trailing blanks dropped)
EventWhen
0 terminal-started TERM PID TASK_IDthe program started (- for a terminal); again at each restart
0 terminal-ended TERM REASON CODE ERRORS WARNINGSit 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 + bytesfinished 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 TERMthe 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.

RequestReplyEffect
ID tasks-set LEN + N LF + N definitionsok IDreplaces the extension's published tasks, checked whole first: ids unique, depends naming ids of the list, no cycles, patterns valid
ID task-start TASK_IDok ID TERMruns a published task after its dependencies (approval as above); a running task is left running and its TERM returned; notask
ID task-run LEN + definitionok ID TERMruns a task that is not published (its depends may name published ones); busy while a task with its id runs
EventWhen
0 task-ready TERM ERRORS WARNINGSa background task's line matched ready
0 task-busy TERMa 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-inLines
gocolumns bytes, 1 {file}:{line}:{col}: {message}, 1 vet: {file}:{line}:{col}: {message}, 1 {file}:{line}: {message} (go test's indented failure)
gcccolumns bytes, 1 {file}:{line}:{col}: {severity}: {message}, 1 {file}:{line}: {severity}: {message} (gcc, clang)
rustc1 {severity}[{code}]: {message}, 1 {severity}: {message}, 2 --> {file}:{line}:{col} (rustc, cargo)
tsc1 {file}({line},{col}): {severity} {code}: {message}, 1 {file}:{line}:{col} - {severity} {code}: {message}
msbuild1 {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.

WhatLimit
tasks-set1000 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 running32, a dependency chain 8 deep
problems2000 per run, 10000 kept in all; more are counted in the footer
line matching64 KiB waiting per tab, 2 ms per tick; lines past that are counted in the footer (N lines were not checked for problems)
terminal-output64 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]

[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

RequestReplyEffect
ID vpatch VIEW LEN + patchok ID REVapplies the patch whole, or nothing of it
ID vshow NAME FOCUSok ID VIEWopens 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 idok IDexpands 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

RequestAnswer
ID vkids VIEW REV LEN + node idok 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 idthe 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.

OpEffect
title LEN + textthe view's title (section header, tab), at most 80 bytes, one line; empty: the manifest's
badge N 0the count beside the title; 0 hides it
message LEN + texta muted line above the content, at most 1000 bytes; empty clears it
state STATE TEXT_LEN LEN + TEXT + KEYSready, 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 0a thin bar at the top of the view, a spinner in its header
action KEY ICON WHERE LEN + TITLEdefines 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 0multi: 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 recordsreplaces 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 recordsappends children with new ids
set LEN + one node recordreplaces an existing node's label, kind of children and extra records; its expansion stays the user's
del LEN + idremoves the node and its subtree (not the root)
expand LEN + id, collapse LEN + idsets 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:

RecordMeaning
desc LENmuted text after the label, cut first when space runs out
tip LENthe tooltip, plain text, at most 4000 bytes
icon SPEC COLOR 0SPEC: 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 0the label's color
hl FROM TO 0label bytes [FROM, TO) in the accent color (zero-based); at most 16
badge N 0a count at the row's right
check STATE 0a checkbox, 0 or 1
open LINE COL LEN + PATHa 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 + ARGSa click (on a leaf) or Enter runs the command reference ("Command references")
inline LEN + keysup 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 + keysup to 16 action keys in its context menu (right click, Shift+F10 or the Menu key)
ctx LENits context value (viewItem), [A-Za-z0-9_.-], at most 100 bytes
data LENbytes 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.

EventWhen
open KIND PLACE NAMEthe instance exists; before any other event of VIEW
visible 0|1it came on or went off screen (collapsed, hidden, its tab not chosen, its panel closed); at most once a frame
focus 0|1the keyboard entered or left it
select REV N LEN + N ids, each ending in LFthe 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 + idEnter 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 + idthe user expanded or collapsed it
check REV STATE LEN + idthe user toggled its checkbox (STATE 0 or 1); rhun shows the new state at once, a later set wins
action REV KEY LEN + idan action was invoked on a node (inline button, context menu), or with an empty id from the header
discarded REV WHAT LEN + ida 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.

OpEffect
log N LEN + N linesappends 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 0removes 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.

OpEffect
form N LEN + N field recordsthe 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 recordadds 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:

RecordMeaning
value SEQ LENthe 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 LENshown in an empty input
options LENa select's options, one a line
lines N 0an area's height in lines (default 4)
range MIN MAX STEP 0a number's Up and Down step by STEP within MIN and MAX (decimals allowed)
msg SEVERITY LENa line under the field in the severity's color (info, warning, error)
disabled 0, primary 0, mono 0, password 0greyed 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 recordEffect
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.

OpEffect
doc LEN + Markdownthe document, replacing the last one (at most 1 MB: toobig doc)
base LEN + folderan absolute folder (/… or D:/…) relative links name
follow DOC 0while 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

RequestReplyEffect
ID vtab KIND TITLEok ID VIEWa 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 VIEWok IDcloses 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:

EventWhen
key MODS NAMEa 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-8a typed character (without Ctrl, Alt or Super), after its key
paste LEN + textthe paste command (Ctrl+V)
selectallthe 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

RequestReplyEffect
ID vimage LEN + bytesok ID IMGa PNG, JPEG or GIF decoded once (at most 8 MB and 4096 pixels a side; anything else is syntax)
ID unimage IMGok IDlets 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

RequestReplyEffect
ID notify SEVERITY LEN + bodyok ID NOTEa card at the window's bottom right, above the status bar; SEVERITY info, warning or error
ID notify-set NOTE LEN + bodyok IDreplaces the card's content
ID unnotify NOTEok IDcloses 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:

RecordMeaning
detail LENa muted line under the message (with the extension's title), at most 1000 bytes
sticky 0it stays until closed
button LEN + titlea button (at most 3; more are left out), at most 60 bytes
command NAME ARGS_LEN + ARGSthe 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

RequestReplyEffect
ID progress WHERE CANCEL LEN + titleok ID PWHERE 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 + messageok IDPERCENT 0-100, or - for indeterminate (a moving bar); the message at most 200 bytes
ID progress-end Pok IDremoves 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.

RequestReplyEffect
ID quick LEN + step bodyok ID Qopens it; busy while one, the palette or a prompt shows
ID quick-set Q LEN + step bodyok IDchanges what its records name, atomically; the rest stays
ID quick-close Qok IDcloses 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.

KeyFeatureAsked for
hoverhover-querythe hover card
declaration, type_definition, implementationnavigationGo to Declaration, Type Definition, Implementation
selection_rangesselection-rangesExpand and Shrink Selection
call_hierarchy, type_hierarchyhierarchycall and type hierarchies
workspace_symbolsworkspace-symbolsGo to Symbol in Project, while a matching document is open
range_format, type_formatpartial-formatFormat Selection, formatting as you type
type_format.triggerspartial-formatcharacters that ask for on-type formatting; the word newline stands for Enter
document_linksdocument-linkslinks 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)PartsShowLimit (base / ceiling)AnswerQuery
completeyesas answers comethe list's session4096 items, 1 MiB8192 items, 2 MiB
complete-resolvenoon arrival1 s64 KiB
signature, signaturesnoon arrivalthe help's session64 KiB
hover-queryno1 s or all answered3 s64 KiB256 KiB
definition, declaration, type-definitionyesall answered5 s / 30 s10000, 1 MiB10000, 4 MiB
implementationyesfirst part10 s / 60 s10000, 1 MiB20000, 8 MiB
referencesyesfirst part10 s / 120 s10000, 1 MiB100000, 16 MiB
symbolsnoon arrival5 s10000, 1 MiB
workspace-symbolsyes300 ms5 s / 30 s10000, 1 MiB20000, 4 MiB
selection-rangesnoon arrival1 s64 chains of 64, 64 KiB
call-hierarchy, type-hierarchynoon arrival5 s64 items, 256 KiB
calls-in, calls-out, supertypes, subtypesyesfirst part5 s / 30 s1000, 1 MiB10000, 4 MiB
code-actions (invoked)noall answered3 s1000, 4 MiB
code-actions (auto)noall answered1 s1000, 4 MiB
code-action-resolvenoon arrival5 s / 60 s4 MiB
prepare-renamenoon arrival2 s4 KiB
renamenoon arrival15 s / 120 sv1's edit caps
format, range-formatnoon arrival10 s / 30 sv1's edit caps
type-formatnoon arrival500 ms64 KiB
document-linksnoon arrival2 s10000, 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:

RequestReply
ID query-part REQ LEN + bodyok ID, or error ID cancelled, toobig, syntax, refused
ID query-progress REQ PERCENT LEN + textok 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.

RecordMeaning
resolve EDITS LEN + datarhun 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 0the 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 + characters1 to 16 printable ASCII characters; typing one while the item is selected accepts it first, then types the character
filter LEN + textone line, matched and ranked instead of the label
suffix LEN + textone line drawn after the label in the muted color (a signature, a module)
preselect 0selected first when it passes the filter
deprecated 0drawn struck through
adjust 0the 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 0after 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 ids (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.
← 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.

[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

DirectionMessageReply or meaning
rhun → extID 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 → extID nb-save NB LEN + PATH_LEN LF <PATH> + notebook bodyok 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 → ext0 nb-run NB CELL RUN LANG LEN + sourcerun 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 → ext0 nb-interrupt NBthe user asked to interrupt the running cell
rhun → ext0 nb-restart NBrestart the kernel (the user confirmed)
rhun → ext0 nb-kernel NBthe kernel button was clicked: offer kernels, for example with choose
rhun → ext0 nb-closed NBthe tab closed
ext → rhunID nb-state NB CELL RUN STATE COUNTok ID. STATE queued, running, done, failed or cancelled; COUNT a number or - (left as it was)
ext → rhunID nb-output NB CELL RUN OP TARGET LEN + output bodyok 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 → rhunID nb-stream NB CELL RUN NAME LEN + bytesok 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 → rhunID nb-kernel-label NB STATE LEN + labelok 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:

[needs]
# kernels rhun may start as the user for this extension's notebooks: names, space separated
kernels = python3 python
DirectionMessageReply or meaning
ext → rhunID 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 → rhunID nb-exec-signal EXEC SIGNALok 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 → ext0 nb-exec-exit EXEC CODEit 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.

[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.

DirectionMessageReply or meaning
rhun → extID editor-open CDOC VIEW MODEL SOURCE LEN + pathMODEL 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 → rhunID editor-read CDOC OFFSET MAXok 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 → rhunID editor-dirty CDOC 0|1ok ID. An owned model's dirty dot and close question follow it (refused for a text model, whose state is rhun's)
rhun → extID editor-save CDOC KIND PARTKIND 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 → extID editor-undo CDOC, ID editor-redo CDOC, ID editor-revert CDOCrhun's Undo, Redo and Revert File in an owned model's tab; reply ok ID 0
rhun → ext0 editor-changed CDOCan owned model's file changed on disk; a clean model should read it again, a dirty one is the extension's call
rhun → ext0 editor-closed CDOCthe 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

[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.

RequestSuccessful reply
ID git-repook ID LEN + ROOT_LEN BRANCH_LEN UPSTREAM_LEN HEAD AHEAD BEHIND STATE LF <ROOT><BRANCH><UPSTREAM>
ID git-statusok ID LEN + N LF, then N entries XY PATH_LEN LF <PATH>
ID git-blame DOCok ID LEN + the blame of the document's current text (below)
ID git-log MAX L1 L2 LEN + LEN bytes of pathok 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 pathok ID LEN + the file's bytes at REV
ID git-diff-open REV1 REV2 LEN + LEN bytes of pathok 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 0s, 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:

RequestReplyEffect
ID quickdiff-refresh LEN + LEN bytes: an absolute path, or -ok IDrhun 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.

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:

LocationTARGET
history_commitcommit SHA
history_filecommit SHA PATH
scm_resourceresource GROUP PATH
scm_groupgroup 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).

RequestReplyEffect
ID scm-register LEN + ROOT_LEN LABEL_LEN LF <ROOT><LABEL>ok ID SCMa sidebar section for the repository at ROOT; registering the same root again returns its SCM
ID scm-update SCM LEN + bodyok IDreplaces everything the section shows (below)
ID scm-input SCMok ID LEN + textthe message box's text
ID scm-input-set SCM LEN + textok IDputs 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.

RequestReplyEffect
ID decoration-type NAME LEN + bodyok IDregisters 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 + bodyok IDreplaces 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
EventWhen
0 visible DOC L1 L2the 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.

LineEffect
color COLORthe text's color
background COLORbehind the range's characters, per row
line-background COLORbehind the text area's whole width, on every line the range touches
border WIDTH COLORan outline of 1 to 3 pixels (times the display scale), per row
underline STYLE COLORSTYLE solid, double, dotted, dashed or squiggle
strike COLORa line through the text; COLOR - is the text's color
fadedthe text halfway to the background
before COLOR BACKGROUNDtext before the range (each range's BEFORE); BACKGROUND - for none
after COLOR BACKGROUNDtext after the range (each range's AFTER)
width CELLSbefore and after text padded or cut to this many cells; 0 natural (0-200)
gutter SHAPE COLORan 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 COLORa tick in the vertical scrollbar at the range's first line
grow EDGEEDGE none (default), start, end or both: whether text typed at that edge joins the range
priority N0-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

WhatLimit
types64 per extension, 512 in all; a type body 4 KB
records20,000 per message and per extension and document; 50,000 per document in all
a body4 MB
drawn per frame4,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

# 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:

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:

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:

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

MessageFormMeaning
event0 selections DOC N LEN + N lines L1 C1 L2 C2replaces 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
requestID select-many DOC N LEN + N lines L1 C1 L2 C2ok 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

RequestReplyEffect
ID reveal DOC LINE COL WHEREok IDscrolls 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 + bodyok IDthe 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

RequestReplyEffect
ID diff LEFT RIGHT FLAGS LEN + TITLEok ID DOCa 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 DOCok IDcloses 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:

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:

# 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:

RequestReplyEffect
ID busy yesok IDit is not stopped for idleness until busy no or its next start
ID busy nook 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"):

[needs]
secrets = yes
RequestReply
ID secret-get KEYok ID 1 LEN + the value, or ok ID 0 0 when nothing is stored
ID secret-set KEY LEN + valueok ID
ID secret-delete KEYok ID, also when nothing was stored
ID secret-keysok 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.

[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.

DirectionMessageReply / meaning
extensionID open LINE COL URIv1'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 requestID content URIok 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
extensionID content-changed URIok 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 provider0 content-closed URIits tab closed
event0 opened DOC VERSION LANG URIto 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.

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.

DirectionMessageReply / meaning
extensionID open-url LEN + URLok ID opened, ok ID copied (the user copied the link instead), or error ID cancelled ...
extensionID external-uri LEN + URIok ID LEN + the URI the user's browser can use for it
extensionID linkok ID LEN + rhun://ext/OWNER/NAME/TOKEN
event0 link ID LEN + the linka 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.

RequestAnswer
ID find-files FOLDER MAX FLAGS LEN + globs0 found ID LEN batches, then ok ID COUNT STATUS
ID search-text FOLDER MAX FLAGS LEN + PATTERN_LEN GLOBS_LEN LF, the pattern, the globs0 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:

[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] keyProviders asked
folding-rangesfoldingone: the language's (section Single-provider selection; never a chooser)
semantic-tokenssemantic_tokensone, as above
document-highlightshighlightsone, as above
inlay-hintsinlay_hintsevery matching one; answers are merged
code-lenscode_lensevery matching one
document-colorscolorsevery matching one

When rhun asks:

FeatureAskedAfter edits
folding rangeswhen the document becomes active, and after edits500 ms after the last edit
semantic tokensas above150 ms
inlay hints, colorsas above, and when a scroll leaves the lines last asked about250 / 500 ms; 100 ms after a scroll
code lensas above500 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 VERSIONok 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

MessageAnswer
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 typesClass
namespace type class enum interface struct typeParameter concept typeAlias uniontype
builtinTypebuiltin
parameter variable property eventvariable
enumMember const static boolean labelconstant
function methodfunction
macropreproc
decorator attribute builtinAttribute deriveattribute
keyword modifier selfKeyword selfTypeKeyword lifetimekeyword
comment, string, numbercomment, string, number
regexp escapeSequence formatSpecifierescape
operator arithmetic bitwise comparison logicaloperator
bracket brace parenthesis punctuation angle colon comma dot semicolonpunctuation
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 L2ok 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 L2ok 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 COLok 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 L2ok ID LEN + N LF and N lines L1 C1 L2 C2 RRGGBBAA
ID color-edit DOC VERSION L1 C1 L2 C2 RRGGBBAAok 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 KINDok 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.

DirectionMessageAnswer
extension → rhunID agent-sessions LEN + rowsok ID
extension → rhunID agent-changed SID TOTAL FIRSTok ID; refused no such session
rhun → extensionID agent-transcript SID FROMok 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

DirectionMessageAnswer
extension → rhunID propose LEN + head + workspace editok ID PROPOSAL at once
extension → rhunID unpropose PROPOSALok ID; refused no such proposal
rhun → extension0 proposal PROPOSAL INDEX accepted (or rejected, stale, partial A N)
rhun → extension0 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:

[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.

DirectionMessageAnswer
extension → rhunID toolsok ID LEN + N LF and N records
extension → rhunID tool-call TOOL LEN + inputwhen done: ok ID LEN + output, or error ID ...
rhun → ownerID tool NAME CALLER LEN + inputok 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:

ToolInputOutputNeeds
rhun.editorsDOC 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.selectionDOC L1 C1 L2 C2 LF and the selected text, of the active document (nodoc without one)documents read
rhun.readpath 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'sdocuments read; a file not open: project/home read
rhun.diagnosticspath P (optional)PATH L1 C1 L2 C2 SEVERITY OWNER/NAME MESSAGE per mark on an open documentdocuments read
rhun.openpath P, line L, col CDOCa file not open: project/home read
rhun.proposeedit 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 Rdocuments read
rhun.runcommand TEXTok once Enter put it in the terminal; cancelled otherwiseterminal

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.

DirectionMessageAnswer
rhun → extensionID agent-prompt SID LEN + textok ID LEN + session id, or error ID CODE TEXT
rhun → extension0 agent-stop SID
extension → rhunID agent-ask SID LEN + prompt line and option linesok 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:

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.

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):

LanguageCommandNote
Rustrustup target add wasm32-wasip1, then cargo build --release --target wasm32-wasip1rhun-ext reads rhun's input without threads on this target
GoGOOS=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.wasmrhunext.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.

[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
KeyValue
titleshown in the picker and the console
languagesgrammar names, space separated
programthe 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
pathsextra folders to search; ~ is the home
argswords ("a b" keeps its space); args.TARGET for one system
transportstdio (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
requestslaunch attach (the default)
installa command shown when program is not found
resolveyes: rhun asks the extension before every launch (debug-resolve, below)
templateslaunch.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.NAMEthe 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:

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

DirectionMessageReply
ext → rhunID debug-start NAME_LEN LEN + NAME + configuration bodyok ID SESSION once the adapter started
ext → rhunID debug-stop SESSIONok ID once Stop was asked
rhun → ext0 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.

ErrorWhen
unknowndebug was not agreed
needsno debug = yes
syntaxthe body does not parse
refused not within 10 s of one of your commandsit 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
cancelledthe user declined the approval
busya dialog is open, or a launch is being prepared (its extension resolves it, or its before task runs)
toobig 8 sessions8 sessions run already
refused TEXTthe 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).

AnswerMeaning
ok ID 0launch the configuration unchanged
ok ID LEN + configuration bodythe complete resolved configuration replaces it (its type must be one of the extension's debuggers)
error ID cancelled TEXTabandon the launch quietly (the user dismissed the extension's question)
error ID refused TEXTabandon 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.