Merge branch 'search-app-prefix-strstr' into 'main'

gdesktopappinfo: Group search results by both categories and match types

See merge request GNOME/glib!3107
This commit is contained in:
Philip Withnall 2022-12-21 11:42:35 +00:00
commit 9e2ad88455
2 changed files with 88 additions and 33 deletions

View File

@ -456,6 +456,14 @@ const gchar desktop_key_match_category[N_DESKTOP_KEYS] = {
[DESKTOP_KEY_Comment] = 6 [DESKTOP_KEY_Comment] = 6
}; };
typedef enum {
/* Lower numbers have higher priority.
* Prefix match should put before substring match.
*/
MATCH_TYPE_PREFIX = 1,
MATCH_TYPE_SUBSTRING = 2
} MatchType;
/* Common prefix commands to ignore from Exec= lines */ /* Common prefix commands to ignore from Exec= lines */
const char * const exec_key_match_blocklist[] = { const char * const exec_key_match_blocklist[] = {
"bash", "bash",
@ -537,6 +545,7 @@ struct search_result
{ {
const gchar *app_name; const gchar *app_name;
gint category; gint category;
gint match_type;
}; };
static struct search_result *static_token_results; static struct search_result *static_token_results;
@ -558,13 +567,20 @@ compare_results (gconstpointer a,
const struct search_result *rb = b; const struct search_result *rb = b;
if (ra->app_name < rb->app_name) if (ra->app_name < rb->app_name)
{
return -1; return -1;
}
else if (ra->app_name > rb->app_name) else if (ra->app_name > rb->app_name)
{
return 1; return 1;
}
else else
{
if (ra->category == rb->category)
return ra->match_type - rb->match_type;
return ra->category - rb->category; return ra->category - rb->category;
}
} }
static gint static gint
@ -574,12 +590,19 @@ compare_categories (gconstpointer a,
const struct search_result *ra = a; const struct search_result *ra = a;
const struct search_result *rb = b; const struct search_result *rb = b;
/* Also compare match types so we can put prefix match in a group while
* substring match in another group.
*/
if (ra->category == rb->category)
return ra->match_type - rb->match_type;
return ra->category - rb->category; return ra->category - rb->category;
} }
static void static void
add_token_result (const gchar *app_name, add_token_result (const gchar *app_name,
guint16 category) guint16 category,
guint16 match_type)
{ {
if G_UNLIKELY (static_token_results_size == static_token_results_allocated) if G_UNLIKELY (static_token_results_size == static_token_results_allocated)
{ {
@ -589,6 +612,7 @@ add_token_result (const gchar *app_name,
static_token_results[static_token_results_size].app_name = app_name; static_token_results[static_token_results_size].app_name = app_name;
static_token_results[static_token_results_size].category = category; static_token_results[static_token_results_size].category = category;
static_token_results[static_token_results_size].match_type = match_type;
static_token_results_size++; static_token_results_size++;
} }
@ -672,10 +696,22 @@ merge_token_results (gboolean first)
* *
* Category should be the worse of the two (ie: * Category should be the worse of the two (ie:
* numerically larger). * numerically larger).
*
* Match type should also be the worse, so if an app has two
* prefix matches it will has higher priority than one prefix
* matches and one substring matches, for example, LibreOffice
* Writer should be higher priority than LibreOffice Draw with
* `lib w`.
*
* (This ignores the difference between partly prefix matches and
* all substring matches, however most time we just focus on exact
* prefix matches, who cares the 10th-20th search results?)
*/ */
static_search_results[j].app_name = static_search_results[k].app_name; static_search_results[j].app_name = static_search_results[k].app_name;
static_search_results[j].category = MAX (static_search_results[k].category, static_search_results[j].category = MAX (static_search_results[k].category,
static_token_results[i].category); static_token_results[i].category);
static_search_results[j].match_type = MAX (static_search_results[k].match_type,
static_token_results[i].match_type);
j++; j++;
} }
} }
@ -1224,13 +1260,24 @@ desktop_file_dir_unindexed_search (DesktopFileDir *dir,
while (g_hash_table_iter_next (&iter, &key, &value)) while (g_hash_table_iter_next (&iter, &key, &value))
{ {
MemoryIndexEntry *mie = value; MemoryIndexEntry *mie = value;
const char *p;
MatchType match_type;
if (strstr (key, search_token) == NULL) /* strstr(haystack, needle) returns haystack if needle is empty, so if
* needle is not empty and return value equals to haystack means a prefix
* match.
*/
p = strstr (key, search_token);
if (p == NULL)
continue; continue;
else if (p == key && search_token != NULL && *search_token != '\0')
match_type = MATCH_TYPE_PREFIX;
else
match_type = MATCH_TYPE_SUBSTRING;
while (mie) while (mie)
{ {
add_token_result (mie->app_name, mie->match_category); add_token_result (mie->app_name, mie->match_category, match_type);
mie = mie->next; mie = mie->next;
} }
} }
@ -4776,9 +4823,10 @@ g_desktop_app_info_search (const gchar *search_string)
{ {
gchar **search_tokens; gchar **search_tokens;
gint last_category = -1; gint last_category = -1;
gint last_match_type = -1;
gchar ***results; gchar ***results;
gint n_categories = 0; gint n_groups = 0;
gint start_of_category; gint start_of_group;
gint i, j; gint i, j;
guint k; guint k;
@ -4800,36 +4848,41 @@ g_desktop_app_info_search (const gchar *search_string)
sort_total_search_results (); sort_total_search_results ();
/* Count the total number of unique categories */ /* Count the total number of unique categories and match types */
for (i = 0; i < static_total_results_size; i++) for (i = 0; i < static_total_results_size; i++)
if (static_total_results[i].category != last_category) if (static_total_results[i].category != last_category ||
static_total_results[i].match_type != last_match_type)
{ {
last_category = static_total_results[i].category; last_category = static_total_results[i].category;
n_categories++; last_match_type = static_total_results[i].match_type;
n_groups++;
} }
results = g_new (gchar **, n_categories + 1); results = g_new (gchar **, n_groups + 1);
/* Start loading into the results list */ /* Start loading into the results list */
start_of_category = 0; start_of_group = 0;
for (i = 0; i < n_categories; i++) for (i = 0; i < n_groups; i++)
{ {
gint n_items_in_category = 0; gint n_items_in_group = 0;
gint this_category; gint this_category;
gint this_match_type;
gint j; gint j;
this_category = static_total_results[start_of_category].category; this_category = static_total_results[start_of_group].category;
this_match_type = static_total_results[start_of_group].match_type;
while (start_of_category + n_items_in_category < static_total_results_size && while (start_of_group + n_items_in_group < static_total_results_size &&
static_total_results[start_of_category + n_items_in_category].category == this_category) static_total_results[start_of_group + n_items_in_group].category == this_category &&
n_items_in_category++; static_total_results[start_of_group + n_items_in_group].match_type == this_match_type)
n_items_in_group++;
results[i] = g_new (gchar *, n_items_in_category + 1); results[i] = g_new (gchar *, n_items_in_group + 1);
for (j = 0; j < n_items_in_category; j++) for (j = 0; j < n_items_in_group; j++)
results[i][j] = g_strdup (static_total_results[start_of_category + j].app_name); results[i][j] = g_strdup (static_total_results[start_of_group + j].app_name);
results[i][j] = NULL; results[i][j] = NULL;
start_of_category += n_items_in_category; start_of_group += n_items_in_group;
} }
results[i] = NULL; results[i] = NULL;

View File

@ -867,11 +867,12 @@ test_search (void)
assert_search ("image viewer", "", TRUE, TRUE, NULL, NULL); assert_search ("image viewer", "", TRUE, TRUE, NULL, NULL);
/* There're "flatpak" apps (clocks) installed as well - they should *not* /* There're "flatpak" apps (clocks) installed as well - they should *not*
* match the prefix command ("/bin/sh") in the Exec= line though. However, * match the prefix command ("/bin/sh") in the Exec= line though. Then with
* with substring matching, Image Viewer (eog) should be in result list * substring matching, Image Viewer (eog) should be in next group because it
* because it contains "Slideshow" in its keywords. * contains "Slideshow" in its keywords.
*/ */
assert_search ("sh", "eog.desktop gnome-terminal.desktop\n", TRUE, FALSE, NULL, NULL); assert_search ("sh", "gnome-terminal.desktop\n"
"eog.desktop\n", TRUE, FALSE, NULL, NULL);
/* "frobnicator.desktop" is ignored by get_all() because the binary is /* "frobnicator.desktop" is ignored by get_all() because the binary is
* missing, but search should still find it (to avoid either stale results * missing, but search should still find it (to avoid either stale results
@ -886,11 +887,12 @@ test_search (void)
assert_search ("files file fil fi f", "nautilus.desktop\n" assert_search ("files file fil fi f", "nautilus.desktop\n"
"gedit.desktop\n", TRUE, TRUE, NULL, NULL); "gedit.desktop\n", TRUE, TRUE, NULL, NULL);
/* With substring match, "con" match dconf, contacts, nautilus-classic (which /* "con" will match "connect" and "contacts" on name with prefix match in
* has a name called "Desktop Icons"), nautilus-connect-server (which has a * first group, then match "Dconf Editor" and "Desktop Icons" with substring
* name called "Connect to Server"). * match in next group.
*/ */
assert_search ("con", "dconf-editor.desktop gnome-contacts.desktop nautilus-classic.desktop nautilus-connect-server.desktop\n", TRUE, TRUE, NULL, NULL); assert_search ("con", "gnome-contacts.desktop nautilus-connect-server.desktop\n"
"dconf-editor.desktop nautilus-classic.desktop\n", TRUE, TRUE, NULL, NULL);
/* "gnome" will match "eye of gnome" from the user's directory, plus /* "gnome" will match "eye of gnome" from the user's directory, plus
* matching "GNOME Clocks" X-GNOME-FullName. It's only a comment on * matching "GNOME Clocks" X-GNOME-FullName. It's only a comment on