vim-patch:9.1.1512: completion: can only complete from keyword characters (#34798)

Problem:  completion: can only complete from keyword characters
Solution: remove this restriction, allow completion functions when
          called from i_CTRL-N/i_CTRL-P to be triggered from non-keyword
          characters (Girish Palya)

Previously, functions specified in the `'complete'` option were
restricted to starting completion only from keyword characters (as
introduced in PR 17065). This change removes that restriction.

With this change, user-defined functions (e.g., `omnifunc`, `userfunc`)
used in `'complete'` can now initiate completion even when triggered
from non-keyword characters. This makes it easier to reuse existing
functions alongside other sources without having to consider whether the
cursor is on a keyword or non-keyword character, or worry about where
the replacement should begin (i.e., the `findstart=1` return value).

The logic for both the “collection” and “filtering” phases now fully
respects each source’s specified start column. This also extends to
fuzzy matching, making completions more predictable.

Internally, this builds on previously merged infrastructure that tracks
per-source metadata. This PR focuses on applying that metadata to
compute the leader string and insertion text appropriately for each
match.

Also, a memory corruption has been fixed in prepare_cpt_compl_funcs().

closes: vim/vim#17651

ba11e78f1d

Co-authored-by: Girish Palya <girishji@gmail.com>
This commit is contained in:
zeertzjq
2025-07-06 06:09:28 +08:00
committed by GitHub
parent 887255362f
commit 85e6feedb0
6 changed files with 274 additions and 69 deletions

View File

@ -628,7 +628,7 @@ Completion can be done for:
10. User defined completion |i_CTRL-X_CTRL-U|
11. omni completion |i_CTRL-X_CTRL-O|
12. Spelling suggestions |i_CTRL-X_s|
13. keywords in 'complete' |i_CTRL-N| |i_CTRL-P|
13. completions from 'complete' |i_CTRL-N| |i_CTRL-P|
14. contents from registers |i_CTRL-X_CTRL-R|
Additionally, |i_CTRL-X_CTRL-Z| stops completion without changing the text.
@ -1082,25 +1082,23 @@ CTRL-X s Locate the word in front of the cursor and find the
previous one.
Completing keywords from different sources *compl-generic*
Completing from different sources *compl-generic*
*i_CTRL-N*
CTRL-N Find next match for words that start with the
keyword in front of the cursor, looking in places
specified with the 'complete' option. The found
keyword is inserted in front of the cursor.
CTRL-N Find the next match for a word ending at the cursor,
using the sources specified in the 'complete' option.
All sources complete from keywords, except functions,
which may complete from non-keyword. The matched
text is inserted before the cursor.
*i_CTRL-P*
CTRL-P Find previous match for words that start with the
keyword in front of the cursor, looking in places
specified with the 'complete' option. The found
keyword is inserted in front of the cursor.
CTRL-P Same as CTRL-N, but find the previous match.
CTRL-N Search forward for next matching keyword. This
keyword replaces the previous matching keyword.
CTRL-N Search forward through the matches and insert the
next one.
CTRL-P Search backwards for next matching keyword. This
keyword replaces the previous matching keyword.
CTRL-P Search backward through the matches and insert the
previous one.
CTRL-X CTRL-N or
CTRL-X CTRL-P Further use of CTRL-X CTRL-N or CTRL-X CTRL-P will

View File

@ -1512,15 +1512,12 @@ A jump table for the options with a short description can be found at |Q_op|.
name of a function or a |Funcref|. For |Funcref| values,
spaces must be escaped with a backslash ('\'), and commas with
double backslashes ('\\') (see |option-backslash|).
Unlike other sources, functions can provide completions starting
from a non-keyword character before the cursor, and their
start position for replacing text may differ from other sources.
If the Dict returned by the {func} includes {"refresh": "always"},
the function will be invoked again whenever the leading text
changes.
Completion matches are always inserted at the keyword
boundary, regardless of the column returned by {func} when
a:findstart is 1. This ensures compatibility with other
completion sources.
To make further modifications to the inserted text, {func}
can make use of |CompleteDonePre|.
If generating matches is potentially slow, |complete_check()|
should be used to avoid blocking and preserve editor
responsiveness.

View File

@ -1033,15 +1033,12 @@ vim.bo.cms = vim.bo.commentstring
--- name of a function or a `Funcref`. For `Funcref` values,
--- spaces must be escaped with a backslash ('\'), and commas with
--- double backslashes ('\\') (see `option-backslash`).
--- Unlike other sources, functions can provide completions starting
--- from a non-keyword character before the cursor, and their
--- start position for replacing text may differ from other sources.
--- If the Dict returned by the {func} includes {"refresh": "always"},
--- the function will be invoked again whenever the leading text
--- changes.
--- Completion matches are always inserted at the keyword
--- boundary, regardless of the column returned by {func} when
--- a:findstart is 1. This ensures compatibility with other
--- completion sources.
--- To make further modifications to the inserted text, {func}
--- can make use of `CompleteDonePre`.
--- If generating matches is potentially slow, `complete_check()`
--- should be used to avoid blocking and preserve editor
--- responsiveness.

View File

@ -313,6 +313,7 @@ typedef struct cpt_source_T {
int cs_max_matches; ///< Max items to display from this source
} cpt_source_T;
#define STARTCOL_NONE -9
/// Pointer to the array of completion sources
static cpt_source_T *cpt_sources_array;
/// Total number of completion sources specified in the 'cpt' option
@ -1336,6 +1337,62 @@ static int cp_compare_nearest(const void *a, const void *b)
return (score_a > score_b) ? 1 : (score_a < score_b) ? -1 : 0;
}
/// Constructs a new string by prepending text from the current line (from
/// startcol to compl_col) to the given source string. Stores the result in
/// dest.
static void prepend_startcol_text(String *dest, String *src, int startcol)
{
int prepend_len = compl_col - startcol;
int new_length = prepend_len + (int)src->size;
dest->size = (size_t)new_length;
dest->data = xmalloc((size_t)new_length + 1); // +1 for NUL
char *line = ml_get(curwin->w_cursor.lnum);
memmove(dest->data, line + startcol, (size_t)prepend_len);
memmove(dest->data + prepend_len, src->data, src->size);
dest->data[new_length] = NUL;
}
/// Returns the completion leader string adjusted for a specific source's
/// startcol. If the source's startcol is before compl_col, prepends text from
/// the buffer line to the original compl_leader.
static String *get_leader_for_startcol(compl_T *match, bool cached)
{
static String adjusted_leader = STRING_INIT;
if (match == NULL) {
API_CLEAR_STRING(adjusted_leader);
return NULL;
}
if (cpt_sources_array == NULL || compl_leader.data == NULL) {
goto theend;
}
int cpt_idx = match->cp_cpt_source_idx;
if (cpt_idx < 0 || compl_col <= 0) {
goto theend;
}
int startcol = cpt_sources_array[cpt_idx].cs_startcol;
if (startcol >= 0 && startcol < compl_col) {
int prepend_len = compl_col - startcol;
int new_length = prepend_len + (int)compl_leader.size;
if (cached && (size_t)new_length == adjusted_leader.size
&& adjusted_leader.data != NULL) {
return &adjusted_leader;
}
API_CLEAR_STRING(adjusted_leader);
prepend_startcol_text(&adjusted_leader, &compl_leader, startcol);
return &adjusted_leader;
}
theend:
return &compl_leader;
}
/// Set fuzzy score.
static void set_fuzzy_score(void)
{
@ -1343,9 +1400,12 @@ static void set_fuzzy_score(void)
return;
}
(void)get_leader_for_startcol(NULL, true); // Clear the cache
compl_T *comp = compl_first_match;
do {
comp->cp_score = fuzzy_match_str(comp->cp_str.data, compl_leader.data);
comp->cp_score = fuzzy_match_str(comp->cp_str.data,
get_leader_for_startcol(comp, true)->data);
comp = comp->cp_next;
} while (comp != NULL && !is_first_match(comp));
}
@ -1422,6 +1482,8 @@ static int ins_compl_build_pum(void)
match_count = xcalloc((size_t)cpt_sources_count, sizeof(int));
}
(void)get_leader_for_startcol(NULL, true); // Clear the cache
comp = compl_first_match;
do {
comp->cp_in_match_array = false;
@ -1432,9 +1494,11 @@ static int ins_compl_build_pum(void)
comp->cp_flags &= ~CP_ICASE;
}
String *leader = get_leader_for_startcol(comp, true);
if (!match_at_original_text(comp)
&& (compl_leader.data == NULL
|| ins_compl_equal(comp, compl_leader.data, compl_leader.size)
&& (leader->data == NULL
|| ins_compl_equal(comp, leader->data, leader->size)
|| (fuzzy_filter && comp->cp_score > 0))) {
// Limit number of items from each source if max_items is set.
bool match_limit_exceeded = false;
@ -2107,6 +2171,7 @@ static bool ins_compl_need_restart(void)
static void ins_compl_new_leader(void)
{
unsigned cur_cot_flags = get_cot_flags();
ins_compl_del_pum();
ins_compl_delete(true);
ins_compl_insert_bytes(compl_leader.data + get_compl_len(), -1);
@ -4272,7 +4337,7 @@ static bool get_next_completion_match(int type, ins_compl_next_state_T *st, pos_
case CTRL_X_FUNCTION:
if (ctrl_x_mode_normal()) { // Invoked by a func in 'cpt' option
get_cpt_func_completion_matches(st->func_cb, true);
get_cpt_func_completion_matches(st->func_cb);
} else {
expand_by_function(type, compl_pattern.data, NULL);
}
@ -4365,6 +4430,10 @@ static void prepare_cpt_compl_funcs(void)
while (*p == ',' || *p == ' ') { // Skip delimiters
p++;
}
if (*p == NUL) {
break;
}
Callback *cb = get_callback_if_cpt_func(p);
if (cb) {
int startcol;
@ -4376,7 +4445,10 @@ static void prepare_cpt_compl_funcs(void)
}
}
cpt_sources_array[idx].cs_startcol = startcol;
} else {
cpt_sources_array[idx].cs_startcol = STARTCOL_NONE;
}
(void)copy_option_part(&p, IObuff, IOSIZE, ","); // Advance p
idx++;
}
@ -4562,23 +4634,27 @@ static int ins_compl_get_exp(pos_T *ini)
/// "compl_leader" is used to omit some of the matches.
static void ins_compl_update_shown_match(void)
{
while (!ins_compl_equal(compl_shown_match,
compl_leader.data, compl_leader.size)
(void)get_leader_for_startcol(NULL, true); // Clear the cache
String *leader = get_leader_for_startcol(compl_shown_match, true);
while (!ins_compl_equal(compl_shown_match, leader->data, leader->size)
&& compl_shown_match->cp_next != NULL
&& !is_first_match(compl_shown_match->cp_next)) {
compl_shown_match = compl_shown_match->cp_next;
leader = get_leader_for_startcol(compl_shown_match, true);
}
// If we didn't find it searching forward, and compl_shows_dir is
// backward, find the last match.
if (compl_shows_dir_backward()
&& !ins_compl_equal(compl_shown_match, compl_leader.data, compl_leader.size)
&& !ins_compl_equal(compl_shown_match, leader->data, leader->size)
&& (compl_shown_match->cp_next == NULL
|| is_first_match(compl_shown_match->cp_next))) {
while (!ins_compl_equal(compl_shown_match, compl_leader.data, compl_leader.size)
while (!ins_compl_equal(compl_shown_match, leader->data, leader->size)
&& compl_shown_match->cp_prev != NULL
&& !is_first_match(compl_shown_match->cp_prev)) {
compl_shown_match = compl_shown_match->cp_prev;
leader = get_leader_for_startcol(compl_shown_match, true);
}
}
}
@ -4694,6 +4770,23 @@ void ins_compl_insert(bool move_cursor)
size_t leader_len = ins_compl_leader_len();
char *has_multiple = strchr(cp_str, '\n');
// Since completion sources may provide matches with varying start
// positions, insert only the portion of the match that corresponds to the
// intended replacement range.
if (cpt_sources_array != NULL) {
int cpt_idx = compl_shown_match->cp_cpt_source_idx;
if (cpt_idx >= 0 && compl_col >= 0) {
int startcol = cpt_sources_array[cpt_idx].cs_startcol;
if (startcol >= 0 && startcol < (int)compl_col) {
int skip = (int)compl_col - startcol;
if ((size_t)skip <= cp_str_len) {
cp_str_len -= (size_t)skip;
cp_str += skip;
}
}
}
}
// Make sure we don't go over the end of the string, this can happen with
// illegal bytes.
if (compl_len < (int)cp_str_len) {
@ -4837,10 +4930,12 @@ static int find_next_completion_match(bool allow_get_expansion, int todo, bool a
}
found_end = false;
}
String *leader = get_leader_for_startcol(compl_shown_match, false);
if (!match_at_original_text(compl_shown_match)
&& compl_leader.data != NULL
&& !ins_compl_equal(compl_shown_match,
compl_leader.data, compl_leader.size)
&& leader->data != NULL
&& !ins_compl_equal(compl_shown_match, leader->data, leader->size)
&& !(compl_fuzzy_match && compl_shown_match->cp_score > 0)) {
todo++;
} else {
@ -5006,7 +5101,7 @@ void ins_compl_check_keys(int frequency, bool in_compl_func)
// Check for a typed key. Do use mappings, otherwise vim_is_ctrl_x_key()
// can't do its work correctly.
int c = vpeekc_any();
if (c != NUL) {
if (c != NUL && !test_disable_char_avail) {
if (vim_is_ctrl_x_key(c) && c != Ctrl_X && c != Ctrl_R) {
c = safe_vgetc(); // Eat the character
compl_shows_dir = ins_compl_key2dir(c);
@ -5255,18 +5350,24 @@ static int get_cmdline_compl_info(char *line, colnr_T curs_col)
/// compl_col, compl_length, compl_pattern, and cpt_compl_pattern.
static void set_compl_globals(int startcol, colnr_T curs_col, bool is_cpt_compl)
{
if (startcol < 0 || startcol > curs_col) {
startcol = curs_col;
}
int len = curs_col - startcol;
if (is_cpt_compl) {
API_CLEAR_STRING(cpt_compl_pattern);
if (startcol < compl_col) {
prepend_startcol_text(&cpt_compl_pattern, &compl_orig_text, startcol);
return;
} else {
cpt_compl_pattern = copy_string(compl_orig_text, NULL);
}
} else {
if (startcol < 0 || startcol > curs_col) {
startcol = curs_col;
}
// Re-obtain line in case it has changed
char *line = ml_get(curwin->w_cursor.lnum);
// Re-obtain line in case it has changed
char *line = ml_get(curwin->w_cursor.lnum);
int len = curs_col - startcol;
String *pattern = is_cpt_compl ? &cpt_compl_pattern : &compl_pattern;
pattern->data = xstrnsave(line + startcol, (size_t)len);
pattern->size = (size_t)len;
if (!is_cpt_compl) {
compl_pattern = cbuf_to_string(line + startcol, (size_t)len);
compl_col = startcol;
compl_length = len;
}
@ -5390,7 +5491,10 @@ static int compl_get_info(char *line, int startcol, colnr_T curs_col, bool *line
if (ctrl_x_mode_normal() || ctrl_x_mode_register()
|| ((ctrl_x_mode & CTRL_X_WANT_IDENT)
&& !thesaurus_func_complete(ctrl_x_mode))) {
return get_normal_compl_info(line, startcol, curs_col);
if (get_normal_compl_info(line, startcol, curs_col) != OK) {
return FAIL;
}
*line_invalid = true; // 'cpt' func may have invalidated "line"
} else if (ctrl_x_mode_line_or_eval()) {
return get_wholeline_compl_info(line, curs_col);
} else if (ctrl_x_mode_files()) {
@ -5971,24 +6075,15 @@ static compl_T *remove_old_matches(void)
/// Retrieve completion matches using the callback function "cb" and store the
/// 'refresh:always' flag.
static void get_cpt_func_completion_matches(Callback *cb, bool restore_leader)
static void get_cpt_func_completion_matches(Callback *cb)
{
int startcol = cpt_sources_array[cpt_sources_index].cs_startcol;
API_CLEAR_STRING(cpt_compl_pattern);
if (startcol == -2 || startcol == -3) {
return;
}
if (restore_leader) { // Re-insert the text removed by ins_compl_delete()
ins_compl_insert_bytes(compl_orig_text.data + get_compl_len(), -1);
}
set_compl_globals(startcol, curwin->w_cursor.col, true);
if (restore_leader) {
ins_compl_delete(false); // Undo insertion
}
expand_by_function(0, cpt_compl_pattern.data, cb);
cpt_sources_array[cpt_sources_index].cs_refresh_always = compl_opt_refresh_always;
compl_opt_refresh_always = false;
@ -6009,6 +6104,9 @@ static void cpt_compl_refresh(void)
while (*p == ',' || *p == ' ') { // Skip delimiters
p++;
}
if (*p == NUL) {
break;
}
if (cpt_sources_array[cpt_sources_index].cs_refresh_always) {
Callback *cb = get_callback_if_cpt_func(p);
@ -6025,8 +6123,10 @@ static void cpt_compl_refresh(void)
}
cpt_sources_array[cpt_sources_index].cs_startcol = startcol;
if (ret == OK) {
get_cpt_func_completion_matches(cb, false);
get_cpt_func_completion_matches(cb);
}
} else {
cpt_sources_array[cpt_sources_index].cs_startcol = STARTCOL_NONE;
}
}

View File

@ -1444,15 +1444,12 @@ local options = {
name of a function or a |Funcref|. For |Funcref| values,
spaces must be escaped with a backslash ('\'), and commas with
double backslashes ('\\') (see |option-backslash|).
Unlike other sources, functions can provide completions starting
from a non-keyword character before the cursor, and their
start position for replacing text may differ from other sources.
If the Dict returned by the {func} includes {"refresh": "always"},
the function will be invoked again whenever the leading text
changes.
Completion matches are always inserted at the keyword
boundary, regardless of the column returned by {func} when
a:findstart is 1. This ensures compatibility with other
completion sources.
To make further modifications to the inserted text, {func}
can make use of |CompleteDonePre|.
If generating matches is potentially slow, |complete_check()|
should be used to avoid blocking and preserve editor
responsiveness.

View File

@ -137,8 +137,7 @@ func Test_omni_dash()
%d
set complete=o
exe "normal Gofind -\<C-n>"
" 'complete' inserts at 'iskeyword' boundary (so you get --help)
call assert_equal("find --help", getline('$'))
call assert_equal("find -help", getline('$'))
bwipe!
delfunc Omni
@ -368,7 +367,7 @@ func Test_CompleteDone_vevent_keys()
call assert_equal('spell', g:complete_type)
bwipe!
set completeopt& omnifunc& completefunc& spell& spelllang& dictionary&
set completeopt& omnifunc& completefunc& spell& spelllang& dictionary& complete&
autocmd! CompleteDone
delfunc OnDone
delfunc CompleteFunc
@ -1112,6 +1111,7 @@ func Test_completefunc_invalid_data()
exe "normal i\<C-N>"
call assert_equal('moon', getline(1))
set completefunc& complete&
delfunc! CompleteFunc
bw!
endfunc
@ -4943,4 +4943,120 @@ func Test_complete_fuzzy_omnifunc_backspace()
unlet g:do_complete
endfunc
" Test 'complete' containing F{func} that complete from nonkeyword
func Test_nonkeyword_trigger()
" Trigger expansion even when another char is waiting in the typehead
call Ntest_override("char_avail", 1)
let g:CallCount = 0
func! NonKeywordComplete(findstart, base)
let line = getline('.')->strpart(0, col('.') - 1)
let nonkeyword2 = len(line) > 1 && match(line[-2:-2], '\k') != 0
if a:findstart
return nonkeyword2 ? col('.') - 3 : (col('.') - 2)
else
let g:CallCount += 1
return [$"{a:base}foo", $"{a:base}bar"]
endif
endfunc
new
inoremap <buffer> <F2> <Cmd>let b:matches = complete_info(["matches"]).matches<CR>
inoremap <buffer> <F3> <Cmd>let b:selected = complete_info(["selected"]).selected<CR>
call setline(1, ['abc', 'abcd', 'fo', 'b', ''])
" Test 1a: Nonkeyword before cursor lists words with at least two letters
call feedkeys("GS=\<C-N>\<F2>\<Esc>0", 'tx!')
call assert_equal(['abc', 'abcd', 'fo'], b:matches->mapnew('v:val.word'))
call assert_equal('=abc', getline('.'))
" Test 1b: With F{func} nonkeyword collects matches
set complete=.,FNonKeywordComplete
for noselect in range(2)
if noselect
set completeopt+=noselect
endif
let g:CallCount = 0
call feedkeys("S=\<C-N>\<F2>\<Esc>0", 'tx!')
call assert_equal(['abc', 'abcd', 'fo', '=foo', '=bar'], b:matches->mapnew('v:val.word'))
call assert_equal(1, g:CallCount)
call assert_equal(noselect ? '=' : '=abc', getline('.'))
let g:CallCount = 0
call feedkeys("S->\<C-N>\<F2>\<Esc>0", 'tx!')
call assert_equal(['abc', 'abcd', 'fo', '->foo', '->bar'], b:matches->mapnew('v:val.word'))
call assert_equal(1, g:CallCount)
call assert_equal(noselect ? '->' : '->abc', getline('.'))
set completeopt&
endfor
" Test 1c: Keyword collects from {func}
let g:CallCount = 0
call feedkeys("Sa\<C-N>\<F2>\<Esc>0", 'tx!')
call assert_equal(['abc', 'abcd', 'afoo', 'abar'], b:matches->mapnew('v:val.word'))
call assert_equal(1, g:CallCount)
call assert_equal('abc', getline('.'))
set completeopt+=noselect
let g:CallCount = 0
call feedkeys("Sa\<C-N>\<F2>\<Esc>0", 'tx!')
call assert_equal(['abc', 'abcd', 'afoo', 'abar'], b:matches->mapnew('v:val.word'))
call assert_equal(1, g:CallCount)
call assert_equal('a', getline('.'))
" Test 1d: Nonkeyword after keyword collects items again
let g:CallCount = 0
call feedkeys("Sa\<C-N>#\<C-N>\<F2>\<Esc>0", 'tx!')
call assert_equal(['abc', 'abcd', 'fo', '#foo', '#bar'], b:matches->mapnew('v:val.word'))
call assert_equal(2, g:CallCount)
call assert_equal('a#', getline('.'))
set completeopt&
" Test 2: Filter nonkeyword and keyword matches with differet startpos
set completeopt+=menuone,noselect
call feedkeys("S#a\<C-N>b\<F2>\<F3>\<Esc>0", 'tx!')
call assert_equal(['abc', 'abcd', '#abar'], b:matches->mapnew('v:val.word'))
call assert_equal(-1, b:selected)
call assert_equal('#ab', getline('.'))
set completeopt+=fuzzy
call feedkeys("S#a\<C-N>b\<F2>\<F3>\<Esc>0", 'tx!')
call assert_equal(['#abar', 'abc', 'abcd'], b:matches->mapnew('v:val.word'))
call assert_equal(-1, b:selected)
call assert_equal('#ab', getline('.'))
set completeopt&
" Test 3: Navigate menu containing nonkeyword and keyword items
call feedkeys("S->\<C-N>\<F2>\<Esc>0", 'tx!')
call assert_equal(['abc', 'abcd', 'fo', '->foo', '->bar'], b:matches->mapnew('v:val.word'))
call assert_equal('->abc', getline('.'))
call feedkeys("S->" . repeat("\<C-N>", 3) . "\<Esc>0", 'tx!')
call assert_equal('->fo', getline('.'))
call feedkeys("S->" . repeat("\<C-N>", 4) . "\<Esc>0", 'tx!')
call assert_equal('->foo', getline('.'))
call feedkeys("S->" . repeat("\<C-N>", 4) . "\<C-P>\<Esc>0", 'tx!')
call assert_equal('->fo', getline('.'))
call feedkeys("S->" . repeat("\<C-N>", 5) . "\<Esc>0", 'tx!')
call assert_equal('->bar', getline('.'))
call feedkeys("S->" . repeat("\<C-N>", 5) . "\<C-P>\<Esc>0", 'tx!')
call assert_equal('->foo', getline('.'))
call feedkeys("S->" . repeat("\<C-N>", 6) . "\<Esc>0", 'tx!')
call assert_equal('->', getline('.'))
call feedkeys("S->" . repeat("\<C-N>", 7) . "\<Esc>0", 'tx!')
call assert_equal('->abc', getline('.'))
call feedkeys("S->" . repeat("\<C-P>", 7) . "\<Esc>0", 'tx!')
call assert_equal('->fo', getline('.'))
" Replace
call feedkeys("S# x y z\<Esc>0lR\<C-N>\<Esc>0", 'tx!')
call assert_equal('#abcy z', getline('.'))
call feedkeys("S# x y z\<Esc>0lR" . repeat("\<C-P>", 4) . "\<Esc>0", 'tx!')
call assert_equal('#bary z', getline('.'))
bw!
call Ntest_override("char_avail", 0)
delfunc NonKeywordComplete
set complete&
unlet g:CallCount
endfunc
" vim: shiftwidth=2 sts=2 expandtab nofoldenable