[geary] Add basic search specifier support
- From: Charles Lindsay <clindsay src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [geary] Add basic search specifier support
- Date: Tue, 17 Dec 2013 19:43:19 +0000 (UTC)
commit aac2956182a51f41c5787a715dbf5a8d393bc0c9
Author: Charles Lindsay <chaz yorba org>
Date: Tue Dec 17 11:40:29 2013 -0800
Add basic search specifier support
The list of supported operators is:
* attachment:
* bcc:
* body:
* cc:
* from:
* subject:
* to:
Closes: bgo bug #714494
src/CMakeLists.txt | 1 +
src/engine/abstract/geary-abstract-account.vala | 4 +-
src/engine/api/geary-account.vala | 8 +-
src/engine/api/geary-search-folder.vala | 16 +-
src/engine/api/geary-search-query.vala | 44 +++++
src/engine/imap-db/imap-db-account.vala | 192 +++++++++++++++-----
.../imap-engine/imap-engine-generic-account.vala | 10 +-
src/engine/util/util-iterable.vala | 36 ++++
8 files changed, 245 insertions(+), 66 deletions(-)
---
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index fab8709..d9e96a4 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -39,6 +39,7 @@ engine/api/geary-named-flag.vala
engine/api/geary-named-flags.vala
engine/api/geary-progress-monitor.vala
engine/api/geary-search-folder.vala
+engine/api/geary-search-query.vala
engine/api/geary-service-provider.vala
engine/api/geary-special-folder-type.vala
diff --git a/src/engine/abstract/geary-abstract-account.vala b/src/engine/abstract/geary-abstract-account.vala
index f9ca814..4ee8115 100644
--- a/src/engine/abstract/geary-abstract-account.vala
+++ b/src/engine/abstract/geary-abstract-account.vala
@@ -116,11 +116,11 @@ 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 Gee.Collection<Geary.EmailIdentifier>? local_search_async(string query,
+ public abstract async Gee.Collection<Geary.EmailIdentifier>? local_search_async(Geary.SearchQuery query,
int limit = 100, int offset = 0, Gee.Collection<Geary.FolderPath?>? folder_blacklist = null,
Gee.Collection<Geary.EmailIdentifier>? search_ids = null, Cancellable? cancellable = null) throws
Error;
- public abstract async Gee.Collection<string>? get_search_matches_async(string query,
+ public abstract async Gee.Collection<string>? get_search_matches_async(Geary.SearchQuery query,
Gee.Collection<Geary.EmailIdentifier> ids, Cancellable? cancellable = null) throws Error;
public abstract async Gee.MultiMap<Geary.EmailIdentifier, Geary.FolderPath>?
get_containing_folders_async(
diff --git a/src/engine/api/geary-account.vala b/src/engine/api/geary-account.vala
index 38b0c39..f2dc933 100644
--- a/src/engine/api/geary-account.vala
+++ b/src/engine/api/geary-account.vala
@@ -305,21 +305,21 @@ public interface Geary.Account : BaseObject {
Geary.Email.Field required_fields, Cancellable? cancellable = null) throws Error;
/**
- * Performs a search with the given query string. Optionally, a list of folders not to search
+ * Performs a search with the given query. 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 EmailIdentifiers, or null if there are no results.
* The list is limited to a maximum number of results and starting offset,
* so you can walk the table. limit can be negative to mean "no limit" but
* offset must not be negative.
*/
- public abstract async Gee.Collection<Geary.EmailIdentifier>? local_search_async(string query,
+ public abstract async Gee.Collection<Geary.EmailIdentifier>? local_search_async(Geary.SearchQuery query,
int limit = 100, int offset = 0, Gee.Collection<Geary.FolderPath?>? folder_blacklist = null,
Gee.Collection<Geary.EmailIdentifier>? search_ids = null, Cancellable? cancellable = null) throws
Error;
/**
- * Given a list of mail IDs, returns a list of words that match the given query string.
+ * Given a list of mail IDs, returns a list of words that match for the query.
*/
- public abstract async Gee.Collection<string>? get_search_matches_async(string query,
+ public abstract async Gee.Collection<string>? get_search_matches_async(Geary.SearchQuery query,
Gee.Collection<Geary.EmailIdentifier> 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 499d0a4..dcd41d0 100644
--- a/src/engine/api/geary-search-folder.vala
+++ b/src/engine/api/geary-search-folder.vala
@@ -55,7 +55,7 @@ public class Geary.SearchFolder : Geary.AbstractLocalFolder, Geary.FolderSupport
Geary.SpecialFolderType.TRASH,
// Orphan emails (without a folder) are also excluded; see ctor.
};
- private string? search_query = null;
+ private Geary.SearchQuery? search_query = null;
private Gee.TreeSet<ImapDB.SearchEmailIdentifier> search_results;
private Geary.Nonblocking.Mutex result_mutex = new Geary.Nonblocking.Mutex();
@@ -113,7 +113,7 @@ public class Geary.SearchFolder : Geary.AbstractLocalFolder, Geary.FolderSupport
}
}
- private async void append_new_email_async(string query, Geary.Folder folder,
+ private async void append_new_email_async(Geary.SearchQuery query, Geary.Folder folder,
Gee.Collection<Geary.EmailIdentifier> ids, Cancellable? cancellable) throws Error {
int result_mutex_token = yield result_mutex.claim_async();
@@ -144,7 +144,7 @@ public class Geary.SearchFolder : Geary.AbstractLocalFolder, Geary.FolderSupport
append_new_email_async.begin(search_query, folder, ids, null, on_append_new_email_complete);
}
- private async void handle_removed_email_async(string query, Geary.Folder folder,
+ private async void handle_removed_email_async(Geary.SearchQuery query, Geary.Folder folder,
Gee.Collection<Geary.EmailIdentifier> ids, Cancellable? cancellable) throws Error {
int result_mutex_token = yield result_mutex.claim_async();
@@ -213,19 +213,21 @@ public class Geary.SearchFolder : Geary.AbstractLocalFolder, Geary.FolderSupport
}
private async void set_search_query_async(string query, Cancellable? cancellable = null) throws Error {
+ Geary.SearchQuery search_query = new Geary.SearchQuery(query);
+
int result_mutex_token = yield result_mutex.claim_async();
Error? error = null;
try {
- yield do_search_async(query, null, null, cancellable);
+ yield do_search_async(search_query, null, null, cancellable);
} catch(Error e) {
error = e;
}
result_mutex.release(ref result_mutex_token);
- search_query = query;
- search_query_changed(query);
+ this.search_query = search_query;
+ search_query_changed(search_query.raw);
if (error != null)
throw error;
@@ -237,7 +239,7 @@ public class Geary.SearchFolder : Geary.AbstractLocalFolder, Geary.FolderSupport
// considered to be a delta and are added or subtracted from the full set.
// add_ids are new ids to search for, remove_ids are ids in our result set
// that will be removed if this search doesn't turn them up.
- private async void do_search_async(string query, Gee.Collection<Geary.EmailIdentifier>? add_ids,
+ private async void do_search_async(Geary.SearchQuery query, Gee.Collection<Geary.EmailIdentifier>?
add_ids,
Gee.Collection<ImapDB.SearchEmailIdentifier>? remove_ids, Cancellable? cancellable) throws Error {
// There are three cases here: 1) replace full result set, where the
// *_ids parameters are both null, 2) add to result set, where just
diff --git a/src/engine/api/geary-search-query.vala b/src/engine/api/geary-search-query.vala
new file mode 100644
index 0000000..c03783d
--- /dev/null
+++ b/src/engine/api/geary-search-query.vala
@@ -0,0 +1,44 @@
+/* Copyright 2013 Yorba Foundation
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later). See the COPYING file in this distribution.
+ */
+
+/**
+ * An object to hold state for various search subsystems that might need to
+ * parse the same text string different ways. The only interaction the API
+ * user should have with this is creating new ones and then passing them off to
+ * the search methods in the engine.
+ *
+ * TODO: support anything other than ImapDB.Account's search methods.
+ */
+public class Geary.SearchQuery : BaseObject {
+ public string raw { get; private set; }
+ public bool parsed { get; internal set; default = false; }
+
+ // Not using a MultiMap because we (might) need a guarantee of order.
+ private Gee.HashMap<string?, Gee.ArrayList<string>> field_map
+ = new Gee.HashMap<string?, Gee.ArrayList<string>>();
+
+ public SearchQuery(string query) {
+ raw = query;
+ }
+
+ internal void add_token(string? field, string token) {
+ if (!field_map.has_key(field))
+ field_map.set(field, new Gee.ArrayList<string>());
+
+ field_map.get(field).add(token);
+ }
+
+ internal Gee.Collection<string?> get_fields() {
+ return field_map.keys;
+ }
+
+ internal Gee.List<string>? get_tokens(string? field) {
+ if (!field_map.has_key(field))
+ return null;
+
+ return field_map.get(field);
+ }
+}
diff --git a/src/engine/imap-db/imap-db-account.vala b/src/engine/imap-db/imap-db-account.vala
index 1cbbd9e..3c0407f 100644
--- a/src/engine/imap-db/imap-db-account.vala
+++ b/src/engine/imap-db/imap-db-account.vala
@@ -624,29 +624,76 @@ private class Geary.ImapDB.Account : BaseObject {
return (messages.size == 0 ? null : messages);
}
- public string prepare_search_query(string raw_query) {
- // Two goals here:
- // 1) append an * after every term so it becomes a prefix search
- // (see <https://www.sqlite.org/fts3.html#section_3>), and
- // 2) strip out common words/operators that might get interpreted as
- // search operators.
+ private string? extract_field_from_token(string[] parts, ref string token) {
+ // Map of user-supplied search field names to column names.
+ Gee.HashMap<string, string> field_names = new Gee.HashMap<string, string>();
+ /// Can be typed in the search box like attachment:file.txt to find
+ /// messages with attachments with a particular name.
+ field_names.set(_("attachment"), "attachment");
+ /// Can be typed in the search box like bcc:johndoe example com to find
+ /// messages bcc'd to a particular person.
+ field_names.set(_("bcc"), "bcc");
+ /// Can be typed in the search box like body:word to find the word only
+ /// if it occurs in the body of a message.
+ field_names.set(_("body"), "body");
+ /// Can be typed in the search box like cc:johndoe example com to find
+ /// messages cc'd to a particular person.
+ field_names.set(_("cc"), "cc");
+ /// Can be typed in the search box like from:johndoe example com to
+ /// find messages from a particular sender.
+ field_names.set(_("from"), "from_field");
+ /// Can be typed in the search box like subject:word to find the word
+ /// only if it occurs in the subject of a message.
+ field_names.set(_("subject"), "subject");
+ /// Can be typed in the search box like to:johndoe example com to find
+ /// messages received by a particular person.
+ field_names.set(_("to"), "receivers");
+
+ // If they stopped at "field:", treat it as if they hadn't typed the :
+ if (Geary.String.is_empty_or_whitespace(parts[1])) {
+ token = parts[0];
+ return null;
+ }
+
+ string key = parts[0].down();
+ if (key in field_names.keys) {
+ token = parts[1];
+ return field_names.get(key);
+ }
+
+ return null;
+ }
+
+ private void prepare_search_query(Geary.SearchQuery query) {
+ if (query.parsed)
+ return;
+
+ // A few goals here:
+ // 1) Append an * after every term so it becomes a prefix search
+ // (see <https://www.sqlite.org/fts3.html#section_3>)
+ // 2) Strip out common words/operators that might get interpreted as
+ // search operators
+ // 3) Parse each word into a list of which field it applies to, so
+ // you can do "to:johndoe example com thing" (quotes excluded)
+ // to find messages to John containing the word thing
// We ignore everything inside quotes to give the user a way to
// override our algorithm here. The idea is to offer one search query
// syntax for Geary that we can use locally and via IMAP, etc.
- string quote_balanced = raw_query;
- if (Geary.String.count_char(raw_query, '"') % 2 != 0) {
+ string quote_balanced = query.raw;
+ if (Geary.String.count_char(query.raw, '"') % 2 != 0) {
// Remove the last quote if it's not balanced. This has the
// benefit of showing decent results as you type a quoted phrase.
- int last_quote = raw_query.last_index_of_char('"');
+ int last_quote = query.raw.last_index_of_char('"');
assert(last_quote >= 0);
- quote_balanced = raw_query.splice(last_quote, last_quote + 1, " ");
+ quote_balanced = query.raw.splice(last_quote, last_quote + 1, " ");
}
- string[] words = quote_balanced.split_set(" \t\r\n:()%*\\");
+ string[] words = quote_balanced.split_set(" \t\r\n()%*\\");
bool in_quote = false;
- StringBuilder prepared_query = new StringBuilder();
foreach (string s in words) {
+ string? field = null;
+
s = s.strip();
int quotes = Geary.String.count_char(s, '"');
@@ -655,7 +702,12 @@ private class Geary.ImapDB.Account : BaseObject {
--quotes;
}
- if (!in_quote) {
+ if (in_quote) {
+ // HACK: this helps prevent a syntax error when the user types
+ // something like from:"somebody". If we ever properly support
+ // quotes after : we can get rid of this.
+ s = s.replace(":", " ");
+ } else {
string lower = s.down();
if (lower == "" || lower == "and" || lower == "or" || lower == "not" || lower == "near"
|| lower.has_prefix("near/"))
@@ -667,24 +719,61 @@ private class Geary.ImapDB.Account : BaseObject {
if (s == "")
continue;
+ // TODO: support quotes after :
+ string[] parts = s.split(":", 2);
+ if (parts.length > 1)
+ field = extract_field_from_token(parts, ref s);
+
s = "\"" + s + "*\"";
}
if (in_quote && quotes % 2 != 0)
in_quote = false;
- prepared_query.append(s);
- prepared_query.append(" ");
+ query.add_token(field, s);
}
assert(!in_quote);
- return prepared_query.str.strip();
+ query.parsed = true;
+ }
+
+ // Return a map of column -> phrase, to use as WHERE column MATCH 'phrase'.
+ private Gee.HashMap<string, string> get_query_phrases(Geary.SearchQuery query) {
+ prepare_search_query(query);
+
+ Gee.HashMap<string, string> phrases = new Gee.HashMap<string, string>();
+ foreach (string? field in query.get_fields()) {
+ string? phrase = null;
+ Gee.List<string>? tokens = query.get_tokens(field);
+ if (tokens != null)
+ phrase = string.joinv(" ", tokens.to_array()).strip();
+
+ if (!Geary.String.is_empty(phrase))
+ phrases.set((field == null ? "MessageSearchTable" : field), phrase);
+ }
+ return phrases;
+ }
+
+ private void sql_add_query_phrases(StringBuilder sql, Gee.HashMap<string, string> query_phrases) {
+ foreach (string field in query_phrases.keys)
+ sql.append(" AND %s MATCH ?".printf(field));
+ }
+
+ private int sql_bind_query_phrases(Db.Statement stmt, int start_index,
+ Gee.HashMap<string, string> query_phrases) throws Geary.DatabaseError {
+ int i = start_index;
+ // This relies on the keys being returned in the same order every time
+ // from the same map. It might not be guaranteed, but I feel pretty
+ // confident it'll work unless you change the map in between.
+ foreach (string field in query_phrases.keys)
+ stmt.bind_string(i++, query_phrases.get(field));
+ return i - start_index;
}
// Append each id in the collection to the StringBuilder, in a format
// suitable for use in an SQL statement IN (...) clause.
- private void sql_append_ids(StringBuilder s, Gee.Collection<int64?> ids) {
+ private void sql_append_ids(StringBuilder s, Gee.Iterable<int64?> ids) {
bool first = true;
foreach (int64? id in ids) {
assert(id != null);
@@ -716,11 +805,15 @@ private class Geary.ImapDB.Account : BaseObject {
return sql.str;
}
- public async Gee.Collection<Geary.EmailIdentifier>? search_async(string prepared_query,
+ public async Gee.Collection<Geary.EmailIdentifier>? search_async(Geary.SearchQuery query,
int limit = 100, int offset = 0, Gee.Collection<Geary.FolderPath?>? folder_blacklist = null,
Gee.Collection<Geary.EmailIdentifier>? search_ids = null, Cancellable? cancellable = null) throws
Error {
check_open();
+ Gee.HashMap<string, string> query_phrases = get_query_phrases(query);
+ if (query_phrases.size == 0)
+ return null;
+
Gee.ArrayList<ImapDB.SearchEmailIdentifier> search_results
= new Gee.ArrayList<ImapDB.SearchEmailIdentifier>();
@@ -738,28 +831,32 @@ private class Geary.ImapDB.Account : BaseObject {
// own), it cuts the running time roughly in half of how it was
// before. The short version is: modify with extreme caution. See
// <http://redmine.yorba.org/issues/7372>.
- string sql = """
+ StringBuilder sql = new StringBuilder();
+ sql.append("""
SELECT id, internaldate_time_t
FROM MessageTable
INDEXED BY MessageTableInternalDateTimeTIndex
WHERE id IN (
SELECT id
FROM MessageSearchTable
- WHERE MessageSearchTable MATCH ?
- )
- """;
+ WHERE 1=1
+ """);
+ sql_add_query_phrases(sql, query_phrases);
+ sql.append(")");
+
if (blacklisted_ids_sql != "")
- sql += " AND id NOT IN (%s)".printf(blacklisted_ids_sql);
+ sql.append(" 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";
+ sql.append(" AND id IN (%s)".printf(search_ids_sql));
+ sql.append(" ORDER BY internaldate_time_t DESC");
if (limit > 0)
- sql += " LIMIT ? OFFSET ?";
- Db.Statement stmt = cx.prepare(sql);
- stmt.bind_string(0, prepared_query);
+ sql.append(" LIMIT ? OFFSET ?");
+
+ Db.Statement stmt = cx.prepare(sql.str);
+ int bind_index = sql_bind_query_phrases(stmt, 0, query_phrases);
if (limit > 0) {
- stmt.bind_int(1, limit);
- stmt.bind_int(2, offset);
+ stmt.bind_int(bind_index++, limit);
+ stmt.bind_int(bind_index++, offset);
}
Db.Result result = stmt.exec(cancellable);
@@ -796,29 +893,30 @@ private class Geary.ImapDB.Account : BaseObject {
}
}
- public async Gee.Collection<string>? get_search_matches_async(string raw_query, string prepared_query,
+ public async Gee.Collection<string>? get_search_matches_async(Geary.SearchQuery query,
Gee.Collection<ImapDB.EmailIdentifier> ids, Cancellable? cancellable = null) throws Error {
check_open();
- Gee.Set<string> search_matches = new Gee.HashSet<string>();
+ Gee.HashMap<string, string> query_phrases = get_query_phrases(query);
+ if (query_phrases.size == 0)
+ return null;
- // Create a question mark for each ID.
- string id_string = "";
- for(int i = 0; i < ids.size; i++) {
- id_string += "?";
- if (i != ids.size - 1)
- id_string += ", ";
- }
+ Gee.Set<string> search_matches = new Gee.HashSet<string>();
yield db.exec_transaction_async(Db.TransactionType.RO, (cx) => {
- Db.Statement stmt = cx.prepare("SELECT offsets(MessageSearchTable), * FROM MessageSearchTable " +
- "WHERE MessageSearchTable MATCH ? AND id IN (%s)".printf(id_string));
+ StringBuilder sql = new StringBuilder();
+ sql.append("""
+ SELECT offsets(MessageSearchTable), *
+ FROM MessageSearchTable
+ WHERE id IN (
+ """);
+ sql_append_ids(sql,
+ Geary.traverse<ImapDB.EmailIdentifier>(ids).map<int64?>(id =>
id.message_id).to_gee_iterable());
+ sql.append(")");
+ sql_add_query_phrases(sql, query_phrases);
- // Bind query and IDs.
- int i = 0;
- stmt.bind_string(i++, prepared_query);
- foreach(ImapDB.EmailIdentifier id in ids)
- stmt.bind_rowid(i++, id.message_id);
+ Db.Statement stmt = cx.prepare(sql.str);
+ sql_bind_query_phrases(stmt, 0, query_phrases);
Db.Result result = stmt.exec(cancellable);
while (!result.finished) {
@@ -847,7 +945,7 @@ private class Geary.ImapDB.Account : BaseObject {
return Db.TransactionOutcome.DONE;
}, cancellable);
- add_literal_matches(raw_query, search_matches);
+ add_literal_matches(query.raw, search_matches);
return (search_matches.size == 0 ? null : search_matches);
}
diff --git a/src/engine/imap-engine/imap-engine-generic-account.vala
b/src/engine/imap-engine/imap-engine-generic-account.vala
index d62f639..d26aef2 100644
--- a/src/engine/imap-engine/imap-engine-generic-account.vala
+++ b/src/engine/imap-engine/imap-engine-generic-account.vala
@@ -567,20 +567,18 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount {
return yield local.fetch_email_async(check_id(email_id), required_fields, cancellable);
}
- public override async Gee.Collection<Geary.EmailIdentifier>? local_search_async(string query,
+ public override async Gee.Collection<Geary.EmailIdentifier>? local_search_async(Geary.SearchQuery query,
int limit = 100, int offset = 0, Gee.Collection<Geary.FolderPath?>? folder_blacklist = null,
Gee.Collection<Geary.EmailIdentifier>? search_ids = null, Cancellable? cancellable = null) throws
Error {
if (offset < 0)
throw new EngineError.BAD_PARAMETERS("Offset must not be negative");
- return yield local.search_async(local.prepare_search_query(query),
- limit, offset, folder_blacklist, search_ids, cancellable);
+ return yield local.search_async(query, limit, offset, folder_blacklist, search_ids, cancellable);
}
- public override async Gee.Collection<string>? get_search_matches_async(string query,
+ public override async Gee.Collection<string>? get_search_matches_async(Geary.SearchQuery query,
Gee.Collection<Geary.EmailIdentifier> ids, Cancellable? cancellable = null) throws Error {
- return yield local.get_search_matches_async(query, local.prepare_search_query(query),
- check_ids(ids), cancellable);
+ return yield local.get_search_matches_async(query, check_ids(ids), cancellable);
}
public override async Gee.MultiMap<Geary.EmailIdentifier, Geary.FolderPath>?
get_containing_folders_async(
diff --git a/src/engine/util/util-iterable.vala b/src/engine/util/util-iterable.vala
index 4c5ad05..4eb004a 100644
--- a/src/engine/util/util-iterable.vala
+++ b/src/engine/util/util-iterable.vala
@@ -5,6 +5,9 @@
*/
namespace Geary {
+ /**
+ * Take a Gee object and return a Geary.Iterable for convenience.
+ */
public Geary.Iterable<G> traverse<G>(Gee.Iterable<G> i) {
return new Geary.Iterable<G>(i.iterator());
}
@@ -21,6 +24,31 @@ namespace Geary {
*/
public class Geary.Iterable<G> : BaseObject {
+ /**
+ * A private class that lets us take a Geary.Iterable and convert it back
+ * into a Gee.Iterable.
+ */
+ private class GeeIterable<G> : Gee.Traversable<G>, Gee.Iterable<G>, BaseObject {
+ private Gee.Iterator<G> i;
+
+ public GeeIterable(Gee.Iterator<G> iterator) {
+ i = iterator;
+ }
+
+ public Gee.Iterator<G> iterator() {
+ return i;
+ }
+
+ // Unfortunately necessary for Gee.Traversable.
+ public virtual bool @foreach(Gee.ForallFunc<G> f) {
+ foreach (G g in this) {
+ if (!f(g))
+ return false;
+ }
+ return true;
+ }
+ }
+
private Gee.Iterator<G> i;
public Iterable(Gee.Iterator<G> iterator) {
@@ -100,6 +128,14 @@ public class Geary.Iterable<G> : BaseObject {
return count;
}
+ /**
+ * The resulting Gee.Iterable comes with the same caveat that you may only
+ * iterate over it once.
+ */
+ public Gee.Iterable<G> to_gee_iterable() {
+ return new GeeIterable<G>(i);
+ }
+
public Gee.Collection<G> add_all_to(Gee.Collection<G> c) {
while (i.next())
c.add(i get());
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]