Skip to content

Commit e142b80

Browse files
eth0netnoaccOS
authored andcommitted
workspace: Fix inconsistent paths order serialization (zed-industries#19232)
Release Notes: - Fixed inconsistent serialization of workspace paths order
1 parent eb7141c commit e142b80

File tree

5 files changed

+107
-33
lines changed

5 files changed

+107
-33
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/recent_projects/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ file_finder.workspace = true
2222
futures.workspace = true
2323
fuzzy.workspace = true
2424
gpui.workspace = true
25+
itertools.workspace = true
2526
log.workspace = true
2627
menu.workspace = true
2728
ordered-float.workspace = true

crates/recent_projects/src/recent_projects.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use gpui::{
1313
Action, AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView,
1414
Subscription, Task, View, ViewContext, WeakView,
1515
};
16+
use itertools::Itertools;
1617
use ordered_float::OrderedFloat;
1718
use picker::{
1819
highlighted_match_with_paths::{HighlightedMatchWithPaths, HighlightedText},
@@ -247,8 +248,9 @@ impl PickerDelegate for RecentProjectsDelegate {
247248
SerializedWorkspaceLocation::Local(paths, order) => order
248249
.order()
249250
.iter()
250-
.filter_map(|i| paths.paths().get(*i))
251-
.map(|path| path.compact().to_string_lossy().into_owned())
251+
.zip(paths.paths().iter())
252+
.sorted_by_key(|(i, _)| *i)
253+
.map(|(_, path)| path.compact().to_string_lossy().into_owned())
252254
.collect::<Vec<_>>()
253255
.join(""),
254256
SerializedWorkspaceLocation::DevServer(dev_server_project) => {
@@ -447,8 +449,9 @@ impl PickerDelegate for RecentProjectsDelegate {
447449
order
448450
.order()
449451
.iter()
450-
.filter_map(|i| paths.paths().get(*i).cloned())
451-
.map(|path| path.compact())
452+
.zip(paths.paths().iter())
453+
.sorted_by_key(|(i, _)| **i)
454+
.map(|(_, path)| path.compact())
452455
.collect(),
453456
),
454457
SerializedWorkspaceLocation::Ssh(ssh_project) => Arc::new(ssh_project.ssh_urls()),

crates/workspace/src/persistence.rs

Lines changed: 83 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,8 @@ impl WorkspaceDb {
380380
&self,
381381
worktree_roots: &[P],
382382
) -> Option<SerializedWorkspace> {
383+
// paths are sorted before db interactions to ensure that the order of the paths
384+
// doesn't affect the workspace selection for existing workspaces
383385
let local_paths = LocalPaths::new(worktree_roots);
384386

385387
// Note that we re-assign the workspace_id here in case it's empty
@@ -833,8 +835,8 @@ impl WorkspaceDb {
833835
}
834836

835837
query! {
836-
fn session_workspaces(session_id: String) -> Result<Vec<(LocalPaths, Option<u64>, Option<u64>)>> {
837-
SELECT local_paths, window_id, ssh_project_id
838+
fn session_workspaces(session_id: String) -> Result<Vec<(LocalPaths, LocalPathsOrder, Option<u64>, Option<u64>)>> {
839+
SELECT local_paths, local_paths_order, window_id, ssh_project_id
838840
FROM workspaces
839841
WHERE session_id = ?1 AND dev_server_project_id IS NULL
840842
ORDER BY timestamp DESC
@@ -971,7 +973,7 @@ impl WorkspaceDb {
971973
) -> Result<Vec<SerializedWorkspaceLocation>> {
972974
let mut workspaces = Vec::new();
973975

974-
for (location, window_id, ssh_project_id) in
976+
for (location, order, window_id, ssh_project_id) in
975977
self.session_workspaces(last_session_id.to_owned())?
976978
{
977979
if let Some(ssh_project_id) = ssh_project_id {
@@ -980,8 +982,7 @@ impl WorkspaceDb {
980982
} else if location.paths().iter().all(|path| path.exists())
981983
&& location.paths().iter().any(|path| path.is_dir())
982984
{
983-
let location =
984-
SerializedWorkspaceLocation::from_local_paths(location.paths().iter());
985+
let location = SerializedWorkspaceLocation::Local(location, order);
985986
workspaces.push((location, window_id.map(WindowId::from)));
986987
}
987988
}
@@ -1603,27 +1604,56 @@ mod tests {
16031604
window_id: Some(50),
16041605
};
16051606

1607+
let workspace_6 = SerializedWorkspace {
1608+
id: WorkspaceId(6),
1609+
location: SerializedWorkspaceLocation::Local(
1610+
LocalPaths::new(["/tmp6a", "/tmp6b", "/tmp6c"]),
1611+
LocalPathsOrder::new([2, 1, 0]),
1612+
),
1613+
center_group: Default::default(),
1614+
window_bounds: Default::default(),
1615+
display: Default::default(),
1616+
docks: Default::default(),
1617+
centered_layout: false,
1618+
session_id: Some("session-id-3".to_owned()),
1619+
window_id: Some(60),
1620+
};
1621+
16061622
db.save_workspace(workspace_1.clone()).await;
16071623
db.save_workspace(workspace_2.clone()).await;
16081624
db.save_workspace(workspace_3.clone()).await;
16091625
db.save_workspace(workspace_4.clone()).await;
16101626
db.save_workspace(workspace_5.clone()).await;
1627+
db.save_workspace(workspace_6.clone()).await;
16111628

16121629
let locations = db.session_workspaces("session-id-1".to_owned()).unwrap();
16131630
assert_eq!(locations.len(), 2);
16141631
assert_eq!(locations[0].0, LocalPaths::new(["/tmp1"]));
1615-
assert_eq!(locations[0].1, Some(10));
1632+
assert_eq!(locations[0].1, LocalPathsOrder::new([0]));
1633+
assert_eq!(locations[0].2, Some(10));
16161634
assert_eq!(locations[1].0, LocalPaths::new(["/tmp2"]));
1617-
assert_eq!(locations[1].1, Some(20));
1635+
assert_eq!(locations[1].1, LocalPathsOrder::new([0]));
1636+
assert_eq!(locations[1].2, Some(20));
16181637

16191638
let locations = db.session_workspaces("session-id-2".to_owned()).unwrap();
16201639
assert_eq!(locations.len(), 2);
16211640
assert_eq!(locations[0].0, LocalPaths::new(["/tmp3"]));
1622-
assert_eq!(locations[0].1, Some(30));
1641+
assert_eq!(locations[0].1, LocalPathsOrder::new([0]));
1642+
assert_eq!(locations[0].2, Some(30));
16231643
let empty_paths: Vec<&str> = Vec::new();
16241644
assert_eq!(locations[1].0, LocalPaths::new(empty_paths.iter()));
1625-
assert_eq!(locations[1].1, Some(50));
1626-
assert_eq!(locations[1].2, Some(ssh_project.id.0));
1645+
assert_eq!(locations[1].1, LocalPathsOrder::new([]));
1646+
assert_eq!(locations[1].2, Some(50));
1647+
assert_eq!(locations[1].3, Some(ssh_project.id.0));
1648+
1649+
let locations = db.session_workspaces("session-id-3".to_owned()).unwrap();
1650+
assert_eq!(locations.len(), 1);
1651+
assert_eq!(
1652+
locations[0].0,
1653+
LocalPaths::new(["/tmp6a", "/tmp6b", "/tmp6c"]),
1654+
);
1655+
assert_eq!(locations[0].1, LocalPathsOrder::new([2, 1, 0]));
1656+
assert_eq!(locations[0].2, Some(60));
16271657
}
16281658

16291659
fn default_workspace<P: AsRef<Path>>(
@@ -1654,15 +1684,30 @@ mod tests {
16541684
WorkspaceDb(open_test_db("test_serializing_workspaces_last_session_workspaces").await);
16551685

16561686
let workspaces = [
1657-
(1, dir1.path().to_str().unwrap(), 9),
1658-
(2, dir2.path().to_str().unwrap(), 5),
1659-
(3, dir3.path().to_str().unwrap(), 8),
1660-
(4, dir4.path().to_str().unwrap(), 2),
1687+
(1, vec![dir1.path()], vec![0], 9),
1688+
(2, vec![dir2.path()], vec![0], 5),
1689+
(3, vec![dir3.path()], vec![0], 8),
1690+
(4, vec![dir4.path()], vec![0], 2),
1691+
(
1692+
5,
1693+
vec![dir1.path(), dir2.path(), dir3.path()],
1694+
vec![0, 1, 2],
1695+
3,
1696+
),
1697+
(
1698+
6,
1699+
vec![dir2.path(), dir3.path(), dir4.path()],
1700+
vec![2, 1, 0],
1701+
4,
1702+
),
16611703
]
16621704
.into_iter()
1663-
.map(|(id, location, window_id)| SerializedWorkspace {
1705+
.map(|(id, locations, order, window_id)| SerializedWorkspace {
16641706
id: WorkspaceId(id),
1665-
location: SerializedWorkspaceLocation::from_local_paths([location]),
1707+
location: SerializedWorkspaceLocation::Local(
1708+
LocalPaths::new(locations),
1709+
LocalPathsOrder::new(order),
1710+
),
16661711
center_group: Default::default(),
16671712
window_bounds: Default::default(),
16681713
display: Default::default(),
@@ -1681,28 +1726,44 @@ mod tests {
16811726
WindowId::from(2), // Top
16821727
WindowId::from(8),
16831728
WindowId::from(5),
1684-
WindowId::from(9), // Bottom
1729+
WindowId::from(9),
1730+
WindowId::from(3),
1731+
WindowId::from(4), // Bottom
16851732
]));
16861733

16871734
let have = db
16881735
.last_session_workspace_locations("one-session", stack)
16891736
.unwrap();
1690-
assert_eq!(have.len(), 4);
1737+
assert_eq!(have.len(), 6);
16911738
assert_eq!(
16921739
have[0],
1693-
SerializedWorkspaceLocation::from_local_paths(&[dir4.path().to_str().unwrap()])
1740+
SerializedWorkspaceLocation::from_local_paths(&[dir4.path()])
16941741
);
16951742
assert_eq!(
16961743
have[1],
1697-
SerializedWorkspaceLocation::from_local_paths([dir3.path().to_str().unwrap()])
1744+
SerializedWorkspaceLocation::from_local_paths([dir3.path()])
16981745
);
16991746
assert_eq!(
17001747
have[2],
1701-
SerializedWorkspaceLocation::from_local_paths([dir2.path().to_str().unwrap()])
1748+
SerializedWorkspaceLocation::from_local_paths([dir2.path()])
17021749
);
17031750
assert_eq!(
17041751
have[3],
1705-
SerializedWorkspaceLocation::from_local_paths([dir1.path().to_str().unwrap()])
1752+
SerializedWorkspaceLocation::from_local_paths([dir1.path()])
1753+
);
1754+
assert_eq!(
1755+
have[4],
1756+
SerializedWorkspaceLocation::Local(
1757+
LocalPaths::new([dir1.path(), dir2.path(), dir3.path()]),
1758+
LocalPathsOrder::new([0, 1, 2]),
1759+
),
1760+
);
1761+
assert_eq!(
1762+
have[5],
1763+
SerializedWorkspaceLocation::Local(
1764+
LocalPaths::new([dir2.path(), dir3.path(), dir4.path()]),
1765+
LocalPathsOrder::new([2, 1, 0]),
1766+
),
17061767
);
17071768
}
17081769

crates/workspace/src/workspace.rs

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1103,20 +1103,28 @@ impl Workspace {
11031103

11041104
let mut paths_to_open = abs_paths;
11051105

1106-
let paths_order = serialized_workspace
1106+
let workspace_location = serialized_workspace
11071107
.as_ref()
11081108
.map(|ws| &ws.location)
11091109
.and_then(|loc| match loc {
1110-
SerializedWorkspaceLocation::Local(_, order) => Some(order.order()),
1110+
SerializedWorkspaceLocation::Local(paths, order) => {
1111+
Some((paths.paths(), order.order()))
1112+
}
11111113
_ => None,
11121114
});
11131115

1114-
if let Some(paths_order) = paths_order {
1115-
paths_to_open = paths_order
1116+
if let Some((paths, order)) = workspace_location {
1117+
// todo: should probably move this logic to a method on the SerializedWorkspaceLocation
1118+
// it's only valid for Local and would be more clear there and be able to be tested
1119+
// and reused elsewhere
1120+
paths_to_open = order
11161121
.iter()
1117-
.filter_map(|i| paths_to_open.get(*i).cloned())
1118-
.collect::<Vec<_>>();
1119-
if paths_order.iter().enumerate().any(|(i, &j)| i != j) {
1122+
.zip(paths.iter())
1123+
.sorted_by_key(|(i, _)| *i)
1124+
.map(|(_, path)| path.clone())
1125+
.collect();
1126+
1127+
if order.iter().enumerate().any(|(i, &j)| i != j) {
11201128
project_handle
11211129
.update(&mut cx, |project, cx| {
11221130
project.set_worktrees_reordered(true, cx);

0 commit comments

Comments
 (0)