From c71e522b4ea1b48865ceb5db3ee1d6a0803813ed Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Thu, 16 Nov 2023 11:37:46 -0500 Subject: [PATCH 01/37] Allow users to set UI font properties in their settings Co-Authored-By: Marshall Bowers <1486634+maxdeviant@users.noreply.github.com> --- assets/settings/default.json | 9 +++++++++ crates/editor2/src/editor.rs | 2 +- crates/theme2/src/settings.rs | 17 ++++++++++++++--- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 85f8a8fbc4..08d85dd723 100644 --- a/assets/settings/default.json +++ b/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, diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 8e7bd5876f..0063bf2ce4 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -9387,7 +9387,7 @@ impl Render for Editor { 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()), + line_height: relative(1.).into(), underline: None, } } diff --git a/crates/theme2/src/settings.rs b/crates/theme2/src/settings.rs index 5e3329ffa1..c705617db0 100644 --- a/crates/theme2/src/settings.rs +++ b/crates/theme2/src/settings.rs @@ -34,6 +34,10 @@ pub struct ThemeSettingsContent { #[serde(default)] pub ui_font_size: Option, #[serde(default)] + pub ui_font_family: Option, + #[serde(default)] + pub ui_font_features: Option, + #[serde(default)] pub buffer_font_family: Option, #[serde(default)] pub buffer_font_size: Option, @@ -120,10 +124,10 @@ impl settings::Settings for ThemeSettings { let themes = cx.default_global::>(); 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; From b2f9c454b0215a8387e8c50c270eabb577896a41 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Thu, 16 Nov 2023 11:38:04 -0500 Subject: [PATCH 02/37] Change the default buffer font size to 16 Co-Authored-By: Marshall Bowers <1486634+maxdeviant@users.noreply.github.com> --- assets/settings/default.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 08d85dd723..0b87808e22 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -23,7 +23,7 @@ // "calt": false }, // The default font size for text in the editor - "buffer_font_size": 15, + "buffer_font_size": 16, // Set the buffer's line height. // May take 3 values: // 1. Use a line height that's comfortable for reading (1.618) From 38d0fdc09aafbeb9388396fd149fd47d568f59e4 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Thu, 16 Nov 2023 11:42:23 -0500 Subject: [PATCH 03/37] Remove todo Co-Authored-By: Marshall Bowers <1486634+maxdeviant@users.noreply.github.com> --- crates/editor2/src/editor.rs | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 0063bf2ce4..c4788ebd71 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -9379,18 +9379,16 @@ impl Render for Editor { fn render(&mut self, cx: &mut ViewContext) -> 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.).into(), - 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!(), From 4540f04dbe4e8d708622d01152b5b9548253ed57 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 16 Nov 2023 09:55:22 -0700 Subject: [PATCH 04/37] Add more detail to panel switcher interaction --- crates/ui2/src/components/icon_button.rs | 6 ++++- crates/workspace2/src/dock.rs | 33 ++++++++++++++++++------ crates/workspace2/src/workspace2.rs | 24 ++++++++--------- 3 files changed, 42 insertions(+), 21 deletions(-) diff --git a/crates/ui2/src/components/icon_button.rs b/crates/ui2/src/components/icon_button.rs index 4408c51f62..1f7b86badd 100644 --- a/crates/ui2/src/components/icon_button.rs +++ b/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 { @@ -69,6 +69,10 @@ impl IconButton { self } + pub fn action(self, action: Box) -> Self { + self.on_click(move |this, cx| cx.dispatch_action(action.boxed_clone())) + } + fn render(mut self, _view: &mut V, cx: &mut ViewContext) -> impl Component { let icon_color = match (self.state, self.color) { (InteractionState::Disabled, _) => TextColor::Disabled, diff --git a/crates/workspace2/src/dock.rs b/crates/workspace2/src/dock.rs index 8e7f08252c..eec0ce309a 100644 --- a/crates/workspace2/src/dock.rs +++ b/crates/workspace2/src/dock.rs @@ -1,8 +1,8 @@ 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, + FocusHandle, FocusableView, ParentComponent, Render, SharedString, Styled, Subscription, View, + ViewContext, WeakView, WindowContext, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -416,6 +416,14 @@ impl Dock { } } + pub fn toggle_action(&self) -> Box { + match self.position { + DockPosition::Left => crate::ToggleLeftDock.boxed_clone(), + DockPosition::Bottom => crate::ToggleBottomDock.boxed_clone(), + DockPosition::Right => crate::ToggleRightDock.boxed_clone(), + } + } + // pub fn render_placeholder(&self, cx: &WindowContext) -> AnyElement { // todo!() // if let Some(active_entry) = self.visible_entry() { @@ -664,13 +672,22 @@ 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)); + let mut button: IconButton = 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(button) }); diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index f28675661d..7282e6839a 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -3213,8 +3213,8 @@ impl Workspace { }) } - fn actions(div: Div) -> Div { - div + fn actions(&self, div: Div) -> Div { + self.add_workspace_actions_listeners(div) // cx.add_async_action(Workspace::open); // cx.add_async_action(Workspace::follow_next_collaborator); // cx.add_async_action(Workspace::close); @@ -3263,15 +3263,15 @@ impl Workspace { .on_action(|this, e: &ToggleLeftDock, cx| { this.toggle_dock(DockPosition::Left, cx); }) - // cx.add_action(|workspace: &mut Workspace, _: &ToggleRightDock, cx| { - // workspace.toggle_dock(DockPosition::Right, cx); - // }); - // cx.add_action(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| { - // workspace.toggle_dock(DockPosition::Bottom, cx); - // }); - // cx.add_action(|workspace: &mut Workspace, _: &CloseAllDocks, cx| { - // workspace.close_all_docks(cx); - // }); + .on_action(|workspace: &mut Workspace, _: &ToggleRightDock, cx| { + workspace.toggle_dock(DockPosition::Right, cx); + }) + .on_action(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| { + workspace.toggle_dock(DockPosition::Bottom, cx); + }) + .on_action(|workspace: &mut Workspace, _: &CloseAllDocks, cx| { + workspace.close_all_docks(cx); + }) // cx.add_action(Workspace::activate_pane_at_index); // cx.add_action(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| { // workspace.reopen_closed_item(cx).detach(); @@ -3616,7 +3616,7 @@ impl Render for Workspace { context.add("Workspace"); let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone(); - self.add_workspace_actions_listeners(div()) + self.actions(div()) .key_context(context) .relative() .size_full() From d782426491a2eda1ec0e1c969b8735810284c88f Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 16 Nov 2023 10:26:09 -0700 Subject: [PATCH 05/37] Dismiss tooltips on click --- crates/gpui2/src/elements/div.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index 31a8827109..49a91a9821 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -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>, tooltip: Option, + _task: Option>, } /// Whether or not the element or a group that contains it is clicked by the mouse. From b559bfd80f23758e482e53e4384143a72aef2556 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Thu, 16 Nov 2023 13:03:30 -0500 Subject: [PATCH 06/37] Parameterize `theme2::init` to allow loading just the base theme (#3345) This PR adds a parameter to the `theme2::init` method to indicate what the theme-loading behavior should be. This allows us to indicate when we want to load all of the additional built-in user themes (like in the Zed binary and in the storybook), and when we don't want to load the user themes (like in tests). We're using an enum over just a `bool` here for clarity at the call site. Release Notes: - N/A --- crates/collab2/src/tests/test_server.rs | 2 +- .../command_palette2/src/command_palette.rs | 2 +- crates/editor2/src/display_map/inlay_map.rs | 2 +- crates/editor2/src/editor_tests.rs | 2 +- crates/editor2/src/inlay_hint_cache.rs | 2 +- crates/file_finder2/src/file_finder.rs | 2 +- crates/project_panel2/src/project_panel.rs | 4 ++-- crates/storybook2/src/storybook2.rs | 2 +- crates/terminal_view2/src/terminal_view.rs | 2 +- crates/theme2/src/registry.rs | 8 +++++--- crates/theme2/src/settings.rs | 2 +- crates/theme2/src/theme2.rs | 19 ++++++++++++++++++- crates/workspace2/src/workspace2.rs | 2 +- crates/zed2/src/main.rs | 2 +- 14 files changed, 36 insertions(+), 17 deletions(-) diff --git a/crates/collab2/src/tests/test_server.rs b/crates/collab2/src/tests/test_server.rs index de6f3e92a1..090a32d4ca 100644 --- a/crates/collab2/src/tests/test_server.rs +++ b/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); diff --git a/crates/command_palette2/src/command_palette.rs b/crates/command_palette2/src/command_palette.rs index 8138f025d3..31eb608ef5 100644 --- a/crates/command_palette2/src/command_palette.rs +++ b/crates/command_palette2/src/command_palette.rs @@ -456,7 +456,7 @@ mod tests { fn init_test(cx: &mut TestAppContext) -> Arc { 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); diff --git a/crates/editor2/src/display_map/inlay_map.rs b/crates/editor2/src/display_map/inlay_map.rs index a6bc6343f4..84fad96a48 100644 --- a/crates/editor2/src/display_map/inlay_map.rs +++ b/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); } } diff --git a/crates/editor2/src/editor_tests.rs b/crates/editor2/src/editor_tests.rs index 63bc6179c2..bd69e7acdf 100644 --- a/crates/editor2/src/editor_tests.rs +++ b/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); diff --git a/crates/editor2/src/inlay_hint_cache.rs b/crates/editor2/src/inlay_hint_cache.rs index af9febf376..eba49ccbf7 100644 --- a/crates/editor2/src/inlay_hint_cache.rs +++ b/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); diff --git a/crates/file_finder2/src/file_finder.rs b/crates/file_finder2/src/file_finder.rs index 236bc15244..b2850761a9 100644 --- a/crates/file_finder2/src/file_finder.rs +++ b/crates/file_finder2/src/file_finder.rs @@ -1763,7 +1763,7 @@ mod tests { fn init_test(cx: &mut TestAppContext) -> Arc { 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); diff --git a/crates/project_panel2/src/project_panel.rs b/crates/project_panel2/src/project_panel.rs index f33633afb9..87edabab52 100644 --- a/crates/project_panel2/src/project_panel.rs +++ b/crates/project_panel2/src/project_panel.rs @@ -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,7 +2798,7 @@ 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); diff --git a/crates/storybook2/src/storybook2.rs b/crates/storybook2/src/storybook2.rs index c4c1d75eac..20adc44c1a 100644 --- a/crates/storybook2/src/storybook2.rs +++ b/crates/storybook2/src/storybook2.rs @@ -60,7 +60,7 @@ 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)); diff --git a/crates/terminal_view2/src/terminal_view.rs b/crates/terminal_view2/src/terminal_view.rs index 14391ca2b2..ad33716384 100644 --- a/crates/terminal_view2/src/terminal_view.rs +++ b/crates/terminal_view2/src/terminal_view.rs @@ -1126,7 +1126,7 @@ mod tests { pub async fn init_test(cx: &mut TestAppContext) -> (Model, View) { 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); }); diff --git a/crates/theme2/src/registry.rs b/crates/theme2/src/registry.rs index 19af0ede51..919dd1b109 100644 --- a/crates/theme2/src/registry.rs +++ b/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 } } diff --git a/crates/theme2/src/settings.rs b/crates/theme2/src/settings.rs index 5e3329ffa1..071199d3d6 100644 --- a/crates/theme2/src/settings.rs +++ b/crates/theme2/src/settings.rs @@ -117,7 +117,7 @@ impl settings::Settings for ThemeSettings { user_values: &[&Self::FileContent], cx: &mut AppContext, ) -> Result { - let themes = cx.default_global::>(); + let themes = cx.default_global::(); let mut this = Self { ui_font_size: defaults.ui_font_size.unwrap_or(16.).into(), diff --git a/crates/theme2/src/theme2.rs b/crates/theme2/src/theme2.rs index b6790b5a6f..05e41ba368 100644 --- a/crates/theme2/src/theme2.rs +++ b/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::().load_user_themes(), + } + ThemeSettings::register(cx); } diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index f28675661d..a146ad1822 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -355,7 +355,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); diff --git a/crates/zed2/src/main.rs b/crates/zed2/src/main.rs index ef65d00fc3..700660a9b7 100644 --- a/crates/zed2/src/main.rs +++ b/crates/zed2/src/main.rs @@ -140,7 +140,7 @@ fn main() { cx.set_global(client.clone()); - theme::init(cx); + theme::init(theme::LoadThemes::All, cx); project::Project::init(&client, cx); client::init(&client, cx); command_palette::init(cx); From 08dddf0b262605d9d5a75c20c31521fdd5f7eaab Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Thu, 16 Nov 2023 13:13:03 -0500 Subject: [PATCH 07/37] Revert change to default buffer font size --- assets/settings/default.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 0b87808e22..08d85dd723 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -23,7 +23,7 @@ // "calt": false }, // The default font size for text in the editor - "buffer_font_size": 16, + "buffer_font_size": 15, // Set the buffer's line height. // May take 3 values: // 1. Use a line height that's comfortable for reading (1.618) From a0e976599c19de4d6a3d5975c779b35151b365c5 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Thu, 16 Nov 2023 10:32:55 -0800 Subject: [PATCH 08/37] Salvage old distributed slice code --- Cargo.lock | 21 ++++++++++++ crates/gpui2/Cargo.toml | 1 + crates/gpui2/src/action.rs | 40 ++++++++++++++++++++++ crates/gpui2/src/gpui2.rs | 1 + crates/gpui2_macros/Cargo.toml | 2 +- crates/gpui2_macros/src/register_action.rs | 27 ++++++++++++--- 6 files changed, 86 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bf2e964ea8..3b45a32918 100644 --- a/Cargo.lock +++ b/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" diff --git a/crates/gpui2/Cargo.toml b/crates/gpui2/Cargo.toml index df461af7b8..1bec9d43dc 100644 --- a/crates/gpui2/Cargo.toml +++ b/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 } diff --git a/crates/gpui2/src/action.rs b/crates/gpui2/src/action.rs index a81bcfcdbc..0a5ea781bd 100644 --- a/crates/gpui2/src/action.rs +++ b/crates/gpui2/src/action.rs @@ -200,3 +200,43 @@ macro_rules! actions { actions!($($rest)*); }; } + +/// 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 build: ActionBuilder, + pub type_id: TypeId, +} + +/// 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 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]; + +fn qualify_name(action_name: &'static str) -> SharedString { + 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::", "::").into() +} + +pub(crate) fn load_actions_2() { + let mut lock = ACTION_REGISTRY.write(); + + for action in __GPUI_ACTIONS { + let action = action(); + let name = qualify_name(action.name); + lock.builders_by_name.insert(name.clone(), action.build); + lock.names_by_type_id.insert(action.type_id, name.clone()); + lock.all_names.push(name); + } +} diff --git a/crates/gpui2/src/gpui2.rs b/crates/gpui2/src/gpui2.rs index 3b98b846c4..c2d10154de 100644 --- a/crates/gpui2/src/gpui2.rs +++ b/crates/gpui2/src/gpui2.rs @@ -49,6 +49,7 @@ 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::*; diff --git a/crates/gpui2_macros/Cargo.toml b/crates/gpui2_macros/Cargo.toml index eb44334095..aab669c1b7 100644 --- a/crates/gpui2_macros/Cargo.toml +++ b/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" diff --git a/crates/gpui2_macros/src/register_action.rs b/crates/gpui2_macros/src/register_action.rs index 68c39ad9bd..e6c47e8c52 100644 --- a/crates/gpui2_macros/src/register_action.rs +++ b/crates/gpui2_macros/src/register_action.rs @@ -18,14 +18,31 @@ use syn::{parse_macro_input, DeriveInput}; pub fn register_action(_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 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() + ); let expanded = quote! { #input - #[allow(non_snake_case)] - #[gpui::ctor] - fn #ctor_fn_name() { - gpui::register_action::<#type_name>() + + #[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, + } } }; From fa9f4a935554758ee9a3f17485ba3b50ebe913af Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Thu, 16 Nov 2023 13:43:36 -0500 Subject: [PATCH 09/37] Init rem_size in the workspace at the start of the render Co-Authored-By: Mikayla Maki --- crates/workspace2/src/workspace2.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index f28675661d..e09624bd2c 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -3614,7 +3614,16 @@ impl Render for Workspace { fn render(&mut self, cx: &mut ViewContext) -> Self::Element { let mut context = KeyContext::default(); context.add("Workspace"); - let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone(); + + let (ui_font, ui_font_size) = { + let theme_settings = ThemeSettings::get_global(cx); + ( + theme_settings.ui_font.family.clone(), + theme_settings.ui_font_size.clone(), + ) + }; + + cx.set_rem_size(ui_font_size); self.add_workspace_actions_listeners(div()) .key_context(context) From ffd092a098c54a8b8c4f8f497038b9f6e626d93f Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Thu, 16 Nov 2023 15:30:50 -0500 Subject: [PATCH 10/37] Add ui_font_* for tests --- crates/settings2/src/settings_file.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/settings2/src/settings_file.rs b/crates/settings2/src/settings_file.rs index 6f2c8d374f..fc4ad5882e 100644 --- a/crates/settings2/src/settings_file.rs +++ b/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, From 267e07472d4ee0c3070f884d4be9fc9966a7a6a3 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 16 Nov 2023 13:32:19 -0700 Subject: [PATCH 11/37] Checkpoint, MenuHandle can open one --- crates/gpui2/src/elements/overlay.rs | 10 +- crates/gpui2/src/window.rs | 1 + crates/workspace2/src/dock.rs | 133 +++++++++++++++++++++++++-- 3 files changed, 136 insertions(+), 8 deletions(-) diff --git a/crates/gpui2/src/elements/overlay.rs b/crates/gpui2/src/elements/overlay.rs index a190337f04..c45ea2a588 100644 --- a/crates/gpui2/src/elements/overlay.rs +++ b/crates/gpui2/src/elements/overlay.rs @@ -1,8 +1,8 @@ use smallvec::SmallVec; 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 { @@ -48,6 +48,12 @@ impl ParentComponent for Overlay { } } +impl Component for Overlay { + fn render(self) -> AnyElement { + AnyElement::new(self) + } +} + impl Element for Overlay { type ElementState = OverlayState; diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index b0d9d07df2..17bd4743b1 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -574,6 +574,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. diff --git a/crates/workspace2/src/dock.rs b/crates/workspace2/src/dock.rs index eec0ce309a..f2dd2c15cf 100644 --- a/crates/workspace2/src/dock.rs +++ b/crates/workspace2/src/dock.rs @@ -1,13 +1,16 @@ use crate::{status_bar::StatusItemView, Axis, Workspace}; use gpui::{ - div, px, Action, AnyView, AppContext, Component, Div, Entity, EntityId, EventEmitter, - FocusHandle, FocusableView, ParentComponent, Render, SharedString, Styled, Subscription, View, - ViewContext, WeakView, WindowContext, + div, overlay, point, px, Action, AnyElement, AnyView, AppContext, Component, DispatchPhase, + Div, Element, ElementId, Entity, EntityId, EventEmitter, FocusHandle, FocusableView, + InteractiveComponent, LayoutId, MouseButton, MouseDownEvent, ParentComponent, Pixels, Point, + Render, SharedString, Style, 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 smallvec::SmallVec; +use std::{cell::RefCell, rc::Rc, sync::Arc}; +use ui::{h_stack, IconButton, InteractionState, Label, Tooltip}; pub enum PanelEvent { ChangePosition, @@ -656,6 +659,118 @@ impl PanelButtons { // } // } +pub struct MenuHandle { + id: ElementId, + children: SmallVec<[AnyElement; 2]>, + builder: Rc) -> AnyView + 'static>, +} + +impl ParentComponent for MenuHandle { + fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { + &mut self.children + } +} + +impl MenuHandle { + fn new( + id: impl Into, + builder: impl Fn(&mut V, &mut ViewContext) -> AnyView + 'static, + ) -> Self { + Self { + id: id.into(), + children: SmallVec::new(), + builder: Rc::new(builder), + } + } +} + +pub struct MenuState { + open: Rc>, + menu: Option>, +} +// Here be dragons +impl Element for MenuHandle { + type ElementState = MenuState; + + fn element_id(&self) -> Option { + Some(self.id.clone()) + } + + fn layout( + &mut self, + view_state: &mut V, + element_state: Option, + cx: &mut crate::ViewContext, + ) -> (gpui::LayoutId, Self::ElementState) { + let mut child_layout_ids = self + .children + .iter_mut() + .map(|child| child.layout(view_state, cx)) + .collect::>(); + + let open = if let Some(element_state) = element_state { + element_state.open + } else { + Rc::new(RefCell::new(false)) + }; + + let mut menu = None; + if *open.borrow() { + let mut view = (self.builder)(view_state, cx).render(); + child_layout_ids.push(view.layout(view_state, cx)); + menu.replace(view); + } + let layout_id = cx.request_layout(&gpui::Style::default(), child_layout_ids.into_iter()); + + (layout_id, MenuState { open, menu }) + } + + fn paint( + &mut self, + bounds: crate::Bounds, + view_state: &mut V, + element_state: &mut Self::ElementState, + cx: &mut crate::ViewContext, + ) { + for child in &mut self.children { + child.paint(view_state, cx); + } + + if let Some(mut menu) = element_state.menu.as_mut() { + menu.paint(view_state, cx); + return; + } + + let open = element_state.open.clone(); + cx.on_mouse_event(move |view_state, event: &MouseDownEvent, phase, cx| { + dbg!(&event, &phase); + if phase == DispatchPhase::Bubble + && event.button == MouseButton::Right + && bounds.contains_point(&event.position) + { + *open.borrow_mut() = true; + cx.notify(); + } + }); + } +} + +impl Component for MenuHandle { + fn render(self) -> AnyElement { + AnyElement::new(self) + } +} + +struct TestMenu {} +impl Render for TestMenu { + type Element = Div; + + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + div().child("0MG!") + } +} + +// here be kittens impl Render for PanelButtons { type Element = Div; @@ -689,7 +804,13 @@ impl Render for PanelButtons { .tooltip(move |_, cx| Tooltip::for_action(name, &*action, cx)) }; - Some(button) + Some( + MenuHandle::new( + SharedString::from(format!("{} tooltip", name)), + move |_, cx| Tooltip::text("HELLOOOOOOOOOOOOOO", cx), + ) + .child(button), + ) }); h_stack().children(buttons) From c2d6d2495267f4ba78a04e41c38e1dfb095b1f72 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Thu, 16 Nov 2023 16:01:42 -0500 Subject: [PATCH 12/37] Ensure the titlebar stays large enough even with small ui sizes --- crates/collab_ui2/src/collab_titlebar_item.rs | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/crates/collab_ui2/src/collab_titlebar_item.rs b/crates/collab_ui2/src/collab_titlebar_item.rs index c9d16c7a5d..ca5118cd8d 100644 --- a/crates/collab_ui2/src/collab_titlebar_item.rs +++ b/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() - .when( - !matches!(cx.window_bounds(), WindowBounds::Fullscreen), - |s| s.pl_20(), - ) .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), + // 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.)), + ) .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() From 3d8e63b93bef7c0e79d0e389de81d70ddff1849c Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Thu, 16 Nov 2023 16:09:11 -0500 Subject: [PATCH 13/37] Buttons should always use `cursor_pointer` --- crates/ui2/src/components/button.rs | 1 + crates/ui2/src/components/icon_button.rs | 11 +++++------ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/ui2/src/components/button.rs b/crates/ui2/src/components/button.rs index 397ce4f4c4..de055bcd5c 100644 --- a/crates/ui2/src/components/button.rs +++ b/crates/ui2/src/components/button.rs @@ -178,6 +178,7 @@ impl Button { .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))); diff --git a/crates/ui2/src/components/icon_button.rs b/crates/ui2/src/components/icon_button.rs index 4408c51f62..5dd7771161 100644 --- a/crates/ui2/src/components/icon_button.rs +++ b/crates/ui2/src/components/icon_button.rs @@ -95,17 +95,16 @@ impl IconButton { .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() { From 9c5f580012820686c6789e64392760ac1ffb7c72 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Thu, 16 Nov 2023 16:17:10 -0500 Subject: [PATCH 14/37] Use `Selected` for active IconButtons --- crates/ui2/src/components/icon_button.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/ui2/src/components/icon_button.rs b/crates/ui2/src/components/icon_button.rs index 5dd7771161..5512da2b34 100644 --- a/crates/ui2/src/components/icon_button.rs +++ b/crates/ui2/src/components/icon_button.rs @@ -72,7 +72,7 @@ impl IconButton { fn render(mut self, _view: &mut V, cx: &mut ViewContext) -> impl Component { let icon_color = match (self.state, self.color) { (InteractionState::Disabled, _) => TextColor::Disabled, - (InteractionState::Active, _) => TextColor::Error, + (InteractionState::Active, _) => TextColor::Selected, _ => self.color, }; From 3223e21d9fcf12d8b22ae63c3d561a74177a4d5b Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Thu, 16 Nov 2023 16:17:17 -0500 Subject: [PATCH 15/37] Add dock borders --- crates/workspace2/src/dock.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/crates/workspace2/src/dock.rs b/crates/workspace2/src/dock.rs index 8e7f08252c..ee45ca862c 100644 --- a/crates/workspace2/src/dock.rs +++ b/crates/workspace2/src/dock.rs @@ -7,6 +7,7 @@ use gpui::{ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::sync::Arc; +use theme2::ActiveTheme; use ui::{h_stack, IconButton, InteractionState, Tooltip}; pub enum PanelEvent { @@ -443,10 +444,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() @@ -675,7 +682,7 @@ impl Render for PanelButtons { Some(button) }); - h_stack().children(buttons) + h_stack().gap_0p5().children(buttons) } } From 9456f716c2b47ab34e3d10f805e92bf0b0f686e6 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 16 Nov 2023 15:30:53 -0700 Subject: [PATCH 16/37] Only send one right click event --- crates/gpui2/src/elements/overlay.rs | 5 +++- crates/gpui2/src/platform/mac/window.rs | 36 +++++++++++++------------ 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/crates/gpui2/src/elements/overlay.rs b/crates/gpui2/src/elements/overlay.rs index c45ea2a588..69ac9c50dc 100644 --- a/crates/gpui2/src/elements/overlay.rs +++ b/crates/gpui2/src/elements/overlay.rs @@ -72,7 +72,10 @@ impl Element for Overlay { .iter_mut() .map(|child| child.layout(view_state, cx)) .collect::>(); - let layout_id = cx.request_layout(&Style::default(), child_layout_ids.iter().copied()); + let mut overlay_style = Style::default(); + overlay_style.position = crate::Position::Absolute; + + let layout_id = cx.request_layout(&overlay_style, child_layout_ids.iter().copied()); (layout_id, OverlayState { child_layout_ids }) } diff --git a/crates/gpui2/src/platform/mac/window.rs b/crates/gpui2/src/platform/mac/window.rs index d07df3d94b..03782d13a8 100644 --- a/crates/gpui2/src/platform/mac/window.rs +++ b/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); } } From 6397c05835886abbdc7ac47ce655cecbae8483f7 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 16 Nov 2023 14:09:01 -0800 Subject: [PATCH 17/37] Add the ability to deprioritize specific labeled tasks in tests --- crates/gpui2/src/executor.rs | 64 ++++++++++++---- crates/gpui2/src/platform.rs | 4 +- crates/gpui2/src/platform/mac/dispatcher.rs | 4 +- crates/gpui2/src/platform/test/dispatcher.rs | 79 +++++++++++++------- 4 files changed, 108 insertions(+), 43 deletions(-) diff --git a/crates/gpui2/src/executor.rs b/crates/gpui2/src/executor.rs index bb9b5d0d79..b29fbbb5a1 100644 --- a/crates/gpui2/src/executor.rs +++ b/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 Future for Task { } } } + +#[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 = Pin>>; + type AnyFuture = Pin>>; + impl BackgroundExecutor { pub fn new(dispatcher: Arc) -> 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(&self, future: impl Future + Send + 'static) -> Task where R: Send + 'static, { + self.spawn_internal::(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( + &self, + label: TaskLabel, + future: impl Future + Send + 'static, + ) -> Task + where + R: Send + 'static, + { + self.spawn_internal::(Box::pin(future), Some(label)) + } + + fn spawn_internal( + &self, + future: AnyFuture, + label: Option, + ) -> Task { let dispatcher = self.dispatcher.clone(); - fn inner( - dispatcher: Arc, - future: AnyFuture, - ) -> Task { - let (runnable, task) = - async_task::spawn(future, move |runnable| dispatcher.dispatch(runnable)); - runnable.schedule(); - Task::Spawned(task) - } - inner::(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"))] @@ -216,11 +244,21 @@ impl BackgroundExecutor { self.dispatcher.as_test().unwrap().simulate_random_delay() } + #[cfg(any(test, feature = "test-support"))] + pub fn deprioritize_task(&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 run_step(&self) -> bool { + self.dispatcher.as_test().unwrap().poll(false) + } + #[cfg(any(test, feature = "test-support"))] pub fn run_until_parked(&self) { self.dispatcher.as_test().unwrap().run_until_parked() diff --git a/crates/gpui2/src/platform.rs b/crates/gpui2/src/platform.rs index 00ce3340f8..882dc332ef 100644 --- a/crates/gpui2/src/platform.rs +++ b/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,7 +162,7 @@ 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); fn dispatch_on_main_thread(&self, runnable: Runnable); fn dispatch_after(&self, duration: Duration, runnable: Runnable); fn poll(&self, background_only: bool) -> bool; diff --git a/crates/gpui2/src/platform/mac/dispatcher.rs b/crates/gpui2/src/platform/mac/dispatcher.rs index 68c0e3b4f5..1752f78601 100644 --- a/crates/gpui2/src/platform/mac/dispatcher.rs +++ b/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) { unsafe { dispatch_async_f( dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT.try_into().unwrap(), 0), diff --git a/crates/gpui2/src/platform/test/dispatcher.rs b/crates/gpui2/src/platform/test/dispatcher.rs index 258c484063..3abe4796b3 100644 --- a/crates/gpui2/src/platform/test/dispatcher.rs +++ b/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>, background: Vec, + deprioritized_background: Vec, delayed: Vec<(Duration, Runnable)>, time: Duration, is_main_thread: bool, next_id: TestDispatcherId, allow_parking: bool, waiting_backtrace: Option, + deprioritized_task_labels: HashSet, } 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,6 +105,13 @@ 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) {} } @@ -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) { + { + 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(); } @@ -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 From f9650b3111f7a1ae071fd14adc4bc9554cf0545b Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 16 Nov 2023 14:09:33 -0800 Subject: [PATCH 18/37] Don't run until all the way until parked when waiting for a model's next event --- crates/gpui2/src/app/test_context.rs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/crates/gpui2/src/app/test_context.rs b/crates/gpui2/src/app/test_context.rs index 5397a2214d..37f81da15b 100644 --- a/crates/gpui2/src/app/test_context.rs +++ b/crates/gpui2/src/app/test_context.rs @@ -354,10 +354,18 @@ impl Model { }) }); - cx.executor().run_until_parked(); - rx.try_next() - .expect("no event received") - .expect("model was dropped") + loop { + match rx.try_next() { + Ok(Some(event)) => return event, + Ok(None) => panic!("model was dropped"), + Err(_) => { + if !cx.executor().run_step() { + break; + } + } + } + } + panic!("no event received") } } From 074a221e0f6a25603ba02ac24c9e5ac347436c5c Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 16 Nov 2023 16:59:27 -0700 Subject: [PATCH 19/37] Progress on ContextMenu --- crates/gpui2/src/elements/overlay.rs | 9 +- crates/terminal_view2/src/terminal_view.rs | 14 +- crates/ui2/src/components/context_menu.rs | 240 ++++++++++++++++++--- crates/ui2/src/components/list.rs | 24 ++- crates/workspace2/src/dock.rs | 118 +--------- 5 files changed, 247 insertions(+), 158 deletions(-) diff --git a/crates/gpui2/src/elements/overlay.rs b/crates/gpui2/src/elements/overlay.rs index 69ac9c50dc..8580ae3eb0 100644 --- a/crates/gpui2/src/elements/overlay.rs +++ b/crates/gpui2/src/elements/overlay.rs @@ -1,8 +1,9 @@ use smallvec::SmallVec; +use taffy::style::Position; use crate::{ - point, AnyElement, BorrowWindow, Bounds, Component, Element, LayoutId, ParentComponent, Pixels, - Point, Size, Style, + point, px, AbsoluteLength, AnyElement, BorrowWindow, Bounds, Component, Element, LayoutId, + ParentComponent, Pixels, Point, Size, Style, }; pub struct OverlayState { @@ -72,8 +73,9 @@ impl Element for Overlay { .iter_mut() .map(|child| child.layout(view_state, cx)) .collect::>(); + let mut overlay_style = Style::default(); - overlay_style.position = crate::Position::Absolute; + overlay_style.position = Position::Absolute; let layout_id = cx.request_layout(&overlay_style, child_layout_ids.iter().copied()); @@ -106,6 +108,7 @@ impl Element for Overlay { origin: Point::zero(), size: cx.viewport_size(), }; + dbg!(bounds, desired, limits); match self.fit_mode { OverlayFitMode::SnapToWindow => { diff --git a/crates/terminal_view2/src/terminal_view.rs b/crates/terminal_view2/src/terminal_view.rs index 14391ca2b2..4d77d172a6 100644 --- a/crates/terminal_view2/src/terminal_view.rs +++ b/crates/terminal_view2/src/terminal_view.rs @@ -87,7 +87,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: Option, + context_menu: Option>, blink_state: bool, blinking_on: bool, blinking_paused: bool, @@ -302,10 +302,14 @@ impl TerminalView { position: gpui::Point, cx: &mut ViewContext, ) { - self.context_menu = Some(ContextMenu::new(vec![ - ContextMenuItem::entry(Label::new("Clear"), Clear), - ContextMenuItem::entry(Label::new("Close"), CloseActiveItem { save_intent: None }), - ])); + 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 diff --git a/crates/ui2/src/components/context_menu.rs b/crates/ui2/src/components/context_menu.rs index 8f32c3ed56..8024d334b5 100644 --- a/crates/ui2/src/components/context_menu.rs +++ b/crates/ui2/src/components/context_menu.rs @@ -1,5 +1,13 @@ -use crate::{prelude::*, ListItemVariant}; +use std::cell::RefCell; +use std::rc::Rc; + +use crate::{h_stack, prelude::*, ListItemVariant}; use crate::{v_stack, Label, List, ListEntry, ListItem, ListSeparator, ListSubHeader}; +use gpui::{ + overlay, px, Action, AnyElement, Bounds, DispatchPhase, Div, EventEmitter, FocusHandle, + Focusable, FocusableView, LayoutId, MouseButton, MouseDownEvent, Overlay, Render, View, +}; +use smallvec::SmallVec; pub enum ContextMenuItem { Header(SharedString), @@ -19,12 +27,12 @@ impl Clone for ContextMenuItem { } } impl ContextMenuItem { - fn to_list_item(self) -> ListItem { + fn to_list_item(self) -> ListItem { match self { ContextMenuItem::Header(label) => ListSubHeader::new(label).into(), ContextMenuItem::Entry(label, action) => ListEntry::new(label) .variant(ListItemVariant::Inset) - .on_click(action) + .action(action) .into(), ContextMenuItem::Separator => ListSeparator::new().into(), } @@ -43,40 +51,196 @@ impl ContextMenuItem { } } -#[derive(Component, Clone)] pub struct ContextMenu { - items: Vec, + items: Vec, + focus_handle: FocusHandle, +} + +pub enum MenuEvent { + Dismissed, +} + +impl EventEmitter for ContextMenu {} +impl FocusableView for ContextMenu { + fn focus_handle(&self, cx: &gpui::AppContext) -> FocusHandle { + self.focus_handle.clone() + } } impl ContextMenu { - pub fn new(items: impl IntoIterator) -> Self { + pub fn new(cx: &mut WindowContext) -> Self { Self { - items: items.into_iter().collect(), + items: Default::default(), + focus_handle: cx.focus_handle(), } } - // todo!() - // cx.add_action(ContextMenu::select_first); - // cx.add_action(ContextMenu::select_last); - // cx.add_action(ContextMenu::select_next); - // cx.add_action(ContextMenu::select_prev); - // cx.add_action(ContextMenu::confirm); - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { - 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::) - .collect(), - )) - .on_mouse_down_out(|_, _, cx| cx.dispatch_action(Box::new(menu::Cancel))) + + pub fn header(mut self, title: impl Into) -> Self { + self.items.push(ListItem::Header(ListSubHeader::new(title))); + self + } + + pub fn separator(mut self) -> Self { + self.items.push(ListItem::Separator(ListSeparator)); + self + } + + pub fn entry(mut self, label: Label, action: Box) -> Self { + self.items.push(ListEntry::new(label).action(action).into()); + self + } + + pub fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { + // todo!() + cx.emit(MenuEvent::Dismissed); + } + + pub fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { + cx.emit(MenuEvent::Dismissed); + } +} + +impl Render for ContextMenu { + type Element = Overlay; + // todo!() + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + overlay().child( + 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())), + ), + ) + } +} + +pub struct MenuHandle { + id: ElementId, + children: SmallVec<[AnyElement; 2]>, + builder: Rc) -> View + 'static>, +} + +impl ParentComponent for MenuHandle { + fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { + &mut self.children + } +} + +impl MenuHandle { + pub fn new( + id: impl Into, + builder: impl Fn(&mut V, &mut ViewContext) -> View + 'static, + ) -> Self { + Self { + id: id.into(), + children: SmallVec::new(), + builder: Rc::new(builder), + } + } +} + +pub struct MenuHandleState { + menu: Rc>>>, + menu_element: Option>, +} +impl Element for MenuHandle { + type ElementState = MenuHandleState; + + fn element_id(&self) -> Option { + Some(self.id.clone()) + } + + fn layout( + &mut self, + view_state: &mut V, + element_state: Option, + cx: &mut crate::ViewContext, + ) -> (gpui::LayoutId, Self::ElementState) { + let mut child_layout_ids = self + .children + .iter_mut() + .map(|child| child.layout(view_state, cx)) + .collect::>(); + + let menu = if let Some(element_state) = element_state { + element_state.menu + } else { + Rc::new(RefCell::new(None)) + }; + + let menu_element = menu.borrow_mut().as_mut().map(|menu| { + let mut view = menu.clone().render(); + child_layout_ids.push(view.layout(view_state, cx)); + view + }); + + let layout_id = cx.request_layout(&gpui::Style::default(), child_layout_ids.into_iter()); + + (layout_id, MenuHandleState { menu, menu_element }) + } + + fn paint( + &mut self, + bounds: Bounds, + view_state: &mut V, + element_state: &mut Self::ElementState, + cx: &mut crate::ViewContext, + ) { + for child in &mut self.children { + child.paint(view_state, cx); + } + + if let Some(menu) = element_state.menu_element.as_mut() { + menu.paint(view_state, cx); + return; + } + + let menu = element_state.menu.clone(); + let builder = self.builder.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 { + MenuEvent::Dismissed => { + *menu2.borrow_mut() = None; + cx.notify(); + } + }) + .detach(); + *menu.borrow_mut() = Some(new_menu); + cx.notify(); + } + }); + } +} + +impl Component for MenuHandle { + fn render(self) -> AnyElement { + AnyElement::new(self) } } -use gpui::Action; #[cfg(feature = "stories")] pub use stories::*; @@ -84,7 +248,7 @@ pub use stories::*; mod stories { use super::*; use crate::story::Story; - use gpui::{action, Div, Render}; + use gpui::{action, Div, Render, VisualContext}; pub struct ContextMenuStory; @@ -97,17 +261,25 @@ mod stories { 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("Print current time"), PrintCurrentDate {}), - ])) .on_action(|_, _: &PrintCurrentDate, _| { if let Ok(unix_time) = std::time::UNIX_EPOCH.elapsed() { println!("Current Unix time is {:?}", unix_time.as_secs()); } }) + .child( + MenuHandle::new("test", move |_, cx| { + cx.build_view(|cx| { + ContextMenu::new(cx) + .header("Section header") + .separator() + .entry( + Label::new("Print current time"), + PrintCurrentDate {}.boxed_clone(), + ) + }) + }) + .child(Label::new("RIGHT CLICK ME")), + ) } } } diff --git a/crates/ui2/src/components/list.rs b/crates/ui2/src/components/list.rs index 4b355dd5b6..b9508c5413 100644 --- a/crates/ui2/src/components/list.rs +++ b/crates/ui2/src/components/list.rs @@ -117,7 +117,7 @@ impl ListHeader { } } -#[derive(Component)] +#[derive(Component, Clone)] pub struct ListSubHeader { label: SharedString, left_icon: Option, @@ -172,7 +172,7 @@ pub enum ListEntrySize { Medium, } -#[derive(Component)] +#[derive(Component, Clone)] pub enum ListItem { Entry(ListEntry), Separator(ListSeparator), @@ -234,6 +234,24 @@ pub struct ListEntry { on_click: Option>, } +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 { pub fn new(label: Label) -> Self { Self { @@ -249,7 +267,7 @@ impl ListEntry { } } - pub fn on_click(mut self, action: impl Into>) -> Self { + pub fn action(mut self, action: impl Into>) -> Self { self.on_click = Some(action.into()); self } diff --git a/crates/workspace2/src/dock.rs b/crates/workspace2/src/dock.rs index f2dd2c15cf..409385cafc 100644 --- a/crates/workspace2/src/dock.rs +++ b/crates/workspace2/src/dock.rs @@ -10,7 +10,10 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use smallvec::SmallVec; use std::{cell::RefCell, rc::Rc, sync::Arc}; -use ui::{h_stack, IconButton, InteractionState, Label, Tooltip}; +use ui::{ + h_stack, ContextMenu, ContextMenuItem, IconButton, InteractionState, Label, MenuEvent, + MenuHandle, Tooltip, +}; pub enum PanelEvent { ChangePosition, @@ -659,117 +662,6 @@ impl PanelButtons { // } // } -pub struct MenuHandle { - id: ElementId, - children: SmallVec<[AnyElement; 2]>, - builder: Rc) -> AnyView + 'static>, -} - -impl ParentComponent for MenuHandle { - fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { - &mut self.children - } -} - -impl MenuHandle { - fn new( - id: impl Into, - builder: impl Fn(&mut V, &mut ViewContext) -> AnyView + 'static, - ) -> Self { - Self { - id: id.into(), - children: SmallVec::new(), - builder: Rc::new(builder), - } - } -} - -pub struct MenuState { - open: Rc>, - menu: Option>, -} -// Here be dragons -impl Element for MenuHandle { - type ElementState = MenuState; - - fn element_id(&self) -> Option { - Some(self.id.clone()) - } - - fn layout( - &mut self, - view_state: &mut V, - element_state: Option, - cx: &mut crate::ViewContext, - ) -> (gpui::LayoutId, Self::ElementState) { - let mut child_layout_ids = self - .children - .iter_mut() - .map(|child| child.layout(view_state, cx)) - .collect::>(); - - let open = if let Some(element_state) = element_state { - element_state.open - } else { - Rc::new(RefCell::new(false)) - }; - - let mut menu = None; - if *open.borrow() { - let mut view = (self.builder)(view_state, cx).render(); - child_layout_ids.push(view.layout(view_state, cx)); - menu.replace(view); - } - let layout_id = cx.request_layout(&gpui::Style::default(), child_layout_ids.into_iter()); - - (layout_id, MenuState { open, menu }) - } - - fn paint( - &mut self, - bounds: crate::Bounds, - view_state: &mut V, - element_state: &mut Self::ElementState, - cx: &mut crate::ViewContext, - ) { - for child in &mut self.children { - child.paint(view_state, cx); - } - - if let Some(mut menu) = element_state.menu.as_mut() { - menu.paint(view_state, cx); - return; - } - - let open = element_state.open.clone(); - cx.on_mouse_event(move |view_state, event: &MouseDownEvent, phase, cx| { - dbg!(&event, &phase); - if phase == DispatchPhase::Bubble - && event.button == MouseButton::Right - && bounds.contains_point(&event.position) - { - *open.borrow_mut() = true; - cx.notify(); - } - }); - } -} - -impl Component for MenuHandle { - fn render(self) -> AnyElement { - AnyElement::new(self) - } -} - -struct TestMenu {} -impl Render for TestMenu { - type Element = Div; - - fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - div().child("0MG!") - } -} - // here be kittens impl Render for PanelButtons { type Element = Div; @@ -807,7 +699,7 @@ impl Render for PanelButtons { Some( MenuHandle::new( SharedString::from(format!("{} tooltip", name)), - move |_, cx| Tooltip::text("HELLOOOOOOOOOOOOOO", cx), + move |_, cx| cx.build_view(|cx| ContextMenu::new(cx).header("SECTION")), ) .child(button), ) From b2451d9dd688841e848f15a84aa1107fcc39e728 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 16 Nov 2023 14:44:32 -0800 Subject: [PATCH 20/37] Combine adjacent edits in buffer's diff --- crates/language2/src/buffer.rs | 61 ++++++++++++++++++++++++++-------- 1 file changed, 47 insertions(+), 14 deletions(-) diff --git a/crates/language2/src/buffer.rs b/crates/language2/src/buffer.rs index 2c8c55d577..d3f005278f 100644 --- a/crates/language2/src/buffer.rs +++ b/crates/language2/src/buffer.rs @@ -1122,26 +1122,59 @@ impl Buffer { 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 = "".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; + + let mut edits = Vec::new(); + let mut old_offset = 0; + let mut new_offset = 0; + let mut last_edit: Option<(Range, Range)> = 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::Delete => { - edits.push((offset..end_offset, empty.clone())); - offset = 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, From 89d73f713ad62b7645880536c841afe2e1b9c3d9 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 16 Nov 2023 15:51:13 -0800 Subject: [PATCH 21/37] Label the buffer's diff task so it can be deprioritized in tests --- crates/language2/src/buffer.rs | 118 ++++++++++++++++++--------------- 1 file changed, 63 insertions(+), 55 deletions(-) diff --git a/crates/language2/src/buffer.rs b/crates/language2/src/buffer.rs index d3f005278f..739fe8508d 100644 --- a/crates/language2/src/buffer.rs +++ b/crates/language2/src/buffer.rs @@ -17,7 +17,8 @@ use crate::{ use anyhow::{anyhow, Result}; pub use clock::ReplicaId; use futures::FutureExt as _; -use gpui::{AppContext, EventEmitter, HighlightStyle, ModelContext, Task}; +use gpui::{AppContext, EventEmitter, HighlightStyle, ModelContext, Task, TaskLabel}; +use lazy_static::lazy_static; use lsp::LanguageServerId; use parking_lot::Mutex; use similar::{ChangeTag, TextDiff}; @@ -51,6 +52,10 @@ 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, @@ -1118,69 +1123,72 @@ impl Buffer { pub fn diff(&self, mut new_text: String, cx: &AppContext) -> Task { 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); + 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 = "".into(); + let diff = TextDiff::from_chars(old_text.as_str(), new_text.as_str()); + let empty: Arc = "".into(); - let mut edits = Vec::new(); - let mut old_offset = 0; - let mut new_offset = 0; - let mut last_edit: Option<(Range, Range)> = 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)); + let mut edits = Vec::new(); + let mut old_offset = 0; + let mut new_offset = 0; + let mut last_edit: Option<(Range, Range)> = 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; } - 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)); + 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; } - new_offset = new_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; + } + } + } + + 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(); } } } - 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 From 5f1acae0d323cd7a17cc6f5f322d230e6c2f1d79 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 16 Nov 2023 15:52:24 -0800 Subject: [PATCH 22/37] Fix race conditions and bugs in Buffer::reload --- crates/language2/src/buffer.rs | 90 +++++++++++++++------------- crates/project2/src/project2.rs | 4 +- crates/project2/src/project_tests.rs | 64 ++++++++++++++++++++ crates/project2/src/worktree.rs | 3 +- crates/rope2/src/rope2.rs | 6 +- 5 files changed, 123 insertions(+), 44 deletions(-) diff --git a/crates/language2/src/buffer.rs b/crates/language2/src/buffer.rs index 739fe8508d..e191f408e7 100644 --- a/crates/language2/src/buffer.rs +++ b/crates/language2/src/buffer.rs @@ -16,7 +16,7 @@ use crate::{ }; use anyhow::{anyhow, Result}; pub use clock::ReplicaId; -use futures::FutureExt as _; +use futures::channel::oneshot; use gpui::{AppContext, EventEmitter, HighlightStyle, ModelContext, Task, TaskLabel}; use lazy_static::lazy_static; use lsp::LanguageServerId; @@ -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}; @@ -66,6 +66,7 @@ pub struct Buffer { saved_mtime: SystemTime, transaction_depth: usize, was_dirty_before_starting_transaction: Option, + reload_task: Option>>, language: Option>, autoindent_requests: Vec>, pending_autoindent: Option>, @@ -473,6 +474,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, @@ -572,37 +574,52 @@ impl Buffer { cx.notify(); } - pub fn reload(&mut self, cx: &mut ModelContext) -> Task>> { - 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, + ) -> oneshot::Receiver> { + 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(), + new_mtime, + cx, + ); + } + + this.reload_task.take(); + }) + })); + rx } pub fn did_reload( @@ -631,13 +648,8 @@ impl Buffer { cx.notify(); } - pub fn file_updated( - &mut self, - new_file: Arc, - cx: &mut ModelContext, - ) -> Task<()> { + pub fn file_updated(&mut self, new_file: Arc, cx: &mut ModelContext) { 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() { @@ -657,8 +669,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(); } } } @@ -672,7 +683,6 @@ impl Buffer { cx.emit(Event::FileHandleChanged); cx.notify(); } - task } pub fn diff_base(&self) -> Option<&str> { diff --git a/crates/project2/src/project2.rs b/crates/project2/src/project2.rs index 61ad500a73..f2e47b7184 100644 --- a/crates/project2/src/project2.rs +++ b/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); } diff --git a/crates/project2/src/project_tests.rs b/crates/project2/src/project_tests.rs index 97b6ed9c74..4c5905ff7e 100644 --- a/crates/project2/src/project_tests.rs +++ b/crates/project2/src/project_tests.rs @@ -2587,6 +2587,70 @@ async fn test_save_file(cx: &mut gpui::TestAppContext) { assert_eq!(new_text, buffer.update(cx, |buffer, _| buffer.text())); } +#[gpui::test(iterations = 10)] +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_task(*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 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 may be in a conflicting state. + else { + assert!( + buffer.is_dirty(), + "buffer should report that it is dirty. 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); diff --git a/crates/project2/src/worktree.rs b/crates/project2/src/worktree.rs index 9444dd9185..a020e8db4c 100644 --- a/crates/project2/src/worktree.rs +++ b/crates/project2/src/worktree.rs @@ -276,6 +276,7 @@ struct ShareState { _maintain_remote_snapshot: Task>, } +#[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); } })?; } diff --git a/crates/rope2/src/rope2.rs b/crates/rope2/src/rope2.rs index 9c764c468e..4cea1d4759 100644 --- a/crates/rope2/src/rope2.rs +++ b/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), } } } From 0bed5e4562b778ba129424c2d4b7e938cc332a9c Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 16 Nov 2023 16:01:49 -0800 Subject: [PATCH 23/37] Port buffer reload bug fixes back to gpui1 crates --- crates/copilot/src/copilot.rs | 20 ++++---- crates/language/src/buffer.rs | 90 +++++++++++++++++++--------------- crates/project/src/project.rs | 4 +- crates/project/src/worktree.rs | 2 +- crates/rope/src/rope.rs | 6 ++- 5 files changed, 67 insertions(+), 55 deletions(-) diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 3b383c2ac9..92d430e3fb 100644 --- a/crates/copilot/src/copilot.rs +++ b/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::() .await, diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 0194123bd2..6d5684e6d7 100644 --- a/crates/language/src/buffer.rs +++ b/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, + reload_task: Option>>, language: Option>, autoindent_requests: Vec>, pending_autoindent: Option>, @@ -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) -> Task>> { - 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, + ) -> oneshot::Receiver> { + 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(), + new_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, - cx: &mut ModelContext, - ) -> Task<()> { + pub fn file_updated(&mut self, new_file: Arc, cx: &mut ModelContext) { 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> { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 322b2ae894..ab6cbd88c0 100644 --- a/crates/project/src/project.rs +++ b/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); } diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 785ce58bb8..d59885225a 100644 --- a/crates/project/src/worktree.rs +++ b/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); } }); } diff --git a/crates/rope/src/rope.rs b/crates/rope/src/rope.rs index 9c764c468e..4cea1d4759 100644 --- a/crates/rope/src/rope.rs +++ b/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), } } } From 4de2c0f7ef5ce209f71c9f4afb71d2013d6c46d7 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Thu, 16 Nov 2023 17:32:02 -0800 Subject: [PATCH 24/37] Re-implement actions as derive macros instead of blanket impls --- crates/collab_ui2/src/collab_titlebar_item.rs | 6 +- crates/editor2/src/editor.rs | 28 +- crates/gpui2/src/action.rs | 264 +++++++----------- crates/gpui2/src/app.rs | 27 +- crates/gpui2/src/elements/div.rs | 10 +- crates/gpui2/src/gpui2.rs | 1 + crates/gpui2/src/key_dispatch.rs | 13 +- crates/gpui2/src/keymap/keymap.rs | 30 +- crates/gpui2/src/window.rs | 4 +- crates/gpui2/tests/action_macros.rs | 45 +++ crates/gpui2_macros/src/action.rs | 99 ++++--- crates/gpui2_macros/src/gpui2_macros.rs | 8 +- crates/gpui2_macros/src/register_action.rs | 55 +++- crates/live_kit_client2/examples/test_app.rs | 4 +- crates/settings2/src/keymap_file.rs | 4 +- crates/terminal_view2/src/terminal_view.rs | 8 +- crates/ui2/src/components/context_menu.rs | 5 +- crates/ui2/src/components/keybinding.rs | 5 +- crates/workspace2/src/pane.rs | 10 +- crates/workspace2/src/workspace2.rs | 13 +- crates/zed2/src/languages/json.rs | 2 +- crates/zed_actions2/src/lib.rs | 7 +- 22 files changed, 360 insertions(+), 288 deletions(-) create mode 100644 crates/gpui2/tests/action_macros.rs diff --git a/crates/collab_ui2/src/collab_titlebar_item.rs b/crates/collab_ui2/src/collab_titlebar_item.rs index c9d16c7a5d..a5a40c48b6 100644 --- a/crates/collab_ui2/src/collab_titlebar_item.rs +++ b/crates/collab_ui2/src/collab_titlebar_item.rs @@ -130,14 +130,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") diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 8e7bd5876f..8f4b9ccf64 100644 --- a/crates/editor2/src/editor.rs +++ b/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, Action)] pub struct SelectNext { #[serde(default)] pub replace_newest: bool, } -#[action] +#[derive(PartialEq, Clone, Deserialize, Action)] pub struct SelectPrevious { #[serde(default)] pub replace_newest: bool, } -#[action] +#[derive(PartialEq, Clone, Deserialize, Action)] pub struct SelectAllMatches { #[serde(default)] pub replace_newest: bool, } -#[action] +#[derive(PartialEq, Clone, Deserialize, Action)] pub struct SelectToBeginningOfLine { #[serde(default)] stop_at_soft_wraps: bool, } -#[action] +#[derive(PartialEq, Clone, Deserialize, Action)] pub struct MovePageUp { #[serde(default)] center_cursor: bool, } -#[action] +#[derive(PartialEq, Clone, Deserialize, Action)] pub struct MovePageDown { #[serde(default)] center_cursor: bool, } -#[action] +#[derive(PartialEq, Clone, Deserialize, Action)] pub struct SelectToEndOfLine { #[serde(default)] stop_at_soft_wraps: bool, } -#[action] +#[derive(PartialEq, Clone, Deserialize, Action)] pub struct ToggleCodeActions { #[serde(default)] pub deployed_from_indicator: bool, } -#[action] +#[derive(PartialEq, Clone, Deserialize, Action)] pub struct ConfirmCompletion { #[serde(default)] pub item_ix: Option, } -#[action] +#[derive(PartialEq, Clone, Deserialize, Action)] pub struct ConfirmCodeAction { #[serde(default)] pub item_ix: Option, } -#[action] +#[derive(PartialEq, Clone, Deserialize, Action)] pub struct ToggleComments { #[serde(default)] pub advance_downwards: bool, } -#[action] +#[derive(PartialEq, Clone, Deserialize, Action)] pub struct FoldAt { pub buffer_row: u32, } -#[action] +#[derive(PartialEq, Clone, Deserialize, Action)] pub struct UnfoldAt { pub buffer_row: u32, } diff --git a/crates/gpui2/src/action.rs b/crates/gpui2/src/action.rs index 0a5ea781bd..8656f31a5e 100644 --- a/crates/gpui2/src/action.rs +++ b/crates/gpui2/src/action.rs @@ -1,10 +1,9 @@ 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}; /// 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 +14,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 +32,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) -> Result> - where - Self: Sized; - fn is_registered() -> bool - where - Self: Sized; - - fn partial_eq(&self, action: &dyn Action) -> bool; +pub trait Action: 'static { fn boxed_clone(&self) -> Box; 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 build(value: serde_json::Value) -> Result> + where + Self: Sized; } -// Types become actions by satisfying a list of trait bounds. -impl Action for A -where - A: for<'a> Deserialize<'a> + PartialEq + Default + Clone + std::fmt::Debug + 'static, -{ - fn qualified_name() -> SharedString { - let name = type_name::(); - 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) -> Result> - 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::()) - .is_some() - } - - fn partial_eq(&self, action: &dyn Action) -> bool { - action - .as_any() - .downcast_ref::() - .map_or(false, |a| self == a) - } - - fn boxed_clone(&self) -> Box { - 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 +62,90 @@ 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) -> anyhow::Result>; +type ActionBuilder = fn(json: serde_json::Value) -> anyhow::Result>; -lazy_static! { - static ref ACTION_REGISTRY: RwLock = RwLock::default(); -} - -#[derive(Default)] -struct ActionRegistry { +pub(crate) struct ActionRegistry { builders_by_name: HashMap, names_by_type_id: HashMap, all_names: Vec, // So we can return a static slice. } -/// Register an action type to allow it to be referenced in keymaps. -pub fn register_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::(), 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> { - 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); +/// 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; - 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 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) -> Result> { - 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]; - let build_action = lock - .builders_by_name - .get(name) - .ok_or_else(|| anyhow!("no action type registered for {}", name))?; - (build_action)(params) -} +impl ActionRegistry { + /// Load all registered actions into the registry. + pub(crate) fn load_actions(&mut self) { + for builder in __GPUI_ACTIONS { + let action = builder(); + let name: SharedString = qualify_action(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); + } + } -pub fn all_action_names() -> MappedRwLockReadGuard<'static, [SharedString]> { - let lock = ACTION_REGISTRY.read(); - RwLockReadGuard::map(lock, |registry: &ActionRegistry| { - registry.all_names.as_slice() - }) + /// 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> { + let name = self + .names_by_type_id + .get(type_id) + .ok_or_else(|| anyhow!("no action type registered for {:?}", type_id))? + .clone(); + + 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, + ) -> Result> { + let build_action = self + .builders_by_name + .get(name) + .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(&self) -> &[SharedString] { + self.all_names.as_slice() + } } /// Defines unit structs that can be used as actions. @@ -191,7 +155,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; }; @@ -201,42 +165,20 @@ macro_rules! actions { }; } -/// 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. +/// This used by our macros to pre-process the action name deterministically #[doc(hidden)] -pub struct ActionData { - pub name: &'static str, - pub build: ActionBuilder, - pub type_id: TypeId, -} - -/// 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 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]; - -fn qualify_name(action_name: &'static str) -> SharedString { +pub fn qualify_action(action_name: &'static 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::", "::").into() + action_name[name_start_ix..] + .replace("2::", "::") + .to_string() } -pub(crate) fn load_actions_2() { - let mut lock = ACTION_REGISTRY.write(); +mod no_action { + use crate as gpui; - for action in __GPUI_ACTIONS { - let action = action(); - let name = qualify_name(action.name); - lock.builders_by_name.insert(name.clone(), action.build); - lock.names_by_type_id.insert(action.type_id, name.clone()); - lock.all_names.push(name); - } + actions!(NoAction); } diff --git a/crates/gpui2/src/app.rs b/crates/gpui2/src/app.rs index c76b62b510..b5083b97c2 100644 --- a/crates/gpui2/src/app.rs +++ b/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, flushing_effects: bool, pending_updates: usize, + pub(crate) actions: Rc, pub(crate) active_drag: Option, pub(crate) active_tooltip: Option, pub(crate) next_frame_callbacks: HashMap>, @@ -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, + ) -> Result> { + self.actions.build_action(name, data) + } + + pub fn all_action_names(&self) -> &[SharedString] { + self.actions.all_action_names() + } } impl Context for AppContext { diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index 31a8827109..ebbc34a48a 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -237,11 +237,11 @@ pub trait InteractiveComponent: Sized + Element { // // 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::(), Box::new(move |view, action, phase, cx| { diff --git a/crates/gpui2/src/gpui2.rs b/crates/gpui2/src/gpui2.rs index c2d10154de..88ecd52c03 100644 --- a/crates/gpui2/src/gpui2.rs +++ b/crates/gpui2/src/gpui2.rs @@ -55,6 +55,7 @@ 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; diff --git a/crates/gpui2/src/key_dispatch.rs b/crates/gpui2/src/key_dispatch.rs index 962a030844..5fbf83bfba 100644 --- a/crates/gpui2/src/key_dispatch.rs +++ b/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, keystroke_matchers: HashMap, KeystrokeMatcher>, keymap: Arc>, + action_registry: Rc, } #[derive(Default)] @@ -41,7 +41,7 @@ pub(crate) struct DispatchActionListener { } impl DispatchTree { - pub fn new(keymap: Arc>) -> Self { + pub fn new(keymap: Arc>, action_registry: Rc) -> 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()); } } } diff --git a/crates/gpui2/src/keymap/keymap.rs b/crates/gpui2/src/keymap/keymap.rs index 989ee7a8d5..8152693c07 100644 --- a/crates/gpui2/src/keymap/keymap.rs +++ b/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>(&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 { diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index b0d9d07df2..ff4c13abce 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -311,8 +311,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, diff --git a/crates/gpui2/tests/action_macros.rs b/crates/gpui2/tests/action_macros.rs new file mode 100644 index 0000000000..49064ffd86 --- /dev/null +++ b/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 { + 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> + where + Self: Sized, + { + todo!() + } + } +} diff --git a/crates/gpui2_macros/src/action.rs b/crates/gpui2_macros/src/action.rs index 564f35d6a4..abc75a8759 100644 --- a/crates/gpui2_macros/src/action.rs +++ b/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::>(); - 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 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 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::(value)?)) + } + }; + + let register_action = register_action(&name); + + let output = quote! { + const _: fn() = || { + fn assert_impl 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>() } - syn::Fields::Unit => { - quote! { - #attributes - #visibility struct #name; - } + + fn debug_name() -> &'static str + where + Self: ::std::marker::Sized + { + ::std::any::type_name::<#name>() } - }, - syn::Data::Enum(ref enum_data) => { - let variants = &enum_data.variants; - quote! { - #attributes - #visibility enum #name { #variants } + + fn build(value: gpui::serde_json::Value) -> gpui::Result<::std::boxed::Box> + where + Self: ::std::marker::Sized { + #build_impl + } + + fn partial_eq(&self, action: &dyn gpui::Action) -> bool { + action + .as_any() + .downcast_ref::() + .map_or(false, |a| self == a) + } + + fn boxed_clone(&self) -> std::boxed::Box { + ::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) diff --git a/crates/gpui2_macros/src/gpui2_macros.rs b/crates/gpui2_macros/src/gpui2_macros.rs index 80b67e1a12..3ce8373689 100644 --- a/crates/gpui2_macros/src/gpui2_macros.rs +++ b/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))] diff --git a/crates/gpui2_macros/src/register_action.rs b/crates/gpui2_macros/src/register_action.rs index e6c47e8c52..3d398c873c 100644 --- a/crates/gpui2_macros/src/register_action.rs +++ b/crates/gpui2_macros/src/register_action.rs @@ -12,13 +12,54 @@ // gpui2::register_action_builder::() // } 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 registration = register_action(&input.ident); + 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 + + #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()); @@ -27,9 +68,7 @@ pub fn register_action(_attr: TokenStream, item: TokenStream) -> TokenStream { type_name.to_string().to_lowercase() ); - let expanded = quote! { - #input - + quote! { #[doc(hidden)] #[gpui::linkme::distributed_slice(gpui::__GPUI_ACTIONS)] #[linkme(crate = gpui::linkme)] @@ -44,7 +83,5 @@ pub fn register_action(_attr: TokenStream, item: TokenStream) -> TokenStream { build: <#type_name as gpui::Action>::build, } } - }; - - TokenStream::from(expanded) + } } diff --git a/crates/live_kit_client2/examples/test_app.rs b/crates/live_kit_client2/examples/test_app.rs index 0b9e54f9b0..00aec53baf 100644 --- a/crates/live_kit_client2/examples/test_app.rs +++ b/crates/live_kit_client2/examples/test_app.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() { diff --git a/crates/settings2/src/keymap_file.rs b/crates/settings2/src/keymap_file.rs index 9f279864ee..93635935cb 100644 --- a/crates/settings2/src/keymap_file.rs +++ b/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:?}"))) diff --git a/crates/terminal_view2/src/terminal_view.rs b/crates/terminal_view2/src/terminal_view.rs index ad33716384..f815dbe0ea 100644 --- a/crates/terminal_view2/src/terminal_view.rs +++ b/crates/terminal_view2/src/terminal_view.rs @@ -9,7 +9,7 @@ pub mod terminal_panel; // use crate::terminal_element::TerminalElement; 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, MouseButton, ParentComponent, Pixels, Render, SharedString, Styled, Task, View, ViewContext, VisualContext, @@ -55,12 +55,10 @@ 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); diff --git a/crates/ui2/src/components/context_menu.rs b/crates/ui2/src/components/context_menu.rs index 8f32c3ed56..6b3371e338 100644 --- a/crates/ui2/src/components/context_menu.rs +++ b/crates/ui2/src/components/context_menu.rs @@ -84,7 +84,8 @@ pub use stories::*; mod stories { use super::*; use crate::story::Story; - use gpui::{action, Div, Render}; + use gpui::{Div, Render}; + use serde::Deserialize; pub struct ContextMenuStory; @@ -92,7 +93,7 @@ mod stories { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - #[action] + #[derive(PartialEq, Clone, Deserialize, gpui::Action)] struct PrintCurrentDate {} Story::container(cx) diff --git a/crates/ui2/src/components/keybinding.rs b/crates/ui2/src/components/keybinding.rs index 04e036f365..8da5273bf5 100644 --- a/crates/ui2/src/components/keybinding.rs +++ b/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) diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index dcc8a4a14f..b86240f419 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -7,7 +7,7 @@ use crate::{ use anyhow::Result; use collections::{HashMap, HashSet, VecDeque}; use gpui::{ - actions, prelude::*, register_action, AppContext, AsyncWindowContext, Component, Div, EntityId, + actions, prelude::*, Action, AppContext, AsyncWindowContext, Component, Div, EntityId, EventEmitter, FocusHandle, Focusable, FocusableView, Model, PromptLevel, Render, Task, View, ViewContext, VisualContext, WeakView, WindowContext, }; @@ -70,15 +70,13 @@ pub struct ActivateItem(pub usize); // pub pane: WeakView, // } -#[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, } -#[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, @@ -1917,7 +1915,7 @@ impl Render for Pane { .on_action(|pane: &mut Pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx)) .on_action(|pane: &mut Pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx)) .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)); }) diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index a146ad1822..ab7a69e75c 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -29,11 +29,11 @@ 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, Model, ModelContext, + ParentComponent, Point, Render, Size, Styled, Subscription, Task, View, ViewContext, + VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle, WindowOptions, }; use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem}; use itertools::Itertools; @@ -194,8 +194,7 @@ 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, } diff --git a/crates/zed2/src/languages/json.rs b/crates/zed2/src/languages/json.rs index cf9b33d968..f04f59cf6d 100644 --- a/crates/zed2/src/languages/json.rs +++ b/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::().json_schema( diff --git a/crates/zed_actions2/src/lib.rs b/crates/zed_actions2/src/lib.rs index 7f0c19853e..456d1f5973 100644 --- a/crates/zed_actions2/src/lib.rs +++ b/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, } From 49d3e1cc4bebc0e66b1c995aa7b38456e2f0a6e9 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Thu, 16 Nov 2023 17:39:05 -0800 Subject: [PATCH 25/37] Add default derive --- crates/editor2/src/editor.rs | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 8f4b9ccf64..8d394270a7 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -180,78 +180,78 @@ pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2); // // .with_soft_wrap(true) // } -#[derive(PartialEq, Clone, Deserialize, Action)] +#[derive(PartialEq, Clone, Deserialize, Default, Action)] pub struct SelectNext { #[serde(default)] pub replace_newest: bool, } -#[derive(PartialEq, Clone, Deserialize, Action)] +#[derive(PartialEq, Clone, Deserialize, Default, Action)] pub struct SelectPrevious { #[serde(default)] pub replace_newest: bool, } -#[derive(PartialEq, Clone, Deserialize, Action)] +#[derive(PartialEq, Clone, Deserialize, Default, Action)] pub struct SelectAllMatches { #[serde(default)] pub replace_newest: bool, } -#[derive(PartialEq, Clone, Deserialize, Action)] +#[derive(PartialEq, Clone, Deserialize, Default, Action)] pub struct SelectToBeginningOfLine { #[serde(default)] stop_at_soft_wraps: bool, } -#[derive(PartialEq, Clone, Deserialize, Action)] +#[derive(PartialEq, Clone, Deserialize, Default, Action)] pub struct MovePageUp { #[serde(default)] center_cursor: bool, } -#[derive(PartialEq, Clone, Deserialize, Action)] +#[derive(PartialEq, Clone, Deserialize, Default, Action)] pub struct MovePageDown { #[serde(default)] center_cursor: bool, } -#[derive(PartialEq, Clone, Deserialize, Action)] +#[derive(PartialEq, Clone, Deserialize, Default, Action)] pub struct SelectToEndOfLine { #[serde(default)] stop_at_soft_wraps: bool, } -#[derive(PartialEq, Clone, Deserialize, Action)] +#[derive(PartialEq, Clone, Deserialize, Default, Action)] pub struct ToggleCodeActions { #[serde(default)] pub deployed_from_indicator: bool, } -#[derive(PartialEq, Clone, Deserialize, Action)] +#[derive(PartialEq, Clone, Deserialize, Default, Action)] pub struct ConfirmCompletion { #[serde(default)] pub item_ix: Option, } -#[derive(PartialEq, Clone, Deserialize, Action)] +#[derive(PartialEq, Clone, Deserialize, Default, Action)] pub struct ConfirmCodeAction { #[serde(default)] pub item_ix: Option, } -#[derive(PartialEq, Clone, Deserialize, Action)] +#[derive(PartialEq, Clone, Deserialize, Default, Action)] pub struct ToggleComments { #[serde(default)] pub advance_downwards: bool, } -#[derive(PartialEq, Clone, Deserialize, Action)] +#[derive(PartialEq, Clone, Deserialize, Default, Action)] pub struct FoldAt { pub buffer_row: u32, } -#[derive(PartialEq, Clone, Deserialize, Action)] +#[derive(PartialEq, Clone, Deserialize, Default, Action)] pub struct UnfoldAt { pub buffer_row: u32, } From 17b2b112bc0f1f1af59e3dae6cde37752837dac1 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 16 Nov 2023 18:02:49 -0800 Subject: [PATCH 26/37] Don't update file's saved mtime when reload is aborted --- crates/language/src/buffer.rs | 2 +- crates/language2/src/buffer.rs | 30 ++++++++++++++++------------ crates/project2/src/project_tests.rs | 13 +++++++----- 3 files changed, 26 insertions(+), 19 deletions(-) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 6d5684e6d7..7feffbf3ed 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -646,7 +646,7 @@ impl Buffer { prev_version, Rope::text_fingerprint(&new_text), this.line_ending(), - new_mtime, + this.saved_mtime, cx, ); } diff --git a/crates/language2/src/buffer.rs b/crates/language2/src/buffer.rs index e191f408e7..51ed192b99 100644 --- a/crates/language2/src/buffer.rs +++ b/crates/language2/src/buffer.rs @@ -61,9 +61,14 @@ pub struct Buffer { diff_base: Option, git_diff: git::diff::BufferDiff, file: Option>, - 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, reload_task: Option>>, @@ -386,8 +391,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"))? @@ -403,7 +407,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()), } } @@ -473,7 +477,7 @@ 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, @@ -540,7 +544,7 @@ impl Buffer { } pub fn saved_version_fingerprint(&self) -> RopeFingerprint { - self.saved_version_fingerprint + self.file_fingerprint } pub fn saved_mtime(&self) -> SystemTime { @@ -568,7 +572,7 @@ impl Buffer { cx: &mut ModelContext, ) { self.saved_version = version; - self.saved_version_fingerprint = fingerprint; + self.file_fingerprint = fingerprint; self.saved_mtime = mtime; cx.emit(Event::Saved); cx.notify(); @@ -611,7 +615,7 @@ impl Buffer { prev_version, Rope::text_fingerprint(&new_text), this.line_ending(), - new_mtime, + this.saved_mtime, cx, ); } @@ -631,14 +635,14 @@ impl Buffer { cx: &mut ModelContext, ) { 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, @@ -1282,12 +1286,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() diff --git a/crates/project2/src/project_tests.rs b/crates/project2/src/project_tests.rs index 4c5905ff7e..e607b30766 100644 --- a/crates/project2/src/project_tests.rs +++ b/crates/project2/src/project_tests.rs @@ -2587,7 +2587,7 @@ async fn test_save_file(cx: &mut gpui::TestAppContext) { assert_eq!(new_text, buffer.update(cx, |buffer, _| buffer.text())); } -#[gpui::test(iterations = 10)] +#[gpui::test(iterations = 30)] async fn test_file_changes_multiple_times_on_disk(cx: &mut gpui::TestAppContext) { init_test(cx); @@ -2638,14 +2638,17 @@ async fn test_file_changes_multiple_times_on_disk(cx: &mut gpui::TestAppContext) buffer.read_with(cx, |buffer, _| { let buffer_text = buffer.text(); if buffer_text == on_disk_text { - assert!(!buffer.is_dirty(), "buffer shouldn't be dirty. text: {buffer_text:?}, disk 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 may be in a conflicting state. + // change, the buffer will be in a conflicting state. else { assert!( - buffer.is_dirty(), - "buffer should report that it is dirty. text: {buffer_text:?}, disk text: {on_disk_text:?}" + buffer.is_dirty() && buffer.has_conflict(), + "buffer should report that it has a conflict. text: {buffer_text:?}, disk text: {on_disk_text:?}" ); } }); From 432572c592c4d735a218be0e04d5757641bde97f Mon Sep 17 00:00:00 2001 From: Mikayla Date: Thu, 16 Nov 2023 18:04:35 -0800 Subject: [PATCH 27/37] #RemoveThe2 --- crates/command_palette2/src/command_palette.rs | 2 +- crates/gpui2/src/action.rs | 17 +++++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/crates/command_palette2/src/command_palette.rs b/crates/command_palette2/src/command_palette.rs index 31eb608ef5..6264606ed9 100644 --- a/crates/command_palette2/src/command_palette.rs +++ b/crates/command_palette2/src/command_palette.rs @@ -47,7 +47,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; diff --git a/crates/gpui2/src/action.rs b/crates/gpui2/src/action.rs index 8656f31a5e..958eaabdb8 100644 --- a/crates/gpui2/src/action.rs +++ b/crates/gpui2/src/action.rs @@ -3,7 +3,10 @@ use anyhow::{anyhow, Context, Result}; use collections::HashMap; pub use no_action::NoAction; use serde_json::json; -use std::any::{Any, TypeId}; +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 @@ -111,7 +114,8 @@ impl ActionRegistry { pub(crate) fn load_actions(&mut self) { for builder in __GPUI_ACTIONS { let action = builder(); - let name: SharedString = qualify_action(action.name).into(); + //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); @@ -135,9 +139,11 @@ impl ActionRegistry { name: &str, params: Option, ) -> Result> { + //todo(remove) + let name = remove_the_2(name); let build_action = self .builders_by_name - .get(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)) @@ -165,9 +171,8 @@ macro_rules! actions { }; } -/// This used by our macros to pre-process the action name deterministically -#[doc(hidden)] -pub fn qualify_action(action_name: &'static str) -> String { +//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); From 547888942f59db89b4936679546e33796750a828 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 16 Nov 2023 19:39:59 -0700 Subject: [PATCH 28/37] Add storybook3 --- Cargo.lock | 11 +++++ Cargo.toml | 1 + crates/storybook2/src/storybook2.rs | 2 +- crates/storybook3/Cargo.toml | 17 +++++++ crates/storybook3/src/storybook3.rs | 75 +++++++++++++++++++++++++++++ crates/ui2/src/story.rs | 1 - 6 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 crates/storybook3/Cargo.toml create mode 100644 crates/storybook3/src/storybook3.rs diff --git a/Cargo.lock b/Cargo.lock index bf2e964ea8..a364cbce64 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8802,6 +8802,17 @@ dependencies = [ "util", ] +[[package]] +name = "storybook3" +version = "0.1.0" +dependencies = [ + "anyhow", + "gpui2", + "settings2", + "theme2", + "ui2", +] + [[package]] name = "stringprep" version = "0.1.4" diff --git a/Cargo.toml b/Cargo.toml index f8d0af77fa..f107dc5390 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -95,6 +95,7 @@ members = [ "crates/sqlez_macros", "crates/rich_text", "crates/storybook2", + "crates/storybook3", "crates/sum_tree", "crates/terminal", "crates/terminal2", diff --git a/crates/storybook2/src/storybook2.rs b/crates/storybook2/src/storybook2.rs index c4c1d75eac..1481ab9fb4 100644 --- a/crates/storybook2/src/storybook2.rs +++ b/crates/storybook2/src/storybook2.rs @@ -66,7 +66,6 @@ fn main() { story_selector.unwrap_or(StorySelector::Component(ComponentStory::Workspace)); let theme_registry = cx.global::(); - 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()) } } diff --git a/crates/storybook3/Cargo.toml b/crates/storybook3/Cargo.toml new file mode 100644 index 0000000000..8b04e4d44b --- /dev/null +++ b/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"} diff --git a/crates/storybook3/src/storybook3.rs b/crates/storybook3/src/storybook3.rs new file mode 100644 index 0000000000..b73bb8c563 --- /dev/null +++ b/crates/storybook3/src/storybook3.rs @@ -0,0 +1,75 @@ +use anyhow::Result; +use gpui::AssetSource; +use gpui::{ + div, hsla, 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> { + todo!(); + } + + fn list(&self, _path: &str) -> Result> { + 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(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; + + fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { + div() + .p(px(10.)) + .bg(hsla(1., 1., 1., 0.)) + .flex() + .flex_col() + .size_full() + .font("Helvetica") + .child(self.story.clone()) + } +} diff --git a/crates/ui2/src/story.rs b/crates/ui2/src/story.rs index 94e38267f4..c98cfa012f 100644 --- a/crates/ui2/src/story.rs +++ b/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) } From 9547e88d883d1f0187c2268a981aae2bb6c2d1a2 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 16 Nov 2023 19:50:31 -0700 Subject: [PATCH 29/37] TEMP --- crates/gpui2/src/elements/overlay.rs | 8 ++++---- crates/ui2/src/components/context_menu.rs | 6 +++--- crates/ui2/src/styled_ext.rs | 1 + 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/crates/gpui2/src/elements/overlay.rs b/crates/gpui2/src/elements/overlay.rs index 8580ae3eb0..4d3e8fdbf7 100644 --- a/crates/gpui2/src/elements/overlay.rs +++ b/crates/gpui2/src/elements/overlay.rs @@ -1,9 +1,9 @@ use smallvec::SmallVec; -use taffy::style::Position; +use taffy::style::{Display, Position}; use crate::{ - point, px, AbsoluteLength, AnyElement, BorrowWindow, Bounds, Component, Element, LayoutId, - ParentComponent, Pixels, Point, Size, Style, + point, AnyElement, BorrowWindow, Bounds, Component, Element, LayoutId, ParentComponent, Pixels, + Point, Size, Style, }; pub struct OverlayState { @@ -76,6 +76,7 @@ impl Element for Overlay { 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()); @@ -108,7 +109,6 @@ impl Element for Overlay { origin: Point::zero(), size: cx.viewport_size(), }; - dbg!(bounds, desired, limits); match self.fit_mode { OverlayFitMode::SnapToWindow => { diff --git a/crates/ui2/src/components/context_menu.rs b/crates/ui2/src/components/context_menu.rs index 8024d334b5..5d4974e631 100644 --- a/crates/ui2/src/components/context_menu.rs +++ b/crates/ui2/src/components/context_menu.rs @@ -1,11 +1,11 @@ use std::cell::RefCell; use std::rc::Rc; -use crate::{h_stack, prelude::*, ListItemVariant}; +use crate::{prelude::*, ListItemVariant}; use crate::{v_stack, Label, List, ListEntry, ListItem, ListSeparator, ListSubHeader}; use gpui::{ - overlay, px, Action, AnyElement, Bounds, DispatchPhase, Div, EventEmitter, FocusHandle, - Focusable, FocusableView, LayoutId, MouseButton, MouseDownEvent, Overlay, Render, View, + overlay, px, Action, AnyElement, Bounds, DispatchPhase, EventEmitter, FocusHandle, + FocusableView, LayoutId, MouseButton, MouseDownEvent, Overlay, Render, View, }; use smallvec::SmallVec; diff --git a/crates/ui2/src/styled_ext.rs b/crates/ui2/src/styled_ext.rs index d9911e6833..9037682807 100644 --- a/crates/ui2/src/styled_ext.rs +++ b/crates/ui2/src/styled_ext.rs @@ -5,6 +5,7 @@ use crate::{ElevationIndex, UITextSize}; fn elevated(this: E, cx: &mut ViewContext, 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) From f3b6719c76a9d540470fca417491ec5c2f1d21d4 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 16 Nov 2023 19:58:18 -0800 Subject: [PATCH 30/37] Rename both PlatformDispatcher::poll and Executor::run_step to 'tick' Co-authored-by: Nathan Sobo --- crates/gpui2/src/app/test_context.rs | 2 +- crates/gpui2/src/executor.rs | 6 +++--- crates/gpui2/src/platform.rs | 2 +- crates/gpui2/src/platform/mac/dispatcher.rs | 2 +- crates/gpui2/src/platform/test/dispatcher.rs | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/gpui2/src/app/test_context.rs b/crates/gpui2/src/app/test_context.rs index 37f81da15b..f78162589e 100644 --- a/crates/gpui2/src/app/test_context.rs +++ b/crates/gpui2/src/app/test_context.rs @@ -359,7 +359,7 @@ impl Model { Ok(Some(event)) => return event, Ok(None) => panic!("model was dropped"), Err(_) => { - if !cx.executor().run_step() { + if !cx.executor().tick() { break; } } diff --git a/crates/gpui2/src/executor.rs b/crates/gpui2/src/executor.rs index b29fbbb5a1..21b67b445d 100644 --- a/crates/gpui2/src/executor.rs +++ b/crates/gpui2/src/executor.rs @@ -158,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; } @@ -255,8 +255,8 @@ impl BackgroundExecutor { } #[cfg(any(test, feature = "test-support"))] - pub fn run_step(&self) -> bool { - self.dispatcher.as_test().unwrap().poll(false) + pub fn tick(&self) -> bool { + self.dispatcher.as_test().unwrap().tick(false) } #[cfg(any(test, feature = "test-support"))] diff --git a/crates/gpui2/src/platform.rs b/crates/gpui2/src/platform.rs index 882dc332ef..3027c05fbd 100644 --- a/crates/gpui2/src/platform.rs +++ b/crates/gpui2/src/platform.rs @@ -165,7 +165,7 @@ pub trait PlatformDispatcher: Send + Sync { fn dispatch(&self, runnable: Runnable, label: Option); 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; diff --git a/crates/gpui2/src/platform/mac/dispatcher.rs b/crates/gpui2/src/platform/mac/dispatcher.rs index 1752f78601..2fb0eef3e5 100644 --- a/crates/gpui2/src/platform/mac/dispatcher.rs +++ b/crates/gpui2/src/platform/mac/dispatcher.rs @@ -71,7 +71,7 @@ impl PlatformDispatcher for MacDispatcher { } } - fn poll(&self, _background_only: bool) -> bool { + fn tick(&self, _background_only: bool) -> bool { false } diff --git a/crates/gpui2/src/platform/test/dispatcher.rs b/crates/gpui2/src/platform/test/dispatcher.rs index 3abe4796b3..e77c1c0529 100644 --- a/crates/gpui2/src/platform/test/dispatcher.rs +++ b/crates/gpui2/src/platform/test/dispatcher.rs @@ -113,7 +113,7 @@ impl TestDispatcher { } pub fn run_until_parked(&self) { - while self.poll(false) {} + while self.tick(false) {} } pub fn parking_allowed(&self) -> bool { @@ -194,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() { From 32979f3acad549c9c2db7ba530ae5c377bf8f19c Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 16 Nov 2023 20:03:18 -0800 Subject: [PATCH 31/37] Rename deprioritize_task -> deprioritize It applies to a family of tasks, not a task. --- crates/gpui2/src/app/test_context.rs | 1 + crates/gpui2/src/executor.rs | 2 +- crates/project2/src/project_tests.rs | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/gpui2/src/app/test_context.rs b/crates/gpui2/src/app/test_context.rs index f78162589e..8b26bb1815 100644 --- a/crates/gpui2/src/app/test_context.rs +++ b/crates/gpui2/src/app/test_context.rs @@ -354,6 +354,7 @@ impl Model { }) }); + // Run other tasks until the event is emitted. loop { match rx.try_next() { Ok(Some(event)) => return event, diff --git a/crates/gpui2/src/executor.rs b/crates/gpui2/src/executor.rs index 21b67b445d..cf138a90db 100644 --- a/crates/gpui2/src/executor.rs +++ b/crates/gpui2/src/executor.rs @@ -245,7 +245,7 @@ impl BackgroundExecutor { } #[cfg(any(test, feature = "test-support"))] - pub fn deprioritize_task(&self, task_label: TaskLabel) { + pub fn deprioritize(&self, task_label: TaskLabel) { self.dispatcher.as_test().unwrap().deprioritize(task_label) } diff --git a/crates/project2/src/project_tests.rs b/crates/project2/src/project_tests.rs index e607b30766..9eb9a49e49 100644 --- a/crates/project2/src/project_tests.rs +++ b/crates/project2/src/project_tests.rs @@ -2609,7 +2609,7 @@ async fn test_file_changes_multiple_times_on_disk(cx: &mut gpui::TestAppContext) // Simulate buffer diffs being slow, so that they don't complete before // the next file change occurs. - cx.executor().deprioritize_task(*language::BUFFER_DIFF_TASK); + 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. From c0ad15756cfef2bdf6f82eccce180af32b6843fd Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 16 Nov 2023 21:45:22 -0700 Subject: [PATCH 32/37] More attachment configuration for context menus --- crates/gpui2/src/elements/overlay.rs | 21 +- crates/gpui2/src/window.rs | 8 + crates/terminal_view2/src/terminal_view.rs | 2 +- crates/ui2/src/components/context_menu.rs | 315 +++++++++++++-------- crates/ui2/src/components/icon_button.rs | 17 +- crates/workspace2/src/dock.rs | 34 ++- 6 files changed, 266 insertions(+), 131 deletions(-) diff --git a/crates/gpui2/src/elements/overlay.rs b/crates/gpui2/src/elements/overlay.rs index 4d3e8fdbf7..14a8048d39 100644 --- a/crates/gpui2/src/elements/overlay.rs +++ b/crates/gpui2/src/elements/overlay.rs @@ -15,7 +15,7 @@ pub struct Overlay { anchor_corner: AnchorCorner, fit_mode: OverlayFitMode, // todo!(); - // anchor_position: Option, + anchor_position: Option>, // position_mode: OverlayPositionMode, } @@ -26,6 +26,7 @@ pub fn overlay() -> Overlay { children: SmallVec::new(), anchor_corner: AnchorCorner::TopLeft, fit_mode: OverlayFitMode::SwitchAnchor, + anchor_position: None, } } @@ -36,6 +37,13 @@ impl Overlay { self } + /// Sets the position in window co-ordinates + /// (otherwise the location the overlay is rendered is used) + pub fn position(mut self, anchor: Point) -> 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; @@ -102,7 +110,7 @@ impl Element for Overlay { child_max = child_max.max(&child_bounds.lower_right()); } let size: Size = (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 { @@ -196,6 +204,15 @@ impl AnchorCorner { Bounds { origin, size } } + pub fn corner(&self, bounds: Bounds) -> Point { + 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 { diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 17bd4743b1..b8066d4889 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -1151,6 +1151,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 { diff --git a/crates/terminal_view2/src/terminal_view.rs b/crates/terminal_view2/src/terminal_view.rs index 4d77d172a6..80bf65aec4 100644 --- a/crates/terminal_view2/src/terminal_view.rs +++ b/crates/terminal_view2/src/terminal_view.rs @@ -32,7 +32,7 @@ use workspace::{ notifications::NotifyResultExt, register_deserializable_item, searchable::{SearchEvent, SearchOptions, SearchableItem}, - ui::{ContextMenu, ContextMenuItem, Label}, + ui::{ContextMenu, Label}, CloseActiveItem, NewCenterTerminal, Pane, ToolbarItemLocation, Workspace, WorkspaceId, }; diff --git a/crates/ui2/src/components/context_menu.rs b/crates/ui2/src/components/context_menu.rs index 5d4974e631..112e1224f9 100644 --- a/crates/ui2/src/components/context_menu.rs +++ b/crates/ui2/src/components/context_menu.rs @@ -1,56 +1,14 @@ use std::cell::RefCell; use std::rc::Rc; -use crate::{prelude::*, ListItemVariant}; +use crate::prelude::*; use crate::{v_stack, Label, List, ListEntry, ListItem, ListSeparator, ListSubHeader}; use gpui::{ - overlay, px, Action, AnyElement, Bounds, DispatchPhase, EventEmitter, FocusHandle, - FocusableView, LayoutId, MouseButton, MouseDownEvent, Overlay, Render, View, + overlay, px, Action, AnchorCorner, AnyElement, Bounds, DispatchPhase, Div, EventEmitter, + FocusHandle, FocusableView, LayoutId, MouseButton, MouseDownEvent, Pixels, Point, Render, View, }; use smallvec::SmallVec; -pub enum ContextMenuItem { - Header(SharedString), - Entry(Label, Box), - Separator, -} - -impl Clone for ContextMenuItem { - fn clone(&self) -> Self { - match self { - ContextMenuItem::Header(name) => ContextMenuItem::Header(name.clone()), - ContextMenuItem::Entry(label, action) => { - ContextMenuItem::Entry(label.clone(), action.boxed_clone()) - } - ContextMenuItem::Separator => ContextMenuItem::Separator, - } - } -} -impl ContextMenuItem { - fn to_list_item(self) -> ListItem { - match self { - ContextMenuItem::Header(label) => ListSubHeader::new(label).into(), - ContextMenuItem::Entry(label, action) => ListEntry::new(label) - .variant(ListItemVariant::Inset) - .action(action) - .into(), - ContextMenuItem::Separator => ListSeparator::new().into(), - } - } - - pub fn header(label: impl Into) -> Self { - Self::Header(label.into()) - } - - pub fn separator() -> Self { - Self::Separator - } - - pub fn entry(label: Label, action: impl Action) -> Self { - Self::Entry(label, Box::new(action)) - } -} - pub struct ContextMenu { items: Vec, focus_handle: FocusHandle, @@ -101,67 +59,93 @@ impl ContextMenu { } impl Render for ContextMenu { - type Element = Overlay; + type Element = Div; // todo!() fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - overlay().child( - 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())), - ), + 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())), ) } } pub struct MenuHandle { - id: ElementId, - children: SmallVec<[AnyElement; 2]>, - builder: Rc) -> View + 'static>, -} + id: Option, + child_builder: Option AnyElement + 'static>>, + menu_builder: Option) -> View + 'static>>, -impl ParentComponent for MenuHandle { - fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { - &mut self.children - } + anchor: Option, + attach: Option, } impl MenuHandle { - pub fn new( - id: impl Into, - builder: impl Fn(&mut V, &mut ViewContext) -> View + 'static, + pub fn id(mut self, id: impl Into) -> Self { + self.id = Some(id.into()); + self + } + + pub fn menu( + mut self, + f: impl Fn(&mut V, &mut ViewContext) -> View + 'static, ) -> Self { - Self { - id: id.into(), - children: SmallVec::new(), - builder: Rc::new(builder), - } + self.menu_builder = Some(Rc::new(f)); + self + } + + pub fn child>(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() -> MenuHandle { + MenuHandle { + id: None, + child_builder: None, + menu_builder: None, + anchor: None, + attach: None, } } pub struct MenuHandleState { menu: Rc>>>, + position: Rc>>, + child_layout_id: Option, + child_element: Option>, menu_element: Option>, } impl Element for MenuHandle { type ElementState = MenuHandleState; fn element_id(&self) -> Option { - Some(self.id.clone()) + Some(self.id.clone().expect("menu_handle must have an id()")) } fn layout( @@ -170,27 +154,50 @@ impl Element for MenuHandle { element_state: Option, cx: &mut crate::ViewContext, ) -> (gpui::LayoutId, Self::ElementState) { - let mut child_layout_ids = self - .children - .iter_mut() - .map(|child| child.layout(view_state, cx)) - .collect::>(); - - let menu = if let Some(element_state) = element_state { - element_state.menu + let (menu, position) = if let Some(element_state) = element_state { + (element_state.menu, element_state.position) } else { - Rc::new(RefCell::new(None)) + (Rc::default(), Rc::default()) }; + let mut menu_layout_id = None; + let menu_element = menu.borrow_mut().as_mut().map(|menu| { - let mut view = menu.clone().render(); - child_layout_ids.push(view.layout(view_state, cx)); + let mut overlay = overlay::().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 layout_id = cx.request_layout(&gpui::Style::default(), child_layout_ids.into_iter()); + let mut child_element = self + .child_builder + .take() + .map(|child_builder| (child_builder)(menu.borrow().is_some())); - (layout_id, MenuHandleState { menu, menu_element }) + 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( @@ -200,7 +207,7 @@ impl Element for MenuHandle { element_state: &mut Self::ElementState, cx: &mut crate::ViewContext, ) { - for child in &mut self.children { + if let Some(child) = element_state.child_element.as_mut() { child.paint(view_state, cx); } @@ -209,8 +216,14 @@ impl Element for MenuHandle { return; } + let Some(builder) = self.menu_builder.clone() else { + return; + }; let menu = element_state.menu.clone(); - let builder = self.builder.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 @@ -229,6 +242,14 @@ impl Element for MenuHandle { }) .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(); } }); @@ -250,35 +271,101 @@ mod stories { use crate::story::Story; use gpui::{action, Div, Render, VisualContext}; + #[action] + struct PrintCurrentDate {} + + fn build_menu(cx: &mut WindowContext, header: impl Into) -> View { + cx.build_view(|cx| { + ContextMenu::new(cx).header(header).separator().entry( + Label::new("Print current time"), + PrintCurrentDate {}.boxed_clone(), + ) + }) + } + pub struct ContextMenuStory; impl Render for ContextMenuStory { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - #[action] - struct PrintCurrentDate {} - Story::container(cx) - .child(Story::title_for::<_, ContextMenu>(cx)) .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( - MenuHandle::new("test", move |_, cx| { - cx.build_view(|cx| { - ContextMenu::new(cx) - .header("Section header") - .separator() - .entry( - Label::new("Print current time"), - PrintCurrentDate {}.boxed_clone(), - ) - }) - }) - .child(Label::new("RIGHT CLICK ME")), + 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")), + ), ) } } diff --git a/crates/ui2/src/components/icon_button.rs b/crates/ui2/src/components/icon_button.rs index 1f7b86badd..2772f10a29 100644 --- a/crates/ui2/src/components/icon_button.rs +++ b/crates/ui2/src/components/icon_button.rs @@ -19,6 +19,7 @@ pub struct IconButton { color: TextColor, variant: ButtonVariant, state: InteractionState, + selected: bool, tooltip: Option) -> AnyView + 'static>>, handlers: IconButtonHandlers, } @@ -31,6 +32,7 @@ impl IconButton { color: TextColor::default(), variant: ButtonVariant::default(), state: InteractionState::default(), + selected: false, tooltip: None, handlers: IconButtonHandlers::default(), } @@ -56,6 +58,11 @@ impl IconButton { 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) -> AnyView + 'static, @@ -80,7 +87,7 @@ impl IconButton { _ => 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, @@ -93,6 +100,10 @@ impl IconButton { ), }; + if self.selected { + bg_color = bg_hover_color; + } + let mut button = h_stack() .id(self.id.clone()) .justify_center() @@ -113,7 +124,9 @@ impl IconButton { } 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 diff --git a/crates/workspace2/src/dock.rs b/crates/workspace2/src/dock.rs index 409385cafc..3a3cc4d490 100644 --- a/crates/workspace2/src/dock.rs +++ b/crates/workspace2/src/dock.rs @@ -1,18 +1,18 @@ use crate::{status_bar::StatusItemView, Axis, Workspace}; use gpui::{ - div, overlay, point, px, Action, AnyElement, AnyView, AppContext, Component, DispatchPhase, - Div, Element, ElementId, Entity, EntityId, EventEmitter, FocusHandle, FocusableView, - InteractiveComponent, LayoutId, MouseButton, MouseDownEvent, ParentComponent, Pixels, Point, - Render, SharedString, Style, Styled, Subscription, View, ViewContext, VisualContext, WeakView, - WindowContext, + div, overlay, point, px, Action, AnchorCorner, AnyElement, AnyView, AppContext, Component, + DispatchPhase, Div, Element, ElementId, Entity, EntityId, EventEmitter, FocusHandle, + FocusableView, InteractiveComponent, LayoutId, MouseButton, MouseDownEvent, ParentComponent, + Pixels, Point, Render, SharedString, Style, Styled, Subscription, View, ViewContext, + VisualContext, WeakView, WindowContext, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use smallvec::SmallVec; use std::{cell::RefCell, rc::Rc, sync::Arc}; use ui::{ - h_stack, ContextMenu, ContextMenuItem, IconButton, InteractionState, Label, MenuEvent, - MenuHandle, Tooltip, + h_stack, menu_handle, ContextMenu, IconButton, InteractionState, Label, MenuEvent, MenuHandle, + Tooltip, }; pub enum PanelEvent { @@ -672,6 +672,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() @@ -697,11 +704,14 @@ impl Render for PanelButtons { }; Some( - MenuHandle::new( - SharedString::from(format!("{} tooltip", name)), - move |_, cx| cx.build_view(|cx| ContextMenu::new(cx).header("SECTION")), - ) - .child(button), + 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)), ) }); From 2182cb26563f40034676f6fd4d24653ca7138fc5 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 16 Nov 2023 22:16:29 -0700 Subject: [PATCH 33/37] Ooh generics --- crates/storybook3/src/storybook3.rs | 2 +- crates/ui2/src/components/context_menu.rs | 26 +++++++++++------------ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/crates/storybook3/src/storybook3.rs b/crates/storybook3/src/storybook3.rs index aa27edb1f9..291f8ce2ac 100644 --- a/crates/storybook3/src/storybook3.rs +++ b/crates/storybook3/src/storybook3.rs @@ -1,7 +1,7 @@ use anyhow::Result; use gpui::AssetSource; use gpui::{ - div, hsla, px, size, AnyView, Bounds, Div, Render, ViewContext, VisualContext, WindowBounds, + div, px, size, AnyView, Bounds, Div, Render, ViewContext, VisualContext, WindowBounds, WindowOptions, }; use settings::{default_settings, Settings, SettingsStore}; diff --git a/crates/ui2/src/components/context_menu.rs b/crates/ui2/src/components/context_menu.rs index dadf27d89e..789523d526 100644 --- a/crates/ui2/src/components/context_menu.rs +++ b/crates/ui2/src/components/context_menu.rs @@ -23,6 +23,7 @@ impl FocusableView for ContextMenu { self.focus_handle.clone() } } +impl Menu for ContextMenu {} impl ContextMenu { pub fn new(cx: &mut WindowContext) -> Self { @@ -81,25 +82,24 @@ impl Render for ContextMenu { } } -pub struct MenuHandle { +pub trait Menu: Render + EventEmitter + FocusableView {} + +pub struct MenuHandle { id: Option, child_builder: Option AnyElement + 'static>>, - menu_builder: Option) -> View + 'static>>, + menu_builder: Option) -> View + 'static>>, anchor: Option, attach: Option, } -impl MenuHandle { +impl MenuHandle { pub fn id(mut self, id: impl Into) -> Self { self.id = Some(id.into()); self } - pub fn menu( - mut self, - f: impl Fn(&mut V, &mut ViewContext) -> View + 'static, - ) -> Self { + pub fn menu(mut self, f: impl Fn(&mut V, &mut ViewContext) -> View + 'static) -> Self { self.menu_builder = Some(Rc::new(f)); self } @@ -123,7 +123,7 @@ impl MenuHandle { } } -pub fn menu_handle() -> MenuHandle { +pub fn menu_handle() -> MenuHandle { MenuHandle { id: None, child_builder: None, @@ -133,15 +133,15 @@ pub fn menu_handle() -> MenuHandle { } } -pub struct MenuHandleState { - menu: Rc>>>, +pub struct MenuHandleState { + menu: Rc>>>, position: Rc>>, child_layout_id: Option, child_element: Option>, menu_element: Option>, } -impl Element for MenuHandle { - type ElementState = MenuHandleState; +impl Element for MenuHandle { + type ElementState = MenuHandleState; fn element_id(&self) -> Option { Some(self.id.clone().expect("menu_handle must have an id()")) @@ -255,7 +255,7 @@ impl Element for MenuHandle { } } -impl Component for MenuHandle { +impl Component for MenuHandle { fn render(self) -> AnyElement { AnyElement::new(self) } From 2d1d75f4829dd0e71e1219fa9e0f6c85931cfda8 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 16 Nov 2023 22:46:44 -0700 Subject: [PATCH 34/37] +ManagedView And some games with rust traits --- .../command_palette2/src/command_palette.rs | 16 +++++----- crates/file_finder2/src/file_finder.rs | 21 ++++++------- crates/go_to_line2/src/go_to_line.rs | 19 ++++++------ crates/gpui2/src/window.rs | 17 +++++++++++ crates/picker2/src/picker2.rs | 10 +++++-- crates/ui2/src/components/context_menu.rs | 30 +++++++------------ crates/workspace2/src/modal_layer.rs | 22 ++++---------- crates/workspace2/src/workspace2.rs | 13 ++++---- 8 files changed, 75 insertions(+), 73 deletions(-) diff --git a/crates/command_palette2/src/command_palette.rs b/crates/command_palette2/src/command_palette.rs index 6264606ed9..9463cab68c 100644 --- a/crates/command_palette2/src/command_palette.rs +++ b/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); @@ -69,10 +68,9 @@ impl CommandPalette { } } -impl EventEmitter 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>) { self.command_palette - .update(cx, |_, cx| cx.emit(ModalEvent::Dismissed)) + .update(cx, |_, cx| cx.emit(Dismiss)) .log_err(); } diff --git a/crates/file_finder2/src/file_finder.rs b/crates/file_finder2/src/file_finder.rs index b2850761a9..0fee5102e6 100644 --- a/crates/file_finder2/src/file_finder.rs +++ b/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 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>) { self.file_finder - .update(cx, |_, cx| cx.emit(ModalEvent::Dismissed)) + .update(cx, |_, cx| cx.emit(Dismiss)) .log_err(); } diff --git a/crates/go_to_line2/src/go_to_line.rs b/crates/go_to_line2/src/go_to_line.rs index ccd6b7ada2..565afb5e93 100644 --- a/crates/go_to_line2/src/go_to_line.rs +++ b/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, } -impl EventEmitter 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) { - cx.emit(ModalEvent::Dismissed); + cx.emit(Dismiss); } fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { @@ -140,7 +139,7 @@ impl GoToLine { self.prev_scroll_position.take(); } - cx.emit(ModalEvent::Dismissed); + cx.emit(Dismiss); } } diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index fe6e3a5f6b..6d07f06d94 100644 --- a/crates/gpui2/src/window.rs +++ b/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 EventEmitter for T {} + +impl 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, diff --git a/crates/picker2/src/picker2.rs b/crates/picker2/src/picker2.rs index 72a2f812e9..3491fc3d4a 100644 --- a/crates/picker2/src/picker2.rs +++ b/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 FocusableView for Picker { + fn focus_handle(&self, cx: &AppContext) -> FocusHandle { + self.editor.focus_handle(cx) + } +} + impl Picker { pub fn new(delegate: D, cx: &mut ViewContext) -> Self { let editor = cx.build_view(|cx| { diff --git a/crates/ui2/src/components/context_menu.rs b/crates/ui2/src/components/context_menu.rs index 789523d526..d3214cbff1 100644 --- a/crates/ui2/src/components/context_menu.rs +++ b/crates/ui2/src/components/context_menu.rs @@ -4,8 +4,8 @@ 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, DispatchPhase, Div, EventEmitter, - FocusHandle, FocusableView, LayoutId, MouseButton, MouseDownEvent, Pixels, Point, Render, View, + overlay, px, Action, AnchorCorner, AnyElement, Bounds, Dismiss, DispatchPhase, Div, + FocusHandle, LayoutId, ManagedView, MouseButton, MouseDownEvent, Pixels, Point, Render, View, }; pub struct ContextMenu { @@ -13,17 +13,11 @@ pub struct ContextMenu { focus_handle: FocusHandle, } -pub enum MenuEvent { - Dismissed, -} - -impl EventEmitter for ContextMenu {} -impl FocusableView for ContextMenu { +impl ManagedView for ContextMenu { fn focus_handle(&self, cx: &gpui::AppContext) -> FocusHandle { self.focus_handle.clone() } } -impl Menu for ContextMenu {} impl ContextMenu { pub fn new(cx: &mut WindowContext) -> Self { @@ -50,11 +44,11 @@ impl ContextMenu { pub fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { // todo!() - cx.emit(MenuEvent::Dismissed); + cx.emit(Dismiss); } pub fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { - cx.emit(MenuEvent::Dismissed); + cx.emit(Dismiss); } } @@ -82,9 +76,7 @@ impl Render for ContextMenu { } } -pub trait Menu: Render + EventEmitter + FocusableView {} - -pub struct MenuHandle { +pub struct MenuHandle { id: Option, child_builder: Option AnyElement + 'static>>, menu_builder: Option) -> View + 'static>>, @@ -93,7 +85,7 @@ pub struct MenuHandle { attach: Option, } -impl MenuHandle { +impl MenuHandle { pub fn id(mut self, id: impl Into) -> Self { self.id = Some(id.into()); self @@ -123,7 +115,7 @@ impl MenuHandle { } } -pub fn menu_handle() -> MenuHandle { +pub fn menu_handle() -> MenuHandle { MenuHandle { id: None, child_builder: None, @@ -140,7 +132,7 @@ pub struct MenuHandleState { child_element: Option>, menu_element: Option>, } -impl Element for MenuHandle { +impl Element for MenuHandle { type ElementState = MenuHandleState; fn element_id(&self) -> Option { @@ -234,7 +226,7 @@ impl Element for MenuHandle { let new_menu = (builder)(view_state, cx); let menu2 = menu.clone(); cx.subscribe(&new_menu, move |this, modal, e, cx| match e { - MenuEvent::Dismissed => { + &Dismiss => { *menu2.borrow_mut() = None; cx.notify(); } @@ -255,7 +247,7 @@ impl Element for MenuHandle { } } -impl Component for MenuHandle { +impl Component for MenuHandle { fn render(self) -> AnyElement { AnyElement::new(self) } diff --git a/crates/workspace2/src/modal_layer.rs b/crates/workspace2/src/modal_layer.rs index cd5995d65e..8afd8317f9 100644 --- a/crates/workspace2/src/modal_layer.rs +++ b/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, } -pub trait Modal: Render + EventEmitter { - 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(&mut self, cx: &mut ViewContext, build_view: B) where - V: Modal, + V: ManagedView, B: FnOnce(&mut ViewContext) -> V, { if let Some(active_modal) = &self.active_modal { @@ -46,17 +38,15 @@ impl ModalLayer { pub fn show_modal(&mut self, new_modal: View, cx: &mut ViewContext) 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(); } diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 96db870d18..dc69280c1e 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -31,9 +31,9 @@ use futures::{ use gpui::{ actions, div, point, 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, + FocusableView, GlobalPixels, InteractiveComponent, KeyContext, ManagedView, Model, + ModelContext, ParentComponent, Point, Render, Size, Styled, Subscription, Task, View, + ViewContext, VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle, WindowOptions, }; use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem}; use itertools::Itertools; @@ -3380,11 +3380,14 @@ impl Workspace { div } - pub fn active_modal(&mut self, cx: &ViewContext) -> Option> { + pub fn active_modal( + &mut self, + cx: &ViewContext, + ) -> Option> { self.modal_layer.read(cx).active_modal() } - pub fn toggle_modal(&mut self, cx: &mut ViewContext, build: B) + pub fn toggle_modal(&mut self, cx: &mut ViewContext, build: B) where B: FnOnce(&mut ViewContext) -> V, { From 9558da868193670a58b593b2908dda264a035bfd Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 16 Nov 2023 20:11:55 -0700 Subject: [PATCH 35/37] Separate WrappedLines from ShapedLines ShapedLines are never wrapped, whereas WrappedLines are optionally wrapped if they are associated with a wrap width. I tried to combine everything because wrapping is inherently optional for the Text element, but we have a bunch of APIs that don't make sense on a line that may wrap, so we need a distinct type for that case. --- crates/editor2/src/display_map.rs | 29 +-- crates/editor2/src/editor.rs | 32 ++- crates/editor2/src/element.rs | 133 +++++----- crates/editor2/src/movement.rs | 10 +- crates/editor2/src/selections_collection.rs | 8 +- crates/gpui2/src/element.rs | 2 +- crates/gpui2/src/elements/text.rs | 191 +++++++++----- crates/gpui2/src/platform/mac/text_system.rs | 10 +- crates/gpui2/src/style.rs | 1 + crates/gpui2/src/text_system.rs | 82 +++++- crates/gpui2/src/text_system/line.rs | 259 +++++++++++-------- crates/gpui2/src/text_system/line_layout.rs | 136 +++++++--- 12 files changed, 563 insertions(+), 330 deletions(-) diff --git a/crates/editor2/src/display_map.rs b/crates/editor2/src/display_map.rs index e64d5e301c..533abcd871 100644 --- a/crates/editor2/src/display_map.rs +++ b/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 { 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( diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index af7da8e837..fff78ee2c5 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -5445,7 +5445,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); @@ -6391,8 +6393,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(); @@ -6431,15 +6433,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 { @@ -6992,7 +6995,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) @@ -9755,7 +9758,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 { diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index de1b6f0622..782410f5f3 100644 --- a/crates/editor2/src/element.rs +++ b/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) -> Pixels { @@ -1338,7 +1338,7 @@ impl EditorElement { relative_rows } - fn layout_line_numbers( + fn shape_line_numbers( &self, rows: Range, active_rows: &BTreeMap, @@ -1347,12 +1347,12 @@ impl EditorElement { snapshot: &EditorSnapshot, cx: &ViewContext, ) -> ( - Vec>, + Vec>, Vec>, ) { 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, - line_number_layouts: &[Option], + line_number_layouts: &[Option], snapshot: &EditorSnapshot, cx: &ViewContext, ) -> Vec { @@ -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, @@ -2199,7 +2192,7 @@ impl EditorElement { #[derive(Debug)] pub struct LineWithInvisibles { - pub line: Line, + pub line: ShapedLine, invisibles: Vec, } @@ -2209,7 +2202,7 @@ impl LineWithInvisibles { text_style: &TextStyle, max_line_len: usize, max_line_count: usize, - line_number_layouts: &[Option], + line_number_layouts: &[Option], editor_mode: EditorMode, cx: &WindowContext, ) -> Vec { @@ -2229,11 +2222,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(), }); @@ -2267,6 +2261,7 @@ impl LineWithInvisibles { len: line_chunk.len(), font: text_style.font(), color: text_style.color, + background_color: None, underline: text_style.underline, }); @@ -3087,7 +3082,7 @@ pub struct LayoutState { visible_display_row_range: Range, active_rows: BTreeMap, highlighted_rows: Option>, - line_number_layouts: Vec>, + line_numbers: Vec>, display_hunks: Vec, blocks: Vec, highlighted_ranges: Vec<(Range, Hsla)>, @@ -3100,8 +3095,8 @@ pub struct LayoutState { code_actions_indicator: Option, // hover_popovers: Option<(DisplayPoint, Vec>)>, fold_indicators: Vec>>, - tab_invisible: Line, - space_invisible: Line, + tab_invisible: ShapedLine, + space_invisible: ShapedLine, } struct CodeActionsIndicator { @@ -3201,7 +3196,7 @@ fn layout_line( snapshot: &EditorSnapshot, style: &EditorStyle, cx: &WindowContext, -) -> Result { +) -> Result { let mut line = snapshot.line(row); if line.len() > MAX_LINE_LEN { @@ -3213,21 +3208,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)] @@ -3237,7 +3228,7 @@ pub struct Cursor { line_height: Pixels, color: Hsla, shape: CursorShape, - block_text: Option, + block_text: Option, } impl Cursor { @@ -3247,7 +3238,7 @@ impl Cursor { line_height: Pixels, color: Hsla, shape: CursorShape, - block_text: Option, + block_text: Option, ) -> Cursor { Cursor { origin, diff --git a/crates/editor2/src/movement.rs b/crates/editor2/src/movement.rs index b28af681e0..1414ae702d 100644 --- a/crates/editor2/src/movement.rs +++ b/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); diff --git a/crates/editor2/src/selections_collection.rs b/crates/editor2/src/selections_collection.rs index 01e241c830..bcf41f135b 100644 --- a/crates/editor2/src/selections_collection.rs +++ b/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); diff --git a/crates/gpui2/src/element.rs b/crates/gpui2/src/element.rs index 221eb903fd..b4b1af630e 100644 --- a/crates/gpui2/src/element.rs +++ b/crates/gpui2/src/element.rs @@ -13,7 +13,7 @@ pub trait Element { fn layout( &mut self, view_state: &mut V, - previous_element_state: Option, + element_state: Option, cx: &mut ViewContext, ) -> (LayoutId, Self::ElementState); diff --git a/crates/gpui2/src/elements/text.rs b/crates/gpui2/src/elements/text.rs index 1081154e7d..6849a89711 100644 --- a/crates/gpui2/src/elements/text.rs +++ b/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 Component for SharedString { - fn render(self) -> AnyElement { - Text { - text: self, - runs: None, - state_type: PhantomData, - } - .render() - } -} - -impl Component for &'static str { - fn render(self) -> AnyElement { - 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 Component for String { - fn render(self) -> AnyElement { - Text { - text: self.into(), - runs: None, - state_type: PhantomData, - } - .render() - } -} - -pub struct Text { +pub struct Text { text: SharedString, runs: Option>, - state_type: PhantomData, } -impl Text { - /// 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) -> Self { Text { text, runs: Some(runs), - state_type: Default::default(), } } } -impl Component for Text { +impl Component for Text { fn render(self) -> AnyElement { AnyElement::new(self) } } -impl Element for Text { - type ElementState = Arc>>; +impl Element for Text { + type ElementState = TextState; fn element_id(&self) -> Option { None @@ -103,7 +66,7 @@ impl Element for Text { 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 Element for Text { ) .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::(); - let size = Size { - width: lines - .iter() - .map(|line| line.layout.width) - .max() - .unwrap() - .ceil(), - height: line_height * line_count, - }; + let mut size: Size = 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 Element for Text { } } -pub struct TextElementState { - lines: SmallVec<[Line; 1]>, +#[derive(Default, Clone)] +pub struct TextState(Arc>>); + +impl TextState { + fn lock(&self) -> MutexGuard> { + 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>>, +} + +impl Element for InteractiveText { + type ElementState = InteractiveTextState; + + fn element_id(&self) -> Option { + Some(self.id.clone()) + } + + fn layout( + &mut self, + view_state: &mut V, + element_state: Option, + cx: &mut ViewContext, + ) -> (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, + view_state: &mut V, + element_state: &mut Self::ElementState, + cx: &mut ViewContext, + ) { + self.text + .paint(bounds, view_state, &mut element_state.text_state, cx) + } +} + +impl Component for SharedString { + fn render(self) -> AnyElement { + Text { + text: self, + runs: None, + } + .render() + } +} + +impl Component for &'static str { + fn render(self) -> AnyElement { + 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 Component for String { + fn render(self) -> AnyElement { + Text { + text: self.into(), + runs: None, + } + .render() + } +} diff --git a/crates/gpui2/src/platform/mac/text_system.rs b/crates/gpui2/src/platform/mac/text_system.rs index 155f3097fe..9ef0f321b6 100644 --- a/crates/gpui2/src/platform/mac/text_system.rs +++ b/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(), } } diff --git a/crates/gpui2/src/style.rs b/crates/gpui2/src/style.rs index 5d9dd5d804..1b0cabb401 100644 --- a/crates/gpui2/src/style.rs +++ b/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(), } } diff --git a/crates/gpui2/src/text_system.rs b/crates/gpui2/src/text_system.rs index c7031fcb4d..b3d7a96aff 100644 --- a/crates/gpui2/src/text_system.rs +++ b/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> { + 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 { + 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, - ) -> Result> { + ) -> Result> { 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, pub underline: Option, } diff --git a/crates/gpui2/src/text_system/line.rs b/crates/gpui2/src/text_system/line.rs index 707274ad33..d05ae9468d 100644 --- a/crates/gpui2/src/text_system/line.rs +++ b/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,17 @@ pub struct DecorationRun { } #[derive(Clone, Default, Debug, Deref, DerefMut)] -pub struct Line { +pub struct ShapedLine { #[deref] #[deref_mut] - pub(crate) layout: Arc, - pub(crate) decorations: SmallVec<[DecorationRun; 32]>, + pub(crate) layout: Arc, + pub text: SharedString, + pub(crate) decoration_runs: SmallVec<[DecorationRun; 32]>, } -impl Line { - pub fn size(&self, line_height: Pixels) -> Size { - size( - self.layout.width, - line_height * (self.layout.wrap_boundaries.len() + 1), - ) - } - - pub fn wrap_count(&self) -> usize { - self.layout.wrap_boundaries.len() +impl ShapedLine { + pub fn len(&self) -> usize { + self.layout.len } pub fn paint( @@ -39,75 +33,84 @@ 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); + paint_line( + origin, + &self.layout, + line_height, + &self.decoration_runs, + None, + &[], + cx, + )?; - 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, UnderlineStyle)> = None; - let text_system = cx.text_system().clone(); + Ok(()) + } +} - 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; +#[derive(Clone, Default, Debug, Deref, DerefMut)] +pub struct WrappedLine { + #[deref] + #[deref_mut] + pub(crate) layout: Arc, + pub text: SharedString, + pub(crate) decoration_runs: SmallVec<[DecorationRun; 32]>, +} - for (glyph_ix, glyph) in run.glyphs.iter().enumerate() { - glyph_origin.x += glyph.position.x - prev_glyph_position.x; +impl WrappedLine { + pub fn len(&self) -> usize { + self.layout.len() + } - 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, - )?; - } + pub fn paint( + &self, + origin: Point, + line_height: Pixels, + cx: &mut WindowContext, + ) -> Result<()> { + paint_line( + origin, + &self.layout.unwrapped_layout, + line_height, + &self.decoration_runs, + self.wrap_width, + &self.wrap_boundaries, + cx, + )?; - glyph_origin.x = origin.x; - glyph_origin.y += line_height; - } - prev_glyph_position = glyph.position; + Ok(()) + } +} - let mut finished_underline: Option<(Point, 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, - }, - )); - } +fn paint_line( + origin: Point, + layout: &LineLayout, + line_height: Pixels, + decoration_runs: &[DecorationRun], + wrap_width: Option, + 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, 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; - run_end += style_run.len as usize; - color = style_run.color; - } else { - run_end = self.layout.text.len(); - finished_underline = current_underline.take(); - } - } + for (glyph_ix, glyph) in run.glyphs.iter().enumerate() { + glyph_origin.x += glyph.position.x - prev_glyph_position.x; - if let Some((underline_origin, underline_style)) = finished_underline { + 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, - }; + glyph_origin.x = origin.x; + glyph_origin.y += line_height; + } + prev_glyph_position = glyph.position; - 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, - )?; + let mut finished_underline: Option<(Point, 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_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, + )?; } } } - - 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, - )?; - } - - 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(()) } diff --git a/crates/gpui2/src/text_system/line_layout.rs b/crates/gpui2/src/text_system/line_layout.rs index 7e9176caca..a5cf814a8c 100644 --- a/crates/gpui2/src/text_system/line_layout.rs +++ b/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, pub wrap_boundaries: SmallVec<[WrapBoundary; 1]>, + pub wrap_width: Option, } #[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 { + 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>>, - curr_frame: RwLock>>, + previous_frame: Mutex>>, + current_frame: RwLock>>, + previous_frame_wrapped: Mutex>>, + current_frame_wrapped: RwLock>>, platform_text_system: Arc, } impl LineLayoutCache { pub fn new(platform_text_system: Arc) -> 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, @@ -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 { + 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, From 9a3cd073c7f1d25b7fb685e76f70381e17b08f6b Mon Sep 17 00:00:00 2001 From: Mikayla Date: Thu, 16 Nov 2023 23:05:28 -0800 Subject: [PATCH 36/37] Restore a bunch of random workspace stuff --- crates/collab_ui2/src/collab_panel.rs | 1 + crates/editor2/src/items.rs | 4 +- crates/gpui2/src/app/async_context.rs | 4 + crates/gpui2/src/geometry.rs | 49 +- crates/project_panel2/src/project_panel.rs | 3 +- crates/terminal_view2/src/terminal_panel.rs | 4 +- crates/workspace2/src/dock.rs | 62 +- crates/workspace2/src/item.rs | 14 +- crates/workspace2/src/pane.rs | 365 ++++---- crates/workspace2/src/searchable.rs | 2 +- crates/workspace2/src/workspace2.rs | 990 +++++++++----------- 11 files changed, 719 insertions(+), 779 deletions(-) diff --git a/crates/collab_ui2/src/collab_panel.rs b/crates/collab_ui2/src/collab_panel.rs index 6fb3f03f60..20e77d7023 100644 --- a/crates/collab_ui2/src/collab_panel.rs +++ b/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()); diff --git a/crates/editor2/src/items.rs b/crates/editor2/src/items.rs index 8efac65ed4..cf2bf5b6dc 100644 --- a/crates/editor2/src/items.rs +++ b/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) { 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, ); } diff --git a/crates/gpui2/src/app/async_context.rs b/crates/gpui2/src/app/async_context.rs index 5b7f8ce590..83b3ccebe7 100644 --- a/crates/gpui2/src/app/async_context.rs +++ b/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 } } diff --git a/crates/gpui2/src/geometry.rs b/crates/gpui2/src/geometry.rs index a1898bfd6d..e1f039e309 100644 --- a/crates/gpui2/src/geometry.rs +++ b/crates/gpui2/src/geometry.rs @@ -343,7 +343,7 @@ where impl Bounds where - T: Clone + Debug + PartialOrd + Add + Sub + Default, + T: Clone + Debug + PartialOrd + Add + Sub + Default + Half, { pub fn intersects(&self, other: &Bounds) -> 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 { + Point { + x: self.origin.x.clone() + self.size.width.clone().half(), + y: self.origin.y.clone() + self.size.height.clone().half(), + } + } } impl + Sub> Bounds { @@ -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; } diff --git a/crates/project_panel2/src/project_panel.rs b/crates/project_panel2/src/project_panel.rs index 87edabab52..7a455fe8ce 100644 --- a/crates/project_panel2/src/project_panel.rs +++ b/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) { @@ -2802,7 +2802,6 @@ mod tests { 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); diff --git a/crates/terminal_view2/src/terminal_panel.rs b/crates/terminal_view2/src/terminal_panel.rs index fbb1bd5352..944cd912be 100644 --- a/crates/terminal_view2/src/terminal_panel.rs +++ b/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::>(); 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( diff --git a/crates/workspace2/src/dock.rs b/crates/workspace2/src/dock.rs index ee45ca862c..64da42cea7 100644 --- a/crates/workspace2/src/dock.rs +++ b/crates/workspace2/src/dock.rs @@ -217,11 +217,11 @@ impl Dock { // .map_or(false, |panel| panel.has_focus(cx)) // } - // pub fn panel(&self) -> Option> { - // self.panel_entries - // .iter() - // .find_map(|entry| entry.panel.as_any().clone().downcast()) - // } + pub fn panel(&self) -> Option> { + self.panel_entries + .iter() + .find_map(|entry| entry.panel.to_any().clone().downcast().ok()) + } pub fn panel_index_for_type(&self) -> Option { self.panel_entries @@ -416,24 +416,6 @@ impl Dock { cx.notify(); } } - - // pub fn render_placeholder(&self, cx: &WindowContext) -> AnyElement { - // todo!() - // if let Some(active_entry) = self.visible_entry() { - // Empty::new() - // .into_any() - // .contained() - // .with_style(self.style(cx)) - // .resizable::( - // self.position.to_resize_handle_side(), - // active_entry.panel.size(cx), - // |_, _, _| {}, - // ) - // .into_any() - // } else { - // Empty::new().into_any() - // } - // } } impl Render for Dock { @@ -461,40 +443,6 @@ impl Render for Dock { } } -// todo!() -// impl View for Dock { -// fn ui_name() -> &'static str { -// "Dock" -// } - -// fn render(&mut self, cx: &mut ViewContext) -> AnyElement { -// 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::( -// 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) { -// 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, diff --git a/crates/workspace2/src/item.rs b/crates/workspace2/src/item.rs index 7252e7135a..5b37656d96 100644 --- a/crates/workspace2/src/item.rs +++ b/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, 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 ItemHandle for View { 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 ItemHandle for View { 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 ItemHandle for View { 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 ItemHandle for View { // }) // .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 ItemHandle for View { 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 FollowableItemHandle for View { 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(), }) }) } diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index b86240f419..d44d347114 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -8,8 +8,8 @@ use anyhow::Result; use collections::{HashMap, HashSet, VecDeque}; use gpui::{ actions, prelude::*, Action, AppContext, AsyncWindowContext, Component, Div, EntityId, - EventEmitter, FocusHandle, Focusable, FocusableView, Model, PromptLevel, Render, Task, View, - ViewContext, VisualContext, WeakView, WindowContext, + EventEmitter, FocusHandle, Focusable, FocusableView, Model, Pixels, Point, PromptLevel, Render, + Task, View, ViewContext, VisualContext, WeakView, WindowContext, }; use parking_lot::Mutex; use project2::{Project, ProjectEntryId, ProjectPath}; @@ -102,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 }, ActivateItem { local: bool }, @@ -140,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) @@ -524,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)); } } } @@ -548,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 @@ -613,21 +593,21 @@ impl Pane { self.items.iter() } - // pub fn items_of_type(&self) -> impl '_ + Iterator> { - // self.items - // .iter() - // .filter_map(|item| item.as_any().clone().downcast()) - // } + pub fn items_of_type(&self) -> impl '_ + Iterator> { + self.items + .iter() + .filter_map(|item| item.to_any().downcast().ok()) + } pub fn active_item(&self) -> Option> { self.items.get(self.active_item_index).cloned() } - // pub fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option { - // self.items - // .get(self.active_item_index)? - // .pixel_position_of_cursor(cx) - // } + pub fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option> { + self.items + .get(self.active_item_index)? + .pixel_position_of_cursor(cx) + } pub fn item_for_entry( &self, @@ -644,24 +624,26 @@ impl Pane { } pub fn index_for_item(&self, item: &dyn ItemHandle) -> Option { - 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) { - // // 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(); + // pub fn toggle_zoom(&mut self, _: &ToggleZoom, cx: &mut ViewContext) { + // // 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); + // 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, @@ -689,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); @@ -705,25 +687,25 @@ impl Pane { } } - // pub fn activate_prev_item(&mut self, activate_pane: bool, cx: &mut ViewContext) { - // 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) { + 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) { - // 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) { + 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, @@ -733,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), @@ -750,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, - // ) -> Option>> { - // if self.items.is_empty() { - // return None; - // } + pub fn close_inactive_items( + &mut self, + _: &CloseInactiveItems, + cx: &mut ViewContext, + ) -> Option>> { + 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, - // ) -> Option>> { - // 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, + ) -> Option>> { + 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, - // ) -> Option>> { - // 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, + ) -> Option>> { + 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, - // ) -> Task> { - // 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, + ) -> Task> { + 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, - // ) -> Option>> { - // 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, + ) -> Option>> { + 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, - // ) -> Task> { - // 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, + ) -> Task> { + 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, - // ) -> Option>> { - // if self.items.is_empty() { - // return None; - // } + pub fn close_all_items( + &mut self, + action: &CloseAllItems, + cx: &mut ViewContext, + ) -> Option>> { + 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>, @@ -896,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()); @@ -949,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)); @@ -977,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); } })?; @@ -995,7 +981,7 @@ impl Pane { cx: &mut ViewContext, ) { 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 @@ -1003,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 @@ -1020,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); @@ -1041,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 { @@ -1323,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 } @@ -1354,10 +1346,10 @@ impl Pane { ) -> impl Component { 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( @@ -1387,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()) @@ -1914,6 +1906,25 @@ 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: &CloseActiveItem, cx| { pane.close_active_item(action, cx) diff --git a/crates/workspace2/src/searchable.rs b/crates/workspace2/src/searchable.rs index 2a393a9f6d..78c3b5a6ce 100644 --- a/crates/workspace2/src/searchable.rs +++ b/crates/workspace2/src/searchable.rs @@ -240,7 +240,7 @@ impl From<&Box> for AnyView { impl PartialEq for Box { fn eq(&self, other: &Self) -> bool { - self.id() == other.id() + self.item_id() == other.item_id() } } diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index a01883b9a0..c7a27848ce 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -32,8 +32,9 @@ use gpui::{ actions, div, point, 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, + 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, -// } +#[derive(Clone, PartialEq)] +pub struct OpenPaths { + pub paths: Vec, +} -// #[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, -// } +#[derive(Clone, PartialEq, Debug, Deserialize, Action)] +#[serde(rename_all = "camelCase")] +pub struct SaveAll { + pub save_intent: Option, +} -// #[derive(Clone, PartialEq, Debug, Deserialize)] -// #[serde(rename_all = "camelCase")] -// pub struct Save { -// pub save_intent: Option, -// } +#[derive(Clone, PartialEq, Debug, Deserialize, Action)] +#[serde(rename_all = "camelCase")] +pub struct Save { + pub save_intent: Option, +} -// #[derive(Clone, PartialEq, Debug, Deserialize, Default)] -// #[serde(rename_all = "camelCase")] -// pub struct CloseAllItemsAndPanes { -// pub save_intent: Option, -// } +#[derive(Clone, PartialEq, Debug, Deserialize, Default, Action)] +#[serde(rename_all = "camelCase")] +pub struct CloseAllItemsAndPanes { + pub save_intent: Option, +} #[derive(Deserialize)] pub struct Toast { @@ -199,20 +200,6 @@ 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) { @@ -222,7 +209,6 @@ pub fn init_settings(cx: &mut AppContext) { pub fn init(app_state: Arc, cx: &mut AppContext) { init_settings(cx); - pane::init(cx); notifications::init(cx); // cx.add_global_action({ @@ -423,6 +409,7 @@ pub enum Event { } pub struct Workspace { + window_self: WindowHandle, weak_self: WeakView, workspace_actions: Vec) -> Div>>, zoomed: Option, @@ -455,6 +442,8 @@ pub struct Workspace { pane_history_timestamp: Arc, } +impl EventEmitter for Workspace {} + #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] pub struct ViewId { pub creator: PeerId, @@ -532,8 +521,8 @@ impl Workspace { ) }); cx.subscribe(¢er_pane, Self::handle_pane_event).detach(); - // todo!() - // cx.focus(¢er_pane); + + cx.focus_view(¢er_pane); cx.emit(Event::PaneAdded(center_pane.clone())); let window_handle = cx.window_handle().downcast::().unwrap(); @@ -636,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, @@ -779,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(); @@ -964,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); @@ -1077,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, - // ) -> Option>> { - // 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, + ) -> Option>> { + 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, @@ -1113,184 +1100,177 @@ impl Workspace { cx: &mut ViewContext, ) -> Task> { //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::()) - // .count(); + let workspace_count = cx.update(|_, cx| { + cx.windows() + .iter() + .filter(|window| window.downcast::().is_some()) + .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(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 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(); - // } - // } - // } - // } + 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( - false, // this - // .update(&mut cx, |this, cx| { - // this.save_all_internal(SaveIntent::Close, cx) - // })? - // .await? - ) + 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, - // ) -> Option>> { - // 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) { + 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, - // ) -> Task> { - // 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::>(); + fn save_all_internal( + &mut self, + mut save_intent: SaveIntent, + cx: &mut ViewContext, + ) -> Task> { + 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::>(); - // 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) - // }) - // } + 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) + }) + } - // pub fn open(&mut self, _: &Open, cx: &mut ViewContext) -> Option>> { - // let mut paths = cx.prompt_for_paths(PathPromptOptions { - // files: true, - // directories: true, - // multiple: true, - // }); + pub fn open(&mut self, _: &Open, cx: &mut ViewContext) { + let mut paths = cx.prompt_for_paths(PathPromptOptions { + files: true, + directories: true, + multiple: 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(()) - // })) - // } + cx.spawn(|this, mut cx| async move { + let Some(paths) = paths.await.log_err().flatten() else { + return; + }; - // pub fn open_workspace_for_paths( - // &mut self, - // paths: Vec, - // cx: &mut ViewContext, - // ) -> Task> { - // let window = cx.window().downcast::(); - // 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(); + if let Some(task) = this + .update(&mut cx, |this, cx| this.open_workspace_for_paths(paths, cx)) + .log_err() + { + task.await.log_err(); + } + }) + .detach() + } - // 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(()) - // }) - // } + pub fn open_workspace_for_paths( + &mut self, + paths: Vec, + cx: &mut ViewContext, + ) -> Task> { + let window = cx.window_handle().downcast::(); + 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( @@ -1368,25 +1348,25 @@ impl Workspace { }) } - // fn add_folder_to_project(&mut self, _: &AddFolderToProject, cx: &mut ViewContext) { - // 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) { + 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, @@ -1417,18 +1397,18 @@ impl Workspace { self.panes.iter().flat_map(|pane| pane.read(cx).items()) } - // pub fn item_of_type(&self, cx: &AppContext) -> Option> { - // self.items_of_type(cx).max_by_key(|item| item.id()) - // } + pub fn item_of_type(&self, cx: &AppContext) -> Option> { + 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> { - // 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> { + self.panes + .iter() + .flat_map(|pane| pane.read(cx).items_of_type()) + } pub fn active_item(&self, cx: &AppContext) -> Option> { self.active_pane().read(cx).active_item() @@ -1465,68 +1445,70 @@ impl Workspace { }) } - // pub fn close_inactive_items_and_panes( - // &mut self, - // _: &CloseInactiveTabsAndPanes, - // cx: &mut ViewContext, - // ) -> Option>> { - // self.close_all_internal(true, SaveIntent::Close, cx) - // } + pub fn close_inactive_items_and_panes( + &mut self, + _: &CloseInactiveTabsAndPanes, + cx: &mut ViewContext, + ) { + 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, - // ) -> Option>> { - // 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.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, - // ) -> Option>> { - // let current_pane = self.active_pane(); + fn close_all_internal( + &mut self, + retain_active_pane: bool, + save_intent: SaveIntent, + cx: &mut ViewContext, + ) -> Option>> { + 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) { let dock = match dock_side { @@ -1634,15 +1616,15 @@ impl Workspace { None } - // pub fn panel(&self, cx: &WindowContext) -> Option> { - // for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] { - // let dock = dock.read(cx); - // if let Some(panel) = dock.panel::() { - // return Some(panel); - // } - // } - // None - // } + pub fn panel(&self, cx: &WindowContext) -> Option> { + for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] { + let dock = dock.read(cx); + if let Some(panel) = dock.panel::() { + return Some(panel); + } + } + None + } fn zoom_out(&mut self, cx: &mut ViewContext) { for pane in &self.panes { @@ -1953,81 +1935,89 @@ impl Workspace { } } - // fn activate_pane_at_index(&mut self, action: &ActivatePane, cx: &mut ViewContext) { - // 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) { + 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) { - // 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) { + 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) { - // 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) { + 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, - // ) { - // 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, + ) { + 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, - // ) { - // 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, + ) { + 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, - // ) -> Option<&View> { - // 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, + ) -> Option<&View> { + 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, cx: &mut ViewContext) { if self.active_pane != pane { @@ -2199,7 +2189,7 @@ impl Workspace { .read(cx) .items() .enumerate() - .find(|(_, item_handle)| item_handle.id() == item_id_to_move); + .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move); if item_to_move.is_none() { log::warn!("Tried to move item handle which was not in `from` pane. Maybe tab was closed during drop"); @@ -2228,7 +2218,7 @@ impl Workspace { self.unfollow(&pane, cx); self.last_leaders_by_pane.remove(&pane.downgrade()); for removed_item in pane.read(cx).items() { - self.panes_by_item.remove(&removed_item.id()); + self.panes_by_item.remove(&removed_item.item_id()); } cx.notify(); @@ -2972,14 +2962,14 @@ impl Workspace { fn serialize_pane_handle(pane_handle: &View, cx: &WindowContext) -> SerializedPane { let (items, active) = { let pane = pane_handle.read(cx); - let active_item_id = pane.active_item().map(|item| item.id()); + let active_item_id = pane.active_item().map(|item| item.item_id()); ( pane.items() .filter_map(|item_handle| { Some(SerializedItem { kind: Arc::from(item_handle.serialized_item_kind()?), - item_id: item_handle.id().as_u64(), - active: Some(item_handle.id()) == active_item_id, + item_id: item_handle.item_id().as_u64(), + active: Some(item_handle.item_id()) == active_item_id, }) }) .collect::>(), @@ -3213,52 +3203,39 @@ impl Workspace { } fn actions(div: Div) -> Div { - div - // cx.add_async_action(Workspace::open); + div.on_action(Self::open) // cx.add_async_action(Workspace::follow_next_collaborator); // cx.add_async_action(Workspace::close); - // cx.add_async_action(Workspace::close_inactive_items_and_panes); - // cx.add_async_action(Workspace::close_all_items_and_panes); + .on_action(Self::close_inactive_items_and_panes) + .on_action(Self::close_all_items_and_panes) // cx.add_global_action(Workspace::close_global); // cx.add_global_action(restart); - // cx.add_async_action(Workspace::save_all); - // cx.add_action(Workspace::add_folder_to_project); - // cx.add_action( - // |workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext| { - // let pane = workspace.active_pane().clone(); - // workspace.unfollow(&pane, cx); - // }, - // ); - // cx.add_action( - // |workspace: &mut Workspace, action: &Save, cx: &mut ViewContext| { - // workspace - // .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), cx) - // .detach_and_log_err(cx); - // }, - // ); - // cx.add_action( - // |workspace: &mut Workspace, _: &SaveAs, cx: &mut ViewContext| { - // workspace - // .save_active_item(SaveIntent::SaveAs, cx) - // .detach_and_log_err(cx); - // }, - // ); - // cx.add_action(|workspace: &mut Workspace, _: &ActivatePreviousPane, cx| { - // workspace.activate_previous_pane(cx) - // }); - // cx.add_action(|workspace: &mut Workspace, _: &ActivateNextPane, cx| { - // workspace.activate_next_pane(cx) - // }); - // cx.add_action( - // |workspace: &mut Workspace, action: &ActivatePaneInDirection, cx| { - // workspace.activate_pane_in_direction(action.0, cx) - // }, - // ); - // cx.add_action( - // |workspace: &mut Workspace, action: &SwapPaneInDirection, cx| { - // workspace.swap_pane_in_direction(action.0, cx) - // }, - // ); + .on_action(Self::save_all) + .on_action(Self::add_folder_to_project) + .on_action(|workspace, _: &Unfollow, cx| { + let pane = workspace.active_pane().clone(); + workspace.unfollow(&pane, cx); + }) + .on_action(|workspace, action: &Save, cx| { + workspace + .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), cx) + .detach_and_log_err(cx); + }) + .on_action(|workspace, _: &SaveAs, cx| { + workspace + .save_active_item(SaveIntent::SaveAs, cx) + .detach_and_log_err(cx); + }) + .on_action(|workspace, _: &ActivatePreviousPane, cx| { + workspace.activate_previous_pane(cx) + }) + .on_action(|workspace, _: &ActivateNextPane, cx| workspace.activate_next_pane(cx)) + .on_action(|workspace, action: &ActivatePaneInDirection, cx| { + workspace.activate_pane_in_direction(action.0, cx) + }) + .on_action(|workspace, action: &SwapPaneInDirection, cx| { + workspace.swap_pane_in_direction(action.0, cx) + }) .on_action(|this, e: &ToggleLeftDock, cx| { this.toggle_dock(DockPosition::Left, cx); }) @@ -3374,6 +3351,12 @@ impl Workspace { } fn add_workspace_actions_listeners(&self, mut div: Div) -> Div { + let mut div = div + .on_action(Self::close_inactive_items_and_panes) + .on_action(Self::close_all_items_and_panes) + .on_action(Self::add_folder_to_project) + .on_action(Self::save_all) + .on_action(Self::open); for action in self.workspace_actions.iter() { div = (action)(div) } @@ -3599,8 +3582,6 @@ fn notify_if_database_failed(workspace: WindowHandle, cx: &mut AsyncA .log_err(); } -impl EventEmitter for Workspace {} - impl FocusableView for Workspace { fn focus_handle(&self, cx: &AppContext) -> FocusHandle { self.active_pane.focus_handle(cx) @@ -3624,7 +3605,7 @@ impl Render for Workspace { cx.set_rem_size(ui_font_size); - self.add_workspace_actions_listeners(div()) + Self::actions(self.add_workspace_actions_listeners(div())) .key_context(context) .relative() .size_full() @@ -3692,20 +3673,6 @@ impl Render for Workspace { ), ) .child(self.status_bar.clone()) - // .when(self.debug.show_toast, |this| { - // this.child(Toast::new(ToastOrigin::Bottom).child(Label::new("A toast"))) - // }) - // .children( - // Some( - // div() - // .absolute() - // .top(px(50.)) - // .left(px(640.)) - // .z_index(8) - // .child(LanguageSelector::new("language-selector")), - // ) - // .filter(|_| self.is_language_selector_open()), - // ) .z_index(8) // Debug .child( @@ -3717,43 +3684,12 @@ impl Render for Workspace { .top_20() .left_1_4() .w_40() - .gap_2(), // .when(self.show_debug, |this| { - // this.child(Button::::new("Toggle User Settings").on_click( - // Arc::new(|workspace, cx| workspace.debug_toggle_user_settings(cx)), - // )) - // .child( - // Button::::new("Toggle Toasts").on_click(Arc::new( - // |workspace, cx| workspace.debug_toggle_toast(cx), - // )), - // ) - // .child( - // Button::::new("Toggle Livestream").on_click(Arc::new( - // |workspace, cx| workspace.debug_toggle_livestream(cx), - // )), - // ) - // }) - // .child( - // Button::::new("Toggle Debug") - // .on_click(Arc::new(|workspace, cx| workspace.toggle_debug(cx))), - // ), + .gap_2(), ) } } -// todo!() -// impl Entity for Workspace { -// type Event = Event; - -// fn release(&mut self, cx: &mut AppContext) { -// self.app_state.workspace_store.update(cx, |store, _| { -// store.workspaces.remove(&self.weak_self); -// }) -// } -// } // impl View for Workspace { -// fn ui_name() -> &'static str { -// "Workspace" -// } // fn render(&mut self, cx: &mut ViewContext) -> AnyElement { // let theme = theme::current(cx).clone(); @@ -3879,12 +3815,6 @@ impl Render for Workspace { // .into_any_named("workspace") // } -// fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { -// if cx.is_self_focused() { -// cx.focus(&self.active_pane); -// } -// } - // fn modifiers_changed(&mut self, e: &ModifiersChangedEvent, cx: &mut ViewContext) -> bool { // DragAndDrop::::update_modifiers(e.modifiers, cx) // } @@ -4061,13 +3991,13 @@ impl WorkspaceHandle for View { } } -// impl std::fmt::Debug for OpenPaths { -// fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { -// f.debug_struct("OpenPaths") -// .field("paths", &self.paths) -// .finish() -// } -// } +impl std::fmt::Debug for OpenPaths { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("OpenPaths") + .field("paths", &self.paths) + .finish() + } +} pub struct WorkspaceCreated(pub WeakView); @@ -4530,7 +4460,7 @@ fn parse_pixel_size_env_var(value: &str) -> Option> { // mod tests { // use super::*; // use crate::{ -// dock::test::{TestPanel, TestPanelEvent}, +// dock::test::TestPanel, // item::test::{TestItem, TestItemEvent, TestProjectItem}, // }; // use fs::FakeFs; @@ -4615,7 +4545,7 @@ fn parse_pixel_size_env_var(value: &str) -> Option> { // let workspace = window.root(cx); // let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); // let worktree_id = project.read_with(cx, |project, cx| { -// project.worktrees(cx).next().unwrap().read(cx).id() +// project.worktrees().next().unwrap().read(cx).id() // }); // let item1 = window.build_view(cx, |cx| { From 89e44d49011d5dc303564cdecf7c1b6460623a9b Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 17 Nov 2023 09:56:28 +0200 Subject: [PATCH 37/37] Remove binary target collisions between zed & zed2 --- crates/live_kit_client2/Cargo.toml | 2 +- crates/live_kit_client2/examples/{test_app.rs => test_app2.rs} | 0 crates/zed2/Cargo.toml | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename crates/live_kit_client2/examples/{test_app.rs => test_app2.rs} (100%) diff --git a/crates/live_kit_client2/Cargo.toml b/crates/live_kit_client2/Cargo.toml index b606434b05..073c0017b0 100644 --- a/crates/live_kit_client2/Cargo.toml +++ b/crates/live_kit_client2/Cargo.toml @@ -10,7 +10,7 @@ path = "src/live_kit_client2.rs" doctest = false [[example]] -name = "test_app" +name = "test_app2" [features] test-support = [ diff --git a/crates/live_kit_client2/examples/test_app.rs b/crates/live_kit_client2/examples/test_app2.rs similarity index 100% rename from crates/live_kit_client2/examples/test_app.rs rename to crates/live_kit_client2/examples/test_app2.rs diff --git a/crates/zed2/Cargo.toml b/crates/zed2/Cargo.toml index df3332803e..aacaedca88 100644 --- a/crates/zed2/Cargo.toml +++ b/crates/zed2/Cargo.toml @@ -11,7 +11,7 @@ path = "src/zed2.rs" doctest = false [[bin]] -name = "Zed" +name = "Zed2" path = "src/main.rs" [dependencies]