vim-patch:9.1.1797: completion: autocompletion can be improved

Problem:  completion: autocompletion can be improved
Solution: Add support for "longest" and "preinsert" in 'autocomplete';
          add preinserted() (Girish Palya)

* Add support for "longest" in 'completeopt' when 'autocomplete'
  is enabled. (Note: the cursor position does not change automatically
  when 'autocomplete' is enabled.)
* Add support for "preinsert" when 'autocomplete' is enabled. Ensure
  "preinsert" works the same with and without 'autocomplete'
* introduce the preinserted() Vim script function, useful for defining
  custom key mappings.

fixes: vim/vim#18314
closes: vim/vim#18387

c05335082a

Co-authored-by: Girish Palya <girishji@gmail.com>
This commit is contained in:
zeertzjq
2025-09-27 09:09:03 +08:00
parent 4896178863
commit 7b9c063d11
11 changed files with 381 additions and 130 deletions

View File

@@ -606,10 +606,10 @@ static int insert_execute(VimState *state, int key)
&& (s->c == CAR || s->c == K_KENTER || s->c == NL)))
&& stop_arrow() == OK) {
ins_compl_delete(false);
if (ins_compl_has_preinsert() && ins_compl_autocomplete_enabled()) {
(void)ins_compl_insert(false, true);
} else {
(void)ins_compl_insert(false, false);
ins_compl_insert(false, !ins_compl_has_preinsert());
if (ins_compl_preinsert_longest()) {
ins_compl_init_get_longest();
return 1;
}
} else if (ascii_iswhite_nl_or_nul(s->c) && ins_compl_preinsert_effect()) {
// Delete preinserted text when typing special chars

View File

@@ -8024,6 +8024,18 @@ M.funcs = {
returns = 'number',
signature = 'pow({x}, {y})',
},
preinserted = {
desc = [=[
Returns non-zero if text has been inserted after the cursor
because "preinsert" is present in 'completeopt', or if
"longest" is present in 'completeopt' while 'autocomplete'
is enabled. Otherwise returns zero.
]=],
name = 'preinserted',
params = {},
returns = 'number',
signature = 'preinserted()',
},
prevnonblank = {
args = 1,
base = 1,

View File

@@ -296,7 +296,7 @@ static bool compl_autocomplete = false; ///< whether autocompletion is ac
static uint64_t compl_timeout_ms = COMPL_INITIAL_TIMEOUT_MS;
static bool compl_time_slice_expired = false; ///< time budget exceeded for current source
static bool compl_from_nonkeyword = false; ///< completion started from non-keyword
static bool compl_autocomplete_preinsert = false; ///< apply preinsert highlight
static bool compl_hi_on_autocompl_longest = false; ///< apply "PreInsert" highlight
// Halve the current completion timeout, simulating exponential decay.
#define COMPL_MIN_TIMEOUT_MS 5
@@ -889,6 +889,15 @@ static bool is_nearest_active(void)
&& !(flags & kOptCotFlagFuzzy);
}
/// Returns true if autocomplete is active and the pre-insert effect targets the
/// longest prefix.
bool ins_compl_preinsert_longest(void)
{
return compl_autocomplete
&& (get_cot_flags() & (kOptCotFlagLongest | kOptCotFlagPreinsert | kOptCotFlagFuzzy))
== kOptCotFlagLongest;
}
/// Add a match to the list of matches
///
/// @param[in] str text of the match to add
@@ -1053,7 +1062,8 @@ static int ins_compl_add(char *const str, int len, char *const fname, char *cons
compl_curr_match = match;
// Find the longest common string if still doing that.
if (compl_get_longest && (flags & CP_ORIGINAL_TEXT) == 0 && !cfc_has_mode()) {
if (compl_get_longest && (flags & CP_ORIGINAL_TEXT) == 0 && !cfc_has_mode()
&& !ins_compl_preinsert_longest()) {
ins_compl_longest_match(match);
}
@@ -1107,17 +1117,12 @@ static size_t ins_compl_leader_len(void)
/// -1 means normal item.
int ins_compl_col_range_attr(linenr_T lnum, int col)
{
const bool has_preinsert = ins_compl_has_preinsert();
const bool has_preinsert = ins_compl_has_preinsert() || ins_compl_preinsert_longest();
int attr;
if ((get_cot_flags() & kOptCotFlagFuzzy)
|| (!has_preinsert
&& (attr = syn_name2attr("ComplMatchIns")) == 0)
|| (!compl_autocomplete && has_preinsert
&& (attr = syn_name2attr("PreInsert")) == 0)
|| (compl_autocomplete
&& (!compl_autocomplete_preinsert
|| (attr = syn_name2attr("PreInsert")) == 0))) {
|| (!compl_hi_on_autocompl_longest && ins_compl_preinsert_longest())
|| (attr = syn_name2attr(has_preinsert ? "PreInsert" : "ComplMatchIns")) == 0) {
return -1;
}
@@ -1511,7 +1516,8 @@ static int ins_compl_build_pum(void)
}
unsigned cur_cot_flags = get_cot_flags();
bool compl_no_select = (cur_cot_flags & kOptCotFlagNoselect) != 0 || compl_autocomplete;
bool compl_no_select = (cur_cot_flags & kOptCotFlagNoselect) != 0
|| (compl_autocomplete && !ins_compl_has_preinsert());
bool fuzzy_filter = (cur_cot_flags & kOptCotFlagFuzzy) != 0;
compl_T *match_head = NULL, *match_tail = NULL;
@@ -2132,6 +2138,9 @@ int ins_compl_len(void)
bool ins_compl_has_preinsert(void)
{
unsigned cur_cot_flags = get_cot_flags();
if (compl_autocomplete && p_ic && !p_inf) {
return false;
}
return (!compl_autocomplete
? (cur_cot_flags & (kOptCotFlagPreinsert|kOptCotFlagFuzzy|kOptCotFlagMenuone))
== (kOptCotFlagPreinsert|kOptCotFlagMenuone)
@@ -2143,19 +2152,13 @@ bool ins_compl_has_preinsert(void)
/// the `compl_ins_end_col` range.
bool ins_compl_preinsert_effect(void)
{
if (!ins_compl_has_preinsert()) {
if (!ins_compl_has_preinsert() && !ins_compl_preinsert_longest()) {
return false;
}
return curwin->w_cursor.col < compl_ins_end_col;
}
/// Returns true if autocompletion is active.
bool ins_compl_autocomplete_enabled(void)
{
return compl_autocomplete;
}
/// Delete one character before the cursor and show the subset of the matches
/// that match the word that is now before the cursor.
/// Returns the character to be used, NUL if the work is done and another char
@@ -2198,7 +2201,7 @@ int ins_compl_bs(void)
(size_t)(p_off - (ptrdiff_t)compl_col));
// Clear selection if a menu item is currently selected in autocompletion
if (compl_autocomplete && compl_first_match) {
if (compl_autocomplete && compl_first_match && !ins_compl_has_preinsert()) {
compl_shown_match = compl_first_match;
}
@@ -2291,18 +2294,14 @@ static void ins_compl_new_leader(void)
// Show the popup menu with a different set of matches.
ins_compl_show_pum();
compl_autocomplete_preinsert = false;
// Don't let Enter select the original text when there is no popup menu.
if (compl_match_array == NULL) {
compl_enter_selects = false;
} else if (ins_compl_has_preinsert() && compl_leader.size > 0) {
if (compl_started && compl_autocomplete && !ins_compl_preinsert_effect()) {
if (ins_compl_insert(true, true) == OK) {
compl_autocomplete_preinsert = true;
}
} else {
(void)ins_compl_insert(true, false);
}
ins_compl_insert(true, false);
} else if (compl_started && ins_compl_preinsert_longest()
&& compl_leader.size > 0 && !ins_compl_preinsert_effect()) {
ins_compl_insert(true, true);
}
// Don't let Enter select when use user function and refresh_always is set
if (ins_compl_refresh_always()) {
@@ -4388,7 +4387,7 @@ static int get_next_default_completion(ins_compl_next_state_T *st, pos_T *start_
ptr = ins_compl_get_next_word_or_line(st->ins_buf,
st->cur_match_pos, &len, &cont_s_ipos);
}
if (ptr == NULL || (!compl_autocomplete && ins_compl_has_preinsert()
if (ptr == NULL || (ins_compl_has_preinsert()
&& strcmp(ptr, compl_pattern.data) == 0)) {
continue;
}
@@ -4895,7 +4894,7 @@ static int ins_compl_get_exp(pos_T *ini)
}
may_trigger_modechanged();
if (is_nearest_active()) {
if (is_nearest_active() && !ins_compl_has_preinsert()) {
sort_compl_match_list(cp_compare_nearest);
}
@@ -5108,6 +5107,21 @@ static char *find_common_prefix(size_t *prefix_len, bool curbuf_only)
xfree(match_count);
if (len > (int)ins_compl_leader_len()) {
assert(first != NULL);
// Avoid inserting text that duplicates the text already present
// after the cursor.
if (len == (int)strlen(first)) {
char *line = get_cursor_line_ptr();
char *p = line + curwin->w_cursor.col;
if (p && !ascii_iswhite_or_nul(*p)) {
char *end = find_word_end(p);
int text_len = (int)(end - p);
if (text_len > 0 && text_len < (len - (int)ins_compl_leader_len())
&& strncmp(first + len - text_len, p, (size_t)text_len) == 0) {
len -= text_len;
}
}
}
*prefix_len = (size_t)len;
return first;
}
@@ -5117,9 +5131,9 @@ static char *find_common_prefix(size_t *prefix_len, bool curbuf_only)
/// Insert the new text being completed.
/// "move_cursor" is used when 'completeopt' includes "preinsert" and when true
/// cursor needs to move back from the inserted text to the compl_leader.
/// When "preinsert_prefix" is true the longest common prefix is inserted
/// instead of shown match.
int ins_compl_insert(bool move_cursor, bool preinsert_prefix)
/// When "insert_prefix" is true the longest common prefix is inserted instead
/// of shown match.
void ins_compl_insert(bool move_cursor, bool insert_prefix)
{
int compl_len = get_compl_len();
bool preinsert = ins_compl_has_preinsert();
@@ -5128,12 +5142,13 @@ int ins_compl_insert(bool move_cursor, bool preinsert_prefix)
size_t leader_len = ins_compl_leader_len();
char *has_multiple = strchr(cp_str, '\n');
if (preinsert_prefix) {
if (insert_prefix) {
cp_str = find_common_prefix(&cp_str_len, false);
if (cp_str == NULL) {
cp_str = find_common_prefix(&cp_str_len, true);
if (cp_str == NULL) {
return FAIL;
cp_str = compl_shown_match->cp_str.data;
cp_str_len = compl_shown_match->cp_str.size;
}
}
} else if (cpt_sources_array != NULL) {
@@ -5160,18 +5175,18 @@ int ins_compl_insert(bool move_cursor, bool preinsert_prefix)
ins_compl_expand_multiple(cp_str + compl_len);
} else {
ins_compl_insert_bytes(cp_str + compl_len,
preinsert_prefix ? (int)cp_str_len - compl_len : -1);
if (preinsert && move_cursor) {
insert_prefix ? (int)cp_str_len - compl_len : -1);
if ((preinsert || insert_prefix) && move_cursor) {
curwin->w_cursor.col -= (colnr_T)(cp_str_len - leader_len);
}
}
}
compl_used_match = !(match_at_original_text(compl_shown_match)
|| (preinsert && !compl_autocomplete));
|| (preinsert && !insert_prefix));
dict_T *dict = ins_compl_dict_alloc(compl_shown_match);
set_vim_var_dict(VV_COMPLETED_ITEM, dict);
return OK;
compl_hi_on_autocompl_longest = insert_prefix && move_cursor;
}
/// show the file name for the completion match (if any). Truncate the file
@@ -5240,7 +5255,8 @@ static int find_next_completion_match(bool allow_get_expansion, int todo, bool a
bool found_end = false;
compl_T *found_compl = NULL;
unsigned cur_cot_flags = get_cot_flags();
bool compl_no_select = (cur_cot_flags & kOptCotFlagNoselect) != 0 || compl_autocomplete;
bool compl_no_select = (cur_cot_flags & kOptCotFlagNoselect) != 0
|| (compl_autocomplete && !ins_compl_has_preinsert());
bool compl_fuzzy_match = (cur_cot_flags & kOptCotFlagFuzzy) != 0;
while (--todo >= 0) {
@@ -5351,7 +5367,8 @@ static int ins_compl_next(bool allow_get_expansion, int count, bool insert_match
const bool started = compl_started;
buf_T *const orig_curbuf = curbuf;
unsigned cur_cot_flags = get_cot_flags();
bool compl_no_insert = (cur_cot_flags & kOptCotFlagNoinsert) != 0 || compl_autocomplete;
bool compl_no_insert = (cur_cot_flags & kOptCotFlagNoinsert) != 0
|| (compl_autocomplete && !ins_compl_has_preinsert());
bool compl_fuzzy_match = (cur_cot_flags & kOptCotFlagFuzzy) != 0;
bool compl_preinsert = ins_compl_has_preinsert();
@@ -5397,35 +5414,18 @@ static int ins_compl_next(bool allow_get_expansion, int count, bool insert_match
return -1;
}
compl_autocomplete_preinsert = false;
// Insert the text of the new completion, or the compl_leader.
if (compl_no_insert && !started) {
bool insert_orig = !compl_preinsert;
if (compl_preinsert && compl_autocomplete) {
if (ins_compl_insert(true, true) == OK) {
compl_autocomplete_preinsert = true;
} else {
insert_orig = true;
}
}
if (insert_orig) {
ins_compl_insert_bytes(compl_orig_text.data + get_compl_len(), -1);
}
if (!started && ins_compl_preinsert_longest()) {
ins_compl_insert(true, true);
} else if (compl_no_insert && !started && !compl_preinsert) {
ins_compl_insert_bytes(compl_orig_text.data + get_compl_len(), -1);
compl_used_match = false;
restore_orig_extmarks();
} else if (insert_match) {
if (!compl_get_longest || compl_used_match) {
bool none_selected = match_at_original_text(compl_shown_match);
if (compl_preinsert && compl_autocomplete && none_selected) {
if (ins_compl_insert(none_selected, true) == OK) {
compl_autocomplete_preinsert = none_selected;
} else {
(void)ins_compl_insert(false, false);
}
} else {
(void)ins_compl_insert(!compl_autocomplete, false);
}
bool preinsert_longest = ins_compl_preinsert_longest()
&& match_at_original_text(compl_shown_match); // none selected
ins_compl_insert(compl_preinsert || preinsert_longest, preinsert_longest);
} else {
assert(compl_leader.data != NULL);
ins_compl_insert_bytes(compl_leader.data + get_compl_len(), -1);
@@ -5534,8 +5534,8 @@ void ins_compl_check_keys(int frequency, bool in_compl_func)
}
}
if (compl_pending != 0 && !got_int && !(cot_flags & kOptCotFlagNoinsert)
&& !compl_autocomplete) {
if (compl_pending && !got_int && !(cot_flags & kOptCotFlagNoinsert)
&& (!compl_autocomplete || ins_compl_has_preinsert())) {
// Insert the first match immediately and advance compl_shown_match,
// before finding other matches.
int todo = compl_pending > 0 ? compl_pending : -compl_pending;
@@ -6599,3 +6599,11 @@ static void cpt_compl_refresh(void)
// Make the list cyclic
compl_matches = ins_compl_make_cyclic();
}
/// "preinserted()" function
void f_preinserted(typval_T *argvars, typval_T *rettv, EvalFuncData fptr)
{
if (ins_compl_preinsert_effect()) {
rettv->vval.v_number = 1;
}
}

View File

@@ -1663,11 +1663,22 @@ local options = {
to gather more alternatives for your candidate list,
see 'completefuzzycollect'.
longest Only insert the longest common text of the matches. If
the menu is displayed you can use CTRL-L to add more
characters. Whether case is ignored depends on the kind
of completion. For buffer text the 'ignorecase' option is
used.
longest
When 'autocomplete' is not active, only the longest
common prefix of the matches is inserted. If the popup
menu is displayed, you can use CTRL-L to add more
characters. Whether case is ignored depends on the type
of completion. For buffer text the 'ignorecase' option
applies.
When 'autocomplete' is active and no completion item is
selected, the longest common prefix of the matches is
inserted after the cursor. The prefix is taken either
from all displayed items or only from items in the current
buffer. The inserted text is highlighted with
|hl-PreInsert|, and the cursor position does not change
(similar to `"preinsert"`). Press CTRL-Y to accept.
See also |preinserted()|.
menu Use a popup menu to show the possible completions. The
menu is only shown when there is more than one match and
@@ -1700,22 +1711,21 @@ local options = {
with "menu" or "menuone". Overrides "preview".
preinsert
When 'autocomplete' is not active, inserts the part of the
first candidate word beyond the current completion leader,
highlighted with |hl-PreInsert|. The cursor doesn't move.
Requires "fuzzy" unset and "menuone" in 'completeopt'.
When 'autocomplete' is active, inserts the longest common
prefix of matches (from all shown items or from the
current buffer items). This occurs only when no menu item
is selected. Press CTRL-Y to accept.
Inserts the text of the first completion candidate
beyond the current leader, highlighted with |hl-PreInsert|.
The cursor does not move.
Requires "fuzzy" to be unset, and either "menuone" in
'completeopt' or 'autocomplete' enabled. When
'autocomplete' is enabled, this does not work if
'ignorecase' is set without 'infercase'.
See also |preinserted()|.
preview Show extra information about the currently selected
completion in the preview window. Only works in
combination with "menu" or "menuone".
Only "fuzzy", "popup", "preinsert" and "preview" have an effect when
'autocomplete' is enabled.
Only "fuzzy", "longest", "popup", "preinsert" and "preview" have an
effect when 'autocomplete' is enabled.
This option does not apply to |cmdline-completion|. See 'wildoptions'
for that.