Add FieldSearchMode enum to replace boolean fields

This commit is contained in:
Max Romanowski 2025-09-03 16:51:36 +02:00
parent c408f2bb62
commit 3df1e834a0
7 changed files with 65 additions and 61 deletions

View file

@ -74,10 +74,15 @@ message SearchNode {
repeated SearchNode nodes = 1; repeated SearchNode nodes = 1;
Joiner joiner = 2; Joiner joiner = 2;
} }
enum FieldSearchMode {
FIELD_SEARCH_NORMAL = 0;
FIELD_SEARCH_REGEX = 1;
FIELD_SEARCH_NOCOMBINING = 2;
}
message Field { message Field {
string field_name = 1; string field_name = 1;
string text = 2; string text = 2;
bool is_re = 3; FieldSearchMode mode = 3;
} }
oneof filter { oneof filter {

View file

@ -6,6 +6,7 @@ use std::mem;
use itertools::Itertools; use itertools::Itertools;
use super::writer::write_nodes; use super::writer::write_nodes;
use super::FieldSearchMode;
use super::Node; use super::Node;
use super::SearchNode; use super::SearchNode;
use super::StateKind; use super::StateKind;
@ -174,8 +175,7 @@ impl SearchNode {
pub fn from_tag_name(name: &str) -> Self { pub fn from_tag_name(name: &str) -> Self {
Self::Tag { Self::Tag {
tag: escape_anki_wildcards_for_search_node(name), tag: escape_anki_wildcards_for_search_node(name),
is_re: false, mode: FieldSearchMode::Normal,
is_nc: false,
} }
} }

View file

@ -13,6 +13,7 @@ pub use builder::JoinSearches;
pub use builder::Negated; pub use builder::Negated;
pub use builder::SearchBuilder; pub use builder::SearchBuilder;
pub use parser::parse as parse_search; pub use parser::parse as parse_search;
pub use parser::FieldSearchMode;
pub use parser::Node; pub use parser::Node;
pub use parser::PropertyKind; pub use parser::PropertyKind;
pub use parser::RatingKind; pub use parser::RatingKind;

View file

@ -48,6 +48,24 @@ pub enum Node {
Search(SearchNode), Search(SearchNode),
} }
#[derive(Copy, Debug, PartialEq, Eq, Clone)]
pub enum FieldSearchMode {
Normal,
Regex,
NoCombining,
}
impl From<i32> for FieldSearchMode {
fn from(val: i32) -> Self {
match val {
0 => Self::Normal,
1 => Self::Regex,
2 => Self::NoCombining,
_ => Self::Normal,
}
}
}
#[derive(Debug, PartialEq, Clone)] #[derive(Debug, PartialEq, Clone)]
pub enum SearchNode { pub enum SearchNode {
// text without a colon // text without a colon
@ -56,8 +74,7 @@ pub enum SearchNode {
SingleField { SingleField {
field: String, field: String,
text: String, text: String,
is_re: bool, mode: FieldSearchMode,
is_nc: bool,
}, },
AddedInDays(u32), AddedInDays(u32),
EditedInDays(u32), EditedInDays(u32),
@ -78,8 +95,7 @@ pub enum SearchNode {
}, },
Tag { Tag {
tag: String, tag: String,
is_re: bool, mode: FieldSearchMode,
is_nc: bool,
}, },
Duplicates { Duplicates {
notetype_id: NotetypeId, notetype_id: NotetypeId,
@ -375,14 +391,12 @@ fn parse_tag(s: &str) -> ParseResult<'_, SearchNode> {
Ok(if let Some(re) = s.strip_prefix("re:") { Ok(if let Some(re) = s.strip_prefix("re:") {
SearchNode::Tag { SearchNode::Tag {
tag: unescape_quotes(re), tag: unescape_quotes(re),
is_re: true, mode: FieldSearchMode::Regex,
is_nc: false,
} }
} else { } else {
SearchNode::Tag { SearchNode::Tag {
tag: unescape(s)?, tag: unescape(s)?,
is_re: false, mode: FieldSearchMode::Normal,
is_nc: false,
} }
}) })
} }
@ -674,22 +688,19 @@ fn parse_single_field<'a>(key: &'a str, val: &'a str) -> ParseResult<'a, SearchN
SearchNode::SingleField { SearchNode::SingleField {
field: unescape(key)?, field: unescape(key)?,
text: unescape_quotes(stripped), text: unescape_quotes(stripped),
is_re: true, mode: FieldSearchMode::Regex,
is_nc: false,
} }
} else if let Some(stripped) = val.strip_prefix("nc:") { } else if let Some(stripped) = val.strip_prefix("nc:") {
SearchNode::SingleField { SearchNode::SingleField {
field: unescape(key)?, field: unescape(key)?,
text: unescape_quotes(stripped), text: unescape_quotes(stripped),
is_re: false, mode: FieldSearchMode::NoCombining,
is_nc: true,
} }
} else { } else {
SearchNode::SingleField { SearchNode::SingleField {
field: unescape(key)?, field: unescape(key)?,
text: unescape(val)?, text: unescape(val)?,
is_re: false, mode: FieldSearchMode::Normal,
is_nc: false,
} }
}) })
} }
@ -819,8 +830,7 @@ mod test {
Search(SingleField { Search(SingleField {
field: "foo".into(), field: "foo".into(),
text: "bar baz".into(), text: "bar baz".into(),
is_re: false, mode: FieldSearchMode::Normal,
is_nc: false,
}) })
]))), ]))),
Or, Or,
@ -833,8 +843,7 @@ mod test {
vec![Search(SingleField { vec![Search(SingleField {
field: "foo".into(), field: "foo".into(),
text: "bar".into(), text: "bar".into(),
is_re: true, mode: FieldSearchMode::Regex,
is_nc: false
})] })]
); );
@ -843,8 +852,7 @@ mod test {
vec![Search(SingleField { vec![Search(SingleField {
field: "foo".into(), field: "foo".into(),
text: "bar".into(), text: "bar".into(),
is_re: false, mode: FieldSearchMode::NoCombining,
is_nc: true
})] })]
); );
@ -854,8 +862,7 @@ mod test {
vec![Search(SingleField { vec![Search(SingleField {
field: "field".into(), field: "field".into(),
text: "va\"lue".into(), text: "va\"lue".into(),
is_re: false, mode: FieldSearchMode::Normal,
is_nc: false
})] })]
); );
assert_eq!(parse(r#""field:va\"lue""#)?, parse(r#"field:"va\"lue""#)?,); assert_eq!(parse(r#""field:va\"lue""#)?, parse(r#"field:"va\"lue""#)?,);
@ -932,16 +939,14 @@ mod test {
parse("tag:hard")?, parse("tag:hard")?,
vec![Search(Tag { vec![Search(Tag {
tag: "hard".into(), tag: "hard".into(),
is_re: false, mode: FieldSearchMode::Normal
is_nc: false
})] })]
); );
assert_eq!( assert_eq!(
parse(r"tag:re:\\")?, parse(r"tag:re:\\")?,
vec![Search(Tag { vec![Search(Tag {
tag: r"\\".into(), tag: r"\\".into(),
is_re: true, mode: FieldSearchMode::Regex
is_nc: false
})] })]
); );
assert_eq!( assert_eq!(

View file

@ -6,6 +6,7 @@ use itertools::Itertools;
use crate::prelude::*; use crate::prelude::*;
use crate::search::parse_search; use crate::search::parse_search;
use crate::search::FieldSearchMode;
use crate::search::Negated; use crate::search::Negated;
use crate::search::Node; use crate::search::Node;
use crate::search::PropertyKind; use crate::search::PropertyKind;
@ -40,8 +41,7 @@ impl TryFrom<anki_proto::search::SearchNode> for Node {
Filter::FieldName(s) => Node::Search(SearchNode::SingleField { Filter::FieldName(s) => Node::Search(SearchNode::SingleField {
field: escape_anki_wildcards_for_search_node(&s), field: escape_anki_wildcards_for_search_node(&s),
text: "_*".to_string(), text: "_*".to_string(),
is_re: false, mode: FieldSearchMode::Normal,
is_nc: false,
}), }),
Filter::Rated(rated) => Node::Search(SearchNode::Rated { Filter::Rated(rated) => Node::Search(SearchNode::Rated {
days: rated.days, days: rated.days,
@ -108,8 +108,7 @@ impl TryFrom<anki_proto::search::SearchNode> for Node {
Filter::Field(field) => Node::Search(SearchNode::SingleField { Filter::Field(field) => Node::Search(SearchNode::SingleField {
field: escape_anki_wildcards(&field.field_name), field: escape_anki_wildcards(&field.field_name),
text: escape_anki_wildcards(&field.text), text: escape_anki_wildcards(&field.text),
is_re: field.is_re, mode: field.mode.into(),
is_nc: false,
}), }),
Filter::LiteralText(text) => { Filter::LiteralText(text) => {
let text = escape_anki_wildcards(&text); let text = escape_anki_wildcards(&text);

View file

@ -7,6 +7,7 @@ use std::ops::Range;
use itertools::Itertools; use itertools::Itertools;
use super::parser::FieldSearchMode;
use super::parser::Node; use super::parser::Node;
use super::parser::PropertyKind; use super::parser::PropertyKind;
use super::parser::RatingKind; use super::parser::RatingKind;
@ -138,12 +139,9 @@ impl SqlWriter<'_> {
false, false,
)? )?
} }
SearchNode::SingleField { SearchNode::SingleField { field, text, mode } => {
field, self.write_field(&norm(field), &self.norm_note(text), *mode)?
text, }
is_re,
is_nc,
} => self.write_field(&norm(field), &self.norm_note(text), *is_re, *is_nc)?,
SearchNode::Duplicates { notetype_id, text } => { SearchNode::Duplicates { notetype_id, text } => {
self.write_dupe(*notetype_id, &self.norm_note(text))? self.write_dupe(*notetype_id, &self.norm_note(text))?
} }
@ -183,7 +181,7 @@ impl SqlWriter<'_> {
SearchNode::Notetype(notetype) => self.write_notetype(&norm(notetype)), SearchNode::Notetype(notetype) => self.write_notetype(&norm(notetype)),
SearchNode::Rated { days, ease } => self.write_rated(">", -i64::from(*days), ease)?, SearchNode::Rated { days, ease } => self.write_rated(">", -i64::from(*days), ease)?,
SearchNode::Tag { tag, is_re, is_nc } => self.write_tag(&norm(tag), *is_re, *is_nc), SearchNode::Tag { tag, mode } => self.write_tag(&norm(tag), *mode),
SearchNode::State(state) => self.write_state(state)?, SearchNode::State(state) => self.write_state(state)?,
SearchNode::Flag(flag) => { SearchNode::Flag(flag) => {
write!(self.sql, "(c.flags & 7) == {flag}").unwrap(); write!(self.sql, "(c.flags & 7) == {flag}").unwrap();
@ -299,8 +297,8 @@ impl SqlWriter<'_> {
Ok(()) Ok(())
} }
fn write_tag(&mut self, tag: &str, is_re: bool, _is_nc: bool) { fn write_tag(&mut self, tag: &str, mode: FieldSearchMode) {
if is_re { if mode == FieldSearchMode::Regex {
self.args.push(format!("(?i){tag}")); self.args.push(format!("(?i){tag}"));
write!(self.sql, "regexp_tags(?{}, n.tags)", self.args.len()).unwrap(); write!(self.sql, "regexp_tags(?{}, n.tags)", self.args.len()).unwrap();
} else { } else {
@ -570,17 +568,17 @@ impl SqlWriter<'_> {
} }
} }
fn write_field(&mut self, field_name: &str, val: &str, is_re: bool, is_nc: bool) -> Result<()> { fn write_field(&mut self, field_name: &str, val: &str, mode: FieldSearchMode) -> Result<()> {
if matches!(field_name, "*" | "_*" | "*_") { if matches!(field_name, "*" | "_*" | "*_") {
if is_re { if mode == FieldSearchMode::Regex {
self.write_all_fields_regexp(val); self.write_all_fields_regexp(val);
} else { } else {
self.write_all_fields(val); self.write_all_fields(val);
} }
Ok(()) Ok(())
} else if is_re { } else if mode == FieldSearchMode::Regex {
self.write_single_field_regexp(field_name, val) self.write_single_field_regexp(field_name, val)
} else if is_nc { } else if mode == FieldSearchMode::NoCombining {
self.write_single_field_nc(field_name, val) self.write_single_field_nc(field_name, val)
} else { } else {
self.write_single_field(field_name, val) self.write_single_field(field_name, val)

View file

@ -9,6 +9,7 @@ use regex::Regex;
use crate::notetype::NotetypeId as NotetypeIdType; use crate::notetype::NotetypeId as NotetypeIdType;
use crate::prelude::*; use crate::prelude::*;
use crate::search::parser::parse; use crate::search::parser::parse;
use crate::search::parser::FieldSearchMode;
use crate::search::parser::Node; use crate::search::parser::Node;
use crate::search::parser::PropertyKind; use crate::search::parser::PropertyKind;
use crate::search::parser::RatingKind; use crate::search::parser::RatingKind;
@ -69,12 +70,7 @@ fn write_search_node(node: &SearchNode) -> String {
use SearchNode::*; use SearchNode::*;
match node { match node {
UnqualifiedText(s) => maybe_quote(&s.replace(':', "\\:")), UnqualifiedText(s) => maybe_quote(&s.replace(':', "\\:")),
SingleField { SingleField { field, text, mode } => write_single_field(field, text, *mode),
field,
text,
is_re,
is_nc,
} => write_single_field(field, text, *is_re, *is_nc),
AddedInDays(u) => format!("added:{u}"), AddedInDays(u) => format!("added:{u}"),
EditedInDays(u) => format!("edited:{u}"), EditedInDays(u) => format!("edited:{u}"),
IntroducedInDays(u) => format!("introduced:{u}"), IntroducedInDays(u) => format!("introduced:{u}"),
@ -86,7 +82,7 @@ fn write_search_node(node: &SearchNode) -> String {
NotetypeId(NotetypeIdType(i)) => format!("mid:{i}"), NotetypeId(NotetypeIdType(i)) => format!("mid:{i}"),
Notetype(s) => maybe_quote(&format!("note:{s}")), Notetype(s) => maybe_quote(&format!("note:{s}")),
Rated { days, ease } => write_rated(days, ease), Rated { days, ease } => write_rated(days, ease),
Tag { tag, is_re, is_nc } => write_single_field("tag", tag, *is_re, *is_nc), Tag { tag, mode } => write_single_field("tag", tag, *mode),
Duplicates { notetype_id, text } => write_dupe(notetype_id, text), Duplicates { notetype_id, text } => write_dupe(notetype_id, text),
State(k) => write_state(k), State(k) => write_state(k),
Flag(u) => format!("flag:{u}"), Flag(u) => format!("flag:{u}"),
@ -121,15 +117,15 @@ fn needs_quotation(txt: &str) -> bool {
} }
/// Also used by tag search, which has the same syntax. /// Also used by tag search, which has the same syntax.
fn write_single_field(field: &str, text: &str, is_re: bool, is_nc: bool) -> String { fn write_single_field(field: &str, text: &str, mode: FieldSearchMode) -> String {
let prefix = if is_re { let prefix = match mode {
"re:" FieldSearchMode::Normal => "",
} else if is_nc { FieldSearchMode::Regex => "re:",
"nc:" FieldSearchMode::NoCombining => "nc:",
} else {
""
}; };
let text = if !is_re && !is_nc && (text.starts_with("re:") || text.starts_with("ne:")) { let text = if mode == FieldSearchMode::Normal
&& (text.starts_with("re:") || text.starts_with("nc:"))
{
text.replacen(':', "\\:", 1) text.replacen(':', "\\:", 1)
} else { } else {
text.to_string() text.to_string()