# HG changeset patch # User Christian Brabandt # Date 1697353870 -7200 # Node ID 46d449fd4fe4e487b3c2090cc9bd5dbb923d3267 # Parent 8498422688a95e237f1abde895fbe4b1bef7af96 patch 9.0.2025: no cmdline completion for ++opt args Commit: https://github.com/vim/vim/commit/989426be6e9ae23d2413943890206cbe15d9df38 Author: Yee Cheng Chin Date: Sat Oct 14 11:46:51 2023 +0200 patch 9.0.2025: no cmdline completion for ++opt args Problem: no cmdline completion for ++opt args Solution: Add cmdline completion for :e ++opt=arg and :terminal [++options] closes: #13319 Signed-off-by: Christian Brabandt Co-authored-by: Yee Cheng Chin diff --git a/runtime/doc/cmdline.txt b/runtime/doc/cmdline.txt --- a/runtime/doc/cmdline.txt +++ b/runtime/doc/cmdline.txt @@ -387,6 +387,7 @@ When editing the command-line, a few com 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 diff --git a/src/cmdexpand.c b/src/cmdexpand.c --- a/src/cmdexpand.c +++ b/src/cmdexpand.c @@ -1769,6 +1769,45 @@ set_context_for_wildcard_arg( } /* + * Set the completion context for the "++opt=arg" argument. Always returns + * NULL. + */ + static char_u * +set_context_in_argopt(expand_T *xp, char_u *arg) +{ + char_u *p; + + p = vim_strchr(arg, '='); + if (p == NULL) + xp->xp_pattern = arg; + else + xp->xp_pattern = p + 1; + + xp->xp_context = EXPAND_ARGOPT; + return NULL; +} + +#ifdef FEAT_TERMINAL +/* + * Set the completion context for :terminal's [options]. Always returns NULL. + */ + static char_u * +set_context_in_terminalopt(expand_T *xp, char_u *arg) +{ + char_u *p; + + p = vim_strchr(arg, '='); + if (p == NULL) + xp->xp_pattern = arg; + else + xp->xp_pattern = p + 1; + + xp->xp_context = EXPAND_TERMINALOPT; + return NULL; +} +#endif + +/* * Set the completion context for the :filter command. Returns a pointer to the * next command after the :filter command. */ @@ -2491,13 +2530,28 @@ set_one_cmd_context( arg = skipwhite(p); - // Skip over ++argopt argument - if ((ea.argt & EX_ARGOPT) && *arg != NUL && STRNCMP(arg, "++", 2) == 0) + // Does command allow "++argopt" argument? + if ((ea.argt & EX_ARGOPT) || ea.cmdidx == CMD_terminal) { - p = arg; - while (*p && !vim_isspace(*p)) - MB_PTR_ADV(p); - arg = skipwhite(p); + while (*arg != NUL && STRNCMP(arg, "++", 2) == 0) + { + p = arg + 2; + while (*p && !vim_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); +#ifdef FEAT_TERMINAL + if (ea.cmdidx == CMD_terminal) + return set_context_in_terminalopt(xp, arg + 2); +#endif + } + + arg = skipwhite(p); + } } if (ea.cmdidx == CMD_write || ea.cmdidx == CMD_update) @@ -3120,6 +3174,12 @@ ExpandFromContext( ret = ExpandSettingSubtract(xp, ®match, numMatches, matches); else if (xp->xp_context == EXPAND_MAPPINGS) ret = ExpandMappings(pat, ®match, numMatches, matches); + else if (xp->xp_context == EXPAND_ARGOPT) + ret = expand_argopt(pat, xp, ®match, matches, numMatches); +#if defined(FEAT_TERMINAL) + else if (xp->xp_context == EXPAND_TERMINALOPT) + ret = expand_terminal_opt(pat, xp, ®match, matches, numMatches); +#endif #if defined(FEAT_EVAL) else if (xp->xp_context == EXPAND_USER_DEFINED) ret = ExpandUserDefined(pat, xp, ®match, matches, numMatches); @@ -3253,7 +3313,9 @@ ExpandGeneric( if (!fuzzy && 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 + && xp->xp_context != EXPAND_TERMINALOPT) sort_matches = TRUE; // functions should be sorted to the end. diff --git a/src/ex_docmd.c b/src/ex_docmd.c --- a/src/ex_docmd.c +++ b/src/ex_docmd.c @@ -5408,6 +5408,25 @@ get_bad_opt(char_u *p, exarg_T *eap) } /* + * Function given to ExpandGeneric() to obtain the list of bad= names. + */ + static char_u * +get_bad_name(expand_T *xp UNUSED, int idx) +{ + // Note: Keep this in sync with getargopt. + static char *(p_bad_values[]) = + { + "?", + "keep", + "drop", + }; + + if (idx < (int)ARRAY_LENGTH(p_bad_values)) + return (char_u*)p_bad_values[idx]; + return NULL; +} + +/* * Get "++opt=arg" argument. * Return FAIL or OK. */ @@ -5419,6 +5438,8 @@ getargopt(exarg_T *eap) int bad_char_idx; char_u *p; + // 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) { @@ -5499,6 +5520,96 @@ getargopt(exarg_T *eap) return OK; } +/* + * Function given to ExpandGeneric() to obtain the list of ++opt names. + */ + static char_u * +get_argopt_name(expand_T *xp UNUSED, int idx) +{ + // Note: Keep this in sync with getargopt. + static char *(p_opt_values[]) = + { + "fileformat=", + "encoding=", + "binary", + "nobinary", + "bad=", + "edit", + }; + + if (idx < (int)ARRAY_LENGTH(p_opt_values)) + return (char_u*)p_opt_values[idx]; + return NULL; +} + +/* + * Command-line expansion for ++opt=name. + */ + int +expand_argopt( + char_u *pat, + expand_T *xp, + regmatch_T *rmp, + char_u ***matches, + int *numMatches) +{ + if (xp->xp_pattern > xp->xp_line && *(xp->xp_pattern-1) == '=') + { + char_u *(*cb)(expand_T *, int) = NULL; + + char_u *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) + { + return ExpandGeneric( + pat, + xp, + rmp, + matches, + numMatches, + cb, + FALSE); + } + 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 = ALLOC_MULT(char_u *, 1); + if (*matches == NULL) + return FAIL; + *numMatches = 1; + (*matches)[0] = vim_strsave((char_u*)"fileformat="); + return OK; + } + + return ExpandGeneric( + pat, + xp, + rmp, + matches, + numMatches, + get_argopt_name, + FALSE); +} + static void ex_autocmd(exarg_T *eap) { diff --git a/src/optionstr.c b/src/optionstr.c --- a/src/optionstr.c +++ b/src/optionstr.c @@ -2105,6 +2105,19 @@ expand_set_fileformat(optexpand_T *args, } /* + * Function given to ExpandGeneric() to obtain the possible arguments of the + * fileformat options. + */ + char_u * +get_fileformat_name(expand_T *xp UNUSED, int idx) +{ + if (idx >= (int)ARRAY_LENGTH(p_ff_values)) + return NULL; + + return (char_u*)p_ff_values[idx]; +} + +/* * The 'fileformats' option is changed. */ char * diff --git a/src/proto/ex_docmd.pro b/src/proto/ex_docmd.pro --- a/src/proto/ex_docmd.pro +++ b/src/proto/ex_docmd.pro @@ -30,6 +30,7 @@ int expand_filename(exarg_T *eap, char_u void separate_nextcmd(exarg_T *eap, int keep_backslash); char_u *skip_cmd_arg(char_u *p, int rembs); int get_bad_opt(char_u *p, exarg_T *eap); +int expand_argopt(char_u *pat, expand_T *xp, regmatch_T *rmp, char_u ***matches, int *numMatches); int ends_excmd(int c); int ends_excmd2(char_u *cmd_start, char_u *cmd); char_u *find_nextcmd(char_u *p); diff --git a/src/proto/optionstr.pro b/src/proto/optionstr.pro --- a/src/proto/optionstr.pro +++ b/src/proto/optionstr.pro @@ -189,6 +189,7 @@ int expand_set_wildoptions(optexpand_T * int expand_set_winaltkeys(optexpand_T *args, int *numMatches, char_u ***matches); int expand_set_wincolor(optexpand_T *args, int *numMatches, char_u ***matches); int check_ff_value(char_u *p); +char_u *get_fileformat_name(expand_T *xp, int idx); void save_clear_shm_value(void); void restore_shm_value(void); /* vim: set ft=c : */ diff --git a/src/proto/terminal.pro b/src/proto/terminal.pro --- a/src/proto/terminal.pro +++ b/src/proto/terminal.pro @@ -2,6 +2,7 @@ void init_job_options(jobopt_T *opt); buf_T *term_start(typval_T *argvar, char **argv, jobopt_T *opt, int flags); void ex_terminal(exarg_T *eap); +int expand_terminal_opt(char_u *pat, expand_T *xp, regmatch_T *rmp, char_u ***matches, int *numMatches); int term_write_session(FILE *fd, win_T *wp, hashtab_T *terminal_bufs); int term_should_restore(buf_T *buf); void free_terminal(buf_T *buf); diff --git a/src/structs.h b/src/structs.h --- a/src/structs.h +++ b/src/structs.h @@ -603,7 +603,8 @@ typedef enum { */ typedef struct expand { - char_u *xp_pattern; // start of item to expand + char_u *xp_pattern; // start of item to expand, guaranteed + // to be part of xp_line int xp_context; // type of expansion int xp_pattern_len; // bytes in xp_pattern before cursor xp_prefix_T xp_prefix; diff --git a/src/terminal.c b/src/terminal.c --- a/src/terminal.c +++ b/src/terminal.c @@ -818,6 +818,8 @@ ex_terminal(exarg_T *eap) ep = NULL; } + // Note: Keep this in sync with get_terminalopt_name. + # define OPTARG_HAS(name) ((int)(p - cmd) == sizeof(name) - 1 \ && STRNICMP(cmd, name, sizeof(name) - 1) == 0) if (OPTARG_HAS("close")) @@ -969,6 +971,96 @@ theend: vim_free(opt.jo_eof_chars); } + static char_u * +get_terminalopt_name(expand_T *xp UNUSED, int idx) +{ + // Note: Keep this in sync with ex_terminal. + static char *(p_termopt_values[]) = + { + "close", + "noclose", + "open", + "curwin", + "hidden", + "norestore", + "shell", + "kill=", + "rows=", + "cols=", + "eof=", + "type=", + "api=", + }; + + if (idx < (int)ARRAY_LENGTH(p_termopt_values)) + return (char_u*)p_termopt_values[idx]; + return NULL; +} + + static char_u * +get_termkill_name(expand_T *xp UNUSED, int idx) +{ + // These are platform-specific values used for job_stop(). They are defined + // in each platform's mch_signal_job(). Just use a unified auto-complete + // list for simplicity. + static char *(p_termkill_values[]) = + { + "term", + "hup", + "quit", + "int", + "kill", + "winch", + }; + + if (idx < (int)ARRAY_LENGTH(p_termkill_values)) + return (char_u*)p_termkill_values[idx]; + return NULL; +} + +/* + * Command-line expansion for :terminal [options] + */ + int +expand_terminal_opt( + char_u *pat, + expand_T *xp, + regmatch_T *rmp, + char_u ***matches, + int *numMatches) +{ + if (xp->xp_pattern > xp->xp_line && *(xp->xp_pattern-1) == '=') + { + char_u *(*cb)(expand_T *, int) = NULL; + + char_u *name_end = xp->xp_pattern - 1; + if (name_end - xp->xp_line >= 4 + && STRNCMP(name_end - 4, "kill", 4) == 0) + cb = get_termkill_name; + + if (cb != NULL) + { + return ExpandGeneric( + pat, + xp, + rmp, + matches, + numMatches, + cb, + FALSE); + } + return FAIL; + } + return ExpandGeneric( + pat, + xp, + rmp, + matches, + numMatches, + get_terminalopt_name, + FALSE); +} + #if defined(FEAT_SESSION) || defined(PROTO) /* * Write a :terminal command to the session file to restore the terminal in diff --git a/src/testdir/test_cmdline.vim b/src/testdir/test_cmdline.vim --- a/src/testdir/test_cmdline.vim +++ b/src/testdir/test_cmdline.vim @@ -1083,6 +1083,46 @@ 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\\\"\", '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' diff --git a/src/version.c b/src/version.c --- a/src/version.c +++ b/src/version.c @@ -705,6 +705,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ /**/ + 2025, +/**/ 2024, /**/ 2023, diff --git a/src/vim.h b/src/vim.h --- a/src/vim.h +++ b/src/vim.h @@ -824,6 +824,8 @@ extern int (*dyn_libintl_wputenv)(const #define EXPAND_RUNTIME 53 #define EXPAND_STRING_SETTING 54 #define EXPAND_SETTING_SUBTRACT 55 +#define EXPAND_ARGOPT 56 +#define EXPAND_TERMINALOPT 57 // Values for exmode_active (0 is no exmode) #define EXMODE_NORMAL 1