Merge remote-tracking branch 'origin/main' into assistant-2

Antonio Scandurra created

Change summary

Cargo.lock                                                     |   12 
Cargo.toml                                                     |    1 
assets/keymaps/default.json                                    |   40 
crates/breadcrumbs2/src/breadcrumbs.rs                         |   81 
crates/collab/src/tests/following_tests.rs                     |  137 
crates/collab/src/tests/integration_tests.rs                   |   27 
crates/collab/src/tests/random_project_collaboration_tests.rs  |    1 
crates/collab2/src/tests/following_tests.rs                    |  425 
crates/collab2/src/tests/integration_tests.rs                  |   32 
crates/collab2/src/tests/random_project_collaboration_tests.rs |    1 
crates/collab_ui2/src/collab_panel.rs                          |  226 
crates/diagnostics2/src/diagnostics.rs                         |   47 
crates/editor2/src/editor.rs                                   |   27 
crates/editor2/src/editor_tests.rs                             |    4 
crates/editor2/src/items.rs                                    |  118 
crates/gpui2/src/elements/canvas.rs                            |   10 
crates/gpui2/src/platform/mac/display_linker.rs                |    3 
crates/gpui2/src/platform/test/platform.rs                     |   29 
crates/project/src/project.rs                                  |  207 
crates/project/src/project_tests.rs                            |   88 
crates/project/src/search.rs                                   |   22 
crates/project/src/worktree.rs                                 |  173 
crates/project/src/worktree_tests.rs                           |   36 
crates/project2/src/project2.rs                                |  208 
crates/project2/src/project_tests.rs                           |   88 
crates/project2/src/search.rs                                  |   22 
crates/project2/src/worktree.rs                                |  172 
crates/project2/src/worktree_tests.rs                          |   36 
crates/project_panel/src/project_panel.rs                      |   55 
crates/project_panel2/src/project_panel.rs                     |   55 
crates/quick_action_bar2/Cargo.toml                            |   22 
crates/quick_action_bar2/src/quick_action_bar.rs               |  288 
crates/rpc/proto/zed.proto                                     |    4 
crates/rpc/src/rpc.rs                                          |    2 
crates/rpc2/proto/zed.proto                                    |    4 
crates/rpc2/src/rpc.rs                                         |    2 
crates/terminal_view/src/terminal_view.rs                      |    1 
crates/terminal_view2/src/terminal_view.rs                     |    7 
crates/theme2/src/default_colors.rs                            |   24 
crates/theme2/src/default_theme.rs                             |   80 
crates/theme2/src/registry.rs                                  |    6 
crates/theme2/src/styles/syntax.rs                             |   16 
crates/theme_selector2/src/theme_selector.rs                   |   14 
crates/ui2/src/components.rs                                   |    2 
crates/ui2/src/components/icon.rs                              |   20 
crates/ui2/src/components/indicator.rs                         |   60 
crates/ui2/src/prelude.rs                                      |    1 
crates/util/src/paths.rs                                       |   18 
crates/welcome2/src/welcome.rs                                 |    6 
crates/workspace/src/pane.rs                                   |   17 
crates/workspace/src/workspace.rs                              |    4 
crates/workspace2/src/item.rs                                  |   48 
crates/workspace2/src/pane.rs                                  |  393 
crates/workspace2/src/shared_screen.rs                         |   10 
crates/workspace2/src/toolbar.rs                               |   24 
crates/workspace2/src/workspace2.rs                            |  152 
crates/zed/src/zed.rs                                          |  126 
crates/zed2/Cargo.toml                                         |    2 
crates/zed2/src/zed2.rs                                        | 1865 +++
59 files changed, 4,273 insertions(+), 1,328 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -7112,6 +7112,17 @@ dependencies = [
  "workspace",
 ]
 
+[[package]]
+name = "quick_action_bar2"
+version = "0.1.0"
+dependencies = [
+ "editor2",
+ "gpui2",
+ "search2",
+ "ui2",
+ "workspace2",
+]
+
 [[package]]
 name = "quote"
 version = "1.0.33"
@@ -11932,6 +11943,7 @@ dependencies = [
  "postage",
  "project2",
  "project_panel2",
+ "quick_action_bar2",
  "rand 0.8.5",
  "regex",
  "rope2",

Cargo.toml 🔗

@@ -90,6 +90,7 @@ members = [
     "crates/project_panel",
     "crates/project_panel2",
     "crates/project_symbols",
+    "crates/quick_action_bar2",
     "crates/recent_projects",
     "crates/rope",
     "crates/rpc",

assets/keymaps/default.json 🔗

@@ -17,18 +17,8 @@
       "cmd-enter": "menu::SecondaryConfirm",
       "escape": "menu::Cancel",
       "ctrl-c": "menu::Cancel",
-      "cmd-{": "pane::ActivatePrevItem",
-      "cmd-}": "pane::ActivateNextItem",
-      "alt-cmd-left": "pane::ActivatePrevItem",
-      "alt-cmd-right": "pane::ActivateNextItem",
-      "cmd-w": "pane::CloseActiveItem",
-      "alt-cmd-t": "pane::CloseInactiveItems",
-      "ctrl-alt-cmd-w": "workspace::CloseInactiveTabsAndPanes",
-      "cmd-k u": "pane::CloseCleanItems",
-      "cmd-k cmd-w": "pane::CloseAllItems",
       "cmd-shift-w": "workspace::CloseWindow",
-      "cmd-s": "workspace::Save",
-      "cmd-shift-s": "workspace::SaveAs",
+      "cmd-o": "workspace::Open",
       "cmd-=": "zed::IncreaseBufferFontSize",
       "cmd-+": "zed::IncreaseBufferFontSize",
       "cmd--": "zed::DecreaseBufferFontSize",
@@ -38,15 +28,7 @@
       "cmd-h": "zed::Hide",
       "alt-cmd-h": "zed::HideOthers",
       "cmd-m": "zed::Minimize",
-      "ctrl-cmd-f": "zed::ToggleFullScreen",
-      "cmd-n": "workspace::NewFile",
-      "cmd-shift-n": "workspace::NewWindow",
-      "cmd-o": "workspace::Open",
-      "alt-cmd-o": "projects::OpenRecent",
-      "alt-cmd-b": "branches::OpenRecent",
-      "ctrl-~": "workspace::NewTerminal",
-      "ctrl-`": "terminal_panel::ToggleFocus",
-      "shift-escape": "workspace::ToggleZoom"
+      "ctrl-cmd-f": "zed::ToggleFullScreen"
     }
   },
   {
@@ -284,6 +266,15 @@
   {
     "context": "Pane",
     "bindings": {
+      "cmd-{": "pane::ActivatePrevItem",
+      "cmd-}": "pane::ActivateNextItem",
+      "alt-cmd-left": "pane::ActivatePrevItem",
+      "alt-cmd-right": "pane::ActivateNextItem",
+      "cmd-w": "pane::CloseActiveItem",
+      "alt-cmd-t": "pane::CloseInactiveItems",
+      "ctrl-alt-cmd-w": "workspace::CloseInactiveTabsAndPanes",
+      "cmd-k u": "pane::CloseCleanItems",
+      "cmd-k cmd-w": "pane::CloseAllItems",
       "cmd-f": "project_search::ToggleFocus",
       "cmd-g": "search::SelectNextMatch",
       "cmd-shift-g": "search::SelectPrevMatch",
@@ -389,6 +380,15 @@
   {
     "context": "Workspace",
     "bindings": {
+      "alt-cmd-o": "projects::OpenRecent",
+      "alt-cmd-b": "branches::OpenRecent",
+      "ctrl-~": "workspace::NewTerminal",
+      "cmd-s": "workspace::Save",
+      "cmd-shift-s": "workspace::SaveAs",
+      "cmd-n": "workspace::NewFile",
+      "cmd-shift-n": "workspace::NewWindow",
+      "ctrl-`": "terminal_panel::ToggleFocus",
+      "shift-escape": "workspace::ToggleZoom",
       "cmd-1": ["workspace::ActivatePane", 0],
       "cmd-2": ["workspace::ActivatePane", 1],
       "cmd-3": ["workspace::ActivatePane", 2],

crates/breadcrumbs2/src/breadcrumbs.rs 🔗

@@ -1,10 +1,10 @@
 use gpui::{
-    Component, Element, EventEmitter, IntoElement, ParentElement, Render, StyledText, Subscription,
+    Div, Element, EventEmitter, IntoElement, ParentElement, Render, StyledText, Subscription,
     ViewContext, WeakView,
 };
 use itertools::Itertools;
 use theme::ActiveTheme;
-use ui::{ButtonCommon, ButtonLike, ButtonStyle, Clickable, Disableable, Label};
+use ui::{prelude::*, ButtonLike, ButtonStyle, Label};
 use workspace::{
     item::{ItemEvent, ItemHandle},
     ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
@@ -36,54 +36,51 @@ impl EventEmitter<Event> for Breadcrumbs {}
 impl EventEmitter<ToolbarItemEvent> for Breadcrumbs {}
 
 impl Render for Breadcrumbs {
-    type Element = Component<ButtonLike>;
+    type Element = Div;
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
-        let button = ButtonLike::new("breadcrumbs")
-            .style(ButtonStyle::Transparent)
-            .disabled(true);
+        let element = h_stack().text_ui();
+
+        let Some(active_item) = &self
+            .active_item
+            .as_ref()
+            .filter(|item| item.downcast::<editor::Editor>().is_some())
+        else {
+            return element;
+        };
 
-        let active_item = match &self.active_item {
-            Some(active_item) => active_item,
-            None => return button.into_element(),
+        let Some(segments) = active_item.breadcrumbs(cx.theme(), cx) else {
+            return element;
         };
-        let not_editor = active_item.downcast::<editor::Editor>().is_none();
 
-        let breadcrumbs = match active_item.breadcrumbs(cx.theme(), cx) {
-            Some(breadcrumbs) => breadcrumbs,
-            None => return button.into_element(),
-        }
-        .into_iter()
-        .map(|breadcrumb| {
-            StyledText::new(breadcrumb.text)
-                .with_highlights(&cx.text_style(), breadcrumb.highlights.unwrap_or_default())
+        let highlighted_segments = segments.into_iter().map(|segment| {
+            StyledText::new(segment.text)
+                .with_highlights(&cx.text_style(), segment.highlights.unwrap_or_default())
                 .into_any()
         });
+        let breadcrumbs = Itertools::intersperse_with(highlighted_segments, || {
+            Label::new("›").into_any_element()
+        });
 
-        let button = button.children(Itertools::intersperse_with(breadcrumbs, || {
-            Label::new(" › ").into_any_element()
-        }));
-
-        if not_editor || !self.pane_focused {
-            return button.into_element();
-        }
-
-        // let this = cx.view().downgrade();
-        button
-            .style(ButtonStyle::Filled)
-            .disabled(false)
-            .on_click(move |_, _cx| {
-                todo!("outline::toggle");
-                // this.update(cx, |this, cx| {
-                //     if let Some(workspace) = this.workspace.upgrade() {
-                //         workspace.update(cx, |_workspace, _cx| {
-                //             outline::toggle(workspace, &Default::default(), cx)
-                //         })
-                //     }
-                // })
-                // .ok();
-            })
-            .into_element()
+        element.child(
+            ButtonLike::new("toggle outline view")
+                .style(ButtonStyle::Subtle)
+                .child(h_stack().gap_1().children(breadcrumbs))
+                // We disable the button when it is not focused
+                // due to ... @julia what was the reason again?
+                .disabled(!self.pane_focused)
+                .on_click(move |_, _cx| {
+                    todo!("outline::toggle");
+                    // this.update(cx, |this, cx| {
+                    //     if let Some(workspace) = this.workspace.upgrade() {
+                    //         workspace.update(cx, |_workspace, _cx| {
+                    //             outline::toggle(workspace, &Default::default(), cx)
+                    //         })
+                    //     }
+                    // })
+                    // .ok();
+                }),
+        )
     }
 }
 

crates/collab/src/tests/following_tests.rs 🔗

@@ -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>> {

crates/collab/src/tests/integration_tests.rs 🔗

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

crates/collab2/src/tests/following_tests.rs 🔗

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

crates/collab2/src/tests/integration_tests.rs 🔗

@@ -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, _| {

crates/collab_ui2/src/collab_panel.rs 🔗

@@ -169,7 +169,7 @@ use editor::Editor;
 use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt};
 use fuzzy::{match_strings, StringMatchCandidate};
 use gpui::{
-    actions, canvas, div, img, overlay, point, prelude::*, px, rems, serde_json, Action,
+    actions, canvas, div, img, overlay, point, prelude::*, px, rems, serde_json, size, Action,
     AppContext, AsyncWindowContext, Bounds, ClipboardItem, DismissEvent, Div, EventEmitter,
     FocusHandle, Focusable, FocusableView, Hsla, InteractiveElement, IntoElement, Length, Model,
     MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Quad, Render, RenderOnce,
@@ -1204,14 +1204,9 @@ impl CollabPanel {
                         .detach_and_log_err(cx);
                 });
             }))
-            .left_child(IconButton::new(0, Icon::Folder))
-            .child(
-                h_stack()
-                    .w_full()
-                    .justify_between()
-                    .child(render_tree_branch(is_last, cx))
-                    .child(Label::new(project_name.clone())),
-            )
+            .left_child(render_tree_branch(is_last, cx))
+            .child(IconButton::new(0, Icon::Folder))
+            .child(Label::new(project_name.clone()))
             .tooltip(move |cx| Tooltip::text(format!("Open {}", project_name), cx))
 
         //         enum JoinProject {}
@@ -1299,70 +1294,20 @@ impl CollabPanel {
         is_last: bool,
         cx: &mut ViewContext<Self>,
     ) -> impl IntoElement {
-        //         enum OpenSharedScreen {}
-
-        //         let host_avatar_width = theme
-        //             .contact_avatar
-        //             .width
-        //             .or(theme.contact_avatar.height)
-        //             .unwrap_or(0.);
-        //         let tree_branch = theme.tree_branch;
-
-        //         let handler = MouseEventHandler::new::<OpenSharedScreen, _>(
-        //             peer_id.map(|id| id.as_u64()).unwrap_or(0) as usize,
-        //             cx,
-        //             |mouse_state, cx| {
-        //                 let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state);
-        //                 let row = theme
-        //                     .project_row
-        //                     .in_state(is_selected)
-        //                     .style_for(mouse_state);
-
-        //                 Flex::row()
-        //                     .with_child(render_tree_branch(
-        //                         tree_branch,
-        //                         &row.name.text,
-        //                         is_last,
-        //                         vec2f(host_avatar_width, theme.row_height),
-        //                         cx.font_cache(),
-        //                     ))
-        //                     .with_child(
-        //                         Svg::new("icons/desktop.svg")
-        //                             .with_color(theme.channel_hash.color)
-        //                             .constrained()
-        //                             .with_width(theme.channel_hash.width)
-        //                             .aligned()
-        //                             .left(),
-        //                     )
-        //                     .with_child(
-        //                         Label::new("Screen", row.name.text.clone())
-        //                             .aligned()
-        //                             .left()
-        //                             .contained()
-        //                             .with_style(row.name.container)
-        //                             .flex(1., false),
-        //                     )
-        //                     .constrained()
-        //                     .with_height(theme.row_height)
-        //                     .contained()
-        //                     .with_style(row.container)
-        //             },
-        //         );
-        //         if peer_id.is_none() {
-        //             return handler.into_any();
-        //         }
-        //         handler
-        //             .with_cursor_style(CursorStyle::PointingHand)
-        //             .on_click(MouseButton::Left, move |_, this, cx| {
-        //                 if let Some(workspace) = this.workspace.upgrade(cx) {
-        //                     workspace.update(cx, |workspace, cx| {
-        //                         workspace.open_shared_screen(peer_id.unwrap(), cx)
-        //                     });
-        //                 }
-        //             })
-        //             .into_any()
-
-        div()
+        let id = peer_id.map_or(usize::MAX, |id| id.as_u64() as usize);
+
+        ListItem::new(("screen", id))
+            .left_child(render_tree_branch(is_last, cx))
+            .child(IconButton::new(0, Icon::Screen))
+            .child(Label::new("Screen"))
+            .when_some(peer_id, |this, _| {
+                this.on_click(cx.listener(move |this, _, cx| {
+                    this.workspace.update(cx, |workspace, cx| {
+                        workspace.open_shared_screen(peer_id.unwrap(), cx)
+                    });
+                }))
+                .tooltip(move |cx| Tooltip::text(format!("Open shared screen"), cx))
+            })
     }
 
     fn take_editing_state(&mut self, cx: &mut ViewContext<Self>) -> bool {
@@ -1415,54 +1360,14 @@ impl CollabPanel {
         channel_id: ChannelId,
         cx: &mut ViewContext<Self>,
     ) -> impl IntoElement {
-        //         enum ChannelNotes {}
-        //         let host_avatar_width = theme
-        //             .contact_avatar
-        //             .width
-        //             .or(theme.contact_avatar.height)
-        //             .unwrap_or(0.);
-
-        //         MouseEventHandler::new::<ChannelNotes, _>(ix as usize, cx, |state, cx| {
-        //             let tree_branch = *theme.tree_branch.in_state(is_selected).style_for(state);
-        //             let row = theme.project_row.in_state(is_selected).style_for(state);
-
-        //             Flex::<Self>::row()
-        //                 .with_child(render_tree_branch(
-        //                     tree_branch,
-        //                     &row.name.text,
-        //                     false,
-        //                     vec2f(host_avatar_width, theme.row_height),
-        //                     cx.font_cache(),
-        //                 ))
-        //                 .with_child(
-        //                     Svg::new("icons/file.svg")
-        //                         .with_color(theme.channel_hash.color)
-        //                         .constrained()
-        //                         .with_width(theme.channel_hash.width)
-        //                         .aligned()
-        //                         .left(),
-        //                 )
-        //                 .with_child(
-        //                     Label::new("notes", theme.channel_name.text.clone())
-        //                         .contained()
-        //                         .with_style(theme.channel_name.container)
-        //                         .aligned()
-        //                         .left()
-        //                         .flex(1., true),
-        //                 )
-        //                 .constrained()
-        //                 .with_height(theme.row_height)
-        //                 .contained()
-        //                 .with_style(*theme.channel_row.style_for(is_selected, state))
-        //                 .with_padding_left(theme.channel_row.default_style().padding.left)
-        //         })
-        //         .on_click(MouseButton::Left, move |_, this, cx| {
-        //             this.open_channel_notes(&OpenChannelNotes { channel_id }, cx);
-        //         })
-        //         .with_cursor_style(CursorStyle::PointingHand)
-        //         .into_any()
-
-        div()
+        ListItem::new("channel-notes")
+            .on_click(cx.listener(move |this, _, cx| {
+                this.open_channel_notes(channel_id, cx);
+            }))
+            .left_child(render_tree_branch(false, cx))
+            .child(IconButton::new(0, Icon::File))
+            .child(Label::new("notes"))
+            .tooltip(move |cx| Tooltip::text("Open Channel Notes", cx))
     }
 
     fn render_channel_chat(
@@ -1470,53 +1375,14 @@ impl CollabPanel {
         channel_id: ChannelId,
         cx: &mut ViewContext<Self>,
     ) -> impl IntoElement {
-        //         enum ChannelChat {}
-        //         let host_avatar_width = theme
-        //             .contact_avatar
-        //             .width
-        //             .or(theme.contact_avatar.height)
-        //             .unwrap_or(0.);
-
-        //         MouseEventHandler::new::<ChannelChat, _>(ix as usize, cx, |state, cx| {
-        //             let tree_branch = *theme.tree_branch.in_state(is_selected).style_for(state);
-        //             let row = theme.project_row.in_state(is_selected).style_for(state);
-
-        //             Flex::<Self>::row()
-        //                 .with_child(render_tree_branch(
-        //                     tree_branch,
-        //                     &row.name.text,
-        //                     true,
-        //                     vec2f(host_avatar_width, theme.row_height),
-        //                     cx.font_cache(),
-        //                 ))
-        //                 .with_child(
-        //                     Svg::new("icons/conversations.svg")
-        //                         .with_color(theme.channel_hash.color)
-        //                         .constrained()
-        //                         .with_width(theme.channel_hash.width)
-        //                         .aligned()
-        //                         .left(),
-        //                 )
-        //                 .with_child(
-        //                     Label::new("chat", theme.channel_name.text.clone())
-        //                         .contained()
-        //                         .with_style(theme.channel_name.container)
-        //                         .aligned()
-        //                         .left()
-        //                         .flex(1., true),
-        //                 )
-        //                 .constrained()
-        //                 .with_height(theme.row_height)
-        //                 .contained()
-        //                 .with_style(*theme.channel_row.style_for(is_selected, state))
-        //                 .with_padding_left(theme.channel_row.default_style().padding.left)
-        //         })
-        //         .on_click(MouseButton::Left, move |_, this, cx| {
-        //             this.join_channel_chat(&JoinChannelChat { channel_id }, cx);
-        //         })
-        //         .with_cursor_style(CursorStyle::PointingHand)
-        //         .into_any()
-        div()
+        ListItem::new("channel-chat")
+            .on_click(cx.listener(move |this, _, cx| {
+                this.join_channel_chat(channel_id, cx);
+            }))
+            .left_child(render_tree_branch(true, cx))
+            .child(IconButton::new(0, Icon::MessageBubbles))
+            .child(Label::new("chat"))
+            .tooltip(move |cx| Tooltip::text("Open Chat", cx))
     }
 
     //     fn render_channel_invite(
@@ -3119,30 +2985,24 @@ impl CollabPanel {
 }
 
 fn render_tree_branch(is_last: bool, cx: &mut WindowContext) -> impl IntoElement {
-    let text_style = cx.text_style();
     let rem_size = cx.rem_size();
-    let text_system = cx.text_system();
-    let font_id = text_system.font_id(&text_style.font()).unwrap();
-    let font_size = text_style.font_size.to_pixels(rem_size);
-    let line_height = text_style.line_height_in_pixels(rem_size);
-    let cap_height = text_system.cap_height(font_id, font_size);
-    let baseline_offset = text_system.baseline_offset(font_id, font_size, line_height);
-    let width = cx.rem_size() * 2.5;
+    let line_height = cx.text_style().line_height_in_pixels(rem_size);
+    let width = rem_size * 1.5;
     let thickness = px(2.);
     let color = cx.theme().colors().text;
 
     canvas(move |bounds, cx| {
-        let start_x = bounds.left() + (bounds.size.width / 2.) - (width / 2.);
-        let end_x = bounds.right();
-        let start_y = bounds.top();
-        let end_y = bounds.top() + baseline_offset - (cap_height / 2.);
+        let start_x = (bounds.left() + bounds.right() - thickness) / 2.;
+        let start_y = (bounds.top() + bounds.bottom() - thickness) / 2.;
+        let right = bounds.right();
+        let top = bounds.top();
 
         cx.paint_quad(
             Bounds::from_corners(
-                point(start_x, start_y),
+                point(start_x, top),
                 point(
                     start_x + thickness,
-                    if is_last { end_y } else { bounds.bottom() },
+                    if is_last { start_y } else { bounds.bottom() },
                 ),
             ),
             Default::default(),
@@ -3151,7 +3011,7 @@ fn render_tree_branch(is_last: bool, cx: &mut WindowContext) -> impl IntoElement
             Hsla::transparent_black(),
         );
         cx.paint_quad(
-            Bounds::from_corners(point(start_x, end_y), point(end_x, end_y + thickness)),
+            Bounds::from_corners(point(start_x, start_y), point(right, start_y + thickness)),
             Default::default(),
             color,
             Default::default(),

crates/diagnostics2/src/diagnostics.rs 🔗

@@ -88,7 +88,7 @@ struct DiagnosticGroupState {
     block_count: usize,
 }
 
-impl EventEmitter<ItemEvent> for ProjectDiagnosticsEditor {}
+impl EventEmitter<EditorEvent> for ProjectDiagnosticsEditor {}
 
 impl Render for ProjectDiagnosticsEditor {
     type Element = Focusable<Div>;
@@ -158,7 +158,7 @@ impl ProjectDiagnosticsEditor {
         });
         let editor_event_subscription =
             cx.subscribe(&editor, |this, _editor, event: &EditorEvent, cx| {
-                Self::emit_item_event_for_editor_event(event, cx);
+                cx.emit(event.clone());
                 if event == &EditorEvent::Focused && this.path_states.is_empty() {
                     cx.focus(&this.focus_handle);
                 }
@@ -183,40 +183,6 @@ impl ProjectDiagnosticsEditor {
         this
     }
 
-    fn emit_item_event_for_editor_event(event: &EditorEvent, cx: &mut ViewContext<Self>) {
-        match event {
-            EditorEvent::Closed => cx.emit(ItemEvent::CloseItem),
-
-            EditorEvent::Saved | EditorEvent::TitleChanged => {
-                cx.emit(ItemEvent::UpdateTab);
-                cx.emit(ItemEvent::UpdateBreadcrumbs);
-            }
-
-            EditorEvent::Reparsed => {
-                cx.emit(ItemEvent::UpdateBreadcrumbs);
-            }
-
-            EditorEvent::SelectionsChanged { local } if *local => {
-                cx.emit(ItemEvent::UpdateBreadcrumbs);
-            }
-
-            EditorEvent::DirtyChanged => {
-                cx.emit(ItemEvent::UpdateTab);
-            }
-
-            EditorEvent::BufferEdited => {
-                cx.emit(ItemEvent::Edit);
-                cx.emit(ItemEvent::UpdateBreadcrumbs);
-            }
-
-            EditorEvent::ExcerptsAdded { .. } | EditorEvent::ExcerptsRemoved { .. } => {
-                cx.emit(ItemEvent::Edit);
-            }
-
-            _ => {}
-        }
-    }
-
     fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext<Workspace>) {
         if let Some(existing) = workspace.item_of_type::<ProjectDiagnosticsEditor>(cx) {
             workspace.activate_item(&existing, cx);
@@ -333,8 +299,7 @@ impl ProjectDiagnosticsEditor {
 
                 this.update(&mut cx, |this, cx| {
                     this.summary = this.project.read(cx).diagnostic_summary(false, cx);
-                    cx.emit(ItemEvent::UpdateTab);
-                    cx.emit(ItemEvent::UpdateBreadcrumbs);
+                    cx.emit(EditorEvent::TitleChanged);
                 })?;
                 anyhow::Ok(())
             }
@@ -649,6 +614,12 @@ impl FocusableView for ProjectDiagnosticsEditor {
 }
 
 impl Item for ProjectDiagnosticsEditor {
+    type Event = EditorEvent;
+
+    fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
+        Editor::to_item_events(event, f)
+    }
+
     fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
         self.editor.update(cx, |editor, cx| editor.deactivated(cx));
     }

crates/editor2/src/editor.rs 🔗

@@ -1675,8 +1675,7 @@ impl Editor {
             if let Some(project) = project.as_ref() {
                 if buffer.read(cx).is_singleton() {
                     project_subscriptions.push(cx.observe(project, |_, _, cx| {
-                        cx.emit(ItemEvent::UpdateTab);
-                        cx.emit(ItemEvent::UpdateBreadcrumbs);
+                        cx.emit(EditorEvent::TitleChanged);
                     }));
                 }
                 project_subscriptions.push(cx.subscribe(project, |editor, _, event, cx| {
@@ -2132,10 +2131,6 @@ impl Editor {
             cx.emit(SearchEvent::ActiveMatchChanged)
         }
 
-        if local {
-            cx.emit(ItemEvent::UpdateBreadcrumbs);
-        }
-
         cx.notify();
     }
 
@@ -8564,8 +8559,6 @@ impl Editor {
                     self.update_visible_copilot_suggestion(cx);
                 }
                 cx.emit(EditorEvent::BufferEdited);
-                cx.emit(ItemEvent::Edit);
-                cx.emit(ItemEvent::UpdateBreadcrumbs);
                 cx.emit(SearchEvent::MatchesInvalidated);
 
                 if *sigleton_buffer_edited {
@@ -8613,20 +8606,14 @@ impl Editor {
                 self.refresh_inlay_hints(InlayHintRefreshReason::ExcerptsRemoved(ids.clone()), cx);
                 cx.emit(EditorEvent::ExcerptsRemoved { ids: ids.clone() })
             }
-            multi_buffer::Event::Reparsed => {
-                cx.emit(ItemEvent::UpdateBreadcrumbs);
-            }
-            multi_buffer::Event::DirtyChanged => {
-                cx.emit(ItemEvent::UpdateTab);
-            }
-            multi_buffer::Event::Saved
-            | multi_buffer::Event::FileHandleChanged
-            | multi_buffer::Event::Reloaded => {
-                cx.emit(ItemEvent::UpdateTab);
-                cx.emit(ItemEvent::UpdateBreadcrumbs);
+            multi_buffer::Event::Reparsed => cx.emit(EditorEvent::Reparsed),
+            multi_buffer::Event::DirtyChanged => cx.emit(EditorEvent::DirtyChanged),
+            multi_buffer::Event::Saved => cx.emit(EditorEvent::Saved),
+            multi_buffer::Event::FileHandleChanged | multi_buffer::Event::Reloaded => {
+                cx.emit(EditorEvent::TitleChanged)
             }
             multi_buffer::Event::DiffBaseChanged => cx.emit(EditorEvent::DiffBaseChanged),
-            multi_buffer::Event::Closed => cx.emit(ItemEvent::CloseItem),
+            multi_buffer::Event::Closed => cx.emit(EditorEvent::Closed),
             multi_buffer::Event::DiagnosticsUpdated => {
                 self.refresh_active_diagnostics(cx);
             }

crates/editor2/src/editor_tests.rs 🔗

@@ -32,7 +32,7 @@ use util::{
     test::{marked_text_ranges, marked_text_ranges_by, sample_text, TextRangeMarker},
 };
 use workspace::{
-    item::{FollowEvent, FollowableEvents, FollowableItem, Item, ItemHandle},
+    item::{FollowEvent, FollowableItem, Item, ItemHandle},
     NavigationEntry, ViewId,
 };
 
@@ -6478,7 +6478,7 @@ async fn test_following(cx: &mut gpui::TestAppContext) {
             cx.subscribe(
                 &follower.root_view(cx).unwrap(),
                 move |_, _, event: &EditorEvent, cx| {
-                    if matches!(event.to_follow_event(), Some(FollowEvent::Unfollow)) {
+                    if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
                         *is_still_following.borrow_mut() = false;
                     }
 

crates/editor2/src/items.rs 🔗

@@ -32,10 +32,10 @@ use std::{
 };
 use text::Selection;
 use theme::{ActiveTheme, Theme};
-use ui::{Color, Label};
+use ui::{h_stack, Color, Label};
 use util::{paths::PathExt, paths::FILE_ROW_COLUMN_DELIMITER, ResultExt, TryFutureExt};
 use workspace::{
-    item::{BreadcrumbText, FollowEvent, FollowableEvents, FollowableItemHandle},
+    item::{BreadcrumbText, FollowEvent, FollowableItemHandle},
     StatusItemView,
 };
 use workspace::{
@@ -46,27 +46,7 @@ use workspace::{
 
 pub const MAX_TAB_TITLE_LEN: usize = 24;
 
-impl FollowableEvents for EditorEvent {
-    fn to_follow_event(&self) -> Option<workspace::item::FollowEvent> {
-        match self {
-            EditorEvent::Edited => Some(FollowEvent::Unfollow),
-            EditorEvent::SelectionsChanged { local }
-            | EditorEvent::ScrollPositionChanged { local, .. } => {
-                if *local {
-                    Some(FollowEvent::Unfollow)
-                } else {
-                    None
-                }
-            }
-            _ => None,
-        }
-    }
-}
-
-impl EventEmitter<ItemEvent> for Editor {}
-
 impl FollowableItem for Editor {
-    type FollowableEvent = EditorEvent;
     fn remote_id(&self) -> Option<ViewId> {
         self.remote_id
     }
@@ -241,9 +221,24 @@ impl FollowableItem for Editor {
         }))
     }
 
+    fn to_follow_event(event: &EditorEvent) -> Option<workspace::item::FollowEvent> {
+        match event {
+            EditorEvent::Edited => Some(FollowEvent::Unfollow),
+            EditorEvent::SelectionsChanged { local }
+            | EditorEvent::ScrollPositionChanged { local, .. } => {
+                if *local {
+                    Some(FollowEvent::Unfollow)
+                } else {
+                    None
+                }
+            }
+            _ => None,
+        }
+    }
+
     fn add_event_to_update_proto(
         &self,
-        event: &Self::FollowableEvent,
+        event: &EditorEvent,
         update: &mut Option<proto::update_view::Variant>,
         cx: &WindowContext,
     ) -> bool {
@@ -528,6 +523,8 @@ fn deserialize_anchor(buffer: &MultiBufferSnapshot, anchor: proto::EditorAnchor)
 }
 
 impl Item for Editor {
+    type Event = EditorEvent;
+
     fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) -> bool {
         if let Ok(data) = data.downcast::<NavigationData>() {
             let newest_selection = self.selections.newest::<Point>(cx);
@@ -586,28 +583,25 @@ impl Item for Editor {
     fn tab_content(&self, detail: Option<usize>, cx: &WindowContext) -> AnyElement {
         let theme = cx.theme();
 
-        AnyElement::new(
-            div()
-                .flex()
-                .flex_row()
-                .items_center()
-                .gap_2()
-                .child(Label::new(self.title(cx).to_string()))
-                .children(detail.and_then(|detail| {
-                    let path = path_for_buffer(&self.buffer, detail, false, cx)?;
-                    let description = path.to_string_lossy();
-
-                    Some(
-                        div().child(
-                            Label::new(util::truncate_and_trailoff(
-                                &description,
-                                MAX_TAB_TITLE_LEN,
-                            ))
-                            .color(Color::Muted),
-                        ),
-                    )
-                })),
-        )
+        let description = detail.and_then(|detail| {
+            let path = path_for_buffer(&self.buffer, detail, false, cx)?;
+            let description = path.to_string_lossy();
+            let description = description.trim();
+
+            if description.is_empty() {
+                return None;
+            }
+
+            Some(util::truncate_and_trailoff(&description, MAX_TAB_TITLE_LEN))
+        });
+
+        h_stack()
+            .gap_2()
+            .child(Label::new(self.title(cx).to_string()))
+            .when_some(description, |this, description| {
+                this.child(Label::new(description).color(Color::Muted))
+            })
+            .into_any_element()
     }
 
     fn for_each_project_item(
@@ -841,6 +835,40 @@ impl Item for Editor {
         Some("Editor")
     }
 
+    fn to_item_events(event: &EditorEvent, mut f: impl FnMut(ItemEvent)) {
+        match event {
+            EditorEvent::Closed => f(ItemEvent::CloseItem),
+
+            EditorEvent::Saved | EditorEvent::TitleChanged => {
+                f(ItemEvent::UpdateTab);
+                f(ItemEvent::UpdateBreadcrumbs);
+            }
+
+            EditorEvent::Reparsed => {
+                f(ItemEvent::UpdateBreadcrumbs);
+            }
+
+            EditorEvent::SelectionsChanged { local } if *local => {
+                f(ItemEvent::UpdateBreadcrumbs);
+            }
+
+            EditorEvent::DirtyChanged => {
+                f(ItemEvent::UpdateTab);
+            }
+
+            EditorEvent::BufferEdited => {
+                f(ItemEvent::Edit);
+                f(ItemEvent::UpdateBreadcrumbs);
+            }
+
+            EditorEvent::ExcerptsAdded { .. } | EditorEvent::ExcerptsRemoved { .. } => {
+                f(ItemEvent::Edit);
+            }
+
+            _ => {}
+        }
+    }
+
     fn deserialize(
         project: Model<Project>,
         _workspace: WeakView<Workspace>,

crates/gpui2/src/elements/canvas.rs 🔗

@@ -1,9 +1,11 @@
-use crate::{Bounds, Element, IntoElement, Pixels, StyleRefinement, Styled, WindowContext};
+use refineable::Refineable as _;
+
+use crate::{Bounds, Element, IntoElement, Pixels, Style, StyleRefinement, Styled, WindowContext};
 
 pub fn canvas(callback: impl 'static + FnOnce(Bounds<Pixels>, &mut WindowContext)) -> Canvas {
     Canvas {
         paint_callback: Box::new(callback),
-        style: Default::default(),
+        style: StyleRefinement::default(),
     }
 }
 
@@ -32,7 +34,9 @@ impl Element for Canvas {
         _: Option<Self::State>,
         cx: &mut WindowContext,
     ) -> (crate::LayoutId, Self::State) {
-        let layout_id = cx.request_layout(&self.style.clone().into(), []);
+        let mut style = Style::default();
+        style.refine(&self.style);
+        let layout_id = cx.request_layout(&style, []);
         (layout_id, ())
     }
 

crates/gpui2/src/platform/mac/display_linker.rs 🔗

@@ -7,6 +7,7 @@ use std::{
 use crate::DisplayId;
 use collections::HashMap;
 use parking_lot::Mutex;
+pub use sys::CVSMPTETime as SmtpeTime;
 pub use sys::CVTimeStamp as VideoTimestamp;
 
 pub(crate) struct MacDisplayLinker {
@@ -153,7 +154,7 @@ mod sys {
         kCVTimeStampTopField | kCVTimeStampBottomField;
 
     #[repr(C)]
-    #[derive(Clone, Copy)]
+    #[derive(Clone, Copy, Default)]
     pub struct CVSMPTETime {
         pub subframes: i16,
         pub subframe_divisor: i16,

crates/gpui2/src/platform/test/platform.rs 🔗

@@ -147,18 +147,25 @@ impl Platform for TestPlatform {
     fn set_display_link_output_callback(
         &self,
         _display_id: DisplayId,
-        _callback: Box<dyn FnMut(&crate::VideoTimestamp, &crate::VideoTimestamp) + Send>,
+        mut callback: Box<dyn FnMut(&crate::VideoTimestamp, &crate::VideoTimestamp) + Send>,
     ) {
-        unimplemented!()
-    }
-
-    fn start_display_link(&self, _display_id: DisplayId) {
-        unimplemented!()
-    }
-
-    fn stop_display_link(&self, _display_id: DisplayId) {
-        unimplemented!()
-    }
+        let timestamp = crate::VideoTimestamp {
+            version: 0,
+            video_time_scale: 0,
+            video_time: 0,
+            host_time: 0,
+            rate_scalar: 0.0,
+            video_refresh_period: 0,
+            smpte_time: crate::SmtpeTime::default(),
+            flags: 0,
+            reserved: 0,
+        };
+        callback(&timestamp, &timestamp)
+    }
+
+    fn start_display_link(&self, _display_id: DisplayId) {}
+
+    fn stop_display_link(&self, _display_id: DisplayId) {}
 
     fn open_url(&self, _url: &str) {
         unimplemented!()

crates/project/src/project.rs 🔗

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

crates/project/src/project_tests.rs 🔗

@@ -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"), "");

crates/project/src/search.rs 🔗

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

crates/project/src/worktree.rs 🔗

@@ -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;

crates/project/src/worktree_tests.rs 🔗

@@ -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(())
+                })
+            }
         }
     }
 }

crates/project2/src/project2.rs 🔗

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

crates/project2/src/project_tests.rs 🔗

@@ -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"), "");

crates/project2/src/search.rs 🔗

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

crates/project2/src/worktree.rs 🔗

@@ -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;

crates/project2/src/worktree_tests.rs 🔗

@@ -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(())
+                })
+            }
         }
     }
 }

crates/project_panel/src/project_panel.rs 🔗

@@ -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);
             }
 

crates/project_panel2/src/project_panel.rs 🔗

@@ -602,7 +602,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)
@@ -616,7 +616,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);
@@ -629,21 +629,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(())
         }))
     }
@@ -923,15 +924,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(())
@@ -1017,7 +1020,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);
     //         }
 

crates/quick_action_bar2/Cargo.toml 🔗

@@ -0,0 +1,22 @@
+[package]
+name = "quick_action_bar2"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/quick_action_bar.rs"
+doctest = false
+
+[dependencies]
+#assistant = { path = "../assistant" }
+editor = { package = "editor2", path = "../editor2" }
+gpui = { package = "gpui2", path = "../gpui2" }
+search = { package = "search2", path = "../search2" }
+workspace = { package = "workspace2", path = "../workspace2" }
+ui = { package = "ui2", path = "../ui2" }
+
+[dev-dependencies]
+editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
+gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
+workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }

crates/quick_action_bar2/src/quick_action_bar.rs 🔗

@@ -0,0 +1,288 @@
+// use assistant::{assistant_panel::InlineAssist, AssistantPanel};
+use editor::Editor;
+
+use gpui::{
+    Action, Div, ElementId, EventEmitter, InteractiveElement, ParentElement, Render, Stateful,
+    Styled, Subscription, View, ViewContext, WeakView,
+};
+use search::BufferSearchBar;
+use ui::{prelude::*, ButtonSize, ButtonStyle, Icon, IconButton, IconSize, Tooltip};
+use workspace::{
+    item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
+};
+
+pub struct QuickActionBar {
+    buffer_search_bar: View<BufferSearchBar>,
+    active_item: Option<Box<dyn ItemHandle>>,
+    _inlay_hints_enabled_subscription: Option<Subscription>,
+    #[allow(unused)]
+    workspace: WeakView<Workspace>,
+}
+
+impl QuickActionBar {
+    pub fn new(buffer_search_bar: View<BufferSearchBar>, workspace: &Workspace) -> Self {
+        Self {
+            buffer_search_bar,
+            active_item: None,
+            _inlay_hints_enabled_subscription: None,
+            workspace: workspace.weak_handle(),
+        }
+    }
+
+    #[allow(dead_code)]
+    fn active_editor(&self) -> Option<View<Editor>> {
+        self.active_item
+            .as_ref()
+            .and_then(|item| item.downcast::<Editor>())
+    }
+}
+
+impl Render for QuickActionBar {
+    type Element = Stateful<Div>;
+
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+        let search_button = QuickActionBarButton::new(
+            "toggle buffer search",
+            Icon::MagnifyingGlass,
+            !self.buffer_search_bar.read(cx).is_dismissed(),
+            Box::new(search::buffer_search::Deploy { focus: false }),
+            "Buffer Search",
+        );
+        let assistant_button = QuickActionBarButton::new(
+            "toggle inline assitant",
+            Icon::MagicWand,
+            false,
+            Box::new(gpui::NoAction),
+            "Inline assistant",
+        );
+        h_stack()
+            .id("quick action bar")
+            .p_1()
+            .gap_2()
+            .child(search_button)
+            .child(
+                div()
+                    .border()
+                    .border_color(gpui::red())
+                    .child(assistant_button),
+            )
+    }
+}
+
+impl EventEmitter<ToolbarItemEvent> for QuickActionBar {}
+
+// impl View for QuickActionBar {
+//     fn ui_name() -> &'static str {
+//         "QuickActionsBar"
+//     }
+
+//     fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement<Self> {
+//         let Some(editor) = self.active_editor() else {
+//             return div();
+//         };
+
+//         let mut bar = Flex::row();
+//         if editor.read(cx).supports_inlay_hints(cx) {
+//             bar = bar.with_child(render_quick_action_bar_button(
+//                 0,
+//                 "icons/inlay_hint.svg",
+//                 editor.read(cx).inlay_hints_enabled(),
+//                 (
+//                     "Toggle Inlay Hints".to_string(),
+//                     Some(Box::new(editor::ToggleInlayHints)),
+//                 ),
+//                 cx,
+//                 |this, cx| {
+//                     if let Some(editor) = this.active_editor() {
+//                         editor.update(cx, |editor, cx| {
+//                             editor.toggle_inlay_hints(&editor::ToggleInlayHints, cx);
+//                         });
+//                     }
+//                 },
+//             ));
+//         }
+
+//         if editor.read(cx).buffer().read(cx).is_singleton() {
+//             let search_bar_shown = !self.buffer_search_bar.read(cx).is_dismissed();
+//             let search_action = buffer_search::Deploy { focus: true };
+
+//             bar = bar.with_child(render_quick_action_bar_button(
+//                 1,
+//                 "icons/magnifying_glass.svg",
+//                 search_bar_shown,
+//                 (
+//                     "Buffer Search".to_string(),
+//                     Some(Box::new(search_action.clone())),
+//                 ),
+//                 cx,
+//                 move |this, cx| {
+//                     this.buffer_search_bar.update(cx, |buffer_search_bar, cx| {
+//                         if search_bar_shown {
+//                             buffer_search_bar.dismiss(&buffer_search::Dismiss, cx);
+//                         } else {
+//                             buffer_search_bar.deploy(&search_action, cx);
+//                         }
+//                     });
+//                 },
+//             ));
+//         }
+
+//         bar.add_child(render_quick_action_bar_button(
+//             2,
+//             "icons/magic-wand.svg",
+//             false,
+//             ("Inline Assist".into(), Some(Box::new(InlineAssist))),
+//             cx,
+//             move |this, cx| {
+//                 if let Some(workspace) = this.workspace.upgrade(cx) {
+//                     workspace.update(cx, |workspace, cx| {
+//                         AssistantPanel::inline_assist(workspace, &Default::default(), cx);
+//                     });
+//                 }
+//             },
+//         ));
+
+//         bar.into_any()
+//     }
+// }
+
+#[derive(IntoElement)]
+struct QuickActionBarButton {
+    id: ElementId,
+    icon: Icon,
+    toggled: bool,
+    action: Box<dyn Action>,
+    tooltip: SharedString,
+    tooltip_meta: Option<SharedString>,
+}
+
+impl QuickActionBarButton {
+    fn new(
+        id: impl Into<ElementId>,
+        icon: Icon,
+        toggled: bool,
+        action: Box<dyn Action>,
+        tooltip: impl Into<SharedString>,
+    ) -> Self {
+        Self {
+            id: id.into(),
+            icon,
+            toggled,
+            action,
+            tooltip: tooltip.into(),
+            tooltip_meta: None,
+        }
+    }
+
+    #[allow(dead_code)]
+    pub fn meta(mut self, meta: Option<impl Into<SharedString>>) -> Self {
+        self.tooltip_meta = meta.map(|meta| meta.into());
+        self
+    }
+}
+
+impl RenderOnce for QuickActionBarButton {
+    type Rendered = IconButton;
+
+    fn render(self, _: &mut WindowContext) -> Self::Rendered {
+        let tooltip = self.tooltip.clone();
+        let action = self.action.boxed_clone();
+        let tooltip_meta = self.tooltip_meta.clone();
+
+        IconButton::new(self.id.clone(), self.icon)
+            .size(ButtonSize::Compact)
+            .icon_size(IconSize::Small)
+            .style(ButtonStyle::Subtle)
+            .selected(self.toggled)
+            .tooltip(move |cx| {
+                if let Some(meta) = &tooltip_meta {
+                    Tooltip::with_meta(tooltip.clone(), Some(&*action), meta.clone(), cx)
+                } else {
+                    Tooltip::for_action(tooltip.clone(), &*action, cx)
+                }
+            })
+            .on_click({
+                let action = self.action.boxed_clone();
+                move |_, cx| cx.dispatch_action(action.boxed_clone())
+            })
+    }
+}
+
+// fn render_quick_action_bar_button<
+//     F: 'static + Fn(&mut QuickActionBar, &mut ViewContext<QuickActionBar>),
+// >(
+//     index: usize,
+//     icon: &'static str,
+//     toggled: bool,
+//     tooltip: (String, Option<Box<dyn Action>>),
+//     cx: &mut ViewContext<QuickActionBar>,
+//     on_click: F,
+// ) -> AnyElement<QuickActionBar> {
+//     enum QuickActionBarButton {}
+
+//     let theme = theme::current(cx);
+//     let (tooltip_text, action) = tooltip;
+
+//     MouseEventHandler::new::<QuickActionBarButton, _>(index, cx, |mouse_state, _| {
+//         let style = theme
+//             .workspace
+//             .toolbar
+//             .toggleable_tool
+//             .in_state(toggled)
+//             .style_for(mouse_state);
+//         Svg::new(icon)
+//             .with_color(style.color)
+//             .constrained()
+//             .with_width(style.icon_width)
+//             .aligned()
+//             .constrained()
+//             .with_width(style.button_width)
+//             .with_height(style.button_width)
+//             .contained()
+//             .with_style(style.container)
+//     })
+//     .with_cursor_style(CursorStyle::PointingHand)
+//     .on_click(MouseButton::Left, move |_, pane, cx| on_click(pane, cx))
+//     .with_tooltip::<QuickActionBarButton>(index, tooltip_text, action, theme.tooltip.clone(), cx)
+//     .into_any_named("quick action bar button")
+// }
+
+impl ToolbarItemView for QuickActionBar {
+    fn set_active_pane_item(
+        &mut self,
+        active_pane_item: Option<&dyn ItemHandle>,
+        cx: &mut ViewContext<Self>,
+    ) -> ToolbarItemLocation {
+        match active_pane_item {
+            Some(active_item) => {
+                self.active_item = Some(active_item.boxed_clone());
+                self._inlay_hints_enabled_subscription.take();
+
+                if let Some(editor) = active_item.downcast::<Editor>() {
+                    let mut inlay_hints_enabled = editor.read(cx).inlay_hints_enabled();
+                    let mut supports_inlay_hints = editor.read(cx).supports_inlay_hints(cx);
+                    self._inlay_hints_enabled_subscription =
+                        Some(cx.observe(&editor, move |_, editor, cx| {
+                            let editor = editor.read(cx);
+                            let new_inlay_hints_enabled = editor.inlay_hints_enabled();
+                            let new_supports_inlay_hints = editor.supports_inlay_hints(cx);
+                            let should_notify = inlay_hints_enabled != new_inlay_hints_enabled
+                                || supports_inlay_hints != new_supports_inlay_hints;
+                            inlay_hints_enabled = new_inlay_hints_enabled;
+                            supports_inlay_hints = new_supports_inlay_hints;
+                            if should_notify {
+                                cx.notify()
+                            }
+                        }));
+                    ToolbarItemLocation::PrimaryRight
+                } else {
+                    ToolbarItemLocation::Hidden
+                }
+            }
+            None => {
+                self.active_item = None;
+                ToolbarItemLocation::Hidden
+            }
+        }
+    }
+}

crates/rpc/proto/zed.proto 🔗

@@ -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;

crates/rpc/src/rpc.rs 🔗

@@ -9,4 +9,4 @@ pub use notification::*;
 pub use peer::*;
 mod macros;
 
-pub const PROTOCOL_VERSION: u32 = 66;
+pub const PROTOCOL_VERSION: u32 = 67;

crates/rpc2/proto/zed.proto 🔗

@@ -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;

crates/rpc2/src/rpc.rs 🔗

@@ -9,4 +9,4 @@ pub use notification::*;
 pub use peer::*;
 mod macros;
 
-pub const PROTOCOL_VERSION: u32 = 66;
+pub const PROTOCOL_VERSION: u32 = 67;

crates/terminal_view2/src/terminal_view.rs 🔗

@@ -736,6 +736,8 @@ impl InputHandler for TerminalView {
 }
 
 impl Item for TerminalView {
+    type Event = ItemEvent;
+
     fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString> {
         Some(self.terminal().read(cx).title().into())
     }
@@ -843,6 +845,10 @@ impl Item for TerminalView {
         //     .detach();
         self.workspace_id = workspace.database_id();
     }
+
+    fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) {
+        f(*event)
+    }
 }
 
 impl SearchableItem for TerminalView {
@@ -1170,6 +1176,7 @@ mod tests {
                 })
             })
             .await
+            .unwrap()
             .unwrap();
 
         (wt, entry)

crates/theme2/src/default_colors.rs 🔗

@@ -5,7 +5,7 @@ use crate::ColorScale;
 use crate::{SystemColors, ThemeColors};
 
 pub(crate) fn neutral() -> ColorScaleSet {
-    slate()
+    sand()
 }
 
 impl ThemeColors {
@@ -29,12 +29,12 @@ impl ThemeColors {
             element_disabled: neutral().light_alpha().step_3(),
             drop_target_background: blue().light_alpha().step_2(),
             ghost_element_background: system.transparent,
-            ghost_element_hover: neutral().light_alpha().step_4(),
-            ghost_element_active: neutral().light_alpha().step_5(),
+            ghost_element_hover: neutral().light_alpha().step_3(),
+            ghost_element_active: neutral().light_alpha().step_4(),
             ghost_element_selected: neutral().light_alpha().step_5(),
             ghost_element_disabled: neutral().light_alpha().step_3(),
-            text: yellow().light().step_9(),
-            text_muted: neutral().light().step_11(),
+            text: neutral().light().step_12(),
+            text_muted: neutral().light().step_10(),
             text_placeholder: neutral().light().step_10(),
             text_disabled: neutral().light().step_9(),
             text_accent: blue().light().step_11(),
@@ -53,13 +53,13 @@ impl ThemeColors {
             editor_gutter_background: neutral().light().step_1(), // todo!("pick the right colors")
             editor_subheader_background: neutral().light().step_2(),
             editor_active_line_background: neutral().light_alpha().step_3(),
-            editor_line_number: neutral().light_alpha().step_3(), // todo!("pick the right colors")
-            editor_active_line_number: neutral().light_alpha().step_3(), // todo!("pick the right colors")
-            editor_highlighted_line_background: neutral().light_alpha().step_4(), // todo!("pick the right colors")
-            editor_invisible: neutral().light_alpha().step_4(), // todo!("pick the right colors")
-            editor_wrap_guide: neutral().light_alpha().step_4(), // todo!("pick the right colors")
-            editor_active_wrap_guide: neutral().light_alpha().step_4(), // todo!("pick the right colors")
-            editor_document_highlight_read_background: neutral().light_alpha().step_4(), // todo!("pick the right colors")
+            editor_line_number: neutral().light().step_10(),
+            editor_active_line_number: neutral().light().step_11(),
+            editor_highlighted_line_background: neutral().light_alpha().step_3(),
+            editor_invisible: neutral().light().step_10(),
+            editor_wrap_guide: neutral().light_alpha().step_7(),
+            editor_active_wrap_guide: neutral().light_alpha().step_8(), // todo!("pick the right colors")
+            editor_document_highlight_read_background: neutral().light_alpha().step_3(), // todo!("pick the right colors")
             editor_document_highlight_write_background: neutral().light_alpha().step_4(), // todo!("pick the right colors")
             terminal_background: neutral().light().step_1(),
             terminal_ansi_black: black().light().step_12(),

crates/theme2/src/default_theme.rs 🔗

@@ -1,47 +1,51 @@
+use std::sync::Arc;
+
 use crate::{
+    default_color_scales,
     one_themes::{one_dark, one_family},
-    Theme, ThemeFamily,
+    Appearance, PlayerColors, StatusColors, SyntaxTheme, SystemColors, Theme, ThemeColors,
+    ThemeFamily, ThemeStyles,
 };
 
-// fn zed_pro_daylight() -> Theme {
-//     Theme {
-//         id: "zed_pro_daylight".to_string(),
-//         name: "Zed Pro Daylight".into(),
-//         appearance: Appearance::Light,
-//         styles: ThemeStyles {
-//             system: SystemColors::default(),
-//             colors: ThemeColors::light(),
-//             status: StatusColors::light(),
-//             player: PlayerColors::light(),
-//             syntax: Arc::new(SyntaxTheme::light()),
-//         },
-//     }
-// }
+fn zed_pro_daylight() -> Theme {
+    Theme {
+        id: "zed_pro_daylight".to_string(),
+        name: "Zed Pro Daylight".into(),
+        appearance: Appearance::Light,
+        styles: ThemeStyles {
+            system: SystemColors::default(),
+            colors: ThemeColors::light(),
+            status: StatusColors::light(),
+            player: PlayerColors::light(),
+            syntax: Arc::new(SyntaxTheme::light()),
+        },
+    }
+}
 
-// pub(crate) fn zed_pro_moonlight() -> Theme {
-//     Theme {
-//         id: "zed_pro_moonlight".to_string(),
-//         name: "Zed Pro Moonlight".into(),
-//         appearance: Appearance::Dark,
-//         styles: ThemeStyles {
-//             system: SystemColors::default(),
-//             colors: ThemeColors::dark(),
-//             status: StatusColors::dark(),
-//             player: PlayerColors::dark(),
-//             syntax: Arc::new(SyntaxTheme::dark()),
-//         },
-//     }
-// }
+pub(crate) fn zed_pro_moonlight() -> Theme {
+    Theme {
+        id: "zed_pro_moonlight".to_string(),
+        name: "Zed Pro Moonlight".into(),
+        appearance: Appearance::Dark,
+        styles: ThemeStyles {
+            system: SystemColors::default(),
+            colors: ThemeColors::dark(),
+            status: StatusColors::dark(),
+            player: PlayerColors::dark(),
+            syntax: Arc::new(SyntaxTheme::dark()),
+        },
+    }
+}
 
-// pub fn zed_pro_family() -> ThemeFamily {
-//     ThemeFamily {
-//         id: "zed_pro".to_string(),
-//         name: "Zed Pro".into(),
-//         author: "Zed Team".into(),
-//         themes: vec![zed_pro_daylight(), zed_pro_moonlight()],
-//         scales: default_color_scales(),
-//     }
-// }
+pub fn zed_pro_family() -> ThemeFamily {
+    ThemeFamily {
+        id: "zed_pro".to_string(),
+        name: "Zed Pro".into(),
+        author: "Zed Team".into(),
+        themes: vec![zed_pro_daylight(), zed_pro_moonlight()],
+        scales: default_color_scales(),
+    }
+}
 
 impl Default for ThemeFamily {
     fn default() -> Self {

crates/theme2/src/registry.rs 🔗

@@ -6,8 +6,8 @@ use gpui::{HighlightStyle, SharedString};
 use refineable::Refineable;
 
 use crate::{
-    one_themes::one_family, Appearance, PlayerColors, StatusColors, SyntaxTheme, SystemColors,
-    Theme, ThemeColors, ThemeFamily, ThemeStyles, UserTheme, UserThemeFamily,
+    one_themes::one_family, zed_pro_family, Appearance, PlayerColors, StatusColors, SyntaxTheme,
+    SystemColors, Theme, ThemeColors, ThemeFamily, ThemeStyles, UserTheme, UserThemeFamily,
 };
 
 pub struct ThemeRegistry {
@@ -117,7 +117,7 @@ impl Default for ThemeRegistry {
             themes: HashMap::default(),
         };
 
-        this.insert_theme_families([one_family()]);
+        this.insert_theme_families([zed_pro_family(), one_family()]);
 
         this
     }

crates/theme2/src/styles/syntax.rs 🔗

@@ -22,8 +22,8 @@ impl SyntaxTheme {
             highlights: vec![
                 ("attribute".into(), cyan().light().step_11().into()),
                 ("boolean".into(), tomato().light().step_11().into()),
-                ("comment".into(), neutral().light().step_11().into()),
-                ("comment.doc".into(), iris().light().step_12().into()),
+                ("comment".into(), neutral().light().step_10().into()),
+                ("comment.doc".into(), iris().light().step_11().into()),
                 ("constant".into(), red().light().step_9().into()),
                 ("constructor".into(), red().light().step_9().into()),
                 ("embedded".into(), red().light().step_9().into()),
@@ -32,11 +32,11 @@ impl SyntaxTheme {
                 ("enum".into(), red().light().step_9().into()),
                 ("function".into(), red().light().step_9().into()),
                 ("hint".into(), red().light().step_9().into()),
-                ("keyword".into(), orange().light().step_11().into()),
+                ("keyword".into(), orange().light().step_9().into()),
                 ("label".into(), red().light().step_9().into()),
                 ("link_text".into(), red().light().step_9().into()),
                 ("link_uri".into(), red().light().step_9().into()),
-                ("number".into(), red().light().step_9().into()),
+                ("number".into(), purple().light().step_10().into()),
                 ("operator".into(), red().light().step_9().into()),
                 ("predictive".into(), red().light().step_9().into()),
                 ("preproc".into(), red().light().step_9().into()),
@@ -49,16 +49,16 @@ impl SyntaxTheme {
                 ),
                 (
                     "punctuation.delimiter".into(),
-                    neutral().light().step_11().into(),
+                    neutral().light().step_10().into(),
                 ),
                 (
                     "punctuation.list_marker".into(),
                     blue().light().step_11().into(),
                 ),
                 ("punctuation.special".into(), red().light().step_9().into()),
-                ("string".into(), jade().light().step_11().into()),
+                ("string".into(), jade().light().step_9().into()),
                 ("string.escape".into(), red().light().step_9().into()),
-                ("string.regex".into(), tomato().light().step_11().into()),
+                ("string.regex".into(), tomato().light().step_9().into()),
                 ("string.special".into(), red().light().step_9().into()),
                 (
                     "string.special.symbol".into(),
@@ -67,7 +67,7 @@ impl SyntaxTheme {
                 ("tag".into(), red().light().step_9().into()),
                 ("text.literal".into(), red().light().step_9().into()),
                 ("title".into(), red().light().step_9().into()),
-                ("type".into(), red().light().step_9().into()),
+                ("type".into(), cyan().light().step_9().into()),
                 ("variable".into(), red().light().step_9().into()),
                 ("variable.special".into(), red().light().step_9().into()),
                 ("variant".into(), red().light().step_9().into()),

crates/theme_selector2/src/theme_selector.rs 🔗

@@ -2,14 +2,14 @@ use feature_flags::FeatureFlagAppExt;
 use fs::Fs;
 use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
 use gpui::{
-    actions, AppContext, DismissEvent, EventEmitter, FocusableView, Render, SharedString, View,
-    ViewContext, VisualContext, WeakView,
+    actions, AppContext, DismissEvent, Div, EventEmitter, FocusableView, Render, SharedString,
+    View, ViewContext, VisualContext, WeakView,
 };
 use picker::{Picker, PickerDelegate};
 use settings::{update_settings_file, SettingsStore};
 use std::sync::Arc;
 use theme::{Theme, ThemeRegistry, ThemeSettings};
-use ui::{prelude::*, ListItem};
+use ui::{prelude::*, v_stack, ListItem};
 use util::ResultExt;
 use workspace::{ui::HighlightedLabel, Workspace};
 
@@ -65,10 +65,10 @@ impl FocusableView for ThemeSelector {
 }
 
 impl Render for ThemeSelector {
-    type Element = View<Picker<ThemeSelectorDelegate>>;
+    type Element = Div;
 
     fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
-        self.picker.clone()
+        v_stack().min_w_96().child(self.picker.clone())
     }
 }
 
@@ -98,7 +98,7 @@ impl ThemeSelectorDelegate {
         let original_theme = cx.theme().clone();
 
         let staff_mode = cx.is_staff();
-        let registry = cx.global::<Arc<ThemeRegistry>>();
+        let registry = cx.global::<ThemeRegistry>();
         let theme_names = registry.list(staff_mode).collect::<Vec<_>>();
         //todo!(theme sorting)
         // theme_names.sort_unstable_by(|a, b| a.is_light.cmp(&b.is_light).then(a.name.cmp(&b.name)));
@@ -126,7 +126,7 @@ impl ThemeSelectorDelegate {
 
     fn show_selected_theme(&mut self, cx: &mut ViewContext<Picker<ThemeSelectorDelegate>>) {
         if let Some(mat) = self.matches.get(self.selected_index) {
-            let registry = cx.global::<Arc<ThemeRegistry>>();
+            let registry = cx.global::<ThemeRegistry>();
             match registry.get(&mat.string) {
                 Ok(theme) => {
                     Self::set_theme(theme, cx);

crates/ui2/src/components.rs 🔗

@@ -5,6 +5,7 @@ mod context_menu;
 mod disclosure;
 mod divider;
 mod icon;
+mod indicator;
 mod keybinding;
 mod label;
 mod list;
@@ -24,6 +25,7 @@ pub use context_menu::*;
 pub use disclosure::*;
 pub use divider::*;
 pub use icon::*;
+pub use indicator::*;
 pub use keybinding::*;
 pub use label::*;
 pub use list::*;

crates/ui2/src/components/icon.rs 🔗

@@ -1,15 +1,26 @@
-use gpui::{rems, svg, IntoElement, Svg};
+use gpui::{rems, svg, IntoElement, Rems, Svg};
 use strum::EnumIter;
 
 use crate::prelude::*;
 
 #[derive(Default, PartialEq, Copy, Clone)]
 pub enum IconSize {
+    XSmall,
     Small,
     #[default]
     Medium,
 }
 
+impl IconSize {
+    pub fn rems(self) -> Rems {
+        match self {
+            IconSize::XSmall => rems(12. / 16.),
+            IconSize::Small => rems(14. / 16.),
+            IconSize::Medium => rems(16. / 16.),
+        }
+    }
+}
+
 #[derive(Debug, PartialEq, Copy, Clone, EnumIter)]
 pub enum Icon {
     Ai,
@@ -172,13 +183,8 @@ impl RenderOnce for IconElement {
     type Rendered = Svg;
 
     fn render(self, cx: &mut WindowContext) -> Self::Rendered {
-        let svg_size = match self.size {
-            IconSize::Small => rems(14. / 16.),
-            IconSize::Medium => rems(16. / 16.),
-        };
-
         svg()
-            .size(svg_size)
+            .size(self.size.rems())
             .flex_none()
             .path(self.path)
             .text_color(self.color.color(cx))

crates/ui2/src/components/indicator.rs 🔗

@@ -0,0 +1,60 @@
+use gpui::{Div, Position};
+
+use crate::prelude::*;
+
+#[derive(Default)]
+pub enum IndicatorStyle {
+    #[default]
+    Dot,
+    Bar,
+}
+
+#[derive(IntoElement)]
+pub struct Indicator {
+    position: Position,
+    style: IndicatorStyle,
+    color: Color,
+}
+
+impl Indicator {
+    pub fn dot() -> Self {
+        Self {
+            position: Position::Relative,
+            style: IndicatorStyle::Dot,
+            color: Color::Default,
+        }
+    }
+
+    pub fn bar() -> Self {
+        Self {
+            position: Position::Relative,
+            style: IndicatorStyle::Dot,
+            color: Color::Default,
+        }
+    }
+
+    pub fn color(mut self, color: Color) -> Self {
+        self.color = color;
+        self
+    }
+
+    pub fn absolute(mut self) -> Self {
+        self.position = Position::Absolute;
+        self
+    }
+}
+
+impl RenderOnce for Indicator {
+    type Rendered = Div;
+
+    fn render(self, cx: &mut WindowContext) -> Self::Rendered {
+        div()
+            .flex_none()
+            .map(|this| match self.style {
+                IndicatorStyle::Dot => this.w_1p5().h_1p5().rounded_full(),
+                IndicatorStyle::Bar => this.w_full().h_1p5().rounded_t_md(),
+            })
+            .when(self.position == Position::Absolute, |this| this.absolute())
+            .bg(self.color.color(cx))
+    }
+}

crates/ui2/src/prelude.rs 🔗

@@ -8,5 +8,6 @@ pub use crate::clickable::*;
 pub use crate::disableable::*;
 pub use crate::fixed::*;
 pub use crate::selectable::*;
+pub use crate::{h_stack, v_stack};
 pub use crate::{ButtonCommon, Color, StyledExt};
 pub use theme::ActiveTheme;

crates/util/src/paths.rs 🔗

@@ -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:?}"
+        );
+    }
 }

crates/welcome2/src/welcome.rs 🔗

@@ -259,6 +259,8 @@ impl FocusableView for WelcomePage {
 }
 
 impl Item for WelcomePage {
+    type Event = ItemEvent;
+
     fn tab_content(&self, _: Option<usize>, _: &WindowContext) -> AnyElement {
         "Welcome to Zed!".into_any()
     }
@@ -278,4 +280,8 @@ impl Item for WelcomePage {
             _settings_subscription: cx.observe_global::<SettingsStore>(move |_, cx| cx.notify()),
         }))
     }
+
+    fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {
+        f(*event)
+    }
 }

crates/workspace/src/pane.rs 🔗

@@ -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;
+                }
             }
         }
 

crates/workspace/src/workspace.rs 🔗

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

crates/workspace2/src/item.rs 🔗

@@ -78,7 +78,7 @@ impl Settings for ItemSettings {
     }
 }
 
-#[derive(Eq, PartialEq, Hash, Debug)]
+#[derive(Clone, Copy, Eq, PartialEq, Hash, Debug)]
 pub enum ItemEvent {
     CloseItem,
     UpdateTab,
@@ -92,7 +92,9 @@ pub struct BreadcrumbText {
     pub highlights: Option<Vec<(Range<usize>, HighlightStyle)>>,
 }
 
-pub trait Item: FocusableView + EventEmitter<ItemEvent> {
+pub trait Item: FocusableView + EventEmitter<Self::Event> {
+    type Event;
+
     fn deactivated(&mut self, _: &mut ViewContext<Self>) {}
     fn workspace_deactivated(&mut self, _: &mut ViewContext<Self>) {}
     fn navigate(&mut self, _: Box<dyn Any>, _: &mut ViewContext<Self>) -> bool {
@@ -155,6 +157,8 @@ pub trait Item: FocusableView + EventEmitter<ItemEvent> {
         unimplemented!("reload() must be implemented if can_save() returns true")
     }
 
+    fn to_item_events(event: &Self::Event, f: impl FnMut(ItemEvent));
+
     fn act_as_type<'a>(
         &'a self,
         type_id: TypeId,
@@ -206,12 +210,12 @@ pub trait Item: FocusableView + EventEmitter<ItemEvent> {
 }
 
 pub trait ItemHandle: 'static + Send {
-    fn focus_handle(&self, cx: &WindowContext) -> FocusHandle;
     fn subscribe_to_item_events(
         &self,
         cx: &mut WindowContext,
-        handler: Box<dyn Fn(&ItemEvent, &mut WindowContext) + Send>,
+        handler: Box<dyn Fn(ItemEvent, &mut WindowContext)>,
     ) -> gpui::Subscription;
+    fn focus_handle(&self, cx: &WindowContext) -> FocusHandle;
     fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString>;
     fn tab_description(&self, detail: usize, cx: &AppContext) -> Option<SharedString>;
     fn tab_content(&self, detail: Option<usize>, cx: &WindowContext) -> AnyElement;
@@ -285,20 +289,20 @@ impl dyn ItemHandle {
 }
 
 impl<T: Item> ItemHandle for View<T> {
-    fn focus_handle(&self, cx: &WindowContext) -> FocusHandle {
-        self.focus_handle(cx)
-    }
-
     fn subscribe_to_item_events(
         &self,
         cx: &mut WindowContext,
-        handler: Box<dyn Fn(&ItemEvent, &mut WindowContext) + Send>,
+        handler: Box<dyn Fn(ItemEvent, &mut WindowContext)>,
     ) -> gpui::Subscription {
         cx.subscribe(self, move |_, event, cx| {
-            handler(event, cx);
+            T::to_item_events(event, |item_event| handler(item_event, cx));
         })
     }
 
+    fn focus_handle(&self, cx: &WindowContext) -> FocusHandle {
+        self.focus_handle(cx)
+    }
+
     fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString> {
         self.read(cx).tab_tooltip_text(cx)
     }
@@ -461,7 +465,7 @@ impl<T: Item> ItemHandle for View<T> {
                         }
                     }
 
-                    match event {
+                    T::to_item_events(event, |event| match event {
                         ItemEvent::CloseItem => {
                             pane.update(cx, |pane, cx| {
                                 pane.close_item_by_id(item.item_id(), crate::SaveIntent::Close, cx)
@@ -489,7 +493,7 @@ impl<T: Item> ItemHandle for View<T> {
                         }
 
                         _ => {}
-                    }
+                    });
                 }));
 
             cx.on_blur(&self.focus_handle(cx), move |workspace, cx| {
@@ -655,12 +659,7 @@ pub enum FollowEvent {
     Unfollow,
 }
 
-pub trait FollowableEvents {
-    fn to_follow_event(&self) -> Option<FollowEvent>;
-}
-
 pub trait FollowableItem: Item {
-    type FollowableEvent: FollowableEvents;
     fn remote_id(&self) -> Option<ViewId>;
     fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant>;
     fn from_state_proto(
@@ -670,9 +669,10 @@ pub trait FollowableItem: Item {
         state: &mut Option<proto::view::Variant>,
         cx: &mut WindowContext,
     ) -> Option<Task<Result<View<Self>>>>;
+    fn to_follow_event(event: &Self::Event) -> Option<FollowEvent>;
     fn add_event_to_update_proto(
         &self,
-        event: &Self::FollowableEvent,
+        event: &Self::Event,
         update: &mut Option<proto::update_view::Variant>,
         cx: &WindowContext,
     ) -> bool;
@@ -683,7 +683,6 @@ pub trait FollowableItem: Item {
         cx: &mut ViewContext<Self>,
     ) -> Task<Result<()>>;
     fn is_project_item(&self, cx: &WindowContext) -> bool;
-
     fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>);
 }
 
@@ -739,10 +738,7 @@ impl<T: FollowableItem> FollowableItemHandle for View<T> {
     }
 
     fn to_follow_event(&self, event: &dyn Any) -> Option<FollowEvent> {
-        event
-            .downcast_ref()
-            .map(T::FollowableEvent::to_follow_event)
-            .flatten()
+        T::to_follow_event(event.downcast_ref()?)
     }
 
     fn apply_update_proto(
@@ -929,6 +925,12 @@ pub mod test {
     }
 
     impl Item for TestItem {
+        type Event = ItemEvent;
+
+        fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) {
+            f(*event)
+        }
+
         fn tab_description(&self, detail: usize, _: &AppContext) -> Option<SharedString> {
             self.tab_descriptions.as_ref().and_then(|descriptions| {
                 let description = *descriptions.get(detail).or_else(|| descriptions.last())?;

crates/workspace2/src/pane.rs 🔗

@@ -1,5 +1,5 @@
 use crate::{
-    item::{Item, ItemHandle, ItemSettings, WeakItemHandle},
+    item::{ClosePosition, Item, ItemHandle, ItemSettings, WeakItemHandle},
     toolbar::Toolbar,
     workspace_settings::{AutosaveSetting, WorkspaceSettings},
     NewCenterTerminal, NewFile, NewSearch, SplitDirection, ToggleZoom, Workspace,
@@ -27,7 +27,8 @@ use std::{
 };
 
 use ui::{
-    h_stack, prelude::*, right_click_menu, Color, Icon, IconButton, IconElement, Label, Tooltip,
+    h_stack, prelude::*, right_click_menu, ButtonSize, Color, Icon, IconButton, IconSize,
+    Indicator, Label, Tooltip,
 };
 use ui::{v_stack, ContextMenu};
 use util::truncate_and_remove_front;
@@ -537,18 +538,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;
+                }
             }
         }
 
@@ -1415,22 +1419,7 @@ impl Pane {
         cx: &mut ViewContext<'_, Pane>,
     ) -> impl IntoElement {
         let label = item.tab_content(Some(detail), cx);
-        let close_icon = || {
-            let id = item.item_id();
-
-            div()
-                .id(ix)
-                .invisible()
-                .group_hover("", |style| style.visible())
-                .child(
-                    IconButton::new("close_tab", Icon::Close).on_click(cx.listener(
-                        move |pane, _, cx| {
-                            pane.close_item_by_id(id, SaveIntent::Close, cx)
-                                .detach_and_log_err(cx);
-                        },
-                    )),
-                )
-        };
+        let close_side = &ItemSettings::get_global(cx).close_position;
 
         let (text_color, tab_bg, tab_hover_bg, tab_active_bg) = match ix == self.active_item_index {
             false => (
@@ -1447,102 +1436,129 @@ impl Pane {
             ),
         };
 
-        let close_right = ItemSettings::get_global(cx).close_position.right();
         let is_active = ix == self.active_item_index;
 
+        let indicator = {
+            let indicator_color = match (item.has_conflict(cx), item.is_dirty(cx)) {
+                (true, _) => Some(Color::Warning),
+                (_, true) => Some(Color::Accent),
+                (false, false) => None,
+            };
+
+            h_stack()
+                .w_3()
+                .h_3()
+                .justify_center()
+                .absolute()
+                .map(|this| match close_side {
+                    ClosePosition::Left => this.right_1(),
+                    ClosePosition::Right => this.left_1(),
+                })
+                .when_some(indicator_color, |this, indicator_color| {
+                    this.child(Indicator::dot().color(indicator_color))
+                })
+        };
+
+        let close_button = {
+            let id = item.item_id();
+
+            h_stack()
+                .invisible()
+                .w_3()
+                .h_3()
+                .justify_center()
+                .absolute()
+                .map(|this| match close_side {
+                    ClosePosition::Left => this.left_1(),
+                    ClosePosition::Right => this.right_1(),
+                })
+                .group_hover("", |style| style.visible())
+                .child(
+                    // TODO: Fix button size
+                    IconButton::new("close tab", Icon::Close)
+                        .icon_color(Color::Muted)
+                        .size(ButtonSize::None)
+                        .icon_size(IconSize::XSmall)
+                        .on_click(cx.listener(move |pane, _, cx| {
+                            pane.close_item_by_id(id, SaveIntent::Close, cx)
+                                .detach_and_log_err(cx);
+                        })),
+                )
+        };
+
         let tab = div()
-            .group("")
-            .id(ix)
-            .cursor_pointer()
-            .when_some(item.tab_tooltip_text(cx), |div, text| {
-                div.tooltip(move |cx| cx.build_view(|cx| Tooltip::new(text.clone())).into())
-            })
-            .on_click(cx.listener(move |v: &mut Self, e, cx| v.activate_item(ix, true, true, cx)))
-            // .on_drag(move |pane, cx| pane.render_tab(ix, item.boxed_clone(), detail, cx))
-            // .drag_over::<DraggedTab>(|d| d.bg(cx.theme().colors().element_drop_target))
-            // .on_drop(|_view, state: View<DraggedTab>, cx| {
-            //     eprintln!("{:?}", state.read(cx));
-            // })
-            .flex()
-            .items_center()
-            .justify_center()
-            // todo!("Nate - I need to do some work to balance all the items in the tab once things stablize")
-            .map(|this| {
-                if close_right {
-                    this.pl_3().pr_1()
-                } else {
-                    this.pr_1().pr_3()
-                }
-            })
-            .py_1()
-            .bg(tab_bg)
             .border_color(cx.theme().colors().border)
-            .text_color(if is_active {
-                cx.theme().colors().text
-            } else {
-                cx.theme().colors().text_muted
-            })
+            .bg(tab_bg)
+            // 30px @ 16px/rem
+            .h(rems(1.875))
             .map(|this| {
+                let is_first_item = ix == 0;
                 let is_last_item = ix == self.items.len() - 1;
                 match ix.cmp(&self.active_item_index) {
-                    cmp::Ordering::Less => this.border_l().mr_px(),
+                    cmp::Ordering::Less => {
+                        if is_first_item {
+                            this.pl_px().pr_px().border_b()
+                        } else {
+                            this.border_l().pr_px().border_b()
+                        }
+                    }
                     cmp::Ordering::Greater => {
                         if is_last_item {
-                            this.mr_px().ml_px()
+                            this.pr_px().pl_px().border_b()
+                        } else {
+                            this.border_r().pl_px().border_b()
+                        }
+                    }
+                    cmp::Ordering::Equal => {
+                        if is_first_item {
+                            this.pl_px().border_r().pb_px()
                         } else {
-                            this.border_r().ml_px()
+                            this.border_l().border_r().pb_px()
                         }
                     }
-                    cmp::Ordering::Equal => this.border_l().border_r(),
                 }
             })
-            // .hover(|h| h.bg(tab_hover_bg))
-            // .active(|a| a.bg(tab_active_bg))
             .child(
-                div()
-                    .flex()
-                    .items_center()
+                h_stack()
+                    .group("")
+                    .id(ix)
+                    .relative()
+                    .h_full()
+                    .cursor_pointer()
+                    .when_some(item.tab_tooltip_text(cx), |div, text| {
+                        div.tooltip(move |cx| cx.build_view(|cx| Tooltip::new(text.clone())).into())
+                    })
+                    .on_click(
+                        cx.listener(move |v: &mut Self, e, cx| v.activate_item(ix, true, true, cx)),
+                    )
+                    // .on_drag(move |pane, cx| pane.render_tab(ix, item.boxed_clone(), detail, cx))
+                    // .drag_over::<DraggedTab>(|d| d.bg(cx.theme().colors().element_drop_target))
+                    // .on_drop(|_view, state: View<DraggedTab>, cx| {
+                    //     eprintln!("{:?}", state.read(cx));
+                    // })
+                    .px_5()
+                    // .hover(|h| h.bg(tab_hover_bg))
+                    // .active(|a| a.bg(tab_active_bg))
                     .gap_1()
                     .text_color(text_color)
-                    .children(
-                        item.has_conflict(cx)
-                            .then(|| {
-                                div().border().border_color(gpui::red()).child(
-                                    IconElement::new(Icon::ExclamationTriangle)
-                                        .size(ui::IconSize::Small)
-                                        .color(Color::Warning),
-                                )
-                            })
-                            .or(item.is_dirty(cx).then(|| {
-                                div().border().border_color(gpui::red()).child(
-                                    IconElement::new(Icon::ExclamationTriangle)
-                                        .size(ui::IconSize::Small)
-                                        .color(Color::Info),
-                                )
-                            })),
-                    )
-                    .children((!close_right).then(|| close_icon()))
-                    .child(label)
-                    .children(close_right.then(|| close_icon())),
+                    .child(indicator)
+                    .child(close_button)
+                    .child(label),
             );
 
         right_click_menu(ix).trigger(tab).menu(|cx| {
             ContextMenu::build(cx, |menu, cx| {
-                menu.action(
-                    "Close Active Item",
-                    CloseActiveItem { save_intent: None }.boxed_clone(),
-                )
-                .action("Close Inactive Items", CloseInactiveItems.boxed_clone())
-                .action("Close Clean Items", CloseCleanItems.boxed_clone())
-                .action("Close Items To The Left", CloseItemsToTheLeft.boxed_clone())
-                .action(
-                    "Close Items To The Right",
-                    CloseItemsToTheRight.boxed_clone(),
-                )
-                .action(
-                    "Close All Items",
-                    CloseAllItems { save_intent: None }.boxed_clone(),
-                )
+                menu.action("Close", CloseActiveItem { save_intent: None }.boxed_clone())
+                    .action("Close Others", CloseInactiveItems.boxed_clone())
+                    .separator()
+                    .action("Close Left", CloseItemsToTheLeft.boxed_clone())
+                    .action("Close Right", CloseItemsToTheRight.boxed_clone())
+                    .separator()
+                    .action("Close Clean", CloseCleanItems.boxed_clone())
+                    .action(
+                        "Close All",
+                        CloseAllItems { save_intent: None }.boxed_clone(),
+                    )
             })
         })
     }
@@ -1562,116 +1578,118 @@ impl Pane {
             // Left Side
             .child(
                 h_stack()
-                    .px_2()
                     .flex()
                     .flex_none()
                     .gap_1()
+                    .px_1()
+                    .border_b()
+                    .border_r()
+                    .border_color(cx.theme().colors().border)
                     // Nav Buttons
                     .child(
-                        div().border().border_color(gpui::red()).child(
-                            IconButton::new("navigate_backward", Icon::ArrowLeft)
-                                .on_click({
-                                    let view = cx.view().clone();
-                                    move |_, cx| view.update(cx, Self::navigate_backward)
-                                })
-                                .disabled(!self.can_navigate_backward()),
-                        ),
+                        IconButton::new("navigate_backward", Icon::ArrowLeft)
+                            .icon_size(IconSize::Small)
+                            .on_click({
+                                let view = cx.view().clone();
+                                move |_, cx| view.update(cx, Self::navigate_backward)
+                            })
+                            .disabled(!self.can_navigate_backward()),
                     )
                     .child(
-                        div().border().border_color(gpui::red()).child(
-                            IconButton::new("navigate_forward", Icon::ArrowRight)
-                                .on_click({
-                                    let view = cx.view().clone();
-                                    move |_, cx| view.update(cx, Self::navigate_backward)
-                                })
-                                .disabled(!self.can_navigate_forward()),
-                        ),
+                        IconButton::new("navigate_forward", Icon::ArrowRight)
+                            .icon_size(IconSize::Small)
+                            .on_click({
+                                let view = cx.view().clone();
+                                move |_, cx| view.update(cx, Self::navigate_backward)
+                            })
+                            .disabled(!self.can_navigate_forward()),
                     ),
             )
             .child(
-                div().flex_1().h_full().child(
-                    div().id("tabs").flex().overflow_x_scroll().children(
-                        self.items
-                            .iter()
-                            .enumerate()
-                            .zip(self.tab_details(cx))
-                            .map(|((ix, item), detail)| self.render_tab(ix, item, detail, cx)),
+                div()
+                    .relative()
+                    .flex_1()
+                    .h_full()
+                    .overflow_hidden_x()
+                    .child(
+                        div()
+                            .absolute()
+                            .top_0()
+                            .left_0()
+                            .z_index(1)
+                            .size_full()
+                            .border_b()
+                            .border_color(cx.theme().colors().border),
+                    )
+                    .child(
+                        h_stack().id("tabs").z_index(2).children(
+                            self.items
+                                .iter()
+                                .enumerate()
+                                .zip(self.tab_details(cx))
+                                .map(|((ix, item), detail)| self.render_tab(ix, item, detail, cx)),
+                        ),
                     ),
-                ),
             )
             // Right Side
             .child(
-                div()
-                    .px_1()
+                h_stack()
                     .flex()
                     .flex_none()
-                    .gap_2()
-                    // Nav Buttons
+                    .gap_1()
+                    .px_1()
+                    .border_b()
+                    .border_l()
+                    .border_color(cx.theme().colors().border)
                     .child(
                         div()
                             .flex()
                             .items_center()
                             .gap_px()
                             .child(
-                                div()
-                                    .bg(gpui::blue())
-                                    .border()
-                                    .border_color(gpui::red())
-                                    .child(IconButton::new("plus", Icon::Plus).on_click(
-                                        cx.listener(|this, _, cx| {
-                                            let menu = ContextMenu::build(cx, |menu, cx| {
-                                                menu.action("New File", NewFile.boxed_clone())
-                                                    .action(
-                                                        "New Terminal",
-                                                        NewCenterTerminal.boxed_clone(),
-                                                    )
-                                                    .action("New Search", NewSearch.boxed_clone())
-                                            });
-                                            cx.subscribe(
-                                                &menu,
-                                                |this, _, event: &DismissEvent, cx| {
-                                                    this.focus(cx);
-                                                    this.new_item_menu = None;
-                                                },
-                                            )
-                                            .detach();
-                                            this.new_item_menu = Some(menu);
-                                        }),
-                                    ))
-                                    .when_some(self.new_item_menu.as_ref(), |el, new_item_menu| {
-                                        el.child(Self::render_menu_overlay(new_item_menu))
-                                    }),
+                                IconButton::new("plus", Icon::Plus)
+                                    .icon_size(IconSize::Small)
+                                    .on_click(cx.listener(|this, _, cx| {
+                                        let menu = ContextMenu::build(cx, |menu, cx| {
+                                            menu.action("New File", NewFile.boxed_clone())
+                                                .action(
+                                                    "New Terminal",
+                                                    NewCenterTerminal.boxed_clone(),
+                                                )
+                                                .action("New Search", NewSearch.boxed_clone())
+                                        });
+                                        cx.subscribe(&menu, |this, _, event: &DismissEvent, cx| {
+                                            this.focus(cx);
+                                            this.new_item_menu = None;
+                                        })
+                                        .detach();
+                                        this.new_item_menu = Some(menu);
+                                    })),
                             )
+                            .when_some(self.new_item_menu.as_ref(), |el, new_item_menu| {
+                                el.child(Self::render_menu_overlay(new_item_menu))
+                            })
                             .child(
-                                div()
-                                    .border()
-                                    .border_color(gpui::red())
-                                    .child(IconButton::new("split", Icon::Split).on_click(
-                                        cx.listener(|this, _, cx| {
-                                            let menu = ContextMenu::build(cx, |menu, cx| {
-                                                menu.action("Split Right", SplitRight.boxed_clone())
-                                                    .action("Split Left", SplitLeft.boxed_clone())
-                                                    .action("Split Up", SplitUp.boxed_clone())
-                                                    .action("Split Down", SplitDown.boxed_clone())
-                                            });
-                                            cx.subscribe(
-                                                &menu,
-                                                |this, _, event: &DismissEvent, cx| {
-                                                    this.focus(cx);
-                                                    this.split_item_menu = None;
-                                                },
-                                            )
-                                            .detach();
-                                            this.split_item_menu = Some(menu);
-                                        }),
-                                    ))
-                                    .when_some(
-                                        self.split_item_menu.as_ref(),
-                                        |el, split_item_menu| {
-                                            el.child(Self::render_menu_overlay(split_item_menu))
-                                        },
-                                    ),
-                            ),
+                                IconButton::new("split", Icon::Split)
+                                    .icon_size(IconSize::Small)
+                                    .on_click(cx.listener(|this, _, cx| {
+                                        let menu = ContextMenu::build(cx, |menu, cx| {
+                                            menu.action("Split Right", SplitRight.boxed_clone())
+                                                .action("Split Left", SplitLeft.boxed_clone())
+                                                .action("Split Up", SplitUp.boxed_clone())
+                                                .action("Split Down", SplitDown.boxed_clone())
+                                        });
+                                        cx.subscribe(&menu, |this, _, event: &DismissEvent, cx| {
+                                            this.focus(cx);
+                                            this.split_item_menu = None;
+                                        })
+                                        .detach();
+                                        this.split_item_menu = Some(menu);
+                                    })),
+                            )
+                            .when_some(self.split_item_menu.as_ref(), |el, split_item_menu| {
+                                el.child(Self::render_menu_overlay(split_item_menu))
+                            }),
                     ),
             )
     }
@@ -2089,6 +2107,8 @@ impl Render for Pane {
         v_stack()
             .key_context("Pane")
             .track_focus(&self.focus_handle)
+            .size_full()
+            .overflow_hidden()
             .on_focus_in({
                 let this = this.clone();
                 move |event, cx| {
@@ -2156,7 +2176,6 @@ impl Render for Pane {
                 pane.close_all_items(action, cx)
                     .map(|task| task.detach_and_log_err(cx));
             }))
-            .size_full()
             .on_action(
                 cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
                     pane.close_active_item(action, cx)

crates/workspace2/src/shared_screen.rs 🔗

@@ -59,7 +59,6 @@ impl SharedScreen {
 }
 
 impl EventEmitter<Event> for SharedScreen {}
-impl EventEmitter<ItemEvent> for SharedScreen {}
 
 impl FocusableView for SharedScreen {
     fn focus_handle(&self, _: &AppContext) -> FocusHandle {
@@ -79,9 +78,12 @@ impl Render for SharedScreen {
 }
 
 impl Item for SharedScreen {
+    type Event = Event;
+
     fn tab_tooltip_text(&self, _: &AppContext) -> Option<SharedString> {
         Some(format!("{}'s screen", self.user.github_login).into())
     }
+
     fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
         if let Some(nav_history) = self.nav_history.as_mut() {
             nav_history.push::<()>(None, cx);
@@ -111,4 +113,10 @@ impl Item for SharedScreen {
         let track = self.track.upgrade()?;
         Some(cx.build_view(|cx| Self::new(&track, self.peer_id, self.user.clone(), cx)))
     }
+
+    fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) {
+        match event {
+            Event::Close => f(ItemEvent::CloseItem),
+        }
+    }
 }

crates/workspace2/src/toolbar.rs 🔗

@@ -1,10 +1,10 @@
 use crate::ItemHandle;
 use gpui::{
-    div, AnyView, Div, Entity, EntityId, EventEmitter, ParentElement as _, Render, Styled, View,
+    AnyView, Div, Entity, EntityId, EventEmitter, ParentElement as _, Render, Styled, View,
     ViewContext, WindowContext,
 };
 use ui::prelude::*;
-use ui::{h_stack, v_stack, Icon, IconButton};
+use ui::{h_stack, v_stack};
 
 pub enum ToolbarItemEvent {
     ChangeLocation(ToolbarItemLocation),
@@ -87,25 +87,7 @@ impl Render for Toolbar {
             .child(
                 h_stack()
                     .justify_between()
-                    // Toolbar left side
-                    .children(self.items.iter().map(|(child, _)| child.to_any()))
-                    // Toolbar right side
-                    .child(
-                        h_stack()
-                            .p_1()
-                            .child(
-                                div()
-                                    .border()
-                                    .border_color(gpui::red())
-                                    .child(IconButton::new("buffer-search", Icon::MagnifyingGlass)),
-                            )
-                            .child(
-                                div()
-                                    .border()
-                                    .border_color(gpui::red())
-                                    .child(IconButton::new("inline-assist", Icon::MagicWand)),
-                            ),
-                    ),
+                    .children(self.items.iter().map(|(child, _)| child.to_any())),
             )
     }
 }

crates/workspace2/src/workspace2.rs 🔗

@@ -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;
@@ -212,27 +212,31 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
     init_settings(cx);
     notifications::init(cx);
 
-    //     cx.add_global_action({
-    //         let app_state = Arc::downgrade(&app_state);
-    //         move |_: &Open, cx: &mut AppContext| {
-    //             let mut paths = cx.prompt_for_paths(PathPromptOptions {
-    //                 files: true,
-    //                 directories: true,
-    //                 multiple: true,
-    //             });
+    cx.on_action(Workspace::close_global);
+    cx.on_action(restart);
 
-    //             if let Some(app_state) = app_state.upgrade() {
-    //                 cx.spawn(move |mut cx| async move {
-    //                     if let Some(paths) = paths.recv().await.flatten() {
-    //                         cx.update(|cx| {
-    //                             open_paths(&paths, &app_state, None, cx).detach_and_log_err(cx)
-    //                         });
-    //                     }
-    //                 })
-    //                 .detach();
-    //             }
-    //         }
-    //     });
+    cx.on_action({
+        let app_state = Arc::downgrade(&app_state);
+        move |_: &Open, cx: &mut AppContext| {
+            let mut paths = cx.prompt_for_paths(PathPromptOptions {
+                files: true,
+                directories: true,
+                multiple: true,
+            });
+
+            if let Some(app_state) = app_state.upgrade() {
+                cx.spawn(move |mut cx| async move {
+                    if let Some(paths) = paths.await.log_err().flatten() {
+                        cx.update(|cx| {
+                            open_paths(&paths, &app_state, None, cx).detach_and_log_err(cx)
+                        })
+                        .ok();
+                    }
+                })
+                .detach();
+            }
+        }
+    });
 }
 
 type ProjectItemBuilders =
@@ -1076,7 +1080,6 @@ impl Workspace {
         }
     }
 
-    // todo!(Non-window-actions)
     pub fn close_global(_: &CloseWindow, cx: &mut AppContext) {
         cx.windows().iter().find(|window| {
             window
@@ -1094,21 +1097,18 @@ impl Workspace {
         });
     }
 
-    pub fn close(
-        &mut self,
-        _: &CloseWindow,
-        cx: &mut ViewContext<Self>,
-    ) -> Option<Task<Result<()>>> {
+    pub fn close_window(&mut self, _: &CloseWindow, cx: &mut ViewContext<Self>) {
         let window = cx.window_handle();
         let prepare = self.prepare_to_close(false, cx);
-        Some(cx.spawn(|_, mut cx| async move {
+        cx.spawn(|_, mut cx| async move {
             if prepare.await? {
                 window.update(&mut cx, |_, cx| {
                     cx.remove_window();
                 })?;
             }
-            Ok(())
-        }))
+            anyhow::Ok(())
+        })
+        .detach_and_log_err(cx)
     }
 
     pub fn prepare_to_close(
@@ -1855,13 +1855,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>,
         )>,
     > {
@@ -2327,42 +2327,44 @@ impl Workspace {
         }))
     }
 
-    //     pub fn follow_next_collaborator(
-    //         &mut self,
-    //         _: &FollowNextCollaborator,
-    //         cx: &mut ViewContext<Self>,
-    //     ) -> Option<Task<Result<()>>> {
-    //         let collaborators = self.project.read(cx).collaborators();
-    //         let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
-    //             let mut collaborators = collaborators.keys().copied();
-    //             for peer_id in collaborators.by_ref() {
-    //                 if peer_id == leader_id {
-    //                     break;
-    //                 }
-    //             }
-    //             collaborators.next()
-    //         } else if let Some(last_leader_id) =
-    //             self.last_leaders_by_pane.get(&self.active_pane.downgrade())
-    //         {
-    //             if collaborators.contains_key(last_leader_id) {
-    //                 Some(*last_leader_id)
-    //             } else {
-    //                 None
+    // pub fn follow_next_collaborator(
+    //     &mut self,
+    //     _: &FollowNextCollaborator,
+    //     cx: &mut ViewContext<Self>,
+    // ) {
+    //     let collaborators = self.project.read(cx).collaborators();
+    //     let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) {
+    //         let mut collaborators = collaborators.keys().copied();
+    //         for peer_id in collaborators.by_ref() {
+    //             if peer_id == leader_id {
+    //                 break;
     //             }
+    //         }
+    //         collaborators.next()
+    //     } else if let Some(last_leader_id) =
+    //         self.last_leaders_by_pane.get(&self.active_pane.downgrade())
+    //     {
+    //         if collaborators.contains_key(last_leader_id) {
+    //             Some(*last_leader_id)
     //         } else {
     //             None
-    //         };
-
-    //         let pane = self.active_pane.clone();
-    //         let Some(leader_id) = next_leader_id.or_else(|| collaborators.keys().copied().next())
-    //         else {
-    //             return None;
-    //         };
-    //         if Some(leader_id) == self.unfollow(&pane, cx) {
-    //             return None;
     //         }
-    //         self.follow(leader_id, cx)
+    //     } else {
+    //         None
+    //     };
+
+    //     let pane = self.active_pane.clone();
+    //     let Some(leader_id) = next_leader_id.or_else(|| collaborators.keys().copied().next())
+    //     else {
+    //         return;
+    //     };
+    //     if Some(leader_id) == self.unfollow(&pane, cx) {
+    //         return;
     //     }
+    //     if let Some(task) = self.follow(leader_id, cx) {
+    //         task.detach();
+    //     }
+    // }
 
     pub fn follow(
         &mut self,
@@ -2411,6 +2413,18 @@ impl Workspace {
         self.start_following(leader_id, cx)
     }
 
+    //     // if you're already following, find the right pane and focus it.
+    //     for (pane, state) in &self.follower_states {
+    //         if leader_id == state.leader_id {
+    //             cx.focus(pane);
+    //             return None;
+    //         }
+    //     }
+
+    //     // Otherwise, follow.
+    //     self.start_following(leader_id, cx)
+    // }
+
     pub fn unfollow(&mut self, pane: &View<Pane>, cx: &mut ViewContext<Self>) -> Option<PeerId> {
         let state = self.follower_states.remove(pane)?;
         let leader_id = state.leader_id;
@@ -2627,8 +2641,6 @@ impl Workspace {
         update: proto::UpdateFollowers,
         cx: &mut AsyncWindowContext,
     ) -> Result<()> {
-        dbg!("process_leader_update", &update);
-
         match update.variant.ok_or_else(|| anyhow!("invalid update"))? {
             proto::update_followers::Variant::UpdateActiveView(update_active_view) => {
                 this.update(cx, |this, _| {
@@ -3223,13 +3235,8 @@ impl Workspace {
 
     fn actions(&self, div: Div, cx: &mut ViewContext<Self>) -> Div {
         self.add_workspace_actions_listeners(div, cx)
-            //     cx.add_async_action(Workspace::open);
-            //     cx.add_async_action(Workspace::follow_next_collaborator);
-            //     cx.add_async_action(Workspace::close);
             .on_action(cx.listener(Self::close_inactive_items_and_panes))
             .on_action(cx.listener(Self::close_all_items_and_panes))
-            //     cx.add_global_action(Workspace::close_global);
-            //     cx.add_global_action(restart);
             .on_action(cx.listener(Self::save_all))
             .on_action(cx.listener(Self::add_folder_to_project))
             .on_action(cx.listener(|workspace, _: &Unfollow, cx| {
@@ -3278,6 +3285,9 @@ impl Workspace {
                     workspace.close_all_docks(cx);
                 }),
             )
+            .on_action(cx.listener(Workspace::open))
+            .on_action(cx.listener(Workspace::close_window))
+
         //     cx.add_action(Workspace::activate_pane_at_index);
         //     cx.add_action(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
         //         workspace.reopen_closed_item(cx).detach();
@@ -3882,8 +3892,6 @@ impl WorkspaceStore {
         let leader_id = envelope.original_sender_id()?;
         let update = envelope.payload;
 
-        dbg!("handle_upate_followers");
-
         this.update(&mut cx, |this, cx| {
             for workspace in &this.workspaces {
                 workspace.update(cx, |workspace, cx| {

crates/zed/src/zed.rs 🔗

@@ -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);

crates/zed2/Cargo.toml 🔗

@@ -55,7 +55,7 @@ outline = { package = "outline2", path = "../outline2" }
 project = { package = "project2", path = "../project2" }
 project_panel = { package = "project_panel2", path = "../project_panel2" }
 # project_symbols = { path = "../project_symbols" }
-# quick_action_bar = { path = "../quick_action_bar" }
+quick_action_bar = { package = "quick_action_bar2", path = "../quick_action_bar2" }
 # recent_projects = { path = "../recent_projects" }
 rope = { package = "rope2", path = "../rope2"}
 rpc = { package = "rpc2", path = "../rpc2" }

crates/zed2/src/zed2.rs 🔗

@@ -19,6 +19,7 @@ pub use open_listener::*;
 
 use anyhow::{anyhow, Context as _};
 use project_panel::ProjectPanel;
+use quick_action_bar::QuickActionBar;
 use settings::{initial_local_settings_content, Settings};
 use std::{borrow::Cow, ops::Deref, sync::Arc};
 use terminal_view::terminal_panel::TerminalPanel;
@@ -100,11 +101,10 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
                             toolbar.add_item(breadcrumbs, cx);
                             let buffer_search_bar = cx.build_view(search::BufferSearchBar::new);
                             toolbar.add_item(buffer_search_bar.clone(), cx);
-                            // todo!()
-                            //     let quick_action_bar = cx.add_view(|_| {
-                            //         QuickActionBar::new(buffer_search_bar, workspace)
-                            //     });
-                            //     toolbar.add_item(quick_action_bar, cx);
+
+                            let quick_action_bar = cx
+                                .build_view(|_| QuickActionBar::new(buffer_search_bar, workspace));
+                            toolbar.add_item(quick_action_bar, cx);
                             let diagnostic_editor_controls =
                                 cx.build_view(|_| diagnostics::ToolbarControls::new());
                             //     toolbar.add_item(diagnostic_editor_controls, cx);
@@ -168,9 +168,7 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
         cx.on_window_should_close(move |cx| {
             handle
                 .update(cx, |workspace, cx| {
-                    if let Some(task) = workspace.close(&Default::default(), cx) {
-                        task.detach_and_log_err(cx);
-                    }
+                    workspace.close_window(&Default::default(), cx);
                     false
                 })
                 .unwrap_or(true)
@@ -582,8 +580,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 +590,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 +716,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()),
+//         ))
+//     }
+// }