Detailed changes
@@ -63,9 +63,9 @@ dependencies = [
[[package]]
name = "anyhow"
-version = "1.0.42"
+version = "1.0.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "595d3cfa7a60d4555cb5067b99f07142a08ea778de5cf993f7b75c7d8fabc486"
+checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc"
[[package]]
name = "arrayref"
@@ -3329,7 +3329,10 @@ dependencies = [
name = "project_panel"
version = "0.1.0"
dependencies = [
+ "editor",
+ "futures",
"gpui",
+ "postage",
"project",
"serde_json",
"settings",
@@ -331,7 +331,9 @@
"context": "ProjectPanel",
"bindings": {
"left": "project_panel::CollapseSelectedEntry",
- "right": "project_panel::ExpandSelectedEntry"
+ "right": "project_panel::ExpandSelectedEntry",
+ "f2": "project_panel::Rename",
+ "backspace": "project_panel::Delete"
}
}
]
@@ -937,6 +937,7 @@
"top": 6,
"bottom": 6
},
+ "indent_width": 20,
"entry": {
"height": 24,
"icon_color": "#8b8792",
@@ -946,41 +947,42 @@
"family": "Zed Mono",
"color": "#8b8792",
"size": 14
+ },
+ "hover": {
+ "background": "#5852603d",
+ "text": {
+ "family": "Zed Mono",
+ "color": "#e2dfe7",
+ "size": 14
+ }
+ },
+ "active": {
+ "background": "#5852605c",
+ "text": {
+ "family": "Zed Mono",
+ "color": "#e2dfe7",
+ "size": 14
+ }
+ },
+ "active_hover": {
+ "background": "#5852603d",
+ "text": {
+ "family": "Zed Mono",
+ "color": "#efecf4",
+ "size": 14
+ }
}
},
- "hovered_entry": {
- "height": 24,
- "background": "#5852603d",
- "icon_color": "#8b8792",
- "icon_size": 8,
- "icon_spacing": 8,
- "text": {
- "family": "Zed Mono",
- "color": "#e2dfe7",
- "size": 14
- }
- },
- "selected_entry": {
- "height": 24,
- "icon_color": "#8b8792",
- "icon_size": 8,
- "icon_spacing": 8,
+ "filename_editor": {
+ "background": "#26232a5c",
"text": {
"family": "Zed Mono",
"color": "#e2dfe7",
"size": 14
- }
- },
- "hovered_selected_entry": {
- "height": 24,
- "background": "#5852603d",
- "icon_color": "#8b8792",
- "icon_size": 8,
- "icon_spacing": 8,
- "text": {
- "family": "Zed Mono",
- "color": "#efecf4",
- "size": 14
+ },
+ "selection": {
+ "cursor": "#576ddb",
+ "selection": "#576ddb3d"
}
}
},
@@ -937,6 +937,7 @@
"top": 6,
"bottom": 6
},
+ "indent_width": 20,
"entry": {
"height": 24,
"icon_color": "#585260",
@@ -946,41 +947,42 @@
"family": "Zed Mono",
"color": "#585260",
"size": 14
+ },
+ "hover": {
+ "background": "#8b87921f",
+ "text": {
+ "family": "Zed Mono",
+ "color": "#26232a",
+ "size": 14
+ }
+ },
+ "active": {
+ "background": "#8b87922e",
+ "text": {
+ "family": "Zed Mono",
+ "color": "#26232a",
+ "size": 14
+ }
+ },
+ "active_hover": {
+ "background": "#8b87921f",
+ "text": {
+ "family": "Zed Mono",
+ "color": "#19171c",
+ "size": 14
+ }
}
},
- "hovered_entry": {
- "height": 24,
- "background": "#8b87921f",
- "icon_color": "#585260",
- "icon_size": 8,
- "icon_spacing": 8,
- "text": {
- "family": "Zed Mono",
- "color": "#26232a",
- "size": 14
- }
- },
- "selected_entry": {
- "height": 24,
- "icon_color": "#585260",
- "icon_size": 8,
- "icon_spacing": 8,
+ "filename_editor": {
+ "background": "#e2dfe72e",
"text": {
"family": "Zed Mono",
"color": "#26232a",
"size": 14
- }
- },
- "hovered_selected_entry": {
- "height": 24,
- "background": "#8b87921f",
- "icon_color": "#585260",
- "icon_size": 8,
- "icon_spacing": 8,
- "text": {
- "family": "Zed Mono",
- "color": "#19171c",
- "size": 14
+ },
+ "selection": {
+ "cursor": "#576ddb",
+ "selection": "#576ddb3d"
}
}
},
@@ -937,6 +937,7 @@
"top": 6,
"bottom": 6
},
+ "indent_width": 20,
"entry": {
"height": 24,
"icon_color": "#555555",
@@ -946,41 +947,42 @@
"family": "Zed Mono",
"color": "#808080",
"size": 14
+ },
+ "hover": {
+ "background": "#232323",
+ "text": {
+ "family": "Zed Mono",
+ "color": "#f1f1f1",
+ "size": 14
+ }
+ },
+ "active": {
+ "background": "#2b2b2b",
+ "text": {
+ "family": "Zed Mono",
+ "color": "#f1f1f1",
+ "size": 14
+ }
+ },
+ "active_hover": {
+ "background": "#232323",
+ "text": {
+ "family": "Zed Mono",
+ "color": "#ffffff",
+ "size": 14
+ }
}
},
- "hovered_entry": {
- "height": 24,
- "background": "#232323",
- "icon_color": "#555555",
- "icon_size": 8,
- "icon_spacing": 8,
- "text": {
- "family": "Zed Mono",
- "color": "#f1f1f1",
- "size": 14
- }
- },
- "selected_entry": {
- "height": 24,
- "icon_color": "#555555",
- "icon_size": 8,
- "icon_spacing": 8,
+ "filename_editor": {
+ "background": "#ffffff1f",
"text": {
"family": "Zed Mono",
"color": "#f1f1f1",
"size": 14
- }
- },
- "hovered_selected_entry": {
- "height": 24,
- "background": "#232323",
- "icon_color": "#555555",
- "icon_size": 8,
- "icon_spacing": 8,
- "text": {
- "family": "Zed Mono",
- "color": "#ffffff",
- "size": 14
+ },
+ "selection": {
+ "cursor": "#2472f2",
+ "selection": "#2472f23d"
}
}
},
@@ -937,6 +937,7 @@
"top": 6,
"bottom": 6
},
+ "indent_width": 20,
"entry": {
"height": 24,
"icon_color": "#9c9c9c",
@@ -946,41 +947,42 @@
"family": "Zed Mono",
"color": "#636363",
"size": 14
+ },
+ "hover": {
+ "background": "#eaeaea",
+ "text": {
+ "family": "Zed Mono",
+ "color": "#2b2b2b",
+ "size": 14
+ }
+ },
+ "active": {
+ "background": "#e3e3e3",
+ "text": {
+ "family": "Zed Mono",
+ "color": "#2b2b2b",
+ "size": 14
+ }
+ },
+ "active_hover": {
+ "background": "#eaeaea",
+ "text": {
+ "family": "Zed Mono",
+ "color": "#000000",
+ "size": 14
+ }
}
},
- "hovered_entry": {
- "height": 24,
- "background": "#eaeaea",
- "icon_color": "#9c9c9c",
- "icon_size": 8,
- "icon_spacing": 8,
- "text": {
- "family": "Zed Mono",
- "color": "#2b2b2b",
- "size": 14
- }
- },
- "selected_entry": {
- "height": 24,
- "icon_color": "#9c9c9c",
- "icon_size": 8,
- "icon_spacing": 8,
+ "filename_editor": {
+ "background": "#0000000f",
"text": {
"family": "Zed Mono",
"color": "#2b2b2b",
"size": 14
- }
- },
- "hovered_selected_entry": {
- "height": 24,
- "background": "#eaeaea",
- "icon_color": "#9c9c9c",
- "icon_size": 8,
- "icon_spacing": 8,
- "text": {
- "family": "Zed Mono",
- "color": "#000000",
- "size": 14
+ },
+ "selection": {
+ "cursor": "#2472f2",
+ "selection": "#2472f23d"
}
}
},
@@ -937,6 +937,7 @@
"top": 6,
"bottom": 6
},
+ "indent_width": 20,
"entry": {
"height": 24,
"icon_color": "#93a1a1",
@@ -946,41 +947,42 @@
"family": "Zed Mono",
"color": "#93a1a1",
"size": 14
+ },
+ "hover": {
+ "background": "#586e753d",
+ "text": {
+ "family": "Zed Mono",
+ "color": "#eee8d5",
+ "size": 14
+ }
+ },
+ "active": {
+ "background": "#586e755c",
+ "text": {
+ "family": "Zed Mono",
+ "color": "#eee8d5",
+ "size": 14
+ }
+ },
+ "active_hover": {
+ "background": "#586e753d",
+ "text": {
+ "family": "Zed Mono",
+ "color": "#fdf6e3",
+ "size": 14
+ }
}
},
- "hovered_entry": {
- "height": 24,
- "background": "#586e753d",
- "icon_color": "#93a1a1",
- "icon_size": 8,
- "icon_spacing": 8,
- "text": {
- "family": "Zed Mono",
- "color": "#eee8d5",
- "size": 14
- }
- },
- "selected_entry": {
- "height": 24,
- "icon_color": "#93a1a1",
- "icon_size": 8,
- "icon_spacing": 8,
+ "filename_editor": {
+ "background": "#0736425c",
"text": {
"family": "Zed Mono",
"color": "#eee8d5",
"size": 14
- }
- },
- "hovered_selected_entry": {
- "height": 24,
- "background": "#586e753d",
- "icon_color": "#93a1a1",
- "icon_size": 8,
- "icon_spacing": 8,
- "text": {
- "family": "Zed Mono",
- "color": "#fdf6e3",
- "size": 14
+ },
+ "selection": {
+ "cursor": "#268bd2",
+ "selection": "#268bd23d"
}
}
},
@@ -937,6 +937,7 @@
"top": 6,
"bottom": 6
},
+ "indent_width": 20,
"entry": {
"height": 24,
"icon_color": "#586e75",
@@ -946,41 +947,42 @@
"family": "Zed Mono",
"color": "#586e75",
"size": 14
+ },
+ "hover": {
+ "background": "#93a1a11f",
+ "text": {
+ "family": "Zed Mono",
+ "color": "#073642",
+ "size": 14
+ }
+ },
+ "active": {
+ "background": "#93a1a12e",
+ "text": {
+ "family": "Zed Mono",
+ "color": "#073642",
+ "size": 14
+ }
+ },
+ "active_hover": {
+ "background": "#93a1a11f",
+ "text": {
+ "family": "Zed Mono",
+ "color": "#002b36",
+ "size": 14
+ }
}
},
- "hovered_entry": {
- "height": 24,
- "background": "#93a1a11f",
- "icon_color": "#586e75",
- "icon_size": 8,
- "icon_spacing": 8,
- "text": {
- "family": "Zed Mono",
- "color": "#073642",
- "size": 14
- }
- },
- "selected_entry": {
- "height": 24,
- "icon_color": "#586e75",
- "icon_size": 8,
- "icon_spacing": 8,
+ "filename_editor": {
+ "background": "#eee8d52e",
"text": {
"family": "Zed Mono",
"color": "#073642",
"size": 14
- }
- },
- "hovered_selected_entry": {
- "height": 24,
- "background": "#93a1a11f",
- "icon_color": "#586e75",
- "icon_size": 8,
- "icon_spacing": 8,
- "text": {
- "family": "Zed Mono",
- "color": "#002b36",
- "size": 14
+ },
+ "selection": {
+ "cursor": "#268bd2",
+ "selection": "#268bd23d"
}
}
},
@@ -937,6 +937,7 @@
"top": 6,
"bottom": 6
},
+ "indent_width": 20,
"entry": {
"height": 24,
"icon_color": "#979db4",
@@ -946,41 +947,42 @@
"family": "Zed Mono",
"color": "#979db4",
"size": 14
+ },
+ "hover": {
+ "background": "#5e66873d",
+ "text": {
+ "family": "Zed Mono",
+ "color": "#dfe2f1",
+ "size": 14
+ }
+ },
+ "active": {
+ "background": "#5e66875c",
+ "text": {
+ "family": "Zed Mono",
+ "color": "#dfe2f1",
+ "size": 14
+ }
+ },
+ "active_hover": {
+ "background": "#5e66873d",
+ "text": {
+ "family": "Zed Mono",
+ "color": "#f5f7ff",
+ "size": 14
+ }
}
},
- "hovered_entry": {
- "height": 24,
- "background": "#5e66873d",
- "icon_color": "#979db4",
- "icon_size": 8,
- "icon_spacing": 8,
- "text": {
- "family": "Zed Mono",
- "color": "#dfe2f1",
- "size": 14
- }
- },
- "selected_entry": {
- "height": 24,
- "icon_color": "#979db4",
- "icon_size": 8,
- "icon_spacing": 8,
+ "filename_editor": {
+ "background": "#2932565c",
"text": {
"family": "Zed Mono",
"color": "#dfe2f1",
"size": 14
- }
- },
- "hovered_selected_entry": {
- "height": 24,
- "background": "#5e66873d",
- "icon_color": "#979db4",
- "icon_size": 8,
- "icon_spacing": 8,
- "text": {
- "family": "Zed Mono",
- "color": "#f5f7ff",
- "size": 14
+ },
+ "selection": {
+ "cursor": "#3d8fd1",
+ "selection": "#3d8fd13d"
}
}
},
@@ -937,6 +937,7 @@
"top": 6,
"bottom": 6
},
+ "indent_width": 20,
"entry": {
"height": 24,
"icon_color": "#5e6687",
@@ -946,41 +947,42 @@
"family": "Zed Mono",
"color": "#5e6687",
"size": 14
+ },
+ "hover": {
+ "background": "#979db41f",
+ "text": {
+ "family": "Zed Mono",
+ "color": "#293256",
+ "size": 14
+ }
+ },
+ "active": {
+ "background": "#979db42e",
+ "text": {
+ "family": "Zed Mono",
+ "color": "#293256",
+ "size": 14
+ }
+ },
+ "active_hover": {
+ "background": "#979db41f",
+ "text": {
+ "family": "Zed Mono",
+ "color": "#202746",
+ "size": 14
+ }
}
},
- "hovered_entry": {
- "height": 24,
- "background": "#979db41f",
- "icon_color": "#5e6687",
- "icon_size": 8,
- "icon_spacing": 8,
- "text": {
- "family": "Zed Mono",
- "color": "#293256",
- "size": 14
- }
- },
- "selected_entry": {
- "height": 24,
- "icon_color": "#5e6687",
- "icon_size": 8,
- "icon_spacing": 8,
+ "filename_editor": {
+ "background": "#dfe2f12e",
"text": {
"family": "Zed Mono",
"color": "#293256",
"size": 14
- }
- },
- "hovered_selected_entry": {
- "height": 24,
- "background": "#979db41f",
- "icon_color": "#5e6687",
- "icon_size": 8,
- "icon_spacing": 8,
- "text": {
- "family": "Zed Mono",
- "color": "#202746",
- "size": 14
+ },
+ "selection": {
+ "cursor": "#3d8fd1",
+ "selection": "#3d8fd13d"
}
}
},
@@ -270,7 +270,7 @@ impl View for AutoUpdateIndicator {
)
.boxed()
})
- .on_click(|cx| cx.dispatch_action(DismissErrorMessage))
+ .on_click(|_, cx| cx.dispatch_action(DismissErrorMessage))
.boxed()
}
AutoUpdateStatus::Idle => Empty::new().boxed(),
@@ -320,7 +320,7 @@ impl ChatPanel {
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
- .on_click(move |cx| {
+ .on_click(move |_, cx| {
let rpc = rpc.clone();
let this = this.clone();
cx.spawn(|mut cx| async move {
@@ -126,6 +126,9 @@ impl Server {
.add_request_handler(Server::forward_project_request::<proto::PerformRename>)
.add_request_handler(Server::forward_project_request::<proto::ReloadBuffers>)
.add_request_handler(Server::forward_project_request::<proto::FormatBuffers>)
+ .add_request_handler(Server::forward_project_request::<proto::CreateProjectEntry>)
+ .add_request_handler(Server::forward_project_request::<proto::RenameProjectEntry>)
+ .add_request_handler(Server::forward_project_request::<proto::DeleteProjectEntry>)
.add_request_handler(Server::update_buffer)
.add_message_handler(Server::update_buffer_file)
.add_message_handler(Server::buffer_reloaded)
@@ -157,9 +160,7 @@ impl Server {
let span = info_span!(
"handle message",
payload_type = envelope.payload_type_name(),
- payload = serde_json::to_string_pretty(&envelope.payload)
- .unwrap()
- .as_str(),
+ payload = format!("{:?}", envelope.payload).as_str(),
);
let future = (handler)(server, *envelope);
async move {
@@ -447,6 +448,7 @@ impl Server {
.cloned()
.collect(),
visible: worktree.visible,
+ scan_id: shared_worktree.scan_id,
})
})
.collect();
@@ -577,6 +579,7 @@ impl Server {
request.payload.worktree_id,
&request.payload.removed_entries,
&request.payload.updated_entries,
+ request.payload.scan_id,
)?;
broadcast(request.sender_id, connection_ids, |connection_id| {
@@ -1808,6 +1811,176 @@ mod tests {
.await;
}
+ #[gpui::test(iterations = 10)]
+ async fn test_fs_operations(
+ executor: Arc<Deterministic>,
+ cx_a: &mut TestAppContext,
+ cx_b: &mut TestAppContext,
+ ) {
+ executor.forbid_parking();
+ let fs = FakeFs::new(cx_a.background());
+
+ // Connect to a server as 2 clients.
+ let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
+ let mut client_a = server.create_client(cx_a, "user_a").await;
+ let mut client_b = server.create_client(cx_b, "user_b").await;
+
+ // Share a project as client A
+ fs.insert_tree(
+ "/dir",
+ json!({
+ ".zed.toml": r#"collaborators = ["user_b"]"#,
+ "a.txt": "a-contents",
+ "b.txt": "b-contents",
+ }),
+ )
+ .await;
+
+ let (project_a, worktree_id) = client_a.build_local_project(fs, "/dir", cx_a).await;
+ let project_id = project_a.read_with(cx_a, |project, _| project.remote_id().unwrap());
+ project_a
+ .update(cx_a, |project, cx| project.share(cx))
+ .await
+ .unwrap();
+
+ let project_b = client_b.build_remote_project(project_id, cx_b).await;
+
+ let worktree_a =
+ project_a.read_with(cx_a, |project, cx| project.worktrees(cx).next().unwrap());
+ let worktree_b =
+ project_b.read_with(cx_b, |project, cx| project.worktrees(cx).next().unwrap());
+
+ let entry = project_b
+ .update(cx_b, |project, cx| {
+ project
+ .create_entry((worktree_id, "c.txt"), false, cx)
+ .unwrap()
+ })
+ .await
+ .unwrap();
+ worktree_a.read_with(cx_a, |worktree, _| {
+ assert_eq!(
+ worktree
+ .paths()
+ .map(|p| p.to_string_lossy())
+ .collect::<Vec<_>>(),
+ [".zed.toml", "a.txt", "b.txt", "c.txt"]
+ );
+ });
+ worktree_b.read_with(cx_b, |worktree, _| {
+ assert_eq!(
+ worktree
+ .paths()
+ .map(|p| p.to_string_lossy())
+ .collect::<Vec<_>>(),
+ [".zed.toml", "a.txt", "b.txt", "c.txt"]
+ );
+ });
+
+ project_b
+ .update(cx_b, |project, cx| {
+ project.rename_entry(entry.id, Path::new("d.txt"), cx)
+ })
+ .unwrap()
+ .await
+ .unwrap();
+ worktree_a.read_with(cx_a, |worktree, _| {
+ assert_eq!(
+ worktree
+ .paths()
+ .map(|p| p.to_string_lossy())
+ .collect::<Vec<_>>(),
+ [".zed.toml", "a.txt", "b.txt", "d.txt"]
+ );
+ });
+ worktree_b.read_with(cx_b, |worktree, _| {
+ assert_eq!(
+ worktree
+ .paths()
+ .map(|p| p.to_string_lossy())
+ .collect::<Vec<_>>(),
+ [".zed.toml", "a.txt", "b.txt", "d.txt"]
+ );
+ });
+
+ let dir_entry = project_b
+ .update(cx_b, |project, cx| {
+ project
+ .create_entry((worktree_id, "DIR"), true, cx)
+ .unwrap()
+ })
+ .await
+ .unwrap();
+ worktree_a.read_with(cx_a, |worktree, _| {
+ assert_eq!(
+ worktree
+ .paths()
+ .map(|p| p.to_string_lossy())
+ .collect::<Vec<_>>(),
+ [".zed.toml", "DIR", "a.txt", "b.txt", "d.txt"]
+ );
+ });
+ worktree_b.read_with(cx_b, |worktree, _| {
+ assert_eq!(
+ worktree
+ .paths()
+ .map(|p| p.to_string_lossy())
+ .collect::<Vec<_>>(),
+ [".zed.toml", "DIR", "a.txt", "b.txt", "d.txt"]
+ );
+ });
+
+ project_b
+ .update(cx_b, |project, cx| {
+ project.delete_entry(dir_entry.id, cx).unwrap()
+ })
+ .await
+ .unwrap();
+ worktree_a.read_with(cx_a, |worktree, _| {
+ assert_eq!(
+ worktree
+ .paths()
+ .map(|p| p.to_string_lossy())
+ .collect::<Vec<_>>(),
+ [".zed.toml", "a.txt", "b.txt", "d.txt"]
+ );
+ });
+ worktree_b.read_with(cx_b, |worktree, _| {
+ assert_eq!(
+ worktree
+ .paths()
+ .map(|p| p.to_string_lossy())
+ .collect::<Vec<_>>(),
+ [".zed.toml", "a.txt", "b.txt", "d.txt"]
+ );
+ });
+
+ project_b
+ .update(cx_b, |project, cx| {
+ project.delete_entry(entry.id, cx).unwrap()
+ })
+ .await
+ .unwrap();
+ worktree_a.read_with(cx_a, |worktree, _| {
+ assert_eq!(
+ worktree
+ .paths()
+ .map(|p| p.to_string_lossy())
+ .collect::<Vec<_>>(),
+ [".zed.toml", "a.txt", "b.txt"]
+ );
+ });
+ worktree_b.read_with(cx_b, |worktree, _| {
+ assert_eq!(
+ worktree
+ .paths()
+ .map(|p| p.to_string_lossy())
+ .collect::<Vec<_>>(),
+ [".zed.toml", "a.txt", "b.txt"]
+ );
+ });
+ }
+
#[gpui::test(iterations = 10)]
async fn test_buffer_conflict_after_save(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
cx_a.foreground().forbid_parking();
@@ -3725,7 +3898,7 @@ mod tests {
let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(¶ms, cx));
let editor_b = workspace_b
.update(cx_b, |workspace, cx| {
- workspace.open_path((worktree_id, "main.rs"), cx)
+ workspace.open_path((worktree_id, "main.rs"), true, cx)
})
.await
.unwrap()
@@ -3973,7 +4146,7 @@ mod tests {
let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(¶ms, cx));
let editor_b = workspace_b
.update(cx_b, |workspace, cx| {
- workspace.open_path((worktree_id, "one.rs"), cx)
+ workspace.open_path((worktree_id, "one.rs"), true, cx)
})
.await
.unwrap()
@@ -4725,7 +4898,7 @@ mod tests {
let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
let editor_a1 = workspace_a
.update(cx_a, |workspace, cx| {
- workspace.open_path((worktree_id, "1.txt"), cx)
+ workspace.open_path((worktree_id, "1.txt"), true, cx)
})
.await
.unwrap()
@@ -4733,7 +4906,7 @@ mod tests {
.unwrap();
let editor_a2 = workspace_a
.update(cx_a, |workspace, cx| {
- workspace.open_path((worktree_id, "2.txt"), cx)
+ workspace.open_path((worktree_id, "2.txt"), true, cx)
})
.await
.unwrap()
@@ -4744,7 +4917,7 @@ mod tests {
let workspace_b = client_b.build_workspace(&project_b, cx_b);
let editor_b1 = workspace_b
.update(cx_b, |workspace, cx| {
- workspace.open_path((worktree_id, "1.txt"), cx)
+ workspace.open_path((worktree_id, "1.txt"), true, cx)
})
.await
.unwrap()
@@ -4937,7 +5110,7 @@ mod tests {
let pane_a1 = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
let _editor_a1 = workspace_a
.update(cx_a, |workspace, cx| {
- workspace.open_path((worktree_id, "1.txt"), cx)
+ workspace.open_path((worktree_id, "1.txt"), true, cx)
})
.await
.unwrap()
@@ -4949,7 +5122,7 @@ mod tests {
let pane_b1 = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
let _editor_b1 = workspace_b
.update(cx_b, |workspace, cx| {
- workspace.open_path((worktree_id, "2.txt"), cx)
+ workspace.open_path((worktree_id, "2.txt"), true, cx)
})
.await
.unwrap()
@@ -4984,7 +5157,7 @@ mod tests {
.update(cx_a, |workspace, cx| {
workspace.activate_next_pane(cx);
assert_eq!(*workspace.active_pane(), pane_a1);
- workspace.open_path((worktree_id, "3.txt"), cx)
+ workspace.open_path((worktree_id, "3.txt"), true, cx)
})
.await
.unwrap();
@@ -4992,7 +5165,7 @@ mod tests {
.update(cx_b, |workspace, cx| {
workspace.activate_next_pane(cx);
assert_eq!(*workspace.active_pane(), pane_b1);
- workspace.open_path((worktree_id, "4.txt"), cx)
+ workspace.open_path((worktree_id, "4.txt"), true, cx)
})
.await
.unwrap();
@@ -5081,7 +5254,7 @@ mod tests {
let workspace_a = client_a.build_workspace(&project_a, cx_a);
let _editor_a1 = workspace_a
.update(cx_a, |workspace, cx| {
- workspace.open_path((worktree_id, "1.txt"), cx)
+ workspace.open_path((worktree_id, "1.txt"), true, cx)
})
.await
.unwrap()
@@ -5194,7 +5367,7 @@ mod tests {
// When client B activates a different item in the original pane, it automatically stops following client A.
workspace_b
.update(cx_b, |workspace, cx| {
- workspace.open_path((worktree_id, "2.txt"), cx)
+ workspace.open_path((worktree_id, "2.txt"), true, cx)
})
.await
.unwrap();
@@ -5633,6 +5806,7 @@ mod tests {
guest_client.username,
id
);
+ assert_eq!(guest_snapshot.scan_id(), host_snapshot.scan_id());
}
guest_client
@@ -6354,6 +6528,49 @@ mod tests {
client.buffers.extend(search.await?.into_keys());
}
}
+ 60..=69 => {
+ let worktree = project
+ .read_with(cx, |project, cx| {
+ project
+ .worktrees(&cx)
+ .filter(|worktree| {
+ let worktree = worktree.read(cx);
+ worktree.is_visible()
+ && worktree.entries(false).any(|e| e.is_file())
+ && worktree
+ .root_entry()
+ .map_or(false, |e| e.is_dir())
+ })
+ .choose(&mut *rng.lock())
+ })
+ .unwrap();
+ let (worktree_id, worktree_root_name) = worktree
+ .read_with(cx, |worktree, _| {
+ (worktree.id(), worktree.root_name().to_string())
+ });
+
+ let mut new_name = String::new();
+ for _ in 0..10 {
+ let letter = rng.lock().gen_range('a'..='z');
+ new_name.push(letter);
+ }
+ let mut new_path = PathBuf::new();
+ new_path.push(new_name);
+ new_path.set_extension("rs");
+ log::info!(
+ "{}: creating {:?} in worktree {} ({})",
+ guest_username,
+ new_path,
+ worktree_id,
+ worktree_root_name,
+ );
+ project
+ .update(cx, |project, cx| {
+ project.create_entry((worktree_id, new_path), false, cx)
+ })
+ .unwrap()
+ .await?;
+ }
_ => {
buffer.update(cx, |buffer, cx| {
log::info!(
@@ -46,6 +46,7 @@ pub struct ProjectShare {
pub struct WorktreeShare {
pub entries: HashMap<u64, proto::Entry>,
pub diagnostic_summaries: BTreeMap<PathBuf, proto::DiagnosticSummary>,
+ pub scan_id: u64,
}
#[derive(Default)]
@@ -561,6 +562,7 @@ impl Store {
worktree_id: u64,
removed_entries: &[u64],
updated_entries: &[proto::Entry],
+ scan_id: u64,
) -> Result<Vec<ConnectionId>> {
let project = self.write_project(project_id, connection_id)?;
let worktree = project
@@ -574,6 +576,7 @@ impl Store {
for entry in updated_entries {
worktree.entries.insert(entry.id, entry.clone());
}
+ worktree.scan_id = scan_id;
let connection_ids = project.connection_ids();
Ok(connection_ids)
}
@@ -204,7 +204,7 @@ impl ContactsPanel {
} else {
CursorStyle::Arrow
})
- .on_click(move |cx| {
+ .on_click(move |_, cx| {
if !is_host && !is_guest {
cx.dispatch_global_action(JoinProject {
project_id,
@@ -161,7 +161,7 @@ impl View for DiagnosticIndicator {
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
- .on_click(|cx| cx.dispatch_action(crate::Deploy))
+ .on_click(|_, cx| cx.dispatch_action(crate::Deploy))
.aligned()
.boxed(),
);
@@ -194,7 +194,7 @@ impl View for DiagnosticIndicator {
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
- .on_click(|cx| cx.dispatch_action(GoToNextDiagnostic))
+ .on_click(|_, cx| cx.dispatch_action(GoToNextDiagnostic))
.boxed(),
);
}
@@ -9354,19 +9354,10 @@ mod tests {
let fs = FakeFs::new(cx.background().clone());
fs.insert_file("/file.rs", Default::default()).await;
- let project = Project::test(fs, cx);
+ let project = Project::test(fs, ["/file.rs"], cx).await;
project.update(cx, |project, _| project.languages().add(Arc::new(language)));
-
- let worktree_id = project
- .update(cx, |project, cx| {
- project.find_or_create_local_worktree("/file.rs", true, cx)
- })
- .await
- .unwrap()
- .0
- .read_with(cx, |tree, _| tree.id());
let buffer = project
- .update(cx, |project, cx| project.open_buffer((worktree_id, ""), cx))
+ .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
.await
.unwrap();
@@ -9485,19 +9476,10 @@ mod tests {
let fs = FakeFs::new(cx.background().clone());
fs.insert_file("/file.rs", text).await;
- let project = Project::test(fs, cx);
+ let project = Project::test(fs, ["/file.rs"], cx).await;
project.update(cx, |project, _| project.languages().add(Arc::new(language)));
-
- let worktree_id = project
- .update(cx, |project, cx| {
- project.find_or_create_local_worktree("/file.rs", true, cx)
- })
- .await
- .unwrap()
- .0
- .read_with(cx, |tree, _| tree.id());
let buffer = project
- .update(cx, |project, cx| project.open_buffer((worktree_id, ""), cx))
+ .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
.await
.unwrap();
let mut fake_server = fake_servers.next().await.unwrap();
@@ -875,6 +875,12 @@ impl Element for EditorElement {
.max(constraint.min_along(Axis::Vertical))
.min(line_height * max_lines as f32),
)
+ } else if let EditorMode::SingleLine = snapshot.mode {
+ size.set_y(
+ line_height
+ .min(constraint.max_along(Axis::Vertical))
+ .max(constraint.min_along(Axis::Vertical)),
+ )
} else if size.y().is_infinite() {
size.set_y(scroll_height);
}
@@ -1183,7 +1189,7 @@ impl Element for EditorElement {
click_count,
..
} => self.mouse_down(*position, *alt, *shift, *click_count, layout, paint, cx),
- Event::LeftMouseUp { position } => self.mouse_up(*position, cx),
+ Event::LeftMouseUp { position, .. } => self.mouse_up(*position, cx),
Event::LeftMouseDragged { position } => {
self.mouse_dragged(*position, layout, paint, cx)
}
@@ -102,7 +102,7 @@ impl FileFinder {
match event {
Event::Selected(project_path) => {
workspace
- .open_path(project_path.clone(), cx)
+ .open_path(project_path.clone(), true, cx)
.detach_and_log_err(cx);
workspace.dismiss_modal(cx);
}
@@ -15,7 +15,7 @@ pub struct MouseEventHandler {
child: ElementBox,
cursor_style: Option<CursorStyle>,
mouse_down_handler: Option<Box<dyn FnMut(&mut EventContext)>>,
- click_handler: Option<Box<dyn FnMut(&mut EventContext)>>,
+ click_handler: Option<Box<dyn FnMut(usize, &mut EventContext)>>,
drag_handler: Option<Box<dyn FnMut(Vector2F, &mut EventContext)>>,
padding: Padding,
}
@@ -57,7 +57,7 @@ impl MouseEventHandler {
self
}
- pub fn on_click(mut self, handler: impl FnMut(&mut EventContext) + 'static) -> Self {
+ pub fn on_click(mut self, handler: impl FnMut(usize, &mut EventContext) + 'static) -> Self {
self.click_handler = Some(Box::new(handler));
self
}
@@ -151,14 +151,18 @@ impl Element for MouseEventHandler {
handled_in_child
}
}
- Event::LeftMouseUp { position, .. } => {
+ Event::LeftMouseUp {
+ position,
+ click_count,
+ ..
+ } => {
state.prev_drag_position = None;
if !handled_in_child && state.clicked {
state.clicked = false;
cx.notify();
if let Some(handler) = click_handler {
if hit_bounds.contains_point(*position) {
- handler(cx);
+ handler(*click_count, cx);
}
}
true
@@ -360,6 +360,14 @@ impl Deterministic {
self.state.lock().now = new_now;
}
+
+ pub fn forbid_parking(&self) {
+ use rand::prelude::*;
+
+ let mut state = self.state.lock();
+ state.forbid_parking = true;
+ state.rng = StdRng::seed_from_u64(state.seed);
+ }
}
impl Drop for Timer {
@@ -507,14 +515,8 @@ impl Foreground {
#[cfg(any(test, feature = "test-support"))]
pub fn forbid_parking(&self) {
- use rand::prelude::*;
-
match self {
- Self::Deterministic { executor, .. } => {
- let mut state = executor.state.lock();
- state.forbid_parking = true;
- state.rng = StdRng::seed_from_u64(state.seed);
- }
+ Self::Deterministic { executor, .. } => executor.forbid_parking(),
_ => panic!("this method can only be called on a deterministic executor"),
}
}
@@ -28,6 +28,7 @@ pub enum Event {
},
LeftMouseUp {
position: Vector2F,
+ click_count: usize,
},
LeftMouseDragged {
position: Vector2F,
@@ -68,7 +69,7 @@ impl Event {
Event::KeyDown { .. } => None,
Event::ScrollWheel { position, .. }
| Event::LeftMouseDown { position, .. }
- | Event::LeftMouseUp { position }
+ | Event::LeftMouseUp { position, .. }
| Event::LeftMouseDragged { position }
| Event::RightMouseDown { position, .. }
| Event::RightMouseUp { position }
@@ -129,6 +129,7 @@ impl Event {
native_event.locationInWindow().x as f32,
window_height - native_event.locationInWindow().y as f32,
),
+ click_count: native_event.clickCount() as usize,
}),
NSEventType::NSRightMouseDown => {
let modifiers = native_event.modifierFlags();
@@ -119,7 +119,7 @@ impl View for Select {
.with_style(style.header)
.boxed()
})
- .on_click(move |cx| cx.dispatch_action(ToggleSelect))
+ .on_click(move |_, cx| cx.dispatch_action(ToggleSelect))
.boxed(),
);
if self.is_open {
@@ -153,7 +153,7 @@ impl View for Select {
)
},
)
- .on_click(move |cx| cx.dispatch_action(SelectItem(ix)))
+ .on_click(move |_, cx| cx.dispatch_action(SelectItem(ix)))
.boxed()
}))
},
@@ -29,7 +29,7 @@ settings = { path = "../settings" }
sum_tree = { path = "../sum_tree" }
util = { path = "../util" }
aho-corasick = "0.7"
-anyhow = "1.0.38"
+anyhow = "1.0.57"
async-trait = "0.1"
futures = "0.3"
ignore = "0.4"
@@ -379,7 +379,7 @@ impl FakeFs {
async fn simulate_random_delay(&self) {
self.executor
.upgrade()
- .expect("excecutor has been dropped")
+ .expect("executor has been dropped")
.simulate_random_delay()
.await;
}
@@ -493,7 +493,7 @@ impl Fs for FakeFs {
});
for (relative_path, entry) in removed {
- let new_path = target.join(relative_path);
+ let new_path = normalize_path(&target.join(relative_path));
state.entries.insert(new_path, entry);
}
@@ -501,13 +501,15 @@ impl Fs for FakeFs {
Ok(())
}
- async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()> {
- let path = normalize_path(path);
+ async fn remove_dir(&self, dir_path: &Path, options: RemoveOptions) -> Result<()> {
+ let dir_path = normalize_path(dir_path);
let mut state = self.state.lock().await;
- state.validate_path(&path)?;
- if let Some(entry) = state.entries.get(&path) {
+ state.validate_path(&dir_path)?;
+ if let Some(entry) = state.entries.get(&dir_path) {
if !entry.metadata.is_dir {
- return Err(anyhow!("cannot remove {path:?} because it is not a dir"));
+ return Err(anyhow!(
+ "cannot remove {dir_path:?} because it is not a dir"
+ ));
}
if !options.recursive {
@@ -517,14 +519,14 @@ impl Fs for FakeFs {
.filter(|path| path.starts_with(path))
.count();
if descendants > 1 {
- return Err(anyhow!("{path:?} is not empty"));
+ return Err(anyhow!("{dir_path:?} is not empty"));
}
}
- state.entries.retain(|path, _| !path.starts_with(path));
- state.emit_event(&[path]).await;
+ state.entries.retain(|path, _| !path.starts_with(&dir_path));
+ state.emit_event(&[dir_path]).await;
} else if !options.ignore_if_not_exists {
- return Err(anyhow!("{path:?} does not exist"));
+ return Err(anyhow!("{dir_path:?} does not exist"));
}
Ok(())
@@ -647,9 +649,16 @@ impl Fs for FakeFs {
let (tx, rx) = smol::channel::unbounded();
state.event_txs.push(tx);
let path = path.to_path_buf();
+ let executor = self.executor.clone();
Box::pin(futures::StreamExt::filter(rx, move |events| {
let result = events.iter().any(|event| event.path.starts_with(&path));
- async move { result }
+ let executor = executor.clone();
+ async move {
+ if let Some(executor) = executor.clone().upgrade() {
+ executor.simulate_random_delay().await;
+ }
+ result
+ }
}))
}
@@ -36,9 +36,11 @@ use std::{
cell::RefCell,
cmp::{self, Ordering},
convert::TryInto,
+ ffi::OsString,
hash::Hash,
mem,
ops::Range,
+ os::unix::{ffi::OsStrExt, prelude::OsStringExt},
path::{Component, Path, PathBuf},
rc::Rc,
sync::{
@@ -225,6 +227,8 @@ impl DiagnosticSummary {
pub struct ProjectEntryId(usize);
impl ProjectEntryId {
+ pub const MAX: Self = Self(usize::MAX);
+
pub fn new(counter: &AtomicUsize) -> Self {
Self(counter.fetch_add(1, SeqCst))
}
@@ -257,6 +261,9 @@ impl Project {
client.add_model_message_handler(Self::handle_update_buffer);
client.add_model_message_handler(Self::handle_update_diagnostic_summary);
client.add_model_message_handler(Self::handle_update_worktree);
+ client.add_model_request_handler(Self::handle_create_project_entry);
+ client.add_model_request_handler(Self::handle_rename_project_entry);
+ client.add_model_request_handler(Self::handle_delete_project_entry);
client.add_model_request_handler(Self::handle_apply_additional_edits_for_completion);
client.add_model_request_handler(Self::handle_apply_code_action);
client.add_model_request_handler(Self::handle_reload_buffers);
@@ -452,12 +459,27 @@ impl Project {
}
#[cfg(any(test, feature = "test-support"))]
- pub fn test(fs: Arc<dyn Fs>, cx: &mut gpui::TestAppContext) -> ModelHandle<Project> {
+ pub async fn test(
+ fs: Arc<dyn Fs>,
+ root_paths: impl IntoIterator<Item = impl AsRef<Path>>,
+ cx: &mut gpui::TestAppContext,
+ ) -> ModelHandle<Project> {
let languages = Arc::new(LanguageRegistry::test());
let http_client = client::test::FakeHttpClient::with_404_response();
let client = client::Client::new(http_client.clone());
let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
- cx.update(|cx| Project::local(client, user_store, languages, fs, cx))
+ let project = cx.update(|cx| Project::local(client, user_store, languages, fs, cx));
+ for path in root_paths {
+ let (tree, _) = project
+ .update(cx, |project, cx| {
+ project.find_or_create_local_worktree(path, true, cx)
+ })
+ .await
+ .unwrap();
+ tree.read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete())
+ .await;
+ }
+ project
}
pub fn buffer_for_id(&self, remote_id: u64, cx: &AppContext) -> Option<ModelHandle<Buffer>> {
@@ -669,6 +691,125 @@ impl Project {
.map(|worktree| worktree.read(cx).id())
}
+ pub fn create_entry(
+ &mut self,
+ project_path: impl Into<ProjectPath>,
+ is_directory: bool,
+ cx: &mut ModelContext<Self>,
+ ) -> Option<Task<Result<Entry>>> {
+ let project_path = project_path.into();
+ let worktree = self.worktree_for_id(project_path.worktree_id, cx)?;
+ if self.is_local() {
+ Some(worktree.update(cx, |worktree, cx| {
+ worktree
+ .as_local_mut()
+ .unwrap()
+ .create_entry(project_path.path, is_directory, cx)
+ }))
+ } else {
+ let client = self.client.clone();
+ let project_id = self.remote_id().unwrap();
+ Some(cx.spawn_weak(|_, mut cx| async move {
+ let response = client
+ .request(proto::CreateProjectEntry {
+ worktree_id: project_path.worktree_id.to_proto(),
+ project_id,
+ path: project_path.path.as_os_str().as_bytes().to_vec(),
+ is_directory,
+ })
+ .await?;
+ let entry = response
+ .entry
+ .ok_or_else(|| anyhow!("missing entry in response"))?;
+ worktree
+ .update(&mut cx, |worktree, cx| {
+ worktree.as_remote().unwrap().insert_entry(
+ entry,
+ response.worktree_scan_id as usize,
+ cx,
+ )
+ })
+ .await
+ }))
+ }
+ }
+
+ pub fn rename_entry(
+ &mut self,
+ entry_id: ProjectEntryId,
+ new_path: impl Into<Arc<Path>>,
+ cx: &mut ModelContext<Self>,
+ ) -> Option<Task<Result<Entry>>> {
+ let worktree = self.worktree_for_entry(entry_id, cx)?;
+ let new_path = new_path.into();
+ if self.is_local() {
+ worktree.update(cx, |worktree, cx| {
+ worktree
+ .as_local_mut()
+ .unwrap()
+ .rename_entry(entry_id, new_path, cx)
+ })
+ } else {
+ let client = self.client.clone();
+ let project_id = self.remote_id().unwrap();
+
+ Some(cx.spawn_weak(|_, mut cx| async move {
+ let response = client
+ .request(proto::RenameProjectEntry {
+ project_id,
+ entry_id: entry_id.to_proto(),
+ new_path: new_path.as_os_str().as_bytes().to_vec(),
+ })
+ .await?;
+ let entry = response
+ .entry
+ .ok_or_else(|| anyhow!("missing entry in response"))?;
+ worktree
+ .update(&mut cx, |worktree, cx| {
+ worktree.as_remote().unwrap().insert_entry(
+ entry,
+ response.worktree_scan_id as usize,
+ cx,
+ )
+ })
+ .await
+ }))
+ }
+ }
+
+ pub fn delete_entry(
+ &mut self,
+ entry_id: ProjectEntryId,
+ cx: &mut ModelContext<Self>,
+ ) -> Option<Task<Result<()>>> {
+ let worktree = self.worktree_for_entry(entry_id, cx)?;
+ if self.is_local() {
+ worktree.update(cx, |worktree, cx| {
+ worktree.as_local_mut().unwrap().delete_entry(entry_id, cx)
+ })
+ } else {
+ let client = self.client.clone();
+ let project_id = self.remote_id().unwrap();
+ Some(cx.spawn_weak(|_, mut cx| async move {
+ let response = client
+ .request(proto::DeleteProjectEntry {
+ project_id,
+ entry_id: entry_id.to_proto(),
+ })
+ .await?;
+ worktree
+ .update(&mut cx, move |worktree, cx| {
+ worktree.as_remote().unwrap().delete_entry(
+ entry_id,
+ response.worktree_scan_id as usize,
+ cx,
+ )
+ })
+ .await
+ }))
+ }
+ }
+
pub fn can_share(&self, cx: &AppContext) -> bool {
self.is_local() && self.visible_worktrees(cx).next().is_some()
}
@@ -850,6 +991,18 @@ impl Project {
})
}
+ pub fn open_local_buffer(
+ &mut self,
+ abs_path: impl AsRef<Path>,
+ cx: &mut ModelContext<Self>,
+ ) -> Task<Result<ModelHandle<Buffer>>> {
+ if let Some((worktree, relative_path)) = self.find_local_worktree(abs_path.as_ref(), cx) {
+ self.open_buffer((worktree.read(cx).id(), relative_path), cx)
+ } else {
+ Task::ready(Err(anyhow!("no such path")))
+ }
+ }
+
pub fn open_buffer(
&mut self,
path: impl Into<ProjectPath>,
@@ -879,9 +1032,9 @@ impl Project {
entry.insert(rx.clone());
let load_buffer = if worktree.read(cx).is_local() {
- self.open_local_buffer(&project_path.path, &worktree, cx)
+ self.open_local_buffer_internal(&project_path.path, &worktree, cx)
} else {
- self.open_remote_buffer(&project_path.path, &worktree, cx)
+ self.open_remote_buffer_internal(&project_path.path, &worktree, cx)
};
cx.spawn(move |this, mut cx| async move {
@@ -911,7 +1064,7 @@ impl Project {
})
}
- fn open_local_buffer(
+ fn open_local_buffer_internal(
&mut self,
path: &Arc<Path>,
worktree: &ModelHandle<Worktree>,
@@ -928,7 +1081,7 @@ impl Project {
})
}
- fn open_remote_buffer(
+ fn open_remote_buffer_internal(
&mut self,
path: &Arc<Path>,
worktree: &ModelHandle<Worktree>,
@@ -3664,6 +3817,7 @@ impl Project {
entries: Default::default(),
diagnostic_summaries: Default::default(),
visible: envelope.payload.visible,
+ scan_id: 0,
};
let (worktree, load_task) =
Worktree::remote(remote_id, replica_id, worktree, client, cx);
@@ -3704,6 +3858,86 @@ impl Project {
})
}
+ async fn handle_create_project_entry(
+ this: ModelHandle<Self>,
+ envelope: TypedEnvelope<proto::CreateProjectEntry>,
+ _: Arc<Client>,
+ mut cx: AsyncAppContext,
+ ) -> Result<proto::ProjectEntryResponse> {
+ let worktree = this.update(&mut cx, |this, cx| {
+ let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
+ this.worktree_for_id(worktree_id, cx)
+ .ok_or_else(|| anyhow!("worktree not found"))
+ })?;
+ let worktree_scan_id = worktree.read_with(&cx, |worktree, _| worktree.scan_id());
+ let entry = worktree
+ .update(&mut cx, |worktree, cx| {
+ let worktree = worktree.as_local_mut().unwrap();
+ let path = PathBuf::from(OsString::from_vec(envelope.payload.path));
+ worktree.create_entry(path, envelope.payload.is_directory, cx)
+ })
+ .await?;
+ Ok(proto::ProjectEntryResponse {
+ entry: Some((&entry).into()),
+ worktree_scan_id: worktree_scan_id as u64,
+ })
+ }
+
+ async fn handle_rename_project_entry(
+ this: ModelHandle<Self>,
+ envelope: TypedEnvelope<proto::RenameProjectEntry>,
+ _: Arc<Client>,
+ mut cx: AsyncAppContext,
+ ) -> Result<proto::ProjectEntryResponse> {
+ let entry_id = ProjectEntryId::from_proto(envelope.payload.entry_id);
+ let worktree = this.read_with(&cx, |this, cx| {
+ this.worktree_for_entry(entry_id, cx)
+ .ok_or_else(|| anyhow!("worktree not found"))
+ })?;
+ let worktree_scan_id = worktree.read_with(&cx, |worktree, _| worktree.scan_id());
+ let entry = worktree
+ .update(&mut cx, |worktree, cx| {
+ let new_path = PathBuf::from(OsString::from_vec(envelope.payload.new_path));
+ worktree
+ .as_local_mut()
+ .unwrap()
+ .rename_entry(entry_id, new_path, cx)
+ .ok_or_else(|| anyhow!("invalid entry"))
+ })?
+ .await?;
+ Ok(proto::ProjectEntryResponse {
+ entry: Some((&entry).into()),
+ worktree_scan_id: worktree_scan_id as u64,
+ })
+ }
+
+ async fn handle_delete_project_entry(
+ this: ModelHandle<Self>,
+ envelope: TypedEnvelope<proto::DeleteProjectEntry>,
+ _: Arc<Client>,
+ mut cx: AsyncAppContext,
+ ) -> Result<proto::ProjectEntryResponse> {
+ let entry_id = ProjectEntryId::from_proto(envelope.payload.entry_id);
+ let worktree = this.read_with(&cx, |this, cx| {
+ this.worktree_for_entry(entry_id, cx)
+ .ok_or_else(|| anyhow!("worktree not found"))
+ })?;
+ let worktree_scan_id = worktree.read_with(&cx, |worktree, _| worktree.scan_id());
+ worktree
+ .update(&mut cx, |worktree, cx| {
+ worktree
+ .as_local_mut()
+ .unwrap()
+ .delete_entry(entry_id, cx)
+ .ok_or_else(|| anyhow!("invalid entry"))
+ })?
+ .await?;
+ Ok(proto::ProjectEntryResponse {
+ entry: None,
+ worktree_scan_id: worktree_scan_id as u64,
+ })
+ }
+
async fn handle_update_diagnostic_summary(
this: ModelHandle<Self>,
envelope: TypedEnvelope<proto::UpdateDiagnosticSummary>,
@@ -4905,6 +5139,8 @@ impl Item for Buffer {
#[cfg(test)]
mod tests {
+ use crate::worktree::WorktreeHandle;
+
use super::{Event, *};
use fs::RealFs;
use futures::{future, StreamExt};
@@ -4918,7 +5154,6 @@ mod tests {
use std::{cell::RefCell, os::unix, path::PathBuf, rc::Rc, task::Poll};
use unindent::Unindent as _;
use util::{assert_set_eq, test::temp_tree};
- use worktree::WorktreeHandle as _;
#[gpui::test]
async fn test_populate_and_search(cx: &mut gpui::TestAppContext) {
@@ -4945,19 +5180,10 @@ mod tests {
)
.unwrap();
- let project = Project::test(Arc::new(RealFs), cx);
+ let project = Project::test(Arc::new(RealFs), [root_link_path], cx).await;
- let (tree, _) = project
- .update(cx, |project, cx| {
- project.find_or_create_local_worktree(&root_link_path, true, cx)
- })
- .await
- .unwrap();
-
- cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
- .await;
- cx.read(|cx| {
- let tree = tree.read(cx);
+ project.read_with(cx, |project, cx| {
+ let tree = project.worktrees(cx).next().unwrap().read(cx);
assert_eq!(tree.file_count(), 5);
assert_eq!(
tree.inode_for_path("fennel/grape"),
@@ -5038,25 +5264,16 @@ mod tests {
)
.await;
- let project = Project::test(fs.clone(), cx);
+ let project = Project::test(fs.clone(), ["/the-root"], cx).await;
project.update(cx, |project, _| {
project.languages.add(Arc::new(rust_language));
project.languages.add(Arc::new(json_language));
});
- let worktree_id = project
- .update(cx, |project, cx| {
- project.find_or_create_local_worktree("/the-root", true, cx)
- })
- .await
- .unwrap()
- .0
- .read_with(cx, |tree, _| tree.id());
-
// Open a buffer without an associated language server.
let toml_buffer = project
.update(cx, |project, cx| {
- project.open_buffer((worktree_id, "Cargo.toml"), cx)
+ project.open_local_buffer("/the-root/Cargo.toml", cx)
})
.await
.unwrap();
@@ -5064,7 +5281,7 @@ mod tests {
// Open a buffer with an associated language server.
let rust_buffer = project
.update(cx, |project, cx| {
- project.open_buffer((worktree_id, "test.rs"), cx)
+ project.open_local_buffer("/the-root/test.rs", cx)
})
.await
.unwrap();
@@ -5111,7 +5328,7 @@ mod tests {
// Open a third buffer with a different associated language server.
let json_buffer = project
.update(cx, |project, cx| {
- project.open_buffer((worktree_id, "package.json"), cx)
+ project.open_local_buffer("/the-root/package.json", cx)
})
.await
.unwrap();
@@ -5141,7 +5358,7 @@ mod tests {
// it is also configured based on the existing language server's capabilities.
let rust_buffer2 = project
.update(cx, |project, cx| {
- project.open_buffer((worktree_id, "test2.rs"), cx)
+ project.open_local_buffer("/the-root/test2.rs", cx)
})
.await
.unwrap();
@@ -5270,6 +5487,7 @@ mod tests {
language_id: Default::default()
},
);
+
// We clear the diagnostics, since the language has changed.
rust_buffer2.read_with(cx, |buffer, _| {
assert_eq!(
@@ -5382,34 +5600,14 @@ mod tests {
)
.await;
- let project = Project::test(fs, cx);
- let worktree_a_id = project
- .update(cx, |project, cx| {
- project.find_or_create_local_worktree("/dir/a.rs", true, cx)
- })
- .await
- .unwrap()
- .0
- .read_with(cx, |tree, _| tree.id());
- let worktree_b_id = project
- .update(cx, |project, cx| {
- project.find_or_create_local_worktree("/dir/b.rs", true, cx)
- })
- .await
- .unwrap()
- .0
- .read_with(cx, |tree, _| tree.id());
+ let project = Project::test(fs, ["/dir/a.rs", "/dir/b.rs"], cx).await;
let buffer_a = project
- .update(cx, |project, cx| {
- project.open_buffer((worktree_a_id, ""), cx)
- })
+ .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
.await
.unwrap();
let buffer_b = project
- .update(cx, |project, cx| {
- project.open_buffer((worktree_b_id, ""), cx)
- })
+ .update(cx, |project, cx| project.open_local_buffer("/dir/b.rs", cx))
.await
.unwrap();
@@ -5513,25 +5711,14 @@ mod tests {
)
.await;
- let project = Project::test(fs, cx);
+ let project = Project::test(fs, ["/dir"], cx).await;
project.update(cx, |project, _| project.languages.add(Arc::new(language)));
-
- let (tree, _) = project
- .update(cx, |project, cx| {
- project.find_or_create_local_worktree("/dir", true, cx)
- })
- .await
- .unwrap();
- let worktree_id = tree.read_with(cx, |tree, _| tree.id());
-
- cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
- .await;
+ let worktree_id =
+ project.read_with(cx, |p, cx| p.worktrees(cx).next().unwrap().read(cx).id());
// Cause worktree to start the fake language server
let _buffer = project
- .update(cx, |project, cx| {
- project.open_buffer((worktree_id, Path::new("b.rs")), cx)
- })
+ .update(cx, |project, cx| project.open_local_buffer("/dir/b.rs", cx))
.await
.unwrap();
@@ -5577,7 +5764,7 @@ mod tests {
);
let buffer = project
- .update(cx, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))
+ .update(cx, |p, cx| p.open_local_buffer("/dir/a.rs", cx))
.await
.unwrap();
@@ -5646,22 +5833,11 @@ mod tests {
let fs = FakeFs::new(cx.background());
fs.insert_tree("/dir", json!({ "a.rs": "" })).await;
- let project = Project::test(fs, cx);
+ let project = Project::test(fs, ["/dir"], cx).await;
project.update(cx, |project, _| project.languages.add(Arc::new(language)));
- let worktree_id = project
- .update(cx, |project, cx| {
- project.find_or_create_local_worktree("/dir", true, cx)
- })
- .await
- .unwrap()
- .0
- .read_with(cx, |tree, _| tree.id());
-
let buffer = project
- .update(cx, |project, cx| {
- project.open_buffer((worktree_id, "a.rs"), cx)
- })
+ .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
.await
.unwrap();
@@ -5726,22 +5902,11 @@ mod tests {
let fs = FakeFs::new(cx.background());
fs.insert_tree("/dir", json!({ "a.rs": text })).await;
- let project = Project::test(fs, cx);
+ let project = Project::test(fs, ["/dir"], cx).await;
project.update(cx, |project, _| project.languages.add(Arc::new(language)));
- let worktree_id = project
- .update(cx, |project, cx| {
- project.find_or_create_local_worktree("/dir", true, cx)
- })
- .await
- .unwrap()
- .0
- .read_with(cx, |tree, _| tree.id());
-
let buffer = project
- .update(cx, |project, cx| {
- project.open_buffer((worktree_id, "a.rs"), cx)
- })
+ .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
.await
.unwrap();
@@ -6006,20 +6171,9 @@ mod tests {
let fs = FakeFs::new(cx.background());
fs.insert_tree("/dir", json!({ "a.rs": text })).await;
- let project = Project::test(fs, cx);
- let worktree_id = project
- .update(cx, |project, cx| {
- project.find_or_create_local_worktree("/dir", true, cx)
- })
- .await
- .unwrap()
- .0
- .read_with(cx, |tree, _| tree.id());
-
+ let project = Project::test(fs, ["/dir"], cx).await;
let buffer = project
- .update(cx, |project, cx| {
- project.open_buffer((worktree_id, "a.rs"), cx)
- })
+ .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
.await
.unwrap();
@@ -6108,22 +6262,10 @@ mod tests {
)
.await;
- let project = Project::test(fs, cx);
+ let project = Project::test(fs, ["/dir"], cx).await;
project.update(cx, |project, _| project.languages.add(Arc::new(language)));
-
- let worktree_id = project
- .update(cx, |project, cx| {
- project.find_or_create_local_worktree("/dir", true, cx)
- })
- .await
- .unwrap()
- .0
- .read_with(cx, |tree, _| tree.id());
-
let buffer = project
- .update(cx, |project, cx| {
- project.open_buffer((worktree_id, "a.rs"), cx)
- })
+ .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
.await
.unwrap();
@@ -6274,20 +6416,9 @@ mod tests {
)
.await;
- let project = Project::test(fs, cx);
- let worktree_id = project
- .update(cx, |project, cx| {
- project.find_or_create_local_worktree("/dir", true, cx)
- })
- .await
- .unwrap()
- .0
- .read_with(cx, |tree, _| tree.id());
-
+ let project = Project::test(fs, ["/dir"], cx).await;
let buffer = project
- .update(cx, |project, cx| {
- project.open_buffer((worktree_id, "a.rs"), cx)
- })
+ .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
.await
.unwrap();
@@ -6408,17 +6539,7 @@ mod tests {
}
}));
- let project = Project::test(Arc::new(RealFs), cx);
- let (tree, _) = project
- .update(cx, |project, cx| {
- project.find_or_create_local_worktree(&dir.path(), true, cx)
- })
- .await
- .unwrap();
-
- cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
- .await;
-
+ let project = Project::test(Arc::new(RealFs), [dir.path()], cx).await;
let cancel_flag = Default::default();
let results = project
.read_with(cx, |project, cx| {
@@ -6451,21 +6572,11 @@ mod tests {
)
.await;
- let project = Project::test(fs, cx);
+ let project = Project::test(fs, ["/dir/b.rs"], cx).await;
project.update(cx, |project, _| project.languages.add(Arc::new(language)));
- let (tree, _) = project
- .update(cx, |project, cx| {
- project.find_or_create_local_worktree("/dir/b.rs", true, cx)
- })
- .await
- .unwrap();
- let worktree_id = tree.read_with(cx, |tree, _| tree.id());
- cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
- .await;
-
let buffer = project
- .update(cx, |project, cx| project.open_buffer((worktree_id, ""), cx))
+ .update(cx, |project, cx| project.open_local_buffer("/dir/b.rs", cx))
.await
.unwrap();
@@ -6555,21 +6666,10 @@ mod tests {
)
.await;
- let project = Project::test(fs, cx);
+ let project = Project::test(fs, ["/dir"], cx).await;
project.update(cx, |project, _| project.languages.add(Arc::new(language)));
-
- let (tree, _) = project
- .update(cx, |project, cx| {
- project.find_or_create_local_worktree("/dir", true, cx)
- })
- .await
- .unwrap();
- let worktree_id = tree.read_with(cx, |tree, _| tree.id());
- cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
- .await;
-
let buffer = project
- .update(cx, |p, cx| p.open_buffer((worktree_id, "a.ts"), cx))
+ .update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
.await
.unwrap();
@@ -6624,21 +6724,10 @@ mod tests {
)
.await;
- let project = Project::test(fs, cx);
+ let project = Project::test(fs, ["/dir"], cx).await;
project.update(cx, |project, _| project.languages.add(Arc::new(language)));
-
- let (tree, _) = project
- .update(cx, |project, cx| {
- project.find_or_create_local_worktree("/dir", true, cx)
- })
- .await
- .unwrap();
- let worktree_id = tree.read_with(cx, |tree, _| tree.id());
- cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
- .await;
-
let buffer = project
- .update(cx, |p, cx| p.open_buffer((worktree_id, "a.ts"), cx))
+ .update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
.await
.unwrap();
@@ -6741,18 +6830,9 @@ mod tests {
)
.await;
- let project = Project::test(fs.clone(), cx);
- let worktree_id = project
- .update(cx, |p, cx| {
- p.find_or_create_local_worktree("/dir", true, cx)
- })
- .await
- .unwrap()
- .0
- .read_with(cx, |tree, _| tree.id());
-
+ let project = Project::test(fs.clone(), ["/dir"], cx).await;
let buffer = project
- .update(cx, |p, cx| p.open_buffer((worktree_id, "file1"), cx))
+ .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
.await
.unwrap();
buffer
@@ -6779,18 +6859,9 @@ mod tests {
)
.await;
- let project = Project::test(fs.clone(), cx);
- let worktree_id = project
- .update(cx, |p, cx| {
- p.find_or_create_local_worktree("/dir/file1", true, cx)
- })
- .await
- .unwrap()
- .0
- .read_with(cx, |tree, _| tree.id());
-
+ let project = Project::test(fs.clone(), ["/dir/file1"], cx).await;
let buffer = project
- .update(cx, |p, cx| p.open_buffer((worktree_id, ""), cx))
+ .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
.await
.unwrap();
buffer
@@ -6810,15 +6881,7 @@ mod tests {
let fs = FakeFs::new(cx.background());
fs.insert_tree("/dir", json!({})).await;
- let project = Project::test(fs.clone(), cx);
- let (worktree, _) = project
- .update(cx, |project, cx| {
- project.find_or_create_local_worktree("/dir", true, cx)
- })
- .await
- .unwrap();
- let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
-
+ let project = Project::test(fs.clone(), ["/dir"], cx).await;
let buffer = project.update(cx, |project, cx| {
project.create_buffer("", None, cx).unwrap()
});
@@ -6842,7 +6905,7 @@ mod tests {
let opened_buffer = project
.update(cx, |project, cx| {
- project.open_buffer((worktree_id, "file1"), cx)
+ project.open_local_buffer("/dir/file1", cx)
})
.await
.unwrap();
@@ -6865,24 +6928,18 @@ mod tests {
}
}));
- let project = Project::test(Arc::new(RealFs), cx);
+ let project = Project::test(Arc::new(RealFs), [dir.path()], cx).await;
let rpc = project.read_with(cx, |p, _| p.client.clone());
- let (tree, _) = project
- .update(cx, |p, cx| {
- p.find_or_create_local_worktree(dir.path(), true, cx)
- })
- .await
- .unwrap();
- let worktree_id = tree.read_with(cx, |tree, _| tree.id());
-
let buffer_for_path = |path: &'static str, cx: &mut gpui::TestAppContext| {
- let buffer = project.update(cx, |p, cx| p.open_buffer((worktree_id, path), cx));
+ let buffer = project.update(cx, |p, cx| p.open_local_buffer(dir.path().join(path), cx));
async move { buffer.await.unwrap() }
};
let id_for_path = |path: &'static str, cx: &gpui::TestAppContext| {
- tree.read_with(cx, |tree, _| {
- tree.entry_for_path(path)
+ project.read_with(cx, |project, cx| {
+ let tree = project.worktrees(cx).next().unwrap();
+ tree.read(cx)
+ .entry_for_path(path)
.expect(&format!("no entry for path {}", path))
.id
})
@@ -6897,11 +6954,8 @@ mod tests {
let file3_id = id_for_path("a/file3", &cx);
let file4_id = id_for_path("b/c/file4", &cx);
- // Wait for the initial scan.
- cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
- .await;
-
// Create a remote copy of this worktree.
+ let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
let initial_snapshot = tree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot());
let (remote, load_task) = cx.update(|cx| {
Worktree::remote(
@@ -6912,6 +6966,7 @@ mod tests {
cx,
)
});
+ // tree
load_task.await;
cx.read(|cx| {
@@ -7005,7 +7060,7 @@ mod tests {
async fn test_buffer_deduping(cx: &mut gpui::TestAppContext) {
let fs = FakeFs::new(cx.background());
fs.insert_tree(
- "/the-dir",
+ "/dir",
json!({
"a.txt": "a-contents",
"b.txt": "b-contents",
@@ -7013,22 +7068,14 @@ mod tests {
)
.await;
- let project = Project::test(fs.clone(), cx);
- let worktree_id = project
- .update(cx, |p, cx| {
- p.find_or_create_local_worktree("/the-dir", true, cx)
- })
- .await
- .unwrap()
- .0
- .read_with(cx, |tree, _| tree.id());
+ let project = Project::test(fs.clone(), ["/dir"], cx).await;
// Spawn multiple tasks to open paths, repeating some paths.
let (buffer_a_1, buffer_b, buffer_a_2) = project.update(cx, |p, cx| {
(
- p.open_buffer((worktree_id, "a.txt"), cx),
- p.open_buffer((worktree_id, "b.txt"), cx),
- p.open_buffer((worktree_id, "a.txt"), cx),
+ p.open_local_buffer("/dir/a.txt", cx),
+ p.open_local_buffer("/dir/b.txt", cx),
+ p.open_local_buffer("/dir/a.txt", cx),
)
});
@@ -7045,7 +7092,7 @@ mod tests {
// Open the same path again while it is still open.
drop(buffer_a_1);
let buffer_a_3 = project
- .update(cx, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))
+ .update(cx, |p, cx| p.open_local_buffer("/dir/a.txt", cx))
.await
.unwrap();
@@ -7055,30 +7102,21 @@ mod tests {
#[gpui::test]
async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) {
- use std::fs;
-
- let dir = temp_tree(json!({
- "file1": "abc",
- "file2": "def",
- "file3": "ghi",
- }));
-
- let project = Project::test(Arc::new(RealFs), cx);
- let (worktree, _) = project
- .update(cx, |p, cx| {
- p.find_or_create_local_worktree(dir.path(), true, cx)
- })
- .await
- .unwrap();
- let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id());
+ let fs = FakeFs::new(cx.background());
+ fs.insert_tree(
+ "/dir",
+ json!({
+ "file1": "abc",
+ "file2": "def",
+ "file3": "ghi",
+ }),
+ )
+ .await;
- worktree.flush_fs_events(&cx).await;
- worktree
- .read_with(cx, |t, _| t.as_local().unwrap().scan_complete())
- .await;
+ let project = Project::test(fs.clone(), ["/dir"], cx).await;
let buffer1 = project
- .update(cx, |p, cx| p.open_buffer((worktree_id, "file1"), cx))
+ .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
.await
.unwrap();
let events = Rc::new(RefCell::new(Vec::new()));
@@ -7148,7 +7186,7 @@ mod tests {
// When a file is deleted, the buffer is considered dirty.
let events = Rc::new(RefCell::new(Vec::new()));
let buffer2 = project
- .update(cx, |p, cx| p.open_buffer((worktree_id, "file2"), cx))
+ .update(cx, |p, cx| p.open_local_buffer("/dir/file2", cx))
.await
.unwrap();
buffer2.update(cx, |_, cx| {
@@ -7159,7 +7197,9 @@ mod tests {
.detach();
});
- fs::remove_file(dir.path().join("file2")).unwrap();
+ fs.remove_file("/dir/file2".as_ref(), Default::default())
+ .await
+ .unwrap();
buffer2.condition(&cx, |b, _| b.is_dirty()).await;
assert_eq!(
*events.borrow(),
@@ -7169,7 +7209,7 @@ mod tests {
// When a file is already dirty when deleted, we don't emit a Dirtied event.
let events = Rc::new(RefCell::new(Vec::new()));
let buffer3 = project
- .update(cx, |p, cx| p.open_buffer((worktree_id, "file3"), cx))
+ .update(cx, |p, cx| p.open_local_buffer("/dir/file3", cx))
.await
.unwrap();
buffer3.update(cx, |_, cx| {
@@ -7180,12 +7220,13 @@ mod tests {
.detach();
});
- worktree.flush_fs_events(&cx).await;
buffer3.update(cx, |buffer, cx| {
buffer.edit([(0..0, "x")], cx);
});
events.borrow_mut().clear();
- fs::remove_file(dir.path().join("file3")).unwrap();
+ fs.remove_file("/dir/file3".as_ref(), Default::default())
+ .await
+ .unwrap();
buffer3
.condition(&cx, |_, _| !events.borrow().is_empty())
.await;
@@ -7195,47 +7236,24 @@ mod tests {
#[gpui::test]
async fn test_buffer_file_changes_on_disk(cx: &mut gpui::TestAppContext) {
- use std::fs;
-
let initial_contents = "aaa\nbbbbb\nc\n";
- let dir = temp_tree(json!({ "the-file": initial_contents }));
-
- let project = Project::test(Arc::new(RealFs), cx);
- let (worktree, _) = project
- .update(cx, |p, cx| {
- p.find_or_create_local_worktree(dir.path(), true, cx)
- })
- .await
- .unwrap();
- let worktree_id = worktree.read_with(cx, |tree, _| tree.id());
-
- worktree
- .read_with(cx, |t, _| t.as_local().unwrap().scan_complete())
- .await;
-
- let abs_path = dir.path().join("the-file");
+ let fs = FakeFs::new(cx.background());
+ fs.insert_tree(
+ "/dir",
+ json!({
+ "the-file": initial_contents,
+ }),
+ )
+ .await;
+ let project = Project::test(fs.clone(), ["/dir"], cx).await;
let buffer = project
- .update(cx, |p, cx| p.open_buffer((worktree_id, "the-file"), cx))
+ .update(cx, |p, cx| p.open_local_buffer("/dir/the-file", cx))
.await
.unwrap();
- // TODO
- // Add a cursor on each row.
- // let selection_set_id = buffer.update(&mut cx, |buffer, cx| {
- // assert!(!buffer.is_dirty());
- // buffer.add_selection_set(
- // &(0..3)
- // .map(|row| Selection {
- // id: row as usize,
- // start: Point::new(row, 1),
- // end: Point::new(row, 1),
- // reversed: false,
- // goal: SelectionGoal::None,
- // })
- // .collect::<Vec<_>>(),
- // cx,
- // )
- // });
+ let anchors = (0..3)
+ .map(|row| buffer.read_with(cx, |b, _| b.anchor_before(Point::new(row, 1))))
+ .collect::<Vec<_>>();
// Change the file on disk, adding two new lines of text, and removing
// one line.
@@ -1,4 +1,4 @@
-use crate::ProjectEntryId;
+use crate::{ProjectEntryId, RemoveOptions};
use super::{
fs::{self, Fs},
@@ -42,6 +42,7 @@ use std::{
fmt,
future::Future,
ops::{Deref, DerefMut},
+ os::unix::prelude::{OsStrExt, OsStringExt},
path::{Path, PathBuf},
sync::{atomic::AtomicUsize, Arc},
time::{Duration, SystemTime},
@@ -78,11 +79,12 @@ pub struct LocalWorktree {
}
pub struct RemoteWorktree {
- pub(crate) snapshot: Snapshot,
+ pub snapshot: Snapshot,
+ pub(crate) background_snapshot: Arc<Mutex<Snapshot>>,
project_id: u64,
- snapshot_rx: watch::Receiver<Snapshot>,
client: Arc<Client>,
updates_tx: UnboundedSender<proto::UpdateWorktree>,
+ last_scan_id_rx: watch::Receiver<usize>,
replica_id: ReplicaId,
diagnostic_summaries: TreeMap<PathKey, DiagnosticSummary>,
visible: bool,
@@ -95,12 +97,12 @@ pub struct Snapshot {
root_char_bag: CharBag,
entries_by_path: SumTree<Entry>,
entries_by_id: SumTree<PathEntry>,
+ scan_id: usize,
}
#[derive(Clone)]
pub struct LocalSnapshot {
abs_path: Arc<Path>,
- scan_id: usize,
ignores: HashMap<Arc<Path>, (Arc<Gitignore>, usize)>,
removed_entry_ids: HashMap<u64, ProjectEntryId>,
next_entry_id: Arc<AtomicUsize>,
@@ -214,17 +216,21 @@ impl Worktree {
root_char_bag,
entries_by_path: Default::default(),
entries_by_id: Default::default(),
+ scan_id: worktree.scan_id as usize,
};
let (updates_tx, mut updates_rx) = mpsc::unbounded();
- let (mut snapshot_tx, snapshot_rx) = watch::channel_with(snapshot.clone());
+ let background_snapshot = Arc::new(Mutex::new(snapshot.clone()));
+ let (mut snapshot_updated_tx, mut snapshot_updated_rx) = watch::channel();
+ let (mut last_scan_id_tx, last_scan_id_rx) = watch::channel_with(worktree.scan_id as usize);
let worktree_handle = cx.add_model(|_: &mut ModelContext<Worktree>| {
Worktree::Remote(RemoteWorktree {
project_id: project_remote_id,
replica_id,
snapshot: snapshot.clone(),
- snapshot_rx: snapshot_rx.clone(),
+ background_snapshot: background_snapshot.clone(),
updates_tx,
+ last_scan_id_rx,
client: client.clone(),
diagnostic_summaries: TreeMap::from_ordered_entries(
worktree.diagnostic_summaries.into_iter().map(|summary| {
@@ -274,37 +280,42 @@ impl Worktree {
.await;
{
- let mut snapshot = snapshot_tx.borrow_mut();
+ let mut snapshot = background_snapshot.lock();
snapshot.entries_by_path = entries_by_path;
snapshot.entries_by_id = entries_by_id;
+ snapshot_updated_tx.send(()).await.ok();
}
cx.background()
.spawn(async move {
while let Some(update) = updates_rx.next().await {
- let mut snapshot = snapshot_tx.borrow().clone();
- if let Err(error) = snapshot.apply_remote_update(update) {
+ if let Err(error) =
+ background_snapshot.lock().apply_remote_update(update)
+ {
log::error!("error applying worktree update: {}", error);
}
- *snapshot_tx.borrow_mut() = snapshot;
+ snapshot_updated_tx.send(()).await.ok();
}
})
.detach();
- {
- let mut snapshot_rx = snapshot_rx.clone();
+ cx.spawn(|mut cx| {
let this = worktree_handle.downgrade();
- cx.spawn(|mut cx| async move {
- while let Some(_) = snapshot_rx.recv().await {
+ async move {
+ while let Some(_) = snapshot_updated_rx.recv().await {
if let Some(this) = this.upgrade(&cx) {
- this.update(&mut cx, |this, cx| this.poll_snapshot(cx));
+ this.update(&mut cx, |this, cx| {
+ this.poll_snapshot(cx);
+ let this = this.as_remote_mut().unwrap();
+ *last_scan_id_tx.borrow_mut() = this.snapshot.scan_id;
+ });
} else {
break;
}
}
- })
- .detach();
- }
+ }
+ })
+ .detach();
}
});
(worktree_handle, deserialize_task)
@@ -357,6 +368,13 @@ impl Worktree {
}
}
+ pub fn scan_id(&self) -> usize {
+ match self {
+ Worktree::Local(worktree) => worktree.snapshot.scan_id,
+ Worktree::Remote(worktree) => worktree.snapshot.scan_id,
+ }
+ }
+
pub fn is_visible(&self) -> bool {
match self {
Worktree::Local(worktree) => worktree.visible,
@@ -410,7 +428,7 @@ impl Worktree {
}
}
Self::Remote(worktree) => {
- worktree.snapshot = worktree.snapshot_rx.borrow().clone();
+ worktree.snapshot = worktree.background_snapshot.lock().clone();
cx.emit(Event::UpdatedEntries);
}
};
@@ -454,7 +472,6 @@ impl LocalWorktree {
let tree = cx.add_model(move |cx: &mut ModelContext<Worktree>| {
let mut snapshot = LocalSnapshot {
abs_path,
- scan_id: 0,
ignores: Default::default(),
removed_entry_ids: Default::default(),
next_entry_id,
@@ -464,6 +481,7 @@ impl LocalWorktree {
root_char_bag,
entries_by_path: Default::default(),
entries_by_id: Default::default(),
+ scan_id: 0,
},
};
if let Some(metadata) = metadata {
@@ -494,24 +512,13 @@ impl LocalWorktree {
cx.spawn_weak(|this, mut cx| async move {
while let Some(scan_state) = scan_states_rx.next().await {
- if let Some(handle) = this.upgrade(&cx) {
- let to_send = handle.update(&mut cx, |this, cx| {
- last_scan_state_tx.blocking_send(scan_state).ok();
+ if let Some(this) = this.upgrade(&cx) {
+ last_scan_state_tx.blocking_send(scan_state).ok();
+ this.update(&mut cx, |this, cx| {
this.poll_snapshot(cx);
- let tree = this.as_local_mut().unwrap();
- if !tree.is_scanning() {
- if let Some(share) = tree.share.as_ref() {
- return Some((tree.snapshot(), share.snapshots_tx.clone()));
- }
- }
- None
- });
-
- if let Some((snapshot, snapshots_to_send_tx)) = to_send {
- if let Err(err) = snapshots_to_send_tx.send(snapshot).await {
- log::error!("error submitting snapshot to send {}", err);
- }
- }
+ this.as_local().unwrap().broadcast_snapshot()
+ })
+ .await;
} else {
break;
}
@@ -623,12 +630,15 @@ impl LocalWorktree {
let handle = cx.handle();
let path = Arc::from(path);
let abs_path = self.absolutize(&path);
- let background_snapshot = self.background_snapshot.clone();
let fs = self.fs.clone();
cx.spawn(|this, mut cx| async move {
let text = fs.load(&abs_path).await?;
// Eagerly populate the snapshot with an updated entry for the loaded file
- let entry = refresh_entry(fs.as_ref(), &background_snapshot, path, &abs_path).await?;
+ let entry = this
+ .update(&mut cx, |this, _| {
+ this.as_local().unwrap().refresh_entry(path, abs_path, None)
+ })
+ .await?;
this.update(&mut cx, |this, cx| this.poll_snapshot(cx));
Ok((
File {
@@ -652,7 +662,7 @@ impl LocalWorktree {
let buffer = buffer_handle.read(cx);
let text = buffer.as_rope().clone();
let version = buffer.version();
- let save = self.save(path, text, cx);
+ let save = self.write_file(path, text, cx);
let handle = cx.handle();
cx.as_mut().spawn(|mut cx| async move {
let entry = save.await?;
@@ -672,28 +682,188 @@ impl LocalWorktree {
})
}
- fn save(
+ pub fn create_entry(
+ &self,
+ path: impl Into<Arc<Path>>,
+ is_dir: bool,
+ cx: &mut ModelContext<Worktree>,
+ ) -> Task<Result<Entry>> {
+ self.write_entry_internal(
+ path,
+ if is_dir {
+ None
+ } else {
+ Some(Default::default())
+ },
+ cx,
+ )
+ }
+
+ pub fn write_file(
&self,
path: impl Into<Arc<Path>>,
text: Rope,
cx: &mut ModelContext<Worktree>,
+ ) -> Task<Result<Entry>> {
+ self.write_entry_internal(path, Some(text), cx)
+ }
+
+ pub fn delete_entry(
+ &self,
+ entry_id: ProjectEntryId,
+ cx: &mut ModelContext<Worktree>,
+ ) -> Option<Task<Result<()>>> {
+ let entry = self.entry_for_id(entry_id)?.clone();
+ let abs_path = self.absolutize(&entry.path);
+ let delete = cx.background().spawn({
+ let fs = self.fs.clone();
+ let abs_path = abs_path.clone();
+ async move {
+ if entry.is_file() {
+ fs.remove_file(&abs_path, Default::default()).await
+ } else {
+ fs.remove_dir(
+ &abs_path,
+ RemoveOptions {
+ recursive: true,
+ ignore_if_not_exists: false,
+ },
+ )
+ .await
+ }
+ }
+ });
+
+ Some(cx.spawn(|this, mut cx| async move {
+ delete.await?;
+ this.update(&mut cx, |this, _| {
+ let this = this.as_local_mut().unwrap();
+ let mut snapshot = this.background_snapshot.lock();
+ snapshot.delete_entry(entry_id);
+ });
+ this.update(&mut cx, |this, cx| {
+ this.poll_snapshot(cx);
+ this.as_local().unwrap().broadcast_snapshot()
+ })
+ .await;
+ Ok(())
+ }))
+ }
+
+ pub fn rename_entry(
+ &self,
+ entry_id: ProjectEntryId,
+ new_path: impl Into<Arc<Path>>,
+ cx: &mut ModelContext<Worktree>,
+ ) -> Option<Task<Result<Entry>>> {
+ let old_path = self.entry_for_id(entry_id)?.path.clone();
+ let new_path = new_path.into();
+ let abs_old_path = self.absolutize(&old_path);
+ let abs_new_path = self.absolutize(&new_path);
+ let rename = cx.background().spawn({
+ let fs = self.fs.clone();
+ let abs_new_path = abs_new_path.clone();
+ async move {
+ fs.rename(&abs_old_path, &abs_new_path, Default::default())
+ .await
+ }
+ });
+
+ Some(cx.spawn(|this, mut cx| async move {
+ rename.await?;
+ let entry = this
+ .update(&mut cx, |this, _| {
+ this.as_local_mut().unwrap().refresh_entry(
+ new_path.clone(),
+ abs_new_path,
+ Some(old_path),
+ )
+ })
+ .await?;
+ this.update(&mut cx, |this, cx| {
+ this.poll_snapshot(cx);
+ this.as_local().unwrap().broadcast_snapshot()
+ })
+ .await;
+ Ok(entry)
+ }))
+ }
+
+ fn write_entry_internal(
+ &self,
+ path: impl Into<Arc<Path>>,
+ text_if_file: Option<Rope>,
+ cx: &mut ModelContext<Worktree>,
) -> Task<Result<Entry>> {
let path = path.into();
let abs_path = self.absolutize(&path);
- let background_snapshot = self.background_snapshot.clone();
- let fs = self.fs.clone();
- let save = cx.background().spawn(async move {
- fs.save(&abs_path, &text).await?;
- refresh_entry(fs.as_ref(), &background_snapshot, path.clone(), &abs_path).await
+ let write = cx.background().spawn({
+ let fs = self.fs.clone();
+ let abs_path = abs_path.clone();
+ async move {
+ if let Some(text) = text_if_file {
+ fs.save(&abs_path, &text).await
+ } else {
+ fs.create_dir(&abs_path).await
+ }
+ }
});
cx.spawn(|this, mut cx| async move {
- let entry = save.await?;
- this.update(&mut cx, |this, cx| this.poll_snapshot(cx));
+ write.await?;
+ let entry = this
+ .update(&mut cx, |this, _| {
+ this.as_local_mut()
+ .unwrap()
+ .refresh_entry(path, abs_path, None)
+ })
+ .await?;
+ this.update(&mut cx, |this, cx| {
+ this.poll_snapshot(cx);
+ this.as_local().unwrap().broadcast_snapshot()
+ })
+ .await;
Ok(entry)
})
}
+ fn refresh_entry(
+ &self,
+ path: Arc<Path>,
+ abs_path: PathBuf,
+ old_path: Option<Arc<Path>>,
+ ) -> impl Future<Output = Result<Entry>> {
+ let root_char_bag;
+ let next_entry_id;
+ let fs = self.fs.clone();
+ let shared_snapshots_tx = self.share.as_ref().map(|share| share.snapshots_tx.clone());
+ let snapshot = self.background_snapshot.clone();
+ {
+ let snapshot = snapshot.lock();
+ root_char_bag = snapshot.root_char_bag;
+ next_entry_id = snapshot.next_entry_id.clone();
+ }
+ async move {
+ let entry = Entry::new(
+ path,
+ &fs.metadata(&abs_path)
+ .await?
+ .ok_or_else(|| anyhow!("could not read saved file metadata"))?,
+ &next_entry_id,
+ root_char_bag,
+ );
+ let mut snapshot = snapshot.lock();
+ if let Some(old_path) = old_path {
+ snapshot.remove_path(&old_path);
+ }
+ let entry = snapshot.insert_entry(entry, fs.as_ref());
+ if let Some(tx) = shared_snapshots_tx {
+ tx.send(snapshot.clone()).await.ok();
+ }
+ Ok(entry)
+ }
+ }
+
pub fn register(
&mut self,
project_id: u64,
@@ -761,6 +931,7 @@ impl LocalWorktree {
.map(Into::into)
.collect(),
removed_entries: Default::default(),
+ scan_id: snapshot.scan_id as u64,
})
.await
{
@@ -829,6 +1000,23 @@ impl LocalWorktree {
pub fn is_shared(&self) -> bool {
self.share.is_some()
}
+
+ fn broadcast_snapshot(&self) -> impl Future<Output = ()> {
+ let mut to_send = None;
+ if !self.is_scanning() {
+ if let Some(share) = self.share.as_ref() {
+ to_send = Some((self.snapshot(), share.snapshots_tx.clone()));
+ }
+ }
+
+ async move {
+ if let Some((snapshot, snapshots_to_send_tx)) = to_send {
+ if let Err(err) = snapshots_to_send_tx.send(snapshot).await {
+ log::error!("error submitting snapshot to send {}", err);
+ }
+ }
+ }
+ }
}
impl RemoteWorktree {
@@ -843,10 +1031,20 @@ impl RemoteWorktree {
self.updates_tx
.unbounded_send(envelope.payload)
.expect("consumer runs to completion");
-
Ok(())
}
+ fn wait_for_snapshot(&self, scan_id: usize) -> impl Future<Output = ()> {
+ let mut rx = self.last_scan_id_rx.clone();
+ async move {
+ while let Some(applied_scan_id) = rx.next().await {
+ if applied_scan_id >= scan_id {
+ return;
+ }
+ }
+ }
+ }
+
pub fn update_diagnostic_summary(
&mut self,
path: Arc<Path>,
@@ -863,6 +1061,44 @@ impl RemoteWorktree {
.insert(PathKey(path.clone()), summary);
}
}
+
+ pub fn insert_entry(
+ &self,
+ entry: proto::Entry,
+ scan_id: usize,
+ cx: &mut ModelContext<Worktree>,
+ ) -> Task<Result<Entry>> {
+ let wait_for_snapshot = self.wait_for_snapshot(scan_id);
+ cx.spawn(|this, mut cx| async move {
+ wait_for_snapshot.await;
+ this.update(&mut cx, |worktree, _| {
+ let worktree = worktree.as_remote_mut().unwrap();
+ let mut snapshot = worktree.background_snapshot.lock();
+ let entry = snapshot.insert_entry(entry);
+ worktree.snapshot = snapshot.clone();
+ entry
+ })
+ })
+ }
+
+ pub(crate) fn delete_entry(
+ &self,
+ id: ProjectEntryId,
+ scan_id: usize,
+ cx: &mut ModelContext<Worktree>,
+ ) -> Task<Result<()>> {
+ let wait_for_snapshot = self.wait_for_snapshot(scan_id);
+ cx.spawn(|this, mut cx| async move {
+ wait_for_snapshot.await;
+ this.update(&mut cx, |worktree, _| {
+ let worktree = worktree.as_remote_mut().unwrap();
+ let mut snapshot = worktree.background_snapshot.lock();
+ snapshot.delete_entry(id);
+ worktree.snapshot = snapshot.clone();
+ });
+ Ok(())
+ })
+ }
}
impl Snapshot {
@@ -874,6 +1110,33 @@ impl Snapshot {
self.entries_by_id.get(&entry_id, &()).is_some()
}
+ pub(crate) fn insert_entry(&mut self, entry: proto::Entry) -> Result<Entry> {
+ let entry = Entry::try_from((&self.root_char_bag, entry))?;
+ let old_entry = self.entries_by_id.insert_or_replace(
+ PathEntry {
+ id: entry.id,
+ path: entry.path.clone(),
+ is_ignored: entry.is_ignored,
+ scan_id: 0,
+ },
+ &(),
+ );
+ if let Some(old_entry) = old_entry {
+ self.entries_by_path.remove(&PathKey(old_entry.path), &());
+ }
+ self.entries_by_path.insert_or_replace(entry.clone(), &());
+ Ok(entry)
+ }
+
+ fn delete_entry(&mut self, entry_id: ProjectEntryId) -> bool {
+ if let Some(entry) = self.entries_by_id.remove(&entry_id, &()) {
+ self.entries_by_path.remove(&PathKey(entry.path), &());
+ true
+ } else {
+ false
+ }
+ }
+
pub(crate) fn apply_remote_update(&mut self, update: proto::UpdateWorktree) -> Result<()> {
let mut entries_by_path_edits = Vec::new();
let mut entries_by_id_edits = Vec::new();
@@ -901,6 +1164,7 @@ impl Snapshot {
self.entries_by_path.edit(entries_by_path_edits, &());
self.entries_by_id.edit(entries_by_id_edits, &());
+ self.scan_id = update.scan_id as usize;
Ok(())
}
@@ -989,6 +1253,10 @@ impl Snapshot {
&self.root_name
}
+ pub fn scan_id(&self) -> usize {
+ self.scan_id
+ }
+
pub fn entry_for_path(&self, path: impl AsRef<Path>) -> Option<&Entry> {
let path = path.as_ref();
self.traverse_from_path(true, true, path)
@@ -1038,6 +1306,7 @@ impl LocalSnapshot {
.map(|(path, summary)| summary.to_proto(&path.0))
.collect(),
visible,
+ scan_id: self.scan_id as u64,
}
}
@@ -1103,6 +1372,7 @@ impl LocalSnapshot {
root_name: self.root_name().to_string(),
updated_entries,
removed_entries,
+ scan_id: self.scan_id as u64,
}
}
@@ -1146,11 +1416,18 @@ impl LocalSnapshot {
entries: impl IntoIterator<Item = Entry>,
ignore: Option<Arc<Gitignore>>,
) {
- let mut parent_entry = self
- .entries_by_path
- .get(&PathKey(parent_path.clone()), &())
- .unwrap()
- .clone();
+ let mut parent_entry = if let Some(parent_entry) =
+ self.entries_by_path.get(&PathKey(parent_path.clone()), &())
+ {
+ parent_entry.clone()
+ } else {
+ log::warn!(
+ "populating a directory {:?} that has been removed",
+ parent_path
+ );
+ return;
+ };
+
if let Some(ignore) = ignore {
self.ignores.insert(parent_path, (ignore, self.scan_id));
}
@@ -1210,7 +1487,7 @@ impl LocalSnapshot {
if path.file_name() == Some(&GITIGNORE) {
if let Some((_, scan_id)) = self.ignores.get_mut(path.parent().unwrap()) {
- *scan_id = self.scan_id;
+ *scan_id = self.snapshot.scan_id;
}
}
}
@@ -1397,7 +1674,7 @@ impl language::File for File {
Worktree::Local(worktree) => {
let rpc = worktree.client.clone();
let project_id = worktree.share.as_ref().map(|share| share.project_id);
- let save = worktree.save(self.path.clone(), text, cx);
+ let save = worktree.write_file(self.path.clone(), text, cx);
cx.background().spawn(async move {
let entry = save.await?;
if let Some(project_id) = project_id {
@@ -1536,7 +1813,7 @@ pub struct Entry {
pub is_ignored: bool,
}
-#[derive(Clone, Debug, PartialEq, Eq)]
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum EntryKind {
PendingDir,
Dir,
@@ -1777,14 +2054,14 @@ impl BackgroundScanner {
let path: Arc<Path> = Arc::from(Path::new(""));
let abs_path = self.abs_path();
let (tx, rx) = channel::unbounded();
- tx.send(ScanJob {
- abs_path: abs_path.to_path_buf(),
- path,
- ignore_stack: IgnoreStack::none(),
- scan_queue: tx.clone(),
- })
- .await
- .unwrap();
+ self.executor
+ .block(tx.send(ScanJob {
+ abs_path: abs_path.to_path_buf(),
+ path,
+ ignore_stack: IgnoreStack::none(),
+ scan_queue: tx.clone(),
+ }))
+ .unwrap();
drop(tx);
self.executor
@@ -1907,83 +2184,91 @@ impl BackgroundScanner {
}
async fn process_events(&mut self, mut events: Vec<fsevent::Event>) -> bool {
- let mut snapshot = self.snapshot();
- snapshot.scan_id += 1;
+ events.sort_unstable_by(|a, b| a.path.cmp(&b.path));
+ events.dedup_by(|a, b| a.path.starts_with(&b.path));
- let root_abs_path = if let Ok(abs_path) = self.fs.canonicalize(&snapshot.abs_path).await {
+ let root_char_bag;
+ let root_abs_path;
+ let next_entry_id;
+ {
+ let mut snapshot = self.snapshot.lock();
+ snapshot.scan_id += 1;
+ root_char_bag = snapshot.root_char_bag;
+ root_abs_path = snapshot.abs_path.clone();
+ next_entry_id = snapshot.next_entry_id.clone();
+ }
+
+ let root_abs_path = if let Ok(abs_path) = self.fs.canonicalize(&root_abs_path).await {
abs_path
} else {
return false;
};
- let root_char_bag = snapshot.root_char_bag;
- let next_entry_id = snapshot.next_entry_id.clone();
-
- events.sort_unstable_by(|a, b| a.path.cmp(&b.path));
- events.dedup_by(|a, b| a.path.starts_with(&b.path));
+ let metadata = futures::future::join_all(
+ events
+ .iter()
+ .map(|event| self.fs.metadata(&event.path))
+ .collect::<Vec<_>>(),
+ )
+ .await;
- for event in &events {
- match event.path.strip_prefix(&root_abs_path) {
- Ok(path) => snapshot.remove_path(&path),
- Err(_) => {
- log::error!(
- "unexpected event {:?} for root path {:?}",
- event.path,
- root_abs_path
- );
- continue;
+ // Hold the snapshot lock while clearing and re-inserting the root entries
+ // for each event. This way, the snapshot is not observable to the foreground
+ // thread while this operation is in-progress.
+ let (scan_queue_tx, scan_queue_rx) = channel::unbounded();
+ {
+ let mut snapshot = self.snapshot.lock();
+ for event in &events {
+ if let Ok(path) = event.path.strip_prefix(&root_abs_path) {
+ snapshot.remove_path(&path);
}
}
- }
- let (scan_queue_tx, scan_queue_rx) = channel::unbounded();
- for event in events {
- let path: Arc<Path> = match event.path.strip_prefix(&root_abs_path) {
- Ok(path) => Arc::from(path.to_path_buf()),
- Err(_) => {
- log::error!(
- "unexpected event {:?} for root path {:?}",
- event.path,
- root_abs_path
- );
- continue;
- }
- };
+ for (event, metadata) in events.into_iter().zip(metadata.into_iter()) {
+ let path: Arc<Path> = match event.path.strip_prefix(&root_abs_path) {
+ Ok(path) => Arc::from(path.to_path_buf()),
+ Err(_) => {
+ log::error!(
+ "unexpected event {:?} for root path {:?}",
+ event.path,
+ root_abs_path
+ );
+ continue;
+ }
+ };
- match self.fs.metadata(&event.path).await {
- Ok(Some(metadata)) => {
- let ignore_stack = snapshot.ignore_stack_for_path(&path, metadata.is_dir);
- let mut fs_entry = Entry::new(
- path.clone(),
- &metadata,
- snapshot.next_entry_id.as_ref(),
- snapshot.root_char_bag,
- );
- fs_entry.is_ignored = ignore_stack.is_all();
- snapshot.insert_entry(fs_entry, self.fs.as_ref());
- if metadata.is_dir {
- scan_queue_tx
- .send(ScanJob {
- abs_path: event.path,
- path,
- ignore_stack,
- scan_queue: scan_queue_tx.clone(),
- })
- .await
- .unwrap();
+ match metadata {
+ Ok(Some(metadata)) => {
+ let ignore_stack = snapshot.ignore_stack_for_path(&path, metadata.is_dir);
+ let mut fs_entry = Entry::new(
+ path.clone(),
+ &metadata,
+ snapshot.next_entry_id.as_ref(),
+ snapshot.root_char_bag,
+ );
+ fs_entry.is_ignored = ignore_stack.is_all();
+ snapshot.insert_entry(fs_entry, self.fs.as_ref());
+ if metadata.is_dir {
+ self.executor
+ .block(scan_queue_tx.send(ScanJob {
+ abs_path: event.path,
+ path,
+ ignore_stack,
+ scan_queue: scan_queue_tx.clone(),
+ }))
+ .unwrap();
+ }
+ }
+ Ok(None) => {}
+ Err(err) => {
+ // TODO - create a special 'error' entry in the entries tree to mark this
+ log::error!("error reading file on event {:?}", err);
}
- }
- Ok(None) => {}
- Err(err) => {
- // TODO - create a special 'error' entry in the entries tree to mark this
- log::error!("error reading file on event {:?}", err);
}
}
+ drop(scan_queue_tx);
}
- *self.snapshot.lock() = snapshot;
-
// Scan any directories that were created as part of this event batch.
- drop(scan_queue_tx);
self.executor
.scoped(|scope| {
for _ in 0..self.executor.num_cpus() {
@@ -2107,30 +2392,6 @@ impl BackgroundScanner {
}
}
-async fn refresh_entry(
- fs: &dyn Fs,
- snapshot: &Mutex<LocalSnapshot>,
- path: Arc<Path>,
- abs_path: &Path,
-) -> Result<Entry> {
- let root_char_bag;
- let next_entry_id;
- {
- let snapshot = snapshot.lock();
- root_char_bag = snapshot.root_char_bag;
- next_entry_id = snapshot.next_entry_id.clone();
- }
- let entry = Entry::new(
- path,
- &fs.metadata(abs_path)
- .await?
- .ok_or_else(|| anyhow!("could not read saved file metadata"))?,
- &next_entry_id,
- root_char_bag,
- );
- Ok(snapshot.lock().insert_entry(entry, fs))
-}
-
fn char_bag_for_path(root_char_bag: CharBag, path: &Path) -> CharBag {
let mut result = root_char_bag;
result.extend(
@@ -2368,7 +2629,7 @@ impl<'a> From<&'a Entry> for proto::Entry {
Self {
id: entry.id.to_proto(),
is_dir: entry.is_dir(),
- path: entry.path.to_string_lossy().to_string(),
+ path: entry.path.as_os_str().as_bytes().to_vec(),
inode: entry.inode,
mtime: Some(entry.mtime.into()),
is_symlink: entry.is_symlink,
@@ -2386,10 +2647,14 @@ impl<'a> TryFrom<(&'a CharBag, proto::Entry)> for Entry {
EntryKind::Dir
} else {
let mut char_bag = root_char_bag.clone();
- char_bag.extend(entry.path.chars().map(|c| c.to_ascii_lowercase()));
+ char_bag.extend(
+ String::from_utf8_lossy(&entry.path)
+ .chars()
+ .map(|c| c.to_ascii_lowercase()),
+ );
EntryKind::File(char_bag)
};
- let path: Arc<Path> = Arc::from(Path::new(&entry.path));
+ let path: Arc<Path> = PathBuf::from(OsString::from_vec(entry.path)).into();
Ok(Entry {
id: ProjectEntryId::from_proto(entry.id),
kind,
@@ -2541,7 +2806,6 @@ mod tests {
let next_entry_id = Arc::new(AtomicUsize::new(0));
let mut initial_snapshot = LocalSnapshot {
abs_path: root_dir.path().into(),
- scan_id: 0,
removed_entry_ids: Default::default(),
ignores: Default::default(),
next_entry_id: next_entry_id.clone(),
@@ -2551,6 +2815,7 @@ mod tests {
entries_by_id: Default::default(),
root_name: Default::default(),
root_char_bag: Default::default(),
+ scan_id: 0,
},
};
initial_snapshot.insert_entry(
@@ -8,15 +8,19 @@ path = "src/project_panel.rs"
doctest = false
[dependencies]
+editor = { path = "../editor" }
gpui = { path = "../gpui" }
project = { path = "../project" }
settings = { path = "../settings" }
theme = { path = "../theme" }
util = { path = "../util" }
workspace = { path = "../workspace" }
+postage = { version = "0.4.1", features = ["futures-traits"] }
+futures = "0.3"
unicase = "2.6"
[dev-dependencies]
+editor = { path = "../editor", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
workspace = { path = "../workspace", features = ["test-support"] }
serde_json = { version = "1.0.64", features = ["preserve_order"] }
@@ -1,15 +1,18 @@
+use editor::{Cancel, Editor};
+use futures::stream::StreamExt;
use gpui::{
actions,
+ anyhow::{anyhow, Result},
elements::{
- Align, ConstrainedBox, Empty, Flex, Label, MouseEventHandler, ParentElement, ScrollTarget,
- Svg, UniformList, UniformListState,
+ ChildView, ConstrainedBox, Empty, Flex, Label, MouseEventHandler, ParentElement,
+ ScrollTarget, Svg, UniformList, UniformListState,
},
impl_internal_actions, keymap,
platform::CursorStyle,
- AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, View, ViewContext,
- ViewHandle, WeakViewHandle,
+ AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, PromptLevel, Task,
+ View, ViewContext, ViewHandle, WeakViewHandle,
};
-use project::{Entry, Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
+use project::{Entry, EntryKind, Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
use settings::Settings;
use std::{
cmp::Ordering,
@@ -19,16 +22,20 @@ use std::{
};
use unicase::UniCase;
use workspace::{
- menu::{SelectNext, SelectPrev},
+ menu::{Confirm, SelectNext, SelectPrev},
Workspace,
};
+const NEW_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX;
+
pub struct ProjectPanel {
project: ModelHandle<Project>,
list: UniformListState,
visible_entries: Vec<(WorktreeId, Vec<Entry>)>,
expanded_dir_ids: HashMap<WorktreeId, Vec<ProjectEntryId>>,
selection: Option<Selection>,
+ edit_state: Option<EditState>,
+ filename_editor: ViewHandle<Editor>,
handle: WeakViewHandle<Self>,
}
@@ -38,22 +45,46 @@ struct Selection {
entry_id: ProjectEntryId,
}
+#[derive(Clone, Debug)]
+struct EditState {
+ worktree_id: WorktreeId,
+ entry_id: ProjectEntryId,
+ is_new_entry: bool,
+ is_dir: bool,
+ processing_filename: Option<String>,
+}
+
#[derive(Debug, PartialEq, Eq)]
struct EntryDetails {
filename: String,
depth: usize,
- is_dir: bool,
+ kind: EntryKind,
is_expanded: bool,
is_selected: bool,
+ is_editing: bool,
+ is_processing: bool,
}
#[derive(Clone)]
pub struct ToggleExpanded(pub ProjectEntryId);
#[derive(Clone)]
-pub struct Open(pub ProjectEntryId);
+pub struct Open {
+ pub entry_id: ProjectEntryId,
+ pub change_focus: bool,
+}
-actions!(project_panel, [ExpandSelectedEntry, CollapseSelectedEntry]);
+actions!(
+ project_panel,
+ [
+ ExpandSelectedEntry,
+ CollapseSelectedEntry,
+ AddDirectory,
+ AddFile,
+ Delete,
+ Rename
+ ]
+);
impl_internal_actions!(project_panel, [Open, ToggleExpanded]);
pub fn init(cx: &mut MutableAppContext) {
@@ -63,10 +94,19 @@ pub fn init(cx: &mut MutableAppContext) {
cx.add_action(ProjectPanel::select_prev);
cx.add_action(ProjectPanel::select_next);
cx.add_action(ProjectPanel::open_entry);
+ cx.add_action(ProjectPanel::add_file);
+ cx.add_action(ProjectPanel::add_directory);
+ cx.add_action(ProjectPanel::rename);
+ cx.add_async_action(ProjectPanel::delete);
+ cx.add_async_action(ProjectPanel::confirm);
+ cx.add_action(ProjectPanel::cancel);
}
pub enum Event {
- OpenedEntry(ProjectEntryId),
+ OpenedEntry {
+ entry_id: ProjectEntryId,
+ focus_opened_item: bool,
+ },
}
impl ProjectPanel {
@@ -96,30 +136,55 @@ impl ProjectPanel {
})
.detach();
+ let filename_editor = cx.add_view(|cx| {
+ Editor::single_line(
+ Some(|theme| {
+ let mut style = theme.project_panel.filename_editor.clone();
+ style.container.background_color.take();
+ style
+ }),
+ cx,
+ )
+ });
+
let mut this = Self {
project: project.clone(),
list: Default::default(),
visible_entries: Default::default(),
expanded_dir_ids: Default::default(),
selection: None,
+ edit_state: None,
+ filename_editor,
handle: cx.weak_handle(),
};
this.update_visible_entries(None, cx);
this
});
- cx.subscribe(&project_panel, move |workspace, _, event, cx| match event {
- &Event::OpenedEntry(entry_id) => {
- if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) {
- if let Some(entry) = worktree.read(cx).entry_for_id(entry_id) {
- workspace
- .open_path(
- ProjectPath {
- worktree_id: worktree.read(cx).id(),
- path: entry.path.clone(),
- },
- cx,
- )
- .detach_and_log_err(cx);
+ cx.subscribe(&project_panel, {
+ let project_panel = project_panel.downgrade();
+ move |workspace, _, event, cx| match event {
+ &Event::OpenedEntry {
+ entry_id,
+ focus_opened_item,
+ } => {
+ if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) {
+ if let Some(entry) = worktree.read(cx).entry_for_id(entry_id) {
+ workspace
+ .open_path(
+ ProjectPath {
+ worktree_id: worktree.read(cx).id(),
+ path: entry.path.clone(),
+ },
+ focus_opened_item,
+ cx,
+ )
+ .detach_and_log_err(cx);
+ if !focus_opened_item {
+ if let Some(project_panel) = project_panel.upgrade(cx) {
+ cx.focus(&project_panel);
+ }
+ }
+ }
}
}
}
@@ -148,7 +213,10 @@ impl ProjectPanel {
}
}
} else {
- let event = Event::OpenedEntry(entry.id);
+ let event = Event::OpenedEntry {
+ entry_id: entry.id,
+ focus_opened_item: true,
+ };
cx.emit(event);
}
}
@@ -230,8 +298,193 @@ impl ProjectPanel {
}
}
+ fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
+ let edit_state = self.edit_state.as_mut()?;
+ cx.focus_self();
+
+ let worktree_id = edit_state.worktree_id;
+ let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?;
+ let entry = worktree.read(cx).entry_for_id(edit_state.entry_id)?.clone();
+ let filename = self.filename_editor.read(cx).text(cx);
+
+ let edit_task;
+ let edited_entry_id;
+
+ if edit_state.is_new_entry {
+ self.selection = Some(Selection {
+ worktree_id,
+ entry_id: NEW_ENTRY_ID,
+ });
+ let new_path = entry.path.join(&filename);
+ edited_entry_id = NEW_ENTRY_ID;
+ edit_task = self.project.update(cx, |project, cx| {
+ project.create_entry((edit_state.worktree_id, new_path), edit_state.is_dir, cx)
+ })?;
+ } else {
+ let new_path = if let Some(parent) = entry.path.clone().parent() {
+ parent.join(&filename)
+ } else {
+ filename.clone().into()
+ };
+ edited_entry_id = entry.id;
+ edit_task = self.project.update(cx, |project, cx| {
+ project.rename_entry(entry.id, new_path, cx)
+ })?;
+ };
+
+ edit_state.processing_filename = Some(filename);
+ cx.notify();
+
+ Some(cx.spawn(|this, mut cx| async move {
+ let new_entry = edit_task.await;
+ this.update(&mut cx, |this, cx| {
+ this.edit_state.take();
+ cx.notify();
+ });
+
+ let new_entry = new_entry?;
+ this.update(&mut cx, |this, cx| {
+ if let Some(selection) = &mut this.selection {
+ if selection.entry_id == edited_entry_id {
+ selection.worktree_id = worktree_id;
+ selection.entry_id = new_entry.id;
+ }
+ }
+ this.update_visible_entries(None, cx);
+ cx.notify();
+ });
+ Ok(())
+ }))
+ }
+
+ fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
+ self.edit_state = None;
+ self.update_visible_entries(None, cx);
+ cx.focus_self();
+ cx.notify();
+ }
+
fn open_entry(&mut self, action: &Open, cx: &mut ViewContext<Self>) {
- cx.emit(Event::OpenedEntry(action.0));
+ cx.emit(Event::OpenedEntry {
+ entry_id: action.entry_id,
+ focus_opened_item: action.change_focus,
+ });
+ }
+
+ fn add_file(&mut self, _: &AddFile, cx: &mut ViewContext<Self>) {
+ self.add_entry(false, cx)
+ }
+
+ fn add_directory(&mut self, _: &AddDirectory, cx: &mut ViewContext<Self>) {
+ self.add_entry(true, cx)
+ }
+
+ fn add_entry(&mut self, is_dir: bool, cx: &mut ViewContext<Self>) {
+ if let Some(Selection {
+ worktree_id,
+ entry_id,
+ }) = self.selection
+ {
+ let directory_id;
+ if let Some((worktree, expanded_dir_ids)) = self
+ .project
+ .read(cx)
+ .worktree_for_id(worktree_id, cx)
+ .zip(self.expanded_dir_ids.get_mut(&worktree_id))
+ {
+ let worktree = worktree.read(cx);
+ if let Some(mut entry) = worktree.entry_for_id(entry_id) {
+ loop {
+ if entry.is_dir() {
+ if let Err(ix) = expanded_dir_ids.binary_search(&entry.id) {
+ expanded_dir_ids.insert(ix, entry.id);
+ }
+ directory_id = entry.id;
+ break;
+ } else {
+ if let Some(parent_path) = entry.path.parent() {
+ if let Some(parent_entry) = worktree.entry_for_path(parent_path) {
+ entry = parent_entry;
+ continue;
+ }
+ }
+ return;
+ }
+ }
+ } else {
+ return;
+ };
+ } else {
+ return;
+ };
+
+ self.edit_state = Some(EditState {
+ worktree_id,
+ entry_id: directory_id,
+ is_new_entry: true,
+ is_dir,
+ processing_filename: None,
+ });
+ self.filename_editor
+ .update(cx, |editor, cx| editor.clear(cx));
+ cx.focus(&self.filename_editor);
+ self.update_visible_entries(Some((worktree_id, NEW_ENTRY_ID)), cx);
+ cx.notify();
+ }
+ }
+
+ fn rename(&mut self, _: &Rename, cx: &mut ViewContext<Self>) {
+ if let Some(Selection {
+ worktree_id,
+ entry_id,
+ }) = self.selection
+ {
+ if let Some(worktree) = self.project.read(cx).worktree_for_id(worktree_id, cx) {
+ if let Some(entry) = worktree.read(cx).entry_for_id(entry_id) {
+ self.edit_state = Some(EditState {
+ worktree_id,
+ entry_id,
+ is_new_entry: false,
+ is_dir: entry.is_dir(),
+ processing_filename: None,
+ });
+ let filename = entry
+ .path
+ .file_name()
+ .map_or(String::new(), |s| s.to_string_lossy().to_string());
+ self.filename_editor.update(cx, |editor, cx| {
+ editor.set_text(filename, cx);
+ editor.select_all(&Default::default(), cx);
+ });
+ cx.focus(&self.filename_editor);
+ self.update_visible_entries(None, cx);
+ cx.notify();
+ }
+ }
+ }
+ }
+
+ fn delete(&mut self, _: &Delete, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
+ let Selection { entry_id, .. } = self.selection?;
+ let path = self.project.read(cx).path_for_entry(entry_id, cx)?.path;
+ let file_name = path.file_name()?;
+
+ let mut answer = cx.prompt(
+ PromptLevel::Info,
+ &format!("Delete {file_name:?}?"),
+ &["Delete", "Cancel"],
+ );
+ Some(cx.spawn(|this, mut cx| async move {
+ if answer.next().await != Some(0) {
+ return Ok(());
+ }
+ this.update(&mut cx, |this, cx| {
+ this.project
+ .update(cx, |project, cx| project.delete_entry(entry_id, cx))
+ .ok_or_else(|| anyhow!("no such entry"))
+ })?
+ .await
+ }))
}
fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
@@ -346,11 +599,35 @@ impl ProjectPanel {
}
};
+ let mut new_entry_parent_id = None;
+ let mut new_entry_kind = EntryKind::Dir;
+ if let Some(edit_state) = &self.edit_state {
+ if edit_state.worktree_id == worktree_id && edit_state.is_new_entry {
+ new_entry_parent_id = Some(edit_state.entry_id);
+ new_entry_kind = if edit_state.is_dir {
+ EntryKind::Dir
+ } else {
+ EntryKind::File(Default::default())
+ };
+ }
+ }
+
let mut visible_worktree_entries = Vec::new();
let mut entry_iter = snapshot.entries(false);
- while let Some(item) = entry_iter.entry() {
- visible_worktree_entries.push(item.clone());
- if expanded_dir_ids.binary_search(&item.id).is_err() {
+ while let Some(entry) = entry_iter.entry() {
+ visible_worktree_entries.push(entry.clone());
+ if Some(entry.id) == new_entry_parent_id {
+ visible_worktree_entries.push(Entry {
+ id: NEW_ENTRY_ID,
+ kind: new_entry_kind,
+ path: entry.path.join("\0").into(),
+ inode: 0,
+ mtime: entry.mtime,
+ is_symlink: false,
+ is_ignored: false,
+ });
+ }
+ if expanded_dir_ids.binary_search(&entry.id).is_err() {
if entry_iter.advance_to_sibling() {
continue;
}
@@ -436,6 +713,7 @@ impl ProjectPanel {
if ix >= range.end {
return;
}
+
if ix + visible_worktree_entries.len() <= range.start {
ix += visible_worktree_entries.len();
continue;
@@ -452,16 +730,42 @@ impl ProjectPanel {
let root_name = OsStr::new(snapshot.root_name());
for entry in &visible_worktree_entries[range.start.saturating_sub(ix)..end_ix - ix]
{
- let filename = entry.path.file_name().unwrap_or(root_name);
- let details = EntryDetails {
- filename: filename.to_string_lossy().to_string(),
+ let mut details = EntryDetails {
+ filename: entry
+ .path
+ .file_name()
+ .unwrap_or(root_name)
+ .to_string_lossy()
+ .to_string(),
depth: entry.path.components().count(),
- is_dir: entry.is_dir(),
+ kind: entry.kind,
is_expanded: expanded_entry_ids.binary_search(&entry.id).is_ok(),
is_selected: self.selection.map_or(false, |e| {
e.worktree_id == snapshot.id() && e.entry_id == entry.id
}),
+ is_editing: false,
+ is_processing: false,
};
+ if let Some(edit_state) = &self.edit_state {
+ let is_edited_entry = if edit_state.is_new_entry {
+ entry.id == NEW_ENTRY_ID
+ } else {
+ entry.id == edit_state.entry_id
+ };
+ if is_edited_entry {
+ if let Some(processing_filename) = &edit_state.processing_filename {
+ details.is_processing = true;
+ details.filename.clear();
+ details.filename.push_str(&processing_filename);
+ } else {
+ if edit_state.is_new_entry {
+ details.filename.clear();
+ }
+ details.is_editing = true;
+ }
+ }
+ }
+
callback(entry.id, details, cx);
}
}
@@ -472,63 +776,73 @@ impl ProjectPanel {
fn render_entry(
entry_id: ProjectEntryId,
details: EntryDetails,
+ editor: &ViewHandle<Editor>,
theme: &theme::ProjectPanel,
cx: &mut ViewContext<Self>,
) -> ElementBox {
- let is_dir = details.is_dir;
+ let kind = details.kind;
+ let show_editor = details.is_editing && !details.is_processing;
MouseEventHandler::new::<Self, _, _>(entry_id.to_usize(), cx, |state, _| {
- let style = match (details.is_selected, state.hovered) {
- (false, false) => &theme.entry,
- (false, true) => &theme.hovered_entry,
- (true, false) => &theme.selected_entry,
- (true, true) => &theme.hovered_selected_entry,
+ let padding = theme.container.padding.left + details.depth as f32 * theme.indent_width;
+ let style = theme.entry.style_for(state, details.is_selected);
+ let row_container_style = if show_editor {
+ theme.filename_editor.container
+ } else {
+ style.container
};
Flex::row()
.with_child(
- ConstrainedBox::new(
- Align::new(
- ConstrainedBox::new(if is_dir {
- if details.is_expanded {
- Svg::new("icons/disclosure-open.svg")
- .with_color(style.icon_color)
- .boxed()
- } else {
- Svg::new("icons/disclosure-closed.svg")
- .with_color(style.icon_color)
- .boxed()
- }
- } else {
- Empty::new().boxed()
- })
- .with_max_width(style.icon_size)
- .with_max_height(style.icon_size)
- .boxed(),
- )
- .boxed(),
- )
+ ConstrainedBox::new(if kind == EntryKind::Dir {
+ if details.is_expanded {
+ Svg::new("icons/disclosure-open.svg")
+ .with_color(style.icon_color)
+ .boxed()
+ } else {
+ Svg::new("icons/disclosure-closed.svg")
+ .with_color(style.icon_color)
+ .boxed()
+ }
+ } else {
+ Empty::new().boxed()
+ })
+ .with_max_width(style.icon_size)
+ .with_max_height(style.icon_size)
+ .aligned()
+ .constrained()
.with_width(style.icon_size)
.boxed(),
)
- .with_child(
+ .with_child(if show_editor {
+ ChildView::new(editor.clone())
+ .contained()
+ .with_margin_left(theme.entry.default.icon_spacing)
+ .aligned()
+ .left()
+ .flex(1.0, true)
+ .boxed()
+ } else {
Label::new(details.filename, style.text.clone())
.contained()
.with_margin_left(style.icon_spacing)
.aligned()
.left()
- .boxed(),
- )
+ .boxed()
+ })
.constrained()
- .with_height(theme.entry.height)
+ .with_height(theme.entry.default.height)
.contained()
- .with_style(style.container)
- .with_padding_left(theme.container.padding.left + details.depth as f32 * 20.)
+ .with_style(row_container_style)
+ .with_padding_left(padding)
.boxed()
})
- .on_click(move |cx| {
- if is_dir {
+ .on_click(move |click_count, cx| {
+ if kind == EntryKind::Dir {
cx.dispatch_action(ToggleExpanded(entry_id))
} else {
- cx.dispatch_action(Open(entry_id))
+ cx.dispatch_action(Open {
+ entry_id,
+ change_focus: click_count > 1,
+ })
}
})
.with_cursor_style(CursorStyle::PointingHand)
@@ -556,8 +870,14 @@ impl View for ProjectPanel {
let theme = cx.global::<Settings>().theme.clone();
let this = handle.upgrade(cx).unwrap();
this.update(cx.app, |this, cx| {
- this.for_each_visible_entry(range.clone(), cx, |entry, details, cx| {
- items.push(Self::render_entry(entry, details, &theme.project_panel, cx));
+ this.for_each_visible_entry(range.clone(), cx, |id, details, cx| {
+ items.push(Self::render_entry(
+ id,
+ details,
+ &this.filename_editor,
+ &theme.project_panel,
+ cx,
+ ));
});
})
},
@@ -584,6 +904,7 @@ impl Entity for ProjectPanel {
mod tests {
use super::*;
use gpui::{TestAppContext, ViewHandle};
+ use project::FakeFs;
use serde_json::json;
use std::{collections::HashSet, path::Path};
use workspace::WorkspaceParams;
@@ -592,8 +913,7 @@ mod tests {
async fn test_visible_list(cx: &mut gpui::TestAppContext) {
cx.foreground().forbid_parking();
- let params = cx.update(WorkspaceParams::test);
- let fs = params.fs.as_fake();
+ let fs = FakeFs::new(cx.background());
fs.insert_tree(
"/root1",
json!({
@@ -630,241 +950,408 @@ mod tests {
)
.await;
- let project = cx.update(|cx| {
- Project::local(
- params.client.clone(),
- params.user_store.clone(),
- params.languages.clone(),
- params.fs.clone(),
- cx,
- )
- });
- let (root1, _) = project
- .update(cx, |project, cx| {
- project.find_or_create_local_worktree("/root1", true, cx)
- })
- .await
- .unwrap();
- root1
- .read_with(cx, |t, _| t.as_local().unwrap().scan_complete())
- .await;
- let (root2, _) = project
- .update(cx, |project, cx| {
- project.find_or_create_local_worktree("/root2", true, cx)
- })
- .await
- .unwrap();
- root2
- .read_with(cx, |t, _| t.as_local().unwrap().scan_complete())
- .await;
-
+ let project = Project::test(fs.clone(), ["/root1", "/root2"], cx).await;
+ let params = cx.update(WorkspaceParams::test);
let (_, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx));
let panel = workspace.update(cx, |_, cx| ProjectPanel::new(project, cx));
assert_eq!(
- visible_entry_details(&panel, 0..50, cx),
+ visible_entries_as_strings(&panel, 0..50, cx),
&[
- EntryDetails {
- filename: "root1".to_string(),
- depth: 0,
- is_dir: true,
- is_expanded: true,
- is_selected: false,
- },
- EntryDetails {
- filename: "a".to_string(),
- depth: 1,
- is_dir: true,
- is_expanded: false,
- is_selected: false,
- },
- EntryDetails {
- filename: "b".to_string(),
- depth: 1,
- is_dir: true,
- is_expanded: false,
- is_selected: false,
- },
- EntryDetails {
- filename: "C".to_string(),
- depth: 1,
- is_dir: true,
- is_expanded: false,
- is_selected: false,
- },
- EntryDetails {
- filename: ".dockerignore".to_string(),
- depth: 1,
- is_dir: false,
- is_expanded: false,
- is_selected: false,
- },
- EntryDetails {
- filename: "root2".to_string(),
- depth: 0,
- is_dir: true,
- is_expanded: true,
- is_selected: false
- },
- EntryDetails {
- filename: "d".to_string(),
- depth: 1,
- is_dir: true,
- is_expanded: false,
- is_selected: false
- },
- EntryDetails {
- filename: "e".to_string(),
- depth: 1,
- is_dir: true,
- is_expanded: false,
- is_selected: false
- }
- ],
+ "v root1",
+ " > a",
+ " > b",
+ " > C",
+ " .dockerignore",
+ "v root2",
+ " > d",
+ " > e",
+ ]
);
toggle_expand_dir(&panel, "root1/b", cx);
assert_eq!(
- visible_entry_details(&panel, 0..50, cx),
+ visible_entries_as_strings(&panel, 0..50, cx),
&[
- EntryDetails {
- filename: "root1".to_string(),
- depth: 0,
- is_dir: true,
- is_expanded: true,
- is_selected: false,
- },
- EntryDetails {
- filename: "a".to_string(),
- depth: 1,
- is_dir: true,
- is_expanded: false,
- is_selected: false,
- },
- EntryDetails {
- filename: "b".to_string(),
- depth: 1,
- is_dir: true,
- is_expanded: true,
- is_selected: true,
- },
- EntryDetails {
- filename: "3".to_string(),
- depth: 2,
- is_dir: true,
- is_expanded: false,
- is_selected: false,
- },
- EntryDetails {
- filename: "4".to_string(),
- depth: 2,
- is_dir: true,
- is_expanded: false,
- is_selected: false,
- },
- EntryDetails {
- filename: "C".to_string(),
- depth: 1,
- is_dir: true,
- is_expanded: false,
- is_selected: false,
- },
- EntryDetails {
- filename: ".dockerignore".to_string(),
- depth: 1,
- is_dir: false,
- is_expanded: false,
- is_selected: false,
+ "v root1",
+ " > a",
+ " v b <== selected",
+ " > 3",
+ " > 4",
+ " > C",
+ " .dockerignore",
+ "v root2",
+ " > d",
+ " > e",
+ ]
+ );
+
+ assert_eq!(
+ visible_entries_as_strings(&panel, 5..8, cx),
+ &[
+ //
+ " > C",
+ " .dockerignore",
+ "v root2",
+ ]
+ );
+ }
+
+ #[gpui::test(iterations = 30)]
+ async fn test_editing_files(cx: &mut gpui::TestAppContext) {
+ cx.foreground().forbid_parking();
+
+ let fs = FakeFs::new(cx.background());
+ fs.insert_tree(
+ "/root1",
+ json!({
+ ".dockerignore": "",
+ ".git": {
+ "HEAD": "",
},
- EntryDetails {
- filename: "root2".to_string(),
- depth: 0,
- is_dir: true,
- is_expanded: true,
- is_selected: false
+ "a": {
+ "0": { "q": "", "r": "", "s": "" },
+ "1": { "t": "", "u": "" },
+ "2": { "v": "", "w": "", "x": "", "y": "" },
},
- EntryDetails {
- filename: "d".to_string(),
- depth: 1,
- is_dir: true,
- is_expanded: false,
- is_selected: false
+ "b": {
+ "3": { "Q": "" },
+ "4": { "R": "", "S": "", "T": "", "U": "" },
},
- EntryDetails {
- filename: "e".to_string(),
- depth: 1,
- is_dir: true,
- is_expanded: false,
- is_selected: false
+ "C": {
+ "5": {},
+ "6": { "V": "", "W": "" },
+ "7": { "X": "" },
+ "8": { "Y": {}, "Z": "" }
}
+ }),
+ )
+ .await;
+ fs.insert_tree(
+ "/root2",
+ json!({
+ "d": {
+ "9": ""
+ },
+ "e": {}
+ }),
+ )
+ .await;
+
+ let project = Project::test(fs.clone(), ["/root1", "/root2"], cx).await;
+ let params = cx.update(WorkspaceParams::test);
+ let (_, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx));
+ let panel = workspace.update(cx, |_, cx| ProjectPanel::new(project, cx));
+
+ select_path(&panel, "root1", cx);
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &[
+ "v root1 <== selected",
+ " > a",
+ " > b",
+ " > C",
+ " .dockerignore",
+ "v root2",
+ " > d",
+ " > e",
]
);
+ // Add a file with the root folder selected. The filename editor is placed
+ // before the first file in the root folder.
+ panel.update(cx, |panel, cx| panel.add_file(&AddFile, cx));
+ assert!(panel.read_with(cx, |panel, cx| panel.filename_editor.is_focused(cx)));
assert_eq!(
- visible_entry_details(&panel, 5..8, cx),
- [
- EntryDetails {
- filename: "C".to_string(),
- depth: 1,
- is_dir: true,
- is_expanded: false,
- is_selected: false
- },
- EntryDetails {
- filename: ".dockerignore".to_string(),
- depth: 1,
- is_dir: false,
- is_expanded: false,
- is_selected: false
- },
- EntryDetails {
- filename: "root2".to_string(),
- depth: 0,
- is_dir: true,
- is_expanded: true,
- is_selected: false
- }
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &[
+ "v root1",
+ " > a",
+ " > b",
+ " > C",
+ " [EDITOR: ''] <== selected",
+ " .dockerignore",
+ "v root2",
+ " > d",
+ " > e",
]
);
- fn toggle_expand_dir(
- panel: &ViewHandle<ProjectPanel>,
- path: impl AsRef<Path>,
- cx: &mut TestAppContext,
- ) {
- let path = path.as_ref();
- panel.update(cx, |panel, cx| {
- for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
- let worktree = worktree.read(cx);
- if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
- let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
- panel.toggle_expanded(&ToggleExpanded(entry_id), cx);
- return;
- }
+ let confirm = panel.update(cx, |panel, cx| {
+ panel
+ .filename_editor
+ .update(cx, |editor, cx| editor.set_text("the-new-filename", cx));
+ panel.confirm(&Confirm, cx).unwrap()
+ });
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &[
+ "v root1",
+ " > a",
+ " > b",
+ " > C",
+ " [PROCESSING: 'the-new-filename'] <== selected",
+ " .dockerignore",
+ "v root2",
+ " > d",
+ " > e",
+ ]
+ );
+
+ confirm.await.unwrap();
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &[
+ "v root1",
+ " > a",
+ " > b",
+ " > C",
+ " .dockerignore",
+ " the-new-filename <== selected",
+ "v root2",
+ " > d",
+ " > e",
+ ]
+ );
+
+ select_path(&panel, "root1/b", cx);
+ panel.update(cx, |panel, cx| panel.add_file(&AddFile, cx));
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..9, cx),
+ &[
+ "v root1",
+ " > a",
+ " v b",
+ " > 3",
+ " > 4",
+ " [EDITOR: ''] <== selected",
+ " > C",
+ " .dockerignore",
+ " the-new-filename",
+ ]
+ );
+
+ panel
+ .update(cx, |panel, cx| {
+ panel
+ .filename_editor
+ .update(cx, |editor, cx| editor.set_text("another-filename", cx));
+ panel.confirm(&Confirm, cx).unwrap()
+ })
+ .await
+ .unwrap();
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..9, cx),
+ &[
+ "v root1",
+ " > a",
+ " v b",
+ " > 3",
+ " > 4",
+ " another-filename <== selected",
+ " > C",
+ " .dockerignore",
+ " the-new-filename",
+ ]
+ );
+
+ select_path(&panel, "root1/b/another-filename", cx);
+ panel.update(cx, |panel, cx| panel.rename(&Rename, cx));
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..9, cx),
+ &[
+ "v root1",
+ " > a",
+ " v b",
+ " > 3",
+ " > 4",
+ " [EDITOR: 'another-filename'] <== selected",
+ " > C",
+ " .dockerignore",
+ " the-new-filename",
+ ]
+ );
+
+ let confirm = panel.update(cx, |panel, cx| {
+ panel
+ .filename_editor
+ .update(cx, |editor, cx| editor.set_text("a-different-filename", cx));
+ panel.confirm(&Confirm, cx).unwrap()
+ });
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..9, cx),
+ &[
+ "v root1",
+ " > a",
+ " v b",
+ " > 3",
+ " > 4",
+ " [PROCESSING: 'a-different-filename'] <== selected",
+ " > C",
+ " .dockerignore",
+ " the-new-filename",
+ ]
+ );
+
+ confirm.await.unwrap();
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..9, cx),
+ &[
+ "v root1",
+ " > a",
+ " v b",
+ " > 3",
+ " > 4",
+ " a-different-filename <== selected",
+ " > C",
+ " .dockerignore",
+ " the-new-filename",
+ ]
+ );
+
+ panel.update(cx, |panel, cx| panel.add_directory(&AddDirectory, cx));
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..9, cx),
+ &[
+ "v root1",
+ " > a",
+ " v b",
+ " > [EDITOR: ''] <== selected",
+ " > 3",
+ " > 4",
+ " a-different-filename",
+ " > C",
+ " .dockerignore",
+ ]
+ );
+
+ let confirm = panel.update(cx, |panel, cx| {
+ panel
+ .filename_editor
+ .update(cx, |editor, cx| editor.set_text("new-dir", cx));
+ panel.confirm(&Confirm, cx).unwrap()
+ });
+ panel.update(cx, |panel, cx| panel.select_next(&Default::default(), cx));
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..9, cx),
+ &[
+ "v root1",
+ " > a",
+ " v b",
+ " > [PROCESSING: 'new-dir']",
+ " > 3 <== selected",
+ " > 4",
+ " a-different-filename",
+ " > C",
+ " .dockerignore",
+ ]
+ );
+
+ confirm.await.unwrap();
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..9, cx),
+ &[
+ "v root1",
+ " > a",
+ " v b",
+ " > 3 <== selected",
+ " > 4",
+ " > new-dir",
+ " a-different-filename",
+ " > C",
+ " .dockerignore",
+ ]
+ );
+ }
+
+ fn toggle_expand_dir(
+ panel: &ViewHandle<ProjectPanel>,
+ path: impl AsRef<Path>,
+ cx: &mut TestAppContext,
+ ) {
+ let path = path.as_ref();
+ panel.update(cx, |panel, cx| {
+ for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
+ let worktree = worktree.read(cx);
+ if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
+ let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
+ panel.toggle_expanded(&ToggleExpanded(entry_id), cx);
+ return;
}
- panic!("no worktree for path {:?}", path);
- });
- }
+ }
+ panic!("no worktree for path {:?}", path);
+ });
+ }
+
+ fn select_path(
+ panel: &ViewHandle<ProjectPanel>,
+ path: impl AsRef<Path>,
+ cx: &mut TestAppContext,
+ ) {
+ let path = path.as_ref();
+ panel.update(cx, |panel, cx| {
+ for worktree in panel.project.read(cx).worktrees(cx).collect::<Vec<_>>() {
+ let worktree = worktree.read(cx);
+ if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
+ let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
+ panel.selection = Some(Selection {
+ worktree_id: worktree.id(),
+ entry_id,
+ });
+ return;
+ }
+ }
+ panic!("no worktree for path {:?}", path);
+ });
+ }
- fn visible_entry_details(
- panel: &ViewHandle<ProjectPanel>,
- range: Range<usize>,
- cx: &mut TestAppContext,
- ) -> Vec<EntryDetails> {
- let mut result = Vec::new();
- let mut project_entries = HashSet::new();
- panel.update(cx, |panel, cx| {
- panel.for_each_visible_entry(range, cx, |project_entry, details, _| {
+ fn visible_entries_as_strings(
+ panel: &ViewHandle<ProjectPanel>,
+ range: Range<usize>,
+ cx: &mut TestAppContext,
+ ) -> Vec<String> {
+ let mut result = Vec::new();
+ let mut project_entries = HashSet::new();
+ let mut has_editor = false;
+ panel.update(cx, |panel, cx| {
+ panel.for_each_visible_entry(range, cx, |project_entry, details, _| {
+ if details.is_editing {
+ assert!(!has_editor, "duplicate editor entry");
+ has_editor = true;
+ } else {
assert!(
project_entries.insert(project_entry),
"duplicate project entry {:?} {:?}",
project_entry,
details
);
- result.push(details);
- });
+ }
+
+ let indent = " ".repeat(details.depth);
+ let icon = if matches!(details.kind, EntryKind::Dir | EntryKind::PendingDir) {
+ if details.is_expanded {
+ "v "
+ } else {
+ "> "
+ }
+ } else {
+ " "
+ };
+ let name = if details.is_editing {
+ format!("[EDITOR: '{}']", details.filename)
+ } else if details.is_processing {
+ format!("[PROCESSING: '{}']", details.filename)
+ } else {
+ details.filename.clone()
+ };
+ let selected = if details.is_selected {
+ " <== selected"
+ } else {
+ ""
+ };
+ result.push(format!("{indent}{icon}{name}{selected}"));
});
+ });
- result
- }
+ result
}
}
@@ -297,23 +297,12 @@ mod tests {
let fs = FakeFs::new(cx.background());
fs.insert_tree("/dir", json!({ "test.rs": "" })).await;
- let project = Project::test(fs.clone(), cx);
- project.update(cx, |project, _| {
- project.languages().add(Arc::new(language));
- });
-
- let worktree_id = project
- .update(cx, |project, cx| {
- project.find_or_create_local_worktree("/dir", true, cx)
- })
- .await
- .unwrap()
- .0
- .read_with(cx, |tree, _| tree.id());
+ let project = Project::test(fs.clone(), ["/dir"], cx).await;
+ project.update(cx, |project, _| project.languages().add(Arc::new(language)));
let _buffer = project
.update(cx, |project, cx| {
- project.open_buffer((worktree_id, "test.rs"), cx)
+ project.open_local_buffer("/dir/test.rs", cx)
})
.await
.unwrap();
@@ -36,57 +36,63 @@ message Envelope {
RegisterWorktree register_worktree = 28;
UnregisterWorktree unregister_worktree = 29;
UpdateWorktree update_worktree = 31;
- UpdateDiagnosticSummary update_diagnostic_summary = 32;
- StartLanguageServer start_language_server = 33;
- UpdateLanguageServer update_language_server = 34;
-
- OpenBufferById open_buffer_by_id = 35;
- OpenBufferByPath open_buffer_by_path = 36;
- OpenBufferResponse open_buffer_response = 37;
- UpdateBuffer update_buffer = 38;
- UpdateBufferFile update_buffer_file = 39;
- SaveBuffer save_buffer = 40;
- BufferSaved buffer_saved = 41;
- BufferReloaded buffer_reloaded = 42;
- ReloadBuffers reload_buffers = 43;
- ReloadBuffersResponse reload_buffers_response = 44;
- FormatBuffers format_buffers = 45;
- FormatBuffersResponse format_buffers_response = 46;
- GetCompletions get_completions = 47;
- GetCompletionsResponse get_completions_response = 48;
- ApplyCompletionAdditionalEdits apply_completion_additional_edits = 49;
- ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 50;
- GetCodeActions get_code_actions = 51;
- GetCodeActionsResponse get_code_actions_response = 52;
- ApplyCodeAction apply_code_action = 53;
- ApplyCodeActionResponse apply_code_action_response = 54;
- PrepareRename prepare_rename = 55;
- PrepareRenameResponse prepare_rename_response = 56;
- PerformRename perform_rename = 57;
- PerformRenameResponse perform_rename_response = 58;
- SearchProject search_project = 59;
- SearchProjectResponse search_project_response = 60;
-
- GetChannels get_channels = 61;
- GetChannelsResponse get_channels_response = 62;
- JoinChannel join_channel = 63;
- JoinChannelResponse join_channel_response = 64;
- LeaveChannel leave_channel = 65;
- SendChannelMessage send_channel_message = 66;
- SendChannelMessageResponse send_channel_message_response = 67;
- ChannelMessageSent channel_message_sent = 68;
- GetChannelMessages get_channel_messages = 69;
- GetChannelMessagesResponse get_channel_messages_response = 70;
-
- UpdateContacts update_contacts = 71;
-
- GetUsers get_users = 72;
- GetUsersResponse get_users_response = 73;
-
- Follow follow = 74;
- FollowResponse follow_response = 75;
- UpdateFollowers update_followers = 76;
- Unfollow unfollow = 77;
+
+ CreateProjectEntry create_project_entry = 32;
+ RenameProjectEntry rename_project_entry = 33;
+ DeleteProjectEntry delete_project_entry = 34;
+ ProjectEntryResponse project_entry_response = 35;
+
+ UpdateDiagnosticSummary update_diagnostic_summary = 36;
+ StartLanguageServer start_language_server = 37;
+ UpdateLanguageServer update_language_server = 38;
+
+ OpenBufferById open_buffer_by_id = 39;
+ OpenBufferByPath open_buffer_by_path = 40;
+ OpenBufferResponse open_buffer_response = 41;
+ UpdateBuffer update_buffer = 42;
+ UpdateBufferFile update_buffer_file = 43;
+ SaveBuffer save_buffer = 44;
+ BufferSaved buffer_saved = 45;
+ BufferReloaded buffer_reloaded = 46;
+ ReloadBuffers reload_buffers = 47;
+ ReloadBuffersResponse reload_buffers_response = 48;
+ FormatBuffers format_buffers = 49;
+ FormatBuffersResponse format_buffers_response = 50;
+ GetCompletions get_completions = 51;
+ GetCompletionsResponse get_completions_response = 52;
+ ApplyCompletionAdditionalEdits apply_completion_additional_edits = 53;
+ ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 54;
+ GetCodeActions get_code_actions = 55;
+ GetCodeActionsResponse get_code_actions_response = 56;
+ ApplyCodeAction apply_code_action = 57;
+ ApplyCodeActionResponse apply_code_action_response = 58;
+ PrepareRename prepare_rename = 59;
+ PrepareRenameResponse prepare_rename_response = 60;
+ PerformRename perform_rename = 61;
+ PerformRenameResponse perform_rename_response = 62;
+ SearchProject search_project = 63;
+ SearchProjectResponse search_project_response = 64;
+
+ GetChannels get_channels = 65;
+ GetChannelsResponse get_channels_response = 66;
+ JoinChannel join_channel = 67;
+ JoinChannelResponse join_channel_response = 68;
+ LeaveChannel leave_channel = 69;
+ SendChannelMessage send_channel_message = 70;
+ SendChannelMessageResponse send_channel_message_response = 71;
+ ChannelMessageSent channel_message_sent = 72;
+ GetChannelMessages get_channel_messages = 73;
+ GetChannelMessagesResponse get_channel_messages_response = 74;
+
+ UpdateContacts update_contacts = 75;
+
+ GetUsers get_users = 76;
+ GetUsersResponse get_users_response = 77;
+
+ Follow follow = 78;
+ FollowResponse follow_response = 79;
+ UpdateFollowers update_followers = 80;
+ Unfollow unfollow = 81;
}
}
@@ -156,6 +162,30 @@ message UpdateWorktree {
string root_name = 3;
repeated Entry updated_entries = 4;
repeated uint64 removed_entries = 5;
+ uint64 scan_id = 6;
+}
+
+message CreateProjectEntry {
+ uint64 project_id = 1;
+ uint64 worktree_id = 2;
+ bytes path = 3;
+ bool is_directory = 4;
+}
+
+message RenameProjectEntry {
+ uint64 project_id = 1;
+ uint64 entry_id = 2;
+ bytes new_path = 3;
+}
+
+message DeleteProjectEntry {
+ uint64 project_id = 1;
+ uint64 entry_id = 2;
+}
+
+message ProjectEntryResponse {
+ Entry entry = 1;
+ uint64 worktree_scan_id = 2;
}
message AddProjectCollaborator {
@@ -630,6 +660,7 @@ message Worktree {
repeated Entry entries = 3;
repeated DiagnosticSummary diagnostic_summaries = 4;
bool visible = 5;
+ uint64 scan_id = 6;
}
message File {
@@ -642,7 +673,7 @@ message File {
message Entry {
uint64 id = 1;
bool is_dir = 2;
- string path = 3;
+ bytes path = 3;
uint64 inode = 4;
Timestamp mtime = 5;
bool is_symlink = 6;
@@ -6,13 +6,14 @@ use prost::Message as _;
use serde::Serialize;
use std::any::{Any, TypeId};
use std::{
+ fmt::Debug,
io,
time::{Duration, SystemTime, UNIX_EPOCH},
};
include!(concat!(env!("OUT_DIR"), "/zed.messages.rs"));
-pub trait EnvelopedMessage: Clone + Serialize + Sized + Send + Sync + 'static {
+pub trait EnvelopedMessage: Clone + Debug + Serialize + Sized + Send + Sync + 'static {
const NAME: &'static str;
const PRIORITY: MessagePriority;
fn into_envelope(
@@ -147,6 +148,8 @@ messages!(
(BufferReloaded, Foreground),
(BufferSaved, Foreground),
(ChannelMessageSent, Foreground),
+ (CreateProjectEntry, Foreground),
+ (DeleteProjectEntry, Foreground),
(Error, Foreground),
(Follow, Foreground),
(FollowResponse, Foreground),
@@ -174,8 +177,6 @@ messages!(
(JoinChannelResponse, Foreground),
(JoinProject, Foreground),
(JoinProjectResponse, Foreground),
- (StartLanguageServer, Foreground),
- (UpdateLanguageServer, Foreground),
(LeaveChannel, Foreground),
(LeaveProject, Foreground),
(OpenBufferById, Background),
@@ -187,6 +188,7 @@ messages!(
(PerformRenameResponse, Background),
(PrepareRename, Background),
(PrepareRenameResponse, Background),
+ (ProjectEntryResponse, Foreground),
(RegisterProjectResponse, Foreground),
(Ping, Foreground),
(RegisterProject, Foreground),
@@ -194,12 +196,14 @@ messages!(
(ReloadBuffers, Foreground),
(ReloadBuffersResponse, Foreground),
(RemoveProjectCollaborator, Foreground),
+ (RenameProjectEntry, Foreground),
(SaveBuffer, Foreground),
(SearchProject, Background),
(SearchProjectResponse, Background),
(SendChannelMessage, Foreground),
(SendChannelMessageResponse, Foreground),
(ShareProject, Foreground),
+ (StartLanguageServer, Foreground),
(Test, Foreground),
(Unfollow, Foreground),
(UnregisterProject, Foreground),
@@ -210,6 +214,7 @@ messages!(
(UpdateContacts, Foreground),
(UpdateDiagnosticSummary, Foreground),
(UpdateFollowers, Foreground),
+ (UpdateLanguageServer, Foreground),
(UpdateWorktree, Foreground),
);
@@ -219,6 +224,8 @@ request_messages!(
ApplyCompletionAdditionalEdits,
ApplyCompletionAdditionalEditsResponse
),
+ (CreateProjectEntry, ProjectEntryResponse),
+ (DeleteProjectEntry, ProjectEntryResponse),
(Follow, FollowResponse),
(FormatBuffers, FormatBuffersResponse),
(GetChannelMessages, GetChannelMessagesResponse),
@@ -241,6 +248,7 @@ request_messages!(
(RegisterProject, RegisterProjectResponse),
(RegisterWorktree, Ack),
(ReloadBuffers, ReloadBuffersResponse),
+ (RenameProjectEntry, ProjectEntryResponse),
(SaveBuffer, BufferSaved),
(SearchProject, SearchProjectResponse),
(SendChannelMessage, SendChannelMessageResponse),
@@ -257,6 +265,9 @@ entity_messages!(
ApplyCompletionAdditionalEdits,
BufferReloaded,
BufferSaved,
+ CreateProjectEntry,
+ RenameProjectEntry,
+ DeleteProjectEntry,
Follow,
FormatBuffers,
GetCodeActions,
@@ -5,4 +5,4 @@ pub mod proto;
pub use conn::Connection;
pub use peer::*;
-pub const PROTOCOL_VERSION: u32 = 15;
+pub const PROTOCOL_VERSION: u32 = 16;
@@ -292,7 +292,7 @@ impl BufferSearchBar {
.with_style(style.container)
.boxed()
})
- .on_click(move |cx| cx.dispatch_action(ToggleSearchOption(search_option)))
+ .on_click(move |_, cx| cx.dispatch_action(ToggleSearchOption(search_option)))
.with_cursor_style(CursorStyle::PointingHand)
.boxed()
}
@@ -316,7 +316,7 @@ impl BufferSearchBar {
.with_style(style.container)
.boxed()
})
- .on_click(move |cx| match direction {
+ .on_click(move |_, cx| match direction {
Direction::Prev => cx.dispatch_action(SelectPrevMatch),
Direction::Next => cx.dispatch_action(SelectNextMatch),
})
@@ -666,7 +666,7 @@ impl ProjectSearchBar {
.with_style(style.container)
.boxed()
})
- .on_click(move |cx| match direction {
+ .on_click(move |_, cx| match direction {
Direction::Prev => cx.dispatch_action(SelectPrevMatch),
Direction::Next => cx.dispatch_action(SelectNextMatch),
})
@@ -693,7 +693,7 @@ impl ProjectSearchBar {
.with_style(style.container)
.boxed()
})
- .on_click(move |cx| cx.dispatch_action(ToggleSearchOption(option)))
+ .on_click(move |_, cx| cx.dispatch_action(ToggleSearchOption(option)))
.with_cursor_style(CursorStyle::PointingHand)
.boxed()
}
@@ -844,16 +844,7 @@ mod tests {
}),
)
.await;
- let project = Project::test(fs.clone(), cx);
- let (tree, _) = project
- .update(cx, |project, cx| {
- project.find_or_create_local_worktree("/dir", true, cx)
- })
- .await
- .unwrap();
- cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
- .await;
-
+ let project = Project::test(fs.clone(), ["/dir"], cx).await;
let search = cx.add_model(|cx| ProjectSearch::new(project, cx));
let search_view = cx.add_view(Default::default(), |cx| {
ProjectSearchView::new(search.clone(), cx)
@@ -483,17 +483,20 @@ impl<T: Item + PartialEq> PartialEq for SumTree<T> {
impl<T: Item + Eq> Eq for SumTree<T> {}
impl<T: KeyedItem> SumTree<T> {
- pub fn insert_or_replace(&mut self, item: T, cx: &<T::Summary as Summary>::Context) -> bool {
- let mut replaced = false;
+ pub fn insert_or_replace(
+ &mut self,
+ item: T,
+ cx: &<T::Summary as Summary>::Context,
+ ) -> Option<T> {
+ let mut replaced = None;
*self = {
let mut cursor = self.cursor::<T::Key>();
let mut new_tree = cursor.slice(&item.key(), Bias::Left, cx);
- if cursor
- .item()
- .map_or(false, |cursor_item| cursor_item.key() == item.key())
- {
- cursor.next(cx);
- replaced = true;
+ if let Some(cursor_item) = cursor.item() {
+ if cursor_item.key() == item.key() {
+ replaced = Some(cursor_item.clone());
+ cursor.next(cx);
+ }
}
new_tree.push(item, cx);
new_tree.push_tree(cursor.suffix(cx), cx);
@@ -502,6 +505,23 @@ impl<T: KeyedItem> SumTree<T> {
replaced
}
+ pub fn remove(&mut self, key: &T::Key, cx: &<T::Summary as Summary>::Context) -> Option<T> {
+ let mut removed = None;
+ *self = {
+ let mut cursor = self.cursor::<T::Key>();
+ let mut new_tree = cursor.slice(key, Bias::Left, cx);
+ if let Some(item) = cursor.item() {
+ if item.key() == *key {
+ removed = Some(item.clone());
+ cursor.next(cx);
+ }
+ }
+ new_tree.push_tree(cursor.suffix(cx), cx);
+ new_tree
+ };
+ removed
+ }
+
pub fn edit(
&mut self,
mut edits: Vec<Edit<T>>,
@@ -204,14 +204,13 @@ pub struct ChatPanel {
pub hovered_sign_in_prompt: TextStyle,
}
-#[derive(Debug, Deserialize, Default)]
+#[derive(Deserialize, Default)]
pub struct ProjectPanel {
#[serde(flatten)]
pub container: ContainerStyle,
- pub entry: ProjectPanelEntry,
- pub hovered_entry: ProjectPanelEntry,
- pub selected_entry: ProjectPanelEntry,
- pub hovered_selected_entry: ProjectPanelEntry,
+ pub entry: Interactive<ProjectPanelEntry>,
+ pub filename_editor: FieldEditor,
+ pub indent_width: f32,
}
#[derive(Debug, Deserialize, Default)]
@@ -50,7 +50,7 @@ impl<'a> VimTestContext<'a> {
let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone());
let item = workspace
- .update(cx, |workspace, cx| workspace.open_path(file, cx))
+ .update(cx, |workspace, cx| workspace.open_path(file, true, cx))
.await
.expect("Could not open test file");
@@ -168,7 +168,7 @@ impl View for LspStatus {
self.failed.join(", "),
if self.failed.len() > 1 { "s" } else { "" }
);
- handler = Some(|cx: &mut EventContext| cx.dispatch_action(DismissErrorMessage));
+ handler = Some(|_, cx: &mut EventContext| cx.dispatch_action(DismissErrorMessage));
} else {
return Empty::new().boxed();
}
@@ -59,7 +59,7 @@ const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
pub fn init(cx: &mut MutableAppContext) {
cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| {
- pane.activate_item(action.0, true, cx);
+ pane.activate_item(action.0, true, true, cx);
});
cx.add_action(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
pane.activate_prev_item(cx);
@@ -213,7 +213,7 @@ impl Pane {
{
let prev_active_item_index = pane.active_item_index;
pane.nav_history.borrow_mut().set_mode(mode);
- pane.activate_item(index, true, cx);
+ pane.activate_item(index, true, true, cx);
pane.nav_history
.borrow_mut()
.set_mode(NavigationMode::Normal);
@@ -257,6 +257,7 @@ impl Pane {
workspace,
pane.clone(),
project_entry_id,
+ true,
cx,
build_item,
)
@@ -287,6 +288,7 @@ impl Pane {
workspace: &mut Workspace,
pane: ViewHandle<Pane>,
project_entry_id: ProjectEntryId,
+ focus_item: bool,
cx: &mut ViewContext<Workspace>,
build_item: impl FnOnce(&mut MutableAppContext) -> Box<dyn ItemHandle>,
) -> Box<dyn ItemHandle> {
@@ -294,7 +296,7 @@ impl Pane {
for (ix, item) in pane.items.iter().enumerate() {
if item.project_entry_id(cx) == Some(project_entry_id) {
let item = item.boxed_clone();
- pane.activate_item(ix, true, cx);
+ pane.activate_item(ix, true, focus_item, cx);
return Some(item);
}
}
@@ -304,7 +306,7 @@ impl Pane {
existing_item
} else {
let item = build_item(cx);
- Self::add_item(workspace, pane, item.boxed_clone(), true, cx);
+ Self::add_item(workspace, pane, item.boxed_clone(), true, focus_item, cx);
item
}
}
@@ -313,12 +315,15 @@ impl Pane {
workspace: &mut Workspace,
pane: ViewHandle<Pane>,
item: Box<dyn ItemHandle>,
- local: bool,
+ activate_pane: bool,
+ focus_item: bool,
cx: &mut ViewContext<Workspace>,
) {
// Prevent adding the same item to the pane more than once.
if let Some(item_ix) = pane.read(cx).items.iter().position(|i| i.id() == item.id()) {
- pane.update(cx, |pane, cx| pane.activate_item(item_ix, local, cx));
+ pane.update(cx, |pane, cx| {
+ pane.activate_item(item_ix, activate_pane, focus_item, cx)
+ });
return;
}
@@ -327,7 +332,7 @@ impl Pane {
pane.update(cx, |pane, cx| {
let item_idx = cmp::min(pane.active_item_index + 1, pane.items.len());
pane.items.insert(item_idx, item);
- pane.activate_item(item_idx, local, cx);
+ pane.activate_item(item_idx, activate_pane, focus_item, cx);
cx.notify();
});
}
@@ -378,7 +383,13 @@ impl Pane {
self.items.iter().position(|i| i.id() == item.id())
}
- pub fn activate_item(&mut self, index: usize, local: bool, cx: &mut ViewContext<Self>) {
+ pub fn activate_item(
+ &mut self,
+ index: usize,
+ activate_pane: bool,
+ focus_item: bool,
+ cx: &mut ViewContext<Self>,
+ ) {
use NavigationMode::{GoingBack, GoingForward};
if index < self.items.len() {
let prev_active_item_ix = mem::replace(&mut self.active_item_index, index);
@@ -387,11 +398,15 @@ impl Pane {
&& prev_active_item_ix < self.items.len())
{
self.items[prev_active_item_ix].deactivated(cx);
- cx.emit(Event::ActivateItem { local });
+ cx.emit(Event::ActivateItem {
+ local: activate_pane,
+ });
}
self.update_toolbar(cx);
- if local {
+ if focus_item {
self.focus_active_item(cx);
+ }
+ if activate_pane {
self.activate(cx);
}
self.autoscroll = true;
@@ -406,7 +421,7 @@ impl Pane {
} else if self.items.len() > 0 {
index = self.items.len() - 1;
}
- self.activate_item(index, true, cx);
+ self.activate_item(index, true, true, cx);
}
pub fn activate_next_item(&mut self, cx: &mut ViewContext<Self>) {
@@ -416,7 +431,7 @@ impl Pane {
} else {
index = 0;
}
- self.activate_item(index, true, cx);
+ self.activate_item(index, true, true, cx);
}
fn close_active_item(
@@ -498,7 +513,7 @@ impl Pane {
if is_last_item_for_entry {
if cx.read(|cx| item.has_conflict(cx) && item.can_save(cx)) {
let mut answer = pane.update(&mut cx, |pane, cx| {
- pane.activate_item(item_to_close_ix, true, cx);
+ pane.activate_item(item_to_close_ix, true, true, cx);
cx.prompt(
PromptLevel::Warning,
CONFLICT_MESSAGE,
@@ -518,7 +533,7 @@ impl Pane {
} else if cx.read(|cx| item.is_dirty(cx)) {
if cx.read(|cx| item.can_save(cx)) {
let mut answer = pane.update(&mut cx, |pane, cx| {
- pane.activate_item(item_to_close_ix, true, cx);
+ pane.activate_item(item_to_close_ix, true, true, cx);
cx.prompt(
PromptLevel::Warning,
DIRTY_MESSAGE,
@@ -535,7 +550,7 @@ impl Pane {
}
} else if cx.read(|cx| item.can_save_as(cx)) {
let mut answer = pane.update(&mut cx, |pane, cx| {
- pane.activate_item(item_to_close_ix, true, cx);
+ pane.activate_item(item_to_close_ix, true, true, cx);
cx.prompt(
PromptLevel::Warning,
DIRTY_MESSAGE,
@@ -737,7 +752,7 @@ impl Pane {
.with_cursor_style(CursorStyle::PointingHand)
.on_click({
let pane = pane.clone();
- move |cx| {
+ move |_, cx| {
cx.dispatch_action(CloseItem {
item_id,
pane: pane.clone(),
@@ -949,7 +964,7 @@ mod tests {
let close_items = workspace.update(cx, |workspace, cx| {
pane.update(cx, |pane, cx| {
- pane.activate_item(1, true, cx);
+ pane.activate_item(1, true, true, cx);
assert_eq!(pane.active_item().unwrap().id(), item2.id());
});
@@ -203,7 +203,7 @@ impl View for SidebarButtons {
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
- .on_click(move |cx| {
+ .on_click(move |_, cx| {
cx.dispatch_action(ToggleSidebarItem {
side,
item_index: ix,
@@ -493,7 +493,7 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
if T::should_activate_item_on_event(event) {
pane.update(cx, |pane, cx| {
if let Some(ix) = pane.index_for_item(&item) {
- pane.activate_item(ix, true, cx);
+ pane.activate_item(ix, true, true, cx);
pane.activate(cx);
}
});
@@ -898,7 +898,7 @@ impl Workspace {
if fs.is_file(&abs_path).await {
Some(
this.update(&mut cx, |this, cx| {
- this.open_path(project_path, cx)
+ this.open_path(project_path, true, cx)
})
.await,
)
@@ -1065,7 +1065,7 @@ impl Workspace {
Side::Right => &mut self.right_sidebar,
};
let active_item = sidebar.update(cx, |sidebar, cx| {
- sidebar.toggle_item(action.item_index, cx);
+ sidebar.activate_item(action.item_index, cx);
sidebar.active_item().cloned()
});
if let Some(active_item) = active_item {
@@ -1099,12 +1099,13 @@ impl Workspace {
pub fn add_item(&mut self, item: Box<dyn ItemHandle>, cx: &mut ViewContext<Self>) {
let pane = self.active_pane().clone();
- Pane::add_item(self, pane, item, true, cx);
+ Pane::add_item(self, pane, item, true, true, cx);
}
pub fn open_path(
&mut self,
path: impl Into<ProjectPath>,
+ focus_item: bool,
cx: &mut ViewContext<Self>,
) -> Task<Result<Box<dyn ItemHandle>, Arc<anyhow::Error>>> {
let pane = self.active_pane().downgrade();
@@ -1119,6 +1120,7 @@ impl Workspace {
this,
pane,
project_entry_id,
+ focus_item,
cx,
build_item,
))
@@ -1187,7 +1189,7 @@ impl Workspace {
});
if let Some((pane, ix)) = result {
self.activate_pane(pane.clone(), cx);
- pane.update(cx, |pane, cx| pane.activate_item(ix, true, cx));
+ pane.update(cx, |pane, cx| pane.activate_item(ix, true, true, cx));
true
} else {
false
@@ -1277,7 +1279,7 @@ impl Workspace {
self.activate_pane(new_pane.clone(), cx);
if let Some(item) = pane.read(cx).active_item() {
if let Some(clone) = item.clone_on_split(cx.as_mut()) {
- Pane::add_item(self, new_pane.clone(), clone, true, cx);
+ Pane::add_item(self, new_pane.clone(), clone, true, true, cx);
}
}
self.center.split(&pane, &new_pane, direction).unwrap();
@@ -1584,7 +1586,7 @@ impl Workspace {
.with_style(style.container)
.boxed()
})
- .on_click(|cx| cx.dispatch_action(Authenticate))
+ .on_click(|_, cx| cx.dispatch_action(Authenticate))
.with_cursor_style(CursorStyle::PointingHand)
.aligned()
.boxed(),
@@ -1635,7 +1637,7 @@ impl Workspace {
if let Some(peer_id) = peer_id {
MouseEventHandler::new::<ToggleFollow, _, _>(replica_id.into(), cx, move |_, _| content)
.with_cursor_style(CursorStyle::PointingHand)
- .on_click(move |cx| cx.dispatch_action(ToggleFollow(peer_id)))
+ .on_click(move |_, cx| cx.dispatch_action(ToggleFollow(peer_id)))
.boxed()
} else {
content
@@ -1667,7 +1669,7 @@ impl Workspace {
.boxed()
})
.with_cursor_style(CursorStyle::PointingHand)
- .on_click(|cx| cx.dispatch_action(ToggleShare))
+ .on_click(|_, cx| cx.dispatch_action(ToggleShare))
.boxed(),
)
} else {
@@ -1961,7 +1963,7 @@ impl Workspace {
}
for (pane, item) in items_to_add {
- Pane::add_item(self, pane.clone(), item.boxed_clone(), false, cx);
+ Pane::add_item(self, pane.clone(), item.boxed_clone(), false, false, cx);
if pane == self.active_pane {
pane.update(cx, |pane, cx| pane.focus_active_item(cx));
}
@@ -446,7 +446,7 @@ mod tests {
// Open the first entry
let entry_1 = workspace
- .update(cx, |w, cx| w.open_path(file1.clone(), cx))
+ .update(cx, |w, cx| w.open_path(file1.clone(), true, cx))
.await
.unwrap();
cx.read(|cx| {
@@ -460,7 +460,7 @@ mod tests {
// Open the second entry
workspace
- .update(cx, |w, cx| w.open_path(file2.clone(), cx))
+ .update(cx, |w, cx| w.open_path(file2.clone(), true, cx))
.await
.unwrap();
cx.read(|cx| {
@@ -474,7 +474,7 @@ mod tests {
// Open the first entry again. The existing pane item is activated.
let entry_1b = workspace
- .update(cx, |w, cx| w.open_path(file1.clone(), cx))
+ .update(cx, |w, cx| w.open_path(file1.clone(), true, cx))
.await
.unwrap();
assert_eq!(entry_1.id(), entry_1b.id());
@@ -492,7 +492,7 @@ mod tests {
workspace
.update(cx, |w, cx| {
w.split_pane(w.active_pane().clone(), SplitDirection::Right, cx);
- w.open_path(file2.clone(), cx)
+ w.open_path(file2.clone(), true, cx)
})
.await
.unwrap();
@@ -511,8 +511,8 @@ mod tests {
// Open the third entry twice concurrently. Only one pane item is added.
let (t1, t2) = workspace.update(cx, |w, cx| {
(
- w.open_path(file3.clone(), cx),
- w.open_path(file3.clone(), cx),
+ w.open_path(file3.clone(), true, cx),
+ w.open_path(file3.clone(), true, cx),
)
});
t1.await.unwrap();
@@ -780,6 +780,7 @@ mod tests {
worktree_id: worktree.read(cx).id(),
path: Path::new("the-new-name.rs").into(),
},
+ true,
cx,
)
})
@@ -875,7 +876,7 @@ mod tests {
let pane_1 = cx.read(|cx| workspace.read(cx).active_pane().clone());
workspace
- .update(cx, |w, cx| w.open_path(file1.clone(), cx))
+ .update(cx, |w, cx| w.open_path(file1.clone(), true, cx))
.await
.unwrap();
@@ -955,7 +956,7 @@ mod tests {
let file3 = entries[2].clone();
let editor1 = workspace
- .update(cx, |w, cx| w.open_path(file1.clone(), cx))
+ .update(cx, |w, cx| w.open_path(file1.clone(), true, cx))
.await
.unwrap()
.downcast::<Editor>()
@@ -964,13 +965,13 @@ mod tests {
editor.select_display_ranges(&[DisplayPoint::new(10, 0)..DisplayPoint::new(10, 0)], cx);
});
let editor2 = workspace
- .update(cx, |w, cx| w.open_path(file2.clone(), cx))
+ .update(cx, |w, cx| w.open_path(file2.clone(), true, cx))
.await
.unwrap()
.downcast::<Editor>()
.unwrap();
let editor3 = workspace
- .update(cx, |w, cx| w.open_path(file3.clone(), cx))
+ .update(cx, |w, cx| w.open_path(file3.clone(), true, cx))
.await
.unwrap()
.downcast::<Editor>()
@@ -1,34 +1,35 @@
import Theme from "../themes/theme";
-import { Color } from "../utils/color";
import { panel } from "./app";
-import { backgroundColor, iconColor, text, TextColor } from "./components";
+import { backgroundColor, iconColor, player, text } from "./components";
export default function projectPanel(theme: Theme) {
- function entry(theme: Theme, textColor: TextColor, background?: Color) {
- return {
+ return {
+ ...panel,
+ padding: { left: 12, right: 12, top: 6, bottom: 6 },
+ indentWidth: 20,
+ entry: {
height: 24,
- background,
iconColor: iconColor(theme, "muted"),
iconSize: 8,
iconSpacing: 8,
- text: text(theme, "mono", textColor, { size: "sm" }),
- };
- }
-
- return {
- ...panel,
- entry: entry(theme, "muted"),
- hoveredEntry: entry(
- theme,
- "primary",
- backgroundColor(theme, 300, "hovered")
- ),
- selectedEntry: entry(theme, "primary"),
- hoveredSelectedEntry: entry(
- theme,
- "active",
- backgroundColor(theme, 300, "hovered")
- ),
- padding: { left: 12, right: 12, top: 6, bottom: 6 },
+ text: text(theme, "mono", "muted", { size: "sm" }),
+ hover: {
+ background: backgroundColor(theme, 300, "hovered"),
+ text: text(theme, "mono", "primary", { size: "sm" }),
+ },
+ active: {
+ background: backgroundColor(theme, 300, "active"),
+ text: text(theme, "mono", "primary", { size: "sm" }),
+ },
+ activeHover: {
+ background: backgroundColor(theme, 300, "hovered"),
+ text: text(theme, "mono", "active", { size: "sm" }),
+ }
+ },
+ filenameEditor: {
+ background: backgroundColor(theme, 500, "active"),
+ text: text(theme, "mono", "primary", { size: "sm" }),
+ selection: player(theme, 1).selection,
+ },
};
}