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",
|
"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]]
|
[[package]]
|
||||||
name = "core-foundation"
|
name = "core-foundation"
|
||||||
version = "0.9.3"
|
version = "0.9.3"
|
||||||
@ -5924,6 +5941,7 @@ dependencies = [
|
|||||||
"gpui",
|
"gpui",
|
||||||
"json_comments",
|
"json_comments",
|
||||||
"postage",
|
"postage",
|
||||||
|
"pretty_assertions",
|
||||||
"schemars",
|
"schemars",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
@ -8507,6 +8525,7 @@ dependencies = [
|
|||||||
"command_palette",
|
"command_palette",
|
||||||
"context_menu",
|
"context_menu",
|
||||||
"copilot",
|
"copilot",
|
||||||
|
"copilot_button",
|
||||||
"ctor",
|
"ctor",
|
||||||
"db",
|
"db",
|
||||||
"diagnostics",
|
"diagnostics",
|
||||||
|
@ -14,6 +14,7 @@ members = [
|
|||||||
"crates/command_palette",
|
"crates/command_palette",
|
||||||
"crates/context_menu",
|
"crates/context_menu",
|
||||||
"crates/copilot",
|
"crates/copilot",
|
||||||
|
"crates/copilot_button",
|
||||||
"crates/db",
|
"crates/db",
|
||||||
"crates/diagnostics",
|
"crates/diagnostics",
|
||||||
"crates/drag_and_drop",
|
"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)
|
.with_style(item_style.container)
|
||||||
.boxed()
|
.boxed()
|
||||||
})),
|
})),
|
||||||
ContextMenuItem::Item {
|
ContextMenuItem::item("Sign out", SignOut),
|
||||||
label: "Sign out".into(),
|
ContextMenuItem::item("Send Feedback", feedback::feedback_editor::GiveFeedback),
|
||||||
action: Box::new(SignOut),
|
|
||||||
},
|
|
||||||
ContextMenuItem::Item {
|
|
||||||
label: "Send Feedback".into(),
|
|
||||||
action: Box::new(feedback::feedback_editor::GiveFeedback),
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
} else {
|
} else {
|
||||||
vec![
|
vec![
|
||||||
ContextMenuItem::Item {
|
ContextMenuItem::item("Sign in", SignIn),
|
||||||
label: "Sign in".into(),
|
ContextMenuItem::item("Send Feedback", feedback::feedback_editor::GiveFeedback),
|
||||||
action: Box::new(SignIn),
|
|
||||||
},
|
|
||||||
ContextMenuItem::Item {
|
|
||||||
label: "Send Feedback".into(),
|
|
||||||
action: Box::new(feedback::feedback_editor::GiveFeedback),
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use gpui::{
|
use gpui::{
|
||||||
elements::*, geometry::vector::Vector2F, impl_internal_actions, keymap_matcher::KeymapContext,
|
elements::*, geometry::vector::Vector2F, impl_internal_actions, keymap_matcher::KeymapContext,
|
||||||
platform::CursorStyle, Action, AnyViewHandle, AppContext, Axis, Entity, MouseButton,
|
platform::CursorStyle, Action, AnyViewHandle, AppContext, Axis, Entity, MouseButton,
|
||||||
MutableAppContext, RenderContext, SizeConstraint, Subscription, View, ViewContext,
|
MouseState, MutableAppContext, RenderContext, SizeConstraint, Subscription, View, ViewContext,
|
||||||
};
|
};
|
||||||
use menu::*;
|
use menu::*;
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
@ -24,20 +24,71 @@ pub fn init(cx: &mut MutableAppContext) {
|
|||||||
cx.add_action(ContextMenu::cancel);
|
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 {
|
pub enum ContextMenuItem {
|
||||||
Item {
|
Item {
|
||||||
label: Cow<'static, str>,
|
label: ContextMenuItemLabel,
|
||||||
action: Box<dyn Action>,
|
action: ContextMenuAction,
|
||||||
},
|
},
|
||||||
Static(StaticItem),
|
Static(StaticItem),
|
||||||
Separator,
|
Separator,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ContextMenuItem {
|
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 {
|
pub fn item(label: impl Into<Cow<'static, str>>, action: impl 'static + Action) -> Self {
|
||||||
Self::Item {
|
Self::Item {
|
||||||
label: label.into(),
|
label: ContextMenuItemLabel::String(label.into()),
|
||||||
|
action: ContextMenuAction::ParentAction {
|
||||||
action: Box::new(action),
|
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>) {
|
fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
|
||||||
if let Some(ix) = self.selected_index {
|
if let Some(ix) = self.selected_index {
|
||||||
if let Some(ContextMenuItem::Item { action, .. }) = self.items.get(ix) {
|
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);
|
self.reset(cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -278,11 +337,18 @@ impl ContextMenu {
|
|||||||
Some(ix) == self.selected_index,
|
Some(ix) == self.selected_index,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
match label {
|
||||||
|
ContextMenuItemLabel::String(label) => {
|
||||||
Label::new(label.to_string(), style.label.clone())
|
Label::new(label.to_string(), style.label.clone())
|
||||||
.contained()
|
.contained()
|
||||||
.with_style(style.container)
|
.with_style(style.container)
|
||||||
.boxed()
|
.boxed()
|
||||||
}
|
}
|
||||||
|
ContextMenuItemLabel::Element(element) => {
|
||||||
|
element(&mut Default::default(), style)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ContextMenuItem::Static(f) => f(cx),
|
ContextMenuItem::Static(f) => f(cx),
|
||||||
|
|
||||||
@ -306,9 +372,18 @@ impl ContextMenu {
|
|||||||
&mut Default::default(),
|
&mut Default::default(),
|
||||||
Some(ix) == self.selected_index,
|
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(
|
KeystrokeLabel::new(
|
||||||
window_id,
|
window_id,
|
||||||
self.parent_view_id,
|
view_id,
|
||||||
action.boxed_clone(),
|
action.boxed_clone(),
|
||||||
style.keystroke.container,
|
style.keystroke.container,
|
||||||
style.keystroke.text.clone(),
|
style.keystroke.text.clone(),
|
||||||
@ -347,22 +422,34 @@ impl ContextMenu {
|
|||||||
.with_children(self.items.iter().enumerate().map(|(ix, item)| {
|
.with_children(self.items.iter().enumerate().map(|(ix, item)| {
|
||||||
match item {
|
match item {
|
||||||
ContextMenuItem::Item { label, action } => {
|
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, _| {
|
MouseEventHandler::<MenuItem>::new(ix, cx, |state, _| {
|
||||||
let style =
|
let style =
|
||||||
style.item.style_for(state, Some(ix) == self.selected_index);
|
style.item.style_for(state, Some(ix) == self.selected_index);
|
||||||
|
|
||||||
Flex::row()
|
Flex::row()
|
||||||
.with_child(
|
.with_child(match label {
|
||||||
|
ContextMenuItemLabel::String(label) => {
|
||||||
Label::new(label.clone(), style.label.clone())
|
Label::new(label.clone(), style.label.clone())
|
||||||
.contained()
|
.contained()
|
||||||
.boxed(),
|
.boxed()
|
||||||
)
|
}
|
||||||
|
ContextMenuItemLabel::Element(element) => {
|
||||||
|
element(state, style)
|
||||||
|
}
|
||||||
|
})
|
||||||
.with_child({
|
.with_child({
|
||||||
KeystrokeLabel::new(
|
KeystrokeLabel::new(
|
||||||
window_id,
|
window_id,
|
||||||
self.parent_view_id,
|
view_id,
|
||||||
action.boxed_clone(),
|
action.boxed_clone(),
|
||||||
style.keystroke.container,
|
style.keystroke.container,
|
||||||
style.keystroke.text.clone(),
|
style.keystroke.text.clone(),
|
||||||
@ -375,9 +462,12 @@ impl ContextMenu {
|
|||||||
.boxed()
|
.boxed()
|
||||||
})
|
})
|
||||||
.with_cursor_style(CursorStyle::PointingHand)
|
.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| {
|
.on_click(MouseButton::Left, move |_, cx| {
|
||||||
cx.dispatch_action(Clicked);
|
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, |_, _| {})
|
.on_drag(MouseButton::Left, |_, _| {})
|
||||||
.boxed()
|
.boxed()
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
pub mod copilot_button;
|
|
||||||
mod request;
|
mod request;
|
||||||
mod sign_in;
|
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,
|
hover_state: HoverState,
|
||||||
gutter_hovered: bool,
|
gutter_hovered: bool,
|
||||||
link_go_to_definition_state: LinkGoToDefinitionState,
|
link_go_to_definition_state: LinkGoToDefinitionState,
|
||||||
copilot_state: CopilotState,
|
pub copilot_state: CopilotState,
|
||||||
_subscriptions: Vec<Subscription>,
|
_subscriptions: Vec<Subscription>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1008,12 +1008,12 @@ impl CodeActionsMenu {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct CopilotState {
|
pub struct CopilotState {
|
||||||
excerpt_id: Option<ExcerptId>,
|
excerpt_id: Option<ExcerptId>,
|
||||||
pending_refresh: Task<Option<()>>,
|
pending_refresh: Task<Option<()>>,
|
||||||
completions: Vec<copilot::Completion>,
|
completions: Vec<copilot::Completion>,
|
||||||
active_completion_index: usize,
|
active_completion_index: usize,
|
||||||
user_enabled: Option<bool>,
|
pub user_enabled: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for CopilotState {
|
impl Default for CopilotState {
|
||||||
@ -2859,6 +2859,7 @@ impl Editor {
|
|||||||
fn next_copilot_suggestion(&mut self, _: &copilot::NextSuggestion, cx: &mut ViewContext<Self>) {
|
fn next_copilot_suggestion(&mut self, _: &copilot::NextSuggestion, cx: &mut ViewContext<Self>) {
|
||||||
// Auto re-enable copilot if you're asking for a suggestion
|
// Auto re-enable copilot if you're asking for a suggestion
|
||||||
if self.copilot_state.user_enabled == Some(false) {
|
if self.copilot_state.user_enabled == Some(false) {
|
||||||
|
cx.notify();
|
||||||
self.copilot_state.user_enabled = Some(true);
|
self.copilot_state.user_enabled = Some(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2880,6 +2881,7 @@ impl Editor {
|
|||||||
) {
|
) {
|
||||||
// Auto re-enable copilot if you're asking for a suggestion
|
// Auto re-enable copilot if you're asking for a suggestion
|
||||||
if self.copilot_state.user_enabled == Some(false) {
|
if self.copilot_state.user_enabled == Some(false) {
|
||||||
|
cx.notify();
|
||||||
self.copilot_state.user_enabled = Some(true);
|
self.copilot_state.user_enabled = Some(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2921,6 +2923,8 @@ impl Editor {
|
|||||||
} else {
|
} else {
|
||||||
self.clear_copilot_suggestions(cx);
|
self.clear_copilot_suggestions(cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sync_suggestion(&mut self, cx: &mut ViewContext<Self>) {
|
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 {
|
impl From<ElementBox> for ElementRc {
|
||||||
fn from(val: ElementBox) -> Self {
|
fn from(val: ElementBox) -> Self {
|
||||||
val.0
|
val.0
|
||||||
|
@ -36,3 +36,4 @@ tree-sitter-json = "*"
|
|||||||
unindent = "0.1"
|
unindent = "0.1"
|
||||||
gpui = { path = "../gpui", features = ["test-support"] }
|
gpui = { path = "../gpui", features = ["test-support"] }
|
||||||
fs = { path = "../fs", features = ["test-support"] }
|
fs = { path = "../fs", features = ["test-support"] }
|
||||||
|
pretty_assertions = "1.3.0"
|
||||||
|
@ -188,17 +188,30 @@ pub enum OnOff {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl OnOff {
|
impl OnOff {
|
||||||
fn as_bool(&self) -> bool {
|
pub fn as_bool(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
OnOff::On => true,
|
OnOff::On => true,
|
||||||
OnOff::Off => false,
|
OnOff::Off => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn from_bool(value: bool) -> OnOff {
|
||||||
|
match value {
|
||||||
|
true => OnOff::On,
|
||||||
|
false => OnOff::Off,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Into<bool> for OnOff {
|
impl From<OnOff> for bool {
|
||||||
fn into(self) -> bool {
|
fn from(value: OnOff) -> bool {
|
||||||
self.as_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);
|
settings_content.insert_str(first_key_start, &content);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
dbg!("here???");
|
||||||
new_value = serde_json::json!({ new_key.to_string(): new_value });
|
new_value = serde_json::json!({ new_key.to_string(): new_value });
|
||||||
let indent_prefix_len = 4 * depth;
|
let indent_prefix_len = 4 * depth;
|
||||||
let new_val = to_pretty_json(&new_value, 4, indent_prefix_len);
|
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(
|
pub fn update_settings_file(
|
||||||
mut text: String,
|
mut text: String,
|
||||||
old_file_content: SettingsFileContent,
|
mut old_file_content: SettingsFileContent,
|
||||||
update: impl FnOnce(&mut SettingsFileContent),
|
update: impl FnOnce(&mut SettingsFileContent),
|
||||||
) -> String {
|
) -> String {
|
||||||
let mut new_file_content = old_file_content.clone();
|
let mut new_file_content = old_file_content.clone();
|
||||||
|
|
||||||
update(&mut new_file_content);
|
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 old_object = to_json_object(old_file_content);
|
||||||
let new_object = to_json_object(new_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() {
|
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
|
// 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();
|
let new_value = new_object.get(key).unwrap();
|
||||||
|
|
||||||
if old_value != new_value {
|
if old_value != new_value {
|
||||||
match new_value {
|
match new_value {
|
||||||
Value::Bool(_) | Value::Number(_) | Value::String(_) => {
|
Value::Bool(_) | Value::Number(_) | Value::String(_) => {
|
||||||
@ -1047,7 +1077,75 @@ mod tests {
|
|||||||
let old_json = old_json.into();
|
let old_json = old_json.into();
|
||||||
let old_content: SettingsFileContent = serde_json::from_str(&old_json).unwrap_or_default();
|
let old_content: SettingsFileContent = serde_json::from_str(&old_json).unwrap_or_default();
|
||||||
let new_json = update_settings_file(old_json, old_content, update);
|
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]
|
#[test]
|
||||||
|
@ -119,6 +119,7 @@ pub struct AvatarStyle {
|
|||||||
|
|
||||||
#[derive(Deserialize, Default, Clone)]
|
#[derive(Deserialize, Default, Clone)]
|
||||||
pub struct Copilot {
|
pub struct Copilot {
|
||||||
|
pub out_link_icon: Interactive<IconStyle>,
|
||||||
pub modal: ModalStyle,
|
pub modal: ModalStyle,
|
||||||
pub auth: CopilotAuth,
|
pub auth: CopilotAuth,
|
||||||
}
|
}
|
||||||
|
@ -141,7 +141,13 @@ pub mod simple_message_notification {
|
|||||||
actions!(message_notifications, [CancelMessageNotification]);
|
actions!(message_notifications, [CancelMessageNotification]);
|
||||||
|
|
||||||
#[derive(Clone, Default, Deserialize, PartialEq)]
|
#[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]);
|
impl_actions!(message_notifications, [OsOpen]);
|
||||||
|
|
||||||
@ -149,7 +155,7 @@ pub mod simple_message_notification {
|
|||||||
cx.add_action(MessageNotification::dismiss);
|
cx.add_action(MessageNotification::dismiss);
|
||||||
cx.add_action(
|
cx.add_action(
|
||||||
|_workspace: &mut Workspace, open_action: &OsOpen, cx: &mut ViewContext<Workspace>| {
|
|_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! {"
|
indoc::indoc! {"
|
||||||
Failed to load any database file :(
|
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"
|
"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
|
backup_path
|
||||||
),
|
),
|
||||||
OsOpen(backup_path.to_string()),
|
OsOpen::new(backup_path.to_string()),
|
||||||
"Click to show old database in finder",
|
"Click to show old database in finder",
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -29,6 +29,7 @@ context_menu = { path = "../context_menu" }
|
|||||||
client = { path = "../client" }
|
client = { path = "../client" }
|
||||||
clock = { path = "../clock" }
|
clock = { path = "../clock" }
|
||||||
copilot = { path = "../copilot" }
|
copilot = { path = "../copilot" }
|
||||||
|
copilot_button = { path = "../copilot_button" }
|
||||||
diagnostics = { path = "../diagnostics" }
|
diagnostics = { path = "../diagnostics" }
|
||||||
db = { path = "../db" }
|
db = { path = "../db" }
|
||||||
editor = { path = "../editor" }
|
editor = { path = "../editor" }
|
||||||
|
@ -8,7 +8,6 @@ use breadcrumbs::Breadcrumbs;
|
|||||||
pub use client;
|
pub use client;
|
||||||
use collab_ui::{CollabTitlebarItem, ToggleContactsMenu};
|
use collab_ui::{CollabTitlebarItem, ToggleContactsMenu};
|
||||||
use collections::VecDeque;
|
use collections::VecDeque;
|
||||||
use copilot::copilot_button::CopilotButton;
|
|
||||||
pub use editor;
|
pub use editor;
|
||||||
use editor::{Editor, MultiBuffer};
|
use editor::{Editor, MultiBuffer};
|
||||||
|
|
||||||
@ -262,6 +261,7 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::MutableAppContext) {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
activity_indicator::init(cx);
|
activity_indicator::init(cx);
|
||||||
|
copilot_button::init(cx);
|
||||||
call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
|
call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
|
||||||
settings::KeymapFileContent::load_defaults(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 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 =
|
let diagnostic_summary =
|
||||||
cx.add_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace.project(), cx));
|
cx.add_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace.project(), cx));
|
||||||
let activity_indicator =
|
let activity_indicator =
|
||||||
|
@ -30,6 +30,16 @@ export default function copilot(colorScheme: ColorScheme) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return {
|
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: {
|
modal: {
|
||||||
titleText: {
|
titleText: {
|
||||||
...text(layer, "sans", { size: "md", color: background(layer, "default") }),
|
...text(layer, "sans", { size: "md", color: background(layer, "default") }),
|
||||||
|
Loading…
Reference in New Issue
Block a user