diff --git a/Cargo.lock b/Cargo.lock index 16031d9bcf43c1c741470411be5450f15dccb421..2e4a4925e58fa5afc8e92aa18bfa9ad5a809b533 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -292,6 +292,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" +[[package]] +name = "assets" +version = "0.1.0" +dependencies = [ + "anyhow", + "gpui", + "rust-embed", +] + [[package]] name = "assistant" version = "0.1.0" @@ -677,6 +686,7 @@ dependencies = [ "log", "menu", "project", + "schemars", "serde", "serde_derive", "serde_json", @@ -1442,7 +1452,7 @@ dependencies = [ [[package]] name = "collab" -version = "0.34.0" +version = "0.36.0" dependencies = [ "anyhow", "async-trait", @@ -1463,6 +1473,7 @@ dependencies = [ "editor", "env_logger", "envy", + "file_finder", "fs", "futures 0.3.28", "git", @@ -1476,6 +1487,7 @@ dependencies = [ "live_kit_server", "log", "lsp", + "menu", "nanoid", "node_runtime", "notifications", @@ -1549,6 +1561,7 @@ dependencies = [ "serde_json", "settings", "smallvec", + "story", "theme", "theme_selector", "time", @@ -1687,12 +1700,11 @@ dependencies = [ "settings", "smol", "theme", - "ui", "util", ] [[package]] -name = "copilot_button" +name = "copilot_ui" version = "0.1.0" dependencies = [ "anyhow", @@ -1705,6 +1717,7 @@ dependencies = [ "settings", "smol", "theme", + "ui", "util", "workspace", "zed_actions", @@ -3427,6 +3440,40 @@ dependencies = [ "tiff", ] +[[package]] +name = "include-flate" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2e11569346406931d20276cc460215ee2826e7cad43aa986999cb244dd7adb0" +dependencies = [ + "include-flate-codegen-exports", + "lazy_static", + "libflate", +] + +[[package]] +name = "include-flate-codegen" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a7d6e1419fa3129eb0802b4c99603c0d425c79fb5d76191d5a20d0ab0d664e8" +dependencies = [ + "libflate", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "include-flate-codegen-exports" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75657043ffe3d8280f1cb8aef0f505532b392ed7758e0baeac22edadcee31a03" +dependencies = [ + "include-flate-codegen", + "proc-macro-hack", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -3818,6 +3865,26 @@ version = "0.2.148" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" +[[package]] +name = "libflate" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ff4ae71b685bbad2f2f391fe74f6b7659a34871c08b210fdc039e43bee07d18" +dependencies = [ + "adler32", + "crc32fast", + "libflate_lz77", +] + +[[package]] +name = "libflate_lz77" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a52d3a8bfc85f250440e4424db7d857e241a3aebbbe301f3eb606ab15c39acbf" +dependencies = [ + "rle-decode-fast", +] + [[package]] name = "libgit2-sys" version = "0.14.2+1.5.1" @@ -5395,6 +5462,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + [[package]] name = "proc-macro2" version = "1.0.67" @@ -6089,6 +6162,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "rle-decode-fast" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3582f63211428f83597b51b2ddb88e2a91a9d52d12831f9d08f5e624e8977422" + [[package]] name = "rmp" version = "0.8.12" @@ -6236,6 +6315,7 @@ version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1e7d90385b59f0a6bf3d3b757f3ca4ece2048265d70db20a2016043d4509a40" dependencies = [ + "include-flate", "rust-embed-impl", "rust-embed-utils", "walkdir", @@ -7437,6 +7517,7 @@ dependencies = [ "backtrace-on-stack-overflow", "chrono", "clap 4.4.4", + "collab_ui", "dialoguer", "editor", "fuzzy", @@ -9528,6 +9609,7 @@ dependencies = [ "activity_indicator", "ai", "anyhow", + "assets", "assistant", "async-compression", "async-recursion 0.3.2", @@ -9546,7 +9628,7 @@ dependencies = [ "collections", "command_palette", "copilot", - "copilot_button", + "copilot_ui", "ctor", "db", "diagnostics", diff --git a/Cargo.toml b/Cargo.toml index 9390bbb265806187a5f29170adfa5c499fc8ce99..7ea79f094c0b6d5d5cd4a1e63f70059db05ec6e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,6 @@ [workspace] members = [ + "crates/assets", "crates/activity_indicator", "crates/ai", "crates/assistant", @@ -16,7 +17,7 @@ members = [ "crates/collections", "crates/command_palette", "crates/copilot", - "crates/copilot_button", + "crates/copilot_ui", "crates/db", "crates/refineable", "crates/refineable/derive_refineable", @@ -109,7 +110,7 @@ prost = { version = "0.8" } rand = { version = "0.8.5" } refineable = { path = "./crates/refineable" } regex = { version = "1.5" } -rust-embed = { version = "8.0", features = ["include-exclude"] } +rust-embed = { version = "8.0", features = ["include-exclude", "compression"] } rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] } schemars = { version = "0.8" } serde = { version = "1.0", features = ["derive", "rc"] } diff --git a/assets/settings/default.json b/assets/settings/default.json index 8217f1675a4382d4dd0ab9daa6247391d40b3e4f..bd157c3e6137ccf5e990b94a3085896563931e60 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -76,7 +76,7 @@ // or waits for a `copilot::Toggle` "show_copilot_suggestions": true, // Whether to show tabs and spaces in the editor. - // This setting can take two values: + // This setting can take three values: // // 1. Draw tabs and spaces only for the selected text (default): // "selection" @@ -183,7 +183,7 @@ // Default height when the assistant is docked to the bottom. "default_height": 320, // The default OpenAI model to use when starting new conversations. This - // setting can take two values: + // setting can take three values: // // 1. "gpt-3.5-turbo-0613"" // 2. "gpt-4-0613"" @@ -351,7 +351,7 @@ // } "working_directory": "current_project_directory", // Set the cursor blinking behavior in the terminal. - // May take 4 values: + // May take 3 values: // 1. Never blink the cursor, ignoring the terminal mode // "blinking": "off", // 2. Default the cursor blink to off, but allow the terminal to diff --git a/crates/activity_indicator/src/activity_indicator.rs b/crates/activity_indicator/src/activity_indicator.rs index 4b990fa430cf5c652457ee42a1dc57915a7a1b8c..d04a5ab319f4c116a319a0a6f4d7b51a1de51184 100644 --- a/crates/activity_indicator/src/activity_indicator.rs +++ b/crates/activity_indicator/src/activity_indicator.rs @@ -77,9 +77,6 @@ impl ActivityIndicator { cx.observe(auto_updater, |_, _, cx| cx.notify()).detach(); } - // cx.observe_active_labeled_tasks(|_, cx| cx.notify()) - // .detach(); - Self { statuses: Default::default(), project: project.clone(), @@ -288,15 +285,6 @@ impl ActivityIndicator { }; } - // todo!(show active tasks) - // if let Some(most_recent_active_task) = cx.active_labeled_tasks().last() { - // return Content { - // icon: None, - // message: most_recent_active_task.to_string(), - // on_click: None, - // }; - // } - Default::default() } } diff --git a/crates/assets/Cargo.toml b/crates/assets/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..7ebae21d7dddb868d85dd9f11ae7fd8a34a277f7 --- /dev/null +++ b/crates/assets/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "assets" +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 = {path = "../gpui"} +rust-embed.workspace = true +anyhow.workspace = true diff --git a/crates/zed/src/assets.rs b/crates/assets/src/lib.rs similarity index 82% rename from crates/zed/src/assets.rs rename to crates/assets/src/lib.rs index 5d5e81a60e4feaff4c04bf3a0f3aff9a87659686..010b7ebda3d1be4532f47d6b5e7cdb79088694e2 100644 --- a/crates/zed/src/assets.rs +++ b/crates/assets/src/lib.rs @@ -1,3 +1,4 @@ +// This crate was essentially pulled out verbatim from main `zed` crate to avoid having to run RustEmbed macro whenever zed has to be rebuilt. It saves a second or two on an incremental build. use anyhow::anyhow; use gpui::{AssetSource, Result, SharedString}; diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index f53343531af09083999e04f1d3dce92eafe34850..d4743afb714ab47be3e38b196a45174fb33c2aa5 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -933,7 +933,7 @@ impl AssistantPanel { } fn render_hamburger_button(cx: &mut ViewContext) -> impl IntoElement { - IconButton::new("hamburger_button", Icon::Menu) + IconButton::new("hamburger_button", IconName::Menu) .on_click(cx.listener(|this, _event, cx| { if this.active_editor().is_some() { this.set_active_editor_index(None, cx); @@ -957,7 +957,7 @@ impl AssistantPanel { } fn render_split_button(cx: &mut ViewContext) -> impl IntoElement { - IconButton::new("split_button", Icon::Snip) + IconButton::new("split_button", IconName::Snip) .on_click(cx.listener(|this, _event, cx| { if let Some(active_editor) = this.active_editor() { active_editor.update(cx, |editor, cx| editor.split(&Default::default(), cx)); @@ -968,7 +968,7 @@ impl AssistantPanel { } fn render_assist_button(cx: &mut ViewContext) -> impl IntoElement { - IconButton::new("assist_button", Icon::MagicWand) + IconButton::new("assist_button", IconName::MagicWand) .on_click(cx.listener(|this, _event, cx| { if let Some(active_editor) = this.active_editor() { active_editor.update(cx, |editor, cx| editor.assist(&Default::default(), cx)); @@ -979,7 +979,7 @@ impl AssistantPanel { } fn render_quote_button(cx: &mut ViewContext) -> impl IntoElement { - IconButton::new("quote_button", Icon::Quote) + IconButton::new("quote_button", IconName::Quote) .on_click(cx.listener(|this, _event, cx| { if let Some(workspace) = this.workspace.upgrade() { cx.window_context().defer(move |cx| { @@ -994,7 +994,7 @@ impl AssistantPanel { } fn render_plus_button(cx: &mut ViewContext) -> impl IntoElement { - IconButton::new("plus_button", Icon::Plus) + IconButton::new("plus_button", IconName::Plus) .on_click(cx.listener(|this, _event, cx| { this.new_conversation(cx); })) @@ -1004,12 +1004,12 @@ impl AssistantPanel { fn render_zoom_button(&self, cx: &mut ViewContext) -> impl IntoElement { let zoomed = self.zoomed; - IconButton::new("zoom_button", Icon::Maximize) + IconButton::new("zoom_button", IconName::Maximize) .on_click(cx.listener(|this, _event, cx| { this.toggle_zoom(&ToggleZoom, cx); })) .selected(zoomed) - .selected_icon(Icon::Minimize) + .selected_icon(IconName::Minimize) .icon_size(IconSize::Small) .tooltip(move |cx| { Tooltip::for_action(if zoomed { "Zoom Out" } else { "Zoom In" }, &ToggleZoom, cx) @@ -1286,8 +1286,8 @@ impl Panel for AssistantPanel { } } - fn icon(&self, cx: &WindowContext) -> Option { - Some(Icon::Ai).filter(|_| AssistantSettings::get_global(cx).button) + fn icon(&self, cx: &WindowContext) -> Option { + Some(IconName::Ai).filter(|_| AssistantSettings::get_global(cx).button) } fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> { @@ -2349,7 +2349,7 @@ impl ConversationEditor { div() .id("error") .tooltip(move |cx| Tooltip::text(error.clone(), cx)) - .child(IconElement::new(Icon::XCircle)), + .child(Icon::new(IconName::XCircle)), ) } else { None @@ -2645,7 +2645,7 @@ impl Render for InlineAssistant { .justify_center() .w(measurements.gutter_width) .child( - IconButton::new("include_conversation", Icon::Ai) + IconButton::new("include_conversation", IconName::Ai) .on_click(cx.listener(|this, _, cx| { this.toggle_include_conversation(&ToggleIncludeConversation, cx) })) @@ -2660,7 +2660,7 @@ impl Render for InlineAssistant { ) .children(if SemanticIndex::enabled(cx) { Some( - IconButton::new("retrieve_context", Icon::MagnifyingGlass) + IconButton::new("retrieve_context", IconName::MagnifyingGlass) .on_click(cx.listener(|this, _, cx| { this.toggle_retrieve_context(&ToggleRetrieveContext, cx) })) @@ -2682,7 +2682,7 @@ impl Render for InlineAssistant { div() .id("error") .tooltip(move |cx| Tooltip::text(error_message.clone(), cx)) - .child(IconElement::new(Icon::XCircle).color(Color::Error)), + .child(Icon::new(IconName::XCircle).color(Color::Error)), ) } else { None @@ -2957,7 +2957,7 @@ impl InlineAssistant { div() .id("error") .tooltip(|cx| Tooltip::text("Not Authenticated. Please ensure you have a valid 'OPENAI_API_KEY' in your environment variables.", cx)) - .child(IconElement::new(Icon::XCircle)) + .child(Icon::new(IconName::XCircle)) .into_any_element() ), @@ -2965,7 +2965,7 @@ impl InlineAssistant { div() .id("error") .tooltip(|cx| Tooltip::text("Not Indexed", cx)) - .child(IconElement::new(Icon::XCircle)) + .child(Icon::new(IconName::XCircle)) .into_any_element() ), @@ -2996,7 +2996,7 @@ impl InlineAssistant { div() .id("update") .tooltip(move |cx| Tooltip::text(status_text.clone(), cx)) - .child(IconElement::new(Icon::Update).color(Color::Info)) + .child(Icon::new(IconName::Update).color(Color::Info)) .into_any_element() ) } @@ -3005,7 +3005,7 @@ impl InlineAssistant { div() .id("check") .tooltip(|cx| Tooltip::text("Index up to date", cx)) - .child(IconElement::new(Icon::Check).color(Color::Success)) + .child(Icon::new(IconName::Check).color(Color::Success)) .into_any_element() ), } diff --git a/crates/assistant/src/assistant_settings.rs b/crates/assistant/src/assistant_settings.rs index c0fbc74e9ae9b465abda293aa16e529a96b830ea..b2a9231a5701f287c9ba62f7c8054c1bc52205d1 100644 --- a/crates/assistant/src/assistant_settings.rs +++ b/crates/assistant/src/assistant_settings.rs @@ -57,12 +57,28 @@ pub struct AssistantSettings { pub default_open_ai_model: OpenAIModel, } +/// Assistant panel settings #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] pub struct AssistantSettingsContent { + /// Whether to show the assistant panel button in the status bar. + /// + /// Default: true pub button: Option, + /// Where to dock the assistant. + /// + /// Default: right pub dock: Option, + /// Default width in pixels when the assistant is docked to the left or right. + /// + /// Default: 640 pub default_width: Option, + /// Default height in pixels when the assistant is docked to the bottom. + /// + /// Default: 320 pub default_height: Option, + /// The default OpenAI model to use when starting new conversations. + /// + /// Default: gpt-4-1106-preview pub default_open_ai_model: Option, } diff --git a/crates/auto_update/Cargo.toml b/crates/auto_update/Cargo.toml index 884ed2b7a061464e8b649cd77c8d02c0a9d22277..5f0224aa7b2590cc0bfe4dc0d24a88a73206d879 100644 --- a/crates/auto_update/Cargo.toml +++ b/crates/auto_update/Cargo.toml @@ -22,6 +22,7 @@ anyhow.workspace = true isahc.workspace = true lazy_static.workspace = true log.workspace = true +schemars.workspace = true serde.workspace = true serde_derive.workspace = true serde_json.workspace = true diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index a2a90d4f2f69c94fc87a56ff13d8aa861304699b..06e445e3de96e4e03c560caee3fe4420dfbaed90 100644 --- a/crates/auto_update/src/auto_update.rs +++ b/crates/auto_update/src/auto_update.rs @@ -10,6 +10,7 @@ use gpui::{ }; use isahc::AsyncBody; +use schemars::JsonSchema; use serde::Deserialize; use serde_derive::Serialize; use smol::io::AsyncReadExt; @@ -61,18 +62,27 @@ struct JsonRelease { struct AutoUpdateSetting(bool); +/// Whether or not to automatically check for updates. +/// +/// Default: true +#[derive(Clone, Default, JsonSchema, Deserialize, Serialize)] +#[serde(transparent)] +struct AutoUpdateSettingOverride(Option); + impl Settings for AutoUpdateSetting { const KEY: Option<&'static str> = Some("auto_update"); - type FileContent = Option; + type FileContent = AutoUpdateSettingOverride; fn load( - default_value: &Option, - user_values: &[&Option], + default_value: &Self::FileContent, + user_values: &[&Self::FileContent], _: &mut AppContext, ) -> Result { Ok(Self( - Self::json_merge(default_value, user_values)?.ok_or_else(Self::missing_default)?, + Self::json_merge(default_value, user_values)? + .0 + .ok_or_else(Self::missing_default)?, )) } } diff --git a/crates/auto_update/src/update_notification.rs b/crates/auto_update/src/update_notification.rs index f00172591ecf37221e34cf4ff3b5b330b72fba28..65f786bca4c4e1a729974459fbdf8e549a9893cc 100644 --- a/crates/auto_update/src/update_notification.rs +++ b/crates/auto_update/src/update_notification.rs @@ -4,7 +4,7 @@ use gpui::{ }; use menu::Cancel; use util::channel::ReleaseChannel; -use workspace::ui::{h_stack, v_stack, Icon, IconElement, Label, StyledExt}; +use workspace::ui::{h_stack, v_stack, Icon, IconName, Label, StyledExt}; pub struct UpdateNotification { version: SemanticVersion, @@ -30,7 +30,7 @@ impl Render for UpdateNotification { .child( div() .id("cancel") - .child(IconElement::new(Icon::Close)) + .child(Icon::new(IconName::Close)) .cursor_pointer() .on_click(cx.listener(|this, _, cx| this.dismiss(&menu::Cancel, cx))), ), diff --git a/crates/breadcrumbs/src/breadcrumbs.rs b/crates/breadcrumbs/src/breadcrumbs.rs index 2e4306f0bc2b3694b9b4312f7a8458ed96ba44d7..e41c0c06b180cab749f7e34bb1199bd28709201b 100644 --- a/crates/breadcrumbs/src/breadcrumbs.rs +++ b/crates/breadcrumbs/src/breadcrumbs.rs @@ -67,7 +67,10 @@ impl Render for Breadcrumbs { }) .tooltip(|cx| Tooltip::for_action("Show symbol outline", &outline::Toggle, cx)), ), - None => element.child(breadcrumbs_stack), + None => element + // Match the height of the `ButtonLike` in the other arm. + .h(rems(22. / 16.)) + .child(breadcrumbs_stack), } } } diff --git a/crates/call/src/call_settings.rs b/crates/call/src/call_settings.rs index 9375feedf07bd805e7952161982f8c022b1ee28f..441323ad5ffcf4d6d525525139905278bd15ca0b 100644 --- a/crates/call/src/call_settings.rs +++ b/crates/call/src/call_settings.rs @@ -9,8 +9,12 @@ pub struct CallSettings { pub mute_on_join: bool, } +/// Configuration of voice calls in Zed. #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] pub struct CallSettingsContent { + /// Whether the microphone should be muted when joining a channel or a call. + /// + /// Default: false pub mute_on_join: Option, } diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index e2c9bf5886faca374334e463b8d0ac4711ce863b..3d1f1e70c74ab481864884e37a33e6db03ba5b05 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -173,7 +173,11 @@ impl Room { cx.spawn(|this, mut cx| async move { connect.await?; - if !cx.update(|cx| Self::mute_on_join(cx))? { + let is_read_only = this + .update(&mut cx, |room, _| room.read_only()) + .unwrap_or(true); + + if !cx.update(|cx| Self::mute_on_join(cx))? && !is_read_only { this.update(&mut cx, |this, cx| this.share_microphone(cx))? .await?; } @@ -620,6 +624,27 @@ impl Room { self.local_participant.role == proto::ChannelRole::Admin } + pub fn set_participant_role( + &mut self, + user_id: u64, + role: proto::ChannelRole, + cx: &ModelContext, + ) -> Task> { + let client = self.client.clone(); + let room_id = self.id; + let role = role.into(); + cx.spawn(|_, _| async move { + client + .request(proto::SetRoomParticipantRole { + room_id, + user_id, + role, + }) + .await + .map(|_| ()) + }) + } + pub fn pending_participants(&self) -> &[Arc] { &self.pending_participants } @@ -729,9 +754,21 @@ impl Room { if this.local_participant.role != role { this.local_participant.role = role; + if role == proto::ChannelRole::Guest { + for project in mem::take(&mut this.shared_projects) { + if let Some(project) = project.upgrade() { + this.unshare_project(project, cx).log_err(); + } + } + this.local_participant.projects.clear(); + if let Some(live_kit_room) = &mut this.live_kit { + live_kit_room.stop_publishing(cx); + } + } + this.joined_projects.retain(|project| { if let Some(project) = project.upgrade() { - project.update(cx, |project, _| project.set_role(role)); + project.update(cx, |project, cx| project.set_role(role, cx)); true } else { false @@ -1607,6 +1644,24 @@ impl LiveKitRoom { Ok((result, old_muted)) } + + fn stop_publishing(&mut self, cx: &mut ModelContext) { + if let LocalTrack::Published { + track_publication, .. + } = mem::replace(&mut self.microphone_track, LocalTrack::None) + { + self.room.unpublish_track(track_publication); + cx.notify(); + } + + if let LocalTrack::Published { + track_publication, .. + } = mem::replace(&mut self.screen_track, LocalTrack::None) + { + self.room.unpublish_track(track_publication); + cx.notify(); + } + } } enum LocalTrack { diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 3eae9d92bb9f8d75cbf7ddb63eab26fdd70ecdc4..0821a8e534d9713b312667dce33eb2c11c93d429 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -352,9 +352,16 @@ pub struct TelemetrySettings { pub metrics: bool, } +/// Control what info is collected by Zed. #[derive(Default, Clone, Serialize, Deserialize, JsonSchema)] pub struct TelemetrySettingsContent { + /// Send debug info like crash reports. + /// + /// Default: true pub diagnostics: Option, + /// Send anonymized usage data like what languages you're using Zed with. + /// + /// Default: true pub metrics: Option, } diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 1c288c875db39e3d3d7aed81ed836c1a69b41f5e..4453bb40eaaf34291cfa5b7f9cc917cd8606706f 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -3,7 +3,7 @@ use anyhow::{anyhow, Context, Result}; use collections::{hash_map::Entry, HashMap, HashSet}; use feature_flags::FeatureFlagAppExt; use futures::{channel::mpsc, Future, StreamExt}; -use gpui::{AsyncAppContext, EventEmitter, Model, ModelContext, SharedString, Task}; +use gpui::{AsyncAppContext, EventEmitter, Model, ModelContext, SharedUrl, Task}; use postage::{sink::Sink, watch}; use rpc::proto::{RequestMessage, UsersResponse}; use std::sync::{Arc, Weak}; @@ -19,7 +19,7 @@ pub struct ParticipantIndex(pub u32); pub struct User { pub id: UserId, pub github_login: String, - pub avatar_uri: SharedString, + pub avatar_uri: SharedUrl, } #[derive(Clone, Debug, PartialEq, Eq)] diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 498ded6d9a70f144bda9aa94259b62c129ecc568..b0917104f98bc30d193e21171aca81f56199c83b 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] default-run = "collab" edition = "2021" name = "collab" -version = "0.34.0" +version = "0.36.0" publish = false [[bin]] @@ -74,6 +74,8 @@ live_kit_client = { path = "../live_kit_client", features = ["test-support"] } lsp = { path = "../lsp", features = ["test-support"] } node_runtime = { path = "../node_runtime" } notifications = { path = "../notifications", features = ["test-support"] } +file_finder = { path = "../file_finder"} +menu = { path = "../menu"} project = { path = "../project", features = ["test-support"] } rpc = { path = "../rpc", features = ["test-support"] } diff --git a/crates/collab/src/db/ids.rs b/crates/collab/src/db/ids.rs index 9bb766147f5b0b2084b76665262962179a62e6eb..9dbbfaff8f8d0e94ea56b02eda37485ddc695fa8 100644 --- a/crates/collab/src/db/ids.rs +++ b/crates/collab/src/db/ids.rs @@ -133,13 +133,29 @@ impl ChannelRole { } } - pub fn can_share_projects(&self) -> bool { + pub fn can_publish_to_rooms(&self) -> bool { use ChannelRole::*; match self { Admin | Member => true, Guest | Banned => false, } } + + pub fn can_edit_projects(&self) -> bool { + use ChannelRole::*; + match self { + Admin | Member => true, + Guest | Banned => false, + } + } + + pub fn can_read_projects(&self) -> bool { + use ChannelRole::*; + match self { + Admin | Member | Guest => true, + Banned => false, + } + } } impl From for ChannelRole { diff --git a/crates/collab/src/db/queries/projects.rs b/crates/collab/src/db/queries/projects.rs index 5b8d54f8d36a746b2c09a77cbf9e75b77b557543..d82a597038d063405a22aeddd0623d7f9bfc9eb0 100644 --- a/crates/collab/src/db/queries/projects.rs +++ b/crates/collab/src/db/queries/projects.rs @@ -49,7 +49,7 @@ impl Database { if !participant .role .unwrap_or(ChannelRole::Member) - .can_share_projects() + .can_publish_to_rooms() { return Err(anyhow!("guests cannot share projects"))?; } @@ -777,13 +777,131 @@ impl Database { .await } - pub async fn project_collaborators( + pub async fn check_user_is_project_host( &self, project_id: ProjectId, connection_id: ConnectionId, + ) -> Result<()> { + let room_id = self.room_id_for_project(project_id).await?; + self.room_transaction(room_id, |tx| async move { + project_collaborator::Entity::find() + .filter( + Condition::all() + .add(project_collaborator::Column::ProjectId.eq(project_id)) + .add(project_collaborator::Column::IsHost.eq(true)) + .add(project_collaborator::Column::ConnectionId.eq(connection_id.id)) + .add( + project_collaborator::Column::ConnectionServerId + .eq(connection_id.owner_id), + ), + ) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("failed to read project host"))?; + + Ok(()) + }) + .await + .map(|guard| guard.into_inner()) + } + + pub async fn host_for_read_only_project_request( + &self, + project_id: ProjectId, + connection_id: ConnectionId, + ) -> Result { + let room_id = self.room_id_for_project(project_id).await?; + self.room_transaction(room_id, |tx| async move { + let current_participant = room_participant::Entity::find() + .filter(room_participant::Column::RoomId.eq(room_id)) + .filter(room_participant::Column::AnsweringConnectionId.eq(connection_id.id)) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no such room"))?; + + if !current_participant + .role + .map_or(false, |role| role.can_read_projects()) + { + Err(anyhow!("not authorized to read projects"))?; + } + + let host = project_collaborator::Entity::find() + .filter( + project_collaborator::Column::ProjectId + .eq(project_id) + .and(project_collaborator::Column::IsHost.eq(true)), + ) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("failed to read project host"))?; + + Ok(host.connection()) + }) + .await + .map(|guard| guard.into_inner()) + } + + pub async fn host_for_mutating_project_request( + &self, + project_id: ProjectId, + connection_id: ConnectionId, + ) -> Result { + let room_id = self.room_id_for_project(project_id).await?; + self.room_transaction(room_id, |tx| async move { + let current_participant = room_participant::Entity::find() + .filter(room_participant::Column::RoomId.eq(room_id)) + .filter(room_participant::Column::AnsweringConnectionId.eq(connection_id.id)) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no such room"))?; + + if !current_participant + .role + .map_or(false, |role| role.can_edit_projects()) + { + Err(anyhow!("not authorized to edit projects"))?; + } + + let host = project_collaborator::Entity::find() + .filter( + project_collaborator::Column::ProjectId + .eq(project_id) + .and(project_collaborator::Column::IsHost.eq(true)), + ) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("failed to read project host"))?; + + Ok(host.connection()) + }) + .await + .map(|guard| guard.into_inner()) + } + + pub async fn project_collaborators_for_buffer_update( + &self, + project_id: ProjectId, + connection_id: ConnectionId, + requires_write: bool, ) -> Result>> { let room_id = self.room_id_for_project(project_id).await?; self.room_transaction(room_id, |tx| async move { + let current_participant = room_participant::Entity::find() + .filter(room_participant::Column::RoomId.eq(room_id)) + .filter(room_participant::Column::AnsweringConnectionId.eq(connection_id.id)) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no such room"))?; + + if requires_write + && !current_participant + .role + .map_or(false, |role| role.can_edit_projects()) + { + Err(anyhow!("not authorized to edit projects"))?; + } + let collaborators = project_collaborator::Entity::find() .filter(project_collaborator::Column::ProjectId.eq(project_id)) .all(&*tx) diff --git a/crates/collab/src/db/queries/rooms.rs b/crates/collab/src/db/queries/rooms.rs index 9a87f91b8182b69b32539810e9fc46f7afdadb14..c3e71880fdc5e8eb871dfd9a9aff3e85251a321e 100644 --- a/crates/collab/src/db/queries/rooms.rs +++ b/crates/collab/src/db/queries/rooms.rs @@ -1004,6 +1004,46 @@ impl Database { .await } + pub async fn set_room_participant_role( + &self, + admin_id: UserId, + room_id: RoomId, + user_id: UserId, + role: ChannelRole, + ) -> Result> { + self.room_transaction(room_id, |tx| async move { + room_participant::Entity::find() + .filter( + Condition::all() + .add(room_participant::Column::RoomId.eq(room_id)) + .add(room_participant::Column::UserId.eq(admin_id)) + .add(room_participant::Column::Role.eq(ChannelRole::Admin)), + ) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("only admins can set participant role"))?; + + let result = room_participant::Entity::update_many() + .filter( + Condition::all() + .add(room_participant::Column::RoomId.eq(room_id)) + .add(room_participant::Column::UserId.eq(user_id)), + ) + .set(room_participant::ActiveModel { + role: ActiveValue::set(Some(ChannelRole::from(role))), + ..Default::default() + }) + .exec(&*tx) + .await?; + + if result.rows_affected != 1 { + Err(anyhow!("could not update room participant role"))?; + } + Ok(self.get_room(room_id, &tx).await?) + }) + .await + } + pub async fn connection_lost(&self, connection: ConnectionId) -> Result<()> { self.transaction(|tx| async move { self.room_connection_lost(connection, &*tx).await?; diff --git a/crates/collab/src/db/tests/db_tests.rs b/crates/collab/src/db/tests/db_tests.rs index 1f825efd74583d6754e762e54e1d5faa3b082889..5332f227ef4277ada2fce222bb7097ef0da396b3 100644 --- a/crates/collab/src/db/tests/db_tests.rs +++ b/crates/collab/src/db/tests/db_tests.rs @@ -455,7 +455,7 @@ async fn test_project_count(db: &Arc) { .unwrap(); let room_id = RoomId::from_proto( - db.create_room(user1.user_id, ConnectionId { owner_id, id: 0 }, "", "dev") + db.create_room(user1.user_id, ConnectionId { owner_id, id: 0 }, "", "test") .await .unwrap() .id, @@ -473,7 +473,7 @@ async fn test_project_count(db: &Arc) { room_id, user2.user_id, ConnectionId { owner_id, id: 1 }, - "dev", + "test", ) .await .unwrap(); diff --git a/crates/collab/src/lib.rs b/crates/collab/src/lib.rs index 85216525b0018c6d051c55a5882af8445f45c7d0..aba9bd75d1f0aa9cc1849309dcb8f8db5b2ed9e3 100644 --- a/crates/collab/src/lib.rs +++ b/crates/collab/src/lib.rs @@ -88,7 +88,7 @@ impl std::fmt::Display for Error { impl std::error::Error for Error {} -#[derive(Default, Deserialize)] +#[derive(Deserialize)] pub struct Config { pub http_port: u16, pub database_url: String, @@ -100,7 +100,13 @@ pub struct Config { pub live_kit_secret: Option, pub rust_log: Option, pub log_json: Option, - pub zed_environment: String, + pub zed_environment: Arc, +} + +impl Config { + pub fn is_development(&self) -> bool { + self.zed_environment == "development".into() + } } #[derive(Default, Deserialize)] diff --git a/crates/collab/src/main.rs b/crates/collab/src/main.rs index 6fbb451fee71a01c1e41c123523715d2aae61f65..87df7cac6fc77f4204d00c4df8a759d141b0345d 100644 --- a/crates/collab/src/main.rs +++ b/crates/collab/src/main.rs @@ -53,6 +53,25 @@ async fn main() -> Result<()> { let config = envy::from_env::().expect("error loading config"); init_tracing(&config); + if config.is_development() { + // sanity check database url so even if we deploy a busted ZED_ENVIRONMENT to production + // we do not run + if config.database_url != "postgres://postgres@localhost/zed" { + panic!("about to run development migrations on a non-development database?") + } + let migrations_path = Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/migrations")); + let db_options = db::ConnectOptions::new(config.database_url.clone()); + let db = Database::new(db_options, Executor::Production).await?; + + let migrations = db.migrate(&migrations_path, false).await?; + for (migration, duration) in migrations { + println!( + "Ran {} {} {:?}", + migration.version, migration.description, duration + ); + } + } + let state = AppState::new(config).await?; let listener = TcpListener::bind(&format!("0.0.0.0:{}", state.config.http_port)) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 835b48809da94dc60cd872d473e564a7456da81e..5d7f68caac9013bf491d1cd37929b1268d075aea 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -42,7 +42,7 @@ use prometheus::{register_int_gauge, IntGauge}; use rpc::{ proto::{ self, Ack, AnyTypedEnvelope, EntityMessage, EnvelopedMessage, LiveKitConnectionInfo, - RequestMessage, UpdateChannelBufferCollaborators, + RequestMessage, ShareProject, UpdateChannelBufferCollaborators, }, Connection, ConnectionId, Peer, Receipt, TypedEnvelope, }; @@ -66,7 +66,6 @@ use time::OffsetDateTime; use tokio::sync::{watch, Semaphore}; use tower::ServiceBuilder; use tracing::{info_span, instrument, Instrument}; -use util::channel::RELEASE_CHANNEL_NAME; pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30); pub const CLEANUP_TIMEOUT: Duration = Duration::from_secs(10); @@ -104,6 +103,7 @@ impl Response { #[derive(Clone)] struct Session { + zed_environment: Arc, user_id: UserId, connection_id: ConnectionId, db: Arc>, @@ -202,6 +202,7 @@ impl Server { .add_request_handler(join_room) .add_request_handler(rejoin_room) .add_request_handler(leave_room) + .add_request_handler(set_room_participant_role) .add_request_handler(call) .add_request_handler(cancel_call) .add_message_handler(decline_call) @@ -216,40 +217,45 @@ impl Server { .add_message_handler(update_language_server) .add_message_handler(update_diagnostic_summary) .add_message_handler(update_worktree_settings) - .add_message_handler(refresh_inlay_hints) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) - .add_request_handler(forward_project_request::) + .add_request_handler(forward_read_only_project_request::) + .add_request_handler(forward_read_only_project_request::) + .add_request_handler(forward_read_only_project_request::) + .add_request_handler(forward_read_only_project_request::) + .add_request_handler(forward_read_only_project_request::) + .add_request_handler(forward_read_only_project_request::) + .add_request_handler(forward_read_only_project_request::) + .add_request_handler(forward_read_only_project_request::) + .add_request_handler(forward_read_only_project_request::) + .add_request_handler(forward_read_only_project_request::) + .add_request_handler(forward_read_only_project_request::) + .add_request_handler(forward_read_only_project_request::) + .add_request_handler(forward_mutating_project_request::) + .add_request_handler( + forward_mutating_project_request::, + ) + .add_request_handler( + forward_mutating_project_request::, + ) + .add_request_handler(forward_mutating_project_request::) + .add_request_handler(forward_mutating_project_request::) + .add_request_handler(forward_mutating_project_request::) + .add_request_handler(forward_mutating_project_request::) + .add_request_handler(forward_mutating_project_request::) + .add_request_handler(forward_mutating_project_request::) + .add_request_handler(forward_mutating_project_request::) + .add_request_handler(forward_mutating_project_request::) + .add_request_handler(forward_mutating_project_request::) + .add_request_handler(forward_mutating_project_request::) + .add_request_handler(forward_mutating_project_request::) + .add_request_handler(forward_mutating_project_request::) + .add_request_handler(forward_mutating_project_request::) .add_message_handler(create_buffer_for_peer) .add_request_handler(update_buffer) - .add_message_handler(update_buffer_file) - .add_message_handler(buffer_reloaded) - .add_message_handler(buffer_saved) - .add_request_handler(forward_project_request::) + .add_message_handler(broadcast_project_message_from_host::) + .add_message_handler(broadcast_project_message_from_host::) + .add_message_handler(broadcast_project_message_from_host::) + .add_message_handler(broadcast_project_message_from_host::) + .add_message_handler(broadcast_project_message_from_host::) .add_request_handler(get_users) .add_request_handler(fuzzy_search_users) .add_request_handler(request_contact) @@ -281,7 +287,6 @@ impl Server { .add_request_handler(follow) .add_message_handler(unfollow) .add_message_handler(update_followers) - .add_message_handler(update_diff_base) .add_request_handler(get_private_user_info) .add_message_handler(acknowledge_channel_message) .add_message_handler(acknowledge_buffer_version); @@ -609,6 +614,7 @@ impl Server { user_id, connection_id, db: Arc::new(tokio::sync::Mutex::new(DbHandle(this.app_state.db.clone()))), + zed_environment: this.app_state.config.zed_environment.clone(), peer: this.peer.clone(), connection_pool: this.connection_pool.clone(), live_kit_client: this.app_state.live_kit_client.clone(), @@ -965,7 +971,7 @@ async fn create_room( session.user_id, session.connection_id, &live_kit_room, - RELEASE_CHANNEL_NAME.as_str(), + &session.zed_environment, ) .await?; @@ -999,7 +1005,7 @@ async fn join_room( room_id, session.user_id, session.connection_id, - RELEASE_CHANNEL_NAME.as_str(), + session.zed_environment.as_ref(), ) .await?; room_updated(&room.room, &session.peer); @@ -1253,6 +1259,50 @@ async fn leave_room( Ok(()) } +async fn set_room_participant_role( + request: proto::SetRoomParticipantRole, + response: Response, + session: Session, +) -> Result<()> { + let (live_kit_room, can_publish) = { + let room = session + .db() + .await + .set_room_participant_role( + session.user_id, + RoomId::from_proto(request.room_id), + UserId::from_proto(request.user_id), + ChannelRole::from(request.role()), + ) + .await?; + + let live_kit_room = room.live_kit_room.clone(); + let can_publish = ChannelRole::from(request.role()).can_publish_to_rooms(); + room_updated(&room, &session.peer); + (live_kit_room, can_publish) + }; + + if let Some(live_kit) = session.live_kit_client.as_ref() { + live_kit + .update_participant( + live_kit_room.clone(), + request.user_id.to_string(), + live_kit_server::proto::ParticipantPermission { + can_subscribe: true, + can_publish, + can_publish_data: can_publish, + hidden: false, + recorder: false, + }, + ) + .await + .trace_err(); + } + + response.send(proto::Ack {})?; + Ok(()) +} + async fn call( request: proto::Call, response: Response, @@ -1693,10 +1743,6 @@ async fn update_worktree_settings( Ok(()) } -async fn refresh_inlay_hints(request: proto::RefreshInlayHints, session: Session) -> Result<()> { - broadcast_project_message(request.project_id, request, session).await -} - async fn start_language_server( request: proto::StartLanguageServer, session: Session, @@ -1741,7 +1787,7 @@ async fn update_language_server( Ok(()) } -async fn forward_project_request( +async fn forward_read_only_project_request( request: T, response: Response, session: Session, @@ -1750,24 +1796,37 @@ where T: EntityMessage + RequestMessage, { let project_id = ProjectId::from_proto(request.remote_entity_id()); - let host_connection_id = { - let collaborators = session - .db() - .await - .project_collaborators(project_id, session.connection_id) - .await?; - collaborators - .iter() - .find(|collaborator| collaborator.is_host) - .ok_or_else(|| anyhow!("host not found"))? - .connection_id - }; - + let host_connection_id = session + .db() + .await + .host_for_read_only_project_request(project_id, session.connection_id) + .await?; let payload = session .peer .forward_request(session.connection_id, host_connection_id, request) .await?; + response.send(payload)?; + Ok(()) +} +async fn forward_mutating_project_request( + request: T, + response: Response, + session: Session, +) -> Result<()> +where + T: EntityMessage + RequestMessage, +{ + let project_id = ProjectId::from_proto(request.remote_entity_id()); + let host_connection_id = session + .db() + .await + .host_for_mutating_project_request(project_id, session.connection_id) + .await?; + let payload = session + .peer + .forward_request(session.connection_id, host_connection_id, request) + .await?; response.send(payload)?; Ok(()) } @@ -1776,6 +1835,14 @@ async fn create_buffer_for_peer( request: proto::CreateBufferForPeer, session: Session, ) -> Result<()> { + session + .db() + .await + .check_user_is_project_host( + ProjectId::from_proto(request.project_id), + session.connection_id, + ) + .await?; let peer_id = request.peer_id.ok_or_else(|| anyhow!("invalid peer id"))?; session .peer @@ -1791,11 +1858,25 @@ async fn update_buffer( let project_id = ProjectId::from_proto(request.project_id); let mut guest_connection_ids; let mut host_connection_id = None; + + let mut requires_write_permission = false; + + for op in request.operations.iter() { + match op.variant { + None | Some(proto::operation::Variant::UpdateSelections(_)) => {} + Some(_) => requires_write_permission = true, + } + } + { let collaborators = session .db() .await - .project_collaborators(project_id, session.connection_id) + .project_collaborators_for_buffer_update( + project_id, + session.connection_id, + requires_write_permission, + ) .await?; guest_connection_ids = Vec::with_capacity(collaborators.len() - 1); for collaborator in collaborators.iter() { @@ -1828,60 +1909,17 @@ async fn update_buffer( Ok(()) } -async fn update_buffer_file(request: proto::UpdateBufferFile, session: Session) -> Result<()> { - let project_id = ProjectId::from_proto(request.project_id); - let project_connection_ids = session - .db() - .await - .project_connection_ids(project_id, session.connection_id) - .await?; - - broadcast( - Some(session.connection_id), - project_connection_ids.iter().copied(), - |connection_id| { - session - .peer - .forward_send(session.connection_id, connection_id, request.clone()) - }, - ); - Ok(()) -} - -async fn buffer_reloaded(request: proto::BufferReloaded, session: Session) -> Result<()> { - let project_id = ProjectId::from_proto(request.project_id); - let project_connection_ids = session - .db() - .await - .project_connection_ids(project_id, session.connection_id) - .await?; - broadcast( - Some(session.connection_id), - project_connection_ids.iter().copied(), - |connection_id| { - session - .peer - .forward_send(session.connection_id, connection_id, request.clone()) - }, - ); - Ok(()) -} - -async fn buffer_saved(request: proto::BufferSaved, session: Session) -> Result<()> { - broadcast_project_message(request.project_id, request, session).await -} - -async fn broadcast_project_message( - project_id: u64, +async fn broadcast_project_message_from_host>( request: T, session: Session, ) -> Result<()> { - let project_id = ProjectId::from_proto(project_id); + let project_id = ProjectId::from_proto(request.remote_entity_id()); let project_connection_ids = session .db() .await .project_connection_ids(project_id, session.connection_id) .await?; + broadcast( Some(session.connection_id), project_connection_ids.iter().copied(), @@ -2608,7 +2646,7 @@ async fn join_channel_internal( channel_id, session.user_id, session.connection_id, - RELEASE_CHANNEL_NAME.as_str(), + session.zed_environment.as_ref(), ) .await?; @@ -3110,25 +3148,6 @@ async fn mark_notification_as_read( Ok(()) } -async fn update_diff_base(request: proto::UpdateDiffBase, session: Session) -> Result<()> { - let project_id = ProjectId::from_proto(request.project_id); - let project_connection_ids = session - .db() - .await - .project_connection_ids(project_id, session.connection_id) - .await?; - broadcast( - Some(session.connection_id), - project_connection_ids.iter().copied(), - |connection_id| { - session - .peer - .forward_send(session.connection_id, connection_id, request.clone()) - }, - ); - Ok(()) -} - async fn get_private_user_info( _request: proto::GetPrivateUserInfo, response: Response, diff --git a/crates/collab/src/tests/channel_guest_tests.rs b/crates/collab/src/tests/channel_guest_tests.rs index e2051c44a038ced0724b616ac4e3b4cd851b4508..9b68ce3922ab24726130c356636dae7d6899ef35 100644 --- a/crates/collab/src/tests/channel_guest_tests.rs +++ b/crates/collab/src/tests/channel_guest_tests.rs @@ -1,8 +1,8 @@ use crate::tests::TestServer; use call::ActiveCall; -use gpui::{BackgroundExecutor, TestAppContext, VisualTestContext}; +use editor::Editor; +use gpui::{BackgroundExecutor, TestAppContext}; use rpc::proto; -use workspace::Workspace; #[gpui::test] async fn test_channel_guests( @@ -13,37 +13,18 @@ async fn test_channel_guests( let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; + let active_call_a = cx_a.read(ActiveCall::global); let channel_id = server - .make_channel("the-channel", None, (&client_a, cx_a), &mut []) - .await; - - client_a - .channel_store() - .update(cx_a, |channel_store, cx| { - channel_store.set_channel_visibility(channel_id, proto::ChannelVisibility::Public, cx) - }) - .await - .unwrap(); - - client_a - .fs() - .insert_tree( - "/a", - serde_json::json!({ - "a.txt": "a-contents", - }), - ) + .make_public_channel("the-channel", &client_a, cx_a) .await; - let active_call_a = cx_a.read(ActiveCall::global); - // Client A shares a project in the channel + let project_a = client_a.build_test_project(cx_a).await; active_call_a .update(cx_a, |call, cx| call.join_channel(channel_id, cx)) .await .unwrap(); - let (project_a, _) = client_a.build_local_project("/a", cx_a).await; let project_id = active_call_a .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await @@ -57,30 +38,122 @@ async fn test_channel_guests( // b should be following a in the shared project. // B is a guest, - cx_a.executor().run_until_parked(); + executor.run_until_parked(); - // todo!() the test window does not call activation handlers - // correctly yet, so this API does not work. - // let project_b = active_call_b.read_with(cx_b, |call, _| { - // call.location() - // .unwrap() - // .upgrade() - // .expect("should not be weak") - // }); - - let window_b = cx_b.update(|cx| cx.active_window().unwrap()); - let cx_b = &mut VisualTestContext::from_window(window_b, cx_b); - - let workspace_b = window_b - .downcast::() - .unwrap() - .root_view(cx_b) - .unwrap(); - let project_b = workspace_b.update(cx_b, |workspace, _| workspace.project().clone()); + let active_call_b = cx_b.read(ActiveCall::global); + let project_b = + active_call_b.read_with(cx_b, |call, _| call.location().unwrap().upgrade().unwrap()); + let room_b = active_call_b.update(cx_b, |call, _| call.room().unwrap().clone()); assert_eq!( project_b.read_with(cx_b, |project, _| project.remote_id()), Some(project_id), ); - assert!(project_b.read_with(cx_b, |project, _| project.is_read_only())) + assert!(project_b.read_with(cx_b, |project, _| project.is_read_only())); + assert!(project_b + .update(cx_b, |project, cx| { + let worktree_id = project.worktrees().next().unwrap().read(cx).id(); + project.create_entry((worktree_id, "b.txt"), false, cx) + }) + .await + .is_err()); + assert!(room_b.read_with(cx_b, |room, _| !room.is_sharing_mic())); +} + +#[gpui::test] +async fn test_channel_guest_promotion(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { + let mut server = TestServer::start(cx_a.executor()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + let active_call_a = cx_a.read(ActiveCall::global); + + let channel_id = server + .make_public_channel("the-channel", &client_a, cx_a) + .await; + + let project_a = client_a.build_test_project(cx_a).await; + cx_a.update(|cx| workspace::join_channel(channel_id, client_a.app_state.clone(), None, cx)) + .await + .unwrap(); + + // Client A shares a project in the channel + active_call_a + .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) + .await + .unwrap(); + cx_a.run_until_parked(); + + // Client B joins channel A as a guest + cx_b.update(|cx| workspace::join_channel(channel_id, client_b.app_state.clone(), None, cx)) + .await + .unwrap(); + cx_a.run_until_parked(); + + // client B opens 1.txt as a guest + let (workspace_b, cx_b) = client_b.active_workspace(cx_b); + let room_b = cx_b + .read(ActiveCall::global) + .update(cx_b, |call, _| call.room().unwrap().clone()); + cx_b.simulate_keystrokes("cmd-p 1 enter"); + + let (project_b, editor_b) = workspace_b.update(cx_b, |workspace, cx| { + ( + workspace.project().clone(), + workspace.active_item_as::(cx).unwrap(), + ) + }); + assert!(project_b.read_with(cx_b, |project, _| project.is_read_only())); + assert!(editor_b.update(cx_b, |e, cx| e.read_only(cx))); + assert!(dbg!( + room_b + .update(cx_b, |room, cx| room.share_microphone(cx)) + .await + ) + .is_err()); + + // B is promoted + active_call_a + .update(cx_a, |call, cx| { + call.room().unwrap().update(cx, |room, cx| { + room.set_participant_role( + client_b.user_id().unwrap(), + proto::ChannelRole::Member, + cx, + ) + }) + }) + .await + .unwrap(); + cx_a.run_until_parked(); + + // project and buffers are now editable + assert!(project_b.read_with(cx_b, |project, _| !project.is_read_only())); + assert!(editor_b.update(cx_b, |editor, cx| !editor.read_only(cx))); + room_b + .update(cx_b, |room, cx| room.share_microphone(cx)) + .await + .unwrap(); + + // B is demoted + active_call_a + .update(cx_a, |call, cx| { + call.room().unwrap().update(cx, |room, cx| { + room.set_participant_role( + client_b.user_id().unwrap(), + proto::ChannelRole::Guest, + cx, + ) + }) + }) + .await + .unwrap(); + cx_a.run_until_parked(); + + // project and buffers are no longer editable + assert!(project_b.read_with(cx_b, |project, _| project.is_read_only())); + assert!(editor_b.update(cx_b, |editor, cx| editor.read_only(cx))); + assert!(room_b + .update(cx_b, |room, cx| room.share_microphone(cx)) + .await + .is_err()); } diff --git a/crates/collab/src/tests/channel_message_tests.rs b/crates/collab/src/tests/channel_message_tests.rs index f5da0e3ee6fc85016dafee4b0e6d01c3ec738520..5870bd193842620a2953e577ab0005b48237bcde 100644 --- a/crates/collab/src/tests/channel_message_tests.rs +++ b/crates/collab/src/tests/channel_message_tests.rs @@ -262,7 +262,6 @@ async fn test_remove_channel_message( #[track_caller] fn assert_messages(chat: &Model, messages: &[&str], cx: &mut TestAppContext) { - // todo!(don't directly borrow here) assert_eq!( chat.read_with(cx, |chat, _| { chat.messages() diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index 49e7060301a4279adda4439aa39784b066ed76dd..2a88bc4c579db9855184e278d1fef37200d1f470 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/crates/collab/src/tests/channel_tests.rs @@ -1337,6 +1337,7 @@ async fn test_guest_access( }) .await .unwrap(); + executor.run_until_parked(); assert_channels_list_shape(client_b.channel_store(), cx_b, &[]); diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs index 6f06e9f10faadf77d44eecbf43f6026c2799adca..0c3601b07531bf5c77459fd5530a31ba8ef68717 100644 --- a/crates/collab/src/tests/editor_tests.rs +++ b/crates/collab/src/tests/editor_tests.rs @@ -71,6 +71,7 @@ async fn test_host_disconnect( let workspace_b = cx_b.add_window(|cx| Workspace::new(0, project_b.clone(), client_b.app_state.clone(), cx)); let cx_b = &mut VisualTestContext::from_window(*workspace_b, cx_b); + let workspace_b_view = workspace_b.root_view(cx_b).unwrap(); let editor_b = workspace_b .update(cx_b, |workspace, cx| { @@ -85,8 +86,10 @@ async fn test_host_disconnect( //TODO: focus assert!(cx_b.update_view(&editor_b, |editor, cx| editor.is_focused(cx))); editor_b.update(cx_b, |editor, cx| editor.insert("X", cx)); - //todo(is_edited) - // assert!(workspace_b.is_edited(cx_b)); + + cx_b.update(|cx| { + assert!(workspace_b_view.read(cx).is_edited()); + }); // Drop client A's connection. Collaborators should disappear and the project should not be shown as shared. server.forbid_connections(); @@ -105,11 +108,11 @@ async fn test_host_disconnect( // Ensure client B's edited state is reset and that the whole window is blurred. workspace_b - .update(cx_b, |_, cx| { + .update(cx_b, |workspace, cx| { assert_eq!(cx.focused(), None); + assert!(!workspace.is_edited()) }) .unwrap(); - // assert!(!workspace_b.is_edited(cx_b)); // Ensure client B is not prompted to save edits when closing window after disconnecting. let can_close = workspace_b diff --git a/crates/collab/src/tests/following_tests.rs b/crates/collab/src/tests/following_tests.rs index 0486e294619fd4fd34604c6cc4113fb03f1057ad..9209760353935bfdd3573317a9cd3d0ca4e85573 100644 --- a/crates/collab/src/tests/following_tests.rs +++ b/crates/collab/src/tests/following_tests.rs @@ -76,6 +76,10 @@ async fn test_basic_following( let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a); let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); + cx_b.update(|cx| { + assert!(cx.is_window_active()); + }); + // Client A opens some editors. let pane_a = workspace_a.update(cx_a, |workspace, _| workspace.active_pane().clone()); let editor_a1 = workspace_a @@ -157,7 +161,6 @@ async fn test_basic_following( .update(cx_c, |call, cx| call.set_location(Some(&project_c), cx)) .await .unwrap(); - let weak_project_c = project_c.downgrade(); drop(project_c); // Client C also follows client A. @@ -234,17 +237,16 @@ async fn test_basic_following( workspace_c.update(cx_c, |workspace, cx| { workspace.close_window(&Default::default(), cx); }); - cx_c.update(|_| { - drop(workspace_c); - }); - cx_b.executor().run_until_parked(); + executor.run_until_parked(); // are you sure you want to leave the call? cx_c.simulate_prompt_answer(0); - cx_b.executor().run_until_parked(); + cx_c.cx.update(|_| { + drop(workspace_c); + }); executor.run_until_parked(); + cx_c.cx.update(|_| {}); weak_workspace_c.assert_dropped(); - weak_project_c.assert_dropped(); // Clients A and B see that client B is following A, and client C is not present in the followers. executor.run_until_parked(); @@ -1363,8 +1365,6 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut let mut server = TestServer::start(executor.clone()).await; let client_a = server.create_client(cx_a, "user_a").await; let client_b = server.create_client(cx_b, "user_b").await; - cx_a.update(editor::init); - cx_b.update(editor::init); client_a .fs() @@ -1400,9 +1400,6 @@ async fn test_following_across_workspaces(cx_a: &mut TestAppContext, cx_b: &mut let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a); let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); - cx_a.update(|cx| collab_ui::init(&client_a.app_state, cx)); - cx_b.update(|cx| collab_ui::init(&client_b.app_state, cx)); - active_call_a .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 457f085f8fe9a1d6de8df497fbff435277f6cfef..cedc841527ff5c4c40d2630dc9c15166ccec46d8 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -3065,6 +3065,7 @@ async fn test_local_settings( .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); + executor.run_until_parked(); // As client B, join that project and observe the local settings. let project_b = client_b.build_remote_project(project_id, cx_b).await; @@ -4936,10 +4937,10 @@ async fn test_project_symbols( .await .unwrap(); - buffer_b_2.read_with(cx_b, |buffer, _| { + buffer_b_2.read_with(cx_b, |buffer, cx| { assert_eq!( - buffer.file().unwrap().path().as_ref(), - Path::new("../crate-2/two.rs") + buffer.file().unwrap().full_path(cx), + Path::new("/code/crate-2/two.rs") ); }); diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index ae84729bac7c31a262dd97da990cc90a140f8669..2b2bb3a6a42b25e35d1e8763bd58bf6d8c7609fd 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -2,7 +2,7 @@ use crate::{ db::{tests::TestDb, NewUserParams, UserId}, executor::Executor, rpc::{Server, CLEANUP_TIMEOUT, RECONNECT_TIMEOUT}, - AppState, + AppState, Config, }; use anyhow::anyhow; use call::ActiveCall; @@ -20,7 +20,11 @@ use node_runtime::FakeNodeRuntime; use notifications::NotificationStore; use parking_lot::Mutex; use project::{Project, WorktreeId}; -use rpc::{proto::ChannelRole, RECEIVE_TIMEOUT}; +use rpc::{ + proto::{self, ChannelRole}, + RECEIVE_TIMEOUT, +}; +use serde_json::json; use settings::SettingsStore; use std::{ cell::{Ref, RefCell, RefMut}, @@ -228,12 +232,16 @@ impl TestServer { Project::init(&client, cx); client::init(&client, cx); language::init(cx); - editor::init_settings(cx); + editor::init(cx); workspace::init(app_state.clone(), cx); audio::init((), cx); call::init(client.clone(), user_store.clone(), cx); channel::init(&client, user_store.clone(), cx); notifications::init(client.clone(), user_store, cx); + collab_ui::init(&app_state, cx); + file_finder::init(cx); + menu::init(); + settings::KeymapFile::load_asset("keymaps/default.json", cx).unwrap(); }); client @@ -351,6 +359,31 @@ impl TestServer { channel_id } + pub async fn make_public_channel( + &self, + channel: &str, + client: &TestClient, + cx: &mut TestAppContext, + ) -> u64 { + let channel_id = self + .make_channel(channel, None, (client, cx), &mut []) + .await; + + client + .channel_store() + .update(cx, |channel_store, cx| { + channel_store.set_channel_visibility( + channel_id, + proto::ChannelVisibility::Public, + cx, + ) + }) + .await + .unwrap(); + + channel_id + } + pub async fn make_channel_tree( &self, channels: &[(&str, Option<&str>)], @@ -414,7 +447,19 @@ impl TestServer { Arc::new(AppState { db: test_db.db().clone(), live_kit_client: Some(Arc::new(fake_server.create_api_client())), - config: Default::default(), + config: Config { + http_port: 0, + database_url: "".into(), + database_max_connections: 0, + api_token: "".into(), + invite_link_prefix: "".into(), + live_kit_server: None, + live_kit_key: None, + live_kit_secret: None, + rust_log: None, + log_json: None, + zed_environment: "test".into(), + }, }) } } @@ -568,6 +613,20 @@ impl TestClient { (project, worktree.read_with(cx, |tree, _| tree.id())) } + pub async fn build_test_project(&self, cx: &mut TestAppContext) -> Model { + self.fs() + .insert_tree( + "/a", + json!({ + "1.txt": "one\none\none", + "2.txt": "two\ntwo\ntwo", + "3.txt": "three\nthree\nthree", + }), + ) + .await; + self.build_local_project("/a", cx).await.0 + } + pub fn build_empty_local_project(&self, cx: &mut TestAppContext) -> Model { cx.update(|cx| { Project::local( @@ -605,7 +664,22 @@ impl TestClient { project: &Model, cx: &'a mut TestAppContext, ) -> (View, &'a mut VisualTestContext) { - cx.add_window_view(|cx| Workspace::new(0, project.clone(), self.app_state.clone(), cx)) + cx.add_window_view(|cx| { + cx.activate_window(); + Workspace::new(0, project.clone(), self.app_state.clone(), cx) + }) + } + + pub fn active_workspace<'a>( + &'a self, + cx: &'a mut TestAppContext, + ) -> (View, &'a mut VisualTestContext) { + let window = cx.update(|cx| cx.active_window().unwrap().downcast::().unwrap()); + + let view = window.root_view(cx).unwrap(); + let cx = Box::new(VisualTestContext::from_window(*window.deref(), cx)); + // it might be nice to try and cleanup these at the end of each test. + (view, Box::leak(cx)) } } diff --git a/crates/collab_ui/Cargo.toml b/crates/collab_ui/Cargo.toml index f845de3a939886fefa42343131e0c4ec18543fea..84c1810bc841d904a7a534fb562671dc9d7232c9 100644 --- a/crates/collab_ui/Cargo.toml +++ b/crates/collab_ui/Cargo.toml @@ -9,6 +9,8 @@ path = "src/collab_ui.rs" doctest = false [features] +default = [] +stories = ["dep:story"] test-support = [ "call/test-support", "client/test-support", @@ -44,6 +46,7 @@ project = { path = "../project" } recent_projects = { path = "../recent_projects" } rpc = { path = "../rpc" } settings = { path = "../settings" } +story = { path = "../story", optional = true } feature_flags = { path = "../feature_flags"} theme = { path = "../theme" } theme_selector = { path = "../theme_selector" } diff --git a/crates/collab_ui/src/chat_panel.rs b/crates/collab_ui/src/chat_panel.rs index a13c0ed384f934d35a64cd29a5ebbc53070a7281..5786ab10d4ca59b998b1b16ea7bb3c53611b4399 100644 --- a/crates/collab_ui/src/chat_panel.rs +++ b/crates/collab_ui/src/chat_panel.rs @@ -19,9 +19,8 @@ use rich_text::RichText; use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsStore}; use std::sync::Arc; -use theme::ActiveTheme as _; use time::{OffsetDateTime, UtcOffset}; -use ui::{prelude::*, Avatar, Button, Icon, IconButton, Label, TabBar, Tooltip}; +use ui::{prelude::*, Avatar, Button, IconButton, IconName, Label, TabBar, Tooltip}; use util::{ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, @@ -48,7 +47,7 @@ pub struct ChatPanel { languages: Arc, message_list: ListState, active_chat: Option<(Model, Subscription)>, - input_editor: View, + message_editor: View, local_timezone: UtcOffset, fs: Arc, width: Option, @@ -120,7 +119,7 @@ impl ChatPanel { message_list, active_chat: Default::default(), pending_serialization: Task::ready(None), - input_editor, + message_editor: input_editor, local_timezone: cx.local_timezone(), subscriptions: Vec::new(), workspace: workspace_handle, @@ -209,7 +208,7 @@ impl ChatPanel { self.message_list.reset(chat.message_count()); let channel_name = chat.channel(cx).map(|channel| channel.name.clone()); - self.input_editor.update(cx, |editor, cx| { + self.message_editor.update(cx, |editor, cx| { editor.set_channel(channel_id, channel_name, cx); }); }; @@ -282,12 +281,12 @@ impl ChatPanel { )), ) .end_child( - IconButton::new("notes", Icon::File) + IconButton::new("notes", IconName::File) .on_click(cx.listener(Self::open_notes)) .tooltip(|cx| Tooltip::text("Open notes", cx)), ) .end_child( - IconButton::new("call", Icon::AudioOn) + IconButton::new("call", IconName::AudioOn) .on_click(cx.listener(Self::join_call)) .tooltip(|cx| Tooltip::text("Join call", cx)), ), @@ -300,13 +299,7 @@ impl ChatPanel { this } })) - .child( - div() - .z_index(1) - .p_2() - .bg(cx.theme().colors().background) - .child(self.input_editor.clone()), - ) + .child(h_stack().p_2().child(self.message_editor.clone())) .into_any() } @@ -402,7 +395,7 @@ impl ChatPanel { .w_8() .visible_on_hover("") .children(message_id_to_remove.map(|message_id| { - IconButton::new(("remove", message_id), Icon::XCircle).on_click( + IconButton::new(("remove", message_id), IconName::XCircle).on_click( cx.listener(move |this, _, cx| { this.remove_message(message_id, cx); }), @@ -428,32 +421,48 @@ impl ChatPanel { rich_text::render_markdown(message.body.clone(), &mentions, language_registry, None) } - fn render_sign_in_prompt(&self, cx: &mut ViewContext) -> AnyElement { - Button::new("sign-in", "Sign in to use chat") - .on_click(cx.listener(move |this, _, cx| { - let client = this.client.clone(); - cx.spawn(|this, mut cx| async move { - if client - .authenticate_and_connect(true, &cx) - .log_err() - .await - .is_some() - { - this.update(&mut cx, |_, cx| { - cx.focus_self(); + fn render_sign_in_prompt(&self, cx: &mut ViewContext) -> impl IntoElement { + v_stack() + .gap_2() + .p_4() + .child( + Button::new("sign-in", "Sign in") + .style(ButtonStyle::Filled) + .icon_color(Color::Muted) + .icon(IconName::Github) + .icon_position(IconPosition::Start) + .full_width() + .on_click(cx.listener(move |this, _, cx| { + let client = this.client.clone(); + cx.spawn(|this, mut cx| async move { + if client + .authenticate_and_connect(true, &cx) + .log_err() + .await + .is_some() + { + this.update(&mut cx, |_, cx| { + cx.focus_self(); + }) + .ok(); + } }) - .ok(); - } - }) - .detach(); - })) - .into_any_element() + .detach(); + })), + ) + .child( + div().flex().w_full().items_center().child( + Label::new("Sign in to chat.") + .color(Color::Muted) + .size(LabelSize::Small), + ), + ) } fn send(&mut self, _: &Confirm, cx: &mut ViewContext) { if let Some((chat, _)) = self.active_chat.as_ref() { let message = self - .input_editor + .message_editor .update(cx, |editor, cx| editor.take_message(cx)); if let Some(task) = chat @@ -550,12 +559,18 @@ impl EventEmitter for ChatPanel {} impl Render for ChatPanel { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - div() - .full() - .child(if self.client.user_id().is_some() { - self.render_channel(cx) - } else { - self.render_sign_in_prompt(cx) + v_stack() + .size_full() + .map(|this| match (self.client.user_id(), self.active_chat()) { + (Some(_), Some(_)) => this.child(self.render_channel(cx)), + (Some(_), None) => this.child( + div().p_4().child( + Label::new("Select a channel to chat in.") + .size(LabelSize::Small) + .color(Color::Muted), + ), + ), + (None, _) => this.child(self.render_sign_in_prompt(cx)), }) .min_w(px(150.)) } @@ -563,7 +578,7 @@ impl Render for ChatPanel { impl FocusableView for ChatPanel { fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle { - self.input_editor.read(cx).focus_handle(cx) + self.message_editor.read(cx).focus_handle(cx) } } @@ -607,12 +622,12 @@ impl Panel for ChatPanel { "ChatPanel" } - fn icon(&self, cx: &WindowContext) -> Option { + fn icon(&self, cx: &WindowContext) -> Option { if !is_channels_feature_enabled(cx) { return None; } - Some(ui::Icon::MessageBubbles).filter(|_| ChatPanelSettings::get_global(cx).button) + Some(ui::IconName::MessageBubbles).filter(|_| ChatPanelSettings::get_global(cx).button) } fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> { diff --git a/crates/collab_ui/src/chat_panel/message_editor.rs b/crates/collab_ui/src/chat_panel/message_editor.rs index 517fac4fbb377f425210d2468d3f18bb8d1ebb6a..7999db529a43985ae2b52cdde9f2108f9620b35c 100644 --- a/crates/collab_ui/src/chat_panel/message_editor.rs +++ b/crates/collab_ui/src/chat_panel/message_editor.rs @@ -1,16 +1,19 @@ +use std::{sync::Arc, time::Duration}; + use channel::{ChannelId, ChannelMembership, ChannelStore, MessageParams}; use client::UserId; use collections::HashMap; -use editor::{AnchorRangeExt, Editor}; +use editor::{AnchorRangeExt, Editor, EditorElement, EditorStyle}; use gpui::{ - AsyncWindowContext, FocusableView, IntoElement, Model, Render, SharedString, Task, View, - ViewContext, WeakView, + AsyncWindowContext, FocusableView, FontStyle, FontWeight, HighlightStyle, IntoElement, Model, + Render, SharedString, Task, TextStyle, View, ViewContext, WeakView, WhiteSpace, }; use language::{language_settings::SoftWrap, Buffer, BufferSnapshot, LanguageRegistry}; use lazy_static::lazy_static; use project::search::SearchQuery; -use std::{sync::Arc, time::Duration}; -use workspace::item::ItemHandle; +use settings::Settings; +use theme::ThemeSettings; +use ui::prelude::*; const MENTIONS_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(50); @@ -181,7 +184,14 @@ impl MessageEditor { } editor.clear_highlights::(cx); - editor.highlight_text::(anchor_ranges, gpui::red().into(), cx) + editor.highlight_text::( + anchor_ranges, + HighlightStyle { + font_weight: Some(FontWeight::BOLD), + ..Default::default() + }, + cx, + ) }); this.mentions = mentioned_user_ids; @@ -196,8 +206,39 @@ impl MessageEditor { } impl Render for MessageEditor { - fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { - self.editor.to_any() + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + let settings = ThemeSettings::get_global(cx); + let text_style = TextStyle { + color: if self.editor.read(cx).read_only(cx) { + cx.theme().colors().text_disabled + } else { + cx.theme().colors().text + }, + font_family: settings.ui_font.family.clone(), + font_features: settings.ui_font.features, + font_size: rems(0.875).into(), + font_weight: FontWeight::NORMAL, + font_style: FontStyle::Normal, + line_height: relative(1.3).into(), + background_color: None, + underline: None, + white_space: WhiteSpace::Normal, + }; + + div() + .w_full() + .px_2() + .py_1() + .bg(cx.theme().colors().editor_background) + .rounded_md() + .child(EditorElement::new( + &self.editor, + EditorStyle { + local_player: cx.theme().players().local(), + text: text_style, + ..Default::default() + }, + )) } } @@ -205,7 +246,7 @@ impl Render for MessageEditor { mod tests { use super::*; use client::{Client, User, UserStore}; - use gpui::{Context as _, TestAppContext, VisualContext as _}; + use gpui::TestAppContext; use language::{Language, LanguageConfig}; use rpc::proto; use settings::SettingsStore; diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index ac0925e7b0b6230ab2688d421af77ae5333f3d56..5ad3d6cfa3213119551ed341be33816336f8ca5c 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -31,13 +31,13 @@ use smallvec::SmallVec; use std::{mem, sync::Arc}; use theme::{ActiveTheme, ThemeSettings}; use ui::{ - prelude::*, Avatar, Button, Color, ContextMenu, Icon, IconButton, IconElement, IconSize, Label, + prelude::*, Avatar, Button, Color, ContextMenu, Icon, IconButton, IconName, IconSize, Label, ListHeader, ListItem, Tooltip, }; use util::{maybe, ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, - notifications::NotifyResultExt, + notifications::{NotifyResultExt, NotifyTaskExt}, Workspace, }; @@ -140,6 +140,7 @@ enum ListEntry { user: Arc, peer_id: Option, is_pending: bool, + role: proto::ChannelRole, }, ParticipantProject { project_id: u64, @@ -151,10 +152,6 @@ enum ListEntry { peer_id: Option, is_last: bool, }, - GuestCount { - count: usize, - has_visible_participants: bool, - }, IncomingRequest(Arc), OutgoingRequest(Arc), ChannelInvite(Arc), @@ -384,14 +381,10 @@ impl CollabPanel { if !self.collapsed_sections.contains(&Section::ActiveCall) { let room = room.read(cx); - let mut guest_count_ix = 0; - let mut guest_count = if room.read_only() { 1 } else { 0 }; - let mut non_guest_count = if room.read_only() { 0 } else { 1 }; if let Some(channel_id) = room.channel_id() { self.entries.push(ListEntry::ChannelNotes { channel_id }); self.entries.push(ListEntry::ChannelChat { channel_id }); - guest_count_ix = self.entries.len(); } // Populate the active user. @@ -410,12 +403,13 @@ impl CollabPanel { &Default::default(), executor.clone(), )); - if !matches.is_empty() && !room.read_only() { + if !matches.is_empty() { let user_id = user.id; self.entries.push(ListEntry::CallParticipant { user, peer_id: None, is_pending: false, + role: room.local_participant().role, }); let mut projects = room.local_participant().projects.iter().peekable(); while let Some(project) = projects.next() { @@ -442,12 +436,6 @@ impl CollabPanel { room.remote_participants() .iter() .filter_map(|(_, participant)| { - if participant.role == proto::ChannelRole::Guest { - guest_count += 1; - return None; - } else { - non_guest_count += 1; - } Some(StringMatchCandidate { id: participant.user.id as usize, string: participant.user.github_login.clone(), @@ -455,7 +443,7 @@ impl CollabPanel { }) }), ); - let matches = executor.block(match_strings( + let mut matches = executor.block(match_strings( &self.match_candidates, &query, true, @@ -463,6 +451,15 @@ impl CollabPanel { &Default::default(), executor.clone(), )); + matches.sort_by(|a, b| { + let a_is_guest = room.role_for_user(a.candidate_id as u64) + == Some(proto::ChannelRole::Guest); + let b_is_guest = room.role_for_user(b.candidate_id as u64) + == Some(proto::ChannelRole::Guest); + a_is_guest + .cmp(&b_is_guest) + .then_with(|| a.string.cmp(&b.string)) + }); for mat in matches { let user_id = mat.candidate_id as u64; let participant = &room.remote_participants()[&user_id]; @@ -470,6 +467,7 @@ impl CollabPanel { user: participant.user.clone(), peer_id: Some(participant.peer_id), is_pending: false, + role: participant.role, }); let mut projects = participant.projects.iter().peekable(); while let Some(project) = projects.next() { @@ -488,15 +486,6 @@ impl CollabPanel { }); } } - if guest_count > 0 { - self.entries.insert( - guest_count_ix, - ListEntry::GuestCount { - count: guest_count, - has_visible_participants: non_guest_count > 0, - }, - ); - } // Populate pending participants. self.match_candidates.clear(); @@ -521,6 +510,7 @@ impl CollabPanel { user: room.pending_participants()[mat.candidate_id].clone(), peer_id: None, is_pending: true, + role: proto::ChannelRole::Member, })); } } @@ -834,13 +824,19 @@ impl CollabPanel { user: &Arc, peer_id: Option, is_pending: bool, + role: proto::ChannelRole, is_selected: bool, cx: &mut ViewContext, ) -> ListItem { + let user_id = user.id; let is_current_user = - self.user_store.read(cx).current_user().map(|user| user.id) == Some(user.id); + self.user_store.read(cx).current_user().map(|user| user.id) == Some(user_id); let tooltip = format!("Follow {}", user.github_login); + let is_call_admin = ActiveCall::global(cx).read(cx).room().is_some_and(|room| { + room.read(cx).local_participant().role == proto::ChannelRole::Admin + }); + ListItem::new(SharedString::from(user.github_login.clone())) .start_slot(Avatar::new(user.avatar_uri.clone())) .child(Label::new(user.github_login.clone())) @@ -848,22 +844,32 @@ impl CollabPanel { .end_slot(if is_pending { Label::new("Calling").color(Color::Muted).into_any_element() } else if is_current_user { - IconButton::new("leave-call", Icon::Exit) + IconButton::new("leave-call", IconName::Exit) .style(ButtonStyle::Subtle) .on_click(move |_, cx| Self::leave_call(cx)) .tooltip(|cx| Tooltip::text("Leave Call", cx)) .into_any_element() + } else if role == proto::ChannelRole::Guest { + Label::new("Guest").color(Color::Muted).into_any_element() } else { div().into_any_element() }) - .when_some(peer_id, |this, peer_id| { - this.tooltip(move |cx| Tooltip::text(tooltip.clone(), cx)) + .when_some(peer_id, |el, peer_id| { + if role == proto::ChannelRole::Guest { + return el; + } + el.tooltip(move |cx| Tooltip::text(tooltip.clone(), cx)) .on_click(cx.listener(move |this, _, cx| { this.workspace .update(cx, |workspace, cx| workspace.follow(peer_id, cx)) .ok(); })) }) + .when(is_call_admin, |el| { + el.on_secondary_mouse_down(cx.listener(move |this, event: &MouseDownEvent, cx| { + this.deploy_participant_context_menu(event.position, user_id, role, cx) + })) + }) } fn render_participant_project( @@ -897,7 +903,7 @@ impl CollabPanel { h_stack() .gap_1() .child(render_tree_branch(is_last, false, cx)) - .child(IconButton::new(0, Icon::Folder)), + .child(IconButton::new(0, IconName::Folder)), ) .child(Label::new(project_name.clone())) .tooltip(move |cx| Tooltip::text(format!("Open {}", project_name), cx)) @@ -918,7 +924,7 @@ impl CollabPanel { h_stack() .gap_1() .child(render_tree_branch(is_last, false, cx)) - .child(IconButton::new(0, Icon::Screen)), + .child(IconButton::new(0, IconName::Screen)), ) .child(Label::new("Screen")) .when_some(peer_id, |this, _| { @@ -959,7 +965,7 @@ impl CollabPanel { h_stack() .gap_1() .child(render_tree_branch(false, true, cx)) - .child(IconButton::new(0, Icon::File)), + .child(IconButton::new(0, IconName::File)), ) .child(div().h_7().w_full().child(Label::new("notes"))) .tooltip(move |cx| Tooltip::text("Open Channel Notes", cx)) @@ -980,47 +986,12 @@ impl CollabPanel { h_stack() .gap_1() .child(render_tree_branch(false, false, cx)) - .child(IconButton::new(0, Icon::MessageBubbles)), + .child(IconButton::new(0, IconName::MessageBubbles)), ) .child(Label::new("chat")) .tooltip(move |cx| Tooltip::text("Open Chat", cx)) } - fn render_guest_count( - &self, - count: usize, - has_visible_participants: bool, - is_selected: bool, - cx: &mut ViewContext, - ) -> impl IntoElement { - let manageable_channel_id = ActiveCall::global(cx).read(cx).room().and_then(|room| { - let room = room.read(cx); - if room.local_participant_is_admin() { - room.channel_id() - } else { - None - } - }); - - ListItem::new("guest_count") - .selected(is_selected) - .start_slot( - h_stack() - .gap_1() - .child(render_tree_branch(!has_visible_participants, false, cx)) - .child(""), - ) - .child(Label::new(if count == 1 { - format!("{} guest", count) - } else { - format!("{} guests", count) - })) - .when_some(manageable_channel_id, |el, channel_id| { - el.tooltip(move |cx| Tooltip::text("Manage Members", cx)) - .on_click(cx.listener(move |this, _, cx| this.manage_members(channel_id, cx))) - }) - } - fn has_subchannels(&self, ix: usize) -> bool { self.entries.get(ix).map_or(false, |entry| { if let ListEntry::Channel { has_children, .. } = entry { @@ -1031,6 +1002,80 @@ impl CollabPanel { }) } + fn deploy_participant_context_menu( + &mut self, + position: Point, + user_id: u64, + role: proto::ChannelRole, + cx: &mut ViewContext, + ) { + let this = cx.view().clone(); + if !(role == proto::ChannelRole::Guest || role == proto::ChannelRole::Member) { + return; + } + + let context_menu = ContextMenu::build(cx, |context_menu, cx| { + if role == proto::ChannelRole::Guest { + context_menu.entry( + "Grant Write Access", + None, + cx.handler_for(&this, move |_, cx| { + ActiveCall::global(cx) + .update(cx, |call, cx| { + let Some(room) = call.room() else { + return Task::ready(Ok(())); + }; + room.update(cx, |room, cx| { + room.set_participant_role( + user_id, + proto::ChannelRole::Member, + cx, + ) + }) + }) + .detach_and_notify_err(cx) + }), + ) + } else if role == proto::ChannelRole::Member { + context_menu.entry( + "Revoke Write Access", + None, + cx.handler_for(&this, move |_, cx| { + ActiveCall::global(cx) + .update(cx, |call, cx| { + let Some(room) = call.room() else { + return Task::ready(Ok(())); + }; + room.update(cx, |room, cx| { + room.set_participant_role( + user_id, + proto::ChannelRole::Guest, + cx, + ) + }) + }) + .detach_and_notify_err(cx) + }), + ) + } else { + unreachable!() + } + }); + + cx.focus_view(&context_menu); + let subscription = + cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| { + if this.context_menu.as_ref().is_some_and(|context_menu| { + context_menu.0.focus_handle(cx).contains_focused(cx) + }) { + cx.focus_self(); + } + this.context_menu.take(); + cx.notify(); + }); + self.context_menu = Some((context_menu, position, subscription)); + } + fn deploy_channel_context_menu( &mut self, position: Point, @@ -1242,18 +1287,6 @@ impl CollabPanel { }); } } - ListEntry::GuestCount { .. } => { - let Some(room) = ActiveCall::global(cx).read(cx).room() else { - return; - }; - let room = room.read(cx); - let Some(channel_id) = room.channel_id() else { - return; - }; - if room.local_participant_is_admin() { - self.manage_members(channel_id, cx) - } - } ListEntry::Channel { channel, .. } => { let is_active = maybe!({ let call_channel = ActiveCall::global(cx) @@ -1724,7 +1757,7 @@ impl CollabPanel { .child( Button::new("sign_in", "Sign in") .icon_color(Color::Muted) - .icon(Icon::Github) + .icon(IconName::Github) .icon_position(IconPosition::Start) .style(ButtonStyle::Filled) .full_width() @@ -1788,8 +1821,9 @@ impl CollabPanel { user, peer_id, is_pending, + role, } => self - .render_call_participant(user, *peer_id, *is_pending, is_selected, cx) + .render_call_participant(user, *peer_id, *is_pending, *role, is_selected, cx) .into_any_element(), ListEntry::ParticipantProject { project_id, @@ -1809,12 +1843,6 @@ impl CollabPanel { ListEntry::ParticipantScreen { peer_id, is_last } => self .render_participant_screen(*peer_id, *is_last, is_selected, cx) .into_any_element(), - ListEntry::GuestCount { - count, - has_visible_participants, - } => self - .render_guest_count(*count, *has_visible_participants, is_selected, cx) - .into_any_element(), ListEntry::ChannelNotes { channel_id } => self .render_channel_notes(*channel_id, is_selected, cx) .into_any_element(), @@ -1921,7 +1949,7 @@ impl CollabPanel { let button = match section { Section::ActiveCall => channel_link.map(|channel_link| { let channel_link_copy = channel_link.clone(); - IconButton::new("channel-link", Icon::Copy) + IconButton::new("channel-link", IconName::Copy) .icon_size(IconSize::Small) .size(ButtonSize::None) .visible_on_hover("section-header") @@ -1933,13 +1961,13 @@ impl CollabPanel { .into_any_element() }), Section::Contacts => Some( - IconButton::new("add-contact", Icon::Plus) + IconButton::new("add-contact", IconName::Plus) .on_click(cx.listener(|this, _, cx| this.toggle_contact_finder(cx))) .tooltip(|cx| Tooltip::text("Search for new contact", cx)) .into_any_element(), ), Section::Channels => Some( - IconButton::new("add-channel", Icon::Plus) + IconButton::new("add-channel", IconName::Plus) .on_click(cx.listener(|this, _, cx| this.new_root_channel(cx))) .tooltip(|cx| Tooltip::text("Create a channel", cx)) .into_any_element(), @@ -2010,7 +2038,7 @@ impl CollabPanel { }) .when(!calling, |el| { el.child( - IconButton::new("remove_contact", Icon::Close) + IconButton::new("remove_contact", IconName::Close) .icon_color(Color::Muted) .visible_on_hover("") .tooltip(|cx| Tooltip::text("Remove Contact", cx)) @@ -2071,13 +2099,13 @@ impl CollabPanel { let controls = if is_incoming { vec![ - IconButton::new("decline-contact", Icon::Close) + IconButton::new("decline-contact", IconName::Close) .on_click(cx.listener(move |this, _, cx| { this.respond_to_contact_request(user_id, false, cx); })) .icon_color(color) .tooltip(|cx| Tooltip::text("Decline invite", cx)), - IconButton::new("accept-contact", Icon::Check) + IconButton::new("accept-contact", IconName::Check) .on_click(cx.listener(move |this, _, cx| { this.respond_to_contact_request(user_id, true, cx); })) @@ -2086,7 +2114,7 @@ impl CollabPanel { ] } else { let github_login = github_login.clone(); - vec![IconButton::new("remove_contact", Icon::Close) + vec![IconButton::new("remove_contact", IconName::Close) .on_click(cx.listener(move |this, _, cx| { this.remove_contact(user_id, &github_login, cx); })) @@ -2126,13 +2154,13 @@ impl CollabPanel { }; let controls = [ - IconButton::new("reject-invite", Icon::Close) + IconButton::new("reject-invite", IconName::Close) .on_click(cx.listener(move |this, _, cx| { this.respond_to_channel_invite(channel_id, false, cx); })) .icon_color(color) .tooltip(|cx| Tooltip::text("Decline invite", cx)), - IconButton::new("accept-invite", Icon::Check) + IconButton::new("accept-invite", IconName::Check) .on_click(cx.listener(move |this, _, cx| { this.respond_to_channel_invite(channel_id, true, cx); })) @@ -2150,7 +2178,7 @@ impl CollabPanel { .child(h_stack().children(controls)), ) .start_slot( - IconElement::new(Icon::Hash) + Icon::new(IconName::Hash) .size(IconSize::Small) .color(Color::Muted), ) @@ -2162,7 +2190,7 @@ impl CollabPanel { cx: &mut ViewContext, ) -> ListItem { ListItem::new("contact-placeholder") - .child(IconElement::new(Icon::Plus)) + .child(Icon::new(IconName::Plus)) .child(Label::new("Add a Contact")) .selected(is_selected) .on_click(cx.listener(|this, _, cx| this.toggle_contact_finder(cx))) @@ -2211,8 +2239,12 @@ impl CollabPanel { .map(|user| Avatar::new(user.avatar_uri.clone()).into_any_element()) .take(FACEPILE_LIMIT) .chain(if extra_count > 0 { - // todo!() @nate - this label looks wrong. - Some(Label::new(format!("+{}", extra_count)).into_any_element()) + Some( + div() + .ml_1() + .child(Label::new(format!("+{extra_count}"))) + .into_any_element(), + ) } else { None }) @@ -2224,47 +2256,6 @@ impl CollabPanel { None }; - let button_container = |cx: &mut ViewContext| { - h_stack() - .absolute() - // We're using a negative coordinate for the right anchor to - // counteract the padding of the `ListItem`. - // - // This prevents a gap from showing up between the background - // of this element and the edge of the collab panel. - .right(rems(-0.5)) - // HACK: Without this the channel name clips on top of the icons, but I'm not sure why. - .z_index(10) - .bg(cx.theme().colors().panel_background) - .when(is_selected || is_active, |this| { - this.bg(cx.theme().colors().ghost_element_selected) - }) - }; - - let messages_button = |cx: &mut ViewContext| { - IconButton::new("channel_chat", Icon::MessageBubbles) - .icon_size(IconSize::Small) - .icon_color(if has_messages_notification { - Color::Default - } else { - Color::Muted - }) - .on_click(cx.listener(move |this, _, cx| this.join_channel_chat(channel_id, cx))) - .tooltip(|cx| Tooltip::text("Open channel chat", cx)) - }; - - let notes_button = |cx: &mut ViewContext| { - IconButton::new("channel_notes", Icon::File) - .icon_size(IconSize::Small) - .icon_color(if has_notes_notification { - Color::Default - } else { - Color::Muted - }) - .on_click(cx.listener(move |this, _, cx| this.open_channel_notes(channel_id, cx))) - .tooltip(|cx| Tooltip::text("Open channel notes", cx)) - }; - let width = self.width.unwrap_or(px(240.)); div() @@ -2311,65 +2302,69 @@ impl CollabPanel { }, )) .start_slot( - IconElement::new(if is_public { Icon::Public } else { Icon::Hash }) - .size(IconSize::Small) - .color(Color::Muted), + Icon::new(if is_public { + IconName::Public + } else { + IconName::Hash + }) + .size(IconSize::Small) + .color(Color::Muted), ) .child( h_stack() .id(channel_id as usize) - // HACK: This is a dirty hack to help with the positioning of the button container. - // - // We're using a pixel width for the elements but then allowing the contents to - // overflow. This means that the label and facepile will be shown, but will not - // push the button container off the edge of the panel. - .w_px() .child(Label::new(channel.name.clone())) .children(face_pile.map(|face_pile| face_pile.render(cx))), - ) - .end_slot::
( - // If we have a notification for either button, we want to show the corresponding - // button(s) as indicators. - if has_messages_notification || has_notes_notification { - Some( - button_container(cx).child( - h_stack() - .px_1() - .children( - // We only want to render the messages button if there are unseen messages. - // This way we don't take up any space that might overlap the channel name - // when there are no notifications. - has_messages_notification.then(|| messages_button(cx)), - ) - .child( - // We always want the notes button to take up space to prevent layout - // shift when hovering over the channel. - // However, if there are is no notes notification we just show an empty slot. - notes_button(cx) - .when(!has_notes_notification, |this| { - this.visible_on_hover("") - }), - ), - ), + ), + ) + .child( + h_stack() + .absolute() + .right(rems(0.)) + .h_full() + // HACK: Without this the channel name clips on top of the icons, but I'm not sure why. + .z_index(10) + .child( + h_stack() + .h_full() + .gap_1() + .px_1() + .child( + IconButton::new("channel_chat", IconName::MessageBubbles) + .style(ButtonStyle::Filled) + .size(ButtonSize::Compact) + .icon_size(IconSize::Small) + .icon_color(if has_messages_notification { + Color::Default + } else { + Color::Muted + }) + .on_click(cx.listener(move |this, _, cx| { + this.join_channel_chat(channel_id, cx) + })) + .tooltip(|cx| Tooltip::text("Open channel chat", cx)) + .when(!has_messages_notification, |this| { + this.visible_on_hover("") + }), ) - } else { - None - }, - ) - .end_hover_slot( - // When we hover the channel entry we want to always show both buttons. - button_container(cx).child( - h_stack() - .px_1() - // The element hover background has a slight transparency to it, so we - // need to apply it to the inner element so that it blends with the solid - // background color of the absolutely-positioned element. - .group_hover("", |style| { - style.bg(cx.theme().colors().ghost_element_hover) - }) - .child(messages_button(cx)) - .child(notes_button(cx)), - ), + .child( + IconButton::new("channel_notes", IconName::File) + .style(ButtonStyle::Filled) + .size(ButtonSize::Compact) + .icon_size(IconSize::Small) + .icon_color(if has_notes_notification { + Color::Default + } else { + Color::Muted + }) + .on_click(cx.listener(move |this, _, cx| { + this.open_channel_notes(channel_id, cx) + })) + .tooltip(|cx| Tooltip::text("Open channel notes", cx)) + .when(!has_notes_notification, |this| { + this.visible_on_hover("") + }), + ), ), ) .tooltip(|cx| Tooltip::text("Join channel", cx)) @@ -2382,7 +2377,7 @@ impl CollabPanel { .indent_level(depth + 1) .indent_step_size(px(20.)) .start_slot( - IconElement::new(Icon::Hash) + Icon::new(IconName::Hash) .size(IconSize::Small) .color(Color::Muted), ); @@ -2394,12 +2389,7 @@ impl CollabPanel { { item.child(Label::new(pending_name)) } else { - item.child( - div() - .w_full() - .py_1() // todo!() @nate this is a px off at the default font size. - .child(self.channel_name_editor.clone()), - ) + item.child(self.channel_name_editor.clone()) } } } @@ -2501,10 +2491,10 @@ impl Panel for CollabPanel { cx.notify(); } - fn icon(&self, cx: &gpui::WindowContext) -> Option { + fn icon(&self, cx: &gpui::WindowContext) -> Option { CollaborationPanelSettings::get_global(cx) .button - .then(|| ui::Icon::Collab) + .then(|| ui::IconName::Collab) } fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> { @@ -2622,11 +2612,6 @@ impl PartialEq for ListEntry { return true; } } - ListEntry::GuestCount { .. } => { - if let ListEntry::GuestCount { .. } = other { - return true; - } - } } false } @@ -2647,11 +2632,11 @@ impl Render for DraggedChannelView { .p_1() .gap_1() .child( - IconElement::new( + Icon::new( if self.channel.visibility == proto::ChannelVisibility::Public { - Icon::Public + IconName::Public } else { - Icon::Hash + IconName::Hash }, ) .size(IconSize::Small) diff --git a/crates/collab_ui/src/collab_panel/channel_modal.rs b/crates/collab_ui/src/collab_panel/channel_modal.rs index f3ae16f7939910cef5d79581725003c7bfe1c987..8020613c1ae2a4cc2ac9e0ad61293541c451f0aa 100644 --- a/crates/collab_ui/src/collab_panel/channel_modal.rs +++ b/crates/collab_ui/src/collab_panel/channel_modal.rs @@ -168,7 +168,7 @@ impl Render for ChannelModal { .w_px() .flex_1() .gap_1() - .child(IconElement::new(Icon::Hash).size(IconSize::Medium)) + .child(Icon::new(IconName::Hash).size(IconSize::Medium)) .child(Label::new(channel_name)), ) .child( @@ -406,7 +406,7 @@ impl PickerDelegate for ChannelModalDelegate { Some(ChannelRole::Guest) => Some(Label::new("Guest")), _ => None, }) - .child(IconButton::new("ellipsis", Icon::Ellipsis)) + .child(IconButton::new("ellipsis", IconName::Ellipsis)) .children( if let (Some((menu, _)), true) = (&self.context_menu, selected) { Some( diff --git a/crates/collab_ui/src/collab_panel/contact_finder.rs b/crates/collab_ui/src/collab_panel/contact_finder.rs index dbcacef7d645de6e30959b7b4d0b91b2f41aceed..b769ec7e7f394fb7a94c38f6220b507d43e77ba1 100644 --- a/crates/collab_ui/src/collab_panel/contact_finder.rs +++ b/crates/collab_ui/src/collab_panel/contact_finder.rs @@ -155,9 +155,7 @@ impl PickerDelegate for ContactFinderDelegate { .selected(selected) .start_slot(Avatar::new(user.avatar_uri.clone())) .child(Label::new(user.github_login.clone())) - .end_slot::( - icon_path.map(|icon_path| IconElement::from_path(icon_path)), - ), + .end_slot::(icon_path.map(|icon_path| Icon::from_path(icon_path))), ) } } diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 6ccad2db0d107f4ee877ecdfd38563e880a79be5..03dfd450704153b31c52ec3d0baad0d215389190 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -15,7 +15,7 @@ use std::sync::Arc; use theme::{ActiveTheme, PlayerColors}; use ui::{ h_stack, popover_menu, prelude::*, Avatar, Button, ButtonLike, ButtonStyle, ContextMenu, Icon, - IconButton, IconElement, TintColor, Tooltip, + IconButton, IconName, TintColor, Tooltip, }; use util::ResultExt; use vcs_menu::{build_branch_list, BranchList, OpenRecent as ToggleVcsMenu}; @@ -41,12 +41,6 @@ pub fn init(cx: &mut AppContext) { workspace.set_titlebar_item(titlebar_item.into(), cx) }) .detach(); - // todo!() - // cx.add_action(CollabTitlebarItem::share_project); - // cx.add_action(CollabTitlebarItem::unshare_project); - // cx.add_action(CollabTitlebarItem::toggle_user_menu); - // cx.add_action(CollabTitlebarItem::toggle_vcs_menu); - // cx.add_action(CollabTitlebarItem::toggle_project_menu); } pub struct CollabTitlebarItem { @@ -213,7 +207,7 @@ impl Render for CollabTitlebarItem { .child( div() .child( - IconButton::new("leave-call", ui::Icon::Exit) + IconButton::new("leave-call", ui::IconName::Exit) .style(ButtonStyle::Subtle) .tooltip(|cx| Tooltip::text("Leave call", cx)) .icon_size(IconSize::Small) @@ -230,9 +224,9 @@ impl Render for CollabTitlebarItem { IconButton::new( "mute-microphone", if is_muted { - ui::Icon::MicMute + ui::IconName::MicMute } else { - ui::Icon::Mic + ui::IconName::Mic }, ) .tooltip(move |cx| { @@ -256,9 +250,9 @@ impl Render for CollabTitlebarItem { IconButton::new( "mute-sound", if is_deafened { - ui::Icon::AudioOff + ui::IconName::AudioOff } else { - ui::Icon::AudioOn + ui::IconName::AudioOn }, ) .style(ButtonStyle::Subtle) @@ -281,7 +275,7 @@ impl Render for CollabTitlebarItem { ) .when(!read_only, |this| { this.child( - IconButton::new("screen-share", ui::Icon::Screen) + IconButton::new("screen-share", ui::IconName::Screen) .style(ButtonStyle::Subtle) .icon_size(IconSize::Small) .selected(is_screen_sharing) @@ -573,7 +567,7 @@ impl CollabTitlebarItem { | client::Status::ReconnectionError { .. } => Some( div() .id("disconnected") - .child(IconElement::new(Icon::Disconnected).size(IconSize::Small)) + .child(Icon::new(IconName::Disconnected).size(IconSize::Small)) .tooltip(|cx| Tooltip::text("Disconnected", cx)) .into_any_element(), ), @@ -643,7 +637,7 @@ impl CollabTitlebarItem { h_stack() .gap_0p5() .child(Avatar::new(user.avatar_uri.clone())) - .child(IconElement::new(Icon::ChevronDown).color(Color::Muted)), + .child(Icon::new(IconName::ChevronDown).color(Color::Muted)), ) .style(ButtonStyle::Subtle) .tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)), @@ -665,7 +659,7 @@ impl CollabTitlebarItem { .child( h_stack() .gap_0p5() - .child(IconElement::new(Icon::ChevronDown).color(Color::Muted)), + .child(Icon::new(IconName::ChevronDown).color(Color::Muted)), ) .style(ButtonStyle::Subtle) .tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)), diff --git a/crates/collab_ui/src/notification_panel.rs b/crates/collab_ui/src/notification_panel.rs index e7c94984b229165aa26a43000221446d7b56e7a5..95473044a3f4242cd497ce0087fe1e47e8865d6f 100644 --- a/crates/collab_ui/src/notification_panel.rs +++ b/crates/collab_ui/src/notification_panel.rs @@ -19,7 +19,7 @@ use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsStore}; use std::{sync::Arc, time::Duration}; use time::{OffsetDateTime, UtcOffset}; -use ui::{h_stack, prelude::*, v_stack, Avatar, Button, Icon, IconButton, IconElement, Label}; +use ui::{h_stack, prelude::*, v_stack, Avatar, Button, Icon, IconButton, IconName, Label}; use util::{ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, @@ -553,7 +553,7 @@ impl Render for NotificationPanel { .border_b_1() .border_color(cx.theme().colors().border) .child(Label::new("Notifications")) - .child(IconElement::new(Icon::Envelope)), + .child(Icon::new(IconName::Envelope)), ) .map(|this| { if self.client.user_id().is_none() { @@ -564,7 +564,7 @@ impl Render for NotificationPanel { .child( Button::new("sign_in_prompt_button", "Sign in") .icon_color(Color::Muted) - .icon(Icon::Github) + .icon(IconName::Github) .icon_position(IconPosition::Start) .style(ButtonStyle::Filled) .full_width() @@ -655,10 +655,10 @@ impl Panel for NotificationPanel { } } - fn icon(&self, cx: &gpui::WindowContext) -> Option { + fn icon(&self, cx: &gpui::WindowContext) -> Option { (NotificationPanelSettings::get_global(cx).button && self.notification_store.read(cx).notification_count() > 0) - .then(|| Icon::Bell) + .then(|| IconName::Bell) } fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> { @@ -716,7 +716,7 @@ impl Render for NotificationToast { .children(user.map(|user| Avatar::new(user.avatar_uri.clone()))) .child(Label::new(self.text.clone())) .child( - IconButton::new("close", Icon::Close) + IconButton::new("close", IconName::Close) .on_click(cx.listener(|_, _, cx| cx.emit(DismissEvent))), ) .on_click(cx.listener(|this, _, cx| { diff --git a/crates/collab_ui/src/notifications.rs b/crates/collab_ui/src/notifications.rs index 5c184ec5c86ab268f4455b21d855ca118d40d50b..7759fef52059fb47dbc74a3803be309dded114c1 100644 --- a/crates/collab_ui/src/notifications.rs +++ b/crates/collab_ui/src/notifications.rs @@ -1,9 +1,16 @@ +mod collab_notification; +pub mod incoming_call_notification; +pub mod project_shared_notification; + +#[cfg(feature = "stories")] +mod stories; + use gpui::AppContext; use std::sync::Arc; use workspace::AppState; -pub mod incoming_call_notification; -pub mod project_shared_notification; +#[cfg(feature = "stories")] +pub use stories::*; pub fn init(app_state: &Arc, cx: &mut AppContext) { incoming_call_notification::init(app_state, cx); diff --git a/crates/collab_ui/src/notifications/collab_notification.rs b/crates/collab_ui/src/notifications/collab_notification.rs new file mode 100644 index 0000000000000000000000000000000000000000..fa0b0a1b14782b8bbe586348487228d75df743f7 --- /dev/null +++ b/crates/collab_ui/src/notifications/collab_notification.rs @@ -0,0 +1,52 @@ +use gpui::{img, prelude::*, AnyElement, SharedUrl}; +use smallvec::SmallVec; +use ui::prelude::*; + +#[derive(IntoElement)] +pub struct CollabNotification { + avatar_uri: SharedUrl, + accept_button: Button, + dismiss_button: Button, + children: SmallVec<[AnyElement; 2]>, +} + +impl CollabNotification { + pub fn new( + avatar_uri: impl Into, + accept_button: Button, + dismiss_button: Button, + ) -> Self { + Self { + avatar_uri: avatar_uri.into(), + accept_button, + dismiss_button, + children: SmallVec::new(), + } + } +} + +impl ParentElement for CollabNotification { + fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { + &mut self.children + } +} + +impl RenderOnce for CollabNotification { + fn render(self, cx: &mut WindowContext) -> impl IntoElement { + h_stack() + .text_ui() + .justify_between() + .size_full() + .overflow_hidden() + .elevation_3(cx) + .p_2() + .gap_2() + .child(img(self.avatar_uri).w_12().h_12().rounded_full()) + .child(v_stack().overflow_hidden().children(self.children)) + .child( + v_stack() + .child(self.accept_button) + .child(self.dismiss_button), + ) + } +} diff --git a/crates/collab_ui/src/notifications/incoming_call_notification.rs b/crates/collab_ui/src/notifications/incoming_call_notification.rs index fa28ef9a6030d4009e96d170e7f7aefc46e52783..93df9a4be5445bd67072affaaf2093e7fd2a32a1 100644 --- a/crates/collab_ui/src/notifications/incoming_call_notification.rs +++ b/crates/collab_ui/src/notifications/incoming_call_notification.rs @@ -1,15 +1,12 @@ use crate::notification_window_options; +use crate::notifications::collab_notification::CollabNotification; use call::{ActiveCall, IncomingCall}; use futures::StreamExt; -use gpui::{ - img, px, AppContext, ParentElement, Render, RenderOnce, Styled, ViewContext, - VisualContext as _, WindowHandle, -}; +use gpui::{prelude::*, AppContext, WindowHandle}; use settings::Settings; use std::sync::{Arc, Weak}; use theme::ThemeSettings; -use ui::prelude::*; -use ui::{h_stack, v_stack, Button, Label}; +use ui::{prelude::*, Button, Label}; use util::ResultExt; use workspace::AppState; @@ -22,7 +19,6 @@ pub fn init(app_state: &Arc, cx: &mut AppContext) { for window in notification_windows.drain(..) { window .update(&mut cx, |_, cx| { - // todo!() cx.remove_window(); }) .log_err(); @@ -31,8 +27,8 @@ pub fn init(app_state: &Arc, cx: &mut AppContext) { if let Some(incoming_call) = incoming_call { let unique_screens = cx.update(|cx| cx.displays()).unwrap(); let window_size = gpui::Size { - width: px(380.), - height: px(64.), + width: px(400.), + height: px(72.), }; for screen in unique_screens { @@ -129,35 +125,22 @@ impl Render for IncomingCallNotification { cx.set_rem_size(ui_font_size); - h_stack() - .font(ui_font) - .text_ui() - .justify_between() - .size_full() - .overflow_hidden() - .elevation_3(cx) - .p_2() - .gap_2() - .child( - img(self.state.call.calling_user.avatar_uri.clone()) - .w_12() - .h_12() - .rounded_full(), + div().size_full().font(ui_font).child( + CollabNotification::new( + self.state.call.calling_user.avatar_uri.clone(), + Button::new("accept", "Accept").on_click({ + let state = self.state.clone(); + move |_, cx| state.respond(true, cx) + }), + Button::new("decline", "Decline").on_click({ + let state = self.state.clone(); + move |_, cx| state.respond(false, cx) + }), ) .child(v_stack().overflow_hidden().child(Label::new(format!( "{} is sharing a project in Zed", self.state.call.calling_user.github_login - )))) - .child( - v_stack() - .child(Button::new("accept", "Accept").render(cx).on_click({ - let state = self.state.clone(); - move |_, cx| state.respond(true, cx) - })) - .child(Button::new("decline", "Decline").render(cx).on_click({ - let state = self.state.clone(); - move |_, cx| state.respond(false, cx) - })), - ) + )))), + ) } } diff --git a/crates/collab_ui/src/notifications/project_shared_notification.rs b/crates/collab_ui/src/notifications/project_shared_notification.rs index 982214c3e596e7290a4837264cc570c5aa786c94..88fe540c397b65c8ddc3ad0230c47210b8bc0e7e 100644 --- a/crates/collab_ui/src/notifications/project_shared_notification.rs +++ b/crates/collab_ui/src/notifications/project_shared_notification.rs @@ -1,12 +1,13 @@ use crate::notification_window_options; +use crate::notifications::collab_notification::CollabNotification; use call::{room, ActiveCall}; use client::User; use collections::HashMap; -use gpui::{img, px, AppContext, ParentElement, Render, Size, Styled, ViewContext, VisualContext}; +use gpui::{AppContext, Size}; use settings::Settings; use std::sync::{Arc, Weak}; use theme::ThemeSettings; -use ui::{h_stack, prelude::*, v_stack, Button, Label}; +use ui::{prelude::*, Button, Label}; use workspace::AppState; pub fn init(app_state: &Arc, cx: &mut AppContext) { @@ -50,7 +51,6 @@ pub fn init(app_state: &Arc, cx: &mut AppContext) { for window in windows { window .update(cx, |_, cx| { - // todo!() cx.remove_window(); }) .ok(); @@ -63,7 +63,6 @@ pub fn init(app_state: &Arc, cx: &mut AppContext) { for window in windows { window .update(cx, |_, cx| { - // todo!() cx.remove_window(); }) .ok(); @@ -130,51 +129,30 @@ impl Render for ProjectSharedNotification { cx.set_rem_size(ui_font_size); - h_stack() - .font(ui_font) - .text_ui() - .justify_between() - .size_full() - .overflow_hidden() - .elevation_3(cx) - .p_2() - .gap_2() - .child( - img(self.owner.avatar_uri.clone()) - .w_12() - .h_12() - .rounded_full(), - ) - .child( - v_stack() - .overflow_hidden() - .child(Label::new(self.owner.github_login.clone())) - .child(Label::new(format!( - "is sharing a project in Zed{}", - if self.worktree_root_names.is_empty() { - "" - } else { - ":" - } - ))) - .children(if self.worktree_root_names.is_empty() { - None - } else { - Some(Label::new(self.worktree_root_names.join(", "))) - }), - ) - .child( - v_stack() - .child(Button::new("open", "Open").on_click(cx.listener( - move |this, _event, cx| { - this.join(cx); - }, - ))) - .child(Button::new("dismiss", "Dismiss").on_click(cx.listener( - move |this, _event, cx| { - this.dismiss(cx); - }, - ))), + div().size_full().font(ui_font).child( + CollabNotification::new( + self.owner.avatar_uri.clone(), + Button::new("open", "Open").on_click(cx.listener(move |this, _event, cx| { + this.join(cx); + })), + Button::new("dismiss", "Dismiss").on_click(cx.listener(move |this, _event, cx| { + this.dismiss(cx); + })), ) + .child(Label::new(self.owner.github_login.clone())) + .child(Label::new(format!( + "is sharing a project in Zed{}", + if self.worktree_root_names.is_empty() { + "" + } else { + ":" + } + ))) + .children(if self.worktree_root_names.is_empty() { + None + } else { + Some(Label::new(self.worktree_root_names.join(", "))) + }), + ) } } diff --git a/crates/collab_ui/src/notifications/stories.rs b/crates/collab_ui/src/notifications/stories.rs new file mode 100644 index 0000000000000000000000000000000000000000..36518679c661627346e45357161ba117a6bfe5b3 --- /dev/null +++ b/crates/collab_ui/src/notifications/stories.rs @@ -0,0 +1,3 @@ +mod collab_notification; + +pub use collab_notification::*; diff --git a/crates/collab_ui/src/notifications/stories/collab_notification.rs b/crates/collab_ui/src/notifications/stories/collab_notification.rs new file mode 100644 index 0000000000000000000000000000000000000000..c43cac46d21352ac8375e33cf530891385c36da0 --- /dev/null +++ b/crates/collab_ui/src/notifications/stories/collab_notification.rs @@ -0,0 +1,50 @@ +use gpui::prelude::*; +use story::{StoryContainer, StoryItem, StorySection}; +use ui::prelude::*; + +use crate::notifications::collab_notification::CollabNotification; + +pub struct CollabNotificationStory; + +impl Render for CollabNotificationStory { + fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { + let window_container = |width, height| div().w(px(width)).h(px(height)); + + StoryContainer::new( + "CollabNotification Story", + "crates/collab_ui/src/notifications/stories/collab_notification.rs", + ) + .child( + StorySection::new().child(StoryItem::new( + "Incoming Call Notification", + window_container(400., 72.).child( + CollabNotification::new( + "https://avatars.githubusercontent.com/u/1486634?v=4", + Button::new("accept", "Accept"), + Button::new("decline", "Decline"), + ) + .child( + v_stack() + .overflow_hidden() + .child(Label::new("maxdeviant is sharing a project in Zed")), + ), + ), + )), + ) + .child( + StorySection::new().child(StoryItem::new( + "Project Shared Notification", + window_container(400., 72.).child( + CollabNotification::new( + "https://avatars.githubusercontent.com/u/1714999?v=4", + Button::new("open", "Open"), + Button::new("dismiss", "Dismiss"), + ) + .child(Label::new("iamnbutler")) + .child(Label::new("is sharing a project in Zed:")) + .child(Label::new("zed")), + ), + )), + ) + } +} diff --git a/crates/collab_ui/src/panel_settings.rs b/crates/collab_ui/src/panel_settings.rs index 250817a803e1088e21ed8965ef9bdbaaa4bf20a5..13fa26a341a57889897e2bf20a1118813448e804 100644 --- a/crates/collab_ui/src/panel_settings.rs +++ b/crates/collab_ui/src/panel_settings.rs @@ -28,8 +28,17 @@ pub struct NotificationPanelSettings { #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] pub struct PanelSettingsContent { + /// Whether to show the panel button in the status bar. + /// + /// Default: true pub button: Option, + /// Where to dock the panel. + /// + /// Default: left pub dock: Option, + /// Default width of the panel in pixels. + /// + /// Default: 240 pub default_width: Option, } diff --git a/crates/copilot/Cargo.toml b/crates/copilot/Cargo.toml index 588c747696c3ed58696bd12fbe97f3cbf3f9ef8a..fefd49090fd2020f4dc6da8aa1c2a1c5d119ea96 100644 --- a/crates/copilot/Cargo.toml +++ b/crates/copilot/Cargo.toml @@ -28,7 +28,6 @@ theme = { path = "../theme" } lsp = { path = "../lsp" } node_runtime = { path = "../node_runtime"} util = { path = "../util" } -ui = { path = "../ui" } async-compression.workspace = true async-tar = "0.4.2" anyhow.workspace = true diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 658eb3451f473d4a9af26053dae20f21479c339e..89d1086c8e4b897c3527964289ff11810078a57c 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -1,6 +1,4 @@ pub mod request; -mod sign_in; - use anyhow::{anyhow, Context as _, Result}; use async_compression::futures::bufread::GzipDecoder; use async_tar::Archive; @@ -98,7 +96,6 @@ pub fn init( }) .detach(); - sign_in::init(cx); cx.on_action(|_: &SignIn, cx| { if let Some(copilot) = Copilot::global(cx) { copilot diff --git a/crates/copilot/src/sign_in.rs b/crates/copilot/src/sign_in.rs deleted file mode 100644 index ba5dbe0e315828ac1761cf1d76ac37bb8211ea4c..0000000000000000000000000000000000000000 --- a/crates/copilot/src/sign_in.rs +++ /dev/null @@ -1,211 +0,0 @@ -use crate::{request::PromptUserDeviceFlow, Copilot, Status}; -use gpui::{ - div, size, AppContext, Bounds, ClipboardItem, Element, GlobalPixels, InteractiveElement, - IntoElement, ParentElement, Point, Render, Styled, ViewContext, VisualContext, WindowBounds, - WindowHandle, WindowKind, WindowOptions, -}; -use theme::ActiveTheme; -use ui::{prelude::*, Button, Icon, IconElement, Label}; - -const COPILOT_SIGN_UP_URL: &'static str = "https://github.com/features/copilot"; - -pub fn init(cx: &mut AppContext) { - if let Some(copilot) = Copilot::global(cx) { - let mut verification_window: Option> = None; - cx.observe(&copilot, move |copilot, cx| { - let status = copilot.read(cx).status(); - - match &status { - crate::Status::SigningIn { prompt } => { - if let Some(window) = verification_window.as_mut() { - let updated = window - .update(cx, |verification, cx| { - verification.set_status(status.clone(), cx); - cx.activate_window(); - }) - .is_ok(); - if !updated { - verification_window = Some(create_copilot_auth_window(cx, &status)); - } - } else if let Some(_prompt) = prompt { - verification_window = Some(create_copilot_auth_window(cx, &status)); - } - } - Status::Authorized | Status::Unauthorized => { - if let Some(window) = verification_window.as_ref() { - window - .update(cx, |verification, cx| { - verification.set_status(status, cx); - cx.activate(true); - cx.activate_window(); - }) - .ok(); - } - } - _ => { - if let Some(code_verification) = verification_window.take() { - code_verification - .update(cx, |_, cx| cx.remove_window()) - .ok(); - } - } - } - }) - .detach(); - } -} - -fn create_copilot_auth_window( - cx: &mut AppContext, - status: &Status, -) -> WindowHandle { - let window_size = size(GlobalPixels::from(280.), GlobalPixels::from(280.)); - let window_options = WindowOptions { - bounds: WindowBounds::Fixed(Bounds::new(Point::default(), window_size)), - titlebar: None, - center: true, - focus: true, - show: true, - kind: WindowKind::PopUp, - is_movable: true, - display_id: None, - }; - let window = cx.open_window(window_options, |cx| { - cx.new_view(|_| CopilotCodeVerification::new(status.clone())) - }); - window -} - -pub struct CopilotCodeVerification { - status: Status, - connect_clicked: bool, -} - -impl CopilotCodeVerification { - pub fn new(status: Status) -> Self { - Self { - status, - connect_clicked: false, - } - } - - pub fn set_status(&mut self, status: Status, cx: &mut ViewContext) { - self.status = status; - cx.notify(); - } - - fn render_device_code( - data: &PromptUserDeviceFlow, - cx: &mut ViewContext, - ) -> impl IntoElement { - let copied = cx - .read_from_clipboard() - .map(|item| item.text() == &data.user_code) - .unwrap_or(false); - h_stack() - .cursor_pointer() - .justify_between() - .on_mouse_down(gpui::MouseButton::Left, { - let user_code = data.user_code.clone(); - move |_, cx| { - cx.write_to_clipboard(ClipboardItem::new(user_code.clone())); - cx.notify(); - } - }) - .child(Label::new(data.user_code.clone())) - .child(div()) - .child(Label::new(if copied { "Copied!" } else { "Copy" })) - } - - fn render_prompting_modal( - connect_clicked: bool, - data: &PromptUserDeviceFlow, - cx: &mut ViewContext, - ) -> impl Element { - let connect_button_label = if connect_clicked { - "Waiting for connection..." - } else { - "Connect to Github" - }; - v_stack() - .flex_1() - .items_center() - .justify_between() - .w_full() - .child(Label::new( - "Enable Copilot by connecting your existing license", - )) - .child(Self::render_device_code(data, cx)) - .child( - Label::new("Paste this code into GitHub after clicking the button below.") - .size(ui::LabelSize::Small), - ) - .child( - Button::new("connect-button", connect_button_label).on_click({ - let verification_uri = data.verification_uri.clone(); - cx.listener(move |this, _, cx| { - cx.open_url(&verification_uri); - this.connect_clicked = true; - }) - }), - ) - } - fn render_enabled_modal() -> impl Element { - v_stack() - .child(Label::new("Copilot Enabled!")) - .child(Label::new( - "You can update your settings or sign out from the Copilot menu in the status bar.", - )) - .child( - Button::new("copilot-enabled-done-button", "Done") - .on_click(|_, cx| cx.remove_window()), - ) - } - - fn render_unauthorized_modal() -> impl Element { - v_stack() - .child(Label::new( - "Enable Copilot by connecting your existing license.", - )) - .child( - Label::new("You must have an active Copilot license to use it in Zed.") - .color(Color::Warning), - ) - .child( - Button::new("copilot-subscribe-button", "Subscibe on Github").on_click(|_, cx| { - cx.remove_window(); - cx.open_url(COPILOT_SIGN_UP_URL) - }), - ) - } -} - -impl Render for CopilotCodeVerification { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - let prompt = match &self.status { - Status::SigningIn { - prompt: Some(prompt), - } => Self::render_prompting_modal(self.connect_clicked, &prompt, cx).into_any_element(), - Status::Unauthorized => { - self.connect_clicked = false; - Self::render_unauthorized_modal().into_any_element() - } - Status::Authorized => { - self.connect_clicked = false; - Self::render_enabled_modal().into_any_element() - } - _ => div().into_any_element(), - }; - div() - .id("copilot code verification") - .flex() - .flex_col() - .size_full() - .items_center() - .p_10() - .bg(cx.theme().colors().element_background) - .child(ui::Label::new("Connect Copilot to Zed")) - .child(IconElement::new(Icon::ZedXCopilot)) - .child(prompt) - } -} diff --git a/crates/copilot_button/Cargo.toml b/crates/copilot_ui/Cargo.toml similarity index 89% rename from crates/copilot_button/Cargo.toml rename to crates/copilot_ui/Cargo.toml index 63788f9d28a5097bab1f02ab340c253b704cc599..491f4f3cdec3d2ebd20fe1d6a2536471f862b90b 100644 --- a/crates/copilot_button/Cargo.toml +++ b/crates/copilot_ui/Cargo.toml @@ -1,11 +1,11 @@ [package] -name = "copilot_button" +name = "copilot_ui" version = "0.1.0" edition = "2021" publish = false [lib] -path = "src/copilot_button.rs" +path = "src/copilot_ui.rs" doctest = false [dependencies] @@ -17,6 +17,7 @@ gpui = { path = "../gpui" } language = { path = "../language" } settings = { path = "../settings" } theme = { path = "../theme" } +ui = { path = "../ui" } util = { path = "../util" } workspace = {path = "../workspace" } anyhow.workspace = true diff --git a/crates/copilot_button/src/copilot_button.rs b/crates/copilot_ui/src/copilot_button.rs similarity index 95% rename from crates/copilot_button/src/copilot_button.rs rename to crates/copilot_ui/src/copilot_button.rs index 60b25fee12ab8c32ca6894fb2363f271f27246a4..e5a1a942358a20c72fbb1037413796aeb84be77a 100644 --- a/crates/copilot_button/src/copilot_button.rs +++ b/crates/copilot_ui/src/copilot_button.rs @@ -1,3 +1,4 @@ +use crate::sign_in::CopilotCodeVerification; use anyhow::Result; use copilot::{Copilot, SignOut, Status}; use editor::{scroll::autoscroll::Autoscroll, Editor}; @@ -16,7 +17,9 @@ use util::{paths, ResultExt}; use workspace::{ create_and_open_local_file, item::ItemHandle, - ui::{popover_menu, ButtonCommon, Clickable, ContextMenu, Icon, IconButton, IconSize, Tooltip}, + ui::{ + popover_menu, ButtonCommon, Clickable, ContextMenu, IconButton, IconName, IconSize, Tooltip, + }, StatusItemView, Toast, Workspace, }; use zed_actions::OpenBrowser; @@ -50,15 +53,15 @@ impl Render for CopilotButton { .unwrap_or_else(|| all_language_settings.copilot_enabled(None, None)); let icon = match status { - Status::Error(_) => Icon::CopilotError, + Status::Error(_) => IconName::CopilotError, Status::Authorized => { if enabled { - Icon::Copilot + IconName::Copilot } else { - Icon::CopilotDisabled + IconName::CopilotDisabled } } - _ => Icon::CopilotInit, + _ => IconName::CopilotInit, }; if let Status::Error(e) = status { @@ -331,7 +334,9 @@ fn initiate_sign_in(cx: &mut WindowContext) { return; }; let status = copilot.read(cx).status(); - + let Some(workspace) = cx.window_handle().downcast::() else { + return; + }; match status { Status::Starting { task } => { let Some(workspace) = cx.window_handle().downcast::() else { @@ -370,9 +375,12 @@ fn initiate_sign_in(cx: &mut WindowContext) { .detach(); } _ => { - copilot - .update(cx, |copilot, cx| copilot.sign_in(cx)) - .detach_and_log_err(cx); + copilot.update(cx, |this, cx| this.sign_in(cx)).detach(); + workspace + .update(cx, |this, cx| { + this.toggle_modal(cx, |cx| CopilotCodeVerification::new(&copilot, cx)); + }) + .ok(); } } } diff --git a/crates/copilot_ui/src/copilot_ui.rs b/crates/copilot_ui/src/copilot_ui.rs new file mode 100644 index 0000000000000000000000000000000000000000..64dd068d5aff5f0910e0ed78ea2746f0f6540189 --- /dev/null +++ b/crates/copilot_ui/src/copilot_ui.rs @@ -0,0 +1,5 @@ +mod copilot_button; +mod sign_in; + +pub use copilot_button::*; +pub use sign_in::*; diff --git a/crates/copilot_ui/src/sign_in.rs b/crates/copilot_ui/src/sign_in.rs new file mode 100644 index 0000000000000000000000000000000000000000..ba6f54b634a0e2f9f14ce296423fc905d40bf744 --- /dev/null +++ b/crates/copilot_ui/src/sign_in.rs @@ -0,0 +1,183 @@ +use copilot::{request::PromptUserDeviceFlow, Copilot, Status}; +use gpui::{ + div, svg, AppContext, ClipboardItem, DismissEvent, Element, EventEmitter, FocusHandle, + FocusableView, InteractiveElement, IntoElement, Model, ParentElement, Render, Styled, + Subscription, ViewContext, +}; +use ui::{prelude::*, Button, IconName, Label}; +use workspace::ModalView; + +const COPILOT_SIGN_UP_URL: &'static str = "https://github.com/features/copilot"; + +pub struct CopilotCodeVerification { + status: Status, + connect_clicked: bool, + focus_handle: FocusHandle, + _subscription: Subscription, +} + +impl FocusableView for CopilotCodeVerification { + fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle { + self.focus_handle.clone() + } +} + +impl EventEmitter for CopilotCodeVerification {} +impl ModalView for CopilotCodeVerification {} + +impl CopilotCodeVerification { + pub(crate) fn new(copilot: &Model, cx: &mut ViewContext) -> Self { + let status = copilot.read(cx).status(); + Self { + status, + connect_clicked: false, + focus_handle: cx.focus_handle(), + _subscription: cx.observe(copilot, |this, copilot, cx| { + let status = copilot.read(cx).status(); + match status { + Status::Authorized | Status::Unauthorized | Status::SigningIn { .. } => { + this.set_status(status, cx) + } + _ => cx.emit(DismissEvent), + } + }), + } + } + + pub fn set_status(&mut self, status: Status, cx: &mut ViewContext) { + self.status = status; + cx.notify(); + } + + fn render_device_code( + data: &PromptUserDeviceFlow, + cx: &mut ViewContext, + ) -> impl IntoElement { + let copied = cx + .read_from_clipboard() + .map(|item| item.text() == &data.user_code) + .unwrap_or(false); + h_stack() + .w_full() + .p_1() + .border() + .border_muted(cx) + .rounded_md() + .cursor_pointer() + .justify_between() + .on_mouse_down(gpui::MouseButton::Left, { + let user_code = data.user_code.clone(); + move |_, cx| { + cx.write_to_clipboard(ClipboardItem::new(user_code.clone())); + cx.notify(); + } + }) + .child(div().flex_1().child(Label::new(data.user_code.clone()))) + .child(div().flex_none().px_1().child(Label::new(if copied { + "Copied!" + } else { + "Copy" + }))) + } + + fn render_prompting_modal( + connect_clicked: bool, + data: &PromptUserDeviceFlow, + cx: &mut ViewContext, + ) -> impl Element { + let connect_button_label = if connect_clicked { + "Waiting for connection..." + } else { + "Connect to Github" + }; + v_stack() + .flex_1() + .gap_2() + .items_center() + .child(Headline::new("Use Github Copilot in Zed.").size(HeadlineSize::Large)) + .child( + Label::new("Using Copilot requres an active subscription on Github.") + .color(Color::Muted), + ) + .child(Self::render_device_code(data, cx)) + .child( + Label::new("Paste this code into GitHub after clicking the button below.") + .size(ui::LabelSize::Small), + ) + .child( + Button::new("connect-button", connect_button_label) + .on_click({ + let verification_uri = data.verification_uri.clone(); + cx.listener(move |this, _, cx| { + cx.open_url(&verification_uri); + this.connect_clicked = true; + }) + }) + .full_width() + .style(ButtonStyle::Filled), + ) + } + fn render_enabled_modal(cx: &mut ViewContext) -> impl Element { + v_stack() + .gap_2() + .child(Headline::new("Copilot Enabled!").size(HeadlineSize::Large)) + .child(Label::new( + "You can update your settings or sign out from the Copilot menu in the status bar.", + )) + .child( + Button::new("copilot-enabled-done-button", "Done") + .full_width() + .on_click(cx.listener(|_, _, cx| cx.emit(DismissEvent))), + ) + } + + fn render_unauthorized_modal() -> impl Element { + v_stack() + .child(Headline::new("You must have an active GitHub Copilot subscription.").size(HeadlineSize::Large)) + + .child(Label::new( + "You can enable Copilot by connecting your existing license once you have subscribed or renewed your subscription.", + ).color(Color::Warning)) + .child( + Button::new("copilot-subscribe-button", "Subscibe on Github") + .full_width() + .on_click(|_, cx| cx.open_url(COPILOT_SIGN_UP_URL)), + ) + } +} + +impl Render for CopilotCodeVerification { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + let prompt = match &self.status { + Status::SigningIn { + prompt: Some(prompt), + } => Self::render_prompting_modal(self.connect_clicked, &prompt, cx).into_any_element(), + Status::Unauthorized => { + self.connect_clicked = false; + Self::render_unauthorized_modal().into_any_element() + } + Status::Authorized => { + self.connect_clicked = false; + Self::render_enabled_modal(cx).into_any_element() + } + _ => div().into_any_element(), + }; + + v_stack() + .id("copilot code verification") + .elevation_3(cx) + .w_96() + .items_center() + .p_4() + .gap_2() + .child( + svg() + .w_32() + .h_16() + .flex_none() + .path(IconName::ZedXCopilot.path()) + .text_color(cx.theme().colors().icon), + ) + .child(prompt) + } +} diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 613fadf7f70caa6e35a7ecd739c8e16f6237be80..844a44c54f8bcf6eeae17ab3e629b4dee6e4a04b 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -36,7 +36,7 @@ use std::{ }; use theme::ActiveTheme; pub use toolbar_controls::ToolbarControls; -use ui::{h_stack, prelude::*, Icon, IconElement, Label}; +use ui::{h_stack, prelude::*, Icon, IconName, Label}; use util::TryFutureExt; use workspace::{ item::{BreadcrumbText, Item, ItemEvent, ItemHandle}, @@ -660,7 +660,7 @@ impl Item for ProjectDiagnosticsEditor { then.child( h_stack() .gap_1() - .child(IconElement::new(Icon::XCircle).color(Color::Error)) + .child(Icon::new(IconName::XCircle).color(Color::Error)) .child(Label::new(self.summary.error_count.to_string()).color( if selected { Color::Default @@ -674,9 +674,7 @@ impl Item for ProjectDiagnosticsEditor { then.child( h_stack() .gap_1() - .child( - IconElement::new(Icon::ExclamationTriangle).color(Color::Warning), - ) + .child(Icon::new(IconName::ExclamationTriangle).color(Color::Warning)) .child(Label::new(self.summary.warning_count.to_string()).color( if selected { Color::Default @@ -816,10 +814,10 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock { .flex_none() .map(|icon| { if diagnostic.severity == DiagnosticSeverity::ERROR { - icon.path(Icon::XCircle.path()) + icon.path(IconName::XCircle.path()) .text_color(Color::Error.color(cx)) } else { - icon.path(Icon::ExclamationTriangle.path()) + icon.path(IconName::ExclamationTriangle.path()) .text_color(Color::Warning.color(cx)) } }), diff --git a/crates/diagnostics/src/items.rs b/crates/diagnostics/src/items.rs index 0c2d673d8e68b5bd43681788bde078442ed43d9d..035b84e1020048cd7c6d2cd107577b7c79786169 100644 --- a/crates/diagnostics/src/items.rs +++ b/crates/diagnostics/src/items.rs @@ -6,7 +6,7 @@ use gpui::{ }; use language::Diagnostic; use lsp::LanguageServerId; -use ui::{h_stack, prelude::*, Button, ButtonLike, Color, Icon, IconElement, Label, Tooltip}; +use ui::{h_stack, prelude::*, Button, ButtonLike, Color, Icon, IconName, Label, Tooltip}; use workspace::{item::ItemHandle, StatusItemView, ToolbarItemEvent, Workspace}; use crate::{Deploy, ProjectDiagnosticsEditor}; @@ -25,7 +25,7 @@ impl Render for DiagnosticIndicator { let diagnostic_indicator = match (self.summary.error_count, self.summary.warning_count) { (0, 0) => h_stack().map(|this| { this.child( - IconElement::new(Icon::Check) + Icon::new(IconName::Check) .size(IconSize::Small) .color(Color::Default), ) @@ -33,7 +33,7 @@ impl Render for DiagnosticIndicator { (0, warning_count) => h_stack() .gap_1() .child( - IconElement::new(Icon::ExclamationTriangle) + Icon::new(IconName::ExclamationTriangle) .size(IconSize::Small) .color(Color::Warning), ) @@ -41,7 +41,7 @@ impl Render for DiagnosticIndicator { (error_count, 0) => h_stack() .gap_1() .child( - IconElement::new(Icon::XCircle) + Icon::new(IconName::XCircle) .size(IconSize::Small) .color(Color::Error), ) @@ -49,13 +49,13 @@ impl Render for DiagnosticIndicator { (error_count, warning_count) => h_stack() .gap_1() .child( - IconElement::new(Icon::XCircle) + Icon::new(IconName::XCircle) .size(IconSize::Small) .color(Color::Error), ) .child(Label::new(error_count.to_string()).size(LabelSize::Small)) .child( - IconElement::new(Icon::ExclamationTriangle) + Icon::new(IconName::ExclamationTriangle) .size(IconSize::Small) .color(Color::Warning), ) @@ -66,7 +66,7 @@ impl Render for DiagnosticIndicator { Some( h_stack() .gap_2() - .child(IconElement::new(Icon::ArrowCircle).size(IconSize::Small)) + .child(Icon::new(IconName::ArrowCircle).size(IconSize::Small)) .child( Label::new("Checking…") .size(LabelSize::Small) diff --git a/crates/diagnostics/src/project_diagnostics_settings.rs b/crates/diagnostics/src/project_diagnostics_settings.rs index f762d2b1e626f8be1394ccecf2b2d683ca7ea437..d0feeeb3a75a2e395c8f5da12255c0232ffef585 100644 --- a/crates/diagnostics/src/project_diagnostics_settings.rs +++ b/crates/diagnostics/src/project_diagnostics_settings.rs @@ -6,8 +6,12 @@ pub struct ProjectDiagnosticsSettings { pub include_warnings: bool, } +/// Diagnostics configuration. #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] pub struct ProjectDiagnosticsSettingsContent { + /// Whether to show warnings or not by default. + /// + /// Default: true include_warnings: Option, } diff --git a/crates/diagnostics/src/toolbar_controls.rs b/crates/diagnostics/src/toolbar_controls.rs index 897e2ccf40f573591d7b9e712e65928a17b28413..3c09e3fad91b952a2447cd78d30645edd3a9c44a 100644 --- a/crates/diagnostics/src/toolbar_controls.rs +++ b/crates/diagnostics/src/toolbar_controls.rs @@ -1,7 +1,7 @@ use crate::ProjectDiagnosticsEditor; use gpui::{div, EventEmitter, ParentElement, Render, ViewContext, WeakView}; use ui::prelude::*; -use ui::{Icon, IconButton, Tooltip}; +use ui::{IconButton, IconName, Tooltip}; use workspace::{item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView}; pub struct ToolbarControls { @@ -24,7 +24,7 @@ impl Render for ToolbarControls { }; div().child( - IconButton::new("toggle-warnings", Icon::ExclamationTriangle) + IconButton::new("toggle-warnings", IconName::ExclamationTriangle) .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()) { diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 4511ffe407849162b603c4b3a44d51e8d552c1c9..4f2d5179dbd08fedd38f46ea23f919d6c30147c8 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -1015,7 +1015,6 @@ pub mod tests { .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) .unwrap_or(10); - let _test_platform = &cx.test_platform; let mut tab_size = rng.gen_range(1..=4); let buffer_start_excerpt_header_height = rng.gen_range(1..=5); let excerpt_header_height = rng.gen_range(1..=5); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 231f76218a44125e6c42f2a99f98027f98414ab1..66687377bda8d9d217ea6c5acc9b6c5dc6acc186 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -99,8 +99,8 @@ use sum_tree::TreeMap; use text::{OffsetUtf16, Rope}; use theme::{ActiveTheme, PlayerColor, StatusColors, SyntaxTheme, ThemeColors, ThemeSettings}; use ui::{ - h_stack, prelude::*, ButtonSize, ButtonStyle, Icon, IconButton, IconSize, ListItem, Popover, - Tooltip, + h_stack, prelude::*, ButtonSize, ButtonStyle, IconButton, IconName, IconSize, ListItem, + Popover, Tooltip, }; use util::{post_inc, RangeExt, ResultExt, TryFutureExt}; use workspace::{searchable::SearchEvent, ItemNavHistory, Pane, SplitDirection, ViewId, Workspace}; @@ -507,7 +507,7 @@ pub enum SoftWrap { Column(u32), } -#[derive(Clone, Default)] +#[derive(Clone)] pub struct EditorStyle { pub background: Hsla, pub local_player: PlayerColor, @@ -519,6 +519,24 @@ pub struct EditorStyle { pub suggestions_style: HighlightStyle, } +impl Default for EditorStyle { + fn default() -> Self { + Self { + background: Hsla::default(), + local_player: PlayerColor::default(), + text: TextStyle::default(), + scrollbar_width: Pixels::default(), + syntax: Default::default(), + // HACK: Status colors don't have a real default. + // We should look into removing the status colors from the editor + // style and retrieve them directly from the theme. + status: StatusColors::dark(), + inlays_style: HighlightStyle::default(), + suggestions_style: HighlightStyle::default(), + } + } +} + type CompletionId = usize; // type GetFieldEditorTheme = dyn Fn(&theme::Theme) -> theme::FieldEditor; @@ -1811,10 +1829,6 @@ impl Editor { this.end_selection(cx); this.scroll_manager.show_scrollbar(cx); - // todo!("use a different mechanism") - // let editor_created_event = EditorCreated(cx.handle()); - // cx.emit_global(editor_created_event); - if mode == EditorMode::Full { let should_auto_hide_scrollbars = cx.should_auto_hide_scrollbars(); cx.set_global(ScrollbarAutoHide(should_auto_hide_scrollbars)); @@ -4223,7 +4237,7 @@ impl Editor { ) -> Option { if self.available_code_actions.is_some() { Some( - IconButton::new("code_actions_indicator", ui::Icon::Bolt) + IconButton::new("code_actions_indicator", ui::IconName::Bolt) .icon_size(IconSize::Small) .icon_color(Color::Muted) .selected(is_active) @@ -4257,7 +4271,7 @@ impl Editor { fold_data .map(|(fold_status, buffer_row, active)| { (active || gutter_hovered || fold_status == FoldStatus::Folded).then(|| { - IconButton::new(ix as usize, ui::Icon::ChevronDown) + IconButton::new(ix as usize, ui::IconName::ChevronDown) .on_click(cx.listener(move |editor, _e, cx| match fold_status { FoldStatus::Folded => { editor.unfold_at(&UnfoldAt { buffer_row }, cx); @@ -4269,7 +4283,7 @@ impl Editor { .icon_color(ui::Color::Muted) .icon_size(ui::IconSize::Small) .selected(fold_status == FoldStatus::Folded) - .selected_icon(ui::Icon::ChevronRight) + .selected_icon(ui::IconName::ChevronRight) .size(ui::ButtonSize::None) }) }) @@ -7036,7 +7050,7 @@ impl Editor { let buffer = self.buffer.read(cx).snapshot(cx); let selection = self.selections.newest::(cx); - // If there is an active Diagnostic Popover. Jump to it's diagnostic instead. + // If there is an active Diagnostic Popover jump to its diagnostic instead. if direction == Direction::Next { if let Some(popover) = self.hover_state.diagnostic_popover.as_ref() { let (group_id, jump_to) = popover.activation_info(); @@ -9739,7 +9753,7 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, _is_valid: bool) -> Ren ), ) .child( - IconButton::new(("copy-block", cx.block_id), Icon::Copy) + IconButton::new(("copy-block", cx.block_id), IconName::Copy) .icon_color(Color::Muted) .size(ButtonSize::Compact) .style(ButtonStyle::Transparent) diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index fd7e2feea31a07f0b35184087508c76e652b9c26..212ce9fd34d776047fce97f5cc32b63cfc207c65 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -14,11 +14,15 @@ pub struct EditorSettings { pub seed_search_query_from_cursor: SeedQuerySetting, } +/// When to populate a new search's query based on the text under the cursor. #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum SeedQuerySetting { + /// Always populate the search query with the word under the cursor. Always, + /// Only populate the search query when there is text selected. Selection, + /// Never populate the search query Never, } @@ -29,31 +33,75 @@ pub struct Scrollbar { pub selections: bool, } +/// When to show the scrollbar in the editor. +/// +/// Default: auto #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum ShowScrollbar { + /// Show the scrollbar if there's important information or + /// follow the system's configured behavior. Auto, + /// Match the system's configured behavior. System, + /// Always show the scrollbar. Always, + /// Never show the scrollbar. Never, } #[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] pub struct EditorSettingsContent { + /// Whether the cursor blinks in the editor. + /// + /// Default: true pub cursor_blink: Option, + /// Whether to show the informational hover box when moving the mouse + /// over symbols in the editor. + /// + /// Default: true pub hover_popover_enabled: Option, + /// Whether to pop the completions menu while typing in an editor without + /// explicitly requesting it. + /// + /// Default: true pub show_completions_on_input: Option, + /// Whether to display inline and alongside documentation for items in the + /// completions menu. + /// + /// Default: true pub show_completion_documentation: Option, + /// Whether to use additional LSP queries to format (and amend) the code after + /// every "trigger" symbol input, defined by LSP server capabilities. + /// + /// Default: true pub use_on_type_format: Option, + /// Scrollbar related settings pub scrollbar: Option, + /// Whether the line numbers on editors gutter are relative or not. + /// + /// Default: false pub relative_line_numbers: Option, + /// When to populate a new search's query based on the text under the cursor. + /// + /// Default: always pub seed_search_query_from_cursor: Option, } +/// Scrollbar related settings #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] pub struct ScrollbarContent { + /// When to show the scrollbar in the editor. + /// + /// Default: auto pub show: Option, + /// Whether to show git diff indicators in the scrollbar. + /// + /// Default: true pub git_diff: Option, + /// Whether to show buffer search result markers in the scrollbar. + /// + /// Default: true pub selections: Option, } diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 66f28db3e463d39d14cff2ab3ab1890e6e98995f..520c3714d3d529dbcd2df4d4cc4d750db2a7a53c 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -539,7 +539,6 @@ fn test_clone(cx: &mut TestAppContext) { ); } -//todo!(editor navigate) #[gpui::test] async fn test_navigation_history(cx: &mut TestAppContext) { init_test(cx, |_| {}); @@ -993,7 +992,6 @@ fn test_move_cursor_multibyte(cx: &mut TestAppContext) { }); } -//todo!(finish editor tests) #[gpui::test] fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) { init_test(cx, |_| {}); @@ -1259,7 +1257,6 @@ fn test_prev_next_word_boundary(cx: &mut TestAppContext) { }); } -//todo!(finish editor tests) #[gpui::test] fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) { init_test(cx, |_| {}); @@ -1318,7 +1315,6 @@ fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) { }); } -//todo!(simulate_resize) #[gpui::test] async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); @@ -2546,7 +2542,6 @@ fn test_delete_line(cx: &mut TestAppContext) { }); } -//todo!(select_anchor_ranges) #[gpui::test] fn test_join_lines_with_single_selection(cx: &mut TestAppContext) { init_test(cx, |_| {}); @@ -3114,7 +3109,6 @@ fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) { }); } -//todo!(test_transpose) #[gpui::test] fn test_transpose(cx: &mut TestAppContext) { init_test(cx, |_| {}); @@ -4860,7 +4854,6 @@ async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) { }); } -// todo!(select_anchor_ranges) #[gpui::test] async fn test_snippets(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); @@ -6455,7 +6448,6 @@ fn test_highlighted_ranges(cx: &mut TestAppContext) { }); } -// todo!(following) #[gpui::test] async fn test_following(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); @@ -7094,7 +7086,6 @@ async fn test_move_to_enclosing_bracket(cx: &mut gpui::TestAppContext) { ); } -// todo!(completions) #[gpui::test(iterations = 10)] async fn test_copilot(executor: BackgroundExecutor, cx: &mut gpui::TestAppContext) { // flaky diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 53a376c2842937a6029cfe6c848b9998153a8d77..7efb43bd4852fc7ef1d8e5a5a43a79ad72a0303e 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -28,7 +28,7 @@ use gpui::{ AnchorCorner, AnyElement, AvailableSpace, BorrowWindow, Bounds, ContentMask, Corners, CursorStyle, DispatchPhase, Edges, Element, ElementInputHandler, Hsla, InteractiveBounds, InteractiveElement, IntoElement, ModifiersChangedEvent, MouseButton, MouseDownEvent, - MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, ScrollWheelEvent, ShapedLine, + MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, ScrollDelta, ScrollWheelEvent, ShapedLine, SharedString, Size, StackingOrder, StatefulInteractiveElement, Style, Styled, TextRun, TextStyle, View, ViewContext, WindowContext, }; @@ -581,41 +581,6 @@ impl EditorElement { } } - fn scroll( - editor: &mut Editor, - event: &ScrollWheelEvent, - position_map: &PositionMap, - bounds: &InteractiveBounds, - cx: &mut ViewContext, - ) { - if !bounds.visibly_contains(&event.position, cx) { - return; - } - - let line_height = position_map.line_height; - let max_glyph_width = position_map.em_width; - let (delta, axis) = match event.delta { - gpui::ScrollDelta::Pixels(mut pixels) => { - //Trackpad - let axis = position_map.snapshot.ongoing_scroll.filter(&mut pixels); - (pixels, axis) - } - - gpui::ScrollDelta::Lines(lines) => { - //Not trackpad - let pixels = point(lines.x * max_glyph_width, lines.y * line_height); - (pixels, None) - } - }; - - let scroll_position = position_map.snapshot.scroll_position(); - let x = f32::from((scroll_position.x * max_glyph_width - delta.x) / max_glyph_width); - let y = f32::from((scroll_position.y * line_height - delta.y) / line_height); - let scroll_position = point(x, y).clamp(&point(0., 0.), &position_map.scroll_max); - editor.scroll(scroll_position, axis, cx); - cx.stop_propagation(); - } - fn paint_background( &self, gutter_bounds: Bounds, @@ -839,9 +804,22 @@ impl EditorElement { let start_row = display_row_range.start; let end_row = display_row_range.end; + // If we're in a multibuffer, row range span might include an + // excerpt header, so if we were to draw the marker straight away, + // the hunk might include the rows of that header. + // Making the range inclusive doesn't quite cut it, as we rely on the exclusivity for the soft wrap. + // Instead, we simply check whether the range we're dealing with includes + // any custom elements and if so, we stop painting the diff hunk on the first row of that custom element. + let end_row_in_current_excerpt = layout + .position_map + .snapshot + .blocks_in_range(start_row..end_row) + .next() + .map(|(start_row, _)| start_row) + .unwrap_or(end_row); let start_y = start_row as f32 * line_height - scroll_top; - let end_y = end_row as f32 * line_height - scroll_top; + let end_y = end_row_in_current_excerpt as f32 * line_height - scroll_top; let width = 0.275 * line_height; let highlight_origin = bounds.origin + point(-width, start_y); @@ -850,7 +828,7 @@ impl EditorElement { cx.paint_quad(quad( highlight_bounds, Corners::all(0.05 * line_height), - color, // todo!("use the right color") + color, Edges::default(), transparent_black(), )); @@ -2450,34 +2428,78 @@ impl EditorElement { ) } - fn paint_mouse_listeners( + fn paint_scroll_wheel_listener( &mut self, - bounds: Bounds, - gutter_bounds: Bounds, - text_bounds: Bounds, + interactive_bounds: &InteractiveBounds, layout: &LayoutState, cx: &mut WindowContext, ) { - let interactive_bounds = InteractiveBounds { - bounds: bounds.intersect(&cx.content_mask().bounds), - stacking_order: cx.stacking_order().clone(), - }; - cx.on_mouse_event({ let position_map = layout.position_map.clone(); let editor = self.editor.clone(); let interactive_bounds = interactive_bounds.clone(); + let mut delta = ScrollDelta::default(); move |event: &ScrollWheelEvent, phase, cx| { if phase == DispatchPhase::Bubble && interactive_bounds.visibly_contains(&event.position, cx) { + delta = delta.coalesce(event.delta); editor.update(cx, |editor, cx| { - Self::scroll(editor, event, &position_map, &interactive_bounds, cx) + let position = event.position; + let position_map: &PositionMap = &position_map; + let bounds = &interactive_bounds; + if !bounds.visibly_contains(&position, cx) { + return; + } + + let line_height = position_map.line_height; + let max_glyph_width = position_map.em_width; + let (delta, axis) = match delta { + gpui::ScrollDelta::Pixels(mut pixels) => { + //Trackpad + let axis = position_map.snapshot.ongoing_scroll.filter(&mut pixels); + (pixels, axis) + } + + gpui::ScrollDelta::Lines(lines) => { + //Not trackpad + let pixels = + point(lines.x * max_glyph_width, lines.y * line_height); + (pixels, None) + } + }; + + let scroll_position = position_map.snapshot.scroll_position(); + let x = f32::from( + (scroll_position.x * max_glyph_width - delta.x) / max_glyph_width, + ); + let y = + f32::from((scroll_position.y * line_height - delta.y) / line_height); + let scroll_position = + point(x, y).clamp(&point(0., 0.), &position_map.scroll_max); + editor.scroll(scroll_position, axis, cx); + cx.stop_propagation(); }); } } }); + } + + fn paint_mouse_listeners( + &mut self, + bounds: Bounds, + gutter_bounds: Bounds, + text_bounds: Bounds, + layout: &LayoutState, + cx: &mut WindowContext, + ) { + let interactive_bounds = InteractiveBounds { + bounds: bounds.intersect(&cx.content_mask().bounds), + stacking_order: cx.stacking_order().clone(), + }; + + self.paint_scroll_wheel_listener(&interactive_bounds, layout, cx); cx.on_mouse_event({ let position_map = layout.position_map.clone(); diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 22c58056f01d660bce7a031782577904881214c4..26ce3e5cf708e0193eef2c87fbf72c27f4da806a 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -16,7 +16,7 @@ use lsp::DiagnosticSeverity; use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart, Project}; use settings::Settings; use std::{ops::Range, sync::Arc, time::Duration}; -use ui::{StyledExt, Tooltip}; +use ui::{prelude::*, Tooltip}; use util::TryFutureExt; use workspace::Workspace; @@ -514,6 +514,8 @@ impl DiagnosticPopover { None => self.local_diagnostic.diagnostic.message.clone(), }; + let status_colors = cx.theme().status(); + struct DiagnosticColors { pub background: Hsla, pub border: Hsla, @@ -521,24 +523,24 @@ impl DiagnosticPopover { let diagnostic_colors = match self.local_diagnostic.diagnostic.severity { DiagnosticSeverity::ERROR => DiagnosticColors { - background: style.status.error_background, - border: style.status.error_border, + background: status_colors.error_background, + border: status_colors.error_border, }, DiagnosticSeverity::WARNING => DiagnosticColors { - background: style.status.warning_background, - border: style.status.warning_border, + background: status_colors.warning_background, + border: status_colors.warning_border, }, DiagnosticSeverity::INFORMATION => DiagnosticColors { - background: style.status.info_background, - border: style.status.info_border, + background: status_colors.info_background, + border: status_colors.info_border, }, DiagnosticSeverity::HINT => DiagnosticColors { - background: style.status.hint_background, - border: style.status.hint_border, + background: status_colors.hint_background, + border: status_colors.hint_border, }, _ => DiagnosticColors { - background: style.status.ignored_background, - border: style.status.ignored_border, + background: status_colors.ignored_background, + border: status_colors.ignored_border, }, }; diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 0b13e25d5dd621f9d62fcf05e7d75657a3902656..72441974c3788d659084c19503ffc22740bfd005 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -95,7 +95,7 @@ pub fn up_by_rows( text_layout_details: &TextLayoutDetails, ) -> (DisplayPoint, SelectionGoal) { let mut goal_x = match goal { - SelectionGoal::HorizontalPosition(x) => x.into(), // todo!("Can the fields in SelectionGoal by Pixels? We should extract a geometry crate and depend on that.") + SelectionGoal::HorizontalPosition(x) => x.into(), SelectionGoal::WrappedHorizontalPosition((_, x)) => x.into(), SelectionGoal::HorizontalRange { end, .. } => end.into(), _ => map.x_for_display_point(start, text_layout_details), diff --git a/crates/editor/src/scroll.rs b/crates/editor/src/scroll.rs index 0798870f76cb37131b86295b57e5f3f01ad22705..bc5fe4bddd1b38d9445f69bd481145a2f3c884f7 100644 --- a/crates/editor/src/scroll.rs +++ b/crates/editor/src/scroll.rs @@ -384,10 +384,12 @@ impl Editor { ) { hide_hover(self, cx); let workspace_id = self.workspace.as_ref().map(|workspace| workspace.1); - let top_row = scroll_anchor - .anchor - .to_point(&self.buffer().read(cx).snapshot(cx)) - .row; + let snapshot = &self.buffer().read(cx).snapshot(cx); + if !scroll_anchor.anchor.is_valid(snapshot) { + log::warn!("Invalid scroll anchor: {:?}", scroll_anchor); + return; + } + let top_row = scroll_anchor.anchor.to_point(snapshot).row; self.scroll_manager .set_anchor(scroll_anchor, top_row, false, false, workspace_id, cx); } diff --git a/crates/editor/src/scroll/actions.rs b/crates/editor/src/scroll/actions.rs index 21a4258f6f186700128e081b1ab5d36c3a5938c7..436a0291d020f00c3c685d72e5e7c50934158b27 100644 --- a/crates/editor/src/scroll/actions.rs +++ b/crates/editor/src/scroll/actions.rs @@ -11,10 +11,9 @@ impl Editor { return; } - // todo!() - // if self.mouse_context_menu.read(cx).visible() { - // return None; - // } + if self.mouse_context_menu.is_some() { + return; + } if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); diff --git a/crates/editor/src/test.rs b/crates/editor/src/test.rs index 4ce539ad797b2a3064a57ca15ff8e97b0f3c307e..d3337db2581b0bc7148059457cc9eef6943cc8d1 100644 --- a/crates/editor/src/test.rs +++ b/crates/editor/src/test.rs @@ -60,8 +60,7 @@ pub fn assert_text_with_selections( #[allow(dead_code)] #[cfg(any(test, feature = "test-support"))] pub(crate) fn build_editor(buffer: Model, cx: &mut ViewContext) -> Editor { - // todo!() - Editor::new(EditorMode::Full, buffer, None, /*None,*/ cx) + Editor::new(EditorMode::Full, buffer, None, cx) } pub(crate) fn build_editor_with_project( @@ -69,6 +68,5 @@ pub(crate) fn build_editor_with_project( buffer: Model, cx: &mut ViewContext, ) -> Editor { - // todo!() - Editor::new(EditorMode::Full, buffer, Some(project), /*None,*/ cx) + Editor::new(EditorMode::Full, buffer, Some(project), cx) } diff --git a/crates/feedback/src/deploy_feedback_button.rs b/crates/feedback/src/deploy_feedback_button.rs index a02540bc5b3339e576f1a1eae336db119bd845ed..377d4cea5c11f3c3c7581e5a2d8b8034811f39e6 100644 --- a/crates/feedback/src/deploy_feedback_button.rs +++ b/crates/feedback/src/deploy_feedback_button.rs @@ -1,5 +1,5 @@ use gpui::{Render, ViewContext, WeakView}; -use ui::{prelude::*, ButtonCommon, Icon, IconButton, Tooltip}; +use ui::{prelude::*, ButtonCommon, IconButton, IconName, Tooltip}; use workspace::{item::ItemHandle, StatusItemView, Workspace}; use crate::{feedback_modal::FeedbackModal, GiveFeedback}; @@ -27,7 +27,7 @@ impl Render for DeployFeedbackButton { }) }) .is_some(); - IconButton::new("give-feedback", Icon::Envelope) + IconButton::new("give-feedback", IconName::Envelope) .style(ui::ButtonStyle::Subtle) .icon_size(IconSize::Small) .selected(is_open) diff --git a/crates/feedback/src/feedback_modal.rs b/crates/feedback/src/feedback_modal.rs index 6c5308c1c64e3b6339dbdfff714c5a317dbe7842..bf7a0715604f20ec6eae1d28ea4988016a8cd2cf 100644 --- a/crates/feedback/src/feedback_modal.rs +++ b/crates/feedback/src/feedback_modal.rs @@ -7,7 +7,7 @@ use db::kvp::KEY_VALUE_STORE; use editor::{Editor, EditorEvent}; use futures::AsyncReadExt; use gpui::{ - div, red, rems, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model, + div, rems, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model, PromptLevel, Render, Task, View, ViewContext, }; use isahc::Request; @@ -179,14 +179,13 @@ impl FeedbackModal { editor }); - // Moved here because providing it inline breaks rustfmt - let placeholder_text = - "You can use markdown to organize your feedback with code and links."; - let feedback_editor = cx.new_view(|cx| { let mut editor = Editor::for_buffer(buffer, Some(project.clone()), cx); - editor.set_placeholder_text(placeholder_text, cx); - // editor.set_show_gutter(false, cx); + editor.set_placeholder_text( + "You can use markdown to organize your feedback with code and links.", + cx, + ); + editor.set_show_gutter(false, cx); editor.set_vertical_scroll_margin(5, cx); editor }); @@ -422,10 +421,6 @@ impl Render for FeedbackModal { let open_community_repo = cx.listener(|_, _, cx| cx.dispatch_action(Box::new(OpenZedCommunityRepo))); - // Moved this here because providing it inline breaks rustfmt - let provide_an_email_address = - "Provide an email address if you want us to be able to reply."; - v_stack() .elevation_3(cx) .key_context("GiveFeedback") @@ -434,11 +429,8 @@ impl Render for FeedbackModal { .max_w(rems(96.)) .h(rems(32.)) .p_4() - .gap_4() - .child(v_stack().child( - // TODO: Add Headline component to `ui2` - div().text_xl().child("Share Feedback"), - )) + .gap_2() + .child(Headline::new("Share Feedback")) .child( Label::new(if self.character_count < *FEEDBACK_CHAR_LIMIT.start() { format!( @@ -468,17 +460,26 @@ impl Render for FeedbackModal { .child(self.feedback_editor.clone()), ) .child( - h_stack() - .bg(cx.theme().colors().editor_background) - .p_2() - .border() - .rounded_md() - .border_color(if self.valid_email_address() { - cx.theme().colors().border - } else { - red() - }) - .child(self.email_address_editor.clone()), + v_stack() + .gap_1() + .child( + h_stack() + .bg(cx.theme().colors().editor_background) + .p_2() + .border() + .rounded_md() + .border_color(if self.valid_email_address() { + cx.theme().colors().border + } else { + cx.theme().status().error_border + }) + .child(self.email_address_editor.clone()), + ) + .child( + Label::new("Provide an email address if you want us to be able to reply.") + .size(LabelSize::Small) + .color(Color::Muted), + ), ) .child( h_stack() @@ -487,7 +488,7 @@ impl Render for FeedbackModal { .child( Button::new("community_repository", "Community Repository") .style(ButtonStyle::Transparent) - .icon(Icon::ExternalLink) + .icon(IconName::ExternalLink) .icon_position(IconPosition::End) .icon_size(IconSize::Small) .on_click(open_community_repo), @@ -515,12 +516,7 @@ impl Render for FeedbackModal { this.submit(cx).detach(); })) .tooltip(move |cx| { - Tooltip::with_meta( - "Submit feedback to the Zed team.", - None, - provide_an_email_address, - cx, - ) + Tooltip::text("Submit feedback to the Zed team.", cx) }) .when(!self.can_submit(), |this| this.disabled(true)), ), diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index ce68819646c9911ff8d89037527e1743d8c59fb7..d49eb9ee603d7e819bab5097adac874107fd9a3f 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -1297,7 +1297,7 @@ mod tests { // so that one should be sorted earlier let b_path = ProjectPath { worktree_id, - path: Arc::from(Path::new("/root/dir2/b.txt")), + path: Arc::from(Path::new("dir2/b.txt")), }; workspace .update(cx, |workspace, cx| { diff --git a/crates/gpui/docs/key_dispatch.md b/crates/gpui/docs/key_dispatch.md index daf6f820cd5c98f45415c071d38518d55b269d2f..804a0b576194b2be4f23852a49369d988b9d81cb 100644 --- a/crates/gpui/docs/key_dispatch.md +++ b/crates/gpui/docs/key_dispatch.md @@ -50,7 +50,7 @@ impl Render for Menu { .on_action(|this, move: &MoveDown, cx| { // ... }) - .children(todo!()) + .children(unimplemented!()) } } ``` @@ -68,7 +68,7 @@ impl Render for Menu { .on_action(|this, move: &MoveDown, cx| { // ... }) - .children(todo!()) + .children(unimplemented!()) } } ``` diff --git a/crates/gpui/src/action.rs b/crates/gpui/src/action.rs index e335c4255e4deb0d6c5720b03ce017952a6fa229..ef02316f83ea9dfa57c68106f6b5706b755e3cd6 100644 --- a/crates/gpui/src/action.rs +++ b/crates/gpui/src/action.rs @@ -104,7 +104,7 @@ pub struct ActionData { } /// This constant must be public to be accessible from other crates. -/// But it's existence is an implementation detail and should not be used directly. +/// But its existence is an implementation detail and should not be used directly. #[doc(hidden)] #[linkme::distributed_slice] pub static __GPUI_ACTIONS: [MacroActionBuilder]; @@ -114,14 +114,26 @@ impl ActionRegistry { pub(crate) fn load_actions(&mut self) { for builder in __GPUI_ACTIONS { let action = builder(); - //todo(remove) - let name: SharedString = action.name.into(); - self.builders_by_name.insert(name.clone(), action.build); - self.names_by_type_id.insert(action.type_id, name.clone()); - self.all_names.push(name); + self.insert_action(action); } } + #[cfg(test)] + pub(crate) fn load_action(&mut self) { + self.insert_action(ActionData { + name: A::debug_name(), + type_id: TypeId::of::(), + build: A::build, + }); + } + + fn insert_action(&mut self, action: ActionData) { + let name: SharedString = action.name.into(); + self.builders_by_name.insert(name.clone(), action.build); + self.names_by_type_id.insert(action.type_id, name.clone()); + self.all_names.push(name); + } + /// Construct an action based on its name and optional JSON parameters sourced from the keymap. pub fn build_action_type(&self, type_id: &TypeId) -> Result> { let name = self @@ -203,7 +215,6 @@ macro_rules! __impl_action { ) } - // todo!() why is this needed in addition to name? fn debug_name() -> &'static str where Self: ::std::marker::Sized diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 638396abc51e97f58a93fabec816bd8d48a50135..108ad28d24a16191b6a419d09b9d56bf212c4050 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -45,11 +45,13 @@ use util::{ /// Temporary(?) wrapper around [`RefCell`] to help us debug any double borrows. /// Strongly consider removing after stabilization. +#[doc(hidden)] pub struct AppCell { app: RefCell, } impl AppCell { + #[doc(hidden)] #[track_caller] pub fn borrow(&self) -> AppRef { if option_env!("TRACK_THREAD_BORROWS").is_some() { @@ -59,6 +61,7 @@ impl AppCell { AppRef(self.app.borrow()) } + #[doc(hidden)] #[track_caller] pub fn borrow_mut(&self) -> AppRefMut { if option_env!("TRACK_THREAD_BORROWS").is_some() { @@ -69,6 +72,7 @@ impl AppCell { } } +#[doc(hidden)] #[derive(Deref, DerefMut)] pub struct AppRef<'a>(Ref<'a, AppContext>); @@ -81,6 +85,7 @@ impl<'a> Drop for AppRef<'a> { } } +#[doc(hidden)] #[derive(Deref, DerefMut)] pub struct AppRefMut<'a>(RefMut<'a, AppContext>); @@ -93,6 +98,8 @@ impl<'a> Drop for AppRefMut<'a> { } } +/// A reference to a GPUI application, typically constructed in the `main` function of your app. +/// You won't interact with this type much outside of initial configuration and startup. pub struct App(Rc); /// Represents an application before it is fully launched. Once your app is @@ -136,6 +143,8 @@ impl App { self } + /// Invokes a handler when an already-running application is launched. + /// On macOS, this can occur when the application icon is double-clicked or the app is launched via the dock. pub fn on_reopen(&self, mut callback: F) -> &Self where F: 'static + FnMut(&mut AppContext), @@ -149,18 +158,22 @@ impl App { self } + /// Returns metadata associated with the application pub fn metadata(&self) -> AppMetadata { self.0.borrow().app_metadata.clone() } + /// Returns a handle to the [`BackgroundExecutor`] associated with this app, which can be used to spawn futures in the background. pub fn background_executor(&self) -> BackgroundExecutor { self.0.borrow().background_executor.clone() } + /// Returns a handle to the [`ForegroundExecutor`] associated with this app, which can be used to spawn futures in the foreground. pub fn foreground_executor(&self) -> ForegroundExecutor { self.0.borrow().foreground_executor.clone() } + /// Returns a reference to the [`TextSystem`] associated with this app. pub fn text_system(&self) -> Arc { self.0.borrow().text_system.clone() } @@ -174,12 +187,6 @@ type QuitHandler = Box LocalBoxFuture<'static, () type ReleaseListener = Box; type NewViewListener = Box; -// struct FrameConsumer { -// next_frame_callbacks: Vec, -// task: Task<()>, -// display_linker -// } - pub struct AppContext { pub(crate) this: Weak, pub(crate) platform: Rc, @@ -292,7 +299,7 @@ impl AppContext { app } - /// Quit the application gracefully. Handlers registered with `ModelContext::on_app_quit` + /// Quit the application gracefully. Handlers registered with [`ModelContext::on_app_quit`] /// will be given 100ms to complete before exiting. pub fn shutdown(&mut self) { let mut futures = Vec::new(); @@ -314,10 +321,12 @@ impl AppContext { } } + /// Gracefully quit the application via the platform's standard routine. pub fn quit(&mut self) { self.platform.quit(); } + /// Get metadata about the app and platform. pub fn app_metadata(&self) -> AppMetadata { self.app_metadata.clone() } @@ -340,6 +349,7 @@ impl AppContext { result } + /// Arrange a callback to be invoked when the given model or view calls `notify` on its respective context. pub fn observe( &mut self, entity: &E, @@ -355,7 +365,7 @@ impl AppContext { }) } - pub fn observe_internal( + pub(crate) fn observe_internal( &mut self, entity: &E, mut on_notify: impl FnMut(E, &mut AppContext) -> bool + 'static, @@ -380,15 +390,17 @@ impl AppContext { subscription } - pub fn subscribe( + /// Arrange for the given callback to be invoked whenever the given model or view emits an event of a given type. + /// The callback is provided a handle to the emitting entity and a reference to the emitted event. + pub fn subscribe( &mut self, entity: &E, - mut on_event: impl FnMut(E, &Evt, &mut AppContext) + 'static, + mut on_event: impl FnMut(E, &Event, &mut AppContext) + 'static, ) -> Subscription where - T: 'static + EventEmitter, + T: 'static + EventEmitter, E: Entity, - Evt: 'static, + Event: 'static, { self.subscribe_internal(entity, move |entity, event, cx| { on_event(entity, event, cx); @@ -426,6 +438,9 @@ impl AppContext { subscription } + /// Returns handles to all open windows in the application. + /// Each handle could be downcast to a handle typed for the root view of that window. + /// To find all windows of a given type, you could filter on pub fn windows(&self) -> Vec { self.windows .values() @@ -565,7 +580,7 @@ impl AppContext { self.pending_effects.push_back(effect); } - /// Called at the end of AppContext::update to complete any side effects + /// Called at the end of [`AppContext::update`] to complete any side effects /// such as notifying observers, emitting events, etc. Effects can themselves /// cause effects, so we continue looping until all effects are processed. fn flush_effects(&mut self) { diff --git a/crates/gpui/src/app/async_context.rs b/crates/gpui/src/app/async_context.rs index 475ef76ef25cec97cc75b30953f9ad30ce87a5cd..6afb356e5e63c6431fe0858b4e1c4fd97159c0b2 100644 --- a/crates/gpui/src/app/async_context.rs +++ b/crates/gpui/src/app/async_context.rs @@ -82,6 +82,7 @@ impl Context for AsyncAppContext { } impl AsyncAppContext { + /// Schedules all windows in the application to be redrawn. pub fn refresh(&mut self) -> Result<()> { let app = self .app @@ -92,14 +93,17 @@ impl AsyncAppContext { Ok(()) } + /// Get an executor which can be used to spawn futures in the background. pub fn background_executor(&self) -> &BackgroundExecutor { &self.background_executor } + /// Get an executor which can be used to spawn futures in the foreground. pub fn foreground_executor(&self) -> &ForegroundExecutor { &self.foreground_executor } + /// Invoke the given function in the context of the app, then flush any effects produced during its invocation. pub fn update(&self, f: impl FnOnce(&mut AppContext) -> R) -> Result { let app = self .app @@ -109,6 +113,7 @@ impl AsyncAppContext { Ok(f(&mut lock)) } + /// Open a window with the given options based on the root view returned by the given function. pub fn open_window( &self, options: crate::WindowOptions, @@ -125,6 +130,7 @@ impl AsyncAppContext { Ok(lock.open_window(options, build_root_view)) } + /// Schedule a future to be polled in the background. pub fn spawn(&self, f: impl FnOnce(AsyncAppContext) -> Fut) -> Task where Fut: Future + 'static, diff --git a/crates/gpui/src/app/entity_map.rs b/crates/gpui/src/app/entity_map.rs index 17f6e47ddfec85de1c82b1fc0b8cd6ffc285aa32..0b213b20f769975e22761f8294251051e39e2753 100644 --- a/crates/gpui/src/app/entity_map.rs +++ b/crates/gpui/src/app/entity_map.rs @@ -19,7 +19,10 @@ use std::{ #[cfg(any(test, feature = "test-support"))] use collections::HashMap; -slotmap::new_key_type! { pub struct EntityId; } +slotmap::new_key_type! { + /// A unique identifier for a model or view across the application. + pub struct EntityId; +} impl EntityId { pub fn as_u64(self) -> u64 { diff --git a/crates/gpui/src/app/test_context.rs b/crates/gpui/src/app/test_context.rs index 0f71ea61a9ec347b01e601f5e3c1a2237b80e691..17c2a573a89e84573fc4667ae40964000c2ac3b7 100644 --- a/crates/gpui/src/app/test_context.rs +++ b/crates/gpui/src/app/test_context.rs @@ -1,3 +1,5 @@ +#![deny(missing_docs)] + use crate::{ div, Action, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext, BackgroundExecutor, ClipboardItem, Context, Entity, EventEmitter, ForegroundExecutor, @@ -9,14 +11,21 @@ use anyhow::{anyhow, bail}; use futures::{Stream, StreamExt}; use std::{future::Future, ops::Deref, rc::Rc, sync::Arc, time::Duration}; +/// A TestAppContext is provided to tests created with `#[gpui::test]`, it provides +/// an implementation of `Context` with additional methods that are useful in tests. #[derive(Clone)] pub struct TestAppContext { + #[doc(hidden)] pub app: Rc, + #[doc(hidden)] pub background_executor: BackgroundExecutor, + #[doc(hidden)] pub foreground_executor: ForegroundExecutor, + #[doc(hidden)] pub dispatcher: TestDispatcher, - pub test_platform: Rc, + test_platform: Rc, text_system: Arc, + fn_name: Option<&'static str>, } impl Context for TestAppContext { @@ -76,7 +85,8 @@ impl Context for TestAppContext { } impl TestAppContext { - pub fn new(dispatcher: TestDispatcher) -> Self { + /// Creates a new `TestAppContext`. Usually you can rely on `#[gpui::test]` to do this for you. + pub fn new(dispatcher: TestDispatcher, fn_name: Option<&'static str>) -> Self { let arc_dispatcher = Arc::new(dispatcher.clone()); let background_executor = BackgroundExecutor::new(arc_dispatcher.clone()); let foreground_executor = ForegroundExecutor::new(arc_dispatcher); @@ -92,41 +102,61 @@ impl TestAppContext { dispatcher: dispatcher.clone(), test_platform: platform, text_system, + fn_name, } } + /// The name of the test function that created this `TestAppContext` + pub fn test_function_name(&self) -> Option<&'static str> { + self.fn_name + } + + /// Checks whether there have been any new path prompts received by the platform. + pub fn did_prompt_for_new_path(&self) -> bool { + self.test_platform.did_prompt_for_new_path() + } + + /// returns a new `TestAppContext` re-using the same executors to interleave tasks. pub fn new_app(&self) -> TestAppContext { - Self::new(self.dispatcher.clone()) + Self::new(self.dispatcher.clone(), self.fn_name) } + /// Simulates quitting the app. pub fn quit(&self) { self.app.borrow_mut().shutdown(); } + /// Schedules all windows to be redrawn on the next effect cycle. pub fn refresh(&mut self) -> Result<()> { let mut app = self.app.borrow_mut(); app.refresh(); Ok(()) } + /// Returns an executor (for running tasks in the background) pub fn executor(&self) -> BackgroundExecutor { self.background_executor.clone() } + /// Returns an executor (for running tasks on the main thread) pub fn foreground_executor(&self) -> &ForegroundExecutor { &self.foreground_executor } + /// Gives you an `&mut AppContext` for the duration of the closure pub fn update(&self, f: impl FnOnce(&mut AppContext) -> R) -> R { let mut cx = self.app.borrow_mut(); cx.update(f) } + /// Gives you an `&AppContext` for the duration of the closure pub fn read(&self, f: impl FnOnce(&AppContext) -> R) -> R { let cx = self.app.borrow(); f(&*cx) } + /// Adds a new window. The Window will always be backed by a `TestWindow` which + /// can be retrieved with `self.test_window(handle)` pub fn add_window(&mut self, build_window: F) -> WindowHandle where F: FnOnce(&mut ViewContext) -> V, @@ -136,12 +166,16 @@ impl TestAppContext { cx.open_window(WindowOptions::default(), |cx| cx.new_view(build_window)) } + /// Adds a new window with no content. pub fn add_empty_window(&mut self) -> AnyWindowHandle { let mut cx = self.app.borrow_mut(); cx.open_window(WindowOptions::default(), |cx| cx.new_view(|_| EmptyView {})) .any_handle } + /// Adds a new window, and returns its root view and a `VisualTestContext` which can be used + /// as a `WindowContext` for the rest of the test. Typically you would shadow this context with + /// the returned one. `let (view, cx) = cx.add_window_view(...);` pub fn add_window_view(&mut self, build_window: F) -> (View, &mut VisualTestContext) where F: FnOnce(&mut ViewContext) -> V, @@ -152,22 +186,28 @@ impl TestAppContext { drop(cx); let view = window.root_view(self).unwrap(); let cx = Box::new(VisualTestContext::from_window(*window.deref(), self)); + cx.run_until_parked(); // it might be nice to try and cleanup these at the end of each test. (view, Box::leak(cx)) } + /// returns the TextSystem pub fn text_system(&self) -> &Arc { &self.text_system } + /// Simulates writing to the platform clipboard pub fn write_to_clipboard(&self, item: ClipboardItem) { self.test_platform.write_to_clipboard(item) } + /// Simulates reading from the platform clipboard. + /// This will return the most recent value from `write_to_clipboard`. pub fn read_from_clipboard(&self) -> Option { self.test_platform.read_from_clipboard() } + /// Simulates choosing a File in the platform's "Open" dialog. pub fn simulate_new_path_selection( &self, select_path: impl FnOnce(&std::path::Path) -> Option, @@ -175,22 +215,27 @@ impl TestAppContext { self.test_platform.simulate_new_path_selection(select_path); } + /// Simulates clicking a button in an platform-level alert dialog. pub fn simulate_prompt_answer(&self, button_ix: usize) { self.test_platform.simulate_prompt_answer(button_ix); } + /// Returns true if there's an alert dialog open. pub fn has_pending_prompt(&self) -> bool { self.test_platform.has_pending_prompt() } + /// Simulates the user resizing the window to the new size. pub fn simulate_window_resize(&self, window_handle: AnyWindowHandle, size: Size) { self.test_window(window_handle).simulate_resize(size); } + /// Returns all windows open in the test. pub fn windows(&self) -> Vec { self.app.borrow().windows().clone() } + /// Run the given task on the main thread. pub fn spawn(&self, f: impl FnOnce(AsyncAppContext) -> Fut) -> Task where Fut: Future + 'static, @@ -199,16 +244,20 @@ impl TestAppContext { self.foreground_executor.spawn(f(self.to_async())) } + /// true if the given global is defined pub fn has_global(&self) -> bool { let app = self.app.borrow(); app.has_global::() } + /// runs the given closure with a reference to the global + /// panics if `has_global` would return false. pub fn read_global(&self, read: impl FnOnce(&G, &AppContext) -> R) -> R { let app = self.app.borrow(); read(app.global(), &app) } + /// runs the given closure with a reference to the global (if set) pub fn try_read_global( &self, read: impl FnOnce(&G, &AppContext) -> R, @@ -217,11 +266,13 @@ impl TestAppContext { Some(read(lock.try_global()?, &lock)) } + /// sets the global in this context. pub fn set_global(&mut self, global: G) { let mut lock = self.app.borrow_mut(); lock.set_global(global); } + /// updates the global in this context. (panics if `has_global` would return false) pub fn update_global( &mut self, update: impl FnOnce(&mut G, &mut AppContext) -> R, @@ -230,6 +281,8 @@ impl TestAppContext { lock.update_global(update) } + /// Returns an `AsyncAppContext` which can be used to run tasks that expect to be on a background + /// thread on the current thread in tests. pub fn to_async(&self) -> AsyncAppContext { AsyncAppContext { app: Rc::downgrade(&self.app), @@ -238,6 +291,12 @@ impl TestAppContext { } } + /// Wait until there are no more pending tasks. + pub fn run_until_parked(&mut self) { + self.background_executor.run_until_parked() + } + + /// Simulate dispatching an action to the currently focused node in the window. pub fn dispatch_action(&mut self, window: AnyWindowHandle, action: A) where A: Action, @@ -251,7 +310,8 @@ impl TestAppContext { /// simulate_keystrokes takes a space-separated list of keys to type. /// cx.simulate_keystrokes("cmd-shift-p b k s p enter") - /// will run backspace on the current editor through the command palette. + /// in Zed, this will run backspace on the current editor through the command palette. + /// This will also run the background executor until it's parked. pub fn simulate_keystrokes(&mut self, window: AnyWindowHandle, keystrokes: &str) { for keystroke in keystrokes .split(" ") @@ -266,7 +326,8 @@ impl TestAppContext { /// simulate_input takes a string of text to type. /// cx.simulate_input("abc") - /// will type abc into your current editor. + /// will type abc into your current editor + /// This will also run the background executor until it's parked. pub fn simulate_input(&mut self, window: AnyWindowHandle, input: &str) { for keystroke in input.split("").map(Keystroke::parse).map(Result::unwrap) { self.dispatch_keystroke(window, keystroke.into(), false); @@ -275,6 +336,7 @@ impl TestAppContext { self.background_executor.run_until_parked() } + /// dispatches a single Keystroke (see also `simulate_keystrokes` and `simulate_input`) pub fn dispatch_keystroke( &mut self, window: AnyWindowHandle, @@ -285,6 +347,7 @@ impl TestAppContext { .simulate_keystroke(keystroke, is_held) } + /// Returns the `TestWindow` backing the given handle. pub fn test_window(&self, window: AnyWindowHandle) -> TestWindow { self.app .borrow_mut() @@ -299,6 +362,7 @@ impl TestAppContext { .clone() } + /// Returns a stream of notifications whenever the View or Model is updated. pub fn notifications(&mut self, entity: &impl Entity) -> impl Stream { let (tx, rx) = futures::channel::mpsc::unbounded(); self.update(|cx| { @@ -315,6 +379,7 @@ impl TestAppContext { rx } + /// Retuens a stream of events emitted by the given Model. pub fn events>( &mut self, entity: &Model, @@ -333,6 +398,8 @@ impl TestAppContext { rx } + /// Runs until the given condition becomes true. (Prefer `run_until_parked` if you + /// don't need to jump in at a specific time). pub async fn condition( &mut self, model: &Model, @@ -362,6 +429,7 @@ impl TestAppContext { } impl Model { + /// Block until the next event is emitted by the model, then return it. pub fn next_event(&self, cx: &mut TestAppContext) -> Evt where Evt: Send + Clone + 'static, @@ -391,6 +459,7 @@ impl Model { } impl View { + /// Returns a future that resolves when the view is next updated. pub fn next_notification(&self, cx: &TestAppContext) -> impl Future { use postage::prelude::{Sink as _, Stream as _}; @@ -417,6 +486,7 @@ impl View { } impl View { + /// Returns a future that resolves when the condition becomes true. pub fn condition( &self, cx: &TestAppContext, @@ -429,7 +499,7 @@ impl View { use postage::prelude::{Sink as _, Stream as _}; let (tx, mut rx) = postage::mpsc::channel(1024); - let timeout_duration = Duration::from_millis(100); //todo!() cx.condition_duration(); + let timeout_duration = Duration::from_millis(100); let mut cx = cx.app.borrow_mut(); let subscriptions = ( @@ -467,12 +537,11 @@ impl View { } } - // todo!(start_waiting) - // cx.borrow().foreground_executor().start_waiting(); + cx.borrow().background_executor().start_waiting(); rx.recv() .await .expect("view dropped with pending condition"); - // cx.borrow().foreground_executor().finish_waiting(); + cx.borrow().background_executor().finish_waiting(); } }) .await @@ -484,18 +553,25 @@ impl View { use derive_more::{Deref, DerefMut}; #[derive(Deref, DerefMut, Clone)] +/// A VisualTestContext is the test-equivalent of a `WindowContext`. It allows you to +/// run window-specific test code. pub struct VisualTestContext { #[deref] #[deref_mut] - cx: TestAppContext, + /// cx is the original TestAppContext (you can more easily access this using Deref) + pub cx: TestAppContext, window: AnyWindowHandle, } impl<'a> VisualTestContext { + /// Provides the `WindowContext` for the duration of the closure. pub fn update(&mut self, f: impl FnOnce(&mut WindowContext) -> R) -> R { self.cx.update_window(self.window, |_, cx| f(cx)).unwrap() } + /// Create a new VisualTestContext. You would typically shadow the passed in + /// TestAppContext with this, as this is typically more useful. + /// `let cx = VisualTestContext::from_window(window, cx);` pub fn from_window(window: AnyWindowHandle, cx: &TestAppContext) -> Self { Self { cx: cx.clone(), @@ -503,10 +579,12 @@ impl<'a> VisualTestContext { } } + /// Wait until there are no more pending tasks. pub fn run_until_parked(&self) { self.cx.background_executor.run_until_parked(); } + /// Dispatch the action to the currently focused node. pub fn dispatch_action(&mut self, action: A) where A: Action, @@ -514,24 +592,32 @@ impl<'a> VisualTestContext { self.cx.dispatch_action(self.window, action) } + /// Read the title off the window (set by `WindowContext#set_window_title`) pub fn window_title(&mut self) -> Option { self.cx.test_window(self.window).0.lock().title.clone() } + /// Simulate a sequence of keystrokes `cx.simulate_keystrokes("cmd-p escape")` + /// Automatically runs until parked. pub fn simulate_keystrokes(&mut self, keystrokes: &str) { self.cx.simulate_keystrokes(self.window, keystrokes) } + /// Simulate typing text `cx.simulate_input("hello")` + /// Automatically runs until parked. pub fn simulate_input(&mut self, input: &str) { self.cx.simulate_input(self.window, input) } + /// Simulates the user blurring the window. pub fn deactivate_window(&mut self) { if Some(self.window) == self.test_platform.active_window() { self.test_platform.set_active_window(None) } self.background_executor.run_until_parked(); } + + /// Simulates the user closing the window. /// Returns true if the window was closed. pub fn simulate_close(&mut self) -> bool { let handler = self @@ -668,6 +754,7 @@ impl VisualContext for VisualTestContext { } impl AnyWindowHandle { + /// Creates the given view in this window. pub fn build_view( &self, cx: &mut TestAppContext, @@ -677,6 +764,7 @@ impl AnyWindowHandle { } } +/// An EmptyView for testing. pub struct EmptyView {} impl Render for EmptyView { diff --git a/crates/gpui/src/arena.rs b/crates/gpui/src/arena.rs index bb493a6d06487a5b07f7a2afbedc36d85fa9f715..b3d7f9b0ecf530a2dbe2c8f9d4dd286ac971a2d9 100644 --- a/crates/gpui/src/arena.rs +++ b/crates/gpui/src/arena.rs @@ -66,18 +66,19 @@ impl Arena { } unsafe { - let layout = alloc::Layout::new::().pad_to_align(); - let next_offset = self.offset.add(layout.size()); - assert!(next_offset <= self.end); + let layout = alloc::Layout::new::(); + let offset = self.offset.add(self.offset.align_offset(layout.align())); + let next_offset = offset.add(layout.size()); + assert!(next_offset <= self.end, "not enough space in Arena"); let result = ArenaBox { - ptr: self.offset.cast(), + ptr: offset.cast(), valid: self.valid.clone(), }; inner_writer(result.ptr, f); self.elements.push(ArenaElement { - value: self.offset, + value: offset, drop: drop::, }); self.offset = next_offset; @@ -199,4 +200,43 @@ mod tests { arena.clear(); assert!(dropped.get()); } + + #[test] + #[should_panic(expected = "not enough space in Arena")] + fn test_arena_overflow() { + let mut arena = Arena::new(16); + arena.alloc(|| 1u64); + arena.alloc(|| 2u64); + // This should panic. + arena.alloc(|| 3u64); + } + + #[test] + fn test_arena_alignment() { + let mut arena = Arena::new(256); + let x1 = arena.alloc(|| 1u8); + let x2 = arena.alloc(|| 2u16); + let x3 = arena.alloc(|| 3u32); + let x4 = arena.alloc(|| 4u64); + let x5 = arena.alloc(|| 5u64); + + assert_eq!(*x1, 1); + assert_eq!(*x2, 2); + assert_eq!(*x3, 3); + assert_eq!(*x4, 4); + assert_eq!(*x5, 5); + + assert_eq!(x1.ptr.align_offset(std::mem::align_of_val(&*x1)), 0); + assert_eq!(x2.ptr.align_offset(std::mem::align_of_val(&*x2)), 0); + } + + #[test] + #[should_panic(expected = "attempted to dereference an ArenaRef after its Arena was cleared")] + fn test_arena_use_after_clear() { + let mut arena = Arena::new(16); + let value = arena.alloc(|| 1u64); + + arena.clear(); + let _read_value = *value; + } } diff --git a/crates/gpui/src/color.rs b/crates/gpui/src/color.rs index dc0f5055e70a123b99a7cc8b746ee69bd23652a3..bc764e564c3340957f947ef669243be0a7d27e4d 100644 --- a/crates/gpui/src/color.rs +++ b/crates/gpui/src/color.rs @@ -321,7 +321,7 @@ impl Hsla { /// /// Assumptions: /// - Alpha values are contained in the range [0, 1], with 1 as fully opaque and 0 as fully transparent. - /// - The relative contributions of `self` and `other` is based on `self`'s alpha value (`self.a`) and `other`'s alpha value (`other.a`), `self` contributing `self.a * (1.0 - other.a)` and `other` contributing it's own alpha value. + /// - The relative contributions of `self` and `other` is based on `self`'s alpha value (`self.a`) and `other`'s alpha value (`other.a`), `self` contributing `self.a * (1.0 - other.a)` and `other` contributing its own alpha value. /// - RGB color components are contained in the range [0, 1]. /// - If `self` and `other` colors are out of the valid range, the blend operation's output and behavior is undefined. pub fn blend(self, other: Hsla) -> Hsla { diff --git a/crates/gpui/src/element.rs b/crates/gpui/src/element.rs index 987b91b791a93b9fb72672a4608c1b6665fc20e2..179c2cb1e25db449556e92cfbf9710278dbd2b73 100644 --- a/crates/gpui/src/element.rs +++ b/crates/gpui/src/element.rs @@ -31,14 +31,14 @@ pub trait IntoElement: Sized { /// The specific type of element into which the implementing type is converted. type Element: Element; - /// The [ElementId] of self once converted into an [Element]. + /// The [`ElementId`] of self once converted into an [`Element`]. /// If present, the resulting element's state will be carried across frames. fn element_id(&self) -> Option; - /// Convert self into a type that implements [Element]. + /// Convert self into a type that implements [`Element`]. fn into_element(self) -> Self::Element; - /// Convert self into a dynamically-typed [AnyElement]. + /// Convert self into a dynamically-typed [`AnyElement`]. fn into_any_element(self) -> AnyElement { self.into_element().into_any() } @@ -115,7 +115,7 @@ pub trait Render: 'static + Sized { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement; } -/// You can derive [IntoElement] on any type that implements this trait. +/// You can derive [`IntoElement`] on any type that implements this trait. /// It is used to allow views to be expressed in terms of abstract data. pub trait RenderOnce: 'static { fn render(self, cx: &mut WindowContext) -> impl IntoElement; @@ -224,7 +224,7 @@ enum ElementDrawPhase { }, } -/// A wrapper around an implementer of [Element] that allows it to be drawn in a window. +/// A wrapper around an implementer of [`Element`] that allows it to be drawn in a window. impl DrawableElement { fn new(element: E) -> Self { DrawableElement { diff --git a/crates/gpui/src/elements/div.rs b/crates/gpui/src/elements/div.rs index 45097411d13875c7b1a8fecc4888711dbc0d9dd3..627a2ac339d631c666926d4fc8e1354396cb36f7 100644 --- a/crates/gpui/src/elements/div.rs +++ b/crates/gpui/src/elements/div.rs @@ -1003,7 +1003,7 @@ impl Interactivity { if let Some(text) = cx .text_system() .shape_text( - &element_id, + element_id.into(), FONT_SIZE, &[cx.text_style().to_run(str_len)], None, @@ -1055,22 +1055,11 @@ impl Interactivity { }; eprintln!( - "This element is created at:\n{}:{}:{}", - location.file(), + "This element was created at:\n{}:{}:{}", + dir.join(location.file()).to_string_lossy(), location.line(), location.column() ); - - std::process::Command::new("zed") - .arg(format!( - "{}/{}:{}:{}", - dir.to_string_lossy(), - location.file(), - location.line(), - location.column() - )) - .spawn() - .ok(); } } }); diff --git a/crates/gpui/src/elements/img.rs b/crates/gpui/src/elements/img.rs index 650b5b666bc821e15873c53f2cdc56c0546b6a20..71a51351fdb24ccf4b275b42e2541a87569014dd 100644 --- a/crates/gpui/src/elements/img.rs +++ b/crates/gpui/src/elements/img.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use crate::{ point, size, BorrowWindow, Bounds, DevicePixels, Element, ImageData, InteractiveElement, - InteractiveElementState, Interactivity, IntoElement, LayoutId, Pixels, SharedString, Size, + InteractiveElementState, Interactivity, IntoElement, LayoutId, Pixels, SharedUrl, Size, StyleRefinement, Styled, WindowContext, }; use futures::FutureExt; @@ -12,13 +12,13 @@ use util::ResultExt; #[derive(Clone, Debug)] pub enum ImageSource { /// Image content will be loaded from provided URI at render time. - Uri(SharedString), + Uri(SharedUrl), Data(Arc), Surface(CVImageBuffer), } -impl From for ImageSource { - fn from(value: SharedString) -> Self { +impl From for ImageSource { + fn from(value: SharedUrl) -> Self { Self::Uri(value) } } diff --git a/crates/gpui/src/elements/overlay.rs b/crates/gpui/src/elements/overlay.rs index eab3ee60b41ded0f39fa548285b3d6be572e2ba1..6996a13cfaac61b84e9ba7df10f240710ca2d162 100644 --- a/crates/gpui/src/elements/overlay.rs +++ b/crates/gpui/src/elements/overlay.rs @@ -14,9 +14,8 @@ pub struct Overlay { children: SmallVec<[AnyElement; 2]>, anchor_corner: AnchorCorner, fit_mode: OverlayFitMode, - // todo!(); anchor_position: Option>, - // position_mode: OverlayPositionMode, + position_mode: OverlayPositionMode, } /// overlay gives you a floating element that will avoid overflowing the window bounds. @@ -27,6 +26,7 @@ pub fn overlay() -> Overlay { anchor_corner: AnchorCorner::TopLeft, fit_mode: OverlayFitMode::SwitchAnchor, anchor_position: None, + position_mode: OverlayPositionMode::Window, } } @@ -44,6 +44,14 @@ impl Overlay { self } + /// Sets the position mode for this overlay. Local will have this + /// interpret its [`Overlay::position`] as relative to the parent element. + /// While Window will have it interpret the position as relative to the window. + pub fn position_mode(mut self, mode: OverlayPositionMode) -> Self { + self.position_mode = mode; + self + } + /// Snap to window edge instead of switching anchor corner when an overflow would occur. pub fn snap_to_window(mut self) -> Self { self.fit_mode = OverlayFitMode::SnapToWindow; @@ -100,9 +108,14 @@ impl Element for Overlay { child_max = child_max.max(&child_bounds.lower_right()); } let size: Size = (child_max - child_min).into(); - let origin = self.anchor_position.unwrap_or(bounds.origin); - let mut desired = self.anchor_corner.get_bounds(origin, size); + let (origin, mut desired) = self.position_mode.get_position_and_bounds( + self.anchor_position, + self.anchor_corner, + size, + bounds, + ); + let limits = Bounds { origin: Point::default(), size: cx.viewport_size(), @@ -184,6 +197,35 @@ pub enum OverlayFitMode { SwitchAnchor, } +#[derive(Copy, Clone, PartialEq)] +pub enum OverlayPositionMode { + Window, + Local, +} + +impl OverlayPositionMode { + fn get_position_and_bounds( + &self, + anchor_position: Option>, + anchor_corner: AnchorCorner, + size: Size, + bounds: Bounds, + ) -> (Point, Bounds) { + match self { + OverlayPositionMode::Window => { + let anchor_position = anchor_position.unwrap_or_else(|| bounds.origin); + let bounds = anchor_corner.get_bounds(anchor_position, size); + (anchor_position, bounds) + } + OverlayPositionMode::Local => { + let anchor_position = anchor_position.unwrap_or_default(); + let bounds = anchor_corner.get_bounds(bounds.origin + anchor_position, size); + (anchor_position, bounds) + } + } + } +} + #[derive(Clone, Copy, PartialEq, Eq)] pub enum AnchorCorner { TopLeft, diff --git a/crates/gpui/src/elements/text.rs b/crates/gpui/src/elements/text.rs index 29c93fd19e91518a96a5f663352b92519264aca9..4e5c6721472398233a457573a56d27d43881c071 100644 --- a/crates/gpui/src/elements/text.rs +++ b/crates/gpui/src/elements/text.rs @@ -202,7 +202,10 @@ impl TextState { let Some(lines) = cx .text_system() .shape_text( - &text, font_size, &runs, wrap_width, // Wrap if we know the width. + text.clone(), + font_size, + &runs, + wrap_width, // Wrap if we know the width. ) .log_err() else { diff --git a/crates/gpui/src/elements/uniform_list.rs b/crates/gpui/src/elements/uniform_list.rs index ffa678e9e5ed4ef8281c97067bc411451dda507e..77ef7df10c206c35ff59d354e1c90c88e7cbec5f 100644 --- a/crates/gpui/src/elements/uniform_list.rs +++ b/crates/gpui/src/elements/uniform_list.rs @@ -1,7 +1,7 @@ use crate::{ point, px, size, AnyElement, AvailableSpace, BorrowWindow, Bounds, ContentMask, Element, ElementId, InteractiveElement, InteractiveElementState, Interactivity, IntoElement, LayoutId, - Pixels, Point, Render, Size, StyleRefinement, Styled, View, ViewContext, WindowContext, + Pixels, Render, Size, StyleRefinement, Styled, View, ViewContext, WindowContext, }; use smallvec::SmallVec; use std::{cell::RefCell, cmp, ops::Range, rc::Rc}; @@ -64,40 +64,19 @@ pub struct UniformList { } #[derive(Clone, Default)] -pub struct UniformListScrollHandle(Rc>>); - -#[derive(Clone, Debug)] -struct ScrollHandleState { - item_height: Pixels, - list_height: Pixels, - scroll_offset: Rc>>, +pub struct UniformListScrollHandle { + deferred_scroll_to_item: Rc>>, } impl UniformListScrollHandle { pub fn new() -> Self { - Self(Rc::new(RefCell::new(None))) - } - - pub fn scroll_to_item(&self, ix: usize) { - if let Some(state) = &*self.0.borrow() { - let mut scroll_offset = state.scroll_offset.borrow_mut(); - let item_top = state.item_height * ix; - let item_bottom = item_top + state.item_height; - let scroll_top = -scroll_offset.y; - if item_top < scroll_top { - scroll_offset.y = -item_top; - } else if item_bottom > scroll_top + state.list_height { - scroll_offset.y = -(item_bottom - state.list_height); - } + Self { + deferred_scroll_to_item: Rc::new(RefCell::new(None)), } } - pub fn scroll_top(&self) -> Pixels { - if let Some(state) = &*self.0.borrow() { - -state.scroll_offset.borrow().y - } else { - Pixels::ZERO - } + pub fn scroll_to_item(&mut self, ix: usize) { + self.deferred_scroll_to_item.replace(Some(ix)); } } @@ -190,18 +169,14 @@ impl Element for UniformList { let shared_scroll_offset = element_state .interactive .scroll_offset - .get_or_insert_with(|| { - if let Some(scroll_handle) = self.scroll_handle.as_ref() { - if let Some(scroll_handle) = scroll_handle.0.borrow().as_ref() { - return scroll_handle.scroll_offset.clone(); - } - } - - Rc::default() - }) + .get_or_insert_with(|| Rc::default()) .clone(); let item_height = self.measure_item(Some(padded_bounds.size.width), cx).height; + let shared_scroll_to_item = self + .scroll_handle + .as_mut() + .and_then(|handle| handle.deferred_scroll_to_item.take()); self.interactivity.paint( bounds, @@ -228,12 +203,18 @@ impl Element for UniformList { scroll_offset.y = min_scroll_offset; } - if let Some(scroll_handle) = self.scroll_handle.clone() { - scroll_handle.0.borrow_mut().replace(ScrollHandleState { - item_height, - list_height: padded_bounds.size.height, - scroll_offset: shared_scroll_offset, - }); + if let Some(ix) = shared_scroll_to_item { + let list_height = padded_bounds.size.height; + let mut updated_scroll_offset = shared_scroll_offset.borrow_mut(); + let item_top = item_height * ix; + let item_bottom = item_top + item_height; + let scroll_top = -updated_scroll_offset.y; + if item_top < scroll_top { + updated_scroll_offset.y = -item_top; + } else if item_bottom > scroll_top + list_height { + updated_scroll_offset.y = -(item_bottom - list_height); + } + scroll_offset = *updated_scroll_offset; } let first_visible_element_ix = diff --git a/crates/gpui/src/executor.rs b/crates/gpui/src/executor.rs index 589493b4bb431586ed004e88e59d0d8f329334fc..fc60cb1ec6afcd1c79f5b561a436eac4635c47bc 100644 --- a/crates/gpui/src/executor.rs +++ b/crates/gpui/src/executor.rs @@ -32,6 +32,12 @@ pub struct ForegroundExecutor { not_send: PhantomData>, } +/// Task is a primitive that allows work to happen in the background. +/// +/// It implements [`Future`] so you can `.await` on it. +/// +/// If you drop a task it will be cancelled immediately. Calling [`Task::detach`] allows +/// the task to continue running in the background, but with no way to return a value. #[must_use] #[derive(Debug)] pub enum Task { @@ -40,10 +46,12 @@ pub enum Task { } impl Task { + /// Create a new task that will resolve with the value pub fn ready(val: T) -> Self { Task::Ready(Some(val)) } + /// Detaching a task runs it to completion in the background pub fn detach(self) { match self { Task::Ready(_) => {} @@ -57,6 +65,8 @@ where T: 'static, E: 'static + Debug, { + /// Run the task to completion in the background and log any + /// errors that occur. #[track_caller] pub fn detach_and_log_err(self, cx: &mut AppContext) { let location = core::panic::Location::caller(); @@ -97,6 +107,10 @@ type AnyLocalFuture = Pin>>; type AnyFuture = Pin>>; +/// BackgroundExecutor lets you run things on background threads. +/// In production this is a thread pool with no ordering guarantees. +/// In tests this is simalated by running tasks one by one in a deterministic +/// (but arbitrary) order controlled by the `SEED` environment variable. impl BackgroundExecutor { pub fn new(dispatcher: Arc) -> Self { Self { dispatcher } @@ -135,6 +149,7 @@ impl BackgroundExecutor { Task::Spawned(task) } + /// Used by the test harness to run an async test in a syncronous fashion. #[cfg(any(test, feature = "test-support"))] #[track_caller] pub fn block_test(&self, future: impl Future) -> R { @@ -145,6 +160,8 @@ impl BackgroundExecutor { } } + /// Block the current thread until the given future resolves. + /// Consider using `block_with_timeout` instead. pub fn block(&self, future: impl Future) -> R { if let Ok(value) = self.block_internal(true, future, usize::MAX) { value @@ -206,6 +223,8 @@ impl BackgroundExecutor { } } + /// Block the current thread until the given future resolves + /// or `duration` has elapsed. pub fn block_with_timeout( &self, duration: Duration, @@ -238,6 +257,8 @@ impl BackgroundExecutor { } } + /// Scoped lets you start a number of tasks and waits + /// for all of them to complete before returning. pub async fn scoped<'scope, F>(&self, scheduler: F) where F: FnOnce(&mut Scope<'scope>), @@ -253,6 +274,9 @@ impl BackgroundExecutor { } } + /// Returns a task that will complete after the given duration. + /// Depending on other concurrent tasks the elapsed duration may be longer + /// than reqested. pub fn timer(&self, duration: Duration) -> Task<()> { let (runnable, task) = async_task::spawn(async move {}, { let dispatcher = self.dispatcher.clone(); @@ -262,65 +286,81 @@ impl BackgroundExecutor { Task::Spawned(task) } + /// in tests, start_waiting lets you indicate which task is waiting (for debugging only) #[cfg(any(test, feature = "test-support"))] pub fn start_waiting(&self) { self.dispatcher.as_test().unwrap().start_waiting(); } + /// in tests, removes the debugging data added by start_waiting #[cfg(any(test, feature = "test-support"))] pub fn finish_waiting(&self) { self.dispatcher.as_test().unwrap().finish_waiting(); } + /// in tests, run an arbitrary number of tasks (determined by the SEED environment variable) #[cfg(any(test, feature = "test-support"))] pub fn simulate_random_delay(&self) -> impl Future { self.dispatcher.as_test().unwrap().simulate_random_delay() } + /// in tests, indicate that a given task from `spawn_labeled` should run after everything else #[cfg(any(test, feature = "test-support"))] pub fn deprioritize(&self, task_label: TaskLabel) { self.dispatcher.as_test().unwrap().deprioritize(task_label) } + /// in tests, move time forward. This does not run any tasks, but does make `timer`s ready. #[cfg(any(test, feature = "test-support"))] pub fn advance_clock(&self, duration: Duration) { self.dispatcher.as_test().unwrap().advance_clock(duration) } + /// in tests, run one task. #[cfg(any(test, feature = "test-support"))] pub fn tick(&self) -> bool { self.dispatcher.as_test().unwrap().tick(false) } + /// in tests, run all tasks that are ready to run. If after doing so + /// the test still has outstanding tasks, this will panic. (See also `allow_parking`) #[cfg(any(test, feature = "test-support"))] pub fn run_until_parked(&self) { self.dispatcher.as_test().unwrap().run_until_parked() } + /// in tests, prevents `run_until_parked` from panicking if there are outstanding tasks. + /// This is useful when you are integrating other (non-GPUI) futures, like disk access, that + /// do take real async time to run. #[cfg(any(test, feature = "test-support"))] pub fn allow_parking(&self) { self.dispatcher.as_test().unwrap().allow_parking(); } + /// in tests, returns the rng used by the dispatcher and seeded by the `SEED` environment variable #[cfg(any(test, feature = "test-support"))] pub fn rng(&self) -> StdRng { self.dispatcher.as_test().unwrap().rng() } + /// How many CPUs are available to the dispatcher pub fn num_cpus(&self) -> usize { num_cpus::get() } + /// Whether we're on the main thread. pub fn is_main_thread(&self) -> bool { self.dispatcher.is_main_thread() } #[cfg(any(test, feature = "test-support"))] + /// in tests, control the number of ticks that `block_with_timeout` will run before timing out. pub fn set_block_on_ticks(&self, range: std::ops::RangeInclusive) { self.dispatcher.as_test().unwrap().set_block_on_ticks(range); } } +/// ForegroundExecutor runs things on the main thread. impl ForegroundExecutor { pub fn new(dispatcher: Arc) -> Self { Self { @@ -329,8 +369,7 @@ impl ForegroundExecutor { } } - /// Enqueues the given closure to be run on any thread. The closure returns - /// a future which will be run to completion on any available thread. + /// Enqueues the given Task to run on the main thread at some point in the future. pub fn spawn(&self, future: impl Future + 'static) -> Task where R: 'static, @@ -350,6 +389,7 @@ impl ForegroundExecutor { } } +/// Scope manages a set of tasks that are enqueued and waited on together. See [`BackgroundExecutor::scoped`]. pub struct Scope<'a> { executor: BackgroundExecutor, futures: Vec + Send + 'static>>>, diff --git a/crates/gpui/src/gpui.rs b/crates/gpui/src/gpui.rs index d5236d8f08c7f2fa4cb3551ee5a58695634f7e8a..6f5e30149d9691b3c364d62ab2e3ce6ec7da1b4c 100644 --- a/crates/gpui/src/gpui.rs +++ b/crates/gpui/src/gpui.rs @@ -18,6 +18,7 @@ mod platform; pub mod prelude; mod scene; mod shared_string; +mod shared_url; mod style; mod styled; mod subscription; @@ -67,6 +68,7 @@ pub use refineable::*; pub use scene::*; use seal::Sealed; pub use shared_string::*; +pub use shared_url::*; pub use smol::Timer; pub use style::*; pub use styled::*; diff --git a/crates/gpui/src/image_cache.rs b/crates/gpui/src/image_cache.rs index f80b0f0c2f71a60fa91dbf87a13ffa3b86f43abf..0d6ec81557aa7b21165a628cf707f0d1caded9d0 100644 --- a/crates/gpui/src/image_cache.rs +++ b/crates/gpui/src/image_cache.rs @@ -1,4 +1,4 @@ -use crate::{ImageData, ImageId, SharedString}; +use crate::{ImageData, ImageId, SharedUrl}; use collections::HashMap; use futures::{ future::{BoxFuture, Shared}, @@ -44,7 +44,7 @@ impl From for Error { pub struct ImageCache { client: Arc, - images: Arc>>, + images: Arc>>, } type FetchImageFuture = Shared, Error>>>; @@ -59,7 +59,7 @@ impl ImageCache { pub fn get( &self, - uri: impl Into, + uri: impl Into, ) -> Shared, Error>>> { let uri = uri.into(); let mut images = self.images.lock(); diff --git a/crates/gpui/src/input.rs b/crates/gpui/src/input.rs index da240a77a8b633683d11d96356451dd889e17818..7290b48abd7157cdbf11ffa74e6eed294bcc271e 100644 --- a/crates/gpui/src/input.rs +++ b/crates/gpui/src/input.rs @@ -34,7 +34,7 @@ pub trait InputHandler: 'static + Sized { ) -> Option>; } -/// The canonical implementation of `PlatformInputHandler`. Call `WindowContext::handle_input` +/// The canonical implementation of [`PlatformInputHandler`]. Call [`WindowContext::handle_input`] /// with an instance during your element's paint. pub struct ElementInputHandler { view: View, diff --git a/crates/gpui/src/interactive.rs b/crates/gpui/src/interactive.rs index 6f396d31aa481571d3331e816f30dbf788d47816..dfccfc35307f1eb2e75e2d7e8fe8eb73b2c4b7ef 100644 --- a/crates/gpui/src/interactive.rs +++ b/crates/gpui/src/interactive.rs @@ -178,6 +178,20 @@ impl ScrollDelta { ScrollDelta::Lines(delta) => point(line_height * delta.x, line_height * delta.y), } } + + pub fn coalesce(self, other: ScrollDelta) -> ScrollDelta { + match (self, other) { + (ScrollDelta::Pixels(px_a), ScrollDelta::Pixels(px_b)) => { + ScrollDelta::Pixels(px_a + px_b) + } + + (ScrollDelta::Lines(lines_a), ScrollDelta::Lines(lines_b)) => { + ScrollDelta::Lines(lines_a + lines_b) + } + + _ => other, + } + } } #[derive(Clone, Debug, Default)] diff --git a/crates/gpui/src/key_dispatch.rs b/crates/gpui/src/key_dispatch.rs index 22c4dffc03a78df8fde5530a3059887e91a2b876..cc4af6d7e30d4bcc546ba264d1999cdbf5bec26c 100644 --- a/crates/gpui/src/key_dispatch.rs +++ b/crates/gpui/src/key_dispatch.rs @@ -192,8 +192,8 @@ impl DispatchTree { keymap .bindings_for_action(action) .filter(|binding| { - for i in 1..context_stack.len() { - let context = &context_stack[0..i]; + for i in 0..context_stack.len() { + let context = &context_stack[0..=i]; if keymap.binding_enabled(binding, context) { return true; } @@ -283,3 +283,76 @@ impl DispatchTree { *self.node_stack.last().unwrap() } } + +#[cfg(test)] +mod tests { + use std::{rc::Rc, sync::Arc}; + + use parking_lot::Mutex; + + use crate::{Action, ActionRegistry, DispatchTree, KeyBinding, KeyContext, Keymap}; + + #[derive(PartialEq, Eq)] + struct TestAction; + + impl Action for TestAction { + fn name(&self) -> &'static str { + "test::TestAction" + } + + fn debug_name() -> &'static str + where + Self: ::std::marker::Sized, + { + "test::TestAction" + } + + fn partial_eq(&self, action: &dyn Action) -> bool { + action + .as_any() + .downcast_ref::() + .map_or(false, |a| self == a) + } + + fn boxed_clone(&self) -> std::boxed::Box { + Box::new(TestAction) + } + + fn as_any(&self) -> &dyn ::std::any::Any { + self + } + + fn build(_value: serde_json::Value) -> anyhow::Result> + where + Self: Sized, + { + Ok(Box::new(TestAction)) + } + } + + #[test] + fn test_keybinding_for_action_bounds() { + let keymap = Keymap::new(vec![KeyBinding::new( + "cmd-n", + TestAction, + Some("ProjectPanel"), + )]); + + let mut registry = ActionRegistry::default(); + + registry.load_action::(); + + let keymap = Arc::new(Mutex::new(keymap)); + + let tree = DispatchTree::new(keymap, Rc::new(registry)); + + let contexts = vec![ + KeyContext::parse("Workspace").unwrap(), + KeyContext::parse("ProjectPanel").unwrap(), + ]; + + let keybinding = tree.bindings_for_action(&TestAction, &contexts); + + assert!(keybinding[0].action.partial_eq(&TestAction)) + } +} diff --git a/crates/gpui/src/platform/mac/display.rs b/crates/gpui/src/platform/mac/display.rs index 123cbf8159be02b0496bf8e3656f837e63b03bd4..25e0921fee28efd25c0c1ddf88d6df84318a19aa 100644 --- a/crates/gpui/src/platform/mac/display.rs +++ b/crates/gpui/src/platform/mac/display.rs @@ -14,12 +14,12 @@ pub struct MacDisplay(pub(crate) CGDirectDisplayID); unsafe impl Send for MacDisplay {} impl MacDisplay { - /// Get the screen with the given [DisplayId]. + /// Get the screen with the given [`DisplayId`]. pub fn find_by_id(id: DisplayId) -> Option { Self::all().find(|screen| screen.id() == id) } - /// Get the screen with the given persistent [Uuid]. + /// Get the screen with the given persistent [`Uuid`]. pub fn find_by_uuid(uuid: Uuid) -> Option { Self::all().find(|screen| screen.uuid().ok() == Some(uuid)) } diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 2beac528c18f53cfa9a39b008dbebf3825502b30..6d03a3b5cd698c62147bd75b62c3594309f38797 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -338,6 +338,7 @@ struct MacWindowState { ime_state: ImeState, // Retains the last IME Text ime_text: Option, + external_files_dragged: bool, } impl MacWindowState { @@ -567,6 +568,7 @@ impl MacWindow { previous_modifiers_changed_event: None, ime_state: ImeState::None, ime_text: None, + external_files_dragged: false, }))); (*native_window).set_ivar( @@ -1223,15 +1225,20 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) { .. }, ) => { - lock.synthetic_drag_counter += 1; - let executor = lock.executor.clone(); - executor - .spawn(synthetic_drag( - weak_window_state, - lock.synthetic_drag_counter, - event.clone(), - )) - .detach(); + // Synthetic drag is used for selecting long buffer contents while buffer is being scrolled. + // External file drag and drop is able to emit its own synthetic mouse events which will conflict + // with these ones. + if !lock.external_files_dragged { + lock.synthetic_drag_counter += 1; + let executor = lock.executor.clone(); + executor + .spawn(synthetic_drag( + weak_window_state, + lock.synthetic_drag_counter, + event.clone(), + )) + .detach(); + } } InputEvent::MouseMove(_) if !(is_active || lock.kind == WindowKind::PopUp) => return, @@ -1675,6 +1682,7 @@ extern "C" fn dragging_entered(this: &Object, _: Sel, dragging_info: id) -> NSDr let paths = external_paths_from_event(dragging_info); InputEvent::FileDrop(FileDropEvent::Entered { position, paths }) }) { + window_state.lock().external_files_dragged = true; NSDragOperationCopy } else { NSDragOperationNone @@ -1697,6 +1705,7 @@ extern "C" fn dragging_updated(this: &Object, _: Sel, dragging_info: id) -> NSDr extern "C" fn dragging_exited(this: &Object, _: Sel, _: id) { let window_state = unsafe { get_window_state(this) }; send_new_event(&window_state, InputEvent::FileDrop(FileDropEvent::Exited)); + window_state.lock().external_files_dragged = false; } extern "C" fn perform_drag_operation(this: &Object, _: Sel, dragging_info: id) -> BOOL { diff --git a/crates/gpui/src/platform/test/display.rs b/crates/gpui/src/platform/test/display.rs index 95f1daf8e92fc8bc620a91d3c1aa1ed12818c384..68dbb0fdf3466b9f3a2800bf68768aea2f5dd1e2 100644 --- a/crates/gpui/src/platform/test/display.rs +++ b/crates/gpui/src/platform/test/display.rs @@ -32,7 +32,7 @@ impl PlatformDisplay for TestDisplay { } fn as_any(&self) -> &dyn std::any::Any { - todo!() + unimplemented!() } fn bounds(&self) -> crate::Bounds { diff --git a/crates/gpui/src/platform/test/platform.rs b/crates/gpui/src/platform/test/platform.rs index 695323e9c46b8e2a8f4260a682d8e214f58c43f4..a7dc6d48419cc2e5d54dc132617752ae030e561e 100644 --- a/crates/gpui/src/platform/test/platform.rs +++ b/crates/gpui/src/platform/test/platform.rs @@ -15,6 +15,7 @@ use std::{ time::Duration, }; +/// TestPlatform implements the Platform trait for use in tests. pub struct TestPlatform { background_executor: BackgroundExecutor, foreground_executor: ForegroundExecutor, @@ -101,9 +102,12 @@ impl TestPlatform { }) .detach(); } + + pub(crate) fn did_prompt_for_new_path(&self) -> bool { + self.prompts.borrow().new_path.len() > 0 + } } -// todo!("implement out what our tests needed in GPUI 1") impl Platform for TestPlatform { fn background_executor(&self) -> BackgroundExecutor { self.background_executor.clone() @@ -278,8 +282,7 @@ impl Platform for TestPlatform { } fn should_auto_hide_scrollbars(&self) -> bool { - // todo() - true + false } fn write_to_clipboard(&self, item: ClipboardItem) { diff --git a/crates/gpui/src/shared_url.rs b/crates/gpui/src/shared_url.rs new file mode 100644 index 0000000000000000000000000000000000000000..8fb901894367d3fa4d87e78d8b95e1a47df0ce10 --- /dev/null +++ b/crates/gpui/src/shared_url.rs @@ -0,0 +1,25 @@ +use derive_more::{Deref, DerefMut}; + +use crate::SharedString; + +/// A [`SharedString`] containing a URL. +#[derive(Deref, DerefMut, Default, PartialEq, Eq, Hash, Clone)] +pub struct SharedUrl(SharedString); + +impl std::fmt::Debug for SharedUrl { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl std::fmt::Display for SharedUrl { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0.as_ref()) + } +} + +impl> From for SharedUrl { + fn from(value: T) -> Self { + Self(value.into()) + } +} diff --git a/crates/gpui/src/style.rs b/crates/gpui/src/style.rs index 244ccebf2498fb9ff275d0818215a9ba658ffc02..a21957611d09feb25f59d4a842ab9b998e83b4d4 100644 --- a/crates/gpui/src/style.rs +++ b/crates/gpui/src/style.rs @@ -165,7 +165,8 @@ impl Default for TextStyle { fn default() -> Self { TextStyle { color: black(), - font_family: "Helvetica".into(), // todo!("Get a font we know exists on the system") + // Helvetica is a web safe font, so it should be available + font_family: "Helvetica".into(), font_features: FontFeatures::default(), font_size: rems(1.).into(), line_height: phi(), diff --git a/crates/gpui/src/subscription.rs b/crates/gpui/src/subscription.rs index b56c9a1ccdbe800d5e24e8a4a353ef4ff08c784c..887283d09486ae139e219f5910206eda317b741b 100644 --- a/crates/gpui/src/subscription.rs +++ b/crates/gpui/src/subscription.rs @@ -37,10 +37,10 @@ where }))) } - /// Inserts a new `[Subscription]` for the given `emitter_key`. By default, subscriptions + /// Inserts a new [`Subscription`] for the given `emitter_key`. By default, subscriptions /// are inert, meaning that they won't be listed when calling `[SubscriberSet::remove]` or `[SubscriberSet::retain]`. - /// This method returns a tuple of a `[Subscription]` and an `impl FnOnce`, and you can use the latter - /// to activate the `[Subscription]`. + /// This method returns a tuple of a [`Subscription`] and an `impl FnOnce`, and you can use the latter + /// to activate the [`Subscription`]. #[must_use] pub fn insert( &self, diff --git a/crates/gpui/src/test.rs b/crates/gpui/src/test.rs index 5a21576fb26178ff67c750ca7ee63652690f1700..f53d19fdc8b1028bdddba0957719e05b3af4d69d 100644 --- a/crates/gpui/src/test.rs +++ b/crates/gpui/src/test.rs @@ -1,3 +1,30 @@ +//! Test support for GPUI. +//! +//! GPUI provides first-class support for testing, which includes a macro to run test that rely on having a context, +//! and a test implementation of the `ForegroundExecutor` and `BackgroundExecutor` which ensure that your tests run +//! deterministically even in the face of arbitrary parallelism. +//! +//! The output of the `gpui::test` macro is understood by other rust test runners, so you can use it with `cargo test` +//! or `cargo-nextest`, or another runner of your choice. +//! +//! To make it possible to test collaborative user interfaces (like Zed) you can ask for as many different contexts +//! as you need. +//! +//! ## Example +//! +//! ``` +//! use gpui; +//! +//! #[gpui::test] +//! async fn test_example(cx: &TestAppContext) { +//! assert!(true) +//! } +//! +//! #[gpui::test] +//! async fn test_collaboration_example(cx_a: &TestAppContext, cx_b: &TestAppContext) { +//! assert!(true) +//! } +//! ``` use crate::{Entity, Subscription, TestAppContext, TestDispatcher}; use futures::StreamExt as _; use rand::prelude::*; @@ -12,7 +39,6 @@ pub fn run_test( max_retries: usize, test_fn: &mut (dyn RefUnwindSafe + Fn(TestDispatcher, u64)), on_fail_fn: Option, - _fn_name: String, // todo!("re-enable fn_name") ) { let starting_seed = env::var("SEED") .map(|seed| seed.parse().expect("invalid SEED variable")) @@ -68,6 +94,7 @@ impl futures::Stream for Observation { } } +/// observe returns a stream of the change events from the given `View` or `Model` pub fn observe(entity: &impl Entity, cx: &mut TestAppContext) -> Observation<()> { let (tx, rx) = smol::channel::unbounded(); let _subscription = cx.update(|cx| { diff --git a/crates/gpui/src/text_system.rs b/crates/gpui/src/text_system.rs index 0969560e95d62e6d74dc82e88eb4b13958a77480..47073bcde0ef77b7b6674d0c0a24b7dfa107e948 100644 --- a/crates/gpui/src/text_system.rs +++ b/crates/gpui/src/text_system.rs @@ -258,7 +258,7 @@ impl TextSystem { pub fn shape_text( &self, - text: &str, // todo!("pass a SharedString and preserve it when passed a single line?") + text: SharedString, font_size: Pixels, runs: &[TextRun], wrap_width: Option, @@ -268,8 +268,8 @@ impl TextSystem { let mut lines = SmallVec::new(); let mut line_start = 0; - for line_text in text.split('\n') { - let line_text = SharedString::from(line_text.to_string()); + + let mut process_line = |line_text: SharedString| { let line_end = line_start + line_text.len(); let mut last_font: Option = None; @@ -335,6 +335,24 @@ impl TextSystem { } font_runs.clear(); + }; + + let mut split_lines = text.split('\n'); + let mut processed = false; + + if let Some(first_line) = split_lines.next() { + if let Some(second_line) = split_lines.next() { + processed = true; + process_line(first_line.to_string().into()); + process_line(second_line.to_string().into()); + for line_text in split_lines { + process_line(line_text.to_string().into()); + } + } + } + + if !processed { + process_line(text); } self.font_runs_pool.lock().push(font_runs); diff --git a/crates/gpui/src/text_system/line_wrapper.rs b/crates/gpui/src/text_system/line_wrapper.rs index e2f0a8a5fdbce9c0e3f81c9fb018bdf517b1db7c..79013adbb2c83cd0e8e816ca5ef7bb88618b5ae8 100644 --- a/crates/gpui/src/text_system/line_wrapper.rs +++ b/crates/gpui/src/text_system/line_wrapper.rs @@ -143,7 +143,7 @@ mod tests { #[test] fn test_wrap_line() { let dispatcher = TestDispatcher::new(StdRng::seed_from_u64(0)); - let cx = TestAppContext::new(dispatcher); + let cx = TestAppContext::new(dispatcher, None); cx.update(|cx| { let text_system = cx.text_system().clone(); diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 7e4c5f93f95e6ea770d404a63a3e6795d9a4be7d..187f28c14b460c08e20500bed19538810f5d814a 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -1,3 +1,5 @@ +#![deny(missing_docs)] + use crate::{ px, size, transparent_black, Action, AnyDrag, AnyView, AppContext, Arena, ArenaBox, ArenaRef, AsyncWindowContext, AvailableSpace, Bounds, BoxShadow, Context, Corners, CursorStyle, @@ -85,10 +87,12 @@ pub enum DispatchPhase { } impl DispatchPhase { + /// Returns true if this represents the "bubble" phase. pub fn bubble(self) -> bool { self == DispatchPhase::Bubble } + /// Returns true if this represents the "capture" phase. pub fn capture(self) -> bool { self == DispatchPhase::Capture } @@ -103,7 +107,10 @@ struct FocusEvent { current_focus_path: SmallVec<[FocusId; 8]>, } -slotmap::new_key_type! { pub struct FocusId; } +slotmap::new_key_type! { + /// A globally unique identifier for a focusable element. + pub struct FocusId; +} thread_local! { pub(crate) static ELEMENT_ARENA: RefCell = RefCell::new(Arena::new(4 * 1024 * 1024)); @@ -231,6 +238,7 @@ impl Drop for FocusHandle { /// FocusableView allows users of your view to easily /// focus it (using cx.focus_view(view)) pub trait FocusableView: 'static + Render { + /// Returns the focus handle associated with this view. fn focus_handle(&self, cx: &AppContext) -> FocusHandle; } @@ -240,9 +248,11 @@ pub trait ManagedView: FocusableView + EventEmitter {} impl> ManagedView for M {} +/// Emitted by implementers of [`ManagedView`] to indicate the view should be dismissed, such as when a view is presented as a modal. pub struct DismissEvent; // Holds the state for a specific window. +#[doc(hidden)] pub struct Window { pub(crate) handle: AnyWindowHandle, pub(crate) removed: bool, @@ -259,7 +269,7 @@ pub struct Window { frame_arena: Arena, pub(crate) focus_handles: Arc>>, focus_listeners: SubscriberSet<(), AnyWindowFocusListener>, - blur_listeners: SubscriberSet<(), AnyObserver>, + focus_lost_listeners: SubscriberSet<(), AnyObserver>, default_prevented: bool, mouse_position: Point, modifiers: Modifiers, @@ -286,6 +296,7 @@ pub(crate) struct ElementStateBox { pub(crate) struct Frame { focus: Option, + window_active: bool, pub(crate) element_states: FxHashMap, mouse_listeners: FxHashMap>, pub(crate) dispatch_tree: DispatchTree, @@ -301,6 +312,7 @@ impl Frame { fn new(dispatch_tree: DispatchTree) -> Self { Frame { focus: None, + window_active: false, element_states: FxHashMap::default(), mouse_listeners: FxHashMap::default(), dispatch_tree, @@ -407,7 +419,7 @@ impl Window { frame_arena: Arena::new(1024 * 1024), focus_handles: Arc::new(RwLock::new(SlotMap::with_key())), focus_listeners: SubscriberSet::new(), - blur_listeners: SubscriberSet::new(), + focus_lost_listeners: SubscriberSet::new(), default_prevented: true, mouse_position, modifiers, @@ -434,6 +446,7 @@ impl Window { #[derive(Clone, Debug, Default, PartialEq, Eq)] #[repr(C)] pub struct ContentMask { + /// The bounds pub bounds: Bounds

, } @@ -453,8 +466,8 @@ impl ContentMask { } /// Provides access to application state in the context of a single window. Derefs -/// to an `AppContext`, so you can also pass a `WindowContext` to any method that takes -/// an `AppContext` and call any `AppContext` methods. +/// to an [`AppContext`], so you can also pass a [`WindowContext`] to any method that takes +/// an [`AppContext`] and call any [`AppContext`] methods. pub struct WindowContext<'a> { pub(crate) app: &'a mut AppContext, pub(crate) window: &'a mut Window, @@ -482,20 +495,20 @@ impl<'a> WindowContext<'a> { self.window.removed = true; } - /// Obtain a new `FocusHandle`, which allows you to track and manipulate the keyboard focus + /// Obtain a new [`FocusHandle`], which allows you to track and manipulate the keyboard focus /// for elements rendered within this window. pub fn focus_handle(&mut self) -> FocusHandle { FocusHandle::new(&self.window.focus_handles) } - /// Obtain the currently focused `FocusHandle`. If no elements are focused, returns `None`. + /// Obtain the currently focused [`FocusHandle`]. If no elements are focused, returns `None`. pub fn focused(&self) -> Option { self.window .focus .and_then(|id| FocusHandle::for_id(id, &self.window.focus_handles)) } - /// Move focus to the element associated with the given `FocusHandle`. + /// Move focus to the element associated with the given [`FocusHandle`]. pub fn focus(&mut self, handle: &FocusHandle) { if !self.window.focus_enabled || self.window.focus == Some(handle.id) { return; @@ -525,11 +538,13 @@ impl<'a> WindowContext<'a> { self.notify(); } + /// Blur the window and don't allow anything in it to be focused again. pub fn disable_focus(&mut self) { self.blur(); self.window.focus_enabled = false; } + /// Dispatch the given action on the currently focused element. pub fn dispatch_action(&mut self, action: Box) { let focus_handle = self.focused(); @@ -591,6 +606,9 @@ impl<'a> WindowContext<'a> { }); } + /// Subscribe to events emitted by a model or view. + /// The entity to which you're subscribing must implement the [`EventEmitter`] trait. + /// The callback will be invoked a handle to the emitting entity (either a [`View`] or [`Model`]), the event, and a window context for the current window. pub fn subscribe( &mut self, entity: &E, @@ -754,6 +772,9 @@ impl<'a> WindowContext<'a> { .request_measured_layout(style, rem_size, measure) } + /// Compute the layout for the given id within the given available space. + /// This method is called for its side effect, typically by the framework prior to painting. + /// After calling it, you can request the bounds of the given layout node id or any descendant. pub fn compute_layout(&mut self, layout_id: LayoutId, available_space: Size) { let mut layout_engine = self.window.layout_engine.take().unwrap(); layout_engine.compute_layout(layout_id, available_space, self); @@ -788,30 +809,37 @@ impl<'a> WindowContext<'a> { .retain(&(), |callback| callback(self)); } + /// Returns the bounds of the current window in the global coordinate space, which could span across multiple displays. pub fn window_bounds(&self) -> WindowBounds { self.window.bounds } + /// Returns the size of the drawable area within the window. pub fn viewport_size(&self) -> Size { self.window.viewport_size } + /// Returns whether this window is focused by the operating system (receiving key events). pub fn is_window_active(&self) -> bool { self.window.active } + /// Toggle zoom on the window. pub fn zoom_window(&self) { self.window.platform_window.zoom(); } + /// Update the window's title at the platform level. pub fn set_window_title(&mut self, title: &str) { self.window.platform_window.set_title(title); } + /// Mark the window as dirty at the platform level. pub fn set_window_edited(&mut self, edited: bool) { self.window.platform_window.set_edited(edited); } + /// Determine the display on which the window is visible. pub fn display(&self) -> Option> { self.platform .displays() @@ -819,6 +847,7 @@ impl<'a> WindowContext<'a> { .find(|display| display.id() == self.window.display_id) } + /// Show the platform character palette. pub fn show_character_palette(&self) { self.window.platform_window.show_character_palette(); } @@ -936,6 +965,7 @@ impl<'a> WindowContext<'a> { .on_action(action_type, ArenaRef::from(listener)); } + /// Determine whether the given action is available along the dispatch path to the currently focused element. pub fn is_action_available(&self, action: &dyn Action) -> bool { let target = self .focused() @@ -962,6 +992,7 @@ impl<'a> WindowContext<'a> { self.window.modifiers } + /// Update the cursor style at the platform level. pub fn set_cursor_style(&mut self, style: CursorStyle) { self.window.requested_cursor_style = Some(style) } @@ -991,7 +1022,7 @@ impl<'a> WindowContext<'a> { true } - pub fn was_top_layer_under_active_drag( + pub(crate) fn was_top_layer_under_active_drag( &self, point: &Point, level: &StackingOrder, @@ -1377,29 +1408,14 @@ impl<'a> WindowContext<'a> { self.window.focus, ); self.window.next_frame.focus = self.window.focus; + self.window.next_frame.window_active = self.window.active; self.window.root_view = Some(root_view); let previous_focus_path = self.window.rendered_frame.focus_path(); + let previous_window_active = self.window.rendered_frame.window_active; mem::swap(&mut self.window.rendered_frame, &mut self.window.next_frame); let current_focus_path = self.window.rendered_frame.focus_path(); - - if previous_focus_path != current_focus_path { - if !previous_focus_path.is_empty() && current_focus_path.is_empty() { - self.window - .blur_listeners - .clone() - .retain(&(), |listener| listener(self)); - } - - let event = FocusEvent { - previous_focus_path, - current_focus_path, - }; - self.window - .focus_listeners - .clone() - .retain(&(), |listener| listener(&event, self)); - } + let current_window_active = self.window.rendered_frame.window_active; let scene = self.window.rendered_frame.scene_builder.build(); @@ -1416,6 +1432,34 @@ impl<'a> WindowContext<'a> { self.window.drawing = false; ELEMENT_ARENA.with_borrow_mut(|element_arena| element_arena.clear()); + if previous_focus_path != current_focus_path + || previous_window_active != current_window_active + { + if !previous_focus_path.is_empty() && current_focus_path.is_empty() { + self.window + .focus_lost_listeners + .clone() + .retain(&(), |listener| listener(self)); + } + + let event = FocusEvent { + previous_focus_path: if previous_window_active { + previous_focus_path + } else { + Default::default() + }, + current_focus_path: if current_window_active { + current_focus_path + } else { + Default::default() + }, + }; + self.window + .focus_listeners + .clone() + .retain(&(), |listener| listener(&event, self)); + } + scene } @@ -1445,9 +1489,7 @@ impl<'a> WindowContext<'a> { InputEvent::MouseUp(mouse_up) } InputEvent::MouseExited(mouse_exited) => { - // todo!("Should we record that the mouse is outside of the window somehow? Or are these global pixels?") self.window.modifiers = mouse_exited.modifiers; - InputEvent::MouseExited(mouse_exited) } InputEvent::ModifiersChanged(modifiers_changed) => { @@ -1649,6 +1691,7 @@ impl<'a> WindowContext<'a> { self.dispatch_keystroke_observers(event, None); } + /// Determine whether a potential multi-stroke key binding is in progress on this window. pub fn has_pending_keystrokes(&self) -> bool { self.window .rendered_frame @@ -1715,27 +1758,34 @@ impl<'a> WindowContext<'a> { subscription } + /// Focus the current window and bring it to the foreground at the platform level. pub fn activate_window(&self) { self.window.platform_window.activate(); } + /// Minimize the current window at the platform level. pub fn minimize_window(&self) { self.window.platform_window.minimize(); } + /// Toggle full screen status on the current window at the platform level. pub fn toggle_full_screen(&self) { self.window.platform_window.toggle_full_screen(); } + /// Present a platform dialog. + /// The provided message will be presented, along with buttons for each answer. + /// When a button is clicked, the returned Receiver will receive the index of the clicked button. pub fn prompt( &self, level: PromptLevel, - msg: &str, + message: &str, answers: &[&str], ) -> oneshot::Receiver { - self.window.platform_window.prompt(level, msg, answers) + self.window.platform_window.prompt(level, message, answers) } + /// Returns all available actions for the focused element. pub fn available_actions(&self) -> Vec> { let node_id = self .window @@ -1754,6 +1804,7 @@ impl<'a> WindowContext<'a> { .available_actions(node_id) } + /// Returns key bindings that invoke the given action on the currently focused element. pub fn bindings_for_action(&self, action: &dyn Action) -> Vec { self.window .rendered_frame @@ -1764,6 +1815,7 @@ impl<'a> WindowContext<'a> { ) } + /// Returns any bindings that would invoke the given action on the given focus handle if it were focused. pub fn bindings_for_action_in( &self, action: &dyn Action, @@ -1782,6 +1834,7 @@ impl<'a> WindowContext<'a> { dispatch_tree.bindings_for_action(action, &context_stack) } + /// Returns a generic event listener that invokes the given listener with the view and context associated with the given view handle. pub fn listener_for( &self, view: &View, @@ -1793,6 +1846,7 @@ impl<'a> WindowContext<'a> { } } + /// Returns a generic handler that invokes the given handler with the view and context associated with the given view handle. pub fn handler_for( &self, view: &View, @@ -1804,7 +1858,8 @@ impl<'a> WindowContext<'a> { } } - //========== ELEMENT RELATED FUNCTIONS =========== + /// Invoke the given function with the given focus handle present on the key dispatch stack. + /// If you want an element to participate in key dispatch, use this method to push its key context and focus handle into the stack during paint. pub fn with_key_dispatch( &mut self, context: Option, @@ -1843,6 +1898,8 @@ impl<'a> WindowContext<'a> { } } + /// Register a callback that can interrupt the closing of the current window based the returned boolean. + /// If the callback returns false, the window won't be closed. pub fn on_window_should_close(&mut self, f: impl Fn(&mut WindowContext) -> bool + 'static) { let mut this = self.to_async(); self.window @@ -2017,19 +2074,24 @@ impl<'a> BorrowMut for WindowContext<'a> { } } +/// This trait contains functionality that is shared across [`ViewContext`] and [`WindowContext`] pub trait BorrowWindow: BorrowMut + BorrowMut { + #[doc(hidden)] fn app_mut(&mut self) -> &mut AppContext { self.borrow_mut() } + #[doc(hidden)] fn app(&self) -> &AppContext { self.borrow() } + #[doc(hidden)] fn window(&self) -> &Window { self.borrow() } + #[doc(hidden)] fn window_mut(&mut self) -> &mut Window { self.borrow_mut() } @@ -2279,6 +2341,10 @@ impl BorrowMut for WindowContext<'_> { impl BorrowWindow for T where T: BorrowMut + BorrowMut {} +/// Provides access to application state that is specialized for a particular [`View`]. +/// Allows you to interact with focus, emit events, etc. +/// ViewContext also derefs to [`WindowContext`], giving you access to all of its methods as well. +/// When you call [`View::update`], you're passed a `&mut V` and an `&mut ViewContext`. pub struct ViewContext<'a, V> { window_cx: WindowContext<'a>, view: &'a View, @@ -2316,14 +2382,17 @@ impl<'a, V: 'static> ViewContext<'a, V> { } } + /// Get the entity_id of this view. pub fn entity_id(&self) -> EntityId { self.view.entity_id() } + /// Get the view pointer underlying this context. pub fn view(&self) -> &View { self.view } + /// Get the model underlying this view. pub fn model(&self) -> &Model { &self.view.model } @@ -2333,6 +2402,7 @@ impl<'a, V: 'static> ViewContext<'a, V> { &mut self.window_cx } + /// Set a given callback to be run on the next frame. pub fn on_next_frame(&mut self, f: impl FnOnce(&mut V, &mut ViewContext) + 'static) where V: 'static, @@ -2350,6 +2420,7 @@ impl<'a, V: 'static> ViewContext<'a, V> { }); } + /// Observe another model or view for changes to its state, as tracked by [`ModelContext::notify`]. pub fn observe( &mut self, entity: &E, @@ -2383,6 +2454,9 @@ impl<'a, V: 'static> ViewContext<'a, V> { subscription } + /// Subscribe to events emitted by another model or view. + /// The entity to which you're subscribing must implement the [`EventEmitter`] trait. + /// The callback will be invoked with a reference to the current view, a handle to the emitting entity (either a [`View`] or [`Model`]), the event, and a view context for the current view. pub fn subscribe( &mut self, entity: &E, @@ -2440,6 +2514,7 @@ impl<'a, V: 'static> ViewContext<'a, V> { subscription } + /// Register a callback to be invoked when the given Model or View is released. pub fn observe_release( &mut self, entity: &E, @@ -2466,6 +2541,8 @@ impl<'a, V: 'static> ViewContext<'a, V> { subscription } + /// Indicate that this view has changed, which will invoke any observers and also mark the window as dirty. + /// If this view or any of its ancestors are *cached*, notifying it will cause it or its ancestors to be redrawn. pub fn notify(&mut self) { if !self.window.drawing { self.window_cx.notify(); @@ -2475,6 +2552,7 @@ impl<'a, V: 'static> ViewContext<'a, V> { } } + /// Register a callback to be invoked when the window is resized. pub fn observe_window_bounds( &mut self, mut callback: impl FnMut(&mut V, &mut ViewContext) + 'static, @@ -2488,6 +2566,7 @@ impl<'a, V: 'static> ViewContext<'a, V> { subscription } + /// Register a callback to be invoked when the window is activated or deactivated. pub fn observe_window_activation( &mut self, mut callback: impl FnMut(&mut V, &mut ViewContext) + 'static, @@ -2579,14 +2658,16 @@ impl<'a, V: 'static> ViewContext<'a, V> { subscription } - /// Register a listener to be called when the window loses focus. + /// Register a listener to be called when nothing in the window has focus. + /// This typically happens when the node that was focused is removed from the tree, + /// and this callback lets you chose a default place to restore the users focus. /// Returns a subscription and persists until the subscription is dropped. - pub fn on_blur_window( + pub fn on_focus_lost( &mut self, mut listener: impl FnMut(&mut V, &mut ViewContext) + 'static, ) -> Subscription { let view = self.view.downgrade(); - let (subscription, activate) = self.window.blur_listeners.insert( + let (subscription, activate) = self.window.focus_lost_listeners.insert( (), Box::new(move |cx| view.update(cx, |view, cx| listener(view, cx)).is_ok()), ); @@ -2620,6 +2701,10 @@ impl<'a, V: 'static> ViewContext<'a, V> { subscription } + /// Schedule a future to be run asynchronously. + /// The given callback is invoked with a [`WeakView`] to avoid leaking the view for a long-running process. + /// It's also given an [`AsyncWindowContext`], which can be used to access the state of the view across await points. + /// The returned future will be polled on the main thread. pub fn spawn( &mut self, f: impl FnOnce(WeakView, AsyncWindowContext) -> Fut, @@ -2632,6 +2717,7 @@ impl<'a, V: 'static> ViewContext<'a, V> { self.window_cx.spawn(|cx| f(view, cx)) } + /// Update the global state of the given type. pub fn update_global(&mut self, f: impl FnOnce(&mut G, &mut Self) -> R) -> R where G: 'static, @@ -2642,6 +2728,7 @@ impl<'a, V: 'static> ViewContext<'a, V> { result } + /// Register a callback to be invoked when the given global state changes. pub fn observe_global( &mut self, mut f: impl FnMut(&mut V, &mut ViewContext<'_, V>) + 'static, @@ -2660,6 +2747,9 @@ impl<'a, V: 'static> ViewContext<'a, V> { subscription } + /// Add a listener for any mouse event that occurs in the window. + /// This is a fairly low level method. + /// Typically, you'll want to use methods on UI elements, which perform bounds checking etc. pub fn on_mouse_event( &mut self, handler: impl Fn(&mut V, &Event, DispatchPhase, &mut ViewContext) + 'static, @@ -2672,6 +2762,7 @@ impl<'a, V: 'static> ViewContext<'a, V> { }); } + /// Register a callback to be invoked when the given Key Event is dispatched to the window. pub fn on_key_event( &mut self, handler: impl Fn(&mut V, &Event, DispatchPhase, &mut ViewContext) + 'static, @@ -2684,6 +2775,7 @@ impl<'a, V: 'static> ViewContext<'a, V> { }); } + /// Register a callback to be invoked when the given Action type is dispatched to the window. pub fn on_action( &mut self, action_type: TypeId, @@ -2698,6 +2790,7 @@ impl<'a, V: 'static> ViewContext<'a, V> { }); } + /// Emit an event to be handled any other views that have subscribed via [ViewContext::subscribe]. pub fn emit(&mut self, event: Evt) where Evt: 'static, @@ -2711,6 +2804,7 @@ impl<'a, V: 'static> ViewContext<'a, V> { }); } + /// Move focus to the current view, assuming it implements [`FocusableView`]. pub fn focus_self(&mut self) where V: FocusableView, @@ -2718,6 +2812,11 @@ impl<'a, V: 'static> ViewContext<'a, V> { self.defer(|view, cx| view.focus_handle(cx).focus(cx)) } + /// Convenience method for accessing view state in an event callback. + /// + /// Many GPUI callbacks take the form of `Fn(&E, &mut WindowContext)`, + /// but it's often useful to be able to access view state in these + /// callbacks. This method provides a convenient way to do so. pub fn listener( &self, f: impl Fn(&mut V, &E, &mut ViewContext) + 'static, @@ -2827,14 +2926,20 @@ impl<'a, V> std::ops::DerefMut for ViewContext<'a, V> { } // #[derive(Clone, Copy, Eq, PartialEq, Hash)] -slotmap::new_key_type! { pub struct WindowId; } +slotmap::new_key_type! { + /// A unique identifier for a window. + pub struct WindowId; +} impl WindowId { + /// Converts this window ID to a `u64`. pub fn as_u64(&self) -> u64 { self.0.as_ffi() } } +/// A handle to a window with a specific root view type. +/// Note that this does not keep the window alive on its own. #[derive(Deref, DerefMut)] pub struct WindowHandle { #[deref] @@ -2844,6 +2949,8 @@ pub struct WindowHandle { } impl WindowHandle { + /// Create a new handle from a window ID. + /// This does not check if the root type of the window is `V`. pub fn new(id: WindowId) -> Self { WindowHandle { any_handle: AnyWindowHandle { @@ -2854,6 +2961,9 @@ impl WindowHandle { } } + /// Get the root view out of this window. + /// + /// This will fail if the window is closed or if the root view's type does not match `V`. pub fn root(&self, cx: &mut C) -> Result> where C: Context, @@ -2865,6 +2975,9 @@ impl WindowHandle { })) } + /// Update the root view of this window. + /// + /// This will fail if the window has been closed or if the root view's type does not match pub fn update( &self, cx: &mut C, @@ -2881,6 +2994,9 @@ impl WindowHandle { })? } + /// Read the root view out of this window. + /// + /// This will fail if the window is closed or if the root view's type does not match `V`. pub fn read<'a>(&self, cx: &'a AppContext) -> Result<&'a V> { let x = cx .windows @@ -2897,6 +3013,9 @@ impl WindowHandle { Ok(x.read(cx)) } + /// Read the root view out of this window, with a callback + /// + /// This will fail if the window is closed or if the root view's type does not match `V`. pub fn read_with(&self, cx: &C, read_with: impl FnOnce(&V, &AppContext) -> R) -> Result where C: Context, @@ -2904,6 +3023,9 @@ impl WindowHandle { cx.read_window(self, |root_view, cx| read_with(root_view.read(cx), cx)) } + /// Read the root view pointer off of this window. + /// + /// This will fail if the window is closed or if the root view's type does not match `V`. pub fn root_view(&self, cx: &C) -> Result> where C: Context, @@ -2911,6 +3033,9 @@ impl WindowHandle { cx.read_window(self, |root_view, _cx| root_view.clone()) } + /// Check if this window is 'active'. + /// + /// Will return `None` if the window is closed. pub fn is_active(&self, cx: &AppContext) -> Option { cx.windows .get(self.id) @@ -2946,6 +3071,7 @@ impl From> for AnyWindowHandle { } } +/// A handle to a window with any root view type, which can be downcast to a window with a specific root view type. #[derive(Copy, Clone, PartialEq, Eq, Hash)] pub struct AnyWindowHandle { pub(crate) id: WindowId, @@ -2953,10 +3079,13 @@ pub struct AnyWindowHandle { } impl AnyWindowHandle { + /// Get the ID of this window. pub fn window_id(&self) -> WindowId { self.id } + /// Attempt to convert this handle to a window handle with a specific root view type. + /// If the types do not match, this will return `None`. pub fn downcast(&self) -> Option> { if TypeId::of::() == self.state_type { Some(WindowHandle { @@ -2968,6 +3097,9 @@ impl AnyWindowHandle { } } + /// Update the state of the root view of this window. + /// + /// This will fail if the window has been closed. pub fn update( self, cx: &mut C, @@ -2979,6 +3111,9 @@ impl AnyWindowHandle { cx.update_window(self, update) } + /// Read the state of the root view of this window. + /// + /// This will fail if the window has been closed. pub fn read(self, cx: &C, read: impl FnOnce(View, &AppContext) -> R) -> Result where C: Context, @@ -2999,12 +3134,21 @@ impl AnyWindowHandle { // } // } +/// An identifier for an [`Element`](crate::Element). +/// +/// Can be constructed with a string, a number, or both, as well +/// as other internal representations. #[derive(Clone, Debug, Eq, PartialEq, Hash)] pub enum ElementId { + /// The ID of a View element View(EntityId), + /// An integer ID. Integer(usize), + /// A string based ID. Name(SharedString), + /// An ID that's equated with a focus handle. FocusHandle(FocusId), + /// A combination of a name and an integer. NamedInteger(SharedString, usize), } @@ -3074,7 +3218,8 @@ impl From<(&'static str, u64)> for ElementId { } } -/// A rectangle, to be rendered on the screen by GPUI at the given position and size. +/// A rectangle to be rendered in the window at the given position and size. +/// Passed as an argument [`WindowContext::paint_quad`]. #[derive(Clone)] pub struct PaintQuad { bounds: Bounds, diff --git a/crates/gpui/tests/action_macros.rs b/crates/gpui/tests/action_macros.rs index 9e5f6dea16ca6ad0ad7a1eb7f7098b61e0fd4cea..99572a4b3c5b1c84a4aa65fb95c72b25b2aef9ec 100644 --- a/crates/gpui/tests/action_macros.rs +++ b/crates/gpui/tests/action_macros.rs @@ -18,33 +18,33 @@ fn test_action_macros() { impl gpui::Action for RegisterableAction { fn boxed_clone(&self) -> Box { - todo!() + unimplemented!() } fn as_any(&self) -> &dyn std::any::Any { - todo!() + unimplemented!() } fn partial_eq(&self, _action: &dyn gpui::Action) -> bool { - todo!() + unimplemented!() } fn name(&self) -> &str { - todo!() + unimplemented!() } fn debug_name() -> &'static str where Self: Sized, { - todo!() + unimplemented!() } fn build(_value: serde_json::Value) -> anyhow::Result> where Self: Sized, { - todo!() + unimplemented!() } } } diff --git a/crates/gpui_macros/src/gpui_macros.rs b/crates/gpui_macros/src/gpui_macros.rs index f0cd59908dc6967d3b00e1b0991d2390b61a59e1..1187d96ca320abbc51b66b24b0639b1211b451dc 100644 --- a/crates/gpui_macros/src/gpui_macros.rs +++ b/crates/gpui_macros/src/gpui_macros.rs @@ -7,26 +7,56 @@ mod test; use proc_macro::TokenStream; #[proc_macro] +/// register_action! can be used to register an action with the GPUI runtime. +/// You should typically use `gpui::actions!` or `gpui::impl_actions!` instead, +/// but this can be used for fine grained customization. pub fn register_action(ident: TokenStream) -> TokenStream { register_action::register_action_macro(ident) } #[proc_macro_derive(IntoElement)] +// #[derive(IntoElement)] is used to create a Component out of anything that implements +// the `RenderOnce` trait. pub fn derive_into_element(input: TokenStream) -> TokenStream { derive_into_element::derive_into_element(input) } #[proc_macro_derive(Render)] +#[doc(hidden)] pub fn derive_render(input: TokenStream) -> TokenStream { derive_render::derive_render(input) } +// Used by gpui to generate the style helpers. #[proc_macro] +#[doc(hidden)] pub fn style_helpers(input: TokenStream) -> TokenStream { style_helpers::style_helpers(input) } #[proc_macro_attribute] +/// #[gpui::test] can be used to annotate test functions that run with GPUI support. +/// it supports both synchronous and asynchronous tests, and can provide you with +/// as many `TestAppContext` instances as you need. +/// The output contains a `#[test]` annotation so this can be used with any existing +/// test harness (`cargo test` or `cargo-nextest`). +/// +/// ``` +/// #[gpui::test] +/// async fn test_foo(mut cx: &TestAppContext) { } +/// ``` +/// +/// In addition to passing a TestAppContext, you can also ask for a `StdRnd` instance. +/// this will be seeded with the `SEED` environment variable and is used internally by +/// the ForegroundExecutor and BackgroundExecutor to run tasks deterministically in tests. +/// Using the same `StdRng` for behaviour in your test will allow you to exercise a wide +/// variety of scenarios and interleavings just by changing the seed. +/// +/// #[gpui::test] also takes three different arguments: +/// - `#[gpui::test(interations=10)]` will run the test ten times with a different initial SEED. +/// - `#[gpui::test(retries=3)]` will run the test up to four times if it fails to try and make it pass. +/// - `#[gpui::test(on_failure="crate::test::report_failure")]` will call the specified function after the +/// tests fail so that you can write out more detail about the failure. pub fn test(args: TokenStream, function: TokenStream) -> TokenStream { test::test(args, function) } diff --git a/crates/gpui_macros/src/test.rs b/crates/gpui_macros/src/test.rs index 70c6da22d5a7ab888f586d87f2d8ad261089d1bf..ee3f8f713701cfc334a1343902edf29fddd781d2 100644 --- a/crates/gpui_macros/src/test.rs +++ b/crates/gpui_macros/src/test.rs @@ -106,7 +106,8 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream { let cx_varname = format_ident!("cx_{}", ix); cx_vars.extend(quote!( let mut #cx_varname = gpui::TestAppContext::new( - dispatcher.clone() + dispatcher.clone(), + Some(stringify!(#outer_fn_name)), ); )); cx_teardowns.extend(quote!( @@ -140,8 +141,7 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream { executor.block_test(#inner_fn_name(#inner_fn_args)); #cx_teardowns }, - #on_failure_fn_name, - stringify!(#outer_fn_name).to_string(), + #on_failure_fn_name ); } } @@ -169,7 +169,8 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream { let cx_varname_lock = format_ident!("cx_{}_lock", ix); cx_vars.extend(quote!( let mut #cx_varname = gpui::TestAppContext::new( - dispatcher.clone() + dispatcher.clone(), + Some(stringify!(#outer_fn_name)) ); let mut #cx_varname_lock = #cx_varname.app.borrow_mut(); )); @@ -186,7 +187,8 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream { let cx_varname = format_ident!("cx_{}", ix); cx_vars.extend(quote!( let mut #cx_varname = gpui::TestAppContext::new( - dispatcher.clone() + dispatcher.clone(), + Some(stringify!(#outer_fn_name)) ); )); cx_teardowns.extend(quote!( @@ -222,7 +224,6 @@ pub fn test(args: TokenStream, function: TokenStream) -> TokenStream { #cx_teardowns }, #on_failure_fn_name, - stringify!(#outer_fn_name).to_string(), ); } } diff --git a/crates/journal/src/journal.rs b/crates/journal/src/journal.rs index 2ae74e7f5d5d60c9485c22235633b1a23f952605..1ffab2f3d3e4e6e34514e148b68b4fba48dd2aa5 100644 --- a/crates/journal/src/journal.rs +++ b/crates/journal/src/journal.rs @@ -15,9 +15,16 @@ use workspace::{AppState, OpenVisible, Workspace}; actions!(journal, [NewJournalEntry]); +/// Settings specific to journaling #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] pub struct JournalSettings { + /// The path of the directory where journal entries are stored. + /// + /// Default: `~` pub path: Option, + /// What format to display the hours in. + /// + /// Default: hour12 pub hour_format: Option, } diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index d9472e8a77afc2dc7222d003aa23f513448ed661..08210875b841cb744b2b9734d9f8c3622da86bc2 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -254,6 +254,7 @@ pub enum Event { LanguageChanged, Reparsed, DiagnosticsUpdated, + CapabilityChanged, Closed, } @@ -631,6 +632,11 @@ impl Buffer { .set_language_registry(language_registry); } + pub fn set_capability(&mut self, capability: Capability, cx: &mut ModelContext) { + self.capability = capability; + cx.emit(Event::CapabilityChanged) + } + pub fn did_save( &mut self, version: clock::Global, diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 49977f690cd7fbac86e2063f6d569ad671839c73..5359d184d65a9249f8fc82b1eae9c95d71beda6c 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -79,36 +79,90 @@ pub struct AllLanguageSettingsContent { #[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] pub struct LanguageSettingsContent { + /// How many columns a tab should occupy. + /// + /// Default: 4 #[serde(default)] pub tab_size: Option, + /// Whether to indent lines using tab characters, as opposed to multiple + /// spaces. + /// + /// Default: false #[serde(default)] pub hard_tabs: Option, + /// How to soft-wrap long lines of text. + /// + /// Default: none #[serde(default)] pub soft_wrap: Option, + /// The column at which to soft-wrap lines, for buffers where soft-wrap + /// is enabled. + /// + /// Default: 80 #[serde(default)] pub preferred_line_length: Option, + /// Whether to show wrap guides in the editor. Setting this to true will + /// show a guide at the 'preferred_line_length' value if softwrap is set to + /// 'preferred_line_length', and will show any additional guides as specified + /// by the 'wrap_guides' setting. + /// + /// Default: true #[serde(default)] pub show_wrap_guides: Option, + /// Character counts at which to show wrap guides in the editor. + /// + /// Default: [] #[serde(default)] pub wrap_guides: Option>, + /// Whether or not to perform a buffer format before saving. + /// + /// Default: on #[serde(default)] pub format_on_save: Option, + /// Whether or not to remove any trailing whitespace from lines of a buffer + /// before saving it. + /// + /// Default: true #[serde(default)] pub remove_trailing_whitespace_on_save: Option, + /// Whether or not to ensure there's a single newline at the end of a buffer + /// when saving it. + /// + /// Default: true #[serde(default)] pub ensure_final_newline_on_save: Option, + /// How to perform a buffer format. + /// + /// Default: auto #[serde(default)] pub formatter: Option, + /// Zed's Prettier integration settings. + /// If Prettier is enabled, Zed will use this its Prettier instance for any applicable file, if + /// project has no other Prettier installed. + /// + /// Default: {} #[serde(default)] pub prettier: Option>, + /// Whether to use language servers to provide code intelligence. + /// + /// Default: true #[serde(default)] pub enable_language_server: Option, + /// Controls whether copilot provides suggestion immediately (true) + /// or waits for a `copilot::Toggle` (false). + /// + /// Default: true #[serde(default)] pub show_copilot_suggestions: Option, + /// Whether to show tabs and spaces in the editor. #[serde(default)] pub show_whitespaces: Option, + /// Whether to start a new line with a comment when a previous line is a comment as well. + /// + /// Default: true #[serde(default)] pub extend_comment_on_newline: Option, + /// Inlay hint related settings. #[serde(default)] pub inlay_hints: Option, } @@ -128,8 +182,11 @@ pub struct FeaturesContent { #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum SoftWrap { + /// Do not soft wrap. None, + /// Soft wrap lines that overflow the editor EditorWidth, + /// Soft wrap lines at the preferred line length PreferredLineLength, } @@ -148,18 +205,26 @@ pub enum FormatOnSave { #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum ShowWhitespaceSetting { + /// Draw tabs and spaces only for the selected text. Selection, + /// Do not draw any tabs or spaces None, + /// Draw all invisible symbols All, } #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum Formatter { + /// Format files using Zed's Prettier integration (if applicable), + /// or falling back to formatting via language server. #[default] Auto, + /// Format code using the current language server. LanguageServer, + /// Format code using Zed's Prettier integration. Prettier, + /// Format code using an external command. External { command: Arc, arguments: Arc<[String]>, @@ -168,6 +233,9 @@ pub enum Formatter { #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] pub struct InlayHintSettings { + /// Global switch to toggle hints on and off. + /// + /// Default: false #[serde(default)] pub enabled: bool, #[serde(default = "default_true")] diff --git a/crates/language/src/syntax_map/syntax_map_tests.rs b/crates/language/src/syntax_map/syntax_map_tests.rs index f20f481613eabfffddee791873d78d6383086ade..8b9169d1cce47f96cd9fb6a768d5f3b5397ed4d4 100644 --- a/crates/language/src/syntax_map/syntax_map_tests.rs +++ b/crates/language/src/syntax_map/syntax_map_tests.rs @@ -258,19 +258,19 @@ fn test_typing_multiple_new_injections() { let (buffer, syntax_map) = test_edit_sequence( "Rust", &[ - "fn a() { dbg }", - "fn a() { dbg«!» }", - "fn a() { dbg!«()» }", - "fn a() { dbg!(«b») }", - "fn a() { dbg!(b«.») }", - "fn a() { dbg!(b.«c») }", - "fn a() { dbg!(b.c«()») }", - "fn a() { dbg!(b.c(«vec»)) }", - "fn a() { dbg!(b.c(vec«!»)) }", - "fn a() { dbg!(b.c(vec!«[]»)) }", - "fn a() { dbg!(b.c(vec![«d»])) }", - "fn a() { dbg!(b.c(vec![d«.»])) }", - "fn a() { dbg!(b.c(vec![d.«e»])) }", + "fn a() { test_macro }", + "fn a() { test_macro«!» }", + "fn a() { test_macro!«()» }", + "fn a() { test_macro!(«b») }", + "fn a() { test_macro!(b«.») }", + "fn a() { test_macro!(b.«c») }", + "fn a() { test_macro!(b.c«()») }", + "fn a() { test_macro!(b.c(«vec»)) }", + "fn a() { test_macro!(b.c(vec«!»)) }", + "fn a() { test_macro!(b.c(vec!«[]»)) }", + "fn a() { test_macro!(b.c(vec![«d»])) }", + "fn a() { test_macro!(b.c(vec![d«.»])) }", + "fn a() { test_macro!(b.c(vec![d.«e»])) }", ], ); @@ -278,7 +278,7 @@ fn test_typing_multiple_new_injections() { &syntax_map, &buffer, &["field"], - "fn a() { dbg!(b.«c»(vec![d.«e»])) }", + "fn a() { test_macro!(b.«c»(vec![d.«e»])) }", ); } diff --git a/crates/language_tools/src/syntax_tree_view.rs b/crates/language_tools/src/syntax_tree_view.rs index c30564e9bfe3f8c0e0fd1f5c2f6827a4f070fd81..b49e4eb649ae28fea72ad2d83402653961caf0b8 100644 --- a/crates/language_tools/src/syntax_tree_view.rs +++ b/crates/language_tools/src/syntax_tree_view.rs @@ -2,13 +2,12 @@ use editor::{scroll::autoscroll::Autoscroll, Anchor, Editor, ExcerptId}; use gpui::{ actions, canvas, div, rems, uniform_list, AnyElement, AppContext, AvailableSpace, Div, EventEmitter, FocusHandle, FocusableView, Hsla, InteractiveElement, IntoElement, Model, - MouseButton, MouseDownEvent, MouseMoveEvent, ParentElement, Pixels, Render, Styled, + MouseButton, MouseDownEvent, MouseMoveEvent, ParentElement, Render, Styled, UniformListScrollHandle, View, ViewContext, VisualContext, WeakView, WindowContext, }; use language::{Buffer, OwnedSyntaxLayerInfo}; -use settings::Settings; use std::{mem, ops::Range}; -use theme::{ActiveTheme, ThemeSettings}; +use theme::ActiveTheme; use tree_sitter::{Node, TreeCursor}; use ui::{h_stack, popover_menu, ButtonLike, Color, ContextMenu, Label, LabelCommon, PopoverMenu}; use workspace::{ @@ -34,8 +33,6 @@ pub fn init(cx: &mut AppContext) { pub struct SyntaxTreeView { workspace_handle: WeakView, editor: Option, - mouse_y: Option, - line_height: Option, list_scroll_handle: UniformListScrollHandle, selected_descendant_ix: Option, hovered_descendant_ix: Option, @@ -70,8 +67,6 @@ impl SyntaxTreeView { workspace_handle: workspace_handle.clone(), list_scroll_handle: UniformListScrollHandle::new(), editor: None, - mouse_y: None, - line_height: None, hovered_descendant_ix: None, selected_descendant_ix: None, focus_handle: cx.focus_handle(), @@ -208,39 +203,6 @@ impl SyntaxTreeView { Some(()) } - fn handle_click(&mut self, y: Pixels, cx: &mut ViewContext) -> Option<()> { - let line_height = self.line_height?; - let ix = ((self.list_scroll_handle.scroll_top() + y) / line_height) as usize; - - self.update_editor_with_range_for_descendant_ix(ix, cx, |editor, mut range, cx| { - // Put the cursor at the beginning of the node. - mem::swap(&mut range.start, &mut range.end); - - editor.change_selections(Some(Autoscroll::newest()), cx, |selections| { - selections.select_ranges(vec![range]); - }); - }); - Some(()) - } - - fn hover_state_changed(&mut self, cx: &mut ViewContext) { - if let Some((y, line_height)) = self.mouse_y.zip(self.line_height) { - let ix = ((self.list_scroll_handle.scroll_top() + y) / line_height) as usize; - if self.hovered_descendant_ix != Some(ix) { - self.hovered_descendant_ix = Some(ix); - self.update_editor_with_range_for_descendant_ix(ix, cx, |editor, range, cx| { - editor.clear_background_highlights::(cx); - editor.highlight_background::( - vec![range], - |theme| theme.editor_document_highlight_write_background, - cx, - ); - }); - cx.notify(); - } - } - } - fn update_editor_with_range_for_descendant_ix( &self, descendant_ix: usize, @@ -306,15 +268,6 @@ impl SyntaxTreeView { impl Render for SyntaxTreeView { fn render(&mut self, cx: &mut gpui::ViewContext<'_, Self>) -> impl IntoElement { - let settings = ThemeSettings::get_global(cx); - let line_height = cx - .text_style() - .line_height_in_pixels(settings.buffer_font_size(cx)); - if Some(line_height) != self.line_height { - self.line_height = Some(line_height); - self.hover_state_changed(cx); - } - let mut rendered = div().flex_1(); if let Some(layer) = self @@ -345,12 +298,51 @@ impl Render for SyntaxTreeView { break; } } else { - items.push(Self::render_node( - &cursor, - depth, - Some(descendant_ix) == this.selected_descendant_ix, - cx, - )); + items.push( + Self::render_node( + &cursor, + depth, + Some(descendant_ix) == this.selected_descendant_ix, + cx, + ) + .on_mouse_down( + MouseButton::Left, + cx.listener(move |tree_view, _: &MouseDownEvent, cx| { + tree_view.update_editor_with_range_for_descendant_ix( + descendant_ix, + cx, + |editor, mut range, cx| { + // Put the cursor at the beginning of the node. + mem::swap(&mut range.start, &mut range.end); + + editor.change_selections( + Some(Autoscroll::newest()), + cx, + |selections| { + selections.select_ranges(vec![range]); + }, + ); + }, + ); + }), + ) + .on_mouse_move(cx.listener( + move |tree_view, _: &MouseMoveEvent, cx| { + if tree_view.hovered_descendant_ix != Some(descendant_ix) { + tree_view.hovered_descendant_ix = Some(descendant_ix); + tree_view.update_editor_with_range_for_descendant_ix(descendant_ix, cx, |editor, range, cx| { + editor.clear_background_highlights::(cx); + editor.highlight_background::( + vec![range], + |theme| theme.editor_document_highlight_write_background, + cx, + ); + }); + cx.notify(); + } + }, + )), + ); descendant_ix += 1; if cursor.goto_first_child() { depth += 1; @@ -364,16 +356,6 @@ impl Render for SyntaxTreeView { ) .size_full() .track_scroll(self.list_scroll_handle.clone()) - .on_mouse_move(cx.listener(move |tree_view, event: &MouseMoveEvent, cx| { - tree_view.mouse_y = Some(event.position.y); - tree_view.hover_state_changed(cx); - })) - .on_mouse_down( - MouseButton::Left, - cx.listener(move |tree_view, event: &MouseDownEvent, cx| { - tree_view.handle_click(event.position.y, cx); - }), - ) .text_bg(cx.theme().colors().background); rendered = rendered.child( diff --git a/crates/live_kit_client/examples/test_app.rs b/crates/live_kit_client/examples/test_app.rs index 96407497aec4b2c94925a7a86dc914e99043169a..68a8a84209b1df63f81f972918a7f1dc63e77a74 100644 --- a/crates/live_kit_client/examples/test_app.rs +++ b/crates/live_kit_client/examples/test_app.rs @@ -1,7 +1,7 @@ use std::{sync::Arc, time::Duration}; use futures::StreamExt; -use gpui::{actions, KeyBinding}; +use gpui::{actions, KeyBinding, Menu, MenuItem}; use live_kit_client::{ LocalAudioTrack, LocalVideoTrack, RemoteAudioTrackUpdate, RemoteVideoTrackUpdate, Room, }; @@ -26,15 +26,14 @@ fn main() { cx.on_action(quit); cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]); - // todo!() - // cx.set_menus(vec![Menu { - // name: "Zed", - // items: vec![MenuItem::Action { - // name: "Quit", - // action: Box::new(Quit), - // os_action: None, - // }], - // }]); + cx.set_menus(vec![Menu { + name: "Zed", + items: vec![MenuItem::Action { + name: "Quit", + action: Box::new(Quit), + os_action: None, + }], + }]); let live_kit_url = std::env::var("LIVE_KIT_URL").unwrap_or("http://localhost:7880".into()); let live_kit_key = std::env::var("LIVE_KIT_KEY").unwrap_or("devkey".into()); diff --git a/crates/live_kit_client/src/prod.rs b/crates/live_kit_client/src/prod.rs index b2b83e95fcf33543aed6d548b21a5f07d6f6b400..5d8ef9bf134a61111247d3fac5b94fd87a50caf8 100644 --- a/crates/live_kit_client/src/prod.rs +++ b/crates/live_kit_client/src/prod.rs @@ -164,29 +164,26 @@ pub enum ConnectionState { } pub struct Room { - native_room: Mutex, + native_room: swift::Room, connection: Mutex<( watch::Sender, watch::Receiver, )>, remote_audio_track_subscribers: Mutex>>, remote_video_track_subscribers: Mutex>>, - _delegate: Mutex, + _delegate: RoomDelegate, } -trait AssertSendSync: Send {} -impl AssertSendSync for Room {} - impl Room { pub fn new() -> Arc { Arc::new_cyclic(|weak_room| { let delegate = RoomDelegate::new(weak_room.clone()); Self { - native_room: Mutex::new(unsafe { LKRoomCreate(delegate.native_delegate) }), + native_room: unsafe { LKRoomCreate(delegate.native_delegate) }, connection: Mutex::new(watch::channel_with(ConnectionState::Disconnected)), remote_audio_track_subscribers: Default::default(), remote_video_track_subscribers: Default::default(), - _delegate: Mutex::new(delegate), + _delegate: delegate, } }) } @@ -201,7 +198,7 @@ impl Room { let (did_connect, tx, rx) = Self::build_done_callback(); unsafe { LKRoomConnect( - *self.native_room.lock(), + self.native_room, url.as_concrete_TypeRef(), token.as_concrete_TypeRef(), did_connect, @@ -271,7 +268,7 @@ impl Room { } unsafe { LKRoomPublishVideoTrack( - *self.native_room.lock(), + self.native_room, track.0, callback, Box::into_raw(Box::new(tx)) as *mut c_void, @@ -301,7 +298,7 @@ impl Room { } unsafe { LKRoomPublishAudioTrack( - *self.native_room.lock(), + self.native_room, track.0, callback, Box::into_raw(Box::new(tx)) as *mut c_void, @@ -312,14 +309,14 @@ impl Room { pub fn unpublish_track(&self, publication: LocalTrackPublication) { unsafe { - LKRoomUnpublishTrack(*self.native_room.lock(), publication.0); + LKRoomUnpublishTrack(self.native_room, publication.0); } } pub fn remote_video_tracks(&self, participant_id: &str) -> Vec> { unsafe { let tracks = LKRoomVideoTracksForRemoteParticipant( - *self.native_room.lock(), + self.native_room, CFString::new(participant_id).as_concrete_TypeRef(), ); @@ -348,7 +345,7 @@ impl Room { pub fn remote_audio_tracks(&self, participant_id: &str) -> Vec> { unsafe { let tracks = LKRoomAudioTracksForRemoteParticipant( - *self.native_room.lock(), + self.native_room, CFString::new(participant_id).as_concrete_TypeRef(), ); @@ -380,7 +377,7 @@ impl Room { ) -> Vec> { unsafe { let tracks = LKRoomAudioTrackPublicationsForRemoteParticipant( - *self.native_room.lock(), + self.native_room, CFString::new(participant_id).as_concrete_TypeRef(), ); @@ -508,23 +505,23 @@ impl Room { impl Drop for Room { fn drop(&mut self) { unsafe { - let native_room = &*self.native_room.lock(); - LKRoomDisconnect(*native_room); - CFRelease(native_room.0); + LKRoomDisconnect(self.native_room); + CFRelease(self.native_room.0); } } } struct RoomDelegate { native_delegate: swift::RoomDelegate, - _weak_room: Weak, + weak_room: *mut c_void, } impl RoomDelegate { fn new(weak_room: Weak) -> Self { + let weak_room = weak_room.into_raw() as *mut c_void; let native_delegate = unsafe { LKRoomDelegateCreate( - weak_room.as_ptr() as *mut c_void, + weak_room, Self::on_did_disconnect, Self::on_did_subscribe_to_remote_audio_track, Self::on_did_unsubscribe_from_remote_audio_track, @@ -536,7 +533,7 @@ impl RoomDelegate { }; Self { native_delegate, - _weak_room: weak_room, + weak_room, } } @@ -651,6 +648,7 @@ impl Drop for RoomDelegate { fn drop(&mut self) { unsafe { CFRelease(self.native_delegate.0); + let _ = Weak::from_raw(self.weak_room); } } } @@ -725,31 +723,22 @@ impl Drop for LocalTrackPublication { } } -pub struct RemoteTrackPublication { - native_publication: Mutex, -} +pub struct RemoteTrackPublication(swift::RemoteTrackPublication); impl RemoteTrackPublication { pub fn new(native_track_publication: swift::RemoteTrackPublication) -> Self { unsafe { CFRetain(native_track_publication.0); } - Self { - native_publication: Mutex::new(native_track_publication), - } + Self(native_track_publication) } pub fn sid(&self) -> String { - unsafe { - CFString::wrap_under_get_rule(LKRemoteTrackPublicationGetSid( - *self.native_publication.lock(), - )) - .to_string() - } + unsafe { CFString::wrap_under_get_rule(LKRemoteTrackPublicationGetSid(self.0)).to_string() } } pub fn is_muted(&self) -> bool { - unsafe { LKRemoteTrackPublicationIsMuted(*self.native_publication.lock()) } + unsafe { LKRemoteTrackPublicationIsMuted(self.0) } } pub fn set_enabled(&self, enabled: bool) -> impl Future> { @@ -767,7 +756,7 @@ impl RemoteTrackPublication { unsafe { LKRemoteTrackPublicationSetEnabled( - *self.native_publication.lock(), + self.0, enabled, complete_callback, Box::into_raw(Box::new(tx)) as *mut c_void, @@ -780,13 +769,13 @@ impl RemoteTrackPublication { impl Drop for RemoteTrackPublication { fn drop(&mut self) { - unsafe { CFRelease((*self.native_publication.lock()).0) } + unsafe { CFRelease(self.0 .0) } } } #[derive(Debug)] pub struct RemoteAudioTrack { - native_track: Mutex, + native_track: swift::RemoteAudioTrack, sid: Sid, publisher_id: String, } @@ -797,7 +786,7 @@ impl RemoteAudioTrack { CFRetain(native_track.0); } Self { - native_track: Mutex::new(native_track), + native_track, sid, publisher_id, } @@ -822,13 +811,13 @@ impl RemoteAudioTrack { impl Drop for RemoteAudioTrack { fn drop(&mut self) { - unsafe { CFRelease(self.native_track.lock().0) } + unsafe { CFRelease(self.native_track.0) } } } #[derive(Debug)] pub struct RemoteVideoTrack { - native_track: Mutex, + native_track: swift::RemoteVideoTrack, sid: Sid, publisher_id: String, } @@ -839,7 +828,7 @@ impl RemoteVideoTrack { CFRetain(native_track.0); } Self { - native_track: Mutex::new(native_track), + native_track, sid, publisher_id, } @@ -888,7 +877,7 @@ impl RemoteVideoTrack { on_frame, on_drop, ); - LKVideoTrackAddRenderer(*self.native_track.lock(), renderer); + LKVideoTrackAddRenderer(self.native_track, renderer); rx } } @@ -896,7 +885,7 @@ impl RemoteVideoTrack { impl Drop for RemoteVideoTrack { fn drop(&mut self) { - unsafe { CFRelease(self.native_track.lock().0) } + unsafe { CFRelease(self.native_track.0) } } } diff --git a/crates/live_kit_client/src/test.rs b/crates/live_kit_client/src/test.rs index 1106e66f31e6d3f517fcc21b2fdffd6a7d209a09..4575fdd2c1845c53b29876d83f1fb6a8498717dc 100644 --- a/crates/live_kit_client/src/test.rs +++ b/crates/live_kit_client/src/test.rs @@ -3,7 +3,7 @@ use async_trait::async_trait; use collections::{BTreeMap, HashMap}; use futures::Stream; use gpui::BackgroundExecutor; -use live_kit_server::token; +use live_kit_server::{proto, token}; use media::core_video::CVImageBuffer; use parking_lot::Mutex; use postage::watch; @@ -151,6 +151,21 @@ impl TestServer { Ok(()) } + async fn update_participant( + &self, + room_name: String, + identity: String, + permission: proto::ParticipantPermission, + ) -> Result<()> { + self.executor.simulate_random_delay().await; + let mut server_rooms = self.rooms.lock(); + let room = server_rooms + .get_mut(&room_name) + .ok_or_else(|| anyhow!("room {} does not exist", room_name))?; + room.participant_permissions.insert(identity, permission); + Ok(()) + } + pub async fn disconnect_client(&self, client_identity: String) { self.executor.simulate_random_delay().await; let mut server_rooms = self.rooms.lock(); @@ -172,6 +187,17 @@ impl TestServer { .get_mut(&*room_name) .ok_or_else(|| anyhow!("room {} does not exist", room_name))?; + let can_publish = room + .participant_permissions + .get(&identity) + .map(|permission| permission.can_publish) + .or(claims.video.can_publish) + .unwrap_or(true); + + if !can_publish { + return Err(anyhow!("user is not allowed to publish")); + } + let track = Arc::new(RemoteVideoTrack { sid: nanoid::nanoid!(17), publisher_id: identity.clone(), @@ -210,6 +236,17 @@ impl TestServer { .get_mut(&*room_name) .ok_or_else(|| anyhow!("room {} does not exist", room_name))?; + let can_publish = room + .participant_permissions + .get(&identity) + .map(|permission| permission.can_publish) + .or(claims.video.can_publish) + .unwrap_or(true); + + if !can_publish { + return Err(anyhow!("user is not allowed to publish")); + } + let track = Arc::new(RemoteAudioTrack { sid: nanoid::nanoid!(17), publisher_id: identity.clone(), @@ -265,6 +302,7 @@ struct TestServerRoom { client_rooms: HashMap>, video_tracks: Vec>, audio_tracks: Vec>, + participant_permissions: HashMap, } impl TestServerRoom {} @@ -297,6 +335,19 @@ impl live_kit_server::api::Client for TestApiClient { Ok(()) } + async fn update_participant( + &self, + room: String, + identity: String, + permission: live_kit_server::proto::ParticipantPermission, + ) -> Result<()> { + let server = TestServer::get(&self.url)?; + server + .update_participant(room, identity, permission) + .await?; + Ok(()) + } + fn room_token(&self, room: &str, identity: &str) -> Result { let server = TestServer::get(&self.url)?; token::create( diff --git a/crates/live_kit_server/src/api.rs b/crates/live_kit_server/src/api.rs index 2c1e174fb41551d22b498b75d9ce36011ca5a5a9..e7e933c9c39d51d9611da135edd3749030444263 100644 --- a/crates/live_kit_server/src/api.rs +++ b/crates/live_kit_server/src/api.rs @@ -11,10 +11,18 @@ pub trait Client: Send + Sync { async fn create_room(&self, name: String) -> Result<()>; async fn delete_room(&self, name: String) -> Result<()>; async fn remove_participant(&self, room: String, identity: String) -> Result<()>; + async fn update_participant( + &self, + room: String, + identity: String, + permission: proto::ParticipantPermission, + ) -> Result<()>; fn room_token(&self, room: &str, identity: &str) -> Result; fn guest_token(&self, room: &str, identity: &str) -> Result; } +pub struct LiveKitParticipantUpdate {} + #[derive(Clone)] pub struct LiveKitClient { http: reqwest::Client, @@ -131,6 +139,27 @@ impl Client for LiveKitClient { Ok(()) } + async fn update_participant( + &self, + room: String, + identity: String, + permission: proto::ParticipantPermission, + ) -> Result<()> { + let _: proto::ParticipantInfo = self + .request( + "twirp/livekit.RoomService/UpdateParticipant", + token::VideoGrant::to_admin(&room), + proto::UpdateParticipantRequest { + room: room.clone(), + identity, + metadata: "".to_string(), + permission: Some(permission), + }, + ) + .await?; + Ok(()) + } + fn room_token(&self, room: &str, identity: &str) -> Result { token::create( &self.key, diff --git a/crates/live_kit_server/src/live_kit_server.rs b/crates/live_kit_server/src/live_kit_server.rs index 7471a96ec418a5ddeeb25d527b98865c31e75686..aa7c1f2fd0179e9062a5165bf4e332e74bf30ea7 100644 --- a/crates/live_kit_server/src/live_kit_server.rs +++ b/crates/live_kit_server/src/live_kit_server.rs @@ -1,3 +1,3 @@ pub mod api; -mod proto; +pub mod proto; pub mod token; diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index 946e6af5ab5fd9a97439edb5acb296972068cf44..f3ecd2d25f2b3866b1f6c3f0ce68415fa7cd53ae 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -80,6 +80,7 @@ pub enum Event { Reloaded, DiffBaseChanged, LanguageChanged, + CapabilityChanged, Reparsed, Saved, FileHandleChanged, @@ -1404,7 +1405,7 @@ impl MultiBuffer { fn on_buffer_event( &mut self, - _: Model, + buffer: Model, event: &language::Event, cx: &mut ModelContext, ) { @@ -1421,6 +1422,10 @@ impl MultiBuffer { language::Event::Reparsed => Event::Reparsed, language::Event::DiagnosticsUpdated => Event::DiagnosticsUpdated, language::Event::Closed => Event::Closed, + language::Event::CapabilityChanged => { + self.capability = buffer.read(cx).capability(); + Event::CapabilityChanged + } // language::Event::Operation(_) => return, diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs index 75d1a0935734c4d40226554202fb24c3a5ce2562..a661a693b1f5733dd27218f27ad502ea0d763c78 100644 --- a/crates/outline/src/outline.rs +++ b/crates/outline/src/outline.rs @@ -54,7 +54,13 @@ impl FocusableView for OutlineView { } impl EventEmitter for OutlineView {} -impl ModalView for OutlineView {} +impl ModalView for OutlineView { + fn on_before_dismiss(&mut self, cx: &mut ViewContext) -> bool { + self.picker + .update(cx, |picker, cx| picker.delegate.restore_active_editor(cx)); + true + } +} impl Render for OutlineView { fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { diff --git a/crates/plugin_runtime/OPAQUE.md b/crates/plugin_runtime/OPAQUE.md index 52ee75dbf9bab78811d0ad858d2036889d9a0517..4d38409ec2629b73aaa83287c4bb3c522ea842f0 100644 --- a/crates/plugin_runtime/OPAQUE.md +++ b/crates/plugin_runtime/OPAQUE.md @@ -34,7 +34,7 @@ Rhai actually exposes a pretty nice interface for working with native Rust types > **Note**: Rhai uses strings, but I wonder if you could get away with something more compact using `TypeIds`. Maybe not, given that `TypeId`s are not deterministic across builds, and we'd need matching IDs both host-side and guest side. -In Rhai, we can alternatively use the method `Engine::register_type_with_name::(name: &str)` if we have a different type name host-side (in Rust) and guest-side (in Rhai). +In Rhai, we can alternatively use the method `Engine::register_type_with_name::(name: &str)` if we have a different type name host-side (in Rust) and guest-side (in Rhai). With respect to Wasm plugins, I think an interface like this is fairly important, because we don't know whether the original plugin was written in Rust. (This may not be true now, because we write all the plugins Zed uses, but once we allow packaging and shipping plugins, it's important to maintain a consistent interface, because even Rust changes over time.) @@ -72,15 +72,15 @@ Union::Variant(v, ..) => (*v).as_boxed_any().downcast().ok().map(|x| *x), Now Rhai can do this because it's implemented in Rust. In other words, unlike Wasm, Rhai scripts can, indirectly, hold references to places in host memory. For us to implement something like this for Wasm plugins, we'd have to keep track of a "`ResourcePool`"—alive for the duration of each function call—that we can check rust types into and out of. I think I've got a handle on how Rhai works now, so let's stop talking about Rhai and discuss what this opaque object system would look like if we implemented it in Rust. - + # Design Sketch - + First things first, we'd have to generalize the arguments we can pass to and return from functions host-side. Currently, we support anything that's `serde`able. We'd have to create a new trait, say `Value`, that has blanket implementations for both `serde` and `Clone` (or something like this; if a type is both `serde` and `clone`, we'd have to figure out a way to disambiguate). - - We'd also create a `ResourcePool` struct that essentially is a `Vec` of `Box`. When calling a function, all `Value` arguments that are resources (e.g. `Clone` instead of `serde`) would be typecasted to `dyn Any` and stored in the `ResourcePool`. - + + We'd also create a `ResourcePool` struct that essentially is a `Vec` of `Box`. When calling a function, all `Value` arguments that are resources (e.g. `Clone` instead of `serde`) would be typecasted to `dyn Any` and stored in the `ResourcePool`. + We'd probably also need a `Resource` trait that defines an associated handle for a resource. Something like this: - + ```rust pub trait Resource { type Handle: Serialize + DeserializeOwned; @@ -88,24 +88,24 @@ First things first, we'd have to generalize the arguments we can pass to and ret fn index(handle: Self) -> u32; } ``` - + Where a handle is just a dead-simple wrapper around a `u32`: - - ```rust + + ```rust #[derive(Serialize, Deserialize)] pub struct CoolHandle(u32); ``` - + It's important that this handle be accessible *both* host-side and plugin side. I don't know if this means that we have another crate, like `plugin_handles`, that contains a bunch of u32 wrappers, or something else. Because a `Resource::Handle` is just a u32, it's trivially `serde`, and can cross the ABI boundary. - - So when we add each `T: Resource` to the `ResourcePool`, the resource pool typecasts it to `Any`, appends it to the `Vec`, and returns the associated `Resource::Handle`. This handle is what we pass through to Wasm. - + + So when we add each `T: Resource` to the `ResourcePool`, the resource pool typecasts it to `Any`, appends it to the `Vec`, and returns the associated `Resource::Handle`. This handle is what we pass through to Wasm. + ```rust // Implementations and attributes omitted pub struct Rope { ... }; pub struct RopeHandle(u32); impl Resource for Arc> { ... } - + let builder: PluginBuilder = ...; let builder = builder .host_fn_async( @@ -127,7 +127,7 @@ use plugin_handles::RopeHandle; pub fn append(rope: RopeHandle, string: &str); ``` -This allows us to perform an operation on a `Rope`, but how do we get a `RopeHandle` into a plugin? Well, as plugins, we can only acquire resources to handles we're given, so we'd need to expose a function that takes a handle. +This allows us to perform an operation on a `Rope`, but how do we get a `RopeHandle` into a plugin? Well, as plugins, we can only acquire resources to handles we're given, so we'd need to expose a function that takes a handle. To illustrate that point, here's an example. First, we'd define a plugin-side function as follows: @@ -185,4 +185,4 @@ Using this approach, it should be possible to add fairly good support for resour This next week, I'll try to get a production-ready version of this working, using the `Language` resource required by some Language Server Adapters. -Hope this guide made sense! \ No newline at end of file +Hope this guide made sense! diff --git a/crates/plugin_runtime/README.md b/crates/plugin_runtime/README.md index 38d1c0bb5d1d2bc44f0c150c8667774a998befaf..25524dd2721ac1b6bf8580f2c062a56bb1d835ed 100644 --- a/crates/plugin_runtime/README.md +++ b/crates/plugin_runtime/README.md @@ -164,7 +164,7 @@ To call the functions that a plugin exports host-side, you need to have 'handles For example, let's suppose we're creating a plugin that: -1. formats a message +1. formats a message 2. processes a list of numbers somehow We could create a struct for this plugin as follows: @@ -179,7 +179,7 @@ pub struct CoolPlugin { } ``` -Note that this plugin also holds an owned reference to the runtime, which is stored in the `Plugin` type. In asynchronous or multithreaded contexts, it may be required to put `Plugin` behind an `Arc>`. Although plugins expose an asynchronous interface, the underlying Wasm engine can only execute a single function at a time. +Note that this plugin also holds an owned reference to the runtime, which is stored in the `Plugin` type. In asynchronous or multithreaded contexts, it may be required to put `Plugin` behind an `Arc>`. Although plugins expose an asynchronous interface, the underlying Wasm engine can only execute a single function at a time. > **Note**: This is a limitation of the WebAssembly standard itself. In the future, to work around this, we've been considering starting a pool of plugins, or instantiating a new plugin per call (this isn't as bad as it sounds, as instantiating a new plugin only takes about 30µs). @@ -203,7 +203,7 @@ To add a sync native function to a plugin, use the `.host_function` method: ```rust let builder = builder.host_function( - "add_f64", + "add_f64", |(a, b): (f64, f64)| a + b, ).unwrap(); ``` @@ -224,7 +224,7 @@ To add an async native function to a plugin, use the `.host_function_async` meth ```rust let builder = builder.host_function_async( - "half", + "half", |n: f64| async move { n / 2.0 }, ).unwrap(); ``` @@ -252,9 +252,9 @@ let plugin = builder .unwrap(); ``` -The `.init` method takes a single argument containing the plugin binary. +The `.init` method takes a single argument containing the plugin binary. -1. If not precompiled, use `PluginBinary::Wasm(bytes)`. This supports both the WebAssembly Textual format (`.wat`) and the WebAssembly Binary format (`.wasm`). +1. If not precompiled, use `PluginBinary::Wasm(bytes)`. This supports both the WebAssembly Textual format (`.wat`) and the WebAssembly Binary format (`.wasm`). 2. If precompiled, use `PluginBinary::Precompiled(bytes)`. This supports precompiled plugins ending in `.wasm.pre`. You need to be extra-careful when using precompiled plugins to ensure that the plugin target matches the target of the binary you are compiling. @@ -317,4 +317,4 @@ The `.call` method takes two arguments: This method is async, and must be `.await`ed. If something goes wrong (e.g. the plugin panics, or there is a type mismatch between the plugin and `WasiFn`), then this method will return an error. ## Last Notes -This has been a brief overview of how the plugin system currently works in Zed. We hope to implement higher-level affordances as time goes on, to make writing plugins easier, and providing tooling so that users of Zed may also write plugins to extend their own editors. \ No newline at end of file +This has been a brief overview of how the plugin system currently works in Zed. We hope to implement higher-level affordances as time goes on, to make writing plugins easier, and providing tooling so that users of Zed may also write plugins to extend their own editors. diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index fb3eae1945f13fb1403b89b7556d548caaa76fff..c412fad0e10ba07ee4849273577d6b2425089820 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -799,7 +799,7 @@ impl Project { prettiers_per_worktree: HashMap::default(), prettier_instances: HashMap::default(), }; - this.set_role(role); + this.set_role(role, cx); for worktree in worktrees { let _ = this.add_worktree(&worktree, cx); } @@ -1622,14 +1622,22 @@ impl Project { cx.notify(); } - pub fn set_role(&mut self, role: proto::ChannelRole) { - if let Some(ProjectClientState::Remote { capability, .. }) = &mut self.client_state { - *capability = if role == proto::ChannelRole::Member || role == proto::ChannelRole::Admin - { + pub fn set_role(&mut self, role: proto::ChannelRole, cx: &mut ModelContext) { + let new_capability = + if role == proto::ChannelRole::Member || role == proto::ChannelRole::Admin { Capability::ReadWrite } else { Capability::ReadOnly }; + if let Some(ProjectClientState::Remote { capability, .. }) = &mut self.client_state { + if *capability == new_capability { + return; + } + + *capability = new_capability; + } + for buffer in self.opened_buffers() { + buffer.update(cx, |buffer, cx| buffer.set_capability(new_capability, cx)); } } @@ -4732,7 +4740,8 @@ impl Project { } else { return Task::ready(Err(anyhow!("worktree not found for symbol"))); }; - let symbol_abs_path = worktree_abs_path.join(&symbol.path.path); + + let symbol_abs_path = resolve_path(worktree_abs_path, &symbol.path.path); let symbol_uri = if let Ok(uri) = lsp::Url::from_file_path(symbol_abs_path) { uri } else { @@ -6581,7 +6590,14 @@ impl Project { let removed = *change == PathChange::Removed; let abs_path = worktree.absolutize(path); settings_contents.push(async move { - (settings_dir, (!removed).then_some(fs.load(&abs_path).await)) + ( + settings_dir, + if removed { + None + } else { + Some(async move { fs.load(&abs_path?).await }.await) + }, + ) }); } } @@ -8718,6 +8734,20 @@ fn relativize_path(base: &Path, path: &Path) -> PathBuf { components.iter().map(|c| c.as_os_str()).collect() } +fn resolve_path(base: &Path, path: &Path) -> PathBuf { + let mut result = base.to_path_buf(); + for component in path.components() { + match component { + Component::ParentDir => { + result.pop(); + } + Component::CurDir => (), + _ => result.push(component), + } + } + result +} + impl Item for Buffer { fn entry_id(&self, cx: &AppContext) -> Option { File::from_dyn(self.file()).and_then(|file| file.project_entry_id(cx)) diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index 2a8df47e67a5b7ad7e540e581b055da546c3f7cf..925109ac964044a2e93a4a1a7ba23b493a2434ed 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -7,16 +7,40 @@ use std::sync::Arc; #[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] pub struct ProjectSettings { + /// Configuration for language servers. + /// + /// The following settings can be overriden for specific language servers: + /// - initialization_options + /// To override settings for a language, add an entry for that language server's + /// name to the lsp value. + /// Default: null #[serde(default)] pub lsp: HashMap, LspSettings>, + + /// Configuration for Git-related features #[serde(default)] pub git: GitSettings, + /// Completely ignore files matching globs from `file_scan_exclusions` + /// + /// Default: [ + /// "**/.git", + /// "**/.svn", + /// "**/.hg", + /// "**/CVS", + /// "**/.DS_Store", + /// "**/Thumbs.db", + /// "**/.classpath", + /// "**/.settings" + /// ] #[serde(default)] pub file_scan_exclusions: Option>, } #[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] pub struct GitSettings { + /// Whether or not to show the git gutter. + /// + /// Default: tracked_files pub git_gutter: Option, pub gutter_debounce: Option, } @@ -24,8 +48,10 @@ pub struct GitSettings { #[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum GitGutterSetting { + /// Show git gutter in tracked files. #[default] TrackedFiles, + /// Hide git gutter Hide, } diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 8f41c75fb4de0415089c1ce66c30cb93278c079c..e90d3237127efa3b29d75ee110a7bbd27caa6de9 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -4278,6 +4278,75 @@ fn test_glob_literal_prefix() { assert_eq!(glob_literal_prefix("foo/bar/baz.js"), "foo/bar/baz.js"); } +#[gpui::test] +async fn test_create_entry(cx: &mut gpui::TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor().clone()); + fs.insert_tree( + "/one/two", + json!({ + "three": { + "a.txt": "", + "four": {} + }, + "c.rs": "" + }), + ) + .await; + + let project = Project::test(fs.clone(), ["/one/two/three".as_ref()], cx).await; + project + .update(cx, |project, cx| { + let id = project.worktrees().next().unwrap().read(cx).id(); + project.create_entry((id, "b.."), true, cx) + }) + .unwrap() + .await + .unwrap(); + + // Can't create paths outside the project + let result = project + .update(cx, |project, cx| { + let id = project.worktrees().next().unwrap().read(cx).id(); + project.create_entry((id, "../../boop"), true, cx) + }) + .await; + assert!(result.is_err()); + + // Can't create paths with '..' + let result = project + .update(cx, |project, cx| { + let id = project.worktrees().next().unwrap().read(cx).id(); + project.create_entry((id, "four/../beep"), true, cx) + }) + .await; + assert!(result.is_err()); + + assert_eq!( + fs.paths(true), + vec![ + PathBuf::from("/"), + PathBuf::from("/one"), + PathBuf::from("/one/two"), + PathBuf::from("/one/two/c.rs"), + PathBuf::from("/one/two/three"), + PathBuf::from("/one/two/three/a.txt"), + PathBuf::from("/one/two/three/b.."), + PathBuf::from("/one/two/three/four"), + ] + ); + + // And we cannot open buffers with '..' + let result = project + .update(cx, |project, cx| { + let id = project.worktrees().next().unwrap().read(cx).id(); + project.open_buffer((id, "../c.rs"), cx) + }) + .await; + assert!(result.is_err()) +} + async fn search( project: &Model, query: SearchQuery, diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index ae0c074188274b95fbed3b078f68378ce715e570..461ea303b3d42cedb577d53f22c9e1de75a14b6a 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -965,6 +965,7 @@ impl LocalWorktree { let entry = self.refresh_entry(path.clone(), None, cx); cx.spawn(|this, mut cx| async move { + let abs_path = abs_path?; let text = fs.load(&abs_path).await?; let mut index_task = None; let snapshot = this.update(&mut cx, |this, _| this.as_local().unwrap().snapshot())?; @@ -1050,6 +1051,7 @@ impl LocalWorktree { cx.spawn(move |this, mut cx| async move { let entry = save.await?; + let abs_path = abs_path?; let this = this.upgrade().context("worktree dropped")?; let (entry_id, mtime, path) = match entry { @@ -1139,9 +1141,9 @@ impl LocalWorktree { let fs = self.fs.clone(); let write = cx.background_executor().spawn(async move { if is_dir { - fs.create_dir(&abs_path).await + fs.create_dir(&abs_path?).await } else { - fs.save(&abs_path, &Default::default(), Default::default()) + fs.save(&abs_path?, &Default::default(), Default::default()) .await } }); @@ -1188,7 +1190,7 @@ impl LocalWorktree { let fs = self.fs.clone(); let write = cx .background_executor() - .spawn(async move { fs.save(&abs_path, &text, line_ending).await }); + .spawn(async move { fs.save(&abs_path?, &text, line_ending).await }); cx.spawn(|this, mut cx| async move { write.await?; @@ -1210,10 +1212,10 @@ impl LocalWorktree { let delete = cx.background_executor().spawn(async move { if entry.is_file() { - fs.remove_file(&abs_path, Default::default()).await?; + fs.remove_file(&abs_path?, Default::default()).await?; } else { fs.remove_dir( - &abs_path, + &abs_path?, RemoveOptions { recursive: true, ignore_if_not_exists: false, @@ -1252,7 +1254,7 @@ impl LocalWorktree { let abs_new_path = self.absolutize(&new_path); let fs = self.fs.clone(); let rename = cx.background_executor().spawn(async move { - fs.rename(&abs_old_path, &abs_new_path, Default::default()) + fs.rename(&abs_old_path?, &abs_new_path?, Default::default()) .await }); @@ -1284,8 +1286,8 @@ impl LocalWorktree { let copy = cx.background_executor().spawn(async move { copy_recursive( fs.as_ref(), - &abs_old_path, - &abs_new_path, + &abs_old_path?, + &abs_new_path?, Default::default(), ) .await @@ -1609,11 +1611,17 @@ impl Snapshot { &self.abs_path } - pub fn absolutize(&self, path: &Path) -> PathBuf { + pub fn absolutize(&self, path: &Path) -> Result { + if path + .components() + .any(|component| !matches!(component, std::path::Component::Normal(_))) + { + return Err(anyhow!("invalid path")); + } if path.file_name().is_some() { - self.abs_path.join(path) + Ok(self.abs_path.join(path)) } else { - self.abs_path.to_path_buf() + Ok(self.abs_path.to_path_buf()) } } @@ -2823,7 +2831,7 @@ impl language::LocalFile for File { let abs_path = worktree.absolutize(&self.path); let fs = worktree.fs.clone(); cx.background_executor() - .spawn(async move { fs.load(&abs_path).await }) + .spawn(async move { fs.load(&abs_path?).await }) } fn buffer_reloaded( diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index a5fb8671f7fd4b6cdd9d8dff0b7dfa6801dcb76d..251e26ebfba004b81a49c1ce28956e01f42bbce5 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -30,7 +30,7 @@ use std::{ sync::Arc, }; use theme::ThemeSettings; -use ui::{prelude::*, v_stack, ContextMenu, IconElement, KeyBinding, Label, ListItem}; +use ui::{prelude::*, v_stack, ContextMenu, Icon, KeyBinding, Label, ListItem}; use unicase::UniCase; use util::{maybe, ResultExt, TryFutureExt}; use workspace::{ @@ -1403,7 +1403,7 @@ impl ProjectPanel { .indent_step_size(px(settings.indent_size)) .selected(is_selected) .child(if let Some(icon) = &icon { - div().child(IconElement::from_path(icon.to_string()).color(Color::Muted)) + div().child(Icon::from_path(icon.to_string()).color(Color::Muted)) } else { div().size(IconSize::default().rems()).invisible() }) @@ -1433,6 +1433,9 @@ impl ProjectPanel { })) .on_secondary_mouse_down(cx.listener( move |this, event: &MouseDownEvent, cx| { + // Stop propagation to prevent the catch-all context menu for the project + // panel from being deployed. + cx.stop_propagation(); this.deploy_context_menu(event.position, entry_id, cx); }, )), @@ -1515,6 +1518,16 @@ impl Render for ProjectPanel { el.on_action(cx.listener(Self::reveal_in_finder)) .on_action(cx.listener(Self::open_in_terminal)) }) + .on_mouse_down( + MouseButton::Right, + cx.listener(move |this, event: &MouseDownEvent, cx| { + // When deploying the context menu anywhere below the last project entry, + // act as if the user clicked the root of the last worktree. + if let Some(entry_id) = this.last_worktree_root_id { + this.deploy_context_menu(event.position, entry_id, cx); + } + }), + ) .track_focus(&self.focus_handle) .child( uniform_list( @@ -1577,7 +1590,7 @@ impl Render for DraggedProjectEntryView { .indent_level(self.details.depth) .indent_step_size(px(settings.indent_size)) .child(if let Some(icon) = &self.details.icon { - div().child(IconElement::from_path(icon.to_string())) + div().child(Icon::from_path(icon.to_string())) } else { div() }) @@ -1627,8 +1640,8 @@ impl Panel for ProjectPanel { cx.notify(); } - fn icon(&self, _: &WindowContext) -> Option { - Some(ui::Icon::FileTree) + fn icon(&self, _: &WindowContext) -> Option { + Some(ui::IconName::FileTree) } fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> { diff --git a/crates/project_panel/src/project_panel_settings.rs b/crates/project_panel/src/project_panel_settings.rs index b9a87a1a03cf3ac2164fb4ad5db6144f0a3fc6de..5285684891e2b2945424b8b99bb70c47652963ac 100644 --- a/crates/project_panel/src/project_panel_settings.rs +++ b/crates/project_panel/src/project_panel_settings.rs @@ -24,12 +24,35 @@ pub struct ProjectPanelSettings { #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] pub struct ProjectPanelSettingsContent { + /// Customise default width (in pixels) taken by project panel + /// + /// Default: 240 pub default_width: Option, + /// The position of project panel + /// + /// Default: left pub dock: Option, + /// Whether to show file icons in the project panel. + /// + /// Default: true pub file_icons: Option, + /// Whether to show folder icons or chevrons for directories in the project panel. + /// + /// Default: true pub folder_icons: Option, + /// Whether to show the git status in the project panel. + /// + /// Default: true pub git_status: Option, + /// Amount of indentation (in pixels) for nested items. + /// + /// Default: 20 pub indent_size: Option, + /// Whether to reveal it in the project panel automatically, + /// when a corresponding project entry becomes active. + /// Gitignored entries are never auto revealed. + /// + /// Default: true pub auto_reveal_entries: Option, } diff --git a/crates/project_symbols/src/project_symbols.rs b/crates/project_symbols/src/project_symbols.rs index ed31ebd94997bd01d53b56df4f62dfff0518f737..23dfd21b8256574bf122215cd6d34e36d8059f1f 100644 --- a/crates/project_symbols/src/project_symbols.rs +++ b/crates/project_symbols/src/project_symbols.rs @@ -242,7 +242,6 @@ impl PickerDelegate for ProjectSymbolsDelegate { .spacing(ListItemSpacing::Sparse) .selected(selected) .child( - // todo!() combine_syntax_and_fuzzy_match_highlights() v_stack() .child( LabelLike::new().child( diff --git a/crates/quick_action_bar/src/quick_action_bar.rs b/crates/quick_action_bar/src/quick_action_bar.rs index d8c42589d63bea9775145f6efb211d64077d3af0..cf4941bcec66cdef77f3f4453a6b3eb25d8b1321 100644 --- a/crates/quick_action_bar/src/quick_action_bar.rs +++ b/crates/quick_action_bar/src/quick_action_bar.rs @@ -6,7 +6,7 @@ use gpui::{ Subscription, View, ViewContext, WeakView, }; use search::{buffer_search, BufferSearchBar}; -use ui::{prelude::*, ButtonSize, ButtonStyle, Icon, IconButton, IconSize, Tooltip}; +use ui::{prelude::*, ButtonSize, ButtonStyle, IconButton, IconName, IconSize, Tooltip}; use workspace::{ item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, }; @@ -43,7 +43,7 @@ impl Render for QuickActionBar { let inlay_hints_button = Some(QuickActionBarButton::new( "toggle inlay hints", - Icon::InlayHint, + IconName::InlayHint, editor.read(cx).inlay_hints_enabled(), Box::new(editor::ToggleInlayHints), "Toggle Inlay Hints", @@ -60,7 +60,7 @@ impl Render for QuickActionBar { let search_button = Some(QuickActionBarButton::new( "toggle buffer search", - Icon::MagnifyingGlass, + IconName::MagnifyingGlass, !self.buffer_search_bar.read(cx).is_dismissed(), Box::new(buffer_search::Deploy { focus: false }), "Buffer Search", @@ -77,7 +77,7 @@ impl Render for QuickActionBar { let assistant_button = QuickActionBarButton::new( "toggle inline assistant", - Icon::MagicWand, + IconName::MagicWand, false, Box::new(InlineAssist), "Inline Assist", @@ -95,7 +95,6 @@ impl Render for QuickActionBar { h_stack() .id("quick action bar") - .p_1() .gap_2() .children(inlay_hints_button) .children(search_button) @@ -108,7 +107,7 @@ impl EventEmitter for QuickActionBar {} #[derive(IntoElement)] struct QuickActionBarButton { id: ElementId, - icon: Icon, + icon: IconName, toggled: bool, action: Box, tooltip: SharedString, @@ -118,7 +117,7 @@ struct QuickActionBarButton { impl QuickActionBarButton { fn new( id: impl Into, - icon: Icon, + icon: IconName, toggled: bool, action: Box, tooltip: impl Into, diff --git a/crates/rope/src/rope.rs b/crates/rope/src/rope.rs index adfcd19a6c5420f12fab5495731cea7b9e70397a..f9922bf5172a08fb2194816d4552a5776fb6c328 100644 --- a/crates/rope/src/rope.rs +++ b/crates/rope/src/rope.rs @@ -25,10 +25,10 @@ const CHUNK_BASE: usize = 6; #[cfg(not(test))] const CHUNK_BASE: usize = 16; -/// Type alias to [HashMatrix], an implementation of a homomorphic hash function. Two [Rope] instances +/// Type alias to [`HashMatrix`], an implementation of a homomorphic hash function. Two [`Rope`] instances /// containing the same text will produce the same fingerprint. This hash function is special in that -/// it allows us to hash individual chunks and aggregate them up the [Rope]'s tree, with the resulting -/// hash being equivalent to hashing all the text contained in the [Rope] at once. +/// it allows us to hash individual chunks and aggregate them up the [`Rope`]'s tree, with the resulting +/// hash being equivalent to hashing all the text contained in the [`Rope`] at once. pub type RopeFingerprint = HashMatrix; #[derive(Clone, Default)] @@ -78,7 +78,7 @@ impl Rope { } pub fn slice_rows(&self, range: Range) -> Rope { - //This would be more efficient with a forward advance after the first, but it's fine + // This would be more efficient with a forward advance after the first, but it's fine. let start = self.point_to_offset(Point::new(range.start, 0)); let end = self.point_to_offset(Point::new(range.end, 0)); self.slice(start..end) diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index e423441a30218674c4d6b68098a8f724c8a32654..dd5d77440edaaae98876f91a4fc5fa85999dd6ed 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -180,7 +180,8 @@ message Envelope { DeleteNotification delete_notification = 152; MarkNotificationRead mark_notification_read = 153; LspExtExpandMacro lsp_ext_expand_macro = 154; - LspExtExpandMacroResponse lsp_ext_expand_macro_response = 155; // Current max + LspExtExpandMacroResponse lsp_ext_expand_macro_response = 155; + SetRoomParticipantRole set_room_participant_role = 156; // Current max } } @@ -1633,3 +1634,9 @@ message LspExtExpandMacroResponse { string name = 1; string expansion = 2; } + +message SetRoomParticipantRole { + uint64 room_id = 1; + uint64 user_id = 2; + ChannelRole role = 3; +} diff --git a/crates/rpc/src/macros.rs b/crates/rpc/src/macros.rs index 89e605540da1157f5530ad7236b23358dc127c1a..85e2b0cf879608aec655d8eb663a73baf65fe7f0 100644 --- a/crates/rpc/src/macros.rs +++ b/crates/rpc/src/macros.rs @@ -60,8 +60,10 @@ macro_rules! request_messages { #[macro_export] macro_rules! entity_messages { - ($id_field:ident, $($name:ident),* $(,)?) => { + ({$id_field:ident, $entity_type:ty}, $($name:ident),* $(,)?) => { $(impl EntityMessage for $name { + type Entity = $entity_type; + fn remote_entity_id(&self) -> u64 { self.$id_field } diff --git a/crates/rpc/src/notification.rs b/crates/rpc/src/notification.rs index c5476469be90112e6ae294665c2bc53319cf38be..57131d3421787a71b4df0edefd59a3d7cfef865e 100644 --- a/crates/rpc/src/notification.rs +++ b/crates/rpc/src/notification.rs @@ -76,30 +76,35 @@ impl Notification { } } -#[test] -fn test_notification() { - // Notifications can be serialized and deserialized. - for notification in [ - Notification::ContactRequest { sender_id: 1 }, - Notification::ContactRequestAccepted { responder_id: 2 }, - Notification::ChannelInvitation { - channel_id: 100, - channel_name: "the-channel".into(), - inviter_id: 50, - }, - Notification::ChannelMessageMention { - sender_id: 200, - channel_id: 30, - message_id: 1, - }, - ] { - let message = notification.to_proto(); - let deserialized = Notification::from_proto(&message).unwrap(); - assert_eq!(deserialized, notification); - } +#[cfg(test)] +mod tests { + use crate::Notification; + + #[test] + fn test_notification() { + // Notifications can be serialized and deserialized. + for notification in [ + Notification::ContactRequest { sender_id: 1 }, + Notification::ContactRequestAccepted { responder_id: 2 }, + Notification::ChannelInvitation { + channel_id: 100, + channel_name: "the-channel".into(), + inviter_id: 50, + }, + Notification::ChannelMessageMention { + sender_id: 200, + channel_id: 30, + message_id: 1, + }, + ] { + let message = notification.to_proto(); + let deserialized = Notification::from_proto(&message).unwrap(); + assert_eq!(deserialized, notification); + } - // When notifications are serialized, the `kind` and `actor_id` fields are - // stored separately, and do not appear redundantly in the JSON. - let notification = Notification::ContactRequest { sender_id: 1 }; - assert_eq!(notification.to_proto().content, "{}"); + // When notifications are serialized, the `kind` and `actor_id` fields are + // stored separately, and do not appear redundantly in the JSON. + let notification = Notification::ContactRequest { sender_id: 1 }; + assert_eq!(notification.to_proto().content, "{}"); + } } diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 336c252630ea51ea9d11a0658fc59d0cb09b214f..5d0a99415480c30969a9a072287841ae6dd6c689 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -31,6 +31,7 @@ pub trait EnvelopedMessage: Clone + Debug + Serialize + Sized + Send + Sync + 's } pub trait EntityMessage: EnvelopedMessage { + type Entity; fn remote_entity_id(&self) -> u64; } @@ -282,6 +283,7 @@ messages!( (UsersResponse, Foreground), (LspExtExpandMacro, Background), (LspExtExpandMacroResponse, Background), + (SetRoomParticipantRole, Foreground), ); request_messages!( @@ -366,10 +368,11 @@ request_messages!( (UpdateProject, Ack), (UpdateWorktree, Ack), (LspExtExpandMacro, LspExtExpandMacroResponse), + (SetRoomParticipantRole, Ack), ); entity_messages!( - project_id, + {project_id, ShareProject}, AddProjectCollaborator, ApplyCodeAction, ApplyCompletionAdditionalEdits, @@ -422,7 +425,7 @@ entity_messages!( ); entity_messages!( - channel_id, + {channel_id, Channel}, ChannelMessageSent, RemoveChannelMessage, UpdateChannelBuffer, diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index c889f0a4a4c11d3f104e130e34e5b87c092565d6..7ef21c42ed3886a81ad6b9b08a17f626d99acb1f 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -21,7 +21,7 @@ use settings::Settings; use std::{any::Any, sync::Arc}; use theme::ThemeSettings; -use ui::{h_stack, prelude::*, Icon, IconButton, IconElement, ToggleButton, Tooltip}; +use ui::{h_stack, prelude::*, Icon, IconButton, IconName, ToggleButton, Tooltip}; use util::ResultExt; use workspace::{ item::ItemHandle, @@ -43,7 +43,7 @@ pub enum Event { } pub fn init(cx: &mut AppContext) { - cx.observe_new_views(|editor: &mut Workspace, _| BufferSearchBar::register(editor)) + cx.observe_new_views(|workspace: &mut Workspace, _| BufferSearchBar::register(workspace)) .detach(); } @@ -225,7 +225,7 @@ impl Render for BufferSearchBar { .border_color(editor_border) .min_w(rems(384. / 16.)) .rounded_lg() - .child(IconElement::new(Icon::MagnifyingGlass)) + .child(Icon::new(IconName::MagnifyingGlass)) .child(self.render_text_input(&self.query_editor, cx)) .children(supported_options.case.then(|| { self.render_search_option_button( @@ -287,7 +287,7 @@ impl Render for BufferSearchBar { this.child( IconButton::new( "buffer-search-bar-toggle-replace-button", - Icon::Replace, + IconName::Replace, ) .style(ButtonStyle::Subtle) .when(self.replace_enabled, |button| { @@ -323,7 +323,7 @@ impl Render for BufferSearchBar { ) .when(should_show_replace_input, |this| { this.child( - IconButton::new("search-replace-next", ui::Icon::ReplaceNext) + IconButton::new("search-replace-next", ui::IconName::ReplaceNext) .tooltip(move |cx| { Tooltip::for_action("Replace next", &ReplaceNext, cx) }) @@ -332,7 +332,7 @@ impl Render for BufferSearchBar { })), ) .child( - IconButton::new("search-replace-all", ui::Icon::ReplaceAll) + IconButton::new("search-replace-all", ui::IconName::ReplaceAll) .tooltip(move |cx| { Tooltip::for_action("Replace all", &ReplaceAll, cx) }) @@ -350,7 +350,7 @@ impl Render for BufferSearchBar { .gap_0p5() .flex_none() .child( - IconButton::new("select-all", ui::Icon::SelectAll) + IconButton::new("select-all", ui::IconName::SelectAll) .on_click(|_, cx| cx.dispatch_action(SelectAllMatches.boxed_clone())) .tooltip(|cx| { Tooltip::for_action("Select all matches", &SelectAllMatches, cx) @@ -358,13 +358,13 @@ impl Render for BufferSearchBar { ) .children(match_count) .child(render_nav_button( - ui::Icon::ChevronLeft, + ui::IconName::ChevronLeft, self.active_match_index.is_some(), "Select previous match", &SelectPrevMatch, )) .child(render_nav_button( - ui::Icon::ChevronRight, + ui::IconName::ChevronRight, self.active_match_index.is_some(), "Select next match", &SelectNextMatch, @@ -423,7 +423,7 @@ impl ToolbarItemView for BufferSearchBar { } } -/// Registrar inverts the dependency between search and it's downstream user, allowing said downstream user to register search action without knowing exactly what those actions are. +/// Registrar inverts the dependency between search and its downstream user, allowing said downstream user to register search action without knowing exactly what those actions are. pub trait SearchActionsRegistrar { fn register_handler( &mut self, @@ -479,6 +479,11 @@ impl SearchActionsRegistrar for Workspace { callback: fn(&mut BufferSearchBar, &A, &mut ViewContext), ) { self.register_action(move |workspace, action: &A, cx| { + if workspace.has_active_modal(cx) { + cx.propagate(); + return; + } + let pane = workspace.active_pane(); pane.update(cx, move |this, cx| { this.toolbar().update(cx, move |this, cx| { @@ -539,11 +544,11 @@ impl BufferSearchBar { this.select_all_matches(action, cx); }); registrar.register_handler(|this, _: &editor::Cancel, cx| { - if !this.dismissed { + if this.dismissed { + cx.propagate(); + } else { this.dismiss(&Dismiss, cx); - return; } - cx.propagate(); }); registrar.register_handler(|this, deploy, cx| { this.deploy(deploy, cx); diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index a370cca9f69c2e89b866232ccb2c385d7726ce25..6fd66b5bad2c1c9ab1036a445f1e2061b791f206 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -38,7 +38,7 @@ use std::{ use theme::ThemeSettings; use ui::{ - h_stack, prelude::*, v_stack, Icon, IconButton, IconElement, Label, LabelCommon, LabelSize, + h_stack, prelude::*, v_stack, Icon, IconButton, IconName, Label, LabelCommon, LabelSize, Selectable, ToggleButton, Tooltip, }; use util::{paths::PathMatcher, ResultExt as _}; @@ -424,7 +424,8 @@ impl Item for ProjectSearchView { .current() .as_ref() .map(|query| { - let query_text = util::truncate_and_trailoff(query, MAX_TAB_TITLE_LEN); + let query = query.replace('\n', ""); + let query_text = util::truncate_and_trailoff(&query, MAX_TAB_TITLE_LEN); query_text.into() }); let tab_name = last_query @@ -432,7 +433,7 @@ impl Item for ProjectSearchView { .unwrap_or_else(|| "Project search".into()); h_stack() .gap_2() - .child(IconElement::new(Icon::MagnifyingGlass).color(if selected { + .child(Icon::new(IconName::MagnifyingGlass).color(if selected { Color::Default } else { Color::Muted @@ -1616,12 +1617,12 @@ impl Render for ProjectSearchBar { .on_action(cx.listener(|this, action, cx| this.confirm(action, cx))) .on_action(cx.listener(|this, action, cx| this.previous_history_query(action, cx))) .on_action(cx.listener(|this, action, cx| this.next_history_query(action, cx))) - .child(IconElement::new(Icon::MagnifyingGlass)) + .child(Icon::new(IconName::MagnifyingGlass)) .child(self.render_text_input(&search.query_editor, cx)) .child( h_stack() .child( - IconButton::new("project-search-filter-button", Icon::Filter) + IconButton::new("project-search-filter-button", IconName::Filter) .tooltip(|cx| { Tooltip::for_action("Toggle filters", &ToggleFilters, cx) }) @@ -1639,7 +1640,7 @@ impl Render for ProjectSearchBar { this.child( IconButton::new( "project-search-case-sensitive", - Icon::CaseSensitive, + IconName::CaseSensitive, ) .tooltip(|cx| { Tooltip::for_action( @@ -1659,7 +1660,7 @@ impl Render for ProjectSearchBar { )), ) .child( - IconButton::new("project-search-whole-word", Icon::WholeWord) + IconButton::new("project-search-whole-word", IconName::WholeWord) .tooltip(|cx| { Tooltip::for_action( "Toggle whole word", @@ -1738,7 +1739,7 @@ impl Render for ProjectSearchBar { }), ) .child( - IconButton::new("project-search-toggle-replace", Icon::Replace) + IconButton::new("project-search-toggle-replace", IconName::Replace) .on_click(cx.listener(|this, _, cx| { this.toggle_replace(&ToggleReplace, cx); })) @@ -1755,7 +1756,7 @@ impl Render for ProjectSearchBar { .border_1() .border_color(cx.theme().colors().border) .rounded_lg() - .child(IconElement::new(Icon::Replace).size(ui::IconSize::Small)) + .child(Icon::new(IconName::Replace).size(ui::IconSize::Small)) .child(self.render_text_input(&search.replacement_editor, cx)) } else { // Fill out the space if we don't have a replacement editor. @@ -1764,7 +1765,7 @@ impl Render for ProjectSearchBar { let actions_column = h_stack() .when(search.replace_enabled, |this| { this.child( - IconButton::new("project-search-replace-next", Icon::ReplaceNext) + IconButton::new("project-search-replace-next", IconName::ReplaceNext) .on_click(cx.listener(|this, _, cx| { if let Some(search) = this.active_project_search.as_ref() { search.update(cx, |this, cx| { @@ -1775,7 +1776,7 @@ impl Render for ProjectSearchBar { .tooltip(|cx| Tooltip::for_action("Replace next match", &ReplaceNext, cx)), ) .child( - IconButton::new("project-search-replace-all", Icon::ReplaceAll) + IconButton::new("project-search-replace-all", IconName::ReplaceAll) .on_click(cx.listener(|this, _, cx| { if let Some(search) = this.active_project_search.as_ref() { search.update(cx, |this, cx| { @@ -1796,7 +1797,7 @@ impl Render for ProjectSearchBar { this }) .child( - IconButton::new("project-search-prev-match", Icon::ChevronLeft) + IconButton::new("project-search-prev-match", IconName::ChevronLeft) .disabled(search.active_match_index.is_none()) .on_click(cx.listener(|this, _, cx| { if let Some(search) = this.active_project_search.as_ref() { @@ -1810,7 +1811,7 @@ impl Render for ProjectSearchBar { }), ) .child( - IconButton::new("project-search-next-match", Icon::ChevronRight) + IconButton::new("project-search-next-match", IconName::ChevronRight) .disabled(search.active_match_index.is_none()) .on_click(cx.listener(|this, _, cx| { if let Some(search) = this.active_project_search.as_ref() { diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index f0301a5bcc7a637c17d47c59f5102f352ac3840c..748996c389370bc576949e9c6752c26cb2e39f29 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -60,11 +60,11 @@ impl SearchOptions { } } - pub fn icon(&self) -> ui::Icon { + pub fn icon(&self) -> ui::IconName { match *self { - SearchOptions::WHOLE_WORD => ui::Icon::WholeWord, - SearchOptions::CASE_SENSITIVE => ui::Icon::CaseSensitive, - SearchOptions::INCLUDE_IGNORED => ui::Icon::FileGit, + SearchOptions::WHOLE_WORD => ui::IconName::WholeWord, + SearchOptions::CASE_SENSITIVE => ui::IconName::CaseSensitive, + SearchOptions::INCLUDE_IGNORED => ui::IconName::FileGit, _ => panic!("{:?} is not a named SearchOption", self), } } @@ -98,7 +98,7 @@ impl SearchOptions { IconButton::new(self.label(), self.icon()) .on_click(action) .style(ButtonStyle::Subtle) - .when(active, |button| button.style(ButtonStyle::Filled)) + .selected(active) .tooltip({ let action = self.to_toggle_action(); let label: SharedString = format!("Toggle {}", self.label()).into(); diff --git a/crates/search/src/search_bar.rs b/crates/search/src/search_bar.rs index 628be3112ecc06e9d00e8d33361e8f17fa6efd54..0594036c25483c8dadcbfdf5988edbc1ba12a65f 100644 --- a/crates/search/src/search_bar.rs +++ b/crates/search/src/search_bar.rs @@ -3,7 +3,7 @@ use ui::IconButton; use ui::{prelude::*, Tooltip}; pub(super) fn render_nav_button( - icon: ui::Icon, + icon: ui::IconName, active: bool, tooltip: &'static str, action: &'static dyn Action, diff --git a/crates/semantic_index/Cargo.toml b/crates/semantic_index/Cargo.toml index a437a596a526de3878c35a1d409bb3f11dd7afee..41a3ab2d579867e602746775e55d68496d6a5387 100644 --- a/crates/semantic_index/Cargo.toml +++ b/crates/semantic_index/Cargo.toml @@ -47,7 +47,7 @@ project = { path = "../project", features = ["test-support"] } rpc = { path = "../rpc", features = ["test-support"] } workspace = { path = "../workspace", features = ["test-support"] } settings = { path = "../settings", features = ["test-support"]} -rust-embed = { version = "8.0", features = ["include-exclude"] } +rust-embed.workspace = true client = { path = "../client" } node_runtime = { path = "../node_runtime"} diff --git a/crates/semantic_index/src/semantic_index.rs b/crates/semantic_index/src/semantic_index.rs index dbcdeee5ed0751d96a086fea691cd1195c2b81cc..801f02e600c1130eb5364e352f7f3823d6821f7a 100644 --- a/crates/semantic_index/src/semantic_index.rs +++ b/crates/semantic_index/src/semantic_index.rs @@ -559,7 +559,7 @@ impl SemanticIndex { .spawn(async move { let mut changed_paths = BTreeMap::new(); for file in worktree.files(false, 0) { - let absolute_path = worktree.absolutize(&file.path); + let absolute_path = worktree.absolutize(&file.path)?; if file.is_external || file.is_ignored || file.is_symlink { continue; @@ -1068,11 +1068,10 @@ impl SemanticIndex { return true; }; - worktree_state.changed_paths.retain(|path, info| { + for (path, info) in &worktree_state.changed_paths { if info.is_deleted { files_to_delete.push((worktree_state.db_id, path.clone())); - } else { - let absolute_path = worktree.read(cx).absolutize(path); + } else if let Ok(absolute_path) = worktree.read(cx).absolutize(path) { let job_handle = JobHandle::new(pending_file_count_tx); pending_files.push(PendingFile { absolute_path, @@ -1083,9 +1082,8 @@ impl SemanticIndex { worktree_db_id: worktree_state.db_id, }); } - - false - }); + } + worktree_state.changed_paths.clear(); true }); @@ -1250,7 +1248,7 @@ impl SemanticIndex { impl Drop for JobHandle { fn drop(&mut self) { if let Some(inner) = Arc::get_mut(&mut self.tx) { - // This is the last instance of the JobHandle (regardless of it's origin - whether it was cloned or not) + // This is the last instance of the JobHandle (regardless of its origin - whether it was cloned or not) if let Some(tx) = inner.upgrade() { let mut tx = tx.lock(); *tx.borrow_mut() -= 1; diff --git a/crates/semantic_index/src/semantic_index_settings.rs b/crates/semantic_index/src/semantic_index_settings.rs index 306a38fa9c2ec52f5a69d27898cc9fccc1af956c..73fd49c8f5f61dad875f963586a87101371f620e 100644 --- a/crates/semantic_index/src/semantic_index_settings.rs +++ b/crates/semantic_index/src/semantic_index_settings.rs @@ -8,8 +8,13 @@ pub struct SemanticIndexSettings { pub enabled: bool, } +/// Configuration of semantic index, an alternate search engine available in +/// project search. #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] pub struct SemanticIndexSettingsContent { + /// Whether or not to display the Semantic mode in project search. + /// + /// Default: true pub enabled: Option, } diff --git a/crates/storybook/Cargo.toml b/crates/storybook/Cargo.toml index 033b3fa8d9077ee9866f0a0a7e382d5f68d3e483..9f08556757b6b59d1aed6fa7361029a36664a40a 100644 --- a/crates/storybook/Cargo.toml +++ b/crates/storybook/Cargo.toml @@ -14,6 +14,7 @@ anyhow.workspace = true backtrace-on-stack-overflow = "0.3.0" chrono = "0.4" clap = { version = "4.4", features = ["derive", "string"] } +collab_ui = { path = "../collab_ui", features = ["stories"] } strum = { version = "0.25.0", features = ["derive"] } dialoguer = { version = "0.11.0", features = ["fuzzy-select"] } editor = { path = "../editor" } diff --git a/crates/storybook/src/story_selector.rs b/crates/storybook/src/story_selector.rs index 27ddfe26ac2884cd327f065c01bdf8bf7617e624..120e60d34a1499ed143394713e0c46fbf1bc2dfa 100644 --- a/crates/storybook/src/story_selector.rs +++ b/crates/storybook/src/story_selector.rs @@ -16,6 +16,7 @@ pub enum ComponentStory { Avatar, Button, Checkbox, + CollabNotification, ContextMenu, Cursor, Disclosure, @@ -45,6 +46,9 @@ impl ComponentStory { Self::Avatar => cx.new_view(|_| ui::AvatarStory).into(), Self::Button => cx.new_view(|_| ui::ButtonStory).into(), Self::Checkbox => cx.new_view(|_| ui::CheckboxStory).into(), + Self::CollabNotification => cx + .new_view(|_| collab_ui::notifications::CollabNotificationStory) + .into(), Self::ContextMenu => cx.new_view(|_| ui::ContextMenuStory).into(), Self::Cursor => cx.new_view(|_| crate::stories::CursorStory).into(), Self::Disclosure => cx.new_view(|_| ui::DisclosureStory).into(), diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index e1605eb4fb4e35b16c7e08bee6107537462cc2ed..3a01f01ca89e03a35586ed325db69a93844ed270 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -983,7 +983,7 @@ impl Terminal { let mut terminal = if let Some(term) = term.try_lock_unfair() { term } else if self.last_synced.elapsed().as_secs_f32() > 0.25 { - term.lock_unfair() //It's been too long, force block + term.lock_unfair() // It's been too long, force block } else if let None = self.sync_task { //Skip this frame let delay = cx.background_executor().timer(Duration::from_millis(16)); @@ -1402,9 +1402,9 @@ fn content_index_for_mouse(pos: Point, size: &TerminalSize) -> usize { clamped_row * size.columns() + clamped_col } -///Converts an 8 bit ANSI color to it's GPUI equivalent. -///Accepts usize for compatibility with the alacritty::Colors interface, -///Other than that use case, should only be called with values in the [0,255] range +/// Converts an 8 bit ANSI color to it's GPUI equivalent. +/// Accepts `usize` for compatibility with the `alacritty::Colors` interface, +/// Other than that use case, should only be called with values in the [0,255] range pub fn get_color_at_index(index: usize, theme: &Theme) -> Hsla { let colors = theme.colors(); @@ -1459,14 +1459,16 @@ pub fn get_color_at_index(index: usize, theme: &Theme) -> Hsla { } } -///Generates the rgb channels in [0, 5] for a given index into the 6x6x6 ANSI color cube -///See: [8 bit ansi color](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit). +/// Generates the RGB channels in [0, 5] for a given index into the 6x6x6 ANSI color cube. +/// See: [8 bit ANSI color](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit). /// -///Wikipedia gives a formula for calculating the index for a given color: +/// Wikipedia gives a formula for calculating the index for a given color: /// -///index = 16 + 36 × r + 6 × g + b (0 ≤ r, g, b ≤ 5) +/// ``` +/// index = 16 + 36 × r + 6 × g + b (0 ≤ r, g, b ≤ 5) +/// ``` /// -///This function does the reverse, calculating the r, g, and b components from a given index. +/// This function does the reverse, calculating the `r`, `g`, and `b` components from a given index. fn rgb_for_index(i: &u8) -> (u8, u8, u8) { debug_assert!((&16..=&231).contains(&i)); let i = i - 16; diff --git a/crates/terminal/src/terminal_settings.rs b/crates/terminal/src/terminal_settings.rs index f63b575bf214871e5391aefe9889fc5b69da2fba..14cff3b5a690a6bc370579604305ff5a1217a351 100644 --- a/crates/terminal/src/terminal_settings.rs +++ b/crates/terminal/src/terminal_settings.rs @@ -36,6 +36,9 @@ pub enum VenvSettings { #[default] Off, On { + /// Default directories to search for virtual environments, relative + /// to the current working directory. We recommend overriding this + /// in your project's settings, rather than globally. activate_script: Option, directories: Option>, }, @@ -73,20 +76,68 @@ pub enum ActivateScript { #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] pub struct TerminalSettingsContent { + /// What shell to use when opening a terminal. + /// + /// Default: system pub shell: Option, + /// What working directory to use when launching the terminal + /// + /// Default: current_project_directory pub working_directory: Option, + /// Set the terminal's font size. + /// + /// If this option is not included, + /// the terminal will default to matching the buffer's font size. pub font_size: Option, + /// Set the terminal's font family. + /// + /// If this option is not included, + /// the terminal will default to matching the buffer's font family. pub font_family: Option, + /// Set the terminal's line height. + /// + /// Default: comfortable pub line_height: Option, pub font_features: Option, + /// Any key-value pairs added to this list will be added to the terminal's + /// environment. Use `:` to separate multiple values. + /// + /// Default: {} pub env: Option>, + /// Set the cursor blinking behavior in the terminal. + /// + /// Default: terminal_controlled pub blinking: Option, + /// Set whether Alternate Scroll mode (code: ?1007) is active by default. + /// Alternate Scroll mode converts mouse scroll events into up / down key + /// presses when in the alternate screen (e.g. when running applications + /// like vim or less). The terminal can still set and unset this mode. + /// + /// Default: off pub alternate_scroll: Option, + /// Set whether the option key behaves as the meta key. + /// + /// Default: false pub option_as_meta: Option, + /// Whether or not selecting text in the terminal will automatically + /// copy to the system clipboard. + /// + /// Default: false pub copy_on_select: Option, pub dock: Option, + /// Default width when the terminal is docked to the left or right. + /// + /// Default: 640 pub default_width: Option, + /// Default height when the terminal is docked to the bottom. + /// + /// Default: 320 pub default_height: Option, + /// Activate the python virtual environment, if one is found, in the + /// terminal's working directory (as resolved by the working_directory + /// setting). Set this to "off" to disable this behavior. + /// + /// Default: on pub detect_venv: Option, } @@ -107,9 +158,13 @@ impl settings::Settings for TerminalSettings { #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)] #[serde(rename_all = "snake_case")] pub enum TerminalLineHeight { + /// Use a line height that's comfortable for reading, 1.618 #[default] Comfortable, + /// Use a standard line height, 1.3. This option is useful for TUIs, + /// particularly if they use box characters Standard, + /// Use a custom line height. Custom(f32), } @@ -127,17 +182,25 @@ impl TerminalLineHeight { #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum TerminalBlink { + /// Never blink the cursor, ignoring the terminal mode. Off, + /// Default the cursor blink to off, but allow the terminal to + /// set blinking. TerminalControlled, + /// Always blink the cursor, ignoring the terminal mode. On, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum Shell { + /// Use the system's default terminal configuration in /etc/passwd System, Program(String), - WithArguments { program: String, args: Vec }, + WithArguments { + program: String, + args: Vec, + }, } #[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] @@ -150,8 +213,15 @@ pub enum AlternateScroll { #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum WorkingDirectory { + /// Use the current file's project directory. Will Fallback to the + /// first project directory strategy if unsuccessful. CurrentProjectDirectory, + /// Use the first project in this workspace's directory. FirstProjectDirectory, + /// Always use this platform's home directory (if it can be found). AlwaysHome, + /// Slways use a specific directory. This value will be shell expanded. + /// If this path is not a valid directory the terminal will default to + /// this platform's home directory (if it can be found). Always { directory: String }, } diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index d936716032a53b432d2f6f1a5dc6b79069656c8b..c52dbcb3d8e453729bd0d1ab57dc8da94e131f58 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -424,7 +424,6 @@ impl TerminalElement { let line_height = font_pixels * line_height.to_pixels(rem_size); let font_id = cx.text_system().resolve_font(&text_style.font()); - // todo!(do we need to keep this unwrap?) let cell_width = text_system .advance(font_id, font_pixels, 'm') .unwrap() @@ -451,6 +450,18 @@ impl TerminalElement { } }); + let interactive_text_bounds = InteractiveBounds { + bounds, + stacking_order: cx.stacking_order().clone(), + }; + if interactive_text_bounds.visibly_contains(&cx.mouse_position(), cx) { + if self.can_navigate_to_selected_word && last_hovered_word.is_some() { + cx.set_cursor_style(gpui::CursorStyle::PointingHand) + } else { + cx.set_cursor_style(gpui::CursorStyle::IBeam) + } + } + let hyperlink_tooltip = last_hovered_word.clone().map(|hovered_word| { div() .size_full() @@ -512,7 +523,6 @@ impl TerminalElement { underline: Default::default(), }], ) - //todo!(do we need to keep this unwrap?) .unwrap() }; @@ -652,21 +662,6 @@ impl TerminalElement { }, ), ); - self.interactivity.on_click({ - let terminal = terminal.clone(); - move |e, cx| { - if e.down.button == MouseButton::Right { - let mouse_mode = terminal.update(cx, |terminal, _cx| { - terminal.mouse_mode(e.down.modifiers.shift) - }); - - if !mouse_mode { - //todo!(context menu) - // view.deploy_context_menu(e.position, cx); - } - } - } - }); self.interactivity.on_scroll_wheel({ let terminal = terminal.clone(); move |e, cx| { diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 99929535700e2badbe0c447ac6dfc4ee5998e7e2..d0b52f5eb217ed60cc9b62e20657e5033506f133 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -19,7 +19,7 @@ use workspace::{ dock::{DockPosition, Panel, PanelEvent}, item::Item, pane, - ui::Icon, + ui::IconName, DraggedTab, Pane, Workspace, }; @@ -71,7 +71,7 @@ impl TerminalPanel { h_stack() .gap_2() .child( - IconButton::new("plus", Icon::Plus) + IconButton::new("plus", IconName::Plus) .icon_size(IconSize::Small) .on_click(move |_, cx| { terminal_panel @@ -82,10 +82,10 @@ impl TerminalPanel { ) .child({ let zoomed = pane.is_zoomed(); - IconButton::new("toggle_zoom", Icon::Maximize) + IconButton::new("toggle_zoom", IconName::Maximize) .icon_size(IconSize::Small) .selected(zoomed) - .selected_icon(Icon::Minimize) + .selected_icon(IconName::Minimize) .on_click(cx.listener(|pane, _, cx| { pane.toggle_zoom(&workspace::ToggleZoom, cx); })) @@ -477,8 +477,8 @@ impl Panel for TerminalPanel { "TerminalPanel" } - fn icon(&self, _cx: &WindowContext) -> Option { - Some(Icon::Terminal) + fn icon(&self, _cx: &WindowContext) -> Option { + Some(IconName::Terminal) } fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> { diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 8f5044e49e01e5ed2244581f24d1663f8ee8c73a..b4a273dd0bc9085c4a6b3b069589ef1d9d640c5d 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -2,8 +2,6 @@ mod persistence; pub mod terminal_element; pub mod terminal_panel; -// todo!() -// use crate::terminal_element::TerminalElement; use editor::{scroll::autoscroll::Autoscroll, Editor}; use gpui::{ div, impl_actions, overlay, AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle, @@ -22,7 +20,7 @@ use terminal::{ Clear, Copy, Event, MaybeNavigationTarget, Paste, ShowCharacterPalette, Terminal, }; use terminal_element::TerminalElement; -use ui::{h_stack, prelude::*, ContextMenu, Icon, IconElement, Label}; +use ui::{h_stack, prelude::*, ContextMenu, Icon, IconName, Label}; use util::{paths::PathLikeWithPosition, ResultExt}; use workspace::{ item::{BreadcrumbText, Item, ItemEvent}, @@ -653,8 +651,10 @@ impl Render for TerminalView { .on_mouse_down( MouseButton::Right, cx.listener(|this, event: &MouseDownEvent, cx| { - this.deploy_context_menu(event.position, cx); - cx.notify(); + if !this.terminal.read(cx).mouse_mode(event.modifiers.shift) { + this.deploy_context_menu(event.position, cx); + cx.notify(); + } }), ) .child( @@ -692,7 +692,7 @@ impl Item for TerminalView { let title = self.terminal().read(cx).title(true); h_stack() .gap_2() - .child(IconElement::new(Icon::Terminal)) + .child(Icon::new(IconName::Terminal)) .child(Label::new(title).color(if selected { Color::Default } else { diff --git a/crates/text/src/anchor.rs b/crates/text/src/anchor.rs index 084be0e336e5ddbdaa2e5bd683aac763e5ca1735..a65e3753d49ae99285931646ffaa3dd7906818cf 100644 --- a/crates/text/src/anchor.rs +++ b/crates/text/src/anchor.rs @@ -90,7 +90,7 @@ impl Anchor { content.summary_for_anchor(self) } - /// Returns true when the [Anchor] is located inside a visible fragment. + /// Returns true when the [`Anchor`] is located inside a visible fragment. pub fn is_valid(&self, buffer: &BufferSnapshot) -> bool { if *self == Anchor::MIN || *self == Anchor::MAX { true diff --git a/crates/text/src/selection.rs b/crates/text/src/selection.rs index 4f1f9a29223b927cd993d53a9df27488c9b37ad0..480cb99d747783b7c7bfc100af8b57401781a984 100644 --- a/crates/text/src/selection.rs +++ b/crates/text/src/selection.rs @@ -5,7 +5,7 @@ use std::ops::Range; #[derive(Copy, Clone, Debug, PartialEq)] pub enum SelectionGoal { None, - HorizontalPosition(f32), // todo!("Can we use pixels here without adding a runtime gpui dependency?") + HorizontalPosition(f32), HorizontalRange { start: f32, end: f32 }, WrappedHorizontalPosition((u32, f32)), } diff --git a/crates/theme/src/scale.rs b/crates/theme/src/scale.rs index c1c2ff924c866473fb90f89842f0d32432b9da27..1146090edcc1e7fbef1f8cc09e43377db4183862 100644 --- a/crates/theme/src/scale.rs +++ b/crates/theme/src/scale.rs @@ -2,7 +2,9 @@ use gpui::{AppContext, Hsla, SharedString}; use crate::{ActiveTheme, Appearance}; -/// A one-based step in a [`ColorScale`]. +/// A collection of colors that are used to style the UI. +/// +/// Each step has a semantic meaning, and is used to style different parts of the UI. #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] pub struct ColorScaleStep(usize); @@ -37,6 +39,10 @@ impl ColorScaleStep { ]; } +/// A scale of colors for a given [`ColorScaleSet`]. +/// +/// Each [`ColorScale`] contains exactly 12 colors. Refer to +/// [`ColorScaleStep`] for a reference of what each step is used for. pub struct ColorScale(Vec); impl FromIterator for ColorScale { @@ -229,6 +235,7 @@ impl IntoIterator for ColorScales { } } +/// Provides groups of [`ColorScale`]s for light and dark themes, as well as transparent versions of each scale. pub struct ColorScaleSet { name: SharedString, light: ColorScale, diff --git a/crates/theme/src/settings.rs b/crates/theme/src/settings.rs index 624b14fe33fe194e2f8b7af3211ff4c9a144c100..3ecf1935a47eeb7fb4c8e4edbce9d99f33a2b3f3 100644 --- a/crates/theme/src/settings.rs +++ b/crates/theme/src/settings.rs @@ -27,7 +27,7 @@ pub struct ThemeSettings { } #[derive(Default)] -pub struct AdjustedBufferFontSize(Pixels); +pub(crate) struct AdjustedBufferFontSize(Pixels); #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] pub struct ThemeSettingsContent { diff --git a/crates/theme/src/styles/colors.rs b/crates/theme/src/styles/colors.rs index 5f2295866535deefbe5ff29f51662847b241692e..51f3949897c2195ac5620886cf6b58dedebea9f2 100644 --- a/crates/theme/src/styles/colors.rs +++ b/crates/theme/src/styles/colors.rs @@ -2,11 +2,12 @@ use gpui::Hsla; use refineable::Refineable; use std::sync::Arc; -use crate::{PlayerColors, StatusColors, SyntaxTheme, SystemColors}; +use crate::{PlayerColors, StatusColors, StatusColorsRefinement, SyntaxTheme, SystemColors}; #[derive(Refineable, Clone, Debug)] #[refineable(Debug, serde::Deserialize)] pub struct ThemeColors { + /// Border color. Used for most borders, is usually a high contrast color. pub border: Hsla, /// Border color. Used for deemphasized borders, like a visual divider between two sections pub border_variant: Hsla, @@ -219,7 +220,10 @@ pub struct ThemeStyles { #[refineable] pub colors: ThemeColors, + + #[refineable] pub status: StatusColors, + pub player: PlayerColors, pub syntax: Arc, } diff --git a/crates/theme/src/styles/players.rs b/crates/theme/src/styles/players.rs index d4d27e71237b0968d4642a92399dca754dada77e..b2b797db08b270513b77809437184f9b782af77d 100644 --- a/crates/theme/src/styles/players.rs +++ b/crates/theme/src/styles/players.rs @@ -122,12 +122,10 @@ impl PlayerColors { impl PlayerColors { pub fn local(&self) -> PlayerColor { - // todo!("use a valid color"); *self.0.first().unwrap() } pub fn absent(&self) -> PlayerColor { - // todo!("use a valid color"); *self.0.last().unwrap() } diff --git a/crates/theme/src/styles/status.rs b/crates/theme/src/styles/status.rs index 0ce166deb59b75a8abd10ea105b5efddd66fa33e..854b876ac20b33702f9f4cce467c162e610ad125 100644 --- a/crates/theme/src/styles/status.rs +++ b/crates/theme/src/styles/status.rs @@ -78,15 +78,6 @@ pub struct StatusColors { pub warning_border: Hsla, } -impl Default for StatusColors { - /// Don't use this! - /// We have to have a default to be `[refineable::Refinable]`. - /// todo!("Find a way to not need this for Refinable") - fn default() -> Self { - Self::dark() - } -} - pub struct DiagnosticColors { pub error: Hsla, pub warning: Hsla, diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index c526a381b72f29d0cb0dc9e9ff5724ba38a69c13..f8d90b7bdc823b0b52348fe94908454002616347 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -1,3 +1,11 @@ +//! # Theme +//! +//! This crate provides the theme system for Zed. +//! +//! ## Overview +//! +//! A theme is a collection of colors used to build a consistent appearance for UI components across the application. + mod default_colors; mod default_theme; mod one_themes; @@ -73,13 +81,6 @@ impl ActiveTheme for AppContext { } } -// todo!() -// impl<'a> ActiveTheme for WindowContext<'a> { -// fn theme(&self) -> &Arc { -// &ThemeSettings::get_global(self.app()).active_theme -// } -// } - pub struct ThemeFamily { pub id: String, pub name: SharedString, diff --git a/crates/theme/theme.md b/crates/theme/theme.md new file mode 100644 index 0000000000000000000000000000000000000000..f9a7a581786eff51b53d1209e6a8518ae60403a5 --- /dev/null +++ b/crates/theme/theme.md @@ -0,0 +1,15 @@ + # Theme + + This crate provides the theme system for Zed. + + ## Overview + + A theme is a collection of colors used to build a consistent appearance for UI components across the application. + To produce a theme in Zed, + + A theme is made of of two parts: A [ThemeFamily] and one or more [Theme]s. + +// + A [ThemeFamily] contains metadata like theme name, author, and theme-specific [ColorScales] as well as a series of themes. + + - [ThemeColors] - A set of colors that are used to style the UI. Refer to the [ThemeColors] documentation for more information. diff --git a/crates/ui/docs/hello-world.md b/crates/ui/docs/hello-world.md index 280763e27b114e56cbb27bb6859a15ec940d881b..12ee4d7abaaef55f0d7f5e674cdff97927a4182c 100644 --- a/crates/ui/docs/hello-world.md +++ b/crates/ui/docs/hello-world.md @@ -74,7 +74,7 @@ As you start using the Tailwind-style conventions you will be surprised how quic **Why `50.0/360.0` in `hsla()`?** -gpui [gpui::Hsla] use `0.0-1.0` for all it's values, but it is common for tools to use `0-360` for hue. +gpui [gpui::Hsla] use `0.0-1.0` for all its values, but it is common for tools to use `0-360` for hue. This may change in the future, but this is a little trick that let's you use familiar looking values. diff --git a/crates/ui/src/clickable.rs b/crates/ui/src/clickable.rs index 44f40b4cd4ccb98244a369b70639e3a768f4ddfa..462d2c60998a993527fc5dab24d1b0a2ed260d86 100644 --- a/crates/ui/src/clickable.rs +++ b/crates/ui/src/clickable.rs @@ -1,6 +1,6 @@ use gpui::{ClickEvent, WindowContext}; -/// A trait for elements that can be clicked. +/// A trait for elements that can be clicked. Enables the use of the `on_click` method. pub trait Clickable { /// Sets the click handler that will fire whenever the element is clicked. fn on_click(self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self; diff --git a/crates/ui/src/components/avatar.rs b/crates/ui/src/components/avatar.rs index b1dba69520c4d02ac735885f69d84db5d653236b..9e64e1223c346f4d153fb7d2955b606ff53736ae 100644 --- a/crates/ui/src/components/avatar.rs +++ b/crates/ui/src/components/avatar.rs @@ -1,13 +1,28 @@ use crate::prelude::*; use gpui::{img, Hsla, ImageSource, Img, IntoElement, Styled}; +/// The shape of an [`Avatar`]. #[derive(Debug, Default, PartialEq, Clone)] -pub enum Shape { +pub enum AvatarShape { + /// The avatar is shown in a circle. #[default] Circle, + /// The avatar is shown in a rectangle with rounded corners. RoundedRectangle, } +/// An element that renders a user avatar with customizable appearance options. +/// +/// # Examples +/// +/// ``` +/// use ui::{Avatar, AvatarShape}; +/// +/// Avatar::new("path/to/image.png") +/// .shape(AvatarShape::Circle) +/// .grayscale(true) +/// .border_color(gpui::red()); +/// ``` #[derive(IntoElement)] pub struct Avatar { image: Img, @@ -18,7 +33,7 @@ pub struct Avatar { impl RenderOnce for Avatar { fn render(mut self, cx: &mut WindowContext) -> impl IntoElement { if self.image.style().corner_radii.top_left.is_none() { - self = self.shape(Shape::Circle); + self = self.shape(AvatarShape::Circle); } let size = cx.rem_size(); @@ -66,14 +81,35 @@ impl Avatar { } } - pub fn shape(mut self, shape: Shape) -> Self { + /// Sets the shape of the avatar image. + /// + /// This method allows the shape of the avatar to be specified using a [`Shape`]. + /// It modifies the corner radius of the image to match the specified shape. + /// + /// # Examples + /// + /// ``` + /// use ui::{Avatar, AvatarShape}; + /// + /// Avatar::new("path/to/image.png").shape(AvatarShape::Circle); + /// ``` + pub fn shape(mut self, shape: AvatarShape) -> Self { self.image = match shape { - Shape::Circle => self.image.rounded_full(), - Shape::RoundedRectangle => self.image.rounded_md(), + AvatarShape::Circle => self.image.rounded_full(), + AvatarShape::RoundedRectangle => self.image.rounded_md(), }; self } + /// Applies a grayscale filter to the avatar image. + /// + /// # Examples + /// + /// ``` + /// use ui::{Avatar, AvatarShape}; + /// + /// let avatar = Avatar::new("path/to/image.png").grayscale(true); + /// ``` pub fn grayscale(mut self, grayscale: bool) -> Self { self.image = self.image.grayscale(grayscale); self diff --git a/crates/ui/src/components/button/button.rs b/crates/ui/src/components/button/button.rs index 1e60aae03b11e84eda1a2041565c6b75a5fae79f..fcc30e633815fd764909c32bb7bbc34b8c5e0624 100644 --- a/crates/ui/src/components/button/button.rs +++ b/crates/ui/src/components/button/button.rs @@ -2,11 +2,78 @@ use gpui::{AnyView, DefiniteLength}; use crate::{prelude::*, IconPosition, KeyBinding}; use crate::{ - ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, Icon, IconSize, Label, LineHeightStyle, + ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, IconName, IconSize, Label, LineHeightStyle, }; use super::button_icon::ButtonIcon; +/// An element that creates a button with a label and an optional icon. +/// +/// Common buttons: +/// - Label, Icon + Label: [`Button`] (this component) +/// - Icon only: [`IconButton`] +/// - Custom: [`ButtonLike`] +/// +/// To create a more complex button than what the [`Button`] or [`IconButton`] components provide, use +/// [`ButtonLike`] directly. +/// +/// # Examples +/// +/// **A button with a label**, is typically used in scenarios such as a form, where the button's label +/// indicates what action will be performed when the button is clicked. +/// +/// ``` +/// use ui::prelude::*; +/// +/// Button::new("button_id", "Click me!") +/// .on_click(|event, cx| { +/// // Handle click event +/// }); +/// ``` +/// +/// **A toggleable button**, is typically used in scenarios such as a toolbar, +/// where the button's state indicates whether a feature is enabled or not, or +/// a trigger for a popover menu, where clicking the button toggles the visibility of the menu. +/// +/// ``` +/// use ui::prelude::*; +/// +/// Button::new("button_id", "Click me!") +/// .icon(IconName::Check) +/// .selected(true) +/// .on_click(|event, cx| { +/// // Handle click event +/// }); +/// ``` +/// +/// To change the style of the button when it is selected use the [`selected_style`][Button::selected_style] method. +/// +/// ``` +/// use ui::prelude::*; +/// use ui::TintColor; +/// +/// Button::new("button_id", "Click me!") +/// .selected(true) +/// .selected_style(ButtonStyle::Tinted(TintColor::Accent)) +/// .on_click(|event, cx| { +/// // Handle click event +/// }); +/// ``` +/// This will create a button with a blue tinted background when selected. +/// +/// **A full-width button**, is typically used in scenarios such as the bottom of a modal or form, where it occupies the entire width of its container. +/// The button's content, including text and icons, is centered by default. +/// +/// ``` +/// use ui::prelude::*; +/// +/// let button = Button::new("button_id", "Click me!") +/// .full_width() +/// .on_click(|event, cx| { +/// // Handle click event +/// }); +/// ``` +/// #[derive(IntoElement)] pub struct Button { base: ButtonLike, @@ -14,15 +81,21 @@ pub struct Button { label_color: Option, label_size: Option, selected_label: Option, - icon: Option, + icon: Option, icon_position: Option, icon_size: Option, icon_color: Option, - selected_icon: Option, + selected_icon: Option, key_binding: Option, } impl Button { + /// Creates a new [`Button`] with a specified identifier and label. + /// + /// This is the primary constructor for a [`Button`] component. It initializes + /// the button with the provided identifier and label text, setting all other + /// properties to their default values, which can be customized using the + /// builder pattern methods provided by this struct. pub fn new(id: impl Into, label: impl Into) -> Self { Self { base: ButtonLike::new(id), @@ -39,46 +112,55 @@ impl Button { } } + /// Sets the color of the button's label. pub fn color(mut self, label_color: impl Into>) -> Self { self.label_color = label_color.into(); self } + /// Defines the size of the button's label. pub fn label_size(mut self, label_size: impl Into>) -> Self { self.label_size = label_size.into(); self } + /// Sets the label used when the button is in a selected state. pub fn selected_label>(mut self, label: impl Into>) -> Self { self.selected_label = label.into().map(Into::into); self } - pub fn icon(mut self, icon: impl Into>) -> Self { + /// Assigns an icon to the button. + pub fn icon(mut self, icon: impl Into>) -> Self { self.icon = icon.into(); self } + /// Sets the position of the icon relative to the label. pub fn icon_position(mut self, icon_position: impl Into>) -> Self { self.icon_position = icon_position.into(); self } + /// Specifies the size of the button's icon. pub fn icon_size(mut self, icon_size: impl Into>) -> Self { self.icon_size = icon_size.into(); self } + /// Sets the color of the button's icon. pub fn icon_color(mut self, icon_color: impl Into>) -> Self { self.icon_color = icon_color.into(); self } - pub fn selected_icon(mut self, icon: impl Into>) -> Self { + /// Chooses an icon to display when the button is in a selected state. + pub fn selected_icon(mut self, icon: impl Into>) -> Self { self.selected_icon = icon.into(); self } + /// Binds a key combination to the button for keyboard shortcuts. pub fn key_binding(mut self, key_binding: impl Into>) -> Self { self.key_binding = key_binding.into(); self @@ -86,6 +168,24 @@ impl Button { } impl Selectable for Button { + /// Sets the selected state of the button. + /// + /// This method allows the selection state of the button to be specified. + /// It modifies the button's appearance to reflect its selected state. + /// + /// # Examples + /// + /// ``` + /// use ui::prelude::*; + /// + /// Button::new("button_id", "Click me!") + /// .selected(true) + /// .on_click(|event, cx| { + /// // Handle click event + /// }); + /// ``` + /// + /// Use [`selected_style`](Button::selected_style) to change the style of the button when it is selected. fn selected(mut self, selected: bool) -> Self { self.base = self.base.selected(selected); self @@ -93,6 +193,22 @@ impl Selectable for Button { } impl SelectableButton for Button { + /// Sets the style for the button when selected. + /// + /// # Examples + /// + /// ``` + /// use ui::prelude::*; + /// use ui::TintColor; + /// + /// Button::new("button_id", "Click me!") + /// .selected(true) + /// .selected_style(ButtonStyle::Tinted(TintColor::Accent)) + /// .on_click(|event, cx| { + /// // Handle click event + /// }); + /// ``` + /// This results in a button with a blue tinted background when selected. fn selected_style(mut self, style: ButtonStyle) -> Self { self.base = self.base.selected_style(style); self @@ -100,6 +216,24 @@ impl SelectableButton for Button { } impl Disableable for Button { + /// Disables the button. + /// + /// This method allows the button to be disabled. When a button is disabled, + /// it doesn't react to user interactions and its appearance is updated to reflect this. + /// + /// # Examples + /// + /// ``` + /// use ui::prelude::*; + /// + /// Button::new("button_id", "Click me!") + /// .disabled(true) + /// .on_click(|event, cx| { + /// // Handle click event + /// }); + /// ``` + /// + /// This results in a button that is disabled and does not respond to click events. fn disabled(mut self, disabled: bool) -> Self { self.base = self.base.disabled(disabled); self @@ -107,6 +241,7 @@ impl Disableable for Button { } impl Clickable for Button { + /// Sets the click event handler for the button. fn on_click( mut self, handler: impl Fn(&gpui::ClickEvent, &mut WindowContext) + 'static, @@ -117,11 +252,44 @@ impl Clickable for Button { } impl FixedWidth for Button { + /// Sets a fixed width for the button. + /// + /// This function allows a button to have a fixed width instead of automatically growing or shrinking. + /// Sets a fixed width for the button. + /// + /// # Examples + /// + /// ``` + /// use ui::prelude::*; + /// + /// Button::new("button_id", "Click me!") + /// .width(px(100.).into()) + /// .on_click(|event, cx| { + /// // Handle click event + /// }); + /// ``` + /// + /// This sets the button's width to be exactly 100 pixels. fn width(mut self, width: DefiniteLength) -> Self { self.base = self.base.width(width); self } + /// Sets the button to occupy the full width of its container. + /// + /// # Examples + /// + /// ``` + /// use ui::prelude::*; + /// + /// Button::new("button_id", "Click me!") + /// .full_width() + /// .on_click(|event, cx| { + /// // Handle click event + /// }); + /// ``` + /// + /// This stretches the button to the full width of its container. fn full_width(mut self) -> Self { self.base = self.base.full_width(); self @@ -129,20 +297,45 @@ impl FixedWidth for Button { } impl ButtonCommon for Button { + /// Sets the button's id. fn id(&self) -> &ElementId { self.base.id() } + /// Sets the visual style of the button using a [`ButtonStyle`]. fn style(mut self, style: ButtonStyle) -> Self { self.base = self.base.style(style); self } + /// Sets the button's size using a [`ButtonSize`]. fn size(mut self, size: ButtonSize) -> Self { self.base = self.base.size(size); self } + /// Sets a tooltip for the button. + /// + /// This method allows a tooltip to be set for the button. The tooltip is a function that + /// takes a mutable reference to a [`WindowContext`] and returns an [`AnyView`]. The tooltip + /// is displayed when the user hovers over the button. + /// + /// # Examples + /// + /// ``` + /// use ui::prelude::*; + /// use ui::Tooltip; + /// + /// Button::new("button_id", "Click me!") + /// .tooltip(move |cx| { + /// Tooltip::text("This is a tooltip", cx) + /// }) + /// .on_click(|event, cx| { + /// // Handle click event + /// }); + /// ``` + /// + /// This will create a button with a tooltip that displays "This is a tooltip" when hovered over. fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self { self.base = self.base.tooltip(tooltip); self diff --git a/crates/ui/src/components/button/button_icon.rs b/crates/ui/src/components/button/button_icon.rs index 15538bb24d79b9f17c3fcef8c4de37466e84fc66..b8f5427d30aaa6dfb21b802bdd49922de9e17433 100644 --- a/crates/ui/src/components/button/button_icon.rs +++ b/crates/ui/src/components/button/button_icon.rs @@ -1,4 +1,4 @@ -use crate::{prelude::*, Icon, IconElement, IconSize}; +use crate::{prelude::*, Icon, IconName, IconSize}; /// An icon that appears within a button. /// @@ -6,17 +6,17 @@ use crate::{prelude::*, Icon, IconElement, IconSize}; /// or as a standalone icon, like in [`IconButton`](crate::IconButton). #[derive(IntoElement)] pub(super) struct ButtonIcon { - icon: Icon, + icon: IconName, size: IconSize, color: Color, disabled: bool, selected: bool, - selected_icon: Option, + selected_icon: Option, selected_style: Option, } impl ButtonIcon { - pub fn new(icon: Icon) -> Self { + pub fn new(icon: IconName) -> Self { Self { icon, size: IconSize::default(), @@ -44,7 +44,7 @@ impl ButtonIcon { self } - pub fn selected_icon(mut self, icon: impl Into>) -> Self { + pub fn selected_icon(mut self, icon: impl Into>) -> Self { self.selected_icon = icon.into(); self } @@ -88,6 +88,6 @@ impl RenderOnce for ButtonIcon { self.color }; - IconElement::new(icon).size(self.size).color(icon_color) + Icon::new(icon).size(self.size).color(icon_color) } } diff --git a/crates/ui/src/components/button/button_like.rs b/crates/ui/src/components/button/button_like.rs index 431286073fb33b263fd08c27ba1c07e44fe82c01..3e4b478a9a237c44b95e5de09158e35e40e55449 100644 --- a/crates/ui/src/components/button/button_like.rs +++ b/crates/ui/src/components/button/button_like.rs @@ -4,10 +4,12 @@ use smallvec::SmallVec; use crate::prelude::*; +/// A trait for buttons that can be Selected. Enables setting the [`ButtonStyle`] of a button when it is selected. pub trait SelectableButton: Selectable { fn selected_style(self, style: ButtonStyle) -> Self; } +/// A common set of traits all buttons must implement. pub trait ButtonCommon: Clickable + Disableable { /// A unique element ID to identify the button. fn id(&self) -> &ElementId; @@ -93,6 +95,7 @@ impl From for Color { } } +/// The visual appearance of a button. #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)] pub enum ButtonStyle { /// A filled button with a solid background color. Provides emphasis versus @@ -260,8 +263,9 @@ impl ButtonStyle { } } -/// ButtonSize can also be used to help build non-button elements -/// that are consistently sized with buttons. +/// The height of a button. +/// +/// Can also be used to size non-button elements to align with [`Button`]s. #[derive(Default, PartialEq, Clone, Copy)] pub enum ButtonSize { Large, diff --git a/crates/ui/src/components/button/icon_button.rs b/crates/ui/src/components/button/icon_button.rs index d9ed6ccb5d86a2c5122ef6cd2fd364be235566bc..7c5313497c77ff6532838966001d2cbd4a3688e0 100644 --- a/crates/ui/src/components/button/icon_button.rs +++ b/crates/ui/src/components/button/icon_button.rs @@ -1,21 +1,21 @@ use gpui::{AnyView, DefiniteLength}; use crate::{prelude::*, SelectableButton}; -use crate::{ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, Icon, IconSize}; +use crate::{ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, IconName, IconSize}; use super::button_icon::ButtonIcon; #[derive(IntoElement)] pub struct IconButton { base: ButtonLike, - icon: Icon, + icon: IconName, icon_size: IconSize, icon_color: Color, - selected_icon: Option, + selected_icon: Option, } impl IconButton { - pub fn new(id: impl Into, icon: Icon) -> Self { + pub fn new(id: impl Into, icon: IconName) -> Self { Self { base: ButtonLike::new(id), icon, @@ -35,7 +35,7 @@ impl IconButton { self } - pub fn selected_icon(mut self, icon: impl Into>) -> Self { + pub fn selected_icon(mut self, icon: impl Into>) -> Self { self.selected_icon = icon.into(); self } diff --git a/crates/ui/src/components/checkbox.rs b/crates/ui/src/components/checkbox.rs index 3b778420291980ff70e3e8cebaa5b3c4d411033f..2180e0061773f8626292224e8fb3a2b97ab4cc65 100644 --- a/crates/ui/src/components/checkbox.rs +++ b/crates/ui/src/components/checkbox.rs @@ -1,7 +1,7 @@ use gpui::{div, prelude::*, ElementId, IntoElement, Styled, WindowContext}; use crate::prelude::*; -use crate::{Color, Icon, IconElement, Selection}; +use crate::{Color, Icon, IconName, Selection}; pub type CheckHandler = Box; @@ -47,7 +47,7 @@ impl RenderOnce for Checkbox { let group_id = format!("checkbox_group_{:?}", self.id); let icon = match self.checked { - Selection::Selected => Some(IconElement::new(Icon::Check).size(IconSize::Small).color( + Selection::Selected => Some(Icon::new(IconName::Check).size(IconSize::Small).color( if self.disabled { Color::Disabled } else { @@ -55,7 +55,7 @@ impl RenderOnce for Checkbox { }, )), Selection::Indeterminate => Some( - IconElement::new(Icon::Dash) + Icon::new(IconName::Dash) .size(IconSize::Small) .color(if self.disabled { Color::Disabled @@ -73,7 +73,7 @@ impl RenderOnce for Checkbox { // - 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. + // but its icon will be different. let selected = self.checked == Selection::Selected || self.checked == Selection::Indeterminate; diff --git a/crates/ui/src/components/context_menu.rs b/crates/ui/src/components/context_menu.rs index 8666ec65651d6f00cbe225f7b065521673326037..098c54f33cb98d831c0806a2b4ec9f5b0a18ad07 100644 --- a/crates/ui/src/components/context_menu.rs +++ b/crates/ui/src/components/context_menu.rs @@ -1,6 +1,6 @@ use crate::{ - h_stack, prelude::*, v_stack, Icon, IconElement, KeyBinding, Label, List, ListItem, - ListSeparator, ListSubHeader, + h_stack, prelude::*, v_stack, Icon, IconName, KeyBinding, Label, List, ListItem, ListSeparator, + ListSubHeader, }; use gpui::{ px, Action, AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, @@ -14,7 +14,7 @@ enum ContextMenuItem { Header(SharedString), Entry { label: SharedString, - icon: Option, + icon: Option, handler: Rc, action: Option>, }, @@ -117,7 +117,7 @@ impl ContextMenu { label: label.into(), action: Some(action.boxed_clone()), handler: Rc::new(move |cx| cx.dispatch_action(action.boxed_clone())), - icon: Some(Icon::Link), + icon: Some(IconName::Link), }); self } @@ -280,7 +280,7 @@ impl Render for ContextMenu { h_stack() .gap_1() .child(Label::new(label.clone())) - .child(IconElement::new(*icon)) + .child(Icon::new(*icon)) .into_any_element() } else { Label::new(label.clone()).into_any_element() diff --git a/crates/ui/src/components/disclosure.rs b/crates/ui/src/components/disclosure.rs index d4349f61a0f53b7f70af3255576aaa4c715796ae..59651ddb0b5ec9154c0180ca89e2331007cd3404 100644 --- a/crates/ui/src/components/disclosure.rs +++ b/crates/ui/src/components/disclosure.rs @@ -1,6 +1,6 @@ use gpui::ClickEvent; -use crate::{prelude::*, Color, Icon, IconButton, IconSize}; +use crate::{prelude::*, Color, IconButton, IconName, IconSize}; #[derive(IntoElement)] pub struct Disclosure { @@ -32,8 +32,8 @@ impl RenderOnce for Disclosure { IconButton::new( self.id, match self.is_open { - true => Icon::ChevronDown, - false => Icon::ChevronRight, + true => IconName::ChevronDown, + false => IconName::ChevronRight, }, ) .icon_color(Color::Muted) diff --git a/crates/ui/src/components/divider.rs b/crates/ui/src/components/divider.rs index 2567c3fc3476c72819f0d30f5eae2a3e74f68059..772fc1a81a34b816649efeae19d36fc7405c66b2 100644 --- a/crates/ui/src/components/divider.rs +++ b/crates/ui/src/components/divider.rs @@ -7,6 +7,7 @@ enum DividerDirection { Vertical, } +/// The color of a [`Divider`]. #[derive(Default)] pub enum DividerColor { Border, diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs index 4c6e48c0fc034dbcfbd4454c920b2d4a994b92b0..908e76ef918b56aefff6949e86ea6473a272253d 100644 --- a/crates/ui/src/components/icon.rs +++ b/crates/ui/src/components/icon.rs @@ -22,7 +22,7 @@ impl IconSize { } #[derive(Debug, PartialEq, Copy, Clone, EnumIter)] -pub enum Icon { +pub enum IconName { Ai, ArrowDown, ArrowLeft, @@ -111,118 +111,108 @@ pub enum Icon { ZedXCopilot, } -impl Icon { +impl IconName { pub fn path(self) -> &'static str { match self { - Icon::Ai => "icons/ai.svg", - Icon::ArrowDown => "icons/arrow_down.svg", - Icon::ArrowLeft => "icons/arrow_left.svg", - Icon::ArrowRight => "icons/arrow_right.svg", - Icon::ArrowUp => "icons/arrow_up.svg", - Icon::ArrowUpRight => "icons/arrow_up_right.svg", - Icon::ArrowCircle => "icons/arrow_circle.svg", - Icon::AtSign => "icons/at_sign.svg", - Icon::AudioOff => "icons/speaker_off.svg", - Icon::AudioOn => "icons/speaker_loud.svg", - Icon::Backspace => "icons/backspace.svg", - Icon::Bell => "icons/bell.svg", - 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", - Icon::ChevronRight => "icons/chevron_right.svg", - Icon::ChevronUp => "icons/chevron_up.svg", - Icon::Close => "icons/x.svg", - Icon::Collab => "icons/user_group_16.svg", - Icon::Command => "icons/command.svg", - Icon::Control => "icons/control.svg", - Icon::Copilot => "icons/copilot.svg", - Icon::CopilotDisabled => "icons/copilot_disabled.svg", - Icon::CopilotError => "icons/copilot_error.svg", - Icon::CopilotInit => "icons/copilot_init.svg", - Icon::Copy => "icons/copy.svg", - Icon::Dash => "icons/dash.svg", - Icon::Delete => "icons/delete.svg", - Icon::Disconnected => "icons/disconnected.svg", - Icon::Ellipsis => "icons/ellipsis.svg", - Icon::Envelope => "icons/feedback.svg", - Icon::Escape => "icons/escape.svg", - Icon::ExclamationTriangle => "icons/warning.svg", - Icon::Exit => "icons/exit.svg", - Icon::ExternalLink => "icons/external_link.svg", - Icon::File => "icons/file.svg", - Icon::FileDoc => "icons/file_icons/book.svg", - Icon::FileGeneric => "icons/file_icons/file.svg", - Icon::FileGit => "icons/file_icons/git.svg", - Icon::FileLock => "icons/file_icons/lock.svg", - Icon::FileRust => "icons/file_icons/rust.svg", - Icon::FileToml => "icons/file_icons/toml.svg", - Icon::FileTree => "icons/project.svg", - Icon::Filter => "icons/filter.svg", - Icon::Folder => "icons/file_icons/folder.svg", - Icon::FolderOpen => "icons/file_icons/folder_open.svg", - Icon::FolderX => "icons/stop_sharing.svg", - Icon::Github => "icons/github.svg", - Icon::Hash => "icons/hash.svg", - Icon::InlayHint => "icons/inlay_hint.svg", - Icon::Link => "icons/link.svg", - Icon::MagicWand => "icons/magic_wand.svg", - Icon::MagnifyingGlass => "icons/magnifying_glass.svg", - Icon::MailOpen => "icons/mail_open.svg", - Icon::Maximize => "icons/maximize.svg", - Icon::Menu => "icons/menu.svg", - Icon::MessageBubbles => "icons/conversations.svg", - Icon::Mic => "icons/mic.svg", - Icon::MicMute => "icons/mic_mute.svg", - Icon::Minimize => "icons/minimize.svg", - Icon::Option => "icons/option.svg", - Icon::PageDown => "icons/page_down.svg", - Icon::PageUp => "icons/page_up.svg", - Icon::Plus => "icons/plus.svg", - Icon::Public => "icons/public.svg", - Icon::Quote => "icons/quote.svg", - Icon::Replace => "icons/replace.svg", - Icon::ReplaceAll => "icons/replace_all.svg", - Icon::ReplaceNext => "icons/replace_next.svg", - Icon::Return => "icons/return.svg", - Icon::Screen => "icons/desktop.svg", - Icon::SelectAll => "icons/select_all.svg", - Icon::Shift => "icons/shift.svg", - Icon::Snip => "icons/snip.svg", - Icon::Space => "icons/space.svg", - Icon::Split => "icons/split.svg", - Icon::Tab => "icons/tab.svg", - Icon::Terminal => "icons/terminal.svg", - Icon::Update => "icons/update.svg", - Icon::WholeWord => "icons/word_search.svg", - Icon::XCircle => "icons/error.svg", - Icon::ZedXCopilot => "icons/zed_x_copilot.svg", + IconName::Ai => "icons/ai.svg", + IconName::ArrowDown => "icons/arrow_down.svg", + IconName::ArrowLeft => "icons/arrow_left.svg", + IconName::ArrowRight => "icons/arrow_right.svg", + IconName::ArrowUp => "icons/arrow_up.svg", + IconName::ArrowUpRight => "icons/arrow_up_right.svg", + IconName::ArrowCircle => "icons/arrow_circle.svg", + IconName::AtSign => "icons/at_sign.svg", + IconName::AudioOff => "icons/speaker_off.svg", + IconName::AudioOn => "icons/speaker_loud.svg", + IconName::Backspace => "icons/backspace.svg", + IconName::Bell => "icons/bell.svg", + IconName::BellOff => "icons/bell_off.svg", + IconName::BellRing => "icons/bell_ring.svg", + IconName::Bolt => "icons/bolt.svg", + IconName::CaseSensitive => "icons/case_insensitive.svg", + IconName::Check => "icons/check.svg", + IconName::ChevronDown => "icons/chevron_down.svg", + IconName::ChevronLeft => "icons/chevron_left.svg", + IconName::ChevronRight => "icons/chevron_right.svg", + IconName::ChevronUp => "icons/chevron_up.svg", + IconName::Close => "icons/x.svg", + IconName::Collab => "icons/user_group_16.svg", + IconName::Command => "icons/command.svg", + IconName::Control => "icons/control.svg", + IconName::Copilot => "icons/copilot.svg", + IconName::CopilotDisabled => "icons/copilot_disabled.svg", + IconName::CopilotError => "icons/copilot_error.svg", + IconName::CopilotInit => "icons/copilot_init.svg", + IconName::Copy => "icons/copy.svg", + IconName::Dash => "icons/dash.svg", + IconName::Delete => "icons/delete.svg", + IconName::Disconnected => "icons/disconnected.svg", + IconName::Ellipsis => "icons/ellipsis.svg", + IconName::Envelope => "icons/feedback.svg", + IconName::Escape => "icons/escape.svg", + IconName::ExclamationTriangle => "icons/warning.svg", + IconName::Exit => "icons/exit.svg", + IconName::ExternalLink => "icons/external_link.svg", + IconName::File => "icons/file.svg", + IconName::FileDoc => "icons/file_icons/book.svg", + IconName::FileGeneric => "icons/file_icons/file.svg", + IconName::FileGit => "icons/file_icons/git.svg", + IconName::FileLock => "icons/file_icons/lock.svg", + IconName::FileRust => "icons/file_icons/rust.svg", + IconName::FileToml => "icons/file_icons/toml.svg", + IconName::FileTree => "icons/project.svg", + IconName::Filter => "icons/filter.svg", + IconName::Folder => "icons/file_icons/folder.svg", + IconName::FolderOpen => "icons/file_icons/folder_open.svg", + IconName::FolderX => "icons/stop_sharing.svg", + IconName::Github => "icons/github.svg", + IconName::Hash => "icons/hash.svg", + IconName::InlayHint => "icons/inlay_hint.svg", + IconName::Link => "icons/link.svg", + IconName::MagicWand => "icons/magic_wand.svg", + IconName::MagnifyingGlass => "icons/magnifying_glass.svg", + IconName::MailOpen => "icons/mail_open.svg", + IconName::Maximize => "icons/maximize.svg", + IconName::Menu => "icons/menu.svg", + IconName::MessageBubbles => "icons/conversations.svg", + IconName::Mic => "icons/mic.svg", + IconName::MicMute => "icons/mic_mute.svg", + IconName::Minimize => "icons/minimize.svg", + IconName::Option => "icons/option.svg", + IconName::PageDown => "icons/page_down.svg", + IconName::PageUp => "icons/page_up.svg", + IconName::Plus => "icons/plus.svg", + IconName::Public => "icons/public.svg", + IconName::Quote => "icons/quote.svg", + IconName::Replace => "icons/replace.svg", + IconName::ReplaceAll => "icons/replace_all.svg", + IconName::ReplaceNext => "icons/replace_next.svg", + IconName::Return => "icons/return.svg", + IconName::Screen => "icons/desktop.svg", + IconName::SelectAll => "icons/select_all.svg", + IconName::Shift => "icons/shift.svg", + IconName::Snip => "icons/snip.svg", + IconName::Space => "icons/space.svg", + IconName::Split => "icons/split.svg", + IconName::Tab => "icons/tab.svg", + IconName::Terminal => "icons/terminal.svg", + IconName::Update => "icons/update.svg", + IconName::WholeWord => "icons/word_search.svg", + IconName::XCircle => "icons/error.svg", + IconName::ZedXCopilot => "icons/zed_x_copilot.svg", } } } #[derive(IntoElement)] -pub struct IconElement { +pub struct Icon { path: SharedString, color: Color, size: IconSize, } -impl RenderOnce for IconElement { - fn render(self, cx: &mut WindowContext) -> impl IntoElement { - svg() - .size(self.size.rems()) - .flex_none() - .path(self.path) - .text_color(self.color.color(cx)) - } -} - -impl IconElement { - pub fn new(icon: Icon) -> Self { +impl Icon { + pub fn new(icon: IconName) -> Self { Self { path: icon.path().into(), color: Color::default(), @@ -248,3 +238,13 @@ impl IconElement { self } } + +impl RenderOnce for Icon { + fn render(self, cx: &mut WindowContext) -> impl IntoElement { + svg() + .size(self.size.rems()) + .flex_none() + .path(self.path) + .text_color(self.color.color(cx)) + } +} diff --git a/crates/ui/src/components/keybinding.rs b/crates/ui/src/components/keybinding.rs index 671f9810831649f8f83dac2e25f88f2223febc40..e0e0583b7cb25e4966c183ae54d9f4742c66935d 100644 --- a/crates/ui/src/components/keybinding.rs +++ b/crates/ui/src/components/keybinding.rs @@ -1,4 +1,4 @@ -use crate::{h_stack, prelude::*, Icon, IconElement, IconSize}; +use crate::{h_stack, prelude::*, Icon, IconName, IconSize}; use gpui::{relative, rems, Action, FocusHandle, IntoElement, Keystroke}; #[derive(IntoElement, Clone)] @@ -26,16 +26,16 @@ impl RenderOnce for KeyBinding { .text_color(cx.theme().colors().text_muted) .when(keystroke.modifiers.function, |el| el.child(Key::new("fn"))) .when(keystroke.modifiers.control, |el| { - el.child(KeyIcon::new(Icon::Control)) + el.child(KeyIcon::new(IconName::Control)) }) .when(keystroke.modifiers.alt, |el| { - el.child(KeyIcon::new(Icon::Option)) + el.child(KeyIcon::new(IconName::Option)) }) .when(keystroke.modifiers.command, |el| { - el.child(KeyIcon::new(Icon::Command)) + el.child(KeyIcon::new(IconName::Command)) }) .when(keystroke.modifiers.shift, |el| { - el.child(KeyIcon::new(Icon::Shift)) + el.child(KeyIcon::new(IconName::Shift)) }) .when_some(key_icon, |el, icon| el.child(KeyIcon::new(icon))) .when(key_icon.is_none(), |el| { @@ -62,21 +62,21 @@ impl KeyBinding { Some(Self::new(key_binding)) } - fn icon_for_key(keystroke: &Keystroke) -> Option { + fn icon_for_key(keystroke: &Keystroke) -> Option { match keystroke.key.as_str() { - "left" => Some(Icon::ArrowLeft), - "right" => Some(Icon::ArrowRight), - "up" => Some(Icon::ArrowUp), - "down" => Some(Icon::ArrowDown), - "backspace" => Some(Icon::Backspace), - "delete" => Some(Icon::Delete), - "return" => Some(Icon::Return), - "enter" => Some(Icon::Return), - "tab" => Some(Icon::Tab), - "space" => Some(Icon::Space), - "escape" => Some(Icon::Escape), - "pagedown" => Some(Icon::PageDown), - "pageup" => Some(Icon::PageUp), + "left" => Some(IconName::ArrowLeft), + "right" => Some(IconName::ArrowRight), + "up" => Some(IconName::ArrowUp), + "down" => Some(IconName::ArrowDown), + "backspace" => Some(IconName::Backspace), + "delete" => Some(IconName::Delete), + "return" => Some(IconName::Return), + "enter" => Some(IconName::Return), + "tab" => Some(IconName::Tab), + "space" => Some(IconName::Space), + "escape" => Some(IconName::Escape), + "pagedown" => Some(IconName::PageDown), + "pageup" => Some(IconName::PageUp), _ => None, } } @@ -120,13 +120,13 @@ impl Key { #[derive(IntoElement)] pub struct KeyIcon { - icon: Icon, + icon: IconName, } impl RenderOnce for KeyIcon { fn render(self, _cx: &mut WindowContext) -> impl IntoElement { div().w(rems(14. / 16.)).child( - IconElement::new(self.icon) + Icon::new(self.icon) .size(IconSize::Small) .color(Color::Muted), ) @@ -134,7 +134,7 @@ impl RenderOnce for KeyIcon { } impl KeyIcon { - pub fn new(icon: Icon) -> Self { + pub fn new(icon: IconName) -> Self { Self { icon } } } diff --git a/crates/ui/src/components/label/label.rs b/crates/ui/src/components/label/label.rs index 61f463a531b85dc9a2780a334c8a771ab5c9f40b..0ba67286a22ccaeec1505612ec34cbc3b1b5883e 100644 --- a/crates/ui/src/components/label/label.rs +++ b/crates/ui/src/components/label/label.rs @@ -2,6 +2,34 @@ use gpui::WindowContext; use crate::{prelude::*, LabelCommon, LabelLike, LabelSize, LineHeightStyle}; +/// A struct representing a label element in the UI. +/// +/// The `Label` struct stores the label text and common properties for a label element. +/// It provides methods for modifying these properties. +/// +/// # Examples +/// +/// ``` +/// use ui::prelude::*; +/// +/// Label::new("Hello, World!"); +/// ``` +/// +/// **A colored label**, for example labeling a dangerous action: +/// +/// ``` +/// use ui::prelude::*; +/// +/// let my_label = Label::new("Delete").color(Color::Error); +/// ``` +/// +/// **A label with a strikethrough**, for example labeling something that has been deleted: +/// +/// ``` +/// use ui::prelude::*; +/// +/// let my_label = Label::new("Deleted").strikethrough(true); +/// ``` #[derive(IntoElement)] pub struct Label { base: LabelLike, @@ -9,6 +37,15 @@ pub struct Label { } impl Label { + /// Create a new [`Label`] with the given text. + /// + /// # Examples + /// + /// ``` + /// use ui::prelude::*; + /// + /// let my_label = Label::new("Hello, World!"); + /// ``` pub fn new(label: impl Into) -> Self { Self { base: LabelLike::new(), @@ -18,21 +55,57 @@ impl Label { } impl LabelCommon for Label { + /// Sets the size of the label using a [`LabelSize`]. + /// + /// # Examples + /// + /// ``` + /// use ui::prelude::*; + /// + /// let my_label = Label::new("Hello, World!").size(LabelSize::Small); + /// ``` fn size(mut self, size: LabelSize) -> Self { self.base = self.base.size(size); self } + /// Sets the line height style of the label using a [`LineHeightStyle`]. + /// + /// # Examples + /// + /// ``` + /// use ui::prelude::*; + /// + /// let my_label = Label::new("Hello, World!").line_height_style(LineHeightStyle::UiLabel); + /// ``` fn line_height_style(mut self, line_height_style: LineHeightStyle) -> Self { self.base = self.base.line_height_style(line_height_style); self } + /// Sets the color of the label using a [`Color`]. + /// + /// # Examples + /// + /// ``` + /// use ui::prelude::*; + /// + /// let my_label = Label::new("Hello, World!").color(Color::Accent); + /// ``` fn color(mut self, color: Color) -> Self { self.base = self.base.color(color); self } + /// Sets the strikethrough property of the label. + /// + /// # Examples + /// + /// ``` + /// use ui::prelude::*; + /// + /// let my_label = Label::new("Hello, World!").strikethrough(true); + /// ``` fn strikethrough(mut self, strikethrough: bool) -> Self { self.base = self.base.strikethrough(strikethrough); self diff --git a/crates/ui/src/components/label/label_like.rs b/crates/ui/src/components/label/label_like.rs index 436461fddeaffa9b6d87af0de11b11f48cafec68..6da07d81a39c8a51355b6d922e19c302c1c683ad 100644 --- a/crates/ui/src/components/label/label_like.rs +++ b/crates/ui/src/components/label/label_like.rs @@ -19,10 +19,18 @@ pub enum LineHeightStyle { UiLabel, } +/// A common set of traits all labels must implement. pub trait LabelCommon { + /// Sets the size of the label using a [`LabelSize`]. fn size(self, size: LabelSize) -> Self; + + /// Sets the line height style of the label using a [`LineHeightStyle`]. fn line_height_style(self, line_height_style: LineHeightStyle) -> Self; + + /// Sets the color of the label using a [`Color`]. fn color(self, color: Color) -> Self; + + /// Sets the strikethrough property of the label. fn strikethrough(self, strikethrough: bool) -> Self; } diff --git a/crates/ui/src/components/list/list_sub_header.rs b/crates/ui/src/components/list/list_sub_header.rs index 2e976b35178a2132cf1f0a6f2c154f03de249389..fc9f35e175c0d42a1517fc1227a4befc7dfdb2da 100644 --- a/crates/ui/src/components/list/list_sub_header.rs +++ b/crates/ui/src/components/list/list_sub_header.rs @@ -1,10 +1,10 @@ use crate::prelude::*; -use crate::{h_stack, Icon, IconElement, IconSize, Label}; +use crate::{h_stack, Icon, IconName, IconSize, Label}; #[derive(IntoElement)] pub struct ListSubHeader { label: SharedString, - start_slot: Option, + start_slot: Option, inset: bool, } @@ -17,7 +17,7 @@ impl ListSubHeader { } } - pub fn left_icon(mut self, left_icon: Option) -> Self { + pub fn left_icon(mut self, left_icon: Option) -> Self { self.start_slot = left_icon; self } @@ -40,11 +40,10 @@ impl RenderOnce for ListSubHeader { .flex() .gap_1() .items_center() - .children(self.start_slot.map(|i| { - IconElement::new(i) - .color(Color::Muted) - .size(IconSize::Small) - })) + .children( + self.start_slot + .map(|i| Icon::new(i).color(Color::Muted).size(IconSize::Small)), + ) .child(Label::new(self.label.clone()).color(Color::Muted)), ), ) diff --git a/crates/ui/src/components/popover_menu.rs b/crates/ui/src/components/popover_menu.rs index fb823b05dba3d18cea7992948abb50f827167e1c..52c907fab51f5cf07e92e42aaedfe055de995247 100644 --- a/crates/ui/src/components/popover_menu.rs +++ b/crates/ui/src/components/popover_menu.rs @@ -108,6 +108,7 @@ impl PopoverMenu { } } +/// Creates a [`PopoverMenu`] pub fn popover_menu(id: impl Into) -> PopoverMenu { PopoverMenu { id: id.into(), diff --git a/crates/ui/src/components/right_click_menu.rs b/crates/ui/src/components/right_click_menu.rs index 8bf40f61a8264ec3b00eec3ec844efa247876da7..cbc924ff59936d4904695cda12d253a28610af36 100644 --- a/crates/ui/src/components/right_click_menu.rs +++ b/crates/ui/src/components/right_click_menu.rs @@ -39,6 +39,7 @@ impl RightClickMenu { } } +/// Creates a [`RightClickMenu`] pub fn right_click_menu(id: impl Into) -> RightClickMenu { RightClickMenu { id: id.into(), diff --git a/crates/ui/src/components/stack.rs b/crates/ui/src/components/stack.rs index a6321b93d7c9e15afa1d3053fa61fb467acdbeb0..76f08de911e8e1e60a67c1d9311355f62d55914b 100644 --- a/crates/ui/src/components/stack.rs +++ b/crates/ui/src/components/stack.rs @@ -2,17 +2,13 @@ use gpui::{div, Div}; use crate::StyledExt; -/// Horizontally stacks elements. -/// -/// Sets `flex()`, `flex_row()`, `items_center()` +/// Horizontally stacks elements. Sets `flex()`, `flex_row()`, `items_center()` #[track_caller] pub fn h_stack() -> Div { div().h_flex() } -/// Vertically stacks elements. -/// -/// Sets `flex()`, `flex_col()` +/// Vertically stacks elements. Sets `flex()`, `flex_col()` #[track_caller] pub fn v_stack() -> Div { div().v_flex() diff --git a/crates/ui/src/components/stories/button.rs b/crates/ui/src/components/stories/button.rs index 7240812fa5a3a0d964a811795f925439def97670..c3fcdc5ae913974dcd86519715ae18ba016084fa 100644 --- a/crates/ui/src/components/stories/button.rs +++ b/crates/ui/src/components/stories/button.rs @@ -1,7 +1,7 @@ use gpui::Render; use story::Story; -use crate::{prelude::*, Icon}; +use crate::{prelude::*, IconName}; use crate::{Button, ButtonStyle}; pub struct ButtonStory; @@ -23,12 +23,12 @@ impl Render for ButtonStory { .child(Story::label("With `label_color`")) .child(Button::new("filled_with_label_color", "Click me").color(Color::Created)) .child(Story::label("With `icon`")) - .child(Button::new("filled_with_icon", "Click me").icon(Icon::FileGit)) + .child(Button::new("filled_with_icon", "Click me").icon(IconName::FileGit)) .child(Story::label("Selected with `icon`")) .child( Button::new("filled_and_selected_with_icon", "Click me") .selected(true) - .icon(Icon::FileGit), + .icon(IconName::FileGit), ) .child(Story::label("Default (Subtle)")) .child(Button::new("default_subtle", "Click me").style(ButtonStyle::Subtle)) diff --git a/crates/ui/src/components/stories/icon.rs b/crates/ui/src/components/stories/icon.rs index 83fc5980dd731bfbaa61770d58c446c47624f888..f6e750de2add47059ecc630bbc85780cfa273872 100644 --- a/crates/ui/src/components/stories/icon.rs +++ b/crates/ui/src/components/stories/icon.rs @@ -3,17 +3,17 @@ use story::Story; use strum::IntoEnumIterator; use crate::prelude::*; -use crate::{Icon, IconElement}; +use crate::{Icon, IconName}; pub struct IconStory; impl Render for IconStory { fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { - let icons = Icon::iter(); + let icons = IconName::iter(); Story::container() - .child(Story::title_for::()) + .child(Story::title_for::()) .child(Story::label("All Icons")) - .child(div().flex().gap_3().children(icons.map(IconElement::new))) + .child(div().flex().gap_3().children(icons.map(Icon::new))) } } diff --git a/crates/ui/src/components/stories/icon_button.rs b/crates/ui/src/components/stories/icon_button.rs index 66fc4affb3b0b63daa8dabe4461b9ec806c9416c..6a67183e97c73b3795fc14a71c11a16b6012f549 100644 --- a/crates/ui/src/components/stories/icon_button.rs +++ b/crates/ui/src/components/stories/icon_button.rs @@ -2,7 +2,7 @@ use gpui::Render; use story::{StoryContainer, StoryItem, StorySection}; use crate::{prelude::*, Tooltip}; -use crate::{Icon, IconButton}; +use crate::{IconButton, IconName}; pub struct IconButtonStory; @@ -10,7 +10,7 @@ impl Render for IconButtonStory { fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { let default_button = StoryItem::new( "Default", - IconButton::new("default_icon_button", Icon::Hash), + IconButton::new("default_icon_button", IconName::Hash), ) .description("Displays an icon button.") .usage( @@ -21,7 +21,7 @@ impl Render for IconButtonStory { let selected_button = StoryItem::new( "Selected", - IconButton::new("selected_icon_button", Icon::Hash).selected(true), + IconButton::new("selected_icon_button", IconName::Hash).selected(true), ) .description("Displays an icon button that is selected.") .usage( @@ -32,9 +32,9 @@ impl Render for IconButtonStory { let selected_with_selected_icon = StoryItem::new( "Selected with `selected_icon`", - IconButton::new("selected_with_selected_icon_button", Icon::AudioOn) + IconButton::new("selected_with_selected_icon_button", IconName::AudioOn) .selected(true) - .selected_icon(Icon::AudioOff), + .selected_icon(IconName::AudioOff), ) .description( "Displays an icon button that is selected and shows a different icon when selected.", @@ -49,7 +49,7 @@ impl Render for IconButtonStory { let disabled_button = StoryItem::new( "Disabled", - IconButton::new("disabled_icon_button", Icon::Hash).disabled(true), + IconButton::new("disabled_icon_button", IconName::Hash).disabled(true), ) .description("Displays an icon button that is disabled.") .usage( @@ -60,7 +60,7 @@ impl Render for IconButtonStory { let with_on_click_button = StoryItem::new( "With `on_click`", - IconButton::new("with_on_click_button", Icon::Ai).on_click(|_event, _cx| { + IconButton::new("with_on_click_button", IconName::Ai).on_click(|_event, _cx| { println!("Clicked!"); }), ) @@ -75,7 +75,7 @@ impl Render for IconButtonStory { let with_tooltip_button = StoryItem::new( "With `tooltip`", - IconButton::new("with_tooltip_button", Icon::MessageBubbles) + IconButton::new("with_tooltip_button", IconName::MessageBubbles) .tooltip(|cx| Tooltip::text("Open messages", cx)), ) .description("Displays an icon button that has a tooltip when hovered.") @@ -88,7 +88,7 @@ impl Render for IconButtonStory { let selected_with_tooltip_button = StoryItem::new( "Selected with `tooltip`", - IconButton::new("selected_with_tooltip_button", Icon::InlayHint) + IconButton::new("selected_with_tooltip_button", IconName::InlayHint) .selected(true) .tooltip(|cx| Tooltip::text("Toggle inlay hints", cx)), ) diff --git a/crates/ui/src/components/stories/list_header.rs b/crates/ui/src/components/stories/list_header.rs index ffbf7157f5b17271633ad72b4ee54023272a831a..358dc26a875c6735373bd22c50edc904ea635597 100644 --- a/crates/ui/src/components/stories/list_header.rs +++ b/crates/ui/src/components/stories/list_header.rs @@ -2,7 +2,7 @@ use gpui::Render; use story::Story; use crate::{prelude::*, IconButton}; -use crate::{Icon, ListHeader}; +use crate::{IconName, ListHeader}; pub struct ListHeaderStory; @@ -13,19 +13,19 @@ impl Render for ListHeaderStory { .child(Story::label("Default")) .child(ListHeader::new("Section 1")) .child(Story::label("With left icon")) - .child(ListHeader::new("Section 2").start_slot(IconElement::new(Icon::Bell))) + .child(ListHeader::new("Section 2").start_slot(Icon::new(IconName::Bell))) .child(Story::label("With left icon and meta")) .child( ListHeader::new("Section 3") - .start_slot(IconElement::new(Icon::BellOff)) - .end_slot(IconButton::new("action_1", Icon::Bolt)), + .start_slot(Icon::new(IconName::BellOff)) + .end_slot(IconButton::new("action_1", IconName::Bolt)), ) .child(Story::label("With multiple meta")) .child( ListHeader::new("Section 4") - .end_slot(IconButton::new("action_1", Icon::Bolt)) - .end_slot(IconButton::new("action_2", Icon::ExclamationTriangle)) - .end_slot(IconButton::new("action_3", Icon::Plus)), + .end_slot(IconButton::new("action_1", IconName::Bolt)) + .end_slot(IconButton::new("action_2", IconName::ExclamationTriangle)) + .end_slot(IconButton::new("action_3", IconName::Plus)), ) } } diff --git a/crates/ui/src/components/stories/list_item.rs b/crates/ui/src/components/stories/list_item.rs index b3ff096d9dfe9d435e68dc7e818b661603267ca2..a25b07df849aeb67a185e3984ab11ec341b07045 100644 --- a/crates/ui/src/components/stories/list_item.rs +++ b/crates/ui/src/components/stories/list_item.rs @@ -1,8 +1,8 @@ -use gpui::Render; +use gpui::{Render, SharedUrl}; use story::Story; use crate::{prelude::*, Avatar}; -use crate::{Icon, ListItem}; +use crate::{IconName, ListItem}; pub struct ListItemStory; @@ -18,13 +18,13 @@ impl Render for ListItemStory { ListItem::new("inset_list_item") .inset(true) .start_slot( - IconElement::new(Icon::Bell) + Icon::new(IconName::Bell) .size(IconSize::Small) .color(Color::Muted), ) .child("Hello, world!") .end_slot( - IconElement::new(Icon::Bell) + Icon::new(IconName::Bell) .size(IconSize::Small) .color(Color::Muted), ), @@ -34,7 +34,7 @@ impl Render for ListItemStory { ListItem::new("with start slot_icon") .child("Hello, world!") .start_slot( - IconElement::new(Icon::Bell) + Icon::new(IconName::Bell) .size(IconSize::Small) .color(Color::Muted), ), @@ -43,7 +43,7 @@ impl Render for ListItemStory { .child( ListItem::new("with_start slot avatar") .child("Hello, world!") - .start_slot(Avatar::new(SharedString::from( + .start_slot(Avatar::new(SharedUrl::from( "https://avatars.githubusercontent.com/u/1714999?v=4", ))), ) @@ -51,7 +51,7 @@ impl Render for ListItemStory { .child( ListItem::new("with_left_avatar") .child("Hello, world!") - .end_slot(Avatar::new(SharedString::from( + .end_slot(Avatar::new(SharedUrl::from( "https://avatars.githubusercontent.com/u/1714999?v=4", ))), ) @@ -62,23 +62,23 @@ impl Render for ListItemStory { .end_slot( h_stack() .gap_2() - .child(Avatar::new(SharedString::from( + .child(Avatar::new(SharedUrl::from( "https://avatars.githubusercontent.com/u/1789?v=4", ))) - .child(Avatar::new(SharedString::from( + .child(Avatar::new(SharedUrl::from( "https://avatars.githubusercontent.com/u/1789?v=4", ))) - .child(Avatar::new(SharedString::from( + .child(Avatar::new(SharedUrl::from( "https://avatars.githubusercontent.com/u/1789?v=4", ))) - .child(Avatar::new(SharedString::from( + .child(Avatar::new(SharedUrl::from( "https://avatars.githubusercontent.com/u/1789?v=4", ))) - .child(Avatar::new(SharedString::from( + .child(Avatar::new(SharedUrl::from( "https://avatars.githubusercontent.com/u/1789?v=4", ))), ) - .end_hover_slot(Avatar::new(SharedString::from( + .end_hover_slot(Avatar::new(SharedUrl::from( "https://avatars.githubusercontent.com/u/1714999?v=4", ))), ) diff --git a/crates/ui/src/components/stories/tab.rs b/crates/ui/src/components/stories/tab.rs index 4c63e593aaa9eab582351b62b618c7e6cae3978c..bd7b602620938775b32be3d46a41b519f2639f63 100644 --- a/crates/ui/src/components/stories/tab.rs +++ b/crates/ui/src/components/stories/tab.rs @@ -27,7 +27,7 @@ impl Render for TabStory { h_stack().child( Tab::new("tab_1") .end_slot( - IconButton::new("close_button", Icon::Close) + IconButton::new("close_button", IconName::Close) .icon_color(Color::Muted) .size(ButtonSize::None) .icon_size(IconSize::XSmall), diff --git a/crates/ui/src/components/stories/tab_bar.rs b/crates/ui/src/components/stories/tab_bar.rs index 805725315c672a34feac758f52991d0cb3d03f01..289ceff9a6f576739daacd02b57e260b295a7ae8 100644 --- a/crates/ui/src/components/stories/tab_bar.rs +++ b/crates/ui/src/components/stories/tab_bar.rs @@ -38,16 +38,19 @@ impl Render for TabBarStory { h_stack().child( TabBar::new("tab_bar_1") .start_child( - IconButton::new("navigate_backward", Icon::ArrowLeft) + IconButton::new("navigate_backward", IconName::ArrowLeft) .icon_size(IconSize::Small), ) .start_child( - IconButton::new("navigate_forward", Icon::ArrowRight) + IconButton::new("navigate_forward", IconName::ArrowRight) .icon_size(IconSize::Small), ) - .end_child(IconButton::new("new", Icon::Plus).icon_size(IconSize::Small)) .end_child( - IconButton::new("split_pane", Icon::Split).icon_size(IconSize::Small), + IconButton::new("new", IconName::Plus).icon_size(IconSize::Small), + ) + .end_child( + IconButton::new("split_pane", IconName::Split) + .icon_size(IconSize::Small), ) .children(tabs), ), diff --git a/crates/ui/src/disableable.rs b/crates/ui/src/disableable.rs index f9b4e5ba910be1e65b6f2b106e9a45b707f20744..9f08ed8d12dd1e0d468e40de85420f13dbbd1698 100644 --- a/crates/ui/src/disableable.rs +++ b/crates/ui/src/disableable.rs @@ -1,4 +1,4 @@ -/// A trait for elements that can be disabled. +/// A trait for elements that can be disabled. Generally used to implement disabling an element's interactivity and changing its appearance to reflect that it is disabled. pub trait Disableable { /// Sets whether the element is disabled. fn disabled(self, disabled: bool) -> Self; diff --git a/crates/ui/src/fixed.rs b/crates/ui/src/fixed.rs index a2c3ed3edc819ed9a4bc4d4a33ff3b9494c11075..9ba64da0905310d2e3409e0c13d110cb81cc5e41 100644 --- a/crates/ui/src/fixed.rs +++ b/crates/ui/src/fixed.rs @@ -1,6 +1,6 @@ use gpui::DefiniteLength; -/// A trait for elements that have a fixed with. +/// A trait for elements that can have a fixed with. Enables the use of the `width` and `full_width` methods. pub trait FixedWidth { /// Sets the width of the element. fn width(self, width: DefiniteLength) -> Self; diff --git a/crates/ui/src/prelude.rs b/crates/ui/src/prelude.rs index 63d6c4b46a498bbce2573814cd645ec1d1b7bccd..69d1d0583d70e8176f577ce7d7fa651845df82f6 100644 --- a/crates/ui/src/prelude.rs +++ b/crates/ui/src/prelude.rs @@ -1,3 +1,5 @@ +//! The prelude of this crate. When building UI in Zed you almost always want to import this. + pub use gpui::prelude::*; pub use gpui::{ div, px, relative, rems, AbsoluteLength, DefiniteLength, Div, Element, ElementId, @@ -14,6 +16,7 @@ pub use crate::visible_on_hover::*; pub use crate::{h_stack, v_stack}; pub use crate::{Button, ButtonSize, ButtonStyle, IconButton, SelectableButton}; pub use crate::{ButtonCommon, Color, StyledExt}; -pub use crate::{Icon, IconElement, IconPosition, IconSize}; +pub use crate::{Headline, HeadlineSize}; +pub use crate::{Icon, IconName, IconPosition, IconSize}; pub use crate::{Label, LabelCommon, LabelSize, LineHeightStyle}; pub use theme::ActiveTheme; diff --git a/crates/ui/src/selectable.rs b/crates/ui/src/selectable.rs index 34c66ab1fa4a4a961ee972588b9e3c7cf211308b..54da86d09492f03a106cb1be265c0a46851000f6 100644 --- a/crates/ui/src/selectable.rs +++ b/crates/ui/src/selectable.rs @@ -1,18 +1,27 @@ /// A trait for elements that can be selected. +/// +/// Generally used to enable "toggle" or "active" behavior and styles on an element through the [`Selection`] status. pub trait Selectable { /// Sets whether the element is selected. fn selected(self, selected: bool) -> Self; } +/// Represents the selection status of an element. #[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Copy)] pub enum Selection { + /// The element is not selected. #[default] Unselected, + /// The selection state of the element is indeterminate. Indeterminate, + /// The element is selected. Selected, } impl Selection { + /// Returns the inverse of the current selection status. + /// + /// Indeterminate states become selected if inverted. pub fn inverse(&self) -> Self { match self { Self::Unselected | Self::Indeterminate => Self::Selected, diff --git a/crates/ui/src/styled_ext.rs b/crates/ui/src/styled_ext.rs index a6381e604f5f672553a2b502a6e4814eee5f384d..b2eaf75ed913b753574be6f449133eb536bd0a72 100644 --- a/crates/ui/src/styled_ext.rs +++ b/crates/ui/src/styled_ext.rs @@ -14,7 +14,7 @@ fn elevated(this: E, cx: &mut WindowContext, index: ElevationIndex) - .shadow(index.shadow()) } -/// Extends [`Styled`](gpui::Styled) with Zed specific styling methods. +/// Extends [`gpui::Styled`] with Zed-specific styling methods. pub trait StyledExt: Styled + Sized { /// Horizontally stacks elements. /// @@ -30,6 +30,7 @@ pub trait StyledExt: Styled + Sized { self.flex().flex_col() } + /// Sets the text size using a [`UiTextSize`]. fn text_ui_size(self, size: UiTextSize) -> Self { self.text_size(size.rems()) } @@ -40,7 +41,7 @@ pub trait StyledExt: Styled + Sized { /// /// Note: The absolute size of this text will change based on a user's `ui_scale` setting. /// - /// Use [`text_ui_sm`] for regular-sized text. + /// Use `text_ui_sm` for smaller text. fn text_ui(self) -> Self { self.text_size(UiTextSize::default().rems()) } @@ -51,7 +52,7 @@ pub trait StyledExt: Styled + Sized { /// /// Note: The absolute size of this text will change based on a user's `ui_scale` setting. /// - /// Use [`text_ui`] for regular-sized text. + /// Use `text_ui` for regular-sized text. fn text_ui_sm(self) -> Self { self.text_size(UiTextSize::Small.rems()) } @@ -62,7 +63,7 @@ pub trait StyledExt: Styled + Sized { /// /// Note: The absolute size of this text will change based on a user's `ui_scale` setting. /// - /// Use [`text_ui`] for regular-sized text. + /// Use `text_ui` for regular-sized text. fn text_ui_xs(self) -> Self { self.text_size(UiTextSize::XSmall.rems()) } @@ -78,7 +79,7 @@ pub trait StyledExt: Styled + Sized { self.text_size(settings.buffer_font_size(cx)) } - /// The [`Surface`](ui::ElevationIndex::Surface) elevation level, located above the app background, is the standard level for all elements + /// The [`Surface`](ElevationIndex::Surface) elevation level, located above the app background, is the standard level for all elements /// /// Sets `bg()`, `rounded_lg()`, `border()`, `border_color()`, `shadow()` /// @@ -87,7 +88,7 @@ pub trait StyledExt: Styled + Sized { elevated(self, cx, ElevationIndex::Surface) } - /// Non-Modal Elevated Surfaces appear above the [`Surface`](ui::ElevationIndex::Surface) layer and is used for things that should appear above most UI elements like an editor or panel, but not elements like popovers, context menus, modals, etc. + /// Non-Modal Elevated Surfaces appear above the [`Surface`](ElevationIndex::Surface) layer and is used for things that should appear above most UI elements like an editor or panel, but not elements like popovers, context menus, modals, etc. /// /// Sets `bg()`, `rounded_lg()`, `border()`, `border_color()`, `shadow()` /// @@ -100,7 +101,7 @@ pub trait StyledExt: Styled + Sized { /// /// 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. /// - /// If the element does not have this behavior, it should be rendered at the [`Elevated Surface`](ui::ElevationIndex::ElevatedSurface) layer. + /// If the element does not have this behavior, it should be rendered at the [`Elevated Surface`](ElevationIndex::ElevatedSurface) layer. /// /// Sets `bg()`, `rounded_lg()`, `border()`, `border_color()`, `shadow()` /// @@ -119,26 +120,32 @@ pub trait StyledExt: Styled + Sized { self.border_color(cx.theme().colors().border_variant) } + /// Sets the background color to red for debugging when building UI. fn debug_bg_red(self) -> Self { self.bg(hsla(0. / 360., 1., 0.5, 1.)) } + /// Sets the background color to green for debugging when building UI. fn debug_bg_green(self) -> Self { self.bg(hsla(120. / 360., 1., 0.5, 1.)) } + /// Sets the background color to blue for debugging when building UI. fn debug_bg_blue(self) -> Self { self.bg(hsla(240. / 360., 1., 0.5, 1.)) } + /// Sets the background color to yellow for debugging when building UI. fn debug_bg_yellow(self) -> Self { self.bg(hsla(60. / 360., 1., 0.5, 1.)) } + /// Sets the background color to cyan for debugging when building UI. fn debug_bg_cyan(self) -> Self { self.bg(hsla(160. / 360., 1., 0.5, 1.)) } + /// Sets the background color to magenta for debugging when building UI. fn debug_bg_magenta(self) -> Self { self.bg(hsla(300. / 360., 1., 0.5, 1.)) } diff --git a/crates/ui/src/styles/color.rs b/crates/ui/src/styles/color.rs index 977a26dedce61d20bb27ee6b65bcc9f3f8738096..434183e5606135cdcb7e420c023c1108d0aa0a42 100644 --- a/crates/ui/src/styles/color.rs +++ b/crates/ui/src/styles/color.rs @@ -1,6 +1,7 @@ use gpui::{Hsla, WindowContext}; use theme::ActiveTheme; +/// Sets a color that has a consistent meaning across all themes. #[derive(Debug, Default, PartialEq, Copy, Clone)] pub enum Color { #[default] diff --git a/crates/ui/src/styles/elevation.rs b/crates/ui/src/styles/elevation.rs index 7b3835c2e54b3e60528ab212311d245dfd7223d3..ec1848ca6093606aea9abcc926f242b20e0e727c 100644 --- a/crates/ui/src/styles/elevation.rs +++ b/crates/ui/src/styles/elevation.rs @@ -85,6 +85,7 @@ impl LayerIndex { } } +/// An appropriate z-index for the given layer based on its intended useage. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ElementIndex { Effect, diff --git a/crates/ui/src/styles/typography.rs b/crates/ui/src/styles/typography.rs index 4819791b02c988bfccc1c59484b373e5d8249bfe..70cd797d5162b534a9ae42a0124f81c34342716e 100644 --- a/crates/ui/src/styles/typography.rs +++ b/crates/ui/src/styles/typography.rs @@ -1,4 +1,8 @@ -use gpui::{rems, Rems}; +use gpui::{ + div, rems, IntoElement, ParentElement, Rems, RenderOnce, SharedString, Styled, WindowContext, +}; +use settings::Settings; +use theme::{ActiveTheme, ThemeSettings}; #[derive(Debug, Default, Clone)] pub enum UiTextSize { @@ -33,3 +37,70 @@ impl UiTextSize { } } } + +/// The size of a [`Headline`] element +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)] +pub enum HeadlineSize { + XSmall, + Small, + #[default] + Medium, + Large, + XLarge, +} + +impl HeadlineSize { + pub fn size(self) -> Rems { + match self { + // Based on the Major Second scale + Self::XSmall => rems(0.88), + Self::Small => rems(1.0), + Self::Medium => rems(1.125), + Self::Large => rems(1.27), + Self::XLarge => rems(1.43), + } + } + + pub fn line_height(self) -> Rems { + match self { + Self::XSmall => rems(1.6), + Self::Small => rems(1.6), + Self::Medium => rems(1.6), + Self::Large => rems(1.6), + Self::XLarge => rems(1.6), + } + } +} + +#[derive(IntoElement)] +pub struct Headline { + size: HeadlineSize, + text: SharedString, +} + +impl RenderOnce for Headline { + fn render(self, cx: &mut WindowContext) -> impl IntoElement { + let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone(); + + div() + .font(ui_font) + .line_height(self.size.line_height()) + .text_size(self.size.size()) + .text_color(cx.theme().colors().text) + .child(self.text) + } +} + +impl Headline { + pub fn new(text: impl Into) -> Self { + Self { + size: HeadlineSize::default(), + text: text.into(), + } + } + + pub fn size(mut self, size: HeadlineSize) -> Self { + self.size = size; + self + } +} diff --git a/crates/ui/src/ui.rs b/crates/ui/src/ui.rs index b074f10dad50b9e61d0ed69ca29f39687e54973e..e8ee51818ea963657aafc2cf1dfdd5f8dbd4668f 100644 --- a/crates/ui/src/ui.rs +++ b/crates/ui/src/ui.rs @@ -3,8 +3,6 @@ //! This crate provides a set of UI primitives and components that are used to build all of the elements in Zed's UI. //! -#![doc = include_str!("../docs/building-ui.md")] - mod clickable; mod components; mod disableable; diff --git a/crates/ui/src/utils.rs b/crates/ui/src/utils.rs index 573a1333efcfa0169a84f52ce27e72838492c696..ed1fec690fc738be2bee5d621c7fc53f0c0f6b9e 100644 --- a/crates/ui/src/utils.rs +++ b/crates/ui/src/utils.rs @@ -1,3 +1,5 @@ +//! UI-related utilities (e.g. converting dates to a human-readable form). + mod format_distance; pub use format_distance::*; diff --git a/crates/ui/src/utils/format_distance.rs b/crates/ui/src/utils/format_distance.rs index 17f6b7a6549b793b6fe58c7a00c083d2dbe53c42..03a0de3adb75c656272397a1f851b8c12ed9eee6 100644 --- a/crates/ui/src/utils/format_distance.rs +++ b/crates/ui/src/utils/format_distance.rs @@ -7,10 +7,10 @@ pub enum DateTimeType { } impl DateTimeType { - /// Converts the DateTimeType to a NaiveDateTime. + /// Converts the [`DateTimeType`] to a [`NaiveDateTime`]. /// - /// If the DateTimeType is already a NaiveDateTime, it will be returned as is. - /// If the DateTimeType is a DateTime, it will be converted to a NaiveDateTime. + /// If the [`DateTimeType`] is already a [`NaiveDateTime`], it will be returned as is. + /// If the [`DateTimeType`] is a [`DateTime`], it will be converted to a [`NaiveDateTime`]. pub fn to_naive(&self) -> NaiveDateTime { match self { DateTimeType::Naive(naive) => *naive, @@ -68,13 +68,13 @@ impl FormatDistance { } } -/// Calculates the distance in seconds between two NaiveDateTime objects. +/// Calculates the distance in seconds between two [`NaiveDateTime`] objects. /// It returns a signed integer denoting the difference. If `date` is earlier than `base_date`, the returned value will be negative. /// /// ## Arguments /// -/// * `date` - A NaiveDateTime object representing the date of interest -/// * `base_date` - A NaiveDateTime object representing the base date against which the comparison is made +/// * `date` - A [NaiveDateTime`] object representing the date of interest +/// * `base_date` - A [NaiveDateTime`] object representing the base date against which the comparison is made fn distance_in_seconds(date: NaiveDateTime, base_date: NaiveDateTime) -> i64 { let duration = date.signed_duration_since(base_date); -duration.num_seconds() @@ -233,29 +233,7 @@ fn distance_string( /// /// For example, "less than a minute ago", "about 2 hours ago", "3 months from now", etc. /// -/// Use [naive_format_distance_from_now] to compare a NaiveDateTime against now. -/// -/// # Arguments -/// -/// * `date` - The NaiveDateTime to compare. -/// * `base_date` - The NaiveDateTime to compare against. -/// * `include_seconds` - A boolean. If true, distances less than a minute are more detailed -/// * `add_suffix` - A boolean. If true, result indicates if the time is in the past or future -/// -/// # Example -/// -/// ```rust -/// use chrono::DateTime; -/// use ui::utils::format_distance; -/// -/// fn time_between_moon_landings() -> String { -/// let date = DateTime::parse_from_rfc3339("1969-07-20T00:00:00Z").unwrap().naive_local(); -/// let base_date = DateTime::parse_from_rfc3339("1972-12-14T00:00:00Z").unwrap().naive_local(); -/// format!("There was {} between the first and last crewed moon landings.", naive_format_distance(date, base_date, false, false)) -/// } -/// ``` -/// -/// Output: `"There was about 3 years between the first and last crewed moon landings."` +/// Use [`format_distance_from_now`] to compare a NaiveDateTime against now. pub fn format_distance( date: DateTimeType, base_date: NaiveDateTime, @@ -271,26 +249,6 @@ pub fn format_distance( /// Get the time difference between a date and now as relative human readable string. /// /// For example, "less than a minute ago", "about 2 hours ago", "3 months from now", etc. -/// -/// # Arguments -/// -/// * `datetime` - The NaiveDateTime to compare with the current time. -/// * `include_seconds` - A boolean. If true, distances less than a minute are more detailed -/// * `add_suffix` - A boolean. If true, result indicates if the time is in the past or future -/// -/// # Example -/// -/// ```rust -/// use chrono::DateTime; -/// use ui::utils::naive_format_distance_from_now; -/// -/// fn time_since_first_moon_landing() -> String { -/// let date = DateTime::parse_from_rfc3339("1969-07-20T00:00:00Z").unwrap().naive_local(); -/// format!("It's been {} since Apollo 11 first landed on the moon.", naive_format_distance_from_now(date, false, false)) -/// } -/// ``` -/// -/// Output: `It's been over 54 years since Apollo 11 first landed on the moon.` pub fn format_distance_from_now( datetime: DateTimeType, include_seconds: bool, diff --git a/crates/util/src/util.rs b/crates/util/src/util.rs index 613a79b19ed546283f3d54b45ea571becb3b9503..e32dd88b86fab6f0e8ad3fa9de1ce7ce6e8793a6 100644 --- a/crates/util/src/util.rs +++ b/crates/util/src/util.rs @@ -41,8 +41,8 @@ pub fn truncate(s: &str, max_chars: usize) -> &str { } } -/// Removes characters from the end of the string if it's length is greater than `max_chars` and -/// appends "..." to the string. Returns string unchanged if it's length is smaller than max_chars. +/// Removes characters from the end of the string if its length is greater than `max_chars` and +/// appends "..." to the string. Returns string unchanged if its length is smaller than max_chars. pub fn truncate_and_trailoff(s: &str, max_chars: usize) -> String { debug_assert!(max_chars >= 5); @@ -53,8 +53,8 @@ pub fn truncate_and_trailoff(s: &str, max_chars: usize) -> String { } } -/// Removes characters from the front of the string if it's length is greater than `max_chars` and -/// prepends the string with "...". Returns string unchanged if it's length is smaller than max_chars. +/// Removes characters from the front of the string if its length is greater than `max_chars` and +/// prepends the string with "...". Returns string unchanged if its length is smaller than max_chars. pub fn truncate_and_remove_front(s: &str, max_chars: usize) -> String { debug_assert!(max_chars >= 5); diff --git a/crates/vcs_menu/src/lib.rs b/crates/vcs_menu/src/lib.rs index 2735f81677c86331e554dd3f27b2aa2362587d5d..0774c6f5755324de77b16f691ee0dde11ed38f16 100644 --- a/crates/vcs_menu/src/lib.rs +++ b/crates/vcs_menu/src/lib.rs @@ -18,7 +18,6 @@ use workspace::{ModalView, Toast, Workspace}; actions!(branches, [OpenRecent]); pub fn init(cx: &mut AppContext) { - // todo!() po cx.observe_new_views(|workspace: &mut Workspace, _| { workspace.register_action(|workspace, action, cx| { BranchList::toggle_modal(workspace, action, cx).log_err(); diff --git a/crates/vim/src/editor_events.rs b/crates/vim/src/editor_events.rs index 3c2f373f9455209a36ef9dac88c3144f7f1a4bdd..e3ed076698d101a12f92c16048748532ea596e1c 100644 --- a/crates/vim/src/editor_events.rs +++ b/crates/vim/src/editor_events.rs @@ -69,7 +69,7 @@ fn released(entity_id: EntityId, cx: &mut AppContext) { mod test { use crate::{test::VimTestContext, Vim}; use editor::Editor; - use gpui::{Context, Entity}; + use gpui::{Context, Entity, VisualTestContext}; use language::Buffer; // regression test for blur called with a different active editor @@ -82,11 +82,13 @@ mod test { let editor2 = cx .update(|cx| { window2.update(cx, |_, cx| { + cx.activate_window(); cx.focus_self(); cx.view().clone() }) }) .unwrap(); + cx.run_until_parked(); cx.update(|cx| { let vim = Vim::read(cx); @@ -101,4 +103,42 @@ mod test { editor1.handle_blur(cx); }); } + + // regression test for focus_in/focus_out being called on window activation + #[gpui::test] + async fn test_focus_across_windows(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + + let mut cx1 = VisualTestContext::from_window(cx.window, &cx); + let editor1 = cx.editor.clone(); + dbg!(editor1.entity_id()); + + let buffer = cx.new_model(|_| Buffer::new(0, 0, "a = 1\nb = 2\n")); + let (editor2, cx2) = cx.add_window_view(|cx| Editor::for_buffer(buffer, None, cx)); + + editor2.update(cx2, |_, cx| { + cx.focus_self(); + cx.activate_window(); + }); + cx.run_until_parked(); + + cx1.update(|cx| { + assert_eq!( + Vim::read(cx).active_editor.as_ref().unwrap().entity_id(), + editor2.entity_id(), + ) + }); + + cx1.update(|cx| { + cx.activate_window(); + }); + cx.run_until_parked(); + + cx.update(|cx| { + assert_eq!( + Vim::read(cx).active_editor.as_ref().unwrap().entity_id(), + editor1.entity_id(), + ) + }); + } } diff --git a/crates/vim/src/test/vim_test_context.rs b/crates/vim/src/test/vim_test_context.rs index 5ed5296bff44d3e76c32f2a4b768afd760d1d121..0e41f5a03687f2c16cd79e21cb33c6abab6476b5 100644 --- a/crates/vim/src/test/vim_test_context.rs +++ b/crates/vim/src/test/vim_test_context.rs @@ -6,7 +6,7 @@ use editor::test::{ use futures::Future; use gpui::{Context, View, VisualContext}; use lsp::request; -use search::BufferSearchBar; +use search::{project_search::ProjectSearchBar, BufferSearchBar}; use crate::{state::Operator, *}; @@ -17,7 +17,6 @@ pub struct VimTestContext { impl VimTestContext { pub fn init(cx: &mut gpui::TestAppContext) { if cx.has_global::() { - dbg!("OOPS"); return; } cx.update(|cx| { @@ -59,9 +58,9 @@ impl VimTestContext { pane.toolbar().update(cx, |toolbar, cx| { let buffer_search_bar = cx.new_view(BufferSearchBar::new); toolbar.add_item(buffer_search_bar, cx); - // todo!(); - // let project_search_bar = cx.add_view(|_| ProjectSearchBar::new()); - // toolbar.add_item(project_search_bar, cx); + + let project_search_bar = cx.new_view(|_| ProjectSearchBar::new()); + toolbar.add_item(project_search_bar, cx); }) }); workspace.status_bar().update(cx, |status_bar, cx| { diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 62205630a153e886af9021f1bb45671dd496b7b7..3579bf36fe29a7276496a1bbefd95dabf6cc8bc2 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -33,6 +33,9 @@ use workspace::{self, Workspace}; use crate::state::ReplayableAction; +/// Whether or not to enable Vim mode (work in progress). +/// +/// Default: false pub struct VimModeSetting(pub bool); #[derive(Clone, Deserialize, PartialEq)] diff --git a/crates/welcome/src/base_keymap_setting.rs b/crates/welcome/src/base_keymap_setting.rs index 411caa820e34e2cc080c160c39f4053161901e92..e05a16c350d4de2a7f1c0ddf56b7b581ef7d6b17 100644 --- a/crates/welcome/src/base_keymap_setting.rs +++ b/crates/welcome/src/base_keymap_setting.rs @@ -4,6 +4,9 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::Settings; +/// Base key bindings scheme. Base keymaps can be overriden with user keymaps. +/// +/// Default: VSCode #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)] pub enum BaseKeymap { #[default] diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index c13a00b11c897b46ef5e2ac69ae10848c573ebf2..0c752597262ca1f64a996d09795db289941a30b2 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -28,7 +28,7 @@ pub trait Panel: FocusableView + EventEmitter { fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext); fn size(&self, cx: &WindowContext) -> Pixels; fn set_size(&mut self, size: Option, cx: &mut ViewContext); - fn icon(&self, cx: &WindowContext) -> Option; + fn icon(&self, cx: &WindowContext) -> Option; fn icon_tooltip(&self, cx: &WindowContext) -> Option<&'static str>; fn toggle_action(&self) -> Box; fn icon_label(&self, _: &WindowContext) -> Option { @@ -52,7 +52,7 @@ pub trait PanelHandle: Send + Sync { fn set_active(&self, active: bool, cx: &mut WindowContext); fn size(&self, cx: &WindowContext) -> Pixels; fn set_size(&self, size: Option, cx: &mut WindowContext); - fn icon(&self, cx: &WindowContext) -> Option; + fn icon(&self, cx: &WindowContext) -> Option; fn icon_tooltip(&self, cx: &WindowContext) -> Option<&'static str>; fn toggle_action(&self, cx: &WindowContext) -> Box; fn icon_label(&self, cx: &WindowContext) -> Option; @@ -104,7 +104,7 @@ where self.update(cx, |this, cx| this.set_size(size, cx)) } - fn icon(&self, cx: &WindowContext) -> Option { + fn icon(&self, cx: &WindowContext) -> Option { self.read(cx).icon(cx) } @@ -167,15 +167,6 @@ impl DockPosition { } } - // todo!() - // fn to_resize_handle_side(self) -> HandleSide { - // match self { - // Self::Left => HandleSide::Right, - // Self::Bottom => HandleSide::Top, - // Self::Right => HandleSide::Left, - // } - // } - pub fn axis(&self) -> Axis { match self { Self::Left | Self::Right => Axis::Horizontal, @@ -186,8 +177,6 @@ impl DockPosition { struct PanelEntry { panel: Arc, - // todo!() - // context_menu: View, _subscriptions: [Subscription; 2], } @@ -265,12 +254,6 @@ impl Dock { self.is_open } - // todo!() - // pub fn has_focus(&self, cx: &WindowContext) -> bool { - // self.visible_panel() - // .map_or(false, |panel| panel.has_focus(cx)) - // } - pub fn panel(&self) -> Option> { self.panel_entries .iter() @@ -395,7 +378,6 @@ impl Dock { }) .ok(); } - // todo!() we do not use this event in the production code (even in zed1), remove it PanelEvent::Activate => { if let Some(ix) = this .panel_entries @@ -418,16 +400,8 @@ impl Dock { }), ]; - // todo!() - // let dock_view_id = cx.view_id(); self.panel_entries.push(PanelEntry { panel: Arc::new(panel), - // todo!() - // context_menu: cx.add_view(|cx| { - // let mut menu = ContextMenu::new(dock_view_id, cx); - // menu.set_position_mode(OverlayPositionMode::Local); - // menu - // }), _subscriptions: subscriptions, }); cx.notify() @@ -619,7 +593,6 @@ impl PanelButtons { impl Render for PanelButtons { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - // todo!() let dock = self.dock.read(cx); let active_index = dock.active_panel_index; let is_open = dock.is_open; @@ -775,7 +748,7 @@ pub mod test { self.size = size.unwrap_or(px(300.)); } - fn icon(&self, _: &WindowContext) -> Option { + fn icon(&self, _: &WindowContext) -> Option { None } diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index 45f6141df2f172ccad176c48bc1f83eab6174c3e..c629edc696b87e0552ca05956e2a1c5cf3b5e6b0 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -60,7 +60,13 @@ impl ClosePosition { #[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] pub struct ItemSettingsContent { + /// Whether to show the Git file status on a tab item. + /// + /// Default: true git_status: Option, + /// Position of the close button in a tab. + /// + /// Default: right close_position: Option, } diff --git a/crates/workspace/src/modal_layer.rs b/crates/workspace/src/modal_layer.rs index ae105345cd360eb072e4a1064efbcca339bfb954..627581c4760c0209de3379d5a2cbf0ead7cbbc62 100644 --- a/crates/workspace/src/modal_layer.rs +++ b/crates/workspace/src/modal_layer.rs @@ -101,6 +101,10 @@ impl ModalLayer { let active_modal = self.active_modal.as_ref()?; active_modal.modal.view().downcast::().ok() } + + pub fn has_active_modal(&self) -> bool { + self.active_modal.is_some() + } } impl Render for ModalLayer { diff --git a/crates/workspace/src/notifications.rs b/crates/workspace/src/notifications.rs index 394772b9c45b0a31bf36260389ee2e2fcc7ec717..cc2450587d7304a8491aa59137cac9ae758da5ee 100644 --- a/crates/workspace/src/notifications.rs +++ b/crates/workspace/src/notifications.rs @@ -2,14 +2,12 @@ use crate::{Toast, Workspace}; use collections::HashMap; use gpui::{ AnyView, AppContext, AsyncWindowContext, DismissEvent, Entity, EntityId, EventEmitter, Render, - View, ViewContext, VisualContext, + Task, View, ViewContext, VisualContext, WindowContext, }; use std::{any::TypeId, ops::DerefMut}; pub fn init(cx: &mut AppContext) { cx.set_global(NotificationTracker::new()); - // todo!() - // simple_message_notification::init(cx); } pub trait Notification: EventEmitter + Render {} @@ -175,7 +173,7 @@ pub mod simple_message_notification { }; use std::sync::Arc; use ui::prelude::*; - use ui::{h_stack, v_stack, Button, Icon, IconElement, Label, StyledExt}; + use ui::{h_stack, v_stack, Button, Icon, IconName, Label, StyledExt}; pub struct MessageNotification { message: SharedString, @@ -230,7 +228,7 @@ pub mod simple_message_notification { .child( div() .id("cancel") - .child(IconElement::new(Icon::Close)) + .child(Icon::new(IconName::Close)) .cursor_pointer() .on_click(cx.listener(|this, _, cx| this.dismiss(cx))), ), @@ -247,105 +245,6 @@ pub mod simple_message_notification { })) } } - // todo!() - // impl View for MessageNotification { - // fn ui_name() -> &'static str { - // "MessageNotification" - // } - - // fn render(&mut self, cx: &mut gpui::ViewContext) -> gpui::AnyElement { - // let theme = theme::current(cx).clone(); - // let theme = &theme.simple_message_notification; - - // enum MessageNotificationTag {} - - // let click_message = self.click_message.clone(); - // let message = match &self.message { - // NotificationMessage::Text(text) => { - // Text::new(text.to_owned(), theme.message.text.clone()).into_any() - // } - // NotificationMessage::Element(e) => e(theme.message.text.clone(), cx), - // }; - // let on_click = self.on_click.clone(); - // let has_click_action = on_click.is_some(); - - // Flex::column() - // .with_child( - // Flex::row() - // .with_child( - // message - // .contained() - // .with_style(theme.message.container) - // .aligned() - // .top() - // .left() - // .flex(1., true), - // ) - // .with_child( - // MouseEventHandler::new::(0, cx, |state, _| { - // let style = theme.dismiss_button.style_for(state); - // Svg::new("icons/x.svg") - // .with_color(style.color) - // .constrained() - // .with_width(style.icon_width) - // .aligned() - // .contained() - // .with_style(style.container) - // .constrained() - // .with_width(style.button_width) - // .with_height(style.button_width) - // }) - // .with_padding(Padding::uniform(5.)) - // .on_click(MouseButton::Left, move |_, this, cx| { - // this.dismiss(&Default::default(), cx); - // }) - // .with_cursor_style(CursorStyle::PointingHand) - // .aligned() - // .constrained() - // .with_height(cx.font_cache().line_height(theme.message.text.font_size)) - // .aligned() - // .top() - // .flex_float(), - // ), - // ) - // .with_children({ - // click_message - // .map(|click_message| { - // MouseEventHandler::new::( - // 0, - // cx, - // |state, _| { - // let style = theme.action_message.style_for(state); - - // Flex::row() - // .with_child( - // Text::new(click_message, style.text.clone()) - // .contained() - // .with_style(style.container), - // ) - // .contained() - // }, - // ) - // .on_click(MouseButton::Left, move |_, this, cx| { - // if let Some(on_click) = on_click.as_ref() { - // on_click(cx); - // this.dismiss(&Default::default(), cx); - // } - // }) - // // Since we're not using a proper overlay, we have to capture these extra events - // .on_down(MouseButton::Left, |_, _, _| {}) - // .on_up(MouseButton::Left, |_, _, _| {}) - // .with_cursor_style(if has_click_action { - // CursorStyle::PointingHand - // } else { - // CursorStyle::Arrow - // }) - // }) - // .into_iter() - // }) - // .into_any() - // } - // } } pub trait NotifyResultExt { @@ -393,3 +292,18 @@ where } } } + +pub trait NotifyTaskExt { + fn detach_and_notify_err(self, cx: &mut WindowContext); +} + +impl NotifyTaskExt for Task> +where + E: std::fmt::Debug + 'static, + R: 'static, +{ + fn detach_and_notify_err(self, cx: &mut WindowContext) { + cx.spawn(|mut cx| async move { self.await.notify_async_err(&mut cx) }) + .detach(); + } +} diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 04a51fc655be0d7b5d2b890479bef484b5cbc14a..dad7b50ca6c307aef780910ee03249633ccfbfda 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -31,8 +31,8 @@ use std::{ use theme::ThemeSettings; use ui::{ - prelude::*, right_click_menu, ButtonSize, Color, Icon, IconButton, IconSize, Indicator, Label, - Tab, TabBar, TabPosition, Tooltip, + prelude::*, right_click_menu, ButtonSize, Color, IconButton, IconName, IconSize, Indicator, + Label, Tab, TabBar, TabPosition, Tooltip, }; use ui::{v_stack, ContextMenu}; use util::{maybe, truncate_and_remove_front, ResultExt}; @@ -242,87 +242,6 @@ pub struct DraggedTab { pub is_active: bool, } -// pub struct DraggedItem { -// pub handle: Box, -// pub pane: WeakView, -// } - -// pub enum ReorderBehavior { -// None, -// MoveAfterActive, -// MoveToIndex(usize), -// } - -// #[derive(Debug, Clone, Copy, PartialEq, Eq)] -// enum TabBarContextMenuKind { -// New, -// Split, -// } - -// struct TabBarContextMenu { -// kind: TabBarContextMenuKind, -// handle: View, -// } - -// impl TabBarContextMenu { -// fn handle_if_kind(&self, kind: TabBarContextMenuKind) -> Option> { -// if self.kind == kind { -// return Some(self.handle.clone()); -// } -// None -// } -// } - -// #[allow(clippy::too_many_arguments)] -// fn nav_button)>( -// svg_path: &'static str, -// style: theme::Interactive, -// nav_button_height: f32, -// tooltip_style: TooltipStyle, -// enabled: bool, -// on_click: F, -// tooltip_action: A, -// action_name: &str, -// cx: &mut ViewContext, -// ) -> AnyElement { -// MouseEventHandler::new::(0, cx, |state, _| { -// let style = if enabled { -// style.style_for(state) -// } else { -// style.disabled_style() -// }; -// Svg::new(svg_path) -// .with_color(style.color) -// .constrained() -// .with_width(style.icon_width) -// .aligned() -// .contained() -// .with_style(style.container) -// .constrained() -// .with_width(style.button_width) -// .with_height(nav_button_height) -// .aligned() -// .top() -// }) -// .with_cursor_style(if enabled { -// CursorStyle::PointingHand -// } else { -// CursorStyle::default() -// }) -// .on_click(MouseButton::Left, move |_, toolbar, cx| { -// on_click(toolbar, cx) -// }) -// .with_tooltip::( -// 0, -// action_name.to_string(), -// Some(Box::new(tooltip_action)), -// tooltip_style, -// cx, -// ) -// .contained() -// .into_any_named("nav button") -// } - impl EventEmitter for Pane {} impl Pane { @@ -333,13 +252,6 @@ impl Pane { can_drop_predicate: Option bool + 'static>>, cx: &mut ViewContext, ) -> Self { - // todo!("context menu") - // let pane_view_id = cx.view_id(); - // let context_menu = cx.build_view(|cx| ContextMenu::new(pane_view_id, cx)); - // context_menu.update(cx, |menu, _| { - // menu.set_position_mode(OverlayPositionMode::Local) - // }); - // let focus_handle = cx.focus_handle(); let subscriptions = vec![ @@ -370,11 +282,6 @@ impl Pane { split_item_menu: None, tab_bar_scroll_handle: ScrollHandle::new(), drag_split_direction: None, - // tab_bar_context_menu: TabBarContextMenu { - // kind: TabBarContextMenuKind::New, - // handle: context_menu, - // }, - // tab_context_menu: cx.build_view(|_| ContextMenu::new(pane_view_id, cx)), workspace, project, can_drop_predicate, @@ -384,7 +291,7 @@ impl Pane { h_stack() .gap_2() .child( - IconButton::new("plus", Icon::Plus) + IconButton::new("plus", IconName::Plus) .icon_size(IconSize::Small) .icon_color(Color::Muted) .on_click(cx.listener(|pane, _, cx| { @@ -406,7 +313,7 @@ impl Pane { el.child(Self::render_menu_overlay(new_item_menu)) }) .child( - IconButton::new("split", Icon::Split) + IconButton::new("split", IconName::Split) .icon_size(IconSize::Small) .icon_color(Color::Muted) .on_click(cx.listener(|pane, _, cx| { @@ -427,11 +334,11 @@ impl Pane { ) .child({ let zoomed = pane.is_zoomed(); - IconButton::new("toggle_zoom", Icon::Maximize) + IconButton::new("toggle_zoom", IconName::Maximize) .icon_size(IconSize::Small) .icon_color(Color::Muted) .selected(zoomed) - .selected_icon(Icon::Minimize) + .selected_icon(IconName::Minimize) .on_click(cx.listener(|pane, _, cx| { pane.toggle_zoom(&crate::ToggleZoom, cx); })) @@ -450,7 +357,6 @@ impl Pane { } pub fn has_focus(&self, cx: &WindowContext) -> bool { - // todo!(); // inline this manually self.focus_handle.contains_focused(cx) } @@ -1570,7 +1476,7 @@ impl Pane { }) .start_slot::(indicator) .end_slot( - IconButton::new("close tab", Icon::Close) + IconButton::new("close tab", IconName::Close) .icon_color(Color::Muted) .size(ButtonSize::None) .icon_size(IconSize::XSmall) @@ -1676,7 +1582,7 @@ impl Pane { h_stack() .gap_2() .child( - IconButton::new("navigate_backward", Icon::ArrowLeft) + IconButton::new("navigate_backward", IconName::ArrowLeft) .icon_size(IconSize::Small) .on_click({ let view = cx.view().clone(); @@ -1686,7 +1592,7 @@ impl Pane { .tooltip(|cx| Tooltip::for_action("Go Back", &GoBack, cx)), ) .child( - IconButton::new("navigate_forward", Icon::ArrowRight) + IconButton::new("navigate_forward", IconName::ArrowRight) .icon_size(IconSize::Small) .on_click({ let view = cx.view().clone(); diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index 4428e42830be725fb79979d6acf3e65a777d9386..3dcdeec37f4e2cceb9e2159d6577b303f5355979 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -12,7 +12,7 @@ use serde::Deserialize; use std::sync::Arc; use ui::{prelude::*, Button}; -const HANDLE_HITBOX_SIZE: f32 = 10.0; //todo!(change this back to 4) +pub const HANDLE_HITBOX_SIZE: f32 = 4.0; const HORIZONTAL_MIN_SIZE: f32 = 80.; const VERTICAL_MIN_SIZE: f32 = 100.; @@ -268,15 +268,6 @@ impl Member { ) }) .into_any() - - // let el = div() - // .flex() - // .flex_1() - // .gap_px() - // .w_full() - // .h_full() - // .bg(cx.theme().colors().editor) - // .children(); } Member::Axis(axis) => axis .render( @@ -579,12 +570,15 @@ mod element { Size, Style, WeakView, WindowContext, }; use parking_lot::Mutex; + use settings::Settings; use smallvec::SmallVec; use ui::prelude::*; use util::ResultExt; use crate::Workspace; + use crate::WorkspaceSettings; + use super::{HANDLE_HITBOX_SIZE, HORIZONTAL_MIN_SIZE, VERTICAL_MIN_SIZE}; const DIVIDER_SIZE: f32 = 1.0; @@ -704,7 +698,6 @@ mod element { proposed_current_pixel_change -= current_pixel_change; } - // todo!(schedule serialize) workspace .update(cx, |this, cx| this.schedule_serialize(cx)) .log_err(); @@ -834,20 +827,39 @@ mod element { debug_assert!(flexes.len() == len); debug_assert!(flex_values_in_bounds(flexes.as_slice())); + let magnification_value = WorkspaceSettings::get(None, cx).active_pane_magnification; + let active_pane_magnification = if magnification_value == 1. { + None + } else { + Some(magnification_value) + }; + + let total_flex = if let Some(flex) = active_pane_magnification { + self.children.len() as f32 - 1. + flex + } else { + len as f32 + }; + let mut origin = bounds.origin; - let space_per_flex = bounds.size.along(self.axis) / len as f32; + let space_per_flex = bounds.size.along(self.axis) / total_flex; let mut bounding_boxes = self.bounding_boxes.lock(); bounding_boxes.clear(); for (ix, child) in self.children.iter_mut().enumerate() { - //todo!(active_pane_magnification) - // If using active pane magnification, need to switch to using - // 1 for all non-active panes, and then the magnification for the - // active pane. + let child_flex = active_pane_magnification + .map(|magnification| { + if self.active_pane_ix == Some(ix) { + magnification + } else { + 1. + } + }) + .unwrap_or_else(|| flexes[ix]); + let child_size = bounds .size - .apply_along(self.axis, |_| space_per_flex * flexes[ix]); + .apply_along(self.axis, |_| space_per_flex * child_flex); let child_bounds = Bounds { origin, @@ -857,20 +869,23 @@ mod element { cx.with_z_index(0, |cx| { child.draw(origin, child_size.into(), cx); }); - cx.with_z_index(1, |cx| { - if ix < len - 1 { - Self::push_handle( - self.flexes.clone(), - state.clone(), - self.axis, - ix, - child_bounds, - bounds, - self.workspace.clone(), - cx, - ); - } - }); + + if active_pane_magnification.is_none() { + cx.with_z_index(1, |cx| { + if ix < len - 1 { + Self::push_handle( + self.flexes.clone(), + state.clone(), + self.axis, + ix, + child_bounds, + bounds, + self.workspace.clone(), + cx, + ); + } + }); + } origin = origin.apply_along(self.axis, |val| val + child_size.along(self.axis)); } diff --git a/crates/workspace/src/searchable.rs b/crates/workspace/src/searchable.rs index 59202cbbaf53e5e4f62d77be693e3b13916b555b..e1f93f31cb22cc7fe7c7b23158de6bb034424526 100644 --- a/crates/workspace/src/searchable.rs +++ b/crates/workspace/src/searchable.rs @@ -1,8 +1,8 @@ use std::{any::Any, sync::Arc}; use gpui::{ - AnyView, AppContext, EventEmitter, Subscription, Task, View, ViewContext, WeakView, - WindowContext, + AnyView, AnyWeakView, AppContext, EventEmitter, Subscription, Task, View, ViewContext, + WeakView, WindowContext, }; use project::search::SearchQuery; @@ -127,7 +127,6 @@ pub trait SearchableItemHandle: ItemHandle { ) -> Option; } -// todo!("here is where we need to use AnyWeakView"); impl SearchableItemHandle for View { fn downgrade(&self) -> Box { Box::new(self.downgrade()) @@ -249,7 +248,7 @@ impl Eq for Box {} pub trait WeakSearchableItemHandle: WeakItemHandle { fn upgrade(&self, cx: &AppContext) -> Option>; - // fn into_any(self) -> AnyWeakView; + fn into_any(self) -> AnyWeakView; } impl WeakSearchableItemHandle for WeakView { @@ -257,9 +256,9 @@ impl WeakSearchableItemHandle for WeakView { Some(Box::new(self.upgrade()?)) } - // fn into_any(self) -> AnyView { - // self.into_any() - // } + fn into_any(self) -> AnyWeakView { + self.into() + } } impl PartialEq for Box { diff --git a/crates/workspace/src/shared_screen.rs b/crates/workspace/src/shared_screen.rs index edfabed60d3a03e2290fb94dc9c8482a7e9b4a5e..5b1ca6477ee99b47ad7abc08e64e0421444c5dfd 100644 --- a/crates/workspace/src/shared_screen.rs +++ b/crates/workspace/src/shared_screen.rs @@ -12,7 +12,7 @@ use gpui::{ WindowContext, }; use std::sync::{Arc, Weak}; -use ui::{h_stack, prelude::*, Icon, IconElement, Label}; +use ui::{h_stack, prelude::*, Icon, IconName, Label}; pub enum Event { Close, @@ -100,7 +100,7 @@ impl Item for SharedScreen { ) -> gpui::AnyElement { h_stack() .gap_1() - .child(IconElement::new(Icon::Screen)) + .child(Icon::new(IconName::Screen)) .child( Label::new(format!("{}'s screen", self.user.github_login)).color(if selected { Color::Default diff --git a/crates/workspace/src/toolbar.rs b/crates/workspace/src/toolbar.rs index dc17cd3c1956b6f8475f04c6478ab86a86eb5689..cc072b08b9eac0cf13039dd45216483018d5701a 100644 --- a/crates/workspace/src/toolbar.rs +++ b/crates/workspace/src/toolbar.rs @@ -133,82 +133,6 @@ impl Render for Toolbar { } } -// todo!() -// impl View for Toolbar { -// fn ui_name() -> &'static str { -// "Toolbar" -// } - -// fn render(&mut self, cx: &mut ViewContext) -> AnyElement { -// let theme = &theme::current(cx).workspace.toolbar; - -// let mut primary_left_items = Vec::new(); -// let mut primary_right_items = Vec::new(); -// let mut secondary_item = None; -// let spacing = theme.item_spacing; -// let mut primary_items_row_count = 1; - -// for (item, position) in &self.items { -// match *position { -// ToolbarItemLocation::Hidden => {} - -// ToolbarItemLocation::PrimaryLeft { flex } => { -// primary_items_row_count = primary_items_row_count.max(item.row_count(cx)); -// let left_item = ChildView::new(item.as_any(), cx).aligned(); -// if let Some((flex, expanded)) = flex { -// primary_left_items.push(left_item.flex(flex, expanded).into_any()); -// } else { -// primary_left_items.push(left_item.into_any()); -// } -// } - -// ToolbarItemLocation::PrimaryRight { flex } => { -// primary_items_row_count = primary_items_row_count.max(item.row_count(cx)); -// let right_item = ChildView::new(item.as_any(), cx).aligned().flex_float(); -// if let Some((flex, expanded)) = flex { -// primary_right_items.push(right_item.flex(flex, expanded).into_any()); -// } else { -// primary_right_items.push(right_item.into_any()); -// } -// } - -// ToolbarItemLocation::Secondary => { -// secondary_item = Some( -// ChildView::new(item.as_any(), cx) -// .constrained() -// .with_height(theme.height * item.row_count(cx) as f32) -// .into_any(), -// ); -// } -// } -// } - -// let container_style = theme.container; -// let height = theme.height * primary_items_row_count as f32; - -// let mut primary_items = Flex::row().with_spacing(spacing); -// primary_items.extend(primary_left_items); -// primary_items.extend(primary_right_items); - -// let mut toolbar = Flex::column(); -// if !primary_items.is_empty() { -// toolbar.add_child(primary_items.constrained().with_height(height)); -// } -// if let Some(secondary_item) = secondary_item { -// toolbar.add_child(secondary_item); -// } - -// if toolbar.is_empty() { -// toolbar.into_any_named("toolbar") -// } else { -// toolbar -// .contained() -// .with_style(container_style) -// .into_any_named("toolbar") -// } -// } -// } - impl Toolbar { pub fn new() -> Self { Self { @@ -312,10 +236,3 @@ impl ToolbarItemViewHandle for View { self.read(cx).row_count(cx) } } - -// todo!() -// impl From<&dyn ToolbarItemViewHandle> for AnyViewHandle { -// fn from(val: &dyn ToolbarItemViewHandle) -> Self { -// val.as_any().clone() -// } -// } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 826a6693d7ca350a85efa4589e5c87ba90bbf94c..839edf1009ab4c9397b16c9f9959bab89f8c169c 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -26,12 +26,12 @@ use futures::{ }; use gpui::{ actions, canvas, div, impl_actions, point, size, Action, AnyElement, AnyModel, AnyView, - AnyWeakView, AnyWindowHandle, AppContext, AsyncAppContext, AsyncWindowContext, BorrowWindow, - Bounds, Context, Div, DragMoveEvent, Element, Entity, EntityId, EventEmitter, FocusHandle, - FocusableView, GlobalPixels, InteractiveElement, IntoElement, KeyContext, LayoutId, - ManagedView, Model, ModelContext, ParentElement, PathPromptOptions, Pixels, Point, PromptLevel, - Render, Size, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, - WindowBounds, WindowContext, WindowHandle, WindowOptions, + AnyWeakView, AppContext, AsyncAppContext, AsyncWindowContext, BorrowWindow, Bounds, Context, + Div, DragMoveEvent, Element, Entity, EntityId, EventEmitter, FocusHandle, FocusableView, + GlobalPixels, InteractiveElement, IntoElement, KeyContext, LayoutId, ManagedView, Model, + ModelContext, ParentElement, PathPromptOptions, Pixels, Point, PromptLevel, Render, Size, + Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowBounds, + WindowContext, WindowHandle, WindowOptions, }; use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem}; use itertools::Itertools; @@ -537,7 +537,7 @@ impl Workspace { }) .detach(); - cx.on_blur_window(|this, cx| { + cx.on_focus_lost(|this, cx| { let focus_handle = this.focus_handle(cx); cx.focus(&focus_handle); }) @@ -852,6 +852,10 @@ impl Workspace { &self.right_dock } + pub fn is_edited(&self) -> bool { + self.window_edited + } + pub fn add_panel(&mut self, panel: View, cx: &mut ViewContext) { let dock = match panel.position(cx) { DockPosition::Left => &self.left_dock, @@ -943,10 +947,8 @@ impl Workspace { cx: &mut ViewContext, ) -> Task> { let to_load = if let Some(pane) = pane.upgrade() { - // todo!("focus") - // cx.focus(&pane); - pane.update(cx, |pane, cx| { + pane.focus(cx); loop { // Retrieve the weak item handle from the history. let entry = pane.nav_history_mut().pop(mode, cx)?; @@ -1145,7 +1147,6 @@ impl Workspace { quitting: bool, cx: &mut ViewContext, ) -> Task> { - //todo!(saveing) let active_call = self.active_call().cloned(); let window = cx.window_handle(); @@ -1631,8 +1632,7 @@ impl Workspace { }); } - // todo!("focus") - // cx.focus_self(); + cx.focus_self(); cx.notify(); self.serialize_workspace(cx); } @@ -1697,27 +1697,6 @@ impl Workspace { None } - // todo!("implement zoom") - #[allow(unused)] - fn zoom_out(&mut self, cx: &mut ViewContext) { - for pane in &self.panes { - pane.update(cx, |pane, cx| pane.set_zoomed(false, cx)); - } - - self.left_dock.update(cx, |dock, cx| dock.zoom_out(cx)); - self.bottom_dock.update(cx, |dock, cx| dock.zoom_out(cx)); - self.right_dock.update(cx, |dock, cx| dock.zoom_out(cx)); - self.zoomed = None; - self.zoomed_position = None; - - cx.notify(); - } - - // #[cfg(any(test, feature = "test-support"))] - // pub fn zoomed_view(&self, cx: &AppContext) -> Option { - // self.zoomed.and_then(|view| view.upgrade(cx)) - // } - fn dismiss_zoomed_items_to_reveal( &mut self, dock_to_reveal: Option, @@ -2080,7 +2059,7 @@ impl Workspace { _ => bounding_box.center(), }; - let distance_to_next = 8.; //todo(pane dividers styling) + let distance_to_next = pane_group::HANDLE_HITBOX_SIZE; let target = match direction { SplitDirection::Left => { @@ -2992,7 +2971,6 @@ impl Workspace { cx.notify(); } - #[allow(unused)] fn schedule_serialize(&mut self, cx: &mut ViewContext) { self._schedule_serialize = Some(cx.spawn(|this, mut cx| async move { cx.background_executor() @@ -3386,6 +3364,10 @@ impl Workspace { div } + pub fn has_active_modal(&self, cx: &WindowContext<'_>) -> bool { + self.modal_layer.read(cx).has_active_modal() + } + pub fn active_modal( &mut self, cx: &ViewContext, @@ -4034,34 +4016,34 @@ pub fn join_channel( return anyhow::Ok(()); } - if requesting_window.is_some() { - return anyhow::Ok(()); - } - // find an existing workspace to focus and show call controls - let mut active_window = activate_any_workspace_window(&mut cx); + let mut active_window = + requesting_window.or_else(|| activate_any_workspace_window(&mut cx)); if active_window.is_none() { // no open workspaces, make one to show the error in (blergh) - cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), requesting_window, cx))? + let (window_handle, _) = cx + .update(|cx| { + Workspace::new_local(vec![], app_state.clone(), requesting_window, cx) + })? .await?; - } - active_window = activate_any_workspace_window(&mut cx); - let Some(active_window) = active_window else { - return anyhow::Ok(()); - }; + active_window = Some(window_handle); + } if let Err(err) = result { - active_window - .update(&mut cx, |_, cx| { - cx.prompt( - PromptLevel::Critical, - &format!("Failed to join channel: {}", err), - &["Ok"], - ) - })? - .await - .ok(); + log::error!("failed to join channel: {}", err); + if let Some(active_window) = active_window { + active_window + .update(&mut cx, |_, cx| { + cx.prompt( + PromptLevel::Critical, + &format!("Failed to join channel: {}", err), + &["Ok"], + ) + })? + .await + .ok(); + } } // return ok, we showed the error to the user. @@ -4079,19 +4061,17 @@ pub async fn get_any_active_workspace( cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, cx))? .await?; } - activate_any_workspace_window(&mut cx) - .context("could not open zed")? - .downcast::() - .context("could not open zed workspace window") + activate_any_workspace_window(&mut cx).context("could not open zed") } -fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option { +fn activate_any_workspace_window(cx: &mut AsyncAppContext) -> Option> { cx.update(|cx| { for window in cx.windows() { - let is_workspace = window.downcast::().is_some(); - if is_workspace { - window.update(cx, |_, cx| cx.activate_window()).ok(); - return Some(window); + if let Some(workspace_window) = window.downcast::() { + workspace_window + .update(cx, |_, cx| cx.activate_window()) + .ok(); + return Some(workspace_window); } } None diff --git a/crates/workspace/src/workspace_settings.rs b/crates/workspace/src/workspace_settings.rs index f3882a9dbdd53b9935a179d86e7ed698eb026bba..4a922b85c2b42034d5e6c05a755c6a5ee43c4b8c 100644 --- a/crates/workspace/src/workspace_settings.rs +++ b/crates/workspace/src/workspace_settings.rs @@ -12,35 +12,39 @@ pub struct WorkspaceSettings { #[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] pub struct WorkspaceSettingsContent { + /// Scale by which to zoom the active pane. + /// When set to 1.0, the active pane has the same size as others, + /// but when set to a larger value, the active pane takes up more space. + /// + /// Default: `1.0` pub active_pane_magnification: Option, + /// Whether or not to prompt the user to confirm before closing the application. + /// + /// Default: false pub confirm_quit: Option, + /// Whether or not to show the call status icon in the status bar. + /// + /// Default: true pub show_call_status_icon: Option, + /// When to automatically save edited buffers. + /// + /// Default: off pub autosave: Option, } #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum AutosaveSetting { + /// Disable autosave. Off, + /// Save after inactivity period of `milliseconds`. AfterDelay { milliseconds: u64 }, + /// Autosave when focus changes. OnFocusChange, + /// Autosave when the active window changes. OnWindowChange, } -#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] -pub struct GitSettings { - pub git_gutter: Option, - pub gutter_debounce: Option, -} - -#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum GitGutterSetting { - #[default] - TrackedFiles, - Hide, -} - impl Settings for WorkspaceSettings { const KEY: Option<&'static str> = None; diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index fbc81c4a4028444fdd5ec3598c91970c24f85de5..9ad15cd27249981b570e05c7cd7ceb3ad1218182 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -30,7 +30,7 @@ command_palette = { path = "../command_palette" } client = { path = "../client" } # clock = { path = "../clock" } copilot = { path = "../copilot" } -copilot_button = { path = "../copilot_button" } +copilot_ui = { path = "../copilot_ui" } diagnostics = { path = "../diagnostics" } db = { path = "../db" } editor = { path = "../editor" } @@ -74,6 +74,7 @@ vim = { path = "../vim" } workspace = { path = "../workspace" } welcome = { path = "../welcome" } zed_actions = {path = "../zed_actions"} +assets = {path = "../assets"} anyhow.workspace = true async-compression.workspace = true async-tar = "0.4.2" diff --git a/crates/zed/build.rs b/crates/zed/build.rs index 08608d0c6a07b3a823c082a4f41ee7f34cc7f3f7..0b13f5bd2f5784e2d17c4b4ff756a5b3a0e221ec 100644 --- a/crates/zed/build.rs +++ b/crates/zed/build.rs @@ -22,7 +22,7 @@ fn main() { println!("cargo:rustc-link-arg=-Wl,-ObjC"); // Populate git sha environment variable if git is available - println!("cargo:rerun-if-changed=.git/logs/HEAD"); + println!("cargo:rerun-if-changed=../../.git/logs/HEAD"); if let Ok(output) = Command::new("git").args(["rev-parse", "HEAD"]).output() { if output.status.success() { let git_sha = String::from_utf8_lossy(&output.stdout); diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 56109d9c9a532d97de0f8b76101b4057203879e2..e10c52a175c8633fa3b3bebdb09223f3505587ad 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -16,6 +16,7 @@ use isahc::{prelude::Configurable, Request}; use language::LanguageRegistry; use log::LevelFilter; +use assets::Assets; use node_runtime::RealNodeRuntime; use parking_lot::Mutex; use serde::{Deserialize, Serialize}; @@ -49,8 +50,8 @@ use welcome::{show_welcome_view, BaseKeymap, FIRST_OPEN}; use workspace::{AppState, WorkspaceStore}; use zed::{ app_menus, build_window_options, ensure_only_instance, handle_cli_connection, - handle_keymap_file_changes, initialize_workspace, languages, Assets, IsOnlyInstance, - OpenListener, OpenRequest, + handle_keymap_file_changes, initialize_workspace, languages, IsOnlyInstance, OpenListener, + OpenRequest, }; fn main() { diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 61b8d6eaf84332474ff992e5a72a85cbde5b4776..38e0bec14e30418bee589450bc21cccc31364daf 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -1,11 +1,9 @@ mod app_menus; -mod assets; pub mod languages; mod only_instance; mod open_listener; pub use app_menus::*; -pub use assets::*; use assistant::AssistantPanel; use breadcrumbs::Breadcrumbs; use collections::VecDeque; @@ -18,13 +16,14 @@ pub use only_instance::*; pub use open_listener::*; use anyhow::{anyhow, Context as _}; +use assets::Assets; use futures::{channel::mpsc, select_biased, StreamExt}; use project_panel::ProjectPanel; use quick_action_bar::QuickActionBar; use search::project_search::ProjectSearchBar; use settings::{initial_local_settings_content, KeymapFile, Settings, SettingsStore}; use std::{borrow::Cow, ops::Deref, sync::Arc}; -use terminal_view::terminal_panel::TerminalPanel; +use terminal_view::terminal_panel::{self, TerminalPanel}; use util::{ asset_str, channel::{AppCommitSha, ReleaseChannel}, @@ -120,8 +119,7 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { // cx.add_view(|cx| CollabTitlebarItem::new(workspace, &workspace_handle, cx)); // workspace.set_titlebar_item(collab_titlebar_item.into_any(), cx); - let copilot = - cx.new_view(|cx| copilot_button::CopilotButton::new(app_state.fs.clone(), cx)); + let copilot = cx.new_view(|cx| copilot_ui::CopilotButton::new(app_state.fs.clone(), cx)); let diagnostic_summary = cx.new_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx)); let activity_indicator = @@ -301,79 +299,42 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { ); }, ) - //todo!() - // cx.add_action({ - // move |workspace: &mut Workspace, _: &DebugElements, cx: &mut ViewContext| { - // let app_state = workspace.app_state().clone(); - // let markdown = app_state.languages.language_for_name("JSON"); - // let window = cx.window(); - // cx.spawn(|workspace, mut cx| async move { - // let markdown = markdown.await.log_err(); - // let content = to_string_pretty(&window.debug_elements(&cx).ok_or_else(|| { - // anyhow!("could not debug elements for window {}", window.id()) - // })?) - // .unwrap(); - // workspace - // .update(&mut cx, |workspace, cx| { - // workspace.with_local_workspace(cx, move |workspace, cx| { - // let project = workspace.project().clone(); - // let buffer = project - // .update(cx, |project, cx| { - // project.create_buffer(&content, markdown, cx) - // }) - // .expect("creating buffers on a local workspace always succeeds"); - // let buffer = cx.add_model(|cx| { - // MultiBuffer::singleton(buffer, cx) - // .with_title("Debug Elements".into()) - // }); - // workspace.add_item( - // Box::new(cx.add_view(|cx| { - // Editor::for_multibuffer(buffer, Some(project.clone()), cx) - // })), - // cx, - // ); - // }) - // })? - // .await - // }) - // .detach_and_log_err(cx); - // } - // }); - // .register_action( - // |workspace: &mut Workspace, - // _: &project_panel::ToggleFocus, - // cx: &mut ViewContext| { - // workspace.toggle_panel_focus::(cx); - // }, - // ); - // cx.add_action( - // |workspace: &mut Workspace, - // _: &collab_ui::collab_panel::ToggleFocus, - // cx: &mut ViewContext| { - // workspace.toggle_panel_focus::(cx); - // }, - // ); - // cx.add_action( - // |workspace: &mut Workspace, - // _: &collab_ui::chat_panel::ToggleFocus, - // cx: &mut ViewContext| { - // workspace.toggle_panel_focus::(cx); - // }, - // ); - // cx.add_action( - // |workspace: &mut Workspace, - // _: &collab_ui::notification_panel::ToggleFocus, - // cx: &mut ViewContext| { - // workspace.toggle_panel_focus::(cx); - // }, - // ); - // cx.add_action( - // |workspace: &mut Workspace, - // _: &terminal_panel::ToggleFocus, - // cx: &mut ViewContext| { - // workspace.toggle_panel_focus::(cx); - // }, - // ); + .register_action( + |workspace: &mut Workspace, + _: &project_panel::ToggleFocus, + cx: &mut ViewContext| { + workspace.toggle_panel_focus::(cx); + }, + ) + .register_action( + |workspace: &mut Workspace, + _: &collab_ui::collab_panel::ToggleFocus, + cx: &mut ViewContext| { + workspace.toggle_panel_focus::(cx); + }, + ) + .register_action( + |workspace: &mut Workspace, + _: &collab_ui::chat_panel::ToggleFocus, + cx: &mut ViewContext| { + workspace.toggle_panel_focus::(cx); + }, + ) + .register_action( + |workspace: &mut Workspace, + _: &collab_ui::notification_panel::ToggleFocus, + cx: &mut ViewContext| { + workspace + .toggle_panel_focus::(cx); + }, + ) + .register_action( + |workspace: &mut Workspace, + _: &terminal_panel::ToggleFocus, + cx: &mut ViewContext| { + workspace.toggle_panel_focus::(cx); + }, + ) .register_action({ let app_state = Arc::downgrade(&app_state); move |_, _: &NewWindow, cx| { @@ -766,7 +727,6 @@ fn open_bundled_file( .detach_and_log_err(cx); } -// todo!() #[cfg(test)] mod tests { use super::*; @@ -791,110 +751,111 @@ mod tests { WorkspaceHandle, }; - // #[gpui::test] - // async fn test_open_paths_action(cx: &mut TestAppContext) { - // let app_state = init_test(cx); - // app_state - // .fs - // .as_fake() - // .insert_tree( - // "/root", - // json!({ - // "a": { - // "aa": null, - // "ab": null, - // }, - // "b": { - // "ba": null, - // "bb": null, - // }, - // "c": { - // "ca": null, - // "cb": null, - // }, - // "d": { - // "da": null, - // "db": null, - // }, - // }), - // ) - // .await; - - // cx.update(|cx| { - // open_paths( - // &[PathBuf::from("/root/a"), PathBuf::from("/root/b")], - // &app_state, - // None, - // cx, - // ) - // }) - // .await - // .unwrap(); - // assert_eq!(cx.read(|cx| cx.windows().len()), 1); - - // cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx)) - // .await - // .unwrap(); - // assert_eq!(cx.read(|cx| cx.windows().len()), 1); - // let workspace_1 = cx - // .read(|cx| cx.windows()[0].downcast::()) - // .unwrap(); - // workspace_1 - // .update(cx, |workspace, cx| { - // assert_eq!(workspace.worktrees(cx).count(), 2); - // assert!(workspace.left_dock().read(cx).is_open()); - // assert!(workspace - // .active_pane() - // .read(cx) - // .focus_handle(cx) - // .is_focused(cx)); - // }) - // .unwrap(); - - // cx.update(|cx| { - // open_paths( - // &[PathBuf::from("/root/b"), PathBuf::from("/root/c")], - // &app_state, - // None, - // cx, - // ) - // }) - // .await - // .unwrap(); - // assert_eq!(cx.read(|cx| cx.windows().len()), 2); - - // // Replace existing windows - // let window = cx - // .update(|cx| cx.windows()[0].downcast::()) - // .unwrap(); - // cx.update(|cx| { - // open_paths( - // &[PathBuf::from("/root/c"), PathBuf::from("/root/d")], - // &app_state, - // Some(window), - // cx, - // ) - // }) - // .await - // .unwrap(); - // assert_eq!(cx.read(|cx| cx.windows().len()), 2); - // let workspace_1 = cx - // .update(|cx| cx.windows()[0].downcast::()) - // .unwrap(); - // workspace_1 - // .update(cx, |workspace, cx| { - // assert_eq!( - // workspace - // .worktrees(cx) - // .map(|w| w.read(cx).abs_path()) - // .collect::>(), - // &[Path::new("/root/c").into(), Path::new("/root/d").into()] - // ); - // assert!(workspace.left_dock().read(cx).is_open()); - // assert!(workspace.active_pane().focus_handle(cx).is_focused(cx)); - // }) - // .unwrap(); - // } + #[gpui::test] + async fn test_open_paths_action(cx: &mut TestAppContext) { + let app_state = init_test(cx); + app_state + .fs + .as_fake() + .insert_tree( + "/root", + json!({ + "a": { + "aa": null, + "ab": null, + }, + "b": { + "ba": null, + "bb": null, + }, + "c": { + "ca": null, + "cb": null, + }, + "d": { + "da": null, + "db": null, + }, + }), + ) + .await; + + cx.update(|cx| { + open_paths( + &[PathBuf::from("/root/a"), PathBuf::from("/root/b")], + &app_state, + None, + cx, + ) + }) + .await + .unwrap(); + assert_eq!(cx.read(|cx| cx.windows().len()), 1); + + cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx)) + .await + .unwrap(); + assert_eq!(cx.read(|cx| cx.windows().len()), 1); + let workspace_1 = cx + .read(|cx| cx.windows()[0].downcast::()) + .unwrap(); + workspace_1 + .update(cx, |workspace, cx| { + assert_eq!(workspace.worktrees(cx).count(), 2); + assert!(workspace.left_dock().read(cx).is_open()); + assert!(workspace + .active_pane() + .read(cx) + .focus_handle(cx) + .is_focused(cx)); + }) + .unwrap(); + + cx.update(|cx| { + open_paths( + &[PathBuf::from("/root/b"), PathBuf::from("/root/c")], + &app_state, + None, + cx, + ) + }) + .await + .unwrap(); + assert_eq!(cx.read(|cx| cx.windows().len()), 2); + + // Replace existing windows + let window = cx + .update(|cx| cx.windows()[0].downcast::()) + .unwrap(); + cx.update(|cx| { + open_paths( + &[PathBuf::from("/root/c"), PathBuf::from("/root/d")], + &app_state, + Some(window), + cx, + ) + }) + .await + .unwrap(); + cx.background_executor.run_until_parked(); + assert_eq!(cx.read(|cx| cx.windows().len()), 2); + let workspace_1 = cx + .update(|cx| cx.windows()[0].downcast::()) + .unwrap(); + workspace_1 + .update(cx, |workspace, cx| { + assert_eq!( + workspace + .worktrees(cx) + .map(|w| w.read(cx).abs_path()) + .collect::>(), + &[Path::new("/root/c").into(), Path::new("/root/d").into()] + ); + assert!(workspace.left_dock().read(cx).is_open()); + assert!(workspace.active_pane().focus_handle(cx).is_focused(cx)); + }) + .unwrap(); + } #[gpui::test] async fn test_window_edit_state(cx: &mut TestAppContext) { @@ -1660,8 +1621,8 @@ mod tests { }) .unwrap(); save_task.await.unwrap(); - // todo!() po - //assert!(!cx.did_prompt_for_new_path()); + + assert!(!cx.did_prompt_for_new_path()); window .update(cx, |_, cx| { editor.update(cx, |editor, cx| { diff --git a/docs/src/developing_zed__building_zed.md b/docs/src/developing_zed__building_zed.md index cb30051ffa19f551b46d6ddefdbf981077cb9fed..7606e369d05cf02226ea5b783f4ff8369a661be3 100644 --- a/docs/src/developing_zed__building_zed.md +++ b/docs/src/developing_zed__building_zed.md @@ -40,7 +40,7 @@ Expect this to take 30min to an hour! Some of these steps will take quite a whil - (not applicable) Fine-grained Tokens, at the moment of writing, did not allow any kind of access of non-owned private repos - Keep the token in the browser tab/editor for the next two steps 1. (Optional but reccomended) Add your GITHUB_TOKEN to your `.zshrc` or `.bashrc` like this: `export GITHUB_TOKEN=yourGithubAPIToken` -1. Ensure the Zed.dev website is checked out in a sibling directory and install it's dependencies: +1. Ensure the Zed.dev website is checked out in a sibling directory and install its dependencies: ``` cd .. git clone https://github.com/zed-industries/zed.dev diff --git a/script/bundle b/script/bundle index 462706679905cbd1b4250f2b455120dfe3e9fcf1..9c0dddbac49bf71dc9ca55534284afadacef9b52 100755 --- a/script/bundle +++ b/script/bundle @@ -132,7 +132,7 @@ else cp -R target/${target_dir}/WebRTC.framework "${app_path}/Contents/Frameworks/" fi -#todo!(The app identifier has been set to 'Dev', but the channel is nightly, RATIONALIZE ALL OF THIS MESS) +# Note: The app identifier for our development builds is the same as the app identifier for nightly. cp crates/${zed_crate}/contents/$channel/embedded.provisionprofile "${app_path}/Contents/" if [[ -n $MACOS_CERTIFICATE && -n $MACOS_CERTIFICATE_PASSWORD && -n $APPLE_NOTARIZATION_USERNAME && -n $APPLE_NOTARIZATION_PASSWORD ]]; then diff --git a/script/lib/bump-version.sh b/script/lib/bump-version.sh index 0e1dfa5131d6a72b4d305511c9e80fab209aaba1..8be7e0b6c80c90262245f0b95e8efd87f38eb43d 100755 --- a/script/lib/bump-version.sh +++ b/script/lib/bump-version.sh @@ -30,7 +30,7 @@ Locally committed and tagged ${package} version ${new_version} To push this: - git push origin ${tag_name} ${branch_name} + git push origin ${branch_name} ${tag_name} To undo this: