Detailed changes
@@ -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"]
@@ -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
@@ -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
@@ -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: |
@@ -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",
@@ -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
@@ -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
@@ -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,
@@ -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
}
@@ -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<NotificationEvent> for UpdateNotification {}
+impl EventEmitter<DismissEvent> for UpdateNotification {}
impl Render for UpdateNotification {
type Element = Div;
@@ -82,6 +83,6 @@ impl UpdateNotification {
}
pub fn _dismiss(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
- cx.emit(NotificationEvent::Dismiss);
+ cx.emit(DismissEvent::Dismiss);
}
}
@@ -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"] }
@@ -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;
+pub use room::Room;
use settings::Settings;
+use shared_screen::SharedScreen;
use std::sync::Arc;
-
-pub use participant::ParticipantLocation;
-pub use room::Room;
+use util::ResultExt;
+use workspace::{item::ItemHandle, CallHandler, Pane, Workspace};
pub fn init(client: Arc<Client>, user_store: Model<UserStore>, 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<u64>,
client: &Arc<Client>,
- 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<ActiveCall>, Vec<Subscription>)>,
+ parent_workspace: WeakView<Workspace>,
+}
+
+impl Call {
+ pub fn new(
+ parent_workspace: WeakView<Workspace>,
+ cx: &mut ViewContext<'_, Workspace>,
+ ) -> Box<dyn CallHandler> {
+ let mut active_call = None;
+ if cx.has_global::<Model<ActiveCall>>() {
+ let call = cx.global::<Model<ActiveCall>>().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<ActiveCall>,
+ event: &room::Event,
+ cx: &mut ViewContext<Workspace>,
+ ) {
+ 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<Workspace>,
+ ) -> 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<Pane>,
+ cx: &mut ViewContext<Workspace>,
+ ) -> Option<Box<dyn ItemHandle>> {
+ 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::<SharedScreen>() {
+ 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<u64> {
+ Some(self.active_call.as_ref()?.0.read(cx).room()?.read(cx).id())
+ }
+ fn hang_up(&self, cx: &mut AppContext) -> Task<Result<()>> {
+ 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<WeakModel<Project>> {
+ ActiveCall::global(cx).read(cx).location().cloned()
+ }
+ fn invite(
+ &mut self,
+ called_user_id: u64,
+ initial_project: Option<Model<Project>>,
+ cx: &mut AppContext,
+ ) -> Task<Result<()>> {
+ ActiveCall::global(cx).update(cx, |this, cx| {
+ this.invite(called_user_id, initial_project, cx)
+ })
+ }
+ fn remote_participants(&self, cx: &AppContext) -> Option<Vec<(Arc<User>, 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<bool> {
+ 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<bool> {
+ 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;
@@ -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;
@@ -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(
@@ -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<RemoteVideoTrack>,
+ frame: Option<Frame>,
+ // temporary addition just to render something interactive.
+ current_frame_id: usize,
+ pub peer_id: PeerId,
+ user: Arc<User>,
+ nav_history: Option<ItemNavHistory>,
+ _maintain_frame: Task<Result<()>>,
+ focus: FocusHandle,
+}
+
+impl SharedScreen {
+ pub fn new(
+ track: &Arc<RemoteVideoTrack>,
+ peer_id: PeerId,
+ user: Arc<User>,
+ cx: &mut ViewContext<Self>,
+ ) -> 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<Event> for SharedScreen {}
+impl EventEmitter<workspace::item::ItemEvent> 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>) -> 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<Self>) -> AnyElement<Self> {
+// enum Focus {}
+
+// let frame = self.frame.clone();
+// MouseEventHandler::new::<Focus, _>(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<SharedString> {
+ Some(format!("{}'s screen", self.user.github_login).into())
+ }
+ fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
+ if let Some(nav_history) = self.nav_history.as_mut() {
+ nav_history.push::<()>(None, cx);
+ }
+ }
+
+ fn tab_content(&self, _: Option<usize>, _: &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>) {
+ self.nav_history = Some(history);
+ }
+
+ fn clone_on_split(
+ &self,
+ _workspace_id: WorkspaceId,
+ cx: &mut ViewContext<Self>,
+ ) -> Option<View<Self>> {
+ let track = self.track.upgrade()?;
+ Some(cx.build_view(|cx| Self::new(&track, self.peer_id, self.user.clone(), cx)))
+ }
+}
@@ -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<Self>,
+ 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<Self>,
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 {
@@ -382,7 +382,7 @@ impl settings::Settings for TelemetrySettings {
}
impl Client {
- pub fn new(http: Arc<dyn HttpClient>, cx: &AppContext) -> Arc<Self> {
+ pub fn new(http: Arc<dyn HttpClient>, cx: &mut AppContext) -> Arc<Self> {
Arc::new(Self {
id: AtomicU64::new(0),
peer: Peer::new(0),
@@ -551,7 +551,6 @@ impl Client {
F: 'static + Future<Output = Result<()>>,
{
let message_type_id = TypeId::of::<M>();
-
let mut state = self.state.write();
state
.models_by_message_type
@@ -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<dyn HttpClient>, cx: &AppContext) -> Arc<Self> {
+ pub fn new(client: Arc<dyn HttpClient>, cx: &mut AppContext) -> Arc<Self> {
let release_channel = if cx.has_global::<ReleaseChannel>() {
Some(cx.global::<ReleaseChannel>().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<Self>, _: &mut AppContext) -> impl Future<Output = ()> {
+ 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<Self>, cx: &mut AppContext) -> impl Future<Output = ()> {
+ let telemetry_settings = TelemetrySettings::get_global(cx).clone();
+ self.report_app_event(telemetry_settings, "close");
+ Task::ready(())
+ }
+
pub fn log_file_path(&self) -> Option<PathBuf> {
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<Self>,
+ 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<Self>,
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 {
@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
default-run = "collab"
edition = "2021"
name = "collab"
-version = "0.28.0"
+version = "0.29.0"
publish = false
[[bin]]
@@ -10,7 +10,7 @@ publish = false
name = "collab2"
[[bin]]
-name = "seed"
+name = "seed2"
required-features = ["seed-support"]
[dependencies]
@@ -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| {
@@ -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<ChannelEditingState>,
// entries: Vec<ListEntry>,
// selection: Option<usize>,
- // user_store: ModelHandle<UserStore>,
- // client: Arc<Client>,
+ user_store: Model<UserStore>,
+ _client: Arc<Client>,
// channel_store: ModelHandle<ChannelStore>,
// project: ModelHandle<Project>,
// match_candidates: Vec<StringMatchCandidate>,
@@ -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<Vec<Arc<Contact>>> {
+ Some(self.user_store.read(cx).contacts().to_owned())
+ }
pub async fn load(
workspace: WeakView<Workspace>,
mut cx: AsyncWindowContext,
@@ -3297,11 +3302,38 @@ impl CollabPanel {
impl Render for CollabPanel {
type Element = Focusable<Div>;
- fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> 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();
+ }
+ })
+ }))
}
}
@@ -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<Div>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> 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);
+ }))
+ }
+ })
}
}
@@ -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<AppState>, cx: &mut AppContext) {
+pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
CollaborationPanelSettings::register(cx);
ChatPanelSettings::register(cx);
NotificationPanelSettings::register(cx);
@@ -32,7 +35,7 @@ pub fn init(_app_state: &Arc<AppState>, 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<AppState>, cx: &mut AppContext) {
// }
// }
-// fn notification_window_options(
-// screen: Rc<dyn Screen>,
-// window_size: Vector2F,
-// ) -> WindowOptions<'static> {
-// const NOTIFICATION_PADDING: f32 = 16.;
+fn notification_window_options(
+ screen: Rc<dyn PlatformDisplay>,
+ window_size: Size<Pixels>,
+) -> 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<GlobalPixels> = 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::<GlobalPixels> {
+ 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<T: 'static>(
// avatar: Option<Arc<ImageData>>,
@@ -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<AnyElement>,
+}
-// pub(crate) struct FacePile<V: View> {
-// overlap: f32,
-// faces: Vec<AnyElement<V>>,
-// }
+impl RenderOnce for FacePile {
+ type Rendered = Div;
-// impl<V: View> FacePile<V> {
-// 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<V: View> Element<V> for FacePile<V> {
-// 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<V>,
-// ) -> (Vector2F, Self::LayoutState) {
-// debug_assert!(constraint.max_along(Axis::Horizontal) == f32::INFINITY);
-
+// state: Option<Self::State>,
+// 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<usize>,
-// _: RectF,
-// _: RectF,
-// _: &Self::LayoutState,
-// _: &Self::PaintState,
-// _: &V,
-// _: &ViewContext<V>,
-// ) -> Option<RectF> {
-// None
-// }
-
-// fn debug(
-// &self,
-// bounds: RectF,
-// _: &Self::LayoutState,
-// _: &Self::PaintState,
-// _: &V,
-// _: &ViewContext<V>,
-// ) -> serde_json::Value {
-// json!({
-// "type": "FacePile",
-// "bounds": bounds.to_json()
-// })
-// }
// }
-// impl<V: View> Extend<AnyElement<V>> for FacePile<V> {
-// fn extend<T: IntoIterator<Item = AnyElement<V>>>(&mut self, children: T) {
-// self.faces.extend(children);
-// }
-// }
+impl Extend<AnyElement> for FacePile {
+ fn extend<T: IntoIterator<Item = AnyElement>>(&mut self, children: T) {
+ self.faces.extend(children);
+ }
+}
@@ -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<AppState>, cx: &mut AppContext) {
-// incoming_call_notification::init(app_state, cx);
-// project_shared_notification::init(app_state, cx);
-// }
+pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
+ incoming_call_notification::init(app_state, cx);
+ //project_shared_notification::init(app_state, cx);
+}
@@ -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<AppState>, cx: &mut AppContext) {
let mut notification_windows: Vec<WindowHandle<IncomingCallNotification>> = 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<AppState>,
}
-impl IncomingCallNotification {
+pub struct IncomingCallNotification {
+ state: Arc<IncomingCallNotificationState>,
+}
+impl IncomingCallNotificationState {
pub fn new(call: IncomingCall, app_state: Weak<AppState>) -> Self {
Self { call, app_state }
}
- fn respond(&mut self, accept: bool, cx: &mut ViewContext<Self>) {
+ 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<Self>) -> AnyElement<Self> {
- 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()
+impl IncomingCallNotification {
+ pub fn new(call: IncomingCall, app_state: Weak<AppState>) -> Self {
+ Self {
+ state: Arc::new(IncomingCallNotificationState::new(call, app_state)),
+ }
}
-
- fn render_buttons(&self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
- enum Accept {}
- enum Decline {}
-
- let theme = theme::current(cx);
- Flex::column()
- .with_child(
- MouseEventHandler::new::<Accept, _>(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),
+ fn render_caller(&self, cx: &mut ViewContext<Self>) -> impl Element {
+ h_stack()
+ .children(
+ self.state
+ .call
+ .calling_user
+ .avatar
+ .as_ref()
+ .map(|avatar| Avatar::data(avatar.clone())),
)
- .with_child(
- MouseEventHandler::new::<Decline, _>(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),
+ .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)),
)
- .constrained()
- .with_width(theme.incoming_call_notification.button_width)
- .into_any()
+ // 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()
}
-}
-impl Entity for IncomingCallNotification {
- type Event = ();
-}
+ fn render_buttons(&self, cx: &mut ViewContext<Self>) -> 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)
+ }))
-impl View for IncomingCallNotification {
- fn ui_name() -> &'static str {
- "IncomingCallNotification"
- }
+ // enum Accept {}
+ // enum Decline {}
- fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
- 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()
+ // let theme = theme::current(cx);
+ // Flex::column()
+ // .with_child(
+ // MouseEventHandler::new::<Accept, _>(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::<Decline, _>(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 Render for IncomingCallNotification {
+ type Element = Div;
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> 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()
}
}
@@ -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<Manager> for CommandPalette {}
+impl EventEmitter<DismissEvent> 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<Picker<Self>>) {
self.command_palette
- .update(cx, |_, cx| cx.emit(Manager::Dismiss))
+ .update(cx, |_, cx| cx.emit(DismissEvent::Dismiss))
.log_err();
}
@@ -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()?,
@@ -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>) -> 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()
@@ -1001,17 +1001,18 @@ impl CompletionsMenu {
fn pre_resolve_completion_documentation(
&self,
- project: Option<ModelHandle<Project>>,
+ editor: &Editor,
cx: &mut ViewContext<Editor>,
- ) {
+ ) -> Option<Task<()>> {
let settings = settings::get::<EditorSettings>(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<Arc<Language>>>,
cx: &mut ViewContext<'_, '_, Editor>,
) -> HashMap<ExcerptId, (ModelHandle<Buffer>, Global, Range<usize>)> {
+ 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));
}
@@ -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<Editor>,
cx: &mut gpui::TestAppContext,
) -> Range<Point> {
- 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,
@@ -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<Tag: 'static>(
-// parsed: &language::ParsedMarkdown,
-// editor_style: &EditorStyle,
-// workspace: Option<WeakView<Workspace>>,
-// cx: &mut ViewContext<Editor>,
-// ) -> Text {
-// enum RenderedMarkdown {}
-
-// let parsed = parsed.clone();
-// let view_id = cx.view_id();
-// let code_span_background_color = editor_style.document_highlight_read_background;
-
-// let mut region_id = 0;
-
-// 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::<Vec<_>>(),
-// // )
-// // .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::<Editor, _>(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)
-// }
+pub fn render_parsed_markdown(
+ element_id: impl Into<ElementId>,
+ parsed: &language::ParsedMarkdown,
+ editor_style: &EditorStyle,
+ workspace: Option<WeakView<Workspace>>,
+ cx: &mut ViewContext<Editor>,
+) -> InteractiveText {
+ let code_span_background_color = cx
+ .theme()
+ .colors()
+ .editor_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);
+
+ // 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());
+ }
+ }
+
+ 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::<Editor>(cx);
workspace::register_followable_item::<Editor>(cx);
workspace::register_deserializable_item::<Editor>(cx);
@@ -663,6 +663,7 @@ pub struct Editor {
pixel_position_of_newest_cursor: Option<gpui::Point<Pixels>>,
gutter_width: Pixels,
style: Option<EditorStyle>,
+ editor_actions: Vec<Box<dyn Fn(&mut ViewContext<Self>)>>,
}
pub struct EditorSnapshot {
@@ -905,12 +906,16 @@ impl ContextMenu {
&self,
cursor_position: DisplayPoint,
style: &EditorStyle,
+ max_height: Pixels,
workspace: Option<WeakView<Workspace>>,
cx: &mut ViewContext<Editor>,
) -> (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<Model<Project>>,
- cx: &mut ViewContext<Editor>,
- ) {
+ _editor: &Editor,
+ _cx: &mut ViewContext<Editor>,
+ ) -> Option<Task<()>> {
// 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,210 +1227,144 @@ impl CompletionsMenu {
fn render(
&self,
style: &EditorStyle,
+ max_height: Pixels,
workspace: Option<WeakView<Workspace>>,
cx: &mut ViewContext<Editor>,
) -> AnyElement {
- todo!("old implementation below")
- }
+ let settings = EditorSettings::get_global(cx);
+ let show_completion_documentation = settings.show_completion_documentation;
- // enum CompletionTag {}
+ let widest_completion_ix = self
+ .matches
+ .iter()
+ .enumerate()
+ .max_by_key(|(_, mat)| {
+ let completions = self.completions.read();
+ let completion = &completions[mat.candidate_id];
+ let documentation = &completion.documentation;
+
+ let mut len = completion.label.text.chars().count();
+ if let Some(Documentation::SingleLine(text)) = documentation {
+ if show_completion_documentation {
+ len += text.chars().count();
+ }
+ }
- // let settings = EditorSettings>(cx);
- // let show_completion_documentation = settings.show_completion_documentation;
+ len
+ })
+ .map(|(ix, _)| ix);
- // let widest_completion_ix = self
- // .matches
- // .iter()
- // .enumerate()
- // .max_by_key(|(_, mat)| {
- // let completions = self.completions.read();
- // let completion = &completions[mat.candidate_id];
- // let documentation = &completion.documentation;
+ let completions = self.completions.clone();
+ let matches = self.matches.clone();
+ let selected_item = self.selected_item;
+ let style = style.clone();
- // let mut len = completion.label.text.chars().count();
- // if let Some(Documentation::SingleLine(text)) = documentation {
- // if show_completion_documentation {
- // len += text.chars().count();
- // }
- // }
+ 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();
- // len
- // })
- // .map(|(ix, _)| ix);
+ matches[range]
+ .iter()
+ .enumerate()
+ .map(|(ix, mat)| {
+ let item_ix = start_ix + ix;
+ let candidate_id = mat.candidate_id;
+ let completion = &completions_guard[candidate_id];
- // let completions = self.completions.clone();
- // let matches = self.matches.clone();
- // let selected_item = self.selected_item;
-
- // let list = UniformList::new(self.list.clone(), matches.len(), cx, {
- // let style = style.clone();
- // move |_, range, items, cx| {
- // let start_ix = range.start;
- // let completions_guard = completions.read();
-
- // for (ix, mat) in matches[range].iter().enumerate() {
- // let item_ix = start_ix + ix;
- // let candidate_id = mat.candidate_id;
- // let completion = &completions_guard[candidate_id];
-
- // let documentation = if show_completion_documentation {
- // &completion.documentation
- // } else {
- // &None
- // };
+ let documentation = if show_completion_documentation {
+ &completion.documentation
+ } else {
+ &None
+ };
- // items.push(
- // MouseEventHandler::new::<CompletionTag, _>(
- // 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::<MultiLineDocumentation>(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::<MultiLineDocumentation>(0, None, cx)
- // .with_child(render_parsed_markdown::<MultiLineDocumentation>(
- // 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()
- // }
+ 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()
+ }
pub async fn filter(&mut self, query: Option<&str>, executor: BackgroundExecutor) {
let mut matches = if let Some(query) = query {
@@ -1540,6 +1480,7 @@ impl CodeActionsMenu {
&self,
mut cursor_position: DisplayPoint,
style: &EditorStyle,
+ max_height: Pixels,
cx: &mut ViewContext<Editor>,
) -> (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<View<Workspace>> {
+ pub fn workspace(&self) -> Option<View<Workspace>> {
self.workspace.as_ref()?.0.upgrade()
}
+ pub fn pane(&self, cx: &AppContext) -> Option<View<Pane>> {
+ 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<Arc<Language>>>,
cx: &mut ViewContext<Editor>,
) -> HashMap<ExcerptId, (Model<Buffer>, clock::Global, Range<usize>)> {
+ 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<Self>,
- // ) -> Option<Task<Result<()>>> {
- // use language::ToOffset as _;
+ pub fn confirm_completion(
+ &mut self,
+ action: &ConfirmCompletion,
+ cx: &mut ViewContext<Self>,
+ ) -> Option<Task<Result<()>>> {
+ 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 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::<usize>(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::<String>();
-
- // let newest_selection = self.selections.newest_anchor();
- // if newest_selection.start.buffer_id != Some(buffer_handle.read(cx).remote_id()) {
- // 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 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::<usize>(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::<String>();
- // 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<Range<isize>> = 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 newest_selection = self.selections.newest_anchor();
+ if newest_selection.start.buffer_id != Some(buffer_handle.read(cx).remote_id()) {
+ return None;
+ }
- // cx.emit(Event::InputHandled {
- // utf16_range_to_replace: range_to_replace,
- // text: text.into(),
- // });
+ 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();
- // 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;
- // }
+ let snapshot = self.buffer.read(cx).snapshot(cx);
+ let mut range_to_replace: Option<Range<isize>> = 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..];
- // 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,
- // );
- // });
- // }
+ cx.emit(EditorEvent::InputHandled {
+ utf16_range_to_replace: range_to_replace,
+ text: text.into(),
+ });
- // this.refresh_copilot_suggestions(true, cx);
- // });
+ 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;
+ }
- // 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(())
- // }))
- // }
+ 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);
+ });
+
+ 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<Self>) {
let mut context_menu = self.context_menu.write();
@@ -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,
@@ -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<Pixels>,
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<Pixels>,
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);
-
- // // 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;
-
- // // 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);
-
- // 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);
-
- // 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);
- // }
-
- // hover_popover.paint(
- // popover_origin,
- // Bounds::<Pixels>::from_points(
- // gpui::Point::<Pixels>::zero(),
- // point(f32::MAX, f32::MAX),
- // ), // Let content bleed outside of editor
- // editor,
- // 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);
-
- // 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);
- // }
-
- // hover_popover.paint(
- // popover_origin,
- // Bounds::<Pixels>::from_points(
- // gpui::Point::<Pixels>::zero(),
- // point(f32::MAX, f32::MAX),
- // ), // Let content bleed outside of editor
- // editor,
- // cx,
- // );
-
- // current_y = popover_origin.y + size.y + HOVER_POPOVER_GAP;
- // }
- // }
-
- // cx.scene().pop_stacking_context();
- // }
+ 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;
+
+ // 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)
+ - 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 > 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 =
+ 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;
+ }
+
+ 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 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 =
+ 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.draw(popover_origin, available_space, cx);
+
+ current_y = popover_origin.y + size.height + HOVER_POPOVER_GAP;
+ }
+ }
+ }
+ })
},
)
}
@@ -1217,7 +1396,6 @@ impl EditorElement {
&mut self,
bounds: Bounds<Pixels>,
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<Editor>) -> 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<Editor>) -> 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<u32>,
line_number_layouts: &[Option<ShapedLine>],
snapshot: &EditorSnapshot,
@@ -1469,483 +1647,457 @@ impl EditorElement {
fn compute_layout(
&mut self,
- editor: &mut Editor,
- cx: &mut ViewContext<'_, Editor>,
mut bounds: Bounds<Pixels>,
+ cx: &mut WindowContext,
) -> LayoutState {
- // let mut size = constraint.max;
- // if size.x.is_infinite() {
- // unimplemented!("we don't yet handle an infinite width constraint on buffer elements");
- // }
-
- let snapshot = editor.snapshot(cx);
- let style = self.style.clone();
+ self.editor.update(cx, |editor, cx| {
+ // let mut size = constraint.max;
+ // if size.x.is_infinite() {
+ // unimplemented!("we don't yet handle an infinite width constraint on buffer elements");
+ // }
+
+ let snapshot = editor.snapshot(cx);
+ let style = self.style.clone();
+
+ let font_id = cx.text_system().font_id(&style.text.font()).unwrap();
+ let font_size = style.text.font_size.to_pixels(cx.rem_size());
+ let line_height = style.text.line_height_in_pixels(cx.rem_size());
+ let em_width = cx
+ .text_system()
+ .typographic_bounds(font_id, font_size, 'm')
+ .unwrap()
+ .size
+ .width;
+ let em_advance = cx
+ .text_system()
+ .advance(font_id, font_size, 'm')
+ .unwrap()
+ .width;
+
+ let gutter_padding;
+ let gutter_width;
+ let gutter_margin;
+ if snapshot.show_gutter {
+ let descent = cx.text_system().descent(font_id, font_size).unwrap();
+
+ let gutter_padding_factor = 3.5;
+ gutter_padding = (em_width * gutter_padding_factor).round();
+ gutter_width = self.max_line_number_width(&snapshot, cx) + gutter_padding * 2.0;
+ gutter_margin = -descent;
+ } else {
+ gutter_padding = Pixels::ZERO;
+ gutter_width = Pixels::ZERO;
+ gutter_margin = Pixels::ZERO;
+ };
- let font_id = cx.text_system().font_id(&style.text.font()).unwrap();
- let font_size = style.text.font_size.to_pixels(cx.rem_size());
- let line_height = style.text.line_height_in_pixels(cx.rem_size());
- let em_width = cx
- .text_system()
- .typographic_bounds(font_id, font_size, 'm')
- .unwrap()
- .size
- .width;
- let em_advance = cx
- .text_system()
- .advance(font_id, font_size, 'm')
- .unwrap()
- .width;
-
- let gutter_padding;
- let gutter_width;
- let gutter_margin;
- if snapshot.show_gutter {
- let descent = cx.text_system().descent(font_id, font_size).unwrap();
-
- let gutter_padding_factor = 3.5;
- gutter_padding = (em_width * gutter_padding_factor).round();
- gutter_width = self.max_line_number_width(&snapshot, cx) + gutter_padding * 2.0;
- gutter_margin = -descent;
- } else {
- gutter_padding = Pixels::ZERO;
- gutter_width = Pixels::ZERO;
- gutter_margin = Pixels::ZERO;
- };
+ editor.gutter_width = gutter_width;
+ let text_width = bounds.size.width - gutter_width;
+ let overscroll = size(em_width, px(0.));
+ let snapshot = {
+ editor.set_visible_line_count((bounds.size.height / line_height).into(), cx);
+
+ let editor_width = text_width - gutter_margin - overscroll.width - em_width;
+ let wrap_width = match editor.soft_wrap_mode(cx) {
+ SoftWrap::None => (MAX_LINE_LEN / 2) as f32 * em_advance,
+ SoftWrap::EditorWidth => editor_width,
+ SoftWrap::Column(column) => editor_width.min(column as f32 * em_advance),
+ };
- editor.gutter_width = gutter_width;
- let text_width = bounds.size.width - gutter_width;
- let overscroll = size(em_width, px(0.));
- let snapshot = {
- editor.set_visible_line_count((bounds.size.height / line_height).into(), cx);
-
- let editor_width = text_width - gutter_margin - overscroll.width - em_width;
- let wrap_width = match editor.soft_wrap_mode(cx) {
- SoftWrap::None => (MAX_LINE_LEN / 2) as f32 * em_advance,
- SoftWrap::EditorWidth => editor_width,
- SoftWrap::Column(column) => editor_width.min(column as f32 * em_advance),
+ if editor.set_wrap_width(Some(wrap_width), cx) {
+ editor.snapshot(cx)
+ } else {
+ snapshot
+ }
};
- if editor.set_wrap_width(Some(wrap_width), cx) {
- editor.snapshot(cx)
+ let wrap_guides = editor
+ .wrap_guides(cx)
+ .iter()
+ .map(|(guide, active)| (self.column_pixels(*guide, cx), *active))
+ .collect::<SmallVec<[_; 2]>>();
+
+ 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::<SmallVec<[_; 2]>>();
-
- let scroll_height = Pixels::from(snapshot.max_point().row() + 1) * line_height;
- // todo!("this should happen during layout")
- let editor_mode = snapshot.mode;
- if let EditorMode::AutoHeight { max_lines } = editor_mode {
- todo!()
- // size.set_y(
- // scroll_height
- // .min(constraint.max_along(Axis::Vertical))
- // .max(constraint.min_along(Axis::Vertical))
- // .max(line_height)
- // .min(line_height * max_lines as f32),
- // )
- } else if let EditorMode::SingleLine = editor_mode {
- bounds.size.height = line_height.min(bounds.size.height);
- }
- // todo!()
- // else if size.y.is_infinite() {
- // // size.set_y(scroll_height);
- // }
- //
- let gutter_size = size(gutter_width, bounds.size.height);
- let text_size = size(text_width, bounds.size.height);
-
- let autoscroll_horizontally =
- editor.autoscroll_vertically(bounds.size.height, line_height, cx);
- let mut snapshot = editor.snapshot(cx);
-
- let scroll_position = snapshot.scroll_position();
- // The scroll position is a fractional point, the whole number of which represents
- // the top of the window in terms of display rows.
- let start_row = scroll_position.y as u32;
- let height_in_lines = f32::from(bounds.size.height / line_height);
- let max_row = snapshot.max_point().row();
-
- // Add 1 to ensure selections bleed off screen
- let end_row = 1 + cmp::min((scroll_position.y + height_in_lines).ceil() as u32, max_row);
-
- let start_anchor = if start_row == 0 {
- Anchor::min()
- } else {
- snapshot
- .buffer_snapshot
- .anchor_before(DisplayPoint::new(start_row, 0).to_offset(&snapshot, Bias::Left))
- };
- let end_anchor = if end_row > max_row {
- Anchor::max()
- } else {
- snapshot
- .buffer_snapshot
- .anchor_before(DisplayPoint::new(end_row, 0).to_offset(&snapshot, Bias::Right))
- };
+ let mut selections: Vec<(PlayerColor, Vec<SelectionLayout>)> = Vec::new();
+ let mut active_rows = BTreeMap::new();
+ let is_singleton = editor.is_singleton(cx);
- let mut selections: Vec<(PlayerColor, Vec<SelectionLayout>)> = Vec::new();
- let mut active_rows = BTreeMap::new();
- let is_singleton = editor.is_singleton(cx);
+ let highlighted_rows = editor.highlighted_rows();
+ let highlighted_ranges = editor.background_highlights_in_range(
+ start_anchor..end_anchor,
+ &snapshot.display_snapshot,
+ cx.theme().colors(),
+ );
- let highlighted_rows = editor.highlighted_rows();
- let highlighted_ranges = editor.background_highlights_in_range(
- start_anchor..end_anchor,
- &snapshot.display_snapshot,
- cx.theme().colors(),
- );
+ let mut newest_selection_head = None;
+
+ if editor.show_local_selections {
+ let mut local_selections: Vec<Selection<Point>> = editor
+ .selections
+ .disjoint_in_range(start_anchor..end_anchor, cx);
+ local_selections.extend(editor.selections.pending(cx));
+ let mut layouts = Vec::new();
+ let newest = editor.selections.newest(cx);
+ for selection in local_selections.drain(..) {
+ let is_empty = selection.start == selection.end;
+ let is_newest = selection == newest;
+
+ let layout = SelectionLayout::new(
+ selection,
+ editor.selections.line_mode,
+ editor.cursor_shape,
+ &snapshot.display_snapshot,
+ is_newest,
+ true,
+ );
+ if is_newest {
+ newest_selection_head = Some(layout.head);
+ }
- let mut newest_selection_head = None;
-
- if editor.show_local_selections {
- let mut local_selections: Vec<Selection<Point>> = editor
- .selections
- .disjoint_in_range(start_anchor..end_anchor, cx);
- local_selections.extend(editor.selections.pending(cx));
- let mut layouts = Vec::new();
- let newest = editor.selections.newest(cx);
- for selection in local_selections.drain(..) {
- let is_empty = selection.start == selection.end;
- let is_newest = selection == newest;
-
- let layout = SelectionLayout::new(
- selection,
- editor.selections.line_mode,
- editor.cursor_shape,
- &snapshot.display_snapshot,
- is_newest,
- true,
- );
- if is_newest {
- newest_selection_head = Some(layout.head);
+ for row in cmp::max(layout.active_rows.start, start_row)
+ ..=cmp::min(layout.active_rows.end, end_row)
+ {
+ let contains_non_empty_selection = active_rows.entry(row).or_insert(!is_empty);
+ *contains_non_empty_selection |= !is_empty;
+ }
+ layouts.push(layout);
}
- for row in cmp::max(layout.active_rows.start, start_row)
- ..=cmp::min(layout.active_rows.end, end_row)
- {
- let contains_non_empty_selection = active_rows.entry(row).or_insert(!is_empty);
- *contains_non_empty_selection |= !is_empty;
- }
- layouts.push(layout);
+ selections.push((style.local_player, layouts));
}
- selections.push((style.local_player, layouts));
- }
-
- if let Some(collaboration_hub) = &editor.collaboration_hub {
- // When following someone, render the local selections in their color.
- if let Some(leader_id) = editor.leader_peer_id {
- if let Some(collaborator) = collaboration_hub.collaborators(cx).get(&leader_id) {
- if let Some(participant_index) = collaboration_hub
- .user_participant_indices(cx)
- .get(&collaborator.user_id)
- {
- if let Some((local_selection_style, _)) = selections.first_mut() {
- *local_selection_style = cx
- .theme()
- .players()
- .color_for_participant(participant_index.0);
+ if let Some(collaboration_hub) = &editor.collaboration_hub {
+ // When following someone, render the local selections in their color.
+ if let Some(leader_id) = editor.leader_peer_id {
+ if let Some(collaborator) = collaboration_hub.collaborators(cx).get(&leader_id) {
+ if let Some(participant_index) = collaboration_hub
+ .user_participant_indices(cx)
+ .get(&collaborator.user_id)
+ {
+ if let Some((local_selection_style, _)) = selections.first_mut() {
+ *local_selection_style = cx
+ .theme()
+ .players()
+ .color_for_participant(participant_index.0);
+ }
}
}
}
- }
- let mut remote_selections = HashMap::default();
- for selection in snapshot.remote_selections_in_range(
- &(start_anchor..end_anchor),
- collaboration_hub.as_ref(),
- cx,
- ) {
- let selection_style = if let Some(participant_index) = selection.participant_index {
- cx.theme()
- .players()
- .color_for_participant(participant_index.0)
- } else {
- cx.theme().players().absent()
- };
+ 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::<Point>(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::<Point>(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 (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 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 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 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(
- 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 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_max = point(
- f32::from((scroll_width - text_size.width) / em_width).max(0.0),
- max_row as f32,
- );
+ 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 clamped = editor.scroll_manager.clamp_scroll_left(scroll_max.x);
+ let scroll_max = point(
+ f32::from((scroll_width - text_size.width) / em_width).max(0.0),
+ max_row as f32,
+ );
- let autoscrolled = if autoscroll_horizontally {
- editor.autoscroll_horizontally(
- start_row,
- text_size.width,
- scroll_width,
- em_width,
- &line_layouts,
- cx,
- )
- } else {
- false
- };
+ 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);
- }
+ 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 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(_))
- );
+ 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,
- });
+ 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 mut fold_indicators = cx.with_element_id(Some("gutter_fold_indicators"), |cx| {
- editor.render_fold_indicators(
- fold_statuses,
+ 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::<Pixels>::zero(),
- // max: point(
- // cx.window_size().x * 0.7,
- // (12. * line_height).min((size.y - line_height) / 2.),
- // ),
- // },
- // editor,
- // 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::<Pixels>::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 mut fold_indicators = cx.with_element_id(Some("gutter_fold_indicators"), |cx| {
+ editor.render_fold_indicators(
+ fold_statuses,
+ &style,
+ editor.gutter_hovered,
+ line_height,
+ gutter_margin,
+ cx,
+ )
+ });
- LayoutState {
- mode: editor_mode,
- position_map: Arc::new(PositionMap {
- size: bounds.size,
- scroll_position: point(
- scroll_position.x * em_width,
- scroll_position.y * line_height,
- ),
- scroll_max,
- line_layouts,
- line_height,
- em_width,
- em_advance,
- snapshot,
- }),
- visible_anchor_range: start_anchor..end_anchor,
- visible_display_row_range: start_row..end_row,
- wrap_guides,
- gutter_size,
- gutter_padding,
- text_size,
- scrollbar_row_range,
- show_scrollbars,
- is_singleton,
- max_row,
- gutter_margin,
- active_rows,
- highlighted_rows,
- highlighted_ranges,
- line_numbers,
- display_hunks,
- blocks,
- selections,
- context_menu,
- code_actions_indicator,
- fold_indicators,
- tab_invisible,
- space_invisible,
- // hover_popovers: hover,
- }
+ let invisible_symbol_font_size = font_size / 2.;
+ let tab_invisible = cx
+ .text_system()
+ .shape_line(
+ "→".into(),
+ invisible_symbol_font_size,
+ &[TextRun {
+ len: "→".len(),
+ font: self.style.text.font(),
+ color: cx.theme().colors().editor_invisible,
+ background_color: None,
+ underline: None,
+ }],
+ )
+ .unwrap();
+ let space_invisible = cx
+ .text_system()
+ .shape_line(
+ "•".into(),
+ invisible_symbol_font_size,
+ &[TextRun {
+ len: "•".len(),
+ font: self.style.text.font(),
+ color: cx.theme().colors().editor_invisible,
+ background_color: None,
+ underline: None,
+ }],
+ )
+ .unwrap();
+
+ LayoutState {
+ mode: editor_mode,
+ position_map: Arc::new(PositionMap {
+ size: bounds.size,
+ scroll_position: point(
+ scroll_position.x * em_width,
+ scroll_position.y * line_height,
+ ),
+ scroll_max,
+ line_layouts,
+ line_height,
+ em_width,
+ em_advance,
+ snapshot,
+ }),
+ visible_anchor_range: start_anchor..end_anchor,
+ visible_display_row_range: start_row..end_row,
+ wrap_guides,
+ gutter_size,
+ gutter_padding,
+ text_size,
+ scrollbar_row_range,
+ show_scrollbars,
+ is_singleton,
+ max_row,
+ gutter_margin,
+ active_rows,
+ highlighted_rows,
+ highlighted_ranges,
+ line_numbers,
+ display_hunks,
+ blocks,
+ selections,
+ context_menu,
+ code_actions_indicator,
+ fold_indicators,
+ tab_invisible,
+ space_invisible,
+ hover_popovers: hover,
+ }
+ })
}
#[allow(clippy::too_many_arguments)]
fn layout_blocks(
- &mut self,
+ &self,
rows: Range<u32>,
snapshot: &EditorSnapshot,
editor_width: Pixels,
@@ -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<Editor>) {
+ 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<Editor>) {
-// 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<DisplayPoint>, cx: &mut ViewContext<Editor>) {
@@ -74,64 +75,63 @@ pub fn find_hovered_hint_part(
}
pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut ViewContext<Editor>) {
- todo!()
- // if EditorSettings::get_global(cx).hover_popover_enabled {
- // if editor.pending_rename.is_some() {
- // 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);
- // }
-
- // 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 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::<HoverState>(
- // vec![inlay_hover.range],
- // |theme| theme.editor.hover_popover.highlight,
- // cx,
- // );
- // this.hover_state.info_popover = Some(hover_popover);
- // cx.notify();
- // })?;
-
- // anyhow::Ok(())
- // }
- // .log_err()
- // });
-
- // editor.hover_state.info_task = Some(task);
- // }
+ if EditorSettings::get_global(cx).hover_popover_enabled {
+ if editor.pending_rename.is_some() {
+ 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);
+ }
+
+ 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 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::<HoverState>(
+ 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()
+ });
+
+ 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<u32>,
+ max_size: Size<Pixels>,
workspace: Option<WeakView<Workspace>>,
cx: &mut ViewContext<Editor>,
) -> Option<(DisplayPoint, Vec<AnyElement>)> {
- 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<WeakView<Workspace>>,
-// cx: &mut ViewContext<Editor>,
-// ) -> AnyElement<Editor> {
-// MouseEventHandler::new::<InfoPopover, _>(0, cx, |_, cx| {
-// Flex::column()
-// .scrollable::<HoverBlock>(0, None, cx)
-// .with_child(crate::render_parsed_markdown::<HoverBlock>(
-// &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<Pixels>,
+ workspace: Option<WeakView<Workspace>>,
+ cx: &mut ViewContext<Editor>,
+ ) -> 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<Editor>) -> AnyElement {
- todo!()
- // enum PrimaryDiagnostic {}
-
- // 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 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::<DiagnosticPopover, _>(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::<PrimaryDiagnostic>(
- // 0,
- // "Go To Diagnostic".to_string(),
- // Some(Box::new(crate::GoToDiagnostic)),
- // tooltip_style,
- // cx,
- // )
- // .into_any()
+ pub fn render(
+ &self,
+ style: &EditorStyle,
+ max_size: Size<Pixels>,
+ cx: &mut ViewContext<Editor>,
+ ) -> 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 container_bg = crate::diagnostic_style(
+ self.local_diagnostic.diagnostic.severity,
+ true,
+ &style.diagnostic_style,
+ );
+
+ 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::<lsp::request::HoverRequest, _, _>(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::<lsp::request::HoverRequest, _, _>(|_, _| 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::<lsp::request::HoverRequest, _, _>(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::<lsp::request::HoverRequest, _, _>(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::<lsp::request::HoverRequest, _, _>(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::<lsp::request::HoverRequest, _, _>(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<HoverBlock>,
-// expected_marked_text: String,
-// expected_styles: Vec<HighlightStyle>,
-// }
-
-// 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 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
-// - 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::<Vec<_>>();
-// 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>(T);
-
-// fn main() {
-// let variableˇ = TestNewType(TestStruct);
-// }
-// "});
-
-// let hint_start_offset = cx.ranges(indoc! {"
-// struct TestStruct;
-
-// // ==================
-
-// struct TestNewType<T>(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>(T);
-
-// fn main() {
-// let variable = TestNewType(TestStruct);
-// }
-// "});
-// let struct_target_range = cx.lsp_range(indoc! {"
-// struct «TestStruct»;
-
-// // ==================
-
-// struct TestNewType<T>(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<TestStruct>";
-// let closure_uri = uri.clone();
-// cx.lsp
-// .handle_request::<lsp::request::InlayHintRequest, _, _>(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>(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::<lsp::request::InlayHintResolveRequest, _, _>(
-// 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<TestStruct>`
-// 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::<lsp::request::HoverRequest, _, _>(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::<lsp::request::HoverRequest, _, _>(|_, _| 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::<lsp::request::HoverRequest, _, _>(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::<lsp::request::HoverRequest, _, _>(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::<lsp::request::HoverRequest, _, _>(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::<lsp::request::HoverRequest, _, _>(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<HoverBlock>,
+ expected_marked_text: String,
+ expected_styles: Vec<HighlightStyle>,
+ }
+
+ 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 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
+ - 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::<Vec<_>>();
+ 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>(T);
+
+ fn main() {
+ let variableˇ = TestNewType(TestStruct);
+ }
+ "});
+
+ let hint_start_offset = cx.ranges(indoc! {"
+ struct TestStruct;
+
+ // ==================
+
+ struct TestNewType<T>(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>(T);
+
+ fn main() {
+ let variable = TestNewType(TestStruct);
+ }
+ "});
+ let struct_target_range = cx.lsp_range(indoc! {"
+ struct «TestStruct»;
+
+ // ==================
+
+ struct TestNewType<T>(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<TestStruct>";
+ let closure_uri = uri.clone();
+ cx.lsp
+ .handle_request::<lsp::request::InlayHintRequest, _, _>(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>(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::<lsp::request::InlayHintResolveRequest, _, _>(
+ 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<TestStruct>`
+ 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"
+ );
+ });
+ }
+}
@@ -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<Point> {
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(),
@@ -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),
),
)
})),
@@ -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<Manager> for FileFinder {}
+impl EventEmitter<DismissEvent> 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<Picker<FileFinderDelegate>>) {
self.file_finder
- .update(cx, |_, cx| cx.emit(Manager::Dismiss))
+ .update(cx, |_, cx| cx.emit(DismissEvent::Dismiss))
.log_err();
}
@@ -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<Item = Range<usize>> {
+ 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()
@@ -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<Manager> for GoToLine {}
+impl EventEmitter<DismissEvent> for GoToLine {}
impl GoToLine {
- fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
- workspace.register_action(|workspace, _: &Toggle, cx| {
- let Some(editor) = workspace
- .active_item(cx)
- .and_then(|active_item| active_item.downcast::<Editor>())
- else {
+ fn register(editor: &mut Editor, cx: &mut ViewContext<Editor>) {
+ 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<Self>) {
- cx.emit(Manager::Dismiss);
+ cx.emit(DismissEvent::Dismiss);
}
fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
@@ -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)),
),
)
}
@@ -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<Fut>(
+ &mut self,
+ mut on_quit: impl FnMut(&mut AppContext) -> Fut + 'static,
+ ) -> Subscription
+ where
+ Fut: 'static + Future<Output = ()>,
+ {
+ 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"))?
@@ -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))
})
}
}
@@ -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()
}
@@ -12,15 +12,15 @@ pub trait Render: 'static + Sized {
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element;
}
-pub trait RenderOnce: Sized {
+pub trait IntoElement: Sized {
type Element: Element + 'static;
fn element_id(&self) -> Option<ElementId>;
- 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<T, R>(
@@ -33,7 +33,7 @@ pub trait RenderOnce: Sized {
where
T: Clone + Default + Debug + Into<AvailableSpace>,
{
- 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<U>(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<C> {
+pub struct Component<C> {
component: Option<C>,
}
-pub struct CompositeElementState<C: Component> {
- rendered_element: Option<<C::Rendered as RenderOnce>::Element>,
- rendered_element_state: <<C::Rendered as RenderOnce>::Element as Element>::State,
+pub struct CompositeElementState<C: RenderOnce> {
+ rendered_element: Option<<C::Rendered as IntoElement>::Element>,
+ rendered_element_state: <<C::Rendered as IntoElement>::Element as Element>::State,
}
-impl<C> CompositeElement<C> {
+impl<C> Component<C> {
pub fn new(component: C) -> Self {
- CompositeElement {
+ Component {
component: Some(component),
}
}
}
-impl<C: Component> Element for CompositeElement<C> {
+impl<C: RenderOnce> Element for Component<C> {
type State = CompositeElementState<C>;
fn layout(
@@ -130,7 +130,7 @@ impl<C: Component> Element for CompositeElement<C> {
state: Option<Self::State>,
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<C: Component> Element for CompositeElement<C> {
}
}
-impl<C: Component> RenderOnce for CompositeElement<C> {
+impl<C: RenderOnce> IntoElement for Component<C> {
type Element = Self;
fn element_id(&self) -> Option<ElementId> {
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<Item = impl RenderOnce>) -> Self
+ fn children(mut self, children: impl IntoIterator<Item = impl IntoElement>) -> 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<dyn ElementObject>)
}
- pub fn element_id(&self) -> Option<ElementId> {
- 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<ElementId> {
+ 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<ElementId> {
- AnyElement::element_id(self)
+ None
}
- fn render_once(self) -> Self::Element {
+ fn into_element(self) -> Self::Element {
self
}
}
@@ -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<ElementId> {
self.interactivity.element_id.clone()
}
- fn render_once(self) -> Self::Element {
+ fn into_element(self) -> Self::Element {
self
}
}
@@ -1278,7 +1278,7 @@ where
}
}
-impl<E> RenderOnce for Focusable<E>
+impl<E> IntoElement for Focusable<E>
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<E> RenderOnce for Stateful<E>
+impl<E> IntoElement for Stateful<E>
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
}
}
@@ -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<ImageData>),
+}
+
+impl From<SharedString> for ImageSource {
+ fn from(value: SharedString) -> Self {
+ Self::Uri(value)
+ }
+}
+
+impl From<Arc<ImageData>> for ImageSource {
+ fn from(value: Arc<ImageData>) -> Self {
+ Self::Data(value)
+ }
+}
+
pub struct Img {
interactivity: Interactivity,
- uri: Option<SharedString>,
+ source: Option<ImageSource>,
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<SharedString>) -> Self {
- self.uri = Some(uri.into());
+ self.source = Some(ImageSource::from(uri.into()));
+ self
+ }
+ pub fn data(mut self, data: Arc<ImageData>) -> Self {
+ self.source = Some(ImageSource::from(data));
self
}
+ pub fn source(mut self, source: impl Into<ImageSource>) -> 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<crate::ElementId> {
self.interactivity.element_id.clone()
}
- fn render_once(self) -> Self::Element {
+ fn into_element(self) -> Self::Element {
self
}
}
@@ -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<crate::ElementId> {
None
}
- fn render_once(self) -> Self::Element {
+ fn into_element(self) -> Self::Element {
self
}
}
@@ -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<ElementId> {
self.interactivity.element_id.clone()
}
- fn render_once(self) -> Self::Element {
+ fn into_element(self) -> Self::Element {
self
}
}
@@ -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<ElementId> {
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<ElementId> {
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<Vec<TextRun>>,
}
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<TextRun>) -> Self {
+ pub fn new(text: impl Into<SharedString>) -> Self {
StyledText {
- text,
- runs: Some(runs),
+ text: text.into(),
+ runs: None,
}
}
+
+ pub fn with_runs(mut self, runs: Vec<TextRun>) -> 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<crate::ElementId> {
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<Pixels>, position: Point<Pixels>) -> Option<usize> {
+ 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<Box<dyn Fn(InteractiveTextClickEvent, &mut WindowContext<'_>)>>,
}
-struct InteractiveTextState {
+struct InteractiveTextClickEvent {
+ mouse_down_index: usize,
+ mouse_up_index: usize,
+}
+
+pub struct InteractiveTextState {
text_state: TextState,
- clicked_range_ixs: Rc<Cell<SmallVec<[usize; 1]>>>,
+ mouse_down_index: Rc<Cell<Option<usize>>>,
+}
+
+impl InteractiveText {
+ pub fn new(id: impl Into<ElementId>, text: StyledText) -> Self {
+ Self {
+ element_id: id.into(),
+ text,
+ click_listener: None,
+ }
+ }
+
+ pub fn on_click(
+ mut self,
+ ranges: Vec<Range<usize>>,
+ 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<Pixels>, 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<ElementId> {
Some(self.element_id.clone())
}
- fn render_once(self) -> Self::Element {
+ fn into_element(self) -> Self::Element {
self
}
}
@@ -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<I, R, V>(
view: View<V>,
id: I,
@@ -18,30 +18,30 @@ pub fn uniform_list<I, R, V>(
) -> UniformList
where
I: Into<ElementId>,
- 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<crate::ElementId> {
Some(self.id.clone())
}
- fn render_once(self) -> Self::Element {
+ fn into_element(self) -> Self::Element {
self
}
}
@@ -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<S, R, V, E>
where
R: Fn(&mut V, &mut ViewContext<V>) -> 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(),
),
)
}
@@ -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();
}
})
@@ -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,
};
@@ -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<Hsla>,
pub underline: Option<UnderlineStyle>,
+ 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<HighlightStyle>) -> 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<Hsla>,
pub font_weight: Option<FontWeight>,
pub font_style: Option<FontStyle>,
+ pub background_color: Option<Hsla>,
pub underline: Option<UnderlineStyle>,
pub fade_out: Option<f32>,
}
@@ -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<Hsla> for HighlightStyle {
}
}
+impl From<FontWeight> for HighlightStyle {
+ fn from(font_weight: FontWeight) -> Self {
+ Self {
+ font_weight: Some(font_weight),
+ ..Default::default()
+ }
+ }
+}
+
+impl From<FontStyle> for HighlightStyle {
+ fn from(font_style: FontStyle) -> Self {
+ Self {
+ font_style: Some(font_style),
+ ..Default::default()
+ }
+ }
+}
+
impl From<Rgba> for HighlightStyle {
fn from(color: Rgba) -> Self {
Self {
@@ -489,3 +532,140 @@ impl From<Rgba> for HighlightStyle {
}
}
}
+
+pub fn combine_highlights(
+ a: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
+ b: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
+) -> impl Iterator<Item = (Range<usize>, 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::<Vec<_>>(),
+ [
+ (
+ 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()
+ }
+ )
+ ]
+ );
+ }
+}
@@ -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<Hsla>) -> Self {
+ self.text_style()
+ .get_or_insert_with(Default::default)
+ .background_color = Some(bg.into());
+ self
+ }
+
fn text_size(mut self, size: impl Into<AbsoluteLength>) -> Self {
self.text_style()
.get_or_insert_with(Default::default)
@@ -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();
}
@@ -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<Hsla>,
pub underline: Option<UnderlineStyle>,
}
@@ -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<Pixels>,
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<Pixels>, UnderlineStyle)> = None;
+ let mut current_background: Option<(Point<Pixels>, 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<Pixels>, Hsla)> = None;
let mut finished_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
if glyph.index >= run_end {
if let Some(style_run) = decoration_runs.next() {
+ if let Some((_, 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(())
@@ -198,6 +198,41 @@ impl WrappedLineLayout {
pub fn runs(&self) -> &[ShapedRun] {
&self.unwrapped_layout.runs
}
+
+ pub fn index_for_position(
+ &self,
+ position: Point<Pixels>,
+ line_height: Pixels,
+ ) -> Option<usize> {
+ 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 {
@@ -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<V: 'static + Render> RenderOnce for View<V> {
+impl<V: 'static + Render> IntoElement for View<V> {
type Element = View<V>;
fn element_id(&self) -> Option<ElementId> {
- 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<ElementId> {
- 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<V: 'static + Render>(
view: &AnyView,
cx: &mut WindowContext,
) -> (LayoutId, AnyElement) {
- cx.with_element_id(Some(view.model.entity_id), |cx| {
- let view = view.clone().downcast::<V>().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::<V>().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<V: 'static + Render>(
- 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);
}
}
@@ -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<Manager> {}
+pub trait ManagedView: FocusableView + EventEmitter<DismissEvent> {}
-impl<M: FocusableView + EventEmitter<Manager>> ManagedView for M {}
+impl<M: FocusableView + EventEmitter<DismissEvent>> ManagedView for M {}
-pub enum Manager {
+pub enum DismissEvent {
Dismiss,
}
@@ -230,9 +230,15 @@ pub struct Window {
pub(crate) focus: Option<FocusId>,
}
+pub(crate) struct ElementStateBox {
+ inner: Box<dyn Any>,
+ #[cfg(debug_assertions)]
+ type_name: &'static str,
+}
+
// #[derive(Default)]
pub(crate) struct Frame {
- pub(crate) element_states: HashMap<GlobalElementId, Box<dyn Any>>,
+ pub(crate) element_states: HashMap<GlobalElementId, ElementStateBox>,
mouse_listeners: HashMap<TypeId, Vec<(StackingOrder, AnyMouseListener)>>,
pub(crate) dispatch_tree: DispatchTree,
pub(crate) focus_listeners: Vec<AnyFocusListener>,
@@ -875,7 +881,7 @@ impl<'a> WindowContext<'a> {
origin: Point<Pixels>,
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<Window> + BorrowMut<AppContext> {
}
}
+ /// Invoke the given function with the content mask reset to that
+ /// of the window.
+ fn break_content_mask<R>(&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<R>(
@@ -1815,10 +1845,37 @@ pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> {
.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::<Option<S>>()
- .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::<S>(),
+ type_name
+ )
+ }
+
+ #[cfg(not(debug_assertions))]
+ {
+ anyhow!(
+ "invalid element state type for id, requested_type {:?}",
+ std::any::type_name::<S>(),
+ )
+ }
+ })
+ .unwrap();
+
+ // Actual: Option<AnyElement> <- View
+ // Requested: () <- AnyElemet
let state = state_box
.take()
.expect("element state is already on the stack");
@@ -1827,14 +1884,27 @@ pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> {
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::<S>()
+ }
+
+ );
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<E>(
@@ -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<SharedString> for ElementId {
type Error = anyhow::Error;
@@ -2611,12 +2687,6 @@ impl TryInto<SharedString> for ElementId {
}
}
-impl From<EntityId> for ElementId {
- fn from(id: EntityId) -> Self {
- ElementId::View(id)
- }
-}
-
impl From<usize> for ElementId {
fn from(id: usize) -> Self {
ElementId::Integer(id)
@@ -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<proc_macro2::Ident> {
- 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::<syn::Ident>()
- .expect("Failed to parse view_type"),
- );
- }
- }
- }
- None
- })
- } else {
- None
- }
-}
@@ -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<Self>;
+ type Element = gpui::Component<Self>;
fn element_id(&self) -> Option<ElementId> {
None
}
- fn render_once(self) -> Self::Element {
- gpui::CompositeElement::new(self)
+ fn into_element(self) -> Self::Element {
+ gpui::Component::new(self)
}
}
};
@@ -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]
@@ -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);
@@ -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));
}
}
@@ -1300,7 +1300,7 @@ fn assert_capture_ranges(
.collect::<Vec<_>>();
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());
}
}
@@ -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<LanguageRegistry>,
+ language: Option<Arc<Language>>,
+) -> 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<Anchor>,
pub new_text: String,
pub label: CodeLabel,
pub server_id: LanguageServerId,
+ pub documentation: Option<Documentation>,
pub lsp_completion: lsp::CompletionItem,
}
@@ -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);
@@ -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));
}
}
@@ -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,
})
@@ -1300,7 +1300,7 @@ fn assert_capture_ranges(
.collect::<Vec<_>>();
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());
}
}
@@ -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<D: PickerDelegate> {
pub delegate: D,
@@ -15,7 +15,7 @@ pub struct Picker<D: PickerDelegate> {
}
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<D: PickerDelegate> Picker<D> {
}
fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
+ dbg!("canceling!");
self.delegate.dismissed(cx);
}
@@ -250,7 +251,7 @@ impl<D: PickerDelegate> Render for Picker<D> {
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)),
),
)
})
@@ -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<Path>,
},
}
@@ -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)
})
@@ -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<Path>], fs: &dyn Fs) {
+ fn reload_repositories(&mut self, dot_git_dirs_to_reload: &HashSet<PathBuf>, 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<Path> =
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(
@@ -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::<SettingsStore, _, _>(|store, cx| {
+ store.update_user_settings::<ProjectSettings>(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);
@@ -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<lsp::CompletionResponse>,
- _: Model<Project>,
+ project: Model<Project>,
buffer: Model<Buffer>,
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,
}
@@ -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<Path>,
},
}
@@ -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)
})?
@@ -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<Path>], fs: &dyn Fs) {
+ fn reload_repositories(&mut self, dot_git_dirs_to_reload: &HashSet<PathBuf>, 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<Path> =
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(
@@ -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::<SettingsStore, _>(|store, cx| {
+ store.update_user_settings::<ProjectSettings>(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<Client> {
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]
@@ -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<Self>,
) {
- 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<Self>) {
+ dbg!("odd");
self.edit_state = None;
self.update_visible_entries(None, cx);
cx.focus(&self.focus_handle);
@@ -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<ProjectSearchBar>| {
let is_active = if let Some(search) = self.active_project_search.as_ref() {
@@ -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<Editor>) {
+ 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::<BufferSearchBar>() {
search_bar.update(cx, |this, cx| {
@@ -316,11 +327,16 @@ impl BufferSearchBar {
});
});
fn register_action<A: Action>(
- workspace: &mut Workspace,
+ editor: &mut Editor,
+ handle: WeakView<Editor>,
update: fn(&mut BufferSearchBar, &A, &mut ViewContext<BufferSearchBar>),
) {
- 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::<BufferSearchBar>() {
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>) -> 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();
@@ -85,6 +85,7 @@ pub fn init(cx: &mut AppContext) {
cx.capture_action(ProjectSearchView::replace_next);
add_toggle_option_action::<ToggleCaseSensitive>(SearchOptions::CASE_SENSITIVE, cx);
add_toggle_option_action::<ToggleWholeWord>(SearchOptions::WHOLE_WORD, cx);
+ add_toggle_option_action::<ToggleIncludeIgnored>(SearchOptions::INCLUDE_IGNORED, cx);
add_toggle_filters_action::<ToggleFilters>(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<ProjectSearchBar>| {
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()
@@ -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());
@@ -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
@@ -1659,13 +1659,13 @@ fn elixir_lang() -> Arc<Language> {
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(),
@@ -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" }
@@ -0,0 +1,3 @@
+mod story;
+
+pub use story::*;
@@ -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<SharedString>) -> impl Element {
+ div()
+ .text_xl()
+ .text_color(hsla(0. / 360., 0. / 100., 0. / 100., 1.))
+ .child(title.into())
+ }
+
+ pub fn title_for<T>() -> impl Element {
+ Self::title(std::any::type_name::<T>())
+ }
+
+ pub fn label(label: impl Into<SharedString>) -> impl Element {
+ div()
+ .mt_4()
+ .mb_2()
+ .text_xs()
+ .text_color(hsla(0. / 360., 0. / 100., 0. / 100., 1.))
+ .child(label.into())
+ }
+}
@@ -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" }
@@ -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::*;
@@ -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>) -> 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))
- })),
- )
- })),
- )
- }
-}
@@ -26,7 +26,7 @@ impl FocusStory {
}
}
-impl Render<Self> for FocusStory {
+impl Render for FocusStory {
type Element = Focusable<Stateful<Div>>;
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> Self::Element {
@@ -52,10 +52,8 @@ impl Render<Self> 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))
@@ -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::<Vec<_>>();
- 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.
@@ -36,7 +36,7 @@ impl Delegate {
}
impl PickerDelegate for Delegate {
- type ListItem = Div<Picker<Self>>;
+ type ListItem = Div;
fn match_count(&self) -> usize {
self.candidates.len()
@@ -205,8 +205,8 @@ impl PickerStory {
}
}
-impl Render<Self> for PickerStory {
- type Element = Div<Self>;
+impl Render for PickerStory {
+ type Element = Div;
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> Self::Element {
div()
@@ -10,8 +10,8 @@ impl ScrollStory {
}
}
-impl Render<Self> for ScrollStory {
- type Element = Stateful<Self, Div<Self>>;
+impl Render for ScrollStory {
+ type Element = Stateful<Div>;
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> Self::Element {
let theme = cx.theme();
@@ -38,7 +38,7 @@ impl Render<Self> 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| {
@@ -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}");
+ })
+ )
}
}
@@ -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<Self> for ZIndexStory {
- type Element = Div<Self>;
+impl Render for ZIndexStory {
+ type Element = Div;
fn render(&mut self, cx: &mut ViewContext<Self>) -> 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<V: 'static> Styles for Div<V> {}
+impl Styles for Div {}
-#[derive(RenderOnce)]
+#[derive(IntoElement)]
struct ZIndexExample {
z_index: u32,
}
-impl<V: 'static> Component<V> for ZIndexExample {
- type Rendered = Div<V>;
+impl RenderOnce for ZIndexExample {
+ type Rendered = Div;
- fn render(self, view: &mut V, cx: &mut ViewContext<V>) -> Self::Rendered {
+ fn render(self, cx: &mut WindowContext) -> Self::Rendered {
div()
.relative()
.size_full()
@@ -1 +0,0 @@
-pub use ui::Story;
@@ -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(),
}
@@ -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::<ThemeRegistry>();
let mut theme_settings = ThemeSettings::get_global(cx).clone();
theme_settings.active_theme = theme_registry.get(&theme_name).unwrap();
ThemeSettings::override_global(theme_settings, cx);
- ui::settings::init(cx);
language::init(cx);
editor::init(cx);
@@ -105,8 +101,8 @@ impl StoryWrapper {
}
}
-impl Render<Self> for StoryWrapper {
- type Element = Div<Self>;
+impl Render for StoryWrapper {
+ type Element = Div;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
div()
@@ -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"}
@@ -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<Cow<[u8]>> {
- todo!();
- }
-
- fn list(&self, _path: &str) -> Result<Vec<SharedString>> {
- Ok(vec![])
- }
-}
-
-fn main() {
- let asset_source = Arc::new(Assets);
- gpui::App::production(asset_source).run(move |cx| {
- let mut store = SettingsStore::default();
- store
- .set_default_settings(default_settings().as_ref(), cx)
- .unwrap();
- cx.set_global(store);
- ui::settings::init(cx);
- theme::init(theme::LoadThemes::JustBase, cx);
-
- cx.open_window(
- WindowOptions {
- bounds: WindowBounds::Fixed(Bounds {
- origin: Default::default(),
- size: size(px(1500.), px(780.)).into(),
- }),
- ..Default::default()
- },
- move |cx| {
- let ui_font_size = ThemeSettings::get_global(cx).ui_font_size;
- cx.set_rem_size(ui_font_size);
-
- cx.build_view(|cx| TestView {
- story: cx.build_view(|_| ContextMenuStory).into(),
- })
- },
- );
-
- cx.activate(true);
- })
-}
-
-struct TestView {
- #[allow(unused)]
- story: AnyView,
-}
-
-impl Render<Self> for TestView {
- type Element = Div<Self>;
-
- fn render(&mut self, _cx: &mut ViewContext<Self>) -> 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.",
- ))),
- )
- }
-}
@@ -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<AnyElement<TerminalView>>,
+// hyperlink_tooltip: Option<AnyElement>,
// gutter: f32,
// }
@@ -183,9 +181,9 @@
// grid: &Vec<IndexedCell>,
// text_style: &TextStyle,
// terminal_theme: &TerminalStyle,
-// text_layout_cache: &TextLayoutCache,
-// font_cache: &FontCache,
+// text_system: &TextSystem,
// hyperlink: Option<(HighlightStyle, &RangeInclusive<AlacPoint>)>,
+// cx: &mut WindowContext<'_>,
// ) -> (Vec<LayoutCell>, Vec<LayoutRect>) {
// 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<AlacPoint>)>,
-// ) -> 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<E>(
-// connection: WeakModel<Terminal>,
-// origin: Point<Pixels>,
-// f: impl Fn(&mut Terminal, Point<Pixels>, E, &mut ModelContext<Terminal>),
-// ) -> impl Fn(E, &mut TerminalView, &mut EventContext<TerminalView>) {
-// move |event, _: &mut TerminalView, cx| {
-// cx.focus_parent();
-// if let Some(conn_handle) = connection.upgrade() {
-// conn_handle.update(cx, |terminal, cx| {
-// f(terminal, origin, event, cx);
-
-// cx.notify();
-// })
-// }
-// }
-// }
+// // todo!()
+// // fn generic_button_handler<E>(
+// // connection: WeakModel<Terminal>,
+// // origin: Point<Pixels>,
+// // f: impl Fn(&mut Terminal, Point<Pixels>, E, &mut ModelContext<Terminal>),
+// // ) -> impl Fn(E, &mut TerminalView, &mut EventContext<TerminalView>) {
+// // move |event, _: &mut TerminalView, cx| {
+// // cx.focus_parent();
+// // if let Some(conn_handle) = connection.upgrade() {
+// // conn_handle.update(cx, |terminal, cx| {
+// // f(terminal, origin, event, cx);
+
+// // cx.notify();
+// // })
+// // }
+// // }
+// // }
// fn attach_mouse_handlers(
// &self,
@@ -389,144 +391,144 @@
// mode: TermMode,
// cx: &mut ViewContext<TerminalView>,
// ) {
-// let connection = self.terminal;
-
-// let mut region = MouseRegion::new::<Self>(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);
-
-// 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();
-// })
-// }
-// });
-
-// // 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);
+// // todo!()
+// // let connection = self.terminal;
+
+// // let mut region = MouseRegion::new::<Self>(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);
+
+// // 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();
+// // })
+// // }
+// // });
+
+// // // 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);
// }
// }
-// impl Element<TerminalView> for TerminalElement {
-// type ElementState = LayoutState;
+// impl Element for TerminalElement {
+// type State = LayoutState;
// fn layout(
// &mut self,
-// view_state: &mut TerminalView,
-// element_state: Option<Self::ElementState>,
-// cx: &mut ViewContext<TerminalView>,
-// ) -> (LayoutId, Self::ElementState) {
+// element_state: Option<Self::State>,
+// 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<Pixels>,
-// view_state: &mut TerminalView,
-// element_state: &mut Self::ElementState,
-// cx: &mut ViewContext<TerminalView>,
+// element_state: &mut Self::State,
+// cx: &mut WindowContext<'_>,
// ) {
-// let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
-
-// //Setup element stuff
-// let clip_bounds = Some(visible_bounds);
-
-// 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);
-
-// 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(),
-// });
-
-// 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 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);
-// })
-// }
-// }
-
-// if let Some(element) = &mut element_state.hyperlink_tooltip {
-// element.paint(origin, visible_bounds, view_state, cx)
-// }
-// });
-// }
-
-// fn element_id(&self) -> Option<ElementId> {
-// todo!()
+// // todo!()
+// // let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
+
+// // //Setup element stuff
+// // let clip_bounds = Some(visible_bounds);
+
+// // 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);
+
+// // 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(),
+// // });
+
+// // 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 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);
+// // })
+// // }
+// // }
+
+// // 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<Pixels>,
-// // _: &Self::ElementState,
+// // _: &Self::State,
// // _: &Self::PaintState,
// // _: &TerminalView,
// // _: &gpui::ViewContext<TerminalView>,
@@ -837,7 +842,7 @@
// // _: Range<usize>,
// // bounds: Bounds<Pixels>,
// // _: Bounds<Pixels>,
-// // layout: &Self::ElementState,
+// // layout: &Self::State,
// // _: &Self::PaintState,
// // _: &TerminalView,
// // _: &gpui::ViewContext<TerminalView>,
@@ -855,10 +860,16 @@
// // }
// }
-// impl Component<TerminalView> for TerminalElement {
-// fn render(self) -> AnyElement<TerminalView> {
+// impl IntoElement for TerminalElement {
+// type Element = Self;
+
+// fn element_id(&self) -> Option<ElementId> {
// 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!()
+// }
@@ -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>,
) {
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!()
@@ -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" }
@@ -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),
@@ -184,7 +184,7 @@ impl settings::Settings for ThemeSettings {
) -> schemars::schema::RootSchema {
let mut root_schema = generator.root_schema_for::<ThemeSettingsContent>();
let theme_names = cx
- .global::<Arc<ThemeRegistry>>()
+ .global::<ThemeRegistry>()
.list_names(params.staff_mode)
.map(|theme_name| Value::String(theme_name.to_string()))
.collect();
@@ -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<T>(cx: &mut WindowContext) -> impl Element {
- Self::title(cx, std::any::type_name::<T>().into())
- }
-
- pub fn label(cx: &mut WindowContext, label: impl Into<SharedString>) -> impl Element {
- div()
- .mt_4()
- .mb_2()
- .text_xs()
- .text_color(cx.theme().colors().text)
- .child(label.into())
- }
-}
@@ -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::*;
@@ -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>) -> Self::Element {
- Story::container(cx).child(
- div()
- .flex()
- .flex_col()
- .gap_4()
- .child(Story::title_for::<PlayerColors>(cx))
- .child(Story::label(cx, "Player Colors"))
- .child(
- div()
- .flex()
- .flex_col()
- .gap_1()
- .child(
- div().flex().gap_1().children(
- cx.theme().players().0.clone().iter_mut().map(|player| {
- div().w_8().h_8().rounded_md().bg(player.cursor)
- }),
- ),
- )
- .child(div().flex().gap_1().children(
- cx.theme().players().0.clone().iter_mut().map(|player| {
- div().w_8().h_8().rounded_md().bg(player.background)
- }),
- ))
- .child(div().flex().gap_1().children(
- cx.theme().players().0.clone().iter_mut().map(|player| {
- div().w_8().h_8().rounded_md().bg(player.selection)
- }),
- )),
- )
- .child(Story::label(cx, "Avatar Rings"))
- .child(div().flex().gap_1().children(
- cx.theme().players().0.clone().iter_mut().map(|player| {
- div()
- .my_1()
- .rounded_full()
- .border_2()
- .border_color(player.cursor)
- .child(
- img()
- .rounded_full()
- .uri("https://avatars.githubusercontent.com/u/1714999?v=4")
- .size_6()
- .bg(gpui::red()),
- )
- }),
- ))
- .child(Story::label(cx, "Player Backgrounds"))
- .child(div().flex().gap_1().children(
- cx.theme().players().0.clone().iter_mut().map(|player| {
- div()
- .my_1()
- .rounded_xl()
- .flex()
- .items_center()
- .h_8()
- .py_0p5()
- .px_1p5()
- .bg(player.background)
- .child(
- div().relative().neg_mx_1().rounded_full().z_index(3)
- .border_2()
- .border_color(player.background)
- .size(px(28.))
- .child(
- img()
- .rounded_full()
- .uri("https://avatars.githubusercontent.com/u/1714999?v=4")
- .size(px(24.))
- .bg(gpui::red()),
- ),
- ).child(
- div().relative().neg_mx_1().rounded_full().z_index(2)
- .border_2()
- .border_color(player.background)
- .size(px(28.))
- .child(
- img()
- .rounded_full()
- .uri("https://avatars.githubusercontent.com/u/1714999?v=4")
- .size(px(24.))
- .bg(gpui::red()),
- ),
- ).child(
- div().relative().neg_mx_1().rounded_full().z_index(1)
- .border_2()
- .border_color(player.background)
- .size(px(28.))
- .child(
- img()
- .rounded_full()
- .uri("https://avatars.githubusercontent.com/u/1714999?v=4")
- .size(px(24.))
- .bg(gpui::red()),
- ),
- )
- }),
- ))
- .child(Story::label(cx, "Player Selections"))
- .child(div().flex().flex_col().gap_px().children(
- cx.theme().players().0.clone().iter_mut().map(|player| {
- div()
- .flex()
- .child(
- div()
- .flex()
- .flex_none()
- .rounded_sm()
- .px_0p5()
- .text_color(cx.theme().colors().text)
- .bg(player.selection)
- .child("The brown fox jumped over the lazy dog."),
- )
- .child(div().flex_1())
- }),
- )),
- )
- }
- }
-}
@@ -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>) -> 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))),
+ ),
+ )
+ })),
+ )
+ }
+}
@@ -0,0 +1,5 @@
+mod color;
+mod players;
+
+pub use color::*;
+pub use players::*;
@@ -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>) -> Self::Element {
+ Story::container().child(
+ div()
+ .flex()
+ .flex_col()
+ .gap_4()
+ .child(Story::title_for::<PlayerColors>())
+ .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())
+ }),
+ )),
+ )
+ }
+}
@@ -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::*;
@@ -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"]
@@ -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::*;
@@ -1,13 +1,22 @@
+use std::sync::Arc;
+
use crate::prelude::*;
-use gpui::{img, Img, RenderOnce};
+use gpui::{img, ImageData, ImageSource, Img, IntoElement};
+
+#[derive(Debug, Default, PartialEq, Clone)]
+pub enum Shape {
+ #[default]
+ Circle,
+ RoundedRectangle,
+}
-#[derive(RenderOnce)]
+#[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<SharedString>) -> Self {
+ pub fn uri(src: impl Into<SharedString>) -> Self {
+ Self {
+ src: src.into().into(),
+ shape: Shape::Circle,
+ }
+ }
+ pub fn data(src: Arc<ImageData>) -> 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>) -> Self::Element {
- Story::container(cx)
- .child(Story::title_for::<Avatar>(cx))
- .child(Story::label(cx, "Default"))
- .child(Avatar::new(
- "https://avatars.githubusercontent.com/u/1714999?v=4",
- ))
- .child(Avatar::new(
- "https://avatars.githubusercontent.com/u/326587?v=4",
- ))
- }
- }
-}
@@ -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<Rc<dyn Fn(&MouseDownEvent, &mut WindowContext)>>,
@@ -73,17 +73,17 @@ pub struct Button {
label: SharedString,
variant: ButtonVariant,
width: Option<DefiniteLength>,
- color: Option<TextColor>,
+ color: Option<Color>,
}
-impl Component for Button {
+impl RenderOnce for Button {
type Rendered = gpui::Stateful<Div>;
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<TextColor>) -> Self {
+ pub fn color(mut self, color: Option<Color>) -> Self {
self.color = color;
self
}
- pub fn label_color(&self, color: Option<TextColor>) -> TextColor {
+ pub fn label_color(&self, color: Option<Color>) -> 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<IconElement> {
+ fn render_icon(&self, icon_color: Color) -> Option<IconElement> {
self.icon.map(|i| IconElement::new(i).color(icon_color))
}
}
-#[derive(RenderOnce)]
+#[derive(IntoElement)]
pub struct ButtonGroup {
buttons: Vec<Button>,
}
-impl Component for ButtonGroup {
+impl RenderOnce for ButtonGroup {
type Rendered = Div;
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
@@ -231,171 +231,3 @@ impl ButtonGroup {
Self { buttons }
}
}
-
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use super::*;
- use crate::{h_stack, v_stack, Story, TextColor};
- use gpui::{rems, Div, Render};
- use strum::IntoEnumIterator;
-
- pub struct ButtonStory;
-
- impl Render for ButtonStory {
- type Element = Div;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- let states = InteractionState::iter();
-
- Story::container(cx)
- .child(Story::title_for::<Button>(cx))
- .child(
- div()
- .flex()
- .gap_8()
- .child(
- div()
- .child(Story::label(cx, "Ghost (Default)"))
- .child(h_stack().gap_2().children(states.clone().map(|state| {
- v_stack()
- .gap_1()
- .child(
- Label::new(state.to_string()).color(TextColor::Muted),
- )
- .child(
- Button::new("Label").variant(ButtonVariant::Ghost), // .state(state),
- )
- })))
- .child(Story::label(cx, "Ghost – Left Icon"))
- .child(h_stack().gap_2().children(states.clone().map(|state| {
- v_stack()
- .gap_1()
- .child(
- Label::new(state.to_string()).color(TextColor::Muted),
- )
- .child(
- Button::new("Label")
- .variant(ButtonVariant::Ghost)
- .icon(Icon::Plus)
- .icon_position(IconPosition::Left), // .state(state),
- )
- })))
- .child(Story::label(cx, "Ghost – Right Icon"))
- .child(h_stack().gap_2().children(states.clone().map(|state| {
- v_stack()
- .gap_1()
- .child(
- Label::new(state.to_string()).color(TextColor::Muted),
- )
- .child(
- Button::new("Label")
- .variant(ButtonVariant::Ghost)
- .icon(Icon::Plus)
- .icon_position(IconPosition::Right), // .state(state),
- )
- }))),
- )
- .child(
- div()
- .child(Story::label(cx, "Filled"))
- .child(h_stack().gap_2().children(states.clone().map(|state| {
- v_stack()
- .gap_1()
- .child(
- Label::new(state.to_string()).color(TextColor::Muted),
- )
- .child(
- Button::new("Label").variant(ButtonVariant::Filled), // .state(state),
- )
- })))
- .child(Story::label(cx, "Filled – Left Button"))
- .child(h_stack().gap_2().children(states.clone().map(|state| {
- v_stack()
- .gap_1()
- .child(
- Label::new(state.to_string()).color(TextColor::Muted),
- )
- .child(
- Button::new("Label")
- .variant(ButtonVariant::Filled)
- .icon(Icon::Plus)
- .icon_position(IconPosition::Left), // .state(state),
- )
- })))
- .child(Story::label(cx, "Filled – Right Button"))
- .child(h_stack().gap_2().children(states.clone().map(|state| {
- v_stack()
- .gap_1()
- .child(
- Label::new(state.to_string()).color(TextColor::Muted),
- )
- .child(
- Button::new("Label")
- .variant(ButtonVariant::Filled)
- .icon(Icon::Plus)
- .icon_position(IconPosition::Right), // .state(state),
- )
- }))),
- )
- .child(
- div()
- .child(Story::label(cx, "Fixed With"))
- .child(h_stack().gap_2().children(states.clone().map(|state| {
- v_stack()
- .gap_1()
- .child(
- Label::new(state.to_string()).color(TextColor::Muted),
- )
- .child(
- Button::new("Label")
- .variant(ButtonVariant::Filled)
- // .state(state)
- .width(Some(rems(6.).into())),
- )
- })))
- .child(Story::label(cx, "Fixed With – Left Icon"))
- .child(h_stack().gap_2().children(states.clone().map(|state| {
- v_stack()
- .gap_1()
- .child(
- Label::new(state.to_string()).color(TextColor::Muted),
- )
- .child(
- Button::new("Label")
- .variant(ButtonVariant::Filled)
- // .state(state)
- .icon(Icon::Plus)
- .icon_position(IconPosition::Left)
- .width(Some(rems(6.).into())),
- )
- })))
- .child(Story::label(cx, "Fixed With – Right Icon"))
- .child(h_stack().gap_2().children(states.clone().map(|state| {
- v_stack()
- .gap_1()
- .child(
- Label::new(state.to_string()).color(TextColor::Muted),
- )
- .child(
- Button::new("Label")
- .variant(ButtonVariant::Filled)
- // .state(state)
- .icon(Icon::Plus)
- .icon_position(IconPosition::Right)
- .width(Some(rems(6.).into())),
- )
- }))),
- ),
- )
- .child(Story::label(cx, "Button with `on_click`"))
- .child(
- Button::new("Label")
- .variant(ButtonVariant::Ghost)
- .on_click(|_, cx| println!("Button clicked.")),
- )
- }
- }
-}
@@ -1,8 +1,8 @@
-use gpui::{div, prelude::*, Div, Element, ElementId, RenderOnce, Styled, WindowContext};
+use gpui::{div, prelude::*, Div, Element, ElementId, IntoElement, Styled, WindowContext};
use theme2::ActiveTheme;
-use crate::{Icon, IconElement, Selection, TextColor};
+use crate::{Color, Icon, IconElement, Selection};
pub type CheckHandler = Box<dyn Fn(&Selection, &mut WindowContext) + 'static>;
@@ -11,7 +11,7 @@ pub type CheckHandler = Box<dyn Fn(&Selection, &mut WindowContext) + 'static>;
/// Checkboxes are used for multiple choices, not for mutually exclusive choices.
/// Each checkbox works independently from other checkboxes in the list,
/// therefore checking an additional box does not affect any other selections.
-#[derive(RenderOnce)]
+#[derive(IntoElement)]
pub struct Checkbox {
id: ElementId,
checked: Selection,
@@ -19,7 +19,7 @@ pub struct Checkbox {
on_click: Option<CheckHandler>,
}
-impl Component for Checkbox {
+impl RenderOnce for Checkbox {
type Rendered = gpui::Stateful<Div>;
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
@@ -34,9 +34,9 @@ impl Component for Checkbox {
.color(
// If the checkbox is disabled we change the color of the icon.
if self.disabled {
- TextColor::Disabled
+ Color::Disabled
} else {
- TextColor::Selected
+ Color::Selected
},
),
)
@@ -49,9 +49,9 @@ impl Component for Checkbox {
.color(
// If the checkbox is disabled we change the color of the icon.
if self.disabled {
- TextColor::Disabled
+ Color::Disabled
} else {
- TextColor::Selected
+ Color::Selected
},
),
)
@@ -176,9 +176,9 @@ impl Checkbox {
.color(
// If the checkbox is disabled we change the color of the icon.
if self.disabled {
- TextColor::Disabled
+ Color::Disabled
} else {
- TextColor::Selected
+ Color::Selected
},
),
)
@@ -191,9 +191,9 @@ impl Checkbox {
.color(
// If the checkbox is disabled we change the color of the icon.
if self.disabled {
- TextColor::Disabled
+ Color::Disabled
} else {
- TextColor::Selected
+ Color::Selected
},
),
)
@@ -283,63 +283,3 @@ impl Checkbox {
)
}
}
-
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use super::*;
- use crate::{h_stack, Story};
- use gpui::{Div, Render, ViewContext};
-
- pub struct CheckboxStory;
-
- impl Render for CheckboxStory {
- type Element = Div;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- Story::container(cx)
- .child(Story::title_for::<Checkbox>(cx))
- .child(Story::label(cx, "Default"))
- .child(
- h_stack()
- .p_2()
- .gap_2()
- .rounded_md()
- .border()
- .border_color(cx.theme().colors().border)
- .child(Checkbox::new("checkbox-enabled", Selection::Unselected))
- .child(Checkbox::new(
- "checkbox-intermediate",
- Selection::Indeterminate,
- ))
- .child(Checkbox::new("checkbox-selected", Selection::Selected)),
- )
- .child(Story::label(cx, "Disabled"))
- .child(
- h_stack()
- .p_2()
- .gap_2()
- .rounded_md()
- .border()
- .border_color(cx.theme().colors().border)
- .child(
- Checkbox::new("checkbox-disabled", Selection::Unselected)
- .disabled(true),
- )
- .child(
- Checkbox::new(
- "checkbox-disabled-intermediate",
- Selection::Indeterminate,
- )
- .disabled(true),
- )
- .child(
- Checkbox::new("checkbox-disabled-selected", Selection::Selected)
- .disabled(true),
- ),
- )
- }
- }
-}
@@ -1,18 +1,18 @@
use std::cell::RefCell;
use std::rc::Rc;
-use crate::{prelude::*, v_stack, List};
+use crate::{prelude::*, v_stack, Label, List};
use crate::{ListItem, ListSeparator, ListSubHeader};
use gpui::{
- overlay, px, Action, AnchorCorner, AnyElement, AppContext, Bounds, ClickEvent, DispatchPhase,
- Div, EventEmitter, FocusHandle, FocusableView, LayoutId, ManagedView, Manager, MouseButton,
- MouseDownEvent, Pixels, Point, Render, RenderOnce, View, VisualContext,
+ overlay, px, Action, AnchorCorner, AnyElement, AppContext, Bounds, ClickEvent, DismissEvent,
+ DispatchPhase, Div, EventEmitter, FocusHandle, FocusableView, IntoElement, LayoutId,
+ ManagedView, MouseButton, MouseDownEvent, Pixels, Point, Render, View, VisualContext,
};
pub enum ContextMenuItem {
- Separator(ListSeparator),
- Header(ListSubHeader),
- Entry(ListItem, Rc<dyn Fn(&ClickEvent, &mut WindowContext)>),
+ Separator,
+ Header(SharedString),
+ Entry(SharedString, Rc<dyn Fn(&ClickEvent, &mut WindowContext)>),
}
pub struct ContextMenu {
@@ -26,7 +26,7 @@ impl FocusableView for ContextMenu {
}
}
-impl EventEmitter<Manager> for ContextMenu {}
+impl EventEmitter<DismissEvent> for ContextMenu {}
impl ContextMenu {
pub fn build(
@@ -46,38 +46,39 @@ impl ContextMenu {
}
pub fn header(mut self, title: impl Into<SharedString>) -> Self {
- self.items
- .push(ContextMenuItem::Header(ListSubHeader::new(title)));
+ self.items.push(ContextMenuItem::Header(title.into()));
self
}
pub fn separator(mut self) -> Self {
- self.items.push(ContextMenuItem::Separator(ListSeparator));
+ self.items.push(ContextMenuItem::Separator);
self
}
pub fn entry(
mut self,
- view: ListItem,
+ label: impl Into<SharedString>,
on_click: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
) -> Self {
self.items
- .push(ContextMenuItem::Entry(view, Rc::new(on_click)));
+ .push(ContextMenuItem::Entry(label.into(), Rc::new(on_click)));
self
}
- pub fn action(self, view: ListItem, action: Box<dyn Action>) -> Self {
+ pub fn action(self, label: impl Into<SharedString>, action: Box<dyn Action>) -> Self {
// todo: add the keybindings to the list entry
- self.entry(view, move |_, cx| cx.dispatch_action(action.boxed_clone()))
+ self.entry(label.into(), move |_, cx| {
+ cx.dispatch_action(action.boxed_clone())
+ })
}
pub fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
// todo!()
- cx.emit(Manager::Dismiss);
+ cx.emit(DismissEvent::Dismiss);
}
pub fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
- cx.emit(Manager::Dismiss);
+ cx.emit(DismissEvent::Dismiss);
}
}
@@ -104,21 +105,21 @@ impl Render for ContextMenu {
// .border_color(cx.theme().colors().border)
.child(
List::new().children(self.items.iter().map(|item| match item {
- ContextMenuItem::Separator(separator) => {
- separator.clone().render_into_any()
+ ContextMenuItem::Separator => ListSeparator::new().into_any_element(),
+ ContextMenuItem::Header(header) => {
+ ListSubHeader::new(header.clone()).into_any_element()
}
- ContextMenuItem::Header(header) => header.clone().render_into_any(),
ContextMenuItem::Entry(entry, callback) => {
let callback = callback.clone();
- let dismiss = cx.listener(|_, _, cx| cx.emit(Manager::Dismiss));
+ let dismiss = cx.listener(|_, _, cx| cx.emit(DismissEvent::Dismiss));
- entry
- .clone()
+ ListItem::new(entry.clone())
+ .child(Label::new(entry.clone()))
.on_click(move |event, cx| {
callback(event, cx);
dismiss(event, cx)
})
- .render_into_any()
+ .into_any_element()
}
})),
),
@@ -140,8 +141,8 @@ impl<M: ManagedView> MenuHandle<M> {
self
}
- pub fn child<R: RenderOnce>(mut self, f: impl FnOnce(bool) -> R + 'static) -> Self {
- self.child_builder = Some(Box::new(|b| f(b).render_once().into_any()));
+ pub fn child<R: IntoElement>(mut self, f: impl FnOnce(bool) -> R + 'static) -> Self {
+ self.child_builder = Some(Box::new(|b| f(b).into_element().into_any()));
self
}
@@ -264,7 +265,7 @@ impl<M: ManagedView> Element for MenuHandle<M> {
let new_menu = (builder)(cx);
let menu2 = menu.clone();
cx.subscribe(&new_menu, move |modal, e, cx| match e {
- &Manager::Dismiss => {
+ &DismissEvent::Dismiss => {
*menu2.borrow_mut() = None;
cx.notify();
}
@@ -286,127 +287,14 @@ impl<M: ManagedView> Element for MenuHandle<M> {
}
}
-impl<M: ManagedView> RenderOnce for MenuHandle<M> {
+impl<M: ManagedView> IntoElement for MenuHandle<M> {
type Element = Self;
fn element_id(&self) -> Option<gpui::ElementId> {
Some(self.id.clone())
}
- fn render_once(self) -> Self::Element {
+ fn into_element(self) -> Self::Element {
self
}
}
-
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use super::*;
- use crate::{story::Story, Label};
- use gpui::{actions, Div, Render};
-
- actions!(PrintCurrentDate, PrintBestFood);
-
- fn build_menu(cx: &mut WindowContext, header: impl Into<SharedString>) -> View<ContextMenu> {
- ContextMenu::build(cx, |menu, _| {
- menu.header(header)
- .separator()
- .entry(
- ListItem::new("Print current time", Label::new("Print current time")),
- |v, cx| {
- println!("dispatching PrintCurrentTime action");
- cx.dispatch_action(PrintCurrentDate.boxed_clone())
- },
- )
- .entry(
- ListItem::new("Print best food", Label::new("Print best food")),
- |v, cx| cx.dispatch_action(PrintBestFood.boxed_clone()),
- )
- })
- }
-
- pub struct ContextMenuStory;
-
- impl Render for ContextMenuStory {
- type Element = Div;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- Story::container(cx)
- .on_action(|_: &PrintCurrentDate, _| {
- println!("printing unix time!");
- if let Ok(unix_time) = std::time::UNIX_EPOCH.elapsed() {
- println!("Current Unix time is {:?}", unix_time.as_secs());
- }
- })
- .on_action(|_: &PrintBestFood, _| {
- println!("burrito");
- })
- .flex()
- .flex_row()
- .justify_between()
- .child(
- div()
- .flex()
- .flex_col()
- .justify_between()
- .child(
- menu_handle("test2")
- .child(|is_open| {
- Label::new(if is_open {
- "TOP LEFT"
- } else {
- "RIGHT CLICK ME"
- })
- })
- .menu(move |cx| build_menu(cx, "top left")),
- )
- .child(
- menu_handle("test1")
- .child(|is_open| {
- Label::new(if is_open {
- "BOTTOM LEFT"
- } else {
- "RIGHT CLICK ME"
- })
- })
- .anchor(AnchorCorner::BottomLeft)
- .attach(AnchorCorner::TopLeft)
- .menu(move |cx| build_menu(cx, "bottom left")),
- ),
- )
- .child(
- div()
- .flex()
- .flex_col()
- .justify_between()
- .child(
- menu_handle("test3")
- .child(|is_open| {
- Label::new(if is_open {
- "TOP RIGHT"
- } else {
- "RIGHT CLICK ME"
- })
- })
- .anchor(AnchorCorner::TopRight)
- .menu(move |cx| build_menu(cx, "top right")),
- )
- .child(
- menu_handle("test4")
- .child(|is_open| {
- Label::new(if is_open {
- "BOTTOM RIGHT"
- } else {
- "RIGHT CLICK ME"
- })
- })
- .anchor(AnchorCorner::BottomRight)
- .attach(AnchorCorner::TopRight)
- .menu(move |cx| build_menu(cx, "bottom right")),
- ),
- )
- }
- }
-}
@@ -1,83 +0,0 @@
-use crate::prelude::*;
-use crate::{v_stack, ButtonGroup};
-
-#[derive(RenderOnce)]
-pub struct Details {
- text: &'static str,
- meta: Option<&'static str>,
- actions: Option<ButtonGroup>,
-}
-
-impl Component for Details {
- type Rendered = Div;
-
- fn render(self, cx: &mut WindowContext) -> Self::Rendered {
- v_stack()
- .p_1()
- .gap_0p5()
- .text_ui_sm()
- .text_color(cx.theme().colors().text)
- .size_full()
- .child(self.text)
- .children(self.meta.map(|m| m))
- .children(self.actions.map(|a| a))
- }
-}
-
-impl Details {
- pub fn new(text: &'static str) -> Self {
- Self {
- text,
- meta: None,
- actions: None,
- }
- }
-
- pub fn meta_text(mut self, meta: &'static str) -> Self {
- self.meta = Some(meta);
- self
- }
-
- pub fn actions(mut self, actions: ButtonGroup) -> Self {
- self.actions = Some(actions);
- self
- }
-}
-
-use gpui::{Div, RenderOnce};
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use super::*;
- use crate::{Button, Story};
- use gpui::{Div, Render};
-
- pub struct DetailsStory;
-
- impl Render for DetailsStory {
- type Element = Div;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- Story::container(cx)
- .child(Story::title_for::<Details>(cx))
- .child(Story::label(cx, "Default"))
- .child(Details::new("The quick brown fox jumps over the lazy dog"))
- .child(Story::label(cx, "With meta"))
- .child(
- Details::new("The quick brown fox jumps over the lazy dog")
- .meta_text("Sphinx of black quartz, judge my vow."),
- )
- .child(Story::label(cx, "With meta and actions"))
- .child(
- Details::new("The quick brown fox jumps over the lazy dog")
- .meta_text("Sphinx of black quartz, judge my vow.")
- .actions(ButtonGroup::new(vec![
- Button::new("Decline"),
- Button::new("Accept").variant(crate::ButtonVariant::Filled),
- ])),
- )
- }
- }
-}
@@ -0,0 +1,19 @@
+use gpui::{div, Element, ParentElement};
+
+use crate::{Color, Icon, IconElement, IconSize, Toggle};
+
+pub fn disclosure_control(toggle: Toggle) -> impl Element {
+ match (toggle.is_toggleable(), toggle.is_toggled()) {
+ (false, _) => div(),
+ (_, true) => div().child(
+ IconElement::new(Icon::ChevronDown)
+ .color(Color::Muted)
+ .size(IconSize::Small),
+ ),
+ (_, false) => div().child(
+ IconElement::new(Icon::ChevronRight)
+ .color(Color::Muted)
+ .size(IconSize::Small),
+ ),
+ }
+}
@@ -1,4 +1,4 @@
-use gpui::{Div, RenderOnce};
+use gpui::{Div, IntoElement};
use crate::prelude::*;
@@ -7,13 +7,13 @@ enum DividerDirection {
Vertical,
}
-#[derive(RenderOnce)]
+#[derive(IntoElement)]
pub struct Divider {
direction: DividerDirection,
inset: bool,
}
-impl Component for Divider {
+impl RenderOnce for Divider {
type Rendered = Div;
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
@@ -1,28 +0,0 @@
-use gpui::Div;
-
-use crate::{prelude::*, v_stack};
-
-/// Create an elevated surface.
-///
-/// Must be used inside of a relative parent element
-pub fn elevated_surface(level: ElevationIndex, cx: &mut WindowContext) -> Div {
- let colors = cx.theme().colors();
-
- // let shadow = BoxShadow {
- // color: hsla(0., 0., 0., 0.1),
- // offset: point(px(0.), px(1.)),
- // blur_radius: px(3.),
- // spread_radius: px(0.),
- // };
-
- v_stack()
- .rounded_lg()
- .bg(colors.elevated_surface_background)
- .border()
- .border_color(colors.border)
- .shadow(level.shadow())
-}
-
-pub fn modal(cx: &mut WindowContext) -> Div {
- elevated_surface(ElevationIndex::ModalSurface, cx)
-}
@@ -1,33 +0,0 @@
-use crate::prelude::*;
-use crate::{Avatar, Player};
-
-#[derive(RenderOnce)]
-pub struct Facepile {
- players: Vec<Player>,
-}
-
-impl Component for Facepile {
- type Rendered = Div;
-
- fn render(self, cx: &mut WindowContext) -> Self::Rendered {
- let player_count = self.players.len();
- let player_list = self.players.iter().enumerate().map(|(ix, player)| {
- let isnt_last = ix < player_count - 1;
-
- div()
- .when(isnt_last, |div| div.neg_mr_1())
- .child(Avatar::new(player.avatar_src().to_string()))
- });
- div().p_1().flex().items_center().children(player_list)
- }
-}
-
-impl Facepile {
- pub fn new<P: Iterator<Item = Player>>(players: P) -> Self {
- Self {
- players: players.collect(),
- }
- }
-}
-
-use gpui::{Div, RenderOnce};
@@ -1,4 +1,4 @@
-use gpui::{rems, svg, RenderOnce, Svg};
+use gpui::{rems, svg, IntoElement, Svg};
use strum::EnumIter;
use crate::prelude::*;
@@ -23,6 +23,7 @@ pub enum Icon {
BellOff,
BellRing,
Bolt,
+ CaseSensitive,
Check,
ChevronDown,
ChevronLeft,
@@ -31,6 +32,9 @@ pub enum Icon {
Close,
Collab,
Copilot,
+ CopilotInit,
+ CopilotError,
+ CopilotDisabled,
Dash,
Envelope,
ExclamationTriangle,
@@ -65,9 +69,8 @@ pub enum Icon {
Split,
SplitMessage,
Terminal,
- XCircle,
WholeWord,
- CaseSensitive,
+ XCircle,
}
impl Icon {
@@ -84,6 +87,7 @@ impl Icon {
Icon::BellOff => "icons/bell-off.svg",
Icon::BellRing => "icons/bell-ring.svg",
Icon::Bolt => "icons/bolt.svg",
+ Icon::CaseSensitive => "icons/case_insensitive.svg",
Icon::Check => "icons/check.svg",
Icon::ChevronDown => "icons/chevron_down.svg",
Icon::ChevronLeft => "icons/chevron_left.svg",
@@ -92,6 +96,9 @@ impl Icon {
Icon::Close => "icons/x.svg",
Icon::Collab => "icons/user_group_16.svg",
Icon::Copilot => "icons/copilot.svg",
+ Icon::CopilotInit => "icons/copilot_init.svg",
+ Icon::CopilotError => "icons/copilot_error.svg",
+ Icon::CopilotDisabled => "icons/copilot_disabled.svg",
Icon::Dash => "icons/dash.svg",
Icon::Envelope => "icons/feedback.svg",
Icon::ExclamationTriangle => "icons/warning.svg",
@@ -126,21 +133,20 @@ impl Icon {
Icon::Split => "icons/split.svg",
Icon::SplitMessage => "icons/split_message.svg",
Icon::Terminal => "icons/terminal.svg",
- Icon::XCircle => "icons/error.svg",
Icon::WholeWord => "icons/word_search.svg",
- Icon::CaseSensitive => "icons/case_insensitive.svg",
+ Icon::XCircle => "icons/error.svg",
}
}
}
-#[derive(RenderOnce)]
+#[derive(IntoElement)]
pub struct IconElement {
path: SharedString,
- color: TextColor,
+ color: Color,
size: IconSize,
}
-impl Component for IconElement {
+impl RenderOnce for IconElement {
type Rendered = Svg;
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
@@ -161,7 +167,7 @@ impl IconElement {
pub fn new(icon: Icon) -> Self {
Self {
path: icon.path().into(),
- color: TextColor::default(),
+ color: Color::default(),
size: IconSize::default(),
}
}
@@ -169,12 +175,12 @@ impl IconElement {
pub fn from_path(path: impl Into<SharedString>) -> Self {
Self {
path: path.into(),
- color: TextColor::default(),
+ color: Color::default(),
size: IconSize::default(),
}
}
- pub fn color(mut self, color: TextColor) -> Self {
+ pub fn color(mut self, color: Color) -> Self {
self.color = color;
self
}
@@ -197,31 +203,3 @@ impl IconElement {
.text_color(self.color.color(cx))
}
}
-
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use gpui::{Div, Render};
- use strum::IntoEnumIterator;
-
- use crate::Story;
-
- use super::*;
-
- pub struct IconStory;
-
- impl Render for IconStory {
- type Element = Div;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- let icons = Icon::iter();
-
- Story::container(cx)
- .child(Story::title_for::<IconElement>(cx))
- .child(Story::label(cx, "All Icons"))
- .child(div().flex().gap_3().children(icons.map(IconElement::new)))
- }
- }
-}
@@ -1,11 +1,11 @@
use crate::{h_stack, prelude::*, Icon, IconElement};
use gpui::{prelude::*, Action, AnyView, Div, MouseButton, MouseDownEvent, Stateful};
-#[derive(RenderOnce)]
+#[derive(IntoElement)]
pub struct IconButton {
id: ElementId,
icon: Icon,
- color: TextColor,
+ color: Color,
variant: ButtonVariant,
state: InteractionState,
selected: bool,
@@ -13,13 +13,13 @@ pub struct IconButton {
on_mouse_down: Option<Box<dyn Fn(&MouseDownEvent, &mut WindowContext) + 'static>>,
}
-impl Component for IconButton {
+impl RenderOnce for IconButton {
type Rendered = Stateful<Div>;
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
let icon_color = match (self.state, self.color) {
- (InteractionState::Disabled, _) => TextColor::Disabled,
- (InteractionState::Active, _) => TextColor::Selected,
+ (InteractionState::Disabled, _) => Color::Disabled,
+ (InteractionState::Active, _) => Color::Selected,
_ => self.color,
};
@@ -37,7 +37,7 @@ impl Component for IconButton {
};
if self.selected {
- bg_color = bg_hover_color;
+ bg_color = cx.theme().colors().element_selected;
}
let mut button = h_stack()
@@ -76,7 +76,7 @@ impl IconButton {
Self {
id: id.into(),
icon,
- color: TextColor::default(),
+ color: Color::default(),
variant: ButtonVariant::default(),
state: InteractionState::default(),
selected: false,
@@ -90,7 +90,7 @@ impl IconButton {
self
}
- pub fn color(mut self, color: TextColor) -> Self {
+ pub fn color(mut self, color: Color) -> Self {
self.color = color;
self
}
@@ -1,37 +0,0 @@
-use crate::prelude::*;
-use gpui::{px, Div, RenderOnce};
-
-#[derive(RenderOnce)]
-pub struct UnreadIndicator;
-
-impl Component for UnreadIndicator {
- type Rendered = Div;
-
- fn render(self, cx: &mut WindowContext) -> Self::Rendered {
- div()
- .rounded_full()
- .border_2()
- .border_color(cx.theme().colors().surface_background)
- .w(px(9.0))
- .h(px(9.0))
- .z_index(2)
- .bg(cx.theme().status().info)
- }
-}
-
-impl UnreadIndicator {
- pub fn new() -> Self {
- Self
- }
-
- fn render(self, cx: &mut WindowContext) -> impl Element {
- div()
- .rounded_full()
- .border_2()
- .border_color(cx.theme().colors().surface_background)
- .w(px(9.0))
- .h(px(9.0))
- .z_index(2)
- .bg(cx.theme().status().info)
- }
-}
@@ -1,5 +1,5 @@
use crate::{prelude::*, Label};
-use gpui::{prelude::*, Div, RenderOnce, Stateful};
+use gpui::{prelude::*, Div, IntoElement, Stateful};
#[derive(Default, PartialEq)]
pub enum InputVariant {
@@ -8,7 +8,7 @@ pub enum InputVariant {
Filled,
}
-#[derive(RenderOnce)]
+#[derive(IntoElement)]
pub struct Input {
placeholder: SharedString,
value: String,
@@ -18,7 +18,7 @@ pub struct Input {
is_active: bool,
}
-impl Component for Input {
+impl RenderOnce for Input {
type Rendered = Stateful<Div>;
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
@@ -36,15 +36,15 @@ impl Component for Input {
};
let placeholder_label = Label::new(self.placeholder.clone()).color(if self.disabled {
- TextColor::Disabled
+ Color::Disabled
} else {
- TextColor::Placeholder
+ Color::Placeholder
});
let label = Label::new(self.value.clone()).color(if self.disabled {
- TextColor::Disabled
+ Color::Disabled
} else {
- TextColor::Default
+ Color::Default
});
div()
@@ -106,26 +106,3 @@ impl Input {
self
}
}
-
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use super::*;
- use crate::Story;
- use gpui::{Div, Render};
-
- pub struct InputStory;
-
- impl Render for InputStory {
- type Element = Div;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- Story::container(cx)
- .child(Story::title_for::<Input>(cx))
- .child(Story::label(cx, "Default"))
- .child(div().flex().child(Input::new("Search")))
- }
- }
-}
@@ -1,7 +1,7 @@
use crate::prelude::*;
-use gpui::{Action, Div, RenderOnce};
+use gpui::{Action, Div, IntoElement};
-#[derive(RenderOnce, Clone)]
+#[derive(IntoElement, Clone)]
pub struct KeyBinding {
/// A keybinding consists of a key and a set of modifier keys.
/// More then one keybinding produces a chord.
@@ -10,7 +10,7 @@ pub struct KeyBinding {
key_binding: gpui::KeyBinding,
}
-impl Component for KeyBinding {
+impl RenderOnce for KeyBinding {
type Rendered = Div;
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
@@ -44,12 +44,12 @@ impl KeyBinding {
}
}
-#[derive(RenderOnce)]
+#[derive(IntoElement)]
pub struct Key {
key: SharedString,
}
-impl Component for Key {
+impl RenderOnce for Key {
type Rendered = Div;
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
@@ -69,70 +69,3 @@ impl Key {
Self { key: key.into() }
}
}
-
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use super::*;
- pub use crate::KeyBinding;
- use crate::Story;
- use gpui::{actions, Div, Render};
- use itertools::Itertools;
- pub struct KeybindingStory;
-
- actions!(NoAction);
-
- pub fn binding(key: &str) -> gpui::KeyBinding {
- gpui::KeyBinding::new(key, NoAction {}, None)
- }
-
- impl Render for KeybindingStory {
- type Element = Div;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- let all_modifier_permutations =
- ["ctrl", "alt", "cmd", "shift"].into_iter().permutations(2);
-
- Story::container(cx)
- .child(Story::title_for::<KeyBinding>(cx))
- .child(Story::label(cx, "Single Key"))
- .child(KeyBinding::new(binding("Z")))
- .child(Story::label(cx, "Single Key with Modifier"))
- .child(
- div()
- .flex()
- .gap_3()
- .child(KeyBinding::new(binding("ctrl-c")))
- .child(KeyBinding::new(binding("alt-c")))
- .child(KeyBinding::new(binding("cmd-c")))
- .child(KeyBinding::new(binding("shift-c"))),
- )
- .child(Story::label(cx, "Single Key with Modifier (Permuted)"))
- .child(
- div().flex().flex_col().children(
- all_modifier_permutations
- .chunks(4)
- .into_iter()
- .map(|chunk| {
- div()
- .flex()
- .gap_4()
- .py_3()
- .children(chunk.map(|permutation| {
- KeyBinding::new(binding(&*(permutation.join("-") + "-x")))
- }))
- }),
- ),
- )
- .child(Story::label(cx, "Single Key with All Modifiers"))
- .child(KeyBinding::new(binding("ctrl-alt-cmd-shift-z")))
- .child(Story::label(cx, "Chord"))
- .child(KeyBinding::new(binding("a z")))
- .child(Story::label(cx, "Chord with Modifier"))
- .child(KeyBinding::new(binding("ctrl-a shift-z")))
- .child(KeyBinding::new(binding("fn-s")))
- }
- }
-}
@@ -1,6 +1,6 @@
use crate::prelude::*;
use crate::styled_ext::StyledExt;
-use gpui::{relative, Div, Hsla, RenderOnce, StyledText, TextRun, WindowContext};
+use gpui::{relative, Div, Hsla, IntoElement, StyledText, TextRun, WindowContext};
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
pub enum LabelSize {
@@ -9,48 +9,6 @@ pub enum LabelSize {
Small,
}
-#[derive(Default, PartialEq, Copy, Clone)]
-pub enum TextColor {
- #[default]
- Default,
- Accent,
- Created,
- Deleted,
- Disabled,
- Error,
- Hidden,
- Info,
- Modified,
- Muted,
- Placeholder,
- Player(u32),
- Selected,
- Success,
- Warning,
-}
-
-impl TextColor {
- pub fn color(&self, cx: &WindowContext) -> Hsla {
- match self {
- TextColor::Default => cx.theme().colors().text,
- TextColor::Muted => cx.theme().colors().text_muted,
- TextColor::Created => cx.theme().status().created,
- TextColor::Modified => cx.theme().status().modified,
- TextColor::Deleted => cx.theme().status().deleted,
- TextColor::Disabled => cx.theme().colors().text_disabled,
- TextColor::Hidden => cx.theme().status().hidden,
- TextColor::Info => cx.theme().status().info,
- TextColor::Placeholder => cx.theme().colors().text_placeholder,
- TextColor::Accent => cx.theme().colors().text_accent,
- TextColor::Player(i) => cx.theme().styles.player.0[i.clone() as usize].cursor,
- TextColor::Error => cx.theme().status().error,
- TextColor::Selected => cx.theme().colors().text_accent,
- TextColor::Success => cx.theme().status().success,
- TextColor::Warning => cx.theme().status().warning,
- }
- }
-}
-
#[derive(Default, PartialEq, Copy, Clone)]
pub enum LineHeightStyle {
#[default]
@@ -59,16 +17,16 @@ pub enum LineHeightStyle {
UILabel,
}
-#[derive(Clone, RenderOnce)]
+#[derive(IntoElement, Clone)]
pub struct Label {
label: SharedString,
size: LabelSize,
line_height_style: LineHeightStyle,
- color: TextColor,
+ color: Color,
strikethrough: bool,
}
-impl Component for Label {
+impl RenderOnce for Label {
type Rendered = Div;
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
@@ -80,7 +38,7 @@ impl Component for Label {
.top_1_2()
.w_full()
.h_px()
- .bg(TextColor::Hidden.color(cx)),
+ .bg(Color::Hidden.color(cx)),
)
})
.map(|this| match self.size {
@@ -101,7 +59,7 @@ impl Label {
label: label.into(),
size: LabelSize::Default,
line_height_style: LineHeightStyle::default(),
- color: TextColor::Default,
+ color: Color::Default,
strikethrough: false,
}
}
@@ -111,7 +69,7 @@ impl Label {
self
}
- pub fn color(mut self, color: TextColor) -> Self {
+ pub fn color(mut self, color: Color) -> Self {
self.color = color;
self
}
@@ -127,16 +85,16 @@ impl Label {
}
}
-#[derive(RenderOnce)]
+#[derive(IntoElement)]
pub struct HighlightedLabel {
label: SharedString,
size: LabelSize,
- color: TextColor,
+ color: Color,
highlight_indices: Vec<usize>,
strikethrough: bool,
}
-impl Component for HighlightedLabel {
+impl RenderOnce for HighlightedLabel {
type Rendered = Div;
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
@@ -185,14 +143,14 @@ impl Component for HighlightedLabel {
.my_auto()
.w_full()
.h_px()
- .bg(TextColor::Hidden.color(cx)),
+ .bg(Color::Hidden.color(cx)),
)
})
.map(|this| match self.size {
LabelSize::Default => this.text_ui(),
LabelSize::Small => this.text_ui_sm(),
})
- .child(StyledText::new(self.label, runs))
+ .child(StyledText::new(self.label).with_runs(runs))
}
}
@@ -203,7 +161,7 @@ impl HighlightedLabel {
Self {
label: label.into(),
size: LabelSize::Default,
- color: TextColor::Default,
+ color: Color::Default,
highlight_indices,
strikethrough: false,
}
@@ -214,7 +172,7 @@ impl HighlightedLabel {
self
}
- pub fn color(mut self, color: TextColor) -> Self {
+ pub fn color(mut self, color: Color) -> Self {
self.color = color;
self
}
@@ -230,35 +188,3 @@ struct Run {
pub text: String,
pub color: Hsla,
}
-
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use super::*;
- use crate::Story;
- use gpui::{Div, Render};
-
- pub struct LabelStory;
-
- impl Render for LabelStory {
- type Element = Div;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- Story::container(cx)
- .child(Story::title_for::<Label>(cx))
- .child(Story::label(cx, "Default"))
- .child(Label::new("Hello, world!"))
- .child(Story::label(cx, "Highlighted"))
- .child(HighlightedLabel::new(
- "Hello, world!",
- vec![0, 1, 2, 7, 8, 12],
- ))
- .child(HighlightedLabel::new(
- "Héllo, world!",
- vec![0, 1, 3, 8, 9, 13],
- ))
- }
- }
-}
@@ -1,5 +1,5 @@
use gpui::{
- div, px, AnyElement, ClickEvent, Div, RenderOnce, Stateful, StatefulInteractiveElement,
+ div, px, AnyElement, ClickEvent, Div, IntoElement, Stateful, StatefulInteractiveElement,
};
use smallvec::SmallVec;
use std::rc::Rc;
@@ -25,7 +25,7 @@ pub enum ListHeaderMeta {
Text(Label),
}
-#[derive(RenderOnce)]
+#[derive(IntoElement)]
pub struct ListHeader {
label: SharedString,
left_icon: Option<Icon>,
@@ -34,7 +34,7 @@ pub struct ListHeader {
toggle: Toggle,
}
-impl Component for ListHeader {
+impl RenderOnce for ListHeader {
type Rendered = Div;
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
@@ -47,7 +47,7 @@ impl Component for ListHeader {
.items_center()
.children(icons.into_iter().map(|i| {
IconElement::new(i)
- .color(TextColor::Muted)
+ .color(Color::Muted)
.size(IconSize::Small)
})),
),
@@ -80,10 +80,10 @@ impl Component for ListHeader {
.items_center()
.children(self.left_icon.map(|i| {
IconElement::new(i)
- .color(TextColor::Muted)
+ .color(Color::Muted)
.size(IconSize::Small)
}))
- .child(Label::new(self.label.clone()).color(TextColor::Muted)),
+ .child(Label::new(self.label.clone()).color(Color::Muted)),
)
.child(disclosure_control),
)
@@ -179,7 +179,7 @@ impl ListHeader {
// }
}
-#[derive(RenderOnce, Clone)]
+#[derive(IntoElement, Clone)]
pub struct ListSubHeader {
label: SharedString,
left_icon: Option<Icon>,
@@ -201,7 +201,7 @@ impl ListSubHeader {
}
}
-impl Component for ListSubHeader {
+impl RenderOnce for ListSubHeader {
type Rendered = Div;
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
@@ -222,10 +222,10 @@ impl Component for ListSubHeader {
.items_center()
.children(self.left_icon.map(|i| {
IconElement::new(i)
- .color(TextColor::Muted)
+ .color(Color::Muted)
.size(IconSize::Small)
}))
- .child(Label::new(self.label.clone()).color(TextColor::Muted)),
+ .child(Label::new(self.label.clone()).color(Color::Muted)),
),
)
}
@@ -238,52 +238,35 @@ pub enum ListEntrySize {
Medium,
}
-#[derive(RenderOnce)]
+#[derive(IntoElement)]
pub struct ListItem {
id: ElementId,
disabled: bool,
// TODO: Reintroduce this
// disclosure_control_style: DisclosureControlVisibility,
indent_level: u32,
- label: Label,
left_slot: Option<GraphicSlot>,
overflow: OverflowStyle,
size: ListEntrySize,
toggle: Toggle,
variant: ListItemVariant,
on_click: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
-}
-
-impl Clone for ListItem {
- fn clone(&self) -> Self {
- Self {
- id: self.id.clone(),
- disabled: self.disabled,
- indent_level: self.indent_level,
- label: self.label.clone(),
- left_slot: self.left_slot.clone(),
- overflow: self.overflow,
- size: self.size,
- toggle: self.toggle,
- variant: self.variant,
- on_click: self.on_click.clone(),
- }
- }
+ children: SmallVec<[AnyElement; 2]>,
}
impl ListItem {
- pub fn new(id: impl Into<ElementId>, label: Label) -> Self {
+ pub fn new(id: impl Into<ElementId>) -> Self {
Self {
id: id.into(),
disabled: false,
indent_level: 0,
- label,
left_slot: None,
overflow: OverflowStyle::Hidden,
size: ListEntrySize::default(),
toggle: Toggle::NotToggleable,
variant: ListItemVariant::default(),
on_click: Default::default(),
+ children: SmallVec::new(),
}
}
@@ -328,7 +311,7 @@ impl ListItem {
}
}
-impl Component for ListItem {
+impl RenderOnce for ListItem {
type Rendered = Stateful<Div>;
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
@@ -337,11 +320,11 @@ impl Component for ListItem {
h_stack().child(
IconElement::new(i)
.size(IconSize::Small)
- .color(TextColor::Muted),
+ .color(Color::Muted),
),
),
- Some(GraphicSlot::Avatar(src)) => Some(h_stack().child(Avatar::new(src))),
- Some(GraphicSlot::PublicActor(src)) => Some(h_stack().child(Avatar::new(src))),
+ Some(GraphicSlot::Avatar(src)) => Some(h_stack().child(Avatar::uri(src))),
+ Some(GraphicSlot::PublicActor(src)) => Some(h_stack().child(Avatar::uri(src))),
None => None,
};
@@ -364,12 +347,13 @@ impl Component for ListItem {
}
}
})
- .bg(cx.theme().colors().surface_background)
// TODO: Add focus state
// .when(self.state == InteractionState::Focused, |this| {
// this.border()
// .border_color(cx.theme().colors().border_focused)
// })
+ .hover(|style| style.bg(cx.theme().colors().ghost_element_hover))
+ .active(|style| style.bg(cx.theme().colors().ghost_element_active))
.child(
sized_item
.when(self.variant == ListItemVariant::Inset, |this| this.px_2())
@@ -393,12 +377,18 @@ impl Component for ListItem {
.relative()
.child(disclosure_control(self.toggle))
.children(left_content)
- .child(self.label),
+ .children(self.children),
)
}
}
-#[derive(RenderOnce, Clone)]
+impl ParentElement for ListItem {
+ fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
+ &mut self.children
+ }
+}
+
+#[derive(IntoElement, Clone)]
pub struct ListSeparator;
impl ListSeparator {
@@ -407,7 +397,7 @@ impl ListSeparator {
}
}
-impl Component for ListSeparator {
+impl RenderOnce for ListSeparator {
type Rendered = Div;
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
@@ -415,7 +405,7 @@ impl Component for ListSeparator {
}
}
-#[derive(RenderOnce)]
+#[derive(IntoElement)]
pub struct List {
/// Message to display when the list is empty
/// Defaults to "No items"
@@ -425,16 +415,14 @@ pub struct List {
children: SmallVec<[AnyElement; 2]>,
}
-impl Component for List {
+impl RenderOnce for List {
type Rendered = Div;
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
let list_content = match (self.children.is_empty(), self.toggle) {
(false, _) => div().children(self.children),
(true, Toggle::Toggled(false)) => div(),
- (true, _) => {
- div().child(Label::new(self.empty_message.clone()).color(TextColor::Muted))
- }
+ (true, _) => div().child(Label::new(self.empty_message.clone()).color(Color::Muted)),
};
v_stack()
@@ -1,85 +0,0 @@
-use gpui::{AnyElement, Div, RenderOnce};
-use smallvec::SmallVec;
-
-use crate::{h_stack, prelude::*, v_stack, Button, Icon, IconButton, Label};
-
-#[derive(RenderOnce)]
-pub struct Modal {
- id: ElementId,
- title: Option<SharedString>,
- primary_action: Option<Button>,
- secondary_action: Option<Button>,
- children: SmallVec<[AnyElement; 2]>,
-}
-
-impl Component for Modal {
- type Rendered = gpui::Stateful<Div>;
-
- fn render(self, cx: &mut WindowContext) -> Self::Rendered {
- v_stack()
- .id(self.id.clone())
- .w_96()
- // .rounded_xl()
- .bg(cx.theme().colors().background)
- .border()
- .border_color(cx.theme().colors().border)
- .shadow_2xl()
- .child(
- h_stack()
- .justify_between()
- .p_1()
- .border_b()
- .border_color(cx.theme().colors().border)
- .child(div().children(self.title.clone().map(|t| Label::new(t))))
- .child(IconButton::new("close", Icon::Close)),
- )
- .child(v_stack().p_1().children(self.children))
- .when(
- self.primary_action.is_some() || self.secondary_action.is_some(),
- |this| {
- this.child(
- h_stack()
- .border_t()
- .border_color(cx.theme().colors().border)
- .p_1()
- .justify_end()
- .children(self.secondary_action)
- .children(self.primary_action),
- )
- },
- )
- }
-}
-
-impl Modal {
- pub fn new(id: impl Into<ElementId>) -> Self {
- Self {
- id: id.into(),
- title: None,
- primary_action: None,
- secondary_action: None,
- children: SmallVec::new(),
- }
- }
-
- pub fn title(mut self, title: impl Into<SharedString>) -> Self {
- self.title = Some(title.into());
- self
- }
-
- pub fn primary_action(mut self, action: Button) -> Self {
- self.primary_action = Some(action);
- self
- }
-
- pub fn secondary_action(mut self, action: Button) -> Self {
- self.secondary_action = Some(action);
- self
- }
-}
-
-impl ParentElement for Modal {
- fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
- &mut self.children
- }
-}
@@ -1,40 +0,0 @@
-use gpui::rems;
-
-use crate::prelude::*;
-use crate::{h_stack, Icon};
-
-// #[derive(RenderOnce)]
-pub struct NotificationToast {
- label: SharedString,
- icon: Option<Icon>,
-}
-
-impl NotificationToast {
- pub fn new(label: SharedString) -> Self {
- Self { label, icon: None }
- }
-
- pub fn icon<I>(mut self, icon: I) -> Self
- where
- I: Into<Option<Icon>>,
- {
- self.icon = icon.into();
- self
- }
-
- fn render(self, cx: &mut WindowContext) -> impl Element {
- h_stack()
- .z_index(5)
- .absolute()
- .top_1()
- .right_1()
- .w(rems(9999.))
- .max_w_56()
- .py_1()
- .px_1p5()
- .rounded_lg()
- .shadow_md()
- .bg(cx.theme().colors().elevated_surface_background)
- .child(div().size_full().child(self.label.clone()))
- }
-}
@@ -1,212 +0,0 @@
-use crate::{h_stack, prelude::*, v_stack, KeyBinding, Label};
-use gpui::prelude::*;
-use gpui::Div;
-
-#[derive(RenderOnce)]
-pub struct Palette {
- id: ElementId,
- input_placeholder: SharedString,
- empty_string: SharedString,
- items: Vec<PaletteItem>,
- default_order: OrderMethod,
-}
-
-impl Component for Palette {
- type Rendered = gpui::Stateful<Div>;
-
- fn render(self, cx: &mut WindowContext) -> Self::Rendered {
- v_stack()
- .id(self.id)
- .w_96()
- .rounded_lg()
- .bg(cx.theme().colors().elevated_surface_background)
- .border()
- .border_color(cx.theme().colors().border)
- .child(
- v_stack()
- .gap_px()
- .child(v_stack().py_0p5().px_1().child(
- div().px_2().py_0p5().child(
- Label::new(self.input_placeholder).color(TextColor::Placeholder),
- ),
- ))
- .child(
- div()
- .h_px()
- .w_full()
- .bg(cx.theme().colors().element_background),
- )
- .child(
- v_stack()
- .id("items")
- .py_0p5()
- .px_1()
- .grow()
- .max_h_96()
- .overflow_y_scroll()
- .children(
- vec![if self.items.is_empty() {
- Some(h_stack().justify_between().px_2().py_1().child(
- Label::new(self.empty_string).color(TextColor::Muted),
- ))
- } else {
- None
- }]
- .into_iter()
- .flatten(),
- )
- .children(self.items.into_iter().enumerate().map(|(index, item)| {
- h_stack()
- .id(index)
- .justify_between()
- .px_2()
- .py_0p5()
- .rounded_lg()
- .hover(|style| {
- style.bg(cx.theme().colors().ghost_element_hover)
- })
- .active(|style| {
- style.bg(cx.theme().colors().ghost_element_active)
- })
- .child(item)
- })),
- ),
- )
- }
-}
-
-impl Palette {
- pub fn new(id: impl Into<ElementId>) -> Self {
- Self {
- id: id.into(),
- input_placeholder: "Find something...".into(),
- empty_string: "No items found.".into(),
- items: vec![],
- default_order: OrderMethod::default(),
- }
- }
-
- pub fn items(mut self, items: Vec<PaletteItem>) -> Self {
- self.items = items;
- self
- }
-
- pub fn placeholder(mut self, input_placeholder: impl Into<SharedString>) -> Self {
- self.input_placeholder = input_placeholder.into();
- self
- }
-
- pub fn empty_string(mut self, empty_string: impl Into<SharedString>) -> Self {
- self.empty_string = empty_string.into();
- self
- }
-
- // TODO: Hook up sort order
- pub fn default_order(mut self, default_order: OrderMethod) -> Self {
- self.default_order = default_order;
- self
- }
-}
-
-#[derive(RenderOnce)]
-pub struct PaletteItem {
- pub label: SharedString,
- pub sublabel: Option<SharedString>,
- pub key_binding: Option<KeyBinding>,
-}
-
-impl Component for PaletteItem {
- type Rendered = Div;
-
- fn render(self, cx: &mut WindowContext) -> Self::Rendered {
- div()
- .flex()
- .flex_row()
- .grow()
- .justify_between()
- .child(
- v_stack()
- .child(Label::new(self.label))
- .children(self.sublabel.map(|sublabel| Label::new(sublabel))),
- )
- .children(self.key_binding)
- }
-}
-
-impl PaletteItem {
- pub fn new(label: impl Into<SharedString>) -> Self {
- Self {
- label: label.into(),
- sublabel: None,
- key_binding: None,
- }
- }
-
- pub fn label(mut self, label: impl Into<SharedString>) -> Self {
- self.label = label.into();
- self
- }
-
- pub fn sublabel(mut self, sublabel: impl Into<Option<SharedString>>) -> Self {
- self.sublabel = sublabel.into();
- self
- }
-
- pub fn key_binding(mut self, key_binding: impl Into<Option<KeyBinding>>) -> Self {
- self.key_binding = key_binding.into();
- self
- }
-}
-
-use gpui::ElementId;
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use gpui::{Div, Render};
-
- use crate::{binding, Story};
-
- use super::*;
-
- pub struct PaletteStory;
-
- impl Render for PaletteStory {
- type Element = Div;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- {
- Story::container(cx)
- .child(Story::title_for::<Palette>(cx))
- .child(Story::label(cx, "Default"))
- .child(Palette::new("palette-1"))
- .child(Story::label(cx, "With Items"))
- .child(
- Palette::new("palette-2")
- .placeholder("Execute a command...")
- .items(vec![
- PaletteItem::new("theme selector: toggle")
- .key_binding(KeyBinding::new(binding("cmd-k cmd-t"))),
- PaletteItem::new("assistant: inline assist")
- .key_binding(KeyBinding::new(binding("cmd-enter"))),
- PaletteItem::new("assistant: quote selection")
- .key_binding(KeyBinding::new(binding("cmd-<"))),
- PaletteItem::new("assistant: toggle focus")
- .key_binding(KeyBinding::new(binding("cmd-?"))),
- PaletteItem::new("auto update: check"),
- PaletteItem::new("auto update: view release notes"),
- PaletteItem::new("branches: open recent")
- .key_binding(KeyBinding::new(binding("cmd-alt-b"))),
- PaletteItem::new("chat panel: toggle focus"),
- PaletteItem::new("cli: install"),
- PaletteItem::new("client: sign in"),
- PaletteItem::new("client: sign out"),
- PaletteItem::new("editor: cancel")
- .key_binding(KeyBinding::new(binding("escape"))),
- ]),
- )
- }
- }
- }
-}
@@ -1,152 +0,0 @@
-use gpui::px;
-use gpui::{prelude::*, AbsoluteLength, AnyElement, Div, RenderOnce};
-use smallvec::SmallVec;
-
-use crate::prelude::*;
-use crate::v_stack;
-
-#[derive(Default, Debug, PartialEq, Eq, Hash, Clone, Copy)]
-pub enum PanelAllowedSides {
- LeftOnly,
- RightOnly,
- BottomOnly,
- #[default]
- LeftAndRight,
- All,
-}
-
-impl PanelAllowedSides {
- /// Return a `HashSet` that contains the allowable `PanelSide`s.
- pub fn allowed_sides(&self) -> HashSet<PanelSide> {
- match self {
- Self::LeftOnly => HashSet::from_iter([PanelSide::Left]),
- Self::RightOnly => HashSet::from_iter([PanelSide::Right]),
- Self::BottomOnly => HashSet::from_iter([PanelSide::Bottom]),
- Self::LeftAndRight => HashSet::from_iter([PanelSide::Left, PanelSide::Right]),
- Self::All => HashSet::from_iter([PanelSide::Left, PanelSide::Right, PanelSide::Bottom]),
- }
- }
-}
-
-#[derive(Default, Debug, PartialEq, Eq, Hash, Clone, Copy)]
-pub enum PanelSide {
- #[default]
- Left,
- Right,
- Bottom,
-}
-
-use std::collections::HashSet;
-
-#[derive(RenderOnce)]
-pub struct Panel {
- id: ElementId,
- current_side: PanelSide,
- /// Defaults to PanelAllowedSides::LeftAndRight
- allowed_sides: PanelAllowedSides,
- initial_width: AbsoluteLength,
- width: Option<AbsoluteLength>,
- children: SmallVec<[AnyElement; 2]>,
-}
-
-impl Component for Panel {
- type Rendered = gpui::Stateful<Div>;
-
- fn render(self, cx: &mut WindowContext) -> Self::Rendered {
- let current_size = self.width.unwrap_or(self.initial_width);
-
- v_stack()
- .id(self.id.clone())
- .flex_initial()
- .map(|this| match self.current_side {
- PanelSide::Left | PanelSide::Right => this.h_full().w(current_size),
- PanelSide::Bottom => this,
- })
- .map(|this| match self.current_side {
- PanelSide::Left => this.border_r(),
- PanelSide::Right => this.border_l(),
- PanelSide::Bottom => this.border_b().w_full().h(current_size),
- })
- .bg(cx.theme().colors().surface_background)
- .border_color(cx.theme().colors().border)
- .children(self.children)
- }
-}
-
-impl Panel {
- pub fn new(id: impl Into<ElementId>, cx: &mut WindowContext) -> Self {
- Self {
- id: id.into(),
- current_side: PanelSide::default(),
- allowed_sides: PanelAllowedSides::default(),
- initial_width: px(320.).into(),
- width: None,
- children: SmallVec::new(),
- }
- }
-
- pub fn initial_width(mut self, initial_width: AbsoluteLength) -> Self {
- self.initial_width = initial_width;
- self
- }
-
- pub fn width(mut self, width: AbsoluteLength) -> Self {
- self.width = Some(width);
- self
- }
-
- pub fn allowed_sides(mut self, allowed_sides: PanelAllowedSides) -> Self {
- self.allowed_sides = allowed_sides;
- self
- }
-
- pub fn side(mut self, side: PanelSide) -> Self {
- let allowed_sides = self.allowed_sides.allowed_sides();
-
- if allowed_sides.contains(&side) {
- self.current_side = side;
- } else {
- panic!(
- "The panel side {:?} was not added as allowed before it was set.",
- side
- );
- }
- self
- }
-}
-
-impl ParentElement for Panel {
- fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
- &mut self.children
- }
-}
-
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use super::*;
- use crate::{Label, Story};
- use gpui::{Div, InteractiveElement, Render};
-
- pub struct PanelStory;
-
- impl Render for PanelStory {
- type Element = Div;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- Story::container(cx)
- .child(Story::title_for::<Panel>(cx))
- .child(Story::label(cx, "Default"))
- .child(
- Panel::new("panel", cx).child(
- div()
- .id("panel-contents")
- .overflow_y_scroll()
- .children((0..100).map(|ix| Label::new(format!("Item {}", ix + 1)))),
- ),
- )
- }
- }
-}
@@ -1,174 +0,0 @@
-use gpui::Hsla;
-
-use crate::prelude::*;
-
-/// Represents a person with a Zed account's public profile.
-/// All data in this struct should be considered public.
-pub struct PublicPlayer {
- pub username: SharedString,
- pub avatar: SharedString,
- pub is_contact: bool,
-}
-
-impl PublicPlayer {
- pub fn new(username: impl Into<SharedString>, avatar: impl Into<SharedString>) -> Self {
- Self {
- username: username.into(),
- avatar: avatar.into(),
- is_contact: false,
- }
- }
-}
-
-#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
-pub enum PlayerStatus {
- #[default]
- Offline,
- Online,
- InCall,
- Away,
- DoNotDisturb,
- Invisible,
-}
-
-#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
-pub enum MicStatus {
- Muted,
- #[default]
- Unmuted,
-}
-
-impl MicStatus {
- pub fn inverse(&self) -> Self {
- match self {
- Self::Muted => Self::Unmuted,
- Self::Unmuted => Self::Muted,
- }
- }
-}
-
-#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
-pub enum VideoStatus {
- On,
- #[default]
- Off,
-}
-
-impl VideoStatus {
- pub fn inverse(&self) -> Self {
- match self {
- Self::On => Self::Off,
- Self::Off => Self::On,
- }
- }
-}
-
-#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
-pub enum ScreenShareStatus {
- Shared,
- #[default]
- NotShared,
-}
-
-impl ScreenShareStatus {
- pub fn inverse(&self) -> Self {
- match self {
- Self::Shared => Self::NotShared,
- Self::NotShared => Self::Shared,
- }
- }
-}
-
-#[derive(Clone)]
-pub struct PlayerCallStatus {
- pub mic_status: MicStatus,
- /// Indicates if the player is currently speaking
- /// And the intensity of the volume coming through
- ///
- /// 0.0 - 1.0
- pub voice_activity: f32,
- pub video_status: VideoStatus,
- pub screen_share_status: ScreenShareStatus,
- pub in_current_project: bool,
- pub disconnected: bool,
- pub following: Option<Vec<Player>>,
- pub followers: Option<Vec<Player>>,
-}
-
-impl PlayerCallStatus {
- pub fn new() -> Self {
- Self {
- mic_status: MicStatus::default(),
- voice_activity: 0.,
- video_status: VideoStatus::default(),
- screen_share_status: ScreenShareStatus::default(),
- in_current_project: true,
- disconnected: false,
- following: None,
- followers: None,
- }
- }
-}
-
-#[derive(PartialEq, Clone)]
-pub struct Player {
- index: usize,
- avatar_src: String,
- username: String,
- status: PlayerStatus,
-}
-
-#[derive(Clone)]
-pub struct PlayerWithCallStatus {
- player: Player,
- call_status: PlayerCallStatus,
-}
-
-impl PlayerWithCallStatus {
- pub fn new(player: Player, call_status: PlayerCallStatus) -> Self {
- Self {
- player,
- call_status,
- }
- }
-
- pub fn get_player(&self) -> &Player {
- &self.player
- }
-
- pub fn get_call_status(&self) -> &PlayerCallStatus {
- &self.call_status
- }
-}
-
-impl Player {
- pub fn new(index: usize, avatar_src: String, username: String) -> Self {
- Self {
- index,
- avatar_src,
- username,
- status: Default::default(),
- }
- }
-
- pub fn set_status(mut self, status: PlayerStatus) -> Self {
- self.status = status;
- self
- }
-
- pub fn cursor_color(&self, cx: &mut WindowContext) -> Hsla {
- cx.theme().styles.player.0[self.index % cx.theme().styles.player.0.len()].cursor
- }
-
- pub fn selection_color(&self, cx: &mut WindowContext) -> Hsla {
- cx.theme().styles.player.0[self.index % cx.theme().styles.player.0.len()].selection
- }
-
- pub fn avatar_src(&self) -> &str {
- &self.avatar_src
- }
-
- pub fn index(&self) -> usize {
- self.index
- }
-}
@@ -1,67 +0,0 @@
-use gpui::{Div, RenderOnce};
-
-use crate::prelude::*;
-use crate::{Avatar, Facepile, PlayerWithCallStatus};
-
-#[derive(RenderOnce)]
-pub struct PlayerStack {
- player_with_call_status: PlayerWithCallStatus,
-}
-
-impl Component for PlayerStack {
- type Rendered = Div;
-
- fn render(self, cx: &mut WindowContext) -> Self::Rendered {
- let player = self.player_with_call_status.get_player();
-
- let followers = self
- .player_with_call_status
- .get_call_status()
- .followers
- .as_ref()
- .map(|followers| followers.clone());
-
- // if we have no followers return a slightly different element
- // if mic_status == muted add a red ring to avatar
-
- div()
- .h_full()
- .flex()
- .flex_col()
- .gap_px()
- .justify_center()
- .child(
- div()
- .flex()
- .justify_center()
- .w_full()
- .child(div().w_4().h_0p5().rounded_sm().bg(player.cursor_color(cx))),
- )
- .child(
- div()
- .flex()
- .items_center()
- .justify_center()
- .h_6()
- .pl_1()
- .rounded_lg()
- .bg(if followers.is_none() {
- cx.theme().styles.system.transparent
- } else {
- player.selection_color(cx)
- })
- .child(Avatar::new(player.avatar_src().to_string()))
- .children(followers.map(|followers| {
- div().neg_ml_2().child(Facepile::new(followers.into_iter()))
- })),
- )
- }
-}
-
-impl PlayerStack {
- pub fn new(player_with_call_status: PlayerWithCallStatus) -> Self {
- Self {
- player_with_call_status,
- }
- }
-}
@@ -0,0 +1,89 @@
+use gpui::{
+ AnyElement, Div, Element, ElementId, IntoElement, ParentElement, RenderOnce, Styled,
+ WindowContext,
+};
+use smallvec::SmallVec;
+
+use crate::{v_stack, StyledExt};
+
+/// A popover is used to display a menu or show some options.
+///
+/// Clicking the element that launches the popover should not change the current view,
+/// and the popover should be statically positioned relative to that element (not the
+/// user's mouse.)
+///
+/// Example: A "new" menu with options like "new file", "new folder", etc,
+/// Linear's "Display" menu, a profile menu that appers when you click your avatar.
+///
+/// Related elements:
+///
+/// `ContextMenu`:
+///
+/// Used to display a popover menu that only contains a list of items. Context menus are always
+/// launched by secondary clicking on an element. The menu is positioned relative to the user's cursor.
+///
+/// Example: Right clicking a file in the file tree to get a list of actions, right clicking
+/// a tab to in the tab bar to get a list of actions.
+///
+/// `Dropdown`:
+///
+/// Used to display a list of options when the user clicks an element. The menu is
+/// positioned relative the element that was clicked, and clicking an item in the
+/// dropdown should change the value of the element that was clicked.
+///
+/// Example: A theme select control. Displays "One Dark", clicking it opens a list of themes.
+/// When one is selected, the theme select control displays the selected theme.
+#[derive(IntoElement)]
+pub struct Popover {
+ children: SmallVec<[AnyElement; 2]>,
+ aside: Option<AnyElement>,
+}
+
+impl RenderOnce for Popover {
+ type Rendered = Div;
+
+ fn render(self, cx: &mut WindowContext) -> Self::Rendered {
+ v_stack()
+ .relative()
+ .elevation_2(cx)
+ .p_1()
+ .children(self.children)
+ .when_some(self.aside, |this, aside| {
+ // TODO: This will statically position the aside to the top right of the popover.
+ // We should update this to use gpui2::overlay avoid collisions with the window edges.
+ this.child(
+ v_stack()
+ .top_0()
+ .left_full()
+ .ml_1()
+ .absolute()
+ .elevation_2(cx)
+ .p_1()
+ .child(aside),
+ )
+ })
+ }
+}
+
+impl Popover {
+ pub fn new() -> Self {
+ Self {
+ children: SmallVec::new(),
+ aside: None,
+ }
+ }
+
+ pub fn aside(mut self, aside: impl IntoElement) -> Self
+ where
+ Self: Sized,
+ {
+ self.aside = Some(aside.into_element().into_any());
+ self
+ }
+}
+
+impl ParentElement for Popover {
+ fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
+ &mut self.children
+ }
+}
@@ -0,0 +1,19 @@
+mod avatar;
+mod button;
+mod checkbox;
+mod context_menu;
+mod icon;
+mod input;
+mod keybinding;
+mod label;
+mod list_item;
+
+pub use avatar::*;
+pub use button::*;
+pub use checkbox::*;
+pub use context_menu::*;
+pub use icon::*;
+pub use input::*;
+pub use keybinding::*;
+pub use label::*;
+pub use list_item::*;
@@ -0,0 +1,23 @@
+use gpui::{Div, Render};
+use story::Story;
+
+use crate::prelude::*;
+use crate::Avatar;
+
+pub struct AvatarStory;
+
+impl Render for AvatarStory {
+ type Element = Div;
+
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+ Story::container()
+ .child(Story::title_for::<Avatar>())
+ .child(Story::label("Default"))
+ .child(Avatar::uri(
+ "https://avatars.githubusercontent.com/u/1714999?v=4",
+ ))
+ .child(Avatar::uri(
+ "https://avatars.githubusercontent.com/u/326587?v=4",
+ ))
+ }
+}
@@ -0,0 +1,145 @@
+use gpui::{rems, Div, Render};
+use story::Story;
+use strum::IntoEnumIterator;
+
+use crate::prelude::*;
+use crate::{h_stack, v_stack, Button, Icon, IconPosition, Label};
+
+pub struct ButtonStory;
+
+impl Render for ButtonStory {
+ type Element = Div;
+
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+ let states = InteractionState::iter();
+
+ Story::container()
+ .child(Story::title_for::<Button>())
+ .child(
+ div()
+ .flex()
+ .gap_8()
+ .child(
+ div()
+ .child(Story::label("Ghost (Default)"))
+ .child(h_stack().gap_2().children(states.clone().map(|state| {
+ v_stack()
+ .gap_1()
+ .child(Label::new(state.to_string()).color(Color::Muted))
+ .child(
+ Button::new("Label").variant(ButtonVariant::Ghost), // .state(state),
+ )
+ })))
+ .child(Story::label("Ghost – Left Icon"))
+ .child(h_stack().gap_2().children(states.clone().map(|state| {
+ v_stack()
+ .gap_1()
+ .child(Label::new(state.to_string()).color(Color::Muted))
+ .child(
+ Button::new("Label")
+ .variant(ButtonVariant::Ghost)
+ .icon(Icon::Plus)
+ .icon_position(IconPosition::Left), // .state(state),
+ )
+ })))
+ .child(Story::label("Ghost – Right Icon"))
+ .child(h_stack().gap_2().children(states.clone().map(|state| {
+ v_stack()
+ .gap_1()
+ .child(Label::new(state.to_string()).color(Color::Muted))
+ .child(
+ Button::new("Label")
+ .variant(ButtonVariant::Ghost)
+ .icon(Icon::Plus)
+ .icon_position(IconPosition::Right), // .state(state),
+ )
+ }))),
+ )
+ .child(
+ div()
+ .child(Story::label("Filled"))
+ .child(h_stack().gap_2().children(states.clone().map(|state| {
+ v_stack()
+ .gap_1()
+ .child(Label::new(state.to_string()).color(Color::Muted))
+ .child(
+ Button::new("Label").variant(ButtonVariant::Filled), // .state(state),
+ )
+ })))
+ .child(Story::label("Filled – Left Button"))
+ .child(h_stack().gap_2().children(states.clone().map(|state| {
+ v_stack()
+ .gap_1()
+ .child(Label::new(state.to_string()).color(Color::Muted))
+ .child(
+ Button::new("Label")
+ .variant(ButtonVariant::Filled)
+ .icon(Icon::Plus)
+ .icon_position(IconPosition::Left), // .state(state),
+ )
+ })))
+ .child(Story::label("Filled – Right Button"))
+ .child(h_stack().gap_2().children(states.clone().map(|state| {
+ v_stack()
+ .gap_1()
+ .child(Label::new(state.to_string()).color(Color::Muted))
+ .child(
+ Button::new("Label")
+ .variant(ButtonVariant::Filled)
+ .icon(Icon::Plus)
+ .icon_position(IconPosition::Right), // .state(state),
+ )
+ }))),
+ )
+ .child(
+ div()
+ .child(Story::label("Fixed With"))
+ .child(h_stack().gap_2().children(states.clone().map(|state| {
+ v_stack()
+ .gap_1()
+ .child(Label::new(state.to_string()).color(Color::Muted))
+ .child(
+ Button::new("Label")
+ .variant(ButtonVariant::Filled)
+ // .state(state)
+ .width(Some(rems(6.).into())),
+ )
+ })))
+ .child(Story::label("Fixed With – Left Icon"))
+ .child(h_stack().gap_2().children(states.clone().map(|state| {
+ v_stack()
+ .gap_1()
+ .child(Label::new(state.to_string()).color(Color::Muted))
+ .child(
+ Button::new("Label")
+ .variant(ButtonVariant::Filled)
+ // .state(state)
+ .icon(Icon::Plus)
+ .icon_position(IconPosition::Left)
+ .width(Some(rems(6.).into())),
+ )
+ })))
+ .child(Story::label("Fixed With – Right Icon"))
+ .child(h_stack().gap_2().children(states.clone().map(|state| {
+ v_stack()
+ .gap_1()
+ .child(Label::new(state.to_string()).color(Color::Muted))
+ .child(
+ Button::new("Label")
+ .variant(ButtonVariant::Filled)
+ // .state(state)
+ .icon(Icon::Plus)
+ .icon_position(IconPosition::Right)
+ .width(Some(rems(6.).into())),
+ )
+ }))),
+ ),
+ )
+ .child(Story::label("Button with `on_click`"))
+ .child(
+ Button::new("Label")
+ .variant(ButtonVariant::Ghost)
+ .on_click(|_, cx| println!("Button clicked.")),
+ )
+ }
+}
@@ -0,0 +1,49 @@
+use gpui::{Div, Render, ViewContext};
+use story::Story;
+
+use crate::prelude::*;
+use crate::{h_stack, Checkbox};
+
+pub struct CheckboxStory;
+
+impl Render for CheckboxStory {
+ type Element = Div;
+
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+ Story::container()
+ .child(Story::title_for::<Checkbox>())
+ .child(Story::label("Default"))
+ .child(
+ h_stack()
+ .p_2()
+ .gap_2()
+ .rounded_md()
+ .border()
+ .border_color(cx.theme().colors().border)
+ .child(Checkbox::new("checkbox-enabled", Selection::Unselected))
+ .child(Checkbox::new(
+ "checkbox-intermediate",
+ Selection::Indeterminate,
+ ))
+ .child(Checkbox::new("checkbox-selected", Selection::Selected)),
+ )
+ .child(Story::label("Disabled"))
+ .child(
+ h_stack()
+ .p_2()
+ .gap_2()
+ .rounded_md()
+ .border()
+ .border_color(cx.theme().colors().border)
+ .child(Checkbox::new("checkbox-disabled", Selection::Unselected).disabled(true))
+ .child(
+ Checkbox::new("checkbox-disabled-intermediate", Selection::Indeterminate)
+ .disabled(true),
+ )
+ .child(
+ Checkbox::new("checkbox-disabled-selected", Selection::Selected)
+ .disabled(true),
+ ),
+ )
+ }
+}
@@ -0,0 +1,104 @@
+use gpui::{actions, Action, AnchorCorner, Div, Render, View};
+use story::Story;
+
+use crate::prelude::*;
+use crate::{menu_handle, ContextMenu, Label};
+
+actions!(PrintCurrentDate, PrintBestFood);
+
+fn build_menu(cx: &mut WindowContext, header: impl Into<SharedString>) -> View<ContextMenu> {
+ ContextMenu::build(cx, |menu, _| {
+ menu.header(header)
+ .separator()
+ .entry("Print current time", |v, cx| {
+ println!("dispatching PrintCurrentTime action");
+ cx.dispatch_action(PrintCurrentDate.boxed_clone())
+ })
+ .entry("Print best foot", |v, cx| {
+ cx.dispatch_action(PrintBestFood.boxed_clone())
+ })
+ })
+}
+
+pub struct ContextMenuStory;
+
+impl Render for ContextMenuStory {
+ type Element = Div;
+
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+ Story::container()
+ .on_action(|_: &PrintCurrentDate, _| {
+ println!("printing unix time!");
+ if let Ok(unix_time) = std::time::UNIX_EPOCH.elapsed() {
+ println!("Current Unix time is {:?}", unix_time.as_secs());
+ }
+ })
+ .on_action(|_: &PrintBestFood, _| {
+ println!("burrito");
+ })
+ .flex()
+ .flex_row()
+ .justify_between()
+ .child(
+ div()
+ .flex()
+ .flex_col()
+ .justify_between()
+ .child(
+ menu_handle("test2")
+ .child(|is_open| {
+ Label::new(if is_open {
+ "TOP LEFT"
+ } else {
+ "RIGHT CLICK ME"
+ })
+ })
+ .menu(move |cx| build_menu(cx, "top left")),
+ )
+ .child(
+ menu_handle("test1")
+ .child(|is_open| {
+ Label::new(if is_open {
+ "BOTTOM LEFT"
+ } else {
+ "RIGHT CLICK ME"
+ })
+ })
+ .anchor(AnchorCorner::BottomLeft)
+ .attach(AnchorCorner::TopLeft)
+ .menu(move |cx| build_menu(cx, "bottom left")),
+ ),
+ )
+ .child(
+ div()
+ .flex()
+ .flex_col()
+ .justify_between()
+ .child(
+ menu_handle("test3")
+ .child(|is_open| {
+ Label::new(if is_open {
+ "TOP RIGHT"
+ } else {
+ "RIGHT CLICK ME"
+ })
+ })
+ .anchor(AnchorCorner::TopRight)
+ .menu(move |cx| build_menu(cx, "top right")),
+ )
+ .child(
+ menu_handle("test4")
+ .child(|is_open| {
+ Label::new(if is_open {
+ "BOTTOM RIGHT"
+ } else {
+ "RIGHT CLICK ME"
+ })
+ })
+ .anchor(AnchorCorner::BottomRight)
+ .attach(AnchorCorner::TopRight)
+ .menu(move |cx| build_menu(cx, "bottom right")),
+ ),
+ )
+ }
+}
@@ -0,0 +1,21 @@
+use gpui::{Div, Render};
+use story::Story;
+use strum::IntoEnumIterator;
+
+use crate::prelude::*;
+use crate::{Icon, IconElement};
+
+pub struct IconStory;
+
+impl Render for IconStory {
+ type Element = Div;
+
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+ let icons = Icon::iter();
+
+ Story::container()
+ .child(Story::title_for::<IconElement>())
+ .child(Story::label("All Icons"))
+ .child(div().flex().gap_3().children(icons.map(IconElement::new)))
+ }
+}
@@ -0,0 +1,18 @@
+use gpui::{Div, Render};
+use story::Story;
+
+use crate::prelude::*;
+use crate::Input;
+
+pub struct InputStory;
+
+impl Render for InputStory {
+ type Element = Div;
+
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+ Story::container()
+ .child(Story::title_for::<Input>())
+ .child(Story::label("Default"))
+ .child(div().flex().child(Input::new("Search")))
+ }
+}
@@ -0,0 +1,61 @@
+use gpui::{actions, Div, Render};
+use itertools::Itertools;
+use story::Story;
+
+use crate::prelude::*;
+use crate::KeyBinding;
+
+pub struct KeybindingStory;
+
+actions!(NoAction);
+
+pub fn binding(key: &str) -> gpui::KeyBinding {
+ gpui::KeyBinding::new(key, NoAction {}, None)
+}
+
+impl Render for KeybindingStory {
+ type Element = Div;
+
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+ let all_modifier_permutations = ["ctrl", "alt", "cmd", "shift"].into_iter().permutations(2);
+
+ Story::container()
+ .child(Story::title_for::<KeyBinding>())
+ .child(Story::label("Single Key"))
+ .child(KeyBinding::new(binding("Z")))
+ .child(Story::label("Single Key with Modifier"))
+ .child(
+ div()
+ .flex()
+ .gap_3()
+ .child(KeyBinding::new(binding("ctrl-c")))
+ .child(KeyBinding::new(binding("alt-c")))
+ .child(KeyBinding::new(binding("cmd-c")))
+ .child(KeyBinding::new(binding("shift-c"))),
+ )
+ .child(Story::label("Single Key with Modifier (Permuted)"))
+ .child(
+ div().flex().flex_col().children(
+ all_modifier_permutations
+ .chunks(4)
+ .into_iter()
+ .map(|chunk| {
+ div()
+ .flex()
+ .gap_4()
+ .py_3()
+ .children(chunk.map(|permutation| {
+ KeyBinding::new(binding(&*(permutation.join("-") + "-x")))
+ }))
+ }),
+ ),
+ )
+ .child(Story::label("Single Key with All Modifiers"))
+ .child(KeyBinding::new(binding("ctrl-alt-cmd-shift-z")))
+ .child(Story::label("Chord"))
+ .child(KeyBinding::new(binding("a z")))
+ .child(Story::label("Chord with Modifier"))
+ .child(KeyBinding::new(binding("ctrl-a shift-z")))
+ .child(KeyBinding::new(binding("fn-s")))
+ }
+}
@@ -0,0 +1,27 @@
+use gpui::{Div, Render};
+use story::Story;
+
+use crate::prelude::*;
+use crate::{HighlightedLabel, Label};
+
+pub struct LabelStory;
+
+impl Render for LabelStory {
+ type Element = Div;
+
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+ Story::container()
+ .child(Story::title_for::<Label>())
+ .child(Story::label("Default"))
+ .child(Label::new("Hello, world!"))
+ .child(Story::label("Highlighted"))
+ .child(HighlightedLabel::new(
+ "Hello, world!",
+ vec![0, 1, 2, 7, 8, 12],
+ ))
+ .child(HighlightedLabel::new(
+ "Héllo, world!",
+ vec![0, 1, 3, 8, 9, 13],
+ ))
+ }
+}
@@ -0,0 +1,26 @@
+use gpui::{Div, Render};
+use story::Story;
+
+use crate::prelude::*;
+use crate::ListItem;
+
+pub struct ListItemStory;
+
+impl Render for ListItemStory {
+ type Element = Div;
+
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+ Story::container()
+ .child(Story::title_for::<ListItem>())
+ .child(Story::label("Default"))
+ .child(ListItem::new("hello_world").child("Hello, world!"))
+ .child(Story::label("With `on_click`"))
+ .child(
+ ListItem::new("with_on_click")
+ .child("Click me")
+ .on_click(|_event, _cx| {
+ println!("Clicked!");
+ }),
+ )
+ }
+}
@@ -1,276 +0,0 @@
-// use crate::prelude::*;
-// use crate::{Icon, IconElement, Label, TextColor};
-// use gpui::{prelude::*, red, Div, ElementId, Render, RenderOnce, View};
-
-// #[derive(RenderOnce, Clone)]
-// pub struct Tab {
-// id: ElementId,
-// title: String,
-// icon: Option<Icon>,
-// current: bool,
-// dirty: bool,
-// fs_status: FileSystemStatus,
-// git_status: GitStatus,
-// diagnostic_status: DiagnosticStatus,
-// close_side: IconSide,
-// }
-
-// #[derive(Clone, Debug)]
-// struct TabDragState {
-// title: String,
-// }
-
-// impl Render for TabDragState {
-// type Element = Div;
-
-// fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
-// div().w_8().h_4().bg(red())
-// }
-// }
-
-// impl Component for Tab {
-// type Rendered = gpui::Stateful<Div>;
-
-// fn render(self, cx: &mut WindowContext) -> Self::Rendered {
-// let has_fs_conflict = self.fs_status == FileSystemStatus::Conflict;
-// let is_deleted = self.fs_status == FileSystemStatus::Deleted;
-
-// let label = match (self.git_status, is_deleted) {
-// (_, true) | (GitStatus::Deleted, false) => Label::new(self.title.clone())
-// .color(TextColor::Hidden)
-// .set_strikethrough(true),
-// (GitStatus::None, false) => Label::new(self.title.clone()),
-// (GitStatus::Created, false) => Label::new(self.title.clone()).color(TextColor::Created),
-// (GitStatus::Modified, false) => {
-// Label::new(self.title.clone()).color(TextColor::Modified)
-// }
-// (GitStatus::Renamed, false) => Label::new(self.title.clone()).color(TextColor::Accent),
-// (GitStatus::Conflict, false) => Label::new(self.title.clone()),
-// };
-
-// let close_icon = || IconElement::new(Icon::Close).color(TextColor::Muted);
-
-// let (tab_bg, tab_hover_bg, tab_active_bg) = match self.current {
-// false => (
-// cx.theme().colors().tab_inactive_background,
-// cx.theme().colors().ghost_element_hover,
-// cx.theme().colors().ghost_element_active,
-// ),
-// true => (
-// cx.theme().colors().tab_active_background,
-// cx.theme().colors().element_hover,
-// cx.theme().colors().element_active,
-// ),
-// };
-
-// let drag_state = TabDragState {
-// title: self.title.clone(),
-// };
-
-// div()
-// .id(self.id.clone())
-// .on_drag(move |_view, cx| cx.build_view(|cx| drag_state.clone()))
-// .drag_over::<TabDragState>(|d| d.bg(cx.theme().colors().drop_target_background))
-// .on_drop(|_view, state: View<TabDragState>, cx| {
-// eprintln!("{:?}", state.read(cx));
-// })
-// .px_2()
-// .py_0p5()
-// .flex()
-// .items_center()
-// .justify_center()
-// .bg(tab_bg)
-// .hover(|h| h.bg(tab_hover_bg))
-// .active(|a| a.bg(tab_active_bg))
-// .child(
-// div()
-// .px_1()
-// .flex()
-// .items_center()
-// .gap_1p5()
-// .children(has_fs_conflict.then(|| {
-// IconElement::new(Icon::ExclamationTriangle)
-// .size(crate::IconSize::Small)
-// .color(TextColor::Warning)
-// }))
-// .children(self.icon.map(IconElement::new))
-// .children(if self.close_side == IconSide::Left {
-// Some(close_icon())
-// } else {
-// None
-// })
-// .child(label)
-// .children(if self.close_side == IconSide::Right {
-// Some(close_icon())
-// } else {
-// None
-// }),
-// )
-// }
-// }
-
-// impl Tab {
-// pub fn new(id: impl Into<ElementId>) -> Self {
-// Self {
-// id: id.into(),
-// title: "untitled".to_string(),
-// icon: None,
-// current: false,
-// dirty: false,
-// fs_status: FileSystemStatus::None,
-// git_status: GitStatus::None,
-// diagnostic_status: DiagnosticStatus::None,
-// close_side: IconSide::Right,
-// }
-// }
-
-// pub fn current(mut self, current: bool) -> Self {
-// self.current = current;
-// self
-// }
-
-// pub fn title(mut self, title: String) -> Self {
-// self.title = title;
-// self
-// }
-
-// pub fn icon<I>(mut self, icon: I) -> Self
-// where
-// I: Into<Option<Icon>>,
-// {
-// self.icon = icon.into();
-// self
-// }
-
-// pub fn dirty(mut self, dirty: bool) -> Self {
-// self.dirty = dirty;
-// self
-// }
-
-// pub fn fs_status(mut self, fs_status: FileSystemStatus) -> Self {
-// self.fs_status = fs_status;
-// self
-// }
-
-// pub fn git_status(mut self, git_status: GitStatus) -> Self {
-// self.git_status = git_status;
-// self
-// }
-
-// pub fn diagnostic_status(mut self, diagnostic_status: DiagnosticStatus) -> Self {
-// self.diagnostic_status = diagnostic_status;
-// self
-// }
-
-// pub fn close_side(mut self, close_side: IconSide) -> Self {
-// self.close_side = close_side;
-// self
-// }
-// }
-
-// #[cfg(feature = "stories")]
-// pub use stories::*;
-
-// #[cfg(feature = "stories")]
-// mod stories {
-// use super::*;
-// use crate::{h_stack, v_stack, Icon, Story};
-// use strum::IntoEnumIterator;
-
-// pub struct TabStory;
-
-// impl Render for TabStory {
-// type Element = Div;
-
-// fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
-// let git_statuses = GitStatus::iter();
-// let fs_statuses = FileSystemStatus::iter();
-
-// Story::container(cx)
-// .child(Story::title_for::<_, Tab>(cx))
-// .child(
-// h_stack().child(
-// v_stack()
-// .gap_2()
-// .child(Story::label(cx, "Default"))
-// .child(Tab::new("default")),
-// ),
-// )
-// .child(
-// h_stack().child(
-// v_stack().gap_2().child(Story::label(cx, "Current")).child(
-// h_stack()
-// .gap_4()
-// .child(
-// Tab::new("current")
-// .title("Current".to_string())
-// .current(true),
-// )
-// .child(
-// Tab::new("not_current")
-// .title("Not Current".to_string())
-// .current(false),
-// ),
-// ),
-// ),
-// )
-// .child(
-// h_stack().child(
-// v_stack()
-// .gap_2()
-// .child(Story::label(cx, "Titled"))
-// .child(Tab::new("titled").title("label".to_string())),
-// ),
-// )
-// .child(
-// h_stack().child(
-// v_stack()
-// .gap_2()
-// .child(Story::label(cx, "With Icon"))
-// .child(
-// Tab::new("with_icon")
-// .title("label".to_string())
-// .icon(Some(Icon::Envelope)),
-// ),
-// ),
-// )
-// .child(
-// h_stack().child(
-// v_stack()
-// .gap_2()
-// .child(Story::label(cx, "Close Side"))
-// .child(
-// h_stack()
-// .gap_4()
-// .child(
-// Tab::new("left")
-// .title("Left".to_string())
-// .close_side(IconSide::Left),
-// )
-// .child(Tab::new("right").title("Right".to_string())),
-// ),
-// ),
-// )
-// .child(
-// v_stack()
-// .gap_2()
-// .child(Story::label(cx, "Git Status"))
-// .child(h_stack().gap_4().children(git_statuses.map(|git_status| {
-// Tab::new("git_status")
-// .title(git_status.to_string())
-// .git_status(git_status)
-// }))),
-// )
-// .child(
-// v_stack()
-// .gap_2()
-// .child(Story::label(cx, "File System Status"))
-// .child(h_stack().gap_4().children(fs_statuses.map(|fs_status| {
-// Tab::new("file_system_status")
-// .title(fs_status.to_string())
-// .fs_status(fs_status)
-// }))),
-// )
-// }
-// }
-// }
@@ -1,117 +0,0 @@
-use crate::prelude::*;
-use gpui::{prelude::*, AnyElement, RenderOnce};
-use gpui::{Div, Element};
-use smallvec::SmallVec;
-
-#[derive(Default, Debug, PartialEq, Eq, Clone, Copy)]
-pub enum ToastOrigin {
- #[default]
- Bottom,
- BottomRight,
-}
-
-/// Don't use toast directly:
-///
-/// - For messages with a required action, use a `NotificationToast`.
-/// - For messages that convey information, use a `StatusToast`.
-///
-/// A toast is a small, temporary window that appears to show a message to the user
-/// or indicate a required action.
-///
-/// Toasts should not persist on the screen for more than a few seconds unless
-/// they are actively showing the a process in progress.
-///
-/// Only one toast may be visible at a time.
-#[derive(RenderOnce)]
-pub struct Toast {
- origin: ToastOrigin,
- children: SmallVec<[AnyElement; 2]>,
-}
-
-impl Component for Toast {
- type Rendered = Div;
-
- fn render(self, cx: &mut WindowContext) -> Self::Rendered {
- let mut div = div();
-
- if self.origin == ToastOrigin::Bottom {
- div = div.right_1_2();
- } else {
- div = div.right_2();
- }
-
- div.z_index(5)
- .absolute()
- .bottom_9()
- .flex()
- .py_1()
- .px_1p5()
- .rounded_lg()
- .shadow_md()
- .overflow_hidden()
- .bg(cx.theme().colors().elevated_surface_background)
- .children(self.children)
- }
-}
-
-impl Toast {
- pub fn new(origin: ToastOrigin) -> Self {
- Self {
- origin,
- children: SmallVec::new(),
- }
- }
-
- fn render(self, cx: &mut WindowContext) -> impl Element {
- let mut div = div();
-
- if self.origin == ToastOrigin::Bottom {
- div = div.right_1_2();
- } else {
- div = div.right_2();
- }
-
- div.z_index(5)
- .absolute()
- .bottom_9()
- .flex()
- .py_1()
- .px_1p5()
- .rounded_lg()
- .shadow_md()
- .overflow_hidden()
- .bg(cx.theme().colors().elevated_surface_background)
- .children(self.children)
- }
-}
-
-impl ParentElement for Toast {
- fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
- &mut self.children
- }
-}
-
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use gpui::{Div, Render};
-
- use crate::{Label, Story};
-
- use super::*;
-
- pub struct ToastStory;
-
- impl Render for ToastStory {
- type Element = Div;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- Story::container(cx)
- .child(Story::title_for::<Toast>(cx))
- .child(Story::label(cx, "Default"))
- .child(Toast::new(ToastOrigin::Bottom).child(Label::new("label")))
- }
- }
-}
@@ -1,7 +1,3 @@
-use gpui::{div, Element, ParentElement};
-
-use crate::{Icon, IconElement, IconSize, TextColor};
-
/// Whether the entry is toggleable, and if so, whether it is currently toggled.
///
/// To make an element toggleable, simply add a `Toggle::Toggled(_)` and handle it's cases.
@@ -43,19 +39,3 @@ impl From<bool> for Toggle {
Toggle::Toggled(toggled)
}
}
-
-pub fn disclosure_control(toggle: Toggle) -> impl Element {
- match (toggle.is_toggleable(), toggle.is_toggled()) {
- (false, _) => div(),
- (_, true) => div().child(
- IconElement::new(Icon::ChevronDown)
- .color(TextColor::Muted)
- .size(IconSize::Small),
- ),
- (_, false) => div().child(
- IconElement::new(Icon::ChevronRight)
- .color(TextColor::Muted)
- .size(IconSize::Small),
- ),
- }
-}
@@ -1,23 +0,0 @@
-use crate::prelude::*;
-use gpui::{Div, RenderOnce};
-
-#[derive(RenderOnce)]
-pub struct ToolDivider;
-
-impl Component for ToolDivider {
- type Rendered = Div;
-
- fn render(self, cx: &mut WindowContext) -> Self::Rendered {
- div().w_px().h_3().bg(cx.theme().colors().border)
- }
-}
-
-impl ToolDivider {
- pub fn new() -> Self {
- Self
- }
-
- fn render(self, cx: &mut WindowContext) -> impl Element {
- div().w_px().h_3().bg(cx.theme().colors().border)
- }
-}
@@ -1,9 +1,9 @@
-use gpui::{overlay, Action, AnyView, Overlay, Render, RenderOnce, VisualContext};
+use gpui::{overlay, Action, AnyView, IntoElement, Overlay, Render, VisualContext};
use settings2::Settings;
use theme2::{ActiveTheme, ThemeSettings};
use crate::prelude::*;
-use crate::{h_stack, v_stack, KeyBinding, Label, LabelSize, StyledExt, TextColor};
+use crate::{h_stack, v_stack, Color, KeyBinding, Label, LabelSize, StyledExt};
pub struct Tooltip {
title: SharedString,
@@ -90,11 +90,7 @@ impl Render for Tooltip {
}),
)
.when_some(self.meta.clone(), |this, meta| {
- this.child(
- Label::new(meta)
- .size(LabelSize::Small)
- .color(TextColor::Muted),
- )
+ this.child(Label::new(meta).size(LabelSize::Small).color(Color::Muted))
}),
),
)
@@ -1,116 +1,14 @@
-use gpui::rems;
-use gpui::Rems;
pub use gpui::{
- div, Component, Element, ElementId, InteractiveElement, ParentElement, SharedString, Styled,
+ div, Element, ElementId, InteractiveElement, ParentElement, RenderOnce, SharedString, Styled,
ViewContext, WindowContext,
};
-pub use crate::elevation::*;
pub use crate::StyledExt;
-pub use crate::{ButtonVariant, TextColor};
+pub use crate::{ButtonVariant, Color};
pub use theme2::ActiveTheme;
-use gpui::Hsla;
use strum::EnumIter;
-#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
-pub enum UITextSize {
- /// The default size for UI text.
- ///
- /// `0.825rem` or `14px` at the default scale of `1rem` = `16px`.
- ///
- /// Note: The absolute size of this text will change based on a user's `ui_scale` setting.
- #[default]
- Default,
- /// The small size for UI text.
- ///
- /// `0.75rem` or `12px` at the default scale of `1rem` = `16px`.
- ///
- /// Note: The absolute size of this text will change based on a user's `ui_scale` setting.
- Small,
-}
-
-impl UITextSize {
- pub fn rems(self) -> Rems {
- match self {
- Self::Default => rems(0.875),
- Self::Small => rems(0.75),
- }
- }
-}
-
-#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
-pub enum FileSystemStatus {
- #[default]
- None,
- Conflict,
- Deleted,
-}
-
-impl std::fmt::Display for FileSystemStatus {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(
- f,
- "{}",
- match self {
- Self::None => "None",
- Self::Conflict => "Conflict",
- Self::Deleted => "Deleted",
- }
- )
- }
-}
-
-#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
-pub enum GitStatus {
- #[default]
- None,
- Created,
- Modified,
- Deleted,
- Conflict,
- Renamed,
-}
-
-impl GitStatus {
- pub fn hsla(&self, cx: &WindowContext) -> Hsla {
- match self {
- Self::None => cx.theme().system().transparent,
- Self::Created => cx.theme().status().created,
- Self::Modified => cx.theme().status().modified,
- Self::Deleted => cx.theme().status().deleted,
- Self::Conflict => cx.theme().status().conflict,
- Self::Renamed => cx.theme().status().renamed,
- }
- }
-}
-
-impl std::fmt::Display for GitStatus {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(
- f,
- "{}",
- match self {
- Self::None => "None",
- Self::Created => "Created",
- Self::Modified => "Modified",
- Self::Deleted => "Deleted",
- Self::Conflict => "Conflict",
- Self::Renamed => "Renamed",
- }
- )
- }
-}
-
-#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
-pub enum DiagnosticStatus {
- #[default]
- None,
- Error,
- Warning,
- Info,
-}
-
#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
pub enum IconSide {
#[default]
@@ -118,45 +16,6 @@ pub enum IconSide {
Right,
}
-#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
-pub enum OrderMethod {
- #[default]
- Ascending,
- Descending,
- MostRecent,
-}
-
-#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
-pub enum Shape {
- #[default]
- Circle,
- RoundedRectangle,
-}
-
-#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
-pub enum DisclosureControlVisibility {
- #[default]
- OnHover,
- Always,
-}
-
-#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
-pub enum DisclosureControlStyle {
- /// Shows the disclosure control only when hovered where possible.
- ///
- /// More compact, but not available everywhere.
- ChevronOnHover,
- /// Shows an icon where possible, otherwise shows a chevron.
- ///
- /// For example, in a file tree a folder or file icon is shown
- /// instead of a chevron
- Icon,
- /// Always shows a chevron.
- Chevron,
- /// Completely hides the disclosure control where possible.
- None,
-}
-
#[derive(Debug, PartialEq, Eq, Clone, Copy, EnumIter)]
pub enum OverflowStyle {
Hidden,
@@ -165,12 +24,22 @@ pub enum OverflowStyle {
#[derive(Default, PartialEq, Copy, Clone, EnumIter, strum::Display)]
pub enum InteractionState {
+ /// An element that is enabled and not hovered, active, focused, or disabled.
+ ///
+ /// This is often referred to as the "default" state.
#[default]
Enabled,
+ /// An element that is hovered.
Hovered,
+ /// An element has an active mouse down or touch start event on it.
Active,
+ /// An element that is focused using the keyboard.
Focused,
+ /// An element that is disabled.
Disabled,
+ /// A toggleable element that is selected, like the active button in a
+ /// button toggle group.
+ Selected,
}
impl InteractionState {
@@ -1,37 +0,0 @@
-use gpui::Div;
-
-use crate::prelude::*;
-
-pub struct Story {}
-
-impl Story {
- pub fn container(cx: &mut gpui::WindowContext) -> Div {
- div()
- .size_full()
- .flex()
- .flex_col()
- .pt_2()
- .px_4()
- .bg(cx.theme().colors().background)
- }
-
- pub fn title(cx: &mut WindowContext, title: impl Into<SharedString>) -> impl Element {
- div()
- .text_xl()
- .text_color(cx.theme().colors().text)
- .child(title.into())
- }
-
- pub fn title_for<T>(cx: &mut WindowContext) -> impl Element {
- Self::title(cx, std::any::type_name::<T>())
- }
-
- pub fn label(cx: &mut WindowContext, label: impl Into<SharedString>) -> impl Element {
- div()
- .mt_4()
- .mb_2()
- .text_xs()
- .text_color(cx.theme().colors().text)
- .child(label.into())
- }
-}
@@ -78,8 +78,6 @@ pub trait StyledExt: Styled + Sized {
elevated(self, cx, ElevationIndex::ElevatedSurface)
}
- // There is no elevation 3, as the third elevation level is reserved for wash layers. See [`Elevation`](ui2::Elevation).
-
/// Modal Surfaces are used for elements that should appear above all other UI elements and are located above the wash layer. This is the maximum elevation at which UI elements can be rendered in their default state.
///
/// Elements rendered at this layer should have an enforced behavior: Any interaction outside of the modal will either dismiss the modal or prompt an action (Save your progress, etc) then dismiss the modal.
@@ -89,7 +87,7 @@ pub trait StyledExt: Styled + Sized {
/// Sets `bg()`, `rounded_lg()`, `border()`, `border_color()`, `shadow()`
///
/// Examples: Settings Modal, Channel Management, Wizards/Setup UI, Dialogs
- fn elevation_4(self, cx: &mut WindowContext) -> Self {
+ fn elevation_3(self, cx: &mut WindowContext) -> Self {
elevated(self, cx, ElevationIndex::ModalSurface)
}
}
@@ -0,0 +1,7 @@
+mod color;
+mod elevation;
+mod typography;
+
+pub use color::*;
+pub use elevation::*;
+pub use typography::*;
@@ -0,0 +1,44 @@
+use gpui::{Hsla, WindowContext};
+use theme2::ActiveTheme;
+
+#[derive(Default, PartialEq, Copy, Clone)]
+pub enum Color {
+ #[default]
+ Default,
+ Accent,
+ Created,
+ Deleted,
+ Disabled,
+ Error,
+ Hidden,
+ Info,
+ Modified,
+ Muted,
+ Placeholder,
+ Player(u32),
+ Selected,
+ Success,
+ Warning,
+}
+
+impl Color {
+ pub fn color(&self, cx: &WindowContext) -> Hsla {
+ match self {
+ Color::Default => cx.theme().colors().text,
+ Color::Muted => cx.theme().colors().text_muted,
+ Color::Created => cx.theme().status().created,
+ Color::Modified => cx.theme().status().modified,
+ Color::Deleted => cx.theme().status().deleted,
+ Color::Disabled => cx.theme().colors().text_disabled,
+ Color::Hidden => cx.theme().status().hidden,
+ Color::Info => cx.theme().status().info,
+ Color::Placeholder => cx.theme().colors().text_placeholder,
+ Color::Accent => cx.theme().colors().text_accent,
+ Color::Player(i) => cx.theme().styles.player.0[i.clone() as usize].cursor,
+ Color::Error => cx.theme().status().error,
+ Color::Selected => cx.theme().colors().text_accent,
+ Color::Success => cx.theme().status().success,
+ Color::Warning => cx.theme().status().warning,
+ }
+ }
+}
@@ -1,27 +1,10 @@
-TODO: Originally sourced from Material Design 3, Rewrite to be more Zed specific
-
# Elevation
-Zed applies elevation to all surfaces and components, which are categorized into levels.
-
-Elevation accomplishes the following:
-- Allows surfaces to move in front of or behind others, such as content scrolling beneath app top bars.
-- Reflects spatial relationships, for instance, how a floating action button’s shadow intimates its disconnection from a collection of cards.
-- Directs attention to structures at the highest elevation, like a temporary dialog arising in front of other surfaces.
-
-Elevations are the initial elevation values assigned to components by default.
-
-Components may transition to a higher elevation in some cases, like user interations.
-
-On such occasions, components transition to predetermined dynamic elevation offsets. These are the typical elevations to which components move when they are not at rest.
-
-## Understanding Elevation
-
Elevation can be thought of as the physical closeness of an element to the user. Elements with lower elevations are physically further away from the user on the z-axis and appear to be underneath elements with higher elevations.
Material Design 3 has a some great visualizations of elevation that may be helpful to understanding the mental modal of elevation. [Material Design – Elevation](https://m3.material.io/styles/elevation/overview)
-## Elevation
+## Elevation Levels
1. App Background (e.x.: Workspace, system window)
1. UI Surface (e.x.: Title Bar, Panel, Tab Bar)
@@ -59,27 +42,3 @@ Modal Surfaces are used for elements that should appear above all other UI eleme
Elements rendered at this layer have an enforced behavior: Any interaction outside of the modal will either dismiss the modal or prompt an action (Save your progress, etc) then dismiss the modal.
If the element does not have this behavior, it should be rendered at the Elevated Surface layer.
-
-## Layer
-Each elevation that can contain elements has its own set of layers that are nested within the elevations.
-
-1. TBD (Z -1 layer)
-1. Element (Text, button, surface, etc)
-1. Elevated Element (Popover, Context Menu, Tooltip)
-999. Dragged Element -> Highest Elevation
-
-Dragged elements jump to the highest elevation the app can render. An active drag should _always_ be the most foreground element in the app at any time.
-
-🚧 Work in Progress 🚧
-
-## Element
-Each elevation that can contain elements has it's own set of layers:
-
-1. Effects
-1. Background
-1. Tint
-1. Highlight
-1. Content
-1. Overlay
-
-🚧 Work in Progress 🚧
@@ -1,7 +1,7 @@
use gpui::{hsla, point, px, BoxShadow};
use smallvec::{smallvec, SmallVec};
-#[doc = include_str!("elevation.md")]
+#[doc = include_str!("docs/elevation.md")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Elevation {
ElevationIndex(ElevationIndex),
@@ -25,8 +25,8 @@ impl ElevationIndex {
ElevationIndex::Background => 0,
ElevationIndex::Surface => 100,
ElevationIndex::ElevatedSurface => 200,
- ElevationIndex::Wash => 300,
- ElevationIndex::ModalSurface => 400,
+ ElevationIndex::Wash => 250,
+ ElevationIndex::ModalSurface => 300,
ElevationIndex::DraggedElement => 900,
}
}
@@ -50,7 +50,7 @@ impl ElevationIndex {
spread_radius: px(0.),
},
BoxShadow {
- color: hsla(0., 0., 0., 0.16),
+ color: hsla(0., 0., 0., 0.20),
offset: point(px(3.), px(1.)),
blur_radius: px(12.),
spread_radius: px(0.),
@@ -0,0 +1,27 @@
+use gpui::{rems, Rems};
+
+#[derive(Debug, Default, Clone)]
+pub enum UITextSize {
+ /// The default size for UI text.
+ ///
+ /// `0.825rem` or `14px` at the default scale of `1rem` = `16px`.
+ ///
+ /// Note: The absolute size of this text will change based on a user's `ui_scale` setting.
+ #[default]
+ Default,
+ /// The small size for UI text.
+ ///
+ /// `0.75rem` or `12px` at the default scale of `1rem` = `16px`.
+ ///
+ /// Note: The absolute size of this text will change based on a user's `ui_scale` setting.
+ Small,
+}
+
+impl UITextSize {
+ pub fn rems(self) -> Rems {
+ match self {
+ Self::Default => rems(0.875),
+ Self::Small => rems(0.75),
+ }
+ }
+}
@@ -15,17 +15,12 @@
#![allow(dead_code, unused_variables)]
mod components;
-mod elevation;
pub mod prelude;
mod styled_ext;
+mod styles;
pub mod utils;
pub use components::*;
pub use prelude::*;
-// pub use static_data::*;
pub use styled_ext::*;
-
-#[cfg(feature = "stories")]
-mod story;
-#[cfg(feature = "stories")]
-pub use story::*;
+pub use styles::*;
@@ -19,7 +19,7 @@ lazy_static! {
pub struct AppCommitSha(pub String);
-#[derive(Copy, Clone, PartialEq, Eq, Default)]
+#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
pub enum ReleaseChannel {
#[default]
Dev,
@@ -59,7 +59,6 @@ impl ReleaseChannel {
pub fn link_prefix(&self) -> &'static str {
match self {
ReleaseChannel::Dev => "https://zed.dev/dev/",
- // TODO kb need to add server handling
ReleaseChannel::Nightly => "https://zed.dev/nightly/",
ReleaseChannel::Preview => "https://zed.dev/preview/",
ReleaseChannel::Stable => "https://zed.dev/",
@@ -56,14 +56,16 @@ use std::{
};
use crate::{
- notifications::{simple_message_notification::MessageNotification, NotificationTracker},
+ notifications::NotificationTracker,
persistence::model::{
DockData, DockStructure, SerializedPane, SerializedPaneGroup, SerializedWorkspace,
},
};
use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle};
use lazy_static::lazy_static;
-use notifications::{NotificationHandle, NotifyResultExt};
+use notifications::{
+ simple_message_notification::MessageNotification, NotificationHandle, NotifyResultExt,
+};
pub use pane::*;
pub use pane_group::*;
use persistence::{model::SerializedItem, DB};
@@ -776,7 +778,9 @@ impl Workspace {
}),
];
- cx.defer(|this, cx| this.update_window_title(cx));
+ cx.defer(|this, cx| {
+ this.update_window_title(cx);
+ });
Workspace {
weak_self: weak_handle.clone(),
modal: None,
@@ -20,7 +20,6 @@ test-support = [
[dependencies]
db2 = { path = "../db2" }
-call2 = { path = "../call2" }
client2 = { path = "../client2" }
collections = { path = "../collections" }
# context_menu = { path = "../context_menu" }
@@ -37,6 +36,7 @@ theme2 = { path = "../theme2" }
util = { path = "../util" }
ui = { package = "ui2", path = "../ui2" }
+async-trait.workspace = true
async-recursion = "1.0.0"
itertools = "0.10"
bincode = "1.2.1"
@@ -1,16 +1,14 @@
use crate::{status_bar::StatusItemView, Axis, Workspace};
use gpui::{
div, px, Action, AnchorCorner, AnyView, AppContext, Div, Entity, EntityId, EventEmitter,
- FocusHandle, FocusableView, ParentElement, Render, RenderOnce, SharedString, Styled,
+ FocusHandle, FocusableView, IntoElement, ParentElement, Render, SharedString, Styled,
Subscription, View, ViewContext, VisualContext, WeakView, WindowContext,
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use theme2::ActiveTheme;
-use ui::{
- h_stack, menu_handle, ContextMenu, IconButton, InteractionState, Label, ListItem, Tooltip,
-};
+use ui::{h_stack, menu_handle, ContextMenu, IconButton, InteractionState, Tooltip};
pub enum PanelEvent {
ChangePosition,
@@ -719,15 +717,9 @@ impl Render for PanelButtons {
&& panel.position_is_valid(position, cx)
{
let panel = panel.clone();
- menu = menu.entry(
- ListItem::new(
- panel.entity_id(),
- Label::new(format!("Dock {}", position.to_label())),
- ),
- move |_, cx| {
- panel.set_position(position, cx);
- },
- )
+ menu = menu.entry(position.to_label(), move |_, cx| {
+ panel.set_position(position, cx);
+ })
}
}
menu
@@ -46,6 +46,7 @@ impl ModalLayer {
previous_focus_handle: cx.focused(),
focus_handle: cx.focus_handle(),
});
+ dbg!("focusing");
cx.focus_view(&new_modal);
cx.notify();
}
@@ -1,6 +1,9 @@
use crate::{Toast, Workspace};
use collections::HashMap;
-use gpui::{AnyView, AppContext, Entity, EntityId, EventEmitter, Render, View, ViewContext};
+use gpui::{
+ AnyView, AppContext, AsyncWindowContext, DismissEvent, Entity, EntityId, EventEmitter, Render,
+ View, ViewContext, VisualContext,
+};
use std::{any::TypeId, ops::DerefMut};
pub fn init(cx: &mut AppContext) {
@@ -9,13 +12,9 @@ pub fn init(cx: &mut AppContext) {
// simple_message_notification::init(cx);
}
-pub enum NotificationEvent {
- Dismiss,
-}
-
-pub trait Notification: EventEmitter<NotificationEvent> + Render {}
+pub trait Notification: EventEmitter<DismissEvent> + Render {}
-impl<V: EventEmitter<NotificationEvent> + Render> Notification for V {}
+impl<V: EventEmitter<DismissEvent> + Render> Notification for V {}
pub trait NotificationHandle: Send {
fn id(&self) -> EntityId;
@@ -107,8 +106,8 @@ impl Workspace {
let notification = build_notification(cx);
cx.subscribe(
¬ification,
- move |this, handle, event: &NotificationEvent, cx| match event {
- NotificationEvent::Dismiss => {
+ move |this, handle, event: &DismissEvent, cx| match event {
+ DismissEvent::Dismiss => {
this.dismiss_notification_internal(type_id, id, cx);
}
},
@@ -120,6 +119,17 @@ impl Workspace {
}
}
+ pub fn show_error<E>(&mut self, err: &E, cx: &mut ViewContext<Self>)
+ where
+ E: std::fmt::Debug,
+ {
+ self.show_notification(0, cx, |cx| {
+ cx.build_view(|_cx| {
+ simple_message_notification::MessageNotification::new(format!("Error: {err:?}"))
+ })
+ });
+ }
+
pub fn dismiss_notification<V: Notification>(&mut self, id: usize, cx: &mut ViewContext<Self>) {
let type_id = TypeId::of::<V>();
@@ -166,13 +176,14 @@ impl Workspace {
}
pub mod simple_message_notification {
- use super::NotificationEvent;
- use gpui::{AnyElement, AppContext, Div, EventEmitter, Render, TextStyle, ViewContext};
+ use gpui::{
+ div, AnyElement, AppContext, DismissEvent, Div, EventEmitter, InteractiveElement,
+ ParentElement, Render, SharedString, StatefulInteractiveElement, Styled, TextStyle,
+ ViewContext,
+ };
use serde::Deserialize;
use std::{borrow::Cow, sync::Arc};
-
- // todo!()
- // actions!(message_notifications, [CancelMessageNotification]);
+ use ui::{h_stack, v_stack, Button, Icon, IconElement, Label, StyledExt};
#[derive(Clone, Default, Deserialize, PartialEq)]
pub struct OsOpen(pub Cow<'static, str>);
@@ -197,22 +208,22 @@ pub mod simple_message_notification {
// }
enum NotificationMessage {
- Text(Cow<'static, str>),
+ Text(SharedString),
Element(fn(TextStyle, &AppContext) -> AnyElement),
}
pub struct MessageNotification {
message: NotificationMessage,
on_click: Option<Arc<dyn Fn(&mut ViewContext<Self>) + Send + Sync>>,
- click_message: Option<Cow<'static, str>>,
+ click_message: Option<SharedString>,
}
- impl EventEmitter<NotificationMessage> for MessageNotification {}
+ impl EventEmitter<DismissEvent> for MessageNotification {}
impl MessageNotification {
pub fn new<S>(message: S) -> MessageNotification
where
- S: Into<Cow<'static, str>>,
+ S: Into<SharedString>,
{
Self {
message: NotificationMessage::Text(message.into()),
@@ -221,19 +232,20 @@ pub mod simple_message_notification {
}
}
- pub fn new_element(
- message: fn(TextStyle, &AppContext) -> AnyElement,
- ) -> MessageNotification {
- Self {
- message: NotificationMessage::Element(message),
- on_click: None,
- click_message: None,
- }
- }
+ // not needed I think (only for the "new panel" toast, which is outdated now)
+ // pub fn new_element(
+ // message: fn(TextStyle, &AppContext) -> AnyElement,
+ // ) -> MessageNotification {
+ // Self {
+ // message: NotificationMessage::Element(message),
+ // on_click: None,
+ // click_message: None,
+ // }
+ // }
pub fn with_click_message<S>(mut self, message: S) -> Self
where
- S: Into<Cow<'static, str>>,
+ S: Into<SharedString>,
{
self.click_message = Some(message.into());
self
@@ -247,17 +259,43 @@ pub mod simple_message_notification {
self
}
- // todo!()
- // pub fn dismiss(&mut self, _: &CancelMessageNotification, cx: &mut ViewContext<Self>) {
- // cx.emit(MessageNotificationEvent::Dismiss);
- // }
+ pub fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
+ cx.emit(DismissEvent::Dismiss);
+ }
}
impl Render for MessageNotification {
type Element = Div;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- todo!()
+ v_stack()
+ .elevation_3(cx)
+ .p_4()
+ .child(
+ h_stack()
+ .justify_between()
+ .child(div().max_w_80().child(match &self.message {
+ NotificationMessage::Text(text) => Label::new(text.clone()),
+ NotificationMessage::Element(element) => {
+ todo!()
+ }
+ }))
+ .child(
+ div()
+ .id("cancel")
+ .child(IconElement::new(Icon::Close))
+ .cursor_pointer()
+ .on_click(cx.listener(|this, event, cx| this.dismiss(cx))),
+ ),
+ )
+ .children(self.click_message.iter().map(|message| {
+ Button::new(message.clone()).on_click(cx.listener(|this, _, cx| {
+ if let Some(on_click) = this.on_click.as_ref() {
+ (on_click)(cx)
+ };
+ this.dismiss(cx)
+ }))
+ }))
}
}
// todo!()
@@ -359,8 +397,6 @@ pub mod simple_message_notification {
// .into_any()
// }
// }
-
- impl EventEmitter<NotificationEvent> for MessageNotification {}
}
pub trait NotifyResultExt {
@@ -371,6 +407,8 @@ pub trait NotifyResultExt {
workspace: &mut Workspace,
cx: &mut ViewContext<Workspace>,
) -> Option<Self::Ok>;
+
+ fn notify_async_err(self, cx: &mut AsyncWindowContext) -> Option<Self::Ok>;
}
impl<T, E> NotifyResultExt for Result<T, E>
@@ -384,14 +422,23 @@ where
Ok(value) => Some(value),
Err(err) => {
log::error!("TODO {err:?}");
- // todo!()
- // workspace.show_notification(0, cx, |cx| {
- // cx.add_view(|_cx| {
- // simple_message_notification::MessageNotification::new(format!(
- // "Error: {err:?}",
- // ))
- // })
- // });
+ workspace.show_error(&err, cx);
+ None
+ }
+ }
+ }
+
+ fn notify_async_err(self, cx: &mut AsyncWindowContext) -> Option<T> {
+ match self {
+ Ok(value) => Some(value),
+ Err(err) => {
+ log::error!("TODO {err:?}");
+ cx.update(|view, cx| {
+ if let Ok(workspace) = view.downcast::<Workspace>() {
+ workspace.update(cx, |workspace, cx| workspace.show_error(&err, cx))
+ }
+ })
+ .ok();
None
}
}
@@ -26,7 +26,7 @@ use std::{
};
use ui::v_stack;
-use ui::{prelude::*, Icon, IconButton, IconElement, TextColor, Tooltip};
+use ui::{prelude::*, Color, Icon, IconButton, IconElement, Tooltip};
use util::truncate_and_remove_front;
#[derive(PartialEq, Clone, Copy, Deserialize, Debug)]
@@ -1344,13 +1344,13 @@ impl Pane {
item: &Box<dyn ItemHandle>,
detail: usize,
cx: &mut ViewContext<'_, Pane>,
- ) -> impl RenderOnce {
+ ) -> impl IntoElement {
let label = item.tab_content(Some(detail), cx);
let close_icon = || {
let id = item.item_id();
div()
- .id(item.item_id())
+ .id(ix)
.invisible()
.group_hover("", |style| style.visible())
.child(
@@ -1379,10 +1379,11 @@ impl Pane {
};
let close_right = ItemSettings::get_global(cx).close_position.right();
+ let is_active = ix == self.active_item_index;
div()
.group("")
- .id(item.item_id())
+ .id(ix)
.cursor_pointer()
.when_some(item.tab_tooltip_text(cx), |div, text| {
div.tooltip(move |cx| cx.build_view(|cx| Tooltip::new(text.clone())).into())
@@ -1407,10 +1408,24 @@ impl Pane {
.py_1()
.bg(tab_bg)
.border_color(cx.theme().colors().border)
- .map(|this| match ix.cmp(&self.active_item_index) {
- cmp::Ordering::Less => this.border_l(),
- cmp::Ordering::Equal => this.border_r(),
- cmp::Ordering::Greater => this.border_l().border_r(),
+ .text_color(if is_active {
+ cx.theme().colors().text
+ } else {
+ cx.theme().colors().text_muted
+ })
+ .map(|this| {
+ let is_last_item = ix == self.items.len() - 1;
+ match ix.cmp(&self.active_item_index) {
+ cmp::Ordering::Less => this.border_l().mr_px(),
+ cmp::Ordering::Greater => {
+ if is_last_item {
+ this.mr_px().ml_px()
+ } else {
+ this.border_r().ml_px()
+ }
+ }
+ cmp::Ordering::Equal => this.border_l().border_r(),
+ }
})
// .hover(|h| h.bg(tab_hover_bg))
// .active(|a| a.bg(tab_active_bg))
@@ -1423,14 +1438,18 @@ impl Pane {
.children(
item.has_conflict(cx)
.then(|| {
- IconElement::new(Icon::ExclamationTriangle)
- .size(ui::IconSize::Small)
- .color(TextColor::Warning)
+ div().border().border_color(gpui::red()).child(
+ IconElement::new(Icon::ExclamationTriangle)
+ .size(ui::IconSize::Small)
+ .color(Color::Warning),
+ )
})
.or(item.is_dirty(cx).then(|| {
- IconElement::new(Icon::ExclamationTriangle)
- .size(ui::IconSize::Small)
- .color(TextColor::Info)
+ div().border().border_color(gpui::red()).child(
+ IconElement::new(Icon::ExclamationTriangle)
+ .size(ui::IconSize::Small)
+ .color(Color::Info),
+ )
})),
)
.children((!close_right).then(|| close_icon()))
@@ -1439,7 +1458,7 @@ impl Pane {
)
}
- fn render_tab_bar(&mut self, cx: &mut ViewContext<'_, Pane>) -> impl RenderOnce {
+ fn render_tab_bar(&mut self, cx: &mut ViewContext<'_, Pane>) -> impl IntoElement {
div()
.group("tab_bar")
.id("tab_bar")
@@ -1461,12 +1480,22 @@ impl Pane {
.flex()
.items_center()
.gap_px()
- .child(IconButton::new("navigate_backward", Icon::ArrowLeft).state(
- InteractionState::Enabled.if_enabled(self.can_navigate_backward()),
- ))
- .child(IconButton::new("navigate_forward", Icon::ArrowRight).state(
- InteractionState::Enabled.if_enabled(self.can_navigate_forward()),
- )),
+ .child(
+ div().border().border_color(gpui::red()).child(
+ IconButton::new("navigate_backward", Icon::ArrowLeft).state(
+ InteractionState::Enabled
+ .if_enabled(self.can_navigate_backward()),
+ ),
+ ),
+ )
+ .child(
+ div().border().border_color(gpui::red()).child(
+ IconButton::new("navigate_forward", Icon::ArrowRight).state(
+ InteractionState::Enabled
+ .if_enabled(self.can_navigate_forward()),
+ ),
+ ),
+ ),
),
)
.child(
@@ -1493,8 +1522,18 @@ impl Pane {
.flex()
.items_center()
.gap_px()
- .child(IconButton::new("plus", Icon::Plus))
- .child(IconButton::new("split", Icon::Split)),
+ .child(
+ div()
+ .border()
+ .border_color(gpui::red())
+ .child(IconButton::new("plus", Icon::Plus)),
+ )
+ .child(
+ div()
+ .border()
+ .border_color(gpui::red())
+ .child(IconButton::new("split", Icon::Split)),
+ ),
),
)
}
@@ -1,13 +1,12 @@
use crate::{AppState, FollowerState, Pane, Workspace};
use anyhow::{anyhow, bail, Result};
-use call2::ActiveCall;
use collections::HashMap;
use db2::sqlez::{
bindable::{Bind, Column, StaticColumnCount},
statement::Statement,
};
use gpui::{
- point, size, AnyWeakView, Bounds, Div, Model, Pixels, Point, RenderOnce, View, ViewContext,
+ point, size, AnyWeakView, Bounds, Div, IntoElement, Model, Pixels, Point, View, ViewContext,
};
use parking_lot::Mutex;
use project2::Project;
@@ -127,17 +126,15 @@ impl PaneGroup {
&self,
project: &Model<Project>,
follower_states: &HashMap<View<Pane>, FollowerState>,
- active_call: Option<&Model<ActiveCall>>,
active_pane: &View<Pane>,
zoomed: Option<&AnyWeakView>,
app_state: &Arc<AppState>,
cx: &mut ViewContext<Workspace>,
- ) -> impl RenderOnce {
+ ) -> impl IntoElement {
self.root.render(
project,
0,
follower_states,
- active_call,
active_pane,
zoomed,
app_state,
@@ -199,12 +196,11 @@ impl Member {
project: &Model<Project>,
basis: usize,
follower_states: &HashMap<View<Pane>, FollowerState>,
- active_call: Option<&Model<ActiveCall>>,
active_pane: &View<Pane>,
zoomed: Option<&AnyWeakView>,
app_state: &Arc<AppState>,
cx: &mut ViewContext<Workspace>,
- ) -> impl RenderOnce {
+ ) -> impl IntoElement {
match self {
Member::Pane(pane) => {
// todo!()
@@ -214,7 +210,7 @@ impl Member {
// Some(pane)
// };
- div().size_full().child(pane.clone())
+ div().size_full().child(pane.clone()).into_any()
// Stack::new()
// .with_child(pane_element.contained().with_border(leader_border))
@@ -230,16 +226,17 @@ impl Member {
// .bg(cx.theme().colors().editor)
// .children();
}
- Member::Axis(axis) => axis.render(
- project,
- basis + 1,
- follower_states,
- active_call,
- active_pane,
- zoomed,
- app_state,
- cx,
- ),
+ Member::Axis(axis) => axis
+ .render(
+ project,
+ basis + 1,
+ follower_states,
+ active_pane,
+ zoomed,
+ app_state,
+ cx,
+ )
+ .into_any(),
}
// enum FollowIntoExternalProject {}
@@ -556,7 +553,6 @@ impl PaneAxis {
project: &Model<Project>,
basis: usize,
follower_states: &HashMap<View<Pane>, FollowerState>,
- active_call: Option<&Model<ActiveCall>>,
active_pane: &View<Pane>,
zoomed: Option<&AnyWeakView>,
app_state: &Arc<AppState>,
@@ -578,14 +574,13 @@ impl PaneAxis {
project,
basis,
follower_states,
- active_call,
active_pane,
zoomed,
app_state,
cx,
)
- .render_into_any(),
- Member::Pane(pane) => pane.clone().render_into_any(),
+ .into_any_element(),
+ Member::Pane(pane) => pane.clone().into_any_element(),
}
}))
@@ -1,151 +0,0 @@
-use crate::{
- item::{Item, ItemEvent},
- ItemNavHistory, WorkspaceId,
-};
-use anyhow::Result;
-use call::participant::{Frame, RemoteVideoTrack};
-use client::{proto::PeerId, User};
-use futures::StreamExt;
-use gpui::{
- elements::*,
- geometry::{rect::RectF, vector::vec2f},
- platform::MouseButton,
- AppContext, Entity, Task, View, ViewContext,
-};
-use smallvec::SmallVec;
-use std::{
- borrow::Cow,
- sync::{Arc, Weak},
-};
-
-pub enum Event {
- Close,
-}
-
-pub struct SharedScreen {
- track: Weak<RemoteVideoTrack>,
- frame: Option<Frame>,
- pub peer_id: PeerId,
- user: Arc<User>,
- nav_history: Option<ItemNavHistory>,
- _maintain_frame: Task<Result<()>>,
-}
-
-impl SharedScreen {
- pub fn new(
- track: &Arc<RemoteVideoTrack>,
- peer_id: PeerId,
- user: Arc<User>,
- cx: &mut ViewContext<Self>,
- ) -> Self {
- 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(())
- }),
- }
- }
-}
-
-impl Entity for SharedScreen {
- type Event = Event;
-}
-
-impl View for SharedScreen {
- fn ui_name() -> &'static str {
- "SharedScreen"
- }
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
- enum Focus {}
-
- let frame = self.frame.clone();
- MouseEventHandler::new::<Focus, _>(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<Cow<str>> {
- Some(format!("{}'s screen", self.user.github_login).into())
- }
- fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
- if let Some(nav_history) = self.nav_history.as_mut() {
- nav_history.push::<()>(None, cx);
- }
- }
-
- fn tab_content<V: 'static>(
- &self,
- _: Option<usize>,
- style: &theme::Tab,
- _: &AppContext,
- ) -> gpui::AnyElement<V> {
- 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>) {
- self.nav_history = Some(history);
- }
-
- fn clone_on_split(
- &self,
- _workspace_id: WorkspaceId,
- cx: &mut ViewContext<Self>,
- ) -> Option<Self> {
- let track = self.track.upgrade()?;
- Some(Self::new(&track, self.peer_id, self.user.clone(), cx))
- }
-
- fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
- match event {
- Event::Close => smallvec::smallvec!(ItemEvent::CloseItem),
- }
- }
-}
@@ -2,11 +2,11 @@ use std::any::TypeId;
use crate::{ItemHandle, Pane};
use gpui::{
- div, AnyView, Div, ParentElement, Render, RenderOnce, Styled, Subscription, View, ViewContext,
+ div, AnyView, Div, IntoElement, ParentElement, Render, Styled, Subscription, View, ViewContext,
WindowContext,
};
use theme2::ActiveTheme;
-use ui::h_stack;
+use ui::{h_stack, Button, Icon, IconButton};
use util::ResultExt;
pub trait StatusItemView: Render {
@@ -47,20 +47,101 @@ impl Render for StatusBar {
.w_full()
.h_8()
.bg(cx.theme().colors().status_bar_background)
- .child(self.render_left_tools(cx))
- .child(self.render_right_tools(cx))
+ // Nate: I know this isn't how we render status bar tools
+ // We can move these to the correct place once we port their tools
+ .child(
+ h_stack().gap_1().child(self.render_left_tools(cx)).child(
+ h_stack().gap_4().child(
+ // TODO: Language Server status
+ div()
+ .border()
+ .border_color(gpui::red())
+ .child("Checking..."),
+ ),
+ ),
+ )
+ .child(
+ h_stack()
+ .gap_4()
+ .child(
+ h_stack()
+ .gap_1()
+ .child(
+ // TODO: Line / column numbers
+ div()
+ .border()
+ .border_color(gpui::red())
+ .child(Button::new("15:22")),
+ )
+ .child(
+ // TODO: Language picker
+ div()
+ .border()
+ .border_color(gpui::red())
+ .child(Button::new("Rust")),
+ ),
+ )
+ .child(
+ h_stack()
+ .gap_1()
+ .child(
+ // Github tool
+ div()
+ .border()
+ .border_color(gpui::red())
+ .child(IconButton::new("status-copilot", Icon::Copilot)),
+ )
+ .child(
+ // Feedback Tool
+ div()
+ .border()
+ .border_color(gpui::red())
+ .child(IconButton::new("status-feedback", Icon::Envelope)),
+ ),
+ )
+ .child(
+ // Bottom Dock
+ h_stack().gap_1().child(
+ // Terminal
+ div()
+ .border()
+ .border_color(gpui::red())
+ .child(IconButton::new("status-terminal", Icon::Terminal)),
+ ),
+ )
+ .child(
+ // Right Dock
+ h_stack()
+ .gap_1()
+ .child(
+ // Terminal
+ div()
+ .border()
+ .border_color(gpui::red())
+ .child(IconButton::new("status-assistant", Icon::Ai)),
+ )
+ .child(
+ // Terminal
+ div()
+ .border()
+ .border_color(gpui::red())
+ .child(IconButton::new("status-chat", Icon::MessageBubbles)),
+ ),
+ )
+ .child(self.render_right_tools(cx)),
+ )
}
}
impl StatusBar {
- fn render_left_tools(&self, cx: &mut ViewContext<Self>) -> impl RenderOnce {
+ fn render_left_tools(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
h_stack()
.items_center()
.gap_2()
.children(self.left_items.iter().map(|item| item.to_any()))
}
- fn render_right_tools(&self, cx: &mut ViewContext<Self>) -> impl RenderOnce {
+ fn render_right_tools(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
h_stack()
.items_center()
.gap_2()
@@ -1,10 +1,10 @@
use crate::ItemHandle;
use gpui::{
- AnyView, Div, Entity, EntityId, EventEmitter, ParentElement as _, Render, Styled, View,
+ div, AnyView, Div, Entity, EntityId, EventEmitter, ParentElement as _, Render, Styled, View,
ViewContext, WindowContext,
};
use theme2::ActiveTheme;
-use ui::{h_stack, v_stack, Button, Icon, IconButton, Label, TextColor};
+use ui::{h_stack, v_stack, Button, Color, Icon, IconButton, Label};
pub enum ToolbarItemEvent {
ChangeLocation(ToolbarItemLocation),
@@ -83,24 +83,37 @@ impl Render for Toolbar {
//dbg!(&self.items.len());
v_stack()
.border_b()
- .border_color(cx.theme().colors().border)
+ .border_color(cx.theme().colors().border_variant)
+ .bg(cx.theme().colors().toolbar_background)
.child(
h_stack()
.justify_between()
.child(
// Toolbar left side
h_stack()
+ .border()
+ .border_color(gpui::red())
.p_1()
.child(Button::new("crates"))
- .child(Label::new("/").color(TextColor::Muted))
+ .child(Label::new("/").color(Color::Muted))
.child(Button::new("workspace2")),
)
// Toolbar right side
.child(
h_stack()
.p_1()
- .child(IconButton::new("buffer-search", Icon::MagnifyingGlass))
- .child(IconButton::new("inline-assist", Icon::MagicWand)),
+ .child(
+ div()
+ .border()
+ .border_color(gpui::red())
+ .child(IconButton::new("buffer-search", Icon::MagnifyingGlass)),
+ )
+ .child(
+ div()
+ .border()
+ .border_color(gpui::red())
+ .child(IconButton::new("inline-assist", Icon::MagicWand)),
+ ),
),
)
.children(self.items.iter().map(|(child, _)| child.to_any()))
@@ -9,17 +9,16 @@ pub mod pane_group;
mod persistence;
pub mod searchable;
// todo!()
-// pub mod shared_screen;
mod modal_layer;
mod status_bar;
mod toolbar;
mod workspace_settings;
use anyhow::{anyhow, Context as _, Result};
-use call2::ActiveCall;
+use async_trait::async_trait;
use client2::{
proto::{self, PeerId},
- Client, TypedEnvelope, UserStore,
+ Client, TypedEnvelope, User, UserStore,
};
use collections::{hash_map, HashMap, HashSet};
use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle};
@@ -33,8 +32,8 @@ use gpui::{
AsyncWindowContext, Bounds, Context, Div, Entity, EntityId, EventEmitter, FocusHandle,
FocusableView, GlobalPixels, InteractiveElement, KeyContext, ManagedView, Model, ModelContext,
ParentElement, PathPromptOptions, Point, PromptLevel, Render, Size, Styled, Subscription, Task,
- View, ViewContext, VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle,
- WindowOptions,
+ View, ViewContext, VisualContext, WeakModel, WeakView, WindowBounds, WindowContext,
+ WindowHandle, WindowOptions,
};
use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem};
use itertools::Itertools;
@@ -210,7 +209,6 @@ pub fn init_settings(cx: &mut AppContext) {
pub fn init(app_state: Arc<AppState>, cx: &mut AppContext) {
init_settings(cx);
notifications::init(cx);
-
// cx.add_global_action({
// let app_state = Arc::downgrade(&app_state);
// move |_: &Open, cx: &mut AppContext| {
@@ -304,6 +302,7 @@ pub struct AppState {
pub user_store: Model<UserStore>,
pub workspace_store: Model<WorkspaceStore>,
pub fs: Arc<dyn fs2::Fs>,
+ pub call_factory: CallFactory,
pub build_window_options:
fn(Option<WindowBounds>, Option<Uuid>, &mut AppContext) -> WindowOptions,
pub node_runtime: Arc<dyn NodeRuntime>,
@@ -322,6 +321,64 @@ struct Follower {
peer_id: PeerId,
}
+#[cfg(any(test, feature = "test-support"))]
+pub struct TestCallHandler;
+
+#[cfg(any(test, feature = "test-support"))]
+impl CallHandler for TestCallHandler {
+ fn peer_state(&mut self, id: PeerId, cx: &mut ViewContext<Workspace>) -> Option<(bool, bool)> {
+ None
+ }
+
+ fn shared_screen_for_peer(
+ &self,
+ peer_id: PeerId,
+ pane: &View<Pane>,
+ cx: &mut ViewContext<Workspace>,
+ ) -> Option<Box<dyn ItemHandle>> {
+ None
+ }
+
+ fn room_id(&self, cx: &AppContext) -> Option<u64> {
+ None
+ }
+
+ fn hang_up(&self, cx: &mut AppContext) -> Task<Result<()>> {
+ Task::ready(Err(anyhow!("TestCallHandler should not be hanging up")))
+ }
+
+ fn active_project(&self, cx: &AppContext) -> Option<WeakModel<Project>> {
+ None
+ }
+
+ fn invite(
+ &mut self,
+ called_user_id: u64,
+ initial_project: Option<Model<Project>>,
+ cx: &mut AppContext,
+ ) -> Task<Result<()>> {
+ unimplemented!()
+ }
+
+ fn remote_participants(&self, cx: &AppContext) -> Option<Vec<(Arc<User>, PeerId)>> {
+ None
+ }
+
+ fn is_muted(&self, cx: &AppContext) -> Option<bool> {
+ None
+ }
+
+ fn toggle_mute(&self, cx: &mut AppContext) {}
+
+ fn toggle_screen_share(&self, cx: &mut AppContext) {}
+
+ fn toggle_deafen(&self, cx: &mut AppContext) {}
+
+ fn is_deafened(&self, cx: &AppContext) -> Option<bool> {
+ None
+ }
+}
+
impl AppState {
#[cfg(any(test, feature = "test-support"))]
pub fn test(cx: &mut AppContext) -> Arc<Self> {
@@ -352,6 +409,7 @@ impl AppState {
workspace_store,
node_runtime: FakeNodeRuntime::new(),
build_window_options: |_, _, _| Default::default(),
+ call_factory: |_, _| Box::new(TestCallHandler),
})
}
}
@@ -408,6 +466,35 @@ pub enum Event {
WorkspaceCreated(WeakView<Workspace>),
}
+#[async_trait(?Send)]
+pub trait CallHandler {
+ fn peer_state(&mut self, id: PeerId, cx: &mut ViewContext<Workspace>) -> Option<(bool, bool)>;
+ fn shared_screen_for_peer(
+ &self,
+ peer_id: PeerId,
+ pane: &View<Pane>,
+ cx: &mut ViewContext<Workspace>,
+ ) -> Option<Box<dyn ItemHandle>>;
+ fn room_id(&self, cx: &AppContext) -> Option<u64>;
+ fn is_in_room(&self, cx: &mut ViewContext<Workspace>) -> bool {
+ self.room_id(cx).is_some()
+ }
+ fn hang_up(&self, cx: &mut AppContext) -> Task<Result<()>>;
+ fn active_project(&self, cx: &AppContext) -> Option<WeakModel<Project>>;
+ fn invite(
+ &mut self,
+ called_user_id: u64,
+ initial_project: Option<Model<Project>>,
+ cx: &mut AppContext,
+ ) -> Task<Result<()>>;
+ fn remote_participants(&self, cx: &AppContext) -> Option<Vec<(Arc<User>, PeerId)>>;
+ fn is_muted(&self, cx: &AppContext) -> Option<bool>;
+ fn is_deafened(&self, cx: &AppContext) -> Option<bool>;
+ fn toggle_mute(&self, cx: &mut AppContext);
+ fn toggle_deafen(&self, cx: &mut AppContext);
+ fn toggle_screen_share(&self, cx: &mut AppContext);
+}
+
pub struct Workspace {
window_self: WindowHandle<Self>,
weak_self: WeakView<Self>,
@@ -428,10 +515,10 @@ pub struct Workspace {
titlebar_item: Option<AnyView>,
notifications: Vec<(TypeId, usize, Box<dyn NotificationHandle>)>,
project: Model<Project>,
+ call_handler: Box<dyn CallHandler>,
follower_states: HashMap<View<Pane>, FollowerState>,
last_leaders_by_pane: HashMap<WeakView<Pane>, PeerId>,
window_edited: bool,
- active_call: Option<(Model<ActiveCall>, Vec<Subscription>)>,
leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
database_id: WorkspaceId,
app_state: Arc<AppState>,
@@ -459,6 +546,7 @@ struct FollowerState {
enum WorkspaceBounds {}
+type CallFactory = fn(WeakView<Workspace>, &mut ViewContext<Workspace>) -> Box<dyn CallHandler>;
impl Workspace {
pub fn new(
workspace_id: WorkspaceId,
@@ -550,9 +638,19 @@ impl Workspace {
mpsc::unbounded::<(PeerId, proto::UpdateFollowers)>();
let _apply_leader_updates = cx.spawn(|this, mut cx| async move {
while let Some((leader_id, update)) = leader_updates_rx.next().await {
- Self::process_leader_update(&this, leader_id, update, &mut cx)
+ let mut cx2 = cx.clone();
+ let t = this.clone();
+
+ Workspace::process_leader_update(&this, leader_id, update, &mut cx)
.await
.log_err();
+
+ // this.update(&mut cx, |this, cxx| {
+ // this.call_handler
+ // .process_leader_update(leader_id, update, cx2)
+ // })?
+ // .await
+ // .log_err();
}
Ok(())
@@ -585,14 +683,6 @@ impl Workspace {
// drag_and_drop.register_container(weak_handle.clone());
// });
- let mut active_call = None;
- if cx.has_global::<Model<ActiveCall>>() {
- let call = cx.global::<Model<ActiveCall>>().clone();
- let mut subscriptions = Vec::new();
- subscriptions.push(cx.subscribe(&call, Self::on_active_call_event));
- active_call = Some((call, subscriptions));
- }
-
let subscriptions = vec![
cx.observe_window_activation(Self::on_window_activation_changed),
cx.observe_window_bounds(move |_, cx| {
@@ -632,7 +722,21 @@ impl Workspace {
}),
];
- cx.defer(|this, cx| this.update_window_title(cx));
+ cx.defer(|this, cx| {
+ this.update_window_title(cx);
+ // todo! @nate - these are useful for testing notifications
+ // this.show_error(
+ // &anyhow::anyhow!("what happens if this message is very very very very very long"),
+ // cx,
+ // );
+
+ // this.show_notification(1, cx, |cx| {
+ // cx.build_view(|_cx| {
+ // simple_message_notification::MessageNotification::new(format!("Error:"))
+ // .with_click_message("click here because!")
+ // })
+ // });
+ });
Workspace {
window_self: window_handle,
weak_self: weak_handle.clone(),
@@ -655,7 +759,8 @@ impl Workspace {
follower_states: Default::default(),
last_leaders_by_pane: Default::default(),
window_edited: false,
- active_call,
+
+ call_handler: (app_state.call_factory)(weak_handle.clone(), cx),
database_id: workspace_id,
app_state,
_observe_current_user,
@@ -1102,7 +1207,7 @@ impl Workspace {
cx: &mut ViewContext<Self>,
) -> Task<Result<bool>> {
//todo!(saveing)
- let active_call = self.active_call().cloned();
+
let window = cx.window_handle();
cx.spawn(|this, mut cx| async move {
@@ -1113,27 +1218,27 @@ impl Workspace {
.count()
})?;
- if let Some(active_call) = active_call {
- if !quitting
- && workspace_count == 1
- && active_call.read_with(&cx, |call, _| call.room().is_some())?
- {
- let answer = window.update(&mut cx, |_, cx| {
- cx.prompt(
- PromptLevel::Warning,
- "Do you want to leave the current call?",
- &["Close window and hang up", "Cancel"],
- )
- })?;
+ if !quitting
+ && workspace_count == 1
+ && this
+ .update(&mut cx, |this, cx| this.call_handler.is_in_room(cx))
+ .log_err()
+ .unwrap_or_default()
+ {
+ let answer = window.update(&mut cx, |_, cx| {
+ cx.prompt(
+ PromptLevel::Warning,
+ "Do you want to leave the current call?",
+ &["Close window and hang up", "Cancel"],
+ )
+ })?;
- if answer.await.log_err() == Some(1) {
- return anyhow::Ok(false);
- } else {
- active_call
- .update(&mut cx, |call, cx| call.hang_up(cx))?
- .await
- .log_err();
- }
+ if answer.await.log_err() == Some(1) {
+ return anyhow::Ok(false);
+ } else {
+ this.update(&mut cx, |this, cx| this.call_handler.hang_up(cx))?
+ .await
+ .log_err();
}
}
@@ -1915,13 +2020,13 @@ impl Workspace {
item
}
- // pub fn open_shared_screen(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
- // if let Some(shared_screen) = self.shared_screen_for_peer(peer_id, &self.active_pane, cx) {
- // self.active_pane.update(cx, |pane, cx| {
- // pane.add_item(Box::new(shared_screen), false, true, None, cx)
- // });
- // }
- // }
+ pub fn open_shared_screen(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
+ if let Some(shared_screen) = self.shared_screen_for_peer(peer_id, &self.active_pane, cx) {
+ self.active_pane.update(cx, |pane, cx| {
+ pane.add_item(shared_screen, false, true, None, cx)
+ });
+ }
+ }
pub fn activate_item(&mut self, item: &dyn ItemHandle, cx: &mut ViewContext<Self>) -> bool {
let result = self.panes.iter().find_map(|pane| {
@@ -2237,6 +2342,11 @@ impl Workspace {
&self.active_pane
}
+ pub fn pane_for(&self, handle: &dyn ItemHandle) -> Option<View<Pane>> {
+ let weak_pane = self.panes_by_item.get(&handle.item_id())?;
+ weak_pane.upgrade()
+ }
+
fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext<Self>) {
self.follower_states.retain(|_, state| {
if state.leader_id == peer_id {
@@ -2391,19 +2501,19 @@ impl Workspace {
// }
pub fn unfollow(&mut self, pane: &View<Pane>, cx: &mut ViewContext<Self>) -> Option<PeerId> {
- let state = self.follower_states.remove(pane)?;
+ let follower_states = &mut self.follower_states;
+ let state = follower_states.remove(pane)?;
let leader_id = state.leader_id;
for (_, item) in state.items_by_leader_view_id {
item.set_leader_peer_id(None, cx);
}
- if self
- .follower_states
+ if follower_states
.values()
.all(|state| state.leader_id != state.leader_id)
{
let project_id = self.project.read(cx).remote_id();
- let room_id = self.active_call()?.read(cx).room()?.read(cx).id();
+ let room_id = self.call_handler.room_id(cx)?;
self.app_state
.client
.send(proto::Unfollow {
@@ -2514,32 +2624,31 @@ impl Workspace {
// }
// }
- // fn render_notifications(
- // &self,
- // theme: &theme::Workspace,
- // cx: &AppContext,
- // ) -> Option<AnyElement<Workspace>> {
- // if self.notifications.is_empty() {
- // None
- // } else {
- // Some(
- // Flex::column()
- // .with_children(self.notifications.iter().map(|(_, _, notification)| {
- // ChildView::new(notification.as_any(), cx)
- // .contained()
- // .with_style(theme.notification)
- // }))
- // .constrained()
- // .with_width(theme.notifications.width)
- // .contained()
- // .with_style(theme.notifications.container)
- // .aligned()
- // .bottom()
- // .right()
- // .into_any(),
- // )
- // }
- // }
+ fn render_notifications(&self, cx: &ViewContext<Self>) -> Option<Div> {
+ if self.notifications.is_empty() {
+ None
+ } else {
+ Some(
+ div()
+ .absolute()
+ .z_index(100)
+ .right_3()
+ .bottom_3()
+ .w_96()
+ .h_full()
+ .flex()
+ .flex_col()
+ .justify_end()
+ .gap_2()
+ .children(self.notifications.iter().map(|(_, _, notification)| {
+ div()
+ .on_any_mouse_down(|_, cx| cx.stop_propagation())
+ .on_any_mouse_up(|_, cx| cx.stop_propagation())
+ .child(notification.to_any())
+ })),
+ )
+ }
+ }
// // RPC handlers
@@ -2762,8 +2871,9 @@ impl Workspace {
} else {
None
};
+ let room_id = self.call_handler.room_id(cx)?;
self.app_state().workspace_store.update(cx, |store, cx| {
- store.update_followers(project_id, update, cx)
+ store.update_followers(project_id, room_id, update, cx)
})
}
@@ -2771,31 +2881,12 @@ impl Workspace {
self.follower_states.get(pane).map(|state| state.leader_id)
}
- fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
+ pub fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext<Self>) -> Option<()> {
cx.notify();
- let call = self.active_call()?;
- let room = call.read(cx).room()?.read(cx);
- let participant = room.remote_participant_for_peer_id(leader_id)?;
+ let (leader_in_this_project, leader_in_this_app) =
+ self.call_handler.peer_state(leader_id, cx)?;
let mut items_to_activate = Vec::new();
-
- let leader_in_this_app;
- let leader_in_this_project;
- match participant.location {
- call2::ParticipantLocation::SharedProject { project_id } => {
- leader_in_this_app = true;
- leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id();
- }
- call2::ParticipantLocation::UnsharedProject => {
- leader_in_this_app = true;
- leader_in_this_project = false;
- }
- call2::ParticipantLocation::External => {
- leader_in_this_app = false;
- leader_in_this_project = false;
- }
- };
-
for (pane, state) in &self.follower_states {
if state.leader_id != leader_id {
continue;
@@ -2814,10 +2905,10 @@ impl Workspace {
}
continue;
}
- // todo!()
- // if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) {
- // items_to_activate.push((pane.clone(), Box::new(shared_screen)));
- // }
+
+ if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) {
+ items_to_activate.push((pane.clone(), shared_screen));
+ }
}
for (pane, item) in items_to_activate {
@@ -2825,8 +2916,8 @@ impl Workspace {
if let Some(index) = pane.update(cx, |pane, _| pane.index_for_item(item.as_ref())) {
pane.update(cx, |pane, cx| pane.activate_item(index, false, false, cx));
} else {
- pane.update(cx, |pane, cx| {
- pane.add_item(item.boxed_clone(), false, false, None, cx)
+ pane.update(cx, |pane, mut cx| {
+ pane.add_item(item.boxed_clone(), false, false, None, &mut cx)
});
}
@@ -2838,27 +2929,27 @@ impl Workspace {
None
}
- // todo!()
- // fn shared_screen_for_peer(
- // &self,
- // peer_id: PeerId,
- // pane: &View<Pane>,
- // cx: &mut ViewContext<Self>,
- // ) -> Option<View<SharedScreen>> {
- // let call = self.active_call()?;
- // 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::<SharedScreen>() {
- // if item.read(cx).peer_id == peer_id {
- // return Some(item);
- // }
- // }
+ fn shared_screen_for_peer(
+ &self,
+ peer_id: PeerId,
+ pane: &View<Pane>,
+ cx: &mut ViewContext<Self>,
+ ) -> Option<Box<dyn ItemHandle>> {
+ self.call_handler.shared_screen_for_peer(peer_id, pane, cx)
+ // let call = self.active_call()?;
+ // 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::<SharedScreen>() {
+ // if item.read(cx).peer_id == peer_id {
+ // return Some(item);
+ // }
+ // }
- // Some(cx.build_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx)))
- // }
+ // Some(cx.build_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx)))
+ }
pub fn on_window_activation_changed(&mut self, cx: &mut ViewContext<Self>) {
if cx.is_window_active() {
@@ -2886,25 +2977,6 @@ impl Workspace {
}
}
- fn active_call(&self) -> Option<&Model<ActiveCall>> {
- self.active_call.as_ref().map(|(call, _)| call)
- }
-
- fn on_active_call_event(
- &mut self,
- _: Model<ActiveCall>,
- event: &call2::room::Event,
- cx: &mut ViewContext<Self>,
- ) {
- match event {
- call2::room::Event::ParticipantLocationChanged { participant_id }
- | call2::room::Event::RemoteVideoTracksChanged { participant_id } => {
- self.leader_updated(*participant_id, cx);
- }
- _ => {}
- }
- }
-
pub fn database_id(&self) -> WorkspaceId {
self.database_id
}
@@ -3314,6 +3386,7 @@ impl Workspace {
fs: project.read(cx).fs().clone(),
build_window_options: |_, _, _| Default::default(),
node_runtime: FakeNodeRuntime::new(),
+ call_factory: |_, _| Box::new(TestCallHandler),
});
let workspace = Self::new(0, project, app_state, cx);
workspace.active_pane.update(cx, |pane, cx| pane.focus(cx));
@@ -3392,6 +3465,10 @@ impl Workspace {
self.modal_layer
.update(cx, |modal_layer, cx| modal_layer.toggle_modal(cx, build))
}
+
+ pub fn call_state(&mut self) -> &mut dyn CallHandler {
+ &mut *self.call_handler
+ }
}
fn window_bounds_env_override(cx: &AsyncAppContext) -> Option<WindowBounds> {
@@ -3637,7 +3714,6 @@ impl Render for Workspace {
.bg(cx.theme().colors().background)
.children(self.titlebar_item.clone())
.child(
- // todo! should this be a component a view?
div()
.id("workspace")
.relative()
@@ -3672,7 +3748,6 @@ impl Render for Workspace {
.child(self.center.render(
&self.project,
&self.follower_states,
- self.active_call(),
&self.active_pane,
self.zoomed.as_ref(),
&self.app_state,
@@ -3688,7 +3763,8 @@ impl Render for Workspace {
.overflow_hidden()
.child(self.right_dock.clone()),
),
- ),
+ )
+ .children(self.render_notifications(cx)),
)
.child(self.status_bar.clone())
}
@@ -3842,14 +3918,10 @@ impl WorkspaceStore {
pub fn update_followers(
&self,
project_id: Option<u64>,
+ room_id: u64,
update: proto::update_followers::Variant,
cx: &AppContext,
) -> Option<()> {
- if !cx.has_global::<Model<ActiveCall>>() {
- return None;
- }
-
- let room_id = ActiveCall::global(cx).read(cx).room()?.read(cx).id();
let follower_ids: Vec<_> = self
.followers
.iter()
@@ -3885,9 +3957,17 @@ impl WorkspaceStore {
project_id: envelope.payload.project_id,
peer_id: envelope.original_sender_id()?,
};
- let active_project = ActiveCall::global(cx).read(cx).location().cloned();
-
let mut response = proto::FollowResponse::default();
+ let active_project = this
+ .workspaces
+ .iter()
+ .next()
+ .and_then(|workspace| {
+ workspace
+ .read_with(cx, |this, cx| this.call_handler.active_project(cx))
+ .log_err()
+ })
+ .flatten();
for workspace in &this.workspaces {
workspace
.update(cx, |workspace, cx| {
@@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
description = "The fast, collaborative code editor."
edition = "2021"
name = "zed"
-version = "0.114.0"
+version = "0.115.0"
publish = false
[lib]
@@ -171,7 +171,6 @@ osx_info_plist_exts = ["resources/info/*"]
osx_url_schemes = ["zed-dev"]
[package.metadata.bundle-nightly]
-# TODO kb different icon?
icon = ["resources/app-icon-preview@2x.png", "resources/app-icon-preview.png"]
identifier = "dev.zed.Zed-Nightly"
name = "Zed Nightly"
@@ -18,10 +18,10 @@
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
@@ -65,7 +65,8 @@ fn main() {
log::info!("========== starting zed ==========");
let mut app = gpui::App::new(Assets).unwrap();
- let installation_id = app.background().block(installation_id()).ok();
+ let (installation_id, existing_installation_id_found) =
+ app.background().block(installation_id()).ok().unzip();
let session_id = Uuid::new_v4().to_string();
init_panic_hook(&app, installation_id.clone(), session_id.clone());
@@ -166,6 +167,14 @@ fn main() {
.detach();
client.telemetry().start(installation_id, session_id, cx);
+ let telemetry_settings = *settings::get::<TelemetrySettings>(cx);
+ let event_operation = match existing_installation_id_found {
+ Some(false) => "first open",
+ _ => "open",
+ };
+ client
+ .telemetry()
+ .report_app_event(telemetry_settings, event_operation);
let app_state = Arc::new(AppState {
languages,
@@ -317,11 +326,11 @@ async fn authenticate(client: Arc<Client>, cx: &AsyncAppContext) -> Result<()> {
Ok::<_, anyhow::Error>(())
}
-async fn installation_id() -> Result<String> {
+async fn installation_id() -> Result<(String, bool)> {
let legacy_key_name = "device_id";
if let Ok(Some(installation_id)) = KEY_VALUE_STORE.read_kvp(legacy_key_name) {
- Ok(installation_id)
+ Ok((installation_id, true))
} else {
let installation_id = Uuid::new_v4().to_string();
@@ -329,7 +338,7 @@ async fn installation_id() -> Result<String> {
.write_kvp(legacy_key_name.to_string(), installation_id.clone())
.await?;
- Ok(installation_id)
+ Ok((installation_id, false))
}
}
@@ -16,7 +16,7 @@ path = "src/main.rs"
[dependencies]
ai = { package = "ai2", path = "../ai2"}
-# audio = { path = "../audio" }
+audio = { package = "audio2", path = "../audio2" }
# activity_indicator = { path = "../activity_indicator" }
auto_update = { package = "auto_update2", path = "../auto_update2" }
# breadcrumbs = { path = "../breadcrumbs" }
@@ -71,7 +71,11 @@ fn main() {
log::info!("========== starting zed ==========");
let app = App::production(Arc::new(Assets));
- let installation_id = app.background_executor().block(installation_id()).ok();
+ let (installation_id, existing_installation_id_found) = app
+ .background_executor()
+ .block(installation_id())
+ .ok()
+ .unzip();
let session_id = Uuid::new_v4().to_string();
init_panic_hook(&app, installation_id.clone(), session_id.clone());
@@ -172,6 +176,14 @@ fn main() {
// .detach();
client.telemetry().start(installation_id, session_id, cx);
+ let telemetry_settings = *client::TelemetrySettings::get_global(cx);
+ let event_operation = match existing_installation_id_found {
+ Some(false) => "first open",
+ _ => "open",
+ };
+ client
+ .telemetry()
+ .report_app_event(telemetry_settings, event_operation);
let app_state = Arc::new(AppState {
languages,
@@ -179,12 +191,13 @@ fn main() {
user_store,
fs,
build_window_options,
+ call_factory: call::Call::new,
workspace_store,
node_runtime,
});
cx.set_global(Arc::downgrade(&app_state));
- // audio::init(Assets, cx);
+ audio::init(Assets, cx);
auto_update::init(http.clone(), client::ZED_SERVER_URL.clone(), cx);
workspace::init(app_state.clone(), cx);
@@ -331,11 +344,11 @@ async fn authenticate(client: Arc<Client>, cx: &AsyncAppContext) -> Result<()> {
Ok::<_, anyhow::Error>(())
}
-async fn installation_id() -> Result<String> {
+async fn installation_id() -> Result<(String, bool)> {
let legacy_key_name = "device_id";
if let Ok(Some(installation_id)) = KEY_VALUE_STORE.read_kvp(legacy_key_name) {
- Ok(installation_id)
+ Ok((installation_id, true))
} else {
let installation_id = Uuid::new_v4().to_string();
@@ -343,7 +356,7 @@ async fn installation_id() -> Result<String> {
.write_kvp(legacy_key_name.to_string(), installation_id.clone())
.await?;
- Ok(installation_id)
+ Ok((installation_id, false))
}
}
@@ -166,12 +166,17 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
// vim::observe_keystrokes(cx);
- // cx.on_window_should_close(|workspace, cx| {
- // if let Some(task) = workspace.close(&Default::default(), cx) {
- // task.detach_and_log_err(cx);
- // }
- // false
- // });
+ let handle = cx.view().downgrade();
+ cx.on_window_should_close(move |cx| {
+ handle
+ .update(cx, |workspace, cx| {
+ if let Some(task) = workspace.close(&Default::default(), cx) {
+ task.detach_and_log_err(cx);
+ }
+ false
+ })
+ .unwrap_or(true)
+ });
cx.spawn(|workspace_handle, mut cx| async move {
let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone());
@@ -59,7 +59,6 @@ if ! git show-ref --quiet refs/heads/${prev_minor_branch_name}; then
echo "previous branch ${minor_branch_name} doesn't exist"
exit 1
fi
-# TODO kb anything else for RELEASE_CHANNEL == nightly needs to be done below?
if [[ $(git show ${prev_minor_branch_name}:crates/zed/RELEASE_CHANNEL) != preview ]]; then
echo "release channel on branch ${prev_minor_branch_name} should be preview"
exit 1
@@ -123,7 +123,6 @@ CARGO_TARGET_DIR="$TARGET_DIR" cargo doc --workspace --no-deps --open \
--exclude snippet \
--exclude sqlez \
--exclude sqlez_macros \
---exclude storybook3 \
--exclude sum_tree \
--exclude terminal \
--exclude terminal_view \
@@ -4,6 +4,7 @@ const {spawn, execFileSync} = require('child_process')
const RESOLUTION_REGEX = /(\d+) x (\d+)/
const DIGIT_FLAG_REGEX = /^--?(\d+)$/
+const ZED_2_MODE = "--zed2"
const args = process.argv.slice(2)
@@ -14,6 +15,7 @@ if (digitMatch) {
instanceCount = parseInt(digitMatch[1])
args.shift()
}
+const isZed2 = args.some(arg => arg === ZED_2_MODE);
if (instanceCount > 4) {
throw new Error('Cannot spawn more than 4 instances')
}
@@ -70,11 +72,12 @@ const positions = [
`${instanceWidth},${instanceHeight}`
]
-execFileSync('cargo', ['build'], {stdio: 'inherit'})
-
+const buildArgs = isZed2 ? ["build", "-p", "zed2"] : ["build"]
+const zedBinary = isZed2 ? "target/debug/Zed2" : "target/debug/Zed"
+execFileSync('cargo', buildArgs, { stdio: 'inherit' })
setTimeout(() => {
for (let i = 0; i < instanceCount; i++) {
- spawn('target/debug/Zed', i == 0 ? args : [], {
+ spawn(zedBinary, i == 0 ? args : [], {
stdio: 'inherit',
env: {
ZED_IMPERSONATE: users[i],