Anki/rslib/src/backend/collection.rs
Damien Elmes be994f4102 add support for custom undo steps, and merging multiple actions
Allows add-on authors to define their own label for a group of undoable
operations. For example:

def mark_and_bury(
    *,
    parent: QWidget,
    card_id: CardId,
) -> CollectionOp[OpChanges]:
    def op(col: Collection) -> OpChanges:
        target = col.add_custom_undo_entry("Mark and Bury")
        col.sched.bury_cards([card_id])
        card = col.get_card(card_id)
        col.tags.bulk_add(note_ids=[card.nid], tags="marked")
        return col.merge_undo_entries(target)

    return CollectionOp(parent, op)

The .add_custom_undo_entry() is for adding your own custom actions.
When extending a standard Anki action, instead store `target = 
col.undo_status().last_step` after executing the standard operation.

This started out as a bigger refactor that required a separate
.commit_undoable() call to be run after each operation, instead of
having each operation return changes directly. But that proved to be
somewhat cumbersome in unit tests, and ran the risk of unexpected
behaviour if the caller invoked an operation without remembering to
finalize it.
2021-05-06 16:39:06 +10:00

109 lines
3.5 KiB
Rust

// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use slog::error;
use super::{progress::Progress, Backend};
pub(super) use crate::backend_proto::collection_service::Service as CollectionService;
use crate::{
backend::progress::progress_to_proto,
backend_proto as pb,
collection::open_collection,
log::{self, default_logger},
prelude::*,
};
impl CollectionService for Backend {
fn latest_progress(&self, _input: pb::Empty) -> Result<pb::Progress> {
let progress = self.progress_state.lock().unwrap().last_progress;
Ok(progress_to_proto(progress, &self.tr))
}
fn set_wants_abort(&self, _input: pb::Empty) -> Result<pb::Empty> {
self.progress_state.lock().unwrap().want_abort = true;
Ok(().into())
}
fn open_collection(&self, input: pb::OpenCollectionIn) -> Result<pb::Empty> {
let mut col = self.col.lock().unwrap();
if col.is_some() {
return Err(AnkiError::CollectionAlreadyOpen);
}
let mut path = input.collection_path.clone();
path.push_str(".log");
let log_path = match input.log_path.as_str() {
"" => None,
path => Some(path),
};
let logger = default_logger(log_path)?;
let new_col = open_collection(
input.collection_path,
input.media_folder_path,
input.media_db_path,
self.server,
self.tr.clone(),
logger,
)?;
*col = Some(new_col);
Ok(().into())
}
fn close_collection(&self, input: pb::CloseCollectionIn) -> Result<pb::Empty> {
self.abort_media_sync_and_wait();
let mut col = self.col.lock().unwrap();
if col.is_none() {
return Err(AnkiError::CollectionNotOpen);
}
let col_inner = col.take().unwrap();
if input.downgrade_to_schema11 {
let log = log::terminal();
if let Err(e) = col_inner.close(input.downgrade_to_schema11) {
error!(log, " failed: {:?}", e);
}
}
Ok(().into())
}
fn check_database(&self, _input: pb::Empty) -> Result<pb::CheckDatabaseOut> {
let mut handler = self.new_progress_handler();
let progress_fn = move |progress, throttle| {
handler.update(Progress::DatabaseCheck(progress), throttle);
};
self.with_col(|col| {
col.check_database(progress_fn)
.map(|problems| pb::CheckDatabaseOut {
problems: problems.to_i18n_strings(&col.tr),
})
})
}
fn get_undo_status(&self, _input: pb::Empty) -> Result<pb::UndoStatus> {
self.with_col(|col| Ok(col.undo_status().into_protobuf(&col.tr)))
}
fn undo(&self, _input: pb::Empty) -> Result<pb::OpChangesAfterUndo> {
self.with_col(|col| col.undo().map(|out| out.into_protobuf(&col.tr)))
}
fn redo(&self, _input: pb::Empty) -> Result<pb::OpChangesAfterUndo> {
self.with_col(|col| col.redo().map(|out| out.into_protobuf(&col.tr)))
}
fn add_custom_undo_entry(&self, input: pb::String) -> Result<pb::UInt32> {
self.with_col(|col| Ok(col.add_custom_undo_step(input.val).into()))
}
fn merge_undo_entries(&self, input: pb::UInt32) -> Result<pb::OpChanges> {
let starting_from = input.val as usize;
self.with_col(|col| col.merge_undoable_ops(starting_from))
.map(Into::into)
}
}