Cargo.lock 🔗
@@ -5033,6 +5033,7 @@ dependencies = [
"language",
"menu",
"postage",
+ "pretty_assertions",
"project",
"schemars",
"serde",
Mikayla Maki created
Cargo.lock | 1
Cargo.toml | 1
crates/collab/Cargo.toml | 2
crates/fs/src/fs.rs | 6
crates/project/Cargo.toml | 2
crates/project/src/worktree.rs | 23 +++
crates/project/src/worktree_tests.rs | 77 ++++++++++++
crates/project_panel/Cargo.toml | 1
crates/project_panel/src/project_panel.rs | 152 ++++++++++++++++++++++++
crates/settings/Cargo.toml | 2
10 files changed, 259 insertions(+), 8 deletions(-)
@@ -5033,6 +5033,7 @@ dependencies = [
"language",
"menu",
"postage",
+ "pretty_assertions",
"project",
"schemars",
"serde",
@@ -101,6 +101,7 @@ time = { version = "0.3", features = ["serde", "serde-well-known"] }
toml = { version = "0.5" }
tree-sitter = "0.20"
unindent = { version = "0.1.7" }
+pretty_assertions = "1.3.0"
[patch.crates-io]
tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "49226023693107fba9a1191136a4f47f38cdca73" }
@@ -67,7 +67,7 @@ fs = { path = "../fs", features = ["test-support"] }
git = { path = "../git", features = ["test-support"] }
live_kit_client = { path = "../live_kit_client", features = ["test-support"] }
lsp = { path = "../lsp", features = ["test-support"] }
-pretty_assertions = "1.3.0"
+pretty_assertions = "*"
project = { path = "../project", features = ["test-support"] }
rpc = { path = "../rpc", features = ["test-support"] }
settings = { path = "../settings", features = ["test-support"] }
@@ -279,6 +279,9 @@ impl Fs for RealFs {
async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()> {
let buffer_size = text.summary().len.min(10 * 1024);
+ if let Some(path) = path.parent() {
+ self.create_dir(path).await?;
+ }
let file = smol::fs::File::create(path).await?;
let mut writer = smol::io::BufWriter::with_capacity(buffer_size, file);
for chunk in chunks(text, line_ending) {
@@ -1077,6 +1080,9 @@ impl Fs for FakeFs {
self.simulate_random_delay().await;
let path = normalize_path(path);
let content = chunks(text, line_ending).collect();
+ if let Some(path) = path.parent() {
+ self.create_dir(path).await?;
+ }
self.write_file_internal(path, content)?;
Ok(())
}
@@ -64,7 +64,7 @@ itertools = "0.10"
[dev-dependencies]
ctor.workspace = true
env_logger.workspace = true
-pretty_assertions = "1.3.0"
+pretty_assertions = "*"
client = { path = "../client", features = ["test-support"] }
collections = { path = "../collections", features = ["test-support"] }
db = { path = "../db", features = ["test-support"] }
@@ -1001,10 +1001,25 @@ impl LocalWorktree {
cx.spawn(|this, mut cx| async move {
write.await?;
- this.update(&mut cx, |this, cx| {
- this.as_local_mut().unwrap().refresh_entry(path, None, cx)
- })
- .await
+ let (result, refreshes) = this.update(&mut cx, |this, cx| {
+ let mut refreshes = Vec::new();
+ for path in path.ancestors().skip(1) {
+ refreshes.push(this.as_local_mut().unwrap().refresh_entry(
+ path.into(),
+ None,
+ cx,
+ ));
+ }
+ (
+ this.as_local_mut().unwrap().refresh_entry(path, None, cx),
+ refreshes,
+ )
+ });
+ for refresh in refreshes {
+ refresh.await.log_err();
+ }
+
+ result.await
})
}
@@ -936,6 +936,83 @@ async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
);
}
+#[gpui::test]
+async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
+ let client_fake = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
+
+ let fs_fake = FakeFs::new(cx.background());
+ fs_fake.insert_tree(
+ "/root",
+ json!({
+ "a": {},
+ }),
+ )
+ .await;
+
+ let tree_fake = Worktree::local(
+ client_fake,
+ "/root".as_ref(),
+ true,
+ fs_fake,
+ Default::default(),
+ &mut cx.to_async(),
+ )
+ .await
+ .unwrap();
+
+ let entry = tree_fake
+ .update(cx, |tree, cx| {
+ tree.as_local_mut()
+ .unwrap()
+ .create_entry("a/b/c/d.txt".as_ref(), false, cx)
+ })
+ .await
+ .unwrap();
+ assert!(entry.is_file());
+
+ cx.foreground().run_until_parked();
+ tree_fake.read_with(cx, |tree, _| {
+ assert!(tree.entry_for_path("a/b/c/d.txt").unwrap().is_file());
+ assert!(tree.entry_for_path("a/b/c/").unwrap().is_dir());
+ assert!(tree.entry_for_path("a/b/").unwrap().is_dir());
+ });
+
+ let client_real = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
+
+ let fs_real = Arc::new(RealFs);
+ let temp_root = temp_tree(json!({
+ "a": {}
+ }));
+
+ let tree_real = Worktree::local(
+ client_real,
+ temp_root.path(),
+ true,
+ fs_real,
+ Default::default(),
+ &mut cx.to_async(),
+ )
+ .await
+ .unwrap();
+
+ let entry = tree_real
+ .update(cx, |tree, cx| {
+ tree.as_local_mut()
+ .unwrap()
+ .create_entry("a/b/c/d.txt".as_ref(), false, cx)
+ })
+ .await
+ .unwrap();
+ assert!(entry.is_file());
+
+ cx.foreground().run_until_parked();
+ tree_real.read_with(cx, |tree, _| {
+ assert!(tree.entry_for_path("a/b/c/d.txt").unwrap().is_file());
+ assert!(tree.entry_for_path("a/b/c/").unwrap().is_dir());
+ assert!(tree.entry_for_path("a/b/").unwrap().is_dir());
+ });
+}
+
#[gpui::test(iterations = 100)]
async fn test_random_worktree_operations_during_initial_scan(
cx: &mut TestAppContext,
@@ -27,6 +27,7 @@ serde_derive.workspace = true
serde_json.workspace = true
anyhow.workspace = true
schemars.workspace = true
+pretty_assertions.workspace = true
unicase = "2.6"
[dev-dependencies]
@@ -64,7 +64,7 @@ pub struct ProjectPanel {
pending_serialization: Task<Option<()>>,
}
-#[derive(Copy, Clone)]
+#[derive(Copy, Clone, Debug)]
struct Selection {
worktree_id: WorktreeId,
entry_id: ProjectEntryId,
@@ -588,6 +588,7 @@ impl ProjectPanel {
if selection.entry_id == edited_entry_id {
selection.worktree_id = worktree_id;
selection.entry_id = new_entry.id;
+ this.expand_to_selection(cx);
}
}
this.update_visible_entries(None, cx);
@@ -965,6 +966,25 @@ impl ProjectPanel {
Some((worktree, entry))
}
+ fn expand_to_selection(&mut self, cx: &mut ViewContext<Self>) -> Option<()> {
+ let (worktree, entry) = self.selected_entry(cx)?;
+ let expanded_dir_ids = self.expanded_dir_ids.entry(worktree.id()).or_default();
+
+ for path in entry.path.ancestors() {
+ let Some(entry) = worktree.entry_for_path(path) else {
+ continue;
+ };
+ if entry.is_dir() {
+ if let Err(idx) = expanded_dir_ids.binary_search(&entry.id) {
+ expanded_dir_ids.insert(idx, entry.id);
+ }
+ }
+ }
+
+
+ Some(())
+ }
+
fn update_visible_entries(
&mut self,
new_selected_entry: Option<(WorktreeId, ProjectEntryId)>,
@@ -1139,6 +1159,7 @@ impl ProjectPanel {
for entry in visible_worktree_entries[entry_range].iter() {
let status = git_status_setting.then(|| entry.git_status).flatten();
+
let mut details = EntryDetails {
filename: entry
.path
@@ -1592,6 +1613,7 @@ impl ClipboardEntry {
mod tests {
use super::*;
use gpui::{TestAppContext, ViewHandle};
+ use pretty_assertions::assert_eq;
use project::FakeFs;
use serde_json::json;
use settings::SettingsStore;
@@ -2002,6 +2024,134 @@ mod tests {
);
}
+ #[gpui::test(iterations = 30)]
+ async fn test_adding_directories_via_file(cx: &mut gpui::TestAppContext) {
+ init_test(cx);
+
+ let fs = FakeFs::new(cx.background());
+ fs.insert_tree(
+ "/root1",
+ json!({
+ ".dockerignore": "",
+ ".git": {
+ "HEAD": "",
+ },
+ "a": {
+ "0": { "q": "", "r": "", "s": "" },
+ "1": { "t": "", "u": "" },
+ "2": { "v": "", "w": "", "x": "", "y": "" },
+ },
+ "b": {
+ "3": { "Q": "" },
+ "4": { "R": "", "S": "", "T": "", "U": "" },
+ },
+ "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".as_ref(), "/root2".as_ref()], cx).await;
+ let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+ let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
+
+ select_path(&panel, "root1", cx);
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &[
+ "v root1 <== selected",
+ " > .git",
+ " > 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.new_file(&NewFile, cx));
+ cx.read_window(window_id, |cx| {
+ let panel = panel.read(cx);
+ assert!(panel.filename_editor.is_focused(cx));
+ });
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &[
+ "v root1",
+ " > .git",
+ " > a",
+ " > b",
+ " > C",
+ " [EDITOR: ''] <== selected",
+ " .dockerignore",
+ "v root2",
+ " > d",
+ " > e",
+ ]
+ );
+
+
+ let confirm = panel.update(cx, |panel, cx| {
+ panel.filename_editor.update(cx, |editor, cx| {
+ editor.set_text("bdir1/dir2/the-new-filename", cx)
+ });
+ panel.confirm(&Confirm, cx).unwrap()
+ });
+
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..10, cx),
+ &[
+ "v root1",
+ " > .git",
+ " > a",
+ " > b",
+ " > C",
+ " [PROCESSING: 'bdir1/dir2/the-new-filename'] <== selected",
+ " .dockerignore",
+ "v root2",
+ " > d",
+ " > e",
+ ]
+ );
+
+ confirm.await.unwrap();
+ assert_eq!(
+ visible_entries_as_strings(&panel, 0..13, cx),
+ &[
+ "v root1",
+ " > .git",
+ " > a",
+ " > b",
+ " v bdir1",
+ " v dir2",
+ " the-new-filename <== selected",
+ " > C",
+ " .dockerignore",
+ "v root2",
+ " > d",
+ " > e",
+ ]
+ );
+ }
+
#[gpui::test]
async fn test_copy_paste(cx: &mut gpui::TestAppContext) {
init_test(cx);
@@ -38,5 +38,5 @@ tree-sitter-json = "*"
gpui = { path = "../gpui", features = ["test-support"] }
fs = { path = "../fs", features = ["test-support"] }
indoc.workspace = true
-pretty_assertions = "1.3.0"
+pretty_assertions = "*"
unindent.workspace = true