patch 9.1.1138: cmdline completion for :hi is too simplistic

Problem:  Existing cmdline completion for :highlight was barebone and
          only completed the highlight group names.

Solution: Implement full completion for the highlight group arguments
          such as guifg and cterm. If the user tries to complete
          immediately after the '=' (e.g. `hi Normal guifg=<Tab>`), the
          completion will fill in the existing value, similar to how
          cmdline completion for options work (Yee Cheng Chin).

closes: #16712

Signed-off-by: Yee Cheng Chin <ychin.git@gmail.com>
Signed-off-by: Christian Brabandt <cb@256bit.org>
diff --git a/src/highlight.c b/src/highlight.c
index 8c1ad80..2d96566 100644
--- a/src/highlight.c
+++ b/src/highlight.c
@@ -1691,6 +1691,8 @@
 		break;
 	    }
 
+	    // Note: Keep this in sync with get_highlight_group_key.
+
 	    // Isolate the key ("term", "ctermfg", "ctermbg", "font", "guifg"
 	    // or "guibg").
 	    while (*linep && !VIM_ISWHITE(*linep) && *linep != '=')
@@ -3058,6 +3060,7 @@
     if (message_filtered(sgp->sg_name))
 	return;
 
+    // Note: Keep this in sync with expand_highlight_group().
     didh = highlight_list_arg(id, didh, LIST_ATTR,
 				    sgp->sg_term, NULL, "term");
     didh = highlight_list_arg(id, didh, LIST_STRING,
@@ -3108,37 +3111,24 @@
 #endif
 }
 
-    static int
-highlight_list_arg(
-    int		id,
-    int		didh,
+    static char_u*
+highlight_arg_to_string(
     int		type,
     int		iarg,
     char_u	*sarg,
-    char	*name)
+    char_u	*buf)
 {
-    char_u	buf[MAX_ATTR_LEN];
-    char_u	*ts;
-    int		i;
-
-    if (got_int)
-	return FALSE;
-
-    if (type == LIST_STRING ? (sarg == NULL) : (iarg == 0))
-	return didh;
-
-    ts = buf;
     if (type == LIST_INT)
 	sprintf((char *)buf, "%d", iarg - 1);
     else if (type == LIST_STRING)
-	ts = sarg;
+	return sarg;
     else // type == LIST_ATTR
     {
 	size_t buflen;
 
 	buf[0] = NUL;
 	buflen = 0;
-	for (i = 0; i < (int)ARRAY_LENGTH(highlight_index_tab); ++i)
+	for (int i = 0; i < (int)ARRAY_LENGTH(highlight_index_tab); ++i)
 	{
 	    if (iarg & highlight_index_tab[i]->key)
 	    {
@@ -3153,6 +3143,28 @@
 	    }
 	}
     }
+    return buf;
+}
+
+    static int
+highlight_list_arg(
+    int		id,
+    int		didh,
+    int		type,
+    int		iarg,
+    char_u	*sarg,
+    char	*name)
+{
+    char_u	buf[MAX_ATTR_LEN];
+    char_u	*ts;
+
+    if (got_int)
+	return FALSE;
+
+    if (type == LIST_STRING ? (sarg == NULL) : (iarg == 0))
+	return didh;
+
+    ts = highlight_arg_to_string(type, iarg, sarg, buf);
 
     (void)syn_list_header(didh,
 	    (int)(vim_strsize(ts) + STRLEN(name) + 1), id);
@@ -4078,6 +4090,15 @@
 static void highlight_list(void);
 static void highlight_list_two(int cnt, int attr);
 
+// context for :highlight <group> <arg> expansion
+static int expand_hi_synid = 0;	    // ID for highlight group being completed
+static int expand_hi_equal_col = 0; // column where the '=' is
+static int expand_hi_include_orig = 0;	    // whether to fill the existing current value or not
+static char_u *expand_hi_curvalue = NULL;   // the existing current value
+#if defined(FEAT_EVAL) && (defined(FEAT_GUI) || defined(FEAT_TERMGUICOLORS))
+static dict_iterator_T expand_colornames_iter;	// iterator for looping through v:colornames
+#endif
+
 /*
  * Handle command line completion for :highlight command.
  */
@@ -4085,10 +4106,12 @@
 set_context_in_highlight_cmd(expand_T *xp, char_u *arg)
 {
     char_u	*p;
+    int		expand_group = TRUE;
 
     // Default: expand group names
     xp->xp_context = EXPAND_HIGHLIGHT;
     xp->xp_pattern = arg;
+    include_none = 0;
     include_link = 2;
     include_default = 1;
 
@@ -4114,9 +4137,11 @@
     // past group name
     include_link = 0;
     if (arg[1] == 'i' && arg[0] == 'N')
+    {
 	highlight_list();
-    if (STRNCMP("link", arg, p - arg) == 0
-	    || STRNCMP("clear", arg, p - arg) == 0)
+	expand_group = FALSE;
+    }
+    if (STRNCMP("link", arg, p - arg) == 0)
     {
 	xp->xp_pattern = skipwhite(p);
 	p = skiptowhite(xp->xp_pattern);
@@ -4124,10 +4149,67 @@
 	{
 	    xp->xp_pattern = skipwhite(p);
 	    p = skiptowhite(xp->xp_pattern);
+	    include_none = 1;
 	}
+	expand_group = FALSE;
+    }
+    else if (STRNCMP("clear", arg, p - arg) == 0)
+    {
+	xp->xp_pattern = skipwhite(p);
+	p = skiptowhite(xp->xp_pattern);
+	expand_group = FALSE;
     }
     if (*p != NUL)			// past group name(s)
-	xp->xp_context = EXPAND_NOTHING;
+    {
+	if (expand_group)
+	{
+	    // expansion will be done in expand_highlight_group()
+	    xp->xp_context = EXPAND_HIGHLIGHT_GROUP;
+
+	    expand_hi_synid = syn_namen2id(arg, (int)(p - arg));
+
+	    while (*p != NUL)
+	    {
+		arg = skipwhite(p);
+		p = skiptowhite(arg);
+	    }
+
+	    p = vim_strchr(arg, '=');
+	    if (p == NULL)
+	    {
+		// Didn't find a key=<value> pattern
+		xp->xp_pattern = arg;
+		expand_hi_equal_col = -1;
+		expand_hi_include_orig = FALSE;
+	    }
+	    else
+	    {
+		// Found key=<value> pattern, record the exact location
+		expand_hi_equal_col = (int)(p - xp->xp_line);
+
+		// Only include the original value if the pattern is empty
+		if (*(p + 1) == NUL)
+		    expand_hi_include_orig = TRUE;
+		else
+		    expand_hi_include_orig = FALSE;
+
+		// Account for comma-separated values
+		if (STRNCMP(arg, "term=", 5) == 0 ||
+			STRNCMP(arg, "cterm=", 6) == 0 ||
+			STRNCMP(arg, "gui=", 4) == 0)
+		{
+		    char_u *comma = vim_strrchr(p + 1, ',');
+		    if (comma != NULL)
+			p = comma;
+		}
+		xp->xp_pattern = p + 1;
+	    }
+	}
+	else
+	{
+	    xp->xp_context = EXPAND_NOTHING;
+	}
+    }
 }
 
 /*
@@ -4178,7 +4260,7 @@
 	return (char_u *)"";
 
     if (idx == highlight_ga.ga_len && include_none != 0)
-	return (char_u *)"none";
+	return (char_u *)"NONE";
     if (idx == highlight_ga.ga_len + include_none && include_default != 0)
 	return (char_u *)"default";
     if (idx == highlight_ga.ga_len + include_none + include_default
@@ -4192,6 +4274,300 @@
     return HL_TABLE()[idx].sg_name;
 }
 
+    static char_u *
+get_highlight_attr_name(expand_T *xp UNUSED, int idx)
+{
+    if (idx == 0)
+    {
+	// Fill with current value first
+	if (expand_hi_curvalue != NULL)
+	    return expand_hi_curvalue;
+	else
+	    return (char_u*)"";
+    }
+    if (idx < (int)ARRAY_LENGTH(highlight_index_tab) + 1)
+    {
+	char_u *value = highlight_index_tab[idx-1]->value.string;
+	if (expand_hi_curvalue != NULL && STRCMP(expand_hi_curvalue, value) == 0)
+	{
+	    // Already returned the current value above, just skip.
+	    return (char_u*)"";
+	}
+	return value;
+    }
+    return NULL;
+}
+
+    static char_u *
+get_highlight_cterm_color(expand_T *xp UNUSED, int idx)
+{
+    if (idx == 0)
+    {
+	// Fill with current value first
+	if (expand_hi_curvalue != NULL)
+	    return expand_hi_curvalue;
+	else
+	    return (char_u*)"";
+    }
+    // See highlight_set_cterm_color()
+    else if (idx == 1)
+	return (char_u*)"fg";
+    else if (idx == 2)
+	return (char_u*)"bg";
+    if (idx < (int)ARRAY_LENGTH(color_name_tab) + 3)
+    {
+	char_u *value = color_name_tab[idx-3].value.string;
+	return value;
+    }
+    return NULL;
+}
+
+#if defined(FEAT_EVAL) && (defined(FEAT_GUI) || defined(FEAT_TERMGUICOLORS))
+    static char_u *
+get_highlight_gui_color(expand_T *xp UNUSED, int idx)
+{
+    if (idx == 0)
+    {
+	// Fill with current value first
+	if (expand_hi_curvalue != NULL)
+	    return expand_hi_curvalue;
+	else
+	    return (char_u*)"";
+    }
+    // See color_name2handle()
+    else if (idx == 1)
+	return (char_u*)"fg";
+    else if (idx == 2)
+	return (char_u*)"bg";
+    else if (idx == 3)
+	return (char_u*)"NONE";
+
+    // Complete from v:colornames. Don't do platform specific names for now.
+    typval_T *tv_result;
+    char_u *colorname = dict_iterate_next(&expand_colornames_iter, &tv_result);
+    if (colorname != NULL)
+    {
+	// :hi command doesn't allow space, so don't suggest any malformed items
+	if (vim_strchr(colorname, ' ') != NULL)
+	    return (char_u*)"";
+
+	if (expand_hi_curvalue != NULL && STRICMP(expand_hi_curvalue, colorname) == 0)
+	{
+	    // Already returned the current value above, just skip.
+	    return (char_u*)"";
+	}
+    }
+    return colorname;
+}
+#endif
+
+    static char_u *
+get_highlight_group_key(expand_T *xp UNUSED, int idx)
+{
+    // Note: Keep this in sync with do_highlight.
+    static char *(p_hi_group_key_values[]) =
+    {
+	"term=",
+	"start=",
+	"stop=",
+	"cterm=",
+	"ctermfg=",
+	"ctermbg=",
+	"ctermul=",
+	"ctermfont=",
+#if defined(FEAT_GUI) || defined(FEAT_TERMGUICOLORS)
+	"gui=",
+	"guifg=",
+	"guibg=",
+	"guisp=",
+#endif
+#ifdef FEAT_GUI
+	"font=",
+#endif
+	"NONE",
+    };
+
+    if (idx < (int)ARRAY_LENGTH(p_hi_group_key_values))
+	return (char_u*)p_hi_group_key_values[idx];
+    return NULL;
+}
+
+/*
+ * Command-line expansion for :hi {group-name} <args>...
+ */
+    int
+expand_highlight_group(
+	char_u	    *pat,
+	expand_T    *xp,
+	regmatch_T  *rmp,
+	char_u	    ***matches,
+	int	    *numMatches)
+{
+    if (expand_hi_equal_col != -1)
+    {
+	// List the values. First fill in the current value, then if possible colors
+	// or attribute names.
+	char_u	    *(*expandfunc)(expand_T *, int) = NULL;
+	int	    type = 0;
+	hl_group_T  *sgp = NULL;
+	int	    iarg = 0;
+	char_u	    *sarg = NULL;
+
+	int	    unsortedItems = -1; // don't sort by default
+
+	if (expand_hi_synid != 0)
+	    sgp = &HL_TABLE()[expand_hi_synid - 1]; // index is ID minus one
+
+	// Note: Keep this in sync with highlight_list_one().
+	char_u	    *name_end = xp->xp_line + expand_hi_equal_col;
+	if (name_end - xp->xp_line >= 5
+		&& STRNCMP(name_end - 5, " term", 5) == 0)
+	{
+	    expandfunc = get_highlight_attr_name;
+	    if (sgp)
+	    {
+		type = LIST_ATTR;
+		iarg = sgp->sg_term;
+	    }
+	}
+	else if (name_end - xp->xp_line >= 6
+		&& STRNCMP(name_end - 6, " cterm", 6) == 0)
+	{
+	    expandfunc = get_highlight_attr_name;
+	    if (sgp)
+	    {
+		type = LIST_ATTR;
+		iarg = sgp->sg_cterm;
+	    }
+	}
+#if defined(FEAT_GUI) || defined(FEAT_TERMGUICOLORS)
+	else if (name_end - xp->xp_line >= 4
+		&& STRNCMP(name_end - 4, " gui", 4) == 0)
+	{
+	    expandfunc = get_highlight_attr_name;
+	    if (sgp)
+	    {
+		type = LIST_ATTR;
+		iarg = sgp->sg_gui;
+	    }
+	}
+#endif
+	else if (name_end - xp->xp_line >= 8
+		&& STRNCMP(name_end - 8, " ctermfg", 8) == 0)
+	{
+	    expandfunc = get_highlight_cterm_color;
+	    if (sgp)
+	    {
+		type = LIST_INT;
+		iarg = sgp->sg_cterm_fg;
+	    }
+	}
+	else if (name_end - xp->xp_line >= 8
+		&& STRNCMP(name_end - 8, " ctermbg", 8) == 0)
+	{
+	    expandfunc = get_highlight_cterm_color;
+	    if (sgp)
+	    {
+		type = LIST_INT;
+		iarg = sgp->sg_cterm_bg;
+	    }
+	}
+	else if (name_end - xp->xp_line >= 8
+		&& STRNCMP(name_end - 8, " ctermul", 8) == 0)
+	{
+	    expandfunc = get_highlight_cterm_color;
+	    if (sgp)
+	    {
+		type = LIST_INT;
+		iarg = sgp->sg_cterm_ul;
+	    }
+	}
+#if defined(FEAT_EVAL) && (defined(FEAT_GUI) || defined(FEAT_TERMGUICOLORS))
+	else if (name_end - xp->xp_line >= 6
+		&& STRNCMP(name_end - 6, " guifg", 6) == 0)
+	{
+	    expandfunc = get_highlight_gui_color;
+	    if (sgp)
+	    {
+		type = LIST_STRING;
+		sarg = sgp->sg_gui_fg_name;
+	    }
+	}
+	else if (name_end - xp->xp_line >= 6
+		&& STRNCMP(name_end - 6, " guibg", 6) == 0)
+	{
+	    expandfunc = get_highlight_gui_color;
+	    if (sgp)
+	    {
+		type = LIST_STRING;
+		sarg = sgp->sg_gui_bg_name;
+	    }
+	}
+	else if (name_end - xp->xp_line >= 6
+		&& STRNCMP(name_end - 6, " guisp", 6) == 0)
+	{
+	    expandfunc = get_highlight_gui_color;
+	    if (sgp)
+	    {
+		type = LIST_STRING;
+		sarg = sgp->sg_gui_sp_name;
+	    }
+	}
+#endif
+
+#if defined(FEAT_EVAL) && (defined(FEAT_GUI) || defined(FEAT_TERMGUICOLORS))
+	if (expandfunc == get_highlight_gui_color)
+	{
+	    // Top 4 items are special, after that sort all the color names
+	    unsortedItems = 4;
+
+	    dict_T *colornames_table = get_vim_var_dict(VV_COLORNAMES);
+	    typval_T colornames_val;
+	    colornames_val.v_type = VAR_DICT;
+	    colornames_val.vval.v_dict = colornames_table;
+	    dict_iterate_start(&colornames_val, &expand_colornames_iter);
+	}
+#endif
+
+	char_u	    buf[MAX_ATTR_LEN];
+
+	if (expand_hi_synid != 0 && type != 0 && expand_hi_include_orig)
+	{
+	    // Retrieve the current value to go first in completion
+	    expand_hi_curvalue = highlight_arg_to_string(
+		    type, iarg, sarg, buf);
+	}
+	else
+	    expand_hi_curvalue = NULL;
+
+	if (expandfunc != NULL)
+	{
+	    return ExpandGenericExt(
+		    pat,
+		    xp,
+		    rmp,
+		    matches,
+		    numMatches,
+		    expandfunc,
+		    FALSE,
+		    unsortedItems);
+	}
+
+	return FAIL;
+    }
+
+    // List all the key names
+    return ExpandGenericExt(
+	    pat,
+	    xp,
+	    rmp,
+	    matches,
+	    numMatches,
+	    get_highlight_group_key,
+	    FALSE,
+	    -1);
+}
+
 #if defined(FEAT_GUI) || defined(PROTO)
 /*
  * Free all the highlight group fonts.