patch 8.2.3524: GUI: ligatures are not used

Problem:    GUI: ligatures are not used.
Solution:   Add the 'guiligatures' option. (Dusan Popovic, closes #8933)
diff --git a/src/errors.h b/src/errors.h
index a2a1394..12d00b7 100644
--- a/src/errors.h
+++ b/src/errors.h
@@ -670,3 +670,5 @@
 	INIT(= N_("E1241: Separator not supported: %s"));
 EXTERN char e_no_white_space_allowed_before_separator_str[]
 	INIT(= N_("E1242: No white space allowed before separator: %s"));
+EXTERN char e_ascii_code_not_in_range[]
+	INIT(= N_("E1243: ASCII code not in 32-127 range"));
diff --git a/src/gui.c b/src/gui.c
index 687e9da..1edf659 100644
--- a/src/gui.c
+++ b/src/gui.c
@@ -460,6 +460,10 @@
     gui.scrollbar_width = gui.scrollbar_height = SB_DEFAULT_WIDTH;
     gui.prev_wrap = -1;
 
+# ifdef FEAT_GUI_GTK
+    CLEAR_FIELD(gui.ligatures_map);
+#endif
+
 #if defined(ALWAYS_USE_GUI) || defined(VIMDLL)
     result = OK;
 #else
@@ -1065,6 +1069,36 @@
     return OK;
 }
 
+#if defined(FEAT_GUI_GTK) || defined(PROTO)
+/*
+ * Set list of ascii characters that combined can create ligature.
+ * Store them in char map for quick access from gui_gtk2_draw_string.
+ */
+    void
+gui_set_ligatures(void)
+{
+    char_u	*p;
+
+    if (*p_guiligatures != NUL)
+    {
+	// check for invalid characters
+	for (p = p_guiligatures; *p != NUL; ++p)
+	    if (*p < 32 || *p > 127)
+	    {
+		emsg(_(e_ascii_code_not_in_range));
+		return;
+	    }
+
+	// store valid setting into ligatures_map
+	CLEAR_FIELD(gui.ligatures_map);
+	for (p = p_guiligatures; *p != NUL; ++p)
+	    gui.ligatures_map[*p] = 1;
+    }
+    else
+	CLEAR_FIELD(gui.ligatures_map);
+}
+#endif
+
     static void
 gui_set_cursor(int row, int col)
 {
diff --git a/src/gui.h b/src/gui.h
index b7b526d..9806c83 100644
--- a/src/gui.h
+++ b/src/gui.h
@@ -409,6 +409,9 @@
     char_u	*browse_fname;	    // file name from filedlg
 
     guint32	event_time;
+
+    char_u ligatures_map[256];	    // ascii map for characters 0-255, value is
+				    // 1 if in 'guiligatures'
 #endif	// FEAT_GUI_GTK
 
 #if defined(FEAT_GUI_TABLINE) \
diff --git a/src/gui_gtk_x11.c b/src/gui_gtk_x11.c
index 1a3eada..c55d979 100644
--- a/src/gui_gtk_x11.c
+++ b/src/gui_gtk_x11.c
@@ -5595,18 +5595,22 @@
     int
 gui_gtk2_draw_string(int row, int col, char_u *s, int len, int flags)
 {
-    GdkRectangle	area;		    // area for clip mask
-    PangoGlyphString	*glyphs;	    // glyphs of current item
-    int			column_offset = 0;  // column offset in cells
-    int			i;
-    char_u		*conv_buf = NULL;   // result of UTF-8 conversion
-    char_u		*new_conv_buf;
-    int			convlen;
-    char_u		*sp, *bp;
-    int			plen;
-#if GTK_CHECK_VERSION(3,0,0)
-    cairo_t		*cr;
-#endif
+    char_u	*conv_buf = NULL;   // result of UTF-8 conversion
+    char_u	*new_conv_buf;
+    int		convlen;
+    char_u	*sp, *bp;
+    int		plen;
+    int		len_sum;	// return value needs to add up since we are
+				// printing substrings
+    int		byte_sum;	// byte position in string
+    char_u	*cs;		// current *s pointer
+    int		needs_pango;	// look ahead, 0=ascii 1=unicode/ligatures
+    int		should_need_pango;
+    int		slen;
+    int		is_ligature;
+    int		next_is_ligature;
+    int		is_utf8;
+    char_u	backup_ch;
 
     if (gui.text_context == NULL || gtk_widget_get_window(gui.drawarea) == NULL)
 	return len;
@@ -5653,6 +5657,124 @@
     }
 
     /*
+     * Ligature support and complex utf-8 char optimization:
+     * String received to output to screen can print using pre-cached glyphs
+     * (fast) or Pango (slow). Ligatures and multibype utf-8 must use Pango.
+     * Since we receive mixed content string, split it into logical segments
+     * that are guaranteed to go trough glyphs as much as possible. Since
+     * single ligature char prints as ascii, print it that way.
+     */
+    len_sum = 0;    // return value needs to add up since we are printing
+		    // substrings
+    byte_sum = 0;
+    cs = s;
+    // look ahead, 0=ascii 1=unicode/ligatures
+    needs_pango = ((*cs & 0x80) || gui.ligatures_map[*cs]);
+
+    // split string into ascii and non-ascii (ligatures + utf-8) substrings,
+    // print glyphs or use Pango
+    while (cs < s + len)
+    {
+	slen = 0;
+	while (slen < (len - byte_sum))
+	{
+	    is_ligature = gui.ligatures_map[*(cs + slen)];
+	    // look ahead, single ligature char between ascii is ascii
+	    if (is_ligature && !needs_pango)
+	    {
+		if ((slen + 1) < (len - byte_sum))
+		{
+		    next_is_ligature = gui.ligatures_map[*(cs + slen + 1)];
+		    if (!next_is_ligature)
+			is_ligature = 0;
+		}
+		else
+		{
+		    is_ligature = 0;
+		}
+	    }
+	    is_utf8 = *(cs + slen) & 0x80;
+	    should_need_pango = (is_ligature || is_utf8);
+	    if (needs_pango != should_need_pango) // mode switch
+		break;
+	    if (needs_pango)
+	    {
+		if (is_ligature)
+		{
+		    slen++; // ligature char by char
+		}
+		else
+		{
+		    if ((*(cs + slen) & 0xC0) == 0x80)
+		    {
+			// a continuation, find next 0xC0 != 0x80 but don't
+			// include it
+			while ((slen < (len - byte_sum))
+					    && ((*(cs + slen) & 0xC0) == 0x80))
+			{
+			    slen++;
+			}
+		    }
+		    else if ((*(cs + slen) & 0xE0) == 0xC0)
+		    {
+			// + one byte utf8
+			slen++;
+		    }
+		    else if ((*(cs + slen) & 0xF0) == 0xE0)
+		    {
+			// + two bytes utf8
+			slen += 2;
+		    }
+		    else if ((*(cs + slen) & 0xF8) == 0xF0)
+		    {
+			// + three bytes utf8
+			slen += 3;
+		    }
+		    else
+		    {
+			// this should not happen, try moving forward, Pango
+			// will catch it
+			slen++;
+		    }
+		}
+	    }
+	    else
+	    {
+		slen++; // ascii
+	    }
+	}
+	// temporarily zero terminate substring, print, restore char, wrap
+	backup_ch = *(cs + slen);
+	*(cs + slen) = 0;
+	len_sum += gui_gtk2_draw_string_ext(row, col + len_sum,
+						 cs, slen, flags, needs_pango);
+	*(cs + slen) = backup_ch;
+	cs += slen;
+	byte_sum += slen;
+	needs_pango = should_need_pango;
+    }
+    vim_free(conv_buf);
+    return len_sum;
+}
+
+    int
+gui_gtk2_draw_string_ext(
+	int	row,
+	int	col,
+	char_u	*s,
+	int	len,
+	int	flags,
+	int	force_pango)
+{
+    GdkRectangle	area;		    // area for clip mask
+    PangoGlyphString	*glyphs;	    // glyphs of current item
+    int			column_offset = 0;  // column offset in cells
+    int			i;
+#if GTK_CHECK_VERSION(3,0,0)
+    cairo_t		*cr;
+#endif
+
+    /*
      * Restrict all drawing to the current screen line in order to prevent
      * fuzzy font lookups from messing up the screen.
      */
@@ -5679,7 +5801,8 @@
      */
     if (!(flags & DRAW_ITALIC)
 	    && !((flags & DRAW_BOLD) && gui.font_can_bold)
-	    && gui.ascii_glyphs != NULL)
+	    && gui.ascii_glyphs != NULL
+	    && !force_pango)
     {
 	char_u *p;
 
@@ -5883,7 +6006,6 @@
 #endif
 
     pango_glyph_string_free(glyphs);
-    vim_free(conv_buf);
 
 #if GTK_CHECK_VERSION(3,0,0)
     cairo_destroy(cr);
diff --git a/src/option.h b/src/option.h
index 75c83d5..89cec94 100644
--- a/src/option.h
+++ b/src/option.h
@@ -622,6 +622,9 @@
 EXTERN char_u	*p_guifontwide;	// 'guifontwide'
 EXTERN int	p_guipty;	// 'guipty'
 #endif
+#ifdef FEAT_GUI_GTK
+EXTERN char_u	*p_guiligatures;  // 'guiligatures'
+# endif
 #if defined(FEAT_GUI_GTK) || defined(FEAT_GUI_X11)
 EXTERN long	p_ghr;		// 'guiheadroom'
 #endif
diff --git a/src/optiondefs.h b/src/optiondefs.h
index a7a3d0c..042f055 100644
--- a/src/optiondefs.h
+++ b/src/optiondefs.h
@@ -1208,6 +1208,19 @@
 			    {(char_u *)NULL, (char_u *)0L}
 #endif
 			    SCTX_INIT},
+
+
+    {"guiligatures", "gli", P_STRING|P_VI_DEF|P_RCLR|P_ONECOMMA|P_NODUP,
+#if defined(FEAT_GUI_GTK)
+			    (char_u *)&p_guiligatures, PV_NONE,
+			    {(char_u *)"", (char_u *)0L}
+#else
+			    (char_u *)NULL, PV_NONE,
+			    {(char_u *)NULL, (char_u *)0L}
+#endif
+			    SCTX_INIT},
+
+
     {"guiheadroom", "ghr",  P_NUM|P_VI_DEF,
 #if defined(FEAT_GUI_GTK) || defined(FEAT_GUI_X11)
 			    (char_u *)&p_ghr, PV_NONE,
diff --git a/src/optionstr.c b/src/optionstr.c
index 06a633b..bced92d 100644
--- a/src/optionstr.c
+++ b/src/optionstr.c
@@ -1560,6 +1560,13 @@
 	redraw_gui_only = TRUE;
     }
 #endif
+# if defined(FEAT_GUI_GTK)
+    else if (varp == &p_guiligatures)
+    {
+	gui_set_ligatures();
+	redraw_gui_only = TRUE;
+    }
+# endif
 
 #ifdef CURSOR_SHAPE
     // 'guicursor'
diff --git a/src/proto/gui.pro b/src/proto/gui.pro
index d762420..209e7f1 100644
--- a/src/proto/gui.pro
+++ b/src/proto/gui.pro
@@ -7,6 +7,7 @@
 void gui_shell_closed(void);
 int gui_init_font(char_u *font_list, int fontset);
 int gui_get_wide_font(void);
+void gui_set_ligatures(void);
 void gui_update_cursor(int force, int clear_selection);
 void gui_position_menu(void);
 int gui_get_base_width(void);
diff --git a/src/proto/gui_gtk_x11.pro b/src/proto/gui_gtk_x11.pro
index 1d0a78b..3fa2ac9 100644
--- a/src/proto/gui_gtk_x11.pro
+++ b/src/proto/gui_gtk_x11.pro
@@ -43,6 +43,7 @@
 void gui_mch_set_bg_color(guicolor_T color);
 void gui_mch_set_sp_color(guicolor_T color);
 int gui_gtk2_draw_string(int row, int col, char_u *s, int len, int flags);
+int gui_gtk2_draw_string_ext(int row, int col, char_u *s, int len, int flags, int force_pango);
 int gui_mch_haskey(char_u *name);
 int gui_get_x11_windis(Window *win, Display **dis);
 Display *gui_mch_get_display(void);
diff --git a/src/testdir/test_gui.vim b/src/testdir/test_gui.vim
index 6b849c7..240fda3 100644
--- a/src/testdir/test_gui.vim
+++ b/src/testdir/test_gui.vim
@@ -567,6 +567,31 @@
   endif
 endfunc
 
+func Test_set_guiligatures()
+  let skipped = ''
+
+  if !g:x11_based_gui
+    let skipped = g:not_supported . 'guiligatures'
+  else
+    if has('gui_gtk') || has('gui_gtk2') || has('gui_gnome') || has('gui_gtk3')
+      " Try correct value
+      set guiligatures=<>=ab
+      call assert_equal("<>=ab", &guiligatures)
+      " Try to throw error
+      try
+        set guiligatures=<>=šab
+        call assert_report("'set guiligatures=<>=šab should have failed")
+      catch
+        call assert_exception('E1243:')
+      endtry
+    endif
+  endif
+
+  if !empty(skipped)
+    throw skipped
+  endif
+endfunc
+
 func Test_set_guiheadroom()
   let skipped = ''
 
diff --git a/src/version.c b/src/version.c
index 8048f8e..16ba2cf 100644
--- a/src/version.c
+++ b/src/version.c
@@ -758,6 +758,8 @@
 static int included_patches[] =
 {   /* Add new patch number below this line */
 /**/
+    3524,
+/**/
     3523,
 /**/
     3522,