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",
]
[[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",

View File

@ -14,6 +14,7 @@ members = [
"crates/command_palette",
"crates/context_menu",
"crates/copilot",
"crates/copilot_button",
"crates/db",
"crates/diagnostics",
"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)
.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),
]
};

View File

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

View File

@ -1,4 +1,3 @@
pub mod copilot_button;
mod request;
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,
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>) {

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 {
fn from(val: ElementBox) -> Self {
val.0

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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