Merge pull request #24723 from glepnir/popup

feat(complete): completeopt support popup like vim
This commit is contained in:
bfredl 2023-12-16 12:41:05 +01:00 committed by GitHub
commit 5ed55ff14c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 519 additions and 21 deletions

View File

@ -700,6 +700,21 @@ nvim_chan_send({chan}, {data}) *nvim_chan_send()*
• {chan} id of the channel
• {data} data to write. 8-bit clean: can contain NUL bytes.
nvim_complete_set({index}, {*opts}) *nvim_complete_set()*
Set info for the completion candidate index. if the info was shown in a
window, then the window and buffer ids are returned for further
customization. If the text was not shown, an empty dict is returned.
Parameters: ~
• {index} the completion candidate index
• {opts} Optional parameters.
• info: (string) info text.
Return: ~
Dictionary containing these keys:
• winid: (number) floating window id
• bufnr: (number) buffer id in floating window
nvim_create_buf({listed}, {scratch}) *nvim_create_buf()*
Creates a new, empty, unnamed buffer.

View File

@ -802,6 +802,8 @@ complete_info([{what}]) *complete_info()*
no item is selected when using the <Up> or
<Down> keys)
inserted Inserted string. [NOT IMPLEMENTED YET]
preview_winid Info floating preview window id.
preview_bufnr Info floating preview buffer id.
*complete_info_mode*
mode values are:

View File

@ -252,6 +252,9 @@ The following new APIs and features were added.
• |vim.text.hexencode()| and |vim.text.hexdecode()| convert strings to and
from byte representations.
• 'completeopt' option supports "popup" flags to show extra information in
in floating window.
==============================================================================
CHANGED FEATURES *news-changed*

View File

@ -1516,6 +1516,10 @@ A jump table for the options with a short description can be found at |Q_op|.
select one from the menu. Only works in combination with
"menu" or "menuone".
popup Show extra information about the currently selected
completion in a popup window. Only works in combination
with "menu" or "menuone". Overrides "preview".
*'completeslash'* *'csl'*
'completeslash' 'csl' string (default "")
local to buffer

View File

@ -773,6 +773,16 @@ function vim.api.nvim_command(command) end
--- @return string
function vim.api.nvim_command_output(command) end
--- Set info for the completion candidate index. if the info was shown in a
--- window, then the window and buffer ids are returned for further
--- customization. If the text was not shown, an empty dict is returned.
---
--- @param index integer the completion candidate index
--- @param opts vim.api.keyset.complete_set Optional parameters.
--- • info: (string) info text.
--- @return table<string,any>
function vim.api.nvim_complete_set(index, opts) end
--- Create or get an autocommand group `autocmd-groups`.
--- To get an existing group id, do:
---

View File

@ -68,6 +68,9 @@ error('Cannot require a meta file')
--- @class vim.api.keyset.cmd_opts
--- @field output? boolean
--- @class vim.api.keyset.complete_set
--- @field info? string
--- @class vim.api.keyset.context
--- @field types? any[]

View File

@ -1061,6 +1061,10 @@ vim.bo.cfu = vim.bo.completefunc
--- select one from the menu. Only works in combination with
--- "menu" or "menuone".
---
--- popup Show extra information about the currently selected
--- completion in a popup window. Only works in combination
--- with "menu" or "menuone". Overrides "preview".
---
--- @type string
vim.o.completeopt = "menu,preview"
vim.o.cot = vim.o.completeopt

View File

@ -1024,6 +1024,8 @@ function vim.fn.complete_check() end
--- no item is selected when using the <Up> or
--- <Down> keys)
--- inserted Inserted string. [NOT IMPLEMENTED YET]
--- preview_winid Info floating preview window id.
--- preview_bufnr Info floating preview buffer id.
---
--- *complete_info_mode*
--- mode values are:

View File

@ -346,3 +346,8 @@ typedef struct {
LuaRef on_input;
Boolean force_crlf;
} Dict(open_term);
typedef struct {
OptionalKeys is_set__complete_set_;
String info;
} Dict(complete_set);

View File

@ -2300,3 +2300,29 @@ void nvim_error_event(uint64_t channel_id, Integer lvl, String data)
// if we fork nvim processes as async workers
ELOG("async error on channel %" PRId64 ": %s", channel_id, data.size ? data.data : "");
}
/// Set info for the completion candidate index.
/// if the info was shown in a window, then the
/// window and buffer ids are returned for further
/// customization. If the text was not shown, an
/// empty dict is returned.
///
/// @param index the completion candidate index
/// @param opts Optional parameters.
/// - info: (string) info text.
/// @return Dictionary containing these keys:
/// - winid: (number) floating window id
/// - bufnr: (number) buffer id in floating window
Dictionary nvim_complete_set(Integer index, Dict(complete_set) *opts)
FUNC_API_SINCE(12)
{
Dictionary rv = ARRAY_DICT_INIT;
if (HAS_KEY(opts, complete_set, info)) {
win_T *wp = pum_set_info((int)index, opts->info.data);
if (wp) {
PUT(rv, "winid", WINDOW_OBJ(wp->handle));
PUT(rv, "bufnr", BUFFER_OBJ(wp->w_buffer->handle));
}
}
return rv;
}

View File

@ -1287,6 +1287,7 @@ struct window_S {
ScreenGrid w_grid_alloc; // the grid specific to the window
bool w_pos_changed; // true if window position changed
bool w_floating; ///< whether the window is floating
bool w_float_is_info; // the floating window is info float
FloatConfig w_float_config;
// w_fraction is the fractional row of the cursor within the window, from

View File

@ -1373,6 +1373,8 @@ M.funcs = {
no item is selected when using the <Up> or
<Down> keys)
inserted Inserted string. [NOT IMPLEMENTED YET]
preview_winid Info floating preview window id.
preview_bufnr Info floating preview buffer id.
*complete_info_mode*
mode values are:

View File

@ -63,6 +63,7 @@
#include "nvim/undo.h"
#include "nvim/vim_defs.h"
#include "nvim/window.h"
#include "nvim/winfloat.h"
// Definitions used for CTRL-X submode.
// Note: If you change CTRL-X submode, you must also maintain ctrl_x_msgs[]
@ -1290,6 +1291,28 @@ void ins_compl_show_pum(void)
}
}
/// used for set or update info
void compl_set_info(int pum_idx)
{
compl_T *comp = compl_first_match;
char *pum_text = compl_match_array[pum_idx].pum_text;
while (comp != NULL) {
if (pum_text == comp->cp_str
|| pum_text == comp->cp_text[CPT_ABBR]) {
comp->cp_text[CPT_INFO] = compl_match_array[pum_idx].pum_info;
// if comp is current match update completed_item value
if (comp == compl_curr_match) {
dict_T *dict = ins_compl_dict_alloc(compl_curr_match);
set_vim_var_dict(VV_COMPLETED_ITEM, dict);
}
break;
}
comp = comp->cp_next;
}
}
#define DICT_FIRST (1) ///< use just first element in "dict"
#define DICT_EXACT (2) ///< "dict" is the exact name of a file
@ -2813,6 +2836,11 @@ static void get_complete_info(list_T *what_list, dict_T *retdict)
ret = tv_dict_add_nr(retdict, S_LEN("selected"),
(compl_curr_match != NULL)
? compl_curr_match->cp_number - 1 : -1);
win_T *wp = win_float_find_preview();
if (wp != NULL) {
tv_dict_add_nr(retdict, S_LEN("preview_winid"), wp->handle);
tv_dict_add_nr(retdict, S_LEN("preview_bufnr"), wp->w_buffer->handle);
}
}
(void)ret;

View File

@ -1450,6 +1450,10 @@ return {
noselect Do not select a match in the menu, force the user to
select one from the menu. Only works in combination with
"menu" or "menuone".
popup Show extra information about the currently selected
completion in a popup window. Only works in combination
with "menu" or "menuone". Overrides "preview".
]=],
expand_cb = 'expand_set_completeopt',
full_name = 'completeopt',

View File

@ -116,7 +116,7 @@ static char *(p_fdm_values[]) = { "manual", "expr", "marker", "indent",
"syntax", "diff", NULL };
static char *(p_fcl_values[]) = { "all", NULL };
static char *(p_cot_values[]) = { "menu", "menuone", "longest", "preview", "noinsert", "noselect",
NULL };
"popup", NULL };
#ifdef BACKSLASH_IN_FILENAME
static char *(p_csl_values[]) = { "slash", "backslash", NULL };
#endif

View File

@ -8,8 +8,11 @@
#include "nvim/api/private/defs.h"
#include "nvim/api/private/helpers.h"
#include "nvim/api/vim.h"
#include "nvim/ascii_defs.h"
#include "nvim/autocmd.h"
#include "nvim/buffer.h"
#include "nvim/buffer_defs.h"
#include "nvim/charset.h"
#include "nvim/drawscreen.h"
#include "nvim/eval/typval.h"
@ -29,6 +32,8 @@
#include "nvim/move.h"
#include "nvim/option.h"
#include "nvim/option_vars.h"
#include "nvim/optionstr.h"
#include "nvim/plines.h"
#include "nvim/popupmenu.h"
#include "nvim/pos_defs.h"
#include "nvim/state_defs.h"
@ -37,6 +42,7 @@
#include "nvim/ui_compositor.h"
#include "nvim/vim_defs.h"
#include "nvim/window.h"
#include "nvim/winfloat.h"
static pumitem_T *pum_array = NULL; // items of displayed pum
static int pum_size; // nr of items in "pum_array"
@ -654,6 +660,142 @@ void pum_redraw(void)
}
}
/// create a floting preview window for info
/// @return NULL when no enough room to show
static win_T *pum_create_float_preview(bool enter)
{
FloatConfig config = FLOAT_CONFIG_INIT;
config.relative = kFloatRelativeEditor;
// when pum_above is SW otherwise is NW
config.anchor = pum_above ? kFloatAnchorSouth : 0;
int col = pum_col + pum_width + pum_scrollbar + 1;
// TODO(glepnir): support config align border by using completepopup
// align menu
config.row = pum_row;
int right_extra = Columns - col;
if (right_extra > 0) {
config.width = right_extra;
config.col = col - 1;
} else if (pum_col - 2 > 0) {
config.width = pum_col - 2;
config.col = pum_col - config.width - 1;
} else {
return NULL;
}
config.height = pum_height;
config.style = kWinStyleMinimal;
config.hide = true;
Error err = ERROR_INIT;
win_T *wp = win_new_float(NULL, true, config, &err);
// TODO(glepnir): remove win_enter usage
if (enter) {
win_enter(wp, false);
}
// create a new buffer
Buffer b = nvim_create_buf(false, true, &err);
if (!b) {
win_free(wp, NULL);
return NULL;
}
buf_T *buf = find_buffer_by_handle(b, &err);
set_string_option_direct_in_buf(buf, kOptBufhidden, "wipe", OPT_FREE | OPT_LOCAL, 0);
wp->w_float_is_info = true;
wp->w_p_diff = false;
buf->b_p_bl = false;
win_set_buf(wp, buf, true, &err);
return wp;
}
/// set info text to preview buffer.
static void pum_preview_set_text(buf_T *buf, char *info, linenr_T *lnum, int *max_width)
{
for (char *p = info; *p != NUL;) {
int text_width = 0;
char *e = vim_strchr(p, '\n');
if (e == NULL) {
ml_append_buf(buf, (*lnum)++, p, 0, false);
text_width = (int)mb_string2cells(p);
if (text_width > *max_width) {
*max_width = text_width;
}
break;
}
*e = NUL;
ml_append_buf(buf, (*lnum)++, p, (int)(e - p + 1), false);
text_width = (int)mb_string2cells(p);
if (text_width > *max_width) {
*max_width = text_width;
}
*e = '\n';
p = e + 1;
}
// delete the empty last line
ml_delete_buf(buf, buf->b_ml.ml_line_count, false);
}
/// adjust floating preview window width and height
static void pum_adjust_float_position(win_T *wp, int height, int width)
{
// when floating window in right and right no enough room to show
// but left has enough room, adjust floating window to left.
if (wp->w_float_config.width < width && wp->w_float_config.col > pum_col) {
if ((pum_col - 2) > width) {
wp->w_float_config.width = width;
wp->w_float_config.col = pum_col - width - 1;
}
}
wp->w_float_config.width = MIN(wp->w_float_config.width, width);
wp->w_float_config.height = MIN(Rows, height);
wp->w_float_config.hide = false;
win_config_float(wp, wp->w_float_config);
}
/// used in nvim_complete_set
win_T *pum_set_info(int pum_idx, char *info)
{
if (!pum_is_visible || pum_idx < 0 || pum_idx > pum_size) {
return NULL;
}
pum_array[pum_idx].pum_info = xstrdup(info);
compl_set_info(pum_idx);
bool use_float = strstr(p_cot, "popup") != NULL ? true : false;
if (pum_idx != pum_selected || !use_float) {
return NULL;
}
block_autocmds();
RedrawingDisabled++;
no_u_sync++;
win_T *wp = win_float_find_preview();
if (wp == NULL) {
wp = pum_create_float_preview(false);
// no enough room to show
if (!wp) {
return NULL;
}
} else {
// clean exist buffer
while (!buf_is_empty(wp->w_buffer)) {
ml_delete_buf(wp->w_buffer, (linenr_T)1, false);
}
}
no_u_sync--;
RedrawingDisabled--;
linenr_T lnum = 0;
int max_info_width = 0;
pum_preview_set_text(wp->w_buffer, info, &lnum, &max_info_width);
redraw_later(wp, UPD_SOME_VALID);
if (wp->w_p_wrap) {
lnum += plines_win(wp, lnum, true);
}
pum_adjust_float_position(wp, lnum, max_info_width);
unblock_autocmds();
return wp;
}
/// Set the index of the currently selected item. The menu will scroll when
/// necessary. When "n" is out of range don't scroll.
/// This may be repeated when the preview window is used:
@ -670,6 +812,7 @@ static bool pum_set_selected(int n, int repeat)
{
int resized = false;
int context = pum_height / 2;
int prev_selected = pum_selected;
pum_selected = n;
@ -718,6 +861,10 @@ static bool pum_set_selected(int n, int repeat)
pum_first = pum_selected + context - pum_height + 1;
}
}
// adjust for the number of lines displayed
if (pum_first > pum_size - pum_height) {
pum_first = pum_size - pum_height;
}
// Show extra info in the preview window if there is something and
// 'completeopt' contains "preview".
@ -730,6 +877,11 @@ static bool pum_set_selected(int n, int repeat)
&& (vim_strchr(p_cot, 'p') != NULL)) {
win_T *curwin_save = curwin;
tabpage_T *curtab_save = curtab;
bool use_float = strstr(p_cot, "popup") != NULL ? true : false;
if (use_float) {
block_autocmds();
}
// Open a preview window. 3 lines by default. Prefer
// 'previewheight' if set and smaller.
@ -742,12 +894,26 @@ static bool pum_set_selected(int n, int repeat)
// Prevent undo sync here, if an autocommand syncs undo weird
// things can happen to the undo tree.
no_u_sync++;
if (!use_float) {
resized = prepare_tagpreview(false);
} else {
win_T *wp = win_float_find_preview();
if (wp) {
win_enter(wp, false);
} else {
wp = pum_create_float_preview(true);
if (wp) {
resized = true;
}
}
}
no_u_sync--;
RedrawingDisabled--;
g_do_tagpreview = 0;
if (curwin->w_p_pvw) {
if (curwin->w_p_pvw || curwin->w_float_is_info) {
int res = OK;
if (!resized
&& (curbuf->b_nwindows == 1)
@ -777,22 +943,11 @@ static bool pum_set_selected(int n, int repeat)
if (res == OK) {
linenr_T lnum = 0;
for (char *p = pum_array[pum_selected].pum_info; *p != NUL;) {
char *e = vim_strchr(p, '\n');
if (e == NULL) {
ml_append(lnum++, p, 0, false);
break;
}
*e = NUL;
ml_append(lnum++, p, (int)(e - p + 1), false);
*e = '\n';
p = e + 1;
}
int max_info_width = 0;
pum_preview_set_text(curbuf, pum_array[pum_selected].pum_info, &lnum, &max_info_width);
// Increase the height of the preview window to show the
// text, but no more than 'previewheight' lines.
if (repeat == 0) {
if (repeat == 0 && !use_float) {
if (lnum > p_pvh) {
lnum = (linenr_T)p_pvh;
}
@ -805,9 +960,22 @@ static bool pum_set_selected(int n, int repeat)
curbuf->b_changed = false;
curbuf->b_p_ma = false;
if (pum_selected != prev_selected) {
curwin->w_topline = 1;
} else if (curwin->w_topline > curbuf->b_ml.ml_line_count) {
curwin->w_topline = curbuf->b_ml.ml_line_count;
}
curwin->w_cursor.lnum = 1;
curwin->w_cursor.col = 0;
if (use_float) {
if (curwin->w_p_wrap) {
lnum += plines_win(curwin, lnum, true);
}
// adjust floting window by actually height and max info text width
pum_adjust_float_position(curwin, lnum, max_info_width);
}
if ((curwin != curwin_save && win_valid(curwin_save))
|| (curtab != curtab_save && valid_tabpage(curtab_save))) {
if (curtab != curtab_save && valid_tabpage(curtab_save)) {
@ -829,7 +997,7 @@ static bool pum_set_selected(int n, int repeat)
// update the view on the buffer. Only go back to
// the window when needed, otherwise it will always be
// redrawn.
if (resized) {
if (resized && win_valid(curwin_save)) {
no_u_sync++;
win_enter(curwin_save, true);
no_u_sync--;
@ -858,6 +1026,10 @@ static bool pum_set_selected(int n, int repeat)
}
}
}
if (use_float) {
unblock_autocmds();
}
}
}
@ -892,6 +1064,10 @@ void pum_check_clear(void)
}
pum_is_drawn = false;
pum_external = false;
win_T *wp = win_float_find_preview();
if (wp != NULL) {
win_close(wp, false, false);
}
}
}

View File

@ -2,6 +2,7 @@
#include <stdbool.h>
#include "nvim/buffer_defs.h"
#include "nvim/eval/typval_defs.h" // IWYU pragma: keep
#include "nvim/grid_defs.h"
#include "nvim/macros_defs.h"

View File

@ -4978,7 +4978,7 @@ void free_wininfo(wininfo_T *wip, buf_T *bp)
/// Remove window 'wp' from the window list and free the structure.
///
/// @param tp tab page "win" is in, NULL for current
static void win_free(win_T *wp, tabpage_T *tp)
void win_free(win_T *wp, tabpage_T *tp)
{
pmap_del(int)(&window_handles, wp->handle, NULL);
clearFolding(wp);

View File

@ -286,3 +286,13 @@ bool win_float_valid(const win_T *win)
}
return false;
}
win_T *win_float_find_preview(void)
{
for (win_T *wp = lastwin; wp && wp->w_floating; wp = wp->w_prev) {
if (wp->w_float_is_info) {
return wp;
}
}
return NULL;
}

View File

@ -1431,7 +1431,7 @@ describe('builtin popupmenu', function()
feed('<C-N>')
screen:expect([[
1info |
|
{1:~ }|
{1:~ }|
{3:[Scratch] [Preview] }|
one^ |
@ -1469,6 +1469,208 @@ describe('builtin popupmenu', function()
end)
end
describe("floating window preview #popup", function()
it('pum popup preview', function()
--row must > 10
screen:try_resize(30, 11)
exec([[
funct Omni_test(findstart, base)
if a:findstart
return col(".") - 1
endif
return [#{word: "one", info: "1info"}, #{word: "two", info: "2info"}, #{word: "three"}]
endfunc
set omnifunc=Omni_test
set completeopt=menu,popup
funct Set_info()
let comp_info = complete_info()
if comp_info['selected'] == 2
call nvim_complete_set(comp_info['selected'], {"info": "3info"})
endif
endfunc
autocmd CompleteChanged * call Set_info()
]])
feed('Gi<C-x><C-o>')
--floating preview in right
if multigrid then
screen:expect{grid=[[
## grid 1
[2:------------------------------]|
[2:------------------------------]|
[2:------------------------------]|
[2:------------------------------]|
[2:------------------------------]|
[2:------------------------------]|
[2:------------------------------]|
[2:------------------------------]|
[2:------------------------------]|
[2:------------------------------]|
[3:------------------------------]|
## grid 2
one^ |
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
## grid 3
{2:-- }{5:match 1 of 3} |
## grid 4
{n:1info}|
{n: }|
## grid 5
{s:one }|
{n:two }|
{n:three }|
]], float_pos={
[5] = {{id = -1}, "NW", 2, 1, 0, false, 100};
[4] = {{id = 1001}, "NW", 1, 1, 15, true, 50};
}}
else
screen:expect{grid=[[
one^ |
{s:one }{n:1info}{1: }|
{n:two }{1: }|
{n:three }{1: }|
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
{2:-- }{5:match 1 of 3} |
]], unchanged = true}
end
-- test nvim_complete_set_info
feed('<C-N><C-N>')
helpers.sleep(10)
if multigrid then
screen:expect{grid=[[
## grid 1
[2:------------------------------]|
[2:------------------------------]|
[2:------------------------------]|
[2:------------------------------]|
[2:------------------------------]|
[2:------------------------------]|
[2:------------------------------]|
[2:------------------------------]|
[2:------------------------------]|
[2:------------------------------]|
[3:------------------------------]|
## grid 2
three^ |
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
## grid 3
{2:-- }{5:match 3 of 3} |
## grid 4
{n:3info}|
{n: }|
## grid 5
{n:one }|
{n:two }|
{s:three }|
]], float_pos={
[5] = {{id = -1}, "NW", 2, 1, 0, false, 100};
[4] = {{id = 1001}, "NW", 1, 1, 15, true, 50};
}}
else
screen:expect{grid=[[
three^ |
{n:one 3info}{1: }|
{n:two }{1: }|
{s:three }{1: }|
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
{2:-- }{5:match 3 of 3} |
]]}
end
-- make sure info has set
feed('<C-y>')
eq('3info', exec_lua('return vim.v.completed_item.info'))
-- preview in left
feed('<ESC>cc')
insert(('test'):rep(5))
feed('i<C-x><C-o>')
if multigrid then
screen:expect{grid=[[
## grid 1
[2:------------------------------]|
[2:------------------------------]|
[2:------------------------------]|
[2:------------------------------]|
[2:------------------------------]|
[2:------------------------------]|
[2:------------------------------]|
[2:------------------------------]|
[2:------------------------------]|
[2:------------------------------]|
[3:------------------------------]|
## grid 2
itesttesttesttesttesone^t |
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
## grid 3
{2:-- }{5:match 1 of 3} |
## grid 5
{s: one }|
{n: two }|
{n: three }|
## grid 6
{n:1info}|
{n: }|
]], float_pos={
[5] = {{id = -1}, "NW", 2, 1, 19, false, 100};
[6] = {{id = 1002}, "NW", 1, 1, 1, true, 50};
}, win_viewport={
[2] = {win = {id = 1000}, topline = 0, botline = 2, curline = 0, curcol = 23, linecount = 1, sum_scroll_delta = 0};
[6] = {win = {id = 1002}, topline = 0, botline = 2, curline = 0, curcol = 0, linecount = 1, sum_scroll_delta = 0};
}}
else
screen:expect{grid=[[
itesttesttesttesttesone^t |
{1:~}{n:1info}{1: }{s: one }{1: }|
{1:~}{n: }{1: }{n: two }{1: }|
{1:~ }{n: three }{1: }|
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
{1:~ }|
{2:-- }{5:match 1 of 3} |
]]}
end
end)
end)
it('with vsplits', function()
screen:try_resize(32, 8)
insert('aaa aab aac\n')