+ 'a>,
+}
+
+impl<'a, Output> BoxedParser<'a, Output> {
+ fn new(parser: P) -> Self
+ where
+ P: Parser<'a, Output> + 'a,
+ {
+ BoxedParser {
+ parser: Box::new(parser),
+ }
+ }
+}
+
+impl<'a, Output> Parser<'a, Output> for BoxedParser<'a, Output> {
+ fn parse(&self, input: &'a str) -> Result<'a, Output> {
+ self.parser.parse(input)
+ }
+}
+
+pub fn either<'a, P1, P2, A>(parser1: P1, parser2: P2) -> impl Parser<'a, A>
+where
+ P1: Parser<'a, A>,
+ P2: Parser<'a, A>,
+{
+ move |input| match parser1.parse(input) {
+ ok @ Ok(_) => ok,
+ Err(_) => parser2.parse(input),
+ }
+}
+
+pub fn whitespace_wrap<'a, P, A>(parser: P) -> impl Parser<'a, A>
+where
+ P: Parser<'a, A>,
+{
+ right(space0(), left(parser, space0()))
+}
+
+pub fn pair<'a, P1, P2, R1, R2>(parser1: P1, parser2: P2) -> impl Parser<'a, (R1, R2)>
+where
+ P1: Parser<'a, R1>,
+ P2: Parser<'a, R2>,
+{
+ move |input| {
+ parser1.parse(input).and_then(|(next_input, result1)| {
+ parser2
+ .parse(next_input)
+ .map(|(last_input, result2)| (last_input, (result1, result2)))
+ })
+ }
+}
+
+pub fn prefix<'a, PN, P, R, RN>(pre: PN, parser: P) -> impl Parser<'a, R>
+where
+ PN: Parser<'a, RN>,
+ P: Parser<'a, R>,
+{
+ move |input| {
+ pre.parse(input).and_then(|(last_input, _)| {
+ parser
+ .parse(last_input)
+ .map(|(rest, result)| (rest, result))
+ })
+ }
+}
+
+pub fn suffix<'a, PN, P, R, RN>(parser: P, suf: PN) -> impl Parser<'a, R>
+where
+ PN: Parser<'a, RN>,
+ P: Parser<'a, R>,
+{
+ move |input| {
+ parser
+ .parse(input)
+ .and_then(|(last_input, result)| suf.parse(last_input).map(|(rest, _)| (rest, result)))
+ }
+}
+
+pub fn delimited<'a, PN, RN, P, R>(lparser: PN, mid: P, rparser: PN) -> impl Parser<'a, R>
+where
+ PN: Parser<'a, RN>,
+ P: Parser<'a, R>,
+{
+ move |input| {
+ lparser.parse(input).and_then(|(next_input, _)| {
+ mid.parse(next_input).and_then(|(last_input, result)| {
+ rparser.parse(last_input).map(|(rest, _)| (rest, result))
+ })
+ })
+ }
+}
+
+pub fn any_char(input: &str) -> Result {
+ match input.chars().next() {
+ Some(next) => Ok((&input[next.len_utf8()..], next)),
+ _ => Err(input),
+ }
+}
+
+pub fn string<'a>() -> impl Parser<'a, String> {
+ one_or_more(pred(any_char, |c| c.is_alphanumeric())).map(|r| r.into_iter().collect())
+}
+
+pub fn space1<'a>() -> impl Parser<'a, Vec> {
+ one_or_more(whitespace_char())
+}
+
+pub fn space0<'a>() -> impl Parser<'a, Vec> {
+ zero_or_more(whitespace_char())
+}
+
+pub fn and_then<'a, P, F, A, B, NextP>(parser: P, f: F) -> impl Parser<'a, B>
+where
+ P: Parser<'a, A>,
+ NextP: Parser<'a, B>,
+ F: Fn(A) -> NextP,
+{
+ move |input| match parser.parse(input) {
+ Ok((next_input, result)) => f(result).parse(next_input),
+ Err(err) => Err(err),
+ }
+}
+
+pub fn opt<'a, P, A>(opt_parser: P) -> impl Parser<'a, Option>
+where
+ P: Parser<'a, A>,
+{
+ move |input| match opt_parser.parse(input) {
+ Ok((next_input, result)) => Ok((next_input, Some(result))),
+ Err(_) => Ok((input, None)),
+ }
+}
+
+pub fn seq<'a, P1, P2, R1, R2>(parser1: P1, parser2: P2) -> impl Parser<'a, R2>
+where
+ P1: Parser<'a, R1>,
+ P2: Parser<'a, R2>,
+{
+ move |input| match parser1.parse(input) {
+ Ok((next_input, result)) => parser2.parse(next_input),
+ Err(e) => Err(e),
+ }
+}
diff --git a/ui/src/cache.rs b/ui/src/cache.rs
index cd0a1301..d2d64a95 100644
--- a/ui/src/cache.rs
+++ b/ui/src/cache.rs
@@ -27,7 +27,7 @@ use std::sync::RwLock;
*/
use melib::email::{Flag, UnixTimestamp};
-#[derive(Debug)]
+#[derive(Debug, PartialEq)]
pub enum Query {
Before(UnixTimestamp),
After(UnixTimestamp),
@@ -47,6 +47,9 @@ pub enum Query {
AllText(String),
/* * * * */
Flag(Flag),
+ And(Box, Box),
+ Or(Box, Box),
+ Not(Box),
}
/*
@@ -113,3 +116,212 @@ impl Cache {
}
}
*/
+impl std::ops::Not for Query {
+ type Output = Query;
+ fn not(self) -> Query {
+ match self {
+ Query::Not(q) => *q,
+ q => Query::Not(Box::new(q)),
+ }
+ }
+}
+
+impl std::ops::BitAnd for Query {
+ type Output = Self;
+
+ fn bitand(self, rhs: Self) -> Self {
+ Query::And(Box::new(self), Box::new(rhs))
+ }
+}
+
+impl std::ops::BitOr for Query {
+ type Output = Self;
+
+ fn bitor(self, rhs: Self) -> Self {
+ Query::Or(Box::new(self), Box::new(rhs))
+ }
+}
+
+pub mod query_parser {
+ use super::Query::{self, *};
+ use melib::parsec::*;
+
+ pub fn subject<'a>() -> impl Parser<'a, Query> {
+ prefix(
+ whitespace_wrap(match_literal("subject:")),
+ whitespace_wrap(literal()),
+ )
+ .map(|term| Query::Subject(term))
+ }
+
+ pub fn from<'a>() -> impl Parser<'a, Query> {
+ prefix(
+ whitespace_wrap(match_literal("from:")),
+ whitespace_wrap(literal()),
+ )
+ .map(|term| Query::From(term))
+ }
+
+ pub fn or<'a>() -> impl Parser<'a, Query> {
+ move |input| {
+ whitespace_wrap(match_literal_anycase("or"))
+ .parse(input)
+ .and_then(|(last_input, _)| query().parse(debug!(last_input)))
+ }
+ }
+
+ pub fn not<'a>() -> impl Parser<'a, Query> {
+ move |input| {
+ whitespace_wrap(either(
+ match_literal_anycase("or"),
+ match_literal_anycase("!"),
+ ))
+ .parse(input)
+ .and_then(|(last_input, _)| query().parse(debug!(last_input)))
+ }
+ }
+
+ pub fn and<'a>() -> impl Parser<'a, Query> {
+ move |input| {
+ whitespace_wrap(match_literal_anycase("and"))
+ .parse(input)
+ .and_then(|(last_input, _)| query().parse(debug!(last_input)))
+ }
+ }
+
+ pub fn literal<'a>() -> impl Parser<'a, String> {
+ move |input| either(quoted_string(), string()).parse(input)
+ }
+
+ pub fn parentheses_query<'a>() -> impl Parser<'a, Query> {
+ move |input| {
+ delimited(
+ whitespace_wrap(match_literal("(")),
+ whitespace_wrap(query()),
+ whitespace_wrap(match_literal(")")),
+ )
+ .parse(input)
+ }
+ }
+
+ pub fn query<'a>() -> impl Parser<'a, Query> {
+ move |input| {
+ let (rest, query_a): (&'a str, Query) = if let Ok(q) = parentheses_query().parse(input)
+ {
+ Ok(q)
+ } else if let Ok(q) = subject().parse(input) {
+ Ok(q)
+ } else if let Ok(q) = from().parse(input) {
+ Ok(q)
+ } else if let Ok(q) = not().parse(input) {
+ Ok(q)
+ } else if let Ok((rest, query_a)) = {
+ let result = literal().parse(input);
+ if result.is_ok()
+ && result
+ .as_ref()
+ .map(|(_, s)| s != "and" && s != "or" && s != "not")
+ .unwrap_or(false)
+ {
+ result.map(|(r, s)| (r, AllText(s)))
+ } else {
+ Err("")
+ }
+ } {
+ Ok((rest, query_a))
+ } else {
+ Err("")
+ }?;
+ if rest.is_empty() {
+ return Ok((rest, query_a));
+ }
+
+ if let Ok((rest, query_b)) = and().parse(rest) {
+ Ok((rest, And(Box::new(query_a), Box::new(query_b))))
+ } else if let Ok((rest, query_b)) = or().parse(rest) {
+ Ok((rest, Or(Box::new(query_a), Box::new(query_b))))
+ } else if let Ok((rest, query_b)) = query().parse(rest) {
+ Ok((rest, Or(Box::new(query_a), Box::new(query_b))))
+ } else {
+ Ok((rest, query_a))
+ }
+ }
+ }
+
+ #[test]
+ fn test_query_parsing() {
+ assert_eq!(
+ Err("subject: test and"),
+ query().parse_complete("subject: test and")
+ );
+ assert_eq!(
+ Ok((
+ "",
+ And(
+ Box::new(Subject("test".to_string())),
+ Box::new(AllText("i".to_string()))
+ )
+ )),
+ query().parse_complete("subject: test and i")
+ );
+ assert_eq!(
+ Ok(("", AllText("test".to_string()))),
+ query().parse_complete("test")
+ );
+ assert_eq!(
+ Ok(("", Subject("test".to_string()))),
+ query().parse_complete("subject: test")
+ );
+ assert_eq!(
+ Ok((
+ "",
+ Or(
+ Box::new(Subject("wah ah ah".to_string())),
+ Box::new(And(
+ Box::new(From("Manos".to_string())),
+ Box::new(From("Sia".to_string()))
+ ))
+ )
+ )),
+ query().parse_complete("subject: \"wah ah ah\" or (from: Manos and from: Sia)")
+ );
+ assert_eq!(
+ Ok((
+ "",
+ Or(
+ Box::new(Subject("wah".to_string())),
+ Box::new(And(
+ Box::new(From("Manos".to_string())),
+ Box::new(Or(
+ Box::new(Subject("foo".to_string())),
+ Box::new(Subject("bar".to_string())),
+ ))
+ ))
+ )
+ )),
+ query()
+ .parse_complete("subject: wah or (from: Manos and (subject:foo or subject: bar))")
+ );
+ assert_eq!(
+ Ok((
+ "",
+ And(
+ Box::new(From("Manos".to_string())),
+ Box::new(And(
+ Box::new(Or(
+ Box::new(Subject("foo".to_string())),
+ Box::new(Subject("bar".to_string()))
+ )),
+ Box::new(Or(
+ Box::new(From("woo".to_string())),
+ Box::new(From("my".to_string()))
+ ))
+ ))
+ )
+ )),
+ query().parse_complete(
+ "(from: Manos and (subject:foo or subject: bar) and (from:woo or from:my))"
+ )
+ );
+ }
+}
diff --git a/ui/src/search.rs b/ui/src/search.rs
deleted file mode 100644
index abc02515..00000000
--- a/ui/src/search.rs
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * meli - ui crate.
- *
- * Copyright 2017-2018 Manos Pitsidianakis
- *
- * This file is part of meli.
- *
- * meli is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * meli is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with meli. If not, see .
- */
-
-use crate::state::Context;
-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, sort, folder_hash)
- }
-
- #[cfg(not(feature = "sqlite3"))]
- {
- let mut ret = StackVec::new();
-
- let account = &context.accounts[account_idx];
- for env_hash in account.folders[folder_hash].as_result()?.envelopes {
- let envelope = &account.collection[&env_hash];
- if envelope.subject().contains(&filter_term) {
- ret.push(env_hash);
- continue;
- }
- if envelope.field_from_to_string().contains(&filter_term) {
- ret.push(env_hash);
- continue;
- }
- let op = account.operation(env_hash);
- let body = envelope.body(op)?;
- let decoded = decode_rec(&body, None);
- let body_text = String::from_utf8_lossy(&decoded);
- if body_text.contains(&filter_term) {
- ret.push(env_hash);
- }
- }
- ret
- }
-}