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