patch 9.1.0463: no fuzzy-matching support for insert-completion

Problem:  no fuzzy-matching support for insert-completion
Solution: enable insert-mode completion with fuzzy-matching
          using :set completopt+=fuzzy (glepnir).

closes: #14878

Signed-off-by: glepnir <glephunter@gmail.com>
Signed-off-by: Christian Brabandt <cb@256bit.org>
diff --git a/src/insexpand.c b/src/insexpand.c
index 897c3b5..c6bf681 100644
--- a/src/insexpand.c
+++ b/src/insexpand.c
@@ -113,6 +113,7 @@
 				// cp_flags has CP_FREE_FNAME
     int		cp_flags;	// CP_ values
     int		cp_number;	// sequence number
+    int		cp_score;	// fuzzy match score
 };
 
 // values for cp_flags
@@ -153,6 +154,7 @@
 						// TRUE: noselect
 static int	  compl_longest = FALSE;	// FALSE: insert full match
 						// TRUE: insert longest prefix
+static int	  compl_fuzzy_match = FALSE;	// True: fuzzy match enabled
 
 // Selected one of the matches.  When FALSE the match was edited or using the
 // longest common string.
@@ -207,6 +209,8 @@
 static int	  compl_opt_refresh_always = FALSE;
 static int	  compl_opt_suppress_empty = FALSE;
 
+static int	  compl_selected_item = -1;
+
 static int ins_compl_add(char_u *str, int len, char_u *fname, char_u **cptext, typval_T *user_data, int cdir, int flags, int adup);
 static void ins_compl_longest_match(compl_T *match);
 static void ins_compl_del_pum(void);
@@ -1059,12 +1063,15 @@
     compl_no_insert = FALSE;
     compl_no_select = FALSE;
     compl_longest = FALSE;
+    compl_fuzzy_match = FALSE;
     if (strstr((char *)p_cot, "noselect") != NULL)
 	compl_no_select = TRUE;
     if (strstr((char *)p_cot, "noinsert") != NULL)
 	compl_no_insert = TRUE;
     if (strstr((char *)p_cot, "longest") != NULL)
 	compl_longest = TRUE;
+    if (strstr((char *)p_cot, "fuzzy") != NULL)
+	compl_fuzzy_match = TRUE;
 }
 
 
@@ -1213,6 +1220,17 @@
 #endif
 
 /*
+ * pumitem qsort compare func
+ */
+    static int
+ins_compl_fuzzy_sort(const void *a, const void *b)
+{
+    const int sa = (*(pumitem_T *)a).pum_score;
+    const int sb = (*(pumitem_T *)b).pum_score;
+    return sa == sb ? 0 : sa < sb ? 1 : -1;
+}
+
+/*
  * Build a popup menu to show the completion matches.
  * Returns the popup menu entry that should be selected. Returns -1 if nothing
  * should be selected.
@@ -1227,6 +1245,7 @@
     int		i;
     int		cur = -1;
     int		lead_len = 0;
+    int		max_fuzzy_score = 0;
 
     // Need to build the popup menu list.
     compl_match_arraysize = 0;
@@ -1236,9 +1255,15 @@
 
     do
     {
+	// when completeopt include fuzzy option and leader is not null or empty
+	// set the cp_score for after compare.
+	if (compl_fuzzy_match && compl_leader != NULL && lead_len > 0)
+	    compl->cp_score = fuzzy_match_str(compl->cp_str, compl_leader);
+
 	if (!match_at_original_text(compl)
 		&& (compl_leader == NULL
-		    || ins_compl_equal(compl, compl_leader, lead_len)))
+		    || ins_compl_equal(compl, compl_leader, lead_len)
+		    || (compl_fuzzy_match && compl->cp_score > 0)))
 	    ++compl_match_arraysize;
 	compl = compl->cp_next;
     } while (compl != NULL && !is_first_match(compl));
@@ -1267,9 +1292,10 @@
     {
 	if (!match_at_original_text(compl)
 		&& (compl_leader == NULL
-		    || ins_compl_equal(compl, compl_leader, lead_len)))
+		    || ins_compl_equal(compl, compl_leader, lead_len)
+		    || (compl_fuzzy_match && compl->cp_score > 0)))
 	{
-	    if (!shown_match_ok)
+	    if (!shown_match_ok && !compl_fuzzy_match)
 	    {
 		if (compl == compl_shown_match || did_find_shown_match)
 		{
@@ -1285,6 +1311,24 @@
 		    shown_compl = compl;
 		cur = i;
 	    }
+	    else if (compl_fuzzy_match)
+	    {
+		if (compl->cp_score > max_fuzzy_score)
+		{
+		    did_find_shown_match = TRUE;
+		    max_fuzzy_score = compl->cp_score;
+		    compl_shown_match = compl;
+		    shown_match_ok = TRUE;
+		}
+
+		if (!compl_no_select
+			&& (max_fuzzy_score > 0
+				|| (compl_leader == NULL || lead_len == 0)))
+		{
+		    shown_match_ok = TRUE;
+		    cur = 0;
+		}
+	    }
 
 	    if (compl->cp_text[CPT_ABBR] != NULL)
 		compl_match_array[i].pum_text =
@@ -1293,6 +1337,7 @@
 		compl_match_array[i].pum_text = compl->cp_str;
 	    compl_match_array[i].pum_kind = compl->cp_text[CPT_KIND];
 	    compl_match_array[i].pum_info = compl->cp_text[CPT_INFO];
+	    compl_match_array[i].pum_score = compl->cp_score;
 	    if (compl->cp_text[CPT_MENU] != NULL)
 		compl_match_array[i++].pum_extra =
 		    compl->cp_text[CPT_MENU];
@@ -1300,7 +1345,7 @@
 		compl_match_array[i++].pum_extra = compl->cp_fname;
 	}
 
-	if (compl == compl_shown_match)
+	if (compl == compl_shown_match && !compl_fuzzy_match)
 	{
 	    did_find_shown_match = TRUE;
 
@@ -1320,6 +1365,10 @@
 	compl = compl->cp_next;
     } while (compl != NULL && !is_first_match(compl));
 
+    if (compl_fuzzy_match && compl_leader != NULL && lead_len > 0)
+	// sort by the largest score of fuzzy match
+	qsort(compl_match_array, (size_t)compl_match_arraysize, sizeof(pumitem_T), ins_compl_fuzzy_sort);
+
     if (!shown_match_ok)    // no displayed match at all
 	cur = -1;
 
@@ -1376,6 +1425,7 @@
     // Use the cursor to get all wrapping and other settings right.
     col = curwin->w_cursor.col;
     curwin->w_cursor.col = compl_col;
+    compl_selected_item = cur;
     pum_display(compl_match_array, compl_match_arraysize, cur);
     curwin->w_cursor.col = col;
 
@@ -4025,6 +4075,40 @@
     redraw_cmdline = FALSE;	    // don't overwrite!
 }
 
+    static compl_T *
+find_comp_when_fuzzy(void)
+{
+    int		score;
+    char_u*	str;
+    int		target_idx = -1;
+    int		is_forward = compl_shows_dir_forward();
+    int		is_backward = compl_shows_dir_backward();
+    compl_T	*comp = NULL;
+
+    if (compl_match_array == NULL ||
+	    (is_forward && compl_selected_item == compl_match_arraysize - 1)
+	    || (is_backward && compl_selected_item == 0))
+	return compl_first_match;
+
+    if (is_forward)
+	target_idx = compl_selected_item + 1;
+    else if (is_backward)
+      target_idx = compl_selected_item == -1 ? compl_match_arraysize - 1
+					    : compl_selected_item - 1;
+
+    score = compl_match_array[target_idx].pum_score;
+    str = compl_match_array[target_idx].pum_text;
+
+    comp = compl_first_match;
+    do {
+      if (comp->cp_score == score && (str == comp->cp_str || str == comp->cp_text[CPT_ABBR]))
+	  return comp;
+      comp = comp->cp_next;
+    } while (comp != NULL && !is_first_match(comp));
+
+    return NULL;
+}
+
 /*
  * Find the next set of matches for completion. Repeat the completion "todo"
  * times.  The number of matches found is returned in 'num_matches'.
@@ -4052,7 +4136,8 @@
     {
 	if (compl_shows_dir_forward() && compl_shown_match->cp_next != NULL)
 	{
-	    compl_shown_match = compl_shown_match->cp_next;
+	    compl_shown_match = !compl_fuzzy_match ? compl_shown_match->cp_next
+						: find_comp_when_fuzzy();
 	    found_end = (compl_first_match != NULL
 		    && (is_first_match(compl_shown_match->cp_next)
 			|| is_first_match(compl_shown_match)));
@@ -4061,7 +4146,8 @@
 		&& compl_shown_match->cp_prev != NULL)
 	{
 	    found_end = is_first_match(compl_shown_match);
-	    compl_shown_match = compl_shown_match->cp_prev;
+	    compl_shown_match = !compl_fuzzy_match ? compl_shown_match->cp_prev
+						   : find_comp_when_fuzzy();
 	    found_end |= is_first_match(compl_shown_match);
 	}
 	else
@@ -4111,7 +4197,8 @@
 	if (!match_at_original_text(compl_shown_match)
 		&& compl_leader != NULL
 		&& !ins_compl_equal(compl_shown_match,
-		    compl_leader, (int)STRLEN(compl_leader)))
+		    compl_leader, (int)STRLEN(compl_leader))
+		&& !(compl_fuzzy_match && compl_shown_match->cp_score > 0))
 	    ++todo;
 	else
 	    // Remember a matching item.
@@ -4167,7 +4254,9 @@
     if (compl_shown_match == NULL)
 	return -1;
 
-    if (compl_leader != NULL && !match_at_original_text(compl_shown_match))
+    if (compl_leader != NULL
+	    && !match_at_original_text(compl_shown_match)
+	    && !compl_fuzzy_match)
 	// Update "compl_shown_match" to the actually shown match
 	ins_compl_update_shown_match();