From 442704b2e5677d471acf72389debf8007760365c Mon Sep 17 00:00:00 2001
From: Andrew Chadwick <a.t.chadwick@gmail.com>
Date: Wed, 26 Oct 2022 22:56:11 +0000
Subject: [PATCH] Better handing of file updates during search

E.g. newly created files in the searched directory will now only be added to the view if they match the search term.

MR !255
---
 thunar/thunar-list-model.c | 212 +++++++++++++++++++++++++++++--------
 1 file changed, 169 insertions(+), 43 deletions(-)

diff --git a/thunar/thunar-list-model.c b/thunar/thunar-list-model.c
index 7c069f38e..ea21f7eab 100644
--- a/thunar/thunar-list-model.c
+++ b/thunar/thunar-list-model.c
@@ -157,6 +157,8 @@ static void               thunar_list_model_files_added                 (ThunarF
 static void               thunar_list_model_files_removed               (ThunarFolder                 *folder,
                                                                          GList                        *files,
                                                                          ThunarListModel              *store);
+static void               thunar_list_model_insert_files                (ThunarListModel              *store,
+                                                                         GList                        *files);
 static gint               sort_by_date                                  (const ThunarFile             *a,
                                                                          const ThunarFile             *b,
                                                                          gboolean                      case_sensitive,
@@ -225,6 +227,15 @@ static void               thunar_list_model_search_folder               (ThunarL
                                                                          enum ThunarListModelSearch    search_type,
                                                                          gboolean                      show_hidden);
 static void               thunar_list_model_cancel_search_job           (ThunarListModel              *model);
+static gchar**            thunar_list_model_split_search_query          (const gchar                  *search_query,
+                                                                         GError                      **error);
+static gboolean           thunar_list_model_search_terms_match          (gchar                       **terms,
+                                                                         gchar                        *str);
+
+static void               thunar_list_model_search_error                (ThunarJob                    *job);
+static void               thunar_list_model_search_finished             (ThunarJob                    *job,
+                                                                         ThunarListModel              *store);
+static gboolean           thunar_list_model_add_search_files            (gpointer user_data);
 
 static gint               thunar_list_model_get_folder_item_count       (ThunarListModel              *store);
 static void               thunar_list_model_set_folder_item_count       (ThunarListModel              *store,
@@ -264,6 +275,12 @@ struct _ThunarListModel
   ThunarDateStyle          date_style;
   char                    *date_custom_style;
 
+  /* Normalized current search terms.
+   * NULL if not presenting a search's results.
+   * Search job may have finished even if this is non-NULL.
+   */
+  gchar **search_terms;
+
   /* Use the shared ThunarFileMonitor instance, so we
    * do not need to connect "changed" handler to every
    * file in the model.
@@ -290,7 +307,7 @@ struct _ThunarListModel
   GList         *files_to_add;
   GMutex         mutex_files_to_add;
 
-  /* used to stop the periodic call to add_search_files when the search is finished/canceled */
+  /* used to stop the periodic call to thunar_list_model_add_search_files when the search is finished/canceled */
   guint          update_search_results_timeout_id;
 };
 
@@ -519,6 +536,8 @@ thunar_list_model_init (ThunarListModel *store)
   store->row_inserted_id = g_signal_lookup ("row-inserted", GTK_TYPE_TREE_MODEL);
   store->row_deleted_id = g_signal_lookup ("row-deleted", GTK_TYPE_TREE_MODEL);
 
+  store->search_terms = NULL;
+
   store->sort_case_sensitive = TRUE;
   store->sort_folders_first = TRUE;
   store->sort_sign = 1;
@@ -571,6 +590,8 @@ thunar_list_model_finalize (GObject *object)
 
   g_free (store->date_custom_style);
 
+  g_strfreev (store->search_terms);
+
   (*G_OBJECT_CLASS (thunar_list_model_parent_class)->finalize) (object);
 }
 
@@ -1522,6 +1543,46 @@ static void
 thunar_list_model_files_added (ThunarFolder    *folder,
                                GList           *files,
                                ThunarListModel *store)
+{
+  GList       *filtered;
+  GList       *lp;
+  ThunarFile  *file;
+  gboolean     matched;
+  gchar       *name_n;
+
+  /* pass the list directly if not currently showing search results */
+  if (store->search_terms == NULL)
+    {
+      thunar_list_model_insert_files (store, files);
+      return;
+    }
+
+  /* otherwise, filter out files that don't match the current search terms */
+  filtered = NULL;
+  for (lp = files; lp != NULL; lp = lp->next)
+    {
+      /* take a reference on that file */
+      file = THUNAR_FILE (g_object_ref (G_OBJECT (lp->data)));
+      _thunar_return_if_fail (THUNAR_IS_FILE (file));
+
+      name_n = (gchar *)thunar_file_get_display_name (file);
+      name_n = thunar_g_utf8_normalize_for_search (name_n, TRUE, TRUE);
+      matched = thunar_list_model_search_terms_match (store->search_terms, name_n);
+      g_free (name_n);
+
+      if (! matched)
+        g_object_unref (file);
+      else
+        filtered = g_list_append (filtered, file);
+    }
+  thunar_list_model_insert_files (store, filtered);
+  thunar_g_list_free_full (filtered);
+}
+
+
+static void
+thunar_list_model_insert_files (ThunarListModel *store,
+                                GList           *files)
 {
   GtkTreePath   *path;
   GtkTreeIter    iter;
@@ -1530,6 +1591,7 @@ thunar_list_model_files_added (ThunarFolder    *folder,
   GSequenceIter *row;
   GList         *lp;
   gboolean       has_handler;
+  gboolean       search_mode;
 
   /* we use a simple trick here to avoid allocating
    * GtkTreePath's again and again, by simply accessing
@@ -1543,16 +1605,23 @@ thunar_list_model_files_added (ThunarFolder    *folder,
   has_handler = g_signal_has_handler_pending (G_OBJECT (store), store->row_inserted_id, 0, FALSE);
 
   /* process all added files */
+  search_mode = (store->search_terms != NULL);
   for (lp = files; lp != NULL; lp = lp->next)
     {
       /* take a reference on that file */
       file = THUNAR_FILE (g_object_ref (G_OBJECT (lp->data)));
       _thunar_return_if_fail (THUNAR_IS_FILE (file));
 
-      /* check if the file should be hidden */
+      /* check if the file should be stashed in the hidden list */
+      /* The ->hidden list is an optimization used by the model when
+       * it is not being used to store search results. In the search
+       * case, we simply restart the search, */
       if (!store->show_hidden && thunar_file_is_hidden (file))
         {
-          store->hidden = g_slist_prepend (store->hidden, file);
+          if (search_mode == FALSE)
+            store->hidden = g_slist_prepend (store->hidden, file);
+          else
+            g_object_unref (file);
         }
       else
         {
@@ -1591,8 +1660,10 @@ thunar_list_model_files_removed (ThunarFolder    *folder,
   GSequenceIter *next;
   GtkTreePath   *path;
   gboolean       found;
+  gboolean       search_mode;
 
   /* drop all the referenced files from the model */
+  search_mode = (store->search_terms != NULL);
   for (lp = files; lp != NULL; lp = lp->next)
     {
       row = g_sequence_get_begin_iter (store->rows);
@@ -1628,10 +1699,14 @@ thunar_list_model_files_removed (ThunarFolder    *folder,
       /* check if the file was found */
       if (!found)
         {
-          /* file is hidden */
-          _thunar_assert (g_slist_find (store->hidden, lp->data) != NULL);
-          store->hidden = g_slist_remove (store->hidden, lp->data);
-          g_object_unref (G_OBJECT (lp->data));
+          if (search_mode == FALSE)
+            {
+              /* file is hidden */
+              /* this only makes sense when not storing search results */
+              _thunar_assert (g_slist_find (store->hidden, lp->data) != NULL);
+              store->hidden = g_slist_remove (store->hidden, lp->data);
+              g_object_unref (G_OBJECT (lp->data));
+            }
         }
     }
 
@@ -2177,13 +2252,13 @@ thunar_list_model_set_job (ThunarListModel  *store,
 
 
 static gboolean
-add_search_files (gpointer user_data)
+thunar_list_model_add_search_files (gpointer user_data)
 {
-  ThunarListModel *model = user_data;
+  ThunarListModel *model = THUNAR_LIST_MODEL (user_data);
 
   g_mutex_lock (&model->mutex_files_to_add);
 
-  thunar_list_model_files_added (model->folder, model->files_to_add, model);
+  thunar_list_model_insert_files (model, model->files_to_add);
   g_list_free (model->files_to_add);
   model->files_to_add = NULL;
 
@@ -2193,6 +2268,61 @@ add_search_files (gpointer user_data)
 }
 
 
+/**
+ * thunar_list_model_split_search_query:
+ * @search_query: The search query to split.
+ * @error: Return location for regex compilation errors.
+ *
+ * Search terms are split on whitespace. Search queries must be
+ * normalized before passing to this function.
+ *
+ * See also: thunar_g_utf8_normalize_for_search().
+ *
+ * Return value: a list of search terms which must be freed with g_strfreev()
+ **/
+
+static gchar **
+thunar_list_model_split_search_query (const gchar  *search_query,
+                                      GError      **error)
+{
+  GRegex *whitespace_regex;
+  gchar **search_terms;
+
+  whitespace_regex = g_regex_new ("\\s+", 0, 0, error);
+  if (whitespace_regex == NULL)
+    return NULL;
+  search_terms = g_regex_split (whitespace_regex, search_query, 0);
+  g_regex_unref (whitespace_regex);
+  return search_terms;
+}
+
+
+
+/**
+ * thunar_list_model_search_terms_match:
+ * @terms: The search terms to look for, prepared with thunar_list_model_split_search_query().
+ * @str: The string which the search terms might be found in.
+ *
+ * All search terms must match. Thunar uses simple substring matching
+ * for the broadest multilingual support. @str must be normalized before
+ * passing to this function.
+ *
+ * See also: thunar_g_utf8_normalize_for_search().
+ *
+ * Return value: TRUE if all terms matched, FALSE otherwise.
+ **/
+
+static gboolean
+thunar_list_model_search_terms_match (gchar **terms,
+                                      gchar  *str)
+{
+  for (gint i = 0; terms[i] != NULL; i++)
+    if (g_strrstr (str, terms[i]) == NULL)
+      return FALSE;
+  return TRUE;
+}
+
+
 
 static gboolean
 _thunar_job_search_directory (ThunarJob  *job,
@@ -2203,7 +2333,6 @@ _thunar_job_search_directory (ThunarJob  *job,
   ThunarFile                 *directory;
   const char                 *search_query_c;
   gchar                     **search_query_c_terms;
-  GRegex                     *whitespace_regex;
   ThunarPreferences          *preferences;
   gboolean                    is_source_device_local;
   ThunarRecursiveSearchMode   mode;
@@ -2228,11 +2357,9 @@ _thunar_job_search_directory (ThunarJob  *job,
   search_query_c = g_value_get_string (&g_array_index (param_values, GValue, 1));
   directory = g_value_get_object (&g_array_index (param_values, GValue, 2));
 
-  whitespace_regex = g_regex_new ("\\s+", 0, 0, error);
-  if (whitespace_regex == NULL)
+  search_query_c_terms = thunar_list_model_split_search_query (search_query_c, error);
+  if (search_query_c_terms == NULL)
     return FALSE;
-  search_query_c_terms = g_regex_split (whitespace_regex, search_query_c, 0);
-  g_regex_unref (whitespace_regex);
 
   is_source_device_local = thunar_g_file_is_on_local_device (thunar_file_get_file (directory));
   if (mode == THUNAR_RECURSIVE_SEARCH_ALWAYS || (mode == THUNAR_RECURSIVE_SEARCH_LOCAL && is_source_device_local))
@@ -2277,7 +2404,7 @@ thunar_list_model_cancel_search_job (ThunarListModel *model)
 
 
 static void
-search_error (ThunarJob *job)
+thunar_list_model_search_error (ThunarJob *job)
 {
   g_error ("Error while searching recursively");
 }
@@ -2285,8 +2412,8 @@ search_error (ThunarJob *job)
 
 
 static void
-search_finished (ThunarJob       *job,
-                 ThunarListModel *store)
+thunar_list_model_search_finished (ThunarJob       *job,
+                                   ThunarListModel *store)
 {
   if (store->recursive_search_job)
     {
@@ -2297,7 +2424,7 @@ search_finished (ThunarJob       *job,
 
   if (store->update_search_results_timeout_id > 0)
     {
-      add_search_files (store);
+      thunar_list_model_add_search_files (store);
       g_source_remove (store->update_search_results_timeout_id);
       store->update_search_results_timeout_id = 0;
     }
@@ -2325,7 +2452,6 @@ thunar_list_model_search_folder (ThunarListModel           *model,
   const gchar     *namespace;
   const gchar     *display_name;
   gchar           *display_name_c; /* converted to ignore case */
-  gboolean         matched;
 
   cancellable = exo_job_get_cancellable (EXO_JOB (job));
   directory = g_file_new_for_uri (uri);
@@ -2395,16 +2521,7 @@ thunar_list_model_search_folder (ThunarListModel           *model,
       display_name_c = thunar_g_utf8_normalize_for_search (display_name, TRUE, TRUE);
 
       /* search for all substrings */
-      matched = TRUE;
-      for (gint i = 0; search_query_c_terms[i]; i++)
-        {
-          if (g_strrstr (display_name_c, search_query_c_terms[i]) == NULL)
-            {
-              matched = FALSE;
-              break;
-            }
-        }
-      if (matched)
+      if (thunar_list_model_search_terms_match (search_query_c_terms, display_name_c))
         files_found = g_list_prepend (files_found, thunar_file_get (file, NULL));
 
       /* free memory */
@@ -2530,34 +2647,43 @@ thunar_list_model_set_folder (ThunarListModel *store,
       /* get the already loaded files or search for files matching the search_query
        * don't start searching if the query is empty, that would be a waste of resources
        */
-      if (search_query == NULL || strlen (search_query) == 0)
+      if (search_query == NULL || strlen (g_strstrip (search_query)) == 0)
         {
           files = thunar_folder_get_files (folder);
+
+          if (store->search_terms != NULL)
+            {
+              g_strfreev (store->search_terms);
+              store->search_terms = NULL;
+            }
         }
       else
         {
           gchar *search_query_c;  /* normalized */
 
           search_query_c = thunar_g_utf8_normalize_for_search (search_query, TRUE, TRUE);
-          files = NULL;
-
-          /* search the current folder
-           * start a new recursive_search_job */
-          store->recursive_search_job = thunar_list_model_job_search_directory (store, search_query_c, thunar_folder_get_corresponding_file (folder));
-          exo_job_launch (EXO_JOB (store->recursive_search_job));
-
-          g_signal_connect (store->recursive_search_job, "error", G_CALLBACK (search_error), NULL);
-          g_signal_connect (store->recursive_search_job, "finished", G_CALLBACK (search_finished), store);
+          g_strfreev (store->search_terms);
+          store->search_terms = thunar_list_model_split_search_query (search_query_c, NULL);
+          if (store->search_terms != NULL)
+            {
+              /* search the current folder
+               * start a new recursive_search_job */
+              store->recursive_search_job = thunar_list_model_job_search_directory (store, search_query_c, thunar_folder_get_corresponding_file (folder));
+              exo_job_launch (EXO_JOB (store->recursive_search_job));
 
-          /* add new results to the model every X ms */
-          store->update_search_results_timeout_id = g_timeout_add (500, add_search_files, store);
+              g_signal_connect (store->recursive_search_job, "error", G_CALLBACK (thunar_list_model_search_error), NULL);
+              g_signal_connect (store->recursive_search_job, "finished", G_CALLBACK (thunar_list_model_search_finished), store);
 
+              /* add new results to the model every X ms */
+              store->update_search_results_timeout_id = g_timeout_add (500, thunar_list_model_add_search_files, store);
+            }
           g_free (search_query_c);
+          files = NULL;
         }
 
       /* insert the files */
       if (files != NULL)
-        thunar_list_model_files_added (folder, files, store);
+        thunar_list_model_insert_files (store, files);
 
       /* connect signals to the new folder */
       g_signal_connect (G_OBJECT (store->folder), "destroy", G_CALLBACK (thunar_list_model_folder_destroy), store);
-- 
GitLab