From deae451b26be2d3813ca11712a702e56b361b1b8 Mon Sep 17 00:00:00 2001 From: Luuk van Baal Date: Thu, 21 Nov 2024 01:57:34 +0100 Subject: [PATCH] feat(ui): builtin ext_cmdline completion popupmenu Problem: Enabling ext_cmdline implicitly requires ext_popupmenu to handle cmdline-completion with wildoptions=pum. Solution: Allow marking a window as the ext_cmdline window through nvim_open_win(), including prompt offset. Anchor the cmdline- completion popupmenu to this window. Projected to be used with a default ext_cmdline/message replacement. --- runtime/doc/api.txt | 7 +- runtime/lua/vim/_meta/api.lua | 5 +- runtime/lua/vim/_meta/api_keysets.lua | 1 + src/nvim/api/keysets_defs.h | 1 + src/nvim/api/win_config.c | 21 +++++- src/nvim/buffer_defs.h | 4 +- src/nvim/cmdexpand.c | 13 ++-- src/nvim/ex_getln.c | 3 +- src/nvim/globals.h | 1 + src/nvim/popupmenu.c | 55 ++++++++-------- test/functional/lua/ui_event_spec.lua | 94 +++++++++++++++++++++++++++ test/functional/ui/cmdline_spec.lua | 3 +- 12 files changed, 163 insertions(+), 45 deletions(-) diff --git a/runtime/doc/api.txt b/runtime/doc/api.txt index cb3b2a3f77..f0adb703a0 100644 --- a/runtime/doc/api.txt +++ b/runtime/doc/api.txt @@ -3201,8 +3201,8 @@ nvim_open_win({buffer}, {enter}, {config}) *nvim_open_win()* like a tooltip near the buffer text). • row: Row position in units of "screen cell height", may be fractional. - • col: Column position in units of "screen cell width", may - be fractional. + • col: Column position in units of screen cell width, may be + fractional. • focusable: Enable focus by user actions (wincmds, mouse events). Defaults to true. Non-focusable windows can be entered by |nvim_set_current_win()|, or, when the `mouse` @@ -3292,6 +3292,9 @@ nvim_open_win({buffer}, {enter}, {config}) *nvim_open_win()* • hide: If true the floating window will be hidden. • vertical: Split vertically |:vertical|. • split: Split direction: "left", "right", "above", "below". + • _cmdline_offset: (EXPERIMENTAL) When provided, anchor the + |cmdline-completion| popupmenu to this window, with an + offset in screen cell width. Return: ~ Window handle, or 0 on error diff --git a/runtime/lua/vim/_meta/api.lua b/runtime/lua/vim/_meta/api.lua index b2385197bd..f67e6a61a7 100644 --- a/runtime/lua/vim/_meta/api.lua +++ b/runtime/lua/vim/_meta/api.lua @@ -1771,8 +1771,7 @@ function vim.api.nvim_open_term(buffer, opts) end --- - `row=0` and `col=0` if `anchor` is "SW" or "SE" --- (thus like a tooltip near the buffer text). --- - row: Row position in units of "screen cell height", may be fractional. ---- - col: Column position in units of "screen cell width", may be ---- fractional. +--- - col: Column position in units of screen cell width, may be fractional. --- - focusable: Enable focus by user actions (wincmds, mouse events). --- Defaults to true. Non-focusable windows can be entered by --- `nvim_set_current_win()`, or, when the `mouse` field is set to true, @@ -1858,6 +1857,8 @@ function vim.api.nvim_open_term(buffer, opts) end --- - hide: If true the floating window will be hidden. --- - vertical: Split vertically `:vertical`. --- - split: Split direction: "left", "right", "above", "below". +--- - _cmdline_offset: (EXPERIMENTAL) When provided, anchor the `cmdline-completion` +--- popupmenu to this window, with an offset in screen cell width. --- @return integer # Window handle, or 0 on error function vim.api.nvim_open_win(buffer, enter, config) end diff --git a/runtime/lua/vim/_meta/api_keysets.lua b/runtime/lua/vim/_meta/api_keysets.lua index e11dddb2d3..36f8f0662f 100644 --- a/runtime/lua/vim/_meta/api_keysets.lua +++ b/runtime/lua/vim/_meta/api_keysets.lua @@ -307,6 +307,7 @@ error('Cannot require a meta file') --- @field noautocmd? boolean --- @field fixed? boolean --- @field hide? boolean +--- @field _cmdline_offset? integer --- @class vim.api.keyset.win_text_height --- @field start_row? integer diff --git a/src/nvim/api/keysets_defs.h b/src/nvim/api/keysets_defs.h index 48f5f7246c..2bd0398ab2 100644 --- a/src/nvim/api/keysets_defs.h +++ b/src/nvim/api/keysets_defs.h @@ -131,6 +131,7 @@ typedef struct { Boolean noautocmd; Boolean fixed; Boolean hide; + Integer _cmdline_offset; } Dict(win_config); typedef struct { diff --git a/src/nvim/api/win_config.c b/src/nvim/api/win_config.c index 6f5a9a90c0..01298c83c7 100644 --- a/src/nvim/api/win_config.c +++ b/src/nvim/api/win_config.c @@ -125,8 +125,7 @@ /// - `row=0` and `col=0` if `anchor` is "SW" or "SE" /// (thus like a tooltip near the buffer text). /// - row: Row position in units of "screen cell height", may be fractional. -/// - col: Column position in units of "screen cell width", may be -/// fractional. +/// - col: Column position in units of screen cell width, may be fractional. /// - focusable: Enable focus by user actions (wincmds, mouse events). /// Defaults to true. Non-focusable windows can be entered by /// |nvim_set_current_win()|, or, when the `mouse` field is set to true, @@ -212,6 +211,8 @@ /// - hide: If true the floating window will be hidden. /// - vertical: Split vertically |:vertical|. /// - split: Split direction: "left", "right", "above", "below". +/// - _cmdline_offset: (EXPERIMENTAL) When provided, anchor the |cmdline-completion| +/// popupmenu to this window, with an offset in screen cell width. /// /// @param[out] err Error details, if any /// @@ -295,6 +296,10 @@ Window nvim_open_win(Buffer buffer, Boolean enter, Dict(win_config) *config, Err goto cleanup; } + if (fconfig._cmdline_offset < INT_MAX) { + cmdline_win = wp; + } + // Autocommands may close `wp` or move it to another tabpage, so update and check `tp` after each // event. In each case, `wp` should already be valid in `tp`, so switch_win should not fail. // Also, autocommands may free the `buf` to switch to, so store a bufref to check. @@ -643,6 +648,11 @@ restore_curwin: didset_window_options(win, true); } } + if (fconfig._cmdline_offset < INT_MAX) { + cmdline_win = win; + } else if (win == cmdline_win && fconfig._cmdline_offset == INT_MAX) { + cmdline_win = NULL; + } #undef HAS_KEY_X } @@ -773,6 +783,9 @@ Dict(win_config) nvim_win_get_config(Window window, Arena *arena, Error *err) const char *rel = (wp->w_floating && !config->external ? float_relative_str[config->relative] : ""); PUT_KEY_X(rv, relative, cstr_as_string(rel)); + if (config->_cmdline_offset < INT_MAX) { + PUT_KEY_X(rv, _cmdline_offset, config->_cmdline_offset); + } return rv; } @@ -1320,6 +1333,10 @@ static bool parse_win_config(win_T *wp, Dict(win_config) *config, WinConfig *fco fconfig->hide = config->hide; } + if (HAS_KEY_X(config, _cmdline_offset)) { + fconfig->_cmdline_offset = (int)config->_cmdline_offset; + } + return true; fail: diff --git a/src/nvim/buffer_defs.h b/src/nvim/buffer_defs.h index bb6eef3c29..3731565354 100644 --- a/src/nvim/buffer_defs.h +++ b/src/nvim/buffer_defs.h @@ -958,6 +958,7 @@ typedef struct { bool noautocmd; bool fixed; bool hide; + int _cmdline_offset; } WinConfig; #define WIN_CONFIG_INIT ((WinConfig){ .height = 0, .width = 0, \ @@ -971,7 +972,8 @@ typedef struct { .style = kWinStyleUnused, \ .noautocmd = false, \ .hide = false, \ - .fixed = false }) + .fixed = false, \ + ._cmdline_offset = INT_MAX }) // Structure to store last cursor position and topline. Used by check_lnums() // and reset_lnums(). diff --git a/src/nvim/cmdexpand.c b/src/nvim/cmdexpand.c index d977b20cc4..38d0076ee1 100644 --- a/src/nvim/cmdexpand.c +++ b/src/nvim/cmdexpand.c @@ -365,7 +365,7 @@ static int cmdline_pum_create(CmdlineInfo *ccline, expand_T *xp, char **matches, // Compute the popup menu starting column char *endpos = showtail ? showmatches_gettail(xp->xp_pattern, true) : xp->xp_pattern; - if (ui_has(kUICmdline)) { + if (ui_has(kUICmdline) && cmdline_win == NULL) { compl_startcol = (int)(endpos - ccline->cmdbuff); } else { compl_startcol = cmd_screencol((int)(endpos - ccline->cmdbuff)); @@ -1067,13 +1067,10 @@ int showmatches(expand_T *xp, bool wildmenu) showtail = cmd_showtail; } - bool compl_use_pum = (ui_has(kUICmdline) - ? ui_has(kUIPopupmenu) - : wildmenu && (wop_flags & kOptWopFlagPum)) - || ui_has(kUIWildmenu); - - if (compl_use_pum) { - // cmdline completion popup menu (with wildoptions=pum) + // Cmdline completion popup menu with ext_wildmenu, or wildoptions=pum and + // no ext_cmdline, or ext_popupmenu, or cmdline_win set (_cmdline_offset). + if (ui_has(kUIWildmenu) || ((!ui_has(kUICmdline) || ui_has(kUIPopupmenu) || cmdline_win != NULL) + && wildmenu && (wop_flags & kOptWopFlagPum))) { return cmdline_pum_create(ccline, xp, matches, numMatches, showtail); } diff --git a/src/nvim/ex_getln.c b/src/nvim/ex_getln.c index 85fbdbd20a..7131235024 100644 --- a/src/nvim/ex_getln.c +++ b/src/nvim/ex_getln.c @@ -2947,10 +2947,9 @@ static int cmd_startcol(void) int cmd_screencol(int bytepos) { int m; // maximum column - int col = cmd_startcol(); if (KeyTyped) { - m = Columns * Rows; + m = cmdline_win ? cmdline_win->w_width_inner * cmdline_win->w_height_inner : Columns * Rows; if (m < 0) { // overflow, Columns or Rows at weird value m = MAXCOL; } diff --git a/src/nvim/globals.h b/src/nvim/globals.h index 472b77ccbe..069745de70 100644 --- a/src/nvim/globals.h +++ b/src/nvim/globals.h @@ -747,6 +747,7 @@ EXTERN int cmdwin_level INIT( = 0); ///< cmdline recursion level EXTERN buf_T *cmdwin_buf INIT( = NULL); ///< buffer of cmdline window or NULL EXTERN win_T *cmdwin_win INIT( = NULL); ///< window of cmdline window or NULL EXTERN win_T *cmdwin_old_curwin INIT( = NULL); ///< curwin before opening cmdline window or NULL +EXTERN win_T *cmdline_win INIT( = NULL); ///< window in use by ext_cmdline EXTERN char no_lines_msg[] INIT( = N_("--No lines in buffer--")); diff --git a/src/nvim/popupmenu.c b/src/nvim/popupmenu.c index 5dc9ddfd60..f51a698043 100644 --- a/src/nvim/popupmenu.c +++ b/src/nvim/popupmenu.c @@ -154,11 +154,12 @@ void pum_display(pumitem_T *array, int size, int selected, bool array_changed, i validate_cursor_col(curwin); int above_row = 0; int below_row = cmdline_row; - + win_T *target_win = (State == MODE_CMDLINE) ? cmdline_win : curwin; // wildoptions=pum if (State == MODE_CMDLINE) { - pum_win_row = ui_has(kUICmdline) ? 0 : cmdline_row; - cursor_col = cmd_startcol; + pum_win_row = cmdline_win ? cmdline_win->w_wrow : ui_has(kUICmdline) ? 0 : cmdline_row; + cursor_col = (cmdline_win ? cmdline_win->w_config._cmdline_offset : 0) + cmd_startcol; + cursor_col %= cmdline_win ? cmdline_win->w_width_inner : Columns; pum_anchor_grid = ui_has(kUICmdline) ? -1 : DEFAULT_GRID_HANDLE; } else { // anchor position: the start of the completed word @@ -168,14 +169,16 @@ void pum_display(pumitem_T *array, int size, int selected, bool array_changed, i } else { cursor_col = curwin->w_wcol; } - pum_anchor_grid = (int)curwin->w_grid.target->handle; - pum_win_row += curwin->w_grid.row_offset; - cursor_col += curwin->w_grid.col_offset; - if (!ui_has(kUIMultigrid) && curwin->w_grid.target != &default_grid) { + } + + if (target_win != NULL) { + pum_win_row += target_win->w_grid.row_offset; + cursor_col += target_win->w_grid.col_offset; + if (!ui_has(kUIMultigrid) && target_win->w_grid.target != &default_grid) { pum_anchor_grid = (int)default_grid.handle; - pum_win_row += curwin->w_winrow; - cursor_col += curwin->w_wincol; + pum_win_row += target_win->w_winrow; + cursor_col += target_win->w_wincol; } } @@ -221,16 +224,16 @@ void pum_display(pumitem_T *array, int size, int selected, bool array_changed, i int min_row = 0; int min_col = 0; int max_col = Columns; - int win_start_col = curwin->w_wincol; - int win_end_col = W_ENDCOL(curwin); - if (!(State & MODE_CMDLINE) && ui_has(kUIMultigrid)) { - above_row -= curwin->w_winrow; - below_row = MAX(below_row - curwin->w_winrow, curwin->w_grid.rows); - min_row = -curwin->w_winrow; - min_col = -curwin->w_wincol; - max_col = MAX(Columns - curwin->w_wincol, curwin->w_grid.cols); + int win_start_col = target_win ? target_win->w_wincol : 0; + int win_end_col = target_win ? W_ENDCOL(target_win) : 0; + if (target_win != NULL && ui_has(kUIMultigrid)) { + above_row -= target_win->w_winrow; + below_row = MAX(below_row - target_win->w_winrow, target_win->w_grid.rows); + min_row = -target_win->w_winrow; + min_col = -target_win->w_wincol; + max_col = MAX(Columns - target_win->w_wincol, target_win->w_grid.cols); win_start_col = 0; - win_end_col = curwin->w_grid.cols; + win_end_col = target_win->w_grid.cols; } // Figure out the size and position of the pum. @@ -246,12 +249,12 @@ void pum_display(pumitem_T *array, int size, int selected, bool array_changed, i // pum above "pum_win_row" pum_above = true; - if (State == MODE_CMDLINE) { - // for cmdline pum, no need for context lines + if (State == MODE_CMDLINE && target_win == NULL) { + // For cmdline pum, no need for context lines unless target_win is set context_lines = 0; } else { // Leave two lines of context if possible - context_lines = MIN(2, curwin->w_wrow - curwin->w_cline_row); + context_lines = MIN(2, target_win->w_wrow - target_win->w_cline_row); } if (pum_win_row - min_row >= size + context_lines) { @@ -270,14 +273,14 @@ void pum_display(pumitem_T *array, int size, int selected, bool array_changed, i // pum below "pum_win_row" pum_above = false; - if (State == MODE_CMDLINE) { - // for cmdline pum, no need for context lines + if (State == MODE_CMDLINE && target_win == NULL) { + // for cmdline pum, no need for context lines unless target_win is set context_lines = 0; } else { // Leave two lines of context if possible - validate_cheight(curwin); - int cline_visible_offset = curwin->w_cline_row + - curwin->w_cline_height - curwin->w_wrow; + validate_cheight(target_win); + int cline_visible_offset = target_win->w_cline_row + + target_win->w_cline_height - target_win->w_wrow; context_lines = MIN(3, cline_visible_offset); } diff --git a/test/functional/lua/ui_event_spec.lua b/test/functional/lua/ui_event_spec.lua index f1cf657d78..909189492e 100644 --- a/test/functional/lua/ui_event_spec.lua +++ b/test/functional/lua/ui_event_spec.lua @@ -306,6 +306,100 @@ describe('vim.ui_attach', function() }, }) end) + + it('ext_cmdline completion popupmenu', function() + screen:try_resize(screen._width, 10) + screen:add_extra_attr_ids { [100] = { background = Screen.colors.Black } } + exec_lua([[ + vim.o.wildoptions = 'pum' + local buf = vim.api.nvim_create_buf(false, true) + vim.cmd('call setline(1, range(1, 10))') + _G.win = vim.api.nvim_open_win(buf, false, { + relative = 'editor', + col = 3, + row = 3, + width = 20, + height = 1, + style = 'minimal', + focusable = false, + zindex = 300, + _cmdline_offset = 0, + }) + vim.ui_attach(ns, { ext_cmdline = true }, function(event, content, _, firstc) + if event == 'cmdline_show' then + local prompt = vim.api.nvim_win_get_config(_G.win)._cmdline_offset == 0 + prompt = (prompt and firstc or 'Excommand:') .. content[1][2] + vim.api.nvim_buf_set_lines(buf, -2, -1, false, { prompt }) + vim.api.nvim_win_set_cursor(_G.win, { 1, #prompt }) + vim.api.nvim__redraw({ win = _G.win, cursor = true, flush = true }) + end + return true + end) + vim.api.nvim_set_hl(0, 'Pmenu', {}) + ]]) + feed(':call buf') + screen:expect([[ + 1 | + 2 | + 3 | + 4 :call bufadd^( | + 5 {12: bufadd( }{100: } | + 6 bufexists( {100: } | + 7 buffer_exists( {12: } | + 8 buffer_name( {12: } | + 9 buffer_number( {12: } | + | + ]]) + exec_lua([[ + vim.api.nvim_win_set_config(_G.win, { + relative = 'editor', + col = 0, + row = 1000, + width = 1000, + height = 1, + }) + ]]) + screen:expect([[ + 1 | + 2 | + 3 | + 4 | + 5 {12: bufadd( }{100: } | + 6 bufexists( {100: } | + 7 buffer_exists( {12: } | + 8 buffer_name( {12: } | + 9 buffer_number( {12: } | + :call bufadd^( | + ]]) + feed('') + screen:expect([[ + 1 bufadd( {100: } | + 2 {12: bufexists( }{100: } | + 3 buffer_exists( {100: } | + 4 buffer_name( {100: } | + 5 buffer_number( {100: } | + 6 buflisted( {100: } | + 7 bufload( {12: } | + 8 bufloaded( {12: } | + 9 bufname( {12: } | + :call bufexists^( | + ]]) + -- Test different offset (e.g. for custom prompt) + exec_lua('vim.api.nvim_win_set_config(_G.win, { _cmdline_offset = 9 })') + feed(':call buf') + screen:expect([[ + 1 {12: bufadd( }{100: } | + 2 bufexists( {100: } | + 3 buffer_exists( {100: } | + 4 buffer_name( {100: } | + 5 buffer_number( {100: } | + 6 buflisted( {100: } | + 7 bufload( {12: } | + 8 bufloaded( {12: } | + 9 bufname( {12: } | + Excommand:call bufadd^( | + ]]) + end) end) describe('vim.ui_attach', function() diff --git a/test/functional/ui/cmdline_spec.lua b/test/functional/ui/cmdline_spec.lua index 0221c1e0b0..c4c6df62f9 100644 --- a/test/functional/ui/cmdline_spec.lua +++ b/test/functional/ui/cmdline_spec.lua @@ -623,8 +623,7 @@ local function test_cmdline(linegrid) { 'unplace', '', '', '' }, } - command('set wildmode=full') - command('set wildmenu') + command('set wildmode=full wildmenu wildoptions=pum') screen:set_option('ext_popupmenu', true) feed(':sign ')