Merge branch 'main' into migrations-on-server-start
This commit is contained in:
commit
bebb528656
130 changed files with 1924 additions and 1289 deletions
21
Cargo.lock
generated
21
Cargo.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 |
|
@ -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
12
crates/assets/Cargo.toml
Normal 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
|
|
@ -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};
|
|
@ -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()
|
||||
),
|
||||
}
|
||||
|
|
|
@ -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>,
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)?,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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))),
|
||||
),
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>,
|
||||
}
|
||||
|
||||
|
|
|
@ -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>,
|
||||
}
|
||||
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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]]
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -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(),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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" }
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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))),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)),
|
||||
|
|
|
@ -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| {
|
||||
|
|
|
@ -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);
|
||||
|
|
52
crates/collab_ui/src/notifications/collab_notification.rs
Normal file
52
crates/collab_ui/src/notifications/collab_notification.rs
Normal 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),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
})),
|
||||
)
|
||||
)))),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(", ")))
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
3
crates/collab_ui/src/notifications/stories.rs
Normal file
3
crates/collab_ui/src/notifications/stories.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
mod collab_notification;
|
||||
|
||||
pub use collab_notification::*;
|
|
@ -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")),
|
||||
),
|
||||
)),
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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>,
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
5
crates/copilot_ui/src/copilot_ui.rs
Normal file
5
crates/copilot_ui/src/copilot_ui.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
mod copilot_button;
|
||||
mod sign_in;
|
||||
|
||||
pub use copilot_button::*;
|
||||
pub use sign_in::*;
|
183
crates/copilot_ui/src/sign_in.rs
Normal file
183
crates/copilot_ui/src/sign_in.rs
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}),
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>,
|
||||
}
|
||||
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>,
|
||||
}
|
||||
|
||||
|
|
|
@ -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(),
|
||||
));
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)),
|
||||
),
|
||||
|
|
|
@ -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| {
|
||||
|
|
|
@ -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!())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
@ -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::*;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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()
|
||||
|
|
25
crates/gpui/src/shared_url.rs
Normal file
25
crates/gpui/src/shared_url.rs
Normal 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())
|
||||
}
|
||||
}
|
|
@ -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!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>,
|
||||
}
|
||||
|
||||
|
|
|
@ -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")]
|
||||
|
|
|
@ -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»])) }",
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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>,
|
||||
}
|
||||
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
});
|
||||
|
||||
|
|
|
@ -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>,
|
||||
}
|
||||
|
||||
|
|
|
@ -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" }
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 },
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue