diff --git a/src/client/geary-controller.vala b/src/client/geary-controller.vala index 379e0eeb..4e6cc51f 100644 --- a/src/client/geary-controller.vala +++ b/src/client/geary-controller.vala @@ -1675,12 +1675,23 @@ public class GearyController { } private void do_search(string search_text) { + Geary.SearchFolder? folder = null; + try { + folder = (Geary.SearchFolder) current_account.get_special_folder( + Geary.SpecialFolderType.SEARCH); + } catch (Error e) { + debug("Could not get search folder: %s", e.message); + + return; + } + if (search_text == "") { if (previous_non_search_folder != null && current_folder is Geary.SearchFolder) main_window.folder_list.select_folder(previous_non_search_folder); main_window.folder_list.remove_search(); search_text_changed(""); + folder.clear(); return; } @@ -1690,16 +1701,7 @@ public class GearyController { cancel_search(); // Stop any search in progress. - Geary.SearchFolder? folder; - try { - folder = (Geary.SearchFolder) current_account.get_special_folder( - Geary.SpecialFolderType.SEARCH); - folder.set_search_keywords(search_text, cancellable_search); - } catch (Error e) { - debug("Could not get search folder: %s", e.message); - - return; - } + folder.set_search_query(search_text, cancellable_search); main_window.folder_list.set_search(folder); search_text_changed(main_window.main_toolbar.search_text); diff --git a/src/client/views/conversation-viewer.vala b/src/client/views/conversation-viewer.vala index 736e62cc..1fa93cc9 100644 --- a/src/client/views/conversation-viewer.vala +++ b/src/client/views/conversation-viewer.vala @@ -286,8 +286,9 @@ public class ConversationViewer : Gtk.Box { } } - private void on_search_text_changed() { - highlight_search_terms.begin(); + private void on_search_text_changed(string? query) { + if (query != null) + highlight_search_terms.begin(); } private async void highlight_search_terms() { @@ -304,7 +305,7 @@ public class ConversationViewer : Gtk.Box { try { // Request a list of search terms. - Gee.Collection? search_keywords = yield search_folder.get_search_keywords_async( + Gee.Collection? search_keywords = yield search_folder.get_search_matches_async( ids, cancellable_fetch); // Highlight the search terms. @@ -1713,7 +1714,7 @@ public class ConversationViewer : Gtk.Box { web_view.unmark_text_matches(); if (search_folder != null) { - search_folder.search_keywords_changed.disconnect(on_search_text_changed); + search_folder.search_query_changed.disconnect(on_search_text_changed); search_folder = null; } @@ -1751,7 +1752,7 @@ public class ConversationViewer : Gtk.Box { private uint on_enter_search_folder(uint state, uint event, void *user, Object? object) { search_folder = current_folder as Geary.SearchFolder; assert(search_folder != null); - search_folder.search_keywords_changed.connect(on_search_text_changed); + search_folder.search_query_changed.connect(on_search_text_changed); return SearchState.SEARCH_FOLDER; } diff --git a/src/engine/abstract/geary-abstract-account.vala b/src/engine/abstract/geary-abstract-account.vala index af2a2fc3..3e0f5b0e 100644 --- a/src/engine/abstract/geary-abstract-account.vala +++ b/src/engine/abstract/geary-abstract-account.vala @@ -97,16 +97,16 @@ public abstract class Geary.AbstractAccount : BaseObject, Geary.Account { public abstract async Geary.Email local_fetch_email_async(Geary.EmailIdentifier email_id, Geary.Email.Field required_fields, Cancellable? cancellable = null) throws Error; - public abstract async Geary.EmailIdentifier? folder_email_id_to_search( + public abstract async Geary.EmailIdentifier? folder_email_id_to_search_async( Geary.FolderPath folder_path, Geary.EmailIdentifier id, Geary.FolderPath? return_folder_path, Cancellable? cancellable = null) throws Error; - public abstract async Gee.Collection? local_search_async(string keywords, + public abstract async Gee.Collection? local_search_async(string query, Geary.Email.Field requested_fields, bool partial_ok, Geary.FolderPath? email_id_folder_path, int limit = 100, int offset = 0, Gee.Collection? folder_blacklist = null, Gee.Collection? search_ids = null, Cancellable? cancellable = null) throws Error; - public abstract async Gee.Collection? get_search_keywords_async( + public abstract async Gee.Collection? get_search_matches_async( Gee.Collection ids, Cancellable? cancellable = null) throws Error; public virtual string to_string() { diff --git a/src/engine/api/geary-account.vala b/src/engine/api/geary-account.vala index 1868e93e..60c3531c 100644 --- a/src/engine/api/geary-account.vala +++ b/src/engine/api/geary-account.vala @@ -248,12 +248,12 @@ public interface Geary.Account : BaseObject { * be used in local_fetch_email_async(). Return null if the email id isn't * in the local database. */ - public abstract async Geary.EmailIdentifier? folder_email_id_to_search( + public abstract async Geary.EmailIdentifier? folder_email_id_to_search_async( Geary.FolderPath folder_path, Geary.EmailIdentifier id, Geary.FolderPath? return_folder_path, Cancellable? cancellable = null) throws Error; /** - * Performs a search with the given keyword string. Optionally, a list of folders not to search + * Performs a search with the given query string. Optionally, a list of folders not to search * can be passed as well as a list of email identifiers to restrict the search to only those messages. * Returns a list of email objects with the requested fields. If partial_ok is false, mail * will only be returned if it includes all requested fields. The @@ -263,16 +263,16 @@ public interface Geary.Account : BaseObject { * you can walk the table. limit can be negative to mean "no limit" but * offset must not be negative. */ - public abstract async Gee.Collection? local_search_async(string keywords, + public abstract async Gee.Collection? local_search_async(string query, Geary.Email.Field requested_fields, bool partial_ok, Geary.FolderPath? email_id_folder_path, int limit = 100, int offset = 0, Gee.Collection? folder_blacklist = null, Gee.Collection? search_ids = null, Cancellable? cancellable = null) throws Error; /** - * Given a list of mail IDs, returns a list of keywords that match for the current - * search keywords. + * Given a list of mail IDs, returns a list of words that match for the + * last run local_search_async() query. */ - public abstract async Gee.Collection? get_search_keywords_async( + public abstract async Gee.Collection? get_search_matches_async( Gee.Collection ids, Cancellable? cancellable = null) throws Error; /** diff --git a/src/engine/api/geary-search-folder.vala b/src/engine/api/geary-search-folder.vala index c12de654..3015b63a 100644 --- a/src/engine/api/geary-search-folder.vala +++ b/src/engine/api/geary-search-folder.vala @@ -55,19 +55,21 @@ public class Geary.SearchFolder : Geary.AbstractLocalFolder { Geary.SpecialFolderType.TRASH, // Orphan emails (without a folder) are also excluded; see ctor. }; + private string? search_query = null; private Gee.TreeSet search_results; private Geary.Nonblocking.Mutex result_mutex = new Geary.Nonblocking.Mutex(); /** - * Fired when the search keywords have changed. This signal is fired *after* the search + * Fired when the search query has changed. This signal is fired *after* the search * has completed. */ - public signal void search_keywords_changed(string keywords); + public signal void search_query_changed(string? query); public SearchFolder(Account account) { _account = account; account.folders_available_unavailable.connect(on_folders_available_unavailable); + account.email_locally_complete.connect(on_email_locally_complete); clear_search_results(); @@ -78,6 +80,7 @@ public class Geary.SearchFolder : Geary.AbstractLocalFolder { ~SearchFolder() { account.folders_available_unavailable.disconnect(on_folders_available_unavailable);; + account.email_locally_complete.disconnect(on_email_locally_complete); } private void on_folders_available_unavailable(Gee.Collection? available, @@ -91,22 +94,102 @@ public class Geary.SearchFolder : Geary.AbstractLocalFolder { } } + private async Gee.ArrayList folder_ids_to_search_async(Geary.Folder folder, + Gee.Collection folder_ids, Cancellable? cancellable) throws Error { + Gee.ArrayList local_ids = new Gee.ArrayList(); + foreach (Geary.EmailIdentifier folder_id in folder_ids) { + // TODO: parallelize. + Geary.EmailIdentifier? local_id = yield account.folder_email_id_to_search_async( + folder.path, folder_id, path, cancellable); + if (local_id != null) + local_ids.add(local_id); + } + return local_ids; + } + + private async void append_new_email_async(string query, Geary.Folder folder, + Gee.Collection ids, Cancellable? cancellable) throws Error { + Gee.ArrayList local_ids = yield folder_ids_to_search_async( + folder, ids, cancellable); + + int result_mutex_token = yield result_mutex.claim_async(); + Error? error = null; + try { + Gee.Collection? results = yield account.local_search_async( + query, Geary.Email.Field.PROPERTIES, false, path, MAX_RESULT_EMAILS, 0, + exclude_folders, local_ids, cancellable); + + if (results != null) { + Gee.HashMap to_add + = new Gee.HashMap(); + foreach(Geary.Email email in results) + if (!search_results.contains(email)) + to_add.set(email.id, email); + + if (to_add.size > 0) { + search_results.add_all(to_add.values); + + _properties.set_total(search_results.size); + + notify_email_appended(to_add.keys); + notify_email_count_changed(search_results.size, CountChangeReason.APPENDED); + } + } + } catch(Error e) { + error = e; + } + + result_mutex.release(ref result_mutex_token); + + if (error != null) + throw error; + } + + private void on_append_new_email_complete(Object? source, AsyncResult result) { + try { + append_new_email_async.end(result); + } catch(Error e) { + debug("Error appending new email to search results: %s", e.message); + } + } + + private void on_email_locally_complete(Geary.Folder folder, + Gee.Collection ids) { + if (search_query != null) + append_new_email_async.begin(search_query, folder, ids, null, on_append_new_email_complete); + } + + /** + * Clears the search query and results. + */ + public void clear() { + Gee.TreeSet local_results = search_results; + clear_search_results(); + notify_email_removed(email_collection_to_ids(local_results)); + notify_email_count_changed(0, Geary.Folder.CountChangeReason.REMOVED); + + if (search_query != null) { + search_query = null; + search_query_changed(null); + } + } + /** * Sets the keyword string for this search. */ - public void set_search_keywords(string keywords, Cancellable? cancellable = null) { - set_search_keywords_async.begin(keywords, cancellable, on_set_search_keywords_complete); + public void set_search_query(string query, Cancellable? cancellable = null) { + set_search_query_async.begin(query, cancellable, on_set_search_query_complete); } - private void on_set_search_keywords_complete(Object? source, AsyncResult result) { + private void on_set_search_query_complete(Object? source, AsyncResult result) { try { - set_search_keywords_async.end(result); + set_search_query_async.end(result); } catch(Error e) { debug("Search error: %s", e.message); } } - private async void set_search_keywords_async(string keywords, Cancellable? cancellable = null) throws Error { + private async void set_search_query_async(string query, Cancellable? cancellable = null) throws Error { int result_mutex_token = yield result_mutex.claim_async(); Error? error = null; try { @@ -115,7 +198,7 @@ public class Geary.SearchFolder : Geary.AbstractLocalFolder { // list_email_async() etc., but this leads to some more // complications when redoing the search. Gee.Collection? _new_results = yield account.local_search_async( - keywords, Geary.Email.Field.PROPERTIES, false, path, MAX_RESULT_EMAILS, 0, + query, Geary.Email.Field.PROPERTIES, false, path, MAX_RESULT_EMAILS, 0, exclude_folders, null, cancellable); if (_new_results == null) { @@ -125,6 +208,12 @@ public class Geary.SearchFolder : Geary.AbstractLocalFolder { Gee.TreeSet local_results = search_results; // Clear existing results. clear_search_results(); + + // Note that we probably shouldn't be firing these signals + // from inside our mutex lock. We do it here, below, and + // in append_new_email_async(). Keep an eye on it, and if + // there's ever a case where it might cause problems, it + // shouldn't be too hard to move the firings outside. notify_email_removed(email_collection_to_ids(local_results)); notify_email_count_changed(0, Geary.Folder.CountChangeReason.REMOVED); } @@ -132,7 +221,7 @@ public class Geary.SearchFolder : Geary.AbstractLocalFolder { // Move new search results into a hashset, using email ID for equality. Gee.HashSet new_results = new Gee.HashSet(email_id_hash, email_id_equal); new_results.add_all(_new_results); - + // Match the new results up with the existing results. Gee.HashSet to_add = new Gee.HashSet(email_id_hash, email_id_equal); Gee.HashSet to_remove = new Gee.HashSet(email_id_hash, email_id_equal); @@ -170,7 +259,8 @@ public class Geary.SearchFolder : Geary.AbstractLocalFolder { result_mutex.release(ref result_mutex_token); - search_keywords_changed(keywords); + search_query = query; + search_query_changed(query); if (error != null) throw error; @@ -280,12 +370,14 @@ public class Geary.SearchFolder : Geary.AbstractLocalFolder { } /** - * Given a list of mail IDs, returns a list of keywords that match for the current - * search keywords. + * Given a list of mail IDs, returns a list of words that match for the current + * search query. */ - public async Gee.Collection? get_search_keywords_async( + public async Gee.Collection? get_search_matches_async( Gee.Collection ids, Cancellable? cancellable = null) throws Error { - return yield account.get_search_keywords_async(ids, cancellable); + if (search_query == null) + return null; + return yield account.get_search_matches_async(ids, cancellable); } private void exclude_folder(Geary.Folder folder) { diff --git a/src/engine/app/app-conversation-monitor.vala b/src/engine/app/app-conversation-monitor.vala index 658b006c..ceba0e22 100644 --- a/src/engine/app/app-conversation-monitor.vala +++ b/src/engine/app/app-conversation-monitor.vala @@ -476,7 +476,7 @@ public class Geary.App.ConversationMonitor : BaseObject { foreach (Geary.EmailIdentifier id in relevant_ids) { // TODO: parallelize this. try { - Geary.EmailIdentifier? search_id = yield folder.account.folder_email_id_to_search( + Geary.EmailIdentifier? search_id = yield folder.account.folder_email_id_to_search_async( folder.path, id, null, cancellable); if (search_id != null) { Geary.Email email = yield folder.account.local_fetch_email_async( diff --git a/src/engine/imap-db/imap-db-account.vala b/src/engine/imap-db/imap-db-account.vala index cbe48b28..fd0bd20f 100644 --- a/src/engine/imap-db/imap-db-account.vala +++ b/src/engine/imap-db/imap-db-account.vala @@ -625,14 +625,32 @@ private class Geary.ImapDB.Account : BaseObject { } } + private string? get_search_ids_sql(Gee.Collection? search_ids) throws Error { + if (search_ids == null) + return null; + + Gee.ArrayList ids = new Gee.ArrayList(); + foreach (Geary.EmailIdentifier id in search_ids) { + if (!(id is Geary.ImapDB.EmailIdentifier)) { + throw new EngineError.BAD_PARAMETERS( + "search_ids must contain only Geary.ImapDB.EmailIdentifiers"); + } + + ids.add(id.ordering); + } + + StringBuilder sql = new StringBuilder(); + sql_append_ids(sql, ids); + return sql.str; + } + public async Gee.Collection? search_async(string prepared_query, Geary.Email.Field requested_fields, bool partial_ok, Geary.FolderPath? email_id_folder_path, int limit = 100, int offset = 0, Gee.Collection? folder_blacklist = null, Gee.Collection? search_ids = null, Cancellable? cancellable = null) throws Error { Gee.Collection search_results = new Gee.HashSet(); - // TODO: support search_ids - + string? search_ids_sql = get_search_ids_sql(search_ids); yield db.exec_transaction_async(Db.TransactionType.RO, (cx) => { string blacklisted_ids_sql = do_get_blacklisted_message_ids_sql( folder_blacklist, cx, cancellable); @@ -645,6 +663,8 @@ private class Geary.ImapDB.Account : BaseObject { """; if (blacklisted_ids_sql != "") sql += " AND id NOT IN (%s)".printf(blacklisted_ids_sql); + if (!Geary.String.is_empty(search_ids_sql)) + sql += " AND id IN (%s)".printf(search_ids_sql); sql += " ORDER BY internaldate_time_t DESC"; if (limit > 0) sql += " LIMIT ? OFFSET ?"; @@ -678,9 +698,9 @@ private class Geary.ImapDB.Account : BaseObject { return (search_results.size == 0 ? null : search_results); } - public async Gee.Collection? get_search_keywords_async(string prepared_query, + public async Gee.Collection? get_search_matches_async(string prepared_query, Gee.Collection ids, Cancellable? cancellable = null) throws Error { - Gee.Set search_keywords = new Gee.HashSet(); + Gee.Set search_matches = new Gee.HashSet(); // Create a question mark for each ID. string id_string = ""; @@ -718,7 +738,7 @@ private class Geary.ImapDB.Account : BaseObject { // the results into our return set. foreach(SearchOffset offset in all_offsets) { string text = result.string_at(offset.column + 1); - search_keywords.add(text[offset.byte_offset : offset.byte_offset + offset.size].down()); + search_matches.add(text[offset.byte_offset : offset.byte_offset + offset.size].down()); } result.next(cancellable); @@ -727,7 +747,7 @@ private class Geary.ImapDB.Account : BaseObject { return Db.TransactionOutcome.DONE; }, cancellable); - return (search_keywords.size == 0 ? null : search_keywords); + return (search_matches.size == 0 ? null : search_matches); } public async Geary.Email fetch_email_async(Geary.EmailIdentifier email_id, @@ -760,7 +780,7 @@ private class Geary.ImapDB.Account : BaseObject { return email; } - public async Geary.EmailIdentifier? folder_email_id_to_search( + public async Geary.EmailIdentifier? folder_email_id_to_search_async( Geary.FolderPath folder_path, Geary.EmailIdentifier id, Geary.FolderPath? return_folder_path, Cancellable? cancellable) throws Error { int64? row_id = null; diff --git a/src/engine/imap-engine/imap-engine-generic-account.vala b/src/engine/imap-engine/imap-engine-generic-account.vala index f3035df9..98625d7c 100644 --- a/src/engine/imap-engine/imap-engine-generic-account.vala +++ b/src/engine/imap-engine/imap-engine-generic-account.vala @@ -510,29 +510,30 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount { return yield local.fetch_email_async(email_id, required_fields, cancellable); } - public override async Geary.EmailIdentifier? folder_email_id_to_search( + public override async Geary.EmailIdentifier? folder_email_id_to_search_async( Geary.FolderPath folder_path, Geary.EmailIdentifier id, Geary.FolderPath? return_folder_path, Cancellable? cancellable = null) throws Error { - return yield local.folder_email_id_to_search(folder_path, id, return_folder_path, cancellable); + return yield local.folder_email_id_to_search_async( + folder_path, id, return_folder_path, cancellable); } - public override async Gee.Collection? local_search_async(string keywords, + public override async Gee.Collection? local_search_async(string query, Geary.Email.Field requested_fields, bool partial_ok, Geary.FolderPath? email_id_folder_path, int limit = 100, int offset = 0, Gee.Collection? folder_blacklist = null, Gee.Collection? search_ids = null, Cancellable? cancellable = null) throws Error { if (offset < 0) throw new EngineError.BAD_PARAMETERS("Offset must not be negative"); - previous_prepared_search_query = local.prepare_search_query(keywords); + previous_prepared_search_query = local.prepare_search_query(query); - return yield local.search_async(local.prepare_search_query(keywords), + return yield local.search_async(previous_prepared_search_query, requested_fields, partial_ok, email_id_folder_path, limit, offset, folder_blacklist, search_ids, cancellable); } - public override async Gee.Collection? get_search_keywords_async( + public override async Gee.Collection? get_search_matches_async( Gee.Collection ids, Cancellable? cancellable = null) throws Error { - return yield local.get_search_keywords_async(previous_prepared_search_query, ids, cancellable); + return yield local.get_search_matches_async(previous_prepared_search_query, ids, cancellable); } private void on_login_failed(Geary.Credentials? credentials) {