diff options
author | Justin M. Keyes <justinkz@gmail.com> | 2017-10-29 02:13:12 +0200 |
---|---|---|
committer | Justin M. Keyes <justinkz@gmail.com> | 2017-10-29 02:13:12 +0200 |
commit | 7b0ceb3726e6d331625d5754be534e28a7f6092a (patch) | |
tree | 2edc27166a5034c1fe4d670769dac2076c30cf7e | |
parent | 122f52bf897abd1cda4fa467157657fee6bcfe94 (diff) | |
parent | 1a93f5883105c304d496d6c2e841626ebb7d44c1 (diff) | |
download | rneovim-7b0ceb3726e6d331625d5754be534e28a7f6092a.tar.gz rneovim-7b0ceb3726e6d331625d5754be534e28a7f6092a.tar.bz2 rneovim-7b0ceb3726e6d331625d5754be534e28a7f6092a.zip |
Merge #7173 'api/ui: externalize cmdline'
closes #6162
-rw-r--r-- | runtime/doc/api.txt | 2 | ||||
-rw-r--r-- | runtime/doc/msgpack_rpc.txt | 199 | ||||
-rw-r--r-- | runtime/doc/ui.txt | 290 | ||||
-rw-r--r-- | src/nvim/api/private/helpers.c | 33 | ||||
-rw-r--r-- | src/nvim/api/ui_events.in.h | 16 | ||||
-rw-r--r-- | src/nvim/eval.c | 39 | ||||
-rw-r--r-- | src/nvim/ex_getln.c | 353 | ||||
-rw-r--r-- | src/nvim/globals.h | 7 | ||||
-rw-r--r-- | src/nvim/screen.c | 5 | ||||
-rw-r--r-- | test/functional/ui/cmdline_spec.lua | 525 |
10 files changed, 1153 insertions, 316 deletions
diff --git a/runtime/doc/api.txt b/runtime/doc/api.txt index 8aead087db..159dd93c5e 100644 --- a/runtime/doc/api.txt +++ b/runtime/doc/api.txt @@ -48,7 +48,7 @@ version.api_compatible API is backwards-compatible with this level version.api_prerelease Declares the current API level as unstable > (version.api_prerelease && fn.since == version.api_level) functions API function signatures -ui_events UI event signatures |rpc-remote-ui| +ui_events UI event signatures |ui| {fn}.since API level where function {fn} was introduced {fn}.deprecated_since API level where function {fn} was deprecated types Custom handle types defined by Nvim diff --git a/runtime/doc/msgpack_rpc.txt b/runtime/doc/msgpack_rpc.txt index 98a74ccfd6..7af6e401da 100644 --- a/runtime/doc/msgpack_rpc.txt +++ b/runtime/doc/msgpack_rpc.txt @@ -243,203 +243,4 @@ Even for statically compiled clients it is good practice to avoid hardcoding 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* - -GUIs can be implemented as external processes communicating with Nvim over the -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"). - -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 on a grid of width × height cells. `options` must be -a dictionary with these (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). - `ext_popupmenu` Externalize the popupmenu. |ui-ext-popupmenu| - `ext_tabline` Externalize the tabline. |ui-ext-tabline| - Externalized widgets will not be drawn by - Nvim; only high-level data will be published - in new UI event kinds. - -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 in the same "redraw" event are processed -(not any intermediate state after processing only a part of the array). - -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 -to be forward-compatible. |api-contract| - -Screen updates are tuples whose first element is the string name of the update -kind. - -["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_info_set", cursor_style_enabled, mode_info] -`cursor_style_enabled` is a boolean indicating if the UI should set the cursor -style. `mode_info` is a list of mode property maps. The current mode is given -by the `mode_idx` field of the `mode_change` event. - -Each mode property map may contain these keys: - KEY DESCRIPTION ~ - `cursor_shape`: "block", "horizontal", "vertical" - `cell_percentage`: Cell % occupied by the cursor. - `blinkwait`, `blinkon`, `blinkoff`: See |cursor-blinking|. - `hl_id`: Cursor highlight group. - `hl_lm`: Cursor highlight group if 'langmap' is active. - `short_name`: Mode code name, see 'guicursor'. - `name`: Mode descriptive name. - `mouse_shape`: (To be implemented.) - -Some keys are missing in some modes. - -["mode_change", mode, mode_idx] -The mode changed. The first parameter `mode` is a string representing the -current mode. `mode_idx` is an index into the array received in the -`mode_info_set` event. UIs should change the cursor style according to the -properties specified in the corresponding item. The set of modes reported will -change in new versions of Nvim, for instance more submodes and temporary -states might be represented as separate modes. - - *ui-ext-popupmenu* -["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. - - *ui-ext-tabline* -["tabline_update", curtab, tabs] - Tabline was updated. UIs should present this data in a custom tabline - widget. - curtab: Current Tabpage - tabs: List of Dicts [{ "tab": Tabpage, "name": String }, ...] -============================================================================== - vim:tw=78:ts=8:noet:ft=help:norl: diff --git a/runtime/doc/ui.txt b/runtime/doc/ui.txt new file mode 100644 index 0000000000..deac1609c0 --- /dev/null +++ b/runtime/doc/ui.txt @@ -0,0 +1,290 @@ +*ui.txt* Nvim + + + NVIM REFERENCE MANUAL + + +Nvim UI protocol *ui* + + Type |gO| to see the table of contents. + +============================================================================== +Introduction *ui-intro* + +GUIs can be implemented as external processes communicating with Nvim over the +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 +a dictionary with these (optional) keys: + `rgb` Decides the color format. + Set true (default) for 24-bit RGB colors. + Set false for terminal colors (max of 256). + *ui-ext-options* + `ext_popupmenu` Externalize the popupmenu. |ui-popupmenu| + `ext_tabline` Externalize the tabline. |ui-tabline| + `ext_cmdline` Externalize the cmdline. |ui-cmdline| + +Nvim will then send msgpack-rpc notifications, with the method name "redraw" +and a single argument, an array of screen update events. +Update events are tuples whose first element is the event name and remaining +elements the event parameters. + +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). + +Nvim sends |ui-global| and |ui-grid| events unconditionally; these suffice to +implement a terminal-like interface. + +Nvim optionally sends screen elements "semantically" as structured events +instead of raw grid-lines. Then the UI must decide how to present those +elements itself; Nvim will not draw those elements on the grid. This is +controlled by the |ui-ext-options|. + +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 +to be forward-compatible. |api-contract| + +============================================================================== +Global Events *ui-global* + +["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. + +["mode_info_set", cursor_style_enabled, mode_info] + `cursor_style_enabled` is a boolean indicating if the UI should set + the cursor style. `mode_info` is a list of mode property maps. The + current mode is given by the `mode_idx` field of the `mode_change` + event. + + Each mode property map may contain these keys: + + KEY DESCRIPTION ~ + `cursor_shape`: "block", "horizontal", "vertical" + `cell_percentage`: Cell % occupied by the cursor. + `blinkwait`, `blinkon`, `blinkoff`: See |cursor-blinking|. + `hl_id`: Cursor highlight group. + `hl_lm`: Cursor highlight group if 'langmap' is active. + `short_name`: Mode code name, see 'guicursor'. + `name`: Mode descriptive name. + `mouse_shape`: (To be implemented.) + + Some keys are missing in some modes. + +["mode_change", mode, mode_idx] + The mode changed. The first parameter `mode` is a string representing + the current mode. `mode_idx` is an index into the array received in + the `mode_info_set` event. UIs should change the cursor style + according to the properties specified in the corresponding item. The + set of modes reported will change in new versions of Nvim, for + instance more submodes and temporary states might be represented as + separate modes. + +["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 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. + +["update_menu"] + The menu mappings changed. + +["bell"] +["visual_bell"] + Notify the user with an audible or visual bell, respectively. + +============================================================================== +Grid Events *ui-grid* + +["resize", width, height] + The grid is resized to `width` and `height` cells. + +["clear"] + Clear the grid. + +["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. + + *ui-event-highlight_set* +["highlight_set", attrs] + Set the attributes that the next text put on the grid 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 + +-------------------------+ +< +============================================================================== +Popupmenu Events *ui-popupmenu* + +Only sent if `ext_popupmenu` option is set in |ui-options| + +["popupmenu_show", items, selected, row, col] + `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. + +============================================================================== +Tabline Events *ui-tabline* + +Only sent if `ext_tabline` option is set in |ui-options| + +["tabline_update", curtab, tabs] + Tabline was updated. UIs should present this data in a custom tabline + widget. + curtab: Current Tabpage + tabs: List of Dicts [{ "tab": Tabpage, "name": String }, ...] + +============================================================================== +Cmdline Events *ui-cmdline* + +Only sent if `ext_cmdline` option is set in |ui-options| + +["cmdline_show", content, pos, firstc, prompt, indent, level] + content: List of [attrs, string] + [[{}, "t"], [attrs, "est"], ...] + + Triggered when the user types in the cmdline. + The `content` is the full content that should be displayed in the + cmdline, and the `pos` is the position of the cursor that in the + cmdline. The content is divided into chunks with different highlight + attributes represented as a dict (see |ui-event-highlight_set|). + + `firstc` and `prompt` are text, that if non-empty should be + displayed in front of the command line. `firstc` always indicates + built-in command lines such as `:` (ex command) and `/` `?` (search), + while `prompt` is an |input()| prompt. `indent` tells how many spaces + the content should be indented. + + The Nvim command line can be invoked recursively, for instance by + typing `<c-r>=` at the command line prompt. The `level` field is used + to distinguish different command lines active at the same time. The + first invoked command line has level 1, the next recursively-invoked + prompt has level 2. A command line invoked from the |cmd-line-window| + has a higher level than than the edited command line. + +["cmdline_pos", pos, level] + Change the cursor position in the cmdline. + +["cmdline_special_char", c, shift, level] + Display a special char in the cmdline at the cursor position. This is + typically used to indicate a pending state, e.g. after |c_CTRL-V|. If + `shift` is true the text after the cursor should be shifted, otherwise + it should overwrite the char at the cursor. + + Should be hidden at next cmdline_pos. + +["cmdline_hide"] + Hide the cmdline. + +["cmdline_block_show", lines] + Show a block of context to the current command line. For example if + the user defines a |:function| interactively: > + :function Foo() + : echo "foo" + : +< + `lines` is a list of lines of highlighted chunks, in the same form as + the "cmdline_show" `contents` parameter. + +["cmdline_block_append", line] + Append a line at the end of the currently shown block. + +["cmdline_block_hide"] + Hide the block. + +============================================================================== + vim:tw=78:ts=8:noet:ft=help:norl: diff --git a/src/nvim/api/private/helpers.c b/src/nvim/api/private/helpers.c index e736e29e2d..2944925a9c 100644 --- a/src/nvim/api/private/helpers.c +++ b/src/nvim/api/private/helpers.c @@ -667,6 +667,22 @@ tabpage_T *find_tab_by_handle(Tabpage tabpage, Error *err) return rv; } +/// Allocates a String consisting of a single char. Does not support multibyte +/// characters. The resulting string is also NUL-terminated, to facilitate +/// interoperating with code using C strings. +/// +/// @param char the char to convert +/// @return the resulting String, if the input char was NUL, an +/// empty String is returned +String cchar_to_string(char c) +{ + char buf[] = { c, NUL }; + return (String) { + .data = xmemdupz(buf, 1), + .size = (c != NUL) ? 1 : 0 + }; +} + /// Copies a C string into a String (binary safe string, characters + length). /// The resulting string is also NUL-terminated, to facilitate interoperating /// with code using C strings. @@ -687,6 +703,23 @@ String cstr_to_string(const char *str) }; } +/// Copies buffer to an allocated String. +/// The resulting string is also NUL-terminated, to facilitate interoperating +/// with code using C strings. +/// +/// @param buf the buffer to copy +/// @param size length of the buffer +/// @return the resulting String, if the input string was NULL, an +/// empty String is returned +String cbuf_to_string(const char *buf, size_t size) + FUNC_ATTR_NONNULL_ALL +{ + return (String) { + .data = xmemdupz(buf, size), + .size = size + }; +} + /// Creates a String using the given C string. Unlike /// cstr_to_string this function DOES NOT copy the C string. /// diff --git a/src/nvim/api/ui_events.in.h b/src/nvim/api/ui_events.in.h index 1b5d17584f..65357d008a 100644 --- a/src/nvim/api/ui_events.in.h +++ b/src/nvim/api/ui_events.in.h @@ -68,4 +68,20 @@ void popupmenu_select(Integer selected) void tabline_update(Tabpage current, Array tabs) FUNC_API_SINCE(3) FUNC_API_REMOTE_ONLY; +void cmdline_show(Array content, Integer pos, String firstc, String prompt, + Integer indent, Integer level) + FUNC_API_SINCE(3) FUNC_API_REMOTE_ONLY; +void cmdline_pos(Integer pos, Integer level) + FUNC_API_SINCE(3) FUNC_API_REMOTE_ONLY; +void cmdline_special_char(String c, Boolean shift, Integer level) + FUNC_API_SINCE(3) FUNC_API_REMOTE_ONLY; +void cmdline_hide(Integer level) + FUNC_API_SINCE(3) FUNC_API_REMOTE_ONLY; +void cmdline_block_show(Array lines) + FUNC_API_SINCE(3) FUNC_API_REMOTE_ONLY; +void cmdline_block_append(Array lines) + FUNC_API_SINCE(3) FUNC_API_REMOTE_ONLY; +void cmdline_block_hide(void) + FUNC_API_SINCE(3) FUNC_API_REMOTE_ONLY; + #endif // NVIM_API_UI_EVENTS_IN_H diff --git a/src/nvim/eval.c b/src/nvim/eval.c index ef2f671f36..c7cb51ac29 100644 --- a/src/nvim/eval.c +++ b/src/nvim/eval.c @@ -11147,17 +11147,18 @@ void get_user_input(const typval_T *const argvars, cmd_silent = false; // Want to see the prompt. // Only the part of the message after the last NL is considered as - // prompt for the command line. - const char *p = strrchr(prompt, '\n'); - if (p == NULL) { - p = prompt; - } else { - p++; - msg_start(); - msg_clr_eos(); - msg_puts_attr_len(prompt, p - prompt, echo_attr); - msg_didout = false; - msg_starthere(); + // prompt for the command line, unlsess cmdline is externalized + const char *p = prompt; + if (!ui_is_external(kUICmdline)) { + const char *lastnl = strrchr(prompt, '\n'); + if (lastnl != NULL) { + p = lastnl+1; + msg_start(); + msg_clr_eos(); + msg_puts_attr_len(prompt, p - prompt, echo_attr); + msg_didout = false; + msg_starthere(); + } } cmdline_row = msg_row; @@ -19643,6 +19644,7 @@ void ex_function(exarg_T *eap) int todo; hashitem_T *hi; int sourcing_lnum_off; + bool show_block = false; /* * ":function" without argument: list functions. @@ -19816,6 +19818,11 @@ void ex_function(exarg_T *eap) goto errret_2; } + if (KeyTyped && ui_is_external(kUICmdline)) { + show_block = true; + ui_ext_cmdline_block_append(0, (const char *)eap->cmd); + } + // find extra arguments "range", "dict", "abort" and "closure" for (;; ) { p = skipwhite(p); @@ -19868,7 +19875,9 @@ void ex_function(exarg_T *eap) if (!eap->skip && did_emsg) goto erret; - msg_putchar('\n'); /* don't overwrite the function name */ + if (!ui_is_external(kUICmdline)) { + msg_putchar('\n'); // don't overwrite the function name + } cmdline_row = msg_row; } @@ -19902,6 +19911,9 @@ void ex_function(exarg_T *eap) EMSG(_("E126: Missing :endfunction")); goto erret; } + if (show_block) { + ui_ext_cmdline_block_append(indent, (const char *)theline); + } /* Detect line continuation: sourcing_lnum increased more than one. */ if (sourcing_lnum > sourcing_lnum_off + 1) @@ -20194,6 +20206,9 @@ ret_free: xfree(name); did_emsg |= saved_did_emsg; need_wait_return |= saved_wait_return; + if (show_block) { + ui_ext_cmdline_block_leave(); + } } /// Get a function name, translating "<SID>" and "<SNR>". diff --git a/src/nvim/ex_getln.c b/src/nvim/ex_getln.c index 54e5bcb9ff..e79476ab53 100644 --- a/src/nvim/ex_getln.c +++ b/src/nvim/ex_getln.c @@ -67,6 +67,30 @@ #include "nvim/api/private/helpers.h" #include "nvim/highlight_defs.h" +/// Command-line colors: one chunk +/// +/// Defines a region which has the same highlighting. +typedef struct { + int start; ///< Colored chunk start. + int end; ///< Colored chunk end (exclusive, > start). + int attr; ///< Highlight attr. +} CmdlineColorChunk; + +/// Command-line colors +/// +/// Holds data about all colors. +typedef kvec_t(CmdlineColorChunk) CmdlineColors; + +/// Command-line coloring +/// +/// Holds both what are the colors and what have been colored. Latter is used to +/// suppress unnecessary calls to coloring callbacks. +typedef struct { + unsigned prompt_id; ///< ID of the prompt which was colored last. + char *cmdbuff; ///< What exactly was colored last time or NULL. + CmdlineColors colors; ///< Last colors. +} ColoredCmdline; + /* * Variables shared between getcmdline(), redrawcmdline() and others. * These need to be saved when using CTRL-R |, that's why they are in a @@ -91,6 +115,11 @@ struct cmdline_info { int input_fn; // when TRUE Invoked for input() function unsigned prompt_id; ///< Prompt number, used to disable coloring on errors. Callback highlight_callback; ///< Callback used for coloring user input. + ColoredCmdline last_colors; ///< Last cmdline colors + int level; // current cmdline level + struct cmdline_info *prev_ccline; ///< pointer to saved cmdline state + char special_char; ///< last putcmdline char (used for redraws) + bool special_shift; ///< shift of last putcmdline char }; /// Last value of prompt_id, incremented when doing new prompt static unsigned last_prompt_id = 0; @@ -143,36 +172,6 @@ typedef struct command_line_state { struct cmdline_info save_ccline; } CommandLineState; -/// Command-line colors: one chunk -/// -/// Defines a region which has the same highlighting. -typedef struct { - int start; ///< Colored chunk start. - int end; ///< Colored chunk end (exclusive, > start). - int attr; ///< Highlight attr. -} CmdlineColorChunk; - -/// Command-line colors -/// -/// Holds data about all colors. -typedef kvec_t(CmdlineColorChunk) CmdlineColors; - -/// Command-line coloring -/// -/// Holds both what are the colors and what have been colored. Latter is used to -/// suppress unnecessary calls to coloring callbacks. -typedef struct { - unsigned prompt_id; ///< ID of the prompt which was colored last. - char *cmdbuff; ///< What exactly was colored last time or NULL. - CmdlineColors colors; ///< Last colors. -} ColoredCmdline; - -/// Last command-line colors. -ColoredCmdline last_ccline_colors = { - .cmdbuff = NULL, - .colors = KV_INITIAL_VALUE -}; - typedef struct cmdline_info CmdlineInfo; /* The current cmdline_info. It is initialized in getcmdline() and after that @@ -185,6 +184,8 @@ static int cmd_showtail; /* Only show path tail in lists ? */ static int new_cmdpos; /* position set by set_cmdline_pos() */ +static Array cmdline_block; ///< currently displayed block of context + /* * Type used by call_user_expand_func */ @@ -239,6 +240,7 @@ static uint8_t *command_line_enter(int firstc, long count, int indent) } ccline.prompt_id = last_prompt_id++; + ccline.level++; ccline.overstrike = false; // always start in insert mode clearpos(&s->match_end); s->save_cursor = curwin->w_cursor; // may be restored later @@ -258,6 +260,9 @@ static uint8_t *command_line_enter(int firstc, long count, int indent) ccline.cmdlen = ccline.cmdpos = 0; ccline.cmdbuff[0] = NUL; + ccline.last_colors = (ColoredCmdline){ .cmdbuff = NULL, + .colors = KV_INITIAL_VALUE }; + // autoindent for :insert and :append if (s->firstc <= 0) { memset(ccline.cmdbuff, ' ', s->indent); @@ -408,12 +413,19 @@ static uint8_t *command_line_enter(int firstc, long count, int indent) setmouse(); ui_cursor_shape(); // may show different cursor shape xfree(s->save_p_icm); + xfree(ccline.last_colors.cmdbuff); + kv_destroy(ccline.last_colors.colors); { char_u *p = ccline.cmdbuff; // Make ccline empty, getcmdline() may try to use it. ccline.cmdbuff = NULL; + + if (ui_is_external(kUICmdline)) { + ui_call_cmdline_hide(ccline.level); + } + ccline.level--; return p; } } @@ -804,7 +816,9 @@ static int command_line_execute(VimState *state, int key) } if (!cmd_silent) { - ui_cursor_goto(msg_row, 0); + if (!ui_is_external(kUICmdline)) { + ui_cursor_goto(msg_row, 0); + } ui_flush(); } return 0; @@ -1134,7 +1148,7 @@ static int command_line_handle_key(CommandLineState *s) xfree(ccline.cmdbuff); // no commandline to return ccline.cmdbuff = NULL; - if (!cmd_silent) { + if (!cmd_silent && !ui_is_external(kUICmdline)) { if (cmdmsg_rl) { msg_col = Columns; } else { @@ -1586,9 +1600,14 @@ static int command_line_handle_key(CommandLineState *s) s->do_abbr = false; // don't do abbreviation now // may need to remove ^ when composing char was typed if (enc_utf8 && utf_iscomposing(s->c) && !cmd_silent) { - draw_cmdline(ccline.cmdpos, ccline.cmdlen - ccline.cmdpos); - msg_putchar(' '); - cursorcmd(); + if (ui_is_external(kUICmdline)) { + // TODO(bfredl): why not make unputcmdline also work with true? + unputcmdline(); + } else { + draw_cmdline(ccline.cmdpos, ccline.cmdlen - ccline.cmdpos); + msg_putchar(' '); + cursorcmd(); + } } break; @@ -2346,8 +2365,7 @@ enum { MAX_CB_ERRORS = 1 }; /// Should use built-in command parser or user-specified one. Currently only the /// latter is supported. /// -/// @param[in] colored_ccline Command-line to color. -/// @param[out] ret_ccline_colors What should be colored. Also holds a cache: +/// @param[in,out] colored_ccline Command-line to color. Also holds a cache: /// if ->prompt_id and ->cmdbuff values happen /// to be equal to those from colored_cmdline it /// will just do nothing, assuming that ->colors @@ -2357,11 +2375,11 @@ enum { MAX_CB_ERRORS = 1 }; /// /// @return true if draw_cmdline may proceed, false if it does not need anything /// to do. -static bool color_cmdline(const CmdlineInfo *const colored_ccline, - ColoredCmdline *const ret_ccline_colors) +static bool color_cmdline(CmdlineInfo *colored_ccline) FUNC_ATTR_NONNULL_ALL FUNC_ATTR_WARN_UNUSED_RESULT { bool printed_errmsg = false; + #define PRINT_ERRMSG(...) \ do { \ msg_putchar('\n'); \ @@ -2370,19 +2388,21 @@ static bool color_cmdline(const CmdlineInfo *const colored_ccline, } while (0) bool ret = true; + ColoredCmdline *ccline_colors = &colored_ccline->last_colors; + // Check whether result of the previous call is still valid. - if (ret_ccline_colors->prompt_id == colored_ccline->prompt_id - && ret_ccline_colors->cmdbuff != NULL - && STRCMP(ret_ccline_colors->cmdbuff, colored_ccline->cmdbuff) == 0) { + if (ccline_colors->prompt_id == colored_ccline->prompt_id + && ccline_colors->cmdbuff != NULL + && STRCMP(ccline_colors->cmdbuff, colored_ccline->cmdbuff) == 0) { return ret; } - kv_size(ret_ccline_colors->colors) = 0; + kv_size(ccline_colors->colors) = 0; if (colored_ccline->cmdbuff == NULL || *colored_ccline->cmdbuff == NUL) { // Nothing to do, exiting. - xfree(ret_ccline_colors->cmdbuff); - ret_ccline_colors->cmdbuff = NULL; + xfree(ccline_colors->cmdbuff); + ccline_colors->cmdbuff = NULL; return ret; } @@ -2395,7 +2415,7 @@ static bool color_cmdline(const CmdlineInfo *const colored_ccline, static unsigned prev_prompt_id = UINT_MAX; static int prev_prompt_errors = 0; - Callback color_cb = { .type = kCallbackNone }; + Callback color_cb = CALLBACK_NONE; bool can_free_cb = false; TryState tstate; Error err = ERROR_INIT; @@ -2504,7 +2524,7 @@ static bool color_cmdline(const CmdlineInfo *const colored_ccline, goto color_cmdline_error; } if (start != prev_end) { - kv_push(ret_ccline_colors->colors, ((CmdlineColorChunk) { + kv_push(ccline_colors->colors, ((CmdlineColorChunk) { .start = prev_end, .end = start, .attr = 0, @@ -2533,14 +2553,14 @@ static bool color_cmdline(const CmdlineInfo *const colored_ccline, } const int id = syn_name2id((char_u *)group); const int attr = (id == 0 ? 0 : syn_id2attr(id)); - kv_push(ret_ccline_colors->colors, ((CmdlineColorChunk) { + kv_push(ccline_colors->colors, ((CmdlineColorChunk) { .start = start, .end = end, .attr = attr, })); } if (prev_end < colored_ccline->cmdlen) { - kv_push(ret_ccline_colors->colors, ((CmdlineColorChunk) { + kv_push(ccline_colors->colors, ((CmdlineColorChunk) { .start = prev_end, .end = colored_ccline->cmdlen, .attr = 0, @@ -2552,14 +2572,14 @@ color_cmdline_end: if (can_free_cb) { callback_free(&color_cb); } - xfree(ret_ccline_colors->cmdbuff); + xfree(ccline_colors->cmdbuff); // Note: errors “output” is cached just as well as regular results. - ret_ccline_colors->prompt_id = colored_ccline->prompt_id; + ccline_colors->prompt_id = colored_ccline->prompt_id; if (arg_allocated) { - ret_ccline_colors->cmdbuff = (char *)arg.vval.v_string; + ccline_colors->cmdbuff = (char *)arg.vval.v_string; } else { - ret_ccline_colors->cmdbuff = xmemdupz((const char *)colored_ccline->cmdbuff, - (size_t)colored_ccline->cmdlen); + ccline_colors->cmdbuff = xmemdupz((const char *)colored_ccline->cmdbuff, + (size_t)colored_ccline->cmdlen); } tv_clear(&tv); return ret; @@ -2572,7 +2592,7 @@ color_cmdline_error: (void)printed_errmsg; prev_prompt_errors++; - kv_size(ret_ccline_colors->colors) = 0; + kv_size(ccline_colors->colors) = 0; redrawcmdline(); ret = false; goto color_cmdline_end; @@ -2585,7 +2605,13 @@ color_cmdline_error: */ static void draw_cmdline(int start, int len) { - if (!color_cmdline(&ccline, &last_ccline_colors)) { + if (!color_cmdline(&ccline)) { + return; + } + + if (ui_is_external(kUICmdline)) { + ccline.special_char = NUL; + ui_ext_cmdline_show(&ccline); return; } @@ -2687,9 +2713,9 @@ static void draw_cmdline(int start, int len) msg_outtrans_len(arshape_buf, newlen); } else { draw_cmdline_no_arabicshape: - if (kv_size(last_ccline_colors.colors)) { - for (size_t i = 0; i < kv_size(last_ccline_colors.colors); i++) { - CmdlineColorChunk chunk = kv_A(last_ccline_colors.colors, i); + if (kv_size(ccline.last_colors.colors)) { + for (size_t i = 0; i < kv_size(ccline.last_colors.colors); i++) { + CmdlineColorChunk chunk = kv_A(ccline.last_colors.colors, i); if (chunk.end <= start) { continue; } @@ -2704,6 +2730,107 @@ draw_cmdline_no_arabicshape: } } +static void ui_ext_cmdline_show(CmdlineInfo *line) +{ + Array content = ARRAY_DICT_INIT; + if (cmdline_star) { + size_t len = 0; + for (char_u *p = ccline.cmdbuff; *p; mb_ptr_adv(p)) { + len++; + } + char *buf = xmallocz(len); + memset(buf, '*', len); + Array item = ARRAY_DICT_INIT; + ADD(item, DICTIONARY_OBJ((Dictionary)ARRAY_DICT_INIT)); + ADD(item, STRING_OBJ(((String) { .data = buf, .size = len }))); + ADD(content, ARRAY_OBJ(item)); + } else if (kv_size(line->last_colors.colors)) { + for (size_t i = 0; i < kv_size(line->last_colors.colors); i++) { + CmdlineColorChunk chunk = kv_A(line->last_colors.colors, i); + Array item = ARRAY_DICT_INIT; + + if (chunk.attr) { + attrentry_T *aep = syn_cterm_attr2entry(chunk.attr); + // TODO(bfredl): this desicion could be delayed by making attr_code a + // recognized type + HlAttrs rgb_attrs = attrentry2hlattrs(aep, true); + ADD(item, DICTIONARY_OBJ(hlattrs2dict(rgb_attrs))); + } else { + ADD(item, DICTIONARY_OBJ((Dictionary)ARRAY_DICT_INIT)); + } + ADD(item, STRING_OBJ(cbuf_to_string((char *)line->cmdbuff + chunk.start, + chunk.end-chunk.start))); + ADD(content, ARRAY_OBJ(item)); + } + } else { + Array item = ARRAY_DICT_INIT; + ADD(item, DICTIONARY_OBJ((Dictionary)ARRAY_DICT_INIT)); + ADD(item, STRING_OBJ(cstr_to_string((char *)(line->cmdbuff)))); + ADD(content, ARRAY_OBJ(item)); + } + ui_call_cmdline_show(content, line->cmdpos, + cchar_to_string((char)line->cmdfirstc), + cstr_to_string((char *)(line->cmdprompt)), + line->cmdindent, + line->level); + if (line->special_char) { + ui_call_cmdline_special_char(cchar_to_string((char)(line->special_char)), + line->special_shift, + line->level); + } +} + +void ui_ext_cmdline_block_append(int indent, const char *line) +{ + char *buf = xmallocz(indent + strlen(line)); + memset(buf, ' ', indent); + memcpy(buf+indent, line, strlen(line)); + + Array item = ARRAY_DICT_INIT; + ADD(item, DICTIONARY_OBJ((Dictionary)ARRAY_DICT_INIT)); + ADD(item, STRING_OBJ(cstr_as_string(buf))); + Array content = ARRAY_DICT_INIT; + ADD(content, ARRAY_OBJ(item)); + ADD(cmdline_block, ARRAY_OBJ(content)); + if (cmdline_block.size > 1) { + ui_call_cmdline_block_append(copy_array(content)); + } else { + ui_call_cmdline_block_show(copy_array(cmdline_block)); + } +} + +void ui_ext_cmdline_block_leave(void) +{ + api_free_array(cmdline_block); + ui_call_cmdline_block_hide(); +} + +/// Extra redrawing needed for redraw! and on ui_attach +/// assumes "redrawcmdline()" will already be invoked +void cmdline_screen_cleared(void) +{ + if (!ui_is_external(kUICmdline)) { + return; + } + + if (cmdline_block.size) { + ui_call_cmdline_block_show(copy_array(cmdline_block)); + } + + int prev_level = ccline.level-1; + CmdlineInfo *prev_ccline = ccline.prev_ccline; + while (prev_level > 0 && prev_ccline) { + if (prev_ccline->level == prev_level) { + // don't redraw a cmdline already shown in the cmdline window + if (prev_level != cmdwin_level) { + ui_ext_cmdline_show(prev_ccline); + } + prev_level--; + } + prev_ccline = prev_ccline->prev_ccline; + } +} + /* * Put a character on the command line. Shifts the following text to the * right when "shift" is TRUE. Used for CTRL-V, CTRL-K, etc. @@ -2711,33 +2838,39 @@ draw_cmdline_no_arabicshape: */ void putcmdline(int c, int shift) { - if (cmd_silent) + if (cmd_silent) { return; - msg_no_more = TRUE; - msg_putchar(c); - if (shift) - draw_cmdline(ccline.cmdpos, ccline.cmdlen - ccline.cmdpos); - msg_no_more = FALSE; + } + if (!ui_is_external(kUICmdline)) { + msg_no_more = true; + msg_putchar(c); + if (shift) { + draw_cmdline(ccline.cmdpos, ccline.cmdlen - ccline.cmdpos); + } + msg_no_more = false; + } else { + ccline.special_char = c; + ccline.special_shift = shift; + ui_call_cmdline_special_char(cchar_to_string((char)(c)), shift, + ccline.level); + } cursorcmd(); ui_cursor_shape(); } -/* - * Undo a putcmdline(c, FALSE). - */ +/// Undo a putcmdline(c, FALSE). void unputcmdline(void) { - if (cmd_silent) + if (cmd_silent) { return; - msg_no_more = TRUE; - if (ccline.cmdlen == ccline.cmdpos) + } + msg_no_more = true; + if (ccline.cmdlen == ccline.cmdpos && !ui_is_external(kUICmdline)) { msg_putchar(' '); - else if (has_mbyte) - draw_cmdline(ccline.cmdpos, - (*mb_ptr2len)(ccline.cmdbuff + ccline.cmdpos)); - else - draw_cmdline(ccline.cmdpos, 1); - msg_no_more = FALSE; + } else { + draw_cmdline(ccline.cmdpos, mb_ptr2len(ccline.cmdbuff + ccline.cmdpos)); + } + msg_no_more = false; cursorcmd(); ui_cursor_shape(); } @@ -2871,9 +3004,6 @@ void put_on_cmdline(char_u *str, int len, int redraw) msg_check(); } -static struct cmdline_info prev_ccline; -static int prev_ccline_used = FALSE; - /* * Save ccline, because obtaining the "=" register may execute "normal :cmd" * and overwrite it. But get_cmdline_str() may need it, thus make it @@ -2881,15 +3011,12 @@ static int prev_ccline_used = FALSE; */ static void save_cmdline(struct cmdline_info *ccp) { - if (!prev_ccline_used) { - memset(&prev_ccline, 0, sizeof(struct cmdline_info)); - prev_ccline_used = TRUE; - } - *ccp = prev_ccline; - prev_ccline = ccline; + *ccp = ccline; + ccline.prev_ccline = ccp; ccline.cmdbuff = NULL; ccline.cmdprompt = NULL; ccline.xpc = NULL; + ccline.special_char = NUL; } /* @@ -2897,8 +3024,7 @@ static void save_cmdline(struct cmdline_info *ccp) */ static void restore_cmdline(struct cmdline_info *ccp) { - ccline = prev_ccline; - prev_ccline = *ccp; + ccline = *ccp; } /* @@ -3066,17 +3192,25 @@ static void redrawcmdprompt(void) if (cmd_silent) return; - if (ccline.cmdfirstc != NUL) + if (ui_is_external(kUICmdline)) { + ui_ext_cmdline_show(&ccline); + return; + } + if (ccline.cmdfirstc != NUL) { msg_putchar(ccline.cmdfirstc); + } if (ccline.cmdprompt != NULL) { msg_puts_attr((const char *)ccline.cmdprompt, ccline.cmdattr); ccline.cmdindent = msg_col + (msg_row - cmdline_row) * Columns; - /* do the reverse of set_cmdspos() */ - if (ccline.cmdfirstc != NUL) - --ccline.cmdindent; - } else - for (i = ccline.cmdindent; i > 0; --i) + // do the reverse of set_cmdspos() + if (ccline.cmdfirstc != NUL) { + ccline.cmdindent--; + } + } else { + for (i = ccline.cmdindent; i > 0; i--) { msg_putchar(' '); + } + } } /* @@ -3087,6 +3221,11 @@ void redrawcmd(void) if (cmd_silent) return; + if (ui_is_external(kUICmdline)) { + draw_cmdline(0, ccline.cmdlen); + return; + } + /* when 'incsearch' is set there may be no command line while redrawing */ if (ccline.cmdbuff == NULL) { ui_cursor_goto(cmdline_row, 0); @@ -3130,6 +3269,11 @@ static void cursorcmd(void) if (cmd_silent) return; + if (ui_is_external(kUICmdline)) { + ui_call_cmdline_pos(ccline.cmdpos, ccline.level); + return; + } + if (cmdmsg_rl) { msg_row = cmdline_row + (ccline.cmdspos / (int)(Columns - 1)); msg_col = (int)Columns - (ccline.cmdspos % (int)(Columns - 1)) - 1; @@ -3147,6 +3291,9 @@ static void cursorcmd(void) void gotocmdline(int clr) { + if (ui_is_external(kUICmdline)) { + return; + } msg_start(); if (cmdmsg_rl) msg_col = Columns - 1; @@ -5204,13 +5351,15 @@ int get_history_idx(int histype) */ static struct cmdline_info *get_ccline_ptr(void) { - if ((State & CMDLINE) == 0) + if ((State & CMDLINE) == 0) { return NULL; - if (ccline.cmdbuff != NULL) + } else if (ccline.cmdbuff != NULL) { return &ccline; - if (prev_ccline_used && prev_ccline.cmdbuff != NULL) - return &prev_ccline; - return NULL; + } else if (ccline.prev_ccline && ccline.prev_ccline->cmdbuff != NULL) { + return ccline.prev_ccline; + } else { + return NULL; + } } /* @@ -5646,6 +5795,7 @@ static int ex_window(void) return K_IGNORE; } cmdwin_type = get_cmdline_type(); + cmdwin_level = ccline.level; // Create empty command-line buffer. buf_open_scratch(0, "[Command Line]"); @@ -5698,6 +5848,9 @@ static int ex_window(void) curwin->w_cursor.col = ccline.cmdpos; changed_line_abv_curs(); invalidate_botline(); + if (ui_is_external(kUICmdline)) { + ui_call_cmdline_hide(ccline.level); + } redraw_later(SOME_VALID); // Save the command line info, can be used recursively. @@ -5740,6 +5893,7 @@ static int ex_window(void) // Restore the command line info. restore_cmdline(&save_ccline); cmdwin_type = 0; + cmdwin_level = 0; exmode_active = save_exmode; @@ -5974,3 +6128,4 @@ static void set_search_match(pos_T *t) coladvance((colnr_T)MAXCOL); } } + diff --git a/src/nvim/globals.h b/src/nvim/globals.h index 300e506854..0f3e132bc0 100644 --- a/src/nvim/globals.h +++ b/src/nvim/globals.h @@ -981,9 +981,10 @@ EXTERN int fill_diff INIT(= '-'); EXTERN int km_stopsel INIT(= FALSE); EXTERN int km_startsel INIT(= FALSE); -EXTERN int cedit_key INIT(= -1); /* key value of 'cedit' option */ -EXTERN int cmdwin_type INIT(= 0); /* type of cmdline window or 0 */ -EXTERN int cmdwin_result INIT(= 0); /* result of cmdline window or 0 */ +EXTERN int cedit_key INIT(= -1); ///< key value of 'cedit' option +EXTERN int cmdwin_type INIT(= 0); ///< type of cmdline window or 0 +EXTERN int cmdwin_result INIT(= 0); ///< result of cmdline window or 0 +EXTERN int cmdwin_level INIT(= 0); ///< cmdline recursion level EXTERN char_u no_lines_msg[] INIT(= N_("--No lines in buffer--")); diff --git a/src/nvim/screen.c b/src/nvim/screen.c index 38ecd88ec0..41e900eb4c 100644 --- a/src/nvim/screen.c +++ b/src/nvim/screen.c @@ -345,8 +345,9 @@ void update_screen(int type) if (need_highlight_changed) highlight_changed(); - if (type == CLEAR) { /* first clear screen */ - screenclear(); /* will reset clear_cmdline */ + if (type == CLEAR) { // first clear screen + screenclear(); // will reset clear_cmdline + cmdline_screen_cleared(); // clear external cmdline state type = NOT_VALID; } diff --git a/test/functional/ui/cmdline_spec.lua b/test/functional/ui/cmdline_spec.lua new file mode 100644 index 0000000000..0f8302b036 --- /dev/null +++ b/test/functional/ui/cmdline_spec.lua @@ -0,0 +1,525 @@ +local helpers = require('test.functional.helpers')(after_each) +local Screen = require('test.functional.ui.screen') +local clear, feed, eq = helpers.clear, helpers.feed, helpers.eq +local source = helpers.source +local ok = helpers.ok +local command = helpers.command + +describe('external cmdline', function() + local screen + local last_level = 0 + local cmdline = {} + local block = nil + + before_each(function() + clear() + cmdline, block = {}, nil + screen = Screen.new(25, 5) + screen:attach({rgb=true, ext_cmdline=true}) + screen:set_on_event_handler(function(name, data) + if name == "cmdline_show" then + local content, pos, firstc, prompt, indent, level = unpack(data) + ok(level > 0) + cmdline[level] = {content=content, pos=pos, firstc=firstc, + prompt=prompt, indent=indent} + last_level = level + elseif name == "cmdline_hide" then + local level = data[1] + cmdline[level] = nil + elseif name == "cmdline_special_char" then + local char, shift, level = unpack(data) + cmdline[level].special = {char, shift} + elseif name == "cmdline_pos" then + local pos, level = unpack(data) + cmdline[level].pos = pos + elseif name == "cmdline_block_show" then + block = data[1] + elseif name == "cmdline_block_append" then + block[#block+1] = data[1] + elseif name == "cmdline_block_hide" then + block = nil + end + end) + end) + + after_each(function() + screen:detach() + end) + + local function expect_cmdline(level, expected) + local attr_ids = screen._default_attr_ids + local attr_ignore = screen._default_attr_ignore + local actual = '' + for _, chunk in ipairs(cmdline[level] and cmdline[level].content or {}) do + local attrs, text = chunk[1], chunk[2] + if screen:_equal_attrs(attrs, {}) then + actual = actual..text + else + local attr_id = screen:_get_attr_id(attr_ids, attr_ignore, attrs) + actual = actual..'{' .. attr_id .. ':' .. text .. '}' + end + end + eq(expected, actual) + end + + it('works', function() + feed(':') + screen:expect([[ + ^ | + ~ | + ~ | + ~ | + | + ]], nil, nil, function() + eq(1, last_level) + eq({{ + content = { { {}, "" } }, + firstc = ":", + indent = 0, + pos = 0, + prompt = "" + }}, cmdline) + end) + + feed('sign') + screen:expect([[ + ^ | + ~ | + ~ | + ~ | + | + ]], nil, nil, function() + eq({{ + content = { { {}, "sign" } }, + firstc = ":", + indent = 0, + pos = 4, + prompt = "" + }}, cmdline) + end) + + feed('<Left>') + screen:expect([[ + ^ | + ~ | + ~ | + ~ | + | + ]], nil, nil, function() + eq({{ + content = { { {}, "sign" } }, + firstc = ":", + indent = 0, + pos = 3, + prompt = "" + }}, cmdline) + end) + + feed('<bs>') + screen:expect([[ + ^ | + ~ | + ~ | + ~ | + | + ]], nil, nil, function() + eq({{ + content = { { {}, "sin" } }, + firstc = ":", + indent = 0, + pos = 2, + prompt = "" + }}, cmdline) + end) + + feed('<Esc>') + screen:expect([[ + ^ | + ~ | + ~ | + ~ | + | + ]], nil, nil, function() + eq({}, cmdline) + end) + end) + + it("works with input()", function() + feed(':call input("input", "default")<cr>') + screen:expect([[ + ^ | + ~ | + ~ | + ~ | + | + ]], nil, nil, function() + eq({{ + content = { { {}, "default" } }, + firstc = "", + indent = 0, + pos = 7, + prompt = "input" + }}, cmdline) + end) + feed('<cr>') + screen:expect([[ + ^ | + ~ | + ~ | + ~ | + | + ]], nil, nil, function() + eq({}, cmdline) + end) + + end) + + it("works with special chars and nested cmdline", function() + feed(':xx<c-r>') + screen:expect([[ + ^ | + ~ | + ~ | + ~ | + | + ]], nil, nil, function() + eq({{ + content = { { {}, "xx" } }, + firstc = ":", + indent = 0, + pos = 2, + prompt = "", + special = {'"', true}, + }}, cmdline) + end) + + feed('=') + screen:expect([[ + ^ | + ~ | + ~ | + ~ | + | + ]], nil, nil, function() + eq({{ + content = { { {}, "xx" } }, + firstc = ":", + indent = 0, + pos = 2, + prompt = "", + special = {'"', true}, + },{ + content = { { {}, "" } }, + firstc = "=", + indent = 0, + pos = 0, + prompt = "", + }}, cmdline) + end) + + feed('1+2') + local expectation = {{ + content = { { {}, "xx" } }, + firstc = ":", + indent = 0, + pos = 2, + prompt = "", + special = {'"', true}, + },{ + content = { { {}, "1+2" } }, + firstc = "=", + indent = 0, + pos = 3, + prompt = "", + }} + screen:expect([[ + ^ | + ~ | + ~ | + ~ | + | + ]], nil, nil, function() + eq(expectation, cmdline) + end) + + -- erase information, so we check if it is retransmitted + cmdline = {} + command("redraw!") + -- redraw! forgets cursor position. Be OK with that, as UI should indicate + -- focus is at external cmdline anyway. + screen:expect([[ + | + ~ | + ~ | + ~ | + ^ | + ]], nil, nil, function() + eq(expectation, cmdline) + end) + + + feed('<cr>') + screen:expect([[ + | + ~ | + ~ | + ~ | + ^ | + ]], nil, nil, function() + eq({{ + content = { { {}, "xx3" } }, + firstc = ":", + indent = 0, + pos = 3, + prompt = "", + }}, cmdline) + end) + + feed('<esc>') + screen:expect([[ + ^ | + ~ | + ~ | + ~ | + | + ]], nil, nil, function() + eq({}, cmdline) + end) + end) + + it("works with function definitions", function() + feed(':function Foo()<cr>') + screen:expect([[ + ^ | + ~ | + ~ | + ~ | + | + ]], nil, nil, function() + eq({{ + content = { { {}, "" } }, + firstc = ":", + indent = 2, + pos = 0, + prompt = "", + }}, cmdline) + eq({{{{}, 'function Foo()'}}}, block) + end) + + feed('line1<cr>') + screen:expect([[ + ^ | + ~ | + ~ | + ~ | + | + ]], nil, nil, function() + eq({{{{}, 'function Foo()'}}, + {{{}, ' line1'}}}, block) + end) + + block = {} + command("redraw!") + screen:expect([[ + | + ~ | + ~ | + ~ | + ^ | + ]], nil, nil, function() + eq({{{{}, 'function Foo()'}}, + {{{}, ' line1'}}}, block) + end) + + + feed('endfunction<cr>') + screen:expect([[ + ^ | + ~ | + ~ | + ~ | + | + ]], nil, nil, function() + eq(nil, block) + end) + end) + + it("works with cmdline window", function() + feed(':make') + screen:expect([[ + ^ | + ~ | + ~ | + ~ | + | + ]], nil, nil, function() + eq({{ + content = { { {}, "make" } }, + firstc = ":", + indent = 0, + pos = 4, + prompt = "" + }}, cmdline) + end) + + feed('<c-f>') + screen:expect([[ + | + [No Name] | + :make^ | + [Command Line] | + | + ]], nil, nil, function() + eq({}, cmdline) + end) + + -- nested cmdline + feed(':yank') + screen:expect([[ + | + [No Name] | + :make^ | + [Command Line] | + | + ]], nil, nil, function() + eq({nil, { + content = { { {}, "yank" } }, + firstc = ":", + indent = 0, + pos = 4, + prompt = "" + }}, cmdline) + end) + + cmdline = {} + command("redraw!") + screen:expect([[ + | + [No Name] | + :make | + [Command Line] | + ^ | + ]], nil, nil, function() + eq({nil, { + content = { { {}, "yank" } }, + firstc = ":", + indent = 0, + pos = 4, + prompt = "" + }}, cmdline) + end) + + feed("<c-c>") + screen:expect([[ + | + [No Name] | + :make^ | + [Command Line] | + | + ]], nil, nil, function() + eq({}, cmdline) + end) + + feed("<c-c>") + screen:expect([[ + | + [No Name] | + :make^ | + [Command Line] | + | + ]], nil, nil, function() + eq({{ + content = { { {}, "make" } }, + firstc = ":", + indent = 0, + pos = 4, + prompt = "" + }}, cmdline) + end) + + cmdline = {} + command("redraw!") + screen:expect([[ + | + ~ | + ~ | + ~ | + ^ | + ]], nil, nil, function() + eq({{ + content = { { {}, "make" } }, + firstc = ":", + indent = 0, + pos = 4, + prompt = "" + }}, cmdline) + end) + end) + + it('works with inputsecret()', function() + feed(":call inputsecret('secret:')<cr>abc123") + screen:expect([[ + ^ | + ~ | + ~ | + ~ | + | + ]], nil, nil, function() + eq({{ + content = { { {}, "******" } }, + firstc = "", + indent = 0, + pos = 6, + prompt = "secret:" + }}, cmdline) + end) + end) + + it('works with highlighted cmdline', function() + source([[ + highlight RBP1 guibg=Red + highlight RBP2 guibg=Yellow + highlight RBP3 guibg=Green + highlight RBP4 guibg=Blue + let g:NUM_LVLS = 4 + function RainBowParens(cmdline) + let ret = [] + let i = 0 + let lvl = 0 + while i < len(a:cmdline) + if a:cmdline[i] is# '(' + call add(ret, [i, i + 1, 'RBP' . ((lvl % g:NUM_LVLS) + 1)]) + let lvl += 1 + elseif a:cmdline[i] is# ')' + let lvl -= 1 + call add(ret, [i, i + 1, 'RBP' . ((lvl % g:NUM_LVLS) + 1)]) + endif + let i += 1 + endwhile + return ret + endfunction + map <f5> :let x = input({'prompt':'>','highlight':'RainBowParens'})<cr> + "map <f5> :let x = input({'prompt':'>'})<cr> + ]]) + screen:set_default_attr_ids({ + RBP1={background = Screen.colors.Red}, + RBP2={background = Screen.colors.Yellow}, + RBP3={background = Screen.colors.Green}, + RBP4={background = Screen.colors.Blue}, + EOB={bold = true, foreground = Screen.colors.Blue1}, + ERR={foreground = Screen.colors.Grey100, background = Screen.colors.Red}, + SK={foreground = Screen.colors.Blue}, + PE={bold = true, foreground = Screen.colors.SeaGreen4} + }) + feed('<f5>(a(b)a)') + screen:expect([[ + ^ | + {EOB:~ }| + {EOB:~ }| + {EOB:~ }| + | + ]], nil, nil, function() + expect_cmdline(1, '{RBP1:(}a{RBP2:(}b{RBP2:)}a{RBP1:)}') + end) + end) +end) |