UI.7: match workspace names whatever their case

find_saved matched names case-insensitively but save_as, remember and
switch compared them exactly: "Mine" saved over "mine" made a second
workspace, and changes kept as "Mine" didn't come back when switching
to "mine". save_as now replaces a user workspace of the name in any
case, remembered layouts are kept under the saved spelling and looked
up whatever the case, and sanitized maps remembered layouts to the
saved spelling and keeps one per workspace.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KcHisW1vt6pkVPYbzL6Cpg
This commit is contained in:
gorkematlyn
2026-10-22 03:40:18 +07:00
committed by bflatastic
co-authored by Claude Opus 5.5
parent e3e761fe32
commit d6d91c0954
2 changed files with 43 additions and 13 deletions
+9 -3
View File
@@ -775,10 +775,16 @@ impl UiState {
crate::edit_toolbar::sanitize(&mut self.toolbar_edits);
self.docking = self.docking.filter(crate::panel_docking::valid);
self.docking_icons.retain(|id| ICON_PANELS.iter().any(|entry| entry.0 == id));
// Changes kept for workspaces that still exist, one per workspace (a hand-edited file).
// Changes kept for workspaces that still exist, under their saved spelling and one per
// workspace whatever the case (a hand-edited file).
let mut seen = std::collections::BTreeSet::new();
let custom: Vec<String> = self.custom_workspaces.iter().map(|w| w.name.clone()).collect();
self.workspace_changes.retain(|w| (crate::workspaces::is_builtin(&w.name) || custom.contains(&w.name)) && seen.insert(w.name.clone()));
let saved: Vec<String> =
self.custom_workspaces.iter().map(|w| w.name.clone()).chain(crate::workspaces::builtins().into_iter().map(|w| w.name)).collect();
self.workspace_changes.retain_mut(|w| {
let Some(name) = saved.iter().find(|n| n.eq_ignore_ascii_case(&w.name)) else { return false };
w.name = name.clone();
seen.insert(name.to_ascii_lowercase())
});
self.workspace_changes.truncate(64);
// Overrides that can't fire (modifier-only chords recorded by older versions, #487) give
// the default back.
+34 -10
View File
@@ -216,12 +216,10 @@ pub fn apply(ui: &mut UiState, w: &Workspace) {
/// Keep the layout of the current workspace as it is now, so switching back brings it back.
fn remember(ui: &mut UiState) {
let current = ui.workspace.clone();
if find_saved(ui, &current).is_none() {
return;
}
let w = capture(ui, &current);
ui.workspace_changes.retain(|x| x.name != current);
// Kept under the saved workspace's spelling, which `find_saved` matches whatever the case.
let Some(saved) = find_saved(ui, &ui.workspace.clone()) else { return };
let w = capture(ui, &saved.name);
ui.workspace_changes.retain(|x| !x.name.eq_ignore_ascii_case(&saved.name));
ui.workspace_changes.push(w);
// Over the cap, the oldest go: the layout just kept is the newest.
let over = ui.workspace_changes.len().saturating_sub(64);
@@ -232,7 +230,7 @@ fn remember(ui: &mut UiState) {
pub fn switch(ui: &mut UiState, name: &str) -> Result<String, String> {
let saved = find_saved(ui, name).ok_or_else(|| format!("unknown workspace (one of {})", names(ui).join(", ")))?;
remember(ui);
let w = ui.workspace_changes.iter().find(|w| w.name == saved.name).cloned().unwrap_or(saved);
let w = ui.workspace_changes.iter().find(|w| w.name.eq_ignore_ascii_case(&saved.name)).cloned().unwrap_or(saved);
apply(ui, &w);
Ok(w.name)
}
@@ -240,7 +238,7 @@ pub fn switch(ui: &mut UiState, name: &str) -> Result<String, String> {
/// Reset the current workspace to the way it was saved, dropping its changes.
pub fn reset(ui: &mut UiState) -> String {
let w = find_saved(ui, &ui.workspace.clone()).unwrap_or_default();
ui.workspace_changes.retain(|x| x.name != w.name);
ui.workspace_changes.retain(|x| !x.name.eq_ignore_ascii_case(&w.name));
apply(ui, &w);
w.name
}
@@ -275,12 +273,13 @@ pub fn save_as(ui: &mut UiState, name: &str) -> Result<(), String> {
}
// The changes go into the workspace saved, which starts with none; the one left keeps the
// changes it had (none of these).
// A user workspace of that name in another case is replaced, as `find_saved` would find it.
let w = capture(ui, name);
match ui.custom_workspaces.iter_mut().find(|x| x.name == name) {
match ui.custom_workspaces.iter_mut().find(|x| x.name.eq_ignore_ascii_case(name)) {
Some(x) => *x = w,
None => ui.custom_workspaces.push(w),
}
ui.workspace_changes.retain(|x| x.name != name);
ui.workspace_changes.retain(|x| !x.name.eq_ignore_ascii_case(name));
ui.workspace = name.into();
Ok(())
}
@@ -626,6 +625,31 @@ mod tests {
assert!(ui.control_bar, "the layout just left is kept");
}
/// Workspace names match whatever their case, as `find` does: "Mine" saved over "mine" replaces
/// it, and changes made under one spelling come back under the other.
#[test]
fn workspace_names_match_whatever_their_case() {
let mut ui = UiState::default();
save_as(&mut ui, "mine").unwrap();
save_as(&mut ui, "Mine").unwrap();
assert_eq!(ui.custom_workspaces.len(), 1, "one workspace, not two spellings");
ui.control_bar = true;
switch(&mut ui, ESSENTIALS).unwrap();
assert_eq!(switch(&mut ui, "MINE").unwrap(), "Mine");
assert!(ui.control_bar, "its changes came back");
// A preferences file with a remembered layout under another spelling, twice: it maps to the
// saved one, once.
ui.control_bar = false;
switch(&mut ui, ESSENTIALS).unwrap();
let mut odd = capture(&ui, "MINE");
odd.control_bar = true;
ui.workspace_changes = vec![odd.clone(), Workspace { name: "mine".into(), ..odd }];
let mut ui = ui.sanitized();
assert_eq!(ui.workspace_changes.iter().map(|w| w.name.as_str()).collect::<Vec<_>>(), ["Mine"]);
switch(&mut ui, "Mine").unwrap();
assert!(ui.control_bar);
}
/// Saving a workspace takes the current layout into it, which then starts with no changes.
#[test]
fn saving_a_workspace_moves_the_changes_into_it() {