diff --git a/.github/actions/run_tests/action.yml b/.github/actions/run_tests/action.yml index de5eadb61a4c78cc4c91e8e7c24f6d85b6745043..1ea51a06a6b4f26935e2c752beb0cad12139fcfb 100644 --- a/.github/actions/run_tests/action.yml +++ b/.github/actions/run_tests/action.yml @@ -19,16 +19,12 @@ runs: - name: Limit target directory size shell: bash -euxo pipefail {0} - run: script/clear-target-dir-if-larger-than 70 + run: script/clear-target-dir-if-larger-than 100 - name: Run check - env: - RUSTFLAGS: -D warnings shell: bash -euxo pipefail {0} run: cargo check --tests --workspace - name: Run tests - env: - RUSTFLAGS: -D warnings shell: bash -euxo pipefail {0} run: cargo nextest run --workspace --no-fail-fast diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 65475a41b90e473ac31e78517239b4692bcce98e..208d538976d4b477ba02c0c635d3b5a271916e7e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,6 +23,9 @@ jobs: - self-hosted - test steps: + - name: Set up default .cargo/config.toml + run: printf "[build]\nrustflags = [\"-D\", \"warnings\"]" > $HOME/.cargo/config.toml + - name: Checkout repo uses: actions/checkout@v3 with: @@ -87,7 +90,7 @@ jobs: submodules: "recursive" - name: Limit target directory size - run: script/clear-target-dir-if-larger-than 70 + run: script/clear-target-dir-if-larger-than 100 - name: Determine version and release channel if: ${{ startsWith(github.ref, 'refs/tags/v') }} diff --git a/.github/workflows/release_nightly.yml b/.github/workflows/release_nightly.yml index 447e928866d0bd877d1369a8da6c130bf2d9e8cd..7b08c52c61395b09e9925ddb99a31470049b021b 100644 --- a/.github/workflows/release_nightly.yml +++ b/.github/workflows/release_nightly.yml @@ -79,7 +79,7 @@ jobs: submodules: "recursive" - name: Limit target directory size - run: script/clear-target-dir-if-larger-than 70 + run: script/clear-target-dir-if-larger-than 100 - name: Set release channel to nightly run: | diff --git a/Cargo.lock b/Cargo.lock index 8d5309c717b75c17a17f9529021fc3083d807db7..8a279b2450b1c095c98b43c18b7e62a481a9d522 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1664,7 +1664,7 @@ dependencies = [ [[package]] name = "collab" -version = "0.28.0" +version = "0.29.0" dependencies = [ "anyhow", "async-trait", @@ -8031,6 +8031,35 @@ dependencies = [ "workspace", ] +[[package]] +name = "search2" +version = "0.1.0" +dependencies = [ + "anyhow", + "bitflags 1.3.2", + "client2", + "collections", + "editor2", + "futures 0.3.28", + "gpui2", + "language2", + "log", + "menu2", + "postage", + "project2", + "serde", + "serde_derive", + "serde_json", + "settings2", + "smallvec", + "smol", + "theme2", + "ui2", + "unindent", + "util", + "workspace2", +] + [[package]] name = "security-framework" version = "2.9.2" @@ -8854,6 +8883,13 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "story" +version = "0.1.0" +dependencies = [ + "gpui2", +] + [[package]] name = "storybook2" version = "0.1.0" @@ -8875,6 +8911,7 @@ dependencies = [ "settings2", "simplelog", "smallvec", + "story", "strum", "theme", "theme2", @@ -8882,17 +8919,6 @@ dependencies = [ "util", ] -[[package]] -name = "storybook3" -version = "0.1.0" -dependencies = [ - "anyhow", - "gpui2", - "settings2", - "theme2", - "ui2", -] - [[package]] name = "stringprep" version = "0.1.4" @@ -9396,6 +9422,7 @@ dependencies = [ "serde_derive", "serde_json", "settings2", + "story", "toml 0.5.11", "util", "uuid 1.4.1", @@ -10239,6 +10266,7 @@ dependencies = [ "serde", "settings2", "smallvec", + "story", "strum", "theme2", ] @@ -11465,7 +11493,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.114.0" +version = "0.115.0" dependencies = [ "activity_indicator", "ai", @@ -11660,6 +11688,7 @@ dependencies = [ "rsa 0.4.0", "rust-embed", "schemars", + "search2", "serde", "serde_derive", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index 7c51d32d05314e31a74e5452a938b2839bde4764..8840d912f02a6fce05e2d322991e35fad606b8e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -90,6 +90,7 @@ members = [ "crates/rpc", "crates/rpc2", "crates/search", + "crates/search2", "crates/settings", "crates/settings2", "crates/snippet", @@ -97,7 +98,6 @@ members = [ "crates/sqlez_macros", "crates/rich_text", "crates/storybook2", - "crates/storybook3", "crates/sum_tree", "crates/terminal", "crates/terminal2", @@ -110,6 +110,7 @@ members = [ "crates/ui2", "crates/util", "crates/semantic_index", + "crates/story", "crates/vim", "crates/vcs_menu", "crates/workspace2", diff --git a/assets/settings/default.json b/assets/settings/default.json index bf2acc708e9fe49653f02ab607059ea1b8a48725..221862ca987d4cc964daa8e436fe96a469e468b8 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -43,7 +43,7 @@ "calt": false }, // The default font size for text in the UI - "ui_font_size": 14, + "ui_font_size": 16, // 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/assets/settings/initial_user_settings.json b/assets/settings/initial_user_settings.json index dc79fd7911caeacf7510a142c9769aeb68f878c4..75d4a02626336c0f8af3e59f108b9847cc9dbf84 100644 --- a/assets/settings/initial_user_settings.json +++ b/assets/settings/initial_user_settings.json @@ -7,5 +7,6 @@ // custom settings, run the `open default settings` command // from the command palette or from `Zed` application menu. { - "buffer_font_size": 15 + "ui_font_size": 16, + "buffer_font_size": 16 } diff --git a/crates/auto_update2/src/update_notification.rs b/crates/auto_update2/src/update_notification.rs index 03a71bcabbba6111953a0754ef3455b317f771dd..e6a22b73248a8fce898c6871abb06d602a3a8e7a 100644 --- a/crates/auto_update2/src/update_notification.rs +++ b/crates/auto_update2/src/update_notification.rs @@ -8,8 +8,8 @@ pub struct UpdateNotification { impl EventEmitter for UpdateNotification {} -impl Render for UpdateNotification { - type Element = Div; +impl Render for UpdateNotification { + type Element = Div; fn render(&mut self, _cx: &mut gpui::ViewContext) -> Self::Element { div().child("Updated zed!") diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index dea6e09245bd3539d3e1b120dc9a8ee830dc7321..bbaf521e1532c32a62d59dfaa9cc31de5600c014 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] default-run = "collab" edition = "2021" name = "collab" -version = "0.28.0" +version = "0.29.0" publish = false [[bin]] diff --git a/crates/collab_ui2/src/collab_panel.rs b/crates/collab_ui2/src/collab_panel.rs index 901348d2e29b34454bac87b3dc7dff250b2ab835..6af188dfd200c82d21771a603b905a5e2377f182 100644 --- a/crates/collab_ui2/src/collab_panel.rs +++ b/crates/collab_ui2/src/collab_panel.rs @@ -3294,8 +3294,8 @@ impl CollabPanel { // .with_width(size.x()) // } -impl Render for CollabPanel { - type Element = Focusable>; +impl Render for CollabPanel { + type Element = Focusable
; fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { div() diff --git a/crates/collab_ui2/src/collab_titlebar_item.rs b/crates/collab_ui2/src/collab_titlebar_item.rs index 42800269c74c31769f15f1008e15ef301a75808b..94db2f4d9f60659cc2e520a2ed3533caa508f9af 100644 --- a/crates/collab_ui2/src/collab_titlebar_item.rs +++ b/crates/collab_ui2/src/collab_titlebar_item.rs @@ -37,7 +37,7 @@ use gpui::{ }; use project::Project; use theme::ActiveTheme; -use ui::{h_stack, Button, ButtonVariant, KeyBinding, Label, TextColor, Tooltip}; +use ui::{h_stack, Button, ButtonVariant, Color, KeyBinding, Label, Tooltip}; use workspace::Workspace; // const MAX_PROJECT_NAME_LENGTH: usize = 40; @@ -81,8 +81,8 @@ pub struct CollabTitlebarItem { _subscriptions: Vec, } -impl Render for CollabTitlebarItem { - type Element = Stateful>; +impl Render for CollabTitlebarItem { + type Element = Stateful
; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { h_stack() @@ -100,7 +100,7 @@ impl Render for CollabTitlebarItem { |s| s.pl(px(68.)), ) .bg(cx.theme().colors().title_bar_background) - .on_click(|_, event, cx| { + .on_click(|event, cx| { if event.up.click_count == 2 { cx.zoom_window(); } @@ -115,16 +115,16 @@ impl Render for CollabTitlebarItem { .child( Button::new("player") .variant(ButtonVariant::Ghost) - .color(Some(TextColor::Player(0))), + .color(Some(Color::Player(0))), ) - .tooltip(move |_, cx| Tooltip::text("Toggle following", cx)), + .tooltip(move |cx| Tooltip::text("Toggle following", cx)), ) // TODO - Add project menu .child( div() .id("titlebar_project_menu_button") .child(Button::new("project_name").variant(ButtonVariant::Ghost)) - .tooltip(move |_, cx| Tooltip::text("Recent Projects", cx)), + .tooltip(move |cx| Tooltip::text("Recent Projects", cx)), ) // TODO - Add git menu .child( @@ -133,9 +133,9 @@ impl Render for CollabTitlebarItem { .child( Button::new("branch_name") .variant(ButtonVariant::Ghost) - .color(Some(TextColor::Muted)), + .color(Some(Color::Muted)), ) - .tooltip(move |_, cx| { + .tooltip(move |cx| { cx.build_view(|_| { Tooltip::new("Recent Branches") .key_binding(KeyBinding::new(gpui::KeyBinding::new( diff --git a/crates/command_palette2/src/command_palette.rs b/crates/command_palette2/src/command_palette.rs index 1296f35c55f05424f13045e2688a740b248b1704..3c6f2fff92150fd302576635e7589eb238e5d01b 100644 --- a/crates/command_palette2/src/command_palette.rs +++ b/crates/command_palette2/src/command_palette.rs @@ -76,8 +76,8 @@ impl FocusableView for CommandPalette { } } -impl Render for CommandPalette { - type Element = Div; +impl Render for CommandPalette { + type Element = Div; fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { v_stack().w_96().child(self.picker.clone()) @@ -140,7 +140,7 @@ impl CommandPaletteDelegate { } impl PickerDelegate for CommandPaletteDelegate { - type ListItem = Div>; + type ListItem = Div; fn placeholder_text(&self) -> Arc { "Execute a command...".into() diff --git a/crates/diagnostics2/src/diagnostics.rs b/crates/diagnostics2/src/diagnostics.rs index 7203748470ce5b5093ca52fc1b13606fcc086600..7ff8cd84dbf072106d04337904350522b660a9e2 100644 --- a/crates/diagnostics2/src/diagnostics.rs +++ b/crates/diagnostics2/src/diagnostics.rs @@ -16,7 +16,7 @@ use gpui::{ actions, div, AnyElement, AnyView, AppContext, Context, Div, EventEmitter, FocusEvent, FocusHandle, Focusable, FocusableElement, FocusableView, InteractiveElement, Model, ParentElement, Render, RenderOnce, SharedString, Styled, Subscription, Task, View, ViewContext, - VisualContext, WeakView, + VisualContext, WeakView, WindowContext, }; use language::{ Anchor, Bias, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, Point, Selection, @@ -36,7 +36,7 @@ use std::{ }; use theme::ActiveTheme; pub use toolbar_controls::ToolbarControls; -use ui::{h_stack, HighlightedLabel, Icon, IconElement, Label, TextColor}; +use ui::{h_stack, Color, HighlightedLabel, Icon, IconElement, Label}; use util::TryFutureExt; use workspace::{ item::{BreadcrumbText, Item, ItemEvent, ItemHandle}, @@ -90,8 +90,8 @@ struct DiagnosticGroupState { impl EventEmitter for ProjectDiagnosticsEditor {} -impl Render for ProjectDiagnosticsEditor { - type Element = Focusable>; +impl Render for ProjectDiagnosticsEditor { + type Element = Focusable
; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { let child = if self.path_states.is_empty() { @@ -109,8 +109,8 @@ impl Render for ProjectDiagnosticsEditor { div() .track_focus(&self.focus_handle) .size_full() - .on_focus_in(Self::focus_in) - .on_action(Self::toggle_warnings) + .on_focus_in(cx.listener(Self::focus_in)) + .on_action(cx.listener(Self::toggle_warnings)) .child(child) } } @@ -662,7 +662,7 @@ impl Item for ProjectDiagnosticsEditor { Some("Project Diagnostics".into()) } - fn tab_content(&self, _detail: Option, _: &AppContext) -> AnyElement { + fn tab_content(&self, _detail: Option, _: &WindowContext) -> AnyElement { render_summary(&self.summary) } @@ -742,7 +742,7 @@ impl Item for ProjectDiagnosticsEditor { } fn breadcrumb_location(&self) -> ToolbarItemLocation { - ToolbarItemLocation::PrimaryLeft { flex: None } + ToolbarItemLocation::PrimaryLeft } fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option> { @@ -778,15 +778,15 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock { .bg(gpui::red()) .map(|stack| { let icon = if diagnostic.severity == DiagnosticSeverity::ERROR { - IconElement::new(Icon::XCircle).color(TextColor::Error) + IconElement::new(Icon::XCircle).color(Color::Error) } else { - IconElement::new(Icon::ExclamationTriangle).color(TextColor::Warning) + IconElement::new(Icon::ExclamationTriangle).color(Color::Warning) }; stack.child(div().pl_8().child(icon)) }) .when_some(diagnostic.source.as_ref(), |stack, source| { - stack.child(Label::new(format!("{source}:")).color(TextColor::Accent)) + stack.child(Label::new(format!("{source}:")).color(Color::Accent)) }) .child(HighlightedLabel::new(message.clone(), highlights.clone())) .when_some(diagnostic.code.as_ref(), |stack, code| { @@ -796,11 +796,10 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock { }) } -pub(crate) fn render_summary(summary: &DiagnosticSummary) -> AnyElement { +pub(crate) fn render_summary(summary: &DiagnosticSummary) -> AnyElement { if summary.error_count == 0 && summary.warning_count == 0 { let label = Label::new("No problems"); label.render_into_any() - //.render() } else { h_stack() .bg(gpui::red()) diff --git a/crates/diagnostics2/src/items.rs b/crates/diagnostics2/src/items.rs index 1d5183634f06bba5fd094c62cd8dfbfb753b9983..bbcfa748d447d66b19df5e2417a11eca1156aa49 100644 --- a/crates/diagnostics2/src/items.rs +++ b/crates/diagnostics2/src/items.rs @@ -7,7 +7,7 @@ use gpui::{ use language::Diagnostic; use lsp::LanguageServerId; use theme::ActiveTheme; -use ui::{h_stack, Icon, IconElement, Label, TextColor, Tooltip}; +use ui::{h_stack, Color, Icon, IconElement, Label, Tooltip}; use workspace::{item::ItemHandle, StatusItemView, ToolbarItemEvent, Workspace}; use crate::ProjectDiagnosticsEditor; @@ -21,31 +21,31 @@ pub struct DiagnosticIndicator { _observe_active_editor: Option, } -impl Render for DiagnosticIndicator { - type Element = Stateful>; +impl Render for DiagnosticIndicator { + type Element = Stateful
; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { let diagnostic_indicator = match (self.summary.error_count, self.summary.warning_count) { - (0, 0) => h_stack().child(IconElement::new(Icon::Check).color(TextColor::Success)), + (0, 0) => h_stack().child(IconElement::new(Icon::Check).color(Color::Success)), (0, warning_count) => h_stack() .gap_1() - .child(IconElement::new(Icon::ExclamationTriangle).color(TextColor::Warning)) + .child(IconElement::new(Icon::ExclamationTriangle).color(Color::Warning)) .child(Label::new(warning_count.to_string())), (error_count, 0) => h_stack() .gap_1() - .child(IconElement::new(Icon::XCircle).color(TextColor::Error)) + .child(IconElement::new(Icon::XCircle).color(Color::Error)) .child(Label::new(error_count.to_string())), (error_count, warning_count) => h_stack() .gap_1() - .child(IconElement::new(Icon::XCircle).color(TextColor::Error)) + .child(IconElement::new(Icon::XCircle).color(Color::Error)) .child(Label::new(error_count.to_string())) - .child(IconElement::new(Icon::ExclamationTriangle).color(TextColor::Warning)) + .child(IconElement::new(Icon::ExclamationTriangle).color(Color::Warning)) .child(Label::new(warning_count.to_string())), }; h_stack() .id(cx.entity_id()) - .on_action(Self::go_to_next_diagnostic) + .on_action(cx.listener(Self::go_to_next_diagnostic)) .rounded_md() .flex_none() .h(rems(1.375)) @@ -54,14 +54,14 @@ impl Render for DiagnosticIndicator { .bg(cx.theme().colors().ghost_element_background) .hover(|style| style.bg(cx.theme().colors().ghost_element_hover)) .active(|style| style.bg(cx.theme().colors().ghost_element_active)) - .tooltip(|_, cx| Tooltip::text("Project Diagnostics", cx)) - .on_click(|this, _, cx| { + .tooltip(|cx| Tooltip::text("Project Diagnostics", cx)) + .on_click(cx.listener(|this, _, cx| { if let Some(workspace) = this.workspace.upgrade() { workspace.update(cx, |workspace, cx| { ProjectDiagnosticsEditor::deploy(workspace, &Default::default(), cx) }) } - }) + })) .child(diagnostic_indicator) } } diff --git a/crates/diagnostics2/src/toolbar_controls.rs b/crates/diagnostics2/src/toolbar_controls.rs index a22217f0a1cbdb4ff45faa1bcb340f111ff4ee36..e513076ec829322d4393babc35b159c6b2bc3312 100644 --- a/crates/diagnostics2/src/toolbar_controls.rs +++ b/crates/diagnostics2/src/toolbar_controls.rs @@ -7,8 +7,8 @@ pub struct ToolbarControls { editor: Option>, } -impl Render for ToolbarControls { - type Element = Div; +impl Render for ToolbarControls { + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { let include_warnings = self @@ -26,14 +26,14 @@ impl Render for ToolbarControls { div().child( IconButton::new("toggle-warnings", Icon::ExclamationTriangle) - .tooltip(move |_, cx| Tooltip::text(tooltip, cx)) - .on_click(|this: &mut Self, cx| { + .tooltip(move |cx| Tooltip::text(tooltip, cx)) + .on_click(cx.listener(|this, _, cx| { if let Some(editor) = this.editor.as_ref().and_then(|editor| editor.upgrade()) { editor.update(cx, |editor, cx| { editor.toggle_warnings(&Default::default(), cx); }); } - }), + })), ) } } @@ -49,7 +49,7 @@ impl ToolbarItemView for ToolbarControls { if let Some(pane_item) = active_pane_item.as_ref() { if let Some(editor) = pane_item.downcast::() { self.editor = Some(editor.downgrade()); - ToolbarItemLocation::PrimaryRight { flex: None } + ToolbarItemLocation::PrimaryRight } else { ToolbarItemLocation::Hidden } diff --git a/crates/editor2/src/display_map/block_map.rs b/crates/editor2/src/display_map/block_map.rs index 05106dd2a1f1416529689750f77b2e264f4d5e83..00778c2eddc8eec3cccf3a3a2a9fe89355d26ded 100644 --- a/crates/editor2/src/display_map/block_map.rs +++ b/crates/editor2/src/display_map/block_map.rs @@ -50,7 +50,7 @@ struct BlockRow(u32); #[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)] struct WrapRow(u32); -pub type RenderBlock = Arc AnyElement>; +pub type RenderBlock = Arc AnyElement>; pub struct Block { id: BlockId, @@ -69,7 +69,7 @@ where pub position: P, pub height: u8, pub style: BlockStyle, - pub render: Arc AnyElement>, + pub render: Arc AnyElement>, pub disposition: BlockDisposition, } @@ -947,7 +947,7 @@ impl DerefMut for BlockContext<'_, '_> { } impl Block { - pub fn render(&self, cx: &mut BlockContext) -> AnyElement { + pub fn render(&self, cx: &mut BlockContext) -> AnyElement { self.render.lock()(cx) } diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index b53ae376d26c2bea757efe331c94850d4f644115..0b367b76565366032da752e3f6324b7569996f04 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -44,7 +44,7 @@ use gpui::{ EventEmitter, FocusHandle, FocusableView, FontFeatures, FontStyle, FontWeight, HighlightStyle, Hsla, InputHandler, KeyContext, Model, MouseButton, ParentElement, Pixels, Render, SharedString, Styled, Subscription, Task, TextStyle, UniformListScrollHandle, View, - ViewContext, VisualContext, WeakView, WindowContext, + ViewContext, VisualContext, WeakView, WhiteSpace, WindowContext, }; use highlight_matching_bracket::refresh_matching_bracket_highlights; use hover_popover::{hide_hover, HoverState}; @@ -54,13 +54,13 @@ use itertools::Itertools; pub use language::{char_kind, CharKind}; use language::{ language_settings::{self, all_language_settings, InlayHintSettings}, - point_from_lsp, AutoindentMode, BracketPair, Buffer, CodeAction, Completion, CursorShape, - Diagnostic, IndentKind, IndentSize, Language, LanguageRegistry, LanguageServerName, - OffsetRangeExt, Point, Selection, SelectionGoal, TransactionId, + point_from_lsp, AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion, + CursorShape, Diagnostic, Documentation, IndentKind, IndentSize, Language, LanguageRegistry, + LanguageServerName, OffsetRangeExt, Point, Selection, SelectionGoal, TransactionId, }; use lazy_static::lazy_static; use link_go_to_definition::{GoToDefinitionLink, InlayHighlight, LinkGoToDefinitionState}; -use lsp::{DiagnosticSeverity, Documentation, LanguageServerId}; +use lsp::{DiagnosticSeverity, LanguageServerId}; use movement::TextLayoutDetails; use multi_buffer::ToOffsetUtf16; pub use multi_buffer::{ @@ -97,7 +97,7 @@ use text::{OffsetUtf16, Rope}; use theme::{ ActiveTheme, DiagnosticStyle, PlayerColor, SyntaxTheme, Theme, ThemeColors, ThemeSettings, }; -use ui::{v_stack, HighlightedLabel, IconButton, StyledExt, Tooltip}; +use ui::{h_stack, v_stack, HighlightedLabel, IconButton, StyledExt, Tooltip}; use util::{post_inc, RangeExt, ResultExt, TryFutureExt}; use workspace::{ item::{ItemEvent, ItemHandle}, @@ -907,7 +907,7 @@ impl ContextMenu { style: &EditorStyle, workspace: Option>, cx: &mut ViewContext, - ) -> (DisplayPoint, AnyElement) { + ) -> (DisplayPoint, AnyElement) { match self { ContextMenu::Completions(menu) => (cursor_position, menu.render(style, workspace, cx)), ContextMenu::CodeActions(menu) => menu.render(cursor_position, style, cx), @@ -1223,208 +1223,202 @@ impl CompletionsMenu { style: &EditorStyle, workspace: Option>, cx: &mut ViewContext, - ) -> AnyElement { - todo!("old implementation below") - } + ) -> AnyElement { + let settings = EditorSettings::get_global(cx); + let show_completion_documentation = settings.show_completion_documentation; - // enum CompletionTag {} + let widest_completion_ix = self + .matches + .iter() + .enumerate() + .max_by_key(|(_, mat)| { + let completions = self.completions.read(); + let completion = &completions[mat.candidate_id]; + let documentation = &completion.documentation; + + let mut len = completion.label.text.chars().count(); + if let Some(Documentation::SingleLine(text)) = documentation { + if show_completion_documentation { + len += text.chars().count(); + } + } - // let settings = EditorSettings>(cx); - // let show_completion_documentation = settings.show_completion_documentation; + len + }) + .map(|(ix, _)| ix); - // let widest_completion_ix = self - // .matches - // .iter() - // .enumerate() - // .max_by_key(|(_, mat)| { - // let completions = self.completions.read(); - // let completion = &completions[mat.candidate_id]; - // let documentation = &completion.documentation; + let completions = self.completions.clone(); + let matches = self.matches.clone(); + let selected_item = self.selected_item; - // let mut len = completion.label.text.chars().count(); - // if let Some(Documentation::SingleLine(text)) = documentation { - // if show_completion_documentation { - // len += text.chars().count(); - // } - // } + let list = uniform_list( + cx.view().clone(), + "completions", + matches.len(), + move |editor, range, cx| { + let start_ix = range.start; + let completions_guard = completions.read(); - // len - // }) - // .map(|(ix, _)| ix); + matches[range] + .iter() + .enumerate() + .map(|(ix, mat)| { + let item_ix = start_ix + ix; + let candidate_id = mat.candidate_id; + let completion = &completions_guard[candidate_id]; - // let completions = self.completions.clone(); - // let matches = self.matches.clone(); - // let selected_item = self.selected_item; - - // let list = UniformList::new(self.list.clone(), matches.len(), cx, { - // let style = style.clone(); - // move |_, range, items, cx| { - // let start_ix = range.start; - // let completions_guard = completions.read(); - - // for (ix, mat) in matches[range].iter().enumerate() { - // let item_ix = start_ix + ix; - // let candidate_id = mat.candidate_id; - // let completion = &completions_guard[candidate_id]; - - // let documentation = if show_completion_documentation { - // &completion.documentation - // } else { - // &None - // }; + let documentation = if show_completion_documentation { + &completion.documentation + } else { + &None + }; - // items.push( - // MouseEventHandler::new::( - // mat.candidate_id, - // cx, - // |state, _| { - // let item_style = if item_ix == selected_item { - // style.autocomplete.selected_item - // } else if state.hovered() { - // style.autocomplete.hovered_item - // } else { - // style.autocomplete.item - // }; - - // let completion_label = - // Text::new(completion.label.text.clone(), style.text.clone()) - // .with_soft_wrap(false) - // .with_highlights( - // combine_syntax_and_fuzzy_match_highlights( - // &completion.label.text, - // style.text.color.into(), - // styled_runs_for_code_label( - // &completion.label, - // &style.syntax, - // ), - // &mat.positions, - // ), - // ); - - // if let Some(Documentation::SingleLine(text)) = documentation { - // Flex::row() - // .with_child(completion_label) - // .with_children((|| { - // let text_style = TextStyle { - // color: style.autocomplete.inline_docs_color, - // font_size: style.text.font_size - // * style.autocomplete.inline_docs_size_percent, - // ..style.text.clone() - // }; - - // let label = Text::new(text.clone(), text_style) - // .aligned() - // .constrained() - // .dynamically(move |constraint, _, _| { - // gpui::SizeConstraint { - // min: constraint.min, - // max: vec2f( - // constraint.max.x(), - // constraint.min.y(), - // ), - // } - // }); - - // if Some(item_ix) == widest_completion_ix { - // Some( - // label - // .contained() - // .with_style( - // style - // .autocomplete - // .inline_docs_container, - // ) - // .into_any(), - // ) - // } else { - // Some(label.flex_float().into_any()) - // } - // })()) - // .into_any() - // } else { - // completion_label.into_any() - // } - // .contained() - // .with_style(item_style) - // .constrained() - // .dynamically( - // move |constraint, _, _| { - // if Some(item_ix) == widest_completion_ix { - // constraint - // } else { - // gpui::SizeConstraint { - // min: constraint.min, - // max: constraint.min, - // } - // } - // }, - // ) - // }, - // ) - // .with_cursor_style(CursorStyle::PointingHand) - // .on_down(MouseButton::Left, move |_, this, cx| { - // this.confirm_completion( - // &ConfirmCompletion { - // item_ix: Some(item_ix), - // }, - // cx, - // ) - // .map(|task| task.detach()); - // }) - // .constrained() - // .with_min_width(style.autocomplete.completion_min_width) - // .with_max_width(style.autocomplete.completion_max_width) - // .into_any(), - // ); - // } - // } - // }) - // .with_width_from_item(widest_completion_ix); - - // enum MultiLineDocumentation {} - - // Flex::row() - // .with_child(list.flex(1., false)) - // .with_children({ - // let mat = &self.matches[selected_item]; - // let completions = self.completions.read(); - // let completion = &completions[mat.candidate_id]; - // let documentation = &completion.documentation; - - // match documentation { - // Some(Documentation::MultiLinePlainText(text)) => Some( - // Flex::column() - // .scrollable::(0, None, cx) - // .with_child( - // Text::new(text.clone(), style.text.clone()).with_soft_wrap(true), - // ) - // .contained() - // .with_style(style.autocomplete.alongside_docs_container) - // .constrained() - // .with_max_width(style.autocomplete.alongside_docs_max_width) - // .flex(1., false), - // ), - - // Some(Documentation::MultiLineMarkdown(parsed)) => Some( - // Flex::column() - // .scrollable::(0, None, cx) - // .with_child(render_parsed_markdown::( - // parsed, &style, workspace, cx, - // )) - // .contained() - // .with_style(style.autocomplete.alongside_docs_container) - // .constrained() - // .with_max_width(style.autocomplete.alongside_docs_max_width) - // .flex(1., false), - // ), - - // _ => None, - // } - // }) - // .contained() - // .with_style(style.autocomplete.container) - // .into_any() - // } + // todo!("highlights") + // let highlights = combine_syntax_and_fuzzy_match_highlights( + // &completion.label.text, + // style.text.color.into(), + // styled_runs_for_code_label(&completion.label, &style.syntax), + // &mat.positions, + // ) + + // todo!("documentation") + // MouseEventHandler::new::(mat.candidate_id, cx, |state, _| { + // let completion_label = HighlightedLabel::new( + // completion.label.text.clone(), + // combine_syntax_and_fuzzy_match_highlights( + // &completion.label.text, + // style.text.color.into(), + // styled_runs_for_code_label(&completion.label, &style.syntax), + // &mat.positions, + // ), + // ); + // Text::new(completion.label.text.clone(), style.text.clone()) + // .with_soft_wrap(false) + // .with_highlights(); + + // if let Some(Documentation::SingleLine(text)) = documentation { + // h_stack() + // .child(completion_label) + // .with_children((|| { + // let text_style = TextStyle { + // color: style.autocomplete.inline_docs_color, + // font_size: style.text.font_size + // * style.autocomplete.inline_docs_size_percent, + // ..style.text.clone() + // }; + + // let label = Text::new(text.clone(), text_style) + // .aligned() + // .constrained() + // .dynamically(move |constraint, _, _| gpui::SizeConstraint { + // min: constraint.min, + // max: vec2f(constraint.max.x(), constraint.min.y()), + // }); + + // if Some(item_ix) == widest_completion_ix { + // Some( + // label + // .contained() + // .with_style(style.autocomplete.inline_docs_container) + // .into_any(), + // ) + // } else { + // Some(label.flex_float().into_any()) + // } + // })()) + // .into_any() + // } else { + // completion_label.into_any() + // } + // .contained() + // .with_style(item_style) + // .constrained() + // .dynamically(move |constraint, _, _| { + // if Some(item_ix) == widest_completion_ix { + // constraint + // } else { + // gpui::SizeConstraint { + // min: constraint.min, + // max: constraint.min, + // } + // } + // }) + // }) + // .with_cursor_style(CursorStyle::PointingHand) + // .on_down(MouseButton::Left, move |_, this, cx| { + // this.confirm_completion( + // &ConfirmCompletion { + // item_ix: Some(item_ix), + // }, + // cx, + // ) + // .map(|task| task.detach()); + // }) + // .constrained() + // + div() + .id(mat.candidate_id) + .whitespace_nowrap() + .overflow_hidden() + .bg(gpui::green()) + .hover(|style| style.bg(gpui::blue())) + .when(item_ix == selected_item, |div| div.bg(gpui::red())) + .child(SharedString::from(completion.label.text.clone())) + .min_w(px(300.)) + .max_w(px(700.)) + }) + .collect() + }, + ) + .track_scroll(self.scroll_handle.clone()) + .with_width_from_item(widest_completion_ix); + + list.render_into_any() + // todo!("multiline documentation") + // enum MultiLineDocumentation {} + + // Flex::row() + // .with_child(list.flex(1., false)) + // .with_children({ + // let mat = &self.matches[selected_item]; + // let completions = self.completions.read(); + // let completion = &completions[mat.candidate_id]; + // let documentation = &completion.documentation; + + // match documentation { + // Some(Documentation::MultiLinePlainText(text)) => Some( + // Flex::column() + // .scrollable::(0, None, cx) + // .with_child( + // Text::new(text.clone(), style.text.clone()).with_soft_wrap(true), + // ) + // .contained() + // .with_style(style.autocomplete.alongside_docs_container) + // .constrained() + // .with_max_width(style.autocomplete.alongside_docs_max_width) + // .flex(1., false), + // ), + + // Some(Documentation::MultiLineMarkdown(parsed)) => Some( + // Flex::column() + // .scrollable::(0, None, cx) + // .with_child(render_parsed_markdown::( + // parsed, &style, workspace, cx, + // )) + // .contained() + // .with_style(style.autocomplete.alongside_docs_container) + // .constrained() + // .with_max_width(style.autocomplete.alongside_docs_max_width) + // .flex(1., false), + // ), + + // _ => None, + // } + // }) + // .contained() + // .with_style(style.autocomplete.container) + // .into_any() + } pub async fn filter(&mut self, query: Option<&str>, executor: BackgroundExecutor) { let mut matches = if let Some(query) = query { @@ -1541,13 +1535,15 @@ impl CodeActionsMenu { mut cursor_position: DisplayPoint, style: &EditorStyle, cx: &mut ViewContext, - ) -> (DisplayPoint, AnyElement) { + ) -> (DisplayPoint, AnyElement) { let actions = self.actions.clone(); let selected_item = self.selected_item; + let element = uniform_list( + cx.view().clone(), "code_actions_menu", self.actions.len(), - move |editor, range, cx| { + move |this, range, cx| { actions[range.clone()] .iter() .enumerate() @@ -1569,17 +1565,20 @@ impl CodeActionsMenu { .bg(colors.element_hover) .text_color(colors.text_accent) }) - .on_mouse_down(MouseButton::Left, move |editor: &mut Editor, _, cx| { - cx.stop_propagation(); - editor - .confirm_code_action( - &ConfirmCodeAction { - item_ix: Some(item_ix), - }, - cx, - ) - .map(|task| task.detach_and_log_err(cx)); - }) + .on_mouse_down( + MouseButton::Left, + cx.listener(move |editor, _, cx| { + cx.stop_propagation(); + editor + .confirm_code_action( + &ConfirmCodeAction { + item_ix: Some(item_ix), + }, + cx, + ) + .map(|task| task.detach_and_log_err(cx)); + }), + ) // TASK: It would be good to make lsp_action.title a SharedString to avoid allocating here. .child(SharedString::from(action.lsp_action.title.clone())) }) @@ -1589,6 +1588,7 @@ impl CodeActionsMenu { .elevation_1(cx) .px_2() .py_1() + .track_scroll(self.scroll_handle.clone()) .with_width_from_item( self.actions .iter() @@ -2320,6 +2320,7 @@ impl Editor { self.blink_manager.update(cx, BlinkManager::pause_blinking); cx.emit(EditorEvent::SelectionsChanged { local }); + cx.emit(SearchEvent::MatchesInvalidated); if self.selections.disjoint_anchors().len() == 1 { cx.emit(SearchEvent::ActiveMatchChanged) @@ -4354,11 +4355,11 @@ impl Editor { style: &EditorStyle, is_active: bool, cx: &mut ViewContext, - ) -> Option> { + ) -> Option { if self.available_code_actions.is_some() { Some( - IconButton::new("code_actions_indicator", ui::Icon::Bolt).on_click( - |editor: &mut Editor, cx| { + IconButton::new("code_actions_indicator", ui::Icon::Bolt).on_click(cx.listener( + |editor, e, cx| { editor.toggle_code_actions( &ToggleCodeActions { deployed_from_indicator: true, @@ -4366,7 +4367,7 @@ impl Editor { cx, ); }, - ), + )), ) } else { None @@ -4381,7 +4382,7 @@ impl Editor { line_height: Pixels, gutter_margin: Pixels, cx: &mut ViewContext, - ) -> Vec>> { + ) -> Vec> { fold_data .iter() .enumerate() @@ -4394,15 +4395,15 @@ impl Editor { FoldStatus::Foldable => ui::Icon::ChevronDown, }; IconButton::new(ix as usize, icon) - .on_click(move |editor: &mut Editor, cx| match fold_status { + .on_click(cx.listener(move |editor, e, cx| match fold_status { FoldStatus::Folded => { editor.unfold_at(&UnfoldAt { buffer_row }, cx); } FoldStatus::Foldable => { editor.fold_at(&FoldAt { buffer_row }, cx); } - }) - .color(ui::TextColor::Muted) + })) + .color(ui::Color::Muted) }) }) .flatten() @@ -4422,7 +4423,7 @@ impl Editor { cursor_position: DisplayPoint, style: &EditorStyle, cx: &mut ViewContext, - ) -> Option<(DisplayPoint, AnyElement)> { + ) -> Option<(DisplayPoint, AnyElement)> { self.context_menu.read().as_ref().map(|menu| { menu.render( cursor_position, @@ -7781,7 +7782,7 @@ impl Editor { } div() .pl(cx.anchor_x) - .child(rename_editor.render_with(EditorElement::new( + .child(EditorElement::new( &rename_editor, EditorStyle { background: cx.theme().system().transparent, @@ -7789,10 +7790,12 @@ impl Editor { text: text_style, scrollbar_width: cx.editor_style.scrollbar_width, syntax: cx.editor_style.syntax.clone(), - diagnostic_style: - cx.editor_style.diagnostic_style.clone(), + diagnostic_style: cx + .editor_style + .diagnostic_style + .clone(), }, - ))) + )) .render_into_any() } }), @@ -9388,7 +9391,7 @@ impl FocusableView for Editor { } } -impl Render for Editor { +impl Render for Editor { type Element = EditorElement; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { @@ -9403,6 +9406,7 @@ impl Render for Editor { font_style: FontStyle::Normal, line_height: relative(1.).into(), underline: None, + white_space: WhiteSpace::Normal, }, EditorMode::AutoHeight { max_lines } => todo!(), @@ -9416,6 +9420,7 @@ impl Render for Editor { font_style: FontStyle::Normal, line_height: relative(settings.buffer_line_height.value()), underline: None, + white_space: WhiteSpace::Normal, }, }; @@ -10002,10 +10007,10 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend .ml(cx.anchor_x) })) .cursor_pointer() - .on_click(move |_, _, cx| { + .on_click(cx.listener(move |_, _, cx| { cx.write_to_clipboard(ClipboardItem::new(message.clone())); - }) - .tooltip(|_, cx| Tooltip::text("Copy diagnostic message", cx)) + })) + .tooltip(|cx| Tooltip::text("Copy diagnostic message", cx)) .render_into_any() }) } @@ -10124,49 +10129,50 @@ pub fn combine_syntax_and_fuzzy_match_highlights( result } -// pub fn styled_runs_for_code_label<'a>( -// label: &'a CodeLabel, -// syntax_theme: &'a theme::SyntaxTheme, -// ) -> impl 'a + Iterator, HighlightStyle)> { -// let fade_out = HighlightStyle { -// fade_out: Some(0.35), -// ..Default::default() -// }; +pub fn styled_runs_for_code_label<'a>( + label: &'a CodeLabel, + syntax_theme: &'a theme::SyntaxTheme, +) -> impl 'a + Iterator, HighlightStyle)> { + let fade_out = HighlightStyle { + fade_out: Some(0.35), + ..Default::default() + }; + + let mut prev_end = label.filter_range.end; + label + .runs + .iter() + .enumerate() + .flat_map(move |(ix, (range, highlight_id))| { + let style = if let Some(style) = highlight_id.style(syntax_theme) { + style + } else { + return Default::default(); + }; + let mut muted_style = style; + muted_style.highlight(fade_out); -// let mut prev_end = label.filter_range.end; -// label -// .runs -// .iter() -// .enumerate() -// .flat_map(move |(ix, (range, highlight_id))| { -// let style = if let Some(style) = highlight_id.style(syntax_theme) { -// style -// } else { -// return Default::default(); -// }; -// let mut muted_style = style; -// muted_style.highlight(fade_out); - -// let mut runs = SmallVec::<[(Range, HighlightStyle); 3]>::new(); -// if range.start >= label.filter_range.end { -// if range.start > prev_end { -// runs.push((prev_end..range.start, fade_out)); -// } -// runs.push((range.clone(), muted_style)); -// } else if range.end <= label.filter_range.end { -// runs.push((range.clone(), style)); -// } else { -// runs.push((range.start..label.filter_range.end, style)); -// runs.push((label.filter_range.end..range.end, muted_style)); -// } -// prev_end = cmp::max(prev_end, range.end); + let mut runs = SmallVec::<[(Range, HighlightStyle); 3]>::new(); + if range.start >= label.filter_range.end { + if range.start > prev_end { + runs.push((prev_end..range.start, fade_out)); + } + runs.push((range.clone(), muted_style)); + } else if range.end <= label.filter_range.end { + runs.push((range.clone(), style)); + } else { + runs.push((range.start..label.filter_range.end, style)); + runs.push((label.filter_range.end..range.end, muted_style)); + } + prev_end = cmp::max(prev_end, range.end); -// if ix + 1 == label.runs.len() && label.text.len() > prev_end { -// runs.push((prev_end..label.text.len(), fade_out)); -// } + if ix + 1 == label.runs.len() && label.text.len() > prev_end { + runs.push((prev_end..label.text.len(), fade_out)); + } -// runs -// }) + runs + }) +} pub fn split_words<'a>(text: &'a str) -> impl std::iter::Iterator + 'a { let mut index = 0; diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index 42cb47da4951c2f616a470bc589fbd333d922c8c..7f6135087a1aedca460ae5fdd7442e30852a53eb 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -23,7 +23,7 @@ use gpui::{ ElementId, ElementInputHandler, Entity, EntityId, Hsla, InteractiveElement, LineLayout, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, RenderOnce, ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled, - TextRun, TextStyle, View, ViewContext, WindowContext, WrappedLine, + TextRun, TextStyle, View, ViewContext, WeakView, WindowContext, WrappedLine, }; use itertools::Itertools; use language::language_settings::ShowWhitespaceSetting; @@ -112,18 +112,192 @@ impl SelectionLayout { } pub struct EditorElement { - editor_id: EntityId, + editor: View, style: EditorStyle, } impl EditorElement { pub fn new(editor: &View, style: EditorStyle) -> Self { Self { - editor_id: editor.entity_id(), + editor: editor.clone(), style, } } + fn register_actions(&self, cx: &mut WindowContext) { + let view = &self.editor; + register_action(view, cx, Editor::move_left); + register_action(view, cx, Editor::move_right); + register_action(view, cx, Editor::move_down); + register_action(view, cx, Editor::move_up); + // on_action(cx, Editor::new_file); todo!() + // on_action(cx, Editor::new_file_in_direction); todo!() + register_action(view, cx, Editor::cancel); + register_action(view, cx, Editor::newline); + register_action(view, cx, Editor::newline_above); + register_action(view, cx, Editor::newline_below); + register_action(view, cx, Editor::backspace); + register_action(view, cx, Editor::delete); + register_action(view, cx, Editor::tab); + register_action(view, cx, Editor::tab_prev); + register_action(view, cx, Editor::indent); + register_action(view, cx, Editor::outdent); + register_action(view, cx, Editor::delete_line); + register_action(view, cx, Editor::join_lines); + register_action(view, cx, Editor::sort_lines_case_sensitive); + register_action(view, cx, Editor::sort_lines_case_insensitive); + register_action(view, cx, Editor::reverse_lines); + register_action(view, cx, Editor::shuffle_lines); + register_action(view, cx, Editor::convert_to_upper_case); + register_action(view, cx, Editor::convert_to_lower_case); + register_action(view, cx, Editor::convert_to_title_case); + register_action(view, cx, Editor::convert_to_snake_case); + register_action(view, cx, Editor::convert_to_kebab_case); + register_action(view, cx, Editor::convert_to_upper_camel_case); + register_action(view, cx, Editor::convert_to_lower_camel_case); + register_action(view, cx, Editor::delete_to_previous_word_start); + register_action(view, cx, Editor::delete_to_previous_subword_start); + register_action(view, cx, Editor::delete_to_next_word_end); + register_action(view, cx, Editor::delete_to_next_subword_end); + register_action(view, cx, Editor::delete_to_beginning_of_line); + register_action(view, cx, Editor::delete_to_end_of_line); + register_action(view, cx, Editor::cut_to_end_of_line); + register_action(view, cx, Editor::duplicate_line); + register_action(view, cx, Editor::move_line_up); + register_action(view, cx, Editor::move_line_down); + register_action(view, cx, Editor::transpose); + register_action(view, cx, Editor::cut); + register_action(view, cx, Editor::copy); + register_action(view, cx, Editor::paste); + register_action(view, cx, Editor::undo); + register_action(view, cx, Editor::redo); + register_action(view, cx, Editor::move_page_up); + register_action(view, cx, Editor::move_page_down); + register_action(view, cx, Editor::next_screen); + register_action(view, cx, Editor::scroll_cursor_top); + register_action(view, cx, Editor::scroll_cursor_center); + register_action(view, cx, Editor::scroll_cursor_bottom); + register_action(view, cx, |editor, _: &LineDown, cx| { + editor.scroll_screen(&ScrollAmount::Line(1.), cx) + }); + register_action(view, cx, |editor, _: &LineUp, cx| { + editor.scroll_screen(&ScrollAmount::Line(-1.), cx) + }); + register_action(view, cx, |editor, _: &HalfPageDown, cx| { + editor.scroll_screen(&ScrollAmount::Page(0.5), cx) + }); + register_action(view, cx, |editor, _: &HalfPageUp, cx| { + editor.scroll_screen(&ScrollAmount::Page(-0.5), cx) + }); + register_action(view, cx, |editor, _: &PageDown, cx| { + editor.scroll_screen(&ScrollAmount::Page(1.), cx) + }); + register_action(view, cx, |editor, _: &PageUp, cx| { + editor.scroll_screen(&ScrollAmount::Page(-1.), cx) + }); + register_action(view, cx, Editor::move_to_previous_word_start); + register_action(view, cx, Editor::move_to_previous_subword_start); + register_action(view, cx, Editor::move_to_next_word_end); + register_action(view, cx, Editor::move_to_next_subword_end); + register_action(view, cx, Editor::move_to_beginning_of_line); + register_action(view, cx, Editor::move_to_end_of_line); + register_action(view, cx, Editor::move_to_start_of_paragraph); + register_action(view, cx, Editor::move_to_end_of_paragraph); + register_action(view, cx, Editor::move_to_beginning); + register_action(view, cx, Editor::move_to_end); + register_action(view, cx, Editor::select_up); + register_action(view, cx, Editor::select_down); + register_action(view, cx, Editor::select_left); + register_action(view, cx, Editor::select_right); + register_action(view, cx, Editor::select_to_previous_word_start); + register_action(view, cx, Editor::select_to_previous_subword_start); + register_action(view, cx, Editor::select_to_next_word_end); + register_action(view, cx, Editor::select_to_next_subword_end); + register_action(view, cx, Editor::select_to_beginning_of_line); + register_action(view, cx, Editor::select_to_end_of_line); + register_action(view, cx, Editor::select_to_start_of_paragraph); + register_action(view, cx, Editor::select_to_end_of_paragraph); + register_action(view, cx, Editor::select_to_beginning); + register_action(view, cx, Editor::select_to_end); + register_action(view, cx, Editor::select_all); + register_action(view, cx, |editor, action, cx| { + editor.select_all_matches(action, cx).log_err(); + }); + register_action(view, cx, Editor::select_line); + register_action(view, cx, Editor::split_selection_into_lines); + register_action(view, cx, Editor::add_selection_above); + register_action(view, cx, Editor::add_selection_below); + register_action(view, cx, |editor, action, cx| { + editor.select_next(action, cx).log_err(); + }); + register_action(view, cx, |editor, action, cx| { + editor.select_previous(action, cx).log_err(); + }); + register_action(view, cx, Editor::toggle_comments); + register_action(view, cx, Editor::select_larger_syntax_node); + register_action(view, cx, Editor::select_smaller_syntax_node); + register_action(view, cx, Editor::move_to_enclosing_bracket); + register_action(view, cx, Editor::undo_selection); + register_action(view, cx, Editor::redo_selection); + register_action(view, cx, Editor::go_to_diagnostic); + register_action(view, cx, Editor::go_to_prev_diagnostic); + register_action(view, cx, Editor::go_to_hunk); + register_action(view, cx, Editor::go_to_prev_hunk); + register_action(view, cx, Editor::go_to_definition); + register_action(view, cx, Editor::go_to_definition_split); + register_action(view, cx, Editor::go_to_type_definition); + register_action(view, cx, Editor::go_to_type_definition_split); + register_action(view, cx, Editor::fold); + register_action(view, cx, Editor::fold_at); + register_action(view, cx, Editor::unfold_lines); + register_action(view, cx, Editor::unfold_at); + register_action(view, cx, Editor::fold_selected_ranges); + register_action(view, cx, Editor::show_completions); + register_action(view, cx, Editor::toggle_code_actions); + // on_action(cx, Editor::open_excerpts); todo!() + register_action(view, cx, Editor::toggle_soft_wrap); + register_action(view, cx, Editor::toggle_inlay_hints); + register_action(view, cx, Editor::reveal_in_finder); + register_action(view, cx, Editor::copy_path); + register_action(view, cx, Editor::copy_relative_path); + register_action(view, cx, Editor::copy_highlight_json); + register_action(view, cx, |editor, action, cx| { + editor + .format(action, cx) + .map(|task| task.detach_and_log_err(cx)); + }); + register_action(view, cx, Editor::restart_language_server); + register_action(view, cx, Editor::show_character_palette); + // on_action(cx, Editor::confirm_completion); todo!() + register_action(view, cx, |editor, action, cx| { + editor + .confirm_code_action(action, cx) + .map(|task| task.detach_and_log_err(cx)); + }); + register_action(view, cx, |editor, action, cx| { + editor + .rename(action, cx) + .map(|task| task.detach_and_log_err(cx)); + }); + register_action(view, cx, |editor, action, cx| { + editor + .confirm_rename(action, cx) + .map(|task| task.detach_and_log_err(cx)); + }); + register_action(view, cx, |editor, action, cx| { + editor + .find_all_references(action, cx) + .map(|task| task.detach_and_log_err(cx)); + }); + register_action(view, cx, Editor::next_copilot_suggestion); + register_action(view, cx, Editor::previous_copilot_suggestion); + register_action(view, cx, Editor::copilot_suggest); + register_action(view, cx, Editor::context_menu_first); + register_action(view, cx, Editor::context_menu_prev); + register_action(view, cx, Editor::context_menu_next); + register_action(view, cx, Editor::context_menu_last); + } + fn mouse_down( editor: &mut Editor, event: &MouseDownEvent, @@ -349,7 +523,7 @@ impl EditorElement { gutter_bounds: Bounds, text_bounds: Bounds, layout: &LayoutState, - cx: &mut ViewContext, + cx: &mut WindowContext, ) { let bounds = gutter_bounds.union(&text_bounds); let scroll_top = @@ -459,8 +633,7 @@ impl EditorElement { &mut self, bounds: Bounds, layout: &mut LayoutState, - editor: &mut Editor, - cx: &mut ViewContext, + cx: &mut WindowContext, ) { let line_height = layout.position_map.line_height; @@ -495,7 +668,7 @@ impl EditorElement { AvailableSpace::MinContent, AvailableSpace::Definite(line_height * 0.55), ); - let fold_indicator_size = fold_indicator.measure(available_space, editor, cx); + let fold_indicator_size = fold_indicator.measure(available_space, cx); let position = point( bounds.size.width - layout.gutter_padding, @@ -506,7 +679,7 @@ impl EditorElement { (line_height - fold_indicator_size.height) / 2., ); let origin = bounds.origin + position + centering_offset; - fold_indicator.draw(origin, available_space, editor, cx); + fold_indicator.draw(origin, available_space, cx); } } @@ -516,7 +689,7 @@ impl EditorElement { AvailableSpace::MinContent, AvailableSpace::Definite(line_height), ); - let indicator_size = button.measure(available_space, editor, cx); + let indicator_size = button.measure(available_space, cx); let mut x = Pixels::ZERO; let mut y = indicator.row as f32 * line_height - scroll_top; @@ -524,15 +697,11 @@ impl EditorElement { x += ((layout.gutter_padding + layout.gutter_margin) - indicator_size.width) / 2.; y += (line_height - indicator_size.height) / 2.; - button.draw(bounds.origin + point(x, y), available_space, editor, cx); + button.draw(bounds.origin + point(x, y), available_space, cx); } } - fn paint_diff_hunks( - bounds: Bounds, - layout: &LayoutState, - cx: &mut ViewContext, - ) { + fn paint_diff_hunks(bounds: Bounds, layout: &LayoutState, cx: &mut WindowContext) { // todo!() // let diff_style = &theme::current(cx).editor.diff.clone(); // let line_height = layout.position_map.line_height; @@ -620,14 +789,19 @@ impl EditorElement { &mut self, text_bounds: Bounds, layout: &mut LayoutState, - editor: &mut Editor, - cx: &mut ViewContext, + cx: &mut WindowContext, ) { let scroll_position = layout.position_map.snapshot.scroll_position(); let start_row = layout.visible_display_row_range.start; let content_origin = text_bounds.origin + point(layout.gutter_margin, Pixels::ZERO); let line_end_overshoot = 0.15 * layout.position_map.line_height; - let whitespace_setting = editor.buffer.read(cx).settings_at(0, cx).show_whitespaces; + let whitespace_setting = self + .editor + .read(cx) + .buffer + .read(cx) + .settings_at(0, cx) + .show_whitespaces; cx.with_content_mask( Some(ContentMask { @@ -676,20 +850,22 @@ impl EditorElement { div() .id(fold.id) .size_full() - .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) - .on_click(move |editor: &mut Editor, _, cx| { - editor.unfold_ranges( - [fold_range.start..fold_range.end], - true, - false, - cx, - ); - cx.stop_propagation(); - }) + .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation()) + .on_click(cx.listener_for( + &self.editor, + move |editor: &mut Editor, _, cx| { + editor.unfold_ranges( + [fold_range.start..fold_range.end], + true, + false, + cx, + ); + cx.stop_propagation(); + }, + )) .draw( fold_bounds.origin, fold_bounds.size, - editor, cx, |fold_element_state, cx| { if fold_element_state.is_active() { @@ -750,7 +926,7 @@ impl EditorElement { invisible_display_ranges.push(selection.range.clone()); } - if !selection.is_local || editor.show_local_cursors(cx) { + if !selection.is_local || self.editor.read(cx).show_local_cursors(cx) { let cursor_position = selection.head; if layout .visible_display_row_range @@ -802,12 +978,14 @@ impl EditorElement { * layout.position_map.line_height - layout.position_map.scroll_position.y; if selection.is_newest { - editor.pixel_position_of_newest_cursor = Some(point( - text_bounds.origin.x + x + block_width / 2., - text_bounds.origin.y - + y - + layout.position_map.line_height / 2., - )); + self.editor.update(cx, |editor, _| { + editor.pixel_position_of_newest_cursor = Some(point( + text_bounds.origin.x + x + block_width / 2., + text_bounds.origin.y + + y + + layout.position_map.line_height / 2., + )) + }); } cursors.push(Cursor { color: selection_style.cursor, @@ -852,7 +1030,7 @@ impl EditorElement { .min((text_bounds.size.height - line_height) / 2.), ), ); - let context_menu_size = context_menu.measure(available_space, editor, cx); + let context_menu_size = context_menu.measure(available_space, cx); let cursor_row_layout = &layout.position_map.line_layouts [(position.row() - start_row) as usize] @@ -876,7 +1054,7 @@ impl EditorElement { list_origin.y -= layout.position_map.line_height - list_height; } - context_menu.draw(list_origin, available_space, editor, cx); + context_menu.draw(list_origin, available_space, cx); }) } @@ -1167,7 +1345,7 @@ impl EditorElement { layout: &LayoutState, content_origin: gpui::Point, bounds: Bounds, - cx: &mut ViewContext, + cx: &mut WindowContext, ) { let start_row = layout.visible_display_row_range.start; let end_row = layout.visible_display_row_range.end; @@ -1219,8 +1397,7 @@ impl EditorElement { &mut self, bounds: Bounds, layout: &mut LayoutState, - editor: &mut Editor, - cx: &mut ViewContext, + cx: &mut WindowContext, ) { let scroll_position = layout.position_map.snapshot.scroll_position(); let scroll_left = scroll_position.x * layout.position_map.em_width; @@ -1235,13 +1412,11 @@ impl EditorElement { if !matches!(block.style, BlockStyle::Sticky) { origin += point(-scroll_left, Pixels::ZERO); } - block - .element - .draw(origin, block.available_space, editor, cx); + block.element.draw(origin, block.available_space, cx); } } - fn column_pixels(&self, column: usize, cx: &ViewContext) -> Pixels { + fn column_pixels(&self, column: usize, cx: &WindowContext) -> Pixels { let style = &self.style; let font_size = style.text.font_size.to_pixels(cx.rem_size()); let layout = cx @@ -1262,7 +1437,7 @@ impl EditorElement { layout.width } - fn max_line_number_width(&self, snapshot: &EditorSnapshot, cx: &ViewContext) -> Pixels { + fn max_line_number_width(&self, snapshot: &EditorSnapshot, cx: &WindowContext) -> Pixels { let digit_count = (snapshot.max_buffer_row() as f32 + 1.).log10().floor() as usize + 1; self.column_pixels(digit_count, cx) } @@ -1417,7 +1592,7 @@ impl EditorElement { } fn layout_lines( - &mut self, + &self, rows: Range, line_number_layouts: &[Option], snapshot: &EditorSnapshot, @@ -1473,483 +1648,469 @@ impl EditorElement { fn compute_layout( &mut self, - editor: &mut Editor, - cx: &mut ViewContext<'_, Editor>, mut bounds: Bounds, + cx: &mut WindowContext, ) -> LayoutState { - // let mut size = constraint.max; - // if size.x.is_infinite() { - // unimplemented!("we don't yet handle an infinite width constraint on buffer elements"); - // } - - let snapshot = editor.snapshot(cx); - let style = self.style.clone(); + self.editor.update(cx, |editor, cx| { + // let mut size = constraint.max; + // if size.x.is_infinite() { + // unimplemented!("we don't yet handle an infinite width constraint on buffer elements"); + // } + + let snapshot = editor.snapshot(cx); + let style = self.style.clone(); + + let font_id = cx.text_system().font_id(&style.text.font()).unwrap(); + let font_size = style.text.font_size.to_pixels(cx.rem_size()); + let line_height = style.text.line_height_in_pixels(cx.rem_size()); + let em_width = cx + .text_system() + .typographic_bounds(font_id, font_size, 'm') + .unwrap() + .size + .width; + let em_advance = cx + .text_system() + .advance(font_id, font_size, 'm') + .unwrap() + .width; + + let gutter_padding; + let gutter_width; + let gutter_margin; + if snapshot.show_gutter { + let descent = cx.text_system().descent(font_id, font_size).unwrap(); + + let gutter_padding_factor = 3.5; + gutter_padding = (em_width * gutter_padding_factor).round(); + gutter_width = self.max_line_number_width(&snapshot, cx) + gutter_padding * 2.0; + gutter_margin = -descent; + } else { + gutter_padding = Pixels::ZERO; + gutter_width = Pixels::ZERO; + gutter_margin = Pixels::ZERO; + }; - let font_id = cx.text_system().font_id(&style.text.font()).unwrap(); - let font_size = style.text.font_size.to_pixels(cx.rem_size()); - let line_height = style.text.line_height_in_pixels(cx.rem_size()); - let em_width = cx - .text_system() - .typographic_bounds(font_id, font_size, 'm') - .unwrap() - .size - .width; - let em_advance = cx - .text_system() - .advance(font_id, font_size, 'm') - .unwrap() - .width; - - let gutter_padding; - let gutter_width; - let gutter_margin; - if snapshot.show_gutter { - let descent = cx.text_system().descent(font_id, font_size).unwrap(); - - let gutter_padding_factor = 3.5; - gutter_padding = (em_width * gutter_padding_factor).round(); - gutter_width = self.max_line_number_width(&snapshot, cx) + gutter_padding * 2.0; - gutter_margin = -descent; - } else { - gutter_padding = Pixels::ZERO; - gutter_width = Pixels::ZERO; - gutter_margin = Pixels::ZERO; - }; + editor.gutter_width = gutter_width; + let text_width = bounds.size.width - gutter_width; + let overscroll = size(em_width, px(0.)); + let snapshot = { + editor.set_visible_line_count((bounds.size.height / line_height).into(), cx); + + let editor_width = text_width - gutter_margin - overscroll.width - em_width; + let wrap_width = match editor.soft_wrap_mode(cx) { + SoftWrap::None => (MAX_LINE_LEN / 2) as f32 * em_advance, + SoftWrap::EditorWidth => editor_width, + SoftWrap::Column(column) => editor_width.min(column as f32 * em_advance), + }; - editor.gutter_width = gutter_width; - let text_width = bounds.size.width - gutter_width; - let overscroll = size(em_width, px(0.)); - let snapshot = { - editor.set_visible_line_count((bounds.size.height / line_height).into(), cx); - - let editor_width = text_width - gutter_margin - overscroll.width - em_width; - let wrap_width = match editor.soft_wrap_mode(cx) { - SoftWrap::None => (MAX_LINE_LEN / 2) as f32 * em_advance, - SoftWrap::EditorWidth => editor_width, - SoftWrap::Column(column) => editor_width.min(column as f32 * em_advance), + if editor.set_wrap_width(Some(wrap_width), cx) { + editor.snapshot(cx) + } else { + snapshot + } }; - if editor.set_wrap_width(Some(wrap_width), cx) { - editor.snapshot(cx) + let wrap_guides = editor + .wrap_guides(cx) + .iter() + .map(|(guide, active)| (self.column_pixels(*guide, cx), *active)) + .collect::>(); + + let scroll_height = Pixels::from(snapshot.max_point().row() + 1) * line_height; + // todo!("this should happen during layout") + let editor_mode = snapshot.mode; + if let EditorMode::AutoHeight { max_lines } = editor_mode { + todo!() + // size.set_y( + // scroll_height + // .min(constraint.max_along(Axis::Vertical)) + // .max(constraint.min_along(Axis::Vertical)) + // .max(line_height) + // .min(line_height * max_lines as f32), + // ) + } else if let EditorMode::SingleLine = editor_mode { + bounds.size.height = line_height.min(bounds.size.height); + } + // todo!() + // else if size.y.is_infinite() { + // // size.set_y(scroll_height); + // } + // + let gutter_size = size(gutter_width, bounds.size.height); + let text_size = size(text_width, bounds.size.height); + + let autoscroll_horizontally = + editor.autoscroll_vertically(bounds.size.height, line_height, cx); + let mut snapshot = editor.snapshot(cx); + + let scroll_position = snapshot.scroll_position(); + // The scroll position is a fractional point, the whole number of which represents + // the top of the window in terms of display rows. + let start_row = scroll_position.y as u32; + let height_in_lines = f32::from(bounds.size.height / line_height); + let max_row = snapshot.max_point().row(); + + // Add 1 to ensure selections bleed off screen + let end_row = 1 + cmp::min((scroll_position.y + height_in_lines).ceil() as u32, max_row); + + let start_anchor = if start_row == 0 { + Anchor::min() } else { snapshot - } - }; + .buffer_snapshot + .anchor_before(DisplayPoint::new(start_row, 0).to_offset(&snapshot, Bias::Left)) + }; + let end_anchor = if end_row > max_row { + Anchor::max() + } else { + snapshot + .buffer_snapshot + .anchor_before(DisplayPoint::new(end_row, 0).to_offset(&snapshot, Bias::Right)) + }; - let wrap_guides = editor - .wrap_guides(cx) - .iter() - .map(|(guide, active)| (self.column_pixels(*guide, cx), *active)) - .collect::>(); - - let scroll_height = Pixels::from(snapshot.max_point().row() + 1) * line_height; - // todo!("this should happen during layout") - let editor_mode = snapshot.mode; - if let EditorMode::AutoHeight { max_lines } = editor_mode { - todo!() - // size.set_y( - // scroll_height - // .min(constraint.max_along(Axis::Vertical)) - // .max(constraint.min_along(Axis::Vertical)) - // .max(line_height) - // .min(line_height * max_lines as f32), - // ) - } else if let EditorMode::SingleLine = editor_mode { - bounds.size.height = line_height.min(bounds.size.height); - } - // todo!() - // else if size.y.is_infinite() { - // // size.set_y(scroll_height); - // } - // - let gutter_size = size(gutter_width, bounds.size.height); - let text_size = size(text_width, bounds.size.height); - - let autoscroll_horizontally = - editor.autoscroll_vertically(bounds.size.height, line_height, cx); - let mut snapshot = editor.snapshot(cx); - - let scroll_position = snapshot.scroll_position(); - // The scroll position is a fractional point, the whole number of which represents - // the top of the window in terms of display rows. - let start_row = scroll_position.y as u32; - let height_in_lines = f32::from(bounds.size.height / line_height); - let max_row = snapshot.max_point().row(); - - // Add 1 to ensure selections bleed off screen - let end_row = 1 + cmp::min((scroll_position.y + height_in_lines).ceil() as u32, max_row); - - let start_anchor = if start_row == 0 { - Anchor::min() - } else { - snapshot - .buffer_snapshot - .anchor_before(DisplayPoint::new(start_row, 0).to_offset(&snapshot, Bias::Left)) - }; - let end_anchor = if end_row > max_row { - Anchor::max() - } else { - snapshot - .buffer_snapshot - .anchor_before(DisplayPoint::new(end_row, 0).to_offset(&snapshot, Bias::Right)) - }; + let mut selections: Vec<(PlayerColor, Vec)> = Vec::new(); + let mut active_rows = BTreeMap::new(); + let is_singleton = editor.is_singleton(cx); - let mut selections: Vec<(PlayerColor, Vec)> = Vec::new(); - let mut active_rows = BTreeMap::new(); - let is_singleton = editor.is_singleton(cx); + let highlighted_rows = editor.highlighted_rows(); + let highlighted_ranges = editor.background_highlights_in_range( + start_anchor..end_anchor, + &snapshot.display_snapshot, + cx.theme().colors(), + ); - let highlighted_rows = editor.highlighted_rows(); - let highlighted_ranges = editor.background_highlights_in_range( - start_anchor..end_anchor, - &snapshot.display_snapshot, - cx.theme().colors(), - ); + let mut newest_selection_head = None; + + if editor.show_local_selections { + let mut local_selections: Vec> = editor + .selections + .disjoint_in_range(start_anchor..end_anchor, cx); + local_selections.extend(editor.selections.pending(cx)); + let mut layouts = Vec::new(); + let newest = editor.selections.newest(cx); + for selection in local_selections.drain(..) { + let is_empty = selection.start == selection.end; + let is_newest = selection == newest; + + let layout = SelectionLayout::new( + selection, + editor.selections.line_mode, + editor.cursor_shape, + &snapshot.display_snapshot, + is_newest, + true, + ); + if is_newest { + newest_selection_head = Some(layout.head); + } - let mut newest_selection_head = None; - - if editor.show_local_selections { - let mut local_selections: Vec> = editor - .selections - .disjoint_in_range(start_anchor..end_anchor, cx); - local_selections.extend(editor.selections.pending(cx)); - let mut layouts = Vec::new(); - let newest = editor.selections.newest(cx); - for selection in local_selections.drain(..) { - let is_empty = selection.start == selection.end; - let is_newest = selection == newest; - - let layout = SelectionLayout::new( - selection, - editor.selections.line_mode, - editor.cursor_shape, - &snapshot.display_snapshot, - is_newest, - true, - ); - if is_newest { - newest_selection_head = Some(layout.head); + for row in cmp::max(layout.active_rows.start, start_row) + ..=cmp::min(layout.active_rows.end, end_row) + { + let contains_non_empty_selection = active_rows.entry(row).or_insert(!is_empty); + *contains_non_empty_selection |= !is_empty; + } + layouts.push(layout); } - for row in cmp::max(layout.active_rows.start, start_row) - ..=cmp::min(layout.active_rows.end, end_row) - { - let contains_non_empty_selection = active_rows.entry(row).or_insert(!is_empty); - *contains_non_empty_selection |= !is_empty; - } - layouts.push(layout); + selections.push((style.local_player, layouts)); } - selections.push((style.local_player, layouts)); - } - - if let Some(collaboration_hub) = &editor.collaboration_hub { - // When following someone, render the local selections in their color. - if let Some(leader_id) = editor.leader_peer_id { - if let Some(collaborator) = collaboration_hub.collaborators(cx).get(&leader_id) { - if let Some(participant_index) = collaboration_hub - .user_participant_indices(cx) - .get(&collaborator.user_id) - { - if let Some((local_selection_style, _)) = selections.first_mut() { - *local_selection_style = cx - .theme() - .players() - .color_for_participant(participant_index.0); + if let Some(collaboration_hub) = &editor.collaboration_hub { + // When following someone, render the local selections in their color. + if let Some(leader_id) = editor.leader_peer_id { + if let Some(collaborator) = collaboration_hub.collaborators(cx).get(&leader_id) { + if let Some(participant_index) = collaboration_hub + .user_participant_indices(cx) + .get(&collaborator.user_id) + { + if let Some((local_selection_style, _)) = selections.first_mut() { + *local_selection_style = cx + .theme() + .players() + .color_for_participant(participant_index.0); + } } } } - } - let mut remote_selections = HashMap::default(); - for selection in snapshot.remote_selections_in_range( - &(start_anchor..end_anchor), - collaboration_hub.as_ref(), - cx, - ) { - let selection_style = if let Some(participant_index) = selection.participant_index { - cx.theme() - .players() - .color_for_participant(participant_index.0) - } else { - cx.theme().players().absent() - }; - - // Don't re-render the leader's selections, since the local selections - // match theirs. - if Some(selection.peer_id) == editor.leader_peer_id { - continue; - } + let mut remote_selections = HashMap::default(); + for selection in snapshot.remote_selections_in_range( + &(start_anchor..end_anchor), + collaboration_hub.as_ref(), + cx, + ) { + let selection_style = if let Some(participant_index) = selection.participant_index { + cx.theme() + .players() + .color_for_participant(participant_index.0) + } else { + cx.theme().players().absent() + }; - remote_selections - .entry(selection.replica_id) - .or_insert((selection_style, Vec::new())) - .1 - .push(SelectionLayout::new( - selection.selection, - selection.line_mode, - selection.cursor_shape, - &snapshot.display_snapshot, - false, - false, - )); - } + // Don't re-render the leader's selections, since the local selections + // match theirs. + if Some(selection.peer_id) == editor.leader_peer_id { + continue; + } - selections.extend(remote_selections.into_values()); - } + remote_selections + .entry(selection.replica_id) + .or_insert((selection_style, Vec::new())) + .1 + .push(SelectionLayout::new( + selection.selection, + selection.line_mode, + selection.cursor_shape, + &snapshot.display_snapshot, + false, + false, + )); + } - let scrollbar_settings = EditorSettings::get_global(cx).scrollbar; - let show_scrollbars = match scrollbar_settings.show { - ShowScrollbar::Auto => { - // Git - (is_singleton && scrollbar_settings.git_diff && snapshot.buffer_snapshot.has_git_diffs()) - || - // Selections - (is_singleton && scrollbar_settings.selections && !highlighted_ranges.is_empty()) - // Scrollmanager - || editor.scroll_manager.scrollbars_visible() + selections.extend(remote_selections.into_values()); } - ShowScrollbar::System => editor.scroll_manager.scrollbars_visible(), - ShowScrollbar::Always => true, - ShowScrollbar::Never => false, - }; - - let head_for_relative = newest_selection_head.unwrap_or_else(|| { - let newest = editor.selections.newest::(cx); - SelectionLayout::new( - newest, - editor.selections.line_mode, - editor.cursor_shape, - &snapshot.display_snapshot, - true, - true, - ) - .head - }); - - let (line_numbers, fold_statuses) = self.shape_line_numbers( - start_row..end_row, - &active_rows, - head_for_relative, - is_singleton, - &snapshot, - cx, - ); - - let display_hunks = self.layout_git_gutters(start_row..end_row, &snapshot); - - 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_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; - } - } + let scrollbar_settings = EditorSettings::get_global(cx).scrollbar; + let show_scrollbars = match scrollbar_settings.show { + ShowScrollbar::Auto => { + // Git + (is_singleton && scrollbar_settings.git_diff && snapshot.buffer_snapshot.has_git_diffs()) + || + // Selections + (is_singleton && scrollbar_settings.selections && !highlighted_ranges.is_empty()) + // Scrollmanager + || editor.scroll_manager.scrollbars_visible() + } + ShowScrollbar::System => editor.scroll_manager.scrollbars_visible(), + ShowScrollbar::Always => true, + ShowScrollbar::Never => false, + }; - let longest_line_width = layout_line(snapshot.longest_row(), &snapshot, &style, cx) - .unwrap() - .width; - let scroll_width = longest_line_width.max(max_visible_line_width) + overscroll.width; + let head_for_relative = newest_selection_head.unwrap_or_else(|| { + let newest = editor.selections.newest::(cx); + SelectionLayout::new( + newest, + editor.selections.line_mode, + editor.cursor_shape, + &snapshot.display_snapshot, + true, + true, + ) + .head + }); - let (scroll_width, blocks) = cx.with_element_id(Some("editor_blocks"), |cx| { - self.layout_blocks( + let (line_numbers, fold_statuses) = self.shape_line_numbers( start_row..end_row, + &active_rows, + head_for_relative, + is_singleton, &snapshot, - bounds.size.width, - scroll_width, - gutter_padding, - gutter_width, - em_width, - gutter_width + gutter_margin, - line_height, - &style, - &line_layouts, - editor, cx, - ) - }); + ); - let scroll_max = point( - f32::from((scroll_width - text_size.width) / em_width).max(0.0), - max_row as f32, - ); + let display_hunks = self.layout_git_gutters(start_row..end_row, &snapshot); - let clamped = editor.scroll_manager.clamp_scroll_left(scroll_max.x); + let scrollbar_row_range = scroll_position.y..(scroll_position.y + height_in_lines); - let autoscrolled = if autoscroll_horizontally { - editor.autoscroll_horizontally( - start_row, - text_size.width, - scroll_width, - em_width, - &line_layouts, - cx, - ) - } else { - false - }; + let mut max_visible_line_width = Pixels::ZERO; + 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; + } + } - if clamped || autoscrolled { - snapshot = editor.snapshot(cx); - } + let longest_line_width = layout_line(snapshot.longest_row(), &snapshot, &style, cx) + .unwrap() + .width; + let scroll_width = longest_line_width.max(max_visible_line_width) + overscroll.width; - let mut context_menu = None; - let mut code_actions_indicator = None; - if let Some(newest_selection_head) = newest_selection_head { - if (start_row..end_row).contains(&newest_selection_head.row()) { - if editor.context_menu_visible() { - context_menu = - editor.render_context_menu(newest_selection_head, &self.style, cx); - } + let (scroll_width, blocks) = cx.with_element_id(Some("editor_blocks"), |cx| { + self.layout_blocks( + start_row..end_row, + &snapshot, + bounds.size.width, + scroll_width, + gutter_padding, + gutter_width, + em_width, + gutter_width + gutter_margin, + line_height, + &style, + &line_layouts, + editor, + cx, + ) + }); - let active = matches!( - editor.context_menu.read().as_ref(), - Some(crate::ContextMenu::CodeActions(_)) - ); + let scroll_max = point( + f32::from((scroll_width - text_size.width) / em_width).max(0.0), + max_row as f32, + ); - code_actions_indicator = editor - .render_code_actions_indicator(&style, active, cx) - .map(|element| CodeActionsIndicator { - row: newest_selection_head.row(), - button: element, - }); + let clamped = editor.scroll_manager.clamp_scroll_left(scroll_max.x); + + let autoscrolled = if autoscroll_horizontally { + editor.autoscroll_horizontally( + start_row, + text_size.width, + scroll_width, + em_width, + &line_layouts, + cx, + ) + } else { + false + }; + + if clamped || autoscrolled { + snapshot = editor.snapshot(cx); } - } - let visible_rows = start_row..start_row + line_layouts.len() as u32; - // todo!("hover") - // let mut hover = editor.hover_state.render( - // &snapshot, - // &style, - // visible_rows, - // editor.workspace.as_ref().map(|(w, _)| w.clone()), - // cx, - // ); - // let mode = editor.mode; - - let mut fold_indicators = cx.with_element_id(Some("gutter_fold_indicators"), |cx| { - editor.render_fold_indicators( - fold_statuses, - &style, - editor.gutter_hovered, - line_height, - gutter_margin, - cx, - ) - }); + let mut context_menu = None; + let mut code_actions_indicator = None; + if let Some(newest_selection_head) = newest_selection_head { + if (start_row..end_row).contains(&newest_selection_head.row()) { + if editor.context_menu_visible() { + context_menu = + editor.render_context_menu(newest_selection_head, &self.style, cx); + } - // todo!("context_menu") - // if let Some((_, context_menu)) = context_menu.as_mut() { - // context_menu.layout( - // SizeConstraint { - // min: gpui::Point::::zero(), - // max: point( - // cx.window_size().x * 0.7, - // (12. * line_height).min((size.y - line_height) / 2.), - // ), - // }, - // editor, - // cx, - // ); - // } + let active = matches!( + editor.context_menu.read().as_ref(), + Some(crate::ContextMenu::CodeActions(_)) + ); - // todo!("hover popovers") - // if let Some((_, hover_popovers)) = hover.as_mut() { - // for hover_popover in hover_popovers.iter_mut() { - // hover_popover.layout( - // SizeConstraint { - // min: gpui::Point::::zero(), - // max: point( - // (120. * em_width) // Default size - // .min(size.x / 2.) // Shrink to half of the editor width - // .max(MIN_POPOVER_CHARACTER_WIDTH * em_width), // Apply minimum width of 20 characters - // (16. * line_height) // Default size - // .min(size.y / 2.) // Shrink to half of the editor height - // .max(MIN_POPOVER_LINE_HEIGHT * line_height), // Apply minimum height of 4 lines - // ), - // }, - // editor, - // cx, - // ); - // } - // } + code_actions_indicator = editor + .render_code_actions_indicator(&style, active, cx) + .map(|element| CodeActionsIndicator { + row: newest_selection_head.row(), + button: element, + }); + } + } - let invisible_symbol_font_size = font_size / 2.; - let tab_invisible = cx - .text_system() - .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, - }], - ) - .unwrap(); - let space_invisible = cx - .text_system() - .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, - }], - ) - .unwrap(); + let visible_rows = start_row..start_row + line_layouts.len() as u32; + // todo!("hover") + // let mut hover = editor.hover_state.render( + // &snapshot, + // &style, + // visible_rows, + // editor.workspace.as_ref().map(|(w, _)| w.clone()), + // cx, + // ); + // let mode = editor.mode; + + let mut fold_indicators = cx.with_element_id(Some("gutter_fold_indicators"), |cx| { + editor.render_fold_indicators( + fold_statuses, + &style, + editor.gutter_hovered, + line_height, + gutter_margin, + cx, + ) + }); - LayoutState { - mode: editor_mode, - position_map: Arc::new(PositionMap { - size: bounds.size, - scroll_position: point( - scroll_position.x * em_width, - scroll_position.y * line_height, - ), - scroll_max, - line_layouts, - line_height, - em_width, - em_advance, - snapshot, - }), - visible_anchor_range: start_anchor..end_anchor, - visible_display_row_range: start_row..end_row, - wrap_guides, - gutter_size, - gutter_padding, - text_size, - scrollbar_row_range, - show_scrollbars, - is_singleton, - max_row, - gutter_margin, - active_rows, - highlighted_rows, - highlighted_ranges, - line_numbers, - display_hunks, - blocks, - selections, - context_menu, - code_actions_indicator, - fold_indicators, - tab_invisible, - space_invisible, - // hover_popovers: hover, - } + // todo!("hover popovers") + // if let Some((_, hover_popovers)) = hover.as_mut() { + // for hover_popover in hover_popovers.iter_mut() { + // hover_popover.layout( + // SizeConstraint { + // min: gpui::Point::::zero(), + // max: point( + // (120. * em_width) // Default size + // .min(size.x / 2.) // Shrink to half of the editor width + // .max(MIN_POPOVER_CHARACTER_WIDTH * em_width), // Apply minimum width of 20 characters + // (16. * line_height) // Default size + // .min(size.y / 2.) // Shrink to half of the editor height + // .max(MIN_POPOVER_LINE_HEIGHT * line_height), // Apply minimum height of 4 lines + // ), + // }, + // editor, + // cx, + // ); + // } + // } + + let invisible_symbol_font_size = font_size / 2.; + let tab_invisible = cx + .text_system() + .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, + }], + ) + .unwrap(); + let space_invisible = cx + .text_system() + .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, + }], + ) + .unwrap(); + + LayoutState { + mode: editor_mode, + position_map: Arc::new(PositionMap { + size: bounds.size, + scroll_position: point( + scroll_position.x * em_width, + scroll_position.y * line_height, + ), + scroll_max, + line_layouts, + line_height, + em_width, + em_advance, + snapshot, + }), + visible_anchor_range: start_anchor..end_anchor, + visible_display_row_range: start_row..end_row, + wrap_guides, + gutter_size, + gutter_padding, + text_size, + scrollbar_row_range, + show_scrollbars, + is_singleton, + max_row, + gutter_margin, + active_rows, + highlighted_rows, + highlighted_ranges, + line_numbers, + display_hunks, + blocks, + selections, + context_menu, + code_actions_indicator, + fold_indicators, + tab_invisible, + space_invisible, + // hover_popovers: hover, + } + }) } #[allow(clippy::too_many_arguments)] fn layout_blocks( - &mut self, + &self, rows: Range, snapshot: &EditorSnapshot, editor_width: Pixels, @@ -2030,12 +2191,10 @@ impl EditorElement { let jump_position = language::ToPoint::to_point(&jump_anchor, buffer); IconButton::new(block_id, ui::Icon::ArrowUpRight) - .on_click(move |editor: &mut Editor, cx| { + .on_click(cx.listener_for(&self.editor, move |editor, e, cx| { editor.jump(jump_path.clone(), jump_position, jump_anchor, cx); - }) - .tooltip(move |_, cx| { - Tooltip::for_action("Jump to Buffer", &OpenExcerpts, cx) - }) + })) + .tooltip(|cx| Tooltip::for_action("Jump to Buffer", &OpenExcerpts, cx)) }); let element = if *starts_new_buffer { @@ -2074,7 +2233,7 @@ impl EditorElement { } }; - let size = element.measure(available_space, editor, cx); + let size = element.measure(available_space, cx); (element, size) }; @@ -2133,47 +2292,61 @@ impl EditorElement { gutter_bounds: Bounds, text_bounds: Bounds, layout: &LayoutState, - cx: &mut ViewContext, + cx: &mut WindowContext, ) { let content_origin = text_bounds.origin + point(layout.gutter_margin, Pixels::ZERO); cx.on_mouse_event({ let position_map = layout.position_map.clone(); - move |editor, event: &ScrollWheelEvent, phase, cx| { + let editor = self.editor.clone(); + + move |event: &ScrollWheelEvent, phase, cx| { if phase != DispatchPhase::Bubble { return; } - if Self::scroll(editor, event, &position_map, bounds, cx) { + let should_cancel = editor.update(cx, |editor, cx| { + Self::scroll(editor, event, &position_map, bounds, cx) + }); + if should_cancel { cx.stop_propagation(); } } }); + cx.on_mouse_event({ let position_map = layout.position_map.clone(); - move |editor, event: &MouseDownEvent, phase, cx| { + let editor = self.editor.clone(); + + move |event: &MouseDownEvent, phase, cx| { if phase != DispatchPhase::Bubble { return; } - if Self::mouse_down(editor, event, &position_map, text_bounds, gutter_bounds, cx) { + let should_cancel = editor.update(cx, |editor, cx| { + Self::mouse_down(editor, event, &position_map, text_bounds, gutter_bounds, cx) + }); + + if should_cancel { cx.stop_propagation() } } }); + cx.on_mouse_event({ let position_map = layout.position_map.clone(); - move |editor, event: &MouseUpEvent, phase, cx| { - if phase != DispatchPhase::Bubble { - return; - } + let editor = self.editor.clone(); + move |event: &MouseUpEvent, phase, cx| { + let should_cancel = editor.update(cx, |editor, cx| { + Self::mouse_up(editor, event, &position_map, text_bounds, cx) + }); - if Self::mouse_up(editor, event, &position_map, text_bounds, cx) { + if should_cancel { cx.stop_propagation() } } }); - // todo!() + //todo!() // on_down(MouseButton::Right, { // let position_map = layout.position_map.clone(); // move |event, editor, cx| { @@ -2190,12 +2363,17 @@ impl EditorElement { // }); cx.on_mouse_event({ let position_map = layout.position_map.clone(); - move |editor, event: &MouseMoveEvent, phase, cx| { + let editor = self.editor.clone(); + move |event: &MouseMoveEvent, phase, cx| { if phase != DispatchPhase::Bubble { return; } - if Self::mouse_moved(editor, event, &position_map, text_bounds, gutter_bounds, cx) { + let stop_propogating = editor.update(cx, |editor, cx| { + Self::mouse_moved(editor, event, &position_map, text_bounds, gutter_bounds, cx) + }); + + if stop_propogating { cx.stop_propagation() } } @@ -2324,7 +2502,7 @@ impl LineWithInvisibles { content_origin: gpui::Point, whitespace_setting: ShowWhitespaceSetting, selection_ranges: &[Range], - cx: &mut ViewContext, + cx: &mut WindowContext, ) { let line_height = layout.position_map.line_height; let line_y = line_height * row as f32 - layout.position_map.scroll_position.y; @@ -2356,7 +2534,7 @@ impl LineWithInvisibles { row: u32, line_height: Pixels, whitespace_setting: ShowWhitespaceSetting, - cx: &mut ViewContext, + cx: &mut WindowContext, ) { let allowed_invisibles_regions = match whitespace_setting { ShowWhitespaceSetting::None => return, @@ -2399,37 +2577,42 @@ enum Invisible { Whitespace { line_offset: usize }, } -impl Element for EditorElement { +impl Element for EditorElement { type State = (); fn layout( &mut self, - editor: &mut Editor, element_state: Option, - cx: &mut gpui::ViewContext, + cx: &mut gpui::WindowContext, ) -> (gpui::LayoutId, Self::State) { - editor.style = Some(self.style.clone()); // Long-term, we'd like to eliminate this. - - let rem_size = cx.rem_size(); - let mut style = Style::default(); - style.size.width = relative(1.).into(); - style.size.height = match editor.mode { - EditorMode::SingleLine => self.style.text.line_height_in_pixels(cx.rem_size()).into(), - EditorMode::AutoHeight { .. } => todo!(), - EditorMode::Full => relative(1.).into(), - }; - let layout_id = cx.request_layout(&style, None); - (layout_id, ()) + self.editor.update(cx, |editor, cx| { + editor.style = Some(self.style.clone()); // Long-term, we'd like to eliminate this. + + let rem_size = cx.rem_size(); + let mut style = Style::default(); + style.size.width = relative(1.).into(); + style.size.height = match editor.mode { + EditorMode::SingleLine => { + self.style.text.line_height_in_pixels(cx.rem_size()).into() + } + EditorMode::AutoHeight { .. } => todo!(), + EditorMode::Full => relative(1.).into(), + }; + let layout_id = cx.request_layout(&style, None); + + (layout_id, ()) + }) } fn paint( mut self, bounds: Bounds, - editor: &mut Editor, element_state: &mut Self::State, - cx: &mut gpui::ViewContext, + cx: &mut gpui::WindowContext, ) { - let mut layout = self.compute_layout(editor, cx, bounds); + let editor = self.editor.clone(); + + let mut layout = self.compute_layout(bounds, cx); let gutter_bounds = Bounds { origin: bounds.origin, size: layout.gutter_size, @@ -2439,45 +2622,42 @@ impl Element for EditorElement { size: layout.text_size, }; - let dispatch_context = editor.dispatch_context(cx); - cx.with_key_dispatch( - dispatch_context, - Some(editor.focus_handle.clone()), - |_, cx| { - register_actions(cx); - - // We call with_z_index to establish a new stacking context. - cx.with_z_index(0, |cx| { - cx.with_content_mask(Some(ContentMask { bounds }), |cx| { - // Paint mouse listeners first, so any elements we paint on top of the editor - // take precedence. - self.paint_mouse_listeners(bounds, gutter_bounds, text_bounds, &layout, cx); - let input_handler = ElementInputHandler::new(bounds, cx); - cx.handle_input(&editor.focus_handle, input_handler); - - self.paint_background(gutter_bounds, text_bounds, &layout, cx); - if layout.gutter_size.width > Pixels::ZERO { - self.paint_gutter(gutter_bounds, &mut layout, editor, cx); - } - self.paint_text(text_bounds, &mut layout, editor, cx); + let focus_handle = editor.focus_handle(cx); + let dispatch_context = self.editor.read(cx).dispatch_context(cx); + cx.with_key_dispatch(dispatch_context, Some(focus_handle.clone()), |_, cx| { + self.register_actions(cx); + + // We call with_z_index to establish a new stacking context. + cx.with_z_index(0, |cx| { + cx.with_content_mask(Some(ContentMask { bounds }), |cx| { + // Paint mouse listeners first, so any elements we paint on top of the editor + // take precedence. + self.paint_mouse_listeners(bounds, gutter_bounds, text_bounds, &layout, cx); + let input_handler = ElementInputHandler::new(bounds, self.editor.clone(), cx); + cx.handle_input(&focus_handle, input_handler); + + self.paint_background(gutter_bounds, text_bounds, &layout, cx); + if layout.gutter_size.width > Pixels::ZERO { + self.paint_gutter(gutter_bounds, &mut layout, cx); + } + self.paint_text(text_bounds, &mut layout, cx); - if !layout.blocks.is_empty() { - cx.with_element_id(Some("editor_blocks"), |cx| { - self.paint_blocks(bounds, &mut layout, editor, cx); - }) - } - }); + if !layout.blocks.is_empty() { + cx.with_element_id(Some("editor_blocks"), |cx| { + self.paint_blocks(bounds, &mut layout, cx); + }) + } }); - }, - ) + }); + }) } } -impl RenderOnce for EditorElement { +impl RenderOnce for EditorElement { type Element = Self; fn element_id(&self) -> Option { - Some(self.editor_id.into()) + self.editor.element_id() } fn render_once(self) -> Self::Element { @@ -3106,17 +3286,17 @@ pub struct LayoutState { show_scrollbars: bool, is_singleton: bool, max_row: u32, - context_menu: Option<(DisplayPoint, AnyElement)>, + context_menu: Option<(DisplayPoint, AnyElement)>, code_actions_indicator: Option, - // hover_popovers: Option<(DisplayPoint, Vec>)>, - fold_indicators: Vec>>, + // hover_popovers: Option<(DisplayPoint, Vec)>, + fold_indicators: Vec>, tab_invisible: ShapedLine, space_invisible: ShapedLine, } struct CodeActionsIndicator { row: u32, - button: IconButton, + button: IconButton, } struct PositionMap { @@ -3201,7 +3381,7 @@ impl PositionMap { struct BlockLayout { row: u32, - element: AnyElement, + element: AnyElement, available_space: Size, style: BlockStyle, } @@ -3906,187 +4086,18 @@ fn scale_horizontal_mouse_autoscroll_delta(delta: Pixels) -> f32 { // } // } -fn register_actions(cx: &mut ViewContext) { - register_action(cx, Editor::move_left); - register_action(cx, Editor::move_right); - register_action(cx, Editor::move_down); - register_action(cx, Editor::move_up); - // on_action(cx, Editor::new_file); todo!() - // on_action(cx, Editor::new_file_in_direction); todo!() - register_action(cx, Editor::cancel); - register_action(cx, Editor::newline); - register_action(cx, Editor::newline_above); - register_action(cx, Editor::newline_below); - register_action(cx, Editor::backspace); - register_action(cx, Editor::delete); - register_action(cx, Editor::tab); - register_action(cx, Editor::tab_prev); - register_action(cx, Editor::indent); - register_action(cx, Editor::outdent); - register_action(cx, Editor::delete_line); - register_action(cx, Editor::join_lines); - register_action(cx, Editor::sort_lines_case_sensitive); - register_action(cx, Editor::sort_lines_case_insensitive); - register_action(cx, Editor::reverse_lines); - register_action(cx, Editor::shuffle_lines); - register_action(cx, Editor::convert_to_upper_case); - register_action(cx, Editor::convert_to_lower_case); - register_action(cx, Editor::convert_to_title_case); - register_action(cx, Editor::convert_to_snake_case); - register_action(cx, Editor::convert_to_kebab_case); - register_action(cx, Editor::convert_to_upper_camel_case); - register_action(cx, Editor::convert_to_lower_camel_case); - register_action(cx, Editor::delete_to_previous_word_start); - register_action(cx, Editor::delete_to_previous_subword_start); - register_action(cx, Editor::delete_to_next_word_end); - register_action(cx, Editor::delete_to_next_subword_end); - register_action(cx, Editor::delete_to_beginning_of_line); - register_action(cx, Editor::delete_to_end_of_line); - register_action(cx, Editor::cut_to_end_of_line); - register_action(cx, Editor::duplicate_line); - register_action(cx, Editor::move_line_up); - register_action(cx, Editor::move_line_down); - register_action(cx, Editor::transpose); - register_action(cx, Editor::cut); - register_action(cx, Editor::copy); - register_action(cx, Editor::paste); - register_action(cx, Editor::undo); - register_action(cx, Editor::redo); - register_action(cx, Editor::move_page_up); - register_action(cx, Editor::move_page_down); - register_action(cx, Editor::next_screen); - register_action(cx, Editor::scroll_cursor_top); - register_action(cx, Editor::scroll_cursor_center); - register_action(cx, Editor::scroll_cursor_bottom); - register_action(cx, |editor, _: &LineDown, cx| { - editor.scroll_screen(&ScrollAmount::Line(1.), cx) - }); - register_action(cx, |editor, _: &LineUp, cx| { - editor.scroll_screen(&ScrollAmount::Line(-1.), cx) - }); - register_action(cx, |editor, _: &HalfPageDown, cx| { - editor.scroll_screen(&ScrollAmount::Page(0.5), cx) - }); - register_action(cx, |editor, _: &HalfPageUp, cx| { - editor.scroll_screen(&ScrollAmount::Page(-0.5), cx) - }); - register_action(cx, |editor, _: &PageDown, cx| { - editor.scroll_screen(&ScrollAmount::Page(1.), cx) - }); - register_action(cx, |editor, _: &PageUp, cx| { - editor.scroll_screen(&ScrollAmount::Page(-1.), cx) - }); - register_action(cx, Editor::move_to_previous_word_start); - register_action(cx, Editor::move_to_previous_subword_start); - register_action(cx, Editor::move_to_next_word_end); - register_action(cx, Editor::move_to_next_subword_end); - register_action(cx, Editor::move_to_beginning_of_line); - register_action(cx, Editor::move_to_end_of_line); - register_action(cx, Editor::move_to_start_of_paragraph); - register_action(cx, Editor::move_to_end_of_paragraph); - register_action(cx, Editor::move_to_beginning); - register_action(cx, Editor::move_to_end); - register_action(cx, Editor::select_up); - register_action(cx, Editor::select_down); - register_action(cx, Editor::select_left); - register_action(cx, Editor::select_right); - register_action(cx, Editor::select_to_previous_word_start); - register_action(cx, Editor::select_to_previous_subword_start); - register_action(cx, Editor::select_to_next_word_end); - register_action(cx, Editor::select_to_next_subword_end); - register_action(cx, Editor::select_to_beginning_of_line); - register_action(cx, Editor::select_to_end_of_line); - register_action(cx, Editor::select_to_start_of_paragraph); - register_action(cx, Editor::select_to_end_of_paragraph); - register_action(cx, Editor::select_to_beginning); - register_action(cx, Editor::select_to_end); - register_action(cx, Editor::select_all); - register_action(cx, |editor, action, cx| { - editor.select_all_matches(action, cx).log_err(); - }); - register_action(cx, Editor::select_line); - register_action(cx, Editor::split_selection_into_lines); - register_action(cx, Editor::add_selection_above); - register_action(cx, Editor::add_selection_below); - register_action(cx, |editor, action, cx| { - editor.select_next(action, cx).log_err(); - }); - register_action(cx, |editor, action, cx| { - editor.select_previous(action, cx).log_err(); - }); - register_action(cx, Editor::toggle_comments); - register_action(cx, Editor::select_larger_syntax_node); - register_action(cx, Editor::select_smaller_syntax_node); - register_action(cx, Editor::move_to_enclosing_bracket); - register_action(cx, Editor::undo_selection); - register_action(cx, Editor::redo_selection); - register_action(cx, Editor::go_to_diagnostic); - register_action(cx, Editor::go_to_prev_diagnostic); - register_action(cx, Editor::go_to_hunk); - register_action(cx, Editor::go_to_prev_hunk); - register_action(cx, Editor::go_to_definition); - register_action(cx, Editor::go_to_definition_split); - register_action(cx, Editor::go_to_type_definition); - register_action(cx, Editor::go_to_type_definition_split); - register_action(cx, Editor::fold); - register_action(cx, Editor::fold_at); - register_action(cx, Editor::unfold_lines); - register_action(cx, Editor::unfold_at); - register_action(cx, Editor::fold_selected_ranges); - register_action(cx, Editor::show_completions); - register_action(cx, Editor::toggle_code_actions); - // on_action(cx, Editor::open_excerpts); todo!() - register_action(cx, Editor::toggle_soft_wrap); - register_action(cx, Editor::toggle_inlay_hints); - register_action(cx, Editor::reveal_in_finder); - register_action(cx, Editor::copy_path); - register_action(cx, Editor::copy_relative_path); - register_action(cx, Editor::copy_highlight_json); - register_action(cx, |editor, action, cx| { - editor - .format(action, cx) - .map(|task| task.detach_and_log_err(cx)); - }); - register_action(cx, Editor::restart_language_server); - register_action(cx, Editor::show_character_palette); - // on_action(cx, Editor::confirm_completion); todo!() - register_action(cx, |editor, action, cx| { - editor - .confirm_code_action(action, cx) - .map(|task| task.detach_and_log_err(cx)); - }); - register_action(cx, |editor, action, cx| { - editor - .rename(action, cx) - .map(|task| task.detach_and_log_err(cx)); - }); - register_action(cx, |editor, action, cx| { - editor - .confirm_rename(action, cx) - .map(|task| task.detach_and_log_err(cx)); - }); - register_action(cx, |editor, action, cx| { - editor - .find_all_references(action, cx) - .map(|task| task.detach_and_log_err(cx)); - }); - register_action(cx, Editor::next_copilot_suggestion); - register_action(cx, Editor::previous_copilot_suggestion); - register_action(cx, Editor::copilot_suggest); - register_action(cx, Editor::context_menu_first); - register_action(cx, Editor::context_menu_prev); - register_action(cx, Editor::context_menu_next); - register_action(cx, Editor::context_menu_last); -} - fn register_action( - cx: &mut ViewContext, + view: &View, + cx: &mut WindowContext, listener: impl Fn(&mut Editor, &T, &mut ViewContext) + 'static, ) { - cx.on_action(TypeId::of::(), move |editor, action, phase, cx| { + let view = view.clone(); + cx.on_action(TypeId::of::(), move |action, phase, cx| { let action = action.downcast_ref().unwrap(); if phase == DispatchPhase::Bubble { - listener(editor, action, cx); + view.update(cx, |editor, cx| { + listener(editor, action, cx); + }) } }) } diff --git a/crates/editor2/src/hover_popover.rs b/crates/editor2/src/hover_popover.rs index 5c8f403d4f7d768d764f5665f45e31863b00e733..07d108cd6525babd12bf55404ba1b561cf2d67f4 100644 --- a/crates/editor2/src/hover_popover.rs +++ b/crates/editor2/src/hover_popover.rs @@ -422,7 +422,7 @@ impl HoverState { visible_rows: Range, workspace: Option>, cx: &mut ViewContext, - ) -> Option<(DisplayPoint, Vec>)> { + ) -> Option<(DisplayPoint, Vec)> { todo!("old version below") } // // If there is a diagnostic, position the popovers based on that. @@ -504,7 +504,7 @@ pub struct DiagnosticPopover { } impl DiagnosticPopover { - pub fn render(&self, style: &EditorStyle, cx: &mut ViewContext) -> AnyElement { + pub fn render(&self, style: &EditorStyle, cx: &mut ViewContext) -> AnyElement { todo!() // enum PrimaryDiagnostic {} diff --git a/crates/editor2/src/items.rs b/crates/editor2/src/items.rs index d503ac9f5c50053ec92e31dcda8efc37b5e38e79..eca3b99d7807b455ba48ed99ef0287a9ee084abf 100644 --- a/crates/editor2/src/items.rs +++ b/crates/editor2/src/items.rs @@ -10,7 +10,7 @@ use futures::future::try_join_all; use gpui::{ div, point, AnyElement, AppContext, AsyncAppContext, Entity, EntityId, EventEmitter, FocusHandle, Model, ParentElement, Pixels, SharedString, Styled, Subscription, Task, View, - ViewContext, VisualContext, WeakView, + ViewContext, VisualContext, WeakView, WindowContext, }; use language::{ proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, CharKind, OffsetRangeExt, @@ -30,7 +30,7 @@ use std::{ }; use text::Selection; use theme::{ActiveTheme, Theme}; -use ui::{Label, TextColor}; +use ui::{Color, Label}; use util::{paths::PathExt, ResultExt, TryFutureExt}; use workspace::item::{BreadcrumbText, FollowEvent, FollowableEvents, FollowableItemHandle}; use workspace::{ @@ -584,7 +584,7 @@ impl Item for Editor { Some(path.to_string_lossy().to_string().into()) } - fn tab_content(&self, detail: Option, cx: &AppContext) -> AnyElement { + fn tab_content(&self, detail: Option, cx: &WindowContext) -> AnyElement { let theme = cx.theme(); AnyElement::new( @@ -604,7 +604,7 @@ impl Item for Editor { &description, MAX_TAB_TITLE_LEN, )) - .color(TextColor::Muted), + .color(Color::Muted), ), ) })), @@ -761,7 +761,7 @@ impl Item for Editor { } fn breadcrumb_location(&self) -> ToolbarItemLocation { - ToolbarItemLocation::PrimaryLeft { flex: None } + ToolbarItemLocation::PrimaryLeft } fn breadcrumbs(&self, variant: &Theme, cx: &AppContext) -> Option> { @@ -907,17 +907,15 @@ impl SearchableItem for Editor { type Match = Range; fn clear_matches(&mut self, cx: &mut ViewContext) { - todo!() - // self.clear_background_highlights::(cx); + self.clear_background_highlights::(cx); } fn update_matches(&mut self, matches: Vec>, cx: &mut ViewContext) { - todo!() - // self.highlight_background::( - // matches, - // |theme| theme.search.match_background, - // cx, - // ); + self.highlight_background::( + matches, + |theme| theme.title_bar_background, // todo: update theme + cx, + ); } fn query_suggestion(&mut self, cx: &mut ViewContext) -> String { @@ -952,22 +950,20 @@ impl SearchableItem for Editor { matches: Vec>, cx: &mut ViewContext, ) { - todo!() - // self.unfold_ranges([matches[index].clone()], false, true, cx); - // let range = self.range_for_match(&matches[index]); - // self.change_selections(Some(Autoscroll::fit()), cx, |s| { - // s.select_ranges([range]); - // }) + self.unfold_ranges([matches[index].clone()], false, true, cx); + let range = self.range_for_match(&matches[index]); + self.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.select_ranges([range]); + }) } fn select_matches(&mut self, matches: Vec, cx: &mut ViewContext) { - todo!() - // self.unfold_ranges(matches.clone(), false, false, cx); - // let mut ranges = Vec::new(); - // for m in &matches { - // ranges.push(self.range_for_match(&m)) - // } - // self.change_selections(None, cx, |s| s.select_ranges(ranges)); + self.unfold_ranges(matches.clone(), false, false, cx); + let mut ranges = Vec::new(); + for m in &matches { + ranges.push(self.range_for_match(&m)) + } + self.change_selections(None, cx, |s| s.select_ranges(ranges)); } fn replace( &mut self, diff --git a/crates/file_finder2/src/file_finder.rs b/crates/file_finder2/src/file_finder.rs index 2e7655298a4ddd151bc89222f4e34cb1722c6c1d..0b9157bb4ffbcbca8a8d84ee57594ed553dbb342 100644 --- a/crates/file_finder2/src/file_finder.rs +++ b/crates/file_finder2/src/file_finder.rs @@ -117,8 +117,8 @@ impl FocusableView for FileFinder { self.picker.focus_handle(cx) } } -impl Render for FileFinder { - type Element = Div; +impl Render for FileFinder { + type Element = Div; fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { v_stack().w_96().child(self.picker.clone()) @@ -530,7 +530,7 @@ impl FileFinderDelegate { } impl PickerDelegate for FileFinderDelegate { - type ListItem = Div>; + type ListItem = Div; fn placeholder_text(&self) -> Arc { "Search project files...".into() diff --git a/crates/go_to_line2/src/go_to_line.rs b/crates/go_to_line2/src/go_to_line.rs index 9b3666ea5c02df827b0d575a8c0c2dee9395e9f6..61f5742750a08ea35ffec2592c526e47f8b0a377 100644 --- a/crates/go_to_line2/src/go_to_line.rs +++ b/crates/go_to_line2/src/go_to_line.rs @@ -5,7 +5,7 @@ use gpui::{ }; use text::{Bias, Point}; use theme::ActiveTheme; -use ui::{h_stack, v_stack, Label, StyledExt, TextColor}; +use ui::{h_stack, v_stack, Color, Label, StyledExt}; use util::paths::FILE_ROW_COLUMN_DELIMITER; use workspace::Workspace; @@ -144,15 +144,15 @@ impl GoToLine { } } -impl Render for GoToLine { - type Element = Div; +impl Render for GoToLine { + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { div() .elevation_2(cx) .key_context("GoToLine") - .on_action(Self::cancel) - .on_action(Self::confirm) + .on_action(cx.listener(Self::cancel)) + .on_action(cx.listener(Self::confirm)) .w_96() .child( v_stack() @@ -176,7 +176,7 @@ impl Render for GoToLine { .justify_between() .px_2() .py_1() - .child(Label::new(self.current_text.clone()).color(TextColor::Muted)), + .child(Label::new(self.current_text.clone()).color(Color::Muted)), ), ) } diff --git a/crates/gpui2/src/app.rs b/crates/gpui2/src/app.rs index 41b514c137dfb7706ee9399599fcd004fca582f9..1d599eaede0ae96c5dbb5fcc61d8a1a5dbba32d6 100644 --- a/crates/gpui2/src/app.rs +++ b/crates/gpui2/src/app.rs @@ -424,7 +424,7 @@ impl AppContext { /// Opens a new window with the given option and the root view returned by the given function. /// The function is invoked with a `WindowContext`, which can be used to interact with window-specific /// functionality. - pub fn open_window>( + pub fn open_window( &mut self, options: crate::WindowOptions, build_root_view: impl FnOnce(&mut WindowContext) -> View, diff --git a/crates/gpui2/src/app/async_context.rs b/crates/gpui2/src/app/async_context.rs index 3fdff1102adfc5a18dc88d04dbbf93f76ad377f1..cc3b0ace57b37d639656ef06fbc5a2c2344b7877 100644 --- a/crates/gpui2/src/app/async_context.rs +++ b/crates/gpui2/src/app/async_context.rs @@ -115,7 +115,7 @@ impl AsyncAppContext { build_root_view: impl FnOnce(&mut WindowContext) -> View, ) -> Result> where - V: 'static + Render, + V: 'static + Render, { let app = self .app @@ -286,7 +286,7 @@ impl VisualContext for AsyncWindowContext { build_view_state: impl FnOnce(&mut ViewContext<'_, V>) -> V, ) -> Self::Result> where - V: 'static + Render, + V: 'static + Render, { self.window .update(self, |_, cx| cx.build_view(build_view_state)) @@ -306,7 +306,7 @@ impl VisualContext for AsyncWindowContext { build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V, ) -> Self::Result> where - V: 'static + Render, + V: 'static + Render, { self.window .update(self, |_, cx| cx.replace_root_view(build_view)) diff --git a/crates/gpui2/src/app/test_context.rs b/crates/gpui2/src/app/test_context.rs index ee1c4602ee12ecfd20b103059bcfb394c514b7bb..2bd3a069caa28c22deb84c156ee590c14029d67d 100644 --- a/crates/gpui2/src/app/test_context.rs +++ b/crates/gpui2/src/app/test_context.rs @@ -1,8 +1,9 @@ use crate::{ div, Action, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext, - BackgroundExecutor, Context, Div, EventEmitter, ForegroundExecutor, InputEvent, KeyDownEvent, - Keystroke, Model, ModelContext, Render, Result, Task, TestDispatcher, TestPlatform, TestWindow, - View, ViewContext, VisualContext, WindowContext, WindowHandle, WindowOptions, + BackgroundExecutor, Context, Div, Entity, EventEmitter, ForegroundExecutor, InputEvent, + KeyDownEvent, Keystroke, Model, ModelContext, Render, Result, Task, TestDispatcher, + TestPlatform, TestWindow, View, ViewContext, VisualContext, WindowContext, WindowHandle, + WindowOptions, }; use anyhow::{anyhow, bail}; use futures::{Stream, StreamExt}; @@ -126,7 +127,7 @@ impl TestAppContext { pub fn add_window(&mut self, build_window: F) -> WindowHandle where F: FnOnce(&mut ViewContext) -> V, - V: 'static + Render, + V: 'static + Render, { let mut cx = self.app.borrow_mut(); cx.open_window(WindowOptions::default(), |cx| cx.build_view(build_window)) @@ -143,7 +144,7 @@ impl TestAppContext { pub fn add_window_view(&mut self, build_window: F) -> (View, &mut VisualTestContext) where F: FnOnce(&mut ViewContext) -> V, - V: 'static + Render, + V: 'static + Render, { let mut cx = self.app.borrow_mut(); let window = cx.open_window(WindowOptions::default(), |cx| cx.build_view(build_window)); @@ -296,21 +297,19 @@ impl TestAppContext { .unwrap() } - pub fn notifications(&mut self, entity: &Model) -> impl Stream { + pub fn notifications(&mut self, entity: &impl Entity) -> impl Stream { let (tx, rx) = futures::channel::mpsc::unbounded(); - - entity.update(self, move |_, cx: &mut ModelContext| { + self.update(|cx| { cx.observe(entity, { let tx = tx.clone(); - move |_, _, _| { + move |_, _| { let _ = tx.unbounded_send(()); } }) .detach(); - - cx.on_release(move |_, _| tx.close_channel()).detach(); + cx.observe_release(entity, move |_, _| tx.close_channel()) + .detach() }); - rx } @@ -569,7 +568,7 @@ impl<'a> VisualContext for VisualTestContext<'a> { build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V, ) -> Self::Result> where - V: 'static + Render, + V: 'static + Render, { self.window .update(self.cx, |_, cx| cx.build_view(build_view)) @@ -591,7 +590,7 @@ impl<'a> VisualContext for VisualTestContext<'a> { build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V, ) -> Self::Result> where - V: 'static + Render, + V: 'static + Render, { self.window .update(self.cx, |_, cx| cx.replace_root_view(build_view)) @@ -619,7 +618,7 @@ impl<'a> VisualContext for VisualTestContext<'a> { } impl AnyWindowHandle { - pub fn build_view + 'static>( + pub fn build_view( &self, cx: &mut TestAppContext, build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V, @@ -630,8 +629,8 @@ impl AnyWindowHandle { pub struct EmptyView {} -impl Render for EmptyView { - type Element = Div; +impl Render for EmptyView { + type Element = Div; fn render(&mut self, _cx: &mut crate::ViewContext) -> Self::Element { div() diff --git a/crates/gpui2/src/element.rs b/crates/gpui2/src/element.rs index 990de68b063ec3c22bc39dabce0ff4c1d64cb968..5cd015503d2ccb2e9792d0e797b19c12b534b9fc 100644 --- a/crates/gpui2/src/element.rs +++ b/crates/gpui2/src/element.rs @@ -1,24 +1,25 @@ use crate::{ AvailableSpace, BorrowWindow, Bounds, ElementId, LayoutId, Pixels, Point, Size, ViewContext, + WindowContext, }; use derive_more::{Deref, DerefMut}; pub(crate) use smallvec::SmallVec; -use std::{any::Any, fmt::Debug, marker::PhantomData}; +use std::{any::Any, fmt::Debug}; -pub trait Render: 'static + Sized { - type Element: Element + 'static; +pub trait Render: 'static + Sized { + type Element: Element + 'static; - fn render(&mut self, cx: &mut ViewContext) -> Self::Element; + fn render(&mut self, cx: &mut ViewContext) -> Self::Element; } -pub trait RenderOnce: Sized { - type Element: Element + 'static; +pub trait RenderOnce: Sized { + type Element: Element + 'static; fn element_id(&self) -> Option; fn render_once(self) -> Self::Element; - fn render_into_any(self) -> AnyElement { + fn render_into_any(self) -> AnyElement { self.render_once().into_any() } @@ -26,9 +27,8 @@ pub trait RenderOnce: Sized { self, origin: Point, available_space: Size, - view_state: &mut V, - cx: &mut ViewContext, - f: impl FnOnce(&mut >::State, &mut ViewContext) -> R, + cx: &mut WindowContext, + f: impl FnOnce(&mut ::State, &mut WindowContext) -> R, ) -> R where T: Clone + Default + Debug + Into, @@ -39,13 +39,9 @@ pub trait RenderOnce: Sized { element: Some(element), phase: ElementDrawPhase::Start, }; - let frame_state = DrawableElement::draw( - element, - origin, - available_space.map(Into::into), - view_state, - cx, - ); + + let frame_state = + DrawableElement::draw(element, origin, available_space.map(Into::into), cx); if let Some(mut frame_state) = frame_state { f(&mut frame_state, cx) @@ -61,7 +57,7 @@ pub trait RenderOnce: Sized { fn map(self, f: impl FnOnce(Self) -> U) -> U where Self: Sized, - U: RenderOnce, + U: RenderOnce, { f(self) } @@ -87,70 +83,55 @@ pub trait RenderOnce: Sized { } } -pub trait Element: 'static + RenderOnce { +pub trait Element: 'static + RenderOnce { type State: 'static; fn layout( &mut self, - view_state: &mut V, - element_state: Option, - cx: &mut ViewContext, + state: Option, + cx: &mut WindowContext, ) -> (LayoutId, Self::State); - fn paint( - self, - bounds: Bounds, - view_state: &mut V, - element_state: &mut Self::State, - cx: &mut ViewContext, - ); + fn paint(self, bounds: Bounds, state: &mut Self::State, cx: &mut WindowContext); - fn into_any(self) -> AnyElement { + fn into_any(self) -> AnyElement { AnyElement::new(self) } } -pub trait Component: 'static { - type Rendered: RenderOnce; +pub trait Component: 'static { + type Rendered: RenderOnce; - fn render(self, view: &mut V, cx: &mut ViewContext) -> Self::Rendered; + fn render(self, cx: &mut WindowContext) -> Self::Rendered; } -pub struct CompositeElement { +pub struct CompositeElement { component: Option, - view_type: PhantomData, } -pub struct CompositeElementState> { - rendered_element: Option<>::Element>, - rendered_element_state: <>::Element as Element>::State, +pub struct CompositeElementState { + rendered_element: Option<::Element>, + rendered_element_state: <::Element as Element>::State, } -impl CompositeElement { +impl CompositeElement { pub fn new(component: C) -> Self { CompositeElement { component: Some(component), - view_type: PhantomData, } } } -impl> Element for CompositeElement { - type State = CompositeElementState; +impl Element for CompositeElement { + type State = CompositeElementState; fn layout( &mut self, - view: &mut V, state: Option, - cx: &mut ViewContext, + cx: &mut WindowContext, ) -> (LayoutId, Self::State) { - let mut element = self - .component - .take() - .unwrap() - .render(view, cx) - .render_once(); - let (layout_id, state) = element.layout(view, state.map(|s| s.rendered_element_state), cx); + let mut element = self.component.take().unwrap().render(cx).render_once(); + let (layout_id, state) = element.layout(state.map(|s| s.rendered_element_state), cx); let state = CompositeElementState { rendered_element: Some(element), rendered_element_state: state, @@ -158,23 +139,16 @@ impl> Element for CompositeElement { (layout_id, state) } - fn paint( - self, - bounds: Bounds, - view: &mut V, - state: &mut Self::State, - cx: &mut ViewContext, - ) { - state.rendered_element.take().unwrap().paint( - bounds, - view, - &mut state.rendered_element_state, - cx, - ); + fn paint(self, bounds: Bounds, state: &mut Self::State, cx: &mut WindowContext) { + state + .rendered_element + .take() + .unwrap() + .paint(bounds, &mut state.rendered_element_state, cx); } } -impl> RenderOnce for CompositeElement { +impl RenderOnce for CompositeElement { type Element = Self; fn element_id(&self) -> Option { @@ -189,10 +163,10 @@ impl> RenderOnce for CompositeElement { #[derive(Deref, DerefMut, Default, Clone, Debug, Eq, PartialEq, Hash)] pub struct GlobalElementId(SmallVec<[ElementId; 32]>); -pub trait ParentElement { - fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]>; +pub trait ParentElement { + fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]>; - fn child(mut self, child: impl RenderOnce) -> Self + fn child(mut self, child: impl RenderOnce) -> Self where Self: Sized, { @@ -200,7 +174,7 @@ pub trait ParentElement { self } - fn children(mut self, children: impl IntoIterator>) -> Self + fn children(mut self, children: impl IntoIterator) -> Self where Self: Sized, { @@ -213,26 +187,28 @@ pub trait ParentElement { } } -trait ElementObject { +trait ElementObject { fn element_id(&self) -> Option; - fn layout(&mut self, view_state: &mut V, cx: &mut ViewContext) -> LayoutId; - fn paint(&mut self, view_state: &mut V, cx: &mut ViewContext); + + fn layout(&mut self, cx: &mut WindowContext) -> LayoutId; + + fn paint(&mut self, cx: &mut WindowContext); + fn measure( &mut self, available_space: Size, - view_state: &mut V, - cx: &mut ViewContext, + cx: &mut WindowContext, ) -> Size; + fn draw( &mut self, origin: Point, available_space: Size, - view_state: &mut V, - cx: &mut ViewContext, + cx: &mut WindowContext, ); } -pub struct DrawableElement> { +pub struct DrawableElement { element: Option, phase: ElementDrawPhase, } @@ -253,7 +229,7 @@ enum ElementDrawPhase { } /// A wrapper around an implementer of [Element] that allows it to be drawn in a window. -impl> DrawableElement { +impl DrawableElement { fn new(element: E) -> Self { DrawableElement { element: Some(element), @@ -265,18 +241,15 @@ impl> DrawableElement { self.element.as_ref()?.element_id() } - fn layout(&mut self, state: &mut V, cx: &mut ViewContext) -> LayoutId { + fn layout(&mut self, cx: &mut WindowContext) -> LayoutId { let (layout_id, frame_state) = if let Some(id) = self.element.as_ref().unwrap().element_id() { let layout_id = cx.with_element_state(id, |element_state, cx| { - self.element - .as_mut() - .unwrap() - .layout(state, element_state, cx) + self.element.as_mut().unwrap().layout(element_state, cx) }); (layout_id, None) } else { - let (layout_id, frame_state) = self.element.as_mut().unwrap().layout(state, None, cx); + let (layout_id, frame_state) = self.element.as_mut().unwrap().layout(None, cx); (layout_id, Some(frame_state)) }; @@ -287,7 +260,7 @@ impl> DrawableElement { layout_id } - fn paint(mut self, view_state: &mut V, cx: &mut ViewContext) -> Option { + fn paint(mut self, cx: &mut WindowContext) -> Option { match self.phase { ElementDrawPhase::LayoutRequested { layout_id, @@ -304,7 +277,7 @@ impl> DrawableElement { self.element .take() .unwrap() - .paint(bounds, view_state, &mut frame_state, cx); + .paint(bounds, &mut frame_state, cx); Some(frame_state) } else { let element_id = self @@ -315,12 +288,10 @@ impl> DrawableElement { .expect("if we don't have frame state, we should have element state"); cx.with_element_state(element_id, |element_state, cx| { let mut element_state = element_state.unwrap(); - self.element.take().unwrap().paint( - bounds, - view_state, - &mut element_state, - cx, - ); + self.element + .take() + .unwrap() + .paint(bounds, &mut element_state, cx); ((), element_state) }); None @@ -334,11 +305,10 @@ impl> DrawableElement { fn measure( &mut self, available_space: Size, - view_state: &mut V, - cx: &mut ViewContext, + cx: &mut WindowContext, ) -> Size { if matches!(&self.phase, ElementDrawPhase::Start) { - self.layout(view_state, cx); + self.layout(cx); } let layout_id = match &mut self.phase { @@ -376,22 +346,20 @@ impl> DrawableElement { mut self, origin: Point, available_space: Size, - view_state: &mut V, - cx: &mut ViewContext, + cx: &mut WindowContext, ) -> Option { - self.measure(available_space, view_state, cx); - cx.with_absolute_element_offset(origin, |cx| self.paint(view_state, cx)) + self.measure(available_space, cx); + cx.with_absolute_element_offset(origin, |cx| self.paint(cx)) } } -// impl> Element for DrawableElement { -// type State = >::State; +// impl Element for DrawableElement { +// type State = ::State; // fn layout( // &mut self, -// view_state: &mut V, // element_state: Option, -// cx: &mut ViewContext, +// cx: &mut WindowContext, // ) -> (LayoutId, Self::State) { // } @@ -399,15 +367,14 @@ impl> DrawableElement { // fn paint( // self, // bounds: Bounds, -// view_state: &mut V, // element_state: &mut Self::State, -// cx: &mut ViewContext, +// cx: &mut WindowContext, // ) { // todo!() // } // } -// impl> RenderOnce for DrawableElement { +// impl RenderOnce for DrawableElement { // type Element = Self; // fn element_id(&self) -> Option { @@ -419,81 +386,71 @@ impl> DrawableElement { // } // } -impl ElementObject for Option> +impl ElementObject for Option> where - E: Element, + E: Element, E::State: 'static, { fn element_id(&self) -> Option { self.as_ref().unwrap().element_id() } - fn layout(&mut self, view_state: &mut V, cx: &mut ViewContext) -> LayoutId { - DrawableElement::layout(self.as_mut().unwrap(), view_state, cx) + fn layout(&mut self, cx: &mut WindowContext) -> LayoutId { + DrawableElement::layout(self.as_mut().unwrap(), cx) } - fn paint(&mut self, view_state: &mut V, cx: &mut ViewContext) { - DrawableElement::paint(self.take().unwrap(), view_state, cx); + fn paint(&mut self, cx: &mut WindowContext) { + DrawableElement::paint(self.take().unwrap(), cx); } fn measure( &mut self, available_space: Size, - view_state: &mut V, - cx: &mut ViewContext, + cx: &mut WindowContext, ) -> Size { - DrawableElement::measure(self.as_mut().unwrap(), available_space, view_state, cx) + DrawableElement::measure(self.as_mut().unwrap(), available_space, cx) } fn draw( &mut self, origin: Point, available_space: Size, - view_state: &mut V, - cx: &mut ViewContext, + cx: &mut WindowContext, ) { - DrawableElement::draw( - self.take().unwrap(), - origin, - available_space, - view_state, - cx, - ); + DrawableElement::draw(self.take().unwrap(), origin, available_space, cx); } } -pub struct AnyElement(Box>); +pub struct AnyElement(Box); -impl AnyElement { +impl AnyElement { pub fn new(element: E) -> Self where - V: 'static, - E: 'static + Element, + E: 'static + Element, E::State: Any, { - AnyElement(Box::new(Some(DrawableElement::new(element))) as Box>) + AnyElement(Box::new(Some(DrawableElement::new(element))) as Box) } pub fn element_id(&self) -> Option { self.0.element_id() } - pub fn layout(&mut self, view_state: &mut V, cx: &mut ViewContext) -> LayoutId { - self.0.layout(view_state, cx) + pub fn layout(&mut self, cx: &mut WindowContext) -> LayoutId { + self.0.layout(cx) } - pub fn paint(mut self, view_state: &mut V, cx: &mut ViewContext) { - self.0.paint(view_state, cx) + pub fn paint(mut self, cx: &mut WindowContext) { + self.0.paint(cx) } /// Initializes this element and performs layout within the given available space to determine its size. pub fn measure( &mut self, available_space: Size, - view_state: &mut V, - cx: &mut ViewContext, + cx: &mut WindowContext, ) -> Size { - self.0.measure(available_space, view_state, cx) + self.0.measure(available_space, cx) } /// Initializes this element and performs layout in the available space, then paints it at the given origin. @@ -501,43 +458,35 @@ impl AnyElement { mut self, origin: Point, available_space: Size, - view_state: &mut V, - cx: &mut ViewContext, + cx: &mut WindowContext, ) { - self.0.draw(origin, available_space, view_state, cx) + self.0.draw(origin, available_space, cx) } /// Converts this `AnyElement` into a trait object that can be stored and manipulated. - pub fn into_any(self) -> AnyElement { + pub fn into_any(self) -> AnyElement { AnyElement::new(self) } } -impl Element for AnyElement { +impl Element for AnyElement { type State = (); fn layout( &mut self, - view_state: &mut V, _: Option, - cx: &mut ViewContext, + cx: &mut WindowContext, ) -> (LayoutId, Self::State) { - let layout_id = self.layout(view_state, cx); + let layout_id = self.layout(cx); (layout_id, ()) } - fn paint( - self, - _bounds: Bounds, - view_state: &mut V, - _: &mut Self::State, - cx: &mut ViewContext, - ) { - self.paint(view_state, cx); + fn paint(self, _: Bounds, _: &mut Self::State, cx: &mut WindowContext) { + self.paint(cx); } } -impl RenderOnce for AnyElement { +impl RenderOnce for AnyElement { type Element = Self; fn element_id(&self) -> Option { @@ -549,13 +498,13 @@ impl RenderOnce for AnyElement { } } -// impl Element for Option +// impl Element for Option // where // V: 'static, -// E: Element, -// F: FnOnce(&mut V, &mut ViewContext<'_, V>) -> E + 'static, +// E: Element, +// F: FnOnce(&mut V, &mut WindowContext<'_, V>) -> E + 'static, // { -// type State = Option>; +// type State = Option; // fn element_id(&self) -> Option { // None @@ -563,9 +512,8 @@ impl RenderOnce for AnyElement { // fn layout( // &mut self, -// view_state: &mut V, // _: Option, -// cx: &mut ViewContext, +// cx: &mut WindowContext, // ) -> (LayoutId, Self::State) { // let render = self.take().unwrap(); // let mut element = (render)(view_state, cx).into_any(); @@ -576,19 +524,18 @@ impl RenderOnce for AnyElement { // fn paint( // self, // _bounds: Bounds, -// view_state: &mut V, // rendered_element: &mut Self::State, -// cx: &mut ViewContext, +// cx: &mut WindowContext, // ) { // rendered_element.take().unwrap().paint(view_state, cx); // } // } -// impl RenderOnce for Option +// impl RenderOnce for Option // where // V: 'static, -// E: Element, -// F: FnOnce(&mut V, &mut ViewContext) -> E + 'static, +// E: Element, +// F: FnOnce(&mut V, &mut WindowContext) -> E + 'static, // { // type Element = Self; diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index 8838e5f6edfa0593c0ba24ac59b979a4765a9f61..630b368b9518aa8c8b46708f4f65e4c45bf01928 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -3,7 +3,7 @@ use crate::{ BorrowWindow, Bounds, ClickEvent, DispatchPhase, Element, ElementId, FocusEvent, FocusHandle, KeyContext, KeyDownEvent, KeyUpEvent, LayoutId, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Point, Render, RenderOnce, ScrollWheelEvent, SharedString, - Size, Style, StyleRefinement, Styled, Task, View, ViewContext, Visibility, + Size, Style, StyleRefinement, Styled, Task, View, Visibility, WindowContext, }; use collections::HashMap; use refineable::Refineable; @@ -12,7 +12,6 @@ use std::{ any::{Any, TypeId}, cell::RefCell, fmt::Debug, - marker::PhantomData, mem, rc::Rc, time::Duration, @@ -28,30 +27,24 @@ pub struct GroupStyle { pub style: StyleRefinement, } -pub trait InteractiveElement: Sized + Element { - fn interactivity(&mut self) -> &mut Interactivity; +pub trait InteractiveElement: Sized + Element { + fn interactivity(&mut self) -> &mut Interactivity; fn group(mut self, group: impl Into) -> Self { self.interactivity().group = Some(group.into()); self } - fn id(mut self, id: impl Into) -> Stateful { + fn id(mut self, id: impl Into) -> Stateful { self.interactivity().element_id = Some(id.into()); - Stateful { - element: self, - view_type: PhantomData, - } + Stateful { element: self } } - fn track_focus(mut self, focus_handle: &FocusHandle) -> Focusable { + fn track_focus(mut self, focus_handle: &FocusHandle) -> Focusable { self.interactivity().focusable = true; self.interactivity().tracked_focus_handle = Some(focus_handle.clone()); - Focusable { - element: self, - view_type: PhantomData, - } + Focusable { element: self } } fn key_context(mut self, key_context: C) -> Self @@ -85,15 +78,15 @@ pub trait InteractiveElement: Sized + Element { fn on_mouse_down( mut self, button: MouseButton, - handler: impl Fn(&mut V, &MouseDownEvent, &mut ViewContext) + 'static, + listener: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static, ) -> Self { self.interactivity().mouse_down_listeners.push(Box::new( - move |view, event, bounds, phase, cx| { + move |event, bounds, phase, cx| { if phase == DispatchPhase::Bubble && event.button == button && bounds.contains_point(&event.position) { - handler(view, event, cx) + (listener)(event, cx) } }, )); @@ -102,12 +95,12 @@ pub trait InteractiveElement: Sized + Element { fn on_any_mouse_down( mut self, - handler: impl Fn(&mut V, &MouseDownEvent, &mut ViewContext) + 'static, + listener: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static, ) -> Self { self.interactivity().mouse_down_listeners.push(Box::new( - move |view, event, bounds, phase, cx| { + move |event, bounds, phase, cx| { if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { - handler(view, event, cx) + (listener)(event, cx) } }, )); @@ -117,43 +110,43 @@ pub trait InteractiveElement: Sized + Element { fn on_mouse_up( mut self, button: MouseButton, - handler: impl Fn(&mut V, &MouseUpEvent, &mut ViewContext) + 'static, + listener: impl Fn(&MouseUpEvent, &mut WindowContext) + 'static, ) -> Self { - self.interactivity().mouse_up_listeners.push(Box::new( - move |view, event, bounds, phase, cx| { + self.interactivity() + .mouse_up_listeners + .push(Box::new(move |event, bounds, phase, cx| { if phase == DispatchPhase::Bubble && event.button == button && bounds.contains_point(&event.position) { - handler(view, event, cx) + (listener)(event, cx) } - }, - )); + })); self } fn on_any_mouse_up( mut self, - handler: impl Fn(&mut V, &MouseUpEvent, &mut ViewContext) + 'static, + listener: impl Fn(&MouseUpEvent, &mut WindowContext) + 'static, ) -> Self { - self.interactivity().mouse_up_listeners.push(Box::new( - move |view, event, bounds, phase, cx| { + self.interactivity() + .mouse_up_listeners + .push(Box::new(move |event, bounds, phase, cx| { if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { - handler(view, event, cx) + (listener)(event, cx) } - }, - )); + })); self } fn on_mouse_down_out( mut self, - handler: impl Fn(&mut V, &MouseDownEvent, &mut ViewContext) + 'static, + listener: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static, ) -> Self { self.interactivity().mouse_down_listeners.push(Box::new( - move |view, event, bounds, phase, cx| { + move |event, bounds, phase, cx| { if phase == DispatchPhase::Capture && !bounds.contains_point(&event.position) { - handler(view, event, cx) + (listener)(event, cx) } }, )); @@ -163,29 +156,29 @@ pub trait InteractiveElement: Sized + Element { fn on_mouse_up_out( mut self, button: MouseButton, - handler: impl Fn(&mut V, &MouseUpEvent, &mut ViewContext) + 'static, + listener: impl Fn(&MouseUpEvent, &mut WindowContext) + 'static, ) -> Self { - self.interactivity().mouse_up_listeners.push(Box::new( - move |view, event, bounds, phase, cx| { + self.interactivity() + .mouse_up_listeners + .push(Box::new(move |event, bounds, phase, cx| { if phase == DispatchPhase::Capture && event.button == button && !bounds.contains_point(&event.position) { - handler(view, event, cx); + (listener)(event, cx); } - }, - )); + })); self } fn on_mouse_move( mut self, - handler: impl Fn(&mut V, &MouseMoveEvent, &mut ViewContext) + 'static, + listener: impl Fn(&MouseMoveEvent, &mut WindowContext) + 'static, ) -> Self { self.interactivity().mouse_move_listeners.push(Box::new( - move |view, event, bounds, phase, cx| { + move |event, bounds, phase, cx| { if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { - handler(view, event, cx); + (listener)(event, cx); } }, )); @@ -194,29 +187,29 @@ pub trait InteractiveElement: Sized + Element { fn on_scroll_wheel( mut self, - handler: impl Fn(&mut V, &ScrollWheelEvent, &mut ViewContext) + 'static, + listener: impl Fn(&ScrollWheelEvent, &mut WindowContext) + 'static, ) -> Self { self.interactivity().scroll_wheel_listeners.push(Box::new( - move |view, event, bounds, phase, cx| { + move |event, bounds, phase, cx| { if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { - handler(view, event, cx); + (listener)(event, cx); } }, )); self } - /// Capture the given action, fires during the capture phase + /// Capture the given action, before normal action dispatch can fire fn capture_action( mut self, - listener: impl Fn(&mut V, &A, &mut ViewContext) + 'static, + listener: impl Fn(&A, &mut WindowContext) + 'static, ) -> Self { self.interactivity().action_listeners.push(( TypeId::of::(), - Box::new(move |view, action, phase, cx| { + Box::new(move |action, phase, cx| { let action = action.downcast_ref().unwrap(); if phase == DispatchPhase::Capture { - listener(view, action, cx) + (listener)(action, cx) } }), )); @@ -224,10 +217,7 @@ pub trait InteractiveElement: Sized + Element { } /// Add a listener for the given action, fires during the bubble event phase - fn on_action( - mut self, - listener: impl Fn(&mut V, &A, &mut ViewContext) + 'static, - ) -> Self { + fn on_action(mut self, listener: impl Fn(&A, &mut WindowContext) + 'static) -> Self { // NOTE: this debug assert has the side-effect of working around // a bug where a crate consisting only of action definitions does // not register the actions in debug builds: @@ -244,10 +234,10 @@ pub trait InteractiveElement: Sized + Element { // ); self.interactivity().action_listeners.push(( TypeId::of::(), - Box::new(move |view, action, phase, cx| { + Box::new(move |action, phase, cx| { let action = action.downcast_ref().unwrap(); if phase == DispatchPhase::Bubble { - listener(view, action, cx) + (listener)(action, cx) } }), )); @@ -256,24 +246,53 @@ pub trait InteractiveElement: Sized + Element { fn on_key_down( mut self, - listener: impl Fn(&mut V, &KeyDownEvent, DispatchPhase, &mut ViewContext) + 'static, + listener: impl Fn(&KeyDownEvent, &mut WindowContext) + 'static, ) -> Self { self.interactivity() .key_down_listeners - .push(Box::new(move |view, event, phase, cx| { - listener(view, event, phase, cx) + .push(Box::new(move |event, phase, cx| { + if phase == DispatchPhase::Bubble { + (listener)(event, cx) + } })); self } - fn on_key_up( + fn capture_key_down( mut self, - listener: impl Fn(&mut V, &KeyUpEvent, DispatchPhase, &mut ViewContext) + 'static, + listener: impl Fn(&KeyDownEvent, &mut WindowContext) + 'static, + ) -> Self { + self.interactivity() + .key_down_listeners + .push(Box::new(move |event, phase, cx| { + if phase == DispatchPhase::Capture { + listener(event, cx) + } + })); + self + } + + fn on_key_up(mut self, listener: impl Fn(&KeyUpEvent, &mut WindowContext) + 'static) -> Self { + self.interactivity() + .key_up_listeners + .push(Box::new(move |event, phase, cx| { + if phase == DispatchPhase::Bubble { + listener(event, cx) + } + })); + self + } + + fn capture_key_up( + mut self, + listener: impl Fn(&KeyUpEvent, &mut WindowContext) + 'static, ) -> Self { self.interactivity() .key_up_listeners - .push(Box::new(move |view, event, phase, cx| { - listener(view, event, phase, cx) + .push(Box::new(move |event, phase, cx| { + if phase == DispatchPhase::Capture { + listener(event, cx) + } })); self } @@ -302,25 +321,22 @@ pub trait InteractiveElement: Sized + Element { fn on_drop( mut self, - listener: impl Fn(&mut V, View, &mut ViewContext) + 'static, + listener: impl Fn(&View, &mut WindowContext) + 'static, ) -> Self { self.interactivity().drop_listeners.push(( TypeId::of::(), - Box::new(move |view, dragged_view, cx| { - listener(view, dragged_view.downcast().unwrap(), cx); + Box::new(move |dragged_view, cx| { + listener(&dragged_view.downcast().unwrap(), cx); }), )); self } } -pub trait StatefulInteractiveElement>: InteractiveElement { - fn focusable(mut self) -> Focusable { +pub trait StatefulInteractiveElement: InteractiveElement { + fn focusable(mut self) -> Focusable { self.interactivity().focusable = true; - Focusable { - element: self, - view_type: PhantomData, - } + Focusable { element: self } } fn overflow_scroll(mut self) -> Self { @@ -362,40 +378,33 @@ pub trait StatefulInteractiveElement>: InteractiveElem self } - fn on_click( - mut self, - listener: impl Fn(&mut V, &ClickEvent, &mut ViewContext) + 'static, - ) -> Self + fn on_click(mut self, listener: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self where Self: Sized, { self.interactivity() .click_listeners - .push(Box::new(move |view, event, cx| listener(view, event, cx))); + .push(Box::new(move |event, cx| listener(event, cx))); self } - fn on_drag( - mut self, - listener: impl Fn(&mut V, &mut ViewContext) -> View + 'static, - ) -> Self + fn on_drag(mut self, listener: impl Fn(&mut WindowContext) -> View + 'static) -> Self where Self: Sized, - W: 'static + Render, + W: 'static + Render, { debug_assert!( self.interactivity().drag_listener.is_none(), "calling on_drag more than once on the same element is not supported" ); - self.interactivity().drag_listener = - Some(Box::new(move |view_state, cursor_offset, cx| AnyDrag { - view: listener(view_state, cx).into(), - cursor_offset, - })); + self.interactivity().drag_listener = Some(Box::new(move |cursor_offset, cx| AnyDrag { + view: listener(cx).into(), + cursor_offset, + })); self } - fn on_hover(mut self, listener: impl 'static + Fn(&mut V, bool, &mut ViewContext)) -> Self + fn on_hover(mut self, listener: impl Fn(&bool, &mut WindowContext) + 'static) -> Self where Self: Sized, { @@ -407,10 +416,7 @@ pub trait StatefulInteractiveElement>: InteractiveElem self } - fn tooltip( - mut self, - build_tooltip: impl Fn(&mut V, &mut ViewContext) -> AnyView + 'static, - ) -> Self + fn tooltip(mut self, build_tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self where Self: Sized, { @@ -418,14 +424,13 @@ pub trait StatefulInteractiveElement>: InteractiveElem self.interactivity().tooltip_builder.is_none(), "calling tooltip more than once on the same element is not supported" ); - self.interactivity().tooltip_builder = - Some(Rc::new(move |view_state, cx| build_tooltip(view_state, cx))); + self.interactivity().tooltip_builder = Some(Rc::new(build_tooltip)); self } } -pub trait FocusableElement: InteractiveElement { +pub trait FocusableElement: InteractiveElement { fn focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self where Self: Sized, @@ -442,49 +447,41 @@ pub trait FocusableElement: InteractiveElement { self } - fn on_focus( - mut self, - listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext) + 'static, - ) -> Self + fn on_focus(mut self, listener: impl Fn(&FocusEvent, &mut WindowContext) + 'static) -> Self where Self: Sized, { - self.interactivity().focus_listeners.push(Box::new( - move |view, focus_handle, event, cx| { + self.interactivity() + .focus_listeners + .push(Box::new(move |focus_handle, event, cx| { if event.focused.as_ref() == Some(focus_handle) { - listener(view, event, cx) + listener(event, cx) } - }, - )); + })); self } - fn on_blur( - mut self, - listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext) + 'static, - ) -> Self + fn on_blur(mut self, listener: impl Fn(&FocusEvent, &mut WindowContext) + 'static) -> Self where Self: Sized, { - self.interactivity().focus_listeners.push(Box::new( - move |view, focus_handle, event, cx| { + self.interactivity() + .focus_listeners + .push(Box::new(move |focus_handle, event, cx| { if event.blurred.as_ref() == Some(focus_handle) { - listener(view, event, cx) + listener(event, cx) } - }, - )); + })); self } - fn on_focus_in( - mut self, - listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext) + 'static, - ) -> Self + fn on_focus_in(mut self, listener: impl Fn(&FocusEvent, &mut WindowContext) + 'static) -> Self where Self: Sized, { - self.interactivity().focus_listeners.push(Box::new( - move |view, focus_handle, event, cx| { + self.interactivity() + .focus_listeners + .push(Box::new(move |focus_handle, event, cx| { let descendant_blurred = event .blurred .as_ref() @@ -495,22 +492,19 @@ pub trait FocusableElement: InteractiveElement { .map_or(false, |focused| focus_handle.contains(focused, cx)); if !descendant_blurred && descendant_focused { - listener(view, event, cx) + listener(event, cx) } - }, - )); + })); self } - fn on_focus_out( - mut self, - listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext) + 'static, - ) -> Self + fn on_focus_out(mut self, listener: impl Fn(&FocusEvent, &mut WindowContext) + 'static) -> Self where Self: Sized, { - self.interactivity().focus_listeners.push(Box::new( - move |view, focus_handle, event, cx| { + self.interactivity() + .focus_listeners + .push(Box::new(move |focus_handle, event, cx| { let descendant_blurred = event .blurred .as_ref() @@ -520,93 +514,79 @@ pub trait FocusableElement: InteractiveElement { .as_ref() .map_or(false, |focused| focus_handle.contains(focused, cx)); if descendant_blurred && !descendant_focused { - listener(view, event, cx) + listener(event, cx) } - }, - )); + })); self } } -pub type FocusListeners = SmallVec<[FocusListener; 2]>; +pub type FocusListeners = SmallVec<[FocusListener; 2]>; -pub type FocusListener = - Box) + 'static>; +pub type FocusListener = Box; -pub type MouseDownListener = Box< - dyn Fn(&mut V, &MouseDownEvent, &Bounds, DispatchPhase, &mut ViewContext) + 'static, ->; -pub type MouseUpListener = Box< - dyn Fn(&mut V, &MouseUpEvent, &Bounds, DispatchPhase, &mut ViewContext) + 'static, ->; +pub type MouseDownListener = + Box, DispatchPhase, &mut WindowContext) + 'static>; +pub type MouseUpListener = + Box, DispatchPhase, &mut WindowContext) + 'static>; -pub type MouseMoveListener = Box< - dyn Fn(&mut V, &MouseMoveEvent, &Bounds, DispatchPhase, &mut ViewContext) + 'static, ->; +pub type MouseMoveListener = + Box, DispatchPhase, &mut WindowContext) + 'static>; -pub type ScrollWheelListener = Box< - dyn Fn(&mut V, &ScrollWheelEvent, &Bounds, DispatchPhase, &mut ViewContext) - + 'static, ->; +pub type ScrollWheelListener = + Box, DispatchPhase, &mut WindowContext) + 'static>; -pub type ClickListener = Box) + 'static>; +pub type ClickListener = Box; -pub type DragListener = - Box, &mut ViewContext) -> AnyDrag + 'static>; +pub type DragListener = Box, &mut WindowContext) -> AnyDrag + 'static>; -type DropListener = dyn Fn(&mut V, AnyView, &mut ViewContext) + 'static; +type DropListener = dyn Fn(AnyView, &mut WindowContext) + 'static; -pub type HoverListener = Box) + 'static>; +pub type TooltipBuilder = Rc AnyView + 'static>; -pub type TooltipBuilder = Rc) -> AnyView + 'static>; +pub type KeyDownListener = Box; -pub type KeyDownListener = - Box) + 'static>; +pub type KeyUpListener = Box; -pub type KeyUpListener = - Box) + 'static>; +pub type ActionListener = Box; -pub type ActionListener = - Box) + 'static>; - -pub fn div() -> Div { +pub fn div() -> Div { Div { interactivity: Interactivity::default(), children: SmallVec::default(), } } -pub struct Div { - interactivity: Interactivity, - children: SmallVec<[AnyElement; 2]>, +pub struct Div { + interactivity: Interactivity, + children: SmallVec<[AnyElement; 2]>, } -impl Styled for Div { +impl Styled for Div { fn style(&mut self) -> &mut StyleRefinement { &mut self.interactivity.base_style } } -impl InteractiveElement for Div { - fn interactivity(&mut self) -> &mut Interactivity { +impl InteractiveElement for Div { + fn interactivity(&mut self) -> &mut Interactivity { &mut self.interactivity } } -impl ParentElement for Div { - fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { +impl ParentElement for Div { + fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { &mut self.children } } -impl Element for Div { +impl Element for Div { type State = DivState; fn layout( &mut self, - view_state: &mut V, element_state: Option, - cx: &mut ViewContext, + cx: &mut WindowContext, ) -> (LayoutId, Self::State) { let mut child_layout_ids = SmallVec::new(); let mut interactivity = mem::take(&mut self.interactivity); @@ -618,7 +598,7 @@ impl Element for Div { child_layout_ids = self .children .iter_mut() - .map(|child| child.layout(view_state, cx)) + .map(|child| child.layout(cx)) .collect::>(); cx.request_layout(&style, child_layout_ids.iter().copied()) }) @@ -637,9 +617,8 @@ impl Element for Div { fn paint( self, bounds: Bounds, - view_state: &mut V, element_state: &mut Self::State, - cx: &mut ViewContext, + cx: &mut WindowContext, ) { let mut child_min = point(Pixels::MAX, Pixels::MAX); let mut child_max = Point::default(); @@ -675,7 +654,7 @@ impl Element for Div { cx.with_content_mask(style.overflow_mask(bounds), |cx| { cx.with_element_offset(scroll_offset, |cx| { for child in self.children { - child.paint(view_state, cx); + child.paint(cx); } }) }) @@ -687,7 +666,7 @@ impl Element for Div { } } -impl RenderOnce for Div { +impl RenderOnce for Div { type Element = Self; fn element_id(&self) -> Option { @@ -710,12 +689,12 @@ impl DivState { } } -pub struct Interactivity { +pub struct Interactivity { pub element_id: Option, pub key_context: KeyContext, pub focusable: bool, pub tracked_focus_handle: Option, - pub focus_listeners: FocusListeners, + pub focus_listeners: FocusListeners, pub group: Option, pub base_style: StyleRefinement, pub focus_style: StyleRefinement, @@ -726,29 +705,26 @@ pub struct Interactivity { pub group_active_style: Option, pub drag_over_styles: SmallVec<[(TypeId, StyleRefinement); 2]>, pub group_drag_over_styles: SmallVec<[(TypeId, GroupStyle); 2]>, - pub mouse_down_listeners: SmallVec<[MouseDownListener; 2]>, - pub mouse_up_listeners: SmallVec<[MouseUpListener; 2]>, - pub mouse_move_listeners: SmallVec<[MouseMoveListener; 2]>, - pub scroll_wheel_listeners: SmallVec<[ScrollWheelListener; 2]>, - pub key_down_listeners: SmallVec<[KeyDownListener; 2]>, - pub key_up_listeners: SmallVec<[KeyUpListener; 2]>, - pub action_listeners: SmallVec<[(TypeId, ActionListener); 8]>, - pub drop_listeners: SmallVec<[(TypeId, Box>); 2]>, - pub click_listeners: SmallVec<[ClickListener; 2]>, - pub drag_listener: Option>, - pub hover_listener: Option>, - pub tooltip_builder: Option>, + pub mouse_down_listeners: SmallVec<[MouseDownListener; 2]>, + pub mouse_up_listeners: SmallVec<[MouseUpListener; 2]>, + pub mouse_move_listeners: SmallVec<[MouseMoveListener; 2]>, + pub scroll_wheel_listeners: SmallVec<[ScrollWheelListener; 2]>, + pub key_down_listeners: SmallVec<[KeyDownListener; 2]>, + pub key_up_listeners: SmallVec<[KeyUpListener; 2]>, + pub action_listeners: SmallVec<[(TypeId, ActionListener); 8]>, + pub drop_listeners: SmallVec<[(TypeId, Box); 2]>, + pub click_listeners: SmallVec<[ClickListener; 2]>, + pub drag_listener: Option, + pub hover_listener: Option>, + pub tooltip_builder: Option, } -impl Interactivity -where - V: 'static, -{ +impl Interactivity { pub fn layout( &mut self, element_state: Option, - cx: &mut ViewContext, - f: impl FnOnce(Style, &mut ViewContext) -> LayoutId, + cx: &mut WindowContext, + f: impl FnOnce(Style, &mut WindowContext) -> LayoutId, ) -> (LayoutId, InteractiveElementState) { let mut element_state = element_state.unwrap_or_default(); @@ -774,8 +750,8 @@ where bounds: Bounds, content_size: Size, element_state: &mut InteractiveElementState, - cx: &mut ViewContext, - f: impl FnOnce(Style, Point, &mut ViewContext), + cx: &mut WindowContext, + f: impl FnOnce(Style, Point, &mut WindowContext), ) { let style = self.compute_style(Some(bounds), element_state, cx); @@ -786,27 +762,27 @@ where } } - for listener in self.mouse_down_listeners { - cx.on_mouse_event(move |state, event: &MouseDownEvent, phase, cx| { - listener(state, event, &bounds, phase, cx); + for listener in self.mouse_down_listeners.drain(..) { + cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| { + listener(event, &bounds, phase, cx); }) } - for listener in self.mouse_up_listeners { - cx.on_mouse_event(move |state, event: &MouseUpEvent, phase, cx| { - listener(state, event, &bounds, phase, cx); + for listener in self.mouse_up_listeners.drain(..) { + cx.on_mouse_event(move |event: &MouseUpEvent, phase, cx| { + listener(event, &bounds, phase, cx); }) } - for listener in self.mouse_move_listeners { - cx.on_mouse_event(move |state, event: &MouseMoveEvent, phase, cx| { - listener(state, event, &bounds, phase, cx); + for listener in self.mouse_move_listeners.drain(..) { + cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| { + listener(event, &bounds, phase, cx); }) } - for listener in self.scroll_wheel_listeners { - cx.on_mouse_event(move |state, event: &ScrollWheelEvent, phase, cx| { - listener(state, event, &bounds, phase, cx); + for listener in self.scroll_wheel_listeners.drain(..) { + cx.on_mouse_event(move |event: &ScrollWheelEvent, phase, cx| { + listener(event, &bounds, phase, cx); }) } @@ -817,7 +793,7 @@ where if let Some(group_bounds) = hover_group_bounds { let hovered = group_bounds.contains_point(&cx.mouse_position()); - cx.on_mouse_event(move |_, event: &MouseMoveEvent, phase, cx| { + cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| { if phase == DispatchPhase::Capture { if group_bounds.contains_point(&event.position) != hovered { cx.notify(); @@ -830,7 +806,7 @@ where || (cx.active_drag.is_some() && !self.drag_over_styles.is_empty()) { let hovered = bounds.contains_point(&cx.mouse_position()); - cx.on_mouse_event(move |_, event: &MouseMoveEvent, phase, cx| { + cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| { if phase == DispatchPhase::Capture { if bounds.contains_point(&event.position) != hovered { cx.notify(); @@ -840,8 +816,8 @@ where } if cx.active_drag.is_some() { - let drop_listeners = self.drop_listeners; - cx.on_mouse_event(move |view, event: &MouseUpEvent, phase, cx| { + let drop_listeners = mem::take(&mut self.drop_listeners); + cx.on_mouse_event(move |event: &MouseUpEvent, phase, cx| { if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { if let Some(drag_state_type) = cx.active_drag.as_ref().map(|drag| drag.view.entity_type()) @@ -852,7 +828,7 @@ where .active_drag .take() .expect("checked for type drag state type above"); - listener(view, drag.view.clone(), cx); + listener(drag.view.clone(), cx); cx.notify(); cx.stop_propagation(); } @@ -872,7 +848,7 @@ where if let Some(drag_listener) = drag_listener { let active_state = element_state.clicked_state.clone(); - cx.on_mouse_event(move |view_state, event: &MouseMoveEvent, phase, cx| { + cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| { if cx.active_drag.is_some() { if phase == DispatchPhase::Capture { cx.notify(); @@ -883,7 +859,7 @@ where { *active_state.borrow_mut() = ElementClickedState::default(); let cursor_offset = event.position - bounds.origin; - let drag = drag_listener(view_state, cursor_offset, cx); + let drag = drag_listener(cursor_offset, cx); cx.active_drag = Some(drag); cx.notify(); cx.stop_propagation(); @@ -891,21 +867,21 @@ where }); } - cx.on_mouse_event(move |view_state, event: &MouseUpEvent, phase, cx| { + cx.on_mouse_event(move |event: &MouseUpEvent, phase, cx| { if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { let mouse_click = ClickEvent { down: mouse_down.clone(), up: event.clone(), }; for listener in &click_listeners { - listener(view_state, &mouse_click, cx); + listener(&mouse_click, cx); } } *pending_mouse_down.borrow_mut() = None; cx.notify(); }); } else { - cx.on_mouse_event(move |_view_state, event: &MouseDownEvent, phase, cx| { + cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| { if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { *pending_mouse_down.borrow_mut() = Some(event.clone()); cx.notify(); @@ -918,7 +894,7 @@ where let was_hovered = element_state.hover_state.clone(); let has_mouse_down = element_state.pending_mouse_down.clone(); - cx.on_mouse_event(move |view_state, event: &MouseMoveEvent, phase, cx| { + cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| { if phase != DispatchPhase::Bubble { return; } @@ -930,7 +906,7 @@ where *was_hovered = is_hovered; drop(was_hovered); - hover_listener(view_state, is_hovered, cx); + hover_listener(&is_hovered, cx); } }); } @@ -939,7 +915,7 @@ where let active_tooltip = element_state.active_tooltip.clone(); let pending_mouse_down = element_state.pending_mouse_down.clone(); - cx.on_mouse_event(move |_, event: &MouseMoveEvent, phase, cx| { + cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| { if phase != DispatchPhase::Bubble { return; } @@ -956,12 +932,12 @@ where let active_tooltip = active_tooltip.clone(); let tooltip_builder = tooltip_builder.clone(); - move |view, mut cx| async move { + move |mut cx| async move { cx.background_executor().timer(TOOLTIP_DELAY).await; - view.update(&mut cx, move |view_state, cx| { + cx.update(|_, cx| { active_tooltip.borrow_mut().replace(ActiveTooltip { tooltip: Some(AnyTooltip { - view: tooltip_builder(view_state, cx), + view: tooltip_builder(cx), cursor_offset: cx.mouse_position(), }), _task: None, @@ -979,7 +955,7 @@ where }); let active_tooltip = element_state.active_tooltip.clone(); - cx.on_mouse_event(move |_, _: &MouseDownEvent, _, _| { + cx.on_mouse_event(move |_: &MouseDownEvent, _, _| { active_tooltip.borrow_mut().take(); }); @@ -992,7 +968,7 @@ where let active_state = element_state.clicked_state.clone(); if !active_state.borrow().is_clicked() { - cx.on_mouse_event(move |_, _: &MouseUpEvent, phase, cx| { + cx.on_mouse_event(move |_: &MouseUpEvent, phase, cx| { if phase == DispatchPhase::Capture { *active_state.borrow_mut() = ElementClickedState::default(); cx.notify(); @@ -1003,7 +979,7 @@ where .group_active_style .as_ref() .and_then(|group_active| GroupBounds::get(&group_active.group, cx)); - cx.on_mouse_event(move |_view, down: &MouseDownEvent, phase, cx| { + cx.on_mouse_event(move |down: &MouseDownEvent, phase, cx| { if phase == DispatchPhase::Bubble { let group = active_group_bounds .map_or(false, |bounds| bounds.contains_point(&down.position)); @@ -1025,7 +1001,7 @@ where let line_height = cx.line_height(); let scroll_max = (content_size - bounds.size).max(&Size::default()); - cx.on_mouse_event(move |_, event: &ScrollWheelEvent, phase, cx| { + cx.on_mouse_event(move |event: &ScrollWheelEvent, phase, cx| { if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) { let mut scroll_offset = scroll_offset.borrow_mut(); let old_scroll_offset = *scroll_offset; @@ -1062,15 +1038,15 @@ where self.key_context.clone(), element_state.focus_handle.clone(), |_, cx| { - for listener in self.key_down_listeners { - cx.on_key_event(move |state, event: &KeyDownEvent, phase, cx| { - listener(state, event, phase, cx); + for listener in self.key_down_listeners.drain(..) { + cx.on_key_event(move |event: &KeyDownEvent, phase, cx| { + listener(event, phase, cx); }) } - for listener in self.key_up_listeners { - cx.on_key_event(move |state, event: &KeyUpEvent, phase, cx| { - listener(state, event, phase, cx); + for listener in self.key_up_listeners.drain(..) { + cx.on_key_event(move |event: &KeyUpEvent, phase, cx| { + listener(event, phase, cx); }) } @@ -1081,9 +1057,7 @@ where if let Some(focus_handle) = element_state.focus_handle.as_ref() { for listener in self.focus_listeners { let focus_handle = focus_handle.clone(); - cx.on_focus_changed(move |view, event, cx| { - listener(view, &focus_handle, event, cx) - }); + cx.on_focus_changed(move |event, cx| listener(&focus_handle, event, cx)); } } @@ -1100,7 +1074,7 @@ where &self, bounds: Option>, element_state: &mut InteractiveElementState, - cx: &mut ViewContext, + cx: &mut WindowContext, ) -> Style { let mut style = Style::default(); style.refine(&self.base_style); @@ -1171,7 +1145,7 @@ where } } -impl Default for Interactivity { +impl Default for Interactivity { fn default() -> Self { Self { element_id: None, @@ -1259,31 +1233,25 @@ impl GroupBounds { } } -pub struct Focusable { +pub struct Focusable { element: E, - view_type: PhantomData, } -impl, E: InteractiveElement> FocusableElement for Focusable {} +impl FocusableElement for Focusable {} -impl InteractiveElement for Focusable +impl InteractiveElement for Focusable where - V: 'static + Render, - E: InteractiveElement, + E: InteractiveElement, { - fn interactivity(&mut self) -> &mut Interactivity { + fn interactivity(&mut self) -> &mut Interactivity { self.element.interactivity() } } -impl, E: StatefulInteractiveElement> StatefulInteractiveElement - for Focusable -{ -} +impl StatefulInteractiveElement for Focusable {} -impl Styled for Focusable +impl Styled for Focusable where - V: 'static, E: Styled, { fn style(&mut self) -> &mut StyleRefinement { @@ -1291,67 +1259,55 @@ where } } -impl Element for Focusable +impl Element for Focusable where - V: 'static + Render, - E: Element, + E: Element, { type State = E::State; fn layout( &mut self, - view_state: &mut V, - element_state: Option, - cx: &mut ViewContext, + state: Option, + cx: &mut WindowContext, ) -> (LayoutId, Self::State) { - self.element.layout(view_state, element_state, cx) + self.element.layout(state, cx) } - fn paint( - self, - bounds: Bounds, - view_state: &mut V, - element_state: &mut Self::State, - cx: &mut ViewContext, - ) { - self.element.paint(bounds, view_state, element_state, cx) + fn paint(self, bounds: Bounds, state: &mut Self::State, cx: &mut WindowContext) { + self.element.paint(bounds, state, cx) } } -impl RenderOnce for Focusable +impl RenderOnce for Focusable where - V: 'static + Render, - E: Element, + E: Element, { - type Element = Self; + type Element = E; fn element_id(&self) -> Option { self.element.element_id() } fn render_once(self) -> Self::Element { - self + self.element } } -impl ParentElement for Focusable +impl ParentElement for Focusable where - V: 'static, - E: ParentElement, + E: ParentElement, { - fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { + fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { self.element.children_mut() } } -pub struct Stateful { +pub struct Stateful { element: E, - view_type: PhantomData, } -impl Styled for Stateful +impl Styled for Stateful where - V: 'static, E: Styled, { fn style(&mut self) -> &mut StyleRefinement { @@ -1359,57 +1315,46 @@ where } } -impl StatefulInteractiveElement for Stateful +impl StatefulInteractiveElement for Stateful where - V: 'static, - E: Element, - Self: InteractiveElement, + E: Element, + Self: InteractiveElement, { } -impl InteractiveElement for Stateful +impl InteractiveElement for Stateful where - V: 'static, - E: InteractiveElement, + E: InteractiveElement, { - fn interactivity(&mut self) -> &mut Interactivity { + fn interactivity(&mut self) -> &mut Interactivity { self.element.interactivity() } } -impl> FocusableElement for Stateful {} +impl FocusableElement for Stateful {} -impl Element for Stateful +impl Element for Stateful where - V: 'static, - E: Element, + E: Element, { type State = E::State; fn layout( &mut self, - view_state: &mut V, - element_state: Option, - cx: &mut ViewContext, + state: Option, + cx: &mut WindowContext, ) -> (LayoutId, Self::State) { - self.element.layout(view_state, element_state, cx) + self.element.layout(state, cx) } - fn paint( - self, - bounds: Bounds, - view_state: &mut V, - element_state: &mut Self::State, - cx: &mut ViewContext, - ) { - self.element.paint(bounds, view_state, element_state, cx) + fn paint(self, bounds: Bounds, state: &mut Self::State, cx: &mut WindowContext) { + self.element.paint(bounds, state, cx) } } -impl RenderOnce for Stateful +impl RenderOnce for Stateful where - V: 'static, - E: Element, + E: Element, { type Element = Self; @@ -1422,12 +1367,11 @@ where } } -impl ParentElement for Stateful +impl ParentElement for Stateful where - V: 'static, - E: ParentElement, + E: ParentElement, { - fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { + fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { self.element.children_mut() } } diff --git a/crates/gpui2/src/elements/img.rs b/crates/gpui2/src/elements/img.rs index a6579b022a649fc5cce99a4b188a9bb017cb4a47..3c0f4c00be852ed2df5295ce83d47b583582f747 100644 --- a/crates/gpui2/src/elements/img.rs +++ b/crates/gpui2/src/elements/img.rs @@ -1,17 +1,17 @@ use crate::{ - BorrowWindow, Bounds, Element, InteractiveElement, InteractiveElementState, Interactivity, - LayoutId, Pixels, RenderOnce, SharedString, StyleRefinement, Styled, ViewContext, + Bounds, Element, InteractiveElement, InteractiveElementState, Interactivity, LayoutId, Pixels, + RenderOnce, SharedString, StyleRefinement, Styled, WindowContext, }; use futures::FutureExt; use util::ResultExt; -pub struct Img { - interactivity: Interactivity, +pub struct Img { + interactivity: Interactivity, uri: Option, grayscale: bool, } -pub fn img() -> Img { +pub fn img() -> Img { Img { interactivity: Interactivity::default(), uri: None, @@ -19,10 +19,7 @@ pub fn img() -> Img { } } -impl Img -where - V: 'static, -{ +impl Img { pub fn uri(mut self, uri: impl Into) -> Self { self.uri = Some(uri.into()); self @@ -34,14 +31,13 @@ where } } -impl Element for Img { +impl Element for Img { type State = InteractiveElementState; fn layout( &mut self, - _view_state: &mut V, element_state: Option, - cx: &mut ViewContext, + cx: &mut WindowContext, ) -> (LayoutId, Self::State) { self.interactivity.layout(element_state, cx, |style, cx| { cx.request_layout(&style, None) @@ -51,9 +47,8 @@ impl Element for Img { fn paint( self, bounds: Bounds, - _view_state: &mut V, element_state: &mut Self::State, - cx: &mut ViewContext, + cx: &mut WindowContext, ) { self.interactivity.paint( bounds, @@ -78,7 +73,7 @@ impl Element for Img { .log_err() }); } else { - cx.spawn(|_, mut cx| async move { + cx.spawn(|mut cx| async move { if image_future.await.ok().is_some() { cx.on_next_frame(|cx| cx.notify()); } @@ -91,7 +86,7 @@ impl Element for Img { } } -impl RenderOnce for Img { +impl RenderOnce for Img { type Element = Self; fn element_id(&self) -> Option { @@ -103,14 +98,14 @@ impl RenderOnce for Img { } } -impl Styled for Img { +impl Styled for Img { fn style(&mut self) -> &mut StyleRefinement { &mut self.interactivity.base_style } } -impl InteractiveElement for Img { - fn interactivity(&mut self) -> &mut Interactivity { +impl InteractiveElement for Img { + fn interactivity(&mut self) -> &mut Interactivity { &mut self.interactivity } } diff --git a/crates/gpui2/src/elements/overlay.rs b/crates/gpui2/src/elements/overlay.rs index 79a3643bdc2bb6e3921e85d9171979bbc8634507..29ac2f00c46b2807c9417a9cbeccfa6dd866ca07 100644 --- a/crates/gpui2/src/elements/overlay.rs +++ b/crates/gpui2/src/elements/overlay.rs @@ -3,15 +3,15 @@ use taffy::style::{Display, Position}; use crate::{ point, AnyElement, BorrowWindow, Bounds, Element, LayoutId, ParentElement, Pixels, Point, - RenderOnce, Size, Style, + RenderOnce, Size, Style, WindowContext, }; pub struct OverlayState { child_layout_ids: SmallVec<[LayoutId; 4]>, } -pub struct Overlay { - children: SmallVec<[AnyElement; 2]>, +pub struct Overlay { + children: SmallVec<[AnyElement; 2]>, anchor_corner: AnchorCorner, fit_mode: OverlayFitMode, // todo!(); @@ -21,7 +21,7 @@ pub struct Overlay { /// overlay gives you a floating element that will avoid overflowing the window bounds. /// Its children should have no margin to avoid measurement issues. -pub fn overlay() -> Overlay { +pub fn overlay() -> Overlay { Overlay { children: SmallVec::new(), anchor_corner: AnchorCorner::TopLeft, @@ -30,7 +30,7 @@ pub fn overlay() -> Overlay { } } -impl Overlay { +impl Overlay { /// Sets which corner of the overlay should be anchored to the current position. pub fn anchor(mut self, anchor: AnchorCorner) -> Self { self.anchor_corner = anchor; @@ -51,25 +51,24 @@ impl Overlay { } } -impl ParentElement for Overlay { - fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { +impl ParentElement for Overlay { + fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { &mut self.children } } -impl Element for Overlay { +impl Element for Overlay { type State = OverlayState; fn layout( &mut self, - view_state: &mut V, _: Option, - cx: &mut crate::ViewContext, + cx: &mut WindowContext, ) -> (crate::LayoutId, Self::State) { let child_layout_ids = self .children .iter_mut() - .map(|child| child.layout(view_state, cx)) + .map(|child| child.layout(cx)) .collect::>(); let mut overlay_style = Style::default(); @@ -84,9 +83,8 @@ impl Element for Overlay { fn paint( self, bounds: crate::Bounds, - view_state: &mut V, element_state: &mut Self::State, - cx: &mut crate::ViewContext, + cx: &mut WindowContext, ) { if element_state.child_layout_ids.is_empty() { return; @@ -147,13 +145,13 @@ impl Element for Overlay { cx.with_element_offset(desired.origin - bounds.origin, |cx| { for child in self.children { - child.paint(view_state, cx); + child.paint(cx); } }) } } -impl RenderOnce for Overlay { +impl RenderOnce for Overlay { type Element = Self; fn element_id(&self) -> Option { diff --git a/crates/gpui2/src/elements/svg.rs b/crates/gpui2/src/elements/svg.rs index 0940bdd1eb2bf52c18cacc56ba9f8d2776ab43e7..c24e4d9b8b88be501e8744e86982a0cbbc542f76 100644 --- a/crates/gpui2/src/elements/svg.rs +++ b/crates/gpui2/src/elements/svg.rs @@ -1,49 +1,43 @@ use crate::{ Bounds, Element, ElementId, InteractiveElement, InteractiveElementState, Interactivity, - LayoutId, Pixels, RenderOnce, SharedString, StyleRefinement, Styled, ViewContext, + LayoutId, Pixels, RenderOnce, SharedString, StyleRefinement, Styled, WindowContext, }; use util::ResultExt; -pub struct Svg { - interactivity: Interactivity, +pub struct Svg { + interactivity: Interactivity, path: Option, } -pub fn svg() -> Svg { +pub fn svg() -> Svg { Svg { interactivity: Interactivity::default(), path: None, } } -impl Svg { +impl Svg { pub fn path(mut self, path: impl Into) -> Self { self.path = Some(path.into()); self } } -impl Element for Svg { +impl Element for Svg { type State = InteractiveElementState; fn layout( &mut self, - _view_state: &mut V, element_state: Option, - cx: &mut ViewContext, + cx: &mut WindowContext, ) -> (LayoutId, Self::State) { self.interactivity.layout(element_state, cx, |style, cx| { cx.request_layout(&style, None) }) } - fn paint( - self, - bounds: Bounds, - _view_state: &mut V, - element_state: &mut Self::State, - cx: &mut ViewContext, - ) where + fn paint(self, bounds: Bounds, element_state: &mut Self::State, cx: &mut WindowContext) + where Self: Sized, { self.interactivity @@ -55,7 +49,7 @@ impl Element for Svg { } } -impl RenderOnce for Svg { +impl RenderOnce for Svg { type Element = Self; fn element_id(&self) -> Option { @@ -67,14 +61,14 @@ impl RenderOnce for Svg { } } -impl Styled for Svg { +impl Styled for Svg { fn style(&mut self) -> &mut StyleRefinement { &mut self.interactivity.base_style } } -impl InteractiveElement for Svg { - fn interactivity(&mut self) -> &mut Interactivity { +impl InteractiveElement for Svg { + fn interactivity(&mut self) -> &mut Interactivity { &mut self.interactivity } } diff --git a/crates/gpui2/src/elements/text.rs b/crates/gpui2/src/elements/text.rs index c68684c8d39dc676294a62cfdd98da8e81bb27b3..a34de482d58c8a36eca9d535e15d2e83fe402e96 100644 --- a/crates/gpui2/src/elements/text.rs +++ b/crates/gpui2/src/elements/text.rs @@ -1,6 +1,6 @@ use crate::{ - BorrowWindow, Bounds, Element, ElementId, LayoutId, Pixels, RenderOnce, SharedString, Size, - TextRun, ViewContext, WindowContext, WrappedLine, + Bounds, Element, ElementId, LayoutId, Pixels, RenderOnce, SharedString, Size, TextRun, + WhiteSpace, WindowContext, WrappedLine, }; use anyhow::anyhow; use parking_lot::{Mutex, MutexGuard}; @@ -8,32 +8,25 @@ use smallvec::SmallVec; use std::{cell::Cell, rc::Rc, sync::Arc}; use util::ResultExt; -impl Element for &'static str { +impl Element for &'static str { type State = TextState; fn layout( &mut self, - _: &mut V, _: Option, - cx: &mut ViewContext, + cx: &mut WindowContext, ) -> (LayoutId, Self::State) { let mut state = TextState::default(); let layout_id = state.layout(SharedString::from(*self), None, cx); (layout_id, state) } - fn paint( - self, - bounds: Bounds, - _: &mut V, - state: &mut TextState, - cx: &mut ViewContext, - ) { + fn paint(self, bounds: Bounds, state: &mut TextState, cx: &mut WindowContext) { state.paint(bounds, self, cx) } } -impl RenderOnce for &'static str { +impl RenderOnce for &'static str { type Element = Self; fn element_id(&self) -> Option { @@ -45,37 +38,30 @@ impl RenderOnce for &'static str { } } -impl Element for SharedString { +impl Element for SharedString { type State = TextState; fn layout( &mut self, - _: &mut V, _: Option, - cx: &mut ViewContext, + cx: &mut WindowContext, ) -> (LayoutId, Self::State) { let mut state = TextState::default(); let layout_id = state.layout(self.clone(), None, cx); (layout_id, state) } - fn paint( - self, - bounds: Bounds, - _: &mut V, - state: &mut TextState, - cx: &mut ViewContext, - ) { + fn paint(self, bounds: Bounds, state: &mut TextState, cx: &mut WindowContext) { let text_str: &str = self.as_ref(); state.paint(bounds, text_str, cx) } } -impl RenderOnce for SharedString { +impl RenderOnce for SharedString { type Element = Self; fn element_id(&self) -> Option { - Some(self.clone().into()) + None } fn render_once(self) -> Self::Element { @@ -102,110 +88,25 @@ impl StyledText { } } -impl Element for StyledText { +impl Element for StyledText { type State = TextState; fn layout( &mut self, - _view: &mut V, - element_state: Option, - cx: &mut ViewContext, + _: Option, + cx: &mut WindowContext, ) -> (LayoutId, Self::State) { - let element_state = element_state.unwrap_or_default(); - let text_system = cx.text_system().clone(); - let text_style = cx.text_style(); - let font_size = text_style.font_size.to_pixels(cx.rem_size()); - let line_height = text_style - .line_height - .to_pixels(font_size.into(), cx.rem_size()); - let text = self.text.clone(); - - let rem_size = cx.rem_size(); - - let runs = if let Some(runs) = self.runs.take() { - runs - } else { - vec![text_style.to_run(text.len())] - }; - - let layout_id = cx.request_measured_layout(Default::default(), rem_size, { - let element_state = element_state.clone(); - move |known_dimensions, available_space| { - let wrap_width = known_dimensions.width.or(match available_space.width { - crate::AvailableSpace::Definite(x) => Some(x), - _ => None, - }); - - if let Some(text_state) = element_state.0.lock().as_ref() { - if text_state.size.is_some() - && (wrap_width.is_none() || wrap_width == text_state.wrap_width) - { - return text_state.size.unwrap(); - } - } - - let Some(lines) = text_system - .shape_text( - &text, - font_size, - &runs[..], - wrap_width, // Wrap if we know the width. - ) - .log_err() - else { - element_state.lock().replace(TextStateInner { - lines: Default::default(), - line_height, - wrap_width, - size: Some(Size::default()), - }); - return Size::default(); - }; - - 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(TextStateInner { - lines, - line_height, - wrap_width, - size: Some(size), - }); - - size - } - }); - - (layout_id, element_state) + let mut state = TextState::default(); + let layout_id = state.layout(self.text.clone(), self.runs.take(), cx); + (layout_id, state) } - fn paint( - self, - bounds: Bounds, - _: &mut V, - element_state: &mut Self::State, - cx: &mut ViewContext, - ) { - let element_state = element_state.lock(); - let element_state = element_state - .as_ref() - .ok_or_else(|| anyhow!("measurement has not been performed on {}", &self.text)) - .unwrap(); - - let line_height = element_state.line_height; - let mut line_origin = bounds.origin; - for line in &element_state.lines { - line.paint(line_origin, line_height, cx).log_err(); - line_origin.y += line.size(line_height).height; - } + fn paint(self, bounds: Bounds, state: &mut Self::State, cx: &mut WindowContext) { + state.paint(bounds, &self.text, cx) } } -impl RenderOnce for StyledText { +impl RenderOnce for StyledText { type Element = Self; fn element_id(&self) -> Option { @@ -258,10 +159,14 @@ impl TextState { let element_state = self.clone(); move |known_dimensions, available_space| { - let wrap_width = known_dimensions.width.or(match available_space.width { - crate::AvailableSpace::Definite(x) => Some(x), - _ => None, - }); + let wrap_width = if text_style.white_space == WhiteSpace::Normal { + known_dimensions.width.or(match available_space.width { + crate::AvailableSpace::Definite(x) => Some(x), + _ => None, + }) + } else { + None + }; if let Some(text_state) = element_state.0.lock().as_ref() { if text_state.size.is_some() @@ -273,10 +178,7 @@ impl TextState { let Some(lines) = text_system .shape_text( - &text, - font_size, - &runs[..], - wrap_width, // Wrap if we know the width. + &text, font_size, &runs, wrap_width, // Wrap if we know the width. ) .log_err() else { @@ -293,7 +195,7 @@ impl TextState { for line in &lines { let line_size = line.size(line_height); size.height += line_size.height; - size.width = size.width.max(line_size.width); + size.width = size.width.max(line_size.width).ceil(); } element_state.lock().replace(TextStateInner { @@ -327,7 +229,7 @@ impl TextState { } struct InteractiveText { - id: ElementId, + element_id: ElementId, text: StyledText, } @@ -336,28 +238,27 @@ struct InteractiveTextState { clicked_range_ixs: Rc>>, } -impl Element for InteractiveText { +impl Element for InteractiveText { type State = InteractiveTextState; fn layout( &mut self, - view_state: &mut V, - element_state: Option, - cx: &mut ViewContext, + state: Option, + cx: &mut WindowContext, ) -> (LayoutId, Self::State) { if let Some(InteractiveTextState { text_state, clicked_range_ixs, - }) = element_state + }) = state { - let (layout_id, text_state) = self.text.layout(view_state, Some(text_state), cx); + let (layout_id, text_state) = self.text.layout(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 (layout_id, text_state) = self.text.layout(None, cx); let element_state = InteractiveTextState { text_state, clicked_range_ixs: Rc::default(), @@ -366,23 +267,16 @@ impl Element for InteractiveText { } } - fn paint( - self, - bounds: Bounds, - view_state: &mut V, - element_state: &mut Self::State, - cx: &mut ViewContext, - ) { - self.text - .paint(bounds, view_state, &mut element_state.text_state, cx) + fn paint(self, bounds: Bounds, state: &mut Self::State, cx: &mut WindowContext) { + self.text.paint(bounds, &mut state.text_state, cx) } } -impl RenderOnce for InteractiveText { +impl RenderOnce for InteractiveText { type Element = Self; fn element_id(&self) -> Option { - Some(self.id.clone()) + Some(self.element_id.clone()) } fn render_once(self) -> Self::Element { diff --git a/crates/gpui2/src/elements/uniform_list.rs b/crates/gpui2/src/elements/uniform_list.rs index caf18962ec03f76b9c88298b8642109c2ac350dc..3c3222c8a0d43ca26c123508db4904ba88ad203a 100644 --- a/crates/gpui2/src/elements/uniform_list.rs +++ b/crates/gpui2/src/elements/uniform_list.rs @@ -1,7 +1,7 @@ use crate::{ - point, px, size, AnyElement, AvailableSpace, BorrowWindow, Bounds, Element, ElementId, - InteractiveElement, InteractiveElementState, Interactivity, LayoutId, Pixels, Point, - RenderOnce, Size, StyleRefinement, Styled, ViewContext, + point, px, size, AnyElement, AvailableSpace, Bounds, Element, ElementId, InteractiveElement, + InteractiveElementState, Interactivity, LayoutId, Pixels, Point, Render, RenderOnce, Size, + StyleRefinement, Styled, View, ViewContext, WindowContext, }; use smallvec::SmallVec; use std::{cell::RefCell, cmp, ops::Range, rc::Rc}; @@ -10,52 +10,51 @@ use taffy::style::Overflow; /// uniform_list provides lazy rendering for a set of items that are of uniform height. /// When rendered into a container with overflow-y: hidden and a fixed (or max) height, /// uniform_list will only render the visibile subset of items. -pub fn uniform_list( +pub fn uniform_list( + view: View, id: I, item_count: usize, - f: impl 'static + Fn(&mut V, Range, &mut ViewContext) -> Vec, -) -> UniformList + f: impl 'static + Fn(&mut V, Range, &mut ViewContext) -> Vec, +) -> UniformList where I: Into, - V: 'static, - E: Element, + R: RenderOnce, + V: Render, { let id = id.into(); - let mut style = StyleRefinement::default(); - style.overflow.y = Some(Overflow::Hidden); + let mut base_style = StyleRefinement::default(); + base_style.overflow.y = Some(Overflow::Scroll); + + let render_range = move |range, cx: &mut WindowContext| { + view.update(cx, |this, cx| { + f(this, range, cx) + .into_iter() + .map(|component| component.render_into_any()) + .collect() + }) + }; UniformList { id: id.clone(), - style, item_count, item_to_measure_index: 0, - render_items: Box::new(move |view, visible_range, cx| { - f(view, visible_range, cx) - .into_iter() - .map(|component| component.into_any()) - .collect() - }), + render_items: Box::new(render_range), interactivity: Interactivity { element_id: Some(id.into()), + base_style, ..Default::default() }, scroll_handle: None, } } -pub struct UniformList { +pub struct UniformList { id: ElementId, - style: StyleRefinement, item_count: usize, item_to_measure_index: usize, - render_items: Box< - dyn for<'a> Fn( - &'a mut V, - Range, - &'a mut ViewContext, - ) -> SmallVec<[AnyElement; 64]>, - >, - interactivity: Interactivity, + render_items: + Box Fn(Range, &'a mut WindowContext) -> SmallVec<[AnyElement; 64]>>, + interactivity: Interactivity, scroll_handle: Option, } @@ -89,9 +88,9 @@ impl UniformListScrollHandle { } } -impl Styled for UniformList { +impl Styled for UniformList { fn style(&mut self) -> &mut StyleRefinement { - &mut self.style + &mut self.interactivity.base_style } } @@ -101,25 +100,24 @@ pub struct UniformListState { item_size: Size, } -impl Element for UniformList { +impl Element for UniformList { type State = UniformListState; fn layout( &mut self, - view_state: &mut V, - element_state: Option, - cx: &mut ViewContext, + state: Option, + cx: &mut WindowContext, ) -> (LayoutId, Self::State) { let max_items = self.item_count; let rem_size = cx.rem_size(); - let item_size = element_state + let item_size = state .as_ref() .map(|s| s.item_size) - .unwrap_or_else(|| self.measure_item(view_state, None, cx)); + .unwrap_or_else(|| self.measure_item(None, cx)); let (layout_id, interactive) = self.interactivity - .layout(element_state.map(|s| s.interactive), cx, |style, cx| { + .layout(state.map(|s| s.interactive), cx, |style, cx| { cx.request_measured_layout( style, rem_size, @@ -157,9 +155,8 @@ impl Element for UniformList { fn paint( self, bounds: Bounds, - view_state: &mut V, element_state: &mut Self::State, - cx: &mut ViewContext, + cx: &mut WindowContext, ) { let style = self.interactivity @@ -185,9 +182,7 @@ impl Element for UniformList { .get_or_insert_with(Rc::default) .clone(); - let item_height = self - .measure_item(view_state, Some(padded_bounds.size.width), cx) - .height; + let item_height = self.measure_item(Some(padded_bounds.size.width), cx).height; self.interactivity.paint( bounds, @@ -229,7 +224,7 @@ impl Element for UniformList { self.item_count, ); - let items = (self.render_items)(view_state, visible_range.clone(), cx); + let items = (self.render_items)(visible_range.clone(), cx); cx.with_z_index(1, |cx| { for (item, ix) in items.into_iter().zip(visible_range) { let item_origin = padded_bounds.origin @@ -238,7 +233,7 @@ impl Element for UniformList { AvailableSpace::Definite(padded_bounds.size.width), AvailableSpace::Definite(item_height), ); - item.draw(item_origin, available_space, view_state, cx); + item.draw(item_origin, available_space, cx); } }); } @@ -248,7 +243,7 @@ impl Element for UniformList { } } -impl RenderOnce for UniformList { +impl RenderOnce for UniformList { type Element = Self; fn element_id(&self) -> Option { @@ -260,24 +255,19 @@ impl RenderOnce for UniformList { } } -impl UniformList { +impl UniformList { pub fn with_width_from_item(mut self, item_index: Option) -> Self { self.item_to_measure_index = item_index.unwrap_or(0); self } - fn measure_item( - &self, - view_state: &mut V, - list_width: Option, - cx: &mut ViewContext, - ) -> Size { + fn measure_item(&self, list_width: Option, cx: &mut WindowContext) -> Size { if self.item_count == 0 { return Size::default(); } let item_ix = cmp::min(self.item_to_measure_index, self.item_count - 1); - let mut items = (self.render_items)(view_state, item_ix..item_ix + 1, cx); + let mut items = (self.render_items)(item_ix..item_ix + 1, cx); let mut item_to_measure = items.pop().unwrap(); let available_space = size( list_width.map_or(AvailableSpace::MinContent, |width| { @@ -285,7 +275,7 @@ impl UniformList { }), AvailableSpace::MinContent, ); - item_to_measure.measure(available_space, view_state, cx) + item_to_measure.measure(available_space, cx) } pub fn track_scroll(mut self, handle: UniformListScrollHandle) -> Self { @@ -294,8 +284,8 @@ impl UniformList { } } -impl InteractiveElement for UniformList { - fn interactivity(&mut self) -> &mut crate::Interactivity { +impl InteractiveElement for UniformList { + fn interactivity(&mut self) -> &mut crate::Interactivity { &mut self.interactivity } } diff --git a/crates/gpui2/src/gpui2.rs b/crates/gpui2/src/gpui2.rs index 98e1c043cd32b4ef168512e58a32d710277731b6..984859f1b005f8fa2edd3256de75c1aa3010ce2b 100644 --- a/crates/gpui2/src/gpui2.rs +++ b/crates/gpui2/src/gpui2.rs @@ -121,7 +121,7 @@ pub trait VisualContext: Context { build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V, ) -> Self::Result> where - V: 'static + Render; + V: 'static + Render; fn update_view( &mut self, @@ -134,7 +134,7 @@ pub trait VisualContext: Context { build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V, ) -> Self::Result> where - V: 'static + Render; + V: 'static + Render; fn focus_view(&mut self, view: &View) -> Self::Result<()> where diff --git a/crates/gpui2/src/input.rs b/crates/gpui2/src/input.rs index 140f72441794d3b66b562ba4bbc22b74e4131d4d..8592eeffeb3573e4d77e55e252a814630d3c959d 100644 --- a/crates/gpui2/src/input.rs +++ b/crates/gpui2/src/input.rs @@ -1,4 +1,6 @@ -use crate::{AsyncWindowContext, Bounds, Pixels, PlatformInputHandler, View, ViewContext}; +use crate::{ + AsyncWindowContext, Bounds, Pixels, PlatformInputHandler, View, ViewContext, WindowContext, +}; use std::ops::Range; /// Implement this trait to allow views to handle textual input when implementing an editor, field, etc. @@ -43,9 +45,9 @@ pub struct ElementInputHandler { impl ElementInputHandler { /// Used in [Element::paint] with the element's bounds and a view context for its /// containing view. - pub fn new(element_bounds: Bounds, cx: &mut ViewContext) -> Self { + pub fn new(element_bounds: Bounds, view: View, cx: &mut WindowContext) -> Self { ElementInputHandler { - view: cx.view().clone(), + view, element_bounds, cx: cx.to_async(), } diff --git a/crates/gpui2/src/interactive.rs b/crates/gpui2/src/interactive.rs index e988697ca70a9a3e27e33ece81ccc63b9e95d58f..3dfe0c3b436dda921b8fe10727fdcd548a114e3e 100644 --- a/crates/gpui2/src/interactive.rs +++ b/crates/gpui2/src/interactive.rs @@ -1,5 +1,6 @@ use crate::{ - div, point, Div, Element, FocusHandle, Keystroke, Modifiers, Pixels, Point, Render, ViewContext, + div, point, Div, Element, FocusHandle, Keystroke, Modifiers, Pixels, Point, Render, RenderOnce, + ViewContext, }; use smallvec::SmallVec; use std::{any::Any, fmt::Debug, marker::PhantomData, ops::Deref, path::PathBuf}; @@ -63,24 +64,24 @@ pub struct Drag where R: Fn(&mut V, &mut ViewContext) -> E, V: 'static, - E: Element<()>, + E: RenderOnce, { pub state: S, pub render_drag_handle: R, - view_type: PhantomData, + view_element_types: PhantomData<(V, E)>, } impl Drag where R: Fn(&mut V, &mut ViewContext) -> E, V: 'static, - E: Element<()>, + E: Element, { pub fn new(state: S, render_drag_handle: R) -> Self { Drag { state, render_drag_handle, - view_type: PhantomData, + view_element_types: Default::default(), } } } @@ -192,8 +193,8 @@ impl Deref for MouseExitEvent { #[derive(Debug, Clone, Default)] pub struct ExternalPaths(pub(crate) SmallVec<[PathBuf; 2]>); -impl Render for ExternalPaths { - type Element = Div; +impl Render for ExternalPaths { + type Element = Div; fn render(&mut self, _: &mut ViewContext) -> Self::Element { div() // Intentionally left empty because the platform will render icons for the dragged files @@ -286,7 +287,7 @@ pub struct FocusEvent { mod test { use crate::{ self as gpui, div, Div, FocusHandle, InteractiveElement, KeyBinding, Keystroke, - ParentElement, Render, Stateful, TestAppContext, VisualContext, + ParentElement, Render, RenderOnce, Stateful, TestAppContext, VisualContext, }; struct TestView { @@ -297,16 +298,25 @@ mod test { actions!(TestAction); - impl Render for TestView { - type Element = Stateful>; + impl Render for TestView { + type Element = Stateful
; - fn render(&mut self, _: &mut gpui::ViewContext) -> Self::Element { + fn render(&mut self, cx: &mut gpui::ViewContext) -> Self::Element { div().id("testview").child( div() .key_context("parent") - .on_key_down(|this: &mut TestView, _, _, _| this.saw_key_down = true) - .on_action(|this: &mut TestView, _: &TestAction, _| this.saw_action = true) - .child(div().key_context("nested").track_focus(&self.focus_handle)), + .on_key_down(cx.listener(|this, _, _| this.saw_key_down = true)) + .on_action( + cx.listener(|this: &mut TestView, _: &TestAction, _| { + this.saw_action = true + }), + ) + .child( + div() + .key_context("nested") + .track_focus(&self.focus_handle) + .render_once(), + ), ) } } diff --git a/crates/gpui2/src/style.rs b/crates/gpui2/src/style.rs index 1b0cabb40154575f60ead9d8404bfa7c0fb5ee34..c6f02f5bca85d7bfc2104d372b2c664cbeb30094 100644 --- a/crates/gpui2/src/style.rs +++ b/crates/gpui2/src/style.rs @@ -2,7 +2,7 @@ use crate::{ black, phi, point, rems, AbsoluteLength, BorrowAppContext, BorrowWindow, Bounds, ContentMask, Corners, CornersRefinement, CursorStyle, DefiniteLength, Edges, EdgesRefinement, Font, FontFeatures, FontStyle, FontWeight, Hsla, Length, Pixels, Point, PointRefinement, Rgba, - SharedString, Size, SizeRefinement, Styled, TextRun, ViewContext, + SharedString, Size, SizeRefinement, Styled, TextRun, WindowContext, }; use refineable::{Cascade, Refineable}; use smallvec::SmallVec; @@ -128,6 +128,13 @@ pub struct BoxShadow { pub spread_radius: Pixels, } +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] +pub enum WhiteSpace { + #[default] + Normal, + Nowrap, +} + #[derive(Refineable, Clone, Debug)] #[refineable(Debug)] pub struct TextStyle { @@ -139,6 +146,7 @@ pub struct TextStyle { pub font_weight: FontWeight, pub font_style: FontStyle, pub underline: Option, + pub white_space: WhiteSpace, } impl Default for TextStyle { @@ -152,6 +160,7 @@ impl Default for TextStyle { font_weight: FontWeight::default(), font_style: FontStyle::default(), underline: None, + white_space: WhiteSpace::Normal, } } } @@ -313,7 +322,7 @@ impl Style { } /// Paints the background of an element styled with this style. - pub fn paint(&self, bounds: Bounds, cx: &mut ViewContext) { + pub fn paint(&self, bounds: Bounds, cx: &mut WindowContext) { let rem_size = cx.rem_size(); cx.with_z_index(0, |cx| { diff --git a/crates/gpui2/src/styled.rs b/crates/gpui2/src/styled.rs index beaf664dd8a51fcd58eba73feb247992945f0217..bdb9d4b4fe9e98f82f1e3aae1291f392bacd7ec5 100644 --- a/crates/gpui2/src/styled.rs +++ b/crates/gpui2/src/styled.rs @@ -1,7 +1,7 @@ use crate::{ self as gpui, hsla, point, px, relative, rems, AbsoluteLength, AlignItems, CursorStyle, DefiniteLength, Display, Fill, FlexDirection, Hsla, JustifyContent, Length, Position, - SharedString, StyleRefinement, Visibility, + SharedString, StyleRefinement, Visibility, WhiteSpace, }; use crate::{BoxShadow, TextStyleRefinement}; use smallvec::{smallvec, SmallVec}; @@ -101,6 +101,24 @@ pub trait Styled: Sized { self } + /// Sets the whitespace of the element to `normal`. + /// [Docs](https://tailwindcss.com/docs/whitespace#normal) + fn whitespace_normal(mut self) -> Self { + self.text_style() + .get_or_insert_with(Default::default) + .white_space = Some(WhiteSpace::Normal); + self + } + + /// Sets the whitespace of the element to `nowrap`. + /// [Docs](https://tailwindcss.com/docs/whitespace#nowrap) + fn whitespace_nowrap(mut self) -> Self { + self.text_style() + .get_or_insert_with(Default::default) + .white_space = Some(WhiteSpace::Nowrap); + self + } + /// Sets the flex direction of the element to `column`. /// [Docs](https://tailwindcss.com/docs/flex-direction#column) fn flex_col(mut self) -> Self { diff --git a/crates/gpui2/src/view.rs b/crates/gpui2/src/view.rs index a1ed8373c5e18d8ba7c46bd172e8dff314957a91..efa40627ace6ae151c94ff3cc28c7072ede68450 100644 --- a/crates/gpui2/src/view.rs +++ b/crates/gpui2/src/view.rs @@ -6,7 +6,7 @@ use crate::{ }; use anyhow::{Context, Result}; use std::{ - any::{Any, TypeId}, + any::TypeId, hash::{Hash, Hasher}, }; @@ -59,15 +59,15 @@ impl View { self.model.read(cx) } - pub fn render_with(&self, component: E) -> RenderViewWith - where - E: 'static + Element, - { - RenderViewWith { - view: self.clone(), - element: Some(component), - } - } + // pub fn render_with(&self, component: E) -> RenderViewWith + // where + // E: 'static + Element, + // { + // RenderViewWith { + // view: self.clone(), + // element: Some(component), + // } + // } pub fn focus_handle(&self, cx: &AppContext) -> FocusHandle where @@ -77,6 +77,24 @@ impl View { } } +impl Element for View { + type State = Option; + + fn layout( + &mut self, + _state: Option, + cx: &mut WindowContext, + ) -> (LayoutId, Self::State) { + let mut element = self.update(cx, |view, cx| view.render(cx).into_any()); + let layout_id = element.layout(cx); + (layout_id, Some(element)) + } + + fn paint(self, _: Bounds, element: &mut Self::State, cx: &mut WindowContext) { + element.take().unwrap().paint(cx); + } +} + impl Clone for View { fn clone(&self) -> Self { Self { @@ -151,8 +169,8 @@ impl Eq for WeakView {} #[derive(Clone, Debug)] pub struct AnyView { model: AnyModel, - layout: fn(&AnyView, &mut WindowContext) -> (LayoutId, Box), - paint: fn(&AnyView, Box, &mut WindowContext), + layout: fn(&AnyView, &mut WindowContext) -> (LayoutId, AnyElement), + paint: fn(&AnyView, AnyElement, &mut WindowContext), } impl AnyView { @@ -199,7 +217,7 @@ impl AnyView { } } -impl> From> for AnyView { +impl From> for AnyView { fn from(value: View) -> Self { AnyView { model: value.model.into_any(), @@ -209,36 +227,24 @@ impl> From> for AnyView { } } -impl, ParentV: 'static> Element for View { - type State = Option>; +impl Element for AnyView { + type State = Option; fn layout( &mut self, - _parent_view: &mut ParentV, _state: Option, - cx: &mut ViewContext, + cx: &mut WindowContext, ) -> (LayoutId, Self::State) { - self.update(cx, |view, cx| { - let mut element = view.render(cx).into_any(); - let layout_id = element.layout(view, cx); - (layout_id, Some(element)) - }) + let (layout_id, state) = (self.layout)(self, cx); + (layout_id, Some(state)) } - fn paint( - self, - _: Bounds, - _parent: &mut ParentV, - element: &mut Self::State, - cx: &mut ViewContext, - ) { - self.update(cx, |view, cx| { - element.take().unwrap().paint(view, cx); - }); + fn paint(self, _: Bounds, state: &mut Self::State, cx: &mut WindowContext) { + (self.paint)(&self, state.take().unwrap(), cx) } } -impl, ParentV: 'static> RenderOnce for View { +impl RenderOnce for View { type Element = View; fn element_id(&self) -> Option { @@ -250,31 +256,7 @@ impl, ParentV: 'static> RenderOnce for View { } } -impl Element for AnyView { - type State = Option>; - - fn layout( - &mut self, - _view_state: &mut V, - _element_state: Option, - cx: &mut ViewContext, - ) -> (LayoutId, Self::State) { - let (layout_id, rendered_element) = (self.layout)(self, cx); - (layout_id, Some(rendered_element)) - } - - fn paint( - mut self, - _bounds: Bounds, - _view_state: &mut V, - rendered_element: &mut Self::State, - cx: &mut ViewContext, - ) { - (self.paint)(&mut self, rendered_element.take().unwrap(), cx) - } -} - -impl RenderOnce for AnyView { +impl RenderOnce for AnyView { type Element = Self; fn element_id(&self) -> Option { @@ -288,8 +270,8 @@ impl RenderOnce for AnyView { pub struct AnyWeakView { model: AnyWeakModel, - layout: fn(&AnyView, &mut WindowContext) -> (LayoutId, Box), - paint: fn(&AnyView, Box, &mut WindowContext), + layout: fn(&AnyView, &mut WindowContext) -> (LayoutId, AnyElement), + paint: fn(&AnyView, AnyElement, &mut WindowContext), } impl AnyWeakView { @@ -303,7 +285,7 @@ impl AnyWeakView { } } -impl> From> for AnyWeakView { +impl From> for AnyWeakView { fn from(view: WeakView) -> Self { Self { model: view.model.into(), @@ -313,88 +295,40 @@ impl> From> for AnyWeakView { } } -pub struct RenderViewWith { - view: View, - element: Option, -} - -impl Element for RenderViewWith +impl Render for T where - E: 'static + Element, - ParentV: 'static, - V: 'static, + T: 'static + FnMut(&mut WindowContext) -> E, + E: 'static + Send + Element, { - type State = Option>; + type Element = E; - fn layout( - &mut self, - _: &mut ParentV, - _: Option, - cx: &mut ViewContext, - ) -> (LayoutId, Self::State) { - self.view.update(cx, |view, cx| { - let mut element = self.element.take().unwrap().into_any(); - let layout_id = element.layout(view, cx); - (layout_id, Some(element)) - }) - } - - fn paint( - self, - _: Bounds, - _: &mut ParentV, - element: &mut Self::State, - cx: &mut ViewContext, - ) { - self.view - .update(cx, |view, cx| element.take().unwrap().paint(view, cx)) - } -} - -impl RenderOnce for RenderViewWith -where - E: 'static + Element, - V: 'static, - ParentV: 'static, -{ - type Element = Self; - - fn element_id(&self) -> Option { - self.element.as_ref().unwrap().element_id() - } - - fn render_once(self) -> Self::Element { - self + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + (self)(cx) } } mod any_view { - use crate::{AnyElement, AnyView, BorrowWindow, LayoutId, Render, WindowContext}; - use std::any::Any; + use crate::{AnyElement, AnyView, BorrowWindow, Element, LayoutId, Render, WindowContext}; - pub(crate) fn layout>( + pub(crate) fn layout( view: &AnyView, cx: &mut WindowContext, - ) -> (LayoutId, Box) { + ) -> (LayoutId, AnyElement) { cx.with_element_id(Some(view.model.entity_id), |cx| { let view = view.clone().downcast::().unwrap(); - view.update(cx, |view, cx| { - let mut element = AnyElement::new(view.render(cx)); - let layout_id = element.layout(view, cx); - (layout_id, Box::new(element) as Box) - }) + let mut element = view.update(cx, |view, cx| view.render(cx).into_any()); + let layout_id = element.layout(cx); + (layout_id, element) }) } - pub(crate) fn paint>( + pub(crate) fn paint( view: &AnyView, - element: Box, + element: AnyElement, cx: &mut WindowContext, ) { cx.with_element_id(Some(view.model.entity_id), |cx| { - let view = view.clone().downcast::().unwrap(); - let element = element.downcast::>().unwrap(); - view.update(cx, |view, cx| element.paint(view, cx)) + element.paint(cx); }) } } diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index c17e7e06f4cf103f7ec23e621ea6f47a57885b9c..483a8fdbee41515f635c2bd3a85baaaa26c3afd8 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -187,7 +187,7 @@ impl Drop for FocusHandle { /// FocusableView allows users of your view to easily /// focus it (using cx.focus_view(view)) -pub trait FocusableView: 'static + Render { +pub trait FocusableView: 'static + Render { fn focus_handle(&self, cx: &AppContext) -> FocusHandle; } @@ -1436,6 +1436,82 @@ impl<'a> WindowContext<'a> { .dispatch_tree .bindings_for_action(action) } + + pub fn listener_for( + &self, + view: &View, + f: impl Fn(&mut V, &E, &mut ViewContext) + 'static, + ) -> impl Fn(&E, &mut WindowContext) + 'static { + let view = view.downgrade(); + move |e: &E, cx: &mut WindowContext| { + view.update(cx, |view, cx| f(view, e, cx)).ok(); + } + } + + pub fn constructor_for( + &self, + view: &View, + f: impl Fn(&mut V, &mut ViewContext) -> R + 'static, + ) -> impl Fn(&mut WindowContext) -> R + 'static { + let view = view.clone(); + move |cx: &mut WindowContext| view.update(cx, |view, cx| f(view, cx)) + } + + //========== ELEMENT RELATED FUNCTIONS =========== + pub fn with_key_dispatch( + &mut self, + context: KeyContext, + focus_handle: Option, + f: impl FnOnce(Option, &mut Self) -> R, + ) -> R { + let window = &mut self.window; + window + .current_frame + .dispatch_tree + .push_node(context.clone()); + if let Some(focus_handle) = focus_handle.as_ref() { + window + .current_frame + .dispatch_tree + .make_focusable(focus_handle.id); + } + let result = f(focus_handle, self); + + self.window.current_frame.dispatch_tree.pop_node(); + + result + } + + /// Register a focus listener for the current frame only. It will be cleared + /// on the next frame render. You should use this method only from within elements, + /// and we may want to enforce that better via a different context type. + // todo!() Move this to `FrameContext` to emphasize its individuality? + pub fn on_focus_changed( + &mut self, + listener: impl Fn(&FocusEvent, &mut WindowContext) + 'static, + ) { + self.window + .current_frame + .focus_listeners + .push(Box::new(move |event, cx| { + listener(event, cx); + })); + } + + /// Set an input handler, such as [ElementInputHandler], which interfaces with the + /// platform to receive textual input with proper integration with concerns such + /// as IME interactions. + pub fn handle_input( + &mut self, + focus_handle: &FocusHandle, + input_handler: impl PlatformInputHandler, + ) { + if focus_handle.is_focused(self) { + self.window + .platform_window + .set_input_handler(Box::new(input_handler)); + } + } } impl Context for WindowContext<'_> { @@ -1520,7 +1596,7 @@ impl VisualContext for WindowContext<'_> { build_view_state: impl FnOnce(&mut ViewContext<'_, V>) -> V, ) -> Self::Result> where - V: 'static + Render, + V: 'static + Render, { let slot = self.app.entities.reserve(); let view = View { @@ -1559,7 +1635,7 @@ impl VisualContext for WindowContext<'_> { build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V, ) -> Self::Result> where - V: 'static + Render, + V: 'static + Render, { let slot = self.app.entities.reserve(); let view = View { @@ -1617,6 +1693,10 @@ pub trait BorrowWindow: BorrowMut + BorrowMut { self.borrow_mut() } + fn app(&self) -> &AppContext { + self.borrow() + } + fn window(&self) -> &Window { self.borrow() } @@ -2124,49 +2204,6 @@ impl<'a, V: 'static> ViewContext<'a, V> { ) } - /// Register a focus listener for the current frame only. It will be cleared - /// on the next frame render. You should use this method only from within elements, - /// and we may want to enforce that better via a different context type. - // todo!() Move this to `FrameContext` to emphasize its individuality? - pub fn on_focus_changed( - &mut self, - listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext) + 'static, - ) { - let handle = self.view().downgrade(); - self.window - .current_frame - .focus_listeners - .push(Box::new(move |event, cx| { - handle - .update(cx, |view, cx| listener(view, event, cx)) - .log_err(); - })); - } - - pub fn with_key_dispatch( - &mut self, - context: KeyContext, - focus_handle: Option, - f: impl FnOnce(Option, &mut Self) -> R, - ) -> R { - let window = &mut self.window; - window - .current_frame - .dispatch_tree - .push_node(context.clone()); - if let Some(focus_handle) = focus_handle.as_ref() { - window - .current_frame - .dispatch_tree - .make_focusable(focus_handle.id); - } - let result = f(focus_handle, self); - - self.window.current_frame.dispatch_tree.pop_node(); - - result - } - pub fn spawn( &mut self, f: impl FnOnce(WeakView, AsyncWindowContext) -> Fut, @@ -2243,21 +2280,6 @@ impl<'a, V: 'static> ViewContext<'a, V> { }); } - /// Set an input handler, such as [ElementInputHandler], which interfaces with the - /// platform to receive textual input with proper integration with concerns such - /// as IME interactions. - pub fn handle_input( - &mut self, - focus_handle: &FocusHandle, - input_handler: impl PlatformInputHandler, - ) { - if focus_handle.is_focused(self) { - self.window - .platform_window - .set_input_handler(Box::new(input_handler)); - } - } - pub fn emit(&mut self, event: Evt) where Evt: 'static, @@ -2284,6 +2306,16 @@ impl<'a, V: 'static> ViewContext<'a, V> { { self.defer(|_, cx| cx.emit(Manager::Dismiss)) } + + pub fn listener( + &self, + f: impl Fn(&mut V, &E, &mut ViewContext) + 'static, + ) -> impl Fn(&E, &mut WindowContext) + 'static { + let view = self.view().downgrade(); + move |e: &E, cx: &mut WindowContext| { + view.update(cx, |view, cx| f(view, e, cx)).ok(); + } + } } impl Context for ViewContext<'_, V> { @@ -2335,7 +2367,7 @@ impl Context for ViewContext<'_, V> { } impl VisualContext for ViewContext<'_, V> { - fn build_view + 'static>( + fn build_view( &mut self, build_view_state: impl FnOnce(&mut ViewContext<'_, W>) -> W, ) -> Self::Result> { @@ -2355,7 +2387,7 @@ impl VisualContext for ViewContext<'_, V> { build_view: impl FnOnce(&mut ViewContext<'_, W>) -> W, ) -> Self::Result> where - W: 'static + Render, + W: 'static + Render, { self.window_cx.replace_root_view(build_view) } @@ -2400,7 +2432,7 @@ pub struct WindowHandle { state_type: PhantomData, } -impl> WindowHandle { +impl WindowHandle { pub fn new(id: WindowId) -> Self { WindowHandle { any_handle: AnyWindowHandle { diff --git a/crates/gpui2_macros/src/derive_render_once.rs b/crates/gpui2_macros/src/derive_render_once.rs index 732f2df21f10ebf409935ed04ce17ea5553d9321..efe6aab0bb9c43776a4f9da26d084c2577ca367f 100644 --- a/crates/gpui2_macros/src/derive_render_once.rs +++ b/crates/gpui2_macros/src/derive_render_once.rs @@ -1,37 +1,17 @@ use proc_macro::TokenStream; use quote::quote; -use syn::{parse_macro_input, parse_quote, DeriveInput}; +use syn::{parse_macro_input, DeriveInput}; pub fn derive_render_once(input: TokenStream) -> TokenStream { let ast = parse_macro_input!(input as DeriveInput); let type_name = &ast.ident; - - let mut trait_generics = ast.generics.clone(); - let view_type = if let Some(view_type) = specified_view_type(&ast) { - quote! { #view_type } - } else { - if let Some(first_type_param) = ast.generics.params.iter().find_map(|param| { - if let syn::GenericParam::Type(type_param) = param { - Some(type_param.ident.clone()) - } else { - None - } - }) { - quote! { #first_type_param } - } else { - trait_generics.params.push(parse_quote! { V: 'static }); - quote! { V } - } - }; - - let (impl_generics, _, where_clause) = trait_generics.split_for_impl(); - let (_, type_generics, _) = ast.generics.split_for_impl(); + let (impl_generics, type_generics, where_clause) = ast.generics.split_for_impl(); let gen = quote! { - impl #impl_generics gpui::RenderOnce<#view_type> for #type_name #type_generics + impl #impl_generics gpui::RenderOnce for #type_name #type_generics #where_clause { - type Element = gpui::CompositeElement<#view_type, Self>; + type Element = gpui::CompositeElement; fn element_id(&self) -> Option { None @@ -45,20 +25,3 @@ pub fn derive_render_once(input: TokenStream) -> TokenStream { gen.into() } - -fn specified_view_type(ast: &DeriveInput) -> Option { - ast.attrs.iter().find_map(|attr| { - if attr.path.is_ident("view") { - if let Ok(syn::Meta::NameValue(meta_name_value)) = attr.parse_meta() { - if let syn::Lit::Str(lit_str) = meta_name_value.lit { - return Some( - lit_str - .parse::() - .expect("Failed to parse view_type"), - ); - } - } - } - None - }) -} diff --git a/crates/language2/src/buffer.rs b/crates/language2/src/buffer.rs index 51ed192b993cdc6442a06056a7c502ead35ba977..26ee93adefa7b53d5734e498e24ecf22a4dd0d15 100644 --- a/crates/language2/src/buffer.rs +++ b/crates/language2/src/buffer.rs @@ -7,6 +7,7 @@ pub use crate::{ use crate::{ diagnostic_set::{DiagnosticEntry, DiagnosticGroup}, language_settings::{language_settings, LanguageSettings}, + markdown::parse_markdown, outline::OutlineItem, syntax_map::{ SyntaxLayerInfo, SyntaxMap, SyntaxMapCapture, SyntaxMapCaptures, SyntaxMapMatches, @@ -155,12 +156,52 @@ pub struct Diagnostic { pub is_unnecessary: bool, } +pub async fn prepare_completion_documentation( + documentation: &lsp::Documentation, + language_registry: &Arc, + language: Option>, +) -> Documentation { + match documentation { + lsp::Documentation::String(text) => { + if text.lines().count() <= 1 { + Documentation::SingleLine(text.clone()) + } else { + Documentation::MultiLinePlainText(text.clone()) + } + } + + lsp::Documentation::MarkupContent(lsp::MarkupContent { kind, value }) => match kind { + lsp::MarkupKind::PlainText => { + if value.lines().count() <= 1 { + Documentation::SingleLine(value.clone()) + } else { + Documentation::MultiLinePlainText(value.clone()) + } + } + + lsp::MarkupKind::Markdown => { + let parsed = parse_markdown(value, language_registry, language).await; + Documentation::MultiLineMarkdown(parsed) + } + }, + } +} + +#[derive(Clone, Debug)] +pub enum Documentation { + Undocumented, + SingleLine(String), + MultiLinePlainText(String), + MultiLineMarkdown(ParsedMarkdown), +} + #[derive(Clone, Debug)] pub struct Completion { pub old_range: Range, pub new_text: String, pub label: CodeLabel, pub server_id: LanguageServerId, + pub documentation: Option, pub lsp_completion: lsp::CompletionItem, } diff --git a/crates/language2/src/proto.rs b/crates/language2/src/proto.rs index c4abe39d4782aafbe90594e3a0bc5de70787fa03..957f4ee7fbc4da251698fd640d33d37d5cf4a06b 100644 --- a/crates/language2/src/proto.rs +++ b/crates/language2/src/proto.rs @@ -482,6 +482,7 @@ pub async fn deserialize_completion( lsp_completion.filter_text.as_deref(), ) }), + documentation: None, server_id: LanguageServerId(completion.server_id as usize), lsp_completion, }) diff --git a/crates/picker2/src/picker2.rs b/crates/picker2/src/picker2.rs index f29312234c42f69286ffd120b07dac63b5031aff..e58b7782efe49dda76717b7570f0b8daae3f020d 100644 --- a/crates/picker2/src/picker2.rs +++ b/crates/picker2/src/picker2.rs @@ -1,10 +1,10 @@ use editor::Editor; use gpui::{ div, prelude::*, uniform_list, AppContext, Div, FocusHandle, FocusableView, MouseButton, - Render, Task, UniformListScrollHandle, View, ViewContext, WindowContext, + MouseDownEvent, Render, Task, UniformListScrollHandle, View, ViewContext, WindowContext, }; use std::{cmp, sync::Arc}; -use ui::{prelude::*, v_stack, Divider, Label, TextColor}; +use ui::{prelude::*, v_stack, Color, Divider, Label}; pub struct Picker { pub delegate: D, @@ -15,7 +15,7 @@ pub struct Picker { } pub trait PickerDelegate: Sized + 'static { - type ListItem: RenderOnce>; + type ListItem: RenderOnce; fn match_count(&self) -> usize; fn selected_index(&self) -> usize; @@ -180,21 +180,21 @@ impl Picker { } } -impl Render for Picker { - type Element = Div; +impl Render for Picker { + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { div() .key_context("picker") .size_full() .elevation_2(cx) - .on_action(Self::select_next) - .on_action(Self::select_prev) - .on_action(Self::select_first) - .on_action(Self::select_last) - .on_action(Self::cancel) - .on_action(Self::confirm) - .on_action(Self::secondary_confirm) + .on_action(cx.listener(Self::select_next)) + .on_action(cx.listener(Self::select_prev)) + .on_action(cx.listener(Self::select_first)) + .on_action(cx.listener(Self::select_last)) + .on_action(cx.listener(Self::cancel)) + .on_action(cx.listener(Self::confirm)) + .on_action(cx.listener(Self::secondary_confirm)) .child( v_stack() .py_0p5() @@ -208,31 +208,37 @@ impl Render for Picker { .p_1() .grow() .child( - uniform_list("candidates", self.delegate.match_count(), { - move |this: &mut Self, visible_range, cx| { - let selected_ix = this.delegate.selected_index(); - visible_range - .map(|ix| { - div() - .on_mouse_down( - MouseButton::Left, - move |this: &mut Self, event, cx| { - this.handle_click( - ix, - event.modifiers.command, - cx, - ) - }, - ) - .child(this.delegate.render_match( - ix, - ix == selected_ix, - cx, - )) - }) - .collect() - } - }) + uniform_list( + cx.view().clone(), + "candidates", + self.delegate.match_count(), + { + let selected_index = self.delegate.selected_index(); + + move |picker, visible_range, cx| { + visible_range + .map(|ix| { + div() + .on_mouse_down( + MouseButton::Left, + cx.listener(move |this, event: &MouseDownEvent, cx| { + this.handle_click( + ix, + event.modifiers.command, + cx, + ) + }), + ) + .child(picker.delegate.render_match( + ix, + ix == selected_index, + cx, + )) + }) + .collect() + } + }, + ) .track_scroll(self.scroll_handle.clone()), ) .max_h_72() @@ -244,7 +250,7 @@ impl Render for Picker { v_stack().p_1().grow().child( div() .px_1() - .child(Label::new("No matches").color(TextColor::Muted)), + .child(Label::new("No matches").color(Color::Muted)), ), ) }) diff --git a/crates/project2/src/lsp_command.rs b/crates/project2/src/lsp_command.rs index cc1821d3fff744b557568f9e11d1fd1b98c4acdb..94c277db1e0277c56493837d45e37a9c0b38e4c0 100644 --- a/crates/project2/src/lsp_command.rs +++ b/crates/project2/src/lsp_command.rs @@ -10,7 +10,7 @@ use futures::future; use gpui::{AppContext, AsyncAppContext, Model}; use language::{ language_settings::{language_settings, InlayHintKind}, - point_from_lsp, point_to_lsp, + point_from_lsp, point_to_lsp, prepare_completion_documentation, proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version}, range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CharKind, CodeAction, Completion, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Transaction, @@ -1339,7 +1339,7 @@ impl LspCommand for GetCompletions { async fn response_from_lsp( self, completions: Option, - _: Model, + project: Model, buffer: Model, server_id: LanguageServerId, mut cx: AsyncAppContext, @@ -1359,7 +1359,8 @@ impl LspCommand for GetCompletions { Default::default() }; - let completions = buffer.update(&mut cx, |buffer, _| { + let completions = buffer.update(&mut cx, |buffer, cx| { + let language_registry = project.read(cx).languages().clone(); let language = buffer.language().cloned(); let snapshot = buffer.snapshot(); let clipped_position = buffer.clip_point_utf16(Unclipped(self.position), Bias::Left); @@ -1443,14 +1444,29 @@ impl LspCommand for GetCompletions { } }; + let language_registry = language_registry.clone(); let language = language.clone(); LineEnding::normalize(&mut new_text); Some(async move { let mut label = None; - if let Some(language) = language { + if let Some(language) = language.as_ref() { language.process_completion(&mut lsp_completion).await; label = language.label_for_completion(&lsp_completion).await; } + + let documentation = if let Some(lsp_docs) = &lsp_completion.documentation { + Some( + prepare_completion_documentation( + lsp_docs, + &language_registry, + language.clone(), + ) + .await, + ) + } else { + None + }; + Completion { old_range, new_text, @@ -1460,6 +1476,7 @@ impl LspCommand for GetCompletions { lsp_completion.filter_text.as_deref(), ) }), + documentation, server_id, lsp_completion, } diff --git a/crates/project_panel2/src/project_panel.rs b/crates/project_panel2/src/project_panel.rs index da3ada4c10936a626f143f0392544724787b7217..0550fc7bd239df14895b571bd41f027bd342cb9a 100644 --- a/crates/project_panel2/src/project_panel.rs +++ b/crates/project_panel2/src/project_panel.rs @@ -10,9 +10,9 @@ use anyhow::{anyhow, Result}; use gpui::{ actions, div, px, uniform_list, Action, AppContext, AssetSource, AsyncWindowContext, ClipboardItem, Div, EventEmitter, FocusHandle, Focusable, FocusableView, InteractiveElement, - Model, MouseButton, ParentElement, Pixels, Point, PromptLevel, Render, RenderOnce, Stateful, - StatefulInteractiveElement, Styled, Task, UniformListScrollHandle, View, ViewContext, - VisualContext as _, WeakView, WindowContext, + Model, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Render, + RenderOnce, Stateful, StatefulInteractiveElement, Styled, Task, UniformListScrollHandle, View, + ViewContext, VisualContext as _, WeakView, WindowContext, }; use menu::{Confirm, SelectNext, SelectPrev}; use project::{ @@ -371,7 +371,7 @@ impl ProjectPanel { _entry_id: ProjectEntryId, _cx: &mut ViewContext, ) { - todo!() + // todo!() // let project = self.project.read(cx); // let worktree_id = if let Some(id) = project.worktree_id_for_entry(entry_id, cx) { @@ -1339,7 +1339,7 @@ impl ProjectPanel { editor: Option<&View>, padding: Pixels, cx: &mut ViewContext, - ) -> Div { + ) -> Div { let show_editor = details.is_editing && !details.is_processing; let theme = cx.theme(); @@ -1378,7 +1378,7 @@ impl ProjectPanel { details: EntryDetails, // dragged_entry_destination: &mut Option>, cx: &mut ViewContext, - ) -> Stateful> { + ) -> Stateful
{ let kind = details.kind; let settings = ProjectPanelSettings::get_global(cx); const INDENT_SIZE: Pixels = px(16.0); @@ -1396,7 +1396,7 @@ impl ProjectPanel { this.bg(cx.theme().colors().element_selected) }) .hover(|style| style.bg(cx.theme().colors().element_hover)) - .on_click(move |this, event, cx| { + .on_click(cx.listener(move |this, event: &gpui::ClickEvent, cx| { if !show_editor { if kind.is_dir() { this.toggle_expanded(entry_id, cx); @@ -1408,10 +1408,13 @@ impl ProjectPanel { } } } - }) - .on_mouse_down(MouseButton::Right, move |this, event, cx| { - this.deploy_context_menu(event.position, entry_id, cx); - }) + })) + .on_mouse_down( + MouseButton::Right, + cx.listener(move |this, event: &MouseDownEvent, cx| { + this.deploy_context_menu(event.position, entry_id, cx); + }), + ) // .on_drop::(|this, event, cx| { // this.move_entry( // *dragged_entry, @@ -1423,10 +1426,10 @@ impl ProjectPanel { } } -impl Render for ProjectPanel { - type Element = Focusable>>; +impl Render for ProjectPanel { + type Element = Focusable>; - fn render(&mut self, _cx: &mut gpui::ViewContext) -> Self::Element { + fn render(&mut self, cx: &mut gpui::ViewContext) -> Self::Element { let has_worktree = self.visible_entries.len() != 0; if has_worktree { @@ -1434,40 +1437,43 @@ impl Render for ProjectPanel { .id("project-panel") .size_full() .key_context("ProjectPanel") - .on_action(Self::select_next) - .on_action(Self::select_prev) - .on_action(Self::expand_selected_entry) - .on_action(Self::collapse_selected_entry) - .on_action(Self::collapse_all_entries) - .on_action(Self::new_file) - .on_action(Self::new_directory) - .on_action(Self::rename) - .on_action(Self::delete) - .on_action(Self::confirm) - .on_action(Self::open_file) - .on_action(Self::cancel) - .on_action(Self::cut) - .on_action(Self::copy) - .on_action(Self::copy_path) - .on_action(Self::copy_relative_path) - .on_action(Self::paste) - .on_action(Self::reveal_in_finder) - .on_action(Self::open_in_terminal) - .on_action(Self::new_search_in_directory) + .on_action(cx.listener(Self::select_next)) + .on_action(cx.listener(Self::select_prev)) + .on_action(cx.listener(Self::expand_selected_entry)) + .on_action(cx.listener(Self::collapse_selected_entry)) + .on_action(cx.listener(Self::collapse_all_entries)) + .on_action(cx.listener(Self::new_file)) + .on_action(cx.listener(Self::new_directory)) + .on_action(cx.listener(Self::rename)) + .on_action(cx.listener(Self::delete)) + .on_action(cx.listener(Self::confirm)) + .on_action(cx.listener(Self::open_file)) + .on_action(cx.listener(Self::cancel)) + .on_action(cx.listener(Self::cut)) + .on_action(cx.listener(Self::copy)) + .on_action(cx.listener(Self::copy_path)) + .on_action(cx.listener(Self::copy_relative_path)) + .on_action(cx.listener(Self::paste)) + .on_action(cx.listener(Self::reveal_in_finder)) + .on_action(cx.listener(Self::open_in_terminal)) + .on_action(cx.listener(Self::new_search_in_directory)) .track_focus(&self.focus_handle) .child( uniform_list( + cx.view().clone(), "entries", self.visible_entries .iter() .map(|(_, worktree_entries)| worktree_entries.len()) .sum(), - |this: &mut Self, range, cx| { - let mut items = Vec::new(); - this.for_each_visible_entry(range, cx, |id, details, cx| { - items.push(this.render_entry(id, details, cx)); - }); - items + { + |this, range, cx| { + let mut items = Vec::new(); + this.for_each_visible_entry(range, cx, |id, details, cx| { + items.push(this.render_entry(id, details, cx)); + }); + items + } }, ) .size_full() diff --git a/crates/rich_text2/src/rich_text.rs b/crates/rich_text2/src/rich_text.rs index 48b530b7c578819c424f42f52459fa9498cb1603..4f64654bd3cdb759901afc20d20a805ad21a2d02 100644 --- a/crates/rich_text2/src/rich_text.rs +++ b/crates/rich_text2/src/rich_text.rs @@ -56,12 +56,12 @@ pub struct Mention { } impl RichText { - pub fn element( + pub fn element( &self, // syntax: Arc, // style: RichTextStyle, // cx: &mut ViewContext, - ) -> AnyElement { + ) -> AnyElement { todo!(); // let mut region_id = 0; diff --git a/crates/search2/Cargo.toml b/crates/search2/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..97cfdd6494099eb802a2fd629df1e74edada4232 --- /dev/null +++ b/crates/search2/Cargo.toml @@ -0,0 +1,40 @@ +[package] +name = "search2" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/search.rs" +doctest = false + +[dependencies] +bitflags = "1" +collections = { path = "../collections" } +editor = { package = "editor2", path = "../editor2" } +gpui = { package = "gpui2", path = "../gpui2" } +language = { package = "language2", path = "../language2" } +menu = { package = "menu2", path = "../menu2" } +project = { package = "project2", path = "../project2" } +settings = { package = "settings2", path = "../settings2" } +theme = { package = "theme2", path = "../theme2" } +util = { path = "../util" } +ui = {package = "ui2", path = "../ui2"} +workspace = { package = "workspace2", path = "../workspace2" } +#semantic_index = { path = "../semantic_index" } +anyhow.workspace = true +futures.workspace = true +log.workspace = true +postage.workspace = true +serde.workspace = true +serde_derive.workspace = true +smallvec.workspace = true +smol.workspace = true +serde_json.workspace = true +[dev-dependencies] +client = { package = "client2", path = "../client2", features = ["test-support"] } +editor = { package = "editor2", path = "../editor2", features = ["test-support"] } +gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } + +workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] } +unindent.workspace = true diff --git a/crates/search2/src/buffer_search.rs b/crates/search2/src/buffer_search.rs new file mode 100644 index 0000000000000000000000000000000000000000..3674baf3569b64542bb3b7bb482a61fcef3f01bb --- /dev/null +++ b/crates/search2/src/buffer_search.rs @@ -0,0 +1,1704 @@ +use crate::{ + history::SearchHistory, + mode::{next_mode, SearchMode}, + search_bar::{render_nav_button, render_search_mode_button}, + ActivateRegexMode, ActivateTextMode, CycleMode, NextHistoryQuery, PreviousHistoryQuery, + ReplaceAll, ReplaceNext, SearchOptions, SelectAllMatches, SelectNextMatch, SelectPrevMatch, + ToggleCaseSensitive, ToggleReplace, ToggleWholeWord, +}; +use collections::HashMap; +use editor::Editor; +use futures::channel::oneshot; +use gpui::{ + actions, div, red, Action, AppContext, Div, EventEmitter, InteractiveElement as _, + ParentElement as _, Render, RenderOnce, Styled, Subscription, Task, View, ViewContext, + VisualContext as _, WindowContext, +}; +use project::search::SearchQuery; +use serde::Deserialize; +use std::{any::Any, sync::Arc}; + +use ui::{h_stack, ButtonGroup, Icon, IconButton, IconElement}; +use util::ResultExt; +use workspace::{ + item::ItemHandle, + searchable::{Direction, SearchEvent, SearchableItemHandle, WeakSearchableItemHandle}, + ToolbarItemLocation, ToolbarItemView, Workspace, +}; + +#[derive(PartialEq, Clone, Deserialize, Default, Action)] +pub struct Deploy { + pub focus: bool, +} + +actions!(Dismiss, FocusEditor); + +pub enum Event { + UpdateLocation, +} + +pub fn init(cx: &mut AppContext) { + cx.observe_new_views(|workspace: &mut Workspace, _| BufferSearchBar::register(workspace)) + .detach(); +} + +pub struct BufferSearchBar { + query_editor: View, + replacement_editor: View, + active_searchable_item: Option>, + active_match_index: Option, + active_searchable_item_subscription: Option, + active_search: Option>, + searchable_items_with_matches: + HashMap, Vec>>, + pending_search: Option>, + search_options: SearchOptions, + default_options: SearchOptions, + query_contains_error: bool, + dismissed: bool, + search_history: SearchHistory, + current_mode: SearchMode, + replace_enabled: bool, +} + +impl EventEmitter for BufferSearchBar {} +impl EventEmitter for BufferSearchBar {} +impl Render for BufferSearchBar { + type Element = Div; + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + // let query_container_style = if self.query_contains_error { + // theme.search.invalid_editor + // } else { + // theme.search.editor.input.container + // }; + if self.dismissed { + return div(); + } + let supported_options = self.supported_options(); + + let previous_query_keystrokes = cx + .bindings_for_action(&PreviousHistoryQuery {}) + .into_iter() + .next() + .map(|binding| { + binding + .keystrokes() + .iter() + .map(|k| k.to_string()) + .collect::>() + }); + let next_query_keystrokes = cx + .bindings_for_action(&NextHistoryQuery {}) + .into_iter() + .next() + .map(|binding| { + binding + .keystrokes() + .iter() + .map(|k| k.to_string()) + .collect::>() + }); + let new_placeholder_text = match (previous_query_keystrokes, next_query_keystrokes) { + (Some(previous_query_keystrokes), Some(next_query_keystrokes)) => { + format!( + "Search ({}/{} for previous/next query)", + previous_query_keystrokes.join(" "), + next_query_keystrokes.join(" ") + ) + } + (None, Some(next_query_keystrokes)) => { + format!( + "Search ({} for next query)", + next_query_keystrokes.join(" ") + ) + } + (Some(previous_query_keystrokes), None) => { + format!( + "Search ({} for previous query)", + previous_query_keystrokes.join(" ") + ) + } + (None, None) => String::new(), + }; + let new_placeholder_text = Arc::from(new_placeholder_text); + self.query_editor.update(cx, |editor, cx| { + editor.set_placeholder_text(new_placeholder_text, cx); + }); + self.replacement_editor.update(cx, |editor, cx| { + editor.set_placeholder_text("Replace with...", cx); + }); + + let search_button_for_mode = |mode| { + let is_active = self.current_mode == mode; + + render_search_mode_button( + mode, + is_active, + cx.listener(move |this, _, cx| { + this.activate_search_mode(mode, cx); + }), + ) + }; + let search_option_button = |option| { + let is_active = self.search_options.contains(option); + option.as_button(is_active) + }; + let match_count = self + .active_searchable_item + .as_ref() + .and_then(|searchable_item| { + if self.query(cx).is_empty() { + return None; + } + let matches = self + .searchable_items_with_matches + .get(&searchable_item.downgrade())?; + let message = if let Some(match_ix) = self.active_match_index { + format!("{}/{}", match_ix + 1, matches.len()) + } else { + "No matches".to_string() + }; + + Some(ui::Label::new(message)) + }); + let nav_button_for_direction = |icon, direction| { + render_nav_button( + icon, + self.active_match_index.is_some(), + cx.listener(move |this, _, cx| match direction { + Direction::Prev => this.select_prev_match(&Default::default(), cx), + Direction::Next => this.select_next_match(&Default::default(), cx), + }), + ) + }; + let should_show_replace_input = self.replace_enabled && supported_options.replacement; + let replace_all = should_show_replace_input + .then(|| super::render_replace_button(ReplaceAll, ui::Icon::ReplaceAll)); + let replace_next = should_show_replace_input + .then(|| super::render_replace_button(ReplaceNext, ui::Icon::Replace)); + let in_replace = self.replacement_editor.focus_handle(cx).is_focused(cx); + + h_stack() + .key_context("BufferSearchBar") + .when(in_replace, |this| { + this.key_context("in_replace") + .on_action(cx.listener(Self::replace_next)) + .on_action(cx.listener(Self::replace_all)) + }) + .on_action(cx.listener(Self::previous_history_query)) + .on_action(cx.listener(Self::next_history_query)) + .w_full() + .p_1() + .child( + div() + .flex() + .flex_1() + .border_1() + .border_color(red()) + .rounded_md() + .items_center() + .child(IconElement::new(Icon::MagnifyingGlass)) + .child(self.query_editor.clone()) + .children( + supported_options + .case + .then(|| search_option_button(SearchOptions::CASE_SENSITIVE)), + ) + .children( + supported_options + .word + .then(|| search_option_button(SearchOptions::WHOLE_WORD)), + ), + ) + .child( + h_stack() + .flex_none() + .child(ButtonGroup::new(vec![ + search_button_for_mode(SearchMode::Text), + search_button_for_mode(SearchMode::Regex), + ])) + .when(supported_options.replacement, |this| { + this.child(super::toggle_replace_button(self.replace_enabled)) + }), + ) + .child( + h_stack() + .gap_0p5() + .flex_1() + .when(self.replace_enabled, |this| { + this.child(self.replacement_editor.clone()) + .children(replace_next) + .children(replace_all) + }), + ) + .child( + h_stack() + .gap_0p5() + .flex_none() + .child(self.render_action_button()) + .children(match_count) + .child(nav_button_for_direction( + ui::Icon::ChevronLeft, + Direction::Prev, + )) + .child(nav_button_for_direction( + ui::Icon::ChevronRight, + Direction::Next, + )), + ) + } +} + +impl ToolbarItemView for BufferSearchBar { + fn set_active_pane_item( + &mut self, + item: Option<&dyn ItemHandle>, + cx: &mut ViewContext, + ) -> ToolbarItemLocation { + cx.notify(); + self.active_searchable_item_subscription.take(); + self.active_searchable_item.take(); + + self.pending_search.take(); + + if let Some(searchable_item_handle) = + item.and_then(|item| item.to_searchable_item_handle(cx)) + { + let this = cx.view().downgrade(); + + searchable_item_handle + .subscribe_to_search_events( + cx, + Box::new(move |search_event, cx| { + if let Some(this) = this.upgrade() { + this.update(cx, |this, cx| { + this.on_active_searchable_item_event(search_event, cx) + }); + } + }), + ) + .detach(); + + self.active_searchable_item = Some(searchable_item_handle); + let _ = self.update_matches(cx); + if !self.dismissed { + return ToolbarItemLocation::Secondary; + } + } + ToolbarItemLocation::Hidden + } + + fn row_count(&self, _: &WindowContext<'_>) -> usize { + 1 + } +} + +impl BufferSearchBar { + pub fn register(workspace: &mut Workspace) { + workspace.register_action(|workspace, a: &Deploy, cx| { + workspace.active_pane().update(cx, |this, cx| { + this.toolbar().update(cx, |this, cx| { + if let Some(search_bar) = this.item_of_type::() { + search_bar.update(cx, |this, cx| { + if this.is_dismissed() { + this.show(cx); + } else { + this.dismiss(&Dismiss, cx); + } + }); + return; + } + let view = cx.build_view(|cx| BufferSearchBar::new(cx)); + this.add_item(view.clone(), cx); + view.update(cx, |this, cx| this.deploy(a, cx)); + cx.notify(); + }) + }); + }); + fn register_action( + workspace: &mut Workspace, + update: fn(&mut BufferSearchBar, &A, &mut ViewContext), + ) { + workspace.register_action(move |workspace, action: &A, cx| { + workspace.active_pane().update(cx, move |this, cx| { + this.toolbar().update(cx, move |this, cx| { + if let Some(search_bar) = this.item_of_type::() { + search_bar.update(cx, move |this, cx| update(this, action, cx)); + cx.notify(); + } + }) + }); + }); + } + + register_action(workspace, |this, action: &ToggleCaseSensitive, cx| { + if this.supported_options().case { + this.toggle_case_sensitive(action, cx); + } + }); + register_action(workspace, |this, action: &ToggleWholeWord, cx| { + if this.supported_options().word { + this.toggle_whole_word(action, cx); + } + }); + register_action(workspace, |this, action: &ToggleReplace, cx| { + if this.supported_options().replacement { + this.toggle_replace(action, cx); + } + }); + register_action(workspace, |this, _: &ActivateRegexMode, cx| { + if this.supported_options().regex { + this.activate_search_mode(SearchMode::Regex, cx); + } + }); + register_action(workspace, |this, _: &ActivateTextMode, cx| { + this.activate_search_mode(SearchMode::Text, cx); + }); + register_action(workspace, |this, action: &CycleMode, cx| { + if this.supported_options().regex { + // If regex is not supported then search has just one mode (text) - in that case there's no point in supporting + // cycling. + this.cycle_mode(action, cx) + } + }); + register_action(workspace, |this, action: &SelectNextMatch, cx| { + this.select_next_match(action, cx); + }); + register_action(workspace, |this, action: &SelectPrevMatch, cx| { + this.select_prev_match(action, cx); + }); + register_action(workspace, |this, action: &SelectAllMatches, cx| { + this.select_all_matches(action, cx); + }); + register_action(workspace, |this, _: &editor::Cancel, cx| { + if !this.dismissed { + this.dismiss(&Dismiss, cx); + } + }); + } + pub fn new(cx: &mut ViewContext) -> Self { + let query_editor = cx.build_view(|cx| Editor::single_line(cx)); + cx.subscribe(&query_editor, Self::on_query_editor_event) + .detach(); + let replacement_editor = cx.build_view(|cx| Editor::single_line(cx)); + cx.subscribe(&replacement_editor, Self::on_query_editor_event) + .detach(); + Self { + query_editor, + replacement_editor, + active_searchable_item: None, + active_searchable_item_subscription: None, + active_match_index: None, + searchable_items_with_matches: Default::default(), + default_options: SearchOptions::NONE, + search_options: SearchOptions::NONE, + pending_search: None, + query_contains_error: false, + dismissed: true, + search_history: SearchHistory::default(), + current_mode: SearchMode::default(), + active_search: None, + replace_enabled: false, + } + } + + pub fn is_dismissed(&self) -> bool { + self.dismissed + } + + pub fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext) { + self.dismissed = true; + for searchable_item in self.searchable_items_with_matches.keys() { + if let Some(searchable_item) = + WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx) + { + searchable_item.clear_matches(cx); + } + } + if let Some(active_editor) = self.active_searchable_item.as_ref() { + let handle = active_editor.focus_handle(cx); + cx.focus(&handle); + } + cx.emit(Event::UpdateLocation); + cx.notify(); + } + + pub fn deploy(&mut self, deploy: &Deploy, cx: &mut ViewContext) -> bool { + if self.show(cx) { + self.search_suggested(cx); + if deploy.focus { + self.select_query(cx); + let handle = cx.focus_handle(); + cx.focus(&handle); + } + return true; + } + + false + } + + pub fn show(&mut self, cx: &mut ViewContext) -> bool { + if self.active_searchable_item.is_none() { + return false; + } + self.dismissed = false; + cx.notify(); + cx.emit(Event::UpdateLocation); + true + } + + fn supported_options(&self) -> workspace::searchable::SearchOptions { + self.active_searchable_item + .as_deref() + .map(SearchableItemHandle::supported_options) + .unwrap_or_default() + } + pub fn search_suggested(&mut self, cx: &mut ViewContext) { + let search = self + .query_suggestion(cx) + .map(|suggestion| self.search(&suggestion, Some(self.default_options), cx)); + + if let Some(search) = search { + cx.spawn(|this, mut cx| async move { + search.await?; + this.update(&mut cx, |this, cx| this.activate_current_match(cx)) + }) + .detach_and_log_err(cx); + } + } + + pub fn activate_current_match(&mut self, cx: &mut ViewContext) { + if let Some(match_ix) = self.active_match_index { + if let Some(active_searchable_item) = self.active_searchable_item.as_ref() { + if let Some(matches) = self + .searchable_items_with_matches + .get(&active_searchable_item.downgrade()) + { + active_searchable_item.activate_match(match_ix, matches, cx) + } + } + } + } + + pub fn select_query(&mut self, cx: &mut ViewContext) { + self.query_editor.update(cx, |query_editor, cx| { + query_editor.select_all(&Default::default(), cx); + }); + } + + pub fn query(&self, cx: &WindowContext) -> String { + self.query_editor.read(cx).text(cx) + } + pub fn replacement(&self, cx: &WindowContext) -> String { + self.replacement_editor.read(cx).text(cx) + } + pub fn query_suggestion(&mut self, cx: &mut ViewContext) -> Option { + self.active_searchable_item + .as_ref() + .map(|searchable_item| searchable_item.query_suggestion(cx)) + .filter(|suggestion| !suggestion.is_empty()) + } + + pub fn set_replacement(&mut self, replacement: Option<&str>, cx: &mut ViewContext) { + if replacement.is_none() { + self.replace_enabled = false; + return; + } + self.replace_enabled = true; + self.replacement_editor + .update(cx, |replacement_editor, cx| { + replacement_editor + .buffer() + .update(cx, |replacement_buffer, cx| { + let len = replacement_buffer.len(cx); + replacement_buffer.edit([(0..len, replacement.unwrap())], None, cx); + }); + }); + } + + pub fn search( + &mut self, + query: &str, + options: Option, + cx: &mut ViewContext, + ) -> oneshot::Receiver<()> { + let options = options.unwrap_or(self.default_options); + if query != self.query(cx) || self.search_options != options { + self.query_editor.update(cx, |query_editor, cx| { + query_editor.buffer().update(cx, |query_buffer, cx| { + let len = query_buffer.len(cx); + query_buffer.edit([(0..len, query)], None, cx); + }); + }); + self.search_options = options; + self.query_contains_error = false; + self.clear_matches(cx); + cx.notify(); + } + self.update_matches(cx) + } + + fn render_action_button(&self) -> impl RenderOnce { + // let tooltip_style = theme.tooltip.clone(); + + // let style = theme.search.action_button.clone(); + + IconButton::new(0, ui::Icon::SelectAll) + .on_click(|_, cx| cx.dispatch_action(Box::new(SelectAllMatches))) + } + + pub fn activate_search_mode(&mut self, mode: SearchMode, cx: &mut ViewContext) { + assert_ne!( + mode, + SearchMode::Semantic, + "Semantic search is not supported in buffer search" + ); + if mode == self.current_mode { + return; + } + self.current_mode = mode; + let _ = self.update_matches(cx); + cx.notify(); + } + + pub fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext) { + if let Some(active_editor) = self.active_searchable_item.as_ref() { + let handle = active_editor.focus_handle(cx); + cx.focus(&handle); + } + } + + fn toggle_search_option(&mut self, search_option: SearchOptions, cx: &mut ViewContext) { + self.search_options.toggle(search_option); + self.default_options = self.search_options; + let _ = self.update_matches(cx); + cx.notify(); + } + + pub fn set_search_options( + &mut self, + search_options: SearchOptions, + cx: &mut ViewContext, + ) { + self.search_options = search_options; + cx.notify(); + } + + fn select_next_match(&mut self, _: &SelectNextMatch, cx: &mut ViewContext) { + self.select_match(Direction::Next, 1, cx); + } + + fn select_prev_match(&mut self, _: &SelectPrevMatch, cx: &mut ViewContext) { + self.select_match(Direction::Prev, 1, cx); + } + + fn select_all_matches(&mut self, _: &SelectAllMatches, cx: &mut ViewContext) { + if !self.dismissed && self.active_match_index.is_some() { + if let Some(searchable_item) = self.active_searchable_item.as_ref() { + if let Some(matches) = self + .searchable_items_with_matches + .get(&searchable_item.downgrade()) + { + searchable_item.select_matches(matches, cx); + self.focus_editor(&FocusEditor, cx); + } + } + } + } + + pub fn select_match(&mut self, direction: Direction, count: usize, cx: &mut ViewContext) { + if let Some(index) = self.active_match_index { + if let Some(searchable_item) = self.active_searchable_item.as_ref() { + if let Some(matches) = self + .searchable_items_with_matches + .get(&searchable_item.downgrade()) + { + let new_match_index = searchable_item + .match_index_for_direction(matches, index, direction, count, cx); + + searchable_item.update_matches(matches, cx); + searchable_item.activate_match(new_match_index, matches, cx); + } + } + } + } + + pub fn select_last_match(&mut self, cx: &mut ViewContext) { + if let Some(searchable_item) = self.active_searchable_item.as_ref() { + if let Some(matches) = self + .searchable_items_with_matches + .get(&searchable_item.downgrade()) + { + if matches.len() == 0 { + return; + } + let new_match_index = matches.len() - 1; + searchable_item.update_matches(matches, cx); + searchable_item.activate_match(new_match_index, matches, cx); + } + } + } + + fn on_query_editor_event( + &mut self, + _: View, + event: &editor::EditorEvent, + cx: &mut ViewContext, + ) { + if let editor::EditorEvent::Edited { .. } = event { + self.query_contains_error = false; + self.clear_matches(cx); + let search = self.update_matches(cx); + cx.spawn(|this, mut cx| async move { + search.await?; + this.update(&mut cx, |this, cx| this.activate_current_match(cx)) + }) + .detach_and_log_err(cx); + } + } + + fn on_active_searchable_item_event(&mut self, event: &SearchEvent, cx: &mut ViewContext) { + match event { + SearchEvent::MatchesInvalidated => { + let _ = self.update_matches(cx); + } + SearchEvent::ActiveMatchChanged => self.update_match_index(cx), + } + } + + fn toggle_case_sensitive(&mut self, _: &ToggleCaseSensitive, cx: &mut ViewContext) { + self.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx) + } + fn toggle_whole_word(&mut self, _: &ToggleWholeWord, cx: &mut ViewContext) { + self.toggle_search_option(SearchOptions::WHOLE_WORD, cx) + } + fn clear_matches(&mut self, cx: &mut ViewContext) { + let mut active_item_matches = None; + for (searchable_item, matches) in self.searchable_items_with_matches.drain() { + if let Some(searchable_item) = + WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx) + { + if Some(&searchable_item) == self.active_searchable_item.as_ref() { + active_item_matches = Some((searchable_item.downgrade(), matches)); + } else { + searchable_item.clear_matches(cx); + } + } + } + + self.searchable_items_with_matches + .extend(active_item_matches); + } + + fn update_matches(&mut self, cx: &mut ViewContext) -> oneshot::Receiver<()> { + let (done_tx, done_rx) = oneshot::channel(); + let query = self.query(cx); + self.pending_search.take(); + + if let Some(active_searchable_item) = self.active_searchable_item.as_ref() { + if query.is_empty() { + self.active_match_index.take(); + active_searchable_item.clear_matches(cx); + let _ = done_tx.send(()); + cx.notify(); + } else { + let query: Arc<_> = if self.current_mode == SearchMode::Regex { + match SearchQuery::regex( + query, + self.search_options.contains(SearchOptions::WHOLE_WORD), + self.search_options.contains(SearchOptions::CASE_SENSITIVE), + false, + Vec::new(), + Vec::new(), + ) { + Ok(query) => query.with_replacement(self.replacement(cx)), + Err(_) => { + self.query_contains_error = true; + cx.notify(); + return done_rx; + } + } + } else { + match SearchQuery::text( + query, + self.search_options.contains(SearchOptions::WHOLE_WORD), + self.search_options.contains(SearchOptions::CASE_SENSITIVE), + false, + Vec::new(), + Vec::new(), + ) { + Ok(query) => query.with_replacement(self.replacement(cx)), + Err(_) => { + self.query_contains_error = true; + cx.notify(); + return done_rx; + } + } + } + .into(); + self.active_search = Some(query.clone()); + let query_text = query.as_str().to_string(); + + let matches = active_searchable_item.find_matches(query, cx); + + let active_searchable_item = active_searchable_item.downgrade(); + self.pending_search = Some(cx.spawn(|this, mut cx| async move { + let matches = matches.await; + + this.update(&mut cx, |this, cx| { + if let Some(active_searchable_item) = + WeakSearchableItemHandle::upgrade(active_searchable_item.as_ref(), cx) + { + this.searchable_items_with_matches + .insert(active_searchable_item.downgrade(), matches); + + this.update_match_index(cx); + this.search_history.add(query_text); + if !this.dismissed { + let matches = this + .searchable_items_with_matches + .get(&active_searchable_item.downgrade()) + .unwrap(); + active_searchable_item.update_matches(matches, cx); + let _ = done_tx.send(()); + } + cx.notify(); + } + }) + .log_err(); + })); + } + } + done_rx + } + + fn update_match_index(&mut self, cx: &mut ViewContext) { + let new_index = self + .active_searchable_item + .as_ref() + .and_then(|searchable_item| { + let matches = self + .searchable_items_with_matches + .get(&searchable_item.downgrade())?; + searchable_item.active_match_index(matches, cx) + }); + if new_index != self.active_match_index { + self.active_match_index = new_index; + cx.notify(); + } + } + + fn next_history_query(&mut self, _: &NextHistoryQuery, cx: &mut ViewContext) { + if let Some(new_query) = self.search_history.next().map(str::to_string) { + let _ = self.search(&new_query, Some(self.search_options), cx); + } else { + self.search_history.reset_selection(); + let _ = self.search("", Some(self.search_options), cx); + } + } + + fn previous_history_query(&mut self, _: &PreviousHistoryQuery, cx: &mut ViewContext) { + if self.query(cx).is_empty() { + if let Some(new_query) = self.search_history.current().map(str::to_string) { + let _ = self.search(&new_query, Some(self.search_options), cx); + return; + } + } + + if let Some(new_query) = self.search_history.previous().map(str::to_string) { + let _ = self.search(&new_query, Some(self.search_options), cx); + } + } + fn cycle_mode(&mut self, _: &CycleMode, cx: &mut ViewContext) { + self.activate_search_mode(next_mode(&self.current_mode, false), cx); + } + fn toggle_replace(&mut self, _: &ToggleReplace, cx: &mut ViewContext) { + if let Some(_) = &self.active_searchable_item { + self.replace_enabled = !self.replace_enabled; + if !self.replace_enabled { + let handle = self.query_editor.focus_handle(cx); + cx.focus(&handle); + } + cx.notify(); + } + } + fn replace_next(&mut self, _: &ReplaceNext, cx: &mut ViewContext) { + let mut should_propagate = true; + if !self.dismissed && self.active_search.is_some() { + if let Some(searchable_item) = self.active_searchable_item.as_ref() { + if let Some(query) = self.active_search.as_ref() { + if let Some(matches) = self + .searchable_items_with_matches + .get(&searchable_item.downgrade()) + { + if let Some(active_index) = self.active_match_index { + let query = query + .as_ref() + .clone() + .with_replacement(self.replacement(cx)); + searchable_item.replace(&matches[active_index], &query, cx); + self.select_next_match(&SelectNextMatch, cx); + } + should_propagate = false; + self.focus_editor(&FocusEditor, cx); + } + } + } + } + if !should_propagate { + cx.stop_propagation(); + } + } + pub fn replace_all(&mut self, _: &ReplaceAll, cx: &mut ViewContext) { + if !self.dismissed && self.active_search.is_some() { + if let Some(searchable_item) = self.active_searchable_item.as_ref() { + if let Some(query) = self.active_search.as_ref() { + if let Some(matches) = self + .searchable_items_with_matches + .get(&searchable_item.downgrade()) + { + let query = query + .as_ref() + .clone() + .with_replacement(self.replacement(cx)); + for m in matches { + searchable_item.replace(m, &query, cx); + } + } + } + } + } + } +} + +#[cfg(test)] +mod tests { + use std::ops::Range; + + use super::*; + use editor::{DisplayPoint, Editor}; + use gpui::{Context, EmptyView, Hsla, TestAppContext, VisualTestContext}; + use language::Buffer; + use smol::stream::StreamExt as _; + use unindent::Unindent as _; + + fn init_globals(cx: &mut TestAppContext) { + cx.update(|cx| { + let store = settings::SettingsStore::test(cx); + cx.set_global(store); + editor::init(cx); + + language::init(cx); + theme::init(theme::LoadThemes::JustBase, cx); + }); + } + fn init_test( + cx: &mut TestAppContext, + ) -> ( + View, + View, + &mut VisualTestContext<'_>, + ) { + init_globals(cx); + let buffer = cx.build_model(|cx| { + Buffer::new( + 0, + cx.entity_id().as_u64(), + r#" + A regular expression (shortened as regex or regexp;[1] also referred to as + rational expression[2][3]) is a sequence of characters that specifies a search + pattern in text. Usually such patterns are used by string-searching algorithms + for "find" or "find and replace" operations on strings, or for input validation. + "# + .unindent(), + ) + }); + let (_, cx) = cx.add_window_view(|_| EmptyView {}); + let editor = cx.build_view(|cx| Editor::for_buffer(buffer.clone(), None, cx)); + + let search_bar = cx.build_view(|cx| { + let mut search_bar = BufferSearchBar::new(cx); + search_bar.set_active_pane_item(Some(&editor), cx); + search_bar.show(cx); + search_bar + }); + + (editor, search_bar, cx) + } + + #[gpui::test] + async fn test_search_simple(cx: &mut TestAppContext) { + let (editor, search_bar, cx) = init_test(cx); + // todo! osiewicz: these tests asserted on background color as well, that should be brought back. + let display_points_of = |background_highlights: Vec<(Range, Hsla)>| { + background_highlights + .into_iter() + .map(|(range, _)| range) + .collect::>() + }; + // Search for a string that appears with different casing. + // By default, search is case-insensitive. + search_bar + .update(cx, |search_bar, cx| search_bar.search("us", None, cx)) + .await + .unwrap(); + editor.update(cx, |editor, cx| { + assert_eq!( + display_points_of(editor.all_text_background_highlights(cx)), + &[ + DisplayPoint::new(2, 17)..DisplayPoint::new(2, 19), + DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45), + ] + ); + }); + + // Switch to a case sensitive search. + search_bar.update(cx, |search_bar, cx| { + search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx); + }); + let mut editor_notifications = cx.notifications(&editor); + editor_notifications.next().await; + editor.update(cx, |editor, cx| { + assert_eq!( + display_points_of(editor.all_text_background_highlights(cx)), + &[DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),] + ); + }); + + // Search for a string that appears both as a whole word and + // within other words. By default, all results are found. + search_bar + .update(cx, |search_bar, cx| search_bar.search("or", None, cx)) + .await + .unwrap(); + editor.update(cx, |editor, cx| { + assert_eq!( + display_points_of(editor.all_text_background_highlights(cx)), + &[ + DisplayPoint::new(0, 24)..DisplayPoint::new(0, 26), + DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43), + DisplayPoint::new(2, 71)..DisplayPoint::new(2, 73), + DisplayPoint::new(3, 1)..DisplayPoint::new(3, 3), + DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13), + DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58), + DisplayPoint::new(3, 60)..DisplayPoint::new(3, 62), + ] + ); + }); + + // Switch to a whole word search. + search_bar.update(cx, |search_bar, cx| { + search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx); + }); + let mut editor_notifications = cx.notifications(&editor); + editor_notifications.next().await; + editor.update(cx, |editor, cx| { + assert_eq!( + display_points_of(editor.all_text_background_highlights(cx)), + &[ + DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43), + DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13), + DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58), + ] + ); + }); + + editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| { + s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)]) + }); + }); + search_bar.update(cx, |search_bar, cx| { + assert_eq!(search_bar.active_match_index, Some(0)); + search_bar.select_next_match(&SelectNextMatch, cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)), + [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] + ); + }); + search_bar.update(cx, |search_bar, _| { + assert_eq!(search_bar.active_match_index, Some(0)); + }); + + search_bar.update(cx, |search_bar, cx| { + search_bar.select_next_match(&SelectNextMatch, cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)), + [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)] + ); + }); + search_bar.update(cx, |search_bar, _| { + assert_eq!(search_bar.active_match_index, Some(1)); + }); + + search_bar.update(cx, |search_bar, cx| { + search_bar.select_next_match(&SelectNextMatch, cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)), + [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)] + ); + }); + search_bar.update(cx, |search_bar, _| { + assert_eq!(search_bar.active_match_index, Some(2)); + }); + + search_bar.update(cx, |search_bar, cx| { + search_bar.select_next_match(&SelectNextMatch, cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)), + [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] + ); + }); + search_bar.update(cx, |search_bar, _| { + assert_eq!(search_bar.active_match_index, Some(0)); + }); + + search_bar.update(cx, |search_bar, cx| { + search_bar.select_prev_match(&SelectPrevMatch, cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)), + [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)] + ); + }); + search_bar.update(cx, |search_bar, _| { + assert_eq!(search_bar.active_match_index, Some(2)); + }); + + search_bar.update(cx, |search_bar, cx| { + search_bar.select_prev_match(&SelectPrevMatch, cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)), + [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)] + ); + }); + search_bar.update(cx, |search_bar, _| { + assert_eq!(search_bar.active_match_index, Some(1)); + }); + + search_bar.update(cx, |search_bar, cx| { + search_bar.select_prev_match(&SelectPrevMatch, cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)), + [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] + ); + }); + search_bar.update(cx, |search_bar, _| { + assert_eq!(search_bar.active_match_index, Some(0)); + }); + + // Park the cursor in between matches and ensure that going to the previous match selects + // the closest match to the left. + editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| { + s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)]) + }); + }); + search_bar.update(cx, |search_bar, cx| { + assert_eq!(search_bar.active_match_index, Some(1)); + search_bar.select_prev_match(&SelectPrevMatch, cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)), + [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] + ); + }); + search_bar.update(cx, |search_bar, _| { + assert_eq!(search_bar.active_match_index, Some(0)); + }); + + // Park the cursor in between matches and ensure that going to the next match selects the + // closest match to the right. + editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| { + s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)]) + }); + }); + search_bar.update(cx, |search_bar, cx| { + assert_eq!(search_bar.active_match_index, Some(1)); + search_bar.select_next_match(&SelectNextMatch, cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)), + [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)] + ); + }); + search_bar.update(cx, |search_bar, _| { + assert_eq!(search_bar.active_match_index, Some(1)); + }); + + // Park the cursor after the last match and ensure that going to the previous match selects + // the last match. + editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| { + s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)]) + }); + }); + search_bar.update(cx, |search_bar, cx| { + assert_eq!(search_bar.active_match_index, Some(2)); + search_bar.select_prev_match(&SelectPrevMatch, cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)), + [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)] + ); + }); + search_bar.update(cx, |search_bar, _| { + assert_eq!(search_bar.active_match_index, Some(2)); + }); + + // Park the cursor after the last match and ensure that going to the next match selects the + // first match. + editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| { + s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)]) + }); + }); + search_bar.update(cx, |search_bar, cx| { + assert_eq!(search_bar.active_match_index, Some(2)); + search_bar.select_next_match(&SelectNextMatch, cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)), + [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] + ); + }); + search_bar.update(cx, |search_bar, _| { + assert_eq!(search_bar.active_match_index, Some(0)); + }); + + // Park the cursor before the first match and ensure that going to the previous match + // selects the last match. + editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| { + s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)]) + }); + }); + search_bar.update(cx, |search_bar, cx| { + assert_eq!(search_bar.active_match_index, Some(0)); + search_bar.select_prev_match(&SelectPrevMatch, cx); + assert_eq!( + editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)), + [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)] + ); + }); + search_bar.update(cx, |search_bar, _| { + assert_eq!(search_bar.active_match_index, Some(2)); + }); + } + + #[gpui::test] + async fn test_search_option_handling(cx: &mut TestAppContext) { + let (editor, search_bar, cx) = init_test(cx); + + // show with options should make current search case sensitive + search_bar + .update(cx, |search_bar, cx| { + search_bar.show(cx); + search_bar.search("us", Some(SearchOptions::CASE_SENSITIVE), cx) + }) + .await + .unwrap(); + // todo! osiewicz: these tests previously asserted on background color highlights; that should be introduced back. + let display_points_of = |background_highlights: Vec<(Range, Hsla)>| { + background_highlights + .into_iter() + .map(|(range, _)| range) + .collect::>() + }; + editor.update(cx, |editor, cx| { + assert_eq!( + display_points_of(editor.all_text_background_highlights(cx)), + &[DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),] + ); + }); + + // search_suggested should restore default options + search_bar.update(cx, |search_bar, cx| { + search_bar.search_suggested(cx); + assert_eq!(search_bar.search_options, SearchOptions::NONE) + }); + + // toggling a search option should update the defaults + search_bar + .update(cx, |search_bar, cx| { + search_bar.search("regex", Some(SearchOptions::CASE_SENSITIVE), cx) + }) + .await + .unwrap(); + search_bar.update(cx, |search_bar, cx| { + search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx) + }); + let mut editor_notifications = cx.notifications(&editor); + editor_notifications.next().await; + editor.update(cx, |editor, cx| { + assert_eq!( + display_points_of(editor.all_text_background_highlights(cx)), + &[DisplayPoint::new(0, 35)..DisplayPoint::new(0, 40),] + ); + }); + + // defaults should still include whole word + search_bar.update(cx, |search_bar, cx| { + search_bar.search_suggested(cx); + assert_eq!( + search_bar.search_options, + SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD + ) + }); + } + + #[gpui::test] + async fn test_search_select_all_matches(cx: &mut TestAppContext) { + init_globals(cx); + let buffer_text = r#" + A regular expression (shortened as regex or regexp;[1] also referred to as + rational expression[2][3]) is a sequence of characters that specifies a search + pattern in text. Usually such patterns are used by string-searching algorithms + for "find" or "find and replace" operations on strings, or for input validation. + "# + .unindent(); + let expected_query_matches_count = buffer_text + .chars() + .filter(|c| c.to_ascii_lowercase() == 'a') + .count(); + assert!( + expected_query_matches_count > 1, + "Should pick a query with multiple results" + ); + let buffer = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), buffer_text)); + let window = cx.add_window(|_| EmptyView {}); + + let editor = window.build_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx)); + + let search_bar = window.build_view(cx, |cx| { + let mut search_bar = BufferSearchBar::new(cx); + search_bar.set_active_pane_item(Some(&editor), cx); + search_bar.show(cx); + search_bar + }); + + window + .update(cx, |_, cx| { + search_bar.update(cx, |search_bar, cx| search_bar.search("a", None, cx)) + }) + .unwrap() + .await + .unwrap(); + let initial_selections = window + .update(cx, |_, cx| { + search_bar.update(cx, |search_bar, cx| { + let handle = search_bar.query_editor.focus_handle(cx); + cx.focus(&handle); + search_bar.activate_current_match(cx); + }); + assert!( + !editor.read(cx).is_focused(cx), + "Initially, the editor should not be focused" + ); + let initial_selections = editor.update(cx, |editor, cx| { + let initial_selections = editor.selections.display_ranges(cx); + assert_eq!( + initial_selections.len(), 1, + "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}", + ); + initial_selections + }); + search_bar.update(cx, |search_bar, cx| { + assert_eq!(search_bar.active_match_index, Some(0)); + let handle = search_bar.query_editor.focus_handle(cx); + cx.focus(&handle); + search_bar.select_all_matches(&SelectAllMatches, cx); + }); + assert!( + editor.read(cx).is_focused(cx), + "Should focus editor after successful SelectAllMatches" + ); + search_bar.update(cx, |search_bar, cx| { + let all_selections = + editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)); + assert_eq!( + all_selections.len(), + expected_query_matches_count, + "Should select all `a` characters in the buffer, but got: {all_selections:?}" + ); + assert_eq!( + search_bar.active_match_index, + Some(0), + "Match index should not change after selecting all matches" + ); + }); + + search_bar.update(cx, |this, cx| this.select_next_match(&SelectNextMatch, cx)); + initial_selections + }).unwrap(); + + window + .update(cx, |_, cx| { + assert!( + editor.read(cx).is_focused(cx), + "Should still have editor focused after SelectNextMatch" + ); + search_bar.update(cx, |search_bar, cx| { + let all_selections = + editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)); + assert_eq!( + all_selections.len(), + 1, + "On next match, should deselect items and select the next match" + ); + assert_ne!( + all_selections, initial_selections, + "Next match should be different from the first selection" + ); + assert_eq!( + search_bar.active_match_index, + Some(1), + "Match index should be updated to the next one" + ); + let handle = search_bar.query_editor.focus_handle(cx); + cx.focus(&handle); + search_bar.select_all_matches(&SelectAllMatches, cx); + }); + }) + .unwrap(); + window + .update(cx, |_, cx| { + assert!( + editor.read(cx).is_focused(cx), + "Should focus editor after successful SelectAllMatches" + ); + search_bar.update(cx, |search_bar, cx| { + let all_selections = + editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)); + assert_eq!( + all_selections.len(), + expected_query_matches_count, + "Should select all `a` characters in the buffer, but got: {all_selections:?}" + ); + assert_eq!( + search_bar.active_match_index, + Some(1), + "Match index should not change after selecting all matches" + ); + }); + search_bar.update(cx, |search_bar, cx| { + search_bar.select_prev_match(&SelectPrevMatch, cx); + }); + }) + .unwrap(); + let last_match_selections = window + .update(cx, |_, cx| { + assert!( + editor.read(cx).is_focused(&cx), + "Should still have editor focused after SelectPrevMatch" + ); + + search_bar.update(cx, |search_bar, cx| { + let all_selections = + editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)); + assert_eq!( + all_selections.len(), + 1, + "On previous match, should deselect items and select the previous item" + ); + assert_eq!( + all_selections, initial_selections, + "Previous match should be the same as the first selection" + ); + assert_eq!( + search_bar.active_match_index, + Some(0), + "Match index should be updated to the previous one" + ); + all_selections + }) + }) + .unwrap(); + + window + .update(cx, |_, cx| { + search_bar.update(cx, |search_bar, cx| { + let handle = search_bar.query_editor.focus_handle(cx); + cx.focus(&handle); + search_bar.search("abas_nonexistent_match", None, cx) + }) + }) + .unwrap() + .await + .unwrap(); + window + .update(cx, |_, cx| { + search_bar.update(cx, |search_bar, cx| { + search_bar.select_all_matches(&SelectAllMatches, cx); + }); + assert!( + editor.update(cx, |this, cx| !this.is_focused(cx.window_context())), + "Should not switch focus to editor if SelectAllMatches does not find any matches" + ); + search_bar.update(cx, |search_bar, cx| { + let all_selections = + editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)); + assert_eq!( + all_selections, last_match_selections, + "Should not select anything new if there are no matches" + ); + assert!( + search_bar.active_match_index.is_none(), + "For no matches, there should be no active match index" + ); + }); + }) + .unwrap(); + } + + #[gpui::test] + async fn test_search_query_history(cx: &mut TestAppContext) { + //crate::project_search::tests::init_test(cx); + init_globals(cx); + let buffer_text = r#" + A regular expression (shortened as regex or regexp;[1] also referred to as + rational expression[2][3]) is a sequence of characters that specifies a search + pattern in text. Usually such patterns are used by string-searching algorithms + for "find" or "find and replace" operations on strings, or for input validation. + "# + .unindent(); + let buffer = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), buffer_text)); + let (_, cx) = cx.add_window_view(|_| EmptyView {}); + + let editor = cx.build_view(|cx| Editor::for_buffer(buffer.clone(), None, cx)); + + let search_bar = cx.build_view(|cx| { + let mut search_bar = BufferSearchBar::new(cx); + search_bar.set_active_pane_item(Some(&editor), cx); + search_bar.show(cx); + search_bar + }); + + // Add 3 search items into the history. + search_bar + .update(cx, |search_bar, cx| search_bar.search("a", None, cx)) + .await + .unwrap(); + search_bar + .update(cx, |search_bar, cx| search_bar.search("b", None, cx)) + .await + .unwrap(); + search_bar + .update(cx, |search_bar, cx| { + search_bar.search("c", Some(SearchOptions::CASE_SENSITIVE), cx) + }) + .await + .unwrap(); + // Ensure that the latest search is active. + search_bar.update(cx, |search_bar, cx| { + assert_eq!(search_bar.query(cx), "c"); + assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); + }); + + // Next history query after the latest should set the query to the empty string. + search_bar.update(cx, |search_bar, cx| { + search_bar.next_history_query(&NextHistoryQuery, cx); + }); + search_bar.update(cx, |search_bar, cx| { + assert_eq!(search_bar.query(cx), ""); + assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); + }); + search_bar.update(cx, |search_bar, cx| { + search_bar.next_history_query(&NextHistoryQuery, cx); + }); + search_bar.update(cx, |search_bar, cx| { + assert_eq!(search_bar.query(cx), ""); + assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); + }); + + // First previous query for empty current query should set the query to the latest. + search_bar.update(cx, |search_bar, cx| { + search_bar.previous_history_query(&PreviousHistoryQuery, cx); + }); + search_bar.update(cx, |search_bar, cx| { + assert_eq!(search_bar.query(cx), "c"); + assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); + }); + + // Further previous items should go over the history in reverse order. + search_bar.update(cx, |search_bar, cx| { + search_bar.previous_history_query(&PreviousHistoryQuery, cx); + }); + search_bar.update(cx, |search_bar, cx| { + assert_eq!(search_bar.query(cx), "b"); + assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); + }); + + // Previous items should never go behind the first history item. + search_bar.update(cx, |search_bar, cx| { + search_bar.previous_history_query(&PreviousHistoryQuery, cx); + }); + search_bar.update(cx, |search_bar, cx| { + assert_eq!(search_bar.query(cx), "a"); + assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); + }); + search_bar.update(cx, |search_bar, cx| { + search_bar.previous_history_query(&PreviousHistoryQuery, cx); + }); + search_bar.update(cx, |search_bar, cx| { + assert_eq!(search_bar.query(cx), "a"); + assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); + }); + + // Next items should go over the history in the original order. + search_bar.update(cx, |search_bar, cx| { + search_bar.next_history_query(&NextHistoryQuery, cx); + }); + search_bar.update(cx, |search_bar, cx| { + assert_eq!(search_bar.query(cx), "b"); + assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); + }); + + search_bar + .update(cx, |search_bar, cx| search_bar.search("ba", None, cx)) + .await + .unwrap(); + search_bar.update(cx, |search_bar, cx| { + assert_eq!(search_bar.query(cx), "ba"); + assert_eq!(search_bar.search_options, SearchOptions::NONE); + }); + + // New search input should add another entry to history and move the selection to the end of the history. + search_bar.update(cx, |search_bar, cx| { + search_bar.previous_history_query(&PreviousHistoryQuery, cx); + }); + search_bar.update(cx, |search_bar, cx| { + assert_eq!(search_bar.query(cx), "c"); + assert_eq!(search_bar.search_options, SearchOptions::NONE); + }); + search_bar.update(cx, |search_bar, cx| { + search_bar.previous_history_query(&PreviousHistoryQuery, cx); + }); + search_bar.update(cx, |search_bar, cx| { + assert_eq!(search_bar.query(cx), "b"); + assert_eq!(search_bar.search_options, SearchOptions::NONE); + }); + search_bar.update(cx, |search_bar, cx| { + search_bar.next_history_query(&NextHistoryQuery, cx); + }); + search_bar.update(cx, |search_bar, cx| { + assert_eq!(search_bar.query(cx), "c"); + assert_eq!(search_bar.search_options, SearchOptions::NONE); + }); + search_bar.update(cx, |search_bar, cx| { + search_bar.next_history_query(&NextHistoryQuery, cx); + }); + search_bar.update(cx, |search_bar, cx| { + assert_eq!(search_bar.query(cx), "ba"); + assert_eq!(search_bar.search_options, SearchOptions::NONE); + }); + search_bar.update(cx, |search_bar, cx| { + search_bar.next_history_query(&NextHistoryQuery, cx); + }); + search_bar.update(cx, |search_bar, cx| { + assert_eq!(search_bar.query(cx), ""); + assert_eq!(search_bar.search_options, SearchOptions::NONE); + }); + } + #[gpui::test] + async fn test_replace_simple(cx: &mut TestAppContext) { + let (editor, search_bar, cx) = init_test(cx); + + search_bar + .update(cx, |search_bar, cx| { + search_bar.search("expression", None, cx) + }) + .await + .unwrap(); + + search_bar.update(cx, |search_bar, cx| { + search_bar.replacement_editor.update(cx, |editor, cx| { + // We use $1 here as initially we should be in Text mode, where `$1` should be treated literally. + editor.set_text("expr$1", cx); + }); + search_bar.replace_all(&ReplaceAll, cx) + }); + assert_eq!( + editor.update(cx, |this, cx| { this.text(cx) }), + r#" + A regular expr$1 (shortened as regex or regexp;[1] also referred to as + rational expr$1[2][3]) is a sequence of characters that specifies a search + pattern in text. Usually such patterns are used by string-searching algorithms + for "find" or "find and replace" operations on strings, or for input validation. + "# + .unindent() + ); + + // Search for word boundaries and replace just a single one. + search_bar + .update(cx, |search_bar, cx| { + search_bar.search("or", Some(SearchOptions::WHOLE_WORD), cx) + }) + .await + .unwrap(); + + search_bar.update(cx, |search_bar, cx| { + search_bar.replacement_editor.update(cx, |editor, cx| { + editor.set_text("banana", cx); + }); + search_bar.replace_next(&ReplaceNext, cx) + }); + // Notice how the first or in the text (shORtened) is not replaced. Neither are the remaining hits of `or` in the text. + assert_eq!( + editor.update(cx, |this, cx| { this.text(cx) }), + r#" + A regular expr$1 (shortened as regex banana regexp;[1] also referred to as + rational expr$1[2][3]) is a sequence of characters that specifies a search + pattern in text. Usually such patterns are used by string-searching algorithms + for "find" or "find and replace" operations on strings, or for input validation. + "# + .unindent() + ); + // Let's turn on regex mode. + search_bar + .update(cx, |search_bar, cx| { + search_bar.activate_search_mode(SearchMode::Regex, cx); + search_bar.search("\\[([^\\]]+)\\]", None, cx) + }) + .await + .unwrap(); + search_bar.update(cx, |search_bar, cx| { + search_bar.replacement_editor.update(cx, |editor, cx| { + editor.set_text("${1}number", cx); + }); + search_bar.replace_all(&ReplaceAll, cx) + }); + assert_eq!( + editor.update(cx, |this, cx| { this.text(cx) }), + r#" + A regular expr$1 (shortened as regex banana regexp;1number also referred to as + rational expr$12number3number) is a sequence of characters that specifies a search + pattern in text. Usually such patterns are used by string-searching algorithms + for "find" or "find and replace" operations on strings, or for input validation. + "# + .unindent() + ); + // Now with a whole-word twist. + search_bar + .update(cx, |search_bar, cx| { + search_bar.activate_search_mode(SearchMode::Regex, cx); + search_bar.search("a\\w+s", Some(SearchOptions::WHOLE_WORD), cx) + }) + .await + .unwrap(); + search_bar.update(cx, |search_bar, cx| { + search_bar.replacement_editor.update(cx, |editor, cx| { + editor.set_text("things", cx); + }); + search_bar.replace_all(&ReplaceAll, cx) + }); + // The only word affected by this edit should be `algorithms`, even though there's a bunch + // of words in this text that would match this regex if not for WHOLE_WORD. + assert_eq!( + editor.update(cx, |this, cx| { this.text(cx) }), + r#" + A regular expr$1 (shortened as regex banana regexp;1number also referred to as + rational expr$12number3number) is a sequence of characters that specifies a search + pattern in text. Usually such patterns are used by string-searching things + for "find" or "find and replace" operations on strings, or for input validation. + "# + .unindent() + ); + } +} diff --git a/crates/search2/src/history.rs b/crates/search2/src/history.rs new file mode 100644 index 0000000000000000000000000000000000000000..6b06c60293d4389693b9d3692a2649856076081f --- /dev/null +++ b/crates/search2/src/history.rs @@ -0,0 +1,184 @@ +use smallvec::SmallVec; +const SEARCH_HISTORY_LIMIT: usize = 20; + +#[derive(Default, Debug, Clone)] +pub struct SearchHistory { + history: SmallVec<[String; SEARCH_HISTORY_LIMIT]>, + selected: Option, +} + +impl SearchHistory { + pub fn add(&mut self, search_string: String) { + if let Some(i) = self.selected { + if search_string == self.history[i] { + return; + } + } + + if let Some(previously_searched) = self.history.last_mut() { + if search_string.find(previously_searched.as_str()).is_some() { + *previously_searched = search_string; + self.selected = Some(self.history.len() - 1); + return; + } + } + + self.history.push(search_string); + if self.history.len() > SEARCH_HISTORY_LIMIT { + self.history.remove(0); + } + self.selected = Some(self.history.len() - 1); + } + + pub fn next(&mut self) -> Option<&str> { + let history_size = self.history.len(); + if history_size == 0 { + return None; + } + + let selected = self.selected?; + if selected == history_size - 1 { + return None; + } + let next_index = selected + 1; + self.selected = Some(next_index); + Some(&self.history[next_index]) + } + + pub fn current(&self) -> Option<&str> { + Some(&self.history[self.selected?]) + } + + pub fn previous(&mut self) -> Option<&str> { + let history_size = self.history.len(); + if history_size == 0 { + return None; + } + + let prev_index = match self.selected { + Some(selected_index) => { + if selected_index == 0 { + return None; + } else { + selected_index - 1 + } + } + None => history_size - 1, + }; + + self.selected = Some(prev_index); + Some(&self.history[prev_index]) + } + + pub fn reset_selection(&mut self) { + self.selected = None; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_add() { + let mut search_history = SearchHistory::default(); + assert_eq!( + search_history.current(), + None, + "No current selection should be set fo the default search history" + ); + + search_history.add("rust".to_string()); + assert_eq!( + search_history.current(), + Some("rust"), + "Newly added item should be selected" + ); + + // check if duplicates are not added + search_history.add("rust".to_string()); + assert_eq!( + search_history.history.len(), + 1, + "Should not add a duplicate" + ); + assert_eq!(search_history.current(), Some("rust")); + + // check if new string containing the previous string replaces it + search_history.add("rustlang".to_string()); + assert_eq!( + search_history.history.len(), + 1, + "Should replace previous item if it's a substring" + ); + assert_eq!(search_history.current(), Some("rustlang")); + + // push enough items to test SEARCH_HISTORY_LIMIT + for i in 0..SEARCH_HISTORY_LIMIT * 2 { + search_history.add(format!("item{i}")); + } + assert!(search_history.history.len() <= SEARCH_HISTORY_LIMIT); + } + + #[test] + fn test_next_and_previous() { + let mut search_history = SearchHistory::default(); + assert_eq!( + search_history.next(), + None, + "Default search history should not have a next item" + ); + + search_history.add("Rust".to_string()); + assert_eq!(search_history.next(), None); + search_history.add("JavaScript".to_string()); + assert_eq!(search_history.next(), None); + search_history.add("TypeScript".to_string()); + assert_eq!(search_history.next(), None); + + assert_eq!(search_history.current(), Some("TypeScript")); + + assert_eq!(search_history.previous(), Some("JavaScript")); + assert_eq!(search_history.current(), Some("JavaScript")); + + assert_eq!(search_history.previous(), Some("Rust")); + assert_eq!(search_history.current(), Some("Rust")); + + assert_eq!(search_history.previous(), None); + assert_eq!(search_history.current(), Some("Rust")); + + assert_eq!(search_history.next(), Some("JavaScript")); + assert_eq!(search_history.current(), Some("JavaScript")); + + assert_eq!(search_history.next(), Some("TypeScript")); + assert_eq!(search_history.current(), Some("TypeScript")); + + assert_eq!(search_history.next(), None); + assert_eq!(search_history.current(), Some("TypeScript")); + } + + #[test] + fn test_reset_selection() { + let mut search_history = SearchHistory::default(); + search_history.add("Rust".to_string()); + search_history.add("JavaScript".to_string()); + search_history.add("TypeScript".to_string()); + + assert_eq!(search_history.current(), Some("TypeScript")); + search_history.reset_selection(); + assert_eq!(search_history.current(), None); + assert_eq!( + search_history.previous(), + Some("TypeScript"), + "Should start from the end after reset on previous item query" + ); + + search_history.previous(); + assert_eq!(search_history.current(), Some("JavaScript")); + search_history.previous(); + assert_eq!(search_history.current(), Some("Rust")); + + search_history.reset_selection(); + assert_eq!(search_history.current(), None); + } +} diff --git a/crates/search2/src/mode.rs b/crates/search2/src/mode.rs new file mode 100644 index 0000000000000000000000000000000000000000..817fb454d2dcb08953d012fbb9814874c786cb78 --- /dev/null +++ b/crates/search2/src/mode.rs @@ -0,0 +1,32 @@ +// TODO: Update the default search mode to get from config +#[derive(Copy, Clone, Debug, Default, PartialEq)] +pub enum SearchMode { + #[default] + Text, + Semantic, + Regex, +} + +impl SearchMode { + pub(crate) fn label(&self) -> &'static str { + match self { + SearchMode::Text => "Text", + SearchMode::Semantic => "Semantic", + SearchMode::Regex => "Regex", + } + } +} + +pub(crate) fn next_mode(mode: &SearchMode, semantic_enabled: bool) -> SearchMode { + match mode { + SearchMode::Text => SearchMode::Regex, + SearchMode::Regex => { + if semantic_enabled { + SearchMode::Semantic + } else { + SearchMode::Text + } + } + SearchMode::Semantic => SearchMode::Text, + } +} diff --git a/crates/search2/src/project_search.rs b/crates/search2/src/project_search.rs new file mode 100644 index 0000000000000000000000000000000000000000..f6e17bbee5d12685385ca64de790d5f8217bb92d --- /dev/null +++ b/crates/search2/src/project_search.rs @@ -0,0 +1,2661 @@ +use crate::{ + history::SearchHistory, + mode::{SearchMode, Side}, + search_bar::{render_nav_button, render_option_button_icon, render_search_mode_button}, + ActivateRegexMode, ActivateSemanticMode, ActivateTextMode, CycleMode, NextHistoryQuery, + PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOptions, SelectNextMatch, SelectPrevMatch, + ToggleCaseSensitive, ToggleReplace, ToggleWholeWord, +}; +use anyhow::{Context, Result}; +use collections::HashMap; +use editor::{ + items::active_match_index, scroll::autoscroll::Autoscroll, Anchor, Editor, MultiBuffer, + SelectAll, MAX_TAB_TITLE_LEN, +}; +use futures::StreamExt; +use gpui::{ + actions, + elements::*, + platform::{MouseButton, PromptLevel}, + Action, AnyElement, AnyViewHandle, AppContext, Entity, ModelContext, ModelHandle, Subscription, + Task, View, ViewContext, ViewHandle, WeakModelHandle, WeakViewHandle, +}; +use menu::Confirm; +use project::{ + search::{SearchInputs, SearchQuery}, + Entry, Project, +}; +use semantic_index::{SemanticIndex, SemanticIndexStatus}; +use smallvec::SmallVec; +use std::{ + any::{Any, TypeId}, + borrow::Cow, + collections::HashSet, + mem, + ops::{Not, Range}, + path::PathBuf, + sync::Arc, + time::{Duration, Instant}, +}; +use util::{paths::PathMatcher, ResultExt as _}; +use workspace::{ + item::{BreadcrumbText, Item, ItemEvent, ItemHandle}, + searchable::{Direction, SearchableItem, SearchableItemHandle}, + ItemNavHistory, Pane, ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceId, +}; + +actions!( + project_search, + [SearchInNew, ToggleFocus, NextField, ToggleFilters,] +); + +#[derive(Default)] +struct ActiveSearches(HashMap, WeakViewHandle>); + +#[derive(Default)] +struct ActiveSettings(HashMap, ProjectSearchSettings>); + +pub fn init(cx: &mut AppContext) { + cx.set_global(ActiveSearches::default()); + cx.set_global(ActiveSettings::default()); + cx.add_action(ProjectSearchView::deploy); + cx.add_action(ProjectSearchView::move_focus_to_results); + cx.add_action(ProjectSearchBar::confirm); + cx.add_action(ProjectSearchBar::search_in_new); + cx.add_action(ProjectSearchBar::select_next_match); + cx.add_action(ProjectSearchBar::select_prev_match); + cx.add_action(ProjectSearchBar::replace_next); + cx.add_action(ProjectSearchBar::replace_all); + cx.add_action(ProjectSearchBar::cycle_mode); + cx.add_action(ProjectSearchBar::next_history_query); + cx.add_action(ProjectSearchBar::previous_history_query); + cx.add_action(ProjectSearchBar::activate_regex_mode); + cx.add_action(ProjectSearchBar::toggle_replace); + cx.add_action(ProjectSearchBar::toggle_replace_on_a_pane); + cx.add_action(ProjectSearchBar::activate_text_mode); + + // This action should only be registered if the semantic index is enabled + // We are registering it all the time, as I dont want to introduce a dependency + // for Semantic Index Settings globally whenever search is tested. + cx.add_action(ProjectSearchBar::activate_semantic_mode); + + cx.capture_action(ProjectSearchBar::tab); + cx.capture_action(ProjectSearchBar::tab_previous); + cx.capture_action(ProjectSearchView::replace_all); + cx.capture_action(ProjectSearchView::replace_next); + add_toggle_option_action::(SearchOptions::CASE_SENSITIVE, cx); + add_toggle_option_action::(SearchOptions::WHOLE_WORD, cx); + add_toggle_filters_action::(cx); +} + +fn add_toggle_filters_action(cx: &mut AppContext) { + cx.add_action(move |pane: &mut Pane, _: &A, cx: &mut ViewContext| { + if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { + if search_bar.update(cx, |search_bar, cx| search_bar.toggle_filters(cx)) { + return; + } + } + cx.propagate_action(); + }); +} + +fn add_toggle_option_action(option: SearchOptions, cx: &mut AppContext) { + cx.add_action(move |pane: &mut Pane, _: &A, cx: &mut ViewContext| { + if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { + if search_bar.update(cx, |search_bar, cx| { + search_bar.toggle_search_option(option, cx) + }) { + return; + } + } + cx.propagate_action(); + }); +} + +struct ProjectSearch { + project: ModelHandle, + excerpts: ModelHandle, + pending_search: Option>>, + match_ranges: Vec>, + active_query: Option, + search_id: usize, + search_history: SearchHistory, + no_results: Option, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +enum InputPanel { + Query, + Exclude, + Include, +} + +pub struct ProjectSearchView { + model: ModelHandle, + query_editor: ViewHandle, + replacement_editor: ViewHandle, + results_editor: ViewHandle, + semantic_state: Option, + semantic_permissioned: Option, + search_options: SearchOptions, + panels_with_errors: HashSet, + active_match_index: Option, + search_id: usize, + query_editor_was_focused: bool, + included_files_editor: ViewHandle, + excluded_files_editor: ViewHandle, + filters_enabled: bool, + replace_enabled: bool, + current_mode: SearchMode, +} + +struct SemanticState { + index_status: SemanticIndexStatus, + maintain_rate_limit: Option>, + _subscription: Subscription, +} + +#[derive(Debug, Clone)] +struct ProjectSearchSettings { + search_options: SearchOptions, + filters_enabled: bool, + current_mode: SearchMode, +} + +pub struct ProjectSearchBar { + active_project_search: Option>, + subscription: Option, +} + +impl Entity for ProjectSearch { + type Event = (); +} + +impl ProjectSearch { + fn new(project: ModelHandle, cx: &mut ModelContext) -> Self { + let replica_id = project.read(cx).replica_id(); + Self { + project, + excerpts: cx.add_model(|_| MultiBuffer::new(replica_id)), + pending_search: Default::default(), + match_ranges: Default::default(), + active_query: None, + search_id: 0, + search_history: SearchHistory::default(), + no_results: None, + } + } + + fn clone(&self, cx: &mut ModelContext) -> ModelHandle { + cx.add_model(|cx| Self { + project: self.project.clone(), + excerpts: self + .excerpts + .update(cx, |excerpts, cx| cx.add_model(|cx| excerpts.clone(cx))), + pending_search: Default::default(), + match_ranges: self.match_ranges.clone(), + active_query: self.active_query.clone(), + search_id: self.search_id, + search_history: self.search_history.clone(), + no_results: self.no_results.clone(), + }) + } + + fn search(&mut self, query: SearchQuery, cx: &mut ModelContext) { + let search = self + .project + .update(cx, |project, cx| project.search(query.clone(), cx)); + self.search_id += 1; + self.search_history.add(query.as_str().to_string()); + self.active_query = Some(query); + self.match_ranges.clear(); + self.pending_search = Some(cx.spawn_weak(|this, mut cx| async move { + let mut matches = search; + let this = this.upgrade(&cx)?; + this.update(&mut cx, |this, cx| { + this.match_ranges.clear(); + this.excerpts.update(cx, |this, cx| this.clear(cx)); + this.no_results = Some(true); + }); + + while let Some((buffer, anchors)) = matches.next().await { + let mut ranges = this.update(&mut cx, |this, cx| { + this.no_results = Some(false); + this.excerpts.update(cx, |excerpts, cx| { + excerpts.stream_excerpts_with_context_lines(buffer, anchors, 1, cx) + }) + }); + + while let Some(range) = ranges.next().await { + this.update(&mut cx, |this, _| this.match_ranges.push(range)); + } + this.update(&mut cx, |_, cx| cx.notify()); + } + + this.update(&mut cx, |this, cx| { + this.pending_search.take(); + cx.notify(); + }); + + None + })); + cx.notify(); + } + + fn semantic_search(&mut self, inputs: &SearchInputs, cx: &mut ModelContext) { + let search = SemanticIndex::global(cx).map(|index| { + index.update(cx, |semantic_index, cx| { + semantic_index.search_project( + self.project.clone(), + inputs.as_str().to_owned(), + 10, + inputs.files_to_include().to_vec(), + inputs.files_to_exclude().to_vec(), + cx, + ) + }) + }); + self.search_id += 1; + self.match_ranges.clear(); + self.search_history.add(inputs.as_str().to_string()); + self.no_results = None; + self.pending_search = Some(cx.spawn(|this, mut cx| async move { + let results = search?.await.log_err()?; + let matches = results + .into_iter() + .map(|result| (result.buffer, vec![result.range.start..result.range.start])); + + this.update(&mut cx, |this, cx| { + this.no_results = Some(true); + this.excerpts.update(cx, |excerpts, cx| { + excerpts.clear(cx); + }); + }); + for (buffer, ranges) in matches { + let mut match_ranges = this.update(&mut cx, |this, cx| { + this.no_results = Some(false); + this.excerpts.update(cx, |excerpts, cx| { + excerpts.stream_excerpts_with_context_lines(buffer, ranges, 3, cx) + }) + }); + while let Some(match_range) = match_ranges.next().await { + this.update(&mut cx, |this, cx| { + this.match_ranges.push(match_range); + while let Ok(Some(match_range)) = match_ranges.try_next() { + this.match_ranges.push(match_range); + } + cx.notify(); + }); + } + } + + this.update(&mut cx, |this, cx| { + this.pending_search.take(); + cx.notify(); + }); + + None + })); + cx.notify(); + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum ViewEvent { + UpdateTab, + Activate, + EditorEvent(editor::Event), + Dismiss, +} + +impl Entity for ProjectSearchView { + type Event = ViewEvent; +} + +impl View for ProjectSearchView { + fn ui_name() -> &'static str { + "ProjectSearchView" + } + + fn render(&mut self, cx: &mut ViewContext) -> AnyElement { + let model = &self.model.read(cx); + if model.match_ranges.is_empty() { + enum Status {} + + let theme = theme::current(cx).clone(); + + // If Search is Active -> Major: Searching..., Minor: None + // If Semantic -> Major: "Search using Natural Language", Minor: {Status}/n{ex...}/n{ex...} + // If Regex -> Major: "Search using Regex", Minor: {ex...} + // If Text -> Major: "Text search all files and folders", Minor: {...} + + let current_mode = self.current_mode; + let mut major_text = if model.pending_search.is_some() { + Cow::Borrowed("Searching...") + } else if model.no_results.is_some_and(|v| v) { + Cow::Borrowed("No Results") + } else { + match current_mode { + SearchMode::Text => Cow::Borrowed("Text search all files and folders"), + SearchMode::Semantic => { + Cow::Borrowed("Search all code objects using Natural Language") + } + SearchMode::Regex => Cow::Borrowed("Regex search all files and folders"), + } + }; + + let mut show_minor_text = true; + let semantic_status = self.semantic_state.as_ref().and_then(|semantic| { + let status = semantic.index_status; + match status { + SemanticIndexStatus::NotAuthenticated => { + major_text = Cow::Borrowed("Not Authenticated"); + show_minor_text = false; + Some(vec![ + "API Key Missing: Please set 'OPENAI_API_KEY' in Environment Variables." + .to_string(), "If you authenticated using the Assistant Panel, please restart Zed to Authenticate.".to_string()]) + } + SemanticIndexStatus::Indexed => Some(vec!["Indexing complete".to_string()]), + SemanticIndexStatus::Indexing { + remaining_files, + rate_limit_expiry, + } => { + if remaining_files == 0 { + Some(vec![format!("Indexing...")]) + } else { + if let Some(rate_limit_expiry) = rate_limit_expiry { + let remaining_seconds = + rate_limit_expiry.duration_since(Instant::now()); + if remaining_seconds > Duration::from_secs(0) { + Some(vec![format!( + "Remaining files to index (rate limit resets in {}s): {}", + remaining_seconds.as_secs(), + remaining_files + )]) + } else { + Some(vec![format!("Remaining files to index: {}", remaining_files)]) + } + } else { + Some(vec![format!("Remaining files to index: {}", remaining_files)]) + } + } + } + SemanticIndexStatus::NotIndexed => None, + } + }); + + let minor_text = if let Some(no_results) = model.no_results { + if model.pending_search.is_none() && no_results { + vec!["No results found in this project for the provided query".to_owned()] + } else { + vec![] + } + } else { + match current_mode { + SearchMode::Semantic => { + let mut minor_text: Vec = Vec::new(); + minor_text.push("".into()); + if let Some(semantic_status) = semantic_status { + minor_text.extend(semantic_status); + } + if show_minor_text { + minor_text + .push("Simply explain the code you are looking to find.".into()); + minor_text.push( + "ex. 'prompt user for permissions to index their project'".into(), + ); + } + minor_text + } + _ => vec![ + "".to_owned(), + "Include/exclude specific paths with the filter option.".to_owned(), + "Matching exact word and/or casing is available too.".to_owned(), + ], + } + }; + + let previous_query_keystrokes = + cx.binding_for_action(&PreviousHistoryQuery {}) + .map(|binding| { + binding + .keystrokes() + .iter() + .map(|k| k.to_string()) + .collect::>() + }); + let next_query_keystrokes = + cx.binding_for_action(&NextHistoryQuery {}).map(|binding| { + binding + .keystrokes() + .iter() + .map(|k| k.to_string()) + .collect::>() + }); + let new_placeholder_text = match (previous_query_keystrokes, next_query_keystrokes) { + (Some(previous_query_keystrokes), Some(next_query_keystrokes)) => { + format!( + "Search ({}/{} for previous/next query)", + previous_query_keystrokes.join(" "), + next_query_keystrokes.join(" ") + ) + } + (None, Some(next_query_keystrokes)) => { + format!( + "Search ({} for next query)", + next_query_keystrokes.join(" ") + ) + } + (Some(previous_query_keystrokes), None) => { + format!( + "Search ({} for previous query)", + previous_query_keystrokes.join(" ") + ) + } + (None, None) => String::new(), + }; + self.query_editor.update(cx, |editor, cx| { + editor.set_placeholder_text(new_placeholder_text, cx); + }); + + MouseEventHandler::new::(0, cx, |_, _| { + Flex::column() + .with_child(Flex::column().contained().flex(1., true)) + .with_child( + Flex::column() + .align_children_center() + .with_child(Label::new( + major_text, + theme.search.major_results_status.clone(), + )) + .with_children( + minor_text.into_iter().map(|x| { + Label::new(x, theme.search.minor_results_status.clone()) + }), + ) + .aligned() + .top() + .contained() + .flex(7., true), + ) + .contained() + .with_background_color(theme.editor.background) + }) + .on_down(MouseButton::Left, |_, _, cx| { + cx.focus_parent(); + }) + .into_any_named("project search view") + } else { + ChildView::new(&self.results_editor, cx) + .flex(1., true) + .into_any_named("project search view") + } + } + + fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { + let handle = cx.weak_handle(); + cx.update_global(|state: &mut ActiveSearches, cx| { + state + .0 + .insert(self.model.read(cx).project.downgrade(), handle) + }); + + cx.update_global(|state: &mut ActiveSettings, cx| { + state.0.insert( + self.model.read(cx).project.downgrade(), + self.current_settings(), + ); + }); + + if cx.is_self_focused() { + if self.query_editor_was_focused { + cx.focus(&self.query_editor); + } else { + cx.focus(&self.results_editor); + } + } + } +} + +impl Item for ProjectSearchView { + fn tab_tooltip_text(&self, cx: &AppContext) -> Option> { + let query_text = self.query_editor.read(cx).text(cx); + + query_text + .is_empty() + .not() + .then(|| query_text.into()) + .or_else(|| Some("Project Search".into())) + } + fn should_close_item_on_event(event: &Self::Event) -> bool { + event == &Self::Event::Dismiss + } + + fn act_as_type<'a>( + &'a self, + type_id: TypeId, + self_handle: &'a ViewHandle, + _: &'a AppContext, + ) -> Option<&'a AnyViewHandle> { + if type_id == TypeId::of::() { + Some(self_handle) + } else if type_id == TypeId::of::() { + Some(&self.results_editor) + } else { + None + } + } + + fn deactivated(&mut self, cx: &mut ViewContext) { + self.results_editor + .update(cx, |editor, cx| editor.deactivated(cx)); + } + + fn tab_content( + &self, + _detail: Option, + tab_theme: &theme::Tab, + cx: &AppContext, + ) -> AnyElement { + Flex::row() + .with_child( + Svg::new("icons/magnifying_glass.svg") + .with_color(tab_theme.label.text.color) + .constrained() + .with_width(tab_theme.type_icon_width) + .aligned() + .contained() + .with_margin_right(tab_theme.spacing), + ) + .with_child({ + let tab_name: Option> = self + .model + .read(cx) + .search_history + .current() + .as_ref() + .map(|query| { + let query_text = util::truncate_and_trailoff(query, MAX_TAB_TITLE_LEN); + query_text.into() + }); + Label::new( + tab_name + .filter(|name| !name.is_empty()) + .unwrap_or("Project search".into()), + tab_theme.label.clone(), + ) + .aligned() + }) + .into_any() + } + + fn for_each_project_item(&self, cx: &AppContext, f: &mut dyn FnMut(usize, &dyn project::Item)) { + self.results_editor.for_each_project_item(cx, f) + } + + fn is_singleton(&self, _: &AppContext) -> bool { + false + } + + fn can_save(&self, _: &AppContext) -> bool { + true + } + + fn is_dirty(&self, cx: &AppContext) -> bool { + self.results_editor.read(cx).is_dirty(cx) + } + + fn has_conflict(&self, cx: &AppContext) -> bool { + self.results_editor.read(cx).has_conflict(cx) + } + + fn save( + &mut self, + project: ModelHandle, + cx: &mut ViewContext, + ) -> Task> { + self.results_editor + .update(cx, |editor, cx| editor.save(project, cx)) + } + + fn save_as( + &mut self, + _: ModelHandle, + _: PathBuf, + _: &mut ViewContext, + ) -> Task> { + unreachable!("save_as should not have been called") + } + + fn reload( + &mut self, + project: ModelHandle, + cx: &mut ViewContext, + ) -> Task> { + self.results_editor + .update(cx, |editor, cx| editor.reload(project, cx)) + } + + fn clone_on_split(&self, _workspace_id: WorkspaceId, cx: &mut ViewContext) -> Option + where + Self: Sized, + { + let model = self.model.update(cx, |model, cx| model.clone(cx)); + Some(Self::new(model, cx, None)) + } + + fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext) { + self.results_editor + .update(cx, |editor, cx| editor.added_to_workspace(workspace, cx)); + } + + fn set_nav_history(&mut self, nav_history: ItemNavHistory, cx: &mut ViewContext) { + self.results_editor.update(cx, |editor, _| { + editor.set_nav_history(Some(nav_history)); + }); + } + + fn navigate(&mut self, data: Box, cx: &mut ViewContext) -> bool { + self.results_editor + .update(cx, |editor, cx| editor.navigate(data, cx)) + } + + fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> { + match event { + ViewEvent::UpdateTab => { + smallvec::smallvec![ItemEvent::UpdateBreadcrumbs, ItemEvent::UpdateTab] + } + ViewEvent::EditorEvent(editor_event) => Editor::to_item_events(editor_event), + ViewEvent::Dismiss => smallvec::smallvec![ItemEvent::CloseItem], + _ => SmallVec::new(), + } + } + + fn breadcrumb_location(&self) -> ToolbarItemLocation { + if self.has_matches() { + ToolbarItemLocation::Secondary + } else { + ToolbarItemLocation::Hidden + } + } + + fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option> { + self.results_editor.breadcrumbs(theme, cx) + } + + fn serialized_item_kind() -> Option<&'static str> { + None + } + + fn deserialize( + _project: ModelHandle, + _workspace: WeakViewHandle, + _workspace_id: workspace::WorkspaceId, + _item_id: workspace::ItemId, + _cx: &mut ViewContext, + ) -> Task>> { + unimplemented!() + } +} + +impl ProjectSearchView { + fn toggle_filters(&mut self, cx: &mut ViewContext) { + self.filters_enabled = !self.filters_enabled; + cx.update_global(|state: &mut ActiveSettings, cx| { + state.0.insert( + self.model.read(cx).project.downgrade(), + self.current_settings(), + ); + }); + } + + fn current_settings(&self) -> ProjectSearchSettings { + ProjectSearchSettings { + search_options: self.search_options, + filters_enabled: self.filters_enabled, + current_mode: self.current_mode, + } + } + fn toggle_search_option(&mut self, option: SearchOptions, cx: &mut ViewContext) { + self.search_options.toggle(option); + cx.update_global(|state: &mut ActiveSettings, cx| { + state.0.insert( + self.model.read(cx).project.downgrade(), + self.current_settings(), + ); + }); + } + + fn index_project(&mut self, cx: &mut ViewContext) { + if let Some(semantic_index) = SemanticIndex::global(cx) { + // Semantic search uses no options + self.search_options = SearchOptions::none(); + + let project = self.model.read(cx).project.clone(); + + semantic_index.update(cx, |semantic_index, cx| { + semantic_index + .index_project(project.clone(), cx) + .detach_and_log_err(cx); + }); + + self.semantic_state = Some(SemanticState { + index_status: semantic_index.read(cx).status(&project), + maintain_rate_limit: None, + _subscription: cx.observe(&semantic_index, Self::semantic_index_changed), + }); + self.semantic_index_changed(semantic_index, cx); + } + } + + fn semantic_index_changed( + &mut self, + semantic_index: ModelHandle, + cx: &mut ViewContext, + ) { + let project = self.model.read(cx).project.clone(); + if let Some(semantic_state) = self.semantic_state.as_mut() { + cx.notify(); + semantic_state.index_status = semantic_index.read(cx).status(&project); + if let SemanticIndexStatus::Indexing { + rate_limit_expiry: Some(_), + .. + } = &semantic_state.index_status + { + if semantic_state.maintain_rate_limit.is_none() { + semantic_state.maintain_rate_limit = + Some(cx.spawn(|this, mut cx| async move { + loop { + cx.background().timer(Duration::from_secs(1)).await; + this.update(&mut cx, |_, cx| cx.notify()).log_err(); + } + })); + return; + } + } else { + semantic_state.maintain_rate_limit = None; + } + } + } + + fn clear_search(&mut self, cx: &mut ViewContext) { + self.model.update(cx, |model, cx| { + model.pending_search = None; + model.no_results = None; + model.match_ranges.clear(); + + model.excerpts.update(cx, |excerpts, cx| { + excerpts.clear(cx); + }); + }); + } + + fn activate_search_mode(&mut self, mode: SearchMode, cx: &mut ViewContext) { + let previous_mode = self.current_mode; + if previous_mode == mode { + return; + } + + self.clear_search(cx); + self.current_mode = mode; + self.active_match_index = None; + + match mode { + SearchMode::Semantic => { + let has_permission = self.semantic_permissioned(cx); + self.active_match_index = None; + cx.spawn(|this, mut cx| async move { + let has_permission = has_permission.await?; + + if !has_permission { + let mut answer = this.update(&mut cx, |this, cx| { + let project = this.model.read(cx).project.clone(); + let project_name = project + .read(cx) + .worktree_root_names(cx) + .collect::>() + .join("/"); + let is_plural = + project_name.chars().filter(|letter| *letter == '/').count() > 0; + let prompt_text = format!("Would you like to index the '{}' project{} for semantic search? This requires sending code to the OpenAI API", project_name, + if is_plural { + "s" + } else {""}); + cx.prompt( + PromptLevel::Info, + prompt_text.as_str(), + &["Continue", "Cancel"], + ) + })?; + + if answer.next().await == Some(0) { + this.update(&mut cx, |this, _| { + this.semantic_permissioned = Some(true); + })?; + } else { + this.update(&mut cx, |this, cx| { + this.semantic_permissioned = Some(false); + debug_assert_ne!(previous_mode, SearchMode::Semantic, "Tried to re-enable semantic search mode after user modal was rejected"); + this.activate_search_mode(previous_mode, cx); + })?; + return anyhow::Ok(()); + } + } + + this.update(&mut cx, |this, cx| { + this.index_project(cx); + })?; + + anyhow::Ok(()) + }).detach_and_log_err(cx); + } + SearchMode::Regex | SearchMode::Text => { + self.semantic_state = None; + self.active_match_index = None; + self.search(cx); + } + } + + cx.update_global(|state: &mut ActiveSettings, cx| { + state.0.insert( + self.model.read(cx).project.downgrade(), + self.current_settings(), + ); + }); + + cx.notify(); + } + fn replace_next(&mut self, _: &ReplaceNext, cx: &mut ViewContext) { + let model = self.model.read(cx); + if let Some(query) = model.active_query.as_ref() { + if model.match_ranges.is_empty() { + return; + } + if let Some(active_index) = self.active_match_index { + let query = query.clone().with_replacement(self.replacement(cx)); + self.results_editor.replace( + &(Box::new(model.match_ranges[active_index].clone()) as _), + &query, + cx, + ); + self.select_match(Direction::Next, cx) + } + } + } + pub fn replacement(&self, cx: &AppContext) -> String { + self.replacement_editor.read(cx).text(cx) + } + fn replace_all(&mut self, _: &ReplaceAll, cx: &mut ViewContext) { + let model = self.model.read(cx); + if let Some(query) = model.active_query.as_ref() { + if model.match_ranges.is_empty() { + return; + } + if self.active_match_index.is_some() { + let query = query.clone().with_replacement(self.replacement(cx)); + let matches = model + .match_ranges + .iter() + .map(|item| Box::new(item.clone()) as _) + .collect::>(); + for item in matches { + self.results_editor.replace(&item, &query, cx); + } + } + } + } + + fn new( + model: ModelHandle, + cx: &mut ViewContext, + settings: Option, + ) -> Self { + let project; + let excerpts; + let mut replacement_text = None; + let mut query_text = String::new(); + + // Read in settings if available + let (mut options, current_mode, filters_enabled) = if let Some(settings) = settings { + ( + settings.search_options, + settings.current_mode, + settings.filters_enabled, + ) + } else { + (SearchOptions::NONE, Default::default(), false) + }; + + { + let model = model.read(cx); + project = model.project.clone(); + excerpts = model.excerpts.clone(); + if let Some(active_query) = model.active_query.as_ref() { + query_text = active_query.as_str().to_string(); + replacement_text = active_query.replacement().map(ToOwned::to_owned); + options = SearchOptions::from_query(active_query); + } + } + cx.observe(&model, |this, _, cx| this.model_changed(cx)) + .detach(); + + let query_editor = cx.add_view(|cx| { + let mut editor = Editor::single_line( + Some(Arc::new(|theme| theme.search.editor.input.clone())), + cx, + ); + editor.set_placeholder_text("Text search all files", cx); + editor.set_text(query_text, cx); + editor + }); + // Subscribe to query_editor in order to reraise editor events for workspace item activation purposes + cx.subscribe(&query_editor, |_, _, event, cx| { + cx.emit(ViewEvent::EditorEvent(event.clone())) + }) + .detach(); + let replacement_editor = cx.add_view(|cx| { + let mut editor = Editor::single_line( + Some(Arc::new(|theme| theme.search.editor.input.clone())), + cx, + ); + editor.set_placeholder_text("Replace in project..", cx); + if let Some(text) = replacement_text { + editor.set_text(text, cx); + } + editor + }); + let results_editor = cx.add_view(|cx| { + let mut editor = Editor::for_multibuffer(excerpts, Some(project.clone()), cx); + editor.set_searchable(false); + editor + }); + cx.observe(&results_editor, |_, _, cx| cx.emit(ViewEvent::UpdateTab)) + .detach(); + + cx.subscribe(&results_editor, |this, _, event, cx| { + if matches!(event, editor::Event::SelectionsChanged { .. }) { + this.update_match_index(cx); + } + // Reraise editor events for workspace item activation purposes + cx.emit(ViewEvent::EditorEvent(event.clone())); + }) + .detach(); + + let included_files_editor = cx.add_view(|cx| { + let mut editor = Editor::single_line( + Some(Arc::new(|theme| { + theme.search.include_exclude_editor.input.clone() + })), + cx, + ); + editor.set_placeholder_text("Include: crates/**/*.toml", cx); + + editor + }); + // Subscribe to include_files_editor in order to reraise editor events for workspace item activation purposes + cx.subscribe(&included_files_editor, |_, _, event, cx| { + cx.emit(ViewEvent::EditorEvent(event.clone())) + }) + .detach(); + + let excluded_files_editor = cx.add_view(|cx| { + let mut editor = Editor::single_line( + Some(Arc::new(|theme| { + theme.search.include_exclude_editor.input.clone() + })), + cx, + ); + editor.set_placeholder_text("Exclude: vendor/*, *.lock", cx); + + editor + }); + // Subscribe to excluded_files_editor in order to reraise editor events for workspace item activation purposes + cx.subscribe(&excluded_files_editor, |_, _, event, cx| { + cx.emit(ViewEvent::EditorEvent(event.clone())) + }) + .detach(); + + // Check if Worktrees have all been previously indexed + let mut this = ProjectSearchView { + replacement_editor, + search_id: model.read(cx).search_id, + model, + query_editor, + results_editor, + semantic_state: None, + semantic_permissioned: None, + search_options: options, + panels_with_errors: HashSet::new(), + active_match_index: None, + query_editor_was_focused: false, + included_files_editor, + excluded_files_editor, + filters_enabled, + current_mode, + replace_enabled: false, + }; + this.model_changed(cx); + this + } + + fn semantic_permissioned(&mut self, cx: &mut ViewContext) -> Task> { + if let Some(value) = self.semantic_permissioned { + return Task::ready(Ok(value)); + } + + SemanticIndex::global(cx) + .map(|semantic| { + let project = self.model.read(cx).project.clone(); + semantic.update(cx, |this, cx| this.project_previously_indexed(&project, cx)) + }) + .unwrap_or(Task::ready(Ok(false))) + } + pub fn new_search_in_directory( + workspace: &mut Workspace, + dir_entry: &Entry, + cx: &mut ViewContext, + ) { + if !dir_entry.is_dir() { + return; + } + let Some(filter_str) = dir_entry.path.to_str() else { + return; + }; + + let model = cx.add_model(|cx| ProjectSearch::new(workspace.project().clone(), cx)); + let search = cx.add_view(|cx| ProjectSearchView::new(model, cx, None)); + workspace.add_item(Box::new(search.clone()), cx); + search.update(cx, |search, cx| { + search + .included_files_editor + .update(cx, |editor, cx| editor.set_text(filter_str, cx)); + search.filters_enabled = true; + search.focus_query_editor(cx) + }); + } + + // Re-activate the most recently activated search or the most recent if it has been closed. + // If no search exists in the workspace, create a new one. + fn deploy( + workspace: &mut Workspace, + _: &workspace::NewSearch, + cx: &mut ViewContext, + ) { + // Clean up entries for dropped projects + cx.update_global(|state: &mut ActiveSearches, cx| { + state.0.retain(|project, _| project.is_upgradable(cx)) + }); + + let active_search = cx + .global::() + .0 + .get(&workspace.project().downgrade()); + + let existing = active_search + .and_then(|active_search| { + workspace + .items_of_type::(cx) + .find(|search| search == active_search) + }) + .or_else(|| workspace.item_of_type::(cx)); + + let query = workspace.active_item(cx).and_then(|item| { + let editor = item.act_as::(cx)?; + let query = editor.query_suggestion(cx); + if query.is_empty() { + None + } else { + Some(query) + } + }); + + let search = if let Some(existing) = existing { + workspace.activate_item(&existing, cx); + existing + } else { + let settings = cx + .global::() + .0 + .get(&workspace.project().downgrade()); + + let settings = if let Some(settings) = settings { + Some(settings.clone()) + } else { + None + }; + + let model = cx.add_model(|cx| ProjectSearch::new(workspace.project().clone(), cx)); + let view = cx.add_view(|cx| ProjectSearchView::new(model, cx, settings)); + + workspace.add_item(Box::new(view.clone()), cx); + view + }; + + search.update(cx, |search, cx| { + if let Some(query) = query { + search.set_query(&query, cx); + } + search.focus_query_editor(cx) + }); + } + + fn search(&mut self, cx: &mut ViewContext) { + let mode = self.current_mode; + match mode { + SearchMode::Semantic => { + if self.semantic_state.is_some() { + if let Some(query) = self.build_search_query(cx) { + self.model + .update(cx, |model, cx| model.semantic_search(query.as_inner(), cx)); + } + } + } + + _ => { + if let Some(query) = self.build_search_query(cx) { + self.model.update(cx, |model, cx| model.search(query, cx)); + } + } + } + } + + fn build_search_query(&mut self, cx: &mut ViewContext) -> Option { + let text = self.query_editor.read(cx).text(cx); + let included_files = + match Self::parse_path_matches(&self.included_files_editor.read(cx).text(cx)) { + Ok(included_files) => { + self.panels_with_errors.remove(&InputPanel::Include); + included_files + } + Err(_e) => { + self.panels_with_errors.insert(InputPanel::Include); + cx.notify(); + return None; + } + }; + let excluded_files = + match Self::parse_path_matches(&self.excluded_files_editor.read(cx).text(cx)) { + Ok(excluded_files) => { + self.panels_with_errors.remove(&InputPanel::Exclude); + excluded_files + } + Err(_e) => { + self.panels_with_errors.insert(InputPanel::Exclude); + cx.notify(); + return None; + } + }; + let current_mode = self.current_mode; + match current_mode { + SearchMode::Regex => { + match SearchQuery::regex( + text, + self.search_options.contains(SearchOptions::WHOLE_WORD), + self.search_options.contains(SearchOptions::CASE_SENSITIVE), + included_files, + excluded_files, + ) { + Ok(query) => { + self.panels_with_errors.remove(&InputPanel::Query); + Some(query) + } + Err(_e) => { + self.panels_with_errors.insert(InputPanel::Query); + cx.notify(); + None + } + } + } + _ => match SearchQuery::text( + text, + self.search_options.contains(SearchOptions::WHOLE_WORD), + self.search_options.contains(SearchOptions::CASE_SENSITIVE), + included_files, + excluded_files, + ) { + Ok(query) => { + self.panels_with_errors.remove(&InputPanel::Query); + Some(query) + } + Err(_e) => { + self.panels_with_errors.insert(InputPanel::Query); + cx.notify(); + None + } + }, + } + } + + fn parse_path_matches(text: &str) -> anyhow::Result> { + text.split(',') + .map(str::trim) + .filter(|maybe_glob_str| !maybe_glob_str.is_empty()) + .map(|maybe_glob_str| { + PathMatcher::new(maybe_glob_str) + .with_context(|| format!("parsing {maybe_glob_str} as path matcher")) + }) + .collect() + } + + fn select_match(&mut self, direction: Direction, cx: &mut ViewContext) { + if let Some(index) = self.active_match_index { + let match_ranges = self.model.read(cx).match_ranges.clone(); + let new_index = self.results_editor.update(cx, |editor, cx| { + editor.match_index_for_direction(&match_ranges, index, direction, 1, cx) + }); + + let range_to_select = match_ranges[new_index].clone(); + self.results_editor.update(cx, |editor, cx| { + let range_to_select = editor.range_for_match(&range_to_select); + editor.unfold_ranges([range_to_select.clone()], false, true, cx); + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.select_ranges([range_to_select]) + }); + }); + } + } + + fn focus_query_editor(&mut self, cx: &mut ViewContext) { + self.query_editor.update(cx, |query_editor, cx| { + query_editor.select_all(&SelectAll, cx); + }); + self.query_editor_was_focused = true; + cx.focus(&self.query_editor); + } + + fn set_query(&mut self, query: &str, cx: &mut ViewContext) { + self.query_editor + .update(cx, |query_editor, cx| query_editor.set_text(query, cx)); + } + + fn focus_results_editor(&mut self, cx: &mut ViewContext) { + self.query_editor.update(cx, |query_editor, cx| { + let cursor = query_editor.selections.newest_anchor().head(); + query_editor.change_selections(None, cx, |s| s.select_ranges([cursor.clone()..cursor])); + }); + self.query_editor_was_focused = false; + cx.focus(&self.results_editor); + } + + fn model_changed(&mut self, cx: &mut ViewContext) { + let match_ranges = self.model.read(cx).match_ranges.clone(); + if match_ranges.is_empty() { + self.active_match_index = None; + } else { + self.active_match_index = Some(0); + self.update_match_index(cx); + let prev_search_id = mem::replace(&mut self.search_id, self.model.read(cx).search_id); + let is_new_search = self.search_id != prev_search_id; + self.results_editor.update(cx, |editor, cx| { + if is_new_search { + let range_to_select = match_ranges + .first() + .clone() + .map(|range| editor.range_for_match(range)); + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.select_ranges(range_to_select) + }); + } + editor.highlight_background::( + match_ranges, + |theme| theme.search.match_background, + cx, + ); + }); + if is_new_search && self.query_editor.is_focused(cx) { + self.focus_results_editor(cx); + } + } + + cx.emit(ViewEvent::UpdateTab); + cx.notify(); + } + + fn update_match_index(&mut self, cx: &mut ViewContext) { + let results_editor = self.results_editor.read(cx); + let new_index = active_match_index( + &self.model.read(cx).match_ranges, + &results_editor.selections.newest_anchor().head(), + &results_editor.buffer().read(cx).snapshot(cx), + ); + if self.active_match_index != new_index { + self.active_match_index = new_index; + cx.notify(); + } + } + + pub fn has_matches(&self) -> bool { + self.active_match_index.is_some() + } + + fn move_focus_to_results(pane: &mut Pane, _: &ToggleFocus, cx: &mut ViewContext) { + if let Some(search_view) = pane + .active_item() + .and_then(|item| item.downcast::()) + { + search_view.update(cx, |search_view, cx| { + if !search_view.results_editor.is_focused(cx) + && !search_view.model.read(cx).match_ranges.is_empty() + { + return search_view.focus_results_editor(cx); + } + }); + } + + cx.propagate_action(); + } +} + +impl Default for ProjectSearchBar { + fn default() -> Self { + Self::new() + } +} + +impl ProjectSearchBar { + pub fn new() -> Self { + Self { + active_project_search: Default::default(), + subscription: Default::default(), + } + } + fn cycle_mode(workspace: &mut Workspace, _: &CycleMode, cx: &mut ViewContext) { + if let Some(search_view) = workspace + .active_item(cx) + .and_then(|item| item.downcast::()) + { + search_view.update(cx, |this, cx| { + let new_mode = + crate::mode::next_mode(&this.current_mode, SemanticIndex::enabled(cx)); + this.activate_search_mode(new_mode, cx); + cx.focus(&this.query_editor); + }) + } + } + fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) { + let mut should_propagate = true; + if let Some(search_view) = self.active_project_search.as_ref() { + search_view.update(cx, |search_view, cx| { + if !search_view.replacement_editor.is_focused(cx) { + should_propagate = false; + search_view.search(cx); + } + }); + } + if should_propagate { + cx.propagate_action(); + } + } + + fn search_in_new(workspace: &mut Workspace, _: &SearchInNew, cx: &mut ViewContext) { + if let Some(search_view) = workspace + .active_item(cx) + .and_then(|item| item.downcast::()) + { + let new_query = search_view.update(cx, |search_view, cx| { + let new_query = search_view.build_search_query(cx); + if new_query.is_some() { + if let Some(old_query) = search_view.model.read(cx).active_query.clone() { + search_view.query_editor.update(cx, |editor, cx| { + editor.set_text(old_query.as_str(), cx); + }); + search_view.search_options = SearchOptions::from_query(&old_query); + } + } + new_query + }); + if let Some(new_query) = new_query { + let model = cx.add_model(|cx| { + let mut model = ProjectSearch::new(workspace.project().clone(), cx); + model.search(new_query, cx); + model + }); + workspace.add_item( + Box::new(cx.add_view(|cx| ProjectSearchView::new(model, cx, None))), + cx, + ); + } + } + } + + fn select_next_match(pane: &mut Pane, _: &SelectNextMatch, cx: &mut ViewContext) { + if let Some(search_view) = pane + .active_item() + .and_then(|item| item.downcast::()) + { + search_view.update(cx, |view, cx| view.select_match(Direction::Next, cx)); + } else { + cx.propagate_action(); + } + } + + fn replace_next(pane: &mut Pane, _: &ReplaceNext, cx: &mut ViewContext) { + if let Some(search_view) = pane + .active_item() + .and_then(|item| item.downcast::()) + { + search_view.update(cx, |view, cx| view.replace_next(&ReplaceNext, cx)); + } else { + cx.propagate_action(); + } + } + fn replace_all(pane: &mut Pane, _: &ReplaceAll, cx: &mut ViewContext) { + if let Some(search_view) = pane + .active_item() + .and_then(|item| item.downcast::()) + { + search_view.update(cx, |view, cx| view.replace_all(&ReplaceAll, cx)); + } else { + cx.propagate_action(); + } + } + fn select_prev_match(pane: &mut Pane, _: &SelectPrevMatch, cx: &mut ViewContext) { + if let Some(search_view) = pane + .active_item() + .and_then(|item| item.downcast::()) + { + search_view.update(cx, |view, cx| view.select_match(Direction::Prev, cx)); + } else { + cx.propagate_action(); + } + } + + fn tab(&mut self, _: &editor::Tab, cx: &mut ViewContext) { + self.cycle_field(Direction::Next, cx); + } + + fn tab_previous(&mut self, _: &editor::TabPrev, cx: &mut ViewContext) { + self.cycle_field(Direction::Prev, cx); + } + + fn cycle_field(&mut self, direction: Direction, cx: &mut ViewContext) { + let active_project_search = match &self.active_project_search { + Some(active_project_search) => active_project_search, + + None => { + cx.propagate_action(); + return; + } + }; + + active_project_search.update(cx, |project_view, cx| { + let mut views = vec![&project_view.query_editor]; + if project_view.filters_enabled { + views.extend([ + &project_view.included_files_editor, + &project_view.excluded_files_editor, + ]); + } + if project_view.replace_enabled { + views.push(&project_view.replacement_editor); + } + let current_index = match views + .iter() + .enumerate() + .find(|(_, view)| view.is_focused(cx)) + { + Some((index, _)) => index, + + None => { + cx.propagate_action(); + return; + } + }; + + let new_index = match direction { + Direction::Next => (current_index + 1) % views.len(), + Direction::Prev if current_index == 0 => views.len() - 1, + Direction::Prev => (current_index - 1) % views.len(), + }; + cx.focus(views[new_index]); + }); + } + + fn toggle_search_option(&mut self, option: SearchOptions, cx: &mut ViewContext) -> bool { + if let Some(search_view) = self.active_project_search.as_ref() { + search_view.update(cx, |search_view, cx| { + search_view.toggle_search_option(option, cx); + search_view.search(cx); + }); + + cx.notify(); + true + } else { + false + } + } + fn toggle_replace(&mut self, _: &ToggleReplace, cx: &mut ViewContext) { + if let Some(search) = &self.active_project_search { + search.update(cx, |this, cx| { + this.replace_enabled = !this.replace_enabled; + if !this.replace_enabled { + cx.focus(&this.query_editor); + } + cx.notify(); + }); + } + } + fn toggle_replace_on_a_pane(pane: &mut Pane, _: &ToggleReplace, cx: &mut ViewContext) { + let mut should_propagate = true; + if let Some(search_view) = pane + .active_item() + .and_then(|item| item.downcast::()) + { + search_view.update(cx, |this, cx| { + should_propagate = false; + this.replace_enabled = !this.replace_enabled; + if !this.replace_enabled { + cx.focus(&this.query_editor); + } + cx.notify(); + }); + } + if should_propagate { + cx.propagate_action(); + } + } + fn activate_text_mode(pane: &mut Pane, _: &ActivateTextMode, cx: &mut ViewContext) { + if let Some(search_view) = pane + .active_item() + .and_then(|item| item.downcast::()) + { + search_view.update(cx, |view, cx| { + view.activate_search_mode(SearchMode::Text, cx) + }); + } else { + cx.propagate_action(); + } + } + + fn activate_regex_mode(pane: &mut Pane, _: &ActivateRegexMode, cx: &mut ViewContext) { + if let Some(search_view) = pane + .active_item() + .and_then(|item| item.downcast::()) + { + search_view.update(cx, |view, cx| { + view.activate_search_mode(SearchMode::Regex, cx) + }); + } else { + cx.propagate_action(); + } + } + + fn activate_semantic_mode( + pane: &mut Pane, + _: &ActivateSemanticMode, + cx: &mut ViewContext, + ) { + if SemanticIndex::enabled(cx) { + if let Some(search_view) = pane + .active_item() + .and_then(|item| item.downcast::()) + { + search_view.update(cx, |view, cx| { + view.activate_search_mode(SearchMode::Semantic, cx) + }); + } else { + cx.propagate_action(); + } + } + } + + fn toggle_filters(&mut self, cx: &mut ViewContext) -> bool { + if let Some(search_view) = self.active_project_search.as_ref() { + search_view.update(cx, |search_view, cx| { + search_view.toggle_filters(cx); + search_view + .included_files_editor + .update(cx, |_, cx| cx.notify()); + search_view + .excluded_files_editor + .update(cx, |_, cx| cx.notify()); + cx.refresh_windows(); + cx.notify(); + }); + cx.notify(); + true + } else { + false + } + } + + fn activate_search_mode(&self, mode: SearchMode, cx: &mut ViewContext) { + // Update Current Mode + if let Some(search_view) = self.active_project_search.as_ref() { + search_view.update(cx, |search_view, cx| { + search_view.activate_search_mode(mode, cx); + }); + cx.notify(); + } + } + + fn is_option_enabled(&self, option: SearchOptions, cx: &AppContext) -> bool { + if let Some(search) = self.active_project_search.as_ref() { + search.read(cx).search_options.contains(option) + } else { + false + } + } + + fn next_history_query(&mut self, _: &NextHistoryQuery, cx: &mut ViewContext) { + if let Some(search_view) = self.active_project_search.as_ref() { + search_view.update(cx, |search_view, cx| { + let new_query = search_view.model.update(cx, |model, _| { + if let Some(new_query) = model.search_history.next().map(str::to_string) { + new_query + } else { + model.search_history.reset_selection(); + String::new() + } + }); + search_view.set_query(&new_query, cx); + }); + } + } + + fn previous_history_query(&mut self, _: &PreviousHistoryQuery, cx: &mut ViewContext) { + if let Some(search_view) = self.active_project_search.as_ref() { + search_view.update(cx, |search_view, cx| { + if search_view.query_editor.read(cx).text(cx).is_empty() { + if let Some(new_query) = search_view + .model + .read(cx) + .search_history + .current() + .map(str::to_string) + { + search_view.set_query(&new_query, cx); + return; + } + } + + if let Some(new_query) = search_view.model.update(cx, |model, _| { + model.search_history.previous().map(str::to_string) + }) { + search_view.set_query(&new_query, cx); + } + }); + } + } +} + +impl Entity for ProjectSearchBar { + type Event = (); +} + +impl View for ProjectSearchBar { + fn ui_name() -> &'static str { + "ProjectSearchBar" + } + + fn update_keymap_context( + &self, + keymap: &mut gpui::keymap_matcher::KeymapContext, + cx: &AppContext, + ) { + Self::reset_to_default_keymap_context(keymap); + let in_replace = self + .active_project_search + .as_ref() + .map(|search| { + search + .read(cx) + .replacement_editor + .read_with(cx, |_, cx| cx.is_self_focused()) + }) + .flatten() + .unwrap_or(false); + if in_replace { + keymap.add_identifier("in_replace"); + } + } + + fn render(&mut self, cx: &mut ViewContext) -> AnyElement { + if let Some(_search) = self.active_project_search.as_ref() { + let search = _search.read(cx); + let theme = theme::current(cx).clone(); + let query_container_style = if search.panels_with_errors.contains(&InputPanel::Query) { + theme.search.invalid_editor + } else { + theme.search.editor.input.container + }; + + let search = _search.read(cx); + let filter_button = render_option_button_icon( + search.filters_enabled, + "icons/filter.svg", + 0, + "Toggle filters", + Box::new(ToggleFilters), + move |_, this, cx| { + this.toggle_filters(cx); + }, + cx, + ); + + let search = _search.read(cx); + let is_semantic_available = SemanticIndex::enabled(cx); + let is_semantic_disabled = search.semantic_state.is_none(); + let icon_style = theme.search.editor_icon.clone(); + let is_active = search.active_match_index.is_some(); + + let render_option_button_icon = |path, option, cx: &mut ViewContext| { + crate::search_bar::render_option_button_icon( + self.is_option_enabled(option, cx), + path, + option.bits as usize, + format!("Toggle {}", option.label()), + option.to_toggle_action(), + move |_, this, cx| { + this.toggle_search_option(option, cx); + }, + cx, + ) + }; + let case_sensitive = is_semantic_disabled.then(|| { + render_option_button_icon( + "icons/case_insensitive.svg", + SearchOptions::CASE_SENSITIVE, + cx, + ) + }); + + let whole_word = is_semantic_disabled.then(|| { + render_option_button_icon("icons/word_search.svg", SearchOptions::WHOLE_WORD, cx) + }); + + let search_button_for_mode = |mode, side, cx: &mut ViewContext| { + let is_active = if let Some(search) = self.active_project_search.as_ref() { + let search = search.read(cx); + search.current_mode == mode + } else { + false + }; + render_search_mode_button( + mode, + side, + is_active, + move |_, this, cx| { + this.activate_search_mode(mode, cx); + }, + cx, + ) + }; + + let search = _search.read(cx); + + let include_container_style = + if search.panels_with_errors.contains(&InputPanel::Include) { + theme.search.invalid_include_exclude_editor + } else { + theme.search.include_exclude_editor.input.container + }; + + let exclude_container_style = + if search.panels_with_errors.contains(&InputPanel::Exclude) { + theme.search.invalid_include_exclude_editor + } else { + theme.search.include_exclude_editor.input.container + }; + + let matches = search.active_match_index.map(|match_ix| { + Label::new( + format!( + "{}/{}", + match_ix + 1, + search.model.read(cx).match_ranges.len() + ), + theme.search.match_index.text.clone(), + ) + .contained() + .with_style(theme.search.match_index.container) + .aligned() + }); + let should_show_replace_input = search.replace_enabled; + let replacement = should_show_replace_input.then(|| { + Flex::row() + .with_child( + Svg::for_style(theme.search.replace_icon.clone().icon) + .contained() + .with_style(theme.search.replace_icon.clone().container), + ) + .with_child(ChildView::new(&search.replacement_editor, cx).flex(1., true)) + .align_children_center() + .flex(1., true) + .contained() + .with_style(query_container_style) + .constrained() + .with_min_width(theme.search.editor.min_width) + .with_max_width(theme.search.editor.max_width) + .with_height(theme.search.search_bar_row_height) + .flex(1., false) + }); + let replace_all = should_show_replace_input.then(|| { + super::replace_action( + ReplaceAll, + "Replace all", + "icons/replace_all.svg", + theme.tooltip.clone(), + theme.search.action_button.clone(), + ) + }); + let replace_next = should_show_replace_input.then(|| { + super::replace_action( + ReplaceNext, + "Replace next", + "icons/replace_next.svg", + theme.tooltip.clone(), + theme.search.action_button.clone(), + ) + }); + let query_column = Flex::column() + .with_spacing(theme.search.search_row_spacing) + .with_child( + Flex::row() + .with_child( + Svg::for_style(icon_style.icon) + .contained() + .with_style(icon_style.container), + ) + .with_child(ChildView::new(&search.query_editor, cx).flex(1., true)) + .with_child( + Flex::row() + .with_child(filter_button) + .with_children(case_sensitive) + .with_children(whole_word) + .flex(1., false) + .constrained() + .contained(), + ) + .align_children_center() + .contained() + .with_style(query_container_style) + .constrained() + .with_min_width(theme.search.editor.min_width) + .with_max_width(theme.search.editor.max_width) + .with_height(theme.search.search_bar_row_height) + .flex(1., false), + ) + .with_children(search.filters_enabled.then(|| { + Flex::row() + .with_child( + ChildView::new(&search.included_files_editor, cx) + .contained() + .with_style(include_container_style) + .constrained() + .with_height(theme.search.search_bar_row_height) + .flex(1., true), + ) + .with_child( + ChildView::new(&search.excluded_files_editor, cx) + .contained() + .with_style(exclude_container_style) + .constrained() + .with_height(theme.search.search_bar_row_height) + .flex(1., true), + ) + .constrained() + .with_min_width(theme.search.editor.min_width) + .with_max_width(theme.search.editor.max_width) + .flex(1., false) + })) + .flex(1., false); + let switches_column = Flex::row() + .align_children_center() + .with_child(super::toggle_replace_button( + search.replace_enabled, + theme.tooltip.clone(), + theme.search.option_button_component.clone(), + )) + .constrained() + .with_height(theme.search.search_bar_row_height) + .contained() + .with_style(theme.search.option_button_group); + let mode_column = + Flex::row() + .with_child(search_button_for_mode( + SearchMode::Text, + Some(Side::Left), + cx, + )) + .with_child(search_button_for_mode( + SearchMode::Regex, + if is_semantic_available { + None + } else { + Some(Side::Right) + }, + cx, + )) + .with_children(is_semantic_available.then(|| { + search_button_for_mode(SearchMode::Semantic, Some(Side::Right), cx) + })) + .contained() + .with_style(theme.search.modes_container); + + let nav_button_for_direction = |label, direction, cx: &mut ViewContext| { + render_nav_button( + label, + direction, + is_active, + move |_, this, cx| { + if let Some(search) = this.active_project_search.as_ref() { + search.update(cx, |search, cx| search.select_match(direction, cx)); + } + }, + cx, + ) + }; + + let nav_column = Flex::row() + .with_children(replace_next) + .with_children(replace_all) + .with_child(Flex::row().with_children(matches)) + .with_child(nav_button_for_direction("<", Direction::Prev, cx)) + .with_child(nav_button_for_direction(">", Direction::Next, cx)) + .constrained() + .with_height(theme.search.search_bar_row_height) + .flex_float(); + + Flex::row() + .with_child(query_column) + .with_child(mode_column) + .with_child(switches_column) + .with_children(replacement) + .with_child(nav_column) + .contained() + .with_style(theme.search.container) + .into_any_named("project search") + } else { + Empty::new().into_any() + } + } +} + +impl ToolbarItemView for ProjectSearchBar { + fn set_active_pane_item( + &mut self, + active_pane_item: Option<&dyn ItemHandle>, + cx: &mut ViewContext, + ) -> ToolbarItemLocation { + cx.notify(); + self.subscription = None; + self.active_project_search = None; + if let Some(search) = active_pane_item.and_then(|i| i.downcast::()) { + search.update(cx, |search, cx| { + if search.current_mode == SearchMode::Semantic { + search.index_project(cx); + } + }); + + self.subscription = Some(cx.observe(&search, |_, _, cx| cx.notify())); + self.active_project_search = Some(search); + ToolbarItemLocation::PrimaryLeft { + flex: Some((1., true)), + } + } else { + ToolbarItemLocation::Hidden + } + } + + fn row_count(&self, cx: &ViewContext) -> usize { + if let Some(search) = self.active_project_search.as_ref() { + if search.read(cx).filters_enabled { + return 2; + } + } + 1 + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + use editor::DisplayPoint; + use gpui::{color::Color, executor::Deterministic, TestAppContext}; + use project::FakeFs; + use semantic_index::semantic_index_settings::SemanticIndexSettings; + use serde_json::json; + use settings::SettingsStore; + use std::sync::Arc; + use theme::ThemeSettings; + + #[gpui::test] + async fn test_project_search(deterministic: Arc, cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/dir", + json!({ + "one.rs": "const ONE: usize = 1;", + "two.rs": "const TWO: usize = one::ONE + one::ONE;", + "three.rs": "const THREE: usize = one::ONE + two::TWO;", + "four.rs": "const FOUR: usize = one::ONE + three::THREE;", + }), + ) + .await; + let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + let search = cx.add_model(|cx| ProjectSearch::new(project, cx)); + let search_view = cx + .add_window(|cx| ProjectSearchView::new(search.clone(), cx, None)) + .root(cx); + + search_view.update(cx, |search_view, cx| { + search_view + .query_editor + .update(cx, |query_editor, cx| query_editor.set_text("TWO", cx)); + search_view.search(cx); + }); + deterministic.run_until_parked(); + search_view.update(cx, |search_view, cx| { + assert_eq!( + search_view + .results_editor + .update(cx, |editor, cx| editor.display_text(cx)), + "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;" + ); + assert_eq!( + search_view + .results_editor + .update(cx, |editor, cx| editor.all_text_background_highlights(cx)), + &[ + ( + DisplayPoint::new(2, 32)..DisplayPoint::new(2, 35), + Color::red() + ), + ( + DisplayPoint::new(2, 37)..DisplayPoint::new(2, 40), + Color::red() + ), + ( + DisplayPoint::new(5, 6)..DisplayPoint::new(5, 9), + Color::red() + ) + ] + ); + assert_eq!(search_view.active_match_index, Some(0)); + assert_eq!( + search_view + .results_editor + .update(cx, |editor, cx| editor.selections.display_ranges(cx)), + [DisplayPoint::new(2, 32)..DisplayPoint::new(2, 35)] + ); + + search_view.select_match(Direction::Next, cx); + }); + + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.active_match_index, Some(1)); + assert_eq!( + search_view + .results_editor + .update(cx, |editor, cx| editor.selections.display_ranges(cx)), + [DisplayPoint::new(2, 37)..DisplayPoint::new(2, 40)] + ); + search_view.select_match(Direction::Next, cx); + }); + + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.active_match_index, Some(2)); + assert_eq!( + search_view + .results_editor + .update(cx, |editor, cx| editor.selections.display_ranges(cx)), + [DisplayPoint::new(5, 6)..DisplayPoint::new(5, 9)] + ); + search_view.select_match(Direction::Next, cx); + }); + + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.active_match_index, Some(0)); + assert_eq!( + search_view + .results_editor + .update(cx, |editor, cx| editor.selections.display_ranges(cx)), + [DisplayPoint::new(2, 32)..DisplayPoint::new(2, 35)] + ); + search_view.select_match(Direction::Prev, cx); + }); + + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.active_match_index, Some(2)); + assert_eq!( + search_view + .results_editor + .update(cx, |editor, cx| editor.selections.display_ranges(cx)), + [DisplayPoint::new(5, 6)..DisplayPoint::new(5, 9)] + ); + search_view.select_match(Direction::Prev, cx); + }); + + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.active_match_index, Some(1)); + assert_eq!( + search_view + .results_editor + .update(cx, |editor, cx| editor.selections.display_ranges(cx)), + [DisplayPoint::new(2, 37)..DisplayPoint::new(2, 40)] + ); + }); + } + + #[gpui::test] + async fn test_project_search_focus(deterministic: Arc, cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/dir", + json!({ + "one.rs": "const ONE: usize = 1;", + "two.rs": "const TWO: usize = one::ONE + one::ONE;", + "three.rs": "const THREE: usize = one::ONE + two::TWO;", + "four.rs": "const FOUR: usize = one::ONE + three::THREE;", + }), + ) + .await; + let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + let window = cx.add_window(|cx| Workspace::test_new(project, cx)); + let workspace = window.root(cx); + + let active_item = cx.read(|cx| { + workspace + .read(cx) + .active_pane() + .read(cx) + .active_item() + .and_then(|item| item.downcast::()) + }); + assert!( + active_item.is_none(), + "Expected no search panel to be active, but got: {active_item:?}" + ); + + workspace.update(cx, |workspace, cx| { + ProjectSearchView::deploy(workspace, &workspace::NewSearch, cx) + }); + + let Some(search_view) = cx.read(|cx| { + workspace + .read(cx) + .active_pane() + .read(cx) + .active_item() + .and_then(|item| item.downcast::()) + }) else { + panic!("Search view expected to appear after new search event trigger") + }; + let search_view_id = search_view.id(); + + cx.spawn(|mut cx| async move { + window.dispatch_action(search_view_id, &ToggleFocus, &mut cx); + }) + .detach(); + deterministic.run_until_parked(); + search_view.update(cx, |search_view, cx| { + assert!( + search_view.query_editor.is_focused(cx), + "Empty search view should be focused after the toggle focus event: no results panel to focus on", + ); + }); + + search_view.update(cx, |search_view, cx| { + let query_editor = &search_view.query_editor; + assert!( + query_editor.is_focused(cx), + "Search view should be focused after the new search view is activated", + ); + let query_text = query_editor.read(cx).text(cx); + assert!( + query_text.is_empty(), + "New search query should be empty but got '{query_text}'", + ); + let results_text = search_view + .results_editor + .update(cx, |editor, cx| editor.display_text(cx)); + assert!( + results_text.is_empty(), + "Empty search view should have no results but got '{results_text}'" + ); + }); + + search_view.update(cx, |search_view, cx| { + search_view.query_editor.update(cx, |query_editor, cx| { + query_editor.set_text("sOMETHINGtHATsURELYdOESnOTeXIST", cx) + }); + search_view.search(cx); + }); + deterministic.run_until_parked(); + search_view.update(cx, |search_view, cx| { + let results_text = search_view + .results_editor + .update(cx, |editor, cx| editor.display_text(cx)); + assert!( + results_text.is_empty(), + "Search view for mismatching query should have no results but got '{results_text}'" + ); + assert!( + search_view.query_editor.is_focused(cx), + "Search view should be focused after mismatching query had been used in search", + ); + }); + cx.spawn( + |mut cx| async move { window.dispatch_action(search_view_id, &ToggleFocus, &mut cx) }, + ) + .detach(); + deterministic.run_until_parked(); + search_view.update(cx, |search_view, cx| { + assert!( + search_view.query_editor.is_focused(cx), + "Search view with mismatching query should be focused after the toggle focus event: still no results panel to focus on", + ); + }); + + search_view.update(cx, |search_view, cx| { + search_view + .query_editor + .update(cx, |query_editor, cx| query_editor.set_text("TWO", cx)); + search_view.search(cx); + }); + deterministic.run_until_parked(); + search_view.update(cx, |search_view, cx| { + assert_eq!( + search_view + .results_editor + .update(cx, |editor, cx| editor.display_text(cx)), + "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;", + "Search view results should match the query" + ); + assert!( + search_view.results_editor.is_focused(cx), + "Search view with mismatching query should be focused after search results are available", + ); + }); + cx.spawn(|mut cx| async move { + window.dispatch_action(search_view_id, &ToggleFocus, &mut cx); + }) + .detach(); + deterministic.run_until_parked(); + search_view.update(cx, |search_view, cx| { + assert!( + search_view.results_editor.is_focused(cx), + "Search view with matching query should still have its results editor focused after the toggle focus event", + ); + }); + + workspace.update(cx, |workspace, cx| { + ProjectSearchView::deploy(workspace, &workspace::NewSearch, cx) + }); + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), "two", "Query should be updated to first search result after search view 2nd open in a row"); + assert_eq!( + search_view + .results_editor + .update(cx, |editor, cx| editor.display_text(cx)), + "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;", + "Results should be unchanged after search view 2nd open in a row" + ); + assert!( + search_view.query_editor.is_focused(cx), + "Focus should be moved into query editor again after search view 2nd open in a row" + ); + }); + + cx.spawn(|mut cx| async move { + window.dispatch_action(search_view_id, &ToggleFocus, &mut cx); + }) + .detach(); + deterministic.run_until_parked(); + search_view.update(cx, |search_view, cx| { + assert!( + search_view.results_editor.is_focused(cx), + "Search view with matching query should switch focus to the results editor after the toggle focus event", + ); + }); + } + + #[gpui::test] + async fn test_new_project_search_in_directory( + deterministic: Arc, + cx: &mut TestAppContext, + ) { + init_test(cx); + + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/dir", + json!({ + "a": { + "one.rs": "const ONE: usize = 1;", + "two.rs": "const TWO: usize = one::ONE + one::ONE;", + }, + "b": { + "three.rs": "const THREE: usize = one::ONE + two::TWO;", + "four.rs": "const FOUR: usize = one::ONE + three::THREE;", + }, + }), + ) + .await; + let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + let worktree_id = project.read_with(cx, |project, cx| { + project.worktrees(cx).next().unwrap().read(cx).id() + }); + let workspace = cx + .add_window(|cx| Workspace::test_new(project, cx)) + .root(cx); + + let active_item = cx.read(|cx| { + workspace + .read(cx) + .active_pane() + .read(cx) + .active_item() + .and_then(|item| item.downcast::()) + }); + assert!( + active_item.is_none(), + "Expected no search panel to be active, but got: {active_item:?}" + ); + + let one_file_entry = cx.update(|cx| { + workspace + .read(cx) + .project() + .read(cx) + .entry_for_path(&(worktree_id, "a/one.rs").into(), cx) + .expect("no entry for /a/one.rs file") + }); + assert!(one_file_entry.is_file()); + workspace.update(cx, |workspace, cx| { + ProjectSearchView::new_search_in_directory(workspace, &one_file_entry, cx) + }); + let active_search_entry = cx.read(|cx| { + workspace + .read(cx) + .active_pane() + .read(cx) + .active_item() + .and_then(|item| item.downcast::()) + }); + assert!( + active_search_entry.is_none(), + "Expected no search panel to be active for file entry" + ); + + let a_dir_entry = cx.update(|cx| { + workspace + .read(cx) + .project() + .read(cx) + .entry_for_path(&(worktree_id, "a").into(), cx) + .expect("no entry for /a/ directory") + }); + assert!(a_dir_entry.is_dir()); + workspace.update(cx, |workspace, cx| { + ProjectSearchView::new_search_in_directory(workspace, &a_dir_entry, cx) + }); + + let Some(search_view) = cx.read(|cx| { + workspace + .read(cx) + .active_pane() + .read(cx) + .active_item() + .and_then(|item| item.downcast::()) + }) else { + panic!("Search view expected to appear after new search in directory event trigger") + }; + deterministic.run_until_parked(); + search_view.update(cx, |search_view, cx| { + assert!( + search_view.query_editor.is_focused(cx), + "On new search in directory, focus should be moved into query editor" + ); + search_view.excluded_files_editor.update(cx, |editor, cx| { + assert!( + editor.display_text(cx).is_empty(), + "New search in directory should not have any excluded files" + ); + }); + search_view.included_files_editor.update(cx, |editor, cx| { + assert_eq!( + editor.display_text(cx), + a_dir_entry.path.to_str().unwrap(), + "New search in directory should have included dir entry path" + ); + }); + }); + + search_view.update(cx, |search_view, cx| { + search_view + .query_editor + .update(cx, |query_editor, cx| query_editor.set_text("const", cx)); + search_view.search(cx); + }); + deterministic.run_until_parked(); + search_view.update(cx, |search_view, cx| { + assert_eq!( + search_view + .results_editor + .update(cx, |editor, cx| editor.display_text(cx)), + "\n\nconst ONE: usize = 1;\n\n\nconst TWO: usize = one::ONE + one::ONE;", + "New search in directory should have a filter that matches a certain directory" + ); + }); + } + + #[gpui::test] + async fn test_search_query_history(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/dir", + json!({ + "one.rs": "const ONE: usize = 1;", + "two.rs": "const TWO: usize = one::ONE + one::ONE;", + "three.rs": "const THREE: usize = one::ONE + two::TWO;", + "four.rs": "const FOUR: usize = one::ONE + three::THREE;", + }), + ) + .await; + let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + let window = cx.add_window(|cx| Workspace::test_new(project, cx)); + let workspace = window.root(cx); + workspace.update(cx, |workspace, cx| { + ProjectSearchView::deploy(workspace, &workspace::NewSearch, cx) + }); + + let search_view = cx.read(|cx| { + workspace + .read(cx) + .active_pane() + .read(cx) + .active_item() + .and_then(|item| item.downcast::()) + .expect("Search view expected to appear after new search event trigger") + }); + + let search_bar = window.add_view(cx, |cx| { + let mut search_bar = ProjectSearchBar::new(); + search_bar.set_active_pane_item(Some(&search_view), cx); + // search_bar.show(cx); + search_bar + }); + + // Add 3 search items into the history + another unsubmitted one. + search_view.update(cx, |search_view, cx| { + search_view.search_options = SearchOptions::CASE_SENSITIVE; + search_view + .query_editor + .update(cx, |query_editor, cx| query_editor.set_text("ONE", cx)); + search_view.search(cx); + }); + cx.foreground().run_until_parked(); + search_view.update(cx, |search_view, cx| { + search_view + .query_editor + .update(cx, |query_editor, cx| query_editor.set_text("TWO", cx)); + search_view.search(cx); + }); + cx.foreground().run_until_parked(); + search_view.update(cx, |search_view, cx| { + search_view + .query_editor + .update(cx, |query_editor, cx| query_editor.set_text("THREE", cx)); + search_view.search(cx); + }); + cx.foreground().run_until_parked(); + search_view.update(cx, |search_view, cx| { + search_view.query_editor.update(cx, |query_editor, cx| { + query_editor.set_text("JUST_TEXT_INPUT", cx) + }); + }); + cx.foreground().run_until_parked(); + + // Ensure that the latest input with search settings is active. + search_view.update(cx, |search_view, cx| { + assert_eq!( + search_view.query_editor.read(cx).text(cx), + "JUST_TEXT_INPUT" + ); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + + // Next history query after the latest should set the query to the empty string. + search_bar.update(cx, |search_bar, cx| { + search_bar.next_history_query(&NextHistoryQuery, cx); + }); + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), ""); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + search_bar.update(cx, |search_bar, cx| { + search_bar.next_history_query(&NextHistoryQuery, cx); + }); + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), ""); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + + // First previous query for empty current query should set the query to the latest submitted one. + search_bar.update(cx, |search_bar, cx| { + search_bar.previous_history_query(&PreviousHistoryQuery, cx); + }); + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), "THREE"); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + + // Further previous items should go over the history in reverse order. + search_bar.update(cx, |search_bar, cx| { + search_bar.previous_history_query(&PreviousHistoryQuery, cx); + }); + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO"); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + + // Previous items should never go behind the first history item. + search_bar.update(cx, |search_bar, cx| { + search_bar.previous_history_query(&PreviousHistoryQuery, cx); + }); + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), "ONE"); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + search_bar.update(cx, |search_bar, cx| { + search_bar.previous_history_query(&PreviousHistoryQuery, cx); + }); + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), "ONE"); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + + // Next items should go over the history in the original order. + search_bar.update(cx, |search_bar, cx| { + search_bar.next_history_query(&NextHistoryQuery, cx); + }); + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO"); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + + search_view.update(cx, |search_view, cx| { + search_view + .query_editor + .update(cx, |query_editor, cx| query_editor.set_text("TWO_NEW", cx)); + search_view.search(cx); + }); + cx.foreground().run_until_parked(); + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO_NEW"); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + + // New search input should add another entry to history and move the selection to the end of the history. + search_bar.update(cx, |search_bar, cx| { + search_bar.previous_history_query(&PreviousHistoryQuery, cx); + }); + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), "THREE"); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + search_bar.update(cx, |search_bar, cx| { + search_bar.previous_history_query(&PreviousHistoryQuery, cx); + }); + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO"); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + search_bar.update(cx, |search_bar, cx| { + search_bar.next_history_query(&NextHistoryQuery, cx); + }); + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), "THREE"); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + search_bar.update(cx, |search_bar, cx| { + search_bar.next_history_query(&NextHistoryQuery, cx); + }); + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO_NEW"); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + search_bar.update(cx, |search_bar, cx| { + search_bar.next_history_query(&NextHistoryQuery, cx); + }); + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), ""); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + } + + pub fn init_test(cx: &mut TestAppContext) { + cx.foreground().forbid_parking(); + let fonts = cx.font_cache(); + let mut theme = gpui::fonts::with_font_cache(fonts.clone(), theme::Theme::default); + theme.search.match_background = Color::red(); + + cx.update(|cx| { + cx.set_global(SettingsStore::test(cx)); + cx.set_global(ActiveSearches::default()); + settings::register::(cx); + + theme::init((), cx); + cx.update_global::(|store, _| { + let mut settings = store.get::(None).clone(); + settings.theme = Arc::new(theme); + store.override_global(settings) + }); + + language::init(cx); + client::init_settings(cx); + editor::init(cx); + workspace::init_settings(cx); + Project::init_settings(cx); + super::init(cx); + }); + } +} diff --git a/crates/search2/src/search.rs b/crates/search2/src/search.rs new file mode 100644 index 0000000000000000000000000000000000000000..12152701bc69fb9dce23ce00267dd4bea55dcb19 --- /dev/null +++ b/crates/search2/src/search.rs @@ -0,0 +1,117 @@ +use bitflags::bitflags; +pub use buffer_search::BufferSearchBar; +use gpui::{actions, Action, AppContext, RenderOnce}; +pub use mode::SearchMode; +use project::search::SearchQuery; +use ui::ButtonVariant; +//pub use project_search::{ProjectSearchBar, ProjectSearchView}; +// use theme::components::{ +// action_button::Button, svg::Svg, ComponentExt, IconButtonStyle, ToggleIconButtonStyle, +// }; + +pub mod buffer_search; +mod history; +mod mode; +//pub mod project_search; +pub(crate) mod search_bar; + +pub fn init(cx: &mut AppContext) { + buffer_search::init(cx); + //project_search::init(cx); +} + +actions!( + CycleMode, + ToggleWholeWord, + ToggleCaseSensitive, + ToggleReplace, + SelectNextMatch, + SelectPrevMatch, + SelectAllMatches, + NextHistoryQuery, + PreviousHistoryQuery, + ActivateTextMode, + ActivateSemanticMode, + ActivateRegexMode, + ReplaceAll, + ReplaceNext, +); + +bitflags! { + #[derive(Default)] + pub struct SearchOptions: u8 { + const NONE = 0b000; + const WHOLE_WORD = 0b001; + const CASE_SENSITIVE = 0b010; + } +} + +impl SearchOptions { + pub fn label(&self) -> &'static str { + match *self { + SearchOptions::WHOLE_WORD => "Match Whole Word", + SearchOptions::CASE_SENSITIVE => "Match Case", + _ => panic!("{:?} is not a named SearchOption", self), + } + } + + pub fn icon(&self) -> ui::Icon { + match *self { + SearchOptions::WHOLE_WORD => ui::Icon::WholeWord, + SearchOptions::CASE_SENSITIVE => ui::Icon::CaseSensitive, + _ => panic!("{:?} is not a named SearchOption", self), + } + } + + pub fn to_toggle_action(&self) -> Box { + match *self { + SearchOptions::WHOLE_WORD => Box::new(ToggleWholeWord), + SearchOptions::CASE_SENSITIVE => Box::new(ToggleCaseSensitive), + _ => panic!("{:?} is not a named SearchOption", self), + } + } + + pub fn none() -> SearchOptions { + SearchOptions::NONE + } + + pub fn from_query(query: &SearchQuery) -> SearchOptions { + let mut options = SearchOptions::NONE; + options.set(SearchOptions::WHOLE_WORD, query.whole_word()); + options.set(SearchOptions::CASE_SENSITIVE, query.case_sensitive()); + options + } + + pub fn as_button(&self, active: bool) -> impl RenderOnce { + ui::IconButton::new(0, self.icon()) + .on_click({ + let action = self.to_toggle_action(); + move |_, cx| { + cx.dispatch_action(action.boxed_clone()); + } + }) + .variant(ui::ButtonVariant::Ghost) + .when(active, |button| button.variant(ButtonVariant::Filled)) + } +} + +fn toggle_replace_button(active: bool) -> impl RenderOnce { + // todo: add toggle_replace button + ui::IconButton::new(0, ui::Icon::Replace) + .on_click(|_, cx| { + cx.dispatch_action(Box::new(ToggleReplace)); + cx.notify(); + }) + .variant(ui::ButtonVariant::Ghost) + .when(active, |button| button.variant(ButtonVariant::Filled)) +} + +fn render_replace_button( + action: impl Action + 'static + Send + Sync, + icon: ui::Icon, +) -> impl RenderOnce { + // todo: add tooltip + ui::IconButton::new(0, icon).on_click(move |_, cx| { + cx.dispatch_action(action.boxed_clone()); + }) +} diff --git a/crates/search2/src/search_bar.rs b/crates/search2/src/search_bar.rs new file mode 100644 index 0000000000000000000000000000000000000000..da097b43a66207b62fc914c1e11caf3c4eccd81e --- /dev/null +++ b/crates/search2/src/search_bar.rs @@ -0,0 +1,35 @@ +use gpui::{MouseDownEvent, RenderOnce, WindowContext}; +use ui::{Button, ButtonVariant, IconButton}; + +use crate::mode::SearchMode; + +pub(super) fn render_nav_button( + icon: ui::Icon, + _active: bool, + on_click: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static, +) -> impl RenderOnce { + // let tooltip_style = cx.theme().tooltip.clone(); + // let cursor_style = if active { + // CursorStyle::PointingHand + // } else { + // CursorStyle::default() + // }; + // enum NavButton {} + IconButton::new("search-nav-button", icon).on_click(on_click) +} + +pub(crate) fn render_search_mode_button( + mode: SearchMode, + is_active: bool, + on_click: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static, +) -> Button { + let button_variant = if is_active { + ButtonVariant::Filled + } else { + ButtonVariant::Ghost + }; + + Button::new(mode.label()) + .on_click(on_click) + .variant(button_variant) +} diff --git a/crates/story/Cargo.toml b/crates/story/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..384447af8fe51472a7da9b614e23b364bb664d25 --- /dev/null +++ b/crates/story/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "story" +version = "0.1.0" +edition = "2021" +publish = false + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +gpui = { package = "gpui2", path = "../gpui2" } diff --git a/crates/story/src/lib.rs b/crates/story/src/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..a9998e65a81010bc4300f6063a1deab8f08deab5 --- /dev/null +++ b/crates/story/src/lib.rs @@ -0,0 +1,3 @@ +mod story; + +pub use story::*; diff --git a/crates/story/src/story.rs b/crates/story/src/story.rs new file mode 100644 index 0000000000000000000000000000000000000000..d95c879ce0068c00fd24ffb64179d4e3d46a5c7a --- /dev/null +++ b/crates/story/src/story.rs @@ -0,0 +1,35 @@ +use gpui::prelude::*; +use gpui::{div, hsla, Div, SharedString}; + +pub struct Story {} + +impl Story { + pub fn container() -> Div { + div().size_full().flex().flex_col().pt_2().px_4().bg(hsla( + 0. / 360., + 0. / 100., + 100. / 100., + 1., + )) + } + + pub fn title(title: impl Into) -> impl Element { + div() + .text_xl() + .text_color(hsla(0. / 360., 0. / 100., 0. / 100., 1.)) + .child(title.into()) + } + + pub fn title_for() -> impl Element { + Self::title(std::any::type_name::()) + } + + pub fn label(label: impl Into) -> impl Element { + div() + .mt_4() + .mb_2() + .text_xs() + .text_color(hsla(0. / 360., 0. / 100., 0. / 100., 1.)) + .child(label.into()) + } +} diff --git a/crates/storybook2/Cargo.toml b/crates/storybook2/Cargo.toml index 7c6776c9309fd68352b7e18b846af8cd4e8fbdae..16386706cfc70ff1325bf8048c4c4625ba4c907e 100644 --- a/crates/storybook2/Cargo.toml +++ b/crates/storybook2/Cargo.toml @@ -25,6 +25,7 @@ serde.workspace = true settings2 = { path = "../settings2" } simplelog = "0.9" smallvec.workspace = true +story = { path = "../story" } strum = { version = "0.25.0", features = ["derive"] } theme = { path = "../theme" } theme2 = { path = "../theme2" } diff --git a/crates/storybook2/src/stories.rs b/crates/storybook2/src/stories.rs index 2620e68d6c102600637f41551e5d6f65b96451e1..0eaf3d126c216650161896fa28cdb404aa1e30ac 100644 --- a/crates/storybook2/src/stories.rs +++ b/crates/storybook2/src/stories.rs @@ -1,4 +1,3 @@ -mod colors; mod focus; mod kitchen_sink; mod picker; @@ -6,7 +5,6 @@ mod scroll; mod text; mod z_index; -pub use colors::*; pub use focus::*; pub use kitchen_sink::*; pub use picker::*; diff --git a/crates/storybook2/src/stories/colors.rs b/crates/storybook2/src/stories/colors.rs deleted file mode 100644 index b690435e01417a2ced66a685f85db4fd9c0cfba1..0000000000000000000000000000000000000000 --- a/crates/storybook2/src/stories/colors.rs +++ /dev/null @@ -1,44 +0,0 @@ -use crate::story::Story; -use gpui::{prelude::*, px, Div, Render}; -use theme2::{default_color_scales, ColorScaleStep}; -use ui::prelude::*; - -pub struct ColorsStory; - -impl Render for ColorsStory { - type Element = Div; - - fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - let color_scales = default_color_scales(); - - Story::container(cx) - .child(Story::title(cx, "Colors")) - .child( - div() - .id("colors") - .flex() - .flex_col() - .gap_1() - .overflow_y_scroll() - .text_color(gpui::white()) - .children(color_scales.into_iter().map(|scale| { - div() - .flex() - .child( - div() - .w(px(75.)) - .line_height(px(24.)) - .child(scale.name().clone()), - ) - .child( - div() - .flex() - .gap_1() - .children(ColorScaleStep::ALL.map(|step| { - div().flex().size_6().bg(scale.step(cx, step)) - })), - ) - })), - ) - } -} diff --git a/crates/storybook2/src/stories/focus.rs b/crates/storybook2/src/stories/focus.rs index 12c7ea81a063181791b0873500242f56a36337b8..6f757240ebe21df85f12e0958946f34ab06797e0 100644 --- a/crates/storybook2/src/stories/focus.rs +++ b/crates/storybook2/src/stories/focus.rs @@ -26,8 +26,8 @@ impl FocusStory { } } -impl Render for FocusStory { - type Element = Focusable>>; +impl Render for FocusStory { + type Element = Focusable>; fn render(&mut self, cx: &mut gpui::ViewContext) -> Self::Element { let theme = cx.theme(); @@ -42,18 +42,18 @@ impl Render for FocusStory { .id("parent") .focusable() .key_context("parent") - .on_action(|_, action: &ActionA, cx| { + .on_action(cx.listener(|_, action: &ActionA, cx| { println!("Action A dispatched on parent"); - }) - .on_action(|_, action: &ActionB, cx| { + })) + .on_action(cx.listener(|_, action: &ActionB, cx| { println!("Action B dispatched on parent"); - }) - .on_focus(|_, _, _| println!("Parent focused")) - .on_blur(|_, _, _| println!("Parent blurred")) - .on_focus_in(|_, _, _| println!("Parent focus_in")) - .on_focus_out(|_, _, _| println!("Parent focus_out")) - .on_key_down(|_, event, phase, _| println!("Key down on parent {:?}", event)) - .on_key_up(|_, event, phase, _| println!("Key up on parent {:?}", event)) + })) + .on_focus(cx.listener(|_, _, _| println!("Parent focused"))) + .on_blur(cx.listener(|_, _, _| println!("Parent blurred"))) + .on_focus_in(cx.listener(|_, _, _| println!("Parent focus_in"))) + .on_focus_out(cx.listener(|_, _, _| println!("Parent focus_out"))) + .on_key_down(cx.listener(|_, event, _| println!("Key down on parent {:?}", event))) + .on_key_up(cx.listener(|_, event, _| println!("Key up on parent {:?}", event))) .size_full() .bg(color_1) .focus(|style| style.bg(color_2)) @@ -61,38 +61,42 @@ impl Render for FocusStory { div() .track_focus(&self.child_1_focus) .key_context("child-1") - .on_action(|_, action: &ActionB, cx| { + .on_action(cx.listener(|_, action: &ActionB, cx| { println!("Action B dispatched on child 1 during"); - }) + })) .w_full() .h_6() .bg(color_4) .focus(|style| style.bg(color_5)) .in_focus(|style| style.bg(color_6)) - .on_focus(|_, _, _| println!("Child 1 focused")) - .on_blur(|_, _, _| println!("Child 1 blurred")) - .on_focus_in(|_, _, _| println!("Child 1 focus_in")) - .on_focus_out(|_, _, _| println!("Child 1 focus_out")) - .on_key_down(|_, event, phase, _| println!("Key down on child 1 {:?}", event)) - .on_key_up(|_, event, phase, _| println!("Key up on child 1 {:?}", event)) + .on_focus(cx.listener(|_, _, _| println!("Child 1 focused"))) + .on_blur(cx.listener(|_, _, _| println!("Child 1 blurred"))) + .on_focus_in(cx.listener(|_, _, _| println!("Child 1 focus_in"))) + .on_focus_out(cx.listener(|_, _, _| println!("Child 1 focus_out"))) + .on_key_down( + cx.listener(|_, event, _| println!("Key down on child 1 {:?}", event)), + ) + .on_key_up(cx.listener(|_, event, _| println!("Key up on child 1 {:?}", event))) .child("Child 1"), ) .child( div() .track_focus(&self.child_2_focus) .key_context("child-2") - .on_action(|_, action: &ActionC, cx| { + .on_action(cx.listener(|_, action: &ActionC, cx| { println!("Action C dispatched on child 2"); - }) + })) .w_full() .h_6() .bg(color_4) - .on_focus(|_, _, _| println!("Child 2 focused")) - .on_blur(|_, _, _| println!("Child 2 blurred")) - .on_focus_in(|_, _, _| println!("Child 2 focus_in")) - .on_focus_out(|_, _, _| println!("Child 2 focus_out")) - .on_key_down(|_, event, phase, _| println!("Key down on child 2 {:?}", event)) - .on_key_up(|_, event, phase, _| println!("Key up on child 2 {:?}", event)) + .on_focus(cx.listener(|_, _, _| println!("Child 2 focused"))) + .on_blur(cx.listener(|_, _, _| println!("Child 2 blurred"))) + .on_focus_in(cx.listener(|_, _, _| println!("Child 2 focus_in"))) + .on_focus_out(cx.listener(|_, _, _| println!("Child 2 focus_out"))) + .on_key_down( + cx.listener(|_, event, _| println!("Key down on child 2 {:?}", event)), + ) + .on_key_up(cx.listener(|_, event, _| println!("Key up on child 2 {:?}", event))) .child("Child 2"), ) } diff --git a/crates/storybook2/src/stories/kitchen_sink.rs b/crates/storybook2/src/stories/kitchen_sink.rs index 2d31cefed6e15cf51a7e368ef6b1aab99b11639b..f79a27aa89f875ae10cfdd12fd15b13192459a54 100644 --- a/crates/storybook2/src/stories/kitchen_sink.rs +++ b/crates/storybook2/src/stories/kitchen_sink.rs @@ -1,8 +1,10 @@ -use crate::{story::Story, story_selector::ComponentStory}; use gpui::{prelude::*, Div, Render, Stateful, View}; +use story::Story; use strum::IntoEnumIterator; use ui::prelude::*; +use crate::story_selector::ComponentStory; + pub struct KitchenSinkStory; impl KitchenSinkStory { @@ -11,19 +13,19 @@ impl KitchenSinkStory { } } -impl Render for KitchenSinkStory { - type Element = Stateful>; +impl Render for KitchenSinkStory { + type Element = Stateful
; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { let component_stories = ComponentStory::iter() .map(|selector| selector.story(cx)) .collect::>(); - Story::container(cx) + Story::container() .id("kitchen-sink") .overflow_y_scroll() - .child(Story::title(cx, "Kitchen Sink")) - .child(Story::label(cx, "Components")) + .child(Story::title("Kitchen Sink")) + .child(Story::label("Components")) .child(div().flex().flex_col().children(component_stories)) // Add a bit of space at the bottom of the kitchen sink so elements // don't end up squished right up against the bottom of the screen. diff --git a/crates/storybook2/src/stories/picker.rs b/crates/storybook2/src/stories/picker.rs index 7c2412a02ff2cccd2316aa06e65a7b57a79541a5..ae6a26161bd3a587c54aa7231336d40428dbbe69 100644 --- a/crates/storybook2/src/stories/picker.rs +++ b/crates/storybook2/src/stories/picker.rs @@ -36,7 +36,7 @@ impl Delegate { } impl PickerDelegate for Delegate { - type ListItem = Div>; + type ListItem = Div; fn match_count(&self) -> usize { self.candidates.len() @@ -205,8 +205,8 @@ impl PickerStory { } } -impl Render for PickerStory { - type Element = Div; +impl Render for PickerStory { + type Element = Div; fn render(&mut self, cx: &mut gpui::ViewContext) -> Self::Element { div() diff --git a/crates/storybook2/src/stories/scroll.rs b/crates/storybook2/src/stories/scroll.rs index bbab0b1d110a33781447f500c14a01df0ea776e8..9b9a54e1e6736e05183654c51f4fff7ccc69b968 100644 --- a/crates/storybook2/src/stories/scroll.rs +++ b/crates/storybook2/src/stories/scroll.rs @@ -10,8 +10,8 @@ impl ScrollStory { } } -impl Render for ScrollStory { - type Element = Stateful>; +impl Render for ScrollStory { + type Element = Stateful
; fn render(&mut self, cx: &mut gpui::ViewContext) -> Self::Element { let theme = cx.theme(); @@ -38,7 +38,7 @@ impl Render for ScrollStory { }; div() .id(id) - .tooltip(move |_, cx| Tooltip::text(format!("{}, {}", row, column), cx)) + .tooltip(move |cx| Tooltip::text(format!("{}, {}", row, column), cx)) .bg(bg) .size(px(100. as f32)) .when(row >= 5 && column >= 5, |d| { diff --git a/crates/storybook2/src/stories/text.rs b/crates/storybook2/src/stories/text.rs index 716004dea7132f87c16b6aebe393aa8d662d7cca..c26e5fd3f1adc78aa3e9e8bf99f8c8f26666db78 100644 --- a/crates/storybook2/src/stories/text.rs +++ b/crates/storybook2/src/stories/text.rs @@ -11,8 +11,8 @@ impl TextStory { } } -impl Render for TextStory { - type Element = Div; +impl Render for TextStory { + type Element = Div; fn render(&mut self, cx: &mut gpui::ViewContext) -> Self::Element { v_stack() diff --git a/crates/storybook2/src/stories/z_index.rs b/crates/storybook2/src/stories/z_index.rs index 087ed913fd711c3e101efb1d400cd7dacfbd2556..4f551c8ea0e761000550b2b4b2cbf967d2a1b7e9 100644 --- a/crates/storybook2/src/stories/z_index.rs +++ b/crates/storybook2/src/stories/z_index.rs @@ -1,52 +1,49 @@ use gpui::{px, rgb, Div, Hsla, Render, RenderOnce}; +use story::Story; use ui::prelude::*; -use crate::story::Story; - /// A reimplementation of the MDN `z-index` example, found here: /// [https://developer.mozilla.org/en-US/docs/Web/CSS/z-index](https://developer.mozilla.org/en-US/docs/Web/CSS/z-index). pub struct ZIndexStory; -impl Render for ZIndexStory { - type Element = Div; +impl Render for ZIndexStory { + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - Story::container(cx) - .child(Story::title(cx, "z-index")) - .child( - div() - .flex() - .child( - div() - .w(px(250.)) - .child(Story::label(cx, "z-index: auto")) - .child(ZIndexExample::new(0)), - ) - .child( - div() - .w(px(250.)) - .child(Story::label(cx, "z-index: 1")) - .child(ZIndexExample::new(1)), - ) - .child( - div() - .w(px(250.)) - .child(Story::label(cx, "z-index: 3")) - .child(ZIndexExample::new(3)), - ) - .child( - div() - .w(px(250.)) - .child(Story::label(cx, "z-index: 5")) - .child(ZIndexExample::new(5)), - ) - .child( - div() - .w(px(250.)) - .child(Story::label(cx, "z-index: 7")) - .child(ZIndexExample::new(7)), - ), - ) + Story::container().child(Story::title("z-index")).child( + div() + .flex() + .child( + div() + .w(px(250.)) + .child(Story::label("z-index: auto")) + .child(ZIndexExample::new(0)), + ) + .child( + div() + .w(px(250.)) + .child(Story::label("z-index: 1")) + .child(ZIndexExample::new(1)), + ) + .child( + div() + .w(px(250.)) + .child(Story::label("z-index: 3")) + .child(ZIndexExample::new(3)), + ) + .child( + div() + .w(px(250.)) + .child(Story::label("z-index: 5")) + .child(ZIndexExample::new(5)), + ) + .child( + div() + .w(px(250.)) + .child(Story::label("z-index: 7")) + .child(ZIndexExample::new(7)), + ), + ) } } @@ -77,17 +74,17 @@ trait Styles: Styled + Sized { } } -impl Styles for Div {} +impl Styles for Div {} #[derive(RenderOnce)] struct ZIndexExample { z_index: u32, } -impl Component for ZIndexExample { - type Rendered = Div; +impl Component for ZIndexExample { + type Rendered = Div; - fn render(self, view: &mut V, cx: &mut ViewContext) -> Self::Rendered { + fn render(self, cx: &mut WindowContext) -> Self::Rendered { div() .relative() .size_full() diff --git a/crates/storybook2/src/story.rs b/crates/storybook2/src/story.rs deleted file mode 100644 index 5c144fdbc102a7754aa1f585f00a6ece2f985b54..0000000000000000000000000000000000000000 --- a/crates/storybook2/src/story.rs +++ /dev/null @@ -1 +0,0 @@ -pub use ui::Story; diff --git a/crates/storybook2/src/story_selector.rs b/crates/storybook2/src/story_selector.rs index 040bd75189104aeb8d1a5437e96b7a3935d9efcc..71d7f80a8e44c90a32dd9b2b550c2b8d858f706d 100644 --- a/crates/storybook2/src/story_selector.rs +++ b/crates/storybook2/src/story_selector.rs @@ -8,49 +8,22 @@ use clap::ValueEnum; use gpui::{AnyView, VisualContext}; use strum::{EnumIter, EnumString, IntoEnumIterator}; use ui::prelude::*; -use ui::{AvatarStory, ButtonStory, DetailsStory, IconStory, InputStory, LabelStory}; +use ui::{AvatarStory, ButtonStory, IconStory, InputStory, LabelStory}; #[derive(Debug, PartialEq, Eq, Clone, Copy, strum::Display, EnumString, EnumIter)] #[strum(serialize_all = "snake_case")] pub enum ComponentStory { - AssistantPanel, Avatar, - Breadcrumb, - Buffer, Button, - ChatPanel, Checkbox, - CollabPanel, - Colors, - CommandPalette, ContextMenu, - Copilot, - Details, - Facepile, Focus, Icon, Input, Keybinding, Label, - LanguageSelector, - MultiBuffer, - NotificationsPanel, - Palette, - Panel, - ProjectPanel, - Players, - RecentProjects, Scroll, - Tab, - TabBar, - Terminal, Text, - ThemeSelector, - TitleBar, - Toast, - Toolbar, - TrafficLights, - Workspace, ZIndex, Picker, } @@ -58,44 +31,17 @@ pub enum ComponentStory { impl ComponentStory { pub fn story(&self, cx: &mut WindowContext) -> AnyView { match self { - Self::AssistantPanel => cx.build_view(|_| ui::AssistantPanelStory).into(), Self::Avatar => cx.build_view(|_| AvatarStory).into(), - Self::Breadcrumb => cx.build_view(|_| ui::BreadcrumbStory).into(), - Self::Buffer => cx.build_view(|_| ui::BufferStory).into(), Self::Button => cx.build_view(|_| ButtonStory).into(), - Self::ChatPanel => cx.build_view(|_| ui::ChatPanelStory).into(), Self::Checkbox => cx.build_view(|_| ui::CheckboxStory).into(), - Self::CollabPanel => cx.build_view(|_| ui::CollabPanelStory).into(), - Self::Colors => cx.build_view(|_| ColorsStory).into(), - Self::CommandPalette => cx.build_view(|_| ui::CommandPaletteStory).into(), Self::ContextMenu => cx.build_view(|_| ui::ContextMenuStory).into(), - Self::Copilot => cx.build_view(|_| ui::CopilotModalStory).into(), - Self::Details => cx.build_view(|_| DetailsStory).into(), - Self::Facepile => cx.build_view(|_| ui::FacepileStory).into(), Self::Focus => FocusStory::view(cx).into(), Self::Icon => cx.build_view(|_| IconStory).into(), Self::Input => cx.build_view(|_| InputStory).into(), Self::Keybinding => cx.build_view(|_| ui::KeybindingStory).into(), Self::Label => cx.build_view(|_| LabelStory).into(), - Self::LanguageSelector => cx.build_view(|_| ui::LanguageSelectorStory).into(), - Self::MultiBuffer => cx.build_view(|_| ui::MultiBufferStory).into(), - Self::NotificationsPanel => cx.build_view(|cx| ui::NotificationsPanelStory).into(), - Self::Palette => cx.build_view(|cx| ui::PaletteStory).into(), - Self::Players => cx.build_view(|_| theme2::PlayerStory).into(), - Self::Panel => cx.build_view(|cx| ui::PanelStory).into(), - Self::ProjectPanel => cx.build_view(|_| ui::ProjectPanelStory).into(), - Self::RecentProjects => cx.build_view(|_| ui::RecentProjectsStory).into(), Self::Scroll => ScrollStory::view(cx).into(), - Self::Tab => cx.build_view(|_| ui::TabStory).into(), - Self::TabBar => cx.build_view(|_| ui::TabBarStory).into(), - Self::Terminal => cx.build_view(|_| ui::TerminalStory).into(), Self::Text => TextStory::view(cx).into(), - Self::ThemeSelector => cx.build_view(|_| ui::ThemeSelectorStory).into(), - Self::TitleBar => ui::TitleBarStory::view(cx).into(), - Self::Toast => cx.build_view(|_| ui::ToastStory).into(), - Self::Toolbar => cx.build_view(|_| ui::ToolbarStory).into(), - Self::TrafficLights => cx.build_view(|_| ui::TrafficLightsStory).into(), - Self::Workspace => ui::WorkspaceStory::view(cx).into(), Self::ZIndex => cx.build_view(|_| ZIndexStory).into(), Self::Picker => PickerStory::new(cx).into(), } diff --git a/crates/storybook2/src/storybook2.rs b/crates/storybook2/src/storybook2.rs index 2a22d91382c45cc2201725f35d32ee9cccc62297..2a62c135b186c85910f497b33b82bbcd46739768 100644 --- a/crates/storybook2/src/storybook2.rs +++ b/crates/storybook2/src/storybook2.rs @@ -2,7 +2,6 @@ mod assets; mod stories; -mod story; mod story_selector; use std::sync::Arc; @@ -15,7 +14,6 @@ use gpui::{ use log::LevelFilter; use settings2::{default_settings, Settings, SettingsStore}; use simplelog::SimpleLogger; -use story_selector::ComponentStory; use theme2::{ThemeRegistry, ThemeSettings}; use ui::prelude::*; @@ -62,15 +60,13 @@ fn main() { theme2::init(theme2::LoadThemes::All, cx); - let selector = - story_selector.unwrap_or(StorySelector::Component(ComponentStory::Workspace)); + let selector = story_selector.unwrap_or(StorySelector::KitchenSink); 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); - ui::settings::init(cx); language::init(cx); editor::init(cx); @@ -105,8 +101,8 @@ impl StoryWrapper { } } -impl Render for StoryWrapper { - type Element = Div; +impl Render for StoryWrapper { + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { div() diff --git a/crates/storybook3/Cargo.toml b/crates/storybook3/Cargo.toml deleted file mode 100644 index 8b04e4d44b306969de63a7bd880d61b724aed32b..0000000000000000000000000000000000000000 --- a/crates/storybook3/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -[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 deleted file mode 100644 index cb64bd7f0dfabdf11724bf09cc0b6cff3846d34b..0000000000000000000000000000000000000000 --- a/crates/storybook3/src/storybook3.rs +++ /dev/null @@ -1,87 +0,0 @@ -use anyhow::Result; -use gpui::{ - div, px, size, AnyView, Bounds, Div, Render, ViewContext, VisualContext, WindowBounds, - WindowOptions, -}; -use gpui::{white, AssetSource}; -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(theme::LoadThemes::JustBase, cx); - - cx.open_window( - WindowOptions { - bounds: WindowBounds::Fixed(Bounds { - origin: Default::default(), - size: size(px(1500.), px(780.)).into(), - }), - ..Default::default() - }, - move |cx| { - let ui_font_size = ThemeSettings::get_global(cx).ui_font_size; - cx.set_rem_size(ui_font_size); - - cx.build_view(|cx| TestView { - story: cx.build_view(|_| ContextMenuStory).into(), - }) - }, - ); - - cx.activate(true); - }) -} - -struct TestView { - #[allow(unused)] - story: AnyView, -} - -impl Render for TestView { - type Element = Div; - - fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { - div() - .flex() - .bg(gpui::blue()) - .flex_col() - .size_full() - .font("Helvetica") - .child(div().h_5()) - .child( - div() - .flex() - .w_96() - .bg(white()) - .relative() - .child(div().child(concat!( - "The quick brown fox jumps over the lazy dog. ", - "Meanwhile, the lazy dog decided it was time for a change. ", - "He started daily workout routines, ate healthier and became the fastest dog in town.", - ))), - ) - } -} diff --git a/crates/terminal_view2/src/terminal_panel.rs b/crates/terminal_view2/src/terminal_panel.rs index 46885913ed97e29e8ec948d9475cae80a8a3a32e..b6582b07b194331f0e33f7e5b6b70557bfa50cbc 100644 --- a/crates/terminal_view2/src/terminal_panel.rs +++ b/crates/terminal_view2/src/terminal_panel.rs @@ -335,8 +335,8 @@ impl TerminalPanel { impl EventEmitter for TerminalPanel {} -impl Render for TerminalPanel { - type Element = Div; +impl Render for TerminalPanel { + type Element = Div; fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { div().child(self.pane.clone()) diff --git a/crates/terminal_view2/src/terminal_view.rs b/crates/terminal_view2/src/terminal_view.rs index 330e289aa8292ec3083532bd44111907d9d7b03a..9f3ed313880989631c25b7cb7f9fe1460c9224c3 100644 --- a/crates/terminal_view2/src/terminal_view.rs +++ b/crates/terminal_view2/src/terminal_view.rs @@ -9,10 +9,10 @@ pub mod terminal_panel; // use crate::terminal_element::TerminalElement; use editor::{scroll::autoscroll::Autoscroll, Editor}; use gpui::{ - actions, div, Action, AnyElement, AppContext, DispatchPhase, Div, Element, EventEmitter, - FocusEvent, FocusHandle, Focusable, FocusableElement, FocusableView, InputHandler, - InteractiveElement, KeyDownEvent, Keystroke, Model, MouseButton, ParentElement, Pixels, Render, - SharedString, Styled, Task, View, ViewContext, VisualContext, WeakView, + actions, div, Action, AnyElement, AppContext, Div, Element, EventEmitter, FocusEvent, + FocusHandle, Focusable, FocusableElement, FocusableView, InputHandler, InteractiveElement, + KeyDownEvent, Keystroke, Model, MouseButton, MouseDownEvent, ParentElement, Pixels, Render, + SharedString, Styled, Task, View, ViewContext, VisualContext, WeakView, WindowContext, }; use language::Bias; use persistence::TERMINAL_DB; @@ -31,7 +31,7 @@ use workspace::{ notifications::NotifyResultExt, register_deserializable_item, searchable::{SearchEvent, SearchOptions, SearchableItem}, - ui::{ContextMenu, Icon, IconElement, Label, ListEntry}, + ui::{ContextMenu, Icon, IconElement, Label}, CloseActiveItem, NewCenterTerminal, Pane, ToolbarItemLocation, Workspace, WorkspaceId, }; @@ -63,7 +63,6 @@ pub struct SendKeystroke(String); actions!(Clear, Copy, Paste, ShowCharacterPalette, SearchTest); pub fn init(cx: &mut AppContext) { - workspace::ui::init(cx); terminal_panel::init(cx); terminal::init(cx); @@ -84,7 +83,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, @@ -300,14 +299,8 @@ impl TerminalView { cx: &mut ViewContext, ) { self.context_menu = Some(ContextMenu::build(cx, |menu, _| { - menu.action( - ListEntry::new("clear", Label::new("Clear")), - Box::new(Clear), - ) - .action( - ListEntry::new("close", Label::new("Close")), - Box::new(CloseActiveItem { save_intent: None }), - ) + menu.action("Clear", Box::new(Clear)) + .action("Close", Box::new(CloseActiveItem { save_intent: None })) })); dbg!(&position); // todo!() @@ -508,12 +501,7 @@ pub fn regex_search_for_query(query: &project::search::SearchQuery) -> Option, - ) { + fn key_down(&mut self, event: &KeyDownEvent, cx: &mut ViewContext) { self.clear_bel(cx); self.pause_cursor_blinking(cx); @@ -540,8 +528,8 @@ impl TerminalView { } } -impl Render for TerminalView { - type Element = Focusable>; +impl Render for TerminalView { + type Element = Focusable
; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { let terminal_handle = self.terminal.clone().downgrade(); @@ -555,14 +543,14 @@ impl Render for TerminalView { div() .z_index(0) .absolute() - .on_key_down(Self::key_down) - .on_action(TerminalView::send_text) - .on_action(TerminalView::send_keystroke) - .on_action(TerminalView::copy) - .on_action(TerminalView::paste) - .on_action(TerminalView::clear) - .on_action(TerminalView::show_character_palette) - .on_action(TerminalView::select_all) + .on_key_down(cx.listener(Self::key_down)) + .on_action(cx.listener(TerminalView::send_text)) + .on_action(cx.listener(TerminalView::send_keystroke)) + .on_action(cx.listener(TerminalView::copy)) + .on_action(cx.listener(TerminalView::paste)) + .on_action(cx.listener(TerminalView::clear)) + .on_action(cx.listener(TerminalView::show_character_palette)) + .on_action(cx.listener(TerminalView::select_all)) // todo!() .child( "TERMINAL HERE", // TerminalElement::new( @@ -572,10 +560,13 @@ impl Render for TerminalView { // self.can_navigate_to_selected_word, // ) ) - .on_mouse_down(MouseButton::Right, |this, event, cx| { - this.deploy_context_menu(event.position, cx); - cx.notify(); - }), + .on_mouse_down( + MouseButton::Right, + cx.listener(|this, event: &MouseDownEvent, cx| { + this.deploy_context_menu(event.position, cx); + cx.notify(); + }), + ), ) .children( self.context_menu @@ -583,8 +574,8 @@ impl Render for TerminalView { .map(|context_menu| div().z_index(1).absolute().child(context_menu)), ) .track_focus(&self.focus_handle) - .on_focus_in(Self::focus_in) - .on_focus_out(Self::focus_out) + .on_focus_in(cx.listener(Self::focus_in)) + .on_focus_out(cx.listener(Self::focus_out)) } } @@ -749,11 +740,7 @@ impl Item for TerminalView { Some(self.terminal().read(cx).title().into()) } - fn tab_content( - &self, - _detail: Option, - cx: &gpui::AppContext, - ) -> AnyElement { + fn tab_content(&self, _detail: Option, cx: &WindowContext) -> AnyElement { let title = self.terminal().read(cx).title(); div() @@ -795,7 +782,7 @@ impl Item for TerminalView { // } fn breadcrumb_location(&self) -> ToolbarItemLocation { - ToolbarItemLocation::PrimaryLeft { flex: None } + ToolbarItemLocation::PrimaryLeft } fn breadcrumbs(&self, _: &theme::Theme, cx: &AppContext) -> Option> { diff --git a/crates/theme2/Cargo.toml b/crates/theme2/Cargo.toml index 22bea20e164c6898ad1a55dca3067abacf3f4d5e..9ed387ebcea15e48528b4b17875cc1813689dd66 100644 --- a/crates/theme2/Cargo.toml +++ b/crates/theme2/Cargo.toml @@ -5,9 +5,9 @@ edition = "2021" publish = false [features] -default = ["stories"] +default = [] importing-themes = [] -stories = ["dep:itertools"] +stories = ["dep:itertools", "dep:story"] test-support = [ "gpui/test-support", "fs/test-support", @@ -30,6 +30,7 @@ serde.workspace = true serde_derive.workspace = true serde_json.workspace = true settings = { package = "settings2", path = "../settings2" } +story = { path = "../story", optional = true } toml.workspace = true uuid.workspace = true util = { path = "../util" } diff --git a/crates/theme2/src/settings.rs b/crates/theme2/src/settings.rs index 01951f2ed0adaef94933e6169f85a74a102201a4..15b578d4b0e10855d0368ce9d31167847ae8944b 100644 --- a/crates/theme2/src/settings.rs +++ b/crates/theme2/src/settings.rs @@ -184,7 +184,7 @@ impl settings::Settings for ThemeSettings { ) -> schemars::schema::RootSchema { let mut root_schema = generator.root_schema_for::(); let theme_names = cx - .global::>() + .global::() .list_names(params.staff_mode) .map(|theme_name| Value::String(theme_name.to_string())) .collect(); diff --git a/crates/theme2/src/story.rs b/crates/theme2/src/story.rs deleted file mode 100644 index e0c802fcc72ac3cbd234cfda1bb65e7224e47cdd..0000000000000000000000000000000000000000 --- a/crates/theme2/src/story.rs +++ /dev/null @@ -1,41 +0,0 @@ -use gpui::{div, Div, Element, ParentElement, SharedString, Styled, ViewContext}; - -use crate::ActiveTheme; - -pub struct Story {} - -impl Story { - pub fn container(cx: &mut ViewContext) -> Div { - div() - .size_full() - .flex() - .flex_col() - .pt_2() - .px_4() - .font("Zed Mono") - .bg(cx.theme().colors().background) - } - - pub fn title(cx: &mut ViewContext, title: SharedString) -> impl Element { - div() - .text_xl() - .text_color(cx.theme().colors().text) - .child(title) - } - - pub fn title_for(cx: &mut ViewContext) -> impl Element { - Self::title(cx, std::any::type_name::().into()) - } - - pub fn label( - cx: &mut ViewContext, - label: impl Into, - ) -> impl Element { - div() - .mt_4() - .mb_2() - .text_xs() - .text_color(cx.theme().colors().text) - .child(label.into()) - } -} diff --git a/crates/theme2/src/styles.rs b/crates/theme2/src/styles.rs index 18f9e76581fb3210da9174378f4fd2e314d19a3b..13a59f486d1598dc2b3aaa092a504ff30ad91021 100644 --- a/crates/theme2/src/styles.rs +++ b/crates/theme2/src/styles.rs @@ -4,8 +4,14 @@ mod status; mod syntax; mod system; +#[cfg(feature = "stories")] +mod stories; + pub use colors::*; pub use players::*; pub use status::*; pub use syntax::*; pub use system::*; + +#[cfg(feature = "stories")] +pub use stories::*; diff --git a/crates/theme2/src/styles/players.rs b/crates/theme2/src/styles/players.rs index 726e4bac56c7d05646c2c0d30e9c0f8df254b116..e8bce8e578200cc05802a4c2d44fb3edace32c6b 100644 --- a/crates/theme2/src/styles/players.rs +++ b/crates/theme2/src/styles/players.rs @@ -1,5 +1,7 @@ use gpui::Hsla; +use crate::{amber, blue, jade, lime, orange, pink, purple, red}; + #[derive(Debug, Clone, Copy, Default)] pub struct PlayerColor { pub cursor: Hsla, @@ -133,141 +135,3 @@ impl PlayerColors { self.0[(participant_index as usize % len) + 1] } } - -#[cfg(feature = "stories")] -pub use stories::*; - -use crate::{amber, blue, jade, lime, orange, pink, purple, red}; - -#[cfg(feature = "stories")] -mod stories { - use super::*; - use crate::{ActiveTheme, Story}; - use gpui::{div, img, px, Div, ParentElement, Render, Styled, ViewContext}; - - pub struct PlayerStory; - - impl Render for PlayerStory { - type Element = Div; - - fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - Story::container(cx).child( - div() - .flex() - .flex_col() - .gap_4() - .child(Story::title_for::<_, PlayerColors>(cx)) - .child(Story::label(cx, "Player Colors")) - .child( - div() - .flex() - .flex_col() - .gap_1() - .child( - div().flex().gap_1().children( - cx.theme().players().0.clone().iter_mut().map(|player| { - div().w_8().h_8().rounded_md().bg(player.cursor) - }), - ), - ) - .child(div().flex().gap_1().children( - cx.theme().players().0.clone().iter_mut().map(|player| { - div().w_8().h_8().rounded_md().bg(player.background) - }), - )) - .child(div().flex().gap_1().children( - cx.theme().players().0.clone().iter_mut().map(|player| { - div().w_8().h_8().rounded_md().bg(player.selection) - }), - )), - ) - .child(Story::label(cx, "Avatar Rings")) - .child(div().flex().gap_1().children( - cx.theme().players().0.clone().iter_mut().map(|player| { - div() - .my_1() - .rounded_full() - .border_2() - .border_color(player.cursor) - .child( - img() - .rounded_full() - .uri("https://avatars.githubusercontent.com/u/1714999?v=4") - .size_6() - .bg(gpui::red()), - ) - }), - )) - .child(Story::label(cx, "Player Backgrounds")) - .child(div().flex().gap_1().children( - cx.theme().players().0.clone().iter_mut().map(|player| { - div() - .my_1() - .rounded_xl() - .flex() - .items_center() - .h_8() - .py_0p5() - .px_1p5() - .bg(player.background) - .child( - div().relative().neg_mx_1().rounded_full().z_index(3) - .border_2() - .border_color(player.background) - .size(px(28.)) - .child( - img() - .rounded_full() - .uri("https://avatars.githubusercontent.com/u/1714999?v=4") - .size(px(24.)) - .bg(gpui::red()), - ), - ).child( - div().relative().neg_mx_1().rounded_full().z_index(2) - .border_2() - .border_color(player.background) - .size(px(28.)) - .child( - img() - .rounded_full() - .uri("https://avatars.githubusercontent.com/u/1714999?v=4") - .size(px(24.)) - .bg(gpui::red()), - ), - ).child( - div().relative().neg_mx_1().rounded_full().z_index(1) - .border_2() - .border_color(player.background) - .size(px(28.)) - .child( - img() - .rounded_full() - .uri("https://avatars.githubusercontent.com/u/1714999?v=4") - .size(px(24.)) - .bg(gpui::red()), - ), - ) - }), - )) - .child(Story::label(cx, "Player Selections")) - .child(div().flex().flex_col().gap_px().children( - cx.theme().players().0.clone().iter_mut().map(|player| { - div() - .flex() - .child( - div() - .flex() - .flex_none() - .rounded_sm() - .px_0p5() - .text_color(cx.theme().colors().text) - .bg(player.selection) - .child("The brown fox jumped over the lazy dog."), - ) - .child(div().flex_1()) - }), - )), - ) - } - } -} diff --git a/crates/theme2/src/styles/stories/color.rs b/crates/theme2/src/styles/stories/color.rs new file mode 100644 index 0000000000000000000000000000000000000000..a7d8885848ba89ed1470b6624a00dd56c39ade51 --- /dev/null +++ b/crates/theme2/src/styles/stories/color.rs @@ -0,0 +1,41 @@ +use gpui::prelude::*; +use gpui::{div, px, Div, ViewContext}; +use story::Story; + +use crate::{default_color_scales, ColorScaleStep}; + +pub struct ColorsStory; + +impl Render for ColorsStory { + type Element = Div; + + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + let color_scales = default_color_scales(); + + Story::container().child(Story::title("Colors")).child( + div() + .id("colors") + .flex() + .flex_col() + .gap_1() + .overflow_y_scroll() + .text_color(gpui::white()) + .children(color_scales.into_iter().map(|scale| { + div() + .flex() + .child( + div() + .w(px(75.)) + .line_height(px(24.)) + .child(scale.name().clone()), + ) + .child( + div().flex().gap_1().children( + ColorScaleStep::ALL + .map(|step| div().flex().size_6().bg(scale.step(cx, step))), + ), + ) + })), + ) + } +} diff --git a/crates/theme2/src/styles/stories/mod.rs b/crates/theme2/src/styles/stories/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..af6af965484adc8f4850f1c01bb69d12e8181873 --- /dev/null +++ b/crates/theme2/src/styles/stories/mod.rs @@ -0,0 +1,5 @@ +mod color; +mod players; + +pub use color::*; +pub use players::*; diff --git a/crates/theme2/src/styles/stories/players.rs b/crates/theme2/src/styles/stories/players.rs new file mode 100644 index 0000000000000000000000000000000000000000..d189d3bfb07bb0efc4691ca14abac4d94948d620 --- /dev/null +++ b/crates/theme2/src/styles/stories/players.rs @@ -0,0 +1,137 @@ +use gpui::{div, img, px, Div, ParentElement, Render, Styled, ViewContext}; +use story::Story; + +use crate::{ActiveTheme, PlayerColors}; + +pub struct PlayerStory; + +impl Render for PlayerStory { + type Element = Div; + + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + Story::container().child( + div() + .flex() + .flex_col() + .gap_4() + .child(Story::title_for::()) + .child(Story::label("Player Colors")) + .child( + div() + .flex() + .flex_col() + .gap_1() + .child( + div().flex().gap_1().children( + cx.theme() + .players() + .0 + .clone() + .iter_mut() + .map(|player| div().w_8().h_8().rounded_md().bg(player.cursor)), + ), + ) + .child( + div().flex().gap_1().children( + cx.theme().players().0.clone().iter_mut().map(|player| { + div().w_8().h_8().rounded_md().bg(player.background) + }), + ), + ) + .child( + div().flex().gap_1().children( + cx.theme().players().0.clone().iter_mut().map(|player| { + div().w_8().h_8().rounded_md().bg(player.selection) + }), + ), + ), + ) + .child(Story::label("Avatar Rings")) + .child(div().flex().gap_1().children( + cx.theme().players().0.clone().iter_mut().map(|player| { + div() + .my_1() + .rounded_full() + .border_2() + .border_color(player.cursor) + .child( + img() + .rounded_full() + .uri("https://avatars.githubusercontent.com/u/1714999?v=4") + .size_6() + .bg(gpui::red()), + ) + }), + )) + .child(Story::label("Player Backgrounds")) + .child(div().flex().gap_1().children( + cx.theme().players().0.clone().iter_mut().map(|player| { + div() + .my_1() + .rounded_xl() + .flex() + .items_center() + .h_8() + .py_0p5() + .px_1p5() + .bg(player.background) + .child( + div().relative().neg_mx_1().rounded_full().z_index(3) + .border_2() + .border_color(player.background) + .size(px(28.)) + .child( + img() + .rounded_full() + .uri("https://avatars.githubusercontent.com/u/1714999?v=4") + .size(px(24.)) + .bg(gpui::red()), + ), + ).child( + div().relative().neg_mx_1().rounded_full().z_index(2) + .border_2() + .border_color(player.background) + .size(px(28.)) + .child( + img() + .rounded_full() + .uri("https://avatars.githubusercontent.com/u/1714999?v=4") + .size(px(24.)) + .bg(gpui::red()), + ), + ).child( + div().relative().neg_mx_1().rounded_full().z_index(1) + .border_2() + .border_color(player.background) + .size(px(28.)) + .child( + img() + .rounded_full() + .uri("https://avatars.githubusercontent.com/u/1714999?v=4") + .size(px(24.)) + .bg(gpui::red()), + ), + ) + }), + )) + .child(Story::label("Player Selections")) + .child(div().flex().flex_col().gap_px().children( + cx.theme().players().0.clone().iter_mut().map(|player| { + div() + .flex() + .child( + div() + .flex() + .flex_none() + .rounded_sm() + .px_0p5() + .text_color(cx.theme().colors().text) + .bg(player.selection) + .child("The brown fox jumped over the lazy dog."), + ) + .child(div().flex_1()) + }), + )), + ) + } +} diff --git a/crates/theme2/src/theme2.rs b/crates/theme2/src/theme2.rs index fb89604865630e2a8f47f712dc8c8d8a2509127f..03407beab9e1d5f0bba67de8294acaeebfdcacd4 100644 --- a/crates/theme2/src/theme2.rs +++ b/crates/theme2/src/theme2.rs @@ -63,6 +63,12 @@ impl ActiveTheme for AppContext { } } +// impl<'a> ActiveTheme for WindowContext<'a> { +// fn theme(&self) -> &Arc { +// &ThemeSettings::get_global(self.app()).active_theme +// } +// } + pub struct ThemeFamily { pub id: String, pub name: SharedString, @@ -138,8 +144,3 @@ pub struct DiagnosticStyle { pub hint: Hsla, pub ignored: Hsla, } - -#[cfg(feature = "stories")] -mod story; -#[cfg(feature = "stories")] -pub use story::*; diff --git a/crates/ui2/Cargo.toml b/crates/ui2/Cargo.toml index efbec22bee4f65ad64d3225063ef7793a0bdec6c..9f98b92296ca40cfd2bd8fdf3eff524937ce7104 100644 --- a/crates/ui2/Cargo.toml +++ b/crates/ui2/Cargo.toml @@ -4,6 +4,10 @@ version = "0.1.0" edition = "2021" publish = false +[lib] +name = "ui2" +path = "src/ui2.rs" + [dependencies] anyhow.workspace = true chrono = "0.4" @@ -13,10 +17,11 @@ menu = { package = "menu2", path = "../menu2"} serde.workspace = true settings2 = { path = "../settings2" } smallvec.workspace = true +story = { path = "../story", optional = true } strum = { version = "0.25.0", features = ["derive"] } theme2 = { path = "../theme2" } rand = "0.8" [features] default = [] -stories = ["dep:itertools"] +stories = ["dep:itertools", "dep:story"] diff --git a/crates/ui2/src/components.rs b/crates/ui2/src/components.rs index e7b2d9cf0f02929fef29ec5f41fffc401ad7fe2c..c467576f4af0d11454830be704cd81fd1dd3ef19 100644 --- a/crates/ui2/src/components.rs +++ b/crates/ui2/src/components.rs @@ -2,56 +2,40 @@ mod avatar; mod button; mod checkbox; mod context_menu; -mod details; +mod disclosure; mod divider; -mod elevated_surface; -mod facepile; mod icon; mod icon_button; -mod indicator; mod input; mod keybinding; mod label; mod list; -mod modal; -mod notification_toast; -mod palette; -mod panel; -mod player; -mod player_stack; +mod popover; mod slot; mod stack; -mod tab; -mod toast; mod toggle; -mod tool_divider; mod tooltip; +#[cfg(feature = "stories")] +mod stories; + pub use avatar::*; pub use button::*; pub use checkbox::*; pub use context_menu::*; -pub use details::*; +pub use disclosure::*; pub use divider::*; -pub use elevated_surface::*; -pub use facepile::*; pub use icon::*; pub use icon_button::*; -pub use indicator::*; pub use input::*; pub use keybinding::*; pub use label::*; pub use list::*; -pub use modal::*; -pub use notification_toast::*; -pub use palette::*; -pub use panel::*; -pub use player::*; -pub use player_stack::*; +pub use popover::*; pub use slot::*; pub use stack::*; -pub use tab::*; -pub use toast::*; pub use toggle::*; -pub use tool_divider::*; pub use tooltip::*; + +#[cfg(feature = "stories")] +pub use stories::*; diff --git a/crates/ui2/src/components/avatar.rs b/crates/ui2/src/components/avatar.rs index da76a95cfa8cc5c7b6842802d626a94d567b8652..364a1454946d51970723938da672b21993f9484f 100644 --- a/crates/ui2/src/components/avatar.rs +++ b/crates/ui2/src/components/avatar.rs @@ -1,16 +1,23 @@ use crate::prelude::*; use gpui::{img, Img, RenderOnce}; +#[derive(Debug, Default, PartialEq, Clone)] +pub enum Shape { + #[default] + Circle, + RoundedRectangle, +} + #[derive(RenderOnce)] pub struct Avatar { src: SharedString, shape: Shape, } -impl Component for Avatar { - type Rendered = Img; +impl Component for Avatar { + type Rendered = Img; - fn render(self, _view: &mut V, cx: &mut ViewContext) -> Self::Rendered { + fn render(self, _: &mut WindowContext) -> Self::Rendered { let mut img = img(); if self.shape == Shape::Circle { @@ -39,31 +46,3 @@ impl Avatar { self } } - -#[cfg(feature = "stories")] -pub use stories::*; - -#[cfg(feature = "stories")] -mod stories { - use super::*; - use crate::Story; - use gpui::{Div, Render}; - - pub struct AvatarStory; - - impl Render for AvatarStory { - type Element = Div; - - fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - Story::container(cx) - .child(Story::title_for::<_, Avatar>(cx)) - .child(Story::label(cx, "Default")) - .child(Avatar::new( - "https://avatars.githubusercontent.com/u/1714999?v=4", - )) - .child(Avatar::new( - "https://avatars.githubusercontent.com/u/326587?v=4", - )) - } - } -} diff --git a/crates/ui2/src/components/button.rs b/crates/ui2/src/components/button.rs index efbb4c9e72e3fc5ff5a7f4a37873cee4bf2e167d..a5176be52ea6b5fa85db6e7ebea943cae677d95b 100644 --- a/crates/ui2/src/components/button.rs +++ b/crates/ui2/src/components/button.rs @@ -1,28 +1,28 @@ -use std::sync::Arc; +use std::rc::Rc; use gpui::{ - DefiniteLength, Div, Hsla, MouseButton, RenderOnce, Stateful, StatefulInteractiveElement, + DefiniteLength, Div, Hsla, MouseButton, MouseDownEvent, RenderOnce, StatefulInteractiveElement, WindowContext, }; use crate::prelude::*; -use crate::{h_stack, Icon, IconButton, IconElement, Label, LineHeightStyle, TextColor}; +use crate::{h_stack, Color, Icon, IconButton, IconElement, Label, LineHeightStyle}; /// Provides the flexibility to use either a standard /// button or an icon button in a given context. -pub enum ButtonOrIconButton { - Button(Button), - IconButton(IconButton), +pub enum ButtonOrIconButton { + Button(Button), + IconButton(IconButton), } -impl From> for ButtonOrIconButton { - fn from(value: Button) -> Self { +impl From