use super::AppSettings;
use super::SettingsError;
pub const CURRENT_CONFIG_VERSION: u32 = 7;
pub struct ConfigMigration;
impl ConfigMigration {
pub fn run(settings: &mut AppSettings) -> Result<bool, SettingsError> {
let mut migrated = false;
if let Some(ref mut wc) = settings.workspace_config
&& !wc.global.current_workspace.is_empty()
&& !wc.workspaces.contains_key(&wc.global.current_workspace)
{
let first = wc.workspaces.keys().next().cloned().unwrap_or_default();
tracing::warn!(
"current_workspace '{}' does not exist, resetting to '{}'",
wc.global.current_workspace,
first
);
wc.global.current_workspace = first;
migrated = true;
}
if settings.config_version < 4 {
Self::migrate_to_v4(settings);
migrated = true;
}
if settings.config_version < 5 {
Self::migrate_to_v5(settings);
migrated = true;
}
if settings.config_version < 6 {
Self::migrate_to_v6(settings);
migrated = true;
}
if settings.config_version < 7 {
Self::migrate_to_v7(settings);
migrated = true;
}
if migrated {
settings.config_version = CURRENT_CONFIG_VERSION;
}
Ok(migrated)
}
fn migrate_to_v6(settings: &mut AppSettings) {
use crate::keys::KeyBindings;
use crate::keys::action_shortcuts::ActionShortcuts;
use crate::keys::key_combo::KeyCombo;
use crate::keys::key_strike::KeyStrike;
let ctrl = crate::keys::key_combo::KeyModifiers::new().and_ctrl();
let ctrl_shift_p = KeyCombo::new(ctrl.and_shift(), KeyStrike::KeyP);
let ctrl_comma = KeyCombo::new(ctrl, KeyStrike::Comma);
let mut map = settings.key_bindings.to_hashmap();
let at_old_default =
still_at_default(&map, &ActionShortcuts::OpenPreferences, ctrl_shift_p);
let comma_free = !map.values().flatten().any(|c| *c == ctrl_comma);
if at_old_default && comma_free {
map.insert(ActionShortcuts::OpenPreferences, vec![ctrl_comma]);
}
settings.key_bindings = KeyBindings::from_hashmap(map);
}
fn migrate_to_v7(settings: &mut AppSettings) {
use crate::keys::KeyBindings;
use crate::keys::action_shortcuts::{ActionShortcuts, TextAction};
use crate::keys::key_combo::{KeyCombo, KeyModifiers};
use crate::keys::key_strike::KeyStrike;
let ctrl = KeyModifiers::new().and_ctrl();
let retired = [
(TextAction::Bold, KeyCombo::new(ctrl, KeyStrike::KeyB)),
(TextAction::Italic, KeyCombo::new(ctrl, KeyStrike::KeyI)),
(TextAction::Underline, KeyCombo::new(ctrl, KeyStrike::KeyU)),
(
TextAction::Strikethrough,
KeyCombo::new(ctrl, KeyStrike::KeyS),
),
(
TextAction::Image,
KeyCombo::new(ctrl.and_shift(), KeyStrike::KeyL),
),
];
let mut map = settings.key_bindings.to_hashmap();
for (action, old_default) in retired {
let action = ActionShortcuts::Text(action);
if still_at_default(&map, &action, old_default) {
map.remove(&action);
}
}
settings.key_bindings = KeyBindings::from_hashmap(map);
}
fn migrate_to_v5(settings: &mut AppSettings) {
use crate::keys::KeyBindings;
use crate::keys::action_shortcuts::ActionShortcuts;
use crate::keys::key_combo::KeyCombo;
use crate::keys::key_strike::KeyStrike;
let ctrl = crate::keys::key_combo::KeyModifiers::new().and_ctrl();
let ctrl_shift = ctrl.and_shift();
let ctrl_p = KeyCombo::new(ctrl, KeyStrike::KeyP);
let ctrl_shift_p = KeyCombo::new(ctrl_shift, KeyStrike::KeyP);
let mut map = settings.key_bindings.to_hashmap();
let settings_is_old_default =
still_at_default(&map, &ActionShortcuts::OpenPreferences, ctrl_p);
let palette_unset_or_old_default = map
.get(&ActionShortcuts::OpenCommandPalette)
.is_none_or(|v| v.is_empty() || v.as_slice() == [ctrl_shift_p]);
if settings_is_old_default && palette_unset_or_old_default {
map.insert(ActionShortcuts::OpenPreferences, vec![ctrl_shift_p]);
map.insert(ActionShortcuts::OpenCommandPalette, vec![ctrl_p]);
}
settings.key_bindings = KeyBindings::from_hashmap(map);
}
fn migrate_to_v4(settings: &mut AppSettings) {
use crate::keys::KeyBindings;
use crate::keys::action_shortcuts::ActionShortcuts;
use crate::keys::key_combo::KeyCombo;
use crate::keys::key_strike::KeyStrike;
let ctrl = crate::keys::key_combo::KeyModifiers::new().and_ctrl();
let ctrl_g = KeyCombo::new(ctrl, KeyStrike::KeyG);
let ctrl_n = KeyCombo::new(ctrl, KeyStrike::KeyN);
let mut map = settings.key_bindings.to_hashmap();
let follow_is_old_default = still_at_default(&map, &ActionShortcuts::FollowLink, ctrl_g);
if follow_is_old_default {
map.insert(ActionShortcuts::FollowLink, vec![ctrl_n]);
map.entry(ActionShortcuts::Leader).or_default().push(ctrl_g);
}
settings.key_bindings = KeyBindings::from_hashmap(map);
}
}
fn still_at_default(
map: &std::collections::HashMap<
crate::keys::action_shortcuts::ActionShortcuts,
Vec<crate::keys::key_combo::KeyCombo>,
>,
action: &crate::keys::action_shortcuts::ActionShortcuts,
default: crate::keys::key_combo::KeyCombo,
) -> bool {
map.get(action).is_some_and(|v| v.as_slice() == [default])
}
#[cfg(test)]
#[allow(clippy::field_reassign_with_default)]
mod tests {
use super::*;
use crate::settings::workspace_config::WorkspaceConfig;
#[test]
fn a_config_already_at_the_current_version_is_left_alone() {
let mut settings = AppSettings::default();
settings.config_version = CURRENT_CONFIG_VERSION;
settings.workspace_config = Some(WorkspaceConfig::new_empty());
let migrated = ConfigMigration::run(&mut settings).unwrap();
assert!(!migrated);
}
#[test]
fn v4_moves_ctrl_g_from_followlink_to_leader() {
use crate::keys::KeyBindings;
use crate::keys::action_shortcuts::ActionShortcuts;
use crate::keys::key_combo::{KeyCombo, KeyModifiers};
use crate::keys::key_strike::KeyStrike;
let ctrl = KeyModifiers::new().and_ctrl();
let ctrl_g = KeyCombo::new(ctrl, KeyStrike::KeyG);
let ctrl_n = KeyCombo::new(ctrl, KeyStrike::KeyN);
let mut settings = AppSettings::default();
let mut map = std::collections::HashMap::new();
map.insert(ActionShortcuts::FollowLink, vec![ctrl_g]);
settings.key_bindings = KeyBindings::from_hashmap(map);
settings.config_version = 3;
assert!(ConfigMigration::run(&mut settings).unwrap());
let map = settings.key_bindings.to_hashmap();
assert_eq!(map.get(&ActionShortcuts::Leader), Some(&vec![ctrl_g]));
assert_eq!(map.get(&ActionShortcuts::FollowLink), Some(&vec![ctrl_n]));
assert_eq!(settings.config_version, CURRENT_CONFIG_VERSION);
}
#[test]
fn v7_retires_the_formatting_chords() {
use crate::keys::KeyBindings;
use crate::keys::action_shortcuts::{ActionShortcuts, TextAction};
use crate::keys::key_combo::{KeyCombo, KeyModifiers};
use crate::keys::key_strike::KeyStrike;
let ctrl = KeyModifiers::new().and_ctrl();
let mut settings = AppSettings::default();
let mut map = std::collections::HashMap::new();
for (action, key) in [
(TextAction::Bold, KeyStrike::KeyB),
(TextAction::Italic, KeyStrike::KeyI),
(TextAction::Underline, KeyStrike::KeyU),
(TextAction::Strikethrough, KeyStrike::KeyS),
] {
map.insert(
ActionShortcuts::Text(action),
vec![KeyCombo::new(ctrl, key)],
);
}
map.insert(
ActionShortcuts::Text(TextAction::Image),
vec![KeyCombo::new(ctrl.and_shift(), KeyStrike::KeyL)],
);
let ctrl_y = KeyCombo::new(ctrl, KeyStrike::KeyY);
map.insert(ActionShortcuts::Text(TextAction::Header(1)), vec![ctrl_y]);
settings.key_bindings = KeyBindings::from_hashmap(map);
settings.config_version = 6;
assert!(ConfigMigration::run(&mut settings).unwrap());
let map = settings.key_bindings.to_hashmap();
for action in [
TextAction::Bold,
TextAction::Italic,
TextAction::Underline,
TextAction::Strikethrough,
TextAction::Image,
] {
let name = format!("{action:?}");
assert!(
!map.contains_key(&ActionShortcuts::Text(action)),
"{name} should have retired to the leader"
);
}
assert_eq!(
map.get(&ActionShortcuts::Text(TextAction::Header(1))),
Some(&vec![ctrl_y]),
"a chord the user chose is not ours to retire"
);
}
#[test]
fn v7_leaves_a_customised_formatting_chord_alone() {
use crate::keys::KeyBindings;
use crate::keys::action_shortcuts::{ActionShortcuts, TextAction};
use crate::keys::key_combo::{KeyCombo, KeyModifiers};
use crate::keys::key_strike::KeyStrike;
let ctrl = KeyModifiers::new().and_ctrl();
let moved = KeyCombo::new(ctrl.and_alt(), KeyStrike::KeyB);
let mut settings = AppSettings::default();
settings.key_bindings = KeyBindings::from_hashmap(std::collections::HashMap::from([(
ActionShortcuts::Text(TextAction::Bold),
vec![moved],
)]));
settings.config_version = 6;
ConfigMigration::run(&mut settings).unwrap();
assert_eq!(
settings
.key_bindings
.to_hashmap()
.get(&ActionShortcuts::Text(TextAction::Bold)),
Some(&vec![moved])
);
}
#[test]
fn v7_keeps_a_hand_written_link_or_header_chord() {
use crate::keys::KeyBindings;
use crate::keys::action_shortcuts::{ActionShortcuts, TextAction};
use crate::keys::key_combo::{KeyCombo, KeyModifiers};
use crate::keys::key_strike::KeyStrike;
let ctrl = KeyModifiers::new().and_ctrl();
let link = KeyCombo::new(ctrl, KeyStrike::KeyL);
let header = KeyCombo::new(ctrl, KeyStrike::KeyT);
let mut settings = AppSettings::default();
settings.key_bindings = KeyBindings::from_hashmap(std::collections::HashMap::from([
(ActionShortcuts::Text(TextAction::Link), vec![link]),
(
ActionShortcuts::Text(TextAction::ToggleHeader),
vec![header],
),
]));
settings.config_version = 6;
ConfigMigration::run(&mut settings).unwrap();
let map = settings.key_bindings.to_hashmap();
assert_eq!(
map.get(&ActionShortcuts::Text(TextAction::Link)),
Some(&vec![link])
);
assert_eq!(
map.get(&ActionShortcuts::Text(TextAction::ToggleHeader)),
Some(&vec![header])
);
}
#[test]
fn v6_moves_settings_to_ctrl_comma() {
use crate::keys::KeyBindings;
use crate::keys::action_shortcuts::ActionShortcuts;
use crate::keys::key_combo::{KeyCombo, KeyModifiers};
use crate::keys::key_strike::KeyStrike;
let ctrl = KeyModifiers::new().and_ctrl();
let ctrl_shift_p = KeyCombo::new(ctrl.and_shift(), KeyStrike::KeyP);
let ctrl_comma = KeyCombo::new(ctrl, KeyStrike::Comma);
let mut settings = AppSettings::default();
let mut map = std::collections::HashMap::new();
map.insert(ActionShortcuts::OpenPreferences, vec![ctrl_shift_p]);
settings.key_bindings = KeyBindings::from_hashmap(map);
settings.config_version = 5;
assert!(ConfigMigration::run(&mut settings).unwrap());
let map = settings.key_bindings.to_hashmap();
assert_eq!(
map.get(&ActionShortcuts::OpenPreferences),
Some(&vec![ctrl_comma])
);
}
#[test]
fn v5_swaps_palette_onto_ctrl_p() {
use crate::keys::KeyBindings;
use crate::keys::action_shortcuts::ActionShortcuts;
use crate::keys::key_combo::{KeyCombo, KeyModifiers};
use crate::keys::key_strike::KeyStrike;
let ctrl = KeyModifiers::new().and_ctrl();
let ctrl_p = KeyCombo::new(ctrl, KeyStrike::KeyP);
let ctrl_shift_p = KeyCombo::new(ctrl.and_shift(), KeyStrike::KeyP);
let mut settings = AppSettings::default();
let mut map = std::collections::HashMap::new();
map.insert(ActionShortcuts::OpenPreferences, vec![ctrl_p]);
settings.key_bindings = KeyBindings::from_hashmap(map);
settings.config_version = 4;
assert!(ConfigMigration::run(&mut settings).unwrap());
let map = settings.key_bindings.to_hashmap();
assert_eq!(
map.get(&ActionShortcuts::OpenCommandPalette),
Some(&vec![ctrl_p])
);
let ctrl_comma = KeyCombo::new(ctrl, KeyStrike::Comma);
assert_eq!(
map.get(&ActionShortcuts::OpenPreferences),
Some(&vec![ctrl_comma])
);
let _ = ctrl_shift_p;
}
#[test]
fn v5_leaves_customised_settings_binding_alone() {
use crate::keys::KeyBindings;
use crate::keys::action_shortcuts::ActionShortcuts;
use crate::keys::key_combo::{KeyCombo, KeyModifiers};
use crate::keys::key_strike::KeyStrike;
let ctrl = KeyModifiers::new().and_ctrl();
let ctrl_x = KeyCombo::new(ctrl, KeyStrike::KeyX);
let mut settings = AppSettings::default();
let mut map = std::collections::HashMap::new();
map.insert(ActionShortcuts::OpenPreferences, vec![ctrl_x]);
settings.key_bindings = KeyBindings::from_hashmap(map);
settings.config_version = 4;
ConfigMigration::run(&mut settings).unwrap();
let map = settings.key_bindings.to_hashmap();
assert_eq!(
map.get(&ActionShortcuts::OpenPreferences),
Some(&vec![ctrl_x])
);
}
#[test]
fn v4_leaves_customised_followlink_alone() {
use crate::keys::KeyBindings;
use crate::keys::action_shortcuts::ActionShortcuts;
use crate::keys::key_combo::{KeyCombo, KeyModifiers};
use crate::keys::key_strike::KeyStrike;
let ctrl = KeyModifiers::new().and_ctrl();
let ctrl_x = KeyCombo::new(ctrl, KeyStrike::KeyX);
let mut settings = AppSettings::default();
let mut map = std::collections::HashMap::new();
map.insert(ActionShortcuts::FollowLink, vec![ctrl_x]);
settings.key_bindings = KeyBindings::from_hashmap(map);
settings.config_version = 3;
ConfigMigration::run(&mut settings).unwrap();
let map = settings.key_bindings.to_hashmap();
assert_eq!(map.get(&ActionShortcuts::FollowLink), Some(&vec![ctrl_x]));
assert!(
map.get(&ActionShortcuts::Leader)
.is_none_or(|v| v.is_empty())
);
}
}