Enable manual worktree organization (#11504)
Release Notes: - Preserve order of worktrees in project ([#10883](https://github.com/zed-industries/zed/issues/10883)). - Enable drag-and-drop reordering for project worktrees Note: worktree order is not synced during collaboration but guests can reorder their own project panels.  --------- Co-authored-by: Kirill Bulatov <kirill@zed.dev>
This commit is contained in:
parent
1e5389a2be
commit
b9697fb487
8 changed files with 479 additions and 54 deletions
|
@ -22,7 +22,9 @@ use model::{
|
|||
SerializedWorkspace,
|
||||
};
|
||||
|
||||
use self::model::{DockStructure, SerializedDevServerProject, SerializedWorkspaceLocation};
|
||||
use self::model::{
|
||||
DockStructure, LocalPathsOrder, SerializedDevServerProject, SerializedWorkspaceLocation,
|
||||
};
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
pub(crate) struct SerializedAxis(pub(crate) gpui::Axis);
|
||||
|
@ -176,6 +178,7 @@ define_connection! {
|
|||
// workspaces(
|
||||
// workspace_id: usize, // Primary key for workspaces
|
||||
// local_paths: Bincode<Vec<PathBuf>>,
|
||||
// local_paths_order: Bincode<Vec<usize>>,
|
||||
// dock_visible: bool, // Deprecated
|
||||
// dock_anchor: DockAnchor, // Deprecated
|
||||
// dock_pane: Option<usize>, // Deprecated
|
||||
|
@ -360,6 +363,9 @@ define_connection! {
|
|||
ALTER TABLE workspaces DROP COLUMN remote_project_id;
|
||||
ALTER TABLE workspaces ADD COLUMN dev_server_project_id INTEGER;
|
||||
),
|
||||
sql!(
|
||||
ALTER TABLE workspaces ADD COLUMN local_paths_order BLOB;
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -378,6 +384,7 @@ impl WorkspaceDb {
|
|||
let (
|
||||
workspace_id,
|
||||
local_paths,
|
||||
local_paths_order,
|
||||
dev_server_project_id,
|
||||
window_bounds,
|
||||
display,
|
||||
|
@ -386,6 +393,7 @@ impl WorkspaceDb {
|
|||
): (
|
||||
WorkspaceId,
|
||||
Option<LocalPaths>,
|
||||
Option<LocalPathsOrder>,
|
||||
Option<u64>,
|
||||
Option<SerializedWindowBounds>,
|
||||
Option<Uuid>,
|
||||
|
@ -396,6 +404,7 @@ impl WorkspaceDb {
|
|||
SELECT
|
||||
workspace_id,
|
||||
local_paths,
|
||||
local_paths_order,
|
||||
dev_server_project_id,
|
||||
window_state,
|
||||
window_x,
|
||||
|
@ -434,7 +443,13 @@ impl WorkspaceDb {
|
|||
.flatten()?;
|
||||
SerializedWorkspaceLocation::DevServer(dev_server_project)
|
||||
} else if let Some(local_paths) = local_paths {
|
||||
SerializedWorkspaceLocation::Local(local_paths)
|
||||
match local_paths_order {
|
||||
Some(order) => SerializedWorkspaceLocation::Local(local_paths, order),
|
||||
None => {
|
||||
let order = LocalPathsOrder::default_for_paths(&local_paths);
|
||||
SerializedWorkspaceLocation::Local(local_paths, order)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
|
@ -465,7 +480,7 @@ impl WorkspaceDb {
|
|||
.context("Clearing old panes")?;
|
||||
|
||||
match workspace.location {
|
||||
SerializedWorkspaceLocation::Local(local_paths) => {
|
||||
SerializedWorkspaceLocation::Local(local_paths, local_paths_order) => {
|
||||
conn.exec_bound(sql!(
|
||||
DELETE FROM workspaces WHERE local_paths = ? AND workspace_id != ?
|
||||
))?((&local_paths, workspace.id))
|
||||
|
@ -476,6 +491,7 @@ impl WorkspaceDb {
|
|||
INSERT INTO workspaces(
|
||||
workspace_id,
|
||||
local_paths,
|
||||
local_paths_order,
|
||||
left_dock_visible,
|
||||
left_dock_active_panel,
|
||||
left_dock_zoom,
|
||||
|
@ -487,21 +503,22 @@ impl WorkspaceDb {
|
|||
bottom_dock_zoom,
|
||||
timestamp
|
||||
)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, CURRENT_TIMESTAMP)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, 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,
|
||||
local_paths_order = ?3,
|
||||
left_dock_visible = ?4,
|
||||
left_dock_active_panel = ?5,
|
||||
left_dock_zoom = ?6,
|
||||
right_dock_visible = ?7,
|
||||
right_dock_active_panel = ?8,
|
||||
right_dock_zoom = ?9,
|
||||
bottom_dock_visible = ?10,
|
||||
bottom_dock_active_panel = ?11,
|
||||
bottom_dock_zoom = ?12,
|
||||
timestamp = CURRENT_TIMESTAMP
|
||||
))?((workspace.id, &local_paths, workspace.docks))
|
||||
))?((workspace.id, &local_paths, &local_paths_order, workspace.docks))
|
||||
.context("Updating workspace")?;
|
||||
}
|
||||
SerializedWorkspaceLocation::DevServer(dev_server_project) => {
|
||||
|
@ -676,7 +693,7 @@ impl WorkspaceDb {
|
|||
.await?
|
||||
.into_iter()
|
||||
.filter_map(|(_, location)| match location {
|
||||
SerializedWorkspaceLocation::Local(local_paths) => Some(local_paths),
|
||||
SerializedWorkspaceLocation::Local(local_paths, _) => Some(local_paths),
|
||||
SerializedWorkspaceLocation::DevServer(_) => None,
|
||||
})
|
||||
.next())
|
||||
|
@ -1080,7 +1097,10 @@ mod tests {
|
|||
|
||||
let workspace = SerializedWorkspace {
|
||||
id: WorkspaceId(5),
|
||||
location: LocalPaths::new(["/tmp", "/tmp2"]).into(),
|
||||
location: SerializedWorkspaceLocation::Local(
|
||||
LocalPaths::new(["/tmp", "/tmp2"]),
|
||||
LocalPathsOrder::new([1, 0]),
|
||||
),
|
||||
center_group,
|
||||
window_bounds: Default::default(),
|
||||
display: Default::default(),
|
||||
|
@ -1089,8 +1109,8 @@ mod tests {
|
|||
};
|
||||
|
||||
db.save_workspace(workspace.clone()).await;
|
||||
let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]);
|
||||
|
||||
let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]);
|
||||
assert_eq!(workspace, round_trip_workspace.unwrap());
|
||||
|
||||
// Test guaranteed duplicate IDs
|
||||
|
@ -1109,7 +1129,10 @@ mod tests {
|
|||
|
||||
let workspace_1 = SerializedWorkspace {
|
||||
id: WorkspaceId(1),
|
||||
location: LocalPaths::new(["/tmp", "/tmp2"]).into(),
|
||||
location: SerializedWorkspaceLocation::Local(
|
||||
LocalPaths::new(["/tmp", "/tmp2"]),
|
||||
LocalPathsOrder::new([0, 1]),
|
||||
),
|
||||
center_group: Default::default(),
|
||||
window_bounds: Default::default(),
|
||||
display: Default::default(),
|
||||
|
@ -1156,7 +1179,10 @@ mod tests {
|
|||
// Test other mechanism for mutating
|
||||
let mut workspace_3 = SerializedWorkspace {
|
||||
id: WorkspaceId(3),
|
||||
location: LocalPaths::new(&["/tmp", "/tmp2"]).into(),
|
||||
location: SerializedWorkspaceLocation::Local(
|
||||
LocalPaths::new(&["/tmp", "/tmp2"]),
|
||||
LocalPathsOrder::new([1, 0]),
|
||||
),
|
||||
center_group: Default::default(),
|
||||
window_bounds: Default::default(),
|
||||
display: Default::default(),
|
||||
|
|
|
@ -33,6 +33,8 @@ impl LocalPaths {
|
|||
.into_iter()
|
||||
.map(|p| p.as_ref().to_path_buf())
|
||||
.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))
|
||||
}
|
||||
|
@ -44,7 +46,8 @@ impl LocalPaths {
|
|||
|
||||
impl From<LocalPaths> for SerializedWorkspaceLocation {
|
||||
fn from(local_paths: LocalPaths) -> Self {
|
||||
Self::Local(local_paths)
|
||||
let order = LocalPathsOrder::default_for_paths(&local_paths);
|
||||
Self::Local(local_paths, order)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -68,6 +71,43 @@ impl Column for LocalPaths {
|
|||
}
|
||||
}
|
||||
|
||||
#[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))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SerializedDevServerProject> for SerializedWorkspaceLocation {
|
||||
fn from(dev_server_project: SerializedDevServerProject) -> Self {
|
||||
Self::DevServer(dev_server_project)
|
||||
|
@ -101,7 +141,7 @@ impl Column for SerializedDevServerProject {
|
|||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum SerializedWorkspaceLocation {
|
||||
Local(LocalPaths),
|
||||
Local(LocalPaths, LocalPathsOrder),
|
||||
DevServer(SerializedDevServerProject),
|
||||
}
|
||||
|
||||
|
|
|
@ -90,11 +90,11 @@ pub use workspace_settings::{
|
|||
AutosaveSetting, RestoreOnStartupBehaviour, TabBarSettings, WorkspaceSettings,
|
||||
};
|
||||
|
||||
use crate::notifications::NotificationId;
|
||||
use crate::persistence::{
|
||||
model::{DockData, DockStructure, SerializedItem, SerializedPane, SerializedPaneGroup},
|
||||
SerializedAxis,
|
||||
};
|
||||
use crate::{notifications::NotificationId, persistence::model::LocalPathsOrder};
|
||||
|
||||
lazy_static! {
|
||||
static ref ZED_WINDOW_SIZE: Option<Size<DevicePixels>> = env::var("ZED_WINDOW_SIZE")
|
||||
|
@ -904,13 +904,35 @@ impl Workspace {
|
|||
let serialized_workspace: Option<SerializedWorkspace> =
|
||||
persistence::DB.workspace_for_roots(abs_paths.as_slice());
|
||||
|
||||
let paths_to_open = Arc::new(abs_paths);
|
||||
let mut paths_to_open = abs_paths;
|
||||
|
||||
let paths_order = serialized_workspace
|
||||
.as_ref()
|
||||
.map(|ws| &ws.location)
|
||||
.and_then(|loc| match loc {
|
||||
SerializedWorkspaceLocation::Local(_, order) => Some(order.order()),
|
||||
_ => None,
|
||||
});
|
||||
|
||||
if let Some(paths_order) = paths_order {
|
||||
paths_to_open = paths_order
|
||||
.iter()
|
||||
.filter_map(|i| paths_to_open.get(*i).cloned())
|
||||
.collect::<Vec<_>>();
|
||||
if paths_order.iter().enumerate().any(|(i, &j)| i != j) {
|
||||
project_handle
|
||||
.update(&mut cx, |project, _| {
|
||||
project.set_worktrees_reordered(true);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
|
||||
// Get project paths for all of the abs_paths
|
||||
let mut worktree_roots: HashSet<Arc<Path>> = Default::default();
|
||||
let mut project_paths: Vec<(PathBuf, Option<ProjectPath>)> =
|
||||
Vec::with_capacity(paths_to_open.len());
|
||||
for path in paths_to_open.iter().cloned() {
|
||||
for path in paths_to_open.into_iter() {
|
||||
if let Some((worktree, project_entry)) = cx
|
||||
.update(|cx| {
|
||||
Workspace::project_path_for_path(project_handle.clone(), &path, true, cx)
|
||||
|
@ -3488,16 +3510,16 @@ impl Workspace {
|
|||
self.database_id
|
||||
}
|
||||
|
||||
fn local_paths(&self, cx: &AppContext) -> Option<LocalPaths> {
|
||||
fn local_paths(&self, cx: &AppContext) -> Option<Vec<Arc<Path>>> {
|
||||
let project = self.project().read(cx);
|
||||
|
||||
if project.is_local() {
|
||||
Some(LocalPaths::new(
|
||||
Some(
|
||||
project
|
||||
.visible_worktrees(cx)
|
||||
.map(|worktree| worktree.read(cx).abs_path())
|
||||
.collect::<Vec<_>>(),
|
||||
))
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
@ -3641,8 +3663,17 @@ impl Workspace {
|
|||
}
|
||||
|
||||
let location = if let Some(local_paths) = self.local_paths(cx) {
|
||||
if !local_paths.paths().is_empty() {
|
||||
Some(SerializedWorkspaceLocation::Local(local_paths))
|
||||
if !local_paths.is_empty() {
|
||||
let (order, paths): (Vec<_>, Vec<_>) = local_paths
|
||||
.iter()
|
||||
.enumerate()
|
||||
.sorted_by(|a, b| a.1.cmp(b.1))
|
||||
.unzip();
|
||||
|
||||
Some(SerializedWorkspaceLocation::Local(
|
||||
LocalPaths::new(paths),
|
||||
LocalPathsOrder::new(order),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
@ -5320,7 +5351,7 @@ mod tests {
|
|||
// Add a project folder
|
||||
project
|
||||
.update(cx, |project, cx| {
|
||||
project.find_or_create_local_worktree("/root2", true, cx)
|
||||
project.find_or_create_local_worktree("root2", true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue