Merge branch 'main' into editor2-autocomplete

Antonio Scandurra created

Change summary

Cargo.lock                                     |  33 
Cargo.toml                                     |   1 
assets/settings/default.json                   |   9 
crates/collab2/src/tests/test_server.rs        |   2 
crates/collab_ui2/src/collab_panel.rs          |   1 
crates/collab_ui2/src/collab_titlebar_item.rs  |  24 
crates/command_palette2/src/command_palette.rs |  20 
crates/copilot/src/copilot.rs                  |  20 
crates/editor2/src/display_map.rs              |  29 
crates/editor2/src/display_map/inlay_map.rs    |   2 
crates/editor2/src/editor.rs                   |  82 
crates/editor2/src/editor_tests.rs             |   2 
crates/editor2/src/element.rs                  | 133 +-
crates/editor2/src/inlay_hint_cache.rs         |   2 
crates/editor2/src/items.rs                    |   4 
crates/editor2/src/movement.rs                 |  10 
crates/editor2/src/selections_collection.rs    |   8 
crates/file_finder2/src/file_finder.rs         |  23 
crates/go_to_line2/src/go_to_line.rs           |  19 
crates/gpui2/Cargo.toml                        |   1 
crates/gpui2/src/action.rs                     | 247 ++---
crates/gpui2/src/app.rs                        |  27 
crates/gpui2/src/app/async_context.rs          |   4 
crates/gpui2/src/app/test_context.rs           |  17 
crates/gpui2/src/element.rs                    |   2 
crates/gpui2/src/elements/div.rs               |  22 
crates/gpui2/src/elements/overlay.rs           |  39 
crates/gpui2/src/elements/text.rs              | 191 ++-
crates/gpui2/src/executor.rs                   |  66 +
crates/gpui2/src/geometry.rs                   |  49 +
crates/gpui2/src/gpui2.rs                      |   2 
crates/gpui2/src/key_dispatch.rs               |  13 
crates/gpui2/src/keymap/keymap.rs              |  30 
crates/gpui2/src/platform.rs                   |   6 
crates/gpui2/src/platform/mac/dispatcher.rs    |   6 
crates/gpui2/src/platform/mac/text_system.rs   |  10 
crates/gpui2/src/platform/mac/window.rs        |  36 
crates/gpui2/src/platform/test/dispatcher.rs   |  83 +
crates/gpui2/src/style.rs                      |   1 
crates/gpui2/src/text_system.rs                |  82 +
crates/gpui2/src/text_system/line.rs           | 265 +++--
crates/gpui2/src/text_system/line_layout.rs    | 136 ++
crates/gpui2/src/window.rs                     |  30 
crates/gpui2/tests/action_macros.rs            |  45 +
crates/gpui2_macros/Cargo.toml                 |   2 
crates/gpui2_macros/src/action.rs              | 103 +
crates/gpui2_macros/src/gpui2_macros.rs        |   8 
crates/gpui2_macros/src/register_action.rs     |  78 +
crates/language/src/buffer.rs                  |  90 +
crates/language2/src/buffer.rs                 | 215 +++-
crates/live_kit_client2/Cargo.toml             |   2 
crates/live_kit_client2/examples/test_app2.rs  |   4 
crates/picker2/src/picker2.rs                  |  10 
crates/project/src/project.rs                  |   4 
crates/project/src/worktree.rs                 |   2 
crates/project2/src/project2.rs                |   4 
crates/project2/src/project_tests.rs           |  67 +
crates/project2/src/worktree.rs                |   3 
crates/project_panel2/src/project_panel.rs     |   7 
crates/rope/src/rope.rs                        |   6 
crates/rope2/src/rope2.rs                      |   6 
crates/settings2/src/keymap_file.rs            |   4 
crates/settings2/src/settings_file.rs          |   3 
crates/storybook2/src/storybook2.rs            |   4 
crates/storybook3/Cargo.toml                   |  17 
crates/storybook3/src/storybook3.rs            |  73 +
crates/terminal_view2/Cargo.toml               |   1 
crates/terminal_view2/src/terminal_element.rs  |  62 
crates/terminal_view2/src/terminal_panel.rs    |   4 
crates/terminal_view2/src/terminal_view.rs     | 129 +-
crates/theme2/src/registry.rs                  |   8 
crates/theme2/src/settings.rs                  |  19 
crates/theme2/src/theme2.rs                    |  19 
crates/ui2/Cargo.toml                          |   1 
crates/ui2/src/components/button.rs            |   1 
crates/ui2/src/components/context_menu.rs      | 368 +++++++-
crates/ui2/src/components/icon_button.rs       |  36 
crates/ui2/src/components/keybinding.rs        |   5 
crates/ui2/src/components/label.rs             |   2 
crates/ui2/src/components/list.rs              |  52 +
crates/ui2/src/story.rs                        |   1 
crates/ui2/src/styled_ext.rs                   |   1 
crates/workspace2/src/dock.rs                  | 127 +-
crates/workspace2/src/item.rs                  |  14 
crates/workspace2/src/modal_layer.rs           |  22 
crates/workspace2/src/pane.rs                  | 377 ++++----
crates/workspace2/src/searchable.rs            |   2 
crates/workspace2/src/workspace2.rs            | 845 +++++++++----------
crates/zed2/Cargo.toml                         |   3 
crates/zed2/src/languages/json.rs              |   2 
crates/zed2/src/main.rs                        |   3 
crates/zed_actions2/src/lib.rs                 |   7 
92 files changed, 2,926 insertions(+), 1,731 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -3797,6 +3797,7 @@ dependencies = [
  "image",
  "itertools 0.10.5",
  "lazy_static",
+ "linkme",
  "log",
  "media",
  "metal",
@@ -4815,6 +4816,26 @@ version = "0.5.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
 
+[[package]]
+name = "linkme"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91ed2ee9464ff9707af8e9ad834cffa4802f072caad90639c583dd3c62e6e608"
+dependencies = [
+ "linkme-impl",
+]
+
+[[package]]
+name = "linkme-impl"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba125974b109d512fccbc6c0244e7580143e460895dfd6ea7f8bbb692fd94396"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.37",
+]
+
 [[package]]
 name = "linux-raw-sys"
 version = "0.0.42"
@@ -8802,6 +8823,17 @@ dependencies = [
  "util",
 ]
 
+[[package]]
+name = "storybook3"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "gpui2",
+ "settings2",
+ "theme2",
+ "ui2",
+]
+
 [[package]]
 name = "stringprep"
 version = "0.1.4"
@@ -10132,6 +10164,7 @@ dependencies = [
  "chrono",
  "gpui2",
  "itertools 0.11.0",
+ "menu2",
  "rand 0.8.5",
  "serde",
  "settings2",

Cargo.toml 🔗

@@ -95,6 +95,7 @@ members = [
     "crates/sqlez_macros",
     "crates/rich_text",
     "crates/storybook2",
+    "crates/storybook3",
     "crates/sum_tree",
     "crates/terminal",
     "crates/terminal2",

assets/settings/default.json 🔗

@@ -35,6 +35,15 @@
   //           "custom": 2
   //         },
   "buffer_line_height": "comfortable",
+  // The name of a font to use for rendering text in the UI
+  "ui_font_family": "Zed Mono",
+  // The OpenType features to enable for text in the UI
+  "ui_font_features": {
+    // Disable ligatures:
+    "calt": false
+  },
+  // The default font size for text in the UI
+  "ui_font_size": 14,
   // The factor to grow the active pane by. Defaults to 1.0
   // which gives the same size as all other panes.
   "active_pane_magnification": 1.0,

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

@@ -224,7 +224,7 @@ impl TestServer {
         });
 
         cx.update(|cx| {
-            theme::init(cx);
+            theme::init(theme::LoadThemes::JustBase, cx);
             Project::init(&client, cx);
             client::init(&client, cx);
             language::init(cx);

crates/collab_ui2/src/collab_panel.rs 🔗

@@ -684,6 +684,7 @@ impl CollabPanel {
             if let Some(serialized_panel) = serialized_panel {
                 panel.update(cx, |panel, cx| {
                     panel.width = serialized_panel.width;
+                    //todo!(collapsed_channels)
                     // panel.collapsed_channels = serialized_panel
                     //     .collapsed_channels
                     //     .unwrap_or_else(|| Vec::new());

crates/collab_ui2/src/collab_titlebar_item.rs 🔗

@@ -31,9 +31,9 @@ use std::sync::Arc;
 use call::ActiveCall;
 use client::{Client, UserStore};
 use gpui::{
-    div, rems, AppContext, Component, Div, InteractiveComponent, Model, ParentComponent, Render,
-    Stateful, StatefulInteractiveComponent, Styled, Subscription, ViewContext, VisualContext,
-    WeakView, WindowBounds,
+    div, px, rems, AppContext, Component, Div, InteractiveComponent, Model, ParentComponent,
+    Render, Stateful, StatefulInteractiveComponent, Styled, Subscription, ViewContext,
+    VisualContext, WeakView, WindowBounds,
 };
 use project::Project;
 use theme::ActiveTheme;
@@ -88,12 +88,17 @@ impl Render for CollabTitlebarItem {
         h_stack()
             .id("titlebar")
             .justify_between()
+            .w_full()
+            .h(rems(1.75))
+            // Set a non-scaling min-height here to ensure the titlebar is
+            // always at least the height of the traffic lights.
+            .min_h(px(32.))
             .when(
                 !matches!(cx.window_bounds(), WindowBounds::Fullscreen),
-                |s| s.pl_20(),
+                // Use pixels here instead of a rem-based size because the macOS traffic
+                // lights are a static size, and don't scale with the rest of the UI.
+                |s| s.pl(px(68.)),
             )
-            .w_full()
-            .h(rems(1.75))
             .bg(cx.theme().colors().title_bar_background)
             .on_click(|_, event, cx| {
                 if event.up.click_count == 2 {
@@ -102,6 +107,7 @@ impl Render for CollabTitlebarItem {
             })
             .child(
                 h_stack()
+                    .gap_1()
                     // TODO - Add player menu
                     .child(
                         div()
@@ -130,14 +136,12 @@ impl Render for CollabTitlebarItem {
                                     .color(Some(TextColor::Muted)),
                             )
                             .tooltip(move |_, cx| {
-                                // todo!() Replace with real action.
-                                #[gpui::action]
-                                struct NoAction {}
                                 cx.build_view(|_| {
                                     Tooltip::new("Recent Branches")
                                         .key_binding(KeyBinding::new(gpui::KeyBinding::new(
                                             "cmd-b",
-                                            NoAction {},
+                                            // todo!() Replace with real action.
+                                            gpui::NoAction,
                                             None,
                                         )))
                                         .meta("Only local branches shown")

crates/command_palette2/src/command_palette.rs 🔗

@@ -1,9 +1,8 @@
 use collections::{CommandPaletteFilter, HashMap};
 use fuzzy::{StringMatch, StringMatchCandidate};
 use gpui::{
-    actions, div, prelude::*, Action, AppContext, Component, Div, EventEmitter, FocusHandle,
-    Keystroke, ParentComponent, Render, Styled, View, ViewContext, VisualContext, WeakView,
-    WindowContext,
+    actions, div, prelude::*, Action, AppContext, Component, Dismiss, Div, FocusHandle, Keystroke,
+    ManagedView, ParentComponent, Render, Styled, View, ViewContext, VisualContext, WeakView,
 };
 use picker::{Picker, PickerDelegate};
 use std::{
@@ -16,7 +15,7 @@ use util::{
     channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL},
     ResultExt,
 };
-use workspace::{Modal, ModalEvent, Workspace};
+use workspace::Workspace;
 use zed_actions::OpenZedURL;
 
 actions!(Toggle);
@@ -47,7 +46,7 @@ impl CommandPalette {
             .available_actions()
             .into_iter()
             .filter_map(|action| {
-                let name = action.name();
+                let name = gpui::remove_the_2(action.name());
                 let namespace = name.split("::").next().unwrap_or("malformed action name");
                 if filter.is_some_and(|f| f.filtered_namespaces.contains(namespace)) {
                     return None;
@@ -69,10 +68,9 @@ impl CommandPalette {
     }
 }
 
-impl EventEmitter<ModalEvent> for CommandPalette {}
-impl Modal for CommandPalette {
-    fn focus(&self, cx: &mut WindowContext) {
-        self.picker.update(cx, |picker, cx| picker.focus(cx));
+impl ManagedView for CommandPalette {
+    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
+        self.picker.focus_handle(cx)
     }
 }
 
@@ -267,7 +265,7 @@ impl PickerDelegate for CommandPaletteDelegate {
 
     fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
         self.command_palette
-            .update(cx, |_, cx| cx.emit(ModalEvent::Dismissed))
+            .update(cx, |_, cx| cx.emit(Dismiss))
             .log_err();
     }
 
@@ -456,7 +454,7 @@ mod tests {
     fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
         cx.update(|cx| {
             let app_state = AppState::test(cx);
-            theme::init(cx);
+            theme::init(theme::LoadThemes::JustBase, cx);
             language::init(cx);
             editor::init(cx);
             workspace::init(app_state.clone(), cx);

crates/copilot/src/copilot.rs 🔗

@@ -1051,17 +1051,15 @@ mod tests {
         );
 
         // Ensure updates to the file are reflected in the LSP.
-        buffer_1
-            .update(cx, |buffer, cx| {
-                buffer.file_updated(
-                    Arc::new(File {
-                        abs_path: "/root/child/buffer-1".into(),
-                        path: Path::new("child/buffer-1").into(),
-                    }),
-                    cx,
-                )
-            })
-            .await;
+        buffer_1.update(cx, |buffer, cx| {
+            buffer.file_updated(
+                Arc::new(File {
+                    abs_path: "/root/child/buffer-1".into(),
+                    path: Path::new("child/buffer-1").into(),
+                }),
+                cx,
+            )
+        });
         assert_eq!(
             lsp.receive_notification::<lsp::notification::DidCloseTextDocument>()
                 .await,

crates/editor2/src/display_map.rs 🔗

@@ -13,7 +13,8 @@ pub use block_map::{BlockMap, BlockPoint};
 use collections::{BTreeMap, HashMap, HashSet};
 use fold_map::FoldMap;
 use gpui::{
-    Font, FontId, HighlightStyle, Hsla, Line, Model, ModelContext, Pixels, TextRun, UnderlineStyle,
+    Font, FontId, HighlightStyle, Hsla, LineLayout, Model, ModelContext, Pixels, ShapedLine,
+    TextRun, UnderlineStyle, WrappedLine,
 };
 use inlay_map::InlayMap;
 use language::{
@@ -561,7 +562,7 @@ impl DisplaySnapshot {
         })
     }
 
-    pub fn lay_out_line_for_row(
+    pub fn layout_row(
         &self,
         display_row: u32,
         TextLayoutDetails {
@@ -569,7 +570,7 @@ impl DisplaySnapshot {
             editor_style,
             rem_size,
         }: &TextLayoutDetails,
-    ) -> Line {
+    ) -> Arc<LineLayout> {
         let mut runs = Vec::new();
         let mut line = String::new();
 
@@ -598,29 +599,27 @@ impl DisplaySnapshot {
 
         let font_size = editor_style.text.font_size.to_pixels(*rem_size);
         text_system
-            .layout_text(&line, font_size, &runs, None)
-            .unwrap()
-            .pop()
-            .unwrap()
+            .layout_line(&line, font_size, &runs)
+            .expect("we expect the font to be loaded because it's rendered by the editor")
     }
 
-    pub fn x_for_point(
+    pub fn x_for_display_point(
         &self,
         display_point: DisplayPoint,
         text_layout_details: &TextLayoutDetails,
     ) -> Pixels {
-        let layout_line = self.lay_out_line_for_row(display_point.row(), text_layout_details);
-        layout_line.x_for_index(display_point.column() as usize)
+        let line = self.layout_row(display_point.row(), text_layout_details);
+        line.x_for_index(display_point.column() as usize)
     }
 
-    pub fn column_for_x(
+    pub fn display_column_for_x(
         &self,
         display_row: u32,
-        x_coordinate: Pixels,
-        text_layout_details: &TextLayoutDetails,
+        x: Pixels,
+        details: &TextLayoutDetails,
     ) -> u32 {
-        let layout_line = self.lay_out_line_for_row(display_row, text_layout_details);
-        layout_line.closest_index_for_x(x_coordinate) as u32
+        let layout_line = self.layout_row(display_row, details);
+        layout_line.closest_index_for_x(x) as u32
     }
 
     pub fn chars_at(

crates/editor2/src/display_map/inlay_map.rs 🔗

@@ -1891,6 +1891,6 @@ mod tests {
     fn init_test(cx: &mut AppContext) {
         let store = SettingsStore::test(cx);
         cx.set_global(store);
-        theme::init(cx);
+        theme::init(theme::LoadThemes::JustBase, cx);
     }
 }

crates/editor2/src/editor.rs 🔗

@@ -39,7 +39,7 @@ use futures::FutureExt;
 use fuzzy::{StringMatch, StringMatchCandidate};
 use git::diff_hunk_to_display;
 use gpui::{
-    action, actions, div, point, prelude::*, px, relative, rems, size, uniform_list, AnyElement,
+    actions, div, point, prelude::*, px, relative, rems, size, uniform_list, Action, AnyElement,
     AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Component, Context,
     EventEmitter, FocusHandle, FocusableView, FontFeatures, FontStyle, FontWeight, HighlightStyle,
     Hsla, InputHandler, KeyContext, Model, MouseButton, ParentComponent, Pixels, Render, Styled,
@@ -180,78 +180,78 @@ pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2);
 //     //     .with_soft_wrap(true)
 // }
 
-#[action]
+#[derive(PartialEq, Clone, Deserialize, Default, Action)]
 pub struct SelectNext {
     #[serde(default)]
     pub replace_newest: bool,
 }
 
-#[action]
+#[derive(PartialEq, Clone, Deserialize, Default, Action)]
 pub struct SelectPrevious {
     #[serde(default)]
     pub replace_newest: bool,
 }
 
-#[action]
+#[derive(PartialEq, Clone, Deserialize, Default, Action)]
 pub struct SelectAllMatches {
     #[serde(default)]
     pub replace_newest: bool,
 }
 
-#[action]
+#[derive(PartialEq, Clone, Deserialize, Default, Action)]
 pub struct SelectToBeginningOfLine {
     #[serde(default)]
     stop_at_soft_wraps: bool,
 }
 
-#[action]
+#[derive(PartialEq, Clone, Deserialize, Default, Action)]
 pub struct MovePageUp {
     #[serde(default)]
     center_cursor: bool,
 }
 
-#[action]
+#[derive(PartialEq, Clone, Deserialize, Default, Action)]
 pub struct MovePageDown {
     #[serde(default)]
     center_cursor: bool,
 }
 
-#[action]
+#[derive(PartialEq, Clone, Deserialize, Default, Action)]
 pub struct SelectToEndOfLine {
     #[serde(default)]
     stop_at_soft_wraps: bool,
 }
 
-#[action]
+#[derive(PartialEq, Clone, Deserialize, Default, Action)]
 pub struct ToggleCodeActions {
     #[serde(default)]
     pub deployed_from_indicator: bool,
 }
 
-#[action]
+#[derive(PartialEq, Clone, Deserialize, Default, Action)]
 pub struct ConfirmCompletion {
     #[serde(default)]
     pub item_ix: Option<usize>,
 }
 
-#[action]
+#[derive(PartialEq, Clone, Deserialize, Default, Action)]
 pub struct ConfirmCodeAction {
     #[serde(default)]
     pub item_ix: Option<usize>,
 }
 
-#[action]
+#[derive(PartialEq, Clone, Deserialize, Default, Action)]
 pub struct ToggleComments {
     #[serde(default)]
     pub advance_downwards: bool,
 }
 
-#[action]
+#[derive(PartialEq, Clone, Deserialize, Default, Action)]
 pub struct FoldAt {
     pub buffer_row: u32,
 }
 
-#[action]
+#[derive(PartialEq, Clone, Deserialize, Default, Action)]
 pub struct UnfoldAt {
     pub buffer_row: u32,
 }
@@ -5433,7 +5433,9 @@ impl Editor {
                     *head.column_mut() += 1;
                     head = display_map.clip_point(head, Bias::Right);
                     let goal = SelectionGoal::HorizontalPosition(
-                        display_map.x_for_point(head, &text_layout_details).into(),
+                        display_map
+                            .x_for_display_point(head, &text_layout_details)
+                            .into(),
                     );
                     selection.collapse_to(head, goal);
 
@@ -6379,8 +6381,8 @@ impl Editor {
             let oldest_selection = selections.iter().min_by_key(|s| s.id).unwrap().clone();
             let range = oldest_selection.display_range(&display_map).sorted();
 
-            let start_x = display_map.x_for_point(range.start, &text_layout_details);
-            let end_x = display_map.x_for_point(range.end, &text_layout_details);
+            let start_x = display_map.x_for_display_point(range.start, &text_layout_details);
+            let end_x = display_map.x_for_display_point(range.end, &text_layout_details);
             let positions = start_x.min(end_x)..start_x.max(end_x);
 
             selections.clear();
@@ -6419,15 +6421,16 @@ impl Editor {
                     let range = selection.display_range(&display_map).sorted();
                     debug_assert_eq!(range.start.row(), range.end.row());
                     let mut row = range.start.row();
-                    let positions = if let SelectionGoal::HorizontalRange { start, end } =
-                        selection.goal
-                    {
-                        px(start)..px(end)
-                    } else {
-                        let start_x = display_map.x_for_point(range.start, &text_layout_details);
-                        let end_x = display_map.x_for_point(range.end, &text_layout_details);
-                        start_x.min(end_x)..start_x.max(end_x)
-                    };
+                    let positions =
+                        if let SelectionGoal::HorizontalRange { start, end } = selection.goal {
+                            px(start)..px(end)
+                        } else {
+                            let start_x =
+                                display_map.x_for_display_point(range.start, &text_layout_details);
+                            let end_x =
+                                display_map.x_for_display_point(range.end, &text_layout_details);
+                            start_x.min(end_x)..start_x.max(end_x)
+                        };
 
                     while row != end_row {
                         if above {
@@ -6980,7 +6983,7 @@ impl Editor {
                         let display_point = point.to_display_point(display_snapshot);
                         let goal = SelectionGoal::HorizontalPosition(
                             display_snapshot
-                                .x_for_point(display_point, &text_layout_details)
+                                .x_for_display_point(display_point, &text_layout_details)
                                 .into(),
                         );
                         (display_point, goal)
@@ -9367,18 +9370,16 @@ impl Render for Editor {
     fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
         let settings = ThemeSettings::get_global(cx);
         let text_style = match self.mode {
-            EditorMode::SingleLine => {
-                TextStyle {
-                    color: cx.theme().colors().text,
-                    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,
-                    line_height: relative(1.3).into(), // TODO relative(settings.buffer_line_height.value()),
-                    underline: None,
-                }
-            }
+            EditorMode::SingleLine => TextStyle {
+                color: cx.theme().colors().text,
+                font_family: settings.ui_font.family.clone(),
+                font_features: settings.ui_font.features,
+                font_size: rems(0.875).into(),
+                font_weight: FontWeight::NORMAL,
+                font_style: FontStyle::Normal,
+                line_height: relative(1.).into(),
+                underline: None,
+            },
 
             EditorMode::AutoHeight { max_lines } => todo!(),
 
@@ -9749,7 +9750,8 @@ impl InputHandler for Editor {
         let scroll_left = scroll_position.x * em_width;
 
         let start = OffsetUtf16(range_utf16.start).to_display_point(&snapshot);
-        let x = snapshot.x_for_point(start, &text_layout_details) - scroll_left + self.gutter_width;
+        let x = snapshot.x_for_display_point(start, &text_layout_details) - scroll_left
+            + self.gutter_width;
         let y = line_height * (start.row() as f32 - scroll_position.y);
 
         Some(Bounds {

crates/editor2/src/editor_tests.rs 🔗

@@ -8277,7 +8277,7 @@ pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsC
     cx.update(|cx| {
         let store = SettingsStore::test(cx);
         cx.set_global(store);
-        theme::init(cx);
+        theme::init(theme::LoadThemes::JustBase, cx);
         client::init_settings(cx);
         language::init(cx);
         Project::init_settings(cx);

crates/editor2/src/element.rs 🔗

@@ -20,10 +20,10 @@ use collections::{BTreeMap, HashMap};
 use gpui::{
     div, point, px, relative, size, transparent_black, Action, AnyElement, AvailableSpace,
     BorrowWindow, Bounds, Component, ContentMask, Corners, DispatchPhase, Edges, Element,
-    ElementId, ElementInputHandler, Entity, EntityId, Hsla, InteractiveComponent, Line,
+    ElementId, ElementInputHandler, Entity, EntityId, Hsla, InteractiveComponent, LineLayout,
     MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentComponent, Pixels,
-    ScrollWheelEvent, Size, StatefulInteractiveComponent, Style, Styled, TextRun, TextStyle, View,
-    ViewContext, WindowContext,
+    ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveComponent, Style, Styled,
+    TextRun, TextStyle, View, ViewContext, WindowContext, WrappedLine,
 };
 use itertools::Itertools;
 use language::language_settings::ShowWhitespaceSetting;
@@ -476,7 +476,7 @@ impl EditorElement {
             Self::paint_diff_hunks(bounds, layout, cx);
         }
 
-        for (ix, line) in layout.line_number_layouts.iter().enumerate() {
+        for (ix, line) in layout.line_numbers.iter().enumerate() {
             if let Some(line) = line {
                 let line_origin = bounds.origin
                     + point(
@@ -775,21 +775,21 @@ impl EditorElement {
                                         .chars_at(cursor_position)
                                         .next()
                                         .and_then(|(character, _)| {
-                                            let text = character.to_string();
+                                            let text = SharedString::from(character.to_string());
+                                            let len = text.len();
                                             cx.text_system()
-                                                .layout_text(
-                                                    &text,
+                                                .shape_line(
+                                                    text,
                                                     cursor_row_layout.font_size,
                                                     &[TextRun {
-                                                        len: text.len(),
+                                                        len,
                                                         font: self.style.text.font(),
                                                         color: self.style.background,
+                                                        background_color: None,
                                                         underline: None,
                                                     }],
-                                                    None,
                                                 )
-                                                .unwrap()
-                                                .pop()
+                                                .log_err()
                                         })
                                 } else {
                                     None
@@ -1244,20 +1244,20 @@ impl EditorElement {
         let font_size = style.text.font_size.to_pixels(cx.rem_size());
         let layout = cx
             .text_system()
-            .layout_text(
-                " ".repeat(column).as_str(),
+            .shape_line(
+                SharedString::from(" ".repeat(column)),
                 font_size,
                 &[TextRun {
                     len: column,
                     font: style.text.font(),
                     color: Hsla::default(),
+                    background_color: None,
                     underline: None,
                 }],
-                None,
             )
             .unwrap();
 
-        layout[0].width
+        layout.width
     }
 
     fn max_line_number_width(&self, snapshot: &EditorSnapshot, cx: &ViewContext<Editor>) -> Pixels {
@@ -1338,7 +1338,7 @@ impl EditorElement {
         relative_rows
     }
 
-    fn layout_line_numbers(
+    fn shape_line_numbers(
         &self,
         rows: Range<u32>,
         active_rows: &BTreeMap<u32, bool>,
@@ -1347,12 +1347,12 @@ impl EditorElement {
         snapshot: &EditorSnapshot,
         cx: &ViewContext<Editor>,
     ) -> (
-        Vec<Option<gpui::Line>>,
+        Vec<Option<ShapedLine>>,
         Vec<Option<(FoldStatus, BufferRow, bool)>>,
     ) {
         let font_size = self.style.text.font_size.to_pixels(cx.rem_size());
         let include_line_numbers = snapshot.mode == EditorMode::Full;
-        let mut line_number_layouts = Vec::with_capacity(rows.len());
+        let mut shaped_line_numbers = Vec::with_capacity(rows.len());
         let mut fold_statuses = Vec::with_capacity(rows.len());
         let mut line_number = String::new();
         let is_relative = EditorSettings::get_global(cx).relative_line_numbers;
@@ -1387,15 +1387,14 @@ impl EditorElement {
                         len: line_number.len(),
                         font: self.style.text.font(),
                         color,
+                        background_color: None,
                         underline: None,
                     };
-                    let layout = cx
+                    let shaped_line = cx
                         .text_system()
-                        .layout_text(&line_number, font_size, &[run], None)
-                        .unwrap()
-                        .pop()
+                        .shape_line(line_number.clone().into(), font_size, &[run])
                         .unwrap();
-                    line_number_layouts.push(Some(layout));
+                    shaped_line_numbers.push(Some(shaped_line));
                     fold_statuses.push(
                         is_singleton
                             .then(|| {
@@ -1408,17 +1407,17 @@ impl EditorElement {
                 }
             } else {
                 fold_statuses.push(None);
-                line_number_layouts.push(None);
+                shaped_line_numbers.push(None);
             }
         }
 
-        (line_number_layouts, fold_statuses)
+        (shaped_line_numbers, fold_statuses)
     }
 
     fn layout_lines(
         &mut self,
         rows: Range<u32>,
-        line_number_layouts: &[Option<Line>],
+        line_number_layouts: &[Option<ShapedLine>],
         snapshot: &EditorSnapshot,
         cx: &ViewContext<Editor>,
     ) -> Vec<LineWithInvisibles> {
@@ -1439,18 +1438,17 @@ impl EditorElement {
                 .chain(iter::repeat(""))
                 .take(rows.len());
             placeholder_lines
-                .map(|line| {
+                .filter_map(move |line| {
                     let run = TextRun {
                         len: line.len(),
                         font: self.style.text.font(),
                         color: placeholder_color,
+                        background_color: None,
                         underline: Default::default(),
                     };
                     cx.text_system()
-                        .layout_text(line, font_size, &[run], None)
-                        .unwrap()
-                        .pop()
-                        .unwrap()
+                        .shape_line(line.to_string().into(), font_size, &[run])
+                        .log_err()
                 })
                 .map(|line| LineWithInvisibles {
                     line,
@@ -1726,7 +1724,7 @@ impl EditorElement {
             .head
         });
 
-        let (line_number_layouts, fold_statuses) = self.layout_line_numbers(
+        let (line_numbers, fold_statuses) = self.shape_line_numbers(
             start_row..end_row,
             &active_rows,
             head_for_relative,
@@ -1740,8 +1738,7 @@ impl EditorElement {
         let scrollbar_row_range = scroll_position.y..(scroll_position.y + height_in_lines);
 
         let mut max_visible_line_width = Pixels::ZERO;
-        let line_layouts =
-            self.layout_lines(start_row..end_row, &line_number_layouts, &snapshot, cx);
+        let line_layouts = self.layout_lines(start_row..end_row, &line_numbers, &snapshot, cx);
         for line_with_invisibles in &line_layouts {
             if line_with_invisibles.line.width > max_visible_line_width {
                 max_visible_line_width = line_with_invisibles.line.width;
@@ -1879,35 +1876,31 @@ impl EditorElement {
         let invisible_symbol_font_size = font_size / 2.;
         let tab_invisible = cx
             .text_system()
-            .layout_text(
-                "→",
+            .shape_line(
+                "→".into(),
                 invisible_symbol_font_size,
                 &[TextRun {
                     len: "→".len(),
                     font: self.style.text.font(),
                     color: cx.theme().colors().editor_invisible,
+                    background_color: None,
                     underline: None,
                 }],
-                None,
             )
-            .unwrap()
-            .pop()
             .unwrap();
         let space_invisible = cx
             .text_system()
-            .layout_text(
-                "•",
+            .shape_line(
+                "•".into(),
                 invisible_symbol_font_size,
                 &[TextRun {
                     len: "•".len(),
                     font: self.style.text.font(),
                     color: cx.theme().colors().editor_invisible,
+                    background_color: None,
                     underline: None,
                 }],
-                None,
             )
-            .unwrap()
-            .pop()
             .unwrap();
 
         LayoutState {
@@ -1939,7 +1932,7 @@ impl EditorElement {
             active_rows,
             highlighted_rows,
             highlighted_ranges,
-            line_number_layouts,
+            line_numbers,
             display_hunks,
             blocks,
             selections,
@@ -2201,7 +2194,7 @@ impl EditorElement {
 
 #[derive(Debug)]
 pub struct LineWithInvisibles {
-    pub line: Line,
+    pub line: ShapedLine,
     invisibles: Vec<Invisible>,
 }
 
@@ -2211,7 +2204,7 @@ impl LineWithInvisibles {
         text_style: &TextStyle,
         max_line_len: usize,
         max_line_count: usize,
-        line_number_layouts: &[Option<Line>],
+        line_number_layouts: &[Option<ShapedLine>],
         editor_mode: EditorMode,
         cx: &WindowContext,
     ) -> Vec<Self> {
@@ -2231,11 +2224,12 @@ impl LineWithInvisibles {
         }]) {
             for (ix, mut line_chunk) in highlighted_chunk.chunk.split('\n').enumerate() {
                 if ix > 0 {
-                    let layout = cx
+                    let shaped_line = cx
                         .text_system()
-                        .layout_text(&line, font_size, &styles, None);
+                        .shape_line(line.clone().into(), font_size, &styles)
+                        .unwrap();
                     layouts.push(Self {
-                        line: layout.unwrap().pop().unwrap(),
+                        line: shaped_line,
                         invisibles: invisibles.drain(..).collect(),
                     });
 
@@ -2269,6 +2263,7 @@ impl LineWithInvisibles {
                         len: line_chunk.len(),
                         font: text_style.font(),
                         color: text_style.color,
+                        background_color: None,
                         underline: text_style.underline,
                     });
 
@@ -3089,7 +3084,7 @@ pub struct LayoutState {
     visible_display_row_range: Range<u32>,
     active_rows: BTreeMap<u32, bool>,
     highlighted_rows: Option<Range<u32>>,
-    line_number_layouts: Vec<Option<gpui::Line>>,
+    line_numbers: Vec<Option<ShapedLine>>,
     display_hunks: Vec<DisplayDiffHunk>,
     blocks: Vec<BlockLayout>,
     highlighted_ranges: Vec<(Range<DisplayPoint>, Hsla)>,
@@ -3102,8 +3097,8 @@ pub struct LayoutState {
     code_actions_indicator: Option<CodeActionsIndicator>,
     // hover_popovers: Option<(DisplayPoint, Vec<AnyElement<Editor>>)>,
     fold_indicators: Vec<Option<AnyElement<Editor>>>,
-    tab_invisible: Line,
-    space_invisible: Line,
+    tab_invisible: ShapedLine,
+    space_invisible: ShapedLine,
 }
 
 struct CodeActionsIndicator {
@@ -3203,7 +3198,7 @@ fn layout_line(
     snapshot: &EditorSnapshot,
     style: &EditorStyle,
     cx: &WindowContext,
-) -> Result<Line> {
+) -> Result<ShapedLine> {
     let mut line = snapshot.line(row);
 
     if line.len() > MAX_LINE_LEN {
@@ -3215,21 +3210,17 @@ fn layout_line(
         line.truncate(len);
     }
 
-    Ok(cx
-        .text_system()
-        .layout_text(
-            &line,
-            style.text.font_size.to_pixels(cx.rem_size()),
-            &[TextRun {
-                len: snapshot.line_len(row) as usize,
-                font: style.text.font(),
-                color: Hsla::default(),
-                underline: None,
-            }],
-            None,
-        )?
-        .pop()
-        .unwrap())
+    cx.text_system().shape_line(
+        line.into(),
+        style.text.font_size.to_pixels(cx.rem_size()),
+        &[TextRun {
+            len: snapshot.line_len(row) as usize,
+            font: style.text.font(),
+            color: Hsla::default(),
+            background_color: None,
+            underline: None,
+        }],
+    )
 }
 
 #[derive(Debug)]
@@ -3239,7 +3230,7 @@ pub struct Cursor {
     line_height: Pixels,
     color: Hsla,
     shape: CursorShape,
-    block_text: Option<Line>,
+    block_text: Option<ShapedLine>,
 }
 
 impl Cursor {
@@ -3249,7 +3240,7 @@ impl Cursor {
         line_height: Pixels,
         color: Hsla,
         shape: CursorShape,
-        block_text: Option<Line>,
+        block_text: Option<ShapedLine>,
     ) -> Cursor {
         Cursor {
             origin,

crates/editor2/src/inlay_hint_cache.rs 🔗

@@ -3179,7 +3179,7 @@ all hints should be invalidated and requeried for all of its visible excerpts"
         cx.update(|cx| {
             let settings_store = SettingsStore::test(cx);
             cx.set_global(settings_store);
-            theme::init(cx);
+            theme::init(theme::LoadThemes::JustBase, cx);
             client::init_settings(cx);
             language::init(cx);
             Project::init_settings(cx);

crates/editor2/src/items.rs 🔗

@@ -797,7 +797,7 @@ impl Item for Editor {
 
     fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
         let workspace_id = workspace.database_id();
-        let item_id = cx.view().entity_id().as_u64() as ItemId;
+        let item_id = cx.view().item_id().as_u64() as ItemId;
         self.workspace = Some((workspace.weak_handle(), workspace.database_id()));
 
         fn serialize(
@@ -828,7 +828,7 @@ impl Item for Editor {
                         serialize(
                             buffer,
                             *workspace_id,
-                            cx.view().entity_id().as_u64() as ItemId,
+                            cx.view().item_id().as_u64() as ItemId,
                             cx,
                         );
                     }

crates/editor2/src/movement.rs 🔗

@@ -98,7 +98,7 @@ pub fn up_by_rows(
         SelectionGoal::HorizontalPosition(x) => x.into(), // todo!("Can the fields in SelectionGoal by Pixels? We should extract a geometry crate and depend on that.")
         SelectionGoal::WrappedHorizontalPosition((_, x)) => x.into(),
         SelectionGoal::HorizontalRange { end, .. } => end.into(),
-        _ => map.x_for_point(start, text_layout_details),
+        _ => map.x_for_display_point(start, text_layout_details),
     };
 
     let prev_row = start.row().saturating_sub(row_count);
@@ -107,7 +107,7 @@ pub fn up_by_rows(
         Bias::Left,
     );
     if point.row() < start.row() {
-        *point.column_mut() = map.column_for_x(point.row(), goal_x, text_layout_details)
+        *point.column_mut() = map.display_column_for_x(point.row(), goal_x, text_layout_details)
     } else if preserve_column_at_start {
         return (start, goal);
     } else {
@@ -137,18 +137,18 @@ pub fn down_by_rows(
         SelectionGoal::HorizontalPosition(x) => x.into(),
         SelectionGoal::WrappedHorizontalPosition((_, x)) => x.into(),
         SelectionGoal::HorizontalRange { end, .. } => end.into(),
-        _ => map.x_for_point(start, text_layout_details),
+        _ => map.x_for_display_point(start, text_layout_details),
     };
 
     let new_row = start.row() + row_count;
     let mut point = map.clip_point(DisplayPoint::new(new_row, 0), Bias::Right);
     if point.row() > start.row() {
-        *point.column_mut() = map.column_for_x(point.row(), goal_x, text_layout_details)
+        *point.column_mut() = map.display_column_for_x(point.row(), goal_x, text_layout_details)
     } else if preserve_column_at_end {
         return (start, goal);
     } else {
         point = map.max_point();
-        goal_x = map.x_for_point(point, text_layout_details)
+        goal_x = map.x_for_display_point(point, text_layout_details)
     }
 
     let mut clipped_point = map.clip_point(point, Bias::Right);

crates/editor2/src/selections_collection.rs 🔗

@@ -313,14 +313,14 @@ impl SelectionsCollection {
         let is_empty = positions.start == positions.end;
         let line_len = display_map.line_len(row);
 
-        let layed_out_line = display_map.lay_out_line_for_row(row, &text_layout_details);
+        let line = display_map.layout_row(row, &text_layout_details);
 
         dbg!("****START COL****");
-        let start_col = layed_out_line.closest_index_for_x(positions.start) as u32;
-        if start_col < line_len || (is_empty && positions.start == layed_out_line.width) {
+        let start_col = line.closest_index_for_x(positions.start) as u32;
+        if start_col < line_len || (is_empty && positions.start == line.width) {
             let start = DisplayPoint::new(row, start_col);
             dbg!("****END COL****");
-            let end_col = layed_out_line.closest_index_for_x(positions.end) as u32;
+            let end_col = line.closest_index_for_x(positions.end) as u32;
             let end = DisplayPoint::new(row, end_col);
             dbg!(start_col, end_col);
 

crates/file_finder2/src/file_finder.rs 🔗

@@ -2,9 +2,9 @@ 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,
+    actions, div, AppContext, Component, Dismiss, Div, FocusHandle, InteractiveComponent,
+    ManagedView, Model, ParentComponent, Render, Styled, Task, View, ViewContext, VisualContext,
+    WeakView,
 };
 use picker::{Picker, PickerDelegate};
 use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
@@ -19,7 +19,7 @@ use text::Point;
 use theme::ActiveTheme;
 use ui::{v_stack, HighlightedLabel, StyledExt};
 use util::{paths::PathLikeWithPosition, post_inc, ResultExt};
-use workspace::{Modal, ModalEvent, Workspace};
+use workspace::Workspace;
 
 actions!(Toggle);
 
@@ -111,10 +111,9 @@ impl FileFinder {
     }
 }
 
-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 ManagedView for FileFinder {
+    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
+        self.picker.focus_handle(cx)
     }
 }
 impl Render for FileFinder {
@@ -689,9 +688,7 @@ impl PickerDelegate for FileFinderDelegate {
                                 .log_err();
                         }
                     }
-                    finder
-                        .update(&mut cx, |_, cx| cx.emit(ModalEvent::Dismissed))
-                        .ok()?;
+                    finder.update(&mut cx, |_, cx| cx.emit(Dismiss)).ok()?;
 
                     Some(())
                 })
@@ -702,7 +699,7 @@ impl PickerDelegate for FileFinderDelegate {
 
     fn dismissed(&mut self, cx: &mut ViewContext<Picker<FileFinderDelegate>>) {
         self.file_finder
-            .update(cx, |_, cx| cx.emit(ModalEvent::Dismissed))
+            .update(cx, |_, cx| cx.emit(Dismiss))
             .log_err();
     }
 
@@ -1763,7 +1760,7 @@ mod tests {
     fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
         cx.update(|cx| {
             let state = AppState::test(cx);
-            theme::init(cx);
+            theme::init(theme::LoadThemes::JustBase, cx);
             language::init(cx);
             super::init(cx);
             editor::init(cx);

crates/go_to_line2/src/go_to_line.rs 🔗

@@ -1,13 +1,13 @@
 use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Editor};
 use gpui::{
-    actions, div, prelude::*, AppContext, Div, EventEmitter, ParentComponent, Render, SharedString,
-    Styled, Subscription, View, ViewContext, VisualContext, WindowContext,
+    actions, div, prelude::*, AppContext, Dismiss, Div, FocusHandle, ManagedView, ParentComponent,
+    Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext, WindowContext,
 };
 use text::{Bias, Point};
 use theme::ActiveTheme;
 use ui::{h_stack, v_stack, Label, StyledExt, TextColor};
 use util::paths::FILE_ROW_COLUMN_DELIMITER;
-use workspace::{Modal, ModalEvent, Workspace};
+use workspace::Workspace;
 
 actions!(Toggle);
 
@@ -23,10 +23,9 @@ pub struct GoToLine {
     _subscriptions: Vec<Subscription>,
 }
 
-impl EventEmitter<ModalEvent> for GoToLine {}
-impl Modal for GoToLine {
-    fn focus(&self, cx: &mut WindowContext) {
-        self.line_editor.update(cx, |editor, cx| editor.focus(cx))
+impl ManagedView for GoToLine {
+    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
+        self.line_editor.focus_handle(cx)
     }
 }
 
@@ -88,7 +87,7 @@ impl GoToLine {
     ) {
         match event {
             // todo!() this isn't working...
-            editor::Event::Blurred => cx.emit(ModalEvent::Dismissed),
+            editor::Event::Blurred => cx.emit(Dismiss),
             editor::Event::BufferEdited { .. } => self.highlight_current_line(cx),
             _ => {}
         }
@@ -123,7 +122,7 @@ impl GoToLine {
     }
 
     fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
-        cx.emit(ModalEvent::Dismissed);
+        cx.emit(Dismiss);
     }
 
     fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
@@ -140,7 +139,7 @@ impl GoToLine {
             self.prev_scroll_position.take();
         }
 
-        cx.emit(ModalEvent::Dismissed);
+        cx.emit(Dismiss);
     }
 }
 

crates/gpui2/Cargo.toml 🔗

@@ -22,6 +22,7 @@ sqlez = { path = "../sqlez" }
 async-task = "4.0.3"
 backtrace = { version = "0.3", optional = true }
 ctor.workspace = true
+linkme = "0.3"
 derive_more.workspace = true
 dhat = { version = "0.3", optional = true }
 env_logger = { version = "0.9", optional = true }

crates/gpui2/src/action.rs 🔗

@@ -1,10 +1,12 @@
 use crate::SharedString;
 use anyhow::{anyhow, Context, Result};
 use collections::HashMap;
-use lazy_static::lazy_static;
-use parking_lot::{MappedRwLockReadGuard, RwLock, RwLockReadGuard};
-use serde::Deserialize;
-use std::any::{type_name, Any, TypeId};
+pub use no_action::NoAction;
+use serde_json::json;
+use std::{
+    any::{Any, TypeId},
+    ops::Deref,
+};
 
 /// Actions are used to implement keyboard-driven UI.
 /// When you declare an action, you can bind keys to the action in the keymap and
@@ -15,24 +17,16 @@ use std::any::{type_name, Any, TypeId};
 /// ```rust
 /// actions!(MoveUp, MoveDown, MoveLeft, MoveRight, Newline);
 /// ```
-/// More complex data types can also be actions. If you annotate your type with the `#[action]` proc macro,
-/// it will automatically
+/// More complex data types can also be actions. If you annotate your type with the action derive macro
+/// it will be implemented and registered automatically.
 /// ```
-/// #[action]
+/// #[derive(Clone, PartialEq, serde_derive::Deserialize, Action)]
 /// pub struct SelectNext {
 ///     pub replace_newest: bool,
 /// }
 ///
-/// Any type A that satisfies the following bounds is automatically an action:
-///
-/// ```
-/// A: for<'a> Deserialize<'a> + PartialEq + Clone + Default + std::fmt::Debug + 'static,
-/// ```
-///
-/// The `#[action]` annotation will derive these implementations for your struct automatically. If you
-/// want to control them manually, you can use the lower-level `#[register_action]` macro, which only
-/// generates the code needed to register your action before `main`. Then you'll need to implement all
-/// the traits manually.
+/// If you want to control the behavior of the action trait manually, you can use the lower-level `#[register_action]`
+/// macro, which only generates the code needed to register your action before `main`.
 ///
 /// ```
 /// #[gpui::register_action]
@@ -41,77 +35,29 @@ use std::any::{type_name, Any, TypeId};
 ///     pub content: SharedString,
 /// }
 ///
-/// impl std::default::Default for Paste {
-///     fn default() -> Self {
-///         Self {
-///             content: SharedString::from("🍝"),
-///         }
-///     }
+/// impl gpui::Action for Paste {
+///      ///...
 /// }
 /// ```
-pub trait Action: std::fmt::Debug + 'static {
-    fn qualified_name() -> SharedString
-    where
-        Self: Sized;
-    fn build(value: Option<serde_json::Value>) -> Result<Box<dyn Action>>
+pub trait Action: 'static {
+    fn boxed_clone(&self) -> Box<dyn Action>;
+    fn as_any(&self) -> &dyn Any;
+    fn partial_eq(&self, action: &dyn Action) -> bool;
+    fn name(&self) -> &str;
+
+    fn debug_name() -> &'static str
     where
         Self: Sized;
-    fn is_registered() -> bool
+    fn build(value: serde_json::Value) -> Result<Box<dyn Action>>
     where
         Self: Sized;
-
-    fn partial_eq(&self, action: &dyn Action) -> bool;
-    fn boxed_clone(&self) -> Box<dyn Action>;
-    fn as_any(&self) -> &dyn Any;
 }
 
-// Types become actions by satisfying a list of trait bounds.
-impl<A> Action for A
-where
-    A: for<'a> Deserialize<'a> + PartialEq + Default + Clone + std::fmt::Debug + 'static,
-{
-    fn qualified_name() -> SharedString {
-        let name = type_name::<A>();
-        let mut separator_matches = name.rmatch_indices("::");
-        separator_matches.next().unwrap();
-        let name_start_ix = separator_matches.next().map_or(0, |(ix, _)| ix + 2);
-        // todo!() remove the 2 replacement when migration is done
-        name[name_start_ix..].replace("2::", "::").into()
-    }
-
-    fn build(params: Option<serde_json::Value>) -> Result<Box<dyn Action>>
-    where
-        Self: Sized,
-    {
-        let action = if let Some(params) = params {
-            serde_json::from_value(params).context("failed to deserialize action")?
-        } else {
-            Self::default()
-        };
-        Ok(Box::new(action))
-    }
-
-    fn is_registered() -> bool {
-        ACTION_REGISTRY
-            .read()
-            .names_by_type_id
-            .get(&TypeId::of::<A>())
-            .is_some()
-    }
-
-    fn partial_eq(&self, action: &dyn Action) -> bool {
-        action
-            .as_any()
-            .downcast_ref::<Self>()
-            .map_or(false, |a| self == a)
-    }
-
-    fn boxed_clone(&self) -> Box<dyn Action> {
-        Box::new(self.clone())
-    }
-
-    fn as_any(&self) -> &dyn Any {
-        self
+impl std::fmt::Debug for dyn Action {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("dyn Action")
+            .field("type_name", &self.name())
+            .finish()
     }
 }
 
@@ -119,69 +65,93 @@ impl dyn Action {
     pub fn type_id(&self) -> TypeId {
         self.as_any().type_id()
     }
-
-    pub fn name(&self) -> SharedString {
-        ACTION_REGISTRY
-            .read()
-            .names_by_type_id
-            .get(&self.type_id())
-            .expect("type is not a registered action")
-            .clone()
-    }
 }
 
-type ActionBuilder = fn(json: Option<serde_json::Value>) -> anyhow::Result<Box<dyn Action>>;
-
-lazy_static! {
-    static ref ACTION_REGISTRY: RwLock<ActionRegistry> = RwLock::default();
-}
+type ActionBuilder = fn(json: serde_json::Value) -> anyhow::Result<Box<dyn Action>>;
 
-#[derive(Default)]
-struct ActionRegistry {
+pub(crate) struct ActionRegistry {
     builders_by_name: HashMap<SharedString, ActionBuilder>,
     names_by_type_id: HashMap<TypeId, SharedString>,
     all_names: Vec<SharedString>, // So we can return a static slice.
 }
 
-/// Register an action type to allow it to be referenced in keymaps.
-pub fn register_action<A: Action>() {
-    let name = A::qualified_name();
-    let mut lock = ACTION_REGISTRY.write();
-    lock.builders_by_name.insert(name.clone(), A::build);
-    lock.names_by_type_id
-        .insert(TypeId::of::<A>(), name.clone());
-    lock.all_names.push(name);
+impl Default for ActionRegistry {
+    fn default() -> Self {
+        let mut this = ActionRegistry {
+            builders_by_name: Default::default(),
+            names_by_type_id: Default::default(),
+            all_names: Default::default(),
+        };
+
+        this.load_actions();
+
+        this
+    }
 }
 
-/// Construct an action based on its name and optional JSON parameters sourced from the keymap.
-pub fn build_action_from_type(type_id: &TypeId) -> Result<Box<dyn Action>> {
-    let lock = ACTION_REGISTRY.read();
-    let name = lock
-        .names_by_type_id
-        .get(type_id)
-        .ok_or_else(|| anyhow!("no action type registered for {:?}", type_id))?
-        .clone();
-    drop(lock);
-
-    build_action(&name, None)
+/// This type must be public so that our macros can build it in other crates.
+/// But this is an implementation detail and should not be used directly.
+#[doc(hidden)]
+pub type MacroActionBuilder = fn() -> ActionData;
+
+/// This type must be public so that our macros can build it in other crates.
+/// But this is an implementation detail and should not be used directly.
+#[doc(hidden)]
+pub struct ActionData {
+    pub name: &'static str,
+    pub type_id: TypeId,
+    pub build: ActionBuilder,
 }
 
-/// Construct an action based on its name and optional JSON parameters sourced from the keymap.
-pub fn build_action(name: &str, params: Option<serde_json::Value>) -> Result<Box<dyn Action>> {
-    let lock = ACTION_REGISTRY.read();
+/// This constant must be public to be accessible from other crates.
+/// But it's existence is an implementation detail and should not be used directly.
+#[doc(hidden)]
+#[linkme::distributed_slice]
+pub static __GPUI_ACTIONS: [MacroActionBuilder];
+
+impl ActionRegistry {
+    /// Load all registered actions into the registry.
+    pub(crate) fn load_actions(&mut self) {
+        for builder in __GPUI_ACTIONS {
+            let action = builder();
+            //todo(remove)
+            let name: SharedString = remove_the_2(action.name).into();
+            self.builders_by_name.insert(name.clone(), action.build);
+            self.names_by_type_id.insert(action.type_id, name.clone());
+            self.all_names.push(name);
+        }
+    }
+
+    /// Construct an action based on its name and optional JSON parameters sourced from the keymap.
+    pub fn build_action_type(&self, type_id: &TypeId) -> Result<Box<dyn Action>> {
+        let name = self
+            .names_by_type_id
+            .get(type_id)
+            .ok_or_else(|| anyhow!("no action type registered for {:?}", type_id))?
+            .clone();
 
-    let build_action = lock
-        .builders_by_name
-        .get(name)
-        .ok_or_else(|| anyhow!("no action type registered for {}", name))?;
-    (build_action)(params)
-}
+        self.build_action(&name, None)
+    }
+
+    /// Construct an action based on its name and optional JSON parameters sourced from the keymap.
+    pub fn build_action(
+        &self,
+        name: &str,
+        params: Option<serde_json::Value>,
+    ) -> Result<Box<dyn Action>> {
+        //todo(remove)
+        let name = remove_the_2(name);
+        let build_action = self
+            .builders_by_name
+            .get(name.deref())
+            .ok_or_else(|| anyhow!("no action type registered for {}", name))?;
+        (build_action)(params.unwrap_or_else(|| json!({})))
+            .with_context(|| format!("Attempting to build action {}", name))
+    }
 
-pub fn all_action_names() -> MappedRwLockReadGuard<'static, [SharedString]> {
-    let lock = ACTION_REGISTRY.read();
-    RwLockReadGuard::map(lock, |registry: &ActionRegistry| {
-        registry.all_names.as_slice()
-    })
+    pub fn all_action_names(&self) -> &[SharedString] {
+        self.all_names.as_slice()
+    }
 }
 
 /// Defines unit structs that can be used as actions.
@@ -191,7 +161,7 @@ macro_rules! actions {
     () => {};
 
     ( $name:ident ) => {
-        #[gpui::action]
+        #[derive(::std::cmp::PartialEq, ::std::clone::Clone, ::std::default::Default, gpui::serde_derive::Deserialize, gpui::Action)]
         pub struct $name;
     };
 
@@ -200,3 +170,20 @@ macro_rules! actions {
         actions!($($rest)*);
     };
 }
+
+//todo!(remove)
+pub fn remove_the_2(action_name: &str) -> String {
+    let mut separator_matches = action_name.rmatch_indices("::");
+    separator_matches.next().unwrap();
+    let name_start_ix = separator_matches.next().map_or(0, |(ix, _)| ix + 2);
+    // todo!() remove the 2 replacement when migration is done
+    action_name[name_start_ix..]
+        .replace("2::", "::")
+        .to_string()
+}
+
+mod no_action {
+    use crate as gpui;
+
+    actions!(NoAction);
+}

crates/gpui2/src/app.rs 🔗

@@ -14,12 +14,13 @@ use smallvec::SmallVec;
 pub use test_context::*;
 
 use crate::{
-    current_platform, image_cache::ImageCache, Action, AnyBox, AnyView, AnyWindowHandle,
-    AppMetadata, AssetSource, BackgroundExecutor, ClipboardItem, Context, DispatchPhase, DisplayId,
-    Entity, EventEmitter, FocusEvent, FocusHandle, FocusId, ForegroundExecutor, KeyBinding, Keymap,
-    LayoutId, PathPromptOptions, Pixels, Platform, PlatformDisplay, Point, Render, SubscriberSet,
-    Subscription, SvgRenderer, Task, TextStyle, TextStyleRefinement, TextSystem, View, ViewContext,
-    Window, WindowContext, WindowHandle, WindowId,
+    current_platform, image_cache::ImageCache, Action, ActionRegistry, AnyBox, AnyView,
+    AnyWindowHandle, AppMetadata, AssetSource, BackgroundExecutor, ClipboardItem, Context,
+    DispatchPhase, DisplayId, Entity, EventEmitter, FocusEvent, FocusHandle, FocusId,
+    ForegroundExecutor, KeyBinding, Keymap, LayoutId, PathPromptOptions, Pixels, Platform,
+    PlatformDisplay, Point, Render, SharedString, SubscriberSet, Subscription, SvgRenderer, Task,
+    TextStyle, TextStyleRefinement, TextSystem, View, ViewContext, Window, WindowContext,
+    WindowHandle, WindowId,
 };
 use anyhow::{anyhow, Result};
 use collections::{HashMap, HashSet, VecDeque};
@@ -182,6 +183,7 @@ pub struct AppContext {
     text_system: Arc<TextSystem>,
     flushing_effects: bool,
     pending_updates: usize,
+    pub(crate) actions: Rc<ActionRegistry>,
     pub(crate) active_drag: Option<AnyDrag>,
     pub(crate) active_tooltip: Option<AnyTooltip>,
     pub(crate) next_frame_callbacks: HashMap<DisplayId, Vec<FrameCallback>>,
@@ -240,6 +242,7 @@ impl AppContext {
                 platform: platform.clone(),
                 app_metadata,
                 text_system,
+                actions: Rc::new(ActionRegistry::default()),
                 flushing_effects: false,
                 pending_updates: 0,
                 active_drag: None,
@@ -964,6 +967,18 @@ impl AppContext {
     pub fn propagate(&mut self) {
         self.propagate_event = true;
     }
+
+    pub fn build_action(
+        &self,
+        name: &str,
+        data: Option<serde_json::Value>,
+    ) -> Result<Box<dyn Action>> {
+        self.actions.build_action(name, data)
+    }
+
+    pub fn all_action_names(&self) -> &[SharedString] {
+        self.actions.all_action_names()
+    }
 }
 
 impl Context for AppContext {

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

@@ -182,6 +182,10 @@ pub struct AsyncWindowContext {
 }
 
 impl AsyncWindowContext {
+    pub fn window_handle(&self) -> AnyWindowHandle {
+        self.window
+    }
+
     pub(crate) fn new(app: AsyncAppContext, window: AnyWindowHandle) -> Self {
         Self { app, window }
     }

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

@@ -370,10 +370,19 @@ impl<T: Send> Model<T> {
             })
         });
 
-        cx.executor().run_until_parked();
-        rx.try_next()
-            .expect("no event received")
-            .expect("model was dropped")
+        // Run other tasks until the event is emitted.
+        loop {
+            match rx.try_next() {
+                Ok(Some(event)) => return event,
+                Ok(None) => panic!("model was dropped"),
+                Err(_) => {
+                    if !cx.executor().tick() {
+                        break;
+                    }
+                }
+            }
+        }
+        panic!("no event received")
     }
 }
 

crates/gpui2/src/element.rs 🔗

@@ -13,7 +13,7 @@ pub trait Element<V: 'static> {
     fn layout(
         &mut self,
         view_state: &mut V,
-        previous_element_state: Option<Self::ElementState>,
+        element_state: Option<Self::ElementState>,
         cx: &mut ViewContext<V>,
     ) -> (LayoutId, Self::ElementState);
 

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

@@ -237,11 +237,11 @@ pub trait InteractiveComponent<V: 'static>: Sized + Element<V> {
         //
         // if we are relying on this side-effect still, removing the debug_assert!
         // likely breaks the command_palette tests.
-        debug_assert!(
-            A::is_registered(),
-            "{:?} is not registered as an action",
-            A::qualified_name()
-        );
+        // debug_assert!(
+        //     A::is_registered(),
+        //     "{:?} is not registered as an action",
+        //     A::qualified_name()
+        // );
         self.interactivity().action_listeners.push((
             TypeId::of::<A>(),
             Box::new(move |view, action, phase, cx| {
@@ -960,11 +960,11 @@ where
                             cx.background_executor().timer(TOOLTIP_DELAY).await;
                             view.update(&mut cx, move |view_state, cx| {
                                 active_tooltip.borrow_mut().replace(ActiveTooltip {
-                                    waiting: None,
                                     tooltip: Some(AnyTooltip {
                                         view: tooltip_builder(view_state, cx),
                                         cursor_offset: cx.mouse_position(),
                                     }),
+                                    _task: None,
                                 });
                                 cx.notify();
                             })
@@ -972,12 +972,17 @@ where
                         }
                     });
                     active_tooltip.borrow_mut().replace(ActiveTooltip {
-                        waiting: Some(task),
                         tooltip: None,
+                        _task: Some(task),
                     });
                 }
             });
 
+            let active_tooltip = element_state.active_tooltip.clone();
+            cx.on_mouse_event(move |_, _: &MouseDownEvent, _, _| {
+                active_tooltip.borrow_mut().take();
+            });
+
             if let Some(active_tooltip) = element_state.active_tooltip.borrow().as_ref() {
                 if active_tooltip.tooltip.is_some() {
                     cx.active_tooltip = active_tooltip.tooltip.clone()
@@ -1207,9 +1212,8 @@ pub struct InteractiveElementState {
 }
 
 pub struct ActiveTooltip {
-    #[allow(unused)] // used to drop the task
-    waiting: Option<Task<()>>,
     tooltip: Option<AnyTooltip>,
+    _task: Option<Task<()>>,
 }
 
 /// Whether or not the element or a group that contains it is clicked by the mouse.

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

@@ -1,8 +1,9 @@
 use smallvec::SmallVec;
+use taffy::style::{Display, Position};
 
 use crate::{
-    point, AnyElement, BorrowWindow, Bounds, Element, LayoutId, ParentComponent, Pixels, Point,
-    Size, Style,
+    point, AnyElement, BorrowWindow, Bounds, Component, Element, LayoutId, ParentComponent, Pixels,
+    Point, Size, Style,
 };
 
 pub struct OverlayState {
@@ -14,7 +15,7 @@ pub struct Overlay<V> {
     anchor_corner: AnchorCorner,
     fit_mode: OverlayFitMode,
     // todo!();
-    // anchor_position: Option<Vector2F>,
+    anchor_position: Option<Point<Pixels>>,
     // position_mode: OverlayPositionMode,
 }
 
@@ -25,6 +26,7 @@ pub fn overlay<V: 'static>() -> Overlay<V> {
         children: SmallVec::new(),
         anchor_corner: AnchorCorner::TopLeft,
         fit_mode: OverlayFitMode::SwitchAnchor,
+        anchor_position: None,
     }
 }
 
@@ -35,6 +37,13 @@ impl<V> Overlay<V> {
         self
     }
 
+    /// Sets the position in window co-ordinates
+    /// (otherwise the location the overlay is rendered is used)
+    pub fn position(mut self, anchor: Point<Pixels>) -> Self {
+        self.anchor_position = Some(anchor);
+        self
+    }
+
     /// Snap to window edge instead of switching anchor corner when an overflow would occur.
     pub fn snap_to_window(mut self) -> Self {
         self.fit_mode = OverlayFitMode::SnapToWindow;
@@ -48,6 +57,12 @@ impl<V: 'static> ParentComponent<V> for Overlay<V> {
     }
 }
 
+impl<V: 'static> Component<V> for Overlay<V> {
+    fn render(self) -> AnyElement<V> {
+        AnyElement::new(self)
+    }
+}
+
 impl<V: 'static> Element<V> for Overlay<V> {
     type ElementState = OverlayState;
 
@@ -66,7 +81,12 @@ impl<V: 'static> Element<V> for Overlay<V> {
             .iter_mut()
             .map(|child| child.layout(view_state, cx))
             .collect::<SmallVec<_>>();
-        let layout_id = cx.request_layout(&Style::default(), child_layout_ids.iter().copied());
+
+        let mut overlay_style = Style::default();
+        overlay_style.position = Position::Absolute;
+        overlay_style.display = Display::Flex;
+
+        let layout_id = cx.request_layout(&overlay_style, child_layout_ids.iter().copied());
 
         (layout_id, OverlayState { child_layout_ids })
     }
@@ -90,7 +110,7 @@ impl<V: 'static> Element<V> for Overlay<V> {
             child_max = child_max.max(&child_bounds.lower_right());
         }
         let size: Size<Pixels> = (child_max - child_min).into();
-        let origin = bounds.origin;
+        let origin = self.anchor_position.unwrap_or(bounds.origin);
 
         let mut desired = self.anchor_corner.get_bounds(origin, size);
         let limits = Bounds {
@@ -184,6 +204,15 @@ impl AnchorCorner {
         Bounds { origin, size }
     }
 
+    pub fn corner(&self, bounds: Bounds<Pixels>) -> Point<Pixels> {
+        match self {
+            Self::TopLeft => bounds.origin,
+            Self::TopRight => bounds.upper_right(),
+            Self::BottomLeft => bounds.lower_left(),
+            Self::BottomRight => bounds.lower_right(),
+        }
+    }
+
     fn switch_axis(self, axis: Axis) -> Self {
         match axis {
             Axis::Vertical => match self {

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

@@ -1,76 +1,39 @@
 use crate::{
-    AnyElement, BorrowWindow, Bounds, Component, Element, LayoutId, Line, Pixels, SharedString,
-    Size, TextRun, ViewContext,
+    AnyElement, BorrowWindow, Bounds, Component, Element, ElementId, LayoutId, Pixels,
+    SharedString, Size, TextRun, ViewContext, WrappedLine,
 };
-use parking_lot::Mutex;
+use parking_lot::{Mutex, MutexGuard};
 use smallvec::SmallVec;
-use std::{marker::PhantomData, sync::Arc};
+use std::{cell::Cell, rc::Rc, sync::Arc};
 use util::ResultExt;
 
-impl<V: 'static> Component<V> for SharedString {
-    fn render(self) -> AnyElement<V> {
-        Text {
-            text: self,
-            runs: None,
-            state_type: PhantomData,
-        }
-        .render()
-    }
-}
-
-impl<V: 'static> Component<V> for &'static str {
-    fn render(self) -> AnyElement<V> {
-        Text {
-            text: self.into(),
-            runs: None,
-            state_type: PhantomData,
-        }
-        .render()
-    }
-}
-
-// TODO: Figure out how to pass `String` to `child` without this.
-// This impl doesn't exist in the `gpui2` crate.
-impl<V: 'static> Component<V> for String {
-    fn render(self) -> AnyElement<V> {
-        Text {
-            text: self.into(),
-            runs: None,
-            state_type: PhantomData,
-        }
-        .render()
-    }
-}
-
-pub struct Text<V> {
+pub struct Text {
     text: SharedString,
     runs: Option<Vec<TextRun>>,
-    state_type: PhantomData<V>,
 }
 
-impl<V: 'static> Text<V> {
-    /// styled renders text that has different runs of different styles.
-    /// callers are responsible for setting the correct style for each run.
-    ////
-    /// For uniform text you can usually just pass a string as a child, and
-    /// cx.text_style() will be used automatically.
+impl Text {
+    /// Renders text with runs of different styles.
+    ///
+    /// Callers are responsible for setting the correct style for each run.
+    /// For text with a uniform style, you can usually avoid calling this constructor
+    /// and just pass text directly.
     pub fn styled(text: SharedString, runs: Vec<TextRun>) -> Self {
         Text {
             text,
             runs: Some(runs),
-            state_type: Default::default(),
         }
     }
 }
 
-impl<V: 'static> Component<V> for Text<V> {
+impl<V: 'static> Component<V> for Text {
     fn render(self) -> AnyElement<V> {
         AnyElement::new(self)
     }
 }
 
-impl<V: 'static> Element<V> for Text<V> {
-    type ElementState = Arc<Mutex<Option<TextElementState>>>;
+impl<V: 'static> Element<V> for Text {
+    type ElementState = TextState;
 
     fn element_id(&self) -> Option<crate::ElementId> {
         None
@@ -103,7 +66,7 @@ impl<V: 'static> Element<V> for Text<V> {
             let element_state = element_state.clone();
             move |known_dimensions, _| {
                 let Some(lines) = text_system
-                    .layout_text(
+                    .shape_text(
                         &text,
                         font_size,
                         &runs[..],
@@ -111,30 +74,23 @@ impl<V: 'static> Element<V> for Text<V> {
                     )
                     .log_err()
                 else {
-                    element_state.lock().replace(TextElementState {
+                    element_state.lock().replace(TextStateInner {
                         lines: Default::default(),
                         line_height,
                     });
                     return Size::default();
                 };
 
-                let line_count = lines
-                    .iter()
-                    .map(|line| line.wrap_count() + 1)
-                    .sum::<usize>();
-                let size = Size {
-                    width: lines
-                        .iter()
-                        .map(|line| line.layout.width)
-                        .max()
-                        .unwrap()
-                        .ceil(),
-                    height: line_height * line_count,
-                };
+                let mut size: Size<Pixels> = Size::default();
+                for line in &lines {
+                    let line_size = line.size(line_height);
+                    size.height += line_size.height;
+                    size.width = size.width.max(line_size.width);
+                }
 
                 element_state
                     .lock()
-                    .replace(TextElementState { lines, line_height });
+                    .replace(TextStateInner { lines, line_height });
 
                 size
             }
@@ -165,7 +121,104 @@ impl<V: 'static> Element<V> for Text<V> {
     }
 }
 
-pub struct TextElementState {
-    lines: SmallVec<[Line; 1]>,
+#[derive(Default, Clone)]
+pub struct TextState(Arc<Mutex<Option<TextStateInner>>>);
+
+impl TextState {
+    fn lock(&self) -> MutexGuard<Option<TextStateInner>> {
+        self.0.lock()
+    }
+}
+
+struct TextStateInner {
+    lines: SmallVec<[WrappedLine; 1]>,
     line_height: Pixels,
 }
+
+struct InteractiveText {
+    id: ElementId,
+    text: Text,
+}
+
+struct InteractiveTextState {
+    text_state: TextState,
+    clicked_range_ixs: Rc<Cell<SmallVec<[usize; 1]>>>,
+}
+
+impl<V: 'static> Element<V> for InteractiveText {
+    type ElementState = InteractiveTextState;
+
+    fn element_id(&self) -> Option<ElementId> {
+        Some(self.id.clone())
+    }
+
+    fn layout(
+        &mut self,
+        view_state: &mut V,
+        element_state: Option<Self::ElementState>,
+        cx: &mut ViewContext<V>,
+    ) -> (LayoutId, Self::ElementState) {
+        if let Some(InteractiveTextState {
+            text_state,
+            clicked_range_ixs,
+        }) = element_state
+        {
+            let (layout_id, text_state) = self.text.layout(view_state, Some(text_state), cx);
+            let element_state = InteractiveTextState {
+                text_state,
+                clicked_range_ixs,
+            };
+            (layout_id, element_state)
+        } else {
+            let (layout_id, text_state) = self.text.layout(view_state, None, cx);
+            let element_state = InteractiveTextState {
+                text_state,
+                clicked_range_ixs: Rc::default(),
+            };
+            (layout_id, element_state)
+        }
+    }
+
+    fn paint(
+        &mut self,
+        bounds: Bounds<Pixels>,
+        view_state: &mut V,
+        element_state: &mut Self::ElementState,
+        cx: &mut ViewContext<V>,
+    ) {
+        self.text
+            .paint(bounds, view_state, &mut element_state.text_state, cx)
+    }
+}
+
+impl<V: 'static> Component<V> for SharedString {
+    fn render(self) -> AnyElement<V> {
+        Text {
+            text: self,
+            runs: None,
+        }
+        .render()
+    }
+}
+
+impl<V: 'static> Component<V> for &'static str {
+    fn render(self) -> AnyElement<V> {
+        Text {
+            text: self.into(),
+            runs: None,
+        }
+        .render()
+    }
+}
+
+// TODO: Figure out how to pass `String` to `child` without this.
+// This impl doesn't exist in the `gpui2` crate.
+impl<V: 'static> Component<V> for String {
+    fn render(self) -> AnyElement<V> {
+        Text {
+            text: self.into(),
+            runs: None,
+        }
+        .render()
+    }
+}

crates/gpui2/src/executor.rs 🔗

@@ -5,10 +5,11 @@ use std::{
     fmt::Debug,
     marker::PhantomData,
     mem,
+    num::NonZeroUsize,
     pin::Pin,
     rc::Rc,
     sync::{
-        atomic::{AtomicBool, Ordering::SeqCst},
+        atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst},
         Arc,
     },
     task::{Context, Poll},
@@ -71,30 +72,57 @@ impl<T> Future for Task<T> {
         }
     }
 }
+
+#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
+pub struct TaskLabel(NonZeroUsize);
+
+impl TaskLabel {
+    pub fn new() -> Self {
+        static NEXT_TASK_LABEL: AtomicUsize = AtomicUsize::new(1);
+        Self(NEXT_TASK_LABEL.fetch_add(1, SeqCst).try_into().unwrap())
+    }
+}
+
 type AnyLocalFuture<R> = Pin<Box<dyn 'static + Future<Output = R>>>;
+
 type AnyFuture<R> = Pin<Box<dyn 'static + Send + Future<Output = R>>>;
+
 impl BackgroundExecutor {
     pub fn new(dispatcher: Arc<dyn PlatformDispatcher>) -> Self {
         Self { dispatcher }
     }
 
-    /// Enqueues the given closure to be run on any thread. The closure returns
-    /// a future which will be run to completion on any available thread.
+    /// Enqueues the given future to be run to completion on a background thread.
     pub fn spawn<R>(&self, future: impl Future<Output = R> + Send + 'static) -> Task<R>
     where
         R: Send + 'static,
     {
+        self.spawn_internal::<R>(Box::pin(future), None)
+    }
+
+    /// Enqueues the given future to be run to completion on a background thread.
+    /// The given label can be used to control the priority of the task in tests.
+    pub fn spawn_labeled<R>(
+        &self,
+        label: TaskLabel,
+        future: impl Future<Output = R> + Send + 'static,
+    ) -> Task<R>
+    where
+        R: Send + 'static,
+    {
+        self.spawn_internal::<R>(Box::pin(future), Some(label))
+    }
+
+    fn spawn_internal<R: Send + 'static>(
+        &self,
+        future: AnyFuture<R>,
+        label: Option<TaskLabel>,
+    ) -> Task<R> {
         let dispatcher = self.dispatcher.clone();
-        fn inner<R: Send + 'static>(
-            dispatcher: Arc<dyn PlatformDispatcher>,
-            future: AnyFuture<R>,
-        ) -> Task<R> {
-            let (runnable, task) =
-                async_task::spawn(future, move |runnable| dispatcher.dispatch(runnable));
-            runnable.schedule();
-            Task::Spawned(task)
-        }
-        inner::<R>(dispatcher, Box::pin(future))
+        let (runnable, task) =
+            async_task::spawn(future, move |runnable| dispatcher.dispatch(runnable, label));
+        runnable.schedule();
+        Task::Spawned(task)
     }
 
     #[cfg(any(test, feature = "test-support"))]
@@ -130,7 +158,7 @@ impl BackgroundExecutor {
             match future.as_mut().poll(&mut cx) {
                 Poll::Ready(result) => return result,
                 Poll::Pending => {
-                    if !self.dispatcher.poll(background_only) {
+                    if !self.dispatcher.tick(background_only) {
                         if awoken.swap(false, SeqCst) {
                             continue;
                         }
@@ -216,11 +244,21 @@ impl BackgroundExecutor {
         self.dispatcher.as_test().unwrap().simulate_random_delay()
     }
 
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn deprioritize(&self, task_label: TaskLabel) {
+        self.dispatcher.as_test().unwrap().deprioritize(task_label)
+    }
+
     #[cfg(any(test, feature = "test-support"))]
     pub fn advance_clock(&self, duration: Duration) {
         self.dispatcher.as_test().unwrap().advance_clock(duration)
     }
 
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn tick(&self) -> bool {
+        self.dispatcher.as_test().unwrap().tick(false)
+    }
+
     #[cfg(any(test, feature = "test-support"))]
     pub fn run_until_parked(&self) {
         self.dispatcher.as_test().unwrap().run_until_parked()

crates/gpui2/src/geometry.rs 🔗

@@ -343,7 +343,7 @@ where
 
 impl<T> Bounds<T>
 where
-    T: Clone + Debug + PartialOrd + Add<T, Output = T> + Sub<Output = T> + Default,
+    T: Clone + Debug + PartialOrd + Add<T, Output = T> + Sub<Output = T> + Default + Half,
 {
     pub fn intersects(&self, other: &Bounds<T>) -> bool {
         let my_lower_right = self.lower_right();
@@ -362,6 +362,13 @@ where
         self.size.width = self.size.width.clone() + double_amount.clone();
         self.size.height = self.size.height.clone() + double_amount;
     }
+
+    pub fn center(&self) -> Point<T> {
+        Point {
+            x: self.origin.x.clone() + self.size.width.clone().half(),
+            y: self.origin.y.clone() + self.size.height.clone().half(),
+        }
+    }
 }
 
 impl<T: Clone + Default + Debug + PartialOrd + Add<T, Output = T> + Sub<Output = T>> Bounds<T> {
@@ -1211,6 +1218,46 @@ impl From<()> for Length {
     }
 }
 
+pub trait Half {
+    fn half(&self) -> Self;
+}
+
+impl Half for f32 {
+    fn half(&self) -> Self {
+        self / 2.
+    }
+}
+
+impl Half for DevicePixels {
+    fn half(&self) -> Self {
+        Self(self.0 / 2)
+    }
+}
+
+impl Half for ScaledPixels {
+    fn half(&self) -> Self {
+        Self(self.0 / 2.)
+    }
+}
+
+impl Half for Pixels {
+    fn half(&self) -> Self {
+        Self(self.0 / 2.)
+    }
+}
+
+impl Half for Rems {
+    fn half(&self) -> Self {
+        Self(self.0 / 2.)
+    }
+}
+
+impl Half for GlobalPixels {
+    fn half(&self) -> Self {
+        Self(self.0 / 2.)
+    }
+}
+
 pub trait IsZero {
     fn is_zero(&self) -> bool;
 }

crates/gpui2/src/gpui2.rs 🔗

@@ -49,11 +49,13 @@ pub use input::*;
 pub use interactive::*;
 pub use key_dispatch::*;
 pub use keymap::*;
+pub use linkme;
 pub use platform::*;
 use private::Sealed;
 pub use refineable::*;
 pub use scene::*;
 pub use serde;
+pub use serde_derive;
 pub use serde_json;
 pub use smallvec;
 pub use smol::Timer;

crates/gpui2/src/key_dispatch.rs 🔗

@@ -1,6 +1,6 @@
 use crate::{
-    build_action_from_type, Action, DispatchPhase, FocusId, KeyBinding, KeyContext, KeyMatch,
-    Keymap, Keystroke, KeystrokeMatcher, WindowContext,
+    Action, ActionRegistry, DispatchPhase, FocusId, KeyBinding, KeyContext, KeyMatch, Keymap,
+    Keystroke, KeystrokeMatcher, WindowContext,
 };
 use collections::HashMap;
 use parking_lot::Mutex;
@@ -10,7 +10,6 @@ use std::{
     rc::Rc,
     sync::Arc,
 };
-use util::ResultExt;
 
 #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
 pub struct DispatchNodeId(usize);
@@ -22,6 +21,7 @@ pub(crate) struct DispatchTree {
     focusable_node_ids: HashMap<FocusId, DispatchNodeId>,
     keystroke_matchers: HashMap<SmallVec<[KeyContext; 4]>, KeystrokeMatcher>,
     keymap: Arc<Mutex<Keymap>>,
+    action_registry: Rc<ActionRegistry>,
 }
 
 #[derive(Default)]
@@ -41,7 +41,7 @@ pub(crate) struct DispatchActionListener {
 }
 
 impl DispatchTree {
-    pub fn new(keymap: Arc<Mutex<Keymap>>) -> Self {
+    pub fn new(keymap: Arc<Mutex<Keymap>>, action_registry: Rc<ActionRegistry>) -> Self {
         Self {
             node_stack: Vec::new(),
             context_stack: Vec::new(),
@@ -49,6 +49,7 @@ impl DispatchTree {
             focusable_node_ids: HashMap::default(),
             keystroke_matchers: HashMap::default(),
             keymap,
+            action_registry,
         }
     }
 
@@ -153,7 +154,9 @@ impl DispatchTree {
             for node_id in self.dispatch_path(*node) {
                 let node = &self.nodes[node_id.0];
                 for DispatchActionListener { action_type, .. } in &node.action_listeners {
-                    actions.extend(build_action_from_type(action_type).log_err());
+                    // Intentionally silence these errors without logging.
+                    // If an action cannot be built by default, it's not available.
+                    actions.extend(self.action_registry.build_action_type(action_type).ok());
                 }
             }
         }

crates/gpui2/src/keymap/keymap.rs 🔗

@@ -1,7 +1,10 @@
-use crate::{KeyBinding, KeyBindingContextPredicate, Keystroke};
+use crate::{KeyBinding, KeyBindingContextPredicate, Keystroke, NoAction};
 use collections::HashSet;
 use smallvec::SmallVec;
-use std::{any::TypeId, collections::HashMap};
+use std::{
+    any::{Any, TypeId},
+    collections::HashMap,
+};
 
 #[derive(Copy, Clone, Eq, PartialEq, Default)]
 pub struct KeymapVersion(usize);
@@ -37,20 +40,19 @@ impl Keymap {
     }
 
     pub fn add_bindings<T: IntoIterator<Item = KeyBinding>>(&mut self, bindings: T) {
-        // todo!("no action")
-        // let no_action_id = (NoAction {}).id();
+        let no_action_id = &(NoAction {}).type_id();
         let mut new_bindings = Vec::new();
-        let has_new_disabled_keystrokes = false;
+        let mut has_new_disabled_keystrokes = false;
         for binding in bindings {
-            // if binding.action().id() == no_action_id {
-            //     has_new_disabled_keystrokes |= self
-            //         .disabled_keystrokes
-            //         .entry(binding.keystrokes)
-            //         .or_default()
-            //         .insert(binding.context_predicate);
-            // } else {
-            new_bindings.push(binding);
-            // }
+            if binding.action.type_id() == *no_action_id {
+                has_new_disabled_keystrokes |= self
+                    .disabled_keystrokes
+                    .entry(binding.keystrokes)
+                    .or_default()
+                    .insert(binding.context_predicate);
+            } else {
+                new_bindings.push(binding);
+            }
         }
 
         if has_new_disabled_keystrokes {

crates/gpui2/src/platform.rs 🔗

@@ -8,7 +8,7 @@ use crate::{
     point, size, AnyWindowHandle, BackgroundExecutor, Bounds, DevicePixels, Font, FontId,
     FontMetrics, FontRun, ForegroundExecutor, GlobalPixels, GlyphId, InputEvent, LineLayout,
     Pixels, Point, RenderGlyphParams, RenderImageParams, RenderSvgParams, Result, Scene,
-    SharedString, Size,
+    SharedString, Size, TaskLabel,
 };
 use anyhow::{anyhow, bail};
 use async_task::Runnable;
@@ -162,10 +162,10 @@ pub(crate) trait PlatformWindow {
 
 pub trait PlatformDispatcher: Send + Sync {
     fn is_main_thread(&self) -> bool;
-    fn dispatch(&self, runnable: Runnable);
+    fn dispatch(&self, runnable: Runnable, label: Option<TaskLabel>);
     fn dispatch_on_main_thread(&self, runnable: Runnable);
     fn dispatch_after(&self, duration: Duration, runnable: Runnable);
-    fn poll(&self, background_only: bool) -> bool;
+    fn tick(&self, background_only: bool) -> bool;
     fn park(&self);
     fn unparker(&self) -> Unparker;
 

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

@@ -2,7 +2,7 @@
 #![allow(non_camel_case_types)]
 #![allow(non_snake_case)]
 
-use crate::PlatformDispatcher;
+use crate::{PlatformDispatcher, TaskLabel};
 use async_task::Runnable;
 use objc::{
     class, msg_send,
@@ -37,7 +37,7 @@ impl PlatformDispatcher for MacDispatcher {
         is_main_thread == YES
     }
 
-    fn dispatch(&self, runnable: Runnable) {
+    fn dispatch(&self, runnable: Runnable, _: Option<TaskLabel>) {
         unsafe {
             dispatch_async_f(
                 dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT.try_into().unwrap(), 0),
@@ -71,7 +71,7 @@ impl PlatformDispatcher for MacDispatcher {
         }
     }
 
-    fn poll(&self, _background_only: bool) -> bool {
+    fn tick(&self, _background_only: bool) -> bool {
         false
     }
 

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

@@ -343,10 +343,10 @@ impl MacTextSystemState {
         // Construct the attributed string, converting UTF8 ranges to UTF16 ranges.
         let mut string = CFMutableAttributedString::new();
         {
-            string.replace_str(&CFString::new(text), CFRange::init(0, 0));
+            string.replace_str(&CFString::new(text.as_ref()), CFRange::init(0, 0));
             let utf16_line_len = string.char_len() as usize;
 
-            let mut ix_converter = StringIndexConverter::new(text);
+            let mut ix_converter = StringIndexConverter::new(text.as_ref());
             for run in font_runs {
                 let utf8_end = ix_converter.utf8_ix + run.len;
                 let utf16_start = ix_converter.utf16_ix;
@@ -390,7 +390,7 @@ impl MacTextSystemState {
             };
             let font_id = self.id_for_native_font(font);
 
-            let mut ix_converter = StringIndexConverter::new(text);
+            let mut ix_converter = StringIndexConverter::new(text.as_ref());
             let mut glyphs = SmallVec::new();
             for ((glyph_id, position), glyph_utf16_ix) in run
                 .glyphs()
@@ -413,11 +413,11 @@ impl MacTextSystemState {
 
         let typographic_bounds = line.get_typographic_bounds();
         LineLayout {
+            runs,
+            font_size,
             width: typographic_bounds.width.into(),
             ascent: typographic_bounds.ascent.into(),
             descent: typographic_bounds.descent.into(),
-            runs,
-            font_size,
             len: text.len(),
         }
     }

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

@@ -1141,7 +1141,7 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
     let event = unsafe { InputEvent::from_native(native_event, Some(window_height)) };
 
     if let Some(mut event) = event {
-        let synthesized_second_event = match &mut event {
+        match &mut event {
             InputEvent::MouseDown(
                 event @ MouseDownEvent {
                     button: MouseButton::Left,
@@ -1149,6 +1149,7 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
                     ..
                 },
             ) => {
+                // On mac, a ctrl-left click should be handled as a right click.
                 *event = MouseDownEvent {
                     button: MouseButton::Right,
                     modifiers: Modifiers {
@@ -1158,26 +1159,30 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
                     click_count: 1,
                     ..*event
                 };
-
-                Some(InputEvent::MouseDown(MouseDownEvent {
-                    button: MouseButton::Right,
-                    ..*event
-                }))
             }
 
             // Because we map a ctrl-left_down to a right_down -> right_up let's ignore
             // the ctrl-left_up to avoid having a mismatch in button down/up events if the
             // user is still holding ctrl when releasing the left mouse button
-            InputEvent::MouseUp(MouseUpEvent {
-                button: MouseButton::Left,
-                modifiers: Modifiers { control: true, .. },
-                ..
-            }) => {
-                lock.synthetic_drag_counter += 1;
-                return;
+            InputEvent::MouseUp(
+                event @ MouseUpEvent {
+                    button: MouseButton::Left,
+                    modifiers: Modifiers { control: true, .. },
+                    ..
+                },
+            ) => {
+                *event = MouseUpEvent {
+                    button: MouseButton::Right,
+                    modifiers: Modifiers {
+                        control: false,
+                        ..event.modifiers
+                    },
+                    click_count: 1,
+                    ..*event
+                };
             }
 
-            _ => None,
+            _ => {}
         };
 
         match &event {
@@ -1227,9 +1232,6 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
         if let Some(mut callback) = lock.event_callback.take() {
             drop(lock);
             callback(event);
-            if let Some(event) = synthesized_second_event {
-                callback(event);
-            }
             window_state.lock().event_callback = Some(callback);
         }
     }

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

@@ -1,7 +1,7 @@
-use crate::PlatformDispatcher;
+use crate::{PlatformDispatcher, TaskLabel};
 use async_task::Runnable;
 use backtrace::Backtrace;
-use collections::{HashMap, VecDeque};
+use collections::{HashMap, HashSet, VecDeque};
 use parking::{Parker, Unparker};
 use parking_lot::Mutex;
 use rand::prelude::*;
@@ -28,12 +28,14 @@ struct TestDispatcherState {
     random: StdRng,
     foreground: HashMap<TestDispatcherId, VecDeque<Runnable>>,
     background: Vec<Runnable>,
+    deprioritized_background: Vec<Runnable>,
     delayed: Vec<(Duration, Runnable)>,
     time: Duration,
     is_main_thread: bool,
     next_id: TestDispatcherId,
     allow_parking: bool,
     waiting_backtrace: Option<Backtrace>,
+    deprioritized_task_labels: HashSet<TaskLabel>,
 }
 
 impl TestDispatcher {
@@ -43,12 +45,14 @@ impl TestDispatcher {
             random,
             foreground: HashMap::default(),
             background: Vec::new(),
+            deprioritized_background: Vec::new(),
             delayed: Vec::new(),
             time: Duration::ZERO,
             is_main_thread: true,
             next_id: TestDispatcherId(1),
             allow_parking: false,
             waiting_backtrace: None,
+            deprioritized_task_labels: Default::default(),
         };
 
         TestDispatcher {
@@ -101,8 +105,15 @@ impl TestDispatcher {
         }
     }
 
+    pub fn deprioritize(&self, task_label: TaskLabel) {
+        self.state
+            .lock()
+            .deprioritized_task_labels
+            .insert(task_label);
+    }
+
     pub fn run_until_parked(&self) {
-        while self.poll(false) {}
+        while self.tick(false) {}
     }
 
     pub fn parking_allowed(&self) -> bool {
@@ -150,8 +161,17 @@ impl PlatformDispatcher for TestDispatcher {
         self.state.lock().is_main_thread
     }
 
-    fn dispatch(&self, runnable: Runnable) {
-        self.state.lock().background.push(runnable);
+    fn dispatch(&self, runnable: Runnable, label: Option<TaskLabel>) {
+        {
+            let mut state = self.state.lock();
+            if label.map_or(false, |label| {
+                state.deprioritized_task_labels.contains(&label)
+            }) {
+                state.deprioritized_background.push(runnable);
+            } else {
+                state.background.push(runnable);
+            }
+        }
         self.unparker.unpark();
     }
 
@@ -174,7 +194,7 @@ impl PlatformDispatcher for TestDispatcher {
         state.delayed.insert(ix, (next_time, runnable));
     }
 
-    fn poll(&self, background_only: bool) -> bool {
+    fn tick(&self, background_only: bool) -> bool {
         let mut state = self.state.lock();
 
         while let Some((deadline, _)) = state.delayed.first() {
@@ -196,34 +216,41 @@ impl PlatformDispatcher for TestDispatcher {
         };
         let background_len = state.background.len();
 
+        let runnable;
+        let main_thread;
         if foreground_len == 0 && background_len == 0 {
-            return false;
-        }
-
-        let main_thread = state.random.gen_ratio(
-            foreground_len as u32,
-            (foreground_len + background_len) as u32,
-        );
-        let was_main_thread = state.is_main_thread;
-        state.is_main_thread = main_thread;
-
-        let runnable = if main_thread {
-            let state = &mut *state;
-            let runnables = state
-                .foreground
-                .values_mut()
-                .filter(|runnables| !runnables.is_empty())
-                .choose(&mut state.random)
-                .unwrap();
-            runnables.pop_front().unwrap()
+            let deprioritized_background_len = state.deprioritized_background.len();
+            if deprioritized_background_len == 0 {
+                return false;
+            }
+            let ix = state.random.gen_range(0..deprioritized_background_len);
+            main_thread = false;
+            runnable = state.deprioritized_background.swap_remove(ix);
         } else {
-            let ix = state.random.gen_range(0..background_len);
-            state.background.swap_remove(ix)
+            main_thread = state.random.gen_ratio(
+                foreground_len as u32,
+                (foreground_len + background_len) as u32,
+            );
+            if main_thread {
+                let state = &mut *state;
+                runnable = state
+                    .foreground
+                    .values_mut()
+                    .filter(|runnables| !runnables.is_empty())
+                    .choose(&mut state.random)
+                    .unwrap()
+                    .pop_front()
+                    .unwrap();
+            } else {
+                let ix = state.random.gen_range(0..background_len);
+                runnable = state.background.swap_remove(ix);
+            };
         };
 
+        let was_main_thread = state.is_main_thread;
+        state.is_main_thread = main_thread;
         drop(state);
         runnable.run();
-
         self.state.lock().is_main_thread = was_main_thread;
 
         true

crates/gpui2/src/style.rs 🔗

@@ -203,6 +203,7 @@ impl TextStyle {
                 style: self.font_style,
             },
             color: self.color,
+            background_color: None,
             underline: self.underline.clone(),
         }
     }

crates/gpui2/src/text_system.rs 🔗

@@ -3,20 +3,20 @@ mod line;
 mod line_layout;
 mod line_wrapper;
 
-use anyhow::anyhow;
 pub use font_features::*;
 pub use line::*;
 pub use line_layout::*;
 pub use line_wrapper::*;
-use smallvec::SmallVec;
 
 use crate::{
     px, Bounds, DevicePixels, Hsla, Pixels, PlatformTextSystem, Point, Result, SharedString, Size,
     UnderlineStyle,
 };
+use anyhow::anyhow;
 use collections::HashMap;
 use core::fmt;
 use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard};
+use smallvec::SmallVec;
 use std::{
     cmp,
     fmt::{Debug, Display, Formatter},
@@ -151,13 +151,79 @@ impl TextSystem {
         }
     }
 
-    pub fn layout_text(
+    pub fn layout_line(
         &self,
         text: &str,
         font_size: Pixels,
         runs: &[TextRun],
+    ) -> Result<Arc<LineLayout>> {
+        let mut font_runs = self.font_runs_pool.lock().pop().unwrap_or_default();
+        for run in runs.iter() {
+            let font_id = self.font_id(&run.font)?;
+            if let Some(last_run) = font_runs.last_mut() {
+                if last_run.font_id == font_id {
+                    last_run.len += run.len;
+                    continue;
+                }
+            }
+            font_runs.push(FontRun {
+                len: run.len,
+                font_id,
+            });
+        }
+
+        let layout = self
+            .line_layout_cache
+            .layout_line(&text, font_size, &font_runs);
+
+        font_runs.clear();
+        self.font_runs_pool.lock().push(font_runs);
+
+        Ok(layout)
+    }
+
+    pub fn shape_line(
+        &self,
+        text: SharedString,
+        font_size: Pixels,
+        runs: &[TextRun],
+    ) -> Result<ShapedLine> {
+        debug_assert!(
+            text.find('\n').is_none(),
+            "text argument should not contain newlines"
+        );
+
+        let mut decoration_runs = SmallVec::<[DecorationRun; 32]>::new();
+        for run in runs {
+            if let Some(last_run) = decoration_runs.last_mut() {
+                if last_run.color == run.color && last_run.underline == run.underline {
+                    last_run.len += run.len as u32;
+                    continue;
+                }
+            }
+            decoration_runs.push(DecorationRun {
+                len: run.len as u32,
+                color: run.color,
+                underline: run.underline.clone(),
+            });
+        }
+
+        let layout = self.layout_line(text.as_ref(), font_size, runs)?;
+
+        Ok(ShapedLine {
+            layout,
+            text,
+            decoration_runs,
+        })
+    }
+
+    pub fn shape_text(
+        &self,
+        text: &str, // todo!("pass a SharedString and preserve it when passed a single line?")
+        font_size: Pixels,
+        runs: &[TextRun],
         wrap_width: Option<Pixels>,
-    ) -> Result<SmallVec<[Line; 1]>> {
+    ) -> Result<SmallVec<[WrappedLine; 1]>> {
         let mut runs = runs.iter().cloned().peekable();
         let mut font_runs = self.font_runs_pool.lock().pop().unwrap_or_default();
 
@@ -210,10 +276,11 @@ impl TextSystem {
 
             let layout = self
                 .line_layout_cache
-                .layout_line(&line_text, font_size, &font_runs, wrap_width);
-            lines.push(Line {
+                .layout_wrapped_line(&line_text, font_size, &font_runs, wrap_width);
+            lines.push(WrappedLine {
                 layout,
-                decorations: decoration_runs,
+                decoration_runs,
+                text: SharedString::from(line_text),
             });
 
             line_start = line_end + 1; // Skip `\n` character.
@@ -384,6 +451,7 @@ pub struct TextRun {
     pub len: usize,
     pub font: Font,
     pub color: Hsla,
+    pub background_color: Option<Hsla>,
     pub underline: Option<UnderlineStyle>,
 }
 

crates/gpui2/src/text_system/line.rs 🔗

@@ -1,5 +1,5 @@
 use crate::{
-    black, point, px, size, BorrowWindow, Bounds, Hsla, Pixels, Point, Result, Size,
+    black, point, px, BorrowWindow, Bounds, Hsla, LineLayout, Pixels, Point, Result, SharedString,
     UnderlineStyle, WindowContext, WrapBoundary, WrappedLineLayout,
 };
 use derive_more::{Deref, DerefMut};
@@ -14,23 +14,51 @@ pub struct DecorationRun {
 }
 
 #[derive(Clone, Default, Debug, Deref, DerefMut)]
-pub struct Line {
+pub struct ShapedLine {
     #[deref]
     #[deref_mut]
-    pub(crate) layout: Arc<WrappedLineLayout>,
-    pub(crate) decorations: SmallVec<[DecorationRun; 32]>,
+    pub(crate) layout: Arc<LineLayout>,
+    pub text: SharedString,
+    pub(crate) decoration_runs: SmallVec<[DecorationRun; 32]>,
 }
 
-impl Line {
-    pub fn size(&self, line_height: Pixels) -> Size<Pixels> {
-        size(
-            self.layout.width,
-            line_height * (self.layout.wrap_boundaries.len() + 1),
-        )
+impl ShapedLine {
+    pub fn len(&self) -> usize {
+        self.layout.len
     }
 
-    pub fn wrap_count(&self) -> usize {
-        self.layout.wrap_boundaries.len()
+    pub fn paint(
+        &self,
+        origin: Point<Pixels>,
+        line_height: Pixels,
+        cx: &mut WindowContext,
+    ) -> Result<()> {
+        paint_line(
+            origin,
+            &self.layout,
+            line_height,
+            &self.decoration_runs,
+            None,
+            &[],
+            cx,
+        )?;
+
+        Ok(())
+    }
+}
+
+#[derive(Clone, Default, Debug, Deref, DerefMut)]
+pub struct WrappedLine {
+    #[deref]
+    #[deref_mut]
+    pub(crate) layout: Arc<WrappedLineLayout>,
+    pub text: SharedString,
+    pub(crate) decoration_runs: SmallVec<[DecorationRun; 32]>,
+}
+
+impl WrappedLine {
+    pub fn len(&self) -> usize {
+        self.layout.len()
     }
 
     pub fn paint(
@@ -39,75 +67,50 @@ impl Line {
         line_height: Pixels,
         cx: &mut WindowContext,
     ) -> Result<()> {
-        let padding_top =
-            (line_height - self.layout.layout.ascent - self.layout.layout.descent) / 2.;
-        let baseline_offset = point(px(0.), padding_top + self.layout.layout.ascent);
-
-        let mut style_runs = self.decorations.iter();
-        let mut wraps = self.layout.wrap_boundaries.iter().peekable();
-        let mut run_end = 0;
-        let mut color = black();
-        let mut current_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
-        let text_system = cx.text_system().clone();
-
-        let mut glyph_origin = origin;
-        let mut prev_glyph_position = Point::default();
-        for (run_ix, run) in self.layout.layout.runs.iter().enumerate() {
-            let max_glyph_size = text_system
-                .bounding_box(run.font_id, self.layout.layout.font_size)?
-                .size;
-
-            for (glyph_ix, glyph) in run.glyphs.iter().enumerate() {
-                glyph_origin.x += glyph.position.x - prev_glyph_position.x;
-
-                if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) {
-                    wraps.next();
-                    if let Some((underline_origin, underline_style)) = current_underline.take() {
-                        cx.paint_underline(
-                            underline_origin,
-                            glyph_origin.x - underline_origin.x,
-                            &underline_style,
-                        )?;
-                    }
-
-                    glyph_origin.x = origin.x;
-                    glyph_origin.y += line_height;
-                }
-                prev_glyph_position = glyph.position;
-
-                let mut finished_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
-                if glyph.index >= run_end {
-                    if let Some(style_run) = style_runs.next() {
-                        if let Some((_, underline_style)) = &mut current_underline {
-                            if style_run.underline.as_ref() != Some(underline_style) {
-                                finished_underline = current_underline.take();
-                            }
-                        }
-                        if let Some(run_underline) = style_run.underline.as_ref() {
-                            current_underline.get_or_insert((
-                                point(
-                                    glyph_origin.x,
-                                    origin.y
-                                        + baseline_offset.y
-                                        + (self.layout.layout.descent * 0.618),
-                                ),
-                                UnderlineStyle {
-                                    color: Some(run_underline.color.unwrap_or(style_run.color)),
-                                    thickness: run_underline.thickness,
-                                    wavy: run_underline.wavy,
-                                },
-                            ));
-                        }
+        paint_line(
+            origin,
+            &self.layout.unwrapped_layout,
+            line_height,
+            &self.decoration_runs,
+            self.wrap_width,
+            &self.wrap_boundaries,
+            cx,
+        )?;
 
-                        run_end += style_run.len as usize;
-                        color = style_run.color;
-                    } else {
-                        run_end = self.layout.text.len();
-                        finished_underline = current_underline.take();
-                    }
-                }
+        Ok(())
+    }
+}
 
-                if let Some((underline_origin, underline_style)) = finished_underline {
+fn paint_line(
+    origin: Point<Pixels>,
+    layout: &LineLayout,
+    line_height: Pixels,
+    decoration_runs: &[DecorationRun],
+    wrap_width: Option<Pixels>,
+    wrap_boundaries: &[WrapBoundary],
+    cx: &mut WindowContext<'_>,
+) -> Result<()> {
+    let padding_top = (line_height - layout.ascent - layout.descent) / 2.;
+    let baseline_offset = point(px(0.), padding_top + layout.ascent);
+    let mut decoration_runs = decoration_runs.iter();
+    let mut wraps = wrap_boundaries.iter().peekable();
+    let mut run_end = 0;
+    let mut color = black();
+    let mut current_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
+    let text_system = cx.text_system().clone();
+    let mut glyph_origin = origin;
+    let mut prev_glyph_position = Point::default();
+    for (run_ix, run) in layout.runs.iter().enumerate() {
+        let max_glyph_size = text_system
+            .bounding_box(run.font_id, layout.font_size)?
+            .size;
+
+        for (glyph_ix, glyph) in run.glyphs.iter().enumerate() {
+            glyph_origin.x += glyph.position.x - prev_glyph_position.x;
+
+            if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) {
+                wraps.next();
+                if let Some((underline_origin, underline_style)) = current_underline.take() {
                     cx.paint_underline(
                         underline_origin,
                         glyph_origin.x - underline_origin.x,
@@ -115,42 +118,84 @@ impl Line {
                     )?;
                 }
 
-                let max_glyph_bounds = Bounds {
-                    origin: glyph_origin,
-                    size: max_glyph_size,
-                };
-
-                let content_mask = cx.content_mask();
-                if max_glyph_bounds.intersects(&content_mask.bounds) {
-                    if glyph.is_emoji {
-                        cx.paint_emoji(
-                            glyph_origin + baseline_offset,
-                            run.font_id,
-                            glyph.id,
-                            self.layout.layout.font_size,
-                        )?;
-                    } else {
-                        cx.paint_glyph(
-                            glyph_origin + baseline_offset,
-                            run.font_id,
-                            glyph.id,
-                            self.layout.layout.font_size,
-                            color,
-                        )?;
+                glyph_origin.x = origin.x;
+                glyph_origin.y += line_height;
+            }
+            prev_glyph_position = glyph.position;
+
+            let mut finished_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
+            if glyph.index >= run_end {
+                if let Some(style_run) = decoration_runs.next() {
+                    if let Some((_, underline_style)) = &mut current_underline {
+                        if style_run.underline.as_ref() != Some(underline_style) {
+                            finished_underline = current_underline.take();
+                        }
                     }
+                    if let Some(run_underline) = style_run.underline.as_ref() {
+                        current_underline.get_or_insert((
+                            point(
+                                glyph_origin.x,
+                                origin.y + baseline_offset.y + (layout.descent * 0.618),
+                            ),
+                            UnderlineStyle {
+                                color: Some(run_underline.color.unwrap_or(style_run.color)),
+                                thickness: run_underline.thickness,
+                                wavy: run_underline.wavy,
+                            },
+                        ));
+                    }
+
+                    run_end += style_run.len as usize;
+                    color = style_run.color;
+                } else {
+                    run_end = layout.len;
+                    finished_underline = current_underline.take();
                 }
             }
-        }
 
-        if let Some((underline_start, underline_style)) = current_underline.take() {
-            let line_end_x = origin.x + self.layout.layout.width;
-            cx.paint_underline(
-                underline_start,
-                line_end_x - underline_start.x,
-                &underline_style,
-            )?;
+            if let Some((underline_origin, underline_style)) = finished_underline {
+                cx.paint_underline(
+                    underline_origin,
+                    glyph_origin.x - underline_origin.x,
+                    &underline_style,
+                )?;
+            }
+
+            let max_glyph_bounds = Bounds {
+                origin: glyph_origin,
+                size: max_glyph_size,
+            };
+
+            let content_mask = cx.content_mask();
+            if max_glyph_bounds.intersects(&content_mask.bounds) {
+                if glyph.is_emoji {
+                    cx.paint_emoji(
+                        glyph_origin + baseline_offset,
+                        run.font_id,
+                        glyph.id,
+                        layout.font_size,
+                    )?;
+                } else {
+                    cx.paint_glyph(
+                        glyph_origin + baseline_offset,
+                        run.font_id,
+                        glyph.id,
+                        layout.font_size,
+                        color,
+                    )?;
+                }
+            }
         }
+    }
 
-        Ok(())
+    if let Some((underline_start, underline_style)) = current_underline.take() {
+        let line_end_x = origin.x + wrap_width.unwrap_or(Pixels::MAX).min(layout.width);
+        cx.paint_underline(
+            underline_start,
+            line_end_x - underline_start.x,
+            &underline_style,
+        )?;
     }
+
+    Ok(())
 }

crates/gpui2/src/text_system/line_layout.rs 🔗

@@ -1,5 +1,4 @@
-use crate::{px, FontId, GlyphId, Pixels, PlatformTextSystem, Point, SharedString};
-use derive_more::{Deref, DerefMut};
+use crate::{px, FontId, GlyphId, Pixels, PlatformTextSystem, Point, Size};
 use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard};
 use smallvec::SmallVec;
 use std::{
@@ -149,13 +148,11 @@ impl LineLayout {
     }
 }
 
-#[derive(Deref, DerefMut, Default, Debug)]
+#[derive(Default, Debug)]
 pub struct WrappedLineLayout {
-    #[deref]
-    #[deref_mut]
-    pub layout: LineLayout,
-    pub text: SharedString,
+    pub unwrapped_layout: Arc<LineLayout>,
     pub wrap_boundaries: SmallVec<[WrapBoundary; 1]>,
+    pub wrap_width: Option<Pixels>,
 }
 
 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
@@ -164,31 +161,74 @@ pub struct WrapBoundary {
     pub glyph_ix: usize,
 }
 
+impl WrappedLineLayout {
+    pub fn len(&self) -> usize {
+        self.unwrapped_layout.len
+    }
+
+    pub fn width(&self) -> Pixels {
+        self.wrap_width
+            .unwrap_or(Pixels::MAX)
+            .min(self.unwrapped_layout.width)
+    }
+
+    pub fn size(&self, line_height: Pixels) -> Size<Pixels> {
+        Size {
+            width: self.width(),
+            height: line_height * (self.wrap_boundaries.len() + 1),
+        }
+    }
+
+    pub fn ascent(&self) -> Pixels {
+        self.unwrapped_layout.ascent
+    }
+
+    pub fn descent(&self) -> Pixels {
+        self.unwrapped_layout.descent
+    }
+
+    pub fn wrap_boundaries(&self) -> &[WrapBoundary] {
+        &self.wrap_boundaries
+    }
+
+    pub fn font_size(&self) -> Pixels {
+        self.unwrapped_layout.font_size
+    }
+
+    pub fn runs(&self) -> &[ShapedRun] {
+        &self.unwrapped_layout.runs
+    }
+}
+
 pub(crate) struct LineLayoutCache {
-    prev_frame: Mutex<HashMap<CacheKey, Arc<WrappedLineLayout>>>,
-    curr_frame: RwLock<HashMap<CacheKey, Arc<WrappedLineLayout>>>,
+    previous_frame: Mutex<HashMap<CacheKey, Arc<LineLayout>>>,
+    current_frame: RwLock<HashMap<CacheKey, Arc<LineLayout>>>,
+    previous_frame_wrapped: Mutex<HashMap<CacheKey, Arc<WrappedLineLayout>>>,
+    current_frame_wrapped: RwLock<HashMap<CacheKey, Arc<WrappedLineLayout>>>,
     platform_text_system: Arc<dyn PlatformTextSystem>,
 }
 
 impl LineLayoutCache {
     pub fn new(platform_text_system: Arc<dyn PlatformTextSystem>) -> Self {
         Self {
-            prev_frame: Mutex::new(HashMap::new()),
-            curr_frame: RwLock::new(HashMap::new()),
+            previous_frame: Mutex::default(),
+            current_frame: RwLock::default(),
+            previous_frame_wrapped: Mutex::default(),
+            current_frame_wrapped: RwLock::default(),
             platform_text_system,
         }
     }
 
     pub fn start_frame(&self) {
-        let mut prev_frame = self.prev_frame.lock();
-        let mut curr_frame = self.curr_frame.write();
+        let mut prev_frame = self.previous_frame.lock();
+        let mut curr_frame = self.current_frame.write();
         std::mem::swap(&mut *prev_frame, &mut *curr_frame);
         curr_frame.clear();
     }
 
-    pub fn layout_line(
+    pub fn layout_wrapped_line(
         &self,
-        text: &SharedString,
+        text: &str,
         font_size: Pixels,
         runs: &[FontRun],
         wrap_width: Option<Pixels>,
@@ -199,34 +239,66 @@ impl LineLayoutCache {
             runs,
             wrap_width,
         } as &dyn AsCacheKeyRef;
-        let curr_frame = self.curr_frame.upgradable_read();
-        if let Some(layout) = curr_frame.get(key) {
+
+        let current_frame = self.current_frame_wrapped.upgradable_read();
+        if let Some(layout) = current_frame.get(key) {
             return layout.clone();
         }
 
-        let mut curr_frame = RwLockUpgradableReadGuard::upgrade(curr_frame);
-        if let Some((key, layout)) = self.prev_frame.lock().remove_entry(key) {
-            curr_frame.insert(key, layout.clone());
+        let mut current_frame = RwLockUpgradableReadGuard::upgrade(current_frame);
+        if let Some((key, layout)) = self.previous_frame_wrapped.lock().remove_entry(key) {
+            current_frame.insert(key, layout.clone());
             layout
         } else {
-            let layout = self.platform_text_system.layout_line(text, font_size, runs);
-            let wrap_boundaries = wrap_width
-                .map(|wrap_width| layout.compute_wrap_boundaries(text.as_ref(), wrap_width))
-                .unwrap_or_default();
-            let wrapped_line = Arc::new(WrappedLineLayout {
-                layout,
-                text: text.clone(),
+            let unwrapped_layout = self.layout_line(text, font_size, runs);
+            let wrap_boundaries = if let Some(wrap_width) = wrap_width {
+                unwrapped_layout.compute_wrap_boundaries(text.as_ref(), wrap_width)
+            } else {
+                SmallVec::new()
+            };
+            let layout = Arc::new(WrappedLineLayout {
+                unwrapped_layout,
                 wrap_boundaries,
+                wrap_width,
             });
-
             let key = CacheKey {
-                text: text.clone(),
+                text: text.into(),
                 font_size,
                 runs: SmallVec::from(runs),
                 wrap_width,
             };
-            curr_frame.insert(key, wrapped_line.clone());
-            wrapped_line
+            current_frame.insert(key, layout.clone());
+            layout
+        }
+    }
+
+    pub fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> Arc<LineLayout> {
+        let key = &CacheKeyRef {
+            text,
+            font_size,
+            runs,
+            wrap_width: None,
+        } as &dyn AsCacheKeyRef;
+
+        let current_frame = self.current_frame.upgradable_read();
+        if let Some(layout) = current_frame.get(key) {
+            return layout.clone();
+        }
+
+        let mut current_frame = RwLockUpgradableReadGuard::upgrade(current_frame);
+        if let Some((key, layout)) = self.previous_frame.lock().remove_entry(key) {
+            current_frame.insert(key, layout.clone());
+            layout
+        } else {
+            let layout = Arc::new(self.platform_text_system.layout_line(text, font_size, runs));
+            let key = CacheKey {
+                text: text.into(),
+                font_size,
+                runs: SmallVec::from(runs),
+                wrap_width: None,
+            };
+            current_frame.insert(key, layout.clone());
+            layout
         }
     }
 }
@@ -243,7 +315,7 @@ trait AsCacheKeyRef {
 
 #[derive(Eq)]
 struct CacheKey {
-    text: SharedString,
+    text: String,
     font_size: Pixels,
     runs: SmallVec<[FontRun; 1]>,
     wrap_width: Option<Pixels>,

crates/gpui2/src/window.rs 🔗

@@ -185,10 +185,27 @@ impl Drop for FocusHandle {
     }
 }
 
+/// FocusableView allows users of your view to easily
+/// focus it (using cx.focus_view(view))
 pub trait FocusableView: Render {
     fn focus_handle(&self, cx: &AppContext) -> FocusHandle;
 }
 
+/// ManagedView is a view (like a Modal, Popover, Menu, etc.)
+/// where the lifecycle of the view is handled by another view.
+pub trait ManagedView: Render {
+    fn focus_handle(&self, cx: &AppContext) -> FocusHandle;
+}
+
+pub struct Dismiss;
+impl<T: ManagedView> EventEmitter<Dismiss> for T {}
+
+impl<T: ManagedView> FocusableView for T {
+    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
+        self.focus_handle(cx)
+    }
+}
+
 // Holds the state for a specific window.
 pub struct Window {
     pub(crate) handle: AnyWindowHandle,
@@ -311,8 +328,8 @@ impl Window {
             layout_engine: TaffyLayoutEngine::new(),
             root_view: None,
             element_id_stack: GlobalElementId::default(),
-            previous_frame: Frame::new(DispatchTree::new(cx.keymap.clone())),
-            current_frame: Frame::new(DispatchTree::new(cx.keymap.clone())),
+            previous_frame: Frame::new(DispatchTree::new(cx.keymap.clone(), cx.actions.clone())),
+            current_frame: Frame::new(DispatchTree::new(cx.keymap.clone(), cx.actions.clone())),
             focus_handles: Arc::new(RwLock::new(SlotMap::with_key())),
             focus_listeners: SubscriberSet::new(),
             default_prevented: true,
@@ -574,6 +591,7 @@ impl<'a> WindowContext<'a> {
         result
     }
 
+    #[must_use]
     /// Add a node to the layout tree for the current frame. Takes the `Style` of the element for which
     /// layout is being requested, along with the layout ids of any children. This method is called during
     /// calls to the `Element::layout` trait method and enables any element to participate in layout.
@@ -1150,6 +1168,14 @@ impl<'a> WindowContext<'a> {
                 self.window.mouse_position = mouse_move.position;
                 InputEvent::MouseMove(mouse_move)
             }
+            InputEvent::MouseDown(mouse_down) => {
+                self.window.mouse_position = mouse_down.position;
+                InputEvent::MouseDown(mouse_down)
+            }
+            InputEvent::MouseUp(mouse_up) => {
+                self.window.mouse_position = mouse_up.position;
+                InputEvent::MouseUp(mouse_up)
+            }
             // Translate dragging and dropping of external files from the operating system
             // to internal drag and drop events.
             InputEvent::FileDrop(file_drop) => match file_drop {

crates/gpui2/tests/action_macros.rs 🔗

@@ -0,0 +1,45 @@
+use serde_derive::Deserialize;
+
+#[test]
+fn test_derive() {
+    use gpui2 as gpui;
+
+    #[derive(PartialEq, Clone, Deserialize, gpui2_macros::Action)]
+    struct AnotherTestAction;
+
+    #[gpui2_macros::register_action]
+    #[derive(PartialEq, Clone, gpui::serde_derive::Deserialize)]
+    struct RegisterableAction {}
+
+    impl gpui::Action for RegisterableAction {
+        fn boxed_clone(&self) -> Box<dyn gpui::Action> {
+            todo!()
+        }
+
+        fn as_any(&self) -> &dyn std::any::Any {
+            todo!()
+        }
+
+        fn partial_eq(&self, _action: &dyn gpui::Action) -> bool {
+            todo!()
+        }
+
+        fn name(&self) -> &str {
+            todo!()
+        }
+
+        fn debug_name() -> &'static str
+        where
+            Self: Sized,
+        {
+            todo!()
+        }
+
+        fn build(_value: serde_json::Value) -> anyhow::Result<Box<dyn gpui::Action>>
+        where
+            Self: Sized,
+        {
+            todo!()
+        }
+    }
+}

crates/gpui2_macros/Cargo.toml 🔗

@@ -9,6 +9,6 @@ path = "src/gpui2_macros.rs"
 proc-macro = true
 
 [dependencies]
-syn = "1.0.72"
+syn = { version = "1.0.72", features = ["full"] }
 quote = "1.0.9"
 proc-macro2 = "1.0.66"

crates/gpui2_macros/src/action.rs 🔗

@@ -15,48 +15,81 @@
 
 use proc_macro::TokenStream;
 use quote::quote;
-use syn::{parse_macro_input, DeriveInput};
+use syn::{parse_macro_input, DeriveInput, Error};
+
+use crate::register_action::register_action;
+
+pub fn action(input: TokenStream) -> TokenStream {
+    let input = parse_macro_input!(input as DeriveInput);
 
-pub fn action(_attr: TokenStream, item: TokenStream) -> TokenStream {
-    let input = parse_macro_input!(item as DeriveInput);
     let name = &input.ident;
-    let attrs = input
-        .attrs
-        .into_iter()
-        .filter(|attr| !attr.path.is_ident("action"))
-        .collect::<Vec<_>>();
-
-    let attributes = quote! {
-        #[gpui::register_action]
-        #[derive(gpui::serde::Deserialize, std::cmp::PartialEq, std::clone::Clone, std::default::Default, std::fmt::Debug)]
-        #(#attrs)*
+
+    if input.generics.lt_token.is_some() {
+        return Error::new(name.span(), "Actions must be a concrete type")
+            .into_compile_error()
+            .into();
+    }
+
+    let is_unit_struct = match input.data {
+        syn::Data::Struct(struct_data) => struct_data.fields.is_empty(),
+        syn::Data::Enum(_) => false,
+        syn::Data::Union(_) => false,
+    };
+
+    let build_impl = if is_unit_struct {
+        quote! {
+            Ok(std::boxed::Box::new(Self {}))
+        }
+    } else {
+        quote! {
+            Ok(std::boxed::Box::new(gpui::serde_json::from_value::<Self>(value)?))
+        }
     };
-    let visibility = input.vis;
-
-    let output = match input.data {
-        syn::Data::Struct(ref struct_data) => match &struct_data.fields {
-            syn::Fields::Named(_) | syn::Fields::Unnamed(_) => {
-                let fields = &struct_data.fields;
-                quote! {
-                    #attributes
-                    #visibility struct #name #fields
-                }
+
+    let register_action = register_action(&name);
+
+    let output = quote! {
+        const _: fn() = || {
+            fn assert_impl<T: ?Sized + for<'a> gpui::serde::Deserialize<'a> +  ::std::cmp::PartialEq + ::std::clone::Clone>() {}
+            assert_impl::<#name>();
+        };
+
+        impl gpui::Action for #name {
+            fn name(&self) -> &'static str
+            {
+                ::std::any::type_name::<#name>()
+            }
+
+            fn debug_name() -> &'static str
+            where
+                Self: ::std::marker::Sized
+            {
+                ::std::any::type_name::<#name>()
+            }
+
+            fn build(value: gpui::serde_json::Value) -> gpui::Result<::std::boxed::Box<dyn gpui::Action>>
+            where
+                Self: ::std::marker::Sized {
+                    #build_impl
             }
-            syn::Fields::Unit => {
-                quote! {
-                    #attributes
-                    #visibility struct #name;
-                }
+
+            fn partial_eq(&self, action: &dyn gpui::Action) -> bool {
+                action
+                    .as_any()
+                    .downcast_ref::<Self>()
+                    .map_or(false, |a| self == a)
             }
-        },
-        syn::Data::Enum(ref enum_data) => {
-            let variants = &enum_data.variants;
-            quote! {
-                #attributes
-                #visibility enum #name { #variants }
+
+            fn boxed_clone(&self) ->  std::boxed::Box<dyn gpui::Action> {
+                ::std::boxed::Box::new(self.clone())
+            }
+
+            fn as_any(&self) -> &dyn ::std::any::Any {
+                self
             }
         }
-        _ => panic!("Expected a struct or an enum."),
+
+        #register_action
     };
 
     TokenStream::from(output)

crates/gpui2_macros/src/gpui2_macros.rs 🔗

@@ -11,14 +11,14 @@ pub fn style_helpers(args: TokenStream) -> TokenStream {
     style_helpers::style_helpers(args)
 }
 
-#[proc_macro_attribute]
-pub fn action(attr: TokenStream, item: TokenStream) -> TokenStream {
-    action::action(attr, item)
+#[proc_macro_derive(Action)]
+pub fn action(input: TokenStream) -> TokenStream {
+    action::action(input)
 }
 
 #[proc_macro_attribute]
 pub fn register_action(attr: TokenStream, item: TokenStream) -> TokenStream {
-    register_action::register_action(attr, item)
+    register_action::register_action_macro(attr, item)
 }
 
 #[proc_macro_derive(Component, attributes(component))]

crates/gpui2_macros/src/register_action.rs 🔗

@@ -12,22 +12,76 @@
 //     gpui2::register_action_builder::<Foo>()
 // }
 use proc_macro::TokenStream;
+use proc_macro2::Ident;
 use quote::{format_ident, quote};
-use syn::{parse_macro_input, DeriveInput};
+use syn::{parse_macro_input, DeriveInput, Error};
 
-pub fn register_action(_attr: TokenStream, item: TokenStream) -> TokenStream {
+pub fn register_action_macro(_attr: TokenStream, item: TokenStream) -> TokenStream {
     let input = parse_macro_input!(item as DeriveInput);
-    let type_name = &input.ident;
-    let ctor_fn_name = format_ident!("register_{}_builder", type_name.to_string().to_lowercase());
+    let registration = register_action(&input.ident);
 
-    let expanded = quote! {
+    let has_action_derive = input
+        .attrs
+        .iter()
+        .find(|attr| {
+            (|| {
+                let meta = attr.parse_meta().ok()?;
+                meta.path().is_ident("derive").then(|| match meta {
+                    syn::Meta::Path(_) => None,
+                    syn::Meta::NameValue(_) => None,
+                    syn::Meta::List(list) => list
+                        .nested
+                        .iter()
+                        .find(|list| match list {
+                            syn::NestedMeta::Meta(meta) => meta.path().is_ident("Action"),
+                            syn::NestedMeta::Lit(_) => false,
+                        })
+                        .map(|_| true),
+                })?
+            })()
+            .unwrap_or(false)
+        })
+        .is_some();
+
+    if has_action_derive {
+        return Error::new(
+            input.ident.span(),
+            "The Action derive macro has already registered this action",
+        )
+        .into_compile_error()
+        .into();
+    }
+
+    TokenStream::from(quote! {
         #input
-        #[allow(non_snake_case)]
-        #[gpui::ctor]
-        fn #ctor_fn_name() {
-            gpui::register_action::<#type_name>()
-        }
-    };
 
-    TokenStream::from(expanded)
+        #registration
+    })
+}
+
+pub(crate) fn register_action(type_name: &Ident) -> proc_macro2::TokenStream {
+    let static_slice_name =
+        format_ident!("__GPUI_ACTIONS_{}", type_name.to_string().to_uppercase());
+
+    let action_builder_fn_name = format_ident!(
+        "__gpui_actions_builder_{}",
+        type_name.to_string().to_lowercase()
+    );
+
+    quote! {
+        #[doc(hidden)]
+        #[gpui::linkme::distributed_slice(gpui::__GPUI_ACTIONS)]
+        #[linkme(crate = gpui::linkme)]
+        static #static_slice_name: gpui::MacroActionBuilder = #action_builder_fn_name;
+
+        /// This is an auto generated function, do not use.
+        #[doc(hidden)]
+        fn #action_builder_fn_name() -> gpui::ActionData {
+            gpui::ActionData {
+                name: ::std::any::type_name::<#type_name>(),
+                type_id: ::std::any::TypeId::of::<#type_name>(),
+                build: <#type_name as gpui::Action>::build,
+            }
+        }
+    }
 }

crates/language/src/buffer.rs 🔗

@@ -17,7 +17,7 @@ use crate::{
 };
 use anyhow::{anyhow, Result};
 pub use clock::ReplicaId;
-use futures::FutureExt as _;
+use futures::channel::oneshot;
 use gpui::{fonts::HighlightStyle, AppContext, Entity, ModelContext, Task};
 use lsp::LanguageServerId;
 use parking_lot::Mutex;
@@ -45,7 +45,7 @@ pub use text::{Buffer as TextBuffer, BufferSnapshot as TextBufferSnapshot, *};
 use theme::SyntaxTheme;
 #[cfg(any(test, feature = "test-support"))]
 use util::RandomCharIter;
-use util::{RangeExt, TryFutureExt as _};
+use util::RangeExt;
 
 #[cfg(any(test, feature = "test-support"))]
 pub use {tree_sitter_rust, tree_sitter_typescript};
@@ -62,6 +62,7 @@ pub struct Buffer {
     saved_mtime: SystemTime,
     transaction_depth: usize,
     was_dirty_before_starting_transaction: Option<bool>,
+    reload_task: Option<Task<Result<()>>>,
     language: Option<Arc<Language>>,
     autoindent_requests: Vec<Arc<AutoindentRequest>>,
     pending_autoindent: Option<Task<()>>,
@@ -509,6 +510,7 @@ impl Buffer {
             saved_mtime,
             saved_version: buffer.version(),
             saved_version_fingerprint: buffer.as_rope().fingerprint(),
+            reload_task: None,
             transaction_depth: 0,
             was_dirty_before_starting_transaction: None,
             text: buffer,
@@ -608,37 +610,52 @@ impl Buffer {
         cx.notify();
     }
 
-    pub fn reload(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<Option<Transaction>>> {
-        cx.spawn(|this, mut cx| async move {
-            if let Some((new_mtime, new_text)) = this.read_with(&cx, |this, cx| {
+    pub fn reload(
+        &mut self,
+        cx: &mut ModelContext<Self>,
+    ) -> oneshot::Receiver<Option<Transaction>> {
+        let (tx, rx) = futures::channel::oneshot::channel();
+        let prev_version = self.text.version();
+        self.reload_task = Some(cx.spawn(|this, mut cx| async move {
+            let Some((new_mtime, new_text)) = this.update(&mut cx, |this, cx| {
                 let file = this.file.as_ref()?.as_local()?;
                 Some((file.mtime(), file.load(cx)))
-            }) {
-                let new_text = new_text.await?;
-                let diff = this
-                    .read_with(&cx, |this, cx| this.diff(new_text, cx))
-                    .await;
-                this.update(&mut cx, |this, cx| {
-                    if this.version() == diff.base_version {
-                        this.finalize_last_transaction();
-                        this.apply_diff(diff, cx);
-                        if let Some(transaction) = this.finalize_last_transaction().cloned() {
-                            this.did_reload(
-                                this.version(),
-                                this.as_rope().fingerprint(),
-                                this.line_ending(),
-                                new_mtime,
-                                cx,
-                            );
-                            return Ok(Some(transaction));
-                        }
-                    }
-                    Ok(None)
-                })
-            } else {
-                Ok(None)
-            }
-        })
+            }) else {
+                return Ok(());
+            };
+
+            let new_text = new_text.await?;
+            let diff = this
+                .update(&mut cx, |this, cx| this.diff(new_text.clone(), cx))
+                .await;
+            this.update(&mut cx, |this, cx| {
+                if this.version() == diff.base_version {
+                    this.finalize_last_transaction();
+                    this.apply_diff(diff, cx);
+                    tx.send(this.finalize_last_transaction().cloned()).ok();
+
+                    this.did_reload(
+                        this.version(),
+                        this.as_rope().fingerprint(),
+                        this.line_ending(),
+                        new_mtime,
+                        cx,
+                    );
+                } else {
+                    this.did_reload(
+                        prev_version,
+                        Rope::text_fingerprint(&new_text),
+                        this.line_ending(),
+                        this.saved_mtime,
+                        cx,
+                    );
+                }
+
+                this.reload_task.take();
+            });
+            Ok(())
+        }));
+        rx
     }
 
     pub fn did_reload(
@@ -667,13 +684,8 @@ impl Buffer {
         cx.notify();
     }
 
-    pub fn file_updated(
-        &mut self,
-        new_file: Arc<dyn File>,
-        cx: &mut ModelContext<Self>,
-    ) -> Task<()> {
+    pub fn file_updated(&mut self, new_file: Arc<dyn File>, cx: &mut ModelContext<Self>) {
         let mut file_changed = false;
-        let mut task = Task::ready(());
 
         if let Some(old_file) = self.file.as_ref() {
             if new_file.path() != old_file.path() {
@@ -693,8 +705,7 @@ impl Buffer {
                     file_changed = true;
 
                     if !self.is_dirty() {
-                        let reload = self.reload(cx).log_err().map(drop);
-                        task = cx.foreground().spawn(reload);
+                        self.reload(cx).close();
                     }
                 }
             }
@@ -708,7 +719,6 @@ impl Buffer {
             cx.emit(Event::FileHandleChanged);
             cx.notify();
         }
-        task
     }
 
     pub fn diff_base(&self) -> Option<&str> {

crates/language2/src/buffer.rs 🔗

@@ -17,8 +17,9 @@ use crate::{
 };
 use anyhow::{anyhow, Result};
 pub use clock::ReplicaId;
-use futures::FutureExt as _;
-use gpui::{AppContext, EventEmitter, HighlightStyle, ModelContext, Task};
+use futures::channel::oneshot;
+use gpui::{AppContext, EventEmitter, HighlightStyle, ModelContext, Task, TaskLabel};
+use lazy_static::lazy_static;
 use lsp::LanguageServerId;
 use parking_lot::Mutex;
 use similar::{ChangeTag, TextDiff};
@@ -45,23 +46,33 @@ pub use text::{Buffer as TextBuffer, BufferSnapshot as TextBufferSnapshot, *};
 use theme::SyntaxTheme;
 #[cfg(any(test, feature = "test-support"))]
 use util::RandomCharIter;
-use util::{RangeExt, TryFutureExt as _};
+use util::RangeExt;
 
 #[cfg(any(test, feature = "test-support"))]
 pub use {tree_sitter_rust, tree_sitter_typescript};
 
 pub use lsp::DiagnosticSeverity;
 
+lazy_static! {
+    pub static ref BUFFER_DIFF_TASK: TaskLabel = TaskLabel::new();
+}
+
 pub struct Buffer {
     text: TextBuffer,
     diff_base: Option<String>,
     git_diff: git::diff::BufferDiff,
     file: Option<Arc<dyn File>>,
-    saved_version: clock::Global,
-    saved_version_fingerprint: RopeFingerprint,
+    /// The mtime of the file when this buffer was last loaded from
+    /// or saved to disk.
     saved_mtime: SystemTime,
+    /// The version vector when this buffer was last loaded from
+    /// or saved to disk.
+    saved_version: clock::Global,
+    /// A hash of the current contents of the buffer's file.
+    file_fingerprint: RopeFingerprint,
     transaction_depth: usize,
     was_dirty_before_starting_transaction: Option<bool>,
+    reload_task: Option<Task<Result<()>>>,
     language: Option<Arc<Language>>,
     autoindent_requests: Vec<Arc<AutoindentRequest>>,
     pending_autoindent: Option<Task<()>>,
@@ -421,8 +432,7 @@ impl Buffer {
                 .ok_or_else(|| anyhow!("missing line_ending"))?,
         ));
         this.saved_version = proto::deserialize_version(&message.saved_version);
-        this.saved_version_fingerprint =
-            proto::deserialize_fingerprint(&message.saved_version_fingerprint)?;
+        this.file_fingerprint = proto::deserialize_fingerprint(&message.saved_version_fingerprint)?;
         this.saved_mtime = message
             .saved_mtime
             .ok_or_else(|| anyhow!("invalid saved_mtime"))?
@@ -438,7 +448,7 @@ impl Buffer {
             diff_base: self.diff_base.as_ref().map(|h| h.to_string()),
             line_ending: proto::serialize_line_ending(self.line_ending()) as i32,
             saved_version: proto::serialize_version(&self.saved_version),
-            saved_version_fingerprint: proto::serialize_fingerprint(self.saved_version_fingerprint),
+            saved_version_fingerprint: proto::serialize_fingerprint(self.file_fingerprint),
             saved_mtime: Some(self.saved_mtime.into()),
         }
     }
@@ -508,7 +518,8 @@ impl Buffer {
         Self {
             saved_mtime,
             saved_version: buffer.version(),
-            saved_version_fingerprint: buffer.as_rope().fingerprint(),
+            file_fingerprint: buffer.as_rope().fingerprint(),
+            reload_task: None,
             transaction_depth: 0,
             was_dirty_before_starting_transaction: None,
             text: buffer,
@@ -574,7 +585,7 @@ impl Buffer {
     }
 
     pub fn saved_version_fingerprint(&self) -> RopeFingerprint {
-        self.saved_version_fingerprint
+        self.file_fingerprint
     }
 
     pub fn saved_mtime(&self) -> SystemTime {
@@ -602,43 +613,58 @@ impl Buffer {
         cx: &mut ModelContext<Self>,
     ) {
         self.saved_version = version;
-        self.saved_version_fingerprint = fingerprint;
+        self.file_fingerprint = fingerprint;
         self.saved_mtime = mtime;
         cx.emit(Event::Saved);
         cx.notify();
     }
 
-    pub fn reload(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<Option<Transaction>>> {
-        cx.spawn(|this, mut cx| async move {
-            if let Some((new_mtime, new_text)) = this.update(&mut cx, |this, cx| {
+    pub fn reload(
+        &mut self,
+        cx: &mut ModelContext<Self>,
+    ) -> oneshot::Receiver<Option<Transaction>> {
+        let (tx, rx) = futures::channel::oneshot::channel();
+        let prev_version = self.text.version();
+        self.reload_task = Some(cx.spawn(|this, mut cx| async move {
+            let Some((new_mtime, new_text)) = this.update(&mut cx, |this, cx| {
                 let file = this.file.as_ref()?.as_local()?;
                 Some((file.mtime(), file.load(cx)))
-            })? {
-                let new_text = new_text.await?;
-                let diff = this
-                    .update(&mut cx, |this, cx| this.diff(new_text, cx))?
-                    .await;
-                this.update(&mut cx, |this, cx| {
-                    if this.version() == diff.base_version {
-                        this.finalize_last_transaction();
-                        this.apply_diff(diff, cx);
-                        if let Some(transaction) = this.finalize_last_transaction().cloned() {
-                            this.did_reload(
-                                this.version(),
-                                this.as_rope().fingerprint(),
-                                this.line_ending(),
-                                new_mtime,
-                                cx,
-                            );
-                            return Some(transaction);
-                        }
-                    }
-                    None
-                })
-            } else {
-                Ok(None)
-            }
-        })
+            })?
+            else {
+                return Ok(());
+            };
+
+            let new_text = new_text.await?;
+            let diff = this
+                .update(&mut cx, |this, cx| this.diff(new_text.clone(), cx))?
+                .await;
+            this.update(&mut cx, |this, cx| {
+                if this.version() == diff.base_version {
+                    this.finalize_last_transaction();
+                    this.apply_diff(diff, cx);
+                    tx.send(this.finalize_last_transaction().cloned()).ok();
+
+                    this.did_reload(
+                        this.version(),
+                        this.as_rope().fingerprint(),
+                        this.line_ending(),
+                        new_mtime,
+                        cx,
+                    );
+                } else {
+                    this.did_reload(
+                        prev_version,
+                        Rope::text_fingerprint(&new_text),
+                        this.line_ending(),
+                        this.saved_mtime,
+                        cx,
+                    );
+                }
+
+                this.reload_task.take();
+            })
+        }));
+        rx
     }
 
     pub fn did_reload(
@@ -650,14 +676,14 @@ impl Buffer {
         cx: &mut ModelContext<Self>,
     ) {
         self.saved_version = version;
-        self.saved_version_fingerprint = fingerprint;
+        self.file_fingerprint = fingerprint;
         self.text.set_line_ending(line_ending);
         self.saved_mtime = mtime;
         if let Some(file) = self.file.as_ref().and_then(|f| f.as_local()) {
             file.buffer_reloaded(
                 self.remote_id(),
                 &self.saved_version,
-                self.saved_version_fingerprint,
+                self.file_fingerprint,
                 self.line_ending(),
                 self.saved_mtime,
                 cx,
@@ -667,13 +693,8 @@ impl Buffer {
         cx.notify();
     }
 
-    pub fn file_updated(
-        &mut self,
-        new_file: Arc<dyn File>,
-        cx: &mut ModelContext<Self>,
-    ) -> Task<()> {
+    pub fn file_updated(&mut self, new_file: Arc<dyn File>, cx: &mut ModelContext<Self>) {
         let mut file_changed = false;
-        let mut task = Task::ready(());
 
         if let Some(old_file) = self.file.as_ref() {
             if new_file.path() != old_file.path() {
@@ -693,8 +714,7 @@ impl Buffer {
                     file_changed = true;
 
                     if !self.is_dirty() {
-                        let reload = self.reload(cx).log_err().map(drop);
-                        task = cx.background_executor().spawn(reload);
+                        self.reload(cx).close();
                     }
                 }
             }
@@ -708,7 +728,6 @@ impl Buffer {
             cx.emit(Event::FileHandleChanged);
             cx.notify();
         }
-        task
     }
 
     pub fn diff_base(&self) -> Option<&str> {
@@ -1159,36 +1178,72 @@ impl Buffer {
     pub fn diff(&self, mut new_text: String, cx: &AppContext) -> Task<Diff> {
         let old_text = self.as_rope().clone();
         let base_version = self.version();
-        cx.background_executor().spawn(async move {
-            let old_text = old_text.to_string();
-            let line_ending = LineEnding::detect(&new_text);
-            LineEnding::normalize(&mut new_text);
-            let diff = TextDiff::from_chars(old_text.as_str(), new_text.as_str());
-            let mut edits = Vec::new();
-            let mut offset = 0;
-            let empty: Arc<str> = "".into();
-            for change in diff.iter_all_changes() {
-                let value = change.value();
-                let end_offset = offset + value.len();
-                match change.tag() {
-                    ChangeTag::Equal => {
-                        offset = end_offset;
-                    }
-                    ChangeTag::Delete => {
-                        edits.push((offset..end_offset, empty.clone()));
-                        offset = end_offset;
+        cx.background_executor()
+            .spawn_labeled(*BUFFER_DIFF_TASK, async move {
+                let old_text = old_text.to_string();
+                let line_ending = LineEnding::detect(&new_text);
+                LineEnding::normalize(&mut new_text);
+
+                let diff = TextDiff::from_chars(old_text.as_str(), new_text.as_str());
+                let empty: Arc<str> = "".into();
+
+                let mut edits = Vec::new();
+                let mut old_offset = 0;
+                let mut new_offset = 0;
+                let mut last_edit: Option<(Range<usize>, Range<usize>)> = None;
+                for change in diff.iter_all_changes().map(Some).chain([None]) {
+                    if let Some(change) = &change {
+                        let len = change.value().len();
+                        match change.tag() {
+                            ChangeTag::Equal => {
+                                old_offset += len;
+                                new_offset += len;
+                            }
+                            ChangeTag::Delete => {
+                                let old_end_offset = old_offset + len;
+                                if let Some((last_old_range, _)) = &mut last_edit {
+                                    last_old_range.end = old_end_offset;
+                                } else {
+                                    last_edit =
+                                        Some((old_offset..old_end_offset, new_offset..new_offset));
+                                }
+                                old_offset = old_end_offset;
+                            }
+                            ChangeTag::Insert => {
+                                let new_end_offset = new_offset + len;
+                                if let Some((_, last_new_range)) = &mut last_edit {
+                                    last_new_range.end = new_end_offset;
+                                } else {
+                                    last_edit =
+                                        Some((old_offset..old_offset, new_offset..new_end_offset));
+                                }
+                                new_offset = new_end_offset;
+                            }
+                        }
                     }
-                    ChangeTag::Insert => {
-                        edits.push((offset..offset, value.into()));
+
+                    if let Some((old_range, new_range)) = &last_edit {
+                        if old_offset > old_range.end
+                            || new_offset > new_range.end
+                            || change.is_none()
+                        {
+                            let text = if new_range.is_empty() {
+                                empty.clone()
+                            } else {
+                                new_text[new_range.clone()].into()
+                            };
+                            edits.push((old_range.clone(), text));
+                            last_edit.take();
+                        }
                     }
                 }
-            }
-            Diff {
-                base_version,
-                line_ending,
-                edits,
-            }
-        })
+
+                Diff {
+                    base_version,
+                    line_ending,
+                    edits,
+                }
+            })
     }
 
     /// Spawn a background task that searches the buffer for any whitespace
@@ -1272,12 +1327,12 @@ impl Buffer {
     }
 
     pub fn is_dirty(&self) -> bool {
-        self.saved_version_fingerprint != self.as_rope().fingerprint()
+        self.file_fingerprint != self.as_rope().fingerprint()
             || self.file.as_ref().map_or(false, |file| file.is_deleted())
     }
 
     pub fn has_conflict(&self) -> bool {
-        self.saved_version_fingerprint != self.as_rope().fingerprint()
+        self.file_fingerprint != self.as_rope().fingerprint()
             && self
                 .file
                 .as_ref()

crates/live_kit_client2/examples/test_app.rs → crates/live_kit_client2/examples/test_app2.rs 🔗

@@ -1,7 +1,7 @@
 use std::{sync::Arc, time::Duration};
 
 use futures::StreamExt;
-use gpui::KeyBinding;
+use gpui::{Action, KeyBinding};
 use live_kit_client2::{
     LocalAudioTrack, LocalVideoTrack, RemoteAudioTrackUpdate, RemoteVideoTrackUpdate, Room,
 };
@@ -10,7 +10,7 @@ use log::LevelFilter;
 use serde_derive::Deserialize;
 use simplelog::SimpleLogger;
 
-#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default)]
+#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Action)]
 struct Quit;
 
 fn main() {

crates/picker2/src/picker2.rs 🔗

@@ -1,7 +1,7 @@
 use editor::Editor;
 use gpui::{
-    div, prelude::*, uniform_list, Component, Div, MouseButton, Render, Task,
-    UniformListScrollHandle, View, ViewContext, WindowContext,
+    div, prelude::*, uniform_list, AppContext, Component, Div, FocusHandle, FocusableView,
+    MouseButton, Render, Task, UniformListScrollHandle, View, ViewContext, WindowContext,
 };
 use std::{cmp, sync::Arc};
 use ui::{prelude::*, v_stack, Divider, Label, TextColor};
@@ -35,6 +35,12 @@ pub trait PickerDelegate: Sized + 'static {
     ) -> Self::ListItem;
 }
 
+impl<D: PickerDelegate> FocusableView for Picker<D> {
+    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
+        self.editor.focus_handle(cx)
+    }
+}
+
 impl<D: PickerDelegate> Picker<D> {
     pub fn new(delegate: D, cx: &mut ViewContext<Self>) -> Self {
         let editor = cx.build_view(|cx| {

crates/project/src/project.rs 🔗

@@ -6190,7 +6190,7 @@ impl Project {
                                 .log_err();
                         }
 
-                        buffer.file_updated(Arc::new(new_file), cx).detach();
+                        buffer.file_updated(Arc::new(new_file), cx);
                     }
                 }
             });
@@ -7182,7 +7182,7 @@ impl Project {
                     .ok_or_else(|| anyhow!("no such worktree"))?;
                 let file = File::from_proto(file, worktree, cx)?;
                 buffer.update(cx, |buffer, cx| {
-                    buffer.file_updated(Arc::new(file), cx).detach();
+                    buffer.file_updated(Arc::new(file), cx);
                 });
                 this.detect_language_for_buffer(&buffer, cx);
             }

crates/project/src/worktree.rs 🔗

@@ -959,7 +959,7 @@ impl LocalWorktree {
 
                 buffer_handle.update(&mut cx, |buffer, cx| {
                     if has_changed_file {
-                        buffer.file_updated(new_file, cx).detach();
+                        buffer.file_updated(new_file, cx);
                     }
                 });
             }

crates/project2/src/project2.rs 🔗

@@ -6262,7 +6262,7 @@ impl Project {
                                 .log_err();
                         }
 
-                        buffer.file_updated(Arc::new(new_file), cx).detach();
+                        buffer.file_updated(Arc::new(new_file), cx);
                     }
                 }
             });
@@ -7256,7 +7256,7 @@ impl Project {
                     .ok_or_else(|| anyhow!("no such worktree"))?;
                 let file = File::from_proto(file, worktree, cx)?;
                 buffer.update(cx, |buffer, cx| {
-                    buffer.file_updated(Arc::new(file), cx).detach();
+                    buffer.file_updated(Arc::new(file), cx);
                 });
                 this.detect_language_for_buffer(&buffer, cx);
             }

crates/project2/src/project_tests.rs 🔗

@@ -2587,6 +2587,73 @@ async fn test_save_file(cx: &mut gpui::TestAppContext) {
     assert_eq!(new_text, buffer.update(cx, |buffer, _| buffer.text()));
 }
 
+#[gpui::test(iterations = 30)]
+async fn test_file_changes_multiple_times_on_disk(cx: &mut gpui::TestAppContext) {
+    init_test(cx);
+
+    let fs = FakeFs::new(cx.executor().clone());
+    fs.insert_tree(
+        "/dir",
+        json!({
+            "file1": "the original contents",
+        }),
+    )
+    .await;
+
+    let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
+    let worktree = project.read_with(cx, |project, _| project.worktrees().next().unwrap());
+    let buffer = project
+        .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
+        .await
+        .unwrap();
+
+    // Simulate buffer diffs being slow, so that they don't complete before
+    // the next file change occurs.
+    cx.executor().deprioritize(*language::BUFFER_DIFF_TASK);
+
+    // Change the buffer's file on disk, and then wait for the file change
+    // to be detected by the worktree, so that the buffer starts reloading.
+    fs.save(
+        "/dir/file1".as_ref(),
+        &"the first contents".into(),
+        Default::default(),
+    )
+    .await
+    .unwrap();
+    worktree.next_event(cx);
+
+    // Change the buffer's file again. Depending on the random seed, the
+    // previous file change may still be in progress.
+    fs.save(
+        "/dir/file1".as_ref(),
+        &"the second contents".into(),
+        Default::default(),
+    )
+    .await
+    .unwrap();
+    worktree.next_event(cx);
+
+    cx.executor().run_until_parked();
+    let on_disk_text = fs.load(Path::new("/dir/file1")).await.unwrap();
+    buffer.read_with(cx, |buffer, _| {
+        let buffer_text = buffer.text();
+        if buffer_text == on_disk_text {
+            assert!(
+                !buffer.is_dirty() && !buffer.has_conflict(),
+                "buffer shouldn't be dirty. text: {buffer_text:?}, disk text: {on_disk_text:?}",
+            );
+        }
+        // If the file change occurred while the buffer was processing the first
+        // change, the buffer will be in a conflicting state.
+        else {
+            assert!(
+                buffer.is_dirty() && buffer.has_conflict(),
+                "buffer should report that it has a conflict. text: {buffer_text:?}, disk text: {on_disk_text:?}"
+            );
+        }
+    });
+}
+
 #[gpui::test]
 async fn test_save_in_single_file_worktree(cx: &mut gpui::TestAppContext) {
     init_test(cx);

crates/project2/src/worktree.rs 🔗

@@ -276,6 +276,7 @@ struct ShareState {
     _maintain_remote_snapshot: Task<Option<()>>,
 }
 
+#[derive(Clone)]
 pub enum Event {
     UpdatedEntries(UpdatedEntriesSet),
     UpdatedGitRepositories(UpdatedGitRepositoriesSet),
@@ -961,7 +962,7 @@ impl LocalWorktree {
 
                 buffer_handle.update(&mut cx, |buffer, cx| {
                     if has_changed_file {
-                        buffer.file_updated(new_file, cx).detach();
+                        buffer.file_updated(new_file, cx);
                     }
                 })?;
             }

crates/project_panel2/src/project_panel.rs 🔗

@@ -1579,7 +1579,7 @@ mod tests {
         path::{Path, PathBuf},
         sync::atomic::{self, AtomicUsize},
     };
-    use workspace::{pane, AppState};
+    use workspace::AppState;
 
     #[gpui::test]
     async fn test_visible_list(cx: &mut gpui::TestAppContext) {
@@ -2785,7 +2785,7 @@ mod tests {
             let settings_store = SettingsStore::test(cx);
             cx.set_global(settings_store);
             init_settings(cx);
-            theme::init(cx);
+            theme::init(theme::LoadThemes::JustBase, cx);
             language::init(cx);
             editor::init_settings(cx);
             crate::init((), cx);
@@ -2798,11 +2798,10 @@ mod tests {
     fn init_test_with_editor(cx: &mut TestAppContext) {
         cx.update(|cx| {
             let app_state = AppState::test(cx);
-            theme::init(cx);
+            theme::init(theme::LoadThemes::JustBase, 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);

crates/rope/src/rope.rs 🔗

@@ -41,6 +41,10 @@ impl Rope {
         Self::default()
     }
 
+    pub fn text_fingerprint(text: &str) -> RopeFingerprint {
+        bromberg_sl2::hash_strict(text.as_bytes())
+    }
+
     pub fn append(&mut self, rope: Rope) {
         let mut chunks = rope.chunks.cursor::<()>();
         chunks.next(&());
@@ -931,7 +935,7 @@ impl<'a> From<&'a str> for ChunkSummary {
     fn from(text: &'a str) -> Self {
         Self {
             text: TextSummary::from(text),
-            fingerprint: bromberg_sl2::hash_strict(text.as_bytes()),
+            fingerprint: Rope::text_fingerprint(text),
         }
     }
 }

crates/rope2/src/rope2.rs 🔗

@@ -41,6 +41,10 @@ impl Rope {
         Self::default()
     }
 
+    pub fn text_fingerprint(text: &str) -> RopeFingerprint {
+        bromberg_sl2::hash_strict(text.as_bytes())
+    }
+
     pub fn append(&mut self, rope: Rope) {
         let mut chunks = rope.chunks.cursor::<()>();
         chunks.next(&());
@@ -931,7 +935,7 @@ impl<'a> From<&'a str> for ChunkSummary {
     fn from(text: &'a str) -> Self {
         Self {
             text: TextSummary::from(text),
-            fingerprint: bromberg_sl2::hash_strict(text.as_bytes()),
+            fingerprint: Rope::text_fingerprint(text),
         }
     }
 }

crates/settings2/src/keymap_file.rs 🔗

@@ -73,9 +73,9 @@ impl KeymapFile {
                                     "Expected first item in array to be a string."
                                 )));
                             };
-                            gpui::build_action(&name, Some(data))
+                            cx.build_action(&name, Some(data))
                         }
-                        Value::String(name) => gpui::build_action(&name, None),
+                        Value::String(name) => cx.build_action(&name, None),
                         Value::Null => Ok(no_action()),
                         _ => {
                             return Some(Err(anyhow!("Expected two-element array, got {action:?}")))

crates/settings2/src/settings_file.rs 🔗

@@ -16,6 +16,9 @@ pub fn test_settings() -> String {
     .unwrap();
     util::merge_non_null_json_value_into(
         serde_json::json!({
+            "ui_font_family": "Courier",
+            "ui_font_features": {},
+            "ui_font_size": 14,
             "buffer_font_family": "Courier",
             "buffer_font_features": {},
             "buffer_font_size": 14,

crates/storybook2/src/storybook2.rs 🔗

@@ -60,13 +60,12 @@ fn main() {
             .unwrap();
         cx.set_global(store);
 
-        theme2::init(cx);
+        theme2::init(theme2::LoadThemes::All, cx);
 
         let selector =
             story_selector.unwrap_or(StorySelector::Component(ComponentStory::Workspace));
 
         let theme_registry = cx.global::<ThemeRegistry>();
-
         let mut theme_settings = ThemeSettings::get_global(cx).clone();
         theme_settings.active_theme = theme_registry.get(&theme_name).unwrap();
         ThemeSettings::override_global(theme_settings, cx);
@@ -114,6 +113,7 @@ impl Render for StoryWrapper {
             .flex()
             .flex_col()
             .size_full()
+            .font("Zed Mono")
             .child(self.story.clone())
     }
 }

crates/storybook3/Cargo.toml 🔗

@@ -0,0 +1,17 @@
+[package]
+name = "storybook3"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[[bin]]
+name = "storybook"
+path = "src/storybook3.rs"
+
+[dependencies]
+anyhow.workspace = true
+
+gpui = { package = "gpui2", path = "../gpui2" }
+ui = { package = "ui2", path = "../ui2", features = ["stories"] }
+theme = { package = "theme2", path = "../theme2", features = ["stories"] }
+settings = { package = "settings2", path = "../settings2"}

crates/storybook3/src/storybook3.rs 🔗

@@ -0,0 +1,73 @@
+use anyhow::Result;
+use gpui::AssetSource;
+use gpui::{
+    div, px, size, AnyView, Bounds, Div, Render, ViewContext, VisualContext, WindowBounds,
+    WindowOptions,
+};
+use settings::{default_settings, Settings, SettingsStore};
+use std::borrow::Cow;
+use std::sync::Arc;
+use theme::ThemeSettings;
+use ui::{prelude::*, ContextMenuStory};
+
+struct Assets;
+
+impl AssetSource for Assets {
+    fn load(&self, _path: &str) -> Result<Cow<[u8]>> {
+        todo!();
+    }
+
+    fn list(&self, _path: &str) -> Result<Vec<SharedString>> {
+        Ok(vec![])
+    }
+}
+
+fn main() {
+    let asset_source = Arc::new(Assets);
+    gpui::App::production(asset_source).run(move |cx| {
+        let mut store = SettingsStore::default();
+        store
+            .set_default_settings(default_settings().as_ref(), cx)
+            .unwrap();
+        cx.set_global(store);
+        ui::settings::init(cx);
+        theme::init(theme::LoadThemes::JustBase, cx);
+
+        cx.open_window(
+            WindowOptions {
+                bounds: WindowBounds::Fixed(Bounds {
+                    origin: Default::default(),
+                    size: size(px(1500.), px(780.)).into(),
+                }),
+                ..Default::default()
+            },
+            move |cx| {
+                let ui_font_size = ThemeSettings::get_global(cx).ui_font_size;
+                cx.set_rem_size(ui_font_size);
+
+                cx.build_view(|cx| TestView {
+                    story: cx.build_view(|_| ContextMenuStory).into(),
+                })
+            },
+        );
+
+        cx.activate(true);
+    })
+}
+
+struct TestView {
+    story: AnyView,
+}
+
+impl Render for TestView {
+    type Element = Div<Self>;
+
+    fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
+        div()
+            .flex()
+            .flex_col()
+            .size_full()
+            .font("Helvetica")
+            .child(self.story.clone())
+    }
+}

crates/terminal_view2/Cargo.toml 🔗

@@ -9,7 +9,6 @@ path = "src/terminal_view.rs"
 doctest = false
 
 [dependencies]
-# context_menu = { package = "context_menu2", path = "../context_menu2" }
 editor = { package = "editor2", path = "../editor2" }
 language = { package = "language2", path = "../language2" }
 gpui = { package = "gpui2", path = "../gpui2" }

crates/terminal_view2/src/terminal_element.rs 🔗

@@ -1,8 +1,8 @@
 // use editor::{Cursor, HighlightedRange, HighlightedRangeLine};
 // use gpui::{
-//     AnyElement, AppContext, Bounds, Component, Element, HighlightStyle, Hsla, LayoutId, Line,
-//     ModelContext, MouseButton, Pixels, Point, TextStyle, Underline, ViewContext, WeakModel,
-//     WindowContext,
+//     point, transparent_black, AnyElement, AppContext, Bounds, Component, CursorStyle, Element,
+//     FontStyle, FontWeight, HighlightStyle, Hsla, LayoutId, Line, ModelContext, MouseButton,
+//     Overlay, Pixels, Point, Quad, TextStyle, Underline, ViewContext, WeakModel, WindowContext,
 // };
 // use itertools::Itertools;
 // use language::CursorShape;
@@ -23,6 +23,7 @@
 //     TerminalSize,
 // };
 // use theme::ThemeSettings;
+// use workspace::ElementId;
 
 // use std::mem;
 // use std::{fmt::Debug, ops::RangeInclusive};
@@ -130,23 +131,24 @@
 //         cx: &mut ViewContext<TerminalView>,
 //     ) {
 //         let position = {
-//             let point = self.point;
-//             vec2f(
-//                 (origin.x() + point.column as f32 * layout.size.cell_width).floor(),
-//                 origin.y() + point.line as f32 * layout.size.line_height,
+//             let alac_point = self.point;
+//             point(
+//                 (origin.x + alac_point.column as f32 * layout.size.cell_width).floor(),
+//                 origin.y + alac_point.line as f32 * layout.size.line_height,
 //             )
 //         };
-//         let size = vec2f(
+//         let size = point(
 //             (layout.size.cell_width * self.num_of_cells as f32).ceil(),
 //             layout.size.line_height,
-//         );
+//         )
+//         .into();
 
 //         cx.paint_quad(
 //             Bounds::new(position, size),
 //             Default::default(),
 //             self.color,
 //             Default::default(),
-//             Default::default(),
+//             transparent_black(),
 //         );
 //     }
 // }
@@ -281,9 +283,9 @@
 //         cursor_point: DisplayCursor,
 //         size: TerminalSize,
 //         text_fragment: &Line,
-//     ) -> Option<(Vector2F, f32)> {
+//     ) -> Option<(Point<Pixels>, Pixels)> {
 //         if cursor_point.line() < size.total_lines() as i32 {
-//             let cursor_width = if text_fragment.width == 0. {
+//             let cursor_width = if text_fragment.width == Pixels::ZERO {
 //                 size.cell_width()
 //             } else {
 //                 text_fragment.width
@@ -292,7 +294,7 @@
 //             //Cursor should always surround as much of the text as possible,
 //             //hence when on pixel boundaries round the origin down and the width up
 //             Some((
-//                 vec2f(
+//                 point(
 //                     (cursor_point.col() as f32 * size.cell_width()).floor(),
 //                     (cursor_point.line() as f32 * size.line_height()).floor(),
 //                 ),
@@ -332,15 +334,15 @@
 
 //         let mut properties = Properties::new();
 //         if indexed.flags.intersects(Flags::BOLD | Flags::DIM_BOLD) {
-//             properties = *properties.weight(Weight::BOLD);
+//             properties = *properties.weight(FontWeight::BOLD);
 //         }
 //         if indexed.flags.intersects(Flags::ITALIC) {
-//             properties = *properties.style(Italic);
+//             properties = *properties.style(FontStyle::Italic);
 //         }
 
 //         let font_id = font_cache
-//             .select_font(text_style.font_family_id, &properties)
-//             .unwrap_or(8text_style.font_id);
+//             .select_font(text_style.font_family, &properties)
+//             .unwrap_or(text_style.font_id);
 
 //         let mut result = RunStyle {
 //             color: fg,
@@ -366,7 +368,7 @@
 //     fn generic_button_handler<E>(
 //         connection: WeakModel<Terminal>,
 //         origin: Point<Pixels>,
-//         f: impl Fn(&mut Terminal, Vector2F, E, &mut ModelContext<Terminal>),
+//         f: impl Fn(&mut Terminal, Point<Pixels>, E, &mut ModelContext<Terminal>),
 //     ) -> impl Fn(E, &mut TerminalView, &mut EventContext<TerminalView>) {
 //         move |event, _: &mut TerminalView, cx| {
 //             cx.focus_parent();
@@ -522,9 +524,9 @@
 //     fn layout(
 //         &mut self,
 //         view_state: &mut TerminalView,
-//         element_state: &mut Self::ElementState,
+//         element_state: Option<Self::ElementState>,
 //         cx: &mut ViewContext<TerminalView>,
-//     ) -> LayoutId {
+//     ) -> (LayoutId, Self::ElementState) {
 //         let settings = ThemeSettings::get_global(cx);
 //         let terminal_settings = TerminalSettings::get_global(cx);
 
@@ -569,7 +571,7 @@
 //             let cell_width = font_cache.em_advance(text_style.font_id, text_style.font_size);
 //             gutter = cell_width;
 
-//             let size = constraint.max - vec2f(gutter, 0.);
+//             let size = constraint.max - point(gutter, 0.);
 //             TerminalSize::new(line_height, cell_width, size)
 //         };
 
@@ -607,11 +609,11 @@
 //                         cx,
 //                     ),
 //             )
-//             .with_position_mode(gpui::elements::OverlayPositionMode::Local)
+//             .with_position_mode(gpui::OverlayPositionMode::Local)
 //             .into_any();
 
 //             tooltip.layout(
-//                 SizeConstraint::new(Vector2F::zero(), cx.window_size()),
+//                 SizeConstraint::new(Point::zero(), cx.window_size()),
 //                 view_state,
 //                 cx,
 //             );
@@ -735,7 +737,7 @@
 //         let clip_bounds = Some(visible_bounds);
 
 //         cx.paint_layer(clip_bounds, |cx| {
-//             let origin = bounds.origin() + vec2f(element_state.gutter, 0.);
+//             let origin = bounds.origin + point(element_state.gutter, 0.);
 
 //             // Elements are ephemeral, only at paint time do we know what could be clicked by a mouse
 //             self.attach_mouse_handlers(origin, visible_bounds, element_state.mode, cx);
@@ -808,7 +810,7 @@
 //         });
 //     }
 
-//     fn id(&self) -> Option<gpui::ElementId> {
+//     fn element_id(&self) -> Option<ElementId> {
 //         todo!()
 //     }
 
@@ -842,12 +844,12 @@
 //     // ) -> Option<Bounds<Pixels>> {
 //     //     // Use the same origin that's passed to `Cursor::paint` in the paint
 //     //     // method bove.
-//     //     let mut origin = bounds.origin() + vec2f(layout.size.cell_width, 0.);
+//     //     let mut origin = bounds.origin() + point(layout.size.cell_width, 0.);
 
 //     //     // TODO - Why is it necessary to move downward one line to get correct
 //     //     // positioning? I would think that we'd want the same rect that is
 //     //     // painted for the cursor.
-//     //     origin += vec2f(0., layout.size.line_height);
+//     //     origin += point(0., layout.size.line_height);
 
 //     //     Some(layout.cursor.as_ref()?.bounding_rect(origin))
 //     // }
@@ -886,7 +888,7 @@
 //     range: &RangeInclusive<AlacPoint>,
 //     layout: &LayoutState,
 //     origin: Point<Pixels>,
-// ) -> Option<(f32, Vec<HighlightedRangeLine>)> {
+// ) -> Option<(Pixels, Vec<HighlightedRangeLine>)> {
 //     // Step 1. Normalize the points to be viewport relative.
 //     // When display_offset = 1, here's how the grid is arranged:
 //     //-2,0 -2,1...
@@ -937,8 +939,8 @@
 //         }
 
 //         highlighted_range_lines.push(HighlightedRangeLine {
-//             start_x: origin.x() + line_start as f32 * layout.size.cell_width,
-//             end_x: origin.x() + line_end as f32 * layout.size.cell_width,
+//             start_x: origin.x + line_start as f32 * layout.size.cell_width,
+//             end_x: origin.x + line_end as f32 * layout.size.cell_width,
 //         });
 //     }
 

crates/terminal_view2/src/terminal_panel.rs 🔗

@@ -304,13 +304,13 @@ impl TerminalPanel {
             .pane
             .read(cx)
             .items()
-            .map(|item| item.id().as_u64())
+            .map(|item| item.item_id().as_u64())
             .collect::<Vec<_>>();
         let active_item_id = self
             .pane
             .read(cx)
             .active_item()
-            .map(|item| item.id().as_u64());
+            .map(|item| item.item_id().as_u64());
         let height = self.height;
         let width = self.width;
         self.pending_serialization = cx.background_executor().spawn(

crates/terminal_view2/src/terminal_view.rs 🔗

@@ -7,27 +7,17 @@ pub mod terminal_panel;
 
 // todo!()
 // use crate::terminal_element::TerminalElement;
-use anyhow::Context;
-use dirs::home_dir;
 use editor::{scroll::autoscroll::Autoscroll, Editor};
 use gpui::{
-    actions, div, img, red, register_action, AnyElement, AppContext, Component, DispatchPhase, Div,
+    actions, div, img, red, Action, AnyElement, AppContext, Component, DispatchPhase, Div,
     EventEmitter, FocusEvent, FocusHandle, Focusable, FocusableComponent, FocusableView,
-    InputHandler, InteractiveComponent, KeyDownEvent, Keystroke, Model, ParentComponent, Pixels,
-    Render, SharedString, Styled, Task, View, ViewContext, VisualContext, WeakView,
+    InputHandler, InteractiveComponent, KeyDownEvent, Keystroke, Model, MouseButton,
+    ParentComponent, Pixels, Render, SharedString, Styled, Task, View, ViewContext, VisualContext,
+    WeakView,
 };
 use language::Bias;
 use persistence::TERMINAL_DB;
 use project::{search::SearchQuery, LocalWorktree, Project};
-use serde::Deserialize;
-use settings::Settings;
-use smol::Timer;
-use std::{
-    ops::RangeInclusive,
-    path::{Path, PathBuf},
-    sync::Arc,
-    time::Duration,
-};
 use terminal::{
     alacritty_terminal::{
         index::Point,
@@ -42,7 +32,21 @@ use workspace::{
     notifications::NotifyResultExt,
     register_deserializable_item,
     searchable::{SearchEvent, SearchOptions, SearchableItem},
-    NewCenterTerminal, Pane, ToolbarItemLocation, Workspace, WorkspaceId,
+    ui::{ContextMenu, Label},
+    CloseActiveItem, NewCenterTerminal, Pane, ToolbarItemLocation, Workspace, WorkspaceId,
+};
+
+use anyhow::Context;
+use dirs::home_dir;
+use serde::Deserialize;
+use settings::Settings;
+use smol::Timer;
+
+use std::{
+    ops::RangeInclusive,
+    path::{Path, PathBuf},
+    sync::Arc,
+    time::Duration,
 };
 
 const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
@@ -51,17 +55,16 @@ const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
 #[derive(Clone, Debug, PartialEq)]
 pub struct ScrollTerminal(pub i32);
 
-#[register_action]
-#[derive(Clone, Debug, Default, Deserialize, PartialEq)]
+#[derive(Clone, Debug, Default, Deserialize, PartialEq, Action)]
 pub struct SendText(String);
 
-#[register_action]
-#[derive(Clone, Debug, Default, Deserialize, PartialEq)]
+#[derive(Clone, Debug, Default, Deserialize, PartialEq, Action)]
 pub struct SendKeystroke(String);
 
 actions!(Clear, Copy, Paste, ShowCharacterPalette, SearchTest);
 
 pub fn init(cx: &mut AppContext) {
+    workspace::ui::init(cx);
     terminal_panel::init(cx);
     terminal::init(cx);
 
@@ -82,7 +85,7 @@ pub struct TerminalView {
     has_new_content: bool,
     //Currently using iTerm bell, show bell emoji in tab until input is received
     has_bell: bool,
-    // context_menu: View<ContextMenu>,
+    context_menu: Option<View<ContextMenu>>,
     blink_state: bool,
     blinking_on: bool,
     blinking_paused: bool,
@@ -265,8 +268,7 @@ impl TerminalView {
             has_new_content: true,
             has_bell: false,
             focus_handle: cx.focus_handle(),
-            // todo!()
-            // context_menu: cx.build_view(|cx| ContextMenu::new(view_id, cx)),
+            context_menu: None,
             blink_state: true,
             blinking_on: false,
             blinking_paused: false,
@@ -293,18 +295,24 @@ impl TerminalView {
         cx.emit(Event::Wakeup);
     }
 
-    pub fn deploy_context_menu(&mut self, _position: Point<Pixels>, _cx: &mut ViewContext<Self>) {
-        //todo!(context_menu)
-        // let menu_entries = vec![
-        //     ContextMenuItem::action("Clear", Clear),
-        //     ContextMenuItem::action("Close", pane::CloseActiveItem { save_intent: None }),
-        // ];
-
-        // self.context_menu.update(cx, |menu, cx| {
-        //     menu.show(position, AnchorCorner::TopLeft, menu_entries, cx)
-        // });
-
-        // cx.notify();
+    pub fn deploy_context_menu(
+        &mut self,
+        position: gpui::Point<Pixels>,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.context_menu = Some(cx.build_view(|cx| {
+            ContextMenu::new(cx)
+                .entry(Label::new("Clear"), Box::new(Clear))
+                .entry(
+                    Label::new("Close"),
+                    Box::new(CloseActiveItem { save_intent: None }),
+                )
+        }));
+        dbg!(&position);
+        // todo!()
+        //     self.context_menu
+        //         .show(position, AnchorCorner::TopLeft, menu_entries, cx);
+        //     cx.notify();
     }
 
     fn show_character_palette(&mut self, _: &ShowCharacterPalette, cx: &mut ViewContext<Self>) {
@@ -541,28 +549,41 @@ impl Render for TerminalView {
         let focused = self.focus_handle.is_focused(cx);
 
         div()
+            .relative()
+            .child(
+                div()
+                    .z_index(0)
+                    .absolute()
+                    .on_key_down(Self::key_down)
+                    .on_action(TerminalView::send_text)
+                    .on_action(TerminalView::send_keystroke)
+                    .on_action(TerminalView::copy)
+                    .on_action(TerminalView::paste)
+                    .on_action(TerminalView::clear)
+                    .on_action(TerminalView::show_character_palette)
+                    .on_action(TerminalView::select_all)
+                    // todo!()
+                    .child(
+                        "TERMINAL HERE", //     TerminalElement::new(
+                                         //     terminal_handle,
+                                         //     focused,
+                                         //     self.should_show_cursor(focused, cx),
+                                         //     self.can_navigate_to_selected_word,
+                                         // )
+                    )
+                    .on_mouse_down(MouseButton::Right, |this, event, cx| {
+                        this.deploy_context_menu(event.position, cx);
+                        cx.notify();
+                    }),
+            )
+            .children(
+                self.context_menu
+                    .clone()
+                    .map(|context_menu| div().z_index(1).absolute().child(context_menu.render())),
+            )
             .track_focus(&self.focus_handle)
             .on_focus_in(Self::focus_in)
             .on_focus_out(Self::focus_out)
-            .on_key_down(Self::key_down)
-            .on_action(TerminalView::send_text)
-            .on_action(TerminalView::send_keystroke)
-            .on_action(TerminalView::copy)
-            .on_action(TerminalView::paste)
-            .on_action(TerminalView::clear)
-            .on_action(TerminalView::show_character_palette)
-            .on_action(TerminalView::select_all)
-            // todo!()
-            .child(
-                "TERMINAL HERE", //     TerminalElement::new(
-                                 //     terminal_handle,
-                                 //     focused,
-                                 //     self.should_show_cursor(focused, cx),
-                                 //     self.can_navigate_to_selected_word,
-                                 // )
-            )
-        // todo!()
-        // .child(ChildView::new(&self.context_menu, cx))
     }
 }
 
@@ -1107,7 +1128,7 @@ mod tests {
     pub async fn init_test(cx: &mut TestAppContext) -> (Model<Project>, View<Workspace>) {
         let params = cx.update(AppState::test);
         cx.update(|cx| {
-            theme::init(cx);
+            theme::init(theme::LoadThemes::JustBase, cx);
             Project::init_settings(cx);
             language::init(cx);
         });

crates/theme2/src/registry.rs 🔗

@@ -100,6 +100,11 @@ impl ThemeRegistry {
             .ok_or_else(|| anyhow!("theme not found: {}", name))
             .cloned()
     }
+
+    pub fn load_user_themes(&mut self) {
+        #[cfg(not(feature = "importing-themes"))]
+        self.insert_user_theme_familes(crate::all_user_themes());
+    }
 }
 
 impl Default for ThemeRegistry {
@@ -110,9 +115,6 @@ impl Default for ThemeRegistry {
 
         this.insert_theme_families([one_family()]);
 
-        #[cfg(not(feature = "importing-themes"))]
-        this.insert_user_theme_familes(crate::all_user_themes());
-
         this
     }
 }

crates/theme2/src/settings.rs 🔗

@@ -34,6 +34,10 @@ pub struct ThemeSettingsContent {
     #[serde(default)]
     pub ui_font_size: Option<f32>,
     #[serde(default)]
+    pub ui_font_family: Option<String>,
+    #[serde(default)]
+    pub ui_font_features: Option<FontFeatures>,
+    #[serde(default)]
     pub buffer_font_family: Option<String>,
     #[serde(default)]
     pub buffer_font_size: Option<f32>,
@@ -117,13 +121,13 @@ impl settings::Settings for ThemeSettings {
         user_values: &[&Self::FileContent],
         cx: &mut AppContext,
     ) -> Result<Self> {
-        let themes = cx.default_global::<Arc<ThemeRegistry>>();
+        let themes = cx.default_global::<ThemeRegistry>();
 
         let mut this = Self {
-            ui_font_size: defaults.ui_font_size.unwrap_or(16.).into(),
+            ui_font_size: defaults.ui_font_size.unwrap().into(),
             ui_font: Font {
-                family: "Helvetica".into(),
-                features: Default::default(),
+                family: defaults.ui_font_family.clone().unwrap().into(),
+                features: defaults.ui_font_features.clone().unwrap(),
                 weight: Default::default(),
                 style: Default::default(),
             },
@@ -149,6 +153,13 @@ impl settings::Settings for ThemeSettings {
                 this.buffer_font.features = value;
             }
 
+            if let Some(value) = value.ui_font_family {
+                this.ui_font.family = value.into();
+            }
+            if let Some(value) = value.ui_font_features {
+                this.ui_font.features = value;
+            }
+
             if let Some(value) = &value.theme {
                 if let Some(theme) = themes.get(value).log_err() {
                     this.active_theme = theme;

crates/theme2/src/theme2.rs 🔗

@@ -31,8 +31,25 @@ pub enum Appearance {
     Dark,
 }
 
-pub fn init(cx: &mut AppContext) {
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+pub enum LoadThemes {
+    /// Only load the base theme.
+    ///
+    /// No user themes will be loaded.
+    JustBase,
+
+    /// Load all of the built-in themes.
+    All,
+}
+
+pub fn init(themes_to_load: LoadThemes, cx: &mut AppContext) {
     cx.set_global(ThemeRegistry::default());
+
+    match themes_to_load {
+        LoadThemes::JustBase => (),
+        LoadThemes::All => cx.global_mut::<ThemeRegistry>().load_user_themes(),
+    }
+
     ThemeSettings::register(cx);
 }
 

crates/ui2/Cargo.toml 🔗

@@ -9,6 +9,7 @@ anyhow.workspace = true
 chrono = "0.4"
 gpui = { package = "gpui2", path = "../gpui2" }
 itertools = { version = "0.11.0", optional = true }
+menu = { package = "menu2", path = "../menu2"}
 serde.workspace = true
 settings2 = { path = "../settings2" }
 smallvec.workspace = true

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

@@ -178,6 +178,7 @@ impl<V: 'static> Button<V> {
             .text_ui()
             .rounded_md()
             .bg(self.variant.bg_color(cx))
+            .cursor_pointer()
             .hover(|style| style.bg(self.variant.bg_color_hover(cx)))
             .active(|style| style.bg(self.variant.bg_color_active(cx)));
 

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

@@ -1,60 +1,255 @@
-use crate::{prelude::*, ListItemVariant};
+use std::cell::RefCell;
+use std::rc::Rc;
+
+use crate::prelude::*;
 use crate::{v_stack, Label, List, ListEntry, ListItem, ListSeparator, ListSubHeader};
+use gpui::{
+    overlay, px, Action, AnchorCorner, AnyElement, Bounds, Dismiss, DispatchPhase, Div,
+    FocusHandle, LayoutId, ManagedView, MouseButton, MouseDownEvent, Pixels, Point, Render, View,
+};
 
-pub enum ContextMenuItem {
-    Header(SharedString),
-    Entry(Label),
-    Separator,
+pub struct ContextMenu {
+    items: Vec<ListItem>,
+    focus_handle: FocusHandle,
 }
 
-impl ContextMenuItem {
-    fn to_list_item<V: 'static>(self) -> ListItem {
-        match self {
-            ContextMenuItem::Header(label) => ListSubHeader::new(label).into(),
-            ContextMenuItem::Entry(label) => {
-                ListEntry::new(label).variant(ListItemVariant::Inset).into()
-            }
-            ContextMenuItem::Separator => ListSeparator::new().into(),
+impl ManagedView for ContextMenu {
+    fn focus_handle(&self, cx: &gpui::AppContext) -> FocusHandle {
+        self.focus_handle.clone()
+    }
+}
+
+impl ContextMenu {
+    pub fn new(cx: &mut WindowContext) -> Self {
+        Self {
+            items: Default::default(),
+            focus_handle: cx.focus_handle(),
         }
     }
 
-    pub fn header(label: impl Into<SharedString>) -> Self {
-        Self::Header(label.into())
+    pub fn header(mut self, title: impl Into<SharedString>) -> Self {
+        self.items.push(ListItem::Header(ListSubHeader::new(title)));
+        self
     }
 
-    pub fn separator() -> Self {
-        Self::Separator
+    pub fn separator(mut self) -> Self {
+        self.items.push(ListItem::Separator(ListSeparator));
+        self
     }
 
-    pub fn entry(label: Label) -> Self {
-        Self::Entry(label)
+    pub fn entry(mut self, label: Label, action: Box<dyn Action>) -> Self {
+        self.items.push(ListEntry::new(label).action(action).into());
+        self
+    }
+
+    pub fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
+        // todo!()
+        cx.emit(Dismiss);
+    }
+
+    pub fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
+        cx.emit(Dismiss);
     }
 }
 
-#[derive(Component)]
-pub struct ContextMenu {
-    items: Vec<ContextMenuItem>,
+impl Render for ContextMenu {
+    type Element = Div<Self>;
+    // todo!()
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+        div().elevation_2(cx).flex().flex_row().child(
+            v_stack()
+                .min_w(px(200.))
+                .track_focus(&self.focus_handle)
+                .on_mouse_down_out(|this: &mut Self, _, cx| this.cancel(&Default::default(), cx))
+                // .on_action(ContextMenu::select_first)
+                // .on_action(ContextMenu::select_last)
+                // .on_action(ContextMenu::select_next)
+                // .on_action(ContextMenu::select_prev)
+                .on_action(ContextMenu::confirm)
+                .on_action(ContextMenu::cancel)
+                .flex_none()
+                // .bg(cx.theme().colors().elevated_surface_background)
+                // .border()
+                // .border_color(cx.theme().colors().border)
+                .child(List::new(self.items.clone())),
+        )
+    }
 }
 
-impl ContextMenu {
-    pub fn new(items: impl IntoIterator<Item = ContextMenuItem>) -> Self {
-        Self {
-            items: items.into_iter().collect(),
+pub struct MenuHandle<V: 'static, M: ManagedView> {
+    id: Option<ElementId>,
+    child_builder: Option<Box<dyn FnOnce(bool) -> AnyElement<V> + 'static>>,
+    menu_builder: Option<Rc<dyn Fn(&mut V, &mut ViewContext<V>) -> View<M> + 'static>>,
+
+    anchor: Option<AnchorCorner>,
+    attach: Option<AnchorCorner>,
+}
+
+impl<V: 'static, M: ManagedView> MenuHandle<V, M> {
+    pub fn id(mut self, id: impl Into<ElementId>) -> Self {
+        self.id = Some(id.into());
+        self
+    }
+
+    pub fn menu(mut self, f: impl Fn(&mut V, &mut ViewContext<V>) -> View<M> + 'static) -> Self {
+        self.menu_builder = Some(Rc::new(f));
+        self
+    }
+
+    pub fn child<R: Component<V>>(mut self, f: impl FnOnce(bool) -> R + 'static) -> Self {
+        self.child_builder = Some(Box::new(|b| f(b).render()));
+        self
+    }
+
+    /// anchor defines which corner of the menu to anchor to the attachment point
+    /// (by default the cursor position, but see attach)
+    pub fn anchor(mut self, anchor: AnchorCorner) -> Self {
+        self.anchor = Some(anchor);
+        self
+    }
+
+    /// attach defines which corner of the handle to attach the menu's anchor to
+    pub fn attach(mut self, attach: AnchorCorner) -> Self {
+        self.attach = Some(attach);
+        self
+    }
+}
+
+pub fn menu_handle<V: 'static, M: ManagedView>() -> MenuHandle<V, M> {
+    MenuHandle {
+        id: None,
+        child_builder: None,
+        menu_builder: None,
+        anchor: None,
+        attach: None,
+    }
+}
+
+pub struct MenuHandleState<V, M> {
+    menu: Rc<RefCell<Option<View<M>>>>,
+    position: Rc<RefCell<Point<Pixels>>>,
+    child_layout_id: Option<LayoutId>,
+    child_element: Option<AnyElement<V>>,
+    menu_element: Option<AnyElement<V>>,
+}
+impl<V: 'static, M: ManagedView> Element<V> for MenuHandle<V, M> {
+    type ElementState = MenuHandleState<V, M>;
+
+    fn element_id(&self) -> Option<gpui::ElementId> {
+        Some(self.id.clone().expect("menu_handle must have an id()"))
+    }
+
+    fn layout(
+        &mut self,
+        view_state: &mut V,
+        element_state: Option<Self::ElementState>,
+        cx: &mut crate::ViewContext<V>,
+    ) -> (gpui::LayoutId, Self::ElementState) {
+        let (menu, position) = if let Some(element_state) = element_state {
+            (element_state.menu, element_state.position)
+        } else {
+            (Rc::default(), Rc::default())
+        };
+
+        let mut menu_layout_id = None;
+
+        let menu_element = menu.borrow_mut().as_mut().map(|menu| {
+            let mut overlay = overlay::<V>().snap_to_window();
+            if let Some(anchor) = self.anchor {
+                overlay = overlay.anchor(anchor);
+            }
+            overlay = overlay.position(*position.borrow());
+
+            let mut view = overlay.child(menu.clone()).render();
+            menu_layout_id = Some(view.layout(view_state, cx));
+            view
+        });
+
+        let mut child_element = self
+            .child_builder
+            .take()
+            .map(|child_builder| (child_builder)(menu.borrow().is_some()));
+
+        let child_layout_id = child_element
+            .as_mut()
+            .map(|child_element| child_element.layout(view_state, cx));
+
+        let layout_id = cx.request_layout(
+            &gpui::Style::default(),
+            menu_layout_id.into_iter().chain(child_layout_id),
+        );
+
+        (
+            layout_id,
+            MenuHandleState {
+                menu,
+                position,
+                child_element,
+                child_layout_id,
+                menu_element,
+            },
+        )
+    }
+
+    fn paint(
+        &mut self,
+        bounds: Bounds<gpui::Pixels>,
+        view_state: &mut V,
+        element_state: &mut Self::ElementState,
+        cx: &mut crate::ViewContext<V>,
+    ) {
+        if let Some(child) = element_state.child_element.as_mut() {
+            child.paint(view_state, cx);
         }
+
+        if let Some(menu) = element_state.menu_element.as_mut() {
+            menu.paint(view_state, cx);
+            return;
+        }
+
+        let Some(builder) = self.menu_builder.clone() else {
+            return;
+        };
+        let menu = element_state.menu.clone();
+        let position = element_state.position.clone();
+        let attach = self.attach.clone();
+        let child_layout_id = element_state.child_layout_id.clone();
+
+        cx.on_mouse_event(move |view_state, event: &MouseDownEvent, phase, cx| {
+            if phase == DispatchPhase::Bubble
+                && event.button == MouseButton::Right
+                && bounds.contains_point(&event.position)
+            {
+                cx.stop_propagation();
+                cx.prevent_default();
+
+                let new_menu = (builder)(view_state, cx);
+                let menu2 = menu.clone();
+                cx.subscribe(&new_menu, move |this, modal, e, cx| match e {
+                    &Dismiss => {
+                        *menu2.borrow_mut() = None;
+                        cx.notify();
+                    }
+                })
+                .detach();
+                *menu.borrow_mut() = Some(new_menu);
+
+                *position.borrow_mut() = if attach.is_some() && child_layout_id.is_some() {
+                    attach
+                        .unwrap()
+                        .corner(cx.layout_bounds(child_layout_id.unwrap()))
+                } else {
+                    cx.mouse_position()
+                };
+                cx.notify();
+            }
+        });
     }
+}
 
-    fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
-        v_stack()
-            .flex()
-            .bg(cx.theme().colors().elevated_surface_background)
-            .border()
-            .border_color(cx.theme().colors().border)
-            .child(List::new(
-                self.items
-                    .into_iter()
-                    .map(ContextMenuItem::to_list_item::<V>)
-                    .collect(),
-            ))
+impl<V: 'static, M: ManagedView> Component<V> for MenuHandle<V, M> {
+    fn render(self) -> AnyElement<V> {
+        AnyElement::new(self)
     }
 }
 
@@ -65,7 +260,18 @@ pub use stories::*;
 mod stories {
     use super::*;
     use crate::story::Story;
-    use gpui::{Div, Render};
+    use gpui::{actions, Div, Render, VisualContext};
+
+    actions!(PrintCurrentDate);
+
+    fn build_menu(cx: &mut WindowContext, header: impl Into<SharedString>) -> View<ContextMenu> {
+        cx.build_view(|cx| {
+            ContextMenu::new(cx).header(header).separator().entry(
+                Label::new("Print current time"),
+                PrintCurrentDate.boxed_clone(),
+            )
+        })
+    }
 
     pub struct ContextMenuStory;
 
@@ -74,13 +280,83 @@ mod stories {
 
         fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
             Story::container(cx)
-                .child(Story::title_for::<_, ContextMenu>(cx))
-                .child(Story::label(cx, "Default"))
-                .child(ContextMenu::new([
-                    ContextMenuItem::header("Section header"),
-                    ContextMenuItem::Separator,
-                    ContextMenuItem::entry(Label::new("Some entry")),
-                ]))
+                .on_action(|_, _: &PrintCurrentDate, _| {
+                    if let Ok(unix_time) = std::time::UNIX_EPOCH.elapsed() {
+                        println!("Current Unix time is {:?}", unix_time.as_secs());
+                    }
+                })
+                .flex()
+                .flex_row()
+                .justify_between()
+                .child(
+                    div()
+                        .flex()
+                        .flex_col()
+                        .justify_between()
+                        .child(
+                            menu_handle()
+                                .id("test2")
+                                .child(|is_open| {
+                                    Label::new(if is_open {
+                                        "TOP LEFT"
+                                    } else {
+                                        "RIGHT CLICK ME"
+                                    })
+                                    .render()
+                                })
+                                .menu(move |_, cx| build_menu(cx, "top left")),
+                        )
+                        .child(
+                            menu_handle()
+                                .id("test1")
+                                .child(|is_open| {
+                                    Label::new(if is_open {
+                                        "BOTTOM LEFT"
+                                    } else {
+                                        "RIGHT CLICK ME"
+                                    })
+                                    .render()
+                                })
+                                .anchor(AnchorCorner::BottomLeft)
+                                .attach(AnchorCorner::TopLeft)
+                                .menu(move |_, cx| build_menu(cx, "bottom left")),
+                        ),
+                )
+                .child(
+                    div()
+                        .flex()
+                        .flex_col()
+                        .justify_between()
+                        .child(
+                            menu_handle()
+                                .id("test3")
+                                .child(|is_open| {
+                                    Label::new(if is_open {
+                                        "TOP RIGHT"
+                                    } else {
+                                        "RIGHT CLICK ME"
+                                    })
+                                    .render()
+                                })
+                                .anchor(AnchorCorner::TopRight)
+                                .menu(move |_, cx| build_menu(cx, "top right")),
+                        )
+                        .child(
+                            menu_handle()
+                                .id("test4")
+                                .child(|is_open| {
+                                    Label::new(if is_open {
+                                        "BOTTOM RIGHT"
+                                    } else {
+                                        "RIGHT CLICK ME"
+                                    })
+                                    .render()
+                                })
+                                .anchor(AnchorCorner::BottomRight)
+                                .attach(AnchorCorner::TopRight)
+                                .menu(move |_, cx| build_menu(cx, "bottom right")),
+                        ),
+                )
         }
     }
 }

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

@@ -1,5 +1,5 @@
 use crate::{h_stack, prelude::*, ClickHandler, Icon, IconElement};
-use gpui::{prelude::*, AnyView, MouseButton};
+use gpui::{prelude::*, Action, AnyView, MouseButton};
 use std::sync::Arc;
 
 struct IconButtonHandlers<V: 'static> {
@@ -19,6 +19,7 @@ pub struct IconButton<V: 'static> {
     color: TextColor,
     variant: ButtonVariant,
     state: InteractionState,
+    selected: bool,
     tooltip: Option<Box<dyn Fn(&mut V, &mut ViewContext<V>) -> AnyView + 'static>>,
     handlers: IconButtonHandlers<V>,
 }
@@ -31,6 +32,7 @@ impl<V: 'static> IconButton<V> {
             color: TextColor::default(),
             variant: ButtonVariant::default(),
             state: InteractionState::default(),
+            selected: false,
             tooltip: None,
             handlers: IconButtonHandlers::default(),
         }
@@ -56,6 +58,11 @@ impl<V: 'static> IconButton<V> {
         self
     }
 
+    pub fn selected(mut self, selected: bool) -> Self {
+        self.selected = selected;
+        self
+    }
+
     pub fn tooltip(
         mut self,
         tooltip: impl Fn(&mut V, &mut ViewContext<V>) -> AnyView + 'static,
@@ -69,14 +76,18 @@ impl<V: 'static> IconButton<V> {
         self
     }
 
+    pub fn action(self, action: Box<dyn Action>) -> Self {
+        self.on_click(move |this, cx| cx.dispatch_action(action.boxed_clone()))
+    }
+
     fn render(mut self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
         let icon_color = match (self.state, self.color) {
             (InteractionState::Disabled, _) => TextColor::Disabled,
-            (InteractionState::Active, _) => TextColor::Error,
+            (InteractionState::Active, _) => TextColor::Selected,
             _ => self.color,
         };
 
-        let (bg_color, bg_hover_color, bg_active_color) = match self.variant {
+        let (mut bg_color, bg_hover_color, bg_active_color) = match self.variant {
             ButtonVariant::Filled => (
                 cx.theme().colors().element_background,
                 cx.theme().colors().element_hover,
@@ -89,27 +100,32 @@ impl<V: 'static> IconButton<V> {
             ),
         };
 
+        if self.selected {
+            bg_color = bg_hover_color;
+        }
+
         let mut button = h_stack()
             .id(self.id.clone())
             .justify_center()
             .rounded_md()
             .p_1()
             .bg(bg_color)
+            .cursor_pointer()
             .hover(|style| style.bg(bg_hover_color))
             .active(|style| style.bg(bg_active_color))
             .child(IconElement::new(self.icon).color(icon_color));
 
         if let Some(click_handler) = self.handlers.click.clone() {
-            button = button
-                .on_mouse_down(MouseButton::Left, move |state, event, cx| {
-                    cx.stop_propagation();
-                    click_handler(state, cx);
-                })
-                .cursor_pointer();
+            button = button.on_mouse_down(MouseButton::Left, move |state, event, cx| {
+                cx.stop_propagation();
+                click_handler(state, cx);
+            })
         }
 
         if let Some(tooltip) = self.tooltip.take() {
-            button = button.tooltip(move |view: &mut V, cx| (tooltip)(view, cx))
+            if !self.selected {
+                button = button.tooltip(move |view: &mut V, cx| (tooltip)(view, cx))
+            }
         }
 
         button

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

@@ -81,13 +81,12 @@ pub use stories::*;
 mod stories {
     use super::*;
     use crate::Story;
-    use gpui::{action, Div, Render};
+    use gpui::{actions, Div, Render};
     use itertools::Itertools;
 
     pub struct KeybindingStory;
 
-    #[action]
-    struct NoAction {}
+    actions!(NoAction);
 
     pub fn binding(key: &str) -> gpui::KeyBinding {
         gpui::KeyBinding::new(key, NoAction {}, None)

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

@@ -60,7 +60,7 @@ pub enum LineHeightStyle {
     UILabel,
 }
 
-#[derive(Component)]
+#[derive(Clone, Component)]
 pub struct Label {
     label: SharedString,
     size: LabelSize,

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

@@ -1,11 +1,10 @@
-use gpui::div;
+use gpui::{div, Action};
 
-use crate::prelude::*;
 use crate::settings::user_settings;
 use crate::{
-    disclosure_control, h_stack, v_stack, Avatar, GraphicSlot, Icon, IconElement, IconSize, Label,
-    TextColor, Toggle,
+    disclosure_control, h_stack, v_stack, Avatar, Icon, IconElement, IconSize, Label, Toggle,
 };
+use crate::{prelude::*, GraphicSlot};
 
 #[derive(Clone, Copy, Default, Debug, PartialEq)]
 pub enum ListItemVariant {
@@ -118,7 +117,7 @@ impl ListHeader {
     }
 }
 
-#[derive(Component)]
+#[derive(Component, Clone)]
 pub struct ListSubHeader {
     label: SharedString,
     left_icon: Option<Icon>,
@@ -173,7 +172,7 @@ pub enum ListEntrySize {
     Medium,
 }
 
-#[derive(Component)]
+#[derive(Component, Clone)]
 pub enum ListItem {
     Entry(ListEntry),
     Separator(ListSeparator),
@@ -232,6 +231,25 @@ pub struct ListEntry {
     size: ListEntrySize,
     toggle: Toggle,
     variant: ListItemVariant,
+    on_click: Option<Box<dyn Action>>,
+}
+
+impl Clone for ListEntry {
+    fn clone(&self) -> Self {
+        Self {
+            disabled: self.disabled,
+            // TODO: Reintroduce this
+            // disclosure_control_style: DisclosureControlVisibility,
+            indent_level: self.indent_level,
+            label: self.label.clone(),
+            left_slot: self.left_slot.clone(),
+            overflow: self.overflow,
+            size: self.size,
+            toggle: self.toggle,
+            variant: self.variant,
+            on_click: self.on_click.as_ref().map(|opt| opt.boxed_clone()),
+        }
+    }
 }
 
 impl ListEntry {
@@ -245,9 +263,15 @@ impl ListEntry {
             size: ListEntrySize::default(),
             toggle: Toggle::NotToggleable,
             variant: ListItemVariant::default(),
+            on_click: Default::default(),
         }
     }
 
+    pub fn action(mut self, action: impl Into<Box<dyn Action>>) -> Self {
+        self.on_click = Some(action.into());
+        self
+    }
+
     pub fn variant(mut self, variant: ListItemVariant) -> Self {
         self.variant = variant;
         self
@@ -303,9 +327,21 @@ impl ListEntry {
             ListEntrySize::Small => div().h_6(),
             ListEntrySize::Medium => div().h_7(),
         };
-
         div()
             .relative()
+            .hover(|mut style| {
+                style.background = Some(cx.theme().colors().editor_background.into());
+                style
+            })
+            .on_mouse_down(gpui::MouseButton::Left, {
+                let action = self.on_click.map(|action| action.boxed_clone());
+
+                move |entry: &mut V, event, cx| {
+                    if let Some(action) = action.as_ref() {
+                        cx.dispatch_action(action.boxed_clone());
+                    }
+                }
+            })
             .group("")
             .bg(cx.theme().colors().surface_background)
             // TODO: Add focus state
@@ -401,7 +437,7 @@ impl List {
         v_stack()
             .w_full()
             .py_1()
-            .children(self.header)
+            .children(self.header.map(|header| header))
             .child(list_content)
     }
 }

crates/ui2/src/story.rs 🔗

@@ -12,7 +12,6 @@ impl Story {
             .flex_col()
             .pt_2()
             .px_4()
-            .font("Zed Mono")
             .bg(cx.theme().colors().background)
     }
 

crates/ui2/src/styled_ext.rs 🔗

@@ -5,6 +5,7 @@ use crate::{ElevationIndex, UITextSize};
 
 fn elevated<E: Styled, V: 'static>(this: E, cx: &mut ViewContext<V>, index: ElevationIndex) -> E {
     this.bg(cx.theme().colors().elevated_surface_background)
+        .z_index(index.z_index())
         .rounded_lg()
         .border()
         .border_color(cx.theme().colors().border_variant)

crates/workspace2/src/dock.rs 🔗

@@ -1,13 +1,14 @@
 use crate::{status_bar::StatusItemView, Axis, Workspace};
 use gpui::{
-    div, px, Action, AnyView, AppContext, Component, Div, Entity, EntityId, EventEmitter,
-    FocusHandle, FocusableView, ParentComponent, Render, Styled, Subscription, View, ViewContext,
-    WeakView, WindowContext,
+    div, px, Action, AnchorCorner, AnyView, AppContext, Component, Div, Entity, EntityId,
+    EventEmitter, FocusHandle, FocusableView, ParentComponent, Render, SharedString, Styled,
+    Subscription, View, ViewContext, VisualContext, WeakView, WindowContext,
 };
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use std::sync::Arc;
-use ui::{h_stack, IconButton, InteractionState, Tooltip};
+use theme2::ActiveTheme;
+use ui::{h_stack, menu_handle, ContextMenu, IconButton, InteractionState, Tooltip};
 
 pub enum PanelEvent {
     ChangePosition,
@@ -216,11 +217,11 @@ impl Dock {
     //             .map_or(false, |panel| panel.has_focus(cx))
     //     }
 
-    //     pub fn panel<T: Panel>(&self) -> Option<View<T>> {
-    //         self.panel_entries
-    //             .iter()
-    //             .find_map(|entry| entry.panel.as_any().clone().downcast())
-    //     }
+    pub fn panel<T: Panel>(&self) -> Option<View<T>> {
+        self.panel_entries
+            .iter()
+            .find_map(|entry| entry.panel.to_any().clone().downcast().ok())
+    }
 
     pub fn panel_index_for_type<T: Panel>(&self) -> Option<usize> {
         self.panel_entries
@@ -416,23 +417,13 @@ impl Dock {
         }
     }
 
-    //     pub fn render_placeholder(&self, cx: &WindowContext) -> AnyElement<Workspace> {
-    //         todo!()
-    // if let Some(active_entry) = self.visible_entry() {
-    //     Empty::new()
-    //         .into_any()
-    //         .contained()
-    //         .with_style(self.style(cx))
-    //         .resizable::<WorkspaceBounds>(
-    //             self.position.to_resize_handle_side(),
-    //             active_entry.panel.size(cx),
-    //             |_, _, _| {},
-    //         )
-    //         .into_any()
-    // } else {
-    //     Empty::new().into_any()
-    // }
-    //     }
+    pub fn toggle_action(&self) -> Box<dyn Action> {
+        match self.position {
+            DockPosition::Left => crate::ToggleLeftDock.boxed_clone(),
+            DockPosition::Bottom => crate::ToggleBottomDock.boxed_clone(),
+            DockPosition::Right => crate::ToggleRightDock.boxed_clone(),
+        }
+    }
 }
 
 impl Render for Dock {
@@ -443,10 +434,16 @@ impl Render for Dock {
             let size = entry.panel.size(cx);
 
             div()
+                .border_color(cx.theme().colors().border)
                 .map(|this| match self.position().axis() {
                     Axis::Horizontal => this.w(px(size)).h_full(),
                     Axis::Vertical => this.h(px(size)).w_full(),
                 })
+                .map(|this| match self.position() {
+                    DockPosition::Left => this.border_r(),
+                    DockPosition::Right => this.border_l(),
+                    DockPosition::Bottom => this.border_t(),
+                })
                 .child(entry.panel.to_any())
         } else {
             div()
@@ -454,40 +451,6 @@ impl Render for Dock {
     }
 }
 
-// todo!()
-// impl View for Dock {
-//     fn ui_name() -> &'static str {
-//         "Dock"
-//     }
-
-//     fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
-//         if let Some(active_entry) = self.visible_entry() {
-//             let style = self.style(cx);
-//             ChildView::new(active_entry.panel.as_any(), cx)
-//                 .contained()
-//                 .with_style(style)
-//                 .resizable::<WorkspaceBounds>(
-//                     self.position.to_resize_handle_side(),
-//                     active_entry.panel.size(cx),
-//                     |dock: &mut Self, size, cx| dock.resize_active_panel(size, cx),
-//                 )
-//                 .into_any()
-//         } else {
-//             Empty::new().into_any()
-//         }
-//     }
-
-//     fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
-//         if cx.is_self_focused() {
-//             if let Some(active_entry) = self.visible_entry() {
-//                 cx.focus(active_entry.panel.as_any());
-//             } else {
-//                 cx.focus_parent();
-//             }
-//         }
-//     }
-// }
-
 impl PanelButtons {
     pub fn new(
         dock: View<Dock>,
@@ -648,6 +611,7 @@ impl PanelButtons {
 //     }
 // }
 
+// here be kittens
 impl Render for PanelButtons {
     type Element = Div<Self>;
 
@@ -657,6 +621,13 @@ impl Render for PanelButtons {
         let active_index = dock.active_panel_index;
         let is_open = dock.is_open;
 
+        let (menu_anchor, menu_attach) = match dock.position {
+            DockPosition::Left => (AnchorCorner::BottomLeft, AnchorCorner::TopLeft),
+            DockPosition::Bottom | DockPosition::Right => {
+                (AnchorCorner::BottomRight, AnchorCorner::TopRight)
+            }
+        };
+
         let buttons = dock
             .panel_entries
             .iter()
@@ -664,18 +635,36 @@ impl Render for PanelButtons {
             .filter_map(|(i, panel)| {
                 let icon = panel.panel.icon(cx)?;
                 let name = panel.panel.persistent_name();
-                let action = panel.panel.toggle_action(cx);
-                let action2 = action.boxed_clone();
-
-                let mut button = IconButton::new(panel.panel.persistent_name(), icon)
-                    .when(i == active_index, |el| el.state(InteractionState::Active))
-                    .on_click(move |this, cx| cx.dispatch_action(action.boxed_clone()))
-                    .tooltip(move |_, cx| Tooltip::for_action(name, &*action2, cx));
 
-                Some(button)
+                let mut button: IconButton<Self> = if i == active_index && is_open {
+                    let action = dock.toggle_action();
+                    let tooltip: SharedString =
+                        format!("Close {} dock", dock.position.to_label()).into();
+                    IconButton::new(name, icon)
+                        .state(InteractionState::Active)
+                        .action(action.boxed_clone())
+                        .tooltip(move |_, cx| Tooltip::for_action(tooltip.clone(), &*action, cx))
+                } else {
+                    let action = panel.panel.toggle_action(cx);
+
+                    IconButton::new(name, icon)
+                        .action(action.boxed_clone())
+                        .tooltip(move |_, cx| Tooltip::for_action(name, &*action, cx))
+                };
+
+                Some(
+                    menu_handle()
+                        .id(name)
+                        .menu(move |_, cx| {
+                            cx.build_view(|cx| ContextMenu::new(cx).header("SECTION"))
+                        })
+                        .anchor(menu_anchor)
+                        .attach(menu_attach)
+                        .child(|is_open| button.selected(is_open)),
+                )
             });
 
-        h_stack().children(buttons)
+        h_stack().gap_0p5().children(buttons)
     }
 }
 

crates/workspace2/src/item.rs 🔗

@@ -240,7 +240,7 @@ pub trait ItemHandle: 'static + Send {
     fn deactivated(&self, cx: &mut WindowContext);
     fn workspace_deactivated(&self, cx: &mut WindowContext);
     fn navigate(&self, data: Box<dyn Any>, cx: &mut WindowContext) -> bool;
-    fn id(&self) -> EntityId;
+    fn item_id(&self) -> EntityId;
     fn to_any(&self) -> AnyView;
     fn is_dirty(&self, cx: &AppContext) -> bool;
     fn has_conflict(&self, cx: &AppContext) -> bool;
@@ -399,7 +399,7 @@ impl<T: Item> ItemHandle for View<T> {
 
         if workspace
             .panes_by_item
-            .insert(self.id(), pane.downgrade())
+            .insert(self.item_id(), pane.downgrade())
             .is_none()
         {
             let mut pending_autosave = DelayedDebouncedEditAction::new();
@@ -410,7 +410,7 @@ impl<T: Item> ItemHandle for View<T> {
                 Some(cx.subscribe(self, move |workspace, item, event, cx| {
                     let pane = if let Some(pane) = workspace
                         .panes_by_item
-                        .get(&item.id())
+                        .get(&item.item_id())
                         .and_then(|pane| pane.upgrade())
                     {
                         pane
@@ -463,7 +463,7 @@ impl<T: Item> ItemHandle for View<T> {
                     match event {
                         ItemEvent::CloseItem => {
                             pane.update(cx, |pane, cx| {
-                                pane.close_item_by_id(item.id(), crate::SaveIntent::Close, cx)
+                                pane.close_item_by_id(item.item_id(), crate::SaveIntent::Close, cx)
                             })
                             .detach_and_log_err(cx);
                             return;
@@ -502,7 +502,7 @@ impl<T: Item> ItemHandle for View<T> {
             // })
             // .detach();
 
-            let item_id = self.id();
+            let item_id = self.item_id();
             cx.observe_release(self, move |workspace, _, _| {
                 workspace.panes_by_item.remove(&item_id);
                 event_subscription.take();
@@ -527,7 +527,7 @@ impl<T: Item> ItemHandle for View<T> {
         self.update(cx, |this, cx| this.navigate(data, cx))
     }
 
-    fn id(&self) -> EntityId {
+    fn item_id(&self) -> EntityId {
         self.entity_id()
     }
 
@@ -712,7 +712,7 @@ impl<T: FollowableItem> FollowableItemHandle for View<T> {
         self.read(cx).remote_id().or_else(|| {
             client.peer_id().map(|creator| ViewId {
                 creator,
-                id: self.id().as_u64(),
+                id: self.item_id().as_u64(),
             })
         })
     }

crates/workspace2/src/modal_layer.rs 🔗

@@ -1,6 +1,6 @@
 use gpui::{
-    div, prelude::*, px, AnyView, Div, EventEmitter, FocusHandle, Render, Subscription, View,
-    ViewContext, WindowContext,
+    div, prelude::*, px, AnyView, Div, FocusHandle, ManagedView, Render, Subscription, View,
+    ViewContext,
 };
 use ui::{h_stack, v_stack};
 
@@ -15,14 +15,6 @@ pub struct ModalLayer {
     active_modal: Option<ActiveModal>,
 }
 
-pub trait Modal: Render + EventEmitter<ModalEvent> {
-    fn focus(&self, cx: &mut WindowContext);
-}
-
-pub enum ModalEvent {
-    Dismissed,
-}
-
 impl ModalLayer {
     pub fn new() -> Self {
         Self { active_modal: None }
@@ -30,7 +22,7 @@ impl ModalLayer {
 
     pub fn toggle_modal<V, B>(&mut self, cx: &mut ViewContext<Self>, build_view: B)
     where
-        V: Modal,
+        V: ManagedView,
         B: FnOnce(&mut ViewContext<V>) -> V,
     {
         if let Some(active_modal) = &self.active_modal {
@@ -46,17 +38,15 @@ impl ModalLayer {
 
     pub fn show_modal<V>(&mut self, new_modal: View<V>, cx: &mut ViewContext<Self>)
     where
-        V: Modal,
+        V: ManagedView,
     {
         self.active_modal = Some(ActiveModal {
             modal: new_modal.clone().into(),
-            subscription: cx.subscribe(&new_modal, |this, modal, e, cx| match e {
-                ModalEvent::Dismissed => this.hide_modal(cx),
-            }),
+            subscription: cx.subscribe(&new_modal, |this, modal, e, cx| this.hide_modal(cx)),
             previous_focus_handle: cx.focused(),
             focus_handle: cx.focus_handle(),
         });
-        new_modal.update(cx, |modal, cx| modal.focus(cx));
+        cx.focus_view(&new_modal);
         cx.notify();
     }
 

crates/workspace2/src/pane.rs 🔗

@@ -7,9 +7,9 @@ use crate::{
 use anyhow::Result;
 use collections::{HashMap, HashSet, VecDeque};
 use gpui::{
-    actions, prelude::*, register_action, AppContext, AsyncWindowContext, Component, Div, EntityId,
-    EventEmitter, FocusHandle, Focusable, FocusableView, Model, PromptLevel, Render, Task, View,
-    ViewContext, VisualContext, WeakView, WindowContext,
+    actions, prelude::*, Action, AppContext, AsyncWindowContext, Component, Div, EntityId,
+    EventEmitter, FocusHandle, Focusable, FocusableView, Model, Pixels, Point, PromptLevel, Render,
+    Task, View, ViewContext, VisualContext, WeakView, WindowContext,
 };
 use parking_lot::Mutex;
 use project2::{Project, ProjectEntryId, ProjectPath};
@@ -70,15 +70,13 @@ pub struct ActivateItem(pub usize);
 //     pub pane: WeakView<Pane>,
 // }
 
-#[register_action]
-#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
+#[derive(Clone, PartialEq, Debug, Deserialize, Default, Action)]
 #[serde(rename_all = "camelCase")]
 pub struct CloseActiveItem {
     pub save_intent: Option<SaveIntent>,
 }
 
-#[register_action]
-#[derive(Clone, PartialEq, Debug, Deserialize, Default)]
+#[derive(Clone, PartialEq, Debug, Deserialize, Default, Action)]
 #[serde(rename_all = "camelCase")]
 pub struct CloseAllItems {
     pub save_intent: Option<SaveIntent>,
@@ -104,29 +102,6 @@ actions!(
 
 const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
 
-pub fn init(cx: &mut AppContext) {
-    // todo!()
-    //     cx.add_action(Pane::toggle_zoom);
-    //     cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| {
-    //         pane.activate_item(action.0, true, true, cx);
-    //     });
-    //     cx.add_action(|pane: &mut Pane, _: &ActivateLastItem, cx| {
-    //         pane.activate_item(pane.items.len() - 1, true, true, cx);
-    //     });
-    //     cx.add_action(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
-    //         pane.activate_prev_item(true, cx);
-    //     });
-    //     cx.add_action(|pane: &mut Pane, _: &ActivateNextItem, cx| {
-    //         pane.activate_next_item(true, cx);
-    //     });
-    //     cx.add_async_action(Pane::close_active_item);
-    //     cx.add_async_action(Pane::close_inactive_items);
-    //     cx.add_async_action(Pane::close_clean_items);
-    //     cx.add_async_action(Pane::close_items_to_the_left);
-    //     cx.add_async_action(Pane::close_items_to_the_right);
-    //     cx.add_async_action(Pane::close_all_items);
-}
-
 pub enum Event {
     AddItem { item: Box<dyn ItemHandle> },
     ActivateItem { local: bool },
@@ -142,7 +117,10 @@ pub enum Event {
 impl fmt::Debug for Event {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         match self {
-            Event::AddItem { item } => f.debug_struct("AddItem").field("item", &item.id()).finish(),
+            Event::AddItem { item } => f
+                .debug_struct("AddItem")
+                .field("item", &item.item_id())
+                .finish(),
             Event::ActivateItem { local } => f
                 .debug_struct("ActivateItem")
                 .field("local", local)
@@ -526,7 +504,7 @@ impl Pane {
                         .0
                         .lock()
                         .paths_by_item
-                        .insert(item.id(), (project_path, abs_path));
+                        .insert(item.item_id(), (project_path, abs_path));
                 }
             }
         }
@@ -550,7 +528,7 @@ impl Pane {
         };
 
         let existing_item_index = self.items.iter().position(|existing_item| {
-            if existing_item.id() == item.id() {
+            if existing_item.item_id() == item.item_id() {
                 true
             } else if existing_item.is_singleton(cx) {
                 existing_item
@@ -615,21 +593,21 @@ impl Pane {
         self.items.iter()
     }
 
-    //     pub fn items_of_type<T: View>(&self) -> impl '_ + Iterator<Item = ViewHandle<T>> {
-    //         self.items
-    //             .iter()
-    //             .filter_map(|item| item.as_any().clone().downcast())
-    //     }
+    pub fn items_of_type<T: Render>(&self) -> impl '_ + Iterator<Item = View<T>> {
+        self.items
+            .iter()
+            .filter_map(|item| item.to_any().downcast().ok())
+    }
 
     pub fn active_item(&self) -> Option<Box<dyn ItemHandle>> {
         self.items.get(self.active_item_index).cloned()
     }
 
-    //     pub fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Vector2F> {
-    //         self.items
-    //             .get(self.active_item_index)?
-    //             .pixel_position_of_cursor(cx)
-    //     }
+    pub fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Point<Pixels>> {
+        self.items
+            .get(self.active_item_index)?
+            .pixel_position_of_cursor(cx)
+    }
 
     pub fn item_for_entry(
         &self,
@@ -646,24 +624,26 @@ impl Pane {
     }
 
     pub fn index_for_item(&self, item: &dyn ItemHandle) -> Option<usize> {
-        self.items.iter().position(|i| i.id() == item.id())
+        self.items
+            .iter()
+            .position(|i| i.item_id() == item.item_id())
     }
 
-    //     pub fn toggle_zoom(&mut self, _: &ToggleZoom, cx: &mut ViewContext<Self>) {
-    //         // Potentially warn the user of the new keybinding
-    //         let workspace_handle = self.workspace().clone();
-    //         cx.spawn(|_, mut cx| async move { notify_of_new_dock(&workspace_handle, &mut cx) })
-    //             .detach();
-
-    //         if self.zoomed {
-    //             cx.emit(Event::ZoomOut);
-    //         } else if !self.items.is_empty() {
-    //             if !self.has_focus {
-    //                 cx.focus_self();
-    //             }
-    //             cx.emit(Event::ZoomIn);
+    // pub fn toggle_zoom(&mut self, _: &ToggleZoom, cx: &mut ViewContext<Self>) {
+    //     // Potentially warn the user of the new keybinding
+    //     let workspace_handle = self.workspace().clone();
+    //     cx.spawn(|_, mut cx| async move { notify_of_new_dock(&workspace_handle, &mut cx) })
+    //         .detach();
+
+    //     if self.zoomed {
+    //         cx.emit(Event::ZoomOut);
+    //     } else if !self.items.is_empty() {
+    //         if !self.has_focus {
+    //             cx.focus_self();
     //         }
+    //         cx.emit(Event::ZoomIn);
     //     }
+    // }
 
     pub fn activate_item(
         &mut self,
@@ -691,9 +671,9 @@ impl Pane {
             if let Some(newly_active_item) = self.items.get(index) {
                 self.activation_history
                     .retain(|&previously_active_item_id| {
-                        previously_active_item_id != newly_active_item.id()
+                        previously_active_item_id != newly_active_item.item_id()
                     });
-                self.activation_history.push(newly_active_item.id());
+                self.activation_history.push(newly_active_item.item_id());
             }
 
             self.update_toolbar(cx);
@@ -707,25 +687,25 @@ impl Pane {
         }
     }
 
-    //     pub fn activate_prev_item(&mut self, activate_pane: bool, cx: &mut ViewContext<Self>) {
-    //         let mut index = self.active_item_index;
-    //         if index > 0 {
-    //             index -= 1;
-    //         } else if !self.items.is_empty() {
-    //             index = self.items.len() - 1;
-    //         }
-    //         self.activate_item(index, activate_pane, activate_pane, cx);
-    //     }
+    pub fn activate_prev_item(&mut self, activate_pane: bool, cx: &mut ViewContext<Self>) {
+        let mut index = self.active_item_index;
+        if index > 0 {
+            index -= 1;
+        } else if !self.items.is_empty() {
+            index = self.items.len() - 1;
+        }
+        self.activate_item(index, activate_pane, activate_pane, cx);
+    }
 
-    //     pub fn activate_next_item(&mut self, activate_pane: bool, cx: &mut ViewContext<Self>) {
-    //         let mut index = self.active_item_index;
-    //         if index + 1 < self.items.len() {
-    //             index += 1;
-    //         } else {
-    //             index = 0;
-    //         }
-    //         self.activate_item(index, activate_pane, activate_pane, cx);
-    //     }
+    pub fn activate_next_item(&mut self, activate_pane: bool, cx: &mut ViewContext<Self>) {
+        let mut index = self.active_item_index;
+        if index + 1 < self.items.len() {
+            index += 1;
+        } else {
+            index = 0;
+        }
+        self.activate_item(index, activate_pane, activate_pane, cx);
+    }
 
     pub fn close_active_item(
         &mut self,
@@ -735,7 +715,7 @@ impl Pane {
         if self.items.is_empty() {
             return None;
         }
-        let active_item_id = self.items[self.active_item_index].id();
+        let active_item_id = self.items[self.active_item_index].item_id();
         Some(self.close_item_by_id(
             active_item_id,
             action.save_intent.unwrap_or(SaveIntent::Close),
@@ -752,106 +732,106 @@ impl Pane {
         self.close_items(cx, save_intent, move |view_id| view_id == item_id_to_close)
     }
 
-    // pub fn close_inactive_items(
-    //     &mut self,
-    //     _: &CloseInactiveItems,
-    //     cx: &mut ViewContext<Self>,
-    // ) -> Option<Task<Result<()>>> {
-    //     if self.items.is_empty() {
-    //         return None;
-    //     }
+    pub fn close_inactive_items(
+        &mut self,
+        _: &CloseInactiveItems,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<Task<Result<()>>> {
+        if self.items.is_empty() {
+            return None;
+        }
 
-    //     let active_item_id = self.items[self.active_item_index].id();
-    //     Some(self.close_items(cx, SaveIntent::Close, move |item_id| {
-    //         item_id != active_item_id
-    //     }))
-    // }
+        let active_item_id = self.items[self.active_item_index].item_id();
+        Some(self.close_items(cx, SaveIntent::Close, move |item_id| {
+            item_id != active_item_id
+        }))
+    }
 
-    // pub fn close_clean_items(
-    //     &mut self,
-    //     _: &CloseCleanItems,
-    //     cx: &mut ViewContext<Self>,
-    // ) -> Option<Task<Result<()>>> {
-    //     let item_ids: Vec<_> = self
-    //         .items()
-    //         .filter(|item| !item.is_dirty(cx))
-    //         .map(|item| item.id())
-    //         .collect();
-    //     Some(self.close_items(cx, SaveIntent::Close, move |item_id| {
-    //         item_ids.contains(&item_id)
-    //     }))
-    // }
+    pub fn close_clean_items(
+        &mut self,
+        _: &CloseCleanItems,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<Task<Result<()>>> {
+        let item_ids: Vec<_> = self
+            .items()
+            .filter(|item| !item.is_dirty(cx))
+            .map(|item| item.item_id())
+            .collect();
+        Some(self.close_items(cx, SaveIntent::Close, move |item_id| {
+            item_ids.contains(&item_id)
+        }))
+    }
 
-    // pub fn close_items_to_the_left(
-    //     &mut self,
-    //     _: &CloseItemsToTheLeft,
-    //     cx: &mut ViewContext<Self>,
-    // ) -> Option<Task<Result<()>>> {
-    //     if self.items.is_empty() {
-    //         return None;
-    //     }
-    //     let active_item_id = self.items[self.active_item_index].id();
-    //     Some(self.close_items_to_the_left_by_id(active_item_id, cx))
-    // }
+    pub fn close_items_to_the_left(
+        &mut self,
+        _: &CloseItemsToTheLeft,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<Task<Result<()>>> {
+        if self.items.is_empty() {
+            return None;
+        }
+        let active_item_id = self.items[self.active_item_index].item_id();
+        Some(self.close_items_to_the_left_by_id(active_item_id, cx))
+    }
 
-    // pub fn close_items_to_the_left_by_id(
-    //     &mut self,
-    //     item_id: usize,
-    //     cx: &mut ViewContext<Self>,
-    // ) -> Task<Result<()>> {
-    //     let item_ids: Vec<_> = self
-    //         .items()
-    //         .take_while(|item| item.id() != item_id)
-    //         .map(|item| item.id())
-    //         .collect();
-    //     self.close_items(cx, SaveIntent::Close, move |item_id| {
-    //         item_ids.contains(&item_id)
-    //     })
-    // }
+    pub fn close_items_to_the_left_by_id(
+        &mut self,
+        item_id: EntityId,
+        cx: &mut ViewContext<Self>,
+    ) -> Task<Result<()>> {
+        let item_ids: Vec<_> = self
+            .items()
+            .take_while(|item| item.item_id() != item_id)
+            .map(|item| item.item_id())
+            .collect();
+        self.close_items(cx, SaveIntent::Close, move |item_id| {
+            item_ids.contains(&item_id)
+        })
+    }
 
-    // pub fn close_items_to_the_right(
-    //     &mut self,
-    //     _: &CloseItemsToTheRight,
-    //     cx: &mut ViewContext<Self>,
-    // ) -> Option<Task<Result<()>>> {
-    //     if self.items.is_empty() {
-    //         return None;
-    //     }
-    //     let active_item_id = self.items[self.active_item_index].id();
-    //     Some(self.close_items_to_the_right_by_id(active_item_id, cx))
-    // }
+    pub fn close_items_to_the_right(
+        &mut self,
+        _: &CloseItemsToTheRight,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<Task<Result<()>>> {
+        if self.items.is_empty() {
+            return None;
+        }
+        let active_item_id = self.items[self.active_item_index].item_id();
+        Some(self.close_items_to_the_right_by_id(active_item_id, cx))
+    }
 
-    // pub fn close_items_to_the_right_by_id(
-    //     &mut self,
-    //     item_id: usize,
-    //     cx: &mut ViewContext<Self>,
-    // ) -> Task<Result<()>> {
-    //     let item_ids: Vec<_> = self
-    //         .items()
-    //         .rev()
-    //         .take_while(|item| item.id() != item_id)
-    //         .map(|item| item.id())
-    //         .collect();
-    //     self.close_items(cx, SaveIntent::Close, move |item_id| {
-    //         item_ids.contains(&item_id)
-    //     })
-    // }
+    pub fn close_items_to_the_right_by_id(
+        &mut self,
+        item_id: EntityId,
+        cx: &mut ViewContext<Self>,
+    ) -> Task<Result<()>> {
+        let item_ids: Vec<_> = self
+            .items()
+            .rev()
+            .take_while(|item| item.item_id() != item_id)
+            .map(|item| item.item_id())
+            .collect();
+        self.close_items(cx, SaveIntent::Close, move |item_id| {
+            item_ids.contains(&item_id)
+        })
+    }
 
-    // pub fn close_all_items(
-    //     &mut self,
-    //     action: &CloseAllItems,
-    //     cx: &mut ViewContext<Self>,
-    // ) -> Option<Task<Result<()>>> {
-    //     if self.items.is_empty() {
-    //         return None;
-    //     }
+    pub fn close_all_items(
+        &mut self,
+        action: &CloseAllItems,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<Task<Result<()>>> {
+        if self.items.is_empty() {
+            return None;
+        }
 
-    //     Some(
-    //         self.close_items(cx, action.save_intent.unwrap_or(SaveIntent::Close), |_| {
-    //             true
-    //         }),
-    //     )
-    // }
+        Some(
+            self.close_items(cx, action.save_intent.unwrap_or(SaveIntent::Close), |_| {
+                true
+            }),
+        )
+    }
 
     pub(super) fn file_names_for_prompt(
         items: &mut dyn Iterator<Item = &Box<dyn ItemHandle>>,
@@ -898,7 +878,7 @@ impl Pane {
         let mut items_to_close = Vec::new();
         let mut dirty_items = Vec::new();
         for item in &self.items {
-            if should_close(item.id()) {
+            if should_close(item.item_id()) {
                 items_to_close.push(item.boxed_clone());
                 if item.is_dirty(cx) {
                     dirty_items.push(item.boxed_clone());
@@ -951,7 +931,7 @@ impl Pane {
                     for item in workspace.items(cx) {
                         if !items_to_close
                             .iter()
-                            .any(|item_to_close| item_to_close.id() == item.id())
+                            .any(|item_to_close| item_to_close.item_id() == item.item_id())
                         {
                             let other_project_item_ids = item.project_item_model_ids(cx);
                             project_item_ids.retain(|id| !other_project_item_ids.contains(id));
@@ -979,7 +959,11 @@ impl Pane {
 
                 // Remove the item from the pane.
                 pane.update(&mut cx, |pane, cx| {
-                    if let Some(item_ix) = pane.items.iter().position(|i| i.id() == item.id()) {
+                    if let Some(item_ix) = pane
+                        .items
+                        .iter()
+                        .position(|i| i.item_id() == item.item_id())
+                    {
                         pane.remove_item(item_ix, false, cx);
                     }
                 })?;
@@ -997,7 +981,7 @@ impl Pane {
         cx: &mut ViewContext<Self>,
     ) {
         self.activation_history
-            .retain(|&history_entry| history_entry != self.items[item_index].id());
+            .retain(|&history_entry| history_entry != self.items[item_index].item_id());
 
         if item_index == self.active_item_index {
             let index_to_activate = self
@@ -1005,7 +989,7 @@ impl Pane {
                 .pop()
                 .and_then(|last_activated_item| {
                     self.items.iter().enumerate().find_map(|(index, item)| {
-                        (item.id() == last_activated_item).then_some(index)
+                        (item.item_id() == last_activated_item).then_some(index)
                     })
                 })
                 // We didn't have a valid activation history entry, so fallback
@@ -1022,7 +1006,9 @@ impl Pane {
 
         let item = self.items.remove(item_index);
 
-        cx.emit(Event::RemoveItem { item_id: item.id() });
+        cx.emit(Event::RemoveItem {
+            item_id: item.item_id(),
+        });
         if self.items.is_empty() {
             item.deactivated(cx);
             self.update_toolbar(cx);
@@ -1043,16 +1029,20 @@ impl Pane {
                 .0
                 .lock()
                 .paths_by_item
-                .get(&item.id())
+                .get(&item.item_id())
                 .and_then(|(_, abs_path)| abs_path.clone());
 
             self.nav_history
                 .0
                 .lock()
                 .paths_by_item
-                .insert(item.id(), (path, abs_path));
+                .insert(item.item_id(), (path, abs_path));
         } else {
-            self.nav_history.0.lock().paths_by_item.remove(&item.id());
+            self.nav_history
+                .0
+                .lock()
+                .paths_by_item
+                .remove(&item.item_id());
         }
 
         if self.items.is_empty() && self.zoomed {
@@ -1325,7 +1315,7 @@ impl Pane {
     ) -> Option<()> {
         let (item_index_to_delete, item_id) = self.items().enumerate().find_map(|(i, item)| {
             if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == [entry_id] {
-                Some((i, item.id()))
+                Some((i, item.item_id()))
             } else {
                 None
             }
@@ -1356,10 +1346,10 @@ impl Pane {
     ) -> impl Component<Self> {
         let label = item.tab_content(Some(detail), cx);
         let close_icon = || {
-            let id = item.id();
+            let id = item.item_id();
 
             div()
-                .id(item.id())
+                .id(item.item_id())
                 .invisible()
                 .group_hover("", |style| style.visible())
                 .child(IconButton::new("close_tab", Icon::Close).on_click(
@@ -1389,7 +1379,7 @@ impl Pane {
 
         div()
             .group("")
-            .id(item.id())
+            .id(item.item_id())
             .cursor_pointer()
             .when_some(item.tab_tooltip_text(cx), |div, text| {
                 div.tooltip(move |_, cx| cx.build_view(|cx| Tooltip::new(text.clone())).into())
@@ -1916,8 +1906,27 @@ impl Render for Pane {
             .on_action(|pane: &mut Pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx))
             .on_action(|pane: &mut Pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx))
             .on_action(|pane: &mut Pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx))
+            //     cx.add_action(Pane::toggle_zoom);
+            //     cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| {
+            //         pane.activate_item(action.0, true, true, cx);
+            //     });
+            //     cx.add_action(|pane: &mut Pane, _: &ActivateLastItem, cx| {
+            //         pane.activate_item(pane.items.len() - 1, true, true, cx);
+            //     });
+            //     cx.add_action(|pane: &mut Pane, _: &ActivatePrevItem, cx| {
+            //         pane.activate_prev_item(true, cx);
+            //     });
+            //     cx.add_action(|pane: &mut Pane, _: &ActivateNextItem, cx| {
+            //         pane.activate_next_item(true, cx);
+            //     });
+            //     cx.add_async_action(Pane::close_active_item);
+            //     cx.add_async_action(Pane::close_inactive_items);
+            //     cx.add_async_action(Pane::close_clean_items);
+            //     cx.add_async_action(Pane::close_items_to_the_left);
+            //     cx.add_async_action(Pane::close_items_to_the_right);
+            //     cx.add_async_action(Pane::close_all_items);
             .size_full()
-            .on_action(|pane: &mut Self, action, cx| {
+            .on_action(|pane: &mut Self, action: &CloseActiveItem, cx| {
                 pane.close_active_item(action, cx)
                     .map(|task| task.detach_and_log_err(cx));
             })

crates/workspace2/src/searchable.rs 🔗

@@ -240,7 +240,7 @@ impl From<&Box<dyn SearchableItemHandle>> for AnyView {
 
 impl PartialEq for Box<dyn SearchableItemHandle> {
     fn eq(&self, other: &Self) -> bool {
-        self.id() == other.id()
+        self.item_id() == other.item_id()
     }
 }
 

crates/workspace2/src/workspace2.rs 🔗

@@ -29,11 +29,12 @@ use futures::{
     Future, FutureExt, StreamExt,
 };
 use gpui::{
-    actions, div, point, register_action, size, Action, AnyModel, AnyView, AnyWeakView, AppContext,
-    AsyncAppContext, AsyncWindowContext, Bounds, Context, Div, Entity, EntityId, EventEmitter,
-    FocusHandle, FocusableView, GlobalPixels, InteractiveComponent, KeyContext, Model,
-    ModelContext, ParentComponent, Point, Render, Size, Styled, Subscription, Task, View,
-    ViewContext, VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle, WindowOptions,
+    actions, div, point, size, Action, AnyModel, AnyView, AnyWeakView, AppContext, AsyncAppContext,
+    AsyncWindowContext, Bounds, Context, Div, Entity, EntityId, EventEmitter, FocusHandle,
+    FocusableView, GlobalPixels, InteractiveComponent, KeyContext, ManagedView, Model,
+    ModelContext, ParentComponent, PathPromptOptions, Point, PromptLevel, Render, Size, Styled,
+    Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowBounds, WindowContext,
+    WindowHandle, WindowOptions,
 };
 use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem};
 use itertools::Itertools;
@@ -49,7 +50,7 @@ pub use persistence::{
     WorkspaceDb, DB,
 };
 use postage::stream::Stream;
-use project2::{Project, ProjectEntryId, ProjectPath, Worktree};
+use project2::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
 use serde::Deserialize;
 use settings2::Settings;
 use status_bar::StatusBar;
@@ -57,7 +58,7 @@ pub use status_bar::StatusItemView;
 use std::{
     any::TypeId,
     borrow::Cow,
-    env,
+    cmp, env,
     path::{Path, PathBuf},
     sync::{atomic::AtomicUsize, Arc},
     time::Duration,
@@ -84,8 +85,8 @@ lazy_static! {
         .and_then(parse_pixel_position_env_var);
 }
 
-// #[derive(Clone, PartialEq)]
-// pub struct RemoveWorktreeFromProject(pub WorktreeId);
+#[derive(Clone, PartialEq)]
+pub struct RemoveWorktreeFromProject(pub WorktreeId);
 
 actions!(
     Open,
@@ -114,40 +115,40 @@ actions!(
     CloseAllDocks,
 );
 
-// #[derive(Clone, PartialEq)]
-// pub struct OpenPaths {
-//     pub paths: Vec<PathBuf>,
-// }
+#[derive(Clone, PartialEq)]
+pub struct OpenPaths {
+    pub paths: Vec<PathBuf>,
+}
 
-// #[derive(Clone, Deserialize, PartialEq)]
-// pub struct ActivatePane(pub usize);
+#[derive(Clone, Deserialize, PartialEq, Action)]
+pub struct ActivatePane(pub usize);
 
-// #[derive(Clone, Deserialize, PartialEq)]
-// pub struct ActivatePaneInDirection(pub SplitDirection);
+#[derive(Clone, Deserialize, PartialEq, Action)]
+pub struct ActivatePaneInDirection(pub SplitDirection);
 
-// #[derive(Clone, Deserialize, PartialEq)]
-// pub struct SwapPaneInDirection(pub SplitDirection);
+#[derive(Clone, Deserialize, PartialEq, Action)]
+pub struct SwapPaneInDirection(pub SplitDirection);
 
-// #[derive(Clone, Deserialize, PartialEq)]
-// pub struct NewFileInDirection(pub SplitDirection);
+#[derive(Clone, Deserialize, PartialEq, Action)]
+pub struct NewFileInDirection(pub SplitDirection);
 
-// #[derive(Clone, PartialEq, Debug, Deserialize)]
-// #[serde(rename_all = "camelCase")]
-// pub struct SaveAll {
-//     pub save_intent: Option<SaveIntent>,
-// }
+#[derive(Clone, PartialEq, Debug, Deserialize, Action)]
+#[serde(rename_all = "camelCase")]
+pub struct SaveAll {
+    pub save_intent: Option<SaveIntent>,
+}
 
-// #[derive(Clone, PartialEq, Debug, Deserialize)]
-// #[serde(rename_all = "camelCase")]
-// pub struct Save {
-//     pub save_intent: Option<SaveIntent>,
-// }
+#[derive(Clone, PartialEq, Debug, Deserialize, Action)]
+#[serde(rename_all = "camelCase")]
+pub struct Save {
+    pub save_intent: Option<SaveIntent>,
+}
 
-// #[derive(Clone, PartialEq, Debug, Deserialize, Default)]
-// #[serde(rename_all = "camelCase")]
-// pub struct CloseAllItemsAndPanes {
-//     pub save_intent: Option<SaveIntent>,
-// }
+#[derive(Clone, PartialEq, Debug, Deserialize, Default, Action)]
+#[serde(rename_all = "camelCase")]
+pub struct CloseAllItemsAndPanes {
+    pub save_intent: Option<SaveIntent>,
+}
 
 #[derive(Deserialize)]
 pub struct Toast {
@@ -194,26 +195,11 @@ impl Clone for Toast {
     }
 }
 
-#[register_action]
-#[derive(Debug, Default, Clone, Deserialize, PartialEq)]
+#[derive(Debug, Default, Clone, Deserialize, PartialEq, Action)]
 pub struct OpenTerminal {
     pub working_directory: PathBuf,
 }
 
-// impl_actions!(
-//     workspace,
-//     [
-//         ActivatePane,
-//         ActivatePaneInDirection,
-//         SwapPaneInDirection,
-//         NewFileInDirection,
-//         Toast,
-//         SaveAll,
-//         Save,
-//         CloseAllItemsAndPanes,
-//     ]
-// );
-
 pub type WorkspaceId = i64;
 
 pub fn init_settings(cx: &mut AppContext) {
@@ -223,7 +209,6 @@ pub fn init_settings(cx: &mut AppContext) {
 
 pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
     init_settings(cx);
-    pane::init(cx);
     notifications::init(cx);
 
     //     cx.add_global_action({
@@ -355,7 +340,7 @@ impl AppState {
         let user_store = cx.build_model(|cx| UserStore::new(client.clone(), http_client, cx));
         let workspace_store = cx.build_model(|cx| WorkspaceStore::new(client.clone(), cx));
 
-        theme2::init(cx);
+        theme2::init(theme2::LoadThemes::JustBase, cx);
         client2::init(&client, cx);
         crate::init_settings(cx);
 
@@ -424,6 +409,7 @@ pub enum Event {
 }
 
 pub struct Workspace {
+    window_self: WindowHandle<Self>,
     weak_self: WeakView<Self>,
     workspace_actions: Vec<Box<dyn Fn(Div<Workspace>) -> Div<Workspace>>>,
     zoomed: Option<AnyWeakView>,
@@ -456,6 +442,8 @@ pub struct Workspace {
     pane_history_timestamp: Arc<AtomicUsize>,
 }
 
+impl EventEmitter<Event> for Workspace {}
+
 #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
 pub struct ViewId {
     pub creator: PeerId,
@@ -533,8 +521,8 @@ impl Workspace {
             )
         });
         cx.subscribe(&center_pane, Self::handle_pane_event).detach();
-        // todo!()
-        // cx.focus(&center_pane);
+
+        cx.focus_view(&center_pane);
         cx.emit(Event::PaneAdded(center_pane.clone()));
 
         let window_handle = cx.window_handle().downcast::<Workspace>().unwrap();
@@ -637,10 +625,16 @@ impl Workspace {
                 this.serialize_workspace(cx);
                 cx.notify();
             }),
+            cx.on_release(|this, cx| {
+                this.app_state.workspace_store.update(cx, |store, _| {
+                    store.workspaces.remove(&this.window_self);
+                })
+            }),
         ];
 
         cx.defer(|this, cx| this.update_window_title(cx));
         Workspace {
+            window_self: window_handle,
             weak_self: weak_handle.clone(),
             zoomed: None,
             zoomed_position: None,
@@ -780,19 +774,6 @@ impl Workspace {
                 })?
             };
 
-            // todo!() Ask how to do this
-            // let weak_view = window.update(&mut cx, |_, cx| cx.view().downgrade())?;
-            // let async_cx = window.update(&mut cx, |_, cx| cx.to_async())?;
-
-            // (app_state.initialize_workspace)(
-            //     weak_view,
-            //     serialized_workspace.is_some(),
-            //     app_state.clone(),
-            //     async_cx,
-            // )
-            // .await
-            // .log_err();
-
             window
                 .update(&mut cx, |_, cx| cx.activate_window())
                 .log_err();
@@ -965,12 +946,12 @@ impl Workspace {
                 if let Some((project_entry_id, build_item)) = task.log_err() {
                     let prev_active_item_id = pane.update(&mut cx, |pane, _| {
                         pane.nav_history_mut().set_mode(mode);
-                        pane.active_item().map(|p| p.id())
+                        pane.active_item().map(|p| p.item_id())
                     })?;
 
                     pane.update(&mut cx, |pane, cx| {
                         let item = pane.open_item(project_entry_id, true, cx, build_item);
-                        navigated |= Some(item.id()) != prev_active_item_id;
+                        navigated |= Some(item.item_id()) != prev_active_item_id;
                         pane.nav_history_mut().set_mode(NavigationMode::Normal);
                         if let Some(data) = entry.data {
                             navigated |= item.navigate(data, cx);
@@ -1078,35 +1059,40 @@ impl Workspace {
         }
     }
 
-    //     pub fn close_global(_: &CloseWindow, cx: &mut AppContext) {
-    //         cx.spawn(|mut cx| async move {
-    //             let window = cx
-    //                 .windows()
-    //                 .into_iter()
-    //                 .find(|window| window.is_active(&cx).unwrap_or(false));
-    //             if let Some(window) = window {
-    //                 //This can only get called when the window's project connection has been lost
-    //                 //so we don't need to prompt the user for anything and instead just close the window
-    //                 window.remove(&mut cx);
-    //             }
-    //         })
-    //         .detach();
-    //     }
+    // todo!(Non-window-actions)
+    pub fn close_global(_: &CloseWindow, cx: &mut AppContext) {
+        cx.windows().iter().find(|window| {
+            window
+                .update(cx, |_, window| {
+                    if window.is_window_active() {
+                        //This can only get called when the window's project connection has been lost
+                        //so we don't need to prompt the user for anything and instead just close the window
+                        window.remove_window();
+                        true
+                    } else {
+                        false
+                    }
+                })
+                .unwrap_or(false)
+        });
+    }
 
-    //     pub fn close(
-    //         &mut self,
-    //         _: &CloseWindow,
-    //         cx: &mut ViewContext<Self>,
-    //     ) -> Option<Task<Result<()>>> {
-    //         let window = cx.window();
-    //         let prepare = self.prepare_to_close(false, cx);
-    //         Some(cx.spawn(|_, mut cx| async move {
-    //             if prepare.await? {
-    //                 window.remove(&mut cx);
-    //             }
-    //             Ok(())
-    //         }))
-    //     }
+    pub fn close(
+        &mut self,
+        _: &CloseWindow,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<Task<Result<()>>> {
+        let window = cx.window_handle();
+        let prepare = self.prepare_to_close(false, cx);
+        Some(cx.spawn(|_, mut cx| async move {
+            if prepare.await? {
+                window.update(&mut cx, |_, cx| {
+                    cx.remove_window();
+                })?;
+            }
+            Ok(())
+        }))
+    }
 
     pub fn prepare_to_close(
         &mut self,
@@ -1114,184 +1100,177 @@ impl Workspace {
         cx: &mut ViewContext<Self>,
     ) -> Task<Result<bool>> {
         //todo!(saveing)
-        // let active_call = self.active_call().cloned();
-        // let window = cx.window();
+        let active_call = self.active_call().cloned();
+        let window = cx.window_handle();
 
         cx.spawn(|this, mut cx| async move {
-            // let workspace_count = cx
-            //     .windows()
-            //     .into_iter()
-            //     .filter(|window| window.root_is::<Workspace>())
-            //     .count();
-
-            // if let Some(active_call) = active_call {
-            //     if !quitting
-            //         && workspace_count == 1
-            //         && active_call.read_with(&cx, |call, _| call.room().is_some())
-            //     {
-            //         let answer = window.prompt(
-            //             PromptLevel::Warning,
-            //             "Do you want to leave the current call?",
-            //             &["Close window and hang up", "Cancel"],
-            //             &mut cx,
-            //         );
-
-            //         if let Some(mut answer) = answer {
-            //             if answer.next().await == Some(1) {
-            //                 return anyhow::Ok(false);
-            //             } else {
-            //                 active_call
-            //                     .update(&mut cx, |call, cx| call.hang_up(cx))
-            //                     .await
-            //                     .log_err();
-            //             }
-            //         }
-            //     }
-            // }
+            let workspace_count = cx.update(|_, cx| {
+                cx.windows()
+                    .iter()
+                    .filter(|window| window.downcast::<Workspace>().is_some())
+                    .count()
+            })?;
 
-            Ok(
-                false, // this
-                      // .update(&mut cx, |this, cx| {
-                      //     this.save_all_internal(SaveIntent::Close, cx)
-                      // })?
-                      // .await?
-            )
+            if let Some(active_call) = active_call {
+                if !quitting
+                    && workspace_count == 1
+                    && active_call.read_with(&cx, |call, _| call.room().is_some())?
+                {
+                    let answer = window.update(&mut cx, |_, cx| {
+                        cx.prompt(
+                            PromptLevel::Warning,
+                            "Do you want to leave the current call?",
+                            &["Close window and hang up", "Cancel"],
+                        )
+                    })?;
+
+                    if answer.await.log_err() == Some(1) {
+                        return anyhow::Ok(false);
+                    } else {
+                        active_call
+                            .update(&mut cx, |call, cx| call.hang_up(cx))?
+                            .await
+                            .log_err();
+                    }
+                }
+            }
+
+            Ok(this
+                .update(&mut cx, |this, cx| {
+                    this.save_all_internal(SaveIntent::Close, cx)
+                })?
+                .await?)
         })
     }
 
-    //     fn save_all(
-    //         &mut self,
-    //         action: &SaveAll,
-    //         cx: &mut ViewContext<Self>,
-    //     ) -> Option<Task<Result<()>>> {
-    //         let save_all =
-    //             self.save_all_internal(action.save_intent.unwrap_or(SaveIntent::SaveAll), cx);
-    //         Some(cx.foreground().spawn(async move {
-    //             save_all.await?;
-    //             Ok(())
-    //         }))
-    //     }
+    fn save_all(&mut self, action: &SaveAll, cx: &mut ViewContext<Self>) {
+        let save_all = self
+            .save_all_internal(action.save_intent.unwrap_or(SaveIntent::SaveAll), cx)
+            .detach_and_log_err(cx);
+    }
 
-    //     fn save_all_internal(
-    //         &mut self,
-    //         mut save_intent: SaveIntent,
-    //         cx: &mut ViewContext<Self>,
-    //     ) -> Task<Result<bool>> {
-    //         if self.project.read(cx).is_read_only() {
-    //             return Task::ready(Ok(true));
-    //         }
-    //         let dirty_items = self
-    //             .panes
-    //             .iter()
-    //             .flat_map(|pane| {
-    //                 pane.read(cx).items().filter_map(|item| {
-    //                     if item.is_dirty(cx) {
-    //                         Some((pane.downgrade(), item.boxed_clone()))
-    //                     } else {
-    //                         None
-    //                     }
-    //                 })
-    //             })
-    //             .collect::<Vec<_>>();
-
-    //         let project = self.project.clone();
-    //         cx.spawn(|workspace, mut cx| async move {
-    //             // Override save mode and display "Save all files" prompt
-    //             if save_intent == SaveIntent::Close && dirty_items.len() > 1 {
-    //                 let mut answer = workspace.update(&mut cx, |_, cx| {
-    //                     let prompt = Pane::file_names_for_prompt(
-    //                         &mut dirty_items.iter().map(|(_, handle)| handle),
-    //                         dirty_items.len(),
-    //                         cx,
-    //                     );
-    //                     cx.prompt(
-    //                         PromptLevel::Warning,
-    //                         &prompt,
-    //                         &["Save all", "Discard all", "Cancel"],
-    //                     )
-    //                 })?;
-    //                 match answer.next().await {
-    //                     Some(0) => save_intent = SaveIntent::SaveAll,
-    //                     Some(1) => save_intent = SaveIntent::Skip,
-    //                     _ => {}
-    //                 }
-    //             }
-    //             for (pane, item) in dirty_items {
-    //                 let (singleton, project_entry_ids) =
-    //                     cx.read(|cx| (item.is_singleton(cx), item.project_entry_ids(cx)));
-    //                 if singleton || !project_entry_ids.is_empty() {
-    //                     if let Some(ix) =
-    //                         pane.read_with(&cx, |pane, _| pane.index_for_item(item.as_ref()))?
-    //                     {
-    //                         if !Pane::save_item(
-    //                             project.clone(),
-    //                             &pane,
-    //                             ix,
-    //                             &*item,
-    //                             save_intent,
-    //                             &mut cx,
-    //                         )
-    //                         .await?
-    //                         {
-    //                             return Ok(false);
-    //                         }
-    //                     }
-    //                 }
-    //             }
-    //             Ok(true)
-    //         })
-    //     }
+    fn save_all_internal(
+        &mut self,
+        mut save_intent: SaveIntent,
+        cx: &mut ViewContext<Self>,
+    ) -> Task<Result<bool>> {
+        if self.project.read(cx).is_read_only() {
+            return Task::ready(Ok(true));
+        }
+        let dirty_items = self
+            .panes
+            .iter()
+            .flat_map(|pane| {
+                pane.read(cx).items().filter_map(|item| {
+                    if item.is_dirty(cx) {
+                        Some((pane.downgrade(), item.boxed_clone()))
+                    } else {
+                        None
+                    }
+                })
+            })
+            .collect::<Vec<_>>();
 
-    //     pub fn open(&mut self, _: &Open, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
-    //         let mut paths = cx.prompt_for_paths(PathPromptOptions {
-    //             files: true,
-    //             directories: true,
-    //             multiple: true,
-    //         });
+        let project = self.project.clone();
+        cx.spawn(|workspace, mut cx| async move {
+            // Override save mode and display "Save all files" prompt
+            if save_intent == SaveIntent::Close && dirty_items.len() > 1 {
+                let mut answer = workspace.update(&mut cx, |_, cx| {
+                    let prompt = Pane::file_names_for_prompt(
+                        &mut dirty_items.iter().map(|(_, handle)| handle),
+                        dirty_items.len(),
+                        cx,
+                    );
+                    cx.prompt(
+                        PromptLevel::Warning,
+                        &prompt,
+                        &["Save all", "Discard all", "Cancel"],
+                    )
+                })?;
+                match answer.await.log_err() {
+                    Some(0) => save_intent = SaveIntent::SaveAll,
+                    Some(1) => save_intent = SaveIntent::Skip,
+                    _ => {}
+                }
+            }
+            for (pane, item) in dirty_items {
+                let (singleton, project_entry_ids) =
+                    cx.update(|_, cx| (item.is_singleton(cx), item.project_entry_ids(cx)))?;
+                if singleton || !project_entry_ids.is_empty() {
+                    if let Some(ix) =
+                        pane.update(&mut cx, |pane, _| pane.index_for_item(item.as_ref()))?
+                    {
+                        if !Pane::save_item(
+                            project.clone(),
+                            &pane,
+                            ix,
+                            &*item,
+                            save_intent,
+                            &mut cx,
+                        )
+                        .await?
+                        {
+                            return Ok(false);
+                        }
+                    }
+                }
+            }
+            Ok(true)
+        })
+    }
 
-    //         Some(cx.spawn(|this, mut cx| async move {
-    //             if let Some(paths) = paths.recv().await.flatten() {
-    //                 if let Some(task) = this
-    //                     .update(&mut cx, |this, cx| this.open_workspace_for_paths(paths, cx))
-    //                     .log_err()
-    //                 {
-    //                     task.await?
-    //                 }
-    //             }
-    //             Ok(())
-    //         }))
-    //     }
+    pub fn open(&mut self, _: &Open, cx: &mut ViewContext<Self>) {
+        let mut paths = cx.prompt_for_paths(PathPromptOptions {
+            files: true,
+            directories: true,
+            multiple: true,
+        });
 
-    //     pub fn open_workspace_for_paths(
-    //         &mut self,
-    //         paths: Vec<PathBuf>,
-    //         cx: &mut ViewContext<Self>,
-    //     ) -> Task<Result<()>> {
-    //         let window = cx.window().downcast::<Self>();
-    //         let is_remote = self.project.read(cx).is_remote();
-    //         let has_worktree = self.project.read(cx).worktrees(cx).next().is_some();
-    //         let has_dirty_items = self.items(cx).any(|item| item.is_dirty(cx));
-    //         let close_task = if is_remote || has_worktree || has_dirty_items {
-    //             None
-    //         } else {
-    //             Some(self.prepare_to_close(false, cx))
-    //         };
-    //         let app_state = self.app_state.clone();
+        cx.spawn(|this, mut cx| async move {
+            let Some(paths) = paths.await.log_err().flatten() else {
+                return;
+            };
 
-    //         cx.spawn(|_, mut cx| async move {
-    //             let window_to_replace = if let Some(close_task) = close_task {
-    //                 if !close_task.await? {
-    //                     return Ok(());
-    //                 }
-    //                 window
-    //             } else {
-    //                 None
-    //             };
-    //             cx.update(|cx| open_paths(&paths, &app_state, window_to_replace, cx))
-    //                 .await?;
-    //             Ok(())
-    //         })
-    //     }
+            if let Some(task) = this
+                .update(&mut cx, |this, cx| this.open_workspace_for_paths(paths, cx))
+                .log_err()
+            {
+                task.await.log_err();
+            }
+        })
+        .detach()
+    }
+
+    pub fn open_workspace_for_paths(
+        &mut self,
+        paths: Vec<PathBuf>,
+        cx: &mut ViewContext<Self>,
+    ) -> Task<Result<()>> {
+        let window = cx.window_handle().downcast::<Self>();
+        let is_remote = self.project.read(cx).is_remote();
+        let has_worktree = self.project.read(cx).worktrees().next().is_some();
+        let has_dirty_items = self.items(cx).any(|item| item.is_dirty(cx));
+        let close_task = if is_remote || has_worktree || has_dirty_items {
+            None
+        } else {
+            Some(self.prepare_to_close(false, cx))
+        };
+        let app_state = self.app_state.clone();
+
+        cx.spawn(|_, mut cx| async move {
+            let window_to_replace = if let Some(close_task) = close_task {
+                if !close_task.await? {
+                    return Ok(());
+                }
+                window
+            } else {
+                None
+            };
+            cx.update(|_, cx| open_paths(&paths, &app_state, window_to_replace, cx))?
+                .await?;
+            Ok(())
+        })
+    }
 
     #[allow(clippy::type_complexity)]
     pub fn open_paths(
@@ -1369,25 +1348,25 @@ impl Workspace {
         })
     }
 
-    //     fn add_folder_to_project(&mut self, _: &AddFolderToProject, cx: &mut ViewContext<Self>) {
-    //         let mut paths = cx.prompt_for_paths(PathPromptOptions {
-    //             files: false,
-    //             directories: true,
-    //             multiple: true,
-    //         });
-    //         cx.spawn(|this, mut cx| async move {
-    //             if let Some(paths) = paths.recv().await.flatten() {
-    //                 let results = this
-    //                     .update(&mut cx, |this, cx| this.open_paths(paths, true, cx))?
-    //                     .await;
-    //                 for result in results.into_iter().flatten() {
-    //                     result.log_err();
-    //                 }
-    //             }
-    //             anyhow::Ok(())
-    //         })
-    //         .detach_and_log_err(cx);
-    //     }
+    fn add_folder_to_project(&mut self, _: &AddFolderToProject, cx: &mut ViewContext<Self>) {
+        let mut paths = cx.prompt_for_paths(PathPromptOptions {
+            files: false,
+            directories: true,
+            multiple: true,
+        });
+        cx.spawn(|this, mut cx| async move {
+            if let Some(paths) = paths.await.log_err().flatten() {
+                let results = this
+                    .update(&mut cx, |this, cx| this.open_paths(paths, true, cx))?
+                    .await;
+                for result in results.into_iter().flatten() {
+                    result.log_err();
+                }
+            }
+            anyhow::Ok(())
+        })
+        .detach_and_log_err(cx);
+    }
 
     fn project_path_for_path(
         project: Model<Project>,
@@ -1418,18 +1397,18 @@ impl Workspace {
         self.panes.iter().flat_map(|pane| pane.read(cx).items())
     }
 
-    //     pub fn item_of_type<T: Item>(&self, cx: &AppContext) -> Option<View<T>> {
-    //         self.items_of_type(cx).max_by_key(|item| item.id())
-    //     }
+    pub fn item_of_type<T: Item>(&self, cx: &AppContext) -> Option<View<T>> {
+        self.items_of_type(cx).max_by_key(|item| item.item_id())
+    }
 
-    //     pub fn items_of_type<'a, T: Item>(
-    //         &'a self,
-    //         cx: &'a AppContext,
-    //     ) -> impl 'a + Iterator<Item = View<T>> {
-    //         self.panes
-    //             .iter()
-    //             .flat_map(|pane| pane.read(cx).items_of_type())
-    //     }
+    pub fn items_of_type<'a, T: Item>(
+        &'a self,
+        cx: &'a AppContext,
+    ) -> impl 'a + Iterator<Item = View<T>> {
+        self.panes
+            .iter()
+            .flat_map(|pane| pane.read(cx).items_of_type())
+    }
 
     pub fn active_item(&self, cx: &AppContext) -> Option<Box<dyn ItemHandle>> {
         self.active_pane().read(cx).active_item()
@@ -1466,68 +1445,70 @@ impl Workspace {
         })
     }
 
-    //     pub fn close_inactive_items_and_panes(
-    //         &mut self,
-    //         _: &CloseInactiveTabsAndPanes,
-    //         cx: &mut ViewContext<Self>,
-    //     ) -> Option<Task<Result<()>>> {
-    //         self.close_all_internal(true, SaveIntent::Close, cx)
-    //     }
+    pub fn close_inactive_items_and_panes(
+        &mut self,
+        _: &CloseInactiveTabsAndPanes,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.close_all_internal(true, SaveIntent::Close, cx)
+            .map(|task| task.detach_and_log_err(cx));
+    }
 
-    //     pub fn close_all_items_and_panes(
-    //         &mut self,
-    //         action: &CloseAllItemsAndPanes,
-    //         cx: &mut ViewContext<Self>,
-    //     ) -> Option<Task<Result<()>>> {
-    //         self.close_all_internal(false, action.save_intent.unwrap_or(SaveIntent::Close), cx)
-    //     }
+    pub fn close_all_items_and_panes(
+        &mut self,
+        action: &CloseAllItemsAndPanes,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.close_all_internal(false, action.save_intent.unwrap_or(SaveIntent::Close), cx)
+            .map(|task| task.detach_and_log_err(cx));
+    }
 
-    //     fn close_all_internal(
-    //         &mut self,
-    //         retain_active_pane: bool,
-    //         save_intent: SaveIntent,
-    //         cx: &mut ViewContext<Self>,
-    //     ) -> Option<Task<Result<()>>> {
-    //         let current_pane = self.active_pane();
+    fn close_all_internal(
+        &mut self,
+        retain_active_pane: bool,
+        save_intent: SaveIntent,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<Task<Result<()>>> {
+        let current_pane = self.active_pane();
 
-    //         let mut tasks = Vec::new();
+        let mut tasks = Vec::new();
 
-    //         if retain_active_pane {
-    //             if let Some(current_pane_close) = current_pane.update(cx, |pane, cx| {
-    //                 pane.close_inactive_items(&CloseInactiveItems, cx)
-    //             }) {
-    //                 tasks.push(current_pane_close);
-    //             };
-    //         }
+        if retain_active_pane {
+            if let Some(current_pane_close) = current_pane.update(cx, |pane, cx| {
+                pane.close_inactive_items(&CloseInactiveItems, cx)
+            }) {
+                tasks.push(current_pane_close);
+            };
+        }
 
-    //         for pane in self.panes() {
-    //             if retain_active_pane && pane.id() == current_pane.id() {
-    //                 continue;
-    //             }
+        for pane in self.panes() {
+            if retain_active_pane && pane.entity_id() == current_pane.entity_id() {
+                continue;
+            }
 
-    //             if let Some(close_pane_items) = pane.update(cx, |pane: &mut Pane, cx| {
-    //                 pane.close_all_items(
-    //                     &CloseAllItems {
-    //                         save_intent: Some(save_intent),
-    //                     },
-    //                     cx,
-    //                 )
-    //             }) {
-    //                 tasks.push(close_pane_items)
-    //             }
-    //         }
+            if let Some(close_pane_items) = pane.update(cx, |pane: &mut Pane, cx| {
+                pane.close_all_items(
+                    &CloseAllItems {
+                        save_intent: Some(save_intent),
+                    },
+                    cx,
+                )
+            }) {
+                tasks.push(close_pane_items)
+            }
+        }
 
-    //         if tasks.is_empty() {
-    //             None
-    //         } else {
-    //             Some(cx.spawn(|_, _| async move {
-    //                 for task in tasks {
-    //                     task.await?
-    //                 }
-    //                 Ok(())
-    //             }))
-    //         }
-    //     }
+        if tasks.is_empty() {
+            None
+        } else {
+            Some(cx.spawn(|_, _| async move {
+                for task in tasks {
+                    task.await?
+                }
+                Ok(())
+            }))
+        }
+    }
 
     pub fn toggle_dock(&mut self, dock_side: DockPosition, cx: &mut ViewContext<Self>) {
         let dock = match dock_side {
@@ -1635,15 +1616,15 @@ impl Workspace {
         None
     }
 
-    //     pub fn panel<T: Panel>(&self, cx: &WindowContext) -> Option<View<T>> {
-    //         for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
-    //             let dock = dock.read(cx);
-    //             if let Some(panel) = dock.panel::<T>() {
-    //                 return Some(panel);
-    //             }
-    //         }
-    //         None
-    //     }
+    pub fn panel<T: Panel>(&self, cx: &WindowContext) -> Option<View<T>> {
+        for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
+            let dock = dock.read(cx);
+            if let Some(panel) = dock.panel::<T>() {
+                return Some(panel);
+            }
+        }
+        None
+    }
 
     fn zoom_out(&mut self, cx: &mut ViewContext<Self>) {
         for pane in &self.panes {
@@ -1954,81 +1935,89 @@ impl Workspace {
         }
     }
 
-    //     fn activate_pane_at_index(&mut self, action: &ActivatePane, cx: &mut ViewContext<Self>) {
-    //         let panes = self.center.panes();
-    //         if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
-    //             cx.focus(&pane);
-    //         } else {
-    //             self.split_and_clone(self.active_pane.clone(), SplitDirection::Right, cx);
-    //         }
-    //     }
+    fn activate_pane_at_index(&mut self, action: &ActivatePane, cx: &mut ViewContext<Self>) {
+        let panes = self.center.panes();
+        if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) {
+            cx.focus_view(&pane);
+        } else {
+            self.split_and_clone(self.active_pane.clone(), SplitDirection::Right, cx);
+        }
+    }
 
-    //     pub fn activate_next_pane(&mut self, cx: &mut ViewContext<Self>) {
-    //         let panes = self.center.panes();
-    //         if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
-    //             let next_ix = (ix + 1) % panes.len();
-    //             let next_pane = panes[next_ix].clone();
-    //             cx.focus(&next_pane);
-    //         }
-    //     }
+    pub fn activate_next_pane(&mut self, cx: &mut ViewContext<Self>) {
+        let panes = self.center.panes();
+        if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
+            let next_ix = (ix + 1) % panes.len();
+            let next_pane = panes[next_ix].clone();
+            cx.focus_view(&next_pane);
+        }
+    }
 
-    //     pub fn activate_previous_pane(&mut self, cx: &mut ViewContext<Self>) {
-    //         let panes = self.center.panes();
-    //         if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
-    //             let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1);
-    //             let prev_pane = panes[prev_ix].clone();
-    //             cx.focus(&prev_pane);
-    //         }
-    //     }
+    pub fn activate_previous_pane(&mut self, cx: &mut ViewContext<Self>) {
+        let panes = self.center.panes();
+        if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) {
+            let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1);
+            let prev_pane = panes[prev_ix].clone();
+            cx.focus_view(&prev_pane);
+        }
+    }
 
-    //     pub fn activate_pane_in_direction(
-    //         &mut self,
-    //         direction: SplitDirection,
-    //         cx: &mut ViewContext<Self>,
-    //     ) {
-    //         if let Some(pane) = self.find_pane_in_direction(direction, cx) {
-    //             cx.focus(pane);
-    //         }
-    //     }
+    pub fn activate_pane_in_direction(
+        &mut self,
+        direction: SplitDirection,
+        cx: &mut ViewContext<Self>,
+    ) {
+        if let Some(pane) = self.find_pane_in_direction(direction, cx) {
+            cx.focus_view(pane);
+        }
+    }
 
-    //     pub fn swap_pane_in_direction(
-    //         &mut self,
-    //         direction: SplitDirection,
-    //         cx: &mut ViewContext<Self>,
-    //     ) {
-    //         if let Some(to) = self
-    //             .find_pane_in_direction(direction, cx)
-    //             .map(|pane| pane.clone())
-    //         {
-    //             self.center.swap(&self.active_pane.clone(), &to);
-    //             cx.notify();
-    //         }
-    //     }
+    pub fn swap_pane_in_direction(
+        &mut self,
+        direction: SplitDirection,
+        cx: &mut ViewContext<Self>,
+    ) {
+        if let Some(to) = self
+            .find_pane_in_direction(direction, cx)
+            .map(|pane| pane.clone())
+        {
+            self.center.swap(&self.active_pane.clone(), &to);
+            cx.notify();
+        }
+    }
 
-    //     fn find_pane_in_direction(
-    //         &mut self,
-    //         direction: SplitDirection,
-    //         cx: &mut ViewContext<Self>,
-    //     ) -> Option<&View<Pane>> {
-    //         let Some(bounding_box) = self.center.bounding_box_for_pane(&self.active_pane) else {
-    //             return None;
-    //         };
-    //         let cursor = self.active_pane.read(cx).pixel_position_of_cursor(cx);
-    //         let center = match cursor {
-    //             Some(cursor) if bounding_box.contains_point(cursor) => cursor,
-    //             _ => bounding_box.center(),
-    //         };
+    fn find_pane_in_direction(
+        &mut self,
+        direction: SplitDirection,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<&View<Pane>> {
+        let Some(bounding_box) = self.center.bounding_box_for_pane(&self.active_pane) else {
+            return None;
+        };
+        let cursor = self.active_pane.read(cx).pixel_position_of_cursor(cx);
+        let center = match cursor {
+            Some(cursor) if bounding_box.contains_point(&cursor) => cursor,
+            _ => bounding_box.center(),
+        };
 
-    //         let distance_to_next = theme::current(cx).workspace.pane_divider.width + 1.;
+        let distance_to_next = 1.; //todo(pane dividers styling)
 
-    //         let target = match direction {
-    //             SplitDirection::Left => vec2f(bounding_box.origin_x() - distance_to_next, center.y()),
-    //             SplitDirection::Right => vec2f(bounding_box.max_x() + distance_to_next, center.y()),
-    //             SplitDirection::Up => vec2f(center.x(), bounding_box.origin_y() - distance_to_next),
-    //             SplitDirection::Down => vec2f(center.x(), bounding_box.max_y() + distance_to_next),
-    //         };
-    //         self.center.pane_at_pixel_position(target)
-    //     }
+        let target = match direction {
+            SplitDirection::Left => {
+                Point::new(bounding_box.origin.x - distance_to_next.into(), center.y)
+            }
+            SplitDirection::Right => {
+                Point::new(bounding_box.right() + distance_to_next.into(), center.y)
+            }
+            SplitDirection::Up => {
+                Point::new(center.x, bounding_box.origin.y - distance_to_next.into())
+            }
+            SplitDirection::Down => {
+                Point::new(center.x, bounding_box.top() + distance_to_next.into())
+            }
+        };
+        self.center.pane_at_pixel_position(target)
+    }
 
     fn handle_pane_focused(&mut self, pane: View<Pane>, cx: &mut ViewContext<Self>) {
         if self.active_pane != pane {

crates/zed2/Cargo.toml 🔗

@@ -11,7 +11,7 @@ path = "src/zed2.rs"
 doctest = false
 
 [[bin]]
-name = "Zed"
+name = "Zed2"
 path = "src/main.rs"
 
 [dependencies]
@@ -27,7 +27,6 @@ collab_ui = { package = "collab_ui2", path = "../collab_ui2" }
 collections = { path = "../collections" }
 command_palette = { package="command_palette2", path = "../command_palette2" }
 # component_test = { path = "../component_test" }
-# context_menu = { path = "../context_menu" }
 client = { package = "client2", path = "../client2" }
 # clock = { path = "../clock" }
 copilot = { package = "copilot2", path = "../copilot2" }

crates/zed2/src/languages/json.rs 🔗

@@ -107,7 +107,7 @@ impl LspAdapter for JsonLspAdapter {
         &self,
         cx: &mut AppContext,
     ) -> BoxFuture<'static, serde_json::Value> {
-        let action_names = gpui::all_action_names();
+        let action_names = cx.all_action_names();
         let staff_mode = cx.is_staff();
         let language_names = &self.languages.language_names();
         let settings_schema = cx.global::<SettingsStore>().json_schema(

crates/zed2/src/main.rs 🔗

@@ -140,8 +140,7 @@ fn main() {
 
         cx.set_global(client.clone());
 
-        theme::init(cx);
-        // context_menu::init(cx);
+        theme::init(theme::LoadThemes::All, cx);
         project::Project::init(&client, cx);
         client::init(&client, cx);
         command_palette::init(cx);

crates/zed_actions2/src/lib.rs 🔗

@@ -1,4 +1,5 @@
-use gpui::action;
+use gpui::Action;
+use serde::Deserialize;
 
 // 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
@@ -9,12 +10,12 @@ use gpui::action;
 // https://github.com/mmastrac/rust-ctor/issues/280
 pub fn init() {}
 
-#[action]
+#[derive(Clone, PartialEq, Deserialize, Action)]
 pub struct OpenBrowser {
     pub url: String,
 }
 
-#[action]
+#[derive(Clone, PartialEq, Deserialize, Action)]
 pub struct OpenZedURL {
     pub url: String,
 }