Remove 2 suffix for language selector, project panel, recent_projects, copilot_button, breadcrumbs, activity_indicator

Max Brunsfeld and Mikayla created

Co-authored-by: Mikayla <mikayla@zed.dev>

Change summary

Cargo.lock                                                    |  134 
Cargo.toml                                                    |    5 
crates/activity_indicator/Cargo.toml                          |   20 
crates/activity_indicator/src/activity_indicator.rs           |  112 
crates/activity_indicator2/Cargo.toml                         |   28 
crates/activity_indicator2/src/activity_indicator.rs          |  331 
crates/breadcrumbs/Cargo.toml                                 |   25 
crates/breadcrumbs/src/breadcrumbs.rs                         |  151 
crates/breadcrumbs2/Cargo.toml                                |   28 
crates/breadcrumbs2/src/breadcrumbs.rs                        |  111 
crates/collab_ui/Cargo.toml                                   |    2 
crates/copilot_button/Cargo.toml                              |   20 
crates/copilot_button/src/copilot_button.rs                   |  343 
crates/copilot_button2/Cargo.toml                             |   27 
crates/copilot_button2/src/copilot_button.rs                  |  378 
crates/language_selector/Cargo.toml                           |   21 
crates/language_selector/src/active_buffer_language.rs        |   61 
crates/language_selector/src/language_selector.rs             |  152 
crates/language_selector2/Cargo.toml                          |   26 
crates/language_selector2/src/active_buffer_language.rs       |   79 
crates/language_selector2/src/language_selector.rs            |  232 
crates/project_panel/Cargo.toml                               |   33 
crates/project_panel/src/file_associations.rs                 |   71 
crates/project_panel/src/project_panel.rs                     |  757 
crates/project_panel/src/project_panel_settings.rs            |    9 
crates/project_panel2/Cargo.toml                              |   41 
crates/project_panel2/src/file_associations.rs                |   87 
crates/project_panel2/src/project_panel.rs                    | 3480 -----
crates/project_panel2/src/project_panel_settings.rs           |   48 
crates/recent_projects/Cargo.toml                             |   22 
crates/recent_projects/src/highlighted_workspace_location.rs  |   14 
crates/recent_projects/src/projects.rs                        |    0 
crates/recent_projects/src/recent_projects.rs                 |  226 
crates/recent_projects2/Cargo.toml                            |   30 
crates/recent_projects2/src/highlighted_workspace_location.rs |  129 
crates/recent_projects2/src/recent_projects.rs                |  247 
crates/zed/Cargo.toml                                         |   12 
37 files changed, 948 insertions(+), 6,544 deletions(-)

Detailed changes

Cargo.lock πŸ”—

@@ -5,23 +5,6 @@ version = 3
 [[package]]
 name = "activity_indicator"
 version = "0.1.0"
-dependencies = [
- "auto_update",
- "editor",
- "futures 0.3.28",
- "gpui",
- "language",
- "project",
- "settings",
- "smallvec",
- "theme",
- "util",
- "workspace",
-]
-
-[[package]]
-name = "activity_indicator2"
-version = "0.1.0"
 dependencies = [
  "anyhow",
  "auto_update2",
@@ -1128,23 +1111,6 @@ dependencies = [
 [[package]]
 name = "breadcrumbs"
 version = "0.1.0"
-dependencies = [
- "collections",
- "editor",
- "gpui",
- "itertools 0.10.5",
- "language",
- "outline",
- "project",
- "search",
- "settings",
- "theme",
- "workspace",
-]
-
-[[package]]
-name = "breadcrumbs2"
-version = "0.1.0"
 dependencies = [
  "collections",
  "editor2",
@@ -1855,7 +1821,7 @@ dependencies = [
  "postage",
  "pretty_assertions",
  "project2",
- "recent_projects2",
+ "recent_projects",
  "rich_text2",
  "rpc2",
  "schemars",
@@ -2059,25 +2025,6 @@ dependencies = [
 [[package]]
 name = "copilot_button"
 version = "0.1.0"
-dependencies = [
- "anyhow",
- "context_menu",
- "copilot",
- "editor",
- "fs",
- "futures 0.3.28",
- "gpui",
- "language",
- "settings",
- "smol",
- "theme",
- "util",
- "workspace",
-]
-
-[[package]]
-name = "copilot_button2"
-version = "0.1.0"
 dependencies = [
  "anyhow",
  "copilot2",
@@ -4593,23 +4540,6 @@ dependencies = [
 [[package]]
 name = "language_selector"
 version = "0.1.0"
-dependencies = [
- "anyhow",
- "editor",
- "fuzzy",
- "gpui",
- "language",
- "picker",
- "project",
- "settings",
- "theme",
- "util",
- "workspace",
-]
-
-[[package]]
-name = "language_selector2"
-version = "0.1.0"
 dependencies = [
  "anyhow",
  "editor2",
@@ -6618,35 +6548,6 @@ dependencies = [
 [[package]]
 name = "project_panel"
 version = "0.1.0"
-dependencies = [
- "anyhow",
- "client",
- "collections",
- "context_menu",
- "db",
- "drag_and_drop",
- "editor",
- "futures 0.3.28",
- "gpui",
- "language",
- "menu",
- "postage",
- "pretty_assertions",
- "project",
- "schemars",
- "serde",
- "serde_derive",
- "serde_json",
- "settings",
- "theme",
- "unicase",
- "util",
- "workspace",
-]
-
-[[package]]
-name = "project_panel2"
-version = "0.1.0"
 dependencies = [
  "anyhow",
  "client2",
@@ -7029,27 +6930,6 @@ dependencies = [
 [[package]]
 name = "recent_projects"
 version = "0.1.0"
-dependencies = [
- "db",
- "editor",
- "futures 0.3.28",
- "fuzzy",
- "gpui",
- "language",
- "ordered-float 2.10.0",
- "picker",
- "postage",
- "settings",
- "smol",
- "text",
- "theme",
- "util",
- "workspace",
-]
-
-[[package]]
-name = "recent_projects2"
-version = "0.1.0"
 dependencies = [
  "editor2",
  "futures 0.3.28",
@@ -11290,7 +11170,7 @@ dependencies = [
 name = "zed"
 version = "0.119.0"
 dependencies = [
- "activity_indicator2",
+ "activity_indicator",
  "ai2",
  "anyhow",
  "assistant2",
@@ -11301,7 +11181,7 @@ dependencies = [
  "audio2",
  "auto_update2",
  "backtrace",
- "breadcrumbs2",
+ "breadcrumbs",
  "call2",
  "channel2",
  "chrono",
@@ -11311,7 +11191,7 @@ dependencies = [
  "collections",
  "command_palette",
  "copilot2",
- "copilot_button2",
+ "copilot_button",
  "ctor",
  "db2",
  "diagnostics",
@@ -11332,7 +11212,7 @@ dependencies = [
  "isahc",
  "journal2",
  "language2",
- "language_selector2",
+ "language_selector",
  "language_tools2",
  "lazy_static",
  "libc",
@@ -11346,11 +11226,11 @@ dependencies = [
  "parking_lot 0.11.2",
  "postage",
  "project2",
- "project_panel2",
+ "project_panel",
  "project_symbols",
  "quick_action_bar",
  "rand 0.8.5",
- "recent_projects2",
+ "recent_projects",
  "regex",
  "rope2",
  "rpc2",

Cargo.toml πŸ”—

@@ -1,7 +1,6 @@
 [workspace]
 members = [
     "crates/activity_indicator",
-    "crates/activity_indicator2",
     "crates/ai",
     "crates/assistant",
     "crates/assistant2",
@@ -10,7 +9,6 @@ members = [
     "crates/auto_update",
     "crates/auto_update2",
     "crates/breadcrumbs",
-    "crates/breadcrumbs2",
     "crates/call",
     "crates/call2",
     "crates/channel",
@@ -58,7 +56,6 @@ members = [
     "crates/language",
     "crates/language2",
     "crates/language_selector",
-    "crates/language_selector2",
     "crates/language_tools",
     "crates/language_tools2",
     "crates/live_kit_client",
@@ -84,11 +81,9 @@ members = [
     "crates/project",
     "crates/project2",
     "crates/project_panel",
-    "crates/project_panel2",
     "crates/project_symbols",
     "crates/quick_action_bar",
     "crates/recent_projects",
-    "crates/recent_projects2",
     "crates/rope",
     "crates/rpc",
     "crates/rpc2",

crates/activity_indicator/Cargo.toml πŸ”—

@@ -9,18 +9,20 @@ path = "src/activity_indicator.rs"
 doctest = false
 
 [dependencies]
-auto_update = { path = "../auto_update" }
-editor = { path = "../editor" }
-language = { path = "../language" }
-gpui = { path = "../gpui" }
-project = { path = "../project" }
-settings = { path = "../settings" }
+auto_update = { path = "../auto_update2", package = "auto_update2" }
+editor = { path = "../editor2", package = "editor2" }
+language = { path = "../language2", package = "language2" }
+gpui = { path = "../gpui2", package = "gpui2" }
+project = { path = "../project2", package = "project2" }
+settings = { path = "../settings2", package = "settings2" }
+ui = { path = "../ui2", package = "ui2" }
 util = { path = "../util" }
-theme = { path = "../theme" }
-workspace = { path = "../workspace" }
+theme = { path = "../theme2", package = "theme2" }
+workspace = { path = "../workspace2", package = "workspace2" }
 
+anyhow.workspace = true
 futures.workspace = true
 smallvec.workspace = true
 
 [dev-dependencies]
-editor = { path = "../editor", features = ["test-support"] }
+editor = { path = "../editor2", package = "editor2", features = ["test-support"] }

crates/activity_indicator/src/activity_indicator.rs πŸ”—

@@ -2,19 +2,19 @@ use auto_update::{AutoUpdateStatus, AutoUpdater, DismissErrorMessage};
 use editor::Editor;
 use futures::StreamExt;
 use gpui::{
-    actions, anyhow,
-    elements::*,
-    platform::{CursorStyle, MouseButton},
-    AppContext, Entity, ModelHandle, View, ViewContext, ViewHandle,
+    actions, svg, AppContext, CursorStyle, EventEmitter, InteractiveElement as _, Model,
+    ParentElement as _, Render, SharedString, StatefulInteractiveElement, Styled, View,
+    ViewContext, VisualContext as _,
 };
 use language::{LanguageRegistry, LanguageServerBinaryStatus};
 use project::{LanguageServerProgress, Project};
 use smallvec::SmallVec;
 use std::{cmp::Reverse, fmt::Write, sync::Arc};
+use ui::prelude::*;
 use util::ResultExt;
 use workspace::{item::ItemHandle, StatusItemView, Workspace};
 
-actions!(lsp_status, [ShowErrorMessage]);
+actions!(activity_indicator, [ShowErrorMessage]);
 
 const DOWNLOAD_ICON: &str = "icons/download.svg";
 const WARNING_ICON: &str = "icons/warning.svg";
@@ -25,8 +25,8 @@ pub enum Event {
 
 pub struct ActivityIndicator {
     statuses: Vec<LspStatus>,
-    project: ModelHandle<Project>,
-    auto_updater: Option<ModelHandle<AutoUpdater>>,
+    project: Model<Project>,
+    auto_updater: Option<Model<AutoUpdater>>,
 }
 
 struct LspStatus {
@@ -47,20 +47,15 @@ struct Content {
     on_click: Option<Arc<dyn Fn(&mut ActivityIndicator, &mut ViewContext<ActivityIndicator>)>>,
 }
 
-pub fn init(cx: &mut AppContext) {
-    cx.add_action(ActivityIndicator::show_error_message);
-    cx.add_action(ActivityIndicator::dismiss_error_message);
-}
-
 impl ActivityIndicator {
     pub fn new(
         workspace: &mut Workspace,
         languages: Arc<LanguageRegistry>,
         cx: &mut ViewContext<Workspace>,
-    ) -> ViewHandle<ActivityIndicator> {
+    ) -> View<ActivityIndicator> {
         let project = workspace.project().clone();
         let auto_updater = AutoUpdater::get(cx);
-        let this = cx.add_view(|cx: &mut ViewContext<Self>| {
+        let this = cx.new_view(|cx: &mut ViewContext<Self>| {
             let mut status_events = languages.language_server_binary_statuses();
             cx.spawn(|this, mut cx| async move {
                 while let Some((language, event)) = status_events.next().await {
@@ -77,11 +72,13 @@ impl ActivityIndicator {
             })
             .detach();
             cx.observe(&project, |_, _, cx| cx.notify()).detach();
+
             if let Some(auto_updater) = auto_updater.as_ref() {
                 cx.observe(auto_updater, |_, _, cx| cx.notify()).detach();
             }
-            cx.observe_active_labeled_tasks(|_, cx| cx.notify())
-                .detach();
+
+            // cx.observe_active_labeled_tasks(|_, cx| cx.notify())
+            //     .detach();
 
             Self {
                 statuses: Default::default(),
@@ -89,6 +86,7 @@ impl ActivityIndicator {
                 auto_updater,
             }
         });
+
         cx.subscribe(&this, move |workspace, _, event, cx| match event {
             Event::ShowError { lsp_name, error } => {
                 if let Some(buffer) = project
@@ -104,7 +102,7 @@ impl ActivityIndicator {
                     });
                     workspace.add_item(
                         Box::new(
-                            cx.add_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx)),
+                            cx.new_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx)),
                         ),
                         cx,
                     );
@@ -290,71 +288,41 @@ impl ActivityIndicator {
             };
         }
 
-        if let Some(most_recent_active_task) = cx.active_labeled_tasks().last() {
-            return Content {
-                icon: None,
-                message: most_recent_active_task.to_string(),
-                on_click: None,
-            };
-        }
+        // todo!(show active tasks)
+        // if let Some(most_recent_active_task) = cx.active_labeled_tasks().last() {
+        //     return Content {
+        //         icon: None,
+        //         message: most_recent_active_task.to_string(),
+        //         on_click: None,
+        //     };
+        // }
 
         Default::default()
     }
 }
 
-impl Entity for ActivityIndicator {
-    type Event = Event;
-}
+impl EventEmitter<Event> for ActivityIndicator {}
 
-impl View for ActivityIndicator {
-    fn ui_name() -> &'static str {
-        "ActivityIndicator"
-    }
+impl Render for ActivityIndicator {
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+        let content = self.content_to_render(cx);
 
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        let Content {
-            icon,
-            message,
-            on_click,
-        } = self.content_to_render(cx);
-
-        let mut element = MouseEventHandler::new::<Self, _>(0, cx, |state, cx| {
-            let theme = &theme::current(cx).workspace.status_bar.lsp_status;
-            let style = if state.hovered() && on_click.is_some() {
-                theme.hovered.as_ref().unwrap_or(&theme.default)
-            } else {
-                &theme.default
-            };
-            Flex::row()
-                .with_children(icon.map(|path| {
-                    Svg::new(path)
-                        .with_color(style.icon_color)
-                        .constrained()
-                        .with_width(style.icon_width)
-                        .contained()
-                        .with_margin_right(style.icon_spacing)
-                        .aligned()
-                        .into_any_named("activity-icon")
-                }))
-                .with_child(
-                    Text::new(message, style.message.clone())
-                        .with_soft_wrap(false)
-                        .aligned(),
-                )
-                .constrained()
-                .with_height(style.height)
-                .contained()
-                .with_style(style.container)
-                .aligned()
-        });
+        let mut result = h_stack()
+            .id("activity-indicator")
+            .on_action(cx.listener(Self::show_error_message))
+            .on_action(cx.listener(Self::dismiss_error_message));
 
-        if let Some(on_click) = on_click.clone() {
-            element = element
-                .with_cursor_style(CursorStyle::PointingHand)
-                .on_click(MouseButton::Left, move |_, this, cx| on_click(this, cx));
+        if let Some(on_click) = content.on_click {
+            result = result
+                .cursor(CursorStyle::PointingHand)
+                .on_click(cx.listener(move |this, _, cx| {
+                    on_click(this, cx);
+                }))
         }
 
-        element.into_any()
+        result
+            .children(content.icon.map(|icon| svg().path(icon)))
+            .child(Label::new(SharedString::from(content.message)).size(LabelSize::Small))
     }
 }
 

crates/activity_indicator2/Cargo.toml πŸ”—

@@ -1,28 +0,0 @@
-[package]
-name = "activity_indicator2"
-version = "0.1.0"
-edition = "2021"
-publish = false
-
-[lib]
-path = "src/activity_indicator.rs"
-doctest = false
-
-[dependencies]
-auto_update = { path = "../auto_update2", package = "auto_update2" }
-editor = { path = "../editor2", package = "editor2" }
-language = { path = "../language2", package = "language2" }
-gpui = { path = "../gpui2", package = "gpui2" }
-project = { path = "../project2", package = "project2" }
-settings = { path = "../settings2", package = "settings2" }
-ui = { path = "../ui2", package = "ui2" }
-util = { path = "../util" }
-theme = { path = "../theme2", package = "theme2" }
-workspace = { path = "../workspace2", package = "workspace2" }
-
-anyhow.workspace = true
-futures.workspace = true
-smallvec.workspace = true
-
-[dev-dependencies]
-editor = { path = "../editor2", package = "editor2", features = ["test-support"] }

crates/activity_indicator2/src/activity_indicator.rs πŸ”—

@@ -1,331 +0,0 @@
-use auto_update::{AutoUpdateStatus, AutoUpdater, DismissErrorMessage};
-use editor::Editor;
-use futures::StreamExt;
-use gpui::{
-    actions, svg, AppContext, CursorStyle, EventEmitter, InteractiveElement as _, Model,
-    ParentElement as _, Render, SharedString, StatefulInteractiveElement, Styled, View,
-    ViewContext, VisualContext as _,
-};
-use language::{LanguageRegistry, LanguageServerBinaryStatus};
-use project::{LanguageServerProgress, Project};
-use smallvec::SmallVec;
-use std::{cmp::Reverse, fmt::Write, sync::Arc};
-use ui::prelude::*;
-use util::ResultExt;
-use workspace::{item::ItemHandle, StatusItemView, Workspace};
-
-actions!(activity_indicator, [ShowErrorMessage]);
-
-const DOWNLOAD_ICON: &str = "icons/download.svg";
-const WARNING_ICON: &str = "icons/warning.svg";
-
-pub enum Event {
-    ShowError { lsp_name: Arc<str>, error: String },
-}
-
-pub struct ActivityIndicator {
-    statuses: Vec<LspStatus>,
-    project: Model<Project>,
-    auto_updater: Option<Model<AutoUpdater>>,
-}
-
-struct LspStatus {
-    name: Arc<str>,
-    status: LanguageServerBinaryStatus,
-}
-
-struct PendingWork<'a> {
-    language_server_name: &'a str,
-    progress_token: &'a str,
-    progress: &'a LanguageServerProgress,
-}
-
-#[derive(Default)]
-struct Content {
-    icon: Option<&'static str>,
-    message: String,
-    on_click: Option<Arc<dyn Fn(&mut ActivityIndicator, &mut ViewContext<ActivityIndicator>)>>,
-}
-
-impl ActivityIndicator {
-    pub fn new(
-        workspace: &mut Workspace,
-        languages: Arc<LanguageRegistry>,
-        cx: &mut ViewContext<Workspace>,
-    ) -> View<ActivityIndicator> {
-        let project = workspace.project().clone();
-        let auto_updater = AutoUpdater::get(cx);
-        let this = cx.new_view(|cx: &mut ViewContext<Self>| {
-            let mut status_events = languages.language_server_binary_statuses();
-            cx.spawn(|this, mut cx| async move {
-                while let Some((language, event)) = status_events.next().await {
-                    this.update(&mut cx, |this, cx| {
-                        this.statuses.retain(|s| s.name != language.name());
-                        this.statuses.push(LspStatus {
-                            name: language.name(),
-                            status: event,
-                        });
-                        cx.notify();
-                    })?;
-                }
-                anyhow::Ok(())
-            })
-            .detach();
-            cx.observe(&project, |_, _, cx| cx.notify()).detach();
-
-            if let Some(auto_updater) = auto_updater.as_ref() {
-                cx.observe(auto_updater, |_, _, cx| cx.notify()).detach();
-            }
-
-            // cx.observe_active_labeled_tasks(|_, cx| cx.notify())
-            //     .detach();
-
-            Self {
-                statuses: Default::default(),
-                project: project.clone(),
-                auto_updater,
-            }
-        });
-
-        cx.subscribe(&this, move |workspace, _, event, cx| match event {
-            Event::ShowError { lsp_name, error } => {
-                if let Some(buffer) = project
-                    .update(cx, |project, cx| project.create_buffer(error, None, cx))
-                    .log_err()
-                {
-                    buffer.update(cx, |buffer, cx| {
-                        buffer.edit(
-                            [(0..0, format!("Language server error: {}\n\n", lsp_name))],
-                            None,
-                            cx,
-                        );
-                    });
-                    workspace.add_item(
-                        Box::new(
-                            cx.new_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx)),
-                        ),
-                        cx,
-                    );
-                }
-            }
-        })
-        .detach();
-        this
-    }
-
-    fn show_error_message(&mut self, _: &ShowErrorMessage, cx: &mut ViewContext<Self>) {
-        self.statuses.retain(|status| {
-            if let LanguageServerBinaryStatus::Failed { error } = &status.status {
-                cx.emit(Event::ShowError {
-                    lsp_name: status.name.clone(),
-                    error: error.clone(),
-                });
-                false
-            } else {
-                true
-            }
-        });
-
-        cx.notify();
-    }
-
-    fn dismiss_error_message(&mut self, _: &DismissErrorMessage, cx: &mut ViewContext<Self>) {
-        if let Some(updater) = &self.auto_updater {
-            updater.update(cx, |updater, cx| {
-                updater.dismiss_error(cx);
-            });
-        }
-        cx.notify();
-    }
-
-    fn pending_language_server_work<'a>(
-        &self,
-        cx: &'a AppContext,
-    ) -> impl Iterator<Item = PendingWork<'a>> {
-        self.project
-            .read(cx)
-            .language_server_statuses()
-            .rev()
-            .filter_map(|status| {
-                if status.pending_work.is_empty() {
-                    None
-                } else {
-                    let mut pending_work = status
-                        .pending_work
-                        .iter()
-                        .map(|(token, progress)| PendingWork {
-                            language_server_name: status.name.as_str(),
-                            progress_token: token.as_str(),
-                            progress,
-                        })
-                        .collect::<SmallVec<[_; 4]>>();
-                    pending_work.sort_by_key(|work| Reverse(work.progress.last_update_at));
-                    Some(pending_work)
-                }
-            })
-            .flatten()
-    }
-
-    fn content_to_render(&mut self, cx: &mut ViewContext<Self>) -> Content {
-        // Show any language server has pending activity.
-        let mut pending_work = self.pending_language_server_work(cx);
-        if let Some(PendingWork {
-            language_server_name,
-            progress_token,
-            progress,
-        }) = pending_work.next()
-        {
-            let mut message = language_server_name.to_string();
-
-            message.push_str(": ");
-            if let Some(progress_message) = progress.message.as_ref() {
-                message.push_str(progress_message);
-            } else {
-                message.push_str(progress_token);
-            }
-
-            if let Some(percentage) = progress.percentage {
-                write!(&mut message, " ({}%)", percentage).unwrap();
-            }
-
-            let additional_work_count = pending_work.count();
-            if additional_work_count > 0 {
-                write!(&mut message, " + {} more", additional_work_count).unwrap();
-            }
-
-            return Content {
-                icon: None,
-                message,
-                on_click: None,
-            };
-        }
-
-        // Show any language server installation info.
-        let mut downloading = SmallVec::<[_; 3]>::new();
-        let mut checking_for_update = SmallVec::<[_; 3]>::new();
-        let mut failed = SmallVec::<[_; 3]>::new();
-        for status in &self.statuses {
-            let name = status.name.clone();
-            match status.status {
-                LanguageServerBinaryStatus::CheckingForUpdate => checking_for_update.push(name),
-                LanguageServerBinaryStatus::Downloading => downloading.push(name),
-                LanguageServerBinaryStatus::Failed { .. } => failed.push(name),
-                LanguageServerBinaryStatus::Downloaded | LanguageServerBinaryStatus::Cached => {}
-            }
-        }
-
-        if !downloading.is_empty() {
-            return Content {
-                icon: Some(DOWNLOAD_ICON),
-                message: format!(
-                    "Downloading {} language server{}...",
-                    downloading.join(", "),
-                    if downloading.len() > 1 { "s" } else { "" }
-                ),
-                on_click: None,
-            };
-        } else if !checking_for_update.is_empty() {
-            return Content {
-                icon: Some(DOWNLOAD_ICON),
-                message: format!(
-                    "Checking for updates to {} language server{}...",
-                    checking_for_update.join(", "),
-                    if checking_for_update.len() > 1 {
-                        "s"
-                    } else {
-                        ""
-                    }
-                ),
-                on_click: None,
-            };
-        } else if !failed.is_empty() {
-            return Content {
-                icon: Some(WARNING_ICON),
-                message: format!(
-                    "Failed to download {} language server{}. Click to show error.",
-                    failed.join(", "),
-                    if failed.len() > 1 { "s" } else { "" }
-                ),
-                on_click: Some(Arc::new(|this, cx| {
-                    this.show_error_message(&Default::default(), cx)
-                })),
-            };
-        }
-
-        // Show any application auto-update info.
-        if let Some(updater) = &self.auto_updater {
-            return match &updater.read(cx).status() {
-                AutoUpdateStatus::Checking => Content {
-                    icon: Some(DOWNLOAD_ICON),
-                    message: "Checking for Zed updates…".to_string(),
-                    on_click: None,
-                },
-                AutoUpdateStatus::Downloading => Content {
-                    icon: Some(DOWNLOAD_ICON),
-                    message: "Downloading Zed update…".to_string(),
-                    on_click: None,
-                },
-                AutoUpdateStatus::Installing => Content {
-                    icon: Some(DOWNLOAD_ICON),
-                    message: "Installing Zed update…".to_string(),
-                    on_click: None,
-                },
-                AutoUpdateStatus::Updated => Content {
-                    icon: None,
-                    message: "Click to restart and update Zed".to_string(),
-                    on_click: Some(Arc::new(|_, cx| {
-                        workspace::restart(&Default::default(), cx)
-                    })),
-                },
-                AutoUpdateStatus::Errored => Content {
-                    icon: Some(WARNING_ICON),
-                    message: "Auto update failed".to_string(),
-                    on_click: Some(Arc::new(|this, cx| {
-                        this.dismiss_error_message(&Default::default(), cx)
-                    })),
-                },
-                AutoUpdateStatus::Idle => Default::default(),
-            };
-        }
-
-        // todo!(show active tasks)
-        // if let Some(most_recent_active_task) = cx.active_labeled_tasks().last() {
-        //     return Content {
-        //         icon: None,
-        //         message: most_recent_active_task.to_string(),
-        //         on_click: None,
-        //     };
-        // }
-
-        Default::default()
-    }
-}
-
-impl EventEmitter<Event> for ActivityIndicator {}
-
-impl Render for ActivityIndicator {
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
-        let content = self.content_to_render(cx);
-
-        let mut result = h_stack()
-            .id("activity-indicator")
-            .on_action(cx.listener(Self::show_error_message))
-            .on_action(cx.listener(Self::dismiss_error_message));
-
-        if let Some(on_click) = content.on_click {
-            result = result
-                .cursor(CursorStyle::PointingHand)
-                .on_click(cx.listener(move |this, _, cx| {
-                    on_click(this, cx);
-                }))
-        }
-
-        result
-            .children(content.icon.map(|icon| svg().path(icon)))
-            .child(Label::new(SharedString::from(content.message)).size(LabelSize::Small))
-    }
-}
-
-impl StatusItemView for ActivityIndicator {
-    fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, _: &mut ViewContext<Self>) {}
-}

crates/breadcrumbs/Cargo.toml πŸ”—

@@ -10,18 +10,19 @@ doctest = false
 
 [dependencies]
 collections = { path = "../collections" }
-editor = { path = "../editor" }
-gpui = { path = "../gpui" }
-language = { path = "../language" }
-project = { path = "../project" }
-search = { path = "../search" }
-settings = { path = "../settings" }
-theme = { path = "../theme" }
-workspace = { path = "../workspace" }
-outline = { path = "../outline" }
+editor = { package = "editor2", path = "../editor2" }
+gpui = { package = "gpui2", path = "../gpui2" }
+ui = { package = "ui2", path = "../ui2" }
+language = { package = "language2", path = "../language2" }
+project = { package = "project2", path = "../project2" }
+search = { package = "search2", path = "../search2" }
+settings = { package = "settings2", path = "../settings2" }
+theme = { package = "theme2", path = "../theme2" }
+workspace = { package = "workspace2", path = "../workspace2" }
+outline = { package = "outline2", path = "../outline2" }
 itertools = "0.10"
 
 [dev-dependencies]
-editor = { path = "../editor", features = ["test-support"] }
-gpui = { path = "../gpui", features = ["test-support"] }
-workspace = { path = "../workspace", features = ["test-support"] }
+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/breadcrumbs/src/breadcrumbs.rs πŸ”—

@@ -1,108 +1,74 @@
+use editor::Editor;
 use gpui::{
-    elements::*, platform::MouseButton, AppContext, Entity, Subscription, View, ViewContext,
-    ViewHandle, WeakViewHandle,
+    Element, EventEmitter, IntoElement, ParentElement, Render, StyledText, Subscription,
+    ViewContext,
 };
 use itertools::Itertools;
-use search::ProjectSearchView;
+use theme::ActiveTheme;
+use ui::{prelude::*, ButtonLike, ButtonStyle, Label, Tooltip};
 use workspace::{
     item::{ItemEvent, ItemHandle},
-    ToolbarItemLocation, ToolbarItemView, Workspace,
+    ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
 };
 
-pub enum Event {
-    UpdateLocation,
-}
-
 pub struct Breadcrumbs {
     pane_focused: bool,
     active_item: Option<Box<dyn ItemHandle>>,
-    project_search: Option<ViewHandle<ProjectSearchView>>,
     subscription: Option<Subscription>,
-    workspace: WeakViewHandle<Workspace>,
 }
 
 impl Breadcrumbs {
-    pub fn new(workspace: &Workspace) -> Self {
+    pub fn new() -> Self {
         Self {
             pane_focused: false,
             active_item: Default::default(),
             subscription: Default::default(),
-            project_search: Default::default(),
-            workspace: workspace.weak_handle(),
         }
     }
 }
 
-impl Entity for Breadcrumbs {
-    type Event = Event;
-}
-
-impl View for Breadcrumbs {
-    fn ui_name() -> &'static str {
-        "Breadcrumbs"
-    }
+impl EventEmitter<ToolbarItemEvent> for Breadcrumbs {}
 
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        let active_item = match &self.active_item {
-            Some(active_item) => active_item,
-            None => return Empty::new().into_any(),
+impl Render for Breadcrumbs {
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+        let element = h_stack().text_ui();
+        let Some(active_item) = self.active_item.as_ref() else {
+            return element;
+        };
+        let Some(segments) = active_item.breadcrumbs(cx.theme(), cx) else {
+            return element;
         };
-        let not_editor = active_item.downcast::<editor::Editor>().is_none();
 
-        let theme = theme::current(cx).clone();
-        let style = &theme.workspace.toolbar.breadcrumbs;
+        let highlighted_segments = segments.into_iter().map(|segment| {
+            let mut text_style = cx.text_style();
+            text_style.color = Color::Muted.color(cx);
 
-        let breadcrumbs = match active_item.breadcrumbs(&theme, cx) {
-            Some(breadcrumbs) => breadcrumbs,
-            None => return Empty::new().into_any(),
-        }
-        .into_iter()
-        .map(|breadcrumb| {
-            Text::new(
-                breadcrumb.text,
-                theme.workspace.toolbar.breadcrumbs.default.text.clone(),
-            )
-            .with_highlights(breadcrumb.highlights.unwrap_or_default())
-            .into_any()
+            StyledText::new(segment.text)
+                .with_highlights(&text_style, segment.highlights.unwrap_or_default())
+                .into_any()
+        });
+        let breadcrumbs = Itertools::intersperse_with(highlighted_segments, || {
+            Label::new("β€Ί").color(Color::Muted).into_any_element()
         });
 
-        let crumbs = Flex::row()
-            .with_children(Itertools::intersperse_with(breadcrumbs, || {
-                Label::new(" β€Ί ", style.default.text.clone()).into_any()
-            }))
-            .constrained()
-            .with_height(theme.workspace.toolbar.breadcrumb_height)
-            .contained();
-
-        if not_editor || !self.pane_focused {
-            return crumbs
-                .with_style(style.default.container)
-                .aligned()
-                .left()
-                .into_any();
+        let breadcrumbs_stack = h_stack().gap_1().children(breadcrumbs);
+        match active_item
+            .downcast::<Editor>()
+            .map(|editor| editor.downgrade())
+        {
+            Some(editor) => element.child(
+                ButtonLike::new("toggle outline view")
+                    .child(breadcrumbs_stack)
+                    .style(ButtonStyle::Subtle)
+                    .on_click(move |_, cx| {
+                        if let Some(editor) = editor.upgrade() {
+                            outline::toggle(editor, &outline::Toggle, cx)
+                        }
+                    })
+                    .tooltip(|cx| Tooltip::for_action("Show symbol outline", &outline::Toggle, cx)),
+            ),
+            None => element.child(breadcrumbs_stack),
         }
-
-        MouseEventHandler::new::<Breadcrumbs, _>(0, cx, |state, _| {
-            let style = style.style_for(state);
-            crumbs.with_style(style.container)
-        })
-        .on_click(MouseButton::Left, |_, this, cx| {
-            if let Some(workspace) = this.workspace.upgrade(cx) {
-                workspace.update(cx, |workspace, cx| {
-                    outline::toggle(workspace, &Default::default(), cx)
-                })
-            }
-        })
-        .with_tooltip::<Breadcrumbs>(
-            0,
-            "Show symbol outline".to_owned(),
-            Some(Box::new(outline::Toggle)),
-            theme.tooltip.clone(),
-            cx,
-        )
-        .aligned()
-        .left()
-        .into_any()
     }
 }
 
@@ -114,19 +80,21 @@ impl ToolbarItemView for Breadcrumbs {
     ) -> ToolbarItemLocation {
         cx.notify();
         self.active_item = None;
-        self.project_search = None;
         if let Some(item) = active_pane_item {
-            let this = cx.weak_handle();
+            let this = cx.view().downgrade();
             self.subscription = Some(item.subscribe_to_item_events(
                 cx,
                 Box::new(move |event, cx| {
-                    if let Some(this) = this.upgrade(cx) {
-                        if let ItemEvent::UpdateBreadcrumbs = event {
-                            this.update(cx, |_, cx| {
-                                cx.emit(Event::UpdateLocation);
-                                cx.notify();
-                            });
-                        }
+                    if let ItemEvent::UpdateBreadcrumbs = event {
+                        this.update(cx, |this, cx| {
+                            cx.notify();
+                            if let Some(active_item) = this.active_item.as_ref() {
+                                cx.emit(ToolbarItemEvent::ChangeLocation(
+                                    active_item.breadcrumb_location(cx),
+                                ))
+                            }
+                        })
+                        .ok();
                     }
                 }),
             ));
@@ -137,19 +105,6 @@ impl ToolbarItemView for Breadcrumbs {
         }
     }
 
-    fn location_for_event(
-        &self,
-        _: &Event,
-        current_location: ToolbarItemLocation,
-        cx: &AppContext,
-    ) -> ToolbarItemLocation {
-        if let Some(active_item) = self.active_item.as_ref() {
-            active_item.breadcrumb_location(cx)
-        } else {
-            current_location
-        }
-    }
-
     fn pane_focus_update(&mut self, pane_focused: bool, _: &mut ViewContext<Self>) {
         self.pane_focused = pane_focused;
     }

crates/breadcrumbs2/Cargo.toml πŸ”—

@@ -1,28 +0,0 @@
-[package]
-name = "breadcrumbs2"
-version = "0.1.0"
-edition = "2021"
-publish = false
-
-[lib]
-path = "src/breadcrumbs.rs"
-doctest = false
-
-[dependencies]
-collections = { path = "../collections" }
-editor = { package = "editor2", path = "../editor2" }
-gpui = { package = "gpui2", path = "../gpui2" }
-ui = { package = "ui2", path = "../ui2" }
-language = { package = "language2", path = "../language2" }
-project = { package = "project2", path = "../project2" }
-search = { package = "search2", path = "../search2" }
-settings = { package = "settings2", path = "../settings2" }
-theme = { package = "theme2", path = "../theme2" }
-workspace = { package = "workspace2", path = "../workspace2" }
-outline = { package = "outline2", path = "../outline2" }
-itertools = "0.10"
-
-[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/breadcrumbs2/src/breadcrumbs.rs πŸ”—

@@ -1,111 +0,0 @@
-use editor::Editor;
-use gpui::{
-    Element, EventEmitter, IntoElement, ParentElement, Render, StyledText, Subscription,
-    ViewContext,
-};
-use itertools::Itertools;
-use theme::ActiveTheme;
-use ui::{prelude::*, ButtonLike, ButtonStyle, Label, Tooltip};
-use workspace::{
-    item::{ItemEvent, ItemHandle},
-    ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
-};
-
-pub struct Breadcrumbs {
-    pane_focused: bool,
-    active_item: Option<Box<dyn ItemHandle>>,
-    subscription: Option<Subscription>,
-}
-
-impl Breadcrumbs {
-    pub fn new() -> Self {
-        Self {
-            pane_focused: false,
-            active_item: Default::default(),
-            subscription: Default::default(),
-        }
-    }
-}
-
-impl EventEmitter<ToolbarItemEvent> for Breadcrumbs {}
-
-impl Render for Breadcrumbs {
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
-        let element = h_stack().text_ui();
-        let Some(active_item) = self.active_item.as_ref() else {
-            return element;
-        };
-        let Some(segments) = active_item.breadcrumbs(cx.theme(), cx) else {
-            return element;
-        };
-
-        let highlighted_segments = segments.into_iter().map(|segment| {
-            let mut text_style = cx.text_style();
-            text_style.color = Color::Muted.color(cx);
-
-            StyledText::new(segment.text)
-                .with_highlights(&text_style, segment.highlights.unwrap_or_default())
-                .into_any()
-        });
-        let breadcrumbs = Itertools::intersperse_with(highlighted_segments, || {
-            Label::new("β€Ί").color(Color::Muted).into_any_element()
-        });
-
-        let breadcrumbs_stack = h_stack().gap_1().children(breadcrumbs);
-        match active_item
-            .downcast::<Editor>()
-            .map(|editor| editor.downgrade())
-        {
-            Some(editor) => element.child(
-                ButtonLike::new("toggle outline view")
-                    .child(breadcrumbs_stack)
-                    .style(ButtonStyle::Subtle)
-                    .on_click(move |_, cx| {
-                        if let Some(editor) = editor.upgrade() {
-                            outline::toggle(editor, &outline::Toggle, cx)
-                        }
-                    })
-                    .tooltip(|cx| Tooltip::for_action("Show symbol outline", &outline::Toggle, cx)),
-            ),
-            None => element.child(breadcrumbs_stack),
-        }
-    }
-}
-
-impl ToolbarItemView for Breadcrumbs {
-    fn set_active_pane_item(
-        &mut self,
-        active_pane_item: Option<&dyn ItemHandle>,
-        cx: &mut ViewContext<Self>,
-    ) -> ToolbarItemLocation {
-        cx.notify();
-        self.active_item = None;
-        if let Some(item) = active_pane_item {
-            let this = cx.view().downgrade();
-            self.subscription = Some(item.subscribe_to_item_events(
-                cx,
-                Box::new(move |event, cx| {
-                    if let ItemEvent::UpdateBreadcrumbs = event {
-                        this.update(cx, |this, cx| {
-                            cx.notify();
-                            if let Some(active_item) = this.active_item.as_ref() {
-                                cx.emit(ToolbarItemEvent::ChangeLocation(
-                                    active_item.breadcrumb_location(cx),
-                                ))
-                            }
-                        })
-                        .ok();
-                    }
-                }),
-            ));
-            self.active_item = Some(item.boxed_clone());
-            item.breadcrumb_location(cx)
-        } else {
-            ToolbarItemLocation::Hidden
-        }
-    }
-
-    fn pane_focus_update(&mut self, pane_focused: bool, _: &mut ViewContext<Self>) {
-        self.pane_focused = pane_focused;
-    }
-}

crates/collab_ui/Cargo.toml πŸ”—

@@ -41,7 +41,7 @@ notifications = { package = "notifications2",  path = "../notifications2" }
 rich_text = { package = "rich_text2", path = "../rich_text2" }
 picker = { package = "picker2", path = "../picker2" }
 project = { package = "project2", path = "../project2" }
-recent_projects = { package = "recent_projects2", path = "../recent_projects2" }
+recent_projects = { path = "../recent_projects" }
 rpc = { package ="rpc2",  path = "../rpc2" }
 settings = { package = "settings2", path = "../settings2" }
 feature_flags = { package = "feature_flags2", path = "../feature_flags2"}

crates/copilot_button/Cargo.toml πŸ”—

@@ -9,19 +9,19 @@ path = "src/copilot_button.rs"
 doctest = false
 
 [dependencies]
-copilot = { path = "../copilot" }
-editor = { path = "../editor" }
-fs = { path = "../fs" }
-context_menu = { path = "../context_menu" }
-gpui = { path = "../gpui" }
-language = { path = "../language" }
-settings = { path = "../settings" }
-theme = { path = "../theme" }
+copilot = { package = "copilot2", path = "../copilot2" }
+editor = { package = "editor2", path = "../editor2" }
+fs = { package = "fs2", path = "../fs2" }
+zed-actions = { package="zed_actions2", path = "../zed_actions2"}
+gpui = { package = "gpui2", path = "../gpui2" }
+language = { package = "language2", path = "../language2" }
+settings = { package = "settings2", path = "../settings2" }
+theme = { package = "theme2", path = "../theme2" }
 util = { path = "../util" }
-workspace = { path = "../workspace" }
+workspace = { package = "workspace2", path = "../workspace2" }
 anyhow.workspace = true
 smol.workspace = true
 futures.workspace = true
 
 [dev-dependencies]
-editor = { path = "../editor", features = ["test-support"] }
+editor = { package = "editor2", path = "../editor2", features = ["test-support"] }

crates/copilot_button/src/copilot_button.rs πŸ”—

@@ -1,32 +1,31 @@
 use anyhow::Result;
-use context_menu::{ContextMenu, ContextMenuItem};
 use copilot::{Copilot, SignOut, Status};
 use editor::{scroll::autoscroll::Autoscroll, Editor};
 use fs::Fs;
 use gpui::{
-    elements::*,
-    platform::{CursorStyle, MouseButton},
-    AnyElement, AppContext, AsyncAppContext, Element, Entity, MouseState, Subscription, View,
-    ViewContext, ViewHandle, WeakViewHandle, WindowContext,
+    div, Action, AnchorCorner, AppContext, AsyncWindowContext, Entity, IntoElement, ParentElement,
+    Render, Subscription, View, ViewContext, WeakView, WindowContext,
 };
 use language::{
     language_settings::{self, all_language_settings, AllLanguageSettings},
     File, Language,
 };
-use settings::{update_settings_file, SettingsStore};
+use settings::{update_settings_file, Settings, SettingsStore};
 use std::{path::Path, sync::Arc};
 use util::{paths, ResultExt};
 use workspace::{
-    create_and_open_local_file, item::ItemHandle,
-    notifications::simple_message_notification::OsOpen, StatusItemView, Toast, Workspace,
+    create_and_open_local_file,
+    item::ItemHandle,
+    ui::{popover_menu, ButtonCommon, Clickable, ContextMenu, Icon, IconButton, IconSize, Tooltip},
+    StatusItemView, Toast, Workspace,
 };
+use zed_actions::OpenBrowser;
 
 const COPILOT_SETTINGS_URL: &str = "https://github.com/settings/copilot";
 const COPILOT_STARTING_TOAST_ID: usize = 1337;
 const COPILOT_ERROR_TOAST_ID: usize = 1338;
 
 pub struct CopilotButton {
-    popup_menu: ViewHandle<ContextMenu>,
     editor_subscription: Option<(Subscription, usize)>,
     editor_enabled: Option<bool>,
     language: Option<Arc<Language>>,
@@ -34,25 +33,15 @@ pub struct CopilotButton {
     fs: Arc<dyn Fs>,
 }
 
-impl Entity for CopilotButton {
-    type Event = ();
-}
-
-impl View for CopilotButton {
-    fn ui_name() -> &'static str {
-        "CopilotButton"
-    }
-
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+impl Render for CopilotButton {
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
         let all_language_settings = all_language_settings(None, cx);
         if !all_language_settings.copilot.feature_enabled {
-            return Empty::new().into_any();
+            return div();
         }
 
-        let theme = theme::current(cx).clone();
-        let active = self.popup_menu.read(cx).visible();
         let Some(copilot) = Copilot::global(cx) else {
-            return Empty::new().into_any();
+            return div();
         };
         let status = copilot.read(cx).status();
 
@@ -60,59 +49,26 @@ impl View for CopilotButton {
             .editor_enabled
             .unwrap_or_else(|| all_language_settings.copilot_enabled(None, None));
 
-        Stack::new()
-            .with_child(
-                MouseEventHandler::new::<Self, _>(0, cx, {
-                    let theme = theme.clone();
-                    let status = status.clone();
-                    move |state, _cx| {
-                        let style = theme
-                            .workspace
-                            .status_bar
-                            .panel_buttons
-                            .button
-                            .in_state(active)
-                            .style_for(state);
-
-                        Flex::row()
-                            .with_child(
-                                Svg::new({
-                                    match status {
-                                        Status::Error(_) => "icons/copilot_error.svg",
-                                        Status::Authorized => {
-                                            if enabled {
-                                                "icons/copilot.svg"
-                                            } else {
-                                                "icons/copilot_disabled.svg"
-                                            }
-                                        }
-                                        _ => "icons/copilot_init.svg",
-                                    }
-                                })
-                                .with_color(style.icon_color)
-                                .constrained()
-                                .with_width(style.icon_size)
-                                .aligned()
-                                .into_any_named("copilot-icon"),
-                            )
-                            .constrained()
-                            .with_height(style.icon_size)
-                            .contained()
-                            .with_style(style.container)
-                    }
-                })
-                .with_cursor_style(CursorStyle::PointingHand)
-                .on_down(MouseButton::Left, |_, this, cx| {
-                    this.popup_menu.update(cx, |menu, _| menu.delay_cancel());
-                })
-                .on_click(MouseButton::Left, {
-                    let status = status.clone();
-                    move |_, this, cx| match status {
-                        Status::Authorized => this.deploy_copilot_menu(cx),
-                        Status::Error(ref e) => {
-                            if let Some(workspace) = cx.root_view().clone().downcast::<Workspace>()
-                            {
-                                workspace.update(cx, |workspace, cx| {
+        let icon = match status {
+            Status::Error(_) => Icon::CopilotError,
+            Status::Authorized => {
+                if enabled {
+                    Icon::Copilot
+                } else {
+                    Icon::CopilotDisabled
+                }
+            }
+            _ => Icon::CopilotInit,
+        };
+
+        if let Status::Error(e) = status {
+            return div().child(
+                IconButton::new("copilot-error", icon)
+                    .icon_size(IconSize::Small)
+                    .on_click(cx.listener(move |_, _, cx| {
+                        if let Some(workspace) = cx.window_handle().downcast::<Workspace>() {
+                            workspace
+                                .update(cx, |workspace, cx| {
                                     workspace.show_toast(
                                         Toast::new(
                                             COPILOT_ERROR_TOAST_ID,
@@ -132,43 +88,40 @@ impl View for CopilotButton {
                                         ),
                                         cx,
                                     );
-                                });
-                            }
+                                })
+                                .ok();
                         }
-                        _ => this.deploy_copilot_start_menu(cx),
+                    }))
+                    .tooltip(|cx| Tooltip::text("GitHub Copilot", cx)),
+            );
+        }
+        let this = cx.view().clone();
+
+        div().child(
+            popover_menu("copilot")
+                .menu(move |cx| match status {
+                    Status::Authorized => {
+                        Some(this.update(cx, |this, cx| this.build_copilot_menu(cx)))
                     }
+                    _ => Some(this.update(cx, |this, cx| this.build_copilot_start_menu(cx))),
                 })
-                .with_tooltip::<Self>(
-                    0,
-                    "GitHub Copilot",
-                    None,
-                    theme.tooltip.clone(),
-                    cx,
+                .anchor(AnchorCorner::BottomRight)
+                .trigger(
+                    IconButton::new("copilot-icon", icon)
+                        .tooltip(|cx| Tooltip::text("GitHub Copilot", cx)),
                 ),
-            )
-            .with_child(ChildView::new(&self.popup_menu, cx).aligned().top().right())
-            .into_any()
+        )
     }
 }
 
 impl CopilotButton {
     pub fn new(fs: Arc<dyn Fs>, cx: &mut ViewContext<Self>) -> Self {
-        let button_view_id = cx.view_id();
-        let menu = cx.add_view(|cx| {
-            let mut menu = ContextMenu::new(button_view_id, cx);
-            menu.set_position_mode(OverlayPositionMode::Local);
-            menu
-        });
-
-        cx.observe(&menu, |_, _, cx| cx.notify()).detach();
-
         Copilot::global(cx).map(|copilot| cx.observe(&copilot, |_, _, cx| cx.notify()).detach());
 
-        cx.observe_global::<SettingsStore, _>(move |_, cx| cx.notify())
+        cx.observe_global::<SettingsStore>(move |_, cx| cx.notify())
             .detach();
 
         Self {
-            popup_menu: menu,
             editor_subscription: None,
             editor_enabled: None,
             language: None,
@@ -177,108 +130,91 @@ impl CopilotButton {
         }
     }
 
-    pub fn deploy_copilot_start_menu(&mut self, cx: &mut ViewContext<Self>) {
-        let mut menu_options = Vec::with_capacity(2);
+    pub fn build_copilot_start_menu(&mut self, cx: &mut ViewContext<Self>) -> View<ContextMenu> {
         let fs = self.fs.clone();
-
-        menu_options.push(ContextMenuItem::handler("Sign In", |cx| {
-            initiate_sign_in(cx)
-        }));
-        menu_options.push(ContextMenuItem::handler("Disable Copilot", move |cx| {
-            hide_copilot(fs.clone(), cx)
-        }));
-
-        self.popup_menu.update(cx, |menu, cx| {
-            menu.toggle(
-                Default::default(),
-                AnchorCorner::BottomRight,
-                menu_options,
-                cx,
-            );
-        });
+        ContextMenu::build(cx, |menu, _| {
+            menu.entry("Sign In", None, initiate_sign_in).entry(
+                "Disable Copilot",
+                None,
+                move |cx| hide_copilot(fs.clone(), cx),
+            )
+        })
     }
 
-    pub fn deploy_copilot_menu(&mut self, cx: &mut ViewContext<Self>) {
+    pub fn build_copilot_menu(&mut self, cx: &mut ViewContext<Self>) -> View<ContextMenu> {
         let fs = self.fs.clone();
-        let mut menu_options = Vec::with_capacity(8);
-
-        if let Some(language) = self.language.clone() {
-            let fs = fs.clone();
-            let language_enabled = language_settings::language_settings(Some(&language), None, cx)
-                .show_copilot_suggestions;
-            menu_options.push(ContextMenuItem::handler(
-                format!(
-                    "{} Suggestions for {}",
-                    if language_enabled { "Hide" } else { "Show" },
-                    language.name()
-                ),
-                move |cx| toggle_copilot_for_language(language.clone(), fs.clone(), cx),
-            ));
-        }
 
-        let settings = settings::get::<AllLanguageSettings>(cx);
+        return ContextMenu::build(cx, move |mut menu, cx| {
+            if let Some(language) = self.language.clone() {
+                let fs = fs.clone();
+                let language_enabled =
+                    language_settings::language_settings(Some(&language), None, cx)
+                        .show_copilot_suggestions;
+
+                menu = menu.entry(
+                    format!(
+                        "{} Suggestions for {}",
+                        if language_enabled { "Hide" } else { "Show" },
+                        language.name()
+                    ),
+                    None,
+                    move |cx| toggle_copilot_for_language(language.clone(), fs.clone(), cx),
+                );
+            }
 
-        if let Some(file) = &self.file {
-            let path = file.path().clone();
-            let path_enabled = settings.copilot_enabled_for_path(&path);
-            menu_options.push(ContextMenuItem::handler(
-                format!(
-                    "{} Suggestions for This Path",
-                    if path_enabled { "Hide" } else { "Show" }
-                ),
-                move |cx| {
-                    if let Some(workspace) = cx.root_view().clone().downcast::<Workspace>() {
-                        let workspace = workspace.downgrade();
-                        cx.spawn(|_, cx| {
-                            configure_disabled_globs(
-                                workspace,
-                                path_enabled.then_some(path.clone()),
-                                cx,
-                            )
-                        })
-                        .detach_and_log_err(cx);
-                    }
-                },
-            ));
-        }
+            let settings = AllLanguageSettings::get_global(cx);
 
-        let globally_enabled = settings.copilot_enabled(None, None);
-        menu_options.push(ContextMenuItem::handler(
-            if globally_enabled {
-                "Hide Suggestions for All Files"
-            } else {
-                "Show Suggestions for All Files"
-            },
-            move |cx| toggle_copilot_globally(fs.clone(), cx),
-        ));
-
-        menu_options.push(ContextMenuItem::Separator);
-
-        let icon_style = theme::current(cx).copilot.out_link_icon.clone();
-        menu_options.push(ContextMenuItem::action(
-            move |state: &mut MouseState, style: &theme::ContextMenuItem| {
-                Flex::row()
-                    .with_child(Label::new("Copilot Settings", style.label.clone()))
-                    .with_child(theme::ui::icon(icon_style.style_for(state)))
-                    .align_children_center()
-                    .into_any()
-            },
-            OsOpen::new(COPILOT_SETTINGS_URL),
-        ));
-
-        menu_options.push(ContextMenuItem::action("Sign Out", SignOut));
-
-        self.popup_menu.update(cx, |menu, cx| {
-            menu.toggle(
-                Default::default(),
-                AnchorCorner::BottomRight,
-                menu_options,
-                cx,
-            );
+            if let Some(file) = &self.file {
+                let path = file.path().clone();
+                let path_enabled = settings.copilot_enabled_for_path(&path);
+
+                menu = menu.entry(
+                    format!(
+                        "{} Suggestions for This Path",
+                        if path_enabled { "Hide" } else { "Show" }
+                    ),
+                    None,
+                    move |cx| {
+                        if let Some(workspace) = cx.window_handle().downcast::<Workspace>() {
+                            if let Ok(workspace) = workspace.root_view(cx) {
+                                let workspace = workspace.downgrade();
+                                cx.spawn(|cx| {
+                                    configure_disabled_globs(
+                                        workspace,
+                                        path_enabled.then_some(path.clone()),
+                                        cx,
+                                    )
+                                })
+                                .detach_and_log_err(cx);
+                            }
+                        }
+                    },
+                );
+            }
+
+            let globally_enabled = settings.copilot_enabled(None, None);
+            menu.entry(
+                if globally_enabled {
+                    "Hide Suggestions for All Files"
+                } else {
+                    "Show Suggestions for All Files"
+                },
+                None,
+                move |cx| toggle_copilot_globally(fs.clone(), cx),
+            )
+            .separator()
+            .link(
+                "Copilot Settings",
+                OpenBrowser {
+                    url: COPILOT_SETTINGS_URL.to_string(),
+                }
+                .boxed_clone(),
+            )
+            .action("Sign Out", SignOut.boxed_clone())
         });
     }
 
-    pub fn update_enabled(&mut self, editor: ViewHandle<Editor>, cx: &mut ViewContext<Self>) {
+    pub fn update_enabled(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
         let editor = editor.read(cx);
         let snapshot = editor.buffer().read(cx).snapshot(cx);
         let suggestion_anchor = editor.selections.newest_anchor().start;
@@ -299,8 +235,10 @@ impl CopilotButton {
 impl StatusItemView for CopilotButton {
     fn set_active_pane_item(&mut self, item: Option<&dyn ItemHandle>, cx: &mut ViewContext<Self>) {
         if let Some(editor) = item.map(|item| item.act_as::<Editor>(cx)).flatten() {
-            self.editor_subscription =
-                Some((cx.observe(&editor, Self::update_enabled), editor.id()));
+            self.editor_subscription = Some((
+                cx.observe(&editor, Self::update_enabled),
+                editor.entity_id().as_u64() as usize,
+            ));
             self.update_enabled(editor, cx);
         } else {
             self.language = None;
@@ -312,9 +250,9 @@ impl StatusItemView for CopilotButton {
 }
 
 async fn configure_disabled_globs(
-    workspace: WeakViewHandle<Workspace>,
+    workspace: WeakView<Workspace>,
     path_to_disable: Option<Arc<Path>>,
-    mut cx: AsyncAppContext,
+    mut cx: AsyncWindowContext,
 ) -> Result<()> {
     let settings_editor = workspace
         .update(&mut cx, |_, cx| {
@@ -396,20 +334,23 @@ fn initiate_sign_in(cx: &mut WindowContext) {
 
     match status {
         Status::Starting { task } => {
-            let Some(workspace) = cx.root_view().clone().downcast::<Workspace>() else {
+            let Some(workspace) = cx.window_handle().downcast::<Workspace>() else {
                 return;
             };
 
-            workspace.update(cx, |workspace, cx| {
+            let Ok(workspace) = workspace.update(cx, |workspace, cx| {
                 workspace.show_toast(
                     Toast::new(COPILOT_STARTING_TOAST_ID, "Copilot is starting..."),
                     cx,
-                )
-            });
-            let workspace = workspace.downgrade();
+                );
+                workspace.weak_handle()
+            }) else {
+                return;
+            };
+
             cx.spawn(|mut cx| async move {
                 task.await;
-                if let Some(copilot) = cx.read(Copilot::global) {
+                if let Some(copilot) = cx.update(|_, cx| Copilot::global(cx)).ok().flatten() {
                     workspace
                         .update(&mut cx, |workspace, cx| match copilot.read(cx).status() {
                             Status::Authorized => workspace.show_toast(

crates/copilot_button2/Cargo.toml πŸ”—

@@ -1,27 +0,0 @@
-[package]
-name = "copilot_button2"
-version = "0.1.0"
-edition = "2021"
-publish = false
-
-[lib]
-path = "src/copilot_button.rs"
-doctest = false
-
-[dependencies]
-copilot = { package = "copilot2", path = "../copilot2" }
-editor = { package = "editor2", path = "../editor2" }
-fs = { package = "fs2", path = "../fs2" }
-zed-actions = { package="zed_actions2", path = "../zed_actions2"}
-gpui = { package = "gpui2", path = "../gpui2" }
-language = { package = "language2", path = "../language2" }
-settings = { package = "settings2", path = "../settings2" }
-theme = { package = "theme2", path = "../theme2" }
-util = { path = "../util" }
-workspace = { package = "workspace2", path = "../workspace2" }
-anyhow.workspace = true
-smol.workspace = true
-futures.workspace = true
-
-[dev-dependencies]
-editor = { package = "editor2", path = "../editor2", features = ["test-support"] }

crates/copilot_button2/src/copilot_button.rs πŸ”—

@@ -1,378 +0,0 @@
-use anyhow::Result;
-use copilot::{Copilot, SignOut, Status};
-use editor::{scroll::autoscroll::Autoscroll, Editor};
-use fs::Fs;
-use gpui::{
-    div, Action, AnchorCorner, AppContext, AsyncWindowContext, Entity, IntoElement, ParentElement,
-    Render, Subscription, View, ViewContext, WeakView, WindowContext,
-};
-use language::{
-    language_settings::{self, all_language_settings, AllLanguageSettings},
-    File, Language,
-};
-use settings::{update_settings_file, Settings, SettingsStore};
-use std::{path::Path, sync::Arc};
-use util::{paths, ResultExt};
-use workspace::{
-    create_and_open_local_file,
-    item::ItemHandle,
-    ui::{popover_menu, ButtonCommon, Clickable, ContextMenu, Icon, IconButton, IconSize, Tooltip},
-    StatusItemView, Toast, Workspace,
-};
-use zed_actions::OpenBrowser;
-
-const COPILOT_SETTINGS_URL: &str = "https://github.com/settings/copilot";
-const COPILOT_STARTING_TOAST_ID: usize = 1337;
-const COPILOT_ERROR_TOAST_ID: usize = 1338;
-
-pub struct CopilotButton {
-    editor_subscription: Option<(Subscription, usize)>,
-    editor_enabled: Option<bool>,
-    language: Option<Arc<Language>>,
-    file: Option<Arc<dyn File>>,
-    fs: Arc<dyn Fs>,
-}
-
-impl Render for CopilotButton {
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
-        let all_language_settings = all_language_settings(None, cx);
-        if !all_language_settings.copilot.feature_enabled {
-            return div();
-        }
-
-        let Some(copilot) = Copilot::global(cx) else {
-            return div();
-        };
-        let status = copilot.read(cx).status();
-
-        let enabled = self
-            .editor_enabled
-            .unwrap_or_else(|| all_language_settings.copilot_enabled(None, None));
-
-        let icon = match status {
-            Status::Error(_) => Icon::CopilotError,
-            Status::Authorized => {
-                if enabled {
-                    Icon::Copilot
-                } else {
-                    Icon::CopilotDisabled
-                }
-            }
-            _ => Icon::CopilotInit,
-        };
-
-        if let Status::Error(e) = status {
-            return div().child(
-                IconButton::new("copilot-error", icon)
-                    .icon_size(IconSize::Small)
-                    .on_click(cx.listener(move |_, _, cx| {
-                        if let Some(workspace) = cx.window_handle().downcast::<Workspace>() {
-                            workspace
-                                .update(cx, |workspace, cx| {
-                                    workspace.show_toast(
-                                        Toast::new(
-                                            COPILOT_ERROR_TOAST_ID,
-                                            format!("Copilot can't be started: {}", e),
-                                        )
-                                        .on_click(
-                                            "Reinstall Copilot",
-                                            |cx| {
-                                                if let Some(copilot) = Copilot::global(cx) {
-                                                    copilot
-                                                        .update(cx, |copilot, cx| {
-                                                            copilot.reinstall(cx)
-                                                        })
-                                                        .detach();
-                                                }
-                                            },
-                                        ),
-                                        cx,
-                                    );
-                                })
-                                .ok();
-                        }
-                    }))
-                    .tooltip(|cx| Tooltip::text("GitHub Copilot", cx)),
-            );
-        }
-        let this = cx.view().clone();
-
-        div().child(
-            popover_menu("copilot")
-                .menu(move |cx| match status {
-                    Status::Authorized => {
-                        Some(this.update(cx, |this, cx| this.build_copilot_menu(cx)))
-                    }
-                    _ => Some(this.update(cx, |this, cx| this.build_copilot_start_menu(cx))),
-                })
-                .anchor(AnchorCorner::BottomRight)
-                .trigger(
-                    IconButton::new("copilot-icon", icon)
-                        .tooltip(|cx| Tooltip::text("GitHub Copilot", cx)),
-                ),
-        )
-    }
-}
-
-impl CopilotButton {
-    pub fn new(fs: Arc<dyn Fs>, cx: &mut ViewContext<Self>) -> Self {
-        Copilot::global(cx).map(|copilot| cx.observe(&copilot, |_, _, cx| cx.notify()).detach());
-
-        cx.observe_global::<SettingsStore>(move |_, cx| cx.notify())
-            .detach();
-
-        Self {
-            editor_subscription: None,
-            editor_enabled: None,
-            language: None,
-            file: None,
-            fs,
-        }
-    }
-
-    pub fn build_copilot_start_menu(&mut self, cx: &mut ViewContext<Self>) -> View<ContextMenu> {
-        let fs = self.fs.clone();
-        ContextMenu::build(cx, |menu, _| {
-            menu.entry("Sign In", None, initiate_sign_in).entry(
-                "Disable Copilot",
-                None,
-                move |cx| hide_copilot(fs.clone(), cx),
-            )
-        })
-    }
-
-    pub fn build_copilot_menu(&mut self, cx: &mut ViewContext<Self>) -> View<ContextMenu> {
-        let fs = self.fs.clone();
-
-        return ContextMenu::build(cx, move |mut menu, cx| {
-            if let Some(language) = self.language.clone() {
-                let fs = fs.clone();
-                let language_enabled =
-                    language_settings::language_settings(Some(&language), None, cx)
-                        .show_copilot_suggestions;
-
-                menu = menu.entry(
-                    format!(
-                        "{} Suggestions for {}",
-                        if language_enabled { "Hide" } else { "Show" },
-                        language.name()
-                    ),
-                    None,
-                    move |cx| toggle_copilot_for_language(language.clone(), fs.clone(), cx),
-                );
-            }
-
-            let settings = AllLanguageSettings::get_global(cx);
-
-            if let Some(file) = &self.file {
-                let path = file.path().clone();
-                let path_enabled = settings.copilot_enabled_for_path(&path);
-
-                menu = menu.entry(
-                    format!(
-                        "{} Suggestions for This Path",
-                        if path_enabled { "Hide" } else { "Show" }
-                    ),
-                    None,
-                    move |cx| {
-                        if let Some(workspace) = cx.window_handle().downcast::<Workspace>() {
-                            if let Ok(workspace) = workspace.root_view(cx) {
-                                let workspace = workspace.downgrade();
-                                cx.spawn(|cx| {
-                                    configure_disabled_globs(
-                                        workspace,
-                                        path_enabled.then_some(path.clone()),
-                                        cx,
-                                    )
-                                })
-                                .detach_and_log_err(cx);
-                            }
-                        }
-                    },
-                );
-            }
-
-            let globally_enabled = settings.copilot_enabled(None, None);
-            menu.entry(
-                if globally_enabled {
-                    "Hide Suggestions for All Files"
-                } else {
-                    "Show Suggestions for All Files"
-                },
-                None,
-                move |cx| toggle_copilot_globally(fs.clone(), cx),
-            )
-            .separator()
-            .link(
-                "Copilot Settings",
-                OpenBrowser {
-                    url: COPILOT_SETTINGS_URL.to_string(),
-                }
-                .boxed_clone(),
-            )
-            .action("Sign Out", SignOut.boxed_clone())
-        });
-    }
-
-    pub fn update_enabled(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
-        let editor = editor.read(cx);
-        let snapshot = editor.buffer().read(cx).snapshot(cx);
-        let suggestion_anchor = editor.selections.newest_anchor().start;
-        let language = snapshot.language_at(suggestion_anchor);
-        let file = snapshot.file_at(suggestion_anchor).cloned();
-
-        self.editor_enabled = Some(
-            all_language_settings(self.file.as_ref(), cx)
-                .copilot_enabled(language, file.as_ref().map(|file| file.path().as_ref())),
-        );
-        self.language = language.cloned();
-        self.file = file;
-
-        cx.notify()
-    }
-}
-
-impl StatusItemView for CopilotButton {
-    fn set_active_pane_item(&mut self, item: Option<&dyn ItemHandle>, cx: &mut ViewContext<Self>) {
-        if let Some(editor) = item.map(|item| item.act_as::<Editor>(cx)).flatten() {
-            self.editor_subscription = Some((
-                cx.observe(&editor, Self::update_enabled),
-                editor.entity_id().as_u64() as usize,
-            ));
-            self.update_enabled(editor, cx);
-        } else {
-            self.language = None;
-            self.editor_subscription = None;
-            self.editor_enabled = None;
-        }
-        cx.notify();
-    }
-}
-
-async fn configure_disabled_globs(
-    workspace: WeakView<Workspace>,
-    path_to_disable: Option<Arc<Path>>,
-    mut cx: AsyncWindowContext,
-) -> Result<()> {
-    let settings_editor = workspace
-        .update(&mut cx, |_, cx| {
-            create_and_open_local_file(&paths::SETTINGS, cx, || {
-                settings::initial_user_settings_content().as_ref().into()
-            })
-        })?
-        .await?
-        .downcast::<Editor>()
-        .unwrap();
-
-    settings_editor.downgrade().update(&mut cx, |item, cx| {
-        let text = item.buffer().read(cx).snapshot(cx).text();
-
-        let settings = cx.global::<SettingsStore>();
-        let edits = settings.edits_for_update::<AllLanguageSettings>(&text, |file| {
-            let copilot = file.copilot.get_or_insert_with(Default::default);
-            let globs = copilot.disabled_globs.get_or_insert_with(|| {
-                settings
-                    .get::<AllLanguageSettings>(None)
-                    .copilot
-                    .disabled_globs
-                    .iter()
-                    .map(|glob| glob.glob().to_string())
-                    .collect()
-            });
-
-            if let Some(path_to_disable) = &path_to_disable {
-                globs.push(path_to_disable.to_string_lossy().into_owned());
-            } else {
-                globs.clear();
-            }
-        });
-
-        if !edits.is_empty() {
-            item.change_selections(Some(Autoscroll::newest()), cx, |selections| {
-                selections.select_ranges(edits.iter().map(|e| e.0.clone()));
-            });
-
-            // When *enabling* a path, don't actually perform an edit, just select the range.
-            if path_to_disable.is_some() {
-                item.edit(edits.iter().cloned(), cx);
-            }
-        }
-    })?;
-
-    anyhow::Ok(())
-}
-
-fn toggle_copilot_globally(fs: Arc<dyn Fs>, cx: &mut AppContext) {
-    let show_copilot_suggestions = all_language_settings(None, cx).copilot_enabled(None, None);
-    update_settings_file::<AllLanguageSettings>(fs, cx, move |file| {
-        file.defaults.show_copilot_suggestions = Some((!show_copilot_suggestions).into())
-    });
-}
-
-fn toggle_copilot_for_language(language: Arc<Language>, fs: Arc<dyn Fs>, cx: &mut AppContext) {
-    let show_copilot_suggestions =
-        all_language_settings(None, cx).copilot_enabled(Some(&language), None);
-    update_settings_file::<AllLanguageSettings>(fs, cx, move |file| {
-        file.languages
-            .entry(language.name())
-            .or_default()
-            .show_copilot_suggestions = Some(!show_copilot_suggestions);
-    });
-}
-
-fn hide_copilot(fs: Arc<dyn Fs>, cx: &mut AppContext) {
-    update_settings_file::<AllLanguageSettings>(fs, cx, move |file| {
-        file.features.get_or_insert(Default::default()).copilot = Some(false);
-    });
-}
-
-fn initiate_sign_in(cx: &mut WindowContext) {
-    let Some(copilot) = Copilot::global(cx) else {
-        return;
-    };
-    let status = copilot.read(cx).status();
-
-    match status {
-        Status::Starting { task } => {
-            let Some(workspace) = cx.window_handle().downcast::<Workspace>() else {
-                return;
-            };
-
-            let Ok(workspace) = workspace.update(cx, |workspace, cx| {
-                workspace.show_toast(
-                    Toast::new(COPILOT_STARTING_TOAST_ID, "Copilot is starting..."),
-                    cx,
-                );
-                workspace.weak_handle()
-            }) else {
-                return;
-            };
-
-            cx.spawn(|mut cx| async move {
-                task.await;
-                if let Some(copilot) = cx.update(|_, cx| Copilot::global(cx)).ok().flatten() {
-                    workspace
-                        .update(&mut cx, |workspace, cx| match copilot.read(cx).status() {
-                            Status::Authorized => workspace.show_toast(
-                                Toast::new(COPILOT_STARTING_TOAST_ID, "Copilot has started!"),
-                                cx,
-                            ),
-                            _ => {
-                                workspace.dismiss_toast(COPILOT_STARTING_TOAST_ID, cx);
-                                copilot
-                                    .update(cx, |copilot, cx| copilot.sign_in(cx))
-                                    .detach_and_log_err(cx);
-                            }
-                        })
-                        .log_err();
-                }
-            })
-            .detach();
-        }
-        _ => {
-            copilot
-                .update(cx, |copilot, cx| copilot.sign_in(cx))
-                .detach_and_log_err(cx);
-        }
-    }
-}

crates/language_selector/Cargo.toml πŸ”—

@@ -9,17 +9,18 @@ path = "src/language_selector.rs"
 doctest = false
 
 [dependencies]
-editor = { path = "../editor" }
-fuzzy = { path = "../fuzzy" }
-language = { path = "../language" }
-gpui = { path = "../gpui" }
-picker = { path = "../picker" }
-project = { path = "../project" }
-theme = { path = "../theme" }
-settings = { path = "../settings" }
+editor = { package = "editor2", path = "../editor2" }
+fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
+language = { package = "language2", path = "../language2" }
+gpui = { package = "gpui2", path = "../gpui2" }
+picker = { package = "picker2", path = "../picker2" }
+project = { package = "project2", path = "../project2" }
+theme = { package = "theme2", path = "../theme2" }
+ui = { package = "ui2", path = "../ui2" }
+settings = { package = "settings2", path = "../settings2" }
 util = { path = "../util" }
-workspace = { path = "../workspace" }
+workspace = { package = "workspace2", path = "../workspace2" }
 anyhow.workspace = true
 
 [dev-dependencies]
-editor = { path = "../editor", features = ["test-support"] }
+editor = { package = "editor2", path = "../editor2", features = ["test-support"] }

crates/language_selector/src/active_buffer_language.rs πŸ”—

@@ -1,15 +1,14 @@
 use editor::Editor;
-use gpui::{
-    elements::*,
-    platform::{CursorStyle, MouseButton},
-    Entity, Subscription, View, ViewContext, ViewHandle, WeakViewHandle,
-};
+use gpui::{div, IntoElement, ParentElement, Render, Subscription, View, ViewContext, WeakView};
 use std::sync::Arc;
+use ui::{Button, ButtonCommon, Clickable, LabelSize, Tooltip};
 use workspace::{item::ItemHandle, StatusItemView, Workspace};
 
+use crate::LanguageSelector;
+
 pub struct ActiveBufferLanguage {
     active_language: Option<Option<Arc<str>>>,
-    workspace: WeakViewHandle<Workspace>,
+    workspace: WeakView<Workspace>,
     _observe_active_editor: Option<Subscription>,
 }
 
@@ -22,7 +21,7 @@ impl ActiveBufferLanguage {
         }
     }
 
-    fn update_language(&mut self, editor: ViewHandle<Editor>, cx: &mut ViewContext<Self>) {
+    fn update_language(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
         self.active_language = Some(None);
 
         let editor = editor.read(cx);
@@ -36,44 +35,28 @@ impl ActiveBufferLanguage {
     }
 }
 
-impl Entity for ActiveBufferLanguage {
-    type Event = ();
-}
-
-impl View for ActiveBufferLanguage {
-    fn ui_name() -> &'static str {
-        "ActiveBufferLanguage"
-    }
-
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-        if let Some(active_language) = self.active_language.as_ref() {
+impl Render for ActiveBufferLanguage {
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+        div().when_some(self.active_language.as_ref(), |el, active_language| {
             let active_language_text = if let Some(active_language_text) = active_language {
                 active_language_text.to_string()
             } else {
                 "Unknown".to_string()
             };
-            let theme = theme::current(cx).clone();
 
-            MouseEventHandler::new::<Self, _>(0, cx, |state, cx| {
-                let theme = &theme::current(cx).workspace.status_bar;
-                let style = theme.active_language.style_for(state);
-                Label::new(active_language_text, style.text.clone())
-                    .contained()
-                    .with_style(style.container)
-            })
-            .with_cursor_style(CursorStyle::PointingHand)
-            .on_click(MouseButton::Left, |_, this, cx| {
-                if let Some(workspace) = this.workspace.upgrade(cx) {
-                    workspace.update(cx, |workspace, cx| {
-                        crate::toggle(workspace, &Default::default(), cx)
-                    });
-                }
-            })
-            .with_tooltip::<Self>(0, "Select Language", None, theme.tooltip.clone(), cx)
-            .into_any()
-        } else {
-            Empty::new().into_any()
-        }
+            el.child(
+                Button::new("change-language", active_language_text)
+                    .label_size(LabelSize::Small)
+                    .on_click(cx.listener(|this, _, cx| {
+                        if let Some(workspace) = this.workspace.upgrade() {
+                            workspace.update(cx, |workspace, cx| {
+                                LanguageSelector::toggle(workspace, cx)
+                            });
+                        }
+                    }))
+                    .tooltip(|cx| Tooltip::text("Select Language", cx)),
+            )
+        })
     }
 }
 

crates/language_selector/src/language_selector.rs πŸ”—

@@ -4,46 +4,87 @@ pub use active_buffer_language::ActiveBufferLanguage;
 use anyhow::anyhow;
 use editor::Editor;
 use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
-use gpui::{actions, elements::*, AppContext, ModelHandle, MouseState, ViewContext};
+use gpui::{
+    actions, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model,
+    ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView,
+};
 use language::{Buffer, LanguageRegistry};
-use picker::{Picker, PickerDelegate, PickerEvent};
+use picker::{Picker, PickerDelegate};
 use project::Project;
 use std::sync::Arc;
+use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing};
 use util::ResultExt;
-use workspace::Workspace;
+use workspace::{ModalView, Workspace};
 
 actions!(language_selector, [Toggle]);
 
 pub fn init(cx: &mut AppContext) {
-    Picker::<LanguageSelectorDelegate>::init(cx);
-    cx.add_action(toggle);
+    cx.observe_new_views(LanguageSelector::register).detach();
 }
 
-pub fn toggle(
-    workspace: &mut Workspace,
-    _: &Toggle,
-    cx: &mut ViewContext<Workspace>,
-) -> Option<()> {
-    let (_, buffer, _) = workspace
-        .active_item(cx)?
-        .act_as::<Editor>(cx)?
-        .read(cx)
-        .active_excerpt(cx)?;
-    workspace.toggle_modal(cx, |workspace, cx| {
+pub struct LanguageSelector {
+    picker: View<Picker<LanguageSelectorDelegate>>,
+}
+
+impl LanguageSelector {
+    fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
+        workspace.register_action(move |workspace, _: &Toggle, cx| {
+            Self::toggle(workspace, cx);
+        });
+    }
+
+    fn toggle(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> Option<()> {
         let registry = workspace.app_state().languages.clone();
-        cx.add_view(|cx| {
-            Picker::new(
-                LanguageSelectorDelegate::new(buffer, workspace.project().clone(), registry),
-                cx,
-            )
-        })
-    });
-    Some(())
+        let (_, buffer, _) = workspace
+            .active_item(cx)?
+            .act_as::<Editor>(cx)?
+            .read(cx)
+            .active_excerpt(cx)?;
+        let project = workspace.project().clone();
+
+        workspace.toggle_modal(cx, move |cx| {
+            LanguageSelector::new(buffer, project, registry, cx)
+        });
+        Some(())
+    }
+
+    fn new(
+        buffer: Model<Buffer>,
+        project: Model<Project>,
+        language_registry: Arc<LanguageRegistry>,
+        cx: &mut ViewContext<Self>,
+    ) -> Self {
+        let delegate = LanguageSelectorDelegate::new(
+            cx.view().downgrade(),
+            buffer,
+            project,
+            language_registry,
+        );
+
+        let picker = cx.new_view(|cx| Picker::new(delegate, cx));
+        Self { picker }
+    }
 }
 
+impl Render for LanguageSelector {
+    fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
+        v_stack().w(rems(34.)).child(self.picker.clone())
+    }
+}
+
+impl FocusableView for LanguageSelector {
+    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
+        self.picker.focus_handle(cx)
+    }
+}
+
+impl EventEmitter<DismissEvent> for LanguageSelector {}
+impl ModalView for LanguageSelector {}
+
 pub struct LanguageSelectorDelegate {
-    buffer: ModelHandle<Buffer>,
-    project: ModelHandle<Project>,
+    language_selector: WeakView<LanguageSelector>,
+    buffer: Model<Buffer>,
+    project: Model<Project>,
     language_registry: Arc<LanguageRegistry>,
     candidates: Vec<StringMatchCandidate>,
     matches: Vec<StringMatch>,
@@ -52,8 +93,9 @@ pub struct LanguageSelectorDelegate {
 
 impl LanguageSelectorDelegate {
     fn new(
-        buffer: ModelHandle<Buffer>,
-        project: ModelHandle<Project>,
+        language_selector: WeakView<LanguageSelector>,
+        buffer: Model<Buffer>,
+        project: Model<Project>,
         language_registry: Arc<LanguageRegistry>,
     ) -> Self {
         let candidates = language_registry
@@ -62,29 +104,22 @@ impl LanguageSelectorDelegate {
             .enumerate()
             .map(|(candidate_id, name)| StringMatchCandidate::new(candidate_id, name))
             .collect::<Vec<_>>();
-        let mut matches = candidates
-            .iter()
-            .map(|candidate| StringMatch {
-                candidate_id: candidate.id,
-                score: 0.,
-                positions: Default::default(),
-                string: candidate.string.clone(),
-            })
-            .collect::<Vec<_>>();
-        matches.sort_unstable_by(|mat1, mat2| mat1.string.cmp(&mat2.string));
 
         Self {
+            language_selector,
             buffer,
             project,
             language_registry,
             candidates,
-            matches,
+            matches: vec![],
             selected_index: 0,
         }
     }
 }
 
 impl PickerDelegate for LanguageSelectorDelegate {
+    type ListItem = ListItem;
+
     fn placeholder_text(&self) -> Arc<str> {
         "Select a language...".into()
     }
@@ -102,23 +137,25 @@ impl PickerDelegate for LanguageSelectorDelegate {
             cx.spawn(|_, mut cx| async move {
                 let language = language.await?;
                 let project = project
-                    .upgrade(&cx)
+                    .upgrade()
                     .ok_or_else(|| anyhow!("project was dropped"))?;
                 let buffer = buffer
-                    .upgrade(&cx)
+                    .upgrade()
                     .ok_or_else(|| anyhow!("buffer was dropped"))?;
                 project.update(&mut cx, |project, cx| {
                     project.set_language_for_buffer(&buffer, language, cx);
-                });
-                anyhow::Ok(())
+                })
             })
             .detach_and_log_err(cx);
         }
-
-        cx.emit(PickerEvent::Dismiss);
+        self.dismissed(cx);
     }
 
-    fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {}
+    fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
+        self.language_selector
+            .update(cx, |_, cx| cx.emit(DismissEvent))
+            .log_err();
+    }
 
     fn selected_index(&self) -> usize {
         self.selected_index
@@ -133,7 +170,7 @@ impl PickerDelegate for LanguageSelectorDelegate {
         query: String,
         cx: &mut ViewContext<Picker<Self>>,
     ) -> gpui::Task<()> {
-        let background = cx.background().clone();
+        let background = cx.background_executor().clone();
         let candidates = self.candidates.clone();
         cx.spawn(|this, mut cx| async move {
             let matches = if query.is_empty() {
@@ -160,7 +197,7 @@ impl PickerDelegate for LanguageSelectorDelegate {
             };
 
             this.update(&mut cx, |this, cx| {
-                let delegate = this.delegate_mut();
+                let delegate = &mut this.delegate;
                 delegate.matches = matches;
                 delegate.selected_index = delegate
                     .selected_index
@@ -174,23 +211,22 @@ impl PickerDelegate for LanguageSelectorDelegate {
     fn render_match(
         &self,
         ix: usize,
-        mouse_state: &mut MouseState,
         selected: bool,
-        cx: &AppContext,
-    ) -> AnyElement<Picker<Self>> {
-        let theme = theme::current(cx);
+        cx: &mut ViewContext<Picker<Self>>,
+    ) -> Option<Self::ListItem> {
         let mat = &self.matches[ix];
-        let style = theme.picker.item.in_state(selected).style_for(mouse_state);
         let buffer_language_name = self.buffer.read(cx).language().map(|l| l.name());
         let mut label = mat.string.clone();
         if buffer_language_name.as_deref() == Some(mat.string.as_str()) {
             label.push_str(" (current)");
         }
 
-        Label::new(label, style.label.clone())
-            .with_highlights(mat.positions.clone())
-            .contained()
-            .with_style(style.container)
-            .into_any()
+        Some(
+            ListItem::new(ix)
+                .inset(true)
+                .spacing(ListItemSpacing::Sparse)
+                .selected(selected)
+                .child(HighlightedLabel::new(label, mat.positions.clone())),
+        )
     }
 }

crates/language_selector2/Cargo.toml πŸ”—

@@ -1,26 +0,0 @@
-[package]
-name = "language_selector2"
-version = "0.1.0"
-edition = "2021"
-publish = false
-
-[lib]
-path = "src/language_selector.rs"
-doctest = false
-
-[dependencies]
-editor = { package = "editor2", path = "../editor2" }
-fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
-language = { package = "language2", path = "../language2" }
-gpui = { package = "gpui2", path = "../gpui2" }
-picker = { package = "picker2", path = "../picker2" }
-project = { package = "project2", path = "../project2" }
-theme = { package = "theme2", path = "../theme2" }
-ui = { package = "ui2", path = "../ui2" }
-settings = { package = "settings2", path = "../settings2" }
-util = { path = "../util" }
-workspace = { package = "workspace2", path = "../workspace2" }
-anyhow.workspace = true
-
-[dev-dependencies]
-editor = { package = "editor2", path = "../editor2", features = ["test-support"] }

crates/language_selector2/src/active_buffer_language.rs πŸ”—

@@ -1,79 +0,0 @@
-use editor::Editor;
-use gpui::{div, IntoElement, ParentElement, Render, Subscription, View, ViewContext, WeakView};
-use std::sync::Arc;
-use ui::{Button, ButtonCommon, Clickable, LabelSize, Tooltip};
-use workspace::{item::ItemHandle, StatusItemView, Workspace};
-
-use crate::LanguageSelector;
-
-pub struct ActiveBufferLanguage {
-    active_language: Option<Option<Arc<str>>>,
-    workspace: WeakView<Workspace>,
-    _observe_active_editor: Option<Subscription>,
-}
-
-impl ActiveBufferLanguage {
-    pub fn new(workspace: &Workspace) -> Self {
-        Self {
-            active_language: None,
-            workspace: workspace.weak_handle(),
-            _observe_active_editor: None,
-        }
-    }
-
-    fn update_language(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
-        self.active_language = Some(None);
-
-        let editor = editor.read(cx);
-        if let Some((_, buffer, _)) = editor.active_excerpt(cx) {
-            if let Some(language) = buffer.read(cx).language() {
-                self.active_language = Some(Some(language.name()));
-            }
-        }
-
-        cx.notify();
-    }
-}
-
-impl Render for ActiveBufferLanguage {
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
-        div().when_some(self.active_language.as_ref(), |el, active_language| {
-            let active_language_text = if let Some(active_language_text) = active_language {
-                active_language_text.to_string()
-            } else {
-                "Unknown".to_string()
-            };
-
-            el.child(
-                Button::new("change-language", active_language_text)
-                    .label_size(LabelSize::Small)
-                    .on_click(cx.listener(|this, _, cx| {
-                        if let Some(workspace) = this.workspace.upgrade() {
-                            workspace.update(cx, |workspace, cx| {
-                                LanguageSelector::toggle(workspace, cx)
-                            });
-                        }
-                    }))
-                    .tooltip(|cx| Tooltip::text("Select Language", cx)),
-            )
-        })
-    }
-}
-
-impl StatusItemView for ActiveBufferLanguage {
-    fn set_active_pane_item(
-        &mut self,
-        active_pane_item: Option<&dyn ItemHandle>,
-        cx: &mut ViewContext<Self>,
-    ) {
-        if let Some(editor) = active_pane_item.and_then(|item| item.act_as::<Editor>(cx)) {
-            self._observe_active_editor = Some(cx.observe(&editor, Self::update_language));
-            self.update_language(editor, cx);
-        } else {
-            self.active_language = None;
-            self._observe_active_editor = None;
-        }
-
-        cx.notify();
-    }
-}

crates/language_selector2/src/language_selector.rs πŸ”—

@@ -1,232 +0,0 @@
-mod active_buffer_language;
-
-pub use active_buffer_language::ActiveBufferLanguage;
-use anyhow::anyhow;
-use editor::Editor;
-use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
-use gpui::{
-    actions, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model,
-    ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView,
-};
-use language::{Buffer, LanguageRegistry};
-use picker::{Picker, PickerDelegate};
-use project::Project;
-use std::sync::Arc;
-use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing};
-use util::ResultExt;
-use workspace::{ModalView, Workspace};
-
-actions!(language_selector, [Toggle]);
-
-pub fn init(cx: &mut AppContext) {
-    cx.observe_new_views(LanguageSelector::register).detach();
-}
-
-pub struct LanguageSelector {
-    picker: View<Picker<LanguageSelectorDelegate>>,
-}
-
-impl LanguageSelector {
-    fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
-        workspace.register_action(move |workspace, _: &Toggle, cx| {
-            Self::toggle(workspace, cx);
-        });
-    }
-
-    fn toggle(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> Option<()> {
-        let registry = workspace.app_state().languages.clone();
-        let (_, buffer, _) = workspace
-            .active_item(cx)?
-            .act_as::<Editor>(cx)?
-            .read(cx)
-            .active_excerpt(cx)?;
-        let project = workspace.project().clone();
-
-        workspace.toggle_modal(cx, move |cx| {
-            LanguageSelector::new(buffer, project, registry, cx)
-        });
-        Some(())
-    }
-
-    fn new(
-        buffer: Model<Buffer>,
-        project: Model<Project>,
-        language_registry: Arc<LanguageRegistry>,
-        cx: &mut ViewContext<Self>,
-    ) -> Self {
-        let delegate = LanguageSelectorDelegate::new(
-            cx.view().downgrade(),
-            buffer,
-            project,
-            language_registry,
-        );
-
-        let picker = cx.new_view(|cx| Picker::new(delegate, cx));
-        Self { picker }
-    }
-}
-
-impl Render for LanguageSelector {
-    fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
-        v_stack().w(rems(34.)).child(self.picker.clone())
-    }
-}
-
-impl FocusableView for LanguageSelector {
-    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
-        self.picker.focus_handle(cx)
-    }
-}
-
-impl EventEmitter<DismissEvent> for LanguageSelector {}
-impl ModalView for LanguageSelector {}
-
-pub struct LanguageSelectorDelegate {
-    language_selector: WeakView<LanguageSelector>,
-    buffer: Model<Buffer>,
-    project: Model<Project>,
-    language_registry: Arc<LanguageRegistry>,
-    candidates: Vec<StringMatchCandidate>,
-    matches: Vec<StringMatch>,
-    selected_index: usize,
-}
-
-impl LanguageSelectorDelegate {
-    fn new(
-        language_selector: WeakView<LanguageSelector>,
-        buffer: Model<Buffer>,
-        project: Model<Project>,
-        language_registry: Arc<LanguageRegistry>,
-    ) -> Self {
-        let candidates = language_registry
-            .language_names()
-            .into_iter()
-            .enumerate()
-            .map(|(candidate_id, name)| StringMatchCandidate::new(candidate_id, name))
-            .collect::<Vec<_>>();
-
-        Self {
-            language_selector,
-            buffer,
-            project,
-            language_registry,
-            candidates,
-            matches: vec![],
-            selected_index: 0,
-        }
-    }
-}
-
-impl PickerDelegate for LanguageSelectorDelegate {
-    type ListItem = ListItem;
-
-    fn placeholder_text(&self) -> Arc<str> {
-        "Select a language...".into()
-    }
-
-    fn match_count(&self) -> usize {
-        self.matches.len()
-    }
-
-    fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
-        if let Some(mat) = self.matches.get(self.selected_index) {
-            let language_name = &self.candidates[mat.candidate_id].string;
-            let language = self.language_registry.language_for_name(language_name);
-            let project = self.project.downgrade();
-            let buffer = self.buffer.downgrade();
-            cx.spawn(|_, mut cx| async move {
-                let language = language.await?;
-                let project = project
-                    .upgrade()
-                    .ok_or_else(|| anyhow!("project was dropped"))?;
-                let buffer = buffer
-                    .upgrade()
-                    .ok_or_else(|| anyhow!("buffer was dropped"))?;
-                project.update(&mut cx, |project, cx| {
-                    project.set_language_for_buffer(&buffer, language, cx);
-                })
-            })
-            .detach_and_log_err(cx);
-        }
-        self.dismissed(cx);
-    }
-
-    fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
-        self.language_selector
-            .update(cx, |_, cx| cx.emit(DismissEvent))
-            .log_err();
-    }
-
-    fn selected_index(&self) -> usize {
-        self.selected_index
-    }
-
-    fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
-        self.selected_index = ix;
-    }
-
-    fn update_matches(
-        &mut self,
-        query: String,
-        cx: &mut ViewContext<Picker<Self>>,
-    ) -> gpui::Task<()> {
-        let background = cx.background_executor().clone();
-        let candidates = self.candidates.clone();
-        cx.spawn(|this, mut cx| async move {
-            let matches = if query.is_empty() {
-                candidates
-                    .into_iter()
-                    .enumerate()
-                    .map(|(index, candidate)| StringMatch {
-                        candidate_id: index,
-                        string: candidate.string,
-                        positions: Vec::new(),
-                        score: 0.0,
-                    })
-                    .collect()
-            } else {
-                match_strings(
-                    &candidates,
-                    &query,
-                    false,
-                    100,
-                    &Default::default(),
-                    background,
-                )
-                .await
-            };
-
-            this.update(&mut cx, |this, cx| {
-                let delegate = &mut this.delegate;
-                delegate.matches = matches;
-                delegate.selected_index = delegate
-                    .selected_index
-                    .min(delegate.matches.len().saturating_sub(1));
-                cx.notify();
-            })
-            .log_err();
-        })
-    }
-
-    fn render_match(
-        &self,
-        ix: usize,
-        selected: bool,
-        cx: &mut ViewContext<Picker<Self>>,
-    ) -> Option<Self::ListItem> {
-        let mat = &self.matches[ix];
-        let buffer_language_name = self.buffer.read(cx).language().map(|l| l.name());
-        let mut label = mat.string.clone();
-        if buffer_language_name.as_deref() == Some(mat.string.as_str()) {
-            label.push_str(" (current)");
-        }
-
-        Some(
-            ListItem::new(ix)
-                .inset(true)
-                .spacing(ListItemSpacing::Sparse)
-                .selected(selected)
-                .child(HighlightedLabel::new(label, mat.positions.clone())),
-        )
-    }
-}

crates/project_panel/Cargo.toml πŸ”—

@@ -9,32 +9,33 @@ path = "src/project_panel.rs"
 doctest = false
 
 [dependencies]
-context_menu = { path = "../context_menu" }
 collections = { path = "../collections" }
-db = { path = "../db" }
-drag_and_drop = { path = "../drag_and_drop" }
-editor = { path = "../editor" }
-gpui = { path = "../gpui" }
-menu = { path = "../menu" }
-project = { path = "../project" }
-settings = { path = "../settings" }
-theme = { path = "../theme" }
+db = { path = "../db2", package = "db2" }
+editor = { path = "../editor2", package = "editor2" }
+gpui = { path = "../gpui2", package = "gpui2" }
+menu = { path = "../menu2", package = "menu2" }
+project = { path = "../project2", package = "project2" }
+search = { package = "search2", path = "../search2" }
+settings = { path = "../settings2", package = "settings2" }
+theme = { path = "../theme2", package = "theme2" }
+ui = { path = "../ui2", package = "ui2" }
 util = { path = "../util" }
-workspace = { path = "../workspace" }
+workspace = { path = "../workspace2", package = "workspace2" }
+anyhow.workspace = true
 postage.workspace = true
 futures.workspace = true
 serde.workspace = true
 serde_derive.workspace = true
 serde_json.workspace = true
-anyhow.workspace = true
 schemars.workspace = true
+smallvec.workspace = true
 pretty_assertions.workspace = true
 unicase = "2.6"
 
 [dev-dependencies]
-client = { path = "../client", features = ["test-support"] }
-language = { path = "../language", features = ["test-support"] }
-editor = { path = "../editor", features = ["test-support"] }
-gpui = { path = "../gpui", features = ["test-support"] }
-workspace = { path = "../workspace", features = ["test-support"] }
+client = { path = "../client2", package = "client2", features = ["test-support"] }
+language = { path = "../language2", package = "language2", features = ["test-support"] }
+editor = { path = "../editor2", package = "editor2", features = ["test-support"] }
+gpui = { path = "../gpui2", package = "gpui2", features = ["test-support"] }
+workspace = { path = "../workspace2", package = "workspace2", features = ["test-support"] }
 serde_json.workspace = true

crates/project_panel/src/file_associations.rs πŸ”—

@@ -41,56 +41,47 @@ impl FileAssociations {
             })
     }
 
-    pub fn get_icon(path: &Path, cx: &AppContext) -> Arc<str> {
-        maybe!({
-            let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
+    pub fn get_icon(path: &Path, cx: &AppContext) -> Option<Arc<str>> {
+        let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
 
-            // FIXME: Associate a type with the languages and have the file's langauge
-            //        override these associations
-            maybe!({
-                let suffix = path.icon_suffix()?;
+        // FIXME: Associate a type with the languages and have the file's langauge
+        //        override these associations
+        maybe!({
+            let suffix = path.icon_suffix()?;
 
-                this.suffixes
-                    .get(suffix)
-                    .and_then(|type_str| this.types.get(type_str))
-                    .map(|type_config| type_config.icon.clone())
-            })
-            .or_else(|| this.types.get("default").map(|config| config.icon.clone()))
+            this.suffixes
+                .get(suffix)
+                .and_then(|type_str| this.types.get(type_str))
+                .map(|type_config| type_config.icon.clone())
         })
-        .unwrap_or_else(|| Arc::from("".to_string()))
+        .or_else(|| this.types.get("default").map(|config| config.icon.clone()))
     }
 
-    pub fn get_folder_icon(expanded: bool, cx: &AppContext) -> Arc<str> {
-        maybe!({
-            let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
+    pub fn get_folder_icon(expanded: bool, cx: &AppContext) -> Option<Arc<str>> {
+        let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
 
-            let key = if expanded {
-                EXPANDED_DIRECTORY_TYPE
-            } else {
-                COLLAPSED_DIRECTORY_TYPE
-            };
+        let key = if expanded {
+            EXPANDED_DIRECTORY_TYPE
+        } else {
+            COLLAPSED_DIRECTORY_TYPE
+        };
 
-            this.types
-                .get(key)
-                .map(|type_config| type_config.icon.clone())
-        })
-        .unwrap_or_else(|| Arc::from("".to_string()))
+        this.types
+            .get(key)
+            .map(|type_config| type_config.icon.clone())
     }
 
-    pub fn get_chevron_icon(expanded: bool, cx: &AppContext) -> Arc<str> {
-        maybe!({
-            let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
+    pub fn get_chevron_icon(expanded: bool, cx: &AppContext) -> Option<Arc<str>> {
+        let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
 
-            let key = if expanded {
-                EXPANDED_CHEVRON_TYPE
-            } else {
-                COLLAPSED_CHEVRON_TYPE
-            };
+        let key = if expanded {
+            EXPANDED_CHEVRON_TYPE
+        } else {
+            COLLAPSED_CHEVRON_TYPE
+        };
 
-            this.types
-                .get(key)
-                .map(|type_config| type_config.icon.clone())
-        })
-        .unwrap_or_else(|| Arc::from("".to_string()))
+        this.types
+            .get(key)
+            .map(|type_config| type_config.icon.clone())
     }
 }

crates/project_panel/src/project_panel.rs πŸ”—

@@ -1,25 +1,18 @@
 pub mod file_associations;
 mod project_panel_settings;
+use settings::{Settings, SettingsStore};
 
-use context_menu::{ContextMenu, ContextMenuItem};
 use db::kvp::KEY_VALUE_STORE;
-use drag_and_drop::{DragAndDrop, Draggable};
 use editor::{scroll::autoscroll::Autoscroll, Cancel, Editor};
 use file_associations::FileAssociations;
 
-use futures::stream::StreamExt;
+use anyhow::{anyhow, Result};
 use gpui::{
-    actions,
-    anyhow::{self, anyhow, Result},
-    elements::{
-        AnchorCorner, ChildView, ContainerStyle, Empty, Flex, Label, MouseEventHandler,
-        ParentElement, ScrollTarget, Stack, Svg, UniformList, UniformListState,
-    },
-    geometry::vector::Vector2F,
-    keymap_matcher::KeymapContext,
-    platform::{CursorStyle, MouseButton, PromptLevel},
-    Action, AnyElement, AppContext, AssetSource, AsyncAppContext, ClipboardItem, Element, Entity,
-    ModelHandle, Task, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
+    actions, div, overlay, px, uniform_list, Action, AppContext, AssetSource, AsyncWindowContext,
+    ClipboardItem, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView, InteractiveElement,
+    KeyContext, Model, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel,
+    Render, Stateful, Styled, Subscription, Task, UniformListScrollHandle, View, ViewContext,
+    VisualContext as _, WeakView, WindowContext,
 };
 use menu::{Confirm, SelectNext, SelectPrev};
 use project::{
@@ -28,7 +21,6 @@ use project::{
 };
 use project_panel_settings::{ProjectPanelDockPosition, ProjectPanelSettings};
 use serde::{Deserialize, Serialize};
-use settings::SettingsStore;
 use std::{
     cmp::Ordering,
     collections::{hash_map, HashMap},
@@ -37,11 +29,12 @@ use std::{
     path::Path,
     sync::Arc,
 };
-use theme::ProjectPanelEntry;
+use theme::ThemeSettings;
+use ui::{prelude::*, v_stack, ContextMenu, IconElement, Label, ListItem};
 use unicase::UniCase;
-use util::{ResultExt, TryFutureExt};
+use util::{maybe, ResultExt, TryFutureExt};
 use workspace::{
-    dock::{DockPosition, Panel},
+    dock::{DockPosition, Panel, PanelEvent},
     Workspace,
 };
 
@@ -49,21 +42,21 @@ const PROJECT_PANEL_KEY: &'static str = "ProjectPanel";
 const NEW_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX;
 
 pub struct ProjectPanel {
-    project: ModelHandle<Project>,
+    project: Model<Project>,
     fs: Arc<dyn Fs>,
-    list: UniformListState,
+    list: UniformListScrollHandle,
+    focus_handle: FocusHandle,
     visible_entries: Vec<(WorktreeId, Vec<Entry>)>,
     last_worktree_root_id: Option<ProjectEntryId>,
     expanded_dir_ids: HashMap<WorktreeId, Vec<ProjectEntryId>>,
     selection: Option<Selection>,
+    context_menu: Option<(View<ContextMenu>, Point<Pixels>, Subscription)>,
     edit_state: Option<EditState>,
-    filename_editor: ViewHandle<Editor>,
+    filename_editor: View<Editor>,
     clipboard_entry: Option<ClipboardEntry>,
-    context_menu: ViewHandle<ContextMenu>,
-    dragged_entry_destination: Option<Arc<Path>>,
-    workspace: WeakViewHandle<Workspace>,
-    has_focus: bool,
-    width: Option<f32>,
+    _dragged_entry_destination: Option<Arc<Path>>,
+    workspace: WeakView<Workspace>,
+    width: Option<Pixels>,
     pending_serialization: Task<Option<()>>,
 }
 
@@ -94,7 +87,7 @@ pub enum ClipboardEntry {
     },
 }
 
-#[derive(Debug, PartialEq, Eq)]
+#[derive(Debug, PartialEq, Eq, Clone)]
 pub struct EntryDetails {
     filename: String,
     icon: Option<Arc<str>>,
@@ -134,36 +127,19 @@ actions!(
 );
 
 pub fn init_settings(cx: &mut AppContext) {
-    settings::register::<ProjectPanelSettings>(cx);
+    ProjectPanelSettings::register(cx);
 }
 
 pub fn init(assets: impl AssetSource, cx: &mut AppContext) {
     init_settings(cx);
     file_associations::init(assets, cx);
-    cx.add_action(ProjectPanel::expand_selected_entry);
-    cx.add_action(ProjectPanel::collapse_selected_entry);
-    cx.add_action(ProjectPanel::collapse_all_entries);
-    cx.add_action(ProjectPanel::select_prev);
-    cx.add_action(ProjectPanel::select_next);
-    cx.add_action(ProjectPanel::new_file);
-    cx.add_action(ProjectPanel::new_directory);
-    cx.add_action(ProjectPanel::rename);
-    cx.add_async_action(ProjectPanel::delete);
-    cx.add_async_action(ProjectPanel::confirm);
-    cx.add_async_action(ProjectPanel::open_file);
-    cx.add_action(ProjectPanel::cancel);
-    cx.add_action(ProjectPanel::cut);
-    cx.add_action(ProjectPanel::copy);
-    cx.add_action(ProjectPanel::copy_path);
-    cx.add_action(ProjectPanel::copy_relative_path);
-    cx.add_action(ProjectPanel::reveal_in_finder);
-    cx.add_action(ProjectPanel::open_in_terminal);
-    cx.add_action(ProjectPanel::new_search_in_directory);
-    cx.add_action(
-        |this: &mut ProjectPanel, action: &Paste, cx: &mut ViewContext<ProjectPanel>| {
-            this.paste(action, cx);
-        },
-    );
+
+    cx.observe_new_views(|workspace: &mut Workspace, _| {
+        workspace.register_action(|workspace, _: &ToggleFocus, cx| {
+            workspace.toggle_panel_focus::<ProjectPanel>(cx);
+        });
+    })
+    .detach();
 }
 
 #[derive(Debug)]
@@ -175,40 +151,45 @@ pub enum Event {
     SplitEntry {
         entry_id: ProjectEntryId,
     },
-    DockPositionChanged,
     Focus,
-    NewSearchInDirectory {
-        dir_entry: Entry,
-    },
-    ActivatePanel,
 }
 
 #[derive(Serialize, Deserialize)]
 struct SerializedProjectPanel {
-    width: Option<f32>,
+    width: Option<Pixels>,
+}
+
+struct DraggedProjectEntryView {
+    entry_id: ProjectEntryId,
+    details: EntryDetails,
+    width: Pixels,
 }
 
 impl ProjectPanel {
-    fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> {
+    fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
         let project = workspace.project().clone();
-        let project_panel = cx.add_view(|cx: &mut ViewContext<Self>| {
+        let project_panel = cx.new_view(|cx: &mut ViewContext<Self>| {
             cx.observe(&project, |this, _, cx| {
                 this.update_visible_entries(None, cx);
                 cx.notify();
             })
             .detach();
+            let focus_handle = cx.focus_handle();
+
+            cx.on_focus(&focus_handle, Self::focus_in).detach();
+
             cx.subscribe(&project, |this, project, event, cx| match event {
                 project::Event::ActiveEntryChanged(Some(entry_id)) => {
-                    if settings::get::<ProjectPanelSettings>(cx).auto_reveal_entries {
+                    if ProjectPanelSettings::get_global(cx).auto_reveal_entries {
                         this.reveal_entry(project, *entry_id, true, cx);
                     }
                 }
                 project::Event::RevealInProjectPanel(entry_id) => {
                     this.reveal_entry(project, *entry_id, false, cx);
-                    cx.emit(Event::ActivatePanel);
+                    cx.emit(PanelEvent::Activate);
                 }
                 project::Event::ActivateProjectPanel => {
-                    cx.emit(Event::ActivatePanel);
+                    cx.emit(PanelEvent::Activate);
                 }
                 project::Event::WorktreeRemoved(id) => {
                     this.expanded_dir_ids.remove(id);
@@ -219,58 +200,47 @@ impl ProjectPanel {
             })
             .detach();
 
-            let filename_editor = cx.add_view(|cx| {
-                Editor::single_line(
-                    Some(Arc::new(|theme| {
-                        let mut style = theme.project_panel.filename_editor.clone();
-                        style.container.background_color.take();
-                        style
-                    })),
-                    cx,
-                )
-            });
+            let filename_editor = cx.new_view(|cx| Editor::single_line(cx));
 
             cx.subscribe(&filename_editor, |this, _, event, cx| match event {
-                editor::Event::BufferEdited | editor::Event::SelectionsChanged { .. } => {
+                editor::EditorEvent::BufferEdited
+                | editor::EditorEvent::SelectionsChanged { .. } => {
                     this.autoscroll(cx);
                 }
-                _ => {}
-            })
-            .detach();
-            cx.observe_focus(&filename_editor, |this, _, is_focused, cx| {
-                if !is_focused
-                    && this
+                editor::EditorEvent::Blurred => {
+                    if this
                         .edit_state
                         .as_ref()
                         .map_or(false, |state| state.processing_filename.is_none())
-                {
-                    this.edit_state = None;
-                    this.update_visible_entries(None, cx);
+                    {
+                        this.edit_state = None;
+                        this.update_visible_entries(None, cx);
+                    }
                 }
+                _ => {}
             })
             .detach();
 
-            cx.observe_global::<FileAssociations, _>(|_, cx| {
-                cx.notify();
-            })
-            .detach();
+            // cx.observe_global::<FileAssociations, _>(|_, cx| {
+            //     cx.notify();
+            // })
+            // .detach();
 
-            let view_id = cx.view_id();
             let mut this = Self {
                 project: project.clone(),
                 fs: workspace.app_state().fs.clone(),
-                list: Default::default(),
+                list: UniformListScrollHandle::new(),
+                focus_handle,
                 visible_entries: Default::default(),
                 last_worktree_root_id: Default::default(),
                 expanded_dir_ids: Default::default(),
                 selection: None,
                 edit_state: None,
+                context_menu: None,
                 filename_editor,
                 clipboard_entry: None,
-                context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)),
-                dragged_entry_destination: None,
+                _dragged_entry_destination: None,
                 workspace: workspace.weak_handle(),
-                has_focus: false,
                 width: None,
                 pending_serialization: Task::ready(None),
             };
@@ -278,11 +248,12 @@ impl ProjectPanel {
 
             // Update the dock position when the setting changes.
             let mut old_dock_position = this.position(cx);
-            cx.observe_global::<SettingsStore, _>(move |this, cx| {
+            ProjectPanelSettings::register(cx);
+            cx.observe_global::<SettingsStore>(move |this, cx| {
                 let new_dock_position = this.position(cx);
                 if new_dock_position != old_dock_position {
                     old_dock_position = new_dock_position;
-                    cx.emit(Event::DockPositionChanged);
+                    cx.emit(PanelEvent::ChangePosition);
                 }
             })
             .detach();
@@ -311,8 +282,9 @@ impl ProjectPanel {
                                 )
                                 .detach_and_log_err(cx);
                             if !focus_opened_item {
-                                if let Some(project_panel) = project_panel.upgrade(cx) {
-                                    cx.focus(&project_panel);
+                                if let Some(project_panel) = project_panel.upgrade() {
+                                    let focus_handle = project_panel.read(cx).focus_handle.clone();
+                                    cx.focus(&focus_handle);
                                 }
                             }
                         }
@@ -320,16 +292,16 @@ impl ProjectPanel {
                 }
                 &Event::SplitEntry { entry_id } => {
                     if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) {
-                        if let Some(entry) = worktree.read(cx).entry_for_id(entry_id) {
-                            workspace
-                                .split_path(
-                                    ProjectPath {
-                                        worktree_id: worktree.read(cx).id(),
-                                        path: entry.path.clone(),
-                                    },
-                                    cx,
-                                )
-                                .detach_and_log_err(cx);
+                        if let Some(_entry) = worktree.read(cx).entry_for_id(entry_id) {
+                            // workspace
+                            //     .split_path(
+                            //         ProjectPath {
+                            //             worktree_id: worktree.read(cx).id(),
+                            //             path: entry.path.clone(),
+                            //         },
+                            //         cx,
+                            //     )
+                            //     .detach_and_log_err(cx);
                         }
                     }
                 }
@@ -341,38 +313,37 @@ impl ProjectPanel {
         project_panel
     }
 
-    pub fn load(
-        workspace: WeakViewHandle<Workspace>,
-        cx: AsyncAppContext,
-    ) -> Task<Result<ViewHandle<Self>>> {
-        cx.spawn(|mut cx| async move {
-            let serialized_panel = if let Some(panel) = cx
-                .background()
-                .spawn(async move { KEY_VALUE_STORE.read_kvp(PROJECT_PANEL_KEY) })
-                .await
-                .log_err()
-                .flatten()
-            {
-                Some(serde_json::from_str::<SerializedProjectPanel>(&panel)?)
-            } else {
-                None
-            };
-            workspace.update(&mut cx, |workspace, cx| {
-                let panel = ProjectPanel::new(workspace, cx);
-                if let Some(serialized_panel) = serialized_panel {
-                    panel.update(cx, |panel, cx| {
-                        panel.width = serialized_panel.width;
-                        cx.notify();
-                    });
-                }
-                panel
-            })
+    pub async fn load(
+        workspace: WeakView<Workspace>,
+        mut cx: AsyncWindowContext,
+    ) -> Result<View<Self>> {
+        let serialized_panel = cx
+            .background_executor()
+            .spawn(async move { KEY_VALUE_STORE.read_kvp(PROJECT_PANEL_KEY) })
+            .await
+            .map_err(|e| anyhow!("Failed to load project panel: {}", e))
+            .log_err()
+            .flatten()
+            .map(|panel| serde_json::from_str::<SerializedProjectPanel>(&panel))
+            .transpose()
+            .log_err()
+            .flatten();
+
+        workspace.update(&mut cx, |workspace, cx| {
+            let panel = ProjectPanel::new(workspace, cx);
+            if let Some(serialized_panel) = serialized_panel {
+                panel.update(cx, |panel, cx| {
+                    panel.width = serialized_panel.width;
+                    cx.notify();
+                });
+            }
+            panel
         })
     }
 
     fn serialize(&mut self, cx: &mut ViewContext<Self>) {
         let width = self.width;
-        self.pending_serialization = cx.background().spawn(
+        self.pending_serialization = cx.background_executor().spawn(
             async move {
                 KEY_VALUE_STORE
                     .write_kvp(
@@ -386,12 +357,19 @@ impl ProjectPanel {
         );
     }
 
+    fn focus_in(&mut self, cx: &mut ViewContext<Self>) {
+        if !self.focus_handle.contains_focused(cx) {
+            cx.emit(Event::Focus);
+        }
+    }
+
     fn deploy_context_menu(
         &mut self,
-        position: Vector2F,
+        position: Point<Pixels>,
         entry_id: ProjectEntryId,
         cx: &mut ViewContext<Self>,
     ) {
+        let this = cx.view().clone();
         let project = self.project.read(cx);
 
         let worktree_id = if let Some(id) = project.worktree_id_for_entry(entry_id, cx) {
@@ -405,60 +383,73 @@ impl ProjectPanel {
             entry_id,
         });
 
-        let mut menu_entries = Vec::new();
         if let Some((worktree, entry)) = self.selected_entry(cx) {
             let is_root = Some(entry) == worktree.root_entry();
-            if !project.is_remote() {
-                menu_entries.push(ContextMenuItem::action(
-                    "Add Folder to Project",
-                    workspace::AddFolderToProject,
-                ));
-                if is_root {
-                    let project = self.project.clone();
-                    menu_entries.push(ContextMenuItem::handler("Remove from Project", move |cx| {
-                        project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx));
-                    }));
+            let is_dir = entry.is_dir();
+            let worktree_id = worktree.id();
+            let is_local = project.is_local();
+
+            let context_menu = ContextMenu::build(cx, |mut menu, cx| {
+                if is_local {
+                    menu = menu.action(
+                        "Add Folder to Project",
+                        Box::new(workspace::AddFolderToProject),
+                    );
+                    if is_root {
+                        menu = menu.entry(
+                            "Remove from Project",
+                            None,
+                            cx.handler_for(&this, move |this, cx| {
+                                this.project.update(cx, |project, cx| {
+                                    project.remove_worktree(worktree_id, cx)
+                                });
+                            }),
+                        );
+                    }
                 }
-            }
-            menu_entries.push(ContextMenuItem::action("New File", NewFile));
-            menu_entries.push(ContextMenuItem::action("New Folder", NewDirectory));
-            menu_entries.push(ContextMenuItem::Separator);
-            menu_entries.push(ContextMenuItem::action("Cut", Cut));
-            menu_entries.push(ContextMenuItem::action("Copy", Copy));
-            if let Some(clipboard_entry) = self.clipboard_entry {
-                if clipboard_entry.worktree_id() == worktree.id() {
-                    menu_entries.push(ContextMenuItem::action("Paste", Paste));
+
+                menu = menu
+                    .action("New File", Box::new(NewFile))
+                    .action("New Folder", Box::new(NewDirectory))
+                    .separator()
+                    .action("Cut", Box::new(Cut))
+                    .action("Copy", Box::new(Copy));
+
+                if let Some(clipboard_entry) = self.clipboard_entry {
+                    if clipboard_entry.worktree_id() == worktree_id {
+                        menu = menu.action("Paste", Box::new(Paste));
+                    }
                 }
-            }
-            menu_entries.push(ContextMenuItem::Separator);
-            menu_entries.push(ContextMenuItem::action("Copy Path", CopyPath));
-            menu_entries.push(ContextMenuItem::action(
-                "Copy Relative Path",
-                CopyRelativePath,
-            ));
 
-            if entry.is_dir() {
-                menu_entries.push(ContextMenuItem::Separator);
-            }
-            menu_entries.push(ContextMenuItem::action("Reveal in Finder", RevealInFinder));
-            if entry.is_dir() {
-                menu_entries.push(ContextMenuItem::action("Open in Terminal", OpenInTerminal));
-                menu_entries.push(ContextMenuItem::action(
-                    "Search Inside",
-                    NewSearchInDirectory,
-                ));
-            }
+                menu = menu
+                    .separator()
+                    .action("Copy Path", Box::new(CopyPath))
+                    .action("Copy Relative Path", Box::new(CopyRelativePath))
+                    .separator()
+                    .action("Reveal in Finder", Box::new(RevealInFinder));
+
+                if is_dir {
+                    menu = menu
+                        .action("Open in Terminal", Box::new(OpenInTerminal))
+                        .action("Search Inside", Box::new(NewSearchInDirectory))
+                }
 
-            menu_entries.push(ContextMenuItem::Separator);
-            menu_entries.push(ContextMenuItem::action("Rename", Rename));
-            if !is_root {
-                menu_entries.push(ContextMenuItem::action("Delete", Delete));
-            }
-        }
+                menu = menu.separator().action("Rename", Box::new(Rename));
 
-        self.context_menu.update(cx, |menu, cx| {
-            menu.show(position, AnchorCorner::TopLeft, menu_entries, cx);
-        });
+                if !is_root {
+                    menu = menu.action("Delete", Box::new(Delete));
+                }
+
+                menu
+            });
+
+            cx.focus_view(&context_menu);
+            let subscription = cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| {
+                this.context_menu.take();
+                cx.notify();
+            });
+            self.context_menu = Some((context_menu, position, subscription));
+        }
 
         cx.notify();
     }
@@ -545,7 +536,7 @@ impl ProjectPanel {
                     }
                 });
                 self.update_visible_entries(Some((worktree_id, entry_id)), cx);
-                cx.focus_self();
+                cx.focus(&self.focus_handle);
                 cx.notify();
             }
         }
@@ -576,27 +567,23 @@ impl ProjectPanel {
         }
     }
 
-    fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
+    fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
         if let Some(task) = self.confirm_edit(cx) {
-            return Some(task);
+            task.detach_and_log_err(cx);
         }
-
-        None
     }
 
-    fn open_file(&mut self, _: &Open, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
+    fn open_file(&mut self, _: &Open, cx: &mut ViewContext<Self>) {
         if let Some((_, entry)) = self.selected_entry(cx) {
             if entry.is_file() {
                 self.open_entry(entry.id, true, cx);
             }
         }
-
-        None
     }
 
     fn confirm_edit(&mut self, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
         let edit_state = self.edit_state.as_mut()?;
-        cx.focus_self();
+        cx.focus(&self.focus_handle);
 
         let worktree_id = edit_state.worktree_id;
         let is_new_entry = edit_state.is_new_entry;
@@ -671,7 +658,7 @@ impl ProjectPanel {
     fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
         self.edit_state = None;
         self.update_visible_entries(None, cx);
-        cx.focus_self();
+        cx.focus(&self.focus_handle);
         cx.notify();
     }
 
@@ -745,9 +732,10 @@ impl ProjectPanel {
                 is_dir,
                 processing_filename: None,
             });
-            self.filename_editor
-                .update(cx, |editor, cx| editor.clear(cx));
-            cx.focus(&self.filename_editor);
+            self.filename_editor.update(cx, |editor, cx| {
+                editor.clear(cx);
+                editor.focus(cx);
+            });
             self.update_visible_entries(Some((worktree_id, NEW_ENTRY_ID)), cx);
             self.autoscroll(cx);
             cx.notify();
@@ -782,42 +770,47 @@ impl ProjectPanel {
                         editor.set_text(file_name, cx);
                         editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
                             s.select_ranges([0..selection_end])
-                        })
+                        });
+                        editor.focus(cx);
                     });
-                    cx.focus(&self.filename_editor);
                     self.update_visible_entries(None, cx);
                     self.autoscroll(cx);
                     cx.notify();
                 }
             }
 
-            cx.update_global(|drag_and_drop: &mut DragAndDrop<Workspace>, cx| {
-                drag_and_drop.cancel_dragging::<ProjectEntryId>(cx);
-            })
+            // cx.update_global(|drag_and_drop: &mut DragAndDrop<Workspace>, cx| {
+            //     drag_and_drop.cancel_dragging::<ProjectEntryId>(cx);
+            // })
         }
     }
 
-    fn delete(&mut self, _: &Delete, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
-        let Selection { entry_id, .. } = self.selection?;
-        let path = self.project.read(cx).path_for_entry(entry_id, cx)?.path;
-        let file_name = path.file_name()?;
+    fn delete(&mut self, _: &Delete, cx: &mut ViewContext<Self>) {
+        maybe!({
+            let Selection { entry_id, .. } = self.selection?;
+            let path = self.project.read(cx).path_for_entry(entry_id, cx)?.path;
+            let file_name = path.file_name()?;
 
-        let mut answer = cx.prompt(
-            PromptLevel::Info,
-            &format!("Delete {file_name:?}?"),
-            &["Delete", "Cancel"],
-        );
-        Some(cx.spawn(|this, mut cx| async move {
-            if answer.next().await != Some(0) {
-                return Ok(());
-            }
-            this.update(&mut cx, |this, cx| {
-                this.project
-                    .update(cx, |project, cx| project.delete_entry(entry_id, cx))
-                    .ok_or_else(|| anyhow!("no such entry"))
-            })??
-            .await
-        }))
+            let answer = cx.prompt(
+                PromptLevel::Info,
+                &format!("Delete {file_name:?}?"),
+                &["Delete", "Cancel"],
+            );
+
+            cx.spawn(|this, mut cx| async move {
+                if answer.await != Ok(0) {
+                    return Ok(());
+                }
+                this.update(&mut cx, |this, cx| {
+                    this.project
+                        .update(cx, |project, cx| project.delete_entry(entry_id, cx))
+                        .ok_or_else(|| anyhow!("no such entry"))
+                })??
+                .await
+            })
+            .detach_and_log_err(cx);
+            Some(())
+        });
     }
 
     fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
@@ -869,7 +862,7 @@ impl ProjectPanel {
 
     fn autoscroll(&mut self, cx: &mut ViewContext<Self>) {
         if let Some((_, _, index)) = self.selection.and_then(|s| self.index_for_selection(s)) {
-            self.list.scroll_to(ScrollTarget::Show(index));
+            self.list.scroll_to_item(index);
             cx.notify();
         }
     }
@@ -894,8 +887,9 @@ impl ProjectPanel {
         }
     }
 
-    fn paste(&mut self, _: &Paste, cx: &mut ViewContext<Self>) -> Option<()> {
-        if let Some((worktree, entry)) = self.selected_entry(cx) {
+    fn paste(&mut self, _: &Paste, cx: &mut ViewContext<Self>) {
+        maybe!({
+            let (worktree, entry) = self.selected_entry(cx)?;
             let clipboard_entry = self.clipboard_entry?;
             if clipboard_entry.worktree_id() != worktree.id() {
                 return None;
@@ -948,8 +942,9 @@ impl ProjectPanel {
                     })
                     .detach_and_log_err(cx)
             }
-        }
-        None
+
+            Some(())
+        });
     }
 
     fn copy_path(&mut self, _: &CopyPath, cx: &mut ViewContext<Self>) {
@@ -976,24 +971,25 @@ impl ProjectPanel {
         }
     }
 
-    fn open_in_terminal(&mut self, _: &OpenInTerminal, cx: &mut ViewContext<Self>) {
-        if let Some((worktree, entry)) = self.selected_entry(cx) {
-            let window = cx.window();
-            let view_id = cx.view_id();
-            let path = worktree.abs_path().join(&entry.path);
-
-            cx.app_context()
-                .spawn(|mut cx| async move {
-                    window.dispatch_action(
-                        view_id,
-                        &workspace::OpenTerminal {
-                            working_directory: path,
-                        },
-                        &mut cx,
-                    );
-                })
-                .detach();
-        }
+    fn open_in_terminal(&mut self, _: &OpenInTerminal, _cx: &mut ViewContext<Self>) {
+        todo!()
+        // if let Some((worktree, entry)) = self.selected_entry(cx) {
+        //     let window = cx.window();
+        //     let view_id = cx.view_id();
+        //     let path = worktree.abs_path().join(&entry.path);
+
+        //     cx.app_context()
+        //         .spawn(|mut cx| async move {
+        //             window.dispatch_action(
+        //                 view_id,
+        //                 &workspace::OpenTerminal {
+        //                     working_directory: path,
+        //                 },
+        //                 &mut cx,
+        //             );
+        //         })
+        //         .detach();
+        // }
     }
 
     pub fn new_search_in_directory(
@@ -1003,9 +999,12 @@ impl ProjectPanel {
     ) {
         if let Some((_, entry)) = self.selected_entry(cx) {
             if entry.is_dir() {
-                cx.emit(Event::NewSearchInDirectory {
-                    dir_entry: entry.clone(),
-                });
+                let entry = entry.clone();
+                self.workspace
+                    .update(cx, |workspace, cx| {
+                        search::ProjectSearchView::new_search_in_directory(workspace, &entry, cx);
+                    })
+                    .ok();
             }
         }
     }
@@ -1030,7 +1029,7 @@ impl ProjectPanel {
             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);
-                cx.foreground().spawn(task).detach_and_log_err(cx);
+                cx.foreground_executor().spawn(task).detach_and_log_err(cx);
             }
 
             Some(project.worktree_id_for_entry(destination, cx)?)
@@ -1075,7 +1074,7 @@ impl ProjectPanel {
     fn selected_entry_handle<'a>(
         &self,
         cx: &'a AppContext,
-    ) -> Option<(ModelHandle<Worktree>, &'a project::Entry)> {
+    ) -> Option<(Model<Worktree>, &'a project::Entry)> {
         let selection = self.selection?;
         let project = self.project.read(cx);
         let worktree = project.worktree_for_id(selection.worktree_id, cx)?;
@@ -1262,7 +1261,7 @@ impl ProjectPanel {
 
             let end_ix = range.end.min(ix + visible_worktree_entries.len());
             let (git_status_setting, show_file_icons, show_folder_icons) = {
-                let settings = settings::get::<ProjectPanelSettings>(cx);
+                let settings = ProjectPanelSettings::get_global(cx);
                 (
                     settings.git_status,
                     settings.file_icons,
@@ -1285,16 +1284,16 @@ impl ProjectPanel {
                     let icon = match entry.kind {
                         EntryKind::File(_) => {
                             if show_file_icons {
-                                Some(FileAssociations::get_icon(&entry.path, cx))
+                                FileAssociations::get_icon(&entry.path, cx)
                             } else {
                                 None
                             }
                         }
                         _ => {
                             if show_folder_icons {
-                                Some(FileAssociations::get_folder_icon(is_expanded, cx))
+                                FileAssociations::get_folder_icon(is_expanded, cx)
                             } else {
-                                Some(FileAssociations::get_chevron_icon(is_expanded, cx))
+                                FileAssociations::get_chevron_icon(is_expanded, cx)
                             }
                         }
                     };
@@ -1351,193 +1350,115 @@ impl ProjectPanel {
         }
     }
 
-    fn render_entry_visual_element<V: 'static>(
-        details: &EntryDetails,
-        editor: Option<&ViewHandle<Editor>>,
-        padding: f32,
-        row_container_style: ContainerStyle,
-        style: &ProjectPanelEntry,
-        cx: &mut ViewContext<V>,
-    ) -> AnyElement<V> {
+    fn render_entry(
+        &self,
+        entry_id: ProjectEntryId,
+        details: EntryDetails,
+        cx: &mut ViewContext<Self>,
+    ) -> Stateful<Div> {
+        let kind = details.kind;
+        let settings = ProjectPanelSettings::get_global(cx);
         let show_editor = details.is_editing && !details.is_processing;
+        let is_selected = self
+            .selection
+            .map_or(false, |selection| selection.entry_id == entry_id);
+        let width = self.width.unwrap_or(px(0.));
 
-        let mut filename_text_style = style.text.clone();
-        filename_text_style.color = details
+        let filename_text_color = details
             .git_status
             .as_ref()
             .map(|status| match status {
-                GitFileStatus::Added => style.status.git.inserted,
-                GitFileStatus::Modified => style.status.git.modified,
-                GitFileStatus::Conflict => style.status.git.conflict,
+                GitFileStatus::Added => Color::Created,
+                GitFileStatus::Modified => Color::Modified,
+                GitFileStatus::Conflict => Color::Conflict,
             })
-            .unwrap_or(style.text.color);
-
-        Flex::row()
-            .with_child(if let Some(icon) = &details.icon {
-                Svg::new(icon.to_string())
-                    .with_color(style.icon_color)
-                    .constrained()
-                    .with_max_width(style.icon_size)
-                    .with_max_height(style.icon_size)
-                    .aligned()
-                    .constrained()
-                    .with_width(style.icon_size)
+            .unwrap_or(if is_selected {
+                Color::Default
             } else {
-                Empty::new()
-                    .constrained()
-                    .with_max_width(style.icon_size)
-                    .with_max_height(style.icon_size)
-                    .aligned()
-                    .constrained()
-                    .with_width(style.icon_size)
+                Color::Muted
+            });
+
+        let file_name = details.filename.clone();
+        let icon = details.icon.clone();
+        let depth = details.depth;
+        div()
+            .id(entry_id.to_proto() as usize)
+            .on_drag(entry_id, move |entry_id, cx| {
+                cx.new_view(|_| DraggedProjectEntryView {
+                    details: details.clone(),
+                    width,
+                    entry_id: *entry_id,
+                })
             })
-            .with_child(if show_editor && editor.is_some() {
-                ChildView::new(editor.as_ref().unwrap(), cx)
-                    .contained()
-                    .with_margin_left(style.icon_spacing)
-                    .aligned()
-                    .left()
-                    .flex(1.0, true)
-                    .into_any()
-            } else {
-                Label::new(details.filename.clone(), filename_text_style)
-                    .contained()
-                    .with_margin_left(style.icon_spacing)
-                    .aligned()
-                    .left()
-                    .into_any()
+            .drag_over::<ProjectEntryId>(|style| {
+                style.bg(cx.theme().colors().drop_target_background)
             })
-            .constrained()
-            .with_height(style.height)
-            .contained()
-            .with_style(row_container_style)
-            .with_padding_left(padding)
-            .into_any_named("project panel entry visual element")
+            .on_drop(cx.listener(move |this, dragged_id: &ProjectEntryId, cx| {
+                this.move_entry(*dragged_id, entry_id, kind.is_file(), cx);
+            }))
+            .child(
+                ListItem::new(entry_id.to_proto() as usize)
+                    .indent_level(depth)
+                    .indent_step_size(px(settings.indent_size))
+                    .selected(is_selected)
+                    .child(if let Some(icon) = &icon {
+                        div().child(IconElement::from_path(icon.to_string()).color(Color::Muted))
+                    } else {
+                        div()
+                    })
+                    .child(
+                        if let (Some(editor), true) = (Some(&self.filename_editor), show_editor) {
+                            div().h_full().w_full().child(editor.clone())
+                        } else {
+                            div().child(Label::new(file_name).color(filename_text_color))
+                        }
+                        .ml_1(),
+                    )
+                    .on_click(cx.listener(move |this, event: &gpui::ClickEvent, cx| {
+                        if event.down.button == MouseButton::Right {
+                            return;
+                        }
+                        if !show_editor {
+                            if kind.is_dir() {
+                                this.toggle_expanded(entry_id, cx);
+                            } else {
+                                if event.down.modifiers.command {
+                                    this.split_entry(entry_id, cx);
+                                } else {
+                                    this.open_entry(entry_id, event.up.click_count > 1, cx);
+                                }
+                            }
+                        }
+                    }))
+                    .on_secondary_mouse_down(cx.listener(
+                        move |this, event: &MouseDownEvent, cx| {
+                            this.deploy_context_menu(event.position, entry_id, cx);
+                        },
+                    )),
+            )
     }
 
-    fn render_entry(
-        entry_id: ProjectEntryId,
-        details: EntryDetails,
-        editor: &ViewHandle<Editor>,
-        dragged_entry_destination: &mut Option<Arc<Path>>,
-        theme: &theme::ProjectPanel,
-        cx: &mut ViewContext<Self>,
-    ) -> AnyElement<Self> {
-        let kind = details.kind;
-        let path = details.path.clone();
-        let settings = settings::get::<ProjectPanelSettings>(cx);
-        let padding = theme.container.padding.left + details.depth as f32 * settings.indent_size;
-
-        let entry_style = if details.is_cut {
-            &theme.cut_entry
-        } else if details.is_ignored {
-            &theme.ignored_entry
+    fn dispatch_context(&self, cx: &ViewContext<Self>) -> KeyContext {
+        let mut dispatch_context = KeyContext::default();
+        dispatch_context.add("ProjectPanel");
+        dispatch_context.add("menu");
+
+        let identifier = if self.filename_editor.focus_handle(cx).is_focused(cx) {
+            "editing"
         } else {
-            &theme.entry
+            "not_editing"
         };
 
-        let show_editor = details.is_editing && !details.is_processing;
-
-        MouseEventHandler::new::<Self, _>(entry_id.to_usize(), cx, |state, cx| {
-            let mut style = entry_style
-                .in_state(details.is_selected)
-                .style_for(state)
-                .clone();
-
-            if cx
-                .global::<DragAndDrop<Workspace>>()
-                .currently_dragged::<ProjectEntryId>(cx.window())
-                .is_some()
-                && dragged_entry_destination
-                    .as_ref()
-                    .filter(|destination| details.path.starts_with(destination))
-                    .is_some()
-            {
-                style = entry_style.active_state().default.clone();
-            }
-
-            let row_container_style = if show_editor {
-                theme.filename_editor.container
-            } else {
-                style.container
-            };
-
-            Self::render_entry_visual_element(
-                &details,
-                Some(editor),
-                padding,
-                row_container_style,
-                &style,
-                cx,
-            )
-        })
-        .on_click(MouseButton::Left, move |event, this, cx| {
-            if !show_editor {
-                if kind.is_dir() {
-                    this.toggle_expanded(entry_id, cx);
-                } else {
-                    if event.cmd {
-                        this.split_entry(entry_id, cx);
-                    } else if !event.cmd {
-                        this.open_entry(entry_id, event.click_count > 1, cx);
-                    }
-                }
-            }
-        })
-        .on_down(MouseButton::Right, move |event, this, cx| {
-            this.deploy_context_menu(event.position, entry_id, cx);
-        })
-        .on_up(MouseButton::Left, move |_, this, cx| {
-            if let Some((_, dragged_entry)) = cx
-                .global::<DragAndDrop<Workspace>>()
-                .currently_dragged::<ProjectEntryId>(cx.window())
-            {
-                this.move_entry(
-                    *dragged_entry,
-                    entry_id,
-                    matches!(details.kind, EntryKind::File(_)),
-                    cx,
-                );
-            }
-        })
-        .on_move(move |_, this, cx| {
-            if cx
-                .global::<DragAndDrop<Workspace>>()
-                .currently_dragged::<ProjectEntryId>(cx.window())
-                .is_some()
-            {
-                this.dragged_entry_destination = if matches!(kind, EntryKind::File(_)) {
-                    path.parent().map(|parent| Arc::from(parent))
-                } else {
-                    Some(path.clone())
-                };
-            }
-        })
-        .as_draggable(entry_id, {
-            let row_container_style = theme.dragged_entry.container;
-
-            move |_, _, cx: &mut ViewContext<Workspace>| {
-                let theme = theme::current(cx).clone();
-                Self::render_entry_visual_element(
-                    &details,
-                    None,
-                    padding,
-                    row_container_style,
-                    &theme.project_panel.dragged_entry,
-                    cx,
-                )
-            }
-        })
-        .with_cursor_style(CursorStyle::PointingHand)
-        .into_any_named("project panel entry")
+        dispatch_context.add(identifier);
+        dispatch_context
     }
 
     fn reveal_entry(
         &mut self,
-        project: ModelHandle<Project>,
+        project: Model<Project>,
         entry_id: ProjectEntryId,
         skip_ignored: bool,
-        cx: &mut ViewContext<'_, '_, ProjectPanel>,
+        cx: &mut ViewContext<'_, ProjectPanel>,
     ) {
         if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) {
             let worktree = worktree.read(cx);

crates/project_panel/src/project_panel_settings.rs πŸ”—

@@ -1,7 +1,8 @@
 use anyhow;
+use gpui::Pixels;
 use schemars::JsonSchema;
 use serde_derive::{Deserialize, Serialize};
-use settings::Setting;
+use settings::Settings;
 
 #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
 #[serde(rename_all = "snake_case")]
@@ -12,7 +13,7 @@ pub enum ProjectPanelDockPosition {
 
 #[derive(Deserialize, Debug)]
 pub struct ProjectPanelSettings {
-    pub default_width: f32,
+    pub default_width: Pixels,
     pub dock: ProjectPanelDockPosition,
     pub file_icons: bool,
     pub folder_icons: bool,
@@ -32,7 +33,7 @@ pub struct ProjectPanelSettingsContent {
     pub auto_reveal_entries: Option<bool>,
 }
 
-impl Setting for ProjectPanelSettings {
+impl Settings for ProjectPanelSettings {
     const KEY: Option<&'static str> = Some("project_panel");
 
     type FileContent = ProjectPanelSettingsContent;
@@ -40,7 +41,7 @@ impl Setting for ProjectPanelSettings {
     fn load(
         default_value: &Self::FileContent,
         user_values: &[&Self::FileContent],
-        _: &gpui::AppContext,
+        _: &mut gpui::AppContext,
     ) -> anyhow::Result<Self> {
         Self::load_via_json_merge(default_value, user_values)
     }

crates/project_panel2/Cargo.toml πŸ”—

@@ -1,41 +0,0 @@
-[package]
-name = "project_panel2"
-version = "0.1.0"
-edition = "2021"
-publish = false
-
-[lib]
-path = "src/project_panel.rs"
-doctest = false
-
-[dependencies]
-collections = { path = "../collections" }
-db = { path = "../db2", package = "db2" }
-editor = { path = "../editor2", package = "editor2" }
-gpui = { path = "../gpui2", package = "gpui2" }
-menu = { path = "../menu2", package = "menu2" }
-project = { path = "../project2", package = "project2" }
-search = { package = "search2", path = "../search2" }
-settings = { path = "../settings2", package = "settings2" }
-theme = { path = "../theme2", package = "theme2" }
-ui = { path = "../ui2", package = "ui2" }
-util = { path = "../util" }
-workspace = { path = "../workspace2", package = "workspace2" }
-anyhow.workspace = true
-postage.workspace = true
-futures.workspace = true
-serde.workspace = true
-serde_derive.workspace = true
-serde_json.workspace = true
-schemars.workspace = true
-smallvec.workspace = true
-pretty_assertions.workspace = true
-unicase = "2.6"
-
-[dev-dependencies]
-client = { path = "../client2", package = "client2", features = ["test-support"] }
-language = { path = "../language2", package = "language2", features = ["test-support"] }
-editor = { path = "../editor2", package = "editor2", features = ["test-support"] }
-gpui = { path = "../gpui2", package = "gpui2", features = ["test-support"] }
-workspace = { path = "../workspace2", package = "workspace2", features = ["test-support"] }
-serde_json.workspace = true

crates/project_panel2/src/file_associations.rs πŸ”—

@@ -1,87 +0,0 @@
-use std::{path::Path, str, sync::Arc};
-
-use collections::HashMap;
-
-use gpui::{AppContext, AssetSource};
-use serde_derive::Deserialize;
-use util::{maybe, paths::PathExt};
-
-#[derive(Deserialize, Debug)]
-struct TypeConfig {
-    icon: Arc<str>,
-}
-
-#[derive(Deserialize, Debug)]
-pub struct FileAssociations {
-    suffixes: HashMap<String, String>,
-    types: HashMap<String, TypeConfig>,
-}
-
-const COLLAPSED_DIRECTORY_TYPE: &'static str = "collapsed_folder";
-const EXPANDED_DIRECTORY_TYPE: &'static str = "expanded_folder";
-const COLLAPSED_CHEVRON_TYPE: &'static str = "collapsed_chevron";
-const EXPANDED_CHEVRON_TYPE: &'static str = "expanded_chevron";
-pub const FILE_TYPES_ASSET: &'static str = "icons/file_icons/file_types.json";
-
-pub fn init(assets: impl AssetSource, cx: &mut AppContext) {
-    cx.set_global(FileAssociations::new(assets))
-}
-
-impl FileAssociations {
-    pub fn new(assets: impl AssetSource) -> Self {
-        assets
-            .load("icons/file_icons/file_types.json")
-            .and_then(|file| {
-                serde_json::from_str::<FileAssociations>(str::from_utf8(&file).unwrap())
-                    .map_err(Into::into)
-            })
-            .unwrap_or_else(|_| FileAssociations {
-                suffixes: HashMap::default(),
-                types: HashMap::default(),
-            })
-    }
-
-    pub fn get_icon(path: &Path, cx: &AppContext) -> Option<Arc<str>> {
-        let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
-
-        // FIXME: Associate a type with the languages and have the file's langauge
-        //        override these associations
-        maybe!({
-            let suffix = path.icon_suffix()?;
-
-            this.suffixes
-                .get(suffix)
-                .and_then(|type_str| this.types.get(type_str))
-                .map(|type_config| type_config.icon.clone())
-        })
-        .or_else(|| this.types.get("default").map(|config| config.icon.clone()))
-    }
-
-    pub fn get_folder_icon(expanded: bool, cx: &AppContext) -> Option<Arc<str>> {
-        let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
-
-        let key = if expanded {
-            EXPANDED_DIRECTORY_TYPE
-        } else {
-            COLLAPSED_DIRECTORY_TYPE
-        };
-
-        this.types
-            .get(key)
-            .map(|type_config| type_config.icon.clone())
-    }
-
-    pub fn get_chevron_icon(expanded: bool, cx: &AppContext) -> Option<Arc<str>> {
-        let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
-
-        let key = if expanded {
-            EXPANDED_CHEVRON_TYPE
-        } else {
-            COLLAPSED_CHEVRON_TYPE
-        };
-
-        this.types
-            .get(key)
-            .map(|type_config| type_config.icon.clone())
-    }
-}

crates/project_panel2/src/project_panel.rs πŸ”—

@@ -1,3480 +0,0 @@
-pub mod file_associations;
-mod project_panel_settings;
-use settings::{Settings, SettingsStore};
-
-use db::kvp::KEY_VALUE_STORE;
-use editor::{scroll::autoscroll::Autoscroll, Cancel, Editor};
-use file_associations::FileAssociations;
-
-use anyhow::{anyhow, Result};
-use gpui::{
-    actions, div, overlay, px, uniform_list, Action, AppContext, AssetSource, AsyncWindowContext,
-    ClipboardItem, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView, InteractiveElement,
-    KeyContext, Model, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel,
-    Render, Stateful, Styled, Subscription, Task, UniformListScrollHandle, View, ViewContext,
-    VisualContext as _, WeakView, WindowContext,
-};
-use menu::{Confirm, SelectNext, SelectPrev};
-use project::{
-    repository::GitFileStatus, Entry, EntryKind, Fs, Project, ProjectEntryId, ProjectPath,
-    Worktree, WorktreeId,
-};
-use project_panel_settings::{ProjectPanelDockPosition, ProjectPanelSettings};
-use serde::{Deserialize, Serialize};
-use std::{
-    cmp::Ordering,
-    collections::{hash_map, HashMap},
-    ffi::OsStr,
-    ops::Range,
-    path::Path,
-    sync::Arc,
-};
-use theme::ThemeSettings;
-use ui::{prelude::*, v_stack, ContextMenu, IconElement, Label, ListItem};
-use unicase::UniCase;
-use util::{maybe, ResultExt, TryFutureExt};
-use workspace::{
-    dock::{DockPosition, Panel, PanelEvent},
-    Workspace,
-};
-
-const PROJECT_PANEL_KEY: &'static str = "ProjectPanel";
-const NEW_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX;
-
-pub struct ProjectPanel {
-    project: Model<Project>,
-    fs: Arc<dyn Fs>,
-    list: UniformListScrollHandle,
-    focus_handle: FocusHandle,
-    visible_entries: Vec<(WorktreeId, Vec<Entry>)>,
-    last_worktree_root_id: Option<ProjectEntryId>,
-    expanded_dir_ids: HashMap<WorktreeId, Vec<ProjectEntryId>>,
-    selection: Option<Selection>,
-    context_menu: Option<(View<ContextMenu>, Point<Pixels>, Subscription)>,
-    edit_state: Option<EditState>,
-    filename_editor: View<Editor>,
-    clipboard_entry: Option<ClipboardEntry>,
-    _dragged_entry_destination: Option<Arc<Path>>,
-    workspace: WeakView<Workspace>,
-    width: Option<Pixels>,
-    pending_serialization: Task<Option<()>>,
-}
-
-#[derive(Copy, Clone, Debug)]
-struct Selection {
-    worktree_id: WorktreeId,
-    entry_id: ProjectEntryId,
-}
-
-#[derive(Clone, Debug)]
-struct EditState {
-    worktree_id: WorktreeId,
-    entry_id: ProjectEntryId,
-    is_new_entry: bool,
-    is_dir: bool,
-    processing_filename: Option<String>,
-}
-
-#[derive(Copy, Clone)]
-pub enum ClipboardEntry {
-    Copied {
-        worktree_id: WorktreeId,
-        entry_id: ProjectEntryId,
-    },
-    Cut {
-        worktree_id: WorktreeId,
-        entry_id: ProjectEntryId,
-    },
-}
-
-#[derive(Debug, PartialEq, Eq, Clone)]
-pub struct EntryDetails {
-    filename: String,
-    icon: Option<Arc<str>>,
-    path: Arc<Path>,
-    depth: usize,
-    kind: EntryKind,
-    is_ignored: bool,
-    is_expanded: bool,
-    is_selected: bool,
-    is_editing: bool,
-    is_processing: bool,
-    is_cut: bool,
-    git_status: Option<GitFileStatus>,
-}
-
-actions!(
-    project_panel,
-    [
-        ExpandSelectedEntry,
-        CollapseSelectedEntry,
-        CollapseAllEntries,
-        NewDirectory,
-        NewFile,
-        Copy,
-        CopyPath,
-        CopyRelativePath,
-        RevealInFinder,
-        OpenInTerminal,
-        Cut,
-        Paste,
-        Delete,
-        Rename,
-        Open,
-        ToggleFocus,
-        NewSearchInDirectory,
-    ]
-);
-
-pub fn init_settings(cx: &mut AppContext) {
-    ProjectPanelSettings::register(cx);
-}
-
-pub fn init(assets: impl AssetSource, cx: &mut AppContext) {
-    init_settings(cx);
-    file_associations::init(assets, cx);
-
-    cx.observe_new_views(|workspace: &mut Workspace, _| {
-        workspace.register_action(|workspace, _: &ToggleFocus, cx| {
-            workspace.toggle_panel_focus::<ProjectPanel>(cx);
-        });
-    })
-    .detach();
-}
-
-#[derive(Debug)]
-pub enum Event {
-    OpenedEntry {
-        entry_id: ProjectEntryId,
-        focus_opened_item: bool,
-    },
-    SplitEntry {
-        entry_id: ProjectEntryId,
-    },
-    Focus,
-}
-
-#[derive(Serialize, Deserialize)]
-struct SerializedProjectPanel {
-    width: Option<Pixels>,
-}
-
-struct DraggedProjectEntryView {
-    entry_id: ProjectEntryId,
-    details: EntryDetails,
-    width: Pixels,
-}
-
-impl ProjectPanel {
-    fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
-        let project = workspace.project().clone();
-        let project_panel = cx.new_view(|cx: &mut ViewContext<Self>| {
-            cx.observe(&project, |this, _, cx| {
-                this.update_visible_entries(None, cx);
-                cx.notify();
-            })
-            .detach();
-            let focus_handle = cx.focus_handle();
-
-            cx.on_focus(&focus_handle, Self::focus_in).detach();
-
-            cx.subscribe(&project, |this, project, event, cx| match event {
-                project::Event::ActiveEntryChanged(Some(entry_id)) => {
-                    if ProjectPanelSettings::get_global(cx).auto_reveal_entries {
-                        this.reveal_entry(project, *entry_id, true, cx);
-                    }
-                }
-                project::Event::RevealInProjectPanel(entry_id) => {
-                    this.reveal_entry(project, *entry_id, false, cx);
-                    cx.emit(PanelEvent::Activate);
-                }
-                project::Event::ActivateProjectPanel => {
-                    cx.emit(PanelEvent::Activate);
-                }
-                project::Event::WorktreeRemoved(id) => {
-                    this.expanded_dir_ids.remove(id);
-                    this.update_visible_entries(None, cx);
-                    cx.notify();
-                }
-                _ => {}
-            })
-            .detach();
-
-            let filename_editor = cx.new_view(|cx| Editor::single_line(cx));
-
-            cx.subscribe(&filename_editor, |this, _, event, cx| match event {
-                editor::EditorEvent::BufferEdited
-                | editor::EditorEvent::SelectionsChanged { .. } => {
-                    this.autoscroll(cx);
-                }
-                editor::EditorEvent::Blurred => {
-                    if this
-                        .edit_state
-                        .as_ref()
-                        .map_or(false, |state| state.processing_filename.is_none())
-                    {
-                        this.edit_state = None;
-                        this.update_visible_entries(None, cx);
-                    }
-                }
-                _ => {}
-            })
-            .detach();
-
-            // cx.observe_global::<FileAssociations, _>(|_, cx| {
-            //     cx.notify();
-            // })
-            // .detach();
-
-            let mut this = Self {
-                project: project.clone(),
-                fs: workspace.app_state().fs.clone(),
-                list: UniformListScrollHandle::new(),
-                focus_handle,
-                visible_entries: Default::default(),
-                last_worktree_root_id: Default::default(),
-                expanded_dir_ids: Default::default(),
-                selection: None,
-                edit_state: None,
-                context_menu: None,
-                filename_editor,
-                clipboard_entry: None,
-                _dragged_entry_destination: None,
-                workspace: workspace.weak_handle(),
-                width: None,
-                pending_serialization: Task::ready(None),
-            };
-            this.update_visible_entries(None, cx);
-
-            // Update the dock position when the setting changes.
-            let mut old_dock_position = this.position(cx);
-            ProjectPanelSettings::register(cx);
-            cx.observe_global::<SettingsStore>(move |this, cx| {
-                let new_dock_position = this.position(cx);
-                if new_dock_position != old_dock_position {
-                    old_dock_position = new_dock_position;
-                    cx.emit(PanelEvent::ChangePosition);
-                }
-            })
-            .detach();
-
-            this
-        });
-
-        cx.subscribe(&project_panel, {
-            let project_panel = project_panel.downgrade();
-            move |workspace, _, event, cx| match event {
-                &Event::OpenedEntry {
-                    entry_id,
-                    focus_opened_item,
-                } => {
-                    if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) {
-                        if let Some(entry) = worktree.read(cx).entry_for_id(entry_id) {
-                            workspace
-                                .open_path(
-                                    ProjectPath {
-                                        worktree_id: worktree.read(cx).id(),
-                                        path: entry.path.clone(),
-                                    },
-                                    None,
-                                    focus_opened_item,
-                                    cx,
-                                )
-                                .detach_and_log_err(cx);
-                            if !focus_opened_item {
-                                if let Some(project_panel) = project_panel.upgrade() {
-                                    let focus_handle = project_panel.read(cx).focus_handle.clone();
-                                    cx.focus(&focus_handle);
-                                }
-                            }
-                        }
-                    }
-                }
-                &Event::SplitEntry { entry_id } => {
-                    if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) {
-                        if let Some(_entry) = worktree.read(cx).entry_for_id(entry_id) {
-                            // workspace
-                            //     .split_path(
-                            //         ProjectPath {
-                            //             worktree_id: worktree.read(cx).id(),
-                            //             path: entry.path.clone(),
-                            //         },
-                            //         cx,
-                            //     )
-                            //     .detach_and_log_err(cx);
-                        }
-                    }
-                }
-                _ => {}
-            }
-        })
-        .detach();
-
-        project_panel
-    }
-
-    pub async fn load(
-        workspace: WeakView<Workspace>,
-        mut cx: AsyncWindowContext,
-    ) -> Result<View<Self>> {
-        let serialized_panel = cx
-            .background_executor()
-            .spawn(async move { KEY_VALUE_STORE.read_kvp(PROJECT_PANEL_KEY) })
-            .await
-            .map_err(|e| anyhow!("Failed to load project panel: {}", e))
-            .log_err()
-            .flatten()
-            .map(|panel| serde_json::from_str::<SerializedProjectPanel>(&panel))
-            .transpose()
-            .log_err()
-            .flatten();
-
-        workspace.update(&mut cx, |workspace, cx| {
-            let panel = ProjectPanel::new(workspace, cx);
-            if let Some(serialized_panel) = serialized_panel {
-                panel.update(cx, |panel, cx| {
-                    panel.width = serialized_panel.width;
-                    cx.notify();
-                });
-            }
-            panel
-        })
-    }
-
-    fn serialize(&mut self, cx: &mut ViewContext<Self>) {
-        let width = self.width;
-        self.pending_serialization = cx.background_executor().spawn(
-            async move {
-                KEY_VALUE_STORE
-                    .write_kvp(
-                        PROJECT_PANEL_KEY.into(),
-                        serde_json::to_string(&SerializedProjectPanel { width })?,
-                    )
-                    .await?;
-                anyhow::Ok(())
-            }
-            .log_err(),
-        );
-    }
-
-    fn focus_in(&mut self, cx: &mut ViewContext<Self>) {
-        if !self.focus_handle.contains_focused(cx) {
-            cx.emit(Event::Focus);
-        }
-    }
-
-    fn deploy_context_menu(
-        &mut self,
-        position: Point<Pixels>,
-        entry_id: ProjectEntryId,
-        cx: &mut ViewContext<Self>,
-    ) {
-        let this = cx.view().clone();
-        let project = self.project.read(cx);
-
-        let worktree_id = if let Some(id) = project.worktree_id_for_entry(entry_id, cx) {
-            id
-        } else {
-            return;
-        };
-
-        self.selection = Some(Selection {
-            worktree_id,
-            entry_id,
-        });
-
-        if let Some((worktree, entry)) = self.selected_entry(cx) {
-            let is_root = Some(entry) == worktree.root_entry();
-            let is_dir = entry.is_dir();
-            let worktree_id = worktree.id();
-            let is_local = project.is_local();
-
-            let context_menu = ContextMenu::build(cx, |mut menu, cx| {
-                if is_local {
-                    menu = menu.action(
-                        "Add Folder to Project",
-                        Box::new(workspace::AddFolderToProject),
-                    );
-                    if is_root {
-                        menu = menu.entry(
-                            "Remove from Project",
-                            None,
-                            cx.handler_for(&this, move |this, cx| {
-                                this.project.update(cx, |project, cx| {
-                                    project.remove_worktree(worktree_id, cx)
-                                });
-                            }),
-                        );
-                    }
-                }
-
-                menu = menu
-                    .action("New File", Box::new(NewFile))
-                    .action("New Folder", Box::new(NewDirectory))
-                    .separator()
-                    .action("Cut", Box::new(Cut))
-                    .action("Copy", Box::new(Copy));
-
-                if let Some(clipboard_entry) = self.clipboard_entry {
-                    if clipboard_entry.worktree_id() == worktree_id {
-                        menu = menu.action("Paste", Box::new(Paste));
-                    }
-                }
-
-                menu = menu
-                    .separator()
-                    .action("Copy Path", Box::new(CopyPath))
-                    .action("Copy Relative Path", Box::new(CopyRelativePath))
-                    .separator()
-                    .action("Reveal in Finder", Box::new(RevealInFinder));
-
-                if is_dir {
-                    menu = menu
-                        .action("Open in Terminal", Box::new(OpenInTerminal))
-                        .action("Search Inside", Box::new(NewSearchInDirectory))
-                }
-
-                menu = menu.separator().action("Rename", Box::new(Rename));
-
-                if !is_root {
-                    menu = menu.action("Delete", Box::new(Delete));
-                }
-
-                menu
-            });
-
-            cx.focus_view(&context_menu);
-            let subscription = cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| {
-                this.context_menu.take();
-                cx.notify();
-            });
-            self.context_menu = Some((context_menu, position, subscription));
-        }
-
-        cx.notify();
-    }
-
-    fn expand_selected_entry(&mut self, _: &ExpandSelectedEntry, cx: &mut ViewContext<Self>) {
-        if let Some((worktree, entry)) = self.selected_entry(cx) {
-            if entry.is_dir() {
-                let worktree_id = worktree.id();
-                let entry_id = entry.id;
-                let expanded_dir_ids =
-                    if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) {
-                        expanded_dir_ids
-                    } else {
-                        return;
-                    };
-
-                match expanded_dir_ids.binary_search(&entry_id) {
-                    Ok(_) => self.select_next(&SelectNext, cx),
-                    Err(ix) => {
-                        self.project.update(cx, |project, cx| {
-                            project.expand_entry(worktree_id, entry_id, cx);
-                        });
-
-                        expanded_dir_ids.insert(ix, entry_id);
-                        self.update_visible_entries(None, cx);
-                        cx.notify();
-                    }
-                }
-            }
-        }
-    }
-
-    fn collapse_selected_entry(&mut self, _: &CollapseSelectedEntry, cx: &mut ViewContext<Self>) {
-        if let Some((worktree, mut entry)) = self.selected_entry(cx) {
-            let worktree_id = worktree.id();
-            let expanded_dir_ids =
-                if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) {
-                    expanded_dir_ids
-                } else {
-                    return;
-                };
-
-            loop {
-                let entry_id = entry.id;
-                match expanded_dir_ids.binary_search(&entry_id) {
-                    Ok(ix) => {
-                        expanded_dir_ids.remove(ix);
-                        self.update_visible_entries(Some((worktree_id, entry_id)), cx);
-                        cx.notify();
-                        break;
-                    }
-                    Err(_) => {
-                        if let Some(parent_entry) =
-                            entry.path.parent().and_then(|p| worktree.entry_for_path(p))
-                        {
-                            entry = parent_entry;
-                        } else {
-                            break;
-                        }
-                    }
-                }
-            }
-        }
-    }
-
-    pub fn collapse_all_entries(&mut self, _: &CollapseAllEntries, cx: &mut ViewContext<Self>) {
-        self.expanded_dir_ids.clear();
-        self.update_visible_entries(None, cx);
-        cx.notify();
-    }
-
-    fn toggle_expanded(&mut self, entry_id: ProjectEntryId, cx: &mut ViewContext<Self>) {
-        if let Some(worktree_id) = self.project.read(cx).worktree_id_for_entry(entry_id, cx) {
-            if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) {
-                self.project.update(cx, |project, cx| {
-                    match expanded_dir_ids.binary_search(&entry_id) {
-                        Ok(ix) => {
-                            expanded_dir_ids.remove(ix);
-                        }
-                        Err(ix) => {
-                            project.expand_entry(worktree_id, entry_id, cx);
-                            expanded_dir_ids.insert(ix, entry_id);
-                        }
-                    }
-                });
-                self.update_visible_entries(Some((worktree_id, entry_id)), cx);
-                cx.focus(&self.focus_handle);
-                cx.notify();
-            }
-        }
-    }
-
-    fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
-        if let Some(selection) = self.selection {
-            let (mut worktree_ix, mut entry_ix, _) =
-                self.index_for_selection(selection).unwrap_or_default();
-            if entry_ix > 0 {
-                entry_ix -= 1;
-            } else if worktree_ix > 0 {
-                worktree_ix -= 1;
-                entry_ix = self.visible_entries[worktree_ix].1.len() - 1;
-            } else {
-                return;
-            }
-
-            let (worktree_id, worktree_entries) = &self.visible_entries[worktree_ix];
-            self.selection = Some(Selection {
-                worktree_id: *worktree_id,
-                entry_id: worktree_entries[entry_ix].id,
-            });
-            self.autoscroll(cx);
-            cx.notify();
-        } else {
-            self.select_first(cx);
-        }
-    }
-
-    fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
-        if let Some(task) = self.confirm_edit(cx) {
-            task.detach_and_log_err(cx);
-        }
-    }
-
-    fn open_file(&mut self, _: &Open, cx: &mut ViewContext<Self>) {
-        if let Some((_, entry)) = self.selected_entry(cx) {
-            if entry.is_file() {
-                self.open_entry(entry.id, true, cx);
-            }
-        }
-    }
-
-    fn confirm_edit(&mut self, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
-        let edit_state = self.edit_state.as_mut()?;
-        cx.focus(&self.focus_handle);
-
-        let worktree_id = edit_state.worktree_id;
-        let is_new_entry = edit_state.is_new_entry;
-        let is_dir = edit_state.is_dir;
-        let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?;
-        let entry = worktree.read(cx).entry_for_id(edit_state.entry_id)?.clone();
-        let filename = self.filename_editor.read(cx).text(cx);
-
-        let path_already_exists = |path| worktree.read(cx).entry_for_path(path).is_some();
-        let edit_task;
-        let edited_entry_id;
-        if is_new_entry {
-            self.selection = Some(Selection {
-                worktree_id,
-                entry_id: NEW_ENTRY_ID,
-            });
-            let new_path = entry.path.join(&filename.trim_start_matches("/"));
-            if path_already_exists(new_path.as_path()) {
-                return None;
-            }
-
-            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)
-            } else {
-                filename.clone().into()
-            };
-            if path_already_exists(new_path.as_path()) {
-                return None;
-            }
-
-            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);
-        cx.notify();
-
-        Some(cx.spawn(|this, mut cx| async move {
-            let new_entry = edit_task.await;
-            this.update(&mut cx, |this, cx| {
-                this.edit_state.take();
-                cx.notify();
-            })?;
-
-            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();
-                })?;
-            }
-            Ok(())
-        }))
-    }
-
-    fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
-        self.edit_state = None;
-        self.update_visible_entries(None, cx);
-        cx.focus(&self.focus_handle);
-        cx.notify();
-    }
-
-    fn open_entry(
-        &mut self,
-        entry_id: ProjectEntryId,
-        focus_opened_item: bool,
-        cx: &mut ViewContext<Self>,
-    ) {
-        cx.emit(Event::OpenedEntry {
-            entry_id,
-            focus_opened_item,
-        });
-    }
-
-    fn split_entry(&mut self, entry_id: ProjectEntryId, cx: &mut ViewContext<Self>) {
-        cx.emit(Event::SplitEntry { entry_id });
-    }
-
-    fn new_file(&mut self, _: &NewFile, cx: &mut ViewContext<Self>) {
-        self.add_entry(false, cx)
-    }
-
-    fn new_directory(&mut self, _: &NewDirectory, cx: &mut ViewContext<Self>) {
-        self.add_entry(true, cx)
-    }
-
-    fn add_entry(&mut self, is_dir: bool, cx: &mut ViewContext<Self>) {
-        if let Some(Selection {
-            worktree_id,
-            entry_id,
-        }) = self.selection
-        {
-            let directory_id;
-            if let Some((worktree, expanded_dir_ids)) = self
-                .project
-                .read(cx)
-                .worktree_for_id(worktree_id, cx)
-                .zip(self.expanded_dir_ids.get_mut(&worktree_id))
-            {
-                let worktree = worktree.read(cx);
-                if let Some(mut entry) = worktree.entry_for_id(entry_id) {
-                    loop {
-                        if entry.is_dir() {
-                            if let Err(ix) = expanded_dir_ids.binary_search(&entry.id) {
-                                expanded_dir_ids.insert(ix, entry.id);
-                            }
-                            directory_id = entry.id;
-                            break;
-                        } else {
-                            if let Some(parent_path) = entry.path.parent() {
-                                if let Some(parent_entry) = worktree.entry_for_path(parent_path) {
-                                    entry = parent_entry;
-                                    continue;
-                                }
-                            }
-                            return;
-                        }
-                    }
-                } else {
-                    return;
-                };
-            } else {
-                return;
-            };
-
-            self.edit_state = Some(EditState {
-                worktree_id,
-                entry_id: directory_id,
-                is_new_entry: true,
-                is_dir,
-                processing_filename: None,
-            });
-            self.filename_editor.update(cx, |editor, cx| {
-                editor.clear(cx);
-                editor.focus(cx);
-            });
-            self.update_visible_entries(Some((worktree_id, NEW_ENTRY_ID)), cx);
-            self.autoscroll(cx);
-            cx.notify();
-        }
-    }
-
-    fn rename(&mut self, _: &Rename, cx: &mut ViewContext<Self>) {
-        if let Some(Selection {
-            worktree_id,
-            entry_id,
-        }) = self.selection
-        {
-            if let Some(worktree) = self.project.read(cx).worktree_for_id(worktree_id, cx) {
-                if let Some(entry) = worktree.read(cx).entry_for_id(entry_id) {
-                    self.edit_state = Some(EditState {
-                        worktree_id,
-                        entry_id,
-                        is_new_entry: false,
-                        is_dir: entry.is_dir(),
-                        processing_filename: None,
-                    });
-                    let file_name = entry
-                        .path
-                        .file_name()
-                        .map(|s| s.to_string_lossy())
-                        .unwrap_or_default()
-                        .to_string();
-                    let file_stem = entry.path.file_stem().map(|s| s.to_string_lossy());
-                    let selection_end =
-                        file_stem.map_or(file_name.len(), |file_stem| file_stem.len());
-                    self.filename_editor.update(cx, |editor, cx| {
-                        editor.set_text(file_name, cx);
-                        editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
-                            s.select_ranges([0..selection_end])
-                        });
-                        editor.focus(cx);
-                    });
-                    self.update_visible_entries(None, cx);
-                    self.autoscroll(cx);
-                    cx.notify();
-                }
-            }
-
-            // cx.update_global(|drag_and_drop: &mut DragAndDrop<Workspace>, cx| {
-            //     drag_and_drop.cancel_dragging::<ProjectEntryId>(cx);
-            // })
-        }
-    }
-
-    fn delete(&mut self, _: &Delete, cx: &mut ViewContext<Self>) {
-        maybe!({
-            let Selection { entry_id, .. } = self.selection?;
-            let path = self.project.read(cx).path_for_entry(entry_id, cx)?.path;
-            let file_name = path.file_name()?;
-
-            let answer = cx.prompt(
-                PromptLevel::Info,
-                &format!("Delete {file_name:?}?"),
-                &["Delete", "Cancel"],
-            );
-
-            cx.spawn(|this, mut cx| async move {
-                if answer.await != Ok(0) {
-                    return Ok(());
-                }
-                this.update(&mut cx, |this, cx| {
-                    this.project
-                        .update(cx, |project, cx| project.delete_entry(entry_id, cx))
-                        .ok_or_else(|| anyhow!("no such entry"))
-                })??
-                .await
-            })
-            .detach_and_log_err(cx);
-            Some(())
-        });
-    }
-
-    fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
-        if let Some(selection) = self.selection {
-            let (mut worktree_ix, mut entry_ix, _) =
-                self.index_for_selection(selection).unwrap_or_default();
-            if let Some((_, worktree_entries)) = self.visible_entries.get(worktree_ix) {
-                if entry_ix + 1 < worktree_entries.len() {
-                    entry_ix += 1;
-                } else {
-                    worktree_ix += 1;
-                    entry_ix = 0;
-                }
-            }
-
-            if let Some((worktree_id, worktree_entries)) = self.visible_entries.get(worktree_ix) {
-                if let Some(entry) = worktree_entries.get(entry_ix) {
-                    self.selection = Some(Selection {
-                        worktree_id: *worktree_id,
-                        entry_id: entry.id,
-                    });
-                    self.autoscroll(cx);
-                    cx.notify();
-                }
-            }
-        } else {
-            self.select_first(cx);
-        }
-    }
-
-    fn select_first(&mut self, cx: &mut ViewContext<Self>) {
-        let worktree = self
-            .visible_entries
-            .first()
-            .and_then(|(worktree_id, _)| self.project.read(cx).worktree_for_id(*worktree_id, cx));
-        if let Some(worktree) = worktree {
-            let worktree = worktree.read(cx);
-            let worktree_id = worktree.id();
-            if let Some(root_entry) = worktree.root_entry() {
-                self.selection = Some(Selection {
-                    worktree_id,
-                    entry_id: root_entry.id,
-                });
-                self.autoscroll(cx);
-                cx.notify();
-            }
-        }
-    }
-
-    fn autoscroll(&mut self, cx: &mut ViewContext<Self>) {
-        if let Some((_, _, index)) = self.selection.and_then(|s| self.index_for_selection(s)) {
-            self.list.scroll_to_item(index);
-            cx.notify();
-        }
-    }
-
-    fn cut(&mut self, _: &Cut, cx: &mut ViewContext<Self>) {
-        if let Some((worktree, entry)) = self.selected_entry(cx) {
-            self.clipboard_entry = Some(ClipboardEntry::Cut {
-                worktree_id: worktree.id(),
-                entry_id: entry.id,
-            });
-            cx.notify();
-        }
-    }
-
-    fn copy(&mut self, _: &Copy, cx: &mut ViewContext<Self>) {
-        if let Some((worktree, entry)) = self.selected_entry(cx) {
-            self.clipboard_entry = Some(ClipboardEntry::Copied {
-                worktree_id: worktree.id(),
-                entry_id: entry.id,
-            });
-            cx.notify();
-        }
-    }
-
-    fn paste(&mut self, _: &Paste, cx: &mut ViewContext<Self>) {
-        maybe!({
-            let (worktree, entry) = self.selected_entry(cx)?;
-            let clipboard_entry = self.clipboard_entry?;
-            if clipboard_entry.worktree_id() != worktree.id() {
-                return None;
-            }
-
-            let clipboard_entry_file_name = self
-                .project
-                .read(cx)
-                .path_for_entry(clipboard_entry.entry_id(), cx)?
-                .path
-                .file_name()?
-                .to_os_string();
-
-            let mut new_path = entry.path.to_path_buf();
-            if entry.is_file() {
-                new_path.pop();
-            }
-
-            new_path.push(&clipboard_entry_file_name);
-            let extension = new_path.extension().map(|e| e.to_os_string());
-            let file_name_without_extension = Path::new(&clipboard_entry_file_name).file_stem()?;
-            let mut ix = 0;
-            while worktree.entry_for_path(&new_path).is_some() {
-                new_path.pop();
-
-                let mut new_file_name = file_name_without_extension.to_os_string();
-                new_file_name.push(" copy");
-                if ix > 0 {
-                    new_file_name.push(format!(" {}", ix));
-                }
-                if let Some(extension) = extension.as_ref() {
-                    new_file_name.push(".");
-                    new_file_name.push(extension);
-                }
-
-                new_path.push(new_file_name);
-                ix += 1;
-            }
-
-            if clipboard_entry.is_cut() {
-                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(())
-        });
-    }
-
-    fn copy_path(&mut self, _: &CopyPath, cx: &mut ViewContext<Self>) {
-        if let Some((worktree, entry)) = self.selected_entry(cx) {
-            cx.write_to_clipboard(ClipboardItem::new(
-                worktree
-                    .abs_path()
-                    .join(&entry.path)
-                    .to_string_lossy()
-                    .to_string(),
-            ));
-        }
-    }
-
-    fn copy_relative_path(&mut self, _: &CopyRelativePath, cx: &mut ViewContext<Self>) {
-        if let Some((_, entry)) = self.selected_entry(cx) {
-            cx.write_to_clipboard(ClipboardItem::new(entry.path.to_string_lossy().to_string()));
-        }
-    }
-
-    fn reveal_in_finder(&mut self, _: &RevealInFinder, cx: &mut ViewContext<Self>) {
-        if let Some((worktree, entry)) = self.selected_entry(cx) {
-            cx.reveal_path(&worktree.abs_path().join(&entry.path));
-        }
-    }
-
-    fn open_in_terminal(&mut self, _: &OpenInTerminal, _cx: &mut ViewContext<Self>) {
-        todo!()
-        // if let Some((worktree, entry)) = self.selected_entry(cx) {
-        //     let window = cx.window();
-        //     let view_id = cx.view_id();
-        //     let path = worktree.abs_path().join(&entry.path);
-
-        //     cx.app_context()
-        //         .spawn(|mut cx| async move {
-        //             window.dispatch_action(
-        //                 view_id,
-        //                 &workspace::OpenTerminal {
-        //                     working_directory: path,
-        //                 },
-        //                 &mut cx,
-        //             );
-        //         })
-        //         .detach();
-        // }
-    }
-
-    pub fn new_search_in_directory(
-        &mut self,
-        _: &NewSearchInDirectory,
-        cx: &mut ViewContext<Self>,
-    ) {
-        if let Some((_, entry)) = self.selected_entry(cx) {
-            if entry.is_dir() {
-                let entry = entry.clone();
-                self.workspace
-                    .update(cx, |workspace, cx| {
-                        search::ProjectSearchView::new_search_in_directory(workspace, &entry, cx);
-                    })
-                    .ok();
-            }
-        }
-    }
-
-    fn move_entry(
-        &mut self,
-        entry_to_move: ProjectEntryId,
-        destination: ProjectEntryId,
-        destination_is_file: bool,
-        cx: &mut ViewContext<Self>,
-    ) {
-        let destination_worktree = self.project.update(cx, |project, cx| {
-            let entry_path = project.path_for_entry(entry_to_move, cx)?;
-            let destination_entry_path = project.path_for_entry(destination, cx)?.path.clone();
-
-            let mut destination_path = destination_entry_path.as_ref();
-            if destination_is_file {
-                destination_path = destination_path.parent()?;
-            }
-
-            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);
-                cx.foreground_executor().spawn(task).detach_and_log_err(cx);
-            }
-
-            Some(project.worktree_id_for_entry(destination, cx)?)
-        });
-
-        if let Some(destination_worktree) = destination_worktree {
-            self.expand_entry(destination_worktree, destination, cx);
-        }
-    }
-
-    fn index_for_selection(&self, selection: Selection) -> Option<(usize, usize, usize)> {
-        let mut entry_index = 0;
-        let mut visible_entries_index = 0;
-        for (worktree_index, (worktree_id, worktree_entries)) in
-            self.visible_entries.iter().enumerate()
-        {
-            if *worktree_id == selection.worktree_id {
-                for entry in worktree_entries {
-                    if entry.id == selection.entry_id {
-                        return Some((worktree_index, entry_index, visible_entries_index));
-                    } else {
-                        visible_entries_index += 1;
-                        entry_index += 1;
-                    }
-                }
-                break;
-            } else {
-                visible_entries_index += worktree_entries.len();
-            }
-        }
-        None
-    }
-
-    pub fn selected_entry<'a>(
-        &self,
-        cx: &'a AppContext,
-    ) -> Option<(&'a Worktree, &'a project::Entry)> {
-        let (worktree, entry) = self.selected_entry_handle(cx)?;
-        Some((worktree.read(cx), entry))
-    }
-
-    fn selected_entry_handle<'a>(
-        &self,
-        cx: &'a AppContext,
-    ) -> Option<(Model<Worktree>, &'a project::Entry)> {
-        let selection = self.selection?;
-        let project = self.project.read(cx);
-        let worktree = project.worktree_for_id(selection.worktree_id, cx)?;
-        let entry = worktree.read(cx).entry_for_id(selection.entry_id)?;
-        Some((worktree, entry))
-    }
-
-    fn expand_to_selection(&mut self, cx: &mut ViewContext<Self>) -> Option<()> {
-        let (worktree, entry) = self.selected_entry(cx)?;
-        let expanded_dir_ids = self.expanded_dir_ids.entry(worktree.id()).or_default();
-
-        for path in entry.path.ancestors() {
-            let Some(entry) = worktree.entry_for_path(path) else {
-                continue;
-            };
-            if entry.is_dir() {
-                if let Err(idx) = expanded_dir_ids.binary_search(&entry.id) {
-                    expanded_dir_ids.insert(idx, entry.id);
-                }
-            }
-        }
-
-        Some(())
-    }
-
-    fn update_visible_entries(
-        &mut self,
-        new_selected_entry: Option<(WorktreeId, ProjectEntryId)>,
-        cx: &mut ViewContext<Self>,
-    ) {
-        let project = self.project.read(cx);
-        self.last_worktree_root_id = project
-            .visible_worktrees(cx)
-            .rev()
-            .next()
-            .and_then(|worktree| worktree.read(cx).root_entry())
-            .map(|entry| entry.id);
-
-        self.visible_entries.clear();
-        for worktree in project.visible_worktrees(cx) {
-            let snapshot = worktree.read(cx).snapshot();
-            let worktree_id = snapshot.id();
-
-            let expanded_dir_ids = match self.expanded_dir_ids.entry(worktree_id) {
-                hash_map::Entry::Occupied(e) => e.into_mut(),
-                hash_map::Entry::Vacant(e) => {
-                    // The first time a worktree's root entry becomes available,
-                    // mark that root entry as expanded.
-                    if let Some(entry) = snapshot.root_entry() {
-                        e.insert(vec![entry.id]).as_slice()
-                    } else {
-                        &[]
-                    }
-                }
-            };
-
-            let mut new_entry_parent_id = None;
-            let mut new_entry_kind = EntryKind::Dir;
-            if let Some(edit_state) = &self.edit_state {
-                if edit_state.worktree_id == worktree_id && edit_state.is_new_entry {
-                    new_entry_parent_id = Some(edit_state.entry_id);
-                    new_entry_kind = if edit_state.is_dir {
-                        EntryKind::Dir
-                    } else {
-                        EntryKind::File(Default::default())
-                    };
-                }
-            }
-
-            let mut visible_worktree_entries = Vec::new();
-            let mut entry_iter = snapshot.entries(true);
-
-            while let Some(entry) = entry_iter.entry() {
-                visible_worktree_entries.push(entry.clone());
-                if Some(entry.id) == new_entry_parent_id {
-                    visible_worktree_entries.push(Entry {
-                        id: NEW_ENTRY_ID,
-                        kind: new_entry_kind,
-                        path: entry.path.join("\0").into(),
-                        inode: 0,
-                        mtime: entry.mtime,
-                        is_symlink: false,
-                        is_ignored: false,
-                        is_external: false,
-                        git_status: entry.git_status,
-                    });
-                }
-                if expanded_dir_ids.binary_search(&entry.id).is_err()
-                    && entry_iter.advance_to_sibling()
-                {
-                    continue;
-                }
-                entry_iter.advance();
-            }
-
-            snapshot.propagate_git_statuses(&mut visible_worktree_entries);
-
-            visible_worktree_entries.sort_by(|entry_a, entry_b| {
-                let mut components_a = entry_a.path.components().peekable();
-                let mut components_b = entry_b.path.components().peekable();
-                loop {
-                    match (components_a.next(), components_b.next()) {
-                        (Some(component_a), Some(component_b)) => {
-                            let a_is_file = components_a.peek().is_none() && entry_a.is_file();
-                            let b_is_file = components_b.peek().is_none() && entry_b.is_file();
-                            let ordering = a_is_file.cmp(&b_is_file).then_with(|| {
-                                let name_a =
-                                    UniCase::new(component_a.as_os_str().to_string_lossy());
-                                let name_b =
-                                    UniCase::new(component_b.as_os_str().to_string_lossy());
-                                name_a.cmp(&name_b)
-                            });
-                            if !ordering.is_eq() {
-                                return ordering;
-                            }
-                        }
-                        (Some(_), None) => break Ordering::Greater,
-                        (None, Some(_)) => break Ordering::Less,
-                        (None, None) => break Ordering::Equal,
-                    }
-                }
-            });
-            self.visible_entries
-                .push((worktree_id, visible_worktree_entries));
-        }
-
-        if let Some((worktree_id, entry_id)) = new_selected_entry {
-            self.selection = Some(Selection {
-                worktree_id,
-                entry_id,
-            });
-        }
-    }
-
-    fn expand_entry(
-        &mut self,
-        worktree_id: WorktreeId,
-        entry_id: ProjectEntryId,
-        cx: &mut ViewContext<Self>,
-    ) {
-        self.project.update(cx, |project, cx| {
-            if let Some((worktree, expanded_dir_ids)) = project
-                .worktree_for_id(worktree_id, cx)
-                .zip(self.expanded_dir_ids.get_mut(&worktree_id))
-            {
-                project.expand_entry(worktree_id, entry_id, cx);
-                let worktree = worktree.read(cx);
-
-                if let Some(mut entry) = worktree.entry_for_id(entry_id) {
-                    loop {
-                        if let Err(ix) = expanded_dir_ids.binary_search(&entry.id) {
-                            expanded_dir_ids.insert(ix, entry.id);
-                        }
-
-                        if let Some(parent_entry) =
-                            entry.path.parent().and_then(|p| worktree.entry_for_path(p))
-                        {
-                            entry = parent_entry;
-                        } else {
-                            break;
-                        }
-                    }
-                }
-            }
-        });
-    }
-
-    fn for_each_visible_entry(
-        &self,
-        range: Range<usize>,
-        cx: &mut ViewContext<ProjectPanel>,
-        mut callback: impl FnMut(ProjectEntryId, EntryDetails, &mut ViewContext<ProjectPanel>),
-    ) {
-        let mut ix = 0;
-        for (worktree_id, visible_worktree_entries) in &self.visible_entries {
-            if ix >= range.end {
-                return;
-            }
-
-            if ix + visible_worktree_entries.len() <= range.start {
-                ix += visible_worktree_entries.len();
-                continue;
-            }
-
-            let end_ix = range.end.min(ix + visible_worktree_entries.len());
-            let (git_status_setting, show_file_icons, show_folder_icons) = {
-                let settings = ProjectPanelSettings::get_global(cx);
-                (
-                    settings.git_status,
-                    settings.file_icons,
-                    settings.folder_icons,
-                )
-            };
-            if let Some(worktree) = self.project.read(cx).worktree_for_id(*worktree_id, cx) {
-                let snapshot = worktree.read(cx).snapshot();
-                let root_name = OsStr::new(snapshot.root_name());
-                let expanded_entry_ids = self
-                    .expanded_dir_ids
-                    .get(&snapshot.id())
-                    .map(Vec::as_slice)
-                    .unwrap_or(&[]);
-
-                let entry_range = range.start.saturating_sub(ix)..end_ix - ix;
-                for entry in visible_worktree_entries[entry_range].iter() {
-                    let status = git_status_setting.then(|| entry.git_status).flatten();
-                    let is_expanded = expanded_entry_ids.binary_search(&entry.id).is_ok();
-                    let icon = match entry.kind {
-                        EntryKind::File(_) => {
-                            if show_file_icons {
-                                FileAssociations::get_icon(&entry.path, cx)
-                            } else {
-                                None
-                            }
-                        }
-                        _ => {
-                            if show_folder_icons {
-                                FileAssociations::get_folder_icon(is_expanded, cx)
-                            } else {
-                                FileAssociations::get_chevron_icon(is_expanded, cx)
-                            }
-                        }
-                    };
-
-                    let mut details = EntryDetails {
-                        filename: entry
-                            .path
-                            .file_name()
-                            .unwrap_or(root_name)
-                            .to_string_lossy()
-                            .to_string(),
-                        icon,
-                        path: entry.path.clone(),
-                        depth: entry.path.components().count(),
-                        kind: entry.kind,
-                        is_ignored: entry.is_ignored,
-                        is_expanded,
-                        is_selected: self.selection.map_or(false, |e| {
-                            e.worktree_id == snapshot.id() && e.entry_id == entry.id
-                        }),
-                        is_editing: false,
-                        is_processing: false,
-                        is_cut: self
-                            .clipboard_entry
-                            .map_or(false, |e| e.is_cut() && e.entry_id() == entry.id),
-                        git_status: status,
-                    };
-
-                    if let Some(edit_state) = &self.edit_state {
-                        let is_edited_entry = if edit_state.is_new_entry {
-                            entry.id == NEW_ENTRY_ID
-                        } else {
-                            entry.id == edit_state.entry_id
-                        };
-
-                        if is_edited_entry {
-                            if let Some(processing_filename) = &edit_state.processing_filename {
-                                details.is_processing = true;
-                                details.filename.clear();
-                                details.filename.push_str(processing_filename);
-                            } else {
-                                if edit_state.is_new_entry {
-                                    details.filename.clear();
-                                }
-                                details.is_editing = true;
-                            }
-                        }
-                    }
-
-                    callback(entry.id, details, cx);
-                }
-            }
-            ix = end_ix;
-        }
-    }
-
-    fn render_entry(
-        &self,
-        entry_id: ProjectEntryId,
-        details: EntryDetails,
-        cx: &mut ViewContext<Self>,
-    ) -> Stateful<Div> {
-        let kind = details.kind;
-        let settings = ProjectPanelSettings::get_global(cx);
-        let show_editor = details.is_editing && !details.is_processing;
-        let is_selected = self
-            .selection
-            .map_or(false, |selection| selection.entry_id == entry_id);
-        let width = self.width.unwrap_or(px(0.));
-
-        let filename_text_color = details
-            .git_status
-            .as_ref()
-            .map(|status| match status {
-                GitFileStatus::Added => Color::Created,
-                GitFileStatus::Modified => Color::Modified,
-                GitFileStatus::Conflict => Color::Conflict,
-            })
-            .unwrap_or(if is_selected {
-                Color::Default
-            } else {
-                Color::Muted
-            });
-
-        let file_name = details.filename.clone();
-        let icon = details.icon.clone();
-        let depth = details.depth;
-        div()
-            .id(entry_id.to_proto() as usize)
-            .on_drag(entry_id, move |entry_id, cx| {
-                cx.new_view(|_| DraggedProjectEntryView {
-                    details: details.clone(),
-                    width,
-                    entry_id: *entry_id,
-                })
-            })
-            .drag_over::<ProjectEntryId>(|style| {
-                style.bg(cx.theme().colors().drop_target_background)
-            })
-            .on_drop(cx.listener(move |this, dragged_id: &ProjectEntryId, cx| {
-                this.move_entry(*dragged_id, entry_id, kind.is_file(), cx);
-            }))
-            .child(
-                ListItem::new(entry_id.to_proto() as usize)
-                    .indent_level(depth)
-                    .indent_step_size(px(settings.indent_size))
-                    .selected(is_selected)
-                    .child(if let Some(icon) = &icon {
-                        div().child(IconElement::from_path(icon.to_string()).color(Color::Muted))
-                    } else {
-                        div()
-                    })
-                    .child(
-                        if let (Some(editor), true) = (Some(&self.filename_editor), show_editor) {
-                            div().h_full().w_full().child(editor.clone())
-                        } else {
-                            div().child(Label::new(file_name).color(filename_text_color))
-                        }
-                        .ml_1(),
-                    )
-                    .on_click(cx.listener(move |this, event: &gpui::ClickEvent, cx| {
-                        if event.down.button == MouseButton::Right {
-                            return;
-                        }
-                        if !show_editor {
-                            if kind.is_dir() {
-                                this.toggle_expanded(entry_id, cx);
-                            } else {
-                                if event.down.modifiers.command {
-                                    this.split_entry(entry_id, cx);
-                                } else {
-                                    this.open_entry(entry_id, event.up.click_count > 1, cx);
-                                }
-                            }
-                        }
-                    }))
-                    .on_secondary_mouse_down(cx.listener(
-                        move |this, event: &MouseDownEvent, cx| {
-                            this.deploy_context_menu(event.position, entry_id, cx);
-                        },
-                    )),
-            )
-    }
-
-    fn dispatch_context(&self, cx: &ViewContext<Self>) -> KeyContext {
-        let mut dispatch_context = KeyContext::default();
-        dispatch_context.add("ProjectPanel");
-        dispatch_context.add("menu");
-
-        let identifier = if self.filename_editor.focus_handle(cx).is_focused(cx) {
-            "editing"
-        } else {
-            "not_editing"
-        };
-
-        dispatch_context.add(identifier);
-        dispatch_context
-    }
-
-    fn reveal_entry(
-        &mut self,
-        project: Model<Project>,
-        entry_id: ProjectEntryId,
-        skip_ignored: bool,
-        cx: &mut ViewContext<'_, ProjectPanel>,
-    ) {
-        if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) {
-            let worktree = worktree.read(cx);
-            if skip_ignored
-                && worktree
-                    .entry_for_id(entry_id)
-                    .map_or(true, |entry| entry.is_ignored)
-            {
-                return;
-            }
-
-            let worktree_id = worktree.id();
-            self.expand_entry(worktree_id, entry_id, cx);
-            self.update_visible_entries(Some((worktree_id, entry_id)), cx);
-            self.autoscroll(cx);
-            cx.notify();
-        }
-    }
-}
-
-impl Render for ProjectPanel {
-    fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl IntoElement {
-        let has_worktree = self.visible_entries.len() != 0;
-
-        if has_worktree {
-            div()
-                .id("project-panel")
-                .size_full()
-                .relative()
-                .key_context(self.dispatch_context(cx))
-                .on_action(cx.listener(Self::select_next))
-                .on_action(cx.listener(Self::select_prev))
-                .on_action(cx.listener(Self::expand_selected_entry))
-                .on_action(cx.listener(Self::collapse_selected_entry))
-                .on_action(cx.listener(Self::collapse_all_entries))
-                .on_action(cx.listener(Self::new_file))
-                .on_action(cx.listener(Self::new_directory))
-                .on_action(cx.listener(Self::rename))
-                .on_action(cx.listener(Self::delete))
-                .on_action(cx.listener(Self::confirm))
-                .on_action(cx.listener(Self::open_file))
-                .on_action(cx.listener(Self::cancel))
-                .on_action(cx.listener(Self::cut))
-                .on_action(cx.listener(Self::copy))
-                .on_action(cx.listener(Self::copy_path))
-                .on_action(cx.listener(Self::copy_relative_path))
-                .on_action(cx.listener(Self::paste))
-                .on_action(cx.listener(Self::reveal_in_finder))
-                .on_action(cx.listener(Self::open_in_terminal))
-                .on_action(cx.listener(Self::new_search_in_directory))
-                .track_focus(&self.focus_handle)
-                .child(
-                    uniform_list(
-                        cx.view().clone(),
-                        "entries",
-                        self.visible_entries
-                            .iter()
-                            .map(|(_, worktree_entries)| worktree_entries.len())
-                            .sum(),
-                        {
-                            |this, range, cx| {
-                                let mut items = Vec::new();
-                                this.for_each_visible_entry(range, cx, |id, details, cx| {
-                                    items.push(this.render_entry(id, details, cx));
-                                });
-                                items
-                            }
-                        },
-                    )
-                    .size_full()
-                    .track_scroll(self.list.clone()),
-                )
-                .children(self.context_menu.as_ref().map(|(menu, position, _)| {
-                    overlay()
-                        .position(*position)
-                        .anchor(gpui::AnchorCorner::TopLeft)
-                        .child(menu.clone())
-                }))
-        } else {
-            v_stack()
-                .id("empty-project_panel")
-                .track_focus(&self.focus_handle)
-        }
-    }
-}
-
-impl Render for DraggedProjectEntryView {
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl Element {
-        let settings = ProjectPanelSettings::get_global(cx);
-        let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone();
-        h_stack()
-            .font(ui_font)
-            .bg(cx.theme().colors().background)
-            .w(self.width)
-            .child(
-                ListItem::new(self.entry_id.to_proto() as usize)
-                    .indent_level(self.details.depth)
-                    .indent_step_size(px(settings.indent_size))
-                    .child(if let Some(icon) = &self.details.icon {
-                        div().child(IconElement::from_path(icon.to_string()))
-                    } else {
-                        div()
-                    })
-                    .child(Label::new(self.details.filename.clone())),
-            )
-    }
-}
-
-impl EventEmitter<Event> for ProjectPanel {}
-
-impl EventEmitter<PanelEvent> for ProjectPanel {}
-
-impl Panel for ProjectPanel {
-    fn position(&self, cx: &WindowContext) -> DockPosition {
-        match ProjectPanelSettings::get_global(cx).dock {
-            ProjectPanelDockPosition::Left => DockPosition::Left,
-            ProjectPanelDockPosition::Right => DockPosition::Right,
-        }
-    }
-
-    fn position_is_valid(&self, position: DockPosition) -> bool {
-        matches!(position, DockPosition::Left | DockPosition::Right)
-    }
-
-    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
-        settings::update_settings_file::<ProjectPanelSettings>(
-            self.fs.clone(),
-            cx,
-            move |settings| {
-                let dock = match position {
-                    DockPosition::Left | DockPosition::Bottom => ProjectPanelDockPosition::Left,
-                    DockPosition::Right => ProjectPanelDockPosition::Right,
-                };
-                settings.dock = Some(dock);
-            },
-        );
-    }
-
-    fn size(&self, cx: &WindowContext) -> Pixels {
-        self.width
-            .unwrap_or_else(|| ProjectPanelSettings::get_global(cx).default_width)
-    }
-
-    fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
-        self.width = size;
-        self.serialize(cx);
-        cx.notify();
-    }
-
-    fn icon(&self, _: &WindowContext) -> Option<ui::Icon> {
-        Some(ui::Icon::FileTree)
-    }
-
-    fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
-        Some("Project Panel")
-    }
-
-    fn toggle_action(&self) -> Box<dyn Action> {
-        Box::new(ToggleFocus)
-    }
-
-    fn persistent_name() -> &'static str {
-        "Project Panel"
-    }
-}
-
-impl FocusableView for ProjectPanel {
-    fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
-        self.focus_handle.clone()
-    }
-}
-
-impl ClipboardEntry {
-    fn is_cut(&self) -> bool {
-        matches!(self, Self::Cut { .. })
-    }
-
-    fn entry_id(&self) -> ProjectEntryId {
-        match self {
-            ClipboardEntry::Copied { entry_id, .. } | ClipboardEntry::Cut { entry_id, .. } => {
-                *entry_id
-            }
-        }
-    }
-
-    fn worktree_id(&self) -> WorktreeId {
-        match self {
-            ClipboardEntry::Copied { worktree_id, .. }
-            | ClipboardEntry::Cut { worktree_id, .. } => *worktree_id,
-        }
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use gpui::{TestAppContext, View, VisualTestContext, WindowHandle};
-    use pretty_assertions::assert_eq;
-    use project::{project_settings::ProjectSettings, FakeFs};
-    use serde_json::json;
-    use settings::SettingsStore;
-    use std::{
-        collections::HashSet,
-        path::{Path, PathBuf},
-    };
-    use workspace::AppState;
-
-    #[gpui::test]
-    async fn test_visible_list(cx: &mut gpui::TestAppContext) {
-        init_test(cx);
-
-        let fs = FakeFs::new(cx.executor().clone());
-        fs.insert_tree(
-            "/root1",
-            json!({
-                ".dockerignore": "",
-                ".git": {
-                    "HEAD": "",
-                },
-                "a": {
-                    "0": { "q": "", "r": "", "s": "" },
-                    "1": { "t": "", "u": "" },
-                    "2": { "v": "", "w": "", "x": "", "y": "" },
-                },
-                "b": {
-                    "3": { "Q": "" },
-                    "4": { "R": "", "S": "", "T": "", "U": "" },
-                },
-                "C": {
-                    "5": {},
-                    "6": { "V": "", "W": "" },
-                    "7": { "X": "" },
-                    "8": { "Y": {}, "Z": "" }
-                }
-            }),
-        )
-        .await;
-        fs.insert_tree(
-            "/root2",
-            json!({
-                "d": {
-                    "9": ""
-                },
-                "e": {}
-            }),
-        )
-        .await;
-
-        let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
-        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
-        let cx = &mut VisualTestContext::from_window(*workspace, cx);
-        let panel = workspace
-            .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx))
-            .unwrap();
-        assert_eq!(
-            visible_entries_as_strings(&panel, 0..50, cx),
-            &[
-                "v root1",
-                "    > .git",
-                "    > a",
-                "    > b",
-                "    > C",
-                "      .dockerignore",
-                "v root2",
-                "    > d",
-                "    > e",
-            ]
-        );
-
-        toggle_expand_dir(&panel, "root1/b", cx);
-        assert_eq!(
-            visible_entries_as_strings(&panel, 0..50, cx),
-            &[
-                "v root1",
-                "    > .git",
-                "    > a",
-                "    v b  <== selected",
-                "        > 3",
-                "        > 4",
-                "    > C",
-                "      .dockerignore",
-                "v root2",
-                "    > d",
-                "    > e",
-            ]
-        );
-
-        assert_eq!(
-            visible_entries_as_strings(&panel, 6..9, cx),
-            &[
-                //
-                "    > C",
-                "      .dockerignore",
-                "v root2",
-            ]
-        );
-    }
-
-    #[gpui::test]
-    async fn test_exclusions_in_visible_list(cx: &mut gpui::TestAppContext) {
-        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!["**/.git".to_string(), "**/4/**".to_string()]);
-                });
-            });
-        });
-
-        let fs = FakeFs::new(cx.background_executor.clone());
-        fs.insert_tree(
-            "/root1",
-            json!({
-                ".dockerignore": "",
-                ".git": {
-                    "HEAD": "",
-                },
-                "a": {
-                    "0": { "q": "", "r": "", "s": "" },
-                    "1": { "t": "", "u": "" },
-                    "2": { "v": "", "w": "", "x": "", "y": "" },
-                },
-                "b": {
-                    "3": { "Q": "" },
-                    "4": { "R": "", "S": "", "T": "", "U": "" },
-                },
-                "C": {
-                    "5": {},
-                    "6": { "V": "", "W": "" },
-                    "7": { "X": "" },
-                    "8": { "Y": {}, "Z": "" }
-                }
-            }),
-        )
-        .await;
-        fs.insert_tree(
-            "/root2",
-            json!({
-                "d": {
-                    "4": ""
-                },
-                "e": {}
-            }),
-        )
-        .await;
-
-        let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
-        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
-        let cx = &mut VisualTestContext::from_window(*workspace, cx);
-        let panel = workspace
-            .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx))
-            .unwrap();
-        assert_eq!(
-            visible_entries_as_strings(&panel, 0..50, cx),
-            &[
-                "v root1",
-                "    > a",
-                "    > b",
-                "    > C",
-                "      .dockerignore",
-                "v root2",
-                "    > d",
-                "    > e",
-            ]
-        );
-
-        toggle_expand_dir(&panel, "root1/b", cx);
-        assert_eq!(
-            visible_entries_as_strings(&panel, 0..50, cx),
-            &[
-                "v root1",
-                "    > a",
-                "    v b  <== selected",
-                "        > 3",
-                "    > C",
-                "      .dockerignore",
-                "v root2",
-                "    > d",
-                "    > e",
-            ]
-        );
-
-        toggle_expand_dir(&panel, "root2/d", cx);
-        assert_eq!(
-            visible_entries_as_strings(&panel, 0..50, cx),
-            &[
-                "v root1",
-                "    > a",
-                "    v b",
-                "        > 3",
-                "    > C",
-                "      .dockerignore",
-                "v root2",
-                "    v d  <== selected",
-                "    > e",
-            ]
-        );
-
-        toggle_expand_dir(&panel, "root2/e", cx);
-        assert_eq!(
-            visible_entries_as_strings(&panel, 0..50, cx),
-            &[
-                "v root1",
-                "    > a",
-                "    v b",
-                "        > 3",
-                "    > C",
-                "      .dockerignore",
-                "v root2",
-                "    v d",
-                "    v e  <== selected",
-            ]
-        );
-    }
-
-    #[gpui::test(iterations = 30)]
-    async fn test_editing_files(cx: &mut gpui::TestAppContext) {
-        init_test(cx);
-
-        let fs = FakeFs::new(cx.executor().clone());
-        fs.insert_tree(
-            "/root1",
-            json!({
-                ".dockerignore": "",
-                ".git": {
-                    "HEAD": "",
-                },
-                "a": {
-                    "0": { "q": "", "r": "", "s": "" },
-                    "1": { "t": "", "u": "" },
-                    "2": { "v": "", "w": "", "x": "", "y": "" },
-                },
-                "b": {
-                    "3": { "Q": "" },
-                    "4": { "R": "", "S": "", "T": "", "U": "" },
-                },
-                "C": {
-                    "5": {},
-                    "6": { "V": "", "W": "" },
-                    "7": { "X": "" },
-                    "8": { "Y": {}, "Z": "" }
-                }
-            }),
-        )
-        .await;
-        fs.insert_tree(
-            "/root2",
-            json!({
-                "d": {
-                    "9": ""
-                },
-                "e": {}
-            }),
-        )
-        .await;
-
-        let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
-        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
-        let cx = &mut VisualTestContext::from_window(*workspace, cx);
-        let panel = workspace
-            .update(cx, |workspace, cx| {
-                let panel = ProjectPanel::new(workspace, cx);
-                workspace.add_panel(panel.clone(), cx);
-                workspace.toggle_dock(panel.read(cx).position(cx), cx);
-                panel
-            })
-            .unwrap();
-
-        select_path(&panel, "root1", cx);
-        assert_eq!(
-            visible_entries_as_strings(&panel, 0..10, cx),
-            &[
-                "v root1  <== selected",
-                "    > .git",
-                "    > a",
-                "    > b",
-                "    > C",
-                "      .dockerignore",
-                "v root2",
-                "    > d",
-                "    > e",
-            ]
-        );
-
-        // Add a file with the root folder selected. The filename editor is placed
-        // before the first file in the root folder.
-        panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx));
-        panel.update(cx, |panel, cx| {
-            assert!(panel.filename_editor.read(cx).is_focused(cx));
-        });
-        assert_eq!(
-            visible_entries_as_strings(&panel, 0..10, cx),
-            &[
-                "v root1",
-                "    > .git",
-                "    > a",
-                "    > b",
-                "    > C",
-                "      [EDITOR: '']  <== selected",
-                "      .dockerignore",
-                "v root2",
-                "    > d",
-                "    > e",
-            ]
-        );
-
-        let confirm = panel.update(cx, |panel, cx| {
-            panel
-                .filename_editor
-                .update(cx, |editor, cx| editor.set_text("the-new-filename", cx));
-            panel.confirm_edit(cx).unwrap()
-        });
-        assert_eq!(
-            visible_entries_as_strings(&panel, 0..10, cx),
-            &[
-                "v root1",
-                "    > .git",
-                "    > a",
-                "    > b",
-                "    > C",
-                "      [PROCESSING: 'the-new-filename']  <== selected",
-                "      .dockerignore",
-                "v root2",
-                "    > d",
-                "    > e",
-            ]
-        );
-
-        confirm.await.unwrap();
-        assert_eq!(
-            visible_entries_as_strings(&panel, 0..10, cx),
-            &[
-                "v root1",
-                "    > .git",
-                "    > a",
-                "    > b",
-                "    > C",
-                "      .dockerignore",
-                "      the-new-filename  <== selected",
-                "v root2",
-                "    > d",
-                "    > e",
-            ]
-        );
-
-        select_path(&panel, "root1/b", cx);
-        panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx));
-        assert_eq!(
-            visible_entries_as_strings(&panel, 0..10, cx),
-            &[
-                "v root1",
-                "    > .git",
-                "    > a",
-                "    v b",
-                "        > 3",
-                "        > 4",
-                "          [EDITOR: '']  <== selected",
-                "    > C",
-                "      .dockerignore",
-                "      the-new-filename",
-            ]
-        );
-
-        panel
-            .update(cx, |panel, cx| {
-                panel
-                    .filename_editor
-                    .update(cx, |editor, cx| editor.set_text("another-filename.txt", cx));
-                panel.confirm_edit(cx).unwrap()
-            })
-            .await
-            .unwrap();
-        assert_eq!(
-            visible_entries_as_strings(&panel, 0..10, cx),
-            &[
-                "v root1",
-                "    > .git",
-                "    > a",
-                "    v b",
-                "        > 3",
-                "        > 4",
-                "          another-filename.txt  <== selected",
-                "    > C",
-                "      .dockerignore",
-                "      the-new-filename",
-            ]
-        );
-
-        select_path(&panel, "root1/b/another-filename.txt", cx);
-        panel.update(cx, |panel, cx| panel.rename(&Rename, cx));
-        assert_eq!(
-            visible_entries_as_strings(&panel, 0..10, cx),
-            &[
-                "v root1",
-                "    > .git",
-                "    > a",
-                "    v b",
-                "        > 3",
-                "        > 4",
-                "          [EDITOR: 'another-filename.txt']  <== selected",
-                "    > C",
-                "      .dockerignore",
-                "      the-new-filename",
-            ]
-        );
-
-        let confirm = panel.update(cx, |panel, cx| {
-            panel.filename_editor.update(cx, |editor, cx| {
-                let file_name_selections = editor.selections.all::<usize>(cx);
-                assert_eq!(file_name_selections.len(), 1, "File editing should have a single selection, but got: {file_name_selections:?}");
-                let file_name_selection = &file_name_selections[0];
-                assert_eq!(file_name_selection.start, 0, "Should select the file name from the start");
-                assert_eq!(file_name_selection.end, "another-filename".len(), "Should not select file extension");
-
-                editor.set_text("a-different-filename.tar.gz", cx)
-            });
-            panel.confirm_edit(cx).unwrap()
-        });
-        assert_eq!(
-            visible_entries_as_strings(&panel, 0..10, cx),
-            &[
-                "v root1",
-                "    > .git",
-                "    > a",
-                "    v b",
-                "        > 3",
-                "        > 4",
-                "          [PROCESSING: 'a-different-filename.tar.gz']  <== selected",
-                "    > C",
-                "      .dockerignore",
-                "      the-new-filename",
-            ]
-        );
-
-        confirm.await.unwrap();
-        assert_eq!(
-            visible_entries_as_strings(&panel, 0..10, cx),
-            &[
-                "v root1",
-                "    > .git",
-                "    > a",
-                "    v b",
-                "        > 3",
-                "        > 4",
-                "          a-different-filename.tar.gz  <== selected",
-                "    > C",
-                "      .dockerignore",
-                "      the-new-filename",
-            ]
-        );
-
-        panel.update(cx, |panel, cx| panel.rename(&Rename, cx));
-        assert_eq!(
-            visible_entries_as_strings(&panel, 0..10, cx),
-            &[
-                "v root1",
-                "    > .git",
-                "    > a",
-                "    v b",
-                "        > 3",
-                "        > 4",
-                "          [EDITOR: 'a-different-filename.tar.gz']  <== selected",
-                "    > C",
-                "      .dockerignore",
-                "      the-new-filename",
-            ]
-        );
-
-        panel.update(cx, |panel, cx| {
-            panel.filename_editor.update(cx, |editor, cx| {
-                let file_name_selections = editor.selections.all::<usize>(cx);
-                assert_eq!(file_name_selections.len(), 1, "File editing should have a single selection, but got: {file_name_selections:?}");
-                let file_name_selection = &file_name_selections[0];
-                assert_eq!(file_name_selection.start, 0, "Should select the file name from the start");
-                assert_eq!(file_name_selection.end, "a-different-filename.tar".len(), "Should not select file extension, but still may select anything up to the last dot..");
-
-            });
-            panel.cancel(&Cancel, cx)
-        });
-
-        panel.update(cx, |panel, cx| panel.new_directory(&NewDirectory, cx));
-        assert_eq!(
-            visible_entries_as_strings(&panel, 0..10, cx),
-            &[
-                "v root1",
-                "    > .git",
-                "    > a",
-                "    v b",
-                "        > [EDITOR: '']  <== selected",
-                "        > 3",
-                "        > 4",
-                "          a-different-filename.tar.gz",
-                "    > C",
-                "      .dockerignore",
-            ]
-        );
-
-        let confirm = panel.update(cx, |panel, cx| {
-            panel
-                .filename_editor
-                .update(cx, |editor, cx| editor.set_text("new-dir", cx));
-            panel.confirm_edit(cx).unwrap()
-        });
-        panel.update(cx, |panel, cx| panel.select_next(&Default::default(), cx));
-        assert_eq!(
-            visible_entries_as_strings(&panel, 0..10, cx),
-            &[
-                "v root1",
-                "    > .git",
-                "    > a",
-                "    v b",
-                "        > [PROCESSING: 'new-dir']",
-                "        > 3  <== selected",
-                "        > 4",
-                "          a-different-filename.tar.gz",
-                "    > C",
-                "      .dockerignore",
-            ]
-        );
-
-        confirm.await.unwrap();
-        assert_eq!(
-            visible_entries_as_strings(&panel, 0..10, cx),
-            &[
-                "v root1",
-                "    > .git",
-                "    > a",
-                "    v b",
-                "        > 3  <== selected",
-                "        > 4",
-                "        > new-dir",
-                "          a-different-filename.tar.gz",
-                "    > C",
-                "      .dockerignore",
-            ]
-        );
-
-        panel.update(cx, |panel, cx| panel.rename(&Default::default(), cx));
-        assert_eq!(
-            visible_entries_as_strings(&panel, 0..10, cx),
-            &[
-                "v root1",
-                "    > .git",
-                "    > a",
-                "    v b",
-                "        > [EDITOR: '3']  <== selected",
-                "        > 4",
-                "        > new-dir",
-                "          a-different-filename.tar.gz",
-                "    > C",
-                "      .dockerignore",
-            ]
-        );
-
-        // Dismiss the rename editor when it loses focus.
-        workspace.update(cx, |_, cx| cx.blur()).unwrap();
-        assert_eq!(
-            visible_entries_as_strings(&panel, 0..10, cx),
-            &[
-                "v root1",
-                "    > .git",
-                "    > a",
-                "    v b",
-                "        > 3  <== selected",
-                "        > 4",
-                "        > new-dir",
-                "          a-different-filename.tar.gz",
-                "    > C",
-                "      .dockerignore",
-            ]
-        );
-    }
-
-    #[gpui::test(iterations = 10)]
-    async fn test_adding_directories_via_file(cx: &mut gpui::TestAppContext) {
-        init_test(cx);
-
-        let fs = FakeFs::new(cx.executor().clone());
-        fs.insert_tree(
-            "/root1",
-            json!({
-                ".dockerignore": "",
-                ".git": {
-                    "HEAD": "",
-                },
-                "a": {
-                    "0": { "q": "", "r": "", "s": "" },
-                    "1": { "t": "", "u": "" },
-                    "2": { "v": "", "w": "", "x": "", "y": "" },
-                },
-                "b": {
-                    "3": { "Q": "" },
-                    "4": { "R": "", "S": "", "T": "", "U": "" },
-                },
-                "C": {
-                    "5": {},
-                    "6": { "V": "", "W": "" },
-                    "7": { "X": "" },
-                    "8": { "Y": {}, "Z": "" }
-                }
-            }),
-        )
-        .await;
-        fs.insert_tree(
-            "/root2",
-            json!({
-                "d": {
-                    "9": ""
-                },
-                "e": {}
-            }),
-        )
-        .await;
-
-        let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
-        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
-        let cx = &mut VisualTestContext::from_window(*workspace, cx);
-        let panel = workspace
-            .update(cx, |workspace, cx| {
-                let panel = ProjectPanel::new(workspace, cx);
-                workspace.add_panel(panel.clone(), cx);
-                workspace.toggle_dock(panel.read(cx).position(cx), cx);
-                panel
-            })
-            .unwrap();
-
-        select_path(&panel, "root1", cx);
-        assert_eq!(
-            visible_entries_as_strings(&panel, 0..10, cx),
-            &[
-                "v root1  <== selected",
-                "    > .git",
-                "    > a",
-                "    > b",
-                "    > C",
-                "      .dockerignore",
-                "v root2",
-                "    > d",
-                "    > e",
-            ]
-        );
-
-        // Add a file with the root folder selected. The filename editor is placed
-        // before the first file in the root folder.
-        panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx));
-        panel.update(cx, |panel, cx| {
-            assert!(panel.filename_editor.read(cx).is_focused(cx));
-        });
-        assert_eq!(
-            visible_entries_as_strings(&panel, 0..10, cx),
-            &[
-                "v root1",
-                "    > .git",
-                "    > a",
-                "    > b",
-                "    > C",
-                "      [EDITOR: '']  <== selected",
-                "      .dockerignore",
-                "v root2",
-                "    > d",
-                "    > e",
-            ]
-        );
-
-        let confirm = panel.update(cx, |panel, cx| {
-            panel.filename_editor.update(cx, |editor, cx| {
-                editor.set_text("/bdir1/dir2/the-new-filename", cx)
-            });
-            panel.confirm_edit(cx).unwrap()
-        });
-
-        assert_eq!(
-            visible_entries_as_strings(&panel, 0..10, cx),
-            &[
-                "v root1",
-                "    > .git",
-                "    > a",
-                "    > b",
-                "    > C",
-                "      [PROCESSING: '/bdir1/dir2/the-new-filename']  <== selected",
-                "      .dockerignore",
-                "v root2",
-                "    > d",
-                "    > e",
-            ]
-        );
-
-        confirm.await.unwrap();
-        assert_eq!(
-            visible_entries_as_strings(&panel, 0..13, cx),
-            &[
-                "v root1",
-                "    > .git",
-                "    > a",
-                "    > b",
-                "    v bdir1",
-                "        v dir2",
-                "              the-new-filename  <== selected",
-                "    > C",
-                "      .dockerignore",
-                "v root2",
-                "    > d",
-                "    > e",
-            ]
-        );
-    }
-
-    #[gpui::test]
-    async fn test_copy_paste(cx: &mut gpui::TestAppContext) {
-        init_test(cx);
-
-        let fs = FakeFs::new(cx.executor().clone());
-        fs.insert_tree(
-            "/root1",
-            json!({
-                "one.two.txt": "",
-                "one.txt": ""
-            }),
-        )
-        .await;
-
-        let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await;
-        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
-        let cx = &mut VisualTestContext::from_window(*workspace, cx);
-        let panel = workspace
-            .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx))
-            .unwrap();
-
-        panel.update(cx, |panel, cx| {
-            panel.select_next(&Default::default(), cx);
-            panel.select_next(&Default::default(), cx);
-        });
-
-        assert_eq!(
-            visible_entries_as_strings(&panel, 0..50, cx),
-            &[
-                //
-                "v root1",
-                "      one.two.txt  <== selected",
-                "      one.txt",
-            ]
-        );
-
-        // Regression test - file name is created correctly when
-        // the copied file's name contains multiple dots.
-        panel.update(cx, |panel, cx| {
-            panel.copy(&Default::default(), cx);
-            panel.paste(&Default::default(), cx);
-        });
-        cx.executor().run_until_parked();
-
-        assert_eq!(
-            visible_entries_as_strings(&panel, 0..50, cx),
-            &[
-                //
-                "v root1",
-                "      one.two copy.txt",
-                "      one.two.txt  <== selected",
-                "      one.txt",
-            ]
-        );
-
-        panel.update(cx, |panel, cx| {
-            panel.paste(&Default::default(), cx);
-        });
-        cx.executor().run_until_parked();
-
-        assert_eq!(
-            visible_entries_as_strings(&panel, 0..50, cx),
-            &[
-                //
-                "v root1",
-                "      one.two copy 1.txt",
-                "      one.two copy.txt",
-                "      one.two.txt  <== selected",
-                "      one.txt",
-            ]
-        );
-    }
-
-    #[gpui::test]
-    async fn test_remove_opened_file(cx: &mut gpui::TestAppContext) {
-        init_test_with_editor(cx);
-
-        let fs = FakeFs::new(cx.executor().clone());
-        fs.insert_tree(
-            "/src",
-            json!({
-                "test": {
-                    "first.rs": "// First Rust file",
-                    "second.rs": "// Second Rust file",
-                    "third.rs": "// Third Rust file",
-                }
-            }),
-        )
-        .await;
-
-        let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
-        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
-        let cx = &mut VisualTestContext::from_window(*workspace, cx);
-        let panel = workspace
-            .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx))
-            .unwrap();
-
-        toggle_expand_dir(&panel, "src/test", cx);
-        select_path(&panel, "src/test/first.rs", cx);
-        panel.update(cx, |panel, cx| panel.open_file(&Open, cx));
-        cx.executor().run_until_parked();
-        assert_eq!(
-            visible_entries_as_strings(&panel, 0..10, cx),
-            &[
-                "v src",
-                "    v test",
-                "          first.rs  <== selected",
-                "          second.rs",
-                "          third.rs"
-            ]
-        );
-        ensure_single_file_is_opened(&workspace, "test/first.rs", cx);
-
-        submit_deletion(&panel, cx);
-        assert_eq!(
-            visible_entries_as_strings(&panel, 0..10, cx),
-            &[
-                "v src",
-                "    v test",
-                "          second.rs",
-                "          third.rs"
-            ],
-            "Project panel should have no deleted file, no other file is selected in it"
-        );
-        ensure_no_open_items_and_panes(&workspace, cx);
-
-        select_path(&panel, "src/test/second.rs", cx);
-        panel.update(cx, |panel, cx| panel.open_file(&Open, cx));
-        cx.executor().run_until_parked();
-        assert_eq!(
-            visible_entries_as_strings(&panel, 0..10, cx),
-            &[
-                "v src",
-                "    v test",
-                "          second.rs  <== selected",
-                "          third.rs"
-            ]
-        );
-        ensure_single_file_is_opened(&workspace, "test/second.rs", cx);
-
-        workspace
-            .update(cx, |workspace, cx| {
-                let active_items = workspace
-                    .panes()
-                    .iter()
-                    .filter_map(|pane| pane.read(cx).active_item())
-                    .collect::<Vec<_>>();
-                assert_eq!(active_items.len(), 1);
-                let open_editor = active_items
-                    .into_iter()
-                    .next()
-                    .unwrap()
-                    .downcast::<Editor>()
-                    .expect("Open item should be an editor");
-                open_editor.update(cx, |editor, cx| editor.set_text("Another text!", cx));
-            })
-            .unwrap();
-        submit_deletion(&panel, cx);
-        assert_eq!(
-            visible_entries_as_strings(&panel, 0..10, cx),
-            &["v src", "    v test", "          third.rs"],
-            "Project panel should have no deleted file, with one last file remaining"
-        );
-        ensure_no_open_items_and_panes(&workspace, cx);
-    }
-
-    #[gpui::test]
-    async fn test_create_duplicate_items(cx: &mut gpui::TestAppContext) {
-        init_test_with_editor(cx);
-
-        let fs = FakeFs::new(cx.executor().clone());
-        fs.insert_tree(
-            "/src",
-            json!({
-                "test": {
-                    "first.rs": "// First Rust file",
-                    "second.rs": "// Second Rust file",
-                    "third.rs": "// Third Rust file",
-                }
-            }),
-        )
-        .await;
-
-        let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await;
-        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
-        let cx = &mut VisualTestContext::from_window(*workspace, cx);
-        let panel = workspace
-            .update(cx, |workspace, cx| {
-                let panel = ProjectPanel::new(workspace, cx);
-                workspace.add_panel(panel.clone(), cx);
-                workspace.toggle_dock(panel.read(cx).position(cx), cx);
-                panel
-            })
-            .unwrap();
-
-        select_path(&panel, "src/", cx);
-        panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
-        cx.executor().run_until_parked();
-        assert_eq!(
-            visible_entries_as_strings(&panel, 0..10, cx),
-            &[
-                //
-                "v src  <== selected",
-                "    > test"
-            ]
-        );
-        panel.update(cx, |panel, cx| panel.new_directory(&NewDirectory, cx));
-        panel.update(cx, |panel, cx| {
-            assert!(panel.filename_editor.read(cx).is_focused(cx));
-        });
-        assert_eq!(
-            visible_entries_as_strings(&panel, 0..10, cx),
-            &[
-                //
-                "v src",
-                "    > [EDITOR: '']  <== selected",
-                "    > test"
-            ]
-        );
-        panel.update(cx, |panel, cx| {
-            panel
-                .filename_editor
-                .update(cx, |editor, cx| editor.set_text("test", cx));
-            assert!(
-                panel.confirm_edit(cx).is_none(),
-                "Should not allow to confirm on conflicting new directory name"
-            )
-        });
-        assert_eq!(
-            visible_entries_as_strings(&panel, 0..10, cx),
-            &[
-                //
-                "v src",
-                "    > test"
-            ],
-            "File list should be unchanged after failed folder create confirmation"
-        );
-
-        select_path(&panel, "src/test/", cx);
-        panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
-        cx.executor().run_until_parked();
-        assert_eq!(
-            visible_entries_as_strings(&panel, 0..10, cx),
-            &[
-                //
-                "v src",
-                "    > test  <== selected"
-            ]
-        );
-        panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx));
-        panel.update(cx, |panel, cx| {
-            assert!(panel.filename_editor.read(cx).is_focused(cx));
-        });
-        assert_eq!(
-            visible_entries_as_strings(&panel, 0..10, cx),
-            &[
-                "v src",
-                "    v test",
-                "          [EDITOR: '']  <== selected",
-                "          first.rs",
-                "          second.rs",
-                "          third.rs"
-            ]
-        );
-        panel.update(cx, |panel, cx| {
-            panel
-                .filename_editor
-                .update(cx, |editor, cx| editor.set_text("first.rs", cx));
-            assert!(
-                panel.confirm_edit(cx).is_none(),
-                "Should not allow to confirm on conflicting new file name"
-            )
-        });
-        assert_eq!(
-            visible_entries_as_strings(&panel, 0..10, cx),
-            &[
-                "v src",
-                "    v test",
-                "          first.rs",
-                "          second.rs",
-                "          third.rs"
-            ],
-            "File list should be unchanged after failed file create confirmation"
-        );
-
-        select_path(&panel, "src/test/first.rs", cx);
-        panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
-        cx.executor().run_until_parked();
-        assert_eq!(
-            visible_entries_as_strings(&panel, 0..10, cx),
-            &[
-                "v src",
-                "    v test",
-                "          first.rs  <== selected",
-                "          second.rs",
-                "          third.rs"
-            ],
-        );
-        panel.update(cx, |panel, cx| panel.rename(&Rename, cx));
-        panel.update(cx, |panel, cx| {
-            assert!(panel.filename_editor.read(cx).is_focused(cx));
-        });
-        assert_eq!(
-            visible_entries_as_strings(&panel, 0..10, cx),
-            &[
-                "v src",
-                "    v test",
-                "          [EDITOR: 'first.rs']  <== selected",
-                "          second.rs",
-                "          third.rs"
-            ]
-        );
-        panel.update(cx, |panel, cx| {
-            panel
-                .filename_editor
-                .update(cx, |editor, cx| editor.set_text("second.rs", cx));
-            assert!(
-                panel.confirm_edit(cx).is_none(),
-                "Should not allow to confirm on conflicting file rename"
-            )
-        });
-        assert_eq!(
-            visible_entries_as_strings(&panel, 0..10, cx),
-            &[
-                "v src",
-                "    v test",
-                "          first.rs  <== selected",
-                "          second.rs",
-                "          third.rs"
-            ],
-            "File list should be unchanged after failed rename confirmation"
-        );
-    }
-
-    #[gpui::test]
-    async fn test_collapse_all_entries(cx: &mut gpui::TestAppContext) {
-        init_test_with_editor(cx);
-
-        let fs = FakeFs::new(cx.executor().clone());
-        fs.insert_tree(
-            "/project_root",
-            json!({
-                "dir_1": {
-                    "nested_dir": {
-                        "file_a.py": "# File contents",
-                        "file_b.py": "# File contents",
-                        "file_c.py": "# File contents",
-                    },
-                    "file_1.py": "# File contents",
-                    "file_2.py": "# File contents",
-                    "file_3.py": "# File contents",
-                },
-                "dir_2": {
-                    "file_1.py": "# File contents",
-                    "file_2.py": "# File contents",
-                    "file_3.py": "# File contents",
-                }
-            }),
-        )
-        .await;
-
-        let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
-        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
-        let cx = &mut VisualTestContext::from_window(*workspace, cx);
-        let panel = workspace
-            .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx))
-            .unwrap();
-
-        panel.update(cx, |panel, cx| {
-            panel.collapse_all_entries(&CollapseAllEntries, cx)
-        });
-        cx.executor().run_until_parked();
-        assert_eq!(
-            visible_entries_as_strings(&panel, 0..10, cx),
-            &["v project_root", "    > dir_1", "    > dir_2",]
-        );
-
-        // Open dir_1 and make sure nested_dir was collapsed when running collapse_all_entries
-        toggle_expand_dir(&panel, "project_root/dir_1", cx);
-        cx.executor().run_until_parked();
-        assert_eq!(
-            visible_entries_as_strings(&panel, 0..10, cx),
-            &[
-                "v project_root",
-                "    v dir_1  <== selected",
-                "        > nested_dir",
-                "          file_1.py",
-                "          file_2.py",
-                "          file_3.py",
-                "    > dir_2",
-            ]
-        );
-    }
-
-    #[gpui::test]
-    async fn test_new_file_move(cx: &mut gpui::TestAppContext) {
-        init_test(cx);
-
-        let fs = FakeFs::new(cx.executor().clone());
-        fs.as_fake().insert_tree("/root", json!({})).await;
-        let project = Project::test(fs, ["/root".as_ref()], cx).await;
-        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
-        let cx = &mut VisualTestContext::from_window(*workspace, cx);
-        let panel = workspace
-            .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx))
-            .unwrap();
-
-        // Make a new buffer with no backing file
-        workspace
-            .update(cx, |workspace, cx| {
-                Editor::new_file(workspace, &Default::default(), cx)
-            })
-            .unwrap();
-
-        // "Save as"" the buffer, creating a new backing file for it
-        let save_task = workspace
-            .update(cx, |workspace, cx| {
-                workspace.save_active_item(workspace::SaveIntent::Save, cx)
-            })
-            .unwrap();
-
-        cx.executor().run_until_parked();
-        cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/new")));
-        save_task.await.unwrap();
-
-        // Rename the file
-        select_path(&panel, "root/new", cx);
-        assert_eq!(
-            visible_entries_as_strings(&panel, 0..10, cx),
-            &["v root", "      new  <== selected"]
-        );
-        panel.update(cx, |panel, cx| panel.rename(&Rename, cx));
-        panel.update(cx, |panel, cx| {
-            panel
-                .filename_editor
-                .update(cx, |editor, cx| editor.set_text("newer", cx));
-        });
-        panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
-
-        cx.executor().run_until_parked();
-        assert_eq!(
-            visible_entries_as_strings(&panel, 0..10, cx),
-            &["v root", "      newer  <== selected"]
-        );
-
-        workspace
-            .update(cx, |workspace, cx| {
-                workspace.save_active_item(workspace::SaveIntent::Save, cx)
-            })
-            .unwrap()
-            .await
-            .unwrap();
-
-        cx.executor().run_until_parked();
-        // assert that saving the file doesn't restore "new"
-        assert_eq!(
-            visible_entries_as_strings(&panel, 0..10, cx),
-            &["v root", "      newer  <== selected"]
-        );
-    }
-
-    #[gpui::test]
-    async fn test_autoreveal_and_gitignored_files(cx: &mut gpui::TestAppContext) {
-        init_test_with_editor(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::new());
-                });
-                store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
-                    project_panel_settings.auto_reveal_entries = Some(false)
-                });
-            })
-        });
-
-        let fs = FakeFs::new(cx.background_executor.clone());
-        fs.insert_tree(
-            "/project_root",
-            json!({
-                ".git": {},
-                ".gitignore": "**/gitignored_dir",
-                "dir_1": {
-                    "file_1.py": "# File 1_1 contents",
-                    "file_2.py": "# File 1_2 contents",
-                    "file_3.py": "# File 1_3 contents",
-                    "gitignored_dir": {
-                        "file_a.py": "# File contents",
-                        "file_b.py": "# File contents",
-                        "file_c.py": "# File contents",
-                    },
-                },
-                "dir_2": {
-                    "file_1.py": "# File 2_1 contents",
-                    "file_2.py": "# File 2_2 contents",
-                    "file_3.py": "# File 2_3 contents",
-                }
-            }),
-        )
-        .await;
-
-        let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
-        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
-        let cx = &mut VisualTestContext::from_window(*workspace, cx);
-        let panel = workspace
-            .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx))
-            .unwrap();
-
-        assert_eq!(
-            visible_entries_as_strings(&panel, 0..20, cx),
-            &[
-                "v project_root",
-                "    > .git",
-                "    > dir_1",
-                "    > dir_2",
-                "      .gitignore",
-            ]
-        );
-
-        let dir_1_file = find_project_entry(&panel, "project_root/dir_1/file_1.py", cx)
-            .expect("dir 1 file is not ignored and should have an entry");
-        let dir_2_file = find_project_entry(&panel, "project_root/dir_2/file_1.py", cx)
-            .expect("dir 2 file is not ignored and should have an entry");
-        let gitignored_dir_file =
-            find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx);
-        assert_eq!(
-            gitignored_dir_file, None,
-            "File in the gitignored dir should not have an entry before its dir is toggled"
-        );
-
-        toggle_expand_dir(&panel, "project_root/dir_1", cx);
-        toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
-        cx.executor().run_until_parked();
-        assert_eq!(
-            visible_entries_as_strings(&panel, 0..20, cx),
-            &[
-                "v project_root",
-                "    > .git",
-                "    v dir_1",
-                "        v gitignored_dir  <== selected",
-                "              file_a.py",
-                "              file_b.py",
-                "              file_c.py",
-                "          file_1.py",
-                "          file_2.py",
-                "          file_3.py",
-                "    > dir_2",
-                "      .gitignore",
-            ],
-            "Should show gitignored dir file list in the project panel"
-        );
-        let gitignored_dir_file =
-            find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx)
-                .expect("after gitignored dir got opened, a file entry should be present");
-
-        toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
-        toggle_expand_dir(&panel, "project_root/dir_1", cx);
-        assert_eq!(
-            visible_entries_as_strings(&panel, 0..20, cx),
-            &[
-                "v project_root",
-                "    > .git",
-                "    > dir_1  <== selected",
-                "    > dir_2",
-                "      .gitignore",
-            ],
-            "Should hide all dir contents again and prepare for the auto reveal test"
-        );
-
-        for file_entry in [dir_1_file, dir_2_file, gitignored_dir_file] {
-            panel.update(cx, |panel, cx| {
-                panel.project.update(cx, |_, cx| {
-                    cx.emit(project::Event::ActiveEntryChanged(Some(file_entry)))
-                })
-            });
-            cx.run_until_parked();
-            assert_eq!(
-                visible_entries_as_strings(&panel, 0..20, cx),
-                &[
-                    "v project_root",
-                    "    > .git",
-                    "    > dir_1  <== selected",
-                    "    > dir_2",
-                    "      .gitignore",
-                ],
-                "When no auto reveal is enabled, the selected entry should not be revealed in the project panel"
-            );
-        }
-
-        cx.update(|cx| {
-            cx.update_global::<SettingsStore, _>(|store, cx| {
-                store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
-                    project_panel_settings.auto_reveal_entries = Some(true)
-                });
-            })
-        });
-
-        panel.update(cx, |panel, cx| {
-            panel.project.update(cx, |_, cx| {
-                cx.emit(project::Event::ActiveEntryChanged(Some(dir_1_file)))
-            })
-        });
-        cx.run_until_parked();
-        assert_eq!(
-            visible_entries_as_strings(&panel, 0..20, cx),
-            &[
-                "v project_root",
-                "    > .git",
-                "    v dir_1",
-                "        > gitignored_dir",
-                "          file_1.py  <== selected",
-                "          file_2.py",
-                "          file_3.py",
-                "    > dir_2",
-                "      .gitignore",
-            ],
-            "When auto reveal is enabled, not ignored dir_1 entry should be revealed"
-        );
-
-        panel.update(cx, |panel, cx| {
-            panel.project.update(cx, |_, cx| {
-                cx.emit(project::Event::ActiveEntryChanged(Some(dir_2_file)))
-            })
-        });
-        cx.run_until_parked();
-        assert_eq!(
-            visible_entries_as_strings(&panel, 0..20, cx),
-            &[
-                "v project_root",
-                "    > .git",
-                "    v dir_1",
-                "        > gitignored_dir",
-                "          file_1.py",
-                "          file_2.py",
-                "          file_3.py",
-                "    v dir_2",
-                "          file_1.py  <== selected",
-                "          file_2.py",
-                "          file_3.py",
-                "      .gitignore",
-            ],
-            "When auto reveal is enabled, not ignored dir_2 entry should be revealed"
-        );
-
-        panel.update(cx, |panel, cx| {
-            panel.project.update(cx, |_, cx| {
-                cx.emit(project::Event::ActiveEntryChanged(Some(
-                    gitignored_dir_file,
-                )))
-            })
-        });
-        cx.run_until_parked();
-        assert_eq!(
-            visible_entries_as_strings(&panel, 0..20, cx),
-            &[
-                "v project_root",
-                "    > .git",
-                "    v dir_1",
-                "        > gitignored_dir",
-                "          file_1.py",
-                "          file_2.py",
-                "          file_3.py",
-                "    v dir_2",
-                "          file_1.py  <== selected",
-                "          file_2.py",
-                "          file_3.py",
-                "      .gitignore",
-            ],
-            "When auto reveal is enabled, a gitignored selected entry should not be revealed in the project panel"
-        );
-
-        panel.update(cx, |panel, cx| {
-            panel.project.update(cx, |_, cx| {
-                cx.emit(project::Event::RevealInProjectPanel(gitignored_dir_file))
-            })
-        });
-        cx.run_until_parked();
-        assert_eq!(
-            visible_entries_as_strings(&panel, 0..20, cx),
-            &[
-                "v project_root",
-                "    > .git",
-                "    v dir_1",
-                "        v gitignored_dir",
-                "              file_a.py  <== selected",
-                "              file_b.py",
-                "              file_c.py",
-                "          file_1.py",
-                "          file_2.py",
-                "          file_3.py",
-                "    v dir_2",
-                "          file_1.py",
-                "          file_2.py",
-                "          file_3.py",
-                "      .gitignore",
-            ],
-            "When a gitignored entry is explicitly revealed, it should be shown in the project tree"
-        );
-    }
-
-    #[gpui::test]
-    async fn test_explicit_reveal(cx: &mut gpui::TestAppContext) {
-        init_test_with_editor(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::new());
-                });
-                store.update_user_settings::<ProjectPanelSettings>(cx, |project_panel_settings| {
-                    project_panel_settings.auto_reveal_entries = Some(false)
-                });
-            })
-        });
-
-        let fs = FakeFs::new(cx.background_executor.clone());
-        fs.insert_tree(
-            "/project_root",
-            json!({
-                ".git": {},
-                ".gitignore": "**/gitignored_dir",
-                "dir_1": {
-                    "file_1.py": "# File 1_1 contents",
-                    "file_2.py": "# File 1_2 contents",
-                    "file_3.py": "# File 1_3 contents",
-                    "gitignored_dir": {
-                        "file_a.py": "# File contents",
-                        "file_b.py": "# File contents",
-                        "file_c.py": "# File contents",
-                    },
-                },
-                "dir_2": {
-                    "file_1.py": "# File 2_1 contents",
-                    "file_2.py": "# File 2_2 contents",
-                    "file_3.py": "# File 2_3 contents",
-                }
-            }),
-        )
-        .await;
-
-        let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await;
-        let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
-        let cx = &mut VisualTestContext::from_window(*workspace, cx);
-        let panel = workspace
-            .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx))
-            .unwrap();
-
-        assert_eq!(
-            visible_entries_as_strings(&panel, 0..20, cx),
-            &[
-                "v project_root",
-                "    > .git",
-                "    > dir_1",
-                "    > dir_2",
-                "      .gitignore",
-            ]
-        );
-
-        let dir_1_file = find_project_entry(&panel, "project_root/dir_1/file_1.py", cx)
-            .expect("dir 1 file is not ignored and should have an entry");
-        let dir_2_file = find_project_entry(&panel, "project_root/dir_2/file_1.py", cx)
-            .expect("dir 2 file is not ignored and should have an entry");
-        let gitignored_dir_file =
-            find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx);
-        assert_eq!(
-            gitignored_dir_file, None,
-            "File in the gitignored dir should not have an entry before its dir is toggled"
-        );
-
-        toggle_expand_dir(&panel, "project_root/dir_1", cx);
-        toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
-        cx.run_until_parked();
-        assert_eq!(
-            visible_entries_as_strings(&panel, 0..20, cx),
-            &[
-                "v project_root",
-                "    > .git",
-                "    v dir_1",
-                "        v gitignored_dir  <== selected",
-                "              file_a.py",
-                "              file_b.py",
-                "              file_c.py",
-                "          file_1.py",
-                "          file_2.py",
-                "          file_3.py",
-                "    > dir_2",
-                "      .gitignore",
-            ],
-            "Should show gitignored dir file list in the project panel"
-        );
-        let gitignored_dir_file =
-            find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx)
-                .expect("after gitignored dir got opened, a file entry should be present");
-
-        toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx);
-        toggle_expand_dir(&panel, "project_root/dir_1", cx);
-        assert_eq!(
-            visible_entries_as_strings(&panel, 0..20, cx),
-            &[
-                "v project_root",
-                "    > .git",
-                "    > dir_1  <== selected",
-                "    > dir_2",
-                "      .gitignore",
-            ],
-            "Should hide all dir contents again and prepare for the explicit reveal test"
-        );
-
-        for file_entry in [dir_1_file, dir_2_file, gitignored_dir_file] {
-            panel.update(cx, |panel, cx| {
-                panel.project.update(cx, |_, cx| {
-                    cx.emit(project::Event::ActiveEntryChanged(Some(file_entry)))
-                })
-            });
-            cx.run_until_parked();
-            assert_eq!(
-                visible_entries_as_strings(&panel, 0..20, cx),
-                &[
-                    "v project_root",
-                    "    > .git",
-                    "    > dir_1  <== selected",
-                    "    > dir_2",
-                    "      .gitignore",
-                ],
-                "When no auto reveal is enabled, the selected entry should not be revealed in the project panel"
-            );
-        }
-
-        panel.update(cx, |panel, cx| {
-            panel.project.update(cx, |_, cx| {
-                cx.emit(project::Event::RevealInProjectPanel(dir_1_file))
-            })
-        });
-        cx.run_until_parked();
-        assert_eq!(
-            visible_entries_as_strings(&panel, 0..20, cx),
-            &[
-                "v project_root",
-                "    > .git",
-                "    v dir_1",
-                "        > gitignored_dir",
-                "          file_1.py  <== selected",
-                "          file_2.py",
-                "          file_3.py",
-                "    > dir_2",
-                "      .gitignore",
-            ],
-            "With no auto reveal, explicit reveal should show the dir_1 entry in the project panel"
-        );
-
-        panel.update(cx, |panel, cx| {
-            panel.project.update(cx, |_, cx| {
-                cx.emit(project::Event::RevealInProjectPanel(dir_2_file))
-            })
-        });
-        cx.run_until_parked();
-        assert_eq!(
-            visible_entries_as_strings(&panel, 0..20, cx),
-            &[
-                "v project_root",
-                "    > .git",
-                "    v dir_1",
-                "        > gitignored_dir",
-                "          file_1.py",
-                "          file_2.py",
-                "          file_3.py",
-                "    v dir_2",
-                "          file_1.py  <== selected",
-                "          file_2.py",
-                "          file_3.py",
-                "      .gitignore",
-            ],
-            "With no auto reveal, explicit reveal should show the dir_2 entry in the project panel"
-        );
-
-        panel.update(cx, |panel, cx| {
-            panel.project.update(cx, |_, cx| {
-                cx.emit(project::Event::RevealInProjectPanel(gitignored_dir_file))
-            })
-        });
-        cx.run_until_parked();
-        assert_eq!(
-            visible_entries_as_strings(&panel, 0..20, cx),
-            &[
-                "v project_root",
-                "    > .git",
-                "    v dir_1",
-                "        v gitignored_dir",
-                "              file_a.py  <== selected",
-                "              file_b.py",
-                "              file_c.py",
-                "          file_1.py",
-                "          file_2.py",
-                "          file_3.py",
-                "    v dir_2",
-                "          file_1.py",
-                "          file_2.py",
-                "          file_3.py",
-                "      .gitignore",
-            ],
-            "With no auto reveal, explicit reveal should show the gitignored entry in the project panel"
-        );
-    }
-
-    fn toggle_expand_dir(
-        panel: &View<ProjectPanel>,
-        path: impl AsRef<Path>,
-        cx: &mut VisualTestContext,
-    ) {
-        let path = path.as_ref();
-        panel.update(cx, |panel, cx| {
-            for worktree in panel.project.read(cx).worktrees().collect::<Vec<_>>() {
-                let worktree = worktree.read(cx);
-                if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
-                    let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
-                    panel.toggle_expanded(entry_id, cx);
-                    return;
-                }
-            }
-            panic!("no worktree for path {:?}", path);
-        });
-    }
-
-    fn select_path(panel: &View<ProjectPanel>, path: impl AsRef<Path>, cx: &mut VisualTestContext) {
-        let path = path.as_ref();
-        panel.update(cx, |panel, cx| {
-            for worktree in panel.project.read(cx).worktrees().collect::<Vec<_>>() {
-                let worktree = worktree.read(cx);
-                if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
-                    let entry_id = worktree.entry_for_path(relative_path).unwrap().id;
-                    panel.selection = Some(crate::Selection {
-                        worktree_id: worktree.id(),
-                        entry_id,
-                    });
-                    return;
-                }
-            }
-            panic!("no worktree for path {:?}", path);
-        });
-    }
-
-    fn find_project_entry(
-        panel: &View<ProjectPanel>,
-        path: impl AsRef<Path>,
-        cx: &mut VisualTestContext,
-    ) -> Option<ProjectEntryId> {
-        let path = path.as_ref();
-        panel.update(cx, |panel, cx| {
-            for worktree in panel.project.read(cx).worktrees().collect::<Vec<_>>() {
-                let worktree = worktree.read(cx);
-                if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) {
-                    return worktree.entry_for_path(relative_path).map(|entry| entry.id);
-                }
-            }
-            panic!("no worktree for path {path:?}");
-        })
-    }
-
-    fn visible_entries_as_strings(
-        panel: &View<ProjectPanel>,
-        range: Range<usize>,
-        cx: &mut VisualTestContext,
-    ) -> Vec<String> {
-        let mut result = Vec::new();
-        let mut project_entries = HashSet::new();
-        let mut has_editor = false;
-
-        panel.update(cx, |panel, cx| {
-            panel.for_each_visible_entry(range, cx, |project_entry, details, _| {
-                if details.is_editing {
-                    assert!(!has_editor, "duplicate editor entry");
-                    has_editor = true;
-                } else {
-                    assert!(
-                        project_entries.insert(project_entry),
-                        "duplicate project entry {:?} {:?}",
-                        project_entry,
-                        details
-                    );
-                }
-
-                let indent = "    ".repeat(details.depth);
-                let icon = if details.kind.is_dir() {
-                    if details.is_expanded {
-                        "v "
-                    } else {
-                        "> "
-                    }
-                } else {
-                    "  "
-                };
-                let name = if details.is_editing {
-                    format!("[EDITOR: '{}']", details.filename)
-                } else if details.is_processing {
-                    format!("[PROCESSING: '{}']", details.filename)
-                } else {
-                    details.filename.clone()
-                };
-                let selected = if details.is_selected {
-                    "  <== selected"
-                } else {
-                    ""
-                };
-                result.push(format!("{indent}{icon}{name}{selected}"));
-            });
-        });
-
-        result
-    }
-
-    fn init_test(cx: &mut TestAppContext) {
-        cx.update(|cx| {
-            let settings_store = SettingsStore::test(cx);
-            cx.set_global(settings_store);
-            init_settings(cx);
-            theme::init(theme::LoadThemes::JustBase, cx);
-            language::init(cx);
-            editor::init_settings(cx);
-            crate::init((), cx);
-            workspace::init_settings(cx);
-            client::init_settings(cx);
-            Project::init_settings(cx);
-
-            cx.update_global::<SettingsStore, _>(|store, cx| {
-                store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
-                    project_settings.file_scan_exclusions = Some(Vec::new());
-                });
-            });
-        });
-    }
-
-    fn init_test_with_editor(cx: &mut TestAppContext) {
-        cx.update(|cx| {
-            let app_state = AppState::test(cx);
-            theme::init(theme::LoadThemes::JustBase, cx);
-            init_settings(cx);
-            language::init(cx);
-            editor::init(cx);
-            crate::init((), cx);
-            workspace::init(app_state.clone(), cx);
-            Project::init_settings(cx);
-        });
-    }
-
-    fn ensure_single_file_is_opened(
-        window: &WindowHandle<Workspace>,
-        expected_path: &str,
-        cx: &mut TestAppContext,
-    ) {
-        window
-            .update(cx, |workspace, cx| {
-                let worktrees = workspace.worktrees(cx).collect::<Vec<_>>();
-                assert_eq!(worktrees.len(), 1);
-                let worktree_id = worktrees[0].read(cx).id();
-
-                let open_project_paths = workspace
-                    .panes()
-                    .iter()
-                    .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx))
-                    .collect::<Vec<_>>();
-                assert_eq!(
-                    open_project_paths,
-                    vec![ProjectPath {
-                        worktree_id,
-                        path: Arc::from(Path::new(expected_path))
-                    }],
-                    "Should have opened file, selected in project panel"
-                );
-            })
-            .unwrap();
-    }
-
-    fn submit_deletion(panel: &View<ProjectPanel>, cx: &mut VisualTestContext) {
-        assert!(
-            !cx.has_pending_prompt(),
-            "Should have no prompts before the deletion"
-        );
-        panel.update(cx, |panel, cx| panel.delete(&Delete, cx));
-        assert!(
-            cx.has_pending_prompt(),
-            "Should have a prompt after the deletion"
-        );
-        cx.simulate_prompt_answer(0);
-        assert!(
-            !cx.has_pending_prompt(),
-            "Should have no prompts after prompt was replied to"
-        );
-        cx.executor().run_until_parked();
-    }
-
-    fn ensure_no_open_items_and_panes(
-        workspace: &WindowHandle<Workspace>,
-        cx: &mut VisualTestContext,
-    ) {
-        assert!(
-            !cx.has_pending_prompt(),
-            "Should have no prompts after deletion operation closes the file"
-        );
-        workspace
-            .read_with(cx, |workspace, cx| {
-                let open_project_paths = workspace
-                    .panes()
-                    .iter()
-                    .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx))
-                    .collect::<Vec<_>>();
-                assert!(
-                    open_project_paths.is_empty(),
-                    "Deleted file's buffer should be closed, but got open files: {open_project_paths:?}"
-                );
-            })
-            .unwrap();
-    }
-}

crates/project_panel2/src/project_panel_settings.rs πŸ”—

@@ -1,48 +0,0 @@
-use anyhow;
-use gpui::Pixels;
-use schemars::JsonSchema;
-use serde_derive::{Deserialize, Serialize};
-use settings::Settings;
-
-#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
-#[serde(rename_all = "snake_case")]
-pub enum ProjectPanelDockPosition {
-    Left,
-    Right,
-}
-
-#[derive(Deserialize, Debug)]
-pub struct ProjectPanelSettings {
-    pub default_width: Pixels,
-    pub dock: ProjectPanelDockPosition,
-    pub file_icons: bool,
-    pub folder_icons: bool,
-    pub git_status: bool,
-    pub indent_size: f32,
-    pub auto_reveal_entries: bool,
-}
-
-#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
-pub struct ProjectPanelSettingsContent {
-    pub default_width: Option<f32>,
-    pub dock: Option<ProjectPanelDockPosition>,
-    pub file_icons: Option<bool>,
-    pub folder_icons: Option<bool>,
-    pub git_status: Option<bool>,
-    pub indent_size: Option<f32>,
-    pub auto_reveal_entries: Option<bool>,
-}
-
-impl Settings for ProjectPanelSettings {
-    const KEY: Option<&'static str> = Some("project_panel");
-
-    type FileContent = ProjectPanelSettingsContent;
-
-    fn load(
-        default_value: &Self::FileContent,
-        user_values: &[&Self::FileContent],
-        _: &mut gpui::AppContext,
-    ) -> anyhow::Result<Self> {
-        Self::load_via_json_merge(default_value, user_values)
-    }
-}

crates/recent_projects/Cargo.toml πŸ”—

@@ -9,17 +9,17 @@ path = "src/recent_projects.rs"
 doctest = false
 
 [dependencies]
-db = { path = "../db" }
-editor = { path = "../editor" }
-fuzzy = { path = "../fuzzy" }
-gpui = { path = "../gpui" }
-language = { path = "../language" }
-picker = { path = "../picker" }
-settings = { path = "../settings" }
-text = { path = "../text" }
+editor = { package = "editor2", path = "../editor2" }
+fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
+gpui = { package = "gpui2", path = "../gpui2" }
+language = { package = "language2", path = "../language2" }
+picker = { package = "picker2", path = "../picker2" }
+settings = { package = "settings2", path = "../settings2" }
+text = { package = "text2", path = "../text2" }
 util = { path = "../util"}
-theme = { path = "../theme" }
-workspace = { path = "../workspace" }
+theme = { package = "theme2", path = "../theme2" }
+ui = { package = "ui2", path = "../ui2" }
+workspace = { package = "workspace2", path = "../workspace2" }
 
 futures.workspace = true
 ordered-float.workspace = true
@@ -27,4 +27,4 @@ postage.workspace = true
 smol.workspace = true
 
 [dev-dependencies]
-editor = { path = "../editor", features = ["test-support"] }
+editor = { package = "editor2", path = "../editor2", features = ["test-support"] }

crates/recent_projects/src/highlighted_workspace_location.rs πŸ”—

@@ -1,13 +1,11 @@
 use std::path::Path;
 
 use fuzzy::StringMatch;
-use gpui::{
-    elements::{Label, LabelStyle},
-    AnyElement, Element,
-};
+use ui::{prelude::*, HighlightedLabel};
 use util::paths::PathExt;
 use workspace::WorkspaceLocation;
 
+#[derive(IntoElement)]
 pub struct HighlightedText {
     pub text: String,
     pub highlight_positions: Vec<usize>,
@@ -42,11 +40,11 @@ impl HighlightedText {
             char_count,
         }
     }
+}
 
-    pub fn render<V: 'static>(self, style: impl Into<LabelStyle>) -> AnyElement<V> {
-        Label::new(self.text, style)
-            .with_highlights(self.highlight_positions)
-            .into_any()
+impl RenderOnce for HighlightedText {
+    fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
+        HighlightedLabel::new(self.text, self.highlight_positions)
     }
 }
 

crates/recent_projects/src/recent_projects.rs πŸ”—

@@ -1,86 +1,122 @@
 mod highlighted_workspace_location;
+mod projects;
 
 use fuzzy::{StringMatch, StringMatchCandidate};
 use gpui::{
-    actions,
-    anyhow::Result,
-    elements::{Flex, ParentElement},
-    AnyElement, AppContext, Element, Task, ViewContext, WeakViewHandle,
+    AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Result, Subscription, Task,
+    View, ViewContext, WeakView,
 };
 use highlighted_workspace_location::HighlightedWorkspaceLocation;
 use ordered_float::OrderedFloat;
-use picker::{Picker, PickerDelegate, PickerEvent};
+use picker::{Picker, PickerDelegate};
 use std::sync::Arc;
+use ui::{prelude::*, ListItem, ListItemSpacing};
 use util::paths::PathExt;
-use workspace::{
-    notifications::simple_message_notification::MessageNotification, Workspace, WorkspaceLocation,
-    WORKSPACE_DB,
-};
+use workspace::{ModalView, Workspace, WorkspaceLocation, WORKSPACE_DB};
 
-actions!(projects, [OpenRecent]);
+pub use projects::OpenRecent;
 
 pub fn init(cx: &mut AppContext) {
-    cx.add_async_action(toggle);
-    RecentProjects::init(cx);
+    cx.observe_new_views(RecentProjects::register).detach();
+}
+
+pub struct RecentProjects {
+    pub picker: View<Picker<RecentProjectsDelegate>>,
+    rem_width: f32,
+    _subscription: Subscription,
 }
 
-fn toggle(
-    _: &mut Workspace,
-    _: &OpenRecent,
-    cx: &mut ViewContext<Workspace>,
-) -> Option<Task<Result<()>>> {
-    Some(cx.spawn(|workspace, mut cx| async move {
-        let workspace_locations: Vec<_> = cx
-            .background()
-            .spawn(async {
-                WORKSPACE_DB
-                    .recent_workspaces_on_disk()
-                    .await
-                    .unwrap_or_default()
-                    .into_iter()
-                    .map(|(_, location)| location)
-                    .collect()
+impl ModalView for RecentProjects {}
+
+impl RecentProjects {
+    fn new(delegate: RecentProjectsDelegate, rem_width: f32, cx: &mut ViewContext<Self>) -> Self {
+        let picker = cx.new_view(|cx| Picker::new(delegate, cx));
+        let _subscription = cx.subscribe(&picker, |_, _, _, cx| cx.emit(DismissEvent));
+        // We do not want to block the UI on a potentially lenghty call to DB, so we're gonna swap
+        // out workspace locations once the future runs to completion.
+        cx.spawn(|this, mut cx| async move {
+            let workspaces = WORKSPACE_DB
+                .recent_workspaces_on_disk()
+                .await
+                .unwrap_or_default()
+                .into_iter()
+                .map(|(_, location)| location)
+                .collect();
+            this.update(&mut cx, move |this, cx| {
+                this.picker.update(cx, move |picker, cx| {
+                    picker.delegate.workspace_locations = workspaces;
+                    picker.update_matches(picker.query(cx), cx)
+                })
             })
-            .await;
-
-        workspace.update(&mut cx, |workspace, cx| {
-            if !workspace_locations.is_empty() {
-                workspace.toggle_modal(cx, |_, cx| {
-                    let workspace = cx.weak_handle();
-                    cx.add_view(|cx| {
-                        RecentProjects::new(
-                            RecentProjectsDelegate::new(workspace, workspace_locations, true),
-                            cx,
-                        )
-                        .with_max_size(800., 1200.)
-                    })
+            .ok()
+        })
+        .detach();
+        Self {
+            picker,
+            rem_width,
+            _subscription,
+        }
+    }
+
+    fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
+        workspace.register_action(|workspace, _: &OpenRecent, cx| {
+            let Some(recent_projects) = workspace.active_modal::<Self>(cx) else {
+                if let Some(handler) = Self::open(workspace, cx) {
+                    handler.detach_and_log_err(cx);
+                }
+                return;
+            };
+
+            recent_projects.update(cx, |recent_projects, cx| {
+                recent_projects
+                    .picker
+                    .update(cx, |picker, cx| picker.cycle_selection(cx))
+            });
+        });
+    }
+
+    fn open(_: &mut Workspace, cx: &mut ViewContext<Workspace>) -> Option<Task<Result<()>>> {
+        Some(cx.spawn(|workspace, mut cx| async move {
+            workspace.update(&mut cx, |workspace, cx| {
+                let weak_workspace = cx.view().downgrade();
+                workspace.toggle_modal(cx, |cx| {
+                    let delegate = RecentProjectsDelegate::new(weak_workspace, true);
+
+                    let modal = RecentProjects::new(delegate, 34., cx);
+                    modal
                 });
-            } else {
-                workspace.show_notification(0, cx, |cx| {
-                    cx.add_view(|_| MessageNotification::new("No recent projects to open."))
-                })
-            }
-        })?;
-        Ok(())
-    }))
+            })?;
+            Ok(())
+        }))
+    }
+    pub fn open_popover(workspace: WeakView<Workspace>, cx: &mut WindowContext<'_>) -> View<Self> {
+        cx.new_view(|cx| Self::new(RecentProjectsDelegate::new(workspace, false), 20., cx))
+    }
 }
 
-pub fn build_recent_projects(
-    workspace: WeakViewHandle<Workspace>,
-    workspaces: Vec<WorkspaceLocation>,
-    cx: &mut ViewContext<RecentProjects>,
-) -> RecentProjects {
-    Picker::new(
-        RecentProjectsDelegate::new(workspace, workspaces, false),
-        cx,
-    )
-    .with_theme(|theme| theme.picker.clone())
+impl EventEmitter<DismissEvent> for RecentProjects {}
+
+impl FocusableView for RecentProjects {
+    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
+        self.picker.focus_handle(cx)
+    }
 }
 
-pub type RecentProjects = Picker<RecentProjectsDelegate>;
+impl Render for RecentProjects {
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+        v_stack()
+            .w(rems(self.rem_width))
+            .child(self.picker.clone())
+            .on_mouse_down_out(cx.listener(|this, _, cx| {
+                this.picker.update(cx, |this, cx| {
+                    this.cancel(&Default::default(), cx);
+                })
+            }))
+    }
+}
 
 pub struct RecentProjectsDelegate {
-    workspace: WeakViewHandle<Workspace>,
+    workspace: WeakView<Workspace>,
     workspace_locations: Vec<WorkspaceLocation>,
     selected_match_index: usize,
     matches: Vec<StringMatch>,
@@ -88,22 +124,20 @@ pub struct RecentProjectsDelegate {
 }
 
 impl RecentProjectsDelegate {
-    fn new(
-        workspace: WeakViewHandle<Workspace>,
-        workspace_locations: Vec<WorkspaceLocation>,
-        render_paths: bool,
-    ) -> Self {
+    fn new(workspace: WeakView<Workspace>, render_paths: bool) -> Self {
         Self {
             workspace,
-            workspace_locations,
+            workspace_locations: vec![],
             selected_match_index: 0,
             matches: Default::default(),
             render_paths,
         }
     }
 }
-
+impl EventEmitter<DismissEvent> for RecentProjectsDelegate {}
 impl PickerDelegate for RecentProjectsDelegate {
+    type ListItem = ListItem;
+
     fn placeholder_text(&self) -> Arc<str> {
         "Recent Projects...".into()
     }
@@ -116,14 +150,14 @@ impl PickerDelegate for RecentProjectsDelegate {
         self.selected_match_index
     }
 
-    fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<RecentProjects>) {
+    fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<Picker<Self>>) {
         self.selected_match_index = ix;
     }
 
     fn update_matches(
         &mut self,
         query: String,
-        cx: &mut ViewContext<RecentProjects>,
+        cx: &mut ViewContext<Picker<Self>>,
     ) -> gpui::Task<()> {
         let query = query.trim_start();
         let smart_case = query.chars().any(|c| c.is_uppercase());
@@ -147,7 +181,7 @@ impl PickerDelegate for RecentProjectsDelegate {
             smart_case,
             100,
             &Default::default(),
-            cx.background().clone(),
+            cx.background_executor().clone(),
         ));
         self.matches.sort_unstable_by_key(|m| m.candidate_id);
 
@@ -162,11 +196,11 @@ impl PickerDelegate for RecentProjectsDelegate {
         Task::ready(())
     }
 
-    fn confirm(&mut self, _: bool, cx: &mut ViewContext<RecentProjects>) {
+    fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
         if let Some((selected_match, workspace)) = self
             .matches
             .get(self.selected_index())
-            .zip(self.workspace.upgrade(cx))
+            .zip(self.workspace.upgrade())
         {
             let workspace_location = &self.workspace_locations[selected_match.candidate_id];
             workspace
@@ -175,41 +209,39 @@ impl PickerDelegate for RecentProjectsDelegate {
                         .open_workspace_for_paths(workspace_location.paths().as_ref().clone(), cx)
                 })
                 .detach_and_log_err(cx);
-            cx.emit(PickerEvent::Dismiss);
+            cx.emit(DismissEvent);
         }
     }
 
-    fn dismissed(&mut self, _cx: &mut ViewContext<RecentProjects>) {}
+    fn dismissed(&mut self, _: &mut ViewContext<Picker<Self>>) {}
 
     fn render_match(
         &self,
         ix: usize,
-        mouse_state: &mut gpui::MouseState,
         selected: bool,
-        cx: &gpui::AppContext,
-    ) -> AnyElement<Picker<Self>> {
-        let theme = theme::current(cx);
-        let style = theme.picker.item.in_state(selected).style_for(mouse_state);
-
-        let string_match = &self.matches[ix];
+        _cx: &mut ViewContext<Picker<Self>>,
+    ) -> Option<Self::ListItem> {
+        let Some(r#match) = self.matches.get(ix) else {
+            return None;
+        };
 
         let highlighted_location = HighlightedWorkspaceLocation::new(
-            &string_match,
-            &self.workspace_locations[string_match.candidate_id],
+            &r#match,
+            &self.workspace_locations[r#match.candidate_id],
         );
 
-        Flex::column()
-            .with_child(highlighted_location.names.render(style.label.clone()))
-            .with_children(
-                highlighted_location
-                    .paths
-                    .into_iter()
-                    .filter(|_| self.render_paths)
-                    .map(|highlighted_path| highlighted_path.render(style.label.clone())),
-            )
-            .flex(1., false)
-            .contained()
-            .with_style(style.container)
-            .into_any_named("match")
+        Some(
+            ListItem::new(ix)
+                .inset(true)
+                .spacing(ListItemSpacing::Sparse)
+                .selected(selected)
+                .child(
+                    v_stack()
+                        .child(highlighted_location.names)
+                        .when(self.render_paths, |this| {
+                            this.children(highlighted_location.paths)
+                        }),
+                ),
+        )
     }
 }

crates/recent_projects2/Cargo.toml πŸ”—

@@ -1,30 +0,0 @@
-[package]
-name = "recent_projects2"
-version = "0.1.0"
-edition = "2021"
-publish = false
-
-[lib]
-path = "src/recent_projects.rs"
-doctest = false
-
-[dependencies]
-editor = { package = "editor2", path = "../editor2" }
-fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
-gpui = { package = "gpui2", path = "../gpui2" }
-language = { package = "language2", path = "../language2" }
-picker = { package = "picker2", path = "../picker2" }
-settings = { package = "settings2", path = "../settings2" }
-text = { package = "text2", path = "../text2" }
-util = { path = "../util"}
-theme = { package = "theme2", path = "../theme2" }
-ui = { package = "ui2", path = "../ui2" }
-workspace = { package = "workspace2", path = "../workspace2" }
-
-futures.workspace = true
-ordered-float.workspace = true
-postage.workspace = true
-smol.workspace = true
-
-[dev-dependencies]
-editor = { package = "editor2", path = "../editor2", features = ["test-support"] }

crates/recent_projects2/src/highlighted_workspace_location.rs πŸ”—

@@ -1,129 +0,0 @@
-use std::path::Path;
-
-use fuzzy::StringMatch;
-use ui::{prelude::*, HighlightedLabel};
-use util::paths::PathExt;
-use workspace::WorkspaceLocation;
-
-#[derive(IntoElement)]
-pub struct HighlightedText {
-    pub text: String,
-    pub highlight_positions: Vec<usize>,
-    char_count: usize,
-}
-
-impl HighlightedText {
-    fn join(components: impl Iterator<Item = Self>, separator: &str) -> Self {
-        let mut char_count = 0;
-        let separator_char_count = separator.chars().count();
-        let mut text = String::new();
-        let mut highlight_positions = Vec::new();
-        for component in components {
-            if char_count != 0 {
-                text.push_str(separator);
-                char_count += separator_char_count;
-            }
-
-            highlight_positions.extend(
-                component
-                    .highlight_positions
-                    .iter()
-                    .map(|position| position + char_count),
-            );
-            text.push_str(&component.text);
-            char_count += component.text.chars().count();
-        }
-
-        Self {
-            text,
-            highlight_positions,
-            char_count,
-        }
-    }
-}
-
-impl RenderOnce for HighlightedText {
-    fn render(self, _cx: &mut WindowContext) -> impl IntoElement {
-        HighlightedLabel::new(self.text, self.highlight_positions)
-    }
-}
-
-pub struct HighlightedWorkspaceLocation {
-    pub names: HighlightedText,
-    pub paths: Vec<HighlightedText>,
-}
-
-impl HighlightedWorkspaceLocation {
-    pub fn new(string_match: &StringMatch, location: &WorkspaceLocation) -> Self {
-        let mut path_start_offset = 0;
-        let (names, paths): (Vec<_>, Vec<_>) = location
-            .paths()
-            .iter()
-            .map(|path| {
-                let path = path.compact();
-                let highlighted_text = Self::highlights_for_path(
-                    path.as_ref(),
-                    &string_match.positions,
-                    path_start_offset,
-                );
-
-                path_start_offset += highlighted_text.1.char_count;
-
-                highlighted_text
-            })
-            .unzip();
-
-        Self {
-            names: HighlightedText::join(names.into_iter().filter_map(|name| name), ", "),
-            paths,
-        }
-    }
-
-    // Compute the highlighted text for the name and path
-    fn highlights_for_path(
-        path: &Path,
-        match_positions: &Vec<usize>,
-        path_start_offset: usize,
-    ) -> (Option<HighlightedText>, HighlightedText) {
-        let path_string = path.to_string_lossy();
-        let path_char_count = path_string.chars().count();
-        // Get the subset of match highlight positions that line up with the given path.
-        // Also adjusts them to start at the path start
-        let path_positions = match_positions
-            .iter()
-            .copied()
-            .skip_while(|position| *position < path_start_offset)
-            .take_while(|position| *position < path_start_offset + path_char_count)
-            .map(|position| position - path_start_offset)
-            .collect::<Vec<_>>();
-
-        // Again subset the highlight positions to just those that line up with the file_name
-        // again adjusted to the start of the file_name
-        let file_name_text_and_positions = path.file_name().map(|file_name| {
-            let text = file_name.to_string_lossy();
-            let char_count = text.chars().count();
-            let file_name_start = path_char_count - char_count;
-            let highlight_positions = path_positions
-                .iter()
-                .copied()
-                .skip_while(|position| *position < file_name_start)
-                .take_while(|position| *position < file_name_start + char_count)
-                .map(|position| position - file_name_start)
-                .collect::<Vec<_>>();
-            HighlightedText {
-                text: text.to_string(),
-                highlight_positions,
-                char_count,
-            }
-        });
-
-        (
-            file_name_text_and_positions,
-            HighlightedText {
-                text: path_string.to_string(),
-                highlight_positions: path_positions,
-                char_count: path_char_count,
-            },
-        )
-    }
-}

crates/recent_projects2/src/recent_projects.rs πŸ”—

@@ -1,247 +0,0 @@
-mod highlighted_workspace_location;
-mod projects;
-
-use fuzzy::{StringMatch, StringMatchCandidate};
-use gpui::{
-    AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Result, Subscription, Task,
-    View, ViewContext, WeakView,
-};
-use highlighted_workspace_location::HighlightedWorkspaceLocation;
-use ordered_float::OrderedFloat;
-use picker::{Picker, PickerDelegate};
-use std::sync::Arc;
-use ui::{prelude::*, ListItem, ListItemSpacing};
-use util::paths::PathExt;
-use workspace::{ModalView, Workspace, WorkspaceLocation, WORKSPACE_DB};
-
-pub use projects::OpenRecent;
-
-pub fn init(cx: &mut AppContext) {
-    cx.observe_new_views(RecentProjects::register).detach();
-}
-
-pub struct RecentProjects {
-    pub picker: View<Picker<RecentProjectsDelegate>>,
-    rem_width: f32,
-    _subscription: Subscription,
-}
-
-impl ModalView for RecentProjects {}
-
-impl RecentProjects {
-    fn new(delegate: RecentProjectsDelegate, rem_width: f32, cx: &mut ViewContext<Self>) -> Self {
-        let picker = cx.new_view(|cx| Picker::new(delegate, cx));
-        let _subscription = cx.subscribe(&picker, |_, _, _, cx| cx.emit(DismissEvent));
-        // We do not want to block the UI on a potentially lenghty call to DB, so we're gonna swap
-        // out workspace locations once the future runs to completion.
-        cx.spawn(|this, mut cx| async move {
-            let workspaces = WORKSPACE_DB
-                .recent_workspaces_on_disk()
-                .await
-                .unwrap_or_default()
-                .into_iter()
-                .map(|(_, location)| location)
-                .collect();
-            this.update(&mut cx, move |this, cx| {
-                this.picker.update(cx, move |picker, cx| {
-                    picker.delegate.workspace_locations = workspaces;
-                    picker.update_matches(picker.query(cx), cx)
-                })
-            })
-            .ok()
-        })
-        .detach();
-        Self {
-            picker,
-            rem_width,
-            _subscription,
-        }
-    }
-
-    fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
-        workspace.register_action(|workspace, _: &OpenRecent, cx| {
-            let Some(recent_projects) = workspace.active_modal::<Self>(cx) else {
-                if let Some(handler) = Self::open(workspace, cx) {
-                    handler.detach_and_log_err(cx);
-                }
-                return;
-            };
-
-            recent_projects.update(cx, |recent_projects, cx| {
-                recent_projects
-                    .picker
-                    .update(cx, |picker, cx| picker.cycle_selection(cx))
-            });
-        });
-    }
-
-    fn open(_: &mut Workspace, cx: &mut ViewContext<Workspace>) -> Option<Task<Result<()>>> {
-        Some(cx.spawn(|workspace, mut cx| async move {
-            workspace.update(&mut cx, |workspace, cx| {
-                let weak_workspace = cx.view().downgrade();
-                workspace.toggle_modal(cx, |cx| {
-                    let delegate = RecentProjectsDelegate::new(weak_workspace, true);
-
-                    let modal = RecentProjects::new(delegate, 34., cx);
-                    modal
-                });
-            })?;
-            Ok(())
-        }))
-    }
-    pub fn open_popover(workspace: WeakView<Workspace>, cx: &mut WindowContext<'_>) -> View<Self> {
-        cx.new_view(|cx| Self::new(RecentProjectsDelegate::new(workspace, false), 20., cx))
-    }
-}
-
-impl EventEmitter<DismissEvent> for RecentProjects {}
-
-impl FocusableView for RecentProjects {
-    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
-        self.picker.focus_handle(cx)
-    }
-}
-
-impl Render for RecentProjects {
-    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
-        v_stack()
-            .w(rems(self.rem_width))
-            .child(self.picker.clone())
-            .on_mouse_down_out(cx.listener(|this, _, cx| {
-                this.picker.update(cx, |this, cx| {
-                    this.cancel(&Default::default(), cx);
-                })
-            }))
-    }
-}
-
-pub struct RecentProjectsDelegate {
-    workspace: WeakView<Workspace>,
-    workspace_locations: Vec<WorkspaceLocation>,
-    selected_match_index: usize,
-    matches: Vec<StringMatch>,
-    render_paths: bool,
-}
-
-impl RecentProjectsDelegate {
-    fn new(workspace: WeakView<Workspace>, render_paths: bool) -> Self {
-        Self {
-            workspace,
-            workspace_locations: vec![],
-            selected_match_index: 0,
-            matches: Default::default(),
-            render_paths,
-        }
-    }
-}
-impl EventEmitter<DismissEvent> for RecentProjectsDelegate {}
-impl PickerDelegate for RecentProjectsDelegate {
-    type ListItem = ListItem;
-
-    fn placeholder_text(&self) -> Arc<str> {
-        "Recent Projects...".into()
-    }
-
-    fn match_count(&self) -> usize {
-        self.matches.len()
-    }
-
-    fn selected_index(&self) -> usize {
-        self.selected_match_index
-    }
-
-    fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<Picker<Self>>) {
-        self.selected_match_index = ix;
-    }
-
-    fn update_matches(
-        &mut self,
-        query: String,
-        cx: &mut ViewContext<Picker<Self>>,
-    ) -> gpui::Task<()> {
-        let query = query.trim_start();
-        let smart_case = query.chars().any(|c| c.is_uppercase());
-        let candidates = self
-            .workspace_locations
-            .iter()
-            .enumerate()
-            .map(|(id, location)| {
-                let combined_string = location
-                    .paths()
-                    .iter()
-                    .map(|path| path.compact().to_string_lossy().into_owned())
-                    .collect::<Vec<_>>()
-                    .join("");
-                StringMatchCandidate::new(id, combined_string)
-            })
-            .collect::<Vec<_>>();
-        self.matches = smol::block_on(fuzzy::match_strings(
-            candidates.as_slice(),
-            query,
-            smart_case,
-            100,
-            &Default::default(),
-            cx.background_executor().clone(),
-        ));
-        self.matches.sort_unstable_by_key(|m| m.candidate_id);
-
-        self.selected_match_index = self
-            .matches
-            .iter()
-            .enumerate()
-            .rev()
-            .max_by_key(|(_, m)| OrderedFloat(m.score))
-            .map(|(ix, _)| ix)
-            .unwrap_or(0);
-        Task::ready(())
-    }
-
-    fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
-        if let Some((selected_match, workspace)) = self
-            .matches
-            .get(self.selected_index())
-            .zip(self.workspace.upgrade())
-        {
-            let workspace_location = &self.workspace_locations[selected_match.candidate_id];
-            workspace
-                .update(cx, |workspace, cx| {
-                    workspace
-                        .open_workspace_for_paths(workspace_location.paths().as_ref().clone(), cx)
-                })
-                .detach_and_log_err(cx);
-            cx.emit(DismissEvent);
-        }
-    }
-
-    fn dismissed(&mut self, _: &mut ViewContext<Picker<Self>>) {}
-
-    fn render_match(
-        &self,
-        ix: usize,
-        selected: bool,
-        _cx: &mut ViewContext<Picker<Self>>,
-    ) -> Option<Self::ListItem> {
-        let Some(r#match) = self.matches.get(ix) else {
-            return None;
-        };
-
-        let highlighted_location = HighlightedWorkspaceLocation::new(
-            &r#match,
-            &self.workspace_locations[r#match.candidate_id],
-        );
-
-        Some(
-            ListItem::new(ix)
-                .inset(true)
-                .spacing(ListItemSpacing::Sparse)
-                .selected(selected)
-                .child(
-                    v_stack()
-                        .child(highlighted_location.names)
-                        .when(self.render_paths, |this| {
-                            this.children(highlighted_location.paths)
-                        }),
-                ),
-        )
-    }
-}

crates/zed/Cargo.toml πŸ”—

@@ -17,9 +17,9 @@ path = "src/main.rs"
 [dependencies]
 ai = { package = "ai2", path = "../ai2"}
 audio = { package = "audio2", path = "../audio2" }
-activity_indicator = { package = "activity_indicator2", path = "../activity_indicator2"}
+activity_indicator = { path = "../activity_indicator"}
 auto_update = { package = "auto_update2", path = "../auto_update2" }
-breadcrumbs = { package = "breadcrumbs2", path = "../breadcrumbs2" }
+breadcrumbs = { path = "../breadcrumbs" }
 call = { package = "call2", path = "../call2" }
 channel = { package = "channel2", path = "../channel2" }
 cli = { path = "../cli" }
@@ -30,7 +30,7 @@ command_palette = { path = "../command_palette" }
 client = { package = "client2", path = "../client2" }
 # clock = { path = "../clock" }
 copilot = { package = "copilot2", path = "../copilot2" }
-copilot_button = { package = "copilot_button2", path = "../copilot_button2" }
+copilot_button = { path = "../copilot_button" }
 diagnostics = { path = "../diagnostics" }
 db = { package = "db2", path = "../db2" }
 editor = { package="editor2", path = "../editor2" }
@@ -44,7 +44,7 @@ gpui = { package = "gpui2", path = "../gpui2" }
 install_cli = { package = "install_cli2", path = "../install_cli2" }
 journal = { package = "journal2", path = "../journal2" }
 language = { package = "language2", path = "../language2" }
-language_selector = { package = "language_selector2", path = "../language_selector2" }
+language_selector = { path = "../language_selector" }
 lsp = { package = "lsp2", path = "../lsp2" }
 menu = { package = "menu2", path = "../menu2" }
 language_tools = { package = "language_tools2", path = "../language_tools2" }
@@ -54,10 +54,10 @@ assistant = { package = "assistant2", path = "../assistant2" }
 outline = { package = "outline2", path = "../outline2" }
 # plugin_runtime = { path = "../plugin_runtime",optional = true }
 project = { package = "project2", path = "../project2" }
-project_panel = { package = "project_panel2", path = "../project_panel2" }
+project_panel = { path = "../project_panel" }
 project_symbols = { path = "../project_symbols" }
 quick_action_bar = { path = "../quick_action_bar" }
-recent_projects = { package = "recent_projects2", path = "../recent_projects2" }
+recent_projects = { path = "../recent_projects" }
 rope = { package = "rope2", path = "../rope2"}
 rpc = { package = "rpc2", path = "../rpc2" }
 settings = { package = "settings2", path = "../settings2" }