changeset 27016:1d9a7aa42744 v8.2.4037

patch 8.2.4037: Insert mode completion is insufficiently tested Commit: https://github.com/vim/vim/commit/370791465e745354d66696de8cbd15504cf958c0 Author: Yegappan Lakshmanan <yegappan@yahoo.com> 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)
author Bram Moolenaar <Bram@vim.org>
date Sat, 08 Jan 2022 11:45:04 +0100
parents 6c5c0c222303
children da790d50f73d
files src/insexpand.c src/testdir/test_ins_complete.vim src/version.c
diffstat 3 files changed, 264 insertions(+), 6 deletions(-) [+]
line wrap: on
line diff
--- 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;
--- 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\<C-X>\<C-F>"
+  call assert_equal('a1b2X3Y4', getline('.'))
+  normal ddk
+
   set cpt=w
   " checks make_cyclic in other window
   exe "normal oST\<C-N>\<C-P>\<C-P>\<C-P>\<C-P>"
@@ -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\<C-P>"
+  call assert_equal('abc', getline(1))
+  exe "normal! od\<C-P>"
+  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\<C-P>"
+  call assert_equal('abc', getline(1))
+  exe "normal! od\<C-P>"
+  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 <C-X><C-P> for local expansion even if 'complete' is set to
+" not to complete matches from the local buffer. Also test using multiple
+" <C-X> to cancel the current completion mode.
+func Test_complete_local_expansion()
+  new
+  set complete=t
+  call setline(1, ['abc', 'def'])
+  exe "normal! Go\<C-X>\<C-P>"
+  call assert_equal("def", getline(3))
+  exe "normal! Go\<C-P>"
+  call assert_equal("", getline(4))
+  exe "normal! Go\<C-X>\<C-N>"
+  call assert_equal("abc", getline(5))
+  exe "normal! Go\<C-N>"
+  call assert_equal("", getline(6))
+
+  " use multiple <C-X> to cancel the previous completion mode
+  exe "normal! Go\<C-P>\<C-X>\<C-P>"
+  call assert_equal("", getline(7))
+  exe "normal! Go\<C-P>\<C-X>\<C-X>\<C-P>"
+  call assert_equal("", getline(8))
+  exe "normal! Go\<C-P>\<C-X>\<C-X>\<C-X>\<C-P>"
+  call assert_equal("abc", getline(9))
+
+  " interrupt the current completion mode
+  set completeopt=menu,noinsert
+  exe "normal! Go\<C-X>\<C-F>\<C-X>\<C-X>\<C-P>\<C-Y>"
+  call assert_equal("abc", getline(10))
+
+  " when only one <C-X> is used to interrupt, do normal expansion
+  exe "normal! Go\<C-X>\<C-F>\<C-X>\<C-P>"
+  call assert_equal("", getline(11))
+  set completeopt&
+
+  " using two <C-X> in non-completion mode and restarting the same mode
+  exe "normal! God\<C-X>\<C-X>\<C-P>\<C-X>\<C-X>\<C-P>\<C-Y>"
+  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\<C-X>\<C-P>\<C-N>\<C-X>\<C-P>"
+  call assert_equal('def', getline(2))
+  exe "normal! 0C\<C-X>\<C-N>\<C-P>\<C-X>\<C-N>"
+  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\<C-G>u\<C-P>"
+  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\<C-G>u\<C-P>"
+  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\<C-G>u\<C-X>\<C-L>"
+  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\<C-X>\<C-P>\<C-X>\<C-P>\<C-X>\<C-P>\<C-X>\<C-P>"
+  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\<C-X>\<C-P>\<C-X>\<C-P>\<C-X>\<C-P>\<C-X>\<C-P>"
+  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\<C-P>\<C-X>\<C-P>"
+  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\<C-R>=CompleteItems(0)\<CR>\<C-N>\<C-Y>"
+  call assert_equal('u2', v:completed_item.user_data)
+  call assert_equal('one', getline(1))
+  exe "normal! o\<C-R>=CompleteItems(1)\<CR>\<C-Y>"
+  call assert_equal('u3', v:completed_item.user_data)
+  call assert_equal('one', getline(2))
+  exe "normal! o\<C-R>=CompleteItems(1)\<CR>\<C-N>"
+  call assert_equal('', getline(3))
+  set completeopt=menu,noinsert
+  exe "normal! o\<C-R>=CompleteItems(2)\<CR>one\<C-N>\<C-Y>"
+  call assert_equal('oNE', getline(4))
+  call assert_equal('u8', v:completed_item.user_data)
+  set completeopt&
+  exe "normal! o\<C-R>=CompleteItems(3)\<CR>"
+  call assert_equal('', getline(5))
+  exe "normal! o\<C-R>=CompleteItems(4)\<CR>"
+  call assert_equal('', getline(6))
+  exe "normal! o\<C-R>=CompleteItems(5)\<CR>"
+  call assert_equal('', getline(7))
+  call assert_equal('u11', v:completed_item.user_data)
+  " pass invalid argument to complete()
+  let cmd = "normal! o\<C-R>=complete(1, [[]])\<CR>"
+  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\<C-X>\<C-U>\<BS>\<BS>\<BS>\<BS>\<BS>"
+  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\<C-P>"
+  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
--- 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,