# rhun extension protocol, version 1

Status: **v1**, the protocol of rhun 0.18.0. Everything below is what rhun does. The manifest's
`description` and `[settings]`, the `setting` event and `on` defaulting to `startup` were added
before 0.18.0 shipped; from its release on, a later rhun adds to the protocol only as v1.1 and
later, announced by fields after `hello 1` and by `RHUN_PROTOCOL`, and never changes what is
written here. The canonical copy is
`v1/v1.md` in [rhun-editor/extension-protocol](https://github.com/rhun-editor/extension-protocol),
next to the line grammar `v1/v1.abnf` and the manifest schema `v1/v1.json`; rhun's repository
carries a copy at `docs/extensions/v1.md`.

An extension is a program. rhun starts it when it is needed, talks to it over its standard
input and output with one text line per message, and stops it when rhun quits. The program may
be written in any language. rhun never waits for it: everything the extension does is applied
when its reply arrives, and only if the document it refers to has not changed meanwhile.

## 1. Files

An installed extension is a folder `~/.config/rhun/extensions/OWNER/NAME/` (on Windows
`%APPDATA%\rhun\extensions\OWNER\NAME\`). `rhun --extensions DIR` uses `DIR` instead of the
folder, for development and tests. Inside:

| Path | Purpose |
| --- | --- |
| `extension.ini` | the manifest, required |
| the program | named by the manifest's `run.*` entries or `run` with `runtime` |
| `themes/*.theme`, `syntax/*.syn` | data files: themes join the list after the user's; grammars go after the user's and before the built-in ones, so an extension can refine a built-in language and the user still wins |
| anything else | the extension's own; the folder is read-only to the extension once installed |

The extension's writable folder is its **state folder**, `~/.local/state/rhun/extensions/OWNER--NAME/`
(`%LOCALAPPDATA%\rhun\extensions\OWNER--NAME\`), created before the first start. `HOME`,
`TMPDIR` and the common cache variables point into it (section 4).

## 2. Manifest

INI, UTF-8, `key = value`. A line whose first character is `#` or `;` is a comment; there are no comments after a value (`color = #2a9d8f` keeps its `#`). Unknown keys are ignored. Required keys are marked.

```ini
# required: [a-z0-9][a-z0-9-]*, equal to the folder name
name = format
# required: the GitHub owner, equal to the parent folder name
owner = rhun-editor
# required: shown on the Extensions page and after command titles
title = Format
# one line, optional
description = Formats the open file with the formatter you already have.
# optional: a PNG in the folder, square, 128 pixels recommended
icon = icon.png
# optional: the badge's background, #rrggbb
color = #2a9d8f
# required
version = 0.1.0
# required: the protocol version this extension speaks
protocol = 1
# the program for that target, relative to the folder
run.linux-x86_64 = bin/format-linux-x86_64
run.macos-arm64 = bin/format-macos-arm64
run.windows-x64 = bin/format-windows-x64.exe
# or, for a script: runtime, a program found on PATH; run, the script, passed as the runtime's
# first argument; requires, shown when the runtime is missing or too old (not checked by rhun)
# runtime = python3
# run = main.py
# requires = python3 >= 3.10
# extra arguments, space separated, optional
args =
# when to start: startup | command | lang:LANG | open:GLOB | project
# several, space separated; "startup" is the default: an installed
# extension runs. Invoking one of the manifest's commands always
# starts it, whatever `on` says
on = startup

[commands]
format_document = Format Document | ctrl+shift+i
# key = Title | default keys (space separated, as in [keys] of the config; "|" and keys optional)

# its own settings, shown on its page; optional (section 6d)
[settings]
# string (default) | bool | int | choice
indent.type = int
indent.default = 2
indent.min = 1
indent.max = 8
indent.title = Indent
indent.description = Spaces per level for JSON

# which events to send, with name patterns: * for all, *.go a suffix, Makefile an exact name,
# matched against the file's name (not its folder), case-sensitively; the default is "no". A
# document is reported at all only when `opened` names it; the other keys select among those.
# An untitled document matches only *. ready, active, closed (of a reported document), project,
# run, resync and bye are always sent
[subscribe]
opened = *
saved = *.go *.json
changed = no
cursor = no
selection = no
# every change as deltas (section 6a)
edits = *.go
# the pointer resting on a position
hover = *.go

# section 12: the language features it answers for these files (completion with the characters
# that ask at once, signature help, definition, references, symbols, code actions, rename,
# format); a provider gets opened and edits for them as if subscribed (it cannot opt out), and
# the first of them to open starts it, whatever `on` says
[provides]
complete = *.go
complete.triggers = . :
signature = *.go
signature.triggers = ( ,
definition = *.go
references = *.go
symbols = *.go
actions = *.go
rename = *.go
format = *.go

# section 5
[needs]
documents = write
project = read
home = none
network = no
terminal = no
run = gofmt rustfmt
```

The badge that stands for the extension in the title bar, its dropdown and the Extensions page
is `icon` drawn on `color`. Without `icon` it is the title's first letter on `color`, and without
`color` on one of eight colors picked by the name. rhun reads the icon the first time a badge
needs it and keeps it scaled to the sizes it is drawn at, its corners rounded as the badge's are;
a file over 64 KB, over 256 pixels a side, not a PNG, or outside the folder leaves the letter.
Transparent parts of the icon show the color.

A manifest that fails to parse, lacks a required key, has a `name` or `owner` other than its
folders' names, names no `run` entry for this target and no `runtime`, names a `runtime` that
is not on `PATH`, or a program that is missing or not executable, is recorded with its reason
and the extension is not started; Settings shows the reason. Commands are registered from the manifest
before the process exists, so the palette shows them and the keys work from startup.

Command names in the palette and in `[keys]` are `NAME.command`, for example
`format.format_document`; titles are shown as "Format Document (Format)". The user's `[keys]`
win over an extension's default keys, and a key a built-in command has stays with it; between
two extensions the one found first in the folder listing keeps a contested key (Settings >
Extensions shows the conflict). A command invoked while its program is still starting is
delivered after `ready` and the catch-up; a second one invoked before that replaces it.

### Activation

`on` lists conditions; the extension starts when any holds:

| Condition | Starts when |
| --- | --- |
| `startup` | rhun has drawn its first frame (the default) |
| `command` | one of its commands is invoked (always, whatever `on` says) |
| `lang:LANG` | a document whose grammar name is `LANG` is open or opened (names as `print-state` shows them) |
| `open:PATTERN` | a document whose file name matches the pattern (`*.ext` or an exact name) is open or opened |
| `project` | a project folder is open |
| (`[provides]`) | a document one of its `[provides]` patterns matches is open or opened |

One process per extension; a start in progress absorbs further triggers.

## 3. Transport

Messages are lines of UTF-8 text ending in LF (CR before it is ignored), at most 65536 bytes.
Fields are separated by single spaces. The **last field of a message may contain spaces**:
it runs to the end of the line. A message whose last numeric field is a length `LEN` is
followed by a **body**: exactly `LEN` bytes after the LF, which may contain anything; the next
message starts at the byte after the body. Bodies are capped per message (section 7).
On Windows, write standard output in binary mode: a text stream (the C runtime's default,
Python's `sys.stdout`) turns each LF into CR LF, so a body no longer has the length its
message gives and rhun drops what it carried (a format on save reports "Formatting failed").

Three kinds of lines:

- **Requests**: `ID verb args…`, `ID` a decimal integer from 1 to 2147483647, unique among the
  sender's requests that have not been answered. Both sides send requests.
- **Replies**: `ok ID args…` or `error ID CODE text…`, in any order relative to other replies,
  always to a request the other side sent. A reply may carry a body, as `ok ID VERSION LEN`.
- **Events**: `0 name args…`, id 0, never answered.

Error codes: `syntax` (the line could not be parsed, or a request's arguments could not),
`unknown` (verb), `needs` (section 5; the text names the capability, as `needs
documents=write`), `nodoc` (no such document, or not a text document), `nofile` (`open`: no
such file), `readonly` (the document cannot be edited: a diff view), `stale` (the version is
not current; the text is the current version), `badpos` (a position is outside the document or
inside a multibyte character), `toobig` (more diagnostics, items or commands than the cap),
`busy` (a prompt is open, or the palette), `cancelled` (the user dismissed the prompt, or it
was closed), `refused` (a `cmd` not on the list of section 8a, a `register` of a name in use).
After a `syntax` error the message is dropped; three `syntax` errors in a row, with no
well-formed request between them, stop the extension. A line of 64 KB with no LF in it, or a
body over its cap, stops it at once: rhun does not read megabytes to discard them.

Requests are handled in the order they arrive and each is complete before the next is read,
except the prompts (`choose`, `ask`, `term`), which are answered when the user acts: `cancel`
of an open prompt closes it and answers it `cancelled`; `cancel` of anything else is `ok` and
does nothing.

An extension may answer a request from rhun at its leisure, or ignore events, but it must keep
reading: rhun's outgoing queue per extension is 1 MB, and an extension that leaves it full for
10 seconds is stopped.

## 4. Lifecycle

1. rhun creates the state folder, opens the log file
   `~/.local/state/rhun/extensions/OWNER--NAME.log` for the extension's standard error (cut back
   to empty when it passes 1 MB: the program keeps writing to it, so it cannot be rotated), and starts the program with its folder as the working directory
   and this environment and nothing else:

   | Variable | Value |
   | --- | --- |
   | `PATH` | rhun's own `PATH` |
   | `HOME` | the state folder |
   | `TMPDIR` | `STATE/tmp`, created |
   | `XDG_CACHE_HOME`, `GOCACHE`, `GOMODCACHE`, `GOPATH`, `npm_config_cache` | under the state folder |
   | `LANG`, `LC_ALL`, `TZ` | rhun's, when set |
   | `xcrun_db`, `DEVELOPER_DIR` | macOS: xcrun's cache in the state folder (Xcode's `/usr/bin` tools look themselves up there, not in the user's shared cache; when it is missing, rhun fills it for the tools `run` names from outside the sandbox while the extension starts, so lookups do not run xcodebuild inside it); rhun's `DEVELOPER_DIR`, when set |
   | `RHUN_VERSION` | rhun's version |
   | `RHUN_PROTOCOL` | the highest protocol revision rhun speaks: `1.1` in rhun 0.18 (v1.1 says what it adds) |
   | `RHUN_EXTENSION` | `OWNER/NAME` |
   | `RHUN_TARGET` | `linux-x86_64`, `macos-arm64` or `windows-x64` |
   | `RHUN_STATE` | the state folder |
   | `RHUN_PROJECT` | the project folder, or unset |
   | `RHUN_SOCKET` | the control socket path, only in a `--extensions DIR` session with `--control` |
   | Windows only: `SYSTEMROOT`, `WINDIR`, `COMSPEC`, `PATHEXT`, `SYSTEMDRIVE`, `PROGRAMDATA`, the `PROGRAMFILES` and `COMMONPROGRAMFILES` family, `NUMBER_OF_PROCESSORS`, `PROCESSOR_ARCHITECTURE`, `OS` | rhun's, when set (Windows programs need several of these to start) |
   | Windows only: `TEMP`, `TMP` | `STATE/tmp` |
   | Windows only: `USERPROFILE`; `APPDATA`; `LOCALAPPDATA` | the state folder; `STATE/appdata`; `STATE/localappdata` |

   On Windows a manifest's `runtime = python3` is looked for as `python3`, then `python`, then
   `py` (the Python launcher), on `PATH`; the Microsoft Store's stubs in `WindowsApps` are used
   only when nothing else is there.

   In a `--extensions DIR` session the extension also inherits rhun's own environment (tests
   pass their settings that way); an installed extension gets only the table above.

2. The extension writes `hello 1` (no id; further fields, if any, name capabilities of later
   protocol versions and are ignored by a rhun that does not know them) within **5 seconds**. Anything else first, or nothing, ends the process ("the
   first line was not hello 1", "no hello within 5 s"). A `hello` with another version:
   "protocol version not supported (rhun speaks 1)".
3. When `hello` named features (section 12) rhun knows, it first sends `0 features NAME…`, the ones
   it will use. Then it answers with the event `0 ready PROJECT` (`-` without a project) and then **catch-up**,
   a snapshot taken between frames: `opened` for every open document the subscription matches,
   `active`, and `cursor`/`selection` for the active one when subscribed. The extension sees
   the same world as one started at launch.
4. Normal operation until rhun quits, the user stops the extension, or a limit trips.
5. Stop: rhun sends `0 bye`, waits 1 second, sends SIGTERM to the process group, waits 1
   second, SIGKILL. When rhun itself quits it sends `bye`, waits up to 300 ms for the programs
   to leave, and sends SIGTERM to the rest. (Windows: the job object ends the process.)
   Everything the extension registered at runtime (its commands, their keys), its marks, status
   text, per-document options and open prompt is removed. A program that closes its own input or output is stopped at once.
6. A process that exits on its own is "crashed" with its exit status; its manifest commands
   stay in the palette and invoking one starts it again. Three crashes within a minute leave
   it stopped: invoking a command then shows the reason, and only Settings restarts it. The
   same holds for every stop a limit caused. A failed start (no such file, permission) is "failed
   to start: reason".

If rhun dies, the extension dies: on Linux through the parent-death signal, on Windows through
the job object; on macOS the extension's standard input reaches end of file, and an extension
should exit when it does. The signal reaches the extension's own process, not programs it
started: an extension that runs a language server must stop it when its input ends.

A program that cannot be executed (a bad interpreter line, a binary for another system) is
"failed to start: the program could not be executed".

At most 16 extensions run at once; the seventeenth waits, idle, and starts when a slot frees.

## 5. Capabilities: `[needs]`

| Key | Values | Gates in the protocol |
| --- | --- | --- |
| `documents` | `none` (default), `read`, `write` | `text`, `range`, `diagnostics`, `hover` need read (`write` includes it); `replace`, `insert` need write |
| `project` | `none` (default), `read`, `write` | the project folder on disk, enforced by the kernel sandbox on Linux and macOS (Windows relies on the declaration) |
| `home` | `none` (default), `read` | the user's home outside the project (the same) |
| `network` | `no` (default), `yes` | (the same) |
| `terminal` | `no` (default), `yes` | `term` |
| `run` | program names, space separated | programs the extension may start (the same); the state folder `$RHUN_STATE` is always writable. On macOS a name that is one of Xcode's tools in `/usr/bin` (`git`, `clangd`, `clang`, `swift`, `sourcekit-lsp`) also lets it read Xcode (or the command line tools) and run the tool there, as that `/usr/bin` program does |

A request without its capability is answered `error ID needs documents=write` and nothing
happens. The install dialog and the extension's page show the block in plain words ("read and
change the files you have open, read the files in your project, run gofmt"). On Linux (Landlock)
and macOS (the sandbox) the kernel holds the program to `project`, `home`, `network` and `run`;
where it cannot (Windows, a Linux kernel older than 6.2), the extension still runs and its page
says that this system cannot hold it to these permissions.

## 6. Documents and positions

- A **document id** `DOC` is an integer, unique for the session, given in `opened`. A closed
  and reopened file gets a new id; a reloaded file keeps its id and gets a new version.
- A **version** is an integer that increases with every change the user or an extension makes;
  by how much is not specified (a `replace` that deletes and inserts counts twice). Equal
  versions mean identical text. `opened`, `saved`, `changed`, `text` and `range` carry it;
  `replace` and `insert` require the current one.
- A **position** is `LINE COL`: 1-based line, 1-based **byte** column within the line's UTF-8
  bytes. `COL` must not fall inside a multibyte sequence (`badpos`). `COL` may be one past the
  line's last byte, which is the position of the line's LF (or the end of the document on the
  last line). A **range** `L1 C1 L2 C2` is half-open: from the first position up to, not
  including, the second; a range that ends at `L+1 1` includes line L's LF. Documents with CRLF
  endings report positions on the stored bytes.
- The **text** of a document is its bytes as rhun stores them: lines end in LF. A file with CRLF
  endings is converted when read and written back with CRLF; the protocol never sees CR.
- An extension that subscribes to `edits` receives every change as deltas (the `edits` event)
  and keeps its own copy of the text in step; `resync` tells it that deltas were lost and it
  must fetch `text` again. An extension that subscribes only to `changed` fetches what it
  needs when it needs it. Choose `changed` + `text` for a whole-document pass (a checker, a
  formatter), `edits` when a copy of the text must stay in step cheaply (a language server).
  Both report the extension's own `replace` and `insert` too.

### 6a. The `edits` body

One record per change, in the order they happened, each applying to the text as the previous
record left it:

```
START DEL INS
<INS bytes>
```

`START` is a byte offset from the start of the document, `DEL` the number of bytes deleted
there, `INS` the number of bytes inserted there (the bytes follow the record's LF; the next
record starts right after them). A keystroke is one record; a paste one; an extension's
`replace` is a delete record and an insert record. Undo and redo are records too. The event's
`VERSION` is the document's version after the last record; an extension that applies them to
its copy of the text at the previous version has the text at `VERSION`. Positions in records
are byte offsets because that is what the editor has; converting them to lines is the
extension's job, and it has the text to do it with.

### 6b. Diagnostics

Each line of a `diagnostics` body is `L1 C1 L2 C2 SEVERITY MESSAGE`: a half-open range of the
document at the given version, a severity among `error`, `warning`, `info` and `hint`, and the
message to show (the rest of the line; no line feeds). rhun draws a dot in the gutter and an
underline under the range, counts errors and warnings in the status bar, and shows the message
when the pointer rests on the range. Marks move with later edits (a mark whose range is deleted
disappears); an extension that wants exact marks sends them again after `changed`. Marks belong
to the extension that sent them: another extension's marks on the same document are untouched,
and they are removed when the extension stops. A range ending at `L+1 1` marks the line's end.

### 6c. Per-document options

`option DOC KEY VALUE` overrides one of the user's settings for one document: `tab_width`
(1 to 16), `insert_spaces`, `trim_trailing_whitespace`, `final_newline` (`yes` or `no`);
`VALUE` `-` removes this extension's override of that key. Precedence is extension over user
over default; when two extensions set the same key the last writer wins; an override is removed
when its extension stops or the document closes, and the user's setting applies again. Options
never reach the config file. An unknown key or value is `syntax`.

### 6d. The extension's own settings

`[settings]` declares settings the user changes on the extension's page, each `KEY.FIELD = VALUE`:
`type` (`string`, the default; `bool`, shown as a switch, `yes` or `no`; `int`, a whole number
from 0 to 2147483647 shown with − and +, within `min` and `max`; `choice`, a row of `options`,
space separated), `default`, `title`, `description`. Keys are `[a-z0-9_]`. rhun keeps the values in `~/.config/rhun/extensions.ini`
(`setting.KEY = VALUE` under `[owner/name]`, only those that differ from their defaults) and sends
each as `0 setting KEY VALUE` right after `ready`, before the catch-up, and again whenever the user
changes it. A value always fits its type: a default that does not is made to (a switch's is `no`
unless it says `yes`, a number is brought into its range, a choice's is its first option).

## 7. Limits

| What | Limit | On overflow |
| --- | --- | --- |
| `extension.ini` | 65536 bytes | discovery records a failed extension; installation rejects the package before the permission dialog |
| a line | 65536 bytes | the extension is stopped ("a line over 64 KB without an end") |
| `replace`, `insert` body | 4 MB | the extension is stopped ("a body over its cap"): rhun will not read megabytes to discard them |
| `diagnostics` | 2000 lines; a message 200 bytes | `toobig`; messages are cut |
| `hover` body | 4000 bytes | cut |
| pending `edits` per document per frame | 1 MB | the records are dropped and `resync` is sent |
| `toast` body | 200 bytes | truncated at a character boundary |
| `choose` | 1000 items; the prompt and each item 200 bytes | `toobig`; cut at a character boundary |
| `ask` prompt | 200 bytes | cut |
| `term` body | 4096 bytes; plain text only | `toobig`; `syntax` |
| `register` | 64 commands per extension; `NAME` 40 bytes, `TITLE` 80 bytes | `toobig`; `syntax`; cut |
| `status` body | 80 bytes | truncated at a character boundary |
| rhun's outgoing queue per extension | 1 MB; stopped after 10 s full; 8 MB stops it at once | stopped: "not reading" |
| one parsing run | 2 ms | the rest waits for the next window |
| messages from one extension per 16 ms window | 500 messages or 256 KB | the rest waits for the next window; 30 windows in a row with 100 messages or 64 KB or more is "flooding" and stops it |
| views (12a) | 8 per extension, 32 in all; a picture 1 MB, 20000 shapes, 200000 points; a rendering covers at most 64 times the view's area | `toobig`; `syntax line N`; the shapes past it are left out |
| a completion list (12b) | 1 MB and 4096 items; rhun keeps the best 50 for what was typed | the list is dropped whole |
| signature help (12c) | 64 KB | hidden |
| locations, symbols (12d, 12e) | 1 MB and 10000 records | the answer is dropped whole |
| code actions (12f) | 4 MB (their edits included) and 1000 actions | the answer is dropped whole |
| a workspace edit (12g, 12h) | 4 MB, 1000 files, 100000 edits | nothing changes, and rhun says why |
| any answer's body (`ok ID LEN`) | 4 MB | the extension is stopped ("a body over its cap") |
| requests rhun waits on, all extensions | 64 | none sent until answers come |

## 8. Messages

### Events from rhun (id 0)

| Event | Fields | When |
| --- | --- | --- |
| `ready` | `PROJECT` | after `hello`; `-` without a project |
| `opened` | `DOC VERSION LANG PATH` | a document the `opened` subscription matches is opened, and at catch-up; `LANG` is the grammar name with spaces as `_` (`Visual_Basic`) or `-`; `PATH` absolute or `-` for an untitled document; PATH is the last field and may contain spaces (a path with a line feed in it is not supported) |
| `active` | `DOC` | the active tab changed, and at catch-up (`0` when no document is active) |
| `saved` | `DOC VERSION` | the document was written to disk |
| `changed` | `DOC VERSION` | the document changed, by the user or by any extension's `replace`/`insert` (this one's included); at most once per frame per document |
| `cursor` | `DOC LINE COL` | the cursor moved, with or without a selection; once per frame; also for the document that just became active, so a subscriber always knows the active document's cursor |
| `selection` | `DOC L1 C1 L2 C2` | a selection exists and changed, or just collapsed (then both positions are equal); once per frame; `L1 C1` is the anchor, `L2 C2` the cursor |
| `closed` | `DOC` | the tab closed |
| `project` | `PATH` | the project changed (`-` when closed); running extensions restart so `RHUN_PROJECT` and disk permissions describe the new project; `on = project` extensions start for the new one |
| `run` | `COMMAND DOC` | one of the extension's commands was invoked, by its full name `NAME.command`; `DOC` is the active document or `0`. It follows the frame's other events (`opened`, `active`, `cursor`, …), and the catch-up when the command started the extension, so what it names is known by then. There is no request for the cursor: an extension that needs it subscribes to `cursor` |
| `bye` | | rhun is about to stop the extension |
| `setting` | `KEY VALUE` | one of its own settings (section 6d): each after `ready`, and on every change; `VALUE` is the rest of the line |
| `edits` | `DOC VERSION LEN` + body | the changes since the last frame to a document the `edits` subscription names, as records that lead from the text the extension has to the text at `VERSION` (section 6a); not sent for the frame in which the document was reported `opened` |
| `resync` | `DOC VERSION` | deltas were lost (more than 1 MB of edits piled up in one frame): fetch `text`. A reload from disk arrives as deltas |
| `hover` | `DOC LINE COL` | the pointer has rested half a second on that position of a document the `hover` subscription names; answer with a `hover` request to show something, or not at all |
| `action` | `LEN` + body | the user chose one of its code actions that has DATA (section 12f): the DATA, after the action's edit was made |

### Requests from the extension

| Request | Reply | Needs | Effect |
| --- | --- | --- | --- |
| `ID text DOC` | `ok ID VERSION LEN` + body | documents read | the whole text |
| `ID range DOC L1 C1 L2 C2` | `ok ID VERSION LEN` + body | documents read | the bytes of the range |
| `ID replace DOC VERSION L1 C1 L2 C2 LEN` + body | `ok ID NEWVERSION` | documents write | replaces the range with the body as one undo step, if `VERSION` is current (`stale` otherwise); the cursor inside the range moves to the end of the new text; the body is inserted as given (bytes, no conversion) |
| `ID insert DOC VERSION LEN` + body | `ok ID NEWVERSION` | documents write | inserts at the cursor, replacing the selection if any |
| `ID apply LEN` + body | `ok ID` | documents write | a workspace edit (section 12g) across documents and files: checked whole, then each document changed as one undo step; files that are not open require read access through `project` or `home` (or belong to the extension or its state folder), and open in tabs, unsaved. Nothing changes on `needs` (a file is outside those permissions), `syntax`, `nofile` (a path that is not absolute, or a file that cannot be opened), `nodoc` (a document it names closed), `stale`, `readonly`, `badpos` (a range outside its text, or two edits that overlap) or `toobig`; the TEXT is a sentence (for `stale` too, not a version: several documents may be named). For a language server's `workspace/applyEdit` |
| `ID select DOC L1 C1 L2 C2` | `ok ID` | | selects the range with the cursor at `L2 C2`; equal positions place the cursor; a document that is not on screen keeps the positions for when it is |
| `ID toast LEN` + body | `ok ID` | | a message on screen for two seconds |
| `ID status LEN` + body | `ok ID` | | the extension's status-bar text; an empty body clears it |
| `ID open LINE COL PATH` | `ok ID DOC` | | opens an existing text file in a tab (or activates its tab) with the cursor at the position, clamped to the document; `0 0` keeps the file's cursor; a relative PATH is inside the project; a closed file requires read access through `project` or `home` (or must belong to the extension or its state folder), checked after resolving symbolic links; `needs` if the path is outside those permissions; `nofile` for a missing file, `nodoc` for an image; the `opened` event follows at the next frame |
| `ID cancel ID2` | `ok ID` | | withdraws request `ID2` if rhun has not started on it |
| `ID register NAME KEYS TITLE` | `ok ID` | | a command for this session: `EXTENSION.NAME` in the palette as "TITLE (Extension)", with the default key `KEYS` (one combination with a modifier, `ctrl+alt+k`; `-` for none; the user's `[keys]` and keys already bound win, and a bare key is ignored); it is invoked like a manifest command (the `run` event) and removed when the extension stops; `NAME` is `[a-z0-9_]+`; a name in use is `refused` |
| `ID option DOC KEY VALUE` | `ok ID` | documents write for `trim_trailing_whitespace` and `final_newline` (they change what is saved) | a per-document setting (section 6c), until the document closes or the extension stops; `VALUE` `-` removes this extension's override |
| `ID choose LEN` + body | `ok ID N` | | a pick list in the palette: the body's first line is the prompt, the other lines the items; the user filters and picks, and `N` is the 1-based number of the item picked; `error ID cancelled` when the user dismisses it; `busy` while a prompt or the palette is open |
| `ID ask LEN` + body | `ok ID LEN` + body | | a text prompt with the body as its label; the answer comes back as the reply's body (empty when the user entered nothing); `cancelled`, `busy` as for `choose` |
| `ID term LEN` + body | `ok ID` | terminal | the body, one line of plain text (valid UTF-8; no control, C1, bidi-control or zero-width characters: `syntax`), shown from its start in rhun's prompt; the user edits it, and Enter (not within the first half second) types it into the terminal panel's shell (opened if needed), after Ctrl-U clears the shell's line, and answers `ok`; Esc or an empty line answers `cancelled`; `busy` as for `choose`. The command never reaches the shell without that step |
| `ID cmd NAME` | `ok ID` | per command (section 8a) | runs one of rhun's own commands, among those of section 8a, as if the user had invoked it; any other name is `refused` |
| `ID diagnostics DOC VERSION N LEN` + body | `ok ID` | documents read | replaces this extension's marks on the document with the `N` lines of the body (section 6b) if `VERSION` is current (`stale` otherwise); `N` is 0 to clear; at most 2000 |
| `ID hover DOC LINE COL LEN` + body | `ok ID` | documents read | the text to show at that position; shown only while the pointer still rests there; at most 4000 bytes. Positions refer to the current text (a reply that arrives after an edit may be off; rhun does not check). When a diagnostic's range covers the position, its message is shown instead |

### 8a. The commands `cmd` may run

Editing, with `documents = write`: `undo`, `redo`, `duplicate_line`, `delete_line`,
`move_line_up`, `move_line_down`, `indent`, `outdent`, `toggle_comment`, `newline_below`,
`newline_above`. Selection: `select_all`, `select_line`, `select_next`. Navigation and search:
`find`, `replace`, `find_next`, `find_prev`, `find_in_files`, `goto_line`, `quick_open`,
`command_palette`, `next_tab`, `prev_tab`, `reveal_file`, `focus_explorer`. View:
`toggle_sidebar`, `toggle_agents`, `toggle_word_wrap`, `toggle_whitespace`,
`toggle_line_numbers`, `zoom_in`, `zoom_out`, `zoom_reset`. Terminal, with `terminal = yes`:
`toggle_terminal`, `new_terminal`, `clear_terminal`. A command whose capability the manifest
lacks is `needs`. Nothing that saves, closes, opens or deletes files, quits, changes settings or
the theme, touches the clipboard (`cut`, `copy`, `paste`: a paste can reach the terminal),
git, the agents or updates, or kills a terminal.

Prompts are one at a time for the whole editor: a second `choose`, `ask` or `term` while one
is open, or while the user has the palette open, is `busy`. The user opening the palette,
the extension stopping, or `cancel` end an open prompt with `cancelled`.

### Requests from rhun

None without features: everything rhun sends is an event (section 12 adds requests). The request form from rhun (`ID verb …`, answered
by the extension with `ok ID` or `error ID CODE`) is reserved for later versions; an extension
that receives a line starting with a non-zero number should answer `error ID unknown`.

## 9. An exchange

```
→ hello 1
← 0 ready /home/me/app
← 0 opened 1 3 Go /home/me/app/main.go
← 0 active 1
← 0 run format.format_document 1
→ 7 text 1
← ok 7 3 41
← package main

func  main() { println("x") }
→ 8 replace 1 3 1 1 4 1 38
→ package main

func main() { println("x") }
← ok 8 4
← 0 changed 1 4
```

## 10. Testing an extension

`rhun PROJECT --headless 1000x600 --scale 1 --extensions DIR --script test.txt` runs rhun
without a display, with the extensions found under `DIR` (laid out as installed:
`DIR/OWNER/NAME/extension.ini`; such a session passes rhun's own environment on to the
programs, so a test can reach them through variables). The state folder is
`$XDG_STATE_HOME/rhun/extensions/OWNER--NAME` (`~/.local/state` when the variable is unset, on
every system), so a test sets `XDG_STATE_HOME` to a folder of its own and seeds files there. The
script is one command per line; what a command prints goes to standard output. The ones an
extension's test needs:

| Command | Does |
| --- | --- |
| `wait MS` | runs the event loop for that long. The frame that follows a step sends its events; the extension's replies land in a later poll: use two `wait 300` lines after a step before reading its effect |
| `open PATH` | opens a file in a tab |
| `cmd NAME.command` | invokes a command by name (an extension's starts it) |
| `type TEXT` | types text into the focused field or document (trailing spaces are dropped: `key space` for those) |
| `key COMBO` | one key or combination per line, as in `[keys]`: `Return`, `Escape`, `Tab`, `space`, `ctrl+End`, `ctrl+Tab`, `ctrl+alt+9` |
| `move X Y` | moves the pointer to a window pixel (a hover event follows after half a second) |
| `print-text-cell LINE COL` | `X Y`, the pixel of that cell of the active document, for `move` |
| `wait-ext NAME STATE` | waits until an extension is `running`, `stopped` or `crashed`; prints `ext=NAME state=STATE` |
| `print-extensions` | one line per extension: `OWNER/NAME state=S pid=P cmds=a,b [status=TEXT] confined=0|1` (registered commands included; `confined` 1 when the system holds it to its `[needs]`) |
| `print-state` | `tabs=N active=NAME line=L col=C sel=BYTES dirty=D lang=LANG …` |
| `print-doc` | the active document's text, then `<eod>` |
| `print-marks` | one line per mark of the active document, `L1 C1 L2 C2 SEVERITY OWNER/NAME MESSAGE`, then `errors=N warnings=N` |
| `print-hover` | `hover=DOC LINE COL sent=S text=T` for the position the pointer rests on (`sent` 1 once the event went out, `text` the extension's answer), or `hover=none` |
| `print-tip` | the tooltip on screen (a mark's message or a hover text), `tip=` when none |
| `print-toast` | `toast=TEXT`, or `toast=` |
| `print-palette` | `choose=PROMPT` or `prompt=LABEL` when an extension's prompt is open, `field=TEXT`, then the rows with `> ` on the selected one; `none` when closed |
| `print-options` | `tab_width=N insert_spaces=B trim_trailing_whitespace=B final_newline=B` for the active document |
| `wait-term TEXT`, `print-term` | waits until the terminal's screen shows the text (10 s at most); prints the screen |
| `print-views` | one line per view (12a): `view=ID title=T shapes=N size=WxH at=X,Y` (`at` is the window pixel of its top left, for `move` and `click`) |
| `click X Y`, `tap X Y` | a press and release at a window pixel (`tap`: in one frame) |
| `shot PATH` | the window as a binary PPM picture, to look at a view or a list |
| `print-complete` | `complete open=O items=N shown=S sel=I labels=A|B|…` (`open` 0 closed, 1 asked, 2 shown; `labels` best first) and `signature open=O from=F to=T text=SIGNATURE` |
| `quit` | ends the session |

For the Extensions page, installing and settings:

| Command | Does |
| --- | --- |
| `cmd extensions`, `ext-page NAME`, `ext-page list` | the Extensions page, an extension's details, the list |
| `print-ext-page` | the list (`Title version on|off [problem=…]` a line each) or the details (`pinned=`, `can: …`, `held=1|0`, `setting KEY=VALUE`) |
| `cmd extensions_menu`, `print-extbar` | the title bar's dropdown; `menu=open|closed` and `pinned=a,b` |
| `ext-menu NAME`, `menu-pick LABEL`, `print-menu` | what a click on the extension's button does (a menu of its commands when it has several); the open menu's item with that label runs; the open menu's labels |
| `ext-set NAME pinned|disabled 0|1`, `ext-set NAME stop|restart` | what the page's switches and buttons do |
| `ext-setting NAME KEY VALUE` | changes one of its settings as its page does |
| `ext-install OWNER/NAME`, `wait-install` | Install from GitHub with that answer; waits for the download: `install=asking|failed|done` |
| `print-dialog`, `dialog yes|no` | the Add or Remove dialog's question, line and button; its button or Cancel |
| `ext-remove NAME` | Remove (the dialog asks) |

`rhun-ext-check DIR` (built from `conformance/ext` in `rhun-editor/extension-protocol`) plays
rhun's side against an installed-layout extension folder without rhun: grammar, ids, positions,
versions, bodies, limits, and that the program survives an unknown event, an error reply,
fragmented input, a burst of events, `bye` and the end of its input. The template's
`tests/run.sh` shows a headless test from start to end.

## 11. Releases and installing

Install from GitHub (the extensions button, then Install from GitHub…, with `owner/name` or the
repository's URL) takes the repository's latest release. Its assets must be, for each target the
manifest names:

| Asset | What |
| --- | --- |
| `NAME-TARGET.tar.gz` | the extension folder's contents at its top level: `extension.ini`, the program (`bin/NAME-TARGET` as the manifest's `run.TARGET` says), `themes/`, `syntax/`, anything else; plain files and folders only |
| `sha256sums.txt` | `SHA256  FILE` lines covering every archive |

rhun downloads the archive and the sums, refuses a mismatch, an archive with links, devices,
absolute paths or `..`, one without `extension.ini`, and one whose manifest names another
`owner` or `name` than the repository (letter case aside: the manifest's spelling names the
folders); then shows the manifest's title (cut after 120 bytes), version and permissions and
asks. Add puts it in `~/.config/rhun/extensions/OWNER/NAME/`,
replacing an earlier version, and starts it at once; Remove stops it and deletes that folder and
its state folder. `RHUN_EXT_BASE` points rhun at another host than `https://github.com` with the
same layout (`BASE/OWNER/NAME/releases/latest/download/ASSET`), which is how tests serve a
release from disk. On Windows the download and the checks run in PowerShell with the `curl.exe`
and `tar.exe` that come with Windows 10 1803 and later. The template's release workflow produces exactly these assets.

## 12. Features

Features are protocol additions an extension asks for by name, so that one built for a later rhun
still works with an earlier one and the other way round. They ship with rhun 0.18.0; sections
1 to 11 are what an extension sees when it names none.

- The extension lists the features it understands after the version: `hello 1 views complete`.
- The agreed set is the intersection of those names and the features rhun supports. rhun sends it
  as `0 features NAME…` before `ready`; without a `features` event before `ready` the set is
  empty. The set is fixed for the life of the process.
- Neither side sends a message of a feature that was not agreed; rhun answers such a request
  `error ID unknown` (after reading its body, which its verb defines).
- A feature's grammar never changes under its name: an incompatible change is a new name
  (`complete2`). Optional parts of a body are length-delimited records a reader skips when it does
  not know them.

| Feature | What it adds |
| --- | --- |
| `views` | pictures the extension draws in tabs of their own (12a) |
| `complete` | completion lists while the user types (12b) |
| `signature` | the signature of the call the cursor is in (12c) |
| `definition` | where the symbol under the cursor is defined, for Go to Definition (12d) |
| `references` | where it is used, for Find All References (12d) |
| `symbols` | the document's outline, for Go to Symbol (12e) |
| `actions` | code actions and quick fixes for a range (12f) |
| `rename` | renaming a symbol across files (12g) |
| `format` | formatting a document (12h) |

**Requests from rhun.** With one of the provider features agreed (all but `views`), rhun sends requests `ID VERB …`
with ids it picks: counting up from 1, never reused while the process runs. The extension answers
each at most once, with `ok ID LEN` followed by exactly `LEN` bytes, or with `error ID CODE TEXT`
(no body). This holds for every request rhun sends, so a reply can be framed without knowing what
it answers. rhun may send `0 cancel ID` when the answer is no longer wanted (the user typed on,
moved away); the extension may still answer, and rhun reads and drops a late or unknown answer.
An extension answers rhun's requests in any order and goes on reading meanwhile; the rule of
section 3 that requests are handled in the order they arrive is about rhun handling the
extension's.

### 12a. Views

A view is an editor tab whose content the extension draws: charts, graphs, diagrams, previews.
The extension describes a picture; rhun keeps that description and renders it into the tab's
own pixels, again after the tab is resized, the scale changes or the theme does (the coordinates
stay as they were until the next picture). No extension code runs while rhun draws a frame, and
nothing a picture holds reaches outside the view.

| Request | Reply | Effect |
| --- | --- | --- |
| `ID view TITLE` | `ok ID VIEW` | opens a tab titled `TITLE` (rest of the line, at most 80 bytes) and shows it; at most 8 open per extension (`error ID toobig 8 views`) |
| `ID draw VIEW LEN` + body | `ok ID` | the picture, replacing the last one once all of it is read; `error ID syntax line N` names the first line it cannot read, and nothing changes |
| `ID unview VIEW` | `ok ID` | closes the tab; no `closed` event follows |

`VIEW` is a number rhun hands out, never reused in a session; another extension's view or an
unknown one is `error ID noview`. Views are not documents: they appear in no document event, and
while one is the active tab `active` reports `0`. When the extension stops, its views close.

| Event | Fields | When |
| --- | --- | --- |
| `view` | `VIEW size W H` | the drawable size in logical pixels: after the view opens (after the `ok`) and whenever it changes |
| `view` | `VIEW metrics small ADV LH normal ADV LH large ADV LH mono ADV LH` | the character advance and line height of each text size, logical pixels; after the view opens and when the scale or font changes |
| `view` | `VIEW hover X Y` | the pointer moved over the view, no button down (at most once per frame) |
| `view` | `VIEW down X Y` | the left button was pressed over the view |
| `view` | `VIEW drag X Y` | the pointer moved with the left button down (at most once per frame) |
| `view` | `VIEW up X Y` | the left button was released |
| `view` | `VIEW click X Y` | a press and release in the same place (4 pixels either way): after `up` |
| `view` | `VIEW leave` | the pointer left the view |
| `view` | `VIEW wheel X Y DY` | the wheel turned over the view: `DY` logical pixels, positive is down |
| `view` | `VIEW closed` | the user closed the tab |

Events describe the pointer, not a picture: they arrive in order, and the extension hit-tests
them against the picture it last drew.

The picture is one shape a line, drawn in order over the editor's background, in logical pixels
with `0 0` at the top left, clipped to the view (coordinates are never moved). A number is
`[-]DIGITS[.DIGITS]`, at most 7 digits before the point and 9 after; widths, heights, radii and
stroke widths are not negative (a zero draws nothing). Colors are `#rrggbb`, `#rrggbbaa`
(alpha blended over what is below), or a name that follows the theme: `fg`, `muted`, `bg`,
`panel`, `border`, `accent`, `error`, `warning`, `success`, and `c1` to `c8` for the series of a
chart.

| Shape | Fields |
| --- | --- |
| `rect` | `X Y W H COLOR`: a filled rectangle |
| `rrect` | `X Y W H R COLOR`: rounded corners of radius `R` |
| `line` | `X1 Y1 X2 Y2 WIDTH COLOR`: round ends |
| `poly` | `WIDTH COLOR X1 Y1 X2 Y2 …`: a polyline, round joins and ends, 2 to 10000 points |
| `fill` | `COLOR X1 Y1 X2 Y2 X3 Y3 …`: a filled polygon, closed, nonzero rule, 3 to 10000 points |
| `circle` | `X Y R COLOR`: filled |
| `ring` | `X Y R WIDTH COLOR`: an outline centered on the radius |
| `pie` | `X Y R FROM TO COLOR`: a filled wedge from angle `FROM` to `TO`, degrees, 0 at the top, clockwise; `0 360` is a full disc |
| `text` | `X Y SIZE ALIGN COLOR TEXT…`: `SIZE` is `small`, `normal`, `large` or `mono`; `ALIGN` is `left`, `center` or `right` about `X`; `Y` is the top of the line (its height is the size's `LH`); the text is the rest of the line after one space |
| `clip` | `X Y W H`: what follows is drawn only inside this rectangle (within the view), until `unclip` or the next `clip`, which replaces it |
| `unclip` | the whole view again |

rhun's interface font is monospaced: a text's width is its characters times the size's `ADV`
(a wide character, as in Chinese, counts twice). A body is at most 1 MB, 20000 shapes and
200000 points; an unknown shape is skipped (a later rhun may draw it). Only the active view keeps
its pixels; the others are rendered again when shown. The SDKs turn data into shapes: the Rust
crate and the C header include charts (lines, bars, pies, axes, legends).

Planned for a later feature: keyboard focus and keys, modifiers on pointer events, images,
curves and transforms, and accessible descriptions of what a view shows.

### 12b. Completion

The manifest says for which files, with section 2's patterns: `[provides]` `complete = *.rs *.go`,
and which characters ask at once: `complete.triggers = . :`. A provider gets `opened` and `edits`
for those files as if it subscribed to them, and is started by the first one that opens. The
list is otherwise asked for after the user has typed two characters of a word (letters, digits,
`_` and any non-ASCII character) and paused for 150 ms, or on `ctrl+space`.

`ID complete DOC VERSION LINE COL KIND CHAR`: `KIND` is `typed`, `trigger` (`CHAR` is the
character typed), `invoked` (the key) or `incomplete` (asking again for a list marked
incomplete); `CHAR` is `-` unless `KIND` is `trigger`. Every event about the document up to
`VERSION` has been sent before.

The answer is `ok ID LEN` + body. The body's first line is `N MORE` (`MORE` 1: the list is
incomplete and typing should ask again); then `N` items, each

```text
L1 C1 L2 C2 KIND LABEL_LEN INSERT_LEN DETAIL_LEN EXTRA_LEN LF
<LABEL bytes><INSERT bytes><DETAIL bytes><EXTRA bytes>
```

with nothing between them and the next item's header. `L1 C1 L2 C2` is the range the insertion
replaces, in the document at `VERSION`; `LABEL` is what the list shows and what typing filters on
(one line); `INSERT` is the text put in (it may hold LF); `DETAIL` is shown beside the label (one
line: the type, the package). `KIND` is one of `text`, `method`, `function`, `field`, `variable`,
`class`, `struct`, `interface`, `module`, `property`, `enum`, `keyword`, `snippet`, `constant`,
`value`, `file`, `folder` (another is shown as `text`). `EXTRA` holds optional records, each a
header line and its bytes, skipped when unknown:

- `edit L1 C1 L2 C2 LEN` + `LEN` bytes: another replacement made with the item (an import), in
  the document at `VERSION`, not overlapping the item's range or another edit;
- `doc LEN` + `LEN` bytes: documentation shown beside the list when the item is selected;
- `cursor OFFSET 0` (no bytes follow): where the cursor goes, `OFFSET` bytes into `INSERT` (a
  snippet's `$0`, between parentheses); one past `INSERT` or inside a character is ignored, and
  without it the cursor goes after `INSERT`.

The body holds exactly `N` items. At most 1 MB and 4096 items; a body that breaks these rules is
dropped whole. rhun shows the list while the cursor stays in the word it was asked for and the
document changes only by typing in that word. It filters and orders the items itself: an item
stays when the letters of the text from its range's start to the cursor appear in its label in
order, in any case (`ap` keeps `grape`); a match at the start, at word starts (`_`, `.`, a
capital) and in a row ranks higher, a shorter label breaks ties, and items that rank the same
keep the provider's order; the best 50 are kept. So answer with every candidate for the word, not
those that match what was typed so far: typing narrows the list, deleting widens it again, and
neither asks again. When `MORE` was 1, every change in the word (typing or deleting) asks again,
with `KIND` `incomplete`. Moving away, Escape, or an edit elsewhere closes the list; an answer
for a document or cursor that has moved on is dropped. Enter or Tab puts the selected item in:
its range, extended to the cursor by what was typed since, is replaced, with its extra edits, as
one undo step. A provider with `documents = read` only may suggest: the user choosing the item is
the edit.

A provider that reads the text with `text` may get a newer version than the request's, the user
having typed on in the word meanwhile: the text before the request's cursor is the same in both,
so ranges that end at the request's cursor can be computed on either. An empty list is the
answer `0 0` and no items.

### 12c. Signature help

`[provides]` `signature = *.rs *.go` and `signature.triggers = ( ,`. When the user types a
trigger, or presses `ctrl+shift+space`, rhun asks `ID signature DOC VERSION LINE COL KIND CHAR`
(`KIND` `trigger`, `invoked` or `update`); while help is shown it asks again (`update`) after
each edit or cursor move.

The answer is `ok ID LEN` + body: the first line `FROM TO`, the active parameter's bytes in the
signature (zero-based, half-open, `-1 -1` for none); the second line the signature; then
documentation, plain text, any number of lines. `ok ID 0` hides it. At most 64 KB. rhun shows the
signature above the cursor (below it on the editor's first lines), the active parameter in the
accent color, its documentation under it, until an empty answer, Escape, another document, or the
document closing. One provider answers per document: the first found that agreed `signature` and
whose patterns match.

Completion asks every provider whose patterns match (for a trigger character, those that list it)
and merges their lists; each answer replaces what that provider said before for the same word.
The keys `ctrl+space` and `ctrl+shift+space` are the commands `complete` and `signature_help`,
which `[keys]` can rebind.
### 12d. Definition and references

The requests of 12d to 12h are answered like those of 12b: `ok ID LEN` and the body, or `error
ID CODE TEXT`, which counts as an empty answer (nothing found, nothing to change); only a
rename's `error ID refused TEXT` is shown to the user (TEXT, cut at 200 bytes; keep it to a
sentence). Versions are those of the `opened` and `edits` events (0 or more). rhun waits 5 s for
definitions and symbols, 10 s for references and format, 3 s for code actions and 15 s for a
rename; an answer after that is dropped (and the request withdrawn with `0 cancel ID`). Numbers
in answers have at most 10 digits; a path is at most 4096 bytes, a name, title or detail at most
1000, a kind word at most 40.

`[provides]` `definition = *.go` and `references = *.go`. The commands `definition` (F12, Go to
Definition) and `references` (shift+F12, Find All References) ask `ID definition DOC VERSION LINE
COL invoked -` and `ID references DOC VERSION LINE COL invoked -` of every provider whose patterns
match the active document. The answer is `ok ID LEN` and a **locations** body:

```text
N LF
then N records: L1 C1 L2 C2 PATH_LEN TEXT_LEN LF <PATH bytes><TEXT bytes>
```

`PATH` is an absolute path, or `#DOC` for a document by its id (an untitled one); `L1 C1 L2 C2`
is the range there, in the open document when the file is open, else in the file on disk. `TEXT`
is a one-line preview (its line's text, trimmed), or empty. At most 1 MB and 10000 records; a
body that breaks the rules is dropped whole. rhun merges the providers' answers: one location
opens at once (the range selected), several make a list in the palette, nearest first (this
file, then by path), and choosing one opens it. The body `0` LF (`ok ID 2`) says there is none.

### 12e. Symbols

`[provides]` `symbols = *.go`. The command `symbols` (ctrl+r, Go to Symbol in File) asks `ID
symbols DOC VERSION LINE COL invoked -` (the cursor's position, for providers that rank by it).
The answer is `N` LF and N records:

```text
L1 C1 L2 C2 KIND DEPTH NAME_LEN DETAIL_LEN LF <NAME bytes><DETAIL bytes>
```

in document order. `KIND` is one of `file module namespace package class method property field
constructor enum interface function variable constant string number boolean array object key
null enum_member struct event operator type_parameter` (another is shown as given); `DEPTH` is
0 for a top-level symbol and one more for each level it is nested in; `DETAIL` is shown beside it
(a signature, a type). At most 1 MB and 10000 records. rhun shows them in the palette, indented
by depth and filtered by what is typed, and puts the cursor at the chosen one's start. One
provider answers: the first that agreed `symbols` and whose patterns match.

### 12f. Code actions

`[provides]` `actions = *.go`. The command `code_actions` (ctrl+., Show Code Actions) asks `ID actions
DOC VERSION L1 C1 L2 C2`, the selection (an empty range at the cursor without one), of every
matching provider. The diagnostics there are the extension's to look up: it published them. The
answer is `N` LF and N records:

```text
KIND TITLE_LEN EDIT_LEN DATA_LEN LF <TITLE bytes><EDIT bytes><DATA bytes>
```

`KIND` is `quickfix`, `refactor`, `source` or another word (shown beside the title); `TITLE` one
line; `EDIT` a **workspace edit** (12g) applied when the action is chosen, or empty; `DATA` bytes
the extension gets back when the action is chosen, as the event `0 action LEN` and the bytes
(after the edit, if there is one; an edit rhun refuses sends nothing): an action that has to run
something does it then. At most
4 MB (the edits included) and 1000 actions. rhun lists the providers' actions in the palette, quick fixes first; with
none it says so.

### 12g. Rename and workspace edits

`[provides]` `rename = *.go`. F2 (the command `rename_file`) in a document that has a rename provider
asks for the new name in the palette, the word under the cursor filled in, then sends `ID rename
DOC VERSION LINE COL invoked NAME` (`NAME` without spaces) to the first matching provider. The
answer is a workspace edit; `error ID refused TEXT` shows `TEXT` (nothing can be renamed there).
F2 elsewhere renames the file, as before.

A **workspace edit** is `N` LF and N groups, one per file:

```text
PATH_LEN VERSION M LF <PATH bytes>
then M edits: L1 C1 L2 C2 LEN LF <LEN bytes of new text>
```

`PATH` as in 12d; `VERSION` the version of the open document the edits were made for, or `-` for
a file that is not open (its text on disk; for a document that is open, `-` is accepted only
while it has no unsaved changes). `#DOC` names an open document, untitled or not; a path names
a file by its absolute path; a file appears in one group only. The edits of a group all refer
to that one text and must not overlap; they need not be in order: rhun sorts them by start (an
insertion before a replacement that starts at the same place), and insertions at one position
keep their order in the list. rhun checks everything before it changes anything: each open
document at its version, each range inside its text, no overlaps, a file that is not open an
existing, readable text file. Then it opens the files that are not open in tabs of
their own and applies each group as one undo step; nothing is saved, so the user reviews and
saves. A check that fails changes nothing and says so. At most 4 MB, 1000 files and 100000 edits.
The cursor and selection of an edited document move with the text around them.

### 12h. Format

`[provides]` `format = *.go`. The command `format_document` (shift+alt+f, Format Document) sends
`ID format DOC VERSION TAB_WIDTH SPACES` (`SPACES` is `yes` or `no`, the document's own settings)
to the first matching provider. The answer is a workspace edit for that document (usually one
group, any number of edits), applied as one undo step; an empty one (`0`) changes nothing.

