changeset 27744:515ce8e07bf2 v8.2.4398

patch 8.2.4398: some command completion functions are too long Commit: https://github.com/vim/vim/commit/b31aec3b9387ed12677dca09069c3ae98c6c7447 Author: Yegappan Lakshmanan <yegappan@yahoo.com> Date: Wed Feb 16 12:44:29 2022 +0000 patch 8.2.4398: some command completion functions are too long Problem: Some command completion functions are too long. Solution: Refactor code into separate functions. Add a few more tests. (Yegappan Lakshmanan, closes #9785)
author Bram Moolenaar <Bram@vim.org>
date Wed, 16 Feb 2022 13:45:04 +0100
parents 629769fd1ff1
children 94b70a7a4d94
files src/cmdexpand.c src/ex_getln.c src/proto/usercmd.pro src/testdir/test_cmdline.vim src/usercmd.c src/version.c
diffstat 6 files changed, 492 insertions(+), 347 deletions(-) [+]
line wrap: on
line diff
--- a/src/cmdexpand.c
+++ b/src/cmdexpand.c
@@ -682,6 +682,98 @@ ExpandCleanup(expand_T *xp)
 }
 
 /*
+ * Display one line of completion matches. Multiple matches are displayed in
+ * each line (used by wildmode=list and CTRL-D)
+ *   files_found - list of completion match names
+ *   num_files - number of completion matches in "files_found"
+ *   lines - number of output lines
+ *   linenr - line number of matches to display
+ *   maxlen - maximum number of characters in each line
+ *   showtail - display only the tail of the full path of a file name
+ *   dir_attr - highlight attribute to use for directory names
+ */
+    static void
+showmatches_oneline(
+	expand_T	*xp,
+	char_u		**files_found,
+	int		num_files,
+	int		lines,
+	int		linenr,
+	int		maxlen,
+	int		showtail,
+	int		dir_attr)
+{
+    int		i, j;
+    int		isdir;
+    int		lastlen;
+    char_u	*p;
+
+    lastlen = 999;
+    for (j = linenr; j < num_files; j += lines)
+    {
+	if (xp->xp_context == EXPAND_TAGS_LISTFILES)
+	{
+	    msg_outtrans_attr(files_found[j], HL_ATTR(HLF_D));
+	    p = files_found[j] + STRLEN(files_found[j]) + 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 (i = maxlen - lastlen; --i >= 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;
+		char_u	*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[j], TRUE);
+		path = exp_path != NULL ? exp_path : files_found[j];
+		halved_slash = backslash_halve_save(path);
+		isdir = mch_isdir(halved_slash != NULL ? halved_slash
+			: files_found[j]);
+		vim_free(exp_path);
+		if (halved_slash != path)
+		    vim_free(halved_slash);
+	    }
+	    else
+		// Expansion was done here, file names are literal.
+		isdir = mch_isdir(files_found[j]);
+	    if (showtail)
+		p = SHOW_FILE_TEXT(j);
+	    else
+	    {
+		home_replace(NULL, files_found[j], NameBuff, MAXPATHL,
+			TRUE);
+		p = NameBuff;
+	    }
+	}
+	else
+	{
+	    isdir = FALSE;
+	    p = SHOW_FILE_TEXT(j);
+	}
+	lastlen = msg_outtrans_attr(p, isdir ? dir_attr : 0);
+    }
+    if (msg_col > 0)	// when not wrapped around
+    {
+	msg_clr_eos();
+	msg_putchar('\n');
+    }
+    out_flush();		    // show one line at a time
+}
+
+/*
  * 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.
@@ -692,12 +784,10 @@ showmatches(expand_T *xp, int wildmenu U
     cmdline_info_T	*ccline = get_cmdline_info();
     int		num_files;
     char_u	**files_found;
-    int		i, j, k;
+    int		i, j;
     int		maxlen;
     int		lines;
     int		columns;
-    char_u	*p;
-    int		lastlen;
     int		attr;
     int		showtail;
 
@@ -709,7 +799,6 @@ showmatches(expand_T *xp, int wildmenu U
 	showtail = expand_showtail(xp);
 	if (i != EXPAND_OK)
 	    return i;
-
     }
     else
     {
@@ -789,69 +878,8 @@ showmatches(expand_T *xp, int wildmenu U
 	// 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;
-			char_u	*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);
-			path = exp_path != NULL ? exp_path : files_found[k];
-			halved_slash = backslash_halve_save(path);
-			j = mch_isdir(halved_slash != NULL ? halved_slash
-							    : files_found[k]);
-			vim_free(exp_path);
-			if (halved_slash != path)
-			    vim_free(halved_slash);
-		    }
-		    else
-			// Expansion was done here, file names are literal.
-			j = mch_isdir(files_found[k]);
-		    if (showtail)
-			p = SHOW_FILE_TEXT(k);
-		    else
-		    {
-			home_replace(NULL, files_found[k], NameBuff, MAXPATHL,
-									TRUE);
-			p = NameBuff;
-		    }
-		}
-		else
-		{
-		    j = FALSE;
-		    p = SHOW_FILE_TEXT(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
+	    showmatches_oneline(xp, files_found, num_files, lines, i,
+						maxlen, showtail, attr);
 	    if (got_int)
 	    {
 		got_int = FALSE;
@@ -1334,6 +1362,73 @@ set_context_for_wildcard_arg(
 }
 
 /*
+ * Returns a pointer to the next command after a :substitute or a :& command.
+ * Returns NULL if there is no next command.
+ */
+    static char_u *
+find_cmd_after_substitute_cmd(char_u *arg)
+{
+    int		delim;
+
+    delim = *arg;
+    if (delim)
+    {
+	// skip "from" part
+	++arg;
+	arg = skip_regexp(arg, delim, magic_isset());
+
+	if (arg[0] != NUL && arg[0] == delim)
+	{
+	    // skip "to" part
+	    ++arg;
+	    while (arg[0] != NUL && arg[0] != delim)
+	    {
+		if (arg[0] == '\\' && arg[1] != NUL)
+		    ++arg;
+		++arg;
+	    }
+	    if (arg[0] != NUL)	// skip delimiter
+		++arg;
+	}
+    }
+    while (arg[0] && vim_strchr((char_u *)"|\"#", arg[0]) == NULL)
+	++arg;
+    if (arg[0] != NUL)
+	return arg;
+
+    return NULL;
+}
+
+/*
+ * Returns a pointer to the next command after a :isearch/:dsearch/:ilist
+ * :dlist/:ijump/:psearch/:djump/:isplit/:dsplit command.
+ * Returns NULL if there is no next command.
+ */
+    static char_u *
+find_cmd_after_isearch_cmd(char_u *arg, expand_T *xp)
+{
+    arg = skipwhite(skipdigits(arg));	    // skip count
+    if (*arg == '/')	// Match regexp, not just whole words
+    {
+	for (++arg; *arg && *arg != '/'; arg++)
+	    if (*arg == '\\' && arg[1] != NUL)
+		arg++;
+	if (*arg)
+	{
+	    arg = skipwhite(arg + 1);
+
+	    // Check for trailing illegal characters
+	    if (*arg == NUL || vim_strchr((char_u *)"|\"\n", *arg) == NULL)
+		xp->xp_context = EXPAND_NOTHING;
+	    else
+		return arg;
+	}
+    }
+
+    return NULL;
+}
+
+/*
  * Set the completion context in 'xp' for command 'cmd' with index 'cmdidx'.
  * The argument to the command is 'arg' and the argument flags is 'argt'.
  * For user-defined commands and for environment variables, 'compl' has the
@@ -1467,32 +1562,7 @@ set_context_by_cmdname(
 	    break;
 	case CMD_and:
 	case CMD_substitute:
-	    delim = *arg;
-	    if (delim)
-	    {
-		// skip "from" part
-		++arg;
-		arg = skip_regexp(arg, delim, magic_isset());
-
-		if (arg[0] != NUL && arg[0] == delim)
-		{
-		    // skip "to" part
-		    ++arg;
-		    while (arg[0] != NUL && arg[0] != delim)
-		    {
-			if (arg[0] == '\\' && arg[1] != NUL)
-			    ++arg;
-			++arg;
-		    }
-		    if (arg[0] != NUL)	// skip delimiter
-			++arg;
-		}
-	    }
-	    while (arg[0] && vim_strchr((char_u *)"|\"#", arg[0]) == NULL)
-		++arg;
-	    if (arg[0] != NUL)
-		return arg;
-	    break;
+	    return find_cmd_after_substitute_cmd(arg);
 	case CMD_isearch:
 	case CMD_dsearch:
 	case CMD_ilist:
@@ -1502,26 +1572,7 @@ set_context_by_cmdname(
 	case CMD_djump:
 	case CMD_isplit:
 	case CMD_dsplit:
-	    arg = skipwhite(skipdigits(arg));	    // skip count
-	    if (*arg == '/')	// Match regexp, not just whole words
-	    {
-		for (++arg; *arg && *arg != '/'; arg++)
-		    if (*arg == '\\' && arg[1] != NUL)
-			arg++;
-		if (*arg)
-		{
-		    arg = skipwhite(arg + 1);
-
-		    // Check for trailing illegal characters
-		    if (*arg == NUL ||
-				vim_strchr((char_u *)"|\"\n", *arg) == NULL)
-			xp->xp_context = EXPAND_NOTHING;
-		    else
-			return arg;
-		}
-	    }
-	    break;
-
+	    return find_cmd_after_isearch_cmd(arg, xp);
 	case CMD_autocmd:
 	    return set_context_in_autocmd(xp, arg, FALSE);
 	case CMD_doautocmd:
@@ -1652,36 +1703,8 @@ set_context_by_cmdname(
 #endif
 	case CMD_USER:
 	case CMD_USER_BUF:
-	    if (compl != EXPAND_NOTHING)
-	    {
-		// EX_XFILE: file names are handled above
-		if (!(argt & EX_XFILE))
-		{
-#ifdef FEAT_MENU
-		    if (compl == EXPAND_MENUS)
-			return set_context_in_menu_cmd(xp, cmd, arg, forceit);
-#endif
-		    if (compl == EXPAND_COMMANDS)
-			return arg;
-		    if (compl == EXPAND_MAPPINGS)
-			return set_context_in_map_cmd(xp, (char_u *)"map",
-					 arg, forceit, FALSE, FALSE, CMD_map);
-		    // Find start of last argument.
-		    p = arg;
-		    while (*p)
-		    {
-			if (*p == ' ')
-			    // argument starts after a space
-			    arg = p + 1;
-			else if (*p == '\\' && *(p + 1) != NUL)
-			    ++p; // skip over escaped character
-			MB_PTR_ADV(p);
-		    }
-		    xp->xp_pattern = arg;
-		}
-		xp->xp_context = compl;
-	    }
-	    break;
+	    return set_context_in_user_cmdarg(cmd, arg, argt, compl, xp,
+								forceit);
 
 	case CMD_map:	    case CMD_noremap:
 	case CMD_nmap:	    case CMD_nnoremap:
@@ -2313,6 +2336,31 @@ ExpandOther(
 }
 
 /*
+ * Map wild expand options to flags for expand_wildcards()
+ */
+    static int
+map_wildopts_to_ewflags(int options)
+{
+    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 (options & WILD_NOERROR)
+	flags |= EW_NOERROR;
+    if (options & WILD_ALLLINKS)
+	flags |= EW_ALLLINKS;
+
+    return flags;
+}
+
+/*
  * Do the expansion based on xp->xp_context and "pat".
  */
     static int
@@ -2328,19 +2376,7 @@ ExpandFromContext(
     int		flags;
     char_u	*tofree = NULL;
 
-    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 (options & WILD_NOERROR)
-	flags |= EW_NOERROR;
-    if (options & WILD_ALLLINKS)
-	flags |= EW_ALLLINKS;
+    flags = map_wildopts_to_ewflags(options);
 
     if (xp->xp_context == EXPAND_FILES
 	    || xp->xp_context == EXPAND_DIRECTORIES
@@ -2550,6 +2586,64 @@ ExpandGeneric(
 }
 
 /*
+ * Expand shell command matches in one directory of $PATH.
+ */
+    static void
+expand_shellcmd_onedir(
+	char_u		*buf,
+	char_u		*s,
+	size_t		l,
+	char_u		*pat,
+	char_u		***files,
+	int		*num_files,
+	int		flags,
+	hashtab_T	*ht,
+	garray_T	*gap)
+{
+    int		ret;
+    int		i;
+    hash_T	hash;
+    hashitem_T	*hi;
+
+    vim_strncpy(buf, s, l);
+    add_pathsep(buf);
+    l = STRLEN(buf);
+    vim_strncpy(buf + l, pat, MAXPATHL - 1 - l);
+
+    // Expand matches in one directory of $PATH.
+    ret = expand_wildcards(1, &buf, num_files, files, flags);
+    if (ret == OK)
+    {
+	if (ga_grow(gap, *num_files) == FAIL)
+	    FreeWild(*num_files, *files);
+	else
+	{
+	    for (i = 0; i < *num_files; ++i)
+	    {
+		char_u *name = (*files)[i];
+
+		if (STRLEN(name) > l)
+		{
+		    // Check if this name was already found.
+		    hash = hash_hash(name + l);
+		    hi = hash_lookup(ht, name + l, hash);
+		    if (HASHITEM_EMPTY(hi))
+		    {
+			// Remove the path that was prepended.
+			STRMOVE(name, name + l);
+			((char_u **)gap->ga_data)[gap->ga_len++] = name;
+			hash_add_item(ht, hi, name, hash);
+			name = NULL;
+		    }
+		}
+		vim_free(name);
+	    }
+	    vim_free(*files);
+	}
+    }
+}
+
+/*
  * Complete a shell command.
  * Returns FAIL or OK;
  */
@@ -2569,11 +2663,8 @@ expand_shellcmd(
     size_t	l;
     char_u	*s, *e;
     int		flags = flagsarg;
-    int		ret;
     int		did_curdir = FALSE;
     hashtab_T	found_ht;
-    hashitem_T	*hi;
-    hash_T	hash;
 
     buf = alloc(MAXPATHL);
     if (buf == NULL)
@@ -2640,42 +2731,10 @@ expand_shellcmd(
 	l = e - s;
 	if (l > MAXPATHL - 5)
 	    break;
-	vim_strncpy(buf, s, l);
-	add_pathsep(buf);
-	l = STRLEN(buf);
-	vim_strncpy(buf + l, pat, MAXPATHL - 1 - l);
-
-	// Expand matches in one directory of $PATH.
-	ret = expand_wildcards(1, &buf, num_file, file, flags);
-	if (ret == OK)
-	{
-	    if (ga_grow(&ga, *num_file) == FAIL)
-		FreeWild(*num_file, *file);
-	    else
-	    {
-		for (i = 0; i < *num_file; ++i)
-		{
-		    char_u *name = (*file)[i];
 
-		    if (STRLEN(name) > l)
-		    {
-			// Check if this name was already found.
-			hash = hash_hash(name + l);
-			hi = hash_lookup(&found_ht, name + l, hash);
-			if (HASHITEM_EMPTY(hi))
-			{
-			    // Remove the path that was prepended.
-			    STRMOVE(name, name + l);
-			    ((char_u **)ga.ga_data)[ga.ga_len++] = name;
-			    hash_add_item(&found_ht, hi, name, hash);
-			    name = NULL;
-			}
-		    }
-		    vim_free(name);
-		}
-		vim_free(*file);
-	    }
-	}
+	expand_shellcmd_onedir(buf, s, l, pat, file, num_file, flags,
+							&found_ht, &ga);
+
 	if (*e != NUL)
 	    ++e;
     }
@@ -2924,7 +2983,7 @@ wildmenu_translate_key(
     }
 #endif
 
-    if (did_wild_list && p_wmnu)
+    if (did_wild_list)
     {
 	if (c == K_LEFT)
 	    c = Ctrl_P;
@@ -2933,7 +2992,7 @@ wildmenu_translate_key(
     }
 
     // Hitting CR after "emenu Name.": complete submenu
-    if (xp->xp_context == EXPAND_MENUNAMES && p_wmnu
+    if (xp->xp_context == EXPAND_MENUNAMES
 	    && cclp->cmdpos > 1
 	    && cclp->cmdbuff[cclp->cmdpos - 1] == '.'
 	    && cclp->cmdbuff[cclp->cmdpos - 2] != '\\'
@@ -2957,170 +3016,188 @@ cmdline_del(cmdline_info_T *cclp, int fr
 }
 
 /*
- * Handle a key pressed when wild menu is displayed
+ * Handle a key pressed when the wild menu for the menu names
+ * (EXPAND_MENUNAMES) is displayed.
  */
-    int
-wildmenu_process_key(cmdline_info_T *cclp, int key, expand_T *xp)
+    static int
+wildmenu_process_key_menunames(cmdline_info_T *cclp, int key, expand_T *xp)
 {
-    int		c = key;
     int		i;
     int		j;
 
-    if (!p_wmnu)
-	return c;
+    // Hitting <Down> after "emenu Name.": complete submenu
+    if (key == K_DOWN && cclp->cmdpos > 0
+	    && cclp->cmdbuff[cclp->cmdpos - 1] == '.')
+    {
+	key = p_wc;
+	KeyTyped = TRUE;  // in case the key was mapped
+    }
+    else if (key == K_UP)
+    {
+	// Hitting <Up>: Remove one submenu name in front of the
+	// cursor
+	int found = FALSE;
 
-    // Special translations for 'wildmenu'
-    if (xp->xp_context == EXPAND_MENUNAMES)
-    {
-	// Hitting <Down> after "emenu Name.": complete submenu
-	if (c == K_DOWN && cclp->cmdpos > 0
-		&& cclp->cmdbuff[cclp->cmdpos - 1] == '.')
+	j = (int)(xp->xp_pattern - cclp->cmdbuff);
+	i = 0;
+	while (--j > 0)
 	{
-	    c = p_wc;
-	    KeyTyped = TRUE;  // in case the key was mapped
-	}
-	else if (c == K_UP)
-	{
-	    // Hitting <Up>: Remove one submenu name in front of the
-	    // cursor
-	    int found = FALSE;
-
-	    j = (int)(xp->xp_pattern - cclp->cmdbuff);
-	    i = 0;
-	    while (--j > 0)
+	    // check for start of menu name
+	    if (cclp->cmdbuff[j] == ' '
+		    && cclp->cmdbuff[j - 1] != '\\')
 	    {
-		// check for start of menu name
-		if (cclp->cmdbuff[j] == ' '
-			&& cclp->cmdbuff[j - 1] != '\\')
+		i = j + 1;
+		break;
+	    }
+	    // check for start of submenu name
+	    if (cclp->cmdbuff[j] == '.'
+		    && cclp->cmdbuff[j - 1] != '\\')
+	    {
+		if (found)
 		{
 		    i = j + 1;
 		    break;
 		}
-		// check for start of submenu name
-		if (cclp->cmdbuff[j] == '.'
-			&& cclp->cmdbuff[j - 1] != '\\')
-		{
-		    if (found)
-		    {
-			i = j + 1;
-			break;
-		    }
-		    else
-			found = TRUE;
-		}
+		else
+		    found = TRUE;
 	    }
-	    if (i > 0)
-		cmdline_del(cclp, i);
-	    c = p_wc;
+	}
+	if (i > 0)
+	    cmdline_del(cclp, i);
+	key = p_wc;
+	KeyTyped = TRUE;  // in case the key was mapped
+	xp->xp_context = EXPAND_NOTHING;
+    }
+
+    return key;
+}
+
+/*
+ * Handle a key pressed when the wild menu for file names (EXPAND_FILES) or
+ * directory names (EXPAND_DIRECTORIES) or shell command names
+ * (EXPAND_SHELLCMD) is displayed.
+ */
+    static int
+wildmenu_process_key_filenames(cmdline_info_T *cclp, int key, expand_T *xp)
+{
+    int		i;
+    int		j;
+    char_u	upseg[5];
+
+    upseg[0] = PATHSEP;
+    upseg[1] = '.';
+    upseg[2] = '.';
+    upseg[3] = PATHSEP;
+    upseg[4] = NUL;
+
+    if (key == K_DOWN
+	    && cclp->cmdpos > 0
+	    && cclp->cmdbuff[cclp->cmdpos - 1] == PATHSEP
+	    && (cclp->cmdpos < 3
+		|| cclp->cmdbuff[cclp->cmdpos - 2] != '.'
+		|| cclp->cmdbuff[cclp->cmdpos - 3] != '.'))
+    {
+	// go down a directory
+	key = p_wc;
+	KeyTyped = TRUE;  // in case the key was mapped
+    }
+    else if (STRNCMP(xp->xp_pattern, upseg + 1, 3) == 0 && key == K_DOWN)
+    {
+	// If in a direct ancestor, strip off one ../ to go down
+	int found = FALSE;
+
+	j = cclp->cmdpos;
+	i = (int)(xp->xp_pattern - cclp->cmdbuff);
+	while (--j > i)
+	{
+	    if (has_mbyte)
+		j -= (*mb_head_off)(cclp->cmdbuff, cclp->cmdbuff + j);
+	    if (vim_ispathsep(cclp->cmdbuff[j]))
+	    {
+		found = TRUE;
+		break;
+	    }
+	}
+	if (found
+		&& cclp->cmdbuff[j - 1] == '.'
+		&& cclp->cmdbuff[j - 2] == '.'
+		&& (vim_ispathsep(cclp->cmdbuff[j - 3]) || j == i + 2))
+	{
+	    cmdline_del(cclp, j - 2);
+	    key = p_wc;
 	    KeyTyped = TRUE;  // in case the key was mapped
-	    xp->xp_context = EXPAND_NOTHING;
 	}
     }
-    if ((xp->xp_context == EXPAND_FILES
-		|| xp->xp_context == EXPAND_DIRECTORIES
-		|| xp->xp_context == EXPAND_SHELLCMD))
+    else if (key == K_UP)
     {
-	char_u upseg[5];
+	// go up a directory
+	int found = FALSE;
 
-	upseg[0] = PATHSEP;
-	upseg[1] = '.';
-	upseg[2] = '.';
-	upseg[3] = PATHSEP;
-	upseg[4] = NUL;
-
-	if (c == K_DOWN
-		&& cclp->cmdpos > 0
-		&& cclp->cmdbuff[cclp->cmdpos - 1] == PATHSEP
-		&& (cclp->cmdpos < 3
-		    || cclp->cmdbuff[cclp->cmdpos - 2] != '.'
-		    || cclp->cmdbuff[cclp->cmdpos - 3] != '.'))
+	j = cclp->cmdpos - 1;
+	i = (int)(xp->xp_pattern - cclp->cmdbuff);
+	while (--j > i)
 	{
-	    // go down a directory
-	    c = p_wc;
-	    KeyTyped = TRUE;  // in case the key was mapped
-	}
-	else if (STRNCMP(xp->xp_pattern, upseg + 1, 3) == 0 && c == K_DOWN)
-	{
-	    // If in a direct ancestor, strip off one ../ to go down
-	    int found = FALSE;
-
-	    j = cclp->cmdpos;
-	    i = (int)(xp->xp_pattern - cclp->cmdbuff);
-	    while (--j > i)
+	    if (has_mbyte)
+		j -= (*mb_head_off)(cclp->cmdbuff, cclp->cmdbuff + j);
+	    if (vim_ispathsep(cclp->cmdbuff[j])
+# ifdef BACKSLASH_IN_FILENAME
+		    && vim_strchr((char_u *)" *?[{`$%#",
+			cclp->cmdbuff[j + 1]) == NULL
+# endif
+	       )
 	    {
-		if (has_mbyte)
-		    j -= (*mb_head_off)(cclp->cmdbuff, cclp->cmdbuff + j);
-		if (vim_ispathsep(cclp->cmdbuff[j]))
+		if (found)
 		{
-		    found = TRUE;
+		    i = j + 1;
 		    break;
 		}
-	    }
-	    if (found
-		    && cclp->cmdbuff[j - 1] == '.'
-		    && cclp->cmdbuff[j - 2] == '.'
-		    && (vim_ispathsep(cclp->cmdbuff[j - 3]) || j == i + 2))
-	    {
-		cmdline_del(cclp, j - 2);
-		c = p_wc;
-		KeyTyped = TRUE;  // in case the key was mapped
+		else
+		    found = TRUE;
 	    }
 	}
-	else if (c == K_UP)
-	{
-	    // go up a directory
-	    int found = FALSE;
 
-	    j = cclp->cmdpos - 1;
-	    i = (int)(xp->xp_pattern - cclp->cmdbuff);
-	    while (--j > i)
-	    {
-		if (has_mbyte)
-		    j -= (*mb_head_off)(cclp->cmdbuff, cclp->cmdbuff + j);
-		if (vim_ispathsep(cclp->cmdbuff[j])
-# ifdef BACKSLASH_IN_FILENAME
-			&& vim_strchr((char_u *)" *?[{`$%#",
-			    cclp->cmdbuff[j + 1]) == NULL
-# endif
-		   )
-		{
-		    if (found)
-		    {
-			i = j + 1;
-			break;
-		    }
-		    else
-			found = TRUE;
-		}
-	    }
+	if (!found)
+	    j = i;
+	else if (STRNCMP(cclp->cmdbuff + j, upseg, 4) == 0)
+	    j += 4;
+	else if (STRNCMP(cclp->cmdbuff + j, upseg + 1, 3) == 0
+		&& j == i)
+	    j += 3;
+	else
+	    j = 0;
+	if (j > 0)
+	{
+	    // TODO this is only for DOS/UNIX systems - need to put in
+	    // machine-specific stuff here and in upseg init
+	    cmdline_del(cclp, j);
+	    put_on_cmdline(upseg + 1, 3, FALSE);
+	}
+	else if (cclp->cmdpos > i)
+	    cmdline_del(cclp, i);
 
-	    if (!found)
-		j = i;
-	    else if (STRNCMP(cclp->cmdbuff + j, upseg, 4) == 0)
-		j += 4;
-	    else if (STRNCMP(cclp->cmdbuff + j, upseg + 1, 3) == 0
-		    && j == i)
-		j += 3;
-	    else
-		j = 0;
-	    if (j > 0)
-	    {
-		// TODO this is only for DOS/UNIX systems - need to put in
-		// machine-specific stuff here and in upseg init
-		cmdline_del(cclp, j);
-		put_on_cmdline(upseg + 1, 3, FALSE);
-	    }
-	    else if (cclp->cmdpos > i)
-		cmdline_del(cclp, i);
-
-	    // Now complete in the new directory. Set KeyTyped in case the
-	    // Up key came from a mapping.
-	    c = p_wc;
-	    KeyTyped = TRUE;
-	}
+	// Now complete in the new directory. Set KeyTyped in case the
+	// Up key came from a mapping.
+	key = p_wc;
+	KeyTyped = TRUE;
     }
 
-    return c;
+    return key;
+}
+
+/*
+ * Handle a key pressed when the wild menu is displayed
+ */
+    int
+wildmenu_process_key(cmdline_info_T *cclp, int key, expand_T *xp)
+{
+    if (xp->xp_context == EXPAND_MENUNAMES)
+	return wildmenu_process_key_menunames(cclp, key, xp);
+    else if ((xp->xp_context == EXPAND_FILES
+		|| xp->xp_context == EXPAND_DIRECTORIES
+		|| xp->xp_context == EXPAND_SHELLCMD))
+	return wildmenu_process_key_filenames(cclp, key, xp);
+
+    return key;
 }
 
 /*
--- a/src/ex_getln.c
+++ b/src/ex_getln.c
@@ -1856,7 +1856,8 @@ getcmdline_int(
 	    c = Ctrl_P;
 
 #ifdef FEAT_WILDMENU
-	c = wildmenu_translate_key(&ccline, c, &xpc, did_wild_list);
+	if (p_wmnu)
+	    c = wildmenu_translate_key(&ccline, c, &xpc, did_wild_list);
 
 	if (cmdline_pum_active())
 	{
@@ -1900,7 +1901,8 @@ getcmdline_int(
 	}
 
 #ifdef FEAT_WILDMENU
-	c = wildmenu_process_key(&ccline, c, &xpc);
+	if (p_wmnu)
+	    c = wildmenu_process_key(&ccline, c, &xpc);
 #endif
 
 	// CTRL-\ CTRL-N goes to Normal mode, CTRL-\ CTRL-G goes to Insert
--- a/src/proto/usercmd.pro
+++ b/src/proto/usercmd.pro
@@ -1,5 +1,6 @@
 /* usercmd.c */
 char_u *find_ucmd(exarg_T *eap, char_u *p, int *full, expand_T *xp, int *complp);
+char_u *set_context_in_user_cmdarg(char_u *cmd, char_u *arg, long argt, int compl, expand_T *xp, int forceit);
 char_u *set_context_in_user_cmd(expand_T *xp, char_u *arg_in);
 char_u *expand_user_command_name(int idx);
 char_u *get_user_commands(expand_T *xp, int idx);
--- a/src/testdir/test_cmdline.vim
+++ b/src/testdir/test_cmdline.vim
@@ -53,9 +53,13 @@ func Test_complete_list()
     set completeslash=backslash
     call feedkeys(":e Xtest\<Tab>\<C-B>\"\<CR>", 'xt')
     call assert_equal('"e Xtest\', @:)
+    call feedkeys(":e Xtest/\<Tab>\<C-B>\"\<CR>", 'xt')
+    call assert_equal('"e Xtest\a.', @:)
     set completeslash=slash
     call feedkeys(":e Xtest\<Tab>\<C-B>\"\<CR>", 'xt')
     call assert_equal('"e Xtest/', @:)
+    call feedkeys(":e Xtest\\\<Tab>\<C-B>\"\<CR>", 'xt')
+    call assert_equal('"e Xtest/a.', @:)
     set completeslash&
   endif
 
@@ -139,6 +143,7 @@ func Test_complete_wildmenu()
   call assert_equal('"e Xtestfile3 Xtestfile4', @:)
   cd -
 
+  " test for wildmenumode()
   cnoremap <expr> <F2> wildmenumode()
   call feedkeys(":cd Xdir\<Tab>\<F2>\<C-B>\"\<CR>", 'tx')
   call assert_equal('"cd Xdir1/0', @:)
@@ -148,12 +153,7 @@ func Test_complete_wildmenu()
 
   " cleanup
   %bwipe
-  call delete('Xdir1/Xdir2/Xtestfile4')
-  call delete('Xdir1/Xdir2/Xtestfile3')
-  call delete('Xdir1/Xtestfile2')
-  call delete('Xdir1/Xtestfile1')
-  call delete('Xdir1/Xdir2', 'd')
-  call delete('Xdir1', 'd')
+  call delete('Xdir1', 'rf')
   set nowildmenu
 endfunc
 
@@ -1100,6 +1100,10 @@ func Test_cmdline_complete_various()
     call feedkeys(":e Xx\*\<Tab>\<C-B>\"\<CR>", 'xt')
     call assert_equal('"e Xx\*Yy', @:)
     call delete('Xx*Yy')
+
+    " use a literal star
+    call feedkeys(":e \\*\<Tab>\<C-B>\"\<CR>", 'xt')
+    call assert_equal('"e \*', @:)
   endif
 
   call feedkeys(":py3f\<Tab>\<C-B>\"\<CR>", 'xt')
@@ -2005,28 +2009,34 @@ endfunc
 func Test_wildmenu_dirstack()
   CheckUnix
   %bw!
-  call mkdir('Xdir1/dir2/dir3', 'p')
+  call mkdir('Xdir1/dir2/dir3/dir4', 'p')
   call writefile([], 'Xdir1/file1_1.txt')
   call writefile([], 'Xdir1/file1_2.txt')
   call writefile([], 'Xdir1/dir2/file2_1.txt')
   call writefile([], 'Xdir1/dir2/file2_2.txt')
   call writefile([], 'Xdir1/dir2/dir3/file3_1.txt')
   call writefile([], 'Xdir1/dir2/dir3/file3_2.txt')
-  cd Xdir1/dir2/dir3
+  call writefile([], 'Xdir1/dir2/dir3/dir4/file4_1.txt')
+  call writefile([], 'Xdir1/dir2/dir3/dir4/file4_2.txt')
   set wildmenu
 
+  cd Xdir1/dir2/dir3/dir4
   call feedkeys(":e \<Tab>\<C-B>\"\<CR>", 'xt')
-  call assert_equal('"e file3_1.txt', @:)
+  call assert_equal('"e file4_1.txt', @:)
   call feedkeys(":e \<Tab>\<Up>\<C-B>\"\<CR>", 'xt')
-  call assert_equal('"e ../dir3/', @:)
+  call assert_equal('"e ../dir4/', @:)
   call feedkeys(":e \<Tab>\<Up>\<Up>\<C-B>\"\<CR>", 'xt')
-  call assert_equal('"e ../../dir2/', @:)
+  call assert_equal('"e ../../dir3/', @:)
+  call feedkeys(":e \<Tab>\<Up>\<Up>\<Up>\<C-B>\"\<CR>", 'xt')
+  call assert_equal('"e ../../../dir2/', @:)
   call feedkeys(":e \<Tab>\<Up>\<Up>\<Down>\<C-B>\"\<CR>", 'xt')
-  call assert_equal('"e ../../dir2/dir3/', @:)
+  call assert_equal('"e ../../dir3/dir4/', @:)
   call feedkeys(":e \<Tab>\<Up>\<Up>\<Down>\<Down>\<C-B>\"\<CR>", 'xt')
-  call assert_equal('"e ../../dir2/dir3/file3_1.txt', @:)
+  call assert_equal('"e ../../dir3/dir4/file4_1.txt', @:)
+  cd -
+  call feedkeys(":e Xdir1/\<Tab>\<Down>\<Down>\<Down>\<C-B>\"\<CR>", 'xt')
+  call assert_equal('"e Xdir1/dir2/dir3/dir4/file4_1.txt', @:)
 
-  cd -
   call delete('Xdir1', 'rf')
   set wildmenu&
 endfunc
--- a/src/usercmd.c
+++ b/src/usercmd.c
@@ -231,6 +231,9 @@ find_ucmd(
     return p;
 }
 
+/*
+ * Set completion context for :command
+ */
     char_u *
 set_context_in_user_cmd(expand_T *xp, char_u *arg_in)
 {
@@ -292,6 +295,56 @@ set_context_in_user_cmd(expand_T *xp, ch
     return skipwhite(p);
 }
 
+/*
+ * Set the completion context for the argument of a user defined command.
+ */
+    char_u *
+set_context_in_user_cmdarg(
+	char_u		*cmd UNUSED,
+	char_u		*arg,
+	long		argt,
+	int		compl,
+	expand_T	*xp,
+	int		forceit)
+{
+    char_u	*p;
+
+    if (compl == EXPAND_NOTHING)
+	return NULL;
+
+    if (argt & EX_XFILE)
+    {
+	// EX_XFILE: file names are handled before this call
+	xp->xp_context = compl;
+	return NULL;
+    }
+
+#ifdef FEAT_MENU
+    if (compl == EXPAND_MENUS)
+	return set_context_in_menu_cmd(xp, cmd, arg, forceit);
+#endif
+    if (compl == EXPAND_COMMANDS)
+	return arg;
+    if (compl == EXPAND_MAPPINGS)
+	return set_context_in_map_cmd(xp, (char_u *)"map", arg, forceit, FALSE,
+							FALSE, CMD_map);
+    // Find start of last argument.
+    p = arg;
+    while (*p)
+    {
+	if (*p == ' ')
+	    // argument starts after a space
+	    arg = p + 1;
+	else if (*p == '\\' && *(p + 1) != NUL)
+	    ++p; // skip over escaped character
+	MB_PTR_ADV(p);
+    }
+    xp->xp_pattern = arg;
+    xp->xp_context = compl;
+
+    return NULL;
+}
+
     char_u *
 expand_user_command_name(int idx)
 {
--- 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 */
 /**/
+    4398,
+/**/
     4397,
 /**/
     4396,