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

View file

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

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

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

View file

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

View file

@ -57,12 +57,28 @@ pub struct AssistantSettings {
pub default_open_ai_model: OpenAIModel,
}
/// Assistant panel settings
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
pub struct AssistantSettingsContent {
/// Whether to show the assistant panel button in the status bar.
///
/// Default: true
pub button: Option<bool>,
/// Where to dock the assistant.
///
/// Default: right
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>,
/// Default height in pixels when the assistant is docked to the bottom.
///
/// Default: 320
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>,
}

View file

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

View file

@ -10,6 +10,7 @@ use gpui::{
};
use isahc::AsyncBody;
use schemars::JsonSchema;
use serde::Deserialize;
use serde_derive::Serialize;
use smol::io::AsyncReadExt;
@ -61,18 +62,27 @@ struct JsonRelease {
struct AutoUpdateSetting(bool);
/// Whether or not to automatically check for updates.
///
/// Default: true
#[derive(Clone, Default, JsonSchema, Deserialize, Serialize)]
#[serde(transparent)]
struct AutoUpdateSettingOverride(Option<bool>);
impl Settings for AutoUpdateSetting {
const KEY: Option<&'static str> = Some("auto_update");
type FileContent = Option<bool>;
type FileContent = AutoUpdateSettingOverride;
fn load(
default_value: &Option<bool>,
user_values: &[&Option<bool>],
default_value: &Self::FileContent,
user_values: &[&Self::FileContent],
_: &mut AppContext,
) -> Result<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 util::channel::ReleaseChannel;
use workspace::ui::{h_stack, v_stack, Icon, IconElement, Label, StyledExt};
use workspace::ui::{h_stack, v_stack, Icon, IconName, Label, StyledExt};
pub struct UpdateNotification {
version: SemanticVersion,
@ -30,7 +30,7 @@ impl Render for UpdateNotification {
.child(
div()
.id("cancel")
.child(IconElement::new(Icon::Close))
.child(Icon::new(IconName::Close))
.cursor_pointer()
.on_click(cx.listener(|this, _, cx| this.dismiss(&menu::Cancel, cx))),
),

View file

@ -67,7 +67,10 @@ impl Render for Breadcrumbs {
})
.tooltip(|cx| Tooltip::for_action("Show symbol outline", &outline::Toggle, cx)),
),
None => element.child(breadcrumbs_stack),
None => element
// Match the height of the `ButtonLike` in the other arm.
.h(rems(22. / 16.))
.child(breadcrumbs_stack),
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -140,6 +140,22 @@ impl ChannelRole {
Guest | Banned => false,
}
}
pub fn can_edit_projects(&self) -> bool {
use ChannelRole::*;
match self {
Admin | Member => true,
Guest | Banned => false,
}
}
pub fn can_read_projects(&self) -> bool {
use ChannelRole::*;
match self {
Admin | Member | Guest => true,
Banned => false,
}
}
}
impl From<proto::ChannelRole> for ChannelRole {

View file

@ -777,13 +777,129 @@ impl Database {
.await
}
pub async fn project_collaborators(
pub async fn check_user_is_project_host(
&self,
project_id: ProjectId,
connection_id: ConnectionId,
) -> Result<()> {
let room_id = self.room_id_for_project(project_id).await?;
self.room_transaction(room_id, |tx| async move {
project_collaborator::Entity::find()
.filter(
Condition::all()
.add(project_collaborator::Column::ProjectId.eq(project_id))
.add(project_collaborator::Column::IsHost.eq(true))
.add(project_collaborator::Column::ConnectionId.eq(connection_id.id))
.add(
project_collaborator::Column::ConnectionServerId
.eq(connection_id.owner_id),
),
)
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("failed to read project host"))?;
Ok(())
})
.await
.map(|guard| guard.into_inner())
}
pub async fn host_for_read_only_project_request(
&self,
project_id: ProjectId,
connection_id: ConnectionId,
) -> Result<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,
project_id: ProjectId,
connection_id: ConnectionId,
) -> Result<RoomGuard<Vec<ProjectCollaborator>>> {
let room_id = self.room_id_for_project(project_id).await?;
self.room_transaction(room_id, |tx| async move {
let current_participant = room_participant::Entity::find()
.filter(room_participant::Column::RoomId.eq(room_id))
.filter(room_participant::Column::AnsweringConnectionId.eq(connection_id.id))
.one(&*tx)
.await?
.ok_or_else(|| anyhow!("no such room"))?;
if !current_participant
.role
.map_or(false, |role| role.can_edit_projects())
{
Err(anyhow!("not authorized to edit projects"))?;
}
let collaborators = project_collaborator::Entity::find()
.filter(project_collaborator::Column::ProjectId.eq(project_id))
.all(&*tx)

View file

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

View file

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

View file

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

View file

@ -82,5 +82,13 @@ async fn test_channel_guests(
project_b.read_with(cx_b, |project, _| project.remote_id()),
Some(project_id),
);
assert!(project_b.read_with(cx_b, |project, _| project.is_read_only()))
assert!(project_b.read_with(cx_b, |project, _| project.is_read_only()));
assert!(project_b
.update(cx_b, |project, cx| {
let worktree_id = project.worktrees().next().unwrap().read(cx).id();
project.create_entry((worktree_id, "b.txt"), false, cx)
})
.await
.is_err())
}

View file

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

View file

@ -2,7 +2,7 @@ use crate::{
db::{tests::TestDb, NewUserParams, UserId},
executor::Executor,
rpc::{Server, CLEANUP_TIMEOUT, RECONNECT_TIMEOUT},
AppState,
AppState, Config,
};
use anyhow::anyhow;
use call::ActiveCall;
@ -414,7 +414,19 @@ impl TestServer {
Arc::new(AppState {
db: test_db.db().clone(),
live_kit_client: Some(Arc::new(fake_server.create_api_client())),
config: Default::default(),
config: Config {
http_port: 0,
database_url: "".into(),
database_max_connections: 0,
api_token: "".into(),
invite_link_prefix: "".into(),
live_kit_server: None,
live_kit_key: None,
live_kit_secret: None,
rust_log: None,
log_json: None,
zed_environment: "test".into(),
},
})
}
}

View file

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

View file

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

View file

@ -1,16 +1,19 @@
use std::{sync::Arc, time::Duration};
use channel::{ChannelId, ChannelMembership, ChannelStore, MessageParams};
use client::UserId;
use collections::HashMap;
use editor::{AnchorRangeExt, Editor};
use editor::{AnchorRangeExt, Editor, EditorElement, EditorStyle};
use gpui::{
AsyncWindowContext, FocusableView, IntoElement, Model, Render, SharedString, Task, View,
ViewContext, WeakView,
AsyncWindowContext, FocusableView, FontStyle, FontWeight, HighlightStyle, IntoElement, Model,
Render, SharedString, Task, TextStyle, View, ViewContext, WeakView, WhiteSpace,
};
use language::{language_settings::SoftWrap, Buffer, BufferSnapshot, LanguageRegistry};
use lazy_static::lazy_static;
use project::search::SearchQuery;
use std::{sync::Arc, time::Duration};
use workspace::item::ItemHandle;
use settings::Settings;
use theme::ThemeSettings;
use ui::prelude::*;
const MENTIONS_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(50);
@ -181,7 +184,14 @@ impl MessageEditor {
}
editor.clear_highlights::<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;
@ -196,8 +206,39 @@ impl MessageEditor {
}
impl Render for MessageEditor {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
self.editor.to_any()
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let settings = ThemeSettings::get_global(cx);
let text_style = TextStyle {
color: if self.editor.read(cx).read_only(cx) {
cx.theme().colors().text_disabled
} else {
cx.theme().colors().text
},
font_family: settings.ui_font.family.clone(),
font_features: settings.ui_font.features,
font_size: rems(0.875).into(),
font_weight: FontWeight::NORMAL,
font_style: FontStyle::Normal,
line_height: relative(1.3).into(),
background_color: None,
underline: None,
white_space: WhiteSpace::Normal,
};
div()
.w_full()
.px_2()
.py_1()
.bg(cx.theme().colors().editor_background)
.rounded_md()
.child(EditorElement::new(
&self.editor,
EditorStyle {
local_player: cx.theme().players().local(),
text: text_style,
..Default::default()
},
))
}
}
@ -205,7 +246,7 @@ impl Render for MessageEditor {
mod tests {
use super::*;
use client::{Client, User, UserStore};
use gpui::{Context as _, TestAppContext, VisualContext as _};
use gpui::TestAppContext;
use language::{Language, LanguageConfig};
use rpc::proto;
use settings::SettingsStore;

View file

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

View file

@ -168,7 +168,7 @@ impl Render for ChannelModal {
.w_px()
.flex_1()
.gap_1()
.child(IconElement::new(Icon::Hash).size(IconSize::Medium))
.child(Icon::new(IconName::Hash).size(IconSize::Medium))
.child(Label::new(channel_name)),
)
.child(
@ -406,7 +406,7 @@ impl PickerDelegate for ChannelModalDelegate {
Some(ChannelRole::Guest) => Some(Label::new("Guest")),
_ => None,
})
.child(IconButton::new("ellipsis", Icon::Ellipsis))
.child(IconButton::new("ellipsis", IconName::Ellipsis))
.children(
if let (Some((menu, _)), true) = (&self.context_menu, selected) {
Some(

View file

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

View file

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

View file

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

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

View file

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

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)]
pub struct PanelSettingsContent {
/// Whether to show the panel button in the status bar.
///
/// Default: true
pub button: Option<bool>,
/// Where to dock the panel.
///
/// Default: left
pub dock: Option<DockPosition>,
/// Default width of the panel in pixels.
///
/// Default: 240
pub default_width: Option<f32>,
}

View file

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

View file

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

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -7,7 +7,7 @@ use db::kvp::KEY_VALUE_STORE;
use editor::{Editor, EditorEvent};
use futures::AsyncReadExt;
use gpui::{
div, red, rems, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model,
div, rems, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model,
PromptLevel, Render, Task, View, ViewContext,
};
use isahc::Request;
@ -179,14 +179,13 @@ impl FeedbackModal {
editor
});
// Moved here because providing it inline breaks rustfmt
let placeholder_text =
"You can use markdown to organize your feedback with code and links.";
let feedback_editor = cx.new_view(|cx| {
let mut editor = Editor::for_buffer(buffer, Some(project.clone()), cx);
editor.set_placeholder_text(placeholder_text, cx);
// editor.set_show_gutter(false, cx);
editor.set_placeholder_text(
"You can use markdown to organize your feedback with code and links.",
cx,
);
editor.set_show_gutter(false, cx);
editor.set_vertical_scroll_margin(5, cx);
editor
});
@ -422,10 +421,6 @@ impl Render for FeedbackModal {
let open_community_repo =
cx.listener(|_, _, cx| cx.dispatch_action(Box::new(OpenZedCommunityRepo)));
// Moved this here because providing it inline breaks rustfmt
let provide_an_email_address =
"Provide an email address if you want us to be able to reply.";
v_stack()
.elevation_3(cx)
.key_context("GiveFeedback")
@ -434,11 +429,8 @@ impl Render for FeedbackModal {
.max_w(rems(96.))
.h(rems(32.))
.p_4()
.gap_4()
.child(v_stack().child(
// TODO: Add Headline component to `ui2`
div().text_xl().child("Share Feedback"),
))
.gap_2()
.child(Headline::new("Share Feedback"))
.child(
Label::new(if self.character_count < *FEEDBACK_CHAR_LIMIT.start() {
format!(
@ -468,17 +460,26 @@ impl Render for FeedbackModal {
.child(self.feedback_editor.clone()),
)
.child(
h_stack()
.bg(cx.theme().colors().editor_background)
.p_2()
.border()
.rounded_md()
.border_color(if self.valid_email_address() {
cx.theme().colors().border
} else {
red()
})
.child(self.email_address_editor.clone()),
v_stack()
.gap_1()
.child(
h_stack()
.bg(cx.theme().colors().editor_background)
.p_2()
.border()
.rounded_md()
.border_color(if self.valid_email_address() {
cx.theme().colors().border
} else {
cx.theme().status().error_border
})
.child(self.email_address_editor.clone()),
)
.child(
Label::new("Provide an email address if you want us to be able to reply.")
.size(LabelSize::Small)
.color(Color::Muted),
),
)
.child(
h_stack()
@ -487,7 +488,7 @@ impl Render for FeedbackModal {
.child(
Button::new("community_repository", "Community Repository")
.style(ButtonStyle::Transparent)
.icon(Icon::ExternalLink)
.icon(IconName::ExternalLink)
.icon_position(IconPosition::End)
.icon_size(IconSize::Small)
.on_click(open_community_repo),
@ -515,12 +516,7 @@ impl Render for FeedbackModal {
this.submit(cx).detach();
}))
.tooltip(move |cx| {
Tooltip::with_meta(
"Submit feedback to the Zed team.",
None,
provide_an_email_address,
cx,
)
Tooltip::text("Submit feedback to the Zed team.", cx)
})
.when(!self.can_submit(), |this| this.disabled(true)),
),

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -32,7 +32,7 @@ impl PlatformDisplay for TestDisplay {
}
fn as_any(&self) -> &dyn std::any::Any {
todo!()
unimplemented!()
}
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 {
fn background_executor(&self) -> BackgroundExecutor {
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 {
fn boxed_clone(&self) -> Box<dyn gpui::Action> {
todo!()
unimplemented!()
}
fn as_any(&self) -> &dyn std::any::Any {
todo!()
unimplemented!()
}
fn partial_eq(&self, _action: &dyn gpui::Action) -> bool {
todo!()
unimplemented!()
}
fn name(&self) -> &str {
todo!()
unimplemented!()
}
fn debug_name() -> &'static str
where
Self: Sized,
{
todo!()
unimplemented!()
}
fn build(_value: serde_json::Value) -> anyhow::Result<Box<dyn gpui::Action>>
where
Self: Sized,
{
todo!()
unimplemented!()
}
}
}

View file

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

View file

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

View file

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

View file

@ -54,7 +54,13 @@ impl FocusableView 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 {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {

View file

@ -4732,7 +4732,8 @@ impl Project {
} else {
return Task::ready(Err(anyhow!("worktree not found for symbol")));
};
let symbol_abs_path = worktree_abs_path.join(&symbol.path.path);
let symbol_abs_path = resolve_path(worktree_abs_path, &symbol.path.path);
let symbol_uri = if let Ok(uri) = lsp::Url::from_file_path(symbol_abs_path) {
uri
} else {
@ -6581,7 +6582,14 @@ impl Project {
let removed = *change == PathChange::Removed;
let abs_path = worktree.absolutize(path);
settings_contents.push(async move {
(settings_dir, (!removed).then_some(fs.load(&abs_path).await))
(
settings_dir,
if removed {
None
} else {
Some(async move { fs.load(&abs_path?).await }.await)
},
)
});
}
}
@ -8718,6 +8726,20 @@ fn relativize_path(base: &Path, path: &Path) -> PathBuf {
components.iter().map(|c| c.as_os_str()).collect()
}
fn resolve_path(base: &Path, path: &Path) -> PathBuf {
let mut result = base.to_path_buf();
for component in path.components() {
match component {
Component::ParentDir => {
result.pop();
}
Component::CurDir => (),
_ => result.push(component),
}
}
result
}
impl Item for Buffer {
fn entry_id(&self, cx: &AppContext) -> Option<ProjectEntryId> {
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)]
pub struct ProjectSettings {
/// Configuration for language servers.
///
/// The following settings can be overriden for specific language servers:
/// - initialization_options
/// To override settings for a language, add an entry for that language server's
/// name to the lsp value.
/// Default: null
#[serde(default)]
pub lsp: HashMap<Arc<str>, LspSettings>,
/// Configuration for Git-related features
#[serde(default)]
pub git: GitSettings,
/// Completely ignore files matching globs from `file_scan_exclusions`
///
/// Default: [
/// "**/.git",
/// "**/.svn",
/// "**/.hg",
/// "**/CVS",
/// "**/.DS_Store",
/// "**/Thumbs.db",
/// "**/.classpath",
/// "**/.settings"
/// ]
#[serde(default)]
pub file_scan_exclusions: Option<Vec<String>>,
}
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
pub struct GitSettings {
/// Whether or not to show the git gutter.
///
/// Default: tracked_files
pub git_gutter: Option<GitGutterSetting>,
pub gutter_debounce: Option<u64>,
}
@ -24,8 +48,10 @@ pub struct GitSettings {
#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum GitGutterSetting {
/// Show git gutter in tracked files.
#[default]
TrackedFiles,
/// Hide git gutter
Hide,
}

View file

@ -4278,6 +4278,75 @@ fn test_glob_literal_prefix() {
assert_eq!(glob_literal_prefix("foo/bar/baz.js"), "foo/bar/baz.js");
}
#[gpui::test]
async fn test_create_entry(cx: &mut gpui::TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor().clone());
fs.insert_tree(
"/one/two",
json!({
"three": {
"a.txt": "",
"four": {}
},
"c.rs": ""
}),
)
.await;
let project = Project::test(fs.clone(), ["/one/two/three".as_ref()], cx).await;
project
.update(cx, |project, cx| {
let id = project.worktrees().next().unwrap().read(cx).id();
project.create_entry((id, "b.."), true, cx)
})
.unwrap()
.await
.unwrap();
// Can't create paths outside the project
let result = project
.update(cx, |project, cx| {
let id = project.worktrees().next().unwrap().read(cx).id();
project.create_entry((id, "../../boop"), true, cx)
})
.await;
assert!(result.is_err());
// Can't create paths with '..'
let result = project
.update(cx, |project, cx| {
let id = project.worktrees().next().unwrap().read(cx).id();
project.create_entry((id, "four/../beep"), true, cx)
})
.await;
assert!(result.is_err());
assert_eq!(
fs.paths(true),
vec![
PathBuf::from("/"),
PathBuf::from("/one"),
PathBuf::from("/one/two"),
PathBuf::from("/one/two/c.rs"),
PathBuf::from("/one/two/three"),
PathBuf::from("/one/two/three/a.txt"),
PathBuf::from("/one/two/three/b.."),
PathBuf::from("/one/two/three/four"),
]
);
// And we cannot open buffers with '..'
let result = project
.update(cx, |project, cx| {
let id = project.worktrees().next().unwrap().read(cx).id();
project.open_buffer((id, "../c.rs"), cx)
})
.await;
assert!(result.is_err())
}
async fn search(
project: &Model<Project>,
query: SearchQuery,

View file

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

View file

@ -30,7 +30,7 @@ use std::{
sync::Arc,
};
use theme::ThemeSettings;
use ui::{prelude::*, v_stack, ContextMenu, IconElement, KeyBinding, Label, ListItem};
use ui::{prelude::*, v_stack, ContextMenu, Icon, KeyBinding, Label, ListItem};
use unicase::UniCase;
use util::{maybe, ResultExt, TryFutureExt};
use workspace::{
@ -1403,7 +1403,7 @@ impl ProjectPanel {
.indent_step_size(px(settings.indent_size))
.selected(is_selected)
.child(if let Some(icon) = &icon {
div().child(IconElement::from_path(icon.to_string()).color(Color::Muted))
div().child(Icon::from_path(icon.to_string()).color(Color::Muted))
} else {
div().size(IconSize::default().rems()).invisible()
})
@ -1433,6 +1433,9 @@ impl ProjectPanel {
}))
.on_secondary_mouse_down(cx.listener(
move |this, event: &MouseDownEvent, cx| {
// Stop propagation to prevent the catch-all context menu for the project
// panel from being deployed.
cx.stop_propagation();
this.deploy_context_menu(event.position, entry_id, cx);
},
)),
@ -1515,6 +1518,16 @@ impl Render for ProjectPanel {
el.on_action(cx.listener(Self::reveal_in_finder))
.on_action(cx.listener(Self::open_in_terminal))
})
.on_mouse_down(
MouseButton::Right,
cx.listener(move |this, event: &MouseDownEvent, cx| {
// When deploying the context menu anywhere below the last project entry,
// act as if the user clicked the root of the last worktree.
if let Some(entry_id) = this.last_worktree_root_id {
this.deploy_context_menu(event.position, entry_id, cx);
}
}),
)
.track_focus(&self.focus_handle)
.child(
uniform_list(
@ -1577,7 +1590,7 @@ impl Render for DraggedProjectEntryView {
.indent_level(self.details.depth)
.indent_step_size(px(settings.indent_size))
.child(if let Some(icon) = &self.details.icon {
div().child(IconElement::from_path(icon.to_string()))
div().child(Icon::from_path(icon.to_string()))
} else {
div()
})
@ -1627,8 +1640,8 @@ impl Panel for ProjectPanel {
cx.notify();
}
fn icon(&self, _: &WindowContext) -> Option<ui::Icon> {
Some(ui::Icon::FileTree)
fn icon(&self, _: &WindowContext) -> Option<ui::IconName> {
Some(ui::IconName::FileTree)
}
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)]
pub struct ProjectPanelSettingsContent {
/// Customise default width (in pixels) taken by project panel
///
/// Default: 240
pub default_width: Option<f32>,
/// The position of project panel
///
/// Default: left
pub dock: Option<ProjectPanelDockPosition>,
/// Whether to show file icons in the project panel.
///
/// Default: true
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>,
/// Whether to show the git status in the project panel.
///
/// Default: true
pub git_status: Option<bool>,
/// Amount of indentation (in pixels) for nested items.
///
/// Default: 20
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>,
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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
///See: [8 bit ansi color](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit).
/// Generates the RGB channels in [0, 5] for a given index into the 6x6x6 ANSI color cube.
/// See: [8 bit ANSI color](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit).
///
///Wikipedia gives a formula for calculating the index for a given color:
/// Wikipedia gives a formula for calculating the index for a given color:
///
///index = 16 + 36 × r + 6 × g + b (0 ≤ r, g, b ≤ 5)
/// ```
/// index = 16 + 36 × r + 6 × g + b (0 ≤ r, g, b ≤ 5)
/// ```
///
///This function does the reverse, calculating the r, g, and b components from a given index.
/// This function does the reverse, calculating the `r`, `g`, and `b` components from a given index.
fn rgb_for_index(i: &u8) -> (u8, u8, u8) {
debug_assert!((&16..=&231).contains(&i));
let i = i - 16;

View file

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

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| {
div()
.size_full()

View file

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

View file

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

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 id: String,
pub name: SharedString,

View file

@ -2,7 +2,7 @@ use gpui::{AnyView, DefiniteLength};
use crate::{prelude::*, IconPosition, KeyBinding};
use crate::{
ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, Icon, IconSize, Label, LineHeightStyle,
ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, IconName, IconSize, Label, LineHeightStyle,
};
use super::button_icon::ButtonIcon;
@ -14,11 +14,11 @@ pub struct Button {
label_color: Option<Color>,
label_size: Option<LabelSize>,
selected_label: Option<SharedString>,
icon: Option<Icon>,
icon: Option<IconName>,
icon_position: Option<IconPosition>,
icon_size: Option<IconSize>,
icon_color: Option<Color>,
selected_icon: Option<Icon>,
selected_icon: Option<IconName>,
key_binding: Option<KeyBinding>,
}
@ -54,7 +54,7 @@ impl Button {
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
}
@ -74,7 +74,7 @@ impl Button {
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
}

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

View file

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

View file

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

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