Restructure persistence of remote workspaces to make room for WSL and other non-ssh remote projects (#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 <mikayla.c.maki@gmail.com>
This commit is contained in:
Max Brunsfeld 2025-08-22 14:10:45 -07:00 committed by GitHub
parent 639417c2bc
commit f649c31bf9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 784 additions and 1080 deletions

View file

@ -5,7 +5,9 @@ use smallvec::SmallVec;
use ui::App;
use util::{ResultExt, paths::PathExt};
use crate::{NewWindow, SerializedWorkspaceLocation, WORKSPACE_DB, WorkspaceId};
use crate::{
NewWindow, SerializedWorkspaceLocation, WORKSPACE_DB, WorkspaceId, path_list::PathList,
};
pub fn init(cx: &mut App) {
let manager = cx.new(|_| HistoryManager::new());
@ -44,7 +46,13 @@ impl HistoryManager {
.unwrap_or_default()
.into_iter()
.rev()
.map(|(id, location)| HistoryManagerEntry::new(id, &location))
.filter_map(|(id, location, paths)| {
if matches!(location, SerializedWorkspaceLocation::Local) {
Some(HistoryManagerEntry::new(id, &paths))
} else {
None
}
})
.collect::<Vec<_>>();
this.update(cx, |this, cx| {
this.history = recent_folders;
@ -118,9 +126,9 @@ impl HistoryManager {
}
impl HistoryManagerEntry {
pub fn new(id: WorkspaceId, location: &SerializedWorkspaceLocation) -> Self {
let path = location
.sorted_paths()
pub fn new(id: WorkspaceId, paths: &PathList) -> Self {
let path = paths
.paths()
.iter()
.map(|path| path.compact())
.collect::<SmallVec<[PathBuf; 2]>>();

View file

@ -0,0 +1,121 @@
use std::{
path::{Path, PathBuf},
sync::Arc,
};
use util::paths::SanitizedPath;
/// A list of absolute paths, in a specific order.
///
/// The paths are stored in lexicographic order, so that they can be compared to
/// other path lists without regard to the order of the paths.
#[derive(Default, PartialEq, Eq, Debug, Clone)]
pub struct PathList {
paths: Arc<[PathBuf]>,
order: Arc<[usize]>,
}
#[derive(Debug)]
pub struct SerializedPathList {
pub paths: String,
pub order: String,
}
impl PathList {
pub fn new<P: AsRef<Path>>(paths: &[P]) -> Self {
let mut indexed_paths: Vec<(usize, PathBuf)> = paths
.iter()
.enumerate()
.map(|(ix, path)| (ix, SanitizedPath::from(path).into()))
.collect();
indexed_paths.sort_by(|(_, a), (_, b)| a.cmp(b));
let order = indexed_paths.iter().map(|e| e.0).collect::<Vec<_>>().into();
let paths = indexed_paths
.into_iter()
.map(|e| e.1)
.collect::<Vec<_>>()
.into();
Self { order, paths }
}
pub fn is_empty(&self) -> bool {
self.paths.is_empty()
}
pub fn paths(&self) -> &[PathBuf] {
self.paths.as_ref()
}
pub fn order(&self) -> &[usize] {
self.order.as_ref()
}
pub fn is_lexicographically_ordered(&self) -> bool {
self.order.iter().enumerate().all(|(i, &j)| i == j)
}
pub fn deserialize(serialized: &SerializedPathList) -> Self {
let mut paths: Vec<PathBuf> = if serialized.paths.is_empty() {
Vec::new()
} else {
serde_json::from_str::<Vec<PathBuf>>(&serialized.paths)
.unwrap_or(Vec::new())
.into_iter()
.map(|s| SanitizedPath::from(s).into())
.collect()
};
let mut order: Vec<usize> = serialized
.order
.split(',')
.filter_map(|s| s.parse().ok())
.collect();
if !paths.is_sorted() || order.len() != paths.len() {
order = (0..paths.len()).collect();
paths.sort();
}
Self {
paths: paths.into(),
order: order.into(),
}
}
pub fn serialize(&self) -> SerializedPathList {
use std::fmt::Write as _;
let paths = serde_json::to_string(&self.paths).unwrap_or_default();
let mut order = String::new();
for ix in self.order.iter() {
if !order.is_empty() {
order.push(',');
}
write!(&mut order, "{}", *ix).unwrap();
}
SerializedPathList { paths, order }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_path_list() {
let list1 = PathList::new(&["a/d", "a/c"]);
let list2 = PathList::new(&["a/c", "a/d"]);
assert_eq!(list1.paths(), list2.paths());
assert_ne!(list1, list2);
assert_eq!(list1.order(), &[1, 0]);
assert_eq!(list2.order(), &[0, 1]);
let list1_deserialized = PathList::deserialize(&list1.serialize());
assert_eq!(list1_deserialized, list1);
let list2_deserialized = PathList::deserialize(&list2.serialize());
assert_eq!(list2_deserialized, list2);
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,15 +1,16 @@
use super::{SerializedAxis, SerializedWindowBounds};
use crate::{
Member, Pane, PaneAxis, SerializableItemRegistry, Workspace, WorkspaceId, item::ItemHandle,
path_list::PathList,
};
use anyhow::{Context as _, Result};
use anyhow::Result;
use async_recursion::async_recursion;
use db::sqlez::{
bindable::{Bind, Column, StaticColumnCount},
statement::Statement,
};
use gpui::{AsyncWindowContext, Entity, WeakEntity};
use itertools::Itertools as _;
use project::{Project, debugger::breakpoint_store::SourceBreakpoint};
use remote::ssh_session::SshProjectId;
use serde::{Deserialize, Serialize};
@ -18,239 +19,27 @@ use std::{
path::{Path, PathBuf},
sync::Arc,
};
use util::{ResultExt, paths::SanitizedPath};
use util::ResultExt;
use uuid::Uuid;
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct SerializedSshProject {
pub struct SerializedSshConnection {
pub id: SshProjectId,
pub host: String,
pub port: Option<u16>,
pub paths: Vec<String>,
pub user: Option<String>,
}
impl SerializedSshProject {
pub fn ssh_urls(&self) -> Vec<PathBuf> {
self.paths
.iter()
.map(|path| {
let mut result = String::new();
if let Some(user) = &self.user {
result.push_str(user);
result.push('@');
}
result.push_str(&self.host);
if let Some(port) = &self.port {
result.push(':');
result.push_str(&port.to_string());
}
result.push_str(path);
PathBuf::from(result)
})
.collect()
}
}
impl StaticColumnCount for SerializedSshProject {
fn column_count() -> usize {
5
}
}
impl Bind for &SerializedSshProject {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
let next_index = statement.bind(&self.id.0, start_index)?;
let next_index = statement.bind(&self.host, next_index)?;
let next_index = statement.bind(&self.port, next_index)?;
let raw_paths = serde_json::to_string(&self.paths)?;
let next_index = statement.bind(&raw_paths, next_index)?;
statement.bind(&self.user, next_index)
}
}
impl Column for SerializedSshProject {
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
let id = statement.column_int64(start_index)?;
let host = statement.column_text(start_index + 1)?.to_string();
let (port, _) = Option::<u16>::column(statement, start_index + 2)?;
let raw_paths = statement.column_text(start_index + 3)?.to_string();
let paths: Vec<String> = serde_json::from_str(&raw_paths)?;
let (user, _) = Option::<String>::column(statement, start_index + 4)?;
Ok((
Self {
id: SshProjectId(id as u64),
host,
port,
paths,
user,
},
start_index + 5,
))
}
}
#[derive(Debug, PartialEq, Clone)]
pub struct LocalPaths(Arc<Vec<PathBuf>>);
impl LocalPaths {
pub fn new<P: AsRef<Path>>(paths: impl IntoIterator<Item = P>) -> Self {
let mut paths: Vec<PathBuf> = paths
.into_iter()
.map(|p| SanitizedPath::from(p).into())
.collect();
// Ensure all future `zed workspace1 workspace2` and `zed workspace2 workspace1` calls are using the same workspace.
// The actual workspace order is stored in the `LocalPathsOrder` struct.
paths.sort();
Self(Arc::new(paths))
}
pub fn paths(&self) -> &Arc<Vec<PathBuf>> {
&self.0
}
}
impl StaticColumnCount for LocalPaths {}
impl Bind for &LocalPaths {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
statement.bind(&bincode::serialize(&self.0)?, start_index)
}
}
impl Column for LocalPaths {
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
let path_blob = statement.column_blob(start_index)?;
let paths: Arc<Vec<PathBuf>> = if path_blob.is_empty() {
Default::default()
} else {
bincode::deserialize(path_blob).context("Bincode deserialization of paths failed")?
};
Ok((Self(paths), start_index + 1))
}
}
#[derive(Debug, PartialEq, Clone)]
pub struct LocalPathsOrder(Vec<usize>);
impl LocalPathsOrder {
pub fn new(order: impl IntoIterator<Item = usize>) -> Self {
Self(order.into_iter().collect())
}
pub fn order(&self) -> &[usize] {
self.0.as_slice()
}
pub fn default_for_paths(paths: &LocalPaths) -> Self {
Self::new(0..paths.0.len())
}
}
impl StaticColumnCount for LocalPathsOrder {}
impl Bind for &LocalPathsOrder {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
statement.bind(&bincode::serialize(&self.0)?, start_index)
}
}
impl Column for LocalPathsOrder {
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
let order_blob = statement.column_blob(start_index)?;
let order = if order_blob.is_empty() {
Vec::new()
} else {
bincode::deserialize(order_blob).context("deserializing workspace root order")?
};
Ok((Self(order), start_index + 1))
}
}
#[derive(Debug, PartialEq, Clone)]
pub enum SerializedWorkspaceLocation {
Local(LocalPaths, LocalPathsOrder),
Ssh(SerializedSshProject),
Local,
Ssh(SerializedSshConnection),
}
impl SerializedWorkspaceLocation {
/// Create a new `SerializedWorkspaceLocation` from a list of local paths.
///
/// The paths will be sorted and the order will be stored in the `LocalPathsOrder` struct.
///
/// # Examples
///
/// ```
/// use std::path::Path;
/// use zed_workspace::SerializedWorkspaceLocation;
///
/// let location = SerializedWorkspaceLocation::from_local_paths(vec![
/// Path::new("path/to/workspace1"),
/// Path::new("path/to/workspace2"),
/// ]);
/// assert_eq!(location, SerializedWorkspaceLocation::Local(
/// LocalPaths::new(vec![
/// Path::new("path/to/workspace1"),
/// Path::new("path/to/workspace2"),
/// ]),
/// LocalPathsOrder::new(vec![0, 1]),
/// ));
/// ```
///
/// ```
/// use std::path::Path;
/// use zed_workspace::SerializedWorkspaceLocation;
///
/// let location = SerializedWorkspaceLocation::from_local_paths(vec![
/// Path::new("path/to/workspace2"),
/// Path::new("path/to/workspace1"),
/// ]);
///
/// assert_eq!(location, SerializedWorkspaceLocation::Local(
/// LocalPaths::new(vec![
/// Path::new("path/to/workspace1"),
/// Path::new("path/to/workspace2"),
/// ]),
/// LocalPathsOrder::new(vec![1, 0]),
/// ));
/// ```
pub fn from_local_paths<P: AsRef<Path>>(paths: impl IntoIterator<Item = P>) -> Self {
let mut indexed_paths: Vec<_> = paths
.into_iter()
.map(|p| p.as_ref().to_path_buf())
.enumerate()
.collect();
indexed_paths.sort_by(|(_, a), (_, b)| a.cmp(b));
let sorted_paths: Vec<_> = indexed_paths.iter().map(|(_, path)| path.clone()).collect();
let order: Vec<_> = indexed_paths.iter().map(|(index, _)| *index).collect();
Self::Local(LocalPaths::new(sorted_paths), LocalPathsOrder::new(order))
}
/// Get sorted paths
pub fn sorted_paths(&self) -> Arc<Vec<PathBuf>> {
match self {
SerializedWorkspaceLocation::Local(paths, order) => {
if order.order().is_empty() {
paths.paths().clone()
} else {
Arc::new(
order
.order()
.iter()
.zip(paths.paths().iter())
.sorted_by_key(|(i, _)| **i)
.map(|(_, p)| p.clone())
.collect(),
)
}
}
SerializedWorkspaceLocation::Ssh(ssh_project) => Arc::new(ssh_project.ssh_urls()),
}
unimplemented!()
}
}
@ -258,6 +47,7 @@ impl SerializedWorkspaceLocation {
pub(crate) struct SerializedWorkspace {
pub(crate) id: WorkspaceId,
pub(crate) location: SerializedWorkspaceLocation,
pub(crate) paths: PathList,
pub(crate) center_group: SerializedPaneGroup,
pub(crate) window_bounds: Option<SerializedWindowBounds>,
pub(crate) centered_layout: bool,
@ -581,80 +371,3 @@ impl Column for SerializedItem {
))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_serialize_local_paths() {
let paths = vec!["b", "a", "c"];
let serialized = SerializedWorkspaceLocation::from_local_paths(paths);
assert_eq!(
serialized,
SerializedWorkspaceLocation::Local(
LocalPaths::new(vec!["a", "b", "c"]),
LocalPathsOrder::new(vec![1, 0, 2])
)
);
}
#[test]
fn test_sorted_paths() {
let paths = vec!["b", "a", "c"];
let serialized = SerializedWorkspaceLocation::from_local_paths(paths);
assert_eq!(
serialized.sorted_paths(),
Arc::new(vec![
PathBuf::from("b"),
PathBuf::from("a"),
PathBuf::from("c"),
])
);
let paths = Arc::new(vec![
PathBuf::from("a"),
PathBuf::from("b"),
PathBuf::from("c"),
]);
let order = vec![2, 0, 1];
let serialized =
SerializedWorkspaceLocation::Local(LocalPaths(paths), LocalPathsOrder(order));
assert_eq!(
serialized.sorted_paths(),
Arc::new(vec![
PathBuf::from("b"),
PathBuf::from("c"),
PathBuf::from("a"),
])
);
let paths = Arc::new(vec![
PathBuf::from("a"),
PathBuf::from("b"),
PathBuf::from("c"),
]);
let order = vec![];
let serialized =
SerializedWorkspaceLocation::Local(LocalPaths(paths.clone()), LocalPathsOrder(order));
assert_eq!(serialized.sorted_paths(), paths);
let urls = ["/a", "/b", "/c"];
let serialized = SerializedWorkspaceLocation::Ssh(SerializedSshProject {
id: SshProjectId(0),
host: "host".to_string(),
port: Some(22),
paths: urls.iter().map(|s| s.to_string()).collect(),
user: Some("user".to_string()),
});
assert_eq!(
serialized.sorted_paths(),
Arc::new(
urls.iter()
.map(|p| PathBuf::from(format!("user@host:22{}", p)))
.collect()
)
);
}
}

View file

@ -6,6 +6,7 @@ mod modal_layer;
pub mod notifications;
pub mod pane;
pub mod pane_group;
mod path_list;
mod persistence;
pub mod searchable;
pub mod shared_screen;
@ -18,6 +19,7 @@ mod workspace_settings;
pub use crate::notifications::NotificationFrame;
pub use dock::Panel;
pub use path_list::PathList;
pub use toast_layer::{ToastAction, ToastLayer, ToastView};
use anyhow::{Context as _, Result, anyhow};
@ -62,20 +64,20 @@ use notifications::{
};
pub use pane::*;
pub use pane_group::*;
use persistence::{
DB, SerializedWindowBounds,
model::{SerializedSshProject, SerializedWorkspace},
};
use persistence::{DB, SerializedWindowBounds, model::SerializedWorkspace};
pub use persistence::{
DB as WORKSPACE_DB, WorkspaceDb, delete_unloaded_items,
model::{ItemId, LocalPaths, SerializedWorkspaceLocation},
model::{ItemId, SerializedSshConnection, SerializedWorkspaceLocation},
};
use postage::stream::Stream;
use project::{
DirectoryLister, Project, ProjectEntryId, ProjectPath, ResolvedPath, Worktree, WorktreeId,
debugger::{breakpoint_store::BreakpointStoreEvent, session::ThreadStatus},
};
use remote::{SshClientDelegate, SshConnectionOptions, ssh_session::ConnectionIdentifier};
use remote::{
SshClientDelegate, SshConnectionOptions,
ssh_session::{ConnectionIdentifier, SshProjectId},
};
use schemars::JsonSchema;
use serde::Deserialize;
use session::AppSession;
@ -1042,7 +1044,7 @@ pub enum OpenVisible {
enum WorkspaceLocation {
// Valid local paths or SSH project to serialize
Location(SerializedWorkspaceLocation),
Location(SerializedWorkspaceLocation, PathList),
// No valid location found hence clear session id
DetachFromSession,
// No valid location found to serialize
@ -1126,7 +1128,7 @@ pub struct Workspace {
terminal_provider: Option<Box<dyn TerminalProvider>>,
debugger_provider: Option<Arc<dyn DebuggerProvider>>,
serializable_items_tx: UnboundedSender<Box<dyn SerializableItemHandle>>,
serialized_ssh_project: Option<SerializedSshProject>,
serialized_ssh_connection_id: Option<SshProjectId>,
_items_serializer: Task<Result<()>>,
session_id: Option<String>,
scheduled_tasks: Vec<Task<()>>,
@ -1175,8 +1177,6 @@ impl Workspace {
project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded(_) => {
this.update_window_title(window, cx);
this.update_ssh_paths(cx);
this.serialize_ssh_paths(window, cx);
this.serialize_workspace(window, cx);
// This event could be triggered by `AddFolderToProject` or `RemoveFromProject`.
this.update_history(cx);
@ -1461,7 +1461,7 @@ impl Workspace {
serializable_items_tx,
_items_serializer,
session_id: Some(session_id),
serialized_ssh_project: None,
serialized_ssh_connection_id: None,
scheduled_tasks: Vec::new(),
}
}
@ -1501,20 +1501,9 @@ impl Workspace {
let serialized_workspace =
persistence::DB.workspace_for_roots(paths_to_open.as_slice());
let workspace_location = serialized_workspace
.as_ref()
.map(|ws| &ws.location)
.and_then(|loc| match loc {
SerializedWorkspaceLocation::Local(_, order) => {
Some((loc.sorted_paths(), order.order()))
}
_ => None,
});
if let Some((paths, order)) = workspace_location {
paths_to_open = paths.iter().cloned().collect();
if order.iter().enumerate().any(|(i, &j)| i != j) {
if let Some(paths) = serialized_workspace.as_ref().map(|ws| &ws.paths) {
paths_to_open = paths.paths().to_vec();
if !paths.is_lexicographically_ordered() {
project_handle
.update(cx, |project, cx| {
project.set_worktrees_reordered(true, cx);
@ -2034,14 +2023,6 @@ impl Workspace {
self.debugger_provider.clone()
}
pub fn serialized_ssh_project(&self) -> Option<SerializedSshProject> {
self.serialized_ssh_project.clone()
}
pub fn set_serialized_ssh_project(&mut self, serialized_ssh_project: SerializedSshProject) {
self.serialized_ssh_project = Some(serialized_ssh_project);
}
pub fn prompt_for_open_path(
&mut self,
path_prompt_options: PathPromptOptions,
@ -5088,59 +5069,12 @@ impl Workspace {
self.session_id.clone()
}
fn local_paths(&self, cx: &App) -> Option<Vec<Arc<Path>>> {
pub fn root_paths(&self, cx: &App) -> Vec<Arc<Path>> {
let project = self.project().read(cx);
if project.is_local() {
Some(
project
.visible_worktrees(cx)
.map(|worktree| worktree.read(cx).abs_path())
.collect::<Vec<_>>(),
)
} else {
None
}
}
fn update_ssh_paths(&mut self, cx: &App) {
let project = self.project().read(cx);
if !project.is_local() {
let paths: Vec<String> = project
.visible_worktrees(cx)
.map(|worktree| worktree.read(cx).abs_path().to_string_lossy().to_string())
.collect();
if let Some(ssh_project) = &mut self.serialized_ssh_project {
ssh_project.paths = paths;
}
}
}
fn serialize_ssh_paths(&mut self, window: &mut Window, cx: &mut Context<Workspace>) {
if self._schedule_serialize_ssh_paths.is_none() {
self._schedule_serialize_ssh_paths =
Some(cx.spawn_in(window, async move |this, cx| {
cx.background_executor()
.timer(SERIALIZATION_THROTTLE_TIME)
.await;
this.update_in(cx, |this, window, cx| {
let task = if let Some(ssh_project) = &this.serialized_ssh_project {
let ssh_project_id = ssh_project.id;
let ssh_project_paths = ssh_project.paths.clone();
window.spawn(cx, async move |_| {
persistence::DB
.update_ssh_project_paths(ssh_project_id, ssh_project_paths)
.await
})
} else {
Task::ready(Err(anyhow::anyhow!("No SSH project to serialize")))
};
task.detach();
this._schedule_serialize_ssh_paths.take();
})
.log_err();
}));
}
project
.visible_worktrees(cx)
.map(|worktree| worktree.read(cx).abs_path())
.collect::<Vec<_>>()
}
fn remove_panes(&mut self, member: Member, window: &mut Window, cx: &mut Context<Workspace>) {
@ -5313,7 +5247,7 @@ impl Workspace {
}
match self.serialize_workspace_location(cx) {
WorkspaceLocation::Location(location) => {
WorkspaceLocation::Location(location, paths) => {
let breakpoints = self.project.update(cx, |project, cx| {
project
.breakpoint_store()
@ -5327,6 +5261,7 @@ impl Workspace {
let serialized_workspace = SerializedWorkspace {
id: database_id,
location,
paths,
center_group,
window_bounds,
display: Default::default(),
@ -5352,13 +5287,21 @@ impl Workspace {
}
fn serialize_workspace_location(&self, cx: &App) -> WorkspaceLocation {
if let Some(ssh_project) = &self.serialized_ssh_project {
WorkspaceLocation::Location(SerializedWorkspaceLocation::Ssh(ssh_project.clone()))
} else if let Some(local_paths) = self.local_paths(cx) {
if !local_paths.is_empty() {
WorkspaceLocation::Location(SerializedWorkspaceLocation::from_local_paths(
local_paths,
))
let paths = PathList::new(&self.root_paths(cx));
let connection = self.project.read(cx).ssh_connection_options(cx);
if let Some((id, connection)) = self.serialized_ssh_connection_id.zip(connection) {
WorkspaceLocation::Location(
SerializedWorkspaceLocation::Ssh(SerializedSshConnection {
id,
host: connection.host,
port: connection.port,
user: connection.username,
}),
paths,
)
} else if self.project.read(cx).is_local() {
if !paths.is_empty() {
WorkspaceLocation::Location(SerializedWorkspaceLocation::Local, paths)
} else {
WorkspaceLocation::DetachFromSession
}
@ -5371,13 +5314,13 @@ impl Workspace {
let Some(id) = self.database_id() else {
return;
};
let location = match self.serialize_workspace_location(cx) {
WorkspaceLocation::Location(location) => location,
_ => return,
};
if !self.project.read(cx).is_local() {
return;
}
if let Some(manager) = HistoryManager::global(cx) {
let paths = PathList::new(&self.root_paths(cx));
manager.update(cx, |this, cx| {
this.update_history(id, HistoryManagerEntry::new(id, &location), cx);
this.update_history(id, HistoryManagerEntry::new(id, &paths), cx);
});
}
}
@ -6843,14 +6786,14 @@ impl WorkspaceHandle for Entity<Workspace> {
}
}
pub async fn last_opened_workspace_location() -> Option<SerializedWorkspaceLocation> {
pub async fn last_opened_workspace_location() -> Option<(SerializedWorkspaceLocation, PathList)> {
DB.last_workspace().await.log_err().flatten()
}
pub fn last_session_workspace_locations(
last_session_id: &str,
last_session_window_stack: Option<Vec<WindowId>>,
) -> Option<Vec<SerializedWorkspaceLocation>> {
) -> Option<Vec<(SerializedWorkspaceLocation, PathList)>> {
DB.last_session_workspace_locations(last_session_id, last_session_window_stack)
.log_err()
}
@ -7353,7 +7296,7 @@ pub fn open_ssh_project_with_new_connection(
cx: &mut App,
) -> Task<Result<()>> {
cx.spawn(async move |cx| {
let (serialized_ssh_project, workspace_id, serialized_workspace) =
let (workspace_id, serialized_workspace) =
serialize_ssh_project(connection_options.clone(), paths.clone(), cx).await?;
let session = match cx
@ -7387,7 +7330,6 @@ pub fn open_ssh_project_with_new_connection(
open_ssh_project_inner(
project,
paths,
serialized_ssh_project,
workspace_id,
serialized_workspace,
app_state,
@ -7407,13 +7349,12 @@ pub fn open_ssh_project_with_existing_connection(
cx: &mut AsyncApp,
) -> Task<Result<()>> {
cx.spawn(async move |cx| {
let (serialized_ssh_project, workspace_id, serialized_workspace) =
let (workspace_id, serialized_workspace) =
serialize_ssh_project(connection_options.clone(), paths.clone(), cx).await?;
open_ssh_project_inner(
project,
paths,
serialized_ssh_project,
workspace_id,
serialized_workspace,
app_state,
@ -7427,7 +7368,6 @@ pub fn open_ssh_project_with_existing_connection(
async fn open_ssh_project_inner(
project: Entity<Project>,
paths: Vec<PathBuf>,
serialized_ssh_project: SerializedSshProject,
workspace_id: WorkspaceId,
serialized_workspace: Option<SerializedWorkspace>,
app_state: Arc<AppState>,
@ -7480,7 +7420,6 @@ async fn open_ssh_project_inner(
let mut workspace =
Workspace::new(Some(workspace_id), project, app_state.clone(), window, cx);
workspace.set_serialized_ssh_project(serialized_ssh_project);
workspace.update_history(cx);
if let Some(ref serialized) = serialized_workspace {
@ -7517,28 +7456,18 @@ fn serialize_ssh_project(
connection_options: SshConnectionOptions,
paths: Vec<PathBuf>,
cx: &AsyncApp,
) -> Task<
Result<(
SerializedSshProject,
WorkspaceId,
Option<SerializedWorkspace>,
)>,
> {
) -> Task<Result<(WorkspaceId, Option<SerializedWorkspace>)>> {
cx.background_spawn(async move {
let serialized_ssh_project = persistence::DB
.get_or_create_ssh_project(
let ssh_connection_id = persistence::DB
.get_or_create_ssh_connection(
connection_options.host.clone(),
connection_options.port,
paths
.iter()
.map(|path| path.to_string_lossy().to_string())
.collect::<Vec<_>>(),
connection_options.username.clone(),
)
.await?;
let serialized_workspace =
persistence::DB.workspace_for_ssh_project(&serialized_ssh_project);
persistence::DB.ssh_workspace_for_roots(&paths, ssh_connection_id);
let workspace_id = if let Some(workspace_id) =
serialized_workspace.as_ref().map(|workspace| workspace.id)
@ -7548,7 +7477,7 @@ fn serialize_ssh_project(
persistence::DB.next_id().await?
};
Ok((serialized_ssh_project, workspace_id, serialized_workspace))
Ok((workspace_id, serialized_workspace))
})
}
@ -8095,18 +8024,15 @@ pub fn ssh_workspace_position_from_db(
paths_to_open: &[PathBuf],
cx: &App,
) -> Task<Result<WorkspacePosition>> {
let paths = paths_to_open
.iter()
.map(|path| path.to_string_lossy().to_string())
.collect::<Vec<_>>();
let paths = paths_to_open.to_vec();
cx.background_spawn(async move {
let serialized_ssh_project = persistence::DB
.get_or_create_ssh_project(host, port, paths, user)
let ssh_connection_id = persistence::DB
.get_or_create_ssh_connection(host, port, user)
.await
.context("fetching serialized ssh project")?;
let serialized_workspace =
persistence::DB.workspace_for_ssh_project(&serialized_ssh_project);
persistence::DB.ssh_workspace_for_roots(&paths, ssh_connection_id);
let (window_bounds, display) = if let Some(bounds) = window_bounds_env_override() {
(Some(WindowBounds::Windowed(bounds)), None)