diff --git a/runtime/doc/insert.txt b/runtime/doc/insert.txt index d52bc6e644..4513a9d846 100644 --- a/runtime/doc/insert.txt +++ b/runtime/doc/insert.txt @@ -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 diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index 2d42b216cb..44900d77b9 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -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 diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt index c01faa5cad..c88daf678a 100644 --- a/runtime/doc/options.txt +++ b/runtime/doc/options.txt @@ -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. diff --git a/runtime/doc/vim_diff.txt b/runtime/doc/vim_diff.txt index fd0243a2a8..db67c2c7ca 100644 --- a/runtime/doc/vim_diff.txt +++ b/runtime/doc/vim_diff.txt @@ -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 diff --git a/runtime/lua/vim/_meta/options.lua b/runtime/lua/vim/_meta/options.lua index d02970714f..1d4bfb977e 100644 --- a/runtime/lua/vim/_meta/options.lua +++ b/runtime/lua/vim/_meta/options.lua @@ -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. diff --git a/runtime/lua/vim/lsp/completion.lua b/runtime/lua/vim/lsp/completion.lua index 51f23915d1..acaa25791e 100644 --- a/runtime/lua/vim/lsp/completion.lua +++ b/runtime/lua/vim/lsp/completion.lua @@ -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 = { diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua index aa9484d10e..93f67b52b9 100644 --- a/runtime/lua/vim/lsp/protocol.lua +++ b/runtime/lua/vim/lsp/protocol.lua @@ -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, diff --git a/src/nvim/insexpand.c b/src/nvim/insexpand.c index 2de4be9822..83af30d6e4 100644 --- a/src/nvim/insexpand.c +++ b/src/nvim/insexpand.c @@ -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); diff --git a/src/nvim/options.lua b/src/nvim/options.lua index 88789825b4..afdcec683a 100644 --- a/src/nvim/options.lua +++ b/src/nvim/options.lua @@ -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. diff --git a/test/functional/editor/completion_spec.lua b/test/functional/editor/completion_spec.lua index a1c5db4d89..d873ec67cf 100644 --- a/test/functional/editor/completion_spec.lua +++ b/test/functional/editor/completion_spec.lua @@ -266,6 +266,70 @@ describe('completion', function() feed('i=TestComplete()') 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=TestComplete()') + 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('S') + command('set pumheight=5') + feed('=TestCompleteMany()') + 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('S') + command('set completeopt=menuone,noselect,preselect pumheight=0') + feed('=TestComplete()') + 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() diff --git a/test/functional/plugin/lsp/completion_spec.lua b/test/functional/plugin/lsp/completion_spec.lua index 9315f723fb..315e84fe2b 100644 --- a/test/functional/plugin/lsp/completion_spec.lua +++ b/test/functional/plugin/lsp/completion_spec.lua @@ -1547,6 +1547,29 @@ describe('vim.lsp.completion: integration', function() feed('') 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') + 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() diff --git a/test/old/testdir/test_options.vim b/test/old/testdir/test_options.vim index d2f0071017..89d88555e8 100644 --- a/test/old/testdir/test_options.vim +++ b/test/old/testdir/test_options.vim @@ -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])