patch 9.0.1991: no cmdline completion for setting the font

Problem:  no cmdline completion for setting the font
Solution: enable it on Win32 and GTK builds

Add guifont cmdline completion (for Windows and GTK)

For Windows, auto-complete will only suggest monospace fonts as that's
the only types allowed. Will also suggest font options after the colon,
including suggesting the current font size for convenience, and misc
charset and quality options like `cANSI` and `qCLEARTYPE`.

For GTK, auto-complete will suggest only monospace fonts for `guifont`
but will include all fonts for `guifontwide`. The completion code
doesn't currently suggest the current font size, as the GTK guifont
format does not have a clear delimiter (':' for other platforms).

closes: #13264

Signed-off-by: Christian Brabandt <cb@256bit.org>
Co-authored-by: Yee Cheng Chin <ychin.git@gmail.com>
diff --git a/src/gui_gtk_x11.c b/src/gui_gtk_x11.c
index 3fd6f3a..4b3f53e 100644
--- a/src/gui_gtk_x11.c
+++ b/src/gui_gtk_x11.c
@@ -5316,6 +5316,62 @@
 }
 
 /*
+ * Cmdline expansion for setting 'guifont' / 'guifontwide'. Will enumerate
+ * through all fonts for completion. When setting 'guifont' it will only show
+ * monospace fonts as it's unlikely other fonts would be useful.
+ */
+    void
+gui_mch_expand_font(optexpand_T *args, void *param, int (*add_match)(char_u *val))
+{
+    PangoFontFamily	**font_families = NULL;
+    int			n_families = 0;
+    int			wide = *(int *)param;
+
+    if (args->oe_include_orig_val && *args->oe_opt_value == NUL && !wide)
+    {
+	// If guifont is empty, and we want to fill in the orig value, suggest
+	// the default so the user can modify it.
+	if (add_match((char_u *)DEFAULT_FONT) != OK)
+	    return;
+    }
+
+    pango_context_list_families(
+	    gui.text_context,
+	    &font_families,
+	    &n_families);
+
+    for (int i = 0; i < n_families; i++)
+    {
+	if (!wide && !pango_font_family_is_monospace(font_families[i]))
+	    continue;
+
+	const char* fam_name = pango_font_family_get_name(font_families[i]);
+	if (input_conv.vc_type != CONV_NONE)
+	{
+	    char_u *buf = string_convert(&input_conv, (char_u*)fam_name, NULL);
+	    if (buf != NULL)
+	    {
+		if (add_match(buf) != OK)
+		{
+		    vim_free(buf);
+		    break;
+		}
+		vim_free(buf);
+	    }
+	    else
+		break;
+	}
+	else
+	{
+	    if (add_match((char_u *)fam_name) != OK)
+		break;
+	}
+    }
+
+    g_free(font_families);
+}
+
+/*
  * Return the Pixel value (color) for the given color name.
  *
  * Return INVALCOLOR for error.
diff --git a/src/optiondefs.h b/src/optiondefs.h
index f850d19..c4810e7 100644
--- a/src/optiondefs.h
+++ b/src/optiondefs.h
@@ -1164,9 +1164,13 @@
 			    {(char_u *)NULL, (char_u *)0L}
 #endif
 			    SCTX_INIT},
-    {"guifont",	    "gfn",  P_STRING|P_VI_DEF|P_RCLR|P_ONECOMMA|P_NODUP,
+    {"guifont",	    "gfn",  P_STRING|P_VI_DEF|P_RCLR|P_ONECOMMA|P_NODUP
+#if !defined(FEAT_GUI_GTK)
+				|P_COLON
+#endif
+				,
 #ifdef FEAT_GUI
-			    (char_u *)&p_guifont, PV_NONE, did_set_guifont, NULL,
+			    (char_u *)&p_guifont, PV_NONE, did_set_guifont, expand_set_guifont,
 			    {(char_u *)"", (char_u *)0L}
 #else
 			    (char_u *)NULL, PV_NONE, NULL, NULL,
@@ -1183,10 +1187,14 @@
 			    {(char_u *)NULL, (char_u *)0L}
 #endif
 			    SCTX_INIT},
-    {"guifontwide", "gfw",  P_STRING|P_VI_DEF|P_RCLR|P_ONECOMMA|P_NODUP,
+    {"guifontwide", "gfw",  P_STRING|P_VI_DEF|P_RCLR|P_ONECOMMA|P_NODUP
+#if !defined(FEAT_GUI_GTK)
+				|P_COLON
+#endif
+				,
 #if defined(FEAT_GUI)
 			    (char_u *)&p_guifontwide, PV_NONE,
-			    did_set_guifontwide, NULL,
+			    did_set_guifontwide, expand_set_guifont,
 			    {(char_u *)"", (char_u *)0L}
 #else
 			    (char_u *)NULL, PV_NONE, NULL, NULL,
diff --git a/src/optionstr.c b/src/optionstr.c
index 135cb9a..6b40dcd 100644
--- a/src/optionstr.c
+++ b/src/optionstr.c
@@ -731,7 +731,8 @@
 }
 
 /*
- * Expand an option that accepts a list of string values.
+ * Expand an option that accepts a list of fixed string values with known
+ * number of items.
  */
     static int
 expand_set_opt_string(
@@ -801,10 +802,11 @@
 static char_u *((*set_opt_callback_func)(expand_T *, int));
 
 /*
- * Callback used by expand_set_opt_generic to also include the original value.
+ * Callback used by expand_set_opt_generic to also include the original value
+ * as the first item.
  */
     static char_u *
-expand_set_opt_callback(expand_T *xp, int idx)
+expand_set_opt_generic_cb(expand_T *xp, int idx)
 {
     if (idx == 0)
     {
@@ -817,7 +819,8 @@
 }
 
 /*
- * Expand an option with a callback that iterates through a list of possible names.
+ * Expand an option with a callback that iterates through a list of possible
+ * names using an index.
  */
     static int
 expand_set_opt_generic(
@@ -838,7 +841,7 @@
 	    args->oe_regmatch,
 	    matches,
 	    numMatches,
-	    expand_set_opt_callback,
+	    expand_set_opt_generic_cb,
 	    FALSE);
 
     set_opt_callback_orig_option = NULL;
@@ -846,6 +849,95 @@
     return ret;
 }
 
+static garray_T *expand_cb_ga;
+static optexpand_T *expand_cb_args;
+
+/*
+ * Callback provided to a function in expand_set_opt_callback. Will perform
+ * regex matching against the value and add to the list.
+ *
+ * Returns OK usually. Returns FAIL if it failed to allocate memory, and the
+ * caller should terminate the enumeration.
+ */
+    static int
+expand_set_opt_callback_cb(char_u *val)
+{
+    regmatch_T	*regmatch = expand_cb_args->oe_regmatch;
+    expand_T	*xp = expand_cb_args->oe_xp;
+    garray_T	*ga = expand_cb_ga;
+    char_u	*str;
+
+    if (val == NULL || *val == NUL)
+	return OK;
+
+    if (xp->xp_pattern[0] != NUL &&
+	    !vim_regexec(regmatch, val, (colnr_T)0))
+	return OK;
+
+    str = vim_strsave_escaped(val, (char_u *)" \t\\");
+
+    if (str == NULL)
+	return FAIL;
+
+    if (ga_grow(ga, 1) == FAIL)
+    {
+	vim_free(str);
+	return FAIL;
+    }
+
+    ((char_u **)ga->ga_data)[ga->ga_len] = str;
+    ++ga->ga_len;
+    return OK;
+}
+
+/*
+ * Expand an option with a provided function that takes a callback. The
+ * function will enumerate through all options and call the callback to add it
+ * to the list.
+ *
+ * "func" is the enumerator function that will generate the list of options.
+ * "func_params" is a single parameter that will be passed to func.
+ */
+    static int
+expand_set_opt_callback(
+	optexpand_T *args,
+	void (*func)(optexpand_T *, void* params, int (*cb)(char_u *val)),
+	void *func_params,
+	int *numMatches,
+	char_u ***matches)
+{
+    garray_T	ga;
+    int		include_orig_val = args->oe_include_orig_val;
+    char_u	*option_val = args->oe_opt_value;
+
+    ga_init2(&ga, sizeof(char *), 30);
+
+    if (include_orig_val && *option_val != NUL)
+    {
+	char_u *p = vim_strsave(option_val);
+	if (p == NULL)
+	    return FAIL;
+	if (ga_grow(&ga, 1) == FAIL)
+	{
+	    vim_free(p);
+	    return FAIL;
+	}
+	((char_u **)ga.ga_data)[ga.ga_len] = p;
+	++ga.ga_len;
+    }
+
+    expand_cb_ga = &ga;
+    expand_cb_args = args;
+
+    func(args, func_params, expand_set_opt_callback_cb);
+
+    expand_cb_ga = NULL;
+    expand_cb_args = NULL;
+
+    *matches = ga.ga_data;
+    *numMatches = ga.ga_len;
+    return OK;
+}
 
 /*
  * Expand an option which is a list of flags.
@@ -2237,6 +2329,27 @@
     return errmsg;
 }
 
+/*
+ * Expand the 'guifont' option. Only when GUI is being used. Each platform has
+ * specific behaviors.
+ */
+    int
+expand_set_guifont(optexpand_T *args, int *numMatches, char_u ***matches)
+{
+    if (!gui.in_use)
+	return FAIL;
+
+# if defined(FEAT_GUI_MSWIN) || defined(FEAT_GUI_GTK)
+    char_u **varp = (char_u **)args->oe_varp;
+    int wide = (varp == &p_guifontwide);
+
+    return expand_set_opt_callback(
+	    args, gui_mch_expand_font, &wide, numMatches, matches);
+# else
+    return FAIL;
+# endif
+}
+
 # if defined(FEAT_XFONTSET) || defined(PROTO)
 /*
  * The 'guifontset' option is changed.
diff --git a/src/os_mswin.c b/src/os_mswin.c
index aa5fe52..46f7375 100644
--- a/src/os_mswin.c
+++ b/src/os_mswin.c
@@ -2819,6 +2819,32 @@
     return pixels;
 }
 
+/*
+ * Convert pixel into point size. This is a reverse of points_to_pixels.
+ */
+    static double
+pixels_to_points(int pixels, int vertical, long_i pprinter_dc)
+{
+    double	points = 0;
+    HWND	hwnd = (HWND)0;
+    HDC		hdc;
+    HDC		printer_dc = (HDC)pprinter_dc;
+
+    if (printer_dc == NULL)
+    {
+	hwnd = GetDesktopWindow();
+	hdc = GetWindowDC(hwnd);
+    }
+    else
+	hdc = printer_dc;
+
+    points = pixels * 72.0 / GetDeviceCaps(hdc, vertical ? LOGPIXELSY : LOGPIXELSX);
+    if (printer_dc == NULL)
+	ReleaseDC(hwnd, hdc);
+
+    return points;
+}
+
     static int CALLBACK
 font_enumproc(
     ENUMLOGFONTW    *elf,
@@ -2890,6 +2916,100 @@
 }
 
 /*
+ * Call back for EnumFontFamiliesW in expand_font_enumproc.
+ *
+ */
+    static int CALLBACK
+expand_font_enumproc(
+    ENUMLOGFONTW    *elf,
+    NEWTEXTMETRICW  *ntm UNUSED,
+    DWORD	    type UNUSED,
+    LPARAM	    lparam)
+{
+    LOGFONTW *lf = (LOGFONTW*)elf;
+
+# ifndef FEAT_PROPORTIONAL_FONTS
+    // Ignore non-monospace fonts without further ado
+    if ((ntm->tmPitchAndFamily & 1) != 0)
+	return 1;
+# endif
+
+    // Filter only on ANSI. Otherwise will see a lot of random fonts that we
+    // usually don't want.
+    if (lf->lfCharSet != ANSI_CHARSET)
+        return 1;
+
+    int (*add_match)(char_u *) = (int (*)(char_u *))lparam;
+
+    WCHAR *faceNameW = lf->lfFaceName;
+    char_u *faceName = utf16_to_enc(faceNameW, NULL);
+    if (!faceName)
+	return 0;
+
+    add_match(faceName);
+    vim_free(faceName);
+
+    return 1;
+}
+
+/*
+ * Cmdline expansion for setting 'guifont'. Will enumerate through all
+ * monospace fonts for completion. If used after ':', will expand to possible
+ * font configuration options like font sizes.
+ *
+ * This function has "gui" in its name because in some platforms (GTK) font
+ * handling is done by the GUI code, whereas in Windows it's part of the
+ * platform code.
+ */
+    void
+gui_mch_expand_font(optexpand_T *args, void *param UNUSED, int (*add_match)(char_u *val))
+{
+    expand_T	    *xp = args->oe_xp;
+    if (xp->xp_pattern > args->oe_set_arg && *(xp->xp_pattern-1) == ':')
+    {
+	char buf[30];
+
+	// Always fill in with the current font size as first option for
+	// convenience. We simply round to the closest integer for simplicity.
+        int font_height = (int)round(
+		pixels_to_points(-current_font_height, TRUE, (long_i)NULL));
+	vim_snprintf(buf, ARRAY_LENGTH(buf), "h%d", font_height);
+	add_match((char_u *)buf);
+
+	// Note: Keep this in sync with get_logfont(). Don't include 'c' and
+	// 'q' as we fill in all the values below.
+	static char *(p_gfn_win_opt_values[]) = {
+	    "h" , "w" , "W" , "b" , "i" , "u" , "s"};
+	for (size_t i = 0; i < ARRAY_LENGTH(p_gfn_win_opt_values); i++)
+	    add_match((char_u *)p_gfn_win_opt_values[i]);
+
+	struct charset_pair *cp;
+	for (cp = charset_pairs; cp->name != NULL; ++cp)
+	{
+	    vim_snprintf(buf, ARRAY_LENGTH(buf), "c%s", cp->name);
+	    add_match((char_u *)buf);
+	}
+	struct quality_pair *qp;
+	for (qp = quality_pairs; qp->name != NULL; ++qp)
+	{
+	    vim_snprintf(buf, ARRAY_LENGTH(buf), "q%s", qp->name);
+	    add_match((char_u *)buf);
+	}
+	return;
+    }
+
+    HWND	hwnd = GetDesktopWindow();
+    HDC		hdc = GetWindowDC(hwnd);
+
+    EnumFontFamiliesW(hdc,
+	    NULL,
+	    (FONTENUMPROCW)expand_font_enumproc,
+	    (LPARAM)add_match);
+
+    ReleaseDC(hwnd, hdc);
+}
+
+/*
  * Compare a UTF-16 string and an ASCII string literally.
  * Only works all the code points are inside ASCII range.
  */
@@ -2995,6 +3115,7 @@
     {
 	switch (*p++)
 	{
+	    // Note: Keep this in sync with gui_mch_expand_font().
 	    case L'h':
 		lf->lfHeight = - points_to_pixels(p, &p, TRUE, (long_i)printer_dc);
 		break;
diff --git a/src/proto/gui_gtk_x11.pro b/src/proto/gui_gtk_x11.pro
index 3fa2ac9..459cdb9 100644
--- a/src/proto/gui_gtk_x11.pro
+++ b/src/proto/gui_gtk_x11.pro
@@ -37,6 +37,7 @@
 GuiFont gui_mch_get_font(char_u *name, int report_error);
 char_u *gui_mch_get_fontname(GuiFont font, char_u *name);
 void gui_mch_free_font(GuiFont font);
+void gui_mch_expand_font(optexpand_T *args, void *param, int (*cb)(char_u *val));
 guicolor_T gui_mch_get_color(char_u *name);
 guicolor_T gui_mch_get_rgb_color(int r, int g, int b);
 void gui_mch_set_fg_color(guicolor_T color);
diff --git a/src/proto/optionstr.pro b/src/proto/optionstr.pro
index 48cccea..88034fc 100644
--- a/src/proto/optionstr.pro
+++ b/src/proto/optionstr.pro
@@ -152,6 +152,7 @@
 int expand_set_foldmethod(optexpand_T *args, int *numMatches, char_u ***matches);
 int expand_set_foldopen(optexpand_T *args, int *numMatches, char_u ***matches);
 int expand_set_formatoptions(optexpand_T *args, int *numMatches, char_u ***matches);
+int expand_set_guifont(optexpand_T *args, int *numMatches, char_u ***matches);
 int expand_set_guioptions(optexpand_T *args, int *numMatches, char_u ***matches);
 int expand_set_highlight(optexpand_T *args, int *numMatches, char_u ***matches);
 int expand_set_jumpoptions(optexpand_T *args, int *numMatches, char_u ***matches);
diff --git a/src/proto/os_mswin.pro b/src/proto/os_mswin.pro
index d9172ee..9f534e0 100644
--- a/src/proto/os_mswin.pro
+++ b/src/proto/os_mswin.pro
@@ -51,5 +51,6 @@
 char *charset_id2name(int id);
 char *quality_id2name(DWORD id);
 int get_logfont(LOGFONTW *lf, char_u *name, HDC printer_dc, int verbose);
+void gui_mch_expand_font(optexpand_T *args, void *param, int (*cb)(char_u *val));
 void channel_init_winsock(void);
 /* vim: set ft=c : */
diff --git a/src/testdir/test_gui.vim b/src/testdir/test_gui.vim
index acc72f8..e7f7c6d 100644
--- a/src/testdir/test_gui.vim
+++ b/src/testdir/test_gui.vim
@@ -580,6 +580,56 @@
   endif
 endfunc
 
+func Test_expand_guifont()
+  if has('gui_win32')
+    let guifont_saved = &guifont
+    let guifontwide_saved = &guifontwide
+
+    " Test recalling existing option, and suggesting current font size
+    set guifont=Courier\ New:h11:cANSI
+    call assert_equal('Courier\ New:h11:cANSI', getcompletion('set guifont=', 'cmdline')[0])
+    call assert_equal('h11', getcompletion('set guifont=Lucida\ Console:', 'cmdline')[0])
+
+    " Test auto-completion working for font names
+    call assert_equal(['Courier\ New'], getcompletion('set guifont=Couri*ew$', 'cmdline'))
+    call assert_equal(['Courier\ New'], getcompletion('set guifontwide=Couri*ew$', 'cmdline'))
+
+    " Make sure non-monospace fonts are filtered out
+    call assert_equal([], getcompletion('set guifont=Arial', 'cmdline'))
+    call assert_equal([], getcompletion('set guifontwide=Arial', 'cmdline'))
+
+    " Test auto-completion working for font options
+    call assert_notequal(-1, index(getcompletion('set guifont=Courier\ New:', 'cmdline'), 'b'))
+    call assert_equal(['cDEFAULT'], getcompletion('set guifont=Courier\ New:cD*T', 'cmdline'))
+    call assert_equal(['qCLEARTYPE'], getcompletion('set guifont=Courier\ New:qC*TYPE', 'cmdline'))
+
+    let &guifontwide = guifontwide_saved
+    let &guifont     = guifont_saved
+  elseif has('gui_gtk')
+    let guifont_saved = &guifont
+    let guifontwide_saved = &guifontwide
+
+    " Test recalling default and existing option
+    set guifont=
+    call assert_equal('Monospace\ 10', getcompletion('set guifont=', 'cmdline')[0])
+    set guifont=Monospace\ 9
+    call assert_equal('Monospace\ 9', getcompletion('set guifont=', 'cmdline')[0])
+
+    " Test auto-completion working for font names
+    call assert_equal(['Monospace'], getcompletion('set guifont=Mono*pace$', 'cmdline'))
+    call assert_equal(['Monospace'], getcompletion('set guifontwide=Mono*pace$', 'cmdline'))
+
+    " Make sure non-monospace fonts are filtered out only in 'guifont'
+    call assert_equal([], getcompletion('set guifont=Sans$', 'cmdline'))
+    call assert_equal(['Sans'], getcompletion('set guifontwide=Sans$', 'cmdline'))
+
+    let &guifontwide = guifontwide_saved
+    let &guifont     = guifont_saved
+  else
+    call assert_equal([], getcompletion('set guifont=', 'cmdline'))
+  endif
+endfunc
+
 func Test_set_guiligatures()
   CheckX11BasedGui
 
diff --git a/src/version.c b/src/version.c
index 0f73dc2..9b1c0b4 100644
--- a/src/version.c
+++ b/src/version.c
@@ -705,6 +705,8 @@
 static int included_patches[] =
 {   /* Add new patch number below this line */
 /**/
+    1991,
+/**/
     1990,
 /**/
     1989,