Detailed changes
@@ -4,8 +4,10 @@ use collab_ui::notifications::project_shared_notification::ProjectSharedNotifica
use editor::{Editor, ExcerptRange, MultiBuffer};
use gpui::{executor::Deterministic, geometry::vector::vec2f, TestAppContext, ViewHandle};
use live_kit_client::MacOSDisplay;
+use project::project_settings::ProjectSettings;
use rpc::proto::PeerId;
use serde_json::json;
+use settings::SettingsStore;
use std::{borrow::Cow, sync::Arc};
use workspace::{
dock::{test::TestPanel, DockPosition},
@@ -1602,6 +1604,141 @@ async fn test_following_across_workspaces(
});
}
+#[gpui::test]
+async fn test_following_into_excluded_file(
+ deterministic: Arc<Deterministic>,
+ mut cx_a: &mut TestAppContext,
+ mut cx_b: &mut TestAppContext,
+) {
+ deterministic.forbid_parking();
+
+ let mut server = TestServer::start(&deterministic).await;
+ let client_a = server.create_client(cx_a, "user_a").await;
+ let client_b = server.create_client(cx_b, "user_b").await;
+ for cx in [&mut cx_a, &mut cx_b] {
+ cx.update(|cx| {
+ cx.update_global::<SettingsStore, _, _>(|store, cx| {
+ store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
+ project_settings.file_scan_exclusions = Some(vec!["**/.git".to_string()]);
+ });
+ });
+ });
+ }
+ server
+ .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
+ .await;
+ let active_call_a = cx_a.read(ActiveCall::global);
+ let active_call_b = cx_b.read(ActiveCall::global);
+
+ cx_a.update(editor::init);
+ cx_b.update(editor::init);
+
+ client_a
+ .fs()
+ .insert_tree(
+ "/a",
+ json!({
+ ".git": {
+ "COMMIT_EDITMSG": "write your commit message here",
+ },
+ "1.txt": "one\none\none",
+ "2.txt": "two\ntwo\ntwo",
+ "3.txt": "three\nthree\nthree",
+ }),
+ )
+ .await;
+ let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
+ active_call_a
+ .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
+ .await
+ .unwrap();
+
+ let project_id = active_call_a
+ .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
+ .await
+ .unwrap();
+ let project_b = client_b.build_remote_project(project_id, cx_b).await;
+ active_call_b
+ .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
+ .await
+ .unwrap();
+
+ let window_a = client_a.build_workspace(&project_a, cx_a);
+ let workspace_a = window_a.root(cx_a);
+ let peer_id_a = client_a.peer_id().unwrap();
+ let window_b = client_b.build_workspace(&project_b, cx_b);
+ let workspace_b = window_b.root(cx_b);
+
+ // Client A opens editors for a regular file and an excluded file.
+ let editor_for_regular = workspace_a
+ .update(cx_a, |workspace, cx| {
+ workspace.open_path((worktree_id, "1.txt"), None, true, cx)
+ })
+ .await
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap();
+ let editor_for_excluded_a = workspace_a
+ .update(cx_a, |workspace, cx| {
+ workspace.open_path((worktree_id, ".git/COMMIT_EDITMSG"), None, true, cx)
+ })
+ .await
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap();
+
+ // Client A updates their selections in those editors
+ editor_for_regular.update(cx_a, |editor, cx| {
+ editor.handle_input("a", cx);
+ editor.handle_input("b", cx);
+ editor.handle_input("c", cx);
+ editor.select_left(&Default::default(), cx);
+ assert_eq!(editor.selections.ranges(cx), vec![3..2]);
+ });
+ editor_for_excluded_a.update(cx_a, |editor, cx| {
+ editor.select_all(&Default::default(), cx);
+ editor.handle_input("new commit message", cx);
+ editor.select_left(&Default::default(), cx);
+ assert_eq!(editor.selections.ranges(cx), vec![18..17]);
+ });
+
+ // When client B starts following client A, currently visible file is replicated
+ workspace_b
+ .update(cx_b, |workspace, cx| {
+ workspace.follow(peer_id_a, cx).unwrap()
+ })
+ .await
+ .unwrap();
+
+ let editor_for_excluded_b = workspace_b.read_with(cx_b, |workspace, cx| {
+ workspace
+ .active_item(cx)
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap()
+ });
+ assert_eq!(
+ cx_b.read(|cx| editor_for_excluded_b.project_path(cx)),
+ Some((worktree_id, ".git/COMMIT_EDITMSG").into())
+ );
+ assert_eq!(
+ editor_for_excluded_b.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
+ vec![18..17]
+ );
+
+ // Changes from B to the excluded file are replicated in A's editor
+ editor_for_excluded_b.update(cx_b, |editor, cx| {
+ editor.handle_input("\nCo-Authored-By: B <b@b.b>", cx);
+ });
+ deterministic.run_until_parked();
+ editor_for_excluded_a.update(cx_a, |editor, cx| {
+ assert_eq!(
+ editor.text(cx),
+ "new commit messag\nCo-Authored-By: B <b@b.b>"
+ );
+ });
+}
+
fn visible_push_notifications(
cx: &mut TestAppContext,
) -> Vec<gpui::ViewHandle<ProjectSharedNotification>> {
@@ -2981,11 +2981,10 @@ async fn test_fs_operations(
let entry = project_b
.update(cx_b, |project, cx| {
- project
- .create_entry((worktree_id, "c.txt"), false, cx)
- .unwrap()
+ project.create_entry((worktree_id, "c.txt"), false, cx)
})
.await
+ .unwrap()
.unwrap();
worktree_a.read_with(cx_a, |worktree, _| {
assert_eq!(
@@ -3010,7 +3009,6 @@ async fn test_fs_operations(
.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, _| {
@@ -3034,11 +3032,10 @@ async fn test_fs_operations(
let dir_entry = project_b
.update(cx_b, |project, cx| {
- project
- .create_entry((worktree_id, "DIR"), true, cx)
- .unwrap()
+ project.create_entry((worktree_id, "DIR"), true, cx)
})
.await
+ .unwrap()
.unwrap();
worktree_a.read_with(cx_a, |worktree, _| {
assert_eq!(
@@ -3061,25 +3058,19 @@ async fn test_fs_operations(
project_b
.update(cx_b, |project, cx| {
- project
- .create_entry((worktree_id, "DIR/e.txt"), false, cx)
- .unwrap()
+ project.create_entry((worktree_id, "DIR/e.txt"), false, cx)
})
.await
.unwrap();
project_b
.update(cx_b, |project, cx| {
- project
- .create_entry((worktree_id, "DIR/SUBDIR"), true, cx)
- .unwrap()
+ project.create_entry((worktree_id, "DIR/SUBDIR"), true, cx)
})
.await
.unwrap();
project_b
.update(cx_b, |project, cx| {
- project
- .create_entry((worktree_id, "DIR/SUBDIR/f.txt"), false, cx)
- .unwrap()
+ project.create_entry((worktree_id, "DIR/SUBDIR/f.txt"), false, cx)
})
.await
.unwrap();
@@ -3120,9 +3111,7 @@ async fn test_fs_operations(
project_b
.update(cx_b, |project, cx| {
- project
- .copy_entry(entry.id, Path::new("f.txt"), cx)
- .unwrap()
+ project.copy_entry(entry.id, Path::new("f.txt"), cx)
})
.await
.unwrap();
@@ -665,7 +665,6 @@ impl RandomizedTest for ProjectCollaborationTest {
ensure_project_shared(&project, client, cx).await;
project
.update(cx, |p, cx| p.create_entry(project_path, is_dir, cx))
- .unwrap()
.await?;
}
@@ -4,10 +4,12 @@
// use call::ActiveCall;
// use collab_ui::notifications::project_shared_notification::ProjectSharedNotification;
// use editor::{Editor, ExcerptRange, MultiBuffer};
-// use gpui::{BackgroundExecutor, TestAppContext, View};
+// use gpui::{point, BackgroundExecutor, TestAppContext, View, VisualTestContext, WindowContext};
// use live_kit_client::MacOSDisplay;
+// use project::project_settings::ProjectSettings;
// use rpc::proto::PeerId;
// use serde_json::json;
+// use settings::SettingsStore;
// use std::borrow::Cow;
// use workspace::{
// dock::{test::TestPanel, DockPosition},
@@ -24,7 +26,7 @@
// cx_c: &mut TestAppContext,
// cx_d: &mut TestAppContext,
// ) {
-// let mut server = TestServer::start(&executor).await;
+// let mut server = TestServer::start(executor.clone()).await;
// let client_a = server.create_client(cx_a, "user_a").await;
// let client_b = server.create_client(cx_b, "user_b").await;
// let client_c = server.create_client(cx_c, "user_c").await;
@@ -71,12 +73,22 @@
// .unwrap();
// let window_a = client_a.build_workspace(&project_a, cx_a);
-// let workspace_a = window_a.root(cx_a);
+// let workspace_a = window_a.root(cx_a).unwrap();
// let window_b = client_b.build_workspace(&project_b, cx_b);
-// let workspace_b = window_b.root(cx_b);
+// let workspace_b = window_b.root(cx_b).unwrap();
+
+// todo!("could be wrong")
+// let mut cx_a = VisualTestContext::from_window(*window_a, cx_a);
+// let cx_a = &mut cx_a;
+// let mut cx_b = VisualTestContext::from_window(*window_b, cx_b);
+// let cx_b = &mut cx_b;
+// let mut cx_c = VisualTestContext::from_window(*window_c, cx_c);
+// let cx_c = &mut cx_c;
+// let mut cx_d = VisualTestContext::from_window(*window_d, cx_d);
+// let cx_d = &mut cx_d;
// // Client A opens some editors.
-// let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
+// let pane_a = workspace_a.update(cx_a, |workspace, _| workspace.active_pane().clone());
// let editor_a1 = workspace_a
// .update(cx_a, |workspace, cx| {
// workspace.open_path((worktree_id, "1.txt"), None, true, cx)
@@ -132,8 +144,8 @@
// .await
// .unwrap();
-// cx_c.foreground().run_until_parked();
-// let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
+// cx_c.executor().run_until_parked();
+// let editor_b2 = workspace_b.update(cx_b, |workspace, cx| {
// workspace
// .active_item(cx)
// .unwrap()
@@ -145,19 +157,19 @@
// Some((worktree_id, "2.txt").into())
// );
// assert_eq!(
-// editor_b2.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
+// editor_b2.update(cx_b, |editor, cx| editor.selections.ranges(cx)),
// vec![2..1]
// );
// assert_eq!(
-// editor_b1.read_with(cx_b, |editor, cx| editor.selections.ranges(cx)),
+// editor_b1.update(cx_b, |editor, cx| editor.selections.ranges(cx)),
// vec![3..2]
// );
-// cx_c.foreground().run_until_parked();
+// cx_c.executor().run_until_parked();
// let active_call_c = cx_c.read(ActiveCall::global);
// let project_c = client_c.build_remote_project(project_id, cx_c).await;
// let window_c = client_c.build_workspace(&project_c, cx_c);
-// let workspace_c = window_c.root(cx_c);
+// let workspace_c = window_c.root(cx_c).unwrap();
// active_call_c
// .update(cx_c, |call, cx| call.set_location(Some(&project_c), cx))
// .await
@@ -172,10 +184,13 @@
// .await
// .unwrap();
-// cx_d.foreground().run_until_parked();
+// cx_d.executor().run_until_parked();
// let active_call_d = cx_d.read(ActiveCall::global);
// let project_d = client_d.build_remote_project(project_id, cx_d).await;
-// let workspace_d = client_d.build_workspace(&project_d, cx_d).root(cx_d);
+// let workspace_d = client_d
+// .build_workspace(&project_d, cx_d)
+// .root(cx_d)
+// .unwrap();
// active_call_d
// .update(cx_d, |call, cx| call.set_location(Some(&project_d), cx))
// .await
@@ -183,7 +198,7 @@
// drop(project_d);
// // All clients see that clients B and C are following client A.
-// cx_c.foreground().run_until_parked();
+// cx_c.executor().run_until_parked();
// for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
// assert_eq!(
// followers_by_leader(project_id, cx),
@@ -198,7 +213,7 @@
// });
// // All clients see that clients B is following client A.
-// cx_c.foreground().run_until_parked();
+// cx_c.executor().run_until_parked();
// for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
// assert_eq!(
// followers_by_leader(project_id, cx),
@@ -216,7 +231,7 @@
// .unwrap();
// // All clients see that clients B and C are following client A.
-// cx_c.foreground().run_until_parked();
+// cx_c.executor().run_until_parked();
// for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
// assert_eq!(
// followers_by_leader(project_id, cx),
@@ -240,7 +255,7 @@
// .unwrap();
// // All clients see that D is following C
-// cx_d.foreground().run_until_parked();
+// cx_d.executor().run_until_parked();
// for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
// assert_eq!(
// followers_by_leader(project_id, cx),
@@ -257,7 +272,7 @@
// cx_c.drop_last(workspace_c);
// // Clients A and B see that client B is following A, and client C is not present in the followers.
-// cx_c.foreground().run_until_parked();
+// cx_c.executor().run_until_parked();
// for (name, cx) in [("A", &cx_a), ("B", &cx_b), ("C", &cx_c), ("D", &cx_d)] {
// assert_eq!(
// followers_by_leader(project_id, cx),
@@ -271,12 +286,15 @@
// workspace.activate_item(&editor_a1, cx)
// });
// executor.run_until_parked();
-// workspace_b.read_with(cx_b, |workspace, cx| {
-// assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
+// workspace_b.update(cx_b, |workspace, cx| {
+// assert_eq!(
+// workspace.active_item(cx).unwrap().item_id(),
+// editor_b1.item_id()
+// );
// });
// // When client A opens a multibuffer, client B does so as well.
-// let multibuffer_a = cx_a.add_model(|cx| {
+// let multibuffer_a = cx_a.build_model(|cx| {
// let buffer_a1 = project_a.update(cx, |project, cx| {
// project
// .get_open_buffer(&(worktree_id, "1.txt").into(), cx)
@@ -308,12 +326,12 @@
// });
// let multibuffer_editor_a = workspace_a.update(cx_a, |workspace, cx| {
// let editor =
-// cx.add_view(|cx| Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), cx));
+// cx.build_view(|cx| Editor::for_multibuffer(multibuffer_a, Some(project_a.clone()), cx));
// workspace.add_item(Box::new(editor.clone()), cx);
// editor
// });
// executor.run_until_parked();
-// let multibuffer_editor_b = workspace_b.read_with(cx_b, |workspace, cx| {
+// let multibuffer_editor_b = workspace_b.update(cx_b, |workspace, cx| {
// workspace
// .active_item(cx)
// .unwrap()
@@ -321,8 +339,8 @@
// .unwrap()
// });
// assert_eq!(
-// multibuffer_editor_a.read_with(cx_a, |editor, cx| editor.text(cx)),
-// multibuffer_editor_b.read_with(cx_b, |editor, cx| editor.text(cx)),
+// multibuffer_editor_a.update(cx_a, |editor, cx| editor.text(cx)),
+// multibuffer_editor_b.update(cx_b, |editor, cx| editor.text(cx)),
// );
// // When client A navigates back and forth, client B does so as well.
@@ -333,8 +351,11 @@
// .await
// .unwrap();
// executor.run_until_parked();
-// workspace_b.read_with(cx_b, |workspace, cx| {
-// assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
+// workspace_b.update(cx_b, |workspace, cx| {
+// assert_eq!(
+// workspace.active_item(cx).unwrap().item_id(),
+// editor_b1.item_id()
+// );
// });
// workspace_a
@@ -344,8 +365,11 @@
// .await
// .unwrap();
// executor.run_until_parked();
-// workspace_b.read_with(cx_b, |workspace, cx| {
-// assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b2.id());
+// workspace_b.update(cx_b, |workspace, cx| {
+// assert_eq!(
+// workspace.active_item(cx).unwrap().item_id(),
+// editor_b2.item_id()
+// );
// });
// workspace_a
@@ -355,8 +379,11 @@
// .await
// .unwrap();
// executor.run_until_parked();
-// workspace_b.read_with(cx_b, |workspace, cx| {
-// assert_eq!(workspace.active_item(cx).unwrap().id(), editor_b1.id());
+// workspace_b.update(cx_b, |workspace, cx| {
+// assert_eq!(
+// workspace.active_item(cx).unwrap().item_id(),
+// editor_b1.item_id()
+// );
// });
// // Changes to client A's editor are reflected on client B.
@@ -364,20 +391,20 @@
// editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2]));
// });
// executor.run_until_parked();
-// editor_b1.read_with(cx_b, |editor, cx| {
+// editor_b1.update(cx_b, |editor, cx| {
// assert_eq!(editor.selections.ranges(cx), &[1..1, 2..2]);
// });
// editor_a1.update(cx_a, |editor, cx| editor.set_text("TWO", cx));
// executor.run_until_parked();
-// editor_b1.read_with(cx_b, |editor, cx| assert_eq!(editor.text(cx), "TWO"));
+// editor_b1.update(cx_b, |editor, cx| assert_eq!(editor.text(cx), "TWO"));
// editor_a1.update(cx_a, |editor, cx| {
// editor.change_selections(None, cx, |s| s.select_ranges([3..3]));
-// editor.set_scroll_position(vec2f(0., 100.), cx);
+// editor.set_scroll_position(point(0., 100.), cx);
// });
// executor.run_until_parked();
-// editor_b1.read_with(cx_b, |editor, cx| {
+// editor_b1.update(cx_b, |editor, cx| {
// assert_eq!(editor.selections.ranges(cx), &[3..3]);
// });
@@ -390,11 +417,11 @@
// });
// executor.run_until_parked();
// assert_eq!(
-// workspace_b.read_with(cx_b, |workspace, cx| workspace
+// workspace_b.update(cx_b, |workspace, cx| workspace
// .active_item(cx)
// .unwrap()
-// .id()),
-// editor_b1.id()
+// .item_id()),
+// editor_b1.item_id()
// );
// // Client A starts following client B.
@@ -405,15 +432,15 @@
// .await
// .unwrap();
// assert_eq!(
-// workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
+// workspace_a.update(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
// Some(peer_id_b)
// );
// assert_eq!(
-// workspace_a.read_with(cx_a, |workspace, cx| workspace
+// workspace_a.update(cx_a, |workspace, cx| workspace
// .active_item(cx)
// .unwrap()
-// .id()),
-// editor_a1.id()
+// .item_id()),
+// editor_a1.item_id()
// );
// // Client B activates an external window, which causes a new screen-sharing item to be added to the pane.
@@ -432,7 +459,7 @@
// .await
// .unwrap();
// executor.run_until_parked();
-// let shared_screen = workspace_a.read_with(cx_a, |workspace, cx| {
+// let shared_screen = workspace_a.update(cx_a, |workspace, cx| {
// workspace
// .active_item(cx)
// .expect("no active item")
@@ -446,8 +473,11 @@
// .await
// .unwrap();
// executor.run_until_parked();
-// workspace_a.read_with(cx_a, |workspace, cx| {
-// assert_eq!(workspace.active_item(cx).unwrap().id(), editor_a1.id())
+// workspace_a.update(cx_a, |workspace, cx| {
+// assert_eq!(
+// workspace.active_item(cx).unwrap().item_id(),
+// editor_a1.item_id()
+// )
// });
// // Client B activates a multibuffer that was created by following client A. Client A returns to that multibuffer.
@@ -455,26 +485,26 @@
// workspace.activate_item(&multibuffer_editor_b, cx)
// });
// executor.run_until_parked();
-// workspace_a.read_with(cx_a, |workspace, cx| {
+// workspace_a.update(cx_a, |workspace, cx| {
// assert_eq!(
-// workspace.active_item(cx).unwrap().id(),
-// multibuffer_editor_a.id()
+// workspace.active_item(cx).unwrap().item_id(),
+// multibuffer_editor_a.item_id()
// )
// });
// // Client B activates a panel, and the previously-opened screen-sharing item gets activated.
-// let panel = window_b.add_view(cx_b, |_| TestPanel::new(DockPosition::Left));
+// let panel = window_b.build_view(cx_b, |_| TestPanel::new(DockPosition::Left));
// workspace_b.update(cx_b, |workspace, cx| {
// workspace.add_panel(panel, cx);
// workspace.toggle_panel_focus::<TestPanel>(cx);
// });
// executor.run_until_parked();
// assert_eq!(
-// workspace_a.read_with(cx_a, |workspace, cx| workspace
+// workspace_a.update(cx_a, |workspace, cx| workspace
// .active_item(cx)
// .unwrap()
-// .id()),
-// shared_screen.id()
+// .item_id()),
+// shared_screen.item_id()
// );
// // Toggling the focus back to the pane causes client A to return to the multibuffer.
@@ -482,16 +512,16 @@
// workspace.toggle_panel_focus::<TestPanel>(cx);
// });
// executor.run_until_parked();
-// workspace_a.read_with(cx_a, |workspace, cx| {
+// workspace_a.update(cx_a, |workspace, cx| {
// assert_eq!(
-// workspace.active_item(cx).unwrap().id(),
-// multibuffer_editor_a.id()
+// workspace.active_item(cx).unwrap().item_id(),
+// multibuffer_editor_a.item_id()
// )
// });
// // Client B activates an item that doesn't implement following,
// // so the previously-opened screen-sharing item gets activated.
-// let unfollowable_item = window_b.add_view(cx_b, |_| TestItem::new());
+// let unfollowable_item = window_b.build_view(cx_b, |_| TestItem::new());
// workspace_b.update(cx_b, |workspace, cx| {
// workspace.active_pane().update(cx, |pane, cx| {
// pane.add_item(Box::new(unfollowable_item), true, true, None, cx)
@@ -499,18 +529,18 @@
// });
// executor.run_until_parked();
// assert_eq!(
-// workspace_a.read_with(cx_a, |workspace, cx| workspace
+// workspace_a.update(cx_a, |workspace, cx| workspace
// .active_item(cx)
// .unwrap()
-// .id()),
-// shared_screen.id()
+// .item_id()),
+// shared_screen.item_id()
// );
// // Following interrupts when client B disconnects.
// client_b.disconnect(&cx_b.to_async());
// executor.advance_clock(RECONNECT_TIMEOUT);
// assert_eq!(
-// workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
+// workspace_a.update(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)),
// None
// );
// }
@@ -521,7 +551,7 @@
// cx_a: &mut TestAppContext,
// cx_b: &mut TestAppContext,
// ) {
-// let mut server = TestServer::start(&executor).await;
+// let mut server = TestServer::start(executor.clone()).await;
// let client_a = server.create_client(cx_a, "user_a").await;
// let client_b = server.create_client(cx_b, "user_b").await;
// server
@@ -560,13 +590,19 @@
// .await
// .unwrap();
-// let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
-// let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
+// let workspace_a = client_a
+// .build_workspace(&project_a, cx_a)
+// .root(cx_a)
+// .unwrap();
+// let pane_a = workspace_a.update(cx_a, |workspace, _| workspace.active_pane().clone());
-// let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
-// let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
+// let workspace_b = client_b
+// .build_workspace(&project_b, cx_b)
+// .root(cx_b)
+// .unwrap();
+// let pane_b = workspace_b.update(cx_b, |workspace, _| workspace.active_pane().clone());
-// let client_b_id = project_a.read_with(cx_a, |project, _| {
+// let client_b_id = project_a.update(cx_a, |project, _| {
// project.collaborators().values().next().unwrap().peer_id
// });
@@ -584,7 +620,7 @@
// .await
// .unwrap();
-// let pane_paths = |pane: &ViewHandle<workspace::Pane>, cx: &mut TestAppContext| {
+// let pane_paths = |pane: &View<workspace::Pane>, cx: &mut TestAppContext| {
// pane.update(cx, |pane, cx| {
// pane.items()
// .map(|item| {
@@ -642,7 +678,7 @@
// cx_a: &mut TestAppContext,
// cx_b: &mut TestAppContext,
// ) {
-// let mut server = TestServer::start(&executor).await;
+// let mut server = TestServer::start(executor.clone()).await;
// let client_a = server.create_client(cx_a, "user_a").await;
// let client_b = server.create_client(cx_b, "user_b").await;
// server
@@ -685,7 +721,10 @@
// .unwrap();
// // Client A opens a file.
-// let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
+// let workspace_a = client_a
+// .build_workspace(&project_a, cx_a)
+// .root(cx_a)
+// .unwrap();
// workspace_a
// .update(cx_a, |workspace, cx| {
// workspace.open_path((worktree_id, "1.txt"), None, true, cx)
@@ -696,7 +735,10 @@
// .unwrap();
// // Client B opens a different file.
-// let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
+// let workspace_b = client_b
+// .build_workspace(&project_b, cx_b)
+// .root(cx_b)
+// .unwrap();
// workspace_b
// .update(cx_b, |workspace, cx| {
// workspace.open_path((worktree_id, "2.txt"), None, true, cx)
@@ -1167,7 +1209,7 @@
// cx_b: &mut TestAppContext,
// ) {
// // 2 clients connect to a server.
-// let mut server = TestServer::start(&executor).await;
+// let mut server = TestServer::start(executor.clone()).await;
// let client_a = server.create_client(cx_a, "user_a").await;
// let client_b = server.create_client(cx_b, "user_b").await;
// server
@@ -1207,8 +1249,17 @@
// .await
// .unwrap();
+// todo!("could be wrong")
+// let mut cx_a = VisualTestContext::from_window(*window_a, cx_a);
+// let cx_a = &mut cx_a;
+// let mut cx_b = VisualTestContext::from_window(*window_b, cx_b);
+// let cx_b = &mut cx_b;
+
// // Client A opens some editors.
-// let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
+// let workspace_a = client_a
+// .build_workspace(&project_a, cx_a)
+// .root(cx_a)
+// .unwrap();
// let _editor_a1 = workspace_a
// .update(cx_a, |workspace, cx| {
// workspace.open_path((worktree_id, "1.txt"), None, true, cx)
@@ -1219,9 +1270,12 @@
// .unwrap();
// // Client B starts following client A.
-// let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
-// let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone());
-// let leader_id = project_b.read_with(cx_b, |project, _| {
+// let workspace_b = client_b
+// .build_workspace(&project_b, cx_b)
+// .root(cx_b)
+// .unwrap();
+// let pane_b = workspace_b.update(cx_b, |workspace, _| workspace.active_pane().clone());
+// let leader_id = project_b.update(cx_b, |project, _| {
// project.collaborators().values().next().unwrap().peer_id
// });
// workspace_b
@@ -1231,10 +1285,10 @@
// .await
// .unwrap();
// assert_eq!(
-// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
// Some(leader_id)
// );
-// let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| {
+// let editor_b2 = workspace_b.update(cx_b, |workspace, cx| {
// workspace
// .active_item(cx)
// .unwrap()
@@ -1245,7 +1299,7 @@
// // When client B moves, it automatically stops following client A.
// editor_b2.update(cx_b, |editor, cx| editor.move_right(&editor::MoveRight, cx));
// assert_eq!(
-// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
// None
// );
@@ -1256,14 +1310,14 @@
// .await
// .unwrap();
// assert_eq!(
-// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
// Some(leader_id)
// );
// // When client B edits, it automatically stops following client A.
// editor_b2.update(cx_b, |editor, cx| editor.insert("X", cx));
// assert_eq!(
-// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
// None
// );
@@ -1274,16 +1328,16 @@
// .await
// .unwrap();
// assert_eq!(
-// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
// Some(leader_id)
// );
// // When client B scrolls, it automatically stops following client A.
// editor_b2.update(cx_b, |editor, cx| {
-// editor.set_scroll_position(vec2f(0., 3.), cx)
+// editor.set_scroll_position(point(0., 3.), cx)
// });
// assert_eq!(
-// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
// None
// );
@@ -1294,7 +1348,7 @@
// .await
// .unwrap();
// assert_eq!(
-// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
// Some(leader_id)
// );
@@ -1303,13 +1357,13 @@
// workspace.split_and_clone(pane_b.clone(), SplitDirection::Right, cx)
// });
// assert_eq!(
-// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
// Some(leader_id)
// );
// workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx));
// assert_eq!(
-// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
// Some(leader_id)
// );
@@ -1321,7 +1375,7 @@
// .await
// .unwrap();
// assert_eq!(
-// workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
+// workspace_b.update(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)),
// None
// );
// }
@@ -1332,7 +1386,7 @@
// cx_a: &mut TestAppContext,
// cx_b: &mut TestAppContext,
// ) {
-// let mut server = TestServer::start(&executor).await;
+// let mut server = TestServer::start(executor.clone()).await;
// let client_a = server.create_client(cx_a, "user_a").await;
// let client_b = server.create_client(cx_b, "user_b").await;
// server
@@ -1345,20 +1399,26 @@
// client_a.fs().insert_tree("/a", json!({})).await;
// let (project_a, _) = client_a.build_local_project("/a", cx_a).await;
-// let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
+// let workspace_a = client_a
+// .build_workspace(&project_a, cx_a)
+// .root(cx_a)
+// .unwrap();
// let project_id = active_call_a
// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
// .await
// .unwrap();
// let project_b = client_b.build_remote_project(project_id, cx_b).await;
-// let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
+// let workspace_b = client_b
+// .build_workspace(&project_b, cx_b)
+// .root(cx_b)
+// .unwrap();
// executor.run_until_parked();
-// let client_a_id = project_b.read_with(cx_b, |project, _| {
+// let client_a_id = project_b.update(cx_b, |project, _| {
// project.collaborators().values().next().unwrap().peer_id
// });
-// let client_b_id = project_a.read_with(cx_a, |project, _| {
+// let client_b_id = project_a.update(cx_a, |project, _| {
// project.collaborators().values().next().unwrap().peer_id
// });
@@ -1370,13 +1430,13 @@
// });
// futures::try_join!(a_follow_b, b_follow_a).unwrap();
-// workspace_a.read_with(cx_a, |workspace, _| {
+// workspace_a.update(cx_a, |workspace, _| {
// assert_eq!(
// workspace.leader_for_pane(workspace.active_pane()),
// Some(client_b_id)
// );
// });
-// workspace_b.read_with(cx_b, |workspace, _| {
+// workspace_b.update(cx_b, |workspace, _| {
// assert_eq!(
// workspace.leader_for_pane(workspace.active_pane()),
// Some(client_a_id)
@@ -1398,7 +1458,7 @@
// // b opens a different file in project 2, a follows b
// // b opens a different file in project 1, a cannot follow b
// // b shares the project, a joins the project and follows b
-// let mut server = TestServer::start(&executor).await;
+// let mut server = TestServer::start(executor.clone()).await;
// let client_a = server.create_client(cx_a, "user_a").await;
// let client_b = server.create_client(cx_b, "user_b").await;
// cx_a.update(editor::init);
@@ -1435,8 +1495,14 @@
// let (project_a, worktree_id_a) = client_a.build_local_project("/a", cx_a).await;
// let (project_b, worktree_id_b) = client_b.build_local_project("/b", cx_b).await;
-// let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a);
-// let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b);
+// let workspace_a = client_a
+// .build_workspace(&project_a, cx_a)
+// .root(cx_a)
+// .unwrap();
+// let workspace_b = client_b
+// .build_workspace(&project_b, cx_b)
+// .root(cx_b)
+// .unwrap();
// cx_a.update(|cx| collab_ui::init(&client_a.app_state, cx));
// cx_b.update(|cx| collab_ui::init(&client_b.app_state, cx));
@@ -1455,6 +1521,12 @@
// .await
// .unwrap();
+// todo!("could be wrong")
+// let mut cx_a = VisualTestContext::from_window(*window_a, cx_a);
+// let cx_a = &mut cx_a;
+// let mut cx_b = VisualTestContext::from_window(*window_b, cx_b);
+// let cx_b = &mut cx_b;
+
// workspace_a
// .update(cx_a, |workspace, cx| {
// workspace.open_path((worktree_id_a, "w.rs"), None, true, cx)
@@ -1476,11 +1548,12 @@
// let workspace_b_project_a = cx_b
// .windows()
// .iter()
-// .max_by_key(|window| window.id())
+// .max_by_key(|window| window.item_id())
// .unwrap()
// .downcast::<Workspace>()
// .unwrap()
-// .root(cx_b);
+// .root(cx_b)
+// .unwrap();
// // assert that b is following a in project a in w.rs
// workspace_b_project_a.update(cx_b, |workspace, cx| {
@@ -1534,7 +1607,7 @@
// workspace.leader_for_pane(workspace.active_pane())
// );
// let item = workspace.active_pane().read(cx).active_item().unwrap();
-// assert_eq!(item.tab_description(0, cx).unwrap(), Cow::Borrowed("x.rs"));
+// assert_eq!(item.tab_description(0, cx).unwrap(), "x.rs".into());
// });
// // b moves to y.rs in b's project, a is still following but can't yet see
@@ -1578,11 +1651,12 @@
// let workspace_a_project_b = cx_a
// .windows()
// .iter()
-// .max_by_key(|window| window.id())
+// .max_by_key(|window| window.item_id())
// .unwrap()
// .downcast::<Workspace>()
// .unwrap()
-// .root(cx_a);
+// .root(cx_a)
+// .unwrap();
// workspace_a_project_b.update(cx_a, |workspace, cx| {
// assert_eq!(workspace.project().read(cx).remote_id(), Some(project_b_id));
@@ -1596,12 +1670,151 @@
// });
// }
+// #[gpui::test]
+// async fn test_following_into_excluded_file(
+// executor: BackgroundExecutor,
+// mut cx_a: &mut TestAppContext,
+// mut cx_b: &mut TestAppContext,
+// ) {
+// let mut server = TestServer::start(executor.clone()).await;
+// let client_a = server.create_client(cx_a, "user_a").await;
+// let client_b = server.create_client(cx_b, "user_b").await;
+// for cx in [&mut cx_a, &mut cx_b] {
+// cx.update(|cx| {
+// cx.update_global::<SettingsStore, _>(|store, cx| {
+// store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
+// project_settings.file_scan_exclusions = Some(vec!["**/.git".to_string()]);
+// });
+// });
+// });
+// }
+// server
+// .create_room(&mut [(&client_a, cx_a), (&client_b, cx_b)])
+// .await;
+// let active_call_a = cx_a.read(ActiveCall::global);
+// let active_call_b = cx_b.read(ActiveCall::global);
+
+// cx_a.update(editor::init);
+// cx_b.update(editor::init);
+
+// client_a
+// .fs()
+// .insert_tree(
+// "/a",
+// json!({
+// ".git": {
+// "COMMIT_EDITMSG": "write your commit message here",
+// },
+// "1.txt": "one\none\none",
+// "2.txt": "two\ntwo\ntwo",
+// "3.txt": "three\nthree\nthree",
+// }),
+// )
+// .await;
+// let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
+// active_call_a
+// .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx))
+// .await
+// .unwrap();
+
+// let project_id = active_call_a
+// .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx))
+// .await
+// .unwrap();
+// let project_b = client_b.build_remote_project(project_id, cx_b).await;
+// active_call_b
+// .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx))
+// .await
+// .unwrap();
+
+// let window_a = client_a.build_workspace(&project_a, cx_a);
+// let workspace_a = window_a.root(cx_a).unwrap();
+// let peer_id_a = client_a.peer_id().unwrap();
+// let window_b = client_b.build_workspace(&project_b, cx_b);
+// let workspace_b = window_b.root(cx_b).unwrap();
+
+// todo!("could be wrong")
+// let mut cx_a = VisualTestContext::from_window(*window_a, cx_a);
+// let cx_a = &mut cx_a;
+// let mut cx_b = VisualTestContext::from_window(*window_b, cx_b);
+// let cx_b = &mut cx_b;
+
+// // Client A opens editors for a regular file and an excluded file.
+// let editor_for_regular = workspace_a
+// .update(cx_a, |workspace, cx| {
+// workspace.open_path((worktree_id, "1.txt"), None, true, cx)
+// })
+// .await
+// .unwrap()
+// .downcast::<Editor>()
+// .unwrap();
+// let editor_for_excluded_a = workspace_a
+// .update(cx_a, |workspace, cx| {
+// workspace.open_path((worktree_id, ".git/COMMIT_EDITMSG"), None, true, cx)
+// })
+// .await
+// .unwrap()
+// .downcast::<Editor>()
+// .unwrap();
+
+// // Client A updates their selections in those editors
+// editor_for_regular.update(cx_a, |editor, cx| {
+// editor.handle_input("a", cx);
+// editor.handle_input("b", cx);
+// editor.handle_input("c", cx);
+// editor.select_left(&Default::default(), cx);
+// assert_eq!(editor.selections.ranges(cx), vec![3..2]);
+// });
+// editor_for_excluded_a.update(cx_a, |editor, cx| {
+// editor.select_all(&Default::default(), cx);
+// editor.handle_input("new commit message", cx);
+// editor.select_left(&Default::default(), cx);
+// assert_eq!(editor.selections.ranges(cx), vec![18..17]);
+// });
+
+// // When client B starts following client A, currently visible file is replicated
+// workspace_b
+// .update(cx_b, |workspace, cx| {
+// workspace.follow(peer_id_a, cx).unwrap()
+// })
+// .await
+// .unwrap();
+
+// let editor_for_excluded_b = workspace_b.update(cx_b, |workspace, cx| {
+// workspace
+// .active_item(cx)
+// .unwrap()
+// .downcast::<Editor>()
+// .unwrap()
+// });
+// assert_eq!(
+// cx_b.read(|cx| editor_for_excluded_b.project_path(cx)),
+// Some((worktree_id, ".git/COMMIT_EDITMSG").into())
+// );
+// assert_eq!(
+// editor_for_excluded_b.update(cx_b, |editor, cx| editor.selections.ranges(cx)),
+// vec![18..17]
+// );
+
+// // Changes from B to the excluded file are replicated in A's editor
+// editor_for_excluded_b.update(cx_b, |editor, cx| {
+// editor.handle_input("\nCo-Authored-By: B <b@b.b>", cx);
+// });
+// executor.run_until_parked();
+// editor_for_excluded_a.update(cx_a, |editor, cx| {
+// assert_eq!(
+// editor.text(cx),
+// "new commit messag\nCo-Authored-By: B <b@b.b>"
+// );
+// });
+// }
+
// fn visible_push_notifications(
// cx: &mut TestAppContext,
-// ) -> Vec<gpui::ViewHandle<ProjectSharedNotification>> {
+// ) -> Vec<gpui::View<ProjectSharedNotification>> {
// let mut ret = Vec::new();
// for window in cx.windows() {
-// window.read_with(cx, |window| {
+// window.update(cx, |window| {
// if let Some(handle) = window
// .root_view()
// .clone()
@@ -1645,8 +1858,8 @@
// })
// }
-// fn pane_summaries(workspace: &ViewHandle<Workspace>, cx: &mut TestAppContext) -> Vec<PaneSummary> {
-// workspace.read_with(cx, |workspace, cx| {
+// fn pane_summaries(workspace: &View<Workspace>, cx: &mut WindowContext<'_>) -> Vec<PaneSummary> {
+// workspace.update(cx, |workspace, cx| {
// let active_pane = workspace.active_pane();
// workspace
// .panes()
@@ -2781,11 +2781,10 @@ async fn test_fs_operations(
let entry = project_b
.update(cx_b, |project, cx| {
- project
- .create_entry((worktree_id, "c.txt"), false, cx)
- .unwrap()
+ project.create_entry((worktree_id, "c.txt"), false, cx)
})
.await
+ .unwrap()
.unwrap();
worktree_a.read_with(cx_a, |worktree, _| {
@@ -2812,8 +2811,8 @@ async fn test_fs_operations(
.update(cx_b, |project, cx| {
project.rename_entry(entry.id, Path::new("d.txt"), cx)
})
- .unwrap()
.await
+ .unwrap()
.unwrap();
worktree_a.read_with(cx_a, |worktree, _| {
@@ -2838,11 +2837,10 @@ async fn test_fs_operations(
let dir_entry = project_b
.update(cx_b, |project, cx| {
- project
- .create_entry((worktree_id, "DIR"), true, cx)
- .unwrap()
+ project.create_entry((worktree_id, "DIR"), true, cx)
})
.await
+ .unwrap()
.unwrap();
worktree_a.read_with(cx_a, |worktree, _| {
@@ -2867,27 +2865,24 @@ async fn test_fs_operations(
project_b
.update(cx_b, |project, cx| {
- project
- .create_entry((worktree_id, "DIR/e.txt"), false, cx)
- .unwrap()
+ project.create_entry((worktree_id, "DIR/e.txt"), false, cx)
})
.await
+ .unwrap()
.unwrap();
project_b
.update(cx_b, |project, cx| {
- project
- .create_entry((worktree_id, "DIR/SUBDIR"), true, cx)
- .unwrap()
+ project.create_entry((worktree_id, "DIR/SUBDIR"), true, cx)
})
.await
+ .unwrap()
.unwrap();
project_b
.update(cx_b, |project, cx| {
- project
- .create_entry((worktree_id, "DIR/SUBDIR/f.txt"), false, cx)
- .unwrap()
+ project.create_entry((worktree_id, "DIR/SUBDIR/f.txt"), false, cx)
})
.await
+ .unwrap()
.unwrap();
worktree_a.read_with(cx_a, |worktree, _| {
@@ -2928,11 +2923,10 @@ async fn test_fs_operations(
project_b
.update(cx_b, |project, cx| {
- project
- .copy_entry(entry.id, Path::new("f.txt"), cx)
- .unwrap()
+ project.copy_entry(entry.id, Path::new("f.txt"), cx)
})
.await
+ .unwrap()
.unwrap();
worktree_a.read_with(cx_a, |worktree, _| {
@@ -665,7 +665,6 @@ impl RandomizedTest for ProjectCollaborationTest {
ensure_project_shared(&project, client, cx).await;
project
.update(cx, |p, cx| p.create_entry(project_path, is_dir, cx))
- .unwrap()
.await?;
}
@@ -1121,20 +1121,22 @@ impl Project {
project_path: impl Into<ProjectPath>,
is_directory: bool,
cx: &mut ModelContext<Self>,
- ) -> Option<Task<Result<Entry>>> {
+ ) -> Task<Result<Option<Entry>>> {
let project_path = project_path.into();
- let worktree = self.worktree_for_id(project_path.worktree_id, cx)?;
+ let Some(worktree) = self.worktree_for_id(project_path.worktree_id, cx) else {
+ return Task::ready(Ok(None));
+ };
if self.is_local() {
- Some(worktree.update(cx, |worktree, cx| {
+ 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 {
+ cx.spawn_weak(|_, mut cx| async move {
let response = client
.request(proto::CreateProjectEntry {
worktree_id: project_path.worktree_id.to_proto(),
@@ -1143,19 +1145,20 @@ impl Project {
is_directory,
})
.await?;
- let entry = response
- .entry
- .ok_or_else(|| anyhow!("missing entry in response"))?;
- worktree
- .update(&mut cx, |worktree, cx| {
- worktree.as_remote_mut().unwrap().insert_entry(
- entry,
- response.worktree_scan_id as usize,
- cx,
- )
- })
- .await
- }))
+ match response.entry {
+ Some(entry) => worktree
+ .update(&mut cx, |worktree, cx| {
+ worktree.as_remote_mut().unwrap().insert_entry(
+ entry,
+ response.worktree_scan_id as usize,
+ cx,
+ )
+ })
+ .await
+ .map(Some),
+ None => Ok(None),
+ }
+ })
}
}
@@ -1164,8 +1167,10 @@ impl Project {
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)?;
+ ) -> Task<Result<Option<Entry>>> {
+ let Some(worktree) = self.worktree_for_entry(entry_id, cx) else {
+ return Task::ready(Ok(None));
+ };
let new_path = new_path.into();
if self.is_local() {
worktree.update(cx, |worktree, cx| {
@@ -1178,7 +1183,7 @@ impl Project {
let client = self.client.clone();
let project_id = self.remote_id().unwrap();
- Some(cx.spawn_weak(|_, mut cx| async move {
+ cx.spawn_weak(|_, mut cx| async move {
let response = client
.request(proto::CopyProjectEntry {
project_id,
@@ -1186,19 +1191,20 @@ impl Project {
new_path: new_path.to_string_lossy().into(),
})
.await?;
- let entry = response
- .entry
- .ok_or_else(|| anyhow!("missing entry in response"))?;
- worktree
- .update(&mut cx, |worktree, cx| {
- worktree.as_remote_mut().unwrap().insert_entry(
- entry,
- response.worktree_scan_id as usize,
- cx,
- )
- })
- .await
- }))
+ match response.entry {
+ Some(entry) => worktree
+ .update(&mut cx, |worktree, cx| {
+ worktree.as_remote_mut().unwrap().insert_entry(
+ entry,
+ response.worktree_scan_id as usize,
+ cx,
+ )
+ })
+ .await
+ .map(Some),
+ None => Ok(None),
+ }
+ })
}
}
@@ -1207,8 +1213,10 @@ impl Project {
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)?;
+ ) -> Task<Result<Option<Entry>>> {
+ let Some(worktree) = self.worktree_for_entry(entry_id, cx) else {
+ return Task::ready(Ok(None));
+ };
let new_path = new_path.into();
if self.is_local() {
worktree.update(cx, |worktree, cx| {
@@ -1221,7 +1229,7 @@ impl Project {
let client = self.client.clone();
let project_id = self.remote_id().unwrap();
- Some(cx.spawn_weak(|_, mut cx| async move {
+ cx.spawn_weak(|_, mut cx| async move {
let response = client
.request(proto::RenameProjectEntry {
project_id,
@@ -1229,19 +1237,20 @@ impl Project {
new_path: new_path.to_string_lossy().into(),
})
.await?;
- let entry = response
- .entry
- .ok_or_else(|| anyhow!("missing entry in response"))?;
- worktree
- .update(&mut cx, |worktree, cx| {
- worktree.as_remote_mut().unwrap().insert_entry(
- entry,
- response.worktree_scan_id as usize,
- cx,
- )
- })
- .await
- }))
+ match response.entry {
+ Some(entry) => worktree
+ .update(&mut cx, |worktree, cx| {
+ worktree.as_remote_mut().unwrap().insert_entry(
+ entry,
+ response.worktree_scan_id as usize,
+ cx,
+ )
+ })
+ .await
+ .map(Some),
+ None => Ok(None),
+ }
+ })
}
}
@@ -1658,19 +1667,15 @@ impl Project {
pub fn open_path(
&mut self,
- path: impl Into<ProjectPath>,
+ path: ProjectPath,
cx: &mut ModelContext<Self>,
- ) -> Task<Result<(ProjectEntryId, AnyModelHandle)>> {
- let project_path = path.into();
- let task = self.open_buffer(project_path.clone(), cx);
+ ) -> Task<Result<(Option<ProjectEntryId>, AnyModelHandle)>> {
+ let task = self.open_buffer(path.clone(), cx);
cx.spawn_weak(|_, cx| async move {
let buffer = task.await?;
- let project_entry_id = buffer
- .read_with(&cx, |buffer, cx| {
- File::from_dyn(buffer.file()).and_then(|file| file.project_entry_id(cx))
- })
- .with_context(|| format!("no project entry for {project_path:?}"))?;
-
+ let project_entry_id = buffer.read_with(&cx, |buffer, cx| {
+ File::from_dyn(buffer.file()).and_then(|file| file.project_entry_id(cx))
+ });
let buffer: &AnyModelHandle = &buffer;
Ok((project_entry_id, buffer.clone()))
})
@@ -1985,8 +1990,10 @@ impl Project {
remote_id,
);
- self.local_buffer_ids_by_entry_id
- .insert(file.entry_id, remote_id);
+ if let Some(entry_id) = file.entry_id {
+ self.local_buffer_ids_by_entry_id
+ .insert(entry_id, remote_id);
+ }
}
}
@@ -2441,24 +2448,25 @@ impl Project {
return None;
};
- match self.local_buffer_ids_by_entry_id.get(&file.entry_id) {
- Some(_) => {
- return None;
- }
- None => {
- let remote_id = buffer.read(cx).remote_id();
- self.local_buffer_ids_by_entry_id
- .insert(file.entry_id, remote_id);
-
- self.local_buffer_ids_by_path.insert(
- ProjectPath {
- worktree_id: file.worktree_id(cx),
- path: file.path.clone(),
- },
- remote_id,
- );
+ let remote_id = buffer.read(cx).remote_id();
+ if let Some(entry_id) = file.entry_id {
+ match self.local_buffer_ids_by_entry_id.get(&entry_id) {
+ Some(_) => {
+ return None;
+ }
+ None => {
+ self.local_buffer_ids_by_entry_id
+ .insert(entry_id, remote_id);
+ }
}
- }
+ };
+ self.local_buffer_ids_by_path.insert(
+ ProjectPath {
+ worktree_id: file.worktree_id(cx),
+ path: file.path.clone(),
+ },
+ remote_id,
+ );
}
_ => {}
}
@@ -5776,11 +5784,6 @@ impl Project {
while let Some(ignored_abs_path) =
ignored_paths_to_process.pop_front()
{
- if !query.file_matches(Some(&ignored_abs_path))
- || snapshot.is_path_excluded(&ignored_abs_path)
- {
- continue;
- }
if let Some(fs_metadata) = fs
.metadata(&ignored_abs_path)
.await
@@ -5808,6 +5811,13 @@ impl Project {
}
}
} else if !fs_metadata.is_symlink {
+ if !query.file_matches(Some(&ignored_abs_path))
+ || snapshot.is_path_excluded(
+ ignored_entry.path.to_path_buf(),
+ )
+ {
+ continue;
+ }
let matches = if let Some(file) = fs
.open_sync(&ignored_abs_path)
.await
@@ -6208,10 +6218,13 @@ impl Project {
return;
}
- let new_file = if let Some(entry) = snapshot.entry_for_id(old_file.entry_id) {
+ let new_file = if let Some(entry) = old_file
+ .entry_id
+ .and_then(|entry_id| snapshot.entry_for_id(entry_id))
+ {
File {
is_local: true,
- entry_id: entry.id,
+ entry_id: Some(entry.id),
mtime: entry.mtime,
path: entry.path.clone(),
worktree: worktree_handle.clone(),
@@ -6220,7 +6233,7 @@ impl Project {
} else if let Some(entry) = snapshot.entry_for_path(old_file.path().as_ref()) {
File {
is_local: true,
- entry_id: entry.id,
+ entry_id: Some(entry.id),
mtime: entry.mtime,
path: entry.path.clone(),
worktree: worktree_handle.clone(),
@@ -6250,10 +6263,12 @@ impl Project {
);
}
- if new_file.entry_id != *entry_id {
+ if new_file.entry_id != Some(*entry_id) {
self.local_buffer_ids_by_entry_id.remove(entry_id);
- self.local_buffer_ids_by_entry_id
- .insert(new_file.entry_id, buffer_id);
+ if let Some(entry_id) = new_file.entry_id {
+ self.local_buffer_ids_by_entry_id
+ .insert(entry_id, buffer_id);
+ }
}
if new_file != *old_file {
@@ -6816,7 +6831,7 @@ impl Project {
})
.await?;
Ok(proto::ProjectEntryResponse {
- entry: Some((&entry).into()),
+ entry: entry.as_ref().map(|e| e.into()),
worktree_scan_id: worktree_scan_id as u64,
})
}
@@ -6840,11 +6855,10 @@ impl Project {
.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()),
+ entry: entry.as_ref().map(|e| e.into()),
worktree_scan_id: worktree_scan_id as u64,
})
}
@@ -6868,11 +6882,10 @@ impl Project {
.as_local_mut()
.unwrap()
.copy_entry(entry_id, new_path, cx)
- .ok_or_else(|| anyhow!("invalid entry"))
- })?
+ })
.await?;
Ok(proto::ProjectEntryResponse {
- entry: Some((&entry).into()),
+ entry: entry.as_ref().map(|e| e.into()),
worktree_scan_id: worktree_scan_id as u64,
})
}
@@ -4050,6 +4050,94 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
);
}
+#[gpui::test]
+async fn test_search_in_gitignored_dirs(cx: &mut gpui::TestAppContext) {
+ init_test(cx);
+
+ let fs = FakeFs::new(cx.background());
+ fs.insert_tree(
+ "/dir",
+ json!({
+ ".git": {},
+ ".gitignore": "**/target\n/node_modules\n",
+ "target": {
+ "index.txt": "index_key:index_value"
+ },
+ "node_modules": {
+ "eslint": {
+ "index.ts": "const eslint_key = 'eslint value'",
+ "package.json": r#"{ "some_key": "some value" }"#,
+ },
+ "prettier": {
+ "index.ts": "const prettier_key = 'prettier value'",
+ "package.json": r#"{ "other_key": "other value" }"#,
+ },
+ },
+ "package.json": r#"{ "main_key": "main value" }"#,
+ }),
+ )
+ .await;
+ let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
+
+ let query = "key";
+ assert_eq!(
+ search(
+ &project,
+ SearchQuery::text(query, false, false, false, Vec::new(), Vec::new()).unwrap(),
+ cx
+ )
+ .await
+ .unwrap(),
+ HashMap::from_iter([("package.json".to_string(), vec![8..11])]),
+ "Only one non-ignored file should have the query"
+ );
+
+ assert_eq!(
+ search(
+ &project,
+ SearchQuery::text(query, false, false, true, Vec::new(), Vec::new()).unwrap(),
+ cx
+ )
+ .await
+ .unwrap(),
+ HashMap::from_iter([
+ ("package.json".to_string(), vec![8..11]),
+ ("target/index.txt".to_string(), vec![6..9]),
+ (
+ "node_modules/prettier/package.json".to_string(),
+ vec![9..12]
+ ),
+ ("node_modules/prettier/index.ts".to_string(), vec![15..18]),
+ ("node_modules/eslint/index.ts".to_string(), vec![13..16]),
+ ("node_modules/eslint/package.json".to_string(), vec![8..11]),
+ ]),
+ "Unrestricted search with ignored directories should find every file with the query"
+ );
+
+ assert_eq!(
+ search(
+ &project,
+ SearchQuery::text(
+ query,
+ false,
+ false,
+ true,
+ vec![PathMatcher::new("node_modules/prettier/**").unwrap()],
+ vec![PathMatcher::new("*.ts").unwrap()],
+ )
+ .unwrap(),
+ cx
+ )
+ .await
+ .unwrap(),
+ HashMap::from_iter([(
+ "node_modules/prettier/package.json".to_string(),
+ vec![9..12]
+ )]),
+ "With search including ignored prettier directory and excluding TS files, only one file should be found"
+ );
+}
+
#[test]
fn test_glob_literal_prefix() {
assert_eq!(glob_literal_prefix("**/*.js"), "");
@@ -371,15 +371,25 @@ impl SearchQuery {
pub fn file_matches(&self, file_path: Option<&Path>) -> bool {
match file_path {
Some(file_path) => {
- !self
- .files_to_exclude()
- .iter()
- .any(|exclude_glob| exclude_glob.is_match(file_path))
- && (self.files_to_include().is_empty()
+ let mut path = file_path.to_path_buf();
+ loop {
+ if self
+ .files_to_exclude()
+ .iter()
+ .any(|exclude_glob| exclude_glob.is_match(&path))
+ {
+ return false;
+ } else if self.files_to_include().is_empty()
|| self
.files_to_include()
.iter()
- .any(|include_glob| include_glob.is_match(file_path)))
+ .any(|include_glob| include_glob.is_match(&path))
+ {
+ return true;
+ } else if !path.pop() {
+ return false;
+ }
+ }
}
None => self.files_to_include().is_empty(),
}
@@ -960,8 +960,6 @@ impl LocalWorktree {
cx.spawn(|this, cx| async move {
let text = fs.load(&abs_path).await?;
- let entry = entry.await?;
-
let mut index_task = None;
let snapshot = this.read_with(&cx, |this, _| this.as_local().unwrap().snapshot());
if let Some(repo) = snapshot.repository_for_path(&path) {
@@ -981,18 +979,43 @@ impl LocalWorktree {
None
};
- Ok((
- File {
- entry_id: entry.id,
- worktree: this,
- path: entry.path,
- mtime: entry.mtime,
- is_local: true,
- is_deleted: false,
- },
- text,
- diff_base,
- ))
+ match entry.await? {
+ Some(entry) => Ok((
+ File {
+ entry_id: Some(entry.id),
+ worktree: this,
+ path: entry.path,
+ mtime: entry.mtime,
+ is_local: true,
+ is_deleted: false,
+ },
+ text,
+ diff_base,
+ )),
+ None => {
+ let metadata = fs
+ .metadata(&abs_path)
+ .await
+ .with_context(|| {
+ format!("Loading metadata for excluded file {abs_path:?}")
+ })?
+ .with_context(|| {
+ format!("Excluded file {abs_path:?} got removed during loading")
+ })?;
+ Ok((
+ File {
+ entry_id: None,
+ worktree: this,
+ path,
+ mtime: metadata.mtime,
+ is_local: true,
+ is_deleted: false,
+ },
+ text,
+ diff_base,
+ ))
+ }
+ }
})
}
@@ -1013,17 +1036,37 @@ impl LocalWorktree {
let text = buffer.as_rope().clone();
let fingerprint = text.fingerprint();
let version = buffer.version();
- let save = self.write_file(path, text, buffer.line_ending(), cx);
+ let save = self.write_file(path.as_ref(), text, buffer.line_ending(), cx);
+ let fs = Arc::clone(&self.fs);
+ let abs_path = self.absolutize(&path);
cx.as_mut().spawn(|mut cx| async move {
let entry = save.await?;
+ let (entry_id, mtime, path) = match entry {
+ Some(entry) => (Some(entry.id), entry.mtime, entry.path),
+ None => {
+ let metadata = fs
+ .metadata(&abs_path)
+ .await
+ .with_context(|| {
+ format!(
+ "Fetching metadata after saving the excluded buffer {abs_path:?}"
+ )
+ })?
+ .with_context(|| {
+ format!("Excluded buffer {path:?} got removed during saving")
+ })?;
+ (None, metadata.mtime, path)
+ }
+ };
+
if has_changed_file {
let new_file = Arc::new(File {
- entry_id: entry.id,
+ entry_id,
worktree: handle,
- path: entry.path,
- mtime: entry.mtime,
+ path,
+ mtime,
is_local: true,
is_deleted: false,
});
@@ -1049,13 +1092,13 @@ impl LocalWorktree {
project_id,
buffer_id,
version: serialize_version(&version),
- mtime: Some(entry.mtime.into()),
+ mtime: Some(mtime.into()),
fingerprint: serialize_fingerprint(fingerprint),
})?;
}
buffer_handle.update(&mut cx, |buffer, cx| {
- buffer.did_save(version.clone(), fingerprint, entry.mtime, cx);
+ buffer.did_save(version.clone(), fingerprint, mtime, cx);
});
Ok(())
@@ -1080,7 +1123,7 @@ impl LocalWorktree {
path: impl Into<Arc<Path>>,
is_dir: bool,
cx: &mut ModelContext<Worktree>,
- ) -> Task<Result<Entry>> {
+ ) -> Task<Result<Option<Entry>>> {
let path = path.into();
let lowest_ancestor = self.lowest_ancestor(&path);
let abs_path = self.absolutize(&path);
@@ -1097,7 +1140,7 @@ impl LocalWorktree {
cx.spawn(|this, mut cx| async move {
write.await?;
let (result, refreshes) = this.update(&mut cx, |this, cx| {
- let mut refreshes = Vec::<Task<anyhow::Result<Entry>>>::new();
+ let mut refreshes = Vec::new();
let refresh_paths = path.strip_prefix(&lowest_ancestor).unwrap();
for refresh_path in refresh_paths.ancestors() {
if refresh_path == Path::new("") {
@@ -1124,14 +1167,14 @@ impl LocalWorktree {
})
}
- pub fn write_file(
+ pub(crate) fn write_file(
&self,
path: impl Into<Arc<Path>>,
text: Rope,
line_ending: LineEnding,
cx: &mut ModelContext<Worktree>,
- ) -> Task<Result<Entry>> {
- let path = path.into();
+ ) -> Task<Result<Option<Entry>>> {
+ let path: Arc<Path> = path.into();
let abs_path = self.absolutize(&path);
let fs = self.fs.clone();
let write = cx
@@ -1190,8 +1233,11 @@ impl LocalWorktree {
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();
+ ) -> Task<Result<Option<Entry>>> {
+ let old_path = match self.entry_for_id(entry_id) {
+ Some(entry) => entry.path.clone(),
+ None => return Task::ready(Ok(None)),
+ };
let new_path = new_path.into();
let abs_old_path = self.absolutize(&old_path);
let abs_new_path = self.absolutize(&new_path);
@@ -1201,7 +1247,7 @@ impl LocalWorktree {
.await
});
- Some(cx.spawn(|this, mut cx| async move {
+ cx.spawn(|this, mut cx| async move {
rename.await?;
this.update(&mut cx, |this, cx| {
this.as_local_mut()
@@ -1209,7 +1255,7 @@ impl LocalWorktree {
.refresh_entry(new_path.clone(), Some(old_path), cx)
})
.await
- }))
+ })
}
pub fn copy_entry(
@@ -1217,8 +1263,11 @@ impl LocalWorktree {
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();
+ ) -> Task<Result<Option<Entry>>> {
+ let old_path = match self.entry_for_id(entry_id) {
+ Some(entry) => entry.path.clone(),
+ None => return Task::ready(Ok(None)),
+ };
let new_path = new_path.into();
let abs_old_path = self.absolutize(&old_path);
let abs_new_path = self.absolutize(&new_path);
@@ -1233,7 +1282,7 @@ impl LocalWorktree {
.await
});
- Some(cx.spawn(|this, mut cx| async move {
+ cx.spawn(|this, mut cx| async move {
copy.await?;
this.update(&mut cx, |this, cx| {
this.as_local_mut()
@@ -1241,7 +1290,7 @@ impl LocalWorktree {
.refresh_entry(new_path.clone(), None, cx)
})
.await
- }))
+ })
}
pub fn expand_entry(
@@ -1277,7 +1326,10 @@ impl LocalWorktree {
path: Arc<Path>,
old_path: Option<Arc<Path>>,
cx: &mut ModelContext<Worktree>,
- ) -> Task<Result<Entry>> {
+ ) -> Task<Result<Option<Entry>>> {
+ if self.is_path_excluded(path.to_path_buf()) {
+ return Task::ready(Ok(None));
+ }
let paths = if let Some(old_path) = old_path.as_ref() {
vec![old_path.clone(), path.clone()]
} else {
@@ -1286,13 +1338,15 @@ impl LocalWorktree {
let mut refresh = self.refresh_entries_for_paths(paths);
cx.spawn_weak(move |this, mut cx| async move {
refresh.recv().await;
- this.upgrade(&cx)
+ let new_entry = this
+ .upgrade(&cx)
.ok_or_else(|| anyhow!("worktree was dropped"))?
.update(&mut cx, |this, _| {
this.entry_for_path(path)
.cloned()
.ok_or_else(|| anyhow!("failed to read path after update"))
- })
+ })?;
+ Ok(Some(new_entry))
})
}
@@ -2226,10 +2280,19 @@ impl LocalSnapshot {
paths
}
- pub fn is_path_excluded(&self, abs_path: &Path) -> bool {
- self.file_scan_exclusions
- .iter()
- .any(|exclude_matcher| exclude_matcher.is_match(abs_path))
+ pub fn is_path_excluded(&self, mut path: PathBuf) -> bool {
+ loop {
+ if self
+ .file_scan_exclusions
+ .iter()
+ .any(|exclude_matcher| exclude_matcher.is_match(&path))
+ {
+ return true;
+ }
+ if !path.pop() {
+ return false;
+ }
+ }
}
}
@@ -2458,8 +2521,7 @@ impl BackgroundScannerState {
ids_to_preserve.insert(work_directory_id);
} else {
let git_dir_abs_path = snapshot.abs_path().join(&entry.git_dir_path);
- let git_dir_excluded = snapshot.is_path_excluded(&entry.git_dir_path)
- || snapshot.is_path_excluded(&git_dir_abs_path);
+ let git_dir_excluded = snapshot.is_path_excluded(entry.git_dir_path.to_path_buf());
if git_dir_excluded
&& !matches!(smol::block_on(fs.metadata(&git_dir_abs_path)), Ok(None))
{
@@ -2666,7 +2728,7 @@ pub struct File {
pub worktree: ModelHandle<Worktree>,
pub path: Arc<Path>,
pub mtime: SystemTime,
- pub(crate) entry_id: ProjectEntryId,
+ pub(crate) entry_id: Option<ProjectEntryId>,
pub(crate) is_local: bool,
pub(crate) is_deleted: bool,
}
@@ -2735,7 +2797,7 @@ impl language::File for File {
fn to_proto(&self) -> rpc::proto::File {
rpc::proto::File {
worktree_id: self.worktree.id() as u64,
- entry_id: self.entry_id.to_proto(),
+ entry_id: self.entry_id.map(|id| id.to_proto()),
path: self.path.to_string_lossy().into(),
mtime: Some(self.mtime.into()),
is_deleted: self.is_deleted,
@@ -2793,7 +2855,7 @@ impl File {
worktree,
path: entry.path.clone(),
mtime: entry.mtime,
- entry_id: entry.id,
+ entry_id: Some(entry.id),
is_local: true,
is_deleted: false,
})
@@ -2818,7 +2880,7 @@ impl File {
worktree,
path: Path::new(&proto.path).into(),
mtime: proto.mtime.ok_or_else(|| anyhow!("no timestamp"))?.into(),
- entry_id: ProjectEntryId::from_proto(proto.entry_id),
+ entry_id: proto.entry_id.map(ProjectEntryId::from_proto),
is_local: false,
is_deleted: proto.is_deleted,
})
@@ -2836,7 +2898,7 @@ impl File {
if self.is_deleted {
None
} else {
- Some(self.entry_id)
+ self.entry_id
}
}
}
@@ -3338,16 +3400,7 @@ impl BackgroundScanner {
return false;
}
- // FS events may come for files which parent directory is excluded, need to check ignore those.
- let mut path_to_test = abs_path.clone();
- let mut excluded_file_event = snapshot.is_path_excluded(abs_path)
- || snapshot.is_path_excluded(&relative_path);
- while !excluded_file_event && path_to_test.pop() {
- if snapshot.is_path_excluded(&path_to_test) {
- excluded_file_event = true;
- }
- }
- if excluded_file_event {
+ if snapshot.is_path_excluded(relative_path.to_path_buf()) {
if !is_git_related {
log::debug!("ignoring FS event for excluded path {relative_path:?}");
}
@@ -3531,7 +3584,7 @@ impl BackgroundScanner {
let state = self.state.lock();
let snapshot = &state.snapshot;
root_abs_path = snapshot.abs_path().clone();
- if snapshot.is_path_excluded(&job.abs_path) {
+ if snapshot.is_path_excluded(job.path.to_path_buf()) {
log::error!("skipping excluded directory {:?}", job.path);
return Ok(());
}
@@ -3603,8 +3656,8 @@ impl BackgroundScanner {
{
let mut state = self.state.lock();
- if state.snapshot.is_path_excluded(&child_abs_path) {
- let relative_path = job.path.join(child_name);
+ let relative_path = job.path.join(child_name);
+ if state.snapshot.is_path_excluded(relative_path.clone()) {
log::debug!("skipping excluded child entry {relative_path:?}");
state.remove_path(&relative_path);
continue;
@@ -1052,11 +1052,12 @@ async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) {
&[
".git/HEAD",
".git/foo",
+ "node_modules",
"node_modules/.DS_Store",
"node_modules/prettier",
"node_modules/prettier/package.json",
],
- &["target", "node_modules"],
+ &["target"],
&[
".DS_Store",
"src/.DS_Store",
@@ -1106,6 +1107,7 @@ async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) {
".git/HEAD",
".git/foo",
".git/new_file",
+ "node_modules",
"node_modules/.DS_Store",
"node_modules/prettier",
"node_modules/prettier/package.json",
@@ -1114,7 +1116,7 @@ async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) {
"build_output/new_file",
"test_output/new_file",
],
- &["target", "node_modules", "test_output"],
+ &["target", "test_output"],
&[
".DS_Store",
"src/.DS_Store",
@@ -1174,6 +1176,7 @@ async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
.create_entry("a/e".as_ref(), true, cx)
})
.await
+ .unwrap()
.unwrap();
assert!(entry.is_dir());
@@ -1222,6 +1225,7 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
.create_entry("a/b/c/d.txt".as_ref(), false, cx)
})
.await
+ .unwrap()
.unwrap();
assert!(entry.is_file());
@@ -1257,6 +1261,7 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
.create_entry("a/b/c/d.txt".as_ref(), false, cx)
})
.await
+ .unwrap()
.unwrap();
assert!(entry.is_file());
@@ -1275,6 +1280,7 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
.create_entry("a/b/c/e.txt".as_ref(), false, cx)
})
.await
+ .unwrap()
.unwrap();
assert!(entry.is_file());
@@ -1291,6 +1297,7 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
.create_entry("d/e/f/g.txt".as_ref(), false, cx)
})
.await
+ .unwrap()
.unwrap();
assert!(entry.is_file());
@@ -1616,14 +1623,14 @@ fn randomly_mutate_worktree(
entry.id.0,
new_path
);
- let task = worktree.rename_entry(entry.id, new_path, cx).unwrap();
+ let task = worktree.rename_entry(entry.id, new_path, cx);
cx.foreground().spawn(async move {
- task.await?;
+ task.await?.unwrap();
Ok(())
})
}
_ => {
- let task = if entry.is_dir() {
+ if entry.is_dir() {
let child_path = entry.path.join(random_filename(rng));
let is_dir = rng.gen_bool(0.3);
log::info!(
@@ -1631,15 +1638,20 @@ fn randomly_mutate_worktree(
if is_dir { "dir" } else { "file" },
child_path,
);
- worktree.create_entry(child_path, is_dir, cx)
+ let task = worktree.create_entry(child_path, is_dir, cx);
+ cx.foreground().spawn(async move {
+ task.await?;
+ Ok(())
+ })
} else {
log::info!("overwriting file {:?} ({})", entry.path, entry.id.0);
- worktree.write_file(entry.path.clone(), "".into(), Default::default(), cx)
- };
- cx.foreground().spawn(async move {
- task.await?;
- Ok(())
- })
+ let task =
+ worktree.write_file(entry.path.clone(), "".into(), Default::default(), cx);
+ cx.foreground().spawn(async move {
+ task.await?;
+ Ok(())
+ })
+ }
}
}
}
@@ -1151,20 +1151,22 @@ impl Project {
project_path: impl Into<ProjectPath>,
is_directory: bool,
cx: &mut ModelContext<Self>,
- ) -> Option<Task<Result<Entry>>> {
+ ) -> Task<Result<Option<Entry>>> {
let project_path = project_path.into();
- let worktree = self.worktree_for_id(project_path.worktree_id, cx)?;
+ let Some(worktree) = self.worktree_for_id(project_path.worktree_id, cx) else {
+ return Task::ready(Ok(None));
+ };
if self.is_local() {
- Some(worktree.update(cx, |worktree, cx| {
+ 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(move |_, mut cx| async move {
+ cx.spawn(move |_, mut cx| async move {
let response = client
.request(proto::CreateProjectEntry {
worktree_id: project_path.worktree_id.to_proto(),
@@ -1173,19 +1175,20 @@ impl Project {
is_directory,
})
.await?;
- let entry = response
- .entry
- .ok_or_else(|| anyhow!("missing entry in response"))?;
- worktree
- .update(&mut cx, |worktree, cx| {
- worktree.as_remote_mut().unwrap().insert_entry(
- entry,
- response.worktree_scan_id as usize,
- cx,
- )
- })?
- .await
- }))
+ match response.entry {
+ Some(entry) => worktree
+ .update(&mut cx, |worktree, cx| {
+ worktree.as_remote_mut().unwrap().insert_entry(
+ entry,
+ response.worktree_scan_id as usize,
+ cx,
+ )
+ })?
+ .await
+ .map(Some),
+ None => Ok(None),
+ }
+ })
}
}
@@ -1194,8 +1197,10 @@ impl Project {
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)?;
+ ) -> Task<Result<Option<Entry>>> {
+ let Some(worktree) = self.worktree_for_entry(entry_id, cx) else {
+ return Task::ready(Ok(None));
+ };
let new_path = new_path.into();
if self.is_local() {
worktree.update(cx, |worktree, cx| {
@@ -1208,7 +1213,7 @@ impl Project {
let client = self.client.clone();
let project_id = self.remote_id().unwrap();
- Some(cx.spawn(move |_, mut cx| async move {
+ cx.spawn(move |_, mut cx| async move {
let response = client
.request(proto::CopyProjectEntry {
project_id,
@@ -1216,19 +1221,20 @@ impl Project {
new_path: new_path.to_string_lossy().into(),
})
.await?;
- let entry = response
- .entry
- .ok_or_else(|| anyhow!("missing entry in response"))?;
- worktree
- .update(&mut cx, |worktree, cx| {
- worktree.as_remote_mut().unwrap().insert_entry(
- entry,
- response.worktree_scan_id as usize,
- cx,
- )
- })?
- .await
- }))
+ match response.entry {
+ Some(entry) => worktree
+ .update(&mut cx, |worktree, cx| {
+ worktree.as_remote_mut().unwrap().insert_entry(
+ entry,
+ response.worktree_scan_id as usize,
+ cx,
+ )
+ })?
+ .await
+ .map(Some),
+ None => Ok(None),
+ }
+ })
}
}
@@ -1237,8 +1243,10 @@ impl Project {
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)?;
+ ) -> Task<Result<Option<Entry>>> {
+ let Some(worktree) = self.worktree_for_entry(entry_id, cx) else {
+ return Task::ready(Ok(None));
+ };
let new_path = new_path.into();
if self.is_local() {
worktree.update(cx, |worktree, cx| {
@@ -1251,7 +1259,7 @@ impl Project {
let client = self.client.clone();
let project_id = self.remote_id().unwrap();
- Some(cx.spawn(move |_, mut cx| async move {
+ cx.spawn(move |_, mut cx| async move {
let response = client
.request(proto::RenameProjectEntry {
project_id,
@@ -1259,19 +1267,20 @@ impl Project {
new_path: new_path.to_string_lossy().into(),
})
.await?;
- let entry = response
- .entry
- .ok_or_else(|| anyhow!("missing entry in response"))?;
- worktree
- .update(&mut cx, |worktree, cx| {
- worktree.as_remote_mut().unwrap().insert_entry(
- entry,
- response.worktree_scan_id as usize,
- cx,
- )
- })?
- .await
- }))
+ match response.entry {
+ Some(entry) => worktree
+ .update(&mut cx, |worktree, cx| {
+ worktree.as_remote_mut().unwrap().insert_entry(
+ entry,
+ response.worktree_scan_id as usize,
+ cx,
+ )
+ })?
+ .await
+ .map(Some),
+ None => Ok(None),
+ }
+ })
}
}
@@ -1688,18 +1697,15 @@ impl Project {
pub fn open_path(
&mut self,
- path: impl Into<ProjectPath>,
+ path: ProjectPath,
cx: &mut ModelContext<Self>,
- ) -> Task<Result<(ProjectEntryId, AnyModel)>> {
- let project_path = path.into();
- let task = self.open_buffer(project_path.clone(), cx);
- cx.spawn(move |_, mut cx| async move {
+ ) -> Task<Result<(Option<ProjectEntryId>, AnyModel)>> {
+ let task = self.open_buffer(path.clone(), cx);
+ cx.spawn(move |_, cx| async move {
let buffer = task.await?;
- let project_entry_id = buffer
- .update(&mut cx, |buffer, cx| {
- File::from_dyn(buffer.file()).and_then(|file| file.project_entry_id(cx))
- })?
- .with_context(|| format!("no project entry for {project_path:?}"))?;
+ let project_entry_id = buffer.read_with(&cx, |buffer, cx| {
+ File::from_dyn(buffer.file()).and_then(|file| file.project_entry_id(cx))
+ })?;
let buffer: &AnyModel = &buffer;
Ok((project_entry_id, buffer.clone()))
@@ -2018,8 +2024,10 @@ impl Project {
remote_id,
);
- self.local_buffer_ids_by_entry_id
- .insert(file.entry_id, remote_id);
+ if let Some(entry_id) = file.entry_id {
+ self.local_buffer_ids_by_entry_id
+ .insert(entry_id, remote_id);
+ }
}
}
@@ -2474,24 +2482,25 @@ impl Project {
return None;
};
- match self.local_buffer_ids_by_entry_id.get(&file.entry_id) {
- Some(_) => {
- return None;
- }
- None => {
- let remote_id = buffer.read(cx).remote_id();
- self.local_buffer_ids_by_entry_id
- .insert(file.entry_id, remote_id);
-
- self.local_buffer_ids_by_path.insert(
- ProjectPath {
- worktree_id: file.worktree_id(cx),
- path: file.path.clone(),
- },
- remote_id,
- );
+ let remote_id = buffer.read(cx).remote_id();
+ if let Some(entry_id) = file.entry_id {
+ match self.local_buffer_ids_by_entry_id.get(&entry_id) {
+ Some(_) => {
+ return None;
+ }
+ None => {
+ self.local_buffer_ids_by_entry_id
+ .insert(entry_id, remote_id);
+ }
}
- }
+ };
+ self.local_buffer_ids_by_path.insert(
+ ProjectPath {
+ worktree_id: file.worktree_id(cx),
+ path: file.path.clone(),
+ },
+ remote_id,
+ );
}
_ => {}
}
@@ -5845,11 +5854,6 @@ impl Project {
while let Some(ignored_abs_path) =
ignored_paths_to_process.pop_front()
{
- if !query.file_matches(Some(&ignored_abs_path))
- || snapshot.is_path_excluded(&ignored_abs_path)
- {
- continue;
- }
if let Some(fs_metadata) = fs
.metadata(&ignored_abs_path)
.await
@@ -5877,6 +5881,13 @@ impl Project {
}
}
} else if !fs_metadata.is_symlink {
+ if !query.file_matches(Some(&ignored_abs_path))
+ || snapshot.is_path_excluded(
+ ignored_entry.path.to_path_buf(),
+ )
+ {
+ continue;
+ }
let matches = if let Some(file) = fs
.open_sync(&ignored_abs_path)
.await
@@ -6278,10 +6289,13 @@ impl Project {
return;
}
- let new_file = if let Some(entry) = snapshot.entry_for_id(old_file.entry_id) {
+ let new_file = if let Some(entry) = old_file
+ .entry_id
+ .and_then(|entry_id| snapshot.entry_for_id(entry_id))
+ {
File {
is_local: true,
- entry_id: entry.id,
+ entry_id: Some(entry.id),
mtime: entry.mtime,
path: entry.path.clone(),
worktree: worktree_handle.clone(),
@@ -6290,7 +6304,7 @@ impl Project {
} else if let Some(entry) = snapshot.entry_for_path(old_file.path().as_ref()) {
File {
is_local: true,
- entry_id: entry.id,
+ entry_id: Some(entry.id),
mtime: entry.mtime,
path: entry.path.clone(),
worktree: worktree_handle.clone(),
@@ -6320,10 +6334,12 @@ impl Project {
);
}
- if new_file.entry_id != *entry_id {
+ if new_file.entry_id != Some(*entry_id) {
self.local_buffer_ids_by_entry_id.remove(entry_id);
- self.local_buffer_ids_by_entry_id
- .insert(new_file.entry_id, buffer_id);
+ if let Some(entry_id) = new_file.entry_id {
+ self.local_buffer_ids_by_entry_id
+ .insert(entry_id, buffer_id);
+ }
}
if new_file != *old_file {
@@ -6890,7 +6906,7 @@ impl Project {
})?
.await?;
Ok(proto::ProjectEntryResponse {
- entry: Some((&entry).into()),
+ entry: entry.as_ref().map(|e| e.into()),
worktree_scan_id: worktree_scan_id as u64,
})
}
@@ -6914,11 +6930,10 @@ impl Project {
.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()),
+ entry: entry.as_ref().map(|e| e.into()),
worktree_scan_id: worktree_scan_id as u64,
})
}
@@ -6942,11 +6957,10 @@ impl Project {
.as_local_mut()
.unwrap()
.copy_entry(entry_id, new_path, cx)
- .ok_or_else(|| anyhow!("invalid entry"))
- })??
+ })?
.await?;
Ok(proto::ProjectEntryResponse {
- entry: Some((&entry).into()),
+ entry: entry.as_ref().map(|e| e.into()),
worktree_scan_id: worktree_scan_id as u64,
})
}
@@ -4182,6 +4182,94 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
);
}
+#[gpui::test]
+async fn test_search_in_gitignored_dirs(cx: &mut gpui::TestAppContext) {
+ init_test(cx);
+
+ let fs = FakeFs::new(cx.background_executor.clone());
+ fs.insert_tree(
+ "/dir",
+ json!({
+ ".git": {},
+ ".gitignore": "**/target\n/node_modules\n",
+ "target": {
+ "index.txt": "index_key:index_value"
+ },
+ "node_modules": {
+ "eslint": {
+ "index.ts": "const eslint_key = 'eslint value'",
+ "package.json": r#"{ "some_key": "some value" }"#,
+ },
+ "prettier": {
+ "index.ts": "const prettier_key = 'prettier value'",
+ "package.json": r#"{ "other_key": "other value" }"#,
+ },
+ },
+ "package.json": r#"{ "main_key": "main value" }"#,
+ }),
+ )
+ .await;
+ let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
+
+ let query = "key";
+ assert_eq!(
+ search(
+ &project,
+ SearchQuery::text(query, false, false, false, Vec::new(), Vec::new()).unwrap(),
+ cx
+ )
+ .await
+ .unwrap(),
+ HashMap::from_iter([("package.json".to_string(), vec![8..11])]),
+ "Only one non-ignored file should have the query"
+ );
+
+ assert_eq!(
+ search(
+ &project,
+ SearchQuery::text(query, false, false, true, Vec::new(), Vec::new()).unwrap(),
+ cx
+ )
+ .await
+ .unwrap(),
+ HashMap::from_iter([
+ ("package.json".to_string(), vec![8..11]),
+ ("target/index.txt".to_string(), vec![6..9]),
+ (
+ "node_modules/prettier/package.json".to_string(),
+ vec![9..12]
+ ),
+ ("node_modules/prettier/index.ts".to_string(), vec![15..18]),
+ ("node_modules/eslint/index.ts".to_string(), vec![13..16]),
+ ("node_modules/eslint/package.json".to_string(), vec![8..11]),
+ ]),
+ "Unrestricted search with ignored directories should find every file with the query"
+ );
+
+ assert_eq!(
+ search(
+ &project,
+ SearchQuery::text(
+ query,
+ false,
+ false,
+ true,
+ vec![PathMatcher::new("node_modules/prettier/**").unwrap()],
+ vec![PathMatcher::new("*.ts").unwrap()],
+ )
+ .unwrap(),
+ cx
+ )
+ .await
+ .unwrap(),
+ HashMap::from_iter([(
+ "node_modules/prettier/package.json".to_string(),
+ vec![9..12]
+ )]),
+ "With search including ignored prettier directory and excluding TS files, only one file should be found"
+ );
+}
+
#[test]
fn test_glob_literal_prefix() {
assert_eq!(glob_literal_prefix("**/*.js"), "");
@@ -371,15 +371,25 @@ impl SearchQuery {
pub fn file_matches(&self, file_path: Option<&Path>) -> bool {
match file_path {
Some(file_path) => {
- !self
- .files_to_exclude()
- .iter()
- .any(|exclude_glob| exclude_glob.is_match(file_path))
- && (self.files_to_include().is_empty()
+ let mut path = file_path.to_path_buf();
+ loop {
+ if self
+ .files_to_exclude()
+ .iter()
+ .any(|exclude_glob| exclude_glob.is_match(&path))
+ {
+ return false;
+ } else if self.files_to_include().is_empty()
|| self
.files_to_include()
.iter()
- .any(|include_glob| include_glob.is_match(file_path)))
+ .any(|include_glob| include_glob.is_match(&path))
+ {
+ return true;
+ } else if !path.pop() {
+ return false;
+ }
+ }
}
None => self.files_to_include().is_empty(),
}
@@ -958,8 +958,6 @@ impl LocalWorktree {
cx.spawn(|this, mut cx| async move {
let text = fs.load(&abs_path).await?;
- let entry = entry.await?;
-
let mut index_task = None;
let snapshot = this.update(&mut cx, |this, _| this.as_local().unwrap().snapshot())?;
if let Some(repo) = snapshot.repository_for_path(&path) {
@@ -982,18 +980,43 @@ impl LocalWorktree {
let worktree = this
.upgrade()
.ok_or_else(|| anyhow!("worktree was dropped"))?;
- Ok((
- File {
- entry_id: entry.id,
- worktree,
- path: entry.path,
- mtime: entry.mtime,
- is_local: true,
- is_deleted: false,
- },
- text,
- diff_base,
- ))
+ match entry.await? {
+ Some(entry) => Ok((
+ File {
+ entry_id: Some(entry.id),
+ worktree,
+ path: entry.path,
+ mtime: entry.mtime,
+ is_local: true,
+ is_deleted: false,
+ },
+ text,
+ diff_base,
+ )),
+ None => {
+ let metadata = fs
+ .metadata(&abs_path)
+ .await
+ .with_context(|| {
+ format!("Loading metadata for excluded file {abs_path:?}")
+ })?
+ .with_context(|| {
+ format!("Excluded file {abs_path:?} got removed during loading")
+ })?;
+ Ok((
+ File {
+ entry_id: None,
+ worktree,
+ path,
+ mtime: metadata.mtime,
+ is_local: true,
+ is_deleted: false,
+ },
+ text,
+ diff_base,
+ ))
+ }
+ }
})
}
@@ -1013,18 +1036,38 @@ impl LocalWorktree {
let text = buffer.as_rope().clone();
let fingerprint = text.fingerprint();
let version = buffer.version();
- let save = self.write_file(path, text, buffer.line_ending(), cx);
+ let save = self.write_file(path.as_ref(), text, buffer.line_ending(), cx);
+ let fs = Arc::clone(&self.fs);
+ let abs_path = self.absolutize(&path);
cx.spawn(move |this, mut cx| async move {
let entry = save.await?;
let this = this.upgrade().context("worktree dropped")?;
+ let (entry_id, mtime, path) = match entry {
+ Some(entry) => (Some(entry.id), entry.mtime, entry.path),
+ None => {
+ let metadata = fs
+ .metadata(&abs_path)
+ .await
+ .with_context(|| {
+ format!(
+ "Fetching metadata after saving the excluded buffer {abs_path:?}"
+ )
+ })?
+ .with_context(|| {
+ format!("Excluded buffer {path:?} got removed during saving")
+ })?;
+ (None, metadata.mtime, path)
+ }
+ };
+
if has_changed_file {
let new_file = Arc::new(File {
- entry_id: entry.id,
+ entry_id,
worktree: this,
- path: entry.path,
- mtime: entry.mtime,
+ path,
+ mtime,
is_local: true,
is_deleted: false,
});
@@ -1050,13 +1093,13 @@ impl LocalWorktree {
project_id,
buffer_id,
version: serialize_version(&version),
- mtime: Some(entry.mtime.into()),
+ mtime: Some(mtime.into()),
fingerprint: serialize_fingerprint(fingerprint),
})?;
}
buffer_handle.update(&mut cx, |buffer, cx| {
- buffer.did_save(version.clone(), fingerprint, entry.mtime, cx);
+ buffer.did_save(version.clone(), fingerprint, mtime, cx);
})?;
Ok(())
@@ -1081,7 +1124,7 @@ impl LocalWorktree {
path: impl Into<Arc<Path>>,
is_dir: bool,
cx: &mut ModelContext<Worktree>,
- ) -> Task<Result<Entry>> {
+ ) -> Task<Result<Option<Entry>>> {
let path = path.into();
let lowest_ancestor = self.lowest_ancestor(&path);
let abs_path = self.absolutize(&path);
@@ -1098,7 +1141,7 @@ impl LocalWorktree {
cx.spawn(|this, mut cx| async move {
write.await?;
let (result, refreshes) = this.update(&mut cx, |this, cx| {
- let mut refreshes = Vec::<Task<anyhow::Result<Entry>>>::new();
+ let mut refreshes = Vec::new();
let refresh_paths = path.strip_prefix(&lowest_ancestor).unwrap();
for refresh_path in refresh_paths.ancestors() {
if refresh_path == Path::new("") {
@@ -1125,14 +1168,14 @@ impl LocalWorktree {
})
}
- pub fn write_file(
+ pub(crate) fn write_file(
&self,
path: impl Into<Arc<Path>>,
text: Rope,
line_ending: LineEnding,
cx: &mut ModelContext<Worktree>,
- ) -> Task<Result<Entry>> {
- let path = path.into();
+ ) -> Task<Result<Option<Entry>>> {
+ let path: Arc<Path> = path.into();
let abs_path = self.absolutize(&path);
let fs = self.fs.clone();
let write = cx
@@ -1191,8 +1234,11 @@ impl LocalWorktree {
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();
+ ) -> Task<Result<Option<Entry>>> {
+ let old_path = match self.entry_for_id(entry_id) {
+ Some(entry) => entry.path.clone(),
+ None => return Task::ready(Ok(None)),
+ };
let new_path = new_path.into();
let abs_old_path = self.absolutize(&old_path);
let abs_new_path = self.absolutize(&new_path);
@@ -1202,7 +1248,7 @@ impl LocalWorktree {
.await
});
- Some(cx.spawn(|this, mut cx| async move {
+ cx.spawn(|this, mut cx| async move {
rename.await?;
this.update(&mut cx, |this, cx| {
this.as_local_mut()
@@ -1210,7 +1256,7 @@ impl LocalWorktree {
.refresh_entry(new_path.clone(), Some(old_path), cx)
})?
.await
- }))
+ })
}
pub fn copy_entry(
@@ -1218,8 +1264,11 @@ impl LocalWorktree {
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();
+ ) -> Task<Result<Option<Entry>>> {
+ let old_path = match self.entry_for_id(entry_id) {
+ Some(entry) => entry.path.clone(),
+ None => return Task::ready(Ok(None)),
+ };
let new_path = new_path.into();
let abs_old_path = self.absolutize(&old_path);
let abs_new_path = self.absolutize(&new_path);
@@ -1234,7 +1283,7 @@ impl LocalWorktree {
.await
});
- Some(cx.spawn(|this, mut cx| async move {
+ cx.spawn(|this, mut cx| async move {
copy.await?;
this.update(&mut cx, |this, cx| {
this.as_local_mut()
@@ -1242,7 +1291,7 @@ impl LocalWorktree {
.refresh_entry(new_path.clone(), None, cx)
})?
.await
- }))
+ })
}
pub fn expand_entry(
@@ -1278,7 +1327,10 @@ impl LocalWorktree {
path: Arc<Path>,
old_path: Option<Arc<Path>>,
cx: &mut ModelContext<Worktree>,
- ) -> Task<Result<Entry>> {
+ ) -> Task<Result<Option<Entry>>> {
+ if self.is_path_excluded(path.to_path_buf()) {
+ return Task::ready(Ok(None));
+ }
let paths = if let Some(old_path) = old_path.as_ref() {
vec![old_path.clone(), path.clone()]
} else {
@@ -1287,11 +1339,12 @@ impl LocalWorktree {
let mut refresh = self.refresh_entries_for_paths(paths);
cx.spawn(move |this, mut cx| async move {
refresh.recv().await;
- this.update(&mut cx, |this, _| {
+ let new_entry = this.update(&mut cx, |this, _| {
this.entry_for_path(path)
.cloned()
.ok_or_else(|| anyhow!("failed to read path after update"))
- })?
+ })??;
+ Ok(Some(new_entry))
})
}
@@ -2222,10 +2275,19 @@ impl LocalSnapshot {
paths
}
- pub fn is_path_excluded(&self, abs_path: &Path) -> bool {
- self.file_scan_exclusions
- .iter()
- .any(|exclude_matcher| exclude_matcher.is_match(abs_path))
+ pub fn is_path_excluded(&self, mut path: PathBuf) -> bool {
+ loop {
+ if self
+ .file_scan_exclusions
+ .iter()
+ .any(|exclude_matcher| exclude_matcher.is_match(&path))
+ {
+ return true;
+ }
+ if !path.pop() {
+ return false;
+ }
+ }
}
}
@@ -2455,8 +2517,7 @@ impl BackgroundScannerState {
ids_to_preserve.insert(work_directory_id);
} else {
let git_dir_abs_path = snapshot.abs_path().join(&entry.git_dir_path);
- let git_dir_excluded = snapshot.is_path_excluded(&entry.git_dir_path)
- || snapshot.is_path_excluded(&git_dir_abs_path);
+ let git_dir_excluded = snapshot.is_path_excluded(entry.git_dir_path.to_path_buf());
if git_dir_excluded
&& !matches!(smol::block_on(fs.metadata(&git_dir_abs_path)), Ok(None))
{
@@ -2663,7 +2724,7 @@ pub struct File {
pub worktree: Model<Worktree>,
pub path: Arc<Path>,
pub mtime: SystemTime,
- pub(crate) entry_id: ProjectEntryId,
+ pub(crate) entry_id: Option<ProjectEntryId>,
pub(crate) is_local: bool,
pub(crate) is_deleted: bool,
}
@@ -2732,7 +2793,7 @@ impl language::File for File {
fn to_proto(&self) -> rpc::proto::File {
rpc::proto::File {
worktree_id: self.worktree.entity_id().as_u64(),
- entry_id: self.entry_id.to_proto(),
+ entry_id: self.entry_id.map(|id| id.to_proto()),
path: self.path.to_string_lossy().into(),
mtime: Some(self.mtime.into()),
is_deleted: self.is_deleted,
@@ -2790,7 +2851,7 @@ impl File {
worktree,
path: entry.path.clone(),
mtime: entry.mtime,
- entry_id: entry.id,
+ entry_id: Some(entry.id),
is_local: true,
is_deleted: false,
})
@@ -2815,7 +2876,7 @@ impl File {
worktree,
path: Path::new(&proto.path).into(),
mtime: proto.mtime.ok_or_else(|| anyhow!("no timestamp"))?.into(),
- entry_id: ProjectEntryId::from_proto(proto.entry_id),
+ entry_id: proto.entry_id.map(ProjectEntryId::from_proto),
is_local: false,
is_deleted: proto.is_deleted,
})
@@ -2833,7 +2894,7 @@ impl File {
if self.is_deleted {
None
} else {
- Some(self.entry_id)
+ self.entry_id
}
}
}
@@ -3329,16 +3390,7 @@ impl BackgroundScanner {
return false;
}
- // FS events may come for files which parent directory is excluded, need to check ignore those.
- let mut path_to_test = abs_path.clone();
- let mut excluded_file_event = snapshot.is_path_excluded(abs_path)
- || snapshot.is_path_excluded(&relative_path);
- while !excluded_file_event && path_to_test.pop() {
- if snapshot.is_path_excluded(&path_to_test) {
- excluded_file_event = true;
- }
- }
- if excluded_file_event {
+ if snapshot.is_path_excluded(relative_path.to_path_buf()) {
if !is_git_related {
log::debug!("ignoring FS event for excluded path {relative_path:?}");
}
@@ -3522,7 +3574,7 @@ impl BackgroundScanner {
let state = self.state.lock();
let snapshot = &state.snapshot;
root_abs_path = snapshot.abs_path().clone();
- if snapshot.is_path_excluded(&job.abs_path) {
+ if snapshot.is_path_excluded(job.path.to_path_buf()) {
log::error!("skipping excluded directory {:?}", job.path);
return Ok(());
}
@@ -3593,9 +3645,9 @@ impl BackgroundScanner {
}
{
+ let relative_path = job.path.join(child_name);
let mut state = self.state.lock();
- if state.snapshot.is_path_excluded(&child_abs_path) {
- let relative_path = job.path.join(child_name);
+ if state.snapshot.is_path_excluded(relative_path.clone()) {
log::debug!("skipping excluded child entry {relative_path:?}");
state.remove_path(&relative_path);
continue;
@@ -1055,11 +1055,12 @@ async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) {
&[
".git/HEAD",
".git/foo",
+ "node_modules",
"node_modules/.DS_Store",
"node_modules/prettier",
"node_modules/prettier/package.json",
],
- &["target", "node_modules"],
+ &["target"],
&[
".DS_Store",
"src/.DS_Store",
@@ -1109,6 +1110,7 @@ async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) {
".git/HEAD",
".git/foo",
".git/new_file",
+ "node_modules",
"node_modules/.DS_Store",
"node_modules/prettier",
"node_modules/prettier/package.json",
@@ -1117,7 +1119,7 @@ async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) {
"build_output/new_file",
"test_output/new_file",
],
- &["target", "node_modules", "test_output"],
+ &["target", "test_output"],
&[
".DS_Store",
"src/.DS_Store",
@@ -1177,6 +1179,7 @@ async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
.create_entry("a/e".as_ref(), true, cx)
})
.await
+ .unwrap()
.unwrap();
assert!(entry.is_dir());
@@ -1226,6 +1229,7 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
.create_entry("a/b/c/d.txt".as_ref(), false, cx)
})
.await
+ .unwrap()
.unwrap();
assert!(entry.is_file());
@@ -1261,6 +1265,7 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
.create_entry("a/b/c/d.txt".as_ref(), false, cx)
})
.await
+ .unwrap()
.unwrap();
assert!(entry.is_file());
@@ -1279,6 +1284,7 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
.create_entry("a/b/c/e.txt".as_ref(), false, cx)
})
.await
+ .unwrap()
.unwrap();
assert!(entry.is_file());
@@ -1295,6 +1301,7 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
.create_entry("d/e/f/g.txt".as_ref(), false, cx)
})
.await
+ .unwrap()
.unwrap();
assert!(entry.is_file());
@@ -1620,14 +1627,14 @@ fn randomly_mutate_worktree(
entry.id.0,
new_path
);
- let task = worktree.rename_entry(entry.id, new_path, cx).unwrap();
+ let task = worktree.rename_entry(entry.id, new_path, cx);
cx.background_executor().spawn(async move {
- task.await?;
+ task.await?.unwrap();
Ok(())
})
}
_ => {
- let task = if entry.is_dir() {
+ if entry.is_dir() {
let child_path = entry.path.join(random_filename(rng));
let is_dir = rng.gen_bool(0.3);
log::info!(
@@ -1635,15 +1642,20 @@ fn randomly_mutate_worktree(
if is_dir { "dir" } else { "file" },
child_path,
);
- worktree.create_entry(child_path, is_dir, cx)
+ let task = worktree.create_entry(child_path, is_dir, cx);
+ cx.background_executor().spawn(async move {
+ task.await?;
+ Ok(())
+ })
} else {
log::info!("overwriting file {:?} ({})", entry.path, entry.id.0);
- worktree.write_file(entry.path.clone(), "".into(), Default::default(), cx)
- };
- cx.background_executor().spawn(async move {
- task.await?;
- Ok(())
- })
+ let task =
+ worktree.write_file(entry.path.clone(), "".into(), Default::default(), cx);
+ cx.background_executor().spawn(async move {
+ task.await?;
+ Ok(())
+ })
+ }
}
}
}
@@ -621,7 +621,7 @@ impl ProjectPanel {
edited_entry_id = NEW_ENTRY_ID;
edit_task = self.project.update(cx, |project, cx| {
project.create_entry((worktree_id, &new_path), is_dir, cx)
- })?;
+ });
} else {
let new_path = if let Some(parent) = entry.path.clone().parent() {
parent.join(&filename)
@@ -635,7 +635,7 @@ impl ProjectPanel {
edited_entry_id = entry.id;
edit_task = self.project.update(cx, |project, cx| {
project.rename_entry(entry.id, new_path.as_path(), cx)
- })?;
+ });
};
edit_state.processing_filename = Some(filename);
@@ -648,21 +648,22 @@ impl ProjectPanel {
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.expand_to_selection(cx);
+ if let Some(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.expand_to_selection(cx);
+ }
}
- }
- this.update_visible_entries(None, cx);
- if is_new_entry && !is_dir {
- this.open_entry(new_entry.id, true, cx);
- }
- cx.notify();
- })?;
+ this.update_visible_entries(None, cx);
+ if is_new_entry && !is_dir {
+ this.open_entry(new_entry.id, true, cx);
+ }
+ cx.notify();
+ })?;
+ }
Ok(())
}))
}
@@ -935,15 +936,17 @@ impl ProjectPanel {
}
if clipboard_entry.is_cut() {
- if let Some(task) = self.project.update(cx, |project, cx| {
- project.rename_entry(clipboard_entry.entry_id(), new_path, cx)
- }) {
- task.detach_and_log_err(cx)
- }
- } else if let Some(task) = self.project.update(cx, |project, cx| {
- project.copy_entry(clipboard_entry.entry_id(), new_path, cx)
- }) {
- task.detach_and_log_err(cx)
+ self.project
+ .update(cx, |project, cx| {
+ project.rename_entry(clipboard_entry.entry_id(), new_path, cx)
+ })
+ .detach_and_log_err(cx)
+ } else {
+ self.project
+ .update(cx, |project, cx| {
+ project.copy_entry(clipboard_entry.entry_id(), new_path, cx)
+ })
+ .detach_and_log_err(cx)
}
}
None
@@ -1026,7 +1029,7 @@ impl ProjectPanel {
let mut new_path = destination_path.to_path_buf();
new_path.push(entry_path.path.file_name()?);
if new_path != entry_path.path.as_ref() {
- let task = project.rename_entry(entry_to_move, new_path, cx)?;
+ let task = project.rename_entry(entry_to_move, new_path, cx);
cx.foreground().spawn(task).detach_and_log_err(cx);
}
@@ -610,7 +610,7 @@ impl ProjectPanel {
edited_entry_id = NEW_ENTRY_ID;
edit_task = self.project.update(cx, |project, cx| {
project.create_entry((worktree_id, &new_path), is_dir, cx)
- })?;
+ });
} else {
let new_path = if let Some(parent) = entry.path.clone().parent() {
parent.join(&filename)
@@ -624,7 +624,7 @@ impl ProjectPanel {
edited_entry_id = entry.id;
edit_task = self.project.update(cx, |project, cx| {
project.rename_entry(entry.id, new_path.as_path(), cx)
- })?;
+ });
};
edit_state.processing_filename = Some(filename);
@@ -637,21 +637,22 @@ impl ProjectPanel {
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.expand_to_selection(cx);
+ if let Some(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.expand_to_selection(cx);
+ }
}
- }
- this.update_visible_entries(None, cx);
- if is_new_entry && !is_dir {
- this.open_entry(new_entry.id, true, cx);
- }
- cx.notify();
- })?;
+ this.update_visible_entries(None, cx);
+ if is_new_entry && !is_dir {
+ this.open_entry(new_entry.id, true, cx);
+ }
+ cx.notify();
+ })?;
+ }
Ok(())
}))
}
@@ -931,15 +932,17 @@ impl ProjectPanel {
}
if clipboard_entry.is_cut() {
- if let Some(task) = self.project.update(cx, |project, cx| {
- project.rename_entry(clipboard_entry.entry_id(), new_path, cx)
- }) {
- task.detach_and_log_err(cx);
- }
- } else if let Some(task) = self.project.update(cx, |project, cx| {
- project.copy_entry(clipboard_entry.entry_id(), new_path, cx)
- }) {
- task.detach_and_log_err(cx);
+ self.project
+ .update(cx, |project, cx| {
+ project.rename_entry(clipboard_entry.entry_id(), new_path, cx)
+ })
+ .detach_and_log_err(cx)
+ } else {
+ self.project
+ .update(cx, |project, cx| {
+ project.copy_entry(clipboard_entry.entry_id(), new_path, cx)
+ })
+ .detach_and_log_err(cx)
}
Some(())
@@ -1025,7 +1028,7 @@ impl ProjectPanel {
// let mut new_path = destination_path.to_path_buf();
// new_path.push(entry_path.path.file_name()?);
// if new_path != entry_path.path.as_ref() {
- // let task = project.rename_entry(entry_to_move, new_path, cx)?;
+ // let task = project.rename_entry(entry_to_move, new_path, cx);
// cx.foreground_executor().spawn(task).detach_and_log_err(cx);
// }
@@ -430,7 +430,7 @@ message ExpandProjectEntryResponse {
}
message ProjectEntryResponse {
- Entry entry = 1;
+ optional Entry entry = 1;
uint64 worktree_scan_id = 2;
}
@@ -1357,7 +1357,7 @@ message User {
message File {
uint64 worktree_id = 1;
- uint64 entry_id = 2;
+ optional uint64 entry_id = 2;
string path = 3;
Timestamp mtime = 4;
bool is_deleted = 5;
@@ -9,4 +9,4 @@ pub use notification::*;
pub use peer::*;
mod macros;
-pub const PROTOCOL_VERSION: u32 = 66;
+pub const PROTOCOL_VERSION: u32 = 67;
@@ -430,7 +430,7 @@ message ExpandProjectEntryResponse {
}
message ProjectEntryResponse {
- Entry entry = 1;
+ optional Entry entry = 1;
uint64 worktree_scan_id = 2;
}
@@ -1357,7 +1357,7 @@ message User {
message File {
uint64 worktree_id = 1;
- uint64 entry_id = 2;
+ optional uint64 entry_id = 2;
string path = 3;
Timestamp mtime = 4;
bool is_deleted = 5;
@@ -9,4 +9,4 @@ pub use notification::*;
pub use peer::*;
mod macros;
-pub const PROTOCOL_VERSION: u32 = 66;
+pub const PROTOCOL_VERSION: u32 = 67;
@@ -1132,6 +1132,7 @@ mod tests {
})
})
.await
+ .unwrap()
.unwrap();
(wt, entry)
@@ -1170,6 +1170,7 @@ mod tests {
})
})
.await
+ .unwrap()
.unwrap();
(wt, entry)
@@ -219,9 +219,11 @@ impl PathMatcher {
}
pub fn is_match<P: AsRef<Path>>(&self, other: P) -> bool {
- other.as_ref().starts_with(&self.maybe_path)
- || self.glob.is_match(&other)
- || self.check_with_end_separator(other.as_ref())
+ let other_path = other.as_ref();
+ other_path.starts_with(&self.maybe_path)
+ || other_path.ends_with(&self.maybe_path)
+ || self.glob.is_match(other_path)
+ || self.check_with_end_separator(other_path)
}
fn check_with_end_separator(&self, path: &Path) -> bool {
@@ -418,4 +420,14 @@ mod tests {
"Path matcher {path_matcher} should match {path:?}"
);
}
+
+ #[test]
+ fn project_search() {
+ let path = Path::new("/Users/someonetoignore/work/zed/zed.dev/node_modules");
+ let path_matcher = PathMatcher::new("**/node_modules/**").unwrap();
+ assert!(
+ path_matcher.is_match(&path),
+ "Path matcher {path_matcher} should match {path:?}"
+ );
+ }
}
@@ -481,18 +481,21 @@ impl Pane {
pub(crate) fn open_item(
&mut self,
- project_entry_id: ProjectEntryId,
+ project_entry_id: Option<ProjectEntryId>,
focus_item: bool,
cx: &mut ViewContext<Self>,
build_item: impl FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
) -> Box<dyn ItemHandle> {
let mut existing_item = None;
- for (index, item) in self.items.iter().enumerate() {
- if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [project_entry_id]
- {
- let item = item.boxed_clone();
- existing_item = Some((index, item));
- break;
+ if let Some(project_entry_id) = project_entry_id {
+ for (index, item) in self.items.iter().enumerate() {
+ if item.is_singleton(cx)
+ && item.project_entry_ids(cx).as_slice() == [project_entry_id]
+ {
+ let item = item.boxed_clone();
+ existing_item = Some((index, item));
+ break;
+ }
}
}
@@ -2129,13 +2129,13 @@ impl Workspace {
})
}
- pub(crate) fn load_path(
+ fn load_path(
&mut self,
path: ProjectPath,
cx: &mut ViewContext<Self>,
) -> Task<
Result<(
- ProjectEntryId,
+ Option<ProjectEntryId>,
impl 'static + FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
)>,
> {
@@ -537,18 +537,21 @@ impl Pane {
pub(crate) fn open_item(
&mut self,
- project_entry_id: ProjectEntryId,
+ project_entry_id: Option<ProjectEntryId>,
focus_item: bool,
cx: &mut ViewContext<Self>,
build_item: impl FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
) -> Box<dyn ItemHandle> {
let mut existing_item = None;
- for (index, item) in self.items.iter().enumerate() {
- if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [project_entry_id]
- {
- let item = item.boxed_clone();
- existing_item = Some((index, item));
- break;
+ if let Some(project_entry_id) = project_entry_id {
+ for (index, item) in self.items.iter().enumerate() {
+ if item.is_singleton(cx)
+ && item.project_entry_ids(cx).as_slice() == [project_entry_id]
+ {
+ let item = item.boxed_clone();
+ existing_item = Some((index, item));
+ break;
+ }
}
}
@@ -10,7 +10,7 @@ mod persistence;
pub mod searchable;
// todo!()
mod modal_layer;
-mod shared_screen;
+pub mod shared_screen;
mod status_bar;
mod toolbar;
mod workspace_settings;
@@ -1853,13 +1853,13 @@ impl Workspace {
})
}
- pub(crate) fn load_path(
+ fn load_path(
&mut self,
path: ProjectPath,
cx: &mut ViewContext<Self>,
) -> Task<
Result<(
- ProjectEntryId,
+ Option<ProjectEntryId>,
impl 'static + Send + FnOnce(&mut ViewContext<Pane>) -> Box<dyn ItemHandle>,
)>,
> {
@@ -615,8 +615,8 @@ fn open_local_settings_file(
.update(&mut cx, |project, cx| {
project.create_entry((tree_id, dir_path), true, cx)
})
- .ok_or_else(|| anyhow!("worktree was removed"))?
- .await?;
+ .await
+ .context("worktree was removed")?;
}
}
@@ -625,8 +625,8 @@ fn open_local_settings_file(
.update(&mut cx, |project, cx| {
project.create_entry((tree_id, file_path), false, cx)
})
- .ok_or_else(|| anyhow!("worktree was removed"))?
- .await?;
+ .await
+ .context("worktree was removed")?;
}
let editor = workspace
@@ -763,7 +763,7 @@ mod tests {
AppContext, AssetSource, Element, Entity, TestAppContext, View, ViewHandle,
};
use language::LanguageRegistry;
- use project::{Project, ProjectPath};
+ use project::{project_settings::ProjectSettings, Project, ProjectPath};
use serde_json::json;
use settings::{handle_settings_file_changes, watch_config_file, SettingsStore};
use std::{
@@ -1308,6 +1308,122 @@ mod tests {
});
}
+ #[gpui::test]
+ async fn test_opening_excluded_paths(cx: &mut TestAppContext) {
+ let app_state = init_test(cx);
+ cx.update(|cx| {
+ cx.update_global::<SettingsStore, _, _>(|store, cx| {
+ store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
+ project_settings.file_scan_exclusions =
+ Some(vec!["excluded_dir".to_string(), "**/.git".to_string()]);
+ });
+ });
+ });
+ app_state
+ .fs
+ .as_fake()
+ .insert_tree(
+ "/root",
+ json!({
+ ".gitignore": "ignored_dir\n",
+ ".git": {
+ "HEAD": "ref: refs/heads/main",
+ },
+ "regular_dir": {
+ "file": "regular file contents",
+ },
+ "ignored_dir": {
+ "ignored_subdir": {
+ "file": "ignored subfile contents",
+ },
+ "file": "ignored file contents",
+ },
+ "excluded_dir": {
+ "file": "excluded file contents",
+ },
+ }),
+ )
+ .await;
+
+ let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
+ let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+ let workspace = window.root(cx);
+
+ let initial_entries = cx.read(|cx| workspace.file_project_paths(cx));
+ let paths_to_open = [
+ Path::new("/root/excluded_dir/file").to_path_buf(),
+ Path::new("/root/.git/HEAD").to_path_buf(),
+ Path::new("/root/excluded_dir/ignored_subdir").to_path_buf(),
+ ];
+ let (opened_workspace, new_items) = cx
+ .update(|cx| workspace::open_paths(&paths_to_open, &app_state, None, cx))
+ .await
+ .unwrap();
+
+ assert_eq!(
+ opened_workspace.id(),
+ workspace.id(),
+ "Excluded files in subfolders of a workspace root should be opened in the workspace"
+ );
+ let mut opened_paths = cx.read(|cx| {
+ assert_eq!(
+ new_items.len(),
+ paths_to_open.len(),
+ "Expect to get the same number of opened items as submitted paths to open"
+ );
+ new_items
+ .iter()
+ .zip(paths_to_open.iter())
+ .map(|(i, path)| {
+ match i {
+ Some(Ok(i)) => {
+ Some(i.project_path(cx).map(|p| p.path.display().to_string()))
+ }
+ Some(Err(e)) => panic!("Excluded file {path:?} failed to open: {e:?}"),
+ None => None,
+ }
+ .flatten()
+ })
+ .collect::<Vec<_>>()
+ });
+ opened_paths.sort();
+ assert_eq!(
+ opened_paths,
+ vec![
+ None,
+ Some(".git/HEAD".to_string()),
+ Some("excluded_dir/file".to_string()),
+ ],
+ "Excluded files should get opened, excluded dir should not get opened"
+ );
+
+ let entries = cx.read(|cx| workspace.file_project_paths(cx));
+ assert_eq!(
+ initial_entries, entries,
+ "Workspace entries should not change after opening excluded files and directories paths"
+ );
+
+ cx.read(|cx| {
+ let pane = workspace.read(cx).active_pane().read(cx);
+ let mut opened_buffer_paths = pane
+ .items()
+ .map(|i| {
+ i.project_path(cx)
+ .expect("all excluded files that got open should have a path")
+ .path
+ .display()
+ .to_string()
+ })
+ .collect::<Vec<_>>();
+ opened_buffer_paths.sort();
+ assert_eq!(
+ opened_buffer_paths,
+ vec![".git/HEAD".to_string(), "excluded_dir/file".to_string()],
+ "Despite not being present in the worktrees, buffers for excluded files are opened and added to the pane"
+ );
+ });
+ }
+
#[gpui::test]
async fn test_save_conflicting_item(cx: &mut TestAppContext) {
let app_state = init_test(cx);
@@ -582,8 +582,8 @@ fn open_local_settings_file(
.update(&mut cx, |project, cx| {
project.create_entry((tree_id, dir_path), true, cx)
})?
- .ok_or_else(|| anyhow!("worktree was removed"))?
- .await?;
+ .await
+ .context("worktree was removed")?;
}
}
@@ -592,8 +592,8 @@ fn open_local_settings_file(
.update(&mut cx, |project, cx| {
project.create_entry((tree_id, file_path), false, cx)
})?
- .ok_or_else(|| anyhow!("worktree was removed"))?
- .await?;
+ .await
+ .context("worktree was removed")?;
}
let editor = workspace
@@ -718,3 +718,1846 @@ fn open_bundled_file(
})
.detach_and_log_err(cx);
}
+
+// todo!()
+// #[cfg(test)]
+// mod tests {
+// use super::*;
+// use assets::Assets;
+// use editor::{scroll::autoscroll::Autoscroll, DisplayPoint, Editor};
+// use fs::{FakeFs, Fs};
+// use gpui::{
+// actions, elements::Empty, executor::Deterministic, Action, AnyElement, AnyWindowHandle,
+// AppContext, AssetSource, Element, Entity, TestAppContext, View, ViewHandle,
+// };
+// use language::LanguageRegistry;
+// use project::{project_settings::ProjectSettings, Project, ProjectPath};
+// use serde_json::json;
+// use settings::{handle_settings_file_changes, watch_config_file, SettingsStore};
+// use std::{
+// collections::HashSet,
+// path::{Path, PathBuf},
+// };
+// use theme::{ThemeRegistry, ThemeSettings};
+// use workspace::{
+// item::{Item, ItemHandle},
+// open_new, open_paths, pane, NewFile, SaveIntent, SplitDirection, WorkspaceHandle,
+// };
+
+// #[gpui::test]
+// async fn test_open_paths_action(cx: &mut TestAppContext) {
+// let app_state = init_test(cx);
+// app_state
+// .fs
+// .as_fake()
+// .insert_tree(
+// "/root",
+// json!({
+// "a": {
+// "aa": null,
+// "ab": null,
+// },
+// "b": {
+// "ba": null,
+// "bb": null,
+// },
+// "c": {
+// "ca": null,
+// "cb": null,
+// },
+// "d": {
+// "da": null,
+// "db": null,
+// },
+// }),
+// )
+// .await;
+
+// cx.update(|cx| {
+// open_paths(
+// &[PathBuf::from("/root/a"), PathBuf::from("/root/b")],
+// &app_state,
+// None,
+// cx,
+// )
+// })
+// .await
+// .unwrap();
+// assert_eq!(cx.windows().len(), 1);
+
+// cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
+// .await
+// .unwrap();
+// assert_eq!(cx.windows().len(), 1);
+// let workspace_1 = cx.windows()[0].downcast::<Workspace>().unwrap().root(cx);
+// workspace_1.update(cx, |workspace, cx| {
+// assert_eq!(workspace.worktrees(cx).count(), 2);
+// assert!(workspace.left_dock().read(cx).is_open());
+// assert!(workspace.active_pane().is_focused(cx));
+// });
+
+// cx.update(|cx| {
+// open_paths(
+// &[PathBuf::from("/root/b"), PathBuf::from("/root/c")],
+// &app_state,
+// None,
+// cx,
+// )
+// })
+// .await
+// .unwrap();
+// assert_eq!(cx.windows().len(), 2);
+
+// // Replace existing windows
+// let window = cx.windows()[0].downcast::<Workspace>().unwrap();
+// cx.update(|cx| {
+// open_paths(
+// &[PathBuf::from("/root/c"), PathBuf::from("/root/d")],
+// &app_state,
+// Some(window),
+// cx,
+// )
+// })
+// .await
+// .unwrap();
+// assert_eq!(cx.windows().len(), 2);
+// let workspace_1 = cx.windows()[0].downcast::<Workspace>().unwrap().root(cx);
+// workspace_1.update(cx, |workspace, cx| {
+// assert_eq!(
+// workspace
+// .worktrees(cx)
+// .map(|w| w.read(cx).abs_path())
+// .collect::<Vec<_>>(),
+// &[Path::new("/root/c").into(), Path::new("/root/d").into()]
+// );
+// assert!(workspace.left_dock().read(cx).is_open());
+// assert!(workspace.active_pane().is_focused(cx));
+// });
+// }
+
+// #[gpui::test]
+// async fn test_window_edit_state(executor: Arc<Deterministic>, cx: &mut TestAppContext) {
+// let app_state = init_test(cx);
+// app_state
+// .fs
+// .as_fake()
+// .insert_tree("/root", json!({"a": "hey"}))
+// .await;
+
+// cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
+// .await
+// .unwrap();
+// assert_eq!(cx.windows().len(), 1);
+
+// // When opening the workspace, the window is not in a edited state.
+// let window = cx.windows()[0].downcast::<Workspace>().unwrap();
+// let workspace = window.root(cx);
+// let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
+// let editor = workspace.read_with(cx, |workspace, cx| {
+// workspace
+// .active_item(cx)
+// .unwrap()
+// .downcast::<Editor>()
+// .unwrap()
+// });
+// assert!(!window.is_edited(cx));
+
+// // Editing a buffer marks the window as edited.
+// editor.update(cx, |editor, cx| editor.insert("EDIT", cx));
+// assert!(window.is_edited(cx));
+
+// // Undoing the edit restores the window's edited state.
+// editor.update(cx, |editor, cx| editor.undo(&Default::default(), cx));
+// assert!(!window.is_edited(cx));
+
+// // Redoing the edit marks the window as edited again.
+// editor.update(cx, |editor, cx| editor.redo(&Default::default(), cx));
+// assert!(window.is_edited(cx));
+
+// // Closing the item restores the window's edited state.
+// let close = pane.update(cx, |pane, cx| {
+// drop(editor);
+// pane.close_active_item(&Default::default(), cx).unwrap()
+// });
+// executor.run_until_parked();
+
+// window.simulate_prompt_answer(1, cx);
+// close.await.unwrap();
+// assert!(!window.is_edited(cx));
+
+// // Opening the buffer again doesn't impact the window's edited state.
+// cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx))
+// .await
+// .unwrap();
+// let editor = workspace.read_with(cx, |workspace, cx| {
+// workspace
+// .active_item(cx)
+// .unwrap()
+// .downcast::<Editor>()
+// .unwrap()
+// });
+// assert!(!window.is_edited(cx));
+
+// // Editing the buffer marks the window as edited.
+// editor.update(cx, |editor, cx| editor.insert("EDIT", cx));
+// assert!(window.is_edited(cx));
+
+// // Ensure closing the window via the mouse gets preempted due to the
+// // buffer having unsaved changes.
+// assert!(!window.simulate_close(cx));
+// executor.run_until_parked();
+// assert_eq!(cx.windows().len(), 1);
+
+// // The window is successfully closed after the user dismisses the prompt.
+// window.simulate_prompt_answer(1, cx);
+// executor.run_until_parked();
+// assert_eq!(cx.windows().len(), 0);
+// }
+
+// #[gpui::test]
+// async fn test_new_empty_workspace(cx: &mut TestAppContext) {
+// let app_state = init_test(cx);
+// cx.update(|cx| {
+// open_new(&app_state, cx, |workspace, cx| {
+// Editor::new_file(workspace, &Default::default(), cx)
+// })
+// })
+// .await;
+
+// let window = cx
+// .windows()
+// .first()
+// .unwrap()
+// .downcast::<Workspace>()
+// .unwrap();
+// let workspace = window.root(cx);
+
+// let editor = workspace.update(cx, |workspace, cx| {
+// workspace
+// .active_item(cx)
+// .unwrap()
+// .downcast::<editor::Editor>()
+// .unwrap()
+// });
+
+// editor.update(cx, |editor, cx| {
+// assert!(editor.text(cx).is_empty());
+// assert!(!editor.is_dirty(cx));
+// });
+
+// let save_task = workspace.update(cx, |workspace, cx| {
+// workspace.save_active_item(SaveIntent::Save, cx)
+// });
+// app_state.fs.create_dir(Path::new("/root")).await.unwrap();
+// cx.foreground().run_until_parked();
+// cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name")));
+// save_task.await.unwrap();
+// editor.read_with(cx, |editor, cx| {
+// assert!(!editor.is_dirty(cx));
+// assert_eq!(editor.title(cx), "the-new-name");
+// });
+// }
+
+// #[gpui::test]
+// async fn test_open_entry(cx: &mut TestAppContext) {
+// let app_state = init_test(cx);
+// app_state
+// .fs
+// .as_fake()
+// .insert_tree(
+// "/root",
+// json!({
+// "a": {
+// "file1": "contents 1",
+// "file2": "contents 2",
+// "file3": "contents 3",
+// },
+// }),
+// )
+// .await;
+
+// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
+// let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+// let workspace = window.root(cx);
+
+// let entries = cx.read(|cx| workspace.file_project_paths(cx));
+// let file1 = entries[0].clone();
+// let file2 = entries[1].clone();
+// let file3 = entries[2].clone();
+
+// // Open the first entry
+// let entry_1 = workspace
+// .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
+// .await
+// .unwrap();
+// cx.read(|cx| {
+// let pane = workspace.read(cx).active_pane().read(cx);
+// assert_eq!(
+// pane.active_item().unwrap().project_path(cx),
+// Some(file1.clone())
+// );
+// assert_eq!(pane.items_len(), 1);
+// });
+
+// // Open the second entry
+// workspace
+// .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx))
+// .await
+// .unwrap();
+// cx.read(|cx| {
+// let pane = workspace.read(cx).active_pane().read(cx);
+// assert_eq!(
+// pane.active_item().unwrap().project_path(cx),
+// Some(file2.clone())
+// );
+// assert_eq!(pane.items_len(), 2);
+// });
+
+// // Open the first entry again. The existing pane item is activated.
+// let entry_1b = workspace
+// .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
+// .await
+// .unwrap();
+// assert_eq!(entry_1.id(), entry_1b.id());
+
+// cx.read(|cx| {
+// let pane = workspace.read(cx).active_pane().read(cx);
+// assert_eq!(
+// pane.active_item().unwrap().project_path(cx),
+// Some(file1.clone())
+// );
+// assert_eq!(pane.items_len(), 2);
+// });
+
+// // Split the pane with the first entry, then open the second entry again.
+// workspace
+// .update(cx, |w, cx| {
+// w.split_and_clone(w.active_pane().clone(), SplitDirection::Right, cx);
+// w.open_path(file2.clone(), None, true, cx)
+// })
+// .await
+// .unwrap();
+
+// workspace.read_with(cx, |w, cx| {
+// assert_eq!(
+// w.active_pane()
+// .read(cx)
+// .active_item()
+// .unwrap()
+// .project_path(cx),
+// Some(file2.clone())
+// );
+// });
+
+// // 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(), None, true, cx),
+// w.open_path(file3.clone(), None, true, cx),
+// )
+// });
+// t1.await.unwrap();
+// t2.await.unwrap();
+// cx.read(|cx| {
+// let pane = workspace.read(cx).active_pane().read(cx);
+// assert_eq!(
+// pane.active_item().unwrap().project_path(cx),
+// Some(file3.clone())
+// );
+// let pane_entries = pane
+// .items()
+// .map(|i| i.project_path(cx).unwrap())
+// .collect::<Vec<_>>();
+// assert_eq!(pane_entries, &[file1, file2, file3]);
+// });
+// }
+
+// #[gpui::test]
+// async fn test_open_paths(cx: &mut TestAppContext) {
+// let app_state = init_test(cx);
+
+// app_state
+// .fs
+// .as_fake()
+// .insert_tree(
+// "/",
+// json!({
+// "dir1": {
+// "a.txt": ""
+// },
+// "dir2": {
+// "b.txt": ""
+// },
+// "dir3": {
+// "c.txt": ""
+// },
+// "d.txt": ""
+// }),
+// )
+// .await;
+
+// cx.update(|cx| open_paths(&[PathBuf::from("/dir1/")], &app_state, None, cx))
+// .await
+// .unwrap();
+// assert_eq!(cx.windows().len(), 1);
+// let workspace = cx.windows()[0].downcast::<Workspace>().unwrap().root(cx);
+
+// #[track_caller]
+// fn assert_project_panel_selection(
+// workspace: &Workspace,
+// expected_worktree_path: &Path,
+// expected_entry_path: &Path,
+// cx: &AppContext,
+// ) {
+// let project_panel = [
+// workspace.left_dock().read(cx).panel::<ProjectPanel>(),
+// workspace.right_dock().read(cx).panel::<ProjectPanel>(),
+// workspace.bottom_dock().read(cx).panel::<ProjectPanel>(),
+// ]
+// .into_iter()
+// .find_map(std::convert::identity)
+// .expect("found no project panels")
+// .read(cx);
+// let (selected_worktree, selected_entry) = project_panel
+// .selected_entry(cx)
+// .expect("project panel should have a selected entry");
+// assert_eq!(
+// selected_worktree.abs_path().as_ref(),
+// expected_worktree_path,
+// "Unexpected project panel selected worktree path"
+// );
+// assert_eq!(
+// selected_entry.path.as_ref(),
+// expected_entry_path,
+// "Unexpected project panel selected entry path"
+// );
+// }
+
+// // Open a file within an existing worktree.
+// workspace
+// .update(cx, |view, cx| {
+// view.open_paths(vec!["/dir1/a.txt".into()], true, cx)
+// })
+// .await;
+// cx.read(|cx| {
+// let workspace = workspace.read(cx);
+// assert_project_panel_selection(workspace, Path::new("/dir1"), Path::new("a.txt"), cx);
+// assert_eq!(
+// workspace
+// .active_pane()
+// .read(cx)
+// .active_item()
+// .unwrap()
+// .as_any()
+// .downcast_ref::<Editor>()
+// .unwrap()
+// .read(cx)
+// .title(cx),
+// "a.txt"
+// );
+// });
+
+// // Open a file outside of any existing worktree.
+// workspace
+// .update(cx, |view, cx| {
+// view.open_paths(vec!["/dir2/b.txt".into()], true, cx)
+// })
+// .await;
+// cx.read(|cx| {
+// let workspace = workspace.read(cx);
+// assert_project_panel_selection(workspace, Path::new("/dir2/b.txt"), Path::new(""), cx);
+// let worktree_roots = workspace
+// .worktrees(cx)
+// .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
+// .collect::<HashSet<_>>();
+// assert_eq!(
+// worktree_roots,
+// vec!["/dir1", "/dir2/b.txt"]
+// .into_iter()
+// .map(Path::new)
+// .collect(),
+// );
+// assert_eq!(
+// workspace
+// .active_pane()
+// .read(cx)
+// .active_item()
+// .unwrap()
+// .as_any()
+// .downcast_ref::<Editor>()
+// .unwrap()
+// .read(cx)
+// .title(cx),
+// "b.txt"
+// );
+// });
+
+// // Ensure opening a directory and one of its children only adds one worktree.
+// workspace
+// .update(cx, |view, cx| {
+// view.open_paths(vec!["/dir3".into(), "/dir3/c.txt".into()], true, cx)
+// })
+// .await;
+// cx.read(|cx| {
+// let workspace = workspace.read(cx);
+// assert_project_panel_selection(workspace, Path::new("/dir3"), Path::new("c.txt"), cx);
+// let worktree_roots = workspace
+// .worktrees(cx)
+// .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
+// .collect::<HashSet<_>>();
+// assert_eq!(
+// worktree_roots,
+// vec!["/dir1", "/dir2/b.txt", "/dir3"]
+// .into_iter()
+// .map(Path::new)
+// .collect(),
+// );
+// assert_eq!(
+// workspace
+// .active_pane()
+// .read(cx)
+// .active_item()
+// .unwrap()
+// .as_any()
+// .downcast_ref::<Editor>()
+// .unwrap()
+// .read(cx)
+// .title(cx),
+// "c.txt"
+// );
+// });
+
+// // Ensure opening invisibly a file outside an existing worktree adds a new, invisible worktree.
+// workspace
+// .update(cx, |view, cx| {
+// view.open_paths(vec!["/d.txt".into()], false, cx)
+// })
+// .await;
+// cx.read(|cx| {
+// let workspace = workspace.read(cx);
+// assert_project_panel_selection(workspace, Path::new("/d.txt"), Path::new(""), cx);
+// let worktree_roots = workspace
+// .worktrees(cx)
+// .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
+// .collect::<HashSet<_>>();
+// assert_eq!(
+// worktree_roots,
+// vec!["/dir1", "/dir2/b.txt", "/dir3", "/d.txt"]
+// .into_iter()
+// .map(Path::new)
+// .collect(),
+// );
+
+// let visible_worktree_roots = workspace
+// .visible_worktrees(cx)
+// .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref())
+// .collect::<HashSet<_>>();
+// assert_eq!(
+// visible_worktree_roots,
+// vec!["/dir1", "/dir2/b.txt", "/dir3"]
+// .into_iter()
+// .map(Path::new)
+// .collect(),
+// );
+
+// assert_eq!(
+// workspace
+// .active_pane()
+// .read(cx)
+// .active_item()
+// .unwrap()
+// .as_any()
+// .downcast_ref::<Editor>()
+// .unwrap()
+// .read(cx)
+// .title(cx),
+// "d.txt"
+// );
+// });
+// }
+
+// #[gpui::test]
+// async fn test_opening_excluded_paths(cx: &mut TestAppContext) {
+// let app_state = init_test(cx);
+// cx.update(|cx| {
+// cx.update_global::<SettingsStore, _, _>(|store, cx| {
+// store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
+// project_settings.file_scan_exclusions =
+// Some(vec!["excluded_dir".to_string(), "**/.git".to_string()]);
+// });
+// });
+// });
+// app_state
+// .fs
+// .as_fake()
+// .insert_tree(
+// "/root",
+// json!({
+// ".gitignore": "ignored_dir\n",
+// ".git": {
+// "HEAD": "ref: refs/heads/main",
+// },
+// "regular_dir": {
+// "file": "regular file contents",
+// },
+// "ignored_dir": {
+// "ignored_subdir": {
+// "file": "ignored subfile contents",
+// },
+// "file": "ignored file contents",
+// },
+// "excluded_dir": {
+// "file": "excluded file contents",
+// },
+// }),
+// )
+// .await;
+
+// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
+// let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+// let workspace = window.root(cx);
+
+// let initial_entries = cx.read(|cx| workspace.file_project_paths(cx));
+// let paths_to_open = [
+// Path::new("/root/excluded_dir/file").to_path_buf(),
+// Path::new("/root/.git/HEAD").to_path_buf(),
+// Path::new("/root/excluded_dir/ignored_subdir").to_path_buf(),
+// ];
+// let (opened_workspace, new_items) = cx
+// .update(|cx| workspace::open_paths(&paths_to_open, &app_state, None, cx))
+// .await
+// .unwrap();
+
+// assert_eq!(
+// opened_workspace.id(),
+// workspace.id(),
+// "Excluded files in subfolders of a workspace root should be opened in the workspace"
+// );
+// let mut opened_paths = cx.read(|cx| {
+// assert_eq!(
+// new_items.len(),
+// paths_to_open.len(),
+// "Expect to get the same number of opened items as submitted paths to open"
+// );
+// new_items
+// .iter()
+// .zip(paths_to_open.iter())
+// .map(|(i, path)| {
+// match i {
+// Some(Ok(i)) => {
+// Some(i.project_path(cx).map(|p| p.path.display().to_string()))
+// }
+// Some(Err(e)) => panic!("Excluded file {path:?} failed to open: {e:?}"),
+// None => None,
+// }
+// .flatten()
+// })
+// .collect::<Vec<_>>()
+// });
+// opened_paths.sort();
+// assert_eq!(
+// opened_paths,
+// vec![
+// None,
+// Some(".git/HEAD".to_string()),
+// Some("excluded_dir/file".to_string()),
+// ],
+// "Excluded files should get opened, excluded dir should not get opened"
+// );
+
+// let entries = cx.read(|cx| workspace.file_project_paths(cx));
+// assert_eq!(
+// initial_entries, entries,
+// "Workspace entries should not change after opening excluded files and directories paths"
+// );
+
+// cx.read(|cx| {
+// let pane = workspace.read(cx).active_pane().read(cx);
+// let mut opened_buffer_paths = pane
+// .items()
+// .map(|i| {
+// i.project_path(cx)
+// .expect("all excluded files that got open should have a path")
+// .path
+// .display()
+// .to_string()
+// })
+// .collect::<Vec<_>>();
+// opened_buffer_paths.sort();
+// assert_eq!(
+// opened_buffer_paths,
+// vec![".git/HEAD".to_string(), "excluded_dir/file".to_string()],
+// "Despite not being present in the worktrees, buffers for excluded files are opened and added to the pane"
+// );
+// });
+// }
+
+// #[gpui::test]
+// async fn test_save_conflicting_item(cx: &mut TestAppContext) {
+// let app_state = init_test(cx);
+// app_state
+// .fs
+// .as_fake()
+// .insert_tree("/root", json!({ "a.txt": "" }))
+// .await;
+
+// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
+// let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+// let workspace = window.root(cx);
+
+// // Open a file within an existing worktree.
+// workspace
+// .update(cx, |view, cx| {
+// view.open_paths(vec![PathBuf::from("/root/a.txt")], true, cx)
+// })
+// .await;
+// let editor = cx.read(|cx| {
+// let pane = workspace.read(cx).active_pane().read(cx);
+// let item = pane.active_item().unwrap();
+// item.downcast::<Editor>().unwrap()
+// });
+
+// editor.update(cx, |editor, cx| editor.handle_input("x", cx));
+// app_state
+// .fs
+// .as_fake()
+// .insert_file("/root/a.txt", "changed".to_string())
+// .await;
+// editor
+// .condition(cx, |editor, cx| editor.has_conflict(cx))
+// .await;
+// cx.read(|cx| assert!(editor.is_dirty(cx)));
+
+// let save_task = workspace.update(cx, |workspace, cx| {
+// workspace.save_active_item(SaveIntent::Save, cx)
+// });
+// cx.foreground().run_until_parked();
+// window.simulate_prompt_answer(0, cx);
+// save_task.await.unwrap();
+// editor.read_with(cx, |editor, cx| {
+// assert!(!editor.is_dirty(cx));
+// assert!(!editor.has_conflict(cx));
+// });
+// }
+
+// #[gpui::test]
+// async fn test_open_and_save_new_file(cx: &mut TestAppContext) {
+// let app_state = init_test(cx);
+// app_state.fs.create_dir(Path::new("/root")).await.unwrap();
+
+// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
+// project.update(cx, |project, _| project.languages().add(rust_lang()));
+// let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+// let workspace = window.root(cx);
+// let worktree = cx.read(|cx| workspace.read(cx).worktrees(cx).next().unwrap());
+
+// // Create a new untitled buffer
+// cx.dispatch_action(window.into(), NewFile);
+// let editor = workspace.read_with(cx, |workspace, cx| {
+// workspace
+// .active_item(cx)
+// .unwrap()
+// .downcast::<Editor>()
+// .unwrap()
+// });
+
+// editor.update(cx, |editor, cx| {
+// assert!(!editor.is_dirty(cx));
+// assert_eq!(editor.title(cx), "untitled");
+// assert!(Arc::ptr_eq(
+// &editor.language_at(0, cx).unwrap(),
+// &languages::PLAIN_TEXT
+// ));
+// editor.handle_input("hi", cx);
+// assert!(editor.is_dirty(cx));
+// });
+
+// // Save the buffer. This prompts for a filename.
+// let save_task = workspace.update(cx, |workspace, cx| {
+// workspace.save_active_item(SaveIntent::Save, cx)
+// });
+// cx.foreground().run_until_parked();
+// cx.simulate_new_path_selection(|parent_dir| {
+// assert_eq!(parent_dir, Path::new("/root"));
+// Some(parent_dir.join("the-new-name.rs"))
+// });
+// cx.read(|cx| {
+// assert!(editor.is_dirty(cx));
+// assert_eq!(editor.read(cx).title(cx), "untitled");
+// });
+
+// // When the save completes, the buffer's title is updated and the language is assigned based
+// // on the path.
+// save_task.await.unwrap();
+// editor.read_with(cx, |editor, cx| {
+// assert!(!editor.is_dirty(cx));
+// assert_eq!(editor.title(cx), "the-new-name.rs");
+// assert_eq!(editor.language_at(0, cx).unwrap().name().as_ref(), "Rust");
+// });
+
+// // Edit the file and save it again. This time, there is no filename prompt.
+// editor.update(cx, |editor, cx| {
+// editor.handle_input(" there", cx);
+// assert!(editor.is_dirty(cx));
+// });
+// let save_task = workspace.update(cx, |workspace, cx| {
+// workspace.save_active_item(SaveIntent::Save, cx)
+// });
+// save_task.await.unwrap();
+// assert!(!cx.did_prompt_for_new_path());
+// editor.read_with(cx, |editor, cx| {
+// assert!(!editor.is_dirty(cx));
+// assert_eq!(editor.title(cx), "the-new-name.rs")
+// });
+
+// // Open the same newly-created file in another pane item. The new editor should reuse
+// // the same buffer.
+// cx.dispatch_action(window.into(), NewFile);
+// workspace
+// .update(cx, |workspace, cx| {
+// workspace.split_and_clone(
+// workspace.active_pane().clone(),
+// SplitDirection::Right,
+// cx,
+// );
+// workspace.open_path((worktree.read(cx).id(), "the-new-name.rs"), None, true, cx)
+// })
+// .await
+// .unwrap();
+// let editor2 = workspace.update(cx, |workspace, cx| {
+// workspace
+// .active_item(cx)
+// .unwrap()
+// .downcast::<Editor>()
+// .unwrap()
+// });
+// cx.read(|cx| {
+// assert_eq!(
+// editor2.read(cx).buffer().read(cx).as_singleton().unwrap(),
+// editor.read(cx).buffer().read(cx).as_singleton().unwrap()
+// );
+// })
+// }
+
+// #[gpui::test]
+// async fn test_setting_language_when_saving_as_single_file_worktree(cx: &mut TestAppContext) {
+// let app_state = init_test(cx);
+// app_state.fs.create_dir(Path::new("/root")).await.unwrap();
+
+// let project = Project::test(app_state.fs.clone(), [], cx).await;
+// project.update(cx, |project, _| project.languages().add(rust_lang()));
+// let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+// let workspace = window.root(cx);
+
+// // Create a new untitled buffer
+// cx.dispatch_action(window.into(), NewFile);
+// let editor = workspace.read_with(cx, |workspace, cx| {
+// workspace
+// .active_item(cx)
+// .unwrap()
+// .downcast::<Editor>()
+// .unwrap()
+// });
+
+// editor.update(cx, |editor, cx| {
+// assert!(Arc::ptr_eq(
+// &editor.language_at(0, cx).unwrap(),
+// &languages::PLAIN_TEXT
+// ));
+// editor.handle_input("hi", cx);
+// assert!(editor.is_dirty(cx));
+// });
+
+// // Save the buffer. This prompts for a filename.
+// let save_task = workspace.update(cx, |workspace, cx| {
+// workspace.save_active_item(SaveIntent::Save, cx)
+// });
+// cx.foreground().run_until_parked();
+// cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name.rs")));
+// save_task.await.unwrap();
+// // The buffer is not dirty anymore and the language is assigned based on the path.
+// editor.read_with(cx, |editor, cx| {
+// assert!(!editor.is_dirty(cx));
+// assert_eq!(editor.language_at(0, cx).unwrap().name().as_ref(), "Rust")
+// });
+// }
+
+// #[gpui::test]
+// async fn test_pane_actions(cx: &mut TestAppContext) {
+// let app_state = init_test(cx);
+// app_state
+// .fs
+// .as_fake()
+// .insert_tree(
+// "/root",
+// json!({
+// "a": {
+// "file1": "contents 1",
+// "file2": "contents 2",
+// "file3": "contents 3",
+// },
+// }),
+// )
+// .await;
+
+// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
+// let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+// let workspace = window.root(cx);
+
+// let entries = cx.read(|cx| workspace.file_project_paths(cx));
+// let file1 = entries[0].clone();
+
+// let pane_1 = cx.read(|cx| workspace.read(cx).active_pane().clone());
+
+// workspace
+// .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
+// .await
+// .unwrap();
+
+// let (editor_1, buffer) = pane_1.update(cx, |pane_1, cx| {
+// let editor = pane_1.active_item().unwrap().downcast::<Editor>().unwrap();
+// assert_eq!(editor.project_path(cx), Some(file1.clone()));
+// let buffer = editor.update(cx, |editor, cx| {
+// editor.insert("dirt", cx);
+// editor.buffer().downgrade()
+// });
+// (editor.downgrade(), buffer)
+// });
+
+// cx.dispatch_action(window.into(), pane::SplitRight);
+// let editor_2 = cx.update(|cx| {
+// let pane_2 = workspace.read(cx).active_pane().clone();
+// assert_ne!(pane_1, pane_2);
+
+// let pane2_item = pane_2.read(cx).active_item().unwrap();
+// assert_eq!(pane2_item.project_path(cx), Some(file1.clone()));
+
+// pane2_item.downcast::<Editor>().unwrap().downgrade()
+// });
+// cx.dispatch_action(
+// window.into(),
+// workspace::CloseActiveItem { save_intent: None },
+// );
+
+// cx.foreground().run_until_parked();
+// workspace.read_with(cx, |workspace, _| {
+// assert_eq!(workspace.panes().len(), 1);
+// assert_eq!(workspace.active_pane(), &pane_1);
+// });
+
+// cx.dispatch_action(
+// window.into(),
+// workspace::CloseActiveItem { save_intent: None },
+// );
+// cx.foreground().run_until_parked();
+// window.simulate_prompt_answer(1, cx);
+// cx.foreground().run_until_parked();
+
+// workspace.read_with(cx, |workspace, cx| {
+// assert_eq!(workspace.panes().len(), 1);
+// assert!(workspace.active_item(cx).is_none());
+// });
+
+// cx.assert_dropped(editor_1);
+// cx.assert_dropped(editor_2);
+// cx.assert_dropped(buffer);
+// }
+
+// #[gpui::test]
+// async fn test_navigation(cx: &mut TestAppContext) {
+// let app_state = init_test(cx);
+// app_state
+// .fs
+// .as_fake()
+// .insert_tree(
+// "/root",
+// json!({
+// "a": {
+// "file1": "contents 1\n".repeat(20),
+// "file2": "contents 2\n".repeat(20),
+// "file3": "contents 3\n".repeat(20),
+// },
+// }),
+// )
+// .await;
+
+// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
+// let workspace = cx
+// .add_window(|cx| Workspace::test_new(project.clone(), cx))
+// .root(cx);
+// let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
+
+// let entries = cx.read(|cx| workspace.file_project_paths(cx));
+// let file1 = entries[0].clone();
+// let file2 = entries[1].clone();
+// let file3 = entries[2].clone();
+
+// let editor1 = workspace
+// .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
+// .await
+// .unwrap()
+// .downcast::<Editor>()
+// .unwrap();
+// editor1.update(cx, |editor, cx| {
+// editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
+// s.select_display_ranges([DisplayPoint::new(10, 0)..DisplayPoint::new(10, 0)])
+// });
+// });
+// let editor2 = workspace
+// .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx))
+// .await
+// .unwrap()
+// .downcast::<Editor>()
+// .unwrap();
+// let editor3 = workspace
+// .update(cx, |w, cx| w.open_path(file3.clone(), None, true, cx))
+// .await
+// .unwrap()
+// .downcast::<Editor>()
+// .unwrap();
+
+// editor3
+// .update(cx, |editor, cx| {
+// editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
+// s.select_display_ranges([DisplayPoint::new(12, 0)..DisplayPoint::new(12, 0)])
+// });
+// editor.newline(&Default::default(), cx);
+// editor.newline(&Default::default(), cx);
+// editor.move_down(&Default::default(), cx);
+// editor.move_down(&Default::default(), cx);
+// editor.save(project.clone(), cx)
+// })
+// .await
+// .unwrap();
+// editor3.update(cx, |editor, cx| {
+// editor.set_scroll_position(vec2f(0., 12.5), cx)
+// });
+// assert_eq!(
+// active_location(&workspace, cx),
+// (file3.clone(), DisplayPoint::new(16, 0), 12.5)
+// );
+
+// workspace
+// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
+// .await
+// .unwrap();
+// assert_eq!(
+// active_location(&workspace, cx),
+// (file3.clone(), DisplayPoint::new(0, 0), 0.)
+// );
+
+// workspace
+// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
+// .await
+// .unwrap();
+// assert_eq!(
+// active_location(&workspace, cx),
+// (file2.clone(), DisplayPoint::new(0, 0), 0.)
+// );
+
+// workspace
+// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
+// .await
+// .unwrap();
+// assert_eq!(
+// active_location(&workspace, cx),
+// (file1.clone(), DisplayPoint::new(10, 0), 0.)
+// );
+
+// workspace
+// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
+// .await
+// .unwrap();
+// assert_eq!(
+// active_location(&workspace, cx),
+// (file1.clone(), DisplayPoint::new(0, 0), 0.)
+// );
+
+// // Go back one more time and ensure we don't navigate past the first item in the history.
+// workspace
+// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
+// .await
+// .unwrap();
+// assert_eq!(
+// active_location(&workspace, cx),
+// (file1.clone(), DisplayPoint::new(0, 0), 0.)
+// );
+
+// workspace
+// .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
+// .await
+// .unwrap();
+// assert_eq!(
+// active_location(&workspace, cx),
+// (file1.clone(), DisplayPoint::new(10, 0), 0.)
+// );
+
+// workspace
+// .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
+// .await
+// .unwrap();
+// assert_eq!(
+// active_location(&workspace, cx),
+// (file2.clone(), DisplayPoint::new(0, 0), 0.)
+// );
+
+// // Go forward to an item that has been closed, ensuring it gets re-opened at the same
+// // location.
+// pane.update(cx, |pane, cx| {
+// let editor3_id = editor3.id();
+// drop(editor3);
+// pane.close_item_by_id(editor3_id, SaveIntent::Close, cx)
+// })
+// .await
+// .unwrap();
+// workspace
+// .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
+// .await
+// .unwrap();
+// assert_eq!(
+// active_location(&workspace, cx),
+// (file3.clone(), DisplayPoint::new(0, 0), 0.)
+// );
+
+// workspace
+// .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
+// .await
+// .unwrap();
+// assert_eq!(
+// active_location(&workspace, cx),
+// (file3.clone(), DisplayPoint::new(16, 0), 12.5)
+// );
+
+// workspace
+// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
+// .await
+// .unwrap();
+// assert_eq!(
+// active_location(&workspace, cx),
+// (file3.clone(), DisplayPoint::new(0, 0), 0.)
+// );
+
+// // Go back to an item that has been closed and removed from disk, ensuring it gets skipped.
+// pane.update(cx, |pane, cx| {
+// let editor2_id = editor2.id();
+// drop(editor2);
+// pane.close_item_by_id(editor2_id, SaveIntent::Close, cx)
+// })
+// .await
+// .unwrap();
+// app_state
+// .fs
+// .remove_file(Path::new("/root/a/file2"), Default::default())
+// .await
+// .unwrap();
+// cx.foreground().run_until_parked();
+
+// workspace
+// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
+// .await
+// .unwrap();
+// assert_eq!(
+// active_location(&workspace, cx),
+// (file1.clone(), DisplayPoint::new(10, 0), 0.)
+// );
+// workspace
+// .update(cx, |w, cx| w.go_forward(w.active_pane().downgrade(), cx))
+// .await
+// .unwrap();
+// assert_eq!(
+// active_location(&workspace, cx),
+// (file3.clone(), DisplayPoint::new(0, 0), 0.)
+// );
+
+// // Modify file to collapse multiple nav history entries into the same location.
+// // Ensure we don't visit the same location twice when navigating.
+// editor1.update(cx, |editor, cx| {
+// editor.change_selections(None, cx, |s| {
+// s.select_display_ranges([DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)])
+// })
+// });
+
+// for _ in 0..5 {
+// editor1.update(cx, |editor, cx| {
+// editor.change_selections(None, cx, |s| {
+// s.select_display_ranges([DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)])
+// });
+// });
+// editor1.update(cx, |editor, cx| {
+// editor.change_selections(None, cx, |s| {
+// s.select_display_ranges([DisplayPoint::new(13, 0)..DisplayPoint::new(13, 0)])
+// })
+// });
+// }
+
+// editor1.update(cx, |editor, cx| {
+// editor.transact(cx, |editor, cx| {
+// editor.change_selections(None, cx, |s| {
+// s.select_display_ranges([DisplayPoint::new(2, 0)..DisplayPoint::new(14, 0)])
+// });
+// editor.insert("", cx);
+// })
+// });
+
+// editor1.update(cx, |editor, cx| {
+// editor.change_selections(None, cx, |s| {
+// s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
+// })
+// });
+// workspace
+// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
+// .await
+// .unwrap();
+// assert_eq!(
+// active_location(&workspace, cx),
+// (file1.clone(), DisplayPoint::new(2, 0), 0.)
+// );
+// workspace
+// .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx))
+// .await
+// .unwrap();
+// assert_eq!(
+// active_location(&workspace, cx),
+// (file1.clone(), DisplayPoint::new(3, 0), 0.)
+// );
+
+// fn active_location(
+// workspace: &ViewHandle<Workspace>,
+// cx: &mut TestAppContext,
+// ) -> (ProjectPath, DisplayPoint, f32) {
+// workspace.update(cx, |workspace, cx| {
+// let item = workspace.active_item(cx).unwrap();
+// let editor = item.downcast::<Editor>().unwrap();
+// let (selections, scroll_position) = editor.update(cx, |editor, cx| {
+// (
+// editor.selections.display_ranges(cx),
+// editor.scroll_position(cx),
+// )
+// });
+// (
+// item.project_path(cx).unwrap(),
+// selections[0].start,
+// scroll_position.y(),
+// )
+// })
+// }
+// }
+
+// #[gpui::test]
+// async fn test_reopening_closed_items(cx: &mut TestAppContext) {
+// let app_state = init_test(cx);
+// app_state
+// .fs
+// .as_fake()
+// .insert_tree(
+// "/root",
+// json!({
+// "a": {
+// "file1": "",
+// "file2": "",
+// "file3": "",
+// "file4": "",
+// },
+// }),
+// )
+// .await;
+
+// let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
+// let workspace = cx
+// .add_window(|cx| Workspace::test_new(project, cx))
+// .root(cx);
+// let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
+
+// let entries = cx.read(|cx| workspace.file_project_paths(cx));
+// let file1 = entries[0].clone();
+// let file2 = entries[1].clone();
+// let file3 = entries[2].clone();
+// let file4 = entries[3].clone();
+
+// let file1_item_id = workspace
+// .update(cx, |w, cx| w.open_path(file1.clone(), None, true, cx))
+// .await
+// .unwrap()
+// .id();
+// let file2_item_id = workspace
+// .update(cx, |w, cx| w.open_path(file2.clone(), None, true, cx))
+// .await
+// .unwrap()
+// .id();
+// let file3_item_id = workspace
+// .update(cx, |w, cx| w.open_path(file3.clone(), None, true, cx))
+// .await
+// .unwrap()
+// .id();
+// let file4_item_id = workspace
+// .update(cx, |w, cx| w.open_path(file4.clone(), None, true, cx))
+// .await
+// .unwrap()
+// .id();
+// assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
+
+// // Close all the pane items in some arbitrary order.
+// pane.update(cx, |pane, cx| {
+// pane.close_item_by_id(file1_item_id, SaveIntent::Close, cx)
+// })
+// .await
+// .unwrap();
+// assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
+
+// pane.update(cx, |pane, cx| {
+// pane.close_item_by_id(file4_item_id, SaveIntent::Close, cx)
+// })
+// .await
+// .unwrap();
+// assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
+
+// pane.update(cx, |pane, cx| {
+// pane.close_item_by_id(file2_item_id, SaveIntent::Close, cx)
+// })
+// .await
+// .unwrap();
+// assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
+
+// pane.update(cx, |pane, cx| {
+// pane.close_item_by_id(file3_item_id, SaveIntent::Close, cx)
+// })
+// .await
+// .unwrap();
+// assert_eq!(active_path(&workspace, cx), None);
+
+// // Reopen all the closed items, ensuring they are reopened in the same order
+// // in which they were closed.
+// workspace
+// .update(cx, Workspace::reopen_closed_item)
+// .await
+// .unwrap();
+// assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
+
+// workspace
+// .update(cx, Workspace::reopen_closed_item)
+// .await
+// .unwrap();
+// assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
+
+// workspace
+// .update(cx, Workspace::reopen_closed_item)
+// .await
+// .unwrap();
+// assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
+
+// workspace
+// .update(cx, Workspace::reopen_closed_item)
+// .await
+// .unwrap();
+// assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
+
+// // Reopening past the last closed item is a no-op.
+// workspace
+// .update(cx, Workspace::reopen_closed_item)
+// .await
+// .unwrap();
+// assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
+
+// // Reopening closed items doesn't interfere with navigation history.
+// workspace
+// .update(cx, |workspace, cx| {
+// workspace.go_back(workspace.active_pane().downgrade(), cx)
+// })
+// .await
+// .unwrap();
+// assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
+
+// workspace
+// .update(cx, |workspace, cx| {
+// workspace.go_back(workspace.active_pane().downgrade(), cx)
+// })
+// .await
+// .unwrap();
+// assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
+
+// workspace
+// .update(cx, |workspace, cx| {
+// workspace.go_back(workspace.active_pane().downgrade(), cx)
+// })
+// .await
+// .unwrap();
+// assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
+
+// workspace
+// .update(cx, |workspace, cx| {
+// workspace.go_back(workspace.active_pane().downgrade(), cx)
+// })
+// .await
+// .unwrap();
+// assert_eq!(active_path(&workspace, cx), Some(file4.clone()));
+
+// workspace
+// .update(cx, |workspace, cx| {
+// workspace.go_back(workspace.active_pane().downgrade(), cx)
+// })
+// .await
+// .unwrap();
+// assert_eq!(active_path(&workspace, cx), Some(file3.clone()));
+
+// workspace
+// .update(cx, |workspace, cx| {
+// workspace.go_back(workspace.active_pane().downgrade(), cx)
+// })
+// .await
+// .unwrap();
+// assert_eq!(active_path(&workspace, cx), Some(file2.clone()));
+
+// workspace
+// .update(cx, |workspace, cx| {
+// workspace.go_back(workspace.active_pane().downgrade(), cx)
+// })
+// .await
+// .unwrap();
+// assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
+
+// workspace
+// .update(cx, |workspace, cx| {
+// workspace.go_back(workspace.active_pane().downgrade(), cx)
+// })
+// .await
+// .unwrap();
+// assert_eq!(active_path(&workspace, cx), Some(file1.clone()));
+
+// fn active_path(
+// workspace: &ViewHandle<Workspace>,
+// cx: &TestAppContext,
+// ) -> Option<ProjectPath> {
+// workspace.read_with(cx, |workspace, cx| {
+// let item = workspace.active_item(cx)?;
+// item.project_path(cx)
+// })
+// }
+// }
+
+// #[gpui::test]
+// async fn test_base_keymap(cx: &mut gpui::TestAppContext) {
+// struct TestView;
+
+// impl Entity for TestView {
+// type Event = ();
+// }
+
+// impl View for TestView {
+// fn ui_name() -> &'static str {
+// "TestView"
+// }
+
+// fn render(&mut self, _: &mut ViewContext<Self>) -> AnyElement<Self> {
+// Empty::new().into_any()
+// }
+// }
+
+// let executor = cx.background();
+// let fs = FakeFs::new(executor.clone());
+
+// actions!(test, [A, B]);
+// // From the Atom keymap
+// actions!(workspace, [ActivatePreviousPane]);
+// // From the JetBrains keymap
+// actions!(pane, [ActivatePrevItem]);
+
+// fs.save(
+// "/settings.json".as_ref(),
+// &r#"
+// {
+// "base_keymap": "Atom"
+// }
+// "#
+// .into(),
+// Default::default(),
+// )
+// .await
+// .unwrap();
+
+// fs.save(
+// "/keymap.json".as_ref(),
+// &r#"
+// [
+// {
+// "bindings": {
+// "backspace": "test::A"
+// }
+// }
+// ]
+// "#
+// .into(),
+// Default::default(),
+// )
+// .await
+// .unwrap();
+
+// cx.update(|cx| {
+// cx.set_global(SettingsStore::test(cx));
+// theme::init(Assets, cx);
+// welcome::init(cx);
+
+// cx.add_global_action(|_: &A, _cx| {});
+// cx.add_global_action(|_: &B, _cx| {});
+// cx.add_global_action(|_: &ActivatePreviousPane, _cx| {});
+// cx.add_global_action(|_: &ActivatePrevItem, _cx| {});
+
+// let settings_rx = watch_config_file(
+// executor.clone(),
+// fs.clone(),
+// PathBuf::from("/settings.json"),
+// );
+// let keymap_rx =
+// watch_config_file(executor.clone(), fs.clone(), PathBuf::from("/keymap.json"));
+
+// handle_keymap_file_changes(keymap_rx, cx);
+// handle_settings_file_changes(settings_rx, cx);
+// });
+
+// cx.foreground().run_until_parked();
+
+// let window = cx.add_window(|_| TestView);
+
+// // Test loading the keymap base at all
+// assert_key_bindings_for(
+// window.into(),
+// cx,
+// vec![("backspace", &A), ("k", &ActivatePreviousPane)],
+// line!(),
+// );
+
+// // Test modifying the users keymap, while retaining the base keymap
+// fs.save(
+// "/keymap.json".as_ref(),
+// &r#"
+// [
+// {
+// "bindings": {
+// "backspace": "test::B"
+// }
+// }
+// ]
+// "#
+// .into(),
+// Default::default(),
+// )
+// .await
+// .unwrap();
+
+// cx.foreground().run_until_parked();
+
+// assert_key_bindings_for(
+// window.into(),
+// cx,
+// vec![("backspace", &B), ("k", &ActivatePreviousPane)],
+// line!(),
+// );
+
+// // Test modifying the base, while retaining the users keymap
+// fs.save(
+// "/settings.json".as_ref(),
+// &r#"
+// {
+// "base_keymap": "JetBrains"
+// }
+// "#
+// .into(),
+// Default::default(),
+// )
+// .await
+// .unwrap();
+
+// cx.foreground().run_until_parked();
+
+// assert_key_bindings_for(
+// window.into(),
+// cx,
+// vec![("backspace", &B), ("[", &ActivatePrevItem)],
+// line!(),
+// );
+
+// #[track_caller]
+// fn assert_key_bindings_for<'a>(
+// window: AnyWindowHandle,
+// cx: &TestAppContext,
+// actions: Vec<(&'static str, &'a dyn Action)>,
+// line: u32,
+// ) {
+// for (key, action) in actions {
+// // assert that...
+// assert!(
+// cx.available_actions(window, 0)
+// .into_iter()
+// .any(|(_, bound_action, b)| {
+// // action names match...
+// bound_action.name() == action.name()
+// && bound_action.namespace() == action.namespace()
+// // and key strokes contain the given key
+// && b.iter()
+// .any(|binding| binding.keystrokes().iter().any(|k| k.key == key))
+// }),
+// "On {} Failed to find {} with key binding {}",
+// line,
+// action.name(),
+// key
+// );
+// }
+// }
+// }
+
+// #[gpui::test]
+// async fn test_disabled_keymap_binding(cx: &mut gpui::TestAppContext) {
+// struct TestView;
+
+// impl Entity for TestView {
+// type Event = ();
+// }
+
+// impl View for TestView {
+// fn ui_name() -> &'static str {
+// "TestView"
+// }
+
+// fn render(&mut self, _: &mut ViewContext<Self>) -> AnyElement<Self> {
+// Empty::new().into_any()
+// }
+// }
+
+// let executor = cx.background();
+// let fs = FakeFs::new(executor.clone());
+
+// actions!(test, [A, B]);
+// // From the Atom keymap
+// actions!(workspace, [ActivatePreviousPane]);
+// // From the JetBrains keymap
+// actions!(pane, [ActivatePrevItem]);
+
+// fs.save(
+// "/settings.json".as_ref(),
+// &r#"
+// {
+// "base_keymap": "Atom"
+// }
+// "#
+// .into(),
+// Default::default(),
+// )
+// .await
+// .unwrap();
+
+// fs.save(
+// "/keymap.json".as_ref(),
+// &r#"
+// [
+// {
+// "bindings": {
+// "backspace": "test::A"
+// }
+// }
+// ]
+// "#
+// .into(),
+// Default::default(),
+// )
+// .await
+// .unwrap();
+
+// cx.update(|cx| {
+// cx.set_global(SettingsStore::test(cx));
+// theme::init(Assets, cx);
+// welcome::init(cx);
+
+// cx.add_global_action(|_: &A, _cx| {});
+// cx.add_global_action(|_: &B, _cx| {});
+// cx.add_global_action(|_: &ActivatePreviousPane, _cx| {});
+// cx.add_global_action(|_: &ActivatePrevItem, _cx| {});
+
+// let settings_rx = watch_config_file(
+// executor.clone(),
+// fs.clone(),
+// PathBuf::from("/settings.json"),
+// );
+// let keymap_rx =
+// watch_config_file(executor.clone(), fs.clone(), PathBuf::from("/keymap.json"));
+
+// handle_keymap_file_changes(keymap_rx, cx);
+// handle_settings_file_changes(settings_rx, cx);
+// });
+
+// cx.foreground().run_until_parked();
+
+// let window = cx.add_window(|_| TestView);
+
+// // Test loading the keymap base at all
+// assert_key_bindings_for(
+// window.into(),
+// cx,
+// vec![("backspace", &A), ("k", &ActivatePreviousPane)],
+// line!(),
+// );
+
+// // Test disabling the key binding for the base keymap
+// fs.save(
+// "/keymap.json".as_ref(),
+// &r#"
+// [
+// {
+// "bindings": {
+// "backspace": null
+// }
+// }
+// ]
+// "#
+// .into(),
+// Default::default(),
+// )
+// .await
+// .unwrap();
+
+// cx.foreground().run_until_parked();
+
+// assert_key_bindings_for(
+// window.into(),
+// cx,
+// vec![("k", &ActivatePreviousPane)],
+// line!(),
+// );
+
+// // Test modifying the base, while retaining the users keymap
+// fs.save(
+// "/settings.json".as_ref(),
+// &r#"
+// {
+// "base_keymap": "JetBrains"
+// }
+// "#
+// .into(),
+// Default::default(),
+// )
+// .await
+// .unwrap();
+
+// cx.foreground().run_until_parked();
+
+// assert_key_bindings_for(window.into(), cx, vec![("[", &ActivatePrevItem)], line!());
+
+// #[track_caller]
+// fn assert_key_bindings_for<'a>(
+// window: AnyWindowHandle,
+// cx: &TestAppContext,
+// actions: Vec<(&'static str, &'a dyn Action)>,
+// line: u32,
+// ) {
+// for (key, action) in actions {
+// // assert that...
+// assert!(
+// cx.available_actions(window, 0)
+// .into_iter()
+// .any(|(_, bound_action, b)| {
+// // action names match...
+// bound_action.name() == action.name()
+// && bound_action.namespace() == action.namespace()
+// // and key strokes contain the given key
+// && b.iter()
+// .any(|binding| binding.keystrokes().iter().any(|k| k.key == key))
+// }),
+// "On {} Failed to find {} with key binding {}",
+// line,
+// action.name(),
+// key
+// );
+// }
+// }
+// }
+
+// #[gpui::test]
+// fn test_bundled_settings_and_themes(cx: &mut AppContext) {
+// cx.platform()
+// .fonts()
+// .add_fonts(&[
+// Assets
+// .load("fonts/zed-sans/zed-sans-extended.ttf")
+// .unwrap()
+// .to_vec()
+// .into(),
+// Assets
+// .load("fonts/zed-mono/zed-mono-extended.ttf")
+// .unwrap()
+// .to_vec()
+// .into(),
+// Assets
+// .load("fonts/plex/IBMPlexSans-Regular.ttf")
+// .unwrap()
+// .to_vec()
+// .into(),
+// ])
+// .unwrap();
+// let themes = ThemeRegistry::new(Assets, cx.font_cache().clone());
+// let mut settings = SettingsStore::default();
+// settings
+// .set_default_settings(&settings::default_settings(), cx)
+// .unwrap();
+// cx.set_global(settings);
+// theme::init(Assets, cx);
+
+// let mut has_default_theme = false;
+// for theme_name in themes.list(false).map(|meta| meta.name) {
+// let theme = themes.get(&theme_name).unwrap();
+// assert_eq!(theme.meta.name, theme_name);
+// if theme.meta.name == settings::get::<ThemeSettings>(cx).theme.meta.name {
+// has_default_theme = true;
+// }
+// }
+// assert!(has_default_theme);
+// }
+
+// #[gpui::test]
+// fn test_bundled_languages(cx: &mut AppContext) {
+// cx.set_global(SettingsStore::test(cx));
+// let mut languages = LanguageRegistry::test();
+// languages.set_executor(cx.background().clone());
+// let languages = Arc::new(languages);
+// let node_runtime = node_runtime::FakeNodeRuntime::new();
+// languages::init(languages.clone(), node_runtime, cx);
+// for name in languages.language_names() {
+// languages.language_for_name(&name);
+// }
+// cx.foreground().run_until_parked();
+// }
+
+// fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
+// cx.foreground().forbid_parking();
+// cx.update(|cx| {
+// let mut app_state = AppState::test(cx);
+// let state = Arc::get_mut(&mut app_state).unwrap();
+// state.initialize_workspace = initialize_workspace;
+// state.build_window_options = build_window_options;
+// theme::init((), cx);
+// audio::init((), cx);
+// channel::init(&app_state.client, app_state.user_store.clone(), cx);
+// call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
+// notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx);
+// workspace::init(app_state.clone(), cx);
+// Project::init_settings(cx);
+// language::init(cx);
+// editor::init(cx);
+// project_panel::init_settings(cx);
+// collab_ui::init(&app_state, cx);
+// pane::init(cx);
+// project_panel::init((), cx);
+// terminal_view::init(cx);
+// assistant::init(cx);
+// app_state
+// })
+// }
+
+// fn rust_lang() -> Arc<language::Language> {
+// Arc::new(language::Language::new(
+// language::LanguageConfig {
+// name: "Rust".into(),
+// path_suffixes: vec!["rs".to_string()],
+// ..Default::default()
+// },
+// Some(tree_sitter_rust::language()),
+// ))
+// }
+// }