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