remote projects per user (#10594)

Release Notes:

- Made remote projects per-user instead of per-channel. If you'd like to
be part of the remote development alpha, please email hi@zed.dev.

---------

Co-authored-by: Bennet Bo Fenner <53836821+bennetbo@users.noreply.github.com>
Co-authored-by: Bennet <bennetbo@gmx.de>
Co-authored-by: Nate Butler <1714999+iamnbutler@users.noreply.github.com>
Co-authored-by: Nate Butler <iamnbutler@gmail.com>
This commit is contained in:
Conrad Irwin 2024-04-23 15:33:09 -06:00 committed by GitHub
parent 8ae4c3277f
commit e0c83a1d32
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
56 changed files with 2807 additions and 1625 deletions

View file

@ -45,6 +45,7 @@ node_runtime.workspace = true
parking_lot.workspace = true
postage.workspace = true
project.workspace = true
remote_projects.workspace = true
task.workspace = true
schemars.workspace = true
serde.workspace = true

View file

@ -513,8 +513,9 @@ impl<T: Item> ItemHandle for View<T> {
}));
}
let mut event_subscription =
Some(cx.subscribe(self, move |workspace, item, event, cx| {
let mut event_subscription = Some(cx.subscribe(
self,
move |workspace, item: View<T>, event, cx| {
let pane = if let Some(pane) = workspace
.panes_by_item
.get(&item.item_id())
@ -575,7 +576,8 @@ impl<T: Item> ItemHandle for View<T> {
_ => {}
});
}));
},
));
cx.on_blur(&self.focus_handle(cx), move |workspace, cx| {
if WorkspaceSettings::get_global(cx).autosave == AutosaveSetting::OnFocusChange {

View file

@ -3,6 +3,7 @@ pub mod model;
use std::path::Path;
use anyhow::{anyhow, bail, Context, Result};
use client::RemoteProjectId;
use db::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql};
use gpui::{point, size, Axis, Bounds};
@ -17,11 +18,11 @@ use uuid::Uuid;
use crate::WorkspaceId;
use model::{
GroupId, PaneId, SerializedItem, SerializedPane, SerializedPaneGroup, SerializedWorkspace,
WorkspaceLocation,
GroupId, LocalPaths, PaneId, SerializedItem, SerializedPane, SerializedPaneGroup,
SerializedWorkspace,
};
use self::model::DockStructure;
use self::model::{DockStructure, SerializedRemoteProject, SerializedWorkspaceLocation};
#[derive(Copy, Clone, Debug, PartialEq)]
pub(crate) struct SerializedAxis(pub(crate) gpui::Axis);
@ -125,7 +126,7 @@ define_connection! {
//
// workspaces(
// workspace_id: usize, // Primary key for workspaces
// workspace_location: Bincode<Vec<PathBuf>>,
// local_paths: Bincode<Vec<PathBuf>>,
// dock_visible: bool, // Deprecated
// dock_anchor: DockAnchor, // Deprecated
// dock_pane: Option<usize>, // Deprecated
@ -289,6 +290,15 @@ define_connection! {
sql!(
ALTER TABLE workspaces ADD COLUMN centered_layout INTEGER; //bool
),
sql!(
CREATE TABLE remote_projects (
remote_project_id INTEGER NOT NULL UNIQUE,
path TEXT,
dev_server_name TEXT
);
ALTER TABLE workspaces ADD COLUMN remote_project_id INTEGER;
ALTER TABLE workspaces RENAME COLUMN workspace_location TO local_paths;
),
];
}
@ -300,13 +310,23 @@ impl WorkspaceDb {
&self,
worktree_roots: &[P],
) -> Option<SerializedWorkspace> {
let workspace_location: WorkspaceLocation = worktree_roots.into();
let local_paths = LocalPaths::new(worktree_roots);
// Note that we re-assign the workspace_id here in case it's empty
// and we've grabbed the most recent workspace
let (workspace_id, workspace_location, bounds, display, fullscreen, centered_layout, docks): (
let (
workspace_id,
local_paths,
remote_project_id,
bounds,
display,
fullscreen,
centered_layout,
docks,
): (
WorkspaceId,
WorkspaceLocation,
Option<LocalPaths>,
Option<u64>,
Option<SerializedWindowsBounds>,
Option<Uuid>,
Option<bool>,
@ -316,7 +336,8 @@ impl WorkspaceDb {
.select_row_bound(sql! {
SELECT
workspace_id,
workspace_location,
local_paths,
remote_project_id,
window_state,
window_x,
window_y,
@ -335,16 +356,34 @@ impl WorkspaceDb {
bottom_dock_active_panel,
bottom_dock_zoom
FROM workspaces
WHERE workspace_location = ?
WHERE local_paths = ?
})
.and_then(|mut prepared_statement| (prepared_statement)(&workspace_location))
.and_then(|mut prepared_statement| (prepared_statement)(&local_paths))
.context("No workspaces found")
.warn_on_err()
.flatten()?;
let location = if let Some(remote_project_id) = remote_project_id {
let remote_project: SerializedRemoteProject = self
.select_row_bound(sql! {
SELECT remote_project_id, path, dev_server_name
FROM remote_projects
WHERE remote_project_id = ?
})
.and_then(|mut prepared_statement| (prepared_statement)(remote_project_id))
.context("No remote project found")
.warn_on_err()
.flatten()?;
SerializedWorkspaceLocation::Remote(remote_project)
} else if let Some(local_paths) = local_paths {
SerializedWorkspaceLocation::Local(local_paths)
} else {
return None;
};
Some(SerializedWorkspace {
id: workspace_id,
location: workspace_location.clone(),
location,
center_group: self
.get_center_pane_group(workspace_id)
.context("Getting center group")
@ -368,43 +407,102 @@ impl WorkspaceDb {
DELETE FROM panes WHERE workspace_id = ?1;))?(workspace.id)
.context("Clearing old panes")?;
conn.exec_bound(sql!(
DELETE FROM workspaces WHERE workspace_location = ? AND workspace_id != ?
))?((&workspace.location, workspace.id))
.context("clearing out old locations")?;
match workspace.location {
SerializedWorkspaceLocation::Local(local_paths) => {
conn.exec_bound(sql!(
DELETE FROM workspaces WHERE local_paths = ? AND workspace_id != ?
))?((&local_paths, workspace.id))
.context("clearing out old locations")?;
// Upsert
conn.exec_bound(sql!(
INSERT INTO workspaces(
workspace_id,
workspace_location,
left_dock_visible,
left_dock_active_panel,
left_dock_zoom,
right_dock_visible,
right_dock_active_panel,
right_dock_zoom,
bottom_dock_visible,
bottom_dock_active_panel,
bottom_dock_zoom,
timestamp
)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, CURRENT_TIMESTAMP)
ON CONFLICT DO
UPDATE SET
workspace_location = ?2,
left_dock_visible = ?3,
left_dock_active_panel = ?4,
left_dock_zoom = ?5,
right_dock_visible = ?6,
right_dock_active_panel = ?7,
right_dock_zoom = ?8,
bottom_dock_visible = ?9,
bottom_dock_active_panel = ?10,
bottom_dock_zoom = ?11,
timestamp = CURRENT_TIMESTAMP
))?((workspace.id, &workspace.location, workspace.docks))
.context("Updating workspace")?;
// Upsert
conn.exec_bound(sql!(
INSERT INTO workspaces(
workspace_id,
local_paths,
left_dock_visible,
left_dock_active_panel,
left_dock_zoom,
right_dock_visible,
right_dock_active_panel,
right_dock_zoom,
bottom_dock_visible,
bottom_dock_active_panel,
bottom_dock_zoom,
timestamp
)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, CURRENT_TIMESTAMP)
ON CONFLICT DO
UPDATE SET
local_paths = ?2,
left_dock_visible = ?3,
left_dock_active_panel = ?4,
left_dock_zoom = ?5,
right_dock_visible = ?6,
right_dock_active_panel = ?7,
right_dock_zoom = ?8,
bottom_dock_visible = ?9,
bottom_dock_active_panel = ?10,
bottom_dock_zoom = ?11,
timestamp = CURRENT_TIMESTAMP
))?((workspace.id, &local_paths, workspace.docks))
.context("Updating workspace")?;
}
SerializedWorkspaceLocation::Remote(remote_project) => {
conn.exec_bound(sql!(
DELETE FROM workspaces WHERE remote_project_id = ? AND workspace_id != ?
))?((remote_project.id.0, workspace.id))
.context("clearing out old locations")?;
conn.exec_bound(sql!(
INSERT INTO remote_projects(
remote_project_id,
path,
dev_server_name
) VALUES (?1, ?2, ?3)
ON CONFLICT DO
UPDATE SET
path = ?2,
dev_server_name = ?3
))?(&remote_project)?;
// Upsert
conn.exec_bound(sql!(
INSERT INTO workspaces(
workspace_id,
remote_project_id,
left_dock_visible,
left_dock_active_panel,
left_dock_zoom,
right_dock_visible,
right_dock_active_panel,
right_dock_zoom,
bottom_dock_visible,
bottom_dock_active_panel,
bottom_dock_zoom,
timestamp
)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, CURRENT_TIMESTAMP)
ON CONFLICT DO
UPDATE SET
remote_project_id = ?2,
left_dock_visible = ?3,
left_dock_active_panel = ?4,
left_dock_zoom = ?5,
right_dock_visible = ?6,
right_dock_active_panel = ?7,
right_dock_zoom = ?8,
bottom_dock_visible = ?9,
bottom_dock_active_panel = ?10,
bottom_dock_zoom = ?11,
timestamp = CURRENT_TIMESTAMP
))?((
workspace.id,
remote_project.id.0,
workspace.docks,
))
.context("Updating workspace")?;
}
}
// Save center pane group
Self::save_pane_group(conn, workspace.id, &workspace.center_group, None)
@ -424,24 +522,43 @@ impl WorkspaceDb {
}
query! {
fn recent_workspaces() -> Result<Vec<(WorkspaceId, WorkspaceLocation)>> {
SELECT workspace_id, workspace_location
fn recent_workspaces() -> Result<Vec<(WorkspaceId, LocalPaths, Option<u64>)>> {
SELECT workspace_id, local_paths, remote_project_id
FROM workspaces
WHERE workspace_location IS NOT NULL
WHERE local_paths IS NOT NULL OR remote_project_id IS NOT NULL
ORDER BY timestamp DESC
}
}
query! {
pub fn last_window() -> Result<(Option<Uuid>, Option<SerializedWindowsBounds>, Option<bool>)> {
SELECT display, window_state, window_x, window_y, window_width, window_height, fullscreen
FROM workspaces
WHERE workspace_location IS NOT NULL
ORDER BY timestamp DESC
LIMIT 1
fn remote_projects() -> Result<Vec<SerializedRemoteProject>> {
SELECT remote_project_id, path, dev_server_name
FROM remote_projects
}
}
pub(crate) fn last_window(
&self,
) -> anyhow::Result<(Option<Uuid>, Option<SerializedWindowsBounds>, Option<bool>)> {
let mut prepared_query =
self.select::<(Option<Uuid>, Option<SerializedWindowsBounds>, Option<bool>)>(sql!(
SELECT
display,
window_state, window_x, window_y, window_width, window_height,
fullscreen
FROM workspaces
WHERE local_paths
IS NOT NULL
ORDER BY timestamp DESC
LIMIT 1
))?;
let result = prepared_query()?;
Ok(result
.into_iter()
.next()
.unwrap_or_else(|| (None, None, None)))
}
query! {
pub async fn delete_workspace_by_id(id: WorkspaceId) -> Result<()> {
DELETE FROM workspaces
@ -451,14 +568,29 @@ impl WorkspaceDb {
// Returns the recent locations which are still valid on disk and deletes ones which no longer
// exist.
pub async fn recent_workspaces_on_disk(&self) -> Result<Vec<(WorkspaceId, WorkspaceLocation)>> {
pub async fn recent_workspaces_on_disk(
&self,
) -> Result<Vec<(WorkspaceId, SerializedWorkspaceLocation)>> {
let mut result = Vec::new();
let mut delete_tasks = Vec::new();
for (id, location) in self.recent_workspaces()? {
let remote_projects = self.remote_projects()?;
for (id, location, remote_project_id) in self.recent_workspaces()? {
if let Some(remote_project_id) = remote_project_id.map(RemoteProjectId) {
if let Some(remote_project) =
remote_projects.iter().find(|rp| rp.id == remote_project_id)
{
result.push((id, remote_project.clone().into()));
} else {
delete_tasks.push(self.delete_workspace_by_id(id));
}
continue;
}
if location.paths().iter().all(|path| path.exists())
&& location.paths().iter().any(|path| path.is_dir())
{
result.push((id, location));
result.push((id, location.into()));
} else {
delete_tasks.push(self.delete_workspace_by_id(id));
}
@ -468,13 +600,16 @@ impl WorkspaceDb {
Ok(result)
}
pub async fn last_workspace(&self) -> Result<Option<WorkspaceLocation>> {
pub async fn last_workspace(&self) -> Result<Option<LocalPaths>> {
Ok(self
.recent_workspaces_on_disk()
.await?
.into_iter()
.next()
.map(|(_, location)| location))
.filter_map(|(_, location)| match location {
SerializedWorkspaceLocation::Local(local_paths) => Some(local_paths),
SerializedWorkspaceLocation::Remote(_) => None,
})
.next())
}
fn get_center_pane_group(&self, workspace_id: WorkspaceId) -> Result<SerializedPaneGroup> {
@ -774,7 +909,7 @@ mod tests {
let mut workspace_1 = SerializedWorkspace {
id: WorkspaceId(1),
location: (["/tmp", "/tmp2"]).into(),
location: LocalPaths::new(["/tmp", "/tmp2"]).into(),
center_group: Default::default(),
bounds: Default::default(),
display: Default::default(),
@ -785,7 +920,7 @@ mod tests {
let workspace_2 = SerializedWorkspace {
id: WorkspaceId(2),
location: (["/tmp"]).into(),
location: LocalPaths::new(["/tmp"]).into(),
center_group: Default::default(),
bounds: Default::default(),
display: Default::default(),
@ -812,7 +947,7 @@ mod tests {
})
.await;
workspace_1.location = (["/tmp", "/tmp3"]).into();
workspace_1.location = LocalPaths::new(["/tmp", "/tmp3"]).into();
db.save_workspace(workspace_1.clone()).await;
db.save_workspace(workspace_1).await;
db.save_workspace(workspace_2).await;
@ -885,7 +1020,7 @@ mod tests {
let workspace = SerializedWorkspace {
id: WorkspaceId(5),
location: (["/tmp", "/tmp2"]).into(),
location: LocalPaths::new(["/tmp", "/tmp2"]).into(),
center_group,
bounds: Default::default(),
display: Default::default(),
@ -915,7 +1050,7 @@ mod tests {
let workspace_1 = SerializedWorkspace {
id: WorkspaceId(1),
location: (["/tmp", "/tmp2"]).into(),
location: LocalPaths::new(["/tmp", "/tmp2"]).into(),
center_group: Default::default(),
bounds: Default::default(),
display: Default::default(),
@ -926,7 +1061,7 @@ mod tests {
let mut workspace_2 = SerializedWorkspace {
id: WorkspaceId(2),
location: (["/tmp"]).into(),
location: LocalPaths::new(["/tmp"]).into(),
center_group: Default::default(),
bounds: Default::default(),
display: Default::default(),
@ -953,7 +1088,7 @@ mod tests {
assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
// Test 'mutate' case of updating a pre-existing id
workspace_2.location = (["/tmp", "/tmp2"]).into();
workspace_2.location = LocalPaths::new(["/tmp", "/tmp2"]).into();
db.save_workspace(workspace_2.clone()).await;
assert_eq!(
@ -964,7 +1099,7 @@ mod tests {
// Test other mechanism for mutating
let mut workspace_3 = SerializedWorkspace {
id: WorkspaceId(3),
location: (&["/tmp", "/tmp2"]).into(),
location: LocalPaths::new(&["/tmp", "/tmp2"]).into(),
center_group: Default::default(),
bounds: Default::default(),
display: Default::default(),
@ -980,7 +1115,7 @@ mod tests {
);
// Make sure that updating paths differently also works
workspace_3.location = (["/tmp3", "/tmp4", "/tmp2"]).into();
workspace_3.location = LocalPaths::new(["/tmp3", "/tmp4", "/tmp2"]).into();
db.save_workspace(workspace_3.clone()).await;
assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
assert_eq!(
@ -999,7 +1134,7 @@ mod tests {
) -> SerializedWorkspace {
SerializedWorkspace {
id: WorkspaceId(4),
location: workspace_id.into(),
location: LocalPaths::new(workspace_id).into(),
center_group: center_group.clone(),
bounds: Default::default(),
display: Default::default(),

View file

@ -2,12 +2,14 @@ use super::SerializedAxis;
use crate::{item::ItemHandle, ItemDeserializers, Member, Pane, PaneAxis, Workspace, WorkspaceId};
use anyhow::{Context, Result};
use async_recursion::async_recursion;
use client::RemoteProjectId;
use db::sqlez::{
bindable::{Bind, Column, StaticColumnCount},
statement::Statement,
};
use gpui::{AsyncWindowContext, Bounds, DevicePixels, Model, Task, View, WeakView};
use project::Project;
use serde::{Deserialize, Serialize};
use std::{
path::{Path, PathBuf},
sync::Arc,
@ -15,59 +17,98 @@ use std::{
use util::ResultExt;
use uuid::Uuid;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorkspaceLocation(Arc<Vec<PathBuf>>);
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub struct SerializedRemoteProject {
pub id: RemoteProjectId,
pub dev_server_name: String,
pub path: String,
}
#[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| p.as_ref().to_path_buf())
.collect();
paths.sort();
Self(Arc::new(paths))
}
impl WorkspaceLocation {
pub fn paths(&self) -> Arc<Vec<PathBuf>> {
self.0.clone()
}
}
#[cfg(any(test, feature = "test-support"))]
pub fn new<P: AsRef<Path>>(paths: Vec<P>) -> Self {
Self(Arc::new(
paths
.into_iter()
.map(|p| p.as_ref().to_path_buf())
.collect(),
))
impl From<LocalPaths> for SerializedWorkspaceLocation {
fn from(local_paths: LocalPaths) -> Self {
Self::Local(local_paths)
}
}
impl<P: AsRef<Path>, T: IntoIterator<Item = P>> From<T> for WorkspaceLocation {
fn from(iterator: T) -> Self {
let mut roots = iterator
.into_iter()
.map(|p| p.as_ref().to_path_buf())
.collect::<Vec<_>>();
roots.sort();
Self(Arc::new(roots))
}
}
impl StaticColumnCount for WorkspaceLocation {}
impl Bind for &WorkspaceLocation {
impl StaticColumnCount for LocalPaths {}
impl Bind for &LocalPaths {
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
bincode::serialize(&self.0)
.expect("Bincode serialization of paths should not fail")
.bind(statement, start_index)
statement.bind(&bincode::serialize(&self.0)?, start_index)
}
}
impl Column for WorkspaceLocation {
impl Column for LocalPaths {
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
let blob = statement.column_blob(start_index)?;
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))
}
}
impl From<SerializedRemoteProject> for SerializedWorkspaceLocation {
fn from(remote_project: SerializedRemoteProject) -> Self {
Self::Remote(remote_project)
}
}
impl StaticColumnCount for SerializedRemoteProject {}
impl Bind for &SerializedRemoteProject {
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.dev_server_name, next_index)?;
statement.bind(&self.path, next_index)
}
}
impl Column for SerializedRemoteProject {
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
let id = statement.column_int64(start_index)?;
let dev_server_name = statement.column_text(start_index + 1)?.to_string();
let path = statement.column_text(start_index + 2)?.to_string();
Ok((
WorkspaceLocation(bincode::deserialize(blob).context("Bincode failed")?),
start_index + 1,
Self {
id: RemoteProjectId(id as u64),
dev_server_name,
path,
},
start_index + 3,
))
}
}
#[derive(Debug, PartialEq, Clone)]
pub enum SerializedWorkspaceLocation {
Local(LocalPaths),
Remote(SerializedRemoteProject),
}
#[derive(Debug, PartialEq, Clone)]
pub(crate) struct SerializedWorkspace {
pub(crate) id: WorkspaceId,
pub(crate) location: WorkspaceLocation,
pub(crate) location: SerializedWorkspaceLocation,
pub(crate) center_group: SerializedPaneGroup,
pub(crate) bounds: Option<Bounds<DevicePixels>>,
pub(crate) fullscreen: bool,

View file

@ -46,7 +46,7 @@ pub use pane::*;
pub use pane_group::*;
use persistence::{model::SerializedWorkspace, SerializedWindowsBounds, DB};
pub use persistence::{
model::{ItemId, WorkspaceLocation},
model::{ItemId, LocalPaths, SerializedRemoteProject, SerializedWorkspaceLocation},
WorkspaceDb, DB as WORKSPACE_DB,
};
use postage::stream::Stream;
@ -82,7 +82,7 @@ use ui::{
InteractiveElement as _, IntoElement, Label, ParentElement as _, Pixels, SharedString,
Styled as _, ViewContext, VisualContext as _, WindowContext,
};
use util::ResultExt;
use util::{maybe, ResultExt};
use uuid::Uuid;
pub use workspace_settings::{
AutosaveSetting, RestoreOnStartupBehaviour, TabBarSettings, WorkspaceSettings,
@ -3392,17 +3392,16 @@ impl Workspace {
self.database_id
}
fn location(&self, cx: &AppContext) -> Option<WorkspaceLocation> {
fn local_paths(&self, cx: &AppContext) -> Option<LocalPaths> {
let project = self.project().read(cx);
if project.is_local() {
Some(
Some(LocalPaths::new(
project
.visible_worktrees(cx)
.map(|worktree| worktree.read(cx).abs_path())
.collect::<Vec<_>>()
.into(),
)
.collect::<Vec<_>>(),
))
} else {
None
}
@ -3540,25 +3539,44 @@ impl Workspace {
}
}
if let Some(location) = self.location(cx) {
// Load bearing special case:
// - with_local_workspace() relies on this to not have other stuff open
// when you open your log
if !location.paths().is_empty() {
let center_group = build_serialized_pane_group(&self.center.root, cx);
let docks = build_serialized_docks(self, cx);
let serialized_workspace = SerializedWorkspace {
id: self.database_id,
location,
center_group,
bounds: Default::default(),
display: Default::default(),
docks,
fullscreen: cx.is_fullscreen(),
centered_layout: self.centered_layout,
};
return cx.spawn(|_| persistence::DB.save_workspace(serialized_workspace));
let location = if let Some(local_paths) = self.local_paths(cx) {
if !local_paths.paths().is_empty() {
Some(SerializedWorkspaceLocation::Local(local_paths))
} else {
None
}
} else if let Some(remote_project_id) = self.project().read(cx).remote_project_id() {
let store = remote_projects::Store::global(cx).read(cx);
maybe!({
let project = store.remote_project(remote_project_id)?;
let dev_server = store.dev_server(project.dev_server_id)?;
let remote_project = SerializedRemoteProject {
id: remote_project_id,
dev_server_name: dev_server.name.to_string(),
path: project.path.to_string(),
};
Some(SerializedWorkspaceLocation::Remote(remote_project))
})
} else {
None
};
// don't save workspace state for the empty workspace.
if let Some(location) = location {
let center_group = build_serialized_pane_group(&self.center.root, cx);
let docks = build_serialized_docks(self, cx);
let serialized_workspace = SerializedWorkspace {
id: self.database_id,
location,
center_group,
bounds: Default::default(),
display: Default::default(),
docks,
fullscreen: cx.is_fullscreen(),
centered_layout: self.centered_layout,
};
return cx.spawn(|_| persistence::DB.save_workspace(serialized_workspace));
}
Task::ready(())
}
@ -4303,7 +4321,7 @@ pub fn activate_workspace_for_project(
None
}
pub async fn last_opened_workspace_paths() -> Option<WorkspaceLocation> {
pub async fn last_opened_workspace_paths() -> Option<LocalPaths> {
DB.last_workspace().await.log_err().flatten()
}
@ -4410,7 +4428,6 @@ async fn join_channel_internal(
if let Some((project, host)) = room.most_active_project(cx) {
return Some(join_in_room_project(project, host, app_state.clone(), cx));
}
// if you are the first to join a channel, share your project
if room.remote_participants().len() == 0 && !room.local_participant_is_guest() {
if let Some(workspace) = requesting_window {
@ -4419,7 +4436,7 @@ async fn join_channel_internal(
return None;
}
let project = workspace.project.read(cx);
if project.is_local()
if (project.is_local() || project.remote_project_id().is_some())
&& project.visible_worktrees(cx).any(|tree| {
tree.read(cx)
.root_entry()