1use super::AppSettings;
8use super::SettingsError;
9
10pub const CURRENT_CONFIG_VERSION: u32 = 7;
17
18pub struct ConfigMigration;
21
22impl ConfigMigration {
23 pub fn run(settings: &mut AppSettings) -> Result<bool, SettingsError> {
26 let mut migrated = false;
27
28 if let Some(ref mut wc) = settings.workspace_config
30 && !wc.global.current_workspace.is_empty()
31 && !wc.workspaces.contains_key(&wc.global.current_workspace)
32 {
33 let first = wc.workspaces.keys().next().cloned().unwrap_or_default();
34 tracing::warn!(
35 "current_workspace '{}' does not exist, resetting to '{}'",
36 wc.global.current_workspace,
37 first
38 );
39 wc.global.current_workspace = first;
40 migrated = true;
41 }
42
43 if settings.config_version < 4 {
46 Self::migrate_to_v4(settings);
47 migrated = true;
48 }
49
50 if settings.config_version < 5 {
53 Self::migrate_to_v5(settings);
54 migrated = true;
55 }
56
57 if settings.config_version < 6 {
60 Self::migrate_to_v6(settings);
61 migrated = true;
62 }
63
64 if settings.config_version < 7 {
66 Self::migrate_to_v7(settings);
67 migrated = true;
68 }
69
70 if migrated {
74 settings.config_version = CURRENT_CONFIG_VERSION;
75 }
76
77 Ok(migrated)
78 }
79
80 fn migrate_to_v6(settings: &mut AppSettings) {
85 use crate::keys::KeyBindings;
86 use crate::keys::action_shortcuts::ActionShortcuts;
87 use crate::keys::key_combo::KeyCombo;
88 use crate::keys::key_strike::KeyStrike;
89
90 let ctrl = crate::keys::key_combo::KeyModifiers::new().and_ctrl();
91 let ctrl_shift_p = KeyCombo::new(ctrl.and_shift(), KeyStrike::KeyP);
92 let ctrl_comma = KeyCombo::new(ctrl, KeyStrike::Comma);
93
94 let mut map = settings.key_bindings.to_hashmap();
95 let at_old_default =
96 still_at_default(&map, &ActionShortcuts::OpenPreferences, ctrl_shift_p);
97 let comma_free = !map.values().flatten().any(|c| *c == ctrl_comma);
98 if at_old_default && comma_free {
99 map.insert(ActionShortcuts::OpenPreferences, vec![ctrl_comma]);
100 }
101 settings.key_bindings = KeyBindings::from_hashmap(map);
102 }
103
104 fn migrate_to_v7(settings: &mut AppSettings) {
119 use crate::keys::KeyBindings;
120 use crate::keys::action_shortcuts::{ActionShortcuts, TextAction};
121 use crate::keys::key_combo::{KeyCombo, KeyModifiers};
122 use crate::keys::key_strike::KeyStrike;
123
124 let ctrl = KeyModifiers::new().and_ctrl();
125 let retired = [
126 (TextAction::Bold, KeyCombo::new(ctrl, KeyStrike::KeyB)),
127 (TextAction::Italic, KeyCombo::new(ctrl, KeyStrike::KeyI)),
128 (TextAction::Underline, KeyCombo::new(ctrl, KeyStrike::KeyU)),
129 (
130 TextAction::Strikethrough,
131 KeyCombo::new(ctrl, KeyStrike::KeyS),
132 ),
133 (
134 TextAction::Image,
135 KeyCombo::new(ctrl.and_shift(), KeyStrike::KeyL),
136 ),
137 ];
142
143 let mut map = settings.key_bindings.to_hashmap();
144 for (action, old_default) in retired {
145 let action = ActionShortcuts::Text(action);
146 if still_at_default(&map, &action, old_default) {
147 map.remove(&action);
148 }
149 }
150 settings.key_bindings = KeyBindings::from_hashmap(map);
151 }
152
153 fn migrate_to_v5(settings: &mut AppSettings) {
157 use crate::keys::KeyBindings;
158 use crate::keys::action_shortcuts::ActionShortcuts;
159 use crate::keys::key_combo::KeyCombo;
160 use crate::keys::key_strike::KeyStrike;
161
162 let ctrl = crate::keys::key_combo::KeyModifiers::new().and_ctrl();
163 let ctrl_shift = ctrl.and_shift();
164 let ctrl_p = KeyCombo::new(ctrl, KeyStrike::KeyP);
165 let ctrl_shift_p = KeyCombo::new(ctrl_shift, KeyStrike::KeyP);
166
167 let mut map = settings.key_bindings.to_hashmap();
168 let settings_is_old_default =
169 still_at_default(&map, &ActionShortcuts::OpenPreferences, ctrl_p);
170 let palette_unset_or_old_default = map
171 .get(&ActionShortcuts::OpenCommandPalette)
172 .is_none_or(|v| v.is_empty() || v.as_slice() == [ctrl_shift_p]);
173 if settings_is_old_default && palette_unset_or_old_default {
174 map.insert(ActionShortcuts::OpenPreferences, vec![ctrl_shift_p]);
175 map.insert(ActionShortcuts::OpenCommandPalette, vec![ctrl_p]);
176 }
177 settings.key_bindings = KeyBindings::from_hashmap(map);
178 }
179
180 fn migrate_to_v4(settings: &mut AppSettings) {
185 use crate::keys::KeyBindings;
186 use crate::keys::action_shortcuts::ActionShortcuts;
187 use crate::keys::key_combo::KeyCombo;
188 use crate::keys::key_strike::KeyStrike;
189
190 let ctrl = crate::keys::key_combo::KeyModifiers::new().and_ctrl();
191 let ctrl_g = KeyCombo::new(ctrl, KeyStrike::KeyG);
192 let ctrl_n = KeyCombo::new(ctrl, KeyStrike::KeyN);
193
194 let mut map = settings.key_bindings.to_hashmap();
195 let follow_is_old_default = still_at_default(&map, &ActionShortcuts::FollowLink, ctrl_g);
196 if follow_is_old_default {
197 map.insert(ActionShortcuts::FollowLink, vec![ctrl_n]);
199 map.entry(ActionShortcuts::Leader).or_default().push(ctrl_g);
200 }
201 settings.key_bindings = KeyBindings::from_hashmap(map);
202 }
206}
207
208fn still_at_default(
215 map: &std::collections::HashMap<
216 crate::keys::action_shortcuts::ActionShortcuts,
217 Vec<crate::keys::key_combo::KeyCombo>,
218 >,
219 action: &crate::keys::action_shortcuts::ActionShortcuts,
220 default: crate::keys::key_combo::KeyCombo,
221) -> bool {
222 map.get(action).is_some_and(|v| v.as_slice() == [default])
223}
224
225#[cfg(test)]
226#[allow(clippy::field_reassign_with_default)]
227mod tests {
228 use super::*;
229 use crate::settings::workspace_config::WorkspaceConfig;
230
231 #[test]
232 fn a_config_already_at_the_current_version_is_left_alone() {
233 let mut settings = AppSettings::default();
234 settings.config_version = CURRENT_CONFIG_VERSION;
235 settings.workspace_config = Some(WorkspaceConfig::new_empty());
236
237 let migrated = ConfigMigration::run(&mut settings).unwrap();
238 assert!(!migrated);
239 }
240
241 #[test]
242 fn v4_moves_ctrl_g_from_followlink_to_leader() {
243 use crate::keys::KeyBindings;
244 use crate::keys::action_shortcuts::ActionShortcuts;
245 use crate::keys::key_combo::{KeyCombo, KeyModifiers};
246 use crate::keys::key_strike::KeyStrike;
247
248 let ctrl = KeyModifiers::new().and_ctrl();
249 let ctrl_g = KeyCombo::new(ctrl, KeyStrike::KeyG);
250 let ctrl_n = KeyCombo::new(ctrl, KeyStrike::KeyN);
251
252 let mut settings = AppSettings::default();
254 let mut map = std::collections::HashMap::new();
255 map.insert(ActionShortcuts::FollowLink, vec![ctrl_g]);
256 settings.key_bindings = KeyBindings::from_hashmap(map);
257 settings.config_version = 3;
258
259 assert!(ConfigMigration::run(&mut settings).unwrap());
260 let map = settings.key_bindings.to_hashmap();
261 assert_eq!(map.get(&ActionShortcuts::Leader), Some(&vec![ctrl_g]));
262 assert_eq!(map.get(&ActionShortcuts::FollowLink), Some(&vec![ctrl_n]));
263 assert_eq!(settings.config_version, CURRENT_CONFIG_VERSION);
264 }
265
266 #[test]
269 fn v7_retires_the_formatting_chords() {
270 use crate::keys::KeyBindings;
271 use crate::keys::action_shortcuts::{ActionShortcuts, TextAction};
272 use crate::keys::key_combo::{KeyCombo, KeyModifiers};
273 use crate::keys::key_strike::KeyStrike;
274
275 let ctrl = KeyModifiers::new().and_ctrl();
276 let mut settings = AppSettings::default();
277 let mut map = std::collections::HashMap::new();
278 for (action, key) in [
280 (TextAction::Bold, KeyStrike::KeyB),
281 (TextAction::Italic, KeyStrike::KeyI),
282 (TextAction::Underline, KeyStrike::KeyU),
283 (TextAction::Strikethrough, KeyStrike::KeyS),
284 ] {
285 map.insert(
286 ActionShortcuts::Text(action),
287 vec![KeyCombo::new(ctrl, key)],
288 );
289 }
290 map.insert(
291 ActionShortcuts::Text(TextAction::Image),
292 vec![KeyCombo::new(ctrl.and_shift(), KeyStrike::KeyL)],
293 );
294 let ctrl_y = KeyCombo::new(ctrl, KeyStrike::KeyY);
296 map.insert(ActionShortcuts::Text(TextAction::Header(1)), vec![ctrl_y]);
297 settings.key_bindings = KeyBindings::from_hashmap(map);
298 settings.config_version = 6;
299
300 assert!(ConfigMigration::run(&mut settings).unwrap());
301 let map = settings.key_bindings.to_hashmap();
302 for action in [
303 TextAction::Bold,
304 TextAction::Italic,
305 TextAction::Underline,
306 TextAction::Strikethrough,
307 TextAction::Image,
308 ] {
309 let name = format!("{action:?}");
310 assert!(
311 !map.contains_key(&ActionShortcuts::Text(action)),
312 "{name} should have retired to the leader"
313 );
314 }
315 assert_eq!(
316 map.get(&ActionShortcuts::Text(TextAction::Header(1))),
317 Some(&vec![ctrl_y]),
318 "a chord the user chose is not ours to retire"
319 );
320 }
321
322 #[test]
325 fn v7_leaves_a_customised_formatting_chord_alone() {
326 use crate::keys::KeyBindings;
327 use crate::keys::action_shortcuts::{ActionShortcuts, TextAction};
328 use crate::keys::key_combo::{KeyCombo, KeyModifiers};
329 use crate::keys::key_strike::KeyStrike;
330
331 let ctrl = KeyModifiers::new().and_ctrl();
332 let moved = KeyCombo::new(ctrl.and_alt(), KeyStrike::KeyB);
333 let mut settings = AppSettings::default();
334 settings.key_bindings = KeyBindings::from_hashmap(std::collections::HashMap::from([(
335 ActionShortcuts::Text(TextAction::Bold),
336 vec![moved],
337 )]));
338 settings.config_version = 6;
339
340 ConfigMigration::run(&mut settings).unwrap();
341 assert_eq!(
342 settings
343 .key_bindings
344 .to_hashmap()
345 .get(&ActionShortcuts::Text(TextAction::Bold)),
346 Some(&vec![moved])
347 );
348 }
349
350 #[test]
354 fn v7_keeps_a_hand_written_link_or_header_chord() {
355 use crate::keys::KeyBindings;
356 use crate::keys::action_shortcuts::{ActionShortcuts, TextAction};
357 use crate::keys::key_combo::{KeyCombo, KeyModifiers};
358 use crate::keys::key_strike::KeyStrike;
359
360 let ctrl = KeyModifiers::new().and_ctrl();
361 let link = KeyCombo::new(ctrl, KeyStrike::KeyL);
362 let header = KeyCombo::new(ctrl, KeyStrike::KeyT);
363 let mut settings = AppSettings::default();
364 settings.key_bindings = KeyBindings::from_hashmap(std::collections::HashMap::from([
365 (ActionShortcuts::Text(TextAction::Link), vec![link]),
366 (
367 ActionShortcuts::Text(TextAction::ToggleHeader),
368 vec![header],
369 ),
370 ]));
371 settings.config_version = 6;
372
373 ConfigMigration::run(&mut settings).unwrap();
374 let map = settings.key_bindings.to_hashmap();
375 assert_eq!(
376 map.get(&ActionShortcuts::Text(TextAction::Link)),
377 Some(&vec![link])
378 );
379 assert_eq!(
380 map.get(&ActionShortcuts::Text(TextAction::ToggleHeader)),
381 Some(&vec![header])
382 );
383 }
384
385 #[test]
386 fn v6_moves_settings_to_ctrl_comma() {
387 use crate::keys::KeyBindings;
388 use crate::keys::action_shortcuts::ActionShortcuts;
389 use crate::keys::key_combo::{KeyCombo, KeyModifiers};
390 use crate::keys::key_strike::KeyStrike;
391
392 let ctrl = KeyModifiers::new().and_ctrl();
393 let ctrl_shift_p = KeyCombo::new(ctrl.and_shift(), KeyStrike::KeyP);
394 let ctrl_comma = KeyCombo::new(ctrl, KeyStrike::Comma);
395
396 let mut settings = AppSettings::default();
397 let mut map = std::collections::HashMap::new();
398 map.insert(ActionShortcuts::OpenPreferences, vec![ctrl_shift_p]);
399 settings.key_bindings = KeyBindings::from_hashmap(map);
400 settings.config_version = 5;
401
402 assert!(ConfigMigration::run(&mut settings).unwrap());
403 let map = settings.key_bindings.to_hashmap();
404 assert_eq!(
405 map.get(&ActionShortcuts::OpenPreferences),
406 Some(&vec![ctrl_comma])
407 );
408 }
409
410 #[test]
411 fn v5_swaps_palette_onto_ctrl_p() {
412 use crate::keys::KeyBindings;
413 use crate::keys::action_shortcuts::ActionShortcuts;
414 use crate::keys::key_combo::{KeyCombo, KeyModifiers};
415 use crate::keys::key_strike::KeyStrike;
416
417 let ctrl = KeyModifiers::new().and_ctrl();
418 let ctrl_p = KeyCombo::new(ctrl, KeyStrike::KeyP);
419 let ctrl_shift_p = KeyCombo::new(ctrl.and_shift(), KeyStrike::KeyP);
420
421 let mut settings = AppSettings::default();
422 let mut map = std::collections::HashMap::new();
423 map.insert(ActionShortcuts::OpenPreferences, vec![ctrl_p]);
424 settings.key_bindings = KeyBindings::from_hashmap(map);
425 settings.config_version = 4;
426
427 assert!(ConfigMigration::run(&mut settings).unwrap());
428 let map = settings.key_bindings.to_hashmap();
429 assert_eq!(
430 map.get(&ActionShortcuts::OpenCommandPalette),
431 Some(&vec![ctrl_p])
432 );
433 let ctrl_comma = KeyCombo::new(ctrl, KeyStrike::Comma);
435 assert_eq!(
436 map.get(&ActionShortcuts::OpenPreferences),
437 Some(&vec![ctrl_comma])
438 );
439 let _ = ctrl_shift_p;
440 }
441
442 #[test]
443 fn v5_leaves_customised_settings_binding_alone() {
444 use crate::keys::KeyBindings;
445 use crate::keys::action_shortcuts::ActionShortcuts;
446 use crate::keys::key_combo::{KeyCombo, KeyModifiers};
447 use crate::keys::key_strike::KeyStrike;
448
449 let ctrl = KeyModifiers::new().and_ctrl();
450 let ctrl_x = KeyCombo::new(ctrl, KeyStrike::KeyX);
451
452 let mut settings = AppSettings::default();
453 let mut map = std::collections::HashMap::new();
454 map.insert(ActionShortcuts::OpenPreferences, vec![ctrl_x]);
455 settings.key_bindings = KeyBindings::from_hashmap(map);
456 settings.config_version = 4;
457
458 ConfigMigration::run(&mut settings).unwrap();
459 let map = settings.key_bindings.to_hashmap();
460 assert_eq!(
461 map.get(&ActionShortcuts::OpenPreferences),
462 Some(&vec![ctrl_x])
463 );
464 }
465
466 #[test]
467 fn v4_leaves_customised_followlink_alone() {
468 use crate::keys::KeyBindings;
469 use crate::keys::action_shortcuts::ActionShortcuts;
470 use crate::keys::key_combo::{KeyCombo, KeyModifiers};
471 use crate::keys::key_strike::KeyStrike;
472
473 let ctrl = KeyModifiers::new().and_ctrl();
474 let ctrl_x = KeyCombo::new(ctrl, KeyStrike::KeyX);
475
476 let mut settings = AppSettings::default();
477 let mut map = std::collections::HashMap::new();
478 map.insert(ActionShortcuts::FollowLink, vec![ctrl_x]);
479 settings.key_bindings = KeyBindings::from_hashmap(map);
480 settings.config_version = 3;
481
482 ConfigMigration::run(&mut settings).unwrap();
483 let map = settings.key_bindings.to_hashmap();
484 assert_eq!(map.get(&ActionShortcuts::FollowLink), Some(&vec![ctrl_x]));
486 assert!(
487 map.get(&ActionShortcuts::Leader)
488 .is_none_or(|v| v.is_empty())
489 );
490 }
491}