mirror of
https://github.com/neovim/neovim.git
synced 2026-07-17 14:41:33 +00:00
feat(completion): completeopt=preselect, LSP CompletionItem.preselect #36613
Problem: LSP CompletionItem.preselect is not supported. https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#completionClientCapabilities Solution: - Add "preselect" field to complete-items and "preselect" flag to 'completeopt'. - Set preselectSupport=true in LSP client capabilities.
This commit is contained in:
@@ -1248,6 +1248,10 @@ items:
|
||||
customization of ctermfg and guifg properties for the
|
||||
completion kind
|
||||
match See "matches" in |complete_info()|.
|
||||
preselect when non-zero this item is selected by default in the
|
||||
popup menu. Only effective when 'completeopt' contains
|
||||
"preselect". If multiple items have "preselect" set,
|
||||
the first one is used.
|
||||
|
||||
All of these except "icase", "equal", "dup" and "empty" must be a string. If
|
||||
an item does not meet these requirements then an error message is given and
|
||||
|
||||
@@ -144,6 +144,8 @@ LSP
|
||||
• |vim.lsp.buf.declaration()|, |vim.lsp.buf.definition()|, |vim.lsp.buf.definition()|,
|
||||
and |vim.lsp.buf.implementation()| now follows 'switchbuf'.
|
||||
• Support for nested snippets.
|
||||
• LSP completion now supports `CompletionItem.preselect`. Requires
|
||||
'completeopt' to include "preselect".
|
||||
|
||||
LUA
|
||||
|
||||
|
||||
@@ -1698,8 +1698,12 @@ A jump table for the options with a short description can be found at |Q_op|.
|
||||
completion in the preview window. Only works in
|
||||
combination with "menu" or "menuone".
|
||||
|
||||
Only "fuzzy", "longest", "popup", "preinsert" and "preview" have an
|
||||
effect when 'autocomplete' is enabled.
|
||||
preselect Select the completion item that has the "preselect"
|
||||
attribute set. If both "noselect" and "preselect" are present,
|
||||
"preselect" takes precedence.
|
||||
|
||||
Only "fuzzy", "longest", "popup", "preinsert", "preselect" and
|
||||
"preview" have an effect when 'autocomplete' is enabled.
|
||||
|
||||
This option does not apply to |cmdline-completion|. See 'wildoptions'
|
||||
for that.
|
||||
|
||||
@@ -396,6 +396,8 @@ Options:
|
||||
- 'ambiwidth' cannot be set to empty.
|
||||
- 'autoread' works in the terminal (if it supports "focus" events)
|
||||
- 'background' cannot be set to empty.
|
||||
- 'completeopt' flag "preselect" selects first completion item that has the
|
||||
"preselect" attribute set.
|
||||
- 'cpoptions' flags: |cpo-_|
|
||||
- 'eadirection' cannot be set to empty.
|
||||
- 'exrc' searches for ".nvim.lua", ".nvimrc", or ".exrc" files. The
|
||||
|
||||
8
runtime/lua/vim/_meta/options.lua
generated
8
runtime/lua/vim/_meta/options.lua
generated
@@ -1218,8 +1218,12 @@ vim.go.cia = vim.go.completeitemalign
|
||||
--- completion in the preview window. Only works in
|
||||
--- combination with "menu" or "menuone".
|
||||
---
|
||||
--- Only "fuzzy", "longest", "popup", "preinsert" and "preview" have an
|
||||
--- effect when 'autocomplete' is enabled.
|
||||
--- preselect Select the completion item that has the "preselect"
|
||||
--- attribute set. If both "noselect" and "preselect" are present,
|
||||
--- "preselect" takes precedence.
|
||||
---
|
||||
--- Only "fuzzy", "longest", "popup", "preinsert", "preselect" and
|
||||
--- "preview" have an effect when 'autocomplete' is enabled.
|
||||
---
|
||||
--- This option does not apply to `cmdline-completion`. See 'wildoptions'
|
||||
--- for that.
|
||||
|
||||
@@ -441,6 +441,7 @@ function M._lsp_to_complete_items(
|
||||
empty = 1,
|
||||
abbr_hlgroup = hl_group,
|
||||
kind_hlgroup = kind_hlgroup,
|
||||
preselect = item.preselect,
|
||||
user_data = {
|
||||
nvim = {
|
||||
lsp = {
|
||||
|
||||
@@ -474,7 +474,7 @@ function protocol.make_client_capabilities()
|
||||
completionItem = {
|
||||
snippetSupport = true,
|
||||
commitCharactersSupport = false,
|
||||
preselectSupport = false,
|
||||
preselectSupport = true,
|
||||
deprecatedSupport = true,
|
||||
documentationFormat = { constants.MarkupKind.Markdown, constants.MarkupKind.PlainText },
|
||||
insertReplaceSupport = true,
|
||||
|
||||
@@ -173,6 +173,7 @@ struct compl_S {
|
||||
///< cp_flags has CP_FREE_FNAME
|
||||
int cp_flags; ///< CP_ values
|
||||
int cp_number; ///< sequence number
|
||||
bool cp_preselect; ///< preselect item
|
||||
int cp_score; ///< fuzzy match score or proximity score
|
||||
bool cp_in_match_array; ///< collected by compl_match_array
|
||||
int cp_user_abbr_hlattr; ///< highlight attribute for abbr
|
||||
@@ -223,6 +224,7 @@ static compl_T *compl_first_match = NULL;
|
||||
static compl_T *compl_curr_match = NULL;
|
||||
static compl_T *compl_shown_match = NULL;
|
||||
static compl_T *compl_old_match = NULL;
|
||||
static compl_T *compl_preselect_match = NULL;
|
||||
|
||||
/// list used to store the compl_T which have the max score
|
||||
static compl_T **compl_best_matches = NULL;
|
||||
@@ -847,7 +849,8 @@ int ins_compl_add_infercase(char *str_arg, int len, bool icase, char *fname, Dir
|
||||
flags |= CP_ICASE;
|
||||
}
|
||||
|
||||
int res = ins_compl_add(str, len, fname, NULL, false, NULL, dir, flags, false, NULL, score);
|
||||
int res = ins_compl_add(str, len, fname, NULL, false, NULL, dir, flags, false, NULL, score,
|
||||
false);
|
||||
xfree(tofree);
|
||||
return res;
|
||||
}
|
||||
@@ -916,7 +919,8 @@ bool ins_compl_preinsert_longest(void)
|
||||
/// returned in case of error.
|
||||
static int ins_compl_add(char *const str, int len, char *const fname, char *const *const cptext,
|
||||
const bool cptext_allocated, typval_T *user_data, const Direction cdir,
|
||||
int flags_arg, const bool adup, const int *user_hl, const int score)
|
||||
int flags_arg, const bool adup, const int *user_hl, const int score,
|
||||
bool preselect)
|
||||
FUNC_ATTR_NONNULL_ARG(1)
|
||||
{
|
||||
compl_T *match;
|
||||
@@ -966,6 +970,10 @@ static int ins_compl_add(char *const str, int len, char *const fname, char *cons
|
||||
match = xcalloc(1, sizeof(compl_T));
|
||||
match->cp_number = flags & CP_ORIGINAL_TEXT ? 0 : -1;
|
||||
match->cp_str = cbuf_to_string(str, (size_t)len);
|
||||
match->cp_preselect = preselect;
|
||||
if (preselect && compl_preselect_match == NULL) {
|
||||
compl_preselect_match = match;
|
||||
}
|
||||
|
||||
// match-fname is:
|
||||
// - compl_curr_match->cp_fname if it is a string equal to fname.
|
||||
@@ -1214,7 +1222,7 @@ static void ins_compl_add_matches(int num_matches, char **matches, int icase)
|
||||
for (int i = 0; i < num_matches && add_r != FAIL; i++) {
|
||||
add_r = ins_compl_add(matches[i], -1, NULL, NULL, false, NULL, dir,
|
||||
CP_FAST | (icase ? CP_ICASE : 0), false, NULL,
|
||||
FUZZY_SCORE_NONE);
|
||||
FUZZY_SCORE_NONE, false);
|
||||
if (add_r == OK) {
|
||||
// If dir was BACKWARD then honor it just once.
|
||||
dir = FORWARD;
|
||||
@@ -1627,6 +1635,11 @@ static int ins_compl_build_pum(void)
|
||||
shown_match_ok = true;
|
||||
}
|
||||
}
|
||||
if (comp == compl_preselect_match) {
|
||||
cur = i;
|
||||
compl_shown_match = comp;
|
||||
shown_match_ok = true;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
@@ -2074,6 +2087,7 @@ static void ins_compl_free(void)
|
||||
} while (compl_curr_match != NULL && !is_first_match(compl_curr_match));
|
||||
compl_first_match = compl_curr_match = NULL;
|
||||
compl_shown_match = NULL;
|
||||
compl_preselect_match = NULL;
|
||||
compl_old_match = NULL;
|
||||
}
|
||||
|
||||
@@ -3283,6 +3297,7 @@ static int ins_compl_add_tv(typval_T *const tv, const Direction dir, bool fast)
|
||||
bool dup = false;
|
||||
bool empty = false;
|
||||
int flags = fast ? CP_FAST : 0;
|
||||
bool preselect = false;
|
||||
char *(cptext[CPT_COUNT]);
|
||||
char *user_abbr_hlname = NULL;
|
||||
char *user_kind_hlname = NULL;
|
||||
@@ -3310,6 +3325,7 @@ static int ins_compl_add_tv(typval_T *const tv, const Direction dir, bool fast)
|
||||
}
|
||||
dup = (bool)tv_dict_get_number(tv->vval.v_dict, "dup");
|
||||
empty = (bool)tv_dict_get_number(tv->vval.v_dict, "empty");
|
||||
preselect = (bool)tv_dict_get_number(tv->vval.v_dict, "preselect");
|
||||
if (tv_dict_get_string(tv->vval.v_dict, "equal", false) != NULL
|
||||
&& tv_dict_get_number(tv->vval.v_dict, "equal")) {
|
||||
flags |= CP_EQUAL;
|
||||
@@ -3324,7 +3340,7 @@ static int ins_compl_add_tv(typval_T *const tv, const Direction dir, bool fast)
|
||||
return FAIL;
|
||||
}
|
||||
int status = ins_compl_add((char *)word, -1, NULL, cptext, true,
|
||||
&user_data, dir, flags, dup, user_hl, FUZZY_SCORE_NONE);
|
||||
&user_data, dir, flags, dup, user_hl, FUZZY_SCORE_NONE, preselect);
|
||||
if (status != OK) {
|
||||
tv_clear(&user_data);
|
||||
}
|
||||
@@ -3420,7 +3436,7 @@ static void set_completion(colnr_T startcol, list_T *list)
|
||||
}
|
||||
if (ins_compl_add(compl_orig_text.data, (int)compl_orig_text.size,
|
||||
NULL, NULL, false, NULL, 0,
|
||||
flags | CP_FAST, false, NULL, FUZZY_SCORE_NONE) != OK) {
|
||||
flags | CP_FAST, false, NULL, FUZZY_SCORE_NONE, false) != OK) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3436,7 +3452,10 @@ static void set_completion(colnr_T startcol, list_T *list)
|
||||
|
||||
compl_curr_match = compl_first_match;
|
||||
bool no_select = compl_no_select || compl_longest;
|
||||
if (compl_no_insert || no_select) {
|
||||
if ((get_cot_flags() & kOptCotFlagPreselect) && compl_preselect_match && !no_select) {
|
||||
compl_curr_match = compl_preselect_match->cp_prev;
|
||||
ins_complete(Ctrl_N, false);
|
||||
} else if (compl_no_insert || no_select) {
|
||||
ins_complete(K_DOWN, false);
|
||||
if (no_select) {
|
||||
ins_complete(K_UP, false);
|
||||
@@ -4117,7 +4136,7 @@ static void get_next_filename_completion(void)
|
||||
int current_score = compl_fuzzy_scores[fuzzy_indices_data[i]];
|
||||
if (ins_compl_add(match, -1, NULL, NULL, false, NULL, dir,
|
||||
CP_FAST | ((p_fic || p_wic) ? CP_ICASE : 0),
|
||||
false, NULL, current_score) == OK) {
|
||||
false, NULL, current_score, false) == OK) {
|
||||
dir = FORWARD;
|
||||
}
|
||||
|
||||
@@ -4581,7 +4600,7 @@ static void get_next_bufname_token(void)
|
||||
char *tail = path_tail(b->b_sfname);
|
||||
if (strncmp(tail, compl_orig_text.data, compl_orig_text.size) == 0) {
|
||||
ins_compl_add(tail, (int)strlen(tail), NULL, NULL, false, NULL, 0,
|
||||
p_ic ? CP_ICASE : 0, false, NULL, FUZZY_SCORE_NONE);
|
||||
p_ic ? CP_ICASE : 0, false, NULL, FUZZY_SCORE_NONE, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6097,7 +6116,7 @@ static int ins_compl_start(void)
|
||||
}
|
||||
if (ins_compl_add(compl_orig_text.data, (int)compl_orig_text.size,
|
||||
NULL, NULL, false, NULL, 0,
|
||||
flags, false, NULL, FUZZY_SCORE_NONE) != OK) {
|
||||
flags, false, NULL, FUZZY_SCORE_NONE, false) != OK) {
|
||||
API_CLEAR_STRING(compl_pattern);
|
||||
API_CLEAR_STRING(compl_orig_text);
|
||||
kv_destroy(compl_orig_extmarks);
|
||||
|
||||
@@ -1603,17 +1603,18 @@ local options = {
|
||||
cb = 'did_set_completeopt',
|
||||
defaults = 'menu,popup',
|
||||
values = {
|
||||
'fuzzy',
|
||||
'longest',
|
||||
'menu',
|
||||
'menuone',
|
||||
'longest',
|
||||
'preview',
|
||||
'popup',
|
||||
'nearest',
|
||||
'noinsert',
|
||||
'noselect',
|
||||
'fuzzy',
|
||||
'nosort',
|
||||
'popup',
|
||||
'preinsert',
|
||||
'nearest',
|
||||
'preselect',
|
||||
'preview',
|
||||
},
|
||||
flags = true,
|
||||
deny_duplicates = true,
|
||||
@@ -1690,8 +1691,12 @@ local options = {
|
||||
completion in the preview window. Only works in
|
||||
combination with "menu" or "menuone".
|
||||
|
||||
Only "fuzzy", "longest", "popup", "preinsert" and "preview" have an
|
||||
effect when 'autocomplete' is enabled.
|
||||
preselect Select the completion item that has the "preselect"
|
||||
attribute set. If both "noselect" and "preselect" are present,
|
||||
"preselect" takes precedence.
|
||||
|
||||
Only "fuzzy", "longest", "popup", "preinsert", "preselect" and
|
||||
"preview" have an effect when 'autocomplete' is enabled.
|
||||
|
||||
This option does not apply to |cmdline-completion|. See 'wildoptions'
|
||||
for that.
|
||||
|
||||
@@ -266,6 +266,70 @@ describe('completion', function()
|
||||
feed('i<C-r>=TestComplete()<CR><ESC>')
|
||||
eq(0, eval('&l:modified'))
|
||||
end)
|
||||
describe('"preselect"', function()
|
||||
it('"preselect" selects first item with preselect attribute', function()
|
||||
source([[
|
||||
function! TestComplete() abort
|
||||
call complete(1, [
|
||||
\ {'word': 'aaa'},
|
||||
\ {'word': 'bbb'},
|
||||
\ {'word': 'ccc', 'preselect': v:true},
|
||||
\ {'word': 'ddd'},
|
||||
\ ])
|
||||
return ''
|
||||
endfunction
|
||||
function! TestCompleteMany() abort
|
||||
let items = []
|
||||
for i in range(20)
|
||||
call add(items, {'word': 'item' .. i})
|
||||
endfor
|
||||
let items[15].preselect = v:true
|
||||
call complete(1, items)
|
||||
return ''
|
||||
endfunction
|
||||
]])
|
||||
|
||||
command('set completeopt=menuone,preselect')
|
||||
feed('i<C-r>=TestComplete()<CR>')
|
||||
screen:expect([[
|
||||
ccc^ |
|
||||
{4:aaa }{1: }|
|
||||
{4:bbb }{1: }|
|
||||
{12:ccc }{1: }|
|
||||
{4:ddd }{1: }|
|
||||
{1:~ }|*2
|
||||
{5:-- INSERT --} |
|
||||
]])
|
||||
|
||||
-- scrolls pum when preselect item is far down
|
||||
feed('<Esc>S')
|
||||
command('set pumheight=5')
|
||||
feed('<C-r>=TestCompleteMany()<CR>')
|
||||
screen:expect([[
|
||||
item15^ |
|
||||
{4:item13 }{12: }{1: }|
|
||||
{4:item14 }{12: }{1: }|
|
||||
{12:item15 }{1: }|
|
||||
{4:item16 }{101: }{1: }|
|
||||
{4:item17 }{12: }{1: }|
|
||||
{1:~ }|
|
||||
{5:-- INSERT --} |
|
||||
]])
|
||||
|
||||
feed('<Esc>S')
|
||||
command('set completeopt=menuone,noselect,preselect pumheight=0')
|
||||
feed('<C-r>=TestComplete()<CR>')
|
||||
screen:expect([[
|
||||
^ |
|
||||
{4:aaa }{1: }|
|
||||
{4:bbb }{1: }|
|
||||
{12:ccc }{1: }|
|
||||
{4:ddd }{1: }|
|
||||
{1:~ }|*2
|
||||
{5:-- INSERT --} |
|
||||
]])
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('completeopt+=noinsert does not add blank undo items', function()
|
||||
|
||||
@@ -1547,6 +1547,29 @@ describe('vim.lsp.completion: integration', function()
|
||||
feed('<C-y>')
|
||||
eq('hallo', n.api.nvim_get_current_line())
|
||||
end)
|
||||
|
||||
it('preselect completion item', function()
|
||||
local completion_list = {
|
||||
isIncomplete = false,
|
||||
items = {
|
||||
{ label = 'aaa' },
|
||||
{ label = 'zzz', preselect = true },
|
||||
{ label = 'mmm' },
|
||||
},
|
||||
}
|
||||
exec_lua(function()
|
||||
vim.o.completeopt = 'menuone,noselect,preselect'
|
||||
end)
|
||||
create_server('dummy', completion_list)
|
||||
feed('i<C-x><C-o>')
|
||||
wait_for_pum()
|
||||
eq(
|
||||
2,
|
||||
exec_lua(function()
|
||||
return vim.fn.complete_info({ 'selected' }).selected
|
||||
end)
|
||||
)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("vim.lsp.completion: omnifunc + 'autocomplete'", function()
|
||||
|
||||
@@ -542,7 +542,7 @@ func Test_set_completion_string_values()
|
||||
call assert_match('unnamed', getcompletion('set clipboard=', 'cmdline')[0])
|
||||
endif
|
||||
call assert_equal('.', getcompletion('set complete=', 'cmdline')[1])
|
||||
call assert_equal('menu', getcompletion('set completeopt=', 'cmdline')[1])
|
||||
call assert_equal('fuzzy', getcompletion('set completeopt=', 'cmdline')[1])
|
||||
" call assert_equal('keyword', getcompletion('set completefuzzycollect=', 'cmdline')[0])
|
||||
if exists('+completeslash')
|
||||
call assert_equal('backslash', getcompletion('set completeslash=', 'cmdline')[1])
|
||||
|
||||
Reference in New Issue
Block a user