undo support for config entries

This commit is contained in:
Damien Elmes 2021-03-09 10:58:55 +10:00
parent ce243c2cae
commit 96940f0527
19 changed files with 237 additions and 59 deletions

View File

@ -44,7 +44,7 @@ pub fn open_collection<P: Into<PathBuf>>(
#[cfg(test)]
pub fn open_test_collection() -> Collection {
use crate::config::SchedulerVersion;
let col = open_test_collection_with_server(false);
let mut col = open_test_collection_with_server(false);
// our unit tests assume v2 is the default, but at the time of writing v1
// is still the default
col.set_scheduler_version_config_key(SchedulerVersion::V2)

View File

@ -61,7 +61,7 @@ impl Collection {
}
}
pub(crate) fn set_bool(&self, key: BoolKey, value: bool) -> Result<()> {
pub(crate) fn set_bool(&mut self, key: BoolKey, value: bool) -> Result<()> {
self.set_config(key, &value)
}
}

View File

@ -34,7 +34,11 @@ impl Collection {
self.get_config_optional(key.as_str())
}
pub(crate) fn set_last_notetype_for_deck(&self, did: DeckID, ntid: NoteTypeID) -> Result<()> {
pub(crate) fn set_last_notetype_for_deck(
&mut self,
did: DeckID,
ntid: NoteTypeID,
) -> Result<()> {
let key = DeckConfigKey::LastNotetype.for_deck(did);
self.set_config(key.as_str(), &ntid)
}

View File

@ -6,6 +6,7 @@ mod deck;
mod notetype;
pub(crate) mod schema11;
mod string;
pub(crate) mod undo;
pub use self::{bool::BoolKey, string::StringKey};
use crate::prelude::*;
@ -15,6 +16,26 @@ use serde_repr::{Deserialize_repr, Serialize_repr};
use slog::warn;
use strum::IntoStaticStr;
/// Only used when updating/undoing.
#[derive(Debug)]
pub(crate) struct ConfigEntry {
pub key: String,
pub value: Vec<u8>,
pub usn: Usn,
pub mtime: TimestampSecs,
}
impl ConfigEntry {
pub(crate) fn boxed(key: &str, value: Vec<u8>, usn: Usn, mtime: TimestampSecs) -> Box<Self> {
Box::new(Self {
key: key.into(),
value,
usn,
mtime,
})
}
}
#[derive(IntoStaticStr)]
#[strum(serialize_all = "camelCase")]
pub(crate) enum ConfigKey {
@ -76,19 +97,24 @@ impl Collection {
self.get_config_optional(key).unwrap_or_default()
}
pub(crate) fn set_config<'a, T: Serialize, K>(&self, key: K, val: &T) -> Result<()>
pub(crate) fn set_config<'a, T: Serialize, K>(&mut self, key: K, val: &T) -> Result<()>
where
K: Into<&'a str>,
{
self.storage
.set_config_value(key.into(), val, self.usn()?, TimestampSecs::now())
let entry = ConfigEntry::boxed(
key.into(),
serde_json::to_vec(val)?,
self.usn()?,
TimestampSecs::now(),
);
self.set_config_undoable(entry)
}
pub(crate) fn remove_config<'a, K>(&self, key: K) -> Result<()>
pub(crate) fn remove_config<'a, K>(&mut self, key: K) -> Result<()>
where
K: Into<&'a str>,
{
self.storage.remove_config(key.into())
self.remove_config_undoable(key.into())
}
/// Remove all keys starting with provided prefix, which must end with '_'.
@ -107,7 +133,7 @@ impl Collection {
self.get_config_optional(ConfigKey::CreationOffset)
}
pub(crate) fn set_creation_utc_offset(&self, mins: Option<i32>) -> Result<()> {
pub(crate) fn set_creation_utc_offset(&mut self, mins: Option<i32>) -> Result<()> {
if let Some(mins) = mins {
self.set_config(ConfigKey::CreationOffset, &mins)
} else {
@ -119,7 +145,7 @@ impl Collection {
self.get_config_optional(ConfigKey::LocalOffset)
}
pub(crate) fn set_configured_utc_offset(&self, mins: i32) -> Result<()> {
pub(crate) fn set_configured_utc_offset(&mut self, mins: i32) -> Result<()> {
self.set_config(ConfigKey::LocalOffset, &mins)
}
@ -128,7 +154,7 @@ impl Collection {
.map(|r| r.min(23))
}
pub(crate) fn set_v2_rollover(&self, hour: u32) -> Result<()> {
pub(crate) fn set_v2_rollover(&mut self, hour: u32) -> Result<()> {
self.set_config(ConfigKey::Rollover, &hour)
}
@ -136,7 +162,7 @@ impl Collection {
self.get_config_default(ConfigKey::NextNewCardPosition)
}
pub(crate) fn get_and_update_next_card_position(&self) -> Result<u32> {
pub(crate) fn get_and_update_next_card_position(&mut self) -> Result<u32> {
let pos: u32 = self
.get_config_optional(ConfigKey::NextNewCardPosition)
.unwrap_or_default();
@ -144,7 +170,7 @@ impl Collection {
Ok(pos)
}
pub(crate) fn set_next_card_position(&self, pos: u32) -> Result<()> {
pub(crate) fn set_next_card_position(&mut self, pos: u32) -> Result<()> {
self.set_config(ConfigKey::NextNewCardPosition, &pos)
}
@ -154,7 +180,7 @@ impl Collection {
}
/// Caution: this only updates the config setting.
pub(crate) fn set_scheduler_version_config_key(&self, ver: SchedulerVersion) -> Result<()> {
pub(crate) fn set_scheduler_version_config_key(&mut self, ver: SchedulerVersion) -> Result<()> {
self.set_config(ConfigKey::SchedulerVersion, &ver)
}
@ -163,7 +189,7 @@ impl Collection {
.unwrap_or(1200)
}
pub(crate) fn set_learn_ahead_secs(&self, secs: u32) -> Result<()> {
pub(crate) fn set_learn_ahead_secs(&mut self, secs: u32) -> Result<()> {
self.set_config(ConfigKey::LearnAheadSecs, &secs)
}
@ -175,7 +201,7 @@ impl Collection {
}
}
pub(crate) fn set_new_review_mix(&self, mix: NewReviewMix) -> Result<()> {
pub(crate) fn set_new_review_mix(&mut self, mix: NewReviewMix) -> Result<()> {
self.set_config(ConfigKey::NewReviewMix, &(mix as u8))
}
@ -184,7 +210,7 @@ impl Collection {
.unwrap_or(Weekday::Sunday)
}
pub(crate) fn set_first_day_of_week(&self, weekday: Weekday) -> Result<()> {
pub(crate) fn set_first_day_of_week(&mut self, weekday: Weekday) -> Result<()> {
self.set_config(ConfigKey::FirstDayOfWeek, &weekday)
}
@ -193,7 +219,7 @@ impl Collection {
.unwrap_or_default()
}
pub(crate) fn set_answer_time_limit_secs(&self, secs: u32) -> Result<()> {
pub(crate) fn set_answer_time_limit_secs(&mut self, secs: u32) -> Result<()> {
self.set_config(ConfigKey::AnswerTimeLimitSecs, &secs)
}
@ -202,7 +228,7 @@ impl Collection {
.unwrap_or_default()
}
pub(crate) fn set_last_unburied_day(&self, day: u32) -> Result<()> {
pub(crate) fn set_last_unburied_day(&mut self, day: u32) -> Result<()> {
self.set_config(ConfigKey::LastUnburiedDay, &day)
}
}
@ -274,7 +300,7 @@ mod test {
#[test]
fn get_set() {
let col = open_test_collection();
let mut col = open_test_collection();
// missing key
assert_eq!(col.get_config_optional::<Vec<i64>, _>("test"), None);

View File

@ -28,7 +28,7 @@ impl Collection {
self.get_config_optional(ConfigKey::CurrentNoteTypeID)
}
pub(crate) fn set_current_notetype_id(&self, ntid: NoteTypeID) -> Result<()> {
pub(crate) fn set_current_notetype_id(&mut self, ntid: NoteTypeID) -> Result<()> {
self.set_config(ConfigKey::CurrentNoteTypeID, &ntid)
}
@ -41,7 +41,7 @@ impl Collection {
self.get_config_optional(key.as_str())
}
pub(crate) fn set_last_deck_for_notetype(&self, id: NoteTypeID, did: DeckID) -> Result<()> {
pub(crate) fn set_last_deck_for_notetype(&mut self, id: NoteTypeID, did: DeckID) -> Result<()> {
let key = NoteTypeConfigKey::LastDeckAddedTo.for_notetype(id);
self.set_config(key.as_str(), &did)
}

View File

@ -22,7 +22,7 @@ impl Collection {
.unwrap_or_else(|| default.to_string())
}
pub(crate) fn set_string(&self, key: StringKey, val: &str) -> Result<()> {
pub(crate) fn set_string(&mut self, key: StringKey, val: &str) -> Result<()> {
self.set_config(key, &val)
}
}

111
rslib/src/config/undo.rs Normal file
View File

@ -0,0 +1,111 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use super::ConfigEntry;
use crate::prelude::*;
#[derive(Debug)]
pub(crate) enum UndoableConfigChange {
Added(Box<ConfigEntry>),
Updated(Box<ConfigEntry>),
Removed(Box<ConfigEntry>),
}
impl Collection {
pub(crate) fn undo_config_change(&mut self, change: UndoableConfigChange) -> Result<()> {
match change {
UndoableConfigChange::Added(entry) => self.remove_config_undoable(&entry.key),
UndoableConfigChange::Updated(entry) => {
let current = self
.storage
.get_config_entry(&entry.key)?
.ok_or_else(|| AnkiError::invalid_input("config disappeared"))?;
self.update_config_entry_undoable(entry, current)
}
UndoableConfigChange::Removed(entry) => self.add_config_entry_undoable(entry),
}
}
pub(super) fn set_config_undoable(&mut self, entry: Box<ConfigEntry>) -> Result<()> {
if let Some(original) = self.storage.get_config_entry(&entry.key)? {
self.update_config_entry_undoable(entry, original)
} else {
self.add_config_entry_undoable(entry)
}
}
pub(super) fn remove_config_undoable(&mut self, key: &str) -> Result<()> {
if let Some(current) = self.storage.get_config_entry(key)? {
self.save_undo(UndoableConfigChange::Removed(current));
self.storage.remove_config(key)?;
}
Ok(())
}
fn add_config_entry_undoable(&mut self, entry: Box<ConfigEntry>) -> Result<()> {
self.storage.set_config_entry(&entry)?;
self.save_undo(UndoableConfigChange::Added(entry));
Ok(())
}
fn update_config_entry_undoable(
&mut self,
entry: Box<ConfigEntry>,
original: Box<ConfigEntry>,
) -> Result<()> {
if entry.value != original.value {
self.save_undo(UndoableConfigChange::Updated(original));
self.storage.set_config_entry(&entry)?;
}
Ok(())
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::collection::open_test_collection;
#[test]
fn undo() -> Result<()> {
let mut col = open_test_collection();
// the op kind doesn't matter, we just need undo enabled
let op = Some(UndoableOpKind::Bury);
// test key
let key = BoolKey::NormalizeNoteText;
// not set by default, but defaults to true
assert_eq!(col.get_bool(key), true);
// first set adds the key
col.transact(op, |col| col.set_bool(key, false))?;
assert_eq!(col.get_bool(key), false);
// mutate it twice
col.transact(op, |col| col.set_bool(key, true))?;
assert_eq!(col.get_bool(key), true);
col.transact(op, |col| col.set_bool(key, false))?;
assert_eq!(col.get_bool(key), false);
// when we remove it, it goes back to its default
col.transact(op, |col| col.remove_config(key))?;
assert_eq!(col.get_bool(key), true);
// undo the removal
col.undo()?;
assert_eq!(col.get_bool(key), false);
// undo the mutations
col.undo()?;
assert_eq!(col.get_bool(key), true);
col.undo()?;
assert_eq!(col.get_bool(key), false);
// and undo the initial add
col.undo()?;
assert_eq!(col.get_bool(key), true);
Ok(())
}
}

View File

@ -409,7 +409,7 @@ impl Collection {
Ok(())
}
fn update_next_new_position(&self) -> Result<()> {
fn update_next_new_position(&mut self) -> Result<()> {
let pos = self.storage.max_new_card_position().unwrap_or(0);
self.set_next_card_position(pos)
}

View File

@ -299,7 +299,12 @@ impl Collection {
// not sure if entry() can be used due to get_deck_config() returning a result
#[allow(clippy::map_entry)]
fn due_for_deck(&self, did: DeckID, dcid: DeckConfID, cache: &mut CardGenCache) -> Result<u32> {
fn due_for_deck(
&mut self,
did: DeckID,
dcid: DeckConfID,
cache: &mut CardGenCache,
) -> Result<u32> {
if !cache.deck_configs.contains_key(&did) {
let conf = self.get_deck_config(dcid, true)?.unwrap();
cache.deck_configs.insert(did, conf);

View File

@ -3,8 +3,13 @@
use super::NoteTypeKind;
use crate::{
config::ConfigKey, err::Result, i18n::I18n, i18n::TR, notetype::NoteType,
storage::SqliteStorage, timestamp::TimestampSecs,
config::{ConfigEntry, ConfigKey},
err::Result,
i18n::I18n,
i18n::TR,
notetype::NoteType,
storage::SqliteStorage,
timestamp::TimestampSecs,
};
use crate::backend_proto::stock_note_type::Kind;
@ -14,12 +19,12 @@ impl SqliteStorage {
for (idx, mut nt) in all_stock_notetypes(i18n).into_iter().enumerate() {
self.add_new_notetype(&mut nt)?;
if idx == Kind::Basic as usize {
self.set_config_value(
self.set_config_entry(&ConfigEntry::boxed(
ConfigKey::CurrentNoteTypeID.into(),
&nt.id,
serde_json::to_vec(&nt.id)?,
self.usn(false)?,
TimestampSecs::now(),
)?;
))?;
}
}
Ok(())

View File

@ -193,7 +193,7 @@ impl Collection {
}
/// Describe the next intervals, to display on the answer buttons.
pub fn describe_next_states(&self, choices: NextCardStates) -> Result<Vec<String>> {
pub fn describe_next_states(&mut self, choices: NextCardStates) -> Result<Vec<String>> {
let collapse_time = self.learn_ahead_secs();
let now = TimestampSecs::now();
let timing = self.timing_for_timestamp(now)?;

View File

@ -23,7 +23,7 @@ use timing::{
};
impl Collection {
pub fn timing_today(&self) -> Result<SchedTimingToday> {
pub fn timing_today(&mut self) -> Result<SchedTimingToday> {
self.timing_for_timestamp(TimestampSecs::now())
}
@ -31,7 +31,7 @@ impl Collection {
Ok(((self.timing_today()?.days_elapsed as i32) + delta).max(0) as u32)
}
pub(crate) fn timing_for_timestamp(&self, now: TimestampSecs) -> Result<SchedTimingToday> {
pub(crate) fn timing_for_timestamp(&mut self, now: TimestampSecs) -> Result<SchedTimingToday> {
let current_utc_offset = self.local_utc_offset_for_user()?;
let rollover_hour = match self.scheduler_version() {
@ -63,7 +63,7 @@ impl Collection {
/// ensuring the config reflects the current value.
/// In the server case, return the value set in the config, and
/// fall back on UTC if it's missing/invalid.
pub(crate) fn local_utc_offset_for_user(&self) -> Result<FixedOffset> {
pub(crate) fn local_utc_offset_for_user(&mut self) -> Result<FixedOffset> {
let config_tz = self
.get_configured_utc_offset()
.and_then(|v| FixedOffset::west_opt(v * 60))
@ -99,7 +99,7 @@ impl Collection {
}
}
pub(crate) fn set_rollover_for_current_scheduler(&self, hour: u8) -> Result<()> {
pub(crate) fn set_rollover_for_current_scheduler(&mut self, hour: u8) -> Result<()> {
match self.scheduler_version() {
SchedulerVersion::V1 => {
self.storage

View File

@ -126,7 +126,7 @@ impl Collection {
})
}
fn card_stats_to_string(&self, cs: CardStats) -> Result<String> {
fn card_stats_to_string(&mut self, cs: CardStats) -> Result<String> {
let offset = self.local_utc_offset_for_user()?;
let i18n = &self.i18n;

View File

@ -20,7 +20,7 @@ impl Collection {
self.graph_data(all, days)
}
fn graph_data(&self, all: bool, days: u32) -> Result<pb::GraphsOut> {
fn graph_data(&mut self, all: bool, days: u32) -> Result<pb::GraphsOut> {
let timing = self.timing_today()?;
let revlog_start = TimestampSecs(if days > 0 {
timing.next_day_at - (((days as i64) + 1) * 86_400)
@ -60,7 +60,7 @@ impl Collection {
})
}
pub(crate) fn set_graph_preferences(&self, prefs: pb::GraphPreferences) -> Result<()> {
pub(crate) fn set_graph_preferences(&mut self, prefs: pb::GraphPreferences) -> Result<()> {
self.set_first_day_of_week(match prefs.calendar_first_day_of_week {
1 => Weekday::Monday,
5 => Weekday::Friday,

View File

@ -18,10 +18,9 @@ pub fn studied_today(cards: u32, secs: f32, i18n: &I18n) -> String {
}
impl Collection {
pub fn studied_today(&self) -> Result<String> {
let today = self
.storage
.studied_today(self.timing_today()?.next_day_at)?;
pub fn studied_today(&mut self) -> Result<String> {
let timing = self.timing_today()?;
let today = self.storage.studied_today(timing.next_day_at)?;
Ok(studied_today(today.cards, today.seconds as f32, &self.i18n))
}
}

View File

@ -0,0 +1,5 @@
SELECT val,
usn,
mtime_secs
FROM config
WHERE KEY = ?

View File

@ -2,24 +2,17 @@
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use super::SqliteStorage;
use crate::{err::Result, timestamp::TimestampSecs, types::Usn};
use crate::{config::ConfigEntry, err::Result, timestamp::TimestampSecs, types::Usn};
use rusqlite::{params, NO_PARAMS};
use serde::{de::DeserializeOwned, Serialize};
use serde::de::DeserializeOwned;
use serde_json::Value;
use std::collections::HashMap;
impl SqliteStorage {
pub(crate) fn set_config_value<T: Serialize>(
&self,
key: &str,
val: &T,
usn: Usn,
mtime: TimestampSecs,
) -> Result<()> {
let json = serde_json::to_vec(val)?;
pub(crate) fn set_config_entry(&self, entry: &ConfigEntry) -> Result<()> {
self.db
.prepare_cached(include_str!("add.sql"))?
.execute(params![key, usn, mtime, &json])?;
.execute(params![&entry.key, entry.usn, entry.mtime, &entry.value])?;
Ok(())
}
@ -41,6 +34,22 @@ impl SqliteStorage {
.transpose()
}
/// Return the raw bytes and other metadata, for undoing.
pub(crate) fn get_config_entry(&self, key: &str) -> Result<Option<Box<ConfigEntry>>> {
self.db
.prepare_cached(include_str!("get_entry.sql"))?
.query_and_then(&[key], |row| {
Ok(ConfigEntry::boxed(
key,
row.get(0)?,
row.get(1)?,
row.get(2)?,
))
})?
.next()
.transpose()
}
/// Prefix is expected to end with '_'.
pub(crate) fn get_config_prefix(&self, prefix: &str) -> Result<Vec<(String, Vec<u8>)>> {
let mut end = prefix.to_string();
@ -70,7 +79,12 @@ impl SqliteStorage {
) -> Result<()> {
self.db.execute("delete from config", NO_PARAMS)?;
for (key, val) in conf.iter() {
self.set_config_value(key, val, usn, mtime)?;
self.set_config_entry(&ConfigEntry::boxed(
key,
serde_json::to_vec(&val)?,
usn,
mtime,
))?;
}
Ok(())
}

View File

@ -328,8 +328,8 @@ where
SyncActionRequired::NormalSyncRequired => {
self.col.discard_undo_and_study_queues();
self.col.storage.begin_trx()?;
self.col
.unbury_if_day_rolled_over(self.col.timing_today()?)?;
let timing = self.col.timing_today()?;
self.col.unbury_if_day_rolled_over(timing)?;
match self.normal_sync_inner(state).await {
Ok(success) => {
self.col.storage.commit_trx()?;

View File

@ -2,9 +2,10 @@
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use crate::{
card::undo::UndoableCardChange, decks::undo::UndoableDeckChange,
notes::undo::UndoableNoteChange, prelude::*, revlog::undo::UndoableRevlogChange,
scheduler::queue::undo::UndoableQueueChange, tags::undo::UndoableTagChange,
card::undo::UndoableCardChange, config::undo::UndoableConfigChange,
decks::undo::UndoableDeckChange, notes::undo::UndoableNoteChange, prelude::*,
revlog::undo::UndoableRevlogChange, scheduler::queue::undo::UndoableQueueChange,
tags::undo::UndoableTagChange,
};
#[derive(Debug)]
@ -15,6 +16,7 @@ pub(crate) enum UndoableChange {
Tag(UndoableTagChange),
Revlog(UndoableRevlogChange),
Queue(UndoableQueueChange),
Config(UndoableConfigChange),
}
impl UndoableChange {
@ -26,6 +28,7 @@ impl UndoableChange {
UndoableChange::Tag(c) => col.undo_tag_change(c),
UndoableChange::Revlog(c) => col.undo_revlog_change(c),
UndoableChange::Queue(c) => col.undo_queue_change(c),
UndoableChange::Config(c) => col.undo_config_change(c),
}
}
}
@ -65,3 +68,9 @@ impl From<UndoableQueueChange> for UndoableChange {
UndoableChange::Queue(c)
}
}
impl From<UndoableConfigChange> for UndoableChange {
fn from(c: UndoableConfigChange) -> Self {
UndoableChange::Config(c)
}
}