introduce separate routine to remove tags from specific notes

We were (ab)using the bulk update routine to do deletions, but that
code was really intended to be used for finding&replacing, where an
exact match is not a requirement.
This commit is contained in:
Damien Elmes 2021-03-19 13:37:42 +10:00
parent 33d467e688
commit 08895c58d9
10 changed files with 181 additions and 70 deletions

View file

@ -68,9 +68,15 @@ class TagManager:
# Bulk addition/removal from specific notes
#############################################################
def bulk_add(self, nids: Sequence[int], tags: str) -> OpChangesWithCount:
def bulk_add(self, note_ids: Sequence[int], tags: str) -> OpChangesWithCount:
"""Add space-separate tags to provided notes, returning changed count."""
return self.col._backend.add_note_tags(nids=nids, tags=tags)
return self.col._backend.add_note_tags(note_ids=note_ids, tags=tags)
def bulk_remove(self, note_ids: Sequence[int], tags: str) -> OpChangesWithCount:
return self.col._backend.remove_note_tags(note_ids=note_ids, tags=tags)
# Find&replace
#############################################################
def bulk_update(
self, nids: Sequence[int], tags: str, replacement: str, regex: bool
@ -81,9 +87,6 @@ class TagManager:
nids=nids, tags=tags, replacement=replacement, regex=regex
)
def bulk_remove(self, nids: Sequence[int], tags: str) -> OpChangesWithCount:
return self.bulk_update(nids, tags, "", False)
# Bulk addition/removal based on tag
#############################################################
@ -167,7 +170,7 @@ class TagManager:
if add:
self.bulk_add(ids, tags)
else:
self.bulk_update(ids, tags, "", False)
self.bulk_remove(ids, tags)
def bulkRem(self, ids: List[int], tags: str) -> None:
self.bulkAdd(ids, tags, False)

View file

@ -1353,7 +1353,14 @@ where id in %s"""
tags := tags or self._prompt_for_tags(tr(TR.BROWSING_ENTER_TAGS_TO_ADD))
):
return
add_tags(mw=self.mw, note_ids=self.selected_notes(), space_separated_tags=tags)
add_tags(
mw=self.mw,
note_ids=self.selected_notes(),
space_separated_tags=tags,
success=lambda out: tooltip(
tr(TR.BROWSING_NOTES_UPDATED, count=out.count), parent=self
),
)
@ensure_editor_saved_on_trigger
def remove_tags_from_selected_notes(self, tags: Optional[str] = None) -> None:
@ -1363,7 +1370,12 @@ where id in %s"""
):
return
remove_tags_for_notes(
mw=self.mw, note_ids=self.selected_notes(), space_separated_tags=tags
mw=self.mw,
note_ids=self.selected_notes(),
space_separated_tags=tags,
success=lambda out: tooltip(
tr(TR.BROWSING_NOTES_UPDATED, count=out.count), parent=self
),
)
def _prompt_for_tags(self, prompt: str) -> Optional[str]:

View file

@ -39,14 +39,28 @@ def remove_notes(
mw.perform_op(lambda: mw.col.remove_notes(note_ids), success=success)
def add_tags(*, mw: AnkiQt, note_ids: Sequence[int], space_separated_tags: str) -> None:
mw.perform_op(lambda: mw.col.tags.bulk_add(note_ids, space_separated_tags))
def add_tags(
*,
mw: AnkiQt,
note_ids: Sequence[int],
space_separated_tags: str,
success: PerformOpOptionalSuccessCallback = None,
) -> None:
mw.perform_op(
lambda: mw.col.tags.bulk_add(note_ids, space_separated_tags), success=success
)
def remove_tags_for_notes(
*, mw: AnkiQt, note_ids: Sequence[int], space_separated_tags: str
*,
mw: AnkiQt,
note_ids: Sequence[int],
space_separated_tags: str,
success: PerformOpOptionalSuccessCallback = None,
) -> None:
mw.perform_op(lambda: mw.col.tags.bulk_remove(note_ids, space_separated_tags))
mw.perform_op(
lambda: mw.col.tags.bulk_remove(note_ids, space_separated_tags), success=success
)
def clear_unused_tags(*, mw: AnkiQt, parent: QWidget) -> None:

View file

@ -152,8 +152,6 @@ service NotesService {
rpc UpdateNote(UpdateNoteIn) returns (OpChanges);
rpc GetNote(NoteID) returns (Note);
rpc RemoveNotes(RemoveNotesIn) returns (OpChanges);
rpc AddNoteTags(AddNoteTagsIn) returns (OpChangesWithCount);
rpc UpdateNoteTags(UpdateNoteTagsIn) returns (OpChangesWithCount);
rpc ClozeNumbersInNote(Note) returns (ClozeNumbersInNoteOut);
rpc AfterNoteUpdates(AfterNoteUpdatesIn) returns (OpChanges);
rpc FieldNamesForNotes(FieldNamesForNotesIn) returns (FieldNamesForNotesOut);
@ -224,6 +222,9 @@ service TagsService {
rpc TagTree(Empty) returns (TagTreeNode);
rpc DragDropTags(DragDropTagsIn) returns (Empty);
rpc RenameTags(RenameTagsIn) returns (OpChangesWithCount);
rpc AddNoteTags(NoteIDsAndTagsIn) returns (OpChangesWithCount);
rpc RemoveNoteTags(NoteIDsAndTagsIn) returns (OpChangesWithCount);
rpc UpdateNoteTags(UpdateNoteTagsIn) returns (OpChangesWithCount);
}
service SearchService {
@ -1043,8 +1044,8 @@ message AfterNoteUpdatesIn {
bool generate_cards = 3;
}
message AddNoteTagsIn {
repeated int64 nids = 1;
message NoteIDsAndTagsIn {
repeated int64 note_ids = 1;
string tags = 2;
}

View file

@ -87,25 +87,6 @@ impl NotesService for Backend {
})
}
fn add_note_tags(&self, input: pb::AddNoteTagsIn) -> Result<pb::OpChangesWithCount> {
self.with_col(|col| {
col.add_tags_to_notes(&to_note_ids(input.nids), &input.tags)
.map(Into::into)
})
}
fn update_note_tags(&self, input: pb::UpdateNoteTagsIn) -> Result<pb::OpChangesWithCount> {
self.with_col(|col| {
col.replace_tags_for_notes(
&to_note_ids(input.nids),
&input.tags,
&input.replacement,
input.regex,
)
.map(Into::into)
})
}
fn cloze_numbers_in_note(&self, note: pb::Note) -> Result<pb::ClozeNumbersInNoteOut> {
let mut set = HashSet::with_capacity(4);
for field in &note.fields {
@ -158,6 +139,6 @@ impl NotesService for Backend {
}
}
fn to_note_ids(ids: Vec<i64>) -> Vec<NoteID> {
pub(super) fn to_note_ids(ids: Vec<i64>) -> Vec<NoteID> {
ids.into_iter().map(NoteID).collect()
}

View file

@ -1,7 +1,7 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use super::Backend;
use super::{notes::to_note_ids, Backend};
use crate::{backend_proto as pb, prelude::*};
pub(super) use pb::tags_service::Service as TagsService;
@ -55,4 +55,30 @@ impl TagsService for Backend {
self.with_col(|col| col.rename_tag(&input.current_prefix, &input.new_prefix))
.map(Into::into)
}
fn add_note_tags(&self, input: pb::NoteIDsAndTagsIn) -> Result<pb::OpChangesWithCount> {
self.with_col(|col| {
col.add_tags_to_notes(&to_note_ids(input.note_ids), &input.tags)
.map(Into::into)
})
}
fn remove_note_tags(&self, input: pb::NoteIDsAndTagsIn) -> Result<pb::OpChangesWithCount> {
self.with_col(|col| {
col.remove_tags_from_notes(&to_note_ids(input.note_ids), &input.tags)
.map(Into::into)
})
}
fn update_note_tags(&self, input: pb::UpdateNoteTagsIn) -> Result<pb::OpChangesWithCount> {
self.with_col(|col| {
col.replace_tags_for_notes(
&to_note_ids(input.nids),
&input.tags,
&input.replacement,
input.regex,
)
.map(Into::into)
})
}
}

View file

@ -445,7 +445,7 @@ impl super::SqliteStorage {
/// Injects the provided card IDs into the search_cids table, for
/// when ids have arrived outside of a search.
/// Clear with clear_searched_cards().
/// Clear with clear_searched_cards_table().
pub(crate) fn set_search_table_to_card_ids(
&mut self,
cards: &[CardID],

View file

@ -20,22 +20,6 @@ pub(crate) fn join_fields(fields: &[String]) -> String {
fields.join("\x1f")
}
fn row_to_note(row: &Row) -> Result<Note> {
Ok(Note::new_from_storage(
row.get(0)?,
row.get(1)?,
row.get(2)?,
row.get(3)?,
row.get(4)?,
split_tags(row.get_raw(5).as_str()?)
.map(Into::into)
.collect(),
split_fields(row.get_raw(6).as_str()?),
Some(row.get(7)?),
Some(row.get(8).unwrap_or_default()),
))
}
impl super::SqliteStorage {
pub fn get_note(&self, nid: NoteID) -> Result<Option<Note>> {
self.db
@ -193,20 +177,28 @@ impl super::SqliteStorage {
pub(crate) fn get_note_tags_by_id(&mut self, note_id: NoteID) -> Result<Option<NoteTags>> {
self.db
.prepare_cached(&format!("{} where id = ?", include_str!("get_tags.sql")))?
.query_and_then(&[note_id], |row| -> Result<_> {
{
Ok(NoteTags {
id: row.get(0)?,
mtime: row.get(1)?,
usn: row.get(2)?,
tags: row.get(3)?,
})
}
})?
.query_and_then(&[note_id], row_to_note_tags)?
.next()
.transpose()
}
pub(crate) fn get_note_tags_by_id_list(
&mut self,
note_ids: &[NoteID],
) -> Result<Vec<NoteTags>> {
self.set_search_table_to_note_ids(note_ids)?;
let out = self
.db
.prepare_cached(&format!(
"{} where id in (select nid from search_nids)",
include_str!("get_tags.sql")
))?
.query_and_then(NO_PARAMS, row_to_note_tags)?
.collect::<Result<Vec<_>>>()?;
self.clear_searched_notes_table()?;
Ok(out)
}
pub(crate) fn get_note_tags_by_predicate<F>(&mut self, want: F) -> Result<Vec<NoteTags>>
where
F: Fn(&str) -> bool,
@ -217,12 +209,7 @@ impl super::SqliteStorage {
while let Some(row) = rows.next()? {
let tags = row.get_raw(3).as_str()?;
if want(tags) {
output.push(NoteTags {
id: row.get(0)?,
mtime: row.get(1)?,
usn: row.get(2)?,
tags: tags.to_owned(),
})
output.push(row_to_note_tags(row)?)
}
}
Ok(output)
@ -234,4 +221,56 @@ impl super::SqliteStorage {
.execute(params![note.mtime, note.usn, note.tags, note.id])?;
Ok(())
}
fn setup_searched_notes_table(&self) -> Result<()> {
self.db
.execute_batch(include_str!("search_nids_setup.sql"))?;
Ok(())
}
fn clear_searched_notes_table(&self) -> Result<()> {
self.db
.execute("drop table if exists search_nids", NO_PARAMS)?;
Ok(())
}
/// Injects the provided card IDs into the search_nids table, for
/// when ids have arrived outside of a search.
/// Clear with clear_searched_notes_table().
fn set_search_table_to_note_ids(&mut self, notes: &[NoteID]) -> Result<()> {
self.setup_searched_notes_table()?;
let mut stmt = self
.db
.prepare_cached("insert into search_nids values (?)")?;
for nid in notes {
stmt.execute(&[nid])?;
}
Ok(())
}
}
fn row_to_note(row: &Row) -> Result<Note> {
Ok(Note::new_from_storage(
row.get(0)?,
row.get(1)?,
row.get(2)?,
row.get(3)?,
row.get(4)?,
split_tags(row.get_raw(5).as_str()?)
.map(Into::into)
.collect(),
split_fields(row.get_raw(6).as_str()?),
Some(row.get(7)?),
Some(row.get(8).unwrap_or_default()),
))
}
fn row_to_note_tags(row: &Row) -> Result<NoteTags> {
Ok(NoteTags {
id: row.get(0)?,
mtime: row.get(1)?,
usn: row.get(2)?,
tags: row.get(3)?,
})
}

View file

@ -0,0 +1,2 @@
DROP TABLE IF EXISTS search_nids;
CREATE TEMPORARY TABLE search_nids (nid integer PRIMARY KEY NOT NULL);

View file

@ -10,6 +10,17 @@ impl Collection {
self.transact(Op::RemoveTag, |col| col.remove_tags_inner(tags))
}
/// Remove whitespace-separated tags from provided notes.
pub fn remove_tags_from_notes(
&mut self,
nids: &[NoteID],
tags: &str,
) -> Result<OpOutput<usize>> {
self.transact(Op::RemoveTag, |col| {
col.remove_tags_from_notes_inner(nids, tags)
})
}
/// Remove tags not referenced by notes, returning removed count.
pub fn clear_unused_tags(&mut self) -> Result<OpOutput<usize>> {
self.transact(Op::ClearUnusedTags, |col| col.clear_unused_tags_inner())
@ -43,6 +54,28 @@ impl Collection {
Ok(match_count)
}
fn remove_tags_from_notes_inner(&mut self, nids: &[NoteID], tags: &str) -> Result<usize> {
let usn = self.usn()?;
let mut re = PrefixReplacer::new(tags)?;
let mut match_count = 0;
let notes = self.storage.get_note_tags_by_id_list(nids)?;
for mut note in notes {
if !re.is_match(&note.tags) {
continue;
}
match_count += 1;
let original = note.clone();
note.tags = re.remove(&note.tags);
note.set_modified(usn);
self.update_note_tags_undoable(&note, original)?;
}
Ok(match_count)
}
fn clear_unused_tags_inner(&mut self) -> Result<usize> {
let mut count = 0;
let in_notes = self.storage.all_tags_in_notes()?;