diff --git a/Cargo.lock b/Cargo.lock index 54e2f483d8..c6e7ecebc1 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.35.0" dependencies = [ "anyhow", "async-trait", @@ -1549,6 +1559,7 @@ dependencies = [ "serde_json", "settings", "smallvec", + "story", "theme", "theme_selector", "time", @@ -1687,12 +1698,11 @@ dependencies = [ "settings", "smol", "theme", - "ui", "util", ] [[package]] -name = "copilot_button" +name = "copilot_ui" version = "0.1.0" dependencies = [ "anyhow", @@ -1705,6 +1715,7 @@ dependencies = [ "settings", "smol", "theme", + "ui", "util", "workspace", "zed_actions", @@ -7437,6 +7448,7 @@ dependencies = [ "backtrace-on-stack-overflow", "chrono", "clap 4.4.4", + "collab_ui", "dialoguer", "editor", "fuzzy", @@ -9528,6 +9540,7 @@ dependencies = [ "activity_indicator", "ai", "anyhow", + "assets", "assistant", "async-compression", "async-recursion 0.3.2", @@ -9546,7 +9559,7 @@ dependencies = [ "collections", "command_palette", "copilot", - "copilot_button", + "copilot_ui", "ctor", "db", "diagnostics", diff --git a/Cargo.toml b/Cargo.toml index 9390bbb265..79d28821d4 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", diff --git a/assets/icons/arrow_circle.svg b/assets/icons/arrow_circle.svg index 750e349e2b..90e352bdea 100644 --- a/assets/icons/arrow_circle.svg +++ b/assets/icons/arrow_circle.svg @@ -1 +1,6 @@ - + + + + + + diff --git a/assets/settings/default.json b/assets/settings/default.json index 8217f1675a..bd157c3e61 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/assets/Cargo.toml b/crates/assets/Cargo.toml new file mode 100644 index 0000000000..7ebae21d7d --- /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 5d5e81a60e..010b7ebda3 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 f53343531a..d4743afb71 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 c0fbc74e9a..b2a9231a57 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 884ed2b7a0..5f0224aa7b 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 a2a90d4f2f..06e445e3de 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 f00172591e..65f786bca4 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 2e4306f0bc..e41c0c06b1 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 9375feedf0..441323ad5f 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/client/src/client.rs b/crates/client/src/client.rs index 3eae9d92bb..0821a8e534 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 1c288c875d..4453bb40ea 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 498ded6d9a..baf279d634 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.35.0" publish = false [[bin]] diff --git a/crates/collab/src/db/ids.rs b/crates/collab/src/db/ids.rs index 9bb766147f..9f77225fb7 100644 --- a/crates/collab/src/db/ids.rs +++ b/crates/collab/src/db/ids.rs @@ -140,6 +140,22 @@ impl ChannelRole { 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 5b8d54f8d3..6e1bf16309 100644 --- a/crates/collab/src/db/queries/projects.rs +++ b/crates/collab/src/db/queries/projects.rs @@ -777,13 +777,129 @@ 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, ) -> 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 collaborators = project_collaborator::Entity::find() .filter(project_collaborator::Column::ProjectId.eq(project_id)) .all(&*tx) diff --git a/crates/collab/src/db/tests/db_tests.rs b/crates/collab/src/db/tests/db_tests.rs index 1f825efd74..5332f227ef 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 68fbb4e4d7..87a423bea9 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,7 @@ 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 { diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 835b48809d..5301ca9a23 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>, @@ -216,40 +216,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 +286,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 +613,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 +970,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 +1004,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); @@ -1693,10 +1698,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 +1742,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 +1751,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 +1790,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 +1813,12 @@ 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 collaborators = session .db() .await - .project_collaborators(project_id, session.connection_id) + .project_collaborators_for_buffer_update(project_id, session.connection_id) .await?; guest_connection_ids = Vec::with_capacity(collaborators.len() - 1); for collaborator in collaborators.iter() { @@ -1828,60 +1851,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 +2588,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 +3090,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 e2051c44a0..32cc074ec9 100644 --- a/crates/collab/src/tests/channel_guest_tests.rs +++ b/crates/collab/src/tests/channel_guest_tests.rs @@ -82,5 +82,13 @@ async fn test_channel_guests( 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()) } diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 457f085f8f..a21235b6f3 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -4936,10 +4936,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 ae84729bac..034a85961f 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; @@ -414,7 +414,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(), + }, }) } } diff --git a/crates/collab_ui/Cargo.toml b/crates/collab_ui/Cargo.toml index f845de3a93..84c1810bc8 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 a13c0ed384..5786ab10d4 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 517fac4fbb..7999db529a 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 ee43b32f10..df8f2a251f 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -31,7 +31,7 @@ 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}; @@ -848,7 +848,7 @@ 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)) @@ -896,8 +896,8 @@ impl CollabPanel { .start_slot( h_stack() .gap_1() - .child(render_tree_branch(is_last, cx)) - .child(IconButton::new(0, Icon::Folder)), + .child(render_tree_branch(is_last, false, cx)) + .child(IconButton::new(0, IconName::Folder)), ) .child(Label::new(project_name.clone())) .tooltip(move |cx| Tooltip::text(format!("Open {}", project_name), cx)) @@ -917,8 +917,8 @@ impl CollabPanel { .start_slot( h_stack() .gap_1() - .child(render_tree_branch(is_last, cx)) - .child(IconButton::new(0, Icon::Screen)), + .child(render_tree_branch(is_last, false, cx)) + .child(IconButton::new(0, IconName::Screen)), ) .child(Label::new("Screen")) .when_some(peer_id, |this, _| { @@ -958,8 +958,8 @@ impl CollabPanel { .start_slot( h_stack() .gap_1() - .child(render_tree_branch(false, cx)) - .child(IconButton::new(0, Icon::File)), + .child(render_tree_branch(false, true, cx)) + .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)) @@ -979,8 +979,8 @@ impl CollabPanel { .start_slot( h_stack() .gap_1() - .child(render_tree_branch(false, cx)) - .child(IconButton::new(0, Icon::MessageBubbles)), + .child(render_tree_branch(false, false, cx)) + .child(IconButton::new(0, IconName::MessageBubbles)), ) .child(Label::new("chat")) .tooltip(move |cx| Tooltip::text("Open Chat", cx)) @@ -1007,7 +1007,7 @@ impl CollabPanel { .start_slot( h_stack() .gap_1() - .child(render_tree_branch(!has_visible_participants, cx)) + .child(render_tree_branch(!has_visible_participants, false, cx)) .child(""), ) .child(Label::new(if count == 1 { @@ -1724,7 +1724,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() @@ -1921,7 +1921,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 +1933,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 +2010,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 +2071,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 +2086,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 +2126,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 +2150,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 +2162,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 +2211,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 }) @@ -2242,7 +2246,7 @@ impl CollabPanel { }; let messages_button = |cx: &mut ViewContext| { - IconButton::new("channel_chat", Icon::MessageBubbles) + IconButton::new("channel_chat", IconName::MessageBubbles) .icon_size(IconSize::Small) .icon_color(if has_messages_notification { Color::Default @@ -2254,7 +2258,7 @@ impl CollabPanel { }; let notes_button = |cx: &mut ViewContext| { - IconButton::new("channel_notes", Icon::File) + IconButton::new("channel_notes", IconName::File) .icon_size(IconSize::Small) .icon_color(if has_notes_notification { Color::Default @@ -2311,9 +2315,13 @@ 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() @@ -2382,7 +2390,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,21 +2402,16 @@ 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()) } } } -fn render_tree_branch(is_last: bool, cx: &mut WindowContext) -> impl IntoElement { +fn render_tree_branch(is_last: bool, overdraw: bool, cx: &mut WindowContext) -> impl IntoElement { let rem_size = cx.rem_size(); let line_height = cx.text_style().line_height_in_pixels(rem_size); let width = rem_size * 1.5; - let thickness = px(2.); + let thickness = px(1.); let color = cx.theme().colors().text; canvas(move |bounds, cx| { @@ -2422,7 +2425,11 @@ fn render_tree_branch(is_last: bool, cx: &mut WindowContext) -> impl IntoElement point(start_x, top), point( start_x + thickness, - if is_last { start_y } else { bounds.bottom() }, + if is_last { + start_y + } else { + bounds.bottom() + if overdraw { px(1.) } else { px(0.) } + }, ), ), color, @@ -2497,10 +2504,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> { @@ -2643,11 +2650,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 f3ae16f793..8020613c1a 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 dbcacef7d6..b769ec7e7f 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 6ccad2db0d..f2106b9a8f 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}; @@ -213,7 +213,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 +230,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 +256,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 +281,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 +573,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 +643,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 +665,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 e7c94984b2..95473044a3 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 5c184ec5c8..7759fef520 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 0000000000..fa0b0a1b14 --- /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 fa28ef9a60..223415119f 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; @@ -31,8 +28,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 +126,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 982214c3e5..79adc69a80 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) { @@ -130,51 +131,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 0000000000..36518679c6 --- /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 0000000000..c43cac46d2 --- /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 250817a803..13fa26a341 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 588c747696..fefd49090f 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 658eb3451f..89d1086c8e 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 ba5dbe0e31..0000000000 --- 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 63788f9d28..491f4f3cde 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 60b25fee12..e5a1a94235 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 0000000000..64dd068d5a --- /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 0000000000..ba6f54b634 --- /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 613fadf7f7..844a44c54f 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 da1f77b9af..035b84e102 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}; @@ -24,24 +24,16 @@ impl Render for DiagnosticIndicator { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { let diagnostic_indicator = match (self.summary.error_count, self.summary.warning_count) { (0, 0) => h_stack().map(|this| { - if !self.in_progress_checks.is_empty() { - this.child( - IconElement::new(Icon::ArrowCircle) - .size(IconSize::Small) - .color(Color::Muted), - ) - } else { - this.child( - IconElement::new(Icon::Check) - .size(IconSize::Small) - .color(Color::Default), - ) - } + this.child( + Icon::new(IconName::Check) + .size(IconSize::Small) + .color(Color::Default), + ) }), (0, warning_count) => h_stack() .gap_1() .child( - IconElement::new(Icon::ExclamationTriangle) + Icon::new(IconName::ExclamationTriangle) .size(IconSize::Small) .color(Color::Warning), ) @@ -49,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), ) @@ -57,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), ) @@ -72,9 +64,14 @@ impl Render for DiagnosticIndicator { let status = if !self.in_progress_checks.is_empty() { Some( - Label::new("Checking…") - .size(LabelSize::Small) - .color(Color::Muted) + h_stack() + .gap_2() + .child(Icon::new(IconName::ArrowCircle).size(IconSize::Small)) + .child( + Label::new("Checking…") + .size(LabelSize::Small) + .into_any_element(), + ) .into_any_element(), ) } else if let Some(diagnostic) = &self.current_diagnostic { diff --git a/crates/diagnostics/src/project_diagnostics_settings.rs b/crates/diagnostics/src/project_diagnostics_settings.rs index f762d2b1e6..d0feeeb3a7 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 897e2ccf40..3c09e3fad9 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/editor.rs b/crates/editor/src/editor.rs index 231f76218a..9858cf8372 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}; @@ -4223,7 +4223,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 +4257,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 +4269,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) }) }) @@ -9739,7 +9739,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 fd7e2feea3..212ce9fd34 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/element.rs b/crates/editor/src/element.rs index e96cb5df0e..c7fbb658a3 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -795,7 +795,7 @@ impl EditorElement { cx.paint_quad(quad( highlight_bounds, Corners::all(1. * line_height), - gpui::yellow(), // todo!("use the right color") + cx.theme().status().modified, Edges::default(), transparent_black(), )); @@ -850,7 +850,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(), )); diff --git a/crates/editor/src/test.rs b/crates/editor/src/test.rs index 4ce539ad79..d3337db258 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 a02540bc5b..377d4cea5c 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 6c5308c1c6..bf7a071560 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 ce68819646..d49eb9ee60 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 daf6f820cd..804a0b5761 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 e335c4255e..81b392087a 100644 --- a/crates/gpui/src/action.rs +++ b/crates/gpui/src/action.rs @@ -203,7 +203,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/test_context.rs b/crates/gpui/src/app/test_context.rs index 0f71ea61a9..a530aaf69b 100644 --- a/crates/gpui/src/app/test_context.rs +++ b/crates/gpui/src/app/test_context.rs @@ -467,12 +467,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 diff --git a/crates/gpui/src/elements/img.rs b/crates/gpui/src/elements/img.rs index 650b5b666b..71a51351fd 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 eab3ee60b4..6772baa2f9 100644 --- a/crates/gpui/src/elements/overlay.rs +++ b/crates/gpui/src/elements/overlay.rs @@ -14,8 +14,8 @@ pub struct Overlay { children: SmallVec<[AnyElement; 2]>, anchor_corner: AnchorCorner, fit_mode: OverlayFitMode, - // todo!(); anchor_position: Option>, + // todo!(); // position_mode: OverlayPositionMode, } diff --git a/crates/gpui/src/gpui.rs b/crates/gpui/src/gpui.rs index d5236d8f08..6f5e30149d 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 f80b0f0c2f..0d6ec81557 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/key_dispatch.rs b/crates/gpui/src/key_dispatch.rs index 22c4dffc03..81f66746c5 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; } diff --git a/crates/gpui/src/platform/test/display.rs b/crates/gpui/src/platform/test/display.rs index 95f1daf8e9..68dbb0fdf3 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 695323e9c4..ec3d7a0ff0 100644 --- a/crates/gpui/src/platform/test/platform.rs +++ b/crates/gpui/src/platform/test/platform.rs @@ -103,7 +103,6 @@ impl TestPlatform { } } -// todo!("implement out what our tests needed in GPUI 1") impl Platform for TestPlatform { fn background_executor(&self) -> BackgroundExecutor { self.background_executor.clone() diff --git a/crates/gpui/src/shared_url.rs b/crates/gpui/src/shared_url.rs new file mode 100644 index 0000000000..8fb9018943 --- /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/tests/action_macros.rs b/crates/gpui/tests/action_macros.rs index 9e5f6dea16..99572a4b3c 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/journal/src/journal.rs b/crates/journal/src/journal.rs index 2ae74e7f5d..1ffab2f3d3 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/language_settings.rs b/crates/language/src/language_settings.rs index 49977f690c..5359d184d6 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 f20f481613..8b9169d1cc 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/outline/src/outline.rs b/crates/outline/src/outline.rs index 75d1a09357..a661a693b1 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/project/src/project.rs b/crates/project/src/project.rs index fb3eae1945..044b750ad9 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -4732,7 +4732,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 +6582,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 +8726,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 2a8df47e67..925109ac96 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 8f41c75fb4..e90d323712 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 ae0c074188..461ea303b3 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 a5fb8671f7..251e26ebfb 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 b9a87a1a03..5285684891 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/quick_action_bar/src/quick_action_bar.rs b/crates/quick_action_bar/src/quick_action_bar.rs index d8c42589d6..cf4941bcec 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/rpc/src/macros.rs b/crates/rpc/src/macros.rs index 89e605540d..85e2b0cf87 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/proto.rs b/crates/rpc/src/proto.rs index 336c252630..25b8b00dae 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; } @@ -369,7 +370,7 @@ request_messages!( ); entity_messages!( - project_id, + {project_id, ShareProject}, AddProjectCollaborator, ApplyCodeAction, ApplyCompletionAdditionalEdits, @@ -422,7 +423,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 c889f0a4a4..9b27199360 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, @@ -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, diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index a370cca9f6..6fd66b5bad 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 f0301a5bcc..1b29801e03 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), } } diff --git a/crates/search/src/search_bar.rs b/crates/search/src/search_bar.rs index 628be3112e..0594036c25 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/src/semantic_index.rs b/crates/semantic_index/src/semantic_index.rs index dbcdeee5ed..81c4fbbc3d 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 }); diff --git a/crates/semantic_index/src/semantic_index_settings.rs b/crates/semantic_index/src/semantic_index_settings.rs index 306a38fa9c..73fd49c8f5 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 033b3fa8d9..9f08556757 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 27ddfe26ac..120e60d34a 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 e1605eb4fb..fa8112bac7 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -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 f63b575bf2..14cff3b5a6 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 d936716032..bcaf147af2 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -451,6 +451,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() diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 9992953570..d0b52f5eb2 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 8f5044e49e..4d2e78f0da 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}, @@ -692,7 +690,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/theme/src/theme.rs b/crates/theme/src/theme.rs index c526a381b7..93253b95e7 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -73,13 +73,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/ui/src/components/button/button.rs b/crates/ui/src/components/button/button.rs index 1e60aae03b..398f8f10e2 100644 --- a/crates/ui/src/components/button/button.rs +++ b/crates/ui/src/components/button/button.rs @@ -2,7 +2,7 @@ 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; @@ -14,11 +14,11 @@ 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, } @@ -54,7 +54,7 @@ impl Button { self } - pub fn icon(mut self, icon: impl Into>) -> Self { + pub fn icon(mut self, icon: impl Into>) -> Self { self.icon = icon.into(); self } @@ -74,7 +74,7 @@ impl Button { 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/button/button_icon.rs b/crates/ui/src/components/button/button_icon.rs index 15538bb24d..b8f5427d30 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/icon_button.rs b/crates/ui/src/components/button/icon_button.rs index d9ed6ccb5d..7c5313497c 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 3b77842029..08c95f2d93 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 diff --git a/crates/ui/src/components/context_menu.rs b/crates/ui/src/components/context_menu.rs index 8666ec6565..098c54f33c 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 d4349f61a0..59651ddb0b 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/icon.rs b/crates/ui/src/components/icon.rs index 4c6e48c0fc..908e76ef91 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 671f981083..e0e0583b7c 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/list/list_sub_header.rs b/crates/ui/src/components/list/list_sub_header.rs index 2e976b3517..fc9f35e175 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/stories/button.rs b/crates/ui/src/components/stories/button.rs index 7240812fa5..c3fcdc5ae9 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 83fc5980dd..f6e750de2a 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 66fc4affb3..6a67183e97 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 ffbf7157f5..358dc26a87 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 b3ff096d9d..a25b07df84 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 4c63e593aa..bd7b602620 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 805725315c..289ceff9a6 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/prelude.rs b/crates/ui/src/prelude.rs index 63d6c4b46a..0a86b99f9f 100644 --- a/crates/ui/src/prelude.rs +++ b/crates/ui/src/prelude.rs @@ -14,6 +14,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/styles/typography.rs b/crates/ui/src/styles/typography.rs index 4819791b02..39937ebff1 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,69 @@ impl UiTextSize { } } } + +#[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/vim/src/vim.rs b/crates/vim/src/vim.rs index 62205630a1..3579bf36fe 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 411caa820e..e05a16c350 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 c13a00b11c..ed03695c5f 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) } @@ -395,7 +395,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 @@ -775,7 +774,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 45f6141df2..c629edc696 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/notifications.rs b/crates/workspace/src/notifications.rs index 394772b9c4..36628290bb 100644 --- a/crates/workspace/src/notifications.rs +++ b/crates/workspace/src/notifications.rs @@ -175,7 +175,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 +230,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 +247,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 { diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 04a51fc655..2a434b32d9 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}; @@ -384,7 +384,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 +406,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 +427,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); })) @@ -1570,7 +1570,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 +1676,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 +1686,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 4428e42830..e28d0e63de 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) +const HANDLE_HITBOX_SIZE: f32 = 4.0; const HORIZONTAL_MIN_SIZE: f32 = 80.; const VERTICAL_MIN_SIZE: f32 = 100.; @@ -579,12 +579,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 +707,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 +836,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 +878,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/shared_screen.rs b/crates/workspace/src/shared_screen.rs index edfabed60d..5b1ca6477e 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 dc17cd3c19..cc072b08b9 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 826a6693d7..739ce88636 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; @@ -943,10 +943,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)?; @@ -1631,8 +1629,7 @@ impl Workspace { }); } - // todo!("focus") - // cx.focus_self(); + cx.focus_self(); cx.notify(); self.serialize_workspace(cx); } @@ -1713,6 +1710,7 @@ impl Workspace { cx.notify(); } + // todo!() // #[cfg(any(test, feature = "test-support"))] // pub fn zoomed_view(&self, cx: &AppContext) -> Option { // self.zoomed.and_then(|view| view.upgrade(cx)) @@ -2992,7 +2990,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() @@ -4034,34 +4031,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 = Some(window_handle); } - active_window = activate_any_workspace_window(&mut cx); - let Some(active_window) = active_window else { - return anyhow::Ok(()); - }; - 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 +4076,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 f3882a9dbd..4a922b85c2 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 c17d9c781c..734c225cb1 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 08608d0c6a..0b13f5bd2f 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 56109d9c9a..e10c52a175 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 702c815d34..0b90961d23 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,6 +16,7 @@ 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; @@ -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 = @@ -766,7 +764,6 @@ fn open_bundled_file( .detach_and_log_err(cx); } -// todo!() #[cfg(test)] mod tests { use super::*; @@ -877,6 +874,7 @@ mod tests { }) .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::()) diff --git a/script/lib/bump-version.sh b/script/lib/bump-version.sh index 0e1dfa5131..8be7e0b6c8 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: