2020-05-05 12:50:17 +02:00
|
|
|
// Copyright: Ankitects Pty Ltd and contributors
|
|
|
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
|
|
|
2021-04-18 10:29:20 +02:00
|
|
|
use std::borrow::Cow;
|
|
|
|
|
|
|
|
use regex::Regex;
|
|
|
|
|
2020-05-05 12:50:17 +02:00
|
|
|
use crate::{
|
|
|
|
collection::Collection,
|
2021-04-01 08:06:24 +02:00
|
|
|
error::{AnkiError, Result},
|
2021-03-27 10:53:33 +01:00
|
|
|
notes::{NoteId, TransformNoteOutput},
|
2021-03-07 14:43:18 +01:00
|
|
|
prelude::*,
|
2020-05-06 12:06:42 +02:00
|
|
|
text::normalize_to_nfc,
|
2020-05-05 12:50:17 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
pub struct FindReplaceContext {
|
2021-03-27 10:53:33 +01:00
|
|
|
nids: Vec<NoteId>,
|
2020-05-05 12:50:17 +02:00
|
|
|
search: Regex,
|
|
|
|
replacement: String,
|
|
|
|
field_name: Option<String>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl FindReplaceContext {
|
|
|
|
pub fn new(
|
2021-03-27 10:53:33 +01:00
|
|
|
nids: Vec<NoteId>,
|
2020-05-05 12:50:17 +02:00
|
|
|
search_re: &str,
|
|
|
|
repl: impl Into<String>,
|
|
|
|
field_name: Option<String>,
|
|
|
|
) -> Result<Self> {
|
|
|
|
Ok(FindReplaceContext {
|
|
|
|
nids,
|
|
|
|
search: Regex::new(search_re).map_err(|_| AnkiError::invalid_input("invalid regex"))?,
|
|
|
|
replacement: repl.into(),
|
|
|
|
field_name,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
fn replace_text<'a>(&self, text: &'a str) -> Cow<'a, str> {
|
|
|
|
self.search.replace_all(text, self.replacement.as_str())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Collection {
|
2020-05-06 12:06:42 +02:00
|
|
|
pub fn find_and_replace(
|
|
|
|
&mut self,
|
2021-03-27 10:53:33 +01:00
|
|
|
nids: Vec<NoteId>,
|
2020-05-06 12:06:42 +02:00
|
|
|
search_re: &str,
|
|
|
|
repl: &str,
|
|
|
|
field_name: Option<String>,
|
undoable ops now return changes directly; add new *_ops.py files
- Introduced a new transact() method that wraps the return value
in a separate struct that describes the changes that were made.
- Changes are now gathered from the undo log, so we don't need to
guess at what was changed - eg if update_note() is called with identical
note contents, no changes are returned. Card changes will only be set
if cards were actually generated by the update_note() call, and tag
will only be set if a new tag was added.
- mw.perform_op() has been updated to expect the op to return the changes,
or a structure with the changes in it, and it will use them to fire the
change hook, instead of fetching the changes from undo_status(), so there
is no risk of race conditions.
- the various calls to mw.perform_op() have been split into separate
files like card_ops.py. Aside from making the code cleaner, this works
around a rather annoying issue with mypy. Because we run it with
no_strict_optional, mypy is happy to accept an operation that returns None,
despite the type signature saying it requires changes to be returned.
Turning no_strict_optional on for the whole codebase is not practical
at the moment, but we can enable it for individual files.
Still todo:
- The cursor keeps moving back to the start of a field when typing -
we need to ignore the refresh hook when we are the initiator.
- The busy cursor icon should probably be delayed a few hundreds ms.
- Still need to think about a nicer way of handling saveNow()
- op_made_changes(), op_affects_study_queue() might be better embedded
as properties in the object instead
2021-03-16 05:26:42 +01:00
|
|
|
) -> Result<OpOutput<usize>> {
|
|
|
|
self.transact(Op::FindAndReplace, |col| {
|
2021-03-07 14:43:18 +01:00
|
|
|
let norm = col.get_bool(BoolKey::NormalizeNoteText);
|
2020-05-06 12:06:42 +02:00
|
|
|
let search = if norm {
|
|
|
|
normalize_to_nfc(search_re)
|
|
|
|
} else {
|
|
|
|
search_re.into()
|
|
|
|
};
|
|
|
|
let ctx = FindReplaceContext::new(nids, &search, repl, field_name)?;
|
2020-05-07 09:54:23 +02:00
|
|
|
col.find_and_replace_inner(ctx)
|
2020-05-06 12:06:42 +02:00
|
|
|
})
|
2020-05-05 12:50:17 +02:00
|
|
|
}
|
|
|
|
|
2020-05-07 09:54:23 +02:00
|
|
|
fn find_and_replace_inner(&mut self, ctx: FindReplaceContext) -> Result<usize> {
|
|
|
|
let mut last_ntid = None;
|
|
|
|
let mut field_ord = None;
|
|
|
|
self.transform_notes(&ctx.nids, |note, nt| {
|
|
|
|
if last_ntid != Some(nt.id) {
|
|
|
|
field_ord = ctx.field_name.as_ref().and_then(|n| nt.get_field_ord(n));
|
|
|
|
last_ntid = Some(nt.id);
|
|
|
|
}
|
|
|
|
|
|
|
|
let mut changed = false;
|
|
|
|
match field_ord {
|
|
|
|
None => {
|
|
|
|
// all fields
|
2021-03-02 10:02:00 +01:00
|
|
|
for txt in note.fields_mut() {
|
2020-05-07 09:54:23 +02:00
|
|
|
if let Cow::Owned(otxt) = ctx.replace_text(txt) {
|
|
|
|
changed = true;
|
|
|
|
*txt = otxt;
|
2020-05-05 12:50:17 +02:00
|
|
|
}
|
|
|
|
}
|
2020-05-07 09:54:23 +02:00
|
|
|
}
|
|
|
|
Some(ord) => {
|
|
|
|
// single field
|
2021-03-02 10:02:00 +01:00
|
|
|
if let Some(txt) = note.fields_mut().get_mut(ord) {
|
2020-05-07 09:54:23 +02:00
|
|
|
if let Cow::Owned(otxt) = ctx.replace_text(txt) {
|
|
|
|
changed = true;
|
|
|
|
*txt = otxt;
|
2020-05-05 12:50:17 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-05-07 09:54:23 +02:00
|
|
|
Ok(TransformNoteOutput {
|
|
|
|
changed,
|
|
|
|
generate_cards: true,
|
|
|
|
mark_modified: true,
|
|
|
|
})
|
|
|
|
})
|
2020-05-05 12:50:17 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod test {
|
|
|
|
use super::*;
|
2021-03-27 10:53:33 +01:00
|
|
|
use crate::{collection::open_test_collection, decks::DeckId};
|
2020-05-05 12:50:17 +02:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn findreplace() -> Result<()> {
|
|
|
|
let mut col = open_test_collection();
|
|
|
|
|
|
|
|
let nt = col.get_notetype_by_name("Basic")?.unwrap();
|
|
|
|
let mut note = nt.new_note();
|
2021-03-02 10:02:00 +01:00
|
|
|
note.set_field(0, "one aaa")?;
|
|
|
|
note.set_field(1, "two aaa")?;
|
2021-03-27 10:53:33 +01:00
|
|
|
col.add_note(&mut note, DeckId(1))?;
|
2020-05-05 12:50:17 +02:00
|
|
|
|
|
|
|
let nt = col.get_notetype_by_name("Cloze")?.unwrap();
|
|
|
|
let mut note2 = nt.new_note();
|
2021-03-02 10:02:00 +01:00
|
|
|
note2.set_field(0, "three aaa")?;
|
2021-03-27 10:53:33 +01:00
|
|
|
col.add_note(&mut note2, DeckId(1))?;
|
2020-05-05 12:50:17 +02:00
|
|
|
|
2020-05-19 03:27:02 +02:00
|
|
|
let nids = col.search_notes("")?;
|
undoable ops now return changes directly; add new *_ops.py files
- Introduced a new transact() method that wraps the return value
in a separate struct that describes the changes that were made.
- Changes are now gathered from the undo log, so we don't need to
guess at what was changed - eg if update_note() is called with identical
note contents, no changes are returned. Card changes will only be set
if cards were actually generated by the update_note() call, and tag
will only be set if a new tag was added.
- mw.perform_op() has been updated to expect the op to return the changes,
or a structure with the changes in it, and it will use them to fire the
change hook, instead of fetching the changes from undo_status(), so there
is no risk of race conditions.
- the various calls to mw.perform_op() have been split into separate
files like card_ops.py. Aside from making the code cleaner, this works
around a rather annoying issue with mypy. Because we run it with
no_strict_optional, mypy is happy to accept an operation that returns None,
despite the type signature saying it requires changes to be returned.
Turning no_strict_optional on for the whole codebase is not practical
at the moment, but we can enable it for individual files.
Still todo:
- The cursor keeps moving back to the start of a field when typing -
we need to ignore the refresh hook when we are the initiator.
- The busy cursor icon should probably be delayed a few hundreds ms.
- Still need to think about a nicer way of handling saveNow()
- op_made_changes(), op_affects_study_queue() might be better embedded
as properties in the object instead
2021-03-16 05:26:42 +01:00
|
|
|
let out = col.find_and_replace(nids.clone(), "(?i)AAA", "BBB", None)?;
|
|
|
|
assert_eq!(out.output, 2);
|
2020-05-05 12:50:17 +02:00
|
|
|
|
|
|
|
let note = col.storage.get_note(note.id)?.unwrap();
|
|
|
|
// but the update should be limited to the specified field when it was available
|
2021-03-02 10:02:00 +01:00
|
|
|
assert_eq!(¬e.fields()[..], &["one BBB", "two BBB"]);
|
2020-05-05 12:50:17 +02:00
|
|
|
|
|
|
|
let note2 = col.storage.get_note(note2.id)?.unwrap();
|
2021-03-02 10:02:00 +01:00
|
|
|
assert_eq!(¬e2.fields()[..], &["three BBB", ""]);
|
2020-05-05 12:50:17 +02:00
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
col.storage.field_names_for_notes(&nids)?,
|
2020-05-07 00:31:49 +02:00
|
|
|
vec![
|
|
|
|
"Back".to_string(),
|
|
|
|
"Back Extra".into(),
|
|
|
|
"Front".into(),
|
|
|
|
"Text".into()
|
|
|
|
]
|
2020-05-05 12:50:17 +02:00
|
|
|
);
|
undoable ops now return changes directly; add new *_ops.py files
- Introduced a new transact() method that wraps the return value
in a separate struct that describes the changes that were made.
- Changes are now gathered from the undo log, so we don't need to
guess at what was changed - eg if update_note() is called with identical
note contents, no changes are returned. Card changes will only be set
if cards were actually generated by the update_note() call, and tag
will only be set if a new tag was added.
- mw.perform_op() has been updated to expect the op to return the changes,
or a structure with the changes in it, and it will use them to fire the
change hook, instead of fetching the changes from undo_status(), so there
is no risk of race conditions.
- the various calls to mw.perform_op() have been split into separate
files like card_ops.py. Aside from making the code cleaner, this works
around a rather annoying issue with mypy. Because we run it with
no_strict_optional, mypy is happy to accept an operation that returns None,
despite the type signature saying it requires changes to be returned.
Turning no_strict_optional on for the whole codebase is not practical
at the moment, but we can enable it for individual files.
Still todo:
- The cursor keeps moving back to the start of a field when typing -
we need to ignore the refresh hook when we are the initiator.
- The busy cursor icon should probably be delayed a few hundreds ms.
- Still need to think about a nicer way of handling saveNow()
- op_made_changes(), op_affects_study_queue() might be better embedded
as properties in the object instead
2021-03-16 05:26:42 +01:00
|
|
|
let out = col.find_and_replace(nids, "BBB", "ccc", Some("Front".into()))?;
|
2020-05-05 12:50:17 +02:00
|
|
|
// still 2, as the caller is expected to provide only note ids that have
|
|
|
|
// that field, and if we can't find the field we fall back on all fields
|
undoable ops now return changes directly; add new *_ops.py files
- Introduced a new transact() method that wraps the return value
in a separate struct that describes the changes that were made.
- Changes are now gathered from the undo log, so we don't need to
guess at what was changed - eg if update_note() is called with identical
note contents, no changes are returned. Card changes will only be set
if cards were actually generated by the update_note() call, and tag
will only be set if a new tag was added.
- mw.perform_op() has been updated to expect the op to return the changes,
or a structure with the changes in it, and it will use them to fire the
change hook, instead of fetching the changes from undo_status(), so there
is no risk of race conditions.
- the various calls to mw.perform_op() have been split into separate
files like card_ops.py. Aside from making the code cleaner, this works
around a rather annoying issue with mypy. Because we run it with
no_strict_optional, mypy is happy to accept an operation that returns None,
despite the type signature saying it requires changes to be returned.
Turning no_strict_optional on for the whole codebase is not practical
at the moment, but we can enable it for individual files.
Still todo:
- The cursor keeps moving back to the start of a field when typing -
we need to ignore the refresh hook when we are the initiator.
- The busy cursor icon should probably be delayed a few hundreds ms.
- Still need to think about a nicer way of handling saveNow()
- op_made_changes(), op_affects_study_queue() might be better embedded
as properties in the object instead
2021-03-16 05:26:42 +01:00
|
|
|
assert_eq!(out.output, 2);
|
2020-05-05 12:50:17 +02:00
|
|
|
|
|
|
|
let note = col.storage.get_note(note.id)?.unwrap();
|
|
|
|
// but the update should be limited to the specified field when it was available
|
2021-03-02 10:02:00 +01:00
|
|
|
assert_eq!(¬e.fields()[..], &["one ccc", "two BBB"]);
|
2020-05-05 12:50:17 +02:00
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
}
|