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: |
@@ -1186,12 +1186,14 @@ version = "0.1.0"
dependencies = [
"anyhow",
"async-broadcast",
+ "async-trait",
"audio2",
"client2",
"collections",
"fs2",
"futures 0.3.28",
"gpui2",
+ "image",
"language2",
"live_kit_client2",
"log",
@@ -1203,7 +1205,10 @@ dependencies = [
"serde_derive",
"serde_json",
"settings2",
+ "smallvec",
+ "ui2",
"util",
+ "workspace2",
]
[[package]]
@@ -1398,7 +1403,7 @@ dependencies = [
"smol",
"sum_tree",
"tempfile",
- "text",
+ "text2",
"thiserror",
"time",
"tiny_http",
@@ -1664,7 +1669,7 @@ dependencies = [
[[package]]
name = "collab"
-version = "0.28.0"
+version = "0.29.0"
dependencies = [
"anyhow",
"async-trait",
@@ -1869,7 +1874,7 @@ dependencies = [
"editor2",
"feature_flags2",
"futures 0.3.28",
- "fuzzy",
+ "fuzzy2",
"gpui2",
"language2",
"lazy_static",
@@ -6763,7 +6768,6 @@ dependencies = [
"anyhow",
"client2",
"collections",
- "context_menu",
"db2",
"editor2",
"futures 0.3.28",
@@ -8032,6 +8036,35 @@ dependencies = [
"workspace",
]
+[[package]]
+name = "search2"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "bitflags 1.3.2",
+ "client2",
+ "collections",
+ "editor2",
+ "futures 0.3.28",
+ "gpui2",
+ "language2",
+ "log",
+ "menu2",
+ "postage",
+ "project2",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "settings2",
+ "smallvec",
+ "smol",
+ "theme2",
+ "ui2",
+ "unindent",
+ "util",
+ "workspace2",
+]
+
[[package]]
name = "security-framework"
version = "2.9.2"
@@ -8855,6 +8888,13 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+[[package]]
+name = "story"
+version = "0.1.0"
+dependencies = [
+ "gpui2",
+]
+
[[package]]
name = "storybook2"
version = "0.1.0"
@@ -8876,6 +8916,7 @@ dependencies = [
"settings2",
"simplelog",
"smallvec",
+ "story",
"strum",
"theme",
"theme2",
@@ -8883,17 +8924,6 @@ dependencies = [
"util",
]
-[[package]]
-name = "storybook3"
-version = "0.1.0"
-dependencies = [
- "anyhow",
- "gpui2",
- "settings2",
- "theme2",
- "ui2",
-]
-
[[package]]
name = "stringprep"
version = "0.1.4"
@@ -9397,6 +9427,7 @@ dependencies = [
"serde_derive",
"serde_json",
"settings2",
+ "story",
"toml 0.5.11",
"util",
"uuid 1.4.1",
@@ -9899,7 +9930,7 @@ dependencies = [
[[package]]
name = "tree-sitter"
version = "0.20.10"
-source = "git+https://github.com/tree-sitter/tree-sitter?rev=35a6052fbcafc5e5fc0f9415b8652be7dcaf7222#35a6052fbcafc5e5fc0f9415b8652be7dcaf7222"
+source = "git+https://github.com/tree-sitter/tree-sitter?rev=3b0159d25559b603af566ade3c83d930bf466db1#3b0159d25559b603af566ade3c83d930bf466db1"
dependencies = [
"cc",
"regex",
@@ -10147,6 +10178,15 @@ dependencies = [
"tree-sitter",
]
+[[package]]
+name = "tree-sitter-uiua"
+version = "0.3.3"
+source = "git+https://github.com/shnarazk/tree-sitter-uiua?rev=9260f11be5900beda4ee6d1a24ab8ddfaf5a19b2#9260f11be5900beda4ee6d1a24ab8ddfaf5a19b2"
+dependencies = [
+ "cc",
+ "tree-sitter",
+]
+
[[package]]
name = "tree-sitter-vue"
version = "0.0.1"
@@ -10240,6 +10280,7 @@ dependencies = [
"serde",
"settings2",
"smallvec",
+ "story",
"strum",
"theme2",
]
@@ -11354,6 +11395,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"async-recursion 1.0.5",
+ "async-trait",
"bincode",
"call2",
"client2",
@@ -11466,7 +11508,7 @@ dependencies = [
[[package]]
name = "zed"
-version = "0.114.0"
+version = "0.115.0"
dependencies = [
"activity_indicator",
"ai",
@@ -11583,6 +11625,7 @@ dependencies = [
"tree-sitter-svelte",
"tree-sitter-toml",
"tree-sitter-typescript",
+ "tree-sitter-uiua",
"tree-sitter-vue",
"tree-sitter-yaml",
"unindent",
@@ -11614,9 +11657,11 @@ dependencies = [
"async-recursion 0.3.2",
"async-tar",
"async-trait",
+ "audio2",
"auto_update2",
"backtrace",
"call2",
+ "channel2",
"chrono",
"cli",
"client2",
@@ -11634,7 +11679,6 @@ dependencies = [
"fs2",
"fsevent",
"futures 0.3.28",
- "fuzzy",
"go_to_line2",
"gpui2",
"ignore",
@@ -11662,6 +11706,7 @@ dependencies = [
"rsa 0.4.0",
"rust-embed",
"schemars",
+ "search2",
"serde",
"serde_derive",
"serde_json",
@@ -11704,6 +11749,7 @@ dependencies = [
"tree-sitter-svelte",
"tree-sitter-toml",
"tree-sitter-typescript",
+ "tree-sitter-uiua",
"tree-sitter-vue",
"tree-sitter-yaml",
"unindent",
@@ -90,6 +90,7 @@ members = [
"crates/rpc",
"crates/rpc2",
"crates/search",
+ "crates/search2",
"crates/settings",
"crates/settings2",
"crates/snippet",
@@ -97,7 +98,6 @@ members = [
"crates/sqlez_macros",
"crates/rich_text",
"crates/storybook2",
- "crates/storybook3",
"crates/sum_tree",
"crates/terminal",
"crates/terminal2",
@@ -110,6 +110,7 @@ members = [
"crates/ui2",
"crates/util",
"crates/semantic_index",
+ "crates/story",
"crates/vim",
"crates/vcs_menu",
"crates/workspace2",
@@ -194,8 +195,10 @@ tree-sitter-lua = "0.0.14"
tree-sitter-nix = { git = "https://github.com/nix-community/tree-sitter-nix", rev = "66e3e9ce9180ae08fc57372061006ef83f0abde7" }
tree-sitter-nu = { git = "https://github.com/nushell/tree-sitter-nu", rev = "786689b0562b9799ce53e824cb45a1a2a04dc673"}
tree-sitter-vue = {git = "https://github.com/zed-industries/tree-sitter-vue", rev = "9b6cb221ccb8d0b956fcb17e9a1efac2feefeb58"}
+tree-sitter-uiua = {git = "https://github.com/shnarazk/tree-sitter-uiua", rev = "9260f11be5900beda4ee6d1a24ab8ddfaf5a19b2"}
+
[patch.crates-io]
-tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "35a6052fbcafc5e5fc0f9415b8652be7dcaf7222" }
+tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "3b0159d25559b603af566ade3c83d930bf466db1" }
async-task = { git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e" }
# TODO - Remove when a version is released with this PR: https://github.com/servo/core-foundation-rs/pull/457
@@ -207,11 +210,12 @@ core-graphics = { git = "https://github.com/servo/core-foundation-rs", rev = "07
[profile.dev]
split-debuginfo = "unpacked"
+debug = "limited"
[profile.dev.package.taffy]
opt-level = 3
[profile.release]
-debug = true
+debug = "limited"
lto = "thin"
codegen-units = 1
@@ -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
}
@@ -84,8 +84,8 @@ impl Settings for AutoUpdateSetting {
pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut AppContext) {
AutoUpdateSetting::register(cx);
- cx.observe_new_views(|wokrspace: &mut Workspace, _cx| {
- wokrspace
+ cx.observe_new_views(|workspace: &mut Workspace, _cx| {
+ workspace
.register_action(|_, action: &Check, cx| check(action, cx))
.register_action(|_, _action: &CheckThatAutoUpdaterWorks, cx| {
let prompt = cx.prompt(gpui::PromptLevel::Info, "It does!", &["Ok"]);
@@ -94,6 +94,11 @@ pub fn init(http_client: Arc<dyn HttpClient>, server_url: String, cx: &mut AppCo
})
.detach();
});
+
+ // @nate - code to trigger update notification on launch
+ // workspace.show_notification(0, _cx, |cx| {
+ // cx.build_view(|_| UpdateNotification::new(SemanticVersion::from_str("1.1.1").unwrap()))
+ // });
})
.detach();
@@ -131,7 +136,7 @@ pub fn check(_: &Check, cx: &mut AppContext) {
}
}
-fn _view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) {
+pub fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) {
if let Some(auto_updater) = AutoUpdater::get(cx) {
let auto_updater = auto_updater.read(cx);
let server_url = &auto_updater.server_url;
@@ -1,87 +1,56 @@
-use gpui::{div, Div, EventEmitter, ParentComponent, Render, SemanticVersion, ViewContext};
-use menu::Cancel;
-use workspace::notifications::NotificationEvent;
+use gpui::{
+ div, DismissEvent, Div, EventEmitter, InteractiveElement, ParentElement, Render,
+ SemanticVersion, StatefulInteractiveElement, Styled, ViewContext,
+};
+use util::channel::ReleaseChannel;
+use workspace::ui::{h_stack, v_stack, Icon, IconElement, Label, StyledExt};
pub struct UpdateNotification {
- _version: SemanticVersion,
+ version: SemanticVersion,
}
-impl EventEmitter<NotificationEvent> for UpdateNotification {}
+impl EventEmitter<DismissEvent> for UpdateNotification {}
impl Render for UpdateNotification {
- type Element = Div<Self>;
-
- fn render(&mut self, _cx: &mut gpui::ViewContext<Self>) -> Self::Element {
- div().child("Updated zed!")
- // let theme = theme::current(cx).clone();
- // let theme = &theme.update_notification;
-
- // let app_name = cx.global::<ReleaseChannel>().display_name();
-
- // MouseEventHandler::new::<ViewReleaseNotes, _>(0, cx, |state, cx| {
- // Flex::column()
- // .with_child(
- // Flex::row()
- // .with_child(
- // Text::new(
- // format!("Updated to {app_name} {}", self.version),
- // theme.message.text.clone(),
- // )
- // .contained()
- // .with_style(theme.message.container)
- // .aligned()
- // .top()
- // .left()
- // .flex(1., true),
- // )
- // .with_child(
- // MouseEventHandler::new::<Cancel, _>(0, cx, |state, _| {
- // let style = theme.dismiss_button.style_for(state);
- // Svg::new("icons/x.svg")
- // .with_color(style.color)
- // .constrained()
- // .with_width(style.icon_width)
- // .aligned()
- // .contained()
- // .with_style(style.container)
- // .constrained()
- // .with_width(style.button_width)
- // .with_height(style.button_width)
- // })
- // .with_padding(Padding::uniform(5.))
- // .on_click(MouseButton::Left, move |_, this, cx| {
- // this.dismiss(&Default::default(), cx)
- // })
- // .aligned()
- // .constrained()
- // .with_height(cx.font_cache().line_height(theme.message.text.font_size))
- // .aligned()
- // .top()
- // .flex_float(),
- // ),
- // )
- // .with_child({
- // let style = theme.action_message.style_for(state);
- // Text::new("View the release notes", style.text.clone())
- // .contained()
- // .with_style(style.container)
- // })
- // .contained()
- // })
- // .with_cursor_style(CursorStyle::PointingHand)
- // .on_click(MouseButton::Left, |_, _, cx| {
- // crate::view_release_notes(&Default::default(), cx)
- // })
- // .into_any_named("update notification")
+ type Element = Div;
+
+ fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> Self::Element {
+ let app_name = cx.global::<ReleaseChannel>().display_name();
+
+ v_stack()
+ .elevation_3(cx)
+ .p_4()
+ .child(
+ h_stack()
+ .justify_between()
+ .child(Label::new(format!(
+ "Updated to {app_name} {}",
+ self.version
+ )))
+ .child(
+ div()
+ .id("cancel")
+ .child(IconElement::new(Icon::Close))
+ .cursor_pointer()
+ .on_click(cx.listener(|this, _, cx| this.dismiss(cx))),
+ ),
+ )
+ .child(
+ div()
+ .id("notes")
+ .child(Label::new("View the release notes"))
+ .cursor_pointer()
+ .on_click(|_, cx| crate::view_release_notes(&Default::default(), cx)),
+ )
}
}
impl UpdateNotification {
pub fn new(version: SemanticVersion) -> Self {
- Self { _version: version }
+ Self { version }
}
- pub fn _dismiss(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
- cx.emit(NotificationEvent::Dismiss);
+ pub fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
+ 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,208 @@ 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| {
+ let room = room.clone();
+ cx.spawn(|_, mut cx| async move {
+ room.update(&mut cx, |this, cx| this.toggle_mute(cx))??
+ .await
+ })
+ .detach_and_log_err(cx);
+ })
+ })
+ });
+ }
+ fn toggle_screen_share(&self, cx: &mut AppContext) {
+ self.active_call.as_ref().map(|call| {
+ call.0.update(cx, |this, cx| {
+ this.room().map(|room| {
+ room.update(cx, |this, cx| {
+ if this.is_screen_sharing() {
+ this.unshare_screen(cx).log_err();
+ } else {
+ let t = this.share_screen(cx);
+ cx.spawn(move |_, _| async move {
+ t.await.log_err();
+ })
+ .detach();
+ }
+ })
+ })
+ })
+ });
+ }
+ fn toggle_deafen(&self, cx: &mut AppContext) {
+ self.active_call.as_ref().map(|call| {
+ call.0.update(cx, |this, cx| {
+ this.room().map(|room| {
+ room.update(cx, |this, cx| {
+ this.toggle_deafen(cx).log_err();
+ })
+ })
+ })
+ });
+ }
+ fn is_deafened(&self, cx: &AppContext) -> Option<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;
@@ -21,7 +21,7 @@ use live_kit_client::{
};
use postage::{sink::Sink, stream::Stream, watch};
use project::Project;
-use settings::Settings;
+use settings::Settings as _;
use std::{future::Future, mem, sync::Arc, time::Duration};
use util::{post_inc, ResultExt, TryFutureExt};
@@ -1267,7 +1267,6 @@ impl Room {
.ok_or_else(|| anyhow!("live-kit was not initialized"))?
.await
};
-
let publication = publish_track.await;
this.upgrade()
.ok_or_else(|| anyhow!("room was dropped"))?
@@ -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)))
+ }
+}
@@ -18,7 +18,7 @@ db = { package = "db2", path = "../db2" }
gpui = { package = "gpui2", path = "../gpui2" }
util = { path = "../util" }
rpc = { package = "rpc2", path = "../rpc2" }
-text = { path = "../text" }
+text = { package = "text2", path = "../text2" }
language = { package = "language2", path = "../language2" }
settings = { package = "settings2", path = "../settings2" }
feature_flags = { package = "feature_flags2", path = "../feature_flags2" }
@@ -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| {
@@ -33,7 +33,7 @@ collections = { path = "../collections" }
# drag_and_drop = { path = "../drag_and_drop" }
editor = { package="editor2", path = "../editor2" }
#feedback = { path = "../feedback" }
-fuzzy = { path = "../fuzzy" }
+fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
gpui = { package = "gpui2", path = "../gpui2" }
language = { package = "language2", path = "../language2" }
menu = { package = "menu2", path = "../menu2" }
@@ -1,5 +1,6 @@
+#![allow(unused)]
// mod channel_modal;
-// mod contact_finder;
+mod contact_finder;
// use crate::{
// channel_view::{self, ChannelView},
@@ -15,7 +16,7 @@
// proto::{self, PeerId},
// Client, Contact, User, UserStore,
// };
-// use contact_finder::ContactFinder;
+use contact_finder::ContactFinder;
// use context_menu::{ContextMenu, ContextMenuItem};
// use db::kvp::KEY_VALUE_STORE;
// use drag_and_drop::{DragAndDrop, Draggable};
@@ -155,20 +156,30 @@ actions!(
const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel";
-use std::sync::Arc;
+use std::{iter::once, mem, sync::Arc};
+use call::ActiveCall;
+use channel::{Channel, ChannelId, ChannelStore};
+use client::{Client, Contact, User, UserStore};
use db::kvp::KEY_VALUE_STORE;
+use editor::Editor;
+use feature_flags::{ChannelsAlpha, FeatureFlagAppExt};
+use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{
- actions, div, serde_json, AppContext, AsyncWindowContext, Div, EventEmitter, FocusHandle,
- Focusable, FocusableView, InteractiveComponent, ParentComponent, Render, View, ViewContext,
- VisualContext, WeakView,
+ actions, div, img, serde_json, AppContext, AsyncWindowContext, Div, EventEmitter, FocusHandle,
+ Focusable, FocusableView, InteractiveElement, IntoElement, Model, ParentElement, Render,
+ RenderOnce, SharedString, Styled, Subscription, View, ViewContext, VisualContext, WeakView,
};
use project::Fs;
use serde_derive::{Deserialize, Serialize};
use settings::Settings;
-use util::ResultExt;
+use ui::{
+ h_stack, v_stack, Avatar, Button, Icon, IconButton, Label, List, ListHeader, ListItem, Tooltip,
+};
+use util::{maybe, ResultExt};
use workspace::{
dock::{DockPosition, Panel, PanelEvent},
+ notifications::NotifyResultExt,
Workspace,
};
@@ -266,26 +277,26 @@ pub fn init(cx: &mut AppContext) {
// );
}
-// #[derive(Debug)]
-// pub enum ChannelEditingState {
-// Create {
-// location: Option<ChannelId>,
-// pending_name: Option<String>,
-// },
-// Rename {
-// location: ChannelId,
-// pending_name: Option<String>,
-// },
-// }
+#[derive(Debug)]
+pub enum ChannelEditingState {
+ Create {
+ location: Option<ChannelId>,
+ pending_name: Option<String>,
+ },
+ Rename {
+ location: ChannelId,
+ pending_name: Option<String>,
+ },
+}
-// impl ChannelEditingState {
-// fn pending_name(&self) -> Option<&str> {
-// match self {
-// ChannelEditingState::Create { pending_name, .. } => pending_name.as_deref(),
-// ChannelEditingState::Rename { pending_name, .. } => pending_name.as_deref(),
-// }
-// }
-// }
+impl ChannelEditingState {
+ fn pending_name(&self) -> Option<&str> {
+ match self {
+ ChannelEditingState::Create { pending_name, .. } => pending_name.as_deref(),
+ ChannelEditingState::Rename { pending_name, .. } => pending_name.as_deref(),
+ }
+ }
+}
pub struct CollabPanel {
width: Option<f32>,
@@ -294,22 +305,22 @@ pub struct CollabPanel {
// channel_clipboard: Option<ChannelMoveClipboard>,
// pending_serialization: Task<Option<()>>,
// context_menu: ViewHandle<ContextMenu>,
- // filter_editor: ViewHandle<Editor>,
+ filter_editor: View<Editor>,
// channel_name_editor: ViewHandle<Editor>,
- // channel_editing_state: Option<ChannelEditingState>,
- // entries: Vec<ListEntry>,
+ channel_editing_state: Option<ChannelEditingState>,
+ entries: Vec<ListEntry>,
// selection: Option<usize>,
- // user_store: ModelHandle<UserStore>,
- // client: Arc<Client>,
- // channel_store: ModelHandle<ChannelStore>,
+ channel_store: Model<ChannelStore>,
+ user_store: Model<UserStore>,
+ client: Arc<Client>,
// project: ModelHandle<Project>,
- // match_candidates: Vec<StringMatchCandidate>,
+ match_candidates: Vec<StringMatchCandidate>,
// list_state: ListState<Self>,
- // subscriptions: Vec<Subscription>,
- // collapsed_sections: Vec<Section>,
- // collapsed_channels: Vec<ChannelId>,
+ subscriptions: Vec<Subscription>,
+ collapsed_sections: Vec<Section>,
+ collapsed_channels: Vec<ChannelId>,
// drag_target_channel: ChannelDragTarget,
- _workspace: WeakView<Workspace>,
+ workspace: WeakView<Workspace>,
// context_menu_on_selected: bool,
}
@@ -333,58 +344,58 @@ struct SerializedCollabPanel {
// Dismissed,
// }
-// #[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)]
-// enum Section {
-// ActiveCall,
-// Channels,
-// ChannelInvites,
-// ContactRequests,
-// Contacts,
-// Online,
-// Offline,
-// }
+#[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)]
+enum Section {
+ ActiveCall,
+ Channels,
+ ChannelInvites,
+ ContactRequests,
+ Contacts,
+ Online,
+ Offline,
+}
-// #[derive(Clone, Debug)]
-// enum ListEntry {
-// Header(Section),
-// CallParticipant {
-// user: Arc<User>,
-// peer_id: Option<PeerId>,
-// is_pending: bool,
-// },
-// ParticipantProject {
-// project_id: u64,
-// worktree_root_names: Vec<String>,
-// host_user_id: u64,
-// is_last: bool,
-// },
-// ParticipantScreen {
-// peer_id: Option<PeerId>,
-// is_last: bool,
-// },
-// IncomingRequest(Arc<User>),
-// OutgoingRequest(Arc<User>),
-// ChannelInvite(Arc<Channel>),
-// Channel {
-// channel: Arc<Channel>,
-// depth: usize,
-// has_children: bool,
-// },
-// ChannelNotes {
-// channel_id: ChannelId,
-// },
-// ChannelChat {
-// channel_id: ChannelId,
-// },
-// ChannelEditor {
-// depth: usize,
-// },
-// Contact {
-// contact: Arc<Contact>,
-// calling: bool,
-// },
-// ContactPlaceholder,
-// }
+#[derive(Clone, Debug)]
+enum ListEntry {
+ Header(Section),
+ // CallParticipant {
+ // user: Arc<User>,
+ // peer_id: Option<PeerId>,
+ // is_pending: bool,
+ // },
+ // ParticipantProject {
+ // project_id: u64,
+ // worktree_root_names: Vec<String>,
+ // host_user_id: u64,
+ // is_last: bool,
+ // },
+ // ParticipantScreen {
+ // peer_id: Option<PeerId>,
+ // is_last: bool,
+ // },
+ IncomingRequest(Arc<User>),
+ OutgoingRequest(Arc<User>),
+ // ChannelInvite(Arc<Channel>),
+ Channel {
+ channel: Arc<Channel>,
+ depth: usize,
+ has_children: bool,
+ },
+ // ChannelNotes {
+ // channel_id: ChannelId,
+ // },
+ // ChannelChat {
+ // channel_id: ChannelId,
+ // },
+ ChannelEditor {
+ depth: usize,
+ },
+ Contact {
+ contact: Arc<Contact>,
+ calling: bool,
+ },
+ ContactPlaceholder,
+}
// impl Entity for CollabPanel {
// type Event = Event;
@@ -395,16 +406,11 @@ impl CollabPanel {
cx.build_view(|cx| {
// let view_id = cx.view_id();
- // let filter_editor = cx.add_view(|cx| {
- // let mut editor = Editor::single_line(
- // Some(Arc::new(|theme| {
- // theme.collab_panel.user_query_editor.clone()
- // })),
- // cx,
- // );
- // editor.set_placeholder_text("Filter channels, contacts", cx);
- // editor
- // });
+ let filter_editor = cx.build_view(|cx| {
+ let mut editor = Editor::single_line(cx);
+ editor.set_placeholder_text("Filter channels, contacts", cx);
+ editor
+ });
// cx.subscribe(&filter_editor, |this, _, event, cx| {
// if let editor::Event::BufferEdited = event {
@@ -583,7 +589,7 @@ impl CollabPanel {
// }
// });
- let this = Self {
+ let mut this = Self {
width: None,
focus_handle: cx.focus_handle(),
// channel_clipboard: None,
@@ -591,25 +597,25 @@ impl CollabPanel {
// pending_serialization: Task::ready(None),
// context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)),
// channel_name_editor,
- // filter_editor,
- // entries: Vec::default(),
- // channel_editing_state: None,
+ filter_editor,
+ entries: Vec::default(),
+ channel_editing_state: None,
// selection: None,
- // user_store: workspace.user_store().clone(),
- // channel_store: ChannelStore::global(cx),
+ channel_store: ChannelStore::global(cx),
+ user_store: workspace.user_store().clone(),
// project: workspace.project().clone(),
- // subscriptions: Vec::default(),
- // match_candidates: Vec::default(),
- // collapsed_sections: vec![Section::Offline],
- // collapsed_channels: Vec::default(),
- _workspace: workspace.weak_handle(),
- // client: workspace.app_state().client.clone(),
+ subscriptions: Vec::default(),
+ match_candidates: Vec::default(),
+ collapsed_sections: vec![Section::Offline],
+ collapsed_channels: Vec::default(),
+ workspace: workspace.weak_handle(),
+ client: workspace.app_state().client.clone(),
// context_menu_on_selected: true,
// drag_target_channel: ChannelDragTarget::None,
// list_state,
};
- // this.update_entries(false, cx);
+ this.update_entries(false, cx);
// // Update the dock position when the setting changes.
// let mut old_dock_position = this.position(cx);
@@ -626,10 +632,10 @@ impl CollabPanel {
// );
// let active_call = ActiveCall::global(cx);
- // this.subscriptions
- // .push(cx.observe(&this.user_store, |this, _, cx| {
- // this.update_entries(true, cx)
- // }));
+ this.subscriptions
+ .push(cx.observe(&this.user_store, |this, _, cx| {
+ this.update_entries(true, cx)
+ }));
// this.subscriptions
// .push(cx.observe(&this.channel_store, |this, _, cx| {
// this.update_entries(true, cx)
@@ -663,6 +669,9 @@ impl CollabPanel {
})
}
+ fn contacts(&self, cx: &AppContext) -> Option<Vec<Arc<Contact>>> {
+ Some(self.user_store.read(cx).contacts().to_owned())
+ }
pub async fn load(
workspace: WeakView<Workspace>,
mut cx: AsyncWindowContext,
@@ -715,449 +724,449 @@ impl CollabPanel {
// );
// }
- // fn update_entries(&mut self, select_same_item: bool, cx: &mut ViewContext<Self>) {
- // let channel_store = self.channel_store.read(cx);
- // let user_store = self.user_store.read(cx);
- // let query = self.filter_editor.read(cx).text(cx);
- // let executor = cx.background().clone();
-
- // let prev_selected_entry = self.selection.and_then(|ix| self.entries.get(ix).cloned());
- // let old_entries = mem::take(&mut self.entries);
- // let mut scroll_to_top = false;
-
- // if let Some(room) = ActiveCall::global(cx).read(cx).room() {
- // self.entries.push(ListEntry::Header(Section::ActiveCall));
- // if !old_entries
- // .iter()
- // .any(|entry| matches!(entry, ListEntry::Header(Section::ActiveCall)))
- // {
- // scroll_to_top = true;
- // }
-
- // if !self.collapsed_sections.contains(&Section::ActiveCall) {
- // let room = room.read(cx);
-
- // if let Some(channel_id) = room.channel_id() {
- // self.entries.push(ListEntry::ChannelNotes { channel_id });
- // self.entries.push(ListEntry::ChannelChat { channel_id })
- // }
-
- // // Populate the active user.
- // if let Some(user) = user_store.current_user() {
- // self.match_candidates.clear();
- // self.match_candidates.push(StringMatchCandidate {
- // id: 0,
- // string: user.github_login.clone(),
- // char_bag: user.github_login.chars().collect(),
- // });
- // let matches = executor.block(match_strings(
- // &self.match_candidates,
- // &query,
- // true,
- // usize::MAX,
- // &Default::default(),
- // executor.clone(),
- // ));
- // if !matches.is_empty() {
- // let user_id = user.id;
- // self.entries.push(ListEntry::CallParticipant {
- // user,
- // peer_id: None,
- // is_pending: false,
- // });
- // let mut projects = room.local_participant().projects.iter().peekable();
- // while let Some(project) = projects.next() {
- // self.entries.push(ListEntry::ParticipantProject {
- // project_id: project.id,
- // worktree_root_names: project.worktree_root_names.clone(),
- // host_user_id: user_id,
- // is_last: projects.peek().is_none() && !room.is_screen_sharing(),
- // });
- // }
- // if room.is_screen_sharing() {
- // self.entries.push(ListEntry::ParticipantScreen {
- // peer_id: None,
- // is_last: true,
- // });
- // }
- // }
- // }
-
- // // Populate remote participants.
- // self.match_candidates.clear();
- // self.match_candidates
- // .extend(room.remote_participants().iter().map(|(_, participant)| {
- // StringMatchCandidate {
- // id: participant.user.id as usize,
- // string: participant.user.github_login.clone(),
- // char_bag: participant.user.github_login.chars().collect(),
- // }
- // }));
- // let matches = executor.block(match_strings(
- // &self.match_candidates,
- // &query,
- // true,
- // usize::MAX,
- // &Default::default(),
- // executor.clone(),
- // ));
- // for mat in matches {
- // let user_id = mat.candidate_id as u64;
- // let participant = &room.remote_participants()[&user_id];
- // self.entries.push(ListEntry::CallParticipant {
- // user: participant.user.clone(),
- // peer_id: Some(participant.peer_id),
- // is_pending: false,
- // });
- // let mut projects = participant.projects.iter().peekable();
- // while let Some(project) = projects.next() {
- // self.entries.push(ListEntry::ParticipantProject {
- // project_id: project.id,
- // worktree_root_names: project.worktree_root_names.clone(),
- // host_user_id: participant.user.id,
- // is_last: projects.peek().is_none()
- // && participant.video_tracks.is_empty(),
- // });
- // }
- // if !participant.video_tracks.is_empty() {
- // self.entries.push(ListEntry::ParticipantScreen {
- // peer_id: Some(participant.peer_id),
- // is_last: true,
- // });
- // }
- // }
-
- // // Populate pending participants.
- // self.match_candidates.clear();
- // self.match_candidates
- // .extend(room.pending_participants().iter().enumerate().map(
- // |(id, participant)| StringMatchCandidate {
- // id,
- // string: participant.github_login.clone(),
- // char_bag: participant.github_login.chars().collect(),
- // },
- // ));
- // let matches = executor.block(match_strings(
- // &self.match_candidates,
- // &query,
- // true,
- // usize::MAX,
- // &Default::default(),
- // executor.clone(),
- // ));
- // self.entries
- // .extend(matches.iter().map(|mat| ListEntry::CallParticipant {
- // user: room.pending_participants()[mat.candidate_id].clone(),
- // peer_id: None,
- // is_pending: true,
- // }));
- // }
- // }
-
- // let mut request_entries = Vec::new();
-
- // if cx.has_flag::<ChannelsAlpha>() {
- // self.entries.push(ListEntry::Header(Section::Channels));
-
- // if channel_store.channel_count() > 0 || self.channel_editing_state.is_some() {
- // self.match_candidates.clear();
- // self.match_candidates
- // .extend(channel_store.ordered_channels().enumerate().map(
- // |(ix, (_, channel))| StringMatchCandidate {
- // id: ix,
- // string: channel.name.clone(),
- // char_bag: channel.name.chars().collect(),
- // },
- // ));
- // let matches = executor.block(match_strings(
- // &self.match_candidates,
- // &query,
- // true,
- // usize::MAX,
- // &Default::default(),
- // executor.clone(),
- // ));
- // if let Some(state) = &self.channel_editing_state {
- // if matches!(state, ChannelEditingState::Create { location: None, .. }) {
- // self.entries.push(ListEntry::ChannelEditor { depth: 0 });
- // }
- // }
- // let mut collapse_depth = None;
- // for mat in matches {
- // let channel = channel_store.channel_at_index(mat.candidate_id).unwrap();
- // let depth = channel.parent_path.len();
-
- // if collapse_depth.is_none() && self.is_channel_collapsed(channel.id) {
- // collapse_depth = Some(depth);
- // } else if let Some(collapsed_depth) = collapse_depth {
- // if depth > collapsed_depth {
- // continue;
- // }
- // if self.is_channel_collapsed(channel.id) {
- // collapse_depth = Some(depth);
- // } else {
- // collapse_depth = None;
- // }
- // }
-
- // let has_children = channel_store
- // .channel_at_index(mat.candidate_id + 1)
- // .map_or(false, |next_channel| {
- // next_channel.parent_path.ends_with(&[channel.id])
- // });
-
- // match &self.channel_editing_state {
- // Some(ChannelEditingState::Create {
- // location: parent_id,
- // ..
- // }) if *parent_id == Some(channel.id) => {
- // self.entries.push(ListEntry::Channel {
- // channel: channel.clone(),
- // depth,
- // has_children: false,
- // });
- // self.entries
- // .push(ListEntry::ChannelEditor { depth: depth + 1 });
- // }
- // Some(ChannelEditingState::Rename {
- // location: parent_id,
- // ..
- // }) if parent_id == &channel.id => {
- // self.entries.push(ListEntry::ChannelEditor { depth });
- // }
- // _ => {
- // self.entries.push(ListEntry::Channel {
- // channel: channel.clone(),
- // depth,
- // has_children,
- // });
- // }
- // }
- // }
- // }
-
- // let channel_invites = channel_store.channel_invitations();
- // if !channel_invites.is_empty() {
- // self.match_candidates.clear();
- // self.match_candidates
- // .extend(channel_invites.iter().enumerate().map(|(ix, channel)| {
- // StringMatchCandidate {
- // id: ix,
- // string: channel.name.clone(),
- // char_bag: channel.name.chars().collect(),
- // }
- // }));
- // let matches = executor.block(match_strings(
- // &self.match_candidates,
- // &query,
- // true,
- // usize::MAX,
- // &Default::default(),
- // executor.clone(),
- // ));
- // request_entries.extend(matches.iter().map(|mat| {
- // ListEntry::ChannelInvite(channel_invites[mat.candidate_id].clone())
- // }));
-
- // if !request_entries.is_empty() {
- // self.entries
- // .push(ListEntry::Header(Section::ChannelInvites));
- // if !self.collapsed_sections.contains(&Section::ChannelInvites) {
- // self.entries.append(&mut request_entries);
- // }
- // }
- // }
- // }
-
- // self.entries.push(ListEntry::Header(Section::Contacts));
-
- // request_entries.clear();
- // let incoming = user_store.incoming_contact_requests();
- // if !incoming.is_empty() {
- // self.match_candidates.clear();
- // self.match_candidates
- // .extend(
- // incoming
- // .iter()
- // .enumerate()
- // .map(|(ix, user)| StringMatchCandidate {
- // id: ix,
- // string: user.github_login.clone(),
- // char_bag: user.github_login.chars().collect(),
- // }),
- // );
- // let matches = executor.block(match_strings(
- // &self.match_candidates,
- // &query,
- // true,
- // usize::MAX,
- // &Default::default(),
- // executor.clone(),
- // ));
- // request_entries.extend(
- // matches
- // .iter()
- // .map(|mat| ListEntry::IncomingRequest(incoming[mat.candidate_id].clone())),
- // );
- // }
-
- // let outgoing = user_store.outgoing_contact_requests();
- // if !outgoing.is_empty() {
- // self.match_candidates.clear();
- // self.match_candidates
- // .extend(
- // outgoing
- // .iter()
- // .enumerate()
- // .map(|(ix, user)| StringMatchCandidate {
- // id: ix,
- // string: user.github_login.clone(),
- // char_bag: user.github_login.chars().collect(),
- // }),
- // );
- // let matches = executor.block(match_strings(
- // &self.match_candidates,
- // &query,
- // true,
- // usize::MAX,
- // &Default::default(),
- // executor.clone(),
- // ));
- // request_entries.extend(
- // matches
- // .iter()
- // .map(|mat| ListEntry::OutgoingRequest(outgoing[mat.candidate_id].clone())),
- // );
- // }
-
- // if !request_entries.is_empty() {
- // self.entries
- // .push(ListEntry::Header(Section::ContactRequests));
- // if !self.collapsed_sections.contains(&Section::ContactRequests) {
- // self.entries.append(&mut request_entries);
- // }
- // }
-
- // let contacts = user_store.contacts();
- // if !contacts.is_empty() {
- // self.match_candidates.clear();
- // self.match_candidates
- // .extend(
- // contacts
- // .iter()
- // .enumerate()
- // .map(|(ix, contact)| StringMatchCandidate {
- // id: ix,
- // string: contact.user.github_login.clone(),
- // char_bag: contact.user.github_login.chars().collect(),
- // }),
- // );
-
- // let matches = executor.block(match_strings(
- // &self.match_candidates,
- // &query,
- // true,
- // usize::MAX,
- // &Default::default(),
- // executor.clone(),
- // ));
-
- // let (online_contacts, offline_contacts) = matches
- // .iter()
- // .partition::<Vec<_>, _>(|mat| contacts[mat.candidate_id].online);
-
- // for (matches, section) in [
- // (online_contacts, Section::Online),
- // (offline_contacts, Section::Offline),
- // ] {
- // if !matches.is_empty() {
- // self.entries.push(ListEntry::Header(section));
- // if !self.collapsed_sections.contains(§ion) {
- // let active_call = &ActiveCall::global(cx).read(cx);
- // for mat in matches {
- // let contact = &contacts[mat.candidate_id];
- // self.entries.push(ListEntry::Contact {
- // contact: contact.clone(),
- // calling: active_call.pending_invites().contains(&contact.user.id),
- // });
- // }
- // }
- // }
- // }
- // }
-
- // if incoming.is_empty() && outgoing.is_empty() && contacts.is_empty() {
- // self.entries.push(ListEntry::ContactPlaceholder);
- // }
-
- // if select_same_item {
- // if let Some(prev_selected_entry) = prev_selected_entry {
- // self.selection.take();
- // for (ix, entry) in self.entries.iter().enumerate() {
- // if *entry == prev_selected_entry {
- // self.selection = Some(ix);
- // break;
- // }
- // }
- // }
- // } else {
- // self.selection = self.selection.and_then(|prev_selection| {
- // if self.entries.is_empty() {
- // None
- // } else {
- // Some(prev_selection.min(self.entries.len() - 1))
- // }
- // });
- // }
-
- // let old_scroll_top = self.list_state.logical_scroll_top();
-
- // self.list_state.reset(self.entries.len());
+ fn update_entries(&mut self, select_same_item: bool, cx: &mut ViewContext<Self>) {
+ let channel_store = self.channel_store.read(cx);
+ let user_store = self.user_store.read(cx);
+ let query = self.filter_editor.read(cx).text(cx);
+ let executor = cx.background_executor().clone();
+
+ // let prev_selected_entry = self.selection.and_then(|ix| self.entries.get(ix).cloned());
+ let _old_entries = mem::take(&mut self.entries);
+ // let mut scroll_to_top = false;
+
+ // if let Some(room) = ActiveCall::global(cx).read(cx).room() {
+ // self.entries.push(ListEntry::Header(Section::ActiveCall));
+ // if !old_entries
+ // .iter()
+ // .any(|entry| matches!(entry, ListEntry::Header(Section::ActiveCall)))
+ // {
+ // scroll_to_top = true;
+ // }
+
+ // if !self.collapsed_sections.contains(&Section::ActiveCall) {
+ // let room = room.read(cx);
+
+ // if let Some(channel_id) = room.channel_id() {
+ // self.entries.push(ListEntry::ChannelNotes { channel_id });
+ // self.entries.push(ListEntry::ChannelChat { channel_id })
+ // }
+
+ // // Populate the active user.
+ // if let Some(user) = user_store.current_user() {
+ // self.match_candidates.clear();
+ // self.match_candidates.push(StringMatchCandidate {
+ // id: 0,
+ // string: user.github_login.clone(),
+ // char_bag: user.github_login.chars().collect(),
+ // });
+ // let matches = executor.block(match_strings(
+ // &self.match_candidates,
+ // &query,
+ // true,
+ // usize::MAX,
+ // &Default::default(),
+ // executor.clone(),
+ // ));
+ // if !matches.is_empty() {
+ // let user_id = user.id;
+ // self.entries.push(ListEntry::CallParticipant {
+ // user,
+ // peer_id: None,
+ // is_pending: false,
+ // });
+ // let mut projects = room.local_participant().projects.iter().peekable();
+ // while let Some(project) = projects.next() {
+ // self.entries.push(ListEntry::ParticipantProject {
+ // project_id: project.id,
+ // worktree_root_names: project.worktree_root_names.clone(),
+ // host_user_id: user_id,
+ // is_last: projects.peek().is_none() && !room.is_screen_sharing(),
+ // });
+ // }
+ // if room.is_screen_sharing() {
+ // self.entries.push(ListEntry::ParticipantScreen {
+ // peer_id: None,
+ // is_last: true,
+ // });
+ // }
+ // }
+ // }
+
+ // // Populate remote participants.
+ // self.match_candidates.clear();
+ // self.match_candidates
+ // .extend(room.remote_participants().iter().map(|(_, participant)| {
+ // StringMatchCandidate {
+ // id: participant.user.id as usize,
+ // string: participant.user.github_login.clone(),
+ // char_bag: participant.user.github_login.chars().collect(),
+ // }
+ // }));
+ // let matches = executor.block(match_strings(
+ // &self.match_candidates,
+ // &query,
+ // true,
+ // usize::MAX,
+ // &Default::default(),
+ // executor.clone(),
+ // ));
+ // for mat in matches {
+ // let user_id = mat.candidate_id as u64;
+ // let participant = &room.remote_participants()[&user_id];
+ // self.entries.push(ListEntry::CallParticipant {
+ // user: participant.user.clone(),
+ // peer_id: Some(participant.peer_id),
+ // is_pending: false,
+ // });
+ // let mut projects = participant.projects.iter().peekable();
+ // while let Some(project) = projects.next() {
+ // self.entries.push(ListEntry::ParticipantProject {
+ // project_id: project.id,
+ // worktree_root_names: project.worktree_root_names.clone(),
+ // host_user_id: participant.user.id,
+ // is_last: projects.peek().is_none()
+ // && participant.video_tracks.is_empty(),
+ // });
+ // }
+ // if !participant.video_tracks.is_empty() {
+ // self.entries.push(ListEntry::ParticipantScreen {
+ // peer_id: Some(participant.peer_id),
+ // is_last: true,
+ // });
+ // }
+ // }
+
+ // // Populate pending participants.
+ // self.match_candidates.clear();
+ // self.match_candidates
+ // .extend(room.pending_participants().iter().enumerate().map(
+ // |(id, participant)| StringMatchCandidate {
+ // id,
+ // string: participant.github_login.clone(),
+ // char_bag: participant.github_login.chars().collect(),
+ // },
+ // ));
+ // let matches = executor.block(match_strings(
+ // &self.match_candidates,
+ // &query,
+ // true,
+ // usize::MAX,
+ // &Default::default(),
+ // executor.clone(),
+ // ));
+ // self.entries
+ // .extend(matches.iter().map(|mat| ListEntry::CallParticipant {
+ // user: room.pending_participants()[mat.candidate_id].clone(),
+ // peer_id: None,
+ // is_pending: true,
+ // }));
+ // }
+ // }
+
+ let mut request_entries = Vec::new();
+
+ if cx.has_flag::<ChannelsAlpha>() {
+ self.entries.push(ListEntry::Header(Section::Channels));
+
+ if channel_store.channel_count() > 0 || self.channel_editing_state.is_some() {
+ self.match_candidates.clear();
+ self.match_candidates
+ .extend(channel_store.ordered_channels().enumerate().map(
+ |(ix, (_, channel))| StringMatchCandidate {
+ id: ix,
+ string: channel.name.clone(),
+ char_bag: channel.name.chars().collect(),
+ },
+ ));
+ let matches = executor.block(match_strings(
+ &self.match_candidates,
+ &query,
+ true,
+ usize::MAX,
+ &Default::default(),
+ executor.clone(),
+ ));
+ if let Some(state) = &self.channel_editing_state {
+ if matches!(state, ChannelEditingState::Create { location: None, .. }) {
+ self.entries.push(ListEntry::ChannelEditor { depth: 0 });
+ }
+ }
+ let mut collapse_depth = None;
+ for mat in matches {
+ let channel = channel_store.channel_at_index(mat.candidate_id).unwrap();
+ let depth = channel.parent_path.len();
+
+ if collapse_depth.is_none() && self.is_channel_collapsed(channel.id) {
+ collapse_depth = Some(depth);
+ } else if let Some(collapsed_depth) = collapse_depth {
+ if depth > collapsed_depth {
+ continue;
+ }
+ if self.is_channel_collapsed(channel.id) {
+ collapse_depth = Some(depth);
+ } else {
+ collapse_depth = None;
+ }
+ }
+
+ let has_children = channel_store
+ .channel_at_index(mat.candidate_id + 1)
+ .map_or(false, |next_channel| {
+ next_channel.parent_path.ends_with(&[channel.id])
+ });
+
+ match &self.channel_editing_state {
+ Some(ChannelEditingState::Create {
+ location: parent_id,
+ ..
+ }) if *parent_id == Some(channel.id) => {
+ self.entries.push(ListEntry::Channel {
+ channel: channel.clone(),
+ depth,
+ has_children: false,
+ });
+ self.entries
+ .push(ListEntry::ChannelEditor { depth: depth + 1 });
+ }
+ Some(ChannelEditingState::Rename {
+ location: parent_id,
+ ..
+ }) if parent_id == &channel.id => {
+ self.entries.push(ListEntry::ChannelEditor { depth });
+ }
+ _ => {
+ self.entries.push(ListEntry::Channel {
+ channel: channel.clone(),
+ depth,
+ has_children,
+ });
+ }
+ }
+ }
+ }
- // if scroll_to_top {
- // self.list_state.scroll_to(ListOffset::default());
- // } else {
- // // Attempt to maintain the same scroll position.
- // if let Some(old_top_entry) = old_entries.get(old_scroll_top.item_ix) {
- // let new_scroll_top = self
- // .entries
- // .iter()
- // .position(|entry| entry == old_top_entry)
- // .map(|item_ix| ListOffset {
- // item_ix,
- // offset_in_item: old_scroll_top.offset_in_item,
- // })
- // .or_else(|| {
- // let entry_after_old_top = old_entries.get(old_scroll_top.item_ix + 1)?;
- // let item_ix = self
- // .entries
- // .iter()
- // .position(|entry| entry == entry_after_old_top)?;
- // Some(ListOffset {
- // item_ix,
- // offset_in_item: 0.,
- // })
- // })
- // .or_else(|| {
- // let entry_before_old_top =
- // old_entries.get(old_scroll_top.item_ix.saturating_sub(1))?;
- // let item_ix = self
- // .entries
- // .iter()
- // .position(|entry| entry == entry_before_old_top)?;
- // Some(ListOffset {
- // item_ix,
- // offset_in_item: 0.,
- // })
- // });
+ // let channel_invites = channel_store.channel_invitations();
+ // if !channel_invites.is_empty() {
+ // self.match_candidates.clear();
+ // self.match_candidates
+ // .extend(channel_invites.iter().enumerate().map(|(ix, channel)| {
+ // StringMatchCandidate {
+ // id: ix,
+ // string: channel.name.clone(),
+ // char_bag: channel.name.chars().collect(),
+ // }
+ // }));
+ // let matches = executor.block(match_strings(
+ // &self.match_candidates,
+ // &query,
+ // true,
+ // usize::MAX,
+ // &Default::default(),
+ // executor.clone(),
+ // ));
+ // request_entries.extend(matches.iter().map(|mat| {
+ // ListEntry::ChannelInvite(channel_invites[mat.candidate_id].clone())
+ // }));
- // self.list_state
- // .scroll_to(new_scroll_top.unwrap_or(old_scroll_top));
- // }
- // }
+ // if !request_entries.is_empty() {
+ // self.entries
+ // .push(ListEntry::Header(Section::ChannelInvites));
+ // if !self.collapsed_sections.contains(&Section::ChannelInvites) {
+ // self.entries.append(&mut request_entries);
+ // }
+ // }
+ // }
+ }
+
+ self.entries.push(ListEntry::Header(Section::Contacts));
+
+ request_entries.clear();
+ let incoming = user_store.incoming_contact_requests();
+ if !incoming.is_empty() {
+ self.match_candidates.clear();
+ self.match_candidates
+ .extend(
+ incoming
+ .iter()
+ .enumerate()
+ .map(|(ix, user)| StringMatchCandidate {
+ id: ix,
+ string: user.github_login.clone(),
+ char_bag: user.github_login.chars().collect(),
+ }),
+ );
+ let matches = executor.block(match_strings(
+ &self.match_candidates,
+ &query,
+ true,
+ usize::MAX,
+ &Default::default(),
+ executor.clone(),
+ ));
+ request_entries.extend(
+ matches
+ .iter()
+ .map(|mat| ListEntry::IncomingRequest(incoming[mat.candidate_id].clone())),
+ );
+ }
+
+ let outgoing = user_store.outgoing_contact_requests();
+ if !outgoing.is_empty() {
+ self.match_candidates.clear();
+ self.match_candidates
+ .extend(
+ outgoing
+ .iter()
+ .enumerate()
+ .map(|(ix, user)| StringMatchCandidate {
+ id: ix,
+ string: user.github_login.clone(),
+ char_bag: user.github_login.chars().collect(),
+ }),
+ );
+ let matches = executor.block(match_strings(
+ &self.match_candidates,
+ &query,
+ true,
+ usize::MAX,
+ &Default::default(),
+ executor.clone(),
+ ));
+ request_entries.extend(
+ matches
+ .iter()
+ .map(|mat| ListEntry::OutgoingRequest(outgoing[mat.candidate_id].clone())),
+ );
+ }
+
+ if !request_entries.is_empty() {
+ self.entries
+ .push(ListEntry::Header(Section::ContactRequests));
+ if !self.collapsed_sections.contains(&Section::ContactRequests) {
+ self.entries.append(&mut request_entries);
+ }
+ }
+
+ let contacts = user_store.contacts();
+ if !contacts.is_empty() {
+ self.match_candidates.clear();
+ self.match_candidates
+ .extend(
+ contacts
+ .iter()
+ .enumerate()
+ .map(|(ix, contact)| StringMatchCandidate {
+ id: ix,
+ string: contact.user.github_login.clone(),
+ char_bag: contact.user.github_login.chars().collect(),
+ }),
+ );
+
+ let matches = executor.block(match_strings(
+ &self.match_candidates,
+ &query,
+ true,
+ usize::MAX,
+ &Default::default(),
+ executor.clone(),
+ ));
+
+ let (online_contacts, offline_contacts) = matches
+ .iter()
+ .partition::<Vec<_>, _>(|mat| contacts[mat.candidate_id].online);
+
+ for (matches, section) in [
+ (online_contacts, Section::Online),
+ (offline_contacts, Section::Offline),
+ ] {
+ if !matches.is_empty() {
+ self.entries.push(ListEntry::Header(section));
+ if !self.collapsed_sections.contains(§ion) {
+ let active_call = &ActiveCall::global(cx).read(cx);
+ for mat in matches {
+ let contact = &contacts[mat.candidate_id];
+ self.entries.push(ListEntry::Contact {
+ contact: contact.clone(),
+ calling: active_call.pending_invites().contains(&contact.user.id),
+ });
+ }
+ }
+ }
+ }
+ }
+
+ if incoming.is_empty() && outgoing.is_empty() && contacts.is_empty() {
+ self.entries.push(ListEntry::ContactPlaceholder);
+ }
+
+ // if select_same_item {
+ // if let Some(prev_selected_entry) = prev_selected_entry {
+ // self.selection.take();
+ // for (ix, entry) in self.entries.iter().enumerate() {
+ // if *entry == prev_selected_entry {
+ // self.selection = Some(ix);
+ // break;
+ // }
+ // }
+ // }
+ // } else {
+ // self.selection = self.selection.and_then(|prev_selection| {
+ // if self.entries.is_empty() {
+ // None
+ // } else {
+ // Some(prev_selection.min(self.entries.len() - 1))
+ // }
+ // });
+ // }
+
+ // let old_scroll_top = self.list_state.logical_scroll_top();
+
+ // self.list_state.reset(self.entries.len());
+
+ // if scroll_to_top {
+ // self.list_state.scroll_to(ListOffset::default());
+ // } else {
+ // // Attempt to maintain the same scroll position.
+ // if let Some(old_top_entry) = old_entries.get(old_scroll_top.item_ix) {
+ // let new_scroll_top = self
+ // .entries
+ // .iter()
+ // .position(|entry| entry == old_top_entry)
+ // .map(|item_ix| ListOffset {
+ // item_ix,
+ // offset_in_item: old_scroll_top.offset_in_item,
+ // })
+ // .or_else(|| {
+ // let entry_after_old_top = old_entries.get(old_scroll_top.item_ix + 1)?;
+ // let item_ix = self
+ // .entries
+ // .iter()
+ // .position(|entry| entry == entry_after_old_top)?;
+ // Some(ListOffset {
+ // item_ix,
+ // offset_in_item: 0.,
+ // })
+ // })
+ // .or_else(|| {
+ // let entry_before_old_top =
+ // old_entries.get(old_scroll_top.item_ix.saturating_sub(1))?;
+ // let item_ix = self
+ // .entries
+ // .iter()
+ // .position(|entry| entry == entry_before_old_top)?;
+ // Some(ListOffset {
+ // item_ix,
+ // offset_in_item: 0.,
+ // })
+ // });
+
+ // self.list_state
+ // .scroll_to(new_scroll_top.unwrap_or(old_scroll_top));
+ // }
+ // }
- // cx.notify();
- // }
+ cx.notify();
+ }
// fn render_call_participant(
// user: &User,
@@ -1,37 +1,34 @@
use client::{ContactRequestStatus, User, UserStore};
use gpui::{
- elements::*, AppContext, Entity, ModelHandle, MouseState, Task, View, ViewContext, ViewHandle,
+ div, img, svg, AnyElement, AppContext, DismissEvent, Div, Entity, EventEmitter, FocusHandle,
+ FocusableView, Img, IntoElement, Model, ParentElement as _, Render, Styled, Task, View,
+ ViewContext, VisualContext, WeakView,
};
-use picker::{Picker, PickerDelegate, PickerEvent};
+use picker::{Picker, PickerDelegate};
use std::sync::Arc;
-use util::TryFutureExt;
-use workspace::Modal;
+use theme::ActiveTheme as _;
+use ui::{h_stack, v_stack, Label};
+use util::{ResultExt as _, TryFutureExt};
pub fn init(cx: &mut AppContext) {
- Picker::<ContactFinderDelegate>::init(cx);
- cx.add_action(ContactFinder::dismiss)
+ //Picker::<ContactFinderDelegate>::init(cx);
+ //cx.add_action(ContactFinder::dismiss)
}
pub struct ContactFinder {
- picker: ViewHandle<Picker<ContactFinderDelegate>>,
+ picker: View<Picker<ContactFinderDelegate>>,
has_focus: bool,
}
impl ContactFinder {
- pub fn new(user_store: ModelHandle<UserStore>, cx: &mut ViewContext<Self>) -> Self {
- let picker = cx.add_view(|cx| {
- Picker::new(
- ContactFinderDelegate {
- user_store,
- potential_contacts: Arc::from([]),
- selected_index: 0,
- },
- cx,
- )
- .with_theme(|theme| theme.collab_panel.tabbed_modal.picker.clone())
- });
-
- cx.subscribe(&picker, |_, _, e, cx| cx.emit(*e)).detach();
+ pub fn new(user_store: Model<UserStore>, cx: &mut ViewContext<Self>) -> Self {
+ let delegate = ContactFinderDelegate {
+ parent: cx.view().downgrade(),
+ user_store,
+ potential_contacts: Arc::from([]),
+ selected_index: 0,
+ };
+ let picker = cx.build_view(|cx| Picker::new(delegate, cx));
Self {
picker,
@@ -41,105 +38,72 @@ impl ContactFinder {
pub fn set_query(&mut self, query: String, cx: &mut ViewContext<Self>) {
self.picker.update(cx, |picker, cx| {
- picker.set_query(query, cx);
+ // todo!()
+ // picker.set_query(query, cx);
});
}
-
- fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
- cx.emit(PickerEvent::Dismiss);
- }
-}
-
-impl Entity for ContactFinder {
- type Event = PickerEvent;
}
-impl View for ContactFinder {
- fn ui_name() -> &'static str {
- "ContactFinder"
- }
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
- let full_theme = &theme::current(cx);
- let theme = &full_theme.collab_panel.tabbed_modal;
-
- fn render_mode_button(
- text: &'static str,
- theme: &theme::TabbedModal,
- _cx: &mut ViewContext<ContactFinder>,
- ) -> AnyElement<ContactFinder> {
- let contained_text = &theme.tab_button.active_state().default;
- Label::new(text, contained_text.text.clone())
- .contained()
- .with_style(contained_text.container.clone())
- .into_any()
+impl Render for ContactFinder {
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+ fn render_mode_button(text: &'static str) -> AnyElement {
+ Label::new(text).into_any_element()
}
- Flex::column()
- .with_child(
- Flex::column()
- .with_child(
- Label::new("Contacts", theme.title.text.clone())
- .contained()
- .with_style(theme.title.container.clone()),
- )
- .with_child(Flex::row().with_children([render_mode_button(
- "Invite new contacts",
- &theme,
- cx,
- )]))
- .expanded()
- .contained()
- .with_style(theme.header),
- )
- .with_child(
- ChildView::new(&self.picker, cx)
- .contained()
- .with_style(theme.body),
+ v_stack()
+ .child(
+ v_stack()
+ .child(Label::new("Contacts"))
+ .child(h_stack().children([render_mode_button("Invite new contacts")]))
+ .bg(cx.theme().colors().element_background),
)
- .constrained()
- .with_max_height(theme.max_height)
- .with_max_width(theme.max_width)
- .contained()
- .with_style(theme.modal)
- .into_any()
+ .child(self.picker.clone())
+ .w_96()
}
- fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
- self.has_focus = true;
- if cx.is_self_focused() {
- cx.focus(&self.picker)
- }
- }
+ // fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
+ // self.has_focus = true;
+ // if cx.is_self_focused() {
+ // cx.focus(&self.picker)
+ // }
+ // }
- fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
- self.has_focus = false;
- }
+ // fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
+ // self.has_focus = false;
+ // }
+
+ type Element = Div;
}
-impl Modal for ContactFinder {
- fn has_focus(&self) -> bool {
- self.has_focus
- }
+// impl Modal for ContactFinder {
+// fn has_focus(&self) -> bool {
+// self.has_focus
+// }
- fn dismiss_on_event(event: &Self::Event) -> bool {
- match event {
- PickerEvent::Dismiss => true,
- }
- }
-}
+// fn dismiss_on_event(event: &Self::Event) -> bool {
+// match event {
+// PickerEvent::Dismiss => true,
+// }
+// }
+// }
pub struct ContactFinderDelegate {
+ parent: WeakView<ContactFinder>,
potential_contacts: Arc<[Arc<User>]>,
- user_store: ModelHandle<UserStore>,
+ user_store: Model<UserStore>,
selected_index: usize,
}
-impl PickerDelegate for ContactFinderDelegate {
- fn placeholder_text(&self) -> Arc<str> {
- "Search collaborator by username...".into()
+impl EventEmitter<DismissEvent> for ContactFinder {}
+
+impl FocusableView for ContactFinder {
+ fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
+ self.picker.focus_handle(cx)
}
+}
+impl PickerDelegate for ContactFinderDelegate {
+ type ListItem = Div;
fn match_count(&self) -> usize {
self.potential_contacts.len()
}
@@ -152,6 +116,10 @@ impl PickerDelegate for ContactFinderDelegate {
self.selected_index = ix;
}
+ fn placeholder_text(&self) -> Arc<str> {
+ "Search collaborator by username...".into()
+ }
+
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
let search_users = self
.user_store
@@ -161,7 +129,7 @@ impl PickerDelegate for ContactFinderDelegate {
async {
let potential_contacts = search_users.await?;
picker.update(&mut cx, |picker, cx| {
- picker.delegate_mut().potential_contacts = potential_contacts.into();
+ picker.delegate.potential_contacts = potential_contacts.into();
cx.notify();
})?;
anyhow::Ok(())
@@ -191,19 +159,18 @@ impl PickerDelegate for ContactFinderDelegate {
}
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
- cx.emit(PickerEvent::Dismiss);
+ //cx.emit(PickerEvent::Dismiss);
+ self.parent
+ .update(cx, |_, cx| cx.emit(DismissEvent::Dismiss))
+ .log_err();
}
fn render_match(
&self,
ix: usize,
- mouse_state: &mut MouseState,
selected: bool,
- cx: &gpui::AppContext,
- ) -> AnyElement<Picker<Self>> {
- let full_theme = &theme::current(cx);
- let theme = &full_theme.collab_panel.contact_finder;
- let tabbed_modal = &full_theme.collab_panel.tabbed_modal;
+ cx: &mut ViewContext<Picker<Self>>,
+ ) -> Option<Self::ListItem> {
let user = &self.potential_contacts[ix];
let request_status = self.user_store.read(cx).contact_request_status(user);
@@ -214,48 +181,47 @@ impl PickerDelegate for ContactFinderDelegate {
ContactRequestStatus::RequestSent => Some("icons/x.svg"),
ContactRequestStatus::RequestAccepted => None,
};
- let button_style = if self.user_store.read(cx).is_contact_request_pending(user) {
- &theme.disabled_contact_button
- } else {
- &theme.contact_button
- };
- let style = tabbed_modal
- .picker
- .item
- .in_state(selected)
- .style_for(mouse_state);
- Flex::row()
- .with_children(user.avatar.clone().map(|avatar| {
- Image::from_data(avatar)
- .with_style(theme.contact_avatar)
- .aligned()
- .left()
- }))
- .with_child(
- Label::new(user.github_login.clone(), style.label.clone())
- .contained()
- .with_style(theme.contact_username)
- .aligned()
- .left(),
- )
- .with_children(icon_path.map(|icon_path| {
- Svg::new(icon_path)
- .with_color(button_style.color)
- .constrained()
- .with_width(button_style.icon_width)
- .aligned()
- .contained()
- .with_style(button_style.container)
- .constrained()
- .with_width(button_style.button_width)
- .with_height(button_style.button_width)
- .aligned()
- .flex_float()
- }))
- .contained()
- .with_style(style.container)
- .constrained()
- .with_height(tabbed_modal.row_height)
- .into_any()
+ dbg!(icon_path);
+ Some(
+ div()
+ .flex_1()
+ .justify_between()
+ .children(user.avatar.clone().map(|avatar| img().data(avatar)))
+ .child(Label::new(user.github_login.clone()))
+ .children(icon_path.map(|icon_path| svg().path(icon_path))),
+ )
+ // Flex::row()
+ // .with_children(user.avatar.clone().map(|avatar| {
+ // Image::from_data(avatar)
+ // .with_style(theme.contact_avatar)
+ // .aligned()
+ // .left()
+ // }))
+ // .with_child(
+ // Label::new(user.github_login.clone(), style.label.clone())
+ // .contained()
+ // .with_style(theme.contact_username)
+ // .aligned()
+ // .left(),
+ // )
+ // .with_children(icon_path.map(|icon_path| {
+ // Svg::new(icon_path)
+ // .with_color(button_style.color)
+ // .constrained()
+ // .with_width(button_style.icon_width)
+ // .aligned()
+ // .contained()
+ // .with_style(button_style.container)
+ // .constrained()
+ // .with_width(button_style.button_width)
+ // .with_height(button_style.button_width)
+ // .aligned()
+ // .flex_float()
+ // }))
+ // .contained()
+ // .with_style(style.container)
+ // .constrained()
+ // .with_height(tabbed_modal.row_height)
+ // .into_any()
}
}
@@ -31,14 +31,17 @@ use std::sync::Arc;
use call::ActiveCall;
use client::{Client, UserStore};
use gpui::{
- div, px, rems, AppContext, Component, Div, InteractiveComponent, Model, ParentComponent,
- Render, Stateful, StatefulInteractiveComponent, Styled, Subscription, ViewContext,
- VisualContext, WeakView, WindowBounds,
+ div, px, rems, AppContext, Div, Element, InteractiveElement, IntoElement, Model, MouseButton,
+ ParentElement, Render, RenderOnce, Stateful, StatefulInteractiveElement, Styled, Subscription,
+ ViewContext, VisualContext, WeakView, WindowBounds,
};
use project::Project;
use theme::ActiveTheme;
-use ui::{h_stack, Button, ButtonVariant, KeyBinding, Label, TextColor, Tooltip};
-use workspace::Workspace;
+use ui::{h_stack, Avatar, Button, ButtonVariant, Color, IconButton, KeyBinding, Tooltip};
+use util::ResultExt;
+use workspace::{notifications::NotifyResultExt, Workspace};
+
+use crate::face_pile::FacePile;
// const MAX_PROJECT_NAME_LENGTH: usize = 40;
// const MAX_BRANCH_NAME_LENGTH: usize = 40;
@@ -82,9 +85,44 @@ pub struct CollabTitlebarItem {
}
impl Render for CollabTitlebarItem {
- type Element = Stateful<Self, Div<Self>>;
+ 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()
@@ -100,7 +138,7 @@ impl Render for CollabTitlebarItem {
|s| s.pl(px(68.)),
)
.bg(cx.theme().colors().title_bar_background)
- .on_click(|_, event, cx| {
+ .on_click(|event, cx| {
if event.up.click_count == 2 {
cx.zoom_window();
}
@@ -111,31 +149,37 @@ impl Render for CollabTitlebarItem {
// TODO - Add player menu
.child(
div()
+ .border()
+ .border_color(gpui::red())
.id("project_owner_indicator")
.child(
Button::new("player")
.variant(ButtonVariant::Ghost)
- .color(Some(TextColor::Player(0))),
+ .color(Some(Color::Player(0))),
)
- .tooltip(move |_, cx| Tooltip::text("Toggle following", cx)),
+ .tooltip(move |cx| Tooltip::text("Toggle following", cx)),
)
// TODO - Add project menu
.child(
div()
+ .border()
+ .border_color(gpui::red())
.id("titlebar_project_menu_button")
.child(Button::new("project_name").variant(ButtonVariant::Ghost))
- .tooltip(move |_, cx| Tooltip::text("Recent Projects", cx)),
+ .tooltip(move |cx| Tooltip::text("Recent Projects", cx)),
)
// TODO - Add git menu
.child(
div()
+ .border()
+ .border_color(gpui::red())
.id("titlebar_git_menu_button")
.child(
Button::new("branch_name")
.variant(ButtonVariant::Ghost)
- .color(Some(TextColor::Muted)),
+ .color(Some(Color::Muted)),
)
- .tooltip(move |_, cx| {
+ .tooltip(move |cx| {
cx.build_view(|_| {
Tooltip::new("Recent Branches")
.key_binding(KeyBinding::new(gpui::KeyBinding::new(
@@ -149,8 +193,113 @@ impl Render for CollabTitlebarItem {
.into()
}),
),
- ) // self.titlebar_item
- .child(h_stack().child(Label::new("Right side titlebar item")))
+ )
+ .when_some(
+ users.zip(current_user.clone()),
+ |this, (remote_participants, current_user)| {
+ let mut pile = FacePile::default();
+ pile.extend(
+ current_user
+ .avatar
+ .clone()
+ .map(|avatar| {
+ div().child(Avatar::data(avatar.clone())).into_any_element()
+ })
+ .into_iter()
+ .chain(remote_participants.into_iter().flat_map(|(user, peer_id)| {
+ user.avatar.as_ref().map(|avatar| {
+ div()
+ .child(
+ Avatar::data(avatar.clone()).into_element().into_any(),
+ )
+ .on_mouse_down(MouseButton::Left, {
+ let workspace = workspace.clone();
+ move |_, cx| {
+ workspace
+ .update(cx, |this, cx| {
+ this.open_shared_screen(peer_id, cx);
+ })
+ .log_err();
+ }
+ })
+ .into_any_element()
+ })
+ })),
+ );
+ this.child(pile.render(cx))
+ },
+ )
+ .child(div().flex_1())
+ .when(is_in_room, |this| {
+ this.child(
+ h_stack()
+ .child(
+ h_stack()
+ .child(Button::new(if is_shared { "Unshare" } else { "Share" }))
+ .child(IconButton::new("leave-call", ui::Icon::Exit).on_click({
+ let workspace = workspace.clone();
+ move |_, cx| {
+ workspace
+ .update(cx, |this, cx| {
+ this.call_state().hang_up(cx).detach();
+ })
+ .log_err();
+ }
+ })),
+ )
+ .child(
+ h_stack()
+ .child(IconButton::new("mute-microphone", mic_icon).on_click({
+ let workspace = workspace.clone();
+ move |_, cx| {
+ workspace
+ .update(cx, |this, cx| {
+ this.call_state().toggle_mute(cx);
+ })
+ .log_err();
+ }
+ }))
+ .child(IconButton::new("mute-sound", speakers_icon).on_click({
+ let workspace = workspace.clone();
+ move |_, cx| {
+ workspace
+ .update(cx, |this, cx| {
+ this.call_state().toggle_deafen(cx);
+ })
+ .log_err();
+ }
+ }))
+ .child(IconButton::new("screen-share", ui::Icon::Screen).on_click(
+ move |_, cx| {
+ workspace
+ .update(cx, |this, cx| {
+ this.call_state().toggle_screen_share(cx);
+ })
+ .log_err();
+ },
+ ))
+ .pl_2(),
+ ),
+ )
+ })
+ .map(|this| {
+ if let Some(user) = current_user {
+ this.when_some(user.avatar.clone(), |this, avatar| {
+ this.child(ui::Avatar::data(avatar))
+ })
+ } else {
+ this.child(Button::new("Sign in").on_click(move |_, cx| {
+ let client = client.clone();
+ cx.spawn(move |mut cx| async move {
+ client
+ .authenticate_and_connect(true, &cx)
+ .await
+ .notify_async_err(&mut cx);
+ })
+ .detach();
+ }))
+ }
+ })
}
}
@@ -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,17 +1,15 @@
use collections::{CommandPaletteFilter, HashMap};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
- actions, div, prelude::*, Action, AppContext, Component, Div, EventEmitter, FocusHandle,
- FocusableView, Keystroke, Manager, ParentComponent, Render, Styled, View, ViewContext,
- VisualContext, WeakView,
+ actions, Action, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView,
+ Keystroke, ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView,
};
use picker::{Picker, PickerDelegate};
use std::{
cmp::{self, Reverse},
sync::Arc,
};
-use theme::ActiveTheme;
-use ui::{h_stack, v_stack, HighlightedLabel, KeyBinding, StyledExt};
+use ui::{h_stack, v_stack, HighlightedLabel, KeyBinding, ListItem};
use util::{
channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL},
ResultExt,
@@ -69,7 +67,7 @@ impl CommandPalette {
}
}
-impl EventEmitter<Manager> for CommandPalette {}
+impl EventEmitter<DismissEvent> for CommandPalette {}
impl FocusableView for CommandPalette {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
@@ -78,7 +76,7 @@ impl FocusableView for CommandPalette {
}
impl Render for CommandPalette {
- type Element = Div<Self>;
+ type Element = Div;
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
v_stack().w_96().child(self.picker.clone())
@@ -141,7 +139,7 @@ impl CommandPaletteDelegate {
}
impl PickerDelegate for CommandPaletteDelegate {
- type ListItem = Div<Picker<Self>>;
+ type ListItem = ListItem;
fn placeholder_text(&self) -> Arc<str> {
"Execute a command...".into()
@@ -269,7 +267,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();
}
@@ -294,24 +292,16 @@ impl PickerDelegate for CommandPaletteDelegate {
ix: usize,
selected: bool,
cx: &mut ViewContext<Picker<Self>>,
- ) -> Self::ListItem {
- let colors = cx.theme().colors();
+ ) -> Option<Self::ListItem> {
let Some(r#match) = self.matches.get(ix) else {
- return div();
+ return None;
};
let Some(command) = self.commands.get(r#match.candidate_id) else {
- return div();
+ return None;
};
- div()
- .px_1()
- .text_color(colors.text)
- .text_ui()
- .bg(colors.ghost_element_background)
- .rounded_md()
- .when(selected, |this| this.bg(colors.ghost_element_selected))
- .hover(|this| this.bg(colors.ghost_element_hover))
- .child(
+ Some(
+ ListItem::new(ix).selected(selected).child(
h_stack()
.justify_between()
.child(HighlightedLabel::new(
@@ -319,7 +309,8 @@ impl PickerDelegate for CommandPaletteDelegate {
r#match.positions.clone(),
))
.children(KeyBinding::for_action(&*command.action, cx)),
- )
+ ),
+ )
}
}
@@ -13,10 +13,10 @@ use editor::{
};
use futures::future::try_join_all;
use gpui::{
- actions, div, AnyElement, AnyView, AppContext, Component, Context, Div, EventEmitter,
- FocusEvent, FocusHandle, Focusable, FocusableComponent, FocusableView, InteractiveComponent,
- Model, ParentComponent, Render, SharedString, Styled, Subscription, Task, View, ViewContext,
- VisualContext, WeakView,
+ actions, div, AnyElement, AnyView, AppContext, Context, Div, EventEmitter, FocusEvent,
+ FocusHandle, Focusable, FocusableElement, FocusableView, InteractiveElement, IntoElement,
+ Model, ParentElement, Render, SharedString, Styled, Subscription, Task, View, ViewContext,
+ VisualContext, WeakView, WindowContext,
};
use language::{
Anchor, Bias, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, Point, Selection,
@@ -36,7 +36,7 @@ use std::{
};
use theme::ActiveTheme;
pub use toolbar_controls::ToolbarControls;
-use ui::{h_stack, HighlightedLabel, Icon, IconElement, Label, TextColor};
+use ui::{h_stack, Color, HighlightedLabel, Icon, IconElement, Label};
use util::TryFutureExt;
use workspace::{
item::{BreadcrumbText, Item, ItemEvent, ItemHandle},
@@ -91,7 +91,7 @@ struct DiagnosticGroupState {
impl EventEmitter<ItemEvent> for ProjectDiagnosticsEditor {}
impl Render for ProjectDiagnosticsEditor {
- type Element = Focusable<Self, Div<Self>>;
+ type Element = Focusable<Div>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
let child = if self.path_states.is_empty() {
@@ -109,8 +109,8 @@ impl Render for ProjectDiagnosticsEditor {
div()
.track_focus(&self.focus_handle)
.size_full()
- .on_focus_in(Self::focus_in)
- .on_action(Self::toggle_warnings)
+ .on_focus_in(cx.listener(Self::focus_in))
+ .on_action(cx.listener(Self::toggle_warnings))
.child(child)
}
}
@@ -662,7 +662,7 @@ impl Item for ProjectDiagnosticsEditor {
Some("Project Diagnostics".into())
}
- fn tab_content<T: 'static>(&self, _detail: Option<usize>, _: &AppContext) -> AnyElement<T> {
+ fn tab_content(&self, _detail: Option<usize>, _: &WindowContext) -> AnyElement {
render_summary(&self.summary)
}
@@ -742,7 +742,7 @@ impl Item for ProjectDiagnosticsEditor {
}
fn breadcrumb_location(&self) -> ToolbarItemLocation {
- ToolbarItemLocation::PrimaryLeft { flex: None }
+ ToolbarItemLocation::PrimaryLeft
}
fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>> {
@@ -778,27 +778,28 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
.bg(gpui::red())
.map(|stack| {
let icon = if diagnostic.severity == DiagnosticSeverity::ERROR {
- IconElement::new(Icon::XCircle).color(TextColor::Error)
+ IconElement::new(Icon::XCircle).color(Color::Error)
} else {
- IconElement::new(Icon::ExclamationTriangle).color(TextColor::Warning)
+ IconElement::new(Icon::ExclamationTriangle).color(Color::Warning)
};
stack.child(div().pl_8().child(icon))
})
.when_some(diagnostic.source.as_ref(), |stack, source| {
- stack.child(Label::new(format!("{source}:")).color(TextColor::Accent))
+ stack.child(Label::new(format!("{source}:")).color(Color::Accent))
})
.child(HighlightedLabel::new(message.clone(), highlights.clone()))
.when_some(diagnostic.code.as_ref(), |stack, code| {
stack.child(Label::new(code.clone()))
})
- .render()
+ .into_any_element()
})
}
-pub(crate) fn render_summary<T: 'static>(summary: &DiagnosticSummary) -> AnyElement<T> {
+pub(crate) fn render_summary(summary: &DiagnosticSummary) -> AnyElement {
if summary.error_count == 0 && summary.warning_count == 0 {
- Label::new("No problems").render()
+ let label = Label::new("No problems");
+ label.into_any_element()
} else {
h_stack()
.bg(gpui::red())
@@ -806,7 +807,7 @@ pub(crate) fn render_summary<T: 'static>(summary: &DiagnosticSummary) -> AnyElem
.child(Label::new(summary.error_count.to_string()))
.child(IconElement::new(Icon::ExclamationTriangle))
.child(Label::new(summary.warning_count.to_string()))
- .render()
+ .into_any_element()
}
}
@@ -1549,7 +1550,7 @@ mod tests {
block_id: ix,
editor_style: &editor::EditorStyle::default(),
})
- .element_id()?
+ .inner_id()?
.try_into()
.ok()?,
@@ -1,13 +1,13 @@
use collections::HashSet;
use editor::{Editor, GoToDiagnostic};
use gpui::{
- rems, Div, EventEmitter, InteractiveComponent, ParentComponent, Render, Stateful,
- StatefulInteractiveComponent, Styled, Subscription, View, ViewContext, WeakView,
+ rems, Div, EventEmitter, InteractiveElement, ParentElement, Render, Stateful,
+ StatefulInteractiveElement, Styled, Subscription, View, ViewContext, WeakView,
};
use language::Diagnostic;
use lsp::LanguageServerId;
use theme::ActiveTheme;
-use ui::{h_stack, Icon, IconElement, Label, TextColor, Tooltip};
+use ui::{h_stack, Color, Icon, IconElement, Label, Tooltip};
use workspace::{item::ItemHandle, StatusItemView, ToolbarItemEvent, Workspace};
use crate::ProjectDiagnosticsEditor;
@@ -22,30 +22,30 @@ pub struct DiagnosticIndicator {
}
impl Render for DiagnosticIndicator {
- type Element = Stateful<Self, Div<Self>>;
+ type Element = Stateful<Div>;
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())
- .on_action(Self::go_to_next_diagnostic)
+ .id("diagnostic-indicator")
+ .on_action(cx.listener(Self::go_to_next_diagnostic))
.rounded_md()
.flex_none()
.h(rems(1.375))
@@ -54,14 +54,14 @@ impl Render for DiagnosticIndicator {
.bg(cx.theme().colors().ghost_element_background)
.hover(|style| style.bg(cx.theme().colors().ghost_element_hover))
.active(|style| style.bg(cx.theme().colors().ghost_element_active))
- .tooltip(|_, cx| Tooltip::text("Project Diagnostics", cx))
- .on_click(|this, _, cx| {
+ .tooltip(|cx| Tooltip::text("Project Diagnostics", cx))
+ .on_click(cx.listener(|this, _, cx| {
if let Some(workspace) = this.workspace.upgrade() {
workspace.update(cx, |workspace, cx| {
ProjectDiagnosticsEditor::deploy(workspace, &Default::default(), cx)
})
}
- })
+ }))
.child(diagnostic_indicator)
}
}
@@ -1,5 +1,5 @@
use crate::ProjectDiagnosticsEditor;
-use gpui::{div, Div, EventEmitter, ParentComponent, Render, ViewContext, WeakView};
+use gpui::{div, Div, EventEmitter, ParentElement, Render, ViewContext, WeakView};
use ui::{Icon, IconButton, Tooltip};
use workspace::{item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
@@ -8,7 +8,7 @@ pub struct ToolbarControls {
}
impl Render for ToolbarControls {
- type Element = Div<Self>;
+ type Element = Div;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
let include_warnings = self
@@ -26,14 +26,14 @@ impl Render for ToolbarControls {
div().child(
IconButton::new("toggle-warnings", Icon::ExclamationTriangle)
- .tooltip(move |_, cx| Tooltip::text(tooltip, cx))
- .on_click(|this: &mut Self, cx| {
+ .tooltip(move |cx| Tooltip::text(tooltip, cx))
+ .on_click(cx.listener(|this, _, cx| {
if let Some(editor) = this.editor.as_ref().and_then(|editor| editor.upgrade()) {
editor.update(cx, |editor, cx| {
editor.toggle_warnings(&Default::default(), cx);
});
}
- }),
+ })),
)
}
}
@@ -49,7 +49,7 @@ impl ToolbarItemView for ToolbarControls {
if let Some(pane_item) = active_pane_item.as_ref() {
if let Some(editor) = pane_item.downcast::<ProjectDiagnosticsEditor>() {
self.editor = Some(editor.downgrade());
- ToolbarItemLocation::PrimaryRight { flex: None }
+ ToolbarItemLocation::PrimaryRight
} else {
ToolbarItemLocation::Hidden
}
@@ -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,
@@ -50,7 +50,7 @@ struct BlockRow(u32);
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
struct WrapRow(u32);
-pub type RenderBlock = Arc<dyn Fn(&mut BlockContext) -> AnyElement<Editor>>;
+pub type RenderBlock = Arc<dyn Fn(&mut BlockContext) -> AnyElement>;
pub struct Block {
id: BlockId,
@@ -69,7 +69,7 @@ where
pub position: P,
pub height: u8,
pub style: BlockStyle,
- pub render: Arc<dyn Fn(&mut BlockContext) -> AnyElement<Editor>>,
+ pub render: Arc<dyn Fn(&mut BlockContext) -> AnyElement>,
pub disposition: BlockDisposition,
}
@@ -947,7 +947,7 @@ impl DerefMut for BlockContext<'_, '_> {
}
impl Block {
- pub fn render(&self, cx: &mut BlockContext) -> AnyElement<Editor> {
+ pub fn render(&self, cx: &mut BlockContext) -> AnyElement {
self.render.lock()(cx)
}
@@ -40,11 +40,12 @@ use fuzzy::{StringMatch, StringMatchCandidate};
use git::diff_hunk_to_display;
use gpui::{
actions, div, point, prelude::*, px, relative, rems, size, uniform_list, Action, AnyElement,
- AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Component, Context,
- EventEmitter, FocusHandle, FocusableView, FontFeatures, FontStyle, FontWeight, HighlightStyle,
- Hsla, InputHandler, KeyContext, Model, MouseButton, ParentComponent, Pixels, Render, Styled,
- Subscription, Task, TextStyle, UniformListScrollHandle, View, ViewContext, VisualContext,
- WeakView, WindowContext,
+ AppContext, AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Context,
+ DispatchPhase, Div, ElementId, EventEmitter, FocusHandle, FocusableView, FontFeatures,
+ FontStyle, FontWeight, HighlightStyle, Hsla, InputHandler, InteractiveText, KeyContext, Model,
+ MouseButton, ParentElement, Pixels, Render, RenderOnce, SharedString, Styled, StyledText,
+ Subscription, Task, TextRun, TextStyle, UniformListScrollHandle, View, ViewContext,
+ VisualContext, WeakView, WhiteSpace, WindowContext,
};
use highlight_matching_bracket::refresh_matching_bracket_highlights;
use hover_popover::{hide_hover, HoverState};
@@ -54,13 +55,14 @@ use itertools::Itertools;
pub use language::{char_kind, CharKind};
use language::{
language_settings::{self, all_language_settings, InlayHintSettings},
- point_from_lsp, AutoindentMode, BracketPair, Buffer, CodeAction, Completion, CursorShape,
- Diagnostic, IndentKind, IndentSize, Language, LanguageRegistry, LanguageServerName,
- OffsetRangeExt, Point, Selection, SelectionGoal, TransactionId,
+ markdown, point_from_lsp, AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel,
+ Completion, CursorShape, Diagnostic, Documentation, IndentKind, IndentSize, Language,
+ LanguageRegistry, LanguageServerName, OffsetRangeExt, Point, Selection, SelectionGoal,
+ TransactionId,
};
use lazy_static::lazy_static;
use link_go_to_definition::{GoToDefinitionLink, InlayHighlight, LinkGoToDefinitionState};
-use lsp::{DiagnosticSeverity, Documentation, LanguageServerId};
+use lsp::{DiagnosticSeverity, LanguageServerId};
use movement::TextLayoutDetails;
use multi_buffer::ToOffsetUtf16;
pub use multi_buffer::{
@@ -97,12 +99,12 @@ use text::{OffsetUtf16, Rope};
use theme::{
ActiveTheme, DiagnosticStyle, PlayerColor, SyntaxTheme, Theme, ThemeColors, ThemeSettings,
};
-use ui::{v_stack, HighlightedLabel, IconButton, StyledExt, Tooltip};
+use ui::{h_stack, v_stack, HighlightedLabel, IconButton, Popover, StyledExt, Tooltip};
use util::{post_inc, RangeExt, ResultExt, TryFutureExt};
use workspace::{
item::{ItemEvent, ItemHandle},
searchable::SearchEvent,
- ItemNavHistory, SplitDirection, ViewId, Workspace,
+ ItemNavHistory, Pane, SplitDirection, ViewId, Workspace,
};
const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
@@ -115,70 +117,70 @@ pub const DOCUMENT_HIGHLIGHTS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis
pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2);
-// pub fn render_parsed_markdown<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<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,156 @@ impl CompletionsMenu {
fn render(
&self,
style: &EditorStyle,
+ max_height: Pixels,
workspace: Option<WeakView<Workspace>>,
cx: &mut ViewContext<Editor>,
- ) -> AnyElement<Editor> {
- todo!("old implementation below")
- }
+ ) -> AnyElement {
+ let settings = EditorSettings::get_global(cx);
+ let show_completion_documentation = settings.show_completion_documentation;
- // enum CompletionTag {}
+ let widest_completion_ix = self
+ .matches
+ .iter()
+ .enumerate()
+ .max_by_key(|(_, mat)| {
+ let completions = self.completions.read();
+ let completion = &completions[mat.candidate_id];
+ let documentation = &completion.documentation;
+
+ let mut len = completion.label.text.chars().count();
+ if let Some(Documentation::SingleLine(text)) = documentation {
+ if show_completion_documentation {
+ len += text.chars().count();
+ }
+ }
- // let settings = EditorSettings>(cx);
- // let show_completion_documentation = settings.show_completion_documentation;
+ len
+ })
+ .map(|(ix, _)| ix);
- // let widest_completion_ix = self
- // .matches
- // .iter()
- // .enumerate()
- // .max_by_key(|(_, mat)| {
- // let completions = self.completions.read();
- // let completion = &completions[mat.candidate_id];
- // let documentation = &completion.documentation;
+ let completions = self.completions.clone();
+ let matches = self.matches.clone();
+ let selected_item = self.selected_item;
+ let 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)
+ .flex_1()
+ .px_1p5()
+ .py_1()
+ .min_w(px(260.))
+ .max_w(px(640.))
+ .w(px(500.))
+ .text_ui()
+ .overflow_y_scroll()
+ // Prevent a mouse down on documentation from being propagated to the editor,
+ // because that would move the cursor.
+ .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
+ })
+ };
+ let list = uniform_list(
+ cx.view().clone(),
+ "completions",
+ matches.len(),
+ move |editor, range, cx| {
+ let start_ix = range.start;
+ let completions_guard = completions.read();
- // 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(220.))
+ .max_w(px(540.))
+ .whitespace_nowrap()
+ .overflow_hidden()
+ .text_ui()
+ .px_1()
+ .rounded(px(4.))
+ .bg(cx.theme().colors().ghost_element_background)
+ .hover(|style| style.bg(cx.theme().colors().ghost_element_hover))
+ .when(item_ix == selected_item, |div| {
+ div.bg(cx.theme().colors().ghost_element_selected)
+ })
+ .on_mouse_down(
+ MouseButton::Left,
+ cx.listener(move |editor, event, cx| {
+ cx.stop_propagation();
+ editor
+ .confirm_completion(
+ &ConfirmCompletion {
+ item_ix: Some(item_ix),
+ },
+ cx,
+ )
+ .map(|task| task.detach_and_log_err(cx));
+ }),
+ )
+ .child(completion_label)
+ .children(documentation_label)
+ })
+ .collect()
+ },
+ )
+ .max_h(max_height)
+ .track_scroll(self.scroll_handle.clone())
+ .with_width_from_item(widest_completion_ix);
+
+ Popover::new()
+ .child(list)
+ .when_some(multiline_docs, |popover, multiline_docs| {
+ popover.aside(multiline_docs)
+ })
+ .into_any_element()
+ }
pub async fn filter(&mut self, query: Option<&str>, executor: BackgroundExecutor) {
let mut matches = if let Some(query) = query {
@@ -1540,14 +1492,17 @@ impl CodeActionsMenu {
&self,
mut cursor_position: DisplayPoint,
style: &EditorStyle,
+ max_height: Pixels,
cx: &mut ViewContext<Editor>,
- ) -> (DisplayPoint, AnyElement<Editor>) {
+ ) -> (DisplayPoint, AnyElement) {
let actions = self.actions.clone();
let selected_item = self.selected_item;
+
let element = uniform_list(
+ cx.view().clone(),
"code_actions_menu",
self.actions.len(),
- move |editor, range, cx| {
+ move |this, range, cx| {
actions[range.clone()]
.iter()
.enumerate()
@@ -1569,18 +1524,22 @@ impl CodeActionsMenu {
.bg(colors.element_hover)
.text_color(colors.text_accent)
})
- .on_mouse_down(MouseButton::Left, move |editor: &mut Editor, _, cx| {
- cx.stop_propagation();
- editor
- .confirm_code_action(
- &ConfirmCodeAction {
- item_ix: Some(item_ix),
- },
- cx,
- )
- .map(|task| task.detach_and_log_err(cx));
- })
- .child(action.lsp_action.title.clone())
+ .on_mouse_down(
+ MouseButton::Left,
+ cx.listener(move |editor, _, cx| {
+ cx.stop_propagation();
+ editor
+ .confirm_code_action(
+ &ConfirmCodeAction {
+ item_ix: Some(item_ix),
+ },
+ cx,
+ )
+ .map(|task| task.detach_and_log_err(cx));
+ }),
+ )
+ // TASK: It would be good to make lsp_action.title a SharedString to avoid allocating here.
+ .child(SharedString::from(action.lsp_action.title.clone()))
})
.collect()
},
@@ -1588,6 +1547,8 @@ impl CodeActionsMenu {
.elevation_1(cx)
.px_2()
.py_1()
+ .max_h(max_height)
+ .track_scroll(self.scroll_handle.clone())
.with_width_from_item(
self.actions
.iter()
@@ -1595,7 +1556,7 @@ impl CodeActionsMenu {
.max_by_key(|(_, action)| action.lsp_action.title.chars().count())
.map(|(ix, _)| ix),
)
- .render();
+ .into_any_element();
if self.deployed_from_indicator {
*cursor_position.column_mut() = 0;
@@ -1943,6 +1904,7 @@ impl Editor {
pixel_position_of_newest_cursor: None,
gutter_width: Default::default(),
style: None,
+ editor_actions: Default::default(),
_subscriptions: vec![
cx.observe(&buffer, Self::on_buffer_changed),
cx.subscribe(&buffer, Self::on_buffer_event),
@@ -2074,10 +2036,14 @@ impl Editor {
&self.buffer
}
- fn workspace(&self) -> Option<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)
}
@@ -2319,6 +2285,7 @@ impl Editor {
self.blink_manager.update(cx, BlinkManager::pause_blinking);
cx.emit(EditorEvent::SelectionsChanged { local });
+ cx.emit(SearchEvent::MatchesInvalidated);
if self.selections.disjoint_anchors().len() == 1 {
cx.emit(SearchEvent::ActiveMatchChanged)
@@ -3437,7 +3404,7 @@ impl Editor {
to_insert,
}) = self.inlay_hint_cache.spawn_hint_refresh(
reason_description,
- self.excerpt_visible_offsets(required_languages.as_ref(), cx),
+ self.excerpts_for_inlay_hints_query(required_languages.as_ref(), cx),
invalidate_cache,
cx,
) {
@@ -3456,11 +3423,15 @@ impl Editor {
.collect()
}
- pub fn excerpt_visible_offsets(
+ pub fn excerpts_for_inlay_hints_query(
&self,
restrict_to_languages: Option<&HashSet<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
@@ -3480,6 +3451,15 @@ impl Editor {
.filter(|(_, excerpt_visible_range, _)| !excerpt_visible_range.is_empty())
.filter_map(|(buffer_handle, excerpt_visible_range, excerpt_id)| {
let buffer = buffer_handle.read(cx);
+ let buffer_file = project::worktree::File::from_dyn(buffer.file())?;
+ let buffer_worktree = project.worktree_for_id(buffer_file.worktree_id(cx), cx)?;
+ let worktree_entry = buffer_worktree
+ .read(cx)
+ .entry_for_id(buffer_file.project_entry_id(cx)?)?;
+ if worktree_entry.is_ignored {
+ return None;
+ }
+
let language = buffer.language()?;
if let Some(restrict_to_languages) = restrict_to_languages {
if !restrict_to_languages.contains(language) {
@@ -3594,7 +3574,8 @@ impl Editor {
let id = post_inc(&mut self.next_completion_id);
let task = cx.spawn(|this, mut cx| {
async move {
- let menu = if let Some(completions) = completions.await.log_err() {
+ let completions = completions.await.log_err();
+ let (menu, pre_resolve_task) = if let Some(completions) = completions {
let mut menu = CompletionsMenu {
id,
initial_position: position,
@@ -3617,20 +3598,24 @@ impl Editor {
};
menu.filter(query.as_deref(), cx.background_executor().clone())
.await;
+
if menu.matches.is_empty() {
- None
+ (None, None)
} else {
- _ = this.update(&mut cx, |editor, cx| {
- menu.pre_resolve_completion_documentation(editor.project.clone(), cx);
- });
- Some(menu)
+ let pre_resolve_task = this
+ .update(&mut cx, |editor, cx| {
+ menu.pre_resolve_completion_documentation(editor, cx)
+ })
+ .ok()
+ .flatten();
+ (Some(menu), pre_resolve_task)
}
} else {
- None
+ (None, None)
};
this.update(&mut cx, |this, cx| {
- this.completion_tasks.retain(|(task_id, _)| *task_id > id);
+ this.completion_tasks.retain(|(task_id, _)| *task_id >= id);
let mut context_menu = this.context_menu.write();
match context_menu.as_ref() {
@@ -3662,142 +3647,147 @@ impl Editor {
}
})?;
+ if let Some(pre_resolve_task) = pre_resolve_task {
+ pre_resolve_task.await;
+ }
+
Ok::<_, anyhow::Error>(())
}
.log_err()
});
+
self.completion_tasks.push((id, task));
}
- // pub fn confirm_completion(
- // &mut self,
- // action: &ConfirmCompletion,
- // cx: &mut ViewContext<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();
@@ -3048,7 +3048,7 @@ fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
position: snapshot.anchor_after(Point::new(2, 0)),
disposition: BlockDisposition::Below,
height: 1,
- render: Arc::new(|_| div().render()),
+ render: Arc::new(|_| div().into_any()),
}],
Some(Autoscroll::fit()),
cx,
@@ -6740,75 +6740,6 @@ async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) {
// );
// }
-#[test]
-fn test_combine_syntax_and_fuzzy_match_highlights() {
- let string = "abcdefghijklmnop";
- let syntax_ranges = [
- (
- 0..3,
- HighlightStyle {
- color: Some(Hsla::red()),
- ..Default::default()
- },
- ),
- (
- 4..8,
- HighlightStyle {
- color: Some(Hsla::green()),
- ..Default::default()
- },
- ),
- ];
- let match_indices = [4, 6, 7, 8];
- assert_eq!(
- combine_syntax_and_fuzzy_match_highlights(
- string,
- Default::default(),
- syntax_ranges.into_iter(),
- &match_indices,
- ),
- &[
- (
- 0..3,
- HighlightStyle {
- color: Some(Hsla::red()),
- ..Default::default()
- },
- ),
- (
- 4..5,
- HighlightStyle {
- color: Some(Hsla::green()),
- font_weight: Some(gpui::FontWeight::BOLD),
- ..Default::default()
- },
- ),
- (
- 5..6,
- HighlightStyle {
- color: Some(Hsla::green()),
- ..Default::default()
- },
- ),
- (
- 6..8,
- HighlightStyle {
- color: Some(Hsla::green()),
- font_weight: Some(gpui::FontWeight::BOLD),
- ..Default::default()
- },
- ),
- (
- 8..9,
- HighlightStyle {
- font_weight: Some(gpui::FontWeight::BOLD),
- ..Default::default()
- },
- ),
- ]
- );
-}
-
#[gpui::test]
async fn go_to_prev_overlapping_diagnostic(
executor: BackgroundExecutor,
@@ -5,7 +5,9 @@ use crate::{
},
editor_settings::ShowScrollbar,
git::{diff_hunk_to_display, DisplayDiffHunk},
- hover_popover::hover_at,
+ hover_popover::{
+ self, hover_at, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT,
+ },
link_go_to_definition::{
go_to_fetched_definition, go_to_fetched_type_definition, update_go_to_definition_link,
update_inlay_link_and_hover_points, GoToDefinitionTrigger,
@@ -19,11 +21,11 @@ use anyhow::Result;
use collections::{BTreeMap, HashMap};
use gpui::{
div, point, px, relative, size, transparent_black, Action, AnyElement, AvailableSpace,
- BorrowWindow, Bounds, Component, ContentMask, Corners, DispatchPhase, Edges, Element,
- ElementId, ElementInputHandler, Entity, EntityId, Hsla, InteractiveComponent, LineLayout,
- MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentComponent, Pixels,
- ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveComponent, Style, Styled,
- TextRun, TextStyle, View, ViewContext, WindowContext, WrappedLine,
+ BorrowWindow, Bounds, ContentMask, Corners, DispatchPhase, Edges, Element, ElementId,
+ ElementInputHandler, Entity, EntityId, Hsla, InteractiveElement, IntoElement, LineLayout,
+ MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, RenderOnce,
+ ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled,
+ TextRun, TextStyle, View, ViewContext, WeakView, WindowContext, WrappedLine,
};
use itertools::Itertools;
use language::language_settings::ShowWhitespaceSetting;
@@ -112,18 +114,202 @@ impl SelectionLayout {
}
pub struct EditorElement {
- editor_id: EntityId,
+ editor: View<Editor>,
style: EditorStyle,
}
impl EditorElement {
pub fn new(editor: &View<Editor>, style: EditorStyle) -> Self {
Self {
- editor_id: editor.entity_id(),
+ editor: editor.clone(),
style,
}
}
+ fn register_actions(&self, cx: &mut WindowContext) {
+ let view = &self.editor;
+ self.editor.update(cx, |editor, cx| {
+ for action in editor.editor_actions.iter() {
+ (action)(cx)
+ }
+ });
+ register_action(view, cx, Editor::move_left);
+ register_action(view, cx, Editor::move_right);
+ register_action(view, cx, Editor::move_down);
+ register_action(view, cx, Editor::move_up);
+ // on_action(cx, Editor::new_file); todo!()
+ // on_action(cx, Editor::new_file_in_direction); todo!()
+ register_action(view, cx, Editor::cancel);
+ register_action(view, cx, Editor::newline);
+ register_action(view, cx, Editor::newline_above);
+ register_action(view, cx, Editor::newline_below);
+ register_action(view, cx, Editor::backspace);
+ register_action(view, cx, Editor::delete);
+ register_action(view, cx, Editor::tab);
+ register_action(view, cx, Editor::tab_prev);
+ register_action(view, cx, Editor::indent);
+ register_action(view, cx, Editor::outdent);
+ register_action(view, cx, Editor::delete_line);
+ register_action(view, cx, Editor::join_lines);
+ register_action(view, cx, Editor::sort_lines_case_sensitive);
+ register_action(view, cx, Editor::sort_lines_case_insensitive);
+ register_action(view, cx, Editor::reverse_lines);
+ register_action(view, cx, Editor::shuffle_lines);
+ register_action(view, cx, Editor::convert_to_upper_case);
+ register_action(view, cx, Editor::convert_to_lower_case);
+ register_action(view, cx, Editor::convert_to_title_case);
+ register_action(view, cx, Editor::convert_to_snake_case);
+ register_action(view, cx, Editor::convert_to_kebab_case);
+ register_action(view, cx, Editor::convert_to_upper_camel_case);
+ register_action(view, cx, Editor::convert_to_lower_camel_case);
+ register_action(view, cx, Editor::delete_to_previous_word_start);
+ register_action(view, cx, Editor::delete_to_previous_subword_start);
+ register_action(view, cx, Editor::delete_to_next_word_end);
+ register_action(view, cx, Editor::delete_to_next_subword_end);
+ register_action(view, cx, Editor::delete_to_beginning_of_line);
+ register_action(view, cx, Editor::delete_to_end_of_line);
+ register_action(view, cx, Editor::cut_to_end_of_line);
+ register_action(view, cx, Editor::duplicate_line);
+ register_action(view, cx, Editor::move_line_up);
+ register_action(view, cx, Editor::move_line_down);
+ register_action(view, cx, Editor::transpose);
+ register_action(view, cx, Editor::cut);
+ register_action(view, cx, Editor::copy);
+ register_action(view, cx, Editor::paste);
+ register_action(view, cx, Editor::undo);
+ register_action(view, cx, Editor::redo);
+ register_action(view, cx, Editor::move_page_up);
+ register_action(view, cx, Editor::move_page_down);
+ register_action(view, cx, Editor::next_screen);
+ register_action(view, cx, Editor::scroll_cursor_top);
+ register_action(view, cx, Editor::scroll_cursor_center);
+ register_action(view, cx, Editor::scroll_cursor_bottom);
+ register_action(view, cx, |editor, _: &LineDown, cx| {
+ editor.scroll_screen(&ScrollAmount::Line(1.), cx)
+ });
+ register_action(view, cx, |editor, _: &LineUp, cx| {
+ editor.scroll_screen(&ScrollAmount::Line(-1.), cx)
+ });
+ register_action(view, cx, |editor, _: &HalfPageDown, cx| {
+ editor.scroll_screen(&ScrollAmount::Page(0.5), cx)
+ });
+ register_action(view, cx, |editor, _: &HalfPageUp, cx| {
+ editor.scroll_screen(&ScrollAmount::Page(-0.5), cx)
+ });
+ register_action(view, cx, |editor, _: &PageDown, cx| {
+ editor.scroll_screen(&ScrollAmount::Page(1.), cx)
+ });
+ register_action(view, cx, |editor, _: &PageUp, cx| {
+ editor.scroll_screen(&ScrollAmount::Page(-1.), cx)
+ });
+ register_action(view, cx, Editor::move_to_previous_word_start);
+ register_action(view, cx, Editor::move_to_previous_subword_start);
+ register_action(view, cx, Editor::move_to_next_word_end);
+ register_action(view, cx, Editor::move_to_next_subword_end);
+ register_action(view, cx, Editor::move_to_beginning_of_line);
+ register_action(view, cx, Editor::move_to_end_of_line);
+ register_action(view, cx, Editor::move_to_start_of_paragraph);
+ register_action(view, cx, Editor::move_to_end_of_paragraph);
+ register_action(view, cx, Editor::move_to_beginning);
+ register_action(view, cx, Editor::move_to_end);
+ register_action(view, cx, Editor::select_up);
+ register_action(view, cx, Editor::select_down);
+ register_action(view, cx, Editor::select_left);
+ register_action(view, cx, Editor::select_right);
+ register_action(view, cx, Editor::select_to_previous_word_start);
+ register_action(view, cx, Editor::select_to_previous_subword_start);
+ register_action(view, cx, Editor::select_to_next_word_end);
+ register_action(view, cx, Editor::select_to_next_subword_end);
+ register_action(view, cx, Editor::select_to_beginning_of_line);
+ register_action(view, cx, Editor::select_to_end_of_line);
+ register_action(view, cx, Editor::select_to_start_of_paragraph);
+ register_action(view, cx, Editor::select_to_end_of_paragraph);
+ register_action(view, cx, Editor::select_to_beginning);
+ register_action(view, cx, Editor::select_to_end);
+ register_action(view, cx, Editor::select_all);
+ register_action(view, cx, |editor, action, cx| {
+ editor.select_all_matches(action, cx).log_err();
+ });
+ register_action(view, cx, Editor::select_line);
+ register_action(view, cx, Editor::split_selection_into_lines);
+ register_action(view, cx, Editor::add_selection_above);
+ register_action(view, cx, Editor::add_selection_below);
+ register_action(view, cx, |editor, action, cx| {
+ editor.select_next(action, cx).log_err();
+ });
+ register_action(view, cx, |editor, action, cx| {
+ editor.select_previous(action, cx).log_err();
+ });
+ register_action(view, cx, Editor::toggle_comments);
+ register_action(view, cx, Editor::select_larger_syntax_node);
+ register_action(view, cx, Editor::select_smaller_syntax_node);
+ register_action(view, cx, Editor::move_to_enclosing_bracket);
+ register_action(view, cx, Editor::undo_selection);
+ register_action(view, cx, Editor::redo_selection);
+ register_action(view, cx, Editor::go_to_diagnostic);
+ register_action(view, cx, Editor::go_to_prev_diagnostic);
+ register_action(view, cx, Editor::go_to_hunk);
+ register_action(view, cx, Editor::go_to_prev_hunk);
+ register_action(view, cx, Editor::go_to_definition);
+ register_action(view, cx, Editor::go_to_definition_split);
+ register_action(view, cx, Editor::go_to_type_definition);
+ register_action(view, cx, Editor::go_to_type_definition_split);
+ register_action(view, cx, Editor::fold);
+ register_action(view, cx, Editor::fold_at);
+ register_action(view, cx, Editor::unfold_lines);
+ register_action(view, cx, Editor::unfold_at);
+ register_action(view, cx, Editor::fold_selected_ranges);
+ register_action(view, cx, Editor::show_completions);
+ register_action(view, cx, Editor::toggle_code_actions);
+ // on_action(cx, Editor::open_excerpts); todo!()
+ register_action(view, cx, Editor::toggle_soft_wrap);
+ register_action(view, cx, Editor::toggle_inlay_hints);
+ register_action(view, cx, hover_popover::hover);
+ register_action(view, cx, Editor::reveal_in_finder);
+ register_action(view, cx, Editor::copy_path);
+ register_action(view, cx, Editor::copy_relative_path);
+ register_action(view, cx, Editor::copy_highlight_json);
+ register_action(view, cx, |editor, action, cx| {
+ editor
+ .format(action, cx)
+ .map(|task| task.detach_and_log_err(cx));
+ });
+ register_action(view, cx, Editor::restart_language_server);
+ register_action(view, cx, Editor::show_character_palette);
+ register_action(view, cx, |editor, action, cx| {
+ editor
+ .confirm_completion(action, cx)
+ .map(|task| task.detach_and_log_err(cx));
+ });
+ register_action(view, cx, |editor, action, cx| {
+ editor
+ .confirm_code_action(action, cx)
+ .map(|task| task.detach_and_log_err(cx));
+ });
+ register_action(view, cx, |editor, action, cx| {
+ editor
+ .rename(action, cx)
+ .map(|task| task.detach_and_log_err(cx));
+ });
+ register_action(view, cx, |editor, action, cx| {
+ editor
+ .confirm_rename(action, cx)
+ .map(|task| task.detach_and_log_err(cx));
+ });
+ register_action(view, cx, |editor, action, cx| {
+ editor
+ .find_all_references(action, cx)
+ .map(|task| task.detach_and_log_err(cx));
+ });
+ register_action(view, cx, Editor::next_copilot_suggestion);
+ register_action(view, cx, Editor::previous_copilot_suggestion);
+ register_action(view, cx, Editor::copilot_suggest);
+ register_action(view, cx, Editor::context_menu_first);
+ register_action(view, cx, Editor::context_menu_prev);
+ register_action(view, cx, Editor::context_menu_next);
+ register_action(view, cx, Editor::context_menu_last);
+ }
+
fn mouse_down(
editor: &mut Editor,
event: &MouseDownEvent,
@@ -349,7 +535,7 @@ impl EditorElement {
gutter_bounds: Bounds<Pixels>,
text_bounds: Bounds<Pixels>,
layout: &LayoutState,
- cx: &mut ViewContext<Editor>,
+ cx: &mut WindowContext,
) {
let bounds = gutter_bounds.union(&text_bounds);
let scroll_top =
@@ -459,8 +645,7 @@ impl EditorElement {
&mut self,
bounds: Bounds<Pixels>,
layout: &mut LayoutState,
- editor: &mut Editor,
- cx: &mut ViewContext<Editor>,
+ cx: &mut WindowContext,
) {
let line_height = layout.position_map.line_height;
@@ -488,13 +673,14 @@ impl EditorElement {
}
}
- for (ix, fold_indicator) in layout.fold_indicators.iter_mut().enumerate() {
- if let Some(fold_indicator) = fold_indicator.as_mut() {
+ for (ix, fold_indicator) in layout.fold_indicators.drain(..).enumerate() {
+ if let Some(mut fold_indicator) = fold_indicator {
+ let mut fold_indicator = fold_indicator.into_any_element();
let available_space = size(
AvailableSpace::MinContent,
AvailableSpace::Definite(line_height * 0.55),
);
- let fold_indicator_size = fold_indicator.measure(available_space, editor, cx);
+ let fold_indicator_size = fold_indicator.measure(available_space, cx);
let position = point(
bounds.size.width - layout.gutter_padding,
@@ -505,32 +691,29 @@ impl EditorElement {
(line_height - fold_indicator_size.height) / 2.,
);
let origin = bounds.origin + position + centering_offset;
- fold_indicator.draw(origin, available_space, editor, cx);
+ fold_indicator.draw(origin, available_space, cx);
}
}
- if let Some(indicator) = layout.code_actions_indicator.as_mut() {
+ if let Some(indicator) = layout.code_actions_indicator.take() {
+ let mut button = indicator.button.into_any_element();
let available_space = size(
AvailableSpace::MinContent,
AvailableSpace::Definite(line_height),
);
- let indicator_size = indicator.element.measure(available_space, editor, cx);
+ let indicator_size = button.measure(available_space, cx);
+
let mut x = Pixels::ZERO;
let mut y = indicator.row as f32 * line_height - scroll_top;
// Center indicator.
x += ((layout.gutter_padding + layout.gutter_margin) - indicator_size.width) / 2.;
y += (line_height - indicator_size.height) / 2.;
- indicator
- .element
- .draw(bounds.origin + point(x, y), available_space, editor, cx);
+
+ button.draw(bounds.origin + point(x, y), available_space, cx);
}
}
- fn paint_diff_hunks(
- bounds: Bounds<Pixels>,
- layout: &LayoutState,
- cx: &mut ViewContext<Editor>,
- ) {
+ fn paint_diff_hunks(bounds: Bounds<Pixels>, layout: &LayoutState, cx: &mut WindowContext) {
// todo!()
// let diff_style = &theme::current(cx).editor.diff.clone();
// let line_height = layout.position_map.line_height;
@@ -618,14 +801,19 @@ impl EditorElement {
&mut self,
text_bounds: Bounds<Pixels>,
layout: &mut LayoutState,
- editor: &mut Editor,
- cx: &mut ViewContext<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 {
@@ -674,20 +862,22 @@ impl EditorElement {
div()
.id(fold.id)
.size_full()
- .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
- .on_click(move |editor: &mut Editor, _, cx| {
- editor.unfold_ranges(
- [fold_range.start..fold_range.end],
- true,
- false,
- cx,
- );
- cx.stop_propagation();
- })
+ .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
+ .on_click(cx.listener_for(
+ &self.editor,
+ move |editor: &mut Editor, _, cx| {
+ editor.unfold_ranges(
+ [fold_range.start..fold_range.end],
+ true,
+ false,
+ cx,
+ );
+ cx.stop_propagation();
+ },
+ ))
.draw(
fold_bounds.origin,
fold_bounds.size,
- editor,
cx,
|fold_element_state, cx| {
if fold_element_state.is_active() {
@@ -748,7 +938,7 @@ impl EditorElement {
invisible_display_ranges.push(selection.range.clone());
}
- if !selection.is_local || editor.show_local_cursors(cx) {
+ if !selection.is_local || self.editor.read(cx).show_local_cursors(cx) {
let cursor_position = selection.head;
if layout
.visible_display_row_range
@@ -800,12 +990,14 @@ impl EditorElement {
* layout.position_map.line_height
- layout.position_map.scroll_position.y;
if selection.is_newest {
- editor.pixel_position_of_newest_cursor = Some(point(
- text_bounds.origin.x + x + block_width / 2.,
- text_bounds.origin.y
- + y
- + layout.position_map.line_height / 2.,
- ));
+ self.editor.update(cx, |editor, _| {
+ editor.pixel_position_of_newest_cursor = Some(point(
+ text_bounds.origin.x + x + block_width / 2.,
+ text_bounds.origin.y
+ + y
+ + layout.position_map.line_height / 2.,
+ ))
+ });
}
cursors.push(Cursor {
color: selection_style.cursor,
@@ -840,17 +1032,11 @@ impl EditorElement {
}
});
- if let Some((position, context_menu)) = layout.context_menu.as_mut() {
- cx.with_z_index(1, |cx| {
- let line_height = self.style.text.line_height_in_pixels(cx.rem_size());
- let available_space = size(
- AvailableSpace::MinContent,
- AvailableSpace::Definite(
- (12. * line_height)
- .min((text_bounds.size.height - line_height) / 2.),
- ),
- );
- let context_menu_size = context_menu.measure(available_space, editor, cx);
+ cx.with_z_index(1, |cx| {
+ if let Some((position, mut context_menu)) = layout.context_menu.take() {
+ let available_space =
+ size(AvailableSpace::MinContent, AvailableSpace::MinContent);
+ let context_menu_size = context_menu.measure(available_space, cx);
let cursor_row_layout = &layout.position_map.line_layouts
[(position.row() - start_row) as usize]
@@ -871,84 +1057,77 @@ impl EditorElement {
}
if list_origin.y + list_height > text_bounds.lower_right().y {
- list_origin.y -= layout.position_map.line_height - list_height;
+ list_origin.y -= layout.position_map.line_height + list_height;
}
- context_menu.draw(list_origin, available_space, editor, cx);
- })
- }
+ cx.break_content_mask(|cx| {
+ context_menu.draw(list_origin, available_space, cx)
+ });
+ }
- // if let Some((position, hover_popovers)) = layout.hover_popovers.as_mut() {
- // cx.scene().push_stacking_context(None, None);
-
- // // 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;
+ }
+ }
+ }
+ })
},
)
}
@@ -1165,7 +1344,7 @@ impl EditorElement {
layout: &LayoutState,
content_origin: gpui::Point<Pixels>,
bounds: Bounds<Pixels>,
- cx: &mut ViewContext<Editor>,
+ cx: &mut WindowContext,
) {
let start_row = layout.visible_display_row_range.start;
let end_row = layout.visible_display_row_range.end;
@@ -1217,14 +1396,13 @@ impl EditorElement {
&mut self,
bounds: Bounds<Pixels>,
layout: &mut LayoutState,
- editor: &mut Editor,
- cx: &mut ViewContext<Editor>,
+ cx: &mut WindowContext,
) {
let scroll_position = layout.position_map.snapshot.scroll_position();
let scroll_left = scroll_position.x * layout.position_map.em_width;
let scroll_top = scroll_position.y * layout.position_map.line_height;
- for block in &mut layout.blocks {
+ for block in layout.blocks.drain(..) {
let mut origin = bounds.origin
+ point(
Pixels::ZERO,
@@ -1233,13 +1411,11 @@ impl EditorElement {
if !matches!(block.style, BlockStyle::Sticky) {
origin += point(-scroll_left, Pixels::ZERO);
}
- block
- .element
- .draw(origin, block.available_space, editor, cx);
+ block.element.draw(origin, block.available_space, cx);
}
}
- fn column_pixels(&self, column: usize, cx: &ViewContext<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
@@ -1260,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)
}
@@ -1415,7 +1591,7 @@ impl EditorElement {
}
fn layout_lines(
- &mut self,
+ &self,
rows: Range<u32>,
line_number_layouts: &[Option<ShapedLine>],
snapshot: &EditorSnapshot,
@@ -1471,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 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 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(),
- 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<Editor>>)> {
- todo!("old version below")
+ ) -> Option<(DisplayPoint, Vec<AnyElement>)> {
+ // 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<Editor> {
- 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(),
@@ -9,8 +9,8 @@ use collections::HashSet;
use futures::future::try_join_all;
use gpui::{
div, point, AnyElement, AppContext, AsyncAppContext, Entity, EntityId, EventEmitter,
- FocusHandle, Model, ParentComponent, Pixels, SharedString, Styled, Subscription, Task, View,
- ViewContext, VisualContext, WeakView,
+ FocusHandle, Model, ParentElement, Pixels, SharedString, Styled, Subscription, Task, View,
+ ViewContext, VisualContext, WeakView, WindowContext,
};
use language::{
proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, CharKind, OffsetRangeExt,
@@ -30,7 +30,7 @@ use std::{
};
use text::Selection;
use theme::{ActiveTheme, Theme};
-use ui::{Label, TextColor};
+use ui::{Color, Label};
use util::{paths::PathExt, ResultExt, TryFutureExt};
use workspace::item::{BreadcrumbText, FollowEvent, FollowableEvents, FollowableItemHandle};
use workspace::{
@@ -584,7 +584,7 @@ impl Item for Editor {
Some(path.to_string_lossy().to_string().into())
}
- fn tab_content<T: 'static>(&self, detail: Option<usize>, cx: &AppContext) -> AnyElement<T> {
+ fn tab_content(&self, detail: Option<usize>, cx: &WindowContext) -> AnyElement {
let theme = cx.theme();
AnyElement::new(
@@ -604,7 +604,7 @@ impl Item for Editor {
&description,
MAX_TAB_TITLE_LEN,
))
- .color(TextColor::Muted),
+ .color(Color::Muted),
),
)
})),
@@ -761,7 +761,7 @@ impl Item for Editor {
}
fn breadcrumb_location(&self) -> ToolbarItemLocation {
- ToolbarItemLocation::PrimaryLeft { flex: None }
+ ToolbarItemLocation::PrimaryLeft
}
fn breadcrumbs(&self, variant: &Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>> {
@@ -907,17 +907,15 @@ impl SearchableItem for Editor {
type Match = Range<Anchor>;
fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
- todo!()
- // self.clear_background_highlights::<BufferSearchHighlights>(cx);
+ self.clear_background_highlights::<BufferSearchHighlights>(cx);
}
fn update_matches(&mut self, matches: Vec<Range<Anchor>>, cx: &mut ViewContext<Self>) {
- todo!()
- // self.highlight_background::<BufferSearchHighlights>(
- // matches,
- // |theme| theme.search.match_background,
- // cx,
- // );
+ self.highlight_background::<BufferSearchHighlights>(
+ matches,
+ |theme| theme.title_bar_background, // todo: update theme
+ cx,
+ );
}
fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String {
@@ -952,22 +950,20 @@ impl SearchableItem for Editor {
matches: Vec<Range<Anchor>>,
cx: &mut ViewContext<Self>,
) {
- todo!()
- // self.unfold_ranges([matches[index].clone()], false, true, cx);
- // let range = self.range_for_match(&matches[index]);
- // self.change_selections(Some(Autoscroll::fit()), cx, |s| {
- // s.select_ranges([range]);
- // })
+ self.unfold_ranges([matches[index].clone()], false, true, cx);
+ let range = self.range_for_match(&matches[index]);
+ self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.select_ranges([range]);
+ })
}
fn select_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>) {
- todo!()
- // self.unfold_ranges(matches.clone(), false, false, cx);
- // let mut ranges = Vec::new();
- // for m in &matches {
- // ranges.push(self.range_for_match(&m))
- // }
- // self.change_selections(None, cx, |s| s.select_ranges(ranges));
+ self.unfold_ranges(matches.clone(), false, false, cx);
+ let mut ranges = Vec::new();
+ for m in &matches {
+ ranges.push(self.range_for_match(&m))
+ }
+ self.change_selections(None, cx, |s| s.select_ranges(ranges));
}
fn replace(
&mut self,
@@ -2,9 +2,8 @@ use collections::HashMap;
use editor::{scroll::autoscroll::Autoscroll, Bias, Editor};
use fuzzy::{CharBag, PathMatch, PathMatchCandidate};
use gpui::{
- actions, div, AppContext, Component, Div, EventEmitter, FocusHandle, FocusableView,
- InteractiveComponent, Manager, Model, ParentComponent, Render, Styled, Task, View, ViewContext,
- VisualContext, WeakView,
+ actions, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView, Model,
+ ParentElement, Render, Styled, Task, View, ViewContext, VisualContext, WeakView,
};
use picker::{Picker, PickerDelegate};
use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
@@ -16,8 +15,7 @@ use std::{
},
};
use text::Point;
-use theme::ActiveTheme;
-use ui::{v_stack, HighlightedLabel, StyledExt};
+use ui::{v_stack, HighlightedLabel, ListItem};
use util::{paths::PathLikeWithPosition, post_inc, ResultExt};
use workspace::Workspace;
@@ -111,14 +109,14 @@ impl FileFinder {
}
}
-impl EventEmitter<Manager> for FileFinder {}
+impl EventEmitter<DismissEvent> for FileFinder {}
impl FocusableView for FileFinder {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
self.picker.focus_handle(cx)
}
}
impl Render for FileFinder {
- type Element = Div<Self>;
+ type Element = Div;
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
v_stack().w_96().child(self.picker.clone())
@@ -530,7 +528,7 @@ impl FileFinderDelegate {
}
impl PickerDelegate for FileFinderDelegate {
- type ListItem = Div<Picker<Self>>;
+ type ListItem = ListItem;
fn placeholder_text(&self) -> Arc<str> {
"Search project files...".into()
@@ -690,7 +688,7 @@ impl PickerDelegate for FileFinderDelegate {
}
}
finder
- .update(&mut cx, |_, cx| cx.emit(Manager::Dismiss))
+ .update(&mut cx, |_, cx| cx.emit(DismissEvent::Dismiss))
.ok()?;
Some(())
@@ -702,7 +700,7 @@ impl PickerDelegate for FileFinderDelegate {
fn dismissed(&mut self, cx: &mut ViewContext<Picker<FileFinderDelegate>>) {
self.file_finder
- .update(cx, |_, cx| cx.emit(Manager::Dismiss))
+ .update(cx, |_, cx| cx.emit(DismissEvent::Dismiss))
.log_err();
}
@@ -711,30 +709,22 @@ impl PickerDelegate for FileFinderDelegate {
ix: usize,
selected: bool,
cx: &mut ViewContext<Picker<Self>>,
- ) -> Self::ListItem {
+ ) -> Option<Self::ListItem> {
let path_match = self
.matches
.get(ix)
.expect("Invalid matches state: no element for index {ix}");
- let theme = cx.theme();
- let colors = theme.colors();
let (file_name, file_name_positions, full_path, full_path_positions) =
self.labels_for_match(path_match, cx, ix);
- div()
- .px_1()
- .text_color(colors.text)
- .text_ui()
- .bg(colors.ghost_element_background)
- .rounded_md()
- .when(selected, |this| this.bg(colors.ghost_element_selected))
- .hover(|this| this.bg(colors.ghost_element_hover))
- .child(
+ Some(
+ ListItem::new(ix).selected(selected).child(
v_stack()
.child(HighlightedLabel::new(file_name, file_name_positions))
.child(HighlightedLabel::new(full_path, full_path_positions)),
- )
+ ),
+ )
}
}
@@ -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,14 +1,13 @@
use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Editor};
use gpui::{
- actions, div, prelude::*, AppContext, Div, EventEmitter, FocusHandle, FocusableView, Manager,
- ParentComponent, Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext,
+ actions, div, prelude::*, AppContext, DismissEvent, Div, EventEmitter, FocusHandle,
+ FocusableView, Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext,
WindowContext,
};
use text::{Bias, Point};
use theme::ActiveTheme;
-use ui::{h_stack, v_stack, Label, StyledExt, TextColor};
+use ui::{h_stack, v_stack, Color, Label, StyledExt};
use util::paths::FILE_ROW_COLUMN_DELIMITER;
-use workspace::Workspace;
actions!(Toggle);
@@ -26,22 +25,24 @@ pub struct GoToLine {
impl FocusableView for GoToLine {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
- self.active_editor.focus_handle(cx)
+ self.line_editor.focus_handle(cx)
}
}
-impl EventEmitter<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));
+ })
});
}
@@ -89,7 +90,7 @@ impl GoToLine {
) {
match event {
// todo!() this isn't working...
- editor::EditorEvent::Blurred => cx.emit(Manager::Dismiss),
+ editor::EditorEvent::Blurred => cx.emit(DismissEvent::Dismiss),
editor::EditorEvent::BufferEdited { .. } => self.highlight_current_line(cx),
_ => {}
}
@@ -124,7 +125,7 @@ impl GoToLine {
}
fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
- cx.emit(Manager::Dismiss);
+ cx.emit(DismissEvent::Dismiss);
}
fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
@@ -141,19 +142,19 @@ impl GoToLine {
self.prev_scroll_position.take();
}
- cx.emit(Manager::Dismiss);
+ cx.emit(DismissEvent::Dismiss);
}
}
impl Render for GoToLine {
- type Element = Div<Self>;
+ type Element = Div;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
div()
.elevation_2(cx)
.key_context("GoToLine")
- .on_action(Self::cancel)
- .on_action(Self::confirm)
+ .on_action(cx.listener(Self::cancel))
+ .on_action(cx.listener(Self::confirm))
.w_96()
.child(
v_stack()
@@ -177,7 +178,7 @@ impl Render for GoToLine {
.justify_between()
.px_2()
.py_1()
- .child(Label::new(self.current_text.clone()).color(TextColor::Muted)),
+ .child(Label::new(self.current_text.clone()).color(Color::Muted)),
),
)
}
@@ -10,11 +10,12 @@ pub use entity_map::*;
pub use model_context::*;
use refineable::Refineable;
use smallvec::SmallVec;
+use smol::future::FutureExt;
#[cfg(any(test, feature = "test-support"))]
pub use test_context::*;
use crate::{
- current_platform, image_cache::ImageCache, Action, ActionRegistry, AnyBox, AnyView,
+ current_platform, image_cache::ImageCache, Action, ActionRegistry, Any, AnyView,
AnyWindowHandle, AppMetadata, AssetSource, BackgroundExecutor, ClipboardItem, Context,
DispatchPhase, DisplayId, Entity, EventEmitter, FocusEvent, FocusHandle, FocusId,
ForegroundExecutor, KeyBinding, Keymap, LayoutId, PathPromptOptions, Pixels, Platform,
@@ -28,7 +29,7 @@ use futures::{channel::oneshot, future::LocalBoxFuture, Future};
use parking_lot::Mutex;
use slotmap::SlotMap;
use std::{
- any::{type_name, Any, TypeId},
+ any::{type_name, TypeId},
cell::{Ref, RefCell, RefMut},
marker::PhantomData,
mem,
@@ -194,7 +195,7 @@ pub struct AppContext {
asset_source: Arc<dyn AssetSource>,
pub(crate) image_cache: ImageCache,
pub(crate) text_style_stack: Vec<TextStyleRefinement>,
- pub(crate) globals_by_type: HashMap<TypeId, AnyBox>,
+ pub(crate) globals_by_type: HashMap<TypeId, Box<dyn Any>>,
pub(crate) entities: EntityMap,
pub(crate) new_view_observers: SubscriberSet<TypeId, NewViewListener>,
pub(crate) windows: SlotMap<WindowId, Option<Window>>,
@@ -424,7 +425,7 @@ impl AppContext {
/// Opens a new window with the given option and the root view returned by the given function.
/// The function is invoked with a `WindowContext`, which can be used to interact with window-specific
/// functionality.
- pub fn open_window<V: Render>(
+ pub fn open_window<V: 'static + Render>(
&mut self,
options: crate::WindowOptions,
build_root_view: impl FnOnce(&mut WindowContext) -> View<V>,
@@ -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"))?
@@ -1104,12 +1123,12 @@ pub(crate) enum Effect {
/// Wraps a global variable value during `update_global` while the value has been moved to the stack.
pub(crate) struct GlobalLease<G: 'static> {
- global: AnyBox,
+ global: Box<dyn Any>,
global_type: PhantomData<G>,
}
impl<G: 'static> GlobalLease<G> {
- fn new(global: AnyBox) -> Self {
+ fn new(global: Box<dyn Any>) -> Self {
GlobalLease {
global,
global_type: PhantomData,
@@ -1,7 +1,7 @@
use crate::{
- AnyView, AnyWindowHandle, AppCell, AppContext, BackgroundExecutor, Context, FocusableView,
- ForegroundExecutor, Manager, Model, ModelContext, Render, Result, Task, View, ViewContext,
- VisualContext, WindowContext, WindowHandle,
+ AnyView, AnyWindowHandle, AppCell, AppContext, BackgroundExecutor, Context, DismissEvent,
+ FocusableView, ForegroundExecutor, Model, ModelContext, Render, Result, Task, View,
+ ViewContext, VisualContext, WindowContext, WindowHandle,
};
use anyhow::{anyhow, Context as _};
use derive_more::{Deref, DerefMut};
@@ -115,7 +115,7 @@ impl AsyncAppContext {
build_root_view: impl FnOnce(&mut WindowContext) -> View<V>,
) -> Result<WindowHandle<V>>
where
- V: Render,
+ V: 'static + Render,
{
let app = self
.app
@@ -306,7 +306,7 @@ impl VisualContext for AsyncWindowContext {
build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V,
) -> Self::Result<View<V>>
where
- V: Render,
+ V: 'static + Render,
{
self.window
.update(self, |_, cx| cx.replace_root_view(build_view))
@@ -326,7 +326,7 @@ impl VisualContext for AsyncWindowContext {
V: crate::ManagedView,
{
self.window.update(self, |_, cx| {
- view.update(cx, |_, cx| cx.emit(Manager::Dismiss))
+ view.update(cx, |_, cx| cx.emit(DismissEvent::Dismiss))
})
}
}
@@ -1,10 +1,10 @@
-use crate::{private::Sealed, AnyBox, AppContext, Context, Entity, ModelContext};
+use crate::{private::Sealed, AppContext, Context, Entity, ModelContext};
use anyhow::{anyhow, Result};
use derive_more::{Deref, DerefMut};
use parking_lot::{RwLock, RwLockUpgradableReadGuard};
use slotmap::{SecondaryMap, SlotMap};
use std::{
- any::{type_name, TypeId},
+ any::{type_name, Any, TypeId},
fmt::{self, Display},
hash::{Hash, Hasher},
marker::PhantomData,
@@ -31,7 +31,7 @@ impl Display for EntityId {
}
pub(crate) struct EntityMap {
- entities: SecondaryMap<EntityId, AnyBox>,
+ entities: SecondaryMap<EntityId, Box<dyn Any>>,
ref_counts: Arc<RwLock<EntityRefCounts>>,
}
@@ -102,7 +102,7 @@ impl EntityMap {
);
}
- pub fn take_dropped(&mut self) -> Vec<(EntityId, AnyBox)> {
+ pub fn take_dropped(&mut self) -> Vec<(EntityId, Box<dyn Any>)> {
let mut ref_counts = self.ref_counts.write();
let dropped_entity_ids = mem::take(&mut ref_counts.dropped_entity_ids);
@@ -122,7 +122,7 @@ impl EntityMap {
}
pub struct Lease<'a, T> {
- entity: Option<AnyBox>,
+ entity: Option<Box<dyn Any>>,
pub model: &'a Model<T>,
entity_type: PhantomData<T>,
}
@@ -1,8 +1,9 @@
use crate::{
div, Action, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext,
- BackgroundExecutor, Context, Div, EventEmitter, ForegroundExecutor, InputEvent, KeyDownEvent,
- Keystroke, Model, ModelContext, Render, Result, Task, TestDispatcher, TestPlatform, TestWindow,
- View, ViewContext, VisualContext, WindowContext, WindowHandle, WindowOptions,
+ BackgroundExecutor, Context, Div, Entity, EventEmitter, ForegroundExecutor, InputEvent,
+ KeyDownEvent, Keystroke, Model, ModelContext, Render, Result, Task, TestDispatcher,
+ TestPlatform, TestWindow, View, ViewContext, VisualContext, WindowContext, WindowHandle,
+ WindowOptions,
};
use anyhow::{anyhow, bail};
use futures::{Stream, StreamExt};
@@ -126,7 +127,7 @@ impl TestAppContext {
pub fn add_window<F, V>(&mut self, build_window: F) -> WindowHandle<V>
where
F: FnOnce(&mut ViewContext<V>) -> V,
- V: Render,
+ V: 'static + Render,
{
let mut cx = self.app.borrow_mut();
cx.open_window(WindowOptions::default(), |cx| cx.build_view(build_window))
@@ -143,7 +144,7 @@ impl TestAppContext {
pub fn add_window_view<F, V>(&mut self, build_window: F) -> (View<V>, &mut VisualTestContext)
where
F: FnOnce(&mut ViewContext<V>) -> V,
- V: Render,
+ V: 'static + Render,
{
let mut cx = self.app.borrow_mut();
let window = cx.open_window(WindowOptions::default(), |cx| cx.build_view(build_window));
@@ -296,21 +297,19 @@ impl TestAppContext {
.unwrap()
}
- pub fn notifications<T: 'static>(&mut self, entity: &Model<T>) -> impl Stream<Item = ()> {
+ pub fn notifications<T: 'static>(&mut self, entity: &impl Entity<T>) -> impl Stream<Item = ()> {
let (tx, rx) = futures::channel::mpsc::unbounded();
-
- entity.update(self, move |_, cx: &mut ModelContext<T>| {
+ self.update(|cx| {
cx.observe(entity, {
let tx = tx.clone();
- move |_, _, _| {
+ move |_, _| {
let _ = tx.unbounded_send(());
}
})
.detach();
-
- cx.on_release(move |_, _| tx.close_channel()).detach();
+ cx.observe_release(entity, move |_, _| tx.close_channel())
+ .detach()
});
-
rx
}
@@ -591,7 +590,7 @@ impl<'a> VisualContext for VisualTestContext<'a> {
build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V,
) -> Self::Result<View<V>>
where
- V: Render,
+ V: 'static + Render,
{
self.window
.update(self.cx, |_, cx| cx.replace_root_view(build_view))
@@ -612,7 +611,7 @@ impl<'a> VisualContext for VisualTestContext<'a> {
{
self.window
.update(self.cx, |_, cx| {
- view.update(cx, |_, cx| cx.emit(crate::Manager::Dismiss))
+ view.update(cx, |_, cx| cx.emit(crate::DismissEvent::Dismiss))
})
.unwrap()
}
@@ -631,7 +630,7 @@ impl AnyWindowHandle {
pub struct EmptyView {}
impl Render for EmptyView {
- type Element = Div<Self>;
+ type Element = Div;
fn render(&mut self, _cx: &mut crate::ViewContext<Self>) -> Self::Element {
div()
@@ -1,236 +1,328 @@
use crate::{
AvailableSpace, BorrowWindow, Bounds, ElementId, LayoutId, Pixels, Point, Size, ViewContext,
+ WindowContext,
};
use derive_more::{Deref, DerefMut};
pub(crate) use smallvec::SmallVec;
-use std::{any::Any, fmt::Debug, mem};
+use std::{any::Any, fmt::Debug};
-pub trait Element<V: 'static> {
- type ElementState: 'static;
+pub trait Render: 'static + Sized {
+ type Element: Element + 'static;
+
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element;
+}
+
+pub trait IntoElement: Sized {
+ type Element: Element + 'static;
fn element_id(&self) -> Option<ElementId>;
- fn layout(
- &mut self,
- view_state: &mut V,
- element_state: Option<Self::ElementState>,
- cx: &mut ViewContext<V>,
- ) -> (LayoutId, Self::ElementState);
+ fn into_element(self) -> Self::Element;
- fn paint(
- &mut self,
- bounds: Bounds<Pixels>,
- view_state: &mut V,
- element_state: &mut Self::ElementState,
- cx: &mut ViewContext<V>,
- );
+ fn into_any_element(self) -> AnyElement {
+ self.into_element().into_any()
+ }
fn draw<T, R>(
self,
origin: Point<Pixels>,
available_space: Size<T>,
- view_state: &mut V,
- cx: &mut ViewContext<V>,
- f: impl FnOnce(&Self::ElementState, &mut ViewContext<V>) -> R,
+ cx: &mut WindowContext,
+ f: impl FnOnce(&mut <Self::Element as Element>::State, &mut WindowContext) -> R,
) -> R
where
- Self: Sized,
T: Clone + Default + Debug + Into<AvailableSpace>,
{
- let mut element = RenderedElement {
- element: self,
- phase: ElementRenderPhase::Start,
+ let element = self.into_element();
+ let element_id = element.element_id();
+ let element = DrawableElement {
+ element: Some(element),
+ phase: ElementDrawPhase::Start,
};
- element.draw(origin, available_space.map(Into::into), view_state, cx);
- if let ElementRenderPhase::Painted { frame_state } = &element.phase {
- if let Some(frame_state) = frame_state.as_ref() {
- f(&frame_state, cx)
+
+ let frame_state =
+ DrawableElement::draw(element, origin, available_space.map(Into::into), cx);
+
+ if let Some(mut frame_state) = frame_state {
+ f(&mut frame_state, cx)
+ } else {
+ cx.with_element_state(element_id.unwrap(), |element_state, cx| {
+ let mut element_state = element_state.unwrap();
+ let result = f(&mut element_state, cx);
+ (result, element_state)
+ })
+ }
+ }
+
+ fn map<U>(self, f: impl FnOnce(Self) -> U) -> U
+ where
+ Self: Sized,
+ U: IntoElement,
+ {
+ f(self)
+ }
+
+ fn when(self, condition: bool, then: impl FnOnce(Self) -> Self) -> Self
+ where
+ Self: Sized,
+ {
+ self.map(|this| if condition { then(this) } else { this })
+ }
+
+ fn when_some<T>(self, option: Option<T>, then: impl FnOnce(Self, T) -> Self) -> Self
+ where
+ Self: Sized,
+ {
+ self.map(|this| {
+ if let Some(value) = option {
+ then(this, value)
} else {
- let element_id = element
- .element
- .element_id()
- .expect("we either have some frame_state or some element_id");
- cx.with_element_state(element_id, |element_state, cx| {
- let element_state = element_state.unwrap();
- let result = f(&element_state, cx);
- (result, element_state)
- })
+ this
}
- } else {
- unreachable!()
+ })
+ }
+}
+
+pub trait Element: 'static + IntoElement {
+ type State: 'static;
+
+ fn layout(
+ &mut self,
+ state: Option<Self::State>,
+ cx: &mut WindowContext,
+ ) -> (LayoutId, Self::State);
+
+ fn paint(self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext);
+
+ fn into_any(self) -> AnyElement {
+ AnyElement::new(self)
+ }
+}
+
+pub trait RenderOnce: 'static {
+ type Rendered: IntoElement;
+
+ fn render(self, cx: &mut WindowContext) -> Self::Rendered;
+}
+
+pub struct Component<C> {
+ component: Option<C>,
+}
+
+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> Component<C> {
+ pub fn new(component: C) -> Self {
+ Component {
+ component: Some(component),
}
}
}
+impl<C: RenderOnce> Element for Component<C> {
+ type State = CompositeElementState<C>;
+
+ fn layout(
+ &mut self,
+ state: Option<Self::State>,
+ cx: &mut WindowContext,
+ ) -> (LayoutId, Self::State) {
+ let mut element = self.component.take().unwrap().render(cx).into_element();
+ let (layout_id, state) = element.layout(state.map(|s| s.rendered_element_state), cx);
+ let state = CompositeElementState {
+ rendered_element: Some(element),
+ rendered_element_state: state,
+ };
+ (layout_id, state)
+ }
+
+ fn paint(self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
+ state
+ .rendered_element
+ .take()
+ .unwrap()
+ .paint(bounds, &mut state.rendered_element_state, cx);
+ }
+}
+
+impl<C: RenderOnce> IntoElement for Component<C> {
+ type Element = Self;
+
+ fn element_id(&self) -> Option<ElementId> {
+ None
+ }
+
+ fn into_element(self) -> Self::Element {
+ self
+ }
+}
+
#[derive(Deref, DerefMut, Default, Clone, Debug, Eq, PartialEq, Hash)]
pub struct GlobalElementId(SmallVec<[ElementId; 32]>);
-pub trait ParentComponent<V: 'static> {
- fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]>;
+pub trait ParentElement {
+ fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]>;
- fn child(mut self, child: impl Component<V>) -> Self
+ fn child(mut self, child: impl IntoElement) -> Self
where
Self: Sized,
{
- self.children_mut().push(child.render());
+ self.children_mut().push(child.into_element().into_any());
self
}
- fn children(mut self, iter: impl IntoIterator<Item = impl Component<V>>) -> Self
+ fn children(mut self, children: impl IntoIterator<Item = impl IntoElement>) -> Self
where
Self: Sized,
{
self.children_mut()
- .extend(iter.into_iter().map(|item| item.render()));
+ .extend(children.into_iter().map(|child| child.into_any_element()));
self
}
}
-trait ElementObject<V> {
+trait ElementObject {
fn element_id(&self) -> Option<ElementId>;
- fn layout(&mut self, view_state: &mut V, cx: &mut ViewContext<V>) -> LayoutId;
- fn paint(&mut self, view_state: &mut V, cx: &mut ViewContext<V>);
+
+ fn layout(&mut self, cx: &mut WindowContext) -> LayoutId;
+
+ fn paint(&mut self, cx: &mut WindowContext);
+
fn measure(
&mut self,
available_space: Size<AvailableSpace>,
- view_state: &mut V,
- cx: &mut ViewContext<V>,
+ cx: &mut WindowContext,
) -> Size<Pixels>;
+
fn draw(
&mut self,
origin: Point<Pixels>,
available_space: Size<AvailableSpace>,
- view_state: &mut V,
- cx: &mut ViewContext<V>,
+ cx: &mut WindowContext,
);
}
-struct RenderedElement<V: 'static, E: Element<V>> {
- element: E,
- phase: ElementRenderPhase<E::ElementState>,
+pub struct DrawableElement<E: Element> {
+ element: Option<E>,
+ phase: ElementDrawPhase<E::State>,
}
#[derive(Default)]
-enum ElementRenderPhase<V> {
+enum ElementDrawPhase<S> {
#[default]
Start,
LayoutRequested {
layout_id: LayoutId,
- frame_state: Option<V>,
+ frame_state: Option<S>,
},
LayoutComputed {
layout_id: LayoutId,
available_space: Size<AvailableSpace>,
- frame_state: Option<V>,
- },
- Painted {
- frame_state: Option<V>,
+ frame_state: Option<S>,
},
}
-/// Internal struct that wraps an element to store Layout and ElementState after the element is rendered.
-/// It's allocated as a trait object to erase the element type and wrapped in AnyElement<E::State> for
-/// improved usability.
-impl<V, E: Element<V>> RenderedElement<V, E> {
+/// A wrapper around an implementer of [Element] that allows it to be drawn in a window.
+impl<E: Element> DrawableElement<E> {
fn new(element: E) -> Self {
- RenderedElement {
- element,
- phase: ElementRenderPhase::Start,
+ DrawableElement {
+ element: Some(element),
+ phase: ElementDrawPhase::Start,
}
}
-}
-impl<V, E> ElementObject<V> for RenderedElement<V, E>
-where
- E: Element<V>,
- E::ElementState: 'static,
-{
fn element_id(&self) -> Option<ElementId> {
- self.element.element_id()
+ self.element.as_ref()?.element_id()
}
- fn layout(&mut self, state: &mut V, cx: &mut ViewContext<V>) -> LayoutId {
- let (layout_id, frame_state) = match mem::take(&mut self.phase) {
- ElementRenderPhase::Start => {
- if let Some(id) = self.element.element_id() {
- let layout_id = cx.with_element_state(id, |element_state, cx| {
- self.element.layout(state, element_state, cx)
- });
- (layout_id, None)
- } else {
- let (layout_id, frame_state) = self.element.layout(state, None, cx);
- (layout_id, Some(frame_state))
- }
- }
- ElementRenderPhase::LayoutRequested { .. }
- | ElementRenderPhase::LayoutComputed { .. }
- | ElementRenderPhase::Painted { .. } => {
- panic!("element rendered twice")
- }
+ fn layout(&mut self, cx: &mut WindowContext) -> LayoutId {
+ let (layout_id, frame_state) = if let Some(id) = self.element.as_ref().unwrap().element_id()
+ {
+ let layout_id = cx.with_element_state(id, |element_state, cx| {
+ self.element.as_mut().unwrap().layout(element_state, cx)
+ });
+ (layout_id, None)
+ } else {
+ let (layout_id, frame_state) = self.element.as_mut().unwrap().layout(None, cx);
+ (layout_id, Some(frame_state))
};
- self.phase = ElementRenderPhase::LayoutRequested {
+ self.phase = ElementDrawPhase::LayoutRequested {
layout_id,
frame_state,
};
layout_id
}
- fn paint(&mut self, view_state: &mut V, cx: &mut ViewContext<V>) {
- self.phase = match mem::take(&mut self.phase) {
- ElementRenderPhase::LayoutRequested {
+ fn paint(mut self, cx: &mut WindowContext) -> Option<E::State> {
+ match self.phase {
+ ElementDrawPhase::LayoutRequested {
layout_id,
- mut frame_state,
+ frame_state,
}
- | ElementRenderPhase::LayoutComputed {
+ | ElementDrawPhase::LayoutComputed {
layout_id,
- mut frame_state,
+ frame_state,
..
} => {
let bounds = cx.layout_bounds(layout_id);
- if let Some(id) = self.element.element_id() {
- cx.with_element_state(id, |element_state, cx| {
+
+ if let Some(mut frame_state) = frame_state {
+ self.element
+ .take()
+ .unwrap()
+ .paint(bounds, &mut frame_state, cx);
+ Some(frame_state)
+ } else {
+ let element_id = self
+ .element
+ .as_ref()
+ .unwrap()
+ .element_id()
+ .expect("if we don't have frame state, we should have element state");
+ cx.with_element_state(element_id, |element_state, cx| {
let mut element_state = element_state.unwrap();
self.element
- .paint(bounds, view_state, &mut element_state, cx);
+ .take()
+ .unwrap()
+ .paint(bounds, &mut element_state, cx);
((), element_state)
});
- } else {
- self.element
- .paint(bounds, view_state, frame_state.as_mut().unwrap(), cx);
+ None
}
- ElementRenderPhase::Painted { frame_state }
}
_ => panic!("must call layout before paint"),
- };
+ }
}
fn measure(
&mut self,
available_space: Size<AvailableSpace>,
- view_state: &mut V,
- cx: &mut ViewContext<V>,
+ cx: &mut WindowContext,
) -> Size<Pixels> {
- if matches!(&self.phase, ElementRenderPhase::Start) {
- self.layout(view_state, cx);
+ if matches!(&self.phase, ElementDrawPhase::Start) {
+ self.layout(cx);
}
let layout_id = match &mut self.phase {
- ElementRenderPhase::LayoutRequested {
+ ElementDrawPhase::LayoutRequested {
layout_id,
frame_state,
} => {
cx.compute_layout(*layout_id, available_space);
let layout_id = *layout_id;
- self.phase = ElementRenderPhase::LayoutComputed {
+ self.phase = ElementDrawPhase::LayoutComputed {
layout_id,
available_space,
frame_state: frame_state.take(),
};
layout_id
}
- ElementRenderPhase::LayoutComputed {
+ ElementDrawPhase::LayoutComputed {
layout_id,
available_space: prev_available_space,
..
@@ -248,154 +340,203 @@ where
}
fn draw(
- &mut self,
+ mut self,
origin: Point<Pixels>,
available_space: Size<AvailableSpace>,
- view_state: &mut V,
- cx: &mut ViewContext<V>,
- ) {
- self.measure(available_space, view_state, cx);
- cx.with_absolute_element_offset(origin, |cx| self.paint(view_state, cx))
+ cx: &mut WindowContext,
+ ) -> Option<E::State> {
+ self.measure(available_space, cx);
+ cx.with_absolute_element_offset(origin, |cx| self.paint(cx))
}
}
-pub struct AnyElement<V>(Box<dyn ElementObject<V>>);
+// impl<V: 'static, E: Element> Element for DrawableElement<V, E> {
+// type State = <E::Element as Element>::State;
-impl<V> AnyElement<V> {
- pub fn new<E>(element: E) -> Self
- where
- V: 'static,
- E: 'static + Element<V>,
- E::ElementState: Any,
- {
- AnyElement(Box::new(RenderedElement::new(element)))
- }
+// fn layout(
+// &mut self,
+// element_state: Option<Self::State>,
+// cx: &mut WindowContext,
+// ) -> (LayoutId, Self::State) {
- pub fn element_id(&self) -> Option<ElementId> {
- self.0.element_id()
+// }
+
+// fn paint(
+// self,
+// bounds: Bounds<Pixels>,
+// element_state: &mut Self::State,
+// cx: &mut WindowContext,
+// ) {
+// todo!()
+// }
+// }
+
+// impl<V: 'static, E: 'static + Element> RenderOnce for DrawableElement<V, E> {
+// type Element = Self;
+
+// fn element_id(&self) -> Option<ElementId> {
+// self.element.as_ref()?.element_id()
+// }
+
+// fn render_once(self) -> Self::Element {
+// self
+// }
+// }
+
+impl<E> ElementObject for Option<DrawableElement<E>>
+where
+ E: Element,
+ E::State: 'static,
+{
+ fn element_id(&self) -> Option<ElementId> {
+ self.as_ref().unwrap().element_id()
}
- pub fn layout(&mut self, view_state: &mut V, cx: &mut ViewContext<V>) -> LayoutId {
- self.0.layout(view_state, cx)
+ fn layout(&mut self, cx: &mut WindowContext) -> LayoutId {
+ DrawableElement::layout(self.as_mut().unwrap(), cx)
}
- pub fn paint(&mut self, view_state: &mut V, cx: &mut ViewContext<V>) {
- self.0.paint(view_state, cx)
+ fn paint(&mut self, cx: &mut WindowContext) {
+ DrawableElement::paint(self.take().unwrap(), cx);
}
- /// Initializes this element and performs layout within the given available space to determine its size.
- pub fn measure(
+ fn measure(
&mut self,
available_space: Size<AvailableSpace>,
- view_state: &mut V,
- cx: &mut ViewContext<V>,
+ cx: &mut WindowContext,
) -> Size<Pixels> {
- self.0.measure(available_space, view_state, cx)
+ DrawableElement::measure(self.as_mut().unwrap(), available_space, cx)
}
- /// Initializes this element and performs layout in the available space, then paints it at the given origin.
- pub fn draw(
+ fn draw(
&mut self,
origin: Point<Pixels>,
available_space: Size<AvailableSpace>,
- view_state: &mut V,
- cx: &mut ViewContext<V>,
+ cx: &mut WindowContext,
) {
- self.0.draw(origin, available_space, view_state, cx)
+ DrawableElement::draw(self.take().unwrap(), origin, available_space, cx);
}
}
-pub trait Component<V> {
- fn render(self) -> AnyElement<V>;
+pub struct AnyElement(Box<dyn ElementObject>);
- fn map<U>(self, f: impl FnOnce(Self) -> U) -> U
+impl AnyElement {
+ pub fn new<E>(element: E) -> Self
where
- Self: Sized,
- U: Component<V>,
+ E: 'static + Element,
+ E::State: Any,
{
- f(self)
+ AnyElement(Box::new(Some(DrawableElement::new(element))) as Box<dyn ElementObject>)
}
- fn when(self, condition: bool, then: impl FnOnce(Self) -> Self) -> Self
- where
- Self: Sized,
- {
- self.map(|this| if condition { then(this) } else { this })
+ pub fn layout(&mut self, cx: &mut WindowContext) -> LayoutId {
+ self.0.layout(cx)
}
- fn when_some<T>(self, option: Option<T>, then: impl FnOnce(Self, T) -> Self) -> Self
- where
- Self: Sized,
- {
- self.map(|this| {
- if let Some(value) = option {
- then(this, value)
- } else {
- this
- }
- })
+ pub fn paint(mut self, cx: &mut WindowContext) {
+ self.0.paint(cx)
}
-}
-impl<V> Component<V> for AnyElement<V> {
- fn render(self) -> AnyElement<V> {
- self
+ /// Initializes this element and performs layout within the given available space to determine its size.
+ pub fn measure(
+ &mut self,
+ available_space: Size<AvailableSpace>,
+ cx: &mut WindowContext,
+ ) -> Size<Pixels> {
+ self.0.measure(available_space, cx)
}
-}
-impl<V, E, F> Element<V> for Option<F>
-where
- V: 'static,
- E: 'static + Component<V>,
- F: FnOnce(&mut V, &mut ViewContext<'_, V>) -> E + 'static,
-{
- type ElementState = AnyElement<V>;
+ /// Initializes this element and performs layout in the available space, then paints it at the given origin.
+ pub fn draw(
+ mut self,
+ origin: Point<Pixels>,
+ available_space: Size<AvailableSpace>,
+ cx: &mut WindowContext,
+ ) {
+ self.0.draw(origin, available_space, cx)
+ }
- fn element_id(&self) -> Option<ElementId> {
- None
+ /// Converts this `AnyElement` into a trait object that can be stored and manipulated.
+ pub fn into_any(self) -> AnyElement {
+ AnyElement::new(self)
}
+ pub fn inner_id(&self) -> Option<ElementId> {
+ self.0.element_id()
+ }
+}
+
+impl Element for AnyElement {
+ type State = ();
+
fn layout(
&mut self,
- view_state: &mut V,
- _: Option<Self::ElementState>,
- cx: &mut ViewContext<V>,
- ) -> (LayoutId, Self::ElementState) {
- let render = self.take().unwrap();
- let mut rendered_element = (render)(view_state, cx).render();
- let layout_id = rendered_element.layout(view_state, cx);
- (layout_id, rendered_element)
+ _: Option<Self::State>,
+ cx: &mut WindowContext,
+ ) -> (LayoutId, Self::State) {
+ let layout_id = self.layout(cx);
+ (layout_id, ())
}
- fn paint(
- &mut self,
- _bounds: Bounds<Pixels>,
- view_state: &mut V,
- rendered_element: &mut Self::ElementState,
- cx: &mut ViewContext<V>,
- ) {
- rendered_element.paint(view_state, cx)
+ fn paint(self, _: Bounds<Pixels>, _: &mut Self::State, cx: &mut WindowContext) {
+ self.paint(cx);
}
}
-impl<V, E, F> Component<V> for Option<F>
-where
- V: 'static,
- E: 'static + Component<V>,
- F: FnOnce(&mut V, &mut ViewContext<'_, V>) -> E + 'static,
-{
- fn render(self) -> AnyElement<V> {
- AnyElement::new(self)
+impl IntoElement for AnyElement {
+ type Element = Self;
+
+ fn element_id(&self) -> Option<ElementId> {
+ None
}
-}
-impl<V, E, F> Component<V> for F
-where
- V: 'static,
- E: 'static + Component<V>,
- F: FnOnce(&mut V, &mut ViewContext<'_, V>) -> E + 'static,
-{
- fn render(self) -> AnyElement<V> {
- AnyElement::new(Some(self))
+ fn into_element(self) -> Self::Element {
+ self
}
}
+
+// impl<V, E, F> Element for Option<F>
+// where
+// V: 'static,
+// E: Element,
+// F: FnOnce(&mut V, &mut WindowContext<'_, V>) -> E + 'static,
+// {
+// type State = Option<AnyElement>;
+
+// fn element_id(&self) -> Option<ElementId> {
+// None
+// }
+
+// fn layout(
+// &mut self,
+// _: Option<Self::State>,
+// cx: &mut WindowContext,
+// ) -> (LayoutId, Self::State) {
+// let render = self.take().unwrap();
+// let mut element = (render)(view_state, cx).into_any();
+// let layout_id = element.layout(view_state, cx);
+// (layout_id, Some(element))
+// }
+
+// fn paint(
+// self,
+// _bounds: Bounds<Pixels>,
+// rendered_element: &mut Self::State,
+// cx: &mut WindowContext,
+// ) {
+// rendered_element.take().unwrap().paint(view_state, cx);
+// }
+// }
+
+// impl<V, E, F> RenderOnce for Option<F>
+// where
+// V: 'static,
+// E: Element,
+// F: FnOnce(&mut V, &mut WindowContext) -> E + 'static,
+// {
+// type Element = Self;
+
+// fn render(self) -> Self::Element {
+// self
+// }
+// }
@@ -1,9 +1,9 @@
use crate::{
point, px, Action, AnyDrag, AnyElement, AnyTooltip, AnyView, AppContext, BorrowAppContext,
- BorrowWindow, Bounds, ClickEvent, Component, DispatchPhase, Element, ElementId, FocusEvent,
- FocusHandle, KeyContext, KeyDownEvent, KeyUpEvent, LayoutId, MouseButton, MouseDownEvent,
- MouseMoveEvent, MouseUpEvent, ParentComponent, Pixels, Point, Render, ScrollWheelEvent,
- SharedString, Size, Style, StyleRefinement, Styled, Task, View, ViewContext, Visibility,
+ BorrowWindow, Bounds, ClickEvent, DispatchPhase, Element, ElementId, FocusEvent, FocusHandle,
+ IntoElement, KeyContext, KeyDownEvent, KeyUpEvent, LayoutId, MouseButton, MouseDownEvent,
+ MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Point, Render, ScrollWheelEvent,
+ SharedString, Size, Style, StyleRefinement, Styled, Task, View, Visibility, WindowContext,
};
use collections::HashMap;
use refineable::Refineable;
@@ -12,7 +12,6 @@ use std::{
any::{Any, TypeId},
cell::RefCell,
fmt::Debug,
- marker::PhantomData,
mem,
rc::Rc,
time::Duration,
@@ -28,30 +27,24 @@ pub struct GroupStyle {
pub style: StyleRefinement,
}
-pub trait InteractiveComponent<V: 'static>: Sized + Element<V> {
- fn interactivity(&mut self) -> &mut Interactivity<V>;
+pub trait InteractiveElement: Sized + Element {
+ fn interactivity(&mut self) -> &mut Interactivity;
fn group(mut self, group: impl Into<SharedString>) -> Self {
self.interactivity().group = Some(group.into());
self
}
- fn id(mut self, id: impl Into<ElementId>) -> Stateful<V, Self> {
+ fn id(mut self, id: impl Into<ElementId>) -> Stateful<Self> {
self.interactivity().element_id = Some(id.into());
- Stateful {
- element: self,
- view_type: PhantomData,
- }
+ Stateful { element: self }
}
- fn track_focus(mut self, focus_handle: &FocusHandle) -> Focusable<V, Self> {
+ fn track_focus(mut self, focus_handle: &FocusHandle) -> Focusable<Self> {
self.interactivity().focusable = true;
self.interactivity().tracked_focus_handle = Some(focus_handle.clone());
- Focusable {
- element: self,
- view_type: PhantomData,
- }
+ Focusable { element: self }
}
fn key_context<C, E>(mut self, key_context: C) -> Self
@@ -85,15 +78,15 @@ pub trait InteractiveComponent<V: 'static>: Sized + Element<V> {
fn on_mouse_down(
mut self,
button: MouseButton,
- handler: impl Fn(&mut V, &MouseDownEvent, &mut ViewContext<V>) + 'static,
+ listener: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
) -> Self {
self.interactivity().mouse_down_listeners.push(Box::new(
- move |view, event, bounds, phase, cx| {
+ move |event, bounds, phase, cx| {
if phase == DispatchPhase::Bubble
&& event.button == button
&& bounds.contains_point(&event.position)
{
- handler(view, event, cx)
+ (listener)(event, cx)
}
},
));
@@ -102,12 +95,12 @@ pub trait InteractiveComponent<V: 'static>: Sized + Element<V> {
fn on_any_mouse_down(
mut self,
- handler: impl Fn(&mut V, &MouseDownEvent, &mut ViewContext<V>) + 'static,
+ listener: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
) -> Self {
self.interactivity().mouse_down_listeners.push(Box::new(
- move |view, event, bounds, phase, cx| {
+ move |event, bounds, phase, cx| {
if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
- handler(view, event, cx)
+ (listener)(event, cx)
}
},
));
@@ -117,43 +110,43 @@ pub trait InteractiveComponent<V: 'static>: Sized + Element<V> {
fn on_mouse_up(
mut self,
button: MouseButton,
- handler: impl Fn(&mut V, &MouseUpEvent, &mut ViewContext<V>) + 'static,
+ listener: impl Fn(&MouseUpEvent, &mut WindowContext) + 'static,
) -> Self {
- self.interactivity().mouse_up_listeners.push(Box::new(
- move |view, event, bounds, phase, cx| {
+ self.interactivity()
+ .mouse_up_listeners
+ .push(Box::new(move |event, bounds, phase, cx| {
if phase == DispatchPhase::Bubble
&& event.button == button
&& bounds.contains_point(&event.position)
{
- handler(view, event, cx)
+ (listener)(event, cx)
}
- },
- ));
+ }));
self
}
fn on_any_mouse_up(
mut self,
- handler: impl Fn(&mut V, &MouseUpEvent, &mut ViewContext<V>) + 'static,
+ listener: impl Fn(&MouseUpEvent, &mut WindowContext) + 'static,
) -> Self {
- self.interactivity().mouse_up_listeners.push(Box::new(
- move |view, event, bounds, phase, cx| {
+ self.interactivity()
+ .mouse_up_listeners
+ .push(Box::new(move |event, bounds, phase, cx| {
if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
- handler(view, event, cx)
+ (listener)(event, cx)
}
- },
- ));
+ }));
self
}
fn on_mouse_down_out(
mut self,
- handler: impl Fn(&mut V, &MouseDownEvent, &mut ViewContext<V>) + 'static,
+ listener: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
) -> Self {
self.interactivity().mouse_down_listeners.push(Box::new(
- move |view, event, bounds, phase, cx| {
+ move |event, bounds, phase, cx| {
if phase == DispatchPhase::Capture && !bounds.contains_point(&event.position) {
- handler(view, event, cx)
+ (listener)(event, cx)
}
},
));
@@ -163,29 +156,29 @@ pub trait InteractiveComponent<V: 'static>: Sized + Element<V> {
fn on_mouse_up_out(
mut self,
button: MouseButton,
- handler: impl Fn(&mut V, &MouseUpEvent, &mut ViewContext<V>) + 'static,
+ listener: impl Fn(&MouseUpEvent, &mut WindowContext) + 'static,
) -> Self {
- self.interactivity().mouse_up_listeners.push(Box::new(
- move |view, event, bounds, phase, cx| {
+ self.interactivity()
+ .mouse_up_listeners
+ .push(Box::new(move |event, bounds, phase, cx| {
if phase == DispatchPhase::Capture
&& event.button == button
&& !bounds.contains_point(&event.position)
{
- handler(view, event, cx);
+ (listener)(event, cx);
}
- },
- ));
+ }));
self
}
fn on_mouse_move(
mut self,
- handler: impl Fn(&mut V, &MouseMoveEvent, &mut ViewContext<V>) + 'static,
+ listener: impl Fn(&MouseMoveEvent, &mut WindowContext) + 'static,
) -> Self {
self.interactivity().mouse_move_listeners.push(Box::new(
- move |view, event, bounds, phase, cx| {
+ move |event, bounds, phase, cx| {
if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
- handler(view, event, cx);
+ (listener)(event, cx);
}
},
));
@@ -194,29 +187,29 @@ pub trait InteractiveComponent<V: 'static>: Sized + Element<V> {
fn on_scroll_wheel(
mut self,
- handler: impl Fn(&mut V, &ScrollWheelEvent, &mut ViewContext<V>) + 'static,
+ listener: impl Fn(&ScrollWheelEvent, &mut WindowContext) + 'static,
) -> Self {
self.interactivity().scroll_wheel_listeners.push(Box::new(
- move |view, event, bounds, phase, cx| {
+ move |event, bounds, phase, cx| {
if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
- handler(view, event, cx);
+ (listener)(event, cx);
}
},
));
self
}
- /// Capture the given action, fires during the capture phase
+ /// Capture the given action, before normal action dispatch can fire
fn capture_action<A: Action>(
mut self,
- listener: impl Fn(&mut V, &A, &mut ViewContext<V>) + 'static,
+ listener: impl Fn(&A, &mut WindowContext) + 'static,
) -> Self {
self.interactivity().action_listeners.push((
TypeId::of::<A>(),
- Box::new(move |view, action, phase, cx| {
+ Box::new(move |action, phase, cx| {
let action = action.downcast_ref().unwrap();
if phase == DispatchPhase::Capture {
- listener(view, action, cx)
+ (listener)(action, cx)
}
}),
));
@@ -224,10 +217,7 @@ pub trait InteractiveComponent<V: 'static>: Sized + Element<V> {
}
/// Add a listener for the given action, fires during the bubble event phase
- fn on_action<A: Action>(
- mut self,
- listener: impl Fn(&mut V, &A, &mut ViewContext<V>) + 'static,
- ) -> Self {
+ fn on_action<A: Action>(mut self, listener: impl Fn(&A, &mut WindowContext) + 'static) -> Self {
// NOTE: this debug assert has the side-effect of working around
// a bug where a crate consisting only of action definitions does
// not register the actions in debug builds:
@@ -244,10 +234,10 @@ pub trait InteractiveComponent<V: 'static>: Sized + Element<V> {
// );
self.interactivity().action_listeners.push((
TypeId::of::<A>(),
- Box::new(move |view, action, phase, cx| {
+ Box::new(move |action, phase, cx| {
let action = action.downcast_ref().unwrap();
if phase == DispatchPhase::Bubble {
- listener(view, action, cx)
+ (listener)(action, cx)
}
}),
));
@@ -256,24 +246,53 @@ pub trait InteractiveComponent<V: 'static>: Sized + Element<V> {
fn on_key_down(
mut self,
- listener: impl Fn(&mut V, &KeyDownEvent, DispatchPhase, &mut ViewContext<V>) + 'static,
+ listener: impl Fn(&KeyDownEvent, &mut WindowContext) + 'static,
) -> Self {
self.interactivity()
.key_down_listeners
- .push(Box::new(move |view, event, phase, cx| {
- listener(view, event, phase, cx)
+ .push(Box::new(move |event, phase, cx| {
+ if phase == DispatchPhase::Bubble {
+ (listener)(event, cx)
+ }
}));
self
}
- fn on_key_up(
+ fn capture_key_down(
mut self,
- listener: impl Fn(&mut V, &KeyUpEvent, DispatchPhase, &mut ViewContext<V>) + 'static,
+ listener: impl Fn(&KeyDownEvent, &mut WindowContext) + 'static,
) -> Self {
+ self.interactivity()
+ .key_down_listeners
+ .push(Box::new(move |event, phase, cx| {
+ if phase == DispatchPhase::Capture {
+ listener(event, cx)
+ }
+ }));
+ self
+ }
+
+ fn on_key_up(mut self, listener: impl Fn(&KeyUpEvent, &mut WindowContext) + 'static) -> Self {
self.interactivity()
.key_up_listeners
- .push(Box::new(move |view, event, phase, cx| {
- listener(view, event, phase, cx)
+ .push(Box::new(move |event, phase, cx| {
+ if phase == DispatchPhase::Bubble {
+ listener(event, cx)
+ }
+ }));
+ self
+ }
+
+ fn capture_key_up(
+ mut self,
+ listener: impl Fn(&KeyUpEvent, &mut WindowContext) + 'static,
+ ) -> Self {
+ self.interactivity()
+ .key_up_listeners
+ .push(Box::new(move |event, phase, cx| {
+ if phase == DispatchPhase::Capture {
+ listener(event, cx)
+ }
}));
self
}
@@ -302,25 +321,22 @@ pub trait InteractiveComponent<V: 'static>: Sized + Element<V> {
fn on_drop<W: 'static>(
mut self,
- listener: impl Fn(&mut V, View<W>, &mut ViewContext<V>) + 'static,
+ listener: impl Fn(&View<W>, &mut WindowContext) + 'static,
) -> Self {
self.interactivity().drop_listeners.push((
TypeId::of::<W>(),
- Box::new(move |view, dragged_view, cx| {
- listener(view, dragged_view.downcast().unwrap(), cx);
+ Box::new(move |dragged_view, cx| {
+ listener(&dragged_view.downcast().unwrap(), cx);
}),
));
self
}
}
-pub trait StatefulInteractiveComponent<V: 'static, E: Element<V>>: InteractiveComponent<V> {
- fn focusable(mut self) -> Focusable<V, Self> {
+pub trait StatefulInteractiveElement: InteractiveElement {
+ fn focusable(mut self) -> Focusable<Self> {
self.interactivity().focusable = true;
- Focusable {
- element: self,
- view_type: PhantomData,
- }
+ Focusable { element: self }
}
fn overflow_scroll(mut self) -> Self {
@@ -362,23 +378,17 @@ pub trait StatefulInteractiveComponent<V: 'static, E: Element<V>>: InteractiveCo
self
}
- fn on_click(
- mut self,
- listener: impl Fn(&mut V, &ClickEvent, &mut ViewContext<V>) + 'static,
- ) -> Self
+ fn on_click(mut self, listener: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self
where
Self: Sized,
{
self.interactivity()
.click_listeners
- .push(Box::new(move |view, event, cx| listener(view, event, cx)));
+ .push(Box::new(move |event, cx| listener(event, cx)));
self
}
- fn on_drag<W>(
- mut self,
- listener: impl Fn(&mut V, &mut ViewContext<V>) -> View<W> + 'static,
- ) -> Self
+ fn on_drag<W>(mut self, listener: impl Fn(&mut WindowContext) -> View<W> + 'static) -> Self
where
Self: Sized,
W: 'static + Render,
@@ -387,15 +397,14 @@ pub trait StatefulInteractiveComponent<V: 'static, E: Element<V>>: InteractiveCo
self.interactivity().drag_listener.is_none(),
"calling on_drag more than once on the same element is not supported"
);
- self.interactivity().drag_listener =
- Some(Box::new(move |view_state, cursor_offset, cx| AnyDrag {
- view: listener(view_state, cx).into(),
- cursor_offset,
- }));
+ self.interactivity().drag_listener = Some(Box::new(move |cursor_offset, cx| AnyDrag {
+ view: listener(cx).into(),
+ cursor_offset,
+ }));
self
}
- fn on_hover(mut self, listener: impl 'static + Fn(&mut V, bool, &mut ViewContext<V>)) -> Self
+ fn on_hover(mut self, listener: impl Fn(&bool, &mut WindowContext) + 'static) -> Self
where
Self: Sized,
{
@@ -407,10 +416,7 @@ pub trait StatefulInteractiveComponent<V: 'static, E: Element<V>>: InteractiveCo
self
}
- fn tooltip(
- mut self,
- build_tooltip: impl Fn(&mut V, &mut ViewContext<V>) -> AnyView + 'static,
- ) -> Self
+ fn tooltip(mut self, build_tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self
where
Self: Sized,
{
@@ -418,14 +424,13 @@ pub trait StatefulInteractiveComponent<V: 'static, E: Element<V>>: InteractiveCo
self.interactivity().tooltip_builder.is_none(),
"calling tooltip more than once on the same element is not supported"
);
- self.interactivity().tooltip_builder =
- Some(Rc::new(move |view_state, cx| build_tooltip(view_state, cx)));
+ self.interactivity().tooltip_builder = Some(Rc::new(build_tooltip));
self
}
}
-pub trait FocusableComponent<V: 'static>: InteractiveComponent<V> {
+pub trait FocusableElement: InteractiveElement {
fn focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
where
Self: Sized,
@@ -442,49 +447,41 @@ pub trait FocusableComponent<V: 'static>: InteractiveComponent<V> {
self
}
- fn on_focus(
- mut self,
- listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext<V>) + 'static,
- ) -> Self
+ fn on_focus(mut self, listener: impl Fn(&FocusEvent, &mut WindowContext) + 'static) -> Self
where
Self: Sized,
{
- self.interactivity().focus_listeners.push(Box::new(
- move |view, focus_handle, event, cx| {
+ self.interactivity()
+ .focus_listeners
+ .push(Box::new(move |focus_handle, event, cx| {
if event.focused.as_ref() == Some(focus_handle) {
- listener(view, event, cx)
+ listener(event, cx)
}
- },
- ));
+ }));
self
}
- fn on_blur(
- mut self,
- listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext<V>) + 'static,
- ) -> Self
+ fn on_blur(mut self, listener: impl Fn(&FocusEvent, &mut WindowContext) + 'static) -> Self
where
Self: Sized,
{
- self.interactivity().focus_listeners.push(Box::new(
- move |view, focus_handle, event, cx| {
+ self.interactivity()
+ .focus_listeners
+ .push(Box::new(move |focus_handle, event, cx| {
if event.blurred.as_ref() == Some(focus_handle) {
- listener(view, event, cx)
+ listener(event, cx)
}
- },
- ));
+ }));
self
}
- fn on_focus_in(
- mut self,
- listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext<V>) + 'static,
- ) -> Self
+ fn on_focus_in(mut self, listener: impl Fn(&FocusEvent, &mut WindowContext) + 'static) -> Self
where
Self: Sized,
{
- self.interactivity().focus_listeners.push(Box::new(
- move |view, focus_handle, event, cx| {
+ self.interactivity()
+ .focus_listeners
+ .push(Box::new(move |focus_handle, event, cx| {
let descendant_blurred = event
.blurred
.as_ref()
@@ -495,22 +492,19 @@ pub trait FocusableComponent<V: 'static>: InteractiveComponent<V> {
.map_or(false, |focused| focus_handle.contains(focused, cx));
if !descendant_blurred && descendant_focused {
- listener(view, event, cx)
+ listener(event, cx)
}
- },
- ));
+ }));
self
}
- fn on_focus_out(
- mut self,
- listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext<V>) + 'static,
- ) -> Self
+ fn on_focus_out(mut self, listener: impl Fn(&FocusEvent, &mut WindowContext) + 'static) -> Self
where
Self: Sized,
{
- self.interactivity().focus_listeners.push(Box::new(
- move |view, focus_handle, event, cx| {
+ self.interactivity()
+ .focus_listeners
+ .push(Box::new(move |focus_handle, event, cx| {
let descendant_blurred = event
.blurred
.as_ref()
@@ -520,98 +514,80 @@ pub trait FocusableComponent<V: 'static>: InteractiveComponent<V> {
.as_ref()
.map_or(false, |focused| focus_handle.contains(focused, cx));
if descendant_blurred && !descendant_focused {
- listener(view, event, cx)
+ listener(event, cx)
}
- },
- ));
+ }));
self
}
}
-pub type FocusListeners<V> = SmallVec<[FocusListener<V>; 2]>;
-
-pub type FocusListener<V> =
- Box<dyn Fn(&mut V, &FocusHandle, &FocusEvent, &mut ViewContext<V>) + 'static>;
+pub type FocusListeners = SmallVec<[FocusListener; 2]>;
-pub type MouseDownListener<V> = Box<
- dyn Fn(&mut V, &MouseDownEvent, &Bounds<Pixels>, DispatchPhase, &mut ViewContext<V>) + 'static,
->;
-pub type MouseUpListener<V> = Box<
- dyn Fn(&mut V, &MouseUpEvent, &Bounds<Pixels>, DispatchPhase, &mut ViewContext<V>) + 'static,
->;
+pub type FocusListener = Box<dyn Fn(&FocusHandle, &FocusEvent, &mut WindowContext) + 'static>;
-pub type MouseMoveListener<V> = Box<
- dyn Fn(&mut V, &MouseMoveEvent, &Bounds<Pixels>, DispatchPhase, &mut ViewContext<V>) + 'static,
->;
+pub type MouseDownListener =
+ Box<dyn Fn(&MouseDownEvent, &Bounds<Pixels>, DispatchPhase, &mut WindowContext) + 'static>;
+pub type MouseUpListener =
+ Box<dyn Fn(&MouseUpEvent, &Bounds<Pixels>, DispatchPhase, &mut WindowContext) + 'static>;
-pub type ScrollWheelListener<V> = Box<
- dyn Fn(&mut V, &ScrollWheelEvent, &Bounds<Pixels>, DispatchPhase, &mut ViewContext<V>)
- + 'static,
->;
+pub type MouseMoveListener =
+ Box<dyn Fn(&MouseMoveEvent, &Bounds<Pixels>, DispatchPhase, &mut WindowContext) + 'static>;
-pub type ClickListener<V> = Box<dyn Fn(&mut V, &ClickEvent, &mut ViewContext<V>) + 'static>;
+pub type ScrollWheelListener =
+ Box<dyn Fn(&ScrollWheelEvent, &Bounds<Pixels>, DispatchPhase, &mut WindowContext) + 'static>;
-pub type DragListener<V> =
- Box<dyn Fn(&mut V, Point<Pixels>, &mut ViewContext<V>) -> AnyDrag + 'static>;
+pub type ClickListener = Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>;
-type DropListener<V> = dyn Fn(&mut V, AnyView, &mut ViewContext<V>) + 'static;
+pub type DragListener = Box<dyn Fn(Point<Pixels>, &mut WindowContext) -> AnyDrag + 'static>;
-pub type HoverListener<V> = Box<dyn Fn(&mut V, bool, &mut ViewContext<V>) + 'static>;
+type DropListener = dyn Fn(AnyView, &mut WindowContext) + 'static;
-pub type TooltipBuilder<V> = Rc<dyn Fn(&mut V, &mut ViewContext<V>) -> AnyView + 'static>;
+pub type TooltipBuilder = Rc<dyn Fn(&mut WindowContext) -> AnyView + 'static>;
-pub type KeyDownListener<V> =
- Box<dyn Fn(&mut V, &KeyDownEvent, DispatchPhase, &mut ViewContext<V>) + 'static>;
+pub type KeyDownListener = Box<dyn Fn(&KeyDownEvent, DispatchPhase, &mut WindowContext) + 'static>;
-pub type KeyUpListener<V> =
- Box<dyn Fn(&mut V, &KeyUpEvent, DispatchPhase, &mut ViewContext<V>) + 'static>;
+pub type KeyUpListener = Box<dyn Fn(&KeyUpEvent, DispatchPhase, &mut WindowContext) + 'static>;
-pub type ActionListener<V> =
- Box<dyn Fn(&mut V, &dyn Any, DispatchPhase, &mut ViewContext<V>) + 'static>;
+pub type ActionListener = Box<dyn Fn(&dyn Any, DispatchPhase, &mut WindowContext) + 'static>;
-pub fn div<V: 'static>() -> Div<V> {
+pub fn div() -> Div {
Div {
interactivity: Interactivity::default(),
children: SmallVec::default(),
}
}
-pub struct Div<V> {
- interactivity: Interactivity<V>,
- children: SmallVec<[AnyElement<V>; 2]>,
+pub struct Div {
+ interactivity: Interactivity,
+ children: SmallVec<[AnyElement; 2]>,
}
-impl<V> Styled for Div<V> {
+impl Styled for Div {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.interactivity.base_style
}
}
-impl<V: 'static> InteractiveComponent<V> for Div<V> {
- fn interactivity(&mut self) -> &mut Interactivity<V> {
+impl InteractiveElement for Div {
+ fn interactivity(&mut self) -> &mut Interactivity {
&mut self.interactivity
}
}
-impl<V: 'static> ParentComponent<V> for Div<V> {
- fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
+impl ParentElement for Div {
+ fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
&mut self.children
}
}
-impl<V: 'static> Element<V> for Div<V> {
- type ElementState = DivState;
-
- fn element_id(&self) -> Option<ElementId> {
- self.interactivity.element_id.clone()
- }
+impl Element for Div {
+ type State = DivState;
fn layout(
&mut self,
- view_state: &mut V,
- element_state: Option<Self::ElementState>,
- cx: &mut ViewContext<V>,
- ) -> (LayoutId, Self::ElementState) {
+ element_state: Option<Self::State>,
+ cx: &mut WindowContext,
+ ) -> (LayoutId, Self::State) {
let mut child_layout_ids = SmallVec::new();
let mut interactivity = mem::take(&mut self.interactivity);
let (layout_id, interactive_state) = interactivity.layout(
@@ -622,7 +598,7 @@ impl<V: 'static> Element<V> for Div<V> {
child_layout_ids = self
.children
.iter_mut()
- .map(|child| child.layout(view_state, cx))
+ .map(|child| child.layout(cx))
.collect::<SmallVec<_>>();
cx.request_layout(&style, child_layout_ids.iter().copied())
})
@@ -639,11 +615,10 @@ impl<V: 'static> Element<V> for Div<V> {
}
fn paint(
- &mut self,
+ self,
bounds: Bounds<Pixels>,
- view_state: &mut V,
- element_state: &mut Self::ElementState,
- cx: &mut ViewContext<V>,
+ element_state: &mut Self::State,
+ cx: &mut WindowContext,
) {
let mut child_min = point(Pixels::MAX, Pixels::MAX);
let mut child_max = Point::default();
@@ -658,8 +633,7 @@ impl<V: 'static> Element<V> for Div<V> {
(child_max - child_min).into()
};
- let mut interactivity = mem::take(&mut self.interactivity);
- interactivity.paint(
+ self.interactivity.paint(
bounds,
content_size,
&mut element_state.interactive_state,
@@ -679,8 +653,8 @@ impl<V: 'static> Element<V> for Div<V> {
cx.with_text_style(style.text_style().cloned(), |cx| {
cx.with_content_mask(style.overflow_mask(bounds), |cx| {
cx.with_element_offset(scroll_offset, |cx| {
- for child in &mut self.children {
- child.paint(view_state, cx);
+ for child in self.children {
+ child.paint(cx);
}
})
})
@@ -689,13 +663,18 @@ impl<V: 'static> Element<V> for Div<V> {
})
},
);
- self.interactivity = interactivity;
}
}
-impl<V: 'static> Component<V> for Div<V> {
- fn render(self) -> AnyElement<V> {
- AnyElement::new(self)
+impl IntoElement for Div {
+ type Element = Self;
+
+ fn element_id(&self) -> Option<ElementId> {
+ self.interactivity.element_id.clone()
+ }
+
+ fn into_element(self) -> Self::Element {
+ self
}
}
@@ -710,12 +689,12 @@ impl DivState {
}
}
-pub struct Interactivity<V> {
+pub struct Interactivity {
pub element_id: Option<ElementId>,
pub key_context: KeyContext,
pub focusable: bool,
pub tracked_focus_handle: Option<FocusHandle>,
- pub focus_listeners: FocusListeners<V>,
+ pub focus_listeners: FocusListeners,
pub group: Option<SharedString>,
pub base_style: StyleRefinement,
pub focus_style: StyleRefinement,
@@ -726,29 +705,26 @@ pub struct Interactivity<V> {
pub group_active_style: Option<GroupStyle>,
pub drag_over_styles: SmallVec<[(TypeId, StyleRefinement); 2]>,
pub group_drag_over_styles: SmallVec<[(TypeId, GroupStyle); 2]>,
- pub mouse_down_listeners: SmallVec<[MouseDownListener<V>; 2]>,
- pub mouse_up_listeners: SmallVec<[MouseUpListener<V>; 2]>,
- pub mouse_move_listeners: SmallVec<[MouseMoveListener<V>; 2]>,
- pub scroll_wheel_listeners: SmallVec<[ScrollWheelListener<V>; 2]>,
- pub key_down_listeners: SmallVec<[KeyDownListener<V>; 2]>,
- pub key_up_listeners: SmallVec<[KeyUpListener<V>; 2]>,
- pub action_listeners: SmallVec<[(TypeId, ActionListener<V>); 8]>,
- pub drop_listeners: SmallVec<[(TypeId, Box<DropListener<V>>); 2]>,
- pub click_listeners: SmallVec<[ClickListener<V>; 2]>,
- pub drag_listener: Option<DragListener<V>>,
- pub hover_listener: Option<HoverListener<V>>,
- pub tooltip_builder: Option<TooltipBuilder<V>>,
+ pub mouse_down_listeners: SmallVec<[MouseDownListener; 2]>,
+ pub mouse_up_listeners: SmallVec<[MouseUpListener; 2]>,
+ pub mouse_move_listeners: SmallVec<[MouseMoveListener; 2]>,
+ pub scroll_wheel_listeners: SmallVec<[ScrollWheelListener; 2]>,
+ pub key_down_listeners: SmallVec<[KeyDownListener; 2]>,
+ pub key_up_listeners: SmallVec<[KeyUpListener; 2]>,
+ pub action_listeners: SmallVec<[(TypeId, ActionListener); 8]>,
+ pub drop_listeners: SmallVec<[(TypeId, Box<DropListener>); 2]>,
+ pub click_listeners: SmallVec<[ClickListener; 2]>,
+ pub drag_listener: Option<DragListener>,
+ pub hover_listener: Option<Box<dyn Fn(&bool, &mut WindowContext)>>,
+ pub tooltip_builder: Option<TooltipBuilder>,
}
-impl<V> Interactivity<V>
-where
- V: 'static,
-{
+impl Interactivity {
pub fn layout(
&mut self,
element_state: Option<InteractiveElementState>,
- cx: &mut ViewContext<V>,
- f: impl FnOnce(Style, &mut ViewContext<V>) -> LayoutId,
+ cx: &mut WindowContext,
+ f: impl FnOnce(Style, &mut WindowContext) -> LayoutId,
) -> (LayoutId, InteractiveElementState) {
let mut element_state = element_state.unwrap_or_default();
@@ -770,12 +746,12 @@ where
}
pub fn paint(
- &mut self,
+ mut self,
bounds: Bounds<Pixels>,
content_size: Size<Pixels>,
element_state: &mut InteractiveElementState,
- cx: &mut ViewContext<V>,
- f: impl FnOnce(Style, Point<Pixels>, &mut ViewContext<V>),
+ cx: &mut WindowContext,
+ f: impl FnOnce(Style, Point<Pixels>, &mut WindowContext),
) {
let style = self.compute_style(Some(bounds), element_state, cx);
@@ -787,26 +763,26 @@ where
}
for listener in self.mouse_down_listeners.drain(..) {
- cx.on_mouse_event(move |state, event: &MouseDownEvent, phase, cx| {
- listener(state, event, &bounds, phase, cx);
+ cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| {
+ listener(event, &bounds, phase, cx);
})
}
for listener in self.mouse_up_listeners.drain(..) {
- cx.on_mouse_event(move |state, event: &MouseUpEvent, phase, cx| {
- listener(state, event, &bounds, phase, cx);
+ cx.on_mouse_event(move |event: &MouseUpEvent, phase, cx| {
+ listener(event, &bounds, phase, cx);
})
}
for listener in self.mouse_move_listeners.drain(..) {
- cx.on_mouse_event(move |state, event: &MouseMoveEvent, phase, cx| {
- listener(state, event, &bounds, phase, cx);
+ cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| {
+ listener(event, &bounds, phase, cx);
})
}
for listener in self.scroll_wheel_listeners.drain(..) {
- cx.on_mouse_event(move |state, event: &ScrollWheelEvent, phase, cx| {
- listener(state, event, &bounds, phase, cx);
+ cx.on_mouse_event(move |event: &ScrollWheelEvent, phase, cx| {
+ listener(event, &bounds, phase, cx);
})
}
@@ -817,7 +793,7 @@ where
if let Some(group_bounds) = hover_group_bounds {
let hovered = group_bounds.contains_point(&cx.mouse_position());
- cx.on_mouse_event(move |_, event: &MouseMoveEvent, phase, cx| {
+ cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| {
if phase == DispatchPhase::Capture {
if group_bounds.contains_point(&event.position) != hovered {
cx.notify();
@@ -830,7 +806,7 @@ where
|| (cx.active_drag.is_some() && !self.drag_over_styles.is_empty())
{
let hovered = bounds.contains_point(&cx.mouse_position());
- cx.on_mouse_event(move |_, event: &MouseMoveEvent, phase, cx| {
+ cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| {
if phase == DispatchPhase::Capture {
if bounds.contains_point(&event.position) != hovered {
cx.notify();
@@ -841,7 +817,7 @@ where
if cx.active_drag.is_some() {
let drop_listeners = mem::take(&mut self.drop_listeners);
- cx.on_mouse_event(move |view, event: &MouseUpEvent, phase, cx| {
+ cx.on_mouse_event(move |event: &MouseUpEvent, phase, cx| {
if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
if let Some(drag_state_type) =
cx.active_drag.as_ref().map(|drag| drag.view.entity_type())
@@ -852,7 +828,7 @@ where
.active_drag
.take()
.expect("checked for type drag state type above");
- listener(view, drag.view.clone(), cx);
+ listener(drag.view.clone(), cx);
cx.notify();
cx.stop_propagation();
}
@@ -872,7 +848,7 @@ where
if let Some(drag_listener) = drag_listener {
let active_state = element_state.clicked_state.clone();
- cx.on_mouse_event(move |view_state, event: &MouseMoveEvent, phase, cx| {
+ cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| {
if cx.active_drag.is_some() {
if phase == DispatchPhase::Capture {
cx.notify();
@@ -883,7 +859,7 @@ where
{
*active_state.borrow_mut() = ElementClickedState::default();
let cursor_offset = event.position - bounds.origin;
- let drag = drag_listener(view_state, cursor_offset, cx);
+ let drag = drag_listener(cursor_offset, cx);
cx.active_drag = Some(drag);
cx.notify();
cx.stop_propagation();
@@ -891,21 +867,21 @@ where
});
}
- cx.on_mouse_event(move |view_state, event: &MouseUpEvent, phase, cx| {
+ cx.on_mouse_event(move |event: &MouseUpEvent, phase, cx| {
if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
let mouse_click = ClickEvent {
down: mouse_down.clone(),
up: event.clone(),
};
for listener in &click_listeners {
- listener(view_state, &mouse_click, cx);
+ listener(&mouse_click, cx);
}
}
*pending_mouse_down.borrow_mut() = None;
cx.notify();
});
} else {
- cx.on_mouse_event(move |_view_state, event: &MouseDownEvent, phase, cx| {
+ cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| {
if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
*pending_mouse_down.borrow_mut() = Some(event.clone());
cx.notify();
@@ -918,7 +894,7 @@ where
let was_hovered = element_state.hover_state.clone();
let has_mouse_down = element_state.pending_mouse_down.clone();
- cx.on_mouse_event(move |view_state, event: &MouseMoveEvent, phase, cx| {
+ cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| {
if phase != DispatchPhase::Bubble {
return;
}
@@ -930,7 +906,7 @@ where
*was_hovered = is_hovered;
drop(was_hovered);
- hover_listener(view_state, is_hovered, cx);
+ hover_listener(&is_hovered, cx);
}
});
}
@@ -939,7 +915,7 @@ where
let active_tooltip = element_state.active_tooltip.clone();
let pending_mouse_down = element_state.pending_mouse_down.clone();
- cx.on_mouse_event(move |_, event: &MouseMoveEvent, phase, cx| {
+ cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| {
if phase != DispatchPhase::Bubble {
return;
}
@@ -956,12 +932,12 @@ where
let active_tooltip = active_tooltip.clone();
let tooltip_builder = tooltip_builder.clone();
- move |view, mut cx| async move {
+ move |mut cx| async move {
cx.background_executor().timer(TOOLTIP_DELAY).await;
- view.update(&mut cx, move |view_state, cx| {
+ cx.update(|_, cx| {
active_tooltip.borrow_mut().replace(ActiveTooltip {
tooltip: Some(AnyTooltip {
- view: tooltip_builder(view_state, cx),
+ view: tooltip_builder(cx),
cursor_offset: cx.mouse_position(),
}),
_task: None,
@@ -979,7 +955,7 @@ where
});
let active_tooltip = element_state.active_tooltip.clone();
- cx.on_mouse_event(move |_, _: &MouseDownEvent, _, _| {
+ cx.on_mouse_event(move |_: &MouseDownEvent, _, _| {
active_tooltip.borrow_mut().take();
});
@@ -992,7 +968,7 @@ where
let active_state = element_state.clicked_state.clone();
if !active_state.borrow().is_clicked() {
- cx.on_mouse_event(move |_, _: &MouseUpEvent, phase, cx| {
+ cx.on_mouse_event(move |_: &MouseUpEvent, phase, cx| {
if phase == DispatchPhase::Capture {
*active_state.borrow_mut() = ElementClickedState::default();
cx.notify();
@@ -1003,7 +979,7 @@ where
.group_active_style
.as_ref()
.and_then(|group_active| GroupBounds::get(&group_active.group, cx));
- cx.on_mouse_event(move |_view, down: &MouseDownEvent, phase, cx| {
+ cx.on_mouse_event(move |down: &MouseDownEvent, phase, cx| {
if phase == DispatchPhase::Bubble {
let group = active_group_bounds
.map_or(false, |bounds| bounds.contains_point(&down.position));
@@ -1025,7 +1001,7 @@ where
let line_height = cx.line_height();
let scroll_max = (content_size - bounds.size).max(&Size::default());
- cx.on_mouse_event(move |_, event: &ScrollWheelEvent, phase, cx| {
+ cx.on_mouse_event(move |event: &ScrollWheelEvent, phase, cx| {
if phase == DispatchPhase::Bubble && bounds.contains_point(&event.position) {
let mut scroll_offset = scroll_offset.borrow_mut();
let old_scroll_offset = *scroll_offset;
@@ -1063,27 +1039,25 @@ where
element_state.focus_handle.clone(),
|_, cx| {
for listener in self.key_down_listeners.drain(..) {
- cx.on_key_event(move |state, event: &KeyDownEvent, phase, cx| {
- listener(state, event, phase, cx);
+ cx.on_key_event(move |event: &KeyDownEvent, phase, cx| {
+ listener(event, phase, cx);
})
}
for listener in self.key_up_listeners.drain(..) {
- cx.on_key_event(move |state, event: &KeyUpEvent, phase, cx| {
- listener(state, event, phase, cx);
+ cx.on_key_event(move |event: &KeyUpEvent, phase, cx| {
+ listener(event, phase, cx);
})
}
- for (action_type, listener) in self.action_listeners.drain(..) {
+ for (action_type, listener) in self.action_listeners {
cx.on_action(action_type, listener)
}
if let Some(focus_handle) = element_state.focus_handle.as_ref() {
- for listener in self.focus_listeners.drain(..) {
+ for listener in self.focus_listeners {
let focus_handle = focus_handle.clone();
- cx.on_focus_changed(move |view, event, cx| {
- listener(view, &focus_handle, event, cx)
- });
+ cx.on_focus_changed(move |event, cx| listener(&focus_handle, event, cx));
}
}
@@ -1,70 +1,83 @@
+use std::sync::Arc;
+
use crate::{
- AnyElement, BorrowWindow, Bounds, Component, Element, InteractiveComponent,
- InteractiveElementState, Interactivity, LayoutId, Pixels, SharedString, StyleRefinement,
- Styled, ViewContext,
+ Bounds, Element, ImageData, InteractiveElement, InteractiveElementState, Interactivity,
+ IntoElement, LayoutId, Pixels, SharedString, StyleRefinement, Styled, WindowContext,
};
use futures::FutureExt;
use util::ResultExt;
-pub struct Img<V: 'static> {
- interactivity: Interactivity<V>,
- uri: Option<SharedString>,
+#[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,
+ source: Option<ImageSource>,
grayscale: bool,
}
-pub fn img<V: 'static>() -> Img<V> {
+pub fn img() -> Img {
Img {
interactivity: Interactivity::default(),
- uri: None,
+ source: None,
grayscale: false,
}
}
-impl<V> Img<V>
-where
- V: 'static,
-{
+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
}
}
-impl<V> Component<V> for Img<V> {
- fn render(self) -> AnyElement<V> {
- AnyElement::new(self)
- }
-}
-
-impl<V> Element<V> for Img<V> {
- type ElementState = InteractiveElementState;
-
- fn element_id(&self) -> Option<crate::ElementId> {
- self.interactivity.element_id.clone()
- }
+impl Element for Img {
+ type State = InteractiveElementState;
fn layout(
&mut self,
- _view_state: &mut V,
- element_state: Option<Self::ElementState>,
- cx: &mut ViewContext<V>,
- ) -> (LayoutId, Self::ElementState) {
+ element_state: Option<Self::State>,
+ cx: &mut WindowContext,
+ ) -> (LayoutId, Self::State) {
self.interactivity.layout(element_state, cx, |style, cx| {
cx.request_layout(&style, None)
})
}
fn paint(
- &mut self,
+ self,
bounds: Bounds<Pixels>,
- _view_state: &mut V,
- element_state: &mut Self::ElementState,
- cx: &mut ViewContext<V>,
+ element_state: &mut Self::State,
+ cx: &mut WindowContext,
) {
self.interactivity.paint(
bounds,
@@ -74,42 +87,59 @@ impl<V> Element<V> for Img<V> {
|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<V> Styled for Img<V> {
+impl IntoElement for Img {
+ type Element = Self;
+
+ fn element_id(&self) -> Option<crate::ElementId> {
+ self.interactivity.element_id.clone()
+ }
+
+ fn into_element(self) -> Self::Element {
+ self
+ }
+}
+
+impl Styled for Img {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.interactivity.base_style
}
}
-impl<V> InteractiveComponent<V> for Img<V> {
- fn interactivity(&mut self) -> &mut Interactivity<V> {
+impl InteractiveElement for Img {
+ fn interactivity(&mut self) -> &mut Interactivity {
&mut self.interactivity
}
}
@@ -2,16 +2,16 @@ use smallvec::SmallVec;
use taffy::style::{Display, Position};
use crate::{
- point, AnyElement, BorrowWindow, Bounds, Component, Element, LayoutId, ParentComponent, Pixels,
- Point, Size, Style,
+ point, AnyElement, BorrowWindow, Bounds, Element, IntoElement, LayoutId, ParentElement, Pixels,
+ Point, Size, Style, WindowContext,
};
pub struct OverlayState {
child_layout_ids: SmallVec<[LayoutId; 4]>,
}
-pub struct Overlay<V> {
- children: SmallVec<[AnyElement<V>; 2]>,
+pub struct Overlay {
+ children: SmallVec<[AnyElement; 2]>,
anchor_corner: AnchorCorner,
fit_mode: OverlayFitMode,
// todo!();
@@ -21,7 +21,7 @@ pub struct Overlay<V> {
/// overlay gives you a floating element that will avoid overflowing the window bounds.
/// Its children should have no margin to avoid measurement issues.
-pub fn overlay<V: 'static>() -> Overlay<V> {
+pub fn overlay() -> Overlay {
Overlay {
children: SmallVec::new(),
anchor_corner: AnchorCorner::TopLeft,
@@ -30,7 +30,7 @@ pub fn overlay<V: 'static>() -> Overlay<V> {
}
}
-impl<V> Overlay<V> {
+impl Overlay {
/// Sets which corner of the overlay should be anchored to the current position.
pub fn anchor(mut self, anchor: AnchorCorner) -> Self {
self.anchor_corner = anchor;
@@ -51,35 +51,24 @@ impl<V> Overlay<V> {
}
}
-impl<V: 'static> ParentComponent<V> for Overlay<V> {
- fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
+impl ParentElement for Overlay {
+ fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
&mut self.children
}
}
-impl<V: 'static> Component<V> for Overlay<V> {
- fn render(self) -> AnyElement<V> {
- AnyElement::new(self)
- }
-}
-
-impl<V: 'static> Element<V> for Overlay<V> {
- type ElementState = OverlayState;
-
- fn element_id(&self) -> Option<crate::ElementId> {
- None
- }
+impl Element for Overlay {
+ type State = OverlayState;
fn layout(
&mut self,
- view_state: &mut V,
- _: Option<Self::ElementState>,
- cx: &mut crate::ViewContext<V>,
- ) -> (crate::LayoutId, Self::ElementState) {
+ _: Option<Self::State>,
+ cx: &mut WindowContext,
+ ) -> (crate::LayoutId, Self::State) {
let child_layout_ids = self
.children
.iter_mut()
- .map(|child| child.layout(view_state, cx))
+ .map(|child| child.layout(cx))
.collect::<SmallVec<_>>();
let mut overlay_style = Style::default();
@@ -92,11 +81,10 @@ impl<V: 'static> Element<V> for Overlay<V> {
}
fn paint(
- &mut self,
+ self,
bounds: crate::Bounds<crate::Pixels>,
- view_state: &mut V,
- element_state: &mut Self::ElementState,
- cx: &mut crate::ViewContext<V>,
+ element_state: &mut Self::State,
+ cx: &mut WindowContext,
) {
if element_state.child_layout_ids.is_empty() {
return;
@@ -117,6 +105,7 @@ impl<V: 'static> Element<V> for Overlay<V> {
origin: Point::zero(),
size: cx.viewport_size(),
};
+ dbg!(limits);
match self.fit_mode {
OverlayFitMode::SnapToWindow => {
@@ -156,13 +145,27 @@ impl<V: 'static> Element<V> for Overlay<V> {
}
cx.with_element_offset(desired.origin - bounds.origin, |cx| {
- for child in &mut self.children {
- child.paint(view_state, cx);
- }
+ cx.break_content_mask(|cx| {
+ for child in self.children {
+ child.paint(cx);
+ }
+ })
})
}
}
+impl IntoElement for Overlay {
+ type Element = Self;
+
+ fn element_id(&self) -> Option<crate::ElementId> {
+ None
+ }
+
+ fn into_element(self) -> Self::Element {
+ self
+ }
+}
+
enum Axis {
Horizontal,
Vertical,
@@ -1,60 +1,43 @@
use crate::{
- AnyElement, Bounds, Component, Element, ElementId, InteractiveComponent,
- InteractiveElementState, Interactivity, LayoutId, Pixels, SharedString, StyleRefinement,
- Styled, ViewContext,
+ Bounds, Element, ElementId, InteractiveElement, InteractiveElementState, Interactivity,
+ IntoElement, LayoutId, Pixels, SharedString, StyleRefinement, Styled, WindowContext,
};
use util::ResultExt;
-pub struct Svg<V: 'static> {
- interactivity: Interactivity<V>,
+pub struct Svg {
+ interactivity: Interactivity,
path: Option<SharedString>,
}
-pub fn svg<V: 'static>() -> Svg<V> {
+pub fn svg() -> Svg {
Svg {
interactivity: Interactivity::default(),
path: None,
}
}
-impl<V> Svg<V> {
+impl Svg {
pub fn path(mut self, path: impl Into<SharedString>) -> Self {
self.path = Some(path.into());
self
}
}
-impl<V> Component<V> for Svg<V> {
- fn render(self) -> AnyElement<V> {
- AnyElement::new(self)
- }
-}
-
-impl<V> Element<V> for Svg<V> {
- type ElementState = InteractiveElementState;
-
- fn element_id(&self) -> Option<ElementId> {
- self.interactivity.element_id.clone()
- }
+impl Element for Svg {
+ type State = InteractiveElementState;
fn layout(
&mut self,
- _view_state: &mut V,
- element_state: Option<Self::ElementState>,
- cx: &mut ViewContext<V>,
- ) -> (LayoutId, Self::ElementState) {
+ element_state: Option<Self::State>,
+ cx: &mut WindowContext,
+ ) -> (LayoutId, Self::State) {
self.interactivity.layout(element_state, cx, |style, cx| {
cx.request_layout(&style, None)
})
}
- fn paint(
- &mut self,
- bounds: Bounds<Pixels>,
- _view_state: &mut V,
- element_state: &mut Self::ElementState,
- cx: &mut ViewContext<V>,
- ) where
+ fn paint(self, bounds: Bounds<Pixels>, element_state: &mut Self::State, cx: &mut WindowContext)
+ where
Self: Sized,
{
self.interactivity
@@ -66,14 +49,26 @@ impl<V> Element<V> for Svg<V> {
}
}
-impl<V> Styled for Svg<V> {
+impl IntoElement for Svg {
+ type Element = Self;
+
+ fn element_id(&self) -> Option<ElementId> {
+ self.interactivity.element_id.clone()
+ }
+
+ fn into_element(self) -> Self::Element {
+ self
+ }
+}
+
+impl Styled for Svg {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.interactivity.base_style
}
}
-impl<V> InteractiveComponent<V> for Svg<V> {
- fn interactivity(&mut self) -> &mut Interactivity<V> {
+impl InteractiveElement for Svg {
+ fn interactivity(&mut self) -> &mut Interactivity {
&mut self.interactivity
}
}
@@ -1,74 +1,177 @@
use crate::{
- AnyElement, BorrowWindow, Bounds, Component, Element, ElementId, LayoutId, Pixels,
- SharedString, Size, TextRun, ViewContext, WrappedLine,
+ Bounds, DispatchPhase, Element, ElementId, IntoElement, LayoutId, MouseDownEvent, MouseUpEvent,
+ Pixels, Point, SharedString, Size, TextRun, WhiteSpace, WindowContext, WrappedLine,
};
+use anyhow::anyhow;
use parking_lot::{Mutex, MutexGuard};
use smallvec::SmallVec;
-use std::{cell::Cell, rc::Rc, sync::Arc};
+use std::{cell::Cell, ops::Range, rc::Rc, sync::Arc};
use util::ResultExt;
-pub struct Text {
+impl Element for &'static str {
+ type State = TextState;
+
+ fn layout(
+ &mut self,
+ _: Option<Self::State>,
+ cx: &mut WindowContext,
+ ) -> (LayoutId, Self::State) {
+ let mut state = TextState::default();
+ let layout_id = state.layout(SharedString::from(*self), None, cx);
+ (layout_id, state)
+ }
+
+ fn paint(self, bounds: Bounds<Pixels>, state: &mut TextState, cx: &mut WindowContext) {
+ state.paint(bounds, self, cx)
+ }
+}
+
+impl IntoElement for &'static str {
+ type Element = Self;
+
+ fn element_id(&self) -> Option<ElementId> {
+ None
+ }
+
+ fn into_element(self) -> Self::Element {
+ self
+ }
+}
+
+impl Element for SharedString {
+ type State = TextState;
+
+ fn layout(
+ &mut self,
+ _: Option<Self::State>,
+ cx: &mut WindowContext,
+ ) -> (LayoutId, Self::State) {
+ let mut state = TextState::default();
+ let layout_id = state.layout(self.clone(), None, cx);
+ (layout_id, state)
+ }
+
+ fn paint(self, bounds: Bounds<Pixels>, state: &mut TextState, cx: &mut WindowContext) {
+ let text_str: &str = self.as_ref();
+ state.paint(bounds, text_str, cx)
+ }
+}
+
+impl IntoElement for SharedString {
+ type Element = Self;
+
+ fn element_id(&self) -> Option<ElementId> {
+ None
+ }
+
+ fn into_element(self) -> Self::Element {
+ self
+ }
+}
+
+/// Renders text with runs of different styles.
+///
+/// Callers are responsible for setting the correct style for each run.
+/// For text with a uniform style, you can usually avoid calling this constructor
+/// and just pass text directly.
+pub struct StyledText {
text: SharedString,
runs: Option<Vec<TextRun>>,
}
-impl Text {
- /// Renders text with runs of different styles.
- ///
- /// Callers are responsible for setting the correct style for each run.
- /// For text with a uniform style, you can usually avoid calling this constructor
- /// and just pass text directly.
- pub fn styled(text: SharedString, runs: Vec<TextRun>) -> Self {
- Text {
- text,
- runs: Some(runs),
+impl StyledText {
+ pub fn new(text: impl Into<SharedString>) -> Self {
+ StyledText {
+ text: text.into(),
+ runs: None,
}
}
+
+ pub fn with_runs(mut self, runs: Vec<TextRun>) -> Self {
+ self.runs = Some(runs);
+ self
+ }
}
-impl<V: 'static> Component<V> for Text {
- fn render(self) -> AnyElement<V> {
- AnyElement::new(self)
+impl Element for StyledText {
+ type State = TextState;
+
+ fn layout(
+ &mut self,
+ _: Option<Self::State>,
+ cx: &mut WindowContext,
+ ) -> (LayoutId, Self::State) {
+ let mut state = TextState::default();
+ let layout_id = state.layout(self.text.clone(), self.runs.take(), cx);
+ (layout_id, state)
+ }
+
+ fn paint(self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
+ state.paint(bounds, &self.text, cx)
}
}
-impl<V: 'static> Element<V> for Text {
- type ElementState = TextState;
+impl IntoElement for StyledText {
+ type Element = Self;
fn element_id(&self) -> Option<crate::ElementId> {
None
}
+ fn into_element(self) -> Self::Element {
+ self
+ }
+}
+
+#[derive(Default, Clone)]
+pub struct TextState(Arc<Mutex<Option<TextStateInner>>>);
+
+struct TextStateInner {
+ lines: SmallVec<[WrappedLine; 1]>,
+ line_height: Pixels,
+ wrap_width: Option<Pixels>,
+ size: Option<Size<Pixels>>,
+}
+
+impl TextState {
+ fn lock(&self) -> MutexGuard<Option<TextStateInner>> {
+ self.0.lock()
+ }
+
fn layout(
&mut self,
- _view: &mut V,
- element_state: Option<Self::ElementState>,
- cx: &mut ViewContext<V>,
- ) -> (LayoutId, Self::ElementState) {
- let element_state = element_state.unwrap_or_default();
+ text: SharedString,
+ runs: Option<Vec<TextRun>>,
+ cx: &mut WindowContext,
+ ) -> LayoutId {
let text_system = cx.text_system().clone();
let text_style = cx.text_style();
let font_size = text_style.font_size.to_pixels(cx.rem_size());
let line_height = text_style
.line_height
.to_pixels(font_size.into(), cx.rem_size());
- let text = self.text.clone();
+ let text = SharedString::from(text);
let rem_size = cx.rem_size();
- let runs = if let Some(runs) = self.runs.take() {
+ let runs = if let Some(runs) = runs {
runs
} else {
vec![text_style.to_run(text.len())]
};
let layout_id = cx.request_measured_layout(Default::default(), rem_size, {
- let element_state = element_state.clone();
+ let element_state = self.clone();
+
move |known_dimensions, available_space| {
- let wrap_width = known_dimensions.width.or(match available_space.width {
- crate::AvailableSpace::Definite(x) => Some(x),
- _ => None,
- });
+ let wrap_width = if text_style.white_space == WhiteSpace::Normal {
+ known_dimensions.width.or(match available_space.width {
+ crate::AvailableSpace::Definite(x) => Some(x),
+ _ => None,
+ })
+ } else {
+ None
+ };
if let Some(text_state) = element_state.0.lock().as_ref() {
if text_state.size.is_some()
@@ -80,10 +183,7 @@ impl<V: 'static> Element<V> for Text {
let Some(lines) = text_system
.shape_text(
- &text,
- font_size,
- &runs[..],
- wrap_width, // Wrap if we know the width.
+ &text, font_size, &runs, wrap_width, // Wrap if we know the width.
)
.log_err()
else {
@@ -100,7 +200,7 @@ impl<V: 'static> Element<V> for Text {
for line in &lines {
let line_size = line.size(line_height);
size.height += line_size.height;
- size.width = size.width.max(line_size.width);
+ size.width = size.width.max(line_size.width).ceil();
}
element_state.lock().replace(TextStateInner {
@@ -114,20 +214,14 @@ impl<V: 'static> Element<V> for Text {
}
});
- (layout_id, element_state)
+ layout_id
}
- fn paint(
- &mut self,
- bounds: Bounds<Pixels>,
- _: &mut V,
- element_state: &mut Self::ElementState,
- cx: &mut ViewContext<V>,
- ) {
- let element_state = element_state.lock();
+ fn paint(&mut self, bounds: Bounds<Pixels>, text: &str, cx: &mut WindowContext) {
+ let element_state = self.lock();
let element_state = element_state
.as_ref()
- .ok_or_else(|| anyhow::anyhow!("measurement has not been performed on {}", &self.text))
+ .ok_or_else(|| anyhow!("measurement has not been performed on {}", text))
.unwrap();
let line_height = element_state.line_height;
@@ -137,108 +231,156 @@ impl<V: 'static> Element<V> for Text {
line_origin.y += line.size(line_height).height;
}
}
-}
-#[derive(Default, Clone)]
-pub struct TextState(Arc<Mutex<Option<TextStateInner>>>);
+ fn index_for_position(&self, bounds: Bounds<Pixels>, position: Point<Pixels>) -> Option<usize> {
+ if !bounds.contains_point(&position) {
+ return None;
+ }
-impl TextState {
- fn lock(&self) -> MutexGuard<Option<TextStateInner>> {
- self.0.lock()
+ let element_state = self.lock();
+ let element_state = element_state
+ .as_ref()
+ .expect("measurement has not been performed");
+
+ let line_height = element_state.line_height;
+ let mut line_origin = bounds.origin;
+ let mut line_start_ix = 0;
+ for line in &element_state.lines {
+ let line_bottom = line_origin.y + line.size(line_height).height;
+ if position.y > line_bottom {
+ line_origin.y = line_bottom;
+ line_start_ix += line.len() + 1;
+ } else {
+ let position_within_line = position - line_origin;
+ let index_within_line =
+ line.index_for_position(position_within_line, line_height)?;
+ return Some(line_start_ix + index_within_line);
+ }
+ }
+
+ None
}
}
-struct TextStateInner {
- lines: SmallVec<[WrappedLine; 1]>,
- line_height: Pixels,
- wrap_width: Option<Pixels>,
- size: Option<Size<Pixels>>,
+pub struct InteractiveText {
+ element_id: ElementId,
+ text: StyledText,
+ click_listener: Option<Box<dyn Fn(InteractiveTextClickEvent, &mut WindowContext<'_>)>>,
}
-struct InteractiveText {
- id: ElementId,
- text: Text,
+struct InteractiveTextClickEvent {
+ mouse_down_index: usize,
+ mouse_up_index: usize,
}
-struct InteractiveTextState {
+pub struct InteractiveTextState {
text_state: TextState,
- clicked_range_ixs: Rc<Cell<SmallVec<[usize; 1]>>>,
+ mouse_down_index: Rc<Cell<Option<usize>>>,
}
-impl<V: 'static> Element<V> for InteractiveText {
- type ElementState = InteractiveTextState;
+impl InteractiveText {
+ pub fn new(id: impl Into<ElementId>, text: StyledText) -> Self {
+ Self {
+ element_id: id.into(),
+ text,
+ click_listener: None,
+ }
+ }
- fn element_id(&self) -> Option<ElementId> {
- Some(self.id.clone())
+ 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 {
+ type State = InteractiveTextState;
fn layout(
&mut self,
- view_state: &mut V,
- element_state: Option<Self::ElementState>,
- cx: &mut ViewContext<V>,
- ) -> (LayoutId, Self::ElementState) {
+ state: Option<Self::State>,
+ cx: &mut WindowContext,
+ ) -> (LayoutId, Self::State) {
if let Some(InteractiveTextState {
- text_state,
- clicked_range_ixs,
- }) = element_state
+ mouse_down_index, ..
+ }) = state
{
- let (layout_id, text_state) = self.text.layout(view_state, Some(text_state), cx);
+ let (layout_id, text_state) = self.text.layout(None, cx);
let element_state = InteractiveTextState {
text_state,
- clicked_range_ixs,
+ mouse_down_index,
};
(layout_id, element_state)
} else {
- let (layout_id, text_state) = self.text.layout(view_state, None, cx);
+ let (layout_id, text_state) = self.text.layout(None, cx);
let element_state = InteractiveTextState {
text_state,
- clicked_range_ixs: Rc::default(),
+ mouse_down_index: Rc::default(),
};
(layout_id, element_state)
}
}
- fn paint(
- &mut self,
- bounds: Bounds<Pixels>,
- view_state: &mut V,
- element_state: &mut Self::ElementState,
- cx: &mut ViewContext<V>,
- ) {
- self.text
- .paint(bounds, view_state, &mut element_state.text_state, cx)
- }
-}
+ 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,
+ )
+ }
-impl<V: 'static> Component<V> for SharedString {
- fn render(self) -> AnyElement<V> {
- Text {
- text: self,
- runs: None,
+ 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();
+ }
+ }
+ });
+ }
}
- .render()
+
+ self.text.paint(bounds, &mut state.text_state, cx)
}
}
-impl<V: 'static> Component<V> for &'static str {
- fn render(self) -> AnyElement<V> {
- Text {
- text: self.into(),
- runs: None,
- }
- .render()
+impl IntoElement for InteractiveText {
+ type Element = Self;
+
+ fn element_id(&self) -> Option<ElementId> {
+ Some(self.element_id.clone())
}
-}
-// TODO: Figure out how to pass `String` to `child` without this.
-// This impl doesn't exist in the `gpui2` crate.
-impl<V: 'static> Component<V> for String {
- fn render(self) -> AnyElement<V> {
- Text {
- text: self.into(),
- runs: None,
- }
- .render()
+ fn into_element(self) -> Self::Element {
+ self
}
}
@@ -1,61 +1,60 @@
use crate::{
- point, px, size, AnyElement, AvailableSpace, BorrowWindow, Bounds, Component, Element,
- ElementId, InteractiveComponent, InteractiveElementState, Interactivity, LayoutId, Pixels,
- Point, Size, StyleRefinement, Styled, ViewContext,
+ point, px, size, AnyElement, AvailableSpace, BorrowWindow, Bounds, ContentMask, Element,
+ ElementId, InteractiveElement, InteractiveElementState, Interactivity, IntoElement, LayoutId,
+ Pixels, Point, Render, Size, StyleRefinement, Styled, View, ViewContext, WindowContext,
};
use smallvec::SmallVec;
-use std::{cell::RefCell, cmp, mem, ops::Range, rc::Rc};
+use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
use taffy::style::Overflow;
/// uniform_list provides lazy rendering for a set of items that are of uniform height.
/// When rendered into a container with overflow-y: hidden and a fixed (or max) height,
-/// uniform_list will only render the visibile subset of items.
-pub fn uniform_list<I, V, C>(
+/// uniform_list will only render the visible subset of items.
+pub fn uniform_list<I, R, V>(
+ view: View<V>,
id: I,
item_count: usize,
- f: impl 'static + Fn(&mut V, Range<usize>, &mut ViewContext<V>) -> Vec<C>,
-) -> UniformList<V>
+ f: impl 'static + Fn(&mut V, Range<usize>, &mut ViewContext<V>) -> Vec<R>,
+) -> UniformList
where
I: Into<ElementId>,
- V: 'static,
- C: Component<V>,
+ R: IntoElement,
+ V: Render,
{
let id = id.into();
- let mut style = StyleRefinement::default();
- style.overflow.y = Some(Overflow::Hidden);
+ let mut base_style = StyleRefinement::default();
+ base_style.overflow.y = Some(Overflow::Scroll);
+
+ let render_range = move |range, cx: &mut WindowContext| {
+ view.update(cx, |this, cx| {
+ f(this, range, cx)
+ .into_iter()
+ .map(|component| component.into_any_element())
+ .collect()
+ })
+ };
UniformList {
id: id.clone(),
- style,
item_count,
item_to_measure_index: 0,
- render_items: Box::new(move |view, visible_range, cx| {
- f(view, visible_range, cx)
- .into_iter()
- .map(|component| component.render())
- .collect()
- }),
+ render_items: Box::new(render_range),
interactivity: Interactivity {
element_id: Some(id.into()),
+ base_style,
..Default::default()
},
scroll_handle: None,
}
}
-pub struct UniformList<V: 'static> {
+pub struct UniformList {
id: ElementId,
- style: StyleRefinement,
item_count: usize,
item_to_measure_index: usize,
- render_items: Box<
- dyn for<'a> Fn(
- &'a mut V,
- Range<usize>,
- &'a mut ViewContext<V>,
- ) -> SmallVec<[AnyElement<V>; 64]>,
- >,
- interactivity: Interactivity<V>,
+ render_items:
+ Box<dyn for<'a> Fn(Range<usize>, &'a mut WindowContext) -> SmallVec<[AnyElement; 64]>>,
+ interactivity: Interactivity,
scroll_handle: Option<UniformListScrollHandle>,
}
@@ -89,9 +88,9 @@ impl UniformListScrollHandle {
}
}
-impl<V: 'static> Styled for UniformList<V> {
+impl Styled for UniformList {
fn style(&mut self) -> &mut StyleRefinement {
- &mut self.style
+ &mut self.interactivity.base_style
}
}
@@ -101,29 +100,24 @@ pub struct UniformListState {
item_size: Size<Pixels>,
}
-impl<V: 'static> Element<V> for UniformList<V> {
- type ElementState = UniformListState;
-
- fn element_id(&self) -> Option<crate::ElementId> {
- Some(self.id.clone())
- }
+impl Element for UniformList {
+ type State = UniformListState;
fn layout(
&mut self,
- view_state: &mut V,
- element_state: Option<Self::ElementState>,
- cx: &mut ViewContext<V>,
- ) -> (LayoutId, Self::ElementState) {
+ state: Option<Self::State>,
+ cx: &mut WindowContext,
+ ) -> (LayoutId, Self::State) {
let max_items = self.item_count;
let rem_size = cx.rem_size();
- let item_size = element_state
+ let item_size = state
.as_ref()
.map(|s| s.item_size)
- .unwrap_or_else(|| self.measure_item(view_state, None, cx));
+ .unwrap_or_else(|| self.measure_item(None, cx));
let (layout_id, interactive) =
self.interactivity
- .layout(element_state.map(|s| s.interactive), cx, |style, cx| {
+ .layout(state.map(|s| s.interactive), cx, |style, cx| {
cx.request_measured_layout(
style,
rem_size,
@@ -159,11 +153,10 @@ impl<V: 'static> Element<V> for UniformList<V> {
}
fn paint(
- &mut self,
+ self,
bounds: Bounds<crate::Pixels>,
- view_state: &mut V,
- element_state: &mut Self::ElementState,
- cx: &mut ViewContext<V>,
+ element_state: &mut Self::State,
+ cx: &mut WindowContext,
) {
let style =
self.interactivity
@@ -183,14 +176,15 @@ impl<V: 'static> Element<V> for UniformList<V> {
height: item_size.height * self.item_count,
};
- let mut interactivity = mem::take(&mut self.interactivity);
let shared_scroll_offset = element_state
.interactive
.scroll_offset
.get_or_insert_with(Rc::default)
.clone();
- interactivity.paint(
+ let item_height = self.measure_item(Some(padded_bounds.size.width), cx).height;
+
+ self.interactivity.paint(
bounds,
content_size,
&mut element_state.interactive,
@@ -209,9 +203,6 @@ impl<V: 'static> Element<V> for UniformList<V> {
style.paint(bounds, cx);
if self.item_count > 0 {
- let item_height = self
- .measure_item(view_state, Some(padded_bounds.size.width), cx)
- .height;
if let Some(scroll_handle) = self.scroll_handle.clone() {
scroll_handle.0.borrow_mut().replace(ScrollHandleState {
item_height,
@@ -219,58 +210,64 @@ impl<V: 'static> Element<V> for UniformList<V> {
scroll_offset: shared_scroll_offset,
});
}
- let visible_item_count = if item_height > px(0.) {
- (padded_bounds.size.height / item_height).ceil() as usize + 1
- } else {
- 0
- };
let first_visible_element_ix =
(-scroll_offset.y / item_height).floor() as usize;
+ let last_visible_element_ix =
+ ((-scroll_offset.y + padded_bounds.size.height) / item_height).ceil()
+ as usize;
let visible_range = first_visible_element_ix
- ..cmp::min(
- first_visible_element_ix + visible_item_count,
- self.item_count,
- );
+ ..cmp::min(last_visible_element_ix, self.item_count);
- let mut items = (self.render_items)(view_state, visible_range.clone(), cx);
+ let items = (self.render_items)(visible_range.clone(), cx);
cx.with_z_index(1, |cx| {
- for (item, ix) in items.iter_mut().zip(visible_range) {
- let item_origin = padded_bounds.origin
- + point(px(0.), item_height * ix + scroll_offset.y);
- let available_space = size(
- AvailableSpace::Definite(padded_bounds.size.width),
- AvailableSpace::Definite(item_height),
- );
- item.draw(item_origin, available_space, view_state, cx);
- }
+ let content_mask = ContentMask {
+ bounds: padded_bounds,
+ };
+ cx.with_content_mask(Some(content_mask), |cx| {
+ for (item, ix) in items.into_iter().zip(visible_range) {
+ let item_origin = padded_bounds.origin
+ + point(px(0.), item_height * ix + scroll_offset.y);
+ let available_space = size(
+ AvailableSpace::Definite(padded_bounds.size.width),
+ AvailableSpace::Definite(item_height),
+ );
+ item.draw(item_origin, available_space, cx);
+ }
+ });
});
}
})
},
);
- self.interactivity = interactivity;
}
}
-impl<V> UniformList<V> {
+impl IntoElement for UniformList {
+ type Element = Self;
+
+ fn element_id(&self) -> Option<crate::ElementId> {
+ Some(self.id.clone())
+ }
+
+ fn into_element(self) -> Self::Element {
+ self
+ }
+}
+
+impl UniformList {
pub fn with_width_from_item(mut self, item_index: Option<usize>) -> Self {
self.item_to_measure_index = item_index.unwrap_or(0);
self
}
- fn measure_item(
- &self,
- view_state: &mut V,
- list_width: Option<Pixels>,
- cx: &mut ViewContext<V>,
- ) -> Size<Pixels> {
+ fn measure_item(&self, list_width: Option<Pixels>, cx: &mut WindowContext) -> Size<Pixels> {
if self.item_count == 0 {
return Size::default();
}
let item_ix = cmp::min(self.item_to_measure_index, self.item_count - 1);
- let mut items = (self.render_items)(view_state, item_ix..item_ix + 1, cx);
+ let mut items = (self.render_items)(item_ix..item_ix + 1, cx);
let mut item_to_measure = items.pop().unwrap();
let available_space = size(
list_width.map_or(AvailableSpace::MinContent, |width| {
@@ -278,7 +275,7 @@ impl<V> UniformList<V> {
}),
AvailableSpace::MinContent,
);
- item_to_measure.measure(available_space, view_state, cx)
+ item_to_measure.measure(available_space, cx)
}
pub fn track_scroll(mut self, handle: UniformListScrollHandle) -> Self {
@@ -287,14 +284,8 @@ impl<V> UniformList<V> {
}
}
-impl<V> InteractiveComponent<V> for UniformList<V> {
- fn interactivity(&mut self) -> &mut crate::Interactivity<V> {
+impl InteractiveElement for UniformList {
+ fn interactivity(&mut self) -> &mut crate::Interactivity {
&mut self.interactivity
}
}
-
-impl<V: 'static> Component<V> for UniformList<V> {
- fn render(self) -> AnyElement<V> {
- AnyElement::new(self)
- }
-}
@@ -78,8 +78,6 @@ use std::{
};
use taffy::TaffyLayoutEngine;
-type AnyBox = Box<dyn Any>;
-
pub trait Context {
type Result<T>;
@@ -136,7 +134,7 @@ pub trait VisualContext: Context {
build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V,
) -> Self::Result<View<V>>
where
- V: Render;
+ V: 'static + Render;
fn focus_view<V>(&mut self, view: &View<V>) -> Self::Result<()>
where
@@ -1,4 +1,6 @@
-use crate::{AsyncWindowContext, Bounds, Pixels, PlatformInputHandler, View, ViewContext};
+use crate::{
+ AsyncWindowContext, Bounds, Pixels, PlatformInputHandler, View, ViewContext, WindowContext,
+};
use std::ops::Range;
/// Implement this trait to allow views to handle textual input when implementing an editor, field, etc.
@@ -43,9 +45,9 @@ pub struct ElementInputHandler<V> {
impl<V: 'static> ElementInputHandler<V> {
/// Used in [Element::paint] with the element's bounds and a view context for its
/// containing view.
- pub fn new(element_bounds: Bounds<Pixels>, cx: &mut ViewContext<V>) -> Self {
+ pub fn new(element_bounds: Bounds<Pixels>, view: View<V>, cx: &mut WindowContext) -> Self {
ElementInputHandler {
- view: cx.view().clone(),
+ view,
element_bounds,
cx: cx.to_async(),
}
@@ -1,6 +1,6 @@
use crate::{
- div, point, Component, Div, FocusHandle, Keystroke, Modifiers, Pixels, Point, Render,
- ViewContext,
+ div, point, Div, Element, FocusHandle, IntoElement, Keystroke, Modifiers, Pixels, Point,
+ Render, ViewContext,
};
use smallvec::SmallVec;
use std::{any::Any, fmt::Debug, marker::PhantomData, ops::Deref, path::PathBuf};
@@ -64,24 +64,24 @@ pub struct Drag<S, R, V, E>
where
R: Fn(&mut V, &mut ViewContext<V>) -> E,
V: 'static,
- E: Component<()>,
+ E: IntoElement,
{
pub state: S,
pub render_drag_handle: R,
- view_type: PhantomData<V>,
+ view_element_types: PhantomData<(V, E)>,
}
impl<S, R, V, E> Drag<S, R, V, E>
where
R: Fn(&mut V, &mut ViewContext<V>) -> E,
V: 'static,
- E: Component<()>,
+ E: Element,
{
pub fn new(state: S, render_drag_handle: R) -> Self {
Drag {
state,
render_drag_handle,
- view_type: PhantomData,
+ view_element_types: Default::default(),
}
}
}
@@ -194,7 +194,7 @@ impl Deref for MouseExitEvent {
pub struct ExternalPaths(pub(crate) SmallVec<[PathBuf; 2]>);
impl Render for ExternalPaths {
- type Element = Div<Self>;
+ type Element = Div;
fn render(&mut self, _: &mut ViewContext<Self>) -> Self::Element {
div() // Intentionally left empty because the platform will render icons for the dragged files
@@ -286,8 +286,8 @@ pub struct FocusEvent {
#[cfg(test)]
mod test {
use crate::{
- self as gpui, div, Component, Div, FocusHandle, InteractiveComponent, KeyBinding,
- Keystroke, ParentComponent, Render, Stateful, TestAppContext, ViewContext, VisualContext,
+ self as gpui, div, Div, FocusHandle, InteractiveElement, IntoElement, KeyBinding,
+ Keystroke, ParentElement, Render, Stateful, TestAppContext, VisualContext,
};
struct TestView {
@@ -299,20 +299,24 @@ mod test {
actions!(TestAction);
impl Render for TestView {
- type Element = Stateful<Self, Div<Self>>;
+ type Element = Stateful<Div>;
- fn render(&mut self, _: &mut gpui::ViewContext<Self>) -> Self::Element {
+ fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> Self::Element {
div().id("testview").child(
div()
.key_context("parent")
- .on_key_down(|this: &mut TestView, _, _, _| this.saw_key_down = true)
- .on_action(|this: &mut TestView, _: &TestAction, _| this.saw_action = true)
- .child(|this: &mut Self, _cx: &mut ViewContext<Self>| {
+ .on_key_down(cx.listener(|this, _, _| this.saw_key_down = true))
+ .on_action(
+ cx.listener(|this: &mut TestView, _: &TestAction, _| {
+ this.saw_action = true
+ }),
+ )
+ .child(
div()
.key_context("nested")
- .track_focus(&this.focus_handle)
- .render()
- }),
+ .track_focus(&self.focus_handle)
+ .into_element(),
+ ),
)
}
}
@@ -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,4 +1,5 @@
pub use crate::{
- BorrowAppContext, BorrowWindow, Component, Context, FocusableComponent, InteractiveComponent,
- ParentComponent, Refineable, Render, StatefulInteractiveComponent, Styled, VisualContext,
+ BorrowAppContext, BorrowWindow, Context, Element, FocusableElement, InteractiveElement,
+ IntoElement, ParentElement, Refineable, Render, RenderOnce, StatefulInteractiveElement, Styled,
+ VisualContext,
};
@@ -1,9 +1,12 @@
+use std::{iter, mem, ops::Range};
+
use crate::{
black, phi, point, rems, AbsoluteLength, BorrowAppContext, BorrowWindow, Bounds, ContentMask,
Corners, CornersRefinement, CursorStyle, DefiniteLength, Edges, EdgesRefinement, Font,
FontFeatures, FontStyle, FontWeight, Hsla, Length, Pixels, Point, PointRefinement, Rgba,
- SharedString, Size, SizeRefinement, Styled, TextRun, ViewContext,
+ SharedString, Size, SizeRefinement, Styled, TextRun, WindowContext,
};
+use collections::HashSet;
use refineable::{Cascade, Refineable};
use smallvec::SmallVec;
pub use taffy::style::{
@@ -128,6 +131,13 @@ pub struct BoxShadow {
pub spread_radius: Pixels,
}
+#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
+pub enum WhiteSpace {
+ #[default]
+ Normal,
+ Nowrap,
+}
+
#[derive(Refineable, Clone, Debug)]
#[refineable(Debug)]
pub struct TextStyle {
@@ -138,7 +148,9 @@ pub struct TextStyle {
pub line_height: DefiniteLength,
pub font_weight: FontWeight,
pub font_style: FontStyle,
+ pub background_color: Option<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>,
}
@@ -313,7 +333,7 @@ impl Style {
}
/// Paints the background of an element styled with this style.
- pub fn paint<V: 'static>(&self, bounds: Bounds<Pixels>, cx: &mut ViewContext<V>) {
+ pub fn paint(&self, bounds: Bounds<Pixels>, cx: &mut WindowContext) {
let rem_size = cx.rem_size();
cx.with_z_index(0, |cx| {
@@ -432,6 +452,7 @@ impl From<&TextStyle> for HighlightStyle {
color: Some(other.color),
font_weight: Some(other.font_weight),
font_style: Some(other.font_style),
+ background_color: other.background_color,
underline: other.underline.clone(),
fade_out: None,
}
@@ -458,6 +479,10 @@ impl HighlightStyle {
self.font_style = other.font_style;
}
+ if other.background_color.is_some() {
+ self.background_color = other.background_color;
+ }
+
if other.underline.is_some() {
self.underline = other.underline;
}
@@ -481,6 +506,24 @@ impl From<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,23 +1,17 @@
use crate::{
- private::Sealed, AnyBox, AnyElement, AnyModel, AnyWeakModel, AppContext, AvailableSpace,
- BorrowWindow, Bounds, Component, Element, ElementId, Entity, EntityId, Flatten, FocusHandle,
- FocusableView, LayoutId, Model, Pixels, Point, Size, ViewContext, VisualContext, WeakModel,
+ private::Sealed, AnyElement, AnyModel, AnyWeakModel, AppContext, AvailableSpace, BorrowWindow,
+ Bounds, Element, ElementId, Entity, EntityId, Flatten, FocusHandle, FocusableView, IntoElement,
+ LayoutId, Model, Pixels, Point, Render, Size, ViewContext, VisualContext, WeakModel,
WindowContext,
};
use anyhow::{Context, Result};
use std::{
- any::{Any, TypeId},
+ any::TypeId,
hash::{Hash, Hasher},
};
-pub trait Render: 'static + Sized {
- type Element: Element<Self> + 'static;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element;
-}
-
pub struct View<V> {
- pub(crate) model: Model<V>,
+ pub model: Model<V>,
}
impl<V> Sealed for View<V> {}
@@ -65,15 +59,15 @@ impl<V: 'static> View<V> {
self.model.read(cx)
}
- pub fn render_with<C>(&self, component: C) -> RenderViewWith<C, V>
- where
- C: 'static + Component<V>,
- {
- RenderViewWith {
- view: self.clone(),
- component: Some(component),
- }
- }
+ // pub fn render_with<E>(&self, component: E) -> RenderViewWith<E, V>
+ // where
+ // E: 'static + Element,
+ // {
+ // RenderViewWith {
+ // view: self.clone(),
+ // element: Some(component),
+ // }
+ // }
pub fn focus_handle(&self, cx: &AppContext) -> FocusHandle
where
@@ -83,6 +77,24 @@ impl<V: 'static> View<V> {
}
}
+impl<V: Render> Element for View<V> {
+ type State = Option<AnyElement>;
+
+ fn layout(
+ &mut self,
+ _state: Option<Self::State>,
+ cx: &mut WindowContext,
+ ) -> (LayoutId, Self::State) {
+ let mut element = self.update(cx, |view, cx| view.render(cx).into_any());
+ let layout_id = element.layout(cx);
+ (layout_id, Some(element))
+ }
+
+ fn paint(self, _: Bounds<Pixels>, element: &mut Self::State, cx: &mut WindowContext) {
+ element.take().unwrap().paint(cx);
+ }
+}
+
impl<V> Clone for View<V> {
fn clone(&self) -> Self {
Self {
@@ -105,12 +117,6 @@ impl<V> PartialEq for View<V> {
impl<V> Eq for View<V> {}
-impl<V: Render, ParentViewState: 'static> Component<ParentViewState> for View<V> {
- fn render(self) -> AnyElement<ParentViewState> {
- AnyElement::new(AnyView::from(self))
- }
-}
-
pub struct WeakView<V> {
pub(crate) model: WeakModel<V>,
}
@@ -163,8 +169,8 @@ impl<V> Eq for WeakView<V> {}
#[derive(Clone, Debug)]
pub struct AnyView {
model: AnyModel,
- layout: fn(&AnyView, &mut WindowContext) -> (LayoutId, Box<dyn Any>),
- paint: fn(&AnyView, &mut AnyBox, &mut WindowContext),
+ layout: fn(&AnyView, &mut WindowContext) -> (LayoutId, AnyElement),
+ paint: fn(&AnyView, AnyElement, &mut WindowContext),
}
impl AnyView {
@@ -202,21 +208,15 @@ impl AnyView {
cx: &mut WindowContext,
) {
cx.with_absolute_element_offset(origin, |cx| {
- let (layout_id, mut rendered_element) = (self.layout)(self, cx);
+ let (layout_id, rendered_element) = (self.layout)(self, cx);
cx.window
.layout_engine
.compute_layout(layout_id, available_space);
- (self.paint)(self, &mut rendered_element, cx);
+ (self.paint)(self, rendered_element, cx);
})
}
}
-impl<V: 'static> Component<V> for AnyView {
- fn render(self) -> AnyElement<V> {
- AnyElement::new(self)
- }
-}
-
impl<V: Render> From<View<V>> for AnyView {
fn from(value: View<V>) -> Self {
AnyView {
@@ -227,37 +227,51 @@ impl<V: Render> From<View<V>> for AnyView {
}
}
-impl<ParentViewState: 'static> Element<ParentViewState> for AnyView {
- type ElementState = Box<dyn Any>;
+impl Element for AnyView {
+ type State = Option<AnyElement>;
+
+ fn layout(
+ &mut self,
+ _state: Option<Self::State>,
+ cx: &mut WindowContext,
+ ) -> (LayoutId, Self::State) {
+ let (layout_id, state) = (self.layout)(self, cx);
+ (layout_id, Some(state))
+ }
+
+ fn paint(self, _: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
+ (self.paint)(&self, state.take().unwrap(), cx)
+ }
+}
+
+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 layout(
- &mut self,
- _view_state: &mut ParentViewState,
- _element_state: Option<Self::ElementState>,
- cx: &mut ViewContext<ParentViewState>,
- ) -> (LayoutId, Self::ElementState) {
- (self.layout)(self, cx)
+ fn into_element(self) -> Self::Element {
+ self
}
+}
- fn paint(
- &mut self,
- _bounds: Bounds<Pixels>,
- _view_state: &mut ParentViewState,
- rendered_element: &mut Self::ElementState,
- cx: &mut ViewContext<ParentViewState>,
- ) {
- (self.paint)(self, rendered_element, cx)
+impl IntoElement for AnyView {
+ type Element = Self;
+
+ fn element_id(&self) -> Option<ElementId> {
+ Some(ElementId::from_entity_id(self.model.entity_id))
+ }
+
+ fn into_element(self) -> Self::Element {
+ self
}
}
pub struct AnyWeakView {
model: AnyWeakModel,
- layout: fn(&AnyView, &mut WindowContext) -> (LayoutId, Box<dyn Any>),
- paint: fn(&AnyView, &mut AnyBox, &mut WindowContext),
+ layout: fn(&AnyView, &mut WindowContext) -> (LayoutId, AnyElement),
+ paint: fn(&AnyView, AnyElement, &mut WindowContext),
}
impl AnyWeakView {
@@ -271,7 +285,7 @@ impl AnyWeakView {
}
}
-impl<V: Render> From<WeakView<V>> for AnyWeakView {
+impl<V: 'static + Render> From<WeakView<V>> for AnyWeakView {
fn from(view: WeakView<V>) -> Self {
Self {
model: view.model.into(),
@@ -281,97 +295,36 @@ impl<V: Render> From<WeakView<V>> for AnyWeakView {
}
}
-// impl<T, E> Render for T
-// where
-// T: 'static + FnMut(&mut WindowContext) -> E,
-// E: 'static + Send + Element<T>,
-// {
-// type Element = E;
-
-// fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
-// (self)(cx)
-// }
-// }
-
-pub struct RenderViewWith<C, V> {
- view: View<V>,
- component: Option<C>,
-}
-
-impl<C, ParentViewState, ViewState> Component<ParentViewState> for RenderViewWith<C, ViewState>
-where
- C: 'static + Component<ViewState>,
- ParentViewState: 'static,
- ViewState: 'static,
-{
- fn render(self) -> AnyElement<ParentViewState> {
- AnyElement::new(self)
- }
-}
-
-impl<C, ParentViewState, ViewState> Element<ParentViewState> for RenderViewWith<C, ViewState>
+impl<T, E> Render for T
where
- C: 'static + Component<ViewState>,
- ParentViewState: 'static,
- ViewState: 'static,
+ T: 'static + FnMut(&mut WindowContext) -> E,
+ E: 'static + Send + Element,
{
- type ElementState = AnyElement<ViewState>;
-
- fn element_id(&self) -> Option<ElementId> {
- Some(self.view.entity_id().into())
- }
+ type Element = E;
- fn layout(
- &mut self,
- _: &mut ParentViewState,
- _: Option<Self::ElementState>,
- cx: &mut ViewContext<ParentViewState>,
- ) -> (LayoutId, Self::ElementState) {
- self.view.update(cx, |view, cx| {
- let mut element = self.component.take().unwrap().render();
- let layout_id = element.layout(view, cx);
- (layout_id, element)
- })
- }
-
- fn paint(
- &mut self,
- _: Bounds<Pixels>,
- _: &mut ParentViewState,
- element: &mut Self::ElementState,
- cx: &mut ViewContext<ParentViewState>,
- ) {
- self.view.update(cx, |view, cx| element.paint(view, cx))
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+ (self)(cx)
}
}
mod any_view {
- use crate::{AnyElement, AnyView, BorrowWindow, LayoutId, Render, WindowContext};
- use std::any::Any;
+ use crate::{AnyElement, AnyView, Element, LayoutId, Render, WindowContext};
- pub(crate) fn layout<V: Render>(
+ pub(crate) fn layout<V: 'static + Render>(
view: &AnyView,
cx: &mut WindowContext,
- ) -> (LayoutId, Box<dyn Any>) {
- cx.with_element_id(Some(view.model.entity_id), |cx| {
- let view = view.clone().downcast::<V>().unwrap();
- view.update(cx, |view, cx| {
- let mut element = AnyElement::new(view.render(cx));
- let layout_id = element.layout(view, cx);
- (layout_id, Box::new(element) as Box<dyn Any>)
- })
- })
+ ) -> (LayoutId, AnyElement) {
+ 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: Render>(
- view: &AnyView,
- element: &mut Box<dyn Any>,
+ pub(crate) fn paint<V: 'static + Render>(
+ _view: &AnyView,
+ element: AnyElement,
cx: &mut WindowContext,
) {
- cx.with_element_id(Some(view.model.entity_id), |cx| {
- let view = view.clone().downcast::<V>().unwrap();
- let element = element.downcast_mut::<AnyElement<V>>().unwrap();
- view.update(cx, |view, cx| element.paint(view, cx))
- })
+ element.paint(cx);
}
}
@@ -1,5 +1,5 @@
use crate::{
- key_dispatch::DispatchActionListener, px, size, Action, AnyBox, AnyDrag, AnyView, AppContext,
+ key_dispatch::DispatchActionListener, px, size, Action, AnyDrag, AnyView, AppContext,
AsyncWindowContext, AvailableSpace, Bounds, BoxShadow, Context, Corners, CursorStyle,
DevicePixels, DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, EntityId,
EventEmitter, FileDropEvent, Flatten, FocusEvent, FontId, GlobalElementId, GlyphId, Hsla,
@@ -187,17 +187,17 @@ impl Drop for FocusHandle {
/// FocusableView allows users of your view to easily
/// focus it (using cx.focus_view(view))
-pub trait FocusableView: Render {
+pub trait FocusableView: 'static + Render {
fn focus_handle(&self, cx: &AppContext) -> FocusHandle;
}
/// ManagedView is a view (like a Modal, Popover, Menu, etc.)
/// where the lifecycle of the view is handled by another view.
-pub trait ManagedView: FocusableView + EventEmitter<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, AnyBox>,
+ 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.
@@ -1436,6 +1441,89 @@ impl<'a> WindowContext<'a> {
.dispatch_tree
.bindings_for_action(action)
}
+
+ pub fn listener_for<V: Render, E>(
+ &self,
+ view: &View<V>,
+ f: impl Fn(&mut V, &E, &mut ViewContext<V>) + 'static,
+ ) -> impl Fn(&E, &mut WindowContext) + 'static {
+ let view = view.downgrade();
+ move |e: &E, cx: &mut WindowContext| {
+ view.update(cx, |view, cx| f(view, e, cx)).ok();
+ }
+ }
+
+ pub fn constructor_for<V: Render, R>(
+ &self,
+ view: &View<V>,
+ f: impl Fn(&mut V, &mut ViewContext<V>) -> R + 'static,
+ ) -> impl Fn(&mut WindowContext) -> R + 'static {
+ let view = view.clone();
+ move |cx: &mut WindowContext| view.update(cx, |view, cx| f(view, cx))
+ }
+
+ //========== ELEMENT RELATED FUNCTIONS ===========
+ pub fn with_key_dispatch<R>(
+ &mut self,
+ context: KeyContext,
+ focus_handle: Option<FocusHandle>,
+ f: impl FnOnce(Option<FocusHandle>, &mut Self) -> R,
+ ) -> R {
+ let window = &mut self.window;
+ window
+ .current_frame
+ .dispatch_tree
+ .push_node(context.clone());
+ if let Some(focus_handle) = focus_handle.as_ref() {
+ window
+ .current_frame
+ .dispatch_tree
+ .make_focusable(focus_handle.id);
+ }
+ let result = f(focus_handle, self);
+
+ self.window.current_frame.dispatch_tree.pop_node();
+
+ result
+ }
+
+ /// Register a focus listener for the current frame only. It will be cleared
+ /// on the next frame render. You should use this method only from within elements,
+ /// and we may want to enforce that better via a different context type.
+ // todo!() Move this to `FrameContext` to emphasize its individuality?
+ pub fn on_focus_changed(
+ &mut self,
+ listener: impl Fn(&FocusEvent, &mut WindowContext) + 'static,
+ ) {
+ self.window
+ .current_frame
+ .focus_listeners
+ .push(Box::new(move |event, cx| {
+ listener(event, cx);
+ }));
+ }
+
+ /// Set an input handler, such as [ElementInputHandler], which interfaces with the
+ /// platform to receive textual input with proper integration with concerns such
+ /// as IME interactions.
+ pub fn handle_input(
+ &mut self,
+ focus_handle: &FocusHandle,
+ input_handler: impl PlatformInputHandler,
+ ) {
+ if focus_handle.is_focused(self) {
+ self.window
+ .platform_window
+ .set_input_handler(Box::new(input_handler));
+ }
+ }
+
+ pub fn on_window_should_close(&mut self, f: impl Fn(&mut WindowContext) -> bool + 'static) {
+ let mut this = self.to_async();
+ self.window
+ .platform_window
+ .on_should_close(Box::new(move || this.update(|_, cx| f(cx)).unwrap_or(true)))
+ }
}
impl Context for WindowContext<'_> {
@@ -1559,7 +1647,7 @@ impl VisualContext for WindowContext<'_> {
build_view: impl FnOnce(&mut ViewContext<'_, V>) -> V,
) -> Self::Result<View<V>>
where
- V: Render,
+ V: 'static + Render,
{
let slot = self.app.entities.reserve();
let view = View {
@@ -1582,7 +1670,7 @@ impl VisualContext for WindowContext<'_> {
where
V: ManagedView,
{
- self.update_view(view, |_, cx| cx.emit(Manager::Dismiss))
+ self.update_view(view, |_, cx| cx.emit(DismissEvent::Dismiss))
}
}
@@ -1617,6 +1705,10 @@ pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> {
self.borrow_mut()
}
+ fn app(&self) -> &AppContext {
+ self.borrow()
+ }
+
fn window(&self) -> &Window {
self.borrow()
}
@@ -1667,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>(
@@ -1735,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");
@@ -1747,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
}
})
@@ -2124,49 +2274,6 @@ impl<'a, V: 'static> ViewContext<'a, V> {
)
}
- /// Register a focus listener for the current frame only. It will be cleared
- /// on the next frame render. You should use this method only from within elements,
- /// and we may want to enforce that better via a different context type.
- // todo!() Move this to `FrameContext` to emphasize its individuality?
- pub fn on_focus_changed(
- &mut self,
- listener: impl Fn(&mut V, &FocusEvent, &mut ViewContext<V>) + 'static,
- ) {
- let handle = self.view().downgrade();
- self.window
- .current_frame
- .focus_listeners
- .push(Box::new(move |event, cx| {
- handle
- .update(cx, |view, cx| listener(view, event, cx))
- .log_err();
- }));
- }
-
- pub fn with_key_dispatch<R>(
- &mut self,
- context: KeyContext,
- focus_handle: Option<FocusHandle>,
- f: impl FnOnce(Option<FocusHandle>, &mut Self) -> R,
- ) -> R {
- let window = &mut self.window;
- window
- .current_frame
- .dispatch_tree
- .push_node(context.clone());
- if let Some(focus_handle) = focus_handle.as_ref() {
- window
- .current_frame
- .dispatch_tree
- .make_focusable(focus_handle.id);
- }
- let result = f(focus_handle, self);
-
- self.window.current_frame.dispatch_tree.pop_node();
-
- result
- }
-
pub fn spawn<Fut, R>(
&mut self,
f: impl FnOnce(WeakView<V>, AsyncWindowContext) -> Fut,
@@ -2243,21 +2350,6 @@ impl<'a, V: 'static> ViewContext<'a, V> {
});
}
- /// Set an input handler, such as [ElementInputHandler], which interfaces with the
- /// platform to receive textual input with proper integration with concerns such
- /// as IME interactions.
- pub fn handle_input(
- &mut self,
- focus_handle: &FocusHandle,
- input_handler: impl PlatformInputHandler,
- ) {
- if focus_handle.is_focused(self) {
- self.window
- .platform_window
- .set_input_handler(Box::new(input_handler));
- }
- }
-
pub fn emit<Evt>(&mut self, event: Evt)
where
Evt: 'static,
@@ -2282,7 +2374,17 @@ impl<'a, V: 'static> ViewContext<'a, V> {
where
V: ManagedView,
{
- self.defer(|_, cx| cx.emit(Manager::Dismiss))
+ self.defer(|_, cx| cx.emit(DismissEvent::Dismiss))
+ }
+
+ pub fn listener<E>(
+ &self,
+ f: impl Fn(&mut V, &E, &mut ViewContext<V>) + 'static,
+ ) -> impl Fn(&E, &mut WindowContext) + 'static {
+ let view = self.view().downgrade();
+ move |e: &E, cx: &mut WindowContext| {
+ view.update(cx, |view, cx| f(view, e, cx)).ok();
+ }
}
}
@@ -2355,7 +2457,7 @@ impl<V: 'static> VisualContext for ViewContext<'_, V> {
build_view: impl FnOnce(&mut ViewContext<'_, W>) -> W,
) -> Self::Result<View<W>>
where
- W: Render,
+ W: 'static + Render,
{
self.window_cx.replace_root_view(build_view)
}
@@ -2567,6 +2669,12 @@ pub enum ElementId {
FocusHandle(FocusId),
}
+impl ElementId {
+ pub(crate) fn from_entity_id(entity_id: EntityId) -> Self {
+ ElementId::View(entity_id)
+ }
+}
+
impl TryInto<SharedString> for ElementId {
type Error = anyhow::Error;
@@ -2579,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
- }
-}
@@ -0,0 +1,27 @@
+use proc_macro::TokenStream;
+use quote::quote;
+use syn::{parse_macro_input, DeriveInput};
+
+pub fn derive_into_element(input: TokenStream) -> TokenStream {
+ let ast = parse_macro_input!(input as DeriveInput);
+ let type_name = &ast.ident;
+ let (impl_generics, type_generics, where_clause) = ast.generics.split_for_impl();
+
+ let gen = quote! {
+ impl #impl_generics gpui::IntoElement for #type_name #type_generics
+ #where_clause
+ {
+ type Element = gpui::Component<Self>;
+
+ fn element_id(&self) -> Option<ElementId> {
+ None
+ }
+
+ fn into_element(self) -> Self::Element {
+ gpui::Component::new(self)
+ }
+ }
+ };
+
+ gen.into()
+}
@@ -1,16 +1,11 @@
mod action;
-mod derive_component;
+mod derive_into_element;
mod register_action;
mod style_helpers;
mod test;
use proc_macro::TokenStream;
-#[proc_macro]
-pub fn style_helpers(args: TokenStream) -> TokenStream {
- style_helpers::style_helpers(args)
-}
-
#[proc_macro_derive(Action)]
pub fn action(input: TokenStream) -> TokenStream {
action::action(input)
@@ -21,9 +16,14 @@ pub fn register_action(attr: TokenStream, item: TokenStream) -> TokenStream {
register_action::register_action_macro(attr, item)
}
-#[proc_macro_derive(Component, attributes(component))]
-pub fn derive_component(input: TokenStream) -> TokenStream {
- derive_component::derive_component(input)
+#[proc_macro_derive(IntoElement)]
+pub fn derive_into_element(input: TokenStream) -> TokenStream {
+ derive_into_element::derive_into_element(input)
+}
+
+#[proc_macro]
+pub fn style_helpers(input: TokenStream) -> TokenStream {
+ style_helpers::style_helpers(input)
}
#[proc_macro_attribute]
@@ -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());
}
}
@@ -61,12 +61,14 @@ fn build_bridge(swift_target: &SwiftTarget) {
let swift_package_root = swift_package_root();
let swift_target_folder = swift_target_folder();
+ let swift_cache_folder = swift_cache_folder();
if !Command::new("swift")
.arg("build")
.arg("--disable-automatic-resolution")
.args(["--configuration", &env::var("PROFILE").unwrap()])
.args(["--triple", &swift_target.target.triple])
.args(["--build-path".into(), swift_target_folder])
+ .args(["--cache-path".into(), swift_cache_folder])
.current_dir(&swift_package_root)
.status()
.unwrap()
@@ -133,9 +135,17 @@ fn swift_package_root() -> PathBuf {
}
fn swift_target_folder() -> PathBuf {
+ let target = env::var("TARGET").unwrap();
env::current_dir()
.unwrap()
- .join(format!("../../target/{SWIFT_PACKAGE_NAME}"))
+ .join(format!("../../target/{target}/{SWIFT_PACKAGE_NAME}_target"))
+}
+
+fn swift_cache_folder() -> PathBuf {
+ let target = env::var("TARGET").unwrap();
+ env::current_dir()
+ .unwrap()
+ .join(format!("../../target/{target}/{SWIFT_PACKAGE_NAME}_cache"))
}
fn copy_dir(source: &Path, destination: &Path) {
@@ -73,6 +73,7 @@ impl RealNodeRuntime {
let npm_file = node_dir.join("bin/npm");
let result = Command::new(&node_binary)
+ .env_clear()
.arg(npm_file)
.arg("--version")
.stdin(Stdio::null())
@@ -149,6 +150,7 @@ impl NodeRuntime for RealNodeRuntime {
}
let mut command = Command::new(node_binary);
+ command.env_clear();
command.env("PATH", env_path);
command.arg(npm_file).arg(subcommand);
command.args(["--cache".into(), installation_path.join("cache")]);
@@ -200,11 +202,11 @@ impl NodeRuntime for RealNodeRuntime {
&[
name,
"--json",
- "-fetch-retry-mintimeout",
+ "--fetch-retry-mintimeout",
"2000",
- "-fetch-retry-maxtimeout",
+ "--fetch-retry-maxtimeout",
"5000",
- "-fetch-timeout",
+ "--fetch-timeout",
"5000",
],
)
@@ -229,11 +231,11 @@ impl NodeRuntime for RealNodeRuntime {
let mut arguments: Vec<_> = packages.iter().map(|p| p.as_str()).collect();
arguments.extend_from_slice(&[
- "-fetch-retry-mintimeout",
+ "--fetch-retry-mintimeout",
"2000",
- "-fetch-retry-maxtimeout",
+ "--fetch-retry-maxtimeout",
"5000",
- "-fetch-timeout",
+ "--fetch-timeout",
"5000",
]);
@@ -1,10 +1,10 @@
use editor::Editor;
use gpui::{
- div, prelude::*, uniform_list, AppContext, Component, Div, FocusHandle, FocusableView,
- MouseButton, Render, Task, UniformListScrollHandle, View, ViewContext, WindowContext,
+ div, prelude::*, uniform_list, AppContext, Div, FocusHandle, FocusableView, MouseButton,
+ MouseDownEvent, Render, Task, UniformListScrollHandle, View, ViewContext, WindowContext,
};
use std::{cmp, sync::Arc};
-use ui::{prelude::*, v_stack, Divider, Label, TextColor};
+use ui::{prelude::*, v_stack, Color, Divider, Label};
pub struct Picker<D: PickerDelegate> {
pub delegate: D,
@@ -15,7 +15,7 @@ pub struct Picker<D: PickerDelegate> {
}
pub trait PickerDelegate: Sized + 'static {
- type ListItem: Component<Picker<Self>>;
+ type ListItem: IntoElement;
fn match_count(&self) -> usize;
fn selected_index(&self) -> usize;
@@ -32,7 +32,7 @@ pub trait PickerDelegate: Sized + 'static {
ix: usize,
selected: bool,
cx: &mut ViewContext<Picker<Self>>,
- ) -> Self::ListItem;
+ ) -> Option<Self::ListItem>;
}
impl<D: PickerDelegate> FocusableView for Picker<D> {
@@ -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);
}
@@ -181,20 +182,20 @@ impl<D: PickerDelegate> Picker<D> {
}
impl<D: PickerDelegate> Render for Picker<D> {
- type Element = Div<Self>;
+ type Element = Div;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
div()
.key_context("picker")
.size_full()
.elevation_2(cx)
- .on_action(Self::select_next)
- .on_action(Self::select_prev)
- .on_action(Self::select_first)
- .on_action(Self::select_last)
- .on_action(Self::cancel)
- .on_action(Self::confirm)
- .on_action(Self::secondary_confirm)
+ .on_action(cx.listener(Self::select_next))
+ .on_action(cx.listener(Self::select_prev))
+ .on_action(cx.listener(Self::select_first))
+ .on_action(cx.listener(Self::select_last))
+ .on_action(cx.listener(Self::cancel))
+ .on_action(cx.listener(Self::confirm))
+ .on_action(cx.listener(Self::secondary_confirm))
.child(
v_stack()
.py_0p5()
@@ -208,31 +209,37 @@ impl<D: PickerDelegate> Render for Picker<D> {
.p_1()
.grow()
.child(
- uniform_list("candidates", self.delegate.match_count(), {
- move |this: &mut Self, visible_range, cx| {
- let selected_ix = this.delegate.selected_index();
- visible_range
- .map(|ix| {
- div()
- .on_mouse_down(
- MouseButton::Left,
- move |this: &mut Self, event, cx| {
- this.handle_click(
- ix,
- event.modifiers.command,
- cx,
- )
- },
- )
- .child(this.delegate.render_match(
- ix,
- ix == selected_ix,
- cx,
- ))
- })
- .collect()
- }
- })
+ uniform_list(
+ cx.view().clone(),
+ "candidates",
+ self.delegate.match_count(),
+ {
+ let selected_index = self.delegate.selected_index();
+
+ move |picker, visible_range, cx| {
+ visible_range
+ .map(|ix| {
+ div()
+ .on_mouse_down(
+ MouseButton::Left,
+ cx.listener(move |this, event: &MouseDownEvent, cx| {
+ this.handle_click(
+ ix,
+ event.modifiers.command,
+ cx,
+ )
+ }),
+ )
+ .children(picker.delegate.render_match(
+ ix,
+ ix == selected_index,
+ cx,
+ ))
+ })
+ .collect()
+ }
+ },
+ )
.track_scroll(self.scroll_handle.clone()),
)
.max_h_72()
@@ -244,7 +251,7 @@ impl<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]
@@ -9,7 +9,6 @@ path = "src/project_panel.rs"
doctest = false
[dependencies]
-context_menu = { path = "../context_menu" }
collections = { path = "../collections" }
db = { path = "../db2", package = "db2" }
editor = { path = "../editor2", package = "editor2" }
@@ -9,10 +9,9 @@ use file_associations::FileAssociations;
use anyhow::{anyhow, Result};
use gpui::{
actions, div, px, uniform_list, Action, AppContext, AssetSource, AsyncWindowContext,
- ClipboardItem, Component, Div, EventEmitter, FocusHandle, Focusable, FocusableView,
- InteractiveComponent, Model, MouseButton, ParentComponent, Pixels, Point, PromptLevel, Render,
- Stateful, StatefulInteractiveComponent, Styled, Task, UniformListScrollHandle, View,
- ViewContext, VisualContext as _, WeakView, WindowContext,
+ ClipboardItem, Div, EventEmitter, FocusHandle, Focusable, FocusableView, InteractiveElement,
+ Model, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Render, Stateful, Styled,
+ Task, UniformListScrollHandle, View, ViewContext, VisualContext as _, WeakView, WindowContext,
};
use menu::{Confirm, SelectNext, SelectPrev};
use project::{
@@ -30,7 +29,7 @@ use std::{
sync::Arc,
};
use theme::ActiveTheme as _;
-use ui::{h_stack, v_stack, IconElement, Label};
+use ui::{v_stack, IconElement, Label, ListItem};
use unicase::UniCase;
use util::{maybe, ResultExt, TryFutureExt};
use workspace::{
@@ -247,7 +246,6 @@ impl ProjectPanel {
let mut old_dock_position = this.position(cx);
ProjectPanelSettings::register(cx);
cx.observe_global::<SettingsStore>(move |this, cx| {
- dbg!("OLA!");
let new_dock_position = this.position(cx);
if new_dock_position != old_dock_position {
old_dock_position = new_dock_position;
@@ -372,7 +370,7 @@ impl ProjectPanel {
_entry_id: ProjectEntryId,
_cx: &mut ViewContext<Self>,
) {
- todo!()
+ // todo!()
// let project = self.project.read(cx);
// let worktree_id = if let Some(id) = project.worktree_id_for_entry(entry_id, cx) {
@@ -645,6 +643,7 @@ impl ProjectPanel {
}
fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
+ dbg!("odd");
self.edit_state = None;
self.update_visible_entries(None, cx);
cx.focus(&self.focus_handle);
@@ -1335,13 +1334,19 @@ impl ProjectPanel {
}
}
- fn render_entry_visual_element(
- details: &EntryDetails,
- editor: Option<&View<Editor>>,
- padding: Pixels,
+ fn render_entry(
+ &self,
+ entry_id: ProjectEntryId,
+ details: EntryDetails,
+ // dragged_entry_destination: &mut Option<Arc<Path>>,
cx: &mut ViewContext<Self>,
- ) -> Div<Self> {
+ ) -> ListItem {
+ let kind = details.kind;
+ let settings = ProjectPanelSettings::get_global(cx);
let show_editor = details.is_editing && !details.is_processing;
+ let is_selected = self
+ .selection
+ .map_or(false, |selection| selection.entry_id == entry_id);
let theme = cx.theme();
let filename_text_color = details
@@ -1354,14 +1359,17 @@ impl ProjectPanel {
})
.unwrap_or(theme.status().info);
- h_stack()
+ ListItem::new(entry_id.to_proto() as usize)
+ .indent_level(details.depth)
+ .indent_step_size(px(settings.indent_size))
+ .selected(is_selected)
.child(if let Some(icon) = &details.icon {
div().child(IconElement::from_path(icon.to_string()))
} else {
div()
})
.child(
- if let (Some(editor), true) = (editor, show_editor) {
+ if let (Some(editor), true) = (Some(&self.filename_editor), show_editor) {
div().w_full().child(editor.clone())
} else {
div()
@@ -1370,34 +1378,7 @@ impl ProjectPanel {
}
.ml_1(),
)
- .pl(padding)
- }
-
- fn render_entry(
- &self,
- entry_id: ProjectEntryId,
- details: EntryDetails,
- // dragged_entry_destination: &mut Option<Arc<Path>>,
- cx: &mut ViewContext<Self>,
- ) -> Stateful<Self, Div<Self>> {
- let kind = details.kind;
- let settings = ProjectPanelSettings::get_global(cx);
- const INDENT_SIZE: Pixels = px(16.0);
- let padding = INDENT_SIZE + details.depth as f32 * px(settings.indent_size);
- let show_editor = details.is_editing && !details.is_processing;
- let is_selected = self
- .selection
- .map_or(false, |selection| selection.entry_id == entry_id);
-
- Self::render_entry_visual_element(&details, Some(&self.filename_editor), padding, cx)
- .id(entry_id.to_proto() as usize)
- .w_full()
- .cursor_pointer()
- .when(is_selected, |this| {
- this.bg(cx.theme().colors().element_selected)
- })
- .hover(|style| style.bg(cx.theme().colors().element_hover))
- .on_click(move |this, event, cx| {
+ .on_click(cx.listener(move |this, event: &gpui::ClickEvent, cx| {
if !show_editor {
if kind.is_dir() {
this.toggle_expanded(entry_id, cx);
@@ -1409,10 +1390,10 @@ impl ProjectPanel {
}
}
}
- })
- .on_mouse_down(MouseButton::Right, move |this, event, cx| {
+ }))
+ .on_secondary_mouse_down(cx.listener(move |this, event: &MouseDownEvent, cx| {
this.deploy_context_menu(event.position, entry_id, cx);
- })
+ }))
// .on_drop::<ProjectEntryId>(|this, event, cx| {
// this.move_entry(
// *dragged_entry,
@@ -1425,9 +1406,9 @@ impl ProjectPanel {
}
impl Render for ProjectPanel {
- type Element = Focusable<Self, Stateful<Self, Div<Self>>>;
+ type Element = Focusable<Stateful<Div>>;
- fn render(&mut self, _cx: &mut gpui::ViewContext<Self>) -> Self::Element {
+ fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> Self::Element {
let has_worktree = self.visible_entries.len() != 0;
if has_worktree {
@@ -1435,40 +1416,43 @@ impl Render for ProjectPanel {
.id("project-panel")
.size_full()
.key_context("ProjectPanel")
- .on_action(Self::select_next)
- .on_action(Self::select_prev)
- .on_action(Self::expand_selected_entry)
- .on_action(Self::collapse_selected_entry)
- .on_action(Self::collapse_all_entries)
- .on_action(Self::new_file)
- .on_action(Self::new_directory)
- .on_action(Self::rename)
- .on_action(Self::delete)
- .on_action(Self::confirm)
- .on_action(Self::open_file)
- .on_action(Self::cancel)
- .on_action(Self::cut)
- .on_action(Self::copy)
- .on_action(Self::copy_path)
- .on_action(Self::copy_relative_path)
- .on_action(Self::paste)
- .on_action(Self::reveal_in_finder)
- .on_action(Self::open_in_terminal)
- .on_action(Self::new_search_in_directory)
+ .on_action(cx.listener(Self::select_next))
+ .on_action(cx.listener(Self::select_prev))
+ .on_action(cx.listener(Self::expand_selected_entry))
+ .on_action(cx.listener(Self::collapse_selected_entry))
+ .on_action(cx.listener(Self::collapse_all_entries))
+ .on_action(cx.listener(Self::new_file))
+ .on_action(cx.listener(Self::new_directory))
+ .on_action(cx.listener(Self::rename))
+ .on_action(cx.listener(Self::delete))
+ .on_action(cx.listener(Self::confirm))
+ .on_action(cx.listener(Self::open_file))
+ .on_action(cx.listener(Self::cancel))
+ .on_action(cx.listener(Self::cut))
+ .on_action(cx.listener(Self::copy))
+ .on_action(cx.listener(Self::copy_path))
+ .on_action(cx.listener(Self::copy_relative_path))
+ .on_action(cx.listener(Self::paste))
+ .on_action(cx.listener(Self::reveal_in_finder))
+ .on_action(cx.listener(Self::open_in_terminal))
+ .on_action(cx.listener(Self::new_search_in_directory))
.track_focus(&self.focus_handle)
.child(
uniform_list(
+ cx.view().clone(),
"entries",
self.visible_entries
.iter()
.map(|(_, worktree_entries)| worktree_entries.len())
.sum(),
- |this: &mut Self, range, cx| {
- let mut items = Vec::new();
- this.for_each_visible_entry(range, cx, |id, details, cx| {
- items.push(this.render_entry(id, details, cx));
- });
- items
+ {
+ |this, range, cx| {
+ let mut items = Vec::new();
+ this.for_each_visible_entry(range, cx, |id, details, cx| {
+ items.push(this.render_entry(id, details, cx));
+ });
+ items
+ }
},
)
.size_full()
@@ -56,12 +56,12 @@ pub struct Mention {
}
impl RichText {
- pub fn element<V: 'static>(
+ pub fn element(
&self,
// syntax: Arc<SyntaxTheme>,
// style: RichTextStyle,
// cx: &mut ViewContext<V>,
- ) -> AnyElement<V> {
+ ) -> AnyElement {
todo!();
// let mut region_id = 0;
@@ -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() {
@@ -0,0 +1,40 @@
+[package]
+name = "search2"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/search.rs"
+doctest = false
+
+[dependencies]
+bitflags = "1"
+collections = { path = "../collections" }
+editor = { package = "editor2", path = "../editor2" }
+gpui = { package = "gpui2", path = "../gpui2" }
+language = { package = "language2", path = "../language2" }
+menu = { package = "menu2", path = "../menu2" }
+project = { package = "project2", path = "../project2" }
+settings = { package = "settings2", path = "../settings2" }
+theme = { package = "theme2", path = "../theme2" }
+util = { path = "../util" }
+ui = {package = "ui2", path = "../ui2"}
+workspace = { package = "workspace2", path = "../workspace2" }
+#semantic_index = { path = "../semantic_index" }
+anyhow.workspace = true
+futures.workspace = true
+log.workspace = true
+postage.workspace = true
+serde.workspace = true
+serde_derive.workspace = true
+smallvec.workspace = true
+smol.workspace = true
+serde_json.workspace = true
+[dev-dependencies]
+client = { package = "client2", path = "../client2", features = ["test-support"] }
+editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
+gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
+
+workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }
+unindent.workspace = true
@@ -0,0 +1,1747 @@
+use crate::{
+ history::SearchHistory,
+ mode::{next_mode, SearchMode},
+ search_bar::{render_nav_button, render_search_mode_button},
+ ActivateRegexMode, ActivateTextMode, CycleMode, NextHistoryQuery, PreviousHistoryQuery,
+ ReplaceAll, ReplaceNext, SearchOptions, SelectAllMatches, SelectNextMatch, SelectPrevMatch,
+ ToggleCaseSensitive, ToggleReplace, ToggleWholeWord,
+};
+use collections::HashMap;
+use editor::{Editor, EditorMode};
+use futures::channel::oneshot;
+use gpui::{
+ actions, div, red, Action, AppContext, Div, EventEmitter, InteractiveElement as _, IntoElement,
+ ParentElement as _, Render, Styled, Subscription, Task, View, ViewContext, VisualContext as _,
+ WeakView, WindowContext,
+};
+use project::search::SearchQuery;
+use serde::Deserialize;
+use std::{any::Any, sync::Arc};
+
+use ui::{h_stack, ButtonGroup, Icon, IconButton, IconElement};
+use util::ResultExt;
+use workspace::{
+ item::ItemHandle,
+ searchable::{Direction, SearchEvent, SearchableItemHandle, WeakSearchableItemHandle},
+ ToolbarItemLocation, ToolbarItemView,
+};
+
+#[derive(PartialEq, Clone, Deserialize, Default, Action)]
+pub struct Deploy {
+ pub focus: bool,
+}
+
+actions!(Dismiss, FocusEditor);
+
+pub enum Event {
+ UpdateLocation,
+}
+
+pub fn init(cx: &mut AppContext) {
+ cx.observe_new_views(|editor: &mut Editor, cx| BufferSearchBar::register(editor, cx))
+ .detach();
+}
+
+pub struct BufferSearchBar {
+ query_editor: View<Editor>,
+ replacement_editor: View<Editor>,
+ active_searchable_item: Option<Box<dyn SearchableItemHandle>>,
+ active_match_index: Option<usize>,
+ active_searchable_item_subscription: Option<Subscription>,
+ active_search: Option<Arc<SearchQuery>>,
+ searchable_items_with_matches:
+ HashMap<Box<dyn WeakSearchableItemHandle>, Vec<Box<dyn Any + Send>>>,
+ pending_search: Option<Task<()>>,
+ search_options: SearchOptions,
+ default_options: SearchOptions,
+ query_contains_error: bool,
+ dismissed: bool,
+ search_history: SearchHistory,
+ current_mode: SearchMode,
+ replace_enabled: bool,
+}
+
+impl EventEmitter<Event> for BufferSearchBar {}
+impl EventEmitter<workspace::ToolbarItemEvent> for BufferSearchBar {}
+impl Render for BufferSearchBar {
+ type Element = Div;
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+ // let query_container_style = if self.query_contains_error {
+ // theme.search.invalid_editor
+ // } else {
+ // theme.search.editor.input.container
+ // };
+ if self.dismissed {
+ return div();
+ }
+ let supported_options = self.supported_options();
+
+ let previous_query_keystrokes = cx
+ .bindings_for_action(&PreviousHistoryQuery {})
+ .into_iter()
+ .next()
+ .map(|binding| {
+ binding
+ .keystrokes()
+ .iter()
+ .map(|k| k.to_string())
+ .collect::<Vec<_>>()
+ });
+ let next_query_keystrokes = cx
+ .bindings_for_action(&NextHistoryQuery {})
+ .into_iter()
+ .next()
+ .map(|binding| {
+ binding
+ .keystrokes()
+ .iter()
+ .map(|k| k.to_string())
+ .collect::<Vec<_>>()
+ });
+ let new_placeholder_text = match (previous_query_keystrokes, next_query_keystrokes) {
+ (Some(previous_query_keystrokes), Some(next_query_keystrokes)) => {
+ format!(
+ "Search ({}/{} for previous/next query)",
+ previous_query_keystrokes.join(" "),
+ next_query_keystrokes.join(" ")
+ )
+ }
+ (None, Some(next_query_keystrokes)) => {
+ format!(
+ "Search ({} for next query)",
+ next_query_keystrokes.join(" ")
+ )
+ }
+ (Some(previous_query_keystrokes), None) => {
+ format!(
+ "Search ({} for previous query)",
+ previous_query_keystrokes.join(" ")
+ )
+ }
+ (None, None) => String::new(),
+ };
+ let new_placeholder_text = Arc::from(new_placeholder_text);
+ self.query_editor.update(cx, |editor, cx| {
+ editor.set_placeholder_text(new_placeholder_text, cx);
+ });
+ self.replacement_editor.update(cx, |editor, cx| {
+ editor.set_placeholder_text("Replace with...", cx);
+ });
+
+ let search_button_for_mode = |mode| {
+ let is_active = self.current_mode == mode;
+
+ render_search_mode_button(
+ mode,
+ is_active,
+ cx.listener(move |this, _, cx| {
+ this.activate_search_mode(mode, cx);
+ }),
+ )
+ };
+ let search_option_button = |option| {
+ let is_active = self.search_options.contains(option);
+ option.as_button(is_active)
+ };
+ let match_count = self
+ .active_searchable_item
+ .as_ref()
+ .and_then(|searchable_item| {
+ if self.query(cx).is_empty() {
+ return None;
+ }
+ let matches = self
+ .searchable_items_with_matches
+ .get(&searchable_item.downgrade())?;
+ let message = if let Some(match_ix) = self.active_match_index {
+ format!("{}/{}", match_ix + 1, matches.len())
+ } else {
+ "No matches".to_string()
+ };
+
+ Some(ui::Label::new(message))
+ });
+ let nav_button_for_direction = |icon, direction| {
+ render_nav_button(
+ icon,
+ self.active_match_index.is_some(),
+ cx.listener(move |this, _, cx| match direction {
+ Direction::Prev => this.select_prev_match(&Default::default(), cx),
+ Direction::Next => this.select_next_match(&Default::default(), cx),
+ }),
+ )
+ };
+ let should_show_replace_input = self.replace_enabled && supported_options.replacement;
+ let replace_all = should_show_replace_input
+ .then(|| super::render_replace_button(ReplaceAll, ui::Icon::ReplaceAll));
+ let replace_next = should_show_replace_input
+ .then(|| super::render_replace_button(ReplaceNext, ui::Icon::Replace));
+ let in_replace = self.replacement_editor.focus_handle(cx).is_focused(cx);
+
+ h_stack()
+ .key_context("BufferSearchBar")
+ .when(in_replace, |this| {
+ this.key_context("in_replace")
+ .on_action(cx.listener(Self::replace_next))
+ .on_action(cx.listener(Self::replace_all))
+ })
+ .on_action(cx.listener(Self::previous_history_query))
+ .on_action(cx.listener(Self::next_history_query))
+ .on_action(cx.listener(Self::dismiss))
+ .w_full()
+ .p_1()
+ .child(
+ div()
+ .flex()
+ .flex_1()
+ .border_1()
+ .border_color(red())
+ .rounded_md()
+ .items_center()
+ .child(IconElement::new(Icon::MagnifyingGlass))
+ .child(self.query_editor.clone())
+ .children(
+ supported_options
+ .case
+ .then(|| search_option_button(SearchOptions::CASE_SENSITIVE)),
+ )
+ .children(
+ supported_options
+ .word
+ .then(|| search_option_button(SearchOptions::WHOLE_WORD)),
+ ),
+ )
+ .child(
+ h_stack()
+ .flex_none()
+ .child(ButtonGroup::new(vec![
+ search_button_for_mode(SearchMode::Text),
+ search_button_for_mode(SearchMode::Regex),
+ ]))
+ .when(supported_options.replacement, |this| {
+ this.child(super::toggle_replace_button(self.replace_enabled))
+ }),
+ )
+ .child(
+ h_stack()
+ .gap_0p5()
+ .flex_1()
+ .when(self.replace_enabled, |this| {
+ this.child(self.replacement_editor.clone())
+ .children(replace_next)
+ .children(replace_all)
+ }),
+ )
+ .child(
+ h_stack()
+ .gap_0p5()
+ .flex_none()
+ .child(self.render_action_button())
+ .children(match_count)
+ .child(nav_button_for_direction(
+ ui::Icon::ChevronLeft,
+ Direction::Prev,
+ ))
+ .child(nav_button_for_direction(
+ ui::Icon::ChevronRight,
+ Direction::Next,
+ )),
+ )
+ }
+}
+
+impl ToolbarItemView for BufferSearchBar {
+ fn set_active_pane_item(
+ &mut self,
+ item: Option<&dyn ItemHandle>,
+ cx: &mut ViewContext<Self>,
+ ) -> ToolbarItemLocation {
+ cx.notify();
+ self.active_searchable_item_subscription.take();
+ self.active_searchable_item.take();
+
+ self.pending_search.take();
+
+ if let Some(searchable_item_handle) =
+ item.and_then(|item| item.to_searchable_item_handle(cx))
+ {
+ let this = cx.view().downgrade();
+
+ searchable_item_handle
+ .subscribe_to_search_events(
+ cx,
+ Box::new(move |search_event, cx| {
+ if let Some(this) = this.upgrade() {
+ this.update(cx, |this, cx| {
+ this.on_active_searchable_item_event(search_event, cx)
+ });
+ }
+ }),
+ )
+ .detach();
+
+ self.active_searchable_item = Some(searchable_item_handle);
+ let _ = self.update_matches(cx);
+ if !self.dismissed {
+ return ToolbarItemLocation::Secondary;
+ }
+ }
+ ToolbarItemLocation::Hidden
+ }
+
+ fn row_count(&self, _: &WindowContext<'_>) -> usize {
+ 1
+ }
+}
+
+impl BufferSearchBar {
+ pub fn register(editor: &mut Editor, cx: &mut ViewContext<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| {
+ if this.is_dismissed() {
+ this.show(cx);
+ } else {
+ this.dismiss(&Dismiss, cx);
+ }
+ });
+ return;
+ }
+ let view = cx.build_view(|cx| BufferSearchBar::new(cx));
+ this.add_item(view.clone(), cx);
+ view.update(cx, |this, cx| this.deploy(a, cx));
+ cx.notify();
+ })
+ });
+ });
+ fn register_action<A: Action>(
+ editor: &mut Editor,
+ handle: WeakView<Editor>,
+ update: fn(&mut BufferSearchBar, &A, &mut ViewContext<BufferSearchBar>),
+ ) {
+ 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));
+ cx.notify();
+ }
+ })
+ });
+ });
+ }
+
+ let handle = cx.view().downgrade();
+ register_action(
+ editor,
+ handle.clone(),
+ |this, action: &ToggleCaseSensitive, cx| {
+ if this.supported_options().case {
+ this.toggle_case_sensitive(action, cx);
+ }
+ },
+ );
+ register_action(
+ editor,
+ handle.clone(),
+ |this, action: &ToggleWholeWord, cx| {
+ if this.supported_options().word {
+ this.toggle_whole_word(action, cx);
+ }
+ },
+ );
+ register_action(
+ editor,
+ handle.clone(),
+ |this, action: &ToggleReplace, cx| {
+ if this.supported_options().replacement {
+ this.toggle_replace(action, cx);
+ }
+ },
+ );
+ register_action(editor, handle.clone(), |this, _: &ActivateRegexMode, cx| {
+ if this.supported_options().regex {
+ this.activate_search_mode(SearchMode::Regex, cx);
+ }
+ });
+ register_action(editor, handle.clone(), |this, _: &ActivateTextMode, cx| {
+ this.activate_search_mode(SearchMode::Text, cx);
+ });
+ register_action(editor, handle.clone(), |this, action: &CycleMode, cx| {
+ if this.supported_options().regex {
+ // If regex is not supported then search has just one mode (text) - in that case there's no point in supporting
+ // cycling.
+ this.cycle_mode(action, cx)
+ }
+ });
+ register_action(
+ editor,
+ handle.clone(),
+ |this, action: &SelectNextMatch, cx| {
+ this.select_next_match(action, cx);
+ },
+ );
+ register_action(
+ editor,
+ handle.clone(),
+ |this, action: &SelectPrevMatch, cx| {
+ this.select_prev_match(action, cx);
+ },
+ );
+ register_action(
+ editor,
+ handle.clone(),
+ |this, action: &SelectAllMatches, cx| {
+ this.select_all_matches(action, cx);
+ },
+ );
+ register_action(editor, handle.clone(), |this, _: &editor::Cancel, cx| {
+ if !this.dismissed {
+ this.dismiss(&Dismiss, cx);
+ return;
+ }
+ cx.propagate();
+ });
+ }
+ pub fn new(cx: &mut ViewContext<Self>) -> Self {
+ let query_editor = cx.build_view(|cx| Editor::single_line(cx));
+ cx.subscribe(&query_editor, Self::on_query_editor_event)
+ .detach();
+ let replacement_editor = cx.build_view(|cx| Editor::single_line(cx));
+ cx.subscribe(&replacement_editor, Self::on_query_editor_event)
+ .detach();
+ Self {
+ query_editor,
+ replacement_editor,
+ active_searchable_item: None,
+ active_searchable_item_subscription: None,
+ active_match_index: None,
+ searchable_items_with_matches: Default::default(),
+ default_options: SearchOptions::NONE,
+ search_options: SearchOptions::NONE,
+ pending_search: None,
+ query_contains_error: false,
+ dismissed: true,
+ search_history: SearchHistory::default(),
+ current_mode: SearchMode::default(),
+ active_search: None,
+ replace_enabled: false,
+ }
+ }
+
+ pub fn is_dismissed(&self) -> bool {
+ self.dismissed
+ }
+
+ pub fn dismiss(&mut self, _: &Dismiss, cx: &mut ViewContext<Self>) {
+ self.dismissed = true;
+ for searchable_item in self.searchable_items_with_matches.keys() {
+ if let Some(searchable_item) =
+ WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
+ {
+ searchable_item.clear_matches(cx);
+ }
+ }
+ if let Some(active_editor) = self.active_searchable_item.as_ref() {
+ let handle = active_editor.focus_handle(cx);
+ cx.focus(&handle);
+ }
+ cx.emit(Event::UpdateLocation);
+ cx.notify();
+ }
+
+ pub fn deploy(&mut self, deploy: &Deploy, cx: &mut ViewContext<Self>) -> bool {
+ if self.show(cx) {
+ self.search_suggested(cx);
+ if deploy.focus {
+ self.select_query(cx);
+ let handle = cx.focus_handle();
+ cx.focus(&handle);
+ }
+ return true;
+ }
+
+ false
+ }
+
+ pub fn show(&mut self, cx: &mut ViewContext<Self>) -> bool {
+ if self.active_searchable_item.is_none() {
+ return false;
+ }
+ self.dismissed = false;
+ cx.notify();
+ cx.emit(Event::UpdateLocation);
+ true
+ }
+
+ fn supported_options(&self) -> workspace::searchable::SearchOptions {
+ self.active_searchable_item
+ .as_deref()
+ .map(SearchableItemHandle::supported_options)
+ .unwrap_or_default()
+ }
+ pub fn search_suggested(&mut self, cx: &mut ViewContext<Self>) {
+ let search = self
+ .query_suggestion(cx)
+ .map(|suggestion| self.search(&suggestion, Some(self.default_options), cx));
+
+ if let Some(search) = search {
+ cx.spawn(|this, mut cx| async move {
+ search.await?;
+ this.update(&mut cx, |this, cx| this.activate_current_match(cx))
+ })
+ .detach_and_log_err(cx);
+ }
+ }
+
+ pub fn activate_current_match(&mut self, cx: &mut ViewContext<Self>) {
+ if let Some(match_ix) = self.active_match_index {
+ if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
+ if let Some(matches) = self
+ .searchable_items_with_matches
+ .get(&active_searchable_item.downgrade())
+ {
+ active_searchable_item.activate_match(match_ix, matches, cx)
+ }
+ }
+ }
+ }
+
+ pub fn select_query(&mut self, cx: &mut ViewContext<Self>) {
+ self.query_editor.update(cx, |query_editor, cx| {
+ query_editor.select_all(&Default::default(), cx);
+ });
+ }
+
+ pub fn query(&self, cx: &WindowContext) -> String {
+ self.query_editor.read(cx).text(cx)
+ }
+ pub fn replacement(&self, cx: &WindowContext) -> String {
+ self.replacement_editor.read(cx).text(cx)
+ }
+ pub fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> Option<String> {
+ self.active_searchable_item
+ .as_ref()
+ .map(|searchable_item| searchable_item.query_suggestion(cx))
+ .filter(|suggestion| !suggestion.is_empty())
+ }
+
+ pub fn set_replacement(&mut self, replacement: Option<&str>, cx: &mut ViewContext<Self>) {
+ if replacement.is_none() {
+ self.replace_enabled = false;
+ return;
+ }
+ self.replace_enabled = true;
+ self.replacement_editor
+ .update(cx, |replacement_editor, cx| {
+ replacement_editor
+ .buffer()
+ .update(cx, |replacement_buffer, cx| {
+ let len = replacement_buffer.len(cx);
+ replacement_buffer.edit([(0..len, replacement.unwrap())], None, cx);
+ });
+ });
+ }
+
+ pub fn search(
+ &mut self,
+ query: &str,
+ options: Option<SearchOptions>,
+ cx: &mut ViewContext<Self>,
+ ) -> oneshot::Receiver<()> {
+ let options = options.unwrap_or(self.default_options);
+ if query != self.query(cx) || self.search_options != options {
+ self.query_editor.update(cx, |query_editor, cx| {
+ query_editor.buffer().update(cx, |query_buffer, cx| {
+ let len = query_buffer.len(cx);
+ query_buffer.edit([(0..len, query)], None, cx);
+ });
+ });
+ self.search_options = options;
+ self.query_contains_error = false;
+ self.clear_matches(cx);
+ cx.notify();
+ }
+ self.update_matches(cx)
+ }
+
+ fn render_action_button(&self) -> impl IntoElement {
+ // let tooltip_style = theme.tooltip.clone();
+
+ // let style = theme.search.action_button.clone();
+
+ IconButton::new(0, ui::Icon::SelectAll)
+ .on_click(|_, cx| cx.dispatch_action(Box::new(SelectAllMatches)))
+ }
+
+ pub fn activate_search_mode(&mut self, mode: SearchMode, cx: &mut ViewContext<Self>) {
+ assert_ne!(
+ mode,
+ SearchMode::Semantic,
+ "Semantic search is not supported in buffer search"
+ );
+ if mode == self.current_mode {
+ return;
+ }
+ self.current_mode = mode;
+ let _ = self.update_matches(cx);
+ cx.notify();
+ }
+
+ pub fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext<Self>) {
+ if let Some(active_editor) = self.active_searchable_item.as_ref() {
+ let handle = active_editor.focus_handle(cx);
+ cx.focus(&handle);
+ }
+ }
+
+ fn toggle_search_option(&mut self, search_option: SearchOptions, cx: &mut ViewContext<Self>) {
+ self.search_options.toggle(search_option);
+ self.default_options = self.search_options;
+ let _ = self.update_matches(cx);
+ cx.notify();
+ }
+
+ pub fn set_search_options(
+ &mut self,
+ search_options: SearchOptions,
+ cx: &mut ViewContext<Self>,
+ ) {
+ self.search_options = search_options;
+ cx.notify();
+ }
+
+ fn select_next_match(&mut self, _: &SelectNextMatch, cx: &mut ViewContext<Self>) {
+ self.select_match(Direction::Next, 1, cx);
+ }
+
+ fn select_prev_match(&mut self, _: &SelectPrevMatch, cx: &mut ViewContext<Self>) {
+ self.select_match(Direction::Prev, 1, cx);
+ }
+
+ fn select_all_matches(&mut self, _: &SelectAllMatches, cx: &mut ViewContext<Self>) {
+ if !self.dismissed && self.active_match_index.is_some() {
+ if let Some(searchable_item) = self.active_searchable_item.as_ref() {
+ if let Some(matches) = self
+ .searchable_items_with_matches
+ .get(&searchable_item.downgrade())
+ {
+ searchable_item.select_matches(matches, cx);
+ self.focus_editor(&FocusEditor, cx);
+ }
+ }
+ }
+ }
+
+ pub fn select_match(&mut self, direction: Direction, count: usize, cx: &mut ViewContext<Self>) {
+ if let Some(index) = self.active_match_index {
+ if let Some(searchable_item) = self.active_searchable_item.as_ref() {
+ if let Some(matches) = self
+ .searchable_items_with_matches
+ .get(&searchable_item.downgrade())
+ {
+ let new_match_index = searchable_item
+ .match_index_for_direction(matches, index, direction, count, cx);
+
+ searchable_item.update_matches(matches, cx);
+ searchable_item.activate_match(new_match_index, matches, cx);
+ }
+ }
+ }
+ }
+
+ pub fn select_last_match(&mut self, cx: &mut ViewContext<Self>) {
+ if let Some(searchable_item) = self.active_searchable_item.as_ref() {
+ if let Some(matches) = self
+ .searchable_items_with_matches
+ .get(&searchable_item.downgrade())
+ {
+ if matches.len() == 0 {
+ return;
+ }
+ let new_match_index = matches.len() - 1;
+ searchable_item.update_matches(matches, cx);
+ searchable_item.activate_match(new_match_index, matches, cx);
+ }
+ }
+ }
+
+ fn on_query_editor_event(
+ &mut self,
+ _: View<Editor>,
+ event: &editor::EditorEvent,
+ cx: &mut ViewContext<Self>,
+ ) {
+ if let editor::EditorEvent::Edited { .. } = event {
+ self.query_contains_error = false;
+ self.clear_matches(cx);
+ let search = self.update_matches(cx);
+ cx.spawn(|this, mut cx| async move {
+ search.await?;
+ this.update(&mut cx, |this, cx| this.activate_current_match(cx))
+ })
+ .detach_and_log_err(cx);
+ }
+ }
+
+ fn on_active_searchable_item_event(&mut self, event: &SearchEvent, cx: &mut ViewContext<Self>) {
+ match event {
+ SearchEvent::MatchesInvalidated => {
+ let _ = self.update_matches(cx);
+ }
+ SearchEvent::ActiveMatchChanged => self.update_match_index(cx),
+ }
+ }
+
+ fn toggle_case_sensitive(&mut self, _: &ToggleCaseSensitive, cx: &mut ViewContext<Self>) {
+ self.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx)
+ }
+ fn toggle_whole_word(&mut self, _: &ToggleWholeWord, cx: &mut ViewContext<Self>) {
+ self.toggle_search_option(SearchOptions::WHOLE_WORD, cx)
+ }
+ fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
+ let mut active_item_matches = None;
+ for (searchable_item, matches) in self.searchable_items_with_matches.drain() {
+ if let Some(searchable_item) =
+ WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx)
+ {
+ if Some(&searchable_item) == self.active_searchable_item.as_ref() {
+ active_item_matches = Some((searchable_item.downgrade(), matches));
+ } else {
+ searchable_item.clear_matches(cx);
+ }
+ }
+ }
+
+ self.searchable_items_with_matches
+ .extend(active_item_matches);
+ }
+
+ fn update_matches(&mut self, cx: &mut ViewContext<Self>) -> oneshot::Receiver<()> {
+ let (done_tx, done_rx) = oneshot::channel();
+ let query = self.query(cx);
+ self.pending_search.take();
+
+ if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
+ if query.is_empty() {
+ self.active_match_index.take();
+ active_searchable_item.clear_matches(cx);
+ let _ = done_tx.send(());
+ cx.notify();
+ } else {
+ let query: Arc<_> = if self.current_mode == SearchMode::Regex {
+ match SearchQuery::regex(
+ query,
+ self.search_options.contains(SearchOptions::WHOLE_WORD),
+ self.search_options.contains(SearchOptions::CASE_SENSITIVE),
+ false,
+ Vec::new(),
+ Vec::new(),
+ ) {
+ Ok(query) => query.with_replacement(self.replacement(cx)),
+ Err(_) => {
+ self.query_contains_error = true;
+ cx.notify();
+ return done_rx;
+ }
+ }
+ } else {
+ match SearchQuery::text(
+ query,
+ self.search_options.contains(SearchOptions::WHOLE_WORD),
+ self.search_options.contains(SearchOptions::CASE_SENSITIVE),
+ false,
+ Vec::new(),
+ Vec::new(),
+ ) {
+ Ok(query) => query.with_replacement(self.replacement(cx)),
+ Err(_) => {
+ self.query_contains_error = true;
+ cx.notify();
+ return done_rx;
+ }
+ }
+ }
+ .into();
+ self.active_search = Some(query.clone());
+ let query_text = query.as_str().to_string();
+
+ let matches = active_searchable_item.find_matches(query, cx);
+
+ let active_searchable_item = active_searchable_item.downgrade();
+ self.pending_search = Some(cx.spawn(|this, mut cx| async move {
+ let matches = matches.await;
+
+ this.update(&mut cx, |this, cx| {
+ if let Some(active_searchable_item) =
+ WeakSearchableItemHandle::upgrade(active_searchable_item.as_ref(), cx)
+ {
+ this.searchable_items_with_matches
+ .insert(active_searchable_item.downgrade(), matches);
+
+ this.update_match_index(cx);
+ this.search_history.add(query_text);
+ if !this.dismissed {
+ let matches = this
+ .searchable_items_with_matches
+ .get(&active_searchable_item.downgrade())
+ .unwrap();
+ active_searchable_item.update_matches(matches, cx);
+ let _ = done_tx.send(());
+ }
+ cx.notify();
+ }
+ })
+ .log_err();
+ }));
+ }
+ }
+ done_rx
+ }
+
+ fn update_match_index(&mut self, cx: &mut ViewContext<Self>) {
+ let new_index = self
+ .active_searchable_item
+ .as_ref()
+ .and_then(|searchable_item| {
+ let matches = self
+ .searchable_items_with_matches
+ .get(&searchable_item.downgrade())?;
+ searchable_item.active_match_index(matches, cx)
+ });
+ if new_index != self.active_match_index {
+ self.active_match_index = new_index;
+ cx.notify();
+ }
+ }
+
+ fn next_history_query(&mut self, _: &NextHistoryQuery, cx: &mut ViewContext<Self>) {
+ if let Some(new_query) = self.search_history.next().map(str::to_string) {
+ let _ = self.search(&new_query, Some(self.search_options), cx);
+ } else {
+ self.search_history.reset_selection();
+ let _ = self.search("", Some(self.search_options), cx);
+ }
+ }
+
+ fn previous_history_query(&mut self, _: &PreviousHistoryQuery, cx: &mut ViewContext<Self>) {
+ if self.query(cx).is_empty() {
+ if let Some(new_query) = self.search_history.current().map(str::to_string) {
+ let _ = self.search(&new_query, Some(self.search_options), cx);
+ return;
+ }
+ }
+
+ if let Some(new_query) = self.search_history.previous().map(str::to_string) {
+ let _ = self.search(&new_query, Some(self.search_options), cx);
+ }
+ }
+ fn cycle_mode(&mut self, _: &CycleMode, cx: &mut ViewContext<Self>) {
+ self.activate_search_mode(next_mode(&self.current_mode, false), cx);
+ }
+ fn toggle_replace(&mut self, _: &ToggleReplace, cx: &mut ViewContext<Self>) {
+ if let Some(_) = &self.active_searchable_item {
+ self.replace_enabled = !self.replace_enabled;
+ if !self.replace_enabled {
+ let handle = self.query_editor.focus_handle(cx);
+ cx.focus(&handle);
+ }
+ cx.notify();
+ }
+ }
+ fn replace_next(&mut self, _: &ReplaceNext, cx: &mut ViewContext<Self>) {
+ let mut should_propagate = true;
+ if !self.dismissed && self.active_search.is_some() {
+ if let Some(searchable_item) = self.active_searchable_item.as_ref() {
+ if let Some(query) = self.active_search.as_ref() {
+ if let Some(matches) = self
+ .searchable_items_with_matches
+ .get(&searchable_item.downgrade())
+ {
+ if let Some(active_index) = self.active_match_index {
+ let query = query
+ .as_ref()
+ .clone()
+ .with_replacement(self.replacement(cx));
+ searchable_item.replace(&matches[active_index], &query, cx);
+ self.select_next_match(&SelectNextMatch, cx);
+ }
+ should_propagate = false;
+ self.focus_editor(&FocusEditor, cx);
+ }
+ }
+ }
+ }
+ if !should_propagate {
+ cx.stop_propagation();
+ }
+ }
+ pub fn replace_all(&mut self, _: &ReplaceAll, cx: &mut ViewContext<Self>) {
+ if !self.dismissed && self.active_search.is_some() {
+ if let Some(searchable_item) = self.active_searchable_item.as_ref() {
+ if let Some(query) = self.active_search.as_ref() {
+ if let Some(matches) = self
+ .searchable_items_with_matches
+ .get(&searchable_item.downgrade())
+ {
+ let query = query
+ .as_ref()
+ .clone()
+ .with_replacement(self.replacement(cx));
+ for m in matches {
+ searchable_item.replace(m, &query, cx);
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use std::ops::Range;
+
+ use super::*;
+ use editor::{DisplayPoint, Editor};
+ use gpui::{Context, EmptyView, Hsla, TestAppContext, VisualTestContext};
+ use language::Buffer;
+ use smol::stream::StreamExt as _;
+ use unindent::Unindent as _;
+
+ fn init_globals(cx: &mut TestAppContext) {
+ cx.update(|cx| {
+ let store = settings::SettingsStore::test(cx);
+ cx.set_global(store);
+ editor::init(cx);
+
+ language::init(cx);
+ theme::init(theme::LoadThemes::JustBase, cx);
+ });
+ }
+ fn init_test(
+ cx: &mut TestAppContext,
+ ) -> (
+ View<Editor>,
+ View<BufferSearchBar>,
+ &mut VisualTestContext<'_>,
+ ) {
+ init_globals(cx);
+ let buffer = cx.build_model(|cx| {
+ Buffer::new(
+ 0,
+ cx.entity_id().as_u64(),
+ r#"
+ A regular expression (shortened as regex or regexp;[1] also referred to as
+ rational expression[2][3]) is a sequence of characters that specifies a search
+ pattern in text. Usually such patterns are used by string-searching algorithms
+ for "find" or "find and replace" operations on strings, or for input validation.
+ "#
+ .unindent(),
+ )
+ });
+ let (_, cx) = cx.add_window_view(|_| EmptyView {});
+ let editor = cx.build_view(|cx| Editor::for_buffer(buffer.clone(), None, cx));
+
+ let search_bar = cx.build_view(|cx| {
+ let mut search_bar = BufferSearchBar::new(cx);
+ search_bar.set_active_pane_item(Some(&editor), cx);
+ search_bar.show(cx);
+ search_bar
+ });
+
+ (editor, search_bar, cx)
+ }
+
+ #[gpui::test]
+ async fn test_search_simple(cx: &mut TestAppContext) {
+ let (editor, search_bar, cx) = init_test(cx);
+ // todo! osiewicz: these tests asserted on background color as well, that should be brought back.
+ let display_points_of = |background_highlights: Vec<(Range<DisplayPoint>, Hsla)>| {
+ background_highlights
+ .into_iter()
+ .map(|(range, _)| range)
+ .collect::<Vec<_>>()
+ };
+ // Search for a string that appears with different casing.
+ // By default, search is case-insensitive.
+ search_bar
+ .update(cx, |search_bar, cx| search_bar.search("us", None, cx))
+ .await
+ .unwrap();
+ editor.update(cx, |editor, cx| {
+ assert_eq!(
+ display_points_of(editor.all_text_background_highlights(cx)),
+ &[
+ DisplayPoint::new(2, 17)..DisplayPoint::new(2, 19),
+ DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),
+ ]
+ );
+ });
+
+ // Switch to a case sensitive search.
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx);
+ });
+ let mut editor_notifications = cx.notifications(&editor);
+ editor_notifications.next().await;
+ editor.update(cx, |editor, cx| {
+ assert_eq!(
+ display_points_of(editor.all_text_background_highlights(cx)),
+ &[DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),]
+ );
+ });
+
+ // Search for a string that appears both as a whole word and
+ // within other words. By default, all results are found.
+ search_bar
+ .update(cx, |search_bar, cx| search_bar.search("or", None, cx))
+ .await
+ .unwrap();
+ editor.update(cx, |editor, cx| {
+ assert_eq!(
+ display_points_of(editor.all_text_background_highlights(cx)),
+ &[
+ DisplayPoint::new(0, 24)..DisplayPoint::new(0, 26),
+ DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
+ DisplayPoint::new(2, 71)..DisplayPoint::new(2, 73),
+ DisplayPoint::new(3, 1)..DisplayPoint::new(3, 3),
+ DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
+ DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
+ DisplayPoint::new(3, 60)..DisplayPoint::new(3, 62),
+ ]
+ );
+ });
+
+ // Switch to a whole word search.
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx);
+ });
+ let mut editor_notifications = cx.notifications(&editor);
+ editor_notifications.next().await;
+ editor.update(cx, |editor, cx| {
+ assert_eq!(
+ display_points_of(editor.all_text_background_highlights(cx)),
+ &[
+ DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43),
+ DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13),
+ DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58),
+ ]
+ );
+ });
+
+ editor.update(cx, |editor, cx| {
+ editor.change_selections(None, cx, |s| {
+ s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)])
+ });
+ });
+ search_bar.update(cx, |search_bar, cx| {
+ assert_eq!(search_bar.active_match_index, Some(0));
+ search_bar.select_next_match(&SelectNextMatch, cx);
+ assert_eq!(
+ editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
+ [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
+ );
+ });
+ search_bar.update(cx, |search_bar, _| {
+ assert_eq!(search_bar.active_match_index, Some(0));
+ });
+
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.select_next_match(&SelectNextMatch, cx);
+ assert_eq!(
+ editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
+ [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
+ );
+ });
+ search_bar.update(cx, |search_bar, _| {
+ assert_eq!(search_bar.active_match_index, Some(1));
+ });
+
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.select_next_match(&SelectNextMatch, cx);
+ assert_eq!(
+ editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
+ [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
+ );
+ });
+ search_bar.update(cx, |search_bar, _| {
+ assert_eq!(search_bar.active_match_index, Some(2));
+ });
+
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.select_next_match(&SelectNextMatch, cx);
+ assert_eq!(
+ editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
+ [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
+ );
+ });
+ search_bar.update(cx, |search_bar, _| {
+ assert_eq!(search_bar.active_match_index, Some(0));
+ });
+
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.select_prev_match(&SelectPrevMatch, cx);
+ assert_eq!(
+ editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
+ [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
+ );
+ });
+ search_bar.update(cx, |search_bar, _| {
+ assert_eq!(search_bar.active_match_index, Some(2));
+ });
+
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.select_prev_match(&SelectPrevMatch, cx);
+ assert_eq!(
+ editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
+ [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
+ );
+ });
+ search_bar.update(cx, |search_bar, _| {
+ assert_eq!(search_bar.active_match_index, Some(1));
+ });
+
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.select_prev_match(&SelectPrevMatch, cx);
+ assert_eq!(
+ editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
+ [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
+ );
+ });
+ search_bar.update(cx, |search_bar, _| {
+ assert_eq!(search_bar.active_match_index, Some(0));
+ });
+
+ // Park the cursor in between matches and ensure that going to the previous match selects
+ // the closest match to the left.
+ editor.update(cx, |editor, cx| {
+ editor.change_selections(None, cx, |s| {
+ s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
+ });
+ });
+ search_bar.update(cx, |search_bar, cx| {
+ assert_eq!(search_bar.active_match_index, Some(1));
+ search_bar.select_prev_match(&SelectPrevMatch, cx);
+ assert_eq!(
+ editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
+ [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
+ );
+ });
+ search_bar.update(cx, |search_bar, _| {
+ assert_eq!(search_bar.active_match_index, Some(0));
+ });
+
+ // Park the cursor in between matches and ensure that going to the next match selects the
+ // closest match to the right.
+ editor.update(cx, |editor, cx| {
+ editor.change_selections(None, cx, |s| {
+ s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
+ });
+ });
+ search_bar.update(cx, |search_bar, cx| {
+ assert_eq!(search_bar.active_match_index, Some(1));
+ search_bar.select_next_match(&SelectNextMatch, cx);
+ assert_eq!(
+ editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
+ [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)]
+ );
+ });
+ search_bar.update(cx, |search_bar, _| {
+ assert_eq!(search_bar.active_match_index, Some(1));
+ });
+
+ // Park the cursor after the last match and ensure that going to the previous match selects
+ // the last match.
+ editor.update(cx, |editor, cx| {
+ editor.change_selections(None, cx, |s| {
+ s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)])
+ });
+ });
+ search_bar.update(cx, |search_bar, cx| {
+ assert_eq!(search_bar.active_match_index, Some(2));
+ search_bar.select_prev_match(&SelectPrevMatch, cx);
+ assert_eq!(
+ editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
+ [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
+ );
+ });
+ search_bar.update(cx, |search_bar, _| {
+ assert_eq!(search_bar.active_match_index, Some(2));
+ });
+
+ // Park the cursor after the last match and ensure that going to the next match selects the
+ // first match.
+ editor.update(cx, |editor, cx| {
+ editor.change_selections(None, cx, |s| {
+ s.select_display_ranges([DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)])
+ });
+ });
+ search_bar.update(cx, |search_bar, cx| {
+ assert_eq!(search_bar.active_match_index, Some(2));
+ search_bar.select_next_match(&SelectNextMatch, cx);
+ assert_eq!(
+ editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
+ [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)]
+ );
+ });
+ search_bar.update(cx, |search_bar, _| {
+ assert_eq!(search_bar.active_match_index, Some(0));
+ });
+
+ // Park the cursor before the first match and ensure that going to the previous match
+ // selects the last match.
+ editor.update(cx, |editor, cx| {
+ editor.change_selections(None, cx, |s| {
+ s.select_display_ranges([DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)])
+ });
+ });
+ search_bar.update(cx, |search_bar, cx| {
+ assert_eq!(search_bar.active_match_index, Some(0));
+ search_bar.select_prev_match(&SelectPrevMatch, cx);
+ assert_eq!(
+ editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
+ [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)]
+ );
+ });
+ search_bar.update(cx, |search_bar, _| {
+ assert_eq!(search_bar.active_match_index, Some(2));
+ });
+ }
+
+ #[gpui::test]
+ async fn test_search_option_handling(cx: &mut TestAppContext) {
+ let (editor, search_bar, cx) = init_test(cx);
+
+ // show with options should make current search case sensitive
+ search_bar
+ .update(cx, |search_bar, cx| {
+ search_bar.show(cx);
+ search_bar.search("us", Some(SearchOptions::CASE_SENSITIVE), cx)
+ })
+ .await
+ .unwrap();
+ // todo! osiewicz: these tests previously asserted on background color highlights; that should be introduced back.
+ let display_points_of = |background_highlights: Vec<(Range<DisplayPoint>, Hsla)>| {
+ background_highlights
+ .into_iter()
+ .map(|(range, _)| range)
+ .collect::<Vec<_>>()
+ };
+ editor.update(cx, |editor, cx| {
+ assert_eq!(
+ display_points_of(editor.all_text_background_highlights(cx)),
+ &[DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45),]
+ );
+ });
+
+ // search_suggested should restore default options
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.search_suggested(cx);
+ assert_eq!(search_bar.search_options, SearchOptions::NONE)
+ });
+
+ // toggling a search option should update the defaults
+ search_bar
+ .update(cx, |search_bar, cx| {
+ search_bar.search("regex", Some(SearchOptions::CASE_SENSITIVE), cx)
+ })
+ .await
+ .unwrap();
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx)
+ });
+ let mut editor_notifications = cx.notifications(&editor);
+ editor_notifications.next().await;
+ editor.update(cx, |editor, cx| {
+ assert_eq!(
+ display_points_of(editor.all_text_background_highlights(cx)),
+ &[DisplayPoint::new(0, 35)..DisplayPoint::new(0, 40),]
+ );
+ });
+
+ // defaults should still include whole word
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.search_suggested(cx);
+ assert_eq!(
+ search_bar.search_options,
+ SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD
+ )
+ });
+ }
+
+ #[gpui::test]
+ async fn test_search_select_all_matches(cx: &mut TestAppContext) {
+ init_globals(cx);
+ let buffer_text = r#"
+ A regular expression (shortened as regex or regexp;[1] also referred to as
+ rational expression[2][3]) is a sequence of characters that specifies a search
+ pattern in text. Usually such patterns are used by string-searching algorithms
+ for "find" or "find and replace" operations on strings, or for input validation.
+ "#
+ .unindent();
+ let expected_query_matches_count = buffer_text
+ .chars()
+ .filter(|c| c.to_ascii_lowercase() == 'a')
+ .count();
+ assert!(
+ expected_query_matches_count > 1,
+ "Should pick a query with multiple results"
+ );
+ let buffer = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), buffer_text));
+ let window = cx.add_window(|_| EmptyView {});
+
+ let editor = window.build_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx));
+
+ let search_bar = window.build_view(cx, |cx| {
+ let mut search_bar = BufferSearchBar::new(cx);
+ search_bar.set_active_pane_item(Some(&editor), cx);
+ search_bar.show(cx);
+ search_bar
+ });
+
+ window
+ .update(cx, |_, cx| {
+ search_bar.update(cx, |search_bar, cx| search_bar.search("a", None, cx))
+ })
+ .unwrap()
+ .await
+ .unwrap();
+ let initial_selections = window
+ .update(cx, |_, cx| {
+ search_bar.update(cx, |search_bar, cx| {
+ let handle = search_bar.query_editor.focus_handle(cx);
+ cx.focus(&handle);
+ search_bar.activate_current_match(cx);
+ });
+ assert!(
+ !editor.read(cx).is_focused(cx),
+ "Initially, the editor should not be focused"
+ );
+ let initial_selections = editor.update(cx, |editor, cx| {
+ let initial_selections = editor.selections.display_ranges(cx);
+ assert_eq!(
+ initial_selections.len(), 1,
+ "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}",
+ );
+ initial_selections
+ });
+ search_bar.update(cx, |search_bar, cx| {
+ assert_eq!(search_bar.active_match_index, Some(0));
+ let handle = search_bar.query_editor.focus_handle(cx);
+ cx.focus(&handle);
+ search_bar.select_all_matches(&SelectAllMatches, cx);
+ });
+ assert!(
+ editor.read(cx).is_focused(cx),
+ "Should focus editor after successful SelectAllMatches"
+ );
+ search_bar.update(cx, |search_bar, cx| {
+ let all_selections =
+ editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
+ assert_eq!(
+ all_selections.len(),
+ expected_query_matches_count,
+ "Should select all `a` characters in the buffer, but got: {all_selections:?}"
+ );
+ assert_eq!(
+ search_bar.active_match_index,
+ Some(0),
+ "Match index should not change after selecting all matches"
+ );
+ });
+
+ search_bar.update(cx, |this, cx| this.select_next_match(&SelectNextMatch, cx));
+ initial_selections
+ }).unwrap();
+
+ window
+ .update(cx, |_, cx| {
+ assert!(
+ editor.read(cx).is_focused(cx),
+ "Should still have editor focused after SelectNextMatch"
+ );
+ search_bar.update(cx, |search_bar, cx| {
+ let all_selections =
+ editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
+ assert_eq!(
+ all_selections.len(),
+ 1,
+ "On next match, should deselect items and select the next match"
+ );
+ assert_ne!(
+ all_selections, initial_selections,
+ "Next match should be different from the first selection"
+ );
+ assert_eq!(
+ search_bar.active_match_index,
+ Some(1),
+ "Match index should be updated to the next one"
+ );
+ let handle = search_bar.query_editor.focus_handle(cx);
+ cx.focus(&handle);
+ search_bar.select_all_matches(&SelectAllMatches, cx);
+ });
+ })
+ .unwrap();
+ window
+ .update(cx, |_, cx| {
+ assert!(
+ editor.read(cx).is_focused(cx),
+ "Should focus editor after successful SelectAllMatches"
+ );
+ search_bar.update(cx, |search_bar, cx| {
+ let all_selections =
+ editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
+ assert_eq!(
+ all_selections.len(),
+ expected_query_matches_count,
+ "Should select all `a` characters in the buffer, but got: {all_selections:?}"
+ );
+ assert_eq!(
+ search_bar.active_match_index,
+ Some(1),
+ "Match index should not change after selecting all matches"
+ );
+ });
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.select_prev_match(&SelectPrevMatch, cx);
+ });
+ })
+ .unwrap();
+ let last_match_selections = window
+ .update(cx, |_, cx| {
+ assert!(
+ editor.read(cx).is_focused(&cx),
+ "Should still have editor focused after SelectPrevMatch"
+ );
+
+ search_bar.update(cx, |search_bar, cx| {
+ let all_selections =
+ editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
+ assert_eq!(
+ all_selections.len(),
+ 1,
+ "On previous match, should deselect items and select the previous item"
+ );
+ assert_eq!(
+ all_selections, initial_selections,
+ "Previous match should be the same as the first selection"
+ );
+ assert_eq!(
+ search_bar.active_match_index,
+ Some(0),
+ "Match index should be updated to the previous one"
+ );
+ all_selections
+ })
+ })
+ .unwrap();
+
+ window
+ .update(cx, |_, cx| {
+ search_bar.update(cx, |search_bar, cx| {
+ let handle = search_bar.query_editor.focus_handle(cx);
+ cx.focus(&handle);
+ search_bar.search("abas_nonexistent_match", None, cx)
+ })
+ })
+ .unwrap()
+ .await
+ .unwrap();
+ window
+ .update(cx, |_, cx| {
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.select_all_matches(&SelectAllMatches, cx);
+ });
+ assert!(
+ editor.update(cx, |this, cx| !this.is_focused(cx.window_context())),
+ "Should not switch focus to editor if SelectAllMatches does not find any matches"
+ );
+ search_bar.update(cx, |search_bar, cx| {
+ let all_selections =
+ editor.update(cx, |editor, cx| editor.selections.display_ranges(cx));
+ assert_eq!(
+ all_selections, last_match_selections,
+ "Should not select anything new if there are no matches"
+ );
+ assert!(
+ search_bar.active_match_index.is_none(),
+ "For no matches, there should be no active match index"
+ );
+ });
+ })
+ .unwrap();
+ }
+
+ #[gpui::test]
+ async fn test_search_query_history(cx: &mut TestAppContext) {
+ //crate::project_search::tests::init_test(cx);
+ init_globals(cx);
+ let buffer_text = r#"
+ A regular expression (shortened as regex or regexp;[1] also referred to as
+ rational expression[2][3]) is a sequence of characters that specifies a search
+ pattern in text. Usually such patterns are used by string-searching algorithms
+ for "find" or "find and replace" operations on strings, or for input validation.
+ "#
+ .unindent();
+ let buffer = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), buffer_text));
+ let (_, cx) = cx.add_window_view(|_| EmptyView {});
+
+ let editor = cx.build_view(|cx| Editor::for_buffer(buffer.clone(), None, cx));
+
+ let search_bar = cx.build_view(|cx| {
+ let mut search_bar = BufferSearchBar::new(cx);
+ search_bar.set_active_pane_item(Some(&editor), cx);
+ search_bar.show(cx);
+ search_bar
+ });
+
+ // Add 3 search items into the history.
+ search_bar
+ .update(cx, |search_bar, cx| search_bar.search("a", None, cx))
+ .await
+ .unwrap();
+ search_bar
+ .update(cx, |search_bar, cx| search_bar.search("b", None, cx))
+ .await
+ .unwrap();
+ search_bar
+ .update(cx, |search_bar, cx| {
+ search_bar.search("c", Some(SearchOptions::CASE_SENSITIVE), cx)
+ })
+ .await
+ .unwrap();
+ // Ensure that the latest search is active.
+ search_bar.update(cx, |search_bar, cx| {
+ assert_eq!(search_bar.query(cx), "c");
+ assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
+ });
+
+ // Next history query after the latest should set the query to the empty string.
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.next_history_query(&NextHistoryQuery, cx);
+ });
+ search_bar.update(cx, |search_bar, cx| {
+ assert_eq!(search_bar.query(cx), "");
+ assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
+ });
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.next_history_query(&NextHistoryQuery, cx);
+ });
+ search_bar.update(cx, |search_bar, cx| {
+ assert_eq!(search_bar.query(cx), "");
+ assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
+ });
+
+ // First previous query for empty current query should set the query to the latest.
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.previous_history_query(&PreviousHistoryQuery, cx);
+ });
+ search_bar.update(cx, |search_bar, cx| {
+ assert_eq!(search_bar.query(cx), "c");
+ assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
+ });
+
+ // Further previous items should go over the history in reverse order.
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.previous_history_query(&PreviousHistoryQuery, cx);
+ });
+ search_bar.update(cx, |search_bar, cx| {
+ assert_eq!(search_bar.query(cx), "b");
+ assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
+ });
+
+ // Previous items should never go behind the first history item.
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.previous_history_query(&PreviousHistoryQuery, cx);
+ });
+ search_bar.update(cx, |search_bar, cx| {
+ assert_eq!(search_bar.query(cx), "a");
+ assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
+ });
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.previous_history_query(&PreviousHistoryQuery, cx);
+ });
+ search_bar.update(cx, |search_bar, cx| {
+ assert_eq!(search_bar.query(cx), "a");
+ assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
+ });
+
+ // Next items should go over the history in the original order.
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.next_history_query(&NextHistoryQuery, cx);
+ });
+ search_bar.update(cx, |search_bar, cx| {
+ assert_eq!(search_bar.query(cx), "b");
+ assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE);
+ });
+
+ search_bar
+ .update(cx, |search_bar, cx| search_bar.search("ba", None, cx))
+ .await
+ .unwrap();
+ search_bar.update(cx, |search_bar, cx| {
+ assert_eq!(search_bar.query(cx), "ba");
+ assert_eq!(search_bar.search_options, SearchOptions::NONE);
+ });
+
+ // New search input should add another entry to history and move the selection to the end of the history.
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.previous_history_query(&PreviousHistoryQuery, cx);
+ });
+ search_bar.update(cx, |search_bar, cx| {
+ assert_eq!(search_bar.query(cx), "c");
+ assert_eq!(search_bar.search_options, SearchOptions::NONE);
+ });
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.previous_history_query(&PreviousHistoryQuery, cx);
+ });
+ search_bar.update(cx, |search_bar, cx| {
+ assert_eq!(search_bar.query(cx), "b");
+ assert_eq!(search_bar.search_options, SearchOptions::NONE);
+ });
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.next_history_query(&NextHistoryQuery, cx);
+ });
+ search_bar.update(cx, |search_bar, cx| {
+ assert_eq!(search_bar.query(cx), "c");
+ assert_eq!(search_bar.search_options, SearchOptions::NONE);
+ });
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.next_history_query(&NextHistoryQuery, cx);
+ });
+ search_bar.update(cx, |search_bar, cx| {
+ assert_eq!(search_bar.query(cx), "ba");
+ assert_eq!(search_bar.search_options, SearchOptions::NONE);
+ });
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.next_history_query(&NextHistoryQuery, cx);
+ });
+ search_bar.update(cx, |search_bar, cx| {
+ assert_eq!(search_bar.query(cx), "");
+ assert_eq!(search_bar.search_options, SearchOptions::NONE);
+ });
+ }
+ #[gpui::test]
+ async fn test_replace_simple(cx: &mut TestAppContext) {
+ let (editor, search_bar, cx) = init_test(cx);
+
+ search_bar
+ .update(cx, |search_bar, cx| {
+ search_bar.search("expression", None, cx)
+ })
+ .await
+ .unwrap();
+
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.replacement_editor.update(cx, |editor, cx| {
+ // We use $1 here as initially we should be in Text mode, where `$1` should be treated literally.
+ editor.set_text("expr$1", cx);
+ });
+ search_bar.replace_all(&ReplaceAll, cx)
+ });
+ assert_eq!(
+ editor.update(cx, |this, cx| { this.text(cx) }),
+ r#"
+ A regular expr$1 (shortened as regex or regexp;[1] also referred to as
+ rational expr$1[2][3]) is a sequence of characters that specifies a search
+ pattern in text. Usually such patterns are used by string-searching algorithms
+ for "find" or "find and replace" operations on strings, or for input validation.
+ "#
+ .unindent()
+ );
+
+ // Search for word boundaries and replace just a single one.
+ search_bar
+ .update(cx, |search_bar, cx| {
+ search_bar.search("or", Some(SearchOptions::WHOLE_WORD), cx)
+ })
+ .await
+ .unwrap();
+
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.replacement_editor.update(cx, |editor, cx| {
+ editor.set_text("banana", cx);
+ });
+ search_bar.replace_next(&ReplaceNext, cx)
+ });
+ // Notice how the first or in the text (shORtened) is not replaced. Neither are the remaining hits of `or` in the text.
+ assert_eq!(
+ editor.update(cx, |this, cx| { this.text(cx) }),
+ r#"
+ A regular expr$1 (shortened as regex banana regexp;[1] also referred to as
+ rational expr$1[2][3]) is a sequence of characters that specifies a search
+ pattern in text. Usually such patterns are used by string-searching algorithms
+ for "find" or "find and replace" operations on strings, or for input validation.
+ "#
+ .unindent()
+ );
+ // Let's turn on regex mode.
+ search_bar
+ .update(cx, |search_bar, cx| {
+ search_bar.activate_search_mode(SearchMode::Regex, cx);
+ search_bar.search("\\[([^\\]]+)\\]", None, cx)
+ })
+ .await
+ .unwrap();
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.replacement_editor.update(cx, |editor, cx| {
+ editor.set_text("${1}number", cx);
+ });
+ search_bar.replace_all(&ReplaceAll, cx)
+ });
+ assert_eq!(
+ editor.update(cx, |this, cx| { this.text(cx) }),
+ r#"
+ A regular expr$1 (shortened as regex banana regexp;1number also referred to as
+ rational expr$12number3number) is a sequence of characters that specifies a search
+ pattern in text. Usually such patterns are used by string-searching algorithms
+ for "find" or "find and replace" operations on strings, or for input validation.
+ "#
+ .unindent()
+ );
+ // Now with a whole-word twist.
+ search_bar
+ .update(cx, |search_bar, cx| {
+ search_bar.activate_search_mode(SearchMode::Regex, cx);
+ search_bar.search("a\\w+s", Some(SearchOptions::WHOLE_WORD), cx)
+ })
+ .await
+ .unwrap();
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.replacement_editor.update(cx, |editor, cx| {
+ editor.set_text("things", cx);
+ });
+ search_bar.replace_all(&ReplaceAll, cx)
+ });
+ // The only word affected by this edit should be `algorithms`, even though there's a bunch
+ // of words in this text that would match this regex if not for WHOLE_WORD.
+ assert_eq!(
+ editor.update(cx, |this, cx| { this.text(cx) }),
+ r#"
+ A regular expr$1 (shortened as regex banana regexp;1number also referred to as
+ rational expr$12number3number) is a sequence of characters that specifies a search
+ pattern in text. Usually such patterns are used by string-searching things
+ for "find" or "find and replace" operations on strings, or for input validation.
+ "#
+ .unindent()
+ );
+ }
+}
@@ -0,0 +1,184 @@
+use smallvec::SmallVec;
+const SEARCH_HISTORY_LIMIT: usize = 20;
+
+#[derive(Default, Debug, Clone)]
+pub struct SearchHistory {
+ history: SmallVec<[String; SEARCH_HISTORY_LIMIT]>,
+ selected: Option<usize>,
+}
+
+impl SearchHistory {
+ pub fn add(&mut self, search_string: String) {
+ if let Some(i) = self.selected {
+ if search_string == self.history[i] {
+ return;
+ }
+ }
+
+ if let Some(previously_searched) = self.history.last_mut() {
+ if search_string.find(previously_searched.as_str()).is_some() {
+ *previously_searched = search_string;
+ self.selected = Some(self.history.len() - 1);
+ return;
+ }
+ }
+
+ self.history.push(search_string);
+ if self.history.len() > SEARCH_HISTORY_LIMIT {
+ self.history.remove(0);
+ }
+ self.selected = Some(self.history.len() - 1);
+ }
+
+ pub fn next(&mut self) -> Option<&str> {
+ let history_size = self.history.len();
+ if history_size == 0 {
+ return None;
+ }
+
+ let selected = self.selected?;
+ if selected == history_size - 1 {
+ return None;
+ }
+ let next_index = selected + 1;
+ self.selected = Some(next_index);
+ Some(&self.history[next_index])
+ }
+
+ pub fn current(&self) -> Option<&str> {
+ Some(&self.history[self.selected?])
+ }
+
+ pub fn previous(&mut self) -> Option<&str> {
+ let history_size = self.history.len();
+ if history_size == 0 {
+ return None;
+ }
+
+ let prev_index = match self.selected {
+ Some(selected_index) => {
+ if selected_index == 0 {
+ return None;
+ } else {
+ selected_index - 1
+ }
+ }
+ None => history_size - 1,
+ };
+
+ self.selected = Some(prev_index);
+ Some(&self.history[prev_index])
+ }
+
+ pub fn reset_selection(&mut self) {
+ self.selected = None;
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_add() {
+ let mut search_history = SearchHistory::default();
+ assert_eq!(
+ search_history.current(),
+ None,
+ "No current selection should be set fo the default search history"
+ );
+
+ search_history.add("rust".to_string());
+ assert_eq!(
+ search_history.current(),
+ Some("rust"),
+ "Newly added item should be selected"
+ );
+
+ // check if duplicates are not added
+ search_history.add("rust".to_string());
+ assert_eq!(
+ search_history.history.len(),
+ 1,
+ "Should not add a duplicate"
+ );
+ assert_eq!(search_history.current(), Some("rust"));
+
+ // check if new string containing the previous string replaces it
+ search_history.add("rustlang".to_string());
+ assert_eq!(
+ search_history.history.len(),
+ 1,
+ "Should replace previous item if it's a substring"
+ );
+ assert_eq!(search_history.current(), Some("rustlang"));
+
+ // push enough items to test SEARCH_HISTORY_LIMIT
+ for i in 0..SEARCH_HISTORY_LIMIT * 2 {
+ search_history.add(format!("item{i}"));
+ }
+ assert!(search_history.history.len() <= SEARCH_HISTORY_LIMIT);
+ }
+
+ #[test]
+ fn test_next_and_previous() {
+ let mut search_history = SearchHistory::default();
+ assert_eq!(
+ search_history.next(),
+ None,
+ "Default search history should not have a next item"
+ );
+
+ search_history.add("Rust".to_string());
+ assert_eq!(search_history.next(), None);
+ search_history.add("JavaScript".to_string());
+ assert_eq!(search_history.next(), None);
+ search_history.add("TypeScript".to_string());
+ assert_eq!(search_history.next(), None);
+
+ assert_eq!(search_history.current(), Some("TypeScript"));
+
+ assert_eq!(search_history.previous(), Some("JavaScript"));
+ assert_eq!(search_history.current(), Some("JavaScript"));
+
+ assert_eq!(search_history.previous(), Some("Rust"));
+ assert_eq!(search_history.current(), Some("Rust"));
+
+ assert_eq!(search_history.previous(), None);
+ assert_eq!(search_history.current(), Some("Rust"));
+
+ assert_eq!(search_history.next(), Some("JavaScript"));
+ assert_eq!(search_history.current(), Some("JavaScript"));
+
+ assert_eq!(search_history.next(), Some("TypeScript"));
+ assert_eq!(search_history.current(), Some("TypeScript"));
+
+ assert_eq!(search_history.next(), None);
+ assert_eq!(search_history.current(), Some("TypeScript"));
+ }
+
+ #[test]
+ fn test_reset_selection() {
+ let mut search_history = SearchHistory::default();
+ search_history.add("Rust".to_string());
+ search_history.add("JavaScript".to_string());
+ search_history.add("TypeScript".to_string());
+
+ assert_eq!(search_history.current(), Some("TypeScript"));
+ search_history.reset_selection();
+ assert_eq!(search_history.current(), None);
+ assert_eq!(
+ search_history.previous(),
+ Some("TypeScript"),
+ "Should start from the end after reset on previous item query"
+ );
+
+ search_history.previous();
+ assert_eq!(search_history.current(), Some("JavaScript"));
+ search_history.previous();
+ assert_eq!(search_history.current(), Some("Rust"));
+
+ search_history.reset_selection();
+ assert_eq!(search_history.current(), None);
+ }
+}
@@ -0,0 +1,32 @@
+// TODO: Update the default search mode to get from config
+#[derive(Copy, Clone, Debug, Default, PartialEq)]
+pub enum SearchMode {
+ #[default]
+ Text,
+ Semantic,
+ Regex,
+}
+
+impl SearchMode {
+ pub(crate) fn label(&self) -> &'static str {
+ match self {
+ SearchMode::Text => "Text",
+ SearchMode::Semantic => "Semantic",
+ SearchMode::Regex => "Regex",
+ }
+ }
+}
+
+pub(crate) fn next_mode(mode: &SearchMode, semantic_enabled: bool) -> SearchMode {
+ match mode {
+ SearchMode::Text => SearchMode::Regex,
+ SearchMode::Regex => {
+ if semantic_enabled {
+ SearchMode::Semantic
+ } else {
+ SearchMode::Text
+ }
+ }
+ SearchMode::Semantic => SearchMode::Text,
+ }
+}
@@ -0,0 +1,2680 @@
+use crate::{
+ history::SearchHistory,
+ mode::{SearchMode, Side},
+ search_bar::{render_nav_button, render_option_button_icon, render_search_mode_button},
+ ActivateRegexMode, ActivateSemanticMode, ActivateTextMode, CycleMode, NextHistoryQuery,
+ PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOptions, SelectNextMatch, SelectPrevMatch,
+ ToggleCaseSensitive, ToggleReplace, ToggleWholeWord,
+};
+use anyhow::{Context, Result};
+use collections::HashMap;
+use editor::{
+ items::active_match_index, scroll::autoscroll::Autoscroll, Anchor, Editor, MultiBuffer,
+ SelectAll, MAX_TAB_TITLE_LEN,
+};
+use futures::StreamExt;
+use gpui::{
+ actions,
+ elements::*,
+ platform::{MouseButton, PromptLevel},
+ Action, AnyElement, AnyViewHandle, AppContext, Entity, ModelContext, ModelHandle, Subscription,
+ Task, View, ViewContext, ViewHandle, WeakModelHandle, WeakViewHandle,
+};
+use menu::Confirm;
+use project::{
+ search::{SearchInputs, SearchQuery},
+ Entry, Project,
+};
+use semantic_index::{SemanticIndex, SemanticIndexStatus};
+use smallvec::SmallVec;
+use std::{
+ any::{Any, TypeId},
+ borrow::Cow,
+ collections::HashSet,
+ mem,
+ ops::{Not, Range},
+ path::PathBuf,
+ sync::Arc,
+ time::{Duration, Instant},
+};
+use util::{paths::PathMatcher, ResultExt as _};
+use workspace::{
+ item::{BreadcrumbText, Item, ItemEvent, ItemHandle},
+ searchable::{Direction, SearchableItem, SearchableItemHandle},
+ ItemNavHistory, Pane, ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceId,
+};
+
+actions!(
+ project_search,
+ [SearchInNew, ToggleFocus, NextField, ToggleFilters,]
+);
+
+#[derive(Default)]
+struct ActiveSearches(HashMap<WeakModelHandle<Project>, WeakViewHandle<ProjectSearchView>>);
+
+#[derive(Default)]
+struct ActiveSettings(HashMap<WeakModelHandle<Project>, ProjectSearchSettings>);
+
+pub fn init(cx: &mut AppContext) {
+ cx.set_global(ActiveSearches::default());
+ cx.set_global(ActiveSettings::default());
+ cx.add_action(ProjectSearchView::deploy);
+ cx.add_action(ProjectSearchView::move_focus_to_results);
+ cx.add_action(ProjectSearchBar::confirm);
+ cx.add_action(ProjectSearchBar::search_in_new);
+ cx.add_action(ProjectSearchBar::select_next_match);
+ cx.add_action(ProjectSearchBar::select_prev_match);
+ cx.add_action(ProjectSearchBar::replace_next);
+ cx.add_action(ProjectSearchBar::replace_all);
+ cx.add_action(ProjectSearchBar::cycle_mode);
+ cx.add_action(ProjectSearchBar::next_history_query);
+ cx.add_action(ProjectSearchBar::previous_history_query);
+ cx.add_action(ProjectSearchBar::activate_regex_mode);
+ cx.add_action(ProjectSearchBar::toggle_replace);
+ cx.add_action(ProjectSearchBar::toggle_replace_on_a_pane);
+ cx.add_action(ProjectSearchBar::activate_text_mode);
+
+ // This action should only be registered if the semantic index is enabled
+ // We are registering it all the time, as I dont want to introduce a dependency
+ // for Semantic Index Settings globally whenever search is tested.
+ cx.add_action(ProjectSearchBar::activate_semantic_mode);
+
+ cx.capture_action(ProjectSearchBar::tab);
+ cx.capture_action(ProjectSearchBar::tab_previous);
+ cx.capture_action(ProjectSearchView::replace_all);
+ cx.capture_action(ProjectSearchView::replace_next);
+ add_toggle_option_action::<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);
+}
+
+fn add_toggle_filters_action<A: Action>(cx: &mut AppContext) {
+ cx.add_action(move |pane: &mut Pane, _: &A, cx: &mut ViewContext<Pane>| {
+ if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<ProjectSearchBar>() {
+ if search_bar.update(cx, |search_bar, cx| search_bar.toggle_filters(cx)) {
+ return;
+ }
+ }
+ cx.propagate_action();
+ });
+}
+
+fn add_toggle_option_action<A: Action>(option: SearchOptions, cx: &mut AppContext) {
+ cx.add_action(move |pane: &mut Pane, _: &A, cx: &mut ViewContext<Pane>| {
+ if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<ProjectSearchBar>() {
+ if search_bar.update(cx, |search_bar, cx| {
+ search_bar.toggle_search_option(option, cx)
+ }) {
+ return;
+ }
+ }
+ cx.propagate_action();
+ });
+}
+
+struct ProjectSearch {
+ project: ModelHandle<Project>,
+ excerpts: ModelHandle<MultiBuffer>,
+ pending_search: Option<Task<Option<()>>>,
+ match_ranges: Vec<Range<Anchor>>,
+ active_query: Option<SearchQuery>,
+ search_id: usize,
+ search_history: SearchHistory,
+ no_results: Option<bool>,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+enum InputPanel {
+ Query,
+ Exclude,
+ Include,
+}
+
+pub struct ProjectSearchView {
+ model: ModelHandle<ProjectSearch>,
+ query_editor: ViewHandle<Editor>,
+ replacement_editor: ViewHandle<Editor>,
+ results_editor: ViewHandle<Editor>,
+ semantic_state: Option<SemanticState>,
+ semantic_permissioned: Option<bool>,
+ search_options: SearchOptions,
+ panels_with_errors: HashSet<InputPanel>,
+ active_match_index: Option<usize>,
+ search_id: usize,
+ query_editor_was_focused: bool,
+ included_files_editor: ViewHandle<Editor>,
+ excluded_files_editor: ViewHandle<Editor>,
+ filters_enabled: bool,
+ replace_enabled: bool,
+ current_mode: SearchMode,
+}
+
+struct SemanticState {
+ index_status: SemanticIndexStatus,
+ maintain_rate_limit: Option<Task<()>>,
+ _subscription: Subscription,
+}
+
+#[derive(Debug, Clone)]
+struct ProjectSearchSettings {
+ search_options: SearchOptions,
+ filters_enabled: bool,
+ current_mode: SearchMode,
+}
+
+pub struct ProjectSearchBar {
+ active_project_search: Option<ViewHandle<ProjectSearchView>>,
+ subscription: Option<Subscription>,
+}
+
+impl Entity for ProjectSearch {
+ type Event = ();
+}
+
+impl ProjectSearch {
+ fn new(project: ModelHandle<Project>, cx: &mut ModelContext<Self>) -> Self {
+ let replica_id = project.read(cx).replica_id();
+ Self {
+ project,
+ excerpts: cx.add_model(|_| MultiBuffer::new(replica_id)),
+ pending_search: Default::default(),
+ match_ranges: Default::default(),
+ active_query: None,
+ search_id: 0,
+ search_history: SearchHistory::default(),
+ no_results: None,
+ }
+ }
+
+ fn clone(&self, cx: &mut ModelContext<Self>) -> ModelHandle<Self> {
+ cx.add_model(|cx| Self {
+ project: self.project.clone(),
+ excerpts: self
+ .excerpts
+ .update(cx, |excerpts, cx| cx.add_model(|cx| excerpts.clone(cx))),
+ pending_search: Default::default(),
+ match_ranges: self.match_ranges.clone(),
+ active_query: self.active_query.clone(),
+ search_id: self.search_id,
+ search_history: self.search_history.clone(),
+ no_results: self.no_results.clone(),
+ })
+ }
+
+ fn search(&mut self, query: SearchQuery, cx: &mut ModelContext<Self>) {
+ let search = self
+ .project
+ .update(cx, |project, cx| project.search(query.clone(), cx));
+ self.search_id += 1;
+ self.search_history.add(query.as_str().to_string());
+ self.active_query = Some(query);
+ self.match_ranges.clear();
+ self.pending_search = Some(cx.spawn_weak(|this, mut cx| async move {
+ let mut matches = search;
+ let this = this.upgrade(&cx)?;
+ this.update(&mut cx, |this, cx| {
+ this.match_ranges.clear();
+ this.excerpts.update(cx, |this, cx| this.clear(cx));
+ this.no_results = Some(true);
+ });
+
+ while let Some((buffer, anchors)) = matches.next().await {
+ let mut ranges = this.update(&mut cx, |this, cx| {
+ this.no_results = Some(false);
+ this.excerpts.update(cx, |excerpts, cx| {
+ excerpts.stream_excerpts_with_context_lines(buffer, anchors, 1, cx)
+ })
+ });
+
+ while let Some(range) = ranges.next().await {
+ this.update(&mut cx, |this, _| this.match_ranges.push(range));
+ }
+ this.update(&mut cx, |_, cx| cx.notify());
+ }
+
+ this.update(&mut cx, |this, cx| {
+ this.pending_search.take();
+ cx.notify();
+ });
+
+ None
+ }));
+ cx.notify();
+ }
+
+ fn semantic_search(&mut self, inputs: &SearchInputs, cx: &mut ModelContext<Self>) {
+ let search = SemanticIndex::global(cx).map(|index| {
+ index.update(cx, |semantic_index, cx| {
+ semantic_index.search_project(
+ self.project.clone(),
+ inputs.as_str().to_owned(),
+ 10,
+ inputs.files_to_include().to_vec(),
+ inputs.files_to_exclude().to_vec(),
+ cx,
+ )
+ })
+ });
+ self.search_id += 1;
+ self.match_ranges.clear();
+ self.search_history.add(inputs.as_str().to_string());
+ self.no_results = None;
+ self.pending_search = Some(cx.spawn(|this, mut cx| async move {
+ let results = search?.await.log_err()?;
+ let matches = results
+ .into_iter()
+ .map(|result| (result.buffer, vec![result.range.start..result.range.start]));
+
+ this.update(&mut cx, |this, cx| {
+ this.no_results = Some(true);
+ this.excerpts.update(cx, |excerpts, cx| {
+ excerpts.clear(cx);
+ });
+ });
+ for (buffer, ranges) in matches {
+ let mut match_ranges = this.update(&mut cx, |this, cx| {
+ this.no_results = Some(false);
+ this.excerpts.update(cx, |excerpts, cx| {
+ excerpts.stream_excerpts_with_context_lines(buffer, ranges, 3, cx)
+ })
+ });
+ while let Some(match_range) = match_ranges.next().await {
+ this.update(&mut cx, |this, cx| {
+ this.match_ranges.push(match_range);
+ while let Ok(Some(match_range)) = match_ranges.try_next() {
+ this.match_ranges.push(match_range);
+ }
+ cx.notify();
+ });
+ }
+ }
+
+ this.update(&mut cx, |this, cx| {
+ this.pending_search.take();
+ cx.notify();
+ });
+
+ None
+ }));
+ cx.notify();
+ }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub enum ViewEvent {
+ UpdateTab,
+ Activate,
+ EditorEvent(editor::Event),
+ Dismiss,
+}
+
+impl Entity for ProjectSearchView {
+ type Event = ViewEvent;
+}
+
+impl View for ProjectSearchView {
+ fn ui_name() -> &'static str {
+ "ProjectSearchView"
+ }
+
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+ let model = &self.model.read(cx);
+ if model.match_ranges.is_empty() {
+ enum Status {}
+
+ let theme = theme::current(cx).clone();
+
+ // If Search is Active -> Major: Searching..., Minor: None
+ // If Semantic -> Major: "Search using Natural Language", Minor: {Status}/n{ex...}/n{ex...}
+ // If Regex -> Major: "Search using Regex", Minor: {ex...}
+ // If Text -> Major: "Text search all files and folders", Minor: {...}
+
+ let current_mode = self.current_mode;
+ let mut major_text = if model.pending_search.is_some() {
+ Cow::Borrowed("Searching...")
+ } else if model.no_results.is_some_and(|v| v) {
+ Cow::Borrowed("No Results")
+ } else {
+ match current_mode {
+ SearchMode::Text => Cow::Borrowed("Text search all files and folders"),
+ SearchMode::Semantic => {
+ Cow::Borrowed("Search all code objects using Natural Language")
+ }
+ SearchMode::Regex => Cow::Borrowed("Regex search all files and folders"),
+ }
+ };
+
+ let mut show_minor_text = true;
+ let semantic_status = self.semantic_state.as_ref().and_then(|semantic| {
+ let status = semantic.index_status;
+ match status {
+ SemanticIndexStatus::NotAuthenticated => {
+ major_text = Cow::Borrowed("Not Authenticated");
+ show_minor_text = false;
+ Some(vec![
+ "API Key Missing: Please set 'OPENAI_API_KEY' in Environment Variables."
+ .to_string(), "If you authenticated using the Assistant Panel, please restart Zed to Authenticate.".to_string()])
+ }
+ SemanticIndexStatus::Indexed => Some(vec!["Indexing complete".to_string()]),
+ SemanticIndexStatus::Indexing {
+ remaining_files,
+ rate_limit_expiry,
+ } => {
+ if remaining_files == 0 {
+ Some(vec![format!("Indexing...")])
+ } else {
+ if let Some(rate_limit_expiry) = rate_limit_expiry {
+ let remaining_seconds =
+ rate_limit_expiry.duration_since(Instant::now());
+ if remaining_seconds > Duration::from_secs(0) {
+ Some(vec![format!(
+ "Remaining files to index (rate limit resets in {}s): {}",
+ remaining_seconds.as_secs(),
+ remaining_files
+ )])
+ } else {
+ Some(vec![format!("Remaining files to index: {}", remaining_files)])
+ }
+ } else {
+ Some(vec![format!("Remaining files to index: {}", remaining_files)])
+ }
+ }
+ }
+ SemanticIndexStatus::NotIndexed => None,
+ }
+ });
+
+ let minor_text = if let Some(no_results) = model.no_results {
+ if model.pending_search.is_none() && no_results {
+ vec!["No results found in this project for the provided query".to_owned()]
+ } else {
+ vec![]
+ }
+ } else {
+ match current_mode {
+ SearchMode::Semantic => {
+ let mut minor_text: Vec<String> = Vec::new();
+ minor_text.push("".into());
+ if let Some(semantic_status) = semantic_status {
+ minor_text.extend(semantic_status);
+ }
+ if show_minor_text {
+ minor_text
+ .push("Simply explain the code you are looking to find.".into());
+ minor_text.push(
+ "ex. 'prompt user for permissions to index their project'".into(),
+ );
+ }
+ minor_text
+ }
+ _ => vec![
+ "".to_owned(),
+ "Include/exclude specific paths with the filter option.".to_owned(),
+ "Matching exact word and/or casing is available too.".to_owned(),
+ ],
+ }
+ };
+
+ let previous_query_keystrokes =
+ cx.binding_for_action(&PreviousHistoryQuery {})
+ .map(|binding| {
+ binding
+ .keystrokes()
+ .iter()
+ .map(|k| k.to_string())
+ .collect::<Vec<_>>()
+ });
+ let next_query_keystrokes =
+ cx.binding_for_action(&NextHistoryQuery {}).map(|binding| {
+ binding
+ .keystrokes()
+ .iter()
+ .map(|k| k.to_string())
+ .collect::<Vec<_>>()
+ });
+ let new_placeholder_text = match (previous_query_keystrokes, next_query_keystrokes) {
+ (Some(previous_query_keystrokes), Some(next_query_keystrokes)) => {
+ format!(
+ "Search ({}/{} for previous/next query)",
+ previous_query_keystrokes.join(" "),
+ next_query_keystrokes.join(" ")
+ )
+ }
+ (None, Some(next_query_keystrokes)) => {
+ format!(
+ "Search ({} for next query)",
+ next_query_keystrokes.join(" ")
+ )
+ }
+ (Some(previous_query_keystrokes), None) => {
+ format!(
+ "Search ({} for previous query)",
+ previous_query_keystrokes.join(" ")
+ )
+ }
+ (None, None) => String::new(),
+ };
+ self.query_editor.update(cx, |editor, cx| {
+ editor.set_placeholder_text(new_placeholder_text, cx);
+ });
+
+ MouseEventHandler::new::<Status, _>(0, cx, |_, _| {
+ Flex::column()
+ .with_child(Flex::column().contained().flex(1., true))
+ .with_child(
+ Flex::column()
+ .align_children_center()
+ .with_child(Label::new(
+ major_text,
+ theme.search.major_results_status.clone(),
+ ))
+ .with_children(
+ minor_text.into_iter().map(|x| {
+ Label::new(x, theme.search.minor_results_status.clone())
+ }),
+ )
+ .aligned()
+ .top()
+ .contained()
+ .flex(7., true),
+ )
+ .contained()
+ .with_background_color(theme.editor.background)
+ })
+ .on_down(MouseButton::Left, |_, _, cx| {
+ cx.focus_parent();
+ })
+ .into_any_named("project search view")
+ } else {
+ ChildView::new(&self.results_editor, cx)
+ .flex(1., true)
+ .into_any_named("project search view")
+ }
+ }
+
+ fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
+ let handle = cx.weak_handle();
+ cx.update_global(|state: &mut ActiveSearches, cx| {
+ state
+ .0
+ .insert(self.model.read(cx).project.downgrade(), handle)
+ });
+
+ cx.update_global(|state: &mut ActiveSettings, cx| {
+ state.0.insert(
+ self.model.read(cx).project.downgrade(),
+ self.current_settings(),
+ );
+ });
+
+ if cx.is_self_focused() {
+ if self.query_editor_was_focused {
+ cx.focus(&self.query_editor);
+ } else {
+ cx.focus(&self.results_editor);
+ }
+ }
+ }
+}
+
+impl Item for ProjectSearchView {
+ fn tab_tooltip_text(&self, cx: &AppContext) -> Option<Cow<str>> {
+ let query_text = self.query_editor.read(cx).text(cx);
+
+ query_text
+ .is_empty()
+ .not()
+ .then(|| query_text.into())
+ .or_else(|| Some("Project Search".into()))
+ }
+ fn should_close_item_on_event(event: &Self::Event) -> bool {
+ event == &Self::Event::Dismiss
+ }
+
+ fn act_as_type<'a>(
+ &'a self,
+ type_id: TypeId,
+ self_handle: &'a ViewHandle<Self>,
+ _: &'a AppContext,
+ ) -> Option<&'a AnyViewHandle> {
+ if type_id == TypeId::of::<Self>() {
+ Some(self_handle)
+ } else if type_id == TypeId::of::<Editor>() {
+ Some(&self.results_editor)
+ } else {
+ None
+ }
+ }
+
+ fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
+ self.results_editor
+ .update(cx, |editor, cx| editor.deactivated(cx));
+ }
+
+ fn tab_content<T: 'static>(
+ &self,
+ _detail: Option<usize>,
+ tab_theme: &theme::Tab,
+ cx: &AppContext,
+ ) -> AnyElement<T> {
+ Flex::row()
+ .with_child(
+ Svg::new("icons/magnifying_glass.svg")
+ .with_color(tab_theme.label.text.color)
+ .constrained()
+ .with_width(tab_theme.type_icon_width)
+ .aligned()
+ .contained()
+ .with_margin_right(tab_theme.spacing),
+ )
+ .with_child({
+ let tab_name: Option<Cow<_>> = self
+ .model
+ .read(cx)
+ .search_history
+ .current()
+ .as_ref()
+ .map(|query| {
+ let query_text = util::truncate_and_trailoff(query, MAX_TAB_TITLE_LEN);
+ query_text.into()
+ });
+ Label::new(
+ tab_name
+ .filter(|name| !name.is_empty())
+ .unwrap_or("Project search".into()),
+ tab_theme.label.clone(),
+ )
+ .aligned()
+ })
+ .into_any()
+ }
+
+ fn for_each_project_item(&self, cx: &AppContext, f: &mut dyn FnMut(usize, &dyn project::Item)) {
+ self.results_editor.for_each_project_item(cx, f)
+ }
+
+ fn is_singleton(&self, _: &AppContext) -> bool {
+ false
+ }
+
+ fn can_save(&self, _: &AppContext) -> bool {
+ true
+ }
+
+ fn is_dirty(&self, cx: &AppContext) -> bool {
+ self.results_editor.read(cx).is_dirty(cx)
+ }
+
+ fn has_conflict(&self, cx: &AppContext) -> bool {
+ self.results_editor.read(cx).has_conflict(cx)
+ }
+
+ fn save(
+ &mut self,
+ project: ModelHandle<Project>,
+ cx: &mut ViewContext<Self>,
+ ) -> Task<anyhow::Result<()>> {
+ self.results_editor
+ .update(cx, |editor, cx| editor.save(project, cx))
+ }
+
+ fn save_as(
+ &mut self,
+ _: ModelHandle<Project>,
+ _: PathBuf,
+ _: &mut ViewContext<Self>,
+ ) -> Task<anyhow::Result<()>> {
+ unreachable!("save_as should not have been called")
+ }
+
+ fn reload(
+ &mut self,
+ project: ModelHandle<Project>,
+ cx: &mut ViewContext<Self>,
+ ) -> Task<anyhow::Result<()>> {
+ self.results_editor
+ .update(cx, |editor, cx| editor.reload(project, cx))
+ }
+
+ fn clone_on_split(&self, _workspace_id: WorkspaceId, cx: &mut ViewContext<Self>) -> Option<Self>
+ where
+ Self: Sized,
+ {
+ let model = self.model.update(cx, |model, cx| model.clone(cx));
+ Some(Self::new(model, cx, None))
+ }
+
+ fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
+ self.results_editor
+ .update(cx, |editor, cx| editor.added_to_workspace(workspace, cx));
+ }
+
+ fn set_nav_history(&mut self, nav_history: ItemNavHistory, cx: &mut ViewContext<Self>) {
+ self.results_editor.update(cx, |editor, _| {
+ editor.set_nav_history(Some(nav_history));
+ });
+ }
+
+ fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
+ self.results_editor
+ .update(cx, |editor, cx| editor.navigate(data, cx))
+ }
+
+ fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
+ match event {
+ ViewEvent::UpdateTab => {
+ smallvec::smallvec![ItemEvent::UpdateBreadcrumbs, ItemEvent::UpdateTab]
+ }
+ ViewEvent::EditorEvent(editor_event) => Editor::to_item_events(editor_event),
+ ViewEvent::Dismiss => smallvec::smallvec![ItemEvent::CloseItem],
+ _ => SmallVec::new(),
+ }
+ }
+
+ fn breadcrumb_location(&self) -> ToolbarItemLocation {
+ if self.has_matches() {
+ ToolbarItemLocation::Secondary
+ } else {
+ ToolbarItemLocation::Hidden
+ }
+ }
+
+ fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>> {
+ self.results_editor.breadcrumbs(theme, cx)
+ }
+
+ fn serialized_item_kind() -> Option<&'static str> {
+ None
+ }
+
+ fn deserialize(
+ _project: ModelHandle<Project>,
+ _workspace: WeakViewHandle<Workspace>,
+ _workspace_id: workspace::WorkspaceId,
+ _item_id: workspace::ItemId,
+ _cx: &mut ViewContext<Pane>,
+ ) -> Task<anyhow::Result<ViewHandle<Self>>> {
+ unimplemented!()
+ }
+}
+
+impl ProjectSearchView {
+ fn toggle_filters(&mut self, cx: &mut ViewContext<Self>) {
+ self.filters_enabled = !self.filters_enabled;
+ cx.update_global(|state: &mut ActiveSettings, cx| {
+ state.0.insert(
+ self.model.read(cx).project.downgrade(),
+ self.current_settings(),
+ );
+ });
+ }
+
+ fn current_settings(&self) -> ProjectSearchSettings {
+ ProjectSearchSettings {
+ search_options: self.search_options,
+ filters_enabled: self.filters_enabled,
+ current_mode: self.current_mode,
+ }
+ }
+ fn toggle_search_option(&mut self, option: SearchOptions, cx: &mut ViewContext<Self>) {
+ self.search_options.toggle(option);
+ cx.update_global(|state: &mut ActiveSettings, cx| {
+ state.0.insert(
+ self.model.read(cx).project.downgrade(),
+ self.current_settings(),
+ );
+ });
+ }
+
+ fn index_project(&mut self, cx: &mut ViewContext<Self>) {
+ if let Some(semantic_index) = SemanticIndex::global(cx) {
+ // Semantic search uses no options
+ self.search_options = SearchOptions::none();
+
+ let project = self.model.read(cx).project.clone();
+
+ semantic_index.update(cx, |semantic_index, cx| {
+ semantic_index
+ .index_project(project.clone(), cx)
+ .detach_and_log_err(cx);
+ });
+
+ self.semantic_state = Some(SemanticState {
+ index_status: semantic_index.read(cx).status(&project),
+ maintain_rate_limit: None,
+ _subscription: cx.observe(&semantic_index, Self::semantic_index_changed),
+ });
+ self.semantic_index_changed(semantic_index, cx);
+ }
+ }
+
+ fn semantic_index_changed(
+ &mut self,
+ semantic_index: ModelHandle<SemanticIndex>,
+ cx: &mut ViewContext<Self>,
+ ) {
+ let project = self.model.read(cx).project.clone();
+ if let Some(semantic_state) = self.semantic_state.as_mut() {
+ cx.notify();
+ semantic_state.index_status = semantic_index.read(cx).status(&project);
+ if let SemanticIndexStatus::Indexing {
+ rate_limit_expiry: Some(_),
+ ..
+ } = &semantic_state.index_status
+ {
+ if semantic_state.maintain_rate_limit.is_none() {
+ semantic_state.maintain_rate_limit =
+ Some(cx.spawn(|this, mut cx| async move {
+ loop {
+ cx.background().timer(Duration::from_secs(1)).await;
+ this.update(&mut cx, |_, cx| cx.notify()).log_err();
+ }
+ }));
+ return;
+ }
+ } else {
+ semantic_state.maintain_rate_limit = None;
+ }
+ }
+ }
+
+ fn clear_search(&mut self, cx: &mut ViewContext<Self>) {
+ self.model.update(cx, |model, cx| {
+ model.pending_search = None;
+ model.no_results = None;
+ model.match_ranges.clear();
+
+ model.excerpts.update(cx, |excerpts, cx| {
+ excerpts.clear(cx);
+ });
+ });
+ }
+
+ fn activate_search_mode(&mut self, mode: SearchMode, cx: &mut ViewContext<Self>) {
+ let previous_mode = self.current_mode;
+ if previous_mode == mode {
+ return;
+ }
+
+ self.clear_search(cx);
+ self.current_mode = mode;
+ self.active_match_index = None;
+
+ match mode {
+ SearchMode::Semantic => {
+ let has_permission = self.semantic_permissioned(cx);
+ self.active_match_index = None;
+ cx.spawn(|this, mut cx| async move {
+ let has_permission = has_permission.await?;
+
+ if !has_permission {
+ let mut answer = this.update(&mut cx, |this, cx| {
+ let project = this.model.read(cx).project.clone();
+ let project_name = project
+ .read(cx)
+ .worktree_root_names(cx)
+ .collect::<Vec<&str>>()
+ .join("/");
+ let is_plural =
+ project_name.chars().filter(|letter| *letter == '/').count() > 0;
+ let prompt_text = format!("Would you like to index the '{}' project{} for semantic search? This requires sending code to the OpenAI API", project_name,
+ if is_plural {
+ "s"
+ } else {""});
+ cx.prompt(
+ PromptLevel::Info,
+ prompt_text.as_str(),
+ &["Continue", "Cancel"],
+ )
+ })?;
+
+ if answer.next().await == Some(0) {
+ this.update(&mut cx, |this, _| {
+ this.semantic_permissioned = Some(true);
+ })?;
+ } else {
+ this.update(&mut cx, |this, cx| {
+ this.semantic_permissioned = Some(false);
+ debug_assert_ne!(previous_mode, SearchMode::Semantic, "Tried to re-enable semantic search mode after user modal was rejected");
+ this.activate_search_mode(previous_mode, cx);
+ })?;
+ return anyhow::Ok(());
+ }
+ }
+
+ this.update(&mut cx, |this, cx| {
+ this.index_project(cx);
+ })?;
+
+ anyhow::Ok(())
+ }).detach_and_log_err(cx);
+ }
+ SearchMode::Regex | SearchMode::Text => {
+ self.semantic_state = None;
+ self.active_match_index = None;
+ self.search(cx);
+ }
+ }
+
+ cx.update_global(|state: &mut ActiveSettings, cx| {
+ state.0.insert(
+ self.model.read(cx).project.downgrade(),
+ self.current_settings(),
+ );
+ });
+
+ cx.notify();
+ }
+ fn replace_next(&mut self, _: &ReplaceNext, cx: &mut ViewContext<Self>) {
+ let model = self.model.read(cx);
+ if let Some(query) = model.active_query.as_ref() {
+ if model.match_ranges.is_empty() {
+ return;
+ }
+ if let Some(active_index) = self.active_match_index {
+ let query = query.clone().with_replacement(self.replacement(cx));
+ self.results_editor.replace(
+ &(Box::new(model.match_ranges[active_index].clone()) as _),
+ &query,
+ cx,
+ );
+ self.select_match(Direction::Next, cx)
+ }
+ }
+ }
+ pub fn replacement(&self, cx: &AppContext) -> String {
+ self.replacement_editor.read(cx).text(cx)
+ }
+ fn replace_all(&mut self, _: &ReplaceAll, cx: &mut ViewContext<Self>) {
+ let model = self.model.read(cx);
+ if let Some(query) = model.active_query.as_ref() {
+ if model.match_ranges.is_empty() {
+ return;
+ }
+ if self.active_match_index.is_some() {
+ let query = query.clone().with_replacement(self.replacement(cx));
+ let matches = model
+ .match_ranges
+ .iter()
+ .map(|item| Box::new(item.clone()) as _)
+ .collect::<Vec<_>>();
+ for item in matches {
+ self.results_editor.replace(&item, &query, cx);
+ }
+ }
+ }
+ }
+
+ fn new(
+ model: ModelHandle<ProjectSearch>,
+ cx: &mut ViewContext<Self>,
+ settings: Option<ProjectSearchSettings>,
+ ) -> Self {
+ let project;
+ let excerpts;
+ let mut replacement_text = None;
+ let mut query_text = String::new();
+
+ // Read in settings if available
+ let (mut options, current_mode, filters_enabled) = if let Some(settings) = settings {
+ (
+ settings.search_options,
+ settings.current_mode,
+ settings.filters_enabled,
+ )
+ } else {
+ (SearchOptions::NONE, Default::default(), false)
+ };
+
+ {
+ let model = model.read(cx);
+ project = model.project.clone();
+ excerpts = model.excerpts.clone();
+ if let Some(active_query) = model.active_query.as_ref() {
+ query_text = active_query.as_str().to_string();
+ replacement_text = active_query.replacement().map(ToOwned::to_owned);
+ options = SearchOptions::from_query(active_query);
+ }
+ }
+ cx.observe(&model, |this, _, cx| this.model_changed(cx))
+ .detach();
+
+ let query_editor = cx.add_view(|cx| {
+ let mut editor = Editor::single_line(
+ Some(Arc::new(|theme| theme.search.editor.input.clone())),
+ cx,
+ );
+ editor.set_placeholder_text("Text search all files", cx);
+ editor.set_text(query_text, cx);
+ editor
+ });
+ // Subscribe to query_editor in order to reraise editor events for workspace item activation purposes
+ cx.subscribe(&query_editor, |_, _, event, cx| {
+ cx.emit(ViewEvent::EditorEvent(event.clone()))
+ })
+ .detach();
+ let replacement_editor = cx.add_view(|cx| {
+ let mut editor = Editor::single_line(
+ Some(Arc::new(|theme| theme.search.editor.input.clone())),
+ cx,
+ );
+ editor.set_placeholder_text("Replace in project..", cx);
+ if let Some(text) = replacement_text {
+ editor.set_text(text, cx);
+ }
+ editor
+ });
+ let results_editor = cx.add_view(|cx| {
+ let mut editor = Editor::for_multibuffer(excerpts, Some(project.clone()), cx);
+ editor.set_searchable(false);
+ editor
+ });
+ cx.observe(&results_editor, |_, _, cx| cx.emit(ViewEvent::UpdateTab))
+ .detach();
+
+ cx.subscribe(&results_editor, |this, _, event, cx| {
+ if matches!(event, editor::Event::SelectionsChanged { .. }) {
+ this.update_match_index(cx);
+ }
+ // Reraise editor events for workspace item activation purposes
+ cx.emit(ViewEvent::EditorEvent(event.clone()));
+ })
+ .detach();
+
+ let included_files_editor = cx.add_view(|cx| {
+ let mut editor = Editor::single_line(
+ Some(Arc::new(|theme| {
+ theme.search.include_exclude_editor.input.clone()
+ })),
+ cx,
+ );
+ editor.set_placeholder_text("Include: crates/**/*.toml", cx);
+
+ editor
+ });
+ // Subscribe to include_files_editor in order to reraise editor events for workspace item activation purposes
+ cx.subscribe(&included_files_editor, |_, _, event, cx| {
+ cx.emit(ViewEvent::EditorEvent(event.clone()))
+ })
+ .detach();
+
+ let excluded_files_editor = cx.add_view(|cx| {
+ let mut editor = Editor::single_line(
+ Some(Arc::new(|theme| {
+ theme.search.include_exclude_editor.input.clone()
+ })),
+ cx,
+ );
+ editor.set_placeholder_text("Exclude: vendor/*, *.lock", cx);
+
+ editor
+ });
+ // Subscribe to excluded_files_editor in order to reraise editor events for workspace item activation purposes
+ cx.subscribe(&excluded_files_editor, |_, _, event, cx| {
+ cx.emit(ViewEvent::EditorEvent(event.clone()))
+ })
+ .detach();
+
+ // Check if Worktrees have all been previously indexed
+ let mut this = ProjectSearchView {
+ replacement_editor,
+ search_id: model.read(cx).search_id,
+ model,
+ query_editor,
+ results_editor,
+ semantic_state: None,
+ semantic_permissioned: None,
+ search_options: options,
+ panels_with_errors: HashSet::new(),
+ active_match_index: None,
+ query_editor_was_focused: false,
+ included_files_editor,
+ excluded_files_editor,
+ filters_enabled,
+ current_mode,
+ replace_enabled: false,
+ };
+ this.model_changed(cx);
+ this
+ }
+
+ fn semantic_permissioned(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<bool>> {
+ if let Some(value) = self.semantic_permissioned {
+ return Task::ready(Ok(value));
+ }
+
+ SemanticIndex::global(cx)
+ .map(|semantic| {
+ let project = self.model.read(cx).project.clone();
+ semantic.update(cx, |this, cx| this.project_previously_indexed(&project, cx))
+ })
+ .unwrap_or(Task::ready(Ok(false)))
+ }
+ pub fn new_search_in_directory(
+ workspace: &mut Workspace,
+ dir_entry: &Entry,
+ cx: &mut ViewContext<Workspace>,
+ ) {
+ if !dir_entry.is_dir() {
+ return;
+ }
+ let Some(filter_str) = dir_entry.path.to_str() else {
+ return;
+ };
+
+ let model = cx.add_model(|cx| ProjectSearch::new(workspace.project().clone(), cx));
+ let search = cx.add_view(|cx| ProjectSearchView::new(model, cx, None));
+ workspace.add_item(Box::new(search.clone()), cx);
+ search.update(cx, |search, cx| {
+ search
+ .included_files_editor
+ .update(cx, |editor, cx| editor.set_text(filter_str, cx));
+ search.filters_enabled = true;
+ search.focus_query_editor(cx)
+ });
+ }
+
+ // Re-activate the most recently activated search or the most recent if it has been closed.
+ // If no search exists in the workspace, create a new one.
+ fn deploy(
+ workspace: &mut Workspace,
+ _: &workspace::NewSearch,
+ cx: &mut ViewContext<Workspace>,
+ ) {
+ // Clean up entries for dropped projects
+ cx.update_global(|state: &mut ActiveSearches, cx| {
+ state.0.retain(|project, _| project.is_upgradable(cx))
+ });
+
+ let active_search = cx
+ .global::<ActiveSearches>()
+ .0
+ .get(&workspace.project().downgrade());
+
+ let existing = active_search
+ .and_then(|active_search| {
+ workspace
+ .items_of_type::<ProjectSearchView>(cx)
+ .find(|search| search == active_search)
+ })
+ .or_else(|| workspace.item_of_type::<ProjectSearchView>(cx));
+
+ let query = workspace.active_item(cx).and_then(|item| {
+ let editor = item.act_as::<Editor>(cx)?;
+ let query = editor.query_suggestion(cx);
+ if query.is_empty() {
+ None
+ } else {
+ Some(query)
+ }
+ });
+
+ let search = if let Some(existing) = existing {
+ workspace.activate_item(&existing, cx);
+ existing
+ } else {
+ let settings = cx
+ .global::<ActiveSettings>()
+ .0
+ .get(&workspace.project().downgrade());
+
+ let settings = if let Some(settings) = settings {
+ Some(settings.clone())
+ } else {
+ None
+ };
+
+ let model = cx.add_model(|cx| ProjectSearch::new(workspace.project().clone(), cx));
+ let view = cx.add_view(|cx| ProjectSearchView::new(model, cx, settings));
+
+ workspace.add_item(Box::new(view.clone()), cx);
+ view
+ };
+
+ search.update(cx, |search, cx| {
+ if let Some(query) = query {
+ search.set_query(&query, cx);
+ }
+ search.focus_query_editor(cx)
+ });
+ }
+
+ fn search(&mut self, cx: &mut ViewContext<Self>) {
+ let mode = self.current_mode;
+ match mode {
+ SearchMode::Semantic => {
+ if self.semantic_state.is_some() {
+ if let Some(query) = self.build_search_query(cx) {
+ self.model
+ .update(cx, |model, cx| model.semantic_search(query.as_inner(), cx));
+ }
+ }
+ }
+
+ _ => {
+ if let Some(query) = self.build_search_query(cx) {
+ self.model.update(cx, |model, cx| model.search(query, cx));
+ }
+ }
+ }
+ }
+
+ fn build_search_query(&mut self, cx: &mut ViewContext<Self>) -> Option<SearchQuery> {
+ let text = self.query_editor.read(cx).text(cx);
+ let included_files =
+ match Self::parse_path_matches(&self.included_files_editor.read(cx).text(cx)) {
+ Ok(included_files) => {
+ self.panels_with_errors.remove(&InputPanel::Include);
+ included_files
+ }
+ Err(_e) => {
+ self.panels_with_errors.insert(InputPanel::Include);
+ cx.notify();
+ return None;
+ }
+ };
+ let excluded_files =
+ match Self::parse_path_matches(&self.excluded_files_editor.read(cx).text(cx)) {
+ Ok(excluded_files) => {
+ self.panels_with_errors.remove(&InputPanel::Exclude);
+ excluded_files
+ }
+ Err(_e) => {
+ self.panels_with_errors.insert(InputPanel::Exclude);
+ cx.notify();
+ return None;
+ }
+ };
+ let current_mode = self.current_mode;
+ match current_mode {
+ SearchMode::Regex => {
+ match SearchQuery::regex(
+ text,
+ self.search_options.contains(SearchOptions::WHOLE_WORD),
+ self.search_options.contains(SearchOptions::CASE_SENSITIVE),
+ self.search_options.contains(SearchOptions::INCLUDE_IGNORED),
+ included_files,
+ excluded_files,
+ ) {
+ Ok(query) => {
+ self.panels_with_errors.remove(&InputPanel::Query);
+ Some(query)
+ }
+ Err(_e) => {
+ self.panels_with_errors.insert(InputPanel::Query);
+ cx.notify();
+ None
+ }
+ }
+ }
+ _ => match SearchQuery::text(
+ text,
+ self.search_options.contains(SearchOptions::WHOLE_WORD),
+ self.search_options.contains(SearchOptions::CASE_SENSITIVE),
+ self.search_options.contains(SearchOptions::INCLUDE_IGNORED),
+ included_files,
+ excluded_files,
+ ) {
+ Ok(query) => {
+ self.panels_with_errors.remove(&InputPanel::Query);
+ Some(query)
+ }
+ Err(_e) => {
+ self.panels_with_errors.insert(InputPanel::Query);
+ cx.notify();
+ None
+ }
+ },
+ }
+ }
+
+ fn parse_path_matches(text: &str) -> anyhow::Result<Vec<PathMatcher>> {
+ text.split(',')
+ .map(str::trim)
+ .filter(|maybe_glob_str| !maybe_glob_str.is_empty())
+ .map(|maybe_glob_str| {
+ PathMatcher::new(maybe_glob_str)
+ .with_context(|| format!("parsing {maybe_glob_str} as path matcher"))
+ })
+ .collect()
+ }
+
+ fn select_match(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
+ if let Some(index) = self.active_match_index {
+ let match_ranges = self.model.read(cx).match_ranges.clone();
+ let new_index = self.results_editor.update(cx, |editor, cx| {
+ editor.match_index_for_direction(&match_ranges, index, direction, 1, cx)
+ });
+
+ let range_to_select = match_ranges[new_index].clone();
+ self.results_editor.update(cx, |editor, cx| {
+ let range_to_select = editor.range_for_match(&range_to_select);
+ editor.unfold_ranges([range_to_select.clone()], false, true, cx);
+ editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.select_ranges([range_to_select])
+ });
+ });
+ }
+ }
+
+ fn focus_query_editor(&mut self, cx: &mut ViewContext<Self>) {
+ self.query_editor.update(cx, |query_editor, cx| {
+ query_editor.select_all(&SelectAll, cx);
+ });
+ self.query_editor_was_focused = true;
+ cx.focus(&self.query_editor);
+ }
+
+ fn set_query(&mut self, query: &str, cx: &mut ViewContext<Self>) {
+ self.query_editor
+ .update(cx, |query_editor, cx| query_editor.set_text(query, cx));
+ }
+
+ fn focus_results_editor(&mut self, cx: &mut ViewContext<Self>) {
+ self.query_editor.update(cx, |query_editor, cx| {
+ let cursor = query_editor.selections.newest_anchor().head();
+ query_editor.change_selections(None, cx, |s| s.select_ranges([cursor.clone()..cursor]));
+ });
+ self.query_editor_was_focused = false;
+ cx.focus(&self.results_editor);
+ }
+
+ fn model_changed(&mut self, cx: &mut ViewContext<Self>) {
+ let match_ranges = self.model.read(cx).match_ranges.clone();
+ if match_ranges.is_empty() {
+ self.active_match_index = None;
+ } else {
+ self.active_match_index = Some(0);
+ self.update_match_index(cx);
+ let prev_search_id = mem::replace(&mut self.search_id, self.model.read(cx).search_id);
+ let is_new_search = self.search_id != prev_search_id;
+ self.results_editor.update(cx, |editor, cx| {
+ if is_new_search {
+ let range_to_select = match_ranges
+ .first()
+ .clone()
+ .map(|range| editor.range_for_match(range));
+ editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.select_ranges(range_to_select)
+ });
+ }
+ editor.highlight_background::<Self>(
+ match_ranges,
+ |theme| theme.search.match_background,
+ cx,
+ );
+ });
+ if is_new_search && self.query_editor.is_focused(cx) {
+ self.focus_results_editor(cx);
+ }
+ }
+
+ cx.emit(ViewEvent::UpdateTab);
+ cx.notify();
+ }
+
+ fn update_match_index(&mut self, cx: &mut ViewContext<Self>) {
+ let results_editor = self.results_editor.read(cx);
+ let new_index = active_match_index(
+ &self.model.read(cx).match_ranges,
+ &results_editor.selections.newest_anchor().head(),
+ &results_editor.buffer().read(cx).snapshot(cx),
+ );
+ if self.active_match_index != new_index {
+ self.active_match_index = new_index;
+ cx.notify();
+ }
+ }
+
+ pub fn has_matches(&self) -> bool {
+ self.active_match_index.is_some()
+ }
+
+ fn move_focus_to_results(pane: &mut Pane, _: &ToggleFocus, cx: &mut ViewContext<Pane>) {
+ if let Some(search_view) = pane
+ .active_item()
+ .and_then(|item| item.downcast::<ProjectSearchView>())
+ {
+ search_view.update(cx, |search_view, cx| {
+ if !search_view.results_editor.is_focused(cx)
+ && !search_view.model.read(cx).match_ranges.is_empty()
+ {
+ return search_view.focus_results_editor(cx);
+ }
+ });
+ }
+
+ cx.propagate_action();
+ }
+}
+
+impl Default for ProjectSearchBar {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl ProjectSearchBar {
+ pub fn new() -> Self {
+ Self {
+ active_project_search: Default::default(),
+ subscription: Default::default(),
+ }
+ }
+ fn cycle_mode(workspace: &mut Workspace, _: &CycleMode, cx: &mut ViewContext<Workspace>) {
+ if let Some(search_view) = workspace
+ .active_item(cx)
+ .and_then(|item| item.downcast::<ProjectSearchView>())
+ {
+ search_view.update(cx, |this, cx| {
+ let new_mode =
+ crate::mode::next_mode(&this.current_mode, SemanticIndex::enabled(cx));
+ this.activate_search_mode(new_mode, cx);
+ cx.focus(&this.query_editor);
+ })
+ }
+ }
+ fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
+ let mut should_propagate = true;
+ if let Some(search_view) = self.active_project_search.as_ref() {
+ search_view.update(cx, |search_view, cx| {
+ if !search_view.replacement_editor.is_focused(cx) {
+ should_propagate = false;
+ search_view.search(cx);
+ }
+ });
+ }
+ if should_propagate {
+ cx.propagate_action();
+ }
+ }
+
+ fn search_in_new(workspace: &mut Workspace, _: &SearchInNew, cx: &mut ViewContext<Workspace>) {
+ if let Some(search_view) = workspace
+ .active_item(cx)
+ .and_then(|item| item.downcast::<ProjectSearchView>())
+ {
+ let new_query = search_view.update(cx, |search_view, cx| {
+ let new_query = search_view.build_search_query(cx);
+ if new_query.is_some() {
+ if let Some(old_query) = search_view.model.read(cx).active_query.clone() {
+ search_view.query_editor.update(cx, |editor, cx| {
+ editor.set_text(old_query.as_str(), cx);
+ });
+ search_view.search_options = SearchOptions::from_query(&old_query);
+ }
+ }
+ new_query
+ });
+ if let Some(new_query) = new_query {
+ let model = cx.add_model(|cx| {
+ let mut model = ProjectSearch::new(workspace.project().clone(), cx);
+ model.search(new_query, cx);
+ model
+ });
+ workspace.add_item(
+ Box::new(cx.add_view(|cx| ProjectSearchView::new(model, cx, None))),
+ cx,
+ );
+ }
+ }
+ }
+
+ fn select_next_match(pane: &mut Pane, _: &SelectNextMatch, cx: &mut ViewContext<Pane>) {
+ if let Some(search_view) = pane
+ .active_item()
+ .and_then(|item| item.downcast::<ProjectSearchView>())
+ {
+ search_view.update(cx, |view, cx| view.select_match(Direction::Next, cx));
+ } else {
+ cx.propagate_action();
+ }
+ }
+
+ fn replace_next(pane: &mut Pane, _: &ReplaceNext, cx: &mut ViewContext<Pane>) {
+ if let Some(search_view) = pane
+ .active_item()
+ .and_then(|item| item.downcast::<ProjectSearchView>())
+ {
+ search_view.update(cx, |view, cx| view.replace_next(&ReplaceNext, cx));
+ } else {
+ cx.propagate_action();
+ }
+ }
+ fn replace_all(pane: &mut Pane, _: &ReplaceAll, cx: &mut ViewContext<Pane>) {
+ if let Some(search_view) = pane
+ .active_item()
+ .and_then(|item| item.downcast::<ProjectSearchView>())
+ {
+ search_view.update(cx, |view, cx| view.replace_all(&ReplaceAll, cx));
+ } else {
+ cx.propagate_action();
+ }
+ }
+ fn select_prev_match(pane: &mut Pane, _: &SelectPrevMatch, cx: &mut ViewContext<Pane>) {
+ if let Some(search_view) = pane
+ .active_item()
+ .and_then(|item| item.downcast::<ProjectSearchView>())
+ {
+ search_view.update(cx, |view, cx| view.select_match(Direction::Prev, cx));
+ } else {
+ cx.propagate_action();
+ }
+ }
+
+ fn tab(&mut self, _: &editor::Tab, cx: &mut ViewContext<Self>) {
+ self.cycle_field(Direction::Next, cx);
+ }
+
+ fn tab_previous(&mut self, _: &editor::TabPrev, cx: &mut ViewContext<Self>) {
+ self.cycle_field(Direction::Prev, cx);
+ }
+
+ fn cycle_field(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
+ let active_project_search = match &self.active_project_search {
+ Some(active_project_search) => active_project_search,
+
+ None => {
+ cx.propagate_action();
+ return;
+ }
+ };
+
+ active_project_search.update(cx, |project_view, cx| {
+ let mut views = vec![&project_view.query_editor];
+ if project_view.filters_enabled {
+ views.extend([
+ &project_view.included_files_editor,
+ &project_view.excluded_files_editor,
+ ]);
+ }
+ if project_view.replace_enabled {
+ views.push(&project_view.replacement_editor);
+ }
+ let current_index = match views
+ .iter()
+ .enumerate()
+ .find(|(_, view)| view.is_focused(cx))
+ {
+ Some((index, _)) => index,
+
+ None => {
+ cx.propagate_action();
+ return;
+ }
+ };
+
+ let new_index = match direction {
+ Direction::Next => (current_index + 1) % views.len(),
+ Direction::Prev if current_index == 0 => views.len() - 1,
+ Direction::Prev => (current_index - 1) % views.len(),
+ };
+ cx.focus(views[new_index]);
+ });
+ }
+
+ fn toggle_search_option(&mut self, option: SearchOptions, cx: &mut ViewContext<Self>) -> bool {
+ if let Some(search_view) = self.active_project_search.as_ref() {
+ search_view.update(cx, |search_view, cx| {
+ search_view.toggle_search_option(option, cx);
+ search_view.search(cx);
+ });
+
+ cx.notify();
+ true
+ } else {
+ false
+ }
+ }
+ fn toggle_replace(&mut self, _: &ToggleReplace, cx: &mut ViewContext<Self>) {
+ if let Some(search) = &self.active_project_search {
+ search.update(cx, |this, cx| {
+ this.replace_enabled = !this.replace_enabled;
+ if !this.replace_enabled {
+ cx.focus(&this.query_editor);
+ }
+ cx.notify();
+ });
+ }
+ }
+ fn toggle_replace_on_a_pane(pane: &mut Pane, _: &ToggleReplace, cx: &mut ViewContext<Pane>) {
+ let mut should_propagate = true;
+ if let Some(search_view) = pane
+ .active_item()
+ .and_then(|item| item.downcast::<ProjectSearchView>())
+ {
+ search_view.update(cx, |this, cx| {
+ should_propagate = false;
+ this.replace_enabled = !this.replace_enabled;
+ if !this.replace_enabled {
+ cx.focus(&this.query_editor);
+ }
+ cx.notify();
+ });
+ }
+ if should_propagate {
+ cx.propagate_action();
+ }
+ }
+ fn activate_text_mode(pane: &mut Pane, _: &ActivateTextMode, cx: &mut ViewContext<Pane>) {
+ if let Some(search_view) = pane
+ .active_item()
+ .and_then(|item| item.downcast::<ProjectSearchView>())
+ {
+ search_view.update(cx, |view, cx| {
+ view.activate_search_mode(SearchMode::Text, cx)
+ });
+ } else {
+ cx.propagate_action();
+ }
+ }
+
+ fn activate_regex_mode(pane: &mut Pane, _: &ActivateRegexMode, cx: &mut ViewContext<Pane>) {
+ if let Some(search_view) = pane
+ .active_item()
+ .and_then(|item| item.downcast::<ProjectSearchView>())
+ {
+ search_view.update(cx, |view, cx| {
+ view.activate_search_mode(SearchMode::Regex, cx)
+ });
+ } else {
+ cx.propagate_action();
+ }
+ }
+
+ fn activate_semantic_mode(
+ pane: &mut Pane,
+ _: &ActivateSemanticMode,
+ cx: &mut ViewContext<Pane>,
+ ) {
+ if SemanticIndex::enabled(cx) {
+ if let Some(search_view) = pane
+ .active_item()
+ .and_then(|item| item.downcast::<ProjectSearchView>())
+ {
+ search_view.update(cx, |view, cx| {
+ view.activate_search_mode(SearchMode::Semantic, cx)
+ });
+ } else {
+ cx.propagate_action();
+ }
+ }
+ }
+
+ fn toggle_filters(&mut self, cx: &mut ViewContext<Self>) -> bool {
+ if let Some(search_view) = self.active_project_search.as_ref() {
+ search_view.update(cx, |search_view, cx| {
+ search_view.toggle_filters(cx);
+ search_view
+ .included_files_editor
+ .update(cx, |_, cx| cx.notify());
+ search_view
+ .excluded_files_editor
+ .update(cx, |_, cx| cx.notify());
+ cx.refresh_windows();
+ cx.notify();
+ });
+ cx.notify();
+ true
+ } else {
+ false
+ }
+ }
+
+ fn activate_search_mode(&self, mode: SearchMode, cx: &mut ViewContext<Self>) {
+ // Update Current Mode
+ if let Some(search_view) = self.active_project_search.as_ref() {
+ search_view.update(cx, |search_view, cx| {
+ search_view.activate_search_mode(mode, cx);
+ });
+ cx.notify();
+ }
+ }
+
+ fn is_option_enabled(&self, option: SearchOptions, cx: &AppContext) -> bool {
+ if let Some(search) = self.active_project_search.as_ref() {
+ search.read(cx).search_options.contains(option)
+ } else {
+ false
+ }
+ }
+
+ fn next_history_query(&mut self, _: &NextHistoryQuery, cx: &mut ViewContext<Self>) {
+ if let Some(search_view) = self.active_project_search.as_ref() {
+ search_view.update(cx, |search_view, cx| {
+ let new_query = search_view.model.update(cx, |model, _| {
+ if let Some(new_query) = model.search_history.next().map(str::to_string) {
+ new_query
+ } else {
+ model.search_history.reset_selection();
+ String::new()
+ }
+ });
+ search_view.set_query(&new_query, cx);
+ });
+ }
+ }
+
+ fn previous_history_query(&mut self, _: &PreviousHistoryQuery, cx: &mut ViewContext<Self>) {
+ if let Some(search_view) = self.active_project_search.as_ref() {
+ search_view.update(cx, |search_view, cx| {
+ if search_view.query_editor.read(cx).text(cx).is_empty() {
+ if let Some(new_query) = search_view
+ .model
+ .read(cx)
+ .search_history
+ .current()
+ .map(str::to_string)
+ {
+ search_view.set_query(&new_query, cx);
+ return;
+ }
+ }
+
+ if let Some(new_query) = search_view.model.update(cx, |model, _| {
+ model.search_history.previous().map(str::to_string)
+ }) {
+ search_view.set_query(&new_query, cx);
+ }
+ });
+ }
+ }
+}
+
+impl Entity for ProjectSearchBar {
+ type Event = ();
+}
+
+impl View for ProjectSearchBar {
+ fn ui_name() -> &'static str {
+ "ProjectSearchBar"
+ }
+
+ fn update_keymap_context(
+ &self,
+ keymap: &mut gpui::keymap_matcher::KeymapContext,
+ cx: &AppContext,
+ ) {
+ Self::reset_to_default_keymap_context(keymap);
+ let in_replace = self
+ .active_project_search
+ .as_ref()
+ .map(|search| {
+ search
+ .read(cx)
+ .replacement_editor
+ .read_with(cx, |_, cx| cx.is_self_focused())
+ })
+ .flatten()
+ .unwrap_or(false);
+ if in_replace {
+ keymap.add_identifier("in_replace");
+ }
+ }
+
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+ if let Some(_search) = self.active_project_search.as_ref() {
+ let search = _search.read(cx);
+ let theme = theme::current(cx).clone();
+ let query_container_style = if search.panels_with_errors.contains(&InputPanel::Query) {
+ theme.search.invalid_editor
+ } else {
+ theme.search.editor.input.container
+ };
+
+ let search = _search.read(cx);
+ let filter_button = render_option_button_icon(
+ search.filters_enabled,
+ "icons/filter.svg",
+ 0,
+ "Toggle filters",
+ Box::new(ToggleFilters),
+ move |_, this, cx| {
+ this.toggle_filters(cx);
+ },
+ cx,
+ );
+
+ let search = _search.read(cx);
+ let is_semantic_available = SemanticIndex::enabled(cx);
+ let is_semantic_disabled = search.semantic_state.is_none();
+ let icon_style = theme.search.editor_icon.clone();
+ let is_active = search.active_match_index.is_some();
+
+ let render_option_button_icon = |path, option, cx: &mut ViewContext<Self>| {
+ crate::search_bar::render_option_button_icon(
+ self.is_option_enabled(option, cx),
+ path,
+ option.bits as usize,
+ format!("Toggle {}", option.label()),
+ option.to_toggle_action(),
+ move |_, this, cx| {
+ this.toggle_search_option(option, cx);
+ },
+ cx,
+ )
+ };
+ let case_sensitive = is_semantic_disabled.then(|| {
+ render_option_button_icon(
+ "icons/case_insensitive.svg",
+ SearchOptions::CASE_SENSITIVE,
+ cx,
+ )
+ });
+
+ let whole_word = is_semantic_disabled.then(|| {
+ render_option_button_icon("icons/word_search.svg", SearchOptions::WHOLE_WORD, cx)
+ });
+
+ let include_ignored = is_semantic_disabled.then(|| {
+ render_option_button_icon(
+ "icons/file_icons/git.svg",
+ SearchOptions::INCLUDE_IGNORED,
+ cx,
+ )
+ });
+
+ let search_button_for_mode = |mode, side, cx: &mut ViewContext<ProjectSearchBar>| {
+ let is_active = if let Some(search) = self.active_project_search.as_ref() {
+ let search = search.read(cx);
+ search.current_mode == mode
+ } else {
+ false
+ };
+ render_search_mode_button(
+ mode,
+ side,
+ is_active,
+ move |_, this, cx| {
+ this.activate_search_mode(mode, cx);
+ },
+ cx,
+ )
+ };
+
+ let search = _search.read(cx);
+
+ let include_container_style =
+ if search.panels_with_errors.contains(&InputPanel::Include) {
+ theme.search.invalid_include_exclude_editor
+ } else {
+ theme.search.include_exclude_editor.input.container
+ };
+
+ let exclude_container_style =
+ if search.panels_with_errors.contains(&InputPanel::Exclude) {
+ theme.search.invalid_include_exclude_editor
+ } else {
+ theme.search.include_exclude_editor.input.container
+ };
+
+ let matches = search.active_match_index.map(|match_ix| {
+ Label::new(
+ format!(
+ "{}/{}",
+ match_ix + 1,
+ search.model.read(cx).match_ranges.len()
+ ),
+ theme.search.match_index.text.clone(),
+ )
+ .contained()
+ .with_style(theme.search.match_index.container)
+ .aligned()
+ });
+ let should_show_replace_input = search.replace_enabled;
+ let replacement = should_show_replace_input.then(|| {
+ Flex::row()
+ .with_child(
+ Svg::for_style(theme.search.replace_icon.clone().icon)
+ .contained()
+ .with_style(theme.search.replace_icon.clone().container),
+ )
+ .with_child(ChildView::new(&search.replacement_editor, cx).flex(1., true))
+ .align_children_center()
+ .flex(1., true)
+ .contained()
+ .with_style(query_container_style)
+ .constrained()
+ .with_min_width(theme.search.editor.min_width)
+ .with_max_width(theme.search.editor.max_width)
+ .with_height(theme.search.search_bar_row_height)
+ .flex(1., false)
+ });
+ let replace_all = should_show_replace_input.then(|| {
+ super::replace_action(
+ ReplaceAll,
+ "Replace all",
+ "icons/replace_all.svg",
+ theme.tooltip.clone(),
+ theme.search.action_button.clone(),
+ )
+ });
+ let replace_next = should_show_replace_input.then(|| {
+ super::replace_action(
+ ReplaceNext,
+ "Replace next",
+ "icons/replace_next.svg",
+ theme.tooltip.clone(),
+ theme.search.action_button.clone(),
+ )
+ });
+ let query_column = Flex::column()
+ .with_spacing(theme.search.search_row_spacing)
+ .with_child(
+ Flex::row()
+ .with_child(
+ Svg::for_style(icon_style.icon)
+ .contained()
+ .with_style(icon_style.container),
+ )
+ .with_child(ChildView::new(&search.query_editor, cx).flex(1., true))
+ .with_child(
+ Flex::row()
+ .with_child(filter_button)
+ .with_children(case_sensitive)
+ .with_children(whole_word)
+ .flex(1., false)
+ .constrained()
+ .contained(),
+ )
+ .align_children_center()
+ .contained()
+ .with_style(query_container_style)
+ .constrained()
+ .with_min_width(theme.search.editor.min_width)
+ .with_max_width(theme.search.editor.max_width)
+ .with_height(theme.search.search_bar_row_height)
+ .flex(1., false),
+ )
+ .with_children(search.filters_enabled.then(|| {
+ Flex::row()
+ .with_child(
+ Flex::row()
+ .with_child(
+ ChildView::new(&search.included_files_editor, cx)
+ .contained()
+ .constrained()
+ .with_height(theme.search.search_bar_row_height)
+ .flex(1., true),
+ )
+ .with_children(include_ignored)
+ .contained()
+ .with_style(include_container_style)
+ .constrained()
+ .with_height(theme.search.search_bar_row_height)
+ .flex(1., true),
+ )
+ .with_child(
+ ChildView::new(&search.excluded_files_editor, cx)
+ .contained()
+ .with_style(exclude_container_style)
+ .constrained()
+ .with_height(theme.search.search_bar_row_height)
+ .flex(1., true),
+ )
+ .constrained()
+ .with_min_width(theme.search.editor.min_width)
+ .with_max_width(theme.search.editor.max_width)
+ .flex(1., false)
+ }))
+ .flex(1., false);
+ let switches_column = Flex::row()
+ .align_children_center()
+ .with_child(super::toggle_replace_button(
+ search.replace_enabled,
+ theme.tooltip.clone(),
+ theme.search.option_button_component.clone(),
+ ))
+ .constrained()
+ .with_height(theme.search.search_bar_row_height)
+ .contained()
+ .with_style(theme.search.option_button_group);
+ let mode_column =
+ Flex::row()
+ .with_child(search_button_for_mode(
+ SearchMode::Text,
+ Some(Side::Left),
+ cx,
+ ))
+ .with_child(search_button_for_mode(
+ SearchMode::Regex,
+ if is_semantic_available {
+ None
+ } else {
+ Some(Side::Right)
+ },
+ cx,
+ ))
+ .with_children(is_semantic_available.then(|| {
+ search_button_for_mode(SearchMode::Semantic, Some(Side::Right), cx)
+ }))
+ .contained()
+ .with_style(theme.search.modes_container);
+
+ let nav_button_for_direction = |label, direction, cx: &mut ViewContext<Self>| {
+ render_nav_button(
+ label,
+ direction,
+ is_active,
+ move |_, this, cx| {
+ if let Some(search) = this.active_project_search.as_ref() {
+ search.update(cx, |search, cx| search.select_match(direction, cx));
+ }
+ },
+ cx,
+ )
+ };
+
+ let nav_column = Flex::row()
+ .with_children(replace_next)
+ .with_children(replace_all)
+ .with_child(Flex::row().with_children(matches))
+ .with_child(nav_button_for_direction("<", Direction::Prev, cx))
+ .with_child(nav_button_for_direction(">", Direction::Next, cx))
+ .constrained()
+ .with_height(theme.search.search_bar_row_height)
+ .flex_float();
+
+ Flex::row()
+ .with_child(query_column)
+ .with_child(mode_column)
+ .with_child(switches_column)
+ .with_children(replacement)
+ .with_child(nav_column)
+ .contained()
+ .with_style(theme.search.container)
+ .into_any_named("project search")
+ } else {
+ Empty::new().into_any()
+ }
+ }
+}
+
+impl ToolbarItemView for ProjectSearchBar {
+ fn set_active_pane_item(
+ &mut self,
+ active_pane_item: Option<&dyn ItemHandle>,
+ cx: &mut ViewContext<Self>,
+ ) -> ToolbarItemLocation {
+ cx.notify();
+ self.subscription = None;
+ self.active_project_search = None;
+ if let Some(search) = active_pane_item.and_then(|i| i.downcast::<ProjectSearchView>()) {
+ search.update(cx, |search, cx| {
+ if search.current_mode == SearchMode::Semantic {
+ search.index_project(cx);
+ }
+ });
+
+ self.subscription = Some(cx.observe(&search, |_, _, cx| cx.notify()));
+ self.active_project_search = Some(search);
+ ToolbarItemLocation::PrimaryLeft {
+ flex: Some((1., true)),
+ }
+ } else {
+ ToolbarItemLocation::Hidden
+ }
+ }
+
+ fn row_count(&self, cx: &ViewContext<Self>) -> usize {
+ if let Some(search) = self.active_project_search.as_ref() {
+ if search.read(cx).filters_enabled {
+ return 2;
+ }
+ }
+ 1
+ }
+}
+
+#[cfg(test)]
+pub mod tests {
+ use super::*;
+ use editor::DisplayPoint;
+ use gpui::{color::Color, executor::Deterministic, TestAppContext};
+ use project::FakeFs;
+ use semantic_index::semantic_index_settings::SemanticIndexSettings;
+ use serde_json::json;
+ use settings::SettingsStore;
+ use std::sync::Arc;
+ use theme::ThemeSettings;
+
+ #[gpui::test]
+ async fn test_project_search(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
+ init_test(cx);
+
+ let fs = FakeFs::new(cx.background());
+ fs.insert_tree(
+ "/dir",
+ json!({
+ "one.rs": "const ONE: usize = 1;",
+ "two.rs": "const TWO: usize = one::ONE + one::ONE;",
+ "three.rs": "const THREE: usize = one::ONE + two::TWO;",
+ "four.rs": "const FOUR: usize = one::ONE + three::THREE;",
+ }),
+ )
+ .await;
+ let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
+ let search = cx.add_model(|cx| ProjectSearch::new(project, cx));
+ let search_view = cx
+ .add_window(|cx| ProjectSearchView::new(search.clone(), cx, None))
+ .root(cx);
+
+ search_view.update(cx, |search_view, cx| {
+ search_view
+ .query_editor
+ .update(cx, |query_editor, cx| query_editor.set_text("TWO", cx));
+ search_view.search(cx);
+ });
+ deterministic.run_until_parked();
+ search_view.update(cx, |search_view, cx| {
+ assert_eq!(
+ search_view
+ .results_editor
+ .update(cx, |editor, cx| editor.display_text(cx)),
+ "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;"
+ );
+ assert_eq!(
+ search_view
+ .results_editor
+ .update(cx, |editor, cx| editor.all_text_background_highlights(cx)),
+ &[
+ (
+ DisplayPoint::new(2, 32)..DisplayPoint::new(2, 35),
+ Color::red()
+ ),
+ (
+ DisplayPoint::new(2, 37)..DisplayPoint::new(2, 40),
+ Color::red()
+ ),
+ (
+ DisplayPoint::new(5, 6)..DisplayPoint::new(5, 9),
+ Color::red()
+ )
+ ]
+ );
+ assert_eq!(search_view.active_match_index, Some(0));
+ assert_eq!(
+ search_view
+ .results_editor
+ .update(cx, |editor, cx| editor.selections.display_ranges(cx)),
+ [DisplayPoint::new(2, 32)..DisplayPoint::new(2, 35)]
+ );
+
+ search_view.select_match(Direction::Next, cx);
+ });
+
+ search_view.update(cx, |search_view, cx| {
+ assert_eq!(search_view.active_match_index, Some(1));
+ assert_eq!(
+ search_view
+ .results_editor
+ .update(cx, |editor, cx| editor.selections.display_ranges(cx)),
+ [DisplayPoint::new(2, 37)..DisplayPoint::new(2, 40)]
+ );
+ search_view.select_match(Direction::Next, cx);
+ });
+
+ search_view.update(cx, |search_view, cx| {
+ assert_eq!(search_view.active_match_index, Some(2));
+ assert_eq!(
+ search_view
+ .results_editor
+ .update(cx, |editor, cx| editor.selections.display_ranges(cx)),
+ [DisplayPoint::new(5, 6)..DisplayPoint::new(5, 9)]
+ );
+ search_view.select_match(Direction::Next, cx);
+ });
+
+ search_view.update(cx, |search_view, cx| {
+ assert_eq!(search_view.active_match_index, Some(0));
+ assert_eq!(
+ search_view
+ .results_editor
+ .update(cx, |editor, cx| editor.selections.display_ranges(cx)),
+ [DisplayPoint::new(2, 32)..DisplayPoint::new(2, 35)]
+ );
+ search_view.select_match(Direction::Prev, cx);
+ });
+
+ search_view.update(cx, |search_view, cx| {
+ assert_eq!(search_view.active_match_index, Some(2));
+ assert_eq!(
+ search_view
+ .results_editor
+ .update(cx, |editor, cx| editor.selections.display_ranges(cx)),
+ [DisplayPoint::new(5, 6)..DisplayPoint::new(5, 9)]
+ );
+ search_view.select_match(Direction::Prev, cx);
+ });
+
+ search_view.update(cx, |search_view, cx| {
+ assert_eq!(search_view.active_match_index, Some(1));
+ assert_eq!(
+ search_view
+ .results_editor
+ .update(cx, |editor, cx| editor.selections.display_ranges(cx)),
+ [DisplayPoint::new(2, 37)..DisplayPoint::new(2, 40)]
+ );
+ });
+ }
+
+ #[gpui::test]
+ async fn test_project_search_focus(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
+ init_test(cx);
+
+ let fs = FakeFs::new(cx.background());
+ fs.insert_tree(
+ "/dir",
+ json!({
+ "one.rs": "const ONE: usize = 1;",
+ "two.rs": "const TWO: usize = one::ONE + one::ONE;",
+ "three.rs": "const THREE: usize = one::ONE + two::TWO;",
+ "four.rs": "const FOUR: usize = one::ONE + three::THREE;",
+ }),
+ )
+ .await;
+ let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
+ let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+ let workspace = window.root(cx);
+
+ let active_item = cx.read(|cx| {
+ workspace
+ .read(cx)
+ .active_pane()
+ .read(cx)
+ .active_item()
+ .and_then(|item| item.downcast::<ProjectSearchView>())
+ });
+ assert!(
+ active_item.is_none(),
+ "Expected no search panel to be active, but got: {active_item:?}"
+ );
+
+ workspace.update(cx, |workspace, cx| {
+ ProjectSearchView::deploy(workspace, &workspace::NewSearch, cx)
+ });
+
+ let Some(search_view) = cx.read(|cx| {
+ workspace
+ .read(cx)
+ .active_pane()
+ .read(cx)
+ .active_item()
+ .and_then(|item| item.downcast::<ProjectSearchView>())
+ }) else {
+ panic!("Search view expected to appear after new search event trigger")
+ };
+ let search_view_id = search_view.id();
+
+ cx.spawn(|mut cx| async move {
+ window.dispatch_action(search_view_id, &ToggleFocus, &mut cx);
+ })
+ .detach();
+ deterministic.run_until_parked();
+ search_view.update(cx, |search_view, cx| {
+ assert!(
+ search_view.query_editor.is_focused(cx),
+ "Empty search view should be focused after the toggle focus event: no results panel to focus on",
+ );
+ });
+
+ search_view.update(cx, |search_view, cx| {
+ let query_editor = &search_view.query_editor;
+ assert!(
+ query_editor.is_focused(cx),
+ "Search view should be focused after the new search view is activated",
+ );
+ let query_text = query_editor.read(cx).text(cx);
+ assert!(
+ query_text.is_empty(),
+ "New search query should be empty but got '{query_text}'",
+ );
+ let results_text = search_view
+ .results_editor
+ .update(cx, |editor, cx| editor.display_text(cx));
+ assert!(
+ results_text.is_empty(),
+ "Empty search view should have no results but got '{results_text}'"
+ );
+ });
+
+ search_view.update(cx, |search_view, cx| {
+ search_view.query_editor.update(cx, |query_editor, cx| {
+ query_editor.set_text("sOMETHINGtHATsURELYdOESnOTeXIST", cx)
+ });
+ search_view.search(cx);
+ });
+ deterministic.run_until_parked();
+ search_view.update(cx, |search_view, cx| {
+ let results_text = search_view
+ .results_editor
+ .update(cx, |editor, cx| editor.display_text(cx));
+ assert!(
+ results_text.is_empty(),
+ "Search view for mismatching query should have no results but got '{results_text}'"
+ );
+ assert!(
+ search_view.query_editor.is_focused(cx),
+ "Search view should be focused after mismatching query had been used in search",
+ );
+ });
+ cx.spawn(
+ |mut cx| async move { window.dispatch_action(search_view_id, &ToggleFocus, &mut cx) },
+ )
+ .detach();
+ deterministic.run_until_parked();
+ search_view.update(cx, |search_view, cx| {
+ assert!(
+ search_view.query_editor.is_focused(cx),
+ "Search view with mismatching query should be focused after the toggle focus event: still no results panel to focus on",
+ );
+ });
+
+ search_view.update(cx, |search_view, cx| {
+ search_view
+ .query_editor
+ .update(cx, |query_editor, cx| query_editor.set_text("TWO", cx));
+ search_view.search(cx);
+ });
+ deterministic.run_until_parked();
+ search_view.update(cx, |search_view, cx| {
+ assert_eq!(
+ search_view
+ .results_editor
+ .update(cx, |editor, cx| editor.display_text(cx)),
+ "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;",
+ "Search view results should match the query"
+ );
+ assert!(
+ search_view.results_editor.is_focused(cx),
+ "Search view with mismatching query should be focused after search results are available",
+ );
+ });
+ cx.spawn(|mut cx| async move {
+ window.dispatch_action(search_view_id, &ToggleFocus, &mut cx);
+ })
+ .detach();
+ deterministic.run_until_parked();
+ search_view.update(cx, |search_view, cx| {
+ assert!(
+ search_view.results_editor.is_focused(cx),
+ "Search view with matching query should still have its results editor focused after the toggle focus event",
+ );
+ });
+
+ workspace.update(cx, |workspace, cx| {
+ ProjectSearchView::deploy(workspace, &workspace::NewSearch, cx)
+ });
+ search_view.update(cx, |search_view, cx| {
+ assert_eq!(search_view.query_editor.read(cx).text(cx), "two", "Query should be updated to first search result after search view 2nd open in a row");
+ assert_eq!(
+ search_view
+ .results_editor
+ .update(cx, |editor, cx| editor.display_text(cx)),
+ "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;",
+ "Results should be unchanged after search view 2nd open in a row"
+ );
+ assert!(
+ search_view.query_editor.is_focused(cx),
+ "Focus should be moved into query editor again after search view 2nd open in a row"
+ );
+ });
+
+ cx.spawn(|mut cx| async move {
+ window.dispatch_action(search_view_id, &ToggleFocus, &mut cx);
+ })
+ .detach();
+ deterministic.run_until_parked();
+ search_view.update(cx, |search_view, cx| {
+ assert!(
+ search_view.results_editor.is_focused(cx),
+ "Search view with matching query should switch focus to the results editor after the toggle focus event",
+ );
+ });
+ }
+
+ #[gpui::test]
+ async fn test_new_project_search_in_directory(
+ deterministic: Arc<Deterministic>,
+ cx: &mut TestAppContext,
+ ) {
+ init_test(cx);
+
+ let fs = FakeFs::new(cx.background());
+ fs.insert_tree(
+ "/dir",
+ json!({
+ "a": {
+ "one.rs": "const ONE: usize = 1;",
+ "two.rs": "const TWO: usize = one::ONE + one::ONE;",
+ },
+ "b": {
+ "three.rs": "const THREE: usize = one::ONE + two::TWO;",
+ "four.rs": "const FOUR: usize = one::ONE + three::THREE;",
+ },
+ }),
+ )
+ .await;
+ let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
+ let worktree_id = project.read_with(cx, |project, cx| {
+ project.worktrees(cx).next().unwrap().read(cx).id()
+ });
+ let workspace = cx
+ .add_window(|cx| Workspace::test_new(project, cx))
+ .root(cx);
+
+ let active_item = cx.read(|cx| {
+ workspace
+ .read(cx)
+ .active_pane()
+ .read(cx)
+ .active_item()
+ .and_then(|item| item.downcast::<ProjectSearchView>())
+ });
+ assert!(
+ active_item.is_none(),
+ "Expected no search panel to be active, but got: {active_item:?}"
+ );
+
+ let one_file_entry = cx.update(|cx| {
+ workspace
+ .read(cx)
+ .project()
+ .read(cx)
+ .entry_for_path(&(worktree_id, "a/one.rs").into(), cx)
+ .expect("no entry for /a/one.rs file")
+ });
+ assert!(one_file_entry.is_file());
+ workspace.update(cx, |workspace, cx| {
+ ProjectSearchView::new_search_in_directory(workspace, &one_file_entry, cx)
+ });
+ let active_search_entry = cx.read(|cx| {
+ workspace
+ .read(cx)
+ .active_pane()
+ .read(cx)
+ .active_item()
+ .and_then(|item| item.downcast::<ProjectSearchView>())
+ });
+ assert!(
+ active_search_entry.is_none(),
+ "Expected no search panel to be active for file entry"
+ );
+
+ let a_dir_entry = cx.update(|cx| {
+ workspace
+ .read(cx)
+ .project()
+ .read(cx)
+ .entry_for_path(&(worktree_id, "a").into(), cx)
+ .expect("no entry for /a/ directory")
+ });
+ assert!(a_dir_entry.is_dir());
+ workspace.update(cx, |workspace, cx| {
+ ProjectSearchView::new_search_in_directory(workspace, &a_dir_entry, cx)
+ });
+
+ let Some(search_view) = cx.read(|cx| {
+ workspace
+ .read(cx)
+ .active_pane()
+ .read(cx)
+ .active_item()
+ .and_then(|item| item.downcast::<ProjectSearchView>())
+ }) else {
+ panic!("Search view expected to appear after new search in directory event trigger")
+ };
+ deterministic.run_until_parked();
+ search_view.update(cx, |search_view, cx| {
+ assert!(
+ search_view.query_editor.is_focused(cx),
+ "On new search in directory, focus should be moved into query editor"
+ );
+ search_view.excluded_files_editor.update(cx, |editor, cx| {
+ assert!(
+ editor.display_text(cx).is_empty(),
+ "New search in directory should not have any excluded files"
+ );
+ });
+ search_view.included_files_editor.update(cx, |editor, cx| {
+ assert_eq!(
+ editor.display_text(cx),
+ a_dir_entry.path.to_str().unwrap(),
+ "New search in directory should have included dir entry path"
+ );
+ });
+ });
+
+ search_view.update(cx, |search_view, cx| {
+ search_view
+ .query_editor
+ .update(cx, |query_editor, cx| query_editor.set_text("const", cx));
+ search_view.search(cx);
+ });
+ deterministic.run_until_parked();
+ search_view.update(cx, |search_view, cx| {
+ assert_eq!(
+ search_view
+ .results_editor
+ .update(cx, |editor, cx| editor.display_text(cx)),
+ "\n\nconst ONE: usize = 1;\n\n\nconst TWO: usize = one::ONE + one::ONE;",
+ "New search in directory should have a filter that matches a certain directory"
+ );
+ });
+ }
+
+ #[gpui::test]
+ async fn test_search_query_history(cx: &mut TestAppContext) {
+ init_test(cx);
+
+ let fs = FakeFs::new(cx.background());
+ fs.insert_tree(
+ "/dir",
+ json!({
+ "one.rs": "const ONE: usize = 1;",
+ "two.rs": "const TWO: usize = one::ONE + one::ONE;",
+ "three.rs": "const THREE: usize = one::ONE + two::TWO;",
+ "four.rs": "const FOUR: usize = one::ONE + three::THREE;",
+ }),
+ )
+ .await;
+ let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
+ let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+ let workspace = window.root(cx);
+ workspace.update(cx, |workspace, cx| {
+ ProjectSearchView::deploy(workspace, &workspace::NewSearch, cx)
+ });
+
+ let search_view = cx.read(|cx| {
+ workspace
+ .read(cx)
+ .active_pane()
+ .read(cx)
+ .active_item()
+ .and_then(|item| item.downcast::<ProjectSearchView>())
+ .expect("Search view expected to appear after new search event trigger")
+ });
+
+ let search_bar = window.add_view(cx, |cx| {
+ let mut search_bar = ProjectSearchBar::new();
+ search_bar.set_active_pane_item(Some(&search_view), cx);
+ // search_bar.show(cx);
+ search_bar
+ });
+
+ // Add 3 search items into the history + another unsubmitted one.
+ search_view.update(cx, |search_view, cx| {
+ search_view.search_options = SearchOptions::CASE_SENSITIVE;
+ search_view
+ .query_editor
+ .update(cx, |query_editor, cx| query_editor.set_text("ONE", cx));
+ search_view.search(cx);
+ });
+ cx.foreground().run_until_parked();
+ search_view.update(cx, |search_view, cx| {
+ search_view
+ .query_editor
+ .update(cx, |query_editor, cx| query_editor.set_text("TWO", cx));
+ search_view.search(cx);
+ });
+ cx.foreground().run_until_parked();
+ search_view.update(cx, |search_view, cx| {
+ search_view
+ .query_editor
+ .update(cx, |query_editor, cx| query_editor.set_text("THREE", cx));
+ search_view.search(cx);
+ });
+ cx.foreground().run_until_parked();
+ search_view.update(cx, |search_view, cx| {
+ search_view.query_editor.update(cx, |query_editor, cx| {
+ query_editor.set_text("JUST_TEXT_INPUT", cx)
+ });
+ });
+ cx.foreground().run_until_parked();
+
+ // Ensure that the latest input with search settings is active.
+ search_view.update(cx, |search_view, cx| {
+ assert_eq!(
+ search_view.query_editor.read(cx).text(cx),
+ "JUST_TEXT_INPUT"
+ );
+ assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
+ });
+
+ // Next history query after the latest should set the query to the empty string.
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.next_history_query(&NextHistoryQuery, cx);
+ });
+ search_view.update(cx, |search_view, cx| {
+ assert_eq!(search_view.query_editor.read(cx).text(cx), "");
+ assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
+ });
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.next_history_query(&NextHistoryQuery, cx);
+ });
+ search_view.update(cx, |search_view, cx| {
+ assert_eq!(search_view.query_editor.read(cx).text(cx), "");
+ assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
+ });
+
+ // First previous query for empty current query should set the query to the latest submitted one.
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.previous_history_query(&PreviousHistoryQuery, cx);
+ });
+ search_view.update(cx, |search_view, cx| {
+ assert_eq!(search_view.query_editor.read(cx).text(cx), "THREE");
+ assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
+ });
+
+ // Further previous items should go over the history in reverse order.
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.previous_history_query(&PreviousHistoryQuery, cx);
+ });
+ search_view.update(cx, |search_view, cx| {
+ assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO");
+ assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
+ });
+
+ // Previous items should never go behind the first history item.
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.previous_history_query(&PreviousHistoryQuery, cx);
+ });
+ search_view.update(cx, |search_view, cx| {
+ assert_eq!(search_view.query_editor.read(cx).text(cx), "ONE");
+ assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
+ });
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.previous_history_query(&PreviousHistoryQuery, cx);
+ });
+ search_view.update(cx, |search_view, cx| {
+ assert_eq!(search_view.query_editor.read(cx).text(cx), "ONE");
+ assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
+ });
+
+ // Next items should go over the history in the original order.
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.next_history_query(&NextHistoryQuery, cx);
+ });
+ search_view.update(cx, |search_view, cx| {
+ assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO");
+ assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
+ });
+
+ search_view.update(cx, |search_view, cx| {
+ search_view
+ .query_editor
+ .update(cx, |query_editor, cx| query_editor.set_text("TWO_NEW", cx));
+ search_view.search(cx);
+ });
+ cx.foreground().run_until_parked();
+ search_view.update(cx, |search_view, cx| {
+ assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO_NEW");
+ assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
+ });
+
+ // New search input should add another entry to history and move the selection to the end of the history.
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.previous_history_query(&PreviousHistoryQuery, cx);
+ });
+ search_view.update(cx, |search_view, cx| {
+ assert_eq!(search_view.query_editor.read(cx).text(cx), "THREE");
+ assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
+ });
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.previous_history_query(&PreviousHistoryQuery, cx);
+ });
+ search_view.update(cx, |search_view, cx| {
+ assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO");
+ assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
+ });
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.next_history_query(&NextHistoryQuery, cx);
+ });
+ search_view.update(cx, |search_view, cx| {
+ assert_eq!(search_view.query_editor.read(cx).text(cx), "THREE");
+ assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
+ });
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.next_history_query(&NextHistoryQuery, cx);
+ });
+ search_view.update(cx, |search_view, cx| {
+ assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO_NEW");
+ assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
+ });
+ search_bar.update(cx, |search_bar, cx| {
+ search_bar.next_history_query(&NextHistoryQuery, cx);
+ });
+ search_view.update(cx, |search_view, cx| {
+ assert_eq!(search_view.query_editor.read(cx).text(cx), "");
+ assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE);
+ });
+ }
+
+ pub fn init_test(cx: &mut TestAppContext) {
+ cx.foreground().forbid_parking();
+ let fonts = cx.font_cache();
+ let mut theme = gpui::fonts::with_font_cache(fonts.clone(), theme::Theme::default);
+ theme.search.match_background = Color::red();
+
+ cx.update(|cx| {
+ cx.set_global(SettingsStore::test(cx));
+ cx.set_global(ActiveSearches::default());
+ settings::register::<SemanticIndexSettings>(cx);
+
+ theme::init((), cx);
+ cx.update_global::<SettingsStore, _, _>(|store, _| {
+ let mut settings = store.get::<ThemeSettings>(None).clone();
+ settings.theme = Arc::new(theme);
+ store.override_global(settings)
+ });
+
+ language::init(cx);
+ client::init_settings(cx);
+ editor::init(cx);
+ workspace::init_settings(cx);
+ Project::init_settings(cx);
+ super::init(cx);
+ });
+ }
+}
@@ -0,0 +1,117 @@
+use bitflags::bitflags;
+pub use buffer_search::BufferSearchBar;
+use gpui::{actions, Action, AppContext, IntoElement};
+pub use mode::SearchMode;
+use project::search::SearchQuery;
+use ui::ButtonVariant;
+//pub use project_search::{ProjectSearchBar, ProjectSearchView};
+// use theme::components::{
+// action_button::Button, svg::Svg, ComponentExt, IconButtonStyle, ToggleIconButtonStyle,
+// };
+
+pub mod buffer_search;
+mod history;
+mod mode;
+//pub mod project_search;
+pub(crate) mod search_bar;
+
+pub fn init(cx: &mut AppContext) {
+ buffer_search::init(cx);
+ //project_search::init(cx);
+}
+
+actions!(
+ CycleMode,
+ ToggleWholeWord,
+ ToggleCaseSensitive,
+ ToggleReplace,
+ SelectNextMatch,
+ SelectPrevMatch,
+ SelectAllMatches,
+ NextHistoryQuery,
+ PreviousHistoryQuery,
+ ActivateTextMode,
+ ActivateSemanticMode,
+ ActivateRegexMode,
+ ReplaceAll,
+ ReplaceNext,
+);
+
+bitflags! {
+ #[derive(Default)]
+ pub struct SearchOptions: u8 {
+ const NONE = 0b000;
+ const WHOLE_WORD = 0b001;
+ const CASE_SENSITIVE = 0b010;
+ }
+}
+
+impl SearchOptions {
+ pub fn label(&self) -> &'static str {
+ match *self {
+ SearchOptions::WHOLE_WORD => "Match Whole Word",
+ SearchOptions::CASE_SENSITIVE => "Match Case",
+ _ => panic!("{:?} is not a named SearchOption", self),
+ }
+ }
+
+ pub fn icon(&self) -> ui::Icon {
+ match *self {
+ SearchOptions::WHOLE_WORD => ui::Icon::WholeWord,
+ SearchOptions::CASE_SENSITIVE => ui::Icon::CaseSensitive,
+ _ => panic!("{:?} is not a named SearchOption", self),
+ }
+ }
+
+ pub fn to_toggle_action(&self) -> Box<dyn Action + Sync + Send + 'static> {
+ match *self {
+ SearchOptions::WHOLE_WORD => Box::new(ToggleWholeWord),
+ SearchOptions::CASE_SENSITIVE => Box::new(ToggleCaseSensitive),
+ _ => panic!("{:?} is not a named SearchOption", self),
+ }
+ }
+
+ pub fn none() -> SearchOptions {
+ SearchOptions::NONE
+ }
+
+ pub fn from_query(query: &SearchQuery) -> SearchOptions {
+ let mut options = SearchOptions::NONE;
+ options.set(SearchOptions::WHOLE_WORD, query.whole_word());
+ options.set(SearchOptions::CASE_SENSITIVE, query.case_sensitive());
+ options
+ }
+
+ pub fn as_button(&self, active: bool) -> impl IntoElement {
+ ui::IconButton::new(0, self.icon())
+ .on_click({
+ let action = self.to_toggle_action();
+ move |_, cx| {
+ cx.dispatch_action(action.boxed_clone());
+ }
+ })
+ .variant(ui::ButtonVariant::Ghost)
+ .when(active, |button| button.variant(ButtonVariant::Filled))
+ }
+}
+
+fn toggle_replace_button(active: bool) -> impl IntoElement {
+ // todo: add toggle_replace button
+ ui::IconButton::new(0, ui::Icon::Replace)
+ .on_click(|_, cx| {
+ cx.dispatch_action(Box::new(ToggleReplace));
+ cx.notify();
+ })
+ .variant(ui::ButtonVariant::Ghost)
+ .when(active, |button| button.variant(ButtonVariant::Filled))
+}
+
+fn render_replace_button(
+ action: impl Action + 'static + Send + Sync,
+ icon: ui::Icon,
+) -> impl IntoElement {
+ // todo: add tooltip
+ ui::IconButton::new(0, icon).on_click(move |_, cx| {
+ cx.dispatch_action(action.boxed_clone());
+ })
+}
@@ -0,0 +1,35 @@
+use gpui::{IntoElement, MouseDownEvent, WindowContext};
+use ui::{Button, ButtonVariant, IconButton};
+
+use crate::mode::SearchMode;
+
+pub(super) fn render_nav_button(
+ icon: ui::Icon,
+ _active: bool,
+ on_click: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
+) -> impl IntoElement {
+ // let tooltip_style = cx.theme().tooltip.clone();
+ // let cursor_style = if active {
+ // CursorStyle::PointingHand
+ // } else {
+ // CursorStyle::default()
+ // };
+ // enum NavButton {}
+ IconButton::new("search-nav-button", icon).on_click(on_click)
+}
+
+pub(crate) fn render_search_mode_button(
+ mode: SearchMode,
+ is_active: bool,
+ on_click: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
+) -> Button {
+ let button_variant = if is_active {
+ ButtonVariant::Filled
+ } else {
+ ButtonVariant::Ghost
+ };
+
+ Button::new(mode.label())
+ .on_click(on_click)
+ .variant(button_variant)
+}
@@ -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<Self>;
-
- 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().to_string()),
- )
- .child(
- div()
- .flex()
- .gap_1()
- .children(ColorScaleStep::ALL.map(|step| {
- div().flex().size_6().bg(scale.step(cx, step))
- })),
- )
- })),
- )
- }
-}
@@ -27,7 +27,7 @@ impl FocusStory {
}
impl Render for FocusStory {
- type Element = Focusable<Self, Stateful<Self, Div<Self>>>;
+ type Element = Focusable<Stateful<Div>>;
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> Self::Element {
let theme = cx.theme();
@@ -42,18 +42,18 @@ impl Render for FocusStory {
.id("parent")
.focusable()
.key_context("parent")
- .on_action(|_, action: &ActionA, cx| {
+ .on_action(cx.listener(|_, action: &ActionA, cx| {
println!("Action A dispatched on parent");
- })
- .on_action(|_, action: &ActionB, cx| {
+ }))
+ .on_action(cx.listener(|_, action: &ActionB, cx| {
println!("Action B dispatched on parent");
- })
- .on_focus(|_, _, _| println!("Parent focused"))
- .on_blur(|_, _, _| println!("Parent blurred"))
- .on_focus_in(|_, _, _| println!("Parent focus_in"))
- .on_focus_out(|_, _, _| println!("Parent focus_out"))
- .on_key_down(|_, event, phase, _| println!("Key down on parent {:?}", event))
- .on_key_up(|_, event, phase, _| println!("Key up on parent {:?}", event))
+ }))
+ .on_focus(cx.listener(|_, _, _| println!("Parent focused")))
+ .on_blur(cx.listener(|_, _, _| println!("Parent blurred")))
+ .on_focus_in(cx.listener(|_, _, _| println!("Parent focus_in")))
+ .on_focus_out(cx.listener(|_, _, _| println!("Parent focus_out")))
+ .on_key_down(cx.listener(|_, event, _| println!("Key down on parent {:?}", event)))
+ .on_key_up(cx.listener(|_, event, _| println!("Key up on parent {:?}", event)))
.size_full()
.bg(color_1)
.focus(|style| style.bg(color_2))
@@ -61,38 +61,42 @@ impl Render for FocusStory {
div()
.track_focus(&self.child_1_focus)
.key_context("child-1")
- .on_action(|_, action: &ActionB, cx| {
+ .on_action(cx.listener(|_, action: &ActionB, cx| {
println!("Action B dispatched on child 1 during");
- })
+ }))
.w_full()
.h_6()
.bg(color_4)
.focus(|style| style.bg(color_5))
.in_focus(|style| style.bg(color_6))
- .on_focus(|_, _, _| println!("Child 1 focused"))
- .on_blur(|_, _, _| println!("Child 1 blurred"))
- .on_focus_in(|_, _, _| println!("Child 1 focus_in"))
- .on_focus_out(|_, _, _| println!("Child 1 focus_out"))
- .on_key_down(|_, event, phase, _| println!("Key down on child 1 {:?}", event))
- .on_key_up(|_, event, phase, _| println!("Key up on child 1 {:?}", event))
+ .on_focus(cx.listener(|_, _, _| println!("Child 1 focused")))
+ .on_blur(cx.listener(|_, _, _| println!("Child 1 blurred")))
+ .on_focus_in(cx.listener(|_, _, _| println!("Child 1 focus_in")))
+ .on_focus_out(cx.listener(|_, _, _| println!("Child 1 focus_out")))
+ .on_key_down(
+ cx.listener(|_, event, _| println!("Key down on child 1 {:?}", event)),
+ )
+ .on_key_up(cx.listener(|_, event, _| println!("Key up on child 1 {:?}", event)))
.child("Child 1"),
)
.child(
div()
.track_focus(&self.child_2_focus)
.key_context("child-2")
- .on_action(|_, action: &ActionC, cx| {
+ .on_action(cx.listener(|_, action: &ActionC, cx| {
println!("Action C dispatched on child 2");
- })
+ }))
.w_full()
.h_6()
.bg(color_4)
- .on_focus(|_, _, _| println!("Child 2 focused"))
- .on_blur(|_, _, _| println!("Child 2 blurred"))
- .on_focus_in(|_, _, _| println!("Child 2 focus_in"))
- .on_focus_out(|_, _, _| println!("Child 2 focus_out"))
- .on_key_down(|_, event, phase, _| println!("Key down on child 2 {:?}", event))
- .on_key_up(|_, event, phase, _| println!("Key up on child 2 {:?}", event))
+ .on_focus(cx.listener(|_, _, _| println!("Child 2 focused")))
+ .on_blur(cx.listener(|_, _, _| println!("Child 2 blurred")))
+ .on_focus_in(cx.listener(|_, _, _| println!("Child 2 focus_in")))
+ .on_focus_out(cx.listener(|_, _, _| println!("Child 2 focus_out")))
+ .on_key_down(
+ cx.listener(|_, event, _| println!("Key down on child 2 {:?}", event)),
+ )
+ .on_key_up(cx.listener(|_, event, _| println!("Key up on child 2 {:?}", event)))
.child("Child 2"),
)
}
@@ -1,8 +1,10 @@
-use crate::{story::Story, story_selector::ComponentStory};
use gpui::{prelude::*, Div, Render, Stateful, View};
+use story::Story;
use strum::IntoEnumIterator;
use ui::prelude::*;
+use crate::story_selector::ComponentStory;
+
pub struct KitchenSinkStory;
impl KitchenSinkStory {
@@ -12,18 +14,18 @@ impl KitchenSinkStory {
}
impl Render for KitchenSinkStory {
- type Element = Stateful<Self, Div<Self>>;
+ type Element = Stateful<Div>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
let component_stories = ComponentStory::iter()
.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.
@@ -1,8 +1,11 @@
use fuzzy::StringMatchCandidate;
-use gpui::{div, prelude::*, Div, KeyBinding, Render, Styled, Task, View, WindowContext};
+use gpui::{
+ div, prelude::*, Div, KeyBinding, Render, SharedString, Styled, Task, View, WindowContext,
+};
use picker::{Picker, PickerDelegate};
use std::sync::Arc;
use theme2::ActiveTheme;
+use ui::{Label, ListItem};
pub struct PickerStory {
picker: View<Picker<Delegate>>,
@@ -34,7 +37,7 @@ impl Delegate {
}
impl PickerDelegate for Delegate {
- type ListItem = Div<Picker<Self>>;
+ type ListItem = ListItem;
fn match_count(&self) -> usize {
self.candidates.len()
@@ -49,24 +52,18 @@ impl PickerDelegate for Delegate {
ix: usize,
selected: bool,
cx: &mut gpui::ViewContext<Picker<Self>>,
- ) -> Self::ListItem {
- let colors = cx.theme().colors();
+ ) -> Option<Self::ListItem> {
let Some(candidate_ix) = self.matches.get(ix) else {
- return div();
+ return None;
};
- let candidate = self.candidates[*candidate_ix].string.clone();
-
- div()
- .text_color(colors.text)
- .when(selected, |s| {
- s.border_l_10().border_color(colors.terminal_ansi_yellow)
- })
- .hover(|style| {
- style
- .bg(colors.element_active)
- .text_color(colors.text_accent)
- })
- .child(candidate)
+ // TASK: Make StringMatchCandidate::string a SharedString
+ let candidate = SharedString::from(self.candidates[*candidate_ix].string.clone());
+
+ Some(
+ ListItem::new(ix)
+ .selected(selected)
+ .child(Label::new(candidate)),
+ )
}
fn selected_index(&self) -> usize {
@@ -203,7 +200,7 @@ impl PickerStory {
}
impl Render for PickerStory {
- type Element = Div<Self>;
+ type Element = Div;
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> Self::Element {
div()
@@ -11,7 +11,7 @@ impl ScrollStory {
}
impl Render for ScrollStory {
- type Element = Stateful<Self, Div<Self>>;
+ 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 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, ParentComponent, Render, Styled, View, VisualContext, WindowContext,
+ blue, div, green, red, white, Div, InteractiveText, ParentElement, Render, Styled, StyledText,
+ TextRun, View, VisualContext, WindowContext,
};
use ui::v_stack;
@@ -12,7 +13,7 @@ impl TextStory {
}
impl Render for TextStory {
- type Element = Div<Self>;
+ type Element = Div;
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> Self::Element {
v_stack()
@@ -55,6 +56,21 @@ impl Render for TextStory {
"flex-row. width 96. The quick brown fox jumps over the lazy dog. ",
"Meanwhile, the lazy dog decided it was time for a change. ",
"He started daily workout routines, ate healthier and became the fastest dog in town.",
- )))
+ ))).child(
+ InteractiveText::new(
+ "interactive",
+ StyledText::new("Hello world, how is it going?").with_runs(vec![
+ cx.text_style().to_run(6),
+ TextRun {
+ background_color: Some(green()),
+ ..cx.text_style().to_run(5)
+ },
+ cx.text_style().to_run(18),
+ ]),
+ )
+ .on_click(vec![2..4, 1..3, 7..9], |range_ix, cx| {
+ println!("Clicked range {range_ix}");
+ })
+ )
}
}
@@ -1,52 +1,49 @@
-use gpui::{px, rgb, Div, Hsla, Render};
+use gpui::{px, rgb, Div, Hsla, IntoElement, Render, RenderOnce};
+use story::Story;
use ui::prelude::*;
-use crate::story::Story;
-
/// A reimplementation of the MDN `z-index` example, found here:
/// [https://developer.mozilla.org/en-US/docs/Web/CSS/z-index](https://developer.mozilla.org/en-US/docs/Web/CSS/z-index).
pub struct ZIndexStory;
impl Render for ZIndexStory {
- type Element = Div<Self>;
+ 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,19 +74,17 @@ trait Styles: Styled + Sized {
}
}
-impl<V: 'static> Styles for Div<V> {}
+impl Styles for Div {}
-#[derive(Component)]
+#[derive(IntoElement)]
struct ZIndexExample {
z_index: u32,
}
-impl ZIndexExample {
- pub fn new(z_index: u32) -> Self {
- Self { z_index }
- }
+impl RenderOnce for ZIndexExample {
+ type Rendered = Div;
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+ fn render(self, cx: &mut WindowContext) -> Self::Rendered {
div()
.relative()
.size_full()
@@ -109,14 +104,14 @@ impl ZIndexExample {
// HACK: Simulate `text-align: center`.
.pl(px(24.))
.z_index(self.z_index)
- .child(format!(
+ .child(SharedString::from(format!(
"z-index: {}",
if self.z_index == 0 {
"auto".to_string()
} else {
self.z_index.to_string()
}
- )),
+ ))),
)
// Blue blocks.
.child(
@@ -173,3 +168,9 @@ impl ZIndexExample {
)
}
}
+
+impl ZIndexExample {
+ pub fn new(z_index: u32) -> Self {
+ Self { z_index }
+ }
+}
@@ -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);
@@ -106,7 +102,7 @@ impl StoryWrapper {
}
impl Render for StoryWrapper {
- type Element = Div<Self>;
+ 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 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!()
+// }
@@ -4,7 +4,7 @@ use crate::TerminalView;
use db::kvp::KEY_VALUE_STORE;
use gpui::{
actions, div, serde_json, AppContext, AsyncWindowContext, Div, Entity, EventEmitter,
- FocusHandle, FocusableView, ParentComponent, Render, Subscription, Task, View, ViewContext,
+ FocusHandle, FocusableView, ParentElement, Render, Subscription, Task, View, ViewContext,
VisualContext, WeakView, WindowContext,
};
use project::Fs;
@@ -336,7 +336,7 @@ impl TerminalPanel {
impl EventEmitter<PanelEvent> for TerminalPanel {}
impl Render for TerminalPanel {
- type Element = Div<Self>;
+ type Element = Div;
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
div().child(self.pane.clone())
@@ -9,10 +9,10 @@ pub mod terminal_panel;
// use crate::terminal_element::TerminalElement;
use editor::{scroll::autoscroll::Autoscroll, Editor};
use gpui::{
- actions, div, Action, AnyElement, AppContext, Component, DispatchPhase, Div, EventEmitter,
- FocusEvent, FocusHandle, Focusable, FocusableComponent, FocusableView, InputHandler,
- InteractiveComponent, KeyDownEvent, Keystroke, Model, MouseButton, ParentComponent, Pixels,
- Render, SharedString, Styled, Task, View, ViewContext, VisualContext, WeakView,
+ actions, div, Action, AnyElement, AppContext, Div, Element, EventEmitter, FocusEvent,
+ FocusHandle, Focusable, FocusableElement, FocusableView, InputHandler, InteractiveElement,
+ KeyDownEvent, Keystroke, Model, MouseButton, MouseDownEvent, ParentElement, Pixels, Render,
+ SharedString, Styled, Task, View, ViewContext, VisualContext, WeakView, WindowContext,
};
use language::Bias;
use persistence::TERMINAL_DB;
@@ -31,7 +31,7 @@ use workspace::{
notifications::NotifyResultExt,
register_deserializable_item,
searchable::{SearchEvent, SearchOptions, SearchableItem},
- ui::{ContextMenu, Icon, IconElement, Label, ListEntry},
+ ui::{ContextMenu, Icon, IconElement, Label},
CloseActiveItem, NewCenterTerminal, Pane, ToolbarItemLocation, Workspace, WorkspaceId,
};
@@ -63,7 +63,6 @@ pub struct SendKeystroke(String);
actions!(Clear, Copy, Paste, ShowCharacterPalette, SearchTest);
pub fn init(cx: &mut AppContext) {
- workspace::ui::init(cx);
terminal_panel::init(cx);
terminal::init(cx);
@@ -84,7 +83,7 @@ pub struct TerminalView {
has_new_content: bool,
//Currently using iTerm bell, show bell emoji in tab until input is received
has_bell: bool,
- context_menu: Option<View<ContextMenu<Self>>>,
+ context_menu: Option<View<ContextMenu>>,
blink_state: bool,
blinking_on: bool,
blinking_paused: bool,
@@ -300,11 +299,8 @@ impl TerminalView {
cx: &mut ViewContext<Self>,
) {
self.context_menu = Some(ContextMenu::build(cx, |menu, _| {
- menu.action(ListEntry::new(Label::new("Clear")), Box::new(Clear))
- .action(
- ListEntry::new(Label::new("Close")),
- Box::new(CloseActiveItem { save_intent: None }),
- )
+ menu.action("Clear", Box::new(Clear))
+ .action("Close", Box::new(CloseActiveItem { save_intent: None }))
}));
dbg!(&position);
// todo!()
@@ -505,12 +501,7 @@ pub fn regex_search_for_query(query: &project::search::SearchQuery) -> Option<Re
}
impl TerminalView {
- fn key_down(
- &mut self,
- event: &KeyDownEvent,
- _dispatch_phase: DispatchPhase,
- cx: &mut ViewContext<Self>,
- ) {
+ fn key_down(&mut self, event: &KeyDownEvent, cx: &mut ViewContext<Self>) {
self.clear_bel(cx);
self.pause_cursor_blinking(cx);
@@ -538,7 +529,7 @@ impl TerminalView {
}
impl Render for TerminalView {
- type Element = Focusable<Self, Div<Self>>;
+ type Element = Focusable<Div>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
let terminal_handle = self.terminal.clone().downgrade();
@@ -552,14 +543,14 @@ impl Render for TerminalView {
div()
.z_index(0)
.absolute()
- .on_key_down(Self::key_down)
- .on_action(TerminalView::send_text)
- .on_action(TerminalView::send_keystroke)
- .on_action(TerminalView::copy)
- .on_action(TerminalView::paste)
- .on_action(TerminalView::clear)
- .on_action(TerminalView::show_character_palette)
- .on_action(TerminalView::select_all)
+ .on_key_down(cx.listener(Self::key_down))
+ .on_action(cx.listener(TerminalView::send_text))
+ .on_action(cx.listener(TerminalView::send_keystroke))
+ .on_action(cx.listener(TerminalView::copy))
+ .on_action(cx.listener(TerminalView::paste))
+ .on_action(cx.listener(TerminalView::clear))
+ .on_action(cx.listener(TerminalView::show_character_palette))
+ .on_action(cx.listener(TerminalView::select_all))
// todo!()
.child(
"TERMINAL HERE", // TerminalElement::new(
@@ -569,19 +560,22 @@ impl Render for TerminalView {
// self.can_navigate_to_selected_word,
// )
)
- .on_mouse_down(MouseButton::Right, |this, event, cx| {
- this.deploy_context_menu(event.position, cx);
- cx.notify();
- }),
+ .on_mouse_down(
+ MouseButton::Right,
+ cx.listener(|this, event: &MouseDownEvent, cx| {
+ this.deploy_context_menu(event.position, cx);
+ cx.notify();
+ }),
+ ),
)
.children(
self.context_menu
.clone()
- .map(|context_menu| div().z_index(1).absolute().child(context_menu.render())),
+ .map(|context_menu| div().z_index(1).absolute().child(context_menu)),
)
.track_focus(&self.focus_handle)
- .on_focus_in(Self::focus_in)
- .on_focus_out(Self::focus_out)
+ .on_focus_in(cx.listener(Self::focus_in))
+ .on_focus_out(cx.listener(Self::focus_out))
}
}
@@ -746,17 +740,13 @@ impl Item for TerminalView {
Some(self.terminal().read(cx).title().into())
}
- fn tab_content<T: 'static>(
- &self,
- _detail: Option<usize>,
- cx: &gpui::AppContext,
- ) -> AnyElement<T> {
+ fn tab_content(&self, _detail: Option<usize>, cx: &WindowContext) -> AnyElement {
let title = self.terminal().read(cx).title();
div()
.child(IconElement::new(Icon::Terminal))
- .child(title)
- .render()
+ .child(Label::new(title))
+ .into_any()
}
fn clone_on_split(
@@ -792,7 +782,7 @@ impl Item for TerminalView {
// }
fn breadcrumb_location(&self) -> ToolbarItemLocation {
- ToolbarItemLocation::PrimaryLeft { flex: None }
+ ToolbarItemLocation::PrimaryLeft
}
fn breadcrumbs(&self, _: &theme::Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>> {
@@ -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, Component, Div, ParentComponent, Styled, ViewContext};
-
-use crate::ActiveTheme;
-
-pub struct Story {}
-
-impl Story {
- pub fn container<V: 'static>(cx: &mut ViewContext<V>) -> Div<V> {
- div()
- .size_full()
- .flex()
- .flex_col()
- .pt_2()
- .px_4()
- .font("Zed Mono")
- .bg(cx.theme().colors().background)
- }
-
- pub fn title<V: 'static>(cx: &mut ViewContext<V>, title: &str) -> impl Component<V> {
- div()
- .text_xl()
- .text_color(cx.theme().colors().text)
- .child(title.to_owned())
- }
-
- pub fn title_for<V: 'static, T>(cx: &mut ViewContext<V>) -> impl Component<V> {
- Self::title(cx, std::any::type_name::<T>())
- }
-
- pub fn label<V: 'static>(cx: &mut ViewContext<V>, label: &str) -> impl Component<V> {
- div()
- .mt_4()
- .mb_2()
- .text_xs()
- .text_color(cx.theme().colors().text)
- .child(label.to_owned())
- }
-}
@@ -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, ParentComponent, Render, Styled, ViewContext};
-
- pub struct PlayerStory;
-
- impl Render for PlayerStory {
- type Element = Div<Self>;
-
- 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())
+ }),
+ )),
+ )
+ }
+}
@@ -63,6 +63,12 @@ impl ActiveTheme for AppContext {
}
}
+// impl<'a> ActiveTheme for WindowContext<'a> {
+// fn theme(&self) -> &Arc<Theme> {
+// &ThemeSettings::get_global(self.app()).active_theme
+// }
+// }
+
pub struct ThemeFamily {
pub id: String,
pub name: SharedString,
@@ -128,6 +134,12 @@ impl Theme {
ignored: self.status().ignored,
}
}
+
+ /// Returns the [`Appearance`] for the theme.
+ #[inline(always)]
+ pub fn appearance(&self) -> Appearance {
+ self.appearance
+ }
}
#[derive(Clone, Debug, Default)]
@@ -138,8 +150,3 @@ pub struct DiagnosticStyle {
pub hint: Hsla,
pub ignored: Hsla,
}
-
-#[cfg(feature = "stories")]
-mod story;
-#[cfg(feature = "stories")]
-pub use story::*;
@@ -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"]
@@ -49,13 +49,13 @@ use gpui::hsla
impl<V: 'static> TodoList<V> {
// ...
- fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+ fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Element<V> {
div().size_4().bg(hsla(50.0/360.0, 1.0, 0.5, 1.0))
}
}
~~~
-Every component needs a render method, and it should return `impl Component<V>`. This basic component will render a 16x16px yellow square on the screen.
+Every component needs a render method, and it should return `impl Element<V>`. This basic component will render a 16x16px yellow square on the screen.
A couple of questions might come to mind:
@@ -87,7 +87,7 @@ We can access the current theme's colors like this:
~~~rust
impl<V: 'static> TodoList<V> {
// ...
- fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+ fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Element<V> {
let color = cx.theme().colors()
div().size_4().hsla(50.0/360.0, 1.0, 0.5, 1.0)
@@ -102,7 +102,7 @@ use gpui::hsla
impl<V: 'static> TodoList<V> {
// ...
- fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+ fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Element<V> {
let color = cx.theme().colors()
div().size_4().bg(color.surface)
@@ -117,7 +117,7 @@ use gpui::hsla
impl<V: 'static> TodoList<V> {
// ...
- fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+ fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Element<V> {
let color = cx.theme().colors()
div()
@@ -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,27 +1,25 @@
-use gpui::img;
+use std::sync::Arc;
use crate::prelude::*;
+use gpui::{img, ImageData, ImageSource, Img, IntoElement};
-#[derive(Component)]
+#[derive(Debug, Default, PartialEq, Clone)]
+pub enum Shape {
+ #[default]
+ Circle,
+ RoundedRectangle,
+}
+
+#[derive(IntoElement)]
pub struct Avatar {
- src: SharedString,
+ src: ImageSource,
shape: Shape,
}
-impl Avatar {
- pub fn new(src: impl Into<SharedString>) -> Self {
- Self {
- src: src.into(),
- shape: Shape::Circle,
- }
- }
-
- pub fn shape(mut self, shape: Shape) -> Self {
- self.shape = shape;
- self
- }
+impl RenderOnce for Avatar {
+ type Rendered = Img;
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+ fn render(self, _: &mut WindowContext) -> Self::Rendered {
let mut img = img();
if self.shape == Shape::Circle {
@@ -30,37 +28,35 @@ impl Avatar {
img = img.rounded_md();
}
- img.uri(self.src.clone())
+ img.source(self.src.clone())
.size_4()
// todo!(Pull the avatar fallback background from the theme.)
.bg(gpui::red())
}
}
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use super::*;
- use crate::Story;
- use gpui::{Div, Render};
-
- pub struct AvatarStory;
-
- impl Render for AvatarStory {
- type Element = Div<Self>;
+impl Avatar {
+ 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,
+ }
+ }
- 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",
- ))
+ pub fn source(src: ImageSource) -> Self {
+ Self {
+ src,
+ shape: Shape::Circle,
}
}
+ pub fn shape(mut self, shape: Shape) -> Self {
+ self.shape = shape;
+ self
+ }
}
@@ -1,25 +1,28 @@
-use std::sync::Arc;
+use std::rc::Rc;
-use gpui::{DefiniteLength, Hsla, MouseButton, StatefulInteractiveComponent, WindowContext};
+use gpui::{
+ DefiniteLength, Div, Hsla, IntoElement, MouseButton, MouseDownEvent,
+ StatefulInteractiveElement, WindowContext,
+};
use crate::prelude::*;
-use crate::{h_stack, Icon, IconButton, IconElement, Label, LineHeightStyle, TextColor};
+use crate::{h_stack, Color, Icon, IconButton, IconElement, Label, LineHeightStyle};
/// Provides the flexibility to use either a standard
/// button or an icon button in a given context.
-pub enum ButtonOrIconButton<V: 'static> {
- Button(Button<V>),
- IconButton(IconButton<V>),
+pub enum ButtonOrIconButton {
+ Button(Button),
+ IconButton(IconButton),
}
-impl<V: 'static> From<Button<V>> for ButtonOrIconButton<V> {
- fn from(value: Button<V>) -> Self {
+impl From<Button> for ButtonOrIconButton {
+ fn from(value: Button) -> Self {
Self::Button(value)
}
}
-impl<V: 'static> From<IconButton<V>> for ButtonOrIconButton<V> {
- fn from(value: IconButton<V>) -> Self {
+impl From<IconButton> for ButtonOrIconButton {
+ fn from(value: IconButton) -> Self {
Self::IconButton(value)
}
}
@@ -61,38 +64,74 @@ impl ButtonVariant {
}
}
-pub type ClickHandler<V> = Arc<dyn Fn(&mut V, &mut ViewContext<V>)>;
-
-struct ButtonHandlers<V: 'static> {
- click: Option<ClickHandler<V>>,
-}
-
-unsafe impl<S> Send for ButtonHandlers<S> {}
-unsafe impl<S> Sync for ButtonHandlers<S> {}
-
-impl<V: 'static> Default for ButtonHandlers<V> {
- fn default() -> Self {
- Self { click: None }
- }
-}
-
-#[derive(Component)]
-pub struct Button<V: 'static> {
+#[derive(IntoElement)]
+pub struct Button {
disabled: bool,
- handlers: ButtonHandlers<V>,
+ click_handler: Option<Rc<dyn Fn(&MouseDownEvent, &mut WindowContext)>>,
icon: Option<Icon>,
icon_position: Option<IconPosition>,
label: SharedString,
variant: ButtonVariant,
width: Option<DefiniteLength>,
- color: Option<TextColor>,
+ color: Option<Color>,
}
-impl<V: 'static> Button<V> {
+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, _) => (Color::Disabled, Color::Disabled),
+ (_, None) => (Color::Default, Color::Default),
+ (_, Some(color)) => (Color::from(color), color),
+ };
+
+ let mut button = h_stack()
+ .id(SharedString::from(format!("{}", self.label)))
+ .relative()
+ .p_1()
+ .text_ui()
+ .rounded_md()
+ .bg(self.variant.bg_color(cx))
+ .cursor_pointer()
+ .hover(|style| style.bg(self.variant.bg_color_hover(cx)))
+ .active(|style| style.bg(self.variant.bg_color_active(cx)));
+
+ match (self.icon, self.icon_position) {
+ (Some(_), Some(IconPosition::Left)) => {
+ button = button
+ .gap_1()
+ .child(self.render_label(label_color))
+ .children(self.render_icon(icon_color))
+ }
+ (Some(_), Some(IconPosition::Right)) => {
+ button = button
+ .gap_1()
+ .children(self.render_icon(icon_color))
+ .child(self.render_label(label_color))
+ }
+ (_, _) => button = button.child(self.render_label(label_color)),
+ }
+
+ if let Some(width) = self.width {
+ button = button.w(width).justify_center();
+ }
+
+ if let Some(click_handler) = self.click_handler.clone() {
+ button = button.on_mouse_down(MouseButton::Left, move |event, cx| {
+ click_handler(event, cx);
+ });
+ }
+
+ button
+ }
+}
+
+impl Button {
pub fn new(label: impl Into<SharedString>) -> Self {
Self {
disabled: false,
- handlers: ButtonHandlers::default(),
+ click_handler: None,
icon: None,
icon_position: None,
label: label.into(),
@@ -129,8 +168,11 @@ impl<V: 'static> Button<V> {
self
}
- pub fn on_click(mut self, handler: ClickHandler<V>) -> Self {
- self.handlers.click = Some(handler);
+ pub fn on_click(
+ mut self,
+ handler: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
+ ) -> Self {
+ self.click_handler = Some(Rc::new(handler));
self
}
@@ -139,14 +181,14 @@ impl<V: 'static> Button<V> {
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 {
@@ -154,249 +196,38 @@ impl<V: 'static> Button<V> {
}
}
- 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))
}
-
- pub fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- 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),
- };
-
- let mut button = h_stack()
- .id(SharedString::from(format!("{}", self.label)))
- .relative()
- .p_1()
- .text_ui()
- .rounded_md()
- .bg(self.variant.bg_color(cx))
- .cursor_pointer()
- .hover(|style| style.bg(self.variant.bg_color_hover(cx)))
- .active(|style| style.bg(self.variant.bg_color_active(cx)));
-
- match (self.icon, self.icon_position) {
- (Some(_), Some(IconPosition::Left)) => {
- button = button
- .gap_1()
- .child(self.render_label(label_color))
- .children(self.render_icon(icon_color))
- }
- (Some(_), Some(IconPosition::Right)) => {
- button = button
- .gap_1()
- .children(self.render_icon(icon_color))
- .child(self.render_label(label_color))
- }
- (_, _) => button = button.child(self.render_label(label_color)),
- }
-
- if let Some(width) = self.width {
- button = button.w(width).justify_center();
- }
-
- if let Some(click_handler) = self.handlers.click.clone() {
- button = button.on_mouse_down(MouseButton::Left, move |state, event, cx| {
- click_handler(state, cx);
- });
- }
-
- button
- }
}
-#[derive(Component)]
-pub struct ButtonGroup<V: 'static> {
- buttons: Vec<Button<V>>,
+#[derive(IntoElement)]
+pub struct ButtonGroup {
+ buttons: Vec<Button>,
}
-impl<V: 'static> ButtonGroup<V> {
- pub fn new(buttons: Vec<Button<V>>) -> Self {
- Self { buttons }
- }
+impl RenderOnce for ButtonGroup {
+ type Rendered = Div;
- fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- let mut el = h_stack().text_ui();
+ fn render(self, cx: &mut WindowContext) -> Self::Rendered {
+ let mut group = h_stack();
- for button in self.buttons {
- el = el.child(button.render(_view, cx));
+ for button in self.buttons.into_iter() {
+ group = group.child(button.render(cx));
}
- el
+ group
}
}
-#[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<Self>;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- let states = InteractionState::iter();
-
- Story::container(cx)
- .child(Story::title_for::<_, Button<Self>>(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(Arc::new(|_view, _cx| println!("Button clicked."))),
- )
- }
+impl ButtonGroup {
+ pub fn new(buttons: Vec<Button>) -> Self {
+ Self { buttons }
}
}
@@ -1,25 +1,147 @@
-use gpui::{div, prelude::*, Component, ElementId, Styled, ViewContext};
-use std::sync::Arc;
+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<V> = Arc<dyn Fn(Selection, &mut V, &mut ViewContext<V>) + Send + Sync>;
+pub type CheckHandler = Box<dyn Fn(&Selection, &mut WindowContext) + 'static>;
/// # Checkbox
///
/// 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(Component)]
-pub struct Checkbox<V: 'static> {
+#[derive(IntoElement)]
+pub struct Checkbox {
id: ElementId,
checked: Selection,
disabled: bool,
- on_click: Option<CheckHandler<V>>,
+ on_click: Option<CheckHandler>,
}
-impl<V: 'static> Checkbox<V> {
+impl RenderOnce for Checkbox {
+ type Rendered = gpui::Stateful<Div>;
+
+ fn render(self, cx: &mut WindowContext) -> Self::Rendered {
+ let group_id = format!("checkbox_group_{:?}", self.id);
+
+ let icon = match self.checked {
+ // When selected, we show a checkmark.
+ Selection::Selected => {
+ Some(
+ IconElement::new(Icon::Check)
+ .size(crate::IconSize::Small)
+ .color(
+ // If the checkbox is disabled we change the color of the icon.
+ if self.disabled {
+ Color::Disabled
+ } else {
+ Color::Selected
+ },
+ ),
+ )
+ }
+ // In an indeterminate state, we show a dash.
+ Selection::Indeterminate => {
+ Some(
+ IconElement::new(Icon::Dash)
+ .size(crate::IconSize::Small)
+ .color(
+ // If the checkbox is disabled we change the color of the icon.
+ if self.disabled {
+ Color::Disabled
+ } else {
+ Color::Selected
+ },
+ ),
+ )
+ }
+ // When unselected, we show nothing.
+ Selection::Unselected => None,
+ };
+
+ // A checkbox could be in an indeterminate state,
+ // for example the indeterminate state could represent:
+ // - a group of options of which only some are selected
+ // - an enabled option that is no longer available
+ // - a previously agreed to license that has been updated
+ //
+ // For the sake of styles we treat the indeterminate state as selected,
+ // but it's icon will be different.
+ let selected =
+ self.checked == Selection::Selected || self.checked == Selection::Indeterminate;
+
+ // We could use something like this to make the checkbox background when selected:
+ //
+ // ~~~rust
+ // ...
+ // .when(selected, |this| {
+ // this.bg(cx.theme().colors().element_selected)
+ // })
+ // ~~~
+ //
+ // But we use a match instead here because the checkbox might be disabled,
+ // and it could be disabled _while_ it is selected, as well as while it is not selected.
+ let (bg_color, border_color) = match (self.disabled, selected) {
+ (true, _) => (
+ cx.theme().colors().ghost_element_disabled,
+ cx.theme().colors().border_disabled,
+ ),
+ (false, true) => (
+ cx.theme().colors().element_selected,
+ cx.theme().colors().border,
+ ),
+ (false, false) => (
+ cx.theme().colors().element_background,
+ cx.theme().colors().border,
+ ),
+ };
+
+ div()
+ .id(self.id)
+ // Rather than adding `px_1()` to add some space around the checkbox,
+ // we use a larger parent element to create a slightly larger
+ // click area for the checkbox.
+ .size_5()
+ // Because we've enlarged the click area, we need to create a
+ // `group` to pass down interactivity events to the checkbox.
+ .group(group_id.clone())
+ .child(
+ div()
+ .flex()
+ // This prevent the flex element from growing
+ // or shrinking in response to any size changes
+ .flex_none()
+ // The combo of `justify_center()` and `items_center()`
+ // is used frequently to center elements in a flex container.
+ //
+ // We use this to center the icon in the checkbox.
+ .justify_center()
+ .items_center()
+ .m_1()
+ .size_4()
+ .rounded_sm()
+ .bg(bg_color)
+ .border()
+ .border_color(border_color)
+ // We only want the interactivity states to fire when we
+ // are in a checkbox that isn't disabled.
+ .when(!self.disabled, |this| {
+ // Here instead of `hover()` we use `group_hover()`
+ // to pass it the group id.
+ this.group_hover(group_id.clone(), |el| {
+ el.bg(cx.theme().colors().element_hover)
+ })
+ })
+ .children(icon),
+ )
+ .when_some(
+ self.on_click.filter(|_| !self.disabled),
+ |this, on_click| this.on_click(move |_, cx| on_click(&self.checked.inverse(), cx)),
+ )
+ }
+}
+impl Checkbox {
pub fn new(id: impl Into<ElementId>, checked: Selection) -> Self {
Self {
id: id.into(),
@@ -36,13 +158,13 @@ impl<V: 'static> Checkbox<V> {
pub fn on_click(
mut self,
- handler: impl 'static + Fn(Selection, &mut V, &mut ViewContext<V>) + Send + Sync,
+ handler: impl 'static + Fn(&Selection, &mut WindowContext) + Send + Sync,
) -> Self {
- self.on_click = Some(Arc::new(handler));
+ self.on_click = Some(Box::new(handler));
self
}
- pub fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+ pub fn render(self, cx: &mut WindowContext) -> impl Element {
let group_id = format!("checkbox_group_{:?}", self.id);
let icon = match self.checked {
@@ -54,9 +176,9 @@ impl<V: 'static> Checkbox<V> {
.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
},
),
)
@@ -69,9 +191,9 @@ impl<V: 'static> Checkbox<V> {
.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
},
),
)
@@ -157,69 +279,7 @@ impl<V: 'static> Checkbox<V> {
)
.when_some(
self.on_click.filter(|_| !self.disabled),
- |this, on_click| {
- this.on_click(move |view, _, cx| on_click(self.checked.inverse(), view, cx))
- },
+ |this, on_click| this.on_click(move |_, cx| on_click(&self.checked.inverse(), cx)),
)
}
}
-
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use super::*;
- use crate::{h_stack, Story};
- use gpui::{Div, Render};
-
- pub struct CheckboxStory;
-
- impl Render for CheckboxStory {
- type Element = Div<Self>;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- Story::container(cx)
- .child(Story::title_for::<_, Checkbox<Self>>(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,47 +1,42 @@
use std::cell::RefCell;
use std::rc::Rc;
-use crate::prelude::*;
-use crate::{v_stack, Label, List, ListEntry, ListItem, ListSeparator, ListSubHeader};
+use crate::{prelude::*, v_stack, Label, List};
+use crate::{ListItem, ListSeparator, ListSubHeader};
use gpui::{
- overlay, px, Action, AnchorCorner, AnyElement, AppContext, Bounds, DispatchPhase, Div,
- EventEmitter, FocusHandle, FocusableView, LayoutId, ManagedView, Manager, MouseButton,
- MouseDownEvent, Pixels, Point, Render, View, VisualContext, WeakView,
+ 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<V> {
- Separator(ListSeparator),
- Header(ListSubHeader),
- Entry(
- ListEntry<ContextMenu<V>>,
- Rc<dyn Fn(&mut V, &mut ViewContext<V>)>,
- ),
+pub enum ContextMenuItem {
+ Separator,
+ Header(SharedString),
+ Entry(SharedString, Rc<dyn Fn(&ClickEvent, &mut WindowContext)>),
}
-pub struct ContextMenu<V> {
- items: Vec<ContextMenuItem<V>>,
+pub struct ContextMenu {
+ items: Vec<ContextMenuItem>,
focus_handle: FocusHandle,
- handle: WeakView<V>,
}
-impl<V: Render> FocusableView for ContextMenu<V> {
+impl FocusableView for ContextMenu {
fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
self.focus_handle.clone()
}
}
-impl<V: Render> EventEmitter<Manager> for ContextMenu<V> {}
+impl EventEmitter<DismissEvent> for ContextMenu {}
-impl<V: Render> ContextMenu<V> {
+impl ContextMenu {
pub fn build(
- cx: &mut ViewContext<V>,
- f: impl FnOnce(Self, &mut ViewContext<Self>) -> Self,
+ cx: &mut WindowContext,
+ f: impl FnOnce(Self, &mut WindowContext) -> Self,
) -> View<Self> {
- let handle = cx.view().downgrade();
+ // let handle = cx.view().downgrade();
cx.build_view(|cx| {
f(
Self {
- handle,
items: Default::default(),
focus_handle: cx.focus_handle(),
},
@@ -51,105 +46,103 @@ impl<V: Render> ContextMenu<V> {
}
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: ListEntry<Self>,
- on_click: impl Fn(&mut V, &mut ViewContext<V>) + 'static,
+ 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: ListEntry<Self>, 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);
}
}
-impl<V: Render> Render for ContextMenu<V> {
- type Element = Div<Self>;
+impl Render for ContextMenu {
+ type Element = Div;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
div().elevation_2(cx).flex().flex_row().child(
v_stack()
.min_w(px(200.))
.track_focus(&self.focus_handle)
- .on_mouse_down_out(|this: &mut Self, _, cx| this.cancel(&Default::default(), cx))
+ .on_mouse_down_out(
+ cx.listener(|this: &mut Self, _, cx| this.cancel(&Default::default(), cx)),
+ )
// .on_action(ContextMenu::select_first)
// .on_action(ContextMenu::select_last)
// .on_action(ContextMenu::select_next)
// .on_action(ContextMenu::select_prev)
- .on_action(ContextMenu::confirm)
- .on_action(ContextMenu::cancel)
+ .on_action(cx.listener(ContextMenu::confirm))
+ .on_action(cx.listener(ContextMenu::cancel))
.flex_none()
// .bg(cx.theme().colors().elevated_surface_background)
// .border()
// .border_color(cx.theme().colors().border)
- .child(List::new(
- self.items
- .iter()
- .map(|item| match item {
- ContextMenuItem::Separator(separator) => {
- ListItem::Separator(separator.clone())
- }
- ContextMenuItem::Header(header) => ListItem::Header(header.clone()),
- ContextMenuItem::Entry(entry, callback) => {
- let callback = callback.clone();
- let handle = self.handle.clone();
- ListItem::Entry(entry.clone().on_click(move |this, cx| {
- handle.update(cx, |view, cx| callback(view, cx)).ok();
- cx.emit(Manager::Dismiss);
- }))
- }
- })
- .collect(),
- )),
+ .child(
+ List::new().children(self.items.iter().map(|item| match item {
+ ContextMenuItem::Separator => ListSeparator::new().into_any_element(),
+ ContextMenuItem::Header(header) => {
+ ListSubHeader::new(header.clone()).into_any_element()
+ }
+ ContextMenuItem::Entry(entry, callback) => {
+ let callback = callback.clone();
+ let dismiss = cx.listener(|_, _, cx| cx.emit(DismissEvent::Dismiss));
+
+ ListItem::new(entry.clone())
+ .child(Label::new(entry.clone()))
+ .on_click(move |event, cx| {
+ callback(event, cx);
+ dismiss(event, cx)
+ })
+ .into_any_element()
+ }
+ })),
+ ),
)
}
}
-pub struct MenuHandle<V: 'static, M: ManagedView> {
- id: Option<ElementId>,
- child_builder: Option<Box<dyn FnOnce(bool) -> AnyElement<V> + 'static>>,
- menu_builder: Option<Rc<dyn Fn(&mut V, &mut ViewContext<V>) -> View<M> + 'static>>,
-
+pub struct MenuHandle<M: ManagedView> {
+ id: ElementId,
+ child_builder: Option<Box<dyn FnOnce(bool) -> AnyElement + 'static>>,
+ menu_builder: Option<Rc<dyn Fn(&mut WindowContext) -> View<M> + 'static>>,
anchor: Option<AnchorCorner>,
attach: Option<AnchorCorner>,
}
-impl<V: 'static, M: ManagedView> MenuHandle<V, M> {
- pub fn id(mut self, id: impl Into<ElementId>) -> Self {
- self.id = Some(id.into());
- self
- }
-
- pub fn menu(mut self, f: impl Fn(&mut V, &mut ViewContext<V>) -> View<M> + 'static) -> Self {
+impl<M: ManagedView> MenuHandle<M> {
+ pub fn menu(mut self, f: impl Fn(&mut WindowContext) -> View<M> + 'static) -> Self {
self.menu_builder = Some(Rc::new(f));
self
}
- pub fn child<R: Component<V>>(mut self, f: impl FnOnce(bool) -> R + 'static) -> Self {
- self.child_builder = Some(Box::new(|b| f(b).render()));
+ 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
}
@@ -167,9 +160,9 @@ impl<V: 'static, M: ManagedView> MenuHandle<V, M> {
}
}
-pub fn menu_handle<V: 'static, M: ManagedView>() -> MenuHandle<V, M> {
+pub fn menu_handle<M: ManagedView>(id: impl Into<ElementId>) -> MenuHandle<M> {
MenuHandle {
- id: None,
+ id: id.into(),
child_builder: None,
menu_builder: None,
anchor: None,
@@ -177,26 +170,21 @@ pub fn menu_handle<V: 'static, M: ManagedView>() -> MenuHandle<V, M> {
}
}
-pub struct MenuHandleState<V, M> {
+pub struct MenuHandleState<M> {
menu: Rc<RefCell<Option<View<M>>>>,
position: Rc<RefCell<Point<Pixels>>>,
child_layout_id: Option<LayoutId>,
- child_element: Option<AnyElement<V>>,
- menu_element: Option<AnyElement<V>>,
+ child_element: Option<AnyElement>,
+ menu_element: Option<AnyElement>,
}
-impl<V: 'static, M: ManagedView> Element<V> for MenuHandle<V, M> {
- type ElementState = MenuHandleState<V, M>;
-
- fn element_id(&self) -> Option<gpui::ElementId> {
- Some(self.id.clone().expect("menu_handle must have an id()"))
- }
+impl<M: ManagedView> Element for MenuHandle<M> {
+ type State = MenuHandleState<M>;
fn layout(
&mut self,
- view_state: &mut V,
- element_state: Option<Self::ElementState>,
- cx: &mut crate::ViewContext<V>,
- ) -> (gpui::LayoutId, Self::ElementState) {
+ element_state: Option<Self::State>,
+ cx: &mut WindowContext,
+ ) -> (gpui::LayoutId, Self::State) {
let (menu, position) = if let Some(element_state) = element_state {
(element_state.menu, element_state.position)
} else {
@@ -206,15 +194,15 @@ impl<V: 'static, M: ManagedView> Element<V> for MenuHandle<V, M> {
let mut menu_layout_id = None;
let menu_element = menu.borrow_mut().as_mut().map(|menu| {
- let mut overlay = overlay::<V>().snap_to_window();
+ let mut overlay = overlay().snap_to_window();
if let Some(anchor) = self.anchor {
overlay = overlay.anchor(anchor);
}
overlay = overlay.position(*position.borrow());
- let mut view = overlay.child(menu.clone()).render();
- menu_layout_id = Some(view.layout(view_state, cx));
- view
+ let mut element = overlay.child(menu.clone()).into_any();
+ menu_layout_id = Some(element.layout(cx));
+ element
});
let mut child_element = self
@@ -224,7 +212,7 @@ impl<V: 'static, M: ManagedView> Element<V> for MenuHandle<V, M> {
let child_layout_id = child_element
.as_mut()
- .map(|child_element| child_element.layout(view_state, cx));
+ .map(|child_element| child_element.layout(cx));
let layout_id = cx.request_layout(
&gpui::Style::default(),
@@ -244,22 +232,21 @@ impl<V: 'static, M: ManagedView> Element<V> for MenuHandle<V, M> {
}
fn paint(
- &mut self,
+ self,
bounds: Bounds<gpui::Pixels>,
- view_state: &mut V,
- element_state: &mut Self::ElementState,
- cx: &mut crate::ViewContext<V>,
+ element_state: &mut Self::State,
+ cx: &mut WindowContext,
) {
- if let Some(child) = element_state.child_element.as_mut() {
- child.paint(view_state, cx);
+ if let Some(child) = element_state.child_element.take() {
+ child.paint(cx);
}
- if let Some(menu) = element_state.menu_element.as_mut() {
- menu.paint(view_state, cx);
+ if let Some(menu) = element_state.menu_element.take() {
+ menu.paint(cx);
return;
}
- let Some(builder) = self.menu_builder.clone() else {
+ let Some(builder) = self.menu_builder else {
return;
};
let menu = element_state.menu.clone();
@@ -267,7 +254,7 @@ impl<V: 'static, M: ManagedView> Element<V> for MenuHandle<V, M> {
let attach = self.attach.clone();
let child_layout_id = element_state.child_layout_id.clone();
- cx.on_mouse_event(move |view_state, event: &MouseDownEvent, phase, cx| {
+ cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| {
if phase == DispatchPhase::Bubble
&& event.button == MouseButton::Right
&& bounds.contains_point(&event.position)
@@ -275,10 +262,10 @@ impl<V: 'static, M: ManagedView> Element<V> for MenuHandle<V, M> {
cx.stop_propagation();
cx.prevent_default();
- let new_menu = (builder)(view_state, cx);
+ let new_menu = (builder)(cx);
let menu2 = menu.clone();
- cx.subscribe(&new_menu, move |this, modal, e, cx| match e {
- &Manager::Dismiss => {
+ cx.subscribe(&new_menu, move |modal, e, cx| match e {
+ &DismissEvent::Dismiss => {
*menu2.borrow_mut() = None;
cx.notify();
}
@@ -300,129 +287,14 @@ impl<V: 'static, M: ManagedView> Element<V> for MenuHandle<V, M> {
}
}
-impl<V: 'static, M: ManagedView> Component<V> for MenuHandle<V, M> {
- fn render(self) -> AnyElement<V> {
- AnyElement::new(self)
- }
-}
+impl<M: ManagedView> IntoElement for MenuHandle<M> {
+ type Element = Self;
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use super::*;
- use crate::story::Story;
- use gpui::{actions, Div, Render};
-
- actions!(PrintCurrentDate, PrintBestFood);
-
- fn build_menu<V: Render>(
- cx: &mut ViewContext<V>,
- header: impl Into<SharedString>,
- ) -> View<ContextMenu<V>> {
- let handle = cx.view().clone();
- ContextMenu::build(cx, |menu, _| {
- menu.header(header)
- .separator()
- .entry(ListEntry::new(Label::new("Print current time")), |v, cx| {
- println!("dispatching PrintCurrentTime action");
- cx.dispatch_action(PrintCurrentDate.boxed_clone())
- })
- .entry(ListEntry::new(Label::new("Print best food")), |v, cx| {
- cx.dispatch_action(PrintBestFood.boxed_clone())
- })
- })
+ fn element_id(&self) -> Option<gpui::ElementId> {
+ Some(self.id.clone())
}
- pub struct ContextMenuStory;
-
- impl Render for ContextMenuStory {
- type Element = Div<Self>;
-
- 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()
- .id("test2")
- .child(|is_open| {
- Label::new(if is_open {
- "TOP LEFT"
- } else {
- "RIGHT CLICK ME"
- })
- .render()
- })
- .menu(move |_, cx| build_menu(cx, "top left")),
- )
- .child(
- menu_handle()
- .id("test1")
- .child(|is_open| {
- Label::new(if is_open {
- "BOTTOM LEFT"
- } else {
- "RIGHT CLICK ME"
- })
- .render()
- })
- .anchor(AnchorCorner::BottomLeft)
- .attach(AnchorCorner::TopLeft)
- .menu(move |_, cx| build_menu(cx, "bottom left")),
- ),
- )
- .child(
- div()
- .flex()
- .flex_col()
- .justify_between()
- .child(
- menu_handle()
- .id("test3")
- .child(|is_open| {
- Label::new(if is_open {
- "TOP RIGHT"
- } else {
- "RIGHT CLICK ME"
- })
- .render()
- })
- .anchor(AnchorCorner::TopRight)
- .menu(move |_, cx| build_menu(cx, "top right")),
- )
- .child(
- menu_handle()
- .id("test4")
- .child(|is_open| {
- Label::new(if is_open {
- "BOTTOM RIGHT"
- } else {
- "RIGHT CLICK ME"
- })
- .render()
- })
- .anchor(AnchorCorner::BottomRight)
- .attach(AnchorCorner::TopRight)
- .menu(move |_, cx| build_menu(cx, "bottom right")),
- ),
- )
- }
+ fn into_element(self) -> Self::Element {
+ self
}
}
@@ -1,78 +0,0 @@
-use crate::prelude::*;
-use crate::{v_stack, ButtonGroup};
-
-#[derive(Component)]
-pub struct Details<V: 'static> {
- text: &'static str,
- meta: Option<&'static str>,
- actions: Option<ButtonGroup<V>>,
-}
-
-impl<V: 'static> Details<V> {
- 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<V>) -> Self {
- self.actions = Some(actions);
- self
- }
-
- fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- 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))
- }
-}
-
-#[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<Self>;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- Story::container(cx)
- .child(Story::title_for::<_, Details<Self>>(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,3 +1,5 @@
+use gpui::{Div, IntoElement};
+
use crate::prelude::*;
enum DividerDirection {
@@ -5,12 +7,29 @@ enum DividerDirection {
Vertical,
}
-#[derive(Component)]
+#[derive(IntoElement)]
pub struct Divider {
direction: DividerDirection,
inset: bool,
}
+impl RenderOnce for Divider {
+ type Rendered = Div;
+
+ fn render(self, cx: &mut WindowContext) -> Self::Rendered {
+ div()
+ .map(|this| match self.direction {
+ DividerDirection::Horizontal => {
+ this.h_px().w_full().when(self.inset, |this| this.mx_1p5())
+ }
+ DividerDirection::Vertical => {
+ this.w_px().h_full().when(self.inset, |this| this.my_1p5())
+ }
+ })
+ .bg(cx.theme().colors().border_variant)
+ }
+}
+
impl Divider {
pub fn horizontal() -> Self {
Self {
@@ -31,7 +50,7 @@ impl Divider {
self
}
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+ fn render(self, cx: &mut WindowContext) -> impl Element {
div()
.map(|this| match self.direction {
DividerDirection::Horizontal => {
@@ -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<V: 'static>(level: ElevationIndex, cx: &mut ViewContext<V>) -> Div<V> {
- 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<V: 'static>(cx: &mut ViewContext<V>) -> Div<V> {
- elevated_surface(ElevationIndex::ModalSurface, cx)
-}
@@ -1,59 +0,0 @@
-use crate::prelude::*;
-use crate::{Avatar, Player};
-
-#[derive(Component)]
-pub struct Facepile {
- players: Vec<Player>,
-}
-
-impl Facepile {
- pub fn new<P: Iterator<Item = Player>>(players: P) -> Self {
- Self {
- players: players.collect(),
- }
- }
-
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- 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)
- }
-}
-
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use super::*;
- use crate::{static_players, Story};
- use gpui::{Div, Render};
-
- pub struct FacepileStory;
-
- impl Render for FacepileStory {
- type Element = Div<Self>;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- let players = static_players();
-
- Story::container(cx)
- .child(Story::title_for::<_, Facepile>(cx))
- .child(Story::label(cx, "Default"))
- .child(
- div()
- .flex()
- .gap_3()
- .child(Facepile::new(players.clone().into_iter().take(1)))
- .child(Facepile::new(players.clone().into_iter().take(2)))
- .child(Facepile::new(players.clone().into_iter().take(3))),
- )
- }
- }
-}
@@ -1,4 +1,4 @@
-use gpui::{rems, svg};
+use gpui::{rems, svg, IntoElement, Svg};
use strum::EnumIter;
use crate::prelude::*;
@@ -25,6 +25,7 @@ pub enum Icon {
BellOff,
BellRing,
Bolt,
+ CaseSensitive,
Check,
ChevronDown,
ChevronLeft,
@@ -33,6 +34,9 @@ pub enum Icon {
Close,
Collab,
Copilot,
+ CopilotInit,
+ CopilotError,
+ CopilotDisabled,
Dash,
Envelope,
ExclamationTriangle,
@@ -67,6 +71,7 @@ pub enum Icon {
Split,
SplitMessage,
Terminal,
+ WholeWord,
XCircle,
Command,
Control,
@@ -91,6 +96,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",
@@ -99,6 +105,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",
@@ -133,6 +142,7 @@ impl Icon {
Icon::Split => "icons/split.svg",
Icon::SplitMessage => "icons/split_message.svg",
Icon::Terminal => "icons/terminal.svg",
+ Icon::WholeWord => "icons/word_search.svg",
Icon::XCircle => "icons/error.svg",
Icon::Command => "icons/command.svg",
Icon::Control => "icons/control.svg",
@@ -143,18 +153,35 @@ impl Icon {
}
}
-#[derive(Component)]
+#[derive(IntoElement)]
pub struct IconElement {
path: SharedString,
- color: TextColor,
+ color: Color,
size: IconSize,
}
+impl RenderOnce for IconElement {
+ type Rendered = Svg;
+
+ fn render(self, cx: &mut WindowContext) -> Self::Rendered {
+ let svg_size = match self.size {
+ IconSize::Small => rems(14. / 16.),
+ IconSize::Medium => rems(16. / 16.),
+ };
+
+ svg()
+ .size(svg_size)
+ .flex_none()
+ .path(self.path)
+ .text_color(self.color.color(cx))
+ }
+}
+
impl IconElement {
pub fn new(icon: Icon) -> Self {
Self {
path: icon.path().into(),
- color: TextColor::default(),
+ color: Color::default(),
size: IconSize::default(),
}
}
@@ -162,12 +189,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
}
@@ -177,10 +204,10 @@ impl IconElement {
self
}
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+ fn render(self, cx: &mut WindowContext) -> impl Element {
let svg_size = match self.size {
- IconSize::Small => rems(14. / 16.),
- IconSize::Medium => rems(16. / 16.),
+ IconSize::Small => rems(0.75),
+ IconSize::Medium => rems(0.9375),
};
svg()
@@ -190,31 +217,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<Self>;
-
- 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,89 +1,25 @@
-use crate::{h_stack, prelude::*, ClickHandler, Icon, IconElement};
-use gpui::{prelude::*, Action, AnyView, MouseButton};
-use std::sync::Arc;
+use crate::{h_stack, prelude::*, Icon, IconElement};
+use gpui::{prelude::*, Action, AnyView, Div, MouseButton, MouseDownEvent, Stateful};
-struct IconButtonHandlers<V: 'static> {
- click: Option<ClickHandler<V>>,
-}
-
-impl<V: 'static> Default for IconButtonHandlers<V> {
- fn default() -> Self {
- Self { click: None }
- }
-}
-
-#[derive(Component)]
-pub struct IconButton<V: 'static> {
+#[derive(IntoElement)]
+pub struct IconButton {
id: ElementId,
icon: Icon,
- color: TextColor,
+ color: Color,
variant: ButtonVariant,
state: InteractionState,
selected: bool,
- tooltip: Option<Box<dyn Fn(&mut V, &mut ViewContext<V>) -> AnyView + 'static>>,
- handlers: IconButtonHandlers<V>,
+ tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView + 'static>>,
+ on_mouse_down: Option<Box<dyn Fn(&MouseDownEvent, &mut WindowContext) + 'static>>,
}
-impl<V: 'static> IconButton<V> {
- pub fn new(id: impl Into<ElementId>, icon: Icon) -> Self {
- Self {
- id: id.into(),
- icon,
- color: TextColor::default(),
- variant: ButtonVariant::default(),
- state: InteractionState::default(),
- selected: false,
- tooltip: None,
- handlers: IconButtonHandlers::default(),
- }
- }
-
- pub fn icon(mut self, icon: Icon) -> Self {
- self.icon = icon;
- self
- }
+impl RenderOnce for IconButton {
+ type Rendered = Stateful<Div>;
- pub fn color(mut self, color: TextColor) -> Self {
- self.color = color;
- self
- }
-
- pub fn variant(mut self, variant: ButtonVariant) -> Self {
- self.variant = variant;
- self
- }
-
- pub fn state(mut self, state: InteractionState) -> Self {
- self.state = state;
- self
- }
-
- pub fn selected(mut self, selected: bool) -> Self {
- self.selected = selected;
- self
- }
-
- pub fn tooltip(
- mut self,
- tooltip: impl Fn(&mut V, &mut ViewContext<V>) -> AnyView + 'static,
- ) -> Self {
- self.tooltip = Some(Box::new(tooltip));
- self
- }
-
- pub fn on_click(mut self, handler: impl 'static + Fn(&mut V, &mut ViewContext<V>)) -> Self {
- self.handlers.click = Some(Arc::new(handler));
- self
- }
-
- pub fn action(self, action: Box<dyn Action>) -> Self {
- self.on_click(move |this, cx| cx.dispatch_action(action.boxed_clone()))
- }
-
- fn render(mut self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+ 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,
};
@@ -101,7 +37,7 @@ impl<V: 'static> IconButton<V> {
};
if self.selected {
- bg_color = bg_hover_color;
+ bg_color = cx.theme().colors().element_selected;
}
let mut button = h_stack()
@@ -118,19 +54,76 @@ impl<V: 'static> IconButton<V> {
.active(|style| style.bg(bg_active_color))
.child(IconElement::new(self.icon).color(icon_color));
- if let Some(click_handler) = self.handlers.click.clone() {
- button = button.on_mouse_down(MouseButton::Left, move |state, event, cx| {
+ if let Some(click_handler) = self.on_mouse_down {
+ button = button.on_mouse_down(MouseButton::Left, move |event, cx| {
cx.stop_propagation();
- click_handler(state, cx);
+ click_handler(event, cx);
})
}
- if let Some(tooltip) = self.tooltip.take() {
+ if let Some(tooltip) = self.tooltip {
if !self.selected {
- button = button.tooltip(move |view: &mut V, cx| (tooltip)(view, cx))
+ button = button.tooltip(move |cx| tooltip(cx))
}
}
button
}
}
+
+impl IconButton {
+ pub fn new(id: impl Into<ElementId>, icon: Icon) -> Self {
+ Self {
+ id: id.into(),
+ icon,
+ color: Color::default(),
+ variant: ButtonVariant::default(),
+ state: InteractionState::default(),
+ selected: false,
+ tooltip: None,
+ on_mouse_down: None,
+ }
+ }
+
+ pub fn icon(mut self, icon: Icon) -> Self {
+ self.icon = icon;
+ self
+ }
+
+ pub fn color(mut self, color: Color) -> Self {
+ self.color = color;
+ self
+ }
+
+ pub fn variant(mut self, variant: ButtonVariant) -> Self {
+ self.variant = variant;
+ self
+ }
+
+ pub fn state(mut self, state: InteractionState) -> Self {
+ self.state = state;
+ self
+ }
+
+ pub fn selected(mut self, selected: bool) -> Self {
+ self.selected = selected;
+ self
+ }
+
+ pub fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self {
+ self.tooltip = Some(Box::new(tooltip));
+ self
+ }
+
+ pub fn on_click(
+ mut self,
+ handler: impl 'static + Fn(&MouseDownEvent, &mut WindowContext),
+ ) -> Self {
+ self.on_mouse_down = Some(Box::new(handler));
+ self
+ }
+
+ pub fn action(self, action: Box<dyn Action>) -> Self {
+ self.on_click(move |this, cx| cx.dispatch_action(action.boxed_clone()))
+ }
+}
@@ -1,23 +0,0 @@
-use gpui::px;
-
-use crate::prelude::*;
-
-#[derive(Component)]
-pub struct UnreadIndicator;
-
-impl UnreadIndicator {
- pub fn new() -> Self {
- Self
- }
-
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- 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::*;
+use gpui::{prelude::*, Div, IntoElement, Stateful};
#[derive(Default, PartialEq)]
pub enum InputVariant {
@@ -8,7 +8,7 @@ pub enum InputVariant {
Filled,
}
-#[derive(Component)]
+#[derive(IntoElement)]
pub struct Input {
placeholder: SharedString,
value: String,
@@ -18,44 +18,10 @@ pub struct Input {
is_active: bool,
}
-impl Input {
- pub fn new(placeholder: impl Into<SharedString>) -> Self {
- Self {
- placeholder: placeholder.into(),
- value: "".to_string(),
- state: InteractionState::default(),
- variant: InputVariant::default(),
- disabled: false,
- is_active: false,
- }
- }
-
- pub fn value(mut self, value: String) -> Self {
- self.value = value;
- self
- }
-
- pub fn state(mut self, state: InteractionState) -> Self {
- self.state = state;
- self
- }
-
- pub fn variant(mut self, variant: InputVariant) -> Self {
- self.variant = variant;
- self
- }
-
- pub fn disabled(mut self, disabled: bool) -> Self {
- self.disabled = disabled;
- self
- }
-
- pub fn is_active(mut self, is_active: bool) -> Self {
- self.is_active = is_active;
- self
- }
+impl RenderOnce for Input {
+ type Rendered = Stateful<Div>;
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+ fn render(self, cx: &mut WindowContext) -> Self::Rendered {
let (input_bg, input_hover_bg, input_active_bg) = match self.variant {
InputVariant::Ghost => (
cx.theme().colors().ghost_element_background,
@@ -70,15 +36,15 @@ impl 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()
@@ -93,7 +59,7 @@ impl Input {
.active(|style| style.bg(input_active_bg))
.flex()
.items_center()
- .child(div().flex().items_center().text_ui_sm().map(|this| {
+ .child(div().flex().items_center().text_ui_sm().map(move |this| {
if self.value.is_empty() {
this.child(placeholder_label)
} else {
@@ -103,25 +69,40 @@ impl Input {
}
}
-#[cfg(feature = "stories")]
-pub use stories::*;
+impl Input {
+ pub fn new(placeholder: impl Into<SharedString>) -> Self {
+ Self {
+ placeholder: placeholder.into(),
+ value: "".to_string(),
+ state: InteractionState::default(),
+ variant: InputVariant::default(),
+ disabled: false,
+ is_active: false,
+ }
+ }
+
+ pub fn value(mut self, value: String) -> Self {
+ self.value = value;
+ self
+ }
-#[cfg(feature = "stories")]
-mod stories {
- use super::*;
- use crate::Story;
- use gpui::{Div, Render};
+ pub fn state(mut self, state: InteractionState) -> Self {
+ self.state = state;
+ self
+ }
- pub struct InputStory;
+ pub fn variant(mut self, variant: InputVariant) -> Self {
+ self.variant = variant;
+ self
+ }
- impl Render for InputStory {
- type Element = Div<Self>;
+ pub fn disabled(mut self, disabled: bool) -> Self {
+ self.disabled = disabled;
+ self
+ }
- 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")))
- }
+ pub fn is_active(mut self, is_active: bool) -> Self {
+ self.is_active = is_active;
+ self
}
}
@@ -1,9 +1,7 @@
-use gpui::{actions, relative, rems, Action, Styled};
-use strum::EnumIter;
+use crate::prelude::*;
+use gpui::{Action, Div, IntoElement};
-use crate::{h_stack, prelude::*, Icon, IconElement, IconSize};
-
-#[derive(Component, 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.
@@ -12,6 +10,27 @@ pub struct KeyBinding {
key_binding: gpui::KeyBinding,
}
+impl RenderOnce for KeyBinding {
+ type Rendered = Div;
+
+ fn render(self, cx: &mut WindowContext) -> Self::Rendered {
+ div()
+ .flex()
+ .gap_2()
+ .children(self.key_binding.keystrokes().iter().map(|keystroke| {
+ div()
+ .flex()
+ .gap_1()
+ .when(keystroke.modifiers.function, |el| el.child(Key::new("fn")))
+ .when(keystroke.modifiers.control, |el| el.child(Key::new("^")))
+ .when(keystroke.modifiers.alt, |el| el.child(Key::new("⌥")))
+ .when(keystroke.modifiers.command, |el| el.child(Key::new("⌘")))
+ .when(keystroke.modifiers.shift, |el| el.child(Key::new("⇧")))
+ .child(Key::new(keystroke.key.clone()))
+ }))
+ }
+}
+
impl KeyBinding {
pub fn for_action(action: &dyn Action, cx: &mut WindowContext) -> Option<Self> {
// todo! this last is arbitrary, we want to prefer users key bindings over defaults,
@@ -23,172 +42,30 @@ impl KeyBinding {
pub fn new(key_binding: gpui::KeyBinding) -> Self {
Self { key_binding }
}
-
- fn icon_for_key(key: &str) -> Option<Icon> {
- match key {
- "left" => Some(Icon::ArrowLeft),
- "right" => Some(Icon::ArrowRight),
- "up" => Some(Icon::ArrowUp),
- "down" => Some(Icon::ArrowDown),
- _ => None,
- }
- }
-
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- h_stack()
- .flex_none()
- .gap_1()
- .children(self.key_binding.keystrokes().iter().map(|keystroke| {
- let key_icon = Self::icon_for_key(&keystroke.key);
-
- h_stack()
- .flex_none()
- .gap_0p5()
- .bg(cx.theme().colors().element_background)
- .p_0p5()
- .rounded_sm()
- .when(keystroke.modifiers.function, |el| el.child(Key::new("fn")))
- .when(keystroke.modifiers.control, |el| {
- el.child(KeyIcon::new(Icon::Control))
- })
- .when(keystroke.modifiers.alt, |el| {
- el.child(KeyIcon::new(Icon::Option))
- })
- .when(keystroke.modifiers.command, |el| {
- el.child(KeyIcon::new(Icon::Command))
- })
- .when(keystroke.modifiers.shift, |el| {
- el.child(KeyIcon::new(Icon::Shift))
- })
- .when_some(key_icon, |el, icon| el.child(KeyIcon::new(icon)))
- .when(key_icon.is_none(), |el| {
- el.child(Key::new(keystroke.key.to_uppercase().clone()))
- })
- }))
- }
}
-#[derive(Component)]
+#[derive(IntoElement)]
pub struct Key {
key: SharedString,
}
-impl Key {
- pub fn new(key: impl Into<SharedString>) -> Self {
- Self { key: key.into() }
- }
-
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- let single_char = self.key.len() == 1;
+impl RenderOnce for Key {
+ type Rendered = Div;
+ fn render(self, cx: &mut WindowContext) -> Self::Rendered {
div()
- // .px_0p5()
+ .px_2()
.py_0()
- .when(single_char, |el| {
- el.w(rems(14. / 16.)).flex().flex_none().justify_center()
- })
- .when(!single_char, |el| el.px_0p5())
- .h(rems(14. / 16.))
- // .rounded_md()
- .text_ui()
- .line_height(relative(1.))
+ .rounded_md()
+ .text_ui_sm()
.text_color(cx.theme().colors().text)
- // .bg(cx.theme().colors().element_background)
+ .bg(cx.theme().colors().element_background)
.child(self.key.clone())
}
}
-#[derive(Component)]
-pub struct KeyIcon {
- icon: Icon,
-}
-
-impl KeyIcon {
- pub fn new(icon: Icon) -> Self {
- Self { icon }
- }
-
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- div()
- .w(rems(14. / 16.))
- // .bg(cx.theme().colors().element_background)
- .child(IconElement::new(self.icon).size(IconSize::Small))
- }
-}
-
-// NOTE: The order the modifier keys appear in this enum impacts the order in
-// which they are rendered in the UI.
-#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
-pub enum ModifierKey {
- Control,
- Alt, // Option
- Shift,
- Command,
-}
-
-actions!(NoAction);
-
-pub fn binding(key: &str) -> gpui::KeyBinding {
- gpui::KeyBinding::new(key, NoAction {}, None)
-}
-
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use super::*;
- pub use crate::KeyBinding;
- use crate::{binding, Story};
- use gpui::{Div, Render};
- use itertools::Itertools;
- pub struct KeybindingStory;
-
- impl Render for KeybindingStory {
- type Element = Div<Self>;
-
- 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")))
- }
+impl Key {
+ pub fn new(key: impl Into<SharedString>) -> Self {
+ Self { key: key.into() }
}
}
@@ -1,7 +1,6 @@
-use gpui::{relative, Hsla, Text, TextRun, WindowContext};
-
use crate::prelude::*;
use crate::styled_ext::StyledExt;
+use gpui::{relative, Div, Hsla, IntoElement, StyledText, TextRun, WindowContext};
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
pub enum LabelSize {
@@ -10,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]
@@ -60,47 +17,19 @@ pub enum LineHeightStyle {
UILabel,
}
-#[derive(Clone, Component)]
+#[derive(IntoElement, Clone)]
pub struct Label {
label: SharedString,
size: LabelSize,
line_height_style: LineHeightStyle,
- color: TextColor,
+ color: Color,
strikethrough: bool,
}
-impl Label {
- pub fn new(label: impl Into<SharedString>) -> Self {
- Self {
- label: label.into(),
- size: LabelSize::Default,
- line_height_style: LineHeightStyle::default(),
- color: TextColor::Default,
- strikethrough: false,
- }
- }
-
- pub fn size(mut self, size: LabelSize) -> Self {
- self.size = size;
- self
- }
+impl RenderOnce for Label {
+ type Rendered = Div;
- pub fn color(mut self, color: TextColor) -> Self {
- self.color = color;
- self
- }
-
- pub fn line_height_style(mut self, line_height_style: LineHeightStyle) -> Self {
- self.line_height_style = line_height_style;
- self
- }
-
- pub fn set_strikethrough(mut self, strikethrough: bool) -> Self {
- self.strikethrough = strikethrough;
- self
- }
-
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+ fn render(self, cx: &mut WindowContext) -> Self::Rendered {
div()
.when(self.strikethrough, |this| {
this.relative().child(
@@ -109,7 +38,7 @@ impl Label {
.top_1_2()
.w_full()
.h_px()
- .bg(TextColor::Hidden.color(cx)),
+ .bg(Color::Hidden.color(cx)),
)
})
.map(|this| match self.size {
@@ -124,24 +53,13 @@ impl Label {
}
}
-#[derive(Component)]
-pub struct HighlightedLabel {
- label: SharedString,
- size: LabelSize,
- color: TextColor,
- highlight_indices: Vec<usize>,
- strikethrough: bool,
-}
-
-impl HighlightedLabel {
- /// shows a label with the given characters highlighted.
- /// characters are identified by utf8 byte position.
- pub fn new(label: impl Into<SharedString>, highlight_indices: Vec<usize>) -> Self {
+impl Label {
+ pub fn new(label: impl Into<SharedString>) -> Self {
Self {
label: label.into(),
size: LabelSize::Default,
- color: TextColor::Default,
- highlight_indices,
+ line_height_style: LineHeightStyle::default(),
+ color: Color::Default,
strikethrough: false,
}
}
@@ -151,17 +69,35 @@ impl HighlightedLabel {
self
}
- pub fn color(mut self, color: TextColor) -> Self {
+ pub fn color(mut self, color: Color) -> Self {
self.color = color;
self
}
+ pub fn line_height_style(mut self, line_height_style: LineHeightStyle) -> Self {
+ self.line_height_style = line_height_style;
+ self
+ }
+
pub fn set_strikethrough(mut self, strikethrough: bool) -> Self {
self.strikethrough = strikethrough;
self
}
+}
+
+#[derive(IntoElement)]
+pub struct HighlightedLabel {
+ label: SharedString,
+ size: LabelSize,
+ color: Color,
+ highlight_indices: Vec<usize>,
+ strikethrough: bool,
+}
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+impl RenderOnce for HighlightedLabel {
+ type Rendered = Div;
+
+ fn render(self, cx: &mut WindowContext) -> Self::Rendered {
let highlight_color = cx.theme().colors().text_accent;
let mut text_style = cx.text_style().clone();
@@ -207,51 +143,48 @@ impl 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(Text::styled(self.label, runs))
+ .child(StyledText::new(self.label).with_runs(runs))
}
}
-/// A run of text that receives the same style.
-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};
+impl HighlightedLabel {
+ /// shows a label with the given characters highlighted.
+ /// characters are identified by utf8 byte position.
+ pub fn new(label: impl Into<SharedString>, highlight_indices: Vec<usize>) -> Self {
+ Self {
+ label: label.into(),
+ size: LabelSize::Default,
+ color: Color::Default,
+ highlight_indices,
+ strikethrough: false,
+ }
+ }
- pub struct LabelStory;
+ pub fn size(mut self, size: LabelSize) -> Self {
+ self.size = size;
+ self
+ }
- impl Render for LabelStory {
- type Element = Div<Self>;
+ pub fn color(mut self, color: Color) -> Self {
+ self.color = color;
+ self
+ }
- 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],
- ))
- }
+ pub fn set_strikethrough(mut self, strikethrough: bool) -> Self {
+ self.strikethrough = strikethrough;
+ self
}
}
+
+/// A run of text that receives the same style.
+struct Run {
+ pub text: String,
+ pub color: Hsla,
+}
@@ -1,10 +1,14 @@
use std::rc::Rc;
-use gpui::{div, Div, Stateful, StatefulInteractiveComponent};
+use gpui::{
+ div, px, AnyElement, ClickEvent, Div, ImageSource, IntoElement, MouseButton, MouseDownEvent,
+ Pixels, Stateful, StatefulInteractiveElement,
+};
+use smallvec::SmallVec;
-use crate::settings::user_settings;
use crate::{
- disclosure_control, h_stack, v_stack, Avatar, Icon, IconElement, IconSize, Label, Toggle,
+ disclosure_control, h_stack, v_stack, Avatar, Icon, IconButton, IconElement, IconSize, Label,
+ Toggle,
};
use crate::{prelude::*, GraphicSlot};
@@ -17,14 +21,13 @@ pub enum ListItemVariant {
}
pub enum ListHeaderMeta {
- // TODO: These should be IconButtons
- Tools(Vec<Icon>),
+ Tools(Vec<IconButton>),
// TODO: This should be a button
Button(Label),
Text(Label),
}
-#[derive(Component)]
+#[derive(IntoElement)]
pub struct ListHeader {
label: SharedString,
left_icon: Option<Icon>,
@@ -33,33 +36,10 @@ pub struct ListHeader {
toggle: Toggle,
}
-impl ListHeader {
- pub fn new(label: impl Into<SharedString>) -> Self {
- Self {
- label: label.into(),
- left_icon: None,
- meta: None,
- variant: ListItemVariant::default(),
- toggle: Toggle::NotToggleable,
- }
- }
-
- pub fn toggle(mut self, toggle: Toggle) -> Self {
- self.toggle = toggle;
- self
- }
+impl RenderOnce for ListHeader {
+ type Rendered = Div;
- pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
- self.left_icon = left_icon;
- self
- }
-
- pub fn meta(mut self, meta: Option<ListHeaderMeta>) -> Self {
- self.meta = meta;
- self
- }
-
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+ fn render(self, cx: &mut WindowContext) -> Self::Rendered {
let disclosure_control = disclosure_control(self.toggle);
let meta = match self.meta {
@@ -67,11 +47,7 @@ impl ListHeader {
h_stack()
.gap_2()
.items_center()
- .children(icons.into_iter().map(|i| {
- IconElement::new(i)
- .color(TextColor::Muted)
- .size(IconSize::Small)
- })),
+ .children(icons.into_iter().map(|i| i.color(Color::Muted))),
),
Some(ListHeaderMeta::Button(label)) => div().child(label),
Some(ListHeaderMeta::Text(label)) => div().child(label),
@@ -81,11 +57,6 @@ impl ListHeader {
h_stack()
.w_full()
.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)
- // })
.relative()
.child(
div()
@@ -107,10 +78,10 @@ impl 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),
)
@@ -119,7 +90,38 @@ impl ListHeader {
}
}
-#[derive(Component, Clone)]
+impl ListHeader {
+ pub fn new(label: impl Into<SharedString>) -> Self {
+ Self {
+ label: label.into(),
+ left_icon: None,
+ meta: None,
+ variant: ListItemVariant::default(),
+ toggle: Toggle::NotToggleable,
+ }
+ }
+
+ pub fn toggle(mut self, toggle: Toggle) -> Self {
+ self.toggle = toggle;
+ self
+ }
+
+ pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
+ self.left_icon = left_icon;
+ self
+ }
+
+ pub fn right_button(self, button: IconButton) -> Self {
+ self.meta(Some(ListHeaderMeta::Tools(vec![button])))
+ }
+
+ pub fn meta(mut self, meta: Option<ListHeaderMeta>) -> Self {
+ self.meta = meta;
+ self
+ }
+}
+
+#[derive(IntoElement, Clone)]
pub struct ListSubHeader {
label: SharedString,
left_icon: Option<Icon>,
@@ -139,8 +141,12 @@ impl ListSubHeader {
self.left_icon = left_icon;
self
}
+}
+
+impl RenderOnce for ListSubHeader {
+ type Rendered = Div;
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+ fn render(self, cx: &mut WindowContext) -> Self::Rendered {
h_stack().flex_1().w_full().relative().py_1().child(
div()
.h_6()
@@ -158,134 +164,89 @@ impl 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)),
),
)
}
}
-#[derive(Default, PartialEq, Copy, Clone)]
-pub enum ListEntrySize {
- #[default]
- Small,
- Medium,
-}
-
-#[derive(Clone)]
-pub enum ListItem<V: 'static> {
- Entry(ListEntry<V>),
- Separator(ListSeparator),
- Header(ListSubHeader),
-}
-
-impl<V: 'static> From<ListEntry<V>> for ListItem<V> {
- fn from(entry: ListEntry<V>) -> Self {
- Self::Entry(entry)
- }
-}
-
-impl<V: 'static> From<ListSeparator> for ListItem<V> {
- fn from(entry: ListSeparator) -> Self {
- Self::Separator(entry)
- }
-}
-
-impl<V: 'static> From<ListSubHeader> for ListItem<V> {
- fn from(entry: ListSubHeader) -> Self {
- Self::Header(entry)
- }
-}
-
-impl<V: 'static> ListItem<V> {
- fn render(self, view: &mut V, ix: usize, cx: &mut ViewContext<V>) -> impl Component<V> {
- match self {
- ListItem::Entry(entry) => div().child(entry.render(ix, cx)),
- ListItem::Separator(separator) => div().child(separator.render(view, cx)),
- ListItem::Header(header) => div().child(header.render(view, cx)),
- }
- }
-
- pub fn new(label: Label) -> Self {
- Self::Entry(ListEntry::new(label))
- }
-
- pub fn as_entry(&mut self) -> Option<&mut ListEntry<V>> {
- if let Self::Entry(entry) = self {
- Some(entry)
- } else {
- None
- }
- }
-}
-
-pub struct ListEntry<V> {
+#[derive(IntoElement)]
+pub struct ListItem {
+ id: ElementId,
disabled: bool,
+ selected: bool,
// TODO: Reintroduce this
// disclosure_control_style: DisclosureControlVisibility,
- indent_level: u32,
- label: Label,
+ indent_level: usize,
+ indent_step_size: Pixels,
left_slot: Option<GraphicSlot>,
overflow: OverflowStyle,
- size: ListEntrySize,
toggle: Toggle,
variant: ListItemVariant,
- on_click: Option<Rc<dyn Fn(&mut V, &mut ViewContext<V>) + 'static>>,
+ on_click: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
+ on_secondary_mouse_down: Option<Rc<dyn Fn(&MouseDownEvent, &mut WindowContext) + 'static>>,
+ children: SmallVec<[AnyElement; 2]>,
}
-impl<V> Clone for ListEntry<V> {
- fn clone(&self) -> Self {
- Self {
- 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(),
- }
- }
-}
-
-impl<V: 'static> ListEntry<V> {
- pub fn new(label: Label) -> Self {
+impl ListItem {
+ pub fn new(id: impl Into<ElementId>) -> Self {
Self {
+ id: id.into(),
disabled: false,
+ selected: false,
indent_level: 0,
- label,
+ indent_step_size: px(12.),
left_slot: None,
overflow: OverflowStyle::Hidden,
- size: ListEntrySize::default(),
toggle: Toggle::NotToggleable,
variant: ListItemVariant::default(),
- on_click: Default::default(),
+ on_click: None,
+ on_secondary_mouse_down: None,
+ children: SmallVec::new(),
}
}
- pub fn on_click(mut self, handler: impl Fn(&mut V, &mut ViewContext<V>) + 'static) -> Self {
+ pub fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self {
self.on_click = Some(Rc::new(handler));
self
}
+ pub fn on_secondary_mouse_down(
+ mut self,
+ handler: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
+ ) -> Self {
+ self.on_secondary_mouse_down = Some(Rc::new(handler));
+ self
+ }
+
pub fn variant(mut self, variant: ListItemVariant) -> Self {
self.variant = variant;
self
}
- pub fn indent_level(mut self, indent_level: u32) -> Self {
+ pub fn indent_level(mut self, indent_level: usize) -> Self {
self.indent_level = indent_level;
self
}
+ pub fn indent_step_size(mut self, indent_step_size: Pixels) -> Self {
+ self.indent_step_size = indent_step_size;
+ self
+ }
+
pub fn toggle(mut self, toggle: Toggle) -> Self {
self.toggle = toggle;
self
}
+ pub fn selected(mut self, selected: bool) -> Self {
+ self.selected = selected;
+ self
+ }
+
pub fn left_content(mut self, left_content: GraphicSlot) -> Self {
self.left_slot = Some(left_content);
self
@@ -296,116 +257,138 @@ impl<V: 'static> ListEntry<V> {
self
}
- pub fn left_avatar(mut self, left_avatar: impl Into<SharedString>) -> Self {
+ pub fn left_avatar(mut self, left_avatar: impl Into<ImageSource>) -> Self {
self.left_slot = Some(GraphicSlot::Avatar(left_avatar.into()));
self
}
+}
- pub fn size(mut self, size: ListEntrySize) -> Self {
- self.size = size;
- self
- }
-
- fn render(self, ix: usize, cx: &mut ViewContext<V>) -> Stateful<V, Div<V>> {
- let settings = user_settings(cx);
+impl RenderOnce for ListItem {
+ type Rendered = Stateful<Div>;
+ fn render(self, cx: &mut WindowContext) -> Self::Rendered {
let left_content = match self.left_slot.clone() {
Some(GraphicSlot::Icon(i)) => Some(
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::source(src))),
+ Some(GraphicSlot::PublicActor(src)) => Some(h_stack().child(Avatar::uri(src))),
None => None,
};
- let sized_item = match self.size {
- ListEntrySize::Small => div().h_6(),
- ListEntrySize::Medium => div().h_7(),
- };
div()
- .id(ix)
+ .id(self.id)
.relative()
.hover(|mut style| {
style.background = Some(cx.theme().colors().editor_background.into());
style
})
- .on_click({
- let on_click = self.on_click.clone();
-
- move |view: &mut V, event, cx| {
- if let Some(on_click) = &on_click {
- (on_click)(view, cx)
- }
- }
- })
- .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))
+ .when(self.selected, |this| {
+ this.bg(cx.theme().colors().ghost_element_selected)
+ })
+ .when_some(self.on_secondary_mouse_down, |this, on_mouse_down| {
+ this.on_mouse_down(MouseButton::Right, move |event, cx| {
+ (on_mouse_down)(event, cx)
+ })
+ })
.child(
- sized_item
+ div()
.when(self.variant == ListItemVariant::Inset, |this| this.px_2())
- // .ml(rems(0.75 * self.indent_level as f32))
- .children((0..self.indent_level).map(|_| {
- div()
- .w(*settings.list_indent_depth)
- .h_full()
- .flex()
- .justify_center()
- .group_hover("", |style| style.bg(cx.theme().colors().border_focused))
- .child(
- h_stack()
- .child(div().w_px().h_full())
- .child(div().w_px().h_full().bg(cx.theme().colors().border)),
- )
- }))
+ .ml(self.indent_level as f32 * self.indent_step_size)
.flex()
.gap_1()
.items_center()
.relative()
.child(disclosure_control(self.toggle))
.children(left_content)
- .child(self.label),
+ .children(self.children)
+ // HACK: We need to attach the `on_click` handler to the child element in order to have the click
+ // event actually fire.
+ // Once this is fixed in GPUI we can remove this and rely on the `on_click` handler set above on the
+ // outer `div`.
+ .id("on_click_hack")
+ .when_some(self.on_click, |this, on_click| {
+ this.on_click(move |event, cx| {
+ // HACK: GPUI currently fires `on_click` with any mouse button,
+ // but we only care about the left button.
+ if event.down.button == MouseButton::Left {
+ (on_click)(event, cx)
+ }
+ })
+ }),
)
}
}
-#[derive(Clone, Component)]
+impl ParentElement for ListItem {
+ fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
+ &mut self.children
+ }
+}
+
+#[derive(IntoElement, Clone)]
pub struct ListSeparator;
impl ListSeparator {
pub fn new() -> Self {
Self
}
+}
+
+impl RenderOnce for ListSeparator {
+ type Rendered = Div;
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
+ fn render(self, cx: &mut WindowContext) -> Self::Rendered {
div().h_px().w_full().bg(cx.theme().colors().border_variant)
}
}
-#[derive(Component)]
-pub struct List<V: 'static> {
- items: Vec<ListItem<V>>,
+#[derive(IntoElement)]
+pub struct List {
/// Message to display when the list is empty
/// Defaults to "No items"
empty_message: SharedString,
header: Option<ListHeader>,
toggle: Toggle,
+ children: SmallVec<[AnyElement; 2]>,
+}
+
+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(Color::Muted)),
+ };
+
+ v_stack()
+ .w_full()
+ .py_1()
+ .children(self.header.map(|header| header))
+ .child(list_content)
+ }
}
-impl<V: 'static> List<V> {
- pub fn new(items: Vec<ListItem<V>>) -> Self {
+impl List {
+ pub fn new() -> Self {
Self {
- items,
empty_message: "No items".into(),
header: None,
toggle: Toggle::NotToggleable,
+ children: SmallVec::new(),
}
}
@@ -423,25 +406,10 @@ impl<V: 'static> List<V> {
self.toggle = toggle;
self
}
+}
- fn render(self, view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- let list_content = match (self.items.is_empty(), self.toggle) {
- (false, _) => div().children(
- self.items
- .into_iter()
- .enumerate()
- .map(|(ix, item)| item.render(view, ix, cx)),
- ),
- (true, Toggle::Toggled(false)) => div(),
- (true, _) => {
- div().child(Label::new(self.empty_message.clone()).color(TextColor::Muted))
- }
- };
-
- v_stack()
- .w_full()
- .py_1()
- .children(self.header.map(|header| header))
- .child(list_content)
+impl ParentElement for List {
+ fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
+ &mut self.children
}
}
@@ -1,81 +0,0 @@
-use gpui::AnyElement;
-use smallvec::SmallVec;
-
-use crate::{h_stack, prelude::*, v_stack, Button, Icon, IconButton, Label};
-
-#[derive(Component)]
-pub struct Modal<V: 'static> {
- id: ElementId,
- title: Option<SharedString>,
- primary_action: Option<Button<V>>,
- secondary_action: Option<Button<V>>,
- children: SmallVec<[AnyElement<V>; 2]>,
-}
-
-impl<V: 'static> Modal<V> {
- 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<V>) -> Self {
- self.primary_action = Some(action);
- self
- }
-
- pub fn secondary_action(mut self, action: Button<V>) -> Self {
- self.secondary_action = Some(action);
- self
- }
-
- fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- 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<V: 'static> ParentComponent<V> for Modal<V> {
- fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
- &mut self.children
- }
-}
@@ -1,40 +0,0 @@
-use gpui::rems;
-
-use crate::prelude::*;
-use crate::{h_stack, Icon};
-
-#[derive(Component)]
-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<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- 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,204 +0,0 @@
-use crate::{h_stack, prelude::*, v_stack, KeyBinding, Label};
-use gpui::prelude::*;
-
-#[derive(Component)]
-pub struct Palette {
- id: ElementId,
- input_placeholder: SharedString,
- empty_string: SharedString,
- items: Vec<PaletteItem>,
- default_order: OrderMethod,
-}
-
-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
- }
-
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- v_stack()
- .id(self.id.clone())
- .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.clone()).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.clone())
- .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)
- })),
- ),
- )
- }
-}
-
-#[derive(Component)]
-pub struct PaletteItem {
- pub label: SharedString,
- pub sublabel: Option<SharedString>,
- pub key_binding: Option<KeyBinding>,
-}
-
-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
- }
-
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- div()
- .flex()
- .flex_row()
- .grow()
- .justify_between()
- .child(
- v_stack()
- .child(Label::new(self.label.clone()))
- .children(self.sublabel.clone().map(|sublabel| Label::new(sublabel))),
- )
- .children(self.key_binding)
- }
-}
-
-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<Self>;
-
- 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,150 +0,0 @@
-use gpui::{prelude::*, AbsoluteLength, AnyElement};
-use smallvec::SmallVec;
-
-use crate::prelude::*;
-use crate::settings::user_settings;
-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(Component)]
-pub struct Panel<V: 'static> {
- id: ElementId,
- current_side: PanelSide,
- /// Defaults to PanelAllowedSides::LeftAndRight
- allowed_sides: PanelAllowedSides,
- initial_width: AbsoluteLength,
- width: Option<AbsoluteLength>,
- children: SmallVec<[AnyElement<V>; 2]>,
-}
-
-impl<V: 'static> Panel<V> {
- pub fn new(id: impl Into<ElementId>, cx: &mut WindowContext) -> Self {
- let settings = user_settings(cx);
-
- Self {
- id: id.into(),
- current_side: PanelSide::default(),
- allowed_sides: PanelAllowedSides::default(),
- initial_width: *settings.default_panel_size,
- 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
- }
-
- fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- 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<V: 'static> ParentComponent<V> for Panel<V> {
- fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
- &mut self.children
- }
-}
-
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use super::*;
- use crate::{Label, Story};
- use gpui::{Div, InteractiveComponent, Render};
-
- pub struct PanelStory;
-
- impl Render for PanelStory {
- type Element = Div<Self>;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- Story::container(cx)
- .child(Story::title_for::<_, Panel<Self>>(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, ViewContext};
-
-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<V: 'static>(&self, cx: &mut ViewContext<V>) -> Hsla {
- cx.theme().styles.player.0[self.index % cx.theme().styles.player.0.len()].cursor
- }
-
- pub fn selection_color<V: 'static>(&self, cx: &mut ViewContext<V>) -> 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,61 +0,0 @@
-use crate::prelude::*;
-use crate::{Avatar, Facepile, PlayerWithCallStatus};
-
-#[derive(Component)]
-pub struct PlayerStack {
- player_with_call_status: PlayerWithCallStatus,
-}
-
-impl PlayerStack {
- pub fn new(player_with_call_status: PlayerWithCallStatus) -> Self {
- Self {
- player_with_call_status,
- }
- }
-
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- 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()))
- })),
- )
- }
-}
@@ -0,0 +1,84 @@
+use gpui::{
+ div, AnyElement, Div, Element, ElementId, IntoElement, ParentElement, RenderOnce, Styled,
+ WindowContext,
+};
+use smallvec::SmallVec;
+use theme2::ActiveTheme;
+
+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 {
+ div()
+ .flex()
+ .gap_1()
+ .child(v_stack().elevation_2(cx).px_1().children(self.children))
+ .when_some(self.aside, |this, aside| {
+ this.child(
+ v_stack()
+ .elevation_2(cx)
+ .bg(cx.theme().colors().surface_background)
+ .px_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
+ }
+}
@@ -1,4 +1,4 @@
-use gpui::SharedString;
+use gpui::{ImageSource, SharedString};
use crate::Icon;
@@ -9,6 +9,6 @@ use crate::Icon;
/// Can be filled with a []
pub enum GraphicSlot {
Icon(Icon),
- Avatar(SharedString),
+ Avatar(ImageSource),
PublicActor(SharedString),
}
@@ -5,13 +5,13 @@ use crate::StyledExt;
/// Horizontally stacks elements.
///
/// Sets `flex()`, `flex_row()`, `items_center()`
-pub fn h_stack<V: 'static>() -> Div<V> {
+pub fn h_stack() -> Div {
div().h_flex()
}
/// Vertically stacks elements.
///
/// Sets `flex()`, `flex_col()`
-pub fn v_stack<V: 'static>() -> Div<V> {
+pub fn v_stack() -> Div {
div().v_flex()
}
@@ -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,34 @@
+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!");
+ }),
+ )
+ .child(Story::label("With `on_secondary_mouse_down`"))
+ .child(
+ ListItem::new("with_on_secondary_mouse_down").on_secondary_mouse_down(
+ |_event, _cx| {
+ println!("Right mouse down!");
+ },
+ ),
+ )
+ }
+}
@@ -1,272 +0,0 @@
-use crate::prelude::*;
-use crate::{Icon, IconElement, Label, TextColor};
-use gpui::{prelude::*, red, Div, ElementId, Render, View};
-
-#[derive(Component, 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<Self>;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- div().w_8().h_4().bg(red())
- }
-}
-
-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
- }
-
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- 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
- }),
- )
- }
-}
-
-#[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<Self>;
-
- 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,90 +0,0 @@
-use crate::prelude::*;
-use gpui::{prelude::*, AnyElement};
-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(Component)]
-pub struct Toast<V: 'static> {
- origin: ToastOrigin,
- children: SmallVec<[AnyElement<V>; 2]>,
-}
-
-impl<V: 'static> Toast<V> {
- pub fn new(origin: ToastOrigin) -> Self {
- Self {
- origin,
- children: SmallVec::new(),
- }
- }
-
- fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- 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<V: 'static> ParentComponent<V> for Toast<V> {
- fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 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<Self>;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- Story::container(cx)
- .child(Story::title_for::<_, Toast<Self>>(cx))
- .child(Story::label(cx, "Default"))
- .child(Toast::new(ToastOrigin::Bottom).child(Label::new("label")))
- }
- }
-}
@@ -1,7 +1,3 @@
-use gpui::{div, Component, ParentComponent};
-
-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<V: 'static>(toggle: Toggle) -> impl Component<V> {
- 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,14 +0,0 @@
-use crate::prelude::*;
-
-#[derive(Component)]
-pub struct ToolDivider;
-
-impl ToolDivider {
- pub fn new() -> Self {
- Self
- }
-
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- div().w_px().h_3().bg(cx.theme().colors().border)
- }
-}
@@ -1,9 +1,9 @@
-use gpui::{overlay, Action, AnyView, Overlay, Render, 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,
@@ -68,7 +68,7 @@ impl Tooltip {
}
impl Render for Tooltip {
- type Element = Overlay<Self>;
+ type Element = Overlay;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone();
@@ -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, InteractiveComponent, ParentComponent, SharedString,
- Styled, ViewContext, WindowContext,
+ 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,74 +0,0 @@
-use std::ops::Deref;
-
-use gpui::{rems, AbsoluteLength, AppContext, WindowContext};
-
-use crate::prelude::*;
-
-pub fn init(cx: &mut AppContext) {
- cx.set_global(FakeSettings::default());
-}
-
-/// Returns the user settings.
-pub fn user_settings(cx: &WindowContext) -> FakeSettings {
- cx.global::<FakeSettings>().clone()
-}
-
-pub fn user_settings_mut<'cx>(cx: &'cx mut WindowContext) -> &'cx mut FakeSettings {
- cx.global_mut::<FakeSettings>()
-}
-
-#[derive(Clone)]
-pub enum SettingValue<T> {
- UserDefined(T),
- Default(T),
-}
-
-impl<T> Deref for SettingValue<T> {
- type Target = T;
-
- fn deref(&self) -> &Self::Target {
- match self {
- Self::UserDefined(value) => value,
- Self::Default(value) => value,
- }
- }
-}
-
-#[derive(Clone)]
-pub struct TitlebarSettings {
- pub show_project_owner: SettingValue<bool>,
- pub show_git_status: SettingValue<bool>,
- pub show_git_controls: SettingValue<bool>,
-}
-
-impl Default for TitlebarSettings {
- fn default() -> Self {
- Self {
- show_project_owner: SettingValue::Default(true),
- show_git_status: SettingValue::Default(true),
- show_git_controls: SettingValue::Default(true),
- }
- }
-}
-
-// These should be merged into settings
-#[derive(Clone)]
-pub struct FakeSettings {
- pub default_panel_size: SettingValue<AbsoluteLength>,
- pub list_disclosure_style: SettingValue<DisclosureControlStyle>,
- pub list_indent_depth: SettingValue<AbsoluteLength>,
- pub titlebar: TitlebarSettings,
-}
-
-impl Default for FakeSettings {
- fn default() -> Self {
- Self {
- titlebar: TitlebarSettings::default(),
- list_disclosure_style: SettingValue::Default(DisclosureControlStyle::ChevronOnHover),
- list_indent_depth: SettingValue::Default(rems(0.3).into()),
- default_panel_size: SettingValue::Default(rems(16.).into()),
- }
- }
-}
-
-impl FakeSettings {}
@@ -1,1111 +0,0 @@
-use std::path::PathBuf;
-use std::str::FromStr;
-use std::sync::Arc;
-
-use chrono::DateTime;
-use gpui::{AppContext, ViewContext};
-use rand::Rng;
-use theme2::ActiveTheme;
-
-use crate::{binding, HighlightedText};
-use crate::{
- Buffer, BufferRow, BufferRows, Button, EditorPane, FileSystemStatus, GitStatus,
- HighlightedLine, Icon, KeyBinding, Label, ListEntry, ListEntrySize, Livestream, MicStatus,
- Notification, PaletteItem, Player, PlayerCallStatus, PlayerWithCallStatus, PublicPlayer,
- ScreenShareStatus, Symbol, Tab, TextColor, Toggle, VideoStatus,
-};
-use crate::{ListItem, NotificationAction};
-
-pub fn static_tabs_example() -> Vec<Tab> {
- vec![
- Tab::new("wip.rs")
- .title("wip.rs".to_string())
- .icon(Icon::FileRust)
- .current(false)
- .fs_status(FileSystemStatus::Deleted),
- Tab::new("Cargo.toml")
- .title("Cargo.toml".to_string())
- .icon(Icon::FileToml)
- .current(false)
- .git_status(GitStatus::Modified),
- Tab::new("Channels Panel")
- .title("Channels Panel".to_string())
- .icon(Icon::Hash)
- .current(false),
- Tab::new("channels_panel.rs")
- .title("channels_panel.rs".to_string())
- .icon(Icon::FileRust)
- .current(true)
- .git_status(GitStatus::Modified),
- Tab::new("workspace.rs")
- .title("workspace.rs".to_string())
- .current(false)
- .icon(Icon::FileRust)
- .git_status(GitStatus::Modified),
- Tab::new("icon_button.rs")
- .title("icon_button.rs".to_string())
- .icon(Icon::FileRust)
- .current(false),
- Tab::new("storybook.rs")
- .title("storybook.rs".to_string())
- .icon(Icon::FileRust)
- .current(false)
- .git_status(GitStatus::Created),
- Tab::new("theme.rs")
- .title("theme.rs".to_string())
- .icon(Icon::FileRust)
- .current(false),
- Tab::new("theme_registry.rs")
- .title("theme_registry.rs".to_string())
- .icon(Icon::FileRust)
- .current(false),
- Tab::new("styleable_helpers.rs")
- .title("styleable_helpers.rs".to_string())
- .icon(Icon::FileRust)
- .current(false),
- ]
-}
-
-pub fn static_tabs_1() -> Vec<Tab> {
- vec![
- Tab::new("project_panel.rs")
- .title("project_panel.rs".to_string())
- .icon(Icon::FileRust)
- .current(false)
- .fs_status(FileSystemStatus::Deleted),
- Tab::new("tab_bar.rs")
- .title("tab_bar.rs".to_string())
- .icon(Icon::FileRust)
- .current(false)
- .git_status(GitStatus::Modified),
- Tab::new("workspace.rs")
- .title("workspace.rs".to_string())
- .icon(Icon::FileRust)
- .current(false),
- Tab::new("tab.rs")
- .title("tab.rs".to_string())
- .icon(Icon::FileRust)
- .current(true)
- .git_status(GitStatus::Modified),
- ]
-}
-
-pub fn static_tabs_2() -> Vec<Tab> {
- vec![
- Tab::new("tab_bar.rs")
- .title("tab_bar.rs".to_string())
- .icon(Icon::FileRust)
- .current(false)
- .fs_status(FileSystemStatus::Deleted),
- Tab::new("static_data.rs")
- .title("static_data.rs".to_string())
- .icon(Icon::FileRust)
- .current(true)
- .git_status(GitStatus::Modified),
- ]
-}
-
-pub fn static_tabs_3() -> Vec<Tab> {
- vec![Tab::new("static_tabs_3")
- .git_status(GitStatus::Created)
- .current(true)]
-}
-
-pub fn static_players() -> Vec<Player> {
- vec![
- Player::new(
- 0,
- "https://avatars.githubusercontent.com/u/1714999?v=4".into(),
- "nathansobo".into(),
- ),
- Player::new(
- 1,
- "https://avatars.githubusercontent.com/u/326587?v=4".into(),
- "maxbrunsfeld".into(),
- ),
- Player::new(
- 2,
- "https://avatars.githubusercontent.com/u/482957?v=4".into(),
- "as-cii".into(),
- ),
- Player::new(
- 3,
- "https://avatars.githubusercontent.com/u/1714999?v=4".into(),
- "iamnbutler".into(),
- ),
- Player::new(
- 4,
- "https://avatars.githubusercontent.com/u/1486634?v=4".into(),
- "maxdeviant".into(),
- ),
- ]
-}
-
-#[derive(Debug)]
-pub struct PlayerData {
- pub url: String,
- pub name: String,
-}
-
-pub fn static_player_data() -> Vec<PlayerData> {
- vec![
- PlayerData {
- url: "https://avatars.githubusercontent.com/u/1714999?v=4".into(),
- name: "iamnbutler".into(),
- },
- PlayerData {
- url: "https://avatars.githubusercontent.com/u/326587?v=4".into(),
- name: "maxbrunsfeld".into(),
- },
- PlayerData {
- url: "https://avatars.githubusercontent.com/u/482957?v=4".into(),
- name: "as-cii".into(),
- },
- PlayerData {
- url: "https://avatars.githubusercontent.com/u/1789?v=4".into(),
- name: "nathansobo".into(),
- },
- PlayerData {
- url: "https://avatars.githubusercontent.com/u/1486634?v=4".into(),
- name: "ForLoveOfCats".into(),
- },
- PlayerData {
- url: "https://avatars.githubusercontent.com/u/2690773?v=4".into(),
- name: "SomeoneToIgnore".into(),
- },
- PlayerData {
- url: "https://avatars.githubusercontent.com/u/19867440?v=4".into(),
- name: "JosephTLyons".into(),
- },
- PlayerData {
- url: "https://avatars.githubusercontent.com/u/24362066?v=4".into(),
- name: "osiewicz".into(),
- },
- PlayerData {
- url: "https://avatars.githubusercontent.com/u/22121886?v=4".into(),
- name: "KCaverly".into(),
- },
- PlayerData {
- url: "https://avatars.githubusercontent.com/u/1486634?v=4".into(),
- name: "maxdeviant".into(),
- },
- ]
-}
-
-pub fn create_static_players(player_data: Vec<PlayerData>) -> Vec<Player> {
- let mut players = Vec::new();
- for data in player_data {
- players.push(Player::new(players.len(), data.url, data.name));
- }
- players
-}
-
-pub fn static_player_1(data: &Vec<PlayerData>) -> Player {
- Player::new(1, data[0].url.clone(), data[0].name.clone())
-}
-
-pub fn static_player_2(data: &Vec<PlayerData>) -> Player {
- Player::new(2, data[1].url.clone(), data[1].name.clone())
-}
-
-pub fn static_player_3(data: &Vec<PlayerData>) -> Player {
- Player::new(3, data[2].url.clone(), data[2].name.clone())
-}
-
-pub fn static_player_4(data: &Vec<PlayerData>) -> Player {
- Player::new(4, data[3].url.clone(), data[3].name.clone())
-}
-
-pub fn static_player_5(data: &Vec<PlayerData>) -> Player {
- Player::new(5, data[4].url.clone(), data[4].name.clone())
-}
-
-pub fn static_player_6(data: &Vec<PlayerData>) -> Player {
- Player::new(6, data[5].url.clone(), data[5].name.clone())
-}
-
-pub fn static_player_7(data: &Vec<PlayerData>) -> Player {
- Player::new(7, data[6].url.clone(), data[6].name.clone())
-}
-
-pub fn static_player_8(data: &Vec<PlayerData>) -> Player {
- Player::new(8, data[7].url.clone(), data[7].name.clone())
-}
-
-pub fn static_player_9(data: &Vec<PlayerData>) -> Player {
- Player::new(9, data[8].url.clone(), data[8].name.clone())
-}
-
-pub fn static_player_10(data: &Vec<PlayerData>) -> Player {
- Player::new(10, data[9].url.clone(), data[9].name.clone())
-}
-
-pub fn static_livestream() -> Livestream {
- Livestream {
- players: random_players_with_call_status(7),
- channel: Some("gpui2-ui".to_string()),
- }
-}
-
-pub fn populate_player_call_status(
- player: Player,
- followers: Option<Vec<Player>>,
-) -> PlayerCallStatus {
- let mut rng = rand::thread_rng();
- let in_current_project: bool = rng.gen();
- let disconnected: bool = rng.gen();
- let voice_activity: f32 = rng.gen();
- let mic_status = if rng.gen_bool(0.5) {
- MicStatus::Muted
- } else {
- MicStatus::Unmuted
- };
- let video_status = if rng.gen_bool(0.5) {
- VideoStatus::On
- } else {
- VideoStatus::Off
- };
- let screen_share_status = if rng.gen_bool(0.5) {
- ScreenShareStatus::Shared
- } else {
- ScreenShareStatus::NotShared
- };
- PlayerCallStatus {
- mic_status,
- voice_activity,
- video_status,
- screen_share_status,
- in_current_project,
- disconnected,
- following: None,
- followers,
- }
-}
-
-pub fn random_players_with_call_status(number_of_players: usize) -> Vec<PlayerWithCallStatus> {
- let players = create_static_players(static_player_data());
- let mut player_status = vec![];
- for i in 0..number_of_players {
- let followers = if i == 0 {
- Some(vec![
- players[1].clone(),
- players[3].clone(),
- players[5].clone(),
- players[6].clone(),
- ])
- } else if i == 1 {
- Some(vec![players[2].clone(), players[6].clone()])
- } else {
- None
- };
- let call_status = populate_player_call_status(players[i].clone(), followers);
- player_status.push(PlayerWithCallStatus::new(players[i].clone(), call_status));
- }
- player_status
-}
-
-pub fn static_players_with_call_status() -> Vec<PlayerWithCallStatus> {
- let players = static_players();
- let mut player_0_status = PlayerCallStatus::new();
- let player_1_status = PlayerCallStatus::new();
- let player_2_status = PlayerCallStatus::new();
- let mut player_3_status = PlayerCallStatus::new();
- let mut player_4_status = PlayerCallStatus::new();
-
- player_0_status.screen_share_status = ScreenShareStatus::Shared;
- player_0_status.followers = Some(vec![players[1].clone(), players[3].clone()]);
-
- player_3_status.voice_activity = 0.5;
- player_4_status.mic_status = MicStatus::Muted;
- player_4_status.in_current_project = false;
-
- vec![
- PlayerWithCallStatus::new(players[0].clone(), player_0_status),
- PlayerWithCallStatus::new(players[1].clone(), player_1_status),
- PlayerWithCallStatus::new(players[2].clone(), player_2_status),
- PlayerWithCallStatus::new(players[3].clone(), player_3_status),
- PlayerWithCallStatus::new(players[4].clone(), player_4_status),
- ]
-}
-
-pub fn static_new_notification_items_2<V: 'static>() -> Vec<Notification<V>> {
- vec![
- Notification::new_icon_message(
- "notif-1",
- "You were mentioned in a note.",
- DateTime::parse_from_rfc3339("2023-11-02T11:59:57Z")
- .unwrap()
- .naive_local(),
- Icon::AtSign,
- Arc::new(|_, _| {}),
- ),
- Notification::new_actor_with_actions(
- "notif-2",
- "as-cii sent you a contact request.",
- DateTime::parse_from_rfc3339("2023-11-02T12:09:07Z")
- .unwrap()
- .naive_local(),
- PublicPlayer::new("as-cii", "http://github.com/as-cii.png?s=50"),
- [
- NotificationAction::new(
- Button::new("Decline"),
- "Decline Request",
- (Some(Icon::XCircle), "Declined"),
- ),
- NotificationAction::new(
- Button::new("Accept").variant(crate::ButtonVariant::Filled),
- "Accept Request",
- (Some(Icon::Check), "Accepted"),
- ),
- ],
- ),
- Notification::new_icon_message(
- "notif-3",
- "You were mentioned #design.",
- DateTime::parse_from_rfc3339("2023-11-02T12:09:07Z")
- .unwrap()
- .naive_local(),
- Icon::MessageBubbles,
- Arc::new(|_, _| {}),
- ),
- Notification::new_actor_with_actions(
- "notif-4",
- "as-cii sent you a contact request.",
- DateTime::parse_from_rfc3339("2023-11-01T12:09:07Z")
- .unwrap()
- .naive_local(),
- PublicPlayer::new("as-cii", "http://github.com/as-cii.png?s=50"),
- [
- NotificationAction::new(
- Button::new("Decline"),
- "Decline Request",
- (Some(Icon::XCircle), "Declined"),
- ),
- NotificationAction::new(
- Button::new("Accept").variant(crate::ButtonVariant::Filled),
- "Accept Request",
- (Some(Icon::Check), "Accepted"),
- ),
- ],
- ),
- Notification::new_icon_message(
- "notif-5",
- "You were mentioned in a note.",
- DateTime::parse_from_rfc3339("2023-10-28T12:09:07Z")
- .unwrap()
- .naive_local(),
- Icon::AtSign,
- Arc::new(|_, _| {}),
- ),
- Notification::new_actor_with_actions(
- "notif-6",
- "as-cii sent you a contact request.",
- DateTime::parse_from_rfc3339("2022-10-25T12:09:07Z")
- .unwrap()
- .naive_local(),
- PublicPlayer::new("as-cii", "http://github.com/as-cii.png?s=50"),
- [
- NotificationAction::new(
- Button::new("Decline"),
- "Decline Request",
- (Some(Icon::XCircle), "Declined"),
- ),
- NotificationAction::new(
- Button::new("Accept").variant(crate::ButtonVariant::Filled),
- "Accept Request",
- (Some(Icon::Check), "Accepted"),
- ),
- ],
- ),
- Notification::new_icon_message(
- "notif-7",
- "You were mentioned in a note.",
- DateTime::parse_from_rfc3339("2022-10-14T12:09:07Z")
- .unwrap()
- .naive_local(),
- Icon::AtSign,
- Arc::new(|_, _| {}),
- ),
- Notification::new_actor_with_actions(
- "notif-8",
- "as-cii sent you a contact request.",
- DateTime::parse_from_rfc3339("2021-10-12T12:09:07Z")
- .unwrap()
- .naive_local(),
- PublicPlayer::new("as-cii", "http://github.com/as-cii.png?s=50"),
- [
- NotificationAction::new(
- Button::new("Decline"),
- "Decline Request",
- (Some(Icon::XCircle), "Declined"),
- ),
- NotificationAction::new(
- Button::new("Accept").variant(crate::ButtonVariant::Filled),
- "Accept Request",
- (Some(Icon::Check), "Accepted"),
- ),
- ],
- ),
- Notification::new_icon_message(
- "notif-9",
- "You were mentioned in a note.",
- DateTime::parse_from_rfc3339("2021-02-02T12:09:07Z")
- .unwrap()
- .naive_local(),
- Icon::AtSign,
- Arc::new(|_, _| {}),
- ),
- Notification::new_actor_with_actions(
- "notif-10",
- "as-cii sent you a contact request.",
- DateTime::parse_from_rfc3339("1969-07-20T00:00:00Z")
- .unwrap()
- .naive_local(),
- PublicPlayer::new("as-cii", "http://github.com/as-cii.png?s=50"),
- [
- NotificationAction::new(
- Button::new("Decline"),
- "Decline Request",
- (Some(Icon::XCircle), "Declined"),
- ),
- NotificationAction::new(
- Button::new("Accept").variant(crate::ButtonVariant::Filled),
- "Accept Request",
- (Some(Icon::Check), "Accepted"),
- ),
- ],
- ),
- ]
-}
-
-pub fn static_project_panel_project_items<V>() -> Vec<ListItem<V>> {
- vec![
- ListEntry::new(Label::new("zed"))
- .left_icon(Icon::FolderOpen.into())
- .indent_level(0)
- .toggle(Toggle::Toggled(true)),
- ListEntry::new(Label::new(".cargo"))
- .left_icon(Icon::Folder.into())
- .indent_level(1),
- ListEntry::new(Label::new(".config"))
- .left_icon(Icon::Folder.into())
- .indent_level(1),
- ListEntry::new(Label::new(".git").color(TextColor::Hidden))
- .left_icon(Icon::Folder.into())
- .indent_level(1),
- ListEntry::new(Label::new(".cargo"))
- .left_icon(Icon::Folder.into())
- .indent_level(1),
- ListEntry::new(Label::new(".idea").color(TextColor::Hidden))
- .left_icon(Icon::Folder.into())
- .indent_level(1),
- ListEntry::new(Label::new("assets"))
- .left_icon(Icon::Folder.into())
- .indent_level(1)
- .toggle(Toggle::Toggled(true)),
- ListEntry::new(Label::new("cargo-target").color(TextColor::Hidden))
- .left_icon(Icon::Folder.into())
- .indent_level(1),
- ListEntry::new(Label::new("crates"))
- .left_icon(Icon::FolderOpen.into())
- .indent_level(1)
- .toggle(Toggle::Toggled(true)),
- ListEntry::new(Label::new("activity_indicator"))
- .left_icon(Icon::Folder.into())
- .indent_level(2),
- ListEntry::new(Label::new("ai"))
- .left_icon(Icon::Folder.into())
- .indent_level(2),
- ListEntry::new(Label::new("audio"))
- .left_icon(Icon::Folder.into())
- .indent_level(2),
- ListEntry::new(Label::new("auto_update"))
- .left_icon(Icon::Folder.into())
- .indent_level(2),
- ListEntry::new(Label::new("breadcrumbs"))
- .left_icon(Icon::Folder.into())
- .indent_level(2),
- ListEntry::new(Label::new("call"))
- .left_icon(Icon::Folder.into())
- .indent_level(2),
- ListEntry::new(Label::new("sqlez").color(TextColor::Modified))
- .left_icon(Icon::Folder.into())
- .indent_level(2)
- .toggle(Toggle::Toggled(false)),
- ListEntry::new(Label::new("gpui2"))
- .left_icon(Icon::FolderOpen.into())
- .indent_level(2)
- .toggle(Toggle::Toggled(true)),
- ListEntry::new(Label::new("src"))
- .left_icon(Icon::FolderOpen.into())
- .indent_level(3)
- .toggle(Toggle::Toggled(true)),
- ListEntry::new(Label::new("derive_element.rs"))
- .left_icon(Icon::FileRust.into())
- .indent_level(4),
- ListEntry::new(Label::new("storybook").color(TextColor::Modified))
- .left_icon(Icon::FolderOpen.into())
- .indent_level(1)
- .toggle(Toggle::Toggled(true)),
- ListEntry::new(Label::new("docs").color(TextColor::Default))
- .left_icon(Icon::Folder.into())
- .indent_level(2)
- .toggle(Toggle::Toggled(true)),
- ListEntry::new(Label::new("src").color(TextColor::Modified))
- .left_icon(Icon::FolderOpen.into())
- .indent_level(3)
- .toggle(Toggle::Toggled(true)),
- ListEntry::new(Label::new("ui").color(TextColor::Modified))
- .left_icon(Icon::FolderOpen.into())
- .indent_level(4)
- .toggle(Toggle::Toggled(true)),
- ListEntry::new(Label::new("component").color(TextColor::Created))
- .left_icon(Icon::FolderOpen.into())
- .indent_level(5)
- .toggle(Toggle::Toggled(true)),
- ListEntry::new(Label::new("facepile.rs").color(TextColor::Default))
- .left_icon(Icon::FileRust.into())
- .indent_level(6),
- ListEntry::new(Label::new("follow_group.rs").color(TextColor::Default))
- .left_icon(Icon::FileRust.into())
- .indent_level(6),
- ListEntry::new(Label::new("list_item.rs").color(TextColor::Created))
- .left_icon(Icon::FileRust.into())
- .indent_level(6),
- ListEntry::new(Label::new("tab.rs").color(TextColor::Default))
- .left_icon(Icon::FileRust.into())
- .indent_level(6),
- ListEntry::new(Label::new("target").color(TextColor::Hidden))
- .left_icon(Icon::Folder.into())
- .indent_level(1),
- ListEntry::new(Label::new(".dockerignore"))
- .left_icon(Icon::FileGeneric.into())
- .indent_level(1),
- ListEntry::new(Label::new(".DS_Store").color(TextColor::Hidden))
- .left_icon(Icon::FileGeneric.into())
- .indent_level(1),
- ListEntry::new(Label::new("Cargo.lock"))
- .left_icon(Icon::FileLock.into())
- .indent_level(1),
- ListEntry::new(Label::new("Cargo.toml"))
- .left_icon(Icon::FileToml.into())
- .indent_level(1),
- ListEntry::new(Label::new("Dockerfile"))
- .left_icon(Icon::FileGeneric.into())
- .indent_level(1),
- ListEntry::new(Label::new("Procfile"))
- .left_icon(Icon::FileGeneric.into())
- .indent_level(1),
- ListEntry::new(Label::new("README.md"))
- .left_icon(Icon::FileDoc.into())
- .indent_level(1),
- ]
- .into_iter()
- .map(From::from)
- .collect()
-}
-
-pub fn static_project_panel_single_items<V>() -> Vec<ListItem<V>> {
- vec![
- ListEntry::new(Label::new("todo.md"))
- .left_icon(Icon::FileDoc.into())
- .indent_level(0),
- ListEntry::new(Label::new("README.md"))
- .left_icon(Icon::FileDoc.into())
- .indent_level(0),
- ListEntry::new(Label::new("config.json"))
- .left_icon(Icon::FileGeneric.into())
- .indent_level(0),
- ]
- .into_iter()
- .map(From::from)
- .collect()
-}
-
-pub fn static_collab_panel_current_call<V>() -> Vec<ListItem<V>> {
- vec![
- ListEntry::new(Label::new("as-cii")).left_avatar("http://github.com/as-cii.png?s=50"),
- ListEntry::new(Label::new("nathansobo"))
- .left_avatar("http://github.com/nathansobo.png?s=50"),
- ListEntry::new(Label::new("maxbrunsfeld"))
- .left_avatar("http://github.com/maxbrunsfeld.png?s=50"),
- ]
- .into_iter()
- .map(From::from)
- .collect()
-}
-
-pub fn static_collab_panel_channels<V>() -> Vec<ListItem<V>> {
- vec![
- ListEntry::new(Label::new("zed"))
- .left_icon(Icon::Hash.into())
- .size(ListEntrySize::Medium)
- .indent_level(0),
- ListEntry::new(Label::new("community"))
- .left_icon(Icon::Hash.into())
- .size(ListEntrySize::Medium)
- .indent_level(1),
- ListEntry::new(Label::new("dashboards"))
- .left_icon(Icon::Hash.into())
- .size(ListEntrySize::Medium)
- .indent_level(2),
- ListEntry::new(Label::new("feedback"))
- .left_icon(Icon::Hash.into())
- .size(ListEntrySize::Medium)
- .indent_level(2),
- ListEntry::new(Label::new("teams-in-channels-alpha"))
- .left_icon(Icon::Hash.into())
- .size(ListEntrySize::Medium)
- .indent_level(2),
- ListEntry::new(Label::new("current-projects"))
- .left_icon(Icon::Hash.into())
- .size(ListEntrySize::Medium)
- .indent_level(1),
- ListEntry::new(Label::new("codegen"))
- .left_icon(Icon::Hash.into())
- .size(ListEntrySize::Medium)
- .indent_level(2),
- ListEntry::new(Label::new("gpui2"))
- .left_icon(Icon::Hash.into())
- .size(ListEntrySize::Medium)
- .indent_level(2),
- ListEntry::new(Label::new("livestreaming"))
- .left_icon(Icon::Hash.into())
- .size(ListEntrySize::Medium)
- .indent_level(2),
- ListEntry::new(Label::new("open-source"))
- .left_icon(Icon::Hash.into())
- .size(ListEntrySize::Medium)
- .indent_level(2),
- ListEntry::new(Label::new("replace"))
- .left_icon(Icon::Hash.into())
- .size(ListEntrySize::Medium)
- .indent_level(2),
- ListEntry::new(Label::new("semantic-index"))
- .left_icon(Icon::Hash.into())
- .size(ListEntrySize::Medium)
- .indent_level(2),
- ListEntry::new(Label::new("vim"))
- .left_icon(Icon::Hash.into())
- .size(ListEntrySize::Medium)
- .indent_level(2),
- ListEntry::new(Label::new("web-tech"))
- .left_icon(Icon::Hash.into())
- .size(ListEntrySize::Medium)
- .indent_level(2),
- ]
- .into_iter()
- .map(From::from)
- .collect()
-}
-
-pub fn example_editor_actions() -> Vec<PaletteItem> {
- vec![
- PaletteItem::new("New File").key_binding(KeyBinding::new(binding("cmd-n"))),
- PaletteItem::new("Open File").key_binding(KeyBinding::new(binding("cmd-o"))),
- PaletteItem::new("Save File").key_binding(KeyBinding::new(binding("cmd-s"))),
- PaletteItem::new("Cut").key_binding(KeyBinding::new(binding("cmd-x"))),
- PaletteItem::new("Copy").key_binding(KeyBinding::new(binding("cmd-c"))),
- PaletteItem::new("Paste").key_binding(KeyBinding::new(binding("cmd-v"))),
- PaletteItem::new("Undo").key_binding(KeyBinding::new(binding("cmd-z"))),
- PaletteItem::new("Redo").key_binding(KeyBinding::new(binding("cmd-shift-z"))),
- PaletteItem::new("Find").key_binding(KeyBinding::new(binding("cmd-f"))),
- PaletteItem::new("Replace").key_binding(KeyBinding::new(binding("cmd-r"))),
- PaletteItem::new("Jump to Line"),
- PaletteItem::new("Select All"),
- PaletteItem::new("Deselect All"),
- PaletteItem::new("Switch Document"),
- PaletteItem::new("Insert Line Below"),
- PaletteItem::new("Insert Line Above"),
- PaletteItem::new("Move Line Up"),
- PaletteItem::new("Move Line Down"),
- PaletteItem::new("Toggle Comment"),
- PaletteItem::new("Delete Line"),
- ]
-}
-
-pub fn empty_editor_example(cx: &mut ViewContext<EditorPane>) -> EditorPane {
- EditorPane::new(
- cx,
- static_tabs_example(),
- PathBuf::from_str("crates/ui/src/static_data.rs").unwrap(),
- vec![],
- empty_buffer_example(),
- )
-}
-
-pub fn empty_buffer_example() -> Buffer {
- Buffer::new("empty-buffer").set_rows(Some(BufferRows::default()))
-}
-
-pub fn hello_world_rust_editor_example(cx: &mut ViewContext<EditorPane>) -> EditorPane {
- EditorPane::new(
- cx,
- static_tabs_example(),
- PathBuf::from_str("crates/ui/src/static_data.rs").unwrap(),
- vec![Symbol(vec![
- HighlightedText {
- text: "fn ".to_string(),
- color: cx.theme().syntax_color("keyword"),
- },
- HighlightedText {
- text: "main".to_string(),
- color: cx.theme().syntax_color("function"),
- },
- ])],
- hello_world_rust_buffer_example(cx),
- )
-}
-
-pub fn hello_world_rust_buffer_example(cx: &AppContext) -> Buffer {
- Buffer::new("hello-world-rust-buffer")
- .set_title("hello_world.rs".to_string())
- .set_path("src/hello_world.rs".to_string())
- .set_language("rust".to_string())
- .set_rows(Some(BufferRows {
- show_line_numbers: true,
- rows: hello_world_rust_buffer_rows(cx),
- }))
-}
-
-pub fn hello_world_rust_buffer_rows(cx: &AppContext) -> Vec<BufferRow> {
- let show_line_number = true;
-
- vec![
- BufferRow {
- line_number: 1,
- code_action: false,
- current: true,
- line: Some(HighlightedLine {
- highlighted_texts: vec![
- HighlightedText {
- text: "fn ".to_string(),
- color: cx.theme().syntax_color("keyword"),
- },
- HighlightedText {
- text: "main".to_string(),
- color: cx.theme().syntax_color("function"),
- },
- HighlightedText {
- text: "() {".to_string(),
- color: cx.theme().colors().text,
- },
- ],
- }),
- cursors: None,
- status: GitStatus::None,
- show_line_number,
- },
- BufferRow {
- line_number: 2,
- code_action: false,
- current: false,
- line: Some(HighlightedLine {
- highlighted_texts: vec![HighlightedText {
- text: " // Statements here are executed when the compiled binary is called."
- .to_string(),
- color: cx.theme().syntax_color("comment"),
- }],
- }),
- cursors: None,
- status: GitStatus::None,
- show_line_number,
- },
- BufferRow {
- line_number: 3,
- code_action: false,
- current: false,
- line: None,
- cursors: None,
- status: GitStatus::None,
- show_line_number,
- },
- BufferRow {
- line_number: 4,
- code_action: false,
- current: false,
- line: Some(HighlightedLine {
- highlighted_texts: vec![HighlightedText {
- text: " // Print text to the console.".to_string(),
- color: cx.theme().syntax_color("comment"),
- }],
- }),
- cursors: None,
- status: GitStatus::None,
- show_line_number,
- },
- BufferRow {
- line_number: 5,
- code_action: false,
- current: false,
- line: Some(HighlightedLine {
- highlighted_texts: vec![
- HighlightedText {
- text: " println!(".to_string(),
- color: cx.theme().colors().text,
- },
- HighlightedText {
- text: "\"Hello, world!\"".to_string(),
- color: cx.theme().syntax_color("string"),
- },
- HighlightedText {
- text: ");".to_string(),
- color: cx.theme().colors().text,
- },
- ],
- }),
- cursors: None,
- status: GitStatus::None,
- show_line_number,
- },
- BufferRow {
- line_number: 6,
- code_action: false,
- current: false,
- line: Some(HighlightedLine {
- highlighted_texts: vec![HighlightedText {
- text: "}".to_string(),
- color: cx.theme().colors().text,
- }],
- }),
- cursors: None,
- status: GitStatus::None,
- show_line_number,
- },
- ]
-}
-
-pub fn hello_world_rust_editor_with_status_example(cx: &mut ViewContext<EditorPane>) -> EditorPane {
- EditorPane::new(
- cx,
- static_tabs_example(),
- PathBuf::from_str("crates/ui/src/static_data.rs").unwrap(),
- vec![Symbol(vec![
- HighlightedText {
- text: "fn ".to_string(),
- color: cx.theme().syntax_color("keyword"),
- },
- HighlightedText {
- text: "main".to_string(),
- color: cx.theme().syntax_color("function"),
- },
- ])],
- hello_world_rust_buffer_with_status_example(cx),
- )
-}
-
-pub fn hello_world_rust_buffer_with_status_example(cx: &AppContext) -> Buffer {
- Buffer::new("hello-world-rust-buffer-with-status")
- .set_title("hello_world.rs".to_string())
- .set_path("src/hello_world.rs".to_string())
- .set_language("rust".to_string())
- .set_rows(Some(BufferRows {
- show_line_numbers: true,
- rows: hello_world_rust_with_status_buffer_rows(cx),
- }))
-}
-
-pub fn hello_world_rust_with_status_buffer_rows(cx: &AppContext) -> Vec<BufferRow> {
- let show_line_number = true;
-
- vec![
- BufferRow {
- line_number: 1,
- code_action: false,
- current: true,
- line: Some(HighlightedLine {
- highlighted_texts: vec![
- HighlightedText {
- text: "fn ".to_string(),
- color: cx.theme().syntax_color("keyword"),
- },
- HighlightedText {
- text: "main".to_string(),
- color: cx.theme().syntax_color("function"),
- },
- HighlightedText {
- text: "() {".to_string(),
- color: cx.theme().colors().text,
- },
- ],
- }),
- cursors: None,
- status: GitStatus::None,
- show_line_number,
- },
- BufferRow {
- line_number: 2,
- code_action: false,
- current: false,
- line: Some(HighlightedLine {
- highlighted_texts: vec![HighlightedText {
- text: "// Statements here are executed when the compiled binary is called."
- .to_string(),
- color: cx.theme().syntax_color("comment"),
- }],
- }),
- cursors: None,
- status: GitStatus::Modified,
- show_line_number,
- },
- BufferRow {
- line_number: 3,
- code_action: false,
- current: false,
- line: None,
- cursors: None,
- status: GitStatus::None,
- show_line_number,
- },
- BufferRow {
- line_number: 4,
- code_action: false,
- current: false,
- line: Some(HighlightedLine {
- highlighted_texts: vec![HighlightedText {
- text: " // Print text to the console.".to_string(),
- color: cx.theme().syntax_color("comment"),
- }],
- }),
- cursors: None,
- status: GitStatus::None,
- show_line_number,
- },
- BufferRow {
- line_number: 5,
- code_action: false,
- current: false,
- line: Some(HighlightedLine {
- highlighted_texts: vec![
- HighlightedText {
- text: " println!(".to_string(),
- color: cx.theme().colors().text,
- },
- HighlightedText {
- text: "\"Hello, world!\"".to_string(),
- color: cx.theme().syntax_color("string"),
- },
- HighlightedText {
- text: ");".to_string(),
- color: cx.theme().colors().text,
- },
- ],
- }),
- cursors: None,
- status: GitStatus::None,
- show_line_number,
- },
- BufferRow {
- line_number: 6,
- code_action: false,
- current: false,
- line: Some(HighlightedLine {
- highlighted_texts: vec![HighlightedText {
- text: "}".to_string(),
- color: cx.theme().colors().text,
- }],
- }),
- cursors: None,
- status: GitStatus::None,
- show_line_number,
- },
- BufferRow {
- line_number: 7,
- code_action: false,
- current: false,
- line: Some(HighlightedLine {
- highlighted_texts: vec![HighlightedText {
- text: "".to_string(),
- color: cx.theme().colors().text,
- }],
- }),
- cursors: None,
- status: GitStatus::Created,
- show_line_number,
- },
- BufferRow {
- line_number: 8,
- code_action: false,
- current: false,
- line: Some(HighlightedLine {
- highlighted_texts: vec![HighlightedText {
- text: "// Marshall and Nate were here".to_string(),
- color: cx.theme().syntax_color("comment"),
- }],
- }),
- cursors: None,
- status: GitStatus::Created,
- show_line_number,
- },
- ]
-}
-
-pub fn terminal_buffer(cx: &AppContext) -> Buffer {
- Buffer::new("terminal")
- .set_title("zed — fish".to_string())
- .set_rows(Some(BufferRows {
- show_line_numbers: false,
- rows: terminal_buffer_rows(cx),
- }))
-}
-
-pub fn terminal_buffer_rows(cx: &AppContext) -> Vec<BufferRow> {
- let show_line_number = false;
-
- vec![
- BufferRow {
- line_number: 1,
- code_action: false,
- current: false,
- line: Some(HighlightedLine {
- highlighted_texts: vec![
- HighlightedText {
- text: "maxdeviant ".to_string(),
- color: cx.theme().syntax_color("keyword"),
- },
- HighlightedText {
- text: "in ".to_string(),
- color: cx.theme().colors().text,
- },
- HighlightedText {
- text: "profaned-capital ".to_string(),
- color: cx.theme().syntax_color("function"),
- },
- HighlightedText {
- text: "in ".to_string(),
- color: cx.theme().colors().text,
- },
- HighlightedText {
- text: "~/p/zed ".to_string(),
- color: cx.theme().syntax_color("function"),
- },
- HighlightedText {
- text: "on ".to_string(),
- color: cx.theme().colors().text,
- },
- HighlightedText {
- text: " gpui2-ui ".to_string(),
- color: cx.theme().syntax_color("keyword"),
- },
- ],
- }),
- cursors: None,
- status: GitStatus::None,
- show_line_number,
- },
- BufferRow {
- line_number: 2,
- code_action: false,
- current: false,
- line: Some(HighlightedLine {
- highlighted_texts: vec![HighlightedText {
- text: "λ ".to_string(),
- color: cx.theme().syntax_color("string"),
- }],
- }),
- cursors: None,
- status: GitStatus::None,
- show_line_number,
- },
- ]
-}
@@ -1,37 +0,0 @@
-use gpui::Div;
-
-use crate::prelude::*;
-
-pub struct Story {}
-
-impl Story {
- pub fn container<V: 'static>(cx: &mut ViewContext<V>) -> Div<V> {
- div()
- .size_full()
- .flex()
- .flex_col()
- .pt_2()
- .px_4()
- .bg(cx.theme().colors().background)
- }
-
- pub fn title<V: 'static>(cx: &mut ViewContext<V>, title: &str) -> impl Component<V> {
- div()
- .text_xl()
- .text_color(cx.theme().colors().text)
- .child(title.to_owned())
- }
-
- pub fn title_for<V: 'static, T>(cx: &mut ViewContext<V>) -> impl Component<V> {
- Self::title(cx, std::any::type_name::<T>())
- }
-
- pub fn label<V: 'static>(cx: &mut ViewContext<V>, label: &str) -> impl Component<V> {
- div()
- .mt_4()
- .mb_2()
- .text_xs()
- .text_color(cx.theme().colors().text)
- .child(label.to_owned())
- }
-}
@@ -1,12 +1,12 @@
-use gpui::{Styled, ViewContext};
+use gpui::{px, Styled, WindowContext};
use theme2::ActiveTheme;
use crate::{ElevationIndex, UITextSize};
-fn elevated<E: Styled, V: 'static>(this: E, cx: &mut ViewContext<V>, index: ElevationIndex) -> E {
+fn elevated<E: Styled>(this: E, cx: &mut WindowContext, index: ElevationIndex) -> E {
this.bg(cx.theme().colors().elevated_surface_background)
.z_index(index.z_index())
- .rounded_lg()
+ .rounded(px(8.))
.border()
.border_color(cx.theme().colors().border_variant)
.shadow(index.shadow())
@@ -65,7 +65,7 @@ pub trait StyledExt: Styled + Sized {
/// Sets `bg()`, `rounded_lg()`, `border()`, `border_color()`, `shadow()`
///
/// Example Elements: Title Bar, Panel, Tab Bar, Editor
- fn elevation_1<V: 'static>(self, cx: &mut ViewContext<V>) -> Self {
+ fn elevation_1(self, cx: &mut WindowContext) -> Self {
elevated(self, cx, ElevationIndex::Surface)
}
@@ -74,12 +74,10 @@ pub trait StyledExt: Styled + Sized {
/// Sets `bg()`, `rounded_lg()`, `border()`, `border_color()`, `shadow()`
///
/// Examples: Notifications, Palettes, Detached/Floating Windows, Detached/Floating Panels
- fn elevation_2<V: 'static>(self, cx: &mut ViewContext<V>) -> Self {
+ fn elevation_2(self, cx: &mut WindowContext) -> Self {
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<V: 'static>(self, cx: &mut ViewContext<V>) -> 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),
+ }
+ }
+}
@@ -1,47 +0,0 @@
-mod assistant_panel;
-mod breadcrumb;
-mod buffer;
-mod buffer_search;
-mod chat_panel;
-mod collab_panel;
-mod command_palette;
-mod copilot;
-mod editor_pane;
-mod language_selector;
-mod multi_buffer;
-mod notifications_panel;
-mod panes;
-mod project_panel;
-mod recent_projects;
-mod status_bar;
-mod tab_bar;
-mod terminal;
-mod theme_selector;
-mod title_bar;
-mod toolbar;
-mod traffic_lights;
-mod workspace;
-
-pub use assistant_panel::*;
-pub use breadcrumb::*;
-pub use buffer::*;
-pub use buffer_search::*;
-pub use chat_panel::*;
-pub use collab_panel::*;
-pub use command_palette::*;
-pub use copilot::*;
-pub use editor_pane::*;
-pub use language_selector::*;
-pub use multi_buffer::*;
-pub use notifications_panel::*;
-pub use panes::*;
-pub use project_panel::*;
-pub use recent_projects::*;
-pub use status_bar::*;
-pub use tab_bar::*;
-pub use terminal::*;
-pub use theme_selector::*;
-pub use title_bar::*;
-pub use toolbar::*;
-pub use traffic_lights::*;
-pub use workspace::*;
@@ -1,93 +0,0 @@
-use crate::prelude::*;
-use crate::{Icon, IconButton, Label, Panel, PanelSide};
-use gpui::{prelude::*, rems, AbsoluteLength};
-
-#[derive(Component)]
-pub struct AssistantPanel {
- id: ElementId,
- current_side: PanelSide,
-}
-
-impl AssistantPanel {
- pub fn new(id: impl Into<ElementId>) -> Self {
- Self {
- id: id.into(),
- current_side: PanelSide::default(),
- }
- }
-
- pub fn side(mut self, side: PanelSide) -> Self {
- self.current_side = side;
- self
- }
-
- fn render<V: 'static>(self, view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- Panel::new(self.id.clone(), cx)
- .children(vec![div()
- .flex()
- .flex_col()
- .h_full()
- .px_2()
- .gap_2()
- // Header
- .child(
- div()
- .flex()
- .justify_between()
- .gap_2()
- .child(
- div()
- .flex()
- .child(IconButton::new("menu", Icon::Menu))
- .child(Label::new("New Conversation")),
- )
- .child(
- div()
- .flex()
- .items_center()
- .gap_px()
- .child(IconButton::new("split_message", Icon::SplitMessage))
- .child(IconButton::new("quote", Icon::Quote))
- .child(IconButton::new("magic_wand", Icon::MagicWand))
- .child(IconButton::new("plus", Icon::Plus))
- .child(IconButton::new("maximize", Icon::Maximize)),
- ),
- )
- // Chat Body
- .child(
- div()
- .id("chat-body")
- .w_full()
- .flex()
- .flex_col()
- .gap_3()
- .overflow_y_scroll()
- .child(Label::new("Is this thing on?")),
- )
- .render()])
- .side(self.current_side)
- .width(AbsoluteLength::Rems(rems(32.)))
- }
-}
-
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use super::*;
- use crate::Story;
- use gpui::{Div, Render};
- pub struct AssistantPanelStory;
-
- impl Render for AssistantPanelStory {
- type Element = Div<Self>;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- Story::container(cx)
- .child(Story::title_for::<_, AssistantPanel>(cx))
- .child(Story::label(cx, "Default"))
- .child(AssistantPanel::new("assistant-panel"))
- }
- }
-}
@@ -1,113 +0,0 @@
-use crate::{h_stack, prelude::*, HighlightedText};
-use gpui::{prelude::*, Div};
-use std::path::PathBuf;
-
-#[derive(Clone)]
-pub struct Symbol(pub Vec<HighlightedText>);
-
-#[derive(Component)]
-pub struct Breadcrumb {
- path: PathBuf,
- symbols: Vec<Symbol>,
-}
-
-impl Breadcrumb {
- pub fn new(path: PathBuf, symbols: Vec<Symbol>) -> Self {
- Self { path, symbols }
- }
-
- fn render_separator<V: 'static>(&self, cx: &WindowContext) -> Div<V> {
- div()
- .child(" › ")
- .text_color(cx.theme().colors().text_muted)
- }
-
- fn render<V: 'static>(self, view_state: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- let symbols_len = self.symbols.len();
-
- h_stack()
- .id("breadcrumb")
- .px_1()
- .text_ui_sm()
- .text_color(cx.theme().colors().text_muted)
- .rounded_md()
- .hover(|style| style.bg(cx.theme().colors().ghost_element_hover))
- .active(|style| style.bg(cx.theme().colors().ghost_element_active))
- .child(self.path.clone().to_str().unwrap().to_string())
- .child(if !self.symbols.is_empty() {
- self.render_separator(cx)
- } else {
- div()
- })
- .child(
- div().flex().children(
- self.symbols
- .iter()
- .enumerate()
- // TODO: Could use something like `intersperse` here instead.
- .flat_map(|(ix, symbol)| {
- let mut items =
- vec![div().flex().children(symbol.0.iter().map(|segment| {
- div().child(segment.text.clone()).text_color(segment.color)
- }))];
-
- let is_last_segment = ix == symbols_len - 1;
- if !is_last_segment {
- items.push(self.render_separator(cx));
- }
-
- items
- })
- .collect::<Vec<_>>(),
- ),
- )
- }
-}
-
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use super::*;
- use crate::Story;
- use gpui::Render;
- use std::str::FromStr;
-
- pub struct BreadcrumbStory;
-
- impl Render for BreadcrumbStory {
- type Element = Div<Self>;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- Story::container(cx)
- .child(Story::title_for::<_, Breadcrumb>(cx))
- .child(Story::label(cx, "Default"))
- .child(Breadcrumb::new(
- PathBuf::from_str("crates/ui/src/components/toolbar.rs").unwrap(),
- vec![
- Symbol(vec![
- HighlightedText {
- text: "impl ".to_string(),
- color: cx.theme().syntax_color("keyword"),
- },
- HighlightedText {
- text: "BreadcrumbStory".to_string(),
- color: cx.theme().syntax_color("function"),
- },
- ]),
- Symbol(vec![
- HighlightedText {
- text: "fn ".to_string(),
- color: cx.theme().syntax_color("keyword"),
- },
- HighlightedText {
- text: "render".to_string(),
- color: cx.theme().syntax_color("function"),
- },
- ]),
- ],
- ))
- }
- }
-}
@@ -1,266 +0,0 @@
-use gpui::{Hsla, WindowContext};
-
-use crate::prelude::*;
-use crate::{h_stack, v_stack, Icon, IconElement};
-
-#[derive(Default, PartialEq, Copy, Clone)]
-pub struct PlayerCursor {
- color: Hsla,
- index: usize,
-}
-
-#[derive(Default, PartialEq, Clone)]
-pub struct HighlightedText {
- pub text: String,
- pub color: Hsla,
-}
-
-#[derive(Default, PartialEq, Clone)]
-pub struct HighlightedLine {
- pub highlighted_texts: Vec<HighlightedText>,
-}
-
-#[derive(Default, PartialEq, Clone)]
-pub struct BufferRow {
- pub line_number: usize,
- pub code_action: bool,
- pub current: bool,
- pub line: Option<HighlightedLine>,
- pub cursors: Option<Vec<PlayerCursor>>,
- pub status: GitStatus,
- pub show_line_number: bool,
-}
-
-#[derive(Clone)]
-pub struct BufferRows {
- pub show_line_numbers: bool,
- pub rows: Vec<BufferRow>,
-}
-
-impl Default for BufferRows {
- fn default() -> Self {
- Self {
- show_line_numbers: true,
- rows: vec![BufferRow {
- line_number: 1,
- code_action: false,
- current: true,
- line: None,
- cursors: None,
- status: GitStatus::None,
- show_line_number: true,
- }],
- }
- }
-}
-
-impl BufferRow {
- pub fn new(line_number: usize) -> Self {
- Self {
- line_number,
- code_action: false,
- current: false,
- line: None,
- cursors: None,
- status: GitStatus::None,
- show_line_number: true,
- }
- }
-
- pub fn set_line(mut self, line: Option<HighlightedLine>) -> Self {
- self.line = line;
- self
- }
-
- pub fn set_cursors(mut self, cursors: Option<Vec<PlayerCursor>>) -> Self {
- self.cursors = cursors;
- self
- }
-
- pub fn add_cursor(mut self, cursor: PlayerCursor) -> Self {
- if let Some(cursors) = &mut self.cursors {
- cursors.push(cursor);
- } else {
- self.cursors = Some(vec![cursor]);
- }
- self
- }
-
- pub fn set_status(mut self, status: GitStatus) -> Self {
- self.status = status;
- self
- }
-
- pub fn set_show_line_number(mut self, show_line_number: bool) -> Self {
- self.show_line_number = show_line_number;
- self
- }
-
- pub fn set_code_action(mut self, code_action: bool) -> Self {
- self.code_action = code_action;
- self
- }
-
- pub fn set_current(mut self, current: bool) -> Self {
- self.current = current;
- self
- }
-}
-
-#[derive(Component, Clone)]
-pub struct Buffer {
- id: ElementId,
- rows: Option<BufferRows>,
- readonly: bool,
- language: Option<String>,
- title: Option<String>,
- path: Option<String>,
-}
-
-impl Buffer {
- pub fn new(id: impl Into<ElementId>) -> Self {
- Self {
- id: id.into(),
- rows: Some(BufferRows::default()),
- readonly: false,
- language: None,
- title: Some("untitled".to_string()),
- path: None,
- }
- }
-
- pub fn set_title<T: Into<Option<String>>>(mut self, title: T) -> Self {
- self.title = title.into();
- self
- }
-
- pub fn set_path<P: Into<Option<String>>>(mut self, path: P) -> Self {
- self.path = path.into();
- self
- }
-
- pub fn set_readonly(mut self, readonly: bool) -> Self {
- self.readonly = readonly;
- self
- }
-
- pub fn set_rows<R: Into<Option<BufferRows>>>(mut self, rows: R) -> Self {
- self.rows = rows.into();
- self
- }
-
- pub fn set_language<L: Into<Option<String>>>(mut self, language: L) -> Self {
- self.language = language.into();
- self
- }
-
- fn render_row<V: 'static>(row: BufferRow, cx: &WindowContext) -> impl Component<V> {
- let line_background = if row.current {
- cx.theme().colors().editor_active_line_background
- } else {
- cx.theme().styles.system.transparent
- };
-
- let line_number_color = if row.current {
- cx.theme().colors().text
- } else {
- cx.theme().syntax_color("comment")
- };
-
- h_stack()
- .bg(line_background)
- .w_full()
- .gap_2()
- .px_1()
- .child(
- h_stack()
- .w_4()
- .h_full()
- .px_0p5()
- .when(row.code_action, |c| {
- div().child(IconElement::new(Icon::Bolt))
- }),
- )
- .when(row.show_line_number, |this| {
- this.child(
- h_stack().justify_end().px_0p5().w_3().child(
- div()
- .text_color(line_number_color)
- .child(row.line_number.to_string()),
- ),
- )
- })
- .child(div().mx_0p5().w_1().h_full().bg(row.status.hsla(cx)))
- .children(row.line.map(|line| {
- div()
- .flex()
- .children(line.highlighted_texts.iter().map(|highlighted_text| {
- div()
- .text_color(highlighted_text.color)
- .child(highlighted_text.text.clone())
- }))
- }))
- }
-
- fn render_rows<V: 'static>(&self, cx: &WindowContext) -> Vec<impl Component<V>> {
- match &self.rows {
- Some(rows) => rows
- .rows
- .iter()
- .map(|row| Self::render_row(row.clone(), cx))
- .collect(),
- None => vec![],
- }
- }
-
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- let rows = self.render_rows(cx);
-
- v_stack()
- .flex_1()
- .w_full()
- .h_full()
- .bg(cx.theme().colors().editor_background)
- .children(rows)
- }
-}
-
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use super::*;
- use crate::{
- empty_buffer_example, hello_world_rust_buffer_example,
- hello_world_rust_buffer_with_status_example, Story,
- };
- use gpui::{rems, Div, Render};
-
- pub struct BufferStory;
-
- impl Render for BufferStory {
- type Element = Div<Self>;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- Story::container(cx)
- .child(Story::title_for::<_, Buffer>(cx))
- .child(Story::label(cx, "Default"))
- .child(div().w(rems(64.)).h_96().child(empty_buffer_example()))
- .child(Story::label(cx, "Hello World (Rust)"))
- .child(
- div()
- .w(rems(64.))
- .h_96()
- .child(hello_world_rust_buffer_example(cx)),
- )
- .child(Story::label(cx, "Hello World (Rust) with Status"))
- .child(
- div()
- .w(rems(64.))
- .h_96()
- .child(hello_world_rust_buffer_with_status_example(cx)),
- )
- }
- }
-}
@@ -1,46 +0,0 @@
-use gpui::{Div, Render, View, VisualContext};
-
-use crate::prelude::*;
-use crate::{h_stack, Icon, IconButton, Input, TextColor};
-
-#[derive(Clone)]
-pub struct BufferSearch {
- is_replace_open: bool,
-}
-
-impl BufferSearch {
- pub fn new() -> Self {
- Self {
- is_replace_open: false,
- }
- }
-
- fn toggle_replace(&mut self, cx: &mut ViewContext<Self>) {
- self.is_replace_open = !self.is_replace_open;
-
- cx.notify();
- }
-
- pub fn view(cx: &mut WindowContext) -> View<Self> {
- cx.build_view(|cx| Self::new())
- }
-}
-
-impl Render for BufferSearch {
- type Element = Div<Self>;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Div<Self> {
- h_stack()
- .bg(cx.theme().colors().toolbar_background)
- .p_2()
- .child(
- h_stack().child(Input::new("Search")).child(
- IconButton::<Self>::new("replace", Icon::Replace)
- .when(self.is_replace_open, |this| this.color(TextColor::Accent))
- .on_click(|buffer_search, cx| {
- buffer_search.toggle_replace(cx);
- }),
- ),
- )
- }
-}
@@ -1,150 +0,0 @@
-use crate::{prelude::*, Icon, IconButton, Input, Label};
-use chrono::NaiveDateTime;
-use gpui::prelude::*;
-
-#[derive(Component)]
-pub struct ChatPanel {
- element_id: ElementId,
- messages: Vec<ChatMessage>,
-}
-
-impl ChatPanel {
- pub fn new(element_id: impl Into<ElementId>) -> Self {
- Self {
- element_id: element_id.into(),
- messages: Vec::new(),
- }
- }
-
- pub fn messages(mut self, messages: Vec<ChatMessage>) -> Self {
- self.messages = messages;
- self
- }
-
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- div()
- .id(self.element_id.clone())
- .flex()
- .flex_col()
- .justify_between()
- .h_full()
- .px_2()
- .gap_2()
- // Header
- .child(
- div()
- .flex()
- .justify_between()
- .py_2()
- .child(div().flex().child(Label::new("#design")))
- .child(
- div()
- .flex()
- .items_center()
- .gap_px()
- .child(IconButton::new("file", Icon::File))
- .child(IconButton::new("audio_on", Icon::AudioOn)),
- ),
- )
- .child(
- div()
- .flex()
- .flex_col()
- // Chat Body
- .child(
- div()
- .id("chat-body")
- .w_full()
- .flex()
- .flex_col()
- .gap_3()
- .overflow_y_scroll()
- .children(self.messages),
- )
- // Composer
- .child(div().flex().my_2().child(Input::new("Message #design"))),
- )
- }
-}
-
-#[derive(Component)]
-pub struct ChatMessage {
- author: String,
- text: String,
- sent_at: NaiveDateTime,
-}
-
-impl ChatMessage {
- pub fn new(author: String, text: String, sent_at: NaiveDateTime) -> Self {
- Self {
- author,
- text,
- sent_at,
- }
- }
-
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- div()
- .flex()
- .flex_col()
- .child(
- div()
- .flex()
- .gap_2()
- .child(Label::new(self.author.clone()))
- .child(
- Label::new(self.sent_at.format("%m/%d/%Y").to_string())
- .color(TextColor::Muted),
- ),
- )
- .child(div().child(Label::new(self.text.clone())))
- }
-}
-
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use chrono::DateTime;
- use gpui::{Div, Render};
-
- use crate::{Panel, Story};
-
- use super::*;
-
- pub struct ChatPanelStory;
-
- impl Render for ChatPanelStory {
- type Element = Div<Self>;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- Story::container(cx)
- .child(Story::title_for::<_, ChatPanel>(cx))
- .child(Story::label(cx, "Default"))
- .child(
- Panel::new("chat-panel-1-outer", cx)
- .child(ChatPanel::new("chat-panel-1-inner")),
- )
- .child(Story::label(cx, "With Mesages"))
- .child(Panel::new("chat-panel-2-outer", cx).child(
- ChatPanel::new("chat-panel-2-inner").messages(vec![
- ChatMessage::new(
- "osiewicz".to_string(),
- "is this thing on?".to_string(),
- DateTime::parse_from_rfc3339("2023-09-27T15:40:52.707Z")
- .unwrap()
- .naive_local(),
- ),
- ChatMessage::new(
- "maxdeviant".to_string(),
- "Reading you loud and clear!".to_string(),
- DateTime::parse_from_rfc3339("2023-09-28T15:40:52.707Z")
- .unwrap()
- .naive_local(),
- ),
- ]),
- ))
- }
- }
-}
@@ -1,110 +0,0 @@
-use crate::{
- prelude::*, static_collab_panel_channels, static_collab_panel_current_call, v_stack, Icon,
- List, ListHeader, Toggle,
-};
-use gpui::prelude::*;
-
-#[derive(Component)]
-pub struct CollabPanel {
- id: ElementId,
-}
-
-impl CollabPanel {
- pub fn new(id: impl Into<ElementId>) -> Self {
- Self { id: id.into() }
- }
-
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- v_stack()
- .id(self.id.clone())
- .h_full()
- .bg(cx.theme().colors().surface_background)
- .child(
- v_stack()
- .id("crdb")
- .w_full()
- .overflow_y_scroll()
- .child(
- div()
- .pb_1()
- .border_color(cx.theme().colors().border)
- .border_b()
- .child(
- List::new(static_collab_panel_current_call())
- .header(
- ListHeader::new("CRDB")
- .left_icon(Icon::Hash.into())
- .toggle(Toggle::Toggled(true)),
- )
- .toggle(Toggle::Toggled(true)),
- ),
- )
- .child(
- v_stack().id("channels").py_1().child(
- List::new(static_collab_panel_channels())
- .header(ListHeader::new("CHANNELS").toggle(Toggle::Toggled(true)))
- .empty_message("No channels yet. Add a channel to get started.")
- .toggle(Toggle::Toggled(true)),
- ),
- )
- .child(
- v_stack().id("contacts-online").py_1().child(
- List::new(static_collab_panel_current_call())
- .header(
- ListHeader::new("CONTACTS – ONLINE")
- .toggle(Toggle::Toggled(true)),
- )
- .toggle(Toggle::Toggled(true)),
- ),
- )
- .child(
- v_stack().id("contacts-offline").py_1().child(
- List::new(static_collab_panel_current_call())
- .header(
- ListHeader::new("CONTACTS – OFFLINE")
- .toggle(Toggle::Toggled(false)),
- )
- .toggle(Toggle::Toggled(false)),
- ),
- ),
- )
- .child(
- div()
- .h_7()
- .px_2()
- .border_t()
- .border_color(cx.theme().colors().border)
- .flex()
- .items_center()
- .child(
- div()
- .text_ui_sm()
- .text_color(cx.theme().colors().text_placeholder)
- .child("Find..."),
- ),
- )
- }
-}
-
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use super::*;
- use crate::Story;
- use gpui::{Div, Render};
-
- pub struct CollabPanelStory;
-
- impl Render for CollabPanelStory {
- type Element = Div<Self>;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- Story::container(cx)
- .child(Story::title_for::<_, CollabPanel>(cx))
- .child(Story::label(cx, "Default"))
- .child(CollabPanel::new("collab-panel"))
- }
- }
-}
@@ -1,48 +0,0 @@
-use crate::prelude::*;
-use crate::{example_editor_actions, OrderMethod, Palette};
-
-#[derive(Component)]
-pub struct CommandPalette {
- id: ElementId,
-}
-
-impl CommandPalette {
- pub fn new(id: impl Into<ElementId>) -> Self {
- Self { id: id.into() }
- }
-
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- div().id(self.id.clone()).child(
- Palette::new("palette")
- .items(example_editor_actions())
- .placeholder("Execute a command...")
- .empty_string("No items found.")
- .default_order(OrderMethod::Ascending),
- )
- }
-}
-
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use gpui::{Div, Render};
-
- use crate::Story;
-
- use super::*;
-
- pub struct CommandPaletteStory;
-
- impl Render for CommandPaletteStory {
- type Element = Div<Self>;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- Story::container(cx)
- .child(Story::title_for::<_, CommandPalette>(cx))
- .child(Story::label(cx, "Default"))
- .child(CommandPalette::new("command-palette"))
- }
- }
-}
@@ -1,46 +0,0 @@
-use crate::{prelude::*, Button, Label, Modal, TextColor};
-
-#[derive(Component)]
-pub struct CopilotModal {
- id: ElementId,
-}
-
-impl CopilotModal {
- pub fn new(id: impl Into<ElementId>) -> Self {
- Self { id: id.into() }
- }
-
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- div().id(self.id.clone()).child(
- Modal::new("some-id")
- .title("Connect Copilot to Zed")
- .child(Label::new("You can update your settings or sign out from the Copilot menu in the status bar.").color(TextColor::Muted))
- .primary_action(Button::new("Connect to Github").variant(ButtonVariant::Filled)),
- )
- }
-}
-
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use gpui::{Div, Render};
-
- use crate::Story;
-
- use super::*;
-
- pub struct CopilotModalStory;
-
- impl Render for CopilotModalStory {
- type Element = Div<Self>;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- Story::container(cx)
- .child(Story::title_for::<_, CopilotModal>(cx))
- .child(Story::label(cx, "Default"))
- .child(CopilotModal::new("copilot-modal"))
- }
- }
-}
@@ -1,77 +0,0 @@
-use std::path::PathBuf;
-
-use gpui::{Div, Render, View, VisualContext};
-
-use crate::prelude::*;
-use crate::{
- hello_world_rust_editor_with_status_example, v_stack, Breadcrumb, Buffer, BufferSearch, Icon,
- IconButton, Symbol, Tab, TabBar, TextColor, Toolbar,
-};
-
-#[derive(Clone)]
-pub struct EditorPane {
- tabs: Vec<Tab>,
- path: PathBuf,
- symbols: Vec<Symbol>,
- buffer: Buffer,
- buffer_search: View<BufferSearch>,
- is_buffer_search_open: bool,
-}
-
-impl EditorPane {
- pub fn new(
- cx: &mut ViewContext<Self>,
- tabs: Vec<Tab>,
- path: PathBuf,
- symbols: Vec<Symbol>,
- buffer: Buffer,
- ) -> Self {
- Self {
- tabs,
- path,
- symbols,
- buffer,
- buffer_search: BufferSearch::view(cx),
- is_buffer_search_open: false,
- }
- }
-
- pub fn toggle_buffer_search(&mut self, cx: &mut ViewContext<Self>) {
- self.is_buffer_search_open = !self.is_buffer_search_open;
-
- cx.notify();
- }
-
- pub fn view(cx: &mut WindowContext) -> View<Self> {
- cx.build_view(|cx| hello_world_rust_editor_with_status_example(cx))
- }
-}
-
-impl Render for EditorPane {
- type Element = Div<Self>;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Div<Self> {
- v_stack()
- .w_full()
- .h_full()
- .flex_1()
- .child(TabBar::new("editor-pane-tabs", self.tabs.clone()).can_navigate((false, true)))
- .child(
- Toolbar::new()
- .left_item(Breadcrumb::new(self.path.clone(), self.symbols.clone()))
- .right_items(vec![
- IconButton::<Self>::new("toggle_inlay_hints", Icon::InlayHint),
- IconButton::<Self>::new("buffer_search", Icon::MagnifyingGlass)
- .when(self.is_buffer_search_open, |this| {
- this.color(TextColor::Accent)
- })
- .on_click(|editor: &mut Self, cx| {
- editor.toggle_buffer_search(cx);
- }),
- IconButton::new("inline_assist", Icon::MagicWand),
- ]),
- )
- .children(Some(self.buffer_search.clone()).filter(|_| self.is_buffer_search_open))
- .child(self.buffer.clone())
- }
-}
@@ -1,57 +0,0 @@
-use crate::prelude::*;
-use crate::{OrderMethod, Palette, PaletteItem};
-
-#[derive(Component)]
-pub struct LanguageSelector {
- id: ElementId,
-}
-
-impl LanguageSelector {
- pub fn new(id: impl Into<ElementId>) -> Self {
- Self { id: id.into() }
- }
-
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- div().id(self.id.clone()).child(
- Palette::new("palette")
- .items(vec![
- PaletteItem::new("C"),
- PaletteItem::new("C++"),
- PaletteItem::new("CSS"),
- PaletteItem::new("Elixir"),
- PaletteItem::new("Elm"),
- PaletteItem::new("ERB"),
- PaletteItem::new("Rust (current)"),
- PaletteItem::new("Scheme"),
- PaletteItem::new("TOML"),
- PaletteItem::new("TypeScript"),
- ])
- .placeholder("Select a language...")
- .empty_string("No matches")
- .default_order(OrderMethod::Ascending),
- )
- }
-}
-
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use super::*;
- use crate::Story;
- use gpui::{Div, Render};
-
- pub struct LanguageSelectorStory;
-
- impl Render for LanguageSelectorStory {
- type Element = Div<Self>;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- Story::container(cx)
- .child(Story::title_for::<_, LanguageSelector>(cx))
- .child(Story::label(cx, "Default"))
- .child(LanguageSelector::new("language-selector"))
- }
- }
-}
@@ -1,63 +0,0 @@
-use crate::prelude::*;
-use crate::{v_stack, Buffer, Icon, IconButton, Label};
-
-#[derive(Component)]
-pub struct MultiBuffer {
- buffers: Vec<Buffer>,
-}
-
-impl MultiBuffer {
- pub fn new(buffers: Vec<Buffer>) -> Self {
- Self { buffers }
- }
-
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- v_stack()
- .w_full()
- .h_full()
- .flex_1()
- .children(self.buffers.clone().into_iter().map(|buffer| {
- v_stack()
- .child(
- div()
- .flex()
- .items_center()
- .justify_between()
- .p_4()
- .bg(cx.theme().colors().editor_subheader_background)
- .child(Label::new("main.rs"))
- .child(IconButton::new("arrow_up_right", Icon::ArrowUpRight)),
- )
- .child(buffer)
- }))
- }
-}
-
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use super::*;
- use crate::{hello_world_rust_buffer_example, Story};
- use gpui::{Div, Render};
-
- pub struct MultiBufferStory;
-
- impl Render for MultiBufferStory {
- type Element = Div<Self>;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- Story::container(cx)
- .child(Story::title_for::<_, MultiBuffer>(cx))
- .child(Story::label(cx, "Default"))
- .child(MultiBuffer::new(vec![
- hello_world_rust_buffer_example(cx),
- hello_world_rust_buffer_example(cx),
- hello_world_rust_buffer_example(cx),
- hello_world_rust_buffer_example(cx),
- hello_world_rust_buffer_example(cx),
- ]))
- }
- }
-}
@@ -1,371 +0,0 @@
-use crate::{
- h_stack, prelude::*, static_new_notification_items_2, utils::naive_format_distance_from_now,
- v_stack, Avatar, ButtonOrIconButton, ClickHandler, Icon, IconElement, Label, LineHeightStyle,
- ListHeader, ListHeaderMeta, ListSeparator, PublicPlayer, TextColor, UnreadIndicator,
-};
-use gpui::prelude::*;
-
-#[derive(Component)]
-pub struct NotificationsPanel {
- id: ElementId,
-}
-
-impl NotificationsPanel {
- pub fn new(id: impl Into<ElementId>) -> Self {
- Self { id: id.into() }
- }
-
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- div()
- .id(self.id.clone())
- .flex()
- .flex_col()
- .size_full()
- .bg(cx.theme().colors().surface_background)
- .child(
- ListHeader::new("Notifications").meta(Some(ListHeaderMeta::Tools(vec![
- Icon::AtSign,
- Icon::BellOff,
- Icon::MailOpen,
- ]))),
- )
- .child(ListSeparator::new())
- .child(
- v_stack()
- .id("notifications-panel-scroll-view")
- .py_1()
- .overflow_y_scroll()
- .flex_1()
- .child(
- div()
- .mx_2()
- .p_1()
- // TODO: Add cursor style
- // .cursor(Cursor::IBeam)
- .bg(cx.theme().colors().element_background)
- .border()
- .border_color(cx.theme().colors().border_variant)
- .child(
- Label::new("Search...")
- .color(TextColor::Placeholder)
- .line_height_style(LineHeightStyle::UILabel),
- ),
- )
- .child(v_stack().px_1().children(static_new_notification_items_2())),
- )
- }
-}
-
-pub struct NotificationAction<V: 'static> {
- button: ButtonOrIconButton<V>,
- tooltip: SharedString,
- /// Shows after action is chosen
- ///
- /// For example, if the action is "Accept" the taken message could be:
- ///
- /// - `(None,"Accepted")` - "Accepted"
- ///
- /// - `(Some(Icon::Check),"Accepted")` - ✓ "Accepted"
- taken_message: (Option<Icon>, SharedString),
-}
-
-impl<V: 'static> NotificationAction<V> {
- pub fn new(
- button: impl Into<ButtonOrIconButton<V>>,
- tooltip: impl Into<SharedString>,
- (icon, taken_message): (Option<Icon>, impl Into<SharedString>),
- ) -> Self {
- Self {
- button: button.into(),
- tooltip: tooltip.into(),
- taken_message: (icon, taken_message.into()),
- }
- }
-}
-
-pub enum ActorOrIcon {
- Actor(PublicPlayer),
- Icon(Icon),
-}
-
-pub struct NotificationMeta<V: 'static> {
- items: Vec<(Option<Icon>, SharedString, Option<ClickHandler<V>>)>,
-}
-
-struct NotificationHandlers<V: 'static> {
- click: Option<ClickHandler<V>>,
-}
-
-impl<V: 'static> Default for NotificationHandlers<V> {
- fn default() -> Self {
- Self { click: None }
- }
-}
-
-#[derive(Component)]
-pub struct Notification<V: 'static> {
- id: ElementId,
- slot: ActorOrIcon,
- message: SharedString,
- date_received: NaiveDateTime,
- meta: Option<NotificationMeta<V>>,
- actions: Option<[NotificationAction<V>; 2]>,
- unread: bool,
- new: bool,
- action_taken: Option<NotificationAction<V>>,
- handlers: NotificationHandlers<V>,
-}
-
-impl<V> Notification<V> {
- fn new(
- id: ElementId,
- message: SharedString,
- date_received: NaiveDateTime,
- slot: ActorOrIcon,
- click_action: Option<ClickHandler<V>>,
- ) -> Self {
- let handlers = if click_action.is_some() {
- NotificationHandlers {
- click: click_action,
- }
- } else {
- NotificationHandlers::default()
- };
-
- Self {
- id,
- date_received,
- message,
- meta: None,
- slot,
- actions: None,
- unread: true,
- new: false,
- action_taken: None,
- handlers,
- }
- }
-
- /// Creates a new notification with an actor slot.
- ///
- /// Requires a click action.
- pub fn new_actor_message(
- id: impl Into<ElementId>,
- message: impl Into<SharedString>,
- date_received: NaiveDateTime,
- actor: PublicPlayer,
- click_action: ClickHandler<V>,
- ) -> Self {
- Self::new(
- id.into(),
- message.into(),
- date_received,
- ActorOrIcon::Actor(actor),
- Some(click_action),
- )
- }
-
- /// Creates a new notification with an icon slot.
- ///
- /// Requires a click action.
- pub fn new_icon_message(
- id: impl Into<ElementId>,
- message: impl Into<SharedString>,
- date_received: NaiveDateTime,
- icon: Icon,
- click_action: ClickHandler<V>,
- ) -> Self {
- Self::new(
- id.into(),
- message.into(),
- date_received,
- ActorOrIcon::Icon(icon),
- Some(click_action),
- )
- }
-
- /// Creates a new notification with an actor slot
- /// and a Call To Action row.
- ///
- /// Cannot take a click action due to required actions.
- pub fn new_actor_with_actions(
- id: impl Into<ElementId>,
- message: impl Into<SharedString>,
- date_received: NaiveDateTime,
- actor: PublicPlayer,
- actions: [NotificationAction<V>; 2],
- ) -> Self {
- Self::new(
- id.into(),
- message.into(),
- date_received,
- ActorOrIcon::Actor(actor),
- None,
- )
- .actions(actions)
- }
-
- /// Creates a new notification with an icon slot
- /// and a Call To Action row.
- ///
- /// Cannot take a click action due to required actions.
- pub fn new_icon_with_actions(
- id: impl Into<ElementId>,
- message: impl Into<SharedString>,
- date_received: NaiveDateTime,
- icon: Icon,
- actions: [NotificationAction<V>; 2],
- ) -> Self {
- Self::new(
- id.into(),
- message.into(),
- date_received,
- ActorOrIcon::Icon(icon),
- None,
- )
- .actions(actions)
- }
-
- fn on_click(mut self, handler: ClickHandler<V>) -> Self {
- self.handlers.click = Some(handler);
- self
- }
-
- pub fn actions(mut self, actions: [NotificationAction<V>; 2]) -> Self {
- self.actions = Some(actions);
- self
- }
-
- pub fn meta(mut self, meta: NotificationMeta<V>) -> Self {
- self.meta = Some(meta);
- self
- }
-
- fn render_meta_items(&self, cx: &mut ViewContext<V>) -> impl Component<V> {
- if let Some(meta) = &self.meta {
- h_stack().children(
- meta.items
- .iter()
- .map(|(icon, text, _)| {
- let mut meta_el = div();
- if let Some(icon) = icon {
- meta_el = meta_el.child(IconElement::new(icon.clone()));
- }
- meta_el.child(Label::new(text.clone()).color(TextColor::Muted))
- })
- .collect::<Vec<_>>(),
- )
- } else {
- div()
- }
- }
-
- fn render_slot(&self, cx: &mut ViewContext<V>) -> impl Component<V> {
- match &self.slot {
- ActorOrIcon::Actor(actor) => Avatar::new(actor.avatar.clone()).render(),
- ActorOrIcon::Icon(icon) => IconElement::new(icon.clone()).render(),
- }
- }
-
- fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- div()
- .relative()
- .id(self.id.clone())
- .p_1()
- .flex()
- .flex_col()
- .w_full()
- .children(
- Some(
- div()
- .absolute()
- .left(px(3.0))
- .top_3()
- .z_index(2)
- .child(UnreadIndicator::new()),
- )
- .filter(|_| self.unread),
- )
- .child(
- v_stack()
- .z_index(1)
- .gap_1()
- .w_full()
- .child(
- h_stack()
- .w_full()
- .gap_2()
- .child(self.render_slot(cx))
- .child(div().flex_1().child(Label::new(self.message.clone()))),
- )
- .child(
- h_stack()
- .justify_between()
- .child(
- h_stack()
- .gap_1()
- .child(
- Label::new(naive_format_distance_from_now(
- self.date_received,
- true,
- true,
- ))
- .color(TextColor::Muted),
- )
- .child(self.render_meta_items(cx)),
- )
- .child(match (self.actions, self.action_taken) {
- // Show nothing
- (None, _) => div(),
- // Show the taken_message
- (Some(_), Some(action_taken)) => h_stack()
- .children(action_taken.taken_message.0.map(|icon| {
- IconElement::new(icon).color(crate::TextColor::Muted)
- }))
- .child(
- Label::new(action_taken.taken_message.1.clone())
- .color(TextColor::Muted),
- ),
- // Show the actions
- (Some(actions), None) => {
- h_stack().children(actions.map(|action| match action.button {
- ButtonOrIconButton::Button(button) => {
- Component::render(button)
- }
- ButtonOrIconButton::IconButton(icon_button) => {
- Component::render(icon_button)
- }
- }))
- }
- }),
- ),
- )
- }
-}
-
-use chrono::NaiveDateTime;
-use gpui::{px, Styled};
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use super::*;
- use crate::{Panel, Story};
- use gpui::{Div, Render};
-
- pub struct NotificationsPanelStory;
-
- impl Render for NotificationsPanelStory {
- type Element = Div<Self>;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- Story::container(cx)
- .child(Story::title_for::<_, NotificationsPanel>(cx))
- .child(Story::label(cx, "Default"))
- .child(
- Panel::new("panel", cx).child(NotificationsPanel::new("notifications_panel")),
- )
- }
- }
-}
@@ -1,128 +0,0 @@
-use gpui::{hsla, red, AnyElement, ElementId, ExternalPaths, Hsla, Length, Size, View};
-use smallvec::SmallVec;
-
-use crate::prelude::*;
-
-#[derive(Default, PartialEq)]
-pub enum SplitDirection {
- #[default]
- Horizontal,
- Vertical,
-}
-
-#[derive(Component)]
-pub struct Pane<V: 'static> {
- id: ElementId,
- size: Size<Length>,
- fill: Hsla,
- children: SmallVec<[AnyElement<V>; 2]>,
-}
-
-impl<V: 'static> Pane<V> {
- pub fn new(id: impl Into<ElementId>, size: Size<Length>) -> Self {
- // Fill is only here for debugging purposes, remove before release
-
- Self {
- id: id.into(),
- size,
- fill: hsla(0.3, 0.3, 0.3, 1.),
- children: SmallVec::new(),
- }
- }
-
- pub fn fill(mut self, fill: Hsla) -> Self {
- self.fill = fill;
- self
- }
-
- fn render(self, view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- div()
- .id(self.id.clone())
- .flex()
- .flex_initial()
- .bg(self.fill)
- .w(self.size.width)
- .h(self.size.height)
- .relative()
- .child(div().z_index(0).size_full().children(self.children))
- .child(
- div()
- .z_index(1)
- .id("drag-target")
- .drag_over::<ExternalPaths>(|d| d.bg(red()))
- .on_drop(|_, files: View<ExternalPaths>, cx| {
- eprintln!("dropped files! {:?}", files.read(cx));
- })
- .absolute()
- .inset_0(),
- )
- }
-}
-
-impl<V: 'static> ParentComponent<V> for Pane<V> {
- fn children_mut(&mut self) -> &mut SmallVec<[AnyElement<V>; 2]> {
- &mut self.children
- }
-}
-
-#[derive(Component)]
-pub struct PaneGroup<V: 'static> {
- groups: Vec<PaneGroup<V>>,
- panes: Vec<Pane<V>>,
- split_direction: SplitDirection,
-}
-
-impl<V: 'static> PaneGroup<V> {
- pub fn new_groups(groups: Vec<PaneGroup<V>>, split_direction: SplitDirection) -> Self {
- Self {
- groups,
- panes: Vec::new(),
- split_direction,
- }
- }
-
- pub fn new_panes(panes: Vec<Pane<V>>, split_direction: SplitDirection) -> Self {
- Self {
- groups: Vec::new(),
- panes,
- split_direction,
- }
- }
-
- fn render(self, view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- if !self.panes.is_empty() {
- let el = div()
- .flex()
- .flex_1()
- .gap_px()
- .w_full()
- .h_full()
- .children(self.panes.into_iter().map(|pane| pane.render(view, cx)));
-
- if self.split_direction == SplitDirection::Horizontal {
- return el;
- } else {
- return el.flex_col();
- }
- }
-
- if !self.groups.is_empty() {
- let el = div()
- .flex()
- .flex_1()
- .gap_px()
- .w_full()
- .h_full()
- .bg(cx.theme().colors().editor_background)
- .children(self.groups.into_iter().map(|group| group.render(view, cx)));
-
- if self.split_direction == SplitDirection::Horizontal {
- return el;
- } else {
- return el.flex_col();
- }
- }
-
- unreachable!()
- }
-}
@@ -1,75 +0,0 @@
-use crate::{
- prelude::*, static_project_panel_project_items, static_project_panel_single_items, Input, List,
- ListHeader,
-};
-use gpui::prelude::*;
-
-#[derive(Component)]
-pub struct ProjectPanel {
- id: ElementId,
-}
-
-impl ProjectPanel {
- pub fn new(id: impl Into<ElementId>) -> Self {
- Self { id: id.into() }
- }
-
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- div()
- .id(self.id.clone())
- .flex()
- .flex_col()
- .size_full()
- .bg(cx.theme().colors().surface_background)
- .child(
- div()
- .id("project-panel-contents")
- .w_full()
- .flex()
- .flex_col()
- .overflow_y_scroll()
- .child(
- List::new(static_project_panel_single_items())
- .header(ListHeader::new("FILES"))
- .empty_message("No files in directory"),
- )
- .child(
- List::new(static_project_panel_project_items())
- .header(ListHeader::new("PROJECT"))
- .empty_message("No folders in directory"),
- ),
- )
- .child(
- Input::new("Find something...")
- .value("buffe".to_string())
- .state(InteractionState::Focused),
- )
- }
-}
-
-use gpui::ElementId;
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use super::*;
- use crate::{Panel, Story};
- use gpui::{Div, Render};
-
- pub struct ProjectPanelStory;
-
- impl Render for ProjectPanelStory {
- type Element = Div<Self>;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- Story::container(cx)
- .child(Story::title_for::<_, ProjectPanel>(cx))
- .child(Story::label(cx, "Default"))
- .child(
- Panel::new("project-panel-outer", cx)
- .child(ProjectPanel::new("project-panel-inner")),
- )
- }
- }
-}
@@ -1,53 +0,0 @@
-use crate::prelude::*;
-use crate::{OrderMethod, Palette, PaletteItem};
-
-#[derive(Component)]
-pub struct RecentProjects {
- id: ElementId,
-}
-
-impl RecentProjects {
- pub fn new(id: impl Into<ElementId>) -> Self {
- Self { id: id.into() }
- }
-
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- div().id(self.id.clone()).child(
- Palette::new("palette")
- .items(vec![
- PaletteItem::new("zed").sublabel(SharedString::from("~/projects/zed")),
- PaletteItem::new("saga").sublabel(SharedString::from("~/projects/saga")),
- PaletteItem::new("journal").sublabel(SharedString::from("~/journal")),
- PaletteItem::new("dotfiles").sublabel(SharedString::from("~/dotfiles")),
- PaletteItem::new("zed.dev").sublabel(SharedString::from("~/projects/zed.dev")),
- PaletteItem::new("laminar").sublabel(SharedString::from("~/projects/laminar")),
- ])
- .placeholder("Recent Projects...")
- .empty_string("No matches")
- .default_order(OrderMethod::Ascending),
- )
- }
-}
-
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use super::*;
- use crate::Story;
- use gpui::{Div, Render};
-
- pub struct RecentProjectsStory;
-
- impl Render for RecentProjectsStory {
- type Element = Div<Self>;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- Story::container(cx)
- .child(Story::title_for::<_, RecentProjects>(cx))
- .child(Story::label(cx, "Default"))
- .child(RecentProjects::new("recent-projects"))
- }
- }
-}
@@ -1,203 +0,0 @@
-use std::sync::Arc;
-
-use crate::prelude::*;
-use crate::{Button, Icon, IconButton, TextColor, ToolDivider, Workspace};
-
-#[derive(Default, PartialEq)]
-pub enum Tool {
- #[default]
- ProjectPanel,
- CollaborationPanel,
- Terminal,
- Assistant,
- Feedback,
- Diagnostics,
-}
-
-struct ToolGroup {
- active_index: Option<usize>,
- tools: Vec<Tool>,
-}
-
-impl Default for ToolGroup {
- fn default() -> Self {
- ToolGroup {
- active_index: None,
- tools: vec![],
- }
- }
-}
-
-#[derive(Component)]
-#[component(view_type = "Workspace")]
-pub struct StatusBar {
- left_tools: Option<ToolGroup>,
- right_tools: Option<ToolGroup>,
- bottom_tools: Option<ToolGroup>,
-}
-
-impl StatusBar {
- pub fn new() -> Self {
- Self {
- left_tools: None,
- right_tools: None,
- bottom_tools: None,
- }
- }
-
- pub fn left_tool(mut self, tool: Tool, active_index: Option<usize>) -> Self {
- self.left_tools = {
- let mut tools = vec![tool];
- tools.extend(self.left_tools.take().unwrap_or_default().tools);
- Some(ToolGroup {
- active_index,
- tools,
- })
- };
- self
- }
-
- pub fn right_tool(mut self, tool: Tool, active_index: Option<usize>) -> Self {
- self.right_tools = {
- let mut tools = vec![tool];
- tools.extend(self.left_tools.take().unwrap_or_default().tools);
- Some(ToolGroup {
- active_index,
- tools,
- })
- };
- self
- }
-
- pub fn bottom_tool(mut self, tool: Tool, active_index: Option<usize>) -> Self {
- self.bottom_tools = {
- let mut tools = vec![tool];
- tools.extend(self.left_tools.take().unwrap_or_default().tools);
- Some(ToolGroup {
- active_index,
- tools,
- })
- };
- self
- }
-
- fn render(
- self,
- view: &mut Workspace,
- cx: &mut ViewContext<Workspace>,
- ) -> impl Component<Workspace> {
- div()
- .py_0p5()
- .px_1()
- .flex()
- .items_center()
- .justify_between()
- .w_full()
- .bg(cx.theme().colors().status_bar_background)
- .child(self.left_tools(view, cx))
- .child(self.right_tools(view, cx))
- }
-
- fn left_tools(
- &self,
- workspace: &mut Workspace,
- cx: &WindowContext,
- ) -> impl Component<Workspace> {
- div()
- .flex()
- .items_center()
- .gap_1()
- .child(
- IconButton::<Workspace>::new("project_panel", Icon::FileTree)
- .when(workspace.is_project_panel_open(), |this| {
- this.color(TextColor::Accent)
- })
- .on_click(|workspace: &mut Workspace, cx| {
- workspace.toggle_project_panel(cx);
- }),
- )
- .child(
- IconButton::<Workspace>::new("collab_panel", Icon::Hash)
- .when(workspace.is_collab_panel_open(), |this| {
- this.color(TextColor::Accent)
- })
- .on_click(|workspace: &mut Workspace, cx| {
- workspace.toggle_collab_panel();
- }),
- )
- .child(ToolDivider::new())
- .child(IconButton::new("diagnostics", Icon::XCircle))
- }
-
- fn right_tools(
- &self,
- workspace: &mut Workspace,
- cx: &WindowContext,
- ) -> impl Component<Workspace> {
- div()
- .flex()
- .items_center()
- .gap_2()
- .child(
- div()
- .flex()
- .items_center()
- .gap_1()
- .child(Button::new("116:25"))
- .child(
- Button::<Workspace>::new("Rust").on_click(Arc::new(|workspace, cx| {
- workspace.toggle_language_selector(cx);
- })),
- ),
- )
- .child(ToolDivider::new())
- .child(
- div()
- .flex()
- .items_center()
- .gap_1()
- .child(
- IconButton::new("copilot", Icon::Copilot)
- .on_click(|_, _| println!("Copilot clicked.")),
- )
- .child(
- IconButton::new("envelope", Icon::Envelope)
- .on_click(|_, _| println!("Send Feedback clicked.")),
- ),
- )
- .child(ToolDivider::new())
- .child(
- div()
- .flex()
- .items_center()
- .gap_1()
- .child(
- IconButton::<Workspace>::new("terminal", Icon::Terminal)
- .when(workspace.is_terminal_open(), |this| {
- this.color(TextColor::Accent)
- })
- .on_click(|workspace: &mut Workspace, cx| {
- workspace.toggle_terminal(cx);
- }),
- )
- .child(
- IconButton::<Workspace>::new("chat_panel", Icon::MessageBubbles)
- .when(workspace.is_chat_panel_open(), |this| {
- this.color(TextColor::Accent)
- })
- .on_click(|workspace: &mut Workspace, cx| {
- workspace.toggle_chat_panel(cx);
- }),
- )
- .child(
- IconButton::<Workspace>::new("assistant_panel", Icon::Ai)
- .when(workspace.is_assistant_panel_open(), |this| {
- this.color(TextColor::Accent)
- })
- .on_click(|workspace: &mut Workspace, cx| {
- workspace.toggle_assistant_panel(cx);
- }),
- ),
- )
- }
-}
@@ -1,150 +0,0 @@
-use crate::{prelude::*, Icon, IconButton, Tab};
-use gpui::prelude::*;
-
-#[derive(Component)]
-pub struct TabBar {
- id: ElementId,
- /// Backwards, Forwards
- can_navigate: (bool, bool),
- tabs: Vec<Tab>,
-}
-
-impl TabBar {
- pub fn new(id: impl Into<ElementId>, tabs: Vec<Tab>) -> Self {
- Self {
- id: id.into(),
- can_navigate: (false, false),
- tabs,
- }
- }
-
- pub fn can_navigate(mut self, can_navigate: (bool, bool)) -> Self {
- self.can_navigate = can_navigate;
- self
- }
-
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- let (can_navigate_back, can_navigate_forward) = self.can_navigate;
-
- div()
- .group("tab_bar")
- .id(self.id.clone())
- .w_full()
- .flex()
- .bg(cx.theme().colors().tab_bar_background)
- // Left Side
- .child(
- div()
- .relative()
- .px_1()
- .flex()
- .flex_none()
- .gap_2()
- // Nav Buttons
- .child(
- div()
- .right_0()
- .flex()
- .items_center()
- .gap_px()
- .child(
- IconButton::new("arrow_left", Icon::ArrowLeft)
- .state(InteractionState::Enabled.if_enabled(can_navigate_back)),
- )
- .child(
- IconButton::new("arrow_right", Icon::ArrowRight).state(
- InteractionState::Enabled.if_enabled(can_navigate_forward),
- ),
- ),
- ),
- )
- .child(
- div().w_0().flex_1().h_full().child(
- div()
- .id("tabs")
- .flex()
- .overflow_x_scroll()
- .children(self.tabs.clone()),
- ),
- )
- // Right Side
- .child(
- div()
- // We only use absolute here since we don't
- // have opacity or `hidden()` yet
- .absolute()
- .neg_top_7()
- .px_1()
- .flex()
- .flex_none()
- .gap_2()
- .group_hover("tab_bar", |this| this.top_0())
- // Nav Buttons
- .child(
- div()
- .flex()
- .items_center()
- .gap_px()
- .child(IconButton::new("plus", Icon::Plus))
- .child(IconButton::new("split", Icon::Split)),
- ),
- )
- }
-}
-
-use gpui::ElementId;
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use super::*;
- use crate::Story;
- use gpui::{Div, Render};
-
- pub struct TabBarStory;
-
- impl Render for TabBarStory {
- type Element = Div<Self>;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- Story::container(cx)
- .child(Story::title_for::<_, TabBar>(cx))
- .child(Story::label(cx, "Default"))
- .child(TabBar::new(
- "tab-bar",
- vec![
- Tab::new(1)
- .title("Cargo.toml".to_string())
- .current(false)
- .git_status(GitStatus::Modified),
- Tab::new(2)
- .title("Channels Panel".to_string())
- .current(false),
- Tab::new(3)
- .title("channels_panel.rs".to_string())
- .current(true)
- .git_status(GitStatus::Modified),
- Tab::new(4)
- .title("workspace.rs".to_string())
- .current(false)
- .git_status(GitStatus::Modified),
- Tab::new(5)
- .title("icon_button.rs".to_string())
- .current(false),
- Tab::new(6)
- .title("storybook.rs".to_string())
- .current(false)
- .git_status(GitStatus::Created),
- Tab::new(7).title("theme.rs".to_string()).current(false),
- Tab::new(8)
- .title("theme_registry.rs".to_string())
- .current(false),
- Tab::new(9)
- .title("styleable_helpers.rs".to_string())
- .current(false),
- ],
- ))
- }
- }
-}
@@ -1,99 +0,0 @@
-use gpui::{relative, rems, Size};
-
-use crate::prelude::*;
-use crate::{Icon, IconButton, Pane, Tab};
-
-#[derive(Component)]
-pub struct Terminal;
-
-impl Terminal {
- pub fn new() -> Self {
- Self
- }
-
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- let can_navigate_back = true;
- let can_navigate_forward = false;
-
- div()
- .flex()
- .flex_col()
- .w_full()
- .child(
- // Terminal Tabs.
- div()
- .w_full()
- .flex()
- .bg(cx.theme().colors().surface_background)
- .child(
- div().px_1().flex().flex_none().gap_2().child(
- div()
- .flex()
- .items_center()
- .gap_px()
- .child(
- IconButton::new("arrow_left", Icon::ArrowLeft).state(
- InteractionState::Enabled.if_enabled(can_navigate_back),
- ),
- )
- .child(IconButton::new("arrow_right", Icon::ArrowRight).state(
- InteractionState::Enabled.if_enabled(can_navigate_forward),
- )),
- ),
- )
- .child(
- div().w_0().flex_1().h_full().child(
- div()
- .flex()
- .child(
- Tab::new(1)
- .title("zed — fish".to_string())
- .icon(Icon::Terminal)
- .close_side(IconSide::Right)
- .current(true),
- )
- .child(
- Tab::new(2)
- .title("zed — fish".to_string())
- .icon(Icon::Terminal)
- .close_side(IconSide::Right)
- .current(false),
- ),
- ),
- ),
- )
- // Terminal Pane.
- .child(
- Pane::new(
- "terminal",
- Size {
- width: relative(1.).into(),
- height: rems(36.).into(),
- },
- )
- .child(crate::static_data::terminal_buffer(cx)),
- )
- }
-}
-
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use super::*;
- use crate::Story;
- use gpui::{Div, Render};
- pub struct TerminalStory;
-
- impl Render for TerminalStory {
- type Element = Div<Self>;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- Story::container(cx)
- .child(Story::title_for::<_, Terminal>(cx))
- .child(Story::label(cx, "Default"))
- .child(Terminal::new())
- }
- }
-}
@@ -1,60 +0,0 @@
-use crate::prelude::*;
-use crate::{OrderMethod, Palette, PaletteItem};
-
-#[derive(Component)]
-pub struct ThemeSelector {
- id: ElementId,
-}
-
-impl ThemeSelector {
- pub fn new(id: impl Into<ElementId>) -> Self {
- Self { id: id.into() }
- }
-
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- div().child(
- Palette::new(self.id.clone())
- .items(vec![
- PaletteItem::new("One Dark"),
- PaletteItem::new("Rosé Pine"),
- PaletteItem::new("Rosé Pine Moon"),
- PaletteItem::new("Sandcastle"),
- PaletteItem::new("Solarized Dark"),
- PaletteItem::new("Summercamp"),
- PaletteItem::new("Atelier Cave Light"),
- PaletteItem::new("Atelier Dune Light"),
- PaletteItem::new("Atelier Estuary Light"),
- PaletteItem::new("Atelier Forest Light"),
- PaletteItem::new("Atelier Heath Light"),
- ])
- .placeholder("Select Theme...")
- .empty_string("No matches")
- .default_order(OrderMethod::Ascending),
- )
- }
-}
-
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use gpui::{Div, Render};
-
- use crate::Story;
-
- use super::*;
-
- pub struct ThemeSelectorStory;
-
- impl Render for ThemeSelectorStory {
- type Element = Div<Self>;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- Story::container(cx)
- .child(Story::title_for::<_, ThemeSelector>(cx))
- .child(Story::label(cx, "Default"))
- .child(ThemeSelector::new("theme-selector"))
- }
- }
-}
@@ -1,218 +0,0 @@
-use std::sync::atomic::AtomicBool;
-use std::sync::Arc;
-
-use gpui::{Div, Render, View, VisualContext};
-
-use crate::prelude::*;
-use crate::settings::user_settings;
-use crate::{
- Avatar, Button, Icon, IconButton, MicStatus, PlayerStack, PlayerWithCallStatus,
- ScreenShareStatus, TextColor, ToolDivider, TrafficLights,
-};
-
-#[derive(Clone)]
-pub struct Livestream {
- pub players: Vec<PlayerWithCallStatus>,
- pub channel: Option<String>, // projects
- // windows
-}
-
-#[derive(Clone)]
-pub struct TitleBar {
- /// If the window is active from the OS's perspective.
- is_active: Arc<AtomicBool>,
- livestream: Option<Livestream>,
- mic_status: MicStatus,
- is_deafened: bool,
- screen_share_status: ScreenShareStatus,
-}
-
-impl TitleBar {
- pub fn new(cx: &mut ViewContext<Self>) -> Self {
- let is_active = Arc::new(AtomicBool::new(true));
- let active = is_active.clone();
-
- // cx.observe_window_activation(move |_, is_active, cx| {
- // active.store(is_active, std::sync::atomic::Ordering::SeqCst);
- // cx.notify();
- // })
- // .detach();
-
- Self {
- is_active,
- livestream: None,
- mic_status: MicStatus::Unmuted,
- is_deafened: false,
- screen_share_status: ScreenShareStatus::NotShared,
- }
- }
-
- pub fn set_livestream(mut self, livestream: Option<Livestream>) -> Self {
- self.livestream = livestream;
- self
- }
-
- pub fn is_mic_muted(&self) -> bool {
- self.mic_status == MicStatus::Muted
- }
-
- pub fn toggle_mic_status(&mut self, cx: &mut ViewContext<Self>) {
- self.mic_status = self.mic_status.inverse();
-
- // Undeafen yourself when unmuting the mic while deafened.
- if self.is_deafened && self.mic_status == MicStatus::Unmuted {
- self.is_deafened = false;
- }
-
- cx.notify();
- }
-
- pub fn toggle_deafened(&mut self, cx: &mut ViewContext<Self>) {
- self.is_deafened = !self.is_deafened;
- self.mic_status = MicStatus::Muted;
-
- cx.notify()
- }
-
- pub fn toggle_screen_share_status(&mut self, cx: &mut ViewContext<Self>) {
- self.screen_share_status = self.screen_share_status.inverse();
-
- cx.notify();
- }
-
- pub fn view(cx: &mut WindowContext, livestream: Option<Livestream>) -> View<Self> {
- cx.build_view(|cx| Self::new(cx).set_livestream(livestream))
- }
-}
-
-impl Render for TitleBar {
- type Element = Div<Self>;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Div<Self> {
- let settings = user_settings(cx);
-
- // let has_focus = cx.window_is_active();
- let has_focus = true;
-
- let player_list = if let Some(livestream) = &self.livestream {
- livestream.players.clone().into_iter()
- } else {
- vec![].into_iter()
- };
-
- div()
- .flex()
- .items_center()
- .justify_between()
- .w_full()
- .bg(cx.theme().colors().background)
- .py_1()
- .child(
- div()
- .flex()
- .items_center()
- .h_full()
- .gap_4()
- .px_2()
- .child(TrafficLights::new().window_has_focus(has_focus))
- // === Project Info === //
- .child(
- div()
- .flex()
- .items_center()
- .gap_1()
- .when(*settings.titlebar.show_project_owner, |this| {
- this.child(Button::new("iamnbutler"))
- })
- .child(Button::new("zed"))
- .child(Button::new("nate/gpui2-ui-components")),
- )
- .children(player_list.map(|p| PlayerStack::new(p)))
- .child(IconButton::new("plus", Icon::Plus)),
- )
- .child(
- div()
- .flex()
- .items_center()
- .child(
- div()
- .px_2()
- .flex()
- .items_center()
- .gap_1()
- .child(IconButton::new("folder_x", Icon::FolderX))
- .child(IconButton::new("exit", Icon::Exit)),
- )
- .child(ToolDivider::new())
- .child(
- div()
- .px_2()
- .flex()
- .items_center()
- .gap_1()
- .child(
- IconButton::<TitleBar>::new("toggle_mic_status", Icon::Mic)
- .when(self.is_mic_muted(), |this| this.color(TextColor::Error))
- .on_click(|title_bar: &mut TitleBar, cx| {
- title_bar.toggle_mic_status(cx)
- }),
- )
- .child(
- IconButton::<TitleBar>::new("toggle_deafened", Icon::AudioOn)
- .when(self.is_deafened, |this| this.color(TextColor::Error))
- .on_click(|title_bar: &mut TitleBar, cx| {
- title_bar.toggle_deafened(cx)
- }),
- )
- .child(
- IconButton::<TitleBar>::new("toggle_screen_share", Icon::Screen)
- .when(
- self.screen_share_status == ScreenShareStatus::Shared,
- |this| this.color(TextColor::Accent),
- )
- .on_click(|title_bar: &mut TitleBar, cx| {
- title_bar.toggle_screen_share_status(cx)
- }),
- ),
- )
- .child(
- div().px_2().flex().items_center().child(
- Avatar::new("https://avatars.githubusercontent.com/u/1714999?v=4")
- .shape(Shape::RoundedRectangle),
- ),
- ),
- )
- }
-}
-
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use super::*;
- use crate::Story;
-
- pub struct TitleBarStory {
- title_bar: View<TitleBar>,
- }
-
- impl TitleBarStory {
- pub fn view(cx: &mut WindowContext) -> View<Self> {
- cx.build_view(|cx| Self {
- title_bar: TitleBar::view(cx, None),
- })
- }
- }
-
- impl Render for TitleBarStory {
- type Element = Div<Self>;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Div<Self> {
- Story::container(cx)
- .child(Story::title_for::<_, TitleBar>(cx))
- .child(Story::label(cx, "Default"))
- .child(self.title_bar.clone())
- }
- }
-}
@@ -1,126 +0,0 @@
-use gpui::AnyElement;
-use smallvec::SmallVec;
-
-use crate::prelude::*;
-
-#[derive(Clone)]
-pub struct ToolbarItem {}
-
-#[derive(Component)]
-pub struct Toolbar<V: 'static> {
- left_items: SmallVec<[AnyElement<V>; 2]>,
- right_items: SmallVec<[AnyElement<V>; 2]>,
-}
-
-impl<V: 'static> Toolbar<V> {
- pub fn new() -> Self {
- Self {
- left_items: SmallVec::new(),
- right_items: SmallVec::new(),
- }
- }
-
- pub fn left_item(mut self, child: impl Component<V>) -> Self
- where
- Self: Sized,
- {
- self.left_items.push(child.render());
- self
- }
-
- pub fn left_items(mut self, iter: impl IntoIterator<Item = impl Component<V>>) -> Self
- where
- Self: Sized,
- {
- self.left_items
- .extend(iter.into_iter().map(|item| item.render()));
- self
- }
-
- pub fn right_item(mut self, child: impl Component<V>) -> Self
- where
- Self: Sized,
- {
- self.right_items.push(child.render());
- self
- }
-
- pub fn right_items(mut self, iter: impl IntoIterator<Item = impl Component<V>>) -> Self
- where
- Self: Sized,
- {
- self.right_items
- .extend(iter.into_iter().map(|item| item.render()));
- self
- }
-
- fn render(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- div()
- .bg(cx.theme().colors().toolbar_background)
- .p_2()
- .flex()
- .justify_between()
- .child(div().flex().children(self.left_items))
- .child(div().flex().children(self.right_items))
- }
-}
-
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use std::path::PathBuf;
- use std::str::FromStr;
-
- use gpui::{Div, Render};
-
- use crate::{Breadcrumb, HighlightedText, Icon, IconButton, Story, Symbol};
-
- use super::*;
-
- pub struct ToolbarStory;
-
- impl Render for ToolbarStory {
- type Element = Div<Self>;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- Story::container(cx)
- .child(Story::title_for::<_, Toolbar<Self>>(cx))
- .child(Story::label(cx, "Default"))
- .child(
- Toolbar::new()
- .left_item(Breadcrumb::new(
- PathBuf::from_str("crates/ui/src/components/toolbar.rs").unwrap(),
- vec![
- Symbol(vec![
- HighlightedText {
- text: "impl ".to_string(),
- color: cx.theme().syntax_color("keyword"),
- },
- HighlightedText {
- text: "ToolbarStory".to_string(),
- color: cx.theme().syntax_color("function"),
- },
- ]),
- Symbol(vec![
- HighlightedText {
- text: "fn ".to_string(),
- color: cx.theme().syntax_color("keyword"),
- },
- HighlightedText {
- text: "render".to_string(),
- color: cx.theme().syntax_color("function"),
- },
- ]),
- ],
- ))
- .right_items(vec![
- IconButton::new("toggle_inlay_hints", Icon::InlayHint),
- IconButton::new("buffer_search", Icon::MagnifyingGlass),
- IconButton::new("inline_assist", Icon::MagicWand),
- ]),
- )
- }
- }
-}
@@ -1,100 +0,0 @@
-use crate::prelude::*;
-
-#[derive(Clone, Copy)]
-enum TrafficLightColor {
- Red,
- Yellow,
- Green,
-}
-
-#[derive(Component)]
-struct TrafficLight {
- color: TrafficLightColor,
- window_has_focus: bool,
-}
-
-impl TrafficLight {
- fn new(color: TrafficLightColor, window_has_focus: bool) -> Self {
- Self {
- color,
- window_has_focus,
- }
- }
-
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- let system_colors = &cx.theme().styles.system;
-
- let fill = match (self.window_has_focus, self.color) {
- (true, TrafficLightColor::Red) => system_colors.mac_os_traffic_light_red,
- (true, TrafficLightColor::Yellow) => system_colors.mac_os_traffic_light_yellow,
- (true, TrafficLightColor::Green) => system_colors.mac_os_traffic_light_green,
- (false, _) => cx.theme().colors().element_background,
- };
-
- div().w_3().h_3().rounded_full().bg(fill)
- }
-}
-
-#[derive(Component)]
-pub struct TrafficLights {
- window_has_focus: bool,
-}
-
-impl TrafficLights {
- pub fn new() -> Self {
- Self {
- window_has_focus: true,
- }
- }
-
- pub fn window_has_focus(mut self, window_has_focus: bool) -> Self {
- self.window_has_focus = window_has_focus;
- self
- }
-
- fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
- div()
- .flex()
- .items_center()
- .gap_2()
- .child(TrafficLight::new(
- TrafficLightColor::Red,
- self.window_has_focus,
- ))
- .child(TrafficLight::new(
- TrafficLightColor::Yellow,
- self.window_has_focus,
- ))
- .child(TrafficLight::new(
- TrafficLightColor::Green,
- self.window_has_focus,
- ))
- }
-}
-
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use gpui::{Div, Render};
-
- use crate::Story;
-
- use super::*;
-
- pub struct TrafficLightsStory;
-
- impl Render for TrafficLightsStory {
- type Element = Div<Self>;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- Story::container(cx)
- .child(Story::title_for::<_, TrafficLights>(cx))
- .child(Story::label(cx, "Default"))
- .child(TrafficLights::new())
- .child(Story::label(cx, "Unfocused"))
- .child(TrafficLights::new().window_has_focus(false))
- }
- }
-}
@@ -1,398 +0,0 @@
-use std::sync::Arc;
-
-use chrono::DateTime;
-use gpui::{px, relative, Div, Render, Size, View, VisualContext};
-use settings2::Settings;
-use theme2::ThemeSettings;
-
-use crate::prelude::*;
-use crate::{
- static_livestream, v_stack, AssistantPanel, Button, ChatMessage, ChatPanel, Checkbox,
- CollabPanel, EditorPane, Label, LanguageSelector, NotificationsPanel, Pane, PaneGroup, Panel,
- PanelAllowedSides, PanelSide, ProjectPanel, SplitDirection, StatusBar, Terminal, TitleBar,
- Toast, ToastOrigin,
-};
-
-#[derive(Clone)]
-pub struct Gpui2UiDebug {
- pub in_livestream: bool,
- pub enable_user_settings: bool,
- pub show_toast: bool,
-}
-
-impl Default for Gpui2UiDebug {
- fn default() -> Self {
- Self {
- in_livestream: false,
- enable_user_settings: false,
- show_toast: false,
- }
- }
-}
-
-#[derive(Clone)]
-pub struct Workspace {
- title_bar: View<TitleBar>,
- editor_1: View<EditorPane>,
- show_project_panel: bool,
- show_collab_panel: bool,
- show_chat_panel: bool,
- show_assistant_panel: bool,
- show_notifications_panel: bool,
- show_terminal: bool,
- show_debug: bool,
- show_language_selector: bool,
- test_checkbox_selection: Selection,
- debug: Gpui2UiDebug,
-}
-
-impl Workspace {
- pub fn new(cx: &mut ViewContext<Self>) -> Self {
- Self {
- title_bar: TitleBar::view(cx, None),
- editor_1: EditorPane::view(cx),
- show_project_panel: true,
- show_collab_panel: false,
- show_chat_panel: false,
- show_assistant_panel: false,
- show_terminal: true,
- show_language_selector: false,
- show_debug: false,
- show_notifications_panel: true,
- test_checkbox_selection: Selection::Unselected,
- debug: Gpui2UiDebug::default(),
- }
- }
-
- pub fn is_project_panel_open(&self) -> bool {
- self.show_project_panel
- }
-
- pub fn toggle_project_panel(&mut self, cx: &mut ViewContext<Self>) {
- self.show_project_panel = !self.show_project_panel;
-
- self.show_collab_panel = false;
-
- cx.notify();
- }
-
- pub fn is_collab_panel_open(&self) -> bool {
- self.show_collab_panel
- }
-
- pub fn toggle_collab_panel(&mut self) {
- self.show_collab_panel = !self.show_collab_panel;
-
- self.show_project_panel = false;
- }
-
- pub fn is_terminal_open(&self) -> bool {
- self.show_terminal
- }
-
- pub fn toggle_terminal(&mut self, cx: &mut ViewContext<Self>) {
- self.show_terminal = !self.show_terminal;
-
- cx.notify();
- }
-
- pub fn is_chat_panel_open(&self) -> bool {
- self.show_chat_panel
- }
-
- pub fn toggle_chat_panel(&mut self, cx: &mut ViewContext<Self>) {
- self.show_chat_panel = !self.show_chat_panel;
-
- self.show_assistant_panel = false;
- self.show_notifications_panel = false;
-
- cx.notify();
- }
-
- pub fn is_notifications_panel_open(&self) -> bool {
- self.show_notifications_panel
- }
-
- pub fn toggle_notifications_panel(&mut self, cx: &mut ViewContext<Self>) {
- self.show_notifications_panel = !self.show_notifications_panel;
-
- self.show_chat_panel = false;
- self.show_assistant_panel = false;
-
- cx.notify();
- }
-
- pub fn is_assistant_panel_open(&self) -> bool {
- self.show_assistant_panel
- }
-
- pub fn toggle_assistant_panel(&mut self, cx: &mut ViewContext<Self>) {
- self.show_assistant_panel = !self.show_assistant_panel;
-
- self.show_chat_panel = false;
- self.show_notifications_panel = false;
-
- cx.notify();
- }
-
- pub fn is_language_selector_open(&self) -> bool {
- self.show_language_selector
- }
-
- pub fn toggle_language_selector(&mut self, cx: &mut ViewContext<Self>) {
- self.show_language_selector = !self.show_language_selector;
-
- cx.notify();
- }
-
- pub fn toggle_debug(&mut self, cx: &mut ViewContext<Self>) {
- self.show_debug = !self.show_debug;
-
- cx.notify();
- }
-
- pub fn debug_toggle_user_settings(&mut self, cx: &mut ViewContext<Self>) {
- self.debug.enable_user_settings = !self.debug.enable_user_settings;
-
- let mut theme_settings = ThemeSettings::get_global(cx).clone();
-
- if self.debug.enable_user_settings {
- theme_settings.ui_font_size = 18.0.into();
- } else {
- theme_settings.ui_font_size = 16.0.into();
- }
-
- ThemeSettings::override_global(theme_settings.clone(), cx);
-
- cx.set_rem_size(theme_settings.ui_font_size);
-
- cx.notify();
- }
-
- pub fn debug_toggle_livestream(&mut self, cx: &mut ViewContext<Self>) {
- self.debug.in_livestream = !self.debug.in_livestream;
-
- self.title_bar = TitleBar::view(
- cx,
- Some(static_livestream()).filter(|_| self.debug.in_livestream),
- );
-
- cx.notify();
- }
-
- pub fn debug_toggle_toast(&mut self, cx: &mut ViewContext<Self>) {
- self.debug.show_toast = !self.debug.show_toast;
-
- cx.notify();
- }
-
- pub fn view(cx: &mut WindowContext) -> View<Self> {
- cx.build_view(|cx| Self::new(cx))
- }
-}
-
-impl Render for Workspace {
- type Element = Div<Self>;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Div<Self> {
- let root_group = PaneGroup::new_panes(
- vec![Pane::new(
- "pane-0",
- Size {
- width: relative(1.).into(),
- height: relative(1.).into(),
- },
- )
- .child(self.editor_1.clone())],
- SplitDirection::Horizontal,
- );
- let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone();
-
- div()
- .relative()
- .size_full()
- .flex()
- .flex_col()
- .font(ui_font)
- .gap_0()
- .justify_start()
- .items_start()
- .text_color(cx.theme().colors().text)
- .bg(cx.theme().colors().background)
- .child(self.title_bar.clone())
- .child(
- div()
- .absolute()
- .top_12()
- .left_12()
- .z_index(99)
- .bg(cx.theme().colors().background)
- .child(
- Checkbox::new("test_checkbox", self.test_checkbox_selection).on_click(
- |selection, workspace: &mut Workspace, cx| {
- workspace.test_checkbox_selection = selection;
-
- cx.notify();
- },
- ),
- ),
- )
- .child(
- div()
- .flex_1()
- .w_full()
- .flex()
- .flex_row()
- .overflow_hidden()
- .border_t()
- .border_b()
- .border_color(cx.theme().colors().border)
- .children(
- Some(
- Panel::new("project-panel-outer", cx)
- .side(PanelSide::Left)
- .child(ProjectPanel::new("project-panel-inner")),
- )
- .filter(|_| self.is_project_panel_open()),
- )
- .children(
- Some(
- Panel::new("collab-panel-outer", cx)
- .child(CollabPanel::new("collab-panel-inner"))
- .side(PanelSide::Left),
- )
- .filter(|_| self.is_collab_panel_open()),
- )
- // .child(NotificationToast::new(
- // "maxbrunsfeld has requested to add you as a contact.".into(),
- // ))
- .child(
- v_stack()
- .flex_1()
- .h_full()
- .child(div().flex().flex_1().child(root_group))
- .children(
- Some(
- Panel::new("terminal-panel", cx)
- .child(Terminal::new())
- .allowed_sides(PanelAllowedSides::BottomOnly)
- .side(PanelSide::Bottom),
- )
- .filter(|_| self.is_terminal_open()),
- ),
- )
- .children(
- Some(
- Panel::new("chat-panel-outer", cx)
- .side(PanelSide::Right)
- .child(ChatPanel::new("chat-panel-inner").messages(vec![
- ChatMessage::new(
- "osiewicz".to_string(),
- "is this thing on?".to_string(),
- DateTime::parse_from_rfc3339("2023-09-27T15:40:52.707Z")
- .unwrap()
- .naive_local(),
- ),
- ChatMessage::new(
- "maxdeviant".to_string(),
- "Reading you loud and clear!".to_string(),
- DateTime::parse_from_rfc3339("2023-09-28T15:40:52.707Z")
- .unwrap()
- .naive_local(),
- ),
- ])),
- )
- .filter(|_| self.is_chat_panel_open()),
- )
- .children(
- Some(
- Panel::new("notifications-panel-outer", cx)
- .side(PanelSide::Right)
- .child(NotificationsPanel::new("notifications-panel-inner")),
- )
- .filter(|_| self.is_notifications_panel_open()),
- )
- .children(
- Some(
- Panel::new("assistant-panel-outer", cx)
- .child(AssistantPanel::new("assistant-panel-inner")),
- )
- .filter(|_| self.is_assistant_panel_open()),
- ),
- )
- .child(StatusBar::new())
- .when(self.debug.show_toast, |this| {
- this.child(Toast::new(ToastOrigin::Bottom).child(Label::new("A toast")))
- })
- .children(
- Some(
- div()
- .absolute()
- .top(px(50.))
- .left(px(640.))
- .z_index(8)
- .child(LanguageSelector::new("language-selector")),
- )
- .filter(|_| self.is_language_selector_open()),
- )
- .z_index(8)
- // Debug
- .child(
- v_stack()
- .z_index(9)
- .absolute()
- .top_20()
- .left_1_4()
- .w_40()
- .gap_2()
- .when(self.show_debug, |this| {
- this.child(Button::<Workspace>::new("Toggle User Settings").on_click(
- Arc::new(|workspace, cx| workspace.debug_toggle_user_settings(cx)),
- ))
- .child(
- Button::<Workspace>::new("Toggle Toasts").on_click(Arc::new(
- |workspace, cx| workspace.debug_toggle_toast(cx),
- )),
- )
- .child(
- Button::<Workspace>::new("Toggle Livestream").on_click(Arc::new(
- |workspace, cx| workspace.debug_toggle_livestream(cx),
- )),
- )
- })
- .child(
- Button::<Workspace>::new("Toggle Debug")
- .on_click(Arc::new(|workspace, cx| workspace.toggle_debug(cx))),
- ),
- )
- }
-}
-
-#[cfg(feature = "stories")]
-pub use stories::*;
-
-#[cfg(feature = "stories")]
-mod stories {
- use super::*;
- use gpui::VisualContext;
-
- pub struct WorkspaceStory {
- workspace: View<Workspace>,
- }
-
- impl WorkspaceStory {
- pub fn view(cx: &mut WindowContext) -> View<Self> {
- cx.build_view(|cx| Self {
- workspace: Workspace::view(cx),
- })
- }
- }
-
- impl Render for WorkspaceStory {
- type Element = Div<Self>;
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- div().child(self.workspace.clone())
- }
- }
-}
@@ -15,36 +15,12 @@
#![allow(dead_code, unused_variables)]
mod components;
-mod elevation;
pub mod prelude;
-pub mod settings;
-mod static_data;
mod styled_ext;
-mod to_extract;
+mod styles;
pub mod utils;
pub use components::*;
-use gpui::actions;
pub use prelude::*;
-pub use static_data::*;
pub use styled_ext::*;
-pub use to_extract::*;
-
-// This needs to be fully qualified with `crate::` otherwise we get a panic
-// at:
-// thread '<unnamed>' panicked at crates/gpui2/src/platform/mac/platform.rs:66:81:
-// called `Option::unwrap()` on a `None` value
-//
-// AFAICT this is something to do with conflicting names between crates and modules that
-// interfaces with declaring the `ClassDecl`.
-pub use crate::settings::*;
-
-#[cfg(feature = "stories")]
-mod story;
-#[cfg(feature = "stories")]
-pub use story::*;
-actions!(NoAction);
-
-pub fn binding(key: &str) -> gpui::KeyBinding {
- gpui::KeyBinding::new(key, NoAction {}, None)
-}
+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, Component, Div, Entity, EntityId,
- EventEmitter, FocusHandle, FocusableView, ParentComponent, Render, SharedString, Styled,
+ div, px, Action, AnchorCorner, AnyView, AppContext, Div, Entity, EntityId, EventEmitter,
+ 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, ListEntry, Tooltip,
-};
+use ui::{h_stack, menu_handle, ContextMenu, IconButton, InteractionState, Tooltip};
pub enum PanelEvent {
ChangePosition,
@@ -477,7 +475,7 @@ impl Dock {
}
impl Render for Dock {
- type Element = Div<Self>;
+ type Element = Div;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
if let Some(entry) = self.visible_entry() {
@@ -663,7 +661,7 @@ impl PanelButtons {
// here be kittens
impl Render for PanelButtons {
- type Element = Div<Self>;
+ type Element = Div;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
// todo!()
@@ -688,46 +686,40 @@ impl Render for PanelButtons {
let name = entry.panel.persistent_name();
let panel = entry.panel.clone();
- let mut button: IconButton<Self> = if i == active_index && is_open {
+ let mut button: IconButton = if i == active_index && is_open {
let action = dock.toggle_action();
let tooltip: SharedString =
format!("Close {} dock", dock.position.to_label()).into();
IconButton::new(name, icon)
.state(InteractionState::Active)
.action(action.boxed_clone())
- .tooltip(move |_, cx| Tooltip::for_action(tooltip.clone(), &*action, cx))
+ .tooltip(move |cx| Tooltip::for_action(tooltip.clone(), &*action, cx))
} else {
let action = entry.panel.toggle_action(cx);
IconButton::new(name, icon)
.action(action.boxed_clone())
- .tooltip(move |_, cx| Tooltip::for_action(name, &*action, cx))
+ .tooltip(move |cx| Tooltip::for_action(name, &*action, cx))
};
Some(
- menu_handle()
- .id(name)
- .menu(move |_, cx| {
+ menu_handle(name)
+ .menu(move |cx| {
const POSITIONS: [DockPosition; 3] = [
DockPosition::Left,
DockPosition::Right,
DockPosition::Bottom,
];
+
ContextMenu::build(cx, |mut menu, cx| {
for position in POSITIONS {
if position != dock_position
&& panel.position_is_valid(position, cx)
{
let panel = panel.clone();
- menu = menu.entry(
- ListEntry::new(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
@@ -782,7 +774,7 @@ pub mod test {
}
impl Render for TestPanel {
- type Element = Div<Self>;
+ type Element = Div;
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
div()
@@ -104,7 +104,7 @@ pub trait Item: FocusableView + EventEmitter<ItemEvent> {
fn tab_description(&self, _: usize, _: &AppContext) -> Option<SharedString> {
None
}
- fn tab_content<V: 'static>(&self, detail: Option<usize>, cx: &AppContext) -> AnyElement<V>;
+ fn tab_content(&self, detail: Option<usize>, cx: &WindowContext) -> AnyElement;
/// (model id, Item)
fn for_each_project_item(
@@ -214,8 +214,8 @@ pub trait ItemHandle: 'static + Send {
) -> gpui::Subscription;
fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString>;
fn tab_description(&self, detail: usize, cx: &AppContext) -> Option<SharedString>;
- fn tab_content(&self, detail: Option<usize>, cx: &AppContext) -> AnyElement<Pane>;
- fn dragged_tab_content(&self, detail: Option<usize>, cx: &AppContext) -> AnyElement<Workspace>;
+ fn tab_content(&self, detail: Option<usize>, cx: &WindowContext) -> AnyElement;
+ fn dragged_tab_content(&self, detail: Option<usize>, cx: &WindowContext) -> AnyElement;
fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]>;
fn project_item_model_ids(&self, cx: &AppContext) -> SmallVec<[EntityId; 3]>;
@@ -307,11 +307,11 @@ impl<T: Item> ItemHandle for View<T> {
self.read(cx).tab_description(detail, cx)
}
- fn tab_content(&self, detail: Option<usize>, cx: &AppContext) -> AnyElement<Pane> {
+ fn tab_content(&self, detail: Option<usize>, cx: &WindowContext) -> AnyElement {
self.read(cx).tab_content(detail, cx)
}
- fn dragged_tab_content(&self, detail: Option<usize>, cx: &AppContext) -> AnyElement<Workspace> {
+ fn dragged_tab_content(&self, detail: Option<usize>, cx: &WindowContext) -> AnyElement {
self.read(cx).tab_content(detail, cx)
}
@@ -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();
}
@@ -72,7 +73,7 @@ impl ModalLayer {
}
impl Render for ModalLayer {
- type Element = Div<Self>;
+ type Element = Div;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
let Some(active_modal) = &self.active_modal else {
@@ -97,11 +98,11 @@ impl Render for ModalLayer {
h_stack()
// needed to prevent mouse events leaking to the
// UI below. // todo! for gpui3.
- .on_any_mouse_down(|_, _, cx| cx.stop_propagation())
- .on_any_mouse_up(|_, _, cx| cx.stop_propagation())
- .on_mouse_down_out(|this: &mut Self, event, cx| {
+ .on_any_mouse_down(|_, cx| cx.stop_propagation())
+ .on_any_mouse_up(|_, cx| cx.stop_propagation())
+ .on_mouse_down_out(cx.listener(|this, _, cx| {
this.hide_modal(cx);
- })
+ }))
.child(active_modal.modal.clone()),
),
)
@@ -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>),
- Element(fn(TextStyle, &AppContext) -> AnyElement<MessageNotification>),
+ 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>,
- ) -> 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<Self>;
+ 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
}
}
@@ -7,9 +7,9 @@ use crate::{
use anyhow::Result;
use collections::{HashMap, HashSet, VecDeque};
use gpui::{
- actions, prelude::*, Action, AppContext, AsyncWindowContext, Component, Div, EntityId,
- EventEmitter, FocusHandle, Focusable, FocusableView, Model, Pixels, Point, PromptLevel, Render,
- Task, View, ViewContext, VisualContext, WeakView, WindowContext,
+ actions, prelude::*, Action, AppContext, AsyncWindowContext, Div, EntityId, EventEmitter,
+ FocusHandle, Focusable, FocusableView, Model, Pixels, Point, PromptLevel, Render, Task, View,
+ ViewContext, VisualContext, WeakView, WindowContext,
};
use parking_lot::Mutex;
use project2::{Project, ProjectEntryId, ProjectPath};
@@ -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,21 +1344,23 @@ impl Pane {
item: &Box<dyn ItemHandle>,
detail: usize,
cx: &mut ViewContext<'_, Pane>,
- ) -> impl Component<Self> {
+ ) -> 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(IconButton::new("close_tab", Icon::Close).on_click(
- move |pane: &mut Self, cx| {
- pane.close_item_by_id(id, SaveIntent::Close, cx)
- .detach_and_log_err(cx);
- },
- ))
+ .child(
+ IconButton::new("close_tab", Icon::Close).on_click(cx.listener(
+ move |pane, _, cx| {
+ pane.close_item_by_id(id, SaveIntent::Close, cx)
+ .detach_and_log_err(cx);
+ },
+ )),
+ )
};
let (text_color, tab_bg, tab_hover_bg, tab_active_bg) = match ix == self.active_item_index {
@@ -1377,15 +1379,16 @@ 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())
+ div.tooltip(move |cx| cx.build_view(|cx| Tooltip::new(text.clone())).into())
})
- .on_click(move |v: &mut Self, e, cx| v.activate_item(ix, true, true, cx))
+ .on_click(cx.listener(move |v: &mut Self, e, cx| v.activate_item(ix, true, true, cx)))
// .on_drag(move |pane, cx| pane.render_tab(ix, item.boxed_clone(), detail, cx))
// .drag_over::<DraggedTab>(|d| d.bg(cx.theme().colors().element_drop_target))
// .on_drop(|_view, state: View<DraggedTab>, cx| {
@@ -1405,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))
@@ -1421,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()))
@@ -1437,7 +1458,7 @@ impl Pane {
)
}
- fn render_tab_bar(&mut self, cx: &mut ViewContext<'_, Pane>) -> impl Component<Self> {
+ fn render_tab_bar(&mut self, cx: &mut ViewContext<'_, Pane>) -> impl IntoElement {
div()
.group("tab_bar")
.id("tab_bar")
@@ -1459,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(
@@ -1491,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)),
+ ),
),
)
}
@@ -1892,16 +1933,24 @@ impl FocusableView for Pane {
}
impl Render for Pane {
- type Element = Focusable<Self, Div<Self>>;
+ type Element = Focusable<Div>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
v_stack()
.key_context("Pane")
.track_focus(&self.focus_handle)
- .on_action(|pane: &mut Pane, _: &SplitLeft, cx| pane.split(SplitDirection::Left, cx))
- .on_action(|pane: &mut Pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx))
- .on_action(|pane: &mut Pane, _: &SplitRight, cx| pane.split(SplitDirection::Right, cx))
- .on_action(|pane: &mut Pane, _: &SplitDown, cx| pane.split(SplitDirection::Down, cx))
+ .on_action(cx.listener(|pane: &mut Pane, _: &SplitLeft, cx| {
+ pane.split(SplitDirection::Left, cx)
+ }))
+ .on_action(
+ cx.listener(|pane: &mut Pane, _: &SplitUp, cx| pane.split(SplitDirection::Up, cx)),
+ )
+ .on_action(cx.listener(|pane: &mut Pane, _: &SplitRight, cx| {
+ pane.split(SplitDirection::Right, cx)
+ }))
+ .on_action(cx.listener(|pane: &mut Pane, _: &SplitDown, cx| {
+ pane.split(SplitDirection::Down, cx)
+ }))
// cx.add_action(Pane::toggle_zoom);
// cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| {
// pane.activate_item(action.0, true, true, cx);
@@ -1922,14 +1971,14 @@ impl Render for Pane {
// cx.add_async_action(Pane::close_items_to_the_right);
// cx.add_async_action(Pane::close_all_items);
.size_full()
- .on_action(|pane: &mut Self, action: &CloseActiveItem, cx| {
- pane.close_active_item(action, cx)
- .map(|task| task.detach_and_log_err(cx));
- })
+ .on_action(
+ cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| {
+ pane.close_active_item(action, cx)
+ .map(|task| task.detach_and_log_err(cx));
+ }),
+ )
.child(self.render_tab_bar(cx))
- // .child(
- // div()
- // ) /* todo!(toolbar) */
+ .child(self.toolbar.clone())
.child(if let Some(item) = self.active_item() {
div().flex().flex_1().child(item.to_any())
} else {
@@ -2948,7 +2997,7 @@ struct DraggedTab {
}
impl Render for DraggedTab {
- type Element = Div<Self>;
+ type Element = Div;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
div().w_8().h_4().bg(gpui::red())
@@ -1,12 +1,13 @@
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, AnyElement, AnyWeakView, Bounds, Model, Pixels, Point, View, ViewContext};
+use gpui::{
+ point, size, AnyWeakView, Bounds, Div, IntoElement, Model, Pixels, Point, View, ViewContext,
+};
use parking_lot::Mutex;
use project2::Project;
use serde::Deserialize;
@@ -125,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 Component<Workspace> {
+ ) -> impl IntoElement {
self.root.render(
project,
0,
follower_states,
- active_call,
active_pane,
zoomed,
app_state,
@@ -197,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 Component<Workspace> {
+ ) -> impl IntoElement {
match self {
Member::Pane(pane) => {
// todo!()
@@ -212,7 +210,7 @@ impl Member {
// Some(pane)
// };
- div().size_full().child(pane.clone()).render()
+ div().size_full().child(pane.clone()).into_any()
// Stack::new()
// .with_child(pane_element.contained().with_border(leader_border))
@@ -228,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 {}
@@ -554,12 +553,11 @@ 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>,
cx: &mut ViewContext<Workspace>,
- ) -> AnyElement<Workspace> {
+ ) -> Div {
debug_assert!(self.members.len() == self.flexes.lock().len());
div()
@@ -576,17 +574,15 @@ impl PaneAxis {
project,
basis,
follower_states,
- active_call,
active_pane,
zoomed,
app_state,
cx,
)
- .render(),
- Member::Pane(pane) => pane.clone().render(),
+ .into_any_element(),
+ Member::Pane(pane) => pane.clone().into_any_element(),
}
}))
- .render()
// let mut pane_axis = PaneAxisElement::new(
// self.axis,
@@ -1,7 +1,8 @@
use std::{any::Any, sync::Arc};
use gpui::{
- AnyView, AppContext, EventEmitter, Subscription, Task, View, ViewContext, WindowContext,
+ AnyView, AppContext, EventEmitter, Subscription, Task, View, ViewContext, WeakView,
+ WindowContext,
};
use project2::search::SearchQuery;
@@ -129,8 +130,7 @@ pub trait SearchableItemHandle: ItemHandle {
// todo!("here is where we need to use AnyWeakView");
impl<T: SearchableItem> SearchableItemHandle for View<T> {
fn downgrade(&self) -> Box<dyn WeakSearchableItemHandle> {
- // Box::new(self.downgrade())
- todo!()
+ Box::new(self.downgrade())
}
fn boxed_clone(&self) -> Box<dyn SearchableItemHandle> {
@@ -252,16 +252,15 @@ pub trait WeakSearchableItemHandle: WeakItemHandle {
// fn into_any(self) -> AnyWeakView;
}
-// todo!()
-// impl<T: SearchableItem> WeakSearchableItemHandle for WeakView<T> {
-// fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn SearchableItemHandle>> {
-// Some(Box::new(self.upgrade(cx)?))
-// }
+impl<T: SearchableItem> WeakSearchableItemHandle for WeakView<T> {
+ fn upgrade(&self, cx: &AppContext) -> Option<Box<dyn SearchableItemHandle>> {
+ Some(Box::new(self.upgrade()?))
+ }
-// // fn into_any(self) -> AnyView {
-// // self.into_any()
-// // }
-// }
+ // fn into_any(self) -> AnyView {
+ // self.into_any()
+ // }
+}
impl PartialEq for Box<dyn WeakSearchableItemHandle> {
fn eq(&self, other: &Self) -> bool {
@@ -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, Component, Div, ParentComponent, Render, 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 {
@@ -35,7 +35,7 @@ pub struct StatusBar {
}
impl Render for StatusBar {
- type Element = Div<Self>;
+ type Element = Div;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
div()
@@ -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 Component<Self> {
+ 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 Component<Self> {
+ fn render_right_tools(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
h_stack()
.items_center()
.gap_2()
@@ -1,7 +1,10 @@
use crate::ItemHandle;
use gpui::{
- AnyView, Div, Entity, EntityId, EventEmitter, Render, View, ViewContext, WindowContext,
+ div, AnyView, Div, Entity, EntityId, EventEmitter, ParentElement as _, Render, Styled, View,
+ ViewContext, WindowContext,
};
+use theme2::ActiveTheme;
+use ui::{h_stack, v_stack, Button, Color, Icon, IconButton, Label};
pub enum ToolbarItemEvent {
ChangeLocation(ToolbarItemLocation),
@@ -39,8 +42,8 @@ trait ToolbarItemViewHandle: Send {
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum ToolbarItemLocation {
Hidden,
- PrimaryLeft { flex: Option<(f32, bool)> },
- PrimaryRight { flex: Option<(f32, bool)> },
+ PrimaryLeft,
+ PrimaryRight,
Secondary,
}
@@ -51,11 +54,69 @@ pub struct Toolbar {
items: Vec<(Box<dyn ToolbarItemViewHandle>, ToolbarItemLocation)>,
}
+impl Toolbar {
+ fn left_items(&self) -> impl Iterator<Item = &dyn ToolbarItemViewHandle> {
+ self.items.iter().filter_map(|(item, location)| {
+ if *location == ToolbarItemLocation::PrimaryLeft {
+ Some(item.as_ref())
+ } else {
+ None
+ }
+ })
+ }
+
+ fn right_items(&self) -> impl Iterator<Item = &dyn ToolbarItemViewHandle> {
+ self.items.iter().filter_map(|(item, location)| {
+ if *location == ToolbarItemLocation::PrimaryRight {
+ Some(item.as_ref())
+ } else {
+ None
+ }
+ })
+ }
+}
+
impl Render for Toolbar {
- type Element = Div<Self>;
+ type Element = Div;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
- todo!()
+ //dbg!(&self.items.len());
+ v_stack()
+ .border_b()
+ .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(Color::Muted))
+ .child(Button::new("workspace2")),
+ )
+ // Toolbar right side
+ .child(
+ h_stack()
+ .p_1()
+ .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};
@@ -31,9 +30,9 @@ use futures::{
use gpui::{
actions, div, point, size, Action, AnyModel, AnyView, AnyWeakView, AppContext, AsyncAppContext,
AsyncWindowContext, Bounds, Context, Div, Entity, EntityId, EventEmitter, FocusHandle,
- FocusableView, GlobalPixels, InteractiveComponent, KeyContext, ManagedView, Model,
- ModelContext, ParentComponent, PathPromptOptions, Point, PromptLevel, Render, Size, Styled,
- Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowBounds, WindowContext,
+ FocusableView, GlobalPixels, InteractiveElement, KeyContext, ManagedView, Model, ModelContext,
+ ParentElement, PathPromptOptions, Point, PromptLevel, Render, Size, Styled, Subscription, Task,
+ View, ViewContext, VisualContext, WeakModel, WeakView, WindowBounds, WindowContext,
WindowHandle, WindowOptions,
};
use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem};
@@ -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,10 +466,39 @@ 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>,
- workspace_actions: Vec<Box<dyn Fn(Div<Workspace>) -> Div<Workspace>>>,
+ workspace_actions: Vec<Box<dyn Fn(Div, &mut ViewContext<Self>) -> Div>>,
zoomed: Option<AnyWeakView>,
zoomed_position: Option<DockPosition>,
center: PaneGroup,
@@ -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
}
@@ -3204,53 +3276,63 @@ impl Workspace {
})
}
- fn actions(&self, div: Div<Self>) -> Div<Self> {
- self.add_workspace_actions_listeners(div)
+ fn actions(&self, div: Div, cx: &mut ViewContext<Self>) -> Div {
+ self.add_workspace_actions_listeners(div, cx)
// cx.add_async_action(Workspace::open);
// cx.add_async_action(Workspace::follow_next_collaborator);
// cx.add_async_action(Workspace::close);
- .on_action(Self::close_inactive_items_and_panes)
- .on_action(Self::close_all_items_and_panes)
+ .on_action(cx.listener(Self::close_inactive_items_and_panes))
+ .on_action(cx.listener(Self::close_all_items_and_panes))
// cx.add_global_action(Workspace::close_global);
// cx.add_global_action(restart);
- .on_action(Self::save_all)
- .on_action(Self::add_folder_to_project)
- .on_action(|workspace, _: &Unfollow, cx| {
+ .on_action(cx.listener(Self::save_all))
+ .on_action(cx.listener(Self::add_folder_to_project))
+ .on_action(cx.listener(|workspace, _: &Unfollow, cx| {
let pane = workspace.active_pane().clone();
workspace.unfollow(&pane, cx);
- })
- .on_action(|workspace, action: &Save, cx| {
+ }))
+ .on_action(cx.listener(|workspace, action: &Save, cx| {
workspace
.save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), cx)
.detach_and_log_err(cx);
- })
- .on_action(|workspace, _: &SaveAs, cx| {
+ }))
+ .on_action(cx.listener(|workspace, _: &SaveAs, cx| {
workspace
.save_active_item(SaveIntent::SaveAs, cx)
.detach_and_log_err(cx);
- })
- .on_action(|workspace, _: &ActivatePreviousPane, cx| {
+ }))
+ .on_action(cx.listener(|workspace, _: &ActivatePreviousPane, cx| {
workspace.activate_previous_pane(cx)
- })
- .on_action(|workspace, _: &ActivateNextPane, cx| workspace.activate_next_pane(cx))
- .on_action(|workspace, action: &ActivatePaneInDirection, cx| {
- workspace.activate_pane_in_direction(action.0, cx)
- })
- .on_action(|workspace, action: &SwapPaneInDirection, cx| {
+ }))
+ .on_action(
+ cx.listener(|workspace, _: &ActivateNextPane, cx| workspace.activate_next_pane(cx)),
+ )
+ .on_action(
+ cx.listener(|workspace, action: &ActivatePaneInDirection, cx| {
+ workspace.activate_pane_in_direction(action.0, cx)
+ }),
+ )
+ .on_action(cx.listener(|workspace, action: &SwapPaneInDirection, cx| {
workspace.swap_pane_in_direction(action.0, cx)
- })
- .on_action(|this, e: &ToggleLeftDock, cx| {
+ }))
+ .on_action(cx.listener(|this, e: &ToggleLeftDock, cx| {
this.toggle_dock(DockPosition::Left, cx);
- })
- .on_action(|workspace: &mut Workspace, _: &ToggleRightDock, cx| {
- workspace.toggle_dock(DockPosition::Right, cx);
- })
- .on_action(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| {
- workspace.toggle_dock(DockPosition::Bottom, cx);
- })
- .on_action(|workspace: &mut Workspace, _: &CloseAllDocks, cx| {
- workspace.close_all_docks(cx);
- })
+ }))
+ .on_action(
+ cx.listener(|workspace: &mut Workspace, _: &ToggleRightDock, cx| {
+ workspace.toggle_dock(DockPosition::Right, cx);
+ }),
+ )
+ .on_action(
+ cx.listener(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| {
+ workspace.toggle_dock(DockPosition::Bottom, cx);
+ }),
+ )
+ .on_action(
+ cx.listener(|workspace: &mut Workspace, _: &CloseAllDocks, cx| {
+ workspace.close_all_docks(cx);
+ }),
+ )
// cx.add_action(Workspace::activate_pane_at_index);
// cx.add_action(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| {
// workspace.reopen_closed_item(cx).detach();
@@ -3304,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));
@@ -3346,22 +3429,24 @@ impl Workspace {
) -> &mut Self {
let callback = Arc::new(callback);
- self.workspace_actions.push(Box::new(move |div| {
+ self.workspace_actions.push(Box::new(move |div, cx| {
let callback = callback.clone();
- div.on_action(move |workspace, event, cx| (callback.clone())(workspace, event, cx))
+ div.on_action(
+ cx.listener(move |workspace, event, cx| (callback.clone())(workspace, event, cx)),
+ )
}));
self
}
- fn add_workspace_actions_listeners(&self, mut div: Div<Workspace>) -> Div<Workspace> {
+ fn add_workspace_actions_listeners(&self, mut div: Div, cx: &mut ViewContext<Self>) -> Div {
let mut div = div
- .on_action(Self::close_inactive_items_and_panes)
- .on_action(Self::close_all_items_and_panes)
- .on_action(Self::add_folder_to_project)
- .on_action(Self::save_all)
- .on_action(Self::open);
+ .on_action(cx.listener(Self::close_inactive_items_and_panes))
+ .on_action(cx.listener(Self::close_all_items_and_panes))
+ .on_action(cx.listener(Self::add_folder_to_project))
+ .on_action(cx.listener(Self::save_all))
+ .on_action(cx.listener(Self::open));
for action in self.workspace_actions.iter() {
- div = (action)(div)
+ div = (action)(div, cx)
}
div
}
@@ -3380,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> {
@@ -3595,7 +3684,7 @@ impl FocusableView for Workspace {
}
impl Render for Workspace {
- type Element = Div<Self>;
+ type Element = Div;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
let mut context = KeyContext::default();
@@ -3611,7 +3700,7 @@ impl Render for Workspace {
cx.set_rem_size(ui_font_size);
- self.actions(div())
+ self.actions(div(), cx)
.key_context(context)
.relative()
.size_full()
@@ -3625,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()
@@ -3660,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,
@@ -3676,7 +3763,8 @@ impl Render for Workspace {
.overflow_hidden()
.child(self.right_dock.clone()),
),
- ),
+ )
+ .children(self.render_notifications(cx)),
)
.child(self.status_bar.clone())
}
@@ -3830,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()
@@ -3873,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]
@@ -140,6 +140,7 @@ tree-sitter-lua.workspace = true
tree-sitter-nix.workspace = true
tree-sitter-nu.workspace = true
tree-sitter-vue.workspace = true
+tree-sitter-uiua.workspace = true
url = "2.2"
urlencoding = "2.1.2"
@@ -171,7 +172,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"
@@ -17,6 +17,7 @@ mod json;
#[cfg(feature = "plugin_runtime")]
mod language_plugin;
mod lua;
+mod nu;
mod php;
mod python;
mod ruby;
@@ -24,6 +25,7 @@ mod rust;
mod svelte;
mod tailwind;
mod typescript;
+mod uiua;
mod vue;
mod yaml;
@@ -210,12 +212,21 @@ pub fn init(
language("elm", tree_sitter_elm::language(), vec![]);
language("glsl", tree_sitter_glsl::language(), vec![]);
language("nix", tree_sitter_nix::language(), vec![]);
- language("nu", tree_sitter_nu::language(), vec![]);
+ language(
+ "nu",
+ tree_sitter_nu::language(),
+ vec![Arc::new(nu::NuLanguageServer {})],
+ );
language(
"vue",
tree_sitter_vue::language(),
vec![Arc::new(vue::VueLspAdapter::new(node_runtime))],
);
+ language(
+ "uiua",
+ tree_sitter_uiua::language(),
+ vec![Arc::new(uiua::UiuaLanguageServer {})],
+ );
}
#[cfg(any(test, feature = "test-support"))]
@@ -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
@@ -0,0 +1,81 @@
+use anyhow::{anyhow, Result};
+use async_trait::async_trait;
+use language::{CodeLabel, Language, LanguageServerName, LspAdapter, LspAdapterDelegate};
+use lsp::LanguageServerBinary;
+use std::{any::Any, path::PathBuf, sync::Arc};
+
+pub struct NuLanguageServer;
+
+#[async_trait]
+impl LspAdapter for NuLanguageServer {
+ async fn name(&self) -> LanguageServerName {
+ LanguageServerName("nu".into())
+ }
+
+ fn short_name(&self) -> &'static str {
+ "nu"
+ }
+
+ async fn fetch_latest_server_version(
+ &self,
+ _: &dyn LspAdapterDelegate,
+ ) -> Result<Box<dyn 'static + Any + Send>> {
+ Ok(Box::new(()))
+ }
+
+ async fn fetch_server_binary(
+ &self,
+ _version: Box<dyn 'static + Send + Any>,
+ _container_dir: PathBuf,
+ _: &dyn LspAdapterDelegate,
+ ) -> Result<LanguageServerBinary> {
+ Err(anyhow!(
+ "nu v0.87.0 or greater must be installed and available in your $PATH"
+ ))
+ }
+
+ async fn cached_server_binary(
+ &self,
+ _: PathBuf,
+ _: &dyn LspAdapterDelegate,
+ ) -> Option<LanguageServerBinary> {
+ Some(LanguageServerBinary {
+ path: "nu".into(),
+ arguments: vec!["--lsp".into()],
+ })
+ }
+
+ fn can_be_reinstalled(&self) -> bool {
+ false
+ }
+
+ async fn installation_test_binary(&self, _: PathBuf) -> Option<LanguageServerBinary> {
+ None
+ }
+
+ async fn label_for_completion(
+ &self,
+ completion: &lsp::CompletionItem,
+ language: &Arc<Language>,
+ ) -> Option<CodeLabel> {
+ return Some(CodeLabel {
+ runs: language
+ .highlight_text(&completion.label.clone().into(), 0..completion.label.len()),
+ text: completion.label.clone(),
+ filter_range: 0..completion.label.len(),
+ });
+ }
+
+ async fn label_for_symbol(
+ &self,
+ name: &str,
+ _: lsp::SymbolKind,
+ language: &Arc<Language>,
+ ) -> Option<CodeLabel> {
+ Some(CodeLabel {
+ runs: language.highlight_text(&name.into(), 0..name.len()),
+ text: name.to_string(),
+ filter_range: 0..name.len(),
+ })
+ }
+}
@@ -0,0 +1,55 @@
+use anyhow::{anyhow, Result};
+use async_trait::async_trait;
+use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
+use lsp::LanguageServerBinary;
+use std::{any::Any, path::PathBuf};
+
+pub struct UiuaLanguageServer;
+
+#[async_trait]
+impl LspAdapter for UiuaLanguageServer {
+ async fn name(&self) -> LanguageServerName {
+ LanguageServerName("uiua".into())
+ }
+
+ fn short_name(&self) -> &'static str {
+ "uiua"
+ }
+
+ async fn fetch_latest_server_version(
+ &self,
+ _: &dyn LspAdapterDelegate,
+ ) -> Result<Box<dyn 'static + Any + Send>> {
+ Ok(Box::new(()))
+ }
+
+ async fn fetch_server_binary(
+ &self,
+ _version: Box<dyn 'static + Send + Any>,
+ _container_dir: PathBuf,
+ _: &dyn LspAdapterDelegate,
+ ) -> Result<LanguageServerBinary> {
+ Err(anyhow!(
+ "uiua must be installed and available in your $PATH"
+ ))
+ }
+
+ async fn cached_server_binary(
+ &self,
+ _: PathBuf,
+ _: &dyn LspAdapterDelegate,
+ ) -> Option<LanguageServerBinary> {
+ Some(LanguageServerBinary {
+ path: "uiua".into(),
+ arguments: vec!["lsp".into()],
+ })
+ }
+
+ fn can_be_reinstalled(&self) -> bool {
+ false
+ }
+
+ async fn installation_test_binary(&self, _: PathBuf) -> Option<LanguageServerBinary> {
+ None
+ }
+}
@@ -0,0 +1,10 @@
+name = "Uiua"
+path_suffixes = ["ua"]
+line_comment = "# "
+autoclose_before = ")]}\""
+brackets = [
+ { start = "{", end = "}", close = true, newline = false },
+ { start = "[", end = "]", close = true, newline = false },
+ { start = "(", end = ")", close = true, newline = false },
+ { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] },
+]
@@ -0,0 +1,50 @@
+[
+ (openParen)
+ (closeParen)
+ (openCurly)
+ (closeCurly)
+ (openBracket)
+ (closeBracket)
+] @punctuation.bracket
+
+[
+ (branchSeparator)
+ (underscore)
+] @constructor
+; ] @punctuation.delimiter
+
+[ (character) ] @constant.character
+[ (comment) ] @comment
+[ (constant) ] @constant.numeric
+[ (identifier) ] @variable
+[ (leftArrow) ] @keyword
+[ (function) ] @function
+[ (modifier1) ] @operator
+[ (modifier2) ] @operator
+[ (number) ] @constant.numeric
+[ (placeHolder) ] @special
+[ (otherConstant) ] @string.special
+[ (signature) ] @type
+[ (system) ] @function.builtin
+[ (tripleMinus) ] @module
+
+; planet
+[
+ "id"
+ "identity"
+ "∘"
+ "dip"
+ "⊙"
+ "gap"
+ "⋅"
+] @tag
+
+[
+ (string)
+ (multiLineString)
+] @string
+
+; [
+; (deprecated)
+; (identifierDeprecated)
+; ] @warning
@@ -0,0 +1,3 @@
+[
+ (array)
+] @indent
@@ -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,12 +16,12 @@ 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" }
call = { package = "call2", path = "../call2" }
-# channel = { path = "../channel" }
+channel = { package = "channel2", path = "../channel2" }
cli = { path = "../cli" }
collab_ui = { package = "collab_ui2", path = "../collab_ui2" }
collections = { path = "../collections" }
@@ -36,10 +36,9 @@ db = { package = "db2", path = "../db2" }
editor = { package="editor2", path = "../editor2" }
# feedback = { path = "../feedback" }
file_finder = { package="file_finder2", path = "../file_finder2" }
-# search = { path = "../search" }
+search = { package = "search2", path = "../search2" }
fs = { package = "fs2", path = "../fs2" }
fsevent = { path = "../fsevent" }
-fuzzy = { path = "../fuzzy" }
go_to_line = { package = "go_to_line2", path = "../go_to_line2" }
gpui = { package = "gpui2", path = "../gpui2" }
install_cli = { package = "install_cli2", path = "../install_cli2" }
@@ -137,6 +136,7 @@ tree-sitter-lua.workspace = true
tree-sitter-nix.workspace = true
tree-sitter-nu.workspace = true
tree-sitter-vue.workspace = true
+tree-sitter-uiua.workspace = true
url = "2.2"
urlencoding = "2.1.2"
@@ -18,6 +18,7 @@ mod json;
#[cfg(feature = "plugin_runtime")]
mod language_plugin;
mod lua;
+mod nu;
mod php;
mod python;
mod ruby;
@@ -25,6 +26,7 @@ mod rust;
mod svelte;
mod tailwind;
mod typescript;
+mod uiua;
mod vue;
mod yaml;
@@ -211,12 +213,21 @@ pub fn init(
language("elm", tree_sitter_elm::language(), vec![]);
language("glsl", tree_sitter_glsl::language(), vec![]);
language("nix", tree_sitter_nix::language(), vec![]);
- language("nu", tree_sitter_nu::language(), vec![]);
+ language(
+ "nu",
+ tree_sitter_nu::language(),
+ vec![Arc::new(nu::NuLanguageServer {})],
+ );
language(
"vue",
tree_sitter_vue::language(),
vec![Arc::new(vue::VueLspAdapter::new(node_runtime))],
);
+ language(
+ "uiua",
+ tree_sitter_uiua::language(),
+ vec![Arc::new(uiua::UiuaLanguageServer {})],
+ );
}
#[cfg(any(test, feature = "test-support"))]
@@ -0,0 +1,55 @@
+use anyhow::{anyhow, Result};
+use async_trait::async_trait;
+use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
+use lsp::LanguageServerBinary;
+use std::{any::Any, path::PathBuf};
+
+pub struct NuLanguageServer;
+
+#[async_trait]
+impl LspAdapter for NuLanguageServer {
+ async fn name(&self) -> LanguageServerName {
+ LanguageServerName("nu".into())
+ }
+
+ fn short_name(&self) -> &'static str {
+ "nu"
+ }
+
+ async fn fetch_latest_server_version(
+ &self,
+ _: &dyn LspAdapterDelegate,
+ ) -> Result<Box<dyn 'static + Any + Send>> {
+ Ok(Box::new(()))
+ }
+
+ async fn fetch_server_binary(
+ &self,
+ _version: Box<dyn 'static + Send + Any>,
+ _container_dir: PathBuf,
+ _: &dyn LspAdapterDelegate,
+ ) -> Result<LanguageServerBinary> {
+ Err(anyhow!(
+ "nu v0.87.0 or greater must be installed and available in your $PATH"
+ ))
+ }
+
+ async fn cached_server_binary(
+ &self,
+ _: PathBuf,
+ _: &dyn LspAdapterDelegate,
+ ) -> Option<LanguageServerBinary> {
+ Some(LanguageServerBinary {
+ path: "nu".into(),
+ arguments: vec!["--lsp".into()],
+ })
+ }
+
+ fn can_be_reinstalled(&self) -> bool {
+ false
+ }
+
+ async fn installation_test_binary(&self, _: PathBuf) -> Option<LanguageServerBinary> {
+ None
+ }
+}
@@ -0,0 +1,55 @@
+use anyhow::{anyhow, Result};
+use async_trait::async_trait;
+use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
+use lsp::LanguageServerBinary;
+use std::{any::Any, path::PathBuf};
+
+pub struct UiuaLanguageServer;
+
+#[async_trait]
+impl LspAdapter for UiuaLanguageServer {
+ async fn name(&self) -> LanguageServerName {
+ LanguageServerName("uiua".into())
+ }
+
+ fn short_name(&self) -> &'static str {
+ "uiua"
+ }
+
+ async fn fetch_latest_server_version(
+ &self,
+ _: &dyn LspAdapterDelegate,
+ ) -> Result<Box<dyn 'static + Any + Send>> {
+ Ok(Box::new(()))
+ }
+
+ async fn fetch_server_binary(
+ &self,
+ _version: Box<dyn 'static + Send + Any>,
+ _container_dir: PathBuf,
+ _: &dyn LspAdapterDelegate,
+ ) -> Result<LanguageServerBinary> {
+ Err(anyhow!(
+ "uiua must be installed and available in your $PATH"
+ ))
+ }
+
+ async fn cached_server_binary(
+ &self,
+ _: PathBuf,
+ _: &dyn LspAdapterDelegate,
+ ) -> Option<LanguageServerBinary> {
+ Some(LanguageServerBinary {
+ path: "uiua".into(),
+ arguments: vec!["lsp".into()],
+ })
+ }
+
+ fn can_be_reinstalled(&self) -> bool {
+ false
+ }
+
+ async fn installation_test_binary(&self, _: PathBuf) -> Option<LanguageServerBinary> {
+ None
+ }
+}
@@ -0,0 +1,10 @@
+name = "Uiua"
+path_suffixes = ["ua"]
+line_comment = "# "
+autoclose_before = ")]}\""
+brackets = [
+ { start = "{", end = "}", close = true, newline = false},
+ { start = "[", end = "]", close = true, newline = false },
+ { start = "(", end = ")", close = true, newline = false },
+ { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] },
+]
@@ -0,0 +1,50 @@
+[
+ (openParen)
+ (closeParen)
+ (openCurly)
+ (closeCurly)
+ (openBracket)
+ (closeBracket)
+] @punctuation.bracket
+
+[
+ (branchSeparator)
+ (underscore)
+] @constructor
+; ] @punctuation.delimiter
+
+[ (character) ] @constant.character
+[ (comment) ] @comment
+[ (constant) ] @constant.numeric
+[ (identifier) ] @variable
+[ (leftArrow) ] @keyword
+[ (function) ] @function
+[ (modifier1) ] @operator
+[ (modifier2) ] @operator
+[ (number) ] @constant.numeric
+[ (placeHolder) ] @special
+[ (otherConstant) ] @string.special
+[ (signature) ] @type
+[ (system) ] @function.builtin
+[ (tripleMinus) ] @module
+
+; planet
+[
+ "id"
+ "identity"
+ "∘"
+ "dip"
+ "⊙"
+ "gap"
+ "⋅"
+] @tag
+
+[
+ (string)
+ (multiLineString)
+] @string
+
+; [
+; (deprecated)
+; (identifierDeprecated)
+; ] @warning
@@ -0,0 +1,3 @@
+[
+ (array)
+] @indent
@@ -8,7 +8,7 @@ use anyhow::{anyhow, Context as _, Result};
use backtrace::Backtrace;
use chrono::Utc;
use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
-use client::UserStore;
+use client::{Client, UserStore};
use db::kvp::KEY_VALUE_STORE;
use editor::Editor;
use fs::RealFs;
@@ -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());
@@ -173,20 +177,29 @@ 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,
client: client.clone(),
- user_store,
+ user_store: user_store.clone(),
fs,
build_window_options,
+ call_factory: call::Call::new,
// background_actions: todo!("ask Mikayla"),
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);
@@ -197,9 +210,9 @@ fn main() {
// outline::init(cx);
// project_symbols::init(cx);
project_panel::init(Assets, cx);
- // channel::init(&client, user_store.clone(), cx);
+ channel::init(&client, user_store.clone(), cx);
// diagnostics::init(cx);
- // search::init(cx);
+ search::init(cx);
// semantic_index::init(fs.clone(), http.clone(), languages.clone(), cx);
// vim::init(cx);
terminal_view::init(cx);
@@ -236,7 +249,7 @@ fn main() {
}
}
- let mut _triggered_authentication = false;
+ let mut triggered_authentication = false;
fn open_paths_and_log_errs(
paths: &[PathBuf],
@@ -315,29 +328,29 @@ fn main() {
})
.detach();
- // if !triggered_authentication {
- // cx.spawn(|cx| async move { authenticate(client, &cx).await })
- // .detach_and_log_err(cx);
- // }
+ if !triggered_authentication {
+ cx.spawn(|cx| async move { authenticate(client, &cx).await })
+ .detach_and_log_err(cx);
+ }
});
}
-// async fn authenticate(client: Arc<Client>, cx: &AsyncAppContext) -> Result<()> {
-// if stdout_is_a_pty() {
-// if client::IMPERSONATE_LOGIN.is_some() {
-// client.authenticate_and_connect(false, &cx).await?;
-// }
-// } else if client.has_keychain_credentials(&cx) {
-// client.authenticate_and_connect(true, &cx).await?;
-// }
-// Ok::<_, anyhow::Error>(())
-// }
+async fn authenticate(client: Arc<Client>, cx: &AsyncAppContext) -> Result<()> {
+ if stdout_is_a_pty() {
+ if client::IMPERSONATE_LOGIN.is_some() {
+ client.authenticate_and_connect(false, &cx).await?;
+ }
+ } else if client.has_keychain_credentials(&cx).await {
+ client.authenticate_and_connect(true, &cx).await?;
+ }
+ 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();
@@ -345,7 +358,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))
}
}
@@ -355,7 +368,6 @@ async fn restore_or_create_workspace(app_state: &Arc<AppState>, mut cx: AsyncApp
cx.update(|cx| workspace::open_paths(location.paths().as_ref(), app_state, None, cx))?
.await
.log_err();
- } else if matches!(KEY_VALUE_STORE.read_kvp("******* THIS IS A BAD KEY PLEASE UNCOMMENT BELOW TO FIX THIS VERY LONG LINE *******"), Ok(None)) {
// todo!(welcome)
//} else if matches!(KEY_VALUE_STORE.read_kvp(FIRST_OPEN), Ok(None)) {
//todo!()
@@ -98,8 +98,8 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
// todo!()
// let breadcrumbs = cx.add_view(|_| Breadcrumbs::new(workspace));
// toolbar.add_item(breadcrumbs, cx);
- // let buffer_search_bar = cx.add_view(BufferSearchBar::new);
- // toolbar.add_item(buffer_search_bar.clone(), cx);
+ let buffer_search_bar = cx.build_view(search::BufferSearchBar::new);
+ toolbar.add_item(buffer_search_bar.clone(), cx);
// let quick_action_bar = cx.add_view(|_| {
// QuickActionBar::new(buffer_search_bar, workspace)
// });
@@ -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());
@@ -1,5 +1,7 @@
#!/bin/bash
+set -e
+
branch=$(git rev-parse --abbrev-ref HEAD)
if [ "$branch" != "main" ]; then
echo "You must be on main to run this script"
@@ -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
@@ -10,6 +10,7 @@ local_only=false
overwrite_local_app=false
bundle_name=""
zed_crate="zed"
+binary_name="Zed"
# This must match the team in the provsiioning profile.
APPLE_NOTORIZATION_TEAM="MQ55VZLNZQ"
@@ -38,7 +39,6 @@ do
export CARGO_INCREMENTAL=true
export CARGO_BUNDLE_SKIP_BUILD=true
build_flag="";
- local_arch=true
target_dir="debug"
;;
l)
@@ -50,7 +50,10 @@ do
target_dir="debug"
;;
f) overwrite_local_app=true;;
- 2) zed_crate="zed2";;
+ 2)
+ zed_crate="zed2"
+ binary_name="Zed2"
+ ;;
h)
help_info
exit 0
@@ -116,7 +119,7 @@ if [ "$local_arch" = false ]; then
echo "Creating fat binaries"
lipo \
-create \
- target/{x86_64-apple-darwin,aarch64-apple-darwin}/${target_dir}/Zed \
+ target/{x86_64-apple-darwin,aarch64-apple-darwin}/${target_dir}/${binary_name} \
-output \
"${app_path}/Contents/MacOS/${zed_crate}"
lipo \
@@ -0,0 +1,159 @@
+#!/bin/bash
+
+# Check if the script is run from the root of the repository
+if [ ! -f "Cargo.toml" ] || [ ! -d "crates/zed" ]; then
+ echo "Please run the script from the root of the repository."
+ exit 1
+fi
+
+# Set the environment variables
+TARGET_DIR="../zed-docs"
+PUSH_CHANGES=false
+CLEAN_FOLDERS=false
+
+# Parse command line arguments
+while getopts "pc" opt; do
+ case ${opt} in
+ p )
+ PUSH_CHANGES=true
+ ;;
+ c )
+ CLEAN_FOLDERS=true
+ ;;
+ \? )
+ echo "Invalid option: $OPTARG" 1>&2
+ exit 1
+ ;;
+ esac
+done
+
+# Check if the target documentation directory exists
+if [ ! -d "$TARGET_DIR" ]; then
+ # Prompt the user for input
+ read -p "Can't find ../zed-docs. Do you want to clone the repository (y/n)?" -n 1 -r
+ echo # Move to a new line
+
+ if [[ $REPLY =~ ^[Yy]$ ]]; then
+ # Clone the repo if the user agrees
+ git clone https://github.com/zed-industries/zed-docs.git "$TARGET_DIR"
+ else
+ # Exit if the user does not agree to clone the repo
+ echo "Exiting without cloning the repository."
+ exit 1
+ fi
+else
+ # If the directory exists, pull the latest changes
+ pushd "$TARGET_DIR" > /dev/null
+ git pull
+ popd > /dev/null
+fi
+
+if "$CLEAN_FOLDERS"; then
+ echo "Cleaning ./doc and ./debug folders..."
+ rm -rf "$TARGET_DIR/doc"
+ rm -rf "$TARGET_DIR/debug"
+fi
+
+# Build the documentation
+CARGO_TARGET_DIR="$TARGET_DIR" cargo doc --workspace --no-deps --open \
+--exclude activity_indicator \
+--exclude ai \
+--exclude assistant \
+--exclude audio \
+--exclude auto_update \
+--exclude breadcrumbs \
+--exclude call \
+--exclude channel \
+--exclude cli \
+--exclude client \
+--exclude clock \
+--exclude collab \
+--exclude collab_ui \
+--exclude collections \
+--exclude command_palette \
+--exclude component_test \
+--exclude context_menu \
+--exclude copilot \
+--exclude copilot_button \
+--exclude db \
+--exclude diagnostics \
+--exclude drag_and_drop \
+--exclude editor \
+--exclude feature_flags \
+--exclude feedback \
+--exclude file_finder \
+--exclude fs \
+--exclude fsevent \
+--exclude fuzzy \
+--exclude git \
+--exclude go_to_line \
+--exclude gpui \
+--exclude gpui_macros \
+--exclude install_cli \
+--exclude journal \
+--exclude language \
+--exclude language_selector \
+--exclude language_tools \
+--exclude live_kit_client \
+--exclude live_kit_server \
+--exclude lsp \
+--exclude media \
+--exclude menu \
+--exclude multi_buffer \
+--exclude node_runtime \
+--exclude notifications \
+--exclude outline \
+--exclude picker \
+--exclude plugin \
+--exclude plugin_macros \
+--exclude plugin_runtime \
+--exclude prettier \
+--exclude project \
+--exclude project_panel \
+--exclude project_symbols \
+--exclude quick_action_bar \
+--exclude recent_projects \
+--exclude refineable \
+--exclude rich_text \
+--exclude rope \
+--exclude rpc \
+--exclude search \
+--exclude semantic_index \
+--exclude settings \
+--exclude snippet \
+--exclude sqlez \
+--exclude sqlez_macros \
+--exclude sum_tree \
+--exclude terminal \
+--exclude terminal_view \
+--exclude text \
+--exclude theme \
+--exclude theme_importer \
+--exclude theme_selector \
+--exclude util \
+--exclude vcs_menu \
+--exclude vim \
+--exclude welcome \
+--exclude xtask \
+--exclude zed \
+--exclude zed-actions
+
+if "$PUSH_CHANGES"; then
+ # Commit the changes and push
+ pushd "$TARGET_DIR" > /dev/null
+ # Check if there are any changes to commit
+ if git diff --quiet && git diff --staged --quiet; then
+ echo "No changes to the documentation."
+ else
+ # Staging the changes
+ git add .
+
+ # Creating a commit with the current datetime
+ DATETIME=$(date +"%Y-%m-%d %H:%M:%S")
+ git commit -m "Update docs – $DATETIME"
+
+ # Pushing the changes
+ git push
+ fi
+ popd > /dev/null
+fi
@@ -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],