Add g_desktop_app_info_search()

The first time this function is called we load all of the keyfiles in
the directory, ignoring the 'Hidden' ones and build an index out of the
interesting fields using g_str_tokenize_and_fold().

We do prefix matching on the tokens to find relevent desktop files.

Right now this is implemented as a hashtable that we iterate over,
checking prefixes on each token.  This could possibly be sped up by
creating an array, but it's already pretty fast...

https://bugzilla.gnome.org/show_bug.cgi?id=711557
This commit is contained in:
Ryan Lortie 2013-11-05 22:51:48 -05:00
parent c45b26d61f
commit 3d32d5359a
2 changed files with 527 additions and 0 deletions

View File

@ -150,6 +150,7 @@ typedef struct
GLocalDirectoryMonitor *monitor;
GHashTable *app_names;
gboolean is_setup;
GHashTable *memory_index;
} DesktopFileDir;
static DesktopFileDir *desktop_file_dirs;
@ -245,6 +246,276 @@ add_to_table_if_appropriate (GHashTable *apps,
g_hash_table_insert (apps, g_strdup (info->desktop_id), info);
}
enum
{
DESKTOP_KEY_Comment,
DESKTOP_KEY_GenericName,
DESKTOP_KEY_Keywords,
DESKTOP_KEY_Name,
DESKTOP_KEY_X_GNOME_FullName,
N_DESKTOP_KEYS
};
const gchar desktop_key_match_category[N_DESKTOP_KEYS] = {
/* Note: lower numbers are a better match.
*
* In case we want two keys to match at the same level, we can just
* use the same number for the two different keys.
*/
[DESKTOP_KEY_Name] = 1,
[DESKTOP_KEY_GenericName] = 2,
[DESKTOP_KEY_Keywords] = 3,
[DESKTOP_KEY_X_GNOME_FullName] = 4,
[DESKTOP_KEY_Comment] = 5
};
static gchar *
desktop_key_get_name (guint key_id)
{
switch (key_id)
{
case DESKTOP_KEY_Comment:
return "Comment";
case DESKTOP_KEY_GenericName:
return "GenericName";
case DESKTOP_KEY_Keywords:
return "Keywords";
case DESKTOP_KEY_Name:
return "Name";
case DESKTOP_KEY_X_GNOME_FullName:
return "X-GNOME-FullName";
default:
g_assert_not_reached ();
}
}
/* Search global state {{{2
*
* We only ever search under a global lock, so we can use (and reuse)
* some global data to reduce allocations made while searching.
*
* In short, we keep around arrays of results that we expand as needed
* (and never shrink).
*
* static_token_results: this is where we append the results for each
* token within a given desktop directory, as we handle it (which is
* a union of all matches for this term)
*
* static_search_results: this is where we build the complete results
* for a single directory (which is an intersection of the matches
* found for each term)
*
* static_total_results: this is where we build the complete results
* across all directories (which is a union of the matches found in
* each directory)
*
* The app_names that enter these tables are always pointer-unique (in
* the sense that string equality is the same as pointer equality).
* This can be guaranteed for two reasons:
*
* - we mask appids so that a given appid will only ever appear within
* the highest-precedence directory that contains it. We never
* return search results from a lower-level directory if a desktop
* file exists in a higher-level one.
*
* - within a given directory, the string is unique because it's the
* key in the hashtable of all app_ids for that directory.
*
* We perform a merging of the results in merge_token_results(). This
* works by ordering the two lists and moving through each of them (at
* the same time) looking for common elements, rejecting uncommon ones.
* "Order" here need not mean any particular thing, as long as it is
* some order. Because of the uniqueness of our strings, we can use
* pointer order. That's what's going on in compare_results() below.
*/
struct search_result
{
const gchar *app_name;
gint category;
};
static struct search_result *static_token_results;
static gint static_token_results_size;
static gint static_token_results_allocated;
static struct search_result *static_search_results;
static gint static_search_results_size;
static gint static_search_results_allocated;
static struct search_result *static_total_results;
static gint static_total_results_size;
static gint static_total_results_allocated;
/* And some functions for performing nice operations against it */
static gint
compare_results (gconstpointer a,
gconstpointer b)
{
const struct search_result *ra = a;
const struct search_result *rb = b;
if (ra->app_name < rb->app_name)
return -1;
else if (ra->app_name > rb->app_name)
return 1;
else
return ra->category - rb->category;
}
static gint
compare_categories (gconstpointer a,
gconstpointer b)
{
const struct search_result *ra = a;
const struct search_result *rb = b;
return ra->category - rb->category;
}
static void
add_token_result (const gchar *app_name,
guint16 category)
{
if G_UNLIKELY (static_token_results_size == static_token_results_allocated)
{
static_token_results_allocated = MAX (16, static_token_results_allocated * 2);
static_token_results = g_renew (struct search_result, static_token_results, static_token_results_allocated);
}
static_token_results[static_token_results_size].app_name = app_name;
static_token_results[static_token_results_size].category = category;
static_token_results_size++;
}
static void
merge_token_results (gboolean first)
{
qsort (static_token_results, static_token_results_size, sizeof (struct search_result), compare_results);
/* If this is the first token then we are basically merging a list with
* itself -- we only perform de-duplication.
*
* If this is not the first token then we are doing a real merge.
*/
if (first)
{
const gchar *last_name = NULL;
gint i;
/* We must de-duplicate, but we do so by taking the best category
* in each case.
*
* The final list can be as large as the input here, so make sure
* we have enough room (even if it's too much room).
*/
if G_UNLIKELY (static_search_results_allocated < static_token_results_size)
{
static_search_results_allocated = static_token_results_allocated;
static_search_results = g_renew (struct search_result,
static_search_results,
static_search_results_allocated);
}
for (i = 0; i < static_token_results_size; i++)
{
/* The list is sorted so that the best match for a given id
* will be at the front, so once we have copied an id, skip
* the rest of the entries for the same id.
*/
if (static_token_results[i].app_name == last_name)
continue;
last_name = static_token_results[i].app_name;
static_search_results[static_search_results_size++] = static_token_results[i];
}
}
else
{
const gchar *last_name = NULL;
gint i, j = 0;
gint k = 0;
/* We only ever remove items from the results list, so no need to
* resize to ensure that we have enough room.
*/
for (i = 0; i < static_token_results_size; i++)
{
if (static_token_results[i].app_name == last_name)
continue;
last_name = static_token_results[i].app_name;
/* Now we only want to have a result in static_search_results
* if we already have it there *and* we have it in
* static_token_results as well. The category will be the
* lesser of the two.
*
* Skip past the results in static_search_results that are not
* going to be matches.
*/
while (k < static_search_results_size &&
static_search_results[k].app_name < static_token_results[i].app_name)
k++;
if (k < static_search_results_size &&
static_search_results[k].app_name == static_token_results[i].app_name)
{
/* We have a match.
*
* Category should be the worse of the two (ie:
* numerically larger).
*/
static_search_results[j].app_name = static_search_results[k].app_name;
static_search_results[j].category = MAX (static_search_results[k].category,
static_token_results[i].category);
j++;
}
}
static_search_results_size = j;
}
/* Clear it out for next time... */
static_token_results_size = 0;
}
static void
reset_total_search_results (void)
{
static_total_results_size = 0;
}
static void
sort_total_search_results (void)
{
qsort (static_total_results, static_total_results_size, sizeof (struct search_result), compare_categories);
}
static void
merge_directory_results (void)
{
if G_UNLIKELY (static_total_results_size + static_search_results_size > static_total_results_allocated)
{
static_total_results_allocated = MAX (16, static_total_results_allocated);
while (static_total_results_allocated < static_total_results_size + static_search_results_size)
static_total_results_allocated *= 2;
static_total_results = g_renew (struct search_result, static_total_results, static_total_results_allocated);
}
memcpy (static_total_results + static_total_results_size,
static_search_results,
static_search_results_size * sizeof (struct search_result));
static_total_results_size += static_search_results_size;
/* Clear it out for next time... */
static_search_results_size = 0;
}
/* Support for unindexed DesktopFileDirs {{{2 */
static void
get_apps_from_dir (GHashTable **apps,
@ -332,6 +603,157 @@ desktop_file_dir_unindexed_get_all (DesktopFileDir *dir,
}
}
typedef struct _MemoryIndexEntry MemoryIndexEntry;
typedef GHashTable MemoryIndex;
struct _MemoryIndexEntry
{
const gchar *app_name; /* pointer to the hashtable key */
gint match_category;
MemoryIndexEntry *next;
};
static void
memory_index_entry_free (gpointer data)
{
MemoryIndexEntry *mie = data;
while (mie)
{
MemoryIndexEntry *next = mie->next;
g_slice_free (MemoryIndexEntry, mie);
mie = next;
}
}
static void
memory_index_add_token (MemoryIndex *mi,
const gchar *token,
gint match_category,
const gchar *app_name)
{
MemoryIndexEntry *mie, *first;
mie = g_slice_new (MemoryIndexEntry);
mie->app_name = app_name;
mie->match_category = match_category;
first = g_hash_table_lookup (mi, token);
if (first)
{
mie->next = first->next;
first->next = mie;
}
else
{
mie->next = NULL;
g_hash_table_insert (mi, g_strdup (token), mie);
}
}
static void
memory_index_add_string (MemoryIndex *mi,
const gchar *string,
gint match_category,
const gchar *app_name)
{
gchar **tokens, **alternates;
gint i;
tokens = g_str_tokenize_and_fold (string, NULL, &alternates);
for (i = 0; tokens[i]; i++)
memory_index_add_token (mi, tokens[i], match_category, app_name);
for (i = 0; alternates[i]; i++)
memory_index_add_token (mi, alternates[i], match_category, app_name);
g_strfreev (alternates);
g_strfreev (tokens);
}
static MemoryIndex *
memory_index_new (void)
{
return g_hash_table_new_full (g_str_hash, g_str_equal, g_free, memory_index_entry_free);
}
static void
desktop_file_dir_unindexed_setup_search (DesktopFileDir *dir)
{
GHashTableIter iter;
gpointer app, path;
dir->memory_index = memory_index_new ();
/* Nothing to search? */
if (dir->app_names == NULL)
return;
g_hash_table_iter_init (&iter, dir->app_names);
while (g_hash_table_iter_next (&iter, &app, &path))
{
GKeyFile *key_file;
if (desktop_file_dir_app_name_is_masked (dir, app))
continue;
key_file = g_key_file_new ();
if (g_key_file_load_from_file (key_file, path, G_KEY_FILE_NONE, NULL) &&
!g_key_file_get_boolean (key_file, "Desktop Entry", "Hidden", NULL))
{
/* Index the interesting keys... */
gint i;
for (i = 0; i < G_N_ELEMENTS (desktop_key_match_category); i++)
{
gchar *value;
if (!desktop_key_match_category[i])
continue;
value = g_key_file_get_locale_string (key_file, "Desktop Entry", desktop_key_get_name (i), NULL, NULL);
if (value)
memory_index_add_string (dir->memory_index, value, desktop_key_match_category[i], app);
g_free (value);
}
}
g_key_file_free (key_file);
}
}
static void
desktop_file_dir_unindexed_search (DesktopFileDir *dir,
const gchar *search_token)
{
GHashTableIter iter;
gpointer key, value;
if (!dir->memory_index)
desktop_file_dir_unindexed_setup_search (dir);
g_hash_table_iter_init (&iter, dir->memory_index);
while (g_hash_table_iter_next (&iter, &key, &value))
{
MemoryIndexEntry *mie = value;
if (!g_str_has_prefix (key, search_token))
continue;
while (mie)
{
add_token_result (mie->app_name, mie->match_category);
mie = mie->next;
}
}
}
/* DesktopFileDir "API" {{{2 */
/*< internal >
@ -375,6 +797,12 @@ desktop_file_dir_reset (DesktopFileDir *dir)
dir->app_names = NULL;
}
if (dir->memory_index)
{
g_hash_table_unref (dir->memory_index);
dir->memory_index = NULL;
}
dir->is_setup = FALSE;
}
@ -442,6 +870,20 @@ desktop_file_dir_get_all (DesktopFileDir *dir,
desktop_file_dir_unindexed_get_all (dir, apps);
}
/*< internal >
* desktop_file_dir_search:
* @dir: a #DesktopFilEDir
* @term: a normalised and casefolded search term
*
* Finds the names of applications in @dir that match @term.
*/
static void
desktop_file_dir_search (DesktopFileDir *dir,
const gchar *search_token)
{
desktop_file_dir_unindexed_search (dir, search_token);
}
/* Lock/unlock and global setup API {{{2 */
static void
@ -3054,6 +3496,89 @@ g_app_info_get_default_for_uri_scheme (const char *uri_scheme)
/* "Get all" API {{{2 */
/**
* g_desktop_app_info_search:
* @search_string: the search string to use
*
* Searches desktop files for ones that match @search_string.
*
* The return value is an array of strvs. Each strv contains a list of
* applications that matched @search_string with an equal score. The
* outer list is sorted by score so that the first strv contains the
* best-matching applications, and so on.
*
* Returns: (array zero-terminated=1) (element-type GStrv) (transfer full): a
* list of strvs. Free each item with g_strfreev() and free the outer
* list with g_free().
*/
gchar ***
g_desktop_app_info_search (const gchar *search_string)
{
gchar **search_tokens;
gint last_category = -1;
gchar ***results;
gint n_categories = 0;
gint start_of_category;
gint i, j;
search_tokens = g_str_tokenize_and_fold (search_string, NULL, NULL);
desktop_file_dirs_lock ();
reset_total_search_results ();
for (i = 0; i < n_desktop_file_dirs; i++)
{
for (j = 0; search_tokens[j]; j++)
{
desktop_file_dir_search (&desktop_file_dirs[i], search_tokens[j]);
merge_token_results (j == 0);
}
merge_directory_results ();
}
sort_total_search_results ();
/* Count the total number of unique categories */
for (i = 0; i < static_total_results_size; i++)
if (static_total_results[i].category != last_category)
{
last_category = static_total_results[i].category;
n_categories++;
}
results = g_new (gchar **, n_categories + 1);
/* Start loading into the results list */
start_of_category = 0;
for (i = 0; i < n_categories; i++)
{
gint n_items_in_category = 0;
gint this_category;
gint j;
this_category = static_total_results[start_of_category].category;
while (start_of_category + n_items_in_category < static_total_results_size &&
static_total_results[start_of_category + n_items_in_category].category == this_category)
n_items_in_category++;
results[i] = g_new (gchar *, n_items_in_category + 1);
for (j = 0; j < n_items_in_category; j++)
results[i][j] = g_strdup (static_total_results[start_of_category + j].app_name);
results[i][j] = NULL;
start_of_category += n_items_in_category;
}
results[i] = NULL;
desktop_file_dirs_unlock ();
g_strfreev (search_tokens);
return results;
}
/**
* g_app_info_get_all:
*

View File

@ -164,6 +164,8 @@ gboolean g_desktop_app_info_launch_uris_as_manager (GDesktopAppInfo
gpointer pid_callback_data,
GError **error);
GLIB_AVAILABLE_IN_2_40
gchar *** g_desktop_app_info_search (const gchar *search_string);
G_END_DECLS