diff options
-rw-r--r-- | runtime/doc/msgpack_rpc.txt | 166 | ||||
-rw-r--r-- | src/nvim/api/ui.c | 112 | ||||
-rw-r--r-- | src/nvim/edit.c | 6 | ||||
-rw-r--r-- | src/nvim/popupmnu.c | 109 | ||||
-rw-r--r-- | src/nvim/screen.c | 33 | ||||
-rw-r--r-- | src/nvim/tui/tui.c | 8 | ||||
-rw-r--r-- | src/nvim/ui.c | 15 | ||||
-rw-r--r-- | src/nvim/ui.h | 5 | ||||
-rw-r--r-- | src/nvim/ui_bridge.c | 1 | ||||
-rw-r--r-- | test/functional/autocmd/termclose_spec.lua | 2 | ||||
-rw-r--r-- | test/functional/helpers.lua | 6 | ||||
-rw-r--r-- | test/functional/terminal/cursor_spec.lua | 2 | ||||
-rw-r--r-- | test/functional/terminal/edit_spec.lua | 2 | ||||
-rw-r--r-- | test/functional/terminal/ex_terminal_spec.lua | 2 | ||||
-rw-r--r-- | test/functional/terminal/helpers.lua | 2 | ||||
-rw-r--r-- | test/functional/terminal/highlight_spec.lua | 10 | ||||
-rw-r--r-- | test/functional/terminal/scrollback_spec.lua | 2 | ||||
-rw-r--r-- | test/functional/ui/screen.lua | 32 | ||||
-rw-r--r-- | test/functional/viml/completion_spec.lua | 102 |
19 files changed, 535 insertions, 82 deletions
diff --git a/runtime/doc/msgpack_rpc.txt b/runtime/doc/msgpack_rpc.txt index 18c0ff8a58..30f68ca942 100644 --- a/runtime/doc/msgpack_rpc.txt +++ b/runtime/doc/msgpack_rpc.txt @@ -11,6 +11,7 @@ RPC API for Nvim *RPC* *rpc* *msgpack-rpc* 3. Connecting |rpc-connecting| 4. Clients |rpc-api-client| 5. Types |rpc-types| +6. Remote UIs |rpc-remote-ui| ============================================================================== 1. Introduction *rpc-intro* @@ -238,4 +239,169 @@ the type codes, because a client may be built against one Nvim version but connect to another with different type codes. ============================================================================== +6. Remote UIs *rpc-remote-ui* + +Nvim allows Graphical user interfaces to be implemented by separate processes +communicating with Nvim over the RPC API. Currently the ui model conists of a +terminal-like grid with one single, monospace font size, with a few elements +that could be drawn separately from the grid (for the momemnt only the popup +menu) + +After connecting to a nvim instance (typically a spawned, embedded instance) +use the |nvim_ui_attach|(width, height, options) API method to tell nvim that your +program wants to draw the nvim screen on a grid with "width" times +"height" cells. "options" should be a dictionary with the following (all +optional) keys: + `rgb`: Controls what color format to use. + Set to true (default) to use 24-bit rgb + colors. + Set to false to use terminal color codes (at + most 256 different colors). + `popupmenu_external`: Instead of drawing the completion popupmenu on + the grid, Nvim will send higher-level events to + the ui and let it draw the popupmenu. + Defaults to false. + +Nvim will then send msgpack-rpc notifications, with the method name "redraw" +and a single argument, an array of screen updates (described below). +These should be processed in order. Preferably the user should only be able to +see the screen state after all updates are processed (not any intermediate +state after processing only a part of the array). + +Screen updates are arrays. The first element a string describing the kind +of update. + +["resize", width, height] + The grid is resized to `width` and `height` cells. + +["clear"] + Clear the screen. + +["eol_clear"] + Clear from the cursor position to the end of the current line. + +["cursor_goto", row, col] + Move the cursor to position (row, col). Currently, the same cursor is + used to define the position for text insertion and the visible cursor. + However, only the last cursor position, after processing the entire + array in the "redraw" event, is intended to be a visible cursor + position. + +["update_fg", color] +["update_bg", color] +["update_sp", color] + Set the default foreground, background and special colors + respectively. + +["highlight_set", attrs] + Set the attributes that the next text put on the screen will have. + `attrs` is a dict with the keys below. Any absent key is reset + to its default value. Color defaults are set by the `update_fg` etc + updates. All boolean keys default to false. + + `foreground`: foreground color. + `background`: backround color. + `special`: color to use for underline and undercurl, when present. + `reverse`: reverse video. Foreground and background colors are + switched. + `italic`: italic text. + `bold`: bold text. + `underline`: underlined text. The line has `special` color. + `undercurl`: undercurled text. The curl has `special` color. + +["put", text] + The (utf-8 encoded) string `text` is put at the cursor position + (and the cursor is advanced), with the highlights as set by the + last `highlight_set` update. + +["set_scroll_region", top, bot, left, right] + Define the scroll region used by `scroll` below. + +["scroll", count] + Scroll the text in the scroll region. The diagrams below illustrate + what will happen, depending on the scroll direction. "=" is used to + represent the SR(scroll region) boundaries and "-" the moved rectangles. + Note that dst and src share a common region. + + If count is bigger than 0, move a rectangle in the SR up, this can + happen while scrolling down. +> + +-------------------------+ + | (clipped above SR) | ^ + |=========================| dst_top | + | dst (still in SR) | | + +-------------------------+ src_top | + | src (moved up) and dst | | + |-------------------------| dst_bot | + | src (cleared) | | + +=========================+ src_bot +< + If count is less than zero, move a rectangle in the SR down, this can + happen while scrolling up. +> + +=========================+ src_top + | src (cleared) | | + |------------------------ | dst_top | + | src (moved down) and dst| | + +-------------------------+ src_bot | + | dst (still in SR) | | + |=========================| dst_bot | + | (clipped below SR) | v + +-------------------------+ +< +["set_title", title] +["set_icon", icon] + Set the window title, and icon (minimized) window title, respectively. + In windowing systems not distinguishing between the two, "set_icon" + can be ignored. + +["mouse_on"] +["mouse_off"] + Tells the client whether mouse support, as determined by |'mouse'| + option, is considered to be active in the current mode. This is mostly + useful for a terminal frontend, or other situations where nvim mouse + 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"] + Nvim started or stopped being busy, and possibly not responsible 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. + +["bell"] +["visual_bell"] + Notify the user with an audible or visual bell, respectively. + +["update_menu"] + The menu mappings changed. + +["mode_change", mode] + The mode changed. Currently sent when "insert", "replace" and "normal" + modes are entered. A client could for instance change the cursor shape. + +["popupmenu_show", items, selected, row, col] + When `popupmenu_external` is set to true, nvim will not draw the + popupmenu on the grid, instead when the popupmenu is to be displayed + this update is sent. `items` is an array of the items to show, the + items are themselves arrays of the form [word, kind, menu, info] + as defined at |complete-items|, except that `word` is replaced by + `abbr` if present. `selected` is the initially selected item, either a + zero-based index into the array of items, or -1 if no item is + selected. `row` and `col` is the anchor position, where the first + character of the completed word will be. + +["popupmenu_select", selected] + An item in the currently displayed popupmenu is selected. `selected` + is either a zero-based index into the array of items from the last + `popupmenu_show` event, or -1 if no item is selected. + +["popupmenu_hide"] + The popupmenu is hidden. + +============================================================================== vim:tw=78:ts=8:noet:ft=help:norl: diff --git a/src/nvim/api/ui.c b/src/nvim/api/ui.c index 1703d49296..2b131443d3 100644 --- a/src/nvim/api/ui.c +++ b/src/nvim/api/ui.c @@ -8,8 +8,10 @@ #include "nvim/memory.h" #include "nvim/map.h" #include "nvim/msgpack_rpc/channel.h" +#include "nvim/api/ui.h" #include "nvim/api/private/defs.h" #include "nvim/api/private/helpers.h" +#include "nvim/popupmnu.h" #ifdef INCLUDE_GENERATED_DECLARATIONS # include "api/ui.c.generated.h" @@ -44,8 +46,8 @@ void remote_ui_disconnect(uint64_t channel_id) xfree(ui); } -void ui_attach(uint64_t channel_id, Integer width, Integer height, - Boolean enable_rgb, Error *err) +void nvim_ui_attach(uint64_t channel_id, Integer width, Integer height, + Dictionary options, Error *err) { if (pmap_has(uint64_t)(connected_uis, channel_id)) { api_set_error(err, Exception, _("UI already attached for channel")); @@ -57,14 +59,11 @@ void ui_attach(uint64_t channel_id, Integer width, Integer height, _("Expected width > 0 and height > 0")); return; } - UIData *data = xmalloc(sizeof(UIData)); - data->channel_id = channel_id; - data->buffer = (Array)ARRAY_DICT_INIT; UI *ui = xcalloc(1, sizeof(UI)); ui->width = (int)width; ui->height = (int)height; - ui->rgb = enable_rgb; - ui->data = data; + ui->rgb = true; + ui->pum_external = false; ui->resize = remote_ui_resize; ui->clear = remote_ui_clear; ui->eol_clear = remote_ui_eol_clear; @@ -88,37 +87,108 @@ void ui_attach(uint64_t channel_id, Integer width, Integer height, ui->suspend = remote_ui_suspend; ui->set_title = remote_ui_set_title; ui->set_icon = remote_ui_set_icon; + ui->event = remote_ui_event; + + for (size_t i = 0; i < options.size; i++) { + ui_set_option(ui, options.items[i].key, options.items[i].value, err); + if (err->set) { + xfree(ui); + return; + } + } + + UIData *data = xmalloc(sizeof(UIData)); + data->channel_id = channel_id; + data->buffer = (Array)ARRAY_DICT_INIT; + ui->data = data; + pmap_put(uint64_t)(connected_uis, channel_id, ui); ui_attach_impl(ui); - return; } -void ui_detach(uint64_t channel_id, Error *err) +/// @deprecated +void ui_attach(uint64_t channel_id, Integer width, Integer height, + Boolean enable_rgb, Error *err) +{ + Dictionary opts = ARRAY_DICT_INIT; + PUT(opts, "rgb", BOOLEAN_OBJ(enable_rgb)); + nvim_ui_attach(channel_id, width, height, opts, err); + api_free_dictionary(opts); +} + +void nvim_ui_detach(uint64_t channel_id, Error *err) { if (!pmap_has(uint64_t)(connected_uis, channel_id)) { api_set_error(err, Exception, _("UI is not attached for channel")); + return; } remote_ui_disconnect(channel_id); } -Object ui_try_resize(uint64_t channel_id, Integer width, - Integer height, Error *err) +/// @deprecated +void ui_detach(uint64_t channel_id, Error *err) +{ + nvim_ui_detach(channel_id, err); +} + +void nvim_ui_try_resize(uint64_t channel_id, Integer width, + Integer height, Error *err) { if (!pmap_has(uint64_t)(connected_uis, channel_id)) { api_set_error(err, Exception, _("UI is not attached for channel")); + return; } if (width <= 0 || height <= 0) { api_set_error(err, Validation, _("Expected width > 0 and height > 0")); - return NIL; + return; } UI *ui = pmap_get(uint64_t)(connected_uis, channel_id); ui->width = (int)width; ui->height = (int)height; ui_refresh(); - return NIL; +} + +/// @deprecated +void ui_try_resize(uint64_t channel_id, Integer width, + Integer height, Error *err) +{ + nvim_ui_try_resize(channel_id, width, height, err); +} + +void nvim_ui_set_option(uint64_t channel_id, String name, + Object value, Error *error) { + if (!pmap_has(uint64_t)(connected_uis, channel_id)) { + api_set_error(error, Exception, _("UI is not attached for channel")); + return; + } + UI *ui = pmap_get(uint64_t)(connected_uis, channel_id); + + ui_set_option(ui, name, value, error); + if (!error->set) { + ui_refresh(); + } +} + +static void ui_set_option(UI *ui, String name, Object value, Error *error) { + if (strcmp(name.data, "rgb") == 0) { + if (value.type != kObjectTypeBoolean) { + api_set_error(error, Validation, _("rgb must be a Boolean")); + return; + } + ui->rgb = value.data.boolean; + } else if (strcmp(name.data, "popupmenu_external") == 0) { + if (value.type != kObjectTypeBoolean) { + api_set_error(error, Validation, + _("popupmenu_external must be a Boolean")); + return; + } + ui->pum_external = value.data.boolean; + } else { + api_set_error(error, Validation, _("No such ui option")); + } } static void push_call(UI *ui, char *name, Array args) @@ -341,3 +411,19 @@ static void remote_ui_set_icon(UI *ui, char *icon) ADD(args, STRING_OBJ(cstr_to_string(icon))); push_call(ui, "set_icon", args); } + +static void remote_ui_event(UI *ui, char *name, Array args, bool *args_consumed) +{ + Array my_args = ARRAY_DICT_INIT; + // Objects are currently single-reference + // make a copy, but only if necessary + if (*args_consumed) { + for (size_t i = 0; i < args.size; i++) { + ADD(my_args, copy_object(args.items[i])); + } + } else { + my_args = args; + *args_consumed = true; + } + push_call(ui, name, my_args); +} diff --git a/src/nvim/edit.c b/src/nvim/edit.c index a71104cfb6..af5ec6c5ac 100644 --- a/src/nvim/edit.c +++ b/src/nvim/edit.c @@ -2472,6 +2472,7 @@ void ins_compl_show_pum(void) int cur = -1; colnr_T col; int lead_len = 0; + bool array_changed = false; if (!pum_wanted() || !pum_enough_matches()) return; @@ -2483,7 +2484,8 @@ void ins_compl_show_pum(void) update_screen(0); if (compl_match_array == NULL) { - /* Need to build the popup menu list. */ + array_changed = true; + // Need to build the popup menu list. compl_match_arraysize = 0; compl = compl_first_match; /* @@ -2586,7 +2588,7 @@ void ins_compl_show_pum(void) // Use the cursor to get all wrapping and other settings right. col = curwin->w_cursor.col; curwin->w_cursor.col = compl_col; - pum_display(compl_match_array, compl_match_arraysize, cur); + pum_display(compl_match_array, compl_match_arraysize, cur, array_changed); curwin->w_cursor.col = col; } diff --git a/src/nvim/popupmnu.c b/src/nvim/popupmnu.c index 5ad621e666..328e913e0e 100644 --- a/src/nvim/popupmnu.c +++ b/src/nvim/popupmnu.c @@ -7,6 +7,7 @@ #include <stdbool.h> #include "nvim/vim.h" +#include "nvim/api/private/helpers.h" #include "nvim/ascii.h" #include "nvim/popupmnu.h" #include "nvim/charset.h" @@ -21,6 +22,7 @@ #include "nvim/memory.h" #include "nvim/window.h" #include "nvim/edit.h" +#include "nvim/ui.h" static pumitem_T *pum_array = NULL; // items of displayed pum static int pum_size; // nr of items in "pum_array" @@ -36,8 +38,10 @@ static int pum_scrollbar; // TRUE when scrollbar present static int pum_row; // top row of pum static int pum_col; // left column of pum -static int pum_do_redraw = FALSE; // do redraw anyway +static bool pum_is_visible = false; +static bool pum_external = false; +static bool pum_wants_external = false; #ifdef INCLUDE_GENERATED_DECLARATIONS # include "popupmnu.c.generated.h" @@ -53,7 +57,10 @@ static int pum_do_redraw = FALSE; // do redraw anyway /// @param array /// @param size /// @param selected index of initially selected item, none if out of range -void pum_display(pumitem_T *array, int size, int selected) +/// @param array_changed if true, array contains different items since last call +/// if false, a new item is selected, but the array +/// is the same +void pum_display(pumitem_T *array, int size, int selected, bool array_changed) { int w; int def_width; @@ -68,20 +75,55 @@ void pum_display(pumitem_T *array, int size, int selected) int above_row = cmdline_row; int redo_count = 0; + if (!pum_is_visible) { + // To keep the code simple, we only allow changing the + // draw mode when the popup menu is not being displayed + pum_external = pum_wants_external; + } + redo: + // Mark the pum as visible already here, + // to avoid that must_redraw is set when 'cursorcolumn' is on. + pum_is_visible = true; + validate_cursor_col(); + + // anchor position: the start of the completed word + row = curwin->w_wrow + curwin->w_winrow; + if (curwin->w_p_rl) { + col = curwin->w_wincol + curwin->w_width - curwin->w_wcol - 1; + } else { + col = curwin->w_wincol + curwin->w_wcol; + } + + if (pum_external) { + Array args = ARRAY_DICT_INIT; + if (array_changed) { + Array arr = ARRAY_DICT_INIT; + for (i = 0; i < size; i++) { + Array item = ARRAY_DICT_INIT; + ADD(item, STRING_OBJ(cstr_to_string((char *)array[i].pum_text))); + ADD(item, STRING_OBJ(cstr_to_string((char *)array[i].pum_kind))); + ADD(item, STRING_OBJ(cstr_to_string((char *)array[i].pum_extra))); + ADD(item, STRING_OBJ(cstr_to_string((char *)array[i].pum_info))); + ADD(arr, ARRAY_OBJ(item)); + } + ADD(args, ARRAY_OBJ(arr)); + ADD(args, INTEGER_OBJ(selected)); + ADD(args, INTEGER_OBJ(row)); + ADD(args, INTEGER_OBJ(col)); + ui_event("popupmenu_show", args); + } else { + ADD(args, INTEGER_OBJ(selected)); + ui_event("popupmenu_select", args); + } + return; + } + def_width = PUM_DEF_WIDTH; max_width = 0; kind_width = 0; extra_width = 0; - // Pretend the pum is already there to avoid that must_redraw is set when - // 'cuc' is on. - pum_array = (pumitem_T *)1; - validate_cursor_col(); - pum_array = NULL; - - row = curwin->w_wrow + curwin->w_winrow; - if (firstwin->w_p_pvw) { top_clear = firstwin->w_height; } else { @@ -194,13 +236,6 @@ redo: pum_base_width = max_width; pum_kind_width = kind_width; - // Calculate column - if (curwin->w_p_rl) { - col = curwin->w_wincol + curwin->w_width - curwin->w_wcol - 1; - } else { - col = curwin->w_wincol + curwin->w_wcol; - } - // if there are more items than room we need a scrollbar if (pum_height < size) { pum_scrollbar = 1; @@ -641,9 +676,9 @@ static int pum_set_selected(int n, int repeat) // Update the screen before drawing the popup menu. // Enable updating the status lines. - pum_do_redraw = TRUE; + pum_is_visible = false; update_screen(0); - pum_do_redraw = FALSE; + pum_is_visible = true; if (!resized && win_valid(curwin_save)) { no_u_sync++; @@ -653,9 +688,9 @@ static int pum_set_selected(int n, int repeat) // May need to update the screen again when there are // autocommands involved. - pum_do_redraw = TRUE; + pum_is_visible = false; update_screen(0); - pum_do_redraw = FALSE; + pum_is_visible = true; } } } @@ -672,10 +707,17 @@ static int pum_set_selected(int n, int repeat) /// Undisplay the popup menu (later). void pum_undisplay(void) { + pum_is_visible = false; pum_array = NULL; - redraw_all_later(SOME_VALID); - redraw_tabline = TRUE; - status_redraw_all(); + + if (pum_external) { + Array args = ARRAY_DICT_INIT; + ui_event("popupmenu_hide", args); + } else { + redraw_all_later(SOME_VALID); + redraw_tabline = true; + status_redraw_all(); + } } /// Clear the popup menu. Currently only resets the offset to the first @@ -685,12 +727,16 @@ void pum_clear(void) pum_first = 0; } -/// Overruled when "pum_do_redraw" is set, used to redraw the status lines. -/// -/// @return TRUE if the popup menu is displayed. -int pum_visible(void) +/// @return true if the popup menu is displayed. +bool pum_visible(void) { - return !pum_do_redraw && pum_array != NULL; + return pum_is_visible; +} + +/// @return true if the popup menu is displayed and drawn on the grid. +bool pum_drawn(void) +{ + return pum_visible() && !pum_external; } /// Gets the height of the menu. @@ -701,3 +747,8 @@ int pum_get_height(void) { return pum_height; } + +void pum_set_external(bool external) +{ + pum_wants_external = external; +} diff --git a/src/nvim/screen.c b/src/nvim/screen.c index 61c88b5875..bb89e49e5f 100644 --- a/src/nvim/screen.c +++ b/src/nvim/screen.c @@ -420,9 +420,10 @@ void update_screen(int type) } } end_search_hl(); - /* May need to redraw the popup menu. */ - if (pum_visible()) + // May need to redraw the popup menu. + if (pum_drawn()) { pum_redraw(); + } /* Reset b_mod_set flags. Going through all windows is probably faster * than going through all buffers (there could be many buffers). */ @@ -4827,15 +4828,12 @@ void win_redr_status(win_T *wp) wp->w_redr_status = FALSE; if (wp->w_status_height == 0) { - /* no status line, can only be last window */ - redraw_cmdline = TRUE; - } else if (!redrawing() - /* don't update status line when popup menu is visible and may be - * drawn over it */ - || pum_visible() - ) { - /* Don't redraw right now, do it later. */ - wp->w_redr_status = TRUE; + // no status line, can only be last window + redraw_cmdline = true; + } else if (!redrawing() || pum_drawn()) { + // Don't redraw right now, do it later. Don't update status line when + // popup menu is visible and may be drawn over it + wp->w_redr_status = true; } else if (*p_stl != NUL || *wp->w_p_stl != NUL) { /* redraw custom status line */ redraw_custom_statusline(wp); @@ -7081,9 +7079,9 @@ void showruler(int always) { if (!always && !redrawing()) return; - if (pum_visible()) { - /* Don't redraw right now, do it later. */ - curwin->w_redr_status = TRUE; + if (pum_drawn()) { + // Don't redraw right now, do it later. + curwin->w_redr_status = true; return; } if ((*p_stl != NUL || *curwin->w_p_stl != NUL) && curwin->w_status_height) { @@ -7119,9 +7117,10 @@ static void win_redr_ruler(win_T *wp, int always) if (wp == lastwin && lastwin->w_status_height == 0) if (edit_submode != NULL) return; - /* Don't draw the ruler when the popup menu is visible, it may overlap. */ - if (pum_visible()) + // Don't draw the ruler when the popup menu is visible, it may overlap. + if (pum_drawn()) { return; + } if (*p_ruf) { int save_called_emsg = called_emsg; @@ -7371,7 +7370,7 @@ void screen_resize(int width, int height) redrawcmdline(); } else { update_topline(); - if (pum_visible()) { + if (pum_drawn()) { redraw_later(NOT_VALID); ins_compl_show_pum(); /* This includes the redraw. */ } else diff --git a/src/nvim/tui/tui.c b/src/nvim/tui/tui.c index d220df508a..bfc03dfb81 100644 --- a/src/nvim/tui/tui.c +++ b/src/nvim/tui/tui.c @@ -83,6 +83,7 @@ UI *tui_start(void) UI *ui = xcalloc(1, sizeof(UI)); ui->stop = tui_stop; ui->rgb = p_tgc; + ui->pum_external = false; ui->resize = tui_resize; ui->clear = tui_clear; ui->eol_clear = tui_eol_clear; @@ -106,6 +107,7 @@ UI *tui_start(void) ui->suspend = tui_suspend; ui->set_title = tui_set_title; ui->set_icon = tui_set_icon; + ui->event = tui_event; return ui_bridge_attach(ui, tui_main, tui_scheduler); } @@ -650,6 +652,12 @@ static void tui_set_icon(UI *ui, char *icon) { } +// NB: if we start to use this, the ui_bridge must be updated +// to make a copy for the tui thread +static void tui_event(UI *ui, char *name, Array args, bool *args_consumed) +{ +} + static void invalidate(UI *ui, int top, int bot, int left, int right) { TUIData *data = ui->data; diff --git a/src/nvim/ui.c b/src/nvim/ui.c index d968cbc390..306dd6c43a 100644 --- a/src/nvim/ui.c +++ b/src/nvim/ui.c @@ -27,6 +27,7 @@ #include "nvim/os/time.h" #include "nvim/os/input.h" #include "nvim/os/signal.h" +#include "nvim/popupmnu.h" #include "nvim/screen.h" #include "nvim/syntax.h" #include "nvim/window.h" @@ -35,6 +36,7 @@ #else # include "nvim/msgpack_rpc/server.h" #endif +#include "nvim/api/private/helpers.h" #ifdef INCLUDE_GENERATED_DECLARATIONS # include "ui.c.generated.h" @@ -143,6 +145,15 @@ void ui_set_icon(char *icon) UI_CALL(flush); } +void ui_event(char *name, Array args) +{ + bool args_consumed = false; + UI_CALL(event, name, args, &args_consumed); + if (!args_consumed) { + api_free_array(args); + } +} + // May update the shape of the cursor. void ui_cursor_shape(void) { @@ -156,15 +167,18 @@ void ui_refresh(void) } int width = INT_MAX, height = INT_MAX; + bool pum_external = true; for (size_t i = 0; i < ui_count; i++) { UI *ui = uis[i]; width = ui->width < width ? ui->width : width; height = ui->height < height ? ui->height : height; + pum_external &= ui->pum_external; } row = col = 0; screen_resize(width, height); + pum_set_external(pum_external); } void ui_resize(int new_width, int new_height) @@ -519,3 +533,4 @@ static void ui_mode_change(void) UI_CALL(mode_change, mode); conceal_check_cursur_line(); } + diff --git a/src/nvim/ui.h b/src/nvim/ui.h index 5934d2fee9..d14bc5812c 100644 --- a/src/nvim/ui.h +++ b/src/nvim/ui.h @@ -5,6 +5,8 @@ #include <stdbool.h> #include <stdint.h> +#include "api/private/defs.h" + typedef struct { bool bold, underline, undercurl, italic, reverse; int foreground, background, special; @@ -13,7 +15,7 @@ typedef struct { typedef struct ui_t UI; struct ui_t { - bool rgb; + bool rgb, pum_external; int width, height; void *data; void (*resize)(UI *ui, int rows, int columns); @@ -39,6 +41,7 @@ struct ui_t { void (*suspend)(UI *ui); void (*set_title)(UI *ui, char *title); void (*set_icon)(UI *ui, char *icon); + void (*event)(UI *ui, char *name, Array args, bool *args_consumed); void (*stop)(UI *ui); }; diff --git a/src/nvim/ui_bridge.c b/src/nvim/ui_bridge.c index 6290fb3d87..34b95baf6c 100644 --- a/src/nvim/ui_bridge.c +++ b/src/nvim/ui_bridge.c @@ -31,6 +31,7 @@ UI *ui_bridge_attach(UI *ui, ui_main_fn ui_main, event_scheduler scheduler) UIBridgeData *rv = xcalloc(1, sizeof(UIBridgeData)); rv->ui = ui; rv->bridge.rgb = ui->rgb; + rv->bridge.pum_external = ui->pum_external; rv->bridge.stop = ui_bridge_stop; rv->bridge.resize = ui_bridge_resize; rv->bridge.clear = ui_bridge_clear; diff --git a/test/functional/autocmd/termclose_spec.lua b/test/functional/autocmd/termclose_spec.lua index 02ea0dbd95..0bc2931992 100644 --- a/test/functional/autocmd/termclose_spec.lua +++ b/test/functional/autocmd/termclose_spec.lua @@ -12,7 +12,7 @@ describe('TermClose event', function() nvim('set_option', 'shell', nvim_dir .. '/shell-test') nvim('set_option', 'shellcmdflag', 'EXE') screen = Screen.new(20, 4) - screen:attach(false) + screen:attach({rgb=false}) end) it('works as expected', function() diff --git a/test/functional/helpers.lua b/test/functional/helpers.lua index 2d54d23254..140e78aeb5 100644 --- a/test/functional/helpers.lua +++ b/test/functional/helpers.lua @@ -306,6 +306,10 @@ local function nvim(method, ...) return request('vim_'..method, ...) end +local function ui(method, ...) + return request('nvim_ui_'..method, ...) +end + local function nvim_async(method, ...) session:notify('vim_'..method, ...) end @@ -432,6 +436,7 @@ end local funcs = create_callindex(nvim_call) local meths = create_callindex(nvim) +local uimeths = create_callindex(ui) local bufmeths = create_callindex(buffer) local winmeths = create_callindex(window) local tabmeths = create_callindex(tabpage) @@ -490,6 +495,7 @@ return function(after_each) bufmeths = bufmeths, winmeths = winmeths, tabmeths = tabmeths, + uimeths = uimeths, curbufmeths = curbufmeths, curwinmeths = curwinmeths, curtabmeths = curtabmeths, diff --git a/test/functional/terminal/cursor_spec.lua b/test/functional/terminal/cursor_spec.lua index 1557868473..45595e0eb8 100644 --- a/test/functional/terminal/cursor_spec.lua +++ b/test/functional/terminal/cursor_spec.lua @@ -135,7 +135,7 @@ describe('cursor with customized highlighting', function() [2] = {foreground = 55, background = 56}, [3] = {bold = true}, }) - screen:attach(false) + screen:attach({rgb=false}) execute('call termopen(["'..nvim_dir..'/tty-test"]) | startinsert') end) diff --git a/test/functional/terminal/edit_spec.lua b/test/functional/terminal/edit_spec.lua index c98aef70b1..1936f70b82 100644 --- a/test/functional/terminal/edit_spec.lua +++ b/test/functional/terminal/edit_spec.lua @@ -12,7 +12,7 @@ local eq = helpers.eq describe(':edit term://*', function() local get_screen = function(columns, lines) local scr = screen.new(columns, lines) - scr:attach(false) + scr:attach({rgb=false}) return scr end diff --git a/test/functional/terminal/ex_terminal_spec.lua b/test/functional/terminal/ex_terminal_spec.lua index 458fa02fca..135a234a23 100644 --- a/test/functional/terminal/ex_terminal_spec.lua +++ b/test/functional/terminal/ex_terminal_spec.lua @@ -10,7 +10,7 @@ describe(':terminal', function() before_each(function() clear() screen = Screen.new(50, 4) - screen:attach(false) + screen:attach({rgb=false}) nvim('set_option', 'shell', nvim_dir..'/shell-test') nvim('set_option', 'shellcmdflag', 'EXE') diff --git a/test/functional/terminal/helpers.lua b/test/functional/terminal/helpers.lua index 1b8893c988..aacf109f2f 100644 --- a/test/functional/terminal/helpers.lua +++ b/test/functional/terminal/helpers.lua @@ -53,7 +53,7 @@ local function screen_setup(extra_height, command) [9] = {foreground = 4}, }) - screen:attach(false) + screen:attach({rgb=false}) -- tty-test puts the terminal into raw mode and echoes all input. tests are -- done by feeding it with terminfo codes to control the display and -- verifying output with screen:expect. diff --git a/test/functional/terminal/highlight_spec.lua b/test/functional/terminal/highlight_spec.lua index 95fbf2c871..3d0ff6091d 100644 --- a/test/functional/terminal/highlight_spec.lua +++ b/test/functional/terminal/highlight_spec.lua @@ -25,7 +25,7 @@ describe('terminal window highlighting', function() [10] = {reverse = true}, [11] = {background = 11}, }) - screen:attach(false) + screen:attach({rgb=false}) execute('enew | call termopen(["'..nvim_dir..'/tty-test"]) | startinsert') screen:expect([[ tty ready | @@ -127,7 +127,7 @@ describe('terminal window highlighting with custom palette', function() [8] = {background = 11}, [9] = {bold = true}, }) - screen:attach(true) + screen:attach({rgb=true}) nvim('set_var', 'terminal_color_3', '#123456') execute('enew | call termopen(["'..nvim_dir..'/tty-test"]) | startinsert') screen:expect([[ @@ -185,7 +185,7 @@ describe('synIDattr()', function() end) it('returns gui-color if RGB-capable UI is attached', function() - screen:attach(true) + screen:attach({rgb=true}) eq('#ff0000', eval('synIDattr(hlID("Normal"), "fg")')) eq('Black', eval('synIDattr(hlID("Normal"), "bg")')) eq('Salmon', eval('synIDattr(hlID("Keyword"), "fg")')) @@ -193,7 +193,7 @@ describe('synIDattr()', function() end) it('returns #RRGGBB value for fg#/bg#/sp#', function() - screen:attach(true) + screen:attach({rgb=true}) eq('#ff0000', eval('synIDattr(hlID("Normal"), "fg#")')) eq('#000000', eval('synIDattr(hlID("Normal"), "bg#")')) eq('#fa8072', eval('synIDattr(hlID("Keyword"), "fg#")')) @@ -201,7 +201,7 @@ describe('synIDattr()', function() end) it('returns color number if non-GUI', function() - screen:attach(false) + screen:attach({rgb=false}) eq('252', eval('synIDattr(hlID("Normal"), "fg")')) eq('79', eval('synIDattr(hlID("Keyword"), "fg")')) end) diff --git a/test/functional/terminal/scrollback_spec.lua b/test/functional/terminal/scrollback_spec.lua index 4790419bc8..60d6a6cc36 100644 --- a/test/functional/terminal/scrollback_spec.lua +++ b/test/functional/terminal/scrollback_spec.lua @@ -331,7 +331,7 @@ describe('terminal prints more lines than the screen height and exits', function it('will push extra lines to scrollback', function() clear() local screen = Screen.new(50, 7) - screen:attach(false) + screen:attach({rgb=false}) execute('call termopen(["'..nvim_dir..'/tty-test", "10"]) | startinsert') wait() screen:expect([[ diff --git a/test/functional/ui/screen.lua b/test/functional/ui/screen.lua index b219196866..96324bfac5 100644 --- a/test/functional/ui/screen.lua +++ b/test/functional/ui/screen.lua @@ -106,7 +106,7 @@ -- use `screen:snapshot_util({},true)` local helpers = require('test.functional.helpers')(nil) -local request, run = helpers.request, helpers.run +local request, run, uimeths = helpers.request, helpers.run, helpers.uimeths local dedent = helpers.dedent local Screen = {} @@ -192,22 +192,22 @@ function Screen:set_default_attr_ignore(attr_ignore) self._default_attr_ignore = attr_ignore end -function Screen:attach(rgb) - if rgb == nil then - rgb = true +function Screen:attach(options) + if options == nil then + options = {rgb=true} end - request('ui_attach', self._width, self._height, rgb) + uimeths.attach(self._width, self._height, options) end function Screen:detach() - request('ui_detach') + uimeths.detach() end function Screen:try_resize(columns, rows) - request('ui_try_resize', columns, rows) + uimeths.try_resize(columns, rows) end -function Screen:expect(expected, attr_ids, attr_ignore) +function Screen:expect(expected, attr_ids, attr_ignore, condition) -- remove the last line and dedent expected = dedent(expected:gsub('\n[ ]+$', '')) local expected_rows = {} @@ -219,6 +219,12 @@ function Screen:expect(expected, attr_ids, attr_ignore) local ids = attr_ids or self._default_attr_ids local ignore = attr_ignore or self._default_attr_ignore self:wait(function() + if condition ~= nil then + local status, res = pcall(condition) + if not status then + return tostring(res) + end + end local actual_rows = {} for i = 1, self._height do actual_rows[i] = self:_row_repr(self._rows[i], ids, ignore) @@ -303,12 +309,20 @@ function Screen:_redraw(updates) local method = update[1] for i = 2, #update do local handler = self['_handle_'..method] - handler(self, unpack(update[i])) + if handler ~= nil then + handler(self, unpack(update[i])) + else + self._on_event(method, update[i]) + end end -- print(self:_current_screen()) end end +function Screen:set_on_event_handler(callback) + self._on_event = callback +end + function Screen:_handle_resize(width, height) local rows = {} for _ = 1, height do diff --git a/test/functional/viml/completion_spec.lua b/test/functional/viml/completion_spec.lua index 07544a91ab..5de3bb5da7 100644 --- a/test/functional/viml/completion_spec.lua +++ b/test/functional/viml/completion_spec.lua @@ -755,4 +755,106 @@ describe('completion', function() ]]) end) end) + +end) + +describe('External completion popupmenu', function() + local screen + local items, selected, anchor + before_each(function() + clear() + screen = Screen.new(60, 8) + screen:attach({rgb=true, popupmenu_external=true}) + screen:set_default_attr_ids({ + [1] = {bold=true, foreground=Screen.colors.Blue}, + [2] = {bold = true}, + }) + screen:set_on_event_handler(function(name, data) + if name == "popupmenu_show" then + local row, col + items, selected, row, col = unpack(data) + anchor = {row, col} + elseif name == "popupmenu_select" then + selected = data[1] + elseif name == "popupmenu_hide" then + items = nil + end + end) + end) + + it('works', function() + source([[ + function! TestComplete() abort + call complete(1, ['foo', 'bar', 'spam']) + return '' + endfunction + ]]) + local expected = { + {'foo', '', '', ''}, + {'bar', '', '', ''}, + {'spam', '', '', ''}, + } + feed('o<C-r>=TestComplete()<CR>') + screen:expect([[ + | + foo^ | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {2:-- INSERT --} | + ]], nil, nil, function() + eq(expected, items) + eq(0, selected) + eq({1,0}, anchor) + end) + + feed('<c-p>') + screen:expect([[ + | + ^ | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {2:-- INSERT --} | + ]], nil, nil, function() + eq(expected, items) + eq(-1, selected) + eq({1,0}, anchor) + end) + + -- down moves the selection in the menu, but does not insert anything + feed('<down><down>') + screen:expect([[ + | + ^ | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {2:-- INSERT --} | + ]], nil, nil, function() + eq(expected, items) + eq(1, selected) + eq({1,0}, anchor) + end) + + feed('<cr>') + screen:expect([[ + | + bar^ | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {2:-- INSERT --} | + ]], nil, nil, function() + eq(nil, items) -- popupmenu was hidden + end) + end) end) |