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

@ -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(),