diff --git a/src/insexpand.c b/src/insexpand.c index 16b2e7b52b6c96..947ca98872e98a 100644 --- a/src/insexpand.c +++ b/src/insexpand.c @@ -136,6 +136,34 @@ static compl_T *compl_curr_match = NULL; static compl_T *compl_shown_match = NULL; static compl_T *compl_old_match = NULL; +// Hashtab with the strings of the matches in the list above, except the +// original-text entries. Keys are the cp_str.string pointers of the +// matches. Used to make the duplicate check O(1) instead of a scan of the +// whole list. When matches were added with "adup" the list can hold +// duplicates; only the first added is present in the hashtab then. +static hashtab_T compl_strings_ht; +static int compl_strings_ready = FALSE; + +/* + * Add the string of "match" to the duplicate-check hashtab, unless an equal + * string is already there. + */ + static void +compl_strings_add(compl_T *match) +{ + hashitem_T *hi; + + if (!compl_strings_ready) + { + hash_init(&compl_strings_ht); + compl_strings_ready = TRUE; + } + hi = hash_find(&compl_strings_ht, match->cp_str.string); + if (HASHITEM_EMPTY(hi)) + (void)hash_add(&compl_strings_ht, match->cp_str.string, + "completion match"); +} + // list used to store the compl_T which have the max score static compl_T **compl_best_matches = NULL; static int compl_num_bests = 0; @@ -902,6 +930,7 @@ ins_compl_add( int dir = (cdir == 0 ? compl_direction : cdir); int flags = flags_arg; int inserted = FALSE; + char_u *new_str = NULL; if (flags & CP_FAST) fast_breakcheck(); @@ -915,20 +944,39 @@ ins_compl_add( // If the same match is already present, don't add it. if (compl_first_match != NULL && !adup) { - match = compl_first_match; - do + if (is_nearest_active()) + { + // Scan all matches, an existing match may need its score + // updated. + match = compl_first_match; + do + { + if (!match_at_original_text(match) + && STRNCMP(match->cp_str.string, str, len) == 0 + && ((int)match->cp_str.length <= len + || match->cp_str.string[len] == NUL)) + { + if (score > 0 && score < match->cp_score) + match->cp_score = score; + return NOTDONE; + } + match = match->cp_next; + } while (match != NULL && !is_first_match(match)); + } + else if (compl_strings_ready) { - if (!match_at_original_text(match) - && STRNCMP(match->cp_str.string, str, len) == 0 - && ((int)match->cp_str.length <= len - || match->cp_str.string[len] == NUL)) + // Look up the string in the hashtab, much faster than scanning + // all matches. The NUL-terminated copy made for the lookup is + // used for the new match below. + new_str = vim_strnsave(str, len); + if (new_str == NULL) + return FAIL; + if (!HASHITEM_EMPTY(hash_find(&compl_strings_ht, new_str))) { - if (is_nearest_active() && score > 0 && score < match->cp_score) - match->cp_score = score; + vim_free(new_str); return NOTDONE; } - match = match->cp_next; - } while (match != NULL && !is_first_match(match)); + } } // Remove any popup menu before changing the list of matches. @@ -938,14 +986,19 @@ ins_compl_add( // Copy the values to the new match structure. match = ALLOC_CLEAR_ONE(compl_T); if (match == NULL) + { + vim_free(new_str); return FAIL; + } match->cp_number = flags & CP_ORIGINAL_TEXT ? 0 : -1; - if ((match->cp_str.string = vim_strnsave(str, len)) == NULL) + if (new_str == NULL) + new_str = vim_strnsave(str, len); + if (new_str == NULL) { vim_free(match); return FAIL; } - + match->cp_str.string = new_str; match->cp_str.length = len; // match-fname is: @@ -1035,6 +1088,10 @@ ins_compl_add( compl_first_match = match; compl_curr_match = match; + // Keep the hashtab used for the duplicate check up to date. + if (!match_at_original_text(match)) + compl_strings_add(match); + // Find the longest common string if still doing that. if (compl_get_longest && (flags & CP_ORIGINAL_TEXT) == 0 && !cot_fuzzy() && !ins_compl_preinsert_longest() && !ctrl_x_mode_thesaurus()) @@ -2263,6 +2320,16 @@ find_line_end(char_u *ptr) static void ins_compl_item_free(compl_T *match) { + // Remove the match string from the duplicate-check hashtab. With + // duplicate strings in the list only the first added one is in the + // hashtab, compare the pointer to only remove that one. + if (compl_strings_ready && match->cp_str.string != NULL) + { + hashitem_T *hi = hash_find(&compl_strings_ht, match->cp_str.string); + + if (!HASHITEM_EMPTY(hi) && hi->hi_key == match->cp_str.string) + hash_remove(&compl_strings_ht, hi, "completion match"); + } VIM_CLEAR_STRING(match->cp_str); // several entries may use the same fname, free it just once. if (match->cp_flags & CP_FREE_FNAME) @@ -2302,6 +2369,14 @@ ins_compl_free(void) compl_first_match = compl_curr_match = NULL; compl_shown_match = NULL; compl_old_match = NULL; + + // The removals above emptied the duplicate-check hashtab, shrink its + // array back to the initial size. + if (compl_strings_ready) + { + hash_clear(&compl_strings_ht); + hash_init(&compl_strings_ht); + } } /* diff --git a/src/testdir/test_ins_complete.vim b/src/testdir/test_ins_complete.vim index 5dd18ecf2f5b41..df8caa0b853577 100644 --- a/src/testdir/test_ins_complete.vim +++ b/src/testdir/test_ins_complete.vim @@ -6634,4 +6634,71 @@ func Test_complete_check_mapped_typed_key() unlet g:compl_iterations endfunc +func GetDedupWords() + let g:compl_words = map(complete_info(['items']).items, {_, v -> v.word}) + return '' +endfunc + +func DoDedupComplete() + call complete(1, ['dup', 'dup', 'uniq', 'dup']) + return '' +endfunc + +" Test for the duplicate check when adding completion matches +func Test_ins_complete_dedup() + new + setl complete=. + + " a word that occurs several times only results in one match + call setline(1, ['alpha beta alpha gamma', 'beta alpha delta beta', '']) + call cursor(3, 1) + call feedkeys("Aal\\=GetDedupWords()\\\", 'tx') + call assert_equal(['alpha'], g:compl_words) + + " the duplicate check is case-sensitive + %delete _ + call setline(1, ['Foo foo FOO fooBar Foo foo', '']) + call cursor(2, 1) + call feedkeys("Afo\\=GetDedupWords()\\\", 'tx') + call assert_equal(['foo', 'fooBar'], g:compl_words) + + " with 'ignorecase' and 'infercase' case variants fold into one match + setl ignorecase infercase + %delete _ + call setline(1, ['Word word WORD wordy Word', '']) + call cursor(2, 1) + call feedkeys("Awo\\=GetDedupWords()\\\", 'tx') + call assert_equal(['word', 'wordy'], g:compl_words) + setl noignorecase noinfercase + + " duplicate dictionary entries only appear once; with 'ignorecase' case + " variants all match but stay separate matches + call writefile(['apple', 'apple', 'Apple', 'apricot', 'apricot', 'banana'], + \ 'Xcompldict', 'D') + setl dictionary=Xcompldict + set ignorecase + %delete _ + call feedkeys("Aap\\\=GetDedupWords()\\\", 'tx') + call assert_equal(['apple', 'Apple', 'apricot'], g:compl_words) + set noignorecase + setl dictionary& + + " duplicate items passed to complete() are only added once + %delete _ + call feedkeys("i\=DoDedupComplete()\" + \ .. "\=GetDedupWords()\\\", 'tx') + call assert_equal(['dup', 'uniq'], g:compl_words) + + " restarting a completion rebuilds the matches without duplicates + %delete _ + call setline(1, ['echo edit eecho edit echo', '']) + call cursor(2, 1) + call feedkeys("Ae\\\", 'tx') + call feedkeys("A\\=GetDedupWords()\\\", 'tx') + call assert_equal(['echo', 'edit', 'eecho'], g:compl_words) + + bwipe! + unlet g:compl_words +endfunc + " vim: shiftwidth=2 sts=2 expandtab nofoldenable