From d1184d4ea5796869ab8892b4edd8b73d7557fabf Mon Sep 17 00:00:00 2001 From: Manos Pitsidianakis Date: Sat, 12 Oct 2019 18:03:58 +0300 Subject: [PATCH] ui/search: add sorting in search --- melib/src/error.rs | 7 ++ melib/src/thread.rs | 79 +++++++++++++++++ ui/src/components/mail/listing/compact.rs | 24 ++++- .../components/mail/listing/conversations.rs | 24 ++++- ui/src/search.rs | 33 ++++++- ui/src/sqlite3.rs | 87 +++++++++++++++++-- 6 files changed, 241 insertions(+), 13 deletions(-) diff --git a/melib/src/error.rs b/melib/src/error.rs index 3f5ffdca..07c9d633 100644 --- a/melib/src/error.rs +++ b/melib/src/error.rs @@ -133,3 +133,10 @@ impl From for MeliError { MeliError::new(format!("{}", kind)) } } + +impl From<&str> for MeliError { + #[inline] + fn from(kind: &str) -> MeliError { + MeliError::new(kind.to_string()) + } +} diff --git a/melib/src/thread.rs b/melib/src/thread.rs index 89981b5b..a4ad62cd 100644 --- a/melib/src/thread.rs +++ b/melib/src/thread.rs @@ -1067,6 +1067,85 @@ impl Threads { */ } + pub fn vec_inner_sort_by( + &self, + vec: &mut Vec, + sort: (SortField, SortOrder), + envelopes: &Envelopes, + ) { + vec.sort_by(|b, a| match sort { + (SortField::Date, SortOrder::Desc) => { + let a = &self.thread_nodes[&a]; + let b = &self.thread_nodes[&b]; + b.date.cmp(&a.date) + } + (SortField::Date, SortOrder::Asc) => { + let a = &self.thread_nodes[&a]; + let b = &self.thread_nodes[&b]; + a.date.cmp(&b.date) + } + (SortField::Subject, SortOrder::Desc) => { + let a = &self.thread_nodes[&a].message(); + let b = &self.thread_nodes[&b].message(); + + match (a, b) { + (Some(_), Some(_)) => {} + (Some(_), None) => { + return Ordering::Greater; + } + (None, Some(_)) => { + return Ordering::Less; + } + (None, None) => { + return Ordering::Equal; + } + } + let ma = &envelopes[&a.unwrap()]; + let mb = &envelopes[&b.unwrap()]; + #[cfg(feature = "unicode_algorithms")] + { + ma.subject() + .split_graphemes() + .cmp(&mb.subject().split_graphemes()) + } + #[cfg(not(feature = "unicode_algorithms"))] + { + ma.subject().cmp(&mb.subject()) + } + } + (SortField::Subject, SortOrder::Asc) => { + let a = &self.thread_nodes[&a].message(); + let b = &self.thread_nodes[&b].message(); + + match (a, b) { + (Some(_), Some(_)) => {} + (Some(_), None) => { + return Ordering::Less; + } + (None, Some(_)) => { + return Ordering::Greater; + } + (None, None) => { + return Ordering::Equal; + } + } + let ma = &envelopes[&a.unwrap()]; + let mb = &envelopes[&b.unwrap()]; + #[cfg(feature = "unicode_algorithms")] + { + mb.subject() + .as_ref() + .split_graphemes() + .cmp(&ma.subject().split_graphemes()) + } + + #[cfg(not(feature = "unicode_algorithms"))] + { + mb.subject().as_ref().cmp(&ma.subject()) + } + } + }); + } fn inner_sort_by(&self, sort: (SortField, SortOrder), envelopes: &Envelopes) { let tree = &mut self.tree_index.borrow_mut(); tree.sort_by(|b, a| match sort { diff --git a/ui/src/components/mail/listing/compact.rs b/ui/src/components/mail/listing/compact.rs index 467beb1e..537bd96f 100644 --- a/ui/src/components/mail/listing/compact.rs +++ b/ui/src/components/mail/listing/compact.rs @@ -360,7 +360,13 @@ impl ListingTrait for CompactListing { let account = &context.accounts[self.cursor_pos.0]; let folder_hash = account[self.cursor_pos.1].unwrap().folder.hash(); - match crate::search::filter(filter_term, context, self.cursor_pos.0, folder_hash) { + match crate::search::filter( + filter_term, + context, + self.cursor_pos.0, + self.sort, + folder_hash, + ) { Ok(results) => { let threads = &account.collection.threads[&folder_hash]; for env_hash in results { @@ -385,6 +391,11 @@ impl ListingTrait for CompactListing { } } if !self.filtered_selection.is_empty() { + threads.vec_inner_sort_by( + &mut self.filtered_selection, + self.sort, + &context.accounts[self.cursor_pos.0].collection, + ); self.new_cursor_pos.2 = std::cmp::min(self.filtered_selection.len() - 1, self.cursor_pos.2); } else { @@ -1080,6 +1091,17 @@ impl Component for CompactListing { debug!("Sort {:?} , {:?}", field, order); self.sort = (*field, *order); if !self.filtered_selection.is_empty() { + let folder_hash = context.accounts[self.cursor_pos.0][self.cursor_pos.1] + .unwrap() + .folder + .hash(); + let threads = + &context.accounts[self.cursor_pos.0].collection.threads[&folder_hash]; + threads.vec_inner_sort_by( + &mut self.filtered_selection, + self.sort, + &context.accounts[self.cursor_pos.0].collection, + ); self.dirty = true; } else { self.refresh_mailbox(context); diff --git a/ui/src/components/mail/listing/conversations.rs b/ui/src/components/mail/listing/conversations.rs index e120b39b..f7ae339a 100644 --- a/ui/src/components/mail/listing/conversations.rs +++ b/ui/src/components/mail/listing/conversations.rs @@ -351,7 +351,13 @@ impl ListingTrait for ConversationsListing { let account = &context.accounts[self.cursor_pos.0]; let folder_hash = account[self.cursor_pos.1].unwrap().folder.hash(); - match crate::search::filter(filter_term, context, self.cursor_pos.0, folder_hash) { + match crate::search::filter( + filter_term, + context, + self.cursor_pos.0, + self.sort, + folder_hash, + ) { Ok(results) => { let threads = &account.collection.threads[&folder_hash]; for env_hash in results { @@ -376,6 +382,11 @@ impl ListingTrait for ConversationsListing { } } if !self.filtered_selection.is_empty() { + threads.vec_inner_sort_by( + &mut self.filtered_selection, + self.sort, + &context.accounts[self.cursor_pos.0].collection, + ); self.new_cursor_pos.2 = std::cmp::min(self.filtered_selection.len() - 1, self.cursor_pos.2); } else { @@ -1119,6 +1130,17 @@ impl Component for ConversationsListing { debug!("Sort {:?} , {:?}", field, order); self.sort = (*field, *order); if !self.filtered_selection.is_empty() { + let folder_hash = context.accounts[self.cursor_pos.0][self.cursor_pos.1] + .unwrap() + .folder + .hash(); + let threads = + &context.accounts[self.cursor_pos.0].collection.threads[&folder_hash]; + threads.vec_inner_sort_by( + &mut self.filtered_selection, + self.sort, + &context.accounts[self.cursor_pos.0].collection, + ); self.dirty = true; } else { self.refresh_mailbox(context); diff --git a/ui/src/search.rs b/ui/src/search.rs index 52d50419..abc02515 100644 --- a/ui/src/search.rs +++ b/ui/src/search.rs @@ -20,17 +20,46 @@ */ use crate::state::Context; -use melib::{backends::FolderHash, email::EnvelopeHash, error::Result, StackVec}; +use melib::{ + backends::FolderHash, + email::{EnvelopeHash, Flag, UnixTimestamp}, + error::Result, + thread::{SortField, SortOrder}, + StackVec, +}; + +#[derive(Debug)] +pub enum Query { + Before(UnixTimestamp), + After(UnixTimestamp), + Between(UnixTimestamp, UnixTimestamp), + On(UnixTimestamp), + /* * * * */ + From(String), + To(String), + Cc(String), + Bcc(String), + InReplyTo(String), + References(String), + AllAddresses(String), + /* * * * */ + Body(String), + Subject(String), + AllText(String), + /* * * * */ + Flag(Flag), +} pub fn filter( filter_term: &str, context: &Context, account_idx: usize, + sort: (SortField, SortOrder), folder_hash: FolderHash, ) -> Result> { #[cfg(feature = "sqlite3")] { - crate::sqlite3::search(filter_term, context, account_idx, folder_hash) + crate::sqlite3::search(filter_term, context, account_idx, sort, folder_hash) } #[cfg(not(feature = "sqlite3"))] diff --git a/ui/src/sqlite3.rs b/ui/src/sqlite3.rs index e7ead33a..2f027a7e 100644 --- a/ui/src/sqlite3.rs +++ b/ui/src/sqlite3.rs @@ -19,11 +19,50 @@ * along with meli. If not, see . */ +use crate::search::Query; use crate::state::Context; -use melib::{backends::FolderHash, email::EnvelopeHash, MeliError, Result, StackVec}; +use melib::{ + backends::FolderHash, + email::EnvelopeHash, + thread::{SortField, SortOrder}, + MeliError, Result, StackVec, +}; use rusqlite::{params, Connection}; +use std::borrow::Cow; use std::convert::TryInto; +#[inline(always)] +fn escape_double_quote(w: &str) -> Cow { + if w.contains('"') { + Cow::from(w.replace('"', "\"\"")) + } else { + Cow::from(w) + } +} + +#[inline(always)] +fn fts5_bareword(w: &str) -> Cow { + if w == "AND" || w == "OR" || w == "NOT" { + Cow::from(w) + } else { + if !w.is_ascii() { + Cow::from(format!("\"{}\"", escape_double_quote(w))) + } else { + for &b in w.as_bytes() { + if !(b > 0x2f && b < 0x3a) + || !(b > 0x40 && b < 0x5b) + || !(b > 0x60 && b < 0x7b) + || b != 0x60 + || b != 26 + { + return Cow::from(format!("\"{}\"", escape_double_quote(w))); + } + } + Cow::from(w) + } + } +} + pub fn open_db(context: &crate::state::Context) -> Result { let data_dir = xdg::BaseDirectories::with_prefix("meli").map_err(|e| MeliError::new(e.to_string()))?; @@ -64,14 +103,31 @@ pub fn open_db(context: &crate::state::Context) -> Result { id INTEGER PRIMARY KEY, hash BLOB NOT NULL, date TEXT NOT NULL, - _from TEXT NOT NULL, - _to TEXT NOT NULL, - cc TEXT NOT NULL, + _from TEXT NOT NULL, + _to TEXT NOT NULL, + cc TEXT NOT NULL, bcc TEXT NOT NULL, subject TEXT NOT NULL, message_id TEXT NOT NULL, in_reply_to TEXT NOT NULL, - _references TEXT NOT NULL, + _references TEXT NOT NULL, + flags INTEGER NOT NULL, + has_attachments BOOLEAN NOT NULL, + body_text TEXT NOT NULL, + timestamp BLOB NOT NULL + ); + CREATE TABLE IF NOT EXISTS folders ( + id INTEGER PRIMARY KEY, + hash BLOB NOT NULL, + date TEXT NOT NULL, + _from TEXT NOT NULL, + _to TEXT NOT NULL, + cc TEXT NOT NULL, + bcc TEXT NOT NULL, + subject TEXT NOT NULL, + message_id TEXT NOT NULL, + in_reply_to TEXT NOT NULL, + _references TEXT NOT NULL, flags INTEGER NOT NULL, has_attachments BOOLEAN NOT NULL, body_text TEXT NOT NULL, @@ -122,7 +178,7 @@ pub fn insert(context: &crate::state::Context) -> Result<()> { conn.execute( "INSERT OR REPLACE INTO envelopes (hash, date, _from, _to, cc, bcc, subject, message_id, in_reply_to, _references, flags, has_attachments, body_text, timestamp) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14)", - params![e.hash().to_be_bytes().to_vec(), e.date_as_str(), e.field_from_to_string(), e.field_to_to_string(), e.field_cc_to_string(), e.field_bcc_to_string(), e.subject().into_owned().trim_end_matches('\u{0}'), e.message_id_display().to_string(), e.in_reply_to_display().map(|f| f.to_string()).unwrap_or(String::new()), e.field_references_to_string(), i64::from(e.flags().bits()), if e.has_attachments() { 1 } else { 0 }, String::from("sdfsa"), e.hash().to_be_bytes().to_vec()], + params![e.hash().to_be_bytes().to_vec(), e.date_as_str(), e.field_from_to_string(), e.field_to_to_string(), e.field_cc_to_string(), e.field_bcc_to_string(), e.subject().into_owned().trim_end_matches('\u{0}'), e.message_id_display().to_string(), e.in_reply_to_display().map(|f| f.to_string()).unwrap_or(String::new()), e.field_references_to_string(), i64::from(e.flags().bits()), if e.has_attachments() { 1 } else { 0 }, String::from("sdfsa"), e.date().to_be_bytes().to_vec()], ) .map_err(|e| MeliError::new(e.to_string()))?; } @@ -135,6 +191,7 @@ pub fn search( term: &str, _context: &Context, _account_idx: usize, + (sort_field, sort_order): (SortField, SortOrder), _folder_hash: FolderHash, ) -> Result> { let data_dir = @@ -145,12 +202,24 @@ pub fn search( .map_err(|e| MeliError::new(e.to_string()))?, ) .map_err(|e| MeliError::new(e.to_string()))?; - let mut stmt= conn.prepare( - "SELECT hash FROM envelopes INNER JOIN fts ON fts.rowid = envelopes.id WHERE fts MATCH ?;") + + let sort_field = match debug!(sort_field) { + SortField::Subject => "subject", + SortField::Date => "timestamp", + }; + + let sort_order = match debug!(sort_order) { + SortOrder::Asc => "ASC", + SortOrder::Desc => "DESC", + }; + + debug!("SELECT hash FROM envelopes INNER JOIN fts ON fts.rowid = envelopes.id WHERE fts MATCH ? ORDER BY {} {};", sort_field, sort_order); + let mut stmt = conn.prepare( + format!("SELECT hash FROM envelopes INNER JOIN fts ON fts.rowid = envelopes.id WHERE fts MATCH ? ORDER BY {} {};", sort_field, sort_order).as_str()) .map_err(|e| MeliError::new(e.to_string()))?; let results = stmt - .query_map(&[term], |row| Ok(row.get(0)?)) + .query_map(&[fts5_bareword(term)], |row| Ok(row.get(0)?)) .map_err(|e| MeliError::new(e.to_string()))? .map(|r: std::result::Result, rusqlite::Error>| { Ok(u64::from_be_bytes(