Add status bar icon reflecting copilot state to Zed status bar

This commit is contained in:
Mikayla Maki 2023-03-29 21:31:33 -07:00
parent 8fac32e1eb
commit cc7c5b416c
20 changed files with 606 additions and 201 deletions

19
Cargo.lock generated
View File

@ -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",

View File

@ -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",

View 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

View File

@ -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),
},
] ]
}; };

View File

@ -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()

View File

@ -1,4 +1,3 @@
pub mod copilot_button;
mod request; mod request;
mod sign_in; mod sign_in;

View File

@ -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();
}
}

View File

@ -0,0 +1,3 @@
use gpui::MutableAppContext;
fn init(cx: &mut MutableAppContext) {}

View 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"

View 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();
}
}

View File

@ -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>) {

View File

@ -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

View File

@ -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"

View File

@ -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]

View File

@ -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,
} }

View File

@ -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());
}, },
) )
} }

View File

@ -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",
) )
}) })

View File

@ -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" }

View File

@ -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 =

View File

@ -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") }),