Skip to content

Commit

Permalink
vim-patch:9.0.2025: no cmdline completion for ++opt args (neovim#25657)
Browse files Browse the repository at this point in the history
Problem:  no cmdline completion for ++opt args
Solution: Add cmdline completion for :e ++opt=arg and :terminal
          [++options]

closes: vim/vim#13319

vim/vim@989426b

Co-authored-by: Yee Cheng Chin <ychin.git@gmail.com>
  • Loading branch information
zeertzjq and ychin committed Oct 15, 2023
1 parent d974a3d commit 75b488d
Show file tree
Hide file tree
Showing 6 changed files with 176 additions and 8 deletions.
1 change: 1 addition & 0 deletions runtime/doc/cmdline.txt
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,7 @@ When editing the command-line, a few commands can be used to complete the
word before the cursor. This is available for:

- Command names: At the start of the command-line.
- |++opt| values.
- Tags: Only after the ":tag" command.
- File names: Only after a command that accepts a file name or a setting for
an option that can be set to a file name. This is called file name
Expand Down
41 changes: 34 additions & 7 deletions src/nvim/cmdexpand.c
Original file line number Diff line number Diff line change
Expand Up @@ -1567,6 +1567,20 @@ static void set_context_for_wildcard_arg(exarg_T *eap, const char *arg, bool use
}
}

/// Set the completion context for the "++opt=arg" argument. Always returns NULL.
static const char *set_context_in_argopt(expand_T *xp, const char *arg)
{
char *p = vim_strchr(arg, '=');
if (p == NULL) {
xp->xp_pattern = (char *)arg;
} else {
xp->xp_pattern = p + 1;
}

xp->xp_context = EXPAND_ARGOPT;
return NULL;
}

/// Set the completion context for the :filter command. Returns a pointer to the
/// next command after the :filter command.
static const char *set_context_in_filter_cmd(expand_T *xp, const char *arg)
Expand Down Expand Up @@ -2238,13 +2252,23 @@ static const char *set_one_cmd_context(expand_T *xp, const char *buff)

const char *arg = skipwhite(p);

// Skip over ++argopt argument
if ((ea.argt & EX_ARGOPT) && *arg != NUL && strncmp(arg, "++", 2) == 0) {
p = arg;
while (*p && !ascii_isspace(*p)) {
MB_PTR_ADV(p);
// Does command allow "++argopt" argument?
if (ea.argt & EX_ARGOPT) {
while (*arg != NUL && strncmp(arg, "++", 2) == 0) {
p = arg + 2;
while (*p && !ascii_isspace(*p)) {
MB_PTR_ADV(p);
}

// Still touching the command after "++"?
if (*p == NUL) {
if (ea.argt & EX_ARGOPT) {
return set_context_in_argopt(xp, arg + 2);
}
}

arg = skipwhite(p);
}
arg = skipwhite(p);
}

if (ea.cmdidx == CMD_write || ea.cmdidx == CMD_update) {
Expand Down Expand Up @@ -2786,6 +2810,8 @@ static int ExpandFromContext(expand_T *xp, char *pat, char ***matches, int *numM
ret = ExpandSettingSubtract(xp, &regmatch, numMatches, matches);
} else if (xp->xp_context == EXPAND_MAPPINGS) {
ret = ExpandMappings(pat, &regmatch, numMatches, matches);
} else if (xp->xp_context == EXPAND_ARGOPT) {
ret = expand_argopt(pat, xp, &regmatch, matches, numMatches);
} else if (xp->xp_context == EXPAND_USER_DEFINED) {
ret = ExpandUserDefined(pat, xp, &regmatch, matches, numMatches);
} else {
Expand Down Expand Up @@ -2883,7 +2909,8 @@ void ExpandGeneric(const char *const pat, expand_T *xp, regmatch_T *regmatch, ch
&& xp->xp_context != EXPAND_MENUNAMES
&& xp->xp_context != EXPAND_STRING_SETTING
&& xp->xp_context != EXPAND_MENUS
&& xp->xp_context != EXPAND_SCRIPTNAMES;
&& xp->xp_context != EXPAND_SCRIPTNAMES
&& xp->xp_context != EXPAND_ARGOPT;

// <SNR> functions should be sorted to the end.
const bool funcsort = xp->xp_context == EXPAND_EXPRESSION
Expand Down
4 changes: 3 additions & 1 deletion src/nvim/cmdexpand_defs.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ enum { EXPAND_BUF_LEN = 256, };

/// used for completion on the command line
typedef struct expand {
char *xp_pattern; ///< start of item to expand
char *xp_pattern; ///< start of item to expand, guaranteed
///< to be part of xp_line
int xp_context; ///< type of expansion
size_t xp_pattern_len; ///< bytes in xp_pattern before cursor
xp_prefix_T xp_prefix;
Expand Down Expand Up @@ -104,6 +105,7 @@ enum {
EXPAND_RUNTIME,
EXPAND_STRING_SETTING,
EXPAND_SETTING_SUBTRACT,
EXPAND_ARGOPT,
EXPAND_CHECKHEALTH,
EXPAND_LUA,
};
Expand Down
82 changes: 82 additions & 0 deletions src/nvim/ex_docmd.c
Original file line number Diff line number Diff line change
Expand Up @@ -4087,6 +4087,22 @@ int get_bad_opt(const char *p, exarg_T *eap)
return OK;
}

/// Function given to ExpandGeneric() to obtain the list of bad= names.
static char *get_bad_name(expand_T *xp FUNC_ATTR_UNUSED, int idx)
{
// Note: Keep this in sync with get_bad_opt().
static char *(p_bad_values[]) = {
"?",
"keep",
"drop",
};

if (idx < (int)ARRAY_SIZE(p_bad_values)) {
return p_bad_values[idx];
}
return NULL;
}

/// Get "++opt=arg" argument.
///
/// @return FAIL or OK.
Expand All @@ -4096,6 +4112,8 @@ static int getargopt(exarg_T *eap)
int *pp = NULL;
int bad_char_idx;

// Note: Keep this in sync with get_argopt_name.

// ":edit ++[no]bin[ary] file"
if (strncmp(arg, "bin", 3) == 0 || strncmp(arg, "nobin", 5) == 0) {
if (*arg == 'n') {
Expand Down Expand Up @@ -4174,6 +4192,70 @@ static int getargopt(exarg_T *eap)
return OK;
}

/// Function given to ExpandGeneric() to obtain the list of ++opt names.
static char *get_argopt_name(expand_T *xp FUNC_ATTR_UNUSED, int idx)
{
// Note: Keep this in sync with getargopt().
static char *(p_opt_values[]) = {
"fileformat=",
"encoding=",
"binary",
"nobinary",
"bad=",
"edit",
"p",
};

if (idx < (int)ARRAY_SIZE(p_opt_values)) {
return p_opt_values[idx];
}
return NULL;
}

/// Command-line expansion for ++opt=name.
int expand_argopt(char *pat, expand_T *xp, regmatch_T *rmp, char ***matches, int *numMatches)
{
if (xp->xp_pattern > xp->xp_line && *(xp->xp_pattern - 1) == '=') {
CompleteListItemGetter cb = NULL;

char *name_end = xp->xp_pattern - 1;
if (name_end - xp->xp_line >= 2
&& strncmp(name_end - 2, "ff", 2) == 0) {
cb = get_fileformat_name;
} else if (name_end - xp->xp_line >= 10
&& strncmp(name_end - 10, "fileformat", 10) == 0) {
cb = get_fileformat_name;
} else if (name_end - xp->xp_line >= 3
&& strncmp(name_end - 3, "enc", 3) == 0) {
cb = get_encoding_name;
} else if (name_end - xp->xp_line >= 8
&& strncmp(name_end - 8, "encoding", 8) == 0) {
cb = get_encoding_name;
} else if (name_end - xp->xp_line >= 3
&& strncmp(name_end - 3, "bad", 3) == 0) {
cb = get_bad_name;
}

if (cb != NULL) {
ExpandGeneric(pat, xp, rmp, matches, numMatches, cb, false);
return OK;
}
return FAIL;
}

// Special handling of "ff" which acts as a short form of
// "fileformat", as "ff" is not a substring of it.
if (strcmp(xp->xp_pattern, "ff") == 0) {
*matches = xmalloc(sizeof(char *));
*numMatches = 1;
(*matches)[0] = xstrdup("fileformat=");
return OK;
}

ExpandGeneric(pat, xp, rmp, matches, numMatches, get_argopt_name, false);
return OK;
}

/// Handle the argument for a tabpage related ex command.
/// When an error is encountered then eap->errmsg is set.
///
Expand Down
11 changes: 11 additions & 0 deletions src/nvim/optionstr.c
Original file line number Diff line number Diff line change
Expand Up @@ -1490,6 +1490,17 @@ int expand_set_fileformat(optexpand_T *args, int *numMatches, char ***matches)
matches);
}

/// Function given to ExpandGeneric() to obtain the possible arguments of the
/// fileformat options.
char *get_fileformat_name(expand_T *xp FUNC_ATTR_UNUSED, int idx)
{
if (idx >= (int)ARRAY_SIZE(p_ff_values)) {
return NULL;
}

return p_ff_values[idx];
}

/// The 'fileformats' option is changed.
const char *did_set_fileformats(optset_T *args)
{
Expand Down
45 changes: 45 additions & 0 deletions test/old/testdir/test_cmdline.vim
Original file line number Diff line number Diff line change
Expand Up @@ -1039,6 +1039,51 @@ func Test_cmdline_complete_expression()
unlet g:SomeVar
endfunc

func Test_cmdline_complete_argopt()
" completion for ++opt=arg for file commands
call assert_equal('fileformat=', getcompletion('edit ++', 'cmdline')[0])
call assert_equal('encoding=', getcompletion('read ++e', 'cmdline')[0])
call assert_equal('edit', getcompletion('read ++bin ++edi', 'cmdline')[0])

call assert_equal(['fileformat='], getcompletion('edit ++ff', 'cmdline'))

call assert_equal('dos', getcompletion('write ++ff=d', 'cmdline')[0])
call assert_equal('mac', getcompletion('args ++fileformat=m', 'cmdline')[0])
call assert_equal('utf-8', getcompletion('split ++enc=ut*-8', 'cmdline')[0])
call assert_equal('latin1', getcompletion('tabedit ++encoding=lati', 'cmdline')[0])
call assert_equal('keep', getcompletion('edit ++bad=k', 'cmdline')[0])

call assert_equal([], getcompletion('edit ++bogus=', 'cmdline'))

" completion should skip the ++opt and continue
call writefile([], 'Xaaaaa.txt', 'D')
call feedkeys(":split ++enc=latin1 Xaaa\<C-A>\<C-B>\"\<CR>", 'xt')
call assert_equal('"split ++enc=latin1 Xaaaaa.txt', @:)

if has('terminal')
" completion for terminal's [options]
call assert_equal('close', getcompletion('terminal ++cl*e', 'cmdline')[0])
call assert_equal('hidden', getcompletion('terminal ++open ++hidd', 'cmdline')[0])
call assert_equal('term', getcompletion('terminal ++kill=ter', 'cmdline')[0])

call assert_equal([], getcompletion('terminal ++bogus=', 'cmdline'))

" :terminal completion should skip the ++opt when considering what is the
" first option, which is a list of shell commands, unlike second option
" onwards.
let first_param = getcompletion('terminal ', 'cmdline')
let second_param = getcompletion('terminal foo ', 'cmdline')
let skipped_opt_param = getcompletion('terminal ++close ', 'cmdline')
call assert_equal(first_param, skipped_opt_param)
call assert_notequal(first_param, second_param)
endif
endfunc

" Unique function name for completion below
func s:WeirdFunc()
echo 'weird'
endfunc

" Test for various command-line completion
func Test_cmdline_complete_various()
" completion for a command starting with a comment
Expand Down

0 comments on commit 75b488d

Please sign in to comment.