Add regex tag search (#1707)
This commit is contained in:
parent
4066518808
commit
5426164ebf
@ -134,7 +134,10 @@ impl SearchNode {
|
||||
|
||||
/// Construct [SearchNode] from an unescaped tag name.
|
||||
pub fn from_tag_name(name: &str) -> Self {
|
||||
Self::Tag(escape_anki_wildcards_for_search_node(name))
|
||||
Self::Tag {
|
||||
tag: escape_anki_wildcards_for_search_node(name),
|
||||
is_re: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct [SearchNode] from an unescaped notetype name.
|
||||
|
@ -64,7 +64,10 @@ pub enum SearchNode {
|
||||
days: u32,
|
||||
ease: RatingKind,
|
||||
},
|
||||
Tag(String),
|
||||
Tag {
|
||||
tag: String,
|
||||
is_re: bool,
|
||||
},
|
||||
Duplicates {
|
||||
notetype_id: NotetypeId,
|
||||
text: String,
|
||||
@ -311,7 +314,7 @@ fn search_node_for_text_with_argument<'a>(
|
||||
Ok(match key.to_ascii_lowercase().as_str() {
|
||||
"deck" => SearchNode::Deck(unescape(val)?),
|
||||
"note" => SearchNode::Notetype(unescape(val)?),
|
||||
"tag" => SearchNode::Tag(unescape(val)?),
|
||||
"tag" => parse_tag(val)?,
|
||||
"card" => parse_template(val)?,
|
||||
"flag" => parse_flag(val)?,
|
||||
"resched" => parse_resched(val)?,
|
||||
@ -334,6 +337,20 @@ fn search_node_for_text_with_argument<'a>(
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_tag(s: &str) -> ParseResult<SearchNode> {
|
||||
Ok(if let Some(re) = s.strip_prefix("re:") {
|
||||
SearchNode::Tag {
|
||||
tag: unescape_quotes(re),
|
||||
is_re: true,
|
||||
}
|
||||
} else {
|
||||
SearchNode::Tag {
|
||||
tag: unescape(s)?,
|
||||
is_re: false,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_template(s: &str) -> ParseResult<SearchNode> {
|
||||
Ok(SearchNode::CardTemplate(match s.parse::<u16>() {
|
||||
Ok(n) => TemplateKind::Ordinal(n.max(1) - 1),
|
||||
@ -820,7 +837,20 @@ mod test {
|
||||
);
|
||||
|
||||
assert_eq!(parse("note:basic")?, vec![Search(Notetype("basic".into()))]);
|
||||
assert_eq!(parse("tag:hard")?, vec![Search(Tag("hard".into()))]);
|
||||
assert_eq!(
|
||||
parse("tag:hard")?,
|
||||
vec![Search(Tag {
|
||||
tag: "hard".into(),
|
||||
is_re: false
|
||||
})]
|
||||
);
|
||||
assert_eq!(
|
||||
parse(r"tag:re:\\")?,
|
||||
vec![Search(Tag {
|
||||
tag: r"\\".into(),
|
||||
is_re: true
|
||||
})]
|
||||
);
|
||||
assert_eq!(
|
||||
parse("nid:1237123712,2,3")?,
|
||||
vec![Search(NoteIds("1237123712,2,3".into()))]
|
||||
|
@ -156,7 +156,7 @@ impl SqlWriter<'_> {
|
||||
SearchNode::Notetype(notetype) => self.write_notetype(&norm(notetype)),
|
||||
SearchNode::Rated { days, ease } => self.write_rated(">", -i64::from(*days), ease)?,
|
||||
|
||||
SearchNode::Tag(tag) => self.write_tag(&norm(tag)),
|
||||
SearchNode::Tag { tag, is_re } => self.write_tag(&norm(tag), *is_re),
|
||||
SearchNode::State(state) => self.write_state(state)?,
|
||||
SearchNode::Flag(flag) => {
|
||||
write!(self.sql, "(c.flags & 7) == {}", flag).unwrap();
|
||||
@ -199,17 +199,19 @@ impl SqlWriter<'_> {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn write_tag(&mut self, text: &str) {
|
||||
if text.contains(' ') {
|
||||
write!(self.sql, "false").unwrap();
|
||||
fn write_tag(&mut self, tag: &str, is_re: bool) {
|
||||
if is_re {
|
||||
self.args.push(format!("(?i){tag}"));
|
||||
write!(self.sql, "regexp_tags(?{}, n.tags)", self.args.len()).unwrap();
|
||||
} else {
|
||||
match text {
|
||||
match tag {
|
||||
"none" => {
|
||||
write!(self.sql, "n.tags = ''").unwrap();
|
||||
}
|
||||
"*" => {
|
||||
write!(self.sql, "true").unwrap();
|
||||
}
|
||||
s if s.contains(' ') => write!(self.sql, "false").unwrap(),
|
||||
text => {
|
||||
write!(self.sql, "n.tags regexp ?").unwrap();
|
||||
let re = &to_custom_re(text, r"\S");
|
||||
@ -660,7 +662,7 @@ impl SearchNode {
|
||||
|
||||
SearchNode::UnqualifiedText(_) => RequiredTable::Notes,
|
||||
SearchNode::SingleField { .. } => RequiredTable::Notes,
|
||||
SearchNode::Tag(_) => RequiredTable::Notes,
|
||||
SearchNode::Tag { .. } => RequiredTable::Notes,
|
||||
SearchNode::Duplicates { .. } => RequiredTable::Notes,
|
||||
SearchNode::Regex(_) => RequiredTable::Notes,
|
||||
SearchNode::NoCombining(_) => RequiredTable::Notes,
|
||||
@ -848,6 +850,13 @@ mod test {
|
||||
);
|
||||
assert_eq!(s(ctx, "tag:none"), ("(n.tags = '')".into(), vec![]));
|
||||
assert_eq!(s(ctx, "tag:*"), ("(true)".into(), vec![]));
|
||||
assert_eq!(
|
||||
s(ctx, "tag:re:.ne|tw."),
|
||||
(
|
||||
"(regexp_tags(?1, n.tags))".into(),
|
||||
vec!["(?i).ne|tw.".into()]
|
||||
)
|
||||
);
|
||||
|
||||
// state
|
||||
assert_eq!(
|
||||
|
@ -70,7 +70,7 @@ fn write_search_node(node: &SearchNode) -> String {
|
||||
NotetypeId(NotetypeIdType(i)) => format!("mid:{}", i),
|
||||
Notetype(s) => maybe_quote(&format!("note:{}", s)),
|
||||
Rated { days, ease } => write_rated(days, ease),
|
||||
Tag(s) => maybe_quote(&format!("tag:{}", s)),
|
||||
Tag { tag, is_re } => write_single_field("tag", tag, *is_re),
|
||||
Duplicates { notetype_id, text } => write_dupe(notetype_id, text),
|
||||
State(k) => write_state(k),
|
||||
Flag(u) => format!("flag:{}", u),
|
||||
@ -102,6 +102,7 @@ fn needs_quotation(txt: &str) -> bool {
|
||||
RE.is_match(txt)
|
||||
}
|
||||
|
||||
/// Also used by tag search, which has the same syntax.
|
||||
fn write_single_field(field: &str, text: &str, is_re: bool) -> String {
|
||||
let re = if is_re { "re:" } else { "" };
|
||||
let text = if !is_re && text.starts_with("re:") {
|
||||
|
@ -52,6 +52,7 @@ fn open_or_create_collection_db(path: &Path) -> Result<Connection> {
|
||||
add_field_index_function(&db)?;
|
||||
add_regexp_function(&db)?;
|
||||
add_regexp_fields_function(&db)?;
|
||||
add_regexp_tags_function(&db)?;
|
||||
add_without_combining_function(&db)?;
|
||||
add_fnvhash_function(&db)?;
|
||||
|
||||
@ -157,6 +158,26 @@ fn add_regexp_fields_function(db: &Connection) -> rusqlite::Result<()> {
|
||||
)
|
||||
}
|
||||
|
||||
/// Adds sql function `regexp_tags(regex, tags) -> is_match`.
|
||||
fn add_regexp_tags_function(db: &Connection) -> rusqlite::Result<()> {
|
||||
db.create_scalar_function(
|
||||
"regexp_tags",
|
||||
2,
|
||||
FunctionFlags::SQLITE_DETERMINISTIC,
|
||||
move |ctx| {
|
||||
assert_eq!(ctx.len(), 2, "called with unexpected number of arguments");
|
||||
|
||||
let re: Arc<Regex> = ctx
|
||||
.get_or_create_aux(0, |vr| -> std::result::Result<_, BoxError> {
|
||||
Ok(Regex::new(vr.as_str()?)?)
|
||||
})?;
|
||||
let mut tags = ctx.get_raw(1).as_str()?.split(' ');
|
||||
|
||||
Ok(tags.any(|tag| re.is_match(tag)))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Fetch schema version from database.
|
||||
/// Return (must_create, version)
|
||||
fn schema_version(db: &Connection) -> Result<(bool, u8)> {
|
||||
|
Loading…
Reference in New Issue
Block a user