diff --git a/.cargo/ci-config.toml b/.cargo/ci-config.toml new file mode 100644 index 0000000000..6dbaf4b446 --- /dev/null +++ b/.cargo/ci-config.toml @@ -0,0 +1,12 @@ +# This config is different from config.toml in this directory, as the latter is recognized by Cargo. +# This file is placed in $HOME/.cargo/config.toml on CI runs. Cargo then merges Zeds .cargo/config.toml with $HOME/.cargo/config.toml +# with preference for settings from Zeds config.toml. +# TL;DR: If a value is set in both ci-config.toml and config.toml, config.toml value takes precedence. +# Arrays are merged together though. See: https://doc.rust-lang.org/cargo/reference/config.html#hierarchical-structure +# The intent for this file is to configure CI build process with a divergance from Zed developers experience; for example, in this config file +# we use `-D warnings` for rustflags (which makes compilation fail in presence of warnings during build process). Placing that in developers `config.toml` +# would be incovenient. +# We *could* override things like RUSTFLAGS manually by setting them as environment variables, but that is less DRY; worse yet, if you forget to set proper environment variables +# in one spot, that's going to trigger a rebuild of all of the artifacts. Using ci-config.toml we can define these overrides for CI in one spot and not worry about it. +[build] +rustflags = ["-D", "warnings"] diff --git a/.github/actions/run_tests/action.yml b/.github/actions/run_tests/action.yml index de5eadb61a..1ea51a06a6 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 65475a41b9..6bfc0ab683 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,6 +29,9 @@ jobs: clean: false submodules: "recursive" + - name: Set up default .cargo/config.toml + run: cp ./.cargo/ci-config.toml ~/.cargo/config.toml + - name: Run rustfmt uses: ./.github/actions/check_formatting @@ -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') }} @@ -131,8 +134,6 @@ jobs: - uses: softprops/action-gh-release@v1 name: Upload app bundle to release - # TODO kb seems that zed.dev relies on GitHub releases for release version tracking. - # Find alternatives for `nightly` or just go on with more releases? if: ${{ env.RELEASE_CHANNEL == 'preview' || env.RELEASE_CHANNEL == 'stable' }} with: draft: true diff --git a/.github/workflows/release_nightly.yml b/.github/workflows/release_nightly.yml index 447e928866..7b08c52c61 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 bdd7b7a1d9..8838fdb130 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1186,12 +1186,14 @@ version = "0.1.0" dependencies = [ "anyhow", "async-broadcast", + "async-trait", "audio2", "client2", "collections", "fs2", "futures 0.3.28", "gpui2", + "image", "language2", "live_kit_client2", "log", @@ -1203,7 +1205,10 @@ dependencies = [ "serde_derive", "serde_json", "settings2", + "smallvec", + "ui2", "util", + "workspace2", ] [[package]] @@ -1398,7 +1403,7 @@ dependencies = [ "smol", "sum_tree", "tempfile", - "text", + "text2", "thiserror", "time", "tiny_http", @@ -1664,7 +1669,7 @@ dependencies = [ [[package]] name = "collab" -version = "0.28.0" +version = "0.29.0" dependencies = [ "anyhow", "async-trait", @@ -1869,7 +1874,7 @@ dependencies = [ "editor2", "feature_flags2", "futures 0.3.28", - "fuzzy", + "fuzzy2", "gpui2", "language2", "lazy_static", @@ -6763,7 +6768,6 @@ dependencies = [ "anyhow", "client2", "collections", - "context_menu", "db2", "editor2", "futures 0.3.28", @@ -8032,6 +8036,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" @@ -8855,6 +8888,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" @@ -8876,6 +8916,7 @@ dependencies = [ "settings2", "simplelog", "smallvec", + "story", "strum", "theme", "theme2", @@ -8883,17 +8924,6 @@ dependencies = [ "util", ] -[[package]] -name = "storybook3" -version = "0.1.0" -dependencies = [ - "anyhow", - "gpui2", - "settings2", - "theme2", - "ui2", -] - [[package]] name = "stringprep" version = "0.1.4" @@ -9397,6 +9427,7 @@ dependencies = [ "serde_derive", "serde_json", "settings2", + "story", "toml 0.5.11", "util", "uuid 1.4.1", @@ -9899,7 +9930,7 @@ dependencies = [ [[package]] name = "tree-sitter" version = "0.20.10" -source = "git+https://github.com/tree-sitter/tree-sitter?rev=35a6052fbcafc5e5fc0f9415b8652be7dcaf7222#35a6052fbcafc5e5fc0f9415b8652be7dcaf7222" +source = "git+https://github.com/tree-sitter/tree-sitter?rev=3b0159d25559b603af566ade3c83d930bf466db1#3b0159d25559b603af566ade3c83d930bf466db1" dependencies = [ "cc", "regex", @@ -10147,6 +10178,15 @@ dependencies = [ "tree-sitter", ] +[[package]] +name = "tree-sitter-uiua" +version = "0.3.3" +source = "git+https://github.com/shnarazk/tree-sitter-uiua?rev=9260f11be5900beda4ee6d1a24ab8ddfaf5a19b2#9260f11be5900beda4ee6d1a24ab8ddfaf5a19b2" +dependencies = [ + "cc", + "tree-sitter", +] + [[package]] name = "tree-sitter-vue" version = "0.0.1" @@ -10240,6 +10280,7 @@ dependencies = [ "serde", "settings2", "smallvec", + "story", "strum", "theme2", ] @@ -11354,6 +11395,7 @@ version = "0.1.0" dependencies = [ "anyhow", "async-recursion 1.0.5", + "async-trait", "bincode", "call2", "client2", @@ -11466,7 +11508,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.114.0" +version = "0.115.0" dependencies = [ "activity_indicator", "ai", @@ -11583,6 +11625,7 @@ dependencies = [ "tree-sitter-svelte", "tree-sitter-toml", "tree-sitter-typescript", + "tree-sitter-uiua", "tree-sitter-vue", "tree-sitter-yaml", "unindent", @@ -11614,9 +11657,11 @@ dependencies = [ "async-recursion 0.3.2", "async-tar", "async-trait", + "audio2", "auto_update2", "backtrace", "call2", + "channel2", "chrono", "cli", "client2", @@ -11634,7 +11679,6 @@ dependencies = [ "fs2", "fsevent", "futures 0.3.28", - "fuzzy", "go_to_line2", "gpui2", "ignore", @@ -11662,6 +11706,7 @@ dependencies = [ "rsa 0.4.0", "rust-embed", "schemars", + "search2", "serde", "serde_derive", "serde_json", @@ -11704,6 +11749,7 @@ dependencies = [ "tree-sitter-svelte", "tree-sitter-toml", "tree-sitter-typescript", + "tree-sitter-uiua", "tree-sitter-vue", "tree-sitter-yaml", "unindent", diff --git a/Cargo.toml b/Cargo.toml index 7c51d32d05..1f6a291a26 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", @@ -194,8 +195,10 @@ tree-sitter-lua = "0.0.14" tree-sitter-nix = { git = "https://github.com/nix-community/tree-sitter-nix", rev = "66e3e9ce9180ae08fc57372061006ef83f0abde7" } tree-sitter-nu = { git = "https://github.com/nushell/tree-sitter-nu", rev = "786689b0562b9799ce53e824cb45a1a2a04dc673"} tree-sitter-vue = {git = "https://github.com/zed-industries/tree-sitter-vue", rev = "9b6cb221ccb8d0b956fcb17e9a1efac2feefeb58"} +tree-sitter-uiua = {git = "https://github.com/shnarazk/tree-sitter-uiua", rev = "9260f11be5900beda4ee6d1a24ab8ddfaf5a19b2"} + [patch.crates-io] -tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "35a6052fbcafc5e5fc0f9415b8652be7dcaf7222" } +tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "3b0159d25559b603af566ade3c83d930bf466db1" } async-task = { git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e" } # TODO - Remove when a version is released with this PR: https://github.com/servo/core-foundation-rs/pull/457 @@ -207,11 +210,12 @@ core-graphics = { git = "https://github.com/servo/core-foundation-rs", rev = "07 [profile.dev] split-debuginfo = "unpacked" +debug = "limited" [profile.dev.package.taffy] opt-level = 3 [profile.release] -debug = true +debug = "limited" lto = "thin" codegen-units = 1 diff --git a/Procfile.zed2 b/Procfile.zed2 new file mode 100644 index 0000000000..51a509209b --- /dev/null +++ b/Procfile.zed2 @@ -0,0 +1,4 @@ +web: cd ../zed.dev && PORT=3000 npm run dev +collab: cd crates/collab2 && RUST_LOG=${RUST_LOG:-warn,collab=info} cargo run serve +livekit: livekit-server --dev +postgrest: postgrest crates/collab2/admin_api.conf diff --git a/assets/settings/default.json b/assets/settings/default.json index bf2acc708e..221862ca98 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 dc79fd7911..75d4a02626 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/auto_update.rs b/crates/auto_update2/src/auto_update.rs index aeff68965f..d2eab15d09 100644 --- a/crates/auto_update2/src/auto_update.rs +++ b/crates/auto_update2/src/auto_update.rs @@ -84,8 +84,8 @@ impl Settings for AutoUpdateSetting { pub fn init(http_client: Arc, server_url: String, cx: &mut AppContext) { AutoUpdateSetting::register(cx); - cx.observe_new_views(|wokrspace: &mut Workspace, _cx| { - wokrspace + cx.observe_new_views(|workspace: &mut Workspace, _cx| { + workspace .register_action(|_, action: &Check, cx| check(action, cx)) .register_action(|_, _action: &CheckThatAutoUpdaterWorks, cx| { let prompt = cx.prompt(gpui::PromptLevel::Info, "It does!", &["Ok"]); @@ -94,6 +94,11 @@ pub fn init(http_client: Arc, server_url: String, cx: &mut AppCo }) .detach(); }); + + // @nate - code to trigger update notification on launch + // workspace.show_notification(0, _cx, |cx| { + // cx.build_view(|_| UpdateNotification::new(SemanticVersion::from_str("1.1.1").unwrap())) + // }); }) .detach(); @@ -131,7 +136,7 @@ pub fn check(_: &Check, cx: &mut AppContext) { } } -fn _view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) { +pub fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) { if let Some(auto_updater) = AutoUpdater::get(cx) { let auto_updater = auto_updater.read(cx); let server_url = &auto_updater.server_url; diff --git a/crates/auto_update2/src/update_notification.rs b/crates/auto_update2/src/update_notification.rs index b77682c9ae..d15d82e112 100644 --- a/crates/auto_update2/src/update_notification.rs +++ b/crates/auto_update2/src/update_notification.rs @@ -1,87 +1,56 @@ -use gpui::{div, Div, EventEmitter, ParentComponent, Render, SemanticVersion, ViewContext}; -use menu::Cancel; -use workspace::notifications::NotificationEvent; +use gpui::{ + div, DismissEvent, Div, EventEmitter, InteractiveElement, ParentElement, Render, + SemanticVersion, StatefulInteractiveElement, Styled, ViewContext, +}; +use util::channel::ReleaseChannel; +use workspace::ui::{h_stack, v_stack, Icon, IconElement, Label, StyledExt}; pub struct UpdateNotification { - _version: SemanticVersion, + version: SemanticVersion, } -impl EventEmitter for UpdateNotification {} +impl EventEmitter for UpdateNotification {} impl Render for UpdateNotification { - type Element = Div; + type Element = Div; - fn render(&mut self, _cx: &mut gpui::ViewContext) -> Self::Element { - div().child("Updated zed!") - // let theme = theme::current(cx).clone(); - // let theme = &theme.update_notification; + fn render(&mut self, cx: &mut gpui::ViewContext) -> Self::Element { + let app_name = cx.global::().display_name(); - // let app_name = cx.global::().display_name(); - - // MouseEventHandler::new::(0, cx, |state, cx| { - // Flex::column() - // .with_child( - // Flex::row() - // .with_child( - // Text::new( - // format!("Updated to {app_name} {}", self.version), - // theme.message.text.clone(), - // ) - // .contained() - // .with_style(theme.message.container) - // .aligned() - // .top() - // .left() - // .flex(1., true), - // ) - // .with_child( - // MouseEventHandler::new::(0, cx, |state, _| { - // let style = theme.dismiss_button.style_for(state); - // Svg::new("icons/x.svg") - // .with_color(style.color) - // .constrained() - // .with_width(style.icon_width) - // .aligned() - // .contained() - // .with_style(style.container) - // .constrained() - // .with_width(style.button_width) - // .with_height(style.button_width) - // }) - // .with_padding(Padding::uniform(5.)) - // .on_click(MouseButton::Left, move |_, this, cx| { - // this.dismiss(&Default::default(), cx) - // }) - // .aligned() - // .constrained() - // .with_height(cx.font_cache().line_height(theme.message.text.font_size)) - // .aligned() - // .top() - // .flex_float(), - // ), - // ) - // .with_child({ - // let style = theme.action_message.style_for(state); - // Text::new("View the release notes", style.text.clone()) - // .contained() - // .with_style(style.container) - // }) - // .contained() - // }) - // .with_cursor_style(CursorStyle::PointingHand) - // .on_click(MouseButton::Left, |_, _, cx| { - // crate::view_release_notes(&Default::default(), cx) - // }) - // .into_any_named("update notification") + v_stack() + .elevation_3(cx) + .p_4() + .child( + h_stack() + .justify_between() + .child(Label::new(format!( + "Updated to {app_name} {}", + self.version + ))) + .child( + div() + .id("cancel") + .child(IconElement::new(Icon::Close)) + .cursor_pointer() + .on_click(cx.listener(|this, _, cx| this.dismiss(cx))), + ), + ) + .child( + div() + .id("notes") + .child(Label::new("View the release notes")) + .cursor_pointer() + .on_click(|_, cx| crate::view_release_notes(&Default::default(), cx)), + ) } } impl UpdateNotification { pub fn new(version: SemanticVersion) -> Self { - Self { _version: version } + Self { version } } - pub fn _dismiss(&mut self, _: &Cancel, cx: &mut ViewContext) { - cx.emit(NotificationEvent::Dismiss); + pub fn dismiss(&mut self, cx: &mut ViewContext) { + cx.emit(DismissEvent::Dismiss); } } diff --git a/crates/call2/Cargo.toml b/crates/call2/Cargo.toml index 9e13463680..8dc37f68dd 100644 --- a/crates/call2/Cargo.toml +++ b/crates/call2/Cargo.toml @@ -31,15 +31,19 @@ media = { path = "../media" } project = { package = "project2", path = "../project2" } settings = { package = "settings2", path = "../settings2" } util = { path = "../util" } - +ui = {package = "ui2", path = "../ui2"} +workspace = {package = "workspace2", path = "../workspace2"} +async-trait.workspace = true anyhow.workspace = true async-broadcast = "0.4" futures.workspace = true +image = "0.23" postage.workspace = true schemars.workspace = true serde.workspace = true serde_json.workspace = true serde_derive.workspace = true +smallvec.workspace = true [dev-dependencies] client = { package = "client2", path = "../client2", features = ["test-support"] } diff --git a/crates/call2/src/call2.rs b/crates/call2/src/call2.rs index 1f11e0650d..7885ef6e3f 100644 --- a/crates/call2/src/call2.rs +++ b/crates/call2/src/call2.rs @@ -1,25 +1,32 @@ pub mod call_settings; pub mod participant; pub mod room; +mod shared_screen; use anyhow::{anyhow, Result}; +use async_trait::async_trait; use audio::Audio; use call_settings::CallSettings; -use client::{proto, Client, TelemetrySettings, TypedEnvelope, User, UserStore, ZED_ALWAYS_ACTIVE}; +use client::{ + proto::{self, PeerId}, + Client, TelemetrySettings, TypedEnvelope, User, UserStore, ZED_ALWAYS_ACTIVE, +}; use collections::HashSet; use futures::{channel::oneshot, future::Shared, Future, FutureExt}; use gpui::{ AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Subscription, Task, - WeakModel, + View, ViewContext, VisualContext, WeakModel, WeakView, }; +pub use participant::ParticipantLocation; use postage::watch; use project::Project; use room::Event; -use settings::Settings; -use std::sync::Arc; - -pub use participant::ParticipantLocation; pub use room::Room; +use settings::Settings; +use shared_screen::SharedScreen; +use std::sync::Arc; +use util::ResultExt; +use workspace::{item::ItemHandle, CallHandler, Pane, Workspace}; pub fn init(client: Arc, user_store: Model, cx: &mut AppContext) { CallSettings::register(cx); @@ -464,7 +471,7 @@ impl ActiveCall { &self.pending_invites } - pub fn report_call_event(&self, operation: &'static str, cx: &AppContext) { + pub fn report_call_event(&self, operation: &'static str, cx: &mut AppContext) { if let Some(room) = self.room() { let room = room.read(cx); report_call_event_for_room(operation, room.id(), room.channel_id(), &self.client, cx); @@ -477,7 +484,7 @@ pub fn report_call_event_for_room( room_id: u64, channel_id: Option, client: &Arc, - cx: &AppContext, + cx: &mut AppContext, ) { let telemetry = client.telemetry(); let telemetry_settings = *TelemetrySettings::get_global(cx); @@ -505,6 +512,208 @@ pub fn report_call_event_for_channel( ) } +pub struct Call { + active_call: Option<(Model, Vec)>, + parent_workspace: WeakView, +} + +impl Call { + pub fn new( + parent_workspace: WeakView, + cx: &mut ViewContext<'_, Workspace>, + ) -> Box { + let mut active_call = None; + if cx.has_global::>() { + let call = cx.global::>().clone(); + let subscriptions = vec![cx.subscribe(&call, Self::on_active_call_event)]; + active_call = Some((call, subscriptions)); + } + Box::new(Self { + active_call, + parent_workspace, + }) + } + fn on_active_call_event( + workspace: &mut Workspace, + _: Model, + event: &room::Event, + cx: &mut ViewContext, + ) { + match event { + room::Event::ParticipantLocationChanged { participant_id } + | room::Event::RemoteVideoTracksChanged { participant_id } => { + workspace.leader_updated(*participant_id, cx); + } + _ => {} + } + } +} + +#[async_trait(?Send)] +impl CallHandler for Call { + fn peer_state( + &mut self, + leader_id: PeerId, + cx: &mut ViewContext, + ) -> Option<(bool, bool)> { + let (call, _) = self.active_call.as_ref()?; + let room = call.read(cx).room()?.read(cx); + let participant = room.remote_participant_for_peer_id(leader_id)?; + + let leader_in_this_app; + let leader_in_this_project; + match participant.location { + ParticipantLocation::SharedProject { project_id } => { + leader_in_this_app = true; + leader_in_this_project = Some(project_id) + == self + .parent_workspace + .update(cx, |this, cx| this.project().read(cx).remote_id()) + .log_err() + .flatten(); + } + ParticipantLocation::UnsharedProject => { + leader_in_this_app = true; + leader_in_this_project = false; + } + ParticipantLocation::External => { + leader_in_this_app = false; + leader_in_this_project = false; + } + }; + + Some((leader_in_this_project, leader_in_this_app)) + } + + fn shared_screen_for_peer( + &self, + peer_id: PeerId, + pane: &View, + cx: &mut ViewContext, + ) -> Option> { + let (call, _) = self.active_call.as_ref()?; + let room = call.read(cx).room()?.read(cx); + let participant = room.remote_participant_for_peer_id(peer_id)?; + let track = participant.video_tracks.values().next()?.clone(); + let user = participant.user.clone(); + for item in pane.read(cx).items_of_type::() { + if item.read(cx).peer_id == peer_id { + return Some(Box::new(item)); + } + } + + Some(Box::new(cx.build_view(|cx| { + SharedScreen::new(&track, peer_id, user.clone(), cx) + }))) + } + fn room_id(&self, cx: &AppContext) -> Option { + Some(self.active_call.as_ref()?.0.read(cx).room()?.read(cx).id()) + } + fn hang_up(&self, cx: &mut AppContext) -> Task> { + let Some((call, _)) = self.active_call.as_ref() else { + return Task::ready(Err(anyhow!("Cannot exit a call; not in a call"))); + }; + + call.update(cx, |this, cx| this.hang_up(cx)) + } + fn active_project(&self, cx: &AppContext) -> Option> { + ActiveCall::global(cx).read(cx).location().cloned() + } + fn invite( + &mut self, + called_user_id: u64, + initial_project: Option>, + cx: &mut AppContext, + ) -> Task> { + ActiveCall::global(cx).update(cx, |this, cx| { + this.invite(called_user_id, initial_project, cx) + }) + } + fn remote_participants(&self, cx: &AppContext) -> Option, PeerId)>> { + self.active_call + .as_ref() + .map(|call| { + call.0.read(cx).room().map(|room| { + room.read(cx) + .remote_participants() + .iter() + .map(|participant| { + (participant.1.user.clone(), participant.1.peer_id.clone()) + }) + .collect() + }) + }) + .flatten() + } + fn is_muted(&self, cx: &AppContext) -> Option { + self.active_call + .as_ref() + .map(|call| { + call.0 + .read(cx) + .room() + .map(|room| room.read(cx).is_muted(cx)) + }) + .flatten() + } + fn toggle_mute(&self, cx: &mut AppContext) { + self.active_call.as_ref().map(|call| { + call.0.update(cx, |this, cx| { + this.room().map(|room| { + let room = room.clone(); + cx.spawn(|_, mut cx| async move { + room.update(&mut cx, |this, cx| this.toggle_mute(cx))?? + .await + }) + .detach_and_log_err(cx); + }) + }) + }); + } + fn toggle_screen_share(&self, cx: &mut AppContext) { + self.active_call.as_ref().map(|call| { + call.0.update(cx, |this, cx| { + this.room().map(|room| { + room.update(cx, |this, cx| { + if this.is_screen_sharing() { + this.unshare_screen(cx).log_err(); + } else { + let t = this.share_screen(cx); + cx.spawn(move |_, _| async move { + t.await.log_err(); + }) + .detach(); + } + }) + }) + }) + }); + } + fn toggle_deafen(&self, cx: &mut AppContext) { + self.active_call.as_ref().map(|call| { + call.0.update(cx, |this, cx| { + this.room().map(|room| { + room.update(cx, |this, cx| { + this.toggle_deafen(cx).log_err(); + }) + }) + }) + }); + } + fn is_deafened(&self, cx: &AppContext) -> Option { + self.active_call + .as_ref() + .map(|call| { + call.0 + .read(cx) + .room() + .map(|room| room.read(cx).is_deafened()) + }) + .flatten() + .flatten() + } +} + #[cfg(test)] mod test { use gpui::TestAppContext; diff --git a/crates/call2/src/participant.rs b/crates/call2/src/participant.rs index f62d103f17..325a4f812b 100644 --- a/crates/call2/src/participant.rs +++ b/crates/call2/src/participant.rs @@ -4,7 +4,7 @@ use client::{proto, User}; use collections::HashMap; use gpui::WeakModel; pub use live_kit_client::Frame; -use live_kit_client::{RemoteAudioTrack, RemoteVideoTrack}; +pub(crate) use live_kit_client::{RemoteAudioTrack, RemoteVideoTrack}; use project::Project; use std::sync::Arc; diff --git a/crates/call2/src/room.rs b/crates/call2/src/room.rs index 87118764fd..694966abe9 100644 --- a/crates/call2/src/room.rs +++ b/crates/call2/src/room.rs @@ -21,7 +21,7 @@ use live_kit_client::{ }; use postage::{sink::Sink, stream::Stream, watch}; use project::Project; -use settings::Settings; +use settings::Settings as _; use std::{future::Future, mem, sync::Arc, time::Duration}; use util::{post_inc, ResultExt, TryFutureExt}; @@ -1267,7 +1267,6 @@ impl Room { .ok_or_else(|| anyhow!("live-kit was not initialized"))? .await }; - let publication = publish_track.await; this.upgrade() .ok_or_else(|| anyhow!("room was dropped"))? diff --git a/crates/call2/src/shared_screen.rs b/crates/call2/src/shared_screen.rs new file mode 100644 index 0000000000..7b7cd7d9df --- /dev/null +++ b/crates/call2/src/shared_screen.rs @@ -0,0 +1,157 @@ +use crate::participant::{Frame, RemoteVideoTrack}; +use anyhow::Result; +use client::{proto::PeerId, User}; +use futures::StreamExt; +use gpui::{ + div, AppContext, Div, Element, EventEmitter, FocusHandle, FocusableView, ParentElement, Render, + SharedString, Task, View, ViewContext, VisualContext, WindowContext, +}; +use std::sync::{Arc, Weak}; +use workspace::{item::Item, ItemNavHistory, WorkspaceId}; + +pub enum Event { + Close, +} + +pub struct SharedScreen { + track: Weak, + frame: Option, + // temporary addition just to render something interactive. + current_frame_id: usize, + pub peer_id: PeerId, + user: Arc, + nav_history: Option, + _maintain_frame: Task>, + focus: FocusHandle, +} + +impl SharedScreen { + pub fn new( + track: &Arc, + peer_id: PeerId, + user: Arc, + cx: &mut ViewContext, + ) -> Self { + cx.focus_handle(); + let mut frames = track.frames(); + Self { + track: Arc::downgrade(track), + frame: None, + peer_id, + user, + nav_history: Default::default(), + _maintain_frame: cx.spawn(|this, mut cx| async move { + while let Some(frame) = frames.next().await { + this.update(&mut cx, |this, cx| { + this.frame = Some(frame); + cx.notify(); + })?; + } + this.update(&mut cx, |_, cx| cx.emit(Event::Close))?; + Ok(()) + }), + focus: cx.focus_handle(), + current_frame_id: 0, + } + } +} + +impl EventEmitter for SharedScreen {} +impl EventEmitter for SharedScreen {} + +impl FocusableView for SharedScreen { + fn focus_handle(&self, _: &AppContext) -> FocusHandle { + self.focus.clone() + } +} +impl Render for SharedScreen { + type Element = Div; + fn render(&mut self, _: &mut ViewContext) -> Self::Element { + let frame = self.frame.clone(); + let frame_id = self.current_frame_id; + self.current_frame_id = self.current_frame_id.wrapping_add(1); + div().children(frame.map(|_| { + ui::Label::new(frame_id.to_string()).color(ui::Color::Error) + // img().data(Arc::new(ImageData::new(image::ImageBuffer::new( + // frame.width() as u32, + // frame.height() as u32, + // )))) + })) + } +} +// impl View for SharedScreen { +// fn ui_name() -> &'static str { +// "SharedScreen" +// } + +// fn render(&mut self, cx: &mut ViewContext) -> AnyElement { +// enum Focus {} + +// let frame = self.frame.clone(); +// MouseEventHandler::new::(0, cx, |_, cx| { +// Canvas::new(move |bounds, _, _, cx| { +// if let Some(frame) = frame.clone() { +// let size = constrain_size_preserving_aspect_ratio( +// bounds.size(), +// vec2f(frame.width() as f32, frame.height() as f32), +// ); +// let origin = bounds.origin() + (bounds.size() / 2.) - size / 2.; +// cx.scene().push_surface(gpui::platform::mac::Surface { +// bounds: RectF::new(origin, size), +// image_buffer: frame.image(), +// }); +// } +// }) +// .contained() +// .with_style(theme::current(cx).shared_screen) +// }) +// .on_down(MouseButton::Left, |_, _, cx| cx.focus_parent()) +// .into_any() +// } +// } + +impl Item for SharedScreen { + fn tab_tooltip_text(&self, _: &AppContext) -> Option { + Some(format!("{}'s screen", self.user.github_login).into()) + } + fn deactivated(&mut self, cx: &mut ViewContext) { + if let Some(nav_history) = self.nav_history.as_mut() { + nav_history.push::<()>(None, cx); + } + } + + fn tab_content(&self, _: Option, _: &WindowContext<'_>) -> gpui::AnyElement { + div().child("Shared screen").into_any() + // Flex::row() + // .with_child( + // Svg::new("icons/desktop.svg") + // .with_color(style.label.text.color) + // .constrained() + // .with_width(style.type_icon_width) + // .aligned() + // .contained() + // .with_margin_right(style.spacing), + // ) + // .with_child( + // Label::new( + // format!("{}'s screen", self.user.github_login), + // style.label.clone(), + // ) + // .aligned(), + // ) + // .into_any() + } + + fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext) { + self.nav_history = Some(history); + } + + fn clone_on_split( + &self, + _workspace_id: WorkspaceId, + cx: &mut ViewContext, + ) -> Option> { + let track = self.track.upgrade()?; + Some(cx.build_view(|cx| Self::new(&track, self.peer_id, self.user.clone(), cx))) + } +} diff --git a/crates/channel2/Cargo.toml b/crates/channel2/Cargo.toml index c292d4e8dd..7af5aa1224 100644 --- a/crates/channel2/Cargo.toml +++ b/crates/channel2/Cargo.toml @@ -18,7 +18,7 @@ db = { package = "db2", path = "../db2" } gpui = { package = "gpui2", path = "../gpui2" } util = { path = "../util" } rpc = { package = "rpc2", path = "../rpc2" } -text = { path = "../text" } +text = { package = "text2", path = "../text2" } language = { package = "language2", path = "../language2" } settings = { package = "settings2", path = "../settings2" } feature_flags = { package = "feature_flags2", path = "../feature_flags2" } diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index 8f7fbeb83d..a3e7449cf8 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -109,6 +109,10 @@ pub enum ClickhouseEvent { virtual_memory_in_bytes: u64, milliseconds_since_first_event: i64, }, + App { + operation: &'static str, + milliseconds_since_first_event: i64, + }, } #[cfg(debug_assertions)] @@ -168,13 +172,8 @@ impl Telemetry { let mut state = self.state.lock(); state.installation_id = installation_id.map(|id| id.into()); state.session_id = Some(session_id.into()); - let has_clickhouse_events = !state.clickhouse_events_queue.is_empty(); drop(state); - if has_clickhouse_events { - self.flush_clickhouse_events(); - } - let this = self.clone(); cx.spawn(|mut cx| async move { // Avoiding calling `System::new_all()`, as there have been crashes related to it @@ -256,7 +255,7 @@ impl Telemetry { milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event, telemetry_settings) + self.report_clickhouse_event(event, telemetry_settings, false) } pub fn report_copilot_event( @@ -273,7 +272,7 @@ impl Telemetry { milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event, telemetry_settings) + self.report_clickhouse_event(event, telemetry_settings, false) } pub fn report_assistant_event( @@ -290,7 +289,7 @@ impl Telemetry { milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event, telemetry_settings) + self.report_clickhouse_event(event, telemetry_settings, false) } pub fn report_call_event( @@ -307,7 +306,7 @@ impl Telemetry { milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event, telemetry_settings) + self.report_clickhouse_event(event, telemetry_settings, false) } pub fn report_cpu_event( @@ -322,7 +321,7 @@ impl Telemetry { milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event, telemetry_settings) + self.report_clickhouse_event(event, telemetry_settings, false) } pub fn report_memory_event( @@ -337,7 +336,21 @@ impl Telemetry { milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event, telemetry_settings) + self.report_clickhouse_event(event, telemetry_settings, false) + } + + // app_events are called at app open and app close, so flush is set to immediately send + pub fn report_app_event( + self: &Arc, + telemetry_settings: TelemetrySettings, + operation: &'static str, + ) { + let event = ClickhouseEvent::App { + operation, + milliseconds_since_first_event: self.milliseconds_since_first_event(), + }; + + self.report_clickhouse_event(event, telemetry_settings, true) } fn milliseconds_since_first_event(&self) -> i64 { @@ -358,6 +371,7 @@ impl Telemetry { self: &Arc, event: ClickhouseEvent, telemetry_settings: TelemetrySettings, + immediate_flush: bool, ) { if !telemetry_settings.metrics { return; @@ -370,7 +384,7 @@ impl Telemetry { .push(ClickhouseEventWrapper { signed_in, event }); if state.installation_id.is_some() { - if state.clickhouse_events_queue.len() >= MAX_QUEUE_LEN { + if immediate_flush || state.clickhouse_events_queue.len() >= MAX_QUEUE_LEN { drop(state); self.flush_clickhouse_events(); } else { diff --git a/crates/client2/src/client2.rs b/crates/client2/src/client2.rs index b4279b023e..b31451aa87 100644 --- a/crates/client2/src/client2.rs +++ b/crates/client2/src/client2.rs @@ -382,7 +382,7 @@ impl settings::Settings for TelemetrySettings { } impl Client { - pub fn new(http: Arc, cx: &AppContext) -> Arc { + pub fn new(http: Arc, cx: &mut AppContext) -> Arc { Arc::new(Self { id: AtomicU64::new(0), peer: Peer::new(0), @@ -551,7 +551,6 @@ impl Client { F: 'static + Future>, { let message_type_id = TypeId::of::(); - let mut state = self.state.write(); state .models_by_message_type diff --git a/crates/client2/src/telemetry.rs b/crates/client2/src/telemetry.rs index 9c88d1102c..b303e68183 100644 --- a/crates/client2/src/telemetry.rs +++ b/crates/client2/src/telemetry.rs @@ -1,5 +1,6 @@ use crate::{TelemetrySettings, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL}; use chrono::{DateTime, Utc}; +use futures::Future; use gpui::{serde_json, AppContext, AppMetadata, BackgroundExecutor, Task}; use lazy_static::lazy_static; use parking_lot::Mutex; @@ -107,6 +108,10 @@ pub enum ClickhouseEvent { virtual_memory_in_bytes: u64, milliseconds_since_first_event: i64, }, + App { + operation: &'static str, + milliseconds_since_first_event: i64, + }, } #[cfg(debug_assertions)] @@ -122,12 +127,13 @@ const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(1); const DEBOUNCE_INTERVAL: Duration = Duration::from_secs(30); impl Telemetry { - pub fn new(client: Arc, cx: &AppContext) -> Arc { + pub fn new(client: Arc, cx: &mut AppContext) -> Arc { let release_channel = if cx.has_global::() { Some(cx.global::().display_name()) } else { None }; + // TODO: Replace all hardware stuff with nested SystemSpecs json let this = Arc::new(Self { http_client: client, @@ -147,9 +153,30 @@ impl Telemetry { }), }); + // We should only ever have one instance of Telemetry, leak the subscription to keep it alive + // rather than store in TelemetryState, complicating spawn as subscriptions are not Send + std::mem::forget(cx.on_app_quit({ + let this = this.clone(); + move |cx| this.shutdown_telemetry(cx) + })); + this } + #[cfg(any(test, feature = "test-support"))] + fn shutdown_telemetry(self: &Arc, _: &mut AppContext) -> impl Future { + Task::ready(()) + } + + // Skip calling this function in tests. + // TestAppContext ends up calling this function on shutdown and it panics when trying to find the TelemetrySettings + #[cfg(not(any(test, feature = "test-support")))] + fn shutdown_telemetry(self: &Arc, cx: &mut AppContext) -> impl Future { + let telemetry_settings = TelemetrySettings::get_global(cx).clone(); + self.report_app_event(telemetry_settings, "close"); + Task::ready(()) + } + pub fn log_file_path(&self) -> Option { Some(self.state.lock().log_file.as_ref()?.path().to_path_buf()) } @@ -163,13 +190,8 @@ impl Telemetry { let mut state = self.state.lock(); state.installation_id = installation_id.map(|id| id.into()); state.session_id = Some(session_id.into()); - let has_clickhouse_events = !state.clickhouse_events_queue.is_empty(); drop(state); - if has_clickhouse_events { - self.flush_clickhouse_events(); - } - let this = self.clone(); cx.spawn(|cx| async move { // Avoiding calling `System::new_all()`, as there have been crashes related to it @@ -257,7 +279,7 @@ impl Telemetry { milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event, telemetry_settings) + self.report_clickhouse_event(event, telemetry_settings, false) } pub fn report_copilot_event( @@ -274,7 +296,7 @@ impl Telemetry { milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event, telemetry_settings) + self.report_clickhouse_event(event, telemetry_settings, false) } pub fn report_assistant_event( @@ -291,7 +313,7 @@ impl Telemetry { milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event, telemetry_settings) + self.report_clickhouse_event(event, telemetry_settings, false) } pub fn report_call_event( @@ -308,7 +330,7 @@ impl Telemetry { milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event, telemetry_settings) + self.report_clickhouse_event(event, telemetry_settings, false) } pub fn report_cpu_event( @@ -323,7 +345,7 @@ impl Telemetry { milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event, telemetry_settings) + self.report_clickhouse_event(event, telemetry_settings, false) } pub fn report_memory_event( @@ -338,7 +360,21 @@ impl Telemetry { milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - self.report_clickhouse_event(event, telemetry_settings) + self.report_clickhouse_event(event, telemetry_settings, false) + } + + // app_events are called at app open and app close, so flush is set to immediately send + pub fn report_app_event( + self: &Arc, + telemetry_settings: TelemetrySettings, + operation: &'static str, + ) { + let event = ClickhouseEvent::App { + operation, + milliseconds_since_first_event: self.milliseconds_since_first_event(), + }; + + self.report_clickhouse_event(event, telemetry_settings, true) } fn milliseconds_since_first_event(&self) -> i64 { @@ -359,6 +395,7 @@ impl Telemetry { self: &Arc, event: ClickhouseEvent, telemetry_settings: TelemetrySettings, + immediate_flush: bool, ) { if !telemetry_settings.metrics { return; @@ -371,7 +408,7 @@ impl Telemetry { .push(ClickhouseEventWrapper { signed_in, event }); if state.installation_id.is_some() { - if state.clickhouse_events_queue.len() >= MAX_QUEUE_LEN { + if immediate_flush || state.clickhouse_events_queue.len() >= MAX_QUEUE_LEN { drop(state); self.flush_clickhouse_events(); } else { diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index dea6e09245..bbaf521e15 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/collab2/Cargo.toml b/crates/collab2/Cargo.toml index 4ce0a843f5..b8e6a45b06 100644 --- a/crates/collab2/Cargo.toml +++ b/crates/collab2/Cargo.toml @@ -10,7 +10,7 @@ publish = false name = "collab2" [[bin]] -name = "seed" +name = "seed2" required-features = ["seed-support"] [dependencies] diff --git a/crates/collab2/src/bin/dotenv.rs b/crates/collab2/src/bin/dotenv2.rs similarity index 100% rename from crates/collab2/src/bin/dotenv.rs rename to crates/collab2/src/bin/dotenv2.rs diff --git a/crates/collab2/src/bin/seed.rs b/crates/collab2/src/bin/seed2.rs similarity index 100% rename from crates/collab2/src/bin/seed.rs rename to crates/collab2/src/bin/seed2.rs diff --git a/crates/collab2/src/tests/test_server.rs b/crates/collab2/src/tests/test_server.rs index 090a32d4ca..969869599b 100644 --- a/crates/collab2/src/tests/test_server.rs +++ b/crates/collab2/src/tests/test_server.rs @@ -149,7 +149,7 @@ impl TestServer { .user_id }; let client_name = name.to_string(); - let mut client = cx.read(|cx| Client::new(http.clone(), cx)); + let mut client = cx.update(|cx| Client::new(http.clone(), cx)); let server = self.server.clone(); let db = self.app_state.db.clone(); let connection_killers = self.connection_killers.clone(); @@ -221,6 +221,7 @@ impl TestServer { fs: fs.clone(), build_window_options: |_, _, _| Default::default(), node_runtime: FakeNodeRuntime::new(), + call_factory: |_, _| Box::new(workspace::TestCallHandler), }); cx.update(|cx| { diff --git a/crates/collab_ui2/Cargo.toml b/crates/collab_ui2/Cargo.toml index 4660880ecd..c7c00d7696 100644 --- a/crates/collab_ui2/Cargo.toml +++ b/crates/collab_ui2/Cargo.toml @@ -33,7 +33,7 @@ collections = { path = "../collections" } # drag_and_drop = { path = "../drag_and_drop" } editor = { package="editor2", path = "../editor2" } #feedback = { path = "../feedback" } -fuzzy = { path = "../fuzzy" } +fuzzy = { package = "fuzzy2", path = "../fuzzy2" } gpui = { package = "gpui2", path = "../gpui2" } language = { package = "language2", path = "../language2" } menu = { package = "menu2", path = "../menu2" } diff --git a/crates/collab_ui2/src/collab_panel.rs b/crates/collab_ui2/src/collab_panel.rs index 20e77d7023..2d8cd2cd3c 100644 --- a/crates/collab_ui2/src/collab_panel.rs +++ b/crates/collab_ui2/src/collab_panel.rs @@ -1,5 +1,6 @@ +#![allow(unused)] // mod channel_modal; -// mod contact_finder; +mod contact_finder; // use crate::{ // channel_view::{self, ChannelView}, @@ -15,7 +16,7 @@ // proto::{self, PeerId}, // Client, Contact, User, UserStore, // }; -// use contact_finder::ContactFinder; +use contact_finder::ContactFinder; // use context_menu::{ContextMenu, ContextMenuItem}; // use db::kvp::KEY_VALUE_STORE; // use drag_and_drop::{DragAndDrop, Draggable}; @@ -155,20 +156,30 @@ actions!( const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel"; -use std::sync::Arc; +use std::{iter::once, mem, sync::Arc}; +use call::ActiveCall; +use channel::{Channel, ChannelId, ChannelStore}; +use client::{Client, Contact, User, UserStore}; use db::kvp::KEY_VALUE_STORE; +use editor::Editor; +use feature_flags::{ChannelsAlpha, FeatureFlagAppExt}; +use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ - actions, div, serde_json, AppContext, AsyncWindowContext, Div, EventEmitter, FocusHandle, - Focusable, FocusableView, InteractiveComponent, ParentComponent, Render, View, ViewContext, - VisualContext, WeakView, + actions, div, img, serde_json, AppContext, AsyncWindowContext, Div, EventEmitter, FocusHandle, + Focusable, FocusableView, InteractiveElement, IntoElement, Model, ParentElement, Render, + RenderOnce, SharedString, Styled, Subscription, View, ViewContext, VisualContext, WeakView, }; use project::Fs; use serde_derive::{Deserialize, Serialize}; use settings::Settings; -use util::ResultExt; +use ui::{ + h_stack, v_stack, Avatar, Button, Icon, IconButton, Label, List, ListHeader, ListItem, Tooltip, +}; +use util::{maybe, ResultExt}; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, + notifications::NotifyResultExt, Workspace, }; @@ -266,26 +277,26 @@ pub fn init(cx: &mut AppContext) { // ); } -// #[derive(Debug)] -// pub enum ChannelEditingState { -// Create { -// location: Option, -// pending_name: Option, -// }, -// Rename { -// location: ChannelId, -// pending_name: Option, -// }, -// } +#[derive(Debug)] +pub enum ChannelEditingState { + Create { + location: Option, + pending_name: Option, + }, + Rename { + location: ChannelId, + pending_name: Option, + }, +} -// impl ChannelEditingState { -// fn pending_name(&self) -> Option<&str> { -// match self { -// ChannelEditingState::Create { pending_name, .. } => pending_name.as_deref(), -// ChannelEditingState::Rename { pending_name, .. } => pending_name.as_deref(), -// } -// } -// } +impl ChannelEditingState { + fn pending_name(&self) -> Option<&str> { + match self { + ChannelEditingState::Create { pending_name, .. } => pending_name.as_deref(), + ChannelEditingState::Rename { pending_name, .. } => pending_name.as_deref(), + } + } +} pub struct CollabPanel { width: Option, @@ -294,22 +305,22 @@ pub struct CollabPanel { // channel_clipboard: Option, // pending_serialization: Task>, // context_menu: ViewHandle, - // filter_editor: ViewHandle, + filter_editor: View, // channel_name_editor: ViewHandle, - // channel_editing_state: Option, - // entries: Vec, + channel_editing_state: Option, + entries: Vec, // selection: Option, - // user_store: ModelHandle, - // client: Arc, - // channel_store: ModelHandle, + channel_store: Model, + user_store: Model, + client: Arc, // project: ModelHandle, - // match_candidates: Vec, + match_candidates: Vec, // list_state: ListState, - // subscriptions: Vec, - // collapsed_sections: Vec
, - // collapsed_channels: Vec, + subscriptions: Vec, + collapsed_sections: Vec
, + collapsed_channels: Vec, // drag_target_channel: ChannelDragTarget, - _workspace: WeakView, + workspace: WeakView, // context_menu_on_selected: bool, } @@ -333,58 +344,58 @@ struct SerializedCollabPanel { // Dismissed, // } -// #[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)] -// enum Section { -// ActiveCall, -// Channels, -// ChannelInvites, -// ContactRequests, -// Contacts, -// Online, -// Offline, -// } +#[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)] +enum Section { + ActiveCall, + Channels, + ChannelInvites, + ContactRequests, + Contacts, + Online, + Offline, +} -// #[derive(Clone, Debug)] -// enum ListEntry { -// Header(Section), -// CallParticipant { -// user: Arc, -// peer_id: Option, -// is_pending: bool, -// }, -// ParticipantProject { -// project_id: u64, -// worktree_root_names: Vec, -// host_user_id: u64, -// is_last: bool, -// }, -// ParticipantScreen { -// peer_id: Option, -// is_last: bool, -// }, -// IncomingRequest(Arc), -// OutgoingRequest(Arc), -// ChannelInvite(Arc), -// Channel { -// channel: Arc, -// depth: usize, -// has_children: bool, -// }, -// ChannelNotes { -// channel_id: ChannelId, -// }, -// ChannelChat { -// channel_id: ChannelId, -// }, -// ChannelEditor { -// depth: usize, -// }, -// Contact { -// contact: Arc, -// calling: bool, -// }, -// ContactPlaceholder, -// } +#[derive(Clone, Debug)] +enum ListEntry { + Header(Section), + // CallParticipant { + // user: Arc, + // peer_id: Option, + // is_pending: bool, + // }, + // ParticipantProject { + // project_id: u64, + // worktree_root_names: Vec, + // host_user_id: u64, + // is_last: bool, + // }, + // ParticipantScreen { + // peer_id: Option, + // is_last: bool, + // }, + IncomingRequest(Arc), + OutgoingRequest(Arc), + // ChannelInvite(Arc), + Channel { + channel: Arc, + depth: usize, + has_children: bool, + }, + // ChannelNotes { + // channel_id: ChannelId, + // }, + // ChannelChat { + // channel_id: ChannelId, + // }, + ChannelEditor { + depth: usize, + }, + Contact { + contact: Arc, + calling: bool, + }, + ContactPlaceholder, +} // impl Entity for CollabPanel { // type Event = Event; @@ -395,16 +406,11 @@ impl CollabPanel { cx.build_view(|cx| { // let view_id = cx.view_id(); - // let filter_editor = cx.add_view(|cx| { - // let mut editor = Editor::single_line( - // Some(Arc::new(|theme| { - // theme.collab_panel.user_query_editor.clone() - // })), - // cx, - // ); - // editor.set_placeholder_text("Filter channels, contacts", cx); - // editor - // }); + let filter_editor = cx.build_view(|cx| { + let mut editor = Editor::single_line(cx); + editor.set_placeholder_text("Filter channels, contacts", cx); + editor + }); // cx.subscribe(&filter_editor, |this, _, event, cx| { // if let editor::Event::BufferEdited = event { @@ -583,7 +589,7 @@ impl CollabPanel { // } // }); - let this = Self { + let mut this = Self { width: None, focus_handle: cx.focus_handle(), // channel_clipboard: None, @@ -591,25 +597,25 @@ impl CollabPanel { // pending_serialization: Task::ready(None), // context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)), // channel_name_editor, - // filter_editor, - // entries: Vec::default(), - // channel_editing_state: None, + filter_editor, + entries: Vec::default(), + channel_editing_state: None, // selection: None, - // user_store: workspace.user_store().clone(), - // channel_store: ChannelStore::global(cx), + channel_store: ChannelStore::global(cx), + user_store: workspace.user_store().clone(), // project: workspace.project().clone(), - // subscriptions: Vec::default(), - // match_candidates: Vec::default(), - // collapsed_sections: vec![Section::Offline], - // collapsed_channels: Vec::default(), - _workspace: workspace.weak_handle(), - // client: workspace.app_state().client.clone(), + subscriptions: Vec::default(), + match_candidates: Vec::default(), + collapsed_sections: vec![Section::Offline], + collapsed_channels: Vec::default(), + workspace: workspace.weak_handle(), + client: workspace.app_state().client.clone(), // context_menu_on_selected: true, // drag_target_channel: ChannelDragTarget::None, // list_state, }; - // this.update_entries(false, cx); + this.update_entries(false, cx); // // Update the dock position when the setting changes. // let mut old_dock_position = this.position(cx); @@ -626,10 +632,10 @@ impl CollabPanel { // ); // let active_call = ActiveCall::global(cx); - // this.subscriptions - // .push(cx.observe(&this.user_store, |this, _, cx| { - // this.update_entries(true, cx) - // })); + this.subscriptions + .push(cx.observe(&this.user_store, |this, _, cx| { + this.update_entries(true, cx) + })); // this.subscriptions // .push(cx.observe(&this.channel_store, |this, _, cx| { // this.update_entries(true, cx) @@ -663,6 +669,9 @@ impl CollabPanel { }) } + fn contacts(&self, cx: &AppContext) -> Option>> { + Some(self.user_store.read(cx).contacts().to_owned()) + } pub async fn load( workspace: WeakView, mut cx: AsyncWindowContext, @@ -715,449 +724,449 @@ impl CollabPanel { // ); // } - // fn update_entries(&mut self, select_same_item: bool, cx: &mut ViewContext) { - // let channel_store = self.channel_store.read(cx); - // let user_store = self.user_store.read(cx); - // let query = self.filter_editor.read(cx).text(cx); - // let executor = cx.background().clone(); + fn update_entries(&mut self, select_same_item: bool, cx: &mut ViewContext) { + let channel_store = self.channel_store.read(cx); + let user_store = self.user_store.read(cx); + let query = self.filter_editor.read(cx).text(cx); + let executor = cx.background_executor().clone(); - // let prev_selected_entry = self.selection.and_then(|ix| self.entries.get(ix).cloned()); - // let old_entries = mem::take(&mut self.entries); - // let mut scroll_to_top = false; + // let prev_selected_entry = self.selection.and_then(|ix| self.entries.get(ix).cloned()); + let _old_entries = mem::take(&mut self.entries); + // let mut scroll_to_top = false; - // if let Some(room) = ActiveCall::global(cx).read(cx).room() { - // self.entries.push(ListEntry::Header(Section::ActiveCall)); - // if !old_entries - // .iter() - // .any(|entry| matches!(entry, ListEntry::Header(Section::ActiveCall))) - // { - // scroll_to_top = true; - // } + // if let Some(room) = ActiveCall::global(cx).read(cx).room() { + // self.entries.push(ListEntry::Header(Section::ActiveCall)); + // if !old_entries + // .iter() + // .any(|entry| matches!(entry, ListEntry::Header(Section::ActiveCall))) + // { + // scroll_to_top = true; + // } - // if !self.collapsed_sections.contains(&Section::ActiveCall) { - // let room = room.read(cx); + // if !self.collapsed_sections.contains(&Section::ActiveCall) { + // let room = room.read(cx); - // if let Some(channel_id) = room.channel_id() { - // self.entries.push(ListEntry::ChannelNotes { channel_id }); - // self.entries.push(ListEntry::ChannelChat { channel_id }) - // } + // if let Some(channel_id) = room.channel_id() { + // self.entries.push(ListEntry::ChannelNotes { channel_id }); + // self.entries.push(ListEntry::ChannelChat { channel_id }) + // } - // // Populate the active user. - // if let Some(user) = user_store.current_user() { - // self.match_candidates.clear(); - // self.match_candidates.push(StringMatchCandidate { - // id: 0, - // string: user.github_login.clone(), - // char_bag: user.github_login.chars().collect(), - // }); - // let matches = executor.block(match_strings( - // &self.match_candidates, - // &query, - // true, - // usize::MAX, - // &Default::default(), - // executor.clone(), - // )); - // if !matches.is_empty() { - // let user_id = user.id; - // self.entries.push(ListEntry::CallParticipant { - // user, - // peer_id: None, - // is_pending: false, - // }); - // let mut projects = room.local_participant().projects.iter().peekable(); - // while let Some(project) = projects.next() { - // self.entries.push(ListEntry::ParticipantProject { - // project_id: project.id, - // worktree_root_names: project.worktree_root_names.clone(), - // host_user_id: user_id, - // is_last: projects.peek().is_none() && !room.is_screen_sharing(), - // }); - // } - // if room.is_screen_sharing() { - // self.entries.push(ListEntry::ParticipantScreen { - // peer_id: None, - // is_last: true, - // }); - // } - // } - // } + // // Populate the active user. + // if let Some(user) = user_store.current_user() { + // self.match_candidates.clear(); + // self.match_candidates.push(StringMatchCandidate { + // id: 0, + // string: user.github_login.clone(), + // char_bag: user.github_login.chars().collect(), + // }); + // let matches = executor.block(match_strings( + // &self.match_candidates, + // &query, + // true, + // usize::MAX, + // &Default::default(), + // executor.clone(), + // )); + // if !matches.is_empty() { + // let user_id = user.id; + // self.entries.push(ListEntry::CallParticipant { + // user, + // peer_id: None, + // is_pending: false, + // }); + // let mut projects = room.local_participant().projects.iter().peekable(); + // while let Some(project) = projects.next() { + // self.entries.push(ListEntry::ParticipantProject { + // project_id: project.id, + // worktree_root_names: project.worktree_root_names.clone(), + // host_user_id: user_id, + // is_last: projects.peek().is_none() && !room.is_screen_sharing(), + // }); + // } + // if room.is_screen_sharing() { + // self.entries.push(ListEntry::ParticipantScreen { + // peer_id: None, + // is_last: true, + // }); + // } + // } + // } - // // Populate remote participants. - // self.match_candidates.clear(); - // self.match_candidates - // .extend(room.remote_participants().iter().map(|(_, participant)| { - // StringMatchCandidate { - // id: participant.user.id as usize, - // string: participant.user.github_login.clone(), - // char_bag: participant.user.github_login.chars().collect(), - // } - // })); - // let matches = executor.block(match_strings( - // &self.match_candidates, - // &query, - // true, - // usize::MAX, - // &Default::default(), - // executor.clone(), - // )); - // for mat in matches { - // let user_id = mat.candidate_id as u64; - // let participant = &room.remote_participants()[&user_id]; - // self.entries.push(ListEntry::CallParticipant { - // user: participant.user.clone(), - // peer_id: Some(participant.peer_id), - // is_pending: false, - // }); - // let mut projects = participant.projects.iter().peekable(); - // while let Some(project) = projects.next() { - // self.entries.push(ListEntry::ParticipantProject { - // project_id: project.id, - // worktree_root_names: project.worktree_root_names.clone(), - // host_user_id: participant.user.id, - // is_last: projects.peek().is_none() - // && participant.video_tracks.is_empty(), - // }); - // } - // if !participant.video_tracks.is_empty() { - // self.entries.push(ListEntry::ParticipantScreen { - // peer_id: Some(participant.peer_id), - // is_last: true, - // }); - // } - // } + // // Populate remote participants. + // self.match_candidates.clear(); + // self.match_candidates + // .extend(room.remote_participants().iter().map(|(_, participant)| { + // StringMatchCandidate { + // id: participant.user.id as usize, + // string: participant.user.github_login.clone(), + // char_bag: participant.user.github_login.chars().collect(), + // } + // })); + // let matches = executor.block(match_strings( + // &self.match_candidates, + // &query, + // true, + // usize::MAX, + // &Default::default(), + // executor.clone(), + // )); + // for mat in matches { + // let user_id = mat.candidate_id as u64; + // let participant = &room.remote_participants()[&user_id]; + // self.entries.push(ListEntry::CallParticipant { + // user: participant.user.clone(), + // peer_id: Some(participant.peer_id), + // is_pending: false, + // }); + // let mut projects = participant.projects.iter().peekable(); + // while let Some(project) = projects.next() { + // self.entries.push(ListEntry::ParticipantProject { + // project_id: project.id, + // worktree_root_names: project.worktree_root_names.clone(), + // host_user_id: participant.user.id, + // is_last: projects.peek().is_none() + // && participant.video_tracks.is_empty(), + // }); + // } + // if !participant.video_tracks.is_empty() { + // self.entries.push(ListEntry::ParticipantScreen { + // peer_id: Some(participant.peer_id), + // is_last: true, + // }); + // } + // } - // // Populate pending participants. - // self.match_candidates.clear(); - // self.match_candidates - // .extend(room.pending_participants().iter().enumerate().map( - // |(id, participant)| StringMatchCandidate { - // id, - // string: participant.github_login.clone(), - // char_bag: participant.github_login.chars().collect(), - // }, - // )); - // let matches = executor.block(match_strings( - // &self.match_candidates, - // &query, - // true, - // usize::MAX, - // &Default::default(), - // executor.clone(), - // )); - // self.entries - // .extend(matches.iter().map(|mat| ListEntry::CallParticipant { - // user: room.pending_participants()[mat.candidate_id].clone(), - // peer_id: None, - // is_pending: true, - // })); - // } - // } + // // Populate pending participants. + // self.match_candidates.clear(); + // self.match_candidates + // .extend(room.pending_participants().iter().enumerate().map( + // |(id, participant)| StringMatchCandidate { + // id, + // string: participant.github_login.clone(), + // char_bag: participant.github_login.chars().collect(), + // }, + // )); + // let matches = executor.block(match_strings( + // &self.match_candidates, + // &query, + // true, + // usize::MAX, + // &Default::default(), + // executor.clone(), + // )); + // self.entries + // .extend(matches.iter().map(|mat| ListEntry::CallParticipant { + // user: room.pending_participants()[mat.candidate_id].clone(), + // peer_id: None, + // is_pending: true, + // })); + // } + // } - // let mut request_entries = Vec::new(); + let mut request_entries = Vec::new(); - // if cx.has_flag::() { - // self.entries.push(ListEntry::Header(Section::Channels)); + if cx.has_flag::() { + self.entries.push(ListEntry::Header(Section::Channels)); - // if channel_store.channel_count() > 0 || self.channel_editing_state.is_some() { - // self.match_candidates.clear(); - // self.match_candidates - // .extend(channel_store.ordered_channels().enumerate().map( - // |(ix, (_, channel))| StringMatchCandidate { - // id: ix, - // string: channel.name.clone(), - // char_bag: channel.name.chars().collect(), - // }, - // )); - // let matches = executor.block(match_strings( - // &self.match_candidates, - // &query, - // true, - // usize::MAX, - // &Default::default(), - // executor.clone(), - // )); - // if let Some(state) = &self.channel_editing_state { - // if matches!(state, ChannelEditingState::Create { location: None, .. }) { - // self.entries.push(ListEntry::ChannelEditor { depth: 0 }); - // } - // } - // let mut collapse_depth = None; - // for mat in matches { - // let channel = channel_store.channel_at_index(mat.candidate_id).unwrap(); - // let depth = channel.parent_path.len(); + if channel_store.channel_count() > 0 || self.channel_editing_state.is_some() { + self.match_candidates.clear(); + self.match_candidates + .extend(channel_store.ordered_channels().enumerate().map( + |(ix, (_, channel))| StringMatchCandidate { + id: ix, + string: channel.name.clone(), + char_bag: channel.name.chars().collect(), + }, + )); + let matches = executor.block(match_strings( + &self.match_candidates, + &query, + true, + usize::MAX, + &Default::default(), + executor.clone(), + )); + if let Some(state) = &self.channel_editing_state { + if matches!(state, ChannelEditingState::Create { location: None, .. }) { + self.entries.push(ListEntry::ChannelEditor { depth: 0 }); + } + } + let mut collapse_depth = None; + for mat in matches { + let channel = channel_store.channel_at_index(mat.candidate_id).unwrap(); + let depth = channel.parent_path.len(); - // if collapse_depth.is_none() && self.is_channel_collapsed(channel.id) { - // collapse_depth = Some(depth); - // } else if let Some(collapsed_depth) = collapse_depth { - // if depth > collapsed_depth { - // continue; - // } - // if self.is_channel_collapsed(channel.id) { - // collapse_depth = Some(depth); - // } else { - // collapse_depth = None; - // } - // } + if collapse_depth.is_none() && self.is_channel_collapsed(channel.id) { + collapse_depth = Some(depth); + } else if let Some(collapsed_depth) = collapse_depth { + if depth > collapsed_depth { + continue; + } + if self.is_channel_collapsed(channel.id) { + collapse_depth = Some(depth); + } else { + collapse_depth = None; + } + } - // let has_children = channel_store - // .channel_at_index(mat.candidate_id + 1) - // .map_or(false, |next_channel| { - // next_channel.parent_path.ends_with(&[channel.id]) - // }); + let has_children = channel_store + .channel_at_index(mat.candidate_id + 1) + .map_or(false, |next_channel| { + next_channel.parent_path.ends_with(&[channel.id]) + }); - // match &self.channel_editing_state { - // Some(ChannelEditingState::Create { - // location: parent_id, - // .. - // }) if *parent_id == Some(channel.id) => { - // self.entries.push(ListEntry::Channel { - // channel: channel.clone(), - // depth, - // has_children: false, - // }); - // self.entries - // .push(ListEntry::ChannelEditor { depth: depth + 1 }); - // } - // Some(ChannelEditingState::Rename { - // location: parent_id, - // .. - // }) if parent_id == &channel.id => { - // self.entries.push(ListEntry::ChannelEditor { depth }); - // } - // _ => { - // self.entries.push(ListEntry::Channel { - // channel: channel.clone(), - // depth, - // has_children, - // }); - // } - // } - // } - // } + match &self.channel_editing_state { + Some(ChannelEditingState::Create { + location: parent_id, + .. + }) if *parent_id == Some(channel.id) => { + self.entries.push(ListEntry::Channel { + channel: channel.clone(), + depth, + has_children: false, + }); + self.entries + .push(ListEntry::ChannelEditor { depth: depth + 1 }); + } + Some(ChannelEditingState::Rename { + location: parent_id, + .. + }) if parent_id == &channel.id => { + self.entries.push(ListEntry::ChannelEditor { depth }); + } + _ => { + self.entries.push(ListEntry::Channel { + channel: channel.clone(), + depth, + has_children, + }); + } + } + } + } - // let channel_invites = channel_store.channel_invitations(); - // if !channel_invites.is_empty() { - // self.match_candidates.clear(); - // self.match_candidates - // .extend(channel_invites.iter().enumerate().map(|(ix, channel)| { - // StringMatchCandidate { - // id: ix, - // string: channel.name.clone(), - // char_bag: channel.name.chars().collect(), - // } - // })); - // let matches = executor.block(match_strings( - // &self.match_candidates, - // &query, - // true, - // usize::MAX, - // &Default::default(), - // executor.clone(), - // )); - // request_entries.extend(matches.iter().map(|mat| { - // ListEntry::ChannelInvite(channel_invites[mat.candidate_id].clone()) - // })); + // let channel_invites = channel_store.channel_invitations(); + // if !channel_invites.is_empty() { + // self.match_candidates.clear(); + // self.match_candidates + // .extend(channel_invites.iter().enumerate().map(|(ix, channel)| { + // StringMatchCandidate { + // id: ix, + // string: channel.name.clone(), + // char_bag: channel.name.chars().collect(), + // } + // })); + // let matches = executor.block(match_strings( + // &self.match_candidates, + // &query, + // true, + // usize::MAX, + // &Default::default(), + // executor.clone(), + // )); + // request_entries.extend(matches.iter().map(|mat| { + // ListEntry::ChannelInvite(channel_invites[mat.candidate_id].clone()) + // })); - // if !request_entries.is_empty() { - // self.entries - // .push(ListEntry::Header(Section::ChannelInvites)); - // if !self.collapsed_sections.contains(&Section::ChannelInvites) { - // self.entries.append(&mut request_entries); - // } - // } - // } - // } + // if !request_entries.is_empty() { + // self.entries + // .push(ListEntry::Header(Section::ChannelInvites)); + // if !self.collapsed_sections.contains(&Section::ChannelInvites) { + // self.entries.append(&mut request_entries); + // } + // } + // } + } - // self.entries.push(ListEntry::Header(Section::Contacts)); + self.entries.push(ListEntry::Header(Section::Contacts)); - // request_entries.clear(); - // let incoming = user_store.incoming_contact_requests(); - // if !incoming.is_empty() { - // self.match_candidates.clear(); - // self.match_candidates - // .extend( - // incoming - // .iter() - // .enumerate() - // .map(|(ix, user)| StringMatchCandidate { - // id: ix, - // string: user.github_login.clone(), - // char_bag: user.github_login.chars().collect(), - // }), - // ); - // let matches = executor.block(match_strings( - // &self.match_candidates, - // &query, - // true, - // usize::MAX, - // &Default::default(), - // executor.clone(), - // )); - // request_entries.extend( - // matches - // .iter() - // .map(|mat| ListEntry::IncomingRequest(incoming[mat.candidate_id].clone())), - // ); - // } + request_entries.clear(); + let incoming = user_store.incoming_contact_requests(); + if !incoming.is_empty() { + self.match_candidates.clear(); + self.match_candidates + .extend( + incoming + .iter() + .enumerate() + .map(|(ix, user)| StringMatchCandidate { + id: ix, + string: user.github_login.clone(), + char_bag: user.github_login.chars().collect(), + }), + ); + let matches = executor.block(match_strings( + &self.match_candidates, + &query, + true, + usize::MAX, + &Default::default(), + executor.clone(), + )); + request_entries.extend( + matches + .iter() + .map(|mat| ListEntry::IncomingRequest(incoming[mat.candidate_id].clone())), + ); + } - // let outgoing = user_store.outgoing_contact_requests(); - // if !outgoing.is_empty() { - // self.match_candidates.clear(); - // self.match_candidates - // .extend( - // outgoing - // .iter() - // .enumerate() - // .map(|(ix, user)| StringMatchCandidate { - // id: ix, - // string: user.github_login.clone(), - // char_bag: user.github_login.chars().collect(), - // }), - // ); - // let matches = executor.block(match_strings( - // &self.match_candidates, - // &query, - // true, - // usize::MAX, - // &Default::default(), - // executor.clone(), - // )); - // request_entries.extend( - // matches - // .iter() - // .map(|mat| ListEntry::OutgoingRequest(outgoing[mat.candidate_id].clone())), - // ); - // } + let outgoing = user_store.outgoing_contact_requests(); + if !outgoing.is_empty() { + self.match_candidates.clear(); + self.match_candidates + .extend( + outgoing + .iter() + .enumerate() + .map(|(ix, user)| StringMatchCandidate { + id: ix, + string: user.github_login.clone(), + char_bag: user.github_login.chars().collect(), + }), + ); + let matches = executor.block(match_strings( + &self.match_candidates, + &query, + true, + usize::MAX, + &Default::default(), + executor.clone(), + )); + request_entries.extend( + matches + .iter() + .map(|mat| ListEntry::OutgoingRequest(outgoing[mat.candidate_id].clone())), + ); + } - // if !request_entries.is_empty() { - // self.entries - // .push(ListEntry::Header(Section::ContactRequests)); - // if !self.collapsed_sections.contains(&Section::ContactRequests) { - // self.entries.append(&mut request_entries); - // } - // } + if !request_entries.is_empty() { + self.entries + .push(ListEntry::Header(Section::ContactRequests)); + if !self.collapsed_sections.contains(&Section::ContactRequests) { + self.entries.append(&mut request_entries); + } + } - // let contacts = user_store.contacts(); - // if !contacts.is_empty() { - // self.match_candidates.clear(); - // self.match_candidates - // .extend( - // contacts - // .iter() - // .enumerate() - // .map(|(ix, contact)| StringMatchCandidate { - // id: ix, - // string: contact.user.github_login.clone(), - // char_bag: contact.user.github_login.chars().collect(), - // }), - // ); + let contacts = user_store.contacts(); + if !contacts.is_empty() { + self.match_candidates.clear(); + self.match_candidates + .extend( + contacts + .iter() + .enumerate() + .map(|(ix, contact)| StringMatchCandidate { + id: ix, + string: contact.user.github_login.clone(), + char_bag: contact.user.github_login.chars().collect(), + }), + ); - // let matches = executor.block(match_strings( - // &self.match_candidates, - // &query, - // true, - // usize::MAX, - // &Default::default(), - // executor.clone(), - // )); + let matches = executor.block(match_strings( + &self.match_candidates, + &query, + true, + usize::MAX, + &Default::default(), + executor.clone(), + )); - // let (online_contacts, offline_contacts) = matches - // .iter() - // .partition::, _>(|mat| contacts[mat.candidate_id].online); + let (online_contacts, offline_contacts) = matches + .iter() + .partition::, _>(|mat| contacts[mat.candidate_id].online); - // for (matches, section) in [ - // (online_contacts, Section::Online), - // (offline_contacts, Section::Offline), - // ] { - // if !matches.is_empty() { - // self.entries.push(ListEntry::Header(section)); - // if !self.collapsed_sections.contains(§ion) { - // let active_call = &ActiveCall::global(cx).read(cx); - // for mat in matches { - // let contact = &contacts[mat.candidate_id]; - // self.entries.push(ListEntry::Contact { - // contact: contact.clone(), - // calling: active_call.pending_invites().contains(&contact.user.id), - // }); - // } - // } - // } - // } - // } + for (matches, section) in [ + (online_contacts, Section::Online), + (offline_contacts, Section::Offline), + ] { + if !matches.is_empty() { + self.entries.push(ListEntry::Header(section)); + if !self.collapsed_sections.contains(§ion) { + let active_call = &ActiveCall::global(cx).read(cx); + for mat in matches { + let contact = &contacts[mat.candidate_id]; + self.entries.push(ListEntry::Contact { + contact: contact.clone(), + calling: active_call.pending_invites().contains(&contact.user.id), + }); + } + } + } + } + } - // if incoming.is_empty() && outgoing.is_empty() && contacts.is_empty() { - // self.entries.push(ListEntry::ContactPlaceholder); - // } + if incoming.is_empty() && outgoing.is_empty() && contacts.is_empty() { + self.entries.push(ListEntry::ContactPlaceholder); + } - // if select_same_item { - // if let Some(prev_selected_entry) = prev_selected_entry { - // self.selection.take(); - // for (ix, entry) in self.entries.iter().enumerate() { - // if *entry == prev_selected_entry { - // self.selection = Some(ix); - // break; - // } - // } - // } - // } else { - // self.selection = self.selection.and_then(|prev_selection| { - // if self.entries.is_empty() { - // None - // } else { - // Some(prev_selection.min(self.entries.len() - 1)) - // } - // }); - // } + // if select_same_item { + // if let Some(prev_selected_entry) = prev_selected_entry { + // self.selection.take(); + // for (ix, entry) in self.entries.iter().enumerate() { + // if *entry == prev_selected_entry { + // self.selection = Some(ix); + // break; + // } + // } + // } + // } else { + // self.selection = self.selection.and_then(|prev_selection| { + // if self.entries.is_empty() { + // None + // } else { + // Some(prev_selection.min(self.entries.len() - 1)) + // } + // }); + // } - // let old_scroll_top = self.list_state.logical_scroll_top(); + // let old_scroll_top = self.list_state.logical_scroll_top(); - // self.list_state.reset(self.entries.len()); + // self.list_state.reset(self.entries.len()); - // if scroll_to_top { - // self.list_state.scroll_to(ListOffset::default()); - // } else { - // // Attempt to maintain the same scroll position. - // if let Some(old_top_entry) = old_entries.get(old_scroll_top.item_ix) { - // let new_scroll_top = self - // .entries - // .iter() - // .position(|entry| entry == old_top_entry) - // .map(|item_ix| ListOffset { - // item_ix, - // offset_in_item: old_scroll_top.offset_in_item, - // }) - // .or_else(|| { - // let entry_after_old_top = old_entries.get(old_scroll_top.item_ix + 1)?; - // let item_ix = self - // .entries - // .iter() - // .position(|entry| entry == entry_after_old_top)?; - // Some(ListOffset { - // item_ix, - // offset_in_item: 0., - // }) - // }) - // .or_else(|| { - // let entry_before_old_top = - // old_entries.get(old_scroll_top.item_ix.saturating_sub(1))?; - // let item_ix = self - // .entries - // .iter() - // .position(|entry| entry == entry_before_old_top)?; - // Some(ListOffset { - // item_ix, - // offset_in_item: 0., - // }) - // }); + // if scroll_to_top { + // self.list_state.scroll_to(ListOffset::default()); + // } else { + // // Attempt to maintain the same scroll position. + // if let Some(old_top_entry) = old_entries.get(old_scroll_top.item_ix) { + // let new_scroll_top = self + // .entries + // .iter() + // .position(|entry| entry == old_top_entry) + // .map(|item_ix| ListOffset { + // item_ix, + // offset_in_item: old_scroll_top.offset_in_item, + // }) + // .or_else(|| { + // let entry_after_old_top = old_entries.get(old_scroll_top.item_ix + 1)?; + // let item_ix = self + // .entries + // .iter() + // .position(|entry| entry == entry_after_old_top)?; + // Some(ListOffset { + // item_ix, + // offset_in_item: 0., + // }) + // }) + // .or_else(|| { + // let entry_before_old_top = + // old_entries.get(old_scroll_top.item_ix.saturating_sub(1))?; + // let item_ix = self + // .entries + // .iter() + // .position(|entry| entry == entry_before_old_top)?; + // Some(ListOffset { + // item_ix, + // offset_in_item: 0., + // }) + // }); - // self.list_state - // .scroll_to(new_scroll_top.unwrap_or(old_scroll_top)); - // } - // } + // self.list_state + // .scroll_to(new_scroll_top.unwrap_or(old_scroll_top)); + // } + // } - // cx.notify(); - // } + cx.notify(); + } // fn render_call_participant( // user: &User, @@ -1455,389 +1464,6 @@ impl CollabPanel { // } // } - // fn render_header( - // &self, - // section: Section, - // theme: &theme::Theme, - // is_selected: bool, - // is_collapsed: bool, - // cx: &mut ViewContext, - // ) -> AnyElement { - // enum Header {} - // enum LeaveCallContactList {} - // enum AddChannel {} - - // let tooltip_style = &theme.tooltip; - // let mut channel_link = None; - // let mut channel_tooltip_text = None; - // let mut channel_icon = None; - // let mut is_dragged_over = false; - - // let text = match section { - // Section::ActiveCall => { - // let channel_name = maybe!({ - // let channel_id = ActiveCall::global(cx).read(cx).channel_id(cx)?; - - // let channel = self.channel_store.read(cx).channel_for_id(channel_id)?; - - // channel_link = Some(channel.link()); - // (channel_icon, channel_tooltip_text) = match channel.visibility { - // proto::ChannelVisibility::Public => { - // (Some("icons/public.svg"), Some("Copy public channel link.")) - // } - // proto::ChannelVisibility::Members => { - // (Some("icons/hash.svg"), Some("Copy private channel link.")) - // } - // }; - - // Some(channel.name.as_str()) - // }); - - // if let Some(name) = channel_name { - // Cow::Owned(format!("{}", name)) - // } else { - // Cow::Borrowed("Current Call") - // } - // } - // Section::ContactRequests => Cow::Borrowed("Requests"), - // Section::Contacts => Cow::Borrowed("Contacts"), - // Section::Channels => Cow::Borrowed("Channels"), - // Section::ChannelInvites => Cow::Borrowed("Invites"), - // Section::Online => Cow::Borrowed("Online"), - // Section::Offline => Cow::Borrowed("Offline"), - // }; - - // enum AddContact {} - // let button = match section { - // Section::ActiveCall => channel_link.map(|channel_link| { - // let channel_link_copy = channel_link.clone(); - // MouseEventHandler::new::(0, cx, |state, _| { - // render_icon_button( - // theme - // .collab_panel - // .leave_call_button - // .style_for(is_selected, state), - // "icons/link.svg", - // ) - // }) - // .with_cursor_style(CursorStyle::PointingHand) - // .on_click(MouseButton::Left, move |_, _, cx| { - // let item = ClipboardItem::new(channel_link_copy.clone()); - // cx.write_to_clipboard(item) - // }) - // .with_tooltip::( - // 0, - // channel_tooltip_text.unwrap(), - // None, - // tooltip_style.clone(), - // cx, - // ) - // }), - // Section::Contacts => Some( - // MouseEventHandler::new::(0, cx, |state, _| { - // render_icon_button( - // theme - // .collab_panel - // .add_contact_button - // .style_for(is_selected, state), - // "icons/plus.svg", - // ) - // }) - // .with_cursor_style(CursorStyle::PointingHand) - // .on_click(MouseButton::Left, |_, this, cx| { - // this.toggle_contact_finder(cx); - // }) - // .with_tooltip::( - // 0, - // "Search for new contact", - // None, - // tooltip_style.clone(), - // cx, - // ), - // ), - // Section::Channels => { - // if cx - // .global::>() - // .currently_dragged::(cx.window()) - // .is_some() - // && self.drag_target_channel == ChannelDragTarget::Root - // { - // is_dragged_over = true; - // } - - // Some( - // MouseEventHandler::new::(0, cx, |state, _| { - // render_icon_button( - // theme - // .collab_panel - // .add_contact_button - // .style_for(is_selected, state), - // "icons/plus.svg", - // ) - // }) - // .with_cursor_style(CursorStyle::PointingHand) - // .on_click(MouseButton::Left, |_, this, cx| this.new_root_channel(cx)) - // .with_tooltip::( - // 0, - // "Create a channel", - // None, - // tooltip_style.clone(), - // cx, - // ), - // ) - // } - // _ => None, - // }; - - // let can_collapse = match section { - // Section::ActiveCall | Section::Channels | Section::Contacts => false, - // Section::ChannelInvites - // | Section::ContactRequests - // | Section::Online - // | Section::Offline => true, - // }; - // let icon_size = (&theme.collab_panel).section_icon_size; - // let mut result = MouseEventHandler::new::(section as usize, cx, |state, _| { - // let header_style = if can_collapse { - // theme - // .collab_panel - // .subheader_row - // .in_state(is_selected) - // .style_for(state) - // } else { - // &theme.collab_panel.header_row - // }; - - // Flex::row() - // .with_children(if can_collapse { - // Some( - // Svg::new(if is_collapsed { - // "icons/chevron_right.svg" - // } else { - // "icons/chevron_down.svg" - // }) - // .with_color(header_style.text.color) - // .constrained() - // .with_max_width(icon_size) - // .with_max_height(icon_size) - // .aligned() - // .constrained() - // .with_width(icon_size) - // .contained() - // .with_margin_right( - // theme.collab_panel.contact_username.container.margin.left, - // ), - // ) - // } else if let Some(channel_icon) = channel_icon { - // Some( - // Svg::new(channel_icon) - // .with_color(header_style.text.color) - // .constrained() - // .with_max_width(icon_size) - // .with_max_height(icon_size) - // .aligned() - // .constrained() - // .with_width(icon_size) - // .contained() - // .with_margin_right( - // theme.collab_panel.contact_username.container.margin.left, - // ), - // ) - // } else { - // None - // }) - // .with_child( - // Label::new(text, header_style.text.clone()) - // .aligned() - // .left() - // .flex(1., true), - // ) - // .with_children(button.map(|button| button.aligned().right())) - // .constrained() - // .with_height(theme.collab_panel.row_height) - // .contained() - // .with_style(if is_dragged_over { - // theme.collab_panel.dragged_over_header - // } else { - // header_style.container - // }) - // }); - - // result = result - // .on_move(move |_, this, cx| { - // if cx - // .global::>() - // .currently_dragged::(cx.window()) - // .is_some() - // { - // this.drag_target_channel = ChannelDragTarget::Root; - // cx.notify() - // } - // }) - // .on_up(MouseButton::Left, move |_, this, cx| { - // if let Some((_, dragged_channel)) = cx - // .global::>() - // .currently_dragged::(cx.window()) - // { - // this.channel_store - // .update(cx, |channel_store, cx| { - // channel_store.move_channel(dragged_channel.id, None, cx) - // }) - // .detach_and_log_err(cx) - // } - // }); - - // if can_collapse { - // result = result - // .with_cursor_style(CursorStyle::PointingHand) - // .on_click(MouseButton::Left, move |_, this, cx| { - // if can_collapse { - // this.toggle_section_expanded(section, cx); - // } - // }) - // } - - // result.into_any() - // } - - // fn render_contact( - // contact: &Contact, - // calling: bool, - // project: &ModelHandle, - // theme: &theme::Theme, - // is_selected: bool, - // cx: &mut ViewContext, - // ) -> AnyElement { - // enum ContactTooltip {} - - // let collab_theme = &theme.collab_panel; - // let online = contact.online; - // let busy = contact.busy || calling; - // let user_id = contact.user.id; - // let github_login = contact.user.github_login.clone(); - // let initial_project = project.clone(); - - // let event_handler = - // MouseEventHandler::new::(contact.user.id as usize, cx, |state, cx| { - // Flex::row() - // .with_children(contact.user.avatar.clone().map(|avatar| { - // let status_badge = if contact.online { - // Some( - // Empty::new() - // .collapsed() - // .contained() - // .with_style(if busy { - // collab_theme.contact_status_busy - // } else { - // collab_theme.contact_status_free - // }) - // .aligned(), - // ) - // } else { - // None - // }; - // Stack::new() - // .with_child( - // Image::from_data(avatar) - // .with_style(collab_theme.contact_avatar) - // .aligned() - // .left(), - // ) - // .with_children(status_badge) - // })) - // .with_child( - // Label::new( - // contact.user.github_login.clone(), - // collab_theme.contact_username.text.clone(), - // ) - // .contained() - // .with_style(collab_theme.contact_username.container) - // .aligned() - // .left() - // .flex(1., true), - // ) - // .with_children(if state.hovered() { - // Some( - // MouseEventHandler::new::( - // contact.user.id as usize, - // cx, - // |mouse_state, _| { - // let button_style = - // collab_theme.contact_button.style_for(mouse_state); - // render_icon_button(button_style, "icons/x.svg") - // .aligned() - // .flex_float() - // }, - // ) - // .with_padding(Padding::uniform(2.)) - // .with_cursor_style(CursorStyle::PointingHand) - // .on_click(MouseButton::Left, move |_, this, cx| { - // this.remove_contact(user_id, &github_login, cx); - // }) - // .flex_float(), - // ) - // } else { - // None - // }) - // .with_children(if calling { - // Some( - // Label::new("Calling", collab_theme.calling_indicator.text.clone()) - // .contained() - // .with_style(collab_theme.calling_indicator.container) - // .aligned(), - // ) - // } else { - // None - // }) - // .constrained() - // .with_height(collab_theme.row_height) - // .contained() - // .with_style( - // *collab_theme - // .contact_row - // .in_state(is_selected) - // .style_for(state), - // ) - // }); - - // if online && !busy { - // let room = ActiveCall::global(cx).read(cx).room(); - // let label = if room.is_some() { - // format!("Invite {} to join call", contact.user.github_login) - // } else { - // format!("Call {}", contact.user.github_login) - // }; - - // event_handler - // .on_click(MouseButton::Left, move |_, this, cx| { - // this.call(user_id, Some(initial_project.clone()), cx); - // }) - // .with_cursor_style(CursorStyle::PointingHand) - // .with_tooltip::( - // contact.user.id as usize, - // label, - // None, - // theme.tooltip.clone(), - // cx, - // ) - // .into_any() - // } else { - // event_handler - // .with_tooltip::( - // contact.user.id as usize, - // format!( - // "{} is {}", - // contact.user.github_login, - // if busy { "on a call" } else { "offline" } - // ), - // None, - // theme.tooltip.clone(), - // cx, - // ) - // .into_any() - // } - // } - // fn render_contact_placeholder( // &self, // theme: &theme::CollabPanel, @@ -1933,335 +1559,6 @@ impl CollabPanel { // .into_any() // } - // fn render_channel( - // &self, - // channel: &Channel, - // depth: usize, - // theme: &theme::Theme, - // is_selected: bool, - // has_children: bool, - // ix: usize, - // cx: &mut ViewContext, - // ) -> AnyElement { - // let channel_id = channel.id; - // let collab_theme = &theme.collab_panel; - // let is_public = self - // .channel_store - // .read(cx) - // .channel_for_id(channel_id) - // .map(|channel| channel.visibility) - // == Some(proto::ChannelVisibility::Public); - // let other_selected = self.selected_channel().map(|channel| channel.id) == Some(channel.id); - // let disclosed = - // has_children.then(|| !self.collapsed_channels.binary_search(&channel.id).is_ok()); - - // let is_active = maybe!({ - // let call_channel = ActiveCall::global(cx) - // .read(cx) - // .room()? - // .read(cx) - // .channel_id()?; - // Some(call_channel == channel_id) - // }) - // .unwrap_or(false); - - // const FACEPILE_LIMIT: usize = 3; - - // enum ChannelCall {} - // enum ChannelNote {} - // enum NotesTooltip {} - // enum ChatTooltip {} - // enum ChannelTooltip {} - - // let mut is_dragged_over = false; - // if cx - // .global::>() - // .currently_dragged::(cx.window()) - // .is_some() - // && self.drag_target_channel == ChannelDragTarget::Channel(channel_id) - // { - // is_dragged_over = true; - // } - - // let has_messages_notification = channel.unseen_message_id.is_some(); - - // MouseEventHandler::new::(ix, cx, |state, cx| { - // let row_hovered = state.hovered(); - - // let mut select_state = |interactive: &Interactive| { - // if state.clicked() == Some(MouseButton::Left) && interactive.clicked.is_some() { - // interactive.clicked.as_ref().unwrap().clone() - // } else if state.hovered() || other_selected { - // interactive - // .hovered - // .as_ref() - // .unwrap_or(&interactive.default) - // .clone() - // } else { - // interactive.default.clone() - // } - // }; - - // Flex::::row() - // .with_child( - // Svg::new(if is_public { - // "icons/public.svg" - // } else { - // "icons/hash.svg" - // }) - // .with_color(collab_theme.channel_hash.color) - // .constrained() - // .with_width(collab_theme.channel_hash.width) - // .aligned() - // .left(), - // ) - // .with_child({ - // let style = collab_theme.channel_name.inactive_state(); - // Flex::row() - // .with_child( - // Label::new(channel.name.clone(), style.text.clone()) - // .contained() - // .with_style(style.container) - // .aligned() - // .left() - // .with_tooltip::( - // ix, - // "Join channel", - // None, - // theme.tooltip.clone(), - // cx, - // ), - // ) - // .with_children({ - // let participants = - // self.channel_store.read(cx).channel_participants(channel_id); - - // if !participants.is_empty() { - // let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT); - - // let result = FacePile::new(collab_theme.face_overlap) - // .with_children( - // participants - // .iter() - // .filter_map(|user| { - // Some( - // Image::from_data(user.avatar.clone()?) - // .with_style(collab_theme.channel_avatar), - // ) - // }) - // .take(FACEPILE_LIMIT), - // ) - // .with_children((extra_count > 0).then(|| { - // Label::new( - // format!("+{}", extra_count), - // collab_theme.extra_participant_label.text.clone(), - // ) - // .contained() - // .with_style(collab_theme.extra_participant_label.container) - // })); - - // Some(result) - // } else { - // None - // } - // }) - // .with_spacing(8.) - // .align_children_center() - // .flex(1., true) - // }) - // .with_child( - // MouseEventHandler::new::(ix, cx, move |mouse_state, _| { - // let container_style = collab_theme - // .disclosure - // .button - // .style_for(mouse_state) - // .container; - - // if channel.unseen_message_id.is_some() { - // Svg::new("icons/conversations.svg") - // .with_color(collab_theme.channel_note_active_color) - // .constrained() - // .with_width(collab_theme.channel_hash.width) - // .contained() - // .with_style(container_style) - // .with_uniform_padding(4.) - // .into_any() - // } else if row_hovered { - // Svg::new("icons/conversations.svg") - // .with_color(collab_theme.channel_hash.color) - // .constrained() - // .with_width(collab_theme.channel_hash.width) - // .contained() - // .with_style(container_style) - // .with_uniform_padding(4.) - // .into_any() - // } else { - // Empty::new().into_any() - // } - // }) - // .on_click(MouseButton::Left, move |_, this, cx| { - // this.join_channel_chat(&JoinChannelChat { channel_id }, cx); - // }) - // .with_tooltip::( - // ix, - // "Open channel chat", - // None, - // theme.tooltip.clone(), - // cx, - // ) - // .contained() - // .with_margin_right(4.), - // ) - // .with_child( - // MouseEventHandler::new::(ix, cx, move |mouse_state, cx| { - // let container_style = collab_theme - // .disclosure - // .button - // .style_for(mouse_state) - // .container; - // if row_hovered || channel.unseen_note_version.is_some() { - // Svg::new("icons/file.svg") - // .with_color(if channel.unseen_note_version.is_some() { - // collab_theme.channel_note_active_color - // } else { - // collab_theme.channel_hash.color - // }) - // .constrained() - // .with_width(collab_theme.channel_hash.width) - // .contained() - // .with_style(container_style) - // .with_uniform_padding(4.) - // .with_margin_right(collab_theme.channel_hash.container.margin.left) - // .with_tooltip::( - // ix as usize, - // "Open channel notes", - // None, - // theme.tooltip.clone(), - // cx, - // ) - // .into_any() - // } else if has_messages_notification { - // Empty::new() - // .constrained() - // .with_width(collab_theme.channel_hash.width) - // .contained() - // .with_uniform_padding(4.) - // .with_margin_right(collab_theme.channel_hash.container.margin.left) - // .into_any() - // } else { - // Empty::new().into_any() - // } - // }) - // .on_click(MouseButton::Left, move |_, this, cx| { - // this.open_channel_notes(&OpenChannelNotes { channel_id }, cx); - // }), - // ) - // .align_children_center() - // .styleable_component() - // .disclosable( - // disclosed, - // Box::new(ToggleCollapse { - // location: channel.id.clone(), - // }), - // ) - // .with_id(ix) - // .with_style(collab_theme.disclosure.clone()) - // .element() - // .constrained() - // .with_height(collab_theme.row_height) - // .contained() - // .with_style(select_state( - // collab_theme - // .channel_row - // .in_state(is_selected || is_active || is_dragged_over), - // )) - // .with_padding_left( - // collab_theme.channel_row.default_style().padding.left - // + collab_theme.channel_indent * depth as f32, - // ) - // }) - // .on_click(MouseButton::Left, move |_, this, cx| { - // if this.drag_target_channel == ChannelDragTarget::None { - // if is_active { - // this.open_channel_notes(&OpenChannelNotes { channel_id }, cx) - // } else { - // this.join_channel(channel_id, cx) - // } - // } - // }) - // .on_click(MouseButton::Right, { - // let channel = channel.clone(); - // move |e, this, cx| { - // this.deploy_channel_context_menu(Some(e.position), &channel, ix, cx); - // } - // }) - // .on_up(MouseButton::Left, move |_, this, cx| { - // if let Some((_, dragged_channel)) = cx - // .global::>() - // .currently_dragged::(cx.window()) - // { - // this.channel_store - // .update(cx, |channel_store, cx| { - // channel_store.move_channel(dragged_channel.id, Some(channel_id), cx) - // }) - // .detach_and_log_err(cx) - // } - // }) - // .on_move({ - // let channel = channel.clone(); - // move |_, this, cx| { - // if let Some((_, dragged_channel)) = cx - // .global::>() - // .currently_dragged::(cx.window()) - // { - // if channel.id != dragged_channel.id { - // this.drag_target_channel = ChannelDragTarget::Channel(channel.id); - // } - // cx.notify() - // } - // } - // }) - // .as_draggable::<_, Channel>( - // channel.clone(), - // move |_, channel, cx: &mut ViewContext| { - // let theme = &theme::current(cx).collab_panel; - - // Flex::::row() - // .with_child( - // Svg::new("icons/hash.svg") - // .with_color(theme.channel_hash.color) - // .constrained() - // .with_width(theme.channel_hash.width) - // .aligned() - // .left(), - // ) - // .with_child( - // Label::new(channel.name.clone(), theme.channel_name.text.clone()) - // .contained() - // .with_style(theme.channel_name.container) - // .aligned() - // .left(), - // ) - // .align_children_center() - // .contained() - // .with_background_color( - // theme - // .container - // .background_color - // .unwrap_or(gpui::color::Color::transparent_black()), - // ) - // .contained() - // .with_padding_left( - // theme.channel_row.default_style().padding.left - // + theme.channel_indent * depth as f32, - // ) - // .into_any() - // }, - // ) - // .with_cursor_style(CursorStyle::PointingHand) - // .into_any() - // } - // fn render_channel_notes( // &self, // channel_id: ChannelId, @@ -2948,9 +2245,9 @@ impl CollabPanel { // cx.focus_self(); // } - // fn is_channel_collapsed(&self, channel_id: ChannelId) -> bool { - // self.collapsed_channels.binary_search(&channel_id).is_ok() - // } + fn is_channel_collapsed(&self, channel_id: ChannelId) -> bool { + self.collapsed_channels.binary_search(&channel_id).is_ok() + } // fn leave_call(cx: &mut ViewContext) { // ActiveCall::global(cx) @@ -2958,19 +2255,17 @@ impl CollabPanel { // .detach_and_log_err(cx); // } - // fn toggle_contact_finder(&mut self, cx: &mut ViewContext) { - // if let Some(workspace) = self.workspace.upgrade(cx) { - // workspace.update(cx, |workspace, cx| { - // workspace.toggle_modal(cx, |_, cx| { - // cx.add_view(|cx| { - // let mut finder = ContactFinder::new(self.user_store.clone(), cx); - // finder.set_query(self.filter_editor.read(cx).text(cx), cx); - // finder - // }) - // }); - // }); - // } - // } + fn toggle_contact_finder(&mut self, cx: &mut ViewContext) { + if let Some(workspace) = self.workspace.upgrade() { + workspace.update(cx, |workspace, cx| { + workspace.toggle_modal(cx, |cx| { + let mut finder = ContactFinder::new(self.user_store.clone(), cx); + finder.set_query(self.filter_editor.read(cx).text(cx), cx); + finder + }); + }); + } + } // fn new_root_channel(&mut self, cx: &mut ViewContext) { // self.channel_editing_state = Some(ChannelEditingState::Create { @@ -3247,6 +2542,802 @@ impl CollabPanel { // let item = ClipboardItem::new(channel.link()); // cx.write_to_clipboard(item) // } + + fn render_signed_out(&mut self, cx: &mut ViewContext) -> Div { + v_stack().child(Button::new("Sign in to collaborate").on_click(cx.listener( + |this, _, cx| { + let client = this.client.clone(); + cx.spawn(|_, mut cx| async move { + client + .authenticate_and_connect(true, &cx) + .await + .notify_async_err(&mut cx); + }) + .detach() + }, + ))) + } + + fn render_signed_in(&mut self, cx: &mut ViewContext) -> List { + let is_selected = false; // todo!() this.selection == Some(ix); + + List::new().children(self.entries.clone().into_iter().map(|entry| { + match entry { + ListEntry::Header(section) => { + let is_collapsed = self.collapsed_sections.contains(§ion); + self.render_header(section, is_selected, is_collapsed, cx) + .into_any_element() + } + ListEntry::Contact { contact, calling } => self + .render_contact(&*contact, calling, is_selected, cx) + .into_any_element(), + ListEntry::ContactPlaceholder => self + .render_contact_placeholder(is_selected, cx) + .into_any_element(), + ListEntry::IncomingRequest(user) => self + .render_contact_request(user, true, is_selected, cx) + .into_any_element(), + ListEntry::OutgoingRequest(user) => self + .render_contact_request(user, false, is_selected, cx) + .into_any_element(), + ListEntry::Channel { + channel, + depth, + has_children, + } => self + .render_channel(&*channel, depth, has_children, is_selected, cx) + .into_any_element(), + ListEntry::ChannelEditor { depth } => todo!(), + } + })) + } + + fn render_header( + &mut self, + section: Section, + is_selected: bool, + is_collapsed: bool, + cx: &ViewContext, + ) -> impl IntoElement { + // let mut channel_link = None; + // let mut channel_tooltip_text = None; + // let mut channel_icon = None; + // let mut is_dragged_over = false; + + let text = match section { + Section::ActiveCall => { + // let channel_name = maybe!({ + // let channel_id = ActiveCall::global(cx).read(cx).channel_id(cx)?; + + // let channel = self.channel_store.read(cx).channel_for_id(channel_id)?; + + // channel_link = Some(channel.link()); + // (channel_icon, channel_tooltip_text) = match channel.visibility { + // proto::ChannelVisibility::Public => { + // (Some("icons/public.svg"), Some("Copy public channel link.")) + // } + // proto::ChannelVisibility::Members => { + // (Some("icons/hash.svg"), Some("Copy private channel link.")) + // } + // }; + + // Some(channel.name.as_str()) + // }); + + // if let Some(name) = channel_name { + // SharedString::from(format!("{}", name)) + // } else { + // SharedString::from("Current Call") + // } + todo!() + } + Section::ContactRequests => SharedString::from("Requests"), + Section::Contacts => SharedString::from("Contacts"), + Section::Channels => SharedString::from("Channels"), + Section::ChannelInvites => SharedString::from("Invites"), + Section::Online => SharedString::from("Online"), + Section::Offline => SharedString::from("Offline"), + }; + + let button = match section { + Section::ActiveCall => + // channel_link.map(|channel_link| { + // let channel_link_copy = channel_link.clone(); + // MouseEventHandler::new::(0, cx, |state, _| { + // render_icon_button( + // theme + // .collab_panel + // .leave_call_button + // .style_for(is_selected, state), + // "icons/link.svg", + // ) + // }) + // .with_cursor_style(CursorStyle::PointingHand) + // .on_click(MouseButton::Left, move |_, _, cx| { + // let item = ClipboardItem::new(channel_link_copy.clone()); + // cx.write_to_clipboard(item) + // }) + // .with_tooltip::( + // 0, + // channel_tooltip_text.unwrap(), + // None, + // tooltip_style.clone(), + // cx, + // ) + // }), + { + todo!() + } + Section::Contacts => Some( + IconButton::new("add-contact", Icon::Plus) + .on_click(cx.listener(|this, _, cx| this.toggle_contact_finder(cx))) + .tooltip(|cx| Tooltip::text("Search for new contact", cx)), + ), + Section::Channels => { + // todo!() + // if cx + // .global::>() + // .currently_dragged::(cx.window()) + // .is_some() + // && self.drag_target_channel == ChannelDragTarget::Root + // { + // is_dragged_over = true; + // } + + Some( + IconButton::new("add-channel", Icon::Plus) + .on_click(cx.listener(|this, _, cx| { + todo!() + // this.new_root_channel(cx) + })) + .tooltip(|cx| Tooltip::text("Create a channel", cx)), + ) + } + _ => None, + }; + + let can_collapse = match section { + Section::ActiveCall | Section::Channels | Section::Contacts => false, + Section::ChannelInvites + | Section::ContactRequests + | Section::Online + | Section::Offline => true, + }; + + let mut header = ListHeader::new(text); + if let Some(button) = button { + header = header.right_button(button) + } + // todo!() is selected + if can_collapse { + // todo!() on click to toggle + header = header.toggle(ui::Toggle::Toggled(is_collapsed)); + } + + header + } + fn render_contact( + &mut self, + contact: &Contact, + calling: bool, + is_selected: bool, + cx: &mut ViewContext, + ) -> impl IntoElement { + enum ContactTooltip {} + + let online = contact.online; + let busy = contact.busy || calling; + let user_id = contact.user.id; + let github_login = SharedString::from(contact.user.github_login.clone()); + let mut item = ListItem::new(github_login.clone()) + .on_click(cx.listener(move |this, _, cx| { + this.workspace + .update(cx, |this, cx| { + this.call_state() + .invite(user_id, None, cx) + .detach_and_log_err(cx) + }) + .log_err(); + })) + .child(Label::new(github_login.clone())); + if let Some(avatar) = contact.user.avatar.clone() { + //item = item.left_avatar(avatar); + } + // let event_handler = + // MouseEventHandler::new::(contact.user.id as usize, cx, |state, cx| { + // Flex::row() + // .with_children(contact.user.avatar.clone().map(|avatar| { + // let status_badge = if contact.online { + // Some( + // Empty::new() + // .collapsed() + // .contained() + // .with_style(if busy { + // collab_theme.contact_status_busy + // } else { + // collab_theme.contact_status_free + // }) + // .aligned(), + // ) + // } else { + // None + // }; + // Stack::new() + // .with_child( + // Image::from_data(avatar) + // .with_style(collab_theme.contact_avatar) + // .aligned() + // .left(), + // ) + // .with_children(status_badge) + // })) + // .with_child( + // Label::new( + // contact.user.github_login.clone(), + // collab_theme.contact_username.text.clone(), + // ) + // .contained() + // .with_style(collab_theme.contact_username.container) + // .aligned() + // .left() + // .flex(1., true), + // ) + // .with_children(if state.hovered() { + // Some( + // MouseEventHandler::new::( + // contact.user.id as usize, + // cx, + // |mouse_state, _| { + // let button_style = + // collab_theme.contact_button.style_for(mouse_state); + // render_icon_button(button_style, "icons/x.svg") + // .aligned() + // .flex_float() + // }, + // ) + // .with_padding(Padding::uniform(2.)) + // .with_cursor_style(CursorStyle::PointingHand) + // .on_click(MouseButton::Left, move |_, this, cx| { + // this.remove_contact(user_id, &github_login, cx); + // }) + // .flex_float(), + // ) + // } else { + // None + // }) + // .with_children(if calling { + // Some( + // Label::new("Calling", collab_theme.calling_indicator.text.clone()) + // .contained() + // .with_style(collab_theme.calling_indicator.container) + // .aligned(), + // ) + // } else { + // None + // }) + // .constrained() + // .with_height(collab_theme.row_height) + // .contained() + // .with_style( + // *collab_theme + // .contact_row + // .in_state(is_selected) + // .style_for(state), + // ) + // }); + + // if online && !busy { + // let room = ActiveCall::global(cx).read(cx).room(); + // let label = if room.is_some() { + // format!("Invite {} to join call", contact.user.github_login) + // } else { + // format!("Call {}", contact.user.github_login) + // }; + + // event_handler + // .on_click(MouseButton::Left, move |_, this, cx| { + // this.call(user_id, Some(initial_project.clone()), cx); + // }) + // .with_cursor_style(CursorStyle::PointingHand) + // .with_tooltip::( + // contact.user.id as usize, + // label, + // None, + // theme.tooltip.clone(), + // cx, + // ) + // .into_any() + // } else { + // event_handler + // .with_tooltip::( + // contact.user.id as usize, + // format!( + // "{} is {}", + // contact.user.github_login, + // if busy { "on a call" } else { "offline" } + // ), + // None, + // theme.tooltip.clone(), + // cx, + // ) + // .into_any() + // }; + + item + } + + fn render_contact_request( + &mut self, + user: Arc, + is_incoming: bool, + is_selected: bool, + cx: &mut ViewContext, + ) -> impl IntoElement { + let github_login = SharedString::from(user.github_login.clone()); + + let mut item = ListItem::new(github_login.clone()) + .child(Label::new(github_login.clone())) + .on_click(cx.listener(|this, _, cx| { + todo!(); + })); + if let Some(avatar) = user.avatar.clone() { + item = item.left_avatar(avatar); + } + // .with_children(user.avatar.clone().map(|avatar| { + // Image::from_data(avatar) + // .with_style(theme.contact_avatar) + // .aligned() + // .left() + // })) + // .with_child( + // Label::new( + // user.github_login.clone(), + // theme.contact_username.text.clone(), + // ) + // .contained() + // .with_style(theme.contact_username.container) + // .aligned() + // .left() + // .flex(1., true), + // ); + + // let user_id = user.id; + // let github_login = user.github_login.clone(); + // let is_contact_request_pending = user_store.read(cx).is_contact_request_pending(&user); + // let button_spacing = theme.contact_button_spacing; + + // if is_incoming { + // row.add_child( + // MouseEventHandler::new::(user.id as usize, cx, |mouse_state, _| { + // let button_style = if is_contact_request_pending { + // &theme.disabled_button + // } else { + // theme.contact_button.style_for(mouse_state) + // }; + // render_icon_button(button_style, "icons/x.svg").aligned() + // }) + // .with_cursor_style(CursorStyle::PointingHand) + // .on_click(MouseButton::Left, move |_, this, cx| { + // this.respond_to_contact_request(user_id, false, cx); + // }) + // .contained() + // .with_margin_right(button_spacing), + // ); + + // row.add_child( + // MouseEventHandler::new::(user.id as usize, cx, |mouse_state, _| { + // let button_style = if is_contact_request_pending { + // &theme.disabled_button + // } else { + // theme.contact_button.style_for(mouse_state) + // }; + // render_icon_button(button_style, "icons/check.svg") + // .aligned() + // .flex_float() + // }) + // .with_cursor_style(CursorStyle::PointingHand) + // .on_click(MouseButton::Left, move |_, this, cx| { + // this.respond_to_contact_request(user_id, true, cx); + // }), + // ); + // } else { + // row.add_child( + // MouseEventHandler::new::(user.id as usize, cx, |mouse_state, _| { + // let button_style = if is_contact_request_pending { + // &theme.disabled_button + // } else { + // theme.contact_button.style_for(mouse_state) + // }; + // render_icon_button(button_style, "icons/x.svg") + // .aligned() + // .flex_float() + // }) + // .with_padding(Padding::uniform(2.)) + // .with_cursor_style(CursorStyle::PointingHand) + // .on_click(MouseButton::Left, move |_, this, cx| { + // this.remove_contact(user_id, &github_login, cx); + // }) + // .flex_float(), + // ); + // } + + // row.constrained() + // .with_height(theme.row_height) + // .contained() + // .with_style( + // *theme + // .contact_row + // .in_state(is_selected) + // .style_for(&mut Default::default()), + // ) + // .into_any() + item + } + + fn render_contact_placeholder( + &self, + is_selected: bool, + cx: &mut ViewContext, + ) -> impl IntoElement { + ListItem::new("contact-placeholder") + .child(Label::new("Add a Contact")) + .on_click(cx.listener(|this, _, cx| todo!())) + // enum AddContacts {} + // MouseEventHandler::new::(0, cx, |state, _| { + // let style = theme.list_empty_state.style_for(is_selected, state); + // Flex::row() + // .with_child( + // Svg::new("icons/plus.svg") + // .with_color(theme.list_empty_icon.color) + // .constrained() + // .with_width(theme.list_empty_icon.width) + // .aligned() + // .left(), + // ) + // .with_child( + // Label::new("Add a contact", style.text.clone()) + // .contained() + // .with_style(theme.list_empty_label_container), + // ) + // .align_children_center() + // .contained() + // .with_style(style.container) + // .into_any() + // }) + // .on_click(MouseButton::Left, |_, this, cx| { + // this.toggle_contact_finder(cx); + // }) + // .into_any() + } + + fn render_channel( + &self, + channel: &Channel, + depth: usize, + has_children: bool, + is_selected: bool, + cx: &mut ViewContext, + ) -> impl IntoElement { + let channel_id = channel.id; + ListItem::new(channel_id as usize).child(Label::new(channel.name.clone())) + // let channel_id = channel.id; + // let collab_theme = &theme.collab_panel; + // let is_public = self + // .channel_store + // .read(cx) + // .channel_for_id(channel_id) + // .map(|channel| channel.visibility) + // == Some(proto::ChannelVisibility::Public); + // let other_selected = self.selected_channel().map(|channel| channel.id) == Some(channel.id); + // let disclosed = + // has_children.then(|| !self.collapsed_channels.binary_search(&channel.id).is_ok()); + + // let is_active = maybe!({ + // let call_channel = ActiveCall::global(cx) + // .read(cx) + // .room()? + // .read(cx) + // .channel_id()?; + // Some(call_channel == channel_id) + // }) + // .unwrap_or(false); + + // const FACEPILE_LIMIT: usize = 3; + + // enum ChannelCall {} + // enum ChannelNote {} + // enum NotesTooltip {} + // enum ChatTooltip {} + // enum ChannelTooltip {} + + // let mut is_dragged_over = false; + // if cx + // .global::>() + // .currently_dragged::(cx.window()) + // .is_some() + // && self.drag_target_channel == ChannelDragTarget::Channel(channel_id) + // { + // is_dragged_over = true; + // } + + // let has_messages_notification = channel.unseen_message_id.is_some(); + + // MouseEventHandler::new::(ix, cx, |state, cx| { + // let row_hovered = state.hovered(); + + // let mut select_state = |interactive: &Interactive| { + // if state.clicked() == Some(MouseButton::Left) && interactive.clicked.is_some() { + // interactive.clicked.as_ref().unwrap().clone() + // } else if state.hovered() || other_selected { + // interactive + // .hovered + // .as_ref() + // .unwrap_or(&interactive.default) + // .clone() + // } else { + // interactive.default.clone() + // } + // }; + + // Flex::::row() + // .with_child( + // Svg::new(if is_public { + // "icons/public.svg" + // } else { + // "icons/hash.svg" + // }) + // .with_color(collab_theme.channel_hash.color) + // .constrained() + // .with_width(collab_theme.channel_hash.width) + // .aligned() + // .left(), + // ) + // .with_child({ + // let style = collab_theme.channel_name.inactive_state(); + // Flex::row() + // .with_child( + // Label::new(channel.name.clone(), style.text.clone()) + // .contained() + // .with_style(style.container) + // .aligned() + // .left() + // .with_tooltip::( + // ix, + // "Join channel", + // None, + // theme.tooltip.clone(), + // cx, + // ), + // ) + // .with_children({ + // let participants = + // self.channel_store.read(cx).channel_participants(channel_id); + + // if !participants.is_empty() { + // let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT); + + // let result = FacePile::new(collab_theme.face_overlap) + // .with_children( + // participants + // .iter() + // .filter_map(|user| { + // Some( + // Image::from_data(user.avatar.clone()?) + // .with_style(collab_theme.channel_avatar), + // ) + // }) + // .take(FACEPILE_LIMIT), + // ) + // .with_children((extra_count > 0).then(|| { + // Label::new( + // format!("+{}", extra_count), + // collab_theme.extra_participant_label.text.clone(), + // ) + // .contained() + // .with_style(collab_theme.extra_participant_label.container) + // })); + + // Some(result) + // } else { + // None + // } + // }) + // .with_spacing(8.) + // .align_children_center() + // .flex(1., true) + // }) + // .with_child( + // MouseEventHandler::new::(ix, cx, move |mouse_state, _| { + // let container_style = collab_theme + // .disclosure + // .button + // .style_for(mouse_state) + // .container; + + // if channel.unseen_message_id.is_some() { + // Svg::new("icons/conversations.svg") + // .with_color(collab_theme.channel_note_active_color) + // .constrained() + // .with_width(collab_theme.channel_hash.width) + // .contained() + // .with_style(container_style) + // .with_uniform_padding(4.) + // .into_any() + // } else if row_hovered { + // Svg::new("icons/conversations.svg") + // .with_color(collab_theme.channel_hash.color) + // .constrained() + // .with_width(collab_theme.channel_hash.width) + // .contained() + // .with_style(container_style) + // .with_uniform_padding(4.) + // .into_any() + // } else { + // Empty::new().into_any() + // } + // }) + // .on_click(MouseButton::Left, move |_, this, cx| { + // this.join_channel_chat(&JoinChannelChat { channel_id }, cx); + // }) + // .with_tooltip::( + // ix, + // "Open channel chat", + // None, + // theme.tooltip.clone(), + // cx, + // ) + // .contained() + // .with_margin_right(4.), + // ) + // .with_child( + // MouseEventHandler::new::(ix, cx, move |mouse_state, cx| { + // let container_style = collab_theme + // .disclosure + // .button + // .style_for(mouse_state) + // .container; + // if row_hovered || channel.unseen_note_version.is_some() { + // Svg::new("icons/file.svg") + // .with_color(if channel.unseen_note_version.is_some() { + // collab_theme.channel_note_active_color + // } else { + // collab_theme.channel_hash.color + // }) + // .constrained() + // .with_width(collab_theme.channel_hash.width) + // .contained() + // .with_style(container_style) + // .with_uniform_padding(4.) + // .with_margin_right(collab_theme.channel_hash.container.margin.left) + // .with_tooltip::( + // ix as usize, + // "Open channel notes", + // None, + // theme.tooltip.clone(), + // cx, + // ) + // .into_any() + // } else if has_messages_notification { + // Empty::new() + // .constrained() + // .with_width(collab_theme.channel_hash.width) + // .contained() + // .with_uniform_padding(4.) + // .with_margin_right(collab_theme.channel_hash.container.margin.left) + // .into_any() + // } else { + // Empty::new().into_any() + // } + // }) + // .on_click(MouseButton::Left, move |_, this, cx| { + // this.open_channel_notes(&OpenChannelNotes { channel_id }, cx); + // }), + // ) + // .align_children_center() + // .styleable_component() + // .disclosable( + // disclosed, + // Box::new(ToggleCollapse { + // location: channel.id.clone(), + // }), + // ) + // .with_id(ix) + // .with_style(collab_theme.disclosure.clone()) + // .element() + // .constrained() + // .with_height(collab_theme.row_height) + // .contained() + // .with_style(select_state( + // collab_theme + // .channel_row + // .in_state(is_selected || is_active || is_dragged_over), + // )) + // .with_padding_left( + // collab_theme.channel_row.default_style().padding.left + // + collab_theme.channel_indent * depth as f32, + // ) + // }) + // .on_click(MouseButton::Left, move |_, this, cx| { + // if this.drag_target_channel == ChannelDragTarget::None { + // if is_active { + // this.open_channel_notes(&OpenChannelNotes { channel_id }, cx) + // } else { + // this.join_channel(channel_id, cx) + // } + // } + // }) + // .on_click(MouseButton::Right, { + // let channel = channel.clone(); + // move |e, this, cx| { + // this.deploy_channel_context_menu(Some(e.position), &channel, ix, cx); + // } + // }) + // .on_up(MouseButton::Left, move |_, this, cx| { + // if let Some((_, dragged_channel)) = cx + // .global::>() + // .currently_dragged::(cx.window()) + // { + // this.channel_store + // .update(cx, |channel_store, cx| { + // channel_store.move_channel(dragged_channel.id, Some(channel_id), cx) + // }) + // .detach_and_log_err(cx) + // } + // }) + // .on_move({ + // let channel = channel.clone(); + // move |_, this, cx| { + // if let Some((_, dragged_channel)) = cx + // .global::>() + // .currently_dragged::(cx.window()) + // { + // if channel.id != dragged_channel.id { + // this.drag_target_channel = ChannelDragTarget::Channel(channel.id); + // } + // cx.notify() + // } + // } + // }) + // .as_draggable::<_, Channel>( + // channel.clone(), + // move |_, channel, cx: &mut ViewContext| { + // let theme = &theme::current(cx).collab_panel; + + // Flex::::row() + // .with_child( + // Svg::new("icons/hash.svg") + // .with_color(theme.channel_hash.color) + // .constrained() + // .with_width(theme.channel_hash.width) + // .aligned() + // .left(), + // ) + // .with_child( + // Label::new(channel.name.clone(), theme.channel_name.text.clone()) + // .contained() + // .with_style(theme.channel_name.container) + // .aligned() + // .left(), + // ) + // .align_children_center() + // .contained() + // .with_background_color( + // theme + // .container + // .background_color + // .unwrap_or(gpui::color::Color::transparent_black()), + // ) + // .contained() + // .with_padding_left( + // theme.channel_row.default_style().padding.left + // + theme.channel_indent * depth as f32, + // ) + // .into_any() + // }, + // ) + // .with_cursor_style(CursorStyle::PointingHand) + // .into_any() + } } // fn render_tree_branch( @@ -3295,13 +3386,19 @@ impl CollabPanel { // } impl Render for CollabPanel { - type Element = Focusable>; + type Element = Focusable
; - fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { div() .key_context("CollabPanel") .track_focus(&self.focus_handle) - .child("COLLAB PANEL") + .map(|el| { + if self.user_store.read(cx).current_user().is_none() { + el.child(self.render_signed_out(cx)) + } else { + el.child(self.render_signed_in(cx)) + } + }) } } diff --git a/crates/collab_ui2/src/collab_panel/contact_finder.rs b/crates/collab_ui2/src/collab_panel/contact_finder.rs index d0c12a7f90..48453ada72 100644 --- a/crates/collab_ui2/src/collab_panel/contact_finder.rs +++ b/crates/collab_ui2/src/collab_panel/contact_finder.rs @@ -1,37 +1,34 @@ use client::{ContactRequestStatus, User, UserStore}; use gpui::{ - elements::*, AppContext, Entity, ModelHandle, MouseState, Task, View, ViewContext, ViewHandle, + div, img, svg, AnyElement, AppContext, DismissEvent, Div, Entity, EventEmitter, FocusHandle, + FocusableView, Img, IntoElement, Model, ParentElement as _, Render, Styled, Task, View, + ViewContext, VisualContext, WeakView, }; -use picker::{Picker, PickerDelegate, PickerEvent}; +use picker::{Picker, PickerDelegate}; use std::sync::Arc; -use util::TryFutureExt; -use workspace::Modal; +use theme::ActiveTheme as _; +use ui::{h_stack, v_stack, Label}; +use util::{ResultExt as _, TryFutureExt}; pub fn init(cx: &mut AppContext) { - Picker::::init(cx); - cx.add_action(ContactFinder::dismiss) + //Picker::::init(cx); + //cx.add_action(ContactFinder::dismiss) } pub struct ContactFinder { - picker: ViewHandle>, + picker: View>, has_focus: bool, } impl ContactFinder { - pub fn new(user_store: ModelHandle, cx: &mut ViewContext) -> Self { - let picker = cx.add_view(|cx| { - Picker::new( - ContactFinderDelegate { - user_store, - potential_contacts: Arc::from([]), - selected_index: 0, - }, - cx, - ) - .with_theme(|theme| theme.collab_panel.tabbed_modal.picker.clone()) - }); - - cx.subscribe(&picker, |_, _, e, cx| cx.emit(*e)).detach(); + pub fn new(user_store: Model, cx: &mut ViewContext) -> Self { + let delegate = ContactFinderDelegate { + parent: cx.view().downgrade(), + user_store, + potential_contacts: Arc::from([]), + selected_index: 0, + }; + let picker = cx.build_view(|cx| Picker::new(delegate, cx)); Self { picker, @@ -41,105 +38,72 @@ impl ContactFinder { pub fn set_query(&mut self, query: String, cx: &mut ViewContext) { self.picker.update(cx, |picker, cx| { - picker.set_query(query, cx); + // todo!() + // picker.set_query(query, cx); }); } - - fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { - cx.emit(PickerEvent::Dismiss); - } } -impl Entity for ContactFinder { - type Event = PickerEvent; -} - -impl View for ContactFinder { - fn ui_name() -> &'static str { - "ContactFinder" - } - - fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - let full_theme = &theme::current(cx); - let theme = &full_theme.collab_panel.tabbed_modal; - - fn render_mode_button( - text: &'static str, - theme: &theme::TabbedModal, - _cx: &mut ViewContext, - ) -> AnyElement { - let contained_text = &theme.tab_button.active_state().default; - Label::new(text, contained_text.text.clone()) - .contained() - .with_style(contained_text.container.clone()) - .into_any() +impl Render for ContactFinder { + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + fn render_mode_button(text: &'static str) -> AnyElement { + Label::new(text).into_any_element() } - Flex::column() - .with_child( - Flex::column() - .with_child( - Label::new("Contacts", theme.title.text.clone()) - .contained() - .with_style(theme.title.container.clone()), - ) - .with_child(Flex::row().with_children([render_mode_button( - "Invite new contacts", - &theme, - cx, - )])) - .expanded() - .contained() - .with_style(theme.header), + v_stack() + .child( + v_stack() + .child(Label::new("Contacts")) + .child(h_stack().children([render_mode_button("Invite new contacts")])) + .bg(cx.theme().colors().element_background), ) - .with_child( - ChildView::new(&self.picker, cx) - .contained() - .with_style(theme.body), - ) - .constrained() - .with_max_height(theme.max_height) - .with_max_width(theme.max_width) - .contained() - .with_style(theme.modal) - .into_any() + .child(self.picker.clone()) + .w_96() } - fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { - self.has_focus = true; - if cx.is_self_focused() { - cx.focus(&self.picker) - } - } + // fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { + // self.has_focus = true; + // if cx.is_self_focused() { + // cx.focus(&self.picker) + // } + // } - fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext) { - self.has_focus = false; - } + // fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext) { + // self.has_focus = false; + // } + + type Element = Div; } -impl Modal for ContactFinder { - fn has_focus(&self) -> bool { - self.has_focus - } +// impl Modal for ContactFinder { +// fn has_focus(&self) -> bool { +// self.has_focus +// } - fn dismiss_on_event(event: &Self::Event) -> bool { - match event { - PickerEvent::Dismiss => true, - } - } -} +// fn dismiss_on_event(event: &Self::Event) -> bool { +// match event { +// PickerEvent::Dismiss => true, +// } +// } +// } pub struct ContactFinderDelegate { + parent: WeakView, potential_contacts: Arc<[Arc]>, - user_store: ModelHandle, + user_store: Model, selected_index: usize, } -impl PickerDelegate for ContactFinderDelegate { - fn placeholder_text(&self) -> Arc { - "Search collaborator by username...".into() - } +impl EventEmitter for ContactFinder {} +impl FocusableView for ContactFinder { + fn focus_handle(&self, cx: &AppContext) -> FocusHandle { + self.picker.focus_handle(cx) + } +} + +impl PickerDelegate for ContactFinderDelegate { + type ListItem = Div; fn match_count(&self) -> usize { self.potential_contacts.len() } @@ -152,6 +116,10 @@ impl PickerDelegate for ContactFinderDelegate { self.selected_index = ix; } + fn placeholder_text(&self) -> Arc { + "Search collaborator by username...".into() + } + fn update_matches(&mut self, query: String, cx: &mut ViewContext>) -> Task<()> { let search_users = self .user_store @@ -161,7 +129,7 @@ impl PickerDelegate for ContactFinderDelegate { async { let potential_contacts = search_users.await?; picker.update(&mut cx, |picker, cx| { - picker.delegate_mut().potential_contacts = potential_contacts.into(); + picker.delegate.potential_contacts = potential_contacts.into(); cx.notify(); })?; anyhow::Ok(()) @@ -191,19 +159,18 @@ impl PickerDelegate for ContactFinderDelegate { } fn dismissed(&mut self, cx: &mut ViewContext>) { - cx.emit(PickerEvent::Dismiss); + //cx.emit(PickerEvent::Dismiss); + self.parent + .update(cx, |_, cx| cx.emit(DismissEvent::Dismiss)) + .log_err(); } fn render_match( &self, ix: usize, - mouse_state: &mut MouseState, selected: bool, - cx: &gpui::AppContext, - ) -> AnyElement> { - let full_theme = &theme::current(cx); - let theme = &full_theme.collab_panel.contact_finder; - let tabbed_modal = &full_theme.collab_panel.tabbed_modal; + cx: &mut ViewContext>, + ) -> Option { let user = &self.potential_contacts[ix]; let request_status = self.user_store.read(cx).contact_request_status(user); @@ -214,48 +181,47 @@ impl PickerDelegate for ContactFinderDelegate { ContactRequestStatus::RequestSent => Some("icons/x.svg"), ContactRequestStatus::RequestAccepted => None, }; - let button_style = if self.user_store.read(cx).is_contact_request_pending(user) { - &theme.disabled_contact_button - } else { - &theme.contact_button - }; - let style = tabbed_modal - .picker - .item - .in_state(selected) - .style_for(mouse_state); - Flex::row() - .with_children(user.avatar.clone().map(|avatar| { - Image::from_data(avatar) - .with_style(theme.contact_avatar) - .aligned() - .left() - })) - .with_child( - Label::new(user.github_login.clone(), style.label.clone()) - .contained() - .with_style(theme.contact_username) - .aligned() - .left(), - ) - .with_children(icon_path.map(|icon_path| { - Svg::new(icon_path) - .with_color(button_style.color) - .constrained() - .with_width(button_style.icon_width) - .aligned() - .contained() - .with_style(button_style.container) - .constrained() - .with_width(button_style.button_width) - .with_height(button_style.button_width) - .aligned() - .flex_float() - })) - .contained() - .with_style(style.container) - .constrained() - .with_height(tabbed_modal.row_height) - .into_any() + dbg!(icon_path); + Some( + div() + .flex_1() + .justify_between() + .children(user.avatar.clone().map(|avatar| img().data(avatar))) + .child(Label::new(user.github_login.clone())) + .children(icon_path.map(|icon_path| svg().path(icon_path))), + ) + // Flex::row() + // .with_children(user.avatar.clone().map(|avatar| { + // Image::from_data(avatar) + // .with_style(theme.contact_avatar) + // .aligned() + // .left() + // })) + // .with_child( + // Label::new(user.github_login.clone(), style.label.clone()) + // .contained() + // .with_style(theme.contact_username) + // .aligned() + // .left(), + // ) + // .with_children(icon_path.map(|icon_path| { + // Svg::new(icon_path) + // .with_color(button_style.color) + // .constrained() + // .with_width(button_style.icon_width) + // .aligned() + // .contained() + // .with_style(button_style.container) + // .constrained() + // .with_width(button_style.button_width) + // .with_height(button_style.button_width) + // .aligned() + // .flex_float() + // })) + // .contained() + // .with_style(style.container) + // .constrained() + // .with_height(tabbed_modal.row_height) + // .into_any() } } diff --git a/crates/collab_ui2/src/collab_titlebar_item.rs b/crates/collab_ui2/src/collab_titlebar_item.rs index ed010cc500..2307ba2fcb 100644 --- a/crates/collab_ui2/src/collab_titlebar_item.rs +++ b/crates/collab_ui2/src/collab_titlebar_item.rs @@ -31,14 +31,17 @@ use std::sync::Arc; use call::ActiveCall; use client::{Client, UserStore}; use gpui::{ - div, px, rems, AppContext, Component, Div, InteractiveComponent, Model, ParentComponent, - Render, Stateful, StatefulInteractiveComponent, Styled, Subscription, ViewContext, - VisualContext, WeakView, WindowBounds, + div, px, rems, AppContext, Div, Element, InteractiveElement, IntoElement, Model, MouseButton, + ParentElement, Render, RenderOnce, Stateful, StatefulInteractiveElement, Styled, Subscription, + ViewContext, VisualContext, WeakView, WindowBounds, }; use project::Project; use theme::ActiveTheme; -use ui::{h_stack, Button, ButtonVariant, KeyBinding, Label, TextColor, Tooltip}; -use workspace::Workspace; +use ui::{h_stack, Avatar, Button, ButtonVariant, Color, IconButton, KeyBinding, Tooltip}; +use util::ResultExt; +use workspace::{notifications::NotifyResultExt, Workspace}; + +use crate::face_pile::FacePile; // const MAX_PROJECT_NAME_LENGTH: usize = 40; // const MAX_BRANCH_NAME_LENGTH: usize = 40; @@ -82,9 +85,44 @@ pub struct CollabTitlebarItem { } impl Render for CollabTitlebarItem { - type Element = Stateful>; + type Element = Stateful
; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + let is_in_room = self + .workspace + .update(cx, |this, cx| this.call_state().is_in_room(cx)) + .unwrap_or_default(); + let is_shared = is_in_room && self.project.read(cx).is_shared(); + let current_user = self.user_store.read(cx).current_user(); + let client = self.client.clone(); + let users = self + .workspace + .update(cx, |this, cx| this.call_state().remote_participants(cx)) + .log_err() + .flatten(); + let mic_icon = if self + .workspace + .update(cx, |this, cx| this.call_state().is_muted(cx)) + .log_err() + .flatten() + .unwrap_or_default() + { + ui::Icon::MicMute + } else { + ui::Icon::Mic + }; + let speakers_icon = if self + .workspace + .update(cx, |this, cx| this.call_state().is_deafened(cx)) + .log_err() + .flatten() + .unwrap_or_default() + { + ui::Icon::AudioOff + } else { + ui::Icon::AudioOn + }; + let workspace = self.workspace.clone(); h_stack() .id("titlebar") .justify_between() @@ -100,7 +138,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(); } @@ -111,31 +149,37 @@ impl Render for CollabTitlebarItem { // TODO - Add player menu .child( div() + .border() + .border_color(gpui::red()) .id("project_owner_indicator") .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() + .border() + .border_color(gpui::red()) .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( div() + .border() + .border_color(gpui::red()) .id("titlebar_git_menu_button") .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( @@ -149,8 +193,113 @@ impl Render for CollabTitlebarItem { .into() }), ), - ) // self.titlebar_item - .child(h_stack().child(Label::new("Right side titlebar item"))) + ) + .when_some( + users.zip(current_user.clone()), + |this, (remote_participants, current_user)| { + let mut pile = FacePile::default(); + pile.extend( + current_user + .avatar + .clone() + .map(|avatar| { + div().child(Avatar::data(avatar.clone())).into_any_element() + }) + .into_iter() + .chain(remote_participants.into_iter().flat_map(|(user, peer_id)| { + user.avatar.as_ref().map(|avatar| { + div() + .child( + Avatar::data(avatar.clone()).into_element().into_any(), + ) + .on_mouse_down(MouseButton::Left, { + let workspace = workspace.clone(); + move |_, cx| { + workspace + .update(cx, |this, cx| { + this.open_shared_screen(peer_id, cx); + }) + .log_err(); + } + }) + .into_any_element() + }) + })), + ); + this.child(pile.render(cx)) + }, + ) + .child(div().flex_1()) + .when(is_in_room, |this| { + this.child( + h_stack() + .child( + h_stack() + .child(Button::new(if is_shared { "Unshare" } else { "Share" })) + .child(IconButton::new("leave-call", ui::Icon::Exit).on_click({ + let workspace = workspace.clone(); + move |_, cx| { + workspace + .update(cx, |this, cx| { + this.call_state().hang_up(cx).detach(); + }) + .log_err(); + } + })), + ) + .child( + h_stack() + .child(IconButton::new("mute-microphone", mic_icon).on_click({ + let workspace = workspace.clone(); + move |_, cx| { + workspace + .update(cx, |this, cx| { + this.call_state().toggle_mute(cx); + }) + .log_err(); + } + })) + .child(IconButton::new("mute-sound", speakers_icon).on_click({ + let workspace = workspace.clone(); + move |_, cx| { + workspace + .update(cx, |this, cx| { + this.call_state().toggle_deafen(cx); + }) + .log_err(); + } + })) + .child(IconButton::new("screen-share", ui::Icon::Screen).on_click( + move |_, cx| { + workspace + .update(cx, |this, cx| { + this.call_state().toggle_screen_share(cx); + }) + .log_err(); + }, + )) + .pl_2(), + ), + ) + }) + .map(|this| { + if let Some(user) = current_user { + this.when_some(user.avatar.clone(), |this, avatar| { + this.child(ui::Avatar::data(avatar)) + }) + } else { + this.child(Button::new("Sign in").on_click(move |_, cx| { + let client = client.clone(); + cx.spawn(move |mut cx| async move { + client + .authenticate_and_connect(true, &cx) + .await + .notify_async_err(&mut cx); + }) + .detach(); + })) + } + }) } } diff --git a/crates/collab_ui2/src/collab_ui.rs b/crates/collab_ui2/src/collab_ui.rs index d2e6b28115..57a33c6790 100644 --- a/crates/collab_ui2/src/collab_ui.rs +++ b/crates/collab_ui2/src/collab_ui.rs @@ -7,11 +7,14 @@ pub mod notification_panel; pub mod notifications; mod panel_settings; -use std::sync::Arc; +use std::{rc::Rc, sync::Arc}; pub use collab_panel::CollabPanel; pub use collab_titlebar_item::CollabTitlebarItem; -use gpui::AppContext; +use gpui::{ + point, AppContext, GlobalPixels, Pixels, PlatformDisplay, Size, WindowBounds, WindowKind, + WindowOptions, +}; pub use panel_settings::{ ChatPanelSettings, CollaborationPanelSettings, NotificationPanelSettings, }; @@ -23,7 +26,7 @@ use workspace::AppState; // [ToggleScreenSharing, ToggleMute, ToggleDeafen, LeaveCall] // ); -pub fn init(_app_state: &Arc, cx: &mut AppContext) { +pub fn init(app_state: &Arc, cx: &mut AppContext) { CollaborationPanelSettings::register(cx); ChatPanelSettings::register(cx); NotificationPanelSettings::register(cx); @@ -32,7 +35,7 @@ pub fn init(_app_state: &Arc, cx: &mut AppContext) { collab_titlebar_item::init(cx); collab_panel::init(cx); // chat_panel::init(cx); - // notifications::init(&app_state, cx); + notifications::init(&app_state, cx); // cx.add_global_action(toggle_screen_sharing); // cx.add_global_action(toggle_mute); @@ -95,31 +98,36 @@ pub fn init(_app_state: &Arc, cx: &mut AppContext) { // } // } -// fn notification_window_options( -// screen: Rc, -// window_size: Vector2F, -// ) -> WindowOptions<'static> { -// const NOTIFICATION_PADDING: f32 = 16.; +fn notification_window_options( + screen: Rc, + window_size: Size, +) -> WindowOptions { + let notification_margin_width = GlobalPixels::from(16.); + let notification_margin_height = GlobalPixels::from(-0.) - GlobalPixels::from(48.); -// let screen_bounds = screen.content_bounds(); -// WindowOptions { -// bounds: WindowBounds::Fixed(RectF::new( -// screen_bounds.upper_right() -// + vec2f( -// -NOTIFICATION_PADDING - window_size.x(), -// NOTIFICATION_PADDING, -// ), -// window_size, -// )), -// titlebar: None, -// center: false, -// focus: false, -// show: true, -// kind: WindowKind::PopUp, -// is_movable: false, -// screen: Some(screen), -// } -// } + let screen_bounds = screen.bounds(); + let size: Size = window_size.into(); + + // todo!() use content bounds instead of screen.bounds and get rid of magics in point's 2nd argument. + let bounds = gpui::Bounds:: { + origin: screen_bounds.upper_right() + - point( + size.width + notification_margin_width, + notification_margin_height, + ), + size: window_size.into(), + }; + WindowOptions { + bounds: WindowBounds::Fixed(bounds), + titlebar: None, + center: false, + focus: false, + show: true, + kind: WindowKind::PopUp, + is_movable: false, + display_id: Some(screen.id()), + } +} // fn render_avatar( // avatar: Option>, diff --git a/crates/collab_ui2/src/face_pile.rs b/crates/collab_ui2/src/face_pile.rs index 077b813fbd..e235f33ce6 100644 --- a/crates/collab_ui2/src/face_pile.rs +++ b/crates/collab_ui2/src/face_pile.rs @@ -1,54 +1,48 @@ -// use std::ops::Range; +use gpui::{ + div, AnyElement, Div, IntoElement as _, ParentElement as _, RenderOnce, Styled, WindowContext, +}; -// use gpui::{ -// geometry::{ -// rect::RectF, -// vector::{vec2f, Vector2F}, -// }, -// json::ToJson, -// serde_json::{self, json}, -// AnyElement, Axis, Element, View, ViewContext, -// }; +#[derive(Default)] +pub(crate) struct FacePile { + faces: Vec, +} -// pub(crate) struct FacePile { -// overlap: f32, -// faces: Vec>, -// } +impl RenderOnce for FacePile { + type Rendered = Div; -// impl FacePile { -// pub fn new(overlap: f32) -> Self { -// Self { -// overlap, -// faces: Vec::new(), -// } -// } -// } + fn render(self, _: &mut WindowContext) -> Self::Rendered { + let player_count = self.faces.len(); + let player_list = self.faces.into_iter().enumerate().map(|(ix, player)| { + let isnt_last = ix < player_count - 1; -// impl Element for FacePile { -// type LayoutState = (); -// type PaintState = (); + div().when(isnt_last, |div| div.neg_mr_1()).child(player) + }); + div().p_1().flex().items_center().children(player_list) + } +} +// impl Element for FacePile { +// type State = (); // fn layout( // &mut self, -// constraint: gpui::SizeConstraint, -// view: &mut V, -// cx: &mut ViewContext, -// ) -> (Vector2F, Self::LayoutState) { -// debug_assert!(constraint.max_along(Axis::Horizontal) == f32::INFINITY); - +// state: Option, +// cx: &mut WindowContext, +// ) -> (LayoutId, Self::State) { // let mut width = 0.; // let mut max_height = 0.; +// let mut faces = Vec::with_capacity(self.faces.len()); // for face in &mut self.faces { -// let layout = face.layout(constraint, view, cx); +// let layout = face.layout(cx); // width += layout.x(); // max_height = f32::max(max_height, layout.y()); +// faces.push(layout); // } // width -= self.overlap * self.faces.len().saturating_sub(1) as f32; - -// ( -// Vector2F::new(width, max_height.clamp(1., constraint.max.y())), -// (), -// ) +// (cx.request_layout(&Style::default(), faces), ()) +// // ( +// // Vector2F::new(width, max_height.clamp(1., constraint.max.y())), +// // (), +// // )) // } // fn paint( @@ -77,37 +71,10 @@ // () // } - -// fn rect_for_text_range( -// &self, -// _: Range, -// _: RectF, -// _: RectF, -// _: &Self::LayoutState, -// _: &Self::PaintState, -// _: &V, -// _: &ViewContext, -// ) -> Option { -// None -// } - -// fn debug( -// &self, -// bounds: RectF, -// _: &Self::LayoutState, -// _: &Self::PaintState, -// _: &V, -// _: &ViewContext, -// ) -> serde_json::Value { -// json!({ -// "type": "FacePile", -// "bounds": bounds.to_json() -// }) -// } // } -// impl Extend> for FacePile { -// fn extend>>(&mut self, children: T) { -// self.faces.extend(children); -// } -// } +impl Extend for FacePile { + fn extend>(&mut self, children: T) { + self.faces.extend(children); + } +} diff --git a/crates/collab_ui2/src/notifications.rs b/crates/collab_ui2/src/notifications.rs index bc5d7ad3bf..b58473476a 100644 --- a/crates/collab_ui2/src/notifications.rs +++ b/crates/collab_ui2/src/notifications.rs @@ -1,11 +1,11 @@ -// use gpui::AppContext; -// use std::sync::Arc; -// use workspace::AppState; +use gpui::AppContext; +use std::sync::Arc; +use workspace::AppState; -// pub mod incoming_call_notification; +pub mod incoming_call_notification; // pub mod project_shared_notification; -// pub fn init(app_state: &Arc, cx: &mut AppContext) { -// incoming_call_notification::init(app_state, cx); -// project_shared_notification::init(app_state, cx); -// } +pub fn init(app_state: &Arc, cx: &mut AppContext) { + incoming_call_notification::init(app_state, cx); + //project_shared_notification::init(app_state, cx); +} diff --git a/crates/collab_ui2/src/notifications/incoming_call_notification.rs b/crates/collab_ui2/src/notifications/incoming_call_notification.rs index c614a814ca..0519b6fc4a 100644 --- a/crates/collab_ui2/src/notifications/incoming_call_notification.rs +++ b/crates/collab_ui2/src/notifications/incoming_call_notification.rs @@ -1,14 +1,12 @@ use crate::notification_window_options; use call::{ActiveCall, IncomingCall}; -use client::proto; use futures::StreamExt; use gpui::{ - elements::*, - geometry::vector::vec2f, - platform::{CursorStyle, MouseButton}, - AnyElement, AppContext, Entity, View, ViewContext, WindowHandle, + div, green, px, red, AppContext, Div, Element, ParentElement, Render, RenderOnce, + StatefulInteractiveElement, Styled, ViewContext, VisualContext as _, WindowHandle, }; use std::sync::{Arc, Weak}; +use ui::{h_stack, v_stack, Avatar, Button, Label}; use util::ResultExt; use workspace::AppState; @@ -19,23 +17,44 @@ pub fn init(app_state: &Arc, cx: &mut AppContext) { let mut notification_windows: Vec> = Vec::new(); while let Some(incoming_call) = incoming_call.next().await { for window in notification_windows.drain(..) { - window.remove(&mut cx); + window + .update(&mut cx, |_, cx| { + // todo!() + cx.remove_window(); + }) + .log_err(); } if let Some(incoming_call) = incoming_call { - let window_size = cx.read(|cx| { - let theme = &theme::current(cx).incoming_call_notification; - vec2f(theme.window_width, theme.window_height) - }); + let unique_screens = cx.update(|cx| cx.displays()).unwrap(); + let window_size = gpui::Size { + width: px(380.), + height: px(64.), + }; - for screen in cx.platform().screens() { + for window in unique_screens { + let options = notification_window_options(window, window_size); let window = cx - .add_window(notification_window_options(screen, window_size), |_| { - IncomingCallNotification::new(incoming_call.clone(), app_state.clone()) - }); - + .open_window(options, |cx| { + cx.build_view(|_| { + IncomingCallNotification::new( + incoming_call.clone(), + app_state.clone(), + ) + }) + }) + .unwrap(); notification_windows.push(window); } + + // for screen in cx.platform().screens() { + // let window = cx + // .add_window(notification_window_options(screen, window_size), |_| { + // IncomingCallNotification::new(incoming_call.clone(), app_state.clone()) + // }); + + // notification_windows.push(window); + // } } } }) @@ -47,167 +66,196 @@ struct RespondToCall { accept: bool, } -pub struct IncomingCallNotification { +struct IncomingCallNotificationState { call: IncomingCall, app_state: Weak, } -impl IncomingCallNotification { +pub struct IncomingCallNotification { + state: Arc, +} +impl IncomingCallNotificationState { pub fn new(call: IncomingCall, app_state: Weak) -> Self { Self { call, app_state } } - fn respond(&mut self, accept: bool, cx: &mut ViewContext) { + fn respond(&self, accept: bool, cx: &mut AppContext) { let active_call = ActiveCall::global(cx); if accept { let join = active_call.update(cx, |active_call, cx| active_call.accept_incoming(cx)); - let caller_user_id = self.call.calling_user.id; let initial_project_id = self.call.initial_project.as_ref().map(|project| project.id); let app_state = self.app_state.clone(); - cx.app_context() - .spawn(|mut cx| async move { - join.await?; - if let Some(project_id) = initial_project_id { - cx.update(|cx| { - if let Some(app_state) = app_state.upgrade() { - workspace::join_remote_project( - project_id, - caller_user_id, - app_state, - cx, - ) - .detach_and_log_err(cx); - } - }); - } - anyhow::Ok(()) - }) - .detach_and_log_err(cx); + let cx: &mut AppContext = cx; + cx.spawn(|cx| async move { + join.await?; + if let Some(_project_id) = initial_project_id { + cx.update(|_cx| { + if let Some(_app_state) = app_state.upgrade() { + // workspace::join_remote_project( + // project_id, + // caller_user_id, + // app_state, + // cx, + // ) + // .detach_and_log_err(cx); + } + }) + .log_err(); + } + anyhow::Ok(()) + }) + .detach_and_log_err(cx); } else { active_call.update(cx, |active_call, cx| { active_call.decline_incoming(cx).log_err(); }); } } +} - fn render_caller(&self, cx: &mut ViewContext) -> AnyElement { - let theme = &theme::current(cx).incoming_call_notification; - let default_project = proto::ParticipantProject::default(); - let initial_project = self - .call - .initial_project - .as_ref() - .unwrap_or(&default_project); - Flex::row() - .with_children(self.call.calling_user.avatar.clone().map(|avatar| { - Image::from_data(avatar) - .with_style(theme.caller_avatar) - .aligned() +impl IncomingCallNotification { + pub fn new(call: IncomingCall, app_state: Weak) -> Self { + Self { + state: Arc::new(IncomingCallNotificationState::new(call, app_state)), + } + } + fn render_caller(&self, cx: &mut ViewContext) -> impl Element { + h_stack() + .children( + self.state + .call + .calling_user + .avatar + .as_ref() + .map(|avatar| Avatar::data(avatar.clone())), + ) + .child( + v_stack() + .child(Label::new(format!( + "{} is sharing a project in Zed", + self.state.call.calling_user.github_login + ))) + .child(self.render_buttons(cx)), + ) + // let theme = &theme::current(cx).incoming_call_notification; + // let default_project = proto::ParticipantProject::default(); + // let initial_project = self + // .call + // .initial_project + // .as_ref() + // .unwrap_or(&default_project); + // Flex::row() + // .with_children(self.call.calling_user.avatar.clone().map(|avatar| { + // Image::from_data(avatar) + // .with_style(theme.caller_avatar) + // .aligned() + // })) + // .with_child( + // Flex::column() + // .with_child( + // Label::new( + // self.call.calling_user.github_login.clone(), + // theme.caller_username.text.clone(), + // ) + // .contained() + // .with_style(theme.caller_username.container), + // ) + // .with_child( + // Label::new( + // format!( + // "is sharing a project in Zed{}", + // if initial_project.worktree_root_names.is_empty() { + // "" + // } else { + // ":" + // } + // ), + // theme.caller_message.text.clone(), + // ) + // .contained() + // .with_style(theme.caller_message.container), + // ) + // .with_children(if initial_project.worktree_root_names.is_empty() { + // None + // } else { + // Some( + // Label::new( + // initial_project.worktree_root_names.join(", "), + // theme.worktree_roots.text.clone(), + // ) + // .contained() + // .with_style(theme.worktree_roots.container), + // ) + // }) + // .contained() + // .with_style(theme.caller_metadata) + // .aligned(), + // ) + // .contained() + // .with_style(theme.caller_container) + // .flex(1., true) + // .into_any() + } + + fn render_buttons(&self, cx: &mut ViewContext) -> impl Element { + h_stack() + .child(Button::new("Accept").render(cx).bg(green()).on_click({ + let state = self.state.clone(); + move |_, cx| state.respond(true, cx) + })) + .child(Button::new("Decline").render(cx).bg(red()).on_click({ + let state = self.state.clone(); + move |_, cx| state.respond(false, cx) })) - .with_child( - Flex::column() - .with_child( - Label::new( - self.call.calling_user.github_login.clone(), - theme.caller_username.text.clone(), - ) - .contained() - .with_style(theme.caller_username.container), - ) - .with_child( - Label::new( - format!( - "is sharing a project in Zed{}", - if initial_project.worktree_root_names.is_empty() { - "" - } else { - ":" - } - ), - theme.caller_message.text.clone(), - ) - .contained() - .with_style(theme.caller_message.container), - ) - .with_children(if initial_project.worktree_root_names.is_empty() { - None - } else { - Some( - Label::new( - initial_project.worktree_root_names.join(", "), - theme.worktree_roots.text.clone(), - ) - .contained() - .with_style(theme.worktree_roots.container), - ) - }) - .contained() - .with_style(theme.caller_metadata) - .aligned(), - ) - .contained() - .with_style(theme.caller_container) - .flex(1., true) - .into_any() - } - fn render_buttons(&self, cx: &mut ViewContext) -> AnyElement { - enum Accept {} - enum Decline {} + // enum Accept {} + // enum Decline {} - let theme = theme::current(cx); - Flex::column() - .with_child( - MouseEventHandler::new::(0, cx, |_, _| { - let theme = &theme.incoming_call_notification; - Label::new("Accept", theme.accept_button.text.clone()) - .aligned() - .contained() - .with_style(theme.accept_button.container) - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, |_, this, cx| { - this.respond(true, cx); - }) - .flex(1., true), - ) - .with_child( - MouseEventHandler::new::(0, cx, |_, _| { - let theme = &theme.incoming_call_notification; - Label::new("Decline", theme.decline_button.text.clone()) - .aligned() - .contained() - .with_style(theme.decline_button.container) - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, |_, this, cx| { - this.respond(false, cx); - }) - .flex(1., true), - ) - .constrained() - .with_width(theme.incoming_call_notification.button_width) - .into_any() + // let theme = theme::current(cx); + // Flex::column() + // .with_child( + // MouseEventHandler::new::(0, cx, |_, _| { + // let theme = &theme.incoming_call_notification; + // Label::new("Accept", theme.accept_button.text.clone()) + // .aligned() + // .contained() + // .with_style(theme.accept_button.container) + // }) + // .with_cursor_style(CursorStyle::PointingHand) + // .on_click(MouseButton::Left, |_, this, cx| { + // this.respond(true, cx); + // }) + // .flex(1., true), + // ) + // .with_child( + // MouseEventHandler::new::(0, cx, |_, _| { + // let theme = &theme.incoming_call_notification; + // Label::new("Decline", theme.decline_button.text.clone()) + // .aligned() + // .contained() + // .with_style(theme.decline_button.container) + // }) + // .with_cursor_style(CursorStyle::PointingHand) + // .on_click(MouseButton::Left, |_, this, cx| { + // this.respond(false, cx); + // }) + // .flex(1., true), + // ) + // .constrained() + // .with_width(theme.incoming_call_notification.button_width) + // .into_any() } } - -impl Entity for IncomingCallNotification { - type Event = (); -} - -impl View for IncomingCallNotification { - fn ui_name() -> &'static str { - "IncomingCallNotification" - } - - fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - let background = theme::current(cx).incoming_call_notification.background; - Flex::row() - .with_child(self.render_caller(cx)) - .with_child(self.render_buttons(cx)) - .contained() - .with_background_color(background) - .expanded() - .into_any() +impl Render for IncomingCallNotification { + type Element = Div; + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + div().bg(red()).flex_none().child(self.render_caller(cx)) + // Flex::row() + // .with_child() + // .with_child(self.render_buttons(cx)) + // .contained() + // .with_background_color(background) + // .expanded() + // .into_any() } } diff --git a/crates/command_palette2/src/command_palette.rs b/crates/command_palette2/src/command_palette.rs index f3573c1a3d..5f3fb4a985 100644 --- a/crates/command_palette2/src/command_palette.rs +++ b/crates/command_palette2/src/command_palette.rs @@ -1,17 +1,15 @@ use collections::{CommandPaletteFilter, HashMap}; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - actions, div, prelude::*, Action, AppContext, Component, Div, EventEmitter, FocusHandle, - FocusableView, Keystroke, Manager, ParentComponent, Render, Styled, View, ViewContext, - VisualContext, WeakView, + actions, Action, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView, + Keystroke, ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView, }; use picker::{Picker, PickerDelegate}; use std::{ cmp::{self, Reverse}, sync::Arc, }; -use theme::ActiveTheme; -use ui::{h_stack, v_stack, HighlightedLabel, KeyBinding, StyledExt}; +use ui::{h_stack, v_stack, HighlightedLabel, KeyBinding, ListItem}; use util::{ channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL}, ResultExt, @@ -69,7 +67,7 @@ impl CommandPalette { } } -impl EventEmitter for CommandPalette {} +impl EventEmitter for CommandPalette {} impl FocusableView for CommandPalette { fn focus_handle(&self, cx: &AppContext) -> FocusHandle { @@ -78,7 +76,7 @@ impl FocusableView for CommandPalette { } impl Render for CommandPalette { - type Element = Div; + type Element = Div; fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { v_stack().w_96().child(self.picker.clone()) @@ -141,7 +139,7 @@ impl CommandPaletteDelegate { } impl PickerDelegate for CommandPaletteDelegate { - type ListItem = Div>; + type ListItem = ListItem; fn placeholder_text(&self) -> Arc { "Execute a command...".into() @@ -269,7 +267,7 @@ impl PickerDelegate for CommandPaletteDelegate { fn dismissed(&mut self, cx: &mut ViewContext>) { self.command_palette - .update(cx, |_, cx| cx.emit(Manager::Dismiss)) + .update(cx, |_, cx| cx.emit(DismissEvent::Dismiss)) .log_err(); } @@ -294,24 +292,16 @@ impl PickerDelegate for CommandPaletteDelegate { ix: usize, selected: bool, cx: &mut ViewContext>, - ) -> Self::ListItem { - let colors = cx.theme().colors(); + ) -> Option { let Some(r#match) = self.matches.get(ix) else { - return div(); + return None; }; let Some(command) = self.commands.get(r#match.candidate_id) else { - return div(); + return None; }; - div() - .px_1() - .text_color(colors.text) - .text_ui() - .bg(colors.ghost_element_background) - .rounded_md() - .when(selected, |this| this.bg(colors.ghost_element_selected)) - .hover(|this| this.bg(colors.ghost_element_hover)) - .child( + Some( + ListItem::new(ix).selected(selected).child( h_stack() .justify_between() .child(HighlightedLabel::new( @@ -319,7 +309,8 @@ impl PickerDelegate for CommandPaletteDelegate { r#match.positions.clone(), )) .children(KeyBinding::for_action(&*command.action, cx)), - ) + ), + ) } } diff --git a/crates/diagnostics2/src/diagnostics.rs b/crates/diagnostics2/src/diagnostics.rs index 623b636319..dc7f0a1f3f 100644 --- a/crates/diagnostics2/src/diagnostics.rs +++ b/crates/diagnostics2/src/diagnostics.rs @@ -13,10 +13,10 @@ use editor::{ }; use futures::future::try_join_all; use gpui::{ - actions, div, AnyElement, AnyView, AppContext, Component, Context, Div, EventEmitter, - FocusEvent, FocusHandle, Focusable, FocusableComponent, FocusableView, InteractiveComponent, - Model, ParentComponent, Render, SharedString, Styled, Subscription, Task, View, ViewContext, - VisualContext, WeakView, + actions, div, AnyElement, AnyView, AppContext, Context, Div, EventEmitter, FocusEvent, + FocusHandle, Focusable, FocusableElement, FocusableView, InteractiveElement, IntoElement, + Model, ParentElement, Render, SharedString, Styled, Subscription, Task, View, ViewContext, + 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}, @@ -91,7 +91,7 @@ struct DiagnosticGroupState { impl EventEmitter for ProjectDiagnosticsEditor {} impl Render for ProjectDiagnosticsEditor { - type Element = Focusable>; + 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,27 +778,28 @@ 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| { stack.child(Label::new(code.clone())) }) - .render() + .into_any_element() }) } -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 { - Label::new("No problems").render() + let label = Label::new("No problems"); + label.into_any_element() } else { h_stack() .bg(gpui::red()) @@ -806,7 +807,7 @@ pub(crate) fn render_summary(summary: &DiagnosticSummary) -> AnyElem .child(Label::new(summary.error_count.to_string())) .child(IconElement::new(Icon::ExclamationTriangle)) .child(Label::new(summary.warning_count.to_string())) - .render() + .into_any_element() } } @@ -1549,7 +1550,7 @@ mod tests { block_id: ix, editor_style: &editor::EditorStyle::default(), }) - .element_id()? + .inner_id()? .try_into() .ok()?, diff --git a/crates/diagnostics2/src/items.rs b/crates/diagnostics2/src/items.rs index dd1b7d98cf..ac24b7ad50 100644 --- a/crates/diagnostics2/src/items.rs +++ b/crates/diagnostics2/src/items.rs @@ -1,13 +1,13 @@ use collections::HashSet; use editor::{Editor, GoToDiagnostic}; use gpui::{ - rems, Div, EventEmitter, InteractiveComponent, ParentComponent, Render, Stateful, - StatefulInteractiveComponent, Styled, Subscription, View, ViewContext, WeakView, + rems, Div, EventEmitter, InteractiveElement, ParentElement, Render, Stateful, + StatefulInteractiveElement, Styled, Subscription, View, ViewContext, WeakView, }; 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; @@ -22,30 +22,30 @@ pub struct DiagnosticIndicator { } impl Render for DiagnosticIndicator { - type Element = Stateful>; + 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) + .id("diagnostic-indicator") + .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 8d4efe00c3..e513076ec8 100644 --- a/crates/diagnostics2/src/toolbar_controls.rs +++ b/crates/diagnostics2/src/toolbar_controls.rs @@ -1,5 +1,5 @@ use crate::ProjectDiagnosticsEditor; -use gpui::{div, Div, EventEmitter, ParentComponent, Render, ViewContext, WeakView}; +use gpui::{div, Div, EventEmitter, ParentElement, Render, ViewContext, WeakView}; use ui::{Icon, IconButton, Tooltip}; use workspace::{item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView}; @@ -8,7 +8,7 @@ pub struct ToolbarControls { } impl Render for ToolbarControls { - type Element = Div; + 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/editor/src/editor.rs b/crates/editor/src/editor.rs index 2558aec121..76dec3e1b6 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1001,17 +1001,18 @@ impl CompletionsMenu { fn pre_resolve_completion_documentation( &self, - project: Option>, + editor: &Editor, cx: &mut ViewContext, - ) { + ) -> Option> { let settings = settings::get::(cx); if !settings.show_completion_documentation { - return; + return None; } - let Some(project) = project else { - return; + let Some(project) = editor.project.clone() else { + return None; }; + let client = project.read(cx).client(); let language_registry = project.read(cx).languages().clone(); @@ -1021,7 +1022,7 @@ impl CompletionsMenu { let completions = self.completions.clone(); let completion_indices: Vec<_> = self.matches.iter().map(|m| m.candidate_id).collect(); - cx.spawn(move |this, mut cx| async move { + Some(cx.spawn(move |this, mut cx| async move { if is_remote { let Some(project_id) = project_id else { log::error!("Remote project without remote_id"); @@ -1083,8 +1084,7 @@ impl CompletionsMenu { _ = this.update(&mut cx, |_, cx| cx.notify()); } } - }) - .detach(); + })) } fn attempt_resolve_selected_completion_documentation( @@ -3423,7 +3423,7 @@ impl Editor { to_insert, }) = self.inlay_hint_cache.spawn_hint_refresh( reason_description, - self.excerpt_visible_offsets(required_languages.as_ref(), cx), + self.excerpts_for_inlay_hints_query(required_languages.as_ref(), cx), invalidate_cache, cx, ) { @@ -3442,11 +3442,15 @@ impl Editor { .collect() } - pub fn excerpt_visible_offsets( + pub fn excerpts_for_inlay_hints_query( &self, restrict_to_languages: Option<&HashSet>>, cx: &mut ViewContext<'_, '_, Editor>, ) -> HashMap, Global, Range)> { + let Some(project) = self.project.as_ref() else { + return HashMap::default(); + }; + let project = project.read(cx); let multi_buffer = self.buffer().read(cx); let multi_buffer_snapshot = multi_buffer.snapshot(cx); let multi_buffer_visible_start = self @@ -3466,6 +3470,14 @@ impl Editor { .filter(|(_, excerpt_visible_range, _)| !excerpt_visible_range.is_empty()) .filter_map(|(buffer_handle, excerpt_visible_range, excerpt_id)| { let buffer = buffer_handle.read(cx); + let buffer_file = project::worktree::File::from_dyn(buffer.file())?; + let buffer_worktree = project.worktree_for_id(buffer_file.worktree_id(cx), cx)?; + let worktree_entry = buffer_worktree + .read(cx) + .entry_for_id(buffer_file.project_entry_id(cx)?)?; + if worktree_entry.is_ignored { + return None; + } let language = buffer.language()?; if let Some(restrict_to_languages) = restrict_to_languages { if !restrict_to_languages.contains(language) { @@ -3580,7 +3592,8 @@ impl Editor { let id = post_inc(&mut self.next_completion_id); let task = cx.spawn(|this, mut cx| { async move { - let menu = if let Some(completions) = completions.await.log_err() { + let completions = completions.await.log_err(); + let (menu, pre_resolve_task) = if let Some(completions) = completions { let mut menu = CompletionsMenu { id, initial_position: position, @@ -3601,21 +3614,26 @@ impl Editor { selected_item: 0, list: Default::default(), }; + menu.filter(query.as_deref(), cx.background()).await; + if menu.matches.is_empty() { - None + (None, None) } else { - _ = this.update(&mut cx, |editor, cx| { - menu.pre_resolve_completion_documentation(editor.project.clone(), cx); - }); - Some(menu) + let pre_resolve_task = this + .update(&mut cx, |editor, cx| { + menu.pre_resolve_completion_documentation(editor, cx) + }) + .ok() + .flatten(); + (Some(menu), pre_resolve_task) } } else { - None + (None, None) }; this.update(&mut cx, |this, cx| { - this.completion_tasks.retain(|(task_id, _)| *task_id > id); + this.completion_tasks.retain(|(task_id, _)| *task_id >= id); let mut context_menu = this.context_menu.write(); match context_menu.as_ref() { @@ -3636,10 +3654,10 @@ impl Editor { drop(context_menu); this.discard_copilot_suggestion(cx); cx.notify(); - } else if this.completion_tasks.is_empty() { - // If there are no more completion tasks and the last menu was - // empty, we should hide it. If it was already hidden, we should - // also show the copilot suggestion when available. + } else if this.completion_tasks.len() <= 1 { + // If there are no more completion tasks (omitting ourself) and + // the last menu was empty, we should hide it. If it was already + // hidden, we should also show the copilot suggestion when available. drop(context_menu); if this.hide_context_menu(cx).is_none() { this.update_visible_copilot_suggestion(cx); @@ -3647,10 +3665,15 @@ impl Editor { } })?; + if let Some(pre_resolve_task) = pre_resolve_task { + pre_resolve_task.await; + } + Ok::<_, anyhow::Error>(()) } .log_err() }); + self.completion_tasks.push((id, task)); } diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 6b2712e7bf..47d8a4cf1f 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -861,7 +861,7 @@ async fn fetch_and_update_hints( let inlay_hints_fetch_task = editor .update(&mut cx, |editor, cx| { if got_throttled { - let query_not_around_visible_range = match editor.excerpt_visible_offsets(None, cx).remove(&query.excerpt_id) { + let query_not_around_visible_range = match editor.excerpts_for_inlay_hints_query(None, cx).remove(&query.excerpt_id) { Some((_, _, current_visible_range)) => { let visible_offset_length = current_visible_range.len(); let double_visible_range = current_visible_range @@ -2237,7 +2237,9 @@ pub mod tests { editor: &ViewHandle, cx: &mut gpui::TestAppContext, ) -> Range { - let ranges = editor.update(cx, |editor, cx| editor.excerpt_visible_offsets(None, cx)); + let ranges = editor.update(cx, |editor, cx| { + editor.excerpts_for_inlay_hints_query(None, cx) + }); assert_eq!( ranges.len(), 1, diff --git a/crates/editor2/src/display_map/block_map.rs b/crates/editor2/src/display_map/block_map.rs index 05106dd2a1..00778c2edd 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 be621bc5f6..c8ce37d7e2 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -40,11 +40,12 @@ use fuzzy::{StringMatch, StringMatchCandidate}; use git::diff_hunk_to_display; use gpui::{ actions, div, point, prelude::*, px, relative, rems, size, uniform_list, Action, AnyElement, - AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Component, Context, - EventEmitter, FocusHandle, FocusableView, FontFeatures, FontStyle, FontWeight, HighlightStyle, - Hsla, InputHandler, KeyContext, Model, MouseButton, ParentComponent, Pixels, Render, Styled, - Subscription, Task, TextStyle, UniformListScrollHandle, View, ViewContext, VisualContext, - WeakView, WindowContext, + AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Context, + DispatchPhase, Div, ElementId, EventEmitter, FocusHandle, FocusableView, FontFeatures, + FontStyle, FontWeight, HighlightStyle, Hsla, InputHandler, InteractiveText, KeyContext, Model, + MouseButton, ParentElement, Pixels, Render, RenderOnce, SharedString, Styled, StyledText, + Subscription, Task, TextRun, TextStyle, UniformListScrollHandle, View, ViewContext, + VisualContext, WeakView, WhiteSpace, WindowContext, }; use highlight_matching_bracket::refresh_matching_bracket_highlights; use hover_popover::{hide_hover, HoverState}; @@ -54,13 +55,14 @@ 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, + markdown, 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,12 +99,12 @@ 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, Popover, StyledExt, Tooltip}; use util::{post_inc, RangeExt, ResultExt, TryFutureExt}; use workspace::{ item::{ItemEvent, ItemHandle}, searchable::SearchEvent, - ItemNavHistory, SplitDirection, ViewId, Workspace, + ItemNavHistory, Pane, SplitDirection, ViewId, Workspace, }; const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); @@ -115,70 +117,70 @@ pub const DOCUMENT_HIGHLIGHTS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2); -// pub fn render_parsed_markdown( -// parsed: &language::ParsedMarkdown, -// editor_style: &EditorStyle, -// workspace: Option>, -// cx: &mut ViewContext, -// ) -> Text { -// enum RenderedMarkdown {} +pub fn render_parsed_markdown( + element_id: impl Into, + parsed: &language::ParsedMarkdown, + editor_style: &EditorStyle, + workspace: Option>, + cx: &mut ViewContext, +) -> InteractiveText { + let code_span_background_color = cx + .theme() + .colors() + .editor_document_highlight_read_background; -// let parsed = parsed.clone(); -// let view_id = cx.view_id(); -// let code_span_background_color = editor_style.document_highlight_read_background; + let highlights = gpui::combine_highlights( + parsed.highlights.iter().filter_map(|(range, highlight)| { + let highlight = highlight.to_highlight_style(&editor_style.syntax)?; + Some((range.clone(), highlight)) + }), + parsed + .regions + .iter() + .zip(&parsed.region_ranges) + .filter_map(|(region, range)| { + if region.code { + Some(( + range.clone(), + HighlightStyle { + background_color: Some(code_span_background_color), + ..Default::default() + }, + )) + } else { + None + } + }), + ); + let runs = text_runs_for_highlights(&parsed.text, &editor_style.text, highlights); -// let mut region_id = 0; + // todo!("add the ability to change cursor style for link ranges") + let mut links = Vec::new(); + let mut link_ranges = Vec::new(); + for (range, region) in parsed.region_ranges.iter().zip(&parsed.regions) { + if let Some(link) = region.link.clone() { + links.push(link); + link_ranges.push(range.clone()); + } + } -// todo!() -// // Text::new(parsed.text, editor_style.text.clone()) -// // .with_highlights( -// // parsed -// // .highlights -// // .iter() -// // .filter_map(|(range, highlight)| { -// // let highlight = highlight.to_highlight_style(&editor_style.syntax)?; -// // Some((range.clone(), highlight)) -// // }) -// // .collect::>(), -// // ) -// // .with_custom_runs(parsed.region_ranges, move |ix, bounds, cx| { -// // region_id += 1; -// // let region = parsed.regions[ix].clone(); - -// // if let Some(link) = region.link { -// // cx.scene().push_cursor_region(CursorRegion { -// // bounds, -// // style: CursorStyle::PointingHand, -// // }); -// // cx.scene().push_mouse_region( -// // MouseRegion::new::<(RenderedMarkdown, Tag)>(view_id, region_id, bounds) -// // .on_down::(MouseButton::Left, move |_, _, cx| match &link { -// // markdown::Link::Web { url } => cx.platform().open_url(url), -// // markdown::Link::Path { path } => { -// // if let Some(workspace) = &workspace { -// // _ = workspace.update(cx, |workspace, cx| { -// // workspace.open_abs_path(path.clone(), false, cx).detach(); -// // }); -// // } -// // } -// // }), -// // ); -// // } - -// // if region.code { -// // cx.draw_quad(Quad { -// // bounds, -// // background: Some(code_span_background_color), -// // corner_radii: (2.0).into(), -// // order: todo!(), -// // content_mask: todo!(), -// // border_color: todo!(), -// // border_widths: todo!(), -// // }); -// // } -// // }) -// // .with_soft_wrap(true) -// } + InteractiveText::new( + element_id, + StyledText::new(parsed.text.clone()).with_runs(runs), + ) + .on_click(link_ranges, move |clicked_range_ix, cx| { + match &links[clicked_range_ix] { + markdown::Link::Web { url } => cx.open_url(url), + markdown::Link::Path { path } => { + if let Some(workspace) = &workspace { + _ = workspace.update(cx, |workspace, cx| { + workspace.open_abs_path(path.clone(), false, cx).detach(); + }); + } + } + } + }) +} #[derive(PartialEq, Clone, Deserialize, Default, Action)] pub struct SelectNext { @@ -529,8 +531,6 @@ pub fn init(cx: &mut AppContext) { // cx.register_action_type(Editor::context_menu_next); // cx.register_action_type(Editor::context_menu_last); - hover_popover::init(cx); - workspace::register_project_item::(cx); workspace::register_followable_item::(cx); workspace::register_deserializable_item::(cx); @@ -663,6 +663,7 @@ pub struct Editor { pixel_position_of_newest_cursor: Option>, gutter_width: Pixels, style: Option, + editor_actions: Vec)>>, } pub struct EditorSnapshot { @@ -905,12 +906,16 @@ impl ContextMenu { &self, cursor_position: DisplayPoint, style: &EditorStyle, + max_height: Pixels, 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), + ContextMenu::Completions(menu) => ( + cursor_position, + menu.render(style, max_height, workspace, cx), + ), + ContextMenu::CodeActions(menu) => menu.render(cursor_position, style, max_height, cx), } } } @@ -966,20 +971,22 @@ impl CompletionsMenu { fn pre_resolve_completion_documentation( &self, - project: Option>, - cx: &mut ViewContext, - ) { + _editor: &Editor, + _cx: &mut ViewContext, + ) -> Option> { // todo!("implementation below "); + None } - // ) { + // { // let settings = EditorSettings::get_global(cx); // if !settings.show_completion_documentation { - // return; + // return None; // } - // let Some(project) = project else { - // return; + // let Some(project) = editor.project.clone() else { + // return None; // }; + // let client = project.read(cx).client(); // let language_registry = project.read(cx).languages().clone(); @@ -989,7 +996,7 @@ impl CompletionsMenu { // let completions = self.completions.clone(); // let completion_indices: Vec<_> = self.matches.iter().map(|m| m.candidate_id).collect(); - // cx.spawn(move |this, mut cx| async move { + // Some(cx.spawn(move |this, mut cx| async move { // if is_remote { // let Some(project_id) = project_id else { // log::error!("Remote project without remote_id"); @@ -1051,8 +1058,7 @@ impl CompletionsMenu { // _ = this.update(&mut cx, |_, cx| cx.notify()); // } // } - // }) - // .detach(); + // })) // } fn attempt_resolve_selected_completion_documentation( @@ -1221,211 +1227,157 @@ impl CompletionsMenu { fn render( &self, style: &EditorStyle, + max_height: Pixels, 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; + + 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(); + } + } + + len + }) + .map(|(ix, _)| ix); + + let completions = self.completions.clone(); + let matches = self.matches.clone(); + let selected_item = self.selected_item; + let style = style.clone(); + + let multiline_docs = { + let mat = &self.matches[selected_item]; + let multiline_docs = match &self.completions.read()[mat.candidate_id].documentation { + Some(Documentation::MultiLinePlainText(text)) => { + Some(div().child(SharedString::from(text.clone()))) + } + Some(Documentation::MultiLineMarkdown(parsed)) => Some(div().child( + render_parsed_markdown("completions_markdown", parsed, &style, workspace, cx), + )), + _ => None, + }; + multiline_docs.map(|div| { + div.id("multiline_docs") + .max_h(max_height) + .flex_1() + .px_1p5() + .py_1() + .min_w(px(260.)) + .max_w(px(640.)) + .w(px(500.)) + .text_ui() + .overflow_y_scroll() + // Prevent a mouse down on documentation from being propagated to the editor, + // because that would move the cursor. + .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation()) + }) + }; + let list = uniform_list( + cx.view().clone(), + "completions", + matches.len(), + move |editor, range, cx| { + let start_ix = range.start; + let completions_guard = completions.read(); + + 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 documentation = if show_completion_documentation { + &completion.documentation + } else { + &None + }; + + let highlights = gpui::combine_highlights( + mat.ranges().map(|range| (range, FontWeight::BOLD.into())), + styled_runs_for_code_label(&completion.label, &style.syntax).map( + |(range, mut highlight)| { + // Ignore font weight for syntax highlighting, as we'll use it + // for fuzzy matches. + highlight.font_weight = None; + (range, highlight) + }, + ), + ); + let completion_label = StyledText::new(completion.label.text.clone()) + .with_runs(text_runs_for_highlights( + &completion.label.text, + &style.text, + highlights, + )); + let documentation_label = + if let Some(Documentation::SingleLine(text)) = documentation { + Some(SharedString::from(text.clone())) + } else { + None + }; + + div() + .id(mat.candidate_id) + .min_w(px(220.)) + .max_w(px(540.)) + .whitespace_nowrap() + .overflow_hidden() + .text_ui() + .px_1() + .rounded(px(4.)) + .bg(cx.theme().colors().ghost_element_background) + .hover(|style| style.bg(cx.theme().colors().ghost_element_hover)) + .when(item_ix == selected_item, |div| { + div.bg(cx.theme().colors().ghost_element_selected) + }) + .on_mouse_down( + MouseButton::Left, + cx.listener(move |editor, event, cx| { + cx.stop_propagation(); + editor + .confirm_completion( + &ConfirmCompletion { + item_ix: Some(item_ix), + }, + cx, + ) + .map(|task| task.detach_and_log_err(cx)); + }), + ) + .child(completion_label) + .children(documentation_label) + }) + .collect() + }, + ) + .max_h(max_height) + .track_scroll(self.scroll_handle.clone()) + .with_width_from_item(widest_completion_ix); + + Popover::new() + .child(list) + .when_some(multiline_docs, |popover, multiline_docs| { + popover.aside(multiline_docs) + }) + .into_any_element() } - // enum CompletionTag {} - - // let settings = EditorSettings>(cx); - // let show_completion_documentation = settings.show_completion_documentation; - - // 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(); - // } - // } - - // len - // }) - // .map(|(ix, _)| ix); - - // 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 - // }; - - // 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() - // } - pub async fn filter(&mut self, query: Option<&str>, executor: BackgroundExecutor) { let mut matches = if let Some(query) = query { fuzzy::match_strings( @@ -1540,14 +1492,17 @@ impl CodeActionsMenu { &self, mut cursor_position: DisplayPoint, style: &EditorStyle, + max_height: Pixels, 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,18 +1524,22 @@ 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)); - }) - .child(action.lsp_action.title.clone()) + .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())) }) .collect() }, @@ -1588,6 +1547,8 @@ impl CodeActionsMenu { .elevation_1(cx) .px_2() .py_1() + .max_h(max_height) + .track_scroll(self.scroll_handle.clone()) .with_width_from_item( self.actions .iter() @@ -1595,7 +1556,7 @@ impl CodeActionsMenu { .max_by_key(|(_, action)| action.lsp_action.title.chars().count()) .map(|(ix, _)| ix), ) - .render(); + .into_any_element(); if self.deployed_from_indicator { *cursor_position.column_mut() = 0; @@ -1943,6 +1904,7 @@ impl Editor { pixel_position_of_newest_cursor: None, gutter_width: Default::default(), style: None, + editor_actions: Default::default(), _subscriptions: vec![ cx.observe(&buffer, Self::on_buffer_changed), cx.subscribe(&buffer, Self::on_buffer_event), @@ -2074,10 +2036,14 @@ impl Editor { &self.buffer } - fn workspace(&self) -> Option> { + pub fn workspace(&self) -> Option> { self.workspace.as_ref()?.0.upgrade() } + pub fn pane(&self, cx: &AppContext) -> Option> { + self.workspace()?.read(cx).pane_for(&self.handle.upgrade()?) + } + pub fn title<'a>(&self, cx: &'a AppContext) -> Cow<'a, str> { self.buffer().read(cx).title(cx) } @@ -2319,6 +2285,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) @@ -3437,7 +3404,7 @@ impl Editor { to_insert, }) = self.inlay_hint_cache.spawn_hint_refresh( reason_description, - self.excerpt_visible_offsets(required_languages.as_ref(), cx), + self.excerpts_for_inlay_hints_query(required_languages.as_ref(), cx), invalidate_cache, cx, ) { @@ -3456,11 +3423,15 @@ impl Editor { .collect() } - pub fn excerpt_visible_offsets( + pub fn excerpts_for_inlay_hints_query( &self, restrict_to_languages: Option<&HashSet>>, cx: &mut ViewContext, ) -> HashMap, clock::Global, Range)> { + let Some(project) = self.project.as_ref() else { + return HashMap::default(); + }; + let project = project.read(cx); let multi_buffer = self.buffer().read(cx); let multi_buffer_snapshot = multi_buffer.snapshot(cx); let multi_buffer_visible_start = self @@ -3480,6 +3451,15 @@ impl Editor { .filter(|(_, excerpt_visible_range, _)| !excerpt_visible_range.is_empty()) .filter_map(|(buffer_handle, excerpt_visible_range, excerpt_id)| { let buffer = buffer_handle.read(cx); + let buffer_file = project::worktree::File::from_dyn(buffer.file())?; + let buffer_worktree = project.worktree_for_id(buffer_file.worktree_id(cx), cx)?; + let worktree_entry = buffer_worktree + .read(cx) + .entry_for_id(buffer_file.project_entry_id(cx)?)?; + if worktree_entry.is_ignored { + return None; + } + let language = buffer.language()?; if let Some(restrict_to_languages) = restrict_to_languages { if !restrict_to_languages.contains(language) { @@ -3594,7 +3574,8 @@ impl Editor { let id = post_inc(&mut self.next_completion_id); let task = cx.spawn(|this, mut cx| { async move { - let menu = if let Some(completions) = completions.await.log_err() { + let completions = completions.await.log_err(); + let (menu, pre_resolve_task) = if let Some(completions) = completions { let mut menu = CompletionsMenu { id, initial_position: position, @@ -3617,20 +3598,24 @@ impl Editor { }; menu.filter(query.as_deref(), cx.background_executor().clone()) .await; + if menu.matches.is_empty() { - None + (None, None) } else { - _ = this.update(&mut cx, |editor, cx| { - menu.pre_resolve_completion_documentation(editor.project.clone(), cx); - }); - Some(menu) + let pre_resolve_task = this + .update(&mut cx, |editor, cx| { + menu.pre_resolve_completion_documentation(editor, cx) + }) + .ok() + .flatten(); + (Some(menu), pre_resolve_task) } } else { - None + (None, None) }; this.update(&mut cx, |this, cx| { - this.completion_tasks.retain(|(task_id, _)| *task_id > id); + this.completion_tasks.retain(|(task_id, _)| *task_id >= id); let mut context_menu = this.context_menu.write(); match context_menu.as_ref() { @@ -3662,142 +3647,147 @@ impl Editor { } })?; + if let Some(pre_resolve_task) = pre_resolve_task { + pre_resolve_task.await; + } + Ok::<_, anyhow::Error>(()) } .log_err() }); + self.completion_tasks.push((id, task)); } - // pub fn confirm_completion( - // &mut self, - // action: &ConfirmCompletion, - // cx: &mut ViewContext, - // ) -> Option>> { - // use language::ToOffset as _; + pub fn confirm_completion( + &mut self, + action: &ConfirmCompletion, + cx: &mut ViewContext, + ) -> Option>> { + use language::ToOffset as _; - // let completions_menu = if let ContextMenu::Completions(menu) = self.hide_context_menu(cx)? { - // menu - // } else { - // return None; - // }; + let completions_menu = if let ContextMenu::Completions(menu) = self.hide_context_menu(cx)? { + menu + } else { + return None; + }; - // let mat = completions_menu - // .matches - // .get(action.item_ix.unwrap_or(completions_menu.selected_item))?; - // let buffer_handle = completions_menu.buffer; - // let completions = completions_menu.completions.read(); - // let completion = completions.get(mat.candidate_id)?; + let mat = completions_menu + .matches + .get(action.item_ix.unwrap_or(completions_menu.selected_item))?; + let buffer_handle = completions_menu.buffer; + let completions = completions_menu.completions.read(); + let completion = completions.get(mat.candidate_id)?; - // let snippet; - // let text; - // if completion.is_snippet() { - // snippet = Some(Snippet::parse(&completion.new_text).log_err()?); - // text = snippet.as_ref().unwrap().text.clone(); - // } else { - // snippet = None; - // text = completion.new_text.clone(); - // }; - // let selections = self.selections.all::(cx); - // let buffer = buffer_handle.read(cx); - // let old_range = completion.old_range.to_offset(buffer); - // let old_text = buffer.text_for_range(old_range.clone()).collect::(); + let snippet; + let text; + if completion.is_snippet() { + snippet = Some(Snippet::parse(&completion.new_text).log_err()?); + text = snippet.as_ref().unwrap().text.clone(); + } else { + snippet = None; + text = completion.new_text.clone(); + }; + let selections = self.selections.all::(cx); + let buffer = buffer_handle.read(cx); + let old_range = completion.old_range.to_offset(buffer); + let old_text = buffer.text_for_range(old_range.clone()).collect::(); - // let newest_selection = self.selections.newest_anchor(); - // if newest_selection.start.buffer_id != Some(buffer_handle.read(cx).remote_id()) { - // return None; - // } + let newest_selection = self.selections.newest_anchor(); + if newest_selection.start.buffer_id != Some(buffer_handle.read(cx).remote_id()) { + return None; + } - // let lookbehind = newest_selection - // .start - // .text_anchor - // .to_offset(buffer) - // .saturating_sub(old_range.start); - // let lookahead = old_range - // .end - // .saturating_sub(newest_selection.end.text_anchor.to_offset(buffer)); - // let mut common_prefix_len = old_text - // .bytes() - // .zip(text.bytes()) - // .take_while(|(a, b)| a == b) - // .count(); + let lookbehind = newest_selection + .start + .text_anchor + .to_offset(buffer) + .saturating_sub(old_range.start); + let lookahead = old_range + .end + .saturating_sub(newest_selection.end.text_anchor.to_offset(buffer)); + let mut common_prefix_len = old_text + .bytes() + .zip(text.bytes()) + .take_while(|(a, b)| a == b) + .count(); - // let snapshot = self.buffer.read(cx).snapshot(cx); - // let mut range_to_replace: Option> = None; - // let mut ranges = Vec::new(); - // for selection in &selections { - // if snapshot.contains_str_at(selection.start.saturating_sub(lookbehind), &old_text) { - // let start = selection.start.saturating_sub(lookbehind); - // let end = selection.end + lookahead; - // if selection.id == newest_selection.id { - // range_to_replace = Some( - // ((start + common_prefix_len) as isize - selection.start as isize) - // ..(end as isize - selection.start as isize), - // ); - // } - // ranges.push(start + common_prefix_len..end); - // } else { - // common_prefix_len = 0; - // ranges.clear(); - // ranges.extend(selections.iter().map(|s| { - // if s.id == newest_selection.id { - // range_to_replace = Some( - // old_range.start.to_offset_utf16(&snapshot).0 as isize - // - selection.start as isize - // ..old_range.end.to_offset_utf16(&snapshot).0 as isize - // - selection.start as isize, - // ); - // old_range.clone() - // } else { - // s.start..s.end - // } - // })); - // break; - // } - // } - // let text = &text[common_prefix_len..]; + let snapshot = self.buffer.read(cx).snapshot(cx); + let mut range_to_replace: Option> = None; + let mut ranges = Vec::new(); + for selection in &selections { + if snapshot.contains_str_at(selection.start.saturating_sub(lookbehind), &old_text) { + let start = selection.start.saturating_sub(lookbehind); + let end = selection.end + lookahead; + if selection.id == newest_selection.id { + range_to_replace = Some( + ((start + common_prefix_len) as isize - selection.start as isize) + ..(end as isize - selection.start as isize), + ); + } + ranges.push(start + common_prefix_len..end); + } else { + common_prefix_len = 0; + ranges.clear(); + ranges.extend(selections.iter().map(|s| { + if s.id == newest_selection.id { + range_to_replace = Some( + old_range.start.to_offset_utf16(&snapshot).0 as isize + - selection.start as isize + ..old_range.end.to_offset_utf16(&snapshot).0 as isize + - selection.start as isize, + ); + old_range.clone() + } else { + s.start..s.end + } + })); + break; + } + } + let text = &text[common_prefix_len..]; - // cx.emit(Event::InputHandled { - // utf16_range_to_replace: range_to_replace, - // text: text.into(), - // }); + cx.emit(EditorEvent::InputHandled { + utf16_range_to_replace: range_to_replace, + text: text.into(), + }); - // self.transact(cx, |this, cx| { - // if let Some(mut snippet) = snippet { - // snippet.text = text.to_string(); - // for tabstop in snippet.tabstops.iter_mut().flatten() { - // tabstop.start -= common_prefix_len as isize; - // tabstop.end -= common_prefix_len as isize; - // } + self.transact(cx, |this, cx| { + if let Some(mut snippet) = snippet { + snippet.text = text.to_string(); + for tabstop in snippet.tabstops.iter_mut().flatten() { + tabstop.start -= common_prefix_len as isize; + tabstop.end -= common_prefix_len as isize; + } - // this.insert_snippet(&ranges, snippet, cx).log_err(); - // } else { - // this.buffer.update(cx, |buffer, cx| { - // buffer.edit( - // ranges.iter().map(|range| (range.clone(), text)), - // this.autoindent_mode.clone(), - // cx, - // ); - // }); - // } + this.insert_snippet(&ranges, snippet, cx).log_err(); + } else { + this.buffer.update(cx, |buffer, cx| { + buffer.edit( + ranges.iter().map(|range| (range.clone(), text)), + this.autoindent_mode.clone(), + cx, + ); + }); + } - // this.refresh_copilot_suggestions(true, cx); - // }); + this.refresh_copilot_suggestions(true, cx); + }); - // let project = self.project.clone()?; - // let apply_edits = project.update(cx, |project, cx| { - // project.apply_additional_edits_for_completion( - // buffer_handle, - // completion.clone(), - // true, - // cx, - // ) - // }); - // Some(cx.foreground().spawn(async move { - // apply_edits.await?; - // Ok(()) - // })) - // } + let project = self.project.clone()?; + let apply_edits = project.update(cx, |project, cx| { + project.apply_additional_edits_for_completion( + buffer_handle, + completion.clone(), + true, + cx, + ) + }); + Some(cx.foreground_executor().spawn(async move { + apply_edits.await?; + Ok(()) + })) + } pub fn toggle_code_actions(&mut self, action: &ToggleCodeActions, cx: &mut ViewContext) { let mut context_menu = self.context_menu.write(); @@ -4353,19 +4343,19 @@ 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, }, cx, ); - }) - .render(), + }, + )), ) } else { None @@ -4380,7 +4370,7 @@ impl Editor { line_height: Pixels, gutter_margin: Pixels, cx: &mut ViewContext, - ) -> Vec>> { + ) -> Vec> { fold_data .iter() .enumerate() @@ -4393,16 +4383,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) - .render() + })) + .color(ui::Color::Muted) }) }) .flatten() @@ -4421,12 +4410,14 @@ impl Editor { &self, cursor_position: DisplayPoint, style: &EditorStyle, + max_height: Pixels, cx: &mut ViewContext, - ) -> Option<(DisplayPoint, AnyElement)> { + ) -> Option<(DisplayPoint, AnyElement)> { self.context_menu.read().as_ref().map(|menu| { menu.render( cursor_position, style, + max_height, self.workspace.as_ref().map(|(w, _)| w.clone()), cx, ) @@ -7781,7 +7772,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,11 +7780,13 @@ 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_element() } }), disposition: BlockDisposition::Below, @@ -9207,6 +9200,26 @@ impl Editor { cx.emit(EditorEvent::Blurred); cx.notify(); } + + pub fn register_action( + &mut self, + listener: impl Fn(&A, &mut WindowContext) + 'static, + ) -> &mut Self { + let listener = Arc::new(listener); + + self.editor_actions.push(Box::new(move |cx| { + let view = cx.view().clone(); + let cx = cx.window_context(); + let listener = listener.clone(); + cx.on_action(TypeId::of::(), move |action, phase, cx| { + let action = action.downcast_ref().unwrap(); + if phase == DispatchPhase::Bubble { + listener(action, cx) + } + }) + })); + self + } } pub trait CollaborationHub { @@ -9396,7 +9409,9 @@ impl Render for Editor { font_weight: FontWeight::NORMAL, font_style: FontStyle::Normal, line_height: relative(1.).into(), + background_color: None, underline: None, + white_space: WhiteSpace::Normal, }, EditorMode::AutoHeight { max_lines } => todo!(), @@ -9409,7 +9424,9 @@ impl Render for Editor { font_weight: FontWeight::NORMAL, font_style: FontStyle::Normal, line_height: relative(settings.buffer_line_height.value()), + background_color: None, underline: None, + white_space: WhiteSpace::Normal, }, }; @@ -9996,11 +10013,11 @@ 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)) - .render() + })) + .tooltip(|cx| Tooltip::text("Copy diagnostic message", cx)) + .into_any_element() }) } @@ -10047,120 +10064,75 @@ pub fn diagnostic_style( } } -pub fn combine_syntax_and_fuzzy_match_highlights( +pub fn text_runs_for_highlights( text: &str, - default_style: HighlightStyle, - syntax_ranges: impl Iterator, HighlightStyle)>, - match_indices: &[usize], -) -> Vec<(Range, HighlightStyle)> { - let mut result = Vec::new(); - let mut match_indices = match_indices.iter().copied().peekable(); - - for (range, mut syntax_highlight) in syntax_ranges.chain([(usize::MAX..0, Default::default())]) - { - syntax_highlight.font_weight = None; - - // Add highlights for any fuzzy match characters before the next - // syntax highlight range. - while let Some(&match_index) = match_indices.peek() { - if match_index >= range.start { - break; - } - match_indices.next(); - let end_index = char_ix_after(match_index, text); - let mut match_style = default_style; - match_style.font_weight = Some(FontWeight::BOLD); - result.push((match_index..end_index, match_style)); - } - - if range.start == usize::MAX { - break; - } - - // Add highlights for any fuzzy match characters within the - // syntax highlight range. - let mut offset = range.start; - while let Some(&match_index) = match_indices.peek() { - if match_index >= range.end { - break; - } - - match_indices.next(); - if match_index > offset { - result.push((offset..match_index, syntax_highlight)); - } - - let mut end_index = char_ix_after(match_index, text); - while let Some(&next_match_index) = match_indices.peek() { - if next_match_index == end_index && next_match_index < range.end { - end_index = char_ix_after(next_match_index, text); - match_indices.next(); - } else { - break; - } - } - - let mut match_style = syntax_highlight; - match_style.font_weight = Some(FontWeight::BOLD); - result.push((match_index..end_index, match_style)); - offset = end_index; - } - - if offset < range.end { - result.push((offset..range.end, syntax_highlight)); + default_style: &TextStyle, + highlights: impl IntoIterator, HighlightStyle)>, +) -> Vec { + let mut runs = Vec::new(); + let mut ix = 0; + for (range, highlight) in highlights { + if ix < range.start { + runs.push(default_style.clone().to_run(range.start - ix)); } + runs.push( + default_style + .clone() + .highlight(highlight) + .to_run(range.len()), + ); + ix = range.end; } - - fn char_ix_after(ix: usize, text: &str) -> usize { - ix + text[ix..].chars().next().unwrap().len_utf8() + if ix < text.len() { + runs.push(default_style.to_run(text.len() - ix)); } - - result + runs } -// 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/editor_tests.rs b/crates/editor2/src/editor_tests.rs index 06a8636f8c..6865e81cfa 100644 --- a/crates/editor2/src/editor_tests.rs +++ b/crates/editor2/src/editor_tests.rs @@ -3048,7 +3048,7 @@ fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) { position: snapshot.anchor_after(Point::new(2, 0)), disposition: BlockDisposition::Below, height: 1, - render: Arc::new(|_| div().render()), + render: Arc::new(|_| div().into_any()), }], Some(Autoscroll::fit()), cx, @@ -6740,75 +6740,6 @@ async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) { // ); // } -#[test] -fn test_combine_syntax_and_fuzzy_match_highlights() { - let string = "abcdefghijklmnop"; - let syntax_ranges = [ - ( - 0..3, - HighlightStyle { - color: Some(Hsla::red()), - ..Default::default() - }, - ), - ( - 4..8, - HighlightStyle { - color: Some(Hsla::green()), - ..Default::default() - }, - ), - ]; - let match_indices = [4, 6, 7, 8]; - assert_eq!( - combine_syntax_and_fuzzy_match_highlights( - string, - Default::default(), - syntax_ranges.into_iter(), - &match_indices, - ), - &[ - ( - 0..3, - HighlightStyle { - color: Some(Hsla::red()), - ..Default::default() - }, - ), - ( - 4..5, - HighlightStyle { - color: Some(Hsla::green()), - font_weight: Some(gpui::FontWeight::BOLD), - ..Default::default() - }, - ), - ( - 5..6, - HighlightStyle { - color: Some(Hsla::green()), - ..Default::default() - }, - ), - ( - 6..8, - HighlightStyle { - color: Some(Hsla::green()), - font_weight: Some(gpui::FontWeight::BOLD), - ..Default::default() - }, - ), - ( - 8..9, - HighlightStyle { - font_weight: Some(gpui::FontWeight::BOLD), - ..Default::default() - }, - ), - ] - ); -} - #[gpui::test] async fn go_to_prev_overlapping_diagnostic( executor: BackgroundExecutor, diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index 220fe89ea6..6a6afd5461 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -5,7 +5,9 @@ use crate::{ }, editor_settings::ShowScrollbar, git::{diff_hunk_to_display, DisplayDiffHunk}, - hover_popover::hover_at, + hover_popover::{ + self, hover_at, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT, + }, link_go_to_definition::{ go_to_fetched_definition, go_to_fetched_type_definition, update_go_to_definition_link, update_inlay_link_and_hover_points, GoToDefinitionTrigger, @@ -19,11 +21,11 @@ use anyhow::Result; use collections::{BTreeMap, HashMap}; use gpui::{ div, point, px, relative, size, transparent_black, Action, AnyElement, AvailableSpace, - BorrowWindow, Bounds, Component, ContentMask, Corners, DispatchPhase, Edges, Element, - ElementId, ElementInputHandler, Entity, EntityId, Hsla, InteractiveComponent, LineLayout, - MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentComponent, Pixels, - ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveComponent, Style, Styled, - TextRun, TextStyle, View, ViewContext, WindowContext, WrappedLine, + BorrowWindow, Bounds, ContentMask, Corners, DispatchPhase, Edges, Element, ElementId, + ElementInputHandler, Entity, EntityId, Hsla, InteractiveElement, IntoElement, LineLayout, + MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, RenderOnce, + ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled, + TextRun, TextStyle, View, ViewContext, WeakView, WindowContext, WrappedLine, }; use itertools::Itertools; use language::language_settings::ShowWhitespaceSetting; @@ -112,18 +114,202 @@ 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; + self.editor.update(cx, |editor, cx| { + for action in editor.editor_actions.iter() { + (action)(cx) + } + }); + 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, hover_popover::hover); + 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); + register_action(view, cx, |editor, action, cx| { + editor + .confirm_completion(action, cx) + .map(|task| task.detach_and_log_err(cx)); + }); + 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 +535,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 +645,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; @@ -488,13 +673,14 @@ impl EditorElement { } } - for (ix, fold_indicator) in layout.fold_indicators.iter_mut().enumerate() { - if let Some(fold_indicator) = fold_indicator.as_mut() { + for (ix, fold_indicator) in layout.fold_indicators.drain(..).enumerate() { + if let Some(mut fold_indicator) = fold_indicator { + let mut fold_indicator = fold_indicator.into_any_element(); let available_space = size( 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, @@ -505,32 +691,29 @@ 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); } } - if let Some(indicator) = layout.code_actions_indicator.as_mut() { + if let Some(indicator) = layout.code_actions_indicator.take() { + let mut button = indicator.button.into_any_element(); let available_space = size( AvailableSpace::MinContent, AvailableSpace::Definite(line_height), ); - let indicator_size = indicator.element.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; // Center indicator. x += ((layout.gutter_padding + layout.gutter_margin) - indicator_size.width) / 2.; y += (line_height - indicator_size.height) / 2.; - indicator - .element - .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; @@ -618,14 +801,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 { @@ -674,20 +862,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() { @@ -748,7 +938,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 @@ -800,12 +990,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, @@ -840,17 +1032,11 @@ impl EditorElement { } }); - if let Some((position, context_menu)) = layout.context_menu.as_mut() { - cx.with_z_index(1, |cx| { - let line_height = self.style.text.line_height_in_pixels(cx.rem_size()); - let available_space = size( - AvailableSpace::MinContent, - AvailableSpace::Definite( - (12. * line_height) - .min((text_bounds.size.height - line_height) / 2.), - ), - ); - let context_menu_size = context_menu.measure(available_space, editor, cx); + cx.with_z_index(1, |cx| { + if let Some((position, mut context_menu)) = layout.context_menu.take() { + let available_space = + size(AvailableSpace::MinContent, AvailableSpace::MinContent); + 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] @@ -871,84 +1057,77 @@ impl EditorElement { } if list_origin.y + list_height > text_bounds.lower_right().y { - list_origin.y -= layout.position_map.line_height - list_height; + list_origin.y -= layout.position_map.line_height + list_height; } - context_menu.draw(list_origin, available_space, editor, cx); - }) - } + cx.break_content_mask(|cx| { + context_menu.draw(list_origin, available_space, cx) + }); + } - // if let Some((position, hover_popovers)) = layout.hover_popovers.as_mut() { - // cx.scene().push_stacking_context(None, None); + if let Some((position, mut hover_popovers)) = layout.hover_popovers.take() { + let available_space = + size(AvailableSpace::MinContent, AvailableSpace::MinContent); - // // This is safe because we check on layout whether the required row is available - // let hovered_row_layout = - // &layout.position_map.line_layouts[(position.row() - start_row) as usize].line; + // This is safe because we check on layout whether the required row is available + let hovered_row_layout = &layout.position_map.line_layouts + [(position.row() - start_row) as usize] + .line; - // // Minimum required size: Take the first popover, and add 1.5 times the minimum popover - // // height. This is the size we will use to decide whether to render popovers above or below - // // the hovered line. - // let first_size = hover_popovers[0].size(); - // let height_to_reserve = first_size.y - // + 1.5 * MIN_POPOVER_LINE_HEIGHT as f32 * layout.position_map.line_height; + // Minimum required size: Take the first popover, and add 1.5 times the minimum popover + // height. This is the size we will use to decide whether to render popovers above or below + // the hovered line. + let first_size = hover_popovers[0].measure(available_space, cx); + let height_to_reserve = first_size.height + + 1.5 * MIN_POPOVER_LINE_HEIGHT * layout.position_map.line_height; - // // Compute Hovered Point - // let x = hovered_row_layout.x_for_index(position.column() as usize) - scroll_left; - // let y = position.row() as f32 * layout.position_map.line_height - scroll_top; - // let hovered_point = content_origin + point(x, y); + // Compute Hovered Point + let x = hovered_row_layout.x_for_index(position.column() as usize) + - layout.position_map.scroll_position.x; + let y = position.row() as f32 * layout.position_map.line_height + - layout.position_map.scroll_position.y; + let hovered_point = content_origin + point(x, y); - // if hovered_point.y - height_to_reserve > 0.0 { - // // There is enough space above. Render popovers above the hovered point - // let mut current_y = hovered_point.y; - // for hover_popover in hover_popovers { - // let size = hover_popover.size(); - // let mut popover_origin = point(hovered_point.x, current_y - size.y); + if hovered_point.y - height_to_reserve > Pixels::ZERO { + // There is enough space above. Render popovers above the hovered point + let mut current_y = hovered_point.y; + for mut hover_popover in hover_popovers { + let size = hover_popover.measure(available_space, cx); + let mut popover_origin = + point(hovered_point.x, current_y - size.height); - // let x_out_of_bounds = bounds.max_x - (popover_origin.x + size.x); - // if x_out_of_bounds < 0.0 { - // popover_origin.set_x(popover_origin.x + x_out_of_bounds); - // } + let x_out_of_bounds = + text_bounds.upper_right().x - (popover_origin.x + size.width); + if x_out_of_bounds < Pixels::ZERO { + popover_origin.x = popover_origin.x + x_out_of_bounds; + } - // hover_popover.paint( - // popover_origin, - // Bounds::::from_points( - // gpui::Point::::zero(), - // point(f32::MAX, f32::MAX), - // ), // Let content bleed outside of editor - // editor, - // cx, - // ); + cx.break_content_mask(|cx| { + hover_popover.draw(popover_origin, available_space, cx) + }); - // current_y = popover_origin.y - HOVER_POPOVER_GAP; - // } - // } else { - // // There is not enough space above. Render popovers below the hovered point - // let mut current_y = hovered_point.y + layout.position_map.line_height; - // for hover_popover in hover_popovers { - // let size = hover_popover.size(); - // let mut popover_origin = point(hovered_point.x, current_y); + current_y = popover_origin.y - HOVER_POPOVER_GAP; + } + } else { + // There is not enough space above. Render popovers below the hovered point + let mut current_y = hovered_point.y + layout.position_map.line_height; + for mut hover_popover in hover_popovers { + let size = hover_popover.measure(available_space, cx); + let mut popover_origin = point(hovered_point.x, current_y); - // let x_out_of_bounds = bounds.max_x - (popover_origin.x + size.x); - // if x_out_of_bounds < 0.0 { - // popover_origin.set_x(popover_origin.x + x_out_of_bounds); - // } + let x_out_of_bounds = + text_bounds.upper_right().x - (popover_origin.x + size.width); + if x_out_of_bounds < Pixels::ZERO { + popover_origin.x = popover_origin.x + x_out_of_bounds; + } - // hover_popover.paint( - // popover_origin, - // Bounds::::from_points( - // gpui::Point::::zero(), - // point(f32::MAX, f32::MAX), - // ), // Let content bleed outside of editor - // editor, - // cx, - // ); + hover_popover.draw(popover_origin, available_space, cx); - // current_y = popover_origin.y + size.y + HOVER_POPOVER_GAP; - // } - // } - - // cx.scene().pop_stacking_context(); - // } + current_y = popover_origin.y + size.height + HOVER_POPOVER_GAP; + } + } + } + }) }, ) } @@ -1165,7 +1344,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; @@ -1217,14 +1396,13 @@ 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; let scroll_top = scroll_position.y * layout.position_map.line_height; - for block in &mut layout.blocks { + for block in layout.blocks.drain(..) { let mut origin = bounds.origin + point( Pixels::ZERO, @@ -1233,13 +1411,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 @@ -1260,7 +1436,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) } @@ -1415,7 +1591,7 @@ impl EditorElement { } fn layout_lines( - &mut self, + &self, rows: Range, line_number_layouts: &[Option], snapshot: &EditorSnapshot, @@ -1471,483 +1647,457 @@ 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"); - // } + 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 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 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; + 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), + 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; }; - if editor.set_wrap_width(Some(wrap_width), cx) { - editor.snapshot(cx) + 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 + } + }; + + 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 mut selections: Vec<(PlayerColor, Vec)> = Vec::new(); + let mut active_rows = BTreeMap::new(); + let is_singleton = editor.is_singleton(cx); - 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 highlighted_rows = editor.highlighted_rows(); + let highlighted_ranges = editor.background_highlights_in_range( + start_anchor..end_anchor, + &snapshot.display_snapshot, + cx.theme().colors(), + ); - let autoscroll_horizontally = - editor.autoscroll_vertically(bounds.size.height, line_height, cx); - let mut snapshot = editor.snapshot(cx); + let mut newest_selection_head = None; - 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(); + 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; - // 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 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 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 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); - } - - 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)); - } - - 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) + for row in cmp::max(layout.active_rows.start, start_row) + ..=cmp::min(layout.active_rows.end, end_row) { - if let Some((local_selection_style, _)) = selections.first_mut() { - *local_selection_style = cx - .theme() - .players() - .color_for_participant(participant_index.0); + 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)); + } + + 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() - }; + 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; + // Don't re-render the leader's selections, since the local selections + // match theirs. + if Some(selection.peer_id) == editor.leader_peer_id { + continue; + } + + 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, + )); } - 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, - )); + selections.extend(remote_selections.into_values()); } - selections.extend(remote_selections.into_values()); - } + 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 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 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 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 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 (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 - }; - - if clamped || autoscrolled { - snapshot = editor.snapshot(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); + 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 active = matches!( - editor.context_menu.read().as_ref(), - Some(crate::ContextMenu::CodeActions(_)) - ); - - code_actions_indicator = editor - .render_code_actions_indicator(&style, active, cx) - .map(|element| CodeActionsIndicator { - row: newest_selection_head.row(), - element, - }); } - } - 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 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 fold_indicators = cx.with_element_id(Some("gutter_fold_indicators"), |cx| { - editor.render_fold_indicators( - fold_statuses, + 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 scroll_max = point( + f32::from((scroll_width - text_size.width) / em_width).max(0.0), + max_row as f32, + ); + + 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 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() { + let max_height = (12. * line_height).min((bounds.size.height - line_height) / 2.); + context_menu = + editor.render_context_menu(newest_selection_head, &self.style, max_height, cx); + } + + let active = matches!( + editor.context_menu.read().as_ref(), + Some(crate::ContextMenu::CodeActions(_)) + ); + + code_actions_indicator = editor + .render_code_actions_indicator(&style, active, cx) + .map(|element| CodeActionsIndicator { + row: newest_selection_head.row(), + button: element, + }); + } + } + + let visible_rows = start_row..start_row + line_layouts.len() as u32; + let max_size = size( + (120. * em_width) // Default size + .min(bounds.size.width / 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(bounds.size.height / 2.) // Shrink to half of the editor height + .max(MIN_POPOVER_LINE_HEIGHT * line_height), // Apply minimum height of 4 lines + ); + + let mut hover = editor.hover_state.render( + &snapshot, &style, - editor.gutter_hovered, - line_height, - gutter_margin, + visible_rows, + max_size, + editor.workspace.as_ref().map(|(w, _)| w.clone()), 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 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, + ) + }); - // 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(); - 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, - } + 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, @@ -2028,12 +2178,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 { @@ -2043,15 +2191,20 @@ impl EditorElement { // Can't use .and_then() because `.file_name()` and `.parent()` return references :( if let Some(path) = path { filename = path.file_name().map(|f| f.to_string_lossy().to_string()); - parent_path = - path.parent().map(|p| p.to_string_lossy().to_string() + "/"); + parent_path = path + .parent() + .map(|p| SharedString::from(p.to_string_lossy().to_string() + "/")); } h_stack() .id("path header block") .size_full() .bg(gpui::red()) - .child(filename.unwrap_or_else(|| "untitled".to_string())) + .child( + filename + .map(SharedString::from) + .unwrap_or_else(|| "untitled".into()), + ) .children(parent_path) .children(jump_icon) // .p_x(gutter_padding) } else { @@ -2063,11 +2216,11 @@ impl EditorElement { .child("⋯") .children(jump_icon) // .p_x(gutter_padding) }; - element.render() + element.into_any() } }; - let size = element.measure(available_space, editor, cx); + let size = element.measure(available_space, cx); (element, size) }; @@ -2126,47 +2279,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| { @@ -2183,12 +2350,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() } } @@ -2267,7 +2439,7 @@ impl LineWithInvisibles { len: line_chunk.len(), font: text_style.font(), color: text_style.color, - background_color: None, + background_color: text_style.background_color, underline: text_style.underline, }); @@ -2317,7 +2489,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; @@ -2349,7 +2521,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, @@ -2392,41 +2564,42 @@ enum Invisible { Whitespace { line_offset: usize }, } -impl Element for EditorElement { - type ElementState = (); - - fn element_id(&self) -> Option { - Some(self.editor_id.into()) - } +impl Element for EditorElement { + type State = (); fn layout( &mut self, - editor: &mut Editor, - element_state: Option, - cx: &mut gpui::ViewContext, - ) -> (gpui::LayoutId, Self::ElementState) { - editor.style = Some(self.style.clone()); // Long-term, we'd like to eliminate this. + element_state: Option, + cx: &mut gpui::WindowContext, + ) -> (gpui::LayoutId, Self::State) { + 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, ()) + 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, + mut self, bounds: Bounds, - editor: &mut Editor, - element_state: &mut Self::ElementState, - cx: &mut gpui::ViewContext, + element_state: &mut Self::State, + 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, @@ -2436,43 +2609,46 @@ 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); + 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, cx); - cx.handle_input(&editor.focus_handle, input_handler); + // 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, editor, cx); - } - self.paint_text(text_bounds, &mut layout, editor, cx); + 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 Component for EditorElement { - fn render(self) -> AnyElement { - AnyElement::new(self) +impl IntoElement for EditorElement { + type Element = Self; + + fn element_id(&self) -> Option { + self.editor.element_id() + } + + fn into_element(self) -> Self::Element { + self } } @@ -3097,17 +3273,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, - element: AnyElement, + button: IconButton, } struct PositionMap { @@ -3192,7 +3368,7 @@ impl PositionMap { struct BlockLayout { row: u32, - element: AnyElement, + element: AnyElement, available_space: Size, style: BlockStyle, } @@ -3897,187 +4073,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, +pub fn register_action( + 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 5c8f403d4f..37c7df650b 100644 --- a/crates/editor2/src/hover_popover.rs +++ b/crates/editor2/src/hover_popover.rs @@ -1,15 +1,21 @@ use crate::{ - display_map::InlayOffset, + display_map::{InlayOffset, ToDisplayPoint}, link_go_to_definition::{InlayHighlight, RangeInEditor}, Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSettings, EditorSnapshot, EditorStyle, ExcerptId, RangeToAnchorExt, }; use futures::FutureExt; -use gpui::{AnyElement, AppContext, Model, Task, ViewContext, WeakView}; +use gpui::{ + actions, div, px, AnyElement, AppContext, CursorStyle, InteractiveElement, IntoElement, Model, + MouseButton, ParentElement, Pixels, SharedString, Size, StatefulInteractiveElement, Styled, + Task, ViewContext, WeakView, +}; use language::{markdown, Bias, DiagnosticEntry, Language, LanguageRegistry, ParsedMarkdown}; +use lsp::DiagnosticSeverity; use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart, Project}; use settings::Settings; use std::{ops::Range, sync::Arc, time::Duration}; +use ui::Tooltip; use util::TryFutureExt; use workspace::Workspace; @@ -17,22 +23,17 @@ pub const HOVER_DELAY_MILLIS: u64 = 350; pub const HOVER_REQUEST_DELAY_MILLIS: u64 = 200; pub const MIN_POPOVER_CHARACTER_WIDTH: f32 = 20.; -pub const MIN_POPOVER_LINE_HEIGHT: f32 = 4.; -pub const HOVER_POPOVER_GAP: f32 = 10.; +pub const MIN_POPOVER_LINE_HEIGHT: Pixels = px(4.); +pub const HOVER_POPOVER_GAP: Pixels = px(10.); -// actions!(editor, [Hover]); +actions!(Hover); -pub fn init(cx: &mut AppContext) { - // cx.add_action(hover); +/// Bindable action which uses the most recent selection head to trigger a hover +pub fn hover(editor: &mut Editor, _: &Hover, cx: &mut ViewContext) { + let head = editor.selections.newest_display(cx).head(); + show_hover(editor, head, true, cx); } -// todo!() -// /// Bindable action which uses the most recent selection head to trigger a hover -// pub fn hover(editor: &mut Editor, _: &Hover, cx: &mut ViewContext) { -// let head = editor.selections.newest_display(cx).head(); -// show_hover(editor, head, true, cx); -// } - /// The internal hover action dispatches between `show_hover` or `hide_hover` /// depending on whether a point to hover over is provided. pub fn hover_at(editor: &mut Editor, point: Option, cx: &mut ViewContext) { @@ -74,64 +75,63 @@ pub fn find_hovered_hint_part( } pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut ViewContext) { - todo!() - // if EditorSettings::get_global(cx).hover_popover_enabled { - // if editor.pending_rename.is_some() { - // return; - // } + if EditorSettings::get_global(cx).hover_popover_enabled { + if editor.pending_rename.is_some() { + return; + } - // let Some(project) = editor.project.clone() else { - // return; - // }; + let Some(project) = editor.project.clone() else { + return; + }; - // if let Some(InfoPopover { symbol_range, .. }) = &editor.hover_state.info_popover { - // if let RangeInEditor::Inlay(range) = symbol_range { - // if range == &inlay_hover.range { - // // Hover triggered from same location as last time. Don't show again. - // return; - // } - // } - // hide_hover(editor, cx); - // } + if let Some(InfoPopover { symbol_range, .. }) = &editor.hover_state.info_popover { + if let RangeInEditor::Inlay(range) = symbol_range { + if range == &inlay_hover.range { + // Hover triggered from same location as last time. Don't show again. + return; + } + } + hide_hover(editor, cx); + } - // let task = cx.spawn(|this, mut cx| { - // async move { - // cx.background_executor() - // .timer(Duration::from_millis(HOVER_DELAY_MILLIS)) - // .await; - // this.update(&mut cx, |this, _| { - // this.hover_state.diagnostic_popover = None; - // })?; + let task = cx.spawn(|this, mut cx| { + async move { + cx.background_executor() + .timer(Duration::from_millis(HOVER_DELAY_MILLIS)) + .await; + this.update(&mut cx, |this, _| { + this.hover_state.diagnostic_popover = None; + })?; - // let language_registry = project.update(&mut cx, |p, _| p.languages().clone())?; - // let blocks = vec![inlay_hover.tooltip]; - // let parsed_content = parse_blocks(&blocks, &language_registry, None).await; + let language_registry = project.update(&mut cx, |p, _| p.languages().clone())?; + let blocks = vec![inlay_hover.tooltip]; + let parsed_content = parse_blocks(&blocks, &language_registry, None).await; - // let hover_popover = InfoPopover { - // project: project.clone(), - // symbol_range: RangeInEditor::Inlay(inlay_hover.range.clone()), - // blocks, - // parsed_content, - // }; + let hover_popover = InfoPopover { + project: project.clone(), + symbol_range: RangeInEditor::Inlay(inlay_hover.range.clone()), + blocks, + parsed_content, + }; - // this.update(&mut cx, |this, cx| { - // // Highlight the selected symbol using a background highlight - // this.highlight_inlay_background::( - // vec![inlay_hover.range], - // |theme| theme.editor.hover_popover.highlight, - // cx, - // ); - // this.hover_state.info_popover = Some(hover_popover); - // cx.notify(); - // })?; + this.update(&mut cx, |this, cx| { + // Highlight the selected symbol using a background highlight + this.highlight_inlay_background::( + vec![inlay_hover.range], + |theme| theme.element_hover, // todo!("use a proper background here") + cx, + ); + this.hover_state.info_popover = Some(hover_popover); + cx.notify(); + })?; - // anyhow::Ok(()) - // } - // .log_err() - // }); + anyhow::Ok(()) + } + .log_err() + }); - // editor.hover_state.info_task = Some(task); - // } + editor.hover_state.info_task = Some(task); + } } /// Hides the type information popup. @@ -420,43 +420,42 @@ impl HoverState { snapshot: &EditorSnapshot, style: &EditorStyle, visible_rows: Range, + max_size: Size, workspace: Option>, cx: &mut ViewContext, - ) -> Option<(DisplayPoint, Vec>)> { - todo!("old version below") + ) -> Option<(DisplayPoint, Vec)> { + // If there is a diagnostic, position the popovers based on that. + // Otherwise use the start of the hover range + let anchor = self + .diagnostic_popover + .as_ref() + .map(|diagnostic_popover| &diagnostic_popover.local_diagnostic.range.start) + .or_else(|| { + self.info_popover + .as_ref() + .map(|info_popover| match &info_popover.symbol_range { + RangeInEditor::Text(range) => &range.start, + RangeInEditor::Inlay(range) => &range.inlay_position, + }) + })?; + let point = anchor.to_display_point(&snapshot.display_snapshot); + + // Don't render if the relevant point isn't on screen + if !self.visible() || !visible_rows.contains(&point.row()) { + return None; + } + + let mut elements = Vec::new(); + + if let Some(diagnostic_popover) = self.diagnostic_popover.as_ref() { + elements.push(diagnostic_popover.render(style, max_size, cx)); + } + if let Some(info_popover) = self.info_popover.as_mut() { + elements.push(info_popover.render(style, max_size, workspace, cx)); + } + + Some((point, elements)) } - // // If there is a diagnostic, position the popovers based on that. - // // Otherwise use the start of the hover range - // let anchor = self - // .diagnostic_popover - // .as_ref() - // .map(|diagnostic_popover| &diagnostic_popover.local_diagnostic.range.start) - // .or_else(|| { - // self.info_popover - // .as_ref() - // .map(|info_popover| match &info_popover.symbol_range { - // RangeInEditor::Text(range) => &range.start, - // RangeInEditor::Inlay(range) => &range.inlay_position, - // }) - // })?; - // let point = anchor.to_display_point(&snapshot.display_snapshot); - - // // Don't render if the relevant point isn't on screen - // if !self.visible() || !visible_rows.contains(&point.row()) { - // return None; - // } - - // let mut elements = Vec::new(); - - // if let Some(diagnostic_popover) = self.diagnostic_popover.as_ref() { - // elements.push(diagnostic_popover.render(style, cx)); - // } - // if let Some(info_popover) = self.info_popover.as_mut() { - // elements.push(info_popover.render(style, workspace, cx)); - // } - - // Some((point, elements)) - // } } #[derive(Debug, Clone)] @@ -467,35 +466,36 @@ pub struct InfoPopover { parsed_content: ParsedMarkdown, } -// impl InfoPopover { -// pub fn render( -// &mut self, -// style: &EditorStyle, -// workspace: Option>, -// cx: &mut ViewContext, -// ) -> AnyElement { -// MouseEventHandler::new::(0, cx, |_, cx| { -// Flex::column() -// .scrollable::(0, None, cx) -// .with_child(crate::render_parsed_markdown::( -// &self.parsed_content, -// style, -// workspace, -// cx, -// )) -// .contained() -// .with_style(style.hover_popover.container) -// }) -// .on_move(|_, _, _| {}) // Consume move events so they don't reach regions underneath. -// .with_cursor_style(CursorStyle::Arrow) -// .with_padding(Padding { -// bottom: HOVER_POPOVER_GAP, -// top: HOVER_POPOVER_GAP, -// ..Default::default() -// }) -// .into_any() -// } -// } +impl InfoPopover { + pub fn render( + &mut self, + style: &EditorStyle, + max_size: Size, + workspace: Option>, + cx: &mut ViewContext, + ) -> AnyElement { + div() + .id("info_popover") + .overflow_y_scroll() + .bg(gpui::red()) + .max_w(max_size.width) + .max_h(max_size.height) + // Prevent a mouse move on the popover from being propagated to the editor, + // because that would dismiss the popover. + .on_mouse_move(|_, cx| cx.stop_propagation()) + // Prevent a mouse down on the popover from being propagated to the editor, + // because that would move the cursor. + .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation()) + .child(crate::render_parsed_markdown( + "content", + &self.parsed_content, + style, + workspace, + cx, + )) + .into_any_element() + } +} #[derive(Debug, Clone)] pub struct DiagnosticPopover { @@ -504,57 +504,40 @@ pub struct DiagnosticPopover { } impl DiagnosticPopover { - pub fn render(&self, style: &EditorStyle, cx: &mut ViewContext) -> AnyElement { - todo!() - // enum PrimaryDiagnostic {} + pub fn render( + &self, + style: &EditorStyle, + max_size: Size, + cx: &mut ViewContext, + ) -> AnyElement { + let text = match &self.local_diagnostic.diagnostic.source { + Some(source) => format!("{source}: {}", self.local_diagnostic.diagnostic.message), + None => self.local_diagnostic.diagnostic.message.clone(), + }; - // let mut text_style = style.hover_popover.prose.clone(); - // text_style.font_size = style.text.font_size; - // let diagnostic_source_style = style.hover_popover.diagnostic_source_highlight.clone(); + let container_bg = crate::diagnostic_style( + self.local_diagnostic.diagnostic.severity, + true, + &style.diagnostic_style, + ); - // let text = match &self.local_diagnostic.diagnostic.source { - // Some(source) => Text::new( - // format!("{source}: {}", self.local_diagnostic.diagnostic.message), - // text_style, - // ) - // .with_highlights(vec![(0..source.len(), diagnostic_source_style)]), - - // None => Text::new(self.local_diagnostic.diagnostic.message.clone(), text_style), - // }; - - // let container_style = match self.local_diagnostic.diagnostic.severity { - // DiagnosticSeverity::HINT => style.hover_popover.info_container, - // DiagnosticSeverity::INFORMATION => style.hover_popover.info_container, - // DiagnosticSeverity::WARNING => style.hover_popover.warning_container, - // DiagnosticSeverity::ERROR => style.hover_popover.error_container, - // _ => style.hover_popover.container, - // }; - - // let tooltip_style = theme::current(cx).tooltip.clone(); - - // MouseEventHandler::new::(0, cx, |_, _| { - // text.with_soft_wrap(true) - // .contained() - // .with_style(container_style) - // }) - // .with_padding(Padding { - // top: HOVER_POPOVER_GAP, - // bottom: HOVER_POPOVER_GAP, - // ..Default::default() - // }) - // .on_move(|_, _, _| {}) // Consume move events so they don't reach regions underneath. - // .on_click(MouseButton::Left, |_, this, cx| { - // this.go_to_diagnostic(&Default::default(), cx) - // }) - // .with_cursor_style(CursorStyle::PointingHand) - // .with_tooltip::( - // 0, - // "Go To Diagnostic".to_string(), - // Some(Box::new(crate::GoToDiagnostic)), - // tooltip_style, - // cx, - // ) - // .into_any() + div() + .id("diagnostic") + .overflow_y_scroll() + .bg(container_bg) + .max_w(max_size.width) + .max_h(max_size.height) + .cursor(CursorStyle::PointingHand) + .tooltip(move |cx| Tooltip::for_action("Go To Diagnostic", &crate::GoToDiagnostic, cx)) + // Prevent a mouse move on the popover from being propagated to the editor, + // because that would dismiss the popover. + .on_mouse_move(|_, cx| cx.stop_propagation()) + // Prevent a mouse down on the popover from being propagated to the editor, + // because that would move the cursor. + .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation()) + .on_click(cx.listener(|editor, _, cx| editor.go_to_diagnostic(&Default::default(), cx))) + .child(SharedString::from(text)) + .into_any_element() } pub fn activation_info(&self) -> (usize, Anchor) { @@ -567,763 +550,763 @@ impl DiagnosticPopover { } } -// #[cfg(test)] -// mod tests { -// use super::*; -// use crate::{ -// editor_tests::init_test, -// element::PointForPosition, -// inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels}, -// link_go_to_definition::update_inlay_link_and_hover_points, -// test::editor_lsp_test_context::EditorLspTestContext, -// InlayId, -// }; -// use collections::BTreeSet; -// use gpui::fonts::{HighlightStyle, Underline, Weight}; -// use indoc::indoc; -// use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet}; -// use lsp::LanguageServerId; -// use project::{HoverBlock, HoverBlockKind}; -// use smol::stream::StreamExt; -// use unindent::Unindent; -// use util::test::marked_text_ranges; - -// #[gpui::test] -// async fn test_mouse_hover_info_popover(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); - -// let mut cx = EditorLspTestContext::new_rust( -// lsp::ServerCapabilities { -// hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), -// ..Default::default() -// }, -// cx, -// ) -// .await; - -// // Basic hover delays and then pops without moving the mouse -// cx.set_state(indoc! {" -// fn ˇtest() { println!(); } -// "}); -// let hover_point = cx.display_point(indoc! {" -// fn test() { printˇln!(); } -// "}); - -// cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx)); -// assert!(!cx.editor(|editor, _| editor.hover_state.visible())); - -// // After delay, hover should be visible. -// let symbol_range = cx.lsp_range(indoc! {" -// fn test() { «println!»(); } -// "}); -// let mut requests = -// cx.handle_request::(move |_, _, _| async move { -// Ok(Some(lsp::Hover { -// contents: lsp::HoverContents::Markup(lsp::MarkupContent { -// kind: lsp::MarkupKind::Markdown, -// value: "some basic docs".to_string(), -// }), -// range: Some(symbol_range), -// })) -// }); -// cx.foreground() -// .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); -// requests.next().await; - -// cx.editor(|editor, _| { -// assert!(editor.hover_state.visible()); -// assert_eq!( -// editor.hover_state.info_popover.clone().unwrap().blocks, -// vec![HoverBlock { -// text: "some basic docs".to_string(), -// kind: HoverBlockKind::Markdown, -// },] -// ) -// }); - -// // Mouse moved with no hover response dismisses -// let hover_point = cx.display_point(indoc! {" -// fn teˇst() { println!(); } -// "}); -// let mut request = cx -// .lsp -// .handle_request::(|_, _| async move { Ok(None) }); -// cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx)); -// cx.foreground() -// .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); -// request.next().await; -// cx.editor(|editor, _| { -// assert!(!editor.hover_state.visible()); -// }); -// } - -// #[gpui::test] -// async fn test_keyboard_hover_info_popover(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); - -// let mut cx = EditorLspTestContext::new_rust( -// lsp::ServerCapabilities { -// hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), -// ..Default::default() -// }, -// cx, -// ) -// .await; - -// // Hover with keyboard has no delay -// cx.set_state(indoc! {" -// fˇn test() { println!(); } -// "}); -// cx.update_editor(|editor, cx| hover(editor, &Hover, cx)); -// let symbol_range = cx.lsp_range(indoc! {" -// «fn» test() { println!(); } -// "}); -// cx.handle_request::(move |_, _, _| async move { -// Ok(Some(lsp::Hover { -// contents: lsp::HoverContents::Markup(lsp::MarkupContent { -// kind: lsp::MarkupKind::Markdown, -// value: "some other basic docs".to_string(), -// }), -// range: Some(symbol_range), -// })) -// }) -// .next() -// .await; - -// cx.condition(|editor, _| editor.hover_state.visible()).await; -// cx.editor(|editor, _| { -// assert_eq!( -// editor.hover_state.info_popover.clone().unwrap().blocks, -// vec![HoverBlock { -// text: "some other basic docs".to_string(), -// kind: HoverBlockKind::Markdown, -// }] -// ) -// }); -// } - -// #[gpui::test] -// async fn test_empty_hovers_filtered(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); - -// let mut cx = EditorLspTestContext::new_rust( -// lsp::ServerCapabilities { -// hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), -// ..Default::default() -// }, -// cx, -// ) -// .await; - -// // Hover with keyboard has no delay -// cx.set_state(indoc! {" -// fˇn test() { println!(); } -// "}); -// cx.update_editor(|editor, cx| hover(editor, &Hover, cx)); -// let symbol_range = cx.lsp_range(indoc! {" -// «fn» test() { println!(); } -// "}); -// cx.handle_request::(move |_, _, _| async move { -// Ok(Some(lsp::Hover { -// contents: lsp::HoverContents::Array(vec![ -// lsp::MarkedString::String("regular text for hover to show".to_string()), -// lsp::MarkedString::String("".to_string()), -// lsp::MarkedString::LanguageString(lsp::LanguageString { -// language: "Rust".to_string(), -// value: "".to_string(), -// }), -// ]), -// range: Some(symbol_range), -// })) -// }) -// .next() -// .await; - -// cx.condition(|editor, _| editor.hover_state.visible()).await; -// cx.editor(|editor, _| { -// assert_eq!( -// editor.hover_state.info_popover.clone().unwrap().blocks, -// vec![HoverBlock { -// text: "regular text for hover to show".to_string(), -// kind: HoverBlockKind::Markdown, -// }], -// "No empty string hovers should be shown" -// ); -// }); -// } - -// #[gpui::test] -// async fn test_line_ends_trimmed(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); - -// let mut cx = EditorLspTestContext::new_rust( -// lsp::ServerCapabilities { -// hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), -// ..Default::default() -// }, -// cx, -// ) -// .await; - -// // Hover with keyboard has no delay -// cx.set_state(indoc! {" -// fˇn test() { println!(); } -// "}); -// cx.update_editor(|editor, cx| hover(editor, &Hover, cx)); -// let symbol_range = cx.lsp_range(indoc! {" -// «fn» test() { println!(); } -// "}); - -// let code_str = "\nlet hovered_point: Vector2F // size = 8, align = 0x4\n"; -// let markdown_string = format!("\n```rust\n{code_str}```"); - -// let closure_markdown_string = markdown_string.clone(); -// cx.handle_request::(move |_, _, _| { -// let future_markdown_string = closure_markdown_string.clone(); -// async move { -// Ok(Some(lsp::Hover { -// contents: lsp::HoverContents::Markup(lsp::MarkupContent { -// kind: lsp::MarkupKind::Markdown, -// value: future_markdown_string, -// }), -// range: Some(symbol_range), -// })) -// } -// }) -// .next() -// .await; - -// cx.condition(|editor, _| editor.hover_state.visible()).await; -// cx.editor(|editor, _| { -// let blocks = editor.hover_state.info_popover.clone().unwrap().blocks; -// assert_eq!( -// blocks, -// vec![HoverBlock { -// text: markdown_string, -// kind: HoverBlockKind::Markdown, -// }], -// ); - -// let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None)); -// assert_eq!( -// rendered.text, -// code_str.trim(), -// "Should not have extra line breaks at end of rendered hover" -// ); -// }); -// } - -// #[gpui::test] -// async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); - -// let mut cx = EditorLspTestContext::new_rust( -// lsp::ServerCapabilities { -// hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), -// ..Default::default() -// }, -// cx, -// ) -// .await; - -// // Hover with just diagnostic, pops DiagnosticPopover immediately and then -// // info popover once request completes -// cx.set_state(indoc! {" -// fn teˇst() { println!(); } -// "}); - -// // Send diagnostic to client -// let range = cx.text_anchor_range(indoc! {" -// fn «test»() { println!(); } -// "}); -// cx.update_buffer(|buffer, cx| { -// let snapshot = buffer.text_snapshot(); -// let set = DiagnosticSet::from_sorted_entries( -// vec![DiagnosticEntry { -// range, -// diagnostic: Diagnostic { -// message: "A test diagnostic message.".to_string(), -// ..Default::default() -// }, -// }], -// &snapshot, -// ); -// buffer.update_diagnostics(LanguageServerId(0), set, cx); -// }); - -// // Hover pops diagnostic immediately -// cx.update_editor(|editor, cx| hover(editor, &Hover, cx)); -// cx.foreground().run_until_parked(); - -// cx.editor(|Editor { hover_state, .. }, _| { -// assert!(hover_state.diagnostic_popover.is_some() && hover_state.info_popover.is_none()) -// }); - -// // Info Popover shows after request responded to -// let range = cx.lsp_range(indoc! {" -// fn «test»() { println!(); } -// "}); -// cx.handle_request::(move |_, _, _| async move { -// Ok(Some(lsp::Hover { -// contents: lsp::HoverContents::Markup(lsp::MarkupContent { -// kind: lsp::MarkupKind::Markdown, -// value: "some new docs".to_string(), -// }), -// range: Some(range), -// })) -// }); -// cx.foreground() -// .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); - -// cx.foreground().run_until_parked(); -// cx.editor(|Editor { hover_state, .. }, _| { -// hover_state.diagnostic_popover.is_some() && hover_state.info_task.is_some() -// }); -// } - -// #[gpui::test] -// fn test_render_blocks(cx: &mut gpui::TestAppContext) { -// init_test(cx, |_| {}); - -// cx.add_window(|cx| { -// let editor = Editor::single_line(None, cx); -// let style = editor.style(cx); - -// struct Row { -// blocks: Vec, -// expected_marked_text: String, -// expected_styles: Vec, -// } - -// let rows = &[ -// // Strong emphasis -// Row { -// blocks: vec![HoverBlock { -// text: "one **two** three".to_string(), -// kind: HoverBlockKind::Markdown, -// }], -// expected_marked_text: "one «two» three".to_string(), -// expected_styles: vec![HighlightStyle { -// weight: Some(Weight::BOLD), -// ..Default::default() -// }], -// }, -// // Links -// Row { -// blocks: vec![HoverBlock { -// text: "one [two](https://the-url) three".to_string(), -// kind: HoverBlockKind::Markdown, -// }], -// expected_marked_text: "one «two» three".to_string(), -// expected_styles: vec![HighlightStyle { -// underline: Some(Underline { -// thickness: 1.0.into(), -// ..Default::default() -// }), -// ..Default::default() -// }], -// }, -// // Lists -// Row { -// blocks: vec![HoverBlock { -// text: " -// lists: -// * one -// - a -// - b -// * two -// - [c](https://the-url) -// - d" -// .unindent(), -// kind: HoverBlockKind::Markdown, -// }], -// expected_marked_text: " -// lists: -// - one -// - a -// - b -// - two -// - «c» -// - d" -// .unindent(), -// expected_styles: vec![HighlightStyle { -// underline: Some(Underline { -// thickness: 1.0.into(), -// ..Default::default() -// }), -// ..Default::default() -// }], -// }, -// // Multi-paragraph list items -// Row { -// blocks: vec![HoverBlock { -// text: " -// * one two -// three - -// * four five -// * six seven -// eight - -// nine -// * ten -// * six" -// .unindent(), -// kind: HoverBlockKind::Markdown, -// }], -// expected_marked_text: " -// - one two three -// - four five -// - six seven eight - -// nine -// - ten -// - six" -// .unindent(), -// expected_styles: vec![HighlightStyle { -// underline: Some(Underline { -// thickness: 1.0.into(), -// ..Default::default() -// }), -// ..Default::default() -// }], -// }, -// ]; - -// for Row { -// blocks, -// expected_marked_text, -// expected_styles, -// } in &rows[0..] -// { -// let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None)); - -// let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false); -// let expected_highlights = ranges -// .into_iter() -// .zip(expected_styles.iter().cloned()) -// .collect::>(); -// assert_eq!( -// rendered.text, expected_text, -// "wrong text for input {blocks:?}" -// ); - -// let rendered_highlights: Vec<_> = rendered -// .highlights -// .iter() -// .filter_map(|(range, highlight)| { -// let highlight = highlight.to_highlight_style(&style.syntax)?; -// Some((range.clone(), highlight)) -// }) -// .collect(); - -// assert_eq!( -// rendered_highlights, expected_highlights, -// "wrong highlights for input {blocks:?}" -// ); -// } - -// editor -// }); -// } - -// #[gpui::test] -// async fn test_hover_inlay_label_parts(cx: &mut gpui::TestAppContext) { -// init_test(cx, |settings| { -// settings.defaults.inlay_hints = Some(InlayHintSettings { -// enabled: true, -// show_type_hints: true, -// show_parameter_hints: true, -// show_other_hints: true, -// }) -// }); - -// let mut cx = EditorLspTestContext::new_rust( -// lsp::ServerCapabilities { -// inlay_hint_provider: Some(lsp::OneOf::Right( -// lsp::InlayHintServerCapabilities::Options(lsp::InlayHintOptions { -// resolve_provider: Some(true), -// ..Default::default() -// }), -// )), -// ..Default::default() -// }, -// cx, -// ) -// .await; - -// cx.set_state(indoc! {" -// struct TestStruct; - -// // ================== - -// struct TestNewType(T); - -// fn main() { -// let variableˇ = TestNewType(TestStruct); -// } -// "}); - -// let hint_start_offset = cx.ranges(indoc! {" -// struct TestStruct; - -// // ================== - -// struct TestNewType(T); - -// fn main() { -// let variableˇ = TestNewType(TestStruct); -// } -// "})[0] -// .start; -// let hint_position = cx.to_lsp(hint_start_offset); -// let new_type_target_range = cx.lsp_range(indoc! {" -// struct TestStruct; - -// // ================== - -// struct «TestNewType»(T); - -// fn main() { -// let variable = TestNewType(TestStruct); -// } -// "}); -// let struct_target_range = cx.lsp_range(indoc! {" -// struct «TestStruct»; - -// // ================== - -// struct TestNewType(T); - -// fn main() { -// let variable = TestNewType(TestStruct); -// } -// "}); - -// let uri = cx.buffer_lsp_url.clone(); -// let new_type_label = "TestNewType"; -// let struct_label = "TestStruct"; -// let entire_hint_label = ": TestNewType"; -// let closure_uri = uri.clone(); -// cx.lsp -// .handle_request::(move |params, _| { -// let task_uri = closure_uri.clone(); -// async move { -// assert_eq!(params.text_document.uri, task_uri); -// Ok(Some(vec![lsp::InlayHint { -// position: hint_position, -// label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart { -// value: entire_hint_label.to_string(), -// ..Default::default() -// }]), -// kind: Some(lsp::InlayHintKind::TYPE), -// text_edits: None, -// tooltip: None, -// padding_left: Some(false), -// padding_right: Some(false), -// data: None, -// }])) -// } -// }) -// .next() -// .await; -// cx.foreground().run_until_parked(); -// cx.update_editor(|editor, cx| { -// let expected_layers = vec![entire_hint_label.to_string()]; -// assert_eq!(expected_layers, cached_hint_labels(editor)); -// assert_eq!(expected_layers, visible_hint_labels(editor, cx)); -// }); - -// let inlay_range = cx -// .ranges(indoc! {" -// struct TestStruct; - -// // ================== - -// struct TestNewType(T); - -// fn main() { -// let variable« »= TestNewType(TestStruct); -// } -// "}) -// .get(0) -// .cloned() -// .unwrap(); -// let new_type_hint_part_hover_position = cx.update_editor(|editor, cx| { -// let snapshot = editor.snapshot(cx); -// let previous_valid = inlay_range.start.to_display_point(&snapshot); -// let next_valid = inlay_range.end.to_display_point(&snapshot); -// assert_eq!(previous_valid.row(), next_valid.row()); -// assert!(previous_valid.column() < next_valid.column()); -// let exact_unclipped = DisplayPoint::new( -// previous_valid.row(), -// previous_valid.column() -// + (entire_hint_label.find(new_type_label).unwrap() + new_type_label.len() / 2) -// as u32, -// ); -// PointForPosition { -// previous_valid, -// next_valid, -// exact_unclipped, -// column_overshoot_after_line_end: 0, -// } -// }); -// cx.update_editor(|editor, cx| { -// update_inlay_link_and_hover_points( -// &editor.snapshot(cx), -// new_type_hint_part_hover_position, -// editor, -// true, -// false, -// cx, -// ); -// }); - -// let resolve_closure_uri = uri.clone(); -// cx.lsp -// .handle_request::( -// move |mut hint_to_resolve, _| { -// let mut resolved_hint_positions = BTreeSet::new(); -// let task_uri = resolve_closure_uri.clone(); -// async move { -// let inserted = resolved_hint_positions.insert(hint_to_resolve.position); -// assert!(inserted, "Hint {hint_to_resolve:?} was resolved twice"); - -// // `: TestNewType` -// hint_to_resolve.label = lsp::InlayHintLabel::LabelParts(vec![ -// lsp::InlayHintLabelPart { -// value: ": ".to_string(), -// ..Default::default() -// }, -// lsp::InlayHintLabelPart { -// value: new_type_label.to_string(), -// location: Some(lsp::Location { -// uri: task_uri.clone(), -// range: new_type_target_range, -// }), -// tooltip: Some(lsp::InlayHintLabelPartTooltip::String(format!( -// "A tooltip for `{new_type_label}`" -// ))), -// ..Default::default() -// }, -// lsp::InlayHintLabelPart { -// value: "<".to_string(), -// ..Default::default() -// }, -// lsp::InlayHintLabelPart { -// value: struct_label.to_string(), -// location: Some(lsp::Location { -// uri: task_uri, -// range: struct_target_range, -// }), -// tooltip: Some(lsp::InlayHintLabelPartTooltip::MarkupContent( -// lsp::MarkupContent { -// kind: lsp::MarkupKind::Markdown, -// value: format!("A tooltip for `{struct_label}`"), -// }, -// )), -// ..Default::default() -// }, -// lsp::InlayHintLabelPart { -// value: ">".to_string(), -// ..Default::default() -// }, -// ]); - -// Ok(hint_to_resolve) -// } -// }, -// ) -// .next() -// .await; -// cx.foreground().run_until_parked(); - -// cx.update_editor(|editor, cx| { -// update_inlay_link_and_hover_points( -// &editor.snapshot(cx), -// new_type_hint_part_hover_position, -// editor, -// true, -// false, -// cx, -// ); -// }); -// cx.foreground() -// .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); -// cx.foreground().run_until_parked(); -// cx.update_editor(|editor, cx| { -// let hover_state = &editor.hover_state; -// assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some()); -// let popover = hover_state.info_popover.as_ref().unwrap(); -// let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx)); -// assert_eq!( -// popover.symbol_range, -// RangeInEditor::Inlay(InlayHighlight { -// inlay: InlayId::Hint(0), -// inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right), -// range: ": ".len()..": ".len() + new_type_label.len(), -// }), -// "Popover range should match the new type label part" -// ); -// assert_eq!( -// popover.parsed_content.text, -// format!("A tooltip for `{new_type_label}`"), -// "Rendered text should not anyhow alter backticks" -// ); -// }); - -// let struct_hint_part_hover_position = cx.update_editor(|editor, cx| { -// let snapshot = editor.snapshot(cx); -// let previous_valid = inlay_range.start.to_display_point(&snapshot); -// let next_valid = inlay_range.end.to_display_point(&snapshot); -// assert_eq!(previous_valid.row(), next_valid.row()); -// assert!(previous_valid.column() < next_valid.column()); -// let exact_unclipped = DisplayPoint::new( -// previous_valid.row(), -// previous_valid.column() -// + (entire_hint_label.find(struct_label).unwrap() + struct_label.len() / 2) -// as u32, -// ); -// PointForPosition { -// previous_valid, -// next_valid, -// exact_unclipped, -// column_overshoot_after_line_end: 0, -// } -// }); -// cx.update_editor(|editor, cx| { -// update_inlay_link_and_hover_points( -// &editor.snapshot(cx), -// struct_hint_part_hover_position, -// editor, -// true, -// false, -// cx, -// ); -// }); -// cx.foreground() -// .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); -// cx.foreground().run_until_parked(); -// cx.update_editor(|editor, cx| { -// let hover_state = &editor.hover_state; -// assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some()); -// let popover = hover_state.info_popover.as_ref().unwrap(); -// let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx)); -// assert_eq!( -// popover.symbol_range, -// RangeInEditor::Inlay(InlayHighlight { -// inlay: InlayId::Hint(0), -// inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right), -// range: ": ".len() + new_type_label.len() + "<".len() -// ..": ".len() + new_type_label.len() + "<".len() + struct_label.len(), -// }), -// "Popover range should match the struct label part" -// ); -// assert_eq!( -// popover.parsed_content.text, -// format!("A tooltip for {struct_label}"), -// "Rendered markdown element should remove backticks from text" -// ); -// }); -// } -// } +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + editor_tests::init_test, + element::PointForPosition, + inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels}, + link_go_to_definition::update_inlay_link_and_hover_points, + test::editor_lsp_test_context::EditorLspTestContext, + InlayId, + }; + use collections::BTreeSet; + use gpui::{FontWeight, HighlightStyle, UnderlineStyle}; + use indoc::indoc; + use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet}; + use lsp::LanguageServerId; + use project::{HoverBlock, HoverBlockKind}; + use smol::stream::StreamExt; + use unindent::Unindent; + use util::test::marked_text_ranges; + + #[gpui::test] + async fn test_mouse_hover_info_popover(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), + ..Default::default() + }, + cx, + ) + .await; + + // Basic hover delays and then pops without moving the mouse + cx.set_state(indoc! {" + fn ˇtest() { println!(); } + "}); + let hover_point = cx.display_point(indoc! {" + fn test() { printˇln!(); } + "}); + + cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx)); + assert!(!cx.editor(|editor, _| editor.hover_state.visible())); + + // After delay, hover should be visible. + let symbol_range = cx.lsp_range(indoc! {" + fn test() { «println!»(); } + "}); + let mut requests = + cx.handle_request::(move |_, _, _| async move { + Ok(Some(lsp::Hover { + contents: lsp::HoverContents::Markup(lsp::MarkupContent { + kind: lsp::MarkupKind::Markdown, + value: "some basic docs".to_string(), + }), + range: Some(symbol_range), + })) + }); + cx.background_executor + .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); + requests.next().await; + + cx.editor(|editor, _| { + assert!(editor.hover_state.visible()); + assert_eq!( + editor.hover_state.info_popover.clone().unwrap().blocks, + vec![HoverBlock { + text: "some basic docs".to_string(), + kind: HoverBlockKind::Markdown, + },] + ) + }); + + // Mouse moved with no hover response dismisses + let hover_point = cx.display_point(indoc! {" + fn teˇst() { println!(); } + "}); + let mut request = cx + .lsp + .handle_request::(|_, _| async move { Ok(None) }); + cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx)); + cx.background_executor + .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); + request.next().await; + cx.editor(|editor, _| { + assert!(!editor.hover_state.visible()); + }); + } + + #[gpui::test] + async fn test_keyboard_hover_info_popover(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), + ..Default::default() + }, + cx, + ) + .await; + + // Hover with keyboard has no delay + cx.set_state(indoc! {" + fˇn test() { println!(); } + "}); + cx.update_editor(|editor, cx| hover(editor, &Hover, cx)); + let symbol_range = cx.lsp_range(indoc! {" + «fn» test() { println!(); } + "}); + cx.handle_request::(move |_, _, _| async move { + Ok(Some(lsp::Hover { + contents: lsp::HoverContents::Markup(lsp::MarkupContent { + kind: lsp::MarkupKind::Markdown, + value: "some other basic docs".to_string(), + }), + range: Some(symbol_range), + })) + }) + .next() + .await; + + cx.condition(|editor, _| editor.hover_state.visible()).await; + cx.editor(|editor, _| { + assert_eq!( + editor.hover_state.info_popover.clone().unwrap().blocks, + vec![HoverBlock { + text: "some other basic docs".to_string(), + kind: HoverBlockKind::Markdown, + }] + ) + }); + } + + #[gpui::test] + async fn test_empty_hovers_filtered(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), + ..Default::default() + }, + cx, + ) + .await; + + // Hover with keyboard has no delay + cx.set_state(indoc! {" + fˇn test() { println!(); } + "}); + cx.update_editor(|editor, cx| hover(editor, &Hover, cx)); + let symbol_range = cx.lsp_range(indoc! {" + «fn» test() { println!(); } + "}); + cx.handle_request::(move |_, _, _| async move { + Ok(Some(lsp::Hover { + contents: lsp::HoverContents::Array(vec![ + lsp::MarkedString::String("regular text for hover to show".to_string()), + lsp::MarkedString::String("".to_string()), + lsp::MarkedString::LanguageString(lsp::LanguageString { + language: "Rust".to_string(), + value: "".to_string(), + }), + ]), + range: Some(symbol_range), + })) + }) + .next() + .await; + + cx.condition(|editor, _| editor.hover_state.visible()).await; + cx.editor(|editor, _| { + assert_eq!( + editor.hover_state.info_popover.clone().unwrap().blocks, + vec![HoverBlock { + text: "regular text for hover to show".to_string(), + kind: HoverBlockKind::Markdown, + }], + "No empty string hovers should be shown" + ); + }); + } + + #[gpui::test] + async fn test_line_ends_trimmed(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), + ..Default::default() + }, + cx, + ) + .await; + + // Hover with keyboard has no delay + cx.set_state(indoc! {" + fˇn test() { println!(); } + "}); + cx.update_editor(|editor, cx| hover(editor, &Hover, cx)); + let symbol_range = cx.lsp_range(indoc! {" + «fn» test() { println!(); } + "}); + + let code_str = "\nlet hovered_point: Vector2F // size = 8, align = 0x4\n"; + let markdown_string = format!("\n```rust\n{code_str}```"); + + let closure_markdown_string = markdown_string.clone(); + cx.handle_request::(move |_, _, _| { + let future_markdown_string = closure_markdown_string.clone(); + async move { + Ok(Some(lsp::Hover { + contents: lsp::HoverContents::Markup(lsp::MarkupContent { + kind: lsp::MarkupKind::Markdown, + value: future_markdown_string, + }), + range: Some(symbol_range), + })) + } + }) + .next() + .await; + + cx.condition(|editor, _| editor.hover_state.visible()).await; + cx.editor(|editor, _| { + let blocks = editor.hover_state.info_popover.clone().unwrap().blocks; + assert_eq!( + blocks, + vec![HoverBlock { + text: markdown_string, + kind: HoverBlockKind::Markdown, + }], + ); + + let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None)); + assert_eq!( + rendered.text, + code_str.trim(), + "Should not have extra line breaks at end of rendered hover" + ); + }); + } + + #[gpui::test] + async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), + ..Default::default() + }, + cx, + ) + .await; + + // Hover with just diagnostic, pops DiagnosticPopover immediately and then + // info popover once request completes + cx.set_state(indoc! {" + fn teˇst() { println!(); } + "}); + + // Send diagnostic to client + let range = cx.text_anchor_range(indoc! {" + fn «test»() { println!(); } + "}); + cx.update_buffer(|buffer, cx| { + let snapshot = buffer.text_snapshot(); + let set = DiagnosticSet::from_sorted_entries( + vec![DiagnosticEntry { + range, + diagnostic: Diagnostic { + message: "A test diagnostic message.".to_string(), + ..Default::default() + }, + }], + &snapshot, + ); + buffer.update_diagnostics(LanguageServerId(0), set, cx); + }); + + // Hover pops diagnostic immediately + cx.update_editor(|editor, cx| hover(editor, &Hover, cx)); + cx.background_executor.run_until_parked(); + + cx.editor(|Editor { hover_state, .. }, _| { + assert!(hover_state.diagnostic_popover.is_some() && hover_state.info_popover.is_none()) + }); + + // Info Popover shows after request responded to + let range = cx.lsp_range(indoc! {" + fn «test»() { println!(); } + "}); + cx.handle_request::(move |_, _, _| async move { + Ok(Some(lsp::Hover { + contents: lsp::HoverContents::Markup(lsp::MarkupContent { + kind: lsp::MarkupKind::Markdown, + value: "some new docs".to_string(), + }), + range: Some(range), + })) + }); + cx.background_executor + .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); + + cx.background_executor.run_until_parked(); + cx.editor(|Editor { hover_state, .. }, _| { + hover_state.diagnostic_popover.is_some() && hover_state.info_task.is_some() + }); + } + + #[gpui::test] + fn test_render_blocks(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let editor = cx.add_window(|cx| Editor::single_line(cx)); + editor + .update(cx, |editor, cx| { + let style = editor.style.clone().unwrap(); + + struct Row { + blocks: Vec, + expected_marked_text: String, + expected_styles: Vec, + } + + let rows = &[ + // Strong emphasis + Row { + blocks: vec![HoverBlock { + text: "one **two** three".to_string(), + kind: HoverBlockKind::Markdown, + }], + expected_marked_text: "one «two» three".to_string(), + expected_styles: vec![HighlightStyle { + font_weight: Some(FontWeight::BOLD), + ..Default::default() + }], + }, + // Links + Row { + blocks: vec![HoverBlock { + text: "one [two](https://the-url) three".to_string(), + kind: HoverBlockKind::Markdown, + }], + expected_marked_text: "one «two» three".to_string(), + expected_styles: vec![HighlightStyle { + underline: Some(UnderlineStyle { + thickness: 1.0.into(), + ..Default::default() + }), + ..Default::default() + }], + }, + // Lists + Row { + blocks: vec![HoverBlock { + text: " + lists: + * one + - a + - b + * two + - [c](https://the-url) + - d" + .unindent(), + kind: HoverBlockKind::Markdown, + }], + expected_marked_text: " + lists: + - one + - a + - b + - two + - «c» + - d" + .unindent(), + expected_styles: vec![HighlightStyle { + underline: Some(UnderlineStyle { + thickness: 1.0.into(), + ..Default::default() + }), + ..Default::default() + }], + }, + // Multi-paragraph list items + Row { + blocks: vec![HoverBlock { + text: " + * one two + three + + * four five + * six seven + eight + + nine + * ten + * six" + .unindent(), + kind: HoverBlockKind::Markdown, + }], + expected_marked_text: " + - one two three + - four five + - six seven eight + + nine + - ten + - six" + .unindent(), + expected_styles: vec![HighlightStyle { + underline: Some(UnderlineStyle { + thickness: 1.0.into(), + ..Default::default() + }), + ..Default::default() + }], + }, + ]; + + for Row { + blocks, + expected_marked_text, + expected_styles, + } in &rows[0..] + { + let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None)); + + let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false); + let expected_highlights = ranges + .into_iter() + .zip(expected_styles.iter().cloned()) + .collect::>(); + assert_eq!( + rendered.text, expected_text, + "wrong text for input {blocks:?}" + ); + + let rendered_highlights: Vec<_> = rendered + .highlights + .iter() + .filter_map(|(range, highlight)| { + let highlight = highlight.to_highlight_style(&style.syntax)?; + Some((range.clone(), highlight)) + }) + .collect(); + + assert_eq!( + rendered_highlights, expected_highlights, + "wrong highlights for input {blocks:?}" + ); + } + }) + .unwrap(); + } + + #[gpui::test] + async fn test_hover_inlay_label_parts(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, + }) + }); + + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Right( + lsp::InlayHintServerCapabilities::Options(lsp::InlayHintOptions { + resolve_provider: Some(true), + ..Default::default() + }), + )), + ..Default::default() + }, + cx, + ) + .await; + + cx.set_state(indoc! {" + struct TestStruct; + + // ================== + + struct TestNewType(T); + + fn main() { + let variableˇ = TestNewType(TestStruct); + } + "}); + + let hint_start_offset = cx.ranges(indoc! {" + struct TestStruct; + + // ================== + + struct TestNewType(T); + + fn main() { + let variableˇ = TestNewType(TestStruct); + } + "})[0] + .start; + let hint_position = cx.to_lsp(hint_start_offset); + let new_type_target_range = cx.lsp_range(indoc! {" + struct TestStruct; + + // ================== + + struct «TestNewType»(T); + + fn main() { + let variable = TestNewType(TestStruct); + } + "}); + let struct_target_range = cx.lsp_range(indoc! {" + struct «TestStruct»; + + // ================== + + struct TestNewType(T); + + fn main() { + let variable = TestNewType(TestStruct); + } + "}); + + let uri = cx.buffer_lsp_url.clone(); + let new_type_label = "TestNewType"; + let struct_label = "TestStruct"; + let entire_hint_label = ": TestNewType"; + let closure_uri = uri.clone(); + cx.lsp + .handle_request::(move |params, _| { + let task_uri = closure_uri.clone(); + async move { + assert_eq!(params.text_document.uri, task_uri); + Ok(Some(vec![lsp::InlayHint { + position: hint_position, + label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart { + value: entire_hint_label.to_string(), + ..Default::default() + }]), + kind: Some(lsp::InlayHintKind::TYPE), + text_edits: None, + tooltip: None, + padding_left: Some(false), + padding_right: Some(false), + data: None, + }])) + } + }) + .next() + .await; + cx.background_executor.run_until_parked(); + cx.update_editor(|editor, cx| { + let expected_layers = vec![entire_hint_label.to_string()]; + assert_eq!(expected_layers, cached_hint_labels(editor)); + assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + }); + + let inlay_range = cx + .ranges(indoc! {" + struct TestStruct; + + // ================== + + struct TestNewType(T); + + fn main() { + let variable« »= TestNewType(TestStruct); + } + "}) + .get(0) + .cloned() + .unwrap(); + let new_type_hint_part_hover_position = cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let previous_valid = inlay_range.start.to_display_point(&snapshot); + let next_valid = inlay_range.end.to_display_point(&snapshot); + assert_eq!(previous_valid.row(), next_valid.row()); + assert!(previous_valid.column() < next_valid.column()); + let exact_unclipped = DisplayPoint::new( + previous_valid.row(), + previous_valid.column() + + (entire_hint_label.find(new_type_label).unwrap() + new_type_label.len() / 2) + as u32, + ); + PointForPosition { + previous_valid, + next_valid, + exact_unclipped, + column_overshoot_after_line_end: 0, + } + }); + cx.update_editor(|editor, cx| { + update_inlay_link_and_hover_points( + &editor.snapshot(cx), + new_type_hint_part_hover_position, + editor, + true, + false, + cx, + ); + }); + + let resolve_closure_uri = uri.clone(); + cx.lsp + .handle_request::( + move |mut hint_to_resolve, _| { + let mut resolved_hint_positions = BTreeSet::new(); + let task_uri = resolve_closure_uri.clone(); + async move { + let inserted = resolved_hint_positions.insert(hint_to_resolve.position); + assert!(inserted, "Hint {hint_to_resolve:?} was resolved twice"); + + // `: TestNewType` + hint_to_resolve.label = lsp::InlayHintLabel::LabelParts(vec![ + lsp::InlayHintLabelPart { + value: ": ".to_string(), + ..Default::default() + }, + lsp::InlayHintLabelPart { + value: new_type_label.to_string(), + location: Some(lsp::Location { + uri: task_uri.clone(), + range: new_type_target_range, + }), + tooltip: Some(lsp::InlayHintLabelPartTooltip::String(format!( + "A tooltip for `{new_type_label}`" + ))), + ..Default::default() + }, + lsp::InlayHintLabelPart { + value: "<".to_string(), + ..Default::default() + }, + lsp::InlayHintLabelPart { + value: struct_label.to_string(), + location: Some(lsp::Location { + uri: task_uri, + range: struct_target_range, + }), + tooltip: Some(lsp::InlayHintLabelPartTooltip::MarkupContent( + lsp::MarkupContent { + kind: lsp::MarkupKind::Markdown, + value: format!("A tooltip for `{struct_label}`"), + }, + )), + ..Default::default() + }, + lsp::InlayHintLabelPart { + value: ">".to_string(), + ..Default::default() + }, + ]); + + Ok(hint_to_resolve) + } + }, + ) + .next() + .await; + cx.background_executor.run_until_parked(); + + cx.update_editor(|editor, cx| { + update_inlay_link_and_hover_points( + &editor.snapshot(cx), + new_type_hint_part_hover_position, + editor, + true, + false, + cx, + ); + }); + cx.background_executor + .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); + cx.background_executor.run_until_parked(); + cx.update_editor(|editor, cx| { + let hover_state = &editor.hover_state; + assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some()); + let popover = hover_state.info_popover.as_ref().unwrap(); + let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx)); + assert_eq!( + popover.symbol_range, + RangeInEditor::Inlay(InlayHighlight { + inlay: InlayId::Hint(0), + inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right), + range: ": ".len()..": ".len() + new_type_label.len(), + }), + "Popover range should match the new type label part" + ); + assert_eq!( + popover.parsed_content.text, + format!("A tooltip for `{new_type_label}`"), + "Rendered text should not anyhow alter backticks" + ); + }); + + let struct_hint_part_hover_position = cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let previous_valid = inlay_range.start.to_display_point(&snapshot); + let next_valid = inlay_range.end.to_display_point(&snapshot); + assert_eq!(previous_valid.row(), next_valid.row()); + assert!(previous_valid.column() < next_valid.column()); + let exact_unclipped = DisplayPoint::new( + previous_valid.row(), + previous_valid.column() + + (entire_hint_label.find(struct_label).unwrap() + struct_label.len() / 2) + as u32, + ); + PointForPosition { + previous_valid, + next_valid, + exact_unclipped, + column_overshoot_after_line_end: 0, + } + }); + cx.update_editor(|editor, cx| { + update_inlay_link_and_hover_points( + &editor.snapshot(cx), + struct_hint_part_hover_position, + editor, + true, + false, + cx, + ); + }); + cx.background_executor + .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); + cx.background_executor.run_until_parked(); + cx.update_editor(|editor, cx| { + let hover_state = &editor.hover_state; + assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some()); + let popover = hover_state.info_popover.as_ref().unwrap(); + let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx)); + assert_eq!( + popover.symbol_range, + RangeInEditor::Inlay(InlayHighlight { + inlay: InlayId::Hint(0), + inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right), + range: ": ".len() + new_type_label.len() + "<".len() + ..": ".len() + new_type_label.len() + "<".len() + struct_label.len(), + }), + "Popover range should match the struct label part" + ); + assert_eq!( + popover.parsed_content.text, + format!("A tooltip for {struct_label}"), + "Rendered markdown element should remove backticks from text" + ); + }); + } +} diff --git a/crates/editor2/src/inlay_hint_cache.rs b/crates/editor2/src/inlay_hint_cache.rs index eba49ccbf7..1610c4826e 100644 --- a/crates/editor2/src/inlay_hint_cache.rs +++ b/crates/editor2/src/inlay_hint_cache.rs @@ -861,7 +861,7 @@ async fn fetch_and_update_hints( let inlay_hints_fetch_task = editor .update(&mut cx, |editor, cx| { if got_throttled { - let query_not_around_visible_range = match editor.excerpt_visible_offsets(None, cx).remove(&query.excerpt_id) { + let query_not_around_visible_range = match editor.excerpts_for_inlay_hints_query(None, cx).remove(&query.excerpt_id) { Some((_, _, current_visible_range)) => { let visible_offset_length = current_visible_range.len(); let double_visible_range = current_visible_range @@ -2201,7 +2201,9 @@ pub mod tests { cx: &mut gpui::TestAppContext, ) -> Range { let ranges = editor - .update(cx, |editor, cx| editor.excerpt_visible_offsets(None, cx)) + .update(cx, |editor, cx| { + editor.excerpts_for_inlay_hints_query(None, cx) + }) .unwrap(); assert_eq!( ranges.len(), diff --git a/crates/editor2/src/items.rs b/crates/editor2/src/items.rs index 8919e26f3b..eca3b99d78 100644 --- a/crates/editor2/src/items.rs +++ b/crates/editor2/src/items.rs @@ -9,8 +9,8 @@ use collections::HashSet; use futures::future::try_join_all; use gpui::{ div, point, AnyElement, AppContext, AsyncAppContext, Entity, EntityId, EventEmitter, - FocusHandle, Model, ParentComponent, Pixels, SharedString, Styled, Subscription, Task, View, - ViewContext, VisualContext, WeakView, + FocusHandle, Model, ParentElement, Pixels, SharedString, Styled, Subscription, Task, View, + 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 3691e69f5d..b54d28a400 100644 --- a/crates/file_finder2/src/file_finder.rs +++ b/crates/file_finder2/src/file_finder.rs @@ -2,9 +2,8 @@ use collections::HashMap; use editor::{scroll::autoscroll::Autoscroll, Bias, Editor}; use fuzzy::{CharBag, PathMatch, PathMatchCandidate}; use gpui::{ - actions, div, AppContext, Component, Div, EventEmitter, FocusHandle, FocusableView, - InteractiveComponent, Manager, Model, ParentComponent, Render, Styled, Task, View, ViewContext, - VisualContext, WeakView, + actions, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView, Model, + ParentElement, Render, Styled, Task, View, ViewContext, VisualContext, WeakView, }; use picker::{Picker, PickerDelegate}; use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId}; @@ -16,8 +15,7 @@ use std::{ }, }; use text::Point; -use theme::ActiveTheme; -use ui::{v_stack, HighlightedLabel, StyledExt}; +use ui::{v_stack, HighlightedLabel, ListItem}; use util::{paths::PathLikeWithPosition, post_inc, ResultExt}; use workspace::Workspace; @@ -111,14 +109,14 @@ impl FileFinder { } } -impl EventEmitter for FileFinder {} +impl EventEmitter for FileFinder {} impl FocusableView for FileFinder { fn focus_handle(&self, cx: &AppContext) -> FocusHandle { self.picker.focus_handle(cx) } } impl Render for FileFinder { - type Element = Div; + type Element = Div; fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { v_stack().w_96().child(self.picker.clone()) @@ -530,7 +528,7 @@ impl FileFinderDelegate { } impl PickerDelegate for FileFinderDelegate { - type ListItem = Div>; + type ListItem = ListItem; fn placeholder_text(&self) -> Arc { "Search project files...".into() @@ -690,7 +688,7 @@ impl PickerDelegate for FileFinderDelegate { } } finder - .update(&mut cx, |_, cx| cx.emit(Manager::Dismiss)) + .update(&mut cx, |_, cx| cx.emit(DismissEvent::Dismiss)) .ok()?; Some(()) @@ -702,7 +700,7 @@ impl PickerDelegate for FileFinderDelegate { fn dismissed(&mut self, cx: &mut ViewContext>) { self.file_finder - .update(cx, |_, cx| cx.emit(Manager::Dismiss)) + .update(cx, |_, cx| cx.emit(DismissEvent::Dismiss)) .log_err(); } @@ -711,30 +709,22 @@ impl PickerDelegate for FileFinderDelegate { ix: usize, selected: bool, cx: &mut ViewContext>, - ) -> Self::ListItem { + ) -> Option { let path_match = self .matches .get(ix) .expect("Invalid matches state: no element for index {ix}"); - let theme = cx.theme(); - let colors = theme.colors(); let (file_name, file_name_positions, full_path, full_path_positions) = self.labels_for_match(path_match, cx, ix); - div() - .px_1() - .text_color(colors.text) - .text_ui() - .bg(colors.ghost_element_background) - .rounded_md() - .when(selected, |this| this.bg(colors.ghost_element_selected)) - .hover(|this| this.bg(colors.ghost_element_hover)) - .child( + Some( + ListItem::new(ix).selected(selected).child( v_stack() .child(HighlightedLabel::new(file_name, file_name_positions)) .child(HighlightedLabel::new(full_path, full_path_positions)), - ) + ), + ) } } diff --git a/crates/fuzzy2/src/strings.rs b/crates/fuzzy2/src/strings.rs index 085362dd2c..5028a43fd7 100644 --- a/crates/fuzzy2/src/strings.rs +++ b/crates/fuzzy2/src/strings.rs @@ -6,6 +6,8 @@ use gpui::BackgroundExecutor; use std::{ borrow::Cow, cmp::{self, Ordering}, + iter, + ops::Range, sync::atomic::AtomicBool, }; @@ -54,6 +56,32 @@ pub struct StringMatch { pub string: String, } +impl StringMatch { + pub fn ranges<'a>(&'a self) -> impl 'a + Iterator> { + let mut positions = self.positions.iter().peekable(); + iter::from_fn(move || { + while let Some(start) = positions.next().copied() { + let mut end = start + self.char_len_at_index(start); + while let Some(next_start) = positions.peek() { + if end == **next_start { + end += self.char_len_at_index(end); + positions.next(); + } else { + break; + } + } + + return Some(start..end); + } + None + }) + } + + fn char_len_at_index(&self, ix: usize) -> usize { + self.string[ix..].chars().next().unwrap().len_utf8() + } +} + impl PartialEq for StringMatch { fn eq(&self, other: &Self) -> bool { self.cmp(other).is_eq() diff --git a/crates/go_to_line2/src/go_to_line.rs b/crates/go_to_line2/src/go_to_line.rs index 8c7ac97d0b..c734281d22 100644 --- a/crates/go_to_line2/src/go_to_line.rs +++ b/crates/go_to_line2/src/go_to_line.rs @@ -1,14 +1,13 @@ use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Editor}; use gpui::{ - actions, div, prelude::*, AppContext, Div, EventEmitter, FocusHandle, FocusableView, Manager, - ParentComponent, Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext, + actions, div, prelude::*, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, + FocusableView, Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext, WindowContext, }; use text::{Bias, Point}; use theme::ActiveTheme; -use ui::{h_stack, v_stack, Label, StyledExt, TextColor}; +use ui::{h_stack, v_stack, Color, Label, StyledExt}; use util::paths::FILE_ROW_COLUMN_DELIMITER; -use workspace::Workspace; actions!(Toggle); @@ -26,22 +25,24 @@ pub struct GoToLine { impl FocusableView for GoToLine { fn focus_handle(&self, cx: &AppContext) -> FocusHandle { - self.active_editor.focus_handle(cx) + self.line_editor.focus_handle(cx) } } -impl EventEmitter for GoToLine {} +impl EventEmitter for GoToLine {} impl GoToLine { - fn register(workspace: &mut Workspace, _: &mut ViewContext) { - workspace.register_action(|workspace, _: &Toggle, cx| { - let Some(editor) = workspace - .active_item(cx) - .and_then(|active_item| active_item.downcast::()) - else { + fn register(editor: &mut Editor, cx: &mut ViewContext) { + let handle = cx.view().downgrade(); + editor.register_action(move |_: &Toggle, cx| { + let Some(editor) = handle.upgrade() else { return; }; - - workspace.toggle_modal(cx, move |cx| GoToLine::new(editor, cx)); + let Some(workspace) = editor.read(cx).workspace() else { + return; + }; + workspace.update(cx, |workspace, cx| { + workspace.toggle_modal(cx, move |cx| GoToLine::new(editor, cx)); + }) }); } @@ -89,7 +90,7 @@ impl GoToLine { ) { match event { // todo!() this isn't working... - editor::EditorEvent::Blurred => cx.emit(Manager::Dismiss), + editor::EditorEvent::Blurred => cx.emit(DismissEvent::Dismiss), editor::EditorEvent::BufferEdited { .. } => self.highlight_current_line(cx), _ => {} } @@ -124,7 +125,7 @@ impl GoToLine { } fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { - cx.emit(Manager::Dismiss); + cx.emit(DismissEvent::Dismiss); } fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { @@ -141,19 +142,19 @@ impl GoToLine { self.prev_scroll_position.take(); } - cx.emit(Manager::Dismiss); + cx.emit(DismissEvent::Dismiss); } } impl Render for GoToLine { - type Element = Div; + 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() @@ -177,7 +178,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 ca96ba210e..e8f2a60a6a 100644 --- a/crates/gpui2/src/app.rs +++ b/crates/gpui2/src/app.rs @@ -10,11 +10,12 @@ pub use entity_map::*; pub use model_context::*; use refineable::Refineable; use smallvec::SmallVec; +use smol::future::FutureExt; #[cfg(any(test, feature = "test-support"))] pub use test_context::*; use crate::{ - current_platform, image_cache::ImageCache, Action, ActionRegistry, AnyBox, AnyView, + current_platform, image_cache::ImageCache, Action, ActionRegistry, Any, AnyView, AnyWindowHandle, AppMetadata, AssetSource, BackgroundExecutor, ClipboardItem, Context, DispatchPhase, DisplayId, Entity, EventEmitter, FocusEvent, FocusHandle, FocusId, ForegroundExecutor, KeyBinding, Keymap, LayoutId, PathPromptOptions, Pixels, Platform, @@ -28,7 +29,7 @@ use futures::{channel::oneshot, future::LocalBoxFuture, Future}; use parking_lot::Mutex; use slotmap::SlotMap; use std::{ - any::{type_name, Any, TypeId}, + any::{type_name, TypeId}, cell::{Ref, RefCell, RefMut}, marker::PhantomData, mem, @@ -194,7 +195,7 @@ pub struct AppContext { asset_source: Arc, pub(crate) image_cache: ImageCache, pub(crate) text_style_stack: Vec, - pub(crate) globals_by_type: HashMap, + pub(crate) globals_by_type: HashMap>, pub(crate) entities: EntityMap, pub(crate) new_view_observers: SubscriberSet, pub(crate) windows: SlotMap>, @@ -424,7 +425,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, @@ -579,7 +580,7 @@ impl AppContext { .windows .iter() .filter_map(|(_, window)| { - let window = window.as_ref().unwrap(); + let window = window.as_ref()?; if window.dirty { Some(window.handle.clone()) } else { @@ -983,6 +984,22 @@ impl AppContext { pub fn all_action_names(&self) -> &[SharedString] { self.actions.all_action_names() } + + pub fn on_app_quit( + &mut self, + mut on_quit: impl FnMut(&mut AppContext) -> Fut + 'static, + ) -> Subscription + where + Fut: 'static + Future, + { + self.quit_observers.insert( + (), + Box::new(move |cx| { + let future = on_quit(cx); + async move { future.await }.boxed_local() + }), + ) + } } impl Context for AppContext { @@ -1032,7 +1049,9 @@ impl Context for AppContext { let root_view = window.root_view.clone().unwrap(); let result = update(root_view, &mut WindowContext::new(cx, &mut window)); - if !window.removed { + if window.removed { + cx.windows.remove(handle.id); + } else { cx.windows .get_mut(handle.id) .ok_or_else(|| anyhow!("window not found"))? @@ -1104,12 +1123,12 @@ pub(crate) enum Effect { /// Wraps a global variable value during `update_global` while the value has been moved to the stack. pub(crate) struct GlobalLease { - global: AnyBox, + global: Box, global_type: PhantomData, } impl GlobalLease { - fn new(global: AnyBox) -> Self { + fn new(global: Box) -> Self { GlobalLease { global, global_type: PhantomData, diff --git a/crates/gpui2/src/app/async_context.rs b/crates/gpui2/src/app/async_context.rs index 65c006b062..11420bee69 100644 --- a/crates/gpui2/src/app/async_context.rs +++ b/crates/gpui2/src/app/async_context.rs @@ -1,7 +1,7 @@ use crate::{ - AnyView, AnyWindowHandle, AppCell, AppContext, BackgroundExecutor, Context, FocusableView, - ForegroundExecutor, Manager, Model, ModelContext, Render, Result, Task, View, ViewContext, - VisualContext, WindowContext, WindowHandle, + AnyView, AnyWindowHandle, AppCell, AppContext, BackgroundExecutor, Context, DismissEvent, + FocusableView, ForegroundExecutor, Model, ModelContext, Render, Result, Task, View, + ViewContext, VisualContext, WindowContext, WindowHandle, }; use anyhow::{anyhow, Context as _}; use derive_more::{Deref, DerefMut}; @@ -115,7 +115,7 @@ impl AsyncAppContext { build_root_view: impl FnOnce(&mut WindowContext) -> View, ) -> Result> where - V: Render, + V: 'static + Render, { let app = self .app @@ -306,7 +306,7 @@ impl VisualContext for AsyncWindowContext { build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V, ) -> Self::Result> where - V: Render, + V: 'static + Render, { self.window .update(self, |_, cx| cx.replace_root_view(build_view)) @@ -326,7 +326,7 @@ impl VisualContext for AsyncWindowContext { V: crate::ManagedView, { self.window.update(self, |_, cx| { - view.update(cx, |_, cx| cx.emit(Manager::Dismiss)) + view.update(cx, |_, cx| cx.emit(DismissEvent::Dismiss)) }) } } diff --git a/crates/gpui2/src/app/entity_map.rs b/crates/gpui2/src/app/entity_map.rs index f1e7fad6a1..a34582f4f4 100644 --- a/crates/gpui2/src/app/entity_map.rs +++ b/crates/gpui2/src/app/entity_map.rs @@ -1,10 +1,10 @@ -use crate::{private::Sealed, AnyBox, AppContext, Context, Entity, ModelContext}; +use crate::{private::Sealed, AppContext, Context, Entity, ModelContext}; use anyhow::{anyhow, Result}; use derive_more::{Deref, DerefMut}; use parking_lot::{RwLock, RwLockUpgradableReadGuard}; use slotmap::{SecondaryMap, SlotMap}; use std::{ - any::{type_name, TypeId}, + any::{type_name, Any, TypeId}, fmt::{self, Display}, hash::{Hash, Hasher}, marker::PhantomData, @@ -31,7 +31,7 @@ impl Display for EntityId { } pub(crate) struct EntityMap { - entities: SecondaryMap, + entities: SecondaryMap>, ref_counts: Arc>, } @@ -102,7 +102,7 @@ impl EntityMap { ); } - pub fn take_dropped(&mut self) -> Vec<(EntityId, AnyBox)> { + pub fn take_dropped(&mut self) -> Vec<(EntityId, Box)> { let mut ref_counts = self.ref_counts.write(); let dropped_entity_ids = mem::take(&mut ref_counts.dropped_entity_ids); @@ -122,7 +122,7 @@ impl EntityMap { } pub struct Lease<'a, T> { - entity: Option, + entity: Option>, pub model: &'a Model, entity_type: PhantomData, } diff --git a/crates/gpui2/src/app/test_context.rs b/crates/gpui2/src/app/test_context.rs index 0315e362b0..71bc8e3d81 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: 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: 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 } @@ -591,7 +590,7 @@ impl<'a> VisualContext for VisualTestContext<'a> { build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V, ) -> Self::Result> where - V: Render, + V: 'static + Render, { self.window .update(self.cx, |_, cx| cx.replace_root_view(build_view)) @@ -612,7 +611,7 @@ impl<'a> VisualContext for VisualTestContext<'a> { { self.window .update(self.cx, |_, cx| { - view.update(cx, |_, cx| cx.emit(crate::Manager::Dismiss)) + view.update(cx, |_, cx| cx.emit(crate::DismissEvent::Dismiss)) }) .unwrap() } @@ -631,7 +630,7 @@ impl AnyWindowHandle { pub struct EmptyView {} impl Render for EmptyView { - type Element = Div; + 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 c773bb6f65..b18ffb8ca6 100644 --- a/crates/gpui2/src/element.rs +++ b/crates/gpui2/src/element.rs @@ -1,317 +1,63 @@ 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, mem}; +use std::{any::Any, fmt::Debug}; -pub trait Element { - type ElementState: 'static; +pub trait Render: 'static + Sized { + type Element: Element + 'static; + + fn render(&mut self, cx: &mut ViewContext) -> Self::Element; +} + +pub trait IntoElement: Sized { + type Element: Element + 'static; fn element_id(&self) -> Option; - fn layout( - &mut self, - view_state: &mut V, - element_state: Option, - cx: &mut ViewContext, - ) -> (LayoutId, Self::ElementState); + fn into_element(self) -> Self::Element; - fn paint( - &mut self, - bounds: Bounds, - view_state: &mut V, - element_state: &mut Self::ElementState, - cx: &mut ViewContext, - ); + fn into_any_element(self) -> AnyElement { + self.into_element().into_any() + } fn draw( self, origin: Point, available_space: Size, - view_state: &mut V, - cx: &mut ViewContext, - f: impl FnOnce(&Self::ElementState, &mut ViewContext) -> R, + cx: &mut WindowContext, + f: impl FnOnce(&mut ::State, &mut WindowContext) -> R, ) -> R where - Self: Sized, T: Clone + Default + Debug + Into, { - let mut element = RenderedElement { - element: self, - phase: ElementRenderPhase::Start, + let element = self.into_element(); + let element_id = element.element_id(); + let element = DrawableElement { + element: Some(element), + phase: ElementDrawPhase::Start, }; - element.draw(origin, available_space.map(Into::into), view_state, cx); - if let ElementRenderPhase::Painted { frame_state } = &element.phase { - if let Some(frame_state) = frame_state.as_ref() { - f(&frame_state, cx) - } else { - let element_id = element - .element - .element_id() - .expect("we either have some frame_state or some element_id"); - cx.with_element_state(element_id, |element_state, cx| { - let element_state = element_state.unwrap(); - let result = f(&element_state, cx); - (result, element_state) - }) - } + + 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) } else { - unreachable!() + cx.with_element_state(element_id.unwrap(), |element_state, cx| { + let mut element_state = element_state.unwrap(); + let result = f(&mut element_state, cx); + (result, element_state) + }) } } -} - -#[derive(Deref, DerefMut, Default, Clone, Debug, Eq, PartialEq, Hash)] -pub struct GlobalElementId(SmallVec<[ElementId; 32]>); - -pub trait ParentComponent { - fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]>; - - fn child(mut self, child: impl Component) -> Self - where - Self: Sized, - { - self.children_mut().push(child.render()); - self - } - - fn children(mut self, iter: impl IntoIterator>) -> Self - where - Self: Sized, - { - self.children_mut() - .extend(iter.into_iter().map(|item| item.render())); - self - } -} - -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 measure( - &mut self, - available_space: Size, - view_state: &mut V, - cx: &mut ViewContext, - ) -> Size; - fn draw( - &mut self, - origin: Point, - available_space: Size, - view_state: &mut V, - cx: &mut ViewContext, - ); -} - -struct RenderedElement> { - element: E, - phase: ElementRenderPhase, -} - -#[derive(Default)] -enum ElementRenderPhase { - #[default] - Start, - LayoutRequested { - layout_id: LayoutId, - frame_state: Option, - }, - LayoutComputed { - layout_id: LayoutId, - available_space: Size, - frame_state: Option, - }, - Painted { - frame_state: Option, - }, -} - -/// Internal struct that wraps an element to store Layout and ElementState after the element is rendered. -/// It's allocated as a trait object to erase the element type and wrapped in AnyElement for -/// improved usability. -impl> RenderedElement { - fn new(element: E) -> Self { - RenderedElement { - element, - phase: ElementRenderPhase::Start, - } - } -} - -impl ElementObject for RenderedElement -where - E: Element, - E::ElementState: 'static, -{ - fn element_id(&self) -> Option { - self.element.element_id() - } - - fn layout(&mut self, state: &mut V, cx: &mut ViewContext) -> LayoutId { - let (layout_id, frame_state) = match mem::take(&mut self.phase) { - ElementRenderPhase::Start => { - if let Some(id) = self.element.element_id() { - let layout_id = cx.with_element_state(id, |element_state, cx| { - self.element.layout(state, element_state, cx) - }); - (layout_id, None) - } else { - let (layout_id, frame_state) = self.element.layout(state, None, cx); - (layout_id, Some(frame_state)) - } - } - ElementRenderPhase::LayoutRequested { .. } - | ElementRenderPhase::LayoutComputed { .. } - | ElementRenderPhase::Painted { .. } => { - panic!("element rendered twice") - } - }; - - self.phase = ElementRenderPhase::LayoutRequested { - layout_id, - frame_state, - }; - layout_id - } - - fn paint(&mut self, view_state: &mut V, cx: &mut ViewContext) { - self.phase = match mem::take(&mut self.phase) { - ElementRenderPhase::LayoutRequested { - layout_id, - mut frame_state, - } - | ElementRenderPhase::LayoutComputed { - layout_id, - mut frame_state, - .. - } => { - let bounds = cx.layout_bounds(layout_id); - if let Some(id) = self.element.element_id() { - cx.with_element_state(id, |element_state, cx| { - let mut element_state = element_state.unwrap(); - self.element - .paint(bounds, view_state, &mut element_state, cx); - ((), element_state) - }); - } else { - self.element - .paint(bounds, view_state, frame_state.as_mut().unwrap(), cx); - } - ElementRenderPhase::Painted { frame_state } - } - - _ => panic!("must call layout before paint"), - }; - } - - fn measure( - &mut self, - available_space: Size, - view_state: &mut V, - cx: &mut ViewContext, - ) -> Size { - if matches!(&self.phase, ElementRenderPhase::Start) { - self.layout(view_state, cx); - } - - let layout_id = match &mut self.phase { - ElementRenderPhase::LayoutRequested { - layout_id, - frame_state, - } => { - cx.compute_layout(*layout_id, available_space); - let layout_id = *layout_id; - self.phase = ElementRenderPhase::LayoutComputed { - layout_id, - available_space, - frame_state: frame_state.take(), - }; - layout_id - } - ElementRenderPhase::LayoutComputed { - layout_id, - available_space: prev_available_space, - .. - } => { - if available_space != *prev_available_space { - cx.compute_layout(*layout_id, available_space); - *prev_available_space = available_space; - } - *layout_id - } - _ => panic!("cannot measure after painting"), - }; - - cx.layout_bounds(layout_id).size - } - - fn draw( - &mut self, - origin: Point, - available_space: Size, - view_state: &mut V, - cx: &mut ViewContext, - ) { - self.measure(available_space, view_state, cx); - cx.with_absolute_element_offset(origin, |cx| self.paint(view_state, cx)) - } -} - -pub struct AnyElement(Box>); - -impl AnyElement { - pub fn new(element: E) -> Self - where - V: 'static, - E: 'static + Element, - E::ElementState: Any, - { - AnyElement(Box::new(RenderedElement::new(element))) - } - - 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 paint(&mut self, view_state: &mut V, cx: &mut ViewContext) { - self.0.paint(view_state, 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, - ) -> Size { - self.0.measure(available_space, view_state, cx) - } - - /// Initializes this element and performs layout in the available space, then paints it at the given origin. - pub fn draw( - &mut self, - origin: Point, - available_space: Size, - view_state: &mut V, - cx: &mut ViewContext, - ) { - self.0.draw(origin, available_space, view_state, cx) - } -} - -pub trait Component { - fn render(self) -> AnyElement; fn map(self, f: impl FnOnce(Self) -> U) -> U where Self: Sized, - U: Component, + U: IntoElement, { f(self) } @@ -337,65 +83,460 @@ pub trait Component { } } -impl Component for AnyElement { - fn render(self) -> AnyElement { - self +pub trait Element: 'static + IntoElement { + type State: 'static; + + fn layout( + &mut self, + state: Option, + cx: &mut WindowContext, + ) -> (LayoutId, Self::State); + + fn paint(self, bounds: Bounds, state: &mut Self::State, cx: &mut WindowContext); + + fn into_any(self) -> AnyElement { + AnyElement::new(self) } } -impl Element for Option -where - V: 'static, - E: 'static + Component, - F: FnOnce(&mut V, &mut ViewContext<'_, V>) -> E + 'static, -{ - type ElementState = AnyElement; +pub trait RenderOnce: 'static { + type Rendered: IntoElement; + + fn render(self, cx: &mut WindowContext) -> Self::Rendered; +} + +pub struct Component { + component: Option, +} + +pub struct CompositeElementState { + rendered_element: Option<::Element>, + rendered_element_state: <::Element as Element>::State, +} + +impl Component { + pub fn new(component: C) -> Self { + Component { + component: Some(component), + } + } +} + +impl Element for Component { + type State = CompositeElementState; + + fn layout( + &mut self, + state: Option, + cx: &mut WindowContext, + ) -> (LayoutId, Self::State) { + let mut element = self.component.take().unwrap().render(cx).into_element(); + 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, + }; + (layout_id, state) + } + + 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 IntoElement for Component { + type Element = Self; fn element_id(&self) -> Option { None } - fn layout( - &mut self, - view_state: &mut V, - _: Option, - cx: &mut ViewContext, - ) -> (LayoutId, Self::ElementState) { - let render = self.take().unwrap(); - let mut rendered_element = (render)(view_state, cx).render(); - let layout_id = rendered_element.layout(view_state, cx); - (layout_id, rendered_element) - } - - fn paint( - &mut self, - _bounds: Bounds, - view_state: &mut V, - rendered_element: &mut Self::ElementState, - cx: &mut ViewContext, - ) { - rendered_element.paint(view_state, cx) + fn into_element(self) -> Self::Element { + self } } -impl Component for Option +#[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]>; + + fn child(mut self, child: impl IntoElement) -> Self + where + Self: Sized, + { + self.children_mut().push(child.into_element().into_any()); + self + } + + fn children(mut self, children: impl IntoIterator) -> Self + where + Self: Sized, + { + self.children_mut() + .extend(children.into_iter().map(|child| child.into_any_element())); + self + } +} + +trait ElementObject { + fn element_id(&self) -> Option; + + fn layout(&mut self, cx: &mut WindowContext) -> LayoutId; + + fn paint(&mut self, cx: &mut WindowContext); + + fn measure( + &mut self, + available_space: Size, + cx: &mut WindowContext, + ) -> Size; + + fn draw( + &mut self, + origin: Point, + available_space: Size, + cx: &mut WindowContext, + ); +} + +pub struct DrawableElement { + element: Option, + phase: ElementDrawPhase, +} + +#[derive(Default)] +enum ElementDrawPhase { + #[default] + Start, + LayoutRequested { + layout_id: LayoutId, + frame_state: Option, + }, + LayoutComputed { + layout_id: LayoutId, + available_space: Size, + frame_state: Option, + }, +} + +/// A wrapper around an implementer of [Element] that allows it to be drawn in a window. +impl DrawableElement { + fn new(element: E) -> Self { + DrawableElement { + element: Some(element), + phase: ElementDrawPhase::Start, + } + } + + fn element_id(&self) -> Option { + self.element.as_ref()?.element_id() + } + + 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(element_state, cx) + }); + (layout_id, None) + } else { + let (layout_id, frame_state) = self.element.as_mut().unwrap().layout(None, cx); + (layout_id, Some(frame_state)) + }; + + self.phase = ElementDrawPhase::LayoutRequested { + layout_id, + frame_state, + }; + layout_id + } + + fn paint(mut self, cx: &mut WindowContext) -> Option { + match self.phase { + ElementDrawPhase::LayoutRequested { + layout_id, + frame_state, + } + | ElementDrawPhase::LayoutComputed { + layout_id, + frame_state, + .. + } => { + let bounds = cx.layout_bounds(layout_id); + + if let Some(mut frame_state) = frame_state { + self.element + .take() + .unwrap() + .paint(bounds, &mut frame_state, cx); + Some(frame_state) + } else { + let element_id = self + .element + .as_ref() + .unwrap() + .element_id() + .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, &mut element_state, cx); + ((), element_state) + }); + None + } + } + + _ => panic!("must call layout before paint"), + } + } + + fn measure( + &mut self, + available_space: Size, + cx: &mut WindowContext, + ) -> Size { + if matches!(&self.phase, ElementDrawPhase::Start) { + self.layout(cx); + } + + let layout_id = match &mut self.phase { + ElementDrawPhase::LayoutRequested { + layout_id, + frame_state, + } => { + cx.compute_layout(*layout_id, available_space); + let layout_id = *layout_id; + self.phase = ElementDrawPhase::LayoutComputed { + layout_id, + available_space, + frame_state: frame_state.take(), + }; + layout_id + } + ElementDrawPhase::LayoutComputed { + layout_id, + available_space: prev_available_space, + .. + } => { + if available_space != *prev_available_space { + cx.compute_layout(*layout_id, available_space); + *prev_available_space = available_space; + } + *layout_id + } + _ => panic!("cannot measure after painting"), + }; + + cx.layout_bounds(layout_id).size + } + + fn draw( + mut self, + origin: Point, + available_space: Size, + cx: &mut WindowContext, + ) -> Option { + self.measure(available_space, cx); + cx.with_absolute_element_offset(origin, |cx| self.paint(cx)) + } +} + +// impl Element for DrawableElement { +// type State = ::State; + +// fn layout( +// &mut self, +// element_state: Option, +// cx: &mut WindowContext, +// ) -> (LayoutId, Self::State) { + +// } + +// fn paint( +// self, +// bounds: Bounds, +// element_state: &mut Self::State, +// cx: &mut WindowContext, +// ) { +// todo!() +// } +// } + +// impl RenderOnce for DrawableElement { +// type Element = Self; + +// fn element_id(&self) -> Option { +// self.element.as_ref()?.element_id() +// } + +// fn render_once(self) -> Self::Element { +// self +// } +// } + +impl ElementObject for Option> where - V: 'static, - E: 'static + Component, - F: FnOnce(&mut V, &mut ViewContext<'_, V>) -> E + 'static, + E: Element, + E::State: 'static, { - fn render(self) -> AnyElement { + fn element_id(&self) -> Option { + self.as_ref().unwrap().element_id() + } + + fn layout(&mut self, cx: &mut WindowContext) -> LayoutId { + DrawableElement::layout(self.as_mut().unwrap(), cx) + } + + fn paint(&mut self, cx: &mut WindowContext) { + DrawableElement::paint(self.take().unwrap(), cx); + } + + fn measure( + &mut self, + available_space: Size, + cx: &mut WindowContext, + ) -> Size { + DrawableElement::measure(self.as_mut().unwrap(), available_space, cx) + } + + fn draw( + &mut self, + origin: Point, + available_space: Size, + cx: &mut WindowContext, + ) { + DrawableElement::draw(self.take().unwrap(), origin, available_space, cx); + } +} + +pub struct AnyElement(Box); + +impl AnyElement { + pub fn new(element: E) -> Self + where + E: 'static + Element, + E::State: Any, + { + AnyElement(Box::new(Some(DrawableElement::new(element))) as Box) + } + + pub fn layout(&mut self, cx: &mut WindowContext) -> LayoutId { + self.0.layout(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, + cx: &mut WindowContext, + ) -> Size { + self.0.measure(available_space, cx) + } + + /// Initializes this element and performs layout in the available space, then paints it at the given origin. + pub fn draw( + mut self, + origin: Point, + available_space: Size, + cx: &mut WindowContext, + ) { + 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 { AnyElement::new(self) } -} -impl Component for F -where - V: 'static, - E: 'static + Component, - F: FnOnce(&mut V, &mut ViewContext<'_, V>) -> E + 'static, -{ - fn render(self) -> AnyElement { - AnyElement::new(Some(self)) + pub fn inner_id(&self) -> Option { + self.0.element_id() } } + +impl Element for AnyElement { + type State = (); + + fn layout( + &mut self, + _: Option, + cx: &mut WindowContext, + ) -> (LayoutId, Self::State) { + let layout_id = self.layout(cx); + (layout_id, ()) + } + + fn paint(self, _: Bounds, _: &mut Self::State, cx: &mut WindowContext) { + self.paint(cx); + } +} + +impl IntoElement for AnyElement { + type Element = Self; + + fn element_id(&self) -> Option { + None + } + + fn into_element(self) -> Self::Element { + self + } +} + +// impl Element for Option +// where +// V: 'static, +// E: Element, +// F: FnOnce(&mut V, &mut WindowContext<'_, V>) -> E + 'static, +// { +// type State = Option; + +// fn element_id(&self) -> Option { +// None +// } + +// fn layout( +// &mut self, +// _: Option, +// cx: &mut WindowContext, +// ) -> (LayoutId, Self::State) { +// let render = self.take().unwrap(); +// let mut element = (render)(view_state, cx).into_any(); +// let layout_id = element.layout(view_state, cx); +// (layout_id, Some(element)) +// } + +// fn paint( +// self, +// _bounds: Bounds, +// rendered_element: &mut Self::State, +// cx: &mut WindowContext, +// ) { +// rendered_element.take().unwrap().paint(view_state, cx); +// } +// } + +// impl RenderOnce for Option +// where +// V: 'static, +// E: Element, +// F: FnOnce(&mut V, &mut WindowContext) -> E + 'static, +// { +// type Element = Self; + +// fn render(self) -> Self::Element { +// self +// } +// } diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index a37e3dee2a..406f2ea311 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -1,9 +1,9 @@ use crate::{ point, px, Action, AnyDrag, AnyElement, AnyTooltip, AnyView, AppContext, BorrowAppContext, - BorrowWindow, Bounds, ClickEvent, Component, DispatchPhase, Element, ElementId, FocusEvent, - FocusHandle, KeyContext, KeyDownEvent, KeyUpEvent, LayoutId, MouseButton, MouseDownEvent, - MouseMoveEvent, MouseUpEvent, ParentComponent, Pixels, Point, Render, ScrollWheelEvent, - SharedString, Size, Style, StyleRefinement, Styled, Task, View, ViewContext, Visibility, + BorrowWindow, Bounds, ClickEvent, DispatchPhase, Element, ElementId, FocusEvent, FocusHandle, + IntoElement, KeyContext, KeyDownEvent, KeyUpEvent, LayoutId, MouseButton, MouseDownEvent, + MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Point, Render, ScrollWheelEvent, + SharedString, 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 InteractiveComponent: 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 InteractiveComponent: 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 InteractiveComponent: 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 InteractiveComponent: 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 InteractiveComponent: 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 InteractiveComponent: 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 InteractiveComponent: 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 InteractiveComponent: 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 InteractiveComponent: 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 InteractiveComponent: 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 StatefulInteractiveComponent>: InteractiveComponent { - 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,23 +378,17 @@ pub trait StatefulInteractiveComponent>: InteractiveCo 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, @@ -387,15 +397,14 @@ pub trait StatefulInteractiveComponent>: InteractiveCo 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 StatefulInteractiveComponent>: InteractiveCo 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 StatefulInteractiveComponent>: InteractiveCo 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 FocusableComponent: InteractiveComponent { +pub trait FocusableElement: InteractiveElement { fn focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self where Self: Sized, @@ -442,49 +447,41 @@ pub trait FocusableComponent: InteractiveComponent { 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 FocusableComponent: InteractiveComponent { .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,98 +514,80 @@ pub trait FocusableComponent: InteractiveComponent { .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 InteractiveComponent for Div { - fn interactivity(&mut self) -> &mut Interactivity { +impl InteractiveElement for Div { + fn interactivity(&mut self) -> &mut Interactivity { &mut self.interactivity } } -impl ParentComponent 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 { - type ElementState = DivState; - - fn element_id(&self) -> Option { - self.interactivity.element_id.clone() - } +impl Element for Div { + type State = DivState; fn layout( &mut self, - view_state: &mut V, - element_state: Option, - cx: &mut ViewContext, - ) -> (LayoutId, Self::ElementState) { + element_state: Option, + cx: &mut WindowContext, + ) -> (LayoutId, Self::State) { let mut child_layout_ids = SmallVec::new(); let mut interactivity = mem::take(&mut self.interactivity); let (layout_id, interactive_state) = interactivity.layout( @@ -622,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()) }) @@ -639,11 +615,10 @@ impl Element for Div { } fn paint( - &mut self, + self, bounds: Bounds, - view_state: &mut V, - element_state: &mut Self::ElementState, - cx: &mut ViewContext, + element_state: &mut Self::State, + cx: &mut WindowContext, ) { let mut child_min = point(Pixels::MAX, Pixels::MAX); let mut child_max = Point::default(); @@ -658,8 +633,7 @@ impl Element for Div { (child_max - child_min).into() }; - let mut interactivity = mem::take(&mut self.interactivity); - interactivity.paint( + self.interactivity.paint( bounds, content_size, &mut element_state.interactive_state, @@ -679,8 +653,8 @@ impl Element for Div { cx.with_text_style(style.text_style().cloned(), |cx| { cx.with_content_mask(style.overflow_mask(bounds), |cx| { cx.with_element_offset(scroll_offset, |cx| { - for child in &mut self.children { - child.paint(view_state, cx); + for child in self.children { + child.paint(cx); } }) }) @@ -689,13 +663,18 @@ impl Element for Div { }) }, ); - self.interactivity = interactivity; } } -impl Component for Div { - fn render(self) -> AnyElement { - AnyElement::new(self) +impl IntoElement for Div { + type Element = Self; + + fn element_id(&self) -> Option { + self.interactivity.element_id.clone() + } + + fn into_element(self) -> Self::Element { + self } } @@ -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(); @@ -770,12 +746,12 @@ where } pub fn paint( - &mut self, + mut self, 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); @@ -787,26 +763,26 @@ where } for listener in self.mouse_down_listeners.drain(..) { - cx.on_mouse_event(move |state, event: &MouseDownEvent, phase, cx| { - listener(state, event, &bounds, phase, cx); + cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| { + listener(event, &bounds, phase, cx); }) } for listener in self.mouse_up_listeners.drain(..) { - cx.on_mouse_event(move |state, event: &MouseUpEvent, phase, cx| { - listener(state, event, &bounds, phase, cx); + cx.on_mouse_event(move |event: &MouseUpEvent, phase, cx| { + listener(event, &bounds, phase, cx); }) } for listener in self.mouse_move_listeners.drain(..) { - cx.on_mouse_event(move |state, event: &MouseMoveEvent, phase, cx| { - listener(state, event, &bounds, phase, cx); + cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| { + listener(event, &bounds, phase, cx); }) } for listener in self.scroll_wheel_listeners.drain(..) { - cx.on_mouse_event(move |state, event: &ScrollWheelEvent, phase, cx| { - listener(state, event, &bounds, phase, cx); + 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(); @@ -841,7 +817,7 @@ where if cx.active_drag.is_some() { let drop_listeners = mem::take(&mut self.drop_listeners); - cx.on_mouse_event(move |view, event: &MouseUpEvent, phase, cx| { + 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; @@ -1063,27 +1039,25 @@ where element_state.focus_handle.clone(), |_, cx| { for listener in self.key_down_listeners.drain(..) { - cx.on_key_event(move |state, event: &KeyDownEvent, phase, cx| { - listener(state, event, phase, cx); + cx.on_key_event(move |event: &KeyDownEvent, phase, cx| { + listener(event, phase, cx); }) } for listener in self.key_up_listeners.drain(..) { - cx.on_key_event(move |state, event: &KeyUpEvent, phase, cx| { - listener(state, event, phase, cx); + cx.on_key_event(move |event: &KeyUpEvent, phase, cx| { + listener(event, phase, cx); }) } - for (action_type, listener) in self.action_listeners.drain(..) { + for (action_type, listener) in self.action_listeners { cx.on_action(action_type, listener) } if let Some(focus_handle) = element_state.focus_handle.as_ref() { - for listener in self.focus_listeners.drain(..) { + 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> FocusableComponent for Focusable {} +impl FocusableElement for Focusable {} -impl InteractiveComponent for Focusable +impl InteractiveElement for Focusable where - V: 'static, - E: InteractiveComponent, + E: InteractiveElement, { - fn interactivity(&mut self) -> &mut Interactivity { + fn interactivity(&mut self) -> &mut Interactivity { self.element.interactivity() } } -impl> StatefulInteractiveComponent - 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,65 +1259,55 @@ where } } -impl Element for Focusable +impl Element for Focusable where - V: 'static, - E: Element, + E: Element, { - type ElementState = E::ElementState; + type State = E::State; + + fn layout( + &mut self, + state: Option, + cx: &mut WindowContext, + ) -> (LayoutId, Self::State) { + self.element.layout(state, cx) + } + + fn paint(self, bounds: Bounds, state: &mut Self::State, cx: &mut WindowContext) { + self.element.paint(bounds, state, cx) + } +} + +impl IntoElement for Focusable +where + E: Element, +{ + type Element = E; fn element_id(&self) -> Option { self.element.element_id() } - fn layout( - &mut self, - view_state: &mut V, - element_state: Option, - cx: &mut ViewContext, - ) -> (LayoutId, Self::ElementState) { - self.element.layout(view_state, element_state, cx) - } - - fn paint( - &mut self, - bounds: Bounds, - view_state: &mut V, - element_state: &mut Self::ElementState, - cx: &mut ViewContext, - ) { - self.element.paint(bounds, view_state, element_state, cx); + fn into_element(self) -> Self::Element { + self.element } } -impl Component for Focusable +impl ParentElement for Focusable where - V: 'static, - E: 'static + Element, + E: ParentElement, { - fn render(self) -> AnyElement { - AnyElement::new(self) - } -} - -impl ParentComponent for Focusable -where - V: 'static, - E: ParentComponent, -{ - 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 { @@ -1357,73 +1315,63 @@ where } } -impl StatefulInteractiveComponent for Stateful +impl StatefulInteractiveElement for Stateful where - V: 'static, - E: Element, - Self: InteractiveComponent, + E: Element, + Self: InteractiveElement, { } -impl InteractiveComponent for Stateful +impl InteractiveElement for Stateful where - V: 'static, - E: InteractiveComponent, + E: InteractiveElement, { - fn interactivity(&mut self) -> &mut Interactivity { + fn interactivity(&mut self) -> &mut Interactivity { self.element.interactivity() } } -impl> FocusableComponent for Stateful {} +impl FocusableElement for Stateful {} -impl Element for Stateful +impl Element for Stateful where - V: 'static, - E: Element, + E: Element, { - type ElementState = E::ElementState; + type State = E::State; + + fn layout( + &mut self, + state: Option, + cx: &mut WindowContext, + ) -> (LayoutId, Self::State) { + self.element.layout(state, cx) + } + + fn paint(self, bounds: Bounds, state: &mut Self::State, cx: &mut WindowContext) { + self.element.paint(bounds, state, cx) + } +} + +impl IntoElement for Stateful +where + E: Element, +{ + type Element = Self; fn element_id(&self) -> Option { self.element.element_id() } - fn layout( - &mut self, - view_state: &mut V, - element_state: Option, - cx: &mut ViewContext, - ) -> (LayoutId, Self::ElementState) { - self.element.layout(view_state, element_state, cx) - } - - fn paint( - &mut self, - bounds: Bounds, - view_state: &mut V, - element_state: &mut Self::ElementState, - cx: &mut ViewContext, - ) { - self.element.paint(bounds, view_state, element_state, cx) + fn into_element(self) -> Self::Element { + self } } -impl Component for Stateful +impl ParentElement for Stateful where - V: 'static, - E: 'static + Element, + E: ParentElement, { - fn render(self) -> AnyElement { - AnyElement::new(self) - } -} - -impl ParentComponent for Stateful -where - V: 'static, - E: ParentComponent, -{ - 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 5376c40012..2aece17b47 100644 --- a/crates/gpui2/src/elements/img.rs +++ b/crates/gpui2/src/elements/img.rs @@ -1,70 +1,83 @@ +use std::sync::Arc; + use crate::{ - AnyElement, BorrowWindow, Bounds, Component, Element, InteractiveComponent, - InteractiveElementState, Interactivity, LayoutId, Pixels, SharedString, StyleRefinement, - Styled, ViewContext, + Bounds, Element, ImageData, InteractiveElement, InteractiveElementState, Interactivity, + IntoElement, LayoutId, Pixels, SharedString, StyleRefinement, Styled, WindowContext, }; use futures::FutureExt; use util::ResultExt; -pub struct Img { - interactivity: Interactivity, - uri: Option, +#[derive(Clone, Debug)] +pub enum ImageSource { + /// Image content will be loaded from provided URI at render time. + Uri(SharedString), + Data(Arc), +} + +impl From for ImageSource { + fn from(value: SharedString) -> Self { + Self::Uri(value) + } +} + +impl From> for ImageSource { + fn from(value: Arc) -> Self { + Self::Data(value) + } +} + +pub struct Img { + interactivity: Interactivity, + source: Option, grayscale: bool, } -pub fn img() -> Img { +pub fn img() -> Img { Img { interactivity: Interactivity::default(), - uri: None, + source: None, grayscale: false, } } -impl Img -where - V: 'static, -{ +impl Img { pub fn uri(mut self, uri: impl Into) -> Self { - self.uri = Some(uri.into()); + self.source = Some(ImageSource::from(uri.into())); + self + } + pub fn data(mut self, data: Arc) -> Self { + self.source = Some(ImageSource::from(data)); self } + pub fn source(mut self, source: impl Into) -> Self { + self.source = Some(source.into()); + self + } pub fn grayscale(mut self, grayscale: bool) -> Self { self.grayscale = grayscale; self } } -impl Component for Img { - fn render(self) -> AnyElement { - AnyElement::new(self) - } -} - -impl Element for Img { - type ElementState = InteractiveElementState; - - fn element_id(&self) -> Option { - self.interactivity.element_id.clone() - } +impl Element for Img { + type State = InteractiveElementState; fn layout( &mut self, - _view_state: &mut V, - element_state: Option, - cx: &mut ViewContext, - ) -> (LayoutId, Self::ElementState) { + element_state: Option, + cx: &mut WindowContext, + ) -> (LayoutId, Self::State) { self.interactivity.layout(element_state, cx, |style, cx| { cx.request_layout(&style, None) }) } fn paint( - &mut self, + self, bounds: Bounds, - _view_state: &mut V, - element_state: &mut Self::ElementState, - cx: &mut ViewContext, + element_state: &mut Self::State, + cx: &mut WindowContext, ) { self.interactivity.paint( bounds, @@ -74,42 +87,59 @@ impl Element for Img { |style, _scroll_offset, cx| { let corner_radii = style.corner_radii; - if let Some(uri) = self.uri.clone() { - // eprintln!(">>> image_cache.get({uri}"); - let image_future = cx.image_cache.get(uri.clone()); - // eprintln!("<<< image_cache.get({uri}"); - if let Some(data) = image_future - .clone() - .now_or_never() - .and_then(|result| result.ok()) - { - let corner_radii = corner_radii.to_pixels(bounds.size, cx.rem_size()); - cx.with_z_index(1, |cx| { - cx.paint_image(bounds, corner_radii, data, self.grayscale) - .log_err() - }); - } else { - cx.spawn(|_, mut cx| async move { - if image_future.await.ok().is_some() { - cx.on_next_frame(|cx| cx.notify()); + if let Some(source) = self.source { + let image = match source { + ImageSource::Uri(uri) => { + let image_future = cx.image_cache.get(uri.clone()); + if let Some(data) = image_future + .clone() + .now_or_never() + .and_then(|result| result.ok()) + { + data + } else { + cx.spawn(|mut cx| async move { + if image_future.await.ok().is_some() { + cx.on_next_frame(|cx| cx.notify()); + } + }) + .detach(); + return; } - }) - .detach() - } + } + ImageSource::Data(image) => image, + }; + let corner_radii = corner_radii.to_pixels(bounds.size, cx.rem_size()); + cx.with_z_index(1, |cx| { + cx.paint_image(bounds, corner_radii, image, self.grayscale) + .log_err() + }); } }, ) } } -impl Styled for Img { +impl IntoElement for Img { + type Element = Self; + + fn element_id(&self) -> Option { + self.interactivity.element_id.clone() + } + + fn into_element(self) -> Self::Element { + self + } +} + +impl Styled for Img { fn style(&mut self) -> &mut StyleRefinement { &mut self.interactivity.base_style } } -impl InteractiveComponent 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 14a8048d39..1a8084ec0a 100644 --- a/crates/gpui2/src/elements/overlay.rs +++ b/crates/gpui2/src/elements/overlay.rs @@ -2,16 +2,16 @@ use smallvec::SmallVec; use taffy::style::{Display, Position}; use crate::{ - point, AnyElement, BorrowWindow, Bounds, Component, Element, LayoutId, ParentComponent, Pixels, - Point, Size, Style, + point, AnyElement, BorrowWindow, Bounds, Element, IntoElement, LayoutId, ParentElement, Pixels, + Point, 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,35 +51,24 @@ impl Overlay { } } -impl ParentComponent 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 Component for Overlay { - fn render(self) -> AnyElement { - AnyElement::new(self) - } -} - -impl Element for Overlay { - type ElementState = OverlayState; - - fn element_id(&self) -> Option { - None - } +impl Element for Overlay { + type State = OverlayState; fn layout( &mut self, - view_state: &mut V, - _: Option, - cx: &mut crate::ViewContext, - ) -> (crate::LayoutId, Self::ElementState) { + _: Option, + 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(); @@ -92,11 +81,10 @@ impl Element for Overlay { } fn paint( - &mut self, + self, bounds: crate::Bounds, - view_state: &mut V, - element_state: &mut Self::ElementState, - cx: &mut crate::ViewContext, + element_state: &mut Self::State, + cx: &mut WindowContext, ) { if element_state.child_layout_ids.is_empty() { return; @@ -117,6 +105,7 @@ impl Element for Overlay { origin: Point::zero(), size: cx.viewport_size(), }; + dbg!(limits); match self.fit_mode { OverlayFitMode::SnapToWindow => { @@ -156,13 +145,27 @@ impl Element for Overlay { } cx.with_element_offset(desired.origin - bounds.origin, |cx| { - for child in &mut self.children { - child.paint(view_state, cx); - } + cx.break_content_mask(|cx| { + for child in self.children { + child.paint(cx); + } + }) }) } } +impl IntoElement for Overlay { + type Element = Self; + + fn element_id(&self) -> Option { + None + } + + fn into_element(self) -> Self::Element { + self + } +} + enum Axis { Horizontal, Vertical, diff --git a/crates/gpui2/src/elements/svg.rs b/crates/gpui2/src/elements/svg.rs index c1c7691fbf..aba31686f5 100644 --- a/crates/gpui2/src/elements/svg.rs +++ b/crates/gpui2/src/elements/svg.rs @@ -1,60 +1,43 @@ use crate::{ - AnyElement, Bounds, Component, Element, ElementId, InteractiveComponent, - InteractiveElementState, Interactivity, LayoutId, Pixels, SharedString, StyleRefinement, - Styled, ViewContext, + Bounds, Element, ElementId, InteractiveElement, InteractiveElementState, Interactivity, + IntoElement, LayoutId, Pixels, 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 Component for Svg { - fn render(self) -> AnyElement { - AnyElement::new(self) - } -} - -impl Element for Svg { - type ElementState = InteractiveElementState; - - fn element_id(&self) -> Option { - self.interactivity.element_id.clone() - } +impl Element for Svg { + type State = InteractiveElementState; fn layout( &mut self, - _view_state: &mut V, - element_state: Option, - cx: &mut ViewContext, - ) -> (LayoutId, Self::ElementState) { + element_state: Option, + cx: &mut WindowContext, + ) -> (LayoutId, Self::State) { self.interactivity.layout(element_state, cx, |style, cx| { cx.request_layout(&style, None) }) } - fn paint( - &mut self, - bounds: Bounds, - _view_state: &mut V, - element_state: &mut Self::ElementState, - cx: &mut ViewContext, - ) where + fn paint(self, bounds: Bounds, element_state: &mut Self::State, cx: &mut WindowContext) + where Self: Sized, { self.interactivity @@ -66,14 +49,26 @@ impl Element for Svg { } } -impl Styled for Svg { +impl IntoElement for Svg { + type Element = Self; + + fn element_id(&self) -> Option { + self.interactivity.element_id.clone() + } + + fn into_element(self) -> Self::Element { + self + } +} + +impl Styled for Svg { fn style(&mut self) -> &mut StyleRefinement { &mut self.interactivity.base_style } } -impl InteractiveComponent 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 e3c63523e1..a0715b81a9 100644 --- a/crates/gpui2/src/elements/text.rs +++ b/crates/gpui2/src/elements/text.rs @@ -1,74 +1,177 @@ use crate::{ - AnyElement, BorrowWindow, Bounds, Component, Element, ElementId, LayoutId, Pixels, - SharedString, Size, TextRun, ViewContext, WrappedLine, + Bounds, DispatchPhase, Element, ElementId, IntoElement, LayoutId, MouseDownEvent, MouseUpEvent, + Pixels, Point, SharedString, Size, TextRun, WhiteSpace, WindowContext, WrappedLine, }; +use anyhow::anyhow; use parking_lot::{Mutex, MutexGuard}; use smallvec::SmallVec; -use std::{cell::Cell, rc::Rc, sync::Arc}; +use std::{cell::Cell, ops::Range, rc::Rc, sync::Arc}; use util::ResultExt; -pub struct Text { +impl Element for &'static str { + type State = TextState; + + fn layout( + &mut self, + _: Option, + 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, state: &mut TextState, cx: &mut WindowContext) { + state.paint(bounds, self, cx) + } +} + +impl IntoElement for &'static str { + type Element = Self; + + fn element_id(&self) -> Option { + None + } + + fn into_element(self) -> Self::Element { + self + } +} + +impl Element for SharedString { + type State = TextState; + + fn layout( + &mut self, + _: Option, + 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, state: &mut TextState, cx: &mut WindowContext) { + let text_str: &str = self.as_ref(); + state.paint(bounds, text_str, cx) + } +} + +impl IntoElement for SharedString { + type Element = Self; + + fn element_id(&self) -> Option { + None + } + + fn into_element(self) -> Self::Element { + self + } +} + +/// Renders text with runs of different styles. +/// +/// Callers are responsible for setting the correct style for each run. +/// For text with a uniform style, you can usually avoid calling this constructor +/// and just pass text directly. +pub struct StyledText { text: SharedString, runs: Option>, } -impl Text { - /// Renders text with runs of different styles. - /// - /// Callers are responsible for setting the correct style for each run. - /// For text with a uniform style, you can usually avoid calling this constructor - /// and just pass text directly. - pub fn styled(text: SharedString, runs: Vec) -> Self { - Text { - text, - runs: Some(runs), +impl StyledText { + pub fn new(text: impl Into) -> Self { + StyledText { + text: text.into(), + runs: None, } } -} -impl Component for Text { - fn render(self) -> AnyElement { - AnyElement::new(self) + pub fn with_runs(mut self, runs: Vec) -> Self { + self.runs = Some(runs); + self } } -impl Element for Text { - type ElementState = TextState; +impl Element for StyledText { + type State = TextState; + + fn layout( + &mut self, + _: Option, + cx: &mut WindowContext, + ) -> (LayoutId, Self::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, state: &mut Self::State, cx: &mut WindowContext) { + state.paint(bounds, &self.text, cx) + } +} + +impl IntoElement for StyledText { + type Element = Self; fn element_id(&self) -> Option { None } + fn into_element(self) -> Self::Element { + self + } +} + +#[derive(Default, Clone)] +pub struct TextState(Arc>>); + +struct TextStateInner { + lines: SmallVec<[WrappedLine; 1]>, + line_height: Pixels, + wrap_width: Option, + size: Option>, +} + +impl TextState { + fn lock(&self) -> MutexGuard> { + self.0.lock() + } + fn layout( &mut self, - _view: &mut V, - element_state: Option, - cx: &mut ViewContext, - ) -> (LayoutId, Self::ElementState) { - let element_state = element_state.unwrap_or_default(); + text: SharedString, + runs: Option>, + cx: &mut WindowContext, + ) -> LayoutId { 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 text = SharedString::from(text); let rem_size = cx.rem_size(); - let runs = if let Some(runs) = self.runs.take() { + let runs = if let Some(runs) = runs { 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(); + 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() @@ -80,10 +183,7 @@ impl Element for Text { 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 { @@ -100,7 +200,7 @@ impl Element for Text { 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 { @@ -114,20 +214,14 @@ impl Element for Text { } }); - (layout_id, element_state) + layout_id } - fn paint( - &mut self, - bounds: Bounds, - _: &mut V, - element_state: &mut Self::ElementState, - cx: &mut ViewContext, - ) { - let element_state = element_state.lock(); + fn paint(&mut self, bounds: Bounds, text: &str, cx: &mut WindowContext) { + let element_state = self.lock(); let element_state = element_state .as_ref() - .ok_or_else(|| anyhow::anyhow!("measurement has not been performed on {}", &self.text)) + .ok_or_else(|| anyhow!("measurement has not been performed on {}", text)) .unwrap(); let line_height = element_state.line_height; @@ -137,108 +231,156 @@ impl Element for Text { line_origin.y += line.size(line_height).height; } } -} -#[derive(Default, Clone)] -pub struct TextState(Arc>>); + fn index_for_position(&self, bounds: Bounds, position: Point) -> Option { + if !bounds.contains_point(&position) { + return None; + } -impl TextState { - fn lock(&self) -> MutexGuard> { - self.0.lock() + let element_state = self.lock(); + let element_state = element_state + .as_ref() + .expect("measurement has not been performed"); + + let line_height = element_state.line_height; + let mut line_origin = bounds.origin; + let mut line_start_ix = 0; + for line in &element_state.lines { + let line_bottom = line_origin.y + line.size(line_height).height; + if position.y > line_bottom { + line_origin.y = line_bottom; + line_start_ix += line.len() + 1; + } else { + let position_within_line = position - line_origin; + let index_within_line = + line.index_for_position(position_within_line, line_height)?; + return Some(line_start_ix + index_within_line); + } + } + + None } } -struct TextStateInner { - lines: SmallVec<[WrappedLine; 1]>, - line_height: Pixels, - wrap_width: Option, - size: Option>, +pub struct InteractiveText { + element_id: ElementId, + text: StyledText, + click_listener: Option)>>, } -struct InteractiveText { - id: ElementId, - text: Text, +struct InteractiveTextClickEvent { + mouse_down_index: usize, + mouse_up_index: usize, } -struct InteractiveTextState { +pub struct InteractiveTextState { text_state: TextState, - clicked_range_ixs: Rc>>, + mouse_down_index: Rc>>, } -impl Element for InteractiveText { - type ElementState = InteractiveTextState; - - fn element_id(&self) -> Option { - Some(self.id.clone()) +impl InteractiveText { + pub fn new(id: impl Into, text: StyledText) -> Self { + Self { + element_id: id.into(), + text, + click_listener: None, + } } + pub fn on_click( + mut self, + ranges: Vec>, + listener: impl Fn(usize, &mut WindowContext<'_>) + 'static, + ) -> Self { + self.click_listener = Some(Box::new(move |event, cx| { + for (range_ix, range) in ranges.iter().enumerate() { + if range.contains(&event.mouse_down_index) && range.contains(&event.mouse_up_index) + { + listener(range_ix, cx); + } + } + })); + self + } +} + +impl Element for InteractiveText { + type State = InteractiveTextState; + fn layout( &mut self, - view_state: &mut V, - element_state: Option, - cx: &mut ViewContext, - ) -> (LayoutId, Self::ElementState) { + state: Option, + cx: &mut WindowContext, + ) -> (LayoutId, Self::State) { if let Some(InteractiveTextState { - text_state, - clicked_range_ixs, - }) = element_state + mouse_down_index, .. + }) = state { - let (layout_id, text_state) = self.text.layout(view_state, Some(text_state), cx); + let (layout_id, text_state) = self.text.layout(None, cx); let element_state = InteractiveTextState { text_state, - clicked_range_ixs, + mouse_down_index, }; (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(), + mouse_down_index: Rc::default(), }; (layout_id, element_state) } } - fn paint( - &mut self, - bounds: Bounds, - view_state: &mut V, - element_state: &mut Self::ElementState, - cx: &mut ViewContext, - ) { - self.text - .paint(bounds, view_state, &mut element_state.text_state, cx) + fn paint(self, bounds: Bounds, state: &mut Self::State, cx: &mut WindowContext) { + if let Some(click_listener) = self.click_listener { + let text_state = state.text_state.clone(); + let mouse_down = state.mouse_down_index.clone(); + if let Some(mouse_down_index) = mouse_down.get() { + cx.on_mouse_event(move |event: &MouseUpEvent, phase, cx| { + if phase == DispatchPhase::Bubble { + if let Some(mouse_up_index) = + text_state.index_for_position(bounds, event.position) + { + click_listener( + InteractiveTextClickEvent { + mouse_down_index, + mouse_up_index, + }, + cx, + ) + } + + mouse_down.take(); + cx.notify(); + } + }); + } else { + cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| { + if phase == DispatchPhase::Bubble { + if let Some(mouse_down_index) = + text_state.index_for_position(bounds, event.position) + { + mouse_down.set(Some(mouse_down_index)); + cx.notify(); + } + } + }); + } + } + + self.text.paint(bounds, &mut state.text_state, cx) } } -impl Component for SharedString { - fn render(self) -> AnyElement { - Text { - text: self, - runs: None, - } - .render() - } -} +impl IntoElement for InteractiveText { + type Element = Self; -impl Component for &'static str { - fn render(self) -> AnyElement { - Text { - text: self.into(), - runs: None, - } - .render() + fn element_id(&self) -> Option { + Some(self.element_id.clone()) } -} -// TODO: Figure out how to pass `String` to `child` without this. -// This impl doesn't exist in the `gpui2` crate. -impl Component for String { - fn render(self) -> AnyElement { - Text { - text: self.into(), - runs: None, - } - .render() + fn into_element(self) -> Self::Element { + self } } diff --git a/crates/gpui2/src/elements/uniform_list.rs b/crates/gpui2/src/elements/uniform_list.rs index 773f9ec8aa..8e61f247bd 100644 --- a/crates/gpui2/src/elements/uniform_list.rs +++ b/crates/gpui2/src/elements/uniform_list.rs @@ -1,61 +1,60 @@ use crate::{ - point, px, size, AnyElement, AvailableSpace, BorrowWindow, Bounds, Component, Element, - ElementId, InteractiveComponent, InteractiveElementState, Interactivity, LayoutId, Pixels, - Point, Size, StyleRefinement, Styled, ViewContext, + point, px, size, AnyElement, AvailableSpace, BorrowWindow, Bounds, ContentMask, Element, + ElementId, InteractiveElement, InteractiveElementState, Interactivity, IntoElement, LayoutId, + Pixels, Point, Render, Size, StyleRefinement, Styled, View, ViewContext, WindowContext, }; use smallvec::SmallVec; -use std::{cell::RefCell, cmp, mem, ops::Range, rc::Rc}; +use std::{cell::RefCell, cmp, ops::Range, rc::Rc}; 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( +/// uniform_list will only render the visible subset of items. +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, - C: Component, + R: IntoElement, + 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.into_any_element()) + .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.render()) - .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,29 +100,24 @@ pub struct UniformListState { item_size: Size, } -impl Element for UniformList { - type ElementState = UniformListState; - - fn element_id(&self) -> Option { - Some(self.id.clone()) - } +impl Element for UniformList { + type State = UniformListState; fn layout( &mut self, - view_state: &mut V, - element_state: Option, - cx: &mut ViewContext, - ) -> (LayoutId, Self::ElementState) { + 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, @@ -159,11 +153,10 @@ impl Element for UniformList { } fn paint( - &mut self, + self, bounds: Bounds, - view_state: &mut V, - element_state: &mut Self::ElementState, - cx: &mut ViewContext, + element_state: &mut Self::State, + cx: &mut WindowContext, ) { let style = self.interactivity @@ -183,14 +176,15 @@ impl Element for UniformList { height: item_size.height * self.item_count, }; - let mut interactivity = mem::take(&mut self.interactivity); let shared_scroll_offset = element_state .interactive .scroll_offset .get_or_insert_with(Rc::default) .clone(); - interactivity.paint( + let item_height = self.measure_item(Some(padded_bounds.size.width), cx).height; + + self.interactivity.paint( bounds, content_size, &mut element_state.interactive, @@ -209,9 +203,6 @@ impl Element for UniformList { style.paint(bounds, cx); if self.item_count > 0 { - let item_height = self - .measure_item(view_state, Some(padded_bounds.size.width), cx) - .height; if let Some(scroll_handle) = self.scroll_handle.clone() { scroll_handle.0.borrow_mut().replace(ScrollHandleState { item_height, @@ -219,58 +210,64 @@ impl Element for UniformList { scroll_offset: shared_scroll_offset, }); } - let visible_item_count = if item_height > px(0.) { - (padded_bounds.size.height / item_height).ceil() as usize + 1 - } else { - 0 - }; let first_visible_element_ix = (-scroll_offset.y / item_height).floor() as usize; + let last_visible_element_ix = + ((-scroll_offset.y + padded_bounds.size.height) / item_height).ceil() + as usize; let visible_range = first_visible_element_ix - ..cmp::min( - first_visible_element_ix + visible_item_count, - self.item_count, - ); + ..cmp::min(last_visible_element_ix, self.item_count); - let mut 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.iter_mut().zip(visible_range) { - let item_origin = padded_bounds.origin - + point(px(0.), item_height * ix + scroll_offset.y); - let available_space = size( - AvailableSpace::Definite(padded_bounds.size.width), - AvailableSpace::Definite(item_height), - ); - item.draw(item_origin, available_space, view_state, cx); - } + let content_mask = ContentMask { + bounds: padded_bounds, + }; + cx.with_content_mask(Some(content_mask), |cx| { + for (item, ix) in items.into_iter().zip(visible_range) { + let item_origin = padded_bounds.origin + + point(px(0.), item_height * ix + scroll_offset.y); + let available_space = size( + AvailableSpace::Definite(padded_bounds.size.width), + AvailableSpace::Definite(item_height), + ); + item.draw(item_origin, available_space, cx); + } + }); }); } }) }, ); - self.interactivity = interactivity; } } -impl UniformList { +impl IntoElement for UniformList { + type Element = Self; + + fn element_id(&self) -> Option { + Some(self.id.clone()) + } + + fn into_element(self) -> Self::Element { + self + } +} + +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| { @@ -278,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 { @@ -287,14 +284,8 @@ impl UniformList { } } -impl InteractiveComponent for UniformList { - fn interactivity(&mut self) -> &mut crate::Interactivity { +impl InteractiveElement for UniformList { + fn interactivity(&mut self) -> &mut crate::Interactivity { &mut self.interactivity } } - -impl Component for UniformList { - fn render(self) -> AnyElement { - AnyElement::new(self) - } -} diff --git a/crates/gpui2/src/gpui2.rs b/crates/gpui2/src/gpui2.rs index a24509386b..984859f1b0 100644 --- a/crates/gpui2/src/gpui2.rs +++ b/crates/gpui2/src/gpui2.rs @@ -78,8 +78,6 @@ use std::{ }; use taffy::TaffyLayoutEngine; -type AnyBox = Box; - pub trait Context { type Result; @@ -136,7 +134,7 @@ pub trait VisualContext: Context { build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V, ) -> Self::Result> where - V: 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 140f724417..8592eeffeb 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 80a89ef625..2d48ec5a11 100644 --- a/crates/gpui2/src/interactive.rs +++ b/crates/gpui2/src/interactive.rs @@ -1,6 +1,6 @@ use crate::{ - div, point, Component, Div, FocusHandle, Keystroke, Modifiers, Pixels, Point, Render, - ViewContext, + div, point, Div, Element, FocusHandle, IntoElement, Keystroke, Modifiers, Pixels, Point, + Render, ViewContext, }; use smallvec::SmallVec; use std::{any::Any, fmt::Debug, marker::PhantomData, ops::Deref, path::PathBuf}; @@ -64,24 +64,24 @@ pub struct Drag where R: Fn(&mut V, &mut ViewContext) -> E, V: 'static, - E: Component<()>, + E: IntoElement, { 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: Component<()>, + 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(), } } } @@ -194,7 +194,7 @@ impl Deref for MouseExitEvent { pub struct ExternalPaths(pub(crate) SmallVec<[PathBuf; 2]>); impl Render for ExternalPaths { - type Element = Div; + 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,8 +286,8 @@ pub struct FocusEvent { #[cfg(test)] mod test { use crate::{ - self as gpui, div, Component, Div, FocusHandle, InteractiveComponent, KeyBinding, - Keystroke, ParentComponent, Render, Stateful, TestAppContext, ViewContext, VisualContext, + self as gpui, div, Div, FocusHandle, InteractiveElement, IntoElement, KeyBinding, + Keystroke, ParentElement, Render, Stateful, TestAppContext, VisualContext, }; struct TestView { @@ -299,20 +299,24 @@ mod test { actions!(TestAction); impl Render for TestView { - type Element = Stateful>; + 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(|this: &mut Self, _cx: &mut ViewContext| { + .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(&this.focus_handle) - .render() - }), + .track_focus(&self.focus_handle) + .into_element(), + ), ) } } diff --git a/crates/gpui2/src/platform/mac/window.rs b/crates/gpui2/src/platform/mac/window.rs index bb3a659a62..5b72c10851 100644 --- a/crates/gpui2/src/platform/mac/window.rs +++ b/crates/gpui2/src/platform/mac/window.rs @@ -683,6 +683,9 @@ impl Drop for MacWindow { this.executor .spawn(async move { unsafe { + // todo!() this panic()s when you click the red close button + // unless should_close returns false. + // (luckliy in zed it always returns false) window.close(); } }) diff --git a/crates/gpui2/src/prelude.rs b/crates/gpui2/src/prelude.rs index 7c2ad3f07f..90d09b3fc5 100644 --- a/crates/gpui2/src/prelude.rs +++ b/crates/gpui2/src/prelude.rs @@ -1,4 +1,5 @@ pub use crate::{ - BorrowAppContext, BorrowWindow, Component, Context, FocusableComponent, InteractiveComponent, - ParentComponent, Refineable, Render, StatefulInteractiveComponent, Styled, VisualContext, + BorrowAppContext, BorrowWindow, Context, Element, FocusableElement, InteractiveElement, + IntoElement, ParentElement, Refineable, Render, RenderOnce, StatefulInteractiveElement, Styled, + VisualContext, }; diff --git a/crates/gpui2/src/style.rs b/crates/gpui2/src/style.rs index 1b0cabb401..640538fff0 100644 --- a/crates/gpui2/src/style.rs +++ b/crates/gpui2/src/style.rs @@ -1,9 +1,12 @@ +use std::{iter, mem, ops::Range}; + 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 collections::HashSet; use refineable::{Cascade, Refineable}; use smallvec::SmallVec; pub use taffy::style::{ @@ -128,6 +131,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 { @@ -138,7 +148,9 @@ pub struct TextStyle { pub line_height: DefiniteLength, pub font_weight: FontWeight, pub font_style: FontStyle, + pub background_color: Option, pub underline: Option, + pub white_space: WhiteSpace, } impl Default for TextStyle { @@ -151,13 +163,16 @@ impl Default for TextStyle { line_height: phi(), font_weight: FontWeight::default(), font_style: FontStyle::default(), + background_color: None, underline: None, + white_space: WhiteSpace::Normal, } } } impl TextStyle { - pub fn highlight(mut self, style: HighlightStyle) -> Self { + pub fn highlight(mut self, style: impl Into) -> Self { + let style = style.into(); if let Some(weight) = style.font_weight { self.font_weight = weight; } @@ -173,6 +188,10 @@ impl TextStyle { self.color.fade_out(factor); } + if let Some(background_color) = style.background_color { + self.background_color = Some(background_color); + } + if let Some(underline) = style.underline { self.underline = Some(underline); } @@ -203,7 +222,7 @@ impl TextStyle { style: self.font_style, }, color: self.color, - background_color: None, + background_color: self.background_color, underline: self.underline.clone(), } } @@ -214,6 +233,7 @@ pub struct HighlightStyle { pub color: Option, pub font_weight: Option, pub font_style: Option, + pub background_color: Option, pub underline: Option, pub fade_out: Option, } @@ -313,7 +333,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| { @@ -432,6 +452,7 @@ impl From<&TextStyle> for HighlightStyle { color: Some(other.color), font_weight: Some(other.font_weight), font_style: Some(other.font_style), + background_color: other.background_color, underline: other.underline.clone(), fade_out: None, } @@ -458,6 +479,10 @@ impl HighlightStyle { self.font_style = other.font_style; } + if other.background_color.is_some() { + self.background_color = other.background_color; + } + if other.underline.is_some() { self.underline = other.underline; } @@ -481,6 +506,24 @@ impl From for HighlightStyle { } } +impl From for HighlightStyle { + fn from(font_weight: FontWeight) -> Self { + Self { + font_weight: Some(font_weight), + ..Default::default() + } + } +} + +impl From for HighlightStyle { + fn from(font_style: FontStyle) -> Self { + Self { + font_style: Some(font_style), + ..Default::default() + } + } +} + impl From for HighlightStyle { fn from(color: Rgba) -> Self { Self { @@ -489,3 +532,140 @@ impl From for HighlightStyle { } } } + +pub fn combine_highlights( + a: impl IntoIterator, HighlightStyle)>, + b: impl IntoIterator, HighlightStyle)>, +) -> impl Iterator, HighlightStyle)> { + let mut endpoints = Vec::new(); + let mut highlights = Vec::new(); + for (range, highlight) in a.into_iter().chain(b) { + if !range.is_empty() { + let highlight_id = highlights.len(); + endpoints.push((range.start, highlight_id, true)); + endpoints.push((range.end, highlight_id, false)); + highlights.push(highlight); + } + } + endpoints.sort_unstable_by_key(|(position, _, _)| *position); + let mut endpoints = endpoints.into_iter().peekable(); + + let mut active_styles = HashSet::default(); + let mut ix = 0; + iter::from_fn(move || { + while let Some((endpoint_ix, highlight_id, is_start)) = endpoints.peek() { + let prev_index = mem::replace(&mut ix, *endpoint_ix); + if ix > prev_index && !active_styles.is_empty() { + let mut current_style = HighlightStyle::default(); + for highlight_id in &active_styles { + current_style.highlight(highlights[*highlight_id]); + } + return Some((prev_index..ix, current_style)); + } + + if *is_start { + active_styles.insert(*highlight_id); + } else { + active_styles.remove(highlight_id); + } + endpoints.next(); + } + None + }) +} + +#[cfg(test)] +mod tests { + use crate::{blue, green, red, yellow}; + + use super::*; + + #[test] + fn test_combine_highlights() { + assert_eq!( + combine_highlights( + [ + (0..5, green().into()), + (4..10, FontWeight::BOLD.into()), + (15..20, yellow().into()), + ], + [ + (2..6, FontStyle::Italic.into()), + (1..3, blue().into()), + (21..23, red().into()), + ] + ) + .collect::>(), + [ + ( + 0..1, + HighlightStyle { + color: Some(green()), + ..Default::default() + } + ), + ( + 1..2, + HighlightStyle { + color: Some(blue()), + ..Default::default() + } + ), + ( + 2..3, + HighlightStyle { + color: Some(blue()), + font_style: Some(FontStyle::Italic), + ..Default::default() + } + ), + ( + 3..4, + HighlightStyle { + color: Some(green()), + font_style: Some(FontStyle::Italic), + ..Default::default() + } + ), + ( + 4..5, + HighlightStyle { + color: Some(green()), + font_weight: Some(FontWeight::BOLD), + font_style: Some(FontStyle::Italic), + ..Default::default() + } + ), + ( + 5..6, + HighlightStyle { + font_weight: Some(FontWeight::BOLD), + font_style: Some(FontStyle::Italic), + ..Default::default() + } + ), + ( + 6..10, + HighlightStyle { + font_weight: Some(FontWeight::BOLD), + ..Default::default() + } + ), + ( + 15..20, + HighlightStyle { + color: Some(yellow()), + ..Default::default() + } + ), + ( + 21..23, + HighlightStyle { + color: Some(red()), + ..Default::default() + } + ) + ] + ); + } +} diff --git a/crates/gpui2/src/styled.rs b/crates/gpui2/src/styled.rs index beaf664dd8..77756154b5 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 { @@ -343,6 +361,13 @@ pub trait Styled: Sized { self } + fn text_bg(mut self, bg: impl Into) -> Self { + self.text_style() + .get_or_insert_with(Default::default) + .background_color = Some(bg.into()); + self + } + fn text_size(mut self, size: impl Into) -> Self { self.text_style() .get_or_insert_with(Default::default) diff --git a/crates/gpui2/src/text_system.rs b/crates/gpui2/src/text_system.rs index b3d7a96aff..440789dd47 100644 --- a/crates/gpui2/src/text_system.rs +++ b/crates/gpui2/src/text_system.rs @@ -196,7 +196,10 @@ impl TextSystem { let mut decoration_runs = SmallVec::<[DecorationRun; 32]>::new(); for run in runs { if let Some(last_run) = decoration_runs.last_mut() { - if last_run.color == run.color && last_run.underline == run.underline { + if last_run.color == run.color + && last_run.underline == run.underline + && last_run.background_color == run.background_color + { last_run.len += run.len as u32; continue; } @@ -204,6 +207,7 @@ impl TextSystem { decoration_runs.push(DecorationRun { len: run.len as u32, color: run.color, + background_color: run.background_color, underline: run.underline.clone(), }); } @@ -254,13 +258,16 @@ impl TextSystem { } if decoration_runs.last().map_or(false, |last_run| { - last_run.color == run.color && last_run.underline == run.underline + last_run.color == run.color + && last_run.underline == run.underline + && last_run.background_color == run.background_color }) { decoration_runs.last_mut().unwrap().len += run_len_within_line as u32; } else { decoration_runs.push(DecorationRun { len: run_len_within_line as u32, color: run.color, + background_color: run.background_color, underline: run.underline.clone(), }); } @@ -283,7 +290,15 @@ impl TextSystem { text: SharedString::from(line_text), }); - line_start = line_end + 1; // Skip `\n` character. + // Skip `\n` character. + line_start = line_end + 1; + if let Some(run) = runs.peek_mut() { + run.len = run.len.saturating_sub(1); + if run.len == 0 { + runs.next(); + } + } + font_runs.clear(); } diff --git a/crates/gpui2/src/text_system/line.rs b/crates/gpui2/src/text_system/line.rs index d05ae9468d..0d15647b88 100644 --- a/crates/gpui2/src/text_system/line.rs +++ b/crates/gpui2/src/text_system/line.rs @@ -1,6 +1,7 @@ use crate::{ - black, point, px, BorrowWindow, Bounds, Hsla, LineLayout, Pixels, Point, Result, SharedString, - UnderlineStyle, WindowContext, WrapBoundary, WrappedLineLayout, + black, point, px, size, transparent_black, BorrowWindow, Bounds, Corners, Edges, Hsla, + LineLayout, Pixels, Point, Result, SharedString, UnderlineStyle, WindowContext, WrapBoundary, + WrappedLineLayout, }; use derive_more::{Deref, DerefMut}; use smallvec::SmallVec; @@ -10,6 +11,7 @@ use std::sync::Arc; pub struct DecorationRun { pub len: u32, pub color: Hsla, + pub background_color: Option, pub underline: Option, } @@ -38,7 +40,6 @@ impl ShapedLine { &self.layout, line_height, &self.decoration_runs, - None, &[], cx, )?; @@ -72,7 +73,6 @@ impl WrappedLine { &self.layout.unwrapped_layout, line_height, &self.decoration_runs, - self.wrap_width, &self.wrap_boundaries, cx, )?; @@ -86,7 +86,6 @@ fn paint_line( layout: &LineLayout, line_height: Pixels, decoration_runs: &[DecorationRun], - wrap_width: Option, wrap_boundaries: &[WrapBoundary], cx: &mut WindowContext<'_>, ) -> Result<()> { @@ -97,6 +96,7 @@ fn paint_line( let mut run_end = 0; let mut color = black(); let mut current_underline: Option<(Point, UnderlineStyle)> = None; + let mut current_background: Option<(Point, Hsla)> = None; let text_system = cx.text_system().clone(); let mut glyph_origin = origin; let mut prev_glyph_position = Point::default(); @@ -110,12 +110,28 @@ fn paint_line( if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) { wraps.next(); - if let Some((underline_origin, underline_style)) = current_underline.take() { + if let Some((background_origin, background_color)) = current_background.as_mut() { + cx.paint_quad( + Bounds { + origin: *background_origin, + size: size(glyph_origin.x - background_origin.x, line_height), + }, + Corners::default(), + *background_color, + Edges::default(), + transparent_black(), + ); + background_origin.x = origin.x; + background_origin.y += line_height; + } + if let Some((underline_origin, underline_style)) = current_underline.as_mut() { cx.paint_underline( - underline_origin, + *underline_origin, glyph_origin.x - underline_origin.x, - &underline_style, - )?; + underline_style, + ); + underline_origin.x = origin.x; + underline_origin.y += line_height; } glyph_origin.x = origin.x; @@ -123,9 +139,20 @@ fn paint_line( } prev_glyph_position = glyph.position; + let mut finished_background: Option<(Point, Hsla)> = None; let mut finished_underline: Option<(Point, UnderlineStyle)> = None; if glyph.index >= run_end { if let Some(style_run) = decoration_runs.next() { + if let Some((_, background_color)) = &mut current_background { + if style_run.background_color.as_ref() != Some(background_color) { + finished_background = current_background.take(); + } + } + if let Some(run_background) = style_run.background_color { + current_background + .get_or_insert((point(glyph_origin.x, glyph_origin.y), run_background)); + } + if let Some((_, underline_style)) = &mut current_underline { if style_run.underline.as_ref() != Some(underline_style) { finished_underline = current_underline.take(); @@ -135,7 +162,7 @@ fn paint_line( current_underline.get_or_insert(( point( glyph_origin.x, - origin.y + baseline_offset.y + (layout.descent * 0.618), + glyph_origin.y + baseline_offset.y + (layout.descent * 0.618), ), UnderlineStyle { color: Some(run_underline.color.unwrap_or(style_run.color)), @@ -149,16 +176,30 @@ fn paint_line( color = style_run.color; } else { run_end = layout.len; + finished_background = current_background.take(); finished_underline = current_underline.take(); } } + if let Some((background_origin, background_color)) = finished_background { + cx.paint_quad( + Bounds { + origin: background_origin, + size: size(glyph_origin.x - background_origin.x, line_height), + }, + Corners::default(), + background_color, + Edges::default(), + transparent_black(), + ); + } + if let Some((underline_origin, underline_style)) = finished_underline { cx.paint_underline( underline_origin, glyph_origin.x - underline_origin.x, &underline_style, - )?; + ); } let max_glyph_bounds = Bounds { @@ -188,13 +229,32 @@ fn paint_line( } } + let mut last_line_end_x = origin.x + layout.width; + if let Some(boundary) = wrap_boundaries.last() { + let run = &layout.runs[boundary.run_ix]; + let glyph = &run.glyphs[boundary.glyph_ix]; + last_line_end_x -= glyph.position.x; + } + + if let Some((background_origin, background_color)) = current_background.take() { + cx.paint_quad( + Bounds { + origin: background_origin, + size: size(last_line_end_x - background_origin.x, line_height), + }, + Corners::default(), + background_color, + Edges::default(), + transparent_black(), + ); + } + if let Some((underline_start, underline_style)) = current_underline.take() { - let line_end_x = origin.x + wrap_width.unwrap_or(Pixels::MAX).min(layout.width); cx.paint_underline( underline_start, - line_end_x - underline_start.x, + last_line_end_x - underline_start.x, &underline_style, - )?; + ); } Ok(()) diff --git a/crates/gpui2/src/text_system/line_layout.rs b/crates/gpui2/src/text_system/line_layout.rs index a5cf814a8c..2370aca83b 100644 --- a/crates/gpui2/src/text_system/line_layout.rs +++ b/crates/gpui2/src/text_system/line_layout.rs @@ -198,6 +198,41 @@ impl WrappedLineLayout { pub fn runs(&self) -> &[ShapedRun] { &self.unwrapped_layout.runs } + + pub fn index_for_position( + &self, + position: Point, + line_height: Pixels, + ) -> Option { + let wrapped_line_ix = (position.y / line_height) as usize; + + let wrapped_line_start_x = if wrapped_line_ix > 0 { + let wrap_boundary_ix = wrapped_line_ix - 1; + let wrap_boundary = self.wrap_boundaries[wrap_boundary_ix]; + let run = &self.unwrapped_layout.runs[wrap_boundary.run_ix]; + run.glyphs[wrap_boundary.glyph_ix].position.x + } else { + Pixels::ZERO + }; + + let wrapped_line_end_x = if wrapped_line_ix < self.wrap_boundaries.len() { + let next_wrap_boundary_ix = wrapped_line_ix; + let next_wrap_boundary = self.wrap_boundaries[next_wrap_boundary_ix]; + let run = &self.unwrapped_layout.runs[next_wrap_boundary.run_ix]; + run.glyphs[next_wrap_boundary.glyph_ix].position.x + } else { + self.unwrapped_layout.width + }; + + let mut position_in_unwrapped_line = position; + position_in_unwrapped_line.x += wrapped_line_start_x; + if position_in_unwrapped_line.x > wrapped_line_end_x { + None + } else { + self.unwrapped_layout + .index_for_x(position_in_unwrapped_line.x) + } + } } pub(crate) struct LineLayoutCache { diff --git a/crates/gpui2/src/view.rs b/crates/gpui2/src/view.rs index c1750d6dc5..f31b0ae753 100644 --- a/crates/gpui2/src/view.rs +++ b/crates/gpui2/src/view.rs @@ -1,23 +1,17 @@ use crate::{ - private::Sealed, AnyBox, AnyElement, AnyModel, AnyWeakModel, AppContext, AvailableSpace, - BorrowWindow, Bounds, Component, Element, ElementId, Entity, EntityId, Flatten, FocusHandle, - FocusableView, LayoutId, Model, Pixels, Point, Size, ViewContext, VisualContext, WeakModel, + private::Sealed, AnyElement, AnyModel, AnyWeakModel, AppContext, AvailableSpace, BorrowWindow, + Bounds, Element, ElementId, Entity, EntityId, Flatten, FocusHandle, FocusableView, IntoElement, + LayoutId, Model, Pixels, Point, Render, Size, ViewContext, VisualContext, WeakModel, WindowContext, }; use anyhow::{Context, Result}; use std::{ - any::{Any, TypeId}, + any::TypeId, hash::{Hash, Hasher}, }; -pub trait Render: 'static + Sized { - type Element: Element + 'static; - - fn render(&mut self, cx: &mut ViewContext) -> Self::Element; -} - pub struct View { - pub(crate) model: Model, + pub model: Model, } impl Sealed for View {} @@ -65,15 +59,15 @@ impl View { self.model.read(cx) } - pub fn render_with(&self, component: C) -> RenderViewWith - where - C: 'static + Component, - { - RenderViewWith { - view: self.clone(), - component: 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 @@ -83,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 { @@ -105,12 +117,6 @@ impl PartialEq for View { impl Eq for View {} -impl Component for View { - fn render(self) -> AnyElement { - AnyElement::new(AnyView::from(self)) - } -} - pub struct WeakView { pub(crate) model: WeakModel, } @@ -163,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, &mut AnyBox, &mut WindowContext), + layout: fn(&AnyView, &mut WindowContext) -> (LayoutId, AnyElement), + paint: fn(&AnyView, AnyElement, &mut WindowContext), } impl AnyView { @@ -202,21 +208,15 @@ impl AnyView { cx: &mut WindowContext, ) { cx.with_absolute_element_offset(origin, |cx| { - let (layout_id, mut rendered_element) = (self.layout)(self, cx); + let (layout_id, rendered_element) = (self.layout)(self, cx); cx.window .layout_engine .compute_layout(layout_id, available_space); - (self.paint)(self, &mut rendered_element, cx); + (self.paint)(self, rendered_element, cx); }) } } -impl Component for AnyView { - fn render(self) -> AnyElement { - AnyElement::new(self) - } -} - impl From> for AnyView { fn from(value: View) -> Self { AnyView { @@ -227,37 +227,51 @@ impl From> for AnyView { } } -impl Element for AnyView { - type ElementState = Box; - - fn element_id(&self) -> Option { - Some(self.model.entity_id.into()) - } +impl Element for AnyView { + type State = Option; fn layout( &mut self, - _view_state: &mut ParentViewState, - _element_state: Option, - cx: &mut ViewContext, - ) -> (LayoutId, Self::ElementState) { - (self.layout)(self, cx) + _state: Option, + cx: &mut WindowContext, + ) -> (LayoutId, Self::State) { + let (layout_id, state) = (self.layout)(self, cx); + (layout_id, Some(state)) } - fn paint( - &mut self, - _bounds: Bounds, - _view_state: &mut ParentViewState, - rendered_element: &mut Self::ElementState, - cx: &mut ViewContext, - ) { - (self.paint)(self, rendered_element, cx) + fn paint(self, _: Bounds, state: &mut Self::State, cx: &mut WindowContext) { + (self.paint)(&self, state.take().unwrap(), cx) + } +} + +impl IntoElement for View { + type Element = View; + + fn element_id(&self) -> Option { + Some(ElementId::from_entity_id(self.model.entity_id)) + } + + fn into_element(self) -> Self::Element { + self + } +} + +impl IntoElement for AnyView { + type Element = Self; + + fn element_id(&self) -> Option { + Some(ElementId::from_entity_id(self.model.entity_id)) + } + + fn into_element(self) -> Self::Element { + self } } pub struct AnyWeakView { model: AnyWeakModel, - layout: fn(&AnyView, &mut WindowContext) -> (LayoutId, Box), - paint: fn(&AnyView, &mut AnyBox, &mut WindowContext), + layout: fn(&AnyView, &mut WindowContext) -> (LayoutId, AnyElement), + paint: fn(&AnyView, AnyElement, &mut WindowContext), } impl AnyWeakView { @@ -271,7 +285,7 @@ impl AnyWeakView { } } -impl From> for AnyWeakView { +impl From> for AnyWeakView { fn from(view: WeakView) -> Self { Self { model: view.model.into(), @@ -281,97 +295,36 @@ impl From> for AnyWeakView { } } -// impl Render for T -// where -// T: 'static + FnMut(&mut WindowContext) -> E, -// E: 'static + Send + Element, -// { -// type Element = E; - -// fn render(&mut self, cx: &mut ViewContext) -> Self::Element { -// (self)(cx) -// } -// } - -pub struct RenderViewWith { - view: View, - component: Option, -} - -impl Component for RenderViewWith +impl Render for T where - C: 'static + Component, - ParentViewState: 'static, - ViewState: 'static, + T: 'static + FnMut(&mut WindowContext) -> E, + E: 'static + Send + Element, { - fn render(self) -> AnyElement { - AnyElement::new(self) - } -} + type Element = E; -impl Element for RenderViewWith -where - C: 'static + Component, - ParentViewState: 'static, - ViewState: 'static, -{ - type ElementState = AnyElement; - - fn element_id(&self) -> Option { - Some(self.view.entity_id().into()) - } - - fn layout( - &mut self, - _: &mut ParentViewState, - _: Option, - cx: &mut ViewContext, - ) -> (LayoutId, Self::ElementState) { - self.view.update(cx, |view, cx| { - let mut element = self.component.take().unwrap().render(); - let layout_id = element.layout(view, cx); - (layout_id, element) - }) - } - - fn paint( - &mut self, - _: Bounds, - _: &mut ParentViewState, - element: &mut Self::ElementState, - cx: &mut ViewContext, - ) { - self.view.update(cx, |view, cx| element.paint(view, cx)) + 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, Element, LayoutId, Render, WindowContext}; - pub(crate) fn layout( + pub(crate) fn layout( view: &AnyView, cx: &mut WindowContext, - ) -> (LayoutId, Box) { - 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) - }) - }) + ) -> (LayoutId, AnyElement) { + let view = view.clone().downcast::().unwrap(); + 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( - view: &AnyView, - element: &mut Box, + pub(crate) fn paint( + _view: &AnyView, + 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_mut::>().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 a3fe05d39f..20561c5443 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -1,5 +1,5 @@ use crate::{ - key_dispatch::DispatchActionListener, px, size, Action, AnyBox, AnyDrag, AnyView, AppContext, + key_dispatch::DispatchActionListener, px, size, Action, AnyDrag, AnyView, AppContext, AsyncWindowContext, AvailableSpace, Bounds, BoxShadow, Context, Corners, CursorStyle, DevicePixels, DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter, FileDropEvent, Flatten, FocusEvent, FontId, GlobalElementId, GlyphId, Hsla, @@ -187,17 +187,17 @@ impl Drop for FocusHandle { /// FocusableView allows users of your view to easily /// focus it (using cx.focus_view(view)) -pub trait FocusableView: Render { +pub trait FocusableView: 'static + Render { fn focus_handle(&self, cx: &AppContext) -> FocusHandle; } /// ManagedView is a view (like a Modal, Popover, Menu, etc.) /// where the lifecycle of the view is handled by another view. -pub trait ManagedView: FocusableView + EventEmitter {} +pub trait ManagedView: FocusableView + EventEmitter {} -impl> ManagedView for M {} +impl> ManagedView for M {} -pub enum Manager { +pub enum DismissEvent { Dismiss, } @@ -230,9 +230,15 @@ pub struct Window { pub(crate) focus: Option, } +pub(crate) struct ElementStateBox { + inner: Box, + #[cfg(debug_assertions)] + type_name: &'static str, +} + // #[derive(Default)] pub(crate) struct Frame { - pub(crate) element_states: HashMap, + pub(crate) element_states: HashMap, mouse_listeners: HashMap>, pub(crate) dispatch_tree: DispatchTree, pub(crate) focus_listeners: Vec, @@ -875,7 +881,7 @@ impl<'a> WindowContext<'a> { origin: Point, width: Pixels, style: &UnderlineStyle, - ) -> Result<()> { + ) { let scale_factor = self.scale_factor(); let height = if style.wavy { style.thickness * 3. @@ -899,7 +905,6 @@ impl<'a> WindowContext<'a> { wavy: style.wavy, }, ); - Ok(()) } /// Paint a monochrome (non-emoji) glyph into the scene for the current frame at the current z-index. @@ -1436,6 +1441,89 @@ 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)); + } + } + + pub fn on_window_should_close(&mut self, f: impl Fn(&mut WindowContext) -> bool + 'static) { + let mut this = self.to_async(); + self.window + .platform_window + .on_should_close(Box::new(move || this.update(|_, cx| f(cx)).unwrap_or(true))) + } } impl Context for WindowContext<'_> { @@ -1559,7 +1647,7 @@ impl VisualContext for WindowContext<'_> { build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V, ) -> Self::Result> where - V: Render, + V: 'static + Render, { let slot = self.app.entities.reserve(); let view = View { @@ -1582,7 +1670,7 @@ impl VisualContext for WindowContext<'_> { where V: ManagedView, { - self.update_view(view, |_, cx| cx.emit(Manager::Dismiss)) + self.update_view(view, |_, cx| cx.emit(DismissEvent::Dismiss)) } } @@ -1617,6 +1705,10 @@ pub trait BorrowWindow: BorrowMut + BorrowMut { self.borrow_mut() } + fn app(&self) -> &AppContext { + self.borrow() + } + fn window(&self) -> &Window { self.borrow() } @@ -1667,6 +1759,24 @@ pub trait BorrowWindow: BorrowMut + BorrowMut { } } + /// Invoke the given function with the content mask reset to that + /// of the window. + fn break_content_mask(&mut self, f: impl FnOnce(&mut Self) -> R) -> R { + let mask = ContentMask { + bounds: Bounds { + origin: Point::default(), + size: self.window().viewport_size, + }, + }; + self.window_mut() + .current_frame + .content_mask_stack + .push(mask); + let result = f(self); + self.window_mut().current_frame.content_mask_stack.pop(); + result + } + /// Update the global element offset relative to the current offset. This is used to implement /// scrolling. fn with_element_offset( @@ -1735,10 +1845,37 @@ pub trait BorrowWindow: BorrowMut + BorrowMut { .remove(&global_id) }) { + let ElementStateBox { + inner, + + #[cfg(debug_assertions)] + type_name + } = any; // Using the extra inner option to avoid needing to reallocate a new box. - let mut state_box = any + let mut state_box = inner .downcast::>() - .expect("invalid element state type for id"); + .map_err(|_| { + #[cfg(debug_assertions)] + { + anyhow!( + "invalid element state type for id, requested_type {:?}, actual type: {:?}", + std::any::type_name::(), + type_name + ) + } + + #[cfg(not(debug_assertions))] + { + anyhow!( + "invalid element state type for id, requested_type {:?}", + std::any::type_name::(), + ) + } + }) + .unwrap(); + + // Actual: Option <- View + // Requested: () <- AnyElemet let state = state_box .take() .expect("element state is already on the stack"); @@ -1747,14 +1884,27 @@ pub trait BorrowWindow: BorrowMut + BorrowMut { cx.window_mut() .current_frame .element_states - .insert(global_id, state_box); + .insert(global_id, ElementStateBox { + inner: state_box, + + #[cfg(debug_assertions)] + type_name + }); result } else { let (result, state) = f(None, cx); cx.window_mut() .current_frame .element_states - .insert(global_id, Box::new(Some(state))); + .insert(global_id, + ElementStateBox { + inner: Box::new(Some(state)), + + #[cfg(debug_assertions)] + type_name: std::any::type_name::() + } + + ); result } }) @@ -2124,49 +2274,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 +2350,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, @@ -2282,7 +2374,17 @@ impl<'a, V: 'static> ViewContext<'a, V> { where V: ManagedView, { - self.defer(|_, cx| cx.emit(Manager::Dismiss)) + self.defer(|_, cx| cx.emit(DismissEvent::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(); + } } } @@ -2355,7 +2457,7 @@ impl VisualContext for ViewContext<'_, V> { build_view: impl FnOnce(&mut ViewContext<'_, W>) -> W, ) -> Self::Result> where - W: Render, + W: 'static + Render, { self.window_cx.replace_root_view(build_view) } @@ -2567,6 +2669,12 @@ pub enum ElementId { FocusHandle(FocusId), } +impl ElementId { + pub(crate) fn from_entity_id(entity_id: EntityId) -> Self { + ElementId::View(entity_id) + } +} + impl TryInto for ElementId { type Error = anyhow::Error; @@ -2579,12 +2687,6 @@ impl TryInto for ElementId { } } -impl From for ElementId { - fn from(id: EntityId) -> Self { - ElementId::View(id) - } -} - impl From for ElementId { fn from(id: usize) -> Self { ElementId::Integer(id) diff --git a/crates/gpui2_macros/src/derive_component.rs b/crates/gpui2_macros/src/derive_component.rs deleted file mode 100644 index aaf814497a..0000000000 --- a/crates/gpui2_macros/src/derive_component.rs +++ /dev/null @@ -1,66 +0,0 @@ -use proc_macro::TokenStream; -use quote::quote; -use syn::{parse_macro_input, parse_quote, DeriveInput}; - -pub fn derive_component(input: TokenStream) -> TokenStream { - let ast = parse_macro_input!(input as DeriveInput); - let 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 (_, ty_generics, _) = ast.generics.split_for_impl(); - - let expanded = quote! { - impl #impl_generics gpui::Component<#view_type> for #name #ty_generics #where_clause { - fn render(self) -> gpui::AnyElement<#view_type> { - (move |view_state: &mut #view_type, cx: &mut gpui::ViewContext<'_, #view_type>| self.render(view_state, cx)) - .render() - } - } - }; - - TokenStream::from(expanded) -} - -fn specified_view_type(ast: &DeriveInput) -> Option { - let component_attr = ast - .attrs - .iter() - .find(|attr| attr.path.is_ident("component"))?; - - if let Ok(syn::Meta::List(meta_list)) = component_attr.parse_meta() { - meta_list.nested.iter().find_map(|nested| { - if let syn::NestedMeta::Meta(syn::Meta::NameValue(nv)) = nested { - if nv.path.is_ident("view_type") { - if let syn::Lit::Str(lit_str) = &nv.lit { - return Some( - lit_str - .parse::() - .expect("Failed to parse view_type"), - ); - } - } - } - None - }) - } else { - None - } -} diff --git a/crates/gpui2_macros/src/derive_into_element.rs b/crates/gpui2_macros/src/derive_into_element.rs new file mode 100644 index 0000000000..12c6975e07 --- /dev/null +++ b/crates/gpui2_macros/src/derive_into_element.rs @@ -0,0 +1,27 @@ +use proc_macro::TokenStream; +use quote::quote; +use syn::{parse_macro_input, DeriveInput}; + +pub fn derive_into_element(input: TokenStream) -> TokenStream { + let ast = parse_macro_input!(input as DeriveInput); + let type_name = &ast.ident; + let (impl_generics, type_generics, where_clause) = ast.generics.split_for_impl(); + + let gen = quote! { + impl #impl_generics gpui::IntoElement for #type_name #type_generics + #where_clause + { + type Element = gpui::Component; + + fn element_id(&self) -> Option { + None + } + + fn into_element(self) -> Self::Element { + gpui::Component::new(self) + } + } + }; + + gen.into() +} diff --git a/crates/gpui2_macros/src/gpui2_macros.rs b/crates/gpui2_macros/src/gpui2_macros.rs index 3ce8373689..a7fb363d36 100644 --- a/crates/gpui2_macros/src/gpui2_macros.rs +++ b/crates/gpui2_macros/src/gpui2_macros.rs @@ -1,16 +1,11 @@ mod action; -mod derive_component; +mod derive_into_element; mod register_action; mod style_helpers; mod test; use proc_macro::TokenStream; -#[proc_macro] -pub fn style_helpers(args: TokenStream) -> TokenStream { - style_helpers::style_helpers(args) -} - #[proc_macro_derive(Action)] pub fn action(input: TokenStream) -> TokenStream { action::action(input) @@ -21,9 +16,14 @@ pub fn register_action(attr: TokenStream, item: TokenStream) -> TokenStream { register_action::register_action_macro(attr, item) } -#[proc_macro_derive(Component, attributes(component))] -pub fn derive_component(input: TokenStream) -> TokenStream { - derive_component::derive_component(input) +#[proc_macro_derive(IntoElement)] +pub fn derive_into_element(input: TokenStream) -> TokenStream { + derive_into_element::derive_into_element(input) +} + +#[proc_macro] +pub fn style_helpers(input: TokenStream) -> TokenStream { + style_helpers::style_helpers(input) } #[proc_macro_attribute] diff --git a/crates/language/src/highlight_map.rs b/crates/language/src/highlight_map.rs index 109d79cf70..cf790e803e 100644 --- a/crates/language/src/highlight_map.rs +++ b/crates/language/src/highlight_map.rs @@ -11,7 +11,7 @@ pub struct HighlightId(pub u32); const DEFAULT_SYNTAX_HIGHLIGHT_ID: HighlightId = HighlightId(u32::MAX); impl HighlightMap { - pub fn new(capture_names: &[String], theme: &SyntaxTheme) -> Self { + pub fn new(capture_names: &[&str], theme: &SyntaxTheme) -> Self { // For each capture name in the highlight query, find the longest // key in the theme's syntax styles that matches all of the // dot-separated components of the capture name. @@ -98,9 +98,9 @@ mod tests { ); let capture_names = &[ - "function.special".to_string(), - "function.async.rust".to_string(), - "variable.builtin.self".to_string(), + "function.special", + "function.async.rust", + "variable.builtin.self", ]; let map = HighlightMap::new(capture_names, &theme); diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 1d22d7773b..af7504529c 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -1383,7 +1383,7 @@ impl Language { let query = Query::new(self.grammar_mut().ts_language, source)?; let mut override_configs_by_id = HashMap::default(); - for (ix, name) in query.capture_names().iter().enumerate() { + for (ix, name) in query.capture_names().iter().copied().enumerate() { if !name.starts_with('_') { let value = self.config.overrides.remove(name).unwrap_or_default(); for server_name in &value.opt_into_language_servers { @@ -1396,7 +1396,7 @@ impl Language { } } - override_configs_by_id.insert(ix as u32, (name.clone(), value)); + override_configs_by_id.insert(ix as u32, (name.into(), value)); } } diff --git a/crates/language/src/syntax_map/syntax_map_tests.rs b/crates/language/src/syntax_map/syntax_map_tests.rs index bd50608122..f20f481613 100644 --- a/crates/language/src/syntax_map/syntax_map_tests.rs +++ b/crates/language/src/syntax_map/syntax_map_tests.rs @@ -1300,7 +1300,7 @@ fn assert_capture_ranges( .collect::>(); for capture in captures { let name = &queries[capture.grammar_index].capture_names()[capture.index as usize]; - if highlight_query_capture_names.contains(&name.as_str()) { + if highlight_query_capture_names.contains(&name) { actual_ranges.push(capture.node.byte_range()); } } diff --git a/crates/language2/src/buffer.rs b/crates/language2/src/buffer.rs index 51ed192b99..26ee93adef 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/highlight_map.rs b/crates/language2/src/highlight_map.rs index 1421ef672d..8e7a35233c 100644 --- a/crates/language2/src/highlight_map.rs +++ b/crates/language2/src/highlight_map.rs @@ -11,7 +11,7 @@ pub struct HighlightId(pub u32); const DEFAULT_SYNTAX_HIGHLIGHT_ID: HighlightId = HighlightId(u32::MAX); impl HighlightMap { - pub fn new(capture_names: &[String], theme: &SyntaxTheme) -> Self { + pub fn new(capture_names: &[&str], theme: &SyntaxTheme) -> Self { // For each capture name in the highlight query, find the longest // key in the theme's syntax styles that matches all of the // dot-separated components of the capture name. @@ -100,9 +100,9 @@ mod tests { }; let capture_names = &[ - "function.special".to_string(), - "function.async.rust".to_string(), - "variable.builtin.self".to_string(), + "function.special", + "function.async.rust", + "variable.builtin.self", ]; let map = HighlightMap::new(capture_names, &theme); diff --git a/crates/language2/src/language2.rs b/crates/language2/src/language2.rs index 311049f032..5c17592f0c 100644 --- a/crates/language2/src/language2.rs +++ b/crates/language2/src/language2.rs @@ -1391,7 +1391,7 @@ impl Language { let mut override_configs_by_id = HashMap::default(); for (ix, name) in query.capture_names().iter().enumerate() { if !name.starts_with('_') { - let value = self.config.overrides.remove(name).unwrap_or_default(); + let value = self.config.overrides.remove(*name).unwrap_or_default(); for server_name in &value.opt_into_language_servers { if !self .config @@ -1402,7 +1402,7 @@ impl Language { } } - override_configs_by_id.insert(ix as u32, (name.clone(), value)); + override_configs_by_id.insert(ix as u32, (name.to_string(), value)); } } diff --git a/crates/language2/src/proto.rs b/crates/language2/src/proto.rs index c4abe39d47..957f4ee7fb 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/language2/src/syntax_map/syntax_map_tests.rs b/crates/language2/src/syntax_map/syntax_map_tests.rs index bd50608122..f20f481613 100644 --- a/crates/language2/src/syntax_map/syntax_map_tests.rs +++ b/crates/language2/src/syntax_map/syntax_map_tests.rs @@ -1300,7 +1300,7 @@ fn assert_capture_ranges( .collect::>(); for capture in captures { let name = &queries[capture.grammar_index].capture_names()[capture.index as usize]; - if highlight_query_capture_names.contains(&name.as_str()) { + if highlight_query_capture_names.contains(&name) { actual_ranges.push(capture.node.byte_range()); } } diff --git a/crates/live_kit_client2/build.rs b/crates/live_kit_client2/build.rs index b346b3168b..a2b7ef866d 100644 --- a/crates/live_kit_client2/build.rs +++ b/crates/live_kit_client2/build.rs @@ -61,12 +61,14 @@ fn build_bridge(swift_target: &SwiftTarget) { let swift_package_root = swift_package_root(); let swift_target_folder = swift_target_folder(); + let swift_cache_folder = swift_cache_folder(); if !Command::new("swift") .arg("build") .arg("--disable-automatic-resolution") .args(["--configuration", &env::var("PROFILE").unwrap()]) .args(["--triple", &swift_target.target.triple]) .args(["--build-path".into(), swift_target_folder]) + .args(["--cache-path".into(), swift_cache_folder]) .current_dir(&swift_package_root) .status() .unwrap() @@ -133,9 +135,17 @@ fn swift_package_root() -> PathBuf { } fn swift_target_folder() -> PathBuf { + let target = env::var("TARGET").unwrap(); env::current_dir() .unwrap() - .join(format!("../../target/{SWIFT_PACKAGE_NAME}")) + .join(format!("../../target/{target}/{SWIFT_PACKAGE_NAME}_target")) +} + +fn swift_cache_folder() -> PathBuf { + let target = env::var("TARGET").unwrap(); + env::current_dir() + .unwrap() + .join(format!("../../target/{target}/{SWIFT_PACKAGE_NAME}_cache")) } fn copy_dir(source: &Path, destination: &Path) { diff --git a/crates/node_runtime/src/node_runtime.rs b/crates/node_runtime/src/node_runtime.rs index 2621c58120..ecabdeb718 100644 --- a/crates/node_runtime/src/node_runtime.rs +++ b/crates/node_runtime/src/node_runtime.rs @@ -73,6 +73,7 @@ impl RealNodeRuntime { let npm_file = node_dir.join("bin/npm"); let result = Command::new(&node_binary) + .env_clear() .arg(npm_file) .arg("--version") .stdin(Stdio::null()) @@ -149,6 +150,7 @@ impl NodeRuntime for RealNodeRuntime { } let mut command = Command::new(node_binary); + command.env_clear(); command.env("PATH", env_path); command.arg(npm_file).arg(subcommand); command.args(["--cache".into(), installation_path.join("cache")]); @@ -200,11 +202,11 @@ impl NodeRuntime for RealNodeRuntime { &[ name, "--json", - "-fetch-retry-mintimeout", + "--fetch-retry-mintimeout", "2000", - "-fetch-retry-maxtimeout", + "--fetch-retry-maxtimeout", "5000", - "-fetch-timeout", + "--fetch-timeout", "5000", ], ) @@ -229,11 +231,11 @@ impl NodeRuntime for RealNodeRuntime { let mut arguments: Vec<_> = packages.iter().map(|p| p.as_str()).collect(); arguments.extend_from_slice(&[ - "-fetch-retry-mintimeout", + "--fetch-retry-mintimeout", "2000", - "-fetch-retry-maxtimeout", + "--fetch-retry-maxtimeout", "5000", - "-fetch-timeout", + "--fetch-timeout", "5000", ]); diff --git a/crates/picker2/src/picker2.rs b/crates/picker2/src/picker2.rs index 80c0e2b219..ea31419201 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, Component, Div, FocusHandle, FocusableView, - MouseButton, Render, Task, UniformListScrollHandle, View, ViewContext, WindowContext, + div, prelude::*, uniform_list, AppContext, Div, FocusHandle, FocusableView, MouseButton, + 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: Component>; + type ListItem: IntoElement; fn match_count(&self) -> usize; fn selected_index(&self) -> usize; @@ -32,7 +32,7 @@ pub trait PickerDelegate: Sized + 'static { ix: usize, selected: bool, cx: &mut ViewContext>, - ) -> Self::ListItem; + ) -> Option; } impl FocusableView for Picker { @@ -114,6 +114,7 @@ impl Picker { } fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { + dbg!("canceling!"); self.delegate.dismissed(cx); } @@ -181,20 +182,20 @@ impl Picker { } impl Render for Picker { - type Element = Div; + 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 +209,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, + ) + }), + ) + .children(picker.delegate.render_match( + ix, + ix == selected_index, + cx, + )) + }) + .collect() + } + }, + ) .track_scroll(self.scroll_handle.clone()), ) .max_h_72() @@ -244,7 +251,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/project/src/project.rs b/crates/project/src/project.rs index c24fb5eea1..cf3fa547f6 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -13,7 +13,7 @@ mod worktree_tests; use anyhow::{anyhow, Context, Result}; use client::{proto, Client, Collaborator, TypedEnvelope, UserStore}; use clock::ReplicaId; -use collections::{hash_map, BTreeMap, HashMap, HashSet}; +use collections::{hash_map, BTreeMap, HashMap, HashSet, VecDeque}; use copilot::Copilot; use futures::{ channel::{ @@ -62,7 +62,10 @@ use serde::Serialize; use settings::SettingsStore; use sha2::{Digest, Sha256}; use similar::{ChangeTag, TextDiff}; -use smol::channel::{Receiver, Sender}; +use smol::{ + channel::{Receiver, Sender}, + lock::Semaphore, +}; use std::{ cmp::{self, Ordering}, convert::TryInto, @@ -557,6 +560,7 @@ enum SearchMatchCandidate { }, Path { worktree_id: WorktreeId, + is_ignored: bool, path: Arc, }, } @@ -5742,13 +5746,18 @@ impl Project { .await .log_err(); } + background .scoped(|scope| { + let max_concurrent_workers = Arc::new(Semaphore::new(workers)); + for worker_ix in 0..workers { let worker_start_ix = worker_ix * paths_per_worker; let worker_end_ix = worker_start_ix + paths_per_worker; let unnamed_buffers = opened_buffers.clone(); + let limiter = Arc::clone(&max_concurrent_workers); scope.spawn(async move { + let _guard = limiter.acquire().await; let mut snapshot_start_ix = 0; let mut abs_path = PathBuf::new(); for snapshot in snapshots { @@ -5797,6 +5806,7 @@ impl Project { let project_path = SearchMatchCandidate::Path { worktree_id: snapshot.id(), path: entry.path.clone(), + is_ignored: entry.is_ignored, }; if matching_paths_tx.send(project_path).await.is_err() { break; @@ -5809,6 +5819,94 @@ impl Project { } }); } + + if query.include_ignored() { + for snapshot in snapshots { + for ignored_entry in snapshot + .entries(query.include_ignored()) + .filter(|e| e.is_ignored) + { + let limiter = Arc::clone(&max_concurrent_workers); + scope.spawn(async move { + let _guard = limiter.acquire().await; + let mut ignored_paths_to_process = + VecDeque::from([snapshot.abs_path().join(&ignored_entry.path)]); + while let Some(ignored_abs_path) = + ignored_paths_to_process.pop_front() + { + if !query.file_matches(Some(&ignored_abs_path)) + || snapshot.is_path_excluded(&ignored_abs_path) + { + continue; + } + if let Some(fs_metadata) = fs + .metadata(&ignored_abs_path) + .await + .with_context(|| { + format!("fetching fs metadata for {ignored_abs_path:?}") + }) + .log_err() + .flatten() + { + if fs_metadata.is_dir { + if let Some(mut subfiles) = fs + .read_dir(&ignored_abs_path) + .await + .with_context(|| { + format!( + "listing ignored path {ignored_abs_path:?}" + ) + }) + .log_err() + { + while let Some(subfile) = subfiles.next().await { + if let Some(subfile) = subfile.log_err() { + ignored_paths_to_process.push_back(subfile); + } + } + } + } else if !fs_metadata.is_symlink { + let matches = if let Some(file) = fs + .open_sync(&ignored_abs_path) + .await + .with_context(|| { + format!( + "Opening ignored path {ignored_abs_path:?}" + ) + }) + .log_err() + { + query.detect(file).unwrap_or(false) + } else { + false + }; + if matches { + let project_path = SearchMatchCandidate::Path { + worktree_id: snapshot.id(), + path: Arc::from( + ignored_abs_path + .strip_prefix(snapshot.abs_path()) + .expect( + "scanning worktree-related files", + ), + ), + is_ignored: true, + }; + if matching_paths_tx + .send(project_path) + .await + .is_err() + { + return; + } + } + } + } + } + }); + } + } + } }) .await; } @@ -5917,11 +6015,24 @@ impl Project { let (buffers_tx, buffers_rx) = smol::channel::bounded(1024); let (sorted_buffers_tx, sorted_buffers_rx) = futures::channel::oneshot::channel(); cx.spawn(|this, cx| async move { - let mut buffers = vec![]; + let mut buffers = Vec::new(); + let mut ignored_buffers = Vec::new(); while let Some(entry) = matching_paths_rx.next().await { - buffers.push(entry); + if matches!( + entry, + SearchMatchCandidate::Path { + is_ignored: true, + .. + } + ) { + ignored_buffers.push(entry); + } else { + buffers.push(entry); + } } buffers.sort_by_key(|candidate| candidate.path()); + ignored_buffers.sort_by_key(|candidate| candidate.path()); + buffers.extend(ignored_buffers); let matching_paths = buffers.clone(); let _ = sorted_buffers_tx.send(buffers); for (index, candidate) in matching_paths.into_iter().enumerate() { @@ -5933,7 +6044,9 @@ impl Project { cx.spawn(|mut cx| async move { let buffer = match candidate { SearchMatchCandidate::OpenBuffer { buffer, .. } => Some(buffer), - SearchMatchCandidate::Path { worktree_id, path } => this + SearchMatchCandidate::Path { + worktree_id, path, .. + } => this .update(&mut cx, |this, cx| { this.open_buffer((worktree_id, path), cx) }) diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 82fa5d6020..d5a046ba0d 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -2226,7 +2226,7 @@ impl LocalSnapshot { paths } - fn is_abs_path_excluded(&self, abs_path: &Path) -> bool { + pub fn is_path_excluded(&self, abs_path: &Path) -> bool { self.file_scan_exclusions .iter() .any(|exclude_matcher| exclude_matcher.is_match(abs_path)) @@ -2399,26 +2399,9 @@ impl BackgroundScannerState { self.snapshot.check_invariants(false); } - fn reload_repositories(&mut self, changed_paths: &[Arc], fs: &dyn Fs) { + fn reload_repositories(&mut self, dot_git_dirs_to_reload: &HashSet, fs: &dyn Fs) { let scan_id = self.snapshot.scan_id; - - // Find each of the .git directories that contain any of the given paths. - let mut prev_dot_git_dir = None; - for changed_path in changed_paths { - let Some(dot_git_dir) = changed_path - .ancestors() - .find(|ancestor| ancestor.file_name() == Some(&*DOT_GIT)) - else { - continue; - }; - - // Avoid processing the same repository multiple times, if multiple paths - // within it have changed. - if prev_dot_git_dir == Some(dot_git_dir) { - continue; - } - prev_dot_git_dir = Some(dot_git_dir); - + for dot_git_dir in dot_git_dirs_to_reload { // If there is already a repository for this .git directory, reload // the status for all of its files. let repository = self @@ -2430,7 +2413,7 @@ impl BackgroundScannerState { }); match repository { None => { - self.build_git_repository(dot_git_dir.into(), fs); + self.build_git_repository(Arc::from(dot_git_dir.as_path()), fs); } Some((entry_id, repository)) => { if repository.git_dir_scan_id == scan_id { @@ -2444,7 +2427,7 @@ impl BackgroundScannerState { continue; }; - log::info!("reload git repository {:?}", dot_git_dir); + log::info!("reload git repository {dot_git_dir:?}"); let repository = repository.repo_ptr.lock(); let branch = repository.branch_name(); repository.reload_index(); @@ -2475,7 +2458,9 @@ impl BackgroundScannerState { ids_to_preserve.insert(work_directory_id); } else { let git_dir_abs_path = snapshot.abs_path().join(&entry.git_dir_path); - if snapshot.is_abs_path_excluded(&git_dir_abs_path) + let git_dir_excluded = snapshot.is_path_excluded(&entry.git_dir_path) + || snapshot.is_path_excluded(&git_dir_abs_path); + if git_dir_excluded && !matches!(smol::block_on(fs.metadata(&git_dir_abs_path)), Ok(None)) { ids_to_preserve.insert(work_directory_id); @@ -3314,11 +3299,26 @@ impl BackgroundScanner { }; let mut relative_paths = Vec::with_capacity(abs_paths.len()); + let mut dot_git_paths_to_reload = HashSet::default(); abs_paths.sort_unstable(); abs_paths.dedup_by(|a, b| a.starts_with(&b)); abs_paths.retain(|abs_path| { let snapshot = &self.state.lock().snapshot; { + let mut is_git_related = false; + if let Some(dot_git_dir) = abs_path + .ancestors() + .find(|ancestor| ancestor.file_name() == Some(&*DOT_GIT)) + { + let dot_git_path = dot_git_dir + .strip_prefix(&root_canonical_path) + .ok() + .map(|path| path.to_path_buf()) + .unwrap_or_else(|| dot_git_dir.to_path_buf()); + dot_git_paths_to_reload.insert(dot_git_path.to_path_buf()); + is_git_related = true; + } + let relative_path: Arc = if let Ok(path) = abs_path.strip_prefix(&root_canonical_path) { path.into() @@ -3328,23 +3328,30 @@ impl BackgroundScanner { ); return false; }; + let parent_dir_is_loaded = relative_path.parent().map_or(true, |parent| { + snapshot + .entry_for_path(parent) + .map_or(false, |entry| entry.kind == EntryKind::Dir) + }); + if !parent_dir_is_loaded { + log::debug!("ignoring event {relative_path:?} within unloaded directory"); + return false; + } - if !is_git_related(&abs_path) { - let parent_dir_is_loaded = relative_path.parent().map_or(true, |parent| { - snapshot - .entry_for_path(parent) - .map_or(false, |entry| entry.kind == EntryKind::Dir) - }); - if !parent_dir_is_loaded { - log::debug!("ignoring event {relative_path:?} within unloaded directory"); - return false; + // FS events may come for files which parent directory is excluded, need to check ignore those. + let mut path_to_test = abs_path.clone(); + let mut excluded_file_event = snapshot.is_path_excluded(abs_path) + || snapshot.is_path_excluded(&relative_path); + while !excluded_file_event && path_to_test.pop() { + if snapshot.is_path_excluded(&path_to_test) { + excluded_file_event = true; } - if snapshot.is_abs_path_excluded(abs_path) { - log::debug!( - "ignoring FS event for path {relative_path:?} within excluded directory" - ); - return false; + } + if excluded_file_event { + if !is_git_related { + log::debug!("ignoring FS event for excluded path {relative_path:?}"); } + return false; } relative_paths.push(relative_path); @@ -3352,31 +3359,39 @@ impl BackgroundScanner { } }); - if relative_paths.is_empty() { + if dot_git_paths_to_reload.is_empty() && relative_paths.is_empty() { return; } - log::debug!("received fs events {:?}", relative_paths); + if !relative_paths.is_empty() { + log::debug!("received fs events {:?}", relative_paths); - let (scan_job_tx, scan_job_rx) = channel::unbounded(); - self.reload_entries_for_paths( - root_path, - root_canonical_path, - &relative_paths, - abs_paths, - Some(scan_job_tx.clone()), - ) - .await; - drop(scan_job_tx); - self.scan_dirs(false, scan_job_rx).await; + let (scan_job_tx, scan_job_rx) = channel::unbounded(); + self.reload_entries_for_paths( + root_path, + root_canonical_path, + &relative_paths, + abs_paths, + Some(scan_job_tx.clone()), + ) + .await; + drop(scan_job_tx); + self.scan_dirs(false, scan_job_rx).await; - let (scan_job_tx, scan_job_rx) = channel::unbounded(); - self.update_ignore_statuses(scan_job_tx).await; - self.scan_dirs(false, scan_job_rx).await; + let (scan_job_tx, scan_job_rx) = channel::unbounded(); + self.update_ignore_statuses(scan_job_tx).await; + self.scan_dirs(false, scan_job_rx).await; + } { let mut state = self.state.lock(); - state.reload_repositories(&relative_paths, self.fs.as_ref()); + if !dot_git_paths_to_reload.is_empty() { + if relative_paths.is_empty() { + state.snapshot.scan_id += 1; + } + log::debug!("reloading repositories: {dot_git_paths_to_reload:?}"); + state.reload_repositories(&dot_git_paths_to_reload, self.fs.as_ref()); + } state.snapshot.completed_scan_id = state.snapshot.scan_id; for (_, entry_id) in mem::take(&mut state.removed_entry_ids) { state.scanned_dirs.remove(&entry_id); @@ -3516,7 +3531,7 @@ impl BackgroundScanner { let state = self.state.lock(); let snapshot = &state.snapshot; root_abs_path = snapshot.abs_path().clone(); - if snapshot.is_abs_path_excluded(&job.abs_path) { + if snapshot.is_path_excluded(&job.abs_path) { log::error!("skipping excluded directory {:?}", job.path); return Ok(()); } @@ -3588,7 +3603,7 @@ impl BackgroundScanner { { let mut state = self.state.lock(); - if state.snapshot.is_abs_path_excluded(&child_abs_path) { + if state.snapshot.is_path_excluded(&child_abs_path) { let relative_path = job.path.join(child_name); log::debug!("skipping excluded child entry {relative_path:?}"); state.remove_path(&relative_path); @@ -4130,12 +4145,6 @@ impl BackgroundScanner { } } -fn is_git_related(abs_path: &Path) -> bool { - abs_path - .components() - .any(|c| c.as_os_str() == *DOT_GIT || c.as_os_str() == *GITIGNORE) -} - fn char_bag_for_path(root_char_bag: CharBag, path: &Path) -> CharBag { let mut result = root_char_bag; result.extend( diff --git a/crates/project/src/worktree_tests.rs b/crates/project/src/worktree_tests.rs index 22a5cc1e01..b4cf162d8f 100644 --- a/crates/project/src/worktree_tests.rs +++ b/crates/project/src/worktree_tests.rs @@ -990,6 +990,145 @@ async fn test_file_scan_exclusions(cx: &mut TestAppContext) { }); } +#[gpui::test] +async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) { + init_test(cx); + let dir = temp_tree(json!({ + ".git": { + "HEAD": "ref: refs/heads/main\n", + "foo": "bar", + }, + ".gitignore": "**/target\n/node_modules\ntest_output\n", + "target": { + "index": "blah2" + }, + "node_modules": { + ".DS_Store": "", + "prettier": { + "package.json": "{}", + }, + }, + "src": { + ".DS_Store": "", + "foo": { + "foo.rs": "mod another;\n", + "another.rs": "// another", + }, + "bar": { + "bar.rs": "// bar", + }, + "lib.rs": "mod foo;\nmod bar;\n", + }, + ".DS_Store": "", + })); + cx.update(|cx| { + cx.update_global::(|store, cx| { + store.update_user_settings::(cx, |project_settings| { + project_settings.file_scan_exclusions = Some(vec![ + "**/.git".to_string(), + "node_modules/".to_string(), + "build_output".to_string(), + ]); + }); + }); + }); + + let tree = Worktree::local( + build_client(cx), + dir.path(), + true, + Arc::new(RealFs), + Default::default(), + &mut cx.to_async(), + ) + .await + .unwrap(); + cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) + .await; + tree.flush_fs_events(cx).await; + tree.read_with(cx, |tree, _| { + check_worktree_entries( + tree, + &[ + ".git/HEAD", + ".git/foo", + "node_modules/.DS_Store", + "node_modules/prettier", + "node_modules/prettier/package.json", + ], + &["target", "node_modules"], + &[ + ".DS_Store", + "src/.DS_Store", + "src/lib.rs", + "src/foo/foo.rs", + "src/foo/another.rs", + "src/bar/bar.rs", + ".gitignore", + ], + ) + }); + + let new_excluded_dir = dir.path().join("build_output"); + let new_ignored_dir = dir.path().join("test_output"); + std::fs::create_dir_all(&new_excluded_dir) + .unwrap_or_else(|e| panic!("Failed to create a {new_excluded_dir:?} directory: {e}")); + std::fs::create_dir_all(&new_ignored_dir) + .unwrap_or_else(|e| panic!("Failed to create a {new_ignored_dir:?} directory: {e}")); + let node_modules_dir = dir.path().join("node_modules"); + let dot_git_dir = dir.path().join(".git"); + let src_dir = dir.path().join("src"); + for existing_dir in [&node_modules_dir, &dot_git_dir, &src_dir] { + assert!( + existing_dir.is_dir(), + "Expect {existing_dir:?} to be present in the FS already" + ); + } + + for directory_for_new_file in [ + new_excluded_dir, + new_ignored_dir, + node_modules_dir, + dot_git_dir, + src_dir, + ] { + std::fs::write(directory_for_new_file.join("new_file"), "new file contents") + .unwrap_or_else(|e| { + panic!("Failed to create in {directory_for_new_file:?} a new file: {e}") + }); + } + tree.flush_fs_events(cx).await; + + tree.read_with(cx, |tree, _| { + check_worktree_entries( + tree, + &[ + ".git/HEAD", + ".git/foo", + ".git/new_file", + "node_modules/.DS_Store", + "node_modules/prettier", + "node_modules/prettier/package.json", + "node_modules/new_file", + "build_output", + "build_output/new_file", + "test_output/new_file", + ], + &["target", "node_modules", "test_output"], + &[ + ".DS_Store", + "src/.DS_Store", + "src/lib.rs", + "src/foo/foo.rs", + "src/foo/another.rs", + "src/bar/bar.rs", + "src/new_file", + ".gitignore", + ], + ) + }); +} + #[gpui::test(iterations = 30)] async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) { init_test(cx); diff --git a/crates/project2/src/lsp_command.rs b/crates/project2/src/lsp_command.rs index cc1821d3ff..94c277db1e 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/project2/src/project2.rs b/crates/project2/src/project2.rs index 3f7c9b7188..856c280ac0 100644 --- a/crates/project2/src/project2.rs +++ b/crates/project2/src/project2.rs @@ -13,7 +13,7 @@ mod worktree_tests; use anyhow::{anyhow, Context as _, Result}; use client::{proto, Client, Collaborator, TypedEnvelope, UserStore}; use clock::ReplicaId; -use collections::{hash_map, BTreeMap, HashMap, HashSet}; +use collections::{hash_map, BTreeMap, HashMap, HashSet, VecDeque}; use copilot::Copilot; use futures::{ channel::{ @@ -63,6 +63,7 @@ use settings::{Settings, SettingsStore}; use sha2::{Digest, Sha256}; use similar::{ChangeTag, TextDiff}; use smol::channel::{Receiver, Sender}; +use smol::lock::Semaphore; use std::{ cmp::{self, Ordering}, convert::TryInto, @@ -557,6 +558,7 @@ enum SearchMatchCandidate { }, Path { worktree_id: WorktreeId, + is_ignored: bool, path: Arc, }, } @@ -5815,11 +5817,15 @@ impl Project { } executor .scoped(|scope| { + let max_concurrent_workers = Arc::new(Semaphore::new(workers)); + for worker_ix in 0..workers { let worker_start_ix = worker_ix * paths_per_worker; let worker_end_ix = worker_start_ix + paths_per_worker; let unnamed_buffers = opened_buffers.clone(); + let limiter = Arc::clone(&max_concurrent_workers); scope.spawn(async move { + let _guard = limiter.acquire().await; let mut snapshot_start_ix = 0; let mut abs_path = PathBuf::new(); for snapshot in snapshots { @@ -5868,6 +5874,7 @@ impl Project { let project_path = SearchMatchCandidate::Path { worktree_id: snapshot.id(), path: entry.path.clone(), + is_ignored: entry.is_ignored, }; if matching_paths_tx.send(project_path).await.is_err() { break; @@ -5880,6 +5887,94 @@ impl Project { } }); } + + if query.include_ignored() { + for snapshot in snapshots { + for ignored_entry in snapshot + .entries(query.include_ignored()) + .filter(|e| e.is_ignored) + { + let limiter = Arc::clone(&max_concurrent_workers); + scope.spawn(async move { + let _guard = limiter.acquire().await; + let mut ignored_paths_to_process = + VecDeque::from([snapshot.abs_path().join(&ignored_entry.path)]); + while let Some(ignored_abs_path) = + ignored_paths_to_process.pop_front() + { + if !query.file_matches(Some(&ignored_abs_path)) + || snapshot.is_path_excluded(&ignored_abs_path) + { + continue; + } + if let Some(fs_metadata) = fs + .metadata(&ignored_abs_path) + .await + .with_context(|| { + format!("fetching fs metadata for {ignored_abs_path:?}") + }) + .log_err() + .flatten() + { + if fs_metadata.is_dir { + if let Some(mut subfiles) = fs + .read_dir(&ignored_abs_path) + .await + .with_context(|| { + format!( + "listing ignored path {ignored_abs_path:?}" + ) + }) + .log_err() + { + while let Some(subfile) = subfiles.next().await { + if let Some(subfile) = subfile.log_err() { + ignored_paths_to_process.push_back(subfile); + } + } + } + } else if !fs_metadata.is_symlink { + let matches = if let Some(file) = fs + .open_sync(&ignored_abs_path) + .await + .with_context(|| { + format!( + "Opening ignored path {ignored_abs_path:?}" + ) + }) + .log_err() + { + query.detect(file).unwrap_or(false) + } else { + false + }; + if matches { + let project_path = SearchMatchCandidate::Path { + worktree_id: snapshot.id(), + path: Arc::from( + ignored_abs_path + .strip_prefix(snapshot.abs_path()) + .expect( + "scanning worktree-related files", + ), + ), + is_ignored: true, + }; + if matching_paths_tx + .send(project_path) + .await + .is_err() + { + return; + } + } + } + } + } + }); + } + } + } }) .await; } @@ -5986,11 +6081,24 @@ impl Project { let (buffers_tx, buffers_rx) = smol::channel::bounded(1024); let (sorted_buffers_tx, sorted_buffers_rx) = futures::channel::oneshot::channel(); cx.spawn(move |this, cx| async move { - let mut buffers = vec![]; + let mut buffers = Vec::new(); + let mut ignored_buffers = Vec::new(); while let Some(entry) = matching_paths_rx.next().await { - buffers.push(entry); + if matches!( + entry, + SearchMatchCandidate::Path { + is_ignored: true, + .. + } + ) { + ignored_buffers.push(entry); + } else { + buffers.push(entry); + } } buffers.sort_by_key(|candidate| candidate.path()); + ignored_buffers.sort_by_key(|candidate| candidate.path()); + buffers.extend(ignored_buffers); let matching_paths = buffers.clone(); let _ = sorted_buffers_tx.send(buffers); for (index, candidate) in matching_paths.into_iter().enumerate() { @@ -6002,7 +6110,9 @@ impl Project { cx.spawn(move |mut cx| async move { let buffer = match candidate { SearchMatchCandidate::OpenBuffer { buffer, .. } => Some(buffer), - SearchMatchCandidate::Path { worktree_id, path } => this + SearchMatchCandidate::Path { + worktree_id, path, .. + } => this .update(&mut cx, |this, cx| { this.open_buffer((worktree_id, path), cx) })? diff --git a/crates/project2/src/worktree.rs b/crates/project2/src/worktree.rs index fcb64c40b4..e424375220 100644 --- a/crates/project2/src/worktree.rs +++ b/crates/project2/src/worktree.rs @@ -2222,7 +2222,7 @@ impl LocalSnapshot { paths } - fn is_abs_path_excluded(&self, abs_path: &Path) -> bool { + pub fn is_path_excluded(&self, abs_path: &Path) -> bool { self.file_scan_exclusions .iter() .any(|exclude_matcher| exclude_matcher.is_match(abs_path)) @@ -2395,26 +2395,10 @@ impl BackgroundScannerState { self.snapshot.check_invariants(false); } - fn reload_repositories(&mut self, changed_paths: &[Arc], fs: &dyn Fs) { + fn reload_repositories(&mut self, dot_git_dirs_to_reload: &HashSet, fs: &dyn Fs) { let scan_id = self.snapshot.scan_id; - // Find each of the .git directories that contain any of the given paths. - let mut prev_dot_git_dir = None; - for changed_path in changed_paths { - let Some(dot_git_dir) = changed_path - .ancestors() - .find(|ancestor| ancestor.file_name() == Some(&*DOT_GIT)) - else { - continue; - }; - - // Avoid processing the same repository multiple times, if multiple paths - // within it have changed. - if prev_dot_git_dir == Some(dot_git_dir) { - continue; - } - prev_dot_git_dir = Some(dot_git_dir); - + for dot_git_dir in dot_git_dirs_to_reload { // If there is already a repository for this .git directory, reload // the status for all of its files. let repository = self @@ -2426,7 +2410,7 @@ impl BackgroundScannerState { }); match repository { None => { - self.build_git_repository(dot_git_dir.into(), fs); + self.build_git_repository(Arc::from(dot_git_dir.as_path()), fs); } Some((entry_id, repository)) => { if repository.git_dir_scan_id == scan_id { @@ -2440,7 +2424,7 @@ impl BackgroundScannerState { continue; }; - log::info!("reload git repository {:?}", dot_git_dir); + log::info!("reload git repository {dot_git_dir:?}"); let repository = repository.repo_ptr.lock(); let branch = repository.branch_name(); repository.reload_index(); @@ -2471,7 +2455,9 @@ impl BackgroundScannerState { ids_to_preserve.insert(work_directory_id); } else { let git_dir_abs_path = snapshot.abs_path().join(&entry.git_dir_path); - if snapshot.is_abs_path_excluded(&git_dir_abs_path) + let git_dir_excluded = snapshot.is_path_excluded(&entry.git_dir_path) + || snapshot.is_path_excluded(&git_dir_abs_path); + if git_dir_excluded && !matches!(smol::block_on(fs.metadata(&git_dir_abs_path)), Ok(None)) { ids_to_preserve.insert(work_directory_id); @@ -3303,11 +3289,26 @@ impl BackgroundScanner { }; let mut relative_paths = Vec::with_capacity(abs_paths.len()); + let mut dot_git_paths_to_reload = HashSet::default(); abs_paths.sort_unstable(); abs_paths.dedup_by(|a, b| a.starts_with(&b)); abs_paths.retain(|abs_path| { let snapshot = &self.state.lock().snapshot; { + let mut is_git_related = false; + if let Some(dot_git_dir) = abs_path + .ancestors() + .find(|ancestor| ancestor.file_name() == Some(&*DOT_GIT)) + { + let dot_git_path = dot_git_dir + .strip_prefix(&root_canonical_path) + .ok() + .map(|path| path.to_path_buf()) + .unwrap_or_else(|| dot_git_dir.to_path_buf()); + dot_git_paths_to_reload.insert(dot_git_path.to_path_buf()); + is_git_related = true; + } + let relative_path: Arc = if let Ok(path) = abs_path.strip_prefix(&root_canonical_path) { path.into() @@ -3318,22 +3319,30 @@ impl BackgroundScanner { return false; }; - if !is_git_related(&abs_path) { - let parent_dir_is_loaded = relative_path.parent().map_or(true, |parent| { - snapshot - .entry_for_path(parent) - .map_or(false, |entry| entry.kind == EntryKind::Dir) - }); - if !parent_dir_is_loaded { - log::debug!("ignoring event {relative_path:?} within unloaded directory"); - return false; + let parent_dir_is_loaded = relative_path.parent().map_or(true, |parent| { + snapshot + .entry_for_path(parent) + .map_or(false, |entry| entry.kind == EntryKind::Dir) + }); + if !parent_dir_is_loaded { + log::debug!("ignoring event {relative_path:?} within unloaded directory"); + return false; + } + + // FS events may come for files which parent directory is excluded, need to check ignore those. + let mut path_to_test = abs_path.clone(); + let mut excluded_file_event = snapshot.is_path_excluded(abs_path) + || snapshot.is_path_excluded(&relative_path); + while !excluded_file_event && path_to_test.pop() { + if snapshot.is_path_excluded(&path_to_test) { + excluded_file_event = true; } - if snapshot.is_abs_path_excluded(abs_path) { - log::debug!( - "ignoring FS event for path {relative_path:?} within excluded directory" - ); - return false; + } + if excluded_file_event { + if !is_git_related { + log::debug!("ignoring FS event for excluded path {relative_path:?}"); } + return false; } relative_paths.push(relative_path); @@ -3341,31 +3350,39 @@ impl BackgroundScanner { } }); - if relative_paths.is_empty() { + if dot_git_paths_to_reload.is_empty() && relative_paths.is_empty() { return; } - log::debug!("received fs events {:?}", relative_paths); + if !relative_paths.is_empty() { + log::debug!("received fs events {:?}", relative_paths); - let (scan_job_tx, scan_job_rx) = channel::unbounded(); - self.reload_entries_for_paths( - root_path, - root_canonical_path, - &relative_paths, - abs_paths, - Some(scan_job_tx.clone()), - ) - .await; - drop(scan_job_tx); - self.scan_dirs(false, scan_job_rx).await; + let (scan_job_tx, scan_job_rx) = channel::unbounded(); + self.reload_entries_for_paths( + root_path, + root_canonical_path, + &relative_paths, + abs_paths, + Some(scan_job_tx.clone()), + ) + .await; + drop(scan_job_tx); + self.scan_dirs(false, scan_job_rx).await; - let (scan_job_tx, scan_job_rx) = channel::unbounded(); - self.update_ignore_statuses(scan_job_tx).await; - self.scan_dirs(false, scan_job_rx).await; + let (scan_job_tx, scan_job_rx) = channel::unbounded(); + self.update_ignore_statuses(scan_job_tx).await; + self.scan_dirs(false, scan_job_rx).await; + } { let mut state = self.state.lock(); - state.reload_repositories(&relative_paths, self.fs.as_ref()); + if !dot_git_paths_to_reload.is_empty() { + if relative_paths.is_empty() { + state.snapshot.scan_id += 1; + } + log::debug!("reloading repositories: {dot_git_paths_to_reload:?}"); + state.reload_repositories(&dot_git_paths_to_reload, self.fs.as_ref()); + } state.snapshot.completed_scan_id = state.snapshot.scan_id; for (_, entry_id) in mem::take(&mut state.removed_entry_ids) { state.scanned_dirs.remove(&entry_id); @@ -3505,7 +3522,7 @@ impl BackgroundScanner { let state = self.state.lock(); let snapshot = &state.snapshot; root_abs_path = snapshot.abs_path().clone(); - if snapshot.is_abs_path_excluded(&job.abs_path) { + if snapshot.is_path_excluded(&job.abs_path) { log::error!("skipping excluded directory {:?}", job.path); return Ok(()); } @@ -3577,7 +3594,7 @@ impl BackgroundScanner { { let mut state = self.state.lock(); - if state.snapshot.is_abs_path_excluded(&child_abs_path) { + if state.snapshot.is_path_excluded(&child_abs_path) { let relative_path = job.path.join(child_name); log::debug!("skipping excluded child entry {relative_path:?}"); state.remove_path(&relative_path); @@ -4119,12 +4136,6 @@ impl BackgroundScanner { } } -fn is_git_related(abs_path: &Path) -> bool { - abs_path - .components() - .any(|c| c.as_os_str() == *DOT_GIT || c.as_os_str() == *GITIGNORE) -} - fn char_bag_for_path(root_char_bag: CharBag, path: &Path) -> CharBag { let mut result = root_char_bag; result.extend( diff --git a/crates/project2/src/worktree_tests.rs b/crates/project2/src/worktree_tests.rs index df7307f694..501a5f736f 100644 --- a/crates/project2/src/worktree_tests.rs +++ b/crates/project2/src/worktree_tests.rs @@ -992,6 +992,146 @@ async fn test_file_scan_exclusions(cx: &mut TestAppContext) { }); } +#[gpui::test] +async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) { + init_test(cx); + cx.executor().allow_parking(); + let dir = temp_tree(json!({ + ".git": { + "HEAD": "ref: refs/heads/main\n", + "foo": "bar", + }, + ".gitignore": "**/target\n/node_modules\ntest_output\n", + "target": { + "index": "blah2" + }, + "node_modules": { + ".DS_Store": "", + "prettier": { + "package.json": "{}", + }, + }, + "src": { + ".DS_Store": "", + "foo": { + "foo.rs": "mod another;\n", + "another.rs": "// another", + }, + "bar": { + "bar.rs": "// bar", + }, + "lib.rs": "mod foo;\nmod bar;\n", + }, + ".DS_Store": "", + })); + cx.update(|cx| { + cx.update_global::(|store, cx| { + store.update_user_settings::(cx, |project_settings| { + project_settings.file_scan_exclusions = Some(vec![ + "**/.git".to_string(), + "node_modules/".to_string(), + "build_output".to_string(), + ]); + }); + }); + }); + + let tree = Worktree::local( + build_client(cx), + dir.path(), + true, + Arc::new(RealFs), + Default::default(), + &mut cx.to_async(), + ) + .await + .unwrap(); + cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) + .await; + tree.flush_fs_events(cx).await; + tree.read_with(cx, |tree, _| { + check_worktree_entries( + tree, + &[ + ".git/HEAD", + ".git/foo", + "node_modules/.DS_Store", + "node_modules/prettier", + "node_modules/prettier/package.json", + ], + &["target", "node_modules"], + &[ + ".DS_Store", + "src/.DS_Store", + "src/lib.rs", + "src/foo/foo.rs", + "src/foo/another.rs", + "src/bar/bar.rs", + ".gitignore", + ], + ) + }); + + let new_excluded_dir = dir.path().join("build_output"); + let new_ignored_dir = dir.path().join("test_output"); + std::fs::create_dir_all(&new_excluded_dir) + .unwrap_or_else(|e| panic!("Failed to create a {new_excluded_dir:?} directory: {e}")); + std::fs::create_dir_all(&new_ignored_dir) + .unwrap_or_else(|e| panic!("Failed to create a {new_ignored_dir:?} directory: {e}")); + let node_modules_dir = dir.path().join("node_modules"); + let dot_git_dir = dir.path().join(".git"); + let src_dir = dir.path().join("src"); + for existing_dir in [&node_modules_dir, &dot_git_dir, &src_dir] { + assert!( + existing_dir.is_dir(), + "Expect {existing_dir:?} to be present in the FS already" + ); + } + + for directory_for_new_file in [ + new_excluded_dir, + new_ignored_dir, + node_modules_dir, + dot_git_dir, + src_dir, + ] { + std::fs::write(directory_for_new_file.join("new_file"), "new file contents") + .unwrap_or_else(|e| { + panic!("Failed to create in {directory_for_new_file:?} a new file: {e}") + }); + } + tree.flush_fs_events(cx).await; + + tree.read_with(cx, |tree, _| { + check_worktree_entries( + tree, + &[ + ".git/HEAD", + ".git/foo", + ".git/new_file", + "node_modules/.DS_Store", + "node_modules/prettier", + "node_modules/prettier/package.json", + "node_modules/new_file", + "build_output", + "build_output/new_file", + "test_output/new_file", + ], + &["target", "node_modules", "test_output"], + &[ + ".DS_Store", + "src/.DS_Store", + "src/lib.rs", + "src/foo/foo.rs", + "src/foo/another.rs", + "src/bar/bar.rs", + "src/new_file", + ".gitignore", + ], + ) + }); +} + #[gpui::test(iterations = 30)] async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) { init_test(cx); @@ -1056,7 +1196,7 @@ async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) { async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) { init_test(cx); cx.executor().allow_parking(); - let client_fake = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); + let client_fake = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); let fs_fake = FakeFs::new(cx.background_executor.clone()); fs_fake @@ -1096,7 +1236,7 @@ async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) { assert!(tree.entry_for_path("a/b/").unwrap().is_dir()); }); - let client_real = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); + let client_real = cx.update(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); let fs_real = Arc::new(RealFs); let temp_root = temp_tree(json!({ @@ -2181,7 +2321,7 @@ async fn test_propagate_git_statuses(cx: &mut TestAppContext) { fn build_client(cx: &mut TestAppContext) -> Arc { let http_client = FakeHttpClient::with_404_response(); - cx.read(|cx| Client::new(http_client, cx)) + cx.update(|cx| Client::new(http_client, cx)) } #[track_caller] diff --git a/crates/project_panel2/Cargo.toml b/crates/project_panel2/Cargo.toml index bd6bc59a65..48abfbe1de 100644 --- a/crates/project_panel2/Cargo.toml +++ b/crates/project_panel2/Cargo.toml @@ -9,7 +9,6 @@ path = "src/project_panel.rs" doctest = false [dependencies] -context_menu = { path = "../context_menu" } collections = { path = "../collections" } db = { path = "../db2", package = "db2" } editor = { path = "../editor2", package = "editor2" } diff --git a/crates/project_panel2/src/project_panel.rs b/crates/project_panel2/src/project_panel.rs index 57ede0c961..d4c63e75bf 100644 --- a/crates/project_panel2/src/project_panel.rs +++ b/crates/project_panel2/src/project_panel.rs @@ -9,10 +9,9 @@ use file_associations::FileAssociations; use anyhow::{anyhow, Result}; use gpui::{ actions, div, px, uniform_list, Action, AppContext, AssetSource, AsyncWindowContext, - ClipboardItem, Component, Div, EventEmitter, FocusHandle, Focusable, FocusableView, - InteractiveComponent, Model, MouseButton, ParentComponent, Pixels, Point, PromptLevel, Render, - Stateful, StatefulInteractiveComponent, Styled, Task, UniformListScrollHandle, View, - ViewContext, VisualContext as _, WeakView, WindowContext, + ClipboardItem, Div, EventEmitter, FocusHandle, Focusable, FocusableView, InteractiveElement, + Model, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Render, Stateful, Styled, + Task, UniformListScrollHandle, View, ViewContext, VisualContext as _, WeakView, WindowContext, }; use menu::{Confirm, SelectNext, SelectPrev}; use project::{ @@ -30,7 +29,7 @@ use std::{ sync::Arc, }; use theme::ActiveTheme as _; -use ui::{h_stack, v_stack, IconElement, Label}; +use ui::{v_stack, IconElement, Label, ListItem}; use unicase::UniCase; use util::{maybe, ResultExt, TryFutureExt}; use workspace::{ @@ -247,7 +246,6 @@ impl ProjectPanel { let mut old_dock_position = this.position(cx); ProjectPanelSettings::register(cx); cx.observe_global::(move |this, cx| { - dbg!("OLA!"); let new_dock_position = this.position(cx); if new_dock_position != old_dock_position { old_dock_position = new_dock_position; @@ -372,7 +370,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) { @@ -645,6 +643,7 @@ impl ProjectPanel { } fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { + dbg!("odd"); self.edit_state = None; self.update_visible_entries(None, cx); cx.focus(&self.focus_handle); @@ -1335,13 +1334,19 @@ impl ProjectPanel { } } - fn render_entry_visual_element( - details: &EntryDetails, - editor: Option<&View>, - padding: Pixels, + fn render_entry( + &self, + entry_id: ProjectEntryId, + details: EntryDetails, + // dragged_entry_destination: &mut Option>, cx: &mut ViewContext, - ) -> Div { + ) -> ListItem { + let kind = details.kind; + let settings = ProjectPanelSettings::get_global(cx); let show_editor = details.is_editing && !details.is_processing; + let is_selected = self + .selection + .map_or(false, |selection| selection.entry_id == entry_id); let theme = cx.theme(); let filename_text_color = details @@ -1354,14 +1359,17 @@ impl ProjectPanel { }) .unwrap_or(theme.status().info); - h_stack() + ListItem::new(entry_id.to_proto() as usize) + .indent_level(details.depth) + .indent_step_size(px(settings.indent_size)) + .selected(is_selected) .child(if let Some(icon) = &details.icon { div().child(IconElement::from_path(icon.to_string())) } else { div() }) .child( - if let (Some(editor), true) = (editor, show_editor) { + if let (Some(editor), true) = (Some(&self.filename_editor), show_editor) { div().w_full().child(editor.clone()) } else { div() @@ -1370,34 +1378,7 @@ impl ProjectPanel { } .ml_1(), ) - .pl(padding) - } - - fn render_entry( - &self, - entry_id: ProjectEntryId, - details: EntryDetails, - // dragged_entry_destination: &mut Option>, - cx: &mut ViewContext, - ) -> Stateful> { - let kind = details.kind; - let settings = ProjectPanelSettings::get_global(cx); - const INDENT_SIZE: Pixels = px(16.0); - let padding = INDENT_SIZE + details.depth as f32 * px(settings.indent_size); - let show_editor = details.is_editing && !details.is_processing; - let is_selected = self - .selection - .map_or(false, |selection| selection.entry_id == entry_id); - - Self::render_entry_visual_element(&details, Some(&self.filename_editor), padding, cx) - .id(entry_id.to_proto() as usize) - .w_full() - .cursor_pointer() - .when(is_selected, |this| { - this.bg(cx.theme().colors().element_selected) - }) - .hover(|style| style.bg(cx.theme().colors().element_hover)) - .on_click(move |this, event, cx| { + .on_click(cx.listener(move |this, event: &gpui::ClickEvent, cx| { if !show_editor { if kind.is_dir() { this.toggle_expanded(entry_id, cx); @@ -1409,10 +1390,10 @@ impl ProjectPanel { } } } - }) - .on_mouse_down(MouseButton::Right, move |this, event, cx| { + })) + .on_secondary_mouse_down(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, @@ -1425,9 +1406,9 @@ impl ProjectPanel { } impl Render for ProjectPanel { - type Element = Focusable>>; + 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 { @@ -1435,40 +1416,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 48b530b7c5..4f64654bd3 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/search/src/project_search.rs b/crates/search/src/project_search.rs index 5f3a6db6d4..42969ecbb6 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1767,16 +1767,13 @@ impl View for ProjectSearchBar { render_option_button_icon("icons/word_search.svg", SearchOptions::WHOLE_WORD, cx) }); - let mut include_ignored = is_semantic_disabled.then(|| { + let include_ignored = is_semantic_disabled.then(|| { render_option_button_icon( - // TODO proper icon - "icons/case_insensitive.svg", + "icons/file_icons/git.svg", SearchOptions::INCLUDE_IGNORED, cx, ) }); - // TODO not implemented yet - let _ = include_ignored.take(); let search_button_for_mode = |mode, side, cx: &mut ViewContext| { let is_active = if let Some(search) = self.active_project_search.as_ref() { diff --git a/crates/search2/Cargo.toml b/crates/search2/Cargo.toml new file mode 100644 index 0000000000..97cfdd6494 --- /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 0000000000..d80d9f5d50 --- /dev/null +++ b/crates/search2/src/buffer_search.rs @@ -0,0 +1,1747 @@ +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, EditorMode}; +use futures::channel::oneshot; +use gpui::{ + actions, div, red, Action, AppContext, Div, EventEmitter, InteractiveElement as _, IntoElement, + ParentElement as _, Render, Styled, Subscription, Task, View, ViewContext, VisualContext as _, + WeakView, 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, +}; + +#[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(|editor: &mut Editor, cx| BufferSearchBar::register(editor, cx)) + .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)) + .on_action(cx.listener(Self::dismiss)) + .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(editor: &mut Editor, cx: &mut ViewContext) { + if editor.mode() != EditorMode::Full { + return; + }; + + let handle = cx.view().downgrade(); + + editor.register_action(move |a: &Deploy, cx| { + let Some(pane) = handle.upgrade().and_then(|editor| editor.read(cx).pane(cx)) else { + return; + }; + + 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( + editor: &mut Editor, + handle: WeakView, + update: fn(&mut BufferSearchBar, &A, &mut ViewContext), + ) { + editor.register_action(move |action: &A, cx| { + let Some(pane) = handle.upgrade().and_then(|editor| editor.read(cx).pane(cx)) + else { + return; + }; + 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(); + } + }) + }); + }); + } + + let handle = cx.view().downgrade(); + register_action( + editor, + handle.clone(), + |this, action: &ToggleCaseSensitive, cx| { + if this.supported_options().case { + this.toggle_case_sensitive(action, cx); + } + }, + ); + register_action( + editor, + handle.clone(), + |this, action: &ToggleWholeWord, cx| { + if this.supported_options().word { + this.toggle_whole_word(action, cx); + } + }, + ); + register_action( + editor, + handle.clone(), + |this, action: &ToggleReplace, cx| { + if this.supported_options().replacement { + this.toggle_replace(action, cx); + } + }, + ); + register_action(editor, handle.clone(), |this, _: &ActivateRegexMode, cx| { + if this.supported_options().regex { + this.activate_search_mode(SearchMode::Regex, cx); + } + }); + register_action(editor, handle.clone(), |this, _: &ActivateTextMode, cx| { + this.activate_search_mode(SearchMode::Text, cx); + }); + register_action(editor, handle.clone(), |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( + editor, + handle.clone(), + |this, action: &SelectNextMatch, cx| { + this.select_next_match(action, cx); + }, + ); + register_action( + editor, + handle.clone(), + |this, action: &SelectPrevMatch, cx| { + this.select_prev_match(action, cx); + }, + ); + register_action( + editor, + handle.clone(), + |this, action: &SelectAllMatches, cx| { + this.select_all_matches(action, cx); + }, + ); + register_action(editor, handle.clone(), |this, _: &editor::Cancel, cx| { + if !this.dismissed { + this.dismiss(&Dismiss, cx); + return; + } + cx.propagate(); + }); + } + 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 IntoElement { + // 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 0000000000..6b06c60293 --- /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 0000000000..817fb454d2 --- /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 0000000000..41dd87d4d3 --- /dev/null +++ b/crates/search2/src/project_search.rs @@ -0,0 +1,2680 @@ +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_option_action::(SearchOptions::INCLUDE_IGNORED, 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), + self.search_options.contains(SearchOptions::INCLUDE_IGNORED), + 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), + self.search_options.contains(SearchOptions::INCLUDE_IGNORED), + 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 include_ignored = is_semantic_disabled.then(|| { + render_option_button_icon( + "icons/file_icons/git.svg", + SearchOptions::INCLUDE_IGNORED, + 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( + Flex::row() + .with_child( + ChildView::new(&search.included_files_editor, cx) + .contained() + .constrained() + .with_height(theme.search.search_bar_row_height) + .flex(1., true), + ) + .with_children(include_ignored) + .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 0000000000..118d9054e6 --- /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, IntoElement}; +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 IntoElement { + 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 IntoElement { + // 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 IntoElement { + // 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 0000000000..1a7456f41c --- /dev/null +++ b/crates/search2/src/search_bar.rs @@ -0,0 +1,35 @@ +use gpui::{IntoElement, MouseDownEvent, 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 IntoElement { + // 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/semantic_index/src/semantic_index_tests.rs b/crates/semantic_index/src/semantic_index_tests.rs index 2145d1f9e0..f4e2c5ea13 100644 --- a/crates/semantic_index/src/semantic_index_tests.rs +++ b/crates/semantic_index/src/semantic_index_tests.rs @@ -1659,13 +1659,13 @@ fn elixir_lang() -> Arc { target: (identifier) @name) operator: "when") ]) - (#match? @name "^(def|defp|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp)$")) @item + (#any-match? @name "^(def|defp|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp)$")) @item ) (call target: (identifier) @name (arguments (alias) @name) - (#match? @name "^(defmodule|defprotocol)$")) @item + (#any-match? @name "^(defmodule|defprotocol)$")) @item "#, ) .unwrap(), diff --git a/crates/story/Cargo.toml b/crates/story/Cargo.toml new file mode 100644 index 0000000000..384447af8f --- /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 0000000000..a9998e65a8 --- /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 0000000000..d95c879ce0 --- /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 7c6776c930..16386706cf 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 2620e68d6c..0eaf3d126c 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 4f8c54fa6f..0000000000 --- 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().to_string()), - ) - .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 571882f1f2..6f757240eb 100644 --- a/crates/storybook2/src/stories/focus.rs +++ b/crates/storybook2/src/stories/focus.rs @@ -27,7 +27,7 @@ impl FocusStory { } impl Render for FocusStory { - type Element = Focusable>>; + 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 507aa8db2d..f79a27aa89 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 { @@ -12,18 +14,18 @@ impl KitchenSinkStory { } impl Render for KitchenSinkStory { - type Element = Stateful>; + 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 a3f9ef5eb8..8bcfb8923d 100644 --- a/crates/storybook2/src/stories/picker.rs +++ b/crates/storybook2/src/stories/picker.rs @@ -1,8 +1,11 @@ use fuzzy::StringMatchCandidate; -use gpui::{div, prelude::*, Div, KeyBinding, Render, Styled, Task, View, WindowContext}; +use gpui::{ + div, prelude::*, Div, KeyBinding, Render, SharedString, Styled, Task, View, WindowContext, +}; use picker::{Picker, PickerDelegate}; use std::sync::Arc; use theme2::ActiveTheme; +use ui::{Label, ListItem}; pub struct PickerStory { picker: View>, @@ -34,7 +37,7 @@ impl Delegate { } impl PickerDelegate for Delegate { - type ListItem = Div>; + type ListItem = ListItem; fn match_count(&self) -> usize { self.candidates.len() @@ -49,24 +52,18 @@ impl PickerDelegate for Delegate { ix: usize, selected: bool, cx: &mut gpui::ViewContext>, - ) -> Self::ListItem { - let colors = cx.theme().colors(); + ) -> Option { let Some(candidate_ix) = self.matches.get(ix) else { - return div(); + return None; }; - let candidate = self.candidates[*candidate_ix].string.clone(); + // TASK: Make StringMatchCandidate::string a SharedString + let candidate = SharedString::from(self.candidates[*candidate_ix].string.clone()); - div() - .text_color(colors.text) - .when(selected, |s| { - s.border_l_10().border_color(colors.terminal_ansi_yellow) - }) - .hover(|style| { - style - .bg(colors.element_active) - .text_color(colors.text_accent) - }) - .child(candidate) + Some( + ListItem::new(ix) + .selected(selected) + .child(Label::new(candidate)), + ) } fn selected_index(&self) -> usize { @@ -203,7 +200,7 @@ impl PickerStory { } impl Render for PickerStory { - type Element = Div; + 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 f1bb7b4e7c..9b9a54e1e6 100644 --- a/crates/storybook2/src/stories/scroll.rs +++ b/crates/storybook2/src/stories/scroll.rs @@ -11,7 +11,7 @@ impl ScrollStory { } impl Render for ScrollStory { - type Element = Stateful>; + 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 512d680d37..42009136c4 100644 --- a/crates/storybook2/src/stories/text.rs +++ b/crates/storybook2/src/stories/text.rs @@ -1,5 +1,6 @@ use gpui::{ - blue, div, red, white, Div, ParentComponent, Render, Styled, View, VisualContext, WindowContext, + blue, div, green, red, white, Div, InteractiveText, ParentElement, Render, Styled, StyledText, + TextRun, View, VisualContext, WindowContext, }; use ui::v_stack; @@ -12,7 +13,7 @@ impl TextStory { } impl Render for TextStory { - type Element = Div; + type Element = Div; fn render(&mut self, cx: &mut gpui::ViewContext) -> Self::Element { v_stack() @@ -55,6 +56,21 @@ impl Render for TextStory { "flex-row. width 96. 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.", - ))) + ))).child( + InteractiveText::new( + "interactive", + StyledText::new("Hello world, how is it going?").with_runs(vec![ + cx.text_style().to_run(6), + TextRun { + background_color: Some(green()), + ..cx.text_style().to_run(5) + }, + cx.text_style().to_run(18), + ]), + ) + .on_click(vec![2..4, 1..3, 7..9], |range_ix, cx| { + println!("Clicked range {range_ix}"); + }) + ) } } diff --git a/crates/storybook2/src/stories/z_index.rs b/crates/storybook2/src/stories/z_index.rs index 46ec0f4a35..9d04d3d81f 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}; +use gpui::{px, rgb, Div, Hsla, IntoElement, 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; + 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,19 +74,17 @@ trait Styles: Styled + Sized { } } -impl Styles for Div {} +impl Styles for Div {} -#[derive(Component)] +#[derive(IntoElement)] struct ZIndexExample { z_index: u32, } -impl ZIndexExample { - pub fn new(z_index: u32) -> Self { - Self { z_index } - } +impl RenderOnce for ZIndexExample { + type Rendered = Div; - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + fn render(self, cx: &mut WindowContext) -> Self::Rendered { div() .relative() .size_full() @@ -109,14 +104,14 @@ impl ZIndexExample { // HACK: Simulate `text-align: center`. .pl(px(24.)) .z_index(self.z_index) - .child(format!( + .child(SharedString::from(format!( "z-index: {}", if self.z_index == 0 { "auto".to_string() } else { self.z_index.to_string() } - )), + ))), ) // Blue blocks. .child( @@ -173,3 +168,9 @@ impl ZIndexExample { ) } } + +impl ZIndexExample { + pub fn new(z_index: u32) -> Self { + Self { z_index } + } +} diff --git a/crates/storybook2/src/story.rs b/crates/storybook2/src/story.rs deleted file mode 100644 index 5c144fdbc1..0000000000 --- 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 040bd75189..0c4abf9a13 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}; #[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, + ListItem, Scroll, - Tab, - TabBar, - Terminal, Text, - ThemeSelector, - TitleBar, - Toast, - Toolbar, - TrafficLights, - Workspace, ZIndex, Picker, } @@ -58,44 +31,18 @@ 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::Avatar => cx.build_view(|_| ui::AvatarStory).into(), + Self::Button => cx.build_view(|_| ui::ButtonStory).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::Icon => cx.build_view(|_| ui::IconStory).into(), + Self::Input => cx.build_view(|_| ui::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::Label => cx.build_view(|_| ui::LabelStory).into(), + Self::ListItem => cx.build_view(|_| ui::ListItemStory).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 a0bc7cd72f..2a62c135b1 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); @@ -106,7 +102,7 @@ impl StoryWrapper { } impl Render for StoryWrapper { - type Element = Div; + 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 8b04e4d44b..0000000000 --- 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 9885208b41..0000000000 --- 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_element.rs b/crates/terminal_view2/src/terminal_element.rs index e93d82047d..363dd90287 100644 --- a/crates/terminal_view2/src/terminal_element.rs +++ b/crates/terminal_view2/src/terminal_element.rs @@ -1,8 +1,11 @@ +// #![allow(unused)] // todo!() + // use editor::{Cursor, HighlightedRange, HighlightedRangeLine}; // use gpui::{ // point, transparent_black, AnyElement, AppContext, Bounds, Component, CursorStyle, Element, -// FontStyle, FontWeight, HighlightStyle, Hsla, LayoutId, Line, ModelContext, MouseButton, -// Overlay, Pixels, Point, Quad, TextStyle, Underline, ViewContext, WeakModel, WindowContext, +// ElementId, FontStyle, FontWeight, HighlightStyle, Hsla, IntoElement, IsZero, LayoutId, +// ModelContext, Overlay, Pixels, Point, Quad, TextRun, TextStyle, TextSystem, Underline, +// ViewContext, WeakModel, WindowContext, // }; // use itertools::Itertools; // use language::CursorShape; @@ -15,15 +18,10 @@ // index::Point as AlacPoint, // term::{cell::Flags, TermMode}, // }, -// // mappings::colors::convert_color, // terminal_settings::TerminalSettings, -// IndexedCell, -// Terminal, -// TerminalContent, -// TerminalSize, +// IndexedCell, Terminal, TerminalContent, TerminalSize, // }; // use theme::ThemeSettings; -// use workspace::ElementId; // use std::mem; // use std::{fmt::Debug, ops::RangeInclusive}; @@ -40,7 +38,7 @@ // size: TerminalSize, // mode: TermMode, // display_offset: usize, -// hyperlink_tooltip: Option>, +// hyperlink_tooltip: Option, // gutter: f32, // } @@ -183,9 +181,9 @@ // grid: &Vec, // text_style: &TextStyle, // terminal_theme: &TerminalStyle, -// text_layout_cache: &TextLayoutCache, -// font_cache: &FontCache, +// text_system: &TextSystem, // hyperlink: Option<(HighlightStyle, &RangeInclusive)>, +// cx: &mut WindowContext<'_>, // ) -> (Vec, Vec) { // let mut cells = vec![]; // let mut rects = vec![]; @@ -252,15 +250,15 @@ // fg, // terminal_theme, // text_style, -// font_cache, +// text_system, // hyperlink, // ); -// let layout_cell = text_layout_cache.layout_str( +// let layout_cell = text_system.layout_line( // cell_text, -// text_style.font_size, +// text_style.font_size.to_pixels(cx.rem_size()), // &[(cell_text.len(), cell_style)], -// ); +// )?; // cells.push(LayoutCell::new( // AlacPoint::new(line_index as i32, cell.point.column.0 as i32), @@ -311,24 +309,27 @@ // fg: terminal::alacritty_terminal::ansi::Color, // style: &TerminalStyle, // text_style: &TextStyle, -// font_cache: &FontCache, +// text_system: &TextSystem, // hyperlink: Option<(HighlightStyle, &RangeInclusive)>, -// ) -> RunStyle { +// ) -> TextRun { // let flags = indexed.cell.flags; // let fg = convert_color(&fg, &style); // let mut underline = flags // .intersects(Flags::ALL_UNDERLINES) // .then(|| Underline { -// color: Some(fg), -// squiggly: flags.contains(Flags::UNDERCURL), -// thickness: OrderedFloat(1.), +// color: fg, +// thickness: Pixels::from(1.0).scale(1.0), +// order: todo!(), +// bounds: todo!(), +// content_mask: todo!(), +// wavy: flags.contains(Flags::UNDERCURL), // }) // .unwrap_or_default(); // if indexed.cell.hyperlink().is_some() { -// if underline.thickness == OrderedFloat(0.) { -// underline.thickness = OrderedFloat(1.); +// if underline.thickness.is_zero() { +// underline.thickness = Pixels::from(1.0).scale(1.0); // } // } @@ -340,11 +341,11 @@ // properties = *properties.style(FontStyle::Italic); // } -// let font_id = font_cache +// let font_id = text_system // .select_font(text_style.font_family, &properties) // .unwrap_or(text_style.font_id); -// let mut result = RunStyle { +// let mut result = TextRun { // color: fg, // font_id, // underline, @@ -353,7 +354,7 @@ // if let Some((style, range)) = hyperlink { // if range.contains(&indexed.point) { // if let Some(underline) = style.underline { -// result.underline = underline; +// result.underline = Some(underline); // } // if let Some(color) = style.color { @@ -365,22 +366,23 @@ // result // } -// fn generic_button_handler( -// connection: WeakModel, -// origin: Point, -// f: impl Fn(&mut Terminal, Point, E, &mut ModelContext), -// ) -> impl Fn(E, &mut TerminalView, &mut EventContext) { -// move |event, _: &mut TerminalView, cx| { -// cx.focus_parent(); -// if let Some(conn_handle) = connection.upgrade() { -// conn_handle.update(cx, |terminal, cx| { -// f(terminal, origin, event, cx); +// // todo!() +// // fn generic_button_handler( +// // connection: WeakModel, +// // origin: Point, +// // f: impl Fn(&mut Terminal, Point, E, &mut ModelContext), +// // ) -> impl Fn(E, &mut TerminalView, &mut EventContext) { +// // move |event, _: &mut TerminalView, cx| { +// // cx.focus_parent(); +// // if let Some(conn_handle) = connection.upgrade() { +// // conn_handle.update(cx, |terminal, cx| { +// // f(terminal, origin, event, cx); -// cx.notify(); -// }) -// } -// } -// } +// // cx.notify(); +// // }) +// // } +// // } +// // } // fn attach_mouse_handlers( // &self, @@ -389,144 +391,144 @@ // mode: TermMode, // cx: &mut ViewContext, // ) { -// let connection = self.terminal; +// // todo!() +// // let connection = self.terminal; -// let mut region = MouseRegion::new::(cx.view_id(), 0, visible_bounds); +// // let mut region = MouseRegion::new::(cx.view_id(), 0, visible_bounds); -// // Terminal Emulator controlled behavior: -// region = region -// // Start selections -// .on_down(MouseButton::Left, move |event, v: &mut TerminalView, cx| { -// let terminal_view = cx.handle(); -// cx.focus(&terminal_view); -// v.context_menu.update(cx, |menu, _cx| menu.delay_cancel()); -// if let Some(conn_handle) = connection.upgrade() { -// conn_handle.update(cx, |terminal, cx| { -// terminal.mouse_down(&event, origin); +// // // Terminal Emulator controlled behavior: +// // region = region +// // // Start selections +// // .on_down(MouseButton::Left, move |event, v: &mut TerminalView, cx| { +// // let terminal_view = cx.handle(); +// // cx.focus(&terminal_view); +// // v.context_menu.update(cx, |menu, _cx| menu.delay_cancel()); +// // if let Some(conn_handle) = connection.upgrade() { +// // conn_handle.update(cx, |terminal, cx| { +// // terminal.mouse_down(&event, origin); -// cx.notify(); -// }) -// } -// }) -// // Update drag selections -// .on_drag(MouseButton::Left, move |event, _: &mut TerminalView, cx| { -// if event.end { -// return; -// } +// // cx.notify(); +// // }) +// // } +// // }) +// // // Update drag selections +// // .on_drag(MouseButton::Left, move |event, _: &mut TerminalView, cx| { +// // if event.end { +// // return; +// // } -// if cx.is_self_focused() { -// if let Some(conn_handle) = connection.upgrade() { -// conn_handle.update(cx, |terminal, cx| { -// terminal.mouse_drag(event, origin); -// cx.notify(); -// }) -// } -// } -// }) -// // Copy on up behavior -// .on_up( -// MouseButton::Left, -// TerminalElement::generic_button_handler( -// connection, -// origin, -// move |terminal, origin, e, cx| { -// terminal.mouse_up(&e, origin, cx); -// }, -// ), -// ) -// // Context menu -// .on_click( -// MouseButton::Right, -// move |event, view: &mut TerminalView, cx| { -// let mouse_mode = if let Some(conn_handle) = connection.upgrade() { -// conn_handle.update(cx, |terminal, _cx| terminal.mouse_mode(event.shift)) -// } else { -// // If we can't get the model handle, probably can't deploy the context menu -// true -// }; -// if !mouse_mode { -// view.deploy_context_menu(event.position, cx); -// } -// }, -// ) -// .on_move(move |event, _: &mut TerminalView, cx| { -// if cx.is_self_focused() { -// if let Some(conn_handle) = connection.upgrade() { -// conn_handle.update(cx, |terminal, cx| { -// terminal.mouse_move(&event, origin); -// cx.notify(); -// }) -// } -// } -// }) -// .on_scroll(move |event, _: &mut TerminalView, cx| { -// if let Some(conn_handle) = connection.upgrade() { -// conn_handle.update(cx, |terminal, cx| { -// terminal.scroll_wheel(event, origin); -// cx.notify(); -// }) -// } -// }); +// // if cx.is_self_focused() { +// // if let Some(conn_handle) = connection.upgrade() { +// // conn_handle.update(cx, |terminal, cx| { +// // terminal.mouse_drag(event, origin); +// // cx.notify(); +// // }) +// // } +// // } +// // }) +// // // Copy on up behavior +// // .on_up( +// // MouseButton::Left, +// // TerminalElement::generic_button_handler( +// // connection, +// // origin, +// // move |terminal, origin, e, cx| { +// // terminal.mouse_up(&e, origin, cx); +// // }, +// // ), +// // ) +// // // Context menu +// // .on_click( +// // MouseButton::Right, +// // move |event, view: &mut TerminalView, cx| { +// // let mouse_mode = if let Some(conn_handle) = connection.upgrade() { +// // conn_handle.update(cx, |terminal, _cx| terminal.mouse_mode(event.shift)) +// // } else { +// // // If we can't get the model handle, probably can't deploy the context menu +// // true +// // }; +// // if !mouse_mode { +// // view.deploy_context_menu(event.position, cx); +// // } +// // }, +// // ) +// // .on_move(move |event, _: &mut TerminalView, cx| { +// // if cx.is_self_focused() { +// // if let Some(conn_handle) = connection.upgrade() { +// // conn_handle.update(cx, |terminal, cx| { +// // terminal.mouse_move(&event, origin); +// // cx.notify(); +// // }) +// // } +// // } +// // }) +// // .on_scroll(move |event, _: &mut TerminalView, cx| { +// // if let Some(conn_handle) = connection.upgrade() { +// // conn_handle.update(cx, |terminal, cx| { +// // terminal.scroll_wheel(event, origin); +// // cx.notify(); +// // }) +// // } +// // }); -// // Mouse mode handlers: -// // All mouse modes need the extra click handlers -// if mode.intersects(TermMode::MOUSE_MODE) { -// region = region -// .on_down( -// MouseButton::Right, -// TerminalElement::generic_button_handler( -// connection, -// origin, -// move |terminal, origin, e, _cx| { -// terminal.mouse_down(&e, origin); -// }, -// ), -// ) -// .on_down( -// MouseButton::Middle, -// TerminalElement::generic_button_handler( -// connection, -// origin, -// move |terminal, origin, e, _cx| { -// terminal.mouse_down(&e, origin); -// }, -// ), -// ) -// .on_up( -// MouseButton::Right, -// TerminalElement::generic_button_handler( -// connection, -// origin, -// move |terminal, origin, e, cx| { -// terminal.mouse_up(&e, origin, cx); -// }, -// ), -// ) -// .on_up( -// MouseButton::Middle, -// TerminalElement::generic_button_handler( -// connection, -// origin, -// move |terminal, origin, e, cx| { -// terminal.mouse_up(&e, origin, cx); -// }, -// ), -// ) -// } +// // // Mouse mode handlers: +// // // All mouse modes need the extra click handlers +// // if mode.intersects(TermMode::MOUSE_MODE) { +// // region = region +// // .on_down( +// // MouseButton::Right, +// // TerminalElement::generic_button_handler( +// // connection, +// // origin, +// // move |terminal, origin, e, _cx| { +// // terminal.mouse_down(&e, origin); +// // }, +// // ), +// // ) +// // .on_down( +// // MouseButton::Middle, +// // TerminalElement::generic_button_handler( +// // connection, +// // origin, +// // move |terminal, origin, e, _cx| { +// // terminal.mouse_down(&e, origin); +// // }, +// // ), +// // ) +// // .on_up( +// // MouseButton::Right, +// // TerminalElement::generic_button_handler( +// // connection, +// // origin, +// // move |terminal, origin, e, cx| { +// // terminal.mouse_up(&e, origin, cx); +// // }, +// // ), +// // ) +// // .on_up( +// // MouseButton::Middle, +// // TerminalElement::generic_button_handler( +// // connection, +// // origin, +// // move |terminal, origin, e, cx| { +// // terminal.mouse_up(&e, origin, cx); +// // }, +// // ), +// // ) +// // } -// cx.scene().push_mouse_region(region); +// // cx.scene().push_mouse_region(region); // } // } -// impl Element for TerminalElement { -// type ElementState = LayoutState; +// impl Element for TerminalElement { +// type State = LayoutState; // fn layout( // &mut self, -// view_state: &mut TerminalView, -// element_state: Option, -// cx: &mut ViewContext, -// ) -> (LayoutId, Self::ElementState) { +// element_state: Option, +// cx: &mut WindowContext<'_>, +// ) -> (LayoutId, Self::State) { // let settings = ThemeSettings::get_global(cx); // let terminal_settings = TerminalSettings::get_global(cx); @@ -535,7 +537,7 @@ // let link_style = settings.theme.editor.link_definition; // let tooltip_style = settings.theme.tooltip.clone(); -// let font_cache = cx.font_cache(); +// let text_system = cx.text_system(); // let font_size = font_size(&terminal_settings, cx).unwrap_or(settings.buffer_font_size(cx)); // let font_family_name = terminal_settings // .font_family @@ -545,30 +547,37 @@ // .font_features // .as_ref() // .unwrap_or(&settings.buffer_font_features); -// let family_id = font_cache +// let family_id = text_system // .load_family(&[font_family_name], &font_features) // .log_err() // .unwrap_or(settings.buffer_font_family); -// let font_id = font_cache +// let font_id = text_system // .select_font(family_id, &Default::default()) // .unwrap(); // let text_style = TextStyle { // color: settings.theme.editor.text_color, // font_family_id: family_id, -// font_family_name: font_cache.family_name(family_id).unwrap(), +// font_family_name: text_system.family_name(family_id).unwrap(), // font_id, // font_size, // font_properties: Default::default(), // underline: Default::default(), // soft_wrap: false, +// font_family: todo!(), +// font_features: todo!(), +// line_height: todo!(), +// font_weight: todo!(), +// font_style: todo!(), +// background_color: todo!(), +// white_space: todo!(), // }; // let selection_color = settings.theme.editor.selection.selection; // let match_color = settings.theme.search.match_background; // let gutter; // let dimensions = { // let line_height = text_style.font_size * terminal_settings.line_height.value(); -// let cell_width = font_cache.em_advance(text_style.font_id, text_style.font_size); +// let cell_width = text_system.em_advance(text_style.font_id, text_style.font_size); // gutter = cell_width; // let size = constraint.max - point(gutter, 0.); @@ -645,11 +654,11 @@ // cells, // &text_style, // &terminal_theme, -// cx.text_layout_cache(), -// cx.font_cache(), +// &cx.text_system(), // last_hovered_word // .as_ref() // .map(|last_hovered_word| (link_style, &last_hovered_word.word_match)), +// cx, // ); // //Layout cursor. Rectangle is used for IME, so we should lay it out even @@ -667,18 +676,18 @@ // terminal_theme.foreground // }; -// cx.text_layout_cache().layout_str( +// cx.text_system().layout_line( // &str_trxt, // text_style.font_size, // &[( // str_trxt.len(), -// RunStyle { +// TextRun { // font_id: text_style.font_id, // color, // underline: Default::default(), // }, // )], -// ) +// )? // }; // let focused = self.focused; @@ -709,7 +718,7 @@ // //Done! // ( // constraint.max, -// Self::ElementState { +// Self::State { // cells, // cursor, // background_color, @@ -725,93 +734,89 @@ // } // fn paint( -// &mut self, +// self, // bounds: Bounds, -// view_state: &mut TerminalView, -// element_state: &mut Self::ElementState, -// cx: &mut ViewContext, +// element_state: &mut Self::State, +// cx: &mut WindowContext<'_>, // ) { -// let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default(); +// // todo!() +// // let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default(); -// //Setup element stuff -// let clip_bounds = Some(visible_bounds); +// // //Setup element stuff +// // let clip_bounds = Some(visible_bounds); -// cx.paint_layer(clip_bounds, |cx| { -// let origin = bounds.origin + point(element_state.gutter, 0.); +// // cx.paint_layer(clip_bounds, |cx| { +// // let origin = bounds.origin + point(element_state.gutter, 0.); -// // Elements are ephemeral, only at paint time do we know what could be clicked by a mouse -// self.attach_mouse_handlers(origin, visible_bounds, element_state.mode, cx); +// // // Elements are ephemeral, only at paint time do we know what could be clicked by a mouse +// // self.attach_mouse_handlers(origin, visible_bounds, element_state.mode, cx); -// cx.scene().push_cursor_region(gpui::CursorRegion { -// bounds, -// style: if element_state.hyperlink_tooltip.is_some() { -// CursorStyle::AlacPointingHand -// } else { -// CursorStyle::IBeam -// }, -// }); +// // cx.scene().push_cursor_region(gpui::CursorRegion { +// // bounds, +// // style: if element_state.hyperlink_tooltip.is_some() { +// // CursorStyle::AlacPointingHand +// // } else { +// // CursorStyle::IBeam +// // }, +// // }); -// cx.paint_layer(clip_bounds, |cx| { -// //Start with a background color -// cx.scene().push_quad(Quad { -// bounds, -// background: Some(element_state.background_color), -// border: Default::default(), -// corner_radii: Default::default(), -// }); +// // cx.paint_layer(clip_bounds, |cx| { +// // //Start with a background color +// // cx.scene().push_quad(Quad { +// // bounds, +// // background: Some(element_state.background_color), +// // border: Default::default(), +// // corner_radii: Default::default(), +// // }); -// for rect in &element_state.rects { -// rect.paint(origin, element_state, view_state, cx); -// } -// }); +// // for rect in &element_state.rects { +// // rect.paint(origin, element_state, view_state, cx); +// // } +// // }); -// //Draw Highlighted Backgrounds -// cx.paint_layer(clip_bounds, |cx| { -// for (relative_highlighted_range, color) in -// element_state.relative_highlighted_ranges.iter() -// { -// if let Some((start_y, highlighted_range_lines)) = to_highlighted_range_lines( -// relative_highlighted_range, -// element_state, -// origin, -// ) { -// let hr = HighlightedRange { -// start_y, //Need to change this -// line_height: element_state.size.line_height, -// lines: highlighted_range_lines, -// color: color.clone(), -// //Copied from editor. TODO: move to theme or something -// corner_radius: 0.15 * element_state.size.line_height, -// }; -// hr.paint(bounds, cx); -// } -// } -// }); +// // //Draw Highlighted Backgrounds +// // cx.paint_layer(clip_bounds, |cx| { +// // for (relative_highlighted_range, color) in +// // element_state.relative_highlighted_ranges.iter() +// // { +// // if let Some((start_y, highlighted_range_lines)) = to_highlighted_range_lines( +// // relative_highlighted_range, +// // element_state, +// // origin, +// // ) { +// // let hr = HighlightedRange { +// // start_y, //Need to change this +// // line_height: element_state.size.line_height, +// // lines: highlighted_range_lines, +// // color: color.clone(), +// // //Copied from editor. TODO: move to theme or something +// // corner_radius: 0.15 * element_state.size.line_height, +// // }; +// // hr.paint(bounds, cx); +// // } +// // } +// // }); -// //Draw the text cells -// cx.paint_layer(clip_bounds, |cx| { -// for cell in &element_state.cells { -// cell.paint(origin, element_state, visible_bounds, view_state, cx); -// } -// }); +// // //Draw the text cells +// // cx.paint_layer(clip_bounds, |cx| { +// // for cell in &element_state.cells { +// // cell.paint(origin, element_state, visible_bounds, view_state, cx); +// // } +// // }); -// //Draw cursor -// if self.cursor_visible { -// if let Some(cursor) = &element_state.cursor { -// cx.paint_layer(clip_bounds, |cx| { -// cursor.paint(origin, cx); -// }) -// } -// } +// // //Draw cursor +// // if self.cursor_visible { +// // if let Some(cursor) = &element_state.cursor { +// // cx.paint_layer(clip_bounds, |cx| { +// // cursor.paint(origin, cx); +// // }) +// // } +// // } -// if let Some(element) = &mut element_state.hyperlink_tooltip { -// element.paint(origin, visible_bounds, view_state, cx) -// } -// }); -// } - -// fn element_id(&self) -> Option { -// todo!() +// // if let Some(element) = &mut element_state.hyperlink_tooltip { +// // element.paint(origin, visible_bounds, view_state, cx) +// // } +// // }); // } // // todo!() remove? @@ -822,7 +827,7 @@ // // fn debug( // // &self, // // _: Bounds, -// // _: &Self::ElementState, +// // _: &Self::State, // // _: &Self::PaintState, // // _: &TerminalView, // // _: &gpui::ViewContext, @@ -837,7 +842,7 @@ // // _: Range, // // bounds: Bounds, // // _: Bounds, -// // layout: &Self::ElementState, +// // layout: &Self::State, // // _: &Self::PaintState, // // _: &TerminalView, // // _: &gpui::ViewContext, @@ -855,10 +860,16 @@ // // } // } -// impl Component for TerminalElement { -// fn render(self) -> AnyElement { +// impl IntoElement for TerminalElement { +// type Element = Self; + +// fn element_id(&self) -> Option { // todo!() // } + +// fn into_element(self) -> Self::Element { +// self +// } // } // fn is_blank(cell: &IndexedCell) -> bool { @@ -952,3 +963,8 @@ // .font_size // .map(|size| theme::adjusted_font_size(size, cx)) // } + +// // mappings::colors::convert_color +// fn convert_color(fg: &terminal::alacritty_terminal::ansi::Color, style: &TerminalStyle) -> Hsla { +// todo!() +// } diff --git a/crates/terminal_view2/src/terminal_panel.rs b/crates/terminal_view2/src/terminal_panel.rs index 944cd912be..b6582b07b1 100644 --- a/crates/terminal_view2/src/terminal_panel.rs +++ b/crates/terminal_view2/src/terminal_panel.rs @@ -4,7 +4,7 @@ use crate::TerminalView; use db::kvp::KEY_VALUE_STORE; use gpui::{ actions, div, serde_json, AppContext, AsyncWindowContext, Div, Entity, EventEmitter, - FocusHandle, FocusableView, ParentComponent, Render, Subscription, Task, View, ViewContext, + FocusHandle, FocusableView, ParentElement, Render, Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowContext, }; use project::Fs; @@ -336,7 +336,7 @@ impl TerminalPanel { impl EventEmitter for TerminalPanel {} impl Render for TerminalPanel { - type Element = Div; + 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 27e55602fb..9f3ed31388 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, Component, DispatchPhase, Div, EventEmitter, - FocusEvent, FocusHandle, Focusable, FocusableComponent, FocusableView, InputHandler, - InteractiveComponent, KeyDownEvent, Keystroke, Model, MouseButton, ParentComponent, 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,11 +299,8 @@ impl TerminalView { cx: &mut ViewContext, ) { self.context_menu = Some(ContextMenu::build(cx, |menu, _| { - menu.action(ListEntry::new(Label::new("Clear")), Box::new(Clear)) - .action( - ListEntry::new(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!() @@ -505,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); @@ -538,7 +529,7 @@ impl TerminalView { } impl Render for TerminalView { - type Element = Focusable>; + type Element = Focusable
; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { let terminal_handle = self.terminal.clone().downgrade(); @@ -552,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( @@ -569,19 +560,22 @@ 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 .clone() - .map(|context_menu| div().z_index(1).absolute().child(context_menu.render())), + .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)) } } @@ -746,17 +740,13 @@ 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() .child(IconElement::new(Icon::Terminal)) - .child(title) - .render() + .child(Label::new(title)) + .into_any() } fn clone_on_split( @@ -792,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 22bea20e16..9ed387ebce 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/one_themes.rs b/crates/theme2/src/one_themes.rs index 533323ce51..2f663618a6 100644 --- a/crates/theme2/src/one_themes.rs +++ b/crates/theme2/src/one_themes.rs @@ -59,8 +59,8 @@ pub(crate) fn one_dark() -> Theme { ghost_element_active: hsla(220.0 / 360., 11.8 / 100., 20.0 / 100., 1.0), ghost_element_selected: hsla(224.0 / 360., 11.3 / 100., 26.1 / 100., 1.0), ghost_element_disabled: hsla(224.0 / 360., 11.3 / 100., 26.1 / 100., 1.0), - text: hsla(222.9 / 360., 9.1 / 100., 84.9 / 100., 1.0), - text_muted: hsla(220.0 / 360., 6.4 / 100., 45.7 / 100., 1.0), + text: hsla(221. / 360., 11. / 100., 86. / 100., 1.0), + text_muted: hsla(218.0 / 360., 7. / 100., 46. / 100., 1.0), text_placeholder: hsla(220.0 / 360., 6.6 / 100., 44.5 / 100., 1.0), text_disabled: hsla(220.0 / 360., 6.6 / 100., 44.5 / 100., 1.0), text_accent: hsla(222.6 / 360., 77.5 / 100., 65.1 / 100., 1.0), diff --git a/crates/theme2/src/settings.rs b/crates/theme2/src/settings.rs index 01951f2ed0..15b578d4b0 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 4296d4f99c..0000000000 --- a/crates/theme2/src/story.rs +++ /dev/null @@ -1,38 +0,0 @@ -use gpui::{div, Component, Div, ParentComponent, 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: &str) -> impl Component { - div() - .text_xl() - .text_color(cx.theme().colors().text) - .child(title.to_owned()) - } - - pub fn title_for(cx: &mut ViewContext) -> impl Component { - Self::title(cx, std::any::type_name::()) - } - - pub fn label(cx: &mut ViewContext, label: &str) -> impl Component { - div() - .mt_4() - .mb_2() - .text_xs() - .text_color(cx.theme().colors().text) - .child(label.to_owned()) - } -} diff --git a/crates/theme2/src/styles.rs b/crates/theme2/src/styles.rs index 18f9e76581..13a59f486d 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 b8a983ba51..e8bce8e578 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, ParentComponent, 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 0000000000..a7d8885848 --- /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 0000000000..af6af96548 --- /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 0000000000..d189d3bfb0 --- /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 fb89604865..c5c79237ba 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, @@ -128,6 +134,12 @@ impl Theme { ignored: self.status().ignored, } } + + /// Returns the [`Appearance`] for the theme. + #[inline(always)] + pub fn appearance(&self) -> Appearance { + self.appearance + } } #[derive(Clone, Debug, Default)] @@ -138,8 +150,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 efbec22bee..9f98b92296 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/docs/hello-world.md b/crates/ui2/docs/hello-world.md index e8ed3bb944..f48dd460b8 100644 --- a/crates/ui2/docs/hello-world.md +++ b/crates/ui2/docs/hello-world.md @@ -49,13 +49,13 @@ use gpui::hsla impl TodoList { // ... - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { div().size_4().bg(hsla(50.0/360.0, 1.0, 0.5, 1.0)) } } ~~~ -Every component needs a render method, and it should return `impl Component`. This basic component will render a 16x16px yellow square on the screen. +Every component needs a render method, and it should return `impl Element`. This basic component will render a 16x16px yellow square on the screen. A couple of questions might come to mind: @@ -87,7 +87,7 @@ We can access the current theme's colors like this: ~~~rust impl TodoList { // ... - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { let color = cx.theme().colors() div().size_4().hsla(50.0/360.0, 1.0, 0.5, 1.0) @@ -102,7 +102,7 @@ use gpui::hsla impl TodoList { // ... - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { let color = cx.theme().colors() div().size_4().bg(color.surface) @@ -117,7 +117,7 @@ use gpui::hsla impl TodoList { // ... - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Element { let color = cx.theme().colors() div() diff --git a/crates/ui2/src/components.rs b/crates/ui2/src/components.rs index e7b2d9cf0f..c467576f4a 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 d083d8fd46..57aa17ebba 100644 --- a/crates/ui2/src/components/avatar.rs +++ b/crates/ui2/src/components/avatar.rs @@ -1,27 +1,25 @@ -use gpui::img; +use std::sync::Arc; use crate::prelude::*; +use gpui::{img, ImageData, ImageSource, Img, IntoElement}; -#[derive(Component)] +#[derive(Debug, Default, PartialEq, Clone)] +pub enum Shape { + #[default] + Circle, + RoundedRectangle, +} + +#[derive(IntoElement)] pub struct Avatar { - src: SharedString, + src: ImageSource, shape: Shape, } -impl Avatar { - pub fn new(src: impl Into) -> Self { - Self { - src: src.into(), - shape: Shape::Circle, - } - } +impl RenderOnce for Avatar { + type Rendered = Img; - pub fn shape(mut self, shape: Shape) -> Self { - self.shape = shape; - self - } - - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + fn render(self, _: &mut WindowContext) -> Self::Rendered { let mut img = img(); if self.shape == Shape::Circle { @@ -30,37 +28,35 @@ impl Avatar { img = img.rounded_md(); } - img.uri(self.src.clone()) + img.source(self.src.clone()) .size_4() // todo!(Pull the avatar fallback background from the theme.) .bg(gpui::red()) } } -#[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", - )) +impl Avatar { + pub fn uri(src: impl Into) -> Self { + Self { + src: src.into().into(), + shape: Shape::Circle, } } + pub fn data(src: Arc) -> Self { + Self { + src: src.into(), + shape: Shape::Circle, + } + } + + pub fn source(src: ImageSource) -> Self { + Self { + src, + shape: Shape::Circle, + } + } + pub fn shape(mut self, shape: Shape) -> Self { + self.shape = shape; + self + } } diff --git a/crates/ui2/src/components/button.rs b/crates/ui2/src/components/button.rs index 1bb611a86e..02902a4b64 100644 --- a/crates/ui2/src/components/button.rs +++ b/crates/ui2/src/components/button.rs @@ -1,25 +1,28 @@ -use std::sync::Arc; +use std::rc::Rc; -use gpui::{DefiniteLength, Hsla, MouseButton, StatefulInteractiveComponent, WindowContext}; +use gpui::{ + DefiniteLength, Div, Hsla, IntoElement, MouseButton, MouseDownEvent, + 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