assets/icons/maybe_link_out.svg → assets/icons/link_out_12.svg 🔗
Mikayla Maki created
Added correct icons
Added a new 'Toast' action which allows other crates to easily pop toasts with an optional click action
assets/icons/link_out_12.svg | 0
crates/copilot/src/copilot.rs | 76 ++++++++++++-
crates/copilot/src/sign_in.rs | 20 +--
crates/copilot_button/src/copilot_button.rs | 87 ++++++++++++---
crates/theme/src/theme.rs | 4
crates/workspace/src/notifications.rs | 40 ++++++-
crates/workspace/src/workspace.rs | 89 ++++++++++++++++
styles/src/styleTree/copilot.ts | 20 ---
styles/src/styleTree/simpleMessageNotification.ts | 16 ++
9 files changed, 279 insertions(+), 73 deletions(-)
@@ -28,7 +28,10 @@ const COPILOT_AUTH_NAMESPACE: &'static str = "copilot_auth";
actions!(copilot_auth, [SignIn, SignOut]);
const COPILOT_NAMESPACE: &'static str = "copilot";
-actions!(copilot, [NextSuggestion, PreviousSuggestion, Toggle]);
+actions!(
+ copilot,
+ [NextSuggestion, PreviousSuggestion, Toggle, Reinstall]
+);
pub fn init(client: Arc<Client>, node_runtime: Arc<NodeRuntime>, cx: &mut MutableAppContext) {
let copilot = cx.add_model(|cx| Copilot::start(client.http_client(), node_runtime, cx));
@@ -46,6 +49,13 @@ pub fn init(client: Arc<Client>, node_runtime: Arc<NodeRuntime>, cx: &mut Mutabl
.detach_and_log_err(cx);
});
+ cx.add_global_action(|_: &Reinstall, cx| {
+ let copilot = Copilot::global(cx).unwrap();
+ copilot
+ .update(cx, |copilot, cx| copilot.reinstall(cx))
+ .detach();
+ });
+
cx.observe(&copilot, |handle, cx| {
let status = handle.read(cx).status();
cx.update_global::<collections::CommandPaletteFilter, _, _>(
@@ -73,7 +83,7 @@ pub fn init(client: Arc<Client>, node_runtime: Arc<NodeRuntime>, cx: &mut Mutabl
enum CopilotServer {
Disabled,
Starting {
- _task: Shared<Task<()>>,
+ task: Shared<Task<()>>,
},
Error(Arc<str>),
Started {
@@ -97,9 +107,11 @@ enum SignInStatus {
SignedOut,
}
-#[derive(Debug, PartialEq, Eq)]
+#[derive(Debug, Clone)]
pub enum Status {
- Starting,
+ Starting {
+ task: Shared<Task<()>>,
+ },
Error(Arc<str>),
Disabled,
SignedOut,
@@ -123,6 +135,8 @@ pub struct Completion {
}
pub struct Copilot {
+ http: Arc<dyn HttpClient>,
+ node_runtime: Arc<NodeRuntime>,
server: CopilotServer,
}
@@ -131,6 +145,13 @@ impl Entity for Copilot {
}
impl Copilot {
+ pub fn starting_task(&self) -> Option<Shared<Task<()>>> {
+ match self.server {
+ CopilotServer::Starting { ref task } => Some(task.clone()),
+ _ => None,
+ }
+ }
+
pub fn global(cx: &AppContext) -> Option<ModelHandle<Self>> {
if cx.has_global::<ModelHandle<Self>>() {
Some(cx.global::<ModelHandle<Self>>().clone())
@@ -159,10 +180,12 @@ impl Copilot {
}
})
.shared();
- this.server = CopilotServer::Starting { _task: start_task }
+ this.server = CopilotServer::Starting { task: start_task };
+ cx.notify();
}
} else {
- this.server = CopilotServer::Disabled
+ this.server = CopilotServer::Disabled;
+ cx.notify();
}
}
})
@@ -178,10 +201,14 @@ impl Copilot {
.shared();
Self {
- server: CopilotServer::Starting { _task: start_task },
+ http,
+ node_runtime,
+ server: CopilotServer::Starting { task: start_task },
}
} else {
Self {
+ http,
+ node_runtime,
server: CopilotServer::Disabled,
}
}
@@ -332,6 +359,27 @@ impl Copilot {
}
}
+ fn reinstall(&mut self, cx: &mut ModelContext<Self>) -> Task<()> {
+ let start_task = cx
+ .spawn({
+ let http = self.http.clone();
+ let node_runtime = self.node_runtime.clone();
+ move |this, cx| async move {
+ clear_copilot_dir().await;
+ Self::start_language_server(http, node_runtime, this, cx).await
+ }
+ })
+ .shared();
+
+ self.server = CopilotServer::Starting {
+ task: start_task.clone(),
+ };
+
+ cx.notify();
+
+ cx.foreground().spawn(start_task)
+ }
+
pub fn completion<T>(
&self,
buffer: &ModelHandle<Buffer>,
@@ -391,7 +439,7 @@ impl Copilot {
pub fn status(&self) -> Status {
match &self.server {
- CopilotServer::Starting { .. } => Status::Starting,
+ CopilotServer::Starting { task } => Status::Starting { task: task.clone() },
CopilotServer::Disabled => Status::Disabled,
CopilotServer::Error(error) => Status::Error(error.clone()),
CopilotServer::Started { status, .. } => match status {
@@ -501,8 +549,12 @@ fn completion_from_lsp(completion: request::Completion, buffer: &BufferSnapshot)
}
}
+async fn clear_copilot_dir() {
+ remove_matching(&paths::COPILOT_DIR, |_| true).await
+}
+
async fn get_copilot_lsp(http: Arc<dyn HttpClient>) -> anyhow::Result<PathBuf> {
- const SERVER_PATH: &'static str = "agent.js";
+ const SERVER_PATH: &'static str = "dist/agent.js";
///Check for the latest copilot language server and download it if we haven't already
async fn fetch_latest(http: Arc<dyn HttpClient>) -> anyhow::Result<PathBuf> {
@@ -514,6 +566,10 @@ async fn get_copilot_lsp(http: Arc<dyn HttpClient>) -> anyhow::Result<PathBuf> {
let server_path = version_dir.join(SERVER_PATH);
if fs::metadata(&server_path).await.is_err() {
+ // Copilot LSP looks for this dist dir specifcially, so lets add it in.
+ let dist_dir = version_dir.join("dist");
+ fs::create_dir_all(dist_dir.as_path()).await?;
+
let url = &release
.assets
.get(0)
@@ -526,7 +582,7 @@ async fn get_copilot_lsp(http: Arc<dyn HttpClient>) -> anyhow::Result<PathBuf> {
.map_err(|err| anyhow!("error downloading copilot release: {}", err))?;
let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut()));
let archive = Archive::new(decompressed_bytes);
- archive.unpack(version_dir).await?;
+ archive.unpack(dist_dir).await?;
remove_matching(&paths::COPILOT_DIR, |entry| entry != version_dir).await;
}
@@ -298,9 +298,7 @@ impl CopilotCodeVerification {
.with_children([
Flex::row()
.with_children([
- theme::ui::svg(&style.auth.copilot_icon).boxed(),
- theme::ui::icon(&style.auth.plus_icon).boxed(),
- theme::ui::svg(&style.auth.zed_icon).boxed(),
+ theme::ui::svg(&style.auth.copilot_plus_zed_icon).boxed()
])
.boxed(),
Flex::column()
@@ -362,9 +360,7 @@ impl CopilotCodeVerification {
.with_children([
Flex::row()
.with_children([
- theme::ui::svg(&style.auth.copilot_icon).boxed(),
- theme::ui::icon(&style.auth.plus_icon).boxed(),
- theme::ui::svg(&style.auth.zed_icon).boxed(),
+ theme::ui::svg(&style.auth.copilot_plus_zed_icon).boxed()
])
.boxed(),
Label::new("Copilot Enabled!", style.auth.enable_text.clone()).boxed(),
@@ -410,9 +406,7 @@ impl CopilotCodeVerification {
.with_children([
Flex::row()
.with_children([
- theme::ui::svg(&style.auth.copilot_icon).boxed(),
- theme::ui::icon(&style.auth.plus_icon).boxed(),
- theme::ui::svg(&style.auth.zed_icon).boxed(),
+ theme::ui::svg(&style.auth.copilot_plus_zed_icon).boxed()
])
.boxed(),
Flex::column()
@@ -483,13 +477,13 @@ impl View for CopilotCodeVerification {
}
fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox {
- let style = cx.global::<Settings>().theme.copilot.clone();
+ let style = cx.global::<Settings>().theme.clone();
match &self.status {
Status::SigningIn {
prompt: Some(prompt),
- } => Self::render_prompting_modal(&prompt, &style, cx),
- Status::Unauthorized => Self::render_unauthorized_modal(&style, cx),
- Status::Authorized => Self::render_enabled_modal(&style, cx),
+ } => Self::render_prompting_modal(&prompt, &style.copilot, cx),
+ Status::Unauthorized => Self::render_unauthorized_modal(&style.copilot, cx),
+ Status::Authorized => Self::render_enabled_modal(&style.copilot, cx),
_ => Empty::new().boxed(),
}
}
@@ -8,12 +8,15 @@ use gpui::{
};
use settings::{settings_file::SettingsFile, Settings};
use workspace::{
- item::ItemHandle, notifications::simple_message_notification::OsOpen, StatusItemView,
+ item::ItemHandle, notifications::simple_message_notification::OsOpen, DismissToast,
+ StatusItemView,
};
-use copilot::{Copilot, SignIn, SignOut, Status};
+use copilot::{Copilot, Reinstall, SignIn, SignOut, Status};
const COPILOT_SETTINGS_URL: &str = "https://github.com/settings/copilot";
+const COPILOT_STARTING_TOAST_ID: usize = 1337;
+const COPILOT_ERROR_TOAST_ID: usize = 1338;
#[derive(Clone, PartialEq)]
pub struct DeployCopilotMenu;
@@ -36,7 +39,7 @@ impl_internal_actions!(
DeployCopilotMenu,
DeployCopilotModal,
ToggleCopilotForLanguage,
- ToggleCopilotGlobally
+ ToggleCopilotGlobally,
]
);
@@ -93,14 +96,18 @@ impl View for CopilotButton {
}
let theme = settings.theme.clone();
- let active = self.popup_menu.read(cx).visible() /* || modal.is_shown */;
- let authorized = Copilot::global(cx).unwrap().read(cx).status() == Status::Authorized;
+ let active = self.popup_menu.read(cx).visible();
+ let status = Copilot::global(cx).unwrap().read(cx).status();
+
let enabled = self.editor_enabled.unwrap_or(settings.copilot_on(None));
+ let view_id = cx.view_id();
+
Stack::new()
.with_child(
MouseEventHandler::<Self>::new(0, cx, {
let theme = theme.clone();
+ let status = status.clone();
move |state, _cx| {
let style = theme
.workspace
@@ -112,14 +119,16 @@ impl View for CopilotButton {
Flex::row()
.with_child(
Svg::new({
- if authorized {
- if enabled {
- "icons/copilot_16.svg"
- } else {
- "icons/copilot_disabled_16.svg"
+ match status {
+ Status::Error(_) => "icons/copilot_error_16.svg",
+ Status::Authorized => {
+ if enabled {
+ "icons/copilot_16.svg"
+ } else {
+ "icons/copilot_disabled_16.svg"
+ }
}
- } else {
- "icons/copilot_init_16.svg"
+ _ => "icons/copilot_init_16.svg",
}
})
.with_color(style.icon_color)
@@ -136,11 +145,50 @@ impl View for CopilotButton {
}
})
.with_cursor_style(CursorStyle::PointingHand)
- .on_click(MouseButton::Left, move |_, cx| {
- if authorized {
- cx.dispatch_action(DeployCopilotMenu);
- } else {
- cx.dispatch_action(SignIn);
+ .on_click(MouseButton::Left, {
+ let status = status.clone();
+ move |_, cx| match status {
+ Status::Authorized => cx.dispatch_action(DeployCopilotMenu),
+ Status::Starting { ref task } => {
+ cx.dispatch_action(workspace::Toast::new(
+ COPILOT_STARTING_TOAST_ID,
+ "Copilot is starting...",
+ ));
+ let window_id = cx.window_id();
+ let task = task.to_owned();
+ cx.spawn(|mut cx| async move {
+ task.await;
+ cx.update(|cx| {
+ let status = Copilot::global(cx).unwrap().read(cx).status();
+ match status {
+ Status::Authorized => cx.dispatch_action_at(
+ window_id,
+ view_id,
+ workspace::Toast::new(
+ COPILOT_STARTING_TOAST_ID,
+ "Copilot has started!",
+ ),
+ ),
+ _ => {
+ cx.dispatch_action_at(
+ window_id,
+ view_id,
+ DismissToast::new(COPILOT_STARTING_TOAST_ID),
+ );
+ cx.dispatch_global_action(SignIn)
+ }
+ }
+ })
+ })
+ .detach();
+ }
+ Status::Error(ref e) => cx.dispatch_action(workspace::Toast::new_action(
+ COPILOT_ERROR_TOAST_ID,
+ format!("Copilot can't be started: {}", e),
+ "Reinstall Copilot",
+ Reinstall,
+ )),
+ _ => cx.dispatch_action(SignIn),
}
})
.with_tooltip::<Self, _>(
@@ -195,9 +243,9 @@ impl CopilotButton {
let locally_enabled = self.editor_enabled.unwrap_or(settings.copilot_on(None));
menu_options.push(ContextMenuItem::item_for_view(
if locally_enabled {
- "Pause Copilot for file"
+ "Pause Copilot for this file"
} else {
- "Resume Copilot for file"
+ "Resume Copilot for this file"
},
*view_id,
copilot::Toggle,
@@ -244,6 +292,7 @@ impl CopilotButton {
Label::new("Copilot Settings", style.label.clone()).boxed(),
theme::ui::icon(icon_style.style_for(state, false)).boxed(),
])
+ .align_children_center()
.boxed()
},
),
@@ -131,9 +131,7 @@ pub struct CopilotAuth {
pub instruction_text: TextStyle,
pub cta_button: ButtonStyle,
pub content_width: f32,
- pub copilot_icon: SvgStyle,
- pub plus_icon: IconStyle,
- pub zed_icon: SvgStyle,
+ pub copilot_plus_zed_icon: SvgStyle,
pub device_code_group: ContainerStyle,
pub github_group: ContainerStyle,
pub header_group: ContainerStyle,
@@ -97,7 +97,7 @@ impl Workspace {
let notification = build_notification(cx);
cx.subscribe(¬ification, move |this, handle, event, cx| {
if handle.read(cx).should_dismiss_notification_on_event(event) {
- this.dismiss_notification(type_id, id, cx);
+ this.dismiss_notification_internal(type_id, id, cx);
}
})
.detach();
@@ -107,7 +107,18 @@ impl Workspace {
}
}
- fn dismiss_notification(&mut self, type_id: TypeId, id: usize, cx: &mut ViewContext<Self>) {
+ pub fn dismiss_notification<V: Notification>(&mut self, id: usize, cx: &mut ViewContext<Self>) {
+ let type_id = TypeId::of::<V>();
+
+ self.dismiss_notification_internal(type_id, id, cx)
+ }
+
+ fn dismiss_notification_internal(
+ &mut self,
+ type_id: TypeId,
+ id: usize,
+ cx: &mut ViewContext<Self>,
+ ) {
self.notifications
.retain(|(existing_type_id, existing_id, _)| {
if (*existing_type_id, *existing_id) == (type_id, id) {
@@ -183,6 +194,18 @@ pub mod simple_message_notification {
}
}
+ pub fn new_boxed_action<S1: Into<Cow<'static, str>>, S2: Into<Cow<'static, str>>>(
+ message: S1,
+ click_action: Box<dyn Action>,
+ click_message: S2,
+ ) -> Self {
+ Self {
+ message: message.into(),
+ click_action: Some(click_action),
+ click_message: Some(click_message.into()),
+ }
+ }
+
pub fn new<S1: Into<Cow<'static, str>>, A: Action, S2: Into<Cow<'static, str>>>(
message: S1,
click_action: A,
@@ -270,9 +293,13 @@ pub mod simple_message_notification {
let style = theme.action_message.style_for(state, false);
if let Some(click_message) = click_message {
Some(
- Text::new(click_message, style.text.clone())
- .contained()
- .with_style(style.container)
+ Flex::row()
+ .with_child(
+ Text::new(click_message, style.text.clone())
+ .contained()
+ .with_style(style.container)
+ .boxed(),
+ )
.boxed(),
)
} else {
@@ -288,7 +315,8 @@ pub mod simple_message_notification {
.on_up(MouseButton::Left, |_, _| {})
.on_click(MouseButton::Left, move |_, cx| {
if let Some(click_action) = click_action.as_ref() {
- cx.dispatch_any_action(click_action.boxed_clone())
+ cx.dispatch_any_action(click_action.boxed_clone());
+ cx.dispatch_action(CancelMessageNotification)
}
})
.with_cursor_style(if has_click_action {
@@ -41,10 +41,10 @@ use gpui::{
impl_actions, impl_internal_actions,
keymap_matcher::KeymapContext,
platform::{CursorStyle, WindowOptions},
- AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
- MouseButton, MutableAppContext, PathPromptOptions, Platform, PromptLevel, RenderContext,
- SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle,
- WindowBounds,
+ Action, AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelContext,
+ ModelHandle, MouseButton, MutableAppContext, PathPromptOptions, Platform, PromptLevel,
+ RenderContext, SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle,
+ WeakViewHandle, WindowBounds,
};
use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ProjectItem};
use language::LanguageRegistry;
@@ -165,6 +165,67 @@ pub struct OpenProjectEntryInPane {
project_entry: ProjectEntryId,
}
+pub struct Toast {
+ id: usize,
+ msg: Cow<'static, str>,
+ click: Option<(Cow<'static, str>, Box<dyn Action>)>,
+}
+
+impl Toast {
+ pub fn new<I: Into<Cow<'static, str>>>(id: usize, msg: I) -> Self {
+ Toast {
+ id,
+ msg: msg.into(),
+ click: None,
+ }
+ }
+
+ pub fn new_action<I1: Into<Cow<'static, str>>, I2: Into<Cow<'static, str>>>(
+ id: usize,
+ msg: I1,
+ click_msg: I2,
+ action: impl Action,
+ ) -> Self {
+ Toast {
+ id,
+ msg: msg.into(),
+ click: Some((click_msg.into(), Box::new(action))),
+ }
+ }
+}
+
+impl PartialEq for Toast {
+ fn eq(&self, other: &Self) -> bool {
+ self.id == other.id
+ && self.msg == other.msg
+ && self.click.is_some() == other.click.is_some()
+ }
+}
+
+impl Clone for Toast {
+ fn clone(&self) -> Self {
+ Toast {
+ id: self.id,
+ msg: self.msg.to_owned(),
+ click: self
+ .click
+ .as_ref()
+ .map(|(msg, click)| (msg.to_owned(), click.boxed_clone())),
+ }
+ }
+}
+
+#[derive(Clone, PartialEq)]
+pub struct DismissToast {
+ id: usize,
+}
+
+impl DismissToast {
+ pub fn new(id: usize) -> Self {
+ DismissToast { id }
+ }
+}
+
pub type WorkspaceId = i64;
impl_internal_actions!(
@@ -178,6 +239,8 @@ impl_internal_actions!(
SplitWithItem,
SplitWithProjectEntry,
OpenProjectEntryInPane,
+ Toast,
+ DismissToast
]
);
impl_actions!(workspace, [ActivatePane]);
@@ -353,6 +416,24 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
.detach();
});
+ cx.add_action(|workspace: &mut Workspace, alert: &Toast, cx| {
+ workspace.dismiss_notification::<MessageNotification>(alert.id, cx);
+ workspace.show_notification(alert.id, cx, |cx| {
+ cx.add_view(|_cx| match &alert.click {
+ Some((click_msg, action)) => MessageNotification::new_boxed_action(
+ alert.msg.clone(),
+ action.boxed_clone(),
+ click_msg.clone(),
+ ),
+ None => MessageNotification::new_message(alert.msg.clone()),
+ })
+ })
+ });
+
+ cx.add_action(|workspace: &mut Workspace, alert: &DismissToast, cx| {
+ workspace.dismiss_notification::<MessageNotification>(alert.id, cx);
+ });
+
let client = &app_state.client;
client.add_view_request_handler(Workspace::handle_follow);
client.add_view_message_handler(Workspace::handle_unfollow);
@@ -31,13 +31,13 @@ export default function copilot(colorScheme: ColorScheme) {
return {
outLinkIcon: {
- icon: svg(foreground(layer, "variant"), "icons/maybe_link_out.svg", 12, 12),
+ icon: svg(foreground(layer, "variant"), "icons/link_out_12.svg", 12, 12),
container: {
cornerRadius: 6,
- padding: { top: 6, bottom: 6, left: 6, right: 6 },
+ padding: { left: 6 },
},
hover: {
- icon: svg(foreground(layer, "hovered"), "icons/maybe_link_out.svg", 12, 12)
+ icon: svg(foreground(layer, "hovered"), "icons/link_out_12.svg", 12, 12)
},
},
modal: {
@@ -103,19 +103,7 @@ export default function copilot(colorScheme: ColorScheme) {
right: 0
}
},
- copilotIcon: svg(foreground(layer, "default"), "icons/github-copilot-dummy.svg", 32, 32),
- plusIcon: {
- icon: svg(foreground(layer, "default"), "icons/plus_12.svg", 12, 12),
- container: {
- padding: {
- top: 12,
- bottom: 12,
- left: 12,
- right: 12,
- }
- }
- },
- zedIcon: svg(foreground(layer, "default"), "icons/logo_96.svg", 32, 32),
+ copilotPlusZedIcon: svg(foreground(layer, "default"), "icons/zed_plus_copilot_32.svg", 32, 92),
enableText: text(layer, "sans", { size: "md" }),
enableGroup: {
margin: {
@@ -1,5 +1,5 @@
import { ColorScheme } from "../themes/common/colorScheme"
-import { foreground, text } from "./components"
+import { background, border, foreground, text } from "./components"
const headerPadding = 8
@@ -14,9 +14,21 @@ export default function simpleMessageNotification(
},
actionMessage: {
...text(layer, "sans", { size: "xs" }),
+ border: border(layer, "active"),
+ cornerRadius: 4,
+ padding: {
+ top: 3,
+ bottom: 3,
+ left: 7,
+ right: 7,
+ },
+
+
margin: { left: headerPadding, top: 6, bottom: 6 },
hover: {
- color: foreground(layer, "hovered"),
+ ...text(layer, "sans", "default", { size: "xs" }),
+ background: background(layer, "hovered"),
+ border: border(layer, "active"),
},
},
dismissButton: {