# HG changeset patch # User Bram Moolenaar # Date 1641638704 -3600 # Node ID 1d9a7aa42744ab0b5aa2fdef6ee502702718a5e5 # Parent 6c5c0c222303c36c80d3f5d358cf9db56b8f0816 patch 8.2.4037: Insert mode completion is insufficiently tested Commit: https://github.com/vim/vim/commit/370791465e745354d66696de8cbd15504cf958c0 Author: Yegappan Lakshmanan Date: Sat Jan 8 10:38:48 2022 +0000 patch 8.2.4037: Insert mode completion is insufficiently tested Problem: Insert mode completion is insufficiently tested. Solution: Add more tests. Fix uncovered memory leak. (Yegappan Lakshmanan, closes #9489) diff --git a/src/insexpand.c b/src/insexpand.c --- a/src/insexpand.c +++ b/src/insexpand.c @@ -698,7 +698,20 @@ ins_compl_add_infercase( } /* - * Add a match to the list of matches. + * Add a match to the list of matches. The arguments are: + * str - text of the match to add + * len - length of "str". If -1, then the length of "str" is + * computed. + * fname - file name to associate with this match. + * cptext - list of strings to use with this match (for abbr, menu, info + * and kind) + * user_data - user supplied data (any vim type) for this match + * cdir - match direction. If 0, use "compl_direction". + * flags_arg - match flags (cp_flags) + * adup - accept this match even if it is already present. + * If "cdir" is FORWARD, then the match is added after the current match. + * Otherwise, it is added before the current match. + * * If the given string is already in the list of completions, then return * NOTDONE, otherwise add it to the list and return OK. If there is an error, * maybe because alloc() returns NULL, then FAIL is returned. @@ -789,7 +802,8 @@ ins_compl_add( match->cp_user_data = *user_data; #endif - // Link the new match structure in the list of matches. + // Link the new match structure after (FORWARD) or before (BACKWARD) the + // current match in the list of matches . if (compl_first_match == NULL) match->cp_next = match->cp_prev = NULL; else if (dir == FORWARD) @@ -2704,6 +2718,7 @@ ins_compl_add_tv(typval_T *tv, int dir, int flags = fast ? CP_FAST : 0; char_u *(cptext[CPT_COUNT]); typval_T user_data; + int status; user_data.v_type = VAR_UNKNOWN; if (tv->v_type == VAR_DICT && tv->vval.v_dict != NULL) @@ -2735,8 +2750,14 @@ ins_compl_add_tv(typval_T *tv, int dir, CLEAR_FIELD(cptext); } if (word == NULL || (!empty && *word == NUL)) + { + clear_tv(&user_data); return FAIL; - return ins_compl_add(word, -1, NULL, cptext, &user_data, dir, flags, dup); + } + status = ins_compl_add(word, -1, NULL, cptext, &user_data, dir, flags, dup); + if (status != OK) + clear_tv(&user_data); + return status; } /* @@ -3157,8 +3178,8 @@ typedef struct * st->dict_f - flag specifying whether "dict" is an exact file name or not * * Returns INS_COMPL_CPT_OK if the next value is processed successfully. - * Returns INS_COMPL_CPT_CONT to skip the current value and process the next - * option value. + * Returns INS_COMPL_CPT_CONT to skip the current completion source matching + * the "st->e_cpt" option value and process the next matching source. * Returns INS_COMPL_CPT_END if all the values in "st->e_cpt" are processed. */ static int @@ -4521,7 +4542,7 @@ get_userdefined_compl_info(colnr_T curs_ return FAIL; } - // Reset extended parameters of completion, when start new + // Reset extended parameters of completion, when starting new // completion. compl_opt_refresh_always = FALSE; compl_opt_suppress_empty = FALSE; diff --git a/src/testdir/test_ins_complete.vim b/src/testdir/test_ins_complete.vim --- a/src/testdir/test_ins_complete.vim +++ b/src/testdir/test_ins_complete.vim @@ -71,6 +71,11 @@ func Test_ins_complete() call assert_equal('Xtest11.one', getline('.')) normal ddk + " Test for expanding a non-existing filename + exe "normal oa1b2X3Y4\\" + call assert_equal('a1b2X3Y4', getline('.')) + normal ddk + set cpt=w " checks make_cyclic in other window exe "normal oST\\\\\" @@ -981,6 +986,27 @@ func Test_complete_erase_firstmatch() bw! endfunc +" Test for completing words from unloaded buffers +func Test_complete_from_unloadedbuf() + call writefile(['abc'], "Xfile1") + call writefile(['def'], "Xfile2") + edit Xfile1 + edit Xfile2 + new | close + enew + bunload Xfile1 Xfile2 + set complete=u + " complete from an unloaded buffer + exe "normal! ia\" + call assert_equal('abc', getline(1)) + exe "normal! od\" + call assert_equal('def', getline(2)) + set complete& + %bw! + call delete("Xfile1") + call delete("Xfile2") +endfunc + " Test for completing whole lines from unloaded buffers func Test_complete_wholeline_unloadedbuf() call writefile(['a line1', 'a line2', 'a line3'], "Xfile1") @@ -999,6 +1025,26 @@ func Test_complete_wholeline_unloadedbuf call delete("Xfile1") endfunc +" Test for completing words from unlisted buffers +func Test_complete_from_unlistedbuf() + call writefile(['abc'], "Xfile1") + call writefile(['def'], "Xfile2") + edit Xfile1 + edit Xfile2 + new | close + bdel Xfile1 Xfile2 + set complete=U + " complete from an unlisted buffer + exe "normal! ia\" + call assert_equal('abc', getline(1)) + exe "normal! od\" + call assert_equal('def', getline(2)) + set complete& + %bw! + call delete("Xfile1") + call delete("Xfile2") +endfunc + " Test for completing whole lines from unlisted buffers func Test_complete_wholeline_unlistedbuf() call writefile(['a line1', 'a line2', 'a line3'], "Xfile1") @@ -1032,6 +1078,195 @@ func Test_complete_mbyte_char_add() bw! endfunc +" Test for using for local expansion even if 'complete' is set to +" not to complete matches from the local buffer. Also test using multiple +" to cancel the current completion mode. +func Test_complete_local_expansion() + new + set complete=t + call setline(1, ['abc', 'def']) + exe "normal! Go\\" + call assert_equal("def", getline(3)) + exe "normal! Go\" + call assert_equal("", getline(4)) + exe "normal! Go\\" + call assert_equal("abc", getline(5)) + exe "normal! Go\" + call assert_equal("", getline(6)) + + " use multiple to cancel the previous completion mode + exe "normal! Go\\\" + call assert_equal("", getline(7)) + exe "normal! Go\\\\" + call assert_equal("", getline(8)) + exe "normal! Go\\\\\" + call assert_equal("abc", getline(9)) + + " interrupt the current completion mode + set completeopt=menu,noinsert + exe "normal! Go\\\\\\" + call assert_equal("abc", getline(10)) + + " when only one is used to interrupt, do normal expansion + exe "normal! Go\\\\" + call assert_equal("", getline(11)) + set completeopt& + + " using two in non-completion mode and restarting the same mode + exe "normal! God\\\\\\\" + call assert_equal("def", getline(12)) + + " test for adding a match from the original empty text + %d + call setline(1, 'abc def g') + exe "normal! o\\\\\" + call assert_equal('def', getline(2)) + exe "normal! 0C\\\\\" + call assert_equal('abc', getline(2)) + + bw! +endfunc + +" Test for undoing changes after a insert-mode completion +func Test_complete_undo() + new + set complete=. + " undo with 'ignorecase' + call setline(1, ['ABOVE', 'BELOW']) + set ignorecase + exe "normal! Goab\u\" + call assert_equal("ABOVE", getline(3)) + undo + call assert_equal("ab", getline(3)) + set ignorecase& + %d + " undo with longest match + set completeopt=menu,longest + call setline(1, ['above', 'about']) + exe "normal! Goa\u\" + call assert_equal("abo", getline(3)) + undo + call assert_equal("a", getline(3)) + set completeopt& + %d + " undo for line completion + call setline(1, ['above that change', 'below that change']) + exe "normal! Goabove\u\\" + call assert_equal("above that change", getline(3)) + undo + call assert_equal("above", getline(3)) + + bw! +endfunc + +" Test for completing a very long word +func Test_complete_long_word() + set complete& + new + call setline(1, repeat('x', 950) .. ' one two three') + exe "normal! Gox\\\\\\\\" + call assert_equal(repeat('x', 950) .. ' one two three', getline(2)) + %d + " should fail when more than 950 characters are in a word + call setline(1, repeat('x', 951) .. ' one two three') + exe "normal! Gox\\\\\\\\" + call assert_equal(repeat('x', 951), getline(2)) + + " Test for adding a very long word to an existing completion + %d + call setline(1, ['abc', repeat('x', 1016) .. '012345']) + exe "normal! Goab\\\" + call assert_equal('abc ' .. repeat('x', 1016) .. '0123', getline(3)) + bw! +endfunc + +" Test for some fields in the complete items used by complete() +func Test_complete_items() + func CompleteItems(idx) + let items = [[#{word: "one", dup: 1, user_data: 'u1'}, #{word: "one", dup: 1, user_data: 'u2'}], + \ [#{word: "one", dup: 0, user_data: 'u3'}, #{word: "one", dup: 0, user_data: 'u4'}], + \ [#{word: "one", icase: 1, user_data: 'u7'}, #{word: "oNE", icase: 1, user_data: 'u8'}], + \ [#{user_data: 'u9'}], + \ [#{word: "", user_data: 'u10'}], + \ [#{word: "", empty: 1, user_data: 'u11'}]] + call complete(col('.'), items[a:idx]) + return '' + endfunc + new + exe "normal! i\=CompleteItems(0)\\\" + call assert_equal('u2', v:completed_item.user_data) + call assert_equal('one', getline(1)) + exe "normal! o\=CompleteItems(1)\\" + call assert_equal('u3', v:completed_item.user_data) + call assert_equal('one', getline(2)) + exe "normal! o\=CompleteItems(1)\\" + call assert_equal('', getline(3)) + set completeopt=menu,noinsert + exe "normal! o\=CompleteItems(2)\one\\" + call assert_equal('oNE', getline(4)) + call assert_equal('u8', v:completed_item.user_data) + set completeopt& + exe "normal! o\=CompleteItems(3)\" + call assert_equal('', getline(5)) + exe "normal! o\=CompleteItems(4)\" + call assert_equal('', getline(6)) + exe "normal! o\=CompleteItems(5)\" + call assert_equal('', getline(7)) + call assert_equal('u11', v:completed_item.user_data) + " pass invalid argument to complete() + let cmd = "normal! o\=complete(1, [[]])\" + call assert_fails('exe cmd', 'E730:') + bw! + delfunc CompleteItems +endfunc + +" Test for the "refresh" item in the dict returned by an insert completion +" function +func Test_complete_item_refresh_always() + let g:CallCount = 0 + func! Tcomplete(findstart, base) + if a:findstart + " locate the start of the word + let line = getline('.') + let start = col('.') - 1 + while start > 0 && line[start - 1] =~ '\a' + let start -= 1 + endwhile + return start + else + let g:CallCount += 1 + let res = ["update1", "update12", "update123"] + return #{words: res, refresh: 'always'} + endif + endfunc + new + set completeopt=menu,longest + set completefunc=Tcomplete + exe "normal! iup\\\\\\\" + call assert_equal('up', getline(1)) + call assert_equal(2, g:CallCount) + set completeopt& + set completefunc& + bw! + delfunc Tcomplete +endfunc + +" Test for completing from a thesaurus file without read permission +func Test_complete_unreadable_thesaurus_file() + CheckUnix + CheckNotRoot + + call writefile(['about', 'above'], 'Xfile') + call setfperm('Xfile', '---r--r--') + new + set complete=sXfile + exe "normal! ia\" + call assert_equal('a', getline(1)) + bw! + call delete('Xfile') + set complete& +endfunc + " Test to ensure 'Scanning...' messages are not recorded in messages history func Test_z1_complete_no_history() new diff --git a/src/version.c b/src/version.c --- a/src/version.c +++ b/src/version.c @@ -751,6 +751,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ /**/ + 4037, +/**/ 4036, /**/ 4035,