Skip to content

Commit 470f033

Browse files
maxbrunsfeldmikayla-maki
authored andcommitted
Restructure persistence of remote workspaces to make room for WSL and other non-ssh remote projects (zed-industries#36714)
This is another pure refactor, to prepare for adding direct WSL support. ### Todo * [x] Represent `paths` in the same way for all workspaces, instead of having a completely separate SSH representation * [x] Adjust sqlite tables * [x] `ssh_projects` -> `ssh_connections` (drop paths) * [x] `workspaces.local_paths` -> `paths` * [x] remove duplicate path columns on `workspaces` * [x] Add migrations for backward-compatibility Release Notes: - N/A --------- Co-authored-by: Mikayla Maki <[email protected]>
1 parent 09facfc commit 470f033

File tree

12 files changed

+782
-1078
lines changed

12 files changed

+782
-1078
lines changed

Cargo.lock

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

crates/client/src/user.rs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,6 @@ impl ProjectId {
4646
}
4747
}
4848

49-
#[derive(
50-
Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, serde::Serialize, serde::Deserialize,
51-
)]
52-
pub struct DevServerProjectId(pub u64);
53-
5449
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5550
pub struct ParticipantIndex(pub u32);
5651

crates/recent_projects/src/disconnected_overlay.rs

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
use std::path::PathBuf;
2-
31
use gpui::{ClickEvent, DismissEvent, EventEmitter, FocusHandle, Focusable, Render, WeakEntity};
42
use project::project_settings::ProjectSettings;
53
use remote::SshConnectionOptions;
@@ -103,17 +101,17 @@ impl DisconnectedOverlay {
103101
return;
104102
};
105103

106-
let Some(ssh_project) = workspace.read(cx).serialized_ssh_project() else {
107-
return;
108-
};
109-
110104
let Some(window_handle) = window.window_handle().downcast::<Workspace>() else {
111105
return;
112106
};
113107

114108
let app_state = workspace.read(cx).app_state().clone();
115-
116-
let paths = ssh_project.paths.iter().map(PathBuf::from).collect();
109+
let paths = workspace
110+
.read(cx)
111+
.root_paths(cx)
112+
.iter()
113+
.map(|path| path.to_path_buf())
114+
.collect();
117115

118116
cx.spawn_in(window, async move |_, cx| {
119117
open_ssh_project(

crates/recent_projects/src/recent_projects.rs

Lines changed: 30 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,12 @@ use picker::{
1919
pub use remote_servers::RemoteServerProjects;
2020
use settings::Settings;
2121
pub use ssh_connections::SshSettings;
22-
use std::{
23-
path::{Path, PathBuf},
24-
sync::Arc,
25-
};
22+
use std::{path::Path, sync::Arc};
2623
use ui::{KeyBinding, ListItem, ListItemSpacing, Tooltip, prelude::*, tooltip_container};
2724
use util::{ResultExt, paths::PathExt};
2825
use workspace::{
29-
CloseIntent, HistoryManager, ModalView, OpenOptions, SerializedWorkspaceLocation, WORKSPACE_DB,
30-
Workspace, WorkspaceId, with_active_or_new_workspace,
26+
CloseIntent, HistoryManager, ModalView, OpenOptions, PathList, SerializedWorkspaceLocation,
27+
WORKSPACE_DB, Workspace, WorkspaceId, with_active_or_new_workspace,
3128
};
3229
use zed_actions::{OpenRecent, OpenRemote};
3330

@@ -154,7 +151,7 @@ impl Render for RecentProjects {
154151

155152
pub struct RecentProjectsDelegate {
156153
workspace: WeakEntity<Workspace>,
157-
workspaces: Vec<(WorkspaceId, SerializedWorkspaceLocation)>,
154+
workspaces: Vec<(WorkspaceId, SerializedWorkspaceLocation, PathList)>,
158155
selected_match_index: usize,
159156
matches: Vec<StringMatch>,
160157
render_paths: bool,
@@ -178,12 +175,15 @@ impl RecentProjectsDelegate {
178175
}
179176
}
180177

181-
pub fn set_workspaces(&mut self, workspaces: Vec<(WorkspaceId, SerializedWorkspaceLocation)>) {
178+
pub fn set_workspaces(
179+
&mut self,
180+
workspaces: Vec<(WorkspaceId, SerializedWorkspaceLocation, PathList)>,
181+
) {
182182
self.workspaces = workspaces;
183183
self.has_any_non_local_projects = !self
184184
.workspaces
185185
.iter()
186-
.all(|(_, location)| matches!(location, SerializedWorkspaceLocation::Local(_, _)));
186+
.all(|(_, location, _)| matches!(location, SerializedWorkspaceLocation::Local));
187187
}
188188
}
189189
impl EventEmitter<DismissEvent> for RecentProjectsDelegate {}
@@ -236,15 +236,14 @@ impl PickerDelegate for RecentProjectsDelegate {
236236
.workspaces
237237
.iter()
238238
.enumerate()
239-
.filter(|(_, (id, _))| !self.is_current_workspace(*id, cx))
240-
.map(|(id, (_, location))| {
241-
let combined_string = location
242-
.sorted_paths()
239+
.filter(|(_, (id, _, _))| !self.is_current_workspace(*id, cx))
240+
.map(|(id, (_, _, paths))| {
241+
let combined_string = paths
242+
.paths()
243243
.iter()
244244
.map(|path| path.compact().to_string_lossy().into_owned())
245245
.collect::<Vec<_>>()
246246
.join("");
247-
248247
StringMatchCandidate::new(id, &combined_string)
249248
})
250249
.collect::<Vec<_>>();
@@ -279,7 +278,7 @@ impl PickerDelegate for RecentProjectsDelegate {
279278
.get(self.selected_index())
280279
.zip(self.workspace.upgrade())
281280
{
282-
let (candidate_workspace_id, candidate_workspace_location) =
281+
let (candidate_workspace_id, candidate_workspace_location, candidate_workspace_paths) =
283282
&self.workspaces[selected_match.candidate_id];
284283
let replace_current_window = if self.create_new_window {
285284
secondary
@@ -292,8 +291,8 @@ impl PickerDelegate for RecentProjectsDelegate {
292291
Task::ready(Ok(()))
293292
} else {
294293
match candidate_workspace_location {
295-
SerializedWorkspaceLocation::Local(paths, _) => {
296-
let paths = paths.paths().to_vec();
294+
SerializedWorkspaceLocation::Local => {
295+
let paths = candidate_workspace_paths.paths().to_vec();
297296
if replace_current_window {
298297
cx.spawn_in(window, async move |workspace, cx| {
299298
let continue_replacing = workspace
@@ -321,7 +320,7 @@ impl PickerDelegate for RecentProjectsDelegate {
321320
workspace.open_workspace_for_paths(false, paths, window, cx)
322321
}
323322
}
324-
SerializedWorkspaceLocation::Ssh(ssh_project) => {
323+
SerializedWorkspaceLocation::Ssh(connection) => {
325324
let app_state = workspace.app_state().clone();
326325

327326
let replace_window = if replace_current_window {
@@ -337,12 +336,12 @@ impl PickerDelegate for RecentProjectsDelegate {
337336

338337
let connection_options = SshSettings::get_global(cx)
339338
.connection_options_for(
340-
ssh_project.host.clone(),
341-
ssh_project.port,
342-
ssh_project.user.clone(),
339+
connection.host.clone(),
340+
connection.port,
341+
connection.user.clone(),
343342
);
344343

345-
let paths = ssh_project.paths.iter().map(PathBuf::from).collect();
344+
let paths = candidate_workspace_paths.paths().to_vec();
346345

347346
cx.spawn_in(window, async move |_, cx| {
348347
open_ssh_project(
@@ -383,12 +382,12 @@ impl PickerDelegate for RecentProjectsDelegate {
383382
) -> Option<Self::ListItem> {
384383
let hit = self.matches.get(ix)?;
385384

386-
let (_, location) = self.workspaces.get(hit.candidate_id)?;
385+
let (_, location, paths) = self.workspaces.get(hit.candidate_id)?;
387386

388387
let mut path_start_offset = 0;
389388

390-
let (match_labels, paths): (Vec<_>, Vec<_>) = location
391-
.sorted_paths()
389+
let (match_labels, paths): (Vec<_>, Vec<_>) = paths
390+
.paths()
392391
.iter()
393392
.map(|p| p.compact())
394393
.map(|path| {
@@ -416,11 +415,9 @@ impl PickerDelegate for RecentProjectsDelegate {
416415
.gap_3()
417416
.when(self.has_any_non_local_projects, |this| {
418417
this.child(match location {
419-
SerializedWorkspaceLocation::Local(_, _) => {
420-
Icon::new(IconName::Screen)
421-
.color(Color::Muted)
422-
.into_any_element()
423-
}
418+
SerializedWorkspaceLocation::Local => Icon::new(IconName::Screen)
419+
.color(Color::Muted)
420+
.into_any_element(),
424421
SerializedWorkspaceLocation::Ssh(_) => Icon::new(IconName::Server)
425422
.color(Color::Muted)
426423
.into_any_element(),
@@ -568,7 +565,7 @@ impl RecentProjectsDelegate {
568565
cx: &mut Context<Picker<Self>>,
569566
) {
570567
if let Some(selected_match) = self.matches.get(ix) {
571-
let (workspace_id, _) = self.workspaces[selected_match.candidate_id];
568+
let (workspace_id, _, _) = self.workspaces[selected_match.candidate_id];
572569
cx.spawn_in(window, async move |this, cx| {
573570
let _ = WORKSPACE_DB.delete_workspace_by_id(workspace_id).await;
574571
let workspaces = WORKSPACE_DB
@@ -707,7 +704,8 @@ mod tests {
707704
}];
708705
delegate.set_workspaces(vec![(
709706
WorkspaceId::default(),
710-
SerializedWorkspaceLocation::from_local_paths(vec![path!("/test/path/")]),
707+
SerializedWorkspaceLocation::Local,
708+
PathList::new(&[path!("/test/path")]),
711709
)]);
712710
});
713711
})

crates/workspace/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ test-support = [
2929
any_vec.workspace = true
3030
anyhow.workspace = true
3131
async-recursion.workspace = true
32-
bincode.workspace = true
3332
call.workspace = true
3433
client.workspace = true
3534
clock.workspace = true
@@ -80,5 +79,6 @@ project = { workspace = true, features = ["test-support"] }
8079
session = { workspace = true, features = ["test-support"] }
8180
settings = { workspace = true, features = ["test-support"] }
8281
http_client = { workspace = true, features = ["test-support"] }
82+
pretty_assertions.workspace = true
8383
tempfile.workspace = true
8484
zlog.workspace = true

crates/workspace/src/history_manager.rs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ use smallvec::SmallVec;
55
use ui::App;
66
use util::{ResultExt, paths::PathExt};
77

8-
use crate::{NewWindow, SerializedWorkspaceLocation, WORKSPACE_DB, WorkspaceId};
8+
use crate::{
9+
NewWindow, SerializedWorkspaceLocation, WORKSPACE_DB, WorkspaceId, path_list::PathList,
10+
};
911

1012
pub fn init(cx: &mut App) {
1113
let manager = cx.new(|_| HistoryManager::new());
@@ -44,7 +46,13 @@ impl HistoryManager {
4446
.unwrap_or_default()
4547
.into_iter()
4648
.rev()
47-
.map(|(id, location)| HistoryManagerEntry::new(id, &location))
49+
.filter_map(|(id, location, paths)| {
50+
if matches!(location, SerializedWorkspaceLocation::Local) {
51+
Some(HistoryManagerEntry::new(id, &paths))
52+
} else {
53+
None
54+
}
55+
})
4856
.collect::<Vec<_>>();
4957
this.update(cx, |this, cx| {
5058
this.history = recent_folders;
@@ -118,9 +126,9 @@ impl HistoryManager {
118126
}
119127

120128
impl HistoryManagerEntry {
121-
pub fn new(id: WorkspaceId, location: &SerializedWorkspaceLocation) -> Self {
122-
let path = location
123-
.sorted_paths()
129+
pub fn new(id: WorkspaceId, paths: &PathList) -> Self {
130+
let path = paths
131+
.paths()
124132
.iter()
125133
.map(|path| path.compact())
126134
.collect::<SmallVec<[PathBuf; 2]>>();

crates/workspace/src/path_list.rs

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
use std::{
2+
path::{Path, PathBuf},
3+
sync::Arc,
4+
};
5+
6+
use util::paths::SanitizedPath;
7+
8+
/// A list of absolute paths, in a specific order.
9+
///
10+
/// The paths are stored in lexicographic order, so that they can be compared to
11+
/// other path lists without regard to the order of the paths.
12+
#[derive(Default, PartialEq, Eq, Debug, Clone)]
13+
pub struct PathList {
14+
paths: Arc<[PathBuf]>,
15+
order: Arc<[usize]>,
16+
}
17+
18+
#[derive(Debug)]
19+
pub struct SerializedPathList {
20+
pub paths: String,
21+
pub order: String,
22+
}
23+
24+
impl PathList {
25+
pub fn new<P: AsRef<Path>>(paths: &[P]) -> Self {
26+
let mut indexed_paths: Vec<(usize, PathBuf)> = paths
27+
.iter()
28+
.enumerate()
29+
.map(|(ix, path)| (ix, SanitizedPath::from(path).into()))
30+
.collect();
31+
indexed_paths.sort_by(|(_, a), (_, b)| a.cmp(b));
32+
let order = indexed_paths.iter().map(|e| e.0).collect::<Vec<_>>().into();
33+
let paths = indexed_paths
34+
.into_iter()
35+
.map(|e| e.1)
36+
.collect::<Vec<_>>()
37+
.into();
38+
Self { order, paths }
39+
}
40+
41+
pub fn is_empty(&self) -> bool {
42+
self.paths.is_empty()
43+
}
44+
45+
pub fn paths(&self) -> &[PathBuf] {
46+
self.paths.as_ref()
47+
}
48+
49+
pub fn order(&self) -> &[usize] {
50+
self.order.as_ref()
51+
}
52+
53+
pub fn is_lexicographically_ordered(&self) -> bool {
54+
self.order.iter().enumerate().all(|(i, &j)| i == j)
55+
}
56+
57+
pub fn deserialize(serialized: &SerializedPathList) -> Self {
58+
let mut paths: Vec<PathBuf> = if serialized.paths.is_empty() {
59+
Vec::new()
60+
} else {
61+
serde_json::from_str::<Vec<PathBuf>>(&serialized.paths)
62+
.unwrap_or(Vec::new())
63+
.into_iter()
64+
.map(|s| SanitizedPath::from(s).into())
65+
.collect()
66+
};
67+
68+
let mut order: Vec<usize> = serialized
69+
.order
70+
.split(',')
71+
.filter_map(|s| s.parse().ok())
72+
.collect();
73+
74+
if !paths.is_sorted() || order.len() != paths.len() {
75+
order = (0..paths.len()).collect();
76+
paths.sort();
77+
}
78+
79+
Self {
80+
paths: paths.into(),
81+
order: order.into(),
82+
}
83+
}
84+
85+
pub fn serialize(&self) -> SerializedPathList {
86+
use std::fmt::Write as _;
87+
88+
let paths = serde_json::to_string(&self.paths).unwrap_or_default();
89+
90+
let mut order = String::new();
91+
for ix in self.order.iter() {
92+
if !order.is_empty() {
93+
order.push(',');
94+
}
95+
write!(&mut order, "{}", *ix).unwrap();
96+
}
97+
SerializedPathList { paths, order }
98+
}
99+
}
100+
101+
#[cfg(test)]
102+
mod tests {
103+
use super::*;
104+
105+
#[test]
106+
fn test_path_list() {
107+
let list1 = PathList::new(&["a/d", "a/c"]);
108+
let list2 = PathList::new(&["a/c", "a/d"]);
109+
110+
assert_eq!(list1.paths(), list2.paths());
111+
assert_ne!(list1, list2);
112+
assert_eq!(list1.order(), &[1, 0]);
113+
assert_eq!(list2.order(), &[0, 1]);
114+
115+
let list1_deserialized = PathList::deserialize(&list1.serialize());
116+
assert_eq!(list1_deserialized, list1);
117+
118+
let list2_deserialized = PathList::deserialize(&list2.serialize());
119+
assert_eq!(list2_deserialized, list2);
120+
}
121+
}

0 commit comments

Comments
 (0)