Merge branch 'main' into migrations-on-server-start

This commit is contained in:
Conrad Irwin 2024-01-09 09:43:14 -07:00
commit bebb528656
130 changed files with 1924 additions and 1289 deletions

21
Cargo.lock generated
View file

@ -292,6 +292,15 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16"
[[package]]
name = "assets"
version = "0.1.0"
dependencies = [
"anyhow",
"gpui",
"rust-embed",
]
[[package]] [[package]]
name = "assistant" name = "assistant"
version = "0.1.0" version = "0.1.0"
@ -677,6 +686,7 @@ dependencies = [
"log", "log",
"menu", "menu",
"project", "project",
"schemars",
"serde", "serde",
"serde_derive", "serde_derive",
"serde_json", "serde_json",
@ -1442,7 +1452,7 @@ dependencies = [
[[package]] [[package]]
name = "collab" name = "collab"
version = "0.34.0" version = "0.35.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@ -1549,6 +1559,7 @@ dependencies = [
"serde_json", "serde_json",
"settings", "settings",
"smallvec", "smallvec",
"story",
"theme", "theme",
"theme_selector", "theme_selector",
"time", "time",
@ -1687,12 +1698,11 @@ dependencies = [
"settings", "settings",
"smol", "smol",
"theme", "theme",
"ui",
"util", "util",
] ]
[[package]] [[package]]
name = "copilot_button" name = "copilot_ui"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
@ -1705,6 +1715,7 @@ dependencies = [
"settings", "settings",
"smol", "smol",
"theme", "theme",
"ui",
"util", "util",
"workspace", "workspace",
"zed_actions", "zed_actions",
@ -7437,6 +7448,7 @@ dependencies = [
"backtrace-on-stack-overflow", "backtrace-on-stack-overflow",
"chrono", "chrono",
"clap 4.4.4", "clap 4.4.4",
"collab_ui",
"dialoguer", "dialoguer",
"editor", "editor",
"fuzzy", "fuzzy",
@ -9528,6 +9540,7 @@ dependencies = [
"activity_indicator", "activity_indicator",
"ai", "ai",
"anyhow", "anyhow",
"assets",
"assistant", "assistant",
"async-compression", "async-compression",
"async-recursion 0.3.2", "async-recursion 0.3.2",
@ -9546,7 +9559,7 @@ dependencies = [
"collections", "collections",
"command_palette", "command_palette",
"copilot", "copilot",
"copilot_button", "copilot_ui",
"ctor", "ctor",
"db", "db",
"diagnostics", "diagnostics",

View file

@ -1,5 +1,6 @@
[workspace] [workspace]
members = [ members = [
"crates/assets",
"crates/activity_indicator", "crates/activity_indicator",
"crates/ai", "crates/ai",
"crates/assistant", "crates/assistant",
@ -16,7 +17,7 @@ members = [
"crates/collections", "crates/collections",
"crates/command_palette", "crates/command_palette",
"crates/copilot", "crates/copilot",
"crates/copilot_button", "crates/copilot_ui",
"crates/db", "crates/db",
"crates/refineable", "crates/refineable",
"crates/refineable/derive_refineable", "crates/refineable/derive_refineable",

View file

@ -1 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-refresh-cw"><path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/><path d="M8 16H3v5"/></svg> <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 8C3 6.67392 3.52678 5.40215 4.46446 4.46447C5.40214 3.52679 6.67391 3.00001 7.99999 3.00001C9.39779 3.00527 10.7394 3.55069 11.7444 4.52223L13 5.77778" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13 3.00001V5.77778H10.2222" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13 8C13 9.32608 12.4732 10.5978 11.5355 11.5355C10.5978 12.4732 9.32607 13 7.99999 13C6.60219 12.9947 5.26054 12.4493 4.25555 11.4778L3 10.2222" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M5.77777 10.2222H3V13" stroke="black" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 389 B

After

Width:  |  Height:  |  Size: 748 B

Before After
Before After

View file

@ -76,7 +76,7 @@
// or waits for a `copilot::Toggle` // or waits for a `copilot::Toggle`
"show_copilot_suggestions": true, "show_copilot_suggestions": true,
// Whether to show tabs and spaces in the editor. // 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): // 1. Draw tabs and spaces only for the selected text (default):
// "selection" // "selection"
@ -183,7 +183,7 @@
// Default height when the assistant is docked to the bottom. // Default height when the assistant is docked to the bottom.
"default_height": 320, "default_height": 320,
// The default OpenAI model to use when starting new conversations. This // 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"" // 1. "gpt-3.5-turbo-0613""
// 2. "gpt-4-0613"" // 2. "gpt-4-0613""
@ -351,7 +351,7 @@
// } // }
"working_directory": "current_project_directory", "working_directory": "current_project_directory",
// Set the cursor blinking behavior in the terminal. // 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 // 1. Never blink the cursor, ignoring the terminal mode
// "blinking": "off", // "blinking": "off",
// 2. Default the cursor blink to off, but allow the terminal to // 2. Default the cursor blink to off, but allow the terminal to

12
crates/assets/Cargo.toml Normal file
View file

@ -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

View file

@ -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 anyhow::anyhow;
use gpui::{AssetSource, Result, SharedString}; use gpui::{AssetSource, Result, SharedString};

View file

@ -933,7 +933,7 @@ impl AssistantPanel {
} }
fn render_hamburger_button(cx: &mut ViewContext<Self>) -> impl IntoElement { fn render_hamburger_button(cx: &mut ViewContext<Self>) -> impl IntoElement {
IconButton::new("hamburger_button", Icon::Menu) IconButton::new("hamburger_button", IconName::Menu)
.on_click(cx.listener(|this, _event, cx| { .on_click(cx.listener(|this, _event, cx| {
if this.active_editor().is_some() { if this.active_editor().is_some() {
this.set_active_editor_index(None, cx); this.set_active_editor_index(None, cx);
@ -957,7 +957,7 @@ impl AssistantPanel {
} }
fn render_split_button(cx: &mut ViewContext<Self>) -> impl IntoElement { fn render_split_button(cx: &mut ViewContext<Self>) -> impl IntoElement {
IconButton::new("split_button", Icon::Snip) IconButton::new("split_button", IconName::Snip)
.on_click(cx.listener(|this, _event, cx| { .on_click(cx.listener(|this, _event, cx| {
if let Some(active_editor) = this.active_editor() { if let Some(active_editor) = this.active_editor() {
active_editor.update(cx, |editor, cx| editor.split(&Default::default(), cx)); active_editor.update(cx, |editor, cx| editor.split(&Default::default(), cx));
@ -968,7 +968,7 @@ impl AssistantPanel {
} }
fn render_assist_button(cx: &mut ViewContext<Self>) -> impl IntoElement { fn render_assist_button(cx: &mut ViewContext<Self>) -> impl IntoElement {
IconButton::new("assist_button", Icon::MagicWand) IconButton::new("assist_button", IconName::MagicWand)
.on_click(cx.listener(|this, _event, cx| { .on_click(cx.listener(|this, _event, cx| {
if let Some(active_editor) = this.active_editor() { if let Some(active_editor) = this.active_editor() {
active_editor.update(cx, |editor, cx| editor.assist(&Default::default(), cx)); active_editor.update(cx, |editor, cx| editor.assist(&Default::default(), cx));
@ -979,7 +979,7 @@ impl AssistantPanel {
} }
fn render_quote_button(cx: &mut ViewContext<Self>) -> impl IntoElement { fn render_quote_button(cx: &mut ViewContext<Self>) -> impl IntoElement {
IconButton::new("quote_button", Icon::Quote) IconButton::new("quote_button", IconName::Quote)
.on_click(cx.listener(|this, _event, cx| { .on_click(cx.listener(|this, _event, cx| {
if let Some(workspace) = this.workspace.upgrade() { if let Some(workspace) = this.workspace.upgrade() {
cx.window_context().defer(move |cx| { cx.window_context().defer(move |cx| {
@ -994,7 +994,7 @@ impl AssistantPanel {
} }
fn render_plus_button(cx: &mut ViewContext<Self>) -> impl IntoElement { fn render_plus_button(cx: &mut ViewContext<Self>) -> impl IntoElement {
IconButton::new("plus_button", Icon::Plus) IconButton::new("plus_button", IconName::Plus)
.on_click(cx.listener(|this, _event, cx| { .on_click(cx.listener(|this, _event, cx| {
this.new_conversation(cx); this.new_conversation(cx);
})) }))
@ -1004,12 +1004,12 @@ impl AssistantPanel {
fn render_zoom_button(&self, cx: &mut ViewContext<Self>) -> impl IntoElement { fn render_zoom_button(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let zoomed = self.zoomed; let zoomed = self.zoomed;
IconButton::new("zoom_button", Icon::Maximize) IconButton::new("zoom_button", IconName::Maximize)
.on_click(cx.listener(|this, _event, cx| { .on_click(cx.listener(|this, _event, cx| {
this.toggle_zoom(&ToggleZoom, cx); this.toggle_zoom(&ToggleZoom, cx);
})) }))
.selected(zoomed) .selected(zoomed)
.selected_icon(Icon::Minimize) .selected_icon(IconName::Minimize)
.icon_size(IconSize::Small) .icon_size(IconSize::Small)
.tooltip(move |cx| { .tooltip(move |cx| {
Tooltip::for_action(if zoomed { "Zoom Out" } else { "Zoom In" }, &ToggleZoom, 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<Icon> { fn icon(&self, cx: &WindowContext) -> Option<IconName> {
Some(Icon::Ai).filter(|_| AssistantSettings::get_global(cx).button) Some(IconName::Ai).filter(|_| AssistantSettings::get_global(cx).button)
} }
fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> { fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
@ -2349,7 +2349,7 @@ impl ConversationEditor {
div() div()
.id("error") .id("error")
.tooltip(move |cx| Tooltip::text(error.clone(), cx)) .tooltip(move |cx| Tooltip::text(error.clone(), cx))
.child(IconElement::new(Icon::XCircle)), .child(Icon::new(IconName::XCircle)),
) )
} else { } else {
None None
@ -2645,7 +2645,7 @@ impl Render for InlineAssistant {
.justify_center() .justify_center()
.w(measurements.gutter_width) .w(measurements.gutter_width)
.child( .child(
IconButton::new("include_conversation", Icon::Ai) IconButton::new("include_conversation", IconName::Ai)
.on_click(cx.listener(|this, _, cx| { .on_click(cx.listener(|this, _, cx| {
this.toggle_include_conversation(&ToggleIncludeConversation, cx) this.toggle_include_conversation(&ToggleIncludeConversation, cx)
})) }))
@ -2660,7 +2660,7 @@ impl Render for InlineAssistant {
) )
.children(if SemanticIndex::enabled(cx) { .children(if SemanticIndex::enabled(cx) {
Some( Some(
IconButton::new("retrieve_context", Icon::MagnifyingGlass) IconButton::new("retrieve_context", IconName::MagnifyingGlass)
.on_click(cx.listener(|this, _, cx| { .on_click(cx.listener(|this, _, cx| {
this.toggle_retrieve_context(&ToggleRetrieveContext, cx) this.toggle_retrieve_context(&ToggleRetrieveContext, cx)
})) }))
@ -2682,7 +2682,7 @@ impl Render for InlineAssistant {
div() div()
.id("error") .id("error")
.tooltip(move |cx| Tooltip::text(error_message.clone(), cx)) .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 { } else {
None None
@ -2957,7 +2957,7 @@ impl InlineAssistant {
div() div()
.id("error") .id("error")
.tooltip(|cx| Tooltip::text("Not Authenticated. Please ensure you have a valid 'OPENAI_API_KEY' in your environment variables.", cx)) .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() .into_any_element()
), ),
@ -2965,7 +2965,7 @@ impl InlineAssistant {
div() div()
.id("error") .id("error")
.tooltip(|cx| Tooltip::text("Not Indexed", cx)) .tooltip(|cx| Tooltip::text("Not Indexed", cx))
.child(IconElement::new(Icon::XCircle)) .child(Icon::new(IconName::XCircle))
.into_any_element() .into_any_element()
), ),
@ -2996,7 +2996,7 @@ impl InlineAssistant {
div() div()
.id("update") .id("update")
.tooltip(move |cx| Tooltip::text(status_text.clone(), cx)) .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() .into_any_element()
) )
} }
@ -3005,7 +3005,7 @@ impl InlineAssistant {
div() div()
.id("check") .id("check")
.tooltip(|cx| Tooltip::text("Index up to date", cx)) .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() .into_any_element()
), ),
} }

View file

@ -57,12 +57,28 @@ pub struct AssistantSettings {
pub default_open_ai_model: OpenAIModel, pub default_open_ai_model: OpenAIModel,
} }
/// Assistant panel settings
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
pub struct AssistantSettingsContent { pub struct AssistantSettingsContent {
/// Whether to show the assistant panel button in the status bar.
///
/// Default: true
pub button: Option<bool>, pub button: Option<bool>,
/// Where to dock the assistant.
///
/// Default: right
pub dock: Option<AssistantDockPosition>, pub dock: Option<AssistantDockPosition>,
/// Default width in pixels when the assistant is docked to the left or right.
///
/// Default: 640
pub default_width: Option<f32>, pub default_width: Option<f32>,
/// Default height in pixels when the assistant is docked to the bottom.
///
/// Default: 320
pub default_height: Option<f32>, pub default_height: Option<f32>,
/// The default OpenAI model to use when starting new conversations.
///
/// Default: gpt-4-1106-preview
pub default_open_ai_model: Option<OpenAIModel>, pub default_open_ai_model: Option<OpenAIModel>,
} }

View file

@ -22,6 +22,7 @@ anyhow.workspace = true
isahc.workspace = true isahc.workspace = true
lazy_static.workspace = true lazy_static.workspace = true
log.workspace = true log.workspace = true
schemars.workspace = true
serde.workspace = true serde.workspace = true
serde_derive.workspace = true serde_derive.workspace = true
serde_json.workspace = true serde_json.workspace = true

View file

@ -10,6 +10,7 @@ use gpui::{
}; };
use isahc::AsyncBody; use isahc::AsyncBody;
use schemars::JsonSchema;
use serde::Deserialize; use serde::Deserialize;
use serde_derive::Serialize; use serde_derive::Serialize;
use smol::io::AsyncReadExt; use smol::io::AsyncReadExt;
@ -61,18 +62,27 @@ struct JsonRelease {
struct AutoUpdateSetting(bool); struct AutoUpdateSetting(bool);
/// Whether or not to automatically check for updates.
///
/// Default: true
#[derive(Clone, Default, JsonSchema, Deserialize, Serialize)]
#[serde(transparent)]
struct AutoUpdateSettingOverride(Option<bool>);
impl Settings for AutoUpdateSetting { impl Settings for AutoUpdateSetting {
const KEY: Option<&'static str> = Some("auto_update"); const KEY: Option<&'static str> = Some("auto_update");
type FileContent = Option<bool>; type FileContent = AutoUpdateSettingOverride;
fn load( fn load(
default_value: &Option<bool>, default_value: &Self::FileContent,
user_values: &[&Option<bool>], user_values: &[&Self::FileContent],
_: &mut AppContext, _: &mut AppContext,
) -> Result<Self> { ) -> Result<Self> {
Ok(Self( 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)?,
)) ))
} }
} }

View file

@ -4,7 +4,7 @@ use gpui::{
}; };
use menu::Cancel; use menu::Cancel;
use util::channel::ReleaseChannel; 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 { pub struct UpdateNotification {
version: SemanticVersion, version: SemanticVersion,
@ -30,7 +30,7 @@ impl Render for UpdateNotification {
.child( .child(
div() div()
.id("cancel") .id("cancel")
.child(IconElement::new(Icon::Close)) .child(Icon::new(IconName::Close))
.cursor_pointer() .cursor_pointer()
.on_click(cx.listener(|this, _, cx| this.dismiss(&menu::Cancel, cx))), .on_click(cx.listener(|this, _, cx| this.dismiss(&menu::Cancel, cx))),
), ),

View file

@ -67,7 +67,10 @@ impl Render for Breadcrumbs {
}) })
.tooltip(|cx| Tooltip::for_action("Show symbol outline", &outline::Toggle, cx)), .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),
} }
} }
} }

View file

@ -9,8 +9,12 @@ pub struct CallSettings {
pub mute_on_join: bool, pub mute_on_join: bool,
} }
/// Configuration of voice calls in Zed.
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
pub struct CallSettingsContent { pub struct CallSettingsContent {
/// Whether the microphone should be muted when joining a channel or a call.
///
/// Default: false
pub mute_on_join: Option<bool>, pub mute_on_join: Option<bool>,
} }

View file

@ -352,9 +352,16 @@ pub struct TelemetrySettings {
pub metrics: bool, pub metrics: bool,
} }
/// Control what info is collected by Zed.
#[derive(Default, Clone, Serialize, Deserialize, JsonSchema)] #[derive(Default, Clone, Serialize, Deserialize, JsonSchema)]
pub struct TelemetrySettingsContent { pub struct TelemetrySettingsContent {
/// Send debug info like crash reports.
///
/// Default: true
pub diagnostics: Option<bool>, pub diagnostics: Option<bool>,
/// Send anonymized usage data like what languages you're using Zed with.
///
/// Default: true
pub metrics: Option<bool>, pub metrics: Option<bool>,
} }

View file

@ -3,7 +3,7 @@ use anyhow::{anyhow, Context, Result};
use collections::{hash_map::Entry, HashMap, HashSet}; use collections::{hash_map::Entry, HashMap, HashSet};
use feature_flags::FeatureFlagAppExt; use feature_flags::FeatureFlagAppExt;
use futures::{channel::mpsc, Future, StreamExt}; 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 postage::{sink::Sink, watch};
use rpc::proto::{RequestMessage, UsersResponse}; use rpc::proto::{RequestMessage, UsersResponse};
use std::sync::{Arc, Weak}; use std::sync::{Arc, Weak};
@ -19,7 +19,7 @@ pub struct ParticipantIndex(pub u32);
pub struct User { pub struct User {
pub id: UserId, pub id: UserId,
pub github_login: String, pub github_login: String,
pub avatar_uri: SharedString, pub avatar_uri: SharedUrl,
} }
#[derive(Clone, Debug, PartialEq, Eq)] #[derive(Clone, Debug, PartialEq, Eq)]

View file

@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathan@zed.dev>"]
default-run = "collab" default-run = "collab"
edition = "2021" edition = "2021"
name = "collab" name = "collab"
version = "0.34.0" version = "0.35.0"
publish = false publish = false
[[bin]] [[bin]]

View file

@ -140,6 +140,22 @@ impl ChannelRole {
Guest | Banned => false, 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<proto::ChannelRole> for ChannelRole { impl From<proto::ChannelRole> for ChannelRole {

View file

@ -777,13 +777,129 @@ impl Database {
.await .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<ConnectionId> {
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<ConnectionId> {
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, &self,
project_id: ProjectId, project_id: ProjectId,
connection_id: ConnectionId, connection_id: ConnectionId,
) -> Result<RoomGuard<Vec<ProjectCollaborator>>> { ) -> Result<RoomGuard<Vec<ProjectCollaborator>>> {
let room_id = self.room_id_for_project(project_id).await?; let room_id = self.room_id_for_project(project_id).await?;
self.room_transaction(room_id, |tx| async move { 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() let collaborators = project_collaborator::Entity::find()
.filter(project_collaborator::Column::ProjectId.eq(project_id)) .filter(project_collaborator::Column::ProjectId.eq(project_id))
.all(&*tx) .all(&*tx)

View file

@ -455,7 +455,7 @@ async fn test_project_count(db: &Arc<Database>) {
.unwrap(); .unwrap();
let room_id = RoomId::from_proto( 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 .await
.unwrap() .unwrap()
.id, .id,
@ -473,7 +473,7 @@ async fn test_project_count(db: &Arc<Database>) {
room_id, room_id,
user2.user_id, user2.user_id,
ConnectionId { owner_id, id: 1 }, ConnectionId { owner_id, id: 1 },
"dev", "test",
) )
.await .await
.unwrap(); .unwrap();

View file

@ -88,7 +88,7 @@ impl std::fmt::Display for Error {
impl std::error::Error for Error {} impl std::error::Error for Error {}
#[derive(Default, Deserialize)] #[derive(Deserialize)]
pub struct Config { pub struct Config {
pub http_port: u16, pub http_port: u16,
pub database_url: String, pub database_url: String,
@ -100,7 +100,7 @@ pub struct Config {
pub live_kit_secret: Option<String>, pub live_kit_secret: Option<String>,
pub rust_log: Option<String>, pub rust_log: Option<String>,
pub log_json: Option<bool>, pub log_json: Option<bool>,
pub zed_environment: String, pub zed_environment: Arc<str>,
} }
impl Config { impl Config {

View file

@ -42,7 +42,7 @@ use prometheus::{register_int_gauge, IntGauge};
use rpc::{ use rpc::{
proto::{ proto::{
self, Ack, AnyTypedEnvelope, EntityMessage, EnvelopedMessage, LiveKitConnectionInfo, self, Ack, AnyTypedEnvelope, EntityMessage, EnvelopedMessage, LiveKitConnectionInfo,
RequestMessage, UpdateChannelBufferCollaborators, RequestMessage, ShareProject, UpdateChannelBufferCollaborators,
}, },
Connection, ConnectionId, Peer, Receipt, TypedEnvelope, Connection, ConnectionId, Peer, Receipt, TypedEnvelope,
}; };
@ -66,7 +66,6 @@ use time::OffsetDateTime;
use tokio::sync::{watch, Semaphore}; use tokio::sync::{watch, Semaphore};
use tower::ServiceBuilder; use tower::ServiceBuilder;
use tracing::{info_span, instrument, Instrument}; use tracing::{info_span, instrument, Instrument};
use util::channel::RELEASE_CHANNEL_NAME;
pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30); pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30);
pub const CLEANUP_TIMEOUT: Duration = Duration::from_secs(10); pub const CLEANUP_TIMEOUT: Duration = Duration::from_secs(10);
@ -104,6 +103,7 @@ impl<R: RequestMessage> Response<R> {
#[derive(Clone)] #[derive(Clone)]
struct Session { struct Session {
zed_environment: Arc<str>,
user_id: UserId, user_id: UserId,
connection_id: ConnectionId, connection_id: ConnectionId,
db: Arc<tokio::sync::Mutex<DbHandle>>, db: Arc<tokio::sync::Mutex<DbHandle>>,
@ -216,40 +216,45 @@ impl Server {
.add_message_handler(update_language_server) .add_message_handler(update_language_server)
.add_message_handler(update_diagnostic_summary) .add_message_handler(update_diagnostic_summary)
.add_message_handler(update_worktree_settings) .add_message_handler(update_worktree_settings)
.add_message_handler(refresh_inlay_hints) .add_request_handler(forward_read_only_project_request::<proto::GetHover>)
.add_request_handler(forward_project_request::<proto::GetHover>) .add_request_handler(forward_read_only_project_request::<proto::GetDefinition>)
.add_request_handler(forward_project_request::<proto::GetDefinition>) .add_request_handler(forward_read_only_project_request::<proto::GetTypeDefinition>)
.add_request_handler(forward_project_request::<proto::GetTypeDefinition>) .add_request_handler(forward_read_only_project_request::<proto::GetReferences>)
.add_request_handler(forward_project_request::<proto::GetReferences>) .add_request_handler(forward_read_only_project_request::<proto::SearchProject>)
.add_request_handler(forward_project_request::<proto::SearchProject>) .add_request_handler(forward_read_only_project_request::<proto::GetDocumentHighlights>)
.add_request_handler(forward_project_request::<proto::GetDocumentHighlights>) .add_request_handler(forward_read_only_project_request::<proto::GetProjectSymbols>)
.add_request_handler(forward_project_request::<proto::GetProjectSymbols>) .add_request_handler(forward_read_only_project_request::<proto::OpenBufferForSymbol>)
.add_request_handler(forward_project_request::<proto::OpenBufferForSymbol>) .add_request_handler(forward_read_only_project_request::<proto::OpenBufferById>)
.add_request_handler(forward_project_request::<proto::OpenBufferById>) .add_request_handler(forward_read_only_project_request::<proto::SynchronizeBuffers>)
.add_request_handler(forward_project_request::<proto::OpenBufferByPath>) .add_request_handler(forward_read_only_project_request::<proto::InlayHints>)
.add_request_handler(forward_project_request::<proto::GetCompletions>) .add_request_handler(forward_read_only_project_request::<proto::OpenBufferByPath>)
.add_request_handler(forward_project_request::<proto::ApplyCompletionAdditionalEdits>) .add_request_handler(forward_mutating_project_request::<proto::GetCompletions>)
.add_request_handler(forward_project_request::<proto::ResolveCompletionDocumentation>) .add_request_handler(
.add_request_handler(forward_project_request::<proto::GetCodeActions>) forward_mutating_project_request::<proto::ApplyCompletionAdditionalEdits>,
.add_request_handler(forward_project_request::<proto::ApplyCodeAction>) )
.add_request_handler(forward_project_request::<proto::PrepareRename>) .add_request_handler(
.add_request_handler(forward_project_request::<proto::PerformRename>) forward_mutating_project_request::<proto::ResolveCompletionDocumentation>,
.add_request_handler(forward_project_request::<proto::ReloadBuffers>) )
.add_request_handler(forward_project_request::<proto::SynchronizeBuffers>) .add_request_handler(forward_mutating_project_request::<proto::GetCodeActions>)
.add_request_handler(forward_project_request::<proto::FormatBuffers>) .add_request_handler(forward_mutating_project_request::<proto::ApplyCodeAction>)
.add_request_handler(forward_project_request::<proto::CreateProjectEntry>) .add_request_handler(forward_mutating_project_request::<proto::PrepareRename>)
.add_request_handler(forward_project_request::<proto::RenameProjectEntry>) .add_request_handler(forward_mutating_project_request::<proto::PerformRename>)
.add_request_handler(forward_project_request::<proto::CopyProjectEntry>) .add_request_handler(forward_mutating_project_request::<proto::ReloadBuffers>)
.add_request_handler(forward_project_request::<proto::DeleteProjectEntry>) .add_request_handler(forward_mutating_project_request::<proto::FormatBuffers>)
.add_request_handler(forward_project_request::<proto::ExpandProjectEntry>) .add_request_handler(forward_mutating_project_request::<proto::CreateProjectEntry>)
.add_request_handler(forward_project_request::<proto::OnTypeFormatting>) .add_request_handler(forward_mutating_project_request::<proto::RenameProjectEntry>)
.add_request_handler(forward_project_request::<proto::InlayHints>) .add_request_handler(forward_mutating_project_request::<proto::CopyProjectEntry>)
.add_request_handler(forward_mutating_project_request::<proto::DeleteProjectEntry>)
.add_request_handler(forward_mutating_project_request::<proto::ExpandProjectEntry>)
.add_request_handler(forward_mutating_project_request::<proto::OnTypeFormatting>)
.add_request_handler(forward_mutating_project_request::<proto::SaveBuffer>)
.add_message_handler(create_buffer_for_peer) .add_message_handler(create_buffer_for_peer)
.add_request_handler(update_buffer) .add_request_handler(update_buffer)
.add_message_handler(update_buffer_file) .add_message_handler(broadcast_project_message_from_host::<proto::RefreshInlayHints>)
.add_message_handler(buffer_reloaded) .add_message_handler(broadcast_project_message_from_host::<proto::UpdateBufferFile>)
.add_message_handler(buffer_saved) .add_message_handler(broadcast_project_message_from_host::<proto::BufferReloaded>)
.add_request_handler(forward_project_request::<proto::SaveBuffer>) .add_message_handler(broadcast_project_message_from_host::<proto::BufferSaved>)
.add_message_handler(broadcast_project_message_from_host::<proto::UpdateDiffBase>)
.add_request_handler(get_users) .add_request_handler(get_users)
.add_request_handler(fuzzy_search_users) .add_request_handler(fuzzy_search_users)
.add_request_handler(request_contact) .add_request_handler(request_contact)
@ -281,7 +286,6 @@ impl Server {
.add_request_handler(follow) .add_request_handler(follow)
.add_message_handler(unfollow) .add_message_handler(unfollow)
.add_message_handler(update_followers) .add_message_handler(update_followers)
.add_message_handler(update_diff_base)
.add_request_handler(get_private_user_info) .add_request_handler(get_private_user_info)
.add_message_handler(acknowledge_channel_message) .add_message_handler(acknowledge_channel_message)
.add_message_handler(acknowledge_buffer_version); .add_message_handler(acknowledge_buffer_version);
@ -609,6 +613,7 @@ impl Server {
user_id, user_id,
connection_id, connection_id,
db: Arc::new(tokio::sync::Mutex::new(DbHandle(this.app_state.db.clone()))), 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(), peer: this.peer.clone(),
connection_pool: this.connection_pool.clone(), connection_pool: this.connection_pool.clone(),
live_kit_client: this.app_state.live_kit_client.clone(), live_kit_client: this.app_state.live_kit_client.clone(),
@ -965,7 +970,7 @@ async fn create_room(
session.user_id, session.user_id,
session.connection_id, session.connection_id,
&live_kit_room, &live_kit_room,
RELEASE_CHANNEL_NAME.as_str(), &session.zed_environment,
) )
.await?; .await?;
@ -999,7 +1004,7 @@ async fn join_room(
room_id, room_id,
session.user_id, session.user_id,
session.connection_id, session.connection_id,
RELEASE_CHANNEL_NAME.as_str(), session.zed_environment.as_ref(),
) )
.await?; .await?;
room_updated(&room.room, &session.peer); room_updated(&room.room, &session.peer);
@ -1693,10 +1698,6 @@ async fn update_worktree_settings(
Ok(()) 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( async fn start_language_server(
request: proto::StartLanguageServer, request: proto::StartLanguageServer,
session: Session, session: Session,
@ -1741,7 +1742,7 @@ async fn update_language_server(
Ok(()) Ok(())
} }
async fn forward_project_request<T>( async fn forward_read_only_project_request<T>(
request: T, request: T,
response: Response<T>, response: Response<T>,
session: Session, session: Session,
@ -1750,24 +1751,37 @@ where
T: EntityMessage + RequestMessage, T: EntityMessage + RequestMessage,
{ {
let project_id = ProjectId::from_proto(request.remote_entity_id()); let project_id = ProjectId::from_proto(request.remote_entity_id());
let host_connection_id = { let host_connection_id = session
let collaborators = session
.db() .db()
.await .await
.project_collaborators(project_id, session.connection_id) .host_for_read_only_project_request(project_id, session.connection_id)
.await?; .await?;
collaborators
.iter()
.find(|collaborator| collaborator.is_host)
.ok_or_else(|| anyhow!("host not found"))?
.connection_id
};
let payload = session let payload = session
.peer .peer
.forward_request(session.connection_id, host_connection_id, request) .forward_request(session.connection_id, host_connection_id, request)
.await?; .await?;
response.send(payload)?;
Ok(())
}
async fn forward_mutating_project_request<T>(
request: T,
response: Response<T>,
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)?; response.send(payload)?;
Ok(()) Ok(())
} }
@ -1776,6 +1790,14 @@ async fn create_buffer_for_peer(
request: proto::CreateBufferForPeer, request: proto::CreateBufferForPeer,
session: Session, session: Session,
) -> Result<()> { ) -> 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"))?; let peer_id = request.peer_id.ok_or_else(|| anyhow!("invalid peer id"))?;
session session
.peer .peer
@ -1791,11 +1813,12 @@ async fn update_buffer(
let project_id = ProjectId::from_proto(request.project_id); let project_id = ProjectId::from_proto(request.project_id);
let mut guest_connection_ids; let mut guest_connection_ids;
let mut host_connection_id = None; let mut host_connection_id = None;
{ {
let collaborators = session let collaborators = session
.db() .db()
.await .await
.project_collaborators(project_id, session.connection_id) .project_collaborators_for_buffer_update(project_id, session.connection_id)
.await?; .await?;
guest_connection_ids = Vec::with_capacity(collaborators.len() - 1); guest_connection_ids = Vec::with_capacity(collaborators.len() - 1);
for collaborator in collaborators.iter() { for collaborator in collaborators.iter() {
@ -1828,60 +1851,17 @@ async fn update_buffer(
Ok(()) Ok(())
} }
async fn update_buffer_file(request: proto::UpdateBufferFile, session: Session) -> Result<()> { async fn broadcast_project_message_from_host<T: EntityMessage<Entity = ShareProject>>(
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<T: EnvelopedMessage>(
project_id: u64,
request: T, request: T,
session: Session, session: Session,
) -> Result<()> { ) -> Result<()> {
let project_id = ProjectId::from_proto(project_id); let project_id = ProjectId::from_proto(request.remote_entity_id());
let project_connection_ids = session let project_connection_ids = session
.db() .db()
.await .await
.project_connection_ids(project_id, session.connection_id) .project_connection_ids(project_id, session.connection_id)
.await?; .await?;
broadcast( broadcast(
Some(session.connection_id), Some(session.connection_id),
project_connection_ids.iter().copied(), project_connection_ids.iter().copied(),
@ -2608,7 +2588,7 @@ async fn join_channel_internal(
channel_id, channel_id,
session.user_id, session.user_id,
session.connection_id, session.connection_id,
RELEASE_CHANNEL_NAME.as_str(), session.zed_environment.as_ref(),
) )
.await?; .await?;
@ -3110,25 +3090,6 @@ async fn mark_notification_as_read(
Ok(()) 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( async fn get_private_user_info(
_request: proto::GetPrivateUserInfo, _request: proto::GetPrivateUserInfo,
response: Response<proto::GetPrivateUserInfo>, response: Response<proto::GetPrivateUserInfo>,

View file

@ -82,5 +82,13 @@ async fn test_channel_guests(
project_b.read_with(cx_b, |project, _| project.remote_id()), project_b.read_with(cx_b, |project, _| project.remote_id()),
Some(project_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())
} }

View file

@ -4936,10 +4936,10 @@ async fn test_project_symbols(
.await .await
.unwrap(); .unwrap();
buffer_b_2.read_with(cx_b, |buffer, _| { buffer_b_2.read_with(cx_b, |buffer, cx| {
assert_eq!( assert_eq!(
buffer.file().unwrap().path().as_ref(), buffer.file().unwrap().full_path(cx),
Path::new("../crate-2/two.rs") Path::new("/code/crate-2/two.rs")
); );
}); });

View file

@ -2,7 +2,7 @@ use crate::{
db::{tests::TestDb, NewUserParams, UserId}, db::{tests::TestDb, NewUserParams, UserId},
executor::Executor, executor::Executor,
rpc::{Server, CLEANUP_TIMEOUT, RECONNECT_TIMEOUT}, rpc::{Server, CLEANUP_TIMEOUT, RECONNECT_TIMEOUT},
AppState, AppState, Config,
}; };
use anyhow::anyhow; use anyhow::anyhow;
use call::ActiveCall; use call::ActiveCall;
@ -414,7 +414,19 @@ impl TestServer {
Arc::new(AppState { Arc::new(AppState {
db: test_db.db().clone(), db: test_db.db().clone(),
live_kit_client: Some(Arc::new(fake_server.create_api_client())), 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(),
},
}) })
} }
} }

View file

@ -9,6 +9,8 @@ path = "src/collab_ui.rs"
doctest = false doctest = false
[features] [features]
default = []
stories = ["dep:story"]
test-support = [ test-support = [
"call/test-support", "call/test-support",
"client/test-support", "client/test-support",
@ -44,6 +46,7 @@ project = { path = "../project" }
recent_projects = { path = "../recent_projects" } recent_projects = { path = "../recent_projects" }
rpc = { path = "../rpc" } rpc = { path = "../rpc" }
settings = { path = "../settings" } settings = { path = "../settings" }
story = { path = "../story", optional = true }
feature_flags = { path = "../feature_flags"} feature_flags = { path = "../feature_flags"}
theme = { path = "../theme" } theme = { path = "../theme" }
theme_selector = { path = "../theme_selector" } theme_selector = { path = "../theme_selector" }

View file

@ -19,9 +19,8 @@ use rich_text::RichText;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsStore}; use settings::{Settings, SettingsStore};
use std::sync::Arc; use std::sync::Arc;
use theme::ActiveTheme as _;
use time::{OffsetDateTime, UtcOffset}; 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 util::{ResultExt, TryFutureExt};
use workspace::{ use workspace::{
dock::{DockPosition, Panel, PanelEvent}, dock::{DockPosition, Panel, PanelEvent},
@ -48,7 +47,7 @@ pub struct ChatPanel {
languages: Arc<LanguageRegistry>, languages: Arc<LanguageRegistry>,
message_list: ListState, message_list: ListState,
active_chat: Option<(Model<ChannelChat>, Subscription)>, active_chat: Option<(Model<ChannelChat>, Subscription)>,
input_editor: View<MessageEditor>, message_editor: View<MessageEditor>,
local_timezone: UtcOffset, local_timezone: UtcOffset,
fs: Arc<dyn Fs>, fs: Arc<dyn Fs>,
width: Option<Pixels>, width: Option<Pixels>,
@ -120,7 +119,7 @@ impl ChatPanel {
message_list, message_list,
active_chat: Default::default(), active_chat: Default::default(),
pending_serialization: Task::ready(None), pending_serialization: Task::ready(None),
input_editor, message_editor: input_editor,
local_timezone: cx.local_timezone(), local_timezone: cx.local_timezone(),
subscriptions: Vec::new(), subscriptions: Vec::new(),
workspace: workspace_handle, workspace: workspace_handle,
@ -209,7 +208,7 @@ impl ChatPanel {
self.message_list.reset(chat.message_count()); self.message_list.reset(chat.message_count());
let channel_name = chat.channel(cx).map(|channel| channel.name.clone()); 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); editor.set_channel(channel_id, channel_name, cx);
}); });
}; };
@ -282,12 +281,12 @@ impl ChatPanel {
)), )),
) )
.end_child( .end_child(
IconButton::new("notes", Icon::File) IconButton::new("notes", IconName::File)
.on_click(cx.listener(Self::open_notes)) .on_click(cx.listener(Self::open_notes))
.tooltip(|cx| Tooltip::text("Open notes", cx)), .tooltip(|cx| Tooltip::text("Open notes", cx)),
) )
.end_child( .end_child(
IconButton::new("call", Icon::AudioOn) IconButton::new("call", IconName::AudioOn)
.on_click(cx.listener(Self::join_call)) .on_click(cx.listener(Self::join_call))
.tooltip(|cx| Tooltip::text("Join call", cx)), .tooltip(|cx| Tooltip::text("Join call", cx)),
), ),
@ -300,13 +299,7 @@ impl ChatPanel {
this this
} }
})) }))
.child( .child(h_stack().p_2().child(self.message_editor.clone()))
div()
.z_index(1)
.p_2()
.bg(cx.theme().colors().background)
.child(self.input_editor.clone()),
)
.into_any() .into_any()
} }
@ -402,7 +395,7 @@ impl ChatPanel {
.w_8() .w_8()
.visible_on_hover("") .visible_on_hover("")
.children(message_id_to_remove.map(|message_id| { .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| { cx.listener(move |this, _, cx| {
this.remove_message(message_id, cx); this.remove_message(message_id, cx);
}), }),
@ -428,8 +421,17 @@ impl ChatPanel {
rich_text::render_markdown(message.body.clone(), &mentions, language_registry, None) rich_text::render_markdown(message.body.clone(), &mentions, language_registry, None)
} }
fn render_sign_in_prompt(&self, cx: &mut ViewContext<Self>) -> AnyElement { fn render_sign_in_prompt(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
Button::new("sign-in", "Sign in to use chat") 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| { .on_click(cx.listener(move |this, _, cx| {
let client = this.client.clone(); let client = this.client.clone();
cx.spawn(|this, mut cx| async move { cx.spawn(|this, mut cx| async move {
@ -446,14 +448,21 @@ impl ChatPanel {
} }
}) })
.detach(); .detach();
})) })),
.into_any_element() )
.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<Self>) { fn send(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
if let Some((chat, _)) = self.active_chat.as_ref() { if let Some((chat, _)) = self.active_chat.as_ref() {
let message = self let message = self
.input_editor .message_editor
.update(cx, |editor, cx| editor.take_message(cx)); .update(cx, |editor, cx| editor.take_message(cx));
if let Some(task) = chat if let Some(task) = chat
@ -550,12 +559,18 @@ impl EventEmitter<Event> for ChatPanel {}
impl Render for ChatPanel { impl Render for ChatPanel {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement { fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
div() v_stack()
.full() .size_full()
.child(if self.client.user_id().is_some() { .map(|this| match (self.client.user_id(), self.active_chat()) {
self.render_channel(cx) (Some(_), Some(_)) => this.child(self.render_channel(cx)),
} else { (Some(_), None) => this.child(
self.render_sign_in_prompt(cx) 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.)) .min_w(px(150.))
} }
@ -563,7 +578,7 @@ impl Render for ChatPanel {
impl FocusableView for ChatPanel { impl FocusableView for ChatPanel {
fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle { 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" "ChatPanel"
} }
fn icon(&self, cx: &WindowContext) -> Option<ui::Icon> { fn icon(&self, cx: &WindowContext) -> Option<ui::IconName> {
if !is_channels_feature_enabled(cx) { if !is_channels_feature_enabled(cx) {
return None; 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> { fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {

View file

@ -1,16 +1,19 @@
use std::{sync::Arc, time::Duration};
use channel::{ChannelId, ChannelMembership, ChannelStore, MessageParams}; use channel::{ChannelId, ChannelMembership, ChannelStore, MessageParams};
use client::UserId; use client::UserId;
use collections::HashMap; use collections::HashMap;
use editor::{AnchorRangeExt, Editor}; use editor::{AnchorRangeExt, Editor, EditorElement, EditorStyle};
use gpui::{ use gpui::{
AsyncWindowContext, FocusableView, IntoElement, Model, Render, SharedString, Task, View, AsyncWindowContext, FocusableView, FontStyle, FontWeight, HighlightStyle, IntoElement, Model,
ViewContext, WeakView, Render, SharedString, Task, TextStyle, View, ViewContext, WeakView, WhiteSpace,
}; };
use language::{language_settings::SoftWrap, Buffer, BufferSnapshot, LanguageRegistry}; use language::{language_settings::SoftWrap, Buffer, BufferSnapshot, LanguageRegistry};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use project::search::SearchQuery; use project::search::SearchQuery;
use std::{sync::Arc, time::Duration}; use settings::Settings;
use workspace::item::ItemHandle; use theme::ThemeSettings;
use ui::prelude::*;
const MENTIONS_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(50); const MENTIONS_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(50);
@ -181,7 +184,14 @@ impl MessageEditor {
} }
editor.clear_highlights::<Self>(cx); editor.clear_highlights::<Self>(cx);
editor.highlight_text::<Self>(anchor_ranges, gpui::red().into(), cx) editor.highlight_text::<Self>(
anchor_ranges,
HighlightStyle {
font_weight: Some(FontWeight::BOLD),
..Default::default()
},
cx,
)
}); });
this.mentions = mentioned_user_ids; this.mentions = mentioned_user_ids;
@ -196,8 +206,39 @@ impl MessageEditor {
} }
impl Render for MessageEditor { impl Render for MessageEditor {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement { fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
self.editor.to_any() 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 { mod tests {
use super::*; use super::*;
use client::{Client, User, UserStore}; use client::{Client, User, UserStore};
use gpui::{Context as _, TestAppContext, VisualContext as _}; use gpui::TestAppContext;
use language::{Language, LanguageConfig}; use language::{Language, LanguageConfig};
use rpc::proto; use rpc::proto;
use settings::SettingsStore; use settings::SettingsStore;

View file

@ -31,7 +31,7 @@ use smallvec::SmallVec;
use std::{mem, sync::Arc}; use std::{mem, sync::Arc};
use theme::{ActiveTheme, ThemeSettings}; use theme::{ActiveTheme, ThemeSettings};
use ui::{ 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, ListHeader, ListItem, Tooltip,
}; };
use util::{maybe, ResultExt, TryFutureExt}; use util::{maybe, ResultExt, TryFutureExt};
@ -848,7 +848,7 @@ impl CollabPanel {
.end_slot(if is_pending { .end_slot(if is_pending {
Label::new("Calling").color(Color::Muted).into_any_element() Label::new("Calling").color(Color::Muted).into_any_element()
} else if is_current_user { } else if is_current_user {
IconButton::new("leave-call", Icon::Exit) IconButton::new("leave-call", IconName::Exit)
.style(ButtonStyle::Subtle) .style(ButtonStyle::Subtle)
.on_click(move |_, cx| Self::leave_call(cx)) .on_click(move |_, cx| Self::leave_call(cx))
.tooltip(|cx| Tooltip::text("Leave Call", cx)) .tooltip(|cx| Tooltip::text("Leave Call", cx))
@ -896,8 +896,8 @@ impl CollabPanel {
.start_slot( .start_slot(
h_stack() h_stack()
.gap_1() .gap_1()
.child(render_tree_branch(is_last, cx)) .child(render_tree_branch(is_last, false, cx))
.child(IconButton::new(0, Icon::Folder)), .child(IconButton::new(0, IconName::Folder)),
) )
.child(Label::new(project_name.clone())) .child(Label::new(project_name.clone()))
.tooltip(move |cx| Tooltip::text(format!("Open {}", project_name), cx)) .tooltip(move |cx| Tooltip::text(format!("Open {}", project_name), cx))
@ -917,8 +917,8 @@ impl CollabPanel {
.start_slot( .start_slot(
h_stack() h_stack()
.gap_1() .gap_1()
.child(render_tree_branch(is_last, cx)) .child(render_tree_branch(is_last, false, cx))
.child(IconButton::new(0, Icon::Screen)), .child(IconButton::new(0, IconName::Screen)),
) )
.child(Label::new("Screen")) .child(Label::new("Screen"))
.when_some(peer_id, |this, _| { .when_some(peer_id, |this, _| {
@ -958,8 +958,8 @@ impl CollabPanel {
.start_slot( .start_slot(
h_stack() h_stack()
.gap_1() .gap_1()
.child(render_tree_branch(false, cx)) .child(render_tree_branch(false, true, cx))
.child(IconButton::new(0, Icon::File)), .child(IconButton::new(0, IconName::File)),
) )
.child(div().h_7().w_full().child(Label::new("notes"))) .child(div().h_7().w_full().child(Label::new("notes")))
.tooltip(move |cx| Tooltip::text("Open Channel Notes", cx)) .tooltip(move |cx| Tooltip::text("Open Channel Notes", cx))
@ -979,8 +979,8 @@ impl CollabPanel {
.start_slot( .start_slot(
h_stack() h_stack()
.gap_1() .gap_1()
.child(render_tree_branch(false, cx)) .child(render_tree_branch(false, false, cx))
.child(IconButton::new(0, Icon::MessageBubbles)), .child(IconButton::new(0, IconName::MessageBubbles)),
) )
.child(Label::new("chat")) .child(Label::new("chat"))
.tooltip(move |cx| Tooltip::text("Open Chat", cx)) .tooltip(move |cx| Tooltip::text("Open Chat", cx))
@ -1007,7 +1007,7 @@ impl CollabPanel {
.start_slot( .start_slot(
h_stack() h_stack()
.gap_1() .gap_1()
.child(render_tree_branch(!has_visible_participants, cx)) .child(render_tree_branch(!has_visible_participants, false, cx))
.child(""), .child(""),
) )
.child(Label::new(if count == 1 { .child(Label::new(if count == 1 {
@ -1724,7 +1724,7 @@ impl CollabPanel {
.child( .child(
Button::new("sign_in", "Sign in") Button::new("sign_in", "Sign in")
.icon_color(Color::Muted) .icon_color(Color::Muted)
.icon(Icon::Github) .icon(IconName::Github)
.icon_position(IconPosition::Start) .icon_position(IconPosition::Start)
.style(ButtonStyle::Filled) .style(ButtonStyle::Filled)
.full_width() .full_width()
@ -1921,7 +1921,7 @@ impl CollabPanel {
let button = match section { let button = match section {
Section::ActiveCall => channel_link.map(|channel_link| { Section::ActiveCall => channel_link.map(|channel_link| {
let channel_link_copy = channel_link.clone(); let channel_link_copy = channel_link.clone();
IconButton::new("channel-link", Icon::Copy) IconButton::new("channel-link", IconName::Copy)
.icon_size(IconSize::Small) .icon_size(IconSize::Small)
.size(ButtonSize::None) .size(ButtonSize::None)
.visible_on_hover("section-header") .visible_on_hover("section-header")
@ -1933,13 +1933,13 @@ impl CollabPanel {
.into_any_element() .into_any_element()
}), }),
Section::Contacts => Some( 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))) .on_click(cx.listener(|this, _, cx| this.toggle_contact_finder(cx)))
.tooltip(|cx| Tooltip::text("Search for new contact", cx)) .tooltip(|cx| Tooltip::text("Search for new contact", cx))
.into_any_element(), .into_any_element(),
), ),
Section::Channels => Some( 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))) .on_click(cx.listener(|this, _, cx| this.new_root_channel(cx)))
.tooltip(|cx| Tooltip::text("Create a channel", cx)) .tooltip(|cx| Tooltip::text("Create a channel", cx))
.into_any_element(), .into_any_element(),
@ -2010,7 +2010,7 @@ impl CollabPanel {
}) })
.when(!calling, |el| { .when(!calling, |el| {
el.child( el.child(
IconButton::new("remove_contact", Icon::Close) IconButton::new("remove_contact", IconName::Close)
.icon_color(Color::Muted) .icon_color(Color::Muted)
.visible_on_hover("") .visible_on_hover("")
.tooltip(|cx| Tooltip::text("Remove Contact", cx)) .tooltip(|cx| Tooltip::text("Remove Contact", cx))
@ -2071,13 +2071,13 @@ impl CollabPanel {
let controls = if is_incoming { let controls = if is_incoming {
vec![ vec![
IconButton::new("decline-contact", Icon::Close) IconButton::new("decline-contact", IconName::Close)
.on_click(cx.listener(move |this, _, cx| { .on_click(cx.listener(move |this, _, cx| {
this.respond_to_contact_request(user_id, false, cx); this.respond_to_contact_request(user_id, false, cx);
})) }))
.icon_color(color) .icon_color(color)
.tooltip(|cx| Tooltip::text("Decline invite", cx)), .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| { .on_click(cx.listener(move |this, _, cx| {
this.respond_to_contact_request(user_id, true, cx); this.respond_to_contact_request(user_id, true, cx);
})) }))
@ -2086,7 +2086,7 @@ impl CollabPanel {
] ]
} else { } else {
let github_login = github_login.clone(); 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| { .on_click(cx.listener(move |this, _, cx| {
this.remove_contact(user_id, &github_login, cx); this.remove_contact(user_id, &github_login, cx);
})) }))
@ -2126,13 +2126,13 @@ impl CollabPanel {
}; };
let controls = [ let controls = [
IconButton::new("reject-invite", Icon::Close) IconButton::new("reject-invite", IconName::Close)
.on_click(cx.listener(move |this, _, cx| { .on_click(cx.listener(move |this, _, cx| {
this.respond_to_channel_invite(channel_id, false, cx); this.respond_to_channel_invite(channel_id, false, cx);
})) }))
.icon_color(color) .icon_color(color)
.tooltip(|cx| Tooltip::text("Decline invite", cx)), .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| { .on_click(cx.listener(move |this, _, cx| {
this.respond_to_channel_invite(channel_id, true, cx); this.respond_to_channel_invite(channel_id, true, cx);
})) }))
@ -2150,7 +2150,7 @@ impl CollabPanel {
.child(h_stack().children(controls)), .child(h_stack().children(controls)),
) )
.start_slot( .start_slot(
IconElement::new(Icon::Hash) Icon::new(IconName::Hash)
.size(IconSize::Small) .size(IconSize::Small)
.color(Color::Muted), .color(Color::Muted),
) )
@ -2162,7 +2162,7 @@ impl CollabPanel {
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> ListItem { ) -> ListItem {
ListItem::new("contact-placeholder") ListItem::new("contact-placeholder")
.child(IconElement::new(Icon::Plus)) .child(Icon::new(IconName::Plus))
.child(Label::new("Add a Contact")) .child(Label::new("Add a Contact"))
.selected(is_selected) .selected(is_selected)
.on_click(cx.listener(|this, _, cx| this.toggle_contact_finder(cx))) .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()) .map(|user| Avatar::new(user.avatar_uri.clone()).into_any_element())
.take(FACEPILE_LIMIT) .take(FACEPILE_LIMIT)
.chain(if extra_count > 0 { .chain(if extra_count > 0 {
// todo!() @nate - this label looks wrong. Some(
Some(Label::new(format!("+{}", extra_count)).into_any_element()) div()
.ml_1()
.child(Label::new(format!("+{extra_count}")))
.into_any_element(),
)
} else { } else {
None None
}) })
@ -2242,7 +2246,7 @@ impl CollabPanel {
}; };
let messages_button = |cx: &mut ViewContext<Self>| { let messages_button = |cx: &mut ViewContext<Self>| {
IconButton::new("channel_chat", Icon::MessageBubbles) IconButton::new("channel_chat", IconName::MessageBubbles)
.icon_size(IconSize::Small) .icon_size(IconSize::Small)
.icon_color(if has_messages_notification { .icon_color(if has_messages_notification {
Color::Default Color::Default
@ -2254,7 +2258,7 @@ impl CollabPanel {
}; };
let notes_button = |cx: &mut ViewContext<Self>| { let notes_button = |cx: &mut ViewContext<Self>| {
IconButton::new("channel_notes", Icon::File) IconButton::new("channel_notes", IconName::File)
.icon_size(IconSize::Small) .icon_size(IconSize::Small)
.icon_color(if has_notes_notification { .icon_color(if has_notes_notification {
Color::Default Color::Default
@ -2311,7 +2315,11 @@ impl CollabPanel {
}, },
)) ))
.start_slot( .start_slot(
IconElement::new(if is_public { Icon::Public } else { Icon::Hash }) Icon::new(if is_public {
IconName::Public
} else {
IconName::Hash
})
.size(IconSize::Small) .size(IconSize::Small)
.color(Color::Muted), .color(Color::Muted),
) )
@ -2382,7 +2390,7 @@ impl CollabPanel {
.indent_level(depth + 1) .indent_level(depth + 1)
.indent_step_size(px(20.)) .indent_step_size(px(20.))
.start_slot( .start_slot(
IconElement::new(Icon::Hash) Icon::new(IconName::Hash)
.size(IconSize::Small) .size(IconSize::Small)
.color(Color::Muted), .color(Color::Muted),
); );
@ -2394,21 +2402,16 @@ impl CollabPanel {
{ {
item.child(Label::new(pending_name)) item.child(Label::new(pending_name))
} else { } else {
item.child( item.child(self.channel_name_editor.clone())
div()
.w_full()
.py_1() // todo!() @nate this is a px off at the default font size.
.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 rem_size = cx.rem_size();
let line_height = cx.text_style().line_height_in_pixels(rem_size); let line_height = cx.text_style().line_height_in_pixels(rem_size);
let width = rem_size * 1.5; let width = rem_size * 1.5;
let thickness = px(2.); let thickness = px(1.);
let color = cx.theme().colors().text; let color = cx.theme().colors().text;
canvas(move |bounds, cx| { 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, top),
point( point(
start_x + thickness, 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, color,
@ -2497,10 +2504,10 @@ impl Panel for CollabPanel {
cx.notify(); cx.notify();
} }
fn icon(&self, cx: &gpui::WindowContext) -> Option<ui::Icon> { fn icon(&self, cx: &gpui::WindowContext) -> Option<ui::IconName> {
CollaborationPanelSettings::get_global(cx) CollaborationPanelSettings::get_global(cx)
.button .button
.then(|| ui::Icon::Collab) .then(|| ui::IconName::Collab)
} }
fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> { fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
@ -2643,11 +2650,11 @@ impl Render for DraggedChannelView {
.p_1() .p_1()
.gap_1() .gap_1()
.child( .child(
IconElement::new( Icon::new(
if self.channel.visibility == proto::ChannelVisibility::Public { if self.channel.visibility == proto::ChannelVisibility::Public {
Icon::Public IconName::Public
} else { } else {
Icon::Hash IconName::Hash
}, },
) )
.size(IconSize::Small) .size(IconSize::Small)

View file

@ -168,7 +168,7 @@ impl Render for ChannelModal {
.w_px() .w_px()
.flex_1() .flex_1()
.gap_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(Label::new(channel_name)),
) )
.child( .child(
@ -406,7 +406,7 @@ impl PickerDelegate for ChannelModalDelegate {
Some(ChannelRole::Guest) => Some(Label::new("Guest")), Some(ChannelRole::Guest) => Some(Label::new("Guest")),
_ => None, _ => None,
}) })
.child(IconButton::new("ellipsis", Icon::Ellipsis)) .child(IconButton::new("ellipsis", IconName::Ellipsis))
.children( .children(
if let (Some((menu, _)), true) = (&self.context_menu, selected) { if let (Some((menu, _)), true) = (&self.context_menu, selected) {
Some( Some(

View file

@ -155,9 +155,7 @@ impl PickerDelegate for ContactFinderDelegate {
.selected(selected) .selected(selected)
.start_slot(Avatar::new(user.avatar_uri.clone())) .start_slot(Avatar::new(user.avatar_uri.clone()))
.child(Label::new(user.github_login.clone())) .child(Label::new(user.github_login.clone()))
.end_slot::<IconElement>( .end_slot::<Icon>(icon_path.map(|icon_path| Icon::from_path(icon_path))),
icon_path.map(|icon_path| IconElement::from_path(icon_path)),
),
) )
} }
} }

View file

@ -15,7 +15,7 @@ use std::sync::Arc;
use theme::{ActiveTheme, PlayerColors}; use theme::{ActiveTheme, PlayerColors};
use ui::{ use ui::{
h_stack, popover_menu, prelude::*, Avatar, Button, ButtonLike, ButtonStyle, ContextMenu, Icon, h_stack, popover_menu, prelude::*, Avatar, Button, ButtonLike, ButtonStyle, ContextMenu, Icon,
IconButton, IconElement, TintColor, Tooltip, IconButton, IconName, TintColor, Tooltip,
}; };
use util::ResultExt; use util::ResultExt;
use vcs_menu::{build_branch_list, BranchList, OpenRecent as ToggleVcsMenu}; use vcs_menu::{build_branch_list, BranchList, OpenRecent as ToggleVcsMenu};
@ -213,7 +213,7 @@ impl Render for CollabTitlebarItem {
.child( .child(
div() div()
.child( .child(
IconButton::new("leave-call", ui::Icon::Exit) IconButton::new("leave-call", ui::IconName::Exit)
.style(ButtonStyle::Subtle) .style(ButtonStyle::Subtle)
.tooltip(|cx| Tooltip::text("Leave call", cx)) .tooltip(|cx| Tooltip::text("Leave call", cx))
.icon_size(IconSize::Small) .icon_size(IconSize::Small)
@ -230,9 +230,9 @@ impl Render for CollabTitlebarItem {
IconButton::new( IconButton::new(
"mute-microphone", "mute-microphone",
if is_muted { if is_muted {
ui::Icon::MicMute ui::IconName::MicMute
} else { } else {
ui::Icon::Mic ui::IconName::Mic
}, },
) )
.tooltip(move |cx| { .tooltip(move |cx| {
@ -256,9 +256,9 @@ impl Render for CollabTitlebarItem {
IconButton::new( IconButton::new(
"mute-sound", "mute-sound",
if is_deafened { if is_deafened {
ui::Icon::AudioOff ui::IconName::AudioOff
} else { } else {
ui::Icon::AudioOn ui::IconName::AudioOn
}, },
) )
.style(ButtonStyle::Subtle) .style(ButtonStyle::Subtle)
@ -281,7 +281,7 @@ impl Render for CollabTitlebarItem {
) )
.when(!read_only, |this| { .when(!read_only, |this| {
this.child( this.child(
IconButton::new("screen-share", ui::Icon::Screen) IconButton::new("screen-share", ui::IconName::Screen)
.style(ButtonStyle::Subtle) .style(ButtonStyle::Subtle)
.icon_size(IconSize::Small) .icon_size(IconSize::Small)
.selected(is_screen_sharing) .selected(is_screen_sharing)
@ -573,7 +573,7 @@ impl CollabTitlebarItem {
| client::Status::ReconnectionError { .. } => Some( | client::Status::ReconnectionError { .. } => Some(
div() div()
.id("disconnected") .id("disconnected")
.child(IconElement::new(Icon::Disconnected).size(IconSize::Small)) .child(Icon::new(IconName::Disconnected).size(IconSize::Small))
.tooltip(|cx| Tooltip::text("Disconnected", cx)) .tooltip(|cx| Tooltip::text("Disconnected", cx))
.into_any_element(), .into_any_element(),
), ),
@ -643,7 +643,7 @@ impl CollabTitlebarItem {
h_stack() h_stack()
.gap_0p5() .gap_0p5()
.child(Avatar::new(user.avatar_uri.clone())) .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) .style(ButtonStyle::Subtle)
.tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)), .tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)),
@ -665,7 +665,7 @@ impl CollabTitlebarItem {
.child( .child(
h_stack() h_stack()
.gap_0p5() .gap_0p5()
.child(IconElement::new(Icon::ChevronDown).color(Color::Muted)), .child(Icon::new(IconName::ChevronDown).color(Color::Muted)),
) )
.style(ButtonStyle::Subtle) .style(ButtonStyle::Subtle)
.tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)), .tooltip(move |cx| Tooltip::text("Toggle User Menu", cx)),

View file

@ -19,7 +19,7 @@ use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsStore}; use settings::{Settings, SettingsStore};
use std::{sync::Arc, time::Duration}; use std::{sync::Arc, time::Duration};
use time::{OffsetDateTime, UtcOffset}; 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 util::{ResultExt, TryFutureExt};
use workspace::{ use workspace::{
dock::{DockPosition, Panel, PanelEvent}, dock::{DockPosition, Panel, PanelEvent},
@ -553,7 +553,7 @@ impl Render for NotificationPanel {
.border_b_1() .border_b_1()
.border_color(cx.theme().colors().border) .border_color(cx.theme().colors().border)
.child(Label::new("Notifications")) .child(Label::new("Notifications"))
.child(IconElement::new(Icon::Envelope)), .child(Icon::new(IconName::Envelope)),
) )
.map(|this| { .map(|this| {
if self.client.user_id().is_none() { if self.client.user_id().is_none() {
@ -564,7 +564,7 @@ impl Render for NotificationPanel {
.child( .child(
Button::new("sign_in_prompt_button", "Sign in") Button::new("sign_in_prompt_button", "Sign in")
.icon_color(Color::Muted) .icon_color(Color::Muted)
.icon(Icon::Github) .icon(IconName::Github)
.icon_position(IconPosition::Start) .icon_position(IconPosition::Start)
.style(ButtonStyle::Filled) .style(ButtonStyle::Filled)
.full_width() .full_width()
@ -655,10 +655,10 @@ impl Panel for NotificationPanel {
} }
} }
fn icon(&self, cx: &gpui::WindowContext) -> Option<Icon> { fn icon(&self, cx: &gpui::WindowContext) -> Option<IconName> {
(NotificationPanelSettings::get_global(cx).button (NotificationPanelSettings::get_global(cx).button
&& self.notification_store.read(cx).notification_count() > 0) && self.notification_store.read(cx).notification_count() > 0)
.then(|| Icon::Bell) .then(|| IconName::Bell)
} }
fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> { 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()))) .children(user.map(|user| Avatar::new(user.avatar_uri.clone())))
.child(Label::new(self.text.clone())) .child(Label::new(self.text.clone()))
.child( .child(
IconButton::new("close", Icon::Close) IconButton::new("close", IconName::Close)
.on_click(cx.listener(|_, _, cx| cx.emit(DismissEvent))), .on_click(cx.listener(|_, _, cx| cx.emit(DismissEvent))),
) )
.on_click(cx.listener(|this, _, cx| { .on_click(cx.listener(|this, _, cx| {

View file

@ -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 gpui::AppContext;
use std::sync::Arc; use std::sync::Arc;
use workspace::AppState; use workspace::AppState;
pub mod incoming_call_notification; #[cfg(feature = "stories")]
pub mod project_shared_notification; pub use stories::*;
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) { pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
incoming_call_notification::init(app_state, cx); incoming_call_notification::init(app_state, cx);

View file

@ -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<SharedUrl>,
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),
)
}
}

View file

@ -1,15 +1,12 @@
use crate::notification_window_options; use crate::notification_window_options;
use crate::notifications::collab_notification::CollabNotification;
use call::{ActiveCall, IncomingCall}; use call::{ActiveCall, IncomingCall};
use futures::StreamExt; use futures::StreamExt;
use gpui::{ use gpui::{prelude::*, AppContext, WindowHandle};
img, px, AppContext, ParentElement, Render, RenderOnce, Styled, ViewContext,
VisualContext as _, WindowHandle,
};
use settings::Settings; use settings::Settings;
use std::sync::{Arc, Weak}; use std::sync::{Arc, Weak};
use theme::ThemeSettings; use theme::ThemeSettings;
use ui::prelude::*; use ui::{prelude::*, Button, Label};
use ui::{h_stack, v_stack, Button, Label};
use util::ResultExt; use util::ResultExt;
use workspace::AppState; use workspace::AppState;
@ -31,8 +28,8 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
if let Some(incoming_call) = incoming_call { if let Some(incoming_call) = incoming_call {
let unique_screens = cx.update(|cx| cx.displays()).unwrap(); let unique_screens = cx.update(|cx| cx.displays()).unwrap();
let window_size = gpui::Size { let window_size = gpui::Size {
width: px(380.), width: px(400.),
height: px(64.), height: px(72.),
}; };
for screen in unique_screens { for screen in unique_screens {
@ -129,35 +126,22 @@ impl Render for IncomingCallNotification {
cx.set_rem_size(ui_font_size); cx.set_rem_size(ui_font_size);
h_stack() div().size_full().font(ui_font).child(
.font(ui_font) CollabNotification::new(
.text_ui() self.state.call.calling_user.avatar_uri.clone(),
.justify_between() Button::new("accept", "Accept").on_click({
.size_full() let state = self.state.clone();
.overflow_hidden() move |_, cx| state.respond(true, cx)
.elevation_3(cx) }),
.p_2() Button::new("decline", "Decline").on_click({
.gap_2() let state = self.state.clone();
.child( move |_, cx| state.respond(false, cx)
img(self.state.call.calling_user.avatar_uri.clone()) }),
.w_12()
.h_12()
.rounded_full(),
) )
.child(v_stack().overflow_hidden().child(Label::new(format!( .child(v_stack().overflow_hidden().child(Label::new(format!(
"{} is sharing a project in Zed", "{} is sharing a project in Zed",
self.state.call.calling_user.github_login 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)
})),
) )
} }
} }

View file

@ -1,12 +1,13 @@
use crate::notification_window_options; use crate::notification_window_options;
use crate::notifications::collab_notification::CollabNotification;
use call::{room, ActiveCall}; use call::{room, ActiveCall};
use client::User; use client::User;
use collections::HashMap; use collections::HashMap;
use gpui::{img, px, AppContext, ParentElement, Render, Size, Styled, ViewContext, VisualContext}; use gpui::{AppContext, Size};
use settings::Settings; use settings::Settings;
use std::sync::{Arc, Weak}; use std::sync::{Arc, Weak};
use theme::ThemeSettings; use theme::ThemeSettings;
use ui::{h_stack, prelude::*, v_stack, Button, Label}; use ui::{prelude::*, Button, Label};
use workspace::AppState; use workspace::AppState;
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) { pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
@ -130,24 +131,16 @@ impl Render for ProjectSharedNotification {
cx.set_rem_size(ui_font_size); cx.set_rem_size(ui_font_size);
h_stack() div().size_full().font(ui_font).child(
.font(ui_font) CollabNotification::new(
.text_ui() self.owner.avatar_uri.clone(),
.justify_between() Button::new("open", "Open").on_click(cx.listener(move |this, _event, cx| {
.size_full() this.join(cx);
.overflow_hidden() })),
.elevation_3(cx) Button::new("dismiss", "Dismiss").on_click(cx.listener(move |this, _event, cx| {
.p_2() this.dismiss(cx);
.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(self.owner.github_login.clone()))
.child(Label::new(format!( .child(Label::new(format!(
"is sharing a project in Zed{}", "is sharing a project in Zed{}",
@ -163,18 +156,5 @@ impl Render for ProjectSharedNotification {
Some(Label::new(self.worktree_root_names.join(", "))) 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);
},
))),
)
} }
} }

View file

@ -0,0 +1,3 @@
mod collab_notification;
pub use collab_notification::*;

View file

@ -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<Self>) -> 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")),
),
)),
)
}
}

View file

@ -28,8 +28,17 @@ pub struct NotificationPanelSettings {
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
pub struct PanelSettingsContent { pub struct PanelSettingsContent {
/// Whether to show the panel button in the status bar.
///
/// Default: true
pub button: Option<bool>, pub button: Option<bool>,
/// Where to dock the panel.
///
/// Default: left
pub dock: Option<DockPosition>, pub dock: Option<DockPosition>,
/// Default width of the panel in pixels.
///
/// Default: 240
pub default_width: Option<f32>, pub default_width: Option<f32>,
} }

View file

@ -28,7 +28,6 @@ theme = { path = "../theme" }
lsp = { path = "../lsp" } lsp = { path = "../lsp" }
node_runtime = { path = "../node_runtime"} node_runtime = { path = "../node_runtime"}
util = { path = "../util" } util = { path = "../util" }
ui = { path = "../ui" }
async-compression.workspace = true async-compression.workspace = true
async-tar = "0.4.2" async-tar = "0.4.2"
anyhow.workspace = true anyhow.workspace = true

View file

@ -1,6 +1,4 @@
pub mod request; pub mod request;
mod sign_in;
use anyhow::{anyhow, Context as _, Result}; use anyhow::{anyhow, Context as _, Result};
use async_compression::futures::bufread::GzipDecoder; use async_compression::futures::bufread::GzipDecoder;
use async_tar::Archive; use async_tar::Archive;
@ -98,7 +96,6 @@ pub fn init(
}) })
.detach(); .detach();
sign_in::init(cx);
cx.on_action(|_: &SignIn, cx| { cx.on_action(|_: &SignIn, cx| {
if let Some(copilot) = Copilot::global(cx) { if let Some(copilot) = Copilot::global(cx) {
copilot copilot

View file

@ -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<WindowHandle<CopilotCodeVerification>> = 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<CopilotCodeVerification> {
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>) {
self.status = status;
cx.notify();
}
fn render_device_code(
data: &PromptUserDeviceFlow,
cx: &mut ViewContext<Self>,
) -> 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<Self>,
) -> 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<Self>) -> 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)
}
}

View file

@ -1,11 +1,11 @@
[package] [package]
name = "copilot_button" name = "copilot_ui"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
publish = false publish = false
[lib] [lib]
path = "src/copilot_button.rs" path = "src/copilot_ui.rs"
doctest = false doctest = false
[dependencies] [dependencies]
@ -17,6 +17,7 @@ gpui = { path = "../gpui" }
language = { path = "../language" } language = { path = "../language" }
settings = { path = "../settings" } settings = { path = "../settings" }
theme = { path = "../theme" } theme = { path = "../theme" }
ui = { path = "../ui" }
util = { path = "../util" } util = { path = "../util" }
workspace = {path = "../workspace" } workspace = {path = "../workspace" }
anyhow.workspace = true anyhow.workspace = true

View file

@ -1,3 +1,4 @@
use crate::sign_in::CopilotCodeVerification;
use anyhow::Result; use anyhow::Result;
use copilot::{Copilot, SignOut, Status}; use copilot::{Copilot, SignOut, Status};
use editor::{scroll::autoscroll::Autoscroll, Editor}; use editor::{scroll::autoscroll::Autoscroll, Editor};
@ -16,7 +17,9 @@ use util::{paths, ResultExt};
use workspace::{ use workspace::{
create_and_open_local_file, create_and_open_local_file,
item::ItemHandle, 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, StatusItemView, Toast, Workspace,
}; };
use zed_actions::OpenBrowser; use zed_actions::OpenBrowser;
@ -50,15 +53,15 @@ impl Render for CopilotButton {
.unwrap_or_else(|| all_language_settings.copilot_enabled(None, None)); .unwrap_or_else(|| all_language_settings.copilot_enabled(None, None));
let icon = match status { let icon = match status {
Status::Error(_) => Icon::CopilotError, Status::Error(_) => IconName::CopilotError,
Status::Authorized => { Status::Authorized => {
if enabled { if enabled {
Icon::Copilot IconName::Copilot
} else { } else {
Icon::CopilotDisabled IconName::CopilotDisabled
} }
} }
_ => Icon::CopilotInit, _ => IconName::CopilotInit,
}; };
if let Status::Error(e) = status { if let Status::Error(e) = status {
@ -331,7 +334,9 @@ fn initiate_sign_in(cx: &mut WindowContext) {
return; return;
}; };
let status = copilot.read(cx).status(); let status = copilot.read(cx).status();
let Some(workspace) = cx.window_handle().downcast::<Workspace>() else {
return;
};
match status { match status {
Status::Starting { task } => { Status::Starting { task } => {
let Some(workspace) = cx.window_handle().downcast::<Workspace>() else { let Some(workspace) = cx.window_handle().downcast::<Workspace>() else {
@ -370,9 +375,12 @@ fn initiate_sign_in(cx: &mut WindowContext) {
.detach(); .detach();
} }
_ => { _ => {
copilot copilot.update(cx, |this, cx| this.sign_in(cx)).detach();
.update(cx, |copilot, cx| copilot.sign_in(cx)) workspace
.detach_and_log_err(cx); .update(cx, |this, cx| {
this.toggle_modal(cx, |cx| CopilotCodeVerification::new(&copilot, cx));
})
.ok();
} }
} }
} }

View file

@ -0,0 +1,5 @@
mod copilot_button;
mod sign_in;
pub use copilot_button::*;
pub use sign_in::*;

View file

@ -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<DismissEvent> for CopilotCodeVerification {}
impl ModalView for CopilotCodeVerification {}
impl CopilotCodeVerification {
pub(crate) fn new(copilot: &Model<Copilot>, cx: &mut ViewContext<Self>) -> 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>) {
self.status = status;
cx.notify();
}
fn render_device_code(
data: &PromptUserDeviceFlow,
cx: &mut ViewContext<Self>,
) -> 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<Self>,
) -> 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<Self>) -> 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<Self>) -> 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)
}
}

View file

@ -36,7 +36,7 @@ use std::{
}; };
use theme::ActiveTheme; use theme::ActiveTheme;
pub use toolbar_controls::ToolbarControls; 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 util::TryFutureExt;
use workspace::{ use workspace::{
item::{BreadcrumbText, Item, ItemEvent, ItemHandle}, item::{BreadcrumbText, Item, ItemEvent, ItemHandle},
@ -660,7 +660,7 @@ impl Item for ProjectDiagnosticsEditor {
then.child( then.child(
h_stack() h_stack()
.gap_1() .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( .child(Label::new(self.summary.error_count.to_string()).color(
if selected { if selected {
Color::Default Color::Default
@ -674,9 +674,7 @@ impl Item for ProjectDiagnosticsEditor {
then.child( then.child(
h_stack() h_stack()
.gap_1() .gap_1()
.child( .child(Icon::new(IconName::ExclamationTriangle).color(Color::Warning))
IconElement::new(Icon::ExclamationTriangle).color(Color::Warning),
)
.child(Label::new(self.summary.warning_count.to_string()).color( .child(Label::new(self.summary.warning_count.to_string()).color(
if selected { if selected {
Color::Default Color::Default
@ -816,10 +814,10 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
.flex_none() .flex_none()
.map(|icon| { .map(|icon| {
if diagnostic.severity == DiagnosticSeverity::ERROR { if diagnostic.severity == DiagnosticSeverity::ERROR {
icon.path(Icon::XCircle.path()) icon.path(IconName::XCircle.path())
.text_color(Color::Error.color(cx)) .text_color(Color::Error.color(cx))
} else { } else {
icon.path(Icon::ExclamationTriangle.path()) icon.path(IconName::ExclamationTriangle.path())
.text_color(Color::Warning.color(cx)) .text_color(Color::Warning.color(cx))
} }
}), }),

View file

@ -6,7 +6,7 @@ use gpui::{
}; };
use language::Diagnostic; use language::Diagnostic;
use lsp::LanguageServerId; 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 workspace::{item::ItemHandle, StatusItemView, ToolbarItemEvent, Workspace};
use crate::{Deploy, ProjectDiagnosticsEditor}; use crate::{Deploy, ProjectDiagnosticsEditor};
@ -24,24 +24,16 @@ impl Render for DiagnosticIndicator {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement { fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let diagnostic_indicator = match (self.summary.error_count, self.summary.warning_count) { let diagnostic_indicator = match (self.summary.error_count, self.summary.warning_count) {
(0, 0) => h_stack().map(|this| { (0, 0) => h_stack().map(|this| {
if !self.in_progress_checks.is_empty() {
this.child( this.child(
IconElement::new(Icon::ArrowCircle) Icon::new(IconName::Check)
.size(IconSize::Small)
.color(Color::Muted),
)
} else {
this.child(
IconElement::new(Icon::Check)
.size(IconSize::Small) .size(IconSize::Small)
.color(Color::Default), .color(Color::Default),
) )
}
}), }),
(0, warning_count) => h_stack() (0, warning_count) => h_stack()
.gap_1() .gap_1()
.child( .child(
IconElement::new(Icon::ExclamationTriangle) Icon::new(IconName::ExclamationTriangle)
.size(IconSize::Small) .size(IconSize::Small)
.color(Color::Warning), .color(Color::Warning),
) )
@ -49,7 +41,7 @@ impl Render for DiagnosticIndicator {
(error_count, 0) => h_stack() (error_count, 0) => h_stack()
.gap_1() .gap_1()
.child( .child(
IconElement::new(Icon::XCircle) Icon::new(IconName::XCircle)
.size(IconSize::Small) .size(IconSize::Small)
.color(Color::Error), .color(Color::Error),
) )
@ -57,13 +49,13 @@ impl Render for DiagnosticIndicator {
(error_count, warning_count) => h_stack() (error_count, warning_count) => h_stack()
.gap_1() .gap_1()
.child( .child(
IconElement::new(Icon::XCircle) Icon::new(IconName::XCircle)
.size(IconSize::Small) .size(IconSize::Small)
.color(Color::Error), .color(Color::Error),
) )
.child(Label::new(error_count.to_string()).size(LabelSize::Small)) .child(Label::new(error_count.to_string()).size(LabelSize::Small))
.child( .child(
IconElement::new(Icon::ExclamationTriangle) Icon::new(IconName::ExclamationTriangle)
.size(IconSize::Small) .size(IconSize::Small)
.color(Color::Warning), .color(Color::Warning),
) )
@ -72,9 +64,14 @@ impl Render for DiagnosticIndicator {
let status = if !self.in_progress_checks.is_empty() { let status = if !self.in_progress_checks.is_empty() {
Some( Some(
h_stack()
.gap_2()
.child(Icon::new(IconName::ArrowCircle).size(IconSize::Small))
.child(
Label::new("Checking…") Label::new("Checking…")
.size(LabelSize::Small) .size(LabelSize::Small)
.color(Color::Muted) .into_any_element(),
)
.into_any_element(), .into_any_element(),
) )
} else if let Some(diagnostic) = &self.current_diagnostic { } else if let Some(diagnostic) = &self.current_diagnostic {

View file

@ -6,8 +6,12 @@ pub struct ProjectDiagnosticsSettings {
pub include_warnings: bool, pub include_warnings: bool,
} }
/// Diagnostics configuration.
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
pub struct ProjectDiagnosticsSettingsContent { pub struct ProjectDiagnosticsSettingsContent {
/// Whether to show warnings or not by default.
///
/// Default: true
include_warnings: Option<bool>, include_warnings: Option<bool>,
} }

View file

@ -1,7 +1,7 @@
use crate::ProjectDiagnosticsEditor; use crate::ProjectDiagnosticsEditor;
use gpui::{div, EventEmitter, ParentElement, Render, ViewContext, WeakView}; use gpui::{div, EventEmitter, ParentElement, Render, ViewContext, WeakView};
use ui::prelude::*; use ui::prelude::*;
use ui::{Icon, IconButton, Tooltip}; use ui::{IconButton, IconName, Tooltip};
use workspace::{item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView}; use workspace::{item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
pub struct ToolbarControls { pub struct ToolbarControls {
@ -24,7 +24,7 @@ impl Render for ToolbarControls {
}; };
div().child( div().child(
IconButton::new("toggle-warnings", Icon::ExclamationTriangle) IconButton::new("toggle-warnings", IconName::ExclamationTriangle)
.tooltip(move |cx| Tooltip::text(tooltip, cx)) .tooltip(move |cx| Tooltip::text(tooltip, cx))
.on_click(cx.listener(|this, _, cx| { .on_click(cx.listener(|this, _, cx| {
if let Some(editor) = this.editor.as_ref().and_then(|editor| editor.upgrade()) { if let Some(editor) = this.editor.as_ref().and_then(|editor| editor.upgrade()) {

View file

@ -99,8 +99,8 @@ use sum_tree::TreeMap;
use text::{OffsetUtf16, Rope}; use text::{OffsetUtf16, Rope};
use theme::{ActiveTheme, PlayerColor, StatusColors, SyntaxTheme, ThemeColors, ThemeSettings}; use theme::{ActiveTheme, PlayerColor, StatusColors, SyntaxTheme, ThemeColors, ThemeSettings};
use ui::{ use ui::{
h_stack, prelude::*, ButtonSize, ButtonStyle, Icon, IconButton, IconSize, ListItem, Popover, h_stack, prelude::*, ButtonSize, ButtonStyle, IconButton, IconName, IconSize, ListItem,
Tooltip, Popover, Tooltip,
}; };
use util::{post_inc, RangeExt, ResultExt, TryFutureExt}; use util::{post_inc, RangeExt, ResultExt, TryFutureExt};
use workspace::{searchable::SearchEvent, ItemNavHistory, Pane, SplitDirection, ViewId, Workspace}; use workspace::{searchable::SearchEvent, ItemNavHistory, Pane, SplitDirection, ViewId, Workspace};
@ -4223,7 +4223,7 @@ impl Editor {
) -> Option<IconButton> { ) -> Option<IconButton> {
if self.available_code_actions.is_some() { if self.available_code_actions.is_some() {
Some( Some(
IconButton::new("code_actions_indicator", ui::Icon::Bolt) IconButton::new("code_actions_indicator", ui::IconName::Bolt)
.icon_size(IconSize::Small) .icon_size(IconSize::Small)
.icon_color(Color::Muted) .icon_color(Color::Muted)
.selected(is_active) .selected(is_active)
@ -4257,7 +4257,7 @@ impl Editor {
fold_data fold_data
.map(|(fold_status, buffer_row, active)| { .map(|(fold_status, buffer_row, active)| {
(active || gutter_hovered || fold_status == FoldStatus::Folded).then(|| { (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 { .on_click(cx.listener(move |editor, _e, cx| match fold_status {
FoldStatus::Folded => { FoldStatus::Folded => {
editor.unfold_at(&UnfoldAt { buffer_row }, cx); editor.unfold_at(&UnfoldAt { buffer_row }, cx);
@ -4269,7 +4269,7 @@ impl Editor {
.icon_color(ui::Color::Muted) .icon_color(ui::Color::Muted)
.icon_size(ui::IconSize::Small) .icon_size(ui::IconSize::Small)
.selected(fold_status == FoldStatus::Folded) .selected(fold_status == FoldStatus::Folded)
.selected_icon(ui::Icon::ChevronRight) .selected_icon(ui::IconName::ChevronRight)
.size(ui::ButtonSize::None) .size(ui::ButtonSize::None)
}) })
}) })
@ -9739,7 +9739,7 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, _is_valid: bool) -> Ren
), ),
) )
.child( .child(
IconButton::new(("copy-block", cx.block_id), Icon::Copy) IconButton::new(("copy-block", cx.block_id), IconName::Copy)
.icon_color(Color::Muted) .icon_color(Color::Muted)
.size(ButtonSize::Compact) .size(ButtonSize::Compact)
.style(ButtonStyle::Transparent) .style(ButtonStyle::Transparent)

View file

@ -14,11 +14,15 @@ pub struct EditorSettings {
pub seed_search_query_from_cursor: SeedQuerySetting, 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)] #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum SeedQuerySetting { pub enum SeedQuerySetting {
/// Always populate the search query with the word under the cursor.
Always, Always,
/// Only populate the search query when there is text selected.
Selection, Selection,
/// Never populate the search query
Never, Never,
} }
@ -29,31 +33,75 @@ pub struct Scrollbar {
pub selections: bool, pub selections: bool,
} }
/// When to show the scrollbar in the editor.
///
/// Default: auto
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum ShowScrollbar { pub enum ShowScrollbar {
/// Show the scrollbar if there's important information or
/// follow the system's configured behavior.
Auto, Auto,
/// Match the system's configured behavior.
System, System,
/// Always show the scrollbar.
Always, Always,
/// Never show the scrollbar.
Never, Never,
} }
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] #[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
pub struct EditorSettingsContent { pub struct EditorSettingsContent {
/// Whether the cursor blinks in the editor.
///
/// Default: true
pub cursor_blink: Option<bool>, pub cursor_blink: Option<bool>,
/// Whether to show the informational hover box when moving the mouse
/// over symbols in the editor.
///
/// Default: true
pub hover_popover_enabled: Option<bool>, pub hover_popover_enabled: Option<bool>,
/// Whether to pop the completions menu while typing in an editor without
/// explicitly requesting it.
///
/// Default: true
pub show_completions_on_input: Option<bool>, pub show_completions_on_input: Option<bool>,
/// Whether to display inline and alongside documentation for items in the
/// completions menu.
///
/// Default: true
pub show_completion_documentation: Option<bool>, pub show_completion_documentation: Option<bool>,
/// 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<bool>, pub use_on_type_format: Option<bool>,
/// Scrollbar related settings
pub scrollbar: Option<ScrollbarContent>, pub scrollbar: Option<ScrollbarContent>,
/// Whether the line numbers on editors gutter are relative or not.
///
/// Default: false
pub relative_line_numbers: Option<bool>, pub relative_line_numbers: Option<bool>,
/// When to populate a new search's query based on the text under the cursor.
///
/// Default: always
pub seed_search_query_from_cursor: Option<SeedQuerySetting>, pub seed_search_query_from_cursor: Option<SeedQuerySetting>,
} }
/// Scrollbar related settings
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
pub struct ScrollbarContent { pub struct ScrollbarContent {
/// When to show the scrollbar in the editor.
///
/// Default: auto
pub show: Option<ShowScrollbar>, pub show: Option<ShowScrollbar>,
/// Whether to show git diff indicators in the scrollbar.
///
/// Default: true
pub git_diff: Option<bool>, pub git_diff: Option<bool>,
/// Whether to show buffer search result markers in the scrollbar.
///
/// Default: true
pub selections: Option<bool>, pub selections: Option<bool>,
} }

View file

@ -795,7 +795,7 @@ impl EditorElement {
cx.paint_quad(quad( cx.paint_quad(quad(
highlight_bounds, highlight_bounds,
Corners::all(1. * line_height), Corners::all(1. * line_height),
gpui::yellow(), // todo!("use the right color") cx.theme().status().modified,
Edges::default(), Edges::default(),
transparent_black(), transparent_black(),
)); ));
@ -850,7 +850,7 @@ impl EditorElement {
cx.paint_quad(quad( cx.paint_quad(quad(
highlight_bounds, highlight_bounds,
Corners::all(0.05 * line_height), Corners::all(0.05 * line_height),
color, // todo!("use the right color") color,
Edges::default(), Edges::default(),
transparent_black(), transparent_black(),
)); ));

View file

@ -60,8 +60,7 @@ pub fn assert_text_with_selections(
#[allow(dead_code)] #[allow(dead_code)]
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
pub(crate) fn build_editor(buffer: Model<MultiBuffer>, cx: &mut ViewContext<Editor>) -> Editor { pub(crate) fn build_editor(buffer: Model<MultiBuffer>, cx: &mut ViewContext<Editor>) -> Editor {
// todo!() Editor::new(EditorMode::Full, buffer, None, cx)
Editor::new(EditorMode::Full, buffer, None, /*None,*/ cx)
} }
pub(crate) fn build_editor_with_project( pub(crate) fn build_editor_with_project(
@ -69,6 +68,5 @@ pub(crate) fn build_editor_with_project(
buffer: Model<MultiBuffer>, buffer: Model<MultiBuffer>,
cx: &mut ViewContext<Editor>, cx: &mut ViewContext<Editor>,
) -> Editor { ) -> Editor {
// todo!() Editor::new(EditorMode::Full, buffer, Some(project), cx)
Editor::new(EditorMode::Full, buffer, Some(project), /*None,*/ cx)
} }

View file

@ -1,5 +1,5 @@
use gpui::{Render, ViewContext, WeakView}; 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 workspace::{item::ItemHandle, StatusItemView, Workspace};
use crate::{feedback_modal::FeedbackModal, GiveFeedback}; use crate::{feedback_modal::FeedbackModal, GiveFeedback};
@ -27,7 +27,7 @@ impl Render for DeployFeedbackButton {
}) })
}) })
.is_some(); .is_some();
IconButton::new("give-feedback", Icon::Envelope) IconButton::new("give-feedback", IconName::Envelope)
.style(ui::ButtonStyle::Subtle) .style(ui::ButtonStyle::Subtle)
.icon_size(IconSize::Small) .icon_size(IconSize::Small)
.selected(is_open) .selected(is_open)

View file

@ -7,7 +7,7 @@ use db::kvp::KEY_VALUE_STORE;
use editor::{Editor, EditorEvent}; use editor::{Editor, EditorEvent};
use futures::AsyncReadExt; use futures::AsyncReadExt;
use gpui::{ use gpui::{
div, red, rems, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model, div, rems, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model,
PromptLevel, Render, Task, View, ViewContext, PromptLevel, Render, Task, View, ViewContext,
}; };
use isahc::Request; use isahc::Request;
@ -179,14 +179,13 @@ impl FeedbackModal {
editor 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 feedback_editor = cx.new_view(|cx| {
let mut editor = Editor::for_buffer(buffer, Some(project.clone()), cx); let mut editor = Editor::for_buffer(buffer, Some(project.clone()), cx);
editor.set_placeholder_text(placeholder_text, cx); editor.set_placeholder_text(
// editor.set_show_gutter(false, cx); "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.set_vertical_scroll_margin(5, cx);
editor editor
}); });
@ -422,10 +421,6 @@ impl Render for FeedbackModal {
let open_community_repo = let open_community_repo =
cx.listener(|_, _, cx| cx.dispatch_action(Box::new(OpenZedCommunityRepo))); 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() v_stack()
.elevation_3(cx) .elevation_3(cx)
.key_context("GiveFeedback") .key_context("GiveFeedback")
@ -434,11 +429,8 @@ impl Render for FeedbackModal {
.max_w(rems(96.)) .max_w(rems(96.))
.h(rems(32.)) .h(rems(32.))
.p_4() .p_4()
.gap_4() .gap_2()
.child(v_stack().child( .child(Headline::new("Share Feedback"))
// TODO: Add Headline component to `ui2`
div().text_xl().child("Share Feedback"),
))
.child( .child(
Label::new(if self.character_count < *FEEDBACK_CHAR_LIMIT.start() { Label::new(if self.character_count < *FEEDBACK_CHAR_LIMIT.start() {
format!( format!(
@ -467,6 +459,9 @@ impl Render for FeedbackModal {
.border_color(cx.theme().colors().border) .border_color(cx.theme().colors().border)
.child(self.feedback_editor.clone()), .child(self.feedback_editor.clone()),
) )
.child(
v_stack()
.gap_1()
.child( .child(
h_stack() h_stack()
.bg(cx.theme().colors().editor_background) .bg(cx.theme().colors().editor_background)
@ -476,10 +471,16 @@ impl Render for FeedbackModal {
.border_color(if self.valid_email_address() { .border_color(if self.valid_email_address() {
cx.theme().colors().border cx.theme().colors().border
} else { } else {
red() cx.theme().status().error_border
}) })
.child(self.email_address_editor.clone()), .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( .child(
h_stack() h_stack()
.justify_between() .justify_between()
@ -487,7 +488,7 @@ impl Render for FeedbackModal {
.child( .child(
Button::new("community_repository", "Community Repository") Button::new("community_repository", "Community Repository")
.style(ButtonStyle::Transparent) .style(ButtonStyle::Transparent)
.icon(Icon::ExternalLink) .icon(IconName::ExternalLink)
.icon_position(IconPosition::End) .icon_position(IconPosition::End)
.icon_size(IconSize::Small) .icon_size(IconSize::Small)
.on_click(open_community_repo), .on_click(open_community_repo),
@ -515,12 +516,7 @@ impl Render for FeedbackModal {
this.submit(cx).detach(); this.submit(cx).detach();
})) }))
.tooltip(move |cx| { .tooltip(move |cx| {
Tooltip::with_meta( Tooltip::text("Submit feedback to the Zed team.", cx)
"Submit feedback to the Zed team.",
None,
provide_an_email_address,
cx,
)
}) })
.when(!self.can_submit(), |this| this.disabled(true)), .when(!self.can_submit(), |this| this.disabled(true)),
), ),

View file

@ -1297,7 +1297,7 @@ mod tests {
// so that one should be sorted earlier // so that one should be sorted earlier
let b_path = ProjectPath { let b_path = ProjectPath {
worktree_id, worktree_id,
path: Arc::from(Path::new("/root/dir2/b.txt")), path: Arc::from(Path::new("dir2/b.txt")),
}; };
workspace workspace
.update(cx, |workspace, cx| { .update(cx, |workspace, cx| {

View file

@ -50,7 +50,7 @@ impl Render for Menu {
.on_action(|this, move: &MoveDown, cx| { .on_action(|this, move: &MoveDown, cx| {
// ... // ...
}) })
.children(todo!()) .children(unimplemented!())
} }
} }
``` ```
@ -68,7 +68,7 @@ impl Render for Menu {
.on_action(|this, move: &MoveDown, cx| { .on_action(|this, move: &MoveDown, cx| {
// ... // ...
}) })
.children(todo!()) .children(unimplemented!())
} }
} }
``` ```

View file

@ -203,7 +203,6 @@ macro_rules! __impl_action {
) )
} }
// todo!() why is this needed in addition to name?
fn debug_name() -> &'static str fn debug_name() -> &'static str
where where
Self: ::std::marker::Sized Self: ::std::marker::Sized

View file

@ -467,12 +467,11 @@ impl<V> View<V> {
} }
} }
// todo!(start_waiting) cx.borrow().background_executor().start_waiting();
// cx.borrow().foreground_executor().start_waiting();
rx.recv() rx.recv()
.await .await
.expect("view dropped with pending condition"); .expect("view dropped with pending condition");
// cx.borrow().foreground_executor().finish_waiting(); cx.borrow().background_executor().finish_waiting();
} }
}) })
.await .await

View file

@ -2,7 +2,7 @@ use std::sync::Arc;
use crate::{ use crate::{
point, size, BorrowWindow, Bounds, DevicePixels, Element, ImageData, InteractiveElement, 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, StyleRefinement, Styled, WindowContext,
}; };
use futures::FutureExt; use futures::FutureExt;
@ -12,13 +12,13 @@ use util::ResultExt;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum ImageSource { pub enum ImageSource {
/// Image content will be loaded from provided URI at render time. /// Image content will be loaded from provided URI at render time.
Uri(SharedString), Uri(SharedUrl),
Data(Arc<ImageData>), Data(Arc<ImageData>),
Surface(CVImageBuffer), Surface(CVImageBuffer),
} }
impl From<SharedString> for ImageSource { impl From<SharedUrl> for ImageSource {
fn from(value: SharedString) -> Self { fn from(value: SharedUrl) -> Self {
Self::Uri(value) Self::Uri(value)
} }
} }

View file

@ -14,8 +14,8 @@ pub struct Overlay {
children: SmallVec<[AnyElement; 2]>, children: SmallVec<[AnyElement; 2]>,
anchor_corner: AnchorCorner, anchor_corner: AnchorCorner,
fit_mode: OverlayFitMode, fit_mode: OverlayFitMode,
// todo!();
anchor_position: Option<Point<Pixels>>, anchor_position: Option<Point<Pixels>>,
// todo!();
// position_mode: OverlayPositionMode, // position_mode: OverlayPositionMode,
} }

View file

@ -18,6 +18,7 @@ mod platform;
pub mod prelude; pub mod prelude;
mod scene; mod scene;
mod shared_string; mod shared_string;
mod shared_url;
mod style; mod style;
mod styled; mod styled;
mod subscription; mod subscription;
@ -67,6 +68,7 @@ pub use refineable::*;
pub use scene::*; pub use scene::*;
use seal::Sealed; use seal::Sealed;
pub use shared_string::*; pub use shared_string::*;
pub use shared_url::*;
pub use smol::Timer; pub use smol::Timer;
pub use style::*; pub use style::*;
pub use styled::*; pub use styled::*;

View file

@ -1,4 +1,4 @@
use crate::{ImageData, ImageId, SharedString}; use crate::{ImageData, ImageId, SharedUrl};
use collections::HashMap; use collections::HashMap;
use futures::{ use futures::{
future::{BoxFuture, Shared}, future::{BoxFuture, Shared},
@ -44,7 +44,7 @@ impl From<ImageError> for Error {
pub struct ImageCache { pub struct ImageCache {
client: Arc<dyn HttpClient>, client: Arc<dyn HttpClient>,
images: Arc<Mutex<HashMap<SharedString, FetchImageFuture>>>, images: Arc<Mutex<HashMap<SharedUrl, FetchImageFuture>>>,
} }
type FetchImageFuture = Shared<BoxFuture<'static, Result<Arc<ImageData>, Error>>>; type FetchImageFuture = Shared<BoxFuture<'static, Result<Arc<ImageData>, Error>>>;
@ -59,7 +59,7 @@ impl ImageCache {
pub fn get( pub fn get(
&self, &self,
uri: impl Into<SharedString>, uri: impl Into<SharedUrl>,
) -> Shared<BoxFuture<'static, Result<Arc<ImageData>, Error>>> { ) -> Shared<BoxFuture<'static, Result<Arc<ImageData>, Error>>> {
let uri = uri.into(); let uri = uri.into();
let mut images = self.images.lock(); let mut images = self.images.lock();

View file

@ -192,8 +192,8 @@ impl DispatchTree {
keymap keymap
.bindings_for_action(action) .bindings_for_action(action)
.filter(|binding| { .filter(|binding| {
for i in 1..context_stack.len() { for i in 0..context_stack.len() {
let context = &context_stack[0..i]; let context = &context_stack[0..=i];
if keymap.binding_enabled(binding, context) { if keymap.binding_enabled(binding, context) {
return true; return true;
} }

View file

@ -32,7 +32,7 @@ impl PlatformDisplay for TestDisplay {
} }
fn as_any(&self) -> &dyn std::any::Any { fn as_any(&self) -> &dyn std::any::Any {
todo!() unimplemented!()
} }
fn bounds(&self) -> crate::Bounds<crate::GlobalPixels> { fn bounds(&self) -> crate::Bounds<crate::GlobalPixels> {

View file

@ -103,7 +103,6 @@ impl TestPlatform {
} }
} }
// todo!("implement out what our tests needed in GPUI 1")
impl Platform for TestPlatform { impl Platform for TestPlatform {
fn background_executor(&self) -> BackgroundExecutor { fn background_executor(&self) -> BackgroundExecutor {
self.background_executor.clone() self.background_executor.clone()

View file

@ -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<T: Into<SharedString>> From<T> for SharedUrl {
fn from(value: T) -> Self {
Self(value.into())
}
}

View file

@ -18,33 +18,33 @@ fn test_action_macros() {
impl gpui::Action for RegisterableAction { impl gpui::Action for RegisterableAction {
fn boxed_clone(&self) -> Box<dyn gpui::Action> { fn boxed_clone(&self) -> Box<dyn gpui::Action> {
todo!() unimplemented!()
} }
fn as_any(&self) -> &dyn std::any::Any { fn as_any(&self) -> &dyn std::any::Any {
todo!() unimplemented!()
} }
fn partial_eq(&self, _action: &dyn gpui::Action) -> bool { fn partial_eq(&self, _action: &dyn gpui::Action) -> bool {
todo!() unimplemented!()
} }
fn name(&self) -> &str { fn name(&self) -> &str {
todo!() unimplemented!()
} }
fn debug_name() -> &'static str fn debug_name() -> &'static str
where where
Self: Sized, Self: Sized,
{ {
todo!() unimplemented!()
} }
fn build(_value: serde_json::Value) -> anyhow::Result<Box<dyn gpui::Action>> fn build(_value: serde_json::Value) -> anyhow::Result<Box<dyn gpui::Action>>
where where
Self: Sized, Self: Sized,
{ {
todo!() unimplemented!()
} }
} }
} }

View file

@ -15,9 +15,16 @@ use workspace::{AppState, OpenVisible, Workspace};
actions!(journal, [NewJournalEntry]); actions!(journal, [NewJournalEntry]);
/// Settings specific to journaling
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
pub struct JournalSettings { pub struct JournalSettings {
/// The path of the directory where journal entries are stored.
///
/// Default: `~`
pub path: Option<String>, pub path: Option<String>,
/// What format to display the hours in.
///
/// Default: hour12
pub hour_format: Option<HourFormat>, pub hour_format: Option<HourFormat>,
} }

View file

@ -79,36 +79,90 @@ pub struct AllLanguageSettingsContent {
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] #[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
pub struct LanguageSettingsContent { pub struct LanguageSettingsContent {
/// How many columns a tab should occupy.
///
/// Default: 4
#[serde(default)] #[serde(default)]
pub tab_size: Option<NonZeroU32>, pub tab_size: Option<NonZeroU32>,
/// Whether to indent lines using tab characters, as opposed to multiple
/// spaces.
///
/// Default: false
#[serde(default)] #[serde(default)]
pub hard_tabs: Option<bool>, pub hard_tabs: Option<bool>,
/// How to soft-wrap long lines of text.
///
/// Default: none
#[serde(default)] #[serde(default)]
pub soft_wrap: Option<SoftWrap>, pub soft_wrap: Option<SoftWrap>,
/// The column at which to soft-wrap lines, for buffers where soft-wrap
/// is enabled.
///
/// Default: 80
#[serde(default)] #[serde(default)]
pub preferred_line_length: Option<u32>, pub preferred_line_length: Option<u32>,
/// 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)] #[serde(default)]
pub show_wrap_guides: Option<bool>, pub show_wrap_guides: Option<bool>,
/// Character counts at which to show wrap guides in the editor.
///
/// Default: []
#[serde(default)] #[serde(default)]
pub wrap_guides: Option<Vec<usize>>, pub wrap_guides: Option<Vec<usize>>,
/// Whether or not to perform a buffer format before saving.
///
/// Default: on
#[serde(default)] #[serde(default)]
pub format_on_save: Option<FormatOnSave>, pub format_on_save: Option<FormatOnSave>,
/// Whether or not to remove any trailing whitespace from lines of a buffer
/// before saving it.
///
/// Default: true
#[serde(default)] #[serde(default)]
pub remove_trailing_whitespace_on_save: Option<bool>, pub remove_trailing_whitespace_on_save: Option<bool>,
/// Whether or not to ensure there's a single newline at the end of a buffer
/// when saving it.
///
/// Default: true
#[serde(default)] #[serde(default)]
pub ensure_final_newline_on_save: Option<bool>, pub ensure_final_newline_on_save: Option<bool>,
/// How to perform a buffer format.
///
/// Default: auto
#[serde(default)] #[serde(default)]
pub formatter: Option<Formatter>, pub formatter: Option<Formatter>,
/// 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)] #[serde(default)]
pub prettier: Option<HashMap<String, serde_json::Value>>, pub prettier: Option<HashMap<String, serde_json::Value>>,
/// Whether to use language servers to provide code intelligence.
///
/// Default: true
#[serde(default)] #[serde(default)]
pub enable_language_server: Option<bool>, pub enable_language_server: Option<bool>,
/// Controls whether copilot provides suggestion immediately (true)
/// or waits for a `copilot::Toggle` (false).
///
/// Default: true
#[serde(default)] #[serde(default)]
pub show_copilot_suggestions: Option<bool>, pub show_copilot_suggestions: Option<bool>,
/// Whether to show tabs and spaces in the editor.
#[serde(default)] #[serde(default)]
pub show_whitespaces: Option<ShowWhitespaceSetting>, pub show_whitespaces: Option<ShowWhitespaceSetting>,
/// Whether to start a new line with a comment when a previous line is a comment as well.
///
/// Default: true
#[serde(default)] #[serde(default)]
pub extend_comment_on_newline: Option<bool>, pub extend_comment_on_newline: Option<bool>,
/// Inlay hint related settings.
#[serde(default)] #[serde(default)]
pub inlay_hints: Option<InlayHintSettings>, pub inlay_hints: Option<InlayHintSettings>,
} }
@ -128,8 +182,11 @@ pub struct FeaturesContent {
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum SoftWrap { pub enum SoftWrap {
/// Do not soft wrap.
None, None,
/// Soft wrap lines that overflow the editor
EditorWidth, EditorWidth,
/// Soft wrap lines at the preferred line length
PreferredLineLength, PreferredLineLength,
} }
@ -148,18 +205,26 @@ pub enum FormatOnSave {
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum ShowWhitespaceSetting { pub enum ShowWhitespaceSetting {
/// Draw tabs and spaces only for the selected text.
Selection, Selection,
/// Do not draw any tabs or spaces
None, None,
/// Draw all invisible symbols
All, All,
} }
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum Formatter { pub enum Formatter {
/// Format files using Zed's Prettier integration (if applicable),
/// or falling back to formatting via language server.
#[default] #[default]
Auto, Auto,
/// Format code using the current language server.
LanguageServer, LanguageServer,
/// Format code using Zed's Prettier integration.
Prettier, Prettier,
/// Format code using an external command.
External { External {
command: Arc<str>, command: Arc<str>,
arguments: Arc<[String]>, arguments: Arc<[String]>,
@ -168,6 +233,9 @@ pub enum Formatter {
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
pub struct InlayHintSettings { pub struct InlayHintSettings {
/// Global switch to toggle hints on and off.
///
/// Default: false
#[serde(default)] #[serde(default)]
pub enabled: bool, pub enabled: bool,
#[serde(default = "default_true")] #[serde(default = "default_true")]

View file

@ -258,19 +258,19 @@ fn test_typing_multiple_new_injections() {
let (buffer, syntax_map) = test_edit_sequence( let (buffer, syntax_map) = test_edit_sequence(
"Rust", "Rust",
&[ &[
"fn a() { dbg }", "fn a() { test_macro }",
"fn a() { dbg«!» }", "fn a() { test_macro«!» }",
"fn a() { dbg!«()» }", "fn a() { test_macro!«()» }",
"fn a() { dbg!(«b») }", "fn a() { test_macro!(«b») }",
"fn a() { dbg!(b«.») }", "fn a() { test_macro!(b«.») }",
"fn a() { dbg!(b.«c») }", "fn a() { test_macro!(b.«c») }",
"fn a() { dbg!(b.c«()») }", "fn a() { test_macro!(b.c«()») }",
"fn a() { dbg!(b.c(«vec»)) }", "fn a() { test_macro!(b.c(«vec»)) }",
"fn a() { dbg!(b.c(vec«!»)) }", "fn a() { test_macro!(b.c(vec«!»)) }",
"fn a() { dbg!(b.c(vec!«[]»)) }", "fn a() { test_macro!(b.c(vec!«[]»)) }",
"fn a() { dbg!(b.c(vec![«d»])) }", "fn a() { test_macro!(b.c(vec![«d»])) }",
"fn a() { dbg!(b.c(vec![d«.»])) }", "fn a() { test_macro!(b.c(vec![d«.»])) }",
"fn a() { dbg!(b.c(vec![d.«e»])) }", "fn a() { test_macro!(b.c(vec![d.«e»])) }",
], ],
); );
@ -278,7 +278,7 @@ fn test_typing_multiple_new_injections() {
&syntax_map, &syntax_map,
&buffer, &buffer,
&["field"], &["field"],
"fn a() { dbg!(b.«c»(vec![d.«e»])) }", "fn a() { test_macro!(b.«c»(vec![d.«e»])) }",
); );
} }

View file

@ -54,7 +54,13 @@ impl FocusableView for OutlineView {
} }
impl EventEmitter<DismissEvent> for OutlineView {} impl EventEmitter<DismissEvent> for OutlineView {}
impl ModalView for OutlineView {} impl ModalView for OutlineView {
fn on_before_dismiss(&mut self, cx: &mut ViewContext<Self>) -> bool {
self.picker
.update(cx, |picker, cx| picker.delegate.restore_active_editor(cx));
true
}
}
impl Render for OutlineView { impl Render for OutlineView {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement { fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {

View file

@ -4732,7 +4732,8 @@ impl Project {
} else { } else {
return Task::ready(Err(anyhow!("worktree not found for symbol"))); 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) { let symbol_uri = if let Ok(uri) = lsp::Url::from_file_path(symbol_abs_path) {
uri uri
} else { } else {
@ -6581,7 +6582,14 @@ impl Project {
let removed = *change == PathChange::Removed; let removed = *change == PathChange::Removed;
let abs_path = worktree.absolutize(path); let abs_path = worktree.absolutize(path);
settings_contents.push(async move { 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() 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 { impl Item for Buffer {
fn entry_id(&self, cx: &AppContext) -> Option<ProjectEntryId> { fn entry_id(&self, cx: &AppContext) -> Option<ProjectEntryId> {
File::from_dyn(self.file()).and_then(|file| file.project_entry_id(cx)) File::from_dyn(self.file()).and_then(|file| file.project_entry_id(cx))

View file

@ -7,16 +7,40 @@ use std::sync::Arc;
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] #[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
pub struct ProjectSettings { 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)] #[serde(default)]
pub lsp: HashMap<Arc<str>, LspSettings>, pub lsp: HashMap<Arc<str>, LspSettings>,
/// Configuration for Git-related features
#[serde(default)] #[serde(default)]
pub git: GitSettings, 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)] #[serde(default)]
pub file_scan_exclusions: Option<Vec<String>>, pub file_scan_exclusions: Option<Vec<String>>,
} }
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] #[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
pub struct GitSettings { pub struct GitSettings {
/// Whether or not to show the git gutter.
///
/// Default: tracked_files
pub git_gutter: Option<GitGutterSetting>, pub git_gutter: Option<GitGutterSetting>,
pub gutter_debounce: Option<u64>, pub gutter_debounce: Option<u64>,
} }
@ -24,8 +48,10 @@ pub struct GitSettings {
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)] #[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum GitGutterSetting { pub enum GitGutterSetting {
/// Show git gutter in tracked files.
#[default] #[default]
TrackedFiles, TrackedFiles,
/// Hide git gutter
Hide, Hide,
} }

View file

@ -4278,6 +4278,75 @@ fn test_glob_literal_prefix() {
assert_eq!(glob_literal_prefix("foo/bar/baz.js"), "foo/bar/baz.js"); 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( async fn search(
project: &Model<Project>, project: &Model<Project>,
query: SearchQuery, query: SearchQuery,

View file

@ -965,6 +965,7 @@ impl LocalWorktree {
let entry = self.refresh_entry(path.clone(), None, cx); let entry = self.refresh_entry(path.clone(), None, cx);
cx.spawn(|this, mut cx| async move { cx.spawn(|this, mut cx| async move {
let abs_path = abs_path?;
let text = fs.load(&abs_path).await?; let text = fs.load(&abs_path).await?;
let mut index_task = None; let mut index_task = None;
let snapshot = this.update(&mut cx, |this, _| this.as_local().unwrap().snapshot())?; 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 { cx.spawn(move |this, mut cx| async move {
let entry = save.await?; let entry = save.await?;
let abs_path = abs_path?;
let this = this.upgrade().context("worktree dropped")?; let this = this.upgrade().context("worktree dropped")?;
let (entry_id, mtime, path) = match entry { let (entry_id, mtime, path) = match entry {
@ -1139,9 +1141,9 @@ impl LocalWorktree {
let fs = self.fs.clone(); let fs = self.fs.clone();
let write = cx.background_executor().spawn(async move { let write = cx.background_executor().spawn(async move {
if is_dir { if is_dir {
fs.create_dir(&abs_path).await fs.create_dir(&abs_path?).await
} else { } else {
fs.save(&abs_path, &Default::default(), Default::default()) fs.save(&abs_path?, &Default::default(), Default::default())
.await .await
} }
}); });
@ -1188,7 +1190,7 @@ impl LocalWorktree {
let fs = self.fs.clone(); let fs = self.fs.clone();
let write = cx let write = cx
.background_executor() .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 { cx.spawn(|this, mut cx| async move {
write.await?; write.await?;
@ -1210,10 +1212,10 @@ impl LocalWorktree {
let delete = cx.background_executor().spawn(async move { let delete = cx.background_executor().spawn(async move {
if entry.is_file() { if entry.is_file() {
fs.remove_file(&abs_path, Default::default()).await?; fs.remove_file(&abs_path?, Default::default()).await?;
} else { } else {
fs.remove_dir( fs.remove_dir(
&abs_path, &abs_path?,
RemoveOptions { RemoveOptions {
recursive: true, recursive: true,
ignore_if_not_exists: false, ignore_if_not_exists: false,
@ -1252,7 +1254,7 @@ impl LocalWorktree {
let abs_new_path = self.absolutize(&new_path); let abs_new_path = self.absolutize(&new_path);
let fs = self.fs.clone(); let fs = self.fs.clone();
let rename = cx.background_executor().spawn(async move { 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 .await
}); });
@ -1284,8 +1286,8 @@ impl LocalWorktree {
let copy = cx.background_executor().spawn(async move { let copy = cx.background_executor().spawn(async move {
copy_recursive( copy_recursive(
fs.as_ref(), fs.as_ref(),
&abs_old_path, &abs_old_path?,
&abs_new_path, &abs_new_path?,
Default::default(), Default::default(),
) )
.await .await
@ -1609,11 +1611,17 @@ impl Snapshot {
&self.abs_path &self.abs_path
} }
pub fn absolutize(&self, path: &Path) -> PathBuf { pub fn absolutize(&self, path: &Path) -> Result<PathBuf> {
if path
.components()
.any(|component| !matches!(component, std::path::Component::Normal(_)))
{
return Err(anyhow!("invalid path"));
}
if path.file_name().is_some() { if path.file_name().is_some() {
self.abs_path.join(path) Ok(self.abs_path.join(path))
} else { } 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 abs_path = worktree.absolutize(&self.path);
let fs = worktree.fs.clone(); let fs = worktree.fs.clone();
cx.background_executor() cx.background_executor()
.spawn(async move { fs.load(&abs_path).await }) .spawn(async move { fs.load(&abs_path?).await })
} }
fn buffer_reloaded( fn buffer_reloaded(

View file

@ -30,7 +30,7 @@ use std::{
sync::Arc, sync::Arc,
}; };
use theme::ThemeSettings; 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 unicase::UniCase;
use util::{maybe, ResultExt, TryFutureExt}; use util::{maybe, ResultExt, TryFutureExt};
use workspace::{ use workspace::{
@ -1403,7 +1403,7 @@ impl ProjectPanel {
.indent_step_size(px(settings.indent_size)) .indent_step_size(px(settings.indent_size))
.selected(is_selected) .selected(is_selected)
.child(if let Some(icon) = &icon { .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 { } else {
div().size(IconSize::default().rems()).invisible() div().size(IconSize::default().rems()).invisible()
}) })
@ -1433,6 +1433,9 @@ impl ProjectPanel {
})) }))
.on_secondary_mouse_down(cx.listener( .on_secondary_mouse_down(cx.listener(
move |this, event: &MouseDownEvent, cx| { 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); 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)) el.on_action(cx.listener(Self::reveal_in_finder))
.on_action(cx.listener(Self::open_in_terminal)) .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) .track_focus(&self.focus_handle)
.child( .child(
uniform_list( uniform_list(
@ -1577,7 +1590,7 @@ impl Render for DraggedProjectEntryView {
.indent_level(self.details.depth) .indent_level(self.details.depth)
.indent_step_size(px(settings.indent_size)) .indent_step_size(px(settings.indent_size))
.child(if let Some(icon) = &self.details.icon { .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 { } else {
div() div()
}) })
@ -1627,8 +1640,8 @@ impl Panel for ProjectPanel {
cx.notify(); cx.notify();
} }
fn icon(&self, _: &WindowContext) -> Option<ui::Icon> { fn icon(&self, _: &WindowContext) -> Option<ui::IconName> {
Some(ui::Icon::FileTree) Some(ui::IconName::FileTree)
} }
fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> { fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {

View file

@ -24,12 +24,35 @@ pub struct ProjectPanelSettings {
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
pub struct ProjectPanelSettingsContent { pub struct ProjectPanelSettingsContent {
/// Customise default width (in pixels) taken by project panel
///
/// Default: 240
pub default_width: Option<f32>, pub default_width: Option<f32>,
/// The position of project panel
///
/// Default: left
pub dock: Option<ProjectPanelDockPosition>, pub dock: Option<ProjectPanelDockPosition>,
/// Whether to show file icons in the project panel.
///
/// Default: true
pub file_icons: Option<bool>, pub file_icons: Option<bool>,
/// Whether to show folder icons or chevrons for directories in the project panel.
///
/// Default: true
pub folder_icons: Option<bool>, pub folder_icons: Option<bool>,
/// Whether to show the git status in the project panel.
///
/// Default: true
pub git_status: Option<bool>, pub git_status: Option<bool>,
/// Amount of indentation (in pixels) for nested items.
///
/// Default: 20
pub indent_size: Option<f32>, pub indent_size: Option<f32>,
/// 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<bool>, pub auto_reveal_entries: Option<bool>,
} }

View file

@ -6,7 +6,7 @@ use gpui::{
Subscription, View, ViewContext, WeakView, Subscription, View, ViewContext, WeakView,
}; };
use search::{buffer_search, BufferSearchBar}; 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::{ use workspace::{
item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
}; };
@ -43,7 +43,7 @@ impl Render for QuickActionBar {
let inlay_hints_button = Some(QuickActionBarButton::new( let inlay_hints_button = Some(QuickActionBarButton::new(
"toggle inlay hints", "toggle inlay hints",
Icon::InlayHint, IconName::InlayHint,
editor.read(cx).inlay_hints_enabled(), editor.read(cx).inlay_hints_enabled(),
Box::new(editor::ToggleInlayHints), Box::new(editor::ToggleInlayHints),
"Toggle Inlay Hints", "Toggle Inlay Hints",
@ -60,7 +60,7 @@ impl Render for QuickActionBar {
let search_button = Some(QuickActionBarButton::new( let search_button = Some(QuickActionBarButton::new(
"toggle buffer search", "toggle buffer search",
Icon::MagnifyingGlass, IconName::MagnifyingGlass,
!self.buffer_search_bar.read(cx).is_dismissed(), !self.buffer_search_bar.read(cx).is_dismissed(),
Box::new(buffer_search::Deploy { focus: false }), Box::new(buffer_search::Deploy { focus: false }),
"Buffer Search", "Buffer Search",
@ -77,7 +77,7 @@ impl Render for QuickActionBar {
let assistant_button = QuickActionBarButton::new( let assistant_button = QuickActionBarButton::new(
"toggle inline assistant", "toggle inline assistant",
Icon::MagicWand, IconName::MagicWand,
false, false,
Box::new(InlineAssist), Box::new(InlineAssist),
"Inline Assist", "Inline Assist",
@ -95,7 +95,6 @@ impl Render for QuickActionBar {
h_stack() h_stack()
.id("quick action bar") .id("quick action bar")
.p_1()
.gap_2() .gap_2()
.children(inlay_hints_button) .children(inlay_hints_button)
.children(search_button) .children(search_button)
@ -108,7 +107,7 @@ impl EventEmitter<ToolbarItemEvent> for QuickActionBar {}
#[derive(IntoElement)] #[derive(IntoElement)]
struct QuickActionBarButton { struct QuickActionBarButton {
id: ElementId, id: ElementId,
icon: Icon, icon: IconName,
toggled: bool, toggled: bool,
action: Box<dyn Action>, action: Box<dyn Action>,
tooltip: SharedString, tooltip: SharedString,
@ -118,7 +117,7 @@ struct QuickActionBarButton {
impl QuickActionBarButton { impl QuickActionBarButton {
fn new( fn new(
id: impl Into<ElementId>, id: impl Into<ElementId>,
icon: Icon, icon: IconName,
toggled: bool, toggled: bool,
action: Box<dyn Action>, action: Box<dyn Action>,
tooltip: impl Into<SharedString>, tooltip: impl Into<SharedString>,

View file

@ -60,8 +60,10 @@ macro_rules! request_messages {
#[macro_export] #[macro_export]
macro_rules! entity_messages { macro_rules! entity_messages {
($id_field:ident, $($name:ident),* $(,)?) => { ({$id_field:ident, $entity_type:ty}, $($name:ident),* $(,)?) => {
$(impl EntityMessage for $name { $(impl EntityMessage for $name {
type Entity = $entity_type;
fn remote_entity_id(&self) -> u64 { fn remote_entity_id(&self) -> u64 {
self.$id_field self.$id_field
} }

View file

@ -31,6 +31,7 @@ pub trait EnvelopedMessage: Clone + Debug + Serialize + Sized + Send + Sync + 's
} }
pub trait EntityMessage: EnvelopedMessage { pub trait EntityMessage: EnvelopedMessage {
type Entity;
fn remote_entity_id(&self) -> u64; fn remote_entity_id(&self) -> u64;
} }
@ -369,7 +370,7 @@ request_messages!(
); );
entity_messages!( entity_messages!(
project_id, {project_id, ShareProject},
AddProjectCollaborator, AddProjectCollaborator,
ApplyCodeAction, ApplyCodeAction,
ApplyCompletionAdditionalEdits, ApplyCompletionAdditionalEdits,
@ -422,7 +423,7 @@ entity_messages!(
); );
entity_messages!( entity_messages!(
channel_id, {channel_id, Channel},
ChannelMessageSent, ChannelMessageSent,
RemoveChannelMessage, RemoveChannelMessage,
UpdateChannelBuffer, UpdateChannelBuffer,

View file

@ -21,7 +21,7 @@ use settings::Settings;
use std::{any::Any, sync::Arc}; use std::{any::Any, sync::Arc};
use theme::ThemeSettings; 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 util::ResultExt;
use workspace::{ use workspace::{
item::ItemHandle, item::ItemHandle,
@ -225,7 +225,7 @@ impl Render for BufferSearchBar {
.border_color(editor_border) .border_color(editor_border)
.min_w(rems(384. / 16.)) .min_w(rems(384. / 16.))
.rounded_lg() .rounded_lg()
.child(IconElement::new(Icon::MagnifyingGlass)) .child(Icon::new(IconName::MagnifyingGlass))
.child(self.render_text_input(&self.query_editor, cx)) .child(self.render_text_input(&self.query_editor, cx))
.children(supported_options.case.then(|| { .children(supported_options.case.then(|| {
self.render_search_option_button( self.render_search_option_button(
@ -287,7 +287,7 @@ impl Render for BufferSearchBar {
this.child( this.child(
IconButton::new( IconButton::new(
"buffer-search-bar-toggle-replace-button", "buffer-search-bar-toggle-replace-button",
Icon::Replace, IconName::Replace,
) )
.style(ButtonStyle::Subtle) .style(ButtonStyle::Subtle)
.when(self.replace_enabled, |button| { .when(self.replace_enabled, |button| {
@ -323,7 +323,7 @@ impl Render for BufferSearchBar {
) )
.when(should_show_replace_input, |this| { .when(should_show_replace_input, |this| {
this.child( this.child(
IconButton::new("search-replace-next", ui::Icon::ReplaceNext) IconButton::new("search-replace-next", ui::IconName::ReplaceNext)
.tooltip(move |cx| { .tooltip(move |cx| {
Tooltip::for_action("Replace next", &ReplaceNext, cx) Tooltip::for_action("Replace next", &ReplaceNext, cx)
}) })
@ -332,7 +332,7 @@ impl Render for BufferSearchBar {
})), })),
) )
.child( .child(
IconButton::new("search-replace-all", ui::Icon::ReplaceAll) IconButton::new("search-replace-all", ui::IconName::ReplaceAll)
.tooltip(move |cx| { .tooltip(move |cx| {
Tooltip::for_action("Replace all", &ReplaceAll, cx) Tooltip::for_action("Replace all", &ReplaceAll, cx)
}) })
@ -350,7 +350,7 @@ impl Render for BufferSearchBar {
.gap_0p5() .gap_0p5()
.flex_none() .flex_none()
.child( .child(
IconButton::new("select-all", ui::Icon::SelectAll) IconButton::new("select-all", ui::IconName::SelectAll)
.on_click(|_, cx| cx.dispatch_action(SelectAllMatches.boxed_clone())) .on_click(|_, cx| cx.dispatch_action(SelectAllMatches.boxed_clone()))
.tooltip(|cx| { .tooltip(|cx| {
Tooltip::for_action("Select all matches", &SelectAllMatches, cx) Tooltip::for_action("Select all matches", &SelectAllMatches, cx)
@ -358,13 +358,13 @@ impl Render for BufferSearchBar {
) )
.children(match_count) .children(match_count)
.child(render_nav_button( .child(render_nav_button(
ui::Icon::ChevronLeft, ui::IconName::ChevronLeft,
self.active_match_index.is_some(), self.active_match_index.is_some(),
"Select previous match", "Select previous match",
&SelectPrevMatch, &SelectPrevMatch,
)) ))
.child(render_nav_button( .child(render_nav_button(
ui::Icon::ChevronRight, ui::IconName::ChevronRight,
self.active_match_index.is_some(), self.active_match_index.is_some(),
"Select next match", "Select next match",
&SelectNextMatch, &SelectNextMatch,

View file

@ -38,7 +38,7 @@ use std::{
use theme::ThemeSettings; use theme::ThemeSettings;
use ui::{ 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, Selectable, ToggleButton, Tooltip,
}; };
use util::{paths::PathMatcher, ResultExt as _}; use util::{paths::PathMatcher, ResultExt as _};
@ -424,7 +424,8 @@ impl Item for ProjectSearchView {
.current() .current()
.as_ref() .as_ref()
.map(|query| { .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() query_text.into()
}); });
let tab_name = last_query let tab_name = last_query
@ -432,7 +433,7 @@ impl Item for ProjectSearchView {
.unwrap_or_else(|| "Project search".into()); .unwrap_or_else(|| "Project search".into());
h_stack() h_stack()
.gap_2() .gap_2()
.child(IconElement::new(Icon::MagnifyingGlass).color(if selected { .child(Icon::new(IconName::MagnifyingGlass).color(if selected {
Color::Default Color::Default
} else { } else {
Color::Muted 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.confirm(action, cx)))
.on_action(cx.listener(|this, action, cx| this.previous_history_query(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))) .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(self.render_text_input(&search.query_editor, cx))
.child( .child(
h_stack() h_stack()
.child( .child(
IconButton::new("project-search-filter-button", Icon::Filter) IconButton::new("project-search-filter-button", IconName::Filter)
.tooltip(|cx| { .tooltip(|cx| {
Tooltip::for_action("Toggle filters", &ToggleFilters, cx) Tooltip::for_action("Toggle filters", &ToggleFilters, cx)
}) })
@ -1639,7 +1640,7 @@ impl Render for ProjectSearchBar {
this.child( this.child(
IconButton::new( IconButton::new(
"project-search-case-sensitive", "project-search-case-sensitive",
Icon::CaseSensitive, IconName::CaseSensitive,
) )
.tooltip(|cx| { .tooltip(|cx| {
Tooltip::for_action( Tooltip::for_action(
@ -1659,7 +1660,7 @@ impl Render for ProjectSearchBar {
)), )),
) )
.child( .child(
IconButton::new("project-search-whole-word", Icon::WholeWord) IconButton::new("project-search-whole-word", IconName::WholeWord)
.tooltip(|cx| { .tooltip(|cx| {
Tooltip::for_action( Tooltip::for_action(
"Toggle whole word", "Toggle whole word",
@ -1738,7 +1739,7 @@ impl Render for ProjectSearchBar {
}), }),
) )
.child( .child(
IconButton::new("project-search-toggle-replace", Icon::Replace) IconButton::new("project-search-toggle-replace", IconName::Replace)
.on_click(cx.listener(|this, _, cx| { .on_click(cx.listener(|this, _, cx| {
this.toggle_replace(&ToggleReplace, cx); this.toggle_replace(&ToggleReplace, cx);
})) }))
@ -1755,7 +1756,7 @@ impl Render for ProjectSearchBar {
.border_1() .border_1()
.border_color(cx.theme().colors().border) .border_color(cx.theme().colors().border)
.rounded_lg() .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)) .child(self.render_text_input(&search.replacement_editor, cx))
} else { } else {
// Fill out the space if we don't have a replacement editor. // 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() let actions_column = h_stack()
.when(search.replace_enabled, |this| { .when(search.replace_enabled, |this| {
this.child( this.child(
IconButton::new("project-search-replace-next", Icon::ReplaceNext) IconButton::new("project-search-replace-next", IconName::ReplaceNext)
.on_click(cx.listener(|this, _, cx| { .on_click(cx.listener(|this, _, cx| {
if let Some(search) = this.active_project_search.as_ref() { if let Some(search) = this.active_project_search.as_ref() {
search.update(cx, |this, cx| { search.update(cx, |this, cx| {
@ -1775,7 +1776,7 @@ impl Render for ProjectSearchBar {
.tooltip(|cx| Tooltip::for_action("Replace next match", &ReplaceNext, cx)), .tooltip(|cx| Tooltip::for_action("Replace next match", &ReplaceNext, cx)),
) )
.child( .child(
IconButton::new("project-search-replace-all", Icon::ReplaceAll) IconButton::new("project-search-replace-all", IconName::ReplaceAll)
.on_click(cx.listener(|this, _, cx| { .on_click(cx.listener(|this, _, cx| {
if let Some(search) = this.active_project_search.as_ref() { if let Some(search) = this.active_project_search.as_ref() {
search.update(cx, |this, cx| { search.update(cx, |this, cx| {
@ -1796,7 +1797,7 @@ impl Render for ProjectSearchBar {
this this
}) })
.child( .child(
IconButton::new("project-search-prev-match", Icon::ChevronLeft) IconButton::new("project-search-prev-match", IconName::ChevronLeft)
.disabled(search.active_match_index.is_none()) .disabled(search.active_match_index.is_none())
.on_click(cx.listener(|this, _, cx| { .on_click(cx.listener(|this, _, cx| {
if let Some(search) = this.active_project_search.as_ref() { if let Some(search) = this.active_project_search.as_ref() {
@ -1810,7 +1811,7 @@ impl Render for ProjectSearchBar {
}), }),
) )
.child( .child(
IconButton::new("project-search-next-match", Icon::ChevronRight) IconButton::new("project-search-next-match", IconName::ChevronRight)
.disabled(search.active_match_index.is_none()) .disabled(search.active_match_index.is_none())
.on_click(cx.listener(|this, _, cx| { .on_click(cx.listener(|this, _, cx| {
if let Some(search) = this.active_project_search.as_ref() { if let Some(search) = this.active_project_search.as_ref() {

View file

@ -60,11 +60,11 @@ impl SearchOptions {
} }
} }
pub fn icon(&self) -> ui::Icon { pub fn icon(&self) -> ui::IconName {
match *self { match *self {
SearchOptions::WHOLE_WORD => ui::Icon::WholeWord, SearchOptions::WHOLE_WORD => ui::IconName::WholeWord,
SearchOptions::CASE_SENSITIVE => ui::Icon::CaseSensitive, SearchOptions::CASE_SENSITIVE => ui::IconName::CaseSensitive,
SearchOptions::INCLUDE_IGNORED => ui::Icon::FileGit, SearchOptions::INCLUDE_IGNORED => ui::IconName::FileGit,
_ => panic!("{:?} is not a named SearchOption", self), _ => panic!("{:?} is not a named SearchOption", self),
} }
} }

View file

@ -3,7 +3,7 @@ use ui::IconButton;
use ui::{prelude::*, Tooltip}; use ui::{prelude::*, Tooltip};
pub(super) fn render_nav_button( pub(super) fn render_nav_button(
icon: ui::Icon, icon: ui::IconName,
active: bool, active: bool,
tooltip: &'static str, tooltip: &'static str,
action: &'static dyn Action, action: &'static dyn Action,

View file

@ -559,7 +559,7 @@ impl SemanticIndex {
.spawn(async move { .spawn(async move {
let mut changed_paths = BTreeMap::new(); let mut changed_paths = BTreeMap::new();
for file in worktree.files(false, 0) { 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 { if file.is_external || file.is_ignored || file.is_symlink {
continue; continue;
@ -1068,11 +1068,10 @@ impl SemanticIndex {
return true; return true;
}; };
worktree_state.changed_paths.retain(|path, info| { for (path, info) in &worktree_state.changed_paths {
if info.is_deleted { if info.is_deleted {
files_to_delete.push((worktree_state.db_id, path.clone())); files_to_delete.push((worktree_state.db_id, path.clone()));
} else { } else if let Ok(absolute_path) = worktree.read(cx).absolutize(path) {
let absolute_path = worktree.read(cx).absolutize(path);
let job_handle = JobHandle::new(pending_file_count_tx); let job_handle = JobHandle::new(pending_file_count_tx);
pending_files.push(PendingFile { pending_files.push(PendingFile {
absolute_path, absolute_path,
@ -1083,9 +1082,8 @@ impl SemanticIndex {
worktree_db_id: worktree_state.db_id, worktree_db_id: worktree_state.db_id,
}); });
} }
}
false worktree_state.changed_paths.clear();
});
true true
}); });

View file

@ -8,8 +8,13 @@ pub struct SemanticIndexSettings {
pub enabled: bool, pub enabled: bool,
} }
/// Configuration of semantic index, an alternate search engine available in
/// project search.
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
pub struct SemanticIndexSettingsContent { pub struct SemanticIndexSettingsContent {
/// Whether or not to display the Semantic mode in project search.
///
/// Default: true
pub enabled: Option<bool>, pub enabled: Option<bool>,
} }

View file

@ -14,6 +14,7 @@ anyhow.workspace = true
backtrace-on-stack-overflow = "0.3.0" backtrace-on-stack-overflow = "0.3.0"
chrono = "0.4" chrono = "0.4"
clap = { version = "4.4", features = ["derive", "string"] } clap = { version = "4.4", features = ["derive", "string"] }
collab_ui = { path = "../collab_ui", features = ["stories"] }
strum = { version = "0.25.0", features = ["derive"] } strum = { version = "0.25.0", features = ["derive"] }
dialoguer = { version = "0.11.0", features = ["fuzzy-select"] } dialoguer = { version = "0.11.0", features = ["fuzzy-select"] }
editor = { path = "../editor" } editor = { path = "../editor" }

View file

@ -16,6 +16,7 @@ pub enum ComponentStory {
Avatar, Avatar,
Button, Button,
Checkbox, Checkbox,
CollabNotification,
ContextMenu, ContextMenu,
Cursor, Cursor,
Disclosure, Disclosure,
@ -45,6 +46,9 @@ impl ComponentStory {
Self::Avatar => cx.new_view(|_| ui::AvatarStory).into(), Self::Avatar => cx.new_view(|_| ui::AvatarStory).into(),
Self::Button => cx.new_view(|_| ui::ButtonStory).into(), Self::Button => cx.new_view(|_| ui::ButtonStory).into(),
Self::Checkbox => cx.new_view(|_| ui::CheckboxStory).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::ContextMenu => cx.new_view(|_| ui::ContextMenuStory).into(),
Self::Cursor => cx.new_view(|_| crate::stories::CursorStory).into(), Self::Cursor => cx.new_view(|_| crate::stories::CursorStory).into(),
Self::Disclosure => cx.new_view(|_| ui::DisclosureStory).into(), Self::Disclosure => cx.new_view(|_| ui::DisclosureStory).into(),

View file

@ -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 /// 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). /// 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) { fn rgb_for_index(i: &u8) -> (u8, u8, u8) {
debug_assert!((&16..=&231).contains(&i)); debug_assert!((&16..=&231).contains(&i));
let i = i - 16; let i = i - 16;

View file

@ -36,6 +36,9 @@ pub enum VenvSettings {
#[default] #[default]
Off, Off,
On { 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<ActivateScript>, activate_script: Option<ActivateScript>,
directories: Option<Vec<PathBuf>>, directories: Option<Vec<PathBuf>>,
}, },
@ -73,20 +76,68 @@ pub enum ActivateScript {
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
pub struct TerminalSettingsContent { pub struct TerminalSettingsContent {
/// What shell to use when opening a terminal.
///
/// Default: system
pub shell: Option<Shell>, pub shell: Option<Shell>,
/// What working directory to use when launching the terminal
///
/// Default: current_project_directory
pub working_directory: Option<WorkingDirectory>, pub working_directory: Option<WorkingDirectory>,
/// 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<f32>, pub font_size: Option<f32>,
/// 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<String>, pub font_family: Option<String>,
/// Set the terminal's line height.
///
/// Default: comfortable
pub line_height: Option<TerminalLineHeight>, pub line_height: Option<TerminalLineHeight>,
pub font_features: Option<FontFeatures>, pub font_features: Option<FontFeatures>,
/// 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<HashMap<String, String>>, pub env: Option<HashMap<String, String>>,
/// Set the cursor blinking behavior in the terminal.
///
/// Default: terminal_controlled
pub blinking: Option<TerminalBlink>, pub blinking: Option<TerminalBlink>,
/// 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<AlternateScroll>, pub alternate_scroll: Option<AlternateScroll>,
/// Set whether the option key behaves as the meta key.
///
/// Default: false
pub option_as_meta: Option<bool>, pub option_as_meta: Option<bool>,
/// Whether or not selecting text in the terminal will automatically
/// copy to the system clipboard.
///
/// Default: false
pub copy_on_select: Option<bool>, pub copy_on_select: Option<bool>,
pub dock: Option<TerminalDockPosition>, pub dock: Option<TerminalDockPosition>,
/// Default width when the terminal is docked to the left or right.
///
/// Default: 640
pub default_width: Option<f32>, pub default_width: Option<f32>,
/// Default height when the terminal is docked to the bottom.
///
/// Default: 320
pub default_height: Option<f32>, pub default_height: Option<f32>,
/// 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<VenvSettings>, pub detect_venv: Option<VenvSettings>,
} }
@ -107,9 +158,13 @@ impl settings::Settings for TerminalSettings {
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum TerminalLineHeight { pub enum TerminalLineHeight {
/// Use a line height that's comfortable for reading, 1.618
#[default] #[default]
Comfortable, Comfortable,
/// Use a standard line height, 1.3. This option is useful for TUIs,
/// particularly if they use box characters
Standard, Standard,
/// Use a custom line height.
Custom(f32), Custom(f32),
} }
@ -127,17 +182,25 @@ impl TerminalLineHeight {
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum TerminalBlink { pub enum TerminalBlink {
/// Never blink the cursor, ignoring the terminal mode.
Off, Off,
/// Default the cursor blink to off, but allow the terminal to
/// set blinking.
TerminalControlled, TerminalControlled,
/// Always blink the cursor, ignoring the terminal mode.
On, On,
} }
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum Shell { pub enum Shell {
/// Use the system's default terminal configuration in /etc/passwd
System, System,
Program(String), Program(String),
WithArguments { program: String, args: Vec<String> }, WithArguments {
program: String,
args: Vec<String>,
},
} }
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
@ -150,8 +213,15 @@ pub enum AlternateScroll {
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum WorkingDirectory { pub enum WorkingDirectory {
/// Use the current file's project directory. Will Fallback to the
/// first project directory strategy if unsuccessful.
CurrentProjectDirectory, CurrentProjectDirectory,
/// Use the first project in this workspace's directory.
FirstProjectDirectory, FirstProjectDirectory,
/// Always use this platform's home directory (if it can be found).
AlwaysHome, AlwaysHome,
/// Slways use a specific directory. This value will be shell expanded.
/// If this path is not a valid directory the terminal will default to
/// this platform's home directory (if it can be found).
Always { directory: String }, Always { directory: String },
} }

View file

@ -451,6 +451,18 @@ impl TerminalElement {
} }
}); });
let interactive_text_bounds = InteractiveBounds {
bounds,
stacking_order: cx.stacking_order().clone(),
};
if interactive_text_bounds.visibly_contains(&cx.mouse_position(), cx) {
if self.can_navigate_to_selected_word && last_hovered_word.is_some() {
cx.set_cursor_style(gpui::CursorStyle::PointingHand)
} else {
cx.set_cursor_style(gpui::CursorStyle::IBeam)
}
}
let hyperlink_tooltip = last_hovered_word.clone().map(|hovered_word| { let hyperlink_tooltip = last_hovered_word.clone().map(|hovered_word| {
div() div()
.size_full() .size_full()

View file

@ -19,7 +19,7 @@ use workspace::{
dock::{DockPosition, Panel, PanelEvent}, dock::{DockPosition, Panel, PanelEvent},
item::Item, item::Item,
pane, pane,
ui::Icon, ui::IconName,
DraggedTab, Pane, Workspace, DraggedTab, Pane, Workspace,
}; };
@ -71,7 +71,7 @@ impl TerminalPanel {
h_stack() h_stack()
.gap_2() .gap_2()
.child( .child(
IconButton::new("plus", Icon::Plus) IconButton::new("plus", IconName::Plus)
.icon_size(IconSize::Small) .icon_size(IconSize::Small)
.on_click(move |_, cx| { .on_click(move |_, cx| {
terminal_panel terminal_panel
@ -82,10 +82,10 @@ impl TerminalPanel {
) )
.child({ .child({
let zoomed = pane.is_zoomed(); let zoomed = pane.is_zoomed();
IconButton::new("toggle_zoom", Icon::Maximize) IconButton::new("toggle_zoom", IconName::Maximize)
.icon_size(IconSize::Small) .icon_size(IconSize::Small)
.selected(zoomed) .selected(zoomed)
.selected_icon(Icon::Minimize) .selected_icon(IconName::Minimize)
.on_click(cx.listener(|pane, _, cx| { .on_click(cx.listener(|pane, _, cx| {
pane.toggle_zoom(&workspace::ToggleZoom, cx); pane.toggle_zoom(&workspace::ToggleZoom, cx);
})) }))
@ -477,8 +477,8 @@ impl Panel for TerminalPanel {
"TerminalPanel" "TerminalPanel"
} }
fn icon(&self, _cx: &WindowContext) -> Option<Icon> { fn icon(&self, _cx: &WindowContext) -> Option<IconName> {
Some(Icon::Terminal) Some(IconName::Terminal)
} }
fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> { fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {

View file

@ -2,8 +2,6 @@ mod persistence;
pub mod terminal_element; pub mod terminal_element;
pub mod terminal_panel; pub mod terminal_panel;
// todo!()
// use crate::terminal_element::TerminalElement;
use editor::{scroll::autoscroll::Autoscroll, Editor}; use editor::{scroll::autoscroll::Autoscroll, Editor};
use gpui::{ use gpui::{
div, impl_actions, overlay, AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle, div, impl_actions, overlay, AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle,
@ -22,7 +20,7 @@ use terminal::{
Clear, Copy, Event, MaybeNavigationTarget, Paste, ShowCharacterPalette, Terminal, Clear, Copy, Event, MaybeNavigationTarget, Paste, ShowCharacterPalette, Terminal,
}; };
use terminal_element::TerminalElement; use terminal_element::TerminalElement;
use ui::{h_stack, prelude::*, ContextMenu, Icon, IconElement, Label}; use ui::{h_stack, prelude::*, ContextMenu, Icon, IconName, Label};
use util::{paths::PathLikeWithPosition, ResultExt}; use util::{paths::PathLikeWithPosition, ResultExt};
use workspace::{ use workspace::{
item::{BreadcrumbText, Item, ItemEvent}, item::{BreadcrumbText, Item, ItemEvent},
@ -692,7 +690,7 @@ impl Item for TerminalView {
let title = self.terminal().read(cx).title(true); let title = self.terminal().read(cx).title(true);
h_stack() h_stack()
.gap_2() .gap_2()
.child(IconElement::new(Icon::Terminal)) .child(Icon::new(IconName::Terminal))
.child(Label::new(title).color(if selected { .child(Label::new(title).color(if selected {
Color::Default Color::Default
} else { } else {

View file

@ -73,13 +73,6 @@ impl ActiveTheme for AppContext {
} }
} }
// todo!()
// impl<'a> ActiveTheme for WindowContext<'a> {
// fn theme(&self) -> &Arc<Theme> {
// &ThemeSettings::get_global(self.app()).active_theme
// }
// }
pub struct ThemeFamily { pub struct ThemeFamily {
pub id: String, pub id: String,
pub name: SharedString, pub name: SharedString,

View file

@ -2,7 +2,7 @@ use gpui::{AnyView, DefiniteLength};
use crate::{prelude::*, IconPosition, KeyBinding}; use crate::{prelude::*, IconPosition, KeyBinding};
use crate::{ use crate::{
ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, Icon, IconSize, Label, LineHeightStyle, ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, IconName, IconSize, Label, LineHeightStyle,
}; };
use super::button_icon::ButtonIcon; use super::button_icon::ButtonIcon;
@ -14,11 +14,11 @@ pub struct Button {
label_color: Option<Color>, label_color: Option<Color>,
label_size: Option<LabelSize>, label_size: Option<LabelSize>,
selected_label: Option<SharedString>, selected_label: Option<SharedString>,
icon: Option<Icon>, icon: Option<IconName>,
icon_position: Option<IconPosition>, icon_position: Option<IconPosition>,
icon_size: Option<IconSize>, icon_size: Option<IconSize>,
icon_color: Option<Color>, icon_color: Option<Color>,
selected_icon: Option<Icon>, selected_icon: Option<IconName>,
key_binding: Option<KeyBinding>, key_binding: Option<KeyBinding>,
} }
@ -54,7 +54,7 @@ impl Button {
self self
} }
pub fn icon(mut self, icon: impl Into<Option<Icon>>) -> Self { pub fn icon(mut self, icon: impl Into<Option<IconName>>) -> Self {
self.icon = icon.into(); self.icon = icon.into();
self self
} }
@ -74,7 +74,7 @@ impl Button {
self self
} }
pub fn selected_icon(mut self, icon: impl Into<Option<Icon>>) -> Self { pub fn selected_icon(mut self, icon: impl Into<Option<IconName>>) -> Self {
self.selected_icon = icon.into(); self.selected_icon = icon.into();
self self
} }

View file

@ -1,4 +1,4 @@
use crate::{prelude::*, Icon, IconElement, IconSize}; use crate::{prelude::*, Icon, IconName, IconSize};
/// An icon that appears within a button. /// An icon that appears within a button.
/// ///
@ -6,17 +6,17 @@ use crate::{prelude::*, Icon, IconElement, IconSize};
/// or as a standalone icon, like in [`IconButton`](crate::IconButton). /// or as a standalone icon, like in [`IconButton`](crate::IconButton).
#[derive(IntoElement)] #[derive(IntoElement)]
pub(super) struct ButtonIcon { pub(super) struct ButtonIcon {
icon: Icon, icon: IconName,
size: IconSize, size: IconSize,
color: Color, color: Color,
disabled: bool, disabled: bool,
selected: bool, selected: bool,
selected_icon: Option<Icon>, selected_icon: Option<IconName>,
selected_style: Option<ButtonStyle>, selected_style: Option<ButtonStyle>,
} }
impl ButtonIcon { impl ButtonIcon {
pub fn new(icon: Icon) -> Self { pub fn new(icon: IconName) -> Self {
Self { Self {
icon, icon,
size: IconSize::default(), size: IconSize::default(),
@ -44,7 +44,7 @@ impl ButtonIcon {
self self
} }
pub fn selected_icon(mut self, icon: impl Into<Option<Icon>>) -> Self { pub fn selected_icon(mut self, icon: impl Into<Option<IconName>>) -> Self {
self.selected_icon = icon.into(); self.selected_icon = icon.into();
self self
} }
@ -88,6 +88,6 @@ impl RenderOnce for ButtonIcon {
self.color self.color
}; };
IconElement::new(icon).size(self.size).color(icon_color) Icon::new(icon).size(self.size).color(icon_color)
} }
} }

View file

@ -1,21 +1,21 @@
use gpui::{AnyView, DefiniteLength}; use gpui::{AnyView, DefiniteLength};
use crate::{prelude::*, SelectableButton}; use crate::{prelude::*, SelectableButton};
use crate::{ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, Icon, IconSize}; use crate::{ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, IconName, IconSize};
use super::button_icon::ButtonIcon; use super::button_icon::ButtonIcon;
#[derive(IntoElement)] #[derive(IntoElement)]
pub struct IconButton { pub struct IconButton {
base: ButtonLike, base: ButtonLike,
icon: Icon, icon: IconName,
icon_size: IconSize, icon_size: IconSize,
icon_color: Color, icon_color: Color,
selected_icon: Option<Icon>, selected_icon: Option<IconName>,
} }
impl IconButton { impl IconButton {
pub fn new(id: impl Into<ElementId>, icon: Icon) -> Self { pub fn new(id: impl Into<ElementId>, icon: IconName) -> Self {
Self { Self {
base: ButtonLike::new(id), base: ButtonLike::new(id),
icon, icon,
@ -35,7 +35,7 @@ impl IconButton {
self self
} }
pub fn selected_icon(mut self, icon: impl Into<Option<Icon>>) -> Self { pub fn selected_icon(mut self, icon: impl Into<Option<IconName>>) -> Self {
self.selected_icon = icon.into(); self.selected_icon = icon.into();
self self
} }

View file

@ -1,7 +1,7 @@
use gpui::{div, prelude::*, ElementId, IntoElement, Styled, WindowContext}; use gpui::{div, prelude::*, ElementId, IntoElement, Styled, WindowContext};
use crate::prelude::*; use crate::prelude::*;
use crate::{Color, Icon, IconElement, Selection}; use crate::{Color, Icon, IconName, Selection};
pub type CheckHandler = Box<dyn Fn(&Selection, &mut WindowContext) + 'static>; pub type CheckHandler = Box<dyn Fn(&Selection, &mut WindowContext) + 'static>;
@ -47,7 +47,7 @@ impl RenderOnce for Checkbox {
let group_id = format!("checkbox_group_{:?}", self.id); let group_id = format!("checkbox_group_{:?}", self.id);
let icon = match self.checked { let icon = match self.checked {
Selection::Selected => Some(IconElement::new(Icon::Check).size(IconSize::Small).color( Selection::Selected => Some(Icon::new(IconName::Check).size(IconSize::Small).color(
if self.disabled { if self.disabled {
Color::Disabled Color::Disabled
} else { } else {
@ -55,7 +55,7 @@ impl RenderOnce for Checkbox {
}, },
)), )),
Selection::Indeterminate => Some( Selection::Indeterminate => Some(
IconElement::new(Icon::Dash) Icon::new(IconName::Dash)
.size(IconSize::Small) .size(IconSize::Small)
.color(if self.disabled { .color(if self.disabled {
Color::Disabled Color::Disabled

Some files were not shown because too many files have changed in this diff Show more