workspace: Fix inconsistent paths order serialization (#19232)

Release Notes:

- Fixed inconsistent serialization of workspace paths order
This commit is contained in:
Elliot Thomas 2024-10-17 16:38:28 +01:00 committed by GitHub
parent e9e4c770ca
commit 398d0396b6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 107 additions and 33 deletions

1
Cargo.lock generated
View file

@ -8974,6 +8974,7 @@ dependencies = [
"futures 0.3.30", "futures 0.3.30",
"fuzzy", "fuzzy",
"gpui", "gpui",
"itertools 0.13.0",
"language", "language",
"log", "log",
"menu", "menu",

View file

@ -22,6 +22,7 @@ file_finder.workspace = true
futures.workspace = true futures.workspace = true
fuzzy.workspace = true fuzzy.workspace = true
gpui.workspace = true gpui.workspace = true
itertools.workspace = true
log.workspace = true log.workspace = true
menu.workspace = true menu.workspace = true
ordered-float.workspace = true ordered-float.workspace = true

View file

@ -13,6 +13,7 @@ use gpui::{
Action, AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Action, AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView,
Subscription, Task, View, ViewContext, WeakView, Subscription, Task, View, ViewContext, WeakView,
}; };
use itertools::Itertools;
use ordered_float::OrderedFloat; use ordered_float::OrderedFloat;
use picker::{ use picker::{
highlighted_match_with_paths::{HighlightedMatchWithPaths, HighlightedText}, highlighted_match_with_paths::{HighlightedMatchWithPaths, HighlightedText},
@ -247,8 +248,9 @@ impl PickerDelegate for RecentProjectsDelegate {
SerializedWorkspaceLocation::Local(paths, order) => order SerializedWorkspaceLocation::Local(paths, order) => order
.order() .order()
.iter() .iter()
.filter_map(|i| paths.paths().get(*i)) .zip(paths.paths().iter())
.map(|path| path.compact().to_string_lossy().into_owned()) .sorted_by_key(|(i, _)| *i)
.map(|(_, path)| path.compact().to_string_lossy().into_owned())
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(""), .join(""),
SerializedWorkspaceLocation::DevServer(dev_server_project) => { SerializedWorkspaceLocation::DevServer(dev_server_project) => {
@ -447,8 +449,9 @@ impl PickerDelegate for RecentProjectsDelegate {
order order
.order() .order()
.iter() .iter()
.filter_map(|i| paths.paths().get(*i).cloned()) .zip(paths.paths().iter())
.map(|path| path.compact()) .sorted_by_key(|(i, _)| **i)
.map(|(_, path)| path.compact())
.collect(), .collect(),
), ),
SerializedWorkspaceLocation::Ssh(ssh_project) => Arc::new(ssh_project.ssh_urls()), SerializedWorkspaceLocation::Ssh(ssh_project) => Arc::new(ssh_project.ssh_urls()),

View file

@ -380,6 +380,8 @@ impl WorkspaceDb {
&self, &self,
worktree_roots: &[P], worktree_roots: &[P],
) -> Option<SerializedWorkspace> { ) -> Option<SerializedWorkspace> {
// paths are sorted before db interactions to ensure that the order of the paths
// doesn't affect the workspace selection for existing workspaces
let local_paths = LocalPaths::new(worktree_roots); let local_paths = LocalPaths::new(worktree_roots);
// Note that we re-assign the workspace_id here in case it's empty // Note that we re-assign the workspace_id here in case it's empty
@ -833,8 +835,8 @@ impl WorkspaceDb {
} }
query! { query! {
fn session_workspaces(session_id: String) -> Result<Vec<(LocalPaths, Option<u64>, Option<u64>)>> { fn session_workspaces(session_id: String) -> Result<Vec<(LocalPaths, LocalPathsOrder, Option<u64>, Option<u64>)>> {
SELECT local_paths, window_id, ssh_project_id SELECT local_paths, local_paths_order, window_id, ssh_project_id
FROM workspaces FROM workspaces
WHERE session_id = ?1 AND dev_server_project_id IS NULL WHERE session_id = ?1 AND dev_server_project_id IS NULL
ORDER BY timestamp DESC ORDER BY timestamp DESC
@ -971,7 +973,7 @@ impl WorkspaceDb {
) -> Result<Vec<SerializedWorkspaceLocation>> { ) -> Result<Vec<SerializedWorkspaceLocation>> {
let mut workspaces = Vec::new(); let mut workspaces = Vec::new();
for (location, window_id, ssh_project_id) in for (location, order, window_id, ssh_project_id) in
self.session_workspaces(last_session_id.to_owned())? self.session_workspaces(last_session_id.to_owned())?
{ {
if let Some(ssh_project_id) = ssh_project_id { if let Some(ssh_project_id) = ssh_project_id {
@ -980,8 +982,7 @@ impl WorkspaceDb {
} else if location.paths().iter().all(|path| path.exists()) } else if location.paths().iter().all(|path| path.exists())
&& location.paths().iter().any(|path| path.is_dir()) && location.paths().iter().any(|path| path.is_dir())
{ {
let location = let location = SerializedWorkspaceLocation::Local(location, order);
SerializedWorkspaceLocation::from_local_paths(location.paths().iter());
workspaces.push((location, window_id.map(WindowId::from))); workspaces.push((location, window_id.map(WindowId::from)));
} }
} }
@ -1603,27 +1604,56 @@ mod tests {
window_id: Some(50), window_id: Some(50),
}; };
let workspace_6 = SerializedWorkspace {
id: WorkspaceId(6),
location: SerializedWorkspaceLocation::Local(
LocalPaths::new(["/tmp6a", "/tmp6b", "/tmp6c"]),
LocalPathsOrder::new([2, 1, 0]),
),
center_group: Default::default(),
window_bounds: Default::default(),
display: Default::default(),
docks: Default::default(),
centered_layout: false,
session_id: Some("session-id-3".to_owned()),
window_id: Some(60),
};
db.save_workspace(workspace_1.clone()).await; db.save_workspace(workspace_1.clone()).await;
db.save_workspace(workspace_2.clone()).await; db.save_workspace(workspace_2.clone()).await;
db.save_workspace(workspace_3.clone()).await; db.save_workspace(workspace_3.clone()).await;
db.save_workspace(workspace_4.clone()).await; db.save_workspace(workspace_4.clone()).await;
db.save_workspace(workspace_5.clone()).await; db.save_workspace(workspace_5.clone()).await;
db.save_workspace(workspace_6.clone()).await;
let locations = db.session_workspaces("session-id-1".to_owned()).unwrap(); let locations = db.session_workspaces("session-id-1".to_owned()).unwrap();
assert_eq!(locations.len(), 2); assert_eq!(locations.len(), 2);
assert_eq!(locations[0].0, LocalPaths::new(["/tmp1"])); assert_eq!(locations[0].0, LocalPaths::new(["/tmp1"]));
assert_eq!(locations[0].1, Some(10)); assert_eq!(locations[0].1, LocalPathsOrder::new([0]));
assert_eq!(locations[0].2, Some(10));
assert_eq!(locations[1].0, LocalPaths::new(["/tmp2"])); assert_eq!(locations[1].0, LocalPaths::new(["/tmp2"]));
assert_eq!(locations[1].1, Some(20)); assert_eq!(locations[1].1, LocalPathsOrder::new([0]));
assert_eq!(locations[1].2, Some(20));
let locations = db.session_workspaces("session-id-2".to_owned()).unwrap(); let locations = db.session_workspaces("session-id-2".to_owned()).unwrap();
assert_eq!(locations.len(), 2); assert_eq!(locations.len(), 2);
assert_eq!(locations[0].0, LocalPaths::new(["/tmp3"])); assert_eq!(locations[0].0, LocalPaths::new(["/tmp3"]));
assert_eq!(locations[0].1, Some(30)); assert_eq!(locations[0].1, LocalPathsOrder::new([0]));
assert_eq!(locations[0].2, Some(30));
let empty_paths: Vec<&str> = Vec::new(); let empty_paths: Vec<&str> = Vec::new();
assert_eq!(locations[1].0, LocalPaths::new(empty_paths.iter())); assert_eq!(locations[1].0, LocalPaths::new(empty_paths.iter()));
assert_eq!(locations[1].1, Some(50)); assert_eq!(locations[1].1, LocalPathsOrder::new([]));
assert_eq!(locations[1].2, Some(ssh_project.id.0)); assert_eq!(locations[1].2, Some(50));
assert_eq!(locations[1].3, Some(ssh_project.id.0));
let locations = db.session_workspaces("session-id-3".to_owned()).unwrap();
assert_eq!(locations.len(), 1);
assert_eq!(
locations[0].0,
LocalPaths::new(["/tmp6a", "/tmp6b", "/tmp6c"]),
);
assert_eq!(locations[0].1, LocalPathsOrder::new([2, 1, 0]));
assert_eq!(locations[0].2, Some(60));
} }
fn default_workspace<P: AsRef<Path>>( fn default_workspace<P: AsRef<Path>>(
@ -1654,15 +1684,30 @@ mod tests {
WorkspaceDb(open_test_db("test_serializing_workspaces_last_session_workspaces").await); WorkspaceDb(open_test_db("test_serializing_workspaces_last_session_workspaces").await);
let workspaces = [ let workspaces = [
(1, dir1.path().to_str().unwrap(), 9), (1, vec![dir1.path()], vec![0], 9),
(2, dir2.path().to_str().unwrap(), 5), (2, vec![dir2.path()], vec![0], 5),
(3, dir3.path().to_str().unwrap(), 8), (3, vec![dir3.path()], vec![0], 8),
(4, dir4.path().to_str().unwrap(), 2), (4, vec![dir4.path()], vec![0], 2),
(
5,
vec![dir1.path(), dir2.path(), dir3.path()],
vec![0, 1, 2],
3,
),
(
6,
vec![dir2.path(), dir3.path(), dir4.path()],
vec![2, 1, 0],
4,
),
] ]
.into_iter() .into_iter()
.map(|(id, location, window_id)| SerializedWorkspace { .map(|(id, locations, order, window_id)| SerializedWorkspace {
id: WorkspaceId(id), id: WorkspaceId(id),
location: SerializedWorkspaceLocation::from_local_paths([location]), location: SerializedWorkspaceLocation::Local(
LocalPaths::new(locations),
LocalPathsOrder::new(order),
),
center_group: Default::default(), center_group: Default::default(),
window_bounds: Default::default(), window_bounds: Default::default(),
display: Default::default(), display: Default::default(),
@ -1681,28 +1726,44 @@ mod tests {
WindowId::from(2), // Top WindowId::from(2), // Top
WindowId::from(8), WindowId::from(8),
WindowId::from(5), WindowId::from(5),
WindowId::from(9), // Bottom WindowId::from(9),
WindowId::from(3),
WindowId::from(4), // Bottom
])); ]));
let have = db let have = db
.last_session_workspace_locations("one-session", stack) .last_session_workspace_locations("one-session", stack)
.unwrap(); .unwrap();
assert_eq!(have.len(), 4); assert_eq!(have.len(), 6);
assert_eq!( assert_eq!(
have[0], have[0],
SerializedWorkspaceLocation::from_local_paths(&[dir4.path().to_str().unwrap()]) SerializedWorkspaceLocation::from_local_paths(&[dir4.path()])
); );
assert_eq!( assert_eq!(
have[1], have[1],
SerializedWorkspaceLocation::from_local_paths([dir3.path().to_str().unwrap()]) SerializedWorkspaceLocation::from_local_paths([dir3.path()])
); );
assert_eq!( assert_eq!(
have[2], have[2],
SerializedWorkspaceLocation::from_local_paths([dir2.path().to_str().unwrap()]) SerializedWorkspaceLocation::from_local_paths([dir2.path()])
); );
assert_eq!( assert_eq!(
have[3], have[3],
SerializedWorkspaceLocation::from_local_paths([dir1.path().to_str().unwrap()]) SerializedWorkspaceLocation::from_local_paths([dir1.path()])
);
assert_eq!(
have[4],
SerializedWorkspaceLocation::Local(
LocalPaths::new([dir1.path(), dir2.path(), dir3.path()]),
LocalPathsOrder::new([0, 1, 2]),
),
);
assert_eq!(
have[5],
SerializedWorkspaceLocation::Local(
LocalPaths::new([dir2.path(), dir3.path(), dir4.path()]),
LocalPathsOrder::new([2, 1, 0]),
),
); );
} }

View file

@ -1103,20 +1103,28 @@ impl Workspace {
let mut paths_to_open = abs_paths; let mut paths_to_open = abs_paths;
let paths_order = serialized_workspace let workspace_location = serialized_workspace
.as_ref() .as_ref()
.map(|ws| &ws.location) .map(|ws| &ws.location)
.and_then(|loc| match loc { .and_then(|loc| match loc {
SerializedWorkspaceLocation::Local(_, order) => Some(order.order()), SerializedWorkspaceLocation::Local(paths, order) => {
Some((paths.paths(), order.order()))
}
_ => None, _ => None,
}); });
if let Some(paths_order) = paths_order { if let Some((paths, order)) = workspace_location {
paths_to_open = paths_order // todo: should probably move this logic to a method on the SerializedWorkspaceLocation
// it's only valid for Local and would be more clear there and be able to be tested
// and reused elsewhere
paths_to_open = order
.iter() .iter()
.filter_map(|i| paths_to_open.get(*i).cloned()) .zip(paths.iter())
.collect::<Vec<_>>(); .sorted_by_key(|(i, _)| *i)
if paths_order.iter().enumerate().any(|(i, &j)| i != j) { .map(|(_, path)| path.clone())
.collect();
if order.iter().enumerate().any(|(i, &j)| i != j) {
project_handle project_handle
.update(&mut cx, |project, cx| { .update(&mut cx, |project, cx| {
project.set_worktrees_reordered(true, cx); project.set_worktrees_reordered(true, cx);