Detailed changes
@@ -1688,12 +1688,11 @@ dependencies = [
"settings",
"smol",
"theme",
- "ui",
"util",
]
[[package]]
-name = "copilot_button"
+name = "copilot_ui"
version = "0.1.0"
dependencies = [
"anyhow",
@@ -1706,6 +1705,7 @@ dependencies = [
"settings",
"smol",
"theme",
+ "ui",
"util",
"workspace",
"zed_actions",
@@ -9547,7 +9547,7 @@ dependencies = [
"collections",
"command_palette",
"copilot",
- "copilot_button",
+ "copilot_ui",
"ctor",
"db",
"diagnostics",
@@ -16,7 +16,7 @@ members = [
"crates/collections",
"crates/command_palette",
"crates/copilot",
- "crates/copilot_button",
+ "crates/copilot_ui",
"crates/db",
"crates/refineable",
"crates/refineable/derive_refineable",
@@ -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};
@@ -331,7 +332,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 +373,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();
}
}
}
@@ -0,0 +1,5 @@
+mod copilot_button;
+mod sign_in;
+
+pub use copilot_button::*;
+pub use sign_in::*;
@@ -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, Icon, 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(Icon::ZedXCopilot.path())
+ .text_color(cx.theme().colors().icon),
+ )
+ .child(prompt)
+ }
+}
@@ -14,6 +14,7 @@ pub use crate::visible_on_hover::*;
pub use crate::{h_stack, v_stack};
pub use crate::{Button, ButtonSize, ButtonStyle, IconButton, SelectableButton};
pub use crate::{ButtonCommon, Color, StyledExt};
+pub use crate::{Headline, HeadlineSize};
pub use crate::{Icon, IconElement, IconPosition, IconSize};
pub use crate::{Label, LabelCommon, LabelSize, LineHeightStyle};
pub use theme::ActiveTheme;
@@ -1,4 +1,8 @@
-use gpui::{rems, Rems};
+use gpui::{
+ div, rems, IntoElement, ParentElement, Rems, RenderOnce, SharedString, Styled, WindowContext,
+};
+use settings::Settings;
+use theme::{ActiveTheme, ThemeSettings};
#[derive(Debug, Default, Clone)]
pub enum UiTextSize {
@@ -33,3 +37,69 @@ impl UiTextSize {
}
}
}
+
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
+pub enum HeadlineSize {
+ XSmall,
+ Small,
+ #[default]
+ Medium,
+ Large,
+ XLarge,
+}
+
+impl HeadlineSize {
+ pub fn size(self) -> Rems {
+ match self {
+ // Based on the Major Second scale
+ Self::XSmall => rems(0.88),
+ Self::Small => rems(1.0),
+ Self::Medium => rems(1.125),
+ Self::Large => rems(1.27),
+ Self::XLarge => rems(1.43),
+ }
+ }
+
+ pub fn line_height(self) -> Rems {
+ match self {
+ Self::XSmall => rems(1.6),
+ Self::Small => rems(1.6),
+ Self::Medium => rems(1.6),
+ Self::Large => rems(1.6),
+ Self::XLarge => rems(1.6),
+ }
+ }
+}
+
+#[derive(IntoElement)]
+pub struct Headline {
+ size: HeadlineSize,
+ text: SharedString,
+}
+
+impl RenderOnce for Headline {
+ fn render(self, cx: &mut WindowContext) -> impl IntoElement {
+ let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone();
+
+ div()
+ .font(ui_font)
+ .line_height(self.size.line_height())
+ .text_size(self.size.size())
+ .text_color(cx.theme().colors().text)
+ .child(self.text)
+ }
+}
+
+impl Headline {
+ pub fn new(text: impl Into<SharedString>) -> Self {
+ Self {
+ size: HeadlineSize::default(),
+ text: text.into(),
+ }
+ }
+
+ pub fn size(mut self, size: HeadlineSize) -> Self {
+ self.size = size;
+ self
+ }
+}
@@ -30,7 +30,7 @@ command_palette = { path = "../command_palette" }
client = { path = "../client" }
# clock = { path = "../clock" }
copilot = { path = "../copilot" }
-copilot_button = { path = "../copilot_button" }
+copilot_ui = { path = "../copilot_ui" }
diagnostics = { path = "../diagnostics" }
db = { path = "../db" }
editor = { path = "../editor" }
@@ -120,8 +120,7 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
// cx.add_view(|cx| CollabTitlebarItem::new(workspace, &workspace_handle, cx));
// workspace.set_titlebar_item(collab_titlebar_item.into_any(), cx);
- let copilot =
- cx.new_view(|cx| copilot_button::CopilotButton::new(app_state.fs.clone(), cx));
+ let copilot = cx.new_view(|cx| copilot_ui::CopilotButton::new(app_state.fs.clone(), cx));
let diagnostic_summary =
cx.new_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx));
let activity_indicator =