ui/search: add sorting in search

jmap
Manos Pitsidianakis 2019-10-12 18:03:58 +03:00
parent 1b0b699527
commit d1184d4ea5
Signed by: Manos Pitsidianakis
GPG Key ID: 73627C2F690DF710
6 changed files with 241 additions and 13 deletions

View File

@ -133,3 +133,10 @@ impl From<native_tls::Error> for MeliError {
MeliError::new(format!("{}", kind))
}
}
impl From<&str> for MeliError {
#[inline]
fn from(kind: &str) -> MeliError {
MeliError::new(kind.to_string())
}
}

View File

@ -1067,6 +1067,85 @@ impl Threads {
*/
}
pub fn vec_inner_sort_by(
&self,
vec: &mut Vec<ThreadHash>,
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 {

View File

@ -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);

View File

@ -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);

View File

@ -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<StackVec<EnvelopeHash>> {
#[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"))]

View File

@ -19,11 +19,50 @@
* along with meli. If not, see <http://www.gnu.org/licenses/>.
*/
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<str> {
if w.contains('"') {
Cow::from(w.replace('"', "\"\""))
} else {
Cow::from(w)
}
}
#[inline(always)]
fn fts5_bareword(w: &str) -> Cow<str> {
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<Connection> {
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<Connection> {
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<StackVec<EnvelopeHash>> {
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<Vec<u8>, rusqlite::Error>| {
Ok(u64::from_be_bytes(