The Frontend Protocol
Table Of Contents
Methods
These are mostly described by example rather than specified in detail. They are given in shorthand, eliding the JSON-RPC boilerplate. For example, the actual interaction on the wire for new_view
is:
to core: {"id":0,"method":"new_view","params":{}} from core: {"id":0,"result": "view-id-1"}
From front-end to back-end
client_started
client_started {"config_dir": "some/path"?, "client_extras_dir": "some/other/path"?}
Sent by the client immediately after establishing the core connection. This is used to perform initial setup. The two arguments are optional; the config_dir
points to a directory where the user’s config files and plugins live, and the client_extras_dir
points to a directory where the frontend can package additional resources, such as bundled plugins.
new_view
new_view { "file_path": "path.md"? }
-> "view-id-1"
Creates a new view, returning the view identifier as a string. file_path
is optional; if specified, the file is loaded into a new buffer; if not a new empty buffer is created. Currently, only a single view into a given file can be open at a time.
Note:, there is currently no mechanism for reporting errors. Also note, the protocol delegates power to load and save arbitrary files. Thus, exposing the protocol to any other agent than a front-end in direct control should be done with extreme caution.
close_view
close_view {"view_id": "view-id-1"}
Closes the view associated with this view_id
.
save
save {"view_id": "view-id-4", "file_path": "save.txt"}
Saves the buffer associated with view_id
to file_path
. See the note for new_view
. Errors are not currently reported.
set_theme
set_theme {"theme_name": "InspiredGitHub"}
Asks core to change the theme. If the change succeeds the client will receive a theme_changed
notification.
set_language
set_language {"view-id":"view-id-1", "language_id":"Rust"}
Asks core to change the language of the buffer associated with the view_id
. You need the syntect plugin for this to work. If the change succeeds the client will receive a language_changed
notification.
modify_user_config
modify_user_config { "domain": Domain, "changes": Object }
Modifies the user’s config settings for the given domain. Domain
should be either the string "general"
or an object of the form {"syntax": "rust"}
, or {"user_override": "view-id-1"}
, where "rust"
is any valid syntax identifier, and "view-id-1"
is the identifier of any open view.
get_config
get_config {"view_id": "view-id-1"} -> Object
Returns the config table for the view associated with this view_id
.
edit namespace
edit {"method": "insert", "params": {"chars": "A"}, "view_id": "view-id-4"}
Dispatches the inner method to the per-tab handler, with individual inner methods described below:
Edit methods
insert
insert {"chars":"A"}
Inserts the chars
string at the current cursor locations.
paste
paste {"chars": "password"}
Inserts the chars
string at the current cursor locations. If there are multiple cursors and chars
has the same number of lines as there are cursors, one line will be inserted at each cursor, in order; otherwise the full string will be inserted at each cursor.
copy
copy -> String|Null
Copies the active selection, returning their contents or Null
if the selection was empty.
cut
cut -> String|Null
Cut the active selection, returning their contents or Null
if the selection was empty.
scroll
scroll [0,18]
Notifies the back-end of the visible scroll region, defined as the first and last (non-inclusive) formatted lines. The visible scroll region is used to compute movement distance for page up and page down commands, and also controls the size of the fragment sent in the update
method.
resize
resize {width: 420, height: 400}
Notifies the backend that the size of the view has changed. This is used for word wrapping, if enabled. Width and height are specified in px units / points, not display pixels.
gesture
gesture {"line": 42, "col": 31, "ty": {"select": {"granularity": "point", "multi": false}}
Gestures correspond to certain pointer events on the text window. Currently, the following gesture types are supported:
{"select": {"granularity": "point", "multi": false}}
Adds a new selection region, preserving existing regions if multi
is true
. Granularity can be one of "point"
, "word"
, or "line"
.
{"select_extend": {"granularity": "point"}}
Modifies the selection to include a location. This gesture is usually mapped to shift+click on the frontend. Granularity can be one of "point"
, "word"
, or "line"
.
"drag"
Extends the selection to the mouse’s new location. Granularity is determined by the preceding select
gesture.
goto_line
goto_line {"line": 1}
Sets the cursor to the beginning of the provided line
and scrolls to this position.
Other movement and deletion commands
The following edit methods take no parameters, and have similar meanings as NSView actions. The pure movement and selection modification methods will be migrated to a more general method that takes a “movement” enum as a parameter.
delete_backward delete_forward delete_word_forward delete_word_backward delete_to_end_of_paragraph delete_to_beginning_of_line insert_newline insert_tab duplicate_line move_up move_up_and_modify_selection move_down move_down_and_modify_selection move_left move_left_and_modify_selection move_right move_right_and_modify_selection move_word_left move_word_left_and_modify_selection move_word_right move_word_right_and_modify_selection move_to_beginning_of_paragraph move_to_beginning_of_paragraph_and_modify_selection move_to_end_of_paragraph move_to_end_of_paragraph_and_modify_selection move_to_left_end_of_line move_to_left_end_of_line_and_modify_selection move_to_right_end_of_line move_to_right_end_of_line_and_modify_selection move_to_beginning_of_document move_to_beginning_of_document_and_modify_selection move_to_end_of_document move_to_end_of_document_and_modify_selection scroll_page_up page_up_and_modify_selection scroll_page_down page_down_and_modify_selection yank transpose select_all collapse_selections add_selection_above add_selection_below undo redo
Transformations
The following methods act by modifying the current selection.
uppercase lowercase capitalize indent outdent
Number Transformations
The following methods work with a caret or multiple selections. If the beginning of a selection (or the caret) is within a positive or negative number, the number will be transformed accordingly:
increase_number decrease_number
Recording
These methods allow manipulation and playback of event recordings.
- If there is no currently active recording, start recording events under the provided name.
- If there is no provided name, the current recording is saved.
- If the name provided matches the current recording name, the current recording is saved.
- If the name provided does not match the current recording name, the events for the current recording are dismissed.
toggle_recording { "recording_name"?: string }
Execute a set of recorded events and modify the document state:
play_recording { "recording_name": string }
Completely remove a specific recording:
clear_recording { "recording_name": string }
Language Support Oriented features (in Edit Namespace)
Hover
Get Hover for a position in file. The request for hover is made as a notification. The client is forwarded result back via a show_hover
rpc
If position is skipped in the request, current cursor position will be used in core.
request_hover { "request_id": number, "position"?: Position }
interface Position { line: number, column: number, }
Plugin namespace
Note: plugin commands are in flux, and may change.
Example: The following RPC dispatches the inner method to the plugin manager.
plugin {"method": "start", params: {"view_id": "view-id-1", plugin_name: "syntect"}}
Plugin methods
start
start {"view_id": "view-id-1", "plugin_name": "syntect"}
Starts the named plugin for the given view.
stop
stop {"view_id": "view-id-1", "plugin_name": "syntect"}
Stops the named plugin for the given view.
plugin_rpc
plugin_rpc {"view_id": "view-id-1", "receiver": "syntect", "notification": { "method": "custom_method", "params": {"foo": "bar"}, }}
Sends a custom rpc command to the named receiver. This may be a notification or a request.
Find and replace methods
find
find {"chars": "a", "case_sensitive": false, "regex": false, "whole_words": true}
Parameters regex
and whole_words
are optional and by default false
.
Sets the current search query and options.
multi_find
This find command supports multiple search queries.
multi_find [{"id": 1, "chars": "a", "case_sensitive": false, "regex": false, "whole_words": true}]
Parameters regex
and whole_words
are optional and by default false
. id
is an optional parameter used to uniquely identify a search query. If left empty, the query is considered as a new query and the backend will generate a new ID.
Sets the current search queries and options.
find_next and find_previous
find_next {"wrap_around": true, "allow_same": false, "modify_selection": "set"}
find_previous {"wrap_around": true, "allow_same": false, "modify_selection": "set"}
All parameters are optional. Boolean parameters are by default false
and modify_selection
is set
by default. If allow_same
is set to true
the current selection is considered a valid next occurrence. Supported options for modify_selection
are:
none
: the selection is not modifiedset
: the next/previous match will be set as the new selectionadd
: the next/previous match will be added to the current selectionadd_removing_current
: the previously added selection will be removed and the next/previous match will be added to the current selection
Selects the next/previous occurrence matching the search query.
find_all
find_all { }
Selects all occurrences matching the search query.
highlight_find
highlight_find {"visible": true}
Shows/hides active search highlights.
selection_for_find
selection_for_find {"case_sensitive": false}
The parameter case_sensitive
is optional and false
if not set.
Sets the current selection as the search query.
replace
replace {"chars": "a", "preserve_case": false}
The parameter preserve_case
is currently not implemented and ignored.
Sets the replacement string.
selection_for_replace
selection_for_replace {"case_sensitive": false}
The parameter case_sensitive
is optional and false
if not set.
Sets the current selection as the replacement string.
replace_next
replace_next { }
Replaces the next matching occurrence with the replacement string.
replace_all
replace_all { }
Replaces all matching occurrences with the replacement string.
selection_into_lines
selection_into_lines { }
Splits all current selections into lines.
From back-end to front-end
View update protocol
The following three methods are used to update the view’s contents. The design of the view update protocol, has a few particular goals in mind:
- Keep everything async.
- Keep network traffic minimal.
- Allow the front-end to retain as much information as possible (including text if only cursors are updated).
- Allow the front-end to use small amounts of memory even when document is large.
Conceptually, the core maintains a full view of the document, which can be considered an array of lines. Each line consists of the text (a string), a set of cursor locations, and a structure representing style information. Many operations update this view, at which point the core sends an update
notification to the front-end.
The front-end maintains a cache of this view. Some lines will be present, others will be missing. A cache is consistent with the true state when all present lines match.
To optimize communication, the core keeps some state about the client. One bit of this state is the scroll window; in general, the core tries to proactively update all lines within this window (plus a certain amount of slop on top and bottom). In addition, the core maintains a set of lines in the client’s cache. If a line changes, the update need only be communicated if it is in this set. This set is conservative; if a line is missing in the actual cache held by the front-end (evicted to save memory), no great harm is done updating it. The frontend reports this scroll window to the core by using the scroll
method of the edit
notification.
def_style
def_style id: number fg_color?: number // 32-bit ARGB (word-order) value bg_color?: number // 32-bit ARGB (word-order) value, default 0 weight?: number // 100..900, default 400 italic?: boolean // default false underline?: boolean // default false
(It’s not hard to imagine more style properties such as typeface, size, OpenType features, etc).
The guarantee on id
is that it is not currently in use in any lines in the view. However, in practice, it will probably just count up. It can also be assumed to be small, so using it as an index into a dense array is reasonable.
Deprecated: There are two reserved style IDs, so new style IDs will begin at 2. Style ID 0 is reserved for selections and ID 1 is reserved for find results. Reserved style IDs will be supported for backward compatibility for a limited time. Instead, selections and find matches are now represented as annotations
.
scroll_to
scroll_to: [number, number] // line, column (in utf-8 code units)
This notification indicates that the frontend should scroll its cursor to the given line and column.
update
update rev?: number ops: Op[] view-id: string pristine: bool annotations: AnnotationSlice[] interface Op { op: "copy" | "skip" | "invalidate" | "update" | "ins" n: number // number of lines affected lines?: Line[] // only present when op is "update" or "ins" ln?: number // the logical number for this line; null if this line is a soft break } interface AnnotationSlice { type: "find" | "selection" | ... ranges: [[number, number, number, number]] // start_line, start_col, end_line, end_col payloads: [{}] // can be any json object or value n: number // number of ranges }
The pristine
flag indicates whether or not, after this update, this document has unsaved changes.
The rev
field is not present in current builds, but will be at some point in the future.
An update request can be seen as a function from the old client cache state to a new one. During evaluation, maintain an index (old_ix
) into the old lines
array, initially 0, and a new lines array, initially empty. [Note that this document specifies the semantics. The actual implementation will almost certainly represent at least initial and trailing sequences of invalid lines by their count; and the editing operations may be more efficiently done in-place than by copying from the old state to the new].
The “copy” op appends the n
lines [old_ix .. old_ix + n]
to the new lines array, and increments old_ix
by n
. Additionally, “copy” includes the ln
field; this represents the new logical line number (that is, the ‘real’ line number, ignoring word wrap) of the first line to be copied. Note: if the first line to be copied is itself a wrapped line, the ln
number will need to be incremented in order to be correct for the first ‘real’ line.
The “skip” op increments old_ix
by n
.
The “invalidate” op appends n invalid lines to the new lines array.
The “ins” op appends new lines, specified by the “lines
” parameter, specified in more detail below. For this op, n
must equal lines.length
(alternative: make n optional in this case). It does not update old_ix
.
The “update” op updates the cursor and/or style of n existing lines. As in “ins”, n must equal lines.length. It also increments old_ix
by n
. If the update modifies the line numbers of the given n lines, the ln
parameter representing the new logical line number of the first line (as in the “copy” op) should be present.
In all cases, n is guaranteed positive and nonzero (as a consequence, any line present in the old state is copied at most once to the new state).
interface Line { text?: string // present when op is "update" ln?: number // the logical/'real' line number for this line. cursor?: number[] // utf-8 code point offsets, in increasing order styles?: number[] // length is a multiple of 3, see below }
The interpretation of a line is different for “update” or “ins” ops. In an “ins” op, text is always present, and missing cursor or styles properties are interpreted as empty (no cursors on that line, no styles).
In an “update” op, then the text property is absent from the line, and text is copied from the previous state (or left invalid if the previous state is invalid), and the cursor and styles are updated if present. To delete cursors from a line, the core sets the cursor property to the empty list.
The styles property represents style spans, in an efficient encoding. It is conceptually an array of triples (though flattened, so triple at is styles[i*3]
, styles[i*3 + 1]
, styles[i*3 + 2]
). The first element of the triple is the start index (in utf-8 code units), but encoded as a delta relative to the end of the last span (or relative to 0 for the first triple). It may be negative, if spans overlap. The second element is the length (in utf-8 code units). It is guaranteed nonzero and positive. The third element is a style id. The core guarantees that any style id sent in a styles property will have previously been set in a set_style request.
The number of lines in the new lines array always matches the view as maintained by the core. Another way of saying this is that adding all “n
” values except for “skip” operations is the number of lines. [Discussion: the last line always represents a partial line, so an empty document is one empty line. But I think the initial state should be the empty array. Then, the empty array represents the state that no updates have been processed].
interface Line { text?: string // present when op is "update" cursor?: number[] // utf-8 code point offsets, in increasing order styles?: number[] // length is a multiple of 3, see below }
“annotations” are used to associate some type data with some document regions. For example, annotations are used to represent selections and find highlights. The Annotations RFC provides a detailed description of the API.
measure_width
measure_width [{"id": number, "strings": string[]}] <- {"id":0, "result":[[28.0,8.0]]}
Asks the frontend to measure the display widths (the width when rendered and presented on screen) of a group of strings. The frontend should return an array of arrays, one for each item in the input array, containing the widths of each of that item’s strings when rendered with the style indicated by that items id argument.
These widths are used to determine how to calculate line breaks and other attributes that depend on the behaviour of the client’s text rendering system.
theme_changed
theme_changed {"name": "InspiredGitHub", "theme": Theme}
Notifies the client that the theme has been changed. The client should use the new theme to set colors as appropriate. The Theme
object is directly serialized from a syntect::highlighting::ThemeSettings
instance.
available_themes
available_themes {"themes": ["InspiredGitHub"]}
Notifies the client of the available themes.
language_changed
language_changed {"view_id": "view-id-1", "language_id": "Rust"}
Notifies the client that the language used for syntax highlighting has been changed.
available_languages
available_languages {"languages": ["Rust"]}
Notifies the client of the available languages.
config_changed
config_changed {"view_id": "view-id-1", "changes": {} }
Notifies the client that the config settings for a view have changed. This is called once when a new view is created, with changes
containing all config settings; afterwards changes
only contains the key/value pairs that have new values.
available_plugins
available_plugins {"view_id": "view-id-1", "plugins": [{"name": "syntect", "running": true]}
Notifies the client of the plugins available to the given view.
plugin_started
plugin_started {"view_id": "view-id-1", "plugin": "syntect"}
Notifies the client that the named plugin is running.
plugin_stopped
plugin_stopped {"view_id": "view-id-1", "plugin": "syntect", "code" 101}
Notifies the client that the named plugin has stopped. The code
field is an integer exit code; currently 0 indicates a user-initiated exit and 1 indicates an abnormal exit, i.e. a plugin crash.
update_cmds
update_cmds {"view_id": "view-id-1", "plugin", "syntect", "cmds": [Command]}
Notifies the client of a change in the available commands for a given plugin. The cmds
field is a list of all commands currently available to this plugin. Clients should store commands on a per-plugin basis; when the cmds
argument is an empty list it means that this plugin is providing no commands; any previously available commands should be disabled.
The format for describing a Command
is in flux. The best place to look for a working example is in the tests in core-lib/src/plugins/manifest.rs. As of this writing, the following is valid json for a Command
object:
{ "title": "Test Command", "description": "Passes the current test", "rpc_cmd": { "rpc_type": "notification", "method": "test.cmd", "params": { "view": "", "non_arg": "plugin supplied value", "arg_one": "", "arg_two": "" } }, "args": [ { "title": "First argument", "description": "Indicates something", "key": "arg_one", "arg_type": "Bool" }, { "title": "Favourite Number", "description": "A number used in a test.", "key": "arg_two", "arg_type": "Choice", "options": [ {"title": "Five", "value": 5}, {"title": "Ten", "value": 10} ] } ] }
update_spans
update_spans {"start": 0, "len": 20, "spans": [{ "start": 1, "end": 3, "scope_id": 4 }], "rev": 3 }
Updates existing scope spans starting at offset start
until offset len
.
update_annotations
update_annotations {"start": 0, "len": 20, "spans": [{ "start": 0, "end": 4, "data": null }], "annotation_type": "find", "rev": 3 }
Updates existing annotations and adds new annotations starting at offset start
until offset len
.
Language Support Specific Commands
Show Hover
show_hover { request_id: number, result: string }
Status Bar Commands
add_status_item
add_status_item { "source": "status_example", "key": "my_key", "value": "hello", "alignment": "left" }
Adds a status item, which will be displayed on the frontend’s status bar. Status items have a reference to whichever plugin added them. The alignment key dictates whether this item appears on the left side or the right side of the bar. This alignment can only be set when the item is added.
update_status_item
update_status_item { "key": "my_key", "value": "hello"}
Update a status item with the specified key with the new value.
remove_status_item
remove_status_item { "key": "my_key" }
Removes a status item from the front end.
Find and replace commands
find_status
Find supports multiple search queries.
find_status {"view_id": "view-id-1", "queries": [{"id": 1, "chars": "a", "case_sensitive": false, "is_regex": false, "whole_words": true, "matches": 6, "lines": [1, 3, 3, 6]}]}
Notifies the client about the current search queries and search options. lines
indicates for each match its line number.
replace_status
replace_status {"view_id": "view-id-1", "status": {"chars": "a", "preserve_case": false}}
Notifies the client about the current replacement string and replace options.
Other future extensions
Things the protocol will need to cover:
-
Dirty state (for visual indication and dialog on unsaved changes).
-
Minimal invalidation.
-
General configuration options (word wrap, etc).
-
Display of autocomplete options.
-
…