Merge pull request #9064 from bfredl/uidoc

UI: rename ext_newgrid to ext_linegrid and add --embed UI startup recommendations
This commit is contained in:
Björn Linse
2018-10-02 10:55:47 +02:00
committed by GitHub
15 changed files with 148 additions and 79 deletions

View File

@@ -358,6 +358,8 @@ argument.
instance a `nvim_get_api_info` call so that UI features can be
safely detected by the UI before attaching.
See |ui-startup| for more information about UI startup.
To embed nvim without using the UI protocol, `--headless` should
be supplied together with `--embed`. Then initialization is
performed without waiting for an UI. This is also equivalent

View File

@@ -16,10 +16,12 @@ RPC API. The UI model consists of a terminal-like grid with a single,
monospace font size. Some elements (UI "widgets") can be drawn separately from
the grid ("externalized").
*ui-options*
After connecting to Nvim (usually a spawned, embedded instance) use the
|nvim_ui_attach()| API method to tell Nvim that your program wants to draw the
Nvim screen grid with a size of width × height cells. `options` must be
The |nvim_ui_attach()| API method is used to tell Nvim that your program wants to
draw the Nvim screen grid with a size of width × height cells. This is typically
done by an embedder, see |ui-startup| below for details, but an UI can also
connect to a running nvim instance and invoke this method. `options` must be
a dictionary with these (optional) keys:
`rgb` Decides the color format. *ui-rgb*
Set true (default) for 24-bit RGB colors.
@@ -29,7 +31,7 @@ a dictionary with these (optional) keys:
`ext_tabline` Externalize the tabline. |ui-tabline|
`ext_cmdline` Externalize the cmdline. |ui-cmdline|
`ext_wildmenu` Externalize the wildmenu. |ui-wildmenu|
`ext_newgrid` Use new revision of the grid events. |ui-newgrid|
`ext_linegrid` Use new revision of the grid events. |ui-linegrid|
`ext_hlstate` Use detailed highlight state. |ui-hlstate|
Specifying a non-existent option is an error. UIs can check the |api-metadata|
@@ -43,22 +45,26 @@ Nvim sends msgpack-rpc notifications to all attached UIs, with method name
Each update event is itself an array whose first element is the event name and
remaining elements are event-parameter tuples. This allows multiple events of
the same kind to be sent in a row without the event name being repeated. This
batching is mostly used for "put", because each "put" event puts contents in
one screen cell, but clients must be prepared for multiple argument sets being
batched for all event kinds.
batching is mostly used for "grid_line", because each "grid_line" event puts
contents in one screen line, but clients must be prepared for multiple argument
sets being batched for all event kinds.
Events must be handled in-order. The user should only see the updated screen
state after all events in the same "redraw" batch are processed (not any
intermediate state after processing only part of the array).
Events must be handled in-order. A "flush" event is sent when nvim is done
redrawing the entire screen (so that all windows have a consistent view of
buffer state, options etc). Clients should be prepared that several "redraw"
batches are sent before the entire screen has been redrawn, and only the last
batch will end in "flush". The user should only see the final state when
"flush" is sent, and not any intermediate state after processing only part of
the batch array, nor after a batch not ending with "flush".
By default, Nvim sends |ui-global| and |ui-grid-old| events; these suffice to
implement a terminal-like interface. However there are two revisions of the
grid part of the protocol. The newer revision |ui-newgrid|, enabled by
`ext_newgrid` option, has some improvements, such as a more efficient
representation of highlighted text, simplified events and room for futher
enhancements that will use multiple grids. The older revision is available and
used by default only for backwards compatibility reasons. New UIs are strongly
recommended to use |ui-newgrid|, as further protocol extensions will require it.
grid part of the protocol. The newer revision |ui-linegrid|, enabled by
`ext_linegrid` option, has a more effecient representation of text (especially
highlighted text), and room for futher enhancements that will use
multiple grids. The older revision is available and used by default only for
backwards compatibility reasons. New UIs are strongly recommended to use
|ui-linegrid|, as further protocol extensions will require it.
Nvim optionally sends screen elements "semantically" as structured events
instead of raw grid-lines, controlled by |ui-ext-options|. The UI must present
@@ -68,9 +74,45 @@ Future versions of Nvim may add new update kinds and may append new parameters
to existing update kinds. Clients must be prepared to ignore such extensions,
for forward-compatibility. |api-contract|
==============================================================================
UI startup *ui-startup*
Nvim defines a standard procedure for how an embedding UI should interact with
the startup phase of Nvim. When spawning the nvim process, use the |--embed| flag
but not the |--headless| flag. The started Nvim process will pause before loading
startup files and reading buffers, and give the UI a chance to invoke requests
to do early initialization. As soon as the UI invokes |nvim_ui_attach()|, the
startup will continue.
A simple UI only need to do a single |nvim_ui_attach()| request and then
be prepared to handle any UI event. A more featureful UI, which might need
additional configuration of the nvim process, should use the following startup
procedure:
1. Invoke |nvim_get_api_info()|, if this is needed to setup the client library
and/or to get the list of supported UI extensions.
2. At this time, any configuration that should be happen before init.vim
loading should be done. Buffers and windows are not available at this
point, but this could be used to set |g:| variables visible to init.vim
3. If the UI wants to do additional setup after the init.vim file was loaded
register an autocmd for VimEnter at this point: >
nvim_command("autocmd VimEnter * call rpcrequest(1, 'vimenter')")
<4. Now invoke |nvim_ui_attach()|. The UI will need to handle keyboard input
at this point, as sourcing init.vim and loading buffers might lead to
blocking prompts.
5. If step 3 was used, nvim will send a blocking "vimenter" request to the
UI. Inside this request handler, the UI can safely do any initialization
before entering normal mode, for instance reading variables set by
init.vim.
==============================================================================
Global Events *ui-global*
The following events will always be available, and describe global state of
the editor.
["set_title", title]
["set_icon", icon]
Set the window title, and icon (minimized) window title, respectively.
@@ -144,15 +186,15 @@ Global Events *ui-global*
would conflict with other usages of the mouse. It is safe for a client
to ignore this and always send mouse events.
["busy_on"]
["busy_off"]
["busy_start"]
["busy_stop"]
Nvim started or stopped being busy, and possibly not responsive to
user input. This could be indicated to the user by hiding the cursor.
["suspend"]
|:suspend| command or |CTRL-Z| mapping is used. A terminal client (or other
client where it makes sense) could suspend itself. Other clients can
safely ignore it.
|:suspend| command or |CTRL-Z| mapping is used. A terminal client (or
another client where it makes sense) could suspend itself. Other
clients can safely ignore it.
["update_menu"]
The menu mappings changed.
@@ -161,16 +203,24 @@ Global Events *ui-global*
["visual_bell"]
Notify the user with an audible or visual bell, respectively.
==============================================================================
Grid Events (new revision) *ui-newgrid*
["flush"]
Nvim is done redrawing the screen. For an implementation that renders
to an internal buffer, this is the time to display the redrawn parts
to the user.
These events are used if `ext_newgrid` option is set (recommended for all new
UIs).
==============================================================================
Grid Events (line-based) *ui-linegrid*
These events are used if `ext_linegrid` option is set (recommended for all new
UIs). The biggest change compared to previous revision is to use a single
event `grid_line` to update the contents of a screen line (where the old
protocol used a combination of cursor, highlight and text events)
Most of these events take a `grid` index as first parameter. Grid 1 is the
global grid used by default for the entire editor screen state. Grids other
than that will be defined by future extensions. Just activating the `ext_newgrid`
option by itself will never cause any additional grids to be created.
than that will be defined by future extensions. Just activating the
`ext_linegrid` option by itself will never cause any additional grids to be
created.
Highlight attribute groups are predefined. UIs should maintain a table to map
numerical highlight `id`:s to the actual attributes.
@@ -299,10 +349,10 @@ numerical highlight `id`:s to the actual attributes.
from `set_scroll_region` which was end-inclusive.
==============================================================================
Grid Events (first revision) *ui-grid-old*
Legacy Grid Events (cell based) *ui-grid-old*
This is an older representation of the screen grid, used if `ext_newgrid`
option is not set.
This is an older representation of the screen grid, used if `ext_linegrid`
option is not set. New UIs should use |ui-linegrid|.
["resize", width, height]
The grid is resized to `width` and `height` cells.
@@ -389,9 +439,8 @@ option is not set.
==============================================================================
Detailed highlight state Extension *ui-hlstate*
Only sent if `ext_hlstate` option is set in |ui-options|. `ext_hlstate` implies
`ext_newgrid`.
`ext_linegrid`.
By default, nvim will only describe grid cells using the final calculated
higlight attributes, as described by the dict keys in |ui-event-highlight_set|.

View File

@@ -127,7 +127,7 @@ void nvim_ui_attach(uint64_t channel_id, Integer width, Integer height,
}
if (ui->ui_ext[kUIHlState]) {
ui->ui_ext[kUINewgrid] = true;
ui->ui_ext[kUILinegrid] = true;
}
UIData *data = xmalloc(sizeof(UIData));
@@ -227,11 +227,11 @@ static void ui_set_option(UI *ui, bool init, String name, Object value,
return;
}
bool boolval = value.data.boolean;
if (!init && i == kUINewgrid && boolval != ui->ui_ext[i]) {
if (!init && i == kUILinegrid && boolval != ui->ui_ext[i]) {
// There shouldn't be a reason for an UI to do this ever
// so explicitly don't support this.
api_set_error(error, kErrorTypeValidation,
"ext_newgrid option cannot be changed");
"ext_linegrid option cannot be changed");
}
ui->ui_ext[i] = boolval;
if (!init) {
@@ -271,10 +271,10 @@ static void push_call(UI *ui, const char *name, Array args)
static void remote_ui_grid_clear(UI *ui, Integer grid)
{
Array args = ARRAY_DICT_INIT;
if (ui->ui_ext[kUINewgrid]) {
if (ui->ui_ext[kUILinegrid]) {
ADD(args, INTEGER_OBJ(grid));
}
const char *name = ui->ui_ext[kUINewgrid] ? "grid_clear" : "clear";
const char *name = ui->ui_ext[kUILinegrid] ? "grid_clear" : "clear";
push_call(ui, name, args);
}
@@ -282,12 +282,12 @@ static void remote_ui_grid_resize(UI *ui, Integer grid,
Integer width, Integer height)
{
Array args = ARRAY_DICT_INIT;
if (ui->ui_ext[kUINewgrid]) {
if (ui->ui_ext[kUILinegrid]) {
ADD(args, INTEGER_OBJ(grid));
}
ADD(args, INTEGER_OBJ(width));
ADD(args, INTEGER_OBJ(height));
const char *name = ui->ui_ext[kUINewgrid] ? "grid_resize" : "resize";
const char *name = ui->ui_ext[kUILinegrid] ? "grid_resize" : "resize";
push_call(ui, name, args);
}
@@ -295,7 +295,7 @@ static void remote_ui_grid_scroll(UI *ui, Integer grid, Integer top,
Integer bot, Integer left, Integer right,
Integer rows, Integer cols)
{
if (ui->ui_ext[kUINewgrid]) {
if (ui->ui_ext[kUILinegrid]) {
Array args = ARRAY_DICT_INIT;
ADD(args, INTEGER_OBJ(grid));
ADD(args, INTEGER_OBJ(top));
@@ -341,7 +341,7 @@ static void remote_ui_default_colors_set(UI *ui, Integer rgb_fg,
push_call(ui, "default_colors_set", args);
// Deprecated
if (!ui->ui_ext[kUINewgrid]) {
if (!ui->ui_ext[kUILinegrid]) {
args = (Array)ARRAY_DICT_INIT;
ADD(args, INTEGER_OBJ(ui->rgb ? rgb_fg : cterm_fg - 1));
push_call(ui, "update_fg", args);
@@ -359,7 +359,7 @@ static void remote_ui_default_colors_set(UI *ui, Integer rgb_fg,
static void remote_ui_hl_attr_define(UI *ui, Integer id, HlAttrs rgb_attrs,
HlAttrs cterm_attrs, Array info)
{
if (!ui->ui_ext[kUINewgrid]) {
if (!ui->ui_ext[kUILinegrid]) {
return;
}
Array args = ARRAY_DICT_INIT;
@@ -397,7 +397,7 @@ static void remote_ui_highlight_set(UI *ui, int id)
static void remote_ui_grid_cursor_goto(UI *ui, Integer grid, Integer row,
Integer col)
{
if (ui->ui_ext[kUINewgrid]) {
if (ui->ui_ext[kUILinegrid]) {
Array args = ARRAY_DICT_INIT;
ADD(args, INTEGER_OBJ(grid));
ADD(args, INTEGER_OBJ(row));
@@ -442,7 +442,7 @@ static void remote_ui_raw_line(UI *ui, Integer grid, Integer row,
const sattr_T *attrs)
{
UIData *data = ui->data;
if (ui->ui_ext[kUINewgrid]) {
if (ui->ui_ext[kUILinegrid]) {
Array args = ARRAY_DICT_INIT;
ADD(args, INTEGER_OBJ(grid));
ADD(args, INTEGER_OBJ(row));
@@ -508,9 +508,10 @@ static void remote_ui_flush(UI *ui)
{
UIData *data = ui->data;
if (data->buffer.size > 0) {
if (!ui->ui_ext[kUINewgrid]) {
if (!ui->ui_ext[kUILinegrid]) {
remote_ui_cursor_goto(ui, data->cursor_row, data->cursor_col);
}
push_call(ui, "flush", (Array)ARRAY_DICT_INIT);
rpc_send_event(data->channel_id, "redraw", data->buffer);
data->buffer = (Array)ARRAY_DICT_INIT;
}
@@ -549,7 +550,7 @@ static Array translate_firstarg(UI *ui, Array args)
static void remote_ui_event(UI *ui, char *name, Array args, bool *args_consumed)
{
if (!ui->ui_ext[kUINewgrid]) {
if (!ui->ui_ext[kUILinegrid]) {
// the representation of highlights in cmdline changed, translate back
// never consumes args
if (strequal(name, "cmdline_show")) {

View File

@@ -63,7 +63,7 @@ void set_scroll_region(Integer top, Integer bot, Integer left, Integer right)
void scroll(Integer count)
FUNC_API_SINCE(3) FUNC_API_REMOTE_ONLY;
// Second revison of the grid protocol, used with ext_newgrid ui option
// Second revison of the grid protocol, used with ext_linegrid ui option
void default_colors_set(Integer rgb_fg, Integer rgb_bg, Integer rgb_sp,
Integer cterm_fg, Integer cterm_bg)
FUNC_API_SINCE(5) FUNC_API_REMOTE_IMPL;

View File

@@ -6884,7 +6884,7 @@ void do_highlight(const char *line, const bool forceit, const bool init)
// "fg", which have been changed now.
highlight_attr_set_all();
if (!ui_is_external(kUINewgrid) && starting == 0) {
if (!ui_is_external(kUILinegrid) && starting == 0) {
// Older UIs assume that we clear the screen after normal group is
// changed
ui_refresh();

View File

@@ -156,7 +156,7 @@ UI *tui_start(void)
ui->raw_line = tui_raw_line;
memset(ui->ui_ext, 0, sizeof(ui->ui_ext));
ui->ui_ext[kUINewgrid] = true;
ui->ui_ext[kUILinegrid] = true;
return ui_bridge_attach(ui, tui_main, tui_scheduler);
}

View File

@@ -15,7 +15,7 @@ typedef enum {
kUITabline,
kUIWildmenu,
#define kUIGlobalCount (kUIWildmenu+1)
kUINewgrid,
kUILinegrid,
kUIHlState,
kUIExtCount,
} UIExtension;
@@ -25,7 +25,7 @@ EXTERN const char *ui_ext_names[] INIT(= {
"ext_popupmenu",
"ext_tabline",
"ext_wildmenu",
"ext_newgrid",
"ext_linegrid",
"ext_hlstate",
});

View File

@@ -156,6 +156,6 @@ describe("ui_options in metadata", function()
local api = helpers.call('api_info')
local options = api.ui_options
eq({'rgb', 'ext_cmdline', 'ext_popupmenu',
'ext_tabline', 'ext_wildmenu', 'ext_newgrid', 'ext_hlstate'}, options)
'ext_tabline', 'ext_wildmenu', 'ext_linegrid', 'ext_hlstate'}, options)
end)
end)

View File

@@ -1254,7 +1254,7 @@ describe('API', function()
ext_popupmenu = false,
ext_tabline = false,
ext_wildmenu = false,
ext_newgrid = screen._options.ext_newgrid or false,
ext_linegrid = screen._options.ext_linegrid or false,
ext_hlstate=false,
height = 4,
rgb = true,

View File

@@ -258,10 +258,10 @@ describe('tui', function()
feed_data(':echo map(nvim_list_uis(), {k,v -> sort(items(v))})\013')
screen:expect([=[
[[['ext_cmdline', v:false], ['ext_hlstate', v:fals|
e], ['ext_newgrid', v:true], ['ext_popupmenu', v:f|
alse], ['ext_tabline', v:false], ['ext_wildmenu', |
v:false], ['height', 6], ['rgb', v:false], ['width|
', 50]]] |
e], ['ext_linegrid', v:true], ['ext_popupmenu', v:|
false], ['ext_tabline', v:false], ['ext_wildmenu',|
v:false], ['height', 6], ['rgb', v:false], ['widt|
h', 50]]] |
{10:Press ENTER or type command to continue}{1: } |
{3:-- TERMINAL --} |
]=])

View File

@@ -4,13 +4,13 @@ local clear, feed = helpers.clear, helpers.feed
local source = helpers.source
local command = helpers.command
local function test_cmdline(newgrid)
local function test_cmdline(linegrid)
local screen
before_each(function()
clear()
screen = Screen.new(25, 5)
screen:attach({rgb=true, ext_cmdline=true, ext_newgrid=newgrid})
screen:attach({rgb=true, ext_cmdline=true, ext_linegrid=linegrid})
screen:set_default_attr_ids({
[1] = {bold = true, foreground = Screen.colors.Blue1},
[2] = {reverse = true},
@@ -608,7 +608,7 @@ local function test_cmdline(newgrid)
end)
end
-- the representation of cmdline and cmdline_block contents changed with ext_newgrid
-- the representation of cmdline and cmdline_block contents changed with ext_linegrid
-- (which uses indexed highlights) so make sure to test both
describe('ui/ext_cmdline', function() test_cmdline(true) end)
describe('ui/ext_cmdline (legacy highlights)', function() test_cmdline(false) end)

View File

@@ -5,14 +5,14 @@ local feed = helpers.feed
local eq = helpers.eq
local clear = helpers.clear
local function test_embed(ext_newgrid)
local function test_embed(ext_linegrid)
local screen
local function startup(...)
clear{headless=false, args={...}}
-- attach immediately after startup, for early UI
screen = Screen.new(60, 8)
screen:attach{ext_newgrid=ext_newgrid}
screen:attach{ext_linegrid=ext_linegrid}
screen:set_default_attr_ids({
[1] = {foreground = Screen.colors.Grey100, background = Screen.colors.Red},
[2] = {bold = true, foreground = Screen.colors.SeaGreen4},
@@ -77,5 +77,5 @@ local function test_embed(ext_newgrid)
end)
end
describe('--embed UI on startup (ext_newgrid=true)', function() test_embed(true) end)
describe('--embed UI on startup (ext_newgrid=false)', function() test_embed(false) end)
describe('--embed UI on startup (ext_linegrid=true)', function() test_embed(true) end)
describe('--embed UI on startup (ext_linegrid=false)', function() test_embed(false) end)

View File

@@ -30,15 +30,15 @@ describe('ui receives option updates', function()
ext_popupmenu=false,
ext_tabline=false,
ext_wildmenu=false,
ext_newgrid=false,
ext_linegrid=false,
ext_hlstate=false,
}
it("for defaults", function()
screen:attach()
-- NB: UI test suite can be run in both "newgrid" and legacy grid mode.
-- NB: UI test suite can be run in both "linegrid" and legacy grid mode.
-- In both cases check that the received value is the one requested.
defaults.ext_newgrid = screen._options.ext_newgrid or false
defaults.ext_linegrid = screen._options.ext_linegrid or false
screen:expect(function()
eq(defaults, screen.options)
end)
@@ -46,7 +46,7 @@ describe('ui receives option updates', function()
it("when setting options", function()
screen:attach()
defaults.ext_newgrid = screen._options.ext_newgrid or false
defaults.ext_linegrid = screen._options.ext_linegrid or false
local changed = {}
for k,v in pairs(defaults) do
changed[k] = v
@@ -95,7 +95,7 @@ describe('ui receives option updates', function()
end
screen:attach({ext_cmdline=true, ext_wildmenu=true})
defaults.ext_newgrid = screen._options.ext_newgrid or false
defaults.ext_linegrid = screen._options.ext_linegrid or false
changed.ext_cmdline = true
changed.ext_wildmenu = true
screen:expect(function()

View File

@@ -185,11 +185,11 @@ function Screen:attach(options)
if options == nil then
options = {}
end
if options.ext_newgrid == nil then
options.ext_newgrid = true
if options.ext_linegrid == nil then
options.ext_linegrid = true
end
self._options = options
self._clear_attrs = (options.ext_newgrid and {{},{}}) or {}
self._clear_attrs = (options.ext_linegrid and {{},{}}) or {}
uimeths.attach(self._width, self._height, options)
if self._options.rgb == nil then
-- nvim defaults to rgb=true internally,
@@ -386,9 +386,13 @@ function Screen:wait(check, timeout)
local err, checked = false
local success_seen = false
local failure_after_success = false
local did_flush = true
local function notification_cb(method, args)
assert(method == 'redraw')
self:_redraw(args)
did_flush = self:_redraw(args)
if not did_flush then
return
end
err = check()
checked = true
if not err then
@@ -402,7 +406,9 @@ function Screen:wait(check, timeout)
return true
end
run(nil, notification_cb, nil, timeout or self.timeout)
if not checked then
if not did_flush then
err = "no flush received"
elseif not checked then
err = check()
end
@@ -431,7 +437,8 @@ function Screen:sleep(ms)
end
function Screen:_redraw(updates)
for _, update in ipairs(updates) do
local did_flush = false
for k, update in ipairs(updates) do
-- print('--')
-- print(require('inspect')(update))
local method = update[1]
@@ -446,7 +453,11 @@ function Screen:_redraw(updates)
self._on_event(method, update[i])
end
end
if k == #updates and method == "flush" then
did_flush = true
end
end
return did_flush
end
function Screen:set_on_event_handler(callback)
@@ -472,6 +483,10 @@ function Screen:_handle_resize(width, height)
}
end
function Screen:_handle_flush()
end
function Screen:_handle_grid_resize(grid, width, height)
assert(grid == 1)
self:_handle_resize(width, height)
@@ -609,6 +624,7 @@ function Screen:_handle_highlight_set(attrs)
end
function Screen:_handle_put(str)
assert(not self._options.ext_linegrid)
local cell = self._rows[self._cursor.row][self._cursor.col]
cell.text = str
cell.attrs = self._attrs
@@ -617,6 +633,7 @@ function Screen:_handle_put(str)
end
function Screen:_handle_grid_line(grid, row, col, items)
assert(self._options.ext_linegrid)
assert(grid == 1)
local line = self._rows[row+1]
local colpos = col+1
@@ -764,7 +781,7 @@ function Screen:_row_repr(row, attr_state)
local current_attr_id
for i = 1, self._width do
local attrs = row[i].attrs
if self._options.ext_newgrid then
if self._options.ext_linegrid then
attrs = attrs[(self._options.rgb and 1) or 2]
end
local attr_id = self:_get_attr_id(attr_state, attrs, row[i].hl_id)
@@ -820,7 +837,7 @@ function Screen:_chunks_repr(chunks, attr_state)
for i, chunk in ipairs(chunks) do
local hl, text = unpack(chunk)
local attrs
if self._options.ext_newgrid then
if self._options.ext_linegrid then
attrs = self._attr_table[hl][1]
else
attrs = hl

View File

@@ -48,13 +48,13 @@ describe('screen', function()
end)
end)
local function screen_tests(newgrid)
local function screen_tests(linegrid)
local screen
before_each(function()
clear()
screen = Screen.new()
screen:attach({rgb=true,ext_newgrid=newgrid})
screen:attach({rgb=true,ext_linegrid=linegrid})
screen:set_default_attr_ids( {
[0] = {bold=true, foreground=255},
[1] = {bold=true, reverse=true},