diff options
author | Bram Moolenaar <Bram@vim.org> | 2019-08-18 21:44:12 +0200 |
---|---|---|
committer | Bram Moolenaar <Bram@vim.org> | 2019-08-18 21:44:12 +0200 |
commit | 66b51420e0c8d49bcf6786b792c938d6099e3393 (patch) | |
tree | 04d7f116a3ae2360ba8b68c2f4ce9544afb1e7f4 /src/cmdexpand.c | |
parent | db1085a5630ffdaa2e9f342c06cc739ebdf1a99a (diff) |
patch 8.1.1886: command line expansion code is spread outv8.1.1886
Problem: Command line expansion code is spread out.
Solution: Move the code to cmdexpand.c. (Yegappan Lakshmanan, closes #4831)
Diffstat (limited to 'src/cmdexpand.c')
-rw-r--r-- | src/cmdexpand.c | 2031 |
1 files changed, 2031 insertions, 0 deletions
diff --git a/src/cmdexpand.c b/src/cmdexpand.c new file mode 100644 index 0000000000..c347c1aba8 --- /dev/null +++ b/src/cmdexpand.c @@ -0,0 +1,2031 @@ +/* vi:set ts=8 sts=4 sw=4 noet: + * + * VIM - Vi IMproved by Bram Moolenaar + * + * Do ":help uganda" in Vim to read copying and usage conditions. + * Do ":help credits" in Vim to see a list of people who contributed. + * See README.txt for an overview of the Vim source code. + */ + +/* + * cmdexpand.c: functions for command-line completion + */ + +#include "vim.h" + +static int cmd_showtail; // Only show path tail in lists ? + +static void set_expand_context(expand_T *xp); +static int ExpandFromContext(expand_T *xp, char_u *, int *, char_u ***, int); +static int expand_showtail(expand_T *xp); +#ifdef FEAT_CMDL_COMPL +static int expand_shellcmd(char_u *filepat, int *num_file, char_u ***file, int flagsarg); +static int ExpandRTDir(char_u *pat, int flags, int *num_file, char_u ***file, char *dirname[]); +static int ExpandPackAddDir(char_u *pat, int *num_file, char_u ***file); +# if defined(FEAT_EVAL) +static int ExpandUserDefined(expand_T *xp, regmatch_T *regmatch, int *num_file, char_u ***file); +static int ExpandUserList(expand_T *xp, int *num_file, char_u ***file); +# endif +#endif + +#if defined(FEAT_CMDL_COMPL) || defined(PROTO) + static int +sort_func_compare(const void *s1, const void *s2) +{ + char_u *p1 = *(char_u **)s1; + char_u *p2 = *(char_u **)s2; + + if (*p1 != '<' && *p2 == '<') return -1; + if (*p1 == '<' && *p2 != '<') return 1; + return STRCMP(p1, p2); +} +#endif + + static void +ExpandEscape( + expand_T *xp, + char_u *str, + int numfiles, + char_u **files, + int options) +{ + int i; + char_u *p; + + // May change home directory back to "~" + if (options & WILD_HOME_REPLACE) + tilde_replace(str, numfiles, files); + + if (options & WILD_ESCAPE) + { + if (xp->xp_context == EXPAND_FILES + || xp->xp_context == EXPAND_FILES_IN_PATH + || xp->xp_context == EXPAND_SHELLCMD + || xp->xp_context == EXPAND_BUFFERS + || xp->xp_context == EXPAND_DIRECTORIES) + { + // Insert a backslash into a file name before a space, \, %, # + // and wildmatch characters, except '~'. + for (i = 0; i < numfiles; ++i) + { + // for ":set path=" we need to escape spaces twice + if (xp->xp_backslash == XP_BS_THREE) + { + p = vim_strsave_escaped(files[i], (char_u *)" "); + if (p != NULL) + { + vim_free(files[i]); + files[i] = p; +#if defined(BACKSLASH_IN_FILENAME) + p = vim_strsave_escaped(files[i], (char_u *)" "); + if (p != NULL) + { + vim_free(files[i]); + files[i] = p; + } +#endif + } + } +#ifdef BACKSLASH_IN_FILENAME + p = vim_strsave_fnameescape(files[i], FALSE); +#else + p = vim_strsave_fnameescape(files[i], xp->xp_shell); +#endif + if (p != NULL) + { + vim_free(files[i]); + files[i] = p; + } + + // If 'str' starts with "\~", replace "~" at start of + // files[i] with "\~". + if (str[0] == '\\' && str[1] == '~' && files[i][0] == '~') + escape_fname(&files[i]); + } + xp->xp_backslash = XP_BS_NONE; + + // If the first file starts with a '+' escape it. Otherwise it + // could be seen as "+cmd". + if (*files[0] == '+') + escape_fname(&files[0]); + } + else if (xp->xp_context == EXPAND_TAGS) + { + // Insert a backslash before characters in a tag name that + // would terminate the ":tag" command. + for (i = 0; i < numfiles; ++i) + { + p = vim_strsave_escaped(files[i], (char_u *)"\\|\""); + if (p != NULL) + { + vim_free(files[i]); + files[i] = p; + } + } + } + } +} + +/* + * Return FAIL if this is not an appropriate context in which to do + * completion of anything, return OK if it is (even if there are no matches). + * For the caller, this means that the character is just passed through like a + * normal character (instead of being expanded). This allows :s/^I^D etc. + */ + int +nextwild( + expand_T *xp, + int type, + int options, // extra options for ExpandOne() + int escape) // if TRUE, escape the returned matches +{ + cmdline_info_T *ccline = get_cmdline_info(); + int i, j; + char_u *p1; + char_u *p2; + int difflen; + int v; + + if (xp->xp_numfiles == -1) + { + set_expand_context(xp); + cmd_showtail = expand_showtail(xp); + } + + if (xp->xp_context == EXPAND_UNSUCCESSFUL) + { + beep_flush(); + return OK; // Something illegal on command line + } + if (xp->xp_context == EXPAND_NOTHING) + { + // Caller can use the character as a normal char instead + return FAIL; + } + + msg_puts("..."); // show that we are busy + out_flush(); + + i = (int)(xp->xp_pattern - ccline->cmdbuff); + xp->xp_pattern_len = ccline->cmdpos - i; + + if (type == WILD_NEXT || type == WILD_PREV) + { + // Get next/previous match for a previous expanded pattern. + p2 = ExpandOne(xp, NULL, NULL, 0, type); + } + else + { + // Translate string into pattern and expand it. + if ((p1 = addstar(xp->xp_pattern, xp->xp_pattern_len, + xp->xp_context)) == NULL) + p2 = NULL; + else + { + int use_options = options | + WILD_HOME_REPLACE|WILD_ADD_SLASH|WILD_SILENT; + if (escape) + use_options |= WILD_ESCAPE; + + if (p_wic) + use_options += WILD_ICASE; + p2 = ExpandOne(xp, p1, + vim_strnsave(&ccline->cmdbuff[i], xp->xp_pattern_len), + use_options, type); + vim_free(p1); + // longest match: make sure it is not shorter, happens with :help + if (p2 != NULL && type == WILD_LONGEST) + { + for (j = 0; j < xp->xp_pattern_len; ++j) + if (ccline->cmdbuff[i + j] == '*' + || ccline->cmdbuff[i + j] == '?') + break; + if ((int)STRLEN(p2) < j) + VIM_CLEAR(p2); + } + } + } + + if (p2 != NULL && !got_int) + { + difflen = (int)STRLEN(p2) - xp->xp_pattern_len; + if (ccline->cmdlen + difflen + 4 > ccline->cmdbufflen) + { + v = realloc_cmdbuff(ccline->cmdlen + difflen + 4); + xp->xp_pattern = ccline->cmdbuff + i; + } + else + v = OK; + if (v == OK) + { + mch_memmove(&ccline->cmdbuff[ccline->cmdpos + difflen], + &ccline->cmdbuff[ccline->cmdpos], + (size_t)(ccline->cmdlen - ccline->cmdpos + 1)); + mch_memmove(&ccline->cmdbuff[i], p2, STRLEN(p2)); + ccline->cmdlen += difflen; + ccline->cmdpos += difflen; + } + } + vim_free(p2); + + redrawcmd(); + cursorcmd(); + + // When expanding a ":map" command and no matches are found, assume that + // the key is supposed to be inserted literally + if (xp->xp_context == EXPAND_MAPPINGS && p2 == NULL) + return FAIL; + + if (xp->xp_numfiles <= 0 && p2 == NULL) + beep_flush(); + else if (xp->xp_numfiles == 1) + // free expanded pattern + (void)ExpandOne(xp, NULL, NULL, 0, WILD_FREE); + + return OK; +} + +/* + * Do wildcard expansion on the string 'str'. + * Chars that should not be expanded must be preceded with a backslash. + * Return a pointer to allocated memory containing the new string. + * Return NULL for failure. + * + * "orig" is the originally expanded string, copied to allocated memory. It + * should either be kept in orig_save or freed. When "mode" is WILD_NEXT or + * WILD_PREV "orig" should be NULL. + * + * Results are cached in xp->xp_files and xp->xp_numfiles, except when "mode" + * is WILD_EXPAND_FREE or WILD_ALL. + * + * mode = WILD_FREE: just free previously expanded matches + * mode = WILD_EXPAND_FREE: normal expansion, do not keep matches + * mode = WILD_EXPAND_KEEP: normal expansion, keep matches + * mode = WILD_NEXT: use next match in multiple match, wrap to first + * mode = WILD_PREV: use previous match in multiple match, wrap to first + * mode = WILD_ALL: return all matches concatenated + * mode = WILD_LONGEST: return longest matched part + * mode = WILD_ALL_KEEP: get all matches, keep matches + * + * options = WILD_LIST_NOTFOUND: list entries without a match + * options = WILD_HOME_REPLACE: do home_replace() for buffer names + * options = WILD_USE_NL: Use '\n' for WILD_ALL + * options = WILD_NO_BEEP: Don't beep for multiple matches + * options = WILD_ADD_SLASH: add a slash after directory names + * options = WILD_KEEP_ALL: don't remove 'wildignore' entries + * options = WILD_SILENT: don't print warning messages + * options = WILD_ESCAPE: put backslash before special chars + * options = WILD_ICASE: ignore case for files + * + * The variables xp->xp_context and xp->xp_backslash must have been set! + */ + char_u * +ExpandOne( + expand_T *xp, + char_u *str, + char_u *orig, // allocated copy of original of expanded string + int options, + int mode) +{ + char_u *ss = NULL; + static int findex; + static char_u *orig_save = NULL; // kept value of orig + int orig_saved = FALSE; + int i; + long_u len; + int non_suf_match; // number without matching suffix + + // first handle the case of using an old match + if (mode == WILD_NEXT || mode == WILD_PREV) + { + if (xp->xp_numfiles > 0) + { + if (mode == WILD_PREV) + { + if (findex == -1) + findex = xp->xp_numfiles; + --findex; + } + else // mode == WILD_NEXT + ++findex; + + // When wrapping around, return the original string, set findex to + // -1. + if (findex < 0) + { + if (orig_save == NULL) + findex = xp->xp_numfiles - 1; + else + findex = -1; + } + if (findex >= xp->xp_numfiles) + { + if (orig_save == NULL) + findex = 0; + else + findex = -1; + } +#ifdef FEAT_WILDMENU + if (p_wmnu) + win_redr_status_matches(xp, xp->xp_numfiles, xp->xp_files, + findex, cmd_showtail); +#endif + if (findex == -1) + return vim_strsave(orig_save); + return vim_strsave(xp->xp_files[findex]); + } + else + return NULL; + } + + // free old names + if (xp->xp_numfiles != -1 && mode != WILD_ALL && mode != WILD_LONGEST) + { + FreeWild(xp->xp_numfiles, xp->xp_files); + xp->xp_numfiles = -1; + VIM_CLEAR(orig_save); + } + findex = 0; + + if (mode == WILD_FREE) // only release file name + return NULL; + + if (xp->xp_numfiles == -1) + { + vim_free(orig_save); + orig_save = orig; + orig_saved = TRUE; + + // Do the expansion. + if (ExpandFromContext(xp, str, &xp->xp_numfiles, &xp->xp_files, + options) == FAIL) + { +#ifdef FNAME_ILLEGAL + // Illegal file name has been silently skipped. But when there + // are wildcards, the real problem is that there was no match, + // causing the pattern to be added, which has illegal characters. + if (!(options & WILD_SILENT) && (options & WILD_LIST_NOTFOUND)) + semsg(_(e_nomatch2), str); +#endif + } + else if (xp->xp_numfiles == 0) + { + if (!(options & WILD_SILENT)) + semsg(_(e_nomatch2), str); + } + else + { + // Escape the matches for use on the command line. + ExpandEscape(xp, str, xp->xp_numfiles, xp->xp_files, options); + + // Check for matching suffixes in file names. + if (mode != WILD_ALL && mode != WILD_ALL_KEEP + && mode != WILD_LONGEST) + { + if (xp->xp_numfiles) + non_suf_match = xp->xp_numfiles; + else + non_suf_match = 1; + if ((xp->xp_context == EXPAND_FILES + || xp->xp_context == EXPAND_DIRECTORIES) + && xp->xp_numfiles > 1) + { + // More than one match; check suffix. + // The files will have been sorted on matching suffix in + // expand_wildcards, only need to check the first two. + non_suf_match = 0; + for (i = 0; i < 2; ++i) + if (match_suffix(xp->xp_files[i])) + ++non_suf_match; + } + if (non_suf_match != 1) + { + // Can we ever get here unless it's while expanding + // interactively? If not, we can get rid of this all + // together. Don't really want to wait for this message + // (and possibly have to hit return to continue!). + if (!(options & WILD_SILENT)) + emsg(_(e_toomany)); + else if (!(options & WILD_NO_BEEP)) + beep_flush(); + } + if (!(non_suf_match != 1 && mode == WILD_EXPAND_FREE)) + ss = vim_strsave(xp->xp_files[0]); + } + } + } + + // Find longest common part + if (mode == WILD_LONGEST && xp->xp_numfiles > 0) + { + int mb_len = 1; + int c0, ci; + + for (len = 0; xp->xp_files[0][len]; len += mb_len) + { + if (has_mbyte) + { + mb_len = (*mb_ptr2len)(&xp->xp_files[0][len]); + c0 =(* mb_ptr2char)(&xp->xp_files[0][len]); + } + else + c0 = xp->xp_files[0][len]; + for (i = 1; i < xp->xp_numfiles; ++i) + { + if (has_mbyte) + ci =(* mb_ptr2char)(&xp->xp_files[i][len]); + else + ci = xp->xp_files[i][len]; + if (p_fic && (xp->xp_context == EXPAND_DIRECTORIES + || xp->xp_context == EXPAND_FILES + || xp->xp_context == EXPAND_SHELLCMD + || xp->xp_context == EXPAND_BUFFERS)) + { + if (MB_TOLOWER(c0) != MB_TOLOWER(ci)) + break; + } + else if (c0 != ci) + break; + } + if (i < xp->xp_numfiles) + { + if (!(options & WILD_NO_BEEP)) + vim_beep(BO_WILD); + break; + } + } + + ss = alloc(len + 1); + if (ss) + vim_strncpy(ss, xp->xp_files[0], (size_t)len); + findex = -1; // next p_wc gets first one + } + + // Concatenate all matching names + if (mode == WILD_ALL && xp->xp_numfiles > 0) + { + len = 0; + for (i = 0; i < xp->xp_numfiles; ++i) + len += (long_u)STRLEN(xp->xp_files[i]) + 1; + ss = alloc(len); + if (ss != NULL) + { + *ss = NUL; + for (i = 0; i < xp->xp_numfiles; ++i) + { + STRCAT(ss, xp->xp_files[i]); + if (i != xp->xp_numfiles - 1) + STRCAT(ss, (options & WILD_USE_NL) ? "\n" : " "); + } + } + } + + if (mode == WILD_EXPAND_FREE || mode == WILD_ALL) + ExpandCleanup(xp); + + // Free "orig" if it wasn't stored in "orig_save". + if (!orig_saved) + vim_free(orig); + + return ss; +} + +/* + * Prepare an expand structure for use. + */ + void +ExpandInit(expand_T *xp) +{ + xp->xp_pattern = NULL; + xp->xp_pattern_len = 0; + xp->xp_backslash = XP_BS_NONE; +#ifndef BACKSLASH_IN_FILENAME + xp->xp_shell = FALSE; +#endif + xp->xp_numfiles = -1; + xp->xp_files = NULL; +#if defined(FEAT_EVAL) && defined(FEAT_CMDL_COMPL) + xp->xp_arg = NULL; +#endif + xp->xp_line = NULL; +} + +/* + * Cleanup an expand structure after use. + */ + void +ExpandCleanup(expand_T *xp) +{ + if (xp->xp_numfiles >= 0) + { + FreeWild(xp->xp_numfiles, xp->xp_files); + xp->xp_numfiles = -1; + } +} + +/* + * Show all matches for completion on the command line. + * Returns EXPAND_NOTHING when the character that triggered expansion should + * be inserted like a normal character. + */ + int +showmatches(expand_T *xp, int wildmenu UNUSED) +{ + cmdline_info_T *ccline = get_cmdline_info(); +#define L_SHOWFILE(m) (showtail ? sm_gettail(files_found[m]) : files_found[m]) + int num_files; + char_u **files_found; + int i, j, k; + int maxlen; + int lines; + int columns; + char_u *p; + int lastlen; + int attr; + int showtail; + + if (xp->xp_numfiles == -1) + { + set_expand_context(xp); + i = expand_cmdline(xp, ccline->cmdbuff, ccline->cmdpos, + &num_files, &files_found); + showtail = expand_showtail(xp); + if (i != EXPAND_OK) + return i; + + } + else + { + num_files = xp->xp_numfiles; + files_found = xp->xp_files; + showtail = cmd_showtail; + } + +#ifdef FEAT_WILDMENU + if (!wildmenu) + { +#endif + msg_didany = FALSE; // lines_left will be set + msg_start(); // prepare for paging + msg_putchar('\n'); + out_flush(); + cmdline_row = msg_row; + msg_didany = FALSE; // lines_left will be set again + msg_start(); // prepare for paging +#ifdef FEAT_WILDMENU + } +#endif + + if (got_int) + got_int = FALSE; // only int. the completion, not the cmd line +#ifdef FEAT_WILDMENU + else if (wildmenu) + win_redr_status_matches(xp, num_files, files_found, -1, showtail); +#endif + else + { + // find the length of the longest file name + maxlen = 0; + for (i = 0; i < num_files; ++i) + { + if (!showtail && (xp->xp_context == EXPAND_FILES + || xp->xp_context == EXPAND_SHELLCMD + || xp->xp_context == EXPAND_BUFFERS)) + { + home_replace(NULL, files_found[i], NameBuff, MAXPATHL, TRUE); + j = vim_strsize(NameBuff); + } + else + j = vim_strsize(L_SHOWFILE(i)); + if (j > maxlen) + maxlen = j; + } + + if (xp->xp_context == EXPAND_TAGS_LISTFILES) + lines = num_files; + else + { + // compute the number of columns and lines for the listing + maxlen += 2; // two spaces between file names + columns = ((int)Columns + 2) / maxlen; + if (columns < 1) + columns = 1; + lines = (num_files + columns - 1) / columns; + } + + attr = HL_ATTR(HLF_D); // find out highlighting for directories + + if (xp->xp_context == EXPAND_TAGS_LISTFILES) + { + msg_puts_attr(_("tagname"), HL_ATTR(HLF_T)); + msg_clr_eos(); + msg_advance(maxlen - 3); + msg_puts_attr(_(" kind file\n"), HL_ATTR(HLF_T)); + } + + // list the files line by line + for (i = 0; i < lines; ++i) + { + lastlen = 999; + for (k = i; k < num_files; k += lines) + { + if (xp->xp_context == EXPAND_TAGS_LISTFILES) + { + msg_outtrans_attr(files_found[k], HL_ATTR(HLF_D)); + p = files_found[k] + STRLEN(files_found[k]) + 1; + msg_advance(maxlen + 1); + msg_puts((char *)p); + msg_advance(maxlen + 3); + msg_outtrans_long_attr(p + 2, HL_ATTR(HLF_D)); + break; + } + for (j = maxlen - lastlen; --j >= 0; ) + msg_putchar(' '); + if (xp->xp_context == EXPAND_FILES + || xp->xp_context == EXPAND_SHELLCMD + || xp->xp_context == EXPAND_BUFFERS) + { + // highlight directories + if (xp->xp_numfiles != -1) + { + char_u *halved_slash; + char_u *exp_path; + + // Expansion was done before and special characters + // were escaped, need to halve backslashes. Also + // $HOME has been replaced with ~/. + exp_path = expand_env_save_opt(files_found[k], TRUE); + halved_slash = backslash_halve_save( + exp_path != NULL ? exp_path : files_found[k]); + j = mch_isdir(halved_slash != NULL ? halved_slash + : files_found[k]); + vim_free(exp_path); + vim_free(halved_slash); + } + else + // Expansion was done here, file names are literal. + j = mch_isdir(files_found[k]); + if (showtail) + p = L_SHOWFILE(k); + else + { + home_replace(NULL, files_found[k], NameBuff, MAXPATHL, + TRUE); + p = NameBuff; + } + } + else + { + j = FALSE; + p = L_SHOWFILE(k); + } + lastlen = msg_outtrans_attr(p, j ? attr : 0); + } + if (msg_col > 0) // when not wrapped around + { + msg_clr_eos(); + msg_putchar('\n'); + } + out_flush(); // show one line at a time + if (got_int) + { + got_int = FALSE; + break; + } + } + + // we redraw the command below the lines that we have just listed + // This is a bit tricky, but it saves a lot of screen updating. + cmdline_row = msg_row; // will put it back later + } + + if (xp->xp_numfiles == -1) + FreeWild(num_files, files_found); + + return EXPAND_OK; +} + +/* + * Private gettail for showmatches() (and win_redr_status_matches()): + * Find tail of file name path, but ignore trailing "/". + */ + char_u * +sm_gettail(char_u *s) +{ + char_u *p; + char_u *t = s; + int had_sep = FALSE; + + for (p = s; *p != NUL; ) + { + if (vim_ispathsep(*p) +#ifdef BACKSLASH_IN_FILENAME + && !rem_backslash(p) +#endif + ) + had_sep = TRUE; + else if (had_sep) + { + t = p; + had_sep = FALSE; + } + MB_PTR_ADV(p); + } + return t; +} + +/* + * Return TRUE if we only need to show the tail of completion matches. + * When not completing file names or there is a wildcard in the path FALSE is + * returned. + */ + static int +expand_showtail(expand_T *xp) +{ + char_u *s; + char_u *end; + + // When not completing file names a "/" may mean something different. + if (xp->xp_context != EXPAND_FILES + && xp->xp_context != EXPAND_SHELLCMD + && xp->xp_context != EXPAND_DIRECTORIES) + return FALSE; + + end = gettail(xp->xp_pattern); + if (end == xp->xp_pattern) // there is no path separator + return FALSE; + + for (s = xp->xp_pattern; s < end; s++) + { + // Skip escaped wildcards. Only when the backslash is not a path + // separator, on DOS the '*' "path\*\file" must not be skipped. + if (rem_backslash(s)) + ++s; + else if (vim_strchr((char_u *)"*?[", *s) != NULL) + return FALSE; + } + return TRUE; +} + +/* + * Prepare a string for expansion. + * When expanding file names: The string will be used with expand_wildcards(). + * Copy "fname[len]" into allocated memory and add a '*' at the end. + * When expanding other names: The string will be used with regcomp(). Copy + * the name into allocated memory and prepend "^". + */ + char_u * +addstar( + char_u *fname, + int len, + int context) // EXPAND_FILES etc. +{ + char_u *retval; + int i, j; + int new_len; + char_u *tail; + int ends_in_star; + + if (context != EXPAND_FILES + && context != EXPAND_FILES_IN_PATH + && context != EXPAND_SHELLCMD + && context != EXPAND_DIRECTORIES) + { + // Matching will be done internally (on something other than files). + // So we convert the file-matching-type wildcards into our kind for + // use with vim_regcomp(). First work out how long it will be: + + // For help tags the translation is done in find_help_tags(). + // For a tag pattern starting with "/" no translation is needed. + if (context == EXPAND_HELP + || context == EXPAND_COLORS + || context == EXPAND_COMPILER + || context == EXPAND_OWNSYNTAX + || context == EXPAND_FILETYPE + || context == EXPAND_PACKADD + || ((context == EXPAND_TAGS_LISTFILES + || context == EXPAND_TAGS) + && fname[0] == '/')) + retval = vim_strnsave(fname, len); + else + { + new_len = len + 2; // +2 for '^' at start, NUL at end + for (i = 0; i < len; i++) + { + if (fname[i] == '*' || fname[i] == '~') + new_len++; // '*' needs to be replaced by ".*" + // '~' needs to be replaced by "\~" + + // Buffer names are like file names. "." should be literal + if (context == EXPAND_BUFFERS && fname[i] == '.') + new_len++; // "." becomes "\." + + // Custom expansion takes care of special things, match + // backslashes literally (perhaps also for other types?) + if ((context == EXPAND_USER_DEFINED + || context == EXPAND_USER_LIST) && fname[i] == '\\') + new_len++; // '\' becomes "\\" + } + retval = alloc(new_len); + if (retval != NULL) + { + retval[0] = '^'; + j = 1; + for (i = 0; i < len; i++, j++) + { + // Skip backslash. But why? At least keep it for custom + // expansion. + if (context != EXPAND_USER_DEFINED + && context != EXPAND_USER_LIST + && fname[i] == '\\' + && ++i == len) + break; + + switch (fname[i]) + { + case '*': retval[j++] = '.'; + break; + case '~': retval[j++] = '\\'; + break; + case '?': retval[j] = '.'; + continue; + case '.': if (context == EXPAND_BUFFERS) + retval[j++] = '\\'; + break; + case '\\': if (context == EXPAND_USER_DEFINED + || context == EXPAND_USER_LIST) + retval[j++] = '\\'; + break; + } + retval[j] = fname[i]; + } + retval[j] = NUL; + } + } + } + else + { + retval = alloc(len + 4); + if (retval != NULL) + { + vim_strncpy(retval, fname, len); + + // Don't add a star to *, ~, ~user, $var or `cmd`. + // * would become **, which walks the whole tree. + // ~ would be at the start of the file name, but not the tail. + // $ could be anywhere in the tail. + // ` could be anywhere in the file name. + // When the name ends in '$' don't add a star, remove the '$'. + tail = gettail(retval); + ends_in_star = (len > 0 && retval[len - 1] == '*'); +#ifndef BACKSLASH_IN_FILENAME + for (i = len - 2; i >= 0; --i) + { + if (retval[i] != '\\') + break; + ends_in_star = !ends_in_star; + } +#endif + if ((*retval != '~' || tail != retval) + && !ends_in_star + && vim_strchr(tail, '$') == NULL + && vim_strchr(retval, '`') == NULL) + retval[len++] = '*'; + else if (len > 0 && retval[len - 1] == '$') + --len; + retval[len] = NUL; + } + } + return retval; +} + +/* + * Must parse the command line so far to work out what context we are in. + * Completion can then be done based on that context. + * This routine sets the variables: + * xp->xp_pattern The start of the pattern to be expanded within + * the command line (ends at the cursor). + * xp->xp_context The type of thing to expand. Will be one of: + * + * EXPAND_UNSUCCESSFUL Used sometimes when there is something illegal on + * the command line, like an unknown command. Caller + * should beep. + * EXPAND_NOTHING Unrecognised context for completion, use char like + * a normal char, rather than for completion. eg + * :s/^I/ + * EXPAND_COMMANDS Cursor is still touching the command, so complete + * it. + * EXPAND_BUFFERS Complete file names for :buf and :sbuf commands. + * EXPAND_FILES After command with EX_XFILE set, or after setting + * with P_EXPAND set. eg :e ^I, :w>>^I + * EXPAND_DIRECTORIES In some cases this is used instead of the latter + * when we know only directories are of interest. eg + * :set dir=^I + * EXPAND_SHELLCMD After ":!cmd", ":r !cmd" or ":w !cmd". + * EXPAND_SETTINGS Complete variable names. eg :set d^I + * EXPAND_BOOL_SETTINGS Complete boolean variables only, eg :set no^I + * EXPAND_TAGS Complete tags from the files in p_tags. eg :ta a^I + * EXPAND_TAGS_LISTFILES As above, but list filenames on ^D, after :tselect + * EXPAND_HELP Complete tags from the file 'helpfile'/tags + * EXPAND_EVENTS Complete event names + * EXPAND_SYNTAX Complete :syntax command arguments + * EXPAND_HIGHLIGHT Complete highlight (syntax) group names + * EXPAND_AUGROUP Complete autocommand group names + * EXPAND_USER_VARS Complete user defined variable names, eg :unlet a^I + * EXPAND_MAPPINGS Complete mapping and abbreviation names, + * eg :unmap a^I , :cunab x^I + * EXPAND_FUNCTIONS Complete internal or user defined function names, + * eg :call sub^I + * EXPAND_USER_FUNC Complete user defined function names, eg :delf F^I + * EXPAND_EXPRESSION Complete internal or user defined function/variable + * names in expressions, eg :while s^I + * EXPAND_ENV_VARS Complete environment variable names + * EXPAND_USER Complete user names + */ + static void +set_expand_context(expand_T *xp) +{ + cmdline_info_T *ccline = get_cmdline_info(); + + // only expansion for ':', '>' and '=' command-lines + if (ccline->cmdfirstc != ':' +#ifdef FEAT_EVAL + && ccline->cmdfirstc != '>' && ccline->cmdfirstc != '=' + && !ccline->input_fn +#endif + ) + { + xp->xp_context = EXPAND_NOTHING; + return; + } + set_cmd_context(xp, ccline->cmdbuff, ccline->cmdlen, ccline->cmdpos, TRUE); +} + + void +set_cmd_context( + expand_T *xp, + char_u *str, // start of command line + int len, // length of command line (excl. NUL) + int col, // position of cursor + int use_ccline UNUSED) // use ccline for info +{ +#ifdef FEAT_EVAL + cmdline_info_T *ccline = get_cmdline_info(); +#endif + int old_char = NUL; + char_u *nextcomm; + + // Avoid a UMR warning from Purify, only save the character if it has been + // written before. + if (col < len) + old_char = str[col]; + str[col] = NUL; + nextcomm = str; + +#ifdef FEAT_EVAL + if (use_ccline && ccline->cmdfirstc == '=') + { +# ifdef FEAT_CMDL_COMPL + // pass CMD_SIZE because there is no real command + set_context_for_expression(xp, str, CMD_SIZE); +# endif + } + else if (use_ccline && ccline->input_fn) + { + xp->xp_context = ccline->xp_context; + xp->xp_pattern = ccline->cmdbuff; +# if defined(FEAT_CMDL_COMPL) + xp->xp_arg = ccline->xp_arg; +# endif + } + else +#endif + while (nextcomm != NULL) + nextcomm = set_one_cmd_context(xp, nextcomm); + + // Store the string here so that call_user_expand_func() can get to them + // easily. + xp->xp_line = str; + xp->xp_col = col; + + str[col] = old_char; +} + +/* + * Expand the command line "str" from context "xp". + * "xp" must have been set by set_cmd_context(). + * xp->xp_pattern points into "str", to where the text that is to be expanded + * starts. + * Returns EXPAND_UNSUCCESSFUL when there is something illegal before the + * cursor. + * Returns EXPAND_NOTHING when there is nothing to expand, might insert the + * key that triggered expansion literally. + * Returns EXPAND_OK otherwise. + */ + int +expand_cmdline( + expand_T *xp, + char_u *str, // start of command line + int col, // position of cursor + int *matchcount, // return: nr of matches + char_u ***matches) // return: array of pointers to matches +{ + char_u *file_str = NULL; + int options = WILD_ADD_SLASH|WILD_SILENT; + + if (xp->xp_context == EXPAND_UNSUCCESSFUL) + { + beep_flush(); + return EXPAND_UNSUCCESSFUL; // Something illegal on command line + } + if (xp->xp_context == EXPAND_NOTHING) + { + // Caller can use the character as a normal char instead + return EXPAND_NOTHING; + } + + // add star to file name, or convert to regexp if not exp. files. + xp->xp_pattern_len = (int)(str + col - xp->xp_pattern); + file_str = addstar(xp->xp_pattern, xp->xp_pattern_len, xp->xp_context); + if (file_str == NULL) + return EXPAND_UNSUCCESSFUL; + + if (p_wic) + options += WILD_ICASE; + + // find all files that match the description + if (ExpandFromContext(xp, file_str, matchcount, matches, options) == FAIL) + { + *matchcount = 0; + *matches = NULL; + } + vim_free(file_str); + + return EXPAND_OK; +} + +#ifdef FEAT_MULTI_LANG +/* + * Cleanup matches for help tags: + * Remove "@ab" if the top of 'helplang' is "ab" and the language of the first + * tag matches it. Otherwise remove "@en" if "en" is the only language. + */ + static void +cleanup_help_tags(int num_file, char_u **file) +{ + int i, j; + int len; + char_u buf[4]; + char_u *p = buf; + + if (p_hlg[0] != NUL && (p_hlg[0] != 'e' || p_hlg[1] != 'n')) + { + *p++ = '@'; + *p++ = p_hlg[0]; + *p++ = p_hlg[1]; + } + *p = NUL; + + for (i = 0; i < num_file; ++i) + { + len = (int)STRLEN(file[i]) - 3; + if (len <= 0) + continue; + if (STRCMP(file[i] + len, "@en") == 0) + { + // Sorting on priority means the same item in another language may + // be anywhere. Search all items for a match up to the "@en". + for (j = 0; j < num_file; ++j) + if (j != i && (int)STRLEN(file[j]) == len + 3 + && STRNCMP(file[i], file[j], len + 1) == 0) + break; + if (j == num_file) + // item only exists with @en, remove it + file[i][len] = NUL; + } + } + + if (*buf != NUL) + for (i = 0; i < num_file; ++i) + { + len = (int)STRLEN(file[i]) - 3; + if (len <= 0) + continue; + if (STRCMP(file[i] + len, buf) == 0) + { + // remove the default language + file[i][len] = NUL; + } + } +} +#endif + +/* + * Do the expansion based on xp->xp_context and "pat". + */ + static int +ExpandFromContext( + expand_T *xp, + char_u *pat, + int *num_file, + char_u ***file, + int options) // WILD_ flags +{ +#ifdef FEAT_CMDL_COMPL + regmatch_T regmatch; +#endif + int ret; + int flags; + + flags = EW_DIR; // include directories + if (options & WILD_LIST_NOTFOUND) + flags |= EW_NOTFOUND; + if (options & WILD_ADD_SLASH) + flags |= EW_ADDSLASH; + if (options & WILD_KEEP_ALL) + flags |= EW_KEEPALL; + if (options & WILD_SILENT) + flags |= EW_SILENT; + if ( |