From f4cfccfc0aa3f37a9d3f6fed5b0166ba76b1c769 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Tue, 12 Jan 2021 23:17:57 +0100 Subject: [PATCH 1/4] Fix writing dupe nodes Rename comments and dupe functions on the backend accordingly to avoid further confusion. --- rslib/src/search/parser.rs | 6 +++--- rslib/src/search/sqlwriter.rs | 4 ++-- rslib/src/search/writer.rs | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/rslib/src/search/parser.rs b/rslib/src/search/parser.rs index 4197e19ed..1ba1328ea 100644 --- a/rslib/src/search/parser.rs +++ b/rslib/src/search/parser.rs @@ -293,8 +293,8 @@ fn search_node_for_text_with_argument<'a>( "is" => parse_state(val)?, "flag" => parse_flag(val)?, "rated" => parse_rated(val)?, + "dupe" => parse_dupe(val)?, "resched" => parse_resched(val)?, - "dupe" => parse_dupes(val)?, "prop" => parse_prop(val)?, "re" => SearchNode::Regex(unescape_quotes(val)), "nc" => SearchNode::NoCombining(unescape(val)?), @@ -392,8 +392,8 @@ fn parse_resched(val: &str) -> ParseResult> { Ok(SearchNode::Rated { days, ease }) } -/// eg dupes:1231,hello -fn parse_dupes(val: &str) -> ParseResult { +/// eg dupe:1231,hello +fn parse_dupe(val: &str) -> ParseResult { let mut it = val.splitn(2, ','); let mid: NoteTypeID = it.next().unwrap().parse()?; let text = it.next().ok_or(ParseError {})?; diff --git a/rslib/src/search/sqlwriter.rs b/rslib/src/search/sqlwriter.rs index dc39999d4..4e2faf076 100644 --- a/rslib/src/search/sqlwriter.rs +++ b/rslib/src/search/sqlwriter.rs @@ -123,7 +123,7 @@ impl SqlWriter<'_> { self.write_single_field(&norm(field), &self.norm_note(text), *is_re)? } SearchNode::Duplicates { note_type_id, text } => { - self.write_dupes(*note_type_id, &self.norm_note(text))? + self.write_dupe(*note_type_id, &self.norm_note(text))? } SearchNode::Regex(re) => self.write_regex(&self.norm_note(re)), SearchNode::NoCombining(text) => self.write_no_combining(&self.norm_note(text)), @@ -443,7 +443,7 @@ impl SqlWriter<'_> { Ok(()) } - fn write_dupes(&mut self, ntid: NoteTypeID, text: &str) -> Result<()> { + fn write_dupe(&mut self, ntid: NoteTypeID, text: &str) -> Result<()> { let text_nohtml = strip_html_preserving_media_filenames(text); let csum = field_checksum(text_nohtml.as_ref()); diff --git a/rslib/src/search/writer.rs b/rslib/src/search/writer.rs index 0b67224f7..8c2199c8a 100644 --- a/rslib/src/search/writer.rs +++ b/rslib/src/search/writer.rs @@ -119,7 +119,7 @@ fn write_search_node(node: &SearchNode) -> String { NoteType(s) => quote(&format!("note:{}", s)), Rated { days, ease } => write_rated(days, ease), Tag(s) => quote(&format!("tag:{}", s)), - Duplicates { note_type_id, text } => quote(&format!("dupes:{},{}", note_type_id, text)), + Duplicates { note_type_id, text } => quote(&format!("dupe:{},{}", note_type_id, text)), State(k) => write_state(k), Flag(u) => format!("\"flag:{}\"", u), NoteIDs(s) => format!("\"nid:{}\"", s), From d9bb20ac2427ee121c01ce1f2e9afa5d75c700de Mon Sep 17 00:00:00 2001 From: RumovZ Date: Wed, 13 Jan 2021 11:54:49 +0100 Subject: [PATCH 2/4] Expect backslashes to be escaped in `dupe:` text --- rslib/src/search/parser.rs | 11 ++++++++++- rslib/src/search/writer.rs | 8 +++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/rslib/src/search/parser.rs b/rslib/src/search/parser.rs index 1ba1328ea..a42d5cd84 100644 --- a/rslib/src/search/parser.rs +++ b/rslib/src/search/parser.rs @@ -399,7 +399,7 @@ fn parse_dupe(val: &str) -> ParseResult { let text = it.next().ok_or(ParseError {})?; Ok(SearchNode::Duplicates { note_type_id: mid, - text: unescape_quotes(text), + text: unescape_quotes_and_backslashes(text), }) } @@ -478,6 +478,15 @@ fn unescape_quotes(s: &str) -> Cow { } } +/// For non-globs like dupe text without any assumption about the content +fn unescape_quotes_and_backslashes(s: &str) -> Cow { + if s.contains('"') || s.contains('\\') { + s.replace(r#"\""#, "\"").replace(r"\\", r"\").into() + } else { + s.into() + } +} + /// Unescape chars with special meaning to the parser. fn unescape(txt: &str) -> ParseResult> { if is_invalid_escape(txt) { diff --git a/rslib/src/search/writer.rs b/rslib/src/search/writer.rs index 8c2199c8a..94215697e 100644 --- a/rslib/src/search/writer.rs +++ b/rslib/src/search/writer.rs @@ -119,7 +119,7 @@ fn write_search_node(node: &SearchNode) -> String { NoteType(s) => quote(&format!("note:{}", s)), Rated { days, ease } => write_rated(days, ease), Tag(s) => quote(&format!("tag:{}", s)), - Duplicates { note_type_id, text } => quote(&format!("dupe:{},{}", note_type_id, text)), + Duplicates { note_type_id, text } => write_dupe(note_type_id, text), State(k) => write_state(k), Flag(u) => format!("\"flag:{}\"", u), NoteIDs(s) => format!("\"nid:{}\"", s), @@ -163,6 +163,12 @@ fn write_rated(days: &u32, ease: &EaseKind) -> String { } } +/// Escape double quotes and backslashes: \" +fn write_dupe(note_type_id: &NoteTypeIDType, text: &str) -> String { + let esc = text.replace(r"\", r"\\").replace('"', r#"\""#); + format!("\"dupe:{},{}\"", note_type_id, esc) +} + fn write_state(kind: &StateKind) -> String { use StateKind::*; format!( From 6d9cfb85abe14b2b63d2a1289e1b7c251e9b99df Mon Sep 17 00:00:00 2001 From: RumovZ Date: Wed, 13 Jan 2021 11:57:41 +0100 Subject: [PATCH 3/4] Add pb message for dupe filter --- rslib/backend.proto | 5 +++++ rslib/src/backend/mod.rs | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/rslib/backend.proto b/rslib/backend.proto index 434241e1a..2a5e213b3 100644 --- a/rslib/backend.proto +++ b/rslib/backend.proto @@ -775,12 +775,17 @@ message FilterToSearchIn { NO_FLAG = 15; ANY_FLAG = 16; } + message DupeIn { + NoteTypeID mid = 1; + string text = 2; + } oneof filter { NamedFilter name = 1; string tag = 2; string deck = 3; string note = 4; uint32 template = 5; + DupeIn dupe = 6; } } diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index ea8a90a26..10c7c5e00 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -327,6 +327,10 @@ impl From for Node<'_> { Filter::Template(u) => { Node::Search(SearchNode::CardTemplate(TemplateKind::Ordinal(u as u16))) } + Filter::Dupe(dupe) => Node::Search(SearchNode::Duplicates { + note_type_id: dupe.mid.unwrap_or(pb::NoteTypeId { ntid: 0 }).into(), + text: dupe.text.into(), + }), } } } From 7b9cc017c431fae9090bdfb4c7eaee734a02b55d Mon Sep 17 00:00:00 2001 From: RumovZ Date: Wed, 13 Jan 2021 11:58:59 +0100 Subject: [PATCH 4/4] Use backend to set dupe filter --- pylib/anki/rsbackend.py | 2 ++ qt/aqt/browser.py | 13 +++++++++++++ qt/aqt/editor.py | 6 +----- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/pylib/anki/rsbackend.py b/pylib/anki/rsbackend.py index 770fefe11..88316b23f 100644 --- a/pylib/anki/rsbackend.py +++ b/pylib/anki/rsbackend.py @@ -48,6 +48,8 @@ DeckTreeNode = pb.DeckTreeNode StockNoteType = pb.StockNoteType FilterToSearchIn = pb.FilterToSearchIn NamedFilter = pb.FilterToSearchIn.NamedFilter +DupeIn = pb.FilterToSearchIn.DupeIn +BackendNoteTypeID = pb.NoteTypeID ConcatSeparator = pb.ConcatenateSearchesIn.Separator SyncAuth = pb.SyncAuth SyncOutput = pb.SyncCollectionOut diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index 0cdadce6b..6ecacc6d0 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -21,8 +21,10 @@ from anki.lang import without_unicode_isolation from anki.models import NoteType from anki.notes import Note from anki.rsbackend import ( + BackendNoteTypeID, ConcatSeparator, DeckTreeNode, + DupeIn, FilterToSearchIn, InvalidInput, NamedFilter, @@ -2016,6 +2018,17 @@ where id in %s""" # Edit: finding dupes ###################################################################### + # filter called by the editor + def search_dupe(self, mid: int, text: str): + self.form.searchEdit.lineEdit().setText( + self.col.backend.filter_to_search( + FilterToSearchIn( + dupe=DupeIn(mid=BackendNoteTypeID(ntid=mid), text=text) + ) + ) + ) + self.onSearchActivated() + def onFindDupes(self): self.editor.saveNow(self._onFindDupes) diff --git a/qt/aqt/editor.py b/qt/aqt/editor.py index 3f69f068e..bbe3a4add 100644 --- a/qt/aqt/editor.py +++ b/qt/aqt/editor.py @@ -537,12 +537,8 @@ class Editor: self.web.eval("setBackgrounds(%s);" % json.dumps(cols)) def showDupes(self): - contents = self.note.fields[0].replace('"', r"\"") browser = aqt.dialogs.open("Browser", self.mw) - browser.form.searchEdit.lineEdit().setText( - '"dupe:%s,%s"' % (self.note.model()["id"], contents) - ) - browser.onSearchActivated() + browser.search_dupe(self.note.model()["id"], self.note.fields[0]) def fieldsAreBlank(self, previousNote=None): if not self.note: