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 2184ba5ebb..9b0351f35a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -841,6 +841,17 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "backtrace-on-stack-overflow" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fd2d70527f3737a1ad17355e260706c1badebabd1fa06a7a053407380df841b" +dependencies = [ + "backtrace", + "libc", + "nix 0.23.2", +] + [[package]] name = "base64" version = "0.13.1" @@ -1175,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", @@ -1192,7 +1205,10 @@ dependencies = [ "serde_derive", "serde_json", "settings2", + "smallvec", + "ui2", "util", + "workspace2", ] [[package]] @@ -1653,7 +1669,7 @@ dependencies = [ [[package]] name = "collab" -version = "0.28.0" +version = "0.29.0" dependencies = [ "anyhow", "async-trait", @@ -5559,6 +5575,19 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "nix" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c" +dependencies = [ + "bitflags 1.3.2", + "cc", + "cfg-if 1.0.0", + "libc", + "memoffset 0.6.5", +] + [[package]] name = "nix" version = "0.24.3" @@ -8859,6 +8888,42 @@ 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" +dependencies = [ + "anyhow", + "backtrace-on-stack-overflow", + "chrono", + "clap 4.4.4", + "editor2", + "fuzzy2", + "gpui2", + "itertools 0.11.0", + "language2", + "log", + "menu2", + "picker2", + "rust-embed", + "serde", + "settings2", + "simplelog", + "smallvec", + "story", + "strum", + "theme", + "theme2", + "ui2", + "util", +] + [[package]] name = "stringprep" version = "0.1.4" @@ -9362,6 +9427,7 @@ dependencies = [ "serde_derive", "serde_json", "settings2", + "story", "toml 0.5.11", "util", "uuid 1.4.1", @@ -9884,7 +9950,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", @@ -10225,6 +10291,7 @@ dependencies = [ "serde", "settings2", "smallvec", + "story", "strum", "theme2", ] @@ -11363,6 +11430,7 @@ version = "0.1.0" dependencies = [ "anyhow", "async-recursion 1.0.5", + "async-trait", "bincode", "call2", "client2", @@ -11475,7 +11543,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.114.0" +version = "0.115.0" dependencies = [ "activity_indicator", "ai", @@ -11623,6 +11691,7 @@ dependencies = [ "async-recursion 0.3.2", "async-tar", "async-trait", + "audio2", "auto_update2", "backtrace", "call2", diff --git a/Cargo.toml b/Cargo.toml index 6c1152cf9c..481ef6f910 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -97,8 +97,7 @@ members = [ "crates/sqlez", "crates/sqlez_macros", "crates/rich_text", - # "crates/storybook2", - # "crates/storybook3", + "crates/storybook2", "crates/sum_tree", "crates/terminal", "crates/terminal2", @@ -112,6 +111,7 @@ members = [ "crates/ui2", "crates/util", "crates/semantic_index", + "crates/story", "crates/vim", "crates/vcs_menu", "crates/workspace2", @@ -197,8 +197,9 @@ 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"} + [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 @@ -210,11 +211,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/update_notification.rs b/crates/auto_update2/src/update_notification.rs index e6a22b7324..9cb1550bd4 100644 --- a/crates/auto_update2/src/update_notification.rs +++ b/crates/auto_update2/src/update_notification.rs @@ -1,12 +1,13 @@ -use gpui::{div, Div, EventEmitter, ParentElement, Render, SemanticVersion, ViewContext}; +use gpui::{ + div, DismissEvent, Div, EventEmitter, ParentElement, Render, SemanticVersion, ViewContext, +}; use menu::Cancel; -use workspace::notifications::NotificationEvent; pub struct UpdateNotification { _version: SemanticVersion, } -impl EventEmitter for UpdateNotification {} +impl EventEmitter for UpdateNotification {} impl Render for UpdateNotification { type Element = Div; @@ -82,6 +83,6 @@ impl UpdateNotification { } pub fn _dismiss(&mut self, _: &Cancel, cx: &mut ViewContext) { - cx.emit(NotificationEvent::Dismiss); + 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..9a89ec7167 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,205 @@ 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| { + room.update(cx, |this, cx| { + this.toggle_mute(cx).log_err(); + }) + }) + }) + }); + } + 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..b55d3348dc 100644 --- a/crates/call2/src/room.rs +++ b/crates/call2/src/room.rs @@ -1,7 +1,4 @@ -use crate::{ - call_settings::CallSettings, - participant::{LocalParticipant, ParticipantLocation, RemoteParticipant}, -}; +use crate::participant::{LocalParticipant, ParticipantLocation, RemoteParticipant}; use anyhow::{anyhow, Result}; use audio::{Audio, Sound}; use client::{ @@ -21,7 +18,6 @@ use live_kit_client::{ }; use postage::{sink::Sink, stream::Stream, watch}; use project::Project; -use settings::Settings; use std::{future::Future, mem, sync::Arc, time::Duration}; use util::{post_inc, ResultExt, TryFutureExt}; @@ -332,8 +328,10 @@ impl Room { } } - pub fn mute_on_join(cx: &AppContext) -> bool { - CallSettings::get_global(cx).mute_on_join || client::IMPERSONATE_LOGIN.is_some() + pub fn mute_on_join(_cx: &AppContext) -> bool { + // todo!() po: This should be uncommented, though then unmuting does not work + false + //CallSettings::get_global(cx).mute_on_join || client::IMPERSONATE_LOGIN.is_some() } fn from_join_response( 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/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 028dec6803..4746c9c6e4 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/src/collab_panel.rs b/crates/collab_ui2/src/collab_panel.rs index 6af188dfd2..e9c17a6589 100644 --- a/crates/collab_ui2/src/collab_panel.rs +++ b/crates/collab_ui2/src/collab_panel.rs @@ -157,15 +157,17 @@ const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel"; use std::sync::Arc; +use client::{Client, Contact, UserStore}; use db::kvp::KEY_VALUE_STORE; use gpui::{ actions, div, serde_json, AppContext, AsyncWindowContext, Div, EventEmitter, FocusHandle, - Focusable, FocusableView, InteractiveElement, ParentElement, Render, View, ViewContext, - VisualContext, WeakView, + Focusable, FocusableView, InteractiveElement, Model, ParentElement, Render, Styled, View, + ViewContext, VisualContext, WeakView, }; use project::Fs; use serde_derive::{Deserialize, Serialize}; use settings::Settings; +use ui::{h_stack, Avatar, Label}; use util::ResultExt; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, @@ -299,8 +301,8 @@ pub struct CollabPanel { // channel_editing_state: Option, // entries: Vec, // selection: Option, - // user_store: ModelHandle, - // client: Arc, + user_store: Model, + _client: Arc, // channel_store: ModelHandle, // project: ModelHandle, // match_candidates: Vec, @@ -595,7 +597,7 @@ impl CollabPanel { // entries: Vec::default(), // channel_editing_state: None, // selection: None, - // user_store: workspace.user_store().clone(), + user_store: workspace.user_store().clone(), // channel_store: ChannelStore::global(cx), // project: workspace.project().clone(), // subscriptions: Vec::default(), @@ -603,7 +605,7 @@ impl CollabPanel { // collapsed_sections: vec![Section::Offline], // collapsed_channels: Vec::default(), _workspace: workspace.weak_handle(), - // client: workspace.app_state().client.clone(), + _client: workspace.app_state().client.clone(), // context_menu_on_selected: true, // drag_target_channel: ChannelDragTarget::None, // list_state, @@ -663,6 +665,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, @@ -3297,11 +3302,38 @@ impl CollabPanel { impl Render for CollabPanel { type Element = Focusable
; - fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + let contacts = self.contacts(cx).unwrap_or_default(); + let workspace = self._workspace.clone(); div() .key_context("CollabPanel") .track_focus(&self.focus_handle) - .child("COLLAB PANEL") + .children(contacts.into_iter().map(|contact| { + let id = contact.user.id; + h_stack() + .p_2() + .gap_2() + .children( + contact + .user + .avatar + .as_ref() + .map(|avatar| Avatar::data(avatar.clone())), + ) + .child(Label::new(contact.user.github_login.clone())) + .on_mouse_down(gpui::MouseButton::Left, { + let workspace = workspace.clone(); + move |_, cx| { + workspace + .update(cx, |this, cx| { + this.call_state() + .invite(id, None, cx) + .detach_and_log_err(cx) + }) + .log_err(); + } + }) + })) } } diff --git a/crates/collab_ui2/src/collab_titlebar_item.rs b/crates/collab_ui2/src/collab_titlebar_item.rs index 2c08840691..d208eb91f1 100644 --- a/crates/collab_ui2/src/collab_titlebar_item.rs +++ b/crates/collab_ui2/src/collab_titlebar_item.rs @@ -31,15 +31,18 @@ use std::sync::Arc; use call::ActiveCall; use client::{Client, UserStore}; use gpui::{ - div, px, rems, AppContext, Div, InteractiveElement, Model, ParentElement, Render, RenderOnce, - Stateful, StatefulInteractiveElement, 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 ui::{h_stack, Avatar, Button, ButtonVariant, Color, IconButton, KeyBinding, Tooltip}; +use util::ResultExt; use workspace::Workspace; +use crate::face_pile::FacePile; + // const MAX_PROJECT_NAME_LENGTH: usize = 40; // const MAX_BRANCH_NAME_LENGTH: usize = 40; @@ -85,6 +88,41 @@ impl Render for CollabTitlebarItem { 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() @@ -111,17 +149,21 @@ 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)), ) // 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)), @@ -129,11 +171,13 @@ impl Render for CollabTitlebarItem { // 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| { cx.build_view(|_| { @@ -149,8 +193,111 @@ 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 |cx| async move { + client.authenticate_and_connect(true, &cx).await?; + Ok::<(), anyhow::Error>(()) + }) + .detach_and_log_err(cx); + })) + } + }) } } 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 3c6f2fff92..07b819d3a1 100644 --- a/crates/command_palette2/src/command_palette.rs +++ b/crates/command_palette2/src/command_palette.rs @@ -1,8 +1,9 @@ use collections::{CommandPaletteFilter, HashMap}; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - actions, div, prelude::*, Action, AppContext, Div, EventEmitter, FocusHandle, FocusableView, - Keystroke, Manager, ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView, + actions, div, prelude::*, Action, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, + FocusableView, Keystroke, ParentElement, Render, Styled, View, ViewContext, VisualContext, + WeakView, }; use picker::{Picker, PickerDelegate}; use std::{ @@ -68,7 +69,7 @@ impl CommandPalette { } } -impl EventEmitter for CommandPalette {} +impl EventEmitter for CommandPalette {} impl FocusableView for CommandPalette { fn focus_handle(&self, cx: &AppContext) -> FocusHandle { @@ -268,7 +269,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(); } diff --git a/crates/diagnostics2/src/diagnostics.rs b/crates/diagnostics2/src/diagnostics.rs index 9cd049597d..dc7f0a1f3f 100644 --- a/crates/diagnostics2/src/diagnostics.rs +++ b/crates/diagnostics2/src/diagnostics.rs @@ -14,8 +14,8 @@ use editor::{ use futures::future::try_join_all; use gpui::{ actions, div, AnyElement, AnyView, AppContext, Context, Div, EventEmitter, FocusEvent, - FocusHandle, Focusable, FocusableElement, FocusableView, InteractiveElement, Model, - ParentElement, Render, RenderOnce, SharedString, Styled, Subscription, Task, View, ViewContext, + FocusHandle, Focusable, FocusableElement, FocusableView, InteractiveElement, IntoElement, + Model, ParentElement, Render, SharedString, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowContext, }; use language::{ @@ -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}, @@ -778,28 +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() + .into_any_element() }) } pub(crate) fn render_summary(summary: &DiagnosticSummary) -> AnyElement { if summary.error_count == 0 && summary.warning_count == 0 { let label = Label::new("No problems"); - label.render_into_any() + label.into_any_element() } else { h_stack() .bg(gpui::red()) @@ -807,7 +807,7 @@ pub(crate) fn render_summary(summary: &DiagnosticSummary) -> AnyElement { .child(Label::new(summary.error_count.to_string())) .child(IconElement::new(Icon::ExclamationTriangle)) .child(Label::new(summary.warning_count.to_string())) - .render_into_any() + .into_any_element() } } @@ -1550,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 730927a79e..ac24b7ad50 100644 --- a/crates/diagnostics2/src/items.rs +++ b/crates/diagnostics2/src/items.rs @@ -7,7 +7,7 @@ use gpui::{ use language::Diagnostic; use lsp::LanguageServerId; use theme::ActiveTheme; -use ui::{h_stack, Icon, IconElement, Label, TextColor, Tooltip}; +use ui::{h_stack, Color, Icon, IconElement, Label, Tooltip}; use workspace::{item::ItemHandle, StatusItemView, ToolbarItemEvent, Workspace}; use crate::ProjectDiagnosticsEditor; @@ -26,25 +26,25 @@ impl Render for DiagnosticIndicator { 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()) + .id("diagnostic-indicator") .on_action(cx.listener(Self::go_to_next_diagnostic)) .rounded_md() .flex_none() 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/editor.rs b/crates/editor2/src/editor.rs index a4482af6af..39d7d9ed09 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, ParentElement, Pixels, Render, - SharedString, 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) { 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,145 @@ impl CompletionsMenu { fn render( &self, style: &EditorStyle, + max_height: Pixels, workspace: Option>, cx: &mut ViewContext, ) -> AnyElement { - todo!("old implementation below") + 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) + .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(300.)) + .max_w(px(700.)) + .whitespace_nowrap() + .overflow_hidden() + .bg(gpui::green()) + .hover(|style| style.bg(gpui::blue())) + .when(item_ix == selected_item, |div| div.bg(gpui::red())) + .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,6 +1480,7 @@ impl CodeActionsMenu { &self, mut cursor_position: DisplayPoint, style: &EditorStyle, + max_height: Pixels, cx: &mut ViewContext, ) -> (DisplayPoint, AnyElement) { let actions = self.actions.clone(); @@ -1594,6 +1535,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() @@ -1601,7 +1544,7 @@ impl CodeActionsMenu { .max_by_key(|(_, action)| action.lsp_action.title.chars().count()) .map(|(ix, _)| ix), ) - .render_into_any(); + .into_any_element(); if self.deployed_from_indicator { *cursor_position.column_mut() = 0; @@ -1949,6 +1892,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), @@ -2080,10 +2024,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) } @@ -3444,7 +3392,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, ) { @@ -3463,11 +3411,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 @@ -3487,6 +3439,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) { @@ -3601,7 +3562,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, @@ -3624,20 +3586,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() { @@ -3669,142 +3635,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(); @@ -4408,7 +4379,7 @@ impl Editor { editor.fold_at(&FoldAt { buffer_row }, cx); } })) - .color(ui::TextColor::Muted) + .color(ui::Color::Muted) }) }) .flatten() @@ -4427,12 +4398,14 @@ impl Editor { &self, cursor_position: DisplayPoint, style: &EditorStyle, + max_height: Pixels, cx: &mut ViewContext, ) -> 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, ) @@ -7801,7 +7774,7 @@ impl Editor { .clone(), }, )) - .render_into_any() + .into_any_element() } }), disposition: BlockDisposition::Below, @@ -9215,6 +9188,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 { @@ -9404,7 +9397,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!(), @@ -9417,7 +9412,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, }, }; @@ -10008,7 +10005,7 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend cx.write_to_clipboard(ClipboardItem::new(message.clone())); })) .tooltip(|cx| Tooltip::text("Copy diagnostic message", cx)) - .render_into_any() + .into_any_element() }) } @@ -10055,120 +10052,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 f0609fc9a8..6865e81cfa 100644 --- a/crates/editor2/src/editor_tests.rs +++ b/crates/editor2/src/editor_tests.rs @@ -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 add9c9ad33..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,8 +21,8 @@ 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, InteractiveElement, LineLayout, + 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, @@ -124,6 +126,190 @@ impl EditorElement { } } + 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, @@ -459,7 +645,6 @@ impl EditorElement { &mut self, bounds: Bounds, layout: &mut LayoutState, - editor: &mut Editor, cx: &mut WindowContext, ) { let line_height = layout.position_map.line_height; @@ -490,7 +675,7 @@ impl EditorElement { for (ix, fold_indicator) in layout.fold_indicators.drain(..).enumerate() { if let Some(mut fold_indicator) = fold_indicator { - let mut fold_indicator = fold_indicator.render_into_any(); + let mut fold_indicator = fold_indicator.into_any_element(); let available_space = size( AvailableSpace::MinContent, AvailableSpace::Definite(line_height * 0.55), @@ -511,7 +696,7 @@ impl EditorElement { } if let Some(indicator) = layout.code_actions_indicator.take() { - let mut button = indicator.button.render_into_any(); + let mut button = indicator.button.into_any_element(); let available_space = size( AvailableSpace::MinContent, AvailableSpace::Definite(line_height), @@ -616,14 +801,19 @@ impl EditorElement { &mut self, text_bounds: Bounds, layout: &mut LayoutState, - editor: &mut Editor, 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 { @@ -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,16 +1032,10 @@ impl EditorElement { } }); - if let Some((position, mut context_menu)) = layout.context_menu.take() { - 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.), - ), - ); + 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 @@ -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, 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; + } + } + } + }) }, ) } @@ -1217,7 +1396,6 @@ impl EditorElement { &mut self, bounds: Bounds, layout: &mut LayoutState, - editor: &mut Editor, cx: &mut WindowContext, ) { let scroll_position = layout.position_map.snapshot.scroll_position(); @@ -1237,7 +1415,7 @@ impl EditorElement { } } - 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 @@ -1258,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) } @@ -1413,7 +1591,7 @@ impl EditorElement { } fn layout_lines( - &mut self, + &self, rows: Range, line_number_layouts: &[Option], snapshot: &EditorSnapshot, @@ -1469,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(), - button: 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, @@ -2287,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, }); @@ -2446,67 +2598,56 @@ impl Element for EditorElement { cx: &mut gpui::WindowContext, ) { let editor = self.editor.clone(); - editor.update(cx, |editor, cx| { - let mut layout = self.compute_layout(editor, cx, bounds); - let gutter_bounds = Bounds { - origin: bounds.origin, - size: layout.gutter_size, - }; - let text_bounds = Bounds { - origin: gutter_bounds.upper_right(), - size: layout.text_size, - }; - let dispatch_context = editor.dispatch_context(cx); - let editor_handle = cx.view().clone(); - cx.with_key_dispatch( - dispatch_context, - Some(editor.focus_handle.clone()), - |_, cx| { - register_actions(&editor_handle, cx); + let mut layout = self.compute_layout(bounds, cx); + let gutter_bounds = Bounds { + origin: bounds.origin, + size: layout.gutter_size, + }; + let text_bounds = Bounds { + origin: gutter_bounds.upper_right(), + size: layout.text_size, + }; - // We call with_z_index to establish a new stacking context. - cx.with_z_index(0, |cx| { - cx.with_content_mask(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, editor_handle, cx); - cx.handle_input(&editor.focus_handle, input_handler); + 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); - 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); + // 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); - if !layout.blocks.is_empty() { - cx.with_element_id(Some("editor_blocks"), |cx| { - self.paint_blocks(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, cx); + }) + } + }); + }); }) } } -impl RenderOnce for EditorElement { +impl IntoElement for EditorElement { type Element = Self; fn element_id(&self) -> Option { self.editor.element_id() } - fn render_once(self) -> Self::Element { + fn into_element(self) -> Self::Element { self } } @@ -3134,7 +3275,7 @@ pub struct LayoutState { max_row: u32, context_menu: Option<(DisplayPoint, AnyElement)>, code_actions_indicator: Option, - // hover_popovers: Option<(DisplayPoint, Vec)>, + hover_popovers: Option<(DisplayPoint, Vec)>, fold_indicators: Vec>, tab_invisible: ShapedLine, space_invisible: ShapedLine, @@ -3932,180 +4073,7 @@ fn scale_horizontal_mouse_autoscroll_delta(delta: Pixels) -> f32 { // } // } -fn register_actions(view: &View, cx: &mut WindowContext) { - register_action(view, cx, Editor::move_left); - register_action(view, cx, Editor::move_right); - register_action(view, cx, Editor::move_down); - register_action(view, cx, Editor::move_up); - // on_action(cx, Editor::new_file); todo!() - // on_action(cx, Editor::new_file_in_direction); todo!() - register_action(view, cx, Editor::cancel); - register_action(view, cx, Editor::newline); - register_action(view, cx, Editor::newline_above); - register_action(view, cx, Editor::newline_below); - register_action(view, cx, Editor::backspace); - register_action(view, cx, Editor::delete); - register_action(view, cx, Editor::tab); - register_action(view, cx, Editor::tab_prev); - register_action(view, cx, Editor::indent); - register_action(view, cx, Editor::outdent); - register_action(view, cx, Editor::delete_line); - register_action(view, cx, Editor::join_lines); - register_action(view, cx, Editor::sort_lines_case_sensitive); - register_action(view, cx, Editor::sort_lines_case_insensitive); - register_action(view, cx, Editor::reverse_lines); - register_action(view, cx, Editor::shuffle_lines); - register_action(view, cx, Editor::convert_to_upper_case); - register_action(view, cx, Editor::convert_to_lower_case); - register_action(view, cx, Editor::convert_to_title_case); - register_action(view, cx, Editor::convert_to_snake_case); - register_action(view, cx, Editor::convert_to_kebab_case); - register_action(view, cx, Editor::convert_to_upper_camel_case); - register_action(view, cx, Editor::convert_to_lower_camel_case); - register_action(view, cx, Editor::delete_to_previous_word_start); - register_action(view, cx, Editor::delete_to_previous_subword_start); - register_action(view, cx, Editor::delete_to_next_word_end); - register_action(view, cx, Editor::delete_to_next_subword_end); - register_action(view, cx, Editor::delete_to_beginning_of_line); - register_action(view, cx, Editor::delete_to_end_of_line); - register_action(view, cx, Editor::cut_to_end_of_line); - register_action(view, cx, Editor::duplicate_line); - register_action(view, cx, Editor::move_line_up); - register_action(view, cx, Editor::move_line_down); - register_action(view, cx, Editor::transpose); - register_action(view, cx, Editor::cut); - register_action(view, cx, Editor::copy); - register_action(view, cx, Editor::paste); - register_action(view, cx, Editor::undo); - register_action(view, cx, Editor::redo); - register_action(view, cx, Editor::move_page_up); - register_action(view, cx, Editor::move_page_down); - register_action(view, cx, Editor::next_screen); - register_action(view, cx, Editor::scroll_cursor_top); - register_action(view, cx, Editor::scroll_cursor_center); - register_action(view, cx, Editor::scroll_cursor_bottom); - register_action(view, cx, |editor, _: &LineDown, cx| { - editor.scroll_screen(&ScrollAmount::Line(1.), cx) - }); - register_action(view, cx, |editor, _: &LineUp, cx| { - editor.scroll_screen(&ScrollAmount::Line(-1.), cx) - }); - register_action(view, cx, |editor, _: &HalfPageDown, cx| { - editor.scroll_screen(&ScrollAmount::Page(0.5), cx) - }); - register_action(view, cx, |editor, _: &HalfPageUp, cx| { - editor.scroll_screen(&ScrollAmount::Page(-0.5), cx) - }); - register_action(view, cx, |editor, _: &PageDown, cx| { - editor.scroll_screen(&ScrollAmount::Page(1.), cx) - }); - register_action(view, cx, |editor, _: &PageUp, cx| { - editor.scroll_screen(&ScrollAmount::Page(-1.), cx) - }); - register_action(view, cx, Editor::move_to_previous_word_start); - register_action(view, cx, Editor::move_to_previous_subword_start); - register_action(view, cx, Editor::move_to_next_word_end); - register_action(view, cx, Editor::move_to_next_subword_end); - register_action(view, cx, Editor::move_to_beginning_of_line); - register_action(view, cx, Editor::move_to_end_of_line); - register_action(view, cx, Editor::move_to_start_of_paragraph); - register_action(view, cx, Editor::move_to_end_of_paragraph); - register_action(view, cx, Editor::move_to_beginning); - register_action(view, cx, Editor::move_to_end); - register_action(view, cx, Editor::select_up); - register_action(view, cx, Editor::select_down); - register_action(view, cx, Editor::select_left); - register_action(view, cx, Editor::select_right); - register_action(view, cx, Editor::select_to_previous_word_start); - register_action(view, cx, Editor::select_to_previous_subword_start); - register_action(view, cx, Editor::select_to_next_word_end); - register_action(view, cx, Editor::select_to_next_subword_end); - register_action(view, cx, Editor::select_to_beginning_of_line); - register_action(view, cx, Editor::select_to_end_of_line); - register_action(view, cx, Editor::select_to_start_of_paragraph); - register_action(view, cx, Editor::select_to_end_of_paragraph); - register_action(view, cx, Editor::select_to_beginning); - register_action(view, cx, Editor::select_to_end); - register_action(view, cx, Editor::select_all); - register_action(view, cx, |editor, action, cx| { - editor.select_all_matches(action, cx).log_err(); - }); - register_action(view, cx, Editor::select_line); - register_action(view, cx, Editor::split_selection_into_lines); - register_action(view, cx, Editor::add_selection_above); - register_action(view, cx, Editor::add_selection_below); - register_action(view, cx, |editor, action, cx| { - editor.select_next(action, cx).log_err(); - }); - register_action(view, cx, |editor, action, cx| { - editor.select_previous(action, cx).log_err(); - }); - register_action(view, cx, Editor::toggle_comments); - register_action(view, cx, Editor::select_larger_syntax_node); - register_action(view, cx, Editor::select_smaller_syntax_node); - register_action(view, cx, Editor::move_to_enclosing_bracket); - register_action(view, cx, Editor::undo_selection); - register_action(view, cx, Editor::redo_selection); - register_action(view, cx, Editor::go_to_diagnostic); - register_action(view, cx, Editor::go_to_prev_diagnostic); - register_action(view, cx, Editor::go_to_hunk); - register_action(view, cx, Editor::go_to_prev_hunk); - register_action(view, cx, Editor::go_to_definition); - register_action(view, cx, Editor::go_to_definition_split); - register_action(view, cx, Editor::go_to_type_definition); - register_action(view, cx, Editor::go_to_type_definition_split); - register_action(view, cx, Editor::fold); - register_action(view, cx, Editor::fold_at); - register_action(view, cx, Editor::unfold_lines); - register_action(view, cx, Editor::unfold_at); - register_action(view, cx, Editor::fold_selected_ranges); - register_action(view, cx, Editor::show_completions); - register_action(view, cx, Editor::toggle_code_actions); - // on_action(cx, Editor::open_excerpts); todo!() - register_action(view, cx, Editor::toggle_soft_wrap); - register_action(view, cx, Editor::toggle_inlay_hints); - register_action(view, cx, Editor::reveal_in_finder); - register_action(view, cx, Editor::copy_path); - register_action(view, cx, Editor::copy_relative_path); - register_action(view, cx, Editor::copy_highlight_json); - register_action(view, cx, |editor, action, cx| { - editor - .format(action, cx) - .map(|task| task.detach_and_log_err(cx)); - }); - register_action(view, cx, Editor::restart_language_server); - register_action(view, cx, Editor::show_character_palette); - // on_action(cx, Editor::confirm_completion); todo!() - register_action(view, cx, |editor, action, cx| { - editor - .confirm_code_action(action, cx) - .map(|task| task.detach_and_log_err(cx)); - }); - register_action(view, cx, |editor, action, cx| { - editor - .rename(action, cx) - .map(|task| task.detach_and_log_err(cx)); - }); - register_action(view, cx, |editor, action, cx| { - editor - .confirm_rename(action, cx) - .map(|task| task.detach_and_log_err(cx)); - }); - register_action(view, cx, |editor, action, cx| { - editor - .find_all_references(action, cx) - .map(|task| task.detach_and_log_err(cx)); - }); - register_action(view, cx, Editor::next_copilot_suggestion); - register_action(view, cx, Editor::previous_copilot_suggestion); - register_action(view, cx, Editor::copilot_suggest); - register_action(view, cx, Editor::context_menu_first); - register_action(view, cx, Editor::context_menu_prev); - register_action(view, cx, Editor::context_menu_next); - register_action(view, cx, Editor::context_menu_last); -} - -fn register_action( +pub fn register_action( view: &View, cx: &mut WindowContext, listener: impl Fn(&mut Editor, &T, &mut ViewContext) + 'static, diff --git a/crates/editor2/src/hover_popover.rs b/crates/editor2/src/hover_popover.rs index 07d108cd65..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") + // 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 cdb15329d3..eca3b99d78 100644 --- a/crates/editor2/src/items.rs +++ b/crates/editor2/src/items.rs @@ -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::{ @@ -604,7 +604,7 @@ impl Item for Editor { &description, MAX_TAB_TITLE_LEN, )) - .color(TextColor::Muted), + .color(Color::Muted), ), ) })), diff --git a/crates/file_finder2/src/file_finder.rs b/crates/file_finder2/src/file_finder.rs index 0b9157bb4f..ea578fbb0e 100644 --- a/crates/file_finder2/src/file_finder.rs +++ b/crates/file_finder2/src/file_finder.rs @@ -2,8 +2,8 @@ use collections::HashMap; use editor::{scroll::autoscroll::Autoscroll, Bias, Editor}; use fuzzy::{CharBag, PathMatch, PathMatchCandidate}; use gpui::{ - actions, div, AppContext, Div, EventEmitter, FocusHandle, FocusableView, InteractiveElement, - Manager, Model, ParentElement, Render, RenderOnce, Styled, Task, View, ViewContext, + actions, div, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView, + InteractiveElement, IntoElement, Model, ParentElement, Render, Styled, Task, View, ViewContext, VisualContext, WeakView, }; use picker::{Picker, PickerDelegate}; @@ -111,7 +111,7 @@ 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) @@ -690,7 +690,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 +702,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(); } 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 4ba605c3c9..c734281d22 100644 --- a/crates/go_to_line2/src/go_to_line.rs +++ b/crates/go_to_line2/src/go_to_line.rs @@ -1,13 +1,13 @@ use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Editor}; use gpui::{ - actions, div, prelude::*, AppContext, Div, EventEmitter, FocusHandle, FocusableView, Manager, - Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext, WindowContext, + 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); @@ -25,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)); + }) }); } @@ -88,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), _ => {} } @@ -123,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) { @@ -140,7 +142,7 @@ impl GoToLine { self.prev_scroll_position.take(); } - cx.emit(Manager::Dismiss); + cx.emit(DismissEvent::Dismiss); } } @@ -176,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 f645129706..e8f2a60a6a 100644 --- a/crates/gpui2/src/app.rs +++ b/crates/gpui2/src/app.rs @@ -10,6 +10,7 @@ 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::*; @@ -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"))? diff --git a/crates/gpui2/src/app/async_context.rs b/crates/gpui2/src/app/async_context.rs index cc3b0ace57..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}; @@ -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/test_context.rs b/crates/gpui2/src/app/test_context.rs index 2bd3a069ca..71bc8e3d81 100644 --- a/crates/gpui2/src/app/test_context.rs +++ b/crates/gpui2/src/app/test_context.rs @@ -611,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() } diff --git a/crates/gpui2/src/element.rs b/crates/gpui2/src/element.rs index 5cd015503d..b18ffb8ca6 100644 --- a/crates/gpui2/src/element.rs +++ b/crates/gpui2/src/element.rs @@ -12,15 +12,15 @@ pub trait Render: 'static + Sized { fn render(&mut self, cx: &mut ViewContext) -> Self::Element; } -pub trait RenderOnce: Sized { +pub trait IntoElement: Sized { type Element: Element + 'static; fn element_id(&self) -> Option; - fn render_once(self) -> Self::Element; + fn into_element(self) -> Self::Element; - fn render_into_any(self) -> AnyElement { - self.render_once().into_any() + fn into_any_element(self) -> AnyElement { + self.into_element().into_any() } fn draw( @@ -33,7 +33,7 @@ pub trait RenderOnce: Sized { where T: Clone + Default + Debug + Into, { - let element = self.render_once(); + let element = self.into_element(); let element_id = element.element_id(); let element = DrawableElement { element: Some(element), @@ -57,7 +57,7 @@ pub trait RenderOnce: Sized { fn map(self, f: impl FnOnce(Self) -> U) -> U where Self: Sized, - U: RenderOnce, + U: IntoElement, { f(self) } @@ -83,7 +83,7 @@ pub trait RenderOnce: Sized { } } -pub trait Element: 'static + RenderOnce { +pub trait Element: 'static + IntoElement { type State: 'static; fn layout( @@ -99,30 +99,30 @@ pub trait Element: 'static + RenderOnce { } } -pub trait Component: 'static { - type Rendered: RenderOnce; +pub trait RenderOnce: 'static { + type Rendered: IntoElement; fn render(self, cx: &mut WindowContext) -> Self::Rendered; } -pub struct CompositeElement { +pub struct Component { component: Option, } -pub struct CompositeElementState { - rendered_element: Option<::Element>, - rendered_element_state: <::Element as Element>::State, +pub struct CompositeElementState { + rendered_element: Option<::Element>, + rendered_element_state: <::Element as Element>::State, } -impl CompositeElement { +impl Component { pub fn new(component: C) -> Self { - CompositeElement { + Component { component: Some(component), } } } -impl Element for CompositeElement { +impl Element for Component { type State = CompositeElementState; fn layout( @@ -130,7 +130,7 @@ impl Element for CompositeElement { state: Option, cx: &mut WindowContext, ) -> (LayoutId, Self::State) { - let mut element = self.component.take().unwrap().render(cx).render_once(); + 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), @@ -148,14 +148,14 @@ impl Element for CompositeElement { } } -impl RenderOnce for CompositeElement { +impl IntoElement for Component { type Element = Self; fn element_id(&self) -> Option { None } - fn render_once(self) -> Self::Element { + fn into_element(self) -> Self::Element { self } } @@ -166,23 +166,20 @@ pub struct GlobalElementId(SmallVec<[ElementId; 32]>); pub trait ParentElement { fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]>; - fn child(mut self, child: impl RenderOnce) -> Self + fn child(mut self, child: impl IntoElement) -> Self where Self: Sized, { - self.children_mut().push(child.render_once().into_any()); + self.children_mut().push(child.into_element().into_any()); self } - fn children(mut self, children: impl IntoIterator) -> Self + fn children(mut self, children: impl IntoIterator) -> Self where Self: Sized, { - self.children_mut().extend( - children - .into_iter() - .map(|child| child.render_once().into_any()), - ); + self.children_mut() + .extend(children.into_iter().map(|child| child.into_any_element())); self } } @@ -432,10 +429,6 @@ impl AnyElement { AnyElement(Box::new(Some(DrawableElement::new(element))) as Box) } - pub fn element_id(&self) -> Option { - self.0.element_id() - } - pub fn layout(&mut self, cx: &mut WindowContext) -> LayoutId { self.0.layout(cx) } @@ -467,6 +460,10 @@ impl AnyElement { pub fn into_any(self) -> AnyElement { AnyElement::new(self) } + + pub fn inner_id(&self) -> Option { + self.0.element_id() + } } impl Element for AnyElement { @@ -486,14 +483,14 @@ impl Element for AnyElement { } } -impl RenderOnce for AnyElement { +impl IntoElement for AnyElement { type Element = Self; fn element_id(&self) -> Option { - AnyElement::element_id(self) + None } - fn render_once(self) -> Self::Element { + fn into_element(self) -> Self::Element { self } } diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index 630b368b95..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, DispatchPhase, Element, ElementId, FocusEvent, FocusHandle, - KeyContext, KeyDownEvent, KeyUpEvent, LayoutId, MouseButton, MouseDownEvent, MouseMoveEvent, - MouseUpEvent, ParentElement, Pixels, Point, Render, RenderOnce, ScrollWheelEvent, SharedString, - Size, Style, StyleRefinement, Styled, Task, View, Visibility, WindowContext, + 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; @@ -666,14 +666,14 @@ impl Element for Div { } } -impl RenderOnce for Div { +impl IntoElement for Div { type Element = Self; fn element_id(&self) -> Option { self.interactivity.element_id.clone() } - fn render_once(self) -> Self::Element { + fn into_element(self) -> Self::Element { self } } @@ -1278,7 +1278,7 @@ where } } -impl RenderOnce for Focusable +impl IntoElement for Focusable where E: Element, { @@ -1288,7 +1288,7 @@ where self.element.element_id() } - fn render_once(self) -> Self::Element { + fn into_element(self) -> Self::Element { self.element } } @@ -1352,7 +1352,7 @@ where } } -impl RenderOnce for Stateful +impl IntoElement for Stateful where E: Element, { @@ -1362,7 +1362,7 @@ where self.element.element_id() } - fn render_once(self) -> Self::Element { + fn into_element(self) -> Self::Element { self } } diff --git a/crates/gpui2/src/elements/img.rs b/crates/gpui2/src/elements/img.rs index 3c0f4c00be..2aece17b47 100644 --- a/crates/gpui2/src/elements/img.rs +++ b/crates/gpui2/src/elements/img.rs @@ -1,30 +1,59 @@ +use std::sync::Arc; + use crate::{ - Bounds, Element, InteractiveElement, InteractiveElementState, Interactivity, LayoutId, Pixels, - RenderOnce, SharedString, StyleRefinement, Styled, WindowContext, + Bounds, Element, ImageData, InteractiveElement, InteractiveElementState, Interactivity, + IntoElement, LayoutId, Pixels, SharedString, StyleRefinement, Styled, WindowContext, }; use futures::FutureExt; use util::ResultExt; +#[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, - uri: Option, + source: Option, grayscale: bool, } pub fn img() -> Img { Img { interactivity: Interactivity::default(), - uri: None, + source: None, grayscale: false, } } 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 @@ -58,42 +87,47 @@ 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 RenderOnce for Img { +impl IntoElement for Img { type Element = Self; fn element_id(&self) -> Option { self.interactivity.element_id.clone() } - fn render_once(self) -> Self::Element { + fn into_element(self) -> Self::Element { self } } diff --git a/crates/gpui2/src/elements/overlay.rs b/crates/gpui2/src/elements/overlay.rs index 29ac2f00c4..764bdfabcd 100644 --- a/crates/gpui2/src/elements/overlay.rs +++ b/crates/gpui2/src/elements/overlay.rs @@ -2,8 +2,8 @@ use smallvec::SmallVec; use taffy::style::{Display, Position}; use crate::{ - point, AnyElement, BorrowWindow, Bounds, Element, LayoutId, ParentElement, Pixels, Point, - RenderOnce, Size, Style, WindowContext, + point, AnyElement, BorrowWindow, Bounds, Element, IntoElement, LayoutId, ParentElement, Pixels, + Point, Size, Style, WindowContext, }; pub struct OverlayState { @@ -144,21 +144,23 @@ impl Element for Overlay { } cx.with_element_offset(desired.origin - bounds.origin, |cx| { - for child in self.children { - child.paint(cx); - } + cx.break_content_mask(|cx| { + for child in self.children { + child.paint(cx); + } + }) }) } } -impl RenderOnce for Overlay { +impl IntoElement for Overlay { type Element = Self; fn element_id(&self) -> Option { None } - fn render_once(self) -> Self::Element { + fn into_element(self) -> Self::Element { self } } diff --git a/crates/gpui2/src/elements/svg.rs b/crates/gpui2/src/elements/svg.rs index c24e4d9b8b..aba31686f5 100644 --- a/crates/gpui2/src/elements/svg.rs +++ b/crates/gpui2/src/elements/svg.rs @@ -1,6 +1,6 @@ use crate::{ Bounds, Element, ElementId, InteractiveElement, InteractiveElementState, Interactivity, - LayoutId, Pixels, RenderOnce, SharedString, StyleRefinement, Styled, WindowContext, + IntoElement, LayoutId, Pixels, SharedString, StyleRefinement, Styled, WindowContext, }; use util::ResultExt; @@ -49,14 +49,14 @@ impl Element for Svg { } } -impl RenderOnce for Svg { +impl IntoElement for Svg { type Element = Self; fn element_id(&self) -> Option { self.interactivity.element_id.clone() } - fn render_once(self) -> Self::Element { + fn into_element(self) -> Self::Element { self } } diff --git a/crates/gpui2/src/elements/text.rs b/crates/gpui2/src/elements/text.rs index 05ab85ca63..a0715b81a9 100644 --- a/crates/gpui2/src/elements/text.rs +++ b/crates/gpui2/src/elements/text.rs @@ -1,11 +1,11 @@ use crate::{ - Bounds, Element, ElementId, LayoutId, Pixels, RenderOnce, SharedString, Size, TextRun, - WindowContext, 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; impl Element for &'static str { @@ -26,14 +26,14 @@ impl Element for &'static str { } } -impl RenderOnce for &'static str { +impl IntoElement for &'static str { type Element = Self; fn element_id(&self) -> Option { None } - fn render_once(self) -> Self::Element { + fn into_element(self) -> Self::Element { self } } @@ -57,35 +57,40 @@ impl Element for SharedString { } } -impl RenderOnce for SharedString { +impl IntoElement for SharedString { type Element = Self; fn element_id(&self) -> Option { None } - fn render_once(self) -> Self::Element { + 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 StyledText { - /// 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 new(text: SharedString, runs: Vec) -> Self { + pub fn new(text: impl Into) -> Self { StyledText { - text, - runs: Some(runs), + text: text.into(), + runs: None, } } + + pub fn with_runs(mut self, runs: Vec) -> Self { + self.runs = Some(runs); + self + } } impl Element for StyledText { @@ -106,14 +111,14 @@ impl Element for StyledText { } } -impl RenderOnce for StyledText { +impl IntoElement for StyledText { type Element = Self; fn element_id(&self) -> Option { None } - fn render_once(self) -> Self::Element { + fn into_element(self) -> Self::Element { self } } @@ -159,10 +164,14 @@ impl TextState { let element_state = self.clone(); move |known_dimensions, available_space| { - let wrap_width = known_dimensions.width.or(match available_space.width { - crate::AvailableSpace::Definite(x) => Some(x), - _ => None, - }); + let wrap_width = if text_style.white_space == WhiteSpace::Normal { + known_dimensions.width.or(match available_space.width { + crate::AvailableSpace::Definite(x) => Some(x), + _ => None, + }) + } else { + None + }; if let Some(text_state) = element_state.0.lock().as_ref() { if text_state.size.is_some() @@ -174,10 +183,7 @@ impl TextState { let Some(lines) = text_system .shape_text( - &text, - font_size, - &runs[..], - wrap_width, // Wrap if we know the width. + &text, font_size, &runs, wrap_width, // Wrap if we know the width. ) .log_err() else { @@ -194,7 +200,7 @@ impl TextState { for line in &lines { let line_size = line.size(line_height); size.height += line_size.height; - size.width = size.width.max(line_size.width); + size.width = size.width.max(line_size.width).ceil(); } element_state.lock().replace(TextStateInner { @@ -225,16 +231,77 @@ impl TextState { line_origin.y += line.size(line_height).height; } } + + fn index_for_position(&self, bounds: Bounds, position: Point) -> Option { + if !bounds.contains_point(&position) { + return None; + } + + 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 InteractiveText { +pub struct InteractiveText { element_id: ElementId, text: StyledText, + click_listener: Option)>>, } -struct InteractiveTextState { +struct InteractiveTextClickEvent { + mouse_down_index: usize, + mouse_up_index: usize, +} + +pub struct InteractiveTextState { text_state: TextState, - clicked_range_ixs: Rc>>, + mouse_down_index: Rc>>, +} + +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 { @@ -246,39 +313,74 @@ impl Element for InteractiveText { cx: &mut WindowContext, ) -> (LayoutId, Self::State) { if let Some(InteractiveTextState { - text_state, - clicked_range_ixs, + mouse_down_index, .. }) = state { - let (layout_id, text_state) = self.text.layout(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(None, cx); let element_state = InteractiveTextState { text_state, - clicked_range_ixs: Rc::default(), + mouse_down_index: Rc::default(), }; (layout_id, element_state) } } 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 RenderOnce for InteractiveText { +impl IntoElement for InteractiveText { type Element = Self; fn element_id(&self) -> Option { Some(self.element_id.clone()) } - fn render_once(self) -> Self::Element { + 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 b24b3935fa..8e61f247bd 100644 --- a/crates/gpui2/src/elements/uniform_list.rs +++ b/crates/gpui2/src/elements/uniform_list.rs @@ -1,7 +1,7 @@ use crate::{ - point, px, size, AnyElement, AvailableSpace, Bounds, Element, ElementId, InteractiveElement, - InteractiveElementState, Interactivity, LayoutId, Pixels, Point, Render, RenderOnce, Size, - StyleRefinement, Styled, View, ViewContext, WindowContext, + 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, ops::Range, rc::Rc}; @@ -9,7 +9,7 @@ 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. +/// uniform_list will only render the visible subset of items. pub fn uniform_list( view: View, id: I, @@ -18,30 +18,30 @@ pub fn uniform_list( ) -> UniformList where I: Into, - R: RenderOnce, + 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.render_into_any()) + .map(|component| component.into_any_element()) .collect() }) }; UniformList { id: id.clone(), - style, item_count, item_to_measure_index: 0, render_items: Box::new(render_range), interactivity: Interactivity { element_id: Some(id.into()), + base_style, ..Default::default() }, scroll_handle: None, @@ -50,7 +50,6 @@ where pub struct UniformList { id: ElementId, - style: StyleRefinement, item_count: usize, item_to_measure_index: usize, render_items: @@ -91,7 +90,7 @@ impl UniformListScrollHandle { impl Styled for UniformList { fn style(&mut self) -> &mut StyleRefinement { - &mut self.style + &mut self.interactivity.base_style } } @@ -211,31 +210,31 @@ 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 items = (self.render_items)(visible_range.clone(), cx); cx.with_z_index(1, |cx| { - for (item, ix) in items.into_iter().zip(visible_range) { - let item_origin = padded_bounds.origin - + 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); - } + 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); + } + }); }); } }) @@ -244,14 +243,14 @@ impl Element for UniformList { } } -impl RenderOnce for UniformList { +impl IntoElement for UniformList { type Element = Self; fn element_id(&self) -> Option { Some(self.id.clone()) } - fn render_once(self) -> Self::Element { + fn into_element(self) -> Self::Element { self } } diff --git a/crates/gpui2/src/interactive.rs b/crates/gpui2/src/interactive.rs index 3dfe0c3b43..2d48ec5a11 100644 --- a/crates/gpui2/src/interactive.rs +++ b/crates/gpui2/src/interactive.rs @@ -1,6 +1,6 @@ use crate::{ - div, point, Div, Element, FocusHandle, Keystroke, Modifiers, Pixels, Point, Render, RenderOnce, - 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,7 +64,7 @@ pub struct Drag where R: Fn(&mut V, &mut ViewContext) -> E, V: 'static, - E: RenderOnce, + E: IntoElement, { pub state: S, pub render_drag_handle: R, @@ -286,8 +286,8 @@ pub struct FocusEvent { #[cfg(test)] mod test { use crate::{ - self as gpui, div, Div, FocusHandle, InteractiveElement, KeyBinding, Keystroke, - ParentElement, Render, RenderOnce, Stateful, TestAppContext, VisualContext, + self as gpui, div, Div, FocusHandle, InteractiveElement, IntoElement, KeyBinding, + Keystroke, ParentElement, Render, Stateful, TestAppContext, VisualContext, }; struct TestView { @@ -315,7 +315,7 @@ mod test { div() .key_context("nested") .track_focus(&self.focus_handle) - .render_once(), + .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 50f48596bc..90d09b3fc5 100644 --- a/crates/gpui2/src/prelude.rs +++ b/crates/gpui2/src/prelude.rs @@ -1,5 +1,5 @@ pub use crate::{ - BorrowAppContext, BorrowWindow, Component, Context, Element, FocusableElement, - InteractiveElement, ParentElement, Refineable, Render, RenderOnce, StatefulInteractiveElement, - 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 f958b8b44c..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, 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, } @@ -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 efa40627ac..f31b0ae753 100644 --- a/crates/gpui2/src/view.rs +++ b/crates/gpui2/src/view.rs @@ -1,7 +1,7 @@ use crate::{ private::Sealed, AnyElement, AnyModel, AnyWeakModel, AppContext, AvailableSpace, BorrowWindow, - Bounds, Element, ElementId, Entity, EntityId, Flatten, FocusHandle, FocusableView, LayoutId, - Model, Pixels, Point, Render, RenderOnce, Size, ViewContext, VisualContext, WeakModel, + Bounds, Element, ElementId, Entity, EntityId, Flatten, FocusHandle, FocusableView, IntoElement, + LayoutId, Model, Pixels, Point, Render, Size, ViewContext, VisualContext, WeakModel, WindowContext, }; use anyhow::{Context, Result}; @@ -244,26 +244,26 @@ impl Element for AnyView { } } -impl RenderOnce for View { +impl IntoElement for View { type Element = View; fn element_id(&self) -> Option { - Some(self.model.entity_id.into()) + Some(ElementId::from_entity_id(self.model.entity_id)) } - fn render_once(self) -> Self::Element { + fn into_element(self) -> Self::Element { self } } -impl RenderOnce for AnyView { +impl IntoElement for AnyView { type Element = Self; fn element_id(&self) -> Option { - Some(self.model.entity_id.into()) + Some(ElementId::from_entity_id(self.model.entity_id)) } - fn render_once(self) -> Self::Element { + fn into_element(self) -> Self::Element { self } } @@ -308,27 +308,23 @@ where } mod any_view { - use crate::{AnyElement, AnyView, BorrowWindow, Element, LayoutId, Render, WindowContext}; + use crate::{AnyElement, AnyView, Element, LayoutId, Render, WindowContext}; pub(crate) fn layout( view: &AnyView, cx: &mut WindowContext, ) -> (LayoutId, AnyElement) { - cx.with_element_id(Some(view.model.entity_id), |cx| { - 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) - }) + 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, + _view: &AnyView, element: AnyElement, cx: &mut WindowContext, ) { - cx.with_element_id(Some(view.model.entity_id), |cx| { - element.paint(cx); - }) + element.paint(cx); } } diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 483a8fdbee..20561c5443 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -193,11 +193,11 @@ pub trait FocusableView: 'static + Render { /// 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. @@ -1512,6 +1517,13 @@ impl<'a> WindowContext<'a> { .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<'_> { @@ -1658,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)) } } @@ -1747,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( @@ -1815,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"); @@ -1827,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 } }) @@ -2304,7 +2374,7 @@ 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( @@ -2599,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; @@ -2611,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_render_once.rs b/crates/gpui2_macros/src/derive_into_element.rs similarity index 60% rename from crates/gpui2_macros/src/derive_render_once.rs rename to crates/gpui2_macros/src/derive_into_element.rs index efe6aab0bb..12c6975e07 100644 --- a/crates/gpui2_macros/src/derive_render_once.rs +++ b/crates/gpui2_macros/src/derive_into_element.rs @@ -2,23 +2,23 @@ use proc_macro::TokenStream; use quote::quote; use syn::{parse_macro_input, DeriveInput}; -pub fn derive_render_once(input: TokenStream) -> TokenStream { +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::RenderOnce for #type_name #type_generics + impl #impl_generics gpui::IntoElement for #type_name #type_generics #where_clause { - type Element = gpui::CompositeElement; + type Element = gpui::Component; fn element_id(&self) -> Option { None } - fn render_once(self) -> Self::Element { - gpui::CompositeElement::new(self) + fn into_element(self) -> Self::Element { + gpui::Component::new(self) } } }; diff --git a/crates/gpui2_macros/src/gpui2_macros.rs b/crates/gpui2_macros/src/gpui2_macros.rs index 6dd817e280..a7fb363d36 100644 --- a/crates/gpui2_macros/src/gpui2_macros.rs +++ b/crates/gpui2_macros/src/gpui2_macros.rs @@ -1,6 +1,5 @@ mod action; -mod derive_component; -mod derive_render_once; +mod derive_into_element; mod register_action; mod style_helpers; mod test; @@ -17,14 +16,9 @@ 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(RenderOnce, attributes(view))] -pub fn derive_render_once(input: TokenStream) -> TokenStream { - derive_render_once::derive_render_once(input) +#[proc_macro_derive(IntoElement)] +pub fn derive_into_element(input: TokenStream) -> TokenStream { + derive_into_element::derive_into_element(input) } #[proc_macro] 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/picker2/src/picker2.rs b/crates/picker2/src/picker2.rs index 786e641ee8..dc6b77c7c7 100644 --- a/crates/picker2/src/picker2.rs +++ b/crates/picker2/src/picker2.rs @@ -4,7 +4,7 @@ use gpui::{ MouseDownEvent, Render, Task, UniformListScrollHandle, View, ViewContext, WindowContext, }; use std::{cmp, sync::Arc}; -use ui::{prelude::*, v_stack, Divider, Label, TextColor}; +use ui::{prelude::*, v_stack, Color, Divider, Label}; pub struct Picker { pub delegate: D, @@ -15,7 +15,7 @@ pub struct Picker { } pub trait PickerDelegate: Sized + 'static { - type ListItem: RenderOnce; + type ListItem: IntoElement; fn match_count(&self) -> usize; fn selected_index(&self) -> usize; @@ -114,6 +114,7 @@ impl Picker { } fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { + dbg!("canceling!"); self.delegate.dismissed(cx); } @@ -250,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/src/project_panel.rs b/crates/project_panel2/src/project_panel.rs index 4d1a6ee8f7..b027209870 100644 --- a/crates/project_panel2/src/project_panel.rs +++ b/crates/project_panel2/src/project_panel.rs @@ -10,8 +10,8 @@ use anyhow::{anyhow, Result}; use gpui::{ actions, div, px, uniform_list, Action, AppContext, AssetSource, AsyncWindowContext, ClipboardItem, Div, EventEmitter, FocusHandle, Focusable, FocusableView, InteractiveElement, - Model, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Render, - RenderOnce, Stateful, StatefulInteractiveElement, Styled, Task, UniformListScrollHandle, View, + IntoElement, Model, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, + Render, Stateful, StatefulInteractiveElement, Styled, Task, UniformListScrollHandle, View, ViewContext, VisualContext as _, WeakView, WindowContext, }; use menu::{Confirm, SelectNext, SelectPrev}; @@ -371,7 +371,7 @@ impl ProjectPanel { _entry_id: ProjectEntryId, _cx: &mut ViewContext, ) { - todo!() + // todo!() // let project = self.project.read(cx); // let worktree_id = if let Some(id) = project.worktree_id_for_entry(entry_id, cx) { @@ -644,6 +644,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); 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/src/buffer_search.rs b/crates/search2/src/buffer_search.rs index 3674baf356..d80d9f5d50 100644 --- a/crates/search2/src/buffer_search.rs +++ b/crates/search2/src/buffer_search.rs @@ -7,12 +7,12 @@ use crate::{ ToggleCaseSensitive, ToggleReplace, ToggleWholeWord, }; use collections::HashMap; -use editor::Editor; +use editor::{Editor, EditorMode}; use futures::channel::oneshot; use gpui::{ - actions, div, red, Action, AppContext, Div, EventEmitter, InteractiveElement as _, - ParentElement as _, Render, RenderOnce, Styled, Subscription, Task, View, ViewContext, - VisualContext as _, WindowContext, + 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; @@ -23,7 +23,7 @@ use util::ResultExt; use workspace::{ item::ItemHandle, searchable::{Direction, SearchEvent, SearchableItemHandle, WeakSearchableItemHandle}, - ToolbarItemLocation, ToolbarItemView, Workspace, + ToolbarItemLocation, ToolbarItemView, }; #[derive(PartialEq, Clone, Deserialize, Default, Action)] @@ -38,7 +38,7 @@ pub enum Event { } pub fn init(cx: &mut AppContext) { - cx.observe_new_views(|workspace: &mut Workspace, _| BufferSearchBar::register(workspace)) + cx.observe_new_views(|editor: &mut Editor, cx| BufferSearchBar::register(editor, cx)) .detach(); } @@ -187,6 +187,7 @@ impl Render for BufferSearchBar { }) .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( @@ -294,9 +295,19 @@ impl ToolbarItemView for BufferSearchBar { } impl BufferSearchBar { - pub fn register(workspace: &mut Workspace) { - workspace.register_action(|workspace, a: &Deploy, cx| { - workspace.active_pane().update(cx, |this, cx| { + 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| { @@ -316,11 +327,16 @@ impl BufferSearchBar { }); }); fn register_action( - workspace: &mut Workspace, + editor: &mut Editor, + handle: WeakView, update: fn(&mut BufferSearchBar, &A, &mut ViewContext), ) { - workspace.register_action(move |workspace, action: &A, cx| { - workspace.active_pane().update(cx, move |this, cx| { + 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)); @@ -331,49 +347,76 @@ impl BufferSearchBar { }); } - register_action(workspace, |this, action: &ToggleCaseSensitive, cx| { - if this.supported_options().case { - this.toggle_case_sensitive(action, cx); - } - }); - register_action(workspace, |this, action: &ToggleWholeWord, cx| { - if this.supported_options().word { - this.toggle_whole_word(action, cx); - } - }); - register_action(workspace, |this, action: &ToggleReplace, cx| { - if this.supported_options().replacement { - this.toggle_replace(action, cx); - } - }); - register_action(workspace, |this, _: &ActivateRegexMode, cx| { + 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(workspace, |this, _: &ActivateTextMode, cx| { + register_action(editor, handle.clone(), |this, _: &ActivateTextMode, cx| { this.activate_search_mode(SearchMode::Text, cx); }); - register_action(workspace, |this, action: &CycleMode, 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(workspace, |this, action: &SelectNextMatch, cx| { - this.select_next_match(action, cx); - }); - register_action(workspace, |this, action: &SelectPrevMatch, cx| { - this.select_prev_match(action, cx); - }); - register_action(workspace, |this, action: &SelectAllMatches, cx| { - this.select_all_matches(action, cx); - }); - register_action(workspace, |this, _: &editor::Cancel, cx| { + 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 { @@ -538,7 +581,7 @@ impl BufferSearchBar { self.update_matches(cx) } - fn render_action_button(&self) -> impl RenderOnce { + fn render_action_button(&self) -> impl IntoElement { // let tooltip_style = theme.tooltip.clone(); // let style = theme.search.action_button.clone(); diff --git a/crates/search2/src/project_search.rs b/crates/search2/src/project_search.rs index f6e17bbee5..41dd87d4d3 100644 --- a/crates/search2/src/project_search.rs +++ b/crates/search2/src/project_search.rs @@ -85,6 +85,7 @@ pub fn init(cx: &mut AppContext) { 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); } @@ -1192,6 +1193,7 @@ impl ProjectSearchView { 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, ) { @@ -1210,6 +1212,7 @@ impl ProjectSearchView { 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, ) { @@ -1764,6 +1767,14 @@ impl View for ProjectSearchBar { 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); @@ -1879,7 +1890,15 @@ impl View for ProjectSearchBar { .with_children(search.filters_enabled.then(|| { Flex::row() .with_child( - ChildView::new(&search.included_files_editor, cx) + 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() diff --git a/crates/search2/src/search.rs b/crates/search2/src/search.rs index 12152701bc..118d9054e6 100644 --- a/crates/search2/src/search.rs +++ b/crates/search2/src/search.rs @@ -1,6 +1,6 @@ use bitflags::bitflags; pub use buffer_search::BufferSearchBar; -use gpui::{actions, Action, AppContext, RenderOnce}; +use gpui::{actions, Action, AppContext, IntoElement}; pub use mode::SearchMode; use project::search::SearchQuery; use ui::ButtonVariant; @@ -82,7 +82,7 @@ impl SearchOptions { options } - pub fn as_button(&self, active: bool) -> impl RenderOnce { + pub fn as_button(&self, active: bool) -> impl IntoElement { ui::IconButton::new(0, self.icon()) .on_click({ let action = self.to_toggle_action(); @@ -95,7 +95,7 @@ impl SearchOptions { } } -fn toggle_replace_button(active: bool) -> impl RenderOnce { +fn toggle_replace_button(active: bool) -> impl IntoElement { // todo: add toggle_replace button ui::IconButton::new(0, ui::Icon::Replace) .on_click(|_, cx| { @@ -109,7 +109,7 @@ fn toggle_replace_button(active: bool) -> impl RenderOnce { fn render_replace_button( action: impl Action + 'static + Send + Sync, icon: ui::Icon, -) -> impl RenderOnce { +) -> 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 index da097b43a6..1a7456f41c 100644 --- a/crates/search2/src/search_bar.rs +++ b/crates/search2/src/search_bar.rs @@ -1,4 +1,4 @@ -use gpui::{MouseDownEvent, RenderOnce, WindowContext}; +use gpui::{IntoElement, MouseDownEvent, WindowContext}; use ui::{Button, ButtonVariant, IconButton}; use crate::mode::SearchMode; @@ -7,7 +7,7 @@ pub(super) fn render_nav_button( icon: ui::Icon, _active: bool, on_click: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static, -) -> impl RenderOnce { +) -> impl IntoElement { // let tooltip_style = cx.theme().tooltip.clone(); // let cursor_style = if active { // CursorStyle::PointingHand 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 8a628a01da..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().clone()), - ) - .child( - div() - .flex() - .gap_1() - .children(ColorScaleStep::ALL.map(|step| { - div().flex().size_6().bg(scale.step(cx, step)) - })), - ) - })), - ) - } -} diff --git a/crates/storybook2/src/stories/focus.rs b/crates/storybook2/src/stories/focus.rs index 7ddeec08bf..6f757240eb 100644 --- a/crates/storybook2/src/stories/focus.rs +++ b/crates/storybook2/src/stories/focus.rs @@ -26,7 +26,7 @@ impl FocusStory { } } -impl Render for FocusStory { +impl Render for FocusStory { type Element = Focusable>; fn render(&mut self, cx: &mut gpui::ViewContext) -> Self::Element { @@ -52,10 +52,8 @@ impl Render for FocusStory { .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, phase, _| println!("Key down on parent {:?}", event)), - ) - .on_key_up(cx.listener(|_, event, phase, _| println!("Key up on parent {:?}", event))) + .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)) diff --git a/crates/storybook2/src/stories/kitchen_sink.rs b/crates/storybook2/src/stories/kitchen_sink.rs index b59e00bf25..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 { @@ -19,11 +21,11 @@ impl Render for KitchenSinkStory { .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 7c2412a02f..ae6a26161b 100644 --- a/crates/storybook2/src/stories/picker.rs +++ b/crates/storybook2/src/stories/picker.rs @@ -36,7 +36,7 @@ impl Delegate { } impl PickerDelegate for Delegate { - type ListItem = Div>; + type ListItem = Div; fn match_count(&self) -> usize { self.candidates.len() @@ -205,8 +205,8 @@ impl PickerStory { } } -impl Render for PickerStory { - type Element = Div; +impl Render for PickerStory { + type Element = Div; fn render(&mut self, cx: &mut gpui::ViewContext) -> Self::Element { div() diff --git a/crates/storybook2/src/stories/scroll.rs b/crates/storybook2/src/stories/scroll.rs index bbab0b1d11..9b9a54e1e6 100644 --- a/crates/storybook2/src/stories/scroll.rs +++ b/crates/storybook2/src/stories/scroll.rs @@ -10,8 +10,8 @@ impl ScrollStory { } } -impl Render for ScrollStory { - type Element = Stateful>; +impl Render for ScrollStory { + type Element = Stateful
; fn render(&mut self, cx: &mut gpui::ViewContext) -> Self::Element { let theme = cx.theme(); @@ -38,7 +38,7 @@ impl Render for ScrollStory { }; div() .id(id) - .tooltip(move |_, cx| Tooltip::text(format!("{}, {}", row, column), cx)) + .tooltip(move |cx| Tooltip::text(format!("{}, {}", row, column), cx)) .bg(bg) .size(px(100. as f32)) .when(row >= 5 && column >= 5, |d| { diff --git a/crates/storybook2/src/stories/text.rs b/crates/storybook2/src/stories/text.rs index c26e5fd3f1..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, ParentElement, Render, Styled, View, VisualContext, WindowContext, + blue, div, green, red, white, Div, InteractiveText, ParentElement, Render, Styled, StyledText, + TextRun, View, VisualContext, WindowContext, }; use ui::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 087ed913fd..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, RenderOnce}; +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; +impl Render for ZIndexStory { + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - Story::container(cx) - .child(Story::title(cx, "z-index")) - .child( - div() - .flex() - .child( - div() - .w(px(250.)) - .child(Story::label(cx, "z-index: auto")) - .child(ZIndexExample::new(0)), - ) - .child( - div() - .w(px(250.)) - .child(Story::label(cx, "z-index: 1")) - .child(ZIndexExample::new(1)), - ) - .child( - div() - .w(px(250.)) - .child(Story::label(cx, "z-index: 3")) - .child(ZIndexExample::new(3)), - ) - .child( - div() - .w(px(250.)) - .child(Story::label(cx, "z-index: 5")) - .child(ZIndexExample::new(5)), - ) - .child( - div() - .w(px(250.)) - .child(Story::label(cx, "z-index: 7")) - .child(ZIndexExample::new(7)), - ), - ) + Story::container().child(Story::title("z-index")).child( + div() + .flex() + .child( + div() + .w(px(250.)) + .child(Story::label("z-index: auto")) + .child(ZIndexExample::new(0)), + ) + .child( + div() + .w(px(250.)) + .child(Story::label("z-index: 1")) + .child(ZIndexExample::new(1)), + ) + .child( + div() + .w(px(250.)) + .child(Story::label("z-index: 3")) + .child(ZIndexExample::new(3)), + ) + .child( + div() + .w(px(250.)) + .child(Story::label("z-index: 5")) + .child(ZIndexExample::new(5)), + ) + .child( + div() + .w(px(250.)) + .child(Story::label("z-index: 7")) + .child(ZIndexExample::new(7)), + ), + ) } } @@ -77,17 +74,17 @@ trait Styles: Styled + Sized { } } -impl Styles for Div {} +impl Styles for Div {} -#[derive(RenderOnce)] +#[derive(IntoElement)] struct ZIndexExample { z_index: u32, } -impl Component for ZIndexExample { - type Rendered = Div; +impl RenderOnce for ZIndexExample { + type Rendered = Div; - fn render(self, view: &mut V, cx: &mut ViewContext) -> Self::Rendered { + fn render(self, cx: &mut WindowContext) -> Self::Rendered { div() .relative() .size_full() diff --git a/crates/storybook2/src/story.rs b/crates/storybook2/src/story.rs deleted file mode 100644 index 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 2a22d91382..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); @@ -105,8 +101,8 @@ impl StoryWrapper { } } -impl Render for StoryWrapper { - type Element = Div; +impl Render for StoryWrapper { + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { div() diff --git a/crates/storybook3/Cargo.toml b/crates/storybook3/Cargo.toml deleted file mode 100644 index 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 cb64bd7f0d..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_view.rs b/crates/terminal_view2/src/terminal_view.rs index 5a5f74f9e1..9f3ed31388 100644 --- a/crates/terminal_view2/src/terminal_view.rs +++ b/crates/terminal_view2/src/terminal_view.rs @@ -31,7 +31,7 @@ use workspace::{ notifications::NotifyResultExt, register_deserializable_item, searchable::{SearchEvent, SearchOptions, SearchableItem}, - ui::{ContextMenu, Icon, IconElement, Label, ListItem}, + ui::{ContextMenu, Icon, IconElement, Label}, CloseActiveItem, NewCenterTerminal, Pane, ToolbarItemLocation, Workspace, WorkspaceId, }; @@ -299,11 +299,8 @@ impl TerminalView { cx: &mut ViewContext, ) { self.context_menu = Some(ContextMenu::build(cx, |menu, _| { - menu.action(ListItem::new("clear", Label::new("Clear")), Box::new(Clear)) - .action( - ListItem::new("close", Label::new("Close")), - Box::new(CloseActiveItem { save_intent: None }), - ) + menu.action("Clear", Box::new(Clear)) + .action("Close", Box::new(CloseActiveItem { save_intent: None })) })); dbg!(&position); // todo!() 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 733cd6c40b..2802bd17b5 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 5e484e12cd..0000000000 --- a/crates/theme2/src/story.rs +++ /dev/null @@ -1,38 +0,0 @@ -use gpui::{div, Div, Element, ParentElement, SharedString, Styled, WindowContext}; - -use crate::ActiveTheme; - -pub struct Story {} - -impl Story { - pub fn container(cx: &mut WindowContext) -> Div { - div() - .size_full() - .flex() - .flex_col() - .pt_2() - .px_4() - .font("Zed Mono") - .bg(cx.theme().colors().background) - } - - pub fn title(cx: &mut WindowContext, title: SharedString) -> impl Element { - div() - .text_xl() - .text_color(cx.theme().colors().text) - .child(title) - } - - pub fn title_for(cx: &mut WindowContext) -> impl Element { - Self::title(cx, std::any::type_name::().into()) - } - - pub fn label(cx: &mut WindowContext, label: impl Into) -> impl Element { - div() - .mt_4() - .mb_2() - .text_xs() - .text_color(cx.theme().colors().text) - .child(label.into()) - } -} diff --git a/crates/theme2/src/styles.rs b/crates/theme2/src/styles.rs index 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 a4734d1c00..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, ParentElement, Render, Styled, ViewContext}; - - pub struct PlayerStory; - - impl Render for PlayerStory { - type Element = Div; - - fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - Story::container(cx).child( - div() - .flex() - .flex_col() - .gap_4() - .child(Story::title_for::(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 39c5924fb9..c5c79237ba 100644 --- a/crates/theme2/src/theme2.rs +++ b/crates/theme2/src/theme2.rs @@ -134,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)] @@ -144,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/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 ab79352f86..d358b221da 100644 --- a/crates/ui2/src/components/avatar.rs +++ b/crates/ui2/src/components/avatar.rs @@ -1,13 +1,22 @@ -use crate::prelude::*; -use gpui::{img, Img, RenderOnce}; +use std::sync::Arc; -#[derive(RenderOnce)] +use crate::prelude::*; +use gpui::{img, ImageData, ImageSource, Img, IntoElement}; + +#[derive(Debug, Default, PartialEq, Clone)] +pub enum Shape { + #[default] + Circle, + RoundedRectangle, +} + +#[derive(IntoElement)] pub struct Avatar { - src: SharedString, + src: ImageSource, shape: Shape, } -impl Component for Avatar { +impl RenderOnce for Avatar { type Rendered = Img; fn render(self, _: &mut WindowContext) -> Self::Rendered { @@ -19,7 +28,7 @@ impl Component for 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()) @@ -27,7 +36,13 @@ impl Component for Avatar { } impl Avatar { - pub fn new(src: impl Into) -> Self { + 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, @@ -39,31 +54,3 @@ impl Avatar { self } } - -#[cfg(feature = "stories")] -pub use stories::*; - -#[cfg(feature = "stories")] -mod stories { - use super::*; - use crate::Story; - use gpui::{Div, Render}; - - pub struct AvatarStory; - - impl Render for AvatarStory { - type Element = Div; - - fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - Story::container(cx) - .child(Story::title_for::(cx)) - .child(Story::label(cx, "Default")) - .child(Avatar::new( - "https://avatars.githubusercontent.com/u/1714999?v=4", - )) - .child(Avatar::new( - "https://avatars.githubusercontent.com/u/326587?v=4", - )) - } - } -} diff --git a/crates/ui2/src/components/button.rs b/crates/ui2/src/components/button.rs index 3d92cebec5..02902a4b64 100644 --- a/crates/ui2/src/components/button.rs +++ b/crates/ui2/src/components/button.rs @@ -1,12 +1,12 @@ use std::rc::Rc; use gpui::{ - DefiniteLength, Div, Hsla, MouseButton, MouseDownEvent, RenderOnce, StatefulInteractiveElement, - WindowContext, + 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. @@ -64,7 +64,7 @@ impl ButtonVariant { } } -#[derive(RenderOnce)] +#[derive(IntoElement)] pub struct Button { disabled: bool, click_handler: Option>, @@ -73,17 +73,17 @@ pub struct Button { label: SharedString, variant: ButtonVariant, width: Option, - color: Option, + color: Option, } -impl Component for Button { +impl RenderOnce for Button { type Rendered = gpui::Stateful
; fn render(self, cx: &mut WindowContext) -> Self::Rendered { let (icon_color, label_color) = match (self.disabled, self.color) { - (true, _) => (TextColor::Disabled, TextColor::Disabled), - (_, None) => (TextColor::Default, TextColor::Default), - (_, Some(color)) => (TextColor::from(color), color), + (true, _) => (Color::Disabled, Color::Disabled), + (_, None) => (Color::Default, Color::Default), + (_, Some(color)) => (Color::from(color), color), }; let mut button = h_stack() @@ -181,14 +181,14 @@ impl Button { self } - pub fn color(mut self, color: Option) -> Self { + pub fn color(mut self, color: Option) -> Self { self.color = color; self } - pub fn label_color(&self, color: Option) -> TextColor { + pub fn label_color(&self, color: Option) -> Color { if self.disabled { - TextColor::Disabled + Color::Disabled } else if let Some(color) = color { color } else { @@ -196,23 +196,23 @@ impl Button { } } - fn render_label(&self, color: TextColor) -> Label { + fn render_label(&self, color: Color) -> Label { Label::new(self.label.clone()) .color(color) .line_height_style(LineHeightStyle::UILabel) } - fn render_icon(&self, icon_color: TextColor) -> Option { + fn render_icon(&self, icon_color: Color) -> Option { self.icon.map(|i| IconElement::new(i).color(icon_color)) } } -#[derive(RenderOnce)] +#[derive(IntoElement)] pub struct ButtonGroup { buttons: Vec