mirror of
https://github.com/zed-industries/zed.git
synced 2024-11-07 20:39:04 +03:00
Add status bar icon reflecting copilot state to Zed status bar
This commit is contained in:
parent
8fac32e1eb
commit
cc7c5b416c
19
Cargo.lock
generated
19
Cargo.lock
generated
@ -1356,6 +1356,23 @@ dependencies = [
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "copilot_button"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"context_menu",
|
||||
"copilot",
|
||||
"editor",
|
||||
"futures 0.3.25",
|
||||
"gpui",
|
||||
"settings",
|
||||
"smol",
|
||||
"theme",
|
||||
"util",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.3"
|
||||
@ -5924,6 +5941,7 @@ dependencies = [
|
||||
"gpui",
|
||||
"json_comments",
|
||||
"postage",
|
||||
"pretty_assertions",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
@ -8507,6 +8525,7 @@ dependencies = [
|
||||
"command_palette",
|
||||
"context_menu",
|
||||
"copilot",
|
||||
"copilot_button",
|
||||
"ctor",
|
||||
"db",
|
||||
"diagnostics",
|
||||
|
@ -14,6 +14,7 @@ members = [
|
||||
"crates/command_palette",
|
||||
"crates/context_menu",
|
||||
"crates/copilot",
|
||||
"crates/copilot_button",
|
||||
"crates/db",
|
||||
"crates/diagnostics",
|
||||
"crates/drag_and_drop",
|
||||
|
5
assets/icons/maybe_link_out.svg
Normal file
5
assets/icons/maybe_link_out.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5.5 1H7.5H8.75C8.88807 1 9 1.11193 9 1.25V4.5" stroke="#838994" stroke-linecap="round"/>
|
||||
<path d="M3.64645 5.64645C3.45118 5.84171 3.45118 6.15829 3.64645 6.35355C3.84171 6.54882 4.15829 6.54882 4.35355 6.35355L3.64645 5.64645ZM8.64645 0.646447L3.64645 5.64645L4.35355 6.35355L9.35355 1.35355L8.64645 0.646447Z" fill="#838994"/>
|
||||
<path d="M7.5 6.5V9C7.5 9.27614 7.27614 9.5 7 9.5H1C0.723858 9.5 0.5 9.27614 0.5 9V3C0.5 2.72386 0.723858 2.5 1 2.5H3.5" stroke="#838994" stroke-linecap="round"/>
|
||||
</svg>
|
After Width: | Height: | Size: 605 B |
@ -301,25 +301,13 @@ impl CollabTitlebarItem {
|
||||
.with_style(item_style.container)
|
||||
.boxed()
|
||||
})),
|
||||
ContextMenuItem::Item {
|
||||
label: "Sign out".into(),
|
||||
action: Box::new(SignOut),
|
||||
},
|
||||
ContextMenuItem::Item {
|
||||
label: "Send Feedback".into(),
|
||||
action: Box::new(feedback::feedback_editor::GiveFeedback),
|
||||
},
|
||||
ContextMenuItem::item("Sign out", SignOut),
|
||||
ContextMenuItem::item("Send Feedback", feedback::feedback_editor::GiveFeedback),
|
||||
]
|
||||
} else {
|
||||
vec![
|
||||
ContextMenuItem::Item {
|
||||
label: "Sign in".into(),
|
||||
action: Box::new(SignIn),
|
||||
},
|
||||
ContextMenuItem::Item {
|
||||
label: "Send Feedback".into(),
|
||||
action: Box::new(feedback::feedback_editor::GiveFeedback),
|
||||
},
|
||||
ContextMenuItem::item("Sign in", SignIn),
|
||||
ContextMenuItem::item("Send Feedback", feedback::feedback_editor::GiveFeedback),
|
||||
]
|
||||
};
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
use gpui::{
|
||||
elements::*, geometry::vector::Vector2F, impl_internal_actions, keymap_matcher::KeymapContext,
|
||||
platform::CursorStyle, Action, AnyViewHandle, AppContext, Axis, Entity, MouseButton,
|
||||
MutableAppContext, RenderContext, SizeConstraint, Subscription, View, ViewContext,
|
||||
MouseState, MutableAppContext, RenderContext, SizeConstraint, Subscription, View, ViewContext,
|
||||
};
|
||||
use menu::*;
|
||||
use settings::Settings;
|
||||
@ -24,20 +24,71 @@ pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_action(ContextMenu::cancel);
|
||||
}
|
||||
|
||||
type ContextMenuItemBuilder = Box<dyn Fn(&mut MouseState, &theme::ContextMenuItem) -> ElementBox>;
|
||||
|
||||
pub enum ContextMenuItemLabel {
|
||||
String(Cow<'static, str>),
|
||||
Element(ContextMenuItemBuilder),
|
||||
}
|
||||
|
||||
pub enum ContextMenuAction {
|
||||
ParentAction {
|
||||
action: Box<dyn Action>,
|
||||
},
|
||||
ViewAction {
|
||||
action: Box<dyn Action>,
|
||||
for_view: usize,
|
||||
},
|
||||
}
|
||||
|
||||
impl ContextMenuAction {
|
||||
fn id(&self) -> TypeId {
|
||||
match self {
|
||||
ContextMenuAction::ParentAction { action } => action.id(),
|
||||
ContextMenuAction::ViewAction { action, .. } => action.id(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub enum ContextMenuItem {
|
||||
Item {
|
||||
label: Cow<'static, str>,
|
||||
action: Box<dyn Action>,
|
||||
label: ContextMenuItemLabel,
|
||||
action: ContextMenuAction,
|
||||
},
|
||||
Static(StaticItem),
|
||||
Separator,
|
||||
}
|
||||
|
||||
impl ContextMenuItem {
|
||||
pub fn element_item(label: ContextMenuItemBuilder, action: impl 'static + Action) -> Self {
|
||||
Self::Item {
|
||||
label: ContextMenuItemLabel::Element(label),
|
||||
action: ContextMenuAction::ParentAction {
|
||||
action: Box::new(action),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn item(label: impl Into<Cow<'static, str>>, action: impl 'static + Action) -> Self {
|
||||
Self::Item {
|
||||
label: label.into(),
|
||||
action: Box::new(action),
|
||||
label: ContextMenuItemLabel::String(label.into()),
|
||||
action: ContextMenuAction::ParentAction {
|
||||
action: Box::new(action),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn item_for_view(
|
||||
label: impl Into<Cow<'static, str>>,
|
||||
view_id: usize,
|
||||
action: impl 'static + Action,
|
||||
) -> Self {
|
||||
Self::Item {
|
||||
label: ContextMenuItemLabel::String(label.into()),
|
||||
action: ContextMenuAction::ViewAction {
|
||||
action: Box::new(action),
|
||||
for_view: view_id,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -168,7 +219,15 @@ impl ContextMenu {
|
||||
fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
|
||||
if let Some(ix) = self.selected_index {
|
||||
if let Some(ContextMenuItem::Item { action, .. }) = self.items.get(ix) {
|
||||
cx.dispatch_any_action(action.boxed_clone());
|
||||
match action {
|
||||
ContextMenuAction::ParentAction { action } => {
|
||||
cx.dispatch_any_action(action.boxed_clone())
|
||||
}
|
||||
ContextMenuAction::ViewAction { action, for_view } => {
|
||||
let window_id = cx.window_id();
|
||||
cx.dispatch_any_action_at(window_id, *for_view, action.boxed_clone())
|
||||
}
|
||||
};
|
||||
self.reset(cx);
|
||||
}
|
||||
}
|
||||
@ -278,10 +337,17 @@ impl ContextMenu {
|
||||
Some(ix) == self.selected_index,
|
||||
);
|
||||
|
||||
Label::new(label.to_string(), style.label.clone())
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.boxed()
|
||||
match label {
|
||||
ContextMenuItemLabel::String(label) => {
|
||||
Label::new(label.to_string(), style.label.clone())
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.boxed()
|
||||
}
|
||||
ContextMenuItemLabel::Element(element) => {
|
||||
element(&mut Default::default(), style)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ContextMenuItem::Static(f) => f(cx),
|
||||
@ -306,9 +372,18 @@ impl ContextMenu {
|
||||
&mut Default::default(),
|
||||
Some(ix) == self.selected_index,
|
||||
);
|
||||
let (action, view_id) = match action {
|
||||
ContextMenuAction::ParentAction { action } => {
|
||||
(action.boxed_clone(), self.parent_view_id)
|
||||
}
|
||||
ContextMenuAction::ViewAction { action, for_view } => {
|
||||
(action.boxed_clone(), *for_view)
|
||||
}
|
||||
};
|
||||
|
||||
KeystrokeLabel::new(
|
||||
window_id,
|
||||
self.parent_view_id,
|
||||
view_id,
|
||||
action.boxed_clone(),
|
||||
style.keystroke.container,
|
||||
style.keystroke.text.clone(),
|
||||
@ -347,22 +422,34 @@ impl ContextMenu {
|
||||
.with_children(self.items.iter().enumerate().map(|(ix, item)| {
|
||||
match item {
|
||||
ContextMenuItem::Item { label, action } => {
|
||||
let action = action.boxed_clone();
|
||||
let (action, view_id) = match action {
|
||||
ContextMenuAction::ParentAction { action } => {
|
||||
(action.boxed_clone(), self.parent_view_id)
|
||||
}
|
||||
ContextMenuAction::ViewAction { action, for_view } => {
|
||||
(action.boxed_clone(), *for_view)
|
||||
}
|
||||
};
|
||||
|
||||
MouseEventHandler::<MenuItem>::new(ix, cx, |state, _| {
|
||||
let style =
|
||||
style.item.style_for(state, Some(ix) == self.selected_index);
|
||||
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Label::new(label.clone(), style.label.clone())
|
||||
.contained()
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(match label {
|
||||
ContextMenuItemLabel::String(label) => {
|
||||
Label::new(label.clone(), style.label.clone())
|
||||
.contained()
|
||||
.boxed()
|
||||
}
|
||||
ContextMenuItemLabel::Element(element) => {
|
||||
element(state, style)
|
||||
}
|
||||
})
|
||||
.with_child({
|
||||
KeystrokeLabel::new(
|
||||
window_id,
|
||||
self.parent_view_id,
|
||||
view_id,
|
||||
action.boxed_clone(),
|
||||
style.keystroke.container,
|
||||
style.keystroke.text.clone(),
|
||||
@ -375,9 +462,12 @@ impl ContextMenu {
|
||||
.boxed()
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_up(MouseButton::Left, |_, _| {}) // Capture these events
|
||||
.on_down(MouseButton::Left, |_, _| {}) // Capture these events
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(Clicked);
|
||||
cx.dispatch_any_action(action.boxed_clone());
|
||||
let window_id = cx.window_id();
|
||||
cx.dispatch_any_action_at(window_id, view_id, action.boxed_clone());
|
||||
})
|
||||
.on_drag(MouseButton::Left, |_, _| {})
|
||||
.boxed()
|
||||
|
@ -1,4 +1,3 @@
|
||||
pub mod copilot_button;
|
||||
mod request;
|
||||
mod sign_in;
|
||||
|
||||
|
@ -1,150 +0,0 @@
|
||||
use context_menu::{ContextMenu, ContextMenuItem};
|
||||
use gpui::{
|
||||
elements::*, impl_internal_actions, CursorStyle, Element, ElementBox, Entity, MouseButton,
|
||||
MutableAppContext, RenderContext, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||
};
|
||||
use settings::Settings;
|
||||
use theme::Editor;
|
||||
use workspace::{item::ItemHandle, NewTerminal, StatusItemView};
|
||||
|
||||
use crate::{Copilot, Status};
|
||||
|
||||
const COPILOT_SETTINGS_URL: &str = "https://github.com/settings/copilot";
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct DeployCopilotMenu;
|
||||
|
||||
// TODO: Make the other code path use `get_or_insert` logic for this modal
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct DeployCopilotModal;
|
||||
|
||||
impl_internal_actions!(copilot, [DeployCopilotMenu, DeployCopilotModal]);
|
||||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_action(CopilotButton::deploy_copilot_menu);
|
||||
}
|
||||
|
||||
pub struct CopilotButton {
|
||||
popup_menu: ViewHandle<ContextMenu>,
|
||||
editor: Option<WeakViewHandle<Editor>>,
|
||||
}
|
||||
|
||||
impl Entity for CopilotButton {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for CopilotButton {
|
||||
fn ui_name() -> &'static str {
|
||||
"CopilotButton"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox {
|
||||
let settings = cx.global::<Settings>();
|
||||
|
||||
if !settings.enable_copilot_integration {
|
||||
return Empty::new().boxed();
|
||||
}
|
||||
|
||||
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 enabled = true;
|
||||
|
||||
Stack::new()
|
||||
.with_child(
|
||||
MouseEventHandler::<Self>::new(0, cx, {
|
||||
let theme = theme.clone();
|
||||
move |state, _cx| {
|
||||
let style = theme
|
||||
.workspace
|
||||
.status_bar
|
||||
.sidebar_buttons
|
||||
.item
|
||||
.style_for(state, active);
|
||||
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Svg::new({
|
||||
if authorized {
|
||||
if enabled {
|
||||
"icons/copilot_16.svg"
|
||||
} else {
|
||||
"icons/copilot_disabled_16.svg"
|
||||
}
|
||||
} else {
|
||||
"icons/copilot_init_16.svg"
|
||||
}
|
||||
})
|
||||
.with_color(style.icon_color)
|
||||
.constrained()
|
||||
.with_width(style.icon_size)
|
||||
.aligned()
|
||||
.named("copilot-icon"),
|
||||
)
|
||||
.constrained()
|
||||
.with_height(style.icon_size)
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.boxed()
|
||||
}
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
if authorized {
|
||||
cx.dispatch_action(DeployCopilotMenu);
|
||||
} else {
|
||||
cx.dispatch_action(DeployCopilotModal);
|
||||
}
|
||||
})
|
||||
.with_tooltip::<Self, _>(
|
||||
0,
|
||||
"GitHub Copilot".into(),
|
||||
None,
|
||||
theme.tooltip.clone(),
|
||||
cx,
|
||||
)
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(
|
||||
ChildView::new(&self.popup_menu, cx)
|
||||
.aligned()
|
||||
.top()
|
||||
.right()
|
||||
.boxed(),
|
||||
)
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
impl CopilotButton {
|
||||
pub fn new(cx: &mut ViewContext<Self>) -> Self {
|
||||
Self {
|
||||
popup_menu: cx.add_view(|cx| {
|
||||
let mut menu = ContextMenu::new(cx);
|
||||
menu.set_position_mode(OverlayPositionMode::Local);
|
||||
menu
|
||||
}),
|
||||
editor: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deploy_copilot_menu(&mut self, _: &DeployCopilotMenu, cx: &mut ViewContext<Self>) {
|
||||
let mut menu_options = vec![ContextMenuItem::item("New Terminal", NewTerminal)];
|
||||
|
||||
self.popup_menu.update(cx, |menu, cx| {
|
||||
menu.show(
|
||||
Default::default(),
|
||||
AnchorCorner::BottomRight,
|
||||
menu_options,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl StatusItemView for CopilotButton {
|
||||
fn set_active_pane_item(&mut self, item: Option<&dyn ItemHandle>, cx: &mut ViewContext<Self>) {
|
||||
if let Some(editor) = item.map(|item| item.act_as::<editor::Editor>(cx)) {}
|
||||
cx.notify();
|
||||
}
|
||||
}
|
3
crates/copilot/src/editor.rs
Normal file
3
crates/copilot/src/editor.rs
Normal file
@ -0,0 +1,3 @@
|
||||
use gpui::MutableAppContext;
|
||||
|
||||
fn init(cx: &mut MutableAppContext) {}
|
22
crates/copilot_button/Cargo.toml
Normal file
22
crates/copilot_button/Cargo.toml
Normal file
@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "copilot_button"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/copilot_button.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
copilot = { path = "../copilot" }
|
||||
editor = { path = "../editor" }
|
||||
context_menu = { path = "../context_menu" }
|
||||
gpui = { path = "../gpui" }
|
||||
settings = { path = "../settings" }
|
||||
theme = { path = "../theme" }
|
||||
util = { path = "../util" }
|
||||
workspace = { path = "../workspace" }
|
||||
anyhow = "1.0"
|
||||
smol = "1.2.5"
|
||||
futures = "0.3"
|
301
crates/copilot_button/src/copilot_button.rs
Normal file
301
crates/copilot_button/src/copilot_button.rs
Normal file
@ -0,0 +1,301 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use context_menu::{ContextMenu, ContextMenuItem};
|
||||
use editor::Editor;
|
||||
use gpui::{
|
||||
elements::*, impl_internal_actions, CursorStyle, Element, ElementBox, Entity, MouseButton,
|
||||
MouseState, MutableAppContext, RenderContext, Subscription, View, ViewContext, ViewHandle,
|
||||
};
|
||||
use settings::{settings_file::SettingsFile, Settings};
|
||||
use workspace::{
|
||||
item::ItemHandle, notifications::simple_message_notification::OsOpen, StatusItemView,
|
||||
};
|
||||
|
||||
use copilot::{Copilot, SignOut, Status};
|
||||
|
||||
const COPILOT_SETTINGS_URL: &str = "https://github.com/settings/copilot";
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct DeployCopilotMenu;
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct ToggleCopilotForLanguage {
|
||||
language: Arc<str>,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct ToggleCopilotGlobally;
|
||||
|
||||
// TODO: Make the other code path use `get_or_insert` logic for this modal
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct DeployCopilotModal;
|
||||
|
||||
impl_internal_actions!(
|
||||
copilot,
|
||||
[
|
||||
DeployCopilotMenu,
|
||||
DeployCopilotModal,
|
||||
ToggleCopilotForLanguage,
|
||||
ToggleCopilotGlobally
|
||||
]
|
||||
);
|
||||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_action(CopilotButton::deploy_copilot_menu);
|
||||
cx.add_action(
|
||||
|_: &mut CopilotButton, action: &ToggleCopilotForLanguage, cx| {
|
||||
let language = action.language.to_owned();
|
||||
|
||||
let current_langauge = cx.global::<Settings>().copilot_on(Some(&language));
|
||||
|
||||
SettingsFile::update(cx, move |file_contents| {
|
||||
file_contents.languages.insert(
|
||||
language.to_owned(),
|
||||
settings::EditorSettings {
|
||||
copilot: Some((!current_langauge).into()),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
})
|
||||
},
|
||||
);
|
||||
|
||||
cx.add_action(|_: &mut CopilotButton, _: &ToggleCopilotGlobally, cx| {
|
||||
let copilot_on = cx.global::<Settings>().copilot_on(None);
|
||||
|
||||
SettingsFile::update(cx, move |file_contents| {
|
||||
file_contents.editor.copilot = Some((!copilot_on).into())
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
pub struct CopilotButton {
|
||||
popup_menu: ViewHandle<ContextMenu>,
|
||||
editor_subscription: Option<(Subscription, usize)>,
|
||||
editor_enabled: Option<bool>,
|
||||
language: Option<Arc<str>>,
|
||||
}
|
||||
|
||||
impl Entity for CopilotButton {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for CopilotButton {
|
||||
fn ui_name() -> &'static str {
|
||||
"CopilotButton"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox {
|
||||
let settings = cx.global::<Settings>();
|
||||
|
||||
if !settings.enable_copilot_integration {
|
||||
return Empty::new().boxed();
|
||||
}
|
||||
|
||||
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 enabled = self.editor_enabled.unwrap_or(settings.copilot_on(None));
|
||||
|
||||
Stack::new()
|
||||
.with_child(
|
||||
MouseEventHandler::<Self>::new(0, cx, {
|
||||
let theme = theme.clone();
|
||||
move |state, _cx| {
|
||||
let style = theme
|
||||
.workspace
|
||||
.status_bar
|
||||
.sidebar_buttons
|
||||
.item
|
||||
.style_for(state, active);
|
||||
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Svg::new({
|
||||
if authorized {
|
||||
if enabled {
|
||||
"icons/copilot_16.svg"
|
||||
} else {
|
||||
"icons/copilot_disabled_16.svg"
|
||||
}
|
||||
} else {
|
||||
"icons/copilot_init_16.svg"
|
||||
}
|
||||
})
|
||||
.with_color(style.icon_color)
|
||||
.constrained()
|
||||
.with_width(style.icon_size)
|
||||
.aligned()
|
||||
.named("copilot-icon"),
|
||||
)
|
||||
.constrained()
|
||||
.with_height(style.icon_size)
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.boxed()
|
||||
}
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
if authorized {
|
||||
cx.dispatch_action(DeployCopilotMenu);
|
||||
} else {
|
||||
cx.dispatch_action(DeployCopilotModal);
|
||||
}
|
||||
})
|
||||
.with_tooltip::<Self, _>(
|
||||
0,
|
||||
"GitHub Copilot".into(),
|
||||
None,
|
||||
theme.tooltip.clone(),
|
||||
cx,
|
||||
)
|
||||
.boxed(),
|
||||
)
|
||||
.with_child(
|
||||
ChildView::new(&self.popup_menu, cx)
|
||||
.aligned()
|
||||
.top()
|
||||
.right()
|
||||
.boxed(),
|
||||
)
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
impl CopilotButton {
|
||||
pub fn new(cx: &mut ViewContext<Self>) -> Self {
|
||||
let menu = cx.add_view(|cx| {
|
||||
let mut menu = ContextMenu::new(cx);
|
||||
menu.set_position_mode(OverlayPositionMode::Local);
|
||||
menu
|
||||
});
|
||||
|
||||
cx.observe(&menu, |_, _, cx| cx.notify()).detach();
|
||||
cx.observe(&Copilot::global(cx).unwrap(), |_, _, cx| cx.notify())
|
||||
.detach();
|
||||
let this_handle = cx.handle();
|
||||
cx.observe_global::<Settings, _>(move |cx| this_handle.update(cx, |_, cx| cx.notify()))
|
||||
.detach();
|
||||
|
||||
Self {
|
||||
popup_menu: menu,
|
||||
editor_subscription: None,
|
||||
editor_enabled: None,
|
||||
language: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deploy_copilot_menu(&mut self, _: &DeployCopilotMenu, cx: &mut ViewContext<Self>) {
|
||||
let settings = cx.global::<Settings>();
|
||||
|
||||
let mut menu_options = Vec::with_capacity(6);
|
||||
|
||||
if let Some((_, view_id)) = self.editor_subscription.as_ref() {
|
||||
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"
|
||||
} else {
|
||||
"Resume Copilot for file"
|
||||
},
|
||||
*view_id,
|
||||
copilot::Toggle,
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(language) = &self.language {
|
||||
let language_enabled = settings.copilot_on(Some(language.as_ref()));
|
||||
|
||||
menu_options.push(ContextMenuItem::item(
|
||||
format!(
|
||||
"{} Copilot for {}",
|
||||
if language_enabled {
|
||||
"Disable"
|
||||
} else {
|
||||
"Enable"
|
||||
},
|
||||
language
|
||||
),
|
||||
ToggleCopilotForLanguage {
|
||||
language: language.to_owned(),
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
let globally_enabled = cx.global::<Settings>().copilot_on(None);
|
||||
menu_options.push(ContextMenuItem::item(
|
||||
if globally_enabled {
|
||||
"Disable Copilot Globally"
|
||||
} else {
|
||||
"Enable Copilot Locally"
|
||||
},
|
||||
ToggleCopilotGlobally,
|
||||
));
|
||||
|
||||
menu_options.push(ContextMenuItem::Separator);
|
||||
|
||||
let icon_style = settings.theme.copilot.out_link_icon.clone();
|
||||
menu_options.push(ContextMenuItem::element_item(
|
||||
Box::new(
|
||||
move |state: &mut MouseState, style: &theme::ContextMenuItem| {
|
||||
Flex::row()
|
||||
.with_children([
|
||||
Label::new("Copilot Settings", style.label.clone()).boxed(),
|
||||
theme::ui::icon(icon_style.style_for(state, false)).boxed(),
|
||||
])
|
||||
.boxed()
|
||||
},
|
||||
),
|
||||
OsOpen::new(COPILOT_SETTINGS_URL),
|
||||
));
|
||||
|
||||
menu_options.push(ContextMenuItem::item("Sign Out", SignOut));
|
||||
|
||||
self.popup_menu.update(cx, |menu, cx| {
|
||||
menu.show(
|
||||
Default::default(),
|
||||
AnchorCorner::BottomRight,
|
||||
menu_options,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn update_enabled(&mut self, editor: ViewHandle<Editor>, cx: &mut ViewContext<Self>) {
|
||||
let editor = editor.read(cx);
|
||||
|
||||
if let Some(enabled) = editor.copilot_state.user_enabled {
|
||||
self.editor_enabled = Some(enabled);
|
||||
cx.notify();
|
||||
return;
|
||||
}
|
||||
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
let settings = cx.global::<Settings>();
|
||||
let suggestion_anchor = editor.selections.newest_anchor().start;
|
||||
|
||||
let language_name = snapshot
|
||||
.language_at(suggestion_anchor)
|
||||
.map(|language| language.name());
|
||||
|
||||
self.language = language_name.clone();
|
||||
self.editor_enabled = Some(settings.copilot_on(language_name.as_deref()));
|
||||
cx.notify()
|
||||
}
|
||||
}
|
||||
|
||||
impl StatusItemView for CopilotButton {
|
||||
fn set_active_pane_item(&mut self, item: Option<&dyn ItemHandle>, cx: &mut ViewContext<Self>) {
|
||||
if let Some(editor) = item.map(|item| item.act_as::<Editor>(cx)).flatten() {
|
||||
self.editor_subscription =
|
||||
Some((cx.observe(&editor, Self::update_enabled), editor.id()));
|
||||
self.update_enabled(editor, cx);
|
||||
} else {
|
||||
self.language = None;
|
||||
self.editor_subscription = None;
|
||||
self.editor_enabled = None;
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
}
|
@ -510,7 +510,7 @@ pub struct Editor {
|
||||
hover_state: HoverState,
|
||||
gutter_hovered: bool,
|
||||
link_go_to_definition_state: LinkGoToDefinitionState,
|
||||
copilot_state: CopilotState,
|
||||
pub copilot_state: CopilotState,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
@ -1008,12 +1008,12 @@ impl CodeActionsMenu {
|
||||
}
|
||||
}
|
||||
|
||||
struct CopilotState {
|
||||
pub struct CopilotState {
|
||||
excerpt_id: Option<ExcerptId>,
|
||||
pending_refresh: Task<Option<()>>,
|
||||
completions: Vec<copilot::Completion>,
|
||||
active_completion_index: usize,
|
||||
user_enabled: Option<bool>,
|
||||
pub user_enabled: Option<bool>,
|
||||
}
|
||||
|
||||
impl Default for CopilotState {
|
||||
@ -2859,6 +2859,7 @@ impl Editor {
|
||||
fn next_copilot_suggestion(&mut self, _: &copilot::NextSuggestion, cx: &mut ViewContext<Self>) {
|
||||
// Auto re-enable copilot if you're asking for a suggestion
|
||||
if self.copilot_state.user_enabled == Some(false) {
|
||||
cx.notify();
|
||||
self.copilot_state.user_enabled = Some(true);
|
||||
}
|
||||
|
||||
@ -2880,6 +2881,7 @@ impl Editor {
|
||||
) {
|
||||
// Auto re-enable copilot if you're asking for a suggestion
|
||||
if self.copilot_state.user_enabled == Some(false) {
|
||||
cx.notify();
|
||||
self.copilot_state.user_enabled = Some(true);
|
||||
}
|
||||
|
||||
@ -2921,6 +2923,8 @@ impl Editor {
|
||||
} else {
|
||||
self.clear_copilot_suggestions(cx);
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn sync_suggestion(&mut self, cx: &mut ViewContext<Self>) {
|
||||
|
@ -389,6 +389,12 @@ impl ElementBox {
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for ElementBox {
|
||||
fn clone(&self) -> Self {
|
||||
ElementBox(self.0.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ElementBox> for ElementRc {
|
||||
fn from(val: ElementBox) -> Self {
|
||||
val.0
|
||||
|
@ -36,3 +36,4 @@ tree-sitter-json = "*"
|
||||
unindent = "0.1"
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
fs = { path = "../fs", features = ["test-support"] }
|
||||
pretty_assertions = "1.3.0"
|
||||
|
@ -188,17 +188,30 @@ pub enum OnOff {
|
||||
}
|
||||
|
||||
impl OnOff {
|
||||
fn as_bool(&self) -> bool {
|
||||
pub fn as_bool(&self) -> bool {
|
||||
match self {
|
||||
OnOff::On => true,
|
||||
OnOff::Off => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_bool(value: bool) -> OnOff {
|
||||
match value {
|
||||
true => OnOff::On,
|
||||
false => OnOff::Off,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<bool> for OnOff {
|
||||
fn into(self) -> bool {
|
||||
self.as_bool()
|
||||
impl From<OnOff> for bool {
|
||||
fn from(value: OnOff) -> bool {
|
||||
value.as_bool()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bool> for OnOff {
|
||||
fn from(value: bool) -> OnOff {
|
||||
OnOff::from_bool(value)
|
||||
}
|
||||
}
|
||||
|
||||
@ -928,6 +941,7 @@ fn write_settings_key(settings_content: &mut String, key_path: &[&str], new_valu
|
||||
settings_content.insert_str(first_key_start, &content);
|
||||
}
|
||||
} else {
|
||||
dbg!("here???");
|
||||
new_value = serde_json::json!({ new_key.to_string(): new_value });
|
||||
let indent_prefix_len = 4 * depth;
|
||||
let new_val = to_pretty_json(&new_value, 4, indent_prefix_len);
|
||||
@ -973,13 +987,28 @@ fn to_pretty_json(
|
||||
|
||||
pub fn update_settings_file(
|
||||
mut text: String,
|
||||
old_file_content: SettingsFileContent,
|
||||
mut old_file_content: SettingsFileContent,
|
||||
update: impl FnOnce(&mut SettingsFileContent),
|
||||
) -> String {
|
||||
let mut new_file_content = old_file_content.clone();
|
||||
|
||||
update(&mut new_file_content);
|
||||
|
||||
if new_file_content.languages.len() != old_file_content.languages.len() {
|
||||
for language in new_file_content.languages.keys() {
|
||||
old_file_content
|
||||
.languages
|
||||
.entry(language.clone())
|
||||
.or_default();
|
||||
}
|
||||
for language in old_file_content.languages.keys() {
|
||||
new_file_content
|
||||
.languages
|
||||
.entry(language.clone())
|
||||
.or_default();
|
||||
}
|
||||
}
|
||||
|
||||
let old_object = to_json_object(old_file_content);
|
||||
let new_object = to_json_object(new_file_content);
|
||||
|
||||
@ -992,6 +1021,7 @@ pub fn update_settings_file(
|
||||
for (key, old_value) in old_object.iter() {
|
||||
// We know that these two are from the same shape of object, so we can just unwrap
|
||||
let new_value = new_object.get(key).unwrap();
|
||||
|
||||
if old_value != new_value {
|
||||
match new_value {
|
||||
Value::Bool(_) | Value::Number(_) | Value::String(_) => {
|
||||
@ -1047,7 +1077,75 @@ mod tests {
|
||||
let old_json = old_json.into();
|
||||
let old_content: SettingsFileContent = serde_json::from_str(&old_json).unwrap_or_default();
|
||||
let new_json = update_settings_file(old_json, old_content, update);
|
||||
assert_eq!(new_json, expected_new_json.into());
|
||||
pretty_assertions::assert_eq!(new_json, expected_new_json.into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_copilot() {
|
||||
assert_new_settings(
|
||||
r#"
|
||||
{
|
||||
"languages": {
|
||||
"JSON": {
|
||||
"copilot": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
|settings| {
|
||||
settings.editor.copilot = Some(OnOff::On);
|
||||
},
|
||||
r#"
|
||||
{
|
||||
"copilot": "on",
|
||||
"languages": {
|
||||
"JSON": {
|
||||
"copilot": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_langauge_copilot() {
|
||||
assert_new_settings(
|
||||
r#"
|
||||
{
|
||||
"languages": {
|
||||
"JSON": {
|
||||
"copilot": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
|settings| {
|
||||
settings.languages.insert(
|
||||
"Rust".into(),
|
||||
EditorSettings {
|
||||
copilot: Some(OnOff::On),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
},
|
||||
r#"
|
||||
{
|
||||
"languages": {
|
||||
"Rust": {
|
||||
"copilot": "on"
|
||||
},
|
||||
"JSON": {
|
||||
"copilot": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -119,6 +119,7 @@ pub struct AvatarStyle {
|
||||
|
||||
#[derive(Deserialize, Default, Clone)]
|
||||
pub struct Copilot {
|
||||
pub out_link_icon: Interactive<IconStyle>,
|
||||
pub modal: ModalStyle,
|
||||
pub auth: CopilotAuth,
|
||||
}
|
||||
|
@ -141,7 +141,13 @@ pub mod simple_message_notification {
|
||||
actions!(message_notifications, [CancelMessageNotification]);
|
||||
|
||||
#[derive(Clone, Default, Deserialize, PartialEq)]
|
||||
pub struct OsOpen(pub String);
|
||||
pub struct OsOpen(pub Cow<'static, str>);
|
||||
|
||||
impl OsOpen {
|
||||
pub fn new<I: Into<Cow<'static, str>>>(url: I) -> Self {
|
||||
OsOpen(url.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl_actions!(message_notifications, [OsOpen]);
|
||||
|
||||
@ -149,7 +155,7 @@ pub mod simple_message_notification {
|
||||
cx.add_action(MessageNotification::dismiss);
|
||||
cx.add_action(
|
||||
|_workspace: &mut Workspace, open_action: &OsOpen, cx: &mut ViewContext<Workspace>| {
|
||||
cx.platform().open_url(open_action.0.as_str());
|
||||
cx.platform().open_url(open_action.0.as_ref());
|
||||
},
|
||||
)
|
||||
}
|
||||
|
@ -2690,7 +2690,7 @@ fn notify_if_database_failed(workspace: &ViewHandle<Workspace>, cx: &mut AsyncAp
|
||||
indoc::indoc! {"
|
||||
Failed to load any database file :(
|
||||
"},
|
||||
OsOpen("https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml".to_string()),
|
||||
OsOpen::new("https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml".to_string()),
|
||||
"Click to let us know about this error"
|
||||
)
|
||||
})
|
||||
@ -2712,7 +2712,7 @@ fn notify_if_database_failed(workspace: &ViewHandle<Workspace>, cx: &mut AsyncAp
|
||||
"},
|
||||
backup_path
|
||||
),
|
||||
OsOpen(backup_path.to_string()),
|
||||
OsOpen::new(backup_path.to_string()),
|
||||
"Click to show old database in finder",
|
||||
)
|
||||
})
|
||||
|
@ -29,6 +29,7 @@ context_menu = { path = "../context_menu" }
|
||||
client = { path = "../client" }
|
||||
clock = { path = "../clock" }
|
||||
copilot = { path = "../copilot" }
|
||||
copilot_button = { path = "../copilot_button" }
|
||||
diagnostics = { path = "../diagnostics" }
|
||||
db = { path = "../db" }
|
||||
editor = { path = "../editor" }
|
||||
|
@ -8,7 +8,6 @@ use breadcrumbs::Breadcrumbs;
|
||||
pub use client;
|
||||
use collab_ui::{CollabTitlebarItem, ToggleContactsMenu};
|
||||
use collections::VecDeque;
|
||||
use copilot::copilot_button::CopilotButton;
|
||||
pub use editor;
|
||||
use editor::{Editor, MultiBuffer};
|
||||
|
||||
@ -262,6 +261,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
|
||||
},
|
||||
);
|
||||
activity_indicator::init(cx);
|
||||
copilot_button::init(cx);
|
||||
call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
|
||||
settings::KeymapFileContent::load_defaults(cx);
|
||||
}
|
||||
@ -312,7 +312,7 @@ pub fn initialize_workspace(
|
||||
});
|
||||
|
||||
let toggle_terminal = cx.add_view(|cx| TerminalButton::new(workspace_handle.clone(), cx));
|
||||
let copilot = cx.add_view(|cx| CopilotButton::new(cx));
|
||||
let copilot = cx.add_view(|cx| copilot_button::CopilotButton::new(cx));
|
||||
let diagnostic_summary =
|
||||
cx.add_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace.project(), cx));
|
||||
let activity_indicator =
|
||||
|
@ -30,6 +30,16 @@ export default function copilot(colorScheme: ColorScheme) {
|
||||
};
|
||||
|
||||
return {
|
||||
outLinkIcon: {
|
||||
icon: svg(foreground(layer, "variant"), "icons/maybe_link_out.svg", 12, 12),
|
||||
container: {
|
||||
cornerRadius: 6,
|
||||
padding: { top: 6, bottom: 6, left: 6, right: 6 },
|
||||
},
|
||||
hover: {
|
||||
icon: svg(foreground(layer, "hovered"), "icons/maybe_link_out.svg", 12, 12)
|
||||
},
|
||||
},
|
||||
modal: {
|
||||
titleText: {
|
||||
...text(layer, "sans", { size: "md", color: background(layer, "default") }),
|
||||
|
Loading…
Reference in New Issue
Block a user