Merge remote-tracking branch 'origin/main' into editor2-rename

Antonio Scandurra created

# Conflicts:
#	crates/editor2/src/editor.rs
#	crates/editor2/src/element.rs
#	crates/gpui2/src/style.rs

Change summary

Cargo.lock                                          |   60 
Cargo.toml                                          |    1 
crates/Cargo.toml                                   |   38 
crates/command_palette2/src/command_palette.rs      |    6 
crates/editor2/src/editor.rs                        |   66 
crates/editor2/src/element.rs                       |  425 +-
crates/editor2/src/items.rs                         |    6 
crates/file_finder2/Cargo.toml                      |   37 
crates/file_finder2/src/file_finder.rs              | 1973 +++++++++
crates/go_to_line2/src/go_to_line.rs                |   10 
crates/gpui/src/app.rs                              |    4 
crates/gpui2/docs/contexts.md                       |   41 
crates/gpui2/docs/key_dispatch.md                   |  101 
crates/gpui2/src/app.rs                             |   35 
crates/gpui2/src/app/entity_map.rs                  |    2 
crates/gpui2/src/app/test_context.rs                |   51 
crates/gpui2/src/color.rs                           |   11 
crates/gpui2/src/element.rs                         |   14 
crates/gpui2/src/elements/div.rs                    | 1566 ++++++-
crates/gpui2/src/elements/img.rs                    |  176 
crates/gpui2/src/elements/mod.rs                    |    0 
crates/gpui2/src/elements/svg.rs                    |  131 
crates/gpui2/src/elements/text.rs                   |    2 
crates/gpui2/src/elements/uniform_list.rs           |  225 
crates/gpui2/src/gpui2.rs                           |   16 
crates/gpui2/src/interactive.rs                     |  981 ----
crates/gpui2/src/key_dispatch.rs                    |  265 -
crates/gpui2/src/keymap/matcher.rs                  |    1 
crates/gpui2/src/platform.rs                        |    6 
crates/gpui2/src/platform/mac/text_system.rs        |   15 
crates/gpui2/src/platform/test/platform.rs          |   86 
crates/gpui2/src/platform/test/window.rs            |   28 
crates/gpui2/src/prelude.rs                         |    5 
crates/gpui2/src/style.rs                           |   44 
crates/gpui2/src/styled.rs                          |  291 -
crates/gpui2/src/text_system.rs                     |   26 
crates/gpui2/src/view.rs                            |   16 
crates/gpui2/src/window.rs                          |   72 
crates/gpui2_macros/src/style_helpers.rs            |    4 
crates/install_cli2/Cargo.toml                      |    1 
crates/install_cli2/src/install_cli2.rs             |    5 
crates/live_kit_client2/examples/test_app.rs        |    2 
crates/node_runtime/src/node_runtime.rs             |   29 
crates/picker2/src/picker2.rs                       |   26 
crates/project_panel2/Cargo.toml                    |   41 
crates/project_panel2/src/file_associations.rs      |   96 
crates/project_panel2/src/project_panel.rs          | 3046 +++++++-------
crates/project_panel2/src/project_panel_settings.rs |   45 
crates/settings2/src/keymap_file.rs                 |    6 
crates/storybook2/src/stories/colors.rs             |    2 
crates/storybook2/src/stories/focus.rs              |   11 
crates/storybook2/src/stories/kitchen_sink.rs       |    4 
crates/storybook2/src/stories/picker.rs             |    8 
crates/storybook2/src/stories/scroll.rs             |    7 
crates/storybook2/src/stories/text.rs               |    2 
crates/theme2/src/one_themes.rs                     |    1 
crates/theme2/src/settings.rs                       |    7 
crates/theme2/src/story.rs                          |    2 
crates/theme2/src/styles/players.rs                 |    2 
crates/ui2/src/components/button.rs                 |   46 
crates/ui2/src/components/checkbox.rs               |   16 
crates/ui2/src/components/elevated_surface.rs       |    2 
crates/ui2/src/components/icon.rs                   |   88 
crates/ui2/src/components/icon_button.rs            |   12 
crates/ui2/src/components/input.rs                  |   13 
crates/ui2/src/components/keybinding.rs             |    2 
crates/ui2/src/components/label.rs                  |   86 
crates/ui2/src/components/list.rs                   |   20 
crates/ui2/src/components/modal.rs                  |    2 
crates/ui2/src/components/palette.rs                |   33 
crates/ui2/src/components/panel.rs                  |    6 
crates/ui2/src/components/tab.rs                    |   18 
crates/ui2/src/components/toast.rs                  |    7 
crates/ui2/src/components/toggle.rs                 |    8 
crates/ui2/src/components/tooltip.rs                |   45 
crates/ui2/src/prelude.rs                           |    6 
crates/ui2/src/static_data.rs                       |   56 
crates/ui2/src/styled_ext.rs                        |   11 
crates/ui2/src/to_extract/assistant_panel.rs        |    2 
crates/ui2/src/to_extract/breadcrumb.rs             |    6 
crates/ui2/src/to_extract/buffer_search.rs          |    4 
crates/ui2/src/to_extract/chat_panel.rs             |    7 
crates/ui2/src/to_extract/collab_panel.rs           |    5 
crates/ui2/src/to_extract/copilot.rs                |    4 
crates/ui2/src/to_extract/editor_pane.rs            |    8 
crates/ui2/src/to_extract/notifications_panel.rs    |   19 
crates/ui2/src/to_extract/panes.rs                  |    2 
crates/ui2/src/to_extract/project_panel.rs          |    5 
crates/ui2/src/to_extract/status_bar.rs             |   22 
crates/ui2/src/to_extract/tab_bar.rs                |    4 
crates/ui2/src/to_extract/title_bar.rs              |   20 
crates/ui2/src/to_extract/workspace.rs              |    3 
crates/workspace2/src/dock.rs                       |   28 
crates/workspace2/src/modal_layer.rs                |   12 
crates/workspace2/src/pane.rs                       |   12 
crates/workspace2/src/pane/dragged_item_receiver.rs |    2 
crates/workspace2/src/status_bar.rs                 |    2 
crates/workspace2/src/workspace2.rs                 |  830 +--
crates/zed2/Cargo.toml                              |    6 
crates/zed2/src/main.rs                             |   19 
crates/zed2/src/open_listener.rs                    |    4 
crates/zed2/src/zed2.rs                             |  593 ++
crates/zed_actions2/src/lib.rs                      |   34 
103 files changed, 7,465 insertions(+), 4,845 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -3061,6 +3061,31 @@ dependencies = [
  "workspace",
 ]
 
+[[package]]
+name = "file_finder2"
+version = "0.1.0"
+dependencies = [
+ "collections",
+ "ctor",
+ "editor2",
+ "env_logger 0.9.3",
+ "fuzzy2",
+ "gpui2",
+ "language2",
+ "menu2",
+ "picker2",
+ "postage",
+ "project2",
+ "serde",
+ "serde_json",
+ "settings2",
+ "text2",
+ "theme2",
+ "ui2",
+ "util",
+ "workspace2",
+]
+
 [[package]]
 name = "filetime"
 version = "0.2.22"
@@ -4198,6 +4223,7 @@ dependencies = [
  "anyhow",
  "gpui2",
  "log",
+ "serde",
  "smol",
  "util",
 ]
@@ -6608,6 +6634,36 @@ dependencies = [
  "workspace",
 ]
 
+[[package]]
+name = "project_panel2"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "client2",
+ "collections",
+ "context_menu",
+ "db2",
+ "editor2",
+ "futures 0.3.28",
+ "gpui2",
+ "language2",
+ "menu2",
+ "postage",
+ "pretty_assertions",
+ "project2",
+ "schemars",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "settings2",
+ "smallvec",
+ "theme2",
+ "ui2",
+ "unicase",
+ "util",
+ "workspace2",
+]
+
 [[package]]
 name = "project_symbols"
 version = "0.1.0"
@@ -11393,6 +11449,7 @@ dependencies = [
  "editor2",
  "env_logger 0.9.3",
  "feature_flags2",
+ "file_finder2",
  "fs2",
  "fsevent",
  "futures 0.3.28",
@@ -11402,7 +11459,7 @@ dependencies = [
  "ignore",
  "image",
  "indexmap 1.9.3",
- "install_cli",
+ "install_cli2",
  "isahc",
  "journal2",
  "language2",
@@ -11417,6 +11474,7 @@ dependencies = [
  "parking_lot 0.11.2",
  "postage",
  "project2",
+ "project_panel2",
  "rand 0.8.5",
  "regex",
  "rope2",

Cargo.toml 🔗

@@ -80,6 +80,7 @@ members = [
     "crates/project",
     "crates/project2",
     "crates/project_panel",
+    "crates/project_panel2",
     "crates/project_symbols",
     "crates/recent_projects",
     "crates/rope",

crates/Cargo.toml 🔗

@@ -1,38 +0,0 @@
-[package]
-name = "ai"
-version = "0.1.0"
-edition = "2021"
-publish = false
-
-[lib]
-path = "src/ai.rs"
-doctest = false
-
-[features]
-test-support = []
-
-[dependencies]
-gpui = { path = "../gpui" }
-util = { path = "../util" }
-language = { path = "../language" }
-async-trait.workspace = true
-anyhow.workspace = true
-futures.workspace = true
-lazy_static.workspace = true
-ordered-float.workspace = true
-parking_lot.workspace = true
-isahc.workspace = true
-regex.workspace = true
-serde.workspace = true
-serde_json.workspace = true
-postage.workspace = true
-rand.workspace = true
-log.workspace = true
-parse_duration = "2.1.1"
-tiktoken-rs.workspace = true
-matrixmultiply = "0.3.7"
-rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] }
-bincode = "1.3.3"
-
-[dev-dependencies]
-gpui = { path = "../gpui", features = ["test-support"] }

crates/command_palette2/src/command_palette.rs 🔗

@@ -1,9 +1,9 @@
 use collections::{CommandPaletteFilter, HashMap};
 use fuzzy::{StringMatch, StringMatchCandidate};
 use gpui::{
-    actions, div, Action, AppContext, Component, Div, EventEmitter, FocusHandle, Keystroke,
-    ParentElement, Render, StatelessInteractive, Styled, View, ViewContext, VisualContext,
-    WeakView, WindowContext,
+    actions, div, prelude::*, Action, AppContext, Component, Div, EventEmitter, FocusHandle,
+    Keystroke, ParentComponent, Render, Styled, View, ViewContext, VisualContext, WeakView,
+    WindowContext,
 };
 use picker::{Picker, PickerDelegate};
 use std::{

crates/editor2/src/editor.rs 🔗

@@ -39,12 +39,12 @@ use futures::FutureExt;
 use fuzzy::{StringMatch, StringMatchCandidate};
 use git::diff_hunk_to_display;
 use gpui::{
-    action, actions, div, point, px, relative, rems, render_view, size, uniform_list, AnyElement,
-    AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Component, Context,
-    Entity, EventEmitter, FocusHandle, FontFeatures, FontStyle, FontWeight, HighlightStyle, Hsla,
-    InputHandler, KeyContext, Model, MouseButton, ParentElement, Pixels, Render,
-    StatefulInteractive, StatelessInteractive, Styled, Subscription, Task, TextStyle,
-    UniformListScrollHandle, View, ViewContext, VisualContext, WeakView, WindowContext,
+    action, actions, div, point, prelude::*, px, relative, rems, render_view, size, uniform_list,
+    AnyElement, AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem,
+    Component, Context, EventEmitter, FocusHandle, FontFeatures, FontStyle, FontWeight,
+    HighlightStyle, Hsla, InputHandler, KeyContext, Model, MouseButton, ParentComponent, Pixels,
+    Render, Styled, Subscription, Task, TextStyle, UniformListScrollHandle, View, ViewContext,
+    VisualContext, WeakView, WindowContext,
 };
 use highlight_matching_bracket::refresh_matching_bracket_highlights;
 use hover_popover::{hide_hover, HoverState};
@@ -7808,17 +7808,22 @@ impl Editor {
                                         .pl(cx.anchor_x)
                                         .child(render_view(
                                             &rename_editor,
-                                            EditorElement::new(EditorStyle {
-                                                background: cx.theme().system().transparent,
-                                                local_player: cx.editor_style.local_player,
-                                                text: text_style,
-                                                scrollbar_width: cx.editor_style.scrollbar_width,
-                                                syntax: cx.editor_style.syntax.clone(),
-                                                diagnostic_style: cx
-                                                    .editor_style
-                                                    .diagnostic_style
-                                                    .clone(),
-                                            }),
+                                            EditorElement::new(
+                                                &rename_editor,
+                                                EditorStyle {
+                                                    background: cx.theme().system().transparent,
+                                                    local_player: cx.editor_style.local_player,
+                                                    text: text_style,
+                                                    scrollbar_width: cx
+                                                        .editor_style
+                                                        .scrollbar_width,
+                                                    syntax: cx.editor_style.syntax.clone(),
+                                                    diagnostic_style: cx
+                                                        .editor_style
+                                                        .diagnostic_style
+                                                        .clone(),
+                                                },
+                                            ),
                                         ))
                                         .render()
                                 }
@@ -9192,6 +9197,10 @@ impl Editor {
         cx.focus(&self.focus_handle)
     }
 
+    pub fn is_focused(&self, cx: &WindowContext) -> bool {
+        self.focus_handle.is_focused(cx)
+    }
+
     fn handle_focus_in(&mut self, cx: &mut ViewContext<Self>) {
         if self.focus_handle.is_focused(cx) {
             // todo!()
@@ -9403,8 +9412,8 @@ impl Render for Editor {
             EditorMode::SingleLine => {
                 TextStyle {
                     color: cx.theme().colors().text,
-                    font_family: "Zed Sans".into(), // todo!()
-                    font_features: FontFeatures::default(),
+                    font_family: settings.ui_font.family.clone(), // todo!()
+                    font_features: settings.ui_font.features,
                     font_size: rems(0.875).into(),
                     font_weight: FontWeight::NORMAL,
                     font_style: FontStyle::Normal,
@@ -9433,14 +9442,17 @@ impl Render for Editor {
             EditorMode::Full => cx.theme().colors().editor_background,
         };
 
-        EditorElement::new(EditorStyle {
-            background,
-            local_player: cx.theme().players().local(),
-            text: text_style,
-            scrollbar_width: px(12.),
-            syntax: cx.theme().syntax().clone(),
-            diagnostic_style: cx.theme().diagnostic_style(),
-        })
+        EditorElement::new(
+            cx.view(),
+            EditorStyle {
+                background,
+                local_player: cx.theme().players().local(),
+                text: text_style,
+                scrollbar_width: px(12.),
+                syntax: cx.theme().syntax().clone(),
+                diagnostic_style: cx.theme().diagnostic_style(),
+            },
+        )
     }
 }
 

crates/editor2/src/element.rs 🔗

@@ -20,9 +20,9 @@ use collections::{BTreeMap, HashMap};
 use gpui::{
     point, px, relative, size, transparent_black, Action, AnyElement, AvailableSpace, BorrowWindow,
     Bounds, Component, ContentMask, Corners, DispatchPhase, Edges, Element, ElementId,
-    ElementInputHandler, Entity, Hsla, Line, MouseButton, MouseDownEvent, MouseMoveEvent,
-    MouseUpEvent, ParentElement, Pixels, ScrollWheelEvent, Size, Style, Styled, TextRun, TextStyle,
-    ViewContext, WindowContext,
+    ElementInputHandler, Entity, EntityId, Hsla, Line, MouseButton, MouseDownEvent, MouseMoveEvent,
+    MouseUpEvent, ParentComponent, Pixels, ScrollWheelEvent, Size, Style, Styled, TextRun,
+    TextStyle, View, ViewContext, WindowContext,
 };
 use itertools::Itertools;
 use language::language_settings::ShowWhitespaceSetting;
@@ -111,12 +111,16 @@ impl SelectionLayout {
 }
 
 pub struct EditorElement {
+    editor_id: EntityId,
     style: EditorStyle,
 }
 
 impl EditorElement {
-    pub fn new(style: EditorStyle) -> Self {
-        Self { style }
+    pub fn new(editor: &View<Editor>, style: EditorStyle) -> Self {
+        Self {
+            editor_id: editor.entity_id(),
+            style,
+        }
     }
 
     fn mouse_down(
@@ -622,7 +626,7 @@ impl EditorElement {
         let line_end_overshoot = 0.15 * layout.position_map.line_height;
         let whitespace_setting = editor.buffer.read(cx).settings_at(0, cx).show_whitespaces;
 
-        cx.with_content_mask(ContentMask { bounds }, |cx| {
+        cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
             // todo!("cursor region")
             // cx.scene().push_cursor_region(CursorRegion {
             //     bounds,
@@ -1448,6 +1452,7 @@ impl EditorElement {
 
         let snapshot = editor.snapshot(cx);
         let style = self.style.clone();
+
         let font_id = cx.text_system().font_id(&style.text.font()).unwrap();
         let font_size = style.text.font_size.to_pixels(cx.rem_size());
         let line_height = style.text.line_height_in_pixels(cx.rem_size());
@@ -2399,8 +2404,8 @@ enum Invisible {
 impl Element<Editor> for EditorElement {
     type ElementState = ();
 
-    fn id(&self) -> Option<gpui::ElementId> {
-        None
+    fn element_id(&self) -> Option<gpui::ElementId> {
+        Some(self.editor_id.into())
     }
 
     fn initialize(
@@ -2410,186 +2415,6 @@ impl Element<Editor> for EditorElement {
         cx: &mut gpui::ViewContext<Editor>,
     ) -> Self::ElementState {
         editor.style = Some(self.style.clone()); // Long-term, we'd like to eliminate this.
-
-        let dispatch_context = editor.dispatch_context(cx);
-        cx.with_element_id(cx.view().entity_id(), |global_id, cx| {
-            cx.with_key_dispatch(
-                dispatch_context,
-                Some(editor.focus_handle.clone()),
-                |_, cx| {
-                    register_action(cx, Editor::move_left);
-                    register_action(cx, Editor::move_right);
-                    register_action(cx, Editor::move_down);
-                    register_action(cx, Editor::move_up);
-                    // on_action(cx, Editor::new_file); todo!()
-                    // on_action(cx, Editor::new_file_in_direction); todo!()
-                    register_action(cx, Editor::cancel);
-                    register_action(cx, Editor::newline);
-                    register_action(cx, Editor::newline_above);
-                    register_action(cx, Editor::newline_below);
-                    register_action(cx, Editor::backspace);
-                    register_action(cx, Editor::delete);
-                    register_action(cx, Editor::tab);
-                    register_action(cx, Editor::tab_prev);
-                    register_action(cx, Editor::indent);
-                    register_action(cx, Editor::outdent);
-                    register_action(cx, Editor::delete_line);
-                    register_action(cx, Editor::join_lines);
-                    register_action(cx, Editor::sort_lines_case_sensitive);
-                    register_action(cx, Editor::sort_lines_case_insensitive);
-                    register_action(cx, Editor::reverse_lines);
-                    register_action(cx, Editor::shuffle_lines);
-                    register_action(cx, Editor::convert_to_upper_case);
-                    register_action(cx, Editor::convert_to_lower_case);
-                    register_action(cx, Editor::convert_to_title_case);
-                    register_action(cx, Editor::convert_to_snake_case);
-                    register_action(cx, Editor::convert_to_kebab_case);
-                    register_action(cx, Editor::convert_to_upper_camel_case);
-                    register_action(cx, Editor::convert_to_lower_camel_case);
-                    register_action(cx, Editor::delete_to_previous_word_start);
-                    register_action(cx, Editor::delete_to_previous_subword_start);
-                    register_action(cx, Editor::delete_to_next_word_end);
-                    register_action(cx, Editor::delete_to_next_subword_end);
-                    register_action(cx, Editor::delete_to_beginning_of_line);
-                    register_action(cx, Editor::delete_to_end_of_line);
-                    register_action(cx, Editor::cut_to_end_of_line);
-                    register_action(cx, Editor::duplicate_line);
-                    register_action(cx, Editor::move_line_up);
-                    register_action(cx, Editor::move_line_down);
-                    register_action(cx, Editor::transpose);
-                    register_action(cx, Editor::cut);
-                    register_action(cx, Editor::copy);
-                    register_action(cx, Editor::paste);
-                    register_action(cx, Editor::undo);
-                    register_action(cx, Editor::redo);
-                    register_action(cx, Editor::move_page_up);
-                    register_action(cx, Editor::move_page_down);
-                    register_action(cx, Editor::next_screen);
-                    register_action(cx, Editor::scroll_cursor_top);
-                    register_action(cx, Editor::scroll_cursor_center);
-                    register_action(cx, Editor::scroll_cursor_bottom);
-                    register_action(cx, |editor, _: &LineDown, cx| {
-                        editor.scroll_screen(&ScrollAmount::Line(1.), cx)
-                    });
-                    register_action(cx, |editor, _: &LineUp, cx| {
-                        editor.scroll_screen(&ScrollAmount::Line(-1.), cx)
-                    });
-                    register_action(cx, |editor, _: &HalfPageDown, cx| {
-                        editor.scroll_screen(&ScrollAmount::Page(0.5), cx)
-                    });
-                    register_action(cx, |editor, _: &HalfPageUp, cx| {
-                        editor.scroll_screen(&ScrollAmount::Page(-0.5), cx)
-                    });
-                    register_action(cx, |editor, _: &PageDown, cx| {
-                        editor.scroll_screen(&ScrollAmount::Page(1.), cx)
-                    });
-                    register_action(cx, |editor, _: &PageUp, cx| {
-                        editor.scroll_screen(&ScrollAmount::Page(-1.), cx)
-                    });
-                    register_action(cx, Editor::move_to_previous_word_start);
-                    register_action(cx, Editor::move_to_previous_subword_start);
-                    register_action(cx, Editor::move_to_next_word_end);
-                    register_action(cx, Editor::move_to_next_subword_end);
-                    register_action(cx, Editor::move_to_beginning_of_line);
-                    register_action(cx, Editor::move_to_end_of_line);
-                    register_action(cx, Editor::move_to_start_of_paragraph);
-                    register_action(cx, Editor::move_to_end_of_paragraph);
-                    register_action(cx, Editor::move_to_beginning);
-                    register_action(cx, Editor::move_to_end);
-                    register_action(cx, Editor::select_up);
-                    register_action(cx, Editor::select_down);
-                    register_action(cx, Editor::select_left);
-                    register_action(cx, Editor::select_right);
-                    register_action(cx, Editor::select_to_previous_word_start);
-                    register_action(cx, Editor::select_to_previous_subword_start);
-                    register_action(cx, Editor::select_to_next_word_end);
-                    register_action(cx, Editor::select_to_next_subword_end);
-                    register_action(cx, Editor::select_to_beginning_of_line);
-                    register_action(cx, Editor::select_to_end_of_line);
-                    register_action(cx, Editor::select_to_start_of_paragraph);
-                    register_action(cx, Editor::select_to_end_of_paragraph);
-                    register_action(cx, Editor::select_to_beginning);
-                    register_action(cx, Editor::select_to_end);
-                    register_action(cx, Editor::select_all);
-                    register_action(cx, |editor, action, cx| {
-                        editor.select_all_matches(action, cx).log_err();
-                    });
-                    register_action(cx, Editor::select_line);
-                    register_action(cx, Editor::split_selection_into_lines);
-                    register_action(cx, Editor::add_selection_above);
-                    register_action(cx, Editor::add_selection_below);
-                    register_action(cx, |editor, action, cx| {
-                        editor.select_next(action, cx).log_err();
-                    });
-                    register_action(cx, |editor, action, cx| {
-                        editor.select_previous(action, cx).log_err();
-                    });
-                    register_action(cx, Editor::toggle_comments);
-                    register_action(cx, Editor::select_larger_syntax_node);
-                    register_action(cx, Editor::select_smaller_syntax_node);
-                    register_action(cx, Editor::move_to_enclosing_bracket);
-                    register_action(cx, Editor::undo_selection);
-                    register_action(cx, Editor::redo_selection);
-                    register_action(cx, Editor::go_to_diagnostic);
-                    register_action(cx, Editor::go_to_prev_diagnostic);
-                    register_action(cx, Editor::go_to_hunk);
-                    register_action(cx, Editor::go_to_prev_hunk);
-                    register_action(cx, Editor::go_to_definition);
-                    register_action(cx, Editor::go_to_definition_split);
-                    register_action(cx, Editor::go_to_type_definition);
-                    register_action(cx, Editor::go_to_type_definition_split);
-                    register_action(cx, Editor::fold);
-                    register_action(cx, Editor::fold_at);
-                    register_action(cx, Editor::unfold_lines);
-                    register_action(cx, Editor::unfold_at);
-                    register_action(cx, Editor::fold_selected_ranges);
-                    register_action(cx, Editor::show_completions);
-                    register_action(cx, Editor::toggle_code_actions);
-                    // on_action(cx, Editor::open_excerpts); todo!()
-                    register_action(cx, Editor::toggle_soft_wrap);
-                    register_action(cx, Editor::toggle_inlay_hints);
-                    register_action(cx, Editor::reveal_in_finder);
-                    register_action(cx, Editor::copy_path);
-                    register_action(cx, Editor::copy_relative_path);
-                    register_action(cx, Editor::copy_highlight_json);
-                    register_action(cx, |editor, action, cx| {
-                        editor
-                            .format(action, cx)
-                            .map(|task| task.detach_and_log_err(cx));
-                    });
-                    register_action(cx, Editor::restart_language_server);
-                    register_action(cx, Editor::show_character_palette);
-                    // on_action(cx, Editor::confirm_completion); todo!()
-                    register_action(cx, |editor, action, cx| {
-                        editor
-                            .confirm_code_action(action, cx)
-                            .map(|task| task.detach_and_log_err(cx));
-                    });
-                    register_action(cx, |editor, action, cx| {
-                        editor
-                            .rename(action, cx)
-                            .map(|task| task.detach_and_log_err(cx));
-                    });
-                    register_action(cx, |editor, action, cx| {
-                        editor
-                            .confirm_rename(action, cx)
-                            .map(|task| task.detach_and_log_err(cx));
-                    });
-                    register_action(cx, |editor, action, cx| {
-                        editor
-                            .find_all_references(action, cx)
-                            .map(|task| task.detach_and_log_err(cx));
-                    });
-                    register_action(cx, Editor::next_copilot_suggestion);
-                    register_action(cx, Editor::previous_copilot_suggestion);
-                    register_action(cx, Editor::copilot_suggest);
-                    register_action(cx, Editor::context_menu_first);
-                    register_action(cx, Editor::context_menu_prev);
-                    register_action(cx, Editor::context_menu_next);
-                    register_action(cx, Editor::context_menu_last);
-                },
-            )
-        });
     }
 
     fn layout(
@@ -2626,32 +2451,208 @@ impl Element<Editor> for EditorElement {
             size: layout.text_size,
         };
 
-        // We call with_z_index to establish a new stacking context.
-        cx.with_z_index(0, |cx| {
-            cx.with_content_mask(ContentMask { bounds }, |cx| {
-                self.paint_mouse_listeners(
-                    bounds,
-                    gutter_bounds,
-                    text_bounds,
-                    &layout.position_map,
-                    cx,
-                );
-
-                self.paint_background(gutter_bounds, text_bounds, &layout, cx);
-                if layout.gutter_size.width > Pixels::ZERO {
-                    self.paint_gutter(gutter_bounds, &mut layout, editor, cx);
-                }
-
-                self.paint_text(text_bounds, &mut layout, editor, cx);
+        let dispatch_context = editor.dispatch_context(cx);
+        cx.with_key_dispatch(
+            dispatch_context,
+            Some(editor.focus_handle.clone()),
+            |_, cx| {
+                register_action(cx, Editor::move_left);
+                register_action(cx, Editor::move_right);
+                register_action(cx, Editor::move_down);
+                register_action(cx, Editor::move_up);
+                // on_action(cx, Editor::new_file); todo!()
+                // on_action(cx, Editor::new_file_in_direction); todo!()
+                register_action(cx, Editor::cancel);
+                register_action(cx, Editor::newline);
+                register_action(cx, Editor::newline_above);
+                register_action(cx, Editor::newline_below);
+                register_action(cx, Editor::backspace);
+                register_action(cx, Editor::delete);
+                register_action(cx, Editor::tab);
+                register_action(cx, Editor::tab_prev);
+                register_action(cx, Editor::indent);
+                register_action(cx, Editor::outdent);
+                register_action(cx, Editor::delete_line);
+                register_action(cx, Editor::join_lines);
+                register_action(cx, Editor::sort_lines_case_sensitive);
+                register_action(cx, Editor::sort_lines_case_insensitive);
+                register_action(cx, Editor::reverse_lines);
+                register_action(cx, Editor::shuffle_lines);
+                register_action(cx, Editor::convert_to_upper_case);
+                register_action(cx, Editor::convert_to_lower_case);
+                register_action(cx, Editor::convert_to_title_case);
+                register_action(cx, Editor::convert_to_snake_case);
+                register_action(cx, Editor::convert_to_kebab_case);
+                register_action(cx, Editor::convert_to_upper_camel_case);
+                register_action(cx, Editor::convert_to_lower_camel_case);
+                register_action(cx, Editor::delete_to_previous_word_start);
+                register_action(cx, Editor::delete_to_previous_subword_start);
+                register_action(cx, Editor::delete_to_next_word_end);
+                register_action(cx, Editor::delete_to_next_subword_end);
+                register_action(cx, Editor::delete_to_beginning_of_line);
+                register_action(cx, Editor::delete_to_end_of_line);
+                register_action(cx, Editor::cut_to_end_of_line);
+                register_action(cx, Editor::duplicate_line);
+                register_action(cx, Editor::move_line_up);
+                register_action(cx, Editor::move_line_down);
+                register_action(cx, Editor::transpose);
+                register_action(cx, Editor::cut);
+                register_action(cx, Editor::copy);
+                register_action(cx, Editor::paste);
+                register_action(cx, Editor::undo);
+                register_action(cx, Editor::redo);
+                register_action(cx, Editor::move_page_up);
+                register_action(cx, Editor::move_page_down);
+                register_action(cx, Editor::next_screen);
+                register_action(cx, Editor::scroll_cursor_top);
+                register_action(cx, Editor::scroll_cursor_center);
+                register_action(cx, Editor::scroll_cursor_bottom);
+                register_action(cx, |editor, _: &LineDown, cx| {
+                    editor.scroll_screen(&ScrollAmount::Line(1.), cx)
+                });
+                register_action(cx, |editor, _: &LineUp, cx| {
+                    editor.scroll_screen(&ScrollAmount::Line(-1.), cx)
+                });
+                register_action(cx, |editor, _: &HalfPageDown, cx| {
+                    editor.scroll_screen(&ScrollAmount::Page(0.5), cx)
+                });
+                register_action(cx, |editor, _: &HalfPageUp, cx| {
+                    editor.scroll_screen(&ScrollAmount::Page(-0.5), cx)
+                });
+                register_action(cx, |editor, _: &PageDown, cx| {
+                    editor.scroll_screen(&ScrollAmount::Page(1.), cx)
+                });
+                register_action(cx, |editor, _: &PageUp, cx| {
+                    editor.scroll_screen(&ScrollAmount::Page(-1.), cx)
+                });
+                register_action(cx, Editor::move_to_previous_word_start);
+                register_action(cx, Editor::move_to_previous_subword_start);
+                register_action(cx, Editor::move_to_next_word_end);
+                register_action(cx, Editor::move_to_next_subword_end);
+                register_action(cx, Editor::move_to_beginning_of_line);
+                register_action(cx, Editor::move_to_end_of_line);
+                register_action(cx, Editor::move_to_start_of_paragraph);
+                register_action(cx, Editor::move_to_end_of_paragraph);
+                register_action(cx, Editor::move_to_beginning);
+                register_action(cx, Editor::move_to_end);
+                register_action(cx, Editor::select_up);
+                register_action(cx, Editor::select_down);
+                register_action(cx, Editor::select_left);
+                register_action(cx, Editor::select_right);
+                register_action(cx, Editor::select_to_previous_word_start);
+                register_action(cx, Editor::select_to_previous_subword_start);
+                register_action(cx, Editor::select_to_next_word_end);
+                register_action(cx, Editor::select_to_next_subword_end);
+                register_action(cx, Editor::select_to_beginning_of_line);
+                register_action(cx, Editor::select_to_end_of_line);
+                register_action(cx, Editor::select_to_start_of_paragraph);
+                register_action(cx, Editor::select_to_end_of_paragraph);
+                register_action(cx, Editor::select_to_beginning);
+                register_action(cx, Editor::select_to_end);
+                register_action(cx, Editor::select_all);
+                register_action(cx, |editor, action, cx| {
+                    editor.select_all_matches(action, cx).log_err();
+                });
+                register_action(cx, Editor::select_line);
+                register_action(cx, Editor::split_selection_into_lines);
+                register_action(cx, Editor::add_selection_above);
+                register_action(cx, Editor::add_selection_below);
+                register_action(cx, |editor, action, cx| {
+                    editor.select_next(action, cx).log_err();
+                });
+                register_action(cx, |editor, action, cx| {
+                    editor.select_previous(action, cx).log_err();
+                });
+                register_action(cx, Editor::toggle_comments);
+                register_action(cx, Editor::select_larger_syntax_node);
+                register_action(cx, Editor::select_smaller_syntax_node);
+                register_action(cx, Editor::move_to_enclosing_bracket);
+                register_action(cx, Editor::undo_selection);
+                register_action(cx, Editor::redo_selection);
+                register_action(cx, Editor::go_to_diagnostic);
+                register_action(cx, Editor::go_to_prev_diagnostic);
+                register_action(cx, Editor::go_to_hunk);
+                register_action(cx, Editor::go_to_prev_hunk);
+                register_action(cx, Editor::go_to_definition);
+                register_action(cx, Editor::go_to_definition_split);
+                register_action(cx, Editor::go_to_type_definition);
+                register_action(cx, Editor::go_to_type_definition_split);
+                register_action(cx, Editor::fold);
+                register_action(cx, Editor::fold_at);
+                register_action(cx, Editor::unfold_lines);
+                register_action(cx, Editor::unfold_at);
+                register_action(cx, Editor::fold_selected_ranges);
+                register_action(cx, Editor::show_completions);
+                register_action(cx, Editor::toggle_code_actions);
+                // on_action(cx, Editor::open_excerpts); todo!()
+                register_action(cx, Editor::toggle_soft_wrap);
+                register_action(cx, Editor::toggle_inlay_hints);
+                register_action(cx, Editor::reveal_in_finder);
+                register_action(cx, Editor::copy_path);
+                register_action(cx, Editor::copy_relative_path);
+                register_action(cx, Editor::copy_highlight_json);
+                register_action(cx, |editor, action, cx| {
+                    editor
+                        .format(action, cx)
+                        .map(|task| task.detach_and_log_err(cx));
+                });
+                register_action(cx, Editor::restart_language_server);
+                register_action(cx, Editor::show_character_palette);
+                // on_action(cx, Editor::confirm_completion); todo!()
+                register_action(cx, |editor, action, cx| {
+                    editor
+                        .confirm_code_action(action, cx)
+                        .map(|task| task.detach_and_log_err(cx));
+                });
+                register_action(cx, |editor, action, cx| {
+                    editor
+                        .rename(action, cx)
+                        .map(|task| task.detach_and_log_err(cx));
+                });
+                register_action(cx, |editor, action, cx| {
+                    editor
+                        .confirm_rename(action, cx)
+                        .map(|task| task.detach_and_log_err(cx));
+                });
+                register_action(cx, |editor, action, cx| {
+                    editor
+                        .find_all_references(action, cx)
+                        .map(|task| task.detach_and_log_err(cx));
+                });
+                register_action(cx, Editor::next_copilot_suggestion);
+                register_action(cx, Editor::previous_copilot_suggestion);
+                register_action(cx, Editor::copilot_suggest);
+                register_action(cx, Editor::context_menu_first);
+                register_action(cx, Editor::context_menu_prev);
+                register_action(cx, Editor::context_menu_next);
+                register_action(cx, Editor::context_menu_last);
+
+                // We call with_z_index to establish a new stacking context.
+                cx.with_z_index(0, |cx| {
+                    cx.with_content_mask(Some(ContentMask { bounds }), |cx| {
+                        self.paint_mouse_listeners(
+                            bounds,
+                            gutter_bounds,
+                            text_bounds,
+                            &layout.position_map,
+                            cx,
+                        );
+                        self.paint_background(gutter_bounds, text_bounds, &layout, cx);
+                        if layout.gutter_size.width > Pixels::ZERO {
+                            self.paint_gutter(gutter_bounds, &mut layout, editor, cx);
+                        }
+                        self.paint_text(text_bounds, &mut layout, editor, cx);
 
-                if !layout.blocks.is_empty() {
-                    self.paint_blocks(bounds, &mut layout, editor, cx);
-                }
+                        if !layout.blocks.is_empty() {
+                            self.paint_blocks(bounds, &mut layout, editor, cx);
+                        }
 
-                let input_handler = ElementInputHandler::new(bounds, cx);
-                cx.handle_input(&editor.focus_handle, input_handler);
-            });
-        });
+                        let input_handler = ElementInputHandler::new(bounds, cx);
+                        cx.handle_input(&editor.focus_handle, input_handler);
+                    });
+                });
+            },
+        )
     }
 }
 

crates/editor2/src/items.rs 🔗

@@ -9,7 +9,7 @@ use collections::HashSet;
 use futures::future::try_join_all;
 use gpui::{
     div, point, AnyElement, AppContext, AsyncAppContext, Entity, EntityId, EventEmitter,
-    FocusHandle, Model, ParentElement, Pixels, SharedString, Styled, Subscription, Task, View,
+    FocusHandle, Model, ParentComponent, Pixels, SharedString, Styled, Subscription, Task, View,
     ViewContext, VisualContext, WeakView,
 };
 use language::{
@@ -30,7 +30,7 @@ use std::{
 };
 use text::Selection;
 use theme::{ActiveTheme, Theme};
-use ui::{Label, LabelColor};
+use ui::{Label, TextColor};
 use util::{paths::PathExt, ResultExt, TryFutureExt};
 use workspace::item::{BreadcrumbText, FollowEvent, FollowableEvents, FollowableItemHandle};
 use workspace::{
@@ -607,7 +607,7 @@ impl Item for Editor {
                                 &description,
                                 MAX_TAB_TITLE_LEN,
                             ))
-                            .color(LabelColor::Muted),
+                            .color(TextColor::Muted),
                         ),
                     )
                 })),

crates/file_finder2/Cargo.toml 🔗

@@ -0,0 +1,37 @@
+[package]
+name = "file_finder2"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/file_finder.rs"
+doctest = false
+
+[dependencies]
+editor = { package = "editor2", path = "../editor2" }
+collections = { path = "../collections" }
+fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
+gpui = { package = "gpui2", path = "../gpui2" }
+menu = { package = "menu2", path = "../menu2" }
+picker = { package = "picker2", path = "../picker2" }
+project = { package = "project2", path = "../project2" }
+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" }
+postage.workspace = true
+serde.workspace = true
+
+[dev-dependencies]
+editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
+gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
+language = { package = "language2", path = "../language2", features = ["test-support"] }
+workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }
+theme = { package = "theme2", path = "../theme2", features = ["test-support"] }
+
+serde_json.workspace = true
+ctor.workspace = true
+env_logger.workspace = true

crates/file_finder2/src/file_finder.rs 🔗

@@ -0,0 +1,1973 @@
+use collections::HashMap;
+use editor::{scroll::autoscroll::Autoscroll, Bias, Editor};
+use fuzzy::{CharBag, PathMatch, PathMatchCandidate};
+use gpui::{
+    actions, div, AppContext, Component, Div, EventEmitter, InteractiveComponent, Model,
+    ParentComponent, Render, Styled, Task, View, ViewContext, VisualContext, WeakView,
+    WindowContext,
+};
+use picker::{Picker, PickerDelegate};
+use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
+use std::{
+    path::{Path, PathBuf},
+    sync::{
+        atomic::{self, AtomicBool},
+        Arc,
+    },
+};
+use text::Point;
+use theme::ActiveTheme;
+use ui::{v_stack, HighlightedLabel, StyledExt};
+use util::{paths::PathLikeWithPosition, post_inc, ResultExt};
+use workspace::{Modal, ModalEvent, Workspace};
+
+actions!(Toggle);
+
+pub struct FileFinder {
+    picker: View<Picker<FileFinderDelegate>>,
+}
+
+pub fn init(cx: &mut AppContext) {
+    cx.observe_new_views(FileFinder::register).detach();
+}
+
+impl FileFinder {
+    fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
+        workspace.register_action(|workspace, _: &Toggle, cx| {
+            let Some(file_finder) = workspace.current_modal::<Self>(cx) else {
+                Self::open(workspace, cx);
+                return;
+            };
+
+            file_finder.update(cx, |file_finder, cx| {
+                file_finder
+                    .picker
+                    .update(cx, |picker, cx| picker.cycle_selection(cx))
+            });
+        });
+    }
+
+    fn open(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
+        let project = workspace.project().read(cx);
+
+        let currently_opened_path = workspace
+            .active_item(cx)
+            .and_then(|item| item.project_path(cx))
+            .map(|project_path| {
+                let abs_path = project
+                    .worktree_for_id(project_path.worktree_id, cx)
+                    .map(|worktree| worktree.read(cx).abs_path().join(&project_path.path));
+                FoundPath::new(project_path, abs_path)
+            });
+
+        // if exists, bubble the currently opened path to the top
+        let history_items = currently_opened_path
+            .clone()
+            .into_iter()
+            .chain(
+                workspace
+                    .recent_navigation_history(Some(MAX_RECENT_SELECTIONS), cx)
+                    .into_iter()
+                    .filter(|(history_path, _)| {
+                        Some(history_path)
+                            != currently_opened_path
+                                .as_ref()
+                                .map(|found_path| &found_path.project)
+                    })
+                    .filter(|(_, history_abs_path)| {
+                        history_abs_path.as_ref()
+                            != currently_opened_path
+                                .as_ref()
+                                .and_then(|found_path| found_path.absolute.as_ref())
+                    })
+                    .filter(|(_, history_abs_path)| match history_abs_path {
+                        Some(abs_path) => history_file_exists(abs_path),
+                        None => true,
+                    })
+                    .map(|(history_path, abs_path)| FoundPath::new(history_path, abs_path)),
+            )
+            .collect();
+
+        let project = workspace.project().clone();
+        let weak_workspace = cx.view().downgrade();
+        workspace.toggle_modal(cx, |cx| {
+            let delegate = FileFinderDelegate::new(
+                cx.view().downgrade(),
+                weak_workspace,
+                project,
+                currently_opened_path,
+                history_items,
+                cx,
+            );
+
+            FileFinder::new(delegate, cx)
+        });
+    }
+
+    fn new(delegate: FileFinderDelegate, cx: &mut ViewContext<Self>) -> Self {
+        Self {
+            picker: cx.build_view(|cx| Picker::new(delegate, cx)),
+        }
+    }
+}
+
+impl EventEmitter<ModalEvent> for FileFinder {}
+impl Modal for FileFinder {
+    fn focus(&self, cx: &mut WindowContext) {
+        self.picker.update(cx, |picker, cx| picker.focus(cx))
+    }
+}
+impl Render for FileFinder {
+    type Element = Div<Self>;
+
+    fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
+        v_stack().w_96().child(self.picker.clone())
+    }
+}
+
+pub struct FileFinderDelegate {
+    file_finder: WeakView<FileFinder>,
+    workspace: WeakView<Workspace>,
+    project: Model<Project>,
+    search_count: usize,
+    latest_search_id: usize,
+    latest_search_did_cancel: bool,
+    latest_search_query: Option<PathLikeWithPosition<FileSearchQuery>>,
+    currently_opened_path: Option<FoundPath>,
+    matches: Matches,
+    selected_index: Option<usize>,
+    cancel_flag: Arc<AtomicBool>,
+    history_items: Vec<FoundPath>,
+}
+
+#[derive(Debug, Default)]
+struct Matches {
+    history: Vec<(FoundPath, Option<PathMatch>)>,
+    search: Vec<PathMatch>,
+}
+
+#[derive(Debug)]
+enum Match<'a> {
+    History(&'a FoundPath, Option<&'a PathMatch>),
+    Search(&'a PathMatch),
+}
+
+impl Matches {
+    fn len(&self) -> usize {
+        self.history.len() + self.search.len()
+    }
+
+    fn get(&self, index: usize) -> Option<Match<'_>> {
+        if index < self.history.len() {
+            self.history
+                .get(index)
+                .map(|(path, path_match)| Match::History(path, path_match.as_ref()))
+        } else {
+            self.search
+                .get(index - self.history.len())
+                .map(Match::Search)
+        }
+    }
+
+    fn push_new_matches(
+        &mut self,
+        history_items: &Vec<FoundPath>,
+        query: &PathLikeWithPosition<FileSearchQuery>,
+        mut new_search_matches: Vec<PathMatch>,
+        extend_old_matches: bool,
+    ) {
+        let matching_history_paths = matching_history_item_paths(history_items, query);
+        new_search_matches
+            .retain(|path_match| !matching_history_paths.contains_key(&path_match.path));
+        let history_items_to_show = history_items
+            .iter()
+            .filter_map(|history_item| {
+                Some((
+                    history_item.clone(),
+                    Some(
+                        matching_history_paths
+                            .get(&history_item.project.path)?
+                            .clone(),
+                    ),
+                ))
+            })
+            .collect::<Vec<_>>();
+        self.history = history_items_to_show;
+        if extend_old_matches {
+            self.search
+                .retain(|path_match| !matching_history_paths.contains_key(&path_match.path));
+            util::extend_sorted(
+                &mut self.search,
+                new_search_matches.into_iter(),
+                100,
+                |a, b| b.cmp(a),
+            )
+        } else {
+            self.search = new_search_matches;
+        }
+    }
+}
+
+fn matching_history_item_paths(
+    history_items: &Vec<FoundPath>,
+    query: &PathLikeWithPosition<FileSearchQuery>,
+) -> HashMap<Arc<Path>, PathMatch> {
+    let history_items_by_worktrees = history_items
+        .iter()
+        .filter_map(|found_path| {
+            let candidate = PathMatchCandidate {
+                path: &found_path.project.path,
+                // Only match history items names, otherwise their paths may match too many queries, producing false positives.
+                // E.g. `foo` would match both `something/foo/bar.rs` and `something/foo/foo.rs` and if the former is a history item,
+                // it would be shown first always, despite the latter being a better match.
+                char_bag: CharBag::from_iter(
+                    found_path
+                        .project
+                        .path
+                        .file_name()?
+                        .to_string_lossy()
+                        .to_lowercase()
+                        .chars(),
+                ),
+            };
+            Some((found_path.project.worktree_id, candidate))
+        })
+        .fold(
+            HashMap::default(),
+            |mut candidates, (worktree_id, new_candidate)| {
+                candidates
+                    .entry(worktree_id)
+                    .or_insert_with(Vec::new)
+                    .push(new_candidate);
+                candidates
+            },
+        );
+    let mut matching_history_paths = HashMap::default();
+    for (worktree, candidates) in history_items_by_worktrees {
+        let max_results = candidates.len() + 1;
+        matching_history_paths.extend(
+            fuzzy::match_fixed_path_set(
+                candidates,
+                worktree.to_usize(),
+                query.path_like.path_query(),
+                false,
+                max_results,
+            )
+            .into_iter()
+            .map(|path_match| (Arc::clone(&path_match.path), path_match)),
+        );
+    }
+    matching_history_paths
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+struct FoundPath {
+    project: ProjectPath,
+    absolute: Option<PathBuf>,
+}
+
+impl FoundPath {
+    fn new(project: ProjectPath, absolute: Option<PathBuf>) -> Self {
+        Self { project, absolute }
+    }
+}
+
+const MAX_RECENT_SELECTIONS: usize = 20;
+
+#[cfg(not(test))]
+fn history_file_exists(abs_path: &PathBuf) -> bool {
+    abs_path.exists()
+}
+
+#[cfg(test)]
+fn history_file_exists(abs_path: &PathBuf) -> bool {
+    !abs_path.ends_with("nonexistent.rs")
+}
+
+pub enum Event {
+    Selected(ProjectPath),
+    Dismissed,
+}
+
+#[derive(Debug, Clone)]
+struct FileSearchQuery {
+    raw_query: String,
+    file_query_end: Option<usize>,
+}
+
+impl FileSearchQuery {
+    fn path_query(&self) -> &str {
+        match self.file_query_end {
+            Some(file_path_end) => &self.raw_query[..file_path_end],
+            None => &self.raw_query,
+        }
+    }
+}
+
+impl FileFinderDelegate {
+    fn new(
+        file_finder: WeakView<FileFinder>,
+        workspace: WeakView<Workspace>,
+        project: Model<Project>,
+        currently_opened_path: Option<FoundPath>,
+        history_items: Vec<FoundPath>,
+        cx: &mut ViewContext<FileFinder>,
+    ) -> Self {
+        cx.observe(&project, |file_finder, _, cx| {
+            //todo!() We should probably not re-render on every project anything
+            file_finder
+                .picker
+                .update(cx, |picker, cx| picker.refresh(cx))
+        })
+        .detach();
+
+        Self {
+            file_finder,
+            workspace,
+            project,
+            search_count: 0,
+            latest_search_id: 0,
+            latest_search_did_cancel: false,
+            latest_search_query: None,
+            currently_opened_path,
+            matches: Matches::default(),
+            selected_index: None,
+            cancel_flag: Arc::new(AtomicBool::new(false)),
+            history_items,
+        }
+    }
+
+    fn spawn_search(
+        &mut self,
+        query: PathLikeWithPosition<FileSearchQuery>,
+        cx: &mut ViewContext<Picker<Self>>,
+    ) -> Task<()> {
+        let relative_to = self
+            .currently_opened_path
+            .as_ref()
+            .map(|found_path| Arc::clone(&found_path.project.path));
+        let worktrees = self
+            .project
+            .read(cx)
+            .visible_worktrees(cx)
+            .collect::<Vec<_>>();
+        let include_root_name = worktrees.len() > 1;
+        let candidate_sets = worktrees
+            .into_iter()
+            .map(|worktree| {
+                let worktree = worktree.read(cx);
+                PathMatchCandidateSet {
+                    snapshot: worktree.snapshot(),
+                    include_ignored: worktree
+                        .root_entry()
+                        .map_or(false, |entry| entry.is_ignored),
+                    include_root_name,
+                }
+            })
+            .collect::<Vec<_>>();
+
+        let search_id = util::post_inc(&mut self.search_count);
+        self.cancel_flag.store(true, atomic::Ordering::Relaxed);
+        self.cancel_flag = Arc::new(AtomicBool::new(false));
+        let cancel_flag = self.cancel_flag.clone();
+        cx.spawn(|picker, mut cx| async move {
+            let matches = fuzzy::match_path_sets(
+                candidate_sets.as_slice(),
+                query.path_like.path_query(),
+                relative_to,
+                false,
+                100,
+                &cancel_flag,
+                cx.background_executor().clone(),
+            )
+            .await;
+            let did_cancel = cancel_flag.load(atomic::Ordering::Relaxed);
+            picker
+                .update(&mut cx, |picker, cx| {
+                    picker
+                        .delegate
+                        .set_search_matches(search_id, did_cancel, query, matches, cx)
+                })
+                .log_err();
+        })
+    }
+
+    fn set_search_matches(
+        &mut self,
+        search_id: usize,
+        did_cancel: bool,
+        query: PathLikeWithPosition<FileSearchQuery>,
+        matches: Vec<PathMatch>,
+        cx: &mut ViewContext<Picker<Self>>,
+    ) {
+        if search_id >= self.latest_search_id {
+            self.latest_search_id = search_id;
+            let extend_old_matches = self.latest_search_did_cancel
+                && Some(query.path_like.path_query())
+                    == self
+                        .latest_search_query
+                        .as_ref()
+                        .map(|query| query.path_like.path_query());
+            self.matches
+                .push_new_matches(&self.history_items, &query, matches, extend_old_matches);
+            self.latest_search_query = Some(query);
+            self.latest_search_did_cancel = did_cancel;
+            cx.notify();
+        }
+    }
+
+    fn labels_for_match(
+        &self,
+        path_match: Match,
+        cx: &AppContext,
+        ix: usize,
+    ) -> (String, Vec<usize>, String, Vec<usize>) {
+        let (file_name, file_name_positions, full_path, full_path_positions) = match path_match {
+            Match::History(found_path, found_path_match) => {
+                let worktree_id = found_path.project.worktree_id;
+                let project_relative_path = &found_path.project.path;
+                let has_worktree = self
+                    .project
+                    .read(cx)
+                    .worktree_for_id(worktree_id, cx)
+                    .is_some();
+
+                if !has_worktree {
+                    if let Some(absolute_path) = &found_path.absolute {
+                        return (
+                            absolute_path
+                                .file_name()
+                                .map_or_else(
+                                    || project_relative_path.to_string_lossy(),
+                                    |file_name| file_name.to_string_lossy(),
+                                )
+                                .to_string(),
+                            Vec::new(),
+                            absolute_path.to_string_lossy().to_string(),
+                            Vec::new(),
+                        );
+                    }
+                }
+
+                let mut path = Arc::clone(project_relative_path);
+                if project_relative_path.as_ref() == Path::new("") {
+                    if let Some(absolute_path) = &found_path.absolute {
+                        path = Arc::from(absolute_path.as_path());
+                    }
+                }
+
+                let mut path_match = PathMatch {
+                    score: ix as f64,
+                    positions: Vec::new(),
+                    worktree_id: worktree_id.to_usize(),
+                    path,
+                    path_prefix: "".into(),
+                    distance_to_relative_ancestor: usize::MAX,
+                };
+                if let Some(found_path_match) = found_path_match {
+                    path_match
+                        .positions
+                        .extend(found_path_match.positions.iter())
+                }
+
+                self.labels_for_path_match(&path_match)
+            }
+            Match::Search(path_match) => self.labels_for_path_match(path_match),
+        };
+
+        if file_name_positions.is_empty() {
+            if let Some(user_home_path) = std::env::var("HOME").ok() {
+                let user_home_path = user_home_path.trim();
+                if !user_home_path.is_empty() {
+                    if (&full_path).starts_with(user_home_path) {
+                        return (
+                            file_name,
+                            file_name_positions,
+                            full_path.replace(user_home_path, "~"),
+                            full_path_positions,
+                        );
+                    }
+                }
+            }
+        }
+
+        (
+            file_name,
+            file_name_positions,
+            full_path,
+            full_path_positions,
+        )
+    }
+
+    fn labels_for_path_match(
+        &self,
+        path_match: &PathMatch,
+    ) -> (String, Vec<usize>, String, Vec<usize>) {
+        let path = &path_match.path;
+        let path_string = path.to_string_lossy();
+        let full_path = [path_match.path_prefix.as_ref(), path_string.as_ref()].join("");
+        let path_positions = path_match.positions.clone();
+
+        let file_name = path.file_name().map_or_else(
+            || path_match.path_prefix.to_string(),
+            |file_name| file_name.to_string_lossy().to_string(),
+        );
+        let file_name_start = path_match.path_prefix.chars().count() + path_string.chars().count()
+            - file_name.chars().count();
+        let file_name_positions = path_positions
+            .iter()
+            .filter_map(|pos| {
+                if pos >= &file_name_start {
+                    Some(pos - file_name_start)
+                } else {
+                    None
+                }
+            })
+            .collect();
+
+        (file_name, file_name_positions, full_path, path_positions)
+    }
+}
+
+impl PickerDelegate for FileFinderDelegate {
+    type ListItem = Div<Picker<Self>>;
+
+    fn placeholder_text(&self) -> Arc<str> {
+        "Search project files...".into()
+    }
+
+    fn match_count(&self) -> usize {
+        self.matches.len()
+    }
+
+    fn selected_index(&self) -> usize {
+        self.selected_index.unwrap_or(0)
+    }
+
+    fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
+        self.selected_index = Some(ix);
+        cx.notify();
+    }
+
+    fn update_matches(
+        &mut self,
+        raw_query: String,
+        cx: &mut ViewContext<Picker<Self>>,
+    ) -> Task<()> {
+        if raw_query.is_empty() {
+            let project = self.project.read(cx);
+            self.latest_search_id = post_inc(&mut self.search_count);
+            self.matches = Matches {
+                history: self
+                    .history_items
+                    .iter()
+                    .filter(|history_item| {
+                        project
+                            .worktree_for_id(history_item.project.worktree_id, cx)
+                            .is_some()
+                            || (project.is_local() && history_item.absolute.is_some())
+                    })
+                    .cloned()
+                    .map(|p| (p, None))
+                    .collect(),
+                search: Vec::new(),
+            };
+            cx.notify();
+            Task::ready(())
+        } else {
+            let raw_query = &raw_query;
+            let query = PathLikeWithPosition::parse_str(raw_query, |path_like_str| {
+                Ok::<_, std::convert::Infallible>(FileSearchQuery {
+                    raw_query: raw_query.to_owned(),
+                    file_query_end: if path_like_str == raw_query {
+                        None
+                    } else {
+                        Some(path_like_str.len())
+                    },
+                })
+            })
+            .expect("infallible");
+            self.spawn_search(query, cx)
+        }
+    }
+
+    fn confirm(&mut self, secondary: bool, cx: &mut ViewContext<Picker<FileFinderDelegate>>) {
+        if let Some(m) = self.matches.get(self.selected_index()) {
+            if let Some(workspace) = self.workspace.upgrade() {
+                let open_task = workspace.update(cx, move |workspace, cx| {
+                    let split_or_open = |workspace: &mut Workspace, project_path, cx| {
+                        if secondary {
+                            workspace.split_path(project_path, cx)
+                        } else {
+                            workspace.open_path(project_path, None, true, cx)
+                        }
+                    };
+                    match m {
+                        Match::History(history_match, _) => {
+                            let worktree_id = history_match.project.worktree_id;
+                            if workspace
+                                .project()
+                                .read(cx)
+                                .worktree_for_id(worktree_id, cx)
+                                .is_some()
+                            {
+                                split_or_open(
+                                    workspace,
+                                    ProjectPath {
+                                        worktree_id,
+                                        path: Arc::clone(&history_match.project.path),
+                                    },
+                                    cx,
+                                )
+                            } else {
+                                match history_match.absolute.as_ref() {
+                                    Some(abs_path) => {
+                                        if secondary {
+                                            workspace.split_abs_path(
+                                                abs_path.to_path_buf(),
+                                                false,
+                                                cx,
+                                            )
+                                        } else {
+                                            workspace.open_abs_path(
+                                                abs_path.to_path_buf(),
+                                                false,
+                                                cx,
+                                            )
+                                        }
+                                    }
+                                    None => split_or_open(
+                                        workspace,
+                                        ProjectPath {
+                                            worktree_id,
+                                            path: Arc::clone(&history_match.project.path),
+                                        },
+                                        cx,
+                                    ),
+                                }
+                            }
+                        }
+                        Match::Search(m) => split_or_open(
+                            workspace,
+                            ProjectPath {
+                                worktree_id: WorktreeId::from_usize(m.worktree_id),
+                                path: m.path.clone(),
+                            },
+                            cx,
+                        ),
+                    }
+                });
+
+                let row = self
+                    .latest_search_query
+                    .as_ref()
+                    .and_then(|query| query.row)
+                    .map(|row| row.saturating_sub(1));
+                let col = self
+                    .latest_search_query
+                    .as_ref()
+                    .and_then(|query| query.column)
+                    .unwrap_or(0)
+                    .saturating_sub(1);
+                let finder = self.file_finder.clone();
+
+                cx.spawn(|_, mut cx| async move {
+                    let item = open_task.await.log_err()?;
+                    if let Some(row) = row {
+                        if let Some(active_editor) = item.downcast::<Editor>() {
+                            active_editor
+                                .downgrade()
+                                .update(&mut cx, |editor, cx| {
+                                    let snapshot = editor.snapshot(cx).display_snapshot;
+                                    let point = snapshot
+                                        .buffer_snapshot
+                                        .clip_point(Point::new(row, col), Bias::Left);
+                                    editor.change_selections(Some(Autoscroll::center()), cx, |s| {
+                                        s.select_ranges([point..point])
+                                    });
+                                })
+                                .log_err();
+                        }
+                    }
+                    finder
+                        .update(&mut cx, |_, cx| cx.emit(ModalEvent::Dismissed))
+                        .ok()?;
+
+                    Some(())
+                })
+                .detach();
+            }
+        }
+    }
+
+    fn dismissed(&mut self, cx: &mut ViewContext<Picker<FileFinderDelegate>>) {
+        self.file_finder
+            .update(cx, |_, cx| cx.emit(ModalEvent::Dismissed))
+            .log_err();
+    }
+
+    fn render_match(
+        &self,
+        ix: usize,
+        selected: bool,
+        cx: &mut ViewContext<Picker<Self>>,
+    ) -> Self::ListItem {
+        let path_match = self
+            .matches
+            .get(ix)
+            .expect("Invalid matches state: no element for index {ix}");
+        let theme = cx.theme();
+        let colors = theme.colors();
+
+        let (file_name, file_name_positions, full_path, full_path_positions) =
+            self.labels_for_match(path_match, cx, ix);
+
+        div()
+            .px_1()
+            .text_color(colors.text)
+            .text_ui()
+            .bg(colors.ghost_element_background)
+            .rounded_md()
+            .when(selected, |this| this.bg(colors.ghost_element_selected))
+            .hover(|this| this.bg(colors.ghost_element_hover))
+            .child(
+                v_stack()
+                    .child(HighlightedLabel::new(file_name, file_name_positions))
+                    .child(HighlightedLabel::new(full_path, full_path_positions)),
+            )
+    }
+}
+
+// #[cfg(test)]
+// mod tests {
+//     use std::{assert_eq, collections::HashMap, path::Path, time::Duration};
+
+//     use super::*;
+//     use editor::Editor;
+//     use gpui::{Entity, TestAppContext, VisualTestContext};
+//     use menu::{Confirm, SelectNext};
+//     use serde_json::json;
+//     use workspace::{AppState, Workspace};
+
+//     #[ctor::ctor]
+//     fn init_logger() {
+//         if std::env::var("RUST_LOG").is_ok() {
+//             env_logger::init();
+//         }
+//     }
+
+//     #[gpui::test]
+//     async fn test_matching_paths(cx: &mut TestAppContext) {
+//         let app_state = init_test(cx);
+//         app_state
+//             .fs
+//             .as_fake()
+//             .insert_tree(
+//                 "/root",
+//                 json!({
+//                     "a": {
+//                         "banana": "",
+//                         "bandana": "",
+//                     }
+//                 }),
+//             )
+//             .await;
+
+//         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
+
+//         let (picker, workspace, mut cx) = build_find_picker(project, cx);
+//         let cx = &mut cx;
+
+//         picker
+//             .update(cx, |picker, cx| {
+//                 picker.delegate.update_matches("bna".to_string(), cx)
+//             })
+//             .await;
+
+//         picker.update(cx, |picker, _| {
+//             assert_eq!(picker.delegate.matches.len(), 2);
+//         });
+
+//         let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
+//         cx.dispatch_action(SelectNext);
+//         cx.dispatch_action(Confirm);
+//         active_pane
+//             .condition(cx, |pane, _| pane.active_item().is_some())
+//             .await;
+//         cx.read(|cx| {
+//             let active_item = active_pane.read(cx).active_item().unwrap();
+//             assert_eq!(
+//                 active_item
+//                     .to_any()
+//                     .downcast::<Editor>()
+//                     .unwrap()
+//                     .read(cx)
+//                     .title(cx),
+//                 "bandana"
+//             );
+//         });
+//     }
+
+//     #[gpui::test]
+//     async fn test_row_column_numbers_query_inside_file(cx: &mut TestAppContext) {
+//         let app_state = init_test(cx);
+
+//         let first_file_name = "first.rs";
+//         let first_file_contents = "// First Rust file";
+//         app_state
+//             .fs
+//             .as_fake()
+//             .insert_tree(
+//                 "/src",
+//                 json!({
+//                     "test": {
+//                         first_file_name: first_file_contents,
+//                         "second.rs": "// Second Rust file",
+//                     }
+//                 }),
+//             )
+//             .await;
+
+//         let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
+
+//         let (picker, workspace, mut cx) = build_find_picker(project, cx);
+//         let cx = &mut cx;
+
+//         let file_query = &first_file_name[..3];
+//         let file_row = 1;
+//         let file_column = 3;
+//         assert!(file_column <= first_file_contents.len());
+//         let query_inside_file = format!("{file_query}:{file_row}:{file_column}");
+//         picker
+//             .update(cx, |finder, cx| {
+//                 finder
+//                     .delegate
+//                     .update_matches(query_inside_file.to_string(), cx)
+//             })
+//             .await;
+//         picker.update(cx, |finder, _| {
+//             let finder = &finder.delegate;
+//             assert_eq!(finder.matches.len(), 1);
+//             let latest_search_query = finder
+//                 .latest_search_query
+//                 .as_ref()
+//                 .expect("Finder should have a query after the update_matches call");
+//             assert_eq!(latest_search_query.path_like.raw_query, query_inside_file);
+//             assert_eq!(
+//                 latest_search_query.path_like.file_query_end,
+//                 Some(file_query.len())
+//             );
+//             assert_eq!(latest_search_query.row, Some(file_row));
+//             assert_eq!(latest_search_query.column, Some(file_column as u32));
+//         });
+
+//         let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
+//         cx.dispatch_action(SelectNext);
+//         cx.dispatch_action(Confirm);
+//         active_pane
+//             .condition(cx, |pane, _| pane.active_item().is_some())
+//             .await;
+//         let editor = cx.update(|cx| {
+//             let active_item = active_pane.read(cx).active_item().unwrap();
+//             active_item.downcast::<Editor>().unwrap()
+//         });
+//         cx.executor().advance_clock(Duration::from_secs(2));
+
+//         editor.update(cx, |editor, cx| {
+//             let all_selections = editor.selections.all_adjusted(cx);
+//             assert_eq!(
+//                 all_selections.len(),
+//                 1,
+//                 "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}"
+//             );
+//             let caret_selection = all_selections.into_iter().next().unwrap();
+//             assert_eq!(caret_selection.start, caret_selection.end,
+//                 "Caret selection should have its start and end at the same position");
+//             assert_eq!(file_row, caret_selection.start.row + 1,
+//                 "Query inside file should get caret with the same focus row");
+//             assert_eq!(file_column, caret_selection.start.column as usize + 1,
+//                 "Query inside file should get caret with the same focus column");
+//         });
+//     }
+
+//     #[gpui::test]
+//     async fn test_row_column_numbers_query_outside_file(cx: &mut TestAppContext) {
+//         let app_state = init_test(cx);
+
+//         let first_file_name = "first.rs";
+//         let first_file_contents = "// First Rust file";
+//         app_state
+//             .fs
+//             .as_fake()
+//             .insert_tree(
+//                 "/src",
+//                 json!({
+//                     "test": {
+//                         first_file_name: first_file_contents,
+//                         "second.rs": "// Second Rust file",
+//                     }
+//                 }),
+//             )
+//             .await;
+
+//         let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
+
+//         let (picker, workspace, mut cx) = build_find_picker(project, cx);
+//         let cx = &mut cx;
+
+//         let file_query = &first_file_name[..3];
+//         let file_row = 200;
+//         let file_column = 300;
+//         assert!(file_column > first_file_contents.len());
+//         let query_outside_file = format!("{file_query}:{file_row}:{file_column}");
+//         picker
+//             .update(cx, |picker, cx| {
+//                 picker
+//                     .delegate
+//                     .update_matches(query_outside_file.to_string(), cx)
+//             })
+//             .await;
+//         picker.update(cx, |finder, _| {
+//             let delegate = &finder.delegate;
+//             assert_eq!(delegate.matches.len(), 1);
+//             let latest_search_query = delegate
+//                 .latest_search_query
+//                 .as_ref()
+//                 .expect("Finder should have a query after the update_matches call");
+//             assert_eq!(latest_search_query.path_like.raw_query, query_outside_file);
+//             assert_eq!(
+//                 latest_search_query.path_like.file_query_end,
+//                 Some(file_query.len())
+//             );
+//             assert_eq!(latest_search_query.row, Some(file_row));
+//             assert_eq!(latest_search_query.column, Some(file_column as u32));
+//         });
+
+//         let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
+//         cx.dispatch_action(SelectNext);
+//         cx.dispatch_action(Confirm);
+//         active_pane
+//             .condition(cx, |pane, _| pane.active_item().is_some())
+//             .await;
+//         let editor = cx.update(|cx| {
+//             let active_item = active_pane.read(cx).active_item().unwrap();
+//             active_item.downcast::<Editor>().unwrap()
+//         });
+//         cx.executor().advance_clock(Duration::from_secs(2));
+
+//         editor.update(cx, |editor, cx| {
+//             let all_selections = editor.selections.all_adjusted(cx);
+//             assert_eq!(
+//                 all_selections.len(),
+//                 1,
+//                 "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}"
+//             );
+//             let caret_selection = all_selections.into_iter().next().unwrap();
+//             assert_eq!(caret_selection.start, caret_selection.end,
+//                 "Caret selection should have its start and end at the same position");
+//             assert_eq!(0, caret_selection.start.row,
+//                 "Excessive rows (as in query outside file borders) should get trimmed to last file row");
+//             assert_eq!(first_file_contents.len(), caret_selection.start.column as usize,
+//                 "Excessive columns (as in query outside file borders) should get trimmed to selected row's last column");
+//         });
+//     }
+
+//     #[gpui::test]
+//     async fn test_matching_cancellation(cx: &mut TestAppContext) {
+//         let app_state = init_test(cx);
+//         app_state
+//             .fs
+//             .as_fake()
+//             .insert_tree(
+//                 "/dir",
+//                 json!({
+//                     "hello": "",
+//                     "goodbye": "",
+//                     "halogen-light": "",
+//                     "happiness": "",
+//                     "height": "",
+//                     "hi": "",
+//                     "hiccup": "",
+//                 }),
+//             )
+//             .await;
+
+//         let project = Project::test(app_state.fs.clone(), ["/dir".as_ref()], cx).await;
+
+//         let (picker, _, mut cx) = build_find_picker(project, cx);
+//         let cx = &mut cx;
+
+//         let query = test_path_like("hi");
+//         picker
+//             .update(cx, |picker, cx| {
+//                 picker.delegate.spawn_search(query.clone(), cx)
+//             })
+//             .await;
+
+//         picker.update(cx, |picker, _cx| {
+//             assert_eq!(picker.delegate.matches.len(), 5)
+//         });
+
+//         picker.update(cx, |picker, cx| {
+//             let delegate = &mut picker.delegate;
+//             assert!(
+//                 delegate.matches.history.is_empty(),
+//                 "Search matches expected"
+//             );
+//             let matches = delegate.matches.search.clone();
+
+//             // Simulate a search being cancelled after the time limit,
+//             // returning only a subset of the matches that would have been found.
+//             drop(delegate.spawn_search(query.clone(), cx));
+//             delegate.set_search_matches(
+//                 delegate.latest_search_id,
+//                 true, // did-cancel
+//                 query.clone(),
+//                 vec![matches[1].clone(), matches[3].clone()],
+//                 cx,
+//             );
+
+//             // Simulate another cancellation.
+//             drop(delegate.spawn_search(query.clone(), cx));
+//             delegate.set_search_matches(
+//                 delegate.latest_search_id,
+//                 true, // did-cancel
+//                 query.clone(),
+//                 vec![matches[0].clone(), matches[2].clone(), matches[3].clone()],
+//                 cx,
+//             );
+
+//             assert!(
+//                 delegate.matches.history.is_empty(),
+//                 "Search matches expected"
+//             );
+//             assert_eq!(delegate.matches.search.as_slice(), &matches[0..4]);
+//         });
+//     }
+
+//     #[gpui::test]
+//     async fn test_ignored_files(cx: &mut TestAppContext) {
+//         let app_state = init_test(cx);
+//         app_state
+//             .fs
+//             .as_fake()
+//             .insert_tree(
+//                 "/ancestor",
+//                 json!({
+//                     ".gitignore": "ignored-root",
+//                     "ignored-root": {
+//                         "happiness": "",
+//                         "height": "",
+//                         "hi": "",
+//                         "hiccup": "",
+//                     },
+//                     "tracked-root": {
+//                         ".gitignore": "height",
+//                         "happiness": "",
+//                         "height": "",
+//                         "hi": "",
+//                         "hiccup": "",
+//                     },
+//                 }),
+//             )
+//             .await;
+
+//         let project = Project::test(
+//             app_state.fs.clone(),
+//             [
+//                 "/ancestor/tracked-root".as_ref(),
+//                 "/ancestor/ignored-root".as_ref(),
+//             ],
+//             cx,
+//         )
+//         .await;
+
+//         let (picker, _, mut cx) = build_find_picker(project, cx);
+//         let cx = &mut cx;
+
+//         picker
+//             .update(cx, |picker, cx| {
+//                 picker.delegate.spawn_search(test_path_like("hi"), cx)
+//             })
+//             .await;
+//         picker.update(cx, |picker, _| assert_eq!(picker.delegate.matches.len(), 7));
+//     }
+
+// #[gpui::test]
+// async fn test_single_file_worktrees(cx: &mut TestAppContext) {
+//     let app_state = init_test(cx);
+//     app_state
+//         .fs
+//         .as_fake()
+//         .insert_tree("/root", json!({ "the-parent-dir": { "the-file": "" } }))
+//         .await;
+
+//     let project = Project::test(
+//         app_state.fs.clone(),
+//         ["/root/the-parent-dir/the-file".as_ref()],
+//         cx,
+//     )
+//     .await;
+
+//     let (picker, _, mut cx) = build_find_picker(project, cx);
+//     let cx = &mut cx;
+
+//     // Even though there is only one worktree, that worktree's filename
+//     // is included in the matching, because the worktree is a single file.
+//     picker
+//         .update(cx, |picker, cx| {
+//             picker.delegate.spawn_search(test_path_like("thf"), cx)
+//         })
+//         .await;
+//     cx.read(|cx| {
+//         let picker = picker.read(cx);
+//         let delegate = &picker.delegate;
+//         assert!(
+//             delegate.matches.history.is_empty(),
+//             "Search matches expected"
+//         );
+//         let matches = delegate.matches.search.clone();
+//         assert_eq!(matches.len(), 1);
+
+//         let (file_name, file_name_positions, full_path, full_path_positions) =
+//             delegate.labels_for_path_match(&matches[0]);
+//         assert_eq!(file_name, "the-file");
+//         assert_eq!(file_name_positions, &[0, 1, 4]);
+//         assert_eq!(full_path, "the-file");
+//         assert_eq!(full_path_positions, &[0, 1, 4]);
+//     });
+
+//     // Since the worktree root is a file, searching for its name followed by a slash does
+//     // not match anything.
+//     picker
+//         .update(cx, |f, cx| {
+//             f.delegate.spawn_search(test_path_like("thf/"), cx)
+//         })
+//         .await;
+//     picker.update(cx, |f, _| assert_eq!(f.delegate.matches.len(), 0));
+// }
+
+// #[gpui::test]
+// async fn test_path_distance_ordering(cx: &mut TestAppContext) {
+//     let app_state = init_test(cx);
+//     app_state
+//         .fs
+//         .as_fake()
+//         .insert_tree(
+//             "/root",
+//             json!({
+//                 "dir1": { "a.txt": "" },
+//                 "dir2": {
+//                     "a.txt": "",
+//                     "b.txt": ""
+//                 }
+//             }),
+//         )
+//         .await;
+
+//     let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
+//     let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
+//     let cx = &mut cx;
+
+//     let worktree_id = cx.read(|cx| {
+//         let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
+//         assert_eq!(worktrees.len(), 1);
+//         WorktreeId::from_usize(worktrees[0].id())
+//     });
+
+//     // When workspace has an active item, sort items which are closer to that item
+//     // first when they have the same name. In this case, b.txt is closer to dir2's a.txt
+//     // so that one should be sorted earlier
+//     let b_path = Some(dummy_found_path(ProjectPath {
+//         worktree_id,
+//         path: Arc::from(Path::new("/root/dir2/b.txt")),
+//     }));
+//     cx.dispatch_action(Toggle);
+
+//     let finder = cx
+//         .add_window(|cx| {
+//             Picker::new(
+//                 FileFinderDelegate::new(
+//                     workspace.downgrade(),
+//                     workspace.read(cx).project().clone(),
+//                     b_path,
+//                     Vec::new(),
+//                     cx,
+//                 ),
+//                 cx,
+//             )
+//         })
+//         .root(cx);
+
+//     finder
+//         .update(cx, |f, cx| {
+//             f.delegate.spawn_search(test_path_like("a.txt"), cx)
+//         })
+//         .await;
+
+//     finder.read_with(cx, |f, _| {
+//         let delegate = &f.delegate;
+//         assert!(
+//             delegate.matches.history.is_empty(),
+//             "Search matches expected"
+//         );
+//         let matches = delegate.matches.search.clone();
+//         assert_eq!(matches[0].path.as_ref(), Path::new("dir2/a.txt"));
+//         assert_eq!(matches[1].path.as_ref(), Path::new("dir1/a.txt"));
+//     });
+// }
+
+// #[gpui::test]
+// async fn test_search_worktree_without_files(cx: &mut TestAppContext) {
+//     let app_state = init_test(cx);
+//     app_state
+//         .fs
+//         .as_fake()
+//         .insert_tree(
+//             "/root",
+//             json!({
+//                 "dir1": {},
+//                 "dir2": {
+//                     "dir3": {}
+//                 }
+//             }),
+//         )
+//         .await;
+
+//     let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
+//     let workspace = cx
+//         .add_window(|cx| Workspace::test_new(project, cx))
+//         .root(cx);
+//     let finder = cx
+//         .add_window(|cx| {
+//             Picker::new(
+//                 FileFinderDelegate::new(
+//                     workspace.downgrade(),
+//                     workspace.read(cx).project().clone(),
+//                     None,
+//                     Vec::new(),
+//                     cx,
+//                 ),
+//                 cx,
+//             )
+//         })
+//         .root(cx);
+//     finder
+//         .update(cx, |f, cx| {
+//             f.delegate.spawn_search(test_path_like("dir"), cx)
+//         })
+//         .await;
+//     cx.read(|cx| {
+//         let finder = finder.read(cx);
+//         assert_eq!(finder.delegate.matches.len(), 0);
+//     });
+// }
+
+//     #[gpui::test]
+//     async fn test_query_history(cx: &mut gpui::TestAppContext) {
+//         let app_state = init_test(cx);
+
+//         app_state
+//             .fs
+//             .as_fake()
+//             .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(app_state.fs.clone(), ["/src".as_ref()], cx).await;
+//         let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
+//         let cx = &mut cx;
+//         let worktree_id = cx.read(|cx| {
+//             let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
+//             assert_eq!(worktrees.len(), 1);
+//             WorktreeId::from_usize(worktrees[0].id())
+//         });
+
+//         // Open and close panels, getting their history items afterwards.
+//         // Ensure history items get populated with opened items, and items are kept in a certain order.
+//         // The history lags one opened buffer behind, since it's updated in the search panel only on its reopen.
+//         //
+//         // TODO: without closing, the opened items do not propagate their history changes for some reason
+//         // it does work in real app though, only tests do not propagate.
+
+//         let initial_history = open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
+//         assert!(
+//             initial_history.is_empty(),
+//             "Should have no history before opening any files"
+//         );
+
+//         let history_after_first =
+//             open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
+//         assert_eq!(
+//             history_after_first,
+//             vec![FoundPath::new(
+//                 ProjectPath {
+//                     worktree_id,
+//                     path: Arc::from(Path::new("test/first.rs")),
+//                 },
+//                 Some(PathBuf::from("/src/test/first.rs"))
+//             )],
+//             "Should show 1st opened item in the history when opening the 2nd item"
+//         );
+
+//         let history_after_second =
+//             open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
+//         assert_eq!(
+//             history_after_second,
+//             vec![
+//                 FoundPath::new(
+//                     ProjectPath {
+//                         worktree_id,
+//                         path: Arc::from(Path::new("test/second.rs")),
+//                     },
+//                     Some(PathBuf::from("/src/test/second.rs"))
+//                 ),
+//                 FoundPath::new(
+//                     ProjectPath {
+//                         worktree_id,
+//                         path: Arc::from(Path::new("test/first.rs")),
+//                     },
+//                     Some(PathBuf::from("/src/test/first.rs"))
+//                 ),
+//             ],
+//             "Should show 1st and 2nd opened items in the history when opening the 3rd item. \
+// 2nd item should be the first in the history, as the last opened."
+//         );
+
+//         let history_after_third =
+//             open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
+//         assert_eq!(
+//             history_after_third,
+//             vec![
+//                 FoundPath::new(
+//                     ProjectPath {
+//                         worktree_id,
+//                         path: Arc::from(Path::new("test/third.rs")),
+//                     },
+//                     Some(PathBuf::from("/src/test/third.rs"))
+//                 ),
+//                 FoundPath::new(
+//                     ProjectPath {
+//                         worktree_id,
+//                         path: Arc::from(Path::new("test/second.rs")),
+//                     },
+//                     Some(PathBuf::from("/src/test/second.rs"))
+//                 ),
+//                 FoundPath::new(
+//                     ProjectPath {
+//                         worktree_id,
+//                         path: Arc::from(Path::new("test/first.rs")),
+//                     },
+//                     Some(PathBuf::from("/src/test/first.rs"))
+//                 ),
+//             ],
+//             "Should show 1st, 2nd and 3rd opened items in the history when opening the 2nd item again. \
+// 3rd item should be the first in the history, as the last opened."
+//         );
+
+//         let history_after_second_again =
+//             open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
+//         assert_eq!(
+//             history_after_second_again,
+//             vec![
+//                 FoundPath::new(
+//                     ProjectPath {
+//                         worktree_id,
+//                         path: Arc::from(Path::new("test/second.rs")),
+//                     },
+//                     Some(PathBuf::from("/src/test/second.rs"))
+//                 ),
+//                 FoundPath::new(
+//                     ProjectPath {
+//                         worktree_id,
+//                         path: Arc::from(Path::new("test/third.rs")),
+//                     },
+//                     Some(PathBuf::from("/src/test/third.rs"))
+//                 ),
+//                 FoundPath::new(
+//                     ProjectPath {
+//                         worktree_id,
+//                         path: Arc::from(Path::new("test/first.rs")),
+//                     },
+//                     Some(PathBuf::from("/src/test/first.rs"))
+//                 ),
+//             ],
+//             "Should show 1st, 2nd and 3rd opened items in the history when opening the 3rd item again. \
+// 2nd item, as the last opened, 3rd item should go next as it was opened right before."
+//         );
+//     }
+
+// #[gpui::test]
+// async fn test_external_files_history(cx: &mut gpui::TestAppContext) {
+//     let app_state = init_test(cx);
+
+//     app_state
+//         .fs
+//         .as_fake()
+//         .insert_tree(
+//             "/src",
+//             json!({
+//                 "test": {
+//                     "first.rs": "// First Rust file",
+//                     "second.rs": "// Second Rust file",
+//                 }
+//             }),
+//         )
+//         .await;
+
+//     app_state
+//         .fs
+//         .as_fake()
+//         .insert_tree(
+//             "/external-src",
+//             json!({
+//                 "test": {
+//                     "third.rs": "// Third Rust file",
+//                     "fourth.rs": "// Fourth Rust file",
+//                 }
+//             }),
+//         )
+//         .await;
+
+//     let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
+//     cx.update(|cx| {
+//         project.update(cx, |project, cx| {
+//             project.find_or_create_local_worktree("/external-src", false, cx)
+//         })
+//     })
+//     .detach();
+//     cx.background_executor.run_until_parked();
+
+//     let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
+//     let cx = &mut cx;
+//     let worktree_id = cx.read(|cx| {
+//         let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
+//         assert_eq!(worktrees.len(), 1,);
+
+//         WorktreeId::from_usize(worktrees[0].id())
+//     });
+//     workspace
+//         .update(cx, |workspace, cx| {
+//             workspace.open_abs_path(PathBuf::from("/external-src/test/third.rs"), false, cx)
+//         })
+//         .detach();
+//     cx.background_executor.run_until_parked();
+//     let external_worktree_id = cx.read(|cx| {
+//         let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
+//         assert_eq!(
+//             worktrees.len(),
+//             2,
+//             "External file should get opened in a new worktree"
+//         );
+
+//         WorktreeId::from_usize(
+//             worktrees
+//                 .into_iter()
+//                 .find(|worktree| worktree.entity_id() != worktree_id.to_usize())
+//                 .expect("New worktree should have a different id")
+//                 .id(),
+//         )
+//     });
+//     close_active_item(&workspace, cx).await;
+
+//     let initial_history_items =
+//         open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
+//     assert_eq!(
+//         initial_history_items,
+//         vec![FoundPath::new(
+//             ProjectPath {
+//                 worktree_id: external_worktree_id,
+//                 path: Arc::from(Path::new("")),
+//             },
+//             Some(PathBuf::from("/external-src/test/third.rs"))
+//         )],
+//         "Should show external file with its full path in the history after it was open"
+//     );
+
+//     let updated_history_items =
+//         open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
+//     assert_eq!(
+//         updated_history_items,
+//         vec![
+//             FoundPath::new(
+//                 ProjectPath {
+//                     worktree_id,
+//                     path: Arc::from(Path::new("test/second.rs")),
+//                 },
+//                 Some(PathBuf::from("/src/test/second.rs"))
+//             ),
+//             FoundPath::new(
+//                 ProjectPath {
+//                     worktree_id: external_worktree_id,
+//                     path: Arc::from(Path::new("")),
+//                 },
+//                 Some(PathBuf::from("/external-src/test/third.rs"))
+//             ),
+//         ],
+//         "Should keep external file with history updates",
+//     );
+// }
+
+// #[gpui::test]
+// async fn test_toggle_panel_new_selections(cx: &mut gpui::TestAppContext) {
+//     let app_state = init_test(cx);
+
+//     app_state
+//         .fs
+//         .as_fake()
+//         .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(app_state.fs.clone(), ["/src".as_ref()], cx).await;
+//     let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
+//     let cx = &mut cx;
+
+//     // generate some history to select from
+//     open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
+//     cx.executor().run_until_parked();
+//     open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
+//     open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
+//     let current_history = open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
+
+//     for expected_selected_index in 0..current_history.len() {
+//         cx.dispatch_action(Toggle);
+//         let selected_index = workspace.update(cx, |workspace, cx| {
+//             workspace
+//                 .current_modal::<FileFinder>(cx)
+//                 .unwrap()
+//                 .read(cx)
+//                 .picker
+//                 .read(cx)
+//                 .delegate
+//                 .selected_index()
+//         });
+//         assert_eq!(
+//             selected_index, expected_selected_index,
+//             "Should select the next item in the history"
+//         );
+//     }
+
+//     cx.dispatch_action(Toggle);
+//     let selected_index = workspace.update(cx, |workspace, cx| {
+//         workspace
+//             .current_modal::<FileFinder>(cx)
+//             .unwrap()
+//             .read(cx)
+//             .picker
+//             .read(cx)
+//             .delegate
+//             .selected_index()
+//     });
+//     assert_eq!(
+//         selected_index, 0,
+//         "Should wrap around the history and start all over"
+//     );
+// }
+
+// #[gpui::test]
+// async fn test_search_preserves_history_items(cx: &mut gpui::TestAppContext) {
+//     let app_state = init_test(cx);
+
+//     app_state
+//         .fs
+//         .as_fake()
+//         .insert_tree(
+//             "/src",
+//             json!({
+//                 "test": {
+//                     "first.rs": "// First Rust file",
+//                     "second.rs": "// Second Rust file",
+//                     "third.rs": "// Third Rust file",
+//                     "fourth.rs": "// Fourth Rust file",
+//                 }
+//             }),
+//         )
+//         .await;
+
+//     let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
+//     let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
+//     let cx = &mut cx;
+//     let worktree_id = cx.read(|cx| {
+//         let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
+//         assert_eq!(worktrees.len(), 1,);
+
+//         WorktreeId::from_usize(worktrees[0].entity_id())
+//     });
+
+//     // generate some history to select from
+//     open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
+//     open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
+//     open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
+//     open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
+
+//     cx.dispatch_action(Toggle);
+//     let first_query = "f";
+//     let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
+//     finder
+//         .update(cx, |finder, cx| {
+//             finder.delegate.update_matches(first_query.to_string(), cx)
+//         })
+//         .await;
+//     finder.read_with(cx, |finder, _| {
+//         let delegate = &finder.delegate;
+//         assert_eq!(delegate.matches.history.len(), 1, "Only one history item contains {first_query}, it should be present and others should be filtered out");
+//         let history_match = delegate.matches.history.first().unwrap();
+//         assert!(history_match.1.is_some(), "Should have path matches for history items after querying");
+//         assert_eq!(history_match.0, FoundPath::new(
+//             ProjectPath {
+//                 worktree_id,
+//                 path: Arc::from(Path::new("test/first.rs")),
+//             },
+//             Some(PathBuf::from("/src/test/first.rs"))
+//         ));
+//         assert_eq!(delegate.matches.search.len(), 1, "Only one non-history item contains {first_query}, it should be present");
+//         assert_eq!(delegate.matches.search.first().unwrap().path.as_ref(), Path::new("test/fourth.rs"));
+//     });
+
+//     let second_query = "fsdasdsa";
+//     let finder = workspace.update(cx, |workspace, cx| {
+//         workspace
+//             .current_modal::<FileFinder>(cx)
+//             .unwrap()
+//             .read(cx)
+//             .picker
+//     });
+//     finder
+//         .update(cx, |finder, cx| {
+//             finder.delegate.update_matches(second_query.to_string(), cx)
+//         })
+//         .await;
+//     finder.update(cx, |finder, _| {
+//         let delegate = &finder.delegate;
+//         assert!(
+//             delegate.matches.history.is_empty(),
+//             "No history entries should match {second_query}"
+//         );
+//         assert!(
+//             delegate.matches.search.is_empty(),
+//             "No search entries should match {second_query}"
+//         );
+//     });
+
+//     let first_query_again = first_query;
+
+//     let finder = workspace.update(cx, |workspace, cx| {
+//         workspace
+//             .current_modal::<FileFinder>(cx)
+//             .unwrap()
+//             .read(cx)
+//             .picker
+//     });
+//     finder
+//         .update(cx, |finder, cx| {
+//             finder
+//                 .delegate
+//                 .update_matches(first_query_again.to_string(), cx)
+//         })
+//         .await;
+//     finder.read_with(cx, |finder, _| {
+//         let delegate = &finder.delegate;
+//         assert_eq!(delegate.matches.history.len(), 1, "Only one history item contains {first_query_again}, it should be present and others should be filtered out, even after non-matching query");
+//         let history_match = delegate.matches.history.first().unwrap();
+//         assert!(history_match.1.is_some(), "Should have path matches for history items after querying");
+//         assert_eq!(history_match.0, FoundPath::new(
+//             ProjectPath {
+//                 worktree_id,
+//                 path: Arc::from(Path::new("test/first.rs")),
+//             },
+//             Some(PathBuf::from("/src/test/first.rs"))
+//         ));
+//         assert_eq!(delegate.matches.search.len(), 1, "Only one non-history item contains {first_query_again}, it should be present, even after non-matching query");
+//         assert_eq!(delegate.matches.search.first().unwrap().path.as_ref(), Path::new("test/fourth.rs"));
+//     });
+// }
+
+// #[gpui::test]
+// async fn test_history_items_vs_very_good_external_match(cx: &mut gpui::TestAppContext) {
+//     let app_state = init_test(cx);
+
+//     app_state
+//         .fs
+//         .as_fake()
+//         .insert_tree(
+//             "/src",
+//             json!({
+//                 "collab_ui": {
+//                     "first.rs": "// First Rust file",
+//                     "second.rs": "// Second Rust file",
+//                     "third.rs": "// Third Rust file",
+//                     "collab_ui.rs": "// Fourth Rust file",
+//                 }
+//             }),
+//         )
+//         .await;
+
+//     let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
+//     let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
+//     let cx = &mut cx;
+//     // generate some history to select from
+//     open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
+//     open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
+//     open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
+//     open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
+
+//     cx.dispatch_action(Toggle);
+//     let query = "collab_ui";
+//     let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
+//     finder
+//         .update(cx, |finder, cx| {
+//             finder.delegate.update_matches(query.to_string(), cx)
+//         })
+//         .await;
+//     finder.read_with(cx, |finder, _| {
+//         let delegate = &finder.delegate;
+//         assert!(
+//             delegate.matches.history.is_empty(),
+//             "History items should not math query {query}, they should be matched by name only"
+//         );
+
+//         let search_entries = delegate
+//             .matches
+//             .search
+//             .iter()
+//             .map(|path_match| path_match.path.to_path_buf())
+//             .collect::<Vec<_>>();
+//         assert_eq!(
+//             search_entries,
+//             vec![
+//                 PathBuf::from("collab_ui/collab_ui.rs"),
+//                 PathBuf::from("collab_ui/third.rs"),
+//                 PathBuf::from("collab_ui/first.rs"),
+//                 PathBuf::from("collab_ui/second.rs"),
+//             ],
+//             "Despite all search results having the same directory name, the most matching one should be on top"
+//         );
+//     });
+// }
+
+// #[gpui::test]
+// async fn test_nonexistent_history_items_not_shown(cx: &mut gpui::TestAppContext) {
+//     let app_state = init_test(cx);
+
+//     app_state
+//         .fs
+//         .as_fake()
+//         .insert_tree(
+//             "/src",
+//             json!({
+//                 "test": {
+//                     "first.rs": "// First Rust file",
+//                     "nonexistent.rs": "// Second Rust file",
+//                     "third.rs": "// Third Rust file",
+//                 }
+//             }),
+//         )
+//         .await;
+
+//     let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
+//     let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
+//     let cx = &mut cx;
+//     // generate some history to select from
+//     open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
+//     open_close_queried_buffer("non", 1, "nonexistent.rs", &workspace, cx).await;
+//     open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
+//     open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
+
+//     cx.dispatch_action(Toggle);
+//     let query = "rs";
+//     let finder = cx.read(|cx| workspace.read(cx).current_modal::<FileFinder>().unwrap());
+//     finder
+//         .update(cx, |finder, cx| {
+//             finder.picker.update(cx, |picker, cx| {
+//                 picker.delegate.update_matches(query.to_string(), cx)
+//             })
+//         })
+//         .await;
+//     finder.update(cx, |finder, _| {
+//         let history_entries = finder.delegate
+//             .matches
+//             .history
+//             .iter()
+//             .map(|(_, path_match)| path_match.as_ref().expect("should have a path match").path.to_path_buf())
+//             .collect::<Vec<_>>();
+//         assert_eq!(
+//             history_entries,
+//             vec![
+//                 PathBuf::from("test/first.rs"),
+//                 PathBuf::from("test/third.rs"),
+//             ],
+//             "Should have all opened files in the history, except the ones that do not exist on disk"
+//         );
+//     });
+// }
+
+//     async fn open_close_queried_buffer(
+//         input: &str,
+//         expected_matches: usize,
+//         expected_editor_title: &str,
+//         workspace: &View<Workspace>,
+//         cx: &mut gpui::VisualTestContext<'_>,
+//     ) -> Vec<FoundPath> {
+//         cx.dispatch_action(Toggle);
+//         let picker = workspace.update(cx, |workspace, cx| {
+//             workspace
+//                 .current_modal::<FileFinder>(cx)
+//                 .unwrap()
+//                 .read(cx)
+//                 .picker
+//                 .clone()
+//         });
+//         picker
+//             .update(cx, |finder, cx| {
+//                 finder.delegate.update_matches(input.to_string(), cx)
+//             })
+//             .await;
+//         let history_items = picker.update(cx, |finder, _| {
+//             assert_eq!(
+//                 finder.delegate.matches.len(),
+//                 expected_matches,
+//                 "Unexpected number of matches found for query {input}"
+//             );
+//             finder.delegate.history_items.clone()
+//         });
+
+//         let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
+//         cx.dispatch_action(SelectNext);
+//         cx.dispatch_action(Confirm);
+//         cx.background_executor.run_until_parked();
+//         active_pane
+//             .condition(cx, |pane, _| pane.active_item().is_some())
+//             .await;
+//         cx.read(|cx| {
+//             let active_item = active_pane.read(cx).active_item().unwrap();
+//             let active_editor_title = active_item
+//                 .to_any()
+//                 .downcast::<Editor>()
+//                 .unwrap()
+//                 .read(cx)
+//                 .title(cx);
+//             assert_eq!(
+//                 expected_editor_title, active_editor_title,
+//                 "Unexpected editor title for query {input}"
+//             );
+//         });
+
+//         close_active_item(workspace, cx).await;
+
+//         history_items
+//     }
+
+//     async fn close_active_item(workspace: &View<Workspace>, cx: &mut VisualTestContext<'_>) {
+//         let mut original_items = HashMap::new();
+//         cx.read(|cx| {
+//             for pane in workspace.read(cx).panes() {
+//                 let pane_id = pane.entity_id();
+//                 let pane = pane.read(cx);
+//                 let insertion_result = original_items.insert(pane_id, pane.items().count());
+//                 assert!(insertion_result.is_none(), "Pane id {pane_id} collision");
+//             }
+//         });
+
+//         let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
+//         active_pane
+//             .update(cx, |pane, cx| {
+//                 pane.close_active_item(&workspace::CloseActiveItem { save_intent: None }, cx)
+//                     .unwrap()
+//             })
+//             .await
+//             .unwrap();
+//         cx.background_executor.run_until_parked();
+//         cx.read(|cx| {
+//             for pane in workspace.read(cx).panes() {
+//                 let pane_id = pane.entity_id();
+//                 let pane = pane.read(cx);
+//                 match original_items.remove(&pane_id) {
+//                     Some(original_items) => {
+//                         assert_eq!(
+//                             pane.items().count(),
+//                             original_items.saturating_sub(1),
+//                             "Pane id {pane_id} should have item closed"
+//                         );
+//                     }
+//                     None => panic!("Pane id {pane_id} not found in original items"),
+//                 }
+//             }
+//         });
+//         assert!(
+//             original_items.len() <= 1,
+//             "At most one panel should got closed"
+//         );
+//     }
+
+//     fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
+//         cx.update(|cx| {
+//             let state = AppState::test(cx);
+//             theme::init(cx);
+//             language::init(cx);
+//             super::init(cx);
+//             editor::init(cx);
+//             workspace::init_settings(cx);
+//             Project::init_settings(cx);
+//             state
+//         })
+//     }
+
+//     fn test_path_like(test_str: &str) -> PathLikeWithPosition<FileSearchQuery> {
+//         PathLikeWithPosition::parse_str(test_str, |path_like_str| {
+//             Ok::<_, std::convert::Infallible>(FileSearchQuery {
+//                 raw_query: test_str.to_owned(),
+//                 file_query_end: if path_like_str == test_str {
+//                     None
+//                 } else {
+//                     Some(path_like_str.len())
+//                 },
+//             })
+//         })
+//         .unwrap()
+//     }
+
+//     fn dummy_found_path(project_path: ProjectPath) -> FoundPath {
+//         FoundPath {
+//             project: project_path,
+//             absolute: None,
+//         }
+//     }
+
+//     fn build_find_picker(
+//         project: Model<Project>,
+//         cx: &mut TestAppContext,
+//     ) -> (
+//         View<Picker<FileFinderDelegate>>,
+//         View<Workspace>,
+//         VisualTestContext,
+//     ) {
+//         let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
+//         cx.dispatch_action(Toggle);
+//         let picker = workspace.update(&mut cx, |workspace, cx| {
+//             workspace
+//                 .current_modal::<FileFinder>(cx)
+//                 .unwrap()
+//                 .read(cx)
+//                 .picker
+//                 .clone()
+//         });
+//         (picker, workspace, cx)
+//     }
+// }

crates/go_to_line2/src/go_to_line.rs 🔗

@@ -1,11 +1,11 @@
 use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Editor};
 use gpui::{
-    actions, div, AppContext, Div, EventEmitter, ParentElement, Render, SharedString,
-    StatelessInteractive, Styled, Subscription, View, ViewContext, VisualContext, WindowContext,
+    actions, div, prelude::*, AppContext, Div, EventEmitter, ParentComponent, Render, SharedString,
+    Styled, Subscription, View, ViewContext, VisualContext, WindowContext,
 };
 use text::{Bias, Point};
 use theme::ActiveTheme;
-use ui::{h_stack, v_stack, Label, LabelColor, StyledExt};
+use ui::{h_stack, v_stack, Label, StyledExt, TextColor};
 use util::paths::FILE_ROW_COLUMN_DELIMITER;
 use workspace::{Modal, ModalEvent, Workspace};
 
@@ -150,7 +150,7 @@ impl Render for GoToLine {
     fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
         div()
             .elevation_2(cx)
-            .context("GoToLine")
+            .key_context("GoToLine")
             .on_action(Self::cancel)
             .on_action(Self::confirm)
             .w_96()
@@ -176,7 +176,7 @@ impl Render for GoToLine {
                             .justify_between()
                             .px_2()
                             .py_1()
-                            .child(Label::new(self.current_text.clone()).color(LabelColor::Muted)),
+                            .child(Label::new(self.current_text.clone()).color(TextColor::Muted)),
                     ),
             )
     }

crates/gpui/src/app.rs 🔗

@@ -2112,6 +2112,10 @@ impl AppContext {
         AsyncAppContext(self.weak_self.as_ref().unwrap().upgrade().unwrap())
     }
 
+    pub fn open_url(&self, url: &str) {
+        self.platform.open_url(url)
+    }
+
     pub fn write_to_clipboard(&self, item: ClipboardItem) {
         self.platform.write_to_clipboard(item);
     }

crates/gpui2/docs/contexts.md 🔗

@@ -0,0 +1,41 @@
+# Contexts
+
+GPUI makes extensive use of *context parameters*, typically named `cx` and positioned at the end of the parameter list, unless they're before a final function parameter. A context reference provides access to application state and services.
+
+There are multiple kinds of contexts, and contexts implement the `Deref` trait so that a function taking `&mut AppContext` could be passed a `&mut WindowContext` or `&mut ViewContext` instead.
+
+```
+     AppContext
+     /        \
+ModelContext  WindowContext
+              /
+        ViewContext
+```
+
+- The `AppContext` forms the root of the hierarchy
+- `ModelContext` and `WindowContext` both dereference to `AppContext`
+- `ViewContext` dereferences to `WindowContext`
+
+## `AppContext`
+
+Provides access to the global application state. All other kinds of contexts ultimately deref to an `AppContext`. You can update a `Model<T>` by passing an `AppContext`, but you can't update a view. For that you need a `WindowContext`...
+
+## `WindowContext`
+
+Provides access to the state of an application window, and also derefs to an `AppContext`, so you can pass a window context reference to any method taking an app context. Obtain this context by calling `WindowHandle::update`.
+
+## `ModelContext<T>`
+
+Available when you create or update a `Model<T>`. It derefs to an `AppContext`, but also contains methods specific to the particular model, such as the ability to notify change observers or emit events.
+
+## `ViewContext<V>`
+
+Available when you create or update a `View<V>`. It derefs to a `WindowContext`, but also contains methods specific to the particular view, such as the ability to notify change observers or emit events.
+
+## `AsyncAppContext` and `AsyncWindowContext`
+
+Whereas the above contexts are always passed to your code as references, you can call `to_async` on the reference to create an async context, which has a static lifetime and can be held across `await` points in async code. When you interact with `Model`s or `View`s with an async context, the calls become fallible, because the context may outlive the window or even the app itself.
+
+## `TestAppContext` and `TestVisualContext`
+
+These are similar to the async contexts above, but they panic if you attempt to access a non-existent app or window, and they also contain other features specific to tests.

crates/gpui2/docs/key_dispatch.md 🔗

@@ -0,0 +1,101 @@
+# Key Dispatch
+
+GPUI is designed for keyboard-first interactivity.
+
+To expose functionality to the mouse, you render a button with a click handler.
+
+To expose functionality to the keyboard, you bind an *action* in a *key context*.
+
+Actions are similar to framework-level events like `MouseDown`, `KeyDown`, etc, but you can define them yourself:
+
+```rust
+mod menu {
+    #[gpui::action]
+    struct MoveUp;
+
+    #[gpui::action]
+    struct MoveDown;
+}
+```
+
+Actions are frequently unit structs, for which we have a macro. The above could also be written:
+
+```rust
+mod menu {
+    actions!(MoveUp, MoveDown);
+}
+```
+
+Actions can also be more complex types:
+
+```rust
+mod menu {
+    #[gpui::action]
+    struct Move {
+        direction: Direction,
+        select: bool,
+    }
+}
+```
+
+To bind actions, chain `on_action` on to your element:
+
+```rust
+impl Render for Menu {
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl Component {
+        div()
+            .on_action(|this: &mut Menu, move: &MoveUp, cx: &mut ViewContext<Menu>| {
+                // ...
+            })
+            .on_action(|this, move: &MoveDown, cx| {
+                // ...
+            })
+            .children(todo!())
+    }
+}
+```
+
+In order to bind keys to actions, you need to declare a *key context* for part of the element tree by calling `key_context`.
+
+```rust
+impl Render for Menu {
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl Component {
+        div()
+            .key_context("menu")
+            .on_action(|this: &mut Menu, move: &MoveUp, cx: &mut ViewContext<Menu>| {
+                // ...
+            })
+            .on_action(|this, move: &MoveDown, cx| {
+                // ...
+            })
+            .children(todo!())
+    }
+}
+```
+
+Now you can target your context in the keymap. Note how actions are identified in the keymap by their fully-qualified type name.
+
+```json
+{
+  "context": "menu",
+  "bindings": {
+    "up": "menu::MoveUp",
+    "down": "menu::MoveDown"
+  }
+}
+```
+
+If you had opted for the more complex type definition, you'd provide the serialized representation of the action alongside the name:
+
+```json
+{
+  "context": "menu",
+  "bindings": {
+    "up": ["menu::Move", {direction: "up", select: false}]
+    "down": ["menu::Move", {direction: "down", select: false}]
+    "shift-up": ["menu::Move", {direction: "up", select: true}]
+    "shift-down": ["menu::Move", {direction: "down", select: true}]
+  }
+}
+
+```

crates/gpui2/src/app.rs 🔗

@@ -234,10 +234,10 @@ impl AppContext {
             app_version: platform.app_version().ok(),
         };
 
-        Rc::new_cyclic(|this| AppCell {
+        let app = Rc::new_cyclic(|this| AppCell {
             app: RefCell::new(AppContext {
                 this: this.clone(),
-                platform,
+                platform: platform.clone(),
                 app_metadata,
                 text_system,
                 flushing_effects: false,
@@ -269,12 +269,21 @@ impl AppContext {
                 layout_id_buffer: Default::default(),
                 propagate_event: true,
             }),
-        })
+        });
+
+        platform.on_quit(Box::new({
+            let cx = app.clone();
+            move || {
+                cx.borrow_mut().shutdown();
+            }
+        }));
+
+        app
     }
 
     /// Quit the application gracefully. Handlers registered with `ModelContext::on_app_quit`
     /// will be given 100ms to complete before exiting.
-    pub fn quit(&mut self) {
+    pub fn shutdown(&mut self) {
         let mut futures = Vec::new();
 
         for observer in self.quit_observers.remove(&()) {
@@ -292,8 +301,10 @@ impl AppContext {
         {
             log::error!("timed out waiting on app_will_quit");
         }
+    }
 
-        self.globals_by_type.clear();
+    pub fn quit(&mut self) {
+        self.platform.quit();
     }
 
     pub fn app_metadata(&self) -> AppMetadata {
@@ -431,6 +442,18 @@ impl AppContext {
         self.platform.activate(ignoring_other_apps);
     }
 
+    pub fn hide(&self) {
+        self.platform.hide();
+    }
+
+    pub fn hide_other_apps(&self) {
+        self.platform.hide_other_apps();
+    }
+
+    pub fn unhide_other_apps(&self) {
+        self.platform.unhide_other_apps();
+    }
+
     /// Returns the list of currently active displays.
     pub fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>> {
         self.platform.displays()
@@ -1091,7 +1114,7 @@ impl<G: 'static> DerefMut for GlobalLease<G> {
 
 /// Contains state associated with an active drag operation, started by dragging an element
 /// within the window or by dragging into the app from the underlying platform.
-pub(crate) struct AnyDrag {
+pub struct AnyDrag {
     pub view: AnyView,
     pub cursor_offset: Point<Pixels>,
 }

crates/gpui2/src/app/entity_map.rs 🔗

@@ -26,7 +26,7 @@ impl EntityId {
 
 impl Display for EntityId {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        write!(f, "{}", self)
+        write!(f, "{}", self.as_u64())
     }
 }
 

crates/gpui2/src/app/test_context.rs 🔗

@@ -1,8 +1,8 @@
 use crate::{
-    div, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext, BackgroundExecutor,
-    Context, Div, EventEmitter, ForegroundExecutor, InputEvent, KeyDownEvent, Keystroke, Model,
-    ModelContext, Render, Result, Task, TestDispatcher, TestPlatform, View, ViewContext,
-    VisualContext, WindowContext, WindowHandle, WindowOptions,
+    div, Action, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext,
+    BackgroundExecutor, Context, Div, EventEmitter, ForegroundExecutor, InputEvent, KeyDownEvent,
+    Keystroke, Model, ModelContext, Render, Result, Task, TestDispatcher, TestPlatform, View,
+    ViewContext, VisualContext, WindowContext, WindowHandle, WindowOptions,
 };
 use anyhow::{anyhow, bail};
 use futures::{Stream, StreamExt};
@@ -14,6 +14,7 @@ pub struct TestAppContext {
     pub background_executor: BackgroundExecutor,
     pub foreground_executor: ForegroundExecutor,
     pub dispatcher: TestDispatcher,
+    pub test_platform: Rc<TestPlatform>,
 }
 
 impl Context for TestAppContext {
@@ -77,17 +78,16 @@ impl TestAppContext {
         let arc_dispatcher = Arc::new(dispatcher.clone());
         let background_executor = BackgroundExecutor::new(arc_dispatcher.clone());
         let foreground_executor = ForegroundExecutor::new(arc_dispatcher);
-        let platform = Rc::new(TestPlatform::new(
-            background_executor.clone(),
-            foreground_executor.clone(),
-        ));
+        let platform = TestPlatform::new(background_executor.clone(), foreground_executor.clone());
         let asset_source = Arc::new(());
         let http_client = util::http::FakeHttpClient::with_404_response();
+
         Self {
-            app: AppContext::new(platform, asset_source, http_client),
+            app: AppContext::new(platform.clone(), asset_source, http_client),
             background_executor,
             foreground_executor,
             dispatcher: dispatcher.clone(),
+            test_platform: platform,
         }
     }
 
@@ -96,7 +96,7 @@ impl TestAppContext {
     }
 
     pub fn quit(&self) {
-        self.app.borrow_mut().quit();
+        self.app.borrow_mut().shutdown();
     }
 
     pub fn refresh(&mut self) -> Result<()> {
@@ -152,6 +152,21 @@ impl TestAppContext {
         (view, VisualTestContext::from_window(*window.deref(), self))
     }
 
+    pub fn simulate_new_path_selection(
+        &self,
+        select_path: impl FnOnce(&std::path::Path) -> Option<std::path::PathBuf>,
+    ) {
+        self.test_platform.simulate_new_path_selection(select_path);
+    }
+
+    pub fn simulate_prompt_answer(&self, button_ix: usize) {
+        self.test_platform.simulate_prompt_answer(button_ix);
+    }
+
+    pub fn has_pending_prompt(&self) -> bool {
+        self.test_platform.has_pending_prompt()
+    }
+
     pub fn spawn<Fut, R>(&self, f: impl FnOnce(AsyncAppContext) -> Fut) -> Task<R>
     where
         Fut: Future<Output = R> + 'static,
@@ -199,6 +214,15 @@ impl TestAppContext {
         }
     }
 
+    pub fn dispatch_action<A>(&mut self, window: AnyWindowHandle, action: A)
+    where
+        A: Action,
+    {
+        window
+            .update(self, |_, cx| cx.dispatch_action(action.boxed_clone()))
+            .unwrap()
+    }
+
     pub fn dispatch_keystroke(
         &mut self,
         window: AnyWindowHandle,
@@ -376,6 +400,13 @@ impl<'a> VisualTestContext<'a> {
     pub fn from_window(window: AnyWindowHandle, cx: &'a mut TestAppContext) -> Self {
         Self { cx, window }
     }
+
+    pub fn dispatch_action<A>(&mut self, action: A)
+    where
+        A: Action,
+    {
+        self.cx.dispatch_action(self.window, action)
+    }
 }
 
 impl<'a> Context for VisualTestContext<'a> {

crates/gpui2/src/color.rs 🔗

@@ -293,7 +293,16 @@ pub fn blue() -> Hsla {
 
 pub fn green() -> Hsla {
     Hsla {
-        h: 0.3,
+        h: 0.33,
+        s: 1.,
+        l: 0.5,
+        a: 1.,
+    }
+}
+
+pub fn yellow() -> Hsla {
+    Hsla {
+        h: 0.16,
         s: 1.,
         l: 0.5,
         a: 1.,

crates/gpui2/src/element.rs 🔗

@@ -8,7 +8,7 @@ use std::{any::Any, mem};
 pub trait Element<V: 'static> {
     type ElementState: 'static;
 
-    fn id(&self) -> Option<ElementId>;
+    fn element_id(&self) -> Option<ElementId>;
 
     /// Called to initialize this element for the current frame. If this
     /// element had state in a previous frame, it will be passed in for the 3rd argument.
@@ -38,7 +38,7 @@ pub trait Element<V: 'static> {
 #[derive(Deref, DerefMut, Default, Clone, Debug, Eq, PartialEq, Hash)]
 pub struct GlobalElementId(SmallVec<[ElementId; 32]>);
 
-pub trait ParentElement<V: 'static> {
+pub trait ParentComponent<V: 'static> {
     fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]>;
 
     fn child(mut self, child: impl Component<V>) -> Self
@@ -120,7 +120,7 @@ where
     E::ElementState: 'static,
 {
     fn initialize(&mut self, view_state: &mut V, cx: &mut ViewContext<V>) {
-        let frame_state = if let Some(id) = self.element.id() {
+        let frame_state = if let Some(id) = self.element.element_id() {
             cx.with_element_state(id, |element_state, cx| {
                 let element_state = self.element.initialize(view_state, element_state, cx);
                 ((), element_state)
@@ -142,7 +142,7 @@ where
                 frame_state: initial_frame_state,
             } => {
                 frame_state = initial_frame_state;
-                if let Some(id) = self.element.id() {
+                if let Some(id) = self.element.element_id() {
                     layout_id = cx.with_element_state(id, |element_state, cx| {
                         let mut element_state = element_state.unwrap();
                         let layout_id = self.element.layout(state, &mut element_state, cx);
@@ -181,7 +181,7 @@ where
                 ..
             } => {
                 let bounds = cx.layout_bounds(layout_id);
-                if let Some(id) = self.element.id() {
+                if let Some(id) = self.element.element_id() {
                     cx.with_element_state(id, |element_state, cx| {
                         let mut element_state = element_state.unwrap();
                         self.element
@@ -255,7 +255,7 @@ where
         // Ignore the element offset when drawing this element, as the origin is already specified
         // in absolute terms.
         origin -= cx.element_offset();
-        cx.with_element_offset(Some(origin), |cx| self.paint(view_state, cx))
+        cx.with_element_offset(origin, |cx| self.paint(view_state, cx))
     }
 }
 
@@ -351,7 +351,7 @@ where
 {
     type ElementState = AnyElement<V>;
 
-    fn id(&self) -> Option<ElementId> {
+    fn element_id(&self) -> Option<ElementId> {
         None
     }
 

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

@@ -1,225 +1,606 @@
-use std::fmt::Debug;
-
 use crate::{
-    point, AnyElement, BorrowWindow, Bounds, Component, Element, ElementId, ElementInteractivity,
-    FocusHandle, FocusListeners, Focusable, FocusableKeyDispatch, GlobalElementId, GroupBounds,
-    InteractiveElementState, KeyContext, KeyDispatch, LayoutId, NonFocusableKeyDispatch, Overflow,
-    ParentElement, Pixels, Point, SharedString, StatefulInteractive, StatefulInteractivity,
-    StatelessInteractive, StatelessInteractivity, Style, StyleRefinement, Styled, ViewContext,
-    Visibility,
+    point, px, Action, AnyDrag, AnyElement, AnyTooltip, AnyView, AppContext, BorrowAppContext,
+    BorrowWindow, Bounds, ClickEvent, Component, DispatchPhase, Element, ElementId, FocusEvent,
+    FocusHandle, KeyContext, KeyDownEvent, KeyUpEvent, LayoutId, MouseButton, MouseDownEvent,
+    MouseMoveEvent, MouseUpEvent, ParentComponent, Pixels, Point, Render, ScrollWheelEvent,
+    SharedString, Size, Style, StyleRefinement, Styled, Task, View, ViewContext, Visibility,
 };
+use collections::HashMap;
+use parking_lot::Mutex;
 use refineable::Refineable;
 use smallvec::SmallVec;
+use std::{
+    any::{Any, TypeId},
+    fmt::Debug,
+    marker::PhantomData,
+    mem,
+    sync::Arc,
+    time::Duration,
+};
+use taffy::style::Overflow;
 use util::ResultExt;
 
-pub struct Div<
-    V: 'static,
-    I: ElementInteractivity<V> = StatelessInteractivity<V>,
-    K: KeyDispatch<V> = NonFocusableKeyDispatch,
-> {
-    interactivity: I,
-    key_dispatch: K,
-    children: SmallVec<[AnyElement<V>; 2]>,
-    group: Option<SharedString>,
-    base_style: StyleRefinement,
+const DRAG_THRESHOLD: f64 = 2.;
+const TOOLTIP_DELAY: Duration = Duration::from_millis(500);
+const TOOLTIP_OFFSET: Point<Pixels> = Point::new(px(10.0), px(8.0));
+
+pub struct GroupStyle {
+    pub group: SharedString,
+    pub style: StyleRefinement,
 }
 
-pub fn div<V: 'static>() -> Div<V, StatelessInteractivity<V>, NonFocusableKeyDispatch> {
-    Div {
-        interactivity: StatelessInteractivity::default(),
-        key_dispatch: NonFocusableKeyDispatch::default(),
-        children: SmallVec::new(),
-        group: None,
-        base_style: StyleRefinement::default(),
+pub trait InteractiveComponent<V: 'static>: Sized + Element<V> {
+    fn interactivity(&mut self) -> &mut Interactivity<V>;
+
+    fn group(mut self, group: impl Into<SharedString>) -> Self {
+        self.interactivity().group = Some(group.into());
+        self
     }
-}
 
-impl<V, F> Div<V, StatelessInteractivity<V>, F>
-where
-    V: 'static,
-    F: KeyDispatch<V>,
-{
-    pub fn id(self, id: impl Into<ElementId>) -> Div<V, StatefulInteractivity<V>, F> {
-        Div {
-            interactivity: StatefulInteractivity::new(id.into(), self.interactivity),
-            key_dispatch: self.key_dispatch,
-            children: self.children,
-            group: self.group,
-            base_style: self.base_style,
+    fn id(mut self, id: impl Into<ElementId>) -> Stateful<V, Self> {
+        self.interactivity().element_id = Some(id.into());
+
+        Stateful {
+            element: self,
+            view_type: PhantomData,
+        }
+    }
+
+    fn track_focus(mut self, focus_handle: &FocusHandle) -> Focusable<V, Self> {
+        self.interactivity().focusable = true;
+        self.interactivity().tracked_focus_handle = Some(focus_handle.clone());
+        Focusable {
+            element: self,
+            view_type: PhantomData,
         }
     }
+
+    fn key_context<C, E>(mut self, key_context: C) -> Self
+    where
+        C: TryInto<KeyContext, Error = E>,
+        E: Debug,
+    {
+        if let Some(key_context) = key_context.try_into().log_err() {
+            self.interactivity().key_context = key_context;
+        }
+        self
+    }
+
+    fn hover(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self {
+        self.interactivity().hover_style = f(StyleRefinement::default());
+        self
+    }
+
+    fn group_hover(
+        mut self,
+        group_name: impl Into<SharedString>,
+        f: impl FnOnce(StyleRefinement) -> StyleRefinement,
+    ) -> Self {
+        self.interactivity().group_hover_style = Some(GroupStyle {
+            group: group_name.into(),
+            style: f(StyleRefinement::default()),
+        });
+        self
+    }
+
+    fn on_mouse_down(
+        mut self,
+        button: MouseButton,
+        handler: impl Fn(&mut V, &MouseDownEvent, &mut ViewContext<V>) + 'static,
+    ) -> Self {
+        self.interactivity().mouse_down_listeners.push(Box::new(
+            move |view, event, bounds, phase, cx| {
+                if phase == DispatchPhase::Bubble
+                    && event.button == button
+                    && bounds.contains_point(&event.position)
+                {
+                    handler(view, event, cx)
+                }
+            },
+        ));
+        self
+    }
+
+    fn on_any_mouse_down(
+        mut self,
+        handler: impl Fn(&mut V, &MouseDownEvent, &mut ViewContext<V>) + 'static,
+    ) -> Self {
+        self.interactivity().mouse_down_listeners.push(Box::new(
+            move |view, event, bounds, phase, cx| {
+                if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
+                    handler(view, event, cx)
+                }
+            },
+        ));
+        self
+    }
+
+    fn on_mouse_up(
+        mut self,
+        button: MouseButton,
+        handler: impl Fn(&mut V, &MouseUpEvent, &mut ViewContext<V>) + 'static,
+    ) -> Self {
+        self.interactivity().mouse_up_listeners.push(Box::new(
+            move |view, event, bounds, phase, cx| {
+                if phase == DispatchPhase::Bubble
+                    && event.button == button
+                    && bounds.contains_point(&event.position)
+                {
+                    handler(view, event, cx)
+                }
+            },
+        ));
+        self
+    }
+
+    fn on_any_mouse_up(
+        mut self,
+        handler: impl Fn(&mut V, &MouseUpEvent, &mut ViewContext<V>) + 'static,
+    ) -> Self {
+        self.interactivity().mouse_up_listeners.push(Box::new(
+            move |view, event, bounds, phase, cx| {
+                if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
+                    handler(view, event, cx)
+                }
+            },
+        ));
+        self
+    }
+
+    fn on_mouse_down_out(
+        mut self,
+        handler: impl Fn(&mut V, &MouseDownEvent, &mut ViewContext<V>) + 'static,
+    ) -> Self {
+        self.interactivity().mouse_down_listeners.push(Box::new(
+            move |view, event, bounds, phase, cx| {
+                if phase == DispatchPhase::Capture && !bounds.contains_point(&event.position) {
+                    handler(view, event, cx)
+                }
+            },
+        ));
+        self
+    }
+
+    fn on_mouse_up_out(
+        mut self,
+        button: MouseButton,
+        handler: impl Fn(&mut V, &MouseUpEvent, &mut ViewContext<V>) + 'static,
+    ) -> Self {
+        self.interactivity().mouse_up_listeners.push(Box::new(
+            move |view, event, bounds, phase, cx| {
+                if phase == DispatchPhase::Capture
+                    && event.button == button
+                    && !bounds.contains_point(&event.position)
+                {
+                    handler(view, event, cx);
+                }
+            },
+        ));
+        self
+    }
+
+    fn on_mouse_move(
+        mut self,
+        handler: impl Fn(&mut V, &MouseMoveEvent, &mut ViewContext<V>) + 'static,
+    ) -> Self {
+        self.interactivity().mouse_move_listeners.push(Box::new(
+            move |view, event, bounds, phase, cx| {
+                if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
+                    handler(view, event, cx);
+                }
+            },
+        ));
+        self
+    }
+
+    fn on_scroll_wheel(
+        mut self,
+        handler: impl Fn(&mut V, &ScrollWheelEvent, &mut ViewContext<V>) + 'static,
+    ) -> Self {
+        self.interactivity().scroll_wheel_listeners.push(Box::new(
+            move |view, event, bounds, phase, cx| {
+                if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
+                    handler(view, event, cx);
+                }
+            },
+        ));
+        self
+    }
+
+    /// Capture the given action, fires during the capture phase
+    fn capture_action<A: Action>(
+        mut self,
+        listener: impl Fn(&mut V, &A, &mut ViewContext<V>) + 'static,
+    ) -> Self {
+        self.interactivity().action_listeners.push((
+            TypeId::of::<A>(),
+            Box::new(move |view, action, phase, cx| {
+                let action = action.downcast_ref().unwrap();
+                if phase == DispatchPhase::Capture {
+                    listener(view, action, cx)
+                }
+            }),
+        ));
+        self
+    }
+
+    /// Add a listener for the given action, fires during the bubble event phase
+    fn on_action<A: Action>(
+        mut self,
+        listener: impl Fn(&mut V, &A, &mut ViewContext<V>) + 'static,
+    ) -> Self {
+        self.interactivity().action_listeners.push((
+            TypeId::of::<A>(),
+            Box::new(move |view, action, phase, cx| {
+                let action = action.downcast_ref().unwrap();
+                if phase == DispatchPhase::Bubble {
+                    listener(view, action, cx)
+                }
+            }),
+        ));
+        self
+    }
+
+    fn on_key_down(
+        mut self,
+        listener: impl Fn(&mut V, &KeyDownEvent, DispatchPhase, &mut ViewContext<V>) + 'static,
+    ) -> Self {
+        self.interactivity()
+            .key_down_listeners
+            .push(Box::new(move |view, event, phase, cx| {
+                listener(view, event, phase, cx)
+            }));
+        self
+    }
+
+    fn on_key_up(
+        mut self,
+        listener: impl Fn(&mut V, &KeyUpEvent, DispatchPhase, &mut ViewContext<V>) + 'static,
+    ) -> Self {
+        self.interactivity()
+            .key_up_listeners
+            .push(Box::new(move |view, event, phase, cx| {
+                listener(view, event, phase, cx)
+            }));
+        self
+    }
+
+    fn drag_over<S: 'static>(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self {
+        self.interactivity()
+            .drag_over_styles
+            .push((TypeId::of::<S>(), f(StyleRefinement::default())));
+        self
+    }
+
+    fn group_drag_over<S: 'static>(
+        mut self,
+        group_name: impl Into<SharedString>,
+        f: impl FnOnce(StyleRefinement) -> StyleRefinement,
+    ) -> Self {
+        self.interactivity().group_drag_over_styles.push((
+            TypeId::of::<S>(),
+            GroupStyle {
+                group: group_name.into(),
+                style: f(StyleRefinement::default()),
+            },
+        ));
+        self
+    }
+
+    fn on_drop<W: 'static>(
+        mut self,
+        listener: impl Fn(&mut V, View<W>, &mut ViewContext<V>) + 'static,
+    ) -> Self {
+        self.interactivity().drop_listeners.push((
+            TypeId::of::<W>(),
+            Box::new(move |view, dragged_view, cx| {
+                listener(view, dragged_view.downcast().unwrap(), cx);
+            }),
+        ));
+        self
+    }
 }
 
-impl<V, I, F> Div<V, I, F>
-where
-    I: ElementInteractivity<V>,
-    F: KeyDispatch<V>,
-{
-    pub fn group(mut self, group: impl Into<SharedString>) -> Self {
-        self.group = Some(group.into());
+pub trait StatefulInteractiveComponent<V: 'static, E: Element<V>>: InteractiveComponent<V> {
+    fn focusable(mut self) -> Focusable<V, Self> {
+        self.interactivity().focusable = true;
+        Focusable {
+            element: self,
+            view_type: PhantomData,
+        }
+    }
+
+    fn overflow_scroll(mut self) -> Self {
+        self.interactivity().base_style.overflow.x = Some(Overflow::Scroll);
+        self.interactivity().base_style.overflow.y = Some(Overflow::Scroll);
+        self
+    }
+
+    fn overflow_x_scroll(mut self) -> Self {
+        self.interactivity().base_style.overflow.x = Some(Overflow::Scroll);
         self
     }
 
-    pub fn z_index(mut self, z_index: u32) -> Self {
-        self.base_style.z_index = Some(z_index);
+    fn overflow_y_scroll(mut self) -> Self {
+        self.interactivity().base_style.overflow.y = Some(Overflow::Scroll);
         self
     }
 
-    pub fn context<C>(mut self, context: C) -> Self
+    fn active(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
     where
         Self: Sized,
-        C: TryInto<KeyContext>,
-        C::Error: Debug,
     {
-        if let Some(context) = context.try_into().log_err() {
-            *self.key_dispatch.key_context_mut() = context;
-        }
+        self.interactivity().active_style = f(StyleRefinement::default());
         self
     }
 
-    pub fn overflow_hidden(mut self) -> Self {
-        self.base_style.overflow.x = Some(Overflow::Hidden);
-        self.base_style.overflow.y = Some(Overflow::Hidden);
+    fn group_active(
+        mut self,
+        group_name: impl Into<SharedString>,
+        f: impl FnOnce(StyleRefinement) -> StyleRefinement,
+    ) -> Self
+    where
+        Self: Sized,
+    {
+        self.interactivity().group_active_style = Some(GroupStyle {
+            group: group_name.into(),
+            style: f(StyleRefinement::default()),
+        });
         self
     }
 
-    pub fn overflow_hidden_x(mut self) -> Self {
-        self.base_style.overflow.x = Some(Overflow::Hidden);
+    fn on_click(
+        mut self,
+        listener: impl Fn(&mut V, &ClickEvent, &mut ViewContext<V>) + 'static,
+    ) -> Self
+    where
+        Self: Sized,
+    {
+        self.interactivity()
+            .click_listeners
+            .push(Box::new(move |view, event, cx| listener(view, event, cx)));
         self
     }
 
-    pub fn overflow_hidden_y(mut self) -> Self {
-        self.base_style.overflow.y = Some(Overflow::Hidden);
+    fn on_drag<W>(
+        mut self,
+        listener: impl Fn(&mut V, &mut ViewContext<V>) -> View<W> + 'static,
+    ) -> Self
+    where
+        Self: Sized,
+        W: 'static + Render,
+    {
+        debug_assert!(
+            self.interactivity().drag_listener.is_none(),
+            "calling on_drag more than once on the same element is not supported"
+        );
+        self.interactivity().drag_listener =
+            Some(Box::new(move |view_state, cursor_offset, cx| AnyDrag {
+                view: listener(view_state, cx).into(),
+                cursor_offset,
+            }));
         self
     }
 
-    fn with_element_id<R>(
-        &mut self,
-        cx: &mut ViewContext<V>,
-        f: impl FnOnce(&mut Self, Option<GlobalElementId>, &mut ViewContext<V>) -> R,
-    ) -> R {
-        if let Some(id) = self.id() {
-            cx.with_element_id(id, |global_id, cx| f(self, Some(global_id), cx))
-        } else {
-            f(self, None, cx)
-        }
+    fn on_hover(mut self, listener: impl 'static + Fn(&mut V, bool, &mut ViewContext<V>)) -> Self
+    where
+        Self: Sized,
+    {
+        debug_assert!(
+            self.interactivity().hover_listener.is_none(),
+            "calling on_hover more than once on the same element is not supported"
+        );
+        self.interactivity().hover_listener = Some(Box::new(listener));
+        self
     }
 
-    pub fn compute_style(
-        &self,
-        bounds: Bounds<Pixels>,
-        element_state: &DivState,
-        cx: &mut ViewContext<V>,
-    ) -> Style {
-        let mut computed_style = Style::default();
-        computed_style.refine(&self.base_style);
-        self.key_dispatch.refine_style(&mut computed_style, cx);
-        self.interactivity.refine_style(
-            &mut computed_style,
-            bounds,
-            &element_state.interactive,
-            cx,
+    fn tooltip<W>(
+        mut self,
+        build_tooltip: impl Fn(&mut V, &mut ViewContext<V>) -> View<W> + 'static,
+    ) -> Self
+    where
+        Self: Sized,
+        W: 'static + Render,
+    {
+        debug_assert!(
+            self.interactivity().tooltip_builder.is_none(),
+            "calling tooltip more than once on the same element is not supported"
         );
-        computed_style
+        self.interactivity().tooltip_builder = Some(Arc::new(move |view_state, cx| {
+            build_tooltip(view_state, cx).into()
+        }));
+
+        self
     }
 }
 
-impl<V: 'static> Div<V, StatefulInteractivity<V>, NonFocusableKeyDispatch> {
-    pub fn focusable(self) -> Div<V, StatefulInteractivity<V>, FocusableKeyDispatch<V>> {
-        Div {
-            interactivity: self.interactivity,
-            key_dispatch: FocusableKeyDispatch::new(self.key_dispatch),
-            children: self.children,
-            group: self.group,
-            base_style: self.base_style,
-        }
+pub trait FocusableComponent<V: 'static>: InteractiveComponent<V> {
+    fn focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
+    where
+        Self: Sized,
+    {
+        self.interactivity().focus_style = f(StyleRefinement::default());
+        self
     }
 
-    pub fn track_focus(
-        self,
-        handle: &FocusHandle,
-    ) -> Div<V, StatefulInteractivity<V>, FocusableKeyDispatch<V>> {
-        Div {
-            interactivity: self.interactivity,
-            key_dispatch: FocusableKeyDispatch::tracked(self.key_dispatch, handle),
-            children: self.children,
-            group: self.group,
-            base_style: self.base_style,
-        }
+    fn focus_in(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
+    where
+        Self: Sized,
+    {
+        self.interactivity().focus_in_style = f(StyleRefinement::default());
+        self
     }
 
-    pub fn overflow_scroll(mut self) -> Self {
-        self.base_style.overflow.x = Some(Overflow::Scroll);
-        self.base_style.overflow.y = Some(Overflow::Scroll);
+    fn in_focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
+    where
+        Self: Sized,
+    {
+        self.interactivity().in_focus_style = f(StyleRefinement::default());
         self
     }
 
-    pub fn overflow_x_scroll(mut self) -> Self {
-        self.base_style.overflow.x = Some(Overflow::Scroll);
+    fn on_focus(
+        mut self,
+        listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext<V>) + 'static,
+    ) -> Self
+    where
+        Self: Sized,
+    {
+        self.interactivity().focus_listeners.push(Box::new(
+            move |view, focus_handle, event, cx| {
+                if event.focused.as_ref() == Some(focus_handle) {
+                    listener(view, event, cx)
+                }
+            },
+        ));
         self
     }
 
-    pub fn overflow_y_scroll(mut self) -> Self {
-        self.base_style.overflow.y = Some(Overflow::Scroll);
+    fn on_blur(
+        mut self,
+        listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext<V>) + 'static,
+    ) -> Self
+    where
+        Self: Sized,
+    {
+        self.interactivity().focus_listeners.push(Box::new(
+            move |view, focus_handle, event, cx| {
+                if event.blurred.as_ref() == Some(focus_handle) {
+                    listener(view, event, cx)
+                }
+            },
+        ));
         self
     }
-}
 
-impl<V: 'static> Div<V, StatelessInteractivity<V>, NonFocusableKeyDispatch> {
-    pub fn track_focus(
-        self,
-        handle: &FocusHandle,
-    ) -> Div<V, StatefulInteractivity<V>, FocusableKeyDispatch<V>> {
-        Div {
-            interactivity: self.interactivity.into_stateful(handle),
-            key_dispatch: FocusableKeyDispatch::tracked(self.key_dispatch, handle),
-            children: self.children,
-            group: self.group,
-            base_style: self.base_style,
-        }
+    fn on_focus_in(
+        mut self,
+        listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext<V>) + 'static,
+    ) -> Self
+    where
+        Self: Sized,
+    {
+        self.interactivity().focus_listeners.push(Box::new(
+            move |view, focus_handle, event, cx| {
+                let descendant_blurred = event
+                    .blurred
+                    .as_ref()
+                    .map_or(false, |blurred| focus_handle.contains(blurred, cx));
+                let descendant_focused = event
+                    .focused
+                    .as_ref()
+                    .map_or(false, |focused| focus_handle.contains(focused, cx));
+
+                if !descendant_blurred && descendant_focused {
+                    listener(view, event, cx)
+                }
+            },
+        ));
+        self
     }
-}
 
-impl<V, I> Focusable<V> for Div<V, I, FocusableKeyDispatch<V>>
-where
-    V: 'static,
-    I: ElementInteractivity<V>,
-{
-    fn focus_listeners(&mut self) -> &mut FocusListeners<V> {
-        &mut self.key_dispatch.focus_listeners
+    fn on_focus_out(
+        mut self,
+        listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext<V>) + 'static,
+    ) -> Self
+    where
+        Self: Sized,
+    {
+        self.interactivity().focus_listeners.push(Box::new(
+            move |view, focus_handle, event, cx| {
+                let descendant_blurred = event
+                    .blurred
+                    .as_ref()
+                    .map_or(false, |blurred| focus_handle.contains(blurred, cx));
+                let descendant_focused = event
+                    .focused
+                    .as_ref()
+                    .map_or(false, |focused| focus_handle.contains(focused, cx));
+                if descendant_blurred && !descendant_focused {
+                    listener(view, event, cx)
+                }
+            },
+        ));
+        self
     }
+}
+
+pub type FocusListeners<V> = SmallVec<[FocusListener<V>; 2]>;
+
+pub type FocusListener<V> =
+    Box<dyn Fn(&mut V, &FocusHandle, &FocusEvent, &mut ViewContext<V>) + 'static>;
+
+pub type MouseDownListener<V> = Box<
+    dyn Fn(&mut V, &MouseDownEvent, &Bounds<Pixels>, DispatchPhase, &mut ViewContext<V>) + 'static,
+>;
+pub type MouseUpListener<V> = Box<
+    dyn Fn(&mut V, &MouseUpEvent, &Bounds<Pixels>, DispatchPhase, &mut ViewContext<V>) + 'static,
+>;
+
+pub type MouseMoveListener<V> = Box<
+    dyn Fn(&mut V, &MouseMoveEvent, &Bounds<Pixels>, DispatchPhase, &mut ViewContext<V>) + 'static,
+>;
+
+pub type ScrollWheelListener<V> = Box<
+    dyn Fn(&mut V, &ScrollWheelEvent, &Bounds<Pixels>, DispatchPhase, &mut ViewContext<V>)
+        + 'static,
+>;
+
+pub type ClickListener<V> = Box<dyn Fn(&mut V, &ClickEvent, &mut ViewContext<V>) + 'static>;
+
+pub type DragListener<V> =
+    Box<dyn Fn(&mut V, Point<Pixels>, &mut ViewContext<V>) -> AnyDrag + 'static>;
+
+type DropListener<V> = dyn Fn(&mut V, AnyView, &mut ViewContext<V>) + 'static;
+
+pub type HoverListener<V> = Box<dyn Fn(&mut V, bool, &mut ViewContext<V>) + 'static>;
+
+pub type TooltipBuilder<V> = Arc<dyn Fn(&mut V, &mut ViewContext<V>) -> AnyView + 'static>;
+
+pub type KeyDownListener<V> =
+    Box<dyn Fn(&mut V, &KeyDownEvent, DispatchPhase, &mut ViewContext<V>) + 'static>;
 
-    fn set_focus_style(&mut self, style: StyleRefinement) {
-        self.key_dispatch.focus_style = style;
+pub type KeyUpListener<V> =
+    Box<dyn Fn(&mut V, &KeyUpEvent, DispatchPhase, &mut ViewContext<V>) + 'static>;
+
+pub type ActionListener<V> =
+    Box<dyn Fn(&mut V, &dyn Any, DispatchPhase, &mut ViewContext<V>) + 'static>;
+
+pub fn div<V: 'static>() -> Div<V> {
+    Div {
+        interactivity: Interactivity::default(),
+        children: SmallVec::default(),
     }
+}
 
-    fn set_focus_in_style(&mut self, style: StyleRefinement) {
-        self.key_dispatch.focus_in_style = style;
+pub struct Div<V> {
+    interactivity: Interactivity<V>,
+    children: SmallVec<[AnyElement<V>; 2]>,
+}
+
+impl<V> Styled for Div<V> {
+    fn style(&mut self) -> &mut StyleRefinement {
+        &mut self.interactivity.base_style
     }
+}
 
-    fn set_in_focus_style(&mut self, style: StyleRefinement) {
-        self.key_dispatch.in_focus_style = style;
+impl<V: 'static> InteractiveComponent<V> for Div<V> {
+    fn interactivity(&mut self) -> &mut Interactivity<V> {
+        &mut self.interactivity
     }
 }
 
-#[derive(Default)]
-pub struct DivState {
-    interactive: InteractiveElementState,
-    focus_handle: Option<FocusHandle>,
-    child_layout_ids: SmallVec<[LayoutId; 4]>,
+impl<V: 'static> ParentComponent<V> for Div<V> {
+    fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
+        &mut self.children
+    }
 }
 
-impl<V, I, F> Element<V> for Div<V, I, F>
-where
-    I: ElementInteractivity<V>,
-    F: KeyDispatch<V>,
-{
+impl<V: 'static> Element<V> for Div<V> {
     type ElementState = DivState;
 
-    fn id(&self) -> Option<ElementId> {
-        self.interactivity
-            .as_stateful()
-            .map(|identified| identified.id.clone())
+    fn element_id(&self) -> Option<ElementId> {
+        self.interactivity.element_id.clone()
     }
 
     fn initialize(
@@ -228,21 +609,18 @@ where
         element_state: Option<Self::ElementState>,
         cx: &mut ViewContext<V>,
     ) -> Self::ElementState {
-        let mut element_state = element_state.unwrap_or_default();
-        self.with_element_id(cx, |this, _global_id, cx| {
-            this.key_dispatch.initialize(
-                element_state.focus_handle.take(),
-                cx,
-                |focus_handle, cx| {
-                    this.interactivity.initialize(cx);
-                    element_state.focus_handle = focus_handle;
-                    for child in &mut this.children {
-                        child.initialize(view_state, cx);
-                    }
-                },
-            );
-        });
-        element_state
+        let interactive_state = self
+            .interactivity
+            .initialize(element_state.map(|s| s.interactive_state), cx);
+
+        for child in &mut self.children {
+            child.initialize(view_state, cx);
+        }
+
+        DivState {
+            interactive_state,
+            child_layout_ids: SmallVec::new(),
+        }
     }
 
     fn layout(
@@ -250,19 +628,21 @@ where
         view_state: &mut V,
         element_state: &mut Self::ElementState,
         cx: &mut ViewContext<V>,
-    ) -> LayoutId {
-        let style = self.compute_style(Bounds::default(), element_state, cx);
-        style.apply_text_style(cx, |cx| {
-            self.with_element_id(cx, |this, _global_id, cx| {
-                let layout_ids = this
-                    .children
-                    .iter_mut()
-                    .map(|child| child.layout(view_state, cx))
-                    .collect::<SmallVec<_>>();
-                element_state.child_layout_ids = layout_ids.clone();
-                cx.request_layout(&style, layout_ids)
-            })
-        })
+    ) -> crate::LayoutId {
+        let mut interactivity = mem::take(&mut self.interactivity);
+        let layout_id =
+            interactivity.layout(&mut element_state.interactive_state, cx, |style, cx| {
+                cx.with_text_style(style.text_style().cloned(), |cx| {
+                    element_state.child_layout_ids = self
+                        .children
+                        .iter_mut()
+                        .map(|child| child.layout(view_state, cx))
+                        .collect::<SmallVec<_>>();
+                    cx.request_layout(&style, element_state.child_layout_ids.iter().copied())
+                })
+            });
+        self.interactivity = interactivity;
+        layout_id
     }
 
     fn paint(
@@ -272,117 +652,801 @@ where
         element_state: &mut Self::ElementState,
         cx: &mut ViewContext<V>,
     ) {
-        self.with_element_id(cx, |this, _global_id, cx| {
-            let style = this.compute_style(bounds, element_state, cx);
-            if style.visibility == Visibility::Hidden {
-                return;
+        let mut child_min = point(Pixels::MAX, Pixels::MAX);
+        let mut child_max = Point::default();
+        let content_size = if element_state.child_layout_ids.is_empty() {
+            bounds.size
+        } else {
+            for child_layout_id in &element_state.child_layout_ids {
+                let child_bounds = cx.layout_bounds(*child_layout_id);
+                child_min = child_min.min(&child_bounds.origin);
+                child_max = child_max.max(&child_bounds.lower_right());
             }
+            (child_max - child_min).into()
+        };
 
-            if let Some(mouse_cursor) = style.mouse_cursor {
-                let hovered = bounds.contains_point(&cx.mouse_position());
-                if hovered {
-                    cx.set_cursor_style(mouse_cursor);
+        let mut interactivity = mem::take(&mut self.interactivity);
+        interactivity.paint(
+            bounds,
+            content_size,
+            &mut element_state.interactive_state,
+            cx,
+            |style, scroll_offset, cx| {
+                if style.visibility == Visibility::Hidden {
+                    return;
                 }
-            }
 
-            if let Some(group) = this.group.clone() {
-                GroupBounds::push(group, bounds, cx);
+                let z_index = style.z_index.unwrap_or(0);
+
+                cx.with_z_index(z_index, |cx| {
+                    cx.with_z_index(0, |cx| {
+                        style.paint(bounds, cx);
+                    });
+                    cx.with_z_index(1, |cx| {
+                        cx.with_text_style(style.text_style().cloned(), |cx| {
+                            cx.with_content_mask(style.overflow_mask(bounds), |cx| {
+                                cx.with_element_offset(scroll_offset, |cx| {
+                                    for child in &mut self.children {
+                                        child.paint(view_state, cx);
+                                    }
+                                })
+                            })
+                        })
+                    })
+                })
+            },
+        );
+        self.interactivity = interactivity;
+    }
+}
+
+impl<V: 'static> Component<V> for Div<V> {
+    fn render(self) -> AnyElement<V> {
+        AnyElement::new(self)
+    }
+}
+
+pub struct DivState {
+    child_layout_ids: SmallVec<[LayoutId; 4]>,
+    interactive_state: InteractiveElementState,
+}
+
+pub struct Interactivity<V> {
+    pub element_id: Option<ElementId>,
+    pub key_context: KeyContext,
+    pub focusable: bool,
+    pub tracked_focus_handle: Option<FocusHandle>,
+    pub focus_listeners: FocusListeners<V>,
+    pub group: Option<SharedString>,
+    pub base_style: StyleRefinement,
+    pub focus_style: StyleRefinement,
+    pub focus_in_style: StyleRefinement,
+    pub in_focus_style: StyleRefinement,
+    pub hover_style: StyleRefinement,
+    pub group_hover_style: Option<GroupStyle>,
+    pub active_style: StyleRefinement,
+    pub group_active_style: Option<GroupStyle>,
+    pub drag_over_styles: SmallVec<[(TypeId, StyleRefinement); 2]>,
+    pub group_drag_over_styles: SmallVec<[(TypeId, GroupStyle); 2]>,
+    pub mouse_down_listeners: SmallVec<[MouseDownListener<V>; 2]>,
+    pub mouse_up_listeners: SmallVec<[MouseUpListener<V>; 2]>,
+    pub mouse_move_listeners: SmallVec<[MouseMoveListener<V>; 2]>,
+    pub scroll_wheel_listeners: SmallVec<[ScrollWheelListener<V>; 2]>,
+    pub key_down_listeners: SmallVec<[KeyDownListener<V>; 2]>,
+    pub key_up_listeners: SmallVec<[KeyUpListener<V>; 2]>,
+    pub action_listeners: SmallVec<[(TypeId, ActionListener<V>); 8]>,
+    pub drop_listeners: SmallVec<[(TypeId, Box<DropListener<V>>); 2]>,
+    pub click_listeners: SmallVec<[ClickListener<V>; 2]>,
+    pub drag_listener: Option<DragListener<V>>,
+    pub hover_listener: Option<HoverListener<V>>,
+    pub tooltip_builder: Option<TooltipBuilder<V>>,
+}
+
+impl<V> Interactivity<V>
+where
+    V: 'static,
+{
+    pub fn initialize(
+        &mut self,
+        element_state: Option<InteractiveElementState>,
+        cx: &mut ViewContext<V>,
+    ) -> InteractiveElementState {
+        let mut element_state = element_state.unwrap_or_default();
+
+        // Ensure we store a focus handle in our element state if we're focusable.
+        // If there's an explicit focus handle we're tracking, use that. Otherwise
+        // create a new handle and store it in the element state, which lives for as
+        // as frames contain an element with this id.
+        if self.focusable {
+            element_state.focus_handle.get_or_insert_with(|| {
+                self.tracked_focus_handle
+                    .clone()
+                    .unwrap_or_else(|| cx.focus_handle())
+            });
+        }
+
+        element_state
+    }
+
+    pub fn layout(
+        &mut self,
+        element_state: &mut InteractiveElementState,
+        cx: &mut ViewContext<V>,
+        f: impl FnOnce(Style, &mut ViewContext<V>) -> LayoutId,
+    ) -> LayoutId {
+        let style = self.compute_style(None, element_state, cx);
+        f(style, cx)
+    }
+
+    pub fn paint(
+        &mut self,
+        bounds: Bounds<Pixels>,
+        content_size: Size<Pixels>,
+        element_state: &mut InteractiveElementState,
+        cx: &mut ViewContext<V>,
+        f: impl FnOnce(Style, Point<Pixels>, &mut ViewContext<V>),
+    ) {
+        let style = self.compute_style(Some(bounds), element_state, cx);
+
+        if let Some(mouse_cursor) = style.mouse_cursor {
+            let hovered = bounds.contains_point(&cx.mouse_position());
+            if hovered {
+                cx.set_cursor_style(mouse_cursor);
             }
+        }
+
+        for listener in self.mouse_down_listeners.drain(..) {
+            cx.on_mouse_event(move |state, event: &MouseDownEvent, phase, cx| {
+                listener(state, event, &bounds, phase, cx);
+            })
+        }
 
-            let z_index = style.z_index.unwrap_or(0);
+        for listener in self.mouse_up_listeners.drain(..) {
+            cx.on_mouse_event(move |state, event: &MouseUpEvent, phase, cx| {
+                listener(state, event, &bounds, phase, cx);
+            })
+        }
+
+        for listener in self.mouse_move_listeners.drain(..) {
+            cx.on_mouse_event(move |state, event: &MouseMoveEvent, phase, cx| {
+                listener(state, event, &bounds, phase, cx);
+            })
+        }
 
-            let mut child_min = point(Pixels::MAX, Pixels::MAX);
-            let mut child_max = Point::default();
+        for listener in self.scroll_wheel_listeners.drain(..) {
+            cx.on_mouse_event(move |state, event: &ScrollWheelEvent, phase, cx| {
+                listener(state, event, &bounds, phase, cx);
+            })
+        }
 
-            let content_size = if element_state.child_layout_ids.is_empty() {
-                bounds.size
-            } else {
-                for child_layout_id in &element_state.child_layout_ids {
-                    let child_bounds = cx.layout_bounds(*child_layout_id);
-                    child_min = child_min.min(&child_bounds.origin);
-                    child_max = child_max.max(&child_bounds.lower_right());
-                }
-                (child_max - child_min).into()
-            };
-
-            cx.with_z_index(z_index, |cx| {
-                cx.with_z_index(0, |cx| {
-                    style.paint(bounds, cx);
-                    this.key_dispatch.paint(bounds, cx);
-                    this.interactivity.paint(
-                        bounds,
-                        content_size,
-                        style.overflow,
-                        &mut element_state.interactive,
-                        cx,
-                    );
+        let hover_group_bounds = self
+            .group_hover_style
+            .as_ref()
+            .and_then(|group_hover| GroupBounds::get(&group_hover.group, cx));
+
+        if let Some(group_bounds) = hover_group_bounds {
+            let hovered = group_bounds.contains_point(&cx.mouse_position());
+            cx.on_mouse_event(move |_, event: &MouseMoveEvent, phase, cx| {
+                if phase == DispatchPhase::Capture {
+                    if group_bounds.contains_point(&event.position) != hovered {
+                        cx.notify();
+                    }
+                }
+            });
+        }
+
+        if self.hover_style.is_some()
+            || (cx.active_drag.is_some() && !self.drag_over_styles.is_empty())
+        {
+            let hovered = bounds.contains_point(&cx.mouse_position());
+            cx.on_mouse_event(move |_, event: &MouseMoveEvent, phase, cx| {
+                if phase == DispatchPhase::Capture {
+                    if bounds.contains_point(&event.position) != hovered {
+                        cx.notify();
+                    }
+                }
+            });
+        }
+
+        if cx.active_drag.is_some() {
+            let drop_listeners = mem::take(&mut self.drop_listeners);
+            cx.on_mouse_event(move |view, event: &MouseUpEvent, phase, cx| {
+                if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
+                    if let Some(drag_state_type) =
+                        cx.active_drag.as_ref().map(|drag| drag.view.entity_type())
+                    {
+                        for (drop_state_type, listener) in &drop_listeners {
+                            if *drop_state_type == drag_state_type {
+                                let drag = cx
+                                    .active_drag
+                                    .take()
+                                    .expect("checked for type drag state type above");
+                                listener(view, drag.view.clone(), cx);
+                                cx.notify();
+                                cx.stop_propagation();
+                            }
+                        }
+                    }
+                }
+            });
+        }
+
+        let click_listeners = mem::take(&mut self.click_listeners);
+        let drag_listener = mem::take(&mut self.drag_listener);
+
+        if !click_listeners.is_empty() || drag_listener.is_some() {
+            let pending_mouse_down = element_state.pending_mouse_down.clone();
+            let mouse_down = pending_mouse_down.lock().clone();
+            if let Some(mouse_down) = mouse_down {
+                if let Some(drag_listener) = drag_listener {
+                    let active_state = element_state.clicked_state.clone();
+
+                    cx.on_mouse_event(move |view_state, event: &MouseMoveEvent, phase, cx| {
+                        if cx.active_drag.is_some() {
+                            if phase == DispatchPhase::Capture {
+                                cx.notify();
+                            }
+                        } else if phase == DispatchPhase::Bubble
+                            && bounds.contains_point(&event.position)
+                            && (event.position - mouse_down.position).magnitude() > DRAG_THRESHOLD
+                        {
+                            *active_state.lock() = ElementClickedState::default();
+                            let cursor_offset = event.position - bounds.origin;
+                            let drag = drag_listener(view_state, cursor_offset, cx);
+                            cx.active_drag = Some(drag);
+                            cx.notify();
+                            cx.stop_propagation();
+                        }
+                    });
+                }
+
+                cx.on_mouse_event(move |view_state, event: &MouseUpEvent, phase, cx| {
+                    if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
+                        let mouse_click = ClickEvent {
+                            down: mouse_down.clone(),
+                            up: event.clone(),
+                        };
+                        for listener in &click_listeners {
+                            listener(view_state, &mouse_click, cx);
+                        }
+                    }
+                    *pending_mouse_down.lock() = None;
+                    cx.notify();
                 });
-                cx.with_z_index(1, |cx| {
-                    style.apply_text_style(cx, |cx| {
-                        style.apply_overflow(bounds, cx, |cx| {
-                            let scroll_offset = element_state.interactive.scroll_offset();
-                            cx.with_element_offset(scroll_offset, |cx| {
-                                for child in &mut this.children {
-                                    child.paint(view_state, cx);
-                                }
-                            });
-                        })
-                    })
+            } else {
+                cx.on_mouse_event(move |_view_state, event: &MouseDownEvent, phase, cx| {
+                    if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
+                        *pending_mouse_down.lock() = Some(event.clone());
+                        cx.notify();
+                    }
                 });
+            }
+        }
+
+        if let Some(hover_listener) = self.hover_listener.take() {
+            let was_hovered = element_state.hover_state.clone();
+            let has_mouse_down = element_state.pending_mouse_down.clone();
+
+            cx.on_mouse_event(move |view_state, event: &MouseMoveEvent, phase, cx| {
+                if phase != DispatchPhase::Bubble {
+                    return;
+                }
+                let is_hovered =
+                    bounds.contains_point(&event.position) && has_mouse_down.lock().is_none();
+                let mut was_hovered = was_hovered.lock();
+
+                if is_hovered != was_hovered.clone() {
+                    *was_hovered = is_hovered;
+                    drop(was_hovered);
+
+                    hover_listener(view_state, is_hovered, cx);
+                }
+            });
+        }
+
+        if let Some(tooltip_builder) = self.tooltip_builder.take() {
+            let active_tooltip = element_state.active_tooltip.clone();
+            let pending_mouse_down = element_state.pending_mouse_down.clone();
+
+            cx.on_mouse_event(move |_, event: &MouseMoveEvent, phase, cx| {
+                if phase != DispatchPhase::Bubble {
+                    return;
+                }
+
+                let is_hovered =
+                    bounds.contains_point(&event.position) && pending_mouse_down.lock().is_none();
+                if !is_hovered {
+                    active_tooltip.lock().take();
+                    return;
+                }
+
+                if active_tooltip.lock().is_none() {
+                    let task = cx.spawn({
+                        let active_tooltip = active_tooltip.clone();
+                        let tooltip_builder = tooltip_builder.clone();
+
+                        move |view, mut cx| async move {
+                            cx.background_executor().timer(TOOLTIP_DELAY).await;
+                            view.update(&mut cx, move |view_state, cx| {
+                                active_tooltip.lock().replace(ActiveTooltip {
+                                    waiting: None,
+                                    tooltip: Some(AnyTooltip {
+                                        view: tooltip_builder(view_state, cx),
+                                        cursor_offset: cx.mouse_position() + TOOLTIP_OFFSET,
+                                    }),
+                                });
+                                cx.notify();
+                            })
+                            .ok();
+                        }
+                    });
+                    active_tooltip.lock().replace(ActiveTooltip {
+                        waiting: Some(task),
+                        tooltip: None,
+                    });
+                }
             });
 
-            if let Some(group) = this.group.as_ref() {
-                GroupBounds::pop(group, cx);
+            if let Some(active_tooltip) = element_state.active_tooltip.lock().as_ref() {
+                if active_tooltip.tooltip.is_some() {
+                    cx.active_tooltip = active_tooltip.tooltip.clone()
+                }
             }
-        })
+        }
+
+        let active_state = element_state.clicked_state.clone();
+        if !active_state.lock().is_clicked() {
+            cx.on_mouse_event(move |_, _: &MouseUpEvent, phase, cx| {
+                if phase == DispatchPhase::Capture {
+                    *active_state.lock() = ElementClickedState::default();
+                    cx.notify();
+                }
+            });
+        } else {
+            let active_group_bounds = self
+                .group_active_style
+                .as_ref()
+                .and_then(|group_active| GroupBounds::get(&group_active.group, cx));
+            cx.on_mouse_event(move |_view, down: &MouseDownEvent, phase, cx| {
+                if phase == DispatchPhase::Bubble {
+                    let group = active_group_bounds
+                        .map_or(false, |bounds| bounds.contains_point(&down.position));
+                    let element = bounds.contains_point(&down.position);
+                    if group || element {
+                        *active_state.lock() = ElementClickedState { group, element };
+                        cx.notify();
+                    }
+                }
+            });
+        }
+
+        let overflow = style.overflow;
+        if overflow.x == Overflow::Scroll || overflow.y == Overflow::Scroll {
+            let scroll_offset = element_state
+                .scroll_offset
+                .get_or_insert_with(Arc::default)
+                .clone();
+            let line_height = cx.line_height();
+            let scroll_max = (content_size - bounds.size).max(&Size::default());
+
+            cx.on_mouse_event(move |_, event: &ScrollWheelEvent, phase, cx| {
+                if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
+                    let mut scroll_offset = scroll_offset.lock();
+                    let old_scroll_offset = *scroll_offset;
+                    let delta = event.delta.pixel_delta(line_height);
+
+                    if overflow.x == Overflow::Scroll {
+                        scroll_offset.x =
+                            (scroll_offset.x + delta.x).clamp(-scroll_max.width, px(0.));
+                    }
+
+                    if overflow.y == Overflow::Scroll {
+                        scroll_offset.y =
+                            (scroll_offset.y + delta.y).clamp(-scroll_max.height, px(0.));
+                    }
+
+                    if *scroll_offset != old_scroll_offset {
+                        cx.notify();
+                        cx.stop_propagation();
+                    }
+                }
+            });
+        }
+
+        if let Some(group) = self.group.clone() {
+            GroupBounds::push(group, bounds, cx);
+        }
+
+        let scroll_offset = element_state
+            .scroll_offset
+            .as_ref()
+            .map(|scroll_offset| *scroll_offset.lock());
+
+        cx.with_key_dispatch(
+            self.key_context.clone(),
+            element_state.focus_handle.clone(),
+            |_, cx| {
+                for listener in self.key_down_listeners.drain(..) {
+                    cx.on_key_event(move |state, event: &KeyDownEvent, phase, cx| {
+                        listener(state, event, phase, cx);
+                    })
+                }
+
+                for listener in self.key_up_listeners.drain(..) {
+                    cx.on_key_event(move |state, event: &KeyUpEvent, phase, cx| {
+                        listener(state, event, phase, cx);
+                    })
+                }
+
+                for (action_type, listener) in self.action_listeners.drain(..) {
+                    cx.on_action(action_type, listener)
+                }
+
+                if let Some(focus_handle) = element_state.focus_handle.as_ref() {
+                    for listener in self.focus_listeners.drain(..) {
+                        let focus_handle = focus_handle.clone();
+                        cx.on_focus_changed(move |view, event, cx| {
+                            listener(view, &focus_handle, event, cx)
+                        });
+                    }
+                }
+
+                f(style, scroll_offset.unwrap_or_default(), cx)
+            },
+        );
+
+        if let Some(group) = self.group.as_ref() {
+            GroupBounds::pop(group, cx);
+        }
+    }
+
+    pub fn compute_style(
+        &self,
+        bounds: Option<Bounds<Pixels>>,
+        element_state: &mut InteractiveElementState,
+        cx: &mut ViewContext<V>,
+    ) -> Style {
+        let mut style = Style::default();
+        style.refine(&self.base_style);
+
+        if let Some(focus_handle) = self.tracked_focus_handle.as_ref() {
+            if focus_handle.contains_focused(cx) {
+                style.refine(&self.focus_in_style);
+            }
+
+            if focus_handle.within_focused(cx) {
+                style.refine(&self.in_focus_style);
+            }
+
+            if focus_handle.is_focused(cx) {
+                style.refine(&self.focus_style);
+            }
+        }
+
+        if let Some(bounds) = bounds {
+            let mouse_position = cx.mouse_position();
+            if let Some(group_hover) = self.group_hover_style.as_ref() {
+                if let Some(group_bounds) = GroupBounds::get(&group_hover.group, cx) {
+                    if group_bounds.contains_point(&mouse_position) {
+                        style.refine(&group_hover.style);
+                    }
+                }
+            }
+            if bounds.contains_point(&mouse_position) {
+                style.refine(&self.hover_style);
+            }
+
+            if let Some(drag) = cx.active_drag.take() {
+                for (state_type, group_drag_style) in &self.group_drag_over_styles {
+                    if let Some(group_bounds) = GroupBounds::get(&group_drag_style.group, cx) {
+                        if *state_type == drag.view.entity_type()
+                            && group_bounds.contains_point(&mouse_position)
+                        {
+                            style.refine(&group_drag_style.style);
+                        }
+                    }
+                }
+
+                for (state_type, drag_over_style) in &self.drag_over_styles {
+                    if *state_type == drag.view.entity_type()
+                        && bounds.contains_point(&mouse_position)
+                    {
+                        style.refine(drag_over_style);
+                    }
+                }
+
+                cx.active_drag = Some(drag);
+            }
+        }
+
+        let clicked_state = element_state.clicked_state.lock();
+        if clicked_state.group {
+            if let Some(group) = self.group_active_style.as_ref() {
+                style.refine(&group.style)
+            }
+        }
+
+        if clicked_state.element {
+            style.refine(&self.active_style)
+        }
+
+        style
+    }
+}
+
+impl<V: 'static> Default for Interactivity<V> {
+    fn default() -> Self {
+        Self {
+            element_id: None,
+            key_context: KeyContext::default(),
+            focusable: false,
+            tracked_focus_handle: None,
+            focus_listeners: SmallVec::default(),
+            // scroll_offset: Point::default(),
+            group: None,
+            base_style: StyleRefinement::default(),
+            focus_style: StyleRefinement::default(),
+            focus_in_style: StyleRefinement::default(),
+            in_focus_style: StyleRefinement::default(),
+            hover_style: StyleRefinement::default(),
+            group_hover_style: None,
+            active_style: StyleRefinement::default(),
+            group_active_style: None,
+            drag_over_styles: SmallVec::new(),
+            group_drag_over_styles: SmallVec::new(),
+            mouse_down_listeners: SmallVec::new(),
+            mouse_up_listeners: SmallVec::new(),
+            mouse_move_listeners: SmallVec::new(),
+            scroll_wheel_listeners: SmallVec::new(),
+            key_down_listeners: SmallVec::new(),
+            key_up_listeners: SmallVec::new(),
+            action_listeners: SmallVec::new(),
+            drop_listeners: SmallVec::new(),
+            click_listeners: SmallVec::new(),
+            drag_listener: None,
+            hover_listener: None,
+            tooltip_builder: None,
+        }
     }
 }
 
-impl<V, I, F> Component<V> for Div<V, I, F>
+#[derive(Default)]
+pub struct InteractiveElementState {
+    pub focus_handle: Option<FocusHandle>,
+    pub clicked_state: Arc<Mutex<ElementClickedState>>,
+    pub hover_state: Arc<Mutex<bool>>,
+    pub pending_mouse_down: Arc<Mutex<Option<MouseDownEvent>>>,
+    pub scroll_offset: Option<Arc<Mutex<Point<Pixels>>>>,
+    pub active_tooltip: Arc<Mutex<Option<ActiveTooltip>>>,
+}
+
+pub struct ActiveTooltip {
+    #[allow(unused)] // used to drop the task
+    waiting: Option<Task<()>>,
+    tooltip: Option<AnyTooltip>,
+}
+
+/// Whether or not the element or a group that contains it is clicked by the mouse.
+#[derive(Copy, Clone, Default, Eq, PartialEq)]
+pub struct ElementClickedState {
+    pub group: bool,
+    pub element: bool,
+}
+
+impl ElementClickedState {
+    fn is_clicked(&self) -> bool {
+        self.group || self.element
+    }
+}
+
+#[derive(Default)]
+pub struct GroupBounds(HashMap<SharedString, SmallVec<[Bounds<Pixels>; 1]>>);
+
+impl GroupBounds {
+    pub fn get(name: &SharedString, cx: &mut AppContext) -> Option<Bounds<Pixels>> {
+        cx.default_global::<Self>()
+            .0
+            .get(name)
+            .and_then(|bounds_stack| bounds_stack.last())
+            .cloned()
+    }
+
+    pub fn push(name: SharedString, bounds: Bounds<Pixels>, cx: &mut AppContext) {
+        cx.default_global::<Self>()
+            .0
+            .entry(name)
+            .or_default()
+            .push(bounds);
+    }
+
+    pub fn pop(name: &SharedString, cx: &mut AppContext) {
+        cx.default_global::<Self>().0.get_mut(name).unwrap().pop();
+    }
+}
+
+pub struct Focusable<V, E> {
+    element: E,
+    view_type: PhantomData<V>,
+}
+
+impl<V: 'static, E: InteractiveComponent<V>> FocusableComponent<V> for Focusable<V, E> {}
+
+impl<V, E> InteractiveComponent<V> for Focusable<V, E>
+where
+    V: 'static,
+    E: InteractiveComponent<V>,
+{
+    fn interactivity(&mut self) -> &mut Interactivity<V> {
+        self.element.interactivity()
+    }
+}
+
+impl<V: 'static, E: StatefulInteractiveComponent<V, E>> StatefulInteractiveComponent<V, E>
+    for Focusable<V, E>
+{
+}
+
+impl<V, E> Styled for Focusable<V, E>
+where
+    V: 'static,
+    E: Styled,
+{
+    fn style(&mut self) -> &mut StyleRefinement {
+        self.element.style()
+    }
+}
+
+impl<V, E> Element<V> for Focusable<V, E>
+where
+    V: 'static,
+    E: Element<V>,
+{
+    type ElementState = E::ElementState;
+
+    fn element_id(&self) -> Option<ElementId> {
+        self.element.element_id()
+    }
+
+    fn initialize(
+        &mut self,
+        view_state: &mut V,
+        element_state: Option<Self::ElementState>,
+        cx: &mut ViewContext<V>,
+    ) -> Self::ElementState {
+        self.element.initialize(view_state, element_state, cx)
+    }
+
+    fn layout(
+        &mut self,
+        view_state: &mut V,
+        element_state: &mut Self::ElementState,
+        cx: &mut ViewContext<V>,
+    ) -> LayoutId {
+        self.element.layout(view_state, element_state, cx)
+    }
+
+    fn paint(
+        &mut self,
+        bounds: Bounds<Pixels>,
+        view_state: &mut V,
+        element_state: &mut Self::ElementState,
+        cx: &mut ViewContext<V>,
+    ) {
+        self.element.paint(bounds, view_state, element_state, cx);
+    }
+}
+
+impl<V, E> Component<V> for Focusable<V, E>
 where
-    I: ElementInteractivity<V>,
-    F: KeyDispatch<V>,
+    V: 'static,
+    E: 'static + Element<V>,
 {
     fn render(self) -> AnyElement<V> {
         AnyElement::new(self)
     }
 }
 
-impl<V, I, F> ParentElement<V> for Div<V, I, F>
+impl<V, E> ParentComponent<V> for Focusable<V, E>
 where
-    I: ElementInteractivity<V>,
-    F: KeyDispatch<V>,
+    V: 'static,
+    E: ParentComponent<V>,
 {
     fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
-        &mut self.children
+        self.element.children_mut()
     }
 }
 
-impl<V, I, F> Styled for Div<V, I, F>
+pub struct Stateful<V, E> {
+    element: E,
+    view_type: PhantomData<V>,
+}
+
+impl<V, E> Styled for Stateful<V, E>
 where
-    I: ElementInteractivity<V>,
-    F: KeyDispatch<V>,
+    V: 'static,
+    E: Styled,
 {
     fn style(&mut self) -> &mut StyleRefinement {
-        &mut self.base_style
+        self.element.style()
     }
 }
 
-impl<V, I, F> StatelessInteractive<V> for Div<V, I, F>
+impl<V, E> StatefulInteractiveComponent<V, E> for Stateful<V, E>
+where
+    V: 'static,
+    E: Element<V>,
+    Self: InteractiveComponent<V>,
+{
+}
+
+impl<V, E> InteractiveComponent<V> for Stateful<V, E>
 where
-    I: ElementInteractivity<V>,
-    F: KeyDispatch<V>,
+    V: 'static,
+    E: InteractiveComponent<V>,
 {
-    fn stateless_interactivity(&mut self) -> &mut StatelessInteractivity<V> {
-        self.interactivity.as_stateless_mut()
+    fn interactivity(&mut self) -> &mut Interactivity<V> {
+        self.element.interactivity()
     }
 }
 
-impl<V, F> StatefulInteractive<V> for Div<V, StatefulInteractivity<V>, F>
+impl<V: 'static, E: FocusableComponent<V>> FocusableComponent<V> for Stateful<V, E> {}
+
+impl<V, E> Element<V> for Stateful<V, E>
 where
-    F: KeyDispatch<V>,
+    V: 'static,
+    E: Element<V>,
 {
-    fn stateful_interactivity(&mut self) -> &mut StatefulInteractivity<V> {
-        &mut self.interactivity
+    type ElementState = E::ElementState;
+
+    fn element_id(&self) -> Option<ElementId> {
+        self.element.element_id()
+    }
+
+    fn initialize(
+        &mut self,
+        view_state: &mut V,
+        element_state: Option<Self::ElementState>,
+        cx: &mut ViewContext<V>,
+    ) -> Self::ElementState {
+        self.element.initialize(view_state, element_state, cx)
+    }
+
+    fn layout(
+        &mut self,
+        view_state: &mut V,
+        element_state: &mut Self::ElementState,
+        cx: &mut ViewContext<V>,
+    ) -> LayoutId {
+        self.element.layout(view_state, element_state, cx)
+    }
+
+    fn paint(
+        &mut self,
+        bounds: Bounds<Pixels>,
+        view_state: &mut V,
+        element_state: &mut Self::ElementState,
+        cx: &mut ViewContext<V>,
+    ) {
+        self.element.paint(bounds, view_state, element_state, cx)
+    }
+}
+
+impl<V, E> Component<V> for Stateful<V, E>
+where
+    V: 'static,
+    E: 'static + Element<V>,
+{
+    fn render(self) -> AnyElement<V> {
+        AnyElement::new(self)
+    }
+}
+
+impl<V, E> ParentComponent<V> for Stateful<V, E>
+where
+    V: 'static,
+    E: ParentComponent<V>,
+{
+    fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
+        self.element.children_mut()
     }
 }

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

@@ -1,35 +1,28 @@
 use crate::{
-    div, AnyElement, BorrowWindow, Bounds, Component, Div, DivState, Element, ElementId,
-    ElementInteractivity, FocusListeners, Focusable, FocusableKeyDispatch, KeyDispatch, LayoutId,
-    NonFocusableKeyDispatch, Pixels, SharedString, StatefulInteractive, StatefulInteractivity,
-    StatelessInteractive, StatelessInteractivity, StyleRefinement, Styled, ViewContext,
+    AnyElement, BorrowWindow, Bounds, Component, Element, InteractiveComponent,
+    InteractiveElementState, Interactivity, LayoutId, Pixels, SharedString, StyleRefinement,
+    Styled, ViewContext,
 };
 use futures::FutureExt;
 use util::ResultExt;
 
-pub struct Img<
-    V: 'static,
-    I: ElementInteractivity<V> = StatelessInteractivity<V>,
-    F: KeyDispatch<V> = NonFocusableKeyDispatch,
-> {
-    base: Div<V, I, F>,
+pub struct Img<V: 'static> {
+    interactivity: Interactivity<V>,
     uri: Option<SharedString>,
     grayscale: bool,
 }
 
-pub fn img<V: 'static>() -> Img<V, StatelessInteractivity<V>, NonFocusableKeyDispatch> {
+pub fn img<V: 'static>() -> Img<V> {
     Img {
-        base: div(),
+        interactivity: Interactivity::default(),
         uri: None,
         grayscale: false,
     }
 }
 
-impl<V, I, F> Img<V, I, F>
+impl<V> Img<V>
 where
     V: 'static,
-    I: ElementInteractivity<V>,
-    F: KeyDispatch<V>,
 {
     pub fn uri(mut self, uri: impl Into<SharedString>) -> Self {
         self.uri = Some(uri.into());
@@ -42,145 +35,90 @@ where
     }
 }
 
-impl<V, F> Img<V, StatelessInteractivity<V>, F>
-where
-    F: KeyDispatch<V>,
-{
-    pub fn id(self, id: impl Into<ElementId>) -> Img<V, StatefulInteractivity<V>, F> {
-        Img {
-            base: self.base.id(id),
-            uri: self.uri,
-            grayscale: self.grayscale,
-        }
-    }
-}
-
-impl<V, I, F> Component<V> for Img<V, I, F>
-where
-    I: ElementInteractivity<V>,
-    F: KeyDispatch<V>,
-{
+impl<V> Component<V> for Img<V> {
     fn render(self) -> AnyElement<V> {
         AnyElement::new(self)
     }
 }
 
-impl<V, I, F> Element<V> for Img<V, I, F>
-where
-    I: ElementInteractivity<V>,
-    F: KeyDispatch<V>,
-{
-    type ElementState = DivState;
+impl<V> Element<V> for Img<V> {
+    type ElementState = InteractiveElementState;
 
-    fn id(&self) -> Option<crate::ElementId> {
-        self.base.id()
+    fn element_id(&self) -> Option<crate::ElementId> {
+        self.interactivity.element_id.clone()
     }
 
     fn initialize(
         &mut self,
-        view_state: &mut V,
+        _view_state: &mut V,
         element_state: Option<Self::ElementState>,
         cx: &mut ViewContext<V>,
     ) -> Self::ElementState {
-        self.base.initialize(view_state, element_state, cx)
+        self.interactivity.initialize(element_state, cx)
     }
 
     fn layout(
         &mut self,
-        view_state: &mut V,
+        _view_state: &mut V,
         element_state: &mut Self::ElementState,
         cx: &mut ViewContext<V>,
     ) -> LayoutId {
-        self.base.layout(view_state, element_state, cx)
+        self.interactivity.layout(element_state, cx, |style, cx| {
+            cx.request_layout(&style, None)
+        })
     }
 
     fn paint(
         &mut self,
         bounds: Bounds<Pixels>,
-        view: &mut V,
+        _view_state: &mut V,
         element_state: &mut Self::ElementState,
         cx: &mut ViewContext<V>,
     ) {
-        cx.with_z_index(0, |cx| {
-            self.base.paint(bounds, view, element_state, cx);
-        });
-
-        let style = self.base.compute_style(bounds, element_state, cx);
-        let corner_radii = style.corner_radii;
-
-        if let Some(uri) = self.uri.clone() {
-            // eprintln!(">>> image_cache.get({uri}");
-            let image_future = cx.image_cache.get(uri.clone());
-            // eprintln!("<<< image_cache.get({uri}");
-            if let Some(data) = image_future
-                .clone()
-                .now_or_never()
-                .and_then(ResultExt::log_err)
-            {
-                let corner_radii = corner_radii.to_pixels(bounds.size, cx.rem_size());
-                cx.with_z_index(1, |cx| {
-                    cx.paint_image(bounds, corner_radii, data, self.grayscale)
-                        .log_err()
-                });
-            } else {
-                cx.spawn(|_, mut cx| async move {
-                    if image_future.await.log_err().is_some() {
-                        cx.on_next_frame(|cx| cx.notify());
+        self.interactivity.paint(
+            bounds,
+            bounds.size,
+            element_state,
+            cx,
+            |style, _scroll_offset, cx| {
+                let corner_radii = style.corner_radii;
+
+                if let Some(uri) = self.uri.clone() {
+                    // eprintln!(">>> image_cache.get({uri}");
+                    let image_future = cx.image_cache.get(uri.clone());
+                    // eprintln!("<<< image_cache.get({uri}");
+                    if let Some(data) = image_future
+                        .clone()
+                        .now_or_never()
+                        .and_then(ResultExt::log_err)
+                    {
+                        let corner_radii = corner_radii.to_pixels(bounds.size, cx.rem_size());
+                        cx.with_z_index(1, |cx| {
+                            cx.paint_image(bounds, corner_radii, data, self.grayscale)
+                                .log_err()
+                        });
+                    } else {
+                        cx.spawn(|_, mut cx| async move {
+                            if image_future.await.log_err().is_some() {
+                                cx.on_next_frame(|cx| cx.notify());
+                            }
+                        })
+                        .detach()
                     }
-                })
-                .detach()
-            }
-        }
+                }
+            },
+        )
     }
 }
 
-impl<V, I, F> Styled for Img<V, I, F>
-where
-    I: ElementInteractivity<V>,
-    F: KeyDispatch<V>,
-{
+impl<V> Styled for Img<V> {
     fn style(&mut self) -> &mut StyleRefinement {
-        self.base.style()
+        &mut self.interactivity.base_style
     }
 }
 
-impl<V, I, F> StatelessInteractive<V> for Img<V, I, F>
-where
-    I: ElementInteractivity<V>,
-    F: KeyDispatch<V>,
-{
-    fn stateless_interactivity(&mut self) -> &mut StatelessInteractivity<V> {
-        self.base.stateless_interactivity()
-    }
-}
-
-impl<V, F> StatefulInteractive<V> for Img<V, StatefulInteractivity<V>, F>
-where
-    F: KeyDispatch<V>,
-{
-    fn stateful_interactivity(&mut self) -> &mut StatefulInteractivity<V> {
-        self.base.stateful_interactivity()
-    }
-}
-
-impl<V, I> Focusable<V> for Img<V, I, FocusableKeyDispatch<V>>
-where
-    V: 'static,
-    I: ElementInteractivity<V>,
-{
-    fn focus_listeners(&mut self) -> &mut FocusListeners<V> {
-        self.base.focus_listeners()
-    }
-
-    fn set_focus_style(&mut self, style: StyleRefinement) {
-        self.base.set_focus_style(style)
-    }
-
-    fn set_focus_in_style(&mut self, style: StyleRefinement) {
-        self.base.set_focus_in_style(style)
-    }
-
-    fn set_in_focus_style(&mut self, style: StyleRefinement) {
-        self.base.set_in_focus_style(style)
+impl<V> InteractiveComponent<V> for Img<V> {
+    fn interactivity(&mut self) -> &mut Interactivity<V> {
+        &mut self.interactivity
     }
 }

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

@@ -1,157 +1,88 @@
 use crate::{
-    div, AnyElement, Bounds, Component, Div, DivState, Element, ElementId, ElementInteractivity,
-    FocusListeners, Focusable, FocusableKeyDispatch, KeyDispatch, LayoutId,
-    NonFocusableKeyDispatch, Pixels, SharedString, StatefulInteractive, StatefulInteractivity,
-    StatelessInteractive, StatelessInteractivity, StyleRefinement, Styled, ViewContext,
+    AnyElement, Bounds, Component, Element, ElementId, InteractiveComponent,
+    InteractiveElementState, Interactivity, LayoutId, Pixels, SharedString, StyleRefinement,
+    Styled, ViewContext,
 };
 use util::ResultExt;
 
-pub struct Svg<
-    V: 'static,
-    I: ElementInteractivity<V> = StatelessInteractivity<V>,
-    F: KeyDispatch<V> = NonFocusableKeyDispatch,
-> {
-    base: Div<V, I, F>,
+pub struct Svg<V: 'static> {
+    interactivity: Interactivity<V>,
     path: Option<SharedString>,
 }
 
-pub fn svg<V: 'static>() -> Svg<V, StatelessInteractivity<V>, NonFocusableKeyDispatch> {
+pub fn svg<V: 'static>() -> Svg<V> {
     Svg {
-        base: div(),
+        interactivity: Interactivity::default(),
         path: None,
     }
 }
 
-impl<V, I, F> Svg<V, I, F>
-where
-    I: ElementInteractivity<V>,
-    F: KeyDispatch<V>,
-{
+impl<V> Svg<V> {
     pub fn path(mut self, path: impl Into<SharedString>) -> Self {
         self.path = Some(path.into());
         self
     }
 }
 
-impl<V, F> Svg<V, StatelessInteractivity<V>, F>
-where
-    F: KeyDispatch<V>,
-{
-    pub fn id(self, id: impl Into<ElementId>) -> Svg<V, StatefulInteractivity<V>, F> {
-        Svg {
-            base: self.base.id(id),
-            path: self.path,
-        }
-    }
-}
-
-impl<V, I, F> Component<V> for Svg<V, I, F>
-where
-    I: ElementInteractivity<V>,
-    F: KeyDispatch<V>,
-{
+impl<V> Component<V> for Svg<V> {
     fn render(self) -> AnyElement<V> {
         AnyElement::new(self)
     }
 }
 
-impl<V, I, F> Element<V> for Svg<V, I, F>
-where
-    I: ElementInteractivity<V>,
-    F: KeyDispatch<V>,
-{
-    type ElementState = DivState;
+impl<V> Element<V> for Svg<V> {
+    type ElementState = InteractiveElementState;
 
-    fn id(&self) -> Option<crate::ElementId> {
-        self.base.id()
+    fn element_id(&self) -> Option<ElementId> {
+        self.interactivity.element_id.clone()
     }
 
     fn initialize(
         &mut self,
-        view_state: &mut V,
+        _view_state: &mut V,
         element_state: Option<Self::ElementState>,
         cx: &mut ViewContext<V>,
     ) -> Self::ElementState {
-        self.base.initialize(view_state, element_state, cx)
+        self.interactivity.initialize(element_state, cx)
     }
 
     fn layout(
         &mut self,
-        view_state: &mut V,
+        _view_state: &mut V,
         element_state: &mut Self::ElementState,
         cx: &mut ViewContext<V>,
     ) -> LayoutId {
-        self.base.layout(view_state, element_state, cx)
+        self.interactivity.layout(element_state, cx, |style, cx| {
+            cx.request_layout(&style, None)
+        })
     }
 
     fn paint(
         &mut self,
         bounds: Bounds<Pixels>,
-        view: &mut V,
+        _view_state: &mut V,
         element_state: &mut Self::ElementState,
         cx: &mut ViewContext<V>,
     ) where
         Self: Sized,
     {
-        self.base.paint(bounds, view, element_state, cx);
-        let color = self
-            .base
-            .compute_style(bounds, element_state, cx)
-            .text
-            .color;
-        if let Some((path, color)) = self.path.as_ref().zip(color) {
-            cx.paint_svg(bounds, path.clone(), color).log_err();
-        }
+        self.interactivity
+            .paint(bounds, bounds.size, element_state, cx, |style, _, cx| {
+                if let Some((path, color)) = self.path.as_ref().zip(style.text.color) {
+                    cx.paint_svg(bounds, path.clone(), color).log_err();
+                }
+            })
     }
 }
 
-impl<V, I, F> Styled for Svg<V, I, F>
-where
-    I: ElementInteractivity<V>,
-    F: KeyDispatch<V>,
-{
+impl<V> Styled for Svg<V> {
     fn style(&mut self) -> &mut StyleRefinement {
-        self.base.style()
-    }
-}
-
-impl<V, I, F> StatelessInteractive<V> for Svg<V, I, F>
-where
-    I: ElementInteractivity<V>,
-    F: KeyDispatch<V>,
-{
-    fn stateless_interactivity(&mut self) -> &mut StatelessInteractivity<V> {
-        self.base.stateless_interactivity()
+        &mut self.interactivity.base_style
     }
 }
 
-impl<V, F> StatefulInteractive<V> for Svg<V, StatefulInteractivity<V>, F>
-where
-    V: 'static,
-    F: KeyDispatch<V>,
-{
-    fn stateful_interactivity(&mut self) -> &mut StatefulInteractivity<V> {
-        self.base.stateful_interactivity()
-    }
-}
-
-impl<V: 'static, I> Focusable<V> for Svg<V, I, FocusableKeyDispatch<V>>
-where
-    I: ElementInteractivity<V>,
-{
-    fn focus_listeners(&mut self) -> &mut FocusListeners<V> {
-        self.base.focus_listeners()
-    }
-
-    fn set_focus_style(&mut self, style: StyleRefinement) {
-        self.base.set_focus_style(style)
-    }
-
-    fn set_focus_in_style(&mut self, style: StyleRefinement) {
-        self.base.set_focus_in_style(style)
-    }
-
-    fn set_in_focus_style(&mut self, style: StyleRefinement) {
-        self.base.set_in_focus_style(style)
+impl<V> InteractiveComponent<V> for Svg<V> {
+    fn interactivity(&mut self) -> &mut Interactivity<V> {
+        &mut self.interactivity
     }
 }

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

@@ -72,7 +72,7 @@ impl<V: 'static> Component<V> for Text<V> {
 impl<V: 'static> Element<V> for Text<V> {
     type ElementState = Arc<Mutex<Option<TextElementState>>>;
 
-    fn id(&self) -> Option<crate::ElementId> {
+    fn element_id(&self) -> Option<crate::ElementId> {
         None
     }
 

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

@@ -1,24 +1,23 @@
 use crate::{
     point, px, size, AnyElement, AvailableSpace, BorrowWindow, Bounds, Component, Element,
-    ElementId, ElementInteractivity, InteractiveElementState, LayoutId, Pixels, Point, Size,
-    StatefulInteractive, StatefulInteractivity, StatelessInteractive, StatelessInteractivity,
-    StyleRefinement, Styled, ViewContext,
+    ElementId, InteractiveComponent, InteractiveElementState, Interactivity, LayoutId, Pixels,
+    Point, Size, StyleRefinement, Styled, ViewContext,
 };
 use parking_lot::Mutex;
 use smallvec::SmallVec;
-use std::{cmp, ops::Range, sync::Arc};
+use std::{cmp, mem, ops::Range, sync::Arc};
 use taffy::style::Overflow;
 
 /// uniform_list provides lazy rendering for a set of items that are of uniform height.
 /// When rendered into a container with overflow-y: hidden and a fixed (or max) height,
 /// uniform_list will only render the visibile subset of items.
-pub fn uniform_list<Id, V, C>(
-    id: Id,
+pub fn uniform_list<I, V, C>(
+    id: I,
     item_count: usize,
-    f: impl 'static + Fn(&mut V, Range<usize>, &mut ViewContext<V>) -> SmallVec<[C; 64]>,
+    f: impl 'static + Fn(&mut V, Range<usize>, &mut ViewContext<V>) -> Vec<C>,
 ) -> UniformList<V>
 where
-    Id: Into<ElementId>,
+    I: Into<ElementId>,
     V: 'static,
     C: Component<V>,
 {
@@ -37,7 +36,10 @@ where
                 .map(|component| component.render())
                 .collect()
         }),
-        interactivity: StatefulInteractivity::new(id, StatelessInteractivity::default()),
+        interactivity: Interactivity {
+            element_id: Some(id.into()),
+            ..Default::default()
+        },
         scroll_handle: None,
     }
 }
@@ -54,7 +56,7 @@ pub struct UniformList<V: 'static> {
             &'a mut ViewContext<V>,
         ) -> SmallVec<[AnyElement<V>; 64]>,
     >,
-    interactivity: StatefulInteractivity<V>,
+    interactivity: Interactivity<V>,
     scroll_handle: Option<UniformListScrollHandle>,
 }
 
@@ -103,7 +105,7 @@ pub struct UniformListState {
 impl<V: 'static> Element<V> for UniformList<V> {
     type ElementState = UniformListState;
 
-    fn id(&self) -> Option<crate::ElementId> {
+    fn element_id(&self) -> Option<crate::ElementId> {
         Some(self.id.clone())
     }
 
@@ -113,13 +115,18 @@ impl<V: 'static> Element<V> for UniformList<V> {
         element_state: Option<Self::ElementState>,
         cx: &mut ViewContext<V>,
     ) -> Self::ElementState {
-        element_state.unwrap_or_else(|| {
+        if let Some(mut element_state) = element_state {
+            element_state.interactive = self
+                .interactivity
+                .initialize(Some(element_state.interactive), cx);
+            element_state
+        } else {
             let item_size = self.measure_item(view_state, None, cx);
             UniformListState {
-                interactive: InteractiveElementState::default(),
+                interactive: self.interactivity.initialize(None, cx),
                 item_size,
             }
-        })
+        }
     }
 
     fn layout(
@@ -132,35 +139,44 @@ impl<V: 'static> Element<V> for UniformList<V> {
         let item_size = element_state.item_size;
         let rem_size = cx.rem_size();
 
-        cx.request_measured_layout(
-            self.computed_style(),
-            rem_size,
-            move |known_dimensions: Size<Option<Pixels>>, available_space: Size<AvailableSpace>| {
-                let desired_height = item_size.height * max_items;
-                let width = known_dimensions
-                    .width
-                    .unwrap_or(match available_space.width {
-                        AvailableSpace::Definite(x) => x,
-                        AvailableSpace::MinContent | AvailableSpace::MaxContent => item_size.width,
-                    });
-                let height = match available_space.height {
-                    AvailableSpace::Definite(x) => desired_height.min(x),
-                    AvailableSpace::MinContent | AvailableSpace::MaxContent => desired_height,
-                };
-                size(width, height)
-            },
-        )
+        self.interactivity
+            .layout(&mut element_state.interactive, cx, |style, cx| {
+                cx.request_measured_layout(
+                    style,
+                    rem_size,
+                    move |known_dimensions: Size<Option<Pixels>>,
+                          available_space: Size<AvailableSpace>| {
+                        let desired_height = item_size.height * max_items;
+                        let width = known_dimensions
+                            .width
+                            .unwrap_or(match available_space.width {
+                                AvailableSpace::Definite(x) => x,
+                                AvailableSpace::MinContent | AvailableSpace::MaxContent => {
+                                    item_size.width
+                                }
+                            });
+                        let height = match available_space.height {
+                            AvailableSpace::Definite(x) => desired_height.min(x),
+                            AvailableSpace::MinContent | AvailableSpace::MaxContent => {
+                                desired_height
+                            }
+                        };
+                        size(width, height)
+                    },
+                )
+            })
     }
 
     fn paint(
         &mut self,
-        bounds: crate::Bounds<crate::Pixels>,
+        bounds: Bounds<crate::Pixels>,
         view_state: &mut V,
         element_state: &mut Self::ElementState,
         cx: &mut ViewContext<V>,
     ) {
-        let style = self.computed_style();
-
+        let style =
+            self.interactivity
+                .compute_style(Some(bounds), &mut element_state.interactive, cx);
         let border = style.border_widths.to_pixels(cx.rem_size());
         let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size());
 
@@ -170,74 +186,79 @@ impl<V: 'static> Element<V> for UniformList<V> {
                 - point(border.right + padding.right, border.bottom + padding.bottom),
         );
 
-        cx.with_z_index(style.z_index.unwrap_or(0), |cx| {
-            style.paint(bounds, cx);
+        let item_size = element_state.item_size;
+        let content_size = Size {
+            width: padded_bounds.size.width,
+            height: item_size.height * self.item_count,
+        };
 
-            let content_size;
-            if self.item_count > 0 {
-                let item_height = self
-                    .measure_item(view_state, Some(padded_bounds.size.width), cx)
-                    .height;
-                if let Some(scroll_handle) = self.scroll_handle.clone() {
-                    scroll_handle.0.lock().replace(ScrollHandleState {
-                        item_height,
-                        list_height: padded_bounds.size.height,
-                        scroll_offset: element_state.interactive.track_scroll_offset(),
-                    });
-                }
-                let visible_item_count = if item_height > px(0.) {
-                    (padded_bounds.size.height / item_height).ceil() as usize + 1
-                } else {
-                    0
-                };
-                let scroll_offset = element_state
-                    .interactive
-                    .scroll_offset()
-                    .map_or((0.0).into(), |offset| offset.y);
-                let first_visible_element_ix = (-scroll_offset / item_height).floor() as usize;
-                let visible_range = first_visible_element_ix
-                    ..cmp::min(
-                        first_visible_element_ix + visible_item_count,
-                        self.item_count,
-                    );
+        let mut interactivity = mem::take(&mut self.interactivity);
+        let shared_scroll_offset = element_state
+            .interactive
+            .scroll_offset
+            .get_or_insert_with(Arc::default)
+            .clone();
 
-                let mut items = (self.render_items)(view_state, visible_range.clone(), cx);
+        interactivity.paint(
+            bounds,
+            content_size,
+            &mut element_state.interactive,
+            cx,
+            |style, scroll_offset, cx| {
+                let border = style.border_widths.to_pixels(cx.rem_size());
+                let padding = style.padding.to_pixels(bounds.size.into(), cx.rem_size());
 
-                content_size = Size {
-                    width: padded_bounds.size.width,
-                    height: item_height * self.item_count,
-                };
+                let padded_bounds = Bounds::from_corners(
+                    bounds.origin + point(border.left + padding.left, border.top + padding.top),
+                    bounds.lower_right()
+                        - point(border.right + padding.right, border.bottom + padding.bottom),
+                );
 
-                cx.with_z_index(1, |cx| {
-                    for (item, ix) in items.iter_mut().zip(visible_range) {
-                        let item_origin =
-                            padded_bounds.origin + point(px(0.), item_height * ix + scroll_offset);
-                        let available_space = size(
-                            AvailableSpace::Definite(padded_bounds.size.width),
-                            AvailableSpace::Definite(item_height),
-                        );
-                        item.draw(item_origin, available_space, view_state, cx);
-                    }
-                });
-            } else {
-                content_size = Size {
-                    width: bounds.size.width,
-                    height: px(0.),
-                };
-            }
+                cx.with_z_index(style.z_index.unwrap_or(0), |cx| {
+                    style.paint(bounds, cx);
 
-            let overflow = point(style.overflow.x, Overflow::Scroll);
+                    if self.item_count > 0 {
+                        let item_height = self
+                            .measure_item(view_state, Some(padded_bounds.size.width), cx)
+                            .height;
+                        if let Some(scroll_handle) = self.scroll_handle.clone() {
+                            scroll_handle.0.lock().replace(ScrollHandleState {
+                                item_height,
+                                list_height: padded_bounds.size.height,
+                                scroll_offset: shared_scroll_offset,
+                            });
+                        }
+                        let visible_item_count = if item_height > px(0.) {
+                            (padded_bounds.size.height / item_height).ceil() as usize + 1
+                        } else {
+                            0
+                        };
 
-            cx.with_z_index(0, |cx| {
-                self.interactivity.paint(
-                    bounds,
-                    content_size,
-                    overflow,
-                    &mut element_state.interactive,
-                    cx,
-                );
-            });
-        })
+                        let first_visible_element_ix =
+                            (-scroll_offset.y / item_height).floor() as usize;
+                        let visible_range = first_visible_element_ix
+                            ..cmp::min(
+                                first_visible_element_ix + visible_item_count,
+                                self.item_count,
+                            );
+
+                        let mut items = (self.render_items)(view_state, visible_range.clone(), cx);
+                        cx.with_z_index(1, |cx| {
+                            for (item, ix) in items.iter_mut().zip(visible_range) {
+                                let item_origin = padded_bounds.origin
+                                    + point(px(0.), item_height * ix + scroll_offset.y);
+                                let available_space = size(
+                                    AvailableSpace::Definite(padded_bounds.size.width),
+                                    AvailableSpace::Definite(item_height),
+                                );
+                                item.draw(item_origin, available_space, view_state, cx);
+                            }
+                        });
+                    }
+                })
+            },
+        );
+        self.interactivity = interactivity;
     }
 }
 
@@ -275,14 +296,8 @@ impl<V> UniformList<V> {
     }
 }
 
-impl<V: 'static> StatelessInteractive<V> for UniformList<V> {
-    fn stateless_interactivity(&mut self) -> &mut StatelessInteractivity<V> {
-        self.interactivity.as_stateless_mut()
-    }
-}
-
-impl<V: 'static> StatefulInteractive<V> for UniformList<V> {
-    fn stateful_interactivity(&mut self) -> &mut StatefulInteractivity<V> {
+impl<V> InteractiveComponent<V> for UniformList<V> {
+    fn interactivity(&mut self) -> &mut crate::Interactivity<V> {
         &mut self.interactivity
     }
 }

crates/gpui2/src/gpui2.rs 🔗

@@ -156,7 +156,7 @@ pub enum GlobalKey {
 }
 
 pub trait BorrowAppContext {
-    fn with_text_style<F, R>(&mut self, style: TextStyleRefinement, f: F) -> R
+    fn with_text_style<F, R>(&mut self, style: Option<TextStyleRefinement>, f: F) -> R
     where
         F: FnOnce(&mut Self) -> R;
 
@@ -167,14 +167,18 @@ impl<C> BorrowAppContext for C
 where
     C: BorrowMut<AppContext>,
 {
-    fn with_text_style<F, R>(&mut self, style: TextStyleRefinement, f: F) -> R
+    fn with_text_style<F, R>(&mut self, style: Option<TextStyleRefinement>, f: F) -> R
     where
         F: FnOnce(&mut Self) -> R,
     {
-        self.borrow_mut().push_text_style(style);
-        let result = f(self);
-        self.borrow_mut().pop_text_style();
-        result
+        if let Some(style) = style {
+            self.borrow_mut().push_text_style(style);
+            let result = f(self);
+            self.borrow_mut().pop_text_style();
+            result
+        } else {
+            f(self)
+        }
     }
 
     fn set_global<G: 'static>(&mut self, global: G) {

crates/gpui2/src/interactive.rs 🔗

@@ -1,944 +1,9 @@
 use crate::{
-    div, point, px, Action, AnyDrag, AnyTooltip, AnyView, AppContext, Bounds, Component,
-    DispatchPhase, Div, Element, ElementId, FocusHandle, KeyContext, Keystroke, Modifiers,
-    Overflow, Pixels, Point, Render, SharedString, Size, Style, StyleRefinement, Task, View,
+    div, point, Component, Div, FocusHandle, Keystroke, Modifiers, Pixels, Point, Render,
     ViewContext,
 };
-use collections::HashMap;
-use derive_more::{Deref, DerefMut};
-use parking_lot::Mutex;
-use refineable::Refineable;
 use smallvec::SmallVec;
-use std::{
-    any::{Any, TypeId},
-    fmt::Debug,
-    marker::PhantomData,
-    mem,
-    ops::Deref,
-    path::PathBuf,
-    sync::Arc,
-    time::Duration,
-};
-
-const DRAG_THRESHOLD: f64 = 2.;
-const TOOLTIP_DELAY: Duration = Duration::from_millis(500);
-const TOOLTIP_OFFSET: Point<Pixels> = Point::new(px(10.0), px(8.0));
-
-pub trait StatelessInteractive<V: 'static>: Element<V> {
-    fn stateless_interactivity(&mut self) -> &mut StatelessInteractivity<V>;
-
-    fn hover(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
-    where
-        Self: Sized,
-    {
-        self.stateless_interactivity().hover_style = f(StyleRefinement::default());
-        self
-    }
-
-    fn group_hover(
-        mut self,
-        group_name: impl Into<SharedString>,
-        f: impl FnOnce(StyleRefinement) -> StyleRefinement,
-    ) -> Self
-    where
-        Self: Sized,
-    {
-        self.stateless_interactivity().group_hover_style = Some(GroupStyle {
-            group: group_name.into(),
-            style: f(StyleRefinement::default()),
-        });
-        self
-    }
-
-    fn on_mouse_down(
-        mut self,
-        button: MouseButton,
-        handler: impl Fn(&mut V, &MouseDownEvent, &mut ViewContext<V>) + 'static,
-    ) -> Self
-    where
-        Self: Sized,
-    {
-        self.stateless_interactivity()
-            .mouse_down_listeners
-            .push(Box::new(move |view, event, bounds, phase, cx| {
-                if phase == DispatchPhase::Bubble
-                    && event.button == button
-                    && bounds.contains_point(&event.position)
-                {
-                    handler(view, event, cx)
-                }
-            }));
-        self
-    }
-
-    fn on_any_mouse_down(
-        mut self,
-        handler: impl Fn(&mut V, &MouseDownEvent, &mut ViewContext<V>) + 'static,
-    ) -> Self
-    where
-        Self: Sized,
-    {
-        self.stateless_interactivity()
-            .mouse_down_listeners
-            .push(Box::new(move |view, event, bounds, phase, cx| {
-                if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
-                    handler(view, event, cx)
-                }
-            }));
-        self
-    }
-
-    fn on_any_mouse_up(
-        mut self,
-        handler: impl Fn(&mut V, &MouseUpEvent, &mut ViewContext<V>) + 'static,
-    ) -> Self
-    where
-        Self: Sized,
-    {
-        self.stateless_interactivity()
-            .mouse_up_listeners
-            .push(Box::new(move |view, event, bounds, phase, cx| {
-                if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
-                    handler(view, event, cx)
-                }
-            }));
-        self
-    }
-
-    fn on_mouse_up(
-        mut self,
-        button: MouseButton,
-        handler: impl Fn(&mut V, &MouseUpEvent, &mut ViewContext<V>) + 'static,
-    ) -> Self
-    where
-        Self: Sized,
-    {
-        self.stateless_interactivity()
-            .mouse_up_listeners
-            .push(Box::new(move |view, event, bounds, phase, cx| {
-                if phase == DispatchPhase::Bubble
-                    && event.button == button
-                    && bounds.contains_point(&event.position)
-                {
-                    handler(view, event, cx)
-                }
-            }));
-        self
-    }
-
-    fn on_mouse_down_out(
-        mut self,
-        handler: impl Fn(&mut V, &MouseDownEvent, &mut ViewContext<V>) + 'static,
-    ) -> Self
-    where
-        Self: Sized,
-    {
-        self.stateless_interactivity()
-            .mouse_down_listeners
-            .push(Box::new(move |view, event, bounds, phase, cx| {
-                if phase == DispatchPhase::Capture && !bounds.contains_point(&event.position) {
-                    handler(view, event, cx)
-                }
-            }));
-        self
-    }
-
-    fn on_mouse_up_out(
-        mut self,
-        handler: impl Fn(&mut V, &MouseUpEvent, &mut ViewContext<V>) + 'static,
-    ) -> Self
-    where
-        Self: Sized,
-    {
-        self.stateless_interactivity()
-            .mouse_up_listeners
-            .push(Box::new(move |view, event, bounds, phase, cx| {
-                if phase == DispatchPhase::Capture && !bounds.contains_point(&event.position) {
-                    handler(view, event, cx);
-                }
-            }));
-        self
-    }
-
-    fn on_mouse_move(
-        mut self,
-        handler: impl Fn(&mut V, &MouseMoveEvent, &mut ViewContext<V>) + 'static,
-    ) -> Self
-    where
-        Self: Sized,
-    {
-        self.stateless_interactivity()
-            .mouse_move_listeners
-            .push(Box::new(move |view, event, bounds, phase, cx| {
-                if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
-                    handler(view, event, cx);
-                }
-            }));
-        self
-    }
-
-    fn on_scroll_wheel(
-        mut self,
-        handler: impl Fn(&mut V, &ScrollWheelEvent, &mut ViewContext<V>) + 'static,
-    ) -> Self
-    where
-        Self: Sized,
-    {
-        self.stateless_interactivity()
-            .scroll_wheel_listeners
-            .push(Box::new(move |view, event, bounds, phase, cx| {
-                if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
-                    handler(view, event, cx);
-                }
-            }));
-        self
-    }
-
-    /// Capture the given action, fires during the capture phase
-    fn capture_action<A: Action>(
-        mut self,
-        listener: impl Fn(&mut V, &A, &mut ViewContext<V>) + 'static,
-    ) -> Self
-    where
-        Self: Sized,
-    {
-        self.stateless_interactivity().action_listeners.push((
-            TypeId::of::<A>(),
-            Box::new(move |view, action, phase, cx| {
-                let action = action.downcast_ref().unwrap();
-                if phase == DispatchPhase::Capture {
-                    listener(view, action, cx)
-                }
-            }),
-        ));
-        self
-    }
-
-    /// Add a listener for the given action, fires during the bubble event phase
-    fn on_action<A: Action>(
-        mut self,
-        listener: impl Fn(&mut V, &A, &mut ViewContext<V>) + 'static,
-    ) -> Self
-    where
-        Self: Sized,
-    {
-        self.stateless_interactivity().action_listeners.push((
-            TypeId::of::<A>(),
-            Box::new(move |view, action, phase, cx| {
-                let action = action.downcast_ref().unwrap();
-                if phase == DispatchPhase::Bubble {
-                    listener(view, action, cx)
-                }
-            }),
-        ));
-        self
-    }
-
-    fn on_key_down(
-        mut self,
-        listener: impl Fn(&mut V, &KeyDownEvent, DispatchPhase, &mut ViewContext<V>) + 'static,
-    ) -> Self
-    where
-        Self: Sized,
-    {
-        self.stateless_interactivity()
-            .key_down_listeners
-            .push(Box::new(move |view, event, phase, cx| {
-                listener(view, event, phase, cx)
-            }));
-        self
-    }
-
-    fn on_key_up(
-        mut self,
-        listener: impl Fn(&mut V, &KeyUpEvent, DispatchPhase, &mut ViewContext<V>) + 'static,
-    ) -> Self
-    where
-        Self: Sized,
-    {
-        self.stateless_interactivity()
-            .key_up_listeners
-            .push(Box::new(move |view, event, phase, cx| {
-                listener(view, event, phase, cx)
-            }));
-        self
-    }
-
-    fn drag_over<S: 'static>(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
-    where
-        Self: Sized,
-    {
-        self.stateless_interactivity()
-            .drag_over_styles
-            .push((TypeId::of::<S>(), f(StyleRefinement::default())));
-        self
-    }
-
-    fn group_drag_over<S: 'static>(
-        mut self,
-        group_name: impl Into<SharedString>,
-        f: impl FnOnce(StyleRefinement) -> StyleRefinement,
-    ) -> Self
-    where
-        Self: Sized,
-    {
-        self.stateless_interactivity().group_drag_over_styles.push((
-            TypeId::of::<S>(),
-            GroupStyle {
-                group: group_name.into(),
-                style: f(StyleRefinement::default()),
-            },
-        ));
-        self
-    }
-
-    fn on_drop<W: 'static>(
-        mut self,
-        listener: impl Fn(&mut V, View<W>, &mut ViewContext<V>) + 'static,
-    ) -> Self
-    where
-        Self: Sized,
-    {
-        self.stateless_interactivity().drop_listeners.push((
-            TypeId::of::<W>(),
-            Box::new(move |view, dragged_view, cx| {
-                listener(view, dragged_view.downcast().unwrap(), cx);
-            }),
-        ));
-        self
-    }
-}
-
-pub trait StatefulInteractive<V: 'static>: StatelessInteractive<V> {
-    fn stateful_interactivity(&mut self) -> &mut StatefulInteractivity<V>;
-
-    fn active(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
-    where
-        Self: Sized,
-    {
-        self.stateful_interactivity().active_style = f(StyleRefinement::default());
-        self
-    }
-
-    fn group_active(
-        mut self,
-        group_name: impl Into<SharedString>,
-        f: impl FnOnce(StyleRefinement) -> StyleRefinement,
-    ) -> Self
-    where
-        Self: Sized,
-    {
-        self.stateful_interactivity().group_active_style = Some(GroupStyle {
-            group: group_name.into(),
-            style: f(StyleRefinement::default()),
-        });
-        self
-    }
-
-    fn on_click(
-        mut self,
-        listener: impl Fn(&mut V, &ClickEvent, &mut ViewContext<V>) + 'static,
-    ) -> Self
-    where
-        Self: Sized,
-    {
-        self.stateful_interactivity()
-            .click_listeners
-            .push(Box::new(move |view, event, cx| listener(view, event, cx)));
-        self
-    }
-
-    fn on_drag<W>(
-        mut self,
-        listener: impl Fn(&mut V, &mut ViewContext<V>) -> View<W> + 'static,
-    ) -> Self
-    where
-        Self: Sized,
-        W: 'static + Render,
-    {
-        debug_assert!(
-            self.stateful_interactivity().drag_listener.is_none(),
-            "calling on_drag more than once on the same element is not supported"
-        );
-        self.stateful_interactivity().drag_listener =
-            Some(Box::new(move |view_state, cursor_offset, cx| AnyDrag {
-                view: listener(view_state, cx).into(),
-                cursor_offset,
-            }));
-        self
-    }
-
-    fn on_hover(mut self, listener: impl 'static + Fn(&mut V, bool, &mut ViewContext<V>)) -> Self
-    where
-        Self: Sized,
-    {
-        debug_assert!(
-            self.stateful_interactivity().hover_listener.is_none(),
-            "calling on_hover more than once on the same element is not supported"
-        );
-        self.stateful_interactivity().hover_listener = Some(Box::new(listener));
-        self
-    }
-
-    fn tooltip<W>(
-        mut self,
-        build_tooltip: impl Fn(&mut V, &mut ViewContext<V>) -> View<W> + 'static,
-    ) -> Self
-    where
-        Self: Sized,
-        W: 'static + Render,
-    {
-        debug_assert!(
-            self.stateful_interactivity().tooltip_builder.is_none(),
-            "calling tooltip more than once on the same element is not supported"
-        );
-        self.stateful_interactivity().tooltip_builder = Some(Arc::new(move |view_state, cx| {
-            build_tooltip(view_state, cx).into()
-        }));
-
-        self
-    }
-}
-
-pub trait ElementInteractivity<V: 'static>: 'static {
-    fn as_stateless(&self) -> &StatelessInteractivity<V>;
-    fn as_stateless_mut(&mut self) -> &mut StatelessInteractivity<V>;
-    fn as_stateful(&self) -> Option<&StatefulInteractivity<V>>;
-    fn as_stateful_mut(&mut self) -> Option<&mut StatefulInteractivity<V>>;
-
-    fn refine_style(
-        &self,
-        style: &mut Style,
-        bounds: Bounds<Pixels>,
-        element_state: &InteractiveElementState,
-        cx: &mut ViewContext<V>,
-    ) {
-        let mouse_position = cx.mouse_position();
-        let stateless = self.as_stateless();
-        if let Some(group_hover) = stateless.group_hover_style.as_ref() {
-            if let Some(group_bounds) = GroupBounds::get(&group_hover.group, cx) {
-                if group_bounds.contains_point(&mouse_position) {
-                    style.refine(&group_hover.style);
-                }
-            }
-        }
-        if bounds.contains_point(&mouse_position) {
-            style.refine(&stateless.hover_style);
-        }
-
-        if let Some(drag) = cx.active_drag.take() {
-            for (state_type, group_drag_style) in &self.as_stateless().group_drag_over_styles {
-                if let Some(group_bounds) = GroupBounds::get(&group_drag_style.group, cx) {
-                    if *state_type == drag.view.entity_type()
-                        && group_bounds.contains_point(&mouse_position)
-                    {
-                        style.refine(&group_drag_style.style);
-                    }
-                }
-            }
-
-            for (state_type, drag_over_style) in &self.as_stateless().drag_over_styles {
-                if *state_type == drag.view.entity_type() && bounds.contains_point(&mouse_position)
-                {
-                    style.refine(drag_over_style);
-                }
-            }
-
-            cx.active_drag = Some(drag);
-        }
-
-        if let Some(stateful) = self.as_stateful() {
-            let active_state = element_state.active_state.lock();
-            if active_state.group {
-                if let Some(group_style) = stateful.group_active_style.as_ref() {
-                    style.refine(&group_style.style);
-                }
-            }
-            if active_state.element {
-                style.refine(&stateful.active_style);
-            }
-        }
-    }
-
-    fn initialize(&mut self, cx: &mut ViewContext<V>) {
-        let stateless = self.as_stateless_mut();
-
-        for listener in stateless.key_down_listeners.drain(..) {
-            cx.on_key_event(move |state, event: &KeyDownEvent, phase, cx| {
-                listener(state, event, phase, cx);
-            })
-        }
-
-        for listener in stateless.key_up_listeners.drain(..) {
-            cx.on_key_event(move |state, event: &KeyUpEvent, phase, cx| {
-                listener(state, event, phase, cx);
-            })
-        }
-
-        for (action_type, listener) in stateless.action_listeners.drain(..) {
-            cx.on_action(action_type, listener)
-        }
-    }
-
-    fn paint(
-        &mut self,
-        bounds: Bounds<Pixels>,
-        content_size: Size<Pixels>,
-        overflow: Point<Overflow>,
-        element_state: &mut InteractiveElementState,
-        cx: &mut ViewContext<V>,
-    ) {
-        let stateless = self.as_stateless_mut();
-        for listener in stateless.mouse_down_listeners.drain(..) {
-            cx.on_mouse_event(move |state, event: &MouseDownEvent, phase, cx| {
-                listener(state, event, &bounds, phase, cx);
-            })
-        }
-
-        for listener in stateless.mouse_up_listeners.drain(..) {
-            cx.on_mouse_event(move |state, event: &MouseUpEvent, phase, cx| {
-                listener(state, event, &bounds, phase, cx);
-            })
-        }
-
-        for listener in stateless.mouse_move_listeners.drain(..) {
-            cx.on_mouse_event(move |state, event: &MouseMoveEvent, phase, cx| {
-                listener(state, event, &bounds, phase, cx);
-            })
-        }
-
-        for listener in stateless.scroll_wheel_listeners.drain(..) {
-            cx.on_mouse_event(move |state, event: &ScrollWheelEvent, phase, cx| {
-                listener(state, event, &bounds, phase, cx);
-            })
-        }
-
-        let hover_group_bounds = stateless
-            .group_hover_style
-            .as_ref()
-            .and_then(|group_hover| GroupBounds::get(&group_hover.group, cx));
-
-        if let Some(group_bounds) = hover_group_bounds {
-            let hovered = group_bounds.contains_point(&cx.mouse_position());
-            cx.on_mouse_event(move |_, event: &MouseMoveEvent, phase, cx| {
-                if phase == DispatchPhase::Capture {
-                    if group_bounds.contains_point(&event.position) != hovered {
-                        cx.notify();
-                    }
-                }
-            });
-        }
-
-        if stateless.hover_style.is_some()
-            || (cx.active_drag.is_some() && !stateless.drag_over_styles.is_empty())
-        {
-            let hovered = bounds.contains_point(&cx.mouse_position());
-            cx.on_mouse_event(move |_, event: &MouseMoveEvent, phase, cx| {
-                if phase == DispatchPhase::Capture {
-                    if bounds.contains_point(&event.position) != hovered {
-                        cx.notify();
-                    }
-                }
-            });
-        }
-
-        if cx.active_drag.is_some() {
-            let drop_listeners = mem::take(&mut stateless.drop_listeners);
-            cx.on_mouse_event(move |view, event: &MouseUpEvent, phase, cx| {
-                if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
-                    if let Some(drag_state_type) =
-                        cx.active_drag.as_ref().map(|drag| drag.view.entity_type())
-                    {
-                        for (drop_state_type, listener) in &drop_listeners {
-                            if *drop_state_type == drag_state_type {
-                                let drag = cx
-                                    .active_drag
-                                    .take()
-                                    .expect("checked for type drag state type above");
-                                listener(view, drag.view.clone(), cx);
-                                cx.notify();
-                                cx.stop_propagation();
-                            }
-                        }
-                    }
-                }
-            });
-        }
-
-        if let Some(stateful) = self.as_stateful_mut() {
-            let click_listeners = mem::take(&mut stateful.click_listeners);
-            let drag_listener = mem::take(&mut stateful.drag_listener);
-
-            if !click_listeners.is_empty() || drag_listener.is_some() {
-                let pending_mouse_down = element_state.pending_mouse_down.clone();
-                let mouse_down = pending_mouse_down.lock().clone();
-                if let Some(mouse_down) = mouse_down {
-                    if let Some(drag_listener) = drag_listener {
-                        let active_state = element_state.active_state.clone();
-
-                        cx.on_mouse_event(move |view_state, event: &MouseMoveEvent, phase, cx| {
-                            if cx.active_drag.is_some() {
-                                if phase == DispatchPhase::Capture {
-                                    cx.notify();
-                                }
-                            } else if phase == DispatchPhase::Bubble
-                                && bounds.contains_point(&event.position)
-                                && (event.position - mouse_down.position).magnitude()
-                                    > DRAG_THRESHOLD
-                            {
-                                *active_state.lock() = ActiveState::default();
-                                let cursor_offset = event.position - bounds.origin;
-                                let drag = drag_listener(view_state, cursor_offset, cx);
-                                cx.active_drag = Some(drag);
-                                cx.notify();
-                                cx.stop_propagation();
-                            }
-                        });
-                    }
-
-                    cx.on_mouse_event(move |view_state, event: &MouseUpEvent, phase, cx| {
-                        if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position)
-                        {
-                            let mouse_click = ClickEvent {
-                                down: mouse_down.clone(),
-                                up: event.clone(),
-                            };
-                            for listener in &click_listeners {
-                                listener(view_state, &mouse_click, cx);
-                            }
-                        }
-                        *pending_mouse_down.lock() = None;
-                    });
-                } else {
-                    cx.on_mouse_event(move |_state, event: &MouseDownEvent, phase, _cx| {
-                        if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position)
-                        {
-                            *pending_mouse_down.lock() = Some(event.clone());
-                        }
-                    });
-                }
-            }
-
-            if let Some(hover_listener) = stateful.hover_listener.take() {
-                let was_hovered = element_state.hover_state.clone();
-                let has_mouse_down = element_state.pending_mouse_down.clone();
-
-                cx.on_mouse_event(move |view_state, event: &MouseMoveEvent, phase, cx| {
-                    if phase != DispatchPhase::Bubble {
-                        return;
-                    }
-                    let is_hovered =
-                        bounds.contains_point(&event.position) && has_mouse_down.lock().is_none();
-                    let mut was_hovered = was_hovered.lock();
-
-                    if is_hovered != was_hovered.clone() {
-                        *was_hovered = is_hovered;
-                        drop(was_hovered);
-
-                        hover_listener(view_state, is_hovered, cx);
-                    }
-                });
-            }
-
-            if let Some(tooltip_builder) = stateful.tooltip_builder.take() {
-                let active_tooltip = element_state.active_tooltip.clone();
-                let pending_mouse_down = element_state.pending_mouse_down.clone();
-
-                cx.on_mouse_event(move |_, event: &MouseMoveEvent, phase, cx| {
-                    if phase != DispatchPhase::Bubble {
-                        return;
-                    }
-
-                    let is_hovered = bounds.contains_point(&event.position)
-                        && pending_mouse_down.lock().is_none();
-                    if !is_hovered {
-                        active_tooltip.lock().take();
-                        return;
-                    }
-
-                    if active_tooltip.lock().is_none() {
-                        let task = cx.spawn({
-                            let active_tooltip = active_tooltip.clone();
-                            let tooltip_builder = tooltip_builder.clone();
-
-                            move |view, mut cx| async move {
-                                cx.background_executor().timer(TOOLTIP_DELAY).await;
-                                view.update(&mut cx, move |view_state, cx| {
-                                    active_tooltip.lock().replace(ActiveTooltip {
-                                        waiting: None,
-                                        tooltip: Some(AnyTooltip {
-                                            view: tooltip_builder(view_state, cx),
-                                            cursor_offset: cx.mouse_position() + TOOLTIP_OFFSET,
-                                        }),
-                                    });
-                                    cx.notify();
-                                })
-                                .ok();
-                            }
-                        });
-                        active_tooltip.lock().replace(ActiveTooltip {
-                            waiting: Some(task),
-                            tooltip: None,
-                        });
-                    }
-                });
-
-                if let Some(active_tooltip) = element_state.active_tooltip.lock().as_ref() {
-                    if active_tooltip.tooltip.is_some() {
-                        cx.active_tooltip = active_tooltip.tooltip.clone()
-                    }
-                }
-            }
-
-            let active_state = element_state.active_state.clone();
-            if active_state.lock().is_none() {
-                let active_group_bounds = stateful
-                    .group_active_style
-                    .as_ref()
-                    .and_then(|group_active| GroupBounds::get(&group_active.group, cx));
-                cx.on_mouse_event(move |_view, down: &MouseDownEvent, phase, cx| {
-                    if phase == DispatchPhase::Bubble {
-                        let group = active_group_bounds
-                            .map_or(false, |bounds| bounds.contains_point(&down.position));
-                        let element = bounds.contains_point(&down.position);
-                        if group || element {
-                            *active_state.lock() = ActiveState { group, element };
-                            cx.notify();
-                        }
-                    }
-                });
-            } else {
-                cx.on_mouse_event(move |_, _: &MouseUpEvent, phase, cx| {
-                    if phase == DispatchPhase::Capture {
-                        *active_state.lock() = ActiveState::default();
-                        cx.notify();
-                    }
-                });
-            }
-
-            if overflow.x == Overflow::Scroll || overflow.y == Overflow::Scroll {
-                let scroll_offset = element_state
-                    .scroll_offset
-                    .get_or_insert_with(Arc::default)
-                    .clone();
-                let line_height = cx.line_height();
-                let scroll_max = (content_size - bounds.size).max(&Size::default());
-
-                cx.on_mouse_event(move |_, event: &ScrollWheelEvent, phase, cx| {
-                    if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
-                        let mut scroll_offset = scroll_offset.lock();
-                        let old_scroll_offset = *scroll_offset;
-                        let delta = event.delta.pixel_delta(line_height);
-
-                        if overflow.x == Overflow::Scroll {
-                            scroll_offset.x =
-                                (scroll_offset.x + delta.x).clamp(-scroll_max.width, px(0.));
-                        }
-
-                        if overflow.y == Overflow::Scroll {
-                            scroll_offset.y =
-                                (scroll_offset.y + delta.y).clamp(-scroll_max.height, px(0.));
-                        }
-
-                        if *scroll_offset != old_scroll_offset {
-                            cx.notify();
-                            cx.stop_propagation();
-                        }
-                    }
-                });
-            }
-        }
-    }
-}
-
-#[derive(Deref, DerefMut)]
-pub struct StatefulInteractivity<V> {
-    pub id: ElementId,
-    #[deref]
-    #[deref_mut]
-    stateless: StatelessInteractivity<V>,
-    click_listeners: SmallVec<[ClickListener<V>; 2]>,
-    active_style: StyleRefinement,
-    group_active_style: Option<GroupStyle>,
-    drag_listener: Option<DragListener<V>>,
-    hover_listener: Option<HoverListener<V>>,
-    tooltip_builder: Option<TooltipBuilder<V>>,
-}
-
-impl<V: 'static> StatefulInteractivity<V> {
-    pub fn new(id: ElementId, stateless: StatelessInteractivity<V>) -> Self {
-        Self {
-            id,
-            stateless,
-            click_listeners: SmallVec::new(),
-            active_style: StyleRefinement::default(),
-            group_active_style: None,
-            drag_listener: None,
-            hover_listener: None,
-            tooltip_builder: None,
-        }
-    }
-}
-
-impl<V: 'static> ElementInteractivity<V> for StatefulInteractivity<V> {
-    fn as_stateful(&self) -> Option<&StatefulInteractivity<V>> {
-        Some(self)
-    }
-
-    fn as_stateful_mut(&mut self) -> Option<&mut StatefulInteractivity<V>> {
-        Some(self)
-    }
-
-    fn as_stateless(&self) -> &StatelessInteractivity<V> {
-        &self.stateless
-    }
-
-    fn as_stateless_mut(&mut self) -> &mut StatelessInteractivity<V> {
-        &mut self.stateless
-    }
-}
-
-type DropListener<V> = dyn Fn(&mut V, AnyView, &mut ViewContext<V>) + 'static;
-
-pub struct StatelessInteractivity<V> {
-    pub dispatch_context: KeyContext,
-    pub mouse_down_listeners: SmallVec<[MouseDownListener<V>; 2]>,
-    pub mouse_up_listeners: SmallVec<[MouseUpListener<V>; 2]>,
-    pub mouse_move_listeners: SmallVec<[MouseMoveListener<V>; 2]>,
-    pub scroll_wheel_listeners: SmallVec<[ScrollWheelListener<V>; 2]>,
-    pub key_down_listeners: SmallVec<[KeyDownListener<V>; 2]>,
-    pub key_up_listeners: SmallVec<[KeyUpListener<V>; 2]>,
-    pub action_listeners: SmallVec<[(TypeId, ActionListener<V>); 8]>,
-    pub hover_style: StyleRefinement,
-    pub group_hover_style: Option<GroupStyle>,
-    drag_over_styles: SmallVec<[(TypeId, StyleRefinement); 2]>,
-    group_drag_over_styles: SmallVec<[(TypeId, GroupStyle); 2]>,
-    drop_listeners: SmallVec<[(TypeId, Box<DropListener<V>>); 2]>,
-}
-
-impl<V> StatelessInteractivity<V> {
-    pub fn into_stateful(self, id: impl Into<ElementId>) -> StatefulInteractivity<V> {
-        StatefulInteractivity {
-            id: id.into(),
-            stateless: self,
-            click_listeners: SmallVec::new(),
-            drag_listener: None,
-            hover_listener: None,
-            tooltip_builder: None,
-            active_style: StyleRefinement::default(),
-            group_active_style: None,
-        }
-    }
-}
-
-pub struct GroupStyle {
-    pub group: SharedString,
-    pub style: StyleRefinement,
-}
-
-#[derive(Default)]
-pub struct GroupBounds(HashMap<SharedString, SmallVec<[Bounds<Pixels>; 1]>>);
-
-impl GroupBounds {
-    pub fn get(name: &SharedString, cx: &mut AppContext) -> Option<Bounds<Pixels>> {
-        cx.default_global::<Self>()
-            .0
-            .get(name)
-            .and_then(|bounds_stack| bounds_stack.last())
-            .cloned()
-    }
-
-    pub fn push(name: SharedString, bounds: Bounds<Pixels>, cx: &mut AppContext) {
-        cx.default_global::<Self>()
-            .0
-            .entry(name)
-            .or_default()
-            .push(bounds);
-    }
-
-    pub fn pop(name: &SharedString, cx: &mut AppContext) {
-        cx.default_global::<Self>().0.get_mut(name).unwrap().pop();
-    }
-}
-
-#[derive(Copy, Clone, Default, Eq, PartialEq)]
-struct ActiveState {
-    pub group: bool,
-    pub element: bool,
-}
-
-impl ActiveState {
-    pub fn is_none(&self) -> bool {
-        !self.group && !self.element
-    }
-}
-
-#[derive(Default)]
-pub struct InteractiveElementState {
-    active_state: Arc<Mutex<ActiveState>>,
-    hover_state: Arc<Mutex<bool>>,
-    pending_mouse_down: Arc<Mutex<Option<MouseDownEvent>>>,
-    scroll_offset: Option<Arc<Mutex<Point<Pixels>>>>,
-    active_tooltip: Arc<Mutex<Option<ActiveTooltip>>>,
-}
-
-struct ActiveTooltip {
-    #[allow(unused)] // used to drop the task
-    waiting: Option<Task<()>>,
-    tooltip: Option<AnyTooltip>,
-}
-
-impl InteractiveElementState {
-    pub fn scroll_offset(&self) -> Option<Point<Pixels>> {
-        self.scroll_offset
-            .as_ref()
-            .map(|offset| offset.lock().clone())
-    }
-
-    pub fn track_scroll_offset(&mut self) -> Arc<Mutex<Point<Pixels>>> {
-        self.scroll_offset
-            .get_or_insert_with(|| Arc::new(Mutex::new(Default::default())))
-            .clone()
-    }
-}
-
-impl<V> Default for StatelessInteractivity<V> {
-    fn default() -> Self {
-        Self {
-            dispatch_context: KeyContext::default(),
-            mouse_down_listeners: SmallVec::new(),
-            mouse_up_listeners: SmallVec::new(),
-            mouse_move_listeners: SmallVec::new(),
-            scroll_wheel_listeners: SmallVec::new(),
-            key_down_listeners: SmallVec::new(),
-            key_up_listeners: SmallVec::new(),
-            action_listeners: SmallVec::new(),
-            hover_style: StyleRefinement::default(),
-            group_hover_style: None,
-            drag_over_styles: SmallVec::new(),
-            group_drag_over_styles: SmallVec::new(),
-            drop_listeners: SmallVec::new(),
-        }
-    }
-}
-
-impl<V: 'static> ElementInteractivity<V> for StatelessInteractivity<V> {
-    fn as_stateful(&self) -> Option<&StatefulInteractivity<V>> {
-        None
-    }
-
-    fn as_stateful_mut(&mut self) -> Option<&mut StatefulInteractivity<V>> {
-        None
-    }
-
-    fn as_stateless(&self) -> &StatelessInteractivity<V> {
-        self
-    }
-
-    fn as_stateless_mut(&mut self) -> &mut StatelessInteractivity<V> {
-        self
-    }
-}
+use std::{any::Any, fmt::Debug, marker::PhantomData, ops::Deref, path::PathBuf};
 
 #[derive(Clone, Debug, Eq, PartialEq)]
 pub struct KeyDownEvent {
@@ -1021,10 +86,6 @@ where
     }
 }
 
-// impl<S, R, V, E> Render for Drag<S, R, V, E> {
-//     // fn render(&mut self, cx: ViewContext<Self>) ->
-// }
-
 #[derive(Hash, PartialEq, Eq, Copy, Clone, Debug)]
 pub enum MouseButton {
     Left,
@@ -1222,45 +283,11 @@ pub struct FocusEvent {
     pub focused: Option<FocusHandle>,
 }
 
-pub type MouseDownListener<V> = Box<
-    dyn Fn(&mut V, &MouseDownEvent, &Bounds<Pixels>, DispatchPhase, &mut ViewContext<V>) + 'static,
->;
-pub type MouseUpListener<V> = Box<
-    dyn Fn(&mut V, &MouseUpEvent, &Bounds<Pixels>, DispatchPhase, &mut ViewContext<V>) + 'static,
->;
-
-pub type MouseMoveListener<V> = Box<
-    dyn Fn(&mut V, &MouseMoveEvent, &Bounds<Pixels>, DispatchPhase, &mut ViewContext<V>) + 'static,
->;
-
-pub type ScrollWheelListener<V> = Box<
-    dyn Fn(&mut V, &ScrollWheelEvent, &Bounds<Pixels>, DispatchPhase, &mut ViewContext<V>)
-        + 'static,
->;
-
-pub type ClickListener<V> = Box<dyn Fn(&mut V, &ClickEvent, &mut ViewContext<V>) + 'static>;
-
-pub(crate) type DragListener<V> =
-    Box<dyn Fn(&mut V, Point<Pixels>, &mut ViewContext<V>) -> AnyDrag + 'static>;
-
-pub(crate) type HoverListener<V> = Box<dyn Fn(&mut V, bool, &mut ViewContext<V>) + 'static>;
-
-pub(crate) type TooltipBuilder<V> = Arc<dyn Fn(&mut V, &mut ViewContext<V>) -> AnyView + 'static>;
-
-pub(crate) type KeyDownListener<V> =
-    Box<dyn Fn(&mut V, &KeyDownEvent, DispatchPhase, &mut ViewContext<V>) + 'static>;
-
-pub(crate) type KeyUpListener<V> =
-    Box<dyn Fn(&mut V, &KeyUpEvent, DispatchPhase, &mut ViewContext<V>) + 'static>;
-
-pub type ActionListener<V> =
-    Box<dyn Fn(&mut V, &dyn Any, DispatchPhase, &mut ViewContext<V>) + 'static>;
-
 #[cfg(test)]
 mod test {
     use crate::{
-        self as gpui, div, Div, FocusHandle, KeyBinding, Keystroke, ParentElement, Render,
-        StatefulInteractivity, StatelessInteractive, TestAppContext, VisualContext,
+        self as gpui, div, Component, Div, FocusHandle, InteractiveComponent, KeyBinding,
+        Keystroke, ParentComponent, Render, Stateful, TestAppContext, ViewContext, VisualContext,
     };
 
     struct TestView {

crates/gpui2/src/key_dispatch.rs 🔗

@@ -1,11 +1,9 @@
 use crate::{
-    build_action_from_type, Action, Bounds, DispatchPhase, Element, FocusEvent, FocusHandle,
-    FocusId, KeyBinding, KeyContext, KeyMatch, Keymap, Keystroke, KeystrokeMatcher, MouseDownEvent,
-    Pixels, Style, StyleRefinement, ViewContext, WindowContext,
+    build_action_from_type, Action, DispatchPhase, FocusId, KeyBinding, KeyContext, KeyMatch,
+    Keymap, Keystroke, KeystrokeMatcher, WindowContext,
 };
 use collections::HashMap;
 use parking_lot::Mutex;
-use refineable::Refineable;
 use smallvec::SmallVec;
 use std::{
     any::{Any, TypeId},
@@ -14,10 +12,6 @@ use std::{
 };
 use util::ResultExt;
 
-pub type FocusListeners<V> = SmallVec<[FocusListener<V>; 2]>;
-pub type FocusListener<V> =
-    Box<dyn Fn(&mut V, &FocusHandle, &FocusEvent, &mut ViewContext<V>) + 'static>;
-
 #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
 pub struct DispatchNodeId(usize);
 
@@ -208,258 +202,3 @@ impl DispatchTree {
         *self.node_stack.last().unwrap()
     }
 }
-
-pub trait KeyDispatch<V: 'static>: 'static {
-    fn as_focusable(&self) -> Option<&FocusableKeyDispatch<V>>;
-    fn as_focusable_mut(&mut self) -> Option<&mut FocusableKeyDispatch<V>>;
-    fn key_context(&self) -> &KeyContext;
-    fn key_context_mut(&mut self) -> &mut KeyContext;
-
-    fn initialize<R>(
-        &mut self,
-        focus_handle: Option<FocusHandle>,
-        cx: &mut ViewContext<V>,
-        f: impl FnOnce(Option<FocusHandle>, &mut ViewContext<V>) -> R,
-    ) -> R {
-        let focus_handle = if let Some(focusable) = self.as_focusable_mut() {
-            let focus_handle = focusable
-                .focus_handle
-                .get_or_insert_with(|| focus_handle.unwrap_or_else(|| cx.focus_handle()))
-                .clone();
-            for listener in focusable.focus_listeners.drain(..) {
-                let focus_handle = focus_handle.clone();
-                cx.on_focus_changed(move |view, event, cx| {
-                    listener(view, &focus_handle, event, cx)
-                });
-            }
-            Some(focus_handle)
-        } else {
-            None
-        };
-
-        cx.with_key_dispatch(self.key_context().clone(), focus_handle, f)
-    }
-
-    fn refine_style(&self, style: &mut Style, cx: &WindowContext) {
-        if let Some(focusable) = self.as_focusable() {
-            let focus_handle = focusable
-                .focus_handle
-                .as_ref()
-                .expect("must call initialize before refine_style");
-            if focus_handle.contains_focused(cx) {
-                style.refine(&focusable.focus_in_style);
-            }
-
-            if focus_handle.within_focused(cx) {
-                style.refine(&focusable.in_focus_style);
-            }
-
-            if focus_handle.is_focused(cx) {
-                style.refine(&focusable.focus_style);
-            }
-        }
-    }
-
-    fn paint(&self, bounds: Bounds<Pixels>, cx: &mut WindowContext) {
-        if let Some(focusable) = self.as_focusable() {
-            let focus_handle = focusable
-                .focus_handle
-                .clone()
-                .expect("must call initialize before paint");
-            cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| {
-                if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
-                    if !cx.default_prevented() {
-                        cx.focus(&focus_handle);
-                        cx.prevent_default();
-                    }
-                }
-            })
-        }
-    }
-}
-
-pub struct FocusableKeyDispatch<V> {
-    pub non_focusable: NonFocusableKeyDispatch,
-    pub focus_handle: Option<FocusHandle>,
-    pub focus_listeners: FocusListeners<V>,
-    pub focus_style: StyleRefinement,
-    pub focus_in_style: StyleRefinement,
-    pub in_focus_style: StyleRefinement,
-}
-
-impl<V> FocusableKeyDispatch<V> {
-    pub fn new(non_focusable: NonFocusableKeyDispatch) -> Self {
-        Self {
-            non_focusable,
-            focus_handle: None,
-            focus_listeners: FocusListeners::default(),
-            focus_style: StyleRefinement::default(),
-            focus_in_style: StyleRefinement::default(),
-            in_focus_style: StyleRefinement::default(),
-        }
-    }
-
-    pub fn tracked(non_focusable: NonFocusableKeyDispatch, handle: &FocusHandle) -> Self {
-        Self {
-            non_focusable,
-            focus_handle: Some(handle.clone()),
-            focus_listeners: FocusListeners::default(),
-            focus_style: StyleRefinement::default(),
-            focus_in_style: StyleRefinement::default(),
-            in_focus_style: StyleRefinement::default(),
-        }
-    }
-}
-
-impl<V: 'static> KeyDispatch<V> for FocusableKeyDispatch<V> {
-    fn as_focusable(&self) -> Option<&FocusableKeyDispatch<V>> {
-        Some(self)
-    }
-
-    fn as_focusable_mut(&mut self) -> Option<&mut FocusableKeyDispatch<V>> {
-        Some(self)
-    }
-
-    fn key_context(&self) -> &KeyContext {
-        &self.non_focusable.key_context
-    }
-
-    fn key_context_mut(&mut self) -> &mut KeyContext {
-        &mut self.non_focusable.key_context
-    }
-}
-
-#[derive(Default)]
-pub struct NonFocusableKeyDispatch {
-    pub(crate) key_context: KeyContext,
-}
-
-impl<V: 'static> KeyDispatch<V> for NonFocusableKeyDispatch {
-    fn as_focusable(&self) -> Option<&FocusableKeyDispatch<V>> {
-        None
-    }
-
-    fn as_focusable_mut(&mut self) -> Option<&mut FocusableKeyDispatch<V>> {
-        None
-    }
-
-    fn key_context(&self) -> &KeyContext {
-        &self.key_context
-    }
-
-    fn key_context_mut(&mut self) -> &mut KeyContext {
-        &mut self.key_context
-    }
-}
-
-pub trait Focusable<V: 'static>: Element<V> {
-    fn focus_listeners(&mut self) -> &mut FocusListeners<V>;
-    fn set_focus_style(&mut self, style: StyleRefinement);
-    fn set_focus_in_style(&mut self, style: StyleRefinement);
-    fn set_in_focus_style(&mut self, style: StyleRefinement);
-
-    fn focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
-    where
-        Self: Sized,
-    {
-        self.set_focus_style(f(StyleRefinement::default()));
-        self
-    }
-
-    fn focus_in(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
-    where
-        Self: Sized,
-    {
-        self.set_focus_in_style(f(StyleRefinement::default()));
-        self
-    }
-
-    fn in_focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
-    where
-        Self: Sized,
-    {
-        self.set_in_focus_style(f(StyleRefinement::default()));
-        self
-    }
-
-    fn on_focus(
-        mut self,
-        listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext<V>) + 'static,
-    ) -> Self
-    where
-        Self: Sized,
-    {
-        self.focus_listeners()
-            .push(Box::new(move |view, focus_handle, event, cx| {
-                if event.focused.as_ref() == Some(focus_handle) {
-                    listener(view, event, cx)
-                }
-            }));
-        self
-    }
-
-    fn on_blur(
-        mut self,
-        listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext<V>) + 'static,
-    ) -> Self
-    where
-        Self: Sized,
-    {
-        self.focus_listeners()
-            .push(Box::new(move |view, focus_handle, event, cx| {
-                if event.blurred.as_ref() == Some(focus_handle) {
-                    listener(view, event, cx)
-                }
-            }));
-        self
-    }
-
-    fn on_focus_in(
-        mut self,
-        listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext<V>) + 'static,
-    ) -> Self
-    where
-        Self: Sized,
-    {
-        self.focus_listeners()
-            .push(Box::new(move |view, focus_handle, event, cx| {
-                let descendant_blurred = event
-                    .blurred
-                    .as_ref()
-                    .map_or(false, |blurred| focus_handle.contains(blurred, cx));
-                let descendant_focused = event
-                    .focused
-                    .as_ref()
-                    .map_or(false, |focused| focus_handle.contains(focused, cx));
-
-                if !descendant_blurred && descendant_focused {
-                    listener(view, event, cx)
-                }
-            }));
-        self
-    }
-
-    fn on_focus_out(
-        mut self,
-        listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext<V>) + 'static,
-    ) -> Self
-    where
-        Self: Sized,
-    {
-        self.focus_listeners()
-            .push(Box::new(move |view, focus_handle, event, cx| {
-                let descendant_blurred = event
-                    .blurred
-                    .as_ref()
-                    .map_or(false, |blurred| focus_handle.contains(blurred, cx));
-                let descendant_focused = event
-                    .focused
-                    .as_ref()
-                    .map_or(false, |focused| focus_handle.contains(focused, cx));
-                if descendant_blurred && !descendant_focused {
-                    listener(view, event, cx)
-                }
-            }));
-        self
-    }
-}

crates/gpui2/src/platform.rs 🔗

@@ -184,7 +184,11 @@ pub trait PlatformTextSystem: Send + Sync {
     fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>>;
     fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId>;
     fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>>;
-    fn rasterize_glyph(&self, params: &RenderGlyphParams) -> Result<(Size<DevicePixels>, Vec<u8>)>;
+    fn rasterize_glyph(
+        &self,
+        params: &RenderGlyphParams,
+        raster_bounds: Bounds<DevicePixels>,
+    ) -> Result<(Size<DevicePixels>, Vec<u8>)>;
     fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout;
     fn wrap_line(
         &self,

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

@@ -116,7 +116,9 @@ impl PlatformTextSystem for MacTextSystem {
                 },
             )?;
 
-            Ok(candidates[ix])
+            let font_id = candidates[ix];
+            lock.font_selections.insert(font.clone(), font_id);
+            Ok(font_id)
         }
     }
 
@@ -145,8 +147,9 @@ impl PlatformTextSystem for MacTextSystem {
     fn rasterize_glyph(
         &self,
         glyph_id: &RenderGlyphParams,
+        raster_bounds: Bounds<DevicePixels>,
     ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
-        self.0.read().rasterize_glyph(glyph_id)
+        self.0.read().rasterize_glyph(glyph_id, raster_bounds)
     }
 
     fn layout_line(&self, text: &str, font_size: Pixels, font_runs: &[FontRun]) -> LineLayout {
@@ -247,8 +250,11 @@ impl MacTextSystemState {
             .into())
     }
 
-    fn rasterize_glyph(&self, params: &RenderGlyphParams) -> Result<(Size<DevicePixels>, Vec<u8>)> {
-        let glyph_bounds = self.raster_bounds(params)?;
+    fn rasterize_glyph(
+        &self,
+        params: &RenderGlyphParams,
+        glyph_bounds: Bounds<DevicePixels>,
+    ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
         if glyph_bounds.size.width.0 == 0 || glyph_bounds.size.height.0 == 0 {
             Err(anyhow!("glyph bounds are empty"))
         } else {
@@ -260,6 +266,7 @@ impl MacTextSystemState {
             if params.subpixel_variant.y > 0 {
                 bitmap_size.height += DevicePixels(1);
             }
+            let bitmap_size = bitmap_size;
 
             let mut bytes;
             let cx;

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

@@ -3,8 +3,15 @@ use crate::{
     PlatformDisplay, PlatformTextSystem, TestDisplay, TestWindow, WindowOptions,
 };
 use anyhow::{anyhow, Result};
+use collections::VecDeque;
+use futures::channel::oneshot;
 use parking_lot::Mutex;
-use std::{rc::Rc, sync::Arc};
+use std::{
+    cell::RefCell,
+    path::PathBuf,
+    rc::{Rc, Weak},
+    sync::Arc,
+};
 
 pub struct TestPlatform {
     background_executor: BackgroundExecutor,
@@ -13,18 +20,60 @@ pub struct TestPlatform {
     active_window: Arc<Mutex<Option<AnyWindowHandle>>>,
     active_display: Rc<dyn PlatformDisplay>,
     active_cursor: Mutex<CursorStyle>,
+    pub(crate) prompts: RefCell<TestPrompts>,
+    weak: Weak<Self>,
+}
+
+#[derive(Default)]
+pub(crate) struct TestPrompts {
+    multiple_choice: VecDeque<oneshot::Sender<usize>>,
+    new_path: VecDeque<(PathBuf, oneshot::Sender<Option<PathBuf>>)>,
 }
 
 impl TestPlatform {
-    pub fn new(executor: BackgroundExecutor, foreground_executor: ForegroundExecutor) -> Self {
-        TestPlatform {
+    pub fn new(executor: BackgroundExecutor, foreground_executor: ForegroundExecutor) -> Rc<Self> {
+        Rc::new_cyclic(|weak| TestPlatform {
             background_executor: executor,
             foreground_executor,
-
+            prompts: Default::default(),
             active_cursor: Default::default(),
             active_display: Rc::new(TestDisplay::new()),
             active_window: Default::default(),
-        }
+            weak: weak.clone(),
+        })
+    }
+
+    pub(crate) fn simulate_new_path_selection(
+        &self,
+        select_path: impl FnOnce(&std::path::Path) -> Option<std::path::PathBuf>,
+    ) {
+        let (path, tx) = self
+            .prompts
+            .borrow_mut()
+            .new_path
+            .pop_front()
+            .expect("no pending new path prompt");
+        tx.send(select_path(&path)).ok();
+    }
+
+    pub(crate) fn simulate_prompt_answer(&self, response_ix: usize) {
+        let tx = self
+            .prompts
+            .borrow_mut()
+            .multiple_choice
+            .pop_front()
+            .expect("no pending multiple choice prompt");
+        tx.send(response_ix).ok();
+    }
+
+    pub(crate) fn has_pending_prompt(&self) -> bool {
+        !self.prompts.borrow().multiple_choice.is_empty()
+    }
+
+    pub(crate) fn prompt(&self) -> oneshot::Receiver<usize> {
+        let (tx, rx) = oneshot::channel();
+        self.prompts.borrow_mut().multiple_choice.push_back(tx);
+        rx
     }
 }
 
@@ -46,9 +95,7 @@ impl Platform for TestPlatform {
         unimplemented!()
     }
 
-    fn quit(&self) {
-        unimplemented!()
-    }
+    fn quit(&self) {}
 
     fn restart(&self) {
         unimplemented!()
@@ -88,7 +135,11 @@ impl Platform for TestPlatform {
         options: WindowOptions,
     ) -> Box<dyn crate::PlatformWindow> {
         *self.active_window.lock() = Some(handle);
-        Box::new(TestWindow::new(options, self.active_display.clone()))
+        Box::new(TestWindow::new(
+            options,
+            self.weak.clone(),
+            self.active_display.clone(),
+        ))
     }
 
     fn set_display_link_output_callback(
@@ -118,15 +169,20 @@ impl Platform for TestPlatform {
     fn prompt_for_paths(
         &self,
         _options: crate::PathPromptOptions,
-    ) -> futures::channel::oneshot::Receiver<Option<Vec<std::path::PathBuf>>> {
+    ) -> oneshot::Receiver<Option<Vec<std::path::PathBuf>>> {
         unimplemented!()
     }
 
     fn prompt_for_new_path(
         &self,
-        _directory: &std::path::Path,
-    ) -> futures::channel::oneshot::Receiver<Option<std::path::PathBuf>> {
-        unimplemented!()
+        directory: &std::path::Path,
+    ) -> oneshot::Receiver<Option<std::path::PathBuf>> {
+        let (tx, rx) = oneshot::channel();
+        self.prompts
+            .borrow_mut()
+            .new_path
+            .push_back((directory.to_path_buf(), tx));
+        rx
     }
 
     fn reveal_path(&self, _path: &std::path::Path) {
@@ -141,9 +197,7 @@ impl Platform for TestPlatform {
         unimplemented!()
     }
 
-    fn on_quit(&self, _callback: Box<dyn FnMut()>) {
-        unimplemented!()
-    }
+    fn on_quit(&self, _callback: Box<dyn FnMut()>) {}
 
     fn on_reopen(&self, _callback: Box<dyn FnMut()>) {
         unimplemented!()

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

@@ -1,15 +1,13 @@
-use std::{
-    rc::Rc,
-    sync::{self, Arc},
+use crate::{
+    px, AtlasKey, AtlasTextureId, AtlasTile, Pixels, PlatformAtlas, PlatformDisplay,
+    PlatformInputHandler, PlatformWindow, Point, Scene, Size, TestPlatform, TileId,
+    WindowAppearance, WindowBounds, WindowOptions,
 };
-
 use collections::HashMap;
 use parking_lot::Mutex;
-
-use crate::{
-    px, AtlasKey, AtlasTextureId, AtlasTile, Pixels, PlatformAtlas, PlatformDisplay,
-    PlatformInputHandler, PlatformWindow, Point, Scene, Size, TileId, WindowAppearance,
-    WindowBounds, WindowOptions,
+use std::{
+    rc::{Rc, Weak},
+    sync::{self, Arc},
 };
 
 #[derive(Default)]
@@ -25,16 +23,22 @@ pub struct TestWindow {
     current_scene: Mutex<Option<Scene>>,
     display: Rc<dyn PlatformDisplay>,
     input_handler: Option<Box<dyn PlatformInputHandler>>,
-
     handlers: Mutex<Handlers>,
+    platform: Weak<TestPlatform>,
     sprite_atlas: Arc<dyn PlatformAtlas>,
 }
+
 impl TestWindow {
-    pub fn new(options: WindowOptions, display: Rc<dyn PlatformDisplay>) -> Self {
+    pub fn new(
+        options: WindowOptions,
+        platform: Weak<TestPlatform>,
+        display: Rc<dyn PlatformDisplay>,
+    ) -> Self {
         Self {
             bounds: options.bounds,
             current_scene: Default::default(),
             display,
+            platform,
             input_handler: None,
             sprite_atlas: Arc::new(TestAtlas::new()),
             handlers: Default::default(),
@@ -89,7 +93,7 @@ impl PlatformWindow for TestWindow {
         _msg: &str,
         _answers: &[&str],
     ) -> futures::channel::oneshot::Receiver<usize> {
-        todo!()
+        self.platform.upgrade().expect("platform dropped").prompt()
     }
 
     fn activate(&self) {

crates/gpui2/src/prelude.rs 🔗

@@ -1 +1,4 @@
-pub use crate::{Context, ParentElement, Refineable};
+pub use crate::{
+    BorrowAppContext, BorrowWindow, Component, Context, FocusableComponent, InteractiveComponent,
+    ParentComponent, Refineable, Render, StatefulInteractiveComponent, Styled, VisualContext,
+};

crates/gpui2/src/style.rs 🔗

@@ -1,8 +1,8 @@
 use crate::{
     black, phi, point, rems, AbsoluteLength, BorrowAppContext, BorrowWindow, Bounds, ContentMask,
     Corners, CornersRefinement, CursorStyle, DefiniteLength, Edges, EdgesRefinement, Font,
-    FontFeatures, FontStyle, FontWeight, Hsla, Length, Pixels, Point, PointRefinement, Rgba,
-    SharedString, Size, SizeRefinement, Styled, TextRun, ViewContext, WindowContext,
+    FontFeatures, FontStyle, FontWeight, Hsla, Length, Pixels, Point, PointRefinement,
+    Rgba, SharedString, Size, SizeRefinement, Styled, TextRun, ViewContext,
 };
 use refineable::{Cascade, Refineable};
 use smallvec::SmallVec;
@@ -220,7 +220,7 @@ pub struct HighlightStyle {
 impl Eq for HighlightStyle {}
 
 impl Style {
-    pub fn text_style(&self, _cx: &WindowContext) -> Option<&TextStyleRefinement> {
+    pub fn text_style(&self) -> Option<&TextStyleRefinement> {
         if self.text.is_some() {
             Some(&self.text)
         } else {
@@ -228,13 +228,47 @@ impl Style {
         }
     }
 
+    pub fn overflow_mask(&self, bounds: Bounds<Pixels>) -> Option<ContentMask<Pixels>> {
+        match self.overflow {
+            Point {
+                x: Overflow::Visible,
+                y: Overflow::Visible,
+            } => None,
+            _ => {
+                let current_mask = bounds;
+                let min = current_mask.origin;
+                let max = current_mask.lower_right();
+                let bounds = match (
+                    self.overflow.x == Overflow::Visible,
+                    self.overflow.y == Overflow::Visible,
+                ) {
+                    // x and y both visible
+                    (true, true) => return None,
+                    // x visible, y hidden
+                    (true, false) => Bounds::from_corners(
+                        point(min.x, bounds.origin.y),
+                        point(max.x, bounds.lower_right().y),
+                    ),
+                    // x hidden, y visible
+                    (false, true) => Bounds::from_corners(
+                        point(bounds.origin.x, min.y),
+                        point(bounds.lower_right().x, max.y),
+                    ),
+                    // both hidden
+                    (false, false) => bounds,
+                };
+                Some(ContentMask { bounds })
+            }
+        }
+    }
+
     pub fn apply_text_style<C, F, R>(&self, cx: &mut C, f: F) -> R
     where
         C: BorrowAppContext,
         F: FnOnce(&mut C) -> R,
     {
         if self.text.is_some() {
-            cx.with_text_style(self.text.clone(), f)
+            cx.with_text_style(Some(self.text.clone()), f)
         } else {
             f(cx)
         }
@@ -274,7 +308,7 @@ impl Style {
             bounds: mask_bounds,
         };
 
-        cx.with_content_mask(mask, f)
+        cx.with_content_mask(Some(mask), f)
     }
 
     /// Paints the background of an element styled with this style.

crates/gpui2/src/styled.rs 🔗

@@ -1,26 +1,24 @@
 use crate::{
     self as gpui, hsla, point, px, relative, rems, AbsoluteLength, AlignItems, CursorStyle,
     DefiniteLength, Display, Fill, FlexDirection, Hsla, JustifyContent, Length, Position,
-    SharedString, Style, StyleRefinement, Visibility,
+    SharedString, StyleRefinement, Visibility,
 };
 use crate::{BoxShadow, TextStyleRefinement};
-use refineable::Refineable;
 use smallvec::{smallvec, SmallVec};
+use taffy::style::Overflow;
 
-pub trait Styled {
+pub trait Styled: Sized {
     fn style(&mut self) -> &mut StyleRefinement;
 
-    fn computed_style(&mut self) -> Style {
-        Style::default().refined(self.style().clone())
-    }
-
     gpui2_macros::style_helpers!();
 
+    fn z_index(mut self, z_index: u32) -> Self {
+        self.style().z_index = Some(z_index);
+        self
+    }
+
     /// Sets the size of the element to the full width and height.
-    fn full(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn full(mut self) -> Self {
         self.style().size.width = Some(relative(1.).into());
         self.style().size.height = Some(relative(1.).into());
         self
@@ -28,118 +26,98 @@ pub trait Styled {
 
     /// Sets the position of the element to `relative`.
     /// [Docs](https://tailwindcss.com/docs/position)
-    fn relative(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn relative(mut self) -> Self {
         self.style().position = Some(Position::Relative);
         self
     }
 
     /// Sets the position of the element to `absolute`.
     /// [Docs](https://tailwindcss.com/docs/position)
-    fn absolute(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn absolute(mut self) -> Self {
         self.style().position = Some(Position::Absolute);
         self
     }
 
     /// Sets the display type of the element to `block`.
     /// [Docs](https://tailwindcss.com/docs/display)
-    fn block(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn block(mut self) -> Self {
         self.style().display = Some(Display::Block);
         self
     }
 
     /// Sets the display type of the element to `flex`.
     /// [Docs](https://tailwindcss.com/docs/display)
-    fn flex(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn flex(mut self) -> Self {
         self.style().display = Some(Display::Flex);
         self
     }
 
     /// Sets the visibility of the element to `visible`.
     /// [Docs](https://tailwindcss.com/docs/visibility)
-    fn visible(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn visible(mut self) -> Self {
         self.style().visibility = Some(Visibility::Visible);
         self
     }
 
     /// Sets the visibility of the element to `hidden`.
     /// [Docs](https://tailwindcss.com/docs/visibility)
-    fn invisible(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn invisible(mut self) -> Self {
         self.style().visibility = Some(Visibility::Hidden);
         self
     }
 
-    fn cursor(mut self, cursor: CursorStyle) -> Self
-    where
-        Self: Sized,
-    {
+    fn overflow_hidden(mut self) -> Self {
+        self.style().overflow.x = Some(Overflow::Hidden);
+        self.style().overflow.y = Some(Overflow::Hidden);
+        self
+    }
+
+    fn overflow_hidden_x(mut self) -> Self {
+        self.style().overflow.x = Some(Overflow::Hidden);
+        self
+    }
+
+    fn overflow_hidden_y(mut self) -> Self {
+        self.style().overflow.y = Some(Overflow::Hidden);
+        self
+    }
+
+    fn cursor(mut self, cursor: CursorStyle) -> Self {
         self.style().mouse_cursor = Some(cursor);
         self
     }
 
     /// Sets the cursor style when hovering an element to `default`.
     /// [Docs](https://tailwindcss.com/docs/cursor)
-    fn cursor_default(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn cursor_default(mut self) -> Self {
         self.style().mouse_cursor = Some(CursorStyle::Arrow);
         self
     }
 
     /// Sets the cursor style when hovering an element to `pointer`.
     /// [Docs](https://tailwindcss.com/docs/cursor)
-    fn cursor_pointer(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn cursor_pointer(mut self) -> Self {
         self.style().mouse_cursor = Some(CursorStyle::PointingHand);
         self
     }
 
     /// Sets the flex direction of the element to `column`.
     /// [Docs](https://tailwindcss.com/docs/flex-direction#column)
-    fn flex_col(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn flex_col(mut self) -> Self {
         self.style().flex_direction = Some(FlexDirection::Column);
         self
     }
 
     /// Sets the flex direction of the element to `row`.
     /// [Docs](https://tailwindcss.com/docs/flex-direction#row)
-    fn flex_row(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn flex_row(mut self) -> Self {
         self.style().flex_direction = Some(FlexDirection::Row);
         self
     }
 
     /// Sets the element to allow a flex item to grow and shrink as needed, ignoring its initial size.
     /// [Docs](https://tailwindcss.com/docs/flex#flex-1)
-    fn flex_1(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn flex_1(mut self) -> Self {
         self.style().flex_grow = Some(1.);
         self.style().flex_shrink = Some(1.);
         self.style().flex_basis = Some(relative(0.).into());
@@ -148,10 +126,7 @@ pub trait Styled {
 
     /// Sets the element to allow a flex item to grow and shrink, taking into account its initial size.
     /// [Docs](https://tailwindcss.com/docs/flex#auto)
-    fn flex_auto(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn flex_auto(mut self) -> Self {
         self.style().flex_grow = Some(1.);
         self.style().flex_shrink = Some(1.);
         self.style().flex_basis = Some(Length::Auto);
@@ -160,10 +135,7 @@ pub trait Styled {
 
     /// Sets the element to allow a flex item to shrink but not grow, taking into account its initial size.
     /// [Docs](https://tailwindcss.com/docs/flex#initial)
-    fn flex_initial(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn flex_initial(mut self) -> Self {
         self.style().flex_grow = Some(0.);
         self.style().flex_shrink = Some(1.);
         self.style().flex_basis = Some(Length::Auto);
@@ -172,10 +144,7 @@ pub trait Styled {
 
     /// Sets the element to prevent a flex item from growing or shrinking.
     /// [Docs](https://tailwindcss.com/docs/flex#none)
-    fn flex_none(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn flex_none(mut self) -> Self {
         self.style().flex_grow = Some(0.);
         self.style().flex_shrink = Some(0.);
         self
@@ -183,40 +152,28 @@ pub trait Styled {
 
     /// Sets the element to allow a flex item to grow to fill any available space.
     /// [Docs](https://tailwindcss.com/docs/flex-grow)
-    fn grow(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn grow(mut self) -> Self {
         self.style().flex_grow = Some(1.);
         self
     }
 
     /// Sets the element to align flex items to the start of the container's cross axis.
     /// [Docs](https://tailwindcss.com/docs/align-items#start)
-    fn items_start(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn items_start(mut self) -> Self {
         self.style().align_items = Some(AlignItems::FlexStart);
         self
     }
 
     /// Sets the element to align flex items to the end of the container's cross axis.
     /// [Docs](https://tailwindcss.com/docs/align-items#end)
-    fn items_end(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn items_end(mut self) -> Self {
         self.style().align_items = Some(AlignItems::FlexEnd);
         self
     }
 
     /// Sets the element to align flex items along the center of the container's cross axis.
     /// [Docs](https://tailwindcss.com/docs/align-items#center)
-    fn items_center(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn items_center(mut self) -> Self {
         self.style().align_items = Some(AlignItems::Center);
         self
     }
@@ -224,40 +181,28 @@ pub trait Styled {
     /// Sets the element to justify flex items along the container's main axis
     /// such that there is an equal amount of space between each item.
     /// [Docs](https://tailwindcss.com/docs/justify-content#space-between)
-    fn justify_between(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn justify_between(mut self) -> Self {
         self.style().justify_content = Some(JustifyContent::SpaceBetween);
         self
     }
 
     /// Sets the element to justify flex items along the center of the container's main axis.
     /// [Docs](https://tailwindcss.com/docs/justify-content#center)
-    fn justify_center(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn justify_center(mut self) -> Self {
         self.style().justify_content = Some(JustifyContent::Center);
         self
     }
 
     /// Sets the element to justify flex items against the start of the container's main axis.
     /// [Docs](https://tailwindcss.com/docs/justify-content#start)
-    fn justify_start(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn justify_start(mut self) -> Self {
         self.style().justify_content = Some(JustifyContent::Start);
         self
     }
 
     /// Sets the element to justify flex items against the end of the container's main axis.
     /// [Docs](https://tailwindcss.com/docs/justify-content#end)
-    fn justify_end(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn justify_end(mut self) -> Self {
         self.style().justify_content = Some(JustifyContent::End);
         self
     }
@@ -265,10 +210,7 @@ pub trait Styled {
     /// Sets the element to justify items along the container's main axis such
     /// that there is an equal amount of space on each side of each item.
     /// [Docs](https://tailwindcss.com/docs/justify-content#space-around)
-    fn justify_around(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn justify_around(mut self) -> Self {
         self.style().justify_content = Some(JustifyContent::SpaceAround);
         self
     }
@@ -295,30 +237,21 @@ pub trait Styled {
 
     /// Sets the box shadow of the element.
     /// [Docs](https://tailwindcss.com/docs/box-shadow)
-    fn shadow(mut self, shadows: SmallVec<[BoxShadow; 2]>) -> Self
-    where
-        Self: Sized,
-    {
+    fn shadow(mut self, shadows: SmallVec<[BoxShadow; 2]>) -> Self {
         self.style().box_shadow = Some(shadows);
         self
     }
 
     /// Clears the box shadow of the element.
     /// [Docs](https://tailwindcss.com/docs/box-shadow)
-    fn shadow_none(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn shadow_none(mut self) -> Self {
         self.style().box_shadow = Some(Default::default());
         self
     }
 
     /// Sets the box shadow of the element.
     /// [Docs](https://tailwindcss.com/docs/box-shadow)
-    fn shadow_sm(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn shadow_sm(mut self) -> Self {
         self.style().box_shadow = Some(smallvec::smallvec![BoxShadow {
             color: hsla(0., 0., 0., 0.05),
             offset: point(px(0.), px(1.)),
@@ -330,10 +263,7 @@ pub trait Styled {
 
     /// Sets the box shadow of the element.
     /// [Docs](https://tailwindcss.com/docs/box-shadow)
-    fn shadow_md(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn shadow_md(mut self) -> Self {
         self.style().box_shadow = Some(smallvec![
             BoxShadow {
                 color: hsla(0.5, 0., 0., 0.1),
@@ -353,10 +283,7 @@ pub trait Styled {
 
     /// Sets the box shadow of the element.
     /// [Docs](https://tailwindcss.com/docs/box-shadow)
-    fn shadow_lg(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn shadow_lg(mut self) -> Self {
         self.style().box_shadow = Some(smallvec![
             BoxShadow {
                 color: hsla(0., 0., 0., 0.1),
@@ -376,10 +303,7 @@ pub trait Styled {
 
     /// Sets the box shadow of the element.
     /// [Docs](https://tailwindcss.com/docs/box-shadow)
-    fn shadow_xl(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn shadow_xl(mut self) -> Self {
         self.style().box_shadow = Some(smallvec![
             BoxShadow {
                 color: hsla(0., 0., 0., 0.1),
@@ -399,10 +323,7 @@ pub trait Styled {
 
     /// Sets the box shadow of the element.
     /// [Docs](https://tailwindcss.com/docs/box-shadow)
-    fn shadow_2xl(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn shadow_2xl(mut self) -> Self {
         self.style().box_shadow = Some(smallvec![BoxShadow {
             color: hsla(0., 0., 0., 0.25),
             offset: point(px(0.), px(25.)),
@@ -417,198 +338,138 @@ pub trait Styled {
         &mut style.text
     }
 
-    fn text_color(mut self, color: impl Into<Hsla>) -> Self
-    where
-        Self: Sized,
-    {
+    fn text_color(mut self, color: impl Into<Hsla>) -> Self {
         self.text_style().get_or_insert_with(Default::default).color = Some(color.into());
         self
     }
 
-    fn text_size(mut self, size: impl Into<AbsoluteLength>) -> Self
-    where
-        Self: Sized,
-    {
+    fn text_size(mut self, size: impl Into<AbsoluteLength>) -> Self {
         self.text_style()
             .get_or_insert_with(Default::default)
             .font_size = Some(size.into());
         self
     }
 
-    fn text_xs(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn text_xs(mut self) -> Self {
         self.text_style()
             .get_or_insert_with(Default::default)
             .font_size = Some(rems(0.75).into());
         self
     }
 
-    fn text_sm(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn text_sm(mut self) -> Self {
         self.text_style()
             .get_or_insert_with(Default::default)
             .font_size = Some(rems(0.875).into());
         self
     }
 
-    fn text_base(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn text_base(mut self) -> Self {
         self.text_style()
             .get_or_insert_with(Default::default)
             .font_size = Some(rems(1.0).into());
         self
     }
 
-    fn text_lg(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn text_lg(mut self) -> Self {
         self.text_style()
             .get_or_insert_with(Default::default)
             .font_size = Some(rems(1.125).into());
         self
     }
 
-    fn text_xl(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn text_xl(mut self) -> Self {
         self.text_style()
             .get_or_insert_with(Default::default)
             .font_size = Some(rems(1.25).into());
         self
     }
 
-    fn text_2xl(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn text_2xl(mut self) -> Self {
         self.text_style()
             .get_or_insert_with(Default::default)
             .font_size = Some(rems(1.5).into());
         self
     }
 
-    fn text_3xl(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn text_3xl(mut self) -> Self {
         self.text_style()
             .get_or_insert_with(Default::default)
             .font_size = Some(rems(1.875).into());
         self
     }
 
-    fn text_decoration_none(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn text_decoration_none(mut self) -> Self {
         self.text_style()
             .get_or_insert_with(Default::default)
             .underline = None;
         self
     }
 
-    fn text_decoration_color(mut self, color: impl Into<Hsla>) -> Self
-    where
-        Self: Sized,
-    {
+    fn text_decoration_color(mut self, color: impl Into<Hsla>) -> Self {
         let style = self.text_style().get_or_insert_with(Default::default);
         let underline = style.underline.get_or_insert_with(Default::default);
         underline.color = Some(color.into());
         self
     }
 
-    fn text_decoration_solid(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn text_decoration_solid(mut self) -> Self {
         let style = self.text_style().get_or_insert_with(Default::default);
         let underline = style.underline.get_or_insert_with(Default::default);
         underline.wavy = false;
         self
     }
 
-    fn text_decoration_wavy(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn text_decoration_wavy(mut self) -> Self {
         let style = self.text_style().get_or_insert_with(Default::default);
         let underline = style.underline.get_or_insert_with(Default::default);
         underline.wavy = true;
         self
     }
 
-    fn text_decoration_0(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn text_decoration_0(mut self) -> Self {
         let style = self.text_style().get_or_insert_with(Default::default);
         let underline = style.underline.get_or_insert_with(Default::default);
         underline.thickness = px(0.);
         self
     }
 
-    fn text_decoration_1(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn text_decoration_1(mut self) -> Self {
         let style = self.text_style().get_or_insert_with(Default::default);
         let underline = style.underline.get_or_insert_with(Default::default);
         underline.thickness = px(1.);
         self
     }
 
-    fn text_decoration_2(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn text_decoration_2(mut self) -> Self {
         let style = self.text_style().get_or_insert_with(Default::default);
         let underline = style.underline.get_or_insert_with(Default::default);
         underline.thickness = px(2.);
         self
     }
 
-    fn text_decoration_4(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn text_decoration_4(mut self) -> Self {
         let style = self.text_style().get_or_insert_with(Default::default);
         let underline = style.underline.get_or_insert_with(Default::default);
         underline.thickness = px(4.);
         self
     }
 
-    fn text_decoration_8(mut self) -> Self
-    where
-        Self: Sized,
-    {
+    fn text_decoration_8(mut self) -> Self {
         let style = self.text_style().get_or_insert_with(Default::default);
         let underline = style.underline.get_or_insert_with(Default::default);
         underline.thickness = px(8.);
         self
     }
 
-    fn font(mut self, family_name: impl Into<SharedString>) -> Self
-    where
-        Self: Sized,
-    {
+    fn font(mut self, family_name: impl Into<SharedString>) -> Self {
         self.text_style()
             .get_or_insert_with(Default::default)
             .font_family = Some(family_name.into());
         self
     }
 
-    fn line_height(mut self, line_height: impl Into<DefiniteLength>) -> Self
-    where
-        Self: Sized,
-    {
+    fn line_height(mut self, line_height: impl Into<DefiniteLength>) -> Self {
         self.text_style()
             .get_or_insert_with(Default::default)
             .line_height = Some(line_height.into());

crates/gpui2/src/text_system.rs 🔗

@@ -39,6 +39,7 @@ pub struct TextSystem {
     platform_text_system: Arc<dyn PlatformTextSystem>,
     font_ids_by_font: RwLock<HashMap<Font, FontId>>,
     font_metrics: RwLock<HashMap<FontId, FontMetrics>>,
+    raster_bounds: RwLock<HashMap<RenderGlyphParams, Bounds<DevicePixels>>>,
     wrapper_pool: Mutex<HashMap<FontIdWithSize, Vec<LineWrapper>>>,
     font_runs_pool: Mutex<Vec<Vec<FontRun>>>,
 }
@@ -48,10 +49,11 @@ impl TextSystem {
         TextSystem {
             line_layout_cache: Arc::new(LineLayoutCache::new(platform_text_system.clone())),
             platform_text_system,
-            font_metrics: RwLock::new(HashMap::default()),
-            font_ids_by_font: RwLock::new(HashMap::default()),
-            wrapper_pool: Mutex::new(HashMap::default()),
-            font_runs_pool: Default::default(),
+            font_metrics: RwLock::default(),
+            raster_bounds: RwLock::default(),
+            font_ids_by_font: RwLock::default(),
+            wrapper_pool: Mutex::default(),
+            font_runs_pool: Mutex::default(),
         }
     }
 
@@ -252,14 +254,24 @@ impl TextSystem {
     }
 
     pub fn raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
-        self.platform_text_system.glyph_raster_bounds(params)
+        let raster_bounds = self.raster_bounds.upgradable_read();
+        if let Some(bounds) = raster_bounds.get(params) {
+            Ok(bounds.clone())
+        } else {
+            let mut raster_bounds = RwLockUpgradableReadGuard::upgrade(raster_bounds);
+            let bounds = self.platform_text_system.glyph_raster_bounds(params)?;
+            raster_bounds.insert(params.clone(), bounds);
+            Ok(bounds)
+        }
     }
 
     pub fn rasterize_glyph(
         &self,
-        glyph_id: &RenderGlyphParams,
+        params: &RenderGlyphParams,
     ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
-        self.platform_text_system.rasterize_glyph(glyph_id)
+        let raster_bounds = self.raster_bounds(params)?;
+        self.platform_text_system
+            .rasterize_glyph(params, raster_bounds)
     }
 }
 

crates/gpui2/src/view.rs 🔗

@@ -206,7 +206,7 @@ impl<V: Render> From<View<V>> for AnyView {
 impl<ParentViewState: 'static> Element<ParentViewState> for AnyView {
     type ElementState = Box<dyn Any>;
 
-    fn id(&self) -> Option<ElementId> {
+    fn element_id(&self) -> Option<ElementId> {
         Some(self.model.entity_id.into())
     }
 
@@ -305,7 +305,7 @@ where
 {
     type ElementState = AnyElement<ViewState>;
 
-    fn id(&self) -> Option<ElementId> {
+    fn element_id(&self) -> Option<ElementId> {
         Some(self.view.entity_id().into())
     }
 
@@ -315,7 +315,7 @@ where
         _: Option<Self::ElementState>,
         cx: &mut ViewContext<ParentViewState>,
     ) -> Self::ElementState {
-        cx.with_element_id(self.view.entity_id(), |_, cx| {
+        cx.with_element_id(Some(self.view.entity_id()), |cx| {
             self.view.update(cx, |view, cx| {
                 let mut element = self.component.take().unwrap().render();
                 element.initialize(view, cx);
@@ -330,7 +330,7 @@ where
         element: &mut Self::ElementState,
         cx: &mut ViewContext<ParentViewState>,
     ) -> LayoutId {
-        cx.with_element_id(self.view.entity_id(), |_, cx| {
+        cx.with_element_id(Some(self.view.entity_id()), |cx| {
             self.view.update(cx, |view, cx| element.layout(view, cx))
         })
     }
@@ -342,7 +342,7 @@ where
         element: &mut Self::ElementState,
         cx: &mut ViewContext<ParentViewState>,
     ) {
-        cx.with_element_id(self.view.entity_id(), |_, cx| {
+        cx.with_element_id(Some(self.view.entity_id()), |cx| {
             self.view.update(cx, |view, cx| element.paint(view, cx))
         })
     }
@@ -364,7 +364,7 @@ mod any_view {
     use std::any::Any;
 
     pub(crate) fn initialize<V: Render>(view: &AnyView, cx: &mut WindowContext) -> Box<dyn Any> {
-        cx.with_element_id(view.model.entity_id, |_, cx| {
+        cx.with_element_id(Some(view.model.entity_id), |cx| {
             let view = view.clone().downcast::<V>().unwrap();
             let element = view.update(cx, |view, cx| {
                 let mut element = AnyElement::new(view.render(cx));
@@ -380,7 +380,7 @@ mod any_view {
         element: &mut Box<dyn Any>,
         cx: &mut WindowContext,
     ) -> LayoutId {
-        cx.with_element_id(view.model.entity_id, |_, cx| {
+        cx.with_element_id(Some(view.model.entity_id), |cx| {
             let view = view.clone().downcast::<V>().unwrap();
             let element = element.downcast_mut::<AnyElement<V>>().unwrap();
             view.update(cx, |view, cx| element.layout(view, cx))
@@ -392,7 +392,7 @@ mod any_view {
         element: &mut Box<dyn Any>,
         cx: &mut WindowContext,
     ) {
-        cx.with_element_id(view.model.entity_id, |_, cx| {
+        cx.with_element_id(Some(view.model.entity_id), |cx| {
             let view = view.clone().downcast::<V>().unwrap();
             let element = element.downcast_mut::<AnyElement<V>>().unwrap();
             view.update(cx, |view, cx| element.paint(view, cx))

crates/gpui2/src/window.rs 🔗

@@ -1074,7 +1074,7 @@ impl<'a> WindowContext<'a> {
         if let Some(active_drag) = self.app.active_drag.take() {
             self.with_z_index(1, |cx| {
                 let offset = cx.mouse_position() - active_drag.cursor_offset;
-                cx.with_element_offset(Some(offset), |cx| {
+                cx.with_element_offset(offset, |cx| {
                     let available_space =
                         size(AvailableSpace::MinContent, AvailableSpace::MinContent);
                     active_drag.view.draw(available_space, cx);
@@ -1083,7 +1083,7 @@ impl<'a> WindowContext<'a> {
             });
         } else if let Some(active_tooltip) = self.app.active_tooltip.take() {
             self.with_z_index(1, |cx| {
-                cx.with_element_offset(Some(active_tooltip.cursor_offset), |cx| {
+                cx.with_element_offset(active_tooltip.cursor_offset, |cx| {
                     let available_space =
                         size(AvailableSpace::MinContent, AvailableSpace::MinContent);
                     active_tooltip.view.draw(available_space, cx);
@@ -1365,6 +1365,14 @@ impl<'a> WindowContext<'a> {
         self.window.platform_window.activate();
     }
 
+    pub fn minimize_window(&self) {
+        self.window.platform_window.minimize();
+    }
+
+    pub fn toggle_full_screen(&self) {
+        self.window.platform_window.toggle_full_screen();
+    }
+
     pub fn prompt(
         &self,
         level: PromptLevel,
@@ -1573,43 +1581,50 @@ pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> {
     /// used to associate state with identified elements across separate frames.
     fn with_element_id<R>(
         &mut self,
-        id: impl Into<ElementId>,
-        f: impl FnOnce(GlobalElementId, &mut Self) -> R,
+        id: Option<impl Into<ElementId>>,
+        f: impl FnOnce(&mut Self) -> R,
     ) -> R {
-        let window = self.window_mut();
-        window.element_id_stack.push(id.into());
-        let global_id = window.element_id_stack.clone();
-        let result = f(global_id, self);
-        let window: &mut Window = self.borrow_mut();
-        window.element_id_stack.pop();
-        result
+        if let Some(id) = id.map(Into::into) {
+            let window = self.window_mut();
+            window.element_id_stack.push(id.into());
+            let result = f(self);
+            let window: &mut Window = self.borrow_mut();
+            window.element_id_stack.pop();
+            result
+        } else {
+            f(self)
+        }
     }
 
     /// Invoke the given function with the given content mask after intersecting it
     /// with the current mask.
     fn with_content_mask<R>(
         &mut self,
-        mask: ContentMask<Pixels>,
+        mask: Option<ContentMask<Pixels>>,
         f: impl FnOnce(&mut Self) -> R,
     ) -> R {
-        let mask = mask.intersect(&self.content_mask());
-        self.window_mut()
-            .current_frame
-            .content_mask_stack
-            .push(mask);
-        let result = f(self);
-        self.window_mut().current_frame.content_mask_stack.pop();
-        result
+        if let Some(mask) = mask {
+            let mask = mask.intersect(&self.content_mask());
+            self.window_mut()
+                .current_frame
+                .content_mask_stack
+                .push(mask);
+            let result = f(self);
+            self.window_mut().current_frame.content_mask_stack.pop();
+            result
+        } else {
+            f(self)
+        }
     }
 
     /// Update the global element offset based on the given offset. This is used to implement
     /// scrolling and position drag handles.
     fn with_element_offset<R>(
         &mut self,
-        offset: Option<Point<Pixels>>,
+        offset: Point<Pixels>,
         f: impl FnOnce(&mut Self) -> R,
     ) -> R {
-        let Some(offset) = offset else {
+        if offset.is_zero() {
             return f(self);
         };
 
@@ -1645,7 +1660,9 @@ pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> {
     where
         S: 'static,
     {
-        self.with_element_id(id, |global_id, cx| {
+        self.with_element_id(Some(id), |cx| {
+            let global_id = cx.window().element_id_stack.clone();
+
             if let Some(any) = cx
                 .window_mut()
                 .current_frame
@@ -2073,11 +2090,10 @@ impl<'a, V: 'static> ViewContext<'a, V> {
         f: impl FnOnce(Option<FocusHandle>, &mut Self) -> R,
     ) -> R {
         let window = &mut self.window;
-
         window
             .current_frame
             .dispatch_tree
-            .push_node(context, &mut window.previous_frame.dispatch_tree);
+            .push_node(context.clone(), &mut window.previous_frame.dispatch_tree);
         if let Some(focus_handle) = focus_handle.as_ref() {
             window
                 .current_frame
@@ -2360,6 +2376,12 @@ impl<V: 'static + Render> WindowHandle<V> {
     {
         cx.read_window(self, |root_view, _cx| root_view.clone())
     }
+
+    pub fn is_active(&self, cx: &WindowContext) -> Option<bool> {
+        cx.windows
+            .get(self.id)
+            .and_then(|window| window.as_ref().map(|window| window.active))
+    }
 }
 
 impl<V> Copy for WindowHandle<V> {}

crates/gpui2_macros/src/style_helpers.rs 🔗

@@ -130,7 +130,7 @@ fn generate_predefined_setter(
 
     let method = quote! {
         #[doc = #doc_string]
-        fn #method_name(mut self) -> Self where Self: std::marker::Sized {
+        fn #method_name(mut self) -> Self {
             let style = self.style();
             #(#field_assignments)*
             self
@@ -163,7 +163,7 @@ fn generate_custom_value_setter(
 
     let method = quote! {
         #[doc = #doc_string]
-        fn #method_name(mut self, length: impl std::clone::Clone + Into<gpui::#length_type>) -> Self where Self: std::marker::Sized {
+        fn #method_name(mut self, length: impl std::clone::Clone + Into<gpui::#length_type>) -> Self {
             let style = self.style();
             #(#field_assignments)*
             self

crates/install_cli2/Cargo.toml 🔗

@@ -14,5 +14,6 @@ test-support = []
 smol.workspace = true
 anyhow.workspace = true
 log.workspace = true
+serde.workspace = true
 gpui = { package = "gpui2", path = "../gpui2" }
 util = { path = "../util" }

crates/install_cli2/src/install_cli2.rs 🔗

@@ -1,10 +1,9 @@
 use anyhow::{anyhow, Result};
-use gpui::AsyncAppContext;
+use gpui::{actions, AsyncAppContext};
 use std::path::Path;
 use util::ResultExt;
 
-// todo!()
-// actions!(cli, [Install]);
+actions!(Install);
 
 pub async fn install_cli(cx: &AsyncAppContext) -> Result<()> {
     let cli_path = cx.update(|cx| cx.path_for_auxiliary_executable("cli"))??;

crates/node_runtime/src/node_runtime.rs 🔗

@@ -2,7 +2,7 @@ use anyhow::{anyhow, bail, Context, Result};
 use async_compression::futures::bufread::GzipDecoder;
 use async_tar::Archive;
 use serde::Deserialize;
-use smol::{fs, io::BufReader, process::Command};
+use smol::{fs, io::BufReader, lock::Mutex, process::Command};
 use std::process::{Output, Stdio};
 use std::{
     env::consts,
@@ -45,14 +45,19 @@ pub trait NodeRuntime: Send + Sync {
 
 pub struct RealNodeRuntime {
     http: Arc<dyn HttpClient>,
+    installation_lock: Mutex<()>,
 }
 
 impl RealNodeRuntime {
     pub fn new(http: Arc<dyn HttpClient>) -> Arc<dyn NodeRuntime> {
-        Arc::new(RealNodeRuntime { http })
+        Arc::new(RealNodeRuntime {
+            http,
+            installation_lock: Mutex::new(()),
+        })
     }
 
     async fn install_if_needed(&self) -> Result<PathBuf> {
+        let _lock = self.installation_lock.lock().await;
         log::info!("Node runtime install_if_needed");
 
         let arch = match consts::ARCH {
@@ -73,6 +78,9 @@ impl RealNodeRuntime {
             .stdin(Stdio::null())
             .stdout(Stdio::null())
             .stderr(Stdio::null())
+            .args(["--cache".into(), node_dir.join("cache")])
+            .args(["--userconfig".into(), node_dir.join("blank_user_npmrc")])
+            .args(["--globalconfig".into(), node_dir.join("blank_global_npmrc")])
             .status()
             .await;
         let valid = matches!(result, Ok(status) if status.success());
@@ -96,6 +104,11 @@ impl RealNodeRuntime {
             archive.unpack(&node_containing_dir).await?;
         }
 
+        // Note: Not in the `if !valid {}` so we can populate these for existing installations
+        _ = fs::create_dir(node_dir.join("cache")).await;
+        _ = fs::write(node_dir.join("blank_user_npmrc"), []).await;
+        _ = fs::write(node_dir.join("blank_global_npmrc"), []).await;
+
         anyhow::Ok(node_dir)
     }
 }
@@ -137,7 +150,17 @@ impl NodeRuntime for RealNodeRuntime {
 
             let mut command = Command::new(node_binary);
             command.env("PATH", env_path);
-            command.arg(npm_file).arg(subcommand).args(args);
+            command.arg(npm_file).arg(subcommand);
+            command.args(["--cache".into(), installation_path.join("cache")]);
+            command.args([
+                "--userconfig".into(),
+                installation_path.join("blank_user_npmrc"),
+            ]);
+            command.args([
+                "--globalconfig".into(),
+                installation_path.join("blank_global_npmrc"),
+            ]);
+            command.args(args);
 
             if let Some(directory) = directory {
                 command.current_dir(directory);

crates/picker2/src/picker2.rs 🔗

@@ -1,10 +1,10 @@
 use editor::Editor;
 use gpui::{
-    div, uniform_list, Component, Div, MouseButton, ParentElement, Render, StatelessInteractive,
-    Styled, Task, UniformListScrollHandle, View, ViewContext, VisualContext, WindowContext,
+    div, prelude::*, uniform_list, Component, Div, MouseButton, Render, Task,
+    UniformListScrollHandle, View, ViewContext, WindowContext,
 };
 use std::{cmp, sync::Arc};
-use ui::{prelude::*, v_stack, Divider, Label, LabelColor};
+use ui::{prelude::*, v_stack, Divider, Label, TextColor};
 
 pub struct Picker<D: PickerDelegate> {
     pub delegate: D,
@@ -58,7 +58,7 @@ impl<D: PickerDelegate> Picker<D> {
         self.editor.update(cx, |editor, cx| editor.focus(cx));
     }
 
-    fn select_next(&mut self, _: &menu::SelectNext, cx: &mut ViewContext<Self>) {
+    pub fn select_next(&mut self, _: &menu::SelectNext, cx: &mut ViewContext<Self>) {
         let count = self.delegate.match_count();
         if count > 0 {
             let index = self.delegate.selected_index();
@@ -98,6 +98,15 @@ impl<D: PickerDelegate> Picker<D> {
         }
     }
 
+    pub fn cycle_selection(&mut self, cx: &mut ViewContext<Self>) {
+        let count = self.delegate.match_count();
+        let index = self.delegate.selected_index();
+        let new_index = if index + 1 == count { 0 } else { index + 1 };
+        self.delegate.set_selected_index(new_index, cx);
+        self.scroll_handle.scroll_to_item(new_index);
+        cx.notify();
+    }
+
     fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
         self.delegate.dismissed(cx);
     }
@@ -137,6 +146,11 @@ impl<D: PickerDelegate> Picker<D> {
         }
     }
 
+    pub fn refresh(&mut self, cx: &mut ViewContext<Self>) {
+        let query = self.editor.read(cx).text(cx);
+        self.update_matches(query, cx);
+    }
+
     pub fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) {
         let update = self.delegate.update_matches(query, cx);
         self.matches_updated(cx);
@@ -165,7 +179,7 @@ impl<D: PickerDelegate> Render for Picker<D> {
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
         div()
-            .context("picker")
+            .key_context("picker")
             .size_full()
             .elevation_2(cx)
             .on_action(Self::select_next)
@@ -224,7 +238,7 @@ impl<D: PickerDelegate> Render for Picker<D> {
                     v_stack().p_1().grow().child(
                         div()
                             .px_1()
-                            .child(Label::new("No matches").color(LabelColor::Muted)),
+                            .child(Label::new("No matches").color(TextColor::Muted)),
                     ),
                 )
             })

crates/project_panel2/Cargo.toml 🔗

@@ -0,0 +1,41 @@
+[package]
+name = "project_panel2"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/project_panel.rs"
+doctest = false
+
+[dependencies]
+context_menu = { path = "../context_menu" }
+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" }
+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 🔗

@@ -0,0 +1,96 @@
+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) -> Arc<str> {
+        maybe!({
+            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()))
+        })
+        .unwrap_or_else(|| Arc::from("".to_string()))
+    }
+
+    pub fn get_folder_icon(expanded: bool, cx: &AppContext) -> Arc<str> {
+        maybe!({
+            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())
+        })
+        .unwrap_or_else(|| Arc::from("".to_string()))
+    }
+
+    pub fn get_chevron_icon(expanded: bool, cx: &AppContext) -> Arc<str> {
+        maybe!({
+            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())
+        })
+        .unwrap_or_else(|| Arc::from("".to_string()))
+    }
+}

crates/project_panel2/src/project_panel.rs 🔗

@@ -8,11 +8,11 @@ use file_associations::FileAssociations;
 
 use anyhow::{anyhow, Result};
 use gpui::{
-    actions, div, px, svg, uniform_list, Action, AppContext, AssetSource, AsyncAppContext,
-    AsyncWindowContext, ClipboardItem, Div, Element, Entity, EventEmitter, FocusEnabled,
-    FocusHandle, Model, ParentElement as _, Pixels, Point, PromptLevel, Render,
-    StatefulInteractive, StatefulInteractivity, Styled, Task, UniformListScrollHandle, View,
-    ViewContext, VisualContext as _, WeakView, WindowContext,
+    actions, div, px, uniform_list, Action, AppContext, AssetSource, AsyncWindowContext,
+    ClipboardItem, Component, Div, EventEmitter, FocusHandle, Focusable, InteractiveComponent,
+    Model, MouseButton, ParentComponent, Pixels, Point, PromptLevel, Render, Stateful,
+    StatefulInteractiveComponent, Styled, Task, UniformListScrollHandle, View, ViewContext,
+    VisualContext as _, WeakView, WindowContext,
 };
 use menu::{Confirm, SelectNext, SelectPrev};
 use project::{
@@ -21,7 +21,6 @@ use project::{
 };
 use project_panel_settings::{ProjectPanelDockPosition, ProjectPanelSettings};
 use serde::{Deserialize, Serialize};
-use smallvec::SmallVec;
 use std::{
     cmp::Ordering,
     collections::{hash_map, HashMap},
@@ -31,9 +30,9 @@ use std::{
     sync::Arc,
 };
 use theme::ActiveTheme as _;
-use ui::{h_stack, v_stack};
+use ui::{h_stack, v_stack, IconElement, Label};
 use unicase::UniCase;
-use util::TryFutureExt;
+use util::{maybe, TryFutureExt};
 use workspace::{
     dock::{DockPosition, PanelEvent},
     Workspace,
@@ -54,8 +53,8 @@ pub struct ProjectPanel {
     edit_state: Option<EditState>,
     filename_editor: View<Editor>,
     clipboard_entry: Option<ClipboardEntry>,
-    dragged_entry_destination: Option<Arc<Path>>,
-    workspace: WeakView<Workspace>,
+    _dragged_entry_destination: Option<Arc<Path>>,
+    _workspace: WeakView<Workspace>,
     has_focus: bool,
     width: Option<f32>,
     pending_serialization: Task<Option<()>>,
@@ -131,31 +130,6 @@ pub fn init_settings(cx: &mut AppContext) {
 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);
-    //     },
-    // );
 }
 
 #[derive(Debug)]
@@ -222,29 +196,25 @@ impl ProjectPanel {
                 editor::Event::BufferEdited | editor::Event::SelectionsChanged { .. } => {
                     this.autoscroll(cx);
                 }
+                editor::Event::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_focus(&filename_editor, |this, _, is_focused, cx| {
-            //     if !is_focused
-            //         && 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 view_id = cx.view().entity_id();
             let mut this = Self {
                 project: project.clone(),
                 fs: workspace.app_state().fs.clone(),
@@ -258,8 +228,8 @@ impl ProjectPanel {
                 filename_editor,
                 clipboard_entry: None,
                 // context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)),
-                dragged_entry_destination: None,
-                workspace: workspace.weak_handle(),
+                _dragged_entry_destination: None,
+                _workspace: workspace.weak_handle(),
                 has_focus: false,
                 width: None,
                 pending_serialization: Task::ready(None),
@@ -311,19 +281,19 @@ 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(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);
+                        }
+                    }
                 }
                 _ => {}
             }
@@ -391,79 +361,80 @@ impl ProjectPanel {
 
     fn deploy_context_menu(
         &mut self,
-        position: Point<Pixels>,
-        entry_id: ProjectEntryId,
-        cx: &mut ViewContext<Self>,
+        _position: Point<Pixels>,
+        _entry_id: ProjectEntryId,
+        _cx: &mut ViewContext<Self>,
     ) {
-        // let project = self.project.read(cx);
-
-        // let worktree_id = if let Some(id) = project.worktree_id_for_entry(entry_id, cx) {
-        //     id
-        // } else {
-        //     return;
-        // };
+        todo!()
+        //     let project = self.project.read(cx);
 
-        // self.selection = Some(Selection {
-        //     worktree_id,
-        //     entry_id,
-        // });
+        //     let worktree_id = if let Some(id) = project.worktree_id_for_entry(entry_id, cx) {
+        //         id
+        //     } else {
+        //         return;
+        //     };
 
-        // 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));
-        //             }));
+        //     self.selection = Some(Selection {
+        //         worktree_id,
+        //         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));
+        //                 }));
+        //             }
         //         }
-        //     }
-        //     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_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_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("Copy Path", CopyPath));
         //         menu_entries.push(ContextMenuItem::action(
-        //             "Search Inside",
-        //             NewSearchInDirectory,
+        //             "Copy Relative Path",
+        //             CopyRelativePath,
         //         ));
-        //     }
 
-        //     menu_entries.push(ContextMenuItem::Separator);
-        //     menu_entries.push(ContextMenuItem::action("Rename", Rename));
-        //     if !is_root {
-        //         menu_entries.push(ContextMenuItem::action("Delete", Delete));
+        //         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_entries.push(ContextMenuItem::Separator);
+        //         menu_entries.push(ContextMenuItem::action("Rename", Rename));
+        //         if !is_root {
+        //             menu_entries.push(ContextMenuItem::action("Delete", Delete));
+        //         }
         //     }
-        // }
 
-        // // self.context_menu.update(cx, |menu, cx| {
-        // //     menu.show(position, AnchorCorner::TopLeft, menu_entries, cx);
-        // // });
+        //     // self.context_menu.update(cx, |menu, cx| {
+        //     //     menu.show(position, AnchorCorner::TopLeft, menu_entries, cx);
+        //     // });
 
-        // cx.notify();
+        //     cx.notify();
     }
 
     fn expand_selected_entry(&mut self, _: &ExpandSelectedEntry, cx: &mut ViewContext<Self>) {
@@ -579,22 +550,18 @@ 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<()>>> {
@@ -800,27 +767,32 @@ impl ProjectPanel {
         }
     }
 
-    fn delete(&mut self, _: &Delete, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
-        let Selection { entry_id, .. } = self.selection?;
-        let path = self.project.read(cx).path_for_entry(entry_id, cx)?.path;
-        let file_name = path.file_name()?;
-
-        let mut answer = cx.prompt(
-            PromptLevel::Info,
-            &format!("Delete {file_name:?}?"),
-            &["Delete", "Cancel"],
-        );
-        Some(cx.spawn(|this, mut cx| async move {
-            if answer.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
-        }))
+    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>) {
@@ -897,8 +869,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;
@@ -942,15 +915,16 @@ impl ProjectPanel {
                 if let Some(task) = self.project.update(cx, |project, cx| {
                     project.rename_entry(clipboard_entry.entry_id(), new_path, cx)
                 }) {
-                    task.detach_and_log_err(cx)
+                    task.detach_and_log_err(cx);
                 }
             } else if let Some(task) = self.project.update(cx, |project, cx| {
                 project.copy_entry(clipboard_entry.entry_id(), new_path, cx)
             }) {
-                task.detach_and_log_err(cx)
+                task.detach_and_log_err(cx);
             }
-        }
-        None
+
+            Some(())
+        });
     }
 
     fn copy_path(&mut self, _: &CopyPath, cx: &mut ViewContext<Self>) {
@@ -977,7 +951,7 @@ impl ProjectPanel {
         }
     }
 
-    fn open_in_terminal(&mut self, _: &OpenInTerminal, cx: &mut ViewContext<Self>) {
+    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();
@@ -1012,36 +986,37 @@ impl ProjectPanel {
         }
     }
 
-    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);
-        }
-    }
+    // todo!()
+    // 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;
@@ -1366,23 +1341,25 @@ impl ProjectPanel {
             .git_status
             .as_ref()
             .map(|status| match status {
-                GitFileStatus::Added => theme.styles.status.created,
-                GitFileStatus::Modified => theme.styles.status.modified,
-                GitFileStatus::Conflict => theme.styles.status.conflict,
+                GitFileStatus::Added => theme.status().created,
+                GitFileStatus::Modified => theme.status().modified,
+                GitFileStatus::Conflict => theme.status().conflict,
             })
-            .unwrap_or(theme.styles.status.info);
+            .unwrap_or(theme.status().info);
 
         h_stack()
             .child(if let Some(icon) = &details.icon {
-                div().child(svg().path(icon.to_string()))
+                div().child(IconElement::from_path(icon.to_string()))
             } else {
                 div()
             })
             .child(
                 if let (Some(editor), true) = (editor, show_editor) {
-                    div().child(editor.clone())
+                    div().w_full().child(editor.clone())
                 } else {
-                    div().child(details.filename.clone())
+                    div()
+                        .text_color(filename_text_color)
+                        .child(Label::new(details.filename.clone()))
                 }
                 .ml_1(),
             )
@@ -1390,21 +1367,29 @@ impl ProjectPanel {
     }
 
     fn render_entry(
+        &self,
         entry_id: ProjectEntryId,
         details: EntryDetails,
-        editor: &View<Editor>,
         // dragged_entry_destination: &mut Option<Arc<Path>>,
-        // theme: &theme::ProjectPanel,
         cx: &mut ViewContext<Self>,
-    ) -> Div<Self, StatefulInteractivity<Self>> {
+    ) -> Stateful<Self, Div<Self>> {
         let kind = details.kind;
         let settings = ProjectPanelSettings::get_global(cx);
         const INDENT_SIZE: Pixels = px(16.0);
         let padding = INDENT_SIZE + details.depth as f32 * px(settings.indent_size);
         let show_editor = details.is_editing && !details.is_processing;
+        let is_selected = self
+            .selection
+            .map_or(false, |selection| selection.entry_id == entry_id);
 
-        Self::render_entry_visual_element(&details, Some(editor), padding, cx)
+        Self::render_entry_visual_element(&details, Some(&self.filename_editor), padding, cx)
             .id(entry_id.to_proto() as usize)
+            .w_full()
+            .cursor_pointer()
+            .when(is_selected, |this| {
+                this.bg(cx.theme().colors().element_selected)
+            })
+            .hover(|style| style.bg(cx.theme().colors().element_hover))
             .on_click(move |this, event, cx| {
                 if !show_editor {
                     if kind.is_dir() {
@@ -1418,38 +1403,51 @@ impl ProjectPanel {
                     }
                 }
             })
-        // .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())
-        // {
+            .on_mouse_down(MouseButton::Right, move |this, event, cx| {
+                this.deploy_context_menu(event.position, entry_id, cx);
+            })
+        // .on_drop::<ProjectEntryId>(|this, event, cx| {
         //     this.move_entry(
         //         *dragged_entry,
         //         entry_id,
         //         matches!(details.kind, EntryKind::File(_)),
         //         cx,
         //     );
-        // }
         // })
     }
 }
 
 impl Render for ProjectPanel {
-    type Element = Div<Self, StatefulInteractivity<Self>, FocusEnabled<Self>>;
-
-    fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> Self::Element {
-        enum ProjectPanel {}
-        let theme = cx.theme();
-        let last_worktree_root_id = self.last_worktree_root_id;
+    type Element = Focusable<Self, Stateful<Self, Div<Self>>>;
 
+    fn render(&mut self, _cx: &mut gpui::ViewContext<Self>) -> Self::Element {
         let has_worktree = self.visible_entries.len() != 0;
 
         if has_worktree {
             div()
                 .id("project-panel")
+                .size_full()
+                .key_context("ProjectPanel")
+                .on_action(Self::select_next)
+                .on_action(Self::select_prev)
+                .on_action(Self::expand_selected_entry)
+                .on_action(Self::collapse_selected_entry)
+                .on_action(Self::collapse_all_entries)
+                .on_action(Self::new_file)
+                .on_action(Self::new_directory)
+                .on_action(Self::rename)
+                .on_action(Self::delete)
+                .on_action(Self::confirm)
+                .on_action(Self::open_file)
+                .on_action(Self::cancel)
+                .on_action(Self::cut)
+                .on_action(Self::copy)
+                .on_action(Self::copy_path)
+                .on_action(Self::copy_relative_path)
+                .on_action(Self::paste)
+                .on_action(Self::reveal_in_finder)
+                .on_action(Self::open_in_terminal)
+                .on_action(Self::new_search_in_directory)
                 .track_focus(&self.focus_handle)
                 .child(
                     uniform_list(
@@ -1459,19 +1457,14 @@ impl Render for ProjectPanel {
                             .map(|(_, worktree_entries)| worktree_entries.len())
                             .sum(),
                         |this: &mut Self, range, cx| {
-                            let mut items = SmallVec::new();
+                            let mut items = Vec::new();
                             this.for_each_visible_entry(range, cx, |id, details, cx| {
-                                items.push(Self::render_entry(
-                                    id,
-                                    details,
-                                    &this.filename_editor,
-                                    // &mut dragged_entry_destination,
-                                    cx,
-                                ));
+                                items.push(this.render_entry(id, details, cx));
                             });
                             items
                         },
                     )
+                    .size_full()
                     .track_scroll(self.list.clone()),
                 )
         } else {
@@ -1573,1296 +1566,1315 @@ impl ClipboardEntry {
     }
 }
 
-// todo!()
-// #[cfg(test)]
-// mod tests {
-//     use super::*;
-//     use gpui::{AnyWindowHandle, TestAppContext, View, WindowHandle};
-//     use pretty_assertions::assert_eq;
-//     use project::FakeFs;
-//     use serde_json::json;
-//     use settings::SettingsStore;
-//     use std::{
-//         collections::HashSet,
-//         path::{Path, PathBuf},
-//         sync::atomic::{self, AtomicUsize},
-//     };
-//     use workspace::{pane, 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))
-//             .root(cx);
-//         let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
-//         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(iterations = 30)]
-//     async fn test_editing_files(cx: &mut gpui::TestAppContext) {
-//         init_test(cx);
-
-//         let fs = FakeFs::new(cx.background());
-//         fs.insert_tree(
-//             "/root1",
-//             json!({
-//                 ".dockerignore": "",
-//                 ".git": {
-//                     "HEAD": "",
-//                 },
-//                 "a": {
-//                     "0": { "q": "", "r": "", "s": "" },
-//                     "1": { "t": "", "u": "" },
-//                     "2": { "v": "", "w": "", "x": "", "y": "" },
-//                 },
-//                 "b": {
-//                     "3": { "Q": "" },
-//                     "4": { "R": "", "S": "", "T": "", "U": "" },
-//                 },
-//                 "C": {
-//                     "5": {},
-//                     "6": { "V": "", "W": "" },
-//                     "7": { "X": "" },
-//                     "8": { "Y": {}, "Z": "" }
-//                 }
-//             }),
-//         )
-//         .await;
-//         fs.insert_tree(
-//             "/root2",
-//             json!({
-//                 "d": {
-//                     "9": ""
-//                 },
-//                 "e": {}
-//             }),
-//         )
-//         .await;
-
-//         let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
-//         let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
-//         let workspace = window.root(cx);
-//         let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
-
-//         select_path(&panel, "root1", cx);
-//         assert_eq!(
-//             visible_entries_as_strings(&panel, 0..10, cx),
-//             &[
-//                 "v root1  <== selected",
-//                 "    > .git",
-//                 "    > a",
-//                 "    > b",
-//                 "    > C",
-//                 "      .dockerignore",
-//                 "v root2",
-//                 "    > d",
-//                 "    > e",
-//             ]
-//         );
-
-//         // Add a file with the root folder selected. The filename editor is placed
-//         // before the first file in the root folder.
-//         panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx));
-//         window.read_with(cx, |cx| {
-//             let panel = panel.read(cx);
-//             assert!(panel.filename_editor.is_focused(cx));
-//         });
-//         assert_eq!(
-//             visible_entries_as_strings(&panel, 0..10, cx),
-//             &[
-//                 "v root1",
-//                 "    > .git",
-//                 "    > a",
-//                 "    > b",
-//                 "    > C",
-//                 "      [EDITOR: '']  <== selected",
-//                 "      .dockerignore",
-//                 "v root2",
-//                 "    > d",
-//                 "    > e",
-//             ]
-//         );
-
-//         let confirm = panel.update(cx, |panel, cx| {
-//             panel
-//                 .filename_editor
-//                 .update(cx, |editor, cx| editor.set_text("the-new-filename", cx));
-//             panel.confirm(&Confirm, cx).unwrap()
-//         });
-//         assert_eq!(
-//             visible_entries_as_strings(&panel, 0..10, cx),
-//             &[
-//                 "v root1",
-//                 "    > .git",
-//                 "    > a",
-//                 "    > b",
-//                 "    > C",
-//                 "      [PROCESSING: '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(&Confirm, 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(&Confirm, 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(&Confirm, 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.focus_self());
-//         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 = 30)]
-//     async fn test_adding_directories_via_file(cx: &mut gpui::TestAppContext) {
-//         init_test(cx);
-
-//         let fs = FakeFs::new(cx.background());
-//         fs.insert_tree(
-//             "/root1",
-//             json!({
-//                 ".dockerignore": "",
-//                 ".git": {
-//                     "HEAD": "",
-//                 },
-//                 "a": {
-//                     "0": { "q": "", "r": "", "s": "" },
-//                     "1": { "t": "", "u": "" },
-//                     "2": { "v": "", "w": "", "x": "", "y": "" },
-//                 },
-//                 "b": {
-//                     "3": { "Q": "" },
-//                     "4": { "R": "", "S": "", "T": "", "U": "" },
-//                 },
-//                 "C": {
-//                     "5": {},
-//                     "6": { "V": "", "W": "" },
-//                     "7": { "X": "" },
-//                     "8": { "Y": {}, "Z": "" }
-//                 }
-//             }),
-//         )
-//         .await;
-//         fs.insert_tree(
-//             "/root2",
-//             json!({
-//                 "d": {
-//                     "9": ""
-//                 },
-//                 "e": {}
-//             }),
-//         )
-//         .await;
-
-//         let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
-//         let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
-//         let workspace = window.root(cx);
-//         let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
-
-//         select_path(&panel, "root1", cx);
-//         assert_eq!(
-//             visible_entries_as_strings(&panel, 0..10, cx),
-//             &[
-//                 "v root1  <== selected",
-//                 "    > .git",
-//                 "    > a",
-//                 "    > b",
-//                 "    > C",
-//                 "      .dockerignore",
-//                 "v root2",
-//                 "    > d",
-//                 "    > e",
-//             ]
-//         );
-
-//         // Add a file with the root folder selected. The filename editor is placed
-//         // before the first file in the root folder.
-//         panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx));
-//         window.read_with(cx, |cx| {
-//             let panel = panel.read(cx);
-//             assert!(panel.filename_editor.is_focused(cx));
-//         });
-//         assert_eq!(
-//             visible_entries_as_strings(&panel, 0..10, cx),
-//             &[
-//                 "v root1",
-//                 "    > .git",
-//                 "    > a",
-//                 "    > b",
-//                 "    > C",
-//                 "      [EDITOR: '']  <== selected",
-//                 "      .dockerignore",
-//                 "v root2",
-//                 "    > d",
-//                 "    > e",
-//             ]
-//         );
-
-//         let confirm = panel.update(cx, |panel, cx| {
-//             panel.filename_editor.update(cx, |editor, cx| {
-//                 editor.set_text("/bdir1/dir2/the-new-filename", cx)
-//             });
-//             panel.confirm(&Confirm, cx).unwrap()
-//         });
-
-//         assert_eq!(
-//             visible_entries_as_strings(&panel, 0..10, cx),
-//             &[
-//                 "v root1",
-//                 "    > .git",
-//                 "    > a",
-//                 "    > b",
-//                 "    > C",
-//                 "      [PROCESSING: '/bdir1/dir2/the-new-filename']  <== selected",
-//                 "      .dockerignore",
-//                 "v root2",
-//                 "    > d",
-//                 "    > e",
-//             ]
-//         );
-
-//         confirm.await.unwrap();
-//         assert_eq!(
-//             visible_entries_as_strings(&panel, 0..13, cx),
-//             &[
-//                 "v root1",
-//                 "    > .git",
-//                 "    > a",
-//                 "    > b",
-//                 "    v bdir1",
-//                 "        v dir2",
-//                 "              the-new-filename  <== selected",
-//                 "    > C",
-//                 "      .dockerignore",
-//                 "v root2",
-//                 "    > d",
-//                 "    > e",
-//             ]
-//         );
-//     }
-
-//     #[gpui::test]
-//     async fn test_copy_paste(cx: &mut gpui::TestAppContext) {
-//         init_test(cx);
-
-//         let fs = FakeFs::new(cx.background());
-//         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))
-//             .root(cx);
-//         let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
-
-//         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.foreground().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.foreground().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.background());
-//         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 window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
-//         let workspace = window.root(cx);
-//         let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
-
-//         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.foreground().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(window, "test/first.rs", cx);
-
-//         submit_deletion(window.into(), &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(window.into(), &workspace, cx);
-
-//         select_path(&panel, "src/test/second.rs", cx);
-//         panel.update(cx, |panel, cx| panel.open_file(&Open, cx));
-//         cx.foreground().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(window, "test/second.rs", cx);
-
-//         window.update(cx, |cx| {
-//             let active_items = workspace
-//                 .read(cx)
-//                 .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));
-//         });
-//         submit_deletion(window.into(), &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(window.into(), &workspace, cx);
-//     }
-
-//     #[gpui::test]
-//     async fn test_create_duplicate_items(cx: &mut gpui::TestAppContext) {
-//         init_test_with_editor(cx);
-
-//         let fs = FakeFs::new(cx.background());
-//         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 window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
-//         let workspace = window.root(cx);
-//         let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
-
-//         select_path(&panel, "src/", cx);
-//         panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
-//         cx.foreground().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));
-//         window.read_with(cx, |cx| {
-//             let panel = panel.read(cx);
-//             assert!(panel.filename_editor.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(&Confirm, 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.foreground().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));
-//         window.read_with(cx, |cx| {
-//             let panel = panel.read(cx);
-//             assert!(panel.filename_editor.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(&Confirm, 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.foreground().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));
-//         window.read_with(cx, |cx| {
-//             let panel = panel.read(cx);
-//             assert!(panel.filename_editor.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(&Confirm, 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_new_search_in_directory_trigger(cx: &mut gpui::TestAppContext) {
-//         init_test_with_editor(cx);
-
-//         let fs = FakeFs::new(cx.background());
-//         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))
-//             .root(cx);
-//         let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
-
-//         let new_search_events_count = Arc::new(AtomicUsize::new(0));
-//         let _subscription = panel.update(cx, |_, cx| {
-//             let subcription_count = Arc::clone(&new_search_events_count);
-//             cx.subscribe(&cx.handle(), move |_, _, event, _| {
-//                 if matches!(event, Event::NewSearchInDirectory { .. }) {
-//                     subcription_count.fetch_add(1, atomic::Ordering::SeqCst);
-//                 }
-//             })
-//         });
-
-//         toggle_expand_dir(&panel, "src/test", cx);
-//         select_path(&panel, "src/test/first.rs", cx);
-//         panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
-//         cx.foreground().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.new_search_in_directory(&NewSearchInDirectory, cx)
-//         });
-//         assert_eq!(
-//             new_search_events_count.load(atomic::Ordering::SeqCst),
-//             0,
-//             "Should not trigger new search in directory when called on a file"
-//         );
-
-//         select_path(&panel, "src/test", cx);
-//         panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx));
-//         cx.foreground().run_until_parked();
-//         assert_eq!(
-//             visible_entries_as_strings(&panel, 0..10, cx),
-//             &[
-//                 "v src",
-//                 "    v test  <== selected",
-//                 "          first.rs",
-//                 "          second.rs",
-//                 "          third.rs"
-//             ]
-//         );
-//         panel.update(cx, |panel, cx| {
-//             panel.new_search_in_directory(&NewSearchInDirectory, cx)
-//         });
-//         assert_eq!(
-//             new_search_events_count.load(atomic::Ordering::SeqCst),
-//             1,
-//             "Should trigger new search in directory when called on a directory"
-//         );
-//     }
-
-//     #[gpui::test]
-//     async fn test_collapse_all_entries(cx: &mut gpui::TestAppContext) {
-//         init_test_with_editor(cx);
-
-//         let fs = FakeFs::new(cx.background());
-//         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))
-//             .root(cx);
-//         let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
-
-//         panel.update(cx, |panel, cx| {
-//             panel.collapse_all_entries(&CollapseAllEntries, cx)
-//         });
-//         cx.foreground().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.foreground().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.background());
-//         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))
-//             .root(cx);
-//         let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
-
-//         // Make a new buffer with no backing file
-//         workspace.update(cx, |workspace, cx| {
-//             Editor::new_file(workspace, &Default::default(), cx)
-//         });
-
-//         // "Save as"" the buffer, creating a new backing file for it
-//         let task = workspace.update(cx, |workspace, cx| {
-//             workspace.save_active_item(workspace::SaveIntent::Save, cx)
-//         });
-
-//         cx.foreground().run_until_parked();
-//         cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/new")));
-//         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))
-//             .unwrap()
-//             .await
-//             .unwrap();
-
-//         cx.foreground().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)
-//             })
-//             .await
-//             .unwrap();
-
-//         cx.foreground().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"]
-//         );
-//     }
-
-//     fn toggle_expand_dir(
-//         panel: &View<ProjectPanel>,
-//         path: impl AsRef<Path>,
-//         cx: &mut TestAppContext,
-//     ) {
-//         let path = path.as_ref();
-//         panel.update(cx, |panel, cx| {
-//             for worktree in panel.project.read(cx).worktrees().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 TestAppContext) {
-//         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(Selection {
-//                         worktree_id: worktree.id(),
-//                         entry_id,
-//                     });
-//                     return;
-//                 }
-//             }
-//             panic!("no worktree for path {:?}", path);
-//         });
-//     }
-
-//     fn visible_entries_as_strings(
-//         panel: &View<ProjectPanel>,
-//         range: Range<usize>,
-//         cx: &mut TestAppContext,
-//     ) -> Vec<String> {
-//         let mut result = Vec::new();
-//         let mut project_entries = HashSet::new();
-//         let mut has_editor = false;
-
-//         panel.update(cx, |panel, cx| {
-//             panel.for_each_visible_entry(range, cx, |project_entry, details, _| {
-//                 if details.is_editing {
-//                     assert!(!has_editor, "duplicate editor entry");
-//                     has_editor = true;
-//                 } else {
-//                     assert!(
-//                         project_entries.insert(project_entry),
-//                         "duplicate project entry {:?} {:?}",
-//                         project_entry,
-//                         details
-//                     );
-//                 }
-
-//                 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.foreground().forbid_parking();
-//         cx.update(|cx| {
-//             cx.set_global(SettingsStore::test(cx));
-//             init_settings(cx);
-//             theme::init(cx);
-//             language::init(cx);
-//             editor::init_settings(cx);
-//             crate::init((), cx);
-//             workspace::init_settings(cx);
-//             client::init_settings(cx);
-//             Project::init_settings(cx);
-//         });
-//     }
-
-//     fn init_test_with_editor(cx: &mut TestAppContext) {
-//         cx.foreground().forbid_parking();
-//         cx.update(|cx| {
-//             let app_state = AppState::test(cx);
-//             theme::init(cx);
-//             init_settings(cx);
-//             language::init(cx);
-//             editor::init(cx);
-//             pane::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_root(cx, |workspace, cx| {
-//             let worktrees = workspace.worktrees(cx).collect::<Vec<_>>();
-//             assert_eq!(worktrees.len(), 1);
-//             let worktree_id = WorktreeId::from_usize(worktrees[0].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"
-//             );
-//         });
-//     }
-
-//     fn submit_deletion(
-//         window: AnyWindowHandle,
-//         panel: &View<ProjectPanel>,
-//         cx: &mut TestAppContext,
-//     ) {
-//         assert!(
-//             !window.has_pending_prompt(cx),
-//             "Should have no prompts before the deletion"
-//         );
-//         panel.update(cx, |panel, cx| {
-//             panel
-//                 .delete(&Delete, cx)
-//                 .expect("Deletion start")
-//                 .detach_and_log_err(cx);
-//         });
-//         assert!(
-//             window.has_pending_prompt(cx),
-//             "Should have a prompt after the deletion"
-//         );
-//         window.simulate_prompt_answer(0, cx);
-//         assert!(
-//             !window.has_pending_prompt(cx),
-//             "Should have no prompts after prompt was replied to"
-//         );
-//         cx.foreground().run_until_parked();
-//     }
-
-//     fn ensure_no_open_items_and_panes(
-//         window: AnyWindowHandle,
-//         workspace: &View<Workspace>,
-//         cx: &mut TestAppContext,
-//     ) {
-//         assert!(
-//             !window.has_pending_prompt(cx),
-//             "Should have no prompts after deletion operation closes the file"
-//         );
-//         window.read_with(cx, |cx| {
-//             let open_project_paths = workspace
-//                 .read(cx)
-//                 .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:?}"
-//             );
-//         });
-//     }
-// }
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use gpui::{TestAppContext, View, VisualTestContext, WindowHandle};
+    use pretty_assertions::assert_eq;
+    use project::FakeFs;
+    use serde_json::json;
+    use settings::SettingsStore;
+    use std::{
+        collections::HashSet,
+        path::{Path, PathBuf},
+        sync::atomic::{self, AtomicUsize},
+    };
+    use workspace::{pane, 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(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| ProjectPanel::new(workspace, cx))
+            .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| ProjectPanel::new(workspace, cx))
+            .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| ProjectPanel::new(workspace, cx))
+            .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_new_search_in_directory_trigger(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();
+
+        let new_search_events_count = Arc::new(AtomicUsize::new(0));
+        let _subscription = panel.update(cx, |_, cx| {
+            let subcription_count = Arc::clone(&new_search_events_count);
+            let view = cx.view().clone();
+            cx.subscribe(&view, move |_, _, event, _| {
+                if matches!(event, Event::NewSearchInDirectory { .. }) {
+                    subcription_count.fetch_add(1, atomic::Ordering::SeqCst);
+                }
+            })
+        });
+
+        toggle_expand_dir(&panel, "src/test", cx);
+        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.new_search_in_directory(&NewSearchInDirectory, cx)
+        });
+        assert_eq!(
+            new_search_events_count.load(atomic::Ordering::SeqCst),
+            0,
+            "Should not trigger new search in directory when called on a file"
+        );
+
+        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",
+                "    v test  <== selected",
+                "          first.rs",
+                "          second.rs",
+                "          third.rs"
+            ]
+        );
+        panel.update(cx, |panel, cx| {
+            panel.new_search_in_directory(&NewSearchInDirectory, cx)
+        });
+        assert_eq!(
+            new_search_events_count.load(atomic::Ordering::SeqCst),
+            1,
+            "Should trigger new search in directory when called on a directory"
+        );
+    }
+
+    #[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"]
+        );
+    }
+
+    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(Selection {
+                        worktree_id: worktree.id(),
+                        entry_id,
+                    });
+                    return;
+                }
+            }
+            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(cx);
+            language::init(cx);
+            editor::init_settings(cx);
+            crate::init((), cx);
+            workspace::init_settings(cx);
+            client::init_settings(cx);
+            Project::init_settings(cx);
+        });
+    }
+
+    fn init_test_with_editor(cx: &mut TestAppContext) {
+        cx.update(|cx| {
+            let app_state = AppState::test(cx);
+            theme::init(cx);
+            init_settings(cx);
+            language::init(cx);
+            editor::init(cx);
+            pane::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 🔗

@@ -0,0 +1,45 @@
+use anyhow;
+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: f32,
+    pub dock: ProjectPanelDockPosition,
+    pub file_icons: bool,
+    pub folder_icons: bool,
+    pub git_status: bool,
+    pub indent_size: f32,
+}
+
+#[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>,
+}
+
+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/settings2/src/keymap_file.rs 🔗

@@ -9,7 +9,7 @@ use schemars::{
 };
 use serde::Deserialize;
 use serde_json::Value;
-use util::{asset_str, ResultExt};
+use util::asset_str;
 
 #[derive(Debug, Deserialize, Default, Clone, JsonSchema)]
 #[serde(transparent)]
@@ -86,7 +86,9 @@ impl KeymapFile {
                             "invalid binding value for keystroke {keystroke}, context {context:?}"
                         )
                     })
-                    .log_err()
+                    // todo!()
+                    .ok()
+                    // .log_err()
                     .map(|action| KeyBinding::load(&keystroke, action, context.as_deref()))
                 })
                 .collect::<Result<Vec<_>>>()?;

crates/storybook2/src/stories/colors.rs 🔗

@@ -1,5 +1,5 @@
 use crate::story::Story;
-use gpui::{px, Div, Render};
+use gpui::{prelude::*, px, Div, Render};
 use theme2::{default_color_scales, ColorScaleStep};
 use ui::prelude::*;
 

crates/storybook2/src/stories/focus.rs 🔗

@@ -1,6 +1,5 @@
 use gpui::{
-    actions, div, Div, FocusHandle, Focusable, FocusableKeyDispatch, KeyBinding, ParentElement,
-    Render, StatefulInteractivity, StatelessInteractive, Styled, View, VisualContext,
+    actions, div, prelude::*, Div, FocusHandle, Focusable, KeyBinding, Render, Stateful, View,
     WindowContext,
 };
 use theme2::ActiveTheme;
@@ -28,7 +27,7 @@ impl FocusStory {
 }
 
 impl Render for FocusStory {
-    type Element = Div<Self, StatefulInteractivity<Self>, FocusableKeyDispatch<Self>>;
+    type Element = Focusable<Self, Stateful<Self, Div<Self>>>;
 
     fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> Self::Element {
         let theme = cx.theme();
@@ -42,7 +41,7 @@ impl Render for FocusStory {
         div()
             .id("parent")
             .focusable()
-            .context("parent")
+            .key_context("parent")
             .on_action(|_, action: &ActionA, cx| {
                 println!("Action A dispatched on parent");
             })
@@ -62,7 +61,7 @@ impl Render for FocusStory {
             .child(
                 div()
                     .track_focus(&self.child_1_focus)
-                    .context("child-1")
+                    .key_context("child-1")
                     .on_action(|_, action: &ActionB, cx| {
                         println!("Action B dispatched on child 1 during");
                     })
@@ -82,7 +81,7 @@ impl Render for FocusStory {
             .child(
                 div()
                     .track_focus(&self.child_2_focus)
-                    .context("child-2")
+                    .key_context("child-2")
                     .on_action(|_, action: &ActionC, cx| {
                         println!("Action C dispatched on child 2");
                     })

crates/storybook2/src/stories/kitchen_sink.rs 🔗

@@ -1,5 +1,5 @@
 use crate::{story::Story, story_selector::ComponentStory};
-use gpui::{Div, Render, StatefulInteractivity, View, VisualContext};
+use gpui::{prelude::*, Div, Render, Stateful, View};
 use strum::IntoEnumIterator;
 use ui::prelude::*;
 
@@ -12,7 +12,7 @@ impl KitchenSinkStory {
 }
 
 impl Render for KitchenSinkStory {
-    type Element = Div<Self, StatefulInteractivity<Self>>;
+    type Element = Stateful<Self, Div<Self>>;
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
         let component_stories = ComponentStory::iter()

crates/storybook2/src/stories/picker.rs 🔗

@@ -1,11 +1,7 @@
-use std::sync::Arc;
-
 use fuzzy::StringMatchCandidate;
-use gpui::{
-    div, Component, Div, KeyBinding, ParentElement, Render, StatelessInteractive, Styled, Task,
-    View, VisualContext, WindowContext,
-};
+use gpui::{div, prelude::*, Div, KeyBinding, Render, Styled, Task, View, WindowContext};
 use picker::{Picker, PickerDelegate};
+use std::sync::Arc;
 use theme2::ActiveTheme;
 
 pub struct PickerStory {

crates/storybook2/src/stories/scroll.rs 🔗

@@ -1,7 +1,4 @@
-use gpui::{
-    div, px, Component, Div, ParentElement, Render, SharedString, StatefulInteractivity, Styled,
-    View, VisualContext, WindowContext,
-};
+use gpui::{div, prelude::*, px, Div, Render, SharedString, Stateful, Styled, View, WindowContext};
 use theme2::ActiveTheme;
 
 pub struct ScrollStory;
@@ -13,7 +10,7 @@ impl ScrollStory {
 }
 
 impl Render for ScrollStory {
-    type Element = Div<Self, StatefulInteractivity<Self>>;
+    type Element = Stateful<Self, Div<Self>>;
 
     fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> Self::Element {
         let theme = cx.theme();

crates/storybook2/src/stories/text.rs 🔗

@@ -1,4 +1,4 @@
-use gpui::{div, white, Div, ParentElement, Render, Styled, View, VisualContext, WindowContext};
+use gpui::{div, white, Div, ParentComponent, Render, Styled, View, VisualContext, WindowContext};
 
 pub struct TextStory;
 

crates/theme2/src/one_themes.rs 🔗

@@ -35,6 +35,7 @@ pub(crate) fn one_dark() -> Theme {
         id: "one_dark".to_string(),
         name: "One Dark".into(),
         appearance: Appearance::Dark,
+
         styles: ThemeStyles {
             system: SystemColors::default(),
             colors: ThemeColors {

crates/theme2/src/settings.rs 🔗

@@ -19,6 +19,7 @@ const MIN_LINE_HEIGHT: f32 = 1.0;
 #[derive(Clone)]
 pub struct ThemeSettings {
     pub ui_font_size: Pixels,
+    pub ui_font: Font,
     pub buffer_font: Font,
     pub buffer_font_size: Pixels,
     pub buffer_line_height: BufferLineHeight,
@@ -120,6 +121,12 @@ impl settings::Settings for ThemeSettings {
 
         let mut this = Self {
             ui_font_size: defaults.ui_font_size.unwrap_or(16.).into(),
+            ui_font: Font {
+                family: "Helvetica".into(),
+                features: Default::default(),
+                weight: Default::default(),
+                style: Default::default(),
+            },
             buffer_font: Font {
                 family: defaults.buffer_font_family.clone().unwrap().into(),
                 features: defaults.buffer_font_features.clone().unwrap(),

crates/theme2/src/story.rs 🔗

@@ -1,4 +1,4 @@
-use gpui::{div, Component, Div, ParentElement, Styled, ViewContext};
+use gpui::{div, Component, Div, ParentComponent, Styled, ViewContext};
 
 use crate::ActiveTheme;
 

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

@@ -143,7 +143,7 @@ use crate::{amber, blue, jade, lime, orange, pink, purple, red};
 mod stories {
     use super::*;
     use crate::{ActiveTheme, Story};
-    use gpui::{div, img, px, Div, ParentElement, Render, Styled, ViewContext};
+    use gpui::{div, img, px, Div, ParentComponent, Render, Styled, ViewContext};
 
     pub struct PlayerStory;
 

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

@@ -1,11 +1,9 @@
 use std::sync::Arc;
 
-use gpui::{div, DefiniteLength, Hsla, MouseButton, WindowContext};
+use gpui::{div, DefiniteLength, Hsla, MouseButton, StatefulInteractiveComponent, WindowContext};
 
-use crate::{
-    h_stack, prelude::*, Icon, IconButton, IconColor, IconElement, Label, LabelColor,
-    LineHeightStyle,
-};
+use crate::prelude::*;
+use crate::{h_stack, Icon, IconButton, IconElement, Label, LineHeightStyle, TextColor};
 
 /// Provides the flexibility to use either a standard
 /// button or an icon button in a given context.
@@ -87,7 +85,7 @@ pub struct Button<V: 'static> {
     label: SharedString,
     variant: ButtonVariant,
     width: Option<DefiniteLength>,
-    color: Option<LabelColor>,
+    color: Option<TextColor>,
 }
 
 impl<V: 'static> Button<V> {
@@ -141,14 +139,14 @@ impl<V: 'static> Button<V> {
         self
     }
 
-    pub fn color(mut self, color: Option<LabelColor>) -> Self {
+    pub fn color(mut self, color: Option<TextColor>) -> Self {
         self.color = color;
         self
     }
 
-    pub fn label_color(&self, color: Option<LabelColor>) -> LabelColor {
+    pub fn label_color(&self, color: Option<TextColor>) -> TextColor {
         if self.disabled {
-            LabelColor::Disabled
+            TextColor::Disabled
         } else if let Some(color) = color {
             color
         } else {
@@ -156,21 +154,21 @@ impl<V: 'static> Button<V> {
         }
     }
 
-    fn render_label(&self, color: LabelColor) -> Label {
+    fn render_label(&self, color: TextColor) -> Label {
         Label::new(self.label.clone())
             .color(color)
             .line_height_style(LineHeightStyle::UILabel)
     }
 
-    fn render_icon(&self, icon_color: IconColor) -> Option<IconElement> {
+    fn render_icon(&self, icon_color: TextColor) -> Option<IconElement> {
         self.icon.map(|i| IconElement::new(i).color(icon_color))
     }
 
     pub fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
         let (icon_color, label_color) = match (self.disabled, self.color) {
-            (true, _) => (IconColor::Disabled, LabelColor::Disabled),
-            (_, None) => (IconColor::Default, LabelColor::Default),
-            (_, Some(color)) => (IconColor::from(color), color),
+            (true, _) => (TextColor::Disabled, TextColor::Disabled),
+            (_, None) => (TextColor::Default, TextColor::Default),
+            (_, Some(color)) => (TextColor::from(color), color),
         };
 
         let mut button = h_stack()
@@ -240,7 +238,7 @@ pub use stories::*;
 #[cfg(feature = "stories")]
 mod stories {
     use super::*;
-    use crate::{h_stack, v_stack, LabelColor, Story};
+    use crate::{h_stack, v_stack, Story, TextColor};
     use gpui::{rems, Div, Render};
     use strum::IntoEnumIterator;
 
@@ -265,7 +263,7 @@ mod stories {
                                     v_stack()
                                         .gap_1()
                                         .child(
-                                            Label::new(state.to_string()).color(LabelColor::Muted),
+                                            Label::new(state.to_string()).color(TextColor::Muted),
                                         )
                                         .child(
                                             Button::new("Label").variant(ButtonVariant::Ghost), // .state(state),
@@ -276,7 +274,7 @@ mod stories {
                                     v_stack()
                                         .gap_1()
                                         .child(
-                                            Label::new(state.to_string()).color(LabelColor::Muted),
+                                            Label::new(state.to_string()).color(TextColor::Muted),
                                         )
                                         .child(
                                             Button::new("Label")
@@ -290,7 +288,7 @@ mod stories {
                                     v_stack()
                                         .gap_1()
                                         .child(
-                                            Label::new(state.to_string()).color(LabelColor::Muted),
+                                            Label::new(state.to_string()).color(TextColor::Muted),
                                         )
                                         .child(
                                             Button::new("Label")
@@ -307,7 +305,7 @@ mod stories {
                                     v_stack()
                                         .gap_1()
                                         .child(
-                                            Label::new(state.to_string()).color(LabelColor::Muted),
+                                            Label::new(state.to_string()).color(TextColor::Muted),
                                         )
                                         .child(
                                             Button::new("Label").variant(ButtonVariant::Filled), // .state(state),
@@ -318,7 +316,7 @@ mod stories {
                                     v_stack()
                                         .gap_1()
                                         .child(
-                                            Label::new(state.to_string()).color(LabelColor::Muted),
+                                            Label::new(state.to_string()).color(TextColor::Muted),
                                         )
                                         .child(
                                             Button::new("Label")
@@ -332,7 +330,7 @@ mod stories {
                                     v_stack()
                                         .gap_1()
                                         .child(
-                                            Label::new(state.to_string()).color(LabelColor::Muted),
+                                            Label::new(state.to_string()).color(TextColor::Muted),
                                         )
                                         .child(
                                             Button::new("Label")
@@ -349,7 +347,7 @@ mod stories {
                                     v_stack()
                                         .gap_1()
                                         .child(
-                                            Label::new(state.to_string()).color(LabelColor::Muted),
+                                            Label::new(state.to_string()).color(TextColor::Muted),
                                         )
                                         .child(
                                             Button::new("Label")
@@ -363,7 +361,7 @@ mod stories {
                                     v_stack()
                                         .gap_1()
                                         .child(
-                                            Label::new(state.to_string()).color(LabelColor::Muted),
+                                            Label::new(state.to_string()).color(TextColor::Muted),
                                         )
                                         .child(
                                             Button::new("Label")
@@ -379,7 +377,7 @@ mod stories {
                                     v_stack()
                                         .gap_1()
                                         .child(
-                                            Label::new(state.to_string()).color(LabelColor::Muted),
+                                            Label::new(state.to_string()).color(TextColor::Muted),
                                         )
                                         .child(
                                             Button::new("Label")

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

@@ -1,12 +1,8 @@
+use gpui::{div, prelude::*, Component, ElementId, Styled, ViewContext};
 use std::sync::Arc;
-
-use gpui::{
-    div, Component, ElementId, ParentElement, StatefulInteractive, StatelessInteractive, Styled,
-    ViewContext,
-};
 use theme2::ActiveTheme;
 
-use crate::{Icon, IconColor, IconElement, Selection};
+use crate::{Icon, IconElement, Selection, TextColor};
 
 pub type CheckHandler<V> = Arc<dyn Fn(Selection, &mut V, &mut ViewContext<V>) + Send + Sync>;
 
@@ -58,9 +54,9 @@ impl<V: 'static> Checkbox<V> {
                         .color(
                             // If the checkbox is disabled we change the color of the icon.
                             if self.disabled {
-                                IconColor::Disabled
+                                TextColor::Disabled
                             } else {
-                                IconColor::Selected
+                                TextColor::Selected
                             },
                         ),
                 )
@@ -73,9 +69,9 @@ impl<V: 'static> Checkbox<V> {
                         .color(
                             // If the checkbox is disabled we change the color of the icon.
                             if self.disabled {
-                                IconColor::Disabled
+                                TextColor::Disabled
                             } else {
-                                IconColor::Selected
+                                TextColor::Selected
                             },
                         ),
                 )

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

@@ -23,6 +23,6 @@ pub fn elevated_surface<V: 'static>(level: ElevationIndex, cx: &mut ViewContext<
         .shadow(level.shadow())
 }
 
-pub fn modal<V>(cx: &mut ViewContext<V>) -> Div<V> {
+pub fn modal<V: 'static>(cx: &mut ViewContext<V>) -> Div<V> {
     elevated_surface(ElevationIndex::ModalSurface, cx)
 }

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

@@ -1,7 +1,7 @@
-use gpui::{rems, svg, Hsla};
+use gpui::{rems, svg};
 use strum::EnumIter;
 
-use crate::{prelude::*, LabelColor};
+use crate::prelude::*;
 
 #[derive(Default, PartialEq, Copy, Clone)]
 pub enum IconSize {
@@ -10,70 +10,6 @@ pub enum IconSize {
     Medium,
 }
 
-#[derive(Default, PartialEq, Copy, Clone)]
-pub enum IconColor {
-    #[default]
-    Default,
-    Accent,
-    Created,
-    Deleted,
-    Disabled,
-    Error,
-    Hidden,
-    Info,
-    Modified,
-    Muted,
-    Placeholder,
-    Player(u32),
-    Selected,
-    Success,
-    Warning,
-}
-
-impl IconColor {
-    pub fn color(self, cx: &WindowContext) -> Hsla {
-        match self {
-            IconColor::Default => cx.theme().colors().icon,
-            IconColor::Muted => cx.theme().colors().icon_muted,
-            IconColor::Disabled => cx.theme().colors().icon_disabled,
-            IconColor::Placeholder => cx.theme().colors().icon_placeholder,
-            IconColor::Accent => cx.theme().colors().icon_accent,
-            IconColor::Error => cx.theme().status().error,
-            IconColor::Warning => cx.theme().status().warning,
-            IconColor::Success => cx.theme().status().success,
-            IconColor::Info => cx.theme().status().info,
-            IconColor::Selected => cx.theme().colors().icon_accent,
-            IconColor::Player(i) => cx.theme().styles.player.0[i.clone() as usize].cursor,
-            IconColor::Created => cx.theme().status().created,
-            IconColor::Modified => cx.theme().status().modified,
-            IconColor::Deleted => cx.theme().status().deleted,
-            IconColor::Hidden => cx.theme().status().hidden,
-        }
-    }
-}
-
-impl From<LabelColor> for IconColor {
-    fn from(label: LabelColor) -> Self {
-        match label {
-            LabelColor::Default => IconColor::Default,
-            LabelColor::Muted => IconColor::Muted,
-            LabelColor::Disabled => IconColor::Disabled,
-            LabelColor::Placeholder => IconColor::Placeholder,
-            LabelColor::Accent => IconColor::Accent,
-            LabelColor::Error => IconColor::Error,
-            LabelColor::Warning => IconColor::Warning,
-            LabelColor::Success => IconColor::Success,
-            LabelColor::Info => IconColor::Info,
-            LabelColor::Selected => IconColor::Selected,
-            LabelColor::Player(i) => IconColor::Player(i),
-            LabelColor::Created => IconColor::Created,
-            LabelColor::Modified => IconColor::Modified,
-            LabelColor::Deleted => IconColor::Deleted,
-            LabelColor::Hidden => IconColor::Hidden,
-        }
-    }
-}
-
 #[derive(Debug, PartialEq, Copy, Clone, EnumIter)]
 pub enum Icon {
     Ai,
@@ -193,21 +129,29 @@ impl Icon {
 
 #[derive(Component)]
 pub struct IconElement {
-    icon: Icon,
-    color: IconColor,
+    path: SharedString,
+    color: TextColor,
     size: IconSize,
 }
 
 impl IconElement {
     pub fn new(icon: Icon) -> Self {
         Self {
-            icon,
-            color: IconColor::default(),
+            path: icon.path().into(),
+            color: TextColor::default(),
+            size: IconSize::default(),
+        }
+    }
+
+    pub fn from_path(path: impl Into<SharedString>) -> Self {
+        Self {
+            path: path.into(),
+            color: TextColor::default(),
             size: IconSize::default(),
         }
     }
 
-    pub fn color(mut self, color: IconColor) -> Self {
+    pub fn color(mut self, color: TextColor) -> Self {
         self.color = color;
         self
     }
@@ -226,7 +170,7 @@ impl IconElement {
         svg()
             .size(svg_size)
             .flex_none()
-            .path(self.icon.path())
+            .path(self.path)
             .text_color(self.color.color(cx))
     }
 }

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

@@ -1,5 +1,5 @@
-use crate::{h_stack, prelude::*, ClickHandler, Icon, IconColor, IconElement, TextTooltip};
-use gpui::{MouseButton, VisualContext};
+use crate::{h_stack, prelude::*, ClickHandler, Icon, IconElement, TextTooltip};
+use gpui::{prelude::*, MouseButton, VisualContext};
 use std::sync::Arc;
 
 struct IconButtonHandlers<V: 'static> {
@@ -16,7 +16,7 @@ impl<V: 'static> Default for IconButtonHandlers<V> {
 pub struct IconButton<V: 'static> {
     id: ElementId,
     icon: Icon,
-    color: IconColor,
+    color: TextColor,
     variant: ButtonVariant,
     state: InteractionState,
     tooltip: Option<SharedString>,
@@ -28,7 +28,7 @@ impl<V: 'static> IconButton<V> {
         Self {
             id: id.into(),
             icon,
-            color: IconColor::default(),
+            color: TextColor::default(),
             variant: ButtonVariant::default(),
             state: InteractionState::default(),
             tooltip: None,
@@ -41,7 +41,7 @@ impl<V: 'static> IconButton<V> {
         self
     }
 
-    pub fn color(mut self, color: IconColor) -> Self {
+    pub fn color(mut self, color: TextColor) -> Self {
         self.color = color;
         self
     }
@@ -71,7 +71,7 @@ impl<V: 'static> IconButton<V> {
 
     fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
         let icon_color = match (self.state, self.color) {
-            (InteractionState::Disabled, _) => IconColor::Disabled,
+            (InteractionState::Disabled, _) => TextColor::Disabled,
             _ => self.color,
         };
 

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

@@ -1,6 +1,5 @@
-use crate::prelude::*;
-use crate::Label;
-use crate::LabelColor;
+use crate::{prelude::*, Label};
+use gpui::prelude::*;
 
 #[derive(Default, PartialEq)]
 pub enum InputVariant {
@@ -71,15 +70,15 @@ impl Input {
         };
 
         let placeholder_label = Label::new(self.placeholder.clone()).color(if self.disabled {
-            LabelColor::Disabled
+            TextColor::Disabled
         } else {
-            LabelColor::Placeholder
+            TextColor::Placeholder
         });
 
         let label = Label::new(self.value.clone()).color(if self.disabled {
-            LabelColor::Disabled
+            TextColor::Disabled
         } else {
-            LabelColor::Default
+            TextColor::Default
         });
 
         div()

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

@@ -3,7 +3,7 @@ use strum::EnumIter;
 
 use crate::prelude::*;
 
-#[derive(Component)]
+#[derive(Component, Clone)]
 pub struct KeyBinding {
     /// A keybinding consists of a key and a set of modifier keys.
     /// More then one keybinding produces a chord.

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

@@ -3,8 +3,15 @@ use gpui::{relative, Hsla, Text, TextRun, WindowContext};
 use crate::prelude::*;
 use crate::styled_ext::StyledExt;
 
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
+pub enum LabelSize {
+    #[default]
+    Default,
+    Small,
+}
+
 #[derive(Default, PartialEq, Copy, Clone)]
-pub enum LabelColor {
+pub enum TextColor {
     #[default]
     Default,
     Accent,
@@ -23,24 +30,24 @@ pub enum LabelColor {
     Warning,
 }
 
-impl LabelColor {
-    pub fn hsla(&self, cx: &WindowContext) -> Hsla {
+impl TextColor {
+    pub fn color(&self, cx: &WindowContext) -> Hsla {
         match self {
-            LabelColor::Default => cx.theme().colors().text,
-            LabelColor::Muted => cx.theme().colors().text_muted,
-            LabelColor::Created => cx.theme().status().created,
-            LabelColor::Modified => cx.theme().status().modified,
-            LabelColor::Deleted => cx.theme().status().deleted,
-            LabelColor::Disabled => cx.theme().colors().text_disabled,
-            LabelColor::Hidden => cx.theme().status().hidden,
-            LabelColor::Info => cx.theme().status().info,
-            LabelColor::Placeholder => cx.theme().colors().text_placeholder,
-            LabelColor::Accent => cx.theme().colors().text_accent,
-            LabelColor::Player(i) => cx.theme().styles.player.0[i.clone() as usize].cursor,
-            LabelColor::Error => cx.theme().status().error,
-            LabelColor::Selected => cx.theme().colors().text_accent,
-            LabelColor::Success => cx.theme().status().success,
-            LabelColor::Warning => cx.theme().status().warning,
+            TextColor::Default => cx.theme().colors().text,
+            TextColor::Muted => cx.theme().colors().text_muted,
+            TextColor::Created => cx.theme().status().created,
+            TextColor::Modified => cx.theme().status().modified,
+            TextColor::Deleted => cx.theme().status().deleted,
+            TextColor::Disabled => cx.theme().colors().text_disabled,
+            TextColor::Hidden => cx.theme().status().hidden,
+            TextColor::Info => cx.theme().status().info,
+            TextColor::Placeholder => cx.theme().colors().text_placeholder,
+            TextColor::Accent => cx.theme().colors().text_accent,
+            TextColor::Player(i) => cx.theme().styles.player.0[i.clone() as usize].cursor,
+            TextColor::Error => cx.theme().status().error,
+            TextColor::Selected => cx.theme().colors().text_accent,
+            TextColor::Success => cx.theme().status().success,
+            TextColor::Warning => cx.theme().status().warning,
         }
     }
 }
@@ -56,8 +63,9 @@ pub enum LineHeightStyle {
 #[derive(Component)]
 pub struct Label {
     label: SharedString,
+    size: LabelSize,
     line_height_style: LineHeightStyle,
-    color: LabelColor,
+    color: TextColor,
     strikethrough: bool,
 }
 
@@ -65,13 +73,19 @@ impl Label {
     pub fn new(label: impl Into<SharedString>) -> Self {
         Self {
             label: label.into(),
+            size: LabelSize::Default,
             line_height_style: LineHeightStyle::default(),
-            color: LabelColor::Default,
+            color: TextColor::Default,
             strikethrough: false,
         }
     }
 
-    pub fn color(mut self, color: LabelColor) -> Self {
+    pub fn size(mut self, size: LabelSize) -> Self {
+        self.size = size;
+        self
+    }
+
+    pub fn color(mut self, color: TextColor) -> Self {
         self.color = color;
         self
     }
@@ -95,14 +109,17 @@ impl Label {
                         .top_1_2()
                         .w_full()
                         .h_px()
-                        .bg(LabelColor::Hidden.hsla(cx)),
+                        .bg(TextColor::Hidden.color(cx)),
                 )
             })
-            .text_ui()
+            .map(|this| match self.size {
+                LabelSize::Default => this.text_ui(),
+                LabelSize::Small => this.text_ui_sm(),
+            })
             .when(self.line_height_style == LineHeightStyle::UILabel, |this| {
                 this.line_height(relative(1.))
             })
-            .text_color(self.color.hsla(cx))
+            .text_color(self.color.color(cx))
             .child(self.label.clone())
     }
 }
@@ -110,7 +127,8 @@ impl Label {
 #[derive(Component)]
 pub struct HighlightedLabel {
     label: SharedString,
-    color: LabelColor,
+    size: LabelSize,
+    color: TextColor,
     highlight_indices: Vec<usize>,
     strikethrough: bool,
 }
@@ -121,13 +139,19 @@ impl HighlightedLabel {
     pub fn new(label: impl Into<SharedString>, highlight_indices: Vec<usize>) -> Self {
         Self {
             label: label.into(),
-            color: LabelColor::Default,
+            size: LabelSize::Default,
+            color: TextColor::Default,
             highlight_indices,
             strikethrough: false,
         }
     }
 
-    pub fn color(mut self, color: LabelColor) -> Self {
+    pub fn size(mut self, size: LabelSize) -> Self {
+        self.size = size;
+        self
+    }
+
+    pub fn color(mut self, color: TextColor) -> Self {
         self.color = color;
         self
     }
@@ -146,7 +170,7 @@ impl HighlightedLabel {
         let mut runs: Vec<TextRun> = Vec::new();
 
         for (char_ix, char) in self.label.char_indices() {
-            let mut color = self.color.hsla(cx);
+            let mut color = self.color.color(cx);
 
             if let Some(highlight_ix) = highlight_indices.peek() {
                 if char_ix == *highlight_ix {
@@ -183,9 +207,13 @@ impl HighlightedLabel {
                         .my_auto()
                         .w_full()
                         .h_px()
-                        .bg(LabelColor::Hidden.hsla(cx)),
+                        .bg(TextColor::Hidden.color(cx)),
                 )
             })
+            .map(|this| match self.size {
+                LabelSize::Default => this.text_ui(),
+                LabelSize::Small => this.text_ui_sm(),
+            })
             .child(Text::styled(self.label, runs))
     }
 }

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

@@ -1,11 +1,11 @@
 use gpui::div;
 
+use crate::prelude::*;
 use crate::settings::user_settings;
 use crate::{
-    disclosure_control, h_stack, v_stack, Avatar, Icon, IconColor, IconElement, IconSize, Label,
-    LabelColor, Toggle,
+    disclosure_control, h_stack, v_stack, Avatar, GraphicSlot, Icon, IconElement, IconSize, Label,
+    TextColor, Toggle,
 };
-use crate::{prelude::*, GraphicSlot};
 
 #[derive(Clone, Copy, Default, Debug, PartialEq)]
 pub enum ListItemVariant {
@@ -68,7 +68,7 @@ impl ListHeader {
                     .items_center()
                     .children(icons.into_iter().map(|i| {
                         IconElement::new(i)
-                            .color(IconColor::Muted)
+                            .color(TextColor::Muted)
                             .size(IconSize::Small)
                     })),
             ),
@@ -106,10 +106,10 @@ impl ListHeader {
                                     .items_center()
                                     .children(self.left_icon.map(|i| {
                                         IconElement::new(i)
-                                            .color(IconColor::Muted)
+                                            .color(TextColor::Muted)
                                             .size(IconSize::Small)
                                     }))
-                                    .child(Label::new(self.label.clone()).color(LabelColor::Muted)),
+                                    .child(Label::new(self.label.clone()).color(TextColor::Muted)),
                             )
                             .child(disclosure_control),
                     )
@@ -157,10 +157,10 @@ impl ListSubHeader {
                         .items_center()
                         .children(self.left_icon.map(|i| {
                             IconElement::new(i)
-                                .color(IconColor::Muted)
+                                .color(TextColor::Muted)
                                 .size(IconSize::Small)
                         }))
-                        .child(Label::new(self.label.clone()).color(LabelColor::Muted)),
+                        .child(Label::new(self.label.clone()).color(TextColor::Muted)),
                 ),
         )
     }
@@ -291,7 +291,7 @@ impl ListEntry {
                 h_stack().child(
                     IconElement::new(i)
                         .size(IconSize::Small)
-                        .color(IconColor::Muted),
+                        .color(TextColor::Muted),
                 ),
             ),
             Some(GraphicSlot::Avatar(src)) => Some(h_stack().child(Avatar::new(src))),
@@ -394,7 +394,7 @@ impl List {
             (false, _) => div().children(self.items),
             (true, Toggle::Toggled(false)) => div(),
             (true, _) => {
-                div().child(Label::new(self.empty_message.clone()).color(LabelColor::Muted))
+                div().child(Label::new(self.empty_message.clone()).color(TextColor::Muted))
             }
         };
 

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

@@ -74,7 +74,7 @@ impl<V: 'static> Modal<V> {
     }
 }
 
-impl<V: 'static> ParentElement<V> for Modal<V> {
+impl<V: 'static> ParentComponent<V> for Modal<V> {
     fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
         &mut self.children
     }

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

@@ -1,5 +1,5 @@
-use crate::prelude::*;
-use crate::{h_stack, v_stack, KeyBinding, Label, LabelColor};
+use crate::{h_stack, prelude::*, v_stack, KeyBinding, Label};
+use gpui::prelude::*;
 
 #[derive(Component)]
 pub struct Palette {
@@ -54,7 +54,7 @@ impl Palette {
                 v_stack()
                     .gap_px()
                     .child(v_stack().py_0p5().px_1().child(div().px_2().py_0p5().child(
-                        Label::new(self.input_placeholder.clone()).color(LabelColor::Placeholder),
+                        Label::new(self.input_placeholder.clone()).color(TextColor::Placeholder),
                     )))
                     .child(
                         div()
@@ -75,7 +75,7 @@ impl Palette {
                                     Some(
                                         h_stack().justify_between().px_2().py_1().child(
                                             Label::new(self.empty_string.clone())
-                                                .color(LabelColor::Muted),
+                                                .color(TextColor::Muted),
                                         ),
                                     )
                                 } else {
@@ -108,7 +108,7 @@ impl Palette {
 pub struct PaletteItem {
     pub label: SharedString,
     pub sublabel: Option<SharedString>,
-    pub keybinding: Option<KeyBinding>,
+    pub key_binding: Option<KeyBinding>,
 }
 
 impl PaletteItem {
@@ -116,7 +116,7 @@ impl PaletteItem {
         Self {
             label: label.into(),
             sublabel: None,
-            keybinding: None,
+            key_binding: None,
         }
     }
 
@@ -130,11 +130,8 @@ impl PaletteItem {
         self
     }
 
-    pub fn keybinding<K>(mut self, keybinding: K) -> Self
-    where
-        K: Into<Option<KeyBinding>>,
-    {
-        self.keybinding = keybinding.into();
+    pub fn key_binding(mut self, key_binding: impl Into<Option<KeyBinding>>) -> Self {
+        self.key_binding = key_binding.into();
         self
     }
 
@@ -149,7 +146,7 @@ impl PaletteItem {
                     .child(Label::new(self.label.clone()))
                     .children(self.sublabel.clone().map(|sublabel| Label::new(sublabel))),
             )
-            .children(self.keybinding)
+            .children(self.key_binding)
     }
 }
 
@@ -182,23 +179,23 @@ mod stories {
                             .placeholder("Execute a command...")
                             .items(vec![
                                 PaletteItem::new("theme selector: toggle")
-                                    .keybinding(KeyBinding::new(binding("cmd-k cmd-t"))),
+                                    .key_binding(KeyBinding::new(binding("cmd-k cmd-t"))),
                                 PaletteItem::new("assistant: inline assist")
-                                    .keybinding(KeyBinding::new(binding("cmd-enter"))),
+                                    .key_binding(KeyBinding::new(binding("cmd-enter"))),
                                 PaletteItem::new("assistant: quote selection")
-                                    .keybinding(KeyBinding::new(binding("cmd-<"))),
+                                    .key_binding(KeyBinding::new(binding("cmd-<"))),
                                 PaletteItem::new("assistant: toggle focus")
-                                    .keybinding(KeyBinding::new(binding("cmd-?"))),
+                                    .key_binding(KeyBinding::new(binding("cmd-?"))),
                                 PaletteItem::new("auto update: check"),
                                 PaletteItem::new("auto update: view release notes"),
                                 PaletteItem::new("branches: open recent")
-                                    .keybinding(KeyBinding::new(binding("cmd-alt-b"))),
+                                    .key_binding(KeyBinding::new(binding("cmd-alt-b"))),
                                 PaletteItem::new("chat panel: toggle focus"),
                                 PaletteItem::new("cli: install"),
                                 PaletteItem::new("client: sign in"),
                                 PaletteItem::new("client: sign out"),
                                 PaletteItem::new("editor: cancel")
-                                    .keybinding(KeyBinding::new(binding("escape"))),
+                                    .key_binding(KeyBinding::new(binding("escape"))),
                             ]),
                     )
             }

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

@@ -1,4 +1,4 @@
-use gpui::{AbsoluteLength, AnyElement};
+use gpui::{prelude::*, AbsoluteLength, AnyElement};
 use smallvec::SmallVec;
 
 use crate::prelude::*;
@@ -113,7 +113,7 @@ impl<V: 'static> Panel<V> {
     }
 }
 
-impl<V: 'static> ParentElement<V> for Panel<V> {
+impl<V: 'static> ParentComponent<V> for Panel<V> {
     fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
         &mut self.children
     }
@@ -126,7 +126,7 @@ pub use stories::*;
 mod stories {
     use super::*;
     use crate::{Label, Story};
-    use gpui::{Div, Render};
+    use gpui::{Div, InteractiveComponent, Render};
 
     pub struct PanelStory;
 

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

@@ -1,6 +1,6 @@
 use crate::prelude::*;
-use crate::{Icon, IconColor, IconElement, Label, LabelColor};
-use gpui::{red, Div, ElementId, Render, View, VisualContext};
+use crate::{Icon, IconElement, Label, TextColor};
+use gpui::{prelude::*, red, Div, ElementId, Render, View};
 
 #[derive(Component, Clone)]
 pub struct Tab {
@@ -92,20 +92,18 @@ impl Tab {
 
         let label = match (self.git_status, is_deleted) {
             (_, true) | (GitStatus::Deleted, false) => Label::new(self.title.clone())
-                .color(LabelColor::Hidden)
+                .color(TextColor::Hidden)
                 .set_strikethrough(true),
             (GitStatus::None, false) => Label::new(self.title.clone()),
-            (GitStatus::Created, false) => {
-                Label::new(self.title.clone()).color(LabelColor::Created)
-            }
+            (GitStatus::Created, false) => Label::new(self.title.clone()).color(TextColor::Created),
             (GitStatus::Modified, false) => {
-                Label::new(self.title.clone()).color(LabelColor::Modified)
+                Label::new(self.title.clone()).color(TextColor::Modified)
             }
-            (GitStatus::Renamed, false) => Label::new(self.title.clone()).color(LabelColor::Accent),
+            (GitStatus::Renamed, false) => Label::new(self.title.clone()).color(TextColor::Accent),
             (GitStatus::Conflict, false) => Label::new(self.title.clone()),
         };
 
-        let close_icon = || IconElement::new(Icon::Close).color(IconColor::Muted);
+        let close_icon = || IconElement::new(Icon::Close).color(TextColor::Muted);
 
         let (tab_bg, tab_hover_bg, tab_active_bg) = match self.current {
             false => (
@@ -148,7 +146,7 @@ impl Tab {
                     .children(has_fs_conflict.then(|| {
                         IconElement::new(Icon::ExclamationTriangle)
                             .size(crate::IconSize::Small)
-                            .color(IconColor::Warning)
+                            .color(TextColor::Warning)
                     }))
                     .children(self.icon.map(IconElement::new))
                     .children(if self.close_side == IconSide::Left {

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

@@ -1,7 +1,6 @@
-use gpui::AnyElement;
-use smallvec::SmallVec;
-
 use crate::prelude::*;
+use gpui::{prelude::*, AnyElement};
+use smallvec::SmallVec;
 
 #[derive(Default, Debug, PartialEq, Eq, Clone, Copy)]
 pub enum ToastOrigin {
@@ -59,7 +58,7 @@ impl<V: 'static> Toast<V> {
     }
 }
 
-impl<V: 'static> ParentElement<V> for Toast<V> {
+impl<V: 'static> ParentComponent<V> for Toast<V> {
     fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
         &mut self.children
     }

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

@@ -1,6 +1,6 @@
-use gpui::{div, Component, ParentElement};
+use gpui::{div, Component, ParentComponent};
 
-use crate::{Icon, IconColor, IconElement, IconSize};
+use crate::{Icon, IconElement, IconSize, TextColor};
 
 /// Whether the entry is toggleable, and if so, whether it is currently toggled.
 ///
@@ -49,12 +49,12 @@ pub fn disclosure_control<V: 'static>(toggle: Toggle) -> impl Component<V> {
         (false, _) => div(),
         (_, true) => div().child(
             IconElement::new(Icon::ChevronDown)
-                .color(IconColor::Muted)
+                .color(TextColor::Muted)
                 .size(IconSize::Small),
         ),
         (_, false) => div().child(
             IconElement::new(Icon::ChevronRight)
-                .color(IconColor::Muted)
+                .color(TextColor::Muted)
                 .size(IconSize::Small),
         ),
     }

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

@@ -1,32 +1,61 @@
-use gpui::{div, Div, ParentElement, Render, SharedString, Styled, ViewContext};
-use theme2::ActiveTheme;
+use gpui::{Div, Render};
+use settings2::Settings;
+use theme2::{ActiveTheme, ThemeSettings};
 
-use crate::StyledExt;
+use crate::prelude::*;
+use crate::{h_stack, v_stack, KeyBinding, Label, LabelSize, StyledExt, TextColor};
 
-#[derive(Clone, Debug)]
 pub struct TextTooltip {
     title: SharedString,
+    meta: Option<SharedString>,
+    key_binding: Option<KeyBinding>,
 }
 
 impl TextTooltip {
     pub fn new(title: impl Into<SharedString>) -> Self {
         Self {
             title: title.into(),
+            meta: None,
+            key_binding: None,
         }
     }
+
+    pub fn meta(mut self, meta: impl Into<SharedString>) -> Self {
+        self.meta = Some(meta.into());
+        self
+    }
+
+    pub fn key_binding(mut self, key_binding: impl Into<Option<KeyBinding>>) -> Self {
+        self.key_binding = key_binding.into();
+        self
+    }
 }
 
 impl Render for TextTooltip {
     type Element = Div<Self>;
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
-        div()
+        let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone();
+        v_stack()
             .elevation_2(cx)
-            .font("Zed Sans")
-            .text_ui()
+            .font(ui_font)
+            .text_ui_sm()
             .text_color(cx.theme().colors().text)
             .py_1()
             .px_2()
-            .child(self.title.clone())
+            .child(
+                h_stack()
+                    .child(self.title.clone())
+                    .when_some(self.key_binding.clone(), |this, key_binding| {
+                        this.justify_between().child(key_binding)
+                    }),
+            )
+            .when_some(self.meta.clone(), |this, meta| {
+                this.child(
+                    Label::new(meta)
+                        .size(LabelSize::Small)
+                        .color(TextColor::Muted),
+                )
+            })
     }
 }

crates/ui2/src/prelude.rs 🔗

@@ -1,13 +1,13 @@
 use gpui::rems;
 use gpui::Rems;
 pub use gpui::{
-    div, Component, Element, ElementId, ParentElement, SharedString, StatefulInteractive,
-    StatelessInteractive, Styled, ViewContext, WindowContext,
+    div, Component, Element, ElementId, InteractiveComponent, ParentComponent, SharedString,
+    Styled, ViewContext, WindowContext,
 };
 
 pub use crate::elevation::*;
-pub use crate::ButtonVariant;
 pub use crate::StyledExt;
+pub use crate::{ButtonVariant, TextColor};
 pub use theme2::ActiveTheme;
 
 use gpui::Hsla;

crates/ui2/src/static_data.rs 🔗

@@ -10,9 +10,9 @@ use theme2::ActiveTheme;
 use crate::{binding, HighlightedText};
 use crate::{
     Buffer, BufferRow, BufferRows, Button, EditorPane, FileSystemStatus, GitStatus,
-    HighlightedLine, Icon, KeyBinding, Label, LabelColor, ListEntry, ListEntrySize, Livestream,
-    MicStatus, Notification, PaletteItem, Player, PlayerCallStatus, PlayerWithCallStatus,
-    PublicPlayer, ScreenShareStatus, Symbol, Tab, Toggle, VideoStatus,
+    HighlightedLine, Icon, KeyBinding, Label, ListEntry, ListEntrySize, Livestream, MicStatus,
+    Notification, PaletteItem, Player, PlayerCallStatus, PlayerWithCallStatus, PublicPlayer,
+    ScreenShareStatus, Symbol, Tab, TextColor, Toggle, VideoStatus,
 };
 use crate::{ListItem, NotificationAction};
 
@@ -490,20 +490,20 @@ pub fn static_project_panel_project_items() -> Vec<ListItem> {
         ListEntry::new(Label::new(".config"))
             .left_icon(Icon::Folder.into())
             .indent_level(1),
-        ListEntry::new(Label::new(".git").color(LabelColor::Hidden))
+        ListEntry::new(Label::new(".git").color(TextColor::Hidden))
             .left_icon(Icon::Folder.into())
             .indent_level(1),
         ListEntry::new(Label::new(".cargo"))
             .left_icon(Icon::Folder.into())
             .indent_level(1),
-        ListEntry::new(Label::new(".idea").color(LabelColor::Hidden))
+        ListEntry::new(Label::new(".idea").color(TextColor::Hidden))
             .left_icon(Icon::Folder.into())
             .indent_level(1),
         ListEntry::new(Label::new("assets"))
             .left_icon(Icon::Folder.into())
             .indent_level(1)
             .toggle(Toggle::Toggled(true)),
-        ListEntry::new(Label::new("cargo-target").color(LabelColor::Hidden))
+        ListEntry::new(Label::new("cargo-target").color(TextColor::Hidden))
             .left_icon(Icon::Folder.into())
             .indent_level(1),
         ListEntry::new(Label::new("crates"))
@@ -528,7 +528,7 @@ pub fn static_project_panel_project_items() -> Vec<ListItem> {
         ListEntry::new(Label::new("call"))
             .left_icon(Icon::Folder.into())
             .indent_level(2),
-        ListEntry::new(Label::new("sqlez").color(LabelColor::Modified))
+        ListEntry::new(Label::new("sqlez").color(TextColor::Modified))
             .left_icon(Icon::Folder.into())
             .indent_level(2)
             .toggle(Toggle::Toggled(false)),
@@ -543,45 +543,45 @@ pub fn static_project_panel_project_items() -> Vec<ListItem> {
         ListEntry::new(Label::new("derive_element.rs"))
             .left_icon(Icon::FileRust.into())
             .indent_level(4),
-        ListEntry::new(Label::new("storybook").color(LabelColor::Modified))
+        ListEntry::new(Label::new("storybook").color(TextColor::Modified))
             .left_icon(Icon::FolderOpen.into())
             .indent_level(1)
             .toggle(Toggle::Toggled(true)),
-        ListEntry::new(Label::new("docs").color(LabelColor::Default))
+        ListEntry::new(Label::new("docs").color(TextColor::Default))
             .left_icon(Icon::Folder.into())
             .indent_level(2)
             .toggle(Toggle::Toggled(true)),
-        ListEntry::new(Label::new("src").color(LabelColor::Modified))
+        ListEntry::new(Label::new("src").color(TextColor::Modified))
             .left_icon(Icon::FolderOpen.into())
             .indent_level(3)
             .toggle(Toggle::Toggled(true)),
-        ListEntry::new(Label::new("ui").color(LabelColor::Modified))
+        ListEntry::new(Label::new("ui").color(TextColor::Modified))
             .left_icon(Icon::FolderOpen.into())
             .indent_level(4)
             .toggle(Toggle::Toggled(true)),
-        ListEntry::new(Label::new("component").color(LabelColor::Created))
+        ListEntry::new(Label::new("component").color(TextColor::Created))
             .left_icon(Icon::FolderOpen.into())
             .indent_level(5)
             .toggle(Toggle::Toggled(true)),
-        ListEntry::new(Label::new("facepile.rs").color(LabelColor::Default))
+        ListEntry::new(Label::new("facepile.rs").color(TextColor::Default))
             .left_icon(Icon::FileRust.into())
             .indent_level(6),
-        ListEntry::new(Label::new("follow_group.rs").color(LabelColor::Default))
+        ListEntry::new(Label::new("follow_group.rs").color(TextColor::Default))
             .left_icon(Icon::FileRust.into())
             .indent_level(6),
-        ListEntry::new(Label::new("list_item.rs").color(LabelColor::Created))
+        ListEntry::new(Label::new("list_item.rs").color(TextColor::Created))
             .left_icon(Icon::FileRust.into())
             .indent_level(6),
-        ListEntry::new(Label::new("tab.rs").color(LabelColor::Default))
+        ListEntry::new(Label::new("tab.rs").color(TextColor::Default))
             .left_icon(Icon::FileRust.into())
             .indent_level(6),
-        ListEntry::new(Label::new("target").color(LabelColor::Hidden))
+        ListEntry::new(Label::new("target").color(TextColor::Hidden))
             .left_icon(Icon::Folder.into())
             .indent_level(1),
         ListEntry::new(Label::new(".dockerignore"))
             .left_icon(Icon::FileGeneric.into())
             .indent_level(1),
-        ListEntry::new(Label::new(".DS_Store").color(LabelColor::Hidden))
+        ListEntry::new(Label::new(".DS_Store").color(TextColor::Hidden))
             .left_icon(Icon::FileGeneric.into())
             .indent_level(1),
         ListEntry::new(Label::new("Cargo.lock"))
@@ -701,16 +701,16 @@ pub fn static_collab_panel_channels() -> Vec<ListItem> {
 
 pub fn example_editor_actions() -> Vec<PaletteItem> {
     vec![
-        PaletteItem::new("New File").keybinding(KeyBinding::new(binding("cmd-n"))),
-        PaletteItem::new("Open File").keybinding(KeyBinding::new(binding("cmd-o"))),
-        PaletteItem::new("Save File").keybinding(KeyBinding::new(binding("cmd-s"))),
-        PaletteItem::new("Cut").keybinding(KeyBinding::new(binding("cmd-x"))),
-        PaletteItem::new("Copy").keybinding(KeyBinding::new(binding("cmd-c"))),
-        PaletteItem::new("Paste").keybinding(KeyBinding::new(binding("cmd-v"))),
-        PaletteItem::new("Undo").keybinding(KeyBinding::new(binding("cmd-z"))),
-        PaletteItem::new("Redo").keybinding(KeyBinding::new(binding("cmd-shift-z"))),
-        PaletteItem::new("Find").keybinding(KeyBinding::new(binding("cmd-f"))),
-        PaletteItem::new("Replace").keybinding(KeyBinding::new(binding("cmd-r"))),
+        PaletteItem::new("New File").key_binding(KeyBinding::new(binding("cmd-n"))),
+        PaletteItem::new("Open File").key_binding(KeyBinding::new(binding("cmd-o"))),
+        PaletteItem::new("Save File").key_binding(KeyBinding::new(binding("cmd-s"))),
+        PaletteItem::new("Cut").key_binding(KeyBinding::new(binding("cmd-x"))),
+        PaletteItem::new("Copy").key_binding(KeyBinding::new(binding("cmd-c"))),
+        PaletteItem::new("Paste").key_binding(KeyBinding::new(binding("cmd-v"))),
+        PaletteItem::new("Undo").key_binding(KeyBinding::new(binding("cmd-z"))),
+        PaletteItem::new("Redo").key_binding(KeyBinding::new(binding("cmd-shift-z"))),
+        PaletteItem::new("Find").key_binding(KeyBinding::new(binding("cmd-f"))),
+        PaletteItem::new("Replace").key_binding(KeyBinding::new(binding("cmd-r"))),
         PaletteItem::new("Jump to Line"),
         PaletteItem::new("Select All"),
         PaletteItem::new("Deselect All"),

crates/ui2/src/styled_ext.rs 🔗

@@ -1,4 +1,4 @@
-use gpui::{Div, ElementInteractivity, KeyDispatch, Styled, UniformList, ViewContext};
+use gpui::{Styled, ViewContext};
 use theme2::ActiveTheme;
 
 use crate::{ElevationIndex, UITextSize};
@@ -93,11 +93,4 @@ pub trait StyledExt: Styled + Sized {
     }
 }
 
-impl<V, I, F> StyledExt for Div<V, I, F>
-where
-    I: ElementInteractivity<V>,
-    F: KeyDispatch<V>,
-{
-}
-
-impl<V> StyledExt for UniformList<V> {}
+impl<E: Styled> StyledExt for E {}

crates/ui2/src/to_extract/assistant_panel.rs 🔗

@@ -1,6 +1,6 @@
 use crate::prelude::*;
 use crate::{Icon, IconButton, Label, Panel, PanelSide};
-use gpui::{rems, AbsoluteLength};
+use gpui::{prelude::*, rems, AbsoluteLength};
 
 #[derive(Component)]
 pub struct AssistantPanel {

crates/ui2/src/to_extract/breadcrumb.rs 🔗

@@ -1,9 +1,7 @@
+use crate::{h_stack, prelude::*, HighlightedText};
+use gpui::{prelude::*, Div};
 use std::path::PathBuf;
 
-use crate::prelude::*;
-use crate::{h_stack, HighlightedText};
-use gpui::Div;
-
 #[derive(Clone)]
 pub struct Symbol(pub Vec<HighlightedText>);
 

crates/ui2/src/to_extract/buffer_search.rs 🔗

@@ -1,7 +1,7 @@
 use gpui::{Div, Render, View, VisualContext};
 
 use crate::prelude::*;
-use crate::{h_stack, Icon, IconButton, IconColor, Input};
+use crate::{h_stack, Icon, IconButton, Input, TextColor};
 
 #[derive(Clone)]
 pub struct BufferSearch {
@@ -36,7 +36,7 @@ impl Render for BufferSearch {
             .child(
                 h_stack().child(Input::new("Search")).child(
                     IconButton::<Self>::new("replace", Icon::Replace)
-                        .when(self.is_replace_open, |this| this.color(IconColor::Accent))
+                        .when(self.is_replace_open, |this| this.color(TextColor::Accent))
                         .on_click(|buffer_search, cx| {
                             buffer_search.toggle_replace(cx);
                         }),

crates/ui2/src/to_extract/chat_panel.rs 🔗

@@ -1,7 +1,6 @@
+use crate::{prelude::*, Icon, IconButton, Input, Label};
 use chrono::NaiveDateTime;
-
-use crate::prelude::*;
-use crate::{Icon, IconButton, Input, Label, LabelColor};
+use gpui::prelude::*;
 
 #[derive(Component)]
 pub struct ChatPanel {
@@ -95,7 +94,7 @@ impl ChatMessage {
                     .child(Label::new(self.author.clone()))
                     .child(
                         Label::new(self.sent_at.format("%m/%d/%Y").to_string())
-                            .color(LabelColor::Muted),
+                            .color(TextColor::Muted),
                     ),
             )
             .child(div().child(Label::new(self.text.clone())))

crates/ui2/src/to_extract/collab_panel.rs 🔗

@@ -1,7 +1,8 @@
-use crate::{prelude::*, Toggle};
 use crate::{
-    static_collab_panel_channels, static_collab_panel_current_call, v_stack, Icon, List, ListHeader,
+    prelude::*, static_collab_panel_channels, static_collab_panel_current_call, v_stack, Icon,
+    List, ListHeader, Toggle,
 };
+use gpui::prelude::*;
 
 #[derive(Component)]
 pub struct CollabPanel {

crates/ui2/src/to_extract/copilot.rs 🔗

@@ -1,4 +1,4 @@
-use crate::{prelude::*, Button, Label, LabelColor, Modal};
+use crate::{prelude::*, Button, Label, Modal, TextColor};
 
 #[derive(Component)]
 pub struct CopilotModal {
@@ -14,7 +14,7 @@ impl CopilotModal {
         div().id(self.id.clone()).child(
             Modal::new("some-id")
                 .title("Connect Copilot to Zed")
-                .child(Label::new("You can update your settings or sign out from the Copilot menu in the status bar.").color(LabelColor::Muted))
+                .child(Label::new("You can update your settings or sign out from the Copilot menu in the status bar.").color(TextColor::Muted))
                 .primary_action(Button::new("Connect to Github").variant(ButtonVariant::Filled)),
         )
     }

crates/ui2/src/to_extract/editor_pane.rs 🔗

@@ -5,7 +5,7 @@ use gpui::{Div, Render, View, VisualContext};
 use crate::prelude::*;
 use crate::{
     hello_world_rust_editor_with_status_example, v_stack, Breadcrumb, Buffer, BufferSearch, Icon,
-    IconButton, IconColor, Symbol, Tab, TabBar, Toolbar,
+    IconButton, Symbol, Tab, TabBar, TextColor, Toolbar,
 };
 
 #[derive(Clone)]
@@ -60,12 +60,12 @@ impl Render for EditorPane {
                 Toolbar::new()
                     .left_item(Breadcrumb::new(self.path.clone(), self.symbols.clone()))
                     .right_items(vec![
-                        IconButton::new("toggle_inlay_hints", Icon::InlayHint),
+                        IconButton::<Self>::new("toggle_inlay_hints", Icon::InlayHint),
                         IconButton::<Self>::new("buffer_search", Icon::MagnifyingGlass)
                             .when(self.is_buffer_search_open, |this| {
-                                this.color(IconColor::Accent)
+                                this.color(TextColor::Accent)
                             })
-                            .on_click(|editor, cx| {
+                            .on_click(|editor: &mut Self, cx| {
                                 editor.toggle_buffer_search(cx);
                             }),
                         IconButton::new("inline_assist", Icon::MagicWand),

crates/ui2/src/to_extract/notifications_panel.rs 🔗

@@ -1,10 +1,9 @@
-use crate::utils::naive_format_distance_from_now;
 use crate::{
-    h_stack, prelude::*, static_new_notification_items_2, v_stack, Avatar, ButtonOrIconButton,
-    Icon, IconElement, Label, LabelColor, LineHeightStyle, ListHeaderMeta, ListSeparator,
-    PublicPlayer, UnreadIndicator,
+    h_stack, prelude::*, static_new_notification_items_2, utils::naive_format_distance_from_now,
+    v_stack, Avatar, ButtonOrIconButton, ClickHandler, Icon, IconElement, Label, LineHeightStyle,
+    ListHeader, ListHeaderMeta, ListSeparator, PublicPlayer, TextColor, UnreadIndicator,
 };
-use crate::{ClickHandler, ListHeader};
+use gpui::prelude::*;
 
 #[derive(Component)]
 pub struct NotificationsPanel {
@@ -48,7 +47,7 @@ impl NotificationsPanel {
                             .border_color(cx.theme().colors().border_variant)
                             .child(
                                 Label::new("Search...")
-                                    .color(LabelColor::Placeholder)
+                                    .color(TextColor::Placeholder)
                                     .line_height_style(LineHeightStyle::UILabel),
                             ),
                     )
@@ -252,7 +251,7 @@ impl<V> Notification<V> {
                         if let Some(icon) = icon {
                             meta_el = meta_el.child(IconElement::new(icon.clone()));
                         }
-                        meta_el.child(Label::new(text.clone()).color(LabelColor::Muted))
+                        meta_el.child(Label::new(text.clone()).color(TextColor::Muted))
                     })
                     .collect::<Vec<_>>(),
             )
@@ -311,7 +310,7 @@ impl<V> Notification<V> {
                                             true,
                                             true,
                                         ))
-                                        .color(LabelColor::Muted),
+                                        .color(TextColor::Muted),
                                     )
                                     .child(self.render_meta_items(cx)),
                             )
@@ -321,11 +320,11 @@ impl<V> Notification<V> {
                                 // Show the taken_message
                                 (Some(_), Some(action_taken)) => h_stack()
                                     .children(action_taken.taken_message.0.map(|icon| {
-                                        IconElement::new(icon).color(crate::IconColor::Muted)
+                                        IconElement::new(icon).color(crate::TextColor::Muted)
                                     }))
                                     .child(
                                         Label::new(action_taken.taken_message.1.clone())
-                                            .color(LabelColor::Muted),
+                                            .color(TextColor::Muted),
                                     ),
                                 // Show the actions
                                 (Some(actions), None) => {

crates/ui2/src/to_extract/panes.rs 🔗

@@ -59,7 +59,7 @@ impl<V: 'static> Pane<V> {
     }
 }
 
-impl<V: 'static> ParentElement<V> for Pane<V> {
+impl<V: 'static> ParentComponent<V> for Pane<V> {
     fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
         &mut self.children
     }

crates/ui2/src/to_extract/project_panel.rs 🔗

@@ -1,7 +1,8 @@
-use crate::prelude::*;
 use crate::{
-    static_project_panel_project_items, static_project_panel_single_items, Input, List, ListHeader,
+    prelude::*, static_project_panel_project_items, static_project_panel_single_items, Input, List,
+    ListHeader,
 };
+use gpui::prelude::*;
 
 #[derive(Component)]
 pub struct ProjectPanel {

crates/ui2/src/to_extract/status_bar.rs 🔗

@@ -1,7 +1,7 @@
 use std::sync::Arc;
 
 use crate::prelude::*;
-use crate::{Button, Icon, IconButton, IconColor, ToolDivider, Workspace};
+use crate::{Button, Icon, IconButton, TextColor, ToolDivider, Workspace};
 
 #[derive(Default, PartialEq)]
 pub enum Tool {
@@ -110,18 +110,18 @@ impl StatusBar {
             .child(
                 IconButton::<Workspace>::new("project_panel", Icon::FileTree)
                     .when(workspace.is_project_panel_open(), |this| {
-                        this.color(IconColor::Accent)
+                        this.color(TextColor::Accent)
                     })
-                    .on_click(|workspace, cx| {
+                    .on_click(|workspace: &mut Workspace, cx| {
                         workspace.toggle_project_panel(cx);
                     }),
             )
             .child(
                 IconButton::<Workspace>::new("collab_panel", Icon::Hash)
                     .when(workspace.is_collab_panel_open(), |this| {
-                        this.color(IconColor::Accent)
+                        this.color(TextColor::Accent)
                     })
-                    .on_click(|workspace, cx| {
+                    .on_click(|workspace: &mut Workspace, cx| {
                         workspace.toggle_collab_panel();
                     }),
             )
@@ -174,27 +174,27 @@ impl StatusBar {
                     .child(
                         IconButton::<Workspace>::new("terminal", Icon::Terminal)
                             .when(workspace.is_terminal_open(), |this| {
-                                this.color(IconColor::Accent)
+                                this.color(TextColor::Accent)
                             })
-                            .on_click(|workspace, cx| {
+                            .on_click(|workspace: &mut Workspace, cx| {
                                 workspace.toggle_terminal(cx);
                             }),
                     )
                     .child(
                         IconButton::<Workspace>::new("chat_panel", Icon::MessageBubbles)
                             .when(workspace.is_chat_panel_open(), |this| {
-                                this.color(IconColor::Accent)
+                                this.color(TextColor::Accent)
                             })
-                            .on_click(|workspace, cx| {
+                            .on_click(|workspace: &mut Workspace, cx| {
                                 workspace.toggle_chat_panel(cx);
                             }),
                     )
                     .child(
                         IconButton::<Workspace>::new("assistant_panel", Icon::Ai)
                             .when(workspace.is_assistant_panel_open(), |this| {
-                                this.color(IconColor::Accent)
+                                this.color(TextColor::Accent)
                             })
-                            .on_click(|workspace, cx| {
+                            .on_click(|workspace: &mut Workspace, cx| {
                                 workspace.toggle_assistant_panel(cx);
                             }),
                     ),

crates/ui2/src/to_extract/tab_bar.rs 🔗

@@ -1,5 +1,5 @@
-use crate::prelude::*;
-use crate::{Icon, IconButton, Tab};
+use crate::{prelude::*, Icon, IconButton, Tab};
+use gpui::prelude::*;
 
 #[derive(Component)]
 pub struct TabBar {

crates/ui2/src/to_extract/title_bar.rs 🔗

@@ -6,8 +6,8 @@ use gpui::{Div, Render, View, VisualContext};
 use crate::prelude::*;
 use crate::settings::user_settings;
 use crate::{
-    Avatar, Button, Icon, IconButton, IconColor, MicStatus, PlayerStack, PlayerWithCallStatus,
-    ScreenShareStatus, ToolDivider, TrafficLights,
+    Avatar, Button, Icon, IconButton, MicStatus, PlayerStack, PlayerWithCallStatus,
+    ScreenShareStatus, TextColor, ToolDivider, TrafficLights,
 };
 
 #[derive(Clone)]
@@ -152,21 +152,25 @@ impl Render for TitleBar {
                             .gap_1()
                             .child(
                                 IconButton::<TitleBar>::new("toggle_mic_status", Icon::Mic)
-                                    .when(self.is_mic_muted(), |this| this.color(IconColor::Error))
-                                    .on_click(|title_bar, cx| title_bar.toggle_mic_status(cx)),
+                                    .when(self.is_mic_muted(), |this| this.color(TextColor::Error))
+                                    .on_click(|title_bar: &mut TitleBar, cx| {
+                                        title_bar.toggle_mic_status(cx)
+                                    }),
                             )
                             .child(
                                 IconButton::<TitleBar>::new("toggle_deafened", Icon::AudioOn)
-                                    .when(self.is_deafened, |this| this.color(IconColor::Error))
-                                    .on_click(|title_bar, cx| title_bar.toggle_deafened(cx)),
+                                    .when(self.is_deafened, |this| this.color(TextColor::Error))
+                                    .on_click(|title_bar: &mut TitleBar, cx| {
+                                        title_bar.toggle_deafened(cx)
+                                    }),
                             )
                             .child(
                                 IconButton::<TitleBar>::new("toggle_screen_share", Icon::Screen)
                                     .when(
                                         self.screen_share_status == ScreenShareStatus::Shared,
-                                        |this| this.color(IconColor::Accent),
+                                        |this| this.color(TextColor::Accent),
                                     )
-                                    .on_click(|title_bar, cx| {
+                                    .on_click(|title_bar: &mut TitleBar, cx| {
                                         title_bar.toggle_screen_share_status(cx)
                                     }),
                             ),

crates/ui2/src/to_extract/workspace.rs 🔗

@@ -206,13 +206,14 @@ impl Render for Workspace {
             .child(self.editor_1.clone())],
             SplitDirection::Horizontal,
         );
+        let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone();
 
         div()
             .relative()
             .size_full()
             .flex()
             .flex_col()
-            .font("Zed Sans")
+            .font(ui_font)
             .gap_0()
             .justify_start()
             .items_start()

crates/workspace2/src/dock.rs 🔗

@@ -1,7 +1,8 @@
 use crate::{status_bar::StatusItemView, Axis, Workspace};
 use gpui::{
-    div, Action, AnyView, AppContext, Div, Entity, EntityId, EventEmitter, ParentElement, Render,
-    Subscription, View, ViewContext, WeakView, WindowContext,
+    div, px, Action, AnyView, AppContext, Component, Div, Entity, EntityId, EventEmitter,
+    FocusHandle, ParentComponent, Render, Styled, Subscription, View, ViewContext, WeakView,
+    WindowContext,
 };
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
@@ -34,6 +35,7 @@ pub trait Panel: Render + EventEmitter<PanelEvent> {
     fn set_zoomed(&mut self, _zoomed: bool, _cx: &mut ViewContext<Self>) {}
     fn set_active(&mut self, _active: bool, _cx: &mut ViewContext<Self>) {}
     fn has_focus(&self, cx: &WindowContext) -> bool;
+    fn focus_handle(&self, cx: &WindowContext) -> FocusHandle;
 }
 
 pub trait PanelHandle: Send + Sync {
@@ -51,6 +53,7 @@ pub trait PanelHandle: Send + Sync {
     fn icon_tooltip(&self, cx: &WindowContext) -> (String, Option<Box<dyn Action>>);
     fn icon_label(&self, cx: &WindowContext) -> Option<String>;
     fn has_focus(&self, cx: &WindowContext) -> bool;
+    fn focus_handle(&self, cx: &WindowContext) -> FocusHandle;
     fn to_any(&self) -> AnyView;
 }
 
@@ -117,6 +120,10 @@ where
     fn to_any(&self) -> AnyView {
         self.clone().into()
     }
+
+    fn focus_handle(&self, cx: &WindowContext) -> FocusHandle {
+        self.read(cx).focus_handle(cx).clone()
+    }
 }
 
 impl From<&dyn PanelHandle> for AnyView {
@@ -422,7 +429,18 @@ impl Render for Dock {
     type Element = Div<Self>;
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
-        todo!()
+        if let Some(entry) = self.visible_entry() {
+            let size = entry.panel.size(cx);
+
+            div()
+                .map(|this| match self.position().axis() {
+                    Axis::Horizontal => this.w(px(size)).h_full(),
+                    Axis::Vertical => this.h(px(size)).w_full(),
+                })
+                .child(entry.panel.to_any())
+        } else {
+            div()
+        }
     }
 }
 
@@ -728,5 +746,9 @@ pub mod test {
         fn has_focus(&self, _cx: &WindowContext) -> bool {
             self.has_focus
         }
+
+        fn focus_handle(&self, cx: &WindowContext) -> FocusHandle {
+            unimplemented!()
+        }
     }
 }

crates/workspace2/src/modal_layer.rs 🔗

@@ -1,6 +1,6 @@
 use gpui::{
-    div, px, AnyView, Div, EventEmitter, FocusHandle, ParentElement, Render, StatelessInteractive,
-    Styled, Subscription, View, ViewContext, VisualContext, WindowContext,
+    div, prelude::*, px, AnyView, Div, EventEmitter, FocusHandle, Render, Subscription, View,
+    ViewContext, WindowContext,
 };
 use ui::{h_stack, v_stack};
 
@@ -71,6 +71,14 @@ impl ModalLayer {
 
         cx.notify();
     }
+
+    pub fn current_modal<V>(&self) -> Option<View<V>>
+    where
+        V: 'static,
+    {
+        let active_modal = self.active_modal.as_ref()?;
+        active_modal.modal.clone().downcast::<V>().ok()
+    }
 }
 
 impl Render for ModalLayer {

crates/workspace2/src/pane.rs 🔗

@@ -1,5 +1,3 @@
-// mod dragged_item_receiver;
-
 use crate::{
     item::{Item, ItemHandle, ItemSettings, WeakItemHandle},
     toolbar::Toolbar,
@@ -9,7 +7,7 @@ use crate::{
 use anyhow::Result;
 use collections::{HashMap, HashSet, VecDeque};
 use gpui::{
-    actions, register_action, AppContext, AsyncWindowContext, Component, Div, EntityId,
+    actions, prelude::*, register_action, AppContext, AsyncWindowContext, Component, Div, EntityId,
     EventEmitter, FocusHandle, Model, PromptLevel, Render, Task, View, ViewContext, VisualContext,
     WeakView, WindowContext,
 };
@@ -27,7 +25,7 @@ use std::{
     },
 };
 use ui::v_stack;
-use ui::{prelude::*, Icon, IconButton, IconColor, IconElement, TextTooltip};
+use ui::{prelude::*, Icon, IconButton, IconElement, TextColor, TextTooltip};
 use util::truncate_and_remove_front;
 
 #[derive(PartialEq, Clone, Copy, Deserialize, Debug)]
@@ -1432,13 +1430,13 @@ impl Pane {
                         Some(
                             IconElement::new(Icon::ExclamationTriangle)
                                 .size(ui::IconSize::Small)
-                                .color(IconColor::Warning),
+                                .color(TextColor::Warning),
                         )
                     } else if item.is_dirty(cx) {
                         Some(
                             IconElement::new(Icon::ExclamationTriangle)
                                 .size(ui::IconSize::Small)
-                                .color(IconColor::Info),
+                                .color(TextColor::Info),
                         )
                     } else {
                         None
@@ -1919,7 +1917,7 @@ impl Render for Pane {
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
         v_stack()
-            .context("Pane")
+            .key_context("Pane")
             .size_full()
             .on_action(|pane: &mut Self, action, cx| {
                 pane.close_active_item(action, cx)

crates/workspace2/src/pane/dragged_item_receiver.rs 🔗

@@ -2,7 +2,7 @@ use super::DraggedItem;
 use crate::{Pane, SplitDirection, Workspace};
 use gpui::{
     color::Color,
-    elements::{Canvas, MouseEventHandler, ParentElement, Stack},
+    elements::{Canvas, MouseEventHandler, ParentComponent, Stack},
     geometry::{rect::RectF, vector::Vector2F},
     platform::MouseButton,
     scene::MouseUp,

crates/workspace2/src/status_bar.rs 🔗

@@ -2,7 +2,7 @@ use std::any::TypeId;
 
 use crate::{ItemHandle, Pane};
 use gpui::{
-    div, AnyView, Component, Div, ParentElement, Render, Styled, Subscription, View, ViewContext,
+    div, AnyView, Component, Div, ParentComponent, Render, Styled, Subscription, View, ViewContext,
     WindowContext,
 };
 use theme2::ActiveTheme;

crates/workspace2/src/workspace2.rs 🔗

@@ -29,23 +29,22 @@ use client2::{
     Client, TypedEnvelope, UserStore,
 };
 use collections::{hash_map, HashMap, HashSet};
-use dock::{Dock, DockPosition, PanelButtons};
+use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle as _};
 use futures::{
     channel::{mpsc, oneshot},
     future::try_join_all,
     Future, FutureExt, StreamExt,
 };
 use gpui::{
-    actions, div, point, rems, size, Action, AnyModel, AnyView, AnyWeakView, AppContext,
-    AsyncAppContext, AsyncWindowContext, Bounds, Component, Div, Entity, EntityId, EventEmitter,
-    FocusHandle, GlobalPixels, KeyContext, Model, ModelContext, ParentElement, Point, Render, Size,
-    StatefulInteractive, StatelessInteractive, StatelessInteractivity, Styled, Subscription, Task,
-    View, ViewContext, VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle,
-    WindowOptions,
+    actions, div, point, prelude::*, rems, size, Action, AnyModel, AnyView, AnyWeakView,
+    AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Component, Div, Entity, EntityId,
+    EventEmitter, FocusHandle, GlobalPixels, KeyContext, Model, ModelContext, ParentComponent,
+    Point, Render, Size, Styled, Subscription, Task, View, ViewContext, WeakView, WindowBounds,
+    WindowContext, WindowHandle, WindowOptions,
 };
 use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem};
 use itertools::Itertools;
-use language2::LanguageRegistry;
+use language2::{LanguageRegistry, Rope};
 use lazy_static::lazy_static;
 pub use modal_layer::*;
 use node_runtime::NodeRuntime;
@@ -67,12 +66,13 @@ use std::{
     sync::{atomic::AtomicUsize, Arc},
     time::Duration,
 };
-use theme2::ActiveTheme;
+use theme2::{ActiveTheme, ThemeSettings};
 pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
-use ui::{h_stack, Button, ButtonVariant, Label, LabelColor};
+use ui::TextColor;
+use ui::{h_stack, Button, ButtonVariant, KeyBinding, Label, TextTooltip};
 use util::ResultExt;
 use uuid::Uuid;
-use workspace_settings::{AutosaveSetting, WorkspaceSettings};
+pub use workspace_settings::{AutosaveSetting, WorkspaceSettings};
 
 lazy_static! {
     static ref ZED_WINDOW_SIZE: Option<Size<GlobalPixels>> = env::var("ZED_WINDOW_SIZE")
@@ -248,102 +248,6 @@ pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
     //             }
     //         }
     //     });
-    //     cx.add_async_action(Workspace::open);
-
-    //     cx.add_async_action(Workspace::follow_next_collaborator);
-    //     cx.add_async_action(Workspace::close);
-    //     cx.add_async_action(Workspace::close_inactive_items_and_panes);
-    //     cx.add_async_action(Workspace::close_all_items_and_panes);
-    //     cx.add_global_action(Workspace::close_global);
-    //     cx.add_global_action(restart);
-    //     cx.add_async_action(Workspace::save_all);
-    //     cx.add_action(Workspace::add_folder_to_project);
-    //     cx.add_action(
-    //         |workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext<Workspace>| {
-    //             let pane = workspace.active_pane().clone();
-    //             workspace.unfollow(&pane, cx);
-    //         },
-    //     );
-    //     cx.add_action(
-    //         |workspace: &mut Workspace, action: &Save, cx: &mut ViewContext<Workspace>| {
-    //             workspace
-    //                 .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), cx)
-    //                 .detach_and_log_err(cx);
-    //         },
-    //     );
-    //     cx.add_action(
-    //         |workspace: &mut Workspace, _: &SaveAs, cx: &mut ViewContext<Workspace>| {
-    //             workspace
-    //                 .save_active_item(SaveIntent::SaveAs, cx)
-    //                 .detach_and_log_err(cx);
-    //         },
-    //     );
-    //     cx.add_action(|workspace: &mut Workspace, _: &ActivatePreviousPane, cx| {
-    //         workspace.activate_previous_pane(cx)
-    //     });
-    //     cx.add_action(|workspace: &mut Workspace, _: &ActivateNextPane, cx| {
-    //         workspace.activate_next_pane(cx)
-    //     });
-
-    //     cx.add_action(
-    //         |workspace: &mut Workspace, action: &ActivatePaneInDirection, cx| {
-    //             workspace.activate_pane_in_direction(action.0, cx)
-    //         },
-    //     );
-
-    //     cx.add_action(
-    //         |workspace: &mut Workspace, action: &SwapPaneInDirection, cx| {
-    //             workspace.swap_pane_in_direction(action.0, cx)
-    //         },
-    //     );
-
-    //     cx.add_action(|workspace: &mut Workspace, _: &ToggleLeftDock, cx| {
-    //         workspace.toggle_dock(DockPosition::Left, cx);
-    //     });
-    //     cx.add_action(|workspace: &mut Workspace, _: &ToggleRightDock, cx| {
-    //         workspace.toggle_dock(DockPosition::Right, cx);
-    //     });
-    //     cx.add_action(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| {
-    //         workspace.toggle_dock(DockPosition::Bottom, cx);
-    //     });
-    //     cx.add_action(|workspace: &mut Workspace, _: &CloseAllDocks, cx| {
-    //         workspace.close_all_docks(cx);
-    //     });
-    //     cx.add_action(Workspace::activate_pane_at_index);
-    //     cx.add_action(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
-    //         workspace.reopen_closed_item(cx).detach();
-    //     });
-    //     cx.add_action(|workspace: &mut Workspace, _: &GoBack, cx| {
-    //         workspace
-    //             .go_back(workspace.active_pane().downgrade(), cx)
-    //             .detach();
-    //     });
-    //     cx.add_action(|workspace: &mut Workspace, _: &GoForward, cx| {
-    //         workspace
-    //             .go_forward(workspace.active_pane().downgrade(), cx)
-    //             .detach();
-    //     });
-
-    //     cx.add_action(|_: &mut Workspace, _: &install_cli::Install, cx| {
-    //         cx.spawn(|workspace, mut cx| async move {
-    //             let err = install_cli::install_cli(&cx)
-    //                 .await
-    //                 .context("Failed to create CLI symlink");
-
-    //             workspace.update(&mut cx, |workspace, cx| {
-    //                 if matches!(err, Err(_)) {
-    //                     err.notify_err(workspace, cx);
-    //                 } else {
-    //                     workspace.show_notification(1, cx, |cx| {
-    //                         cx.build_view(|_| {
-    //                             MessageNotification::new("Successfully installed the `zed` binary")
-    //                         })
-    //                     });
-    //                 }
-    //             })
-    //         })
-    //         .detach();
-    //     });
 }
 
 type ProjectItemBuilders =
@@ -443,7 +347,6 @@ struct Follower {
 impl AppState {
     #[cfg(any(test, feature = "test-support"))]
     pub fn test(cx: &mut AppContext) -> Arc<Self> {
-        use gpui::Context;
         use node_runtime::FakeNodeRuntime;
         use settings2::SettingsStore;
 
@@ -531,13 +434,7 @@ pub enum Event {
 pub struct Workspace {
     weak_self: WeakView<Self>,
     focus_handle: FocusHandle,
-    workspace_actions: Vec<
-        Box<
-            dyn Fn(
-                Div<Workspace, StatelessInteractivity<Workspace>>,
-            ) -> Div<Workspace, StatelessInteractivity<Workspace>>,
-        >,
-    >,
+    workspace_actions: Vec<Box<dyn Fn(Div<Workspace>) -> Div<Workspace>>>,
     zoomed: Option<AnyWeakView>,
     zoomed_position: Option<DockPosition>,
     center: PaneGroup,
@@ -942,108 +839,15 @@ impl Workspace {
         &self.right_dock
     }
 
-    //     pub fn add_panel<T: Panel>(&mut self, panel: View<T>, cx: &mut ViewContext<Self>)
-    //     where
-    //         T::Event: std::fmt::Debug,
-    //     {
-    //         self.add_panel_with_extra_event_handler(panel, cx, |_, _, _, _| {})
-    //     }
-
-    //     pub fn add_panel_with_extra_event_handler<T: Panel, F>(
-    //         &mut self,
-    //         panel: View<T>,
-    //         cx: &mut ViewContext<Self>,
-    //         handler: F,
-    //     ) where
-    //         T::Event: std::fmt::Debug,
-    //         F: Fn(&mut Self, &View<T>, &T::Event, &mut ViewContext<Self>) + 'static,
-    //     {
-    //         let dock = match panel.position(cx) {
-    //             DockPosition::Left => &self.left_dock,
-    //             DockPosition::Bottom => &self.bottom_dock,
-    //             DockPosition::Right => &self.right_dock,
-    //         };
-
-    //         self.subscriptions.push(cx.subscribe(&panel, {
-    //             let mut dock = dock.clone();
-    //             let mut prev_position = panel.position(cx);
-    //             move |this, panel, event, cx| {
-    //                 if T::should_change_position_on_event(event) {
-    //                     THIS HAS BEEN MOVED TO NORMAL EVENT EMISSION
-    //                     See: Dock::add_panel
-    //
-    //                     let new_position = panel.read(cx).position(cx);
-    //                     let mut was_visible = false;
-    //                     dock.update(cx, |dock, cx| {
-    //                         prev_position = new_position;
-
-    //                         was_visible = dock.is_open()
-    //                             && dock
-    //                                 .visible_panel()
-    //                                 .map_or(false, |active_panel| active_panel.id() == panel.id());
-    //                         dock.remove_panel(&panel, cx);
-    //                     });
-
-    //                     if panel.is_zoomed(cx) {
-    //                         this.zoomed_position = Some(new_position);
-    //                     }
-
-    //                     dock = match panel.read(cx).position(cx) {
-    //                         DockPosition::Left => &this.left_dock,
-    //                         DockPosition::Bottom => &this.bottom_dock,
-    //                         DockPosition::Right => &this.right_dock,
-    //                     }
-    //                     .clone();
-    //                     dock.update(cx, |dock, cx| {
-    //                         dock.add_panel(panel.clone(), cx);
-    //                         if was_visible {
-    //                             dock.set_open(true, cx);
-    //                             dock.activate_panel(dock.panels_len() - 1, cx);
-    //                         }
-    //                     });
-    //                 } else if T::should_zoom_in_on_event(event) {
-    //                     THIS HAS BEEN MOVED TO NORMAL EVENT EMISSION
-    //                     See: Dock::add_panel
-    //
-    //                     dock.update(cx, |dock, cx| dock.set_panel_zoomed(&panel, true, cx));
-    //                     if !panel.has_focus(cx) {
-    //                         cx.focus(&panel);
-    //                     }
-    //                     this.zoomed = Some(panel.downgrade().into_any());
-    //                     this.zoomed_position = Some(panel.read(cx).position(cx));
-    //                 } else if T::should_zoom_out_on_event(event) {
-    //                     THIS HAS BEEN MOVED TO NORMAL EVENT EMISSION
-    //                     See: Dock::add_panel
-    //
-    //                     dock.update(cx, |dock, cx| dock.set_panel_zoomed(&panel, false, cx));
-    //                     if this.zoomed_position == Some(prev_position) {
-    //                         this.zoomed = None;
-    //                         this.zoomed_position = None;
-    //                     }
-    //                     cx.notify();
-    //                 } else if T::is_focus_event(event) {
-    //                     THIS HAS BEEN MOVED TO NORMAL EVENT EMISSION
-    //                     See: Dock::add_panel
-    //
-    //                     let position = panel.read(cx).position(cx);
-    //                     this.dismiss_zoomed_items_to_reveal(Some(position), cx);
-    //                     if panel.is_zoomed(cx) {
-    //                         this.zoomed = Some(panel.downgrade().into_any());
-    //                         this.zoomed_position = Some(position);
-    //                     } else {
-    //                         this.zoomed = None;
-    //                         this.zoomed_position = None;
-    //                     }
-    //                     this.update_active_view_for_followers(cx);
-    //                     cx.notify();
-    //                 } else {
-    //                     handler(this, &panel, event, cx)
-    //                 }
-    //             }
-    //         }));
+    pub fn add_panel<T: Panel>(&mut self, panel: View<T>, cx: &mut ViewContext<Self>) {
+        let dock = match panel.position(cx) {
+            DockPosition::Left => &self.left_dock,
+            DockPosition::Bottom => &self.bottom_dock,
+            DockPosition::Right => &self.right_dock,
+        };
 
-    //         dock.update(cx, |dock, cx| dock.add_panel(panel, cx));
-    //     }
+        dock.update(cx, |dock, cx| dock.add_panel(panel, cx));
+    }
 
     pub fn status_bar(&self) -> &View<StatusBar> {
         &self.status_bar
@@ -1241,29 +1045,29 @@ impl Workspace {
     //     self.titlebar_item.clone()
     // }
 
-    //     /// Call the given callback with a workspace whose project is local.
-    //     ///
-    //     /// If the given workspace has a local project, then it will be passed
-    //     /// to the callback. Otherwise, a new empty window will be created.
-    //     pub fn with_local_workspace<T, F>(
-    //         &mut self,
-    //         cx: &mut ViewContext<Self>,
-    //         callback: F,
-    //     ) -> Task<Result<T>>
-    //     where
-    //         T: 'static,
-    //         F: 'static + FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
-    //     {
-    //         if self.project.read(cx).is_local() {
-    //             Task::Ready(Some(Ok(callback(self, cx))))
-    //         } else {
-    //             let task = Self::new_local(Vec::new(), self.app_state.clone(), None, cx);
-    //             cx.spawn(|_vh, mut cx| async move {
-    //                 let (workspace, _) = task.await;
-    //                 workspace.update(&mut cx, callback)
-    //             })
-    //         }
-    //     }
+    /// Call the given callback with a workspace whose project is local.
+    ///
+    /// If the given workspace has a local project, then it will be passed
+    /// to the callback. Otherwise, a new empty window will be created.
+    pub fn with_local_workspace<T, F>(
+        &mut self,
+        cx: &mut ViewContext<Self>,
+        callback: F,
+    ) -> Task<Result<T>>
+    where
+        T: 'static,
+        F: 'static + FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
+    {
+        if self.project.read(cx).is_local() {
+            Task::Ready(Some(Ok(callback(self, cx))))
+        } else {
+            let task = Self::new_local(Vec::new(), self.app_state.clone(), None, cx);
+            cx.spawn(|_vh, mut cx| async move {
+                let (workspace, _) = task.await?;
+                workspace.update(&mut cx, callback)
+            })
+        }
+    }
 
     pub fn worktrees<'a>(&self, cx: &'a AppContext) -> impl 'a + Iterator<Item = Model<Worktree>> {
         self.project.read(cx).worktrees()
@@ -1735,42 +1539,43 @@ impl Workspace {
     //         }
     //     }
 
-    //     pub fn toggle_dock(&mut self, dock_side: DockPosition, cx: &mut ViewContext<Self>) {
-    //         let dock = match dock_side {
-    //             DockPosition::Left => &self.left_dock,
-    //             DockPosition::Bottom => &self.bottom_dock,
-    //             DockPosition::Right => &self.right_dock,
-    //         };
-    //         let mut focus_center = false;
-    //         let mut reveal_dock = false;
-    //         dock.update(cx, |dock, cx| {
-    //             let other_is_zoomed = self.zoomed.is_some() && self.zoomed_position != Some(dock_side);
-    //             let was_visible = dock.is_open() && !other_is_zoomed;
-    //             dock.set_open(!was_visible, cx);
-
-    //             if let Some(active_panel) = dock.active_panel() {
-    //                 if was_visible {
-    //                     if active_panel.has_focus(cx) {
-    //                         focus_center = true;
-    //                     }
-    //                 } else {
-    //                     cx.focus(active_panel.as_any());
-    //                     reveal_dock = true;
-    //                 }
-    //             }
-    //         });
+    pub fn toggle_dock(&mut self, dock_side: DockPosition, cx: &mut ViewContext<Self>) {
+        let dock = match dock_side {
+            DockPosition::Left => &self.left_dock,
+            DockPosition::Bottom => &self.bottom_dock,
+            DockPosition::Right => &self.right_dock,
+        };
+        let mut focus_center = false;
+        let mut reveal_dock = false;
+        dock.update(cx, |dock, cx| {
+            let other_is_zoomed = self.zoomed.is_some() && self.zoomed_position != Some(dock_side);
+            let was_visible = dock.is_open() && !other_is_zoomed;
+            dock.set_open(!was_visible, cx);
+
+            if let Some(active_panel) = dock.active_panel() {
+                if was_visible {
+                    if active_panel.has_focus(cx) {
+                        focus_center = true;
+                    }
+                } else {
+                    let focus_handle = &active_panel.focus_handle(cx);
+                    cx.focus(focus_handle);
+                    reveal_dock = true;
+                }
+            }
+        });
 
-    //         if reveal_dock {
-    //             self.dismiss_zoomed_items_to_reveal(Some(dock_side), cx);
-    //         }
+        if reveal_dock {
+            self.dismiss_zoomed_items_to_reveal(Some(dock_side), cx);
+        }
 
-    //         if focus_center {
-    //             cx.focus_self();
-    //         }
+        if focus_center {
+            cx.focus(&self.focus_handle);
+        }
 
-    //         cx.notify();
-    //         self.serialize_workspace(cx);
-    //     }
+        cx.notify();
+        self.serialize_workspace(cx);
+    }
 
     pub fn close_all_docks(&mut self, cx: &mut ViewContext<Self>) {
         let docks = [&self.left_dock, &self.bottom_dock, &self.right_dock];
@@ -1961,50 +1766,50 @@ impl Workspace {
         })
     }
 
-    //     pub fn open_abs_path(
-    //         &mut self,
-    //         abs_path: PathBuf,
-    //         visible: bool,
-    //         cx: &mut ViewContext<Self>,
-    //     ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
-    //         cx.spawn(|workspace, mut cx| async move {
-    //             let open_paths_task_result = workspace
-    //                 .update(&mut cx, |workspace, cx| {
-    //                     workspace.open_paths(vec![abs_path.clone()], visible, cx)
-    //                 })
-    //                 .with_context(|| format!("open abs path {abs_path:?} task spawn"))?
-    //                 .await;
-    //             anyhow::ensure!(
-    //                 open_paths_task_result.len() == 1,
-    //                 "open abs path {abs_path:?} task returned incorrect number of results"
-    //             );
-    //             match open_paths_task_result
-    //                 .into_iter()
-    //                 .next()
-    //                 .expect("ensured single task result")
-    //             {
-    //                 Some(open_result) => {
-    //                     open_result.with_context(|| format!("open abs path {abs_path:?} task join"))
-    //                 }
-    //                 None => anyhow::bail!("open abs path {abs_path:?} task returned None"),
-    //             }
-    //         })
-    //     }
+    pub fn open_abs_path(
+        &mut self,
+        abs_path: PathBuf,
+        visible: bool,
+        cx: &mut ViewContext<Self>,
+    ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
+        cx.spawn(|workspace, mut cx| async move {
+            let open_paths_task_result = workspace
+                .update(&mut cx, |workspace, cx| {
+                    workspace.open_paths(vec![abs_path.clone()], visible, cx)
+                })
+                .with_context(|| format!("open abs path {abs_path:?} task spawn"))?
+                .await;
+            anyhow::ensure!(
+                open_paths_task_result.len() == 1,
+                "open abs path {abs_path:?} task returned incorrect number of results"
+            );
+            match open_paths_task_result
+                .into_iter()
+                .next()
+                .expect("ensured single task result")
+            {
+                Some(open_result) => {
+                    open_result.with_context(|| format!("open abs path {abs_path:?} task join"))
+                }
+                None => anyhow::bail!("open abs path {abs_path:?} task returned None"),
+            }
+        })
+    }
 
-    //     pub fn split_abs_path(
-    //         &mut self,
-    //         abs_path: PathBuf,
-    //         visible: bool,
-    //         cx: &mut ViewContext<Self>,
-    //     ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
-    //         let project_path_task =
-    //             Workspace::project_path_for_path(self.project.clone(), &abs_path, visible, cx);
-    //         cx.spawn(|this, mut cx| async move {
-    //             let (_, path) = project_path_task.await?;
-    //             this.update(&mut cx, |this, cx| this.split_path(path, cx))?
-    //                 .await
-    //         })
-    //     }
+    pub fn split_abs_path(
+        &mut self,
+        abs_path: PathBuf,
+        visible: bool,
+        cx: &mut ViewContext<Self>,
+    ) -> Task<anyhow::Result<Box<dyn ItemHandle>>> {
+        let project_path_task =
+            Workspace::project_path_for_path(self.project.clone(), &abs_path, visible, cx);
+        cx.spawn(|this, mut cx| async move {
+            let (_, path) = project_path_task.await?;
+            this.update(&mut cx, |this, cx| this.split_path(path, cx))?
+                .await
+        })
+    }
 
     pub fn open_path(
         &mut self,
@@ -2031,37 +1836,37 @@ impl Workspace {
         })
     }
 
-    //     pub fn split_path(
-    //         &mut self,
-    //         path: impl Into<ProjectPath>,
-    //         cx: &mut ViewContext<Self>,
-    //     ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
-    //         let pane = self.last_active_center_pane.clone().unwrap_or_else(|| {
-    //             self.panes
-    //                 .first()
-    //                 .expect("There must be an active pane")
-    //                 .downgrade()
-    //         });
+    pub fn split_path(
+        &mut self,
+        path: impl Into<ProjectPath>,
+        cx: &mut ViewContext<Self>,
+    ) -> Task<Result<Box<dyn ItemHandle>, anyhow::Error>> {
+        let pane = self.last_active_center_pane.clone().unwrap_or_else(|| {
+            self.panes
+                .first()
+                .expect("There must be an active pane")
+                .downgrade()
+        });
 
-    //         if let Member::Pane(center_pane) = &self.center.root {
-    //             if center_pane.read(cx).items_len() == 0 {
-    //                 return self.open_path(path, Some(pane), true, cx);
-    //             }
-    //         }
+        if let Member::Pane(center_pane) = &self.center.root {
+            if center_pane.read(cx).items_len() == 0 {
+                return self.open_path(path, Some(pane), true, cx);
+            }
+        }
 
-    //         let task = self.load_path(path.into(), cx);
-    //         cx.spawn(|this, mut cx| async move {
-    //             let (project_entry_id, build_item) = task.await?;
-    //             this.update(&mut cx, move |this, cx| -> Option<_> {
-    //                 let pane = pane.upgrade(cx)?;
-    //                 let new_pane = this.split_pane(pane, SplitDirection::Right, cx);
-    //                 new_pane.update(cx, |new_pane, cx| {
-    //                     Some(new_pane.open_item(project_entry_id, true, cx, build_item))
-    //                 })
-    //             })
-    //             .map(|option| option.ok_or_else(|| anyhow!("pane was dropped")))?
-    //         })
-    //     }
+        let task = self.load_path(path.into(), cx);
+        cx.spawn(|this, mut cx| async move {
+            let (project_entry_id, build_item) = task.await?;
+            this.update(&mut cx, move |this, cx| -> Option<_> {
+                let pane = pane.upgrade()?;
+                let new_pane = this.split_pane(pane, SplitDirection::Right, cx);
+                new_pane.update(cx, |new_pane, cx| {
+                    Some(new_pane.open_item(project_entry_id, true, cx, build_item))
+                })
+            })
+            .map(|option| option.ok_or_else(|| anyhow!("pane was dropped")))?
+        })
+    }
 
     pub(crate) fn load_path(
         &mut self,
@@ -2660,17 +2465,50 @@ impl Workspace {
                 h_stack()
                     // TODO - Add player menu
                     .child(
-                        Button::new("player")
-                            .variant(ButtonVariant::Ghost)
-                            .color(Some(LabelColor::Player(0))),
+                        div()
+                            .id("project_owner_indicator")
+                            .child(
+                                Button::new("player")
+                                    .variant(ButtonVariant::Ghost)
+                                    .color(Some(TextColor::Player(0))),
+                            )
+                            .tooltip(move |_, cx| {
+                                cx.build_view(|cx| TextTooltip::new("Toggle following"))
+                            }),
                     )
                     // TODO - Add project menu
-                    .child(Button::new("project_name").variant(ButtonVariant::Ghost))
+                    .child(
+                        div()
+                            .id("titlebar_project_menu_button")
+                            .child(Button::new("project_name").variant(ButtonVariant::Ghost))
+                            .tooltip(move |_, cx| {
+                                cx.build_view(|cx| TextTooltip::new("Recent Projects"))
+                            }),
+                    )
                     // TODO - Add git menu
                     .child(
-                        Button::new("branch_name")
-                            .variant(ButtonVariant::Ghost)
-                            .color(Some(LabelColor::Muted)),
+                        div()
+                            .id("titlebar_git_menu_button")
+                            .child(
+                                Button::new("branch_name")
+                                    .variant(ButtonVariant::Ghost)
+                                    .color(Some(TextColor::Muted)),
+                            )
+                            .tooltip(move |_, cx| {
+                                // todo!() Replace with real action.
+                                #[gpui::action]
+                                struct NoAction {}
+
+                                cx.build_view(|cx| {
+                                    TextTooltip::new("Recent Branches")
+                                        .key_binding(KeyBinding::new(gpui::KeyBinding::new(
+                                            "cmd-b",
+                                            NoAction {},
+                                            None,
+                                        )))
+                                        .meta("Only local branches shown")
+                                })
+                            }),
                     ),
             ) // self.titlebar_item
             .child(h_stack().child(Label::new("Right side titlebar item")))
@@ -3192,10 +3030,10 @@ impl Workspace {
 
     fn force_remove_pane(&mut self, pane: &View<Pane>, cx: &mut ViewContext<Workspace>) {
         self.panes.retain(|p| p != pane);
-        if true {
-            todo!()
-            // cx.focus(self.panes.last().unwrap());
-        }
+        self.panes
+            .last()
+            .unwrap()
+            .update(cx, |pane, cx| pane.focus(cx));
         if self.last_active_center_pane == Some(pane.downgrade()) {
             self.last_active_center_pane = None;
         }
@@ -3467,9 +3305,105 @@ impl Workspace {
         })
     }
 
+    fn actions(div: Div<Self>) -> Div<Self> {
+        div
+            //     cx.add_async_action(Workspace::open);
+            //     cx.add_async_action(Workspace::follow_next_collaborator);
+            //     cx.add_async_action(Workspace::close);
+            //     cx.add_async_action(Workspace::close_inactive_items_and_panes);
+            //     cx.add_async_action(Workspace::close_all_items_and_panes);
+            //     cx.add_global_action(Workspace::close_global);
+            //     cx.add_global_action(restart);
+            //     cx.add_async_action(Workspace::save_all);
+            //     cx.add_action(Workspace::add_folder_to_project);
+            //     cx.add_action(
+            //         |workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext<Workspace>| {
+            //             let pane = workspace.active_pane().clone();
+            //             workspace.unfollow(&pane, cx);
+            //         },
+            //     );
+            //     cx.add_action(
+            //         |workspace: &mut Workspace, action: &Save, cx: &mut ViewContext<Workspace>| {
+            //             workspace
+            //                 .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), cx)
+            //                 .detach_and_log_err(cx);
+            //         },
+            //     );
+            //     cx.add_action(
+            //         |workspace: &mut Workspace, _: &SaveAs, cx: &mut ViewContext<Workspace>| {
+            //             workspace
+            //                 .save_active_item(SaveIntent::SaveAs, cx)
+            //                 .detach_and_log_err(cx);
+            //         },
+            //     );
+            //     cx.add_action(|workspace: &mut Workspace, _: &ActivatePreviousPane, cx| {
+            //         workspace.activate_previous_pane(cx)
+            //     });
+            //     cx.add_action(|workspace: &mut Workspace, _: &ActivateNextPane, cx| {
+            //         workspace.activate_next_pane(cx)
+            //     });
+            //     cx.add_action(
+            //         |workspace: &mut Workspace, action: &ActivatePaneInDirection, cx| {
+            //             workspace.activate_pane_in_direction(action.0, cx)
+            //         },
+            //     );
+            //     cx.add_action(
+            //         |workspace: &mut Workspace, action: &SwapPaneInDirection, cx| {
+            //             workspace.swap_pane_in_direction(action.0, cx)
+            //         },
+            //     );
+            .on_action(|this, e: &ToggleLeftDock, cx| {
+                println!("TOGGLING DOCK");
+                this.toggle_dock(DockPosition::Left, cx);
+            })
+        //     cx.add_action(|workspace: &mut Workspace, _: &ToggleRightDock, cx| {
+        //         workspace.toggle_dock(DockPosition::Right, cx);
+        //     });
+        //     cx.add_action(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| {
+        //         workspace.toggle_dock(DockPosition::Bottom, cx);
+        //     });
+        //     cx.add_action(|workspace: &mut Workspace, _: &CloseAllDocks, cx| {
+        //         workspace.close_all_docks(cx);
+        //     });
+        //     cx.add_action(Workspace::activate_pane_at_index);
+        //     cx.add_action(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
+        //         workspace.reopen_closed_item(cx).detach();
+        //     });
+        //     cx.add_action(|workspace: &mut Workspace, _: &GoBack, cx| {
+        //         workspace
+        //             .go_back(workspace.active_pane().downgrade(), cx)
+        //             .detach();
+        //     });
+        //     cx.add_action(|workspace: &mut Workspace, _: &GoForward, cx| {
+        //         workspace
+        //             .go_forward(workspace.active_pane().downgrade(), cx)
+        //             .detach();
+        //     });
+
+        //     cx.add_action(|_: &mut Workspace, _: &install_cli::Install, cx| {
+        //         cx.spawn(|workspace, mut cx| async move {
+        //             let err = install_cli::install_cli(&cx)
+        //                 .await
+        //                 .context("Failed to create CLI symlink");
+
+        //             workspace.update(&mut cx, |workspace, cx| {
+        //                 if matches!(err, Err(_)) {
+        //                     err.notify_err(workspace, cx);
+        //                 } else {
+        //                     workspace.show_notification(1, cx, |cx| {
+        //                         cx.build_view(|_| {
+        //                             MessageNotification::new("Successfully installed the `zed` binary")
+        //                         })
+        //                     });
+        //                 }
+        //             })
+        //         })
+        //         .detach();
+        //     });
+    }
+
     #[cfg(any(test, feature = "test-support"))]
     pub fn test_new(project: Model<Project>, cx: &mut ViewContext<Self>) -> Self {
-        use gpui::Context;
         use node_runtime::FakeNodeRuntime;
 
         let client = project.read(cx).client();
@@ -3486,7 +3420,9 @@ impl Workspace {
             initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
             node_runtime: FakeNodeRuntime::new(),
         });
-        Self::new(0, project, app_state, cx)
+        let workspace = Self::new(0, project, app_state, cx);
+        workspace.active_pane.update(cx, |pane, cx| pane.focus(cx));
+        workspace
     }
 
     //     fn render_dock(&self, position: DockPosition, cx: &WindowContext) -> Option<AnyElement<Self>> {
@@ -3522,25 +3458,27 @@ impl Workspace {
     pub fn register_action<A: Action>(
         &mut self,
         callback: impl Fn(&mut Self, &A, &mut ViewContext<Self>) + 'static,
-    ) {
+    ) -> &mut Self {
         let callback = Arc::new(callback);
 
         self.workspace_actions.push(Box::new(move |div| {
             let callback = callback.clone();
             div.on_action(move |workspace, event, cx| (callback.clone())(workspace, event, cx))
         }));
+        self
     }
 
-    fn add_workspace_actions_listeners(
-        &self,
-        mut div: Div<Workspace, StatelessInteractivity<Workspace>>,
-    ) -> Div<Workspace, StatelessInteractivity<Workspace>> {
+    fn add_workspace_actions_listeners(&self, mut div: Div<Workspace>) -> Div<Workspace> {
         for action in self.workspace_actions.iter() {
             div = (action)(div)
         }
         div
     }
 
+    pub fn current_modal<V: Modal + 'static>(&mut self, cx: &ViewContext<Self>) -> Option<View<V>> {
+        self.modal_layer.read(cx).current_modal()
+    }
+
     pub fn toggle_modal<V: Modal, B>(&mut self, cx: &mut ViewContext<Self>, build: B)
     where
         B: FnOnce(&mut ViewContext<V>) -> V,
@@ -3764,14 +3702,15 @@ impl Render for Workspace {
     fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
         let mut context = KeyContext::default();
         context.add("Workspace");
+        let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone();
 
         self.add_workspace_actions_listeners(div())
-            .context(context)
+            .key_context(context)
             .relative()
             .size_full()
             .flex()
             .flex_col()
-            .font("Zed Sans")
+            .font(ui_font)
             .gap_0()
             .justify_start()
             .items_start()
@@ -3791,83 +3730,46 @@ impl Render for Workspace {
                     .border_b()
                     .border_color(cx.theme().colors().border)
                     .child(self.modal_layer.clone())
-                    // .children(
-                    //     Some(
-                    //         Panel::new("project-panel-outer", cx)
-                    //             .side(PanelSide::Left)
-                    //             .child(ProjectPanel::new("project-panel-inner")),
-                    //     )
-                    //     .filter(|_| self.is_project_panel_open()),
-                    // )
-                    // .children(
-                    //     Some(
-                    //         Panel::new("collab-panel-outer", cx)
-                    //             .child(CollabPanel::new("collab-panel-inner"))
-                    //             .side(PanelSide::Left),
-                    //     )
-                    //     .filter(|_| self.is_collab_panel_open()),
-                    // )
-                    // .child(NotificationToast::new(
-                    //     "maxbrunsfeld has requested to add you as a contact.".into(),
-                    // ))
                     .child(
-                        div().flex().flex_col().flex_1().h_full().child(
-                            div().flex().flex_1().child(self.center.render(
-                                &self.project,
-                                &self.follower_states,
-                                self.active_call(),
-                                &self.active_pane,
-                                self.zoomed.as_ref(),
-                                &self.app_state,
-                                cx,
-                            )),
-                        ), // .children(
-                           //     Some(
-                           //         Panel::new("terminal-panel", cx)
-                           //             .child(Terminal::new())
-                           //             .allowed_sides(PanelAllowedSides::BottomOnly)
-                           //             .side(PanelSide::Bottom),
-                           //     )
-                           //     .filter(|_| self.is_terminal_open()),
-                           // ),
-                    ), // .children(
-                       //     Some(
-                       //         Panel::new("chat-panel-outer", cx)
-                       //             .side(PanelSide::Right)
-                       //             .child(ChatPanel::new("chat-panel-inner").messages(vec![
-                       //                 ChatMessage::new(
-                       //                     "osiewicz".to_string(),
-                       //                     "is this thing on?".to_string(),
-                       //                     DateTime::parse_from_rfc3339("2023-09-27T15:40:52.707Z")
-                       //                         .unwrap()
-                       //                         .naive_local(),
-                       //                 ),
-                       //                 ChatMessage::new(
-                       //                     "maxdeviant".to_string(),
-                       //                     "Reading you loud and clear!".to_string(),
-                       //                     DateTime::parse_from_rfc3339("2023-09-28T15:40:52.707Z")
-                       //                         .unwrap()
-                       //                         .naive_local(),
-                       //                 ),
-                       //             ])),
-                       //     )
-                       //     .filter(|_| self.is_chat_panel_open()),
-                       // )
-                       // .children(
-                       //     Some(
-                       //         Panel::new("notifications-panel-outer", cx)
-                       //             .side(PanelSide::Right)
-                       //             .child(NotificationsPanel::new("notifications-panel-inner")),
-                       //     )
-                       //     .filter(|_| self.is_notifications_panel_open()),
-                       // )
-                       // .children(
-                       //     Some(
-                       //         Panel::new("assistant-panel-outer", cx)
-                       //             .child(AssistantPanel::new("assistant-panel-inner")),
-                       //     )
-                       //     .filter(|_| self.is_assistant_panel_open()),
-                       // ),
+                        div()
+                            .flex()
+                            .flex_row()
+                            .flex_1()
+                            .h_full()
+                            // Left Dock
+                            .child(
+                                div()
+                                    .flex()
+                                    .flex_none()
+                                    .overflow_hidden()
+                                    .child(self.left_dock.clone()),
+                            )
+                            // Panes
+                            .child(
+                                div()
+                                    .flex()
+                                    .flex_col()
+                                    .flex_1()
+                                    .child(self.center.render(
+                                        &self.project,
+                                        &self.follower_states,
+                                        self.active_call(),
+                                        &self.active_pane,
+                                        self.zoomed.as_ref(),
+                                        &self.app_state,
+                                        cx,
+                                    ))
+                                    .child(div().flex().flex_1().child(self.bottom_dock.clone())),
+                            )
+                            // Right Dock
+                            .child(
+                                div()
+                                    .flex()
+                                    .flex_none()
+                                    .overflow_hidden()
+                                    .child(self.right_dock.clone()),
+                            ),
+                    ),
             )
             .child(self.status_bar.clone())
             // .when(self.debug.show_toast, |this| {
@@ -4522,32 +4424,32 @@ pub fn open_new(
     })
 }
 
-// pub fn create_and_open_local_file(
-//     path: &'static Path,
-//     cx: &mut ViewContext<Workspace>,
-//     default_content: impl 'static + Send + FnOnce() -> Rope,
-// ) -> Task<Result<Box<dyn ItemHandle>>> {
-//     cx.spawn(|workspace, mut cx| async move {
-//         let fs = workspace.read_with(&cx, |workspace, _| workspace.app_state().fs.clone())?;
-//         if !fs.is_file(path).await {
-//             fs.create_file(path, Default::default()).await?;
-//             fs.save(path, &default_content(), Default::default())
-//                 .await?;
-//         }
+pub fn create_and_open_local_file(
+    path: &'static Path,
+    cx: &mut ViewContext<Workspace>,
+    default_content: impl 'static + Send + FnOnce() -> Rope,
+) -> Task<Result<Box<dyn ItemHandle>>> {
+    cx.spawn(|workspace, mut cx| async move {
+        let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?;
+        if !fs.is_file(path).await {
+            fs.create_file(path, Default::default()).await?;
+            fs.save(path, &default_content(), Default::default())
+                .await?;
+        }
 
-//         let mut items = workspace
-//             .update(&mut cx, |workspace, cx| {
-//                 workspace.with_local_workspace(cx, |workspace, cx| {
-//                     workspace.open_paths(vec![path.to_path_buf()], false, cx)
-//                 })
-//             })?
-//             .await?
-//             .await;
+        let mut items = workspace
+            .update(&mut cx, |workspace, cx| {
+                workspace.with_local_workspace(cx, |workspace, cx| {
+                    workspace.open_paths(vec![path.to_path_buf()], false, cx)
+                })
+            })?
+            .await?
+            .await;
 
-//         let item = items.pop().flatten();
-//         item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
-//     })
-// }
+        let item = items.pop().flatten();
+        item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
+    })
+}
 
 // pub fn join_remote_project(
 //     project_id: u64,

crates/zed2/Cargo.toml 🔗

@@ -36,14 +36,14 @@ copilot = { package = "copilot2", path = "../copilot2" }
 db = { package = "db2", path = "../db2" }
 editor = { package="editor2", path = "../editor2" }
 # feedback = { path = "../feedback" }
-# file_finder = { path = "../file_finder" }
+file_finder = { package="file_finder2", path = "../file_finder2" }
 # search = { path = "../search" }
 fs = { package = "fs2", path = "../fs2" }
 fsevent = { path = "../fsevent" }
 fuzzy = { path = "../fuzzy" }
 go_to_line = { package = "go_to_line2", path = "../go_to_line2" }
 gpui = { package = "gpui2", path = "../gpui2" }
-install_cli = { path = "../install_cli" }
+install_cli = { package = "install_cli2", path = "../install_cli2" }
 journal = { package = "journal2", path = "../journal2" }
 language = { package = "language2", path = "../language2" }
 # language_selector = { path = "../language_selector" }
@@ -55,7 +55,7 @@ node_runtime = { path = "../node_runtime" }
 # outline = { path = "../outline" }
 # plugin_runtime = { path = "../plugin_runtime",optional = true }
 project = { package = "project2", path = "../project2" }
-# project_panel = { path = "../project_panel" }
+project_panel = { package = "project_panel2", path = "../project_panel2" }
 # project_symbols = { path = "../project_symbols" }
 # quick_action_bar = { path = "../quick_action_bar" }
 # recent_projects = { path = "../recent_projects" }

crates/zed2/src/main.rs 🔗

@@ -50,14 +50,16 @@ use util::{
 use uuid::Uuid;
 use workspace::{AppState, WorkspaceStore};
 use zed2::{
-    build_window_options, ensure_only_instance, handle_cli_connection, initialize_workspace,
-    languages, Assets, IsOnlyInstance, OpenListener, OpenRequest,
+    build_window_options, ensure_only_instance, handle_cli_connection, init_zed_actions,
+    initialize_workspace, languages, Assets, IsOnlyInstance, OpenListener, OpenRequest,
 };
 
 mod open_listener;
 
 fn main() {
     menu::init();
+    zed_actions::init();
+
     let http = http::client();
     init_paths();
     init_logger();
@@ -96,7 +98,7 @@ fn main() {
     let (listener, mut open_rx) = OpenListener::new();
     let listener = Arc::new(listener);
     let open_listener = listener.clone();
-    app.on_open_urls(move |urls, _| open_listener.open_urls(urls));
+    app.on_open_urls(move |urls, _| open_listener.open_urls(&urls));
     app.on_reopen(move |_cx| {
         // todo!("workspace")
         // if cx.has_global::<Weak<AppState>>() {
@@ -111,6 +113,8 @@ fn main() {
 
     app.run(move |cx| {
         cx.set_global(*RELEASE_CHANNEL);
+        cx.set_global(listener.clone());
+
         load_embedded_fonts(cx);
 
         let mut store = SettingsStore::default();
@@ -186,10 +190,10 @@ fn main() {
         // recent_projects::init(cx);
 
         go_to_line::init(cx);
-        // file_finder::init(cx);
+        file_finder::init(cx);
         // outline::init(cx);
         // project_symbols::init(cx);
-        // project_panel::init(Assets, cx);
+        project_panel::init(Assets, cx);
         // channel::init(&client, user_store.clone(), cx);
         // diagnostics::init(cx);
         // search::init(cx);
@@ -209,12 +213,13 @@ fn main() {
         // zed::init(&app_state, cx);
 
         // cx.set_menus(menus::menus());
+        init_zed_actions(app_state.clone(), cx);
 
         if stdout_is_a_pty() {
             cx.activate(true);
             let urls = collect_url_args();
             if !urls.is_empty() {
-                listener.open_urls(urls)
+                listener.open_urls(&urls)
             }
         } else {
             upload_previous_panics(http.clone(), cx);
@@ -224,7 +229,7 @@ fn main() {
             if std::env::var(FORCE_CLI_MODE_ENV_VAR_NAME).ok().is_some()
                 && !listener.triggered.load(Ordering::Acquire)
             {
-                listener.open_urls(collect_url_args())
+                listener.open_urls(&collect_url_args())
             }
         }
 

crates/zed2/src/open_listener.rs 🔗

@@ -54,7 +54,7 @@ impl OpenListener {
         )
     }
 
-    pub fn open_urls(&self, urls: Vec<String>) {
+    pub fn open_urls(&self, urls: &[String]) {
         self.triggered.store(true, Ordering::Release);
         let request = if let Some(server_name) =
             urls.first().and_then(|url| url.strip_prefix("zed-cli://"))
@@ -101,7 +101,7 @@ impl OpenListener {
         None
     }
 
-    fn handle_file_urls(&self, urls: Vec<String>) -> Option<OpenRequest> {
+    fn handle_file_urls(&self, urls: &[String]) -> Option<OpenRequest> {
         let paths: Vec<_> = urls
             .iter()
             .flat_map(|url| url.strip_prefix("file://"))

crates/zed2/src/zed2.rs 🔗

@@ -1,5 +1,5 @@
-#![allow(unused_variables, dead_code, unused_mut)]
-// todo!() this is to make transition easier.
+#![allow(unused_variables, unused_mut)]
+//todo!()
 
 mod assets;
 pub mod languages;
@@ -7,17 +7,56 @@ mod only_instance;
 mod open_listener;
 
 pub use assets::*;
+use collections::VecDeque;
+use editor::{Editor, MultiBuffer};
 use gpui::{
-    point, px, AppContext, AsyncWindowContext, Task, TitlebarOptions, WeakView, WindowBounds,
-    WindowKind, WindowOptions,
+    actions, point, px, AppContext, AsyncWindowContext, Context, PromptLevel, Task,
+    TitlebarOptions, ViewContext, VisualContext, WeakView, WindowBounds, WindowKind, WindowOptions,
 };
 pub use only_instance::*;
 pub use open_listener::*;
 
-use anyhow::Result;
-use std::sync::Arc;
+use anyhow::{anyhow, Context as _, Result};
+use project_panel::ProjectPanel;
+use settings::{initial_local_settings_content, Settings};
+use std::{borrow::Cow, ops::Deref, sync::Arc};
+use util::{
+    asset_str,
+    channel::ReleaseChannel,
+    paths::{self, LOCAL_SETTINGS_RELATIVE_PATH},
+    ResultExt,
+};
 use uuid::Uuid;
-use workspace::{AppState, Workspace};
+use workspace::{
+    create_and_open_local_file, dock::PanelHandle,
+    notifications::simple_message_notification::MessageNotification, open_new, AppState, NewFile,
+    NewWindow, Workspace, WorkspaceSettings,
+};
+use zed_actions::{OpenBrowser, OpenZedURL};
+
+actions!(
+    About,
+    DebugElements,
+    DecreaseBufferFontSize,
+    Hide,
+    HideOthers,
+    IncreaseBufferFontSize,
+    Minimize,
+    OpenDefaultKeymap,
+    OpenDefaultSettings,
+    OpenKeymap,
+    OpenLicenses,
+    OpenLocalSettings,
+    OpenLog,
+    OpenSettings,
+    OpenTelemetryLog,
+    Quit,
+    ResetBufferFontSize,
+    ResetDatabase,
+    ShowAll,
+    ToggleFullScreen,
+    Zoom,
+);
 
 pub fn build_window_options(
     bounds: Option<WindowBounds>,
@@ -47,6 +86,211 @@ pub fn build_window_options(
     }
 }
 
+pub fn init_zed_actions(app_state: Arc<AppState>, cx: &mut AppContext) {
+    cx.observe_new_views(move |workspace: &mut Workspace, _cx| {
+        workspace
+            .register_action(about)
+            .register_action(|_, _: &Hide, cx| {
+                cx.hide();
+            })
+            .register_action(|_, _: &HideOthers, cx| {
+                cx.hide_other_apps();
+            })
+            .register_action(|_, _: &ShowAll, cx| {
+                cx.unhide_other_apps();
+            })
+            .register_action(|_, _: &Minimize, cx| {
+                cx.minimize_window();
+            })
+            .register_action(|_, _: &Zoom, cx| {
+                cx.zoom_window();
+            })
+            .register_action(|_, _: &ToggleFullScreen, cx| {
+                cx.toggle_full_screen();
+            })
+            .register_action(quit)
+            .register_action(|_, action: &OpenZedURL, cx| {
+                cx.global::<Arc<OpenListener>>()
+                    .open_urls(&[action.url.clone()])
+            })
+            .register_action(|_, action: &OpenBrowser, cx| cx.open_url(&action.url))
+            //todo!(buffer font size)
+            // cx.add_global_action(move |_: &IncreaseBufferFontSize, cx| {
+            //     theme::adjust_font_size(cx, |size| *size += 1.0)
+            // });
+            // cx.add_global_action(move |_: &DecreaseBufferFontSize, cx| {
+            //     theme::adjust_font_size(cx, |size| *size -= 1.0)
+            // });
+            // cx.add_global_action(move |_: &ResetBufferFontSize, cx| theme::reset_font_size(cx));
+            .register_action(|_, _: &install_cli::Install, cx| {
+                cx.spawn(|_, cx| async move {
+                    install_cli::install_cli(cx.deref())
+                        .await
+                        .context("error creating CLI symlink")
+                })
+                .detach_and_log_err(cx);
+            })
+            .register_action(|workspace, _: &OpenLog, cx| {
+                open_log_file(workspace, cx);
+            })
+            .register_action(|workspace, _: &OpenLicenses, cx| {
+                open_bundled_file(
+                    workspace,
+                    asset_str::<Assets>("licenses.md"),
+                    "Open Source License Attribution",
+                    "Markdown",
+                    cx,
+                );
+            })
+            .register_action(
+                move |workspace: &mut Workspace,
+                      _: &OpenTelemetryLog,
+                      cx: &mut ViewContext<Workspace>| {
+                    open_telemetry_log_file(workspace, cx);
+                },
+            )
+            .register_action(
+                move |_: &mut Workspace, _: &OpenKeymap, cx: &mut ViewContext<Workspace>| {
+                    create_and_open_local_file(&paths::KEYMAP, cx, Default::default)
+                        .detach_and_log_err(cx);
+                },
+            )
+            .register_action(
+                move |_: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext<Workspace>| {
+                    create_and_open_local_file(&paths::SETTINGS, cx, || {
+                        settings::initial_user_settings_content().as_ref().into()
+                    })
+                    .detach_and_log_err(cx);
+                },
+            )
+            .register_action(open_local_settings_file)
+            .register_action(
+                move |workspace: &mut Workspace,
+                      _: &OpenDefaultKeymap,
+                      cx: &mut ViewContext<Workspace>| {
+                    open_bundled_file(
+                        workspace,
+                        settings::default_keymap(),
+                        "Default Key Bindings",
+                        "JSON",
+                        cx,
+                    );
+                },
+            )
+            .register_action(
+                move |workspace: &mut Workspace,
+                      _: &OpenDefaultSettings,
+                      cx: &mut ViewContext<Workspace>| {
+                    open_bundled_file(
+                        workspace,
+                        settings::default_settings(),
+                        "Default Settings",
+                        "JSON",
+                        cx,
+                    );
+                },
+            )
+            //todo!()
+            // cx.add_action({
+            //     move |workspace: &mut Workspace, _: &DebugElements, cx: &mut ViewContext<Workspace>| {
+            //         let app_state = workspace.app_state().clone();
+            //         let markdown = app_state.languages.language_for_name("JSON");
+            //         let window = cx.window();
+            //         cx.spawn(|workspace, mut cx| async move {
+            //             let markdown = markdown.await.log_err();
+            //             let content = to_string_pretty(&window.debug_elements(&cx).ok_or_else(|| {
+            //                 anyhow!("could not debug elements for window {}", window.id())
+            //             })?)
+            //             .unwrap();
+            //             workspace
+            //                 .update(&mut cx, |workspace, cx| {
+            //                     workspace.with_local_workspace(cx, move |workspace, cx| {
+            //                         let project = workspace.project().clone();
+            //                         let buffer = project
+            //                             .update(cx, |project, cx| {
+            //                                 project.create_buffer(&content, markdown, cx)
+            //                             })
+            //                             .expect("creating buffers on a local workspace always succeeds");
+            //                         let buffer = cx.add_model(|cx| {
+            //                             MultiBuffer::singleton(buffer, cx)
+            //                                 .with_title("Debug Elements".into())
+            //                         });
+            //                         workspace.add_item(
+            //                             Box::new(cx.add_view(|cx| {
+            //                                 Editor::for_multibuffer(buffer, Some(project.clone()), cx)
+            //                             })),
+            //                             cx,
+            //                         );
+            //                     })
+            //                 })?
+            //                 .await
+            //         })
+            //         .detach_and_log_err(cx);
+            //     }
+            // });
+            // .register_action(
+            //     |workspace: &mut Workspace,
+            //      _: &project_panel::ToggleFocus,
+            //      cx: &mut ViewContext<Workspace>| {
+            //         workspace.toggle_panel_focus::<ProjectPanel>(cx);
+            //     },
+            // );
+            // cx.add_action(
+            //     |workspace: &mut Workspace,
+            //      _: &collab_ui::collab_panel::ToggleFocus,
+            //      cx: &mut ViewContext<Workspace>| {
+            //         workspace.toggle_panel_focus::<collab_ui::collab_panel::CollabPanel>(cx);
+            //     },
+            // );
+            // cx.add_action(
+            //     |workspace: &mut Workspace,
+            //      _: &collab_ui::chat_panel::ToggleFocus,
+            //      cx: &mut ViewContext<Workspace>| {
+            //         workspace.toggle_panel_focus::<collab_ui::chat_panel::ChatPanel>(cx);
+            //     },
+            // );
+            // cx.add_action(
+            //     |workspace: &mut Workspace,
+            //      _: &collab_ui::notification_panel::ToggleFocus,
+            //      cx: &mut ViewContext<Workspace>| {
+            //         workspace.toggle_panel_focus::<collab_ui::notification_panel::NotificationPanel>(cx);
+            //     },
+            // );
+            // cx.add_action(
+            //     |workspace: &mut Workspace,
+            //      _: &terminal_panel::ToggleFocus,
+            //      cx: &mut ViewContext<Workspace>| {
+            //         workspace.toggle_panel_focus::<TerminalPanel>(cx);
+            //     },
+            // );
+            .register_action({
+                let app_state = Arc::downgrade(&app_state);
+                move |_, _: &NewWindow, cx| {
+                    if let Some(app_state) = app_state.upgrade() {
+                        open_new(&app_state, cx, |workspace, cx| {
+                            Editor::new_file(workspace, &Default::default(), cx)
+                        })
+                        .detach();
+                    }
+                }
+            })
+            .register_action({
+                let app_state = Arc::downgrade(&app_state);
+                move |_, _: &NewFile, cx| {
+                    if let Some(app_state) = app_state.upgrade() {
+                        open_new(&app_state, cx, |workspace, cx| {
+                            Editor::new_file(workspace, &Default::default(), cx)
+                        })
+                        .detach();
+                    }
+                }
+            });
+        //todo!()
+        // load_default_keymap(cx);
+    })
+    .detach();
+}
+
 pub fn initialize_workspace(
     workspace_handle: WeakView<Workspace>,
     was_deserialized: bool,
@@ -138,49 +382,38 @@ pub fn initialize_workspace(
             //         }
             //         false
             //     });
-            // })?;
-
-            // let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone());
-            // let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone());
-            // let assistant_panel = AssistantPanel::load(workspace_handle.clone(), cx.clone());
-            // let channels_panel =
-            //     collab_ui::collab_panel::CollabPanel::load(workspace_handle.clone(), cx.clone());
-            // let chat_panel =
-            //     collab_ui::chat_panel::ChatPanel::load(workspace_handle.clone(), cx.clone());
-            // let notification_panel = collab_ui::notification_panel::NotificationPanel::load(
-            //     workspace_handle.clone(),
-            //     cx.clone(),
-            // );
-            // let (
-            //     project_panel,
+        })?;
+
+        let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone());
+        // let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone());
+        // let assistant_panel = AssistantPanel::load(workspace_handle.clone(), cx.clone());
+        // let channels_panel =
+        //     collab_ui::collab_panel::CollabPanel::load(workspace_handle.clone(), cx.clone());
+        // let chat_panel =
+        //     collab_ui::chat_panel::ChatPanel::load(workspace_handle.clone(), cx.clone());
+        // let notification_panel = collab_ui::notification_panel::NotificationPanel::load(
+        //     workspace_handle.clone(),
+        //     cx.clone(),
+        // );
+        let (
+            project_panel,
             //     terminal_panel,
             //     assistant_panel,
             //     channels_panel,
             //     chat_panel,
             //     notification_panel,
-            // ) = futures::try_join!(
-            //     project_panel,
+        ) = futures::try_join!(
+            project_panel,
             //     terminal_panel,
             //     assistant_panel,
             //     channels_panel,
             //     chat_panel,
             //     notification_panel,
-            // )?;
-            // workspace_handle.update(&mut cx, |workspace, cx| {
-            //     let project_panel_position = project_panel.position(cx);
-            //     workspace.add_panel_with_extra_event_handler(
-            //         project_panel,
-            //         cx,
-            //         |workspace, _, event, cx| match event {
-            //             project_panel::Event::NewSearchInDirectory { dir_entry } => {
-            //                 search::ProjectSearchView::new_search_in_directory(workspace, dir_entry, cx)
-            //             }
-            //             project_panel::Event::ActivatePanel => {
-            //                 workspace.focus_panel::<ProjectPanel>(cx);
-            //             }
-            //             _ => {}
-            //         },
-            //     );
+        )?;
+
+        workspace_handle.update(&mut cx, |workspace, cx| {
+            let project_panel_position = project_panel.position(cx);
+            workspace.add_panel(project_panel, cx);
             //     workspace.add_panel(terminal_panel, cx);
             //     workspace.add_panel(assistant_panel, cx);
             //     workspace.add_panel(channels_panel, cx);
@@ -198,10 +431,286 @@ pub fn initialize_workspace(
             //                     .map_or(false, |entry| entry.is_dir())
             //             })
             //     {
-            //         workspace.toggle_dock(project_panel_position, cx);
+            workspace.toggle_dock(project_panel_position, cx);
             //     }
-            //     cx.focus_self();
+            // cx.focus_self();
         })?;
         Ok(())
     })
 }
+
+fn about(_: &mut Workspace, _: &About, cx: &mut gpui::ViewContext<Workspace>) {
+    let app_name = cx.global::<ReleaseChannel>().display_name();
+    let version = env!("CARGO_PKG_VERSION");
+    let prompt = cx.prompt(PromptLevel::Info, &format!("{app_name} {version}"), &["OK"]);
+    cx.foreground_executor()
+        .spawn(async {
+            prompt.await.ok();
+        })
+        .detach();
+}
+
+fn quit(_: &mut Workspace, _: &Quit, cx: &mut gpui::ViewContext<Workspace>) {
+    let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
+    cx.spawn(|_, mut cx| async move {
+        let mut workspace_windows = cx.update(|_, cx| {
+            cx.windows()
+                .into_iter()
+                .filter_map(|window| window.downcast::<Workspace>())
+                .collect::<Vec<_>>()
+        })?;
+
+        // If multiple windows have unsaved changes, and need a save prompt,
+        // prompt in the active window before switching to a different window.
+        cx.update(|_, cx| {
+            workspace_windows.sort_by_key(|window| window.is_active(&cx) == Some(false));
+        })
+        .log_err();
+
+        if let (true, Some(window)) = (should_confirm, workspace_windows.first().copied()) {
+            let answer = cx
+                .update(|_, cx| {
+                    cx.prompt(
+                        PromptLevel::Info,
+                        "Are you sure you want to quit?",
+                        &["Quit", "Cancel"],
+                    )
+                })
+                .log_err();
+
+            if let Some(mut answer) = answer {
+                let answer = answer.await.ok();
+                if answer != Some(0) {
+                    return Ok(());
+                }
+            }
+        }
+
+        // If the user cancels any save prompt, then keep the app open.
+        for window in workspace_windows {
+            if let Some(should_close) = window
+                .update(&mut cx, |workspace, cx| {
+                    workspace.prepare_to_close(true, cx)
+                })
+                .log_err()
+            {
+                if !should_close.await? {
+                    return Ok(());
+                }
+            }
+        }
+        cx.update(|_, cx| {
+            cx.quit();
+        })?;
+        anyhow::Ok(())
+    })
+    .detach_and_log_err(cx);
+}
+
+fn open_log_file(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
+    const MAX_LINES: usize = 1000;
+    workspace
+        .with_local_workspace(cx, move |workspace, cx| {
+            let fs = workspace.app_state().fs.clone();
+            cx.spawn(|workspace, mut cx| async move {
+                let (old_log, new_log) =
+                    futures::join!(fs.load(&paths::OLD_LOG), fs.load(&paths::LOG));
+
+                let mut lines = VecDeque::with_capacity(MAX_LINES);
+                for line in old_log
+                    .iter()
+                    .flat_map(|log| log.lines())
+                    .chain(new_log.iter().flat_map(|log| log.lines()))
+                {
+                    if lines.len() == MAX_LINES {
+                        lines.pop_front();
+                    }
+                    lines.push_back(line);
+                }
+                let log = lines
+                    .into_iter()
+                    .flat_map(|line| [line, "\n"])
+                    .collect::<String>();
+
+                workspace
+                    .update(&mut cx, |workspace, cx| {
+                        let project = workspace.project().clone();
+                        let buffer = project
+                            .update(cx, |project, cx| project.create_buffer("", None, cx))
+                            .expect("creating buffers on a local workspace always succeeds");
+                        buffer.update(cx, |buffer, cx| buffer.edit([(0..0, log)], None, cx));
+
+                        let buffer = cx.build_model(|cx| {
+                            MultiBuffer::singleton(buffer, cx).with_title("Log".into())
+                        });
+                        workspace.add_item(
+                            Box::new(cx.build_view(|cx| {
+                                Editor::for_multibuffer(buffer, Some(project), cx)
+                            })),
+                            cx,
+                        );
+                    })
+                    .log_err();
+            })
+            .detach();
+        })
+        .detach();
+}
+
+fn open_local_settings_file(
+    workspace: &mut Workspace,
+    _: &OpenLocalSettings,
+    cx: &mut ViewContext<Workspace>,
+) {
+    let project = workspace.project().clone();
+    let worktree = project
+        .read(cx)
+        .visible_worktrees(cx)
+        .find_map(|tree| tree.read(cx).root_entry()?.is_dir().then_some(tree));
+    if let Some(worktree) = worktree {
+        let tree_id = worktree.read(cx).id();
+        cx.spawn(|workspace, mut cx| async move {
+            let file_path = &*LOCAL_SETTINGS_RELATIVE_PATH;
+
+            if let Some(dir_path) = file_path.parent() {
+                if worktree.update(&mut cx, |tree, _| tree.entry_for_path(dir_path).is_none())? {
+                    project
+                        .update(&mut cx, |project, cx| {
+                            project.create_entry((tree_id, dir_path), true, cx)
+                        })?
+                        .ok_or_else(|| anyhow!("worktree was removed"))?
+                        .await?;
+                }
+            }
+
+            if worktree.update(&mut cx, |tree, _| tree.entry_for_path(file_path).is_none())? {
+                project
+                    .update(&mut cx, |project, cx| {
+                        project.create_entry((tree_id, file_path), false, cx)
+                    })?
+                    .ok_or_else(|| anyhow!("worktree was removed"))?
+                    .await?;
+            }
+
+            let editor = workspace
+                .update(&mut cx, |workspace, cx| {
+                    workspace.open_path((tree_id, file_path), None, true, cx)
+                })?
+                .await?
+                .downcast::<Editor>()
+                .ok_or_else(|| anyhow!("unexpected item type"))?;
+
+            editor
+                .downgrade()
+                .update(&mut cx, |editor, cx| {
+                    if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
+                        if buffer.read(cx).is_empty() {
+                            buffer.update(cx, |buffer, cx| {
+                                buffer.edit([(0..0, initial_local_settings_content())], None, cx)
+                            });
+                        }
+                    }
+                })
+                .ok();
+
+            anyhow::Ok(())
+        })
+        .detach();
+    } else {
+        workspace.show_notification(0, cx, |cx| {
+            cx.build_view(|_| MessageNotification::new("This project has no folders open."))
+        })
+    }
+}
+
+fn open_telemetry_log_file(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
+    workspace.with_local_workspace(cx, move |workspace, cx| {
+        let app_state = workspace.app_state().clone();
+        cx.spawn(|workspace, mut cx| async move {
+            async fn fetch_log_string(app_state: &Arc<AppState>) -> Option<String> {
+                let path = app_state.client.telemetry().log_file_path()?;
+                app_state.fs.load(&path).await.log_err()
+            }
+
+            let log = fetch_log_string(&app_state).await.unwrap_or_else(|| "// No data has been collected yet".to_string());
+
+            const MAX_TELEMETRY_LOG_LEN: usize = 5 * 1024 * 1024;
+            let mut start_offset = log.len().saturating_sub(MAX_TELEMETRY_LOG_LEN);
+            if let Some(newline_offset) = log[start_offset..].find('\n') {
+                start_offset += newline_offset + 1;
+            }
+            let log_suffix = &log[start_offset..];
+            let json = app_state.languages.language_for_name("JSON").await.log_err();
+
+            workspace.update(&mut cx, |workspace, cx| {
+                let project = workspace.project().clone();
+                let buffer = project
+                    .update(cx, |project, cx| project.create_buffer("", None, cx))
+                    .expect("creating buffers on a local workspace always succeeds");
+                buffer.update(cx, |buffer, cx| {
+                    buffer.set_language(json, cx);
+                    buffer.edit(
+                        [(
+                            0..0,
+                            concat!(
+                                "// Zed collects anonymous usage data to help us understand how people are using the app.\n",
+                                "// Telemetry can be disabled via the `settings.json` file.\n",
+                                "// Here is the data that has been reported for the current session:\n",
+                                "\n"
+                            ),
+                        )],
+                        None,
+                        cx,
+                    );
+                    buffer.edit([(buffer.len()..buffer.len(), log_suffix)], None, cx);
+                });
+
+                let buffer = cx.build_model(|cx| {
+                    MultiBuffer::singleton(buffer, cx).with_title("Telemetry Log".into())
+                });
+                workspace.add_item(
+                    Box::new(cx.build_view(|cx| Editor::for_multibuffer(buffer, Some(project), cx))),
+                    cx,
+                );
+            }).log_err()?;
+
+            Some(())
+        })
+        .detach();
+    }).detach();
+}
+
+fn open_bundled_file(
+    workspace: &mut Workspace,
+    text: Cow<'static, str>,
+    title: &'static str,
+    language: &'static str,
+    cx: &mut ViewContext<Workspace>,
+) {
+    let language = workspace.app_state().languages.language_for_name(language);
+    cx.spawn(|workspace, mut cx| async move {
+        let language = language.await.log_err();
+        workspace
+            .update(&mut cx, |workspace, cx| {
+                workspace.with_local_workspace(cx, |workspace, cx| {
+                    let project = workspace.project();
+                    let buffer = project.update(cx, move |project, cx| {
+                        project
+                            .create_buffer(text.as_ref(), language, cx)
+                            .expect("creating buffers on a local workspace always succeeds")
+                    });
+                    let buffer = cx.build_model(|cx| {
+                        MultiBuffer::singleton(buffer, cx).with_title(title.into())
+                    });
+                    workspace.add_item(
+                        Box::new(cx.build_view(|cx| {
+                            Editor::for_multibuffer(buffer, Some(project.clone()), cx)
+                        })),
+                        cx,
+                    );
+                })
+            })?
+            .await
+    })
+    .detach_and_log_err(cx);
+}

crates/zed_actions2/src/lib.rs 🔗

@@ -1,33 +1,19 @@
-use gpui::{action, actions};
+use gpui::action;
 
-actions!(
-    About,
-    DebugElements,
-    DecreaseBufferFontSize,
-    Hide,
-    HideOthers,
-    IncreaseBufferFontSize,
-    Minimize,
-    OpenDefaultKeymap,
-    OpenDefaultSettings,
-    OpenKeymap,
-    OpenLicenses,
-    OpenLocalSettings,
-    OpenLog,
-    OpenSettings,
-    OpenTelemetryLog,
-    Quit,
-    ResetBufferFontSize,
-    ResetDatabase,
-    ShowAll,
-    ToggleFullScreen,
-    Zoom,
-);
+// If the zed binary doesn't use anything in this crate, it will be optimized away
+// and the actions won't initialize. So we just provide an empty initialization function
+// to be called from main.
+//
+// These may provide relevant context:
+// https://github.com/rust-lang/rust/issues/47384
+// https://github.com/mmastrac/rust-ctor/issues/280
+pub fn init() {}
 
 #[action]
 pub struct OpenBrowser {
     pub url: String,
 }
+
 #[action]
 pub struct OpenZedURL {
     pub url: String,