Merge pull request #2386 from zed-industries/copilot-shipping

Get copilot ready to ship
This commit is contained in:
Mikayla Maki 2023-04-20 12:38:27 +12:00 committed by GitHub
commit dd73233973
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 334 additions and 189 deletions

View File

@ -178,7 +178,7 @@
"focus": false "focus": false
} }
], ],
"alt-\\": "copilot::NextSuggestion", "alt-\\": "copilot::Suggest",
"alt-]": "copilot::NextSuggestion", "alt-]": "copilot::NextSuggestion",
"alt-[": "copilot::PreviousSuggestion" "alt-[": "copilot::PreviousSuggestion"
} }

View File

@ -1,6 +1,11 @@
{ {
// The name of the Zed theme to use for the UI // The name of the Zed theme to use for the UI
"theme": "One Dark", "theme": "One Dark",
// Features that can be globally enabled or disabled
"features": {
// Show Copilot icon in status bar
"copilot": true
},
// The name of a font to use for rendering text in the editor // The name of a font to use for rendering text in the editor
"buffer_font_family": "Zed Mono", "buffer_font_family": "Zed Mono",
// The OpenType features to enable for text in the editor. // The OpenType features to enable for text in the editor.
@ -13,11 +18,6 @@
// The factor to grow the active pane by. Defaults to 1.0 // The factor to grow the active pane by. Defaults to 1.0
// which gives the same size as all other panes. // which gives the same size as all other panes.
"active_pane_magnification": 1.0, "active_pane_magnification": 1.0,
// Enable / disable copilot integration.
"enable_copilot_integration": true,
// Controls whether copilot provides suggestion immediately
// or waits for a `copilot::Toggle`
"copilot": "on",
// Whether to enable vim modes and key bindings // Whether to enable vim modes and key bindings
"vim_mode": false, "vim_mode": false,
// Whether to show the informational hover box when moving the mouse // Whether to show the informational hover box when moving the mouse
@ -30,6 +30,9 @@
// Whether to pop the completions menu while typing in an editor without // Whether to pop the completions menu while typing in an editor without
// explicitly requesting it. // explicitly requesting it.
"show_completions_on_input": true, "show_completions_on_input": true,
// Controls whether copilot provides suggestion immediately
// or waits for a `copilot::Toggle`
"show_copilot_suggestions": true,
// Whether the screen sharing icon is shown in the os status bar. // Whether the screen sharing icon is shown in the os status bar.
"show_call_status_icon": true, "show_call_status_icon": true,
// Whether to use language servers to provide code intelligence. // Whether to use language servers to provide code intelligence.

View File

@ -29,7 +29,10 @@ const COPILOT_AUTH_NAMESPACE: &'static str = "copilot_auth";
actions!(copilot_auth, [SignIn, SignOut]); actions!(copilot_auth, [SignIn, SignOut]);
const COPILOT_NAMESPACE: &'static str = "copilot"; const COPILOT_NAMESPACE: &'static str = "copilot";
actions!(copilot, [NextSuggestion, PreviousSuggestion, Reinstall]); actions!(
copilot,
[Suggest, NextSuggestion, PreviousSuggestion, Reinstall]
);
pub fn init(http: Arc<dyn HttpClient>, node_runtime: Arc<NodeRuntime>, cx: &mut AppContext) { pub fn init(http: Arc<dyn HttpClient>, node_runtime: Arc<NodeRuntime>, cx: &mut AppContext) {
// Disable Copilot for stable releases. // Disable Copilot for stable releases.
@ -172,7 +175,7 @@ impl Copilot {
let http = http.clone(); let http = http.clone();
let node_runtime = node_runtime.clone(); let node_runtime = node_runtime.clone();
move |this, cx| { move |this, cx| {
if cx.global::<Settings>().enable_copilot_integration { if cx.global::<Settings>().features.copilot {
if matches!(this.server, CopilotServer::Disabled) { if matches!(this.server, CopilotServer::Disabled) {
let start_task = cx let start_task = cx
.spawn({ .spawn({
@ -194,12 +197,14 @@ impl Copilot {
}) })
.detach(); .detach();
if cx.global::<Settings>().enable_copilot_integration { if cx.global::<Settings>().features.copilot {
let start_task = cx let start_task = cx
.spawn({ .spawn({
let http = http.clone(); let http = http.clone();
let node_runtime = node_runtime.clone(); let node_runtime = node_runtime.clone();
move |this, cx| Self::start_language_server(http, node_runtime, this, cx) move |this, cx| async {
Self::start_language_server(http, node_runtime, this, cx).await
}
}) })
.shared(); .shared();

View File

@ -2,12 +2,18 @@ use crate::{request::PromptUserDeviceFlow, Copilot, Status};
use gpui::{ use gpui::{
elements::*, elements::*,
geometry::rect::RectF, geometry::rect::RectF,
impl_internal_actions,
platform::{WindowBounds, WindowKind, WindowOptions}, platform::{WindowBounds, WindowKind, WindowOptions},
AppContext, ClipboardItem, Element, Entity, View, ViewContext, ViewHandle, AppContext, ClipboardItem, Element, Entity, View, ViewContext, ViewHandle,
}; };
use settings::Settings; use settings::Settings;
use theme::ui::modal; use theme::ui::modal;
#[derive(PartialEq, Eq, Debug, Clone)]
struct ClickedConnect;
impl_internal_actions!(copilot_verification, [ClickedConnect]);
#[derive(PartialEq, Eq, Debug, Clone)] #[derive(PartialEq, Eq, Debug, Clone)]
struct CopyUserCode; struct CopyUserCode;
@ -56,6 +62,12 @@ pub fn init(cx: &mut AppContext) {
} }
}) })
.detach(); .detach();
cx.add_action(
|code_verification: &mut CopilotCodeVerification, _: &ClickedConnect, _| {
code_verification.connect_clicked = true;
},
);
} }
fn create_copilot_auth_window( fn create_copilot_auth_window(
@ -81,11 +93,15 @@ fn create_copilot_auth_window(
pub struct CopilotCodeVerification { pub struct CopilotCodeVerification {
status: Status, status: Status,
connect_clicked: bool,
} }
impl CopilotCodeVerification { impl CopilotCodeVerification {
pub fn new(status: Status) -> Self { pub fn new(status: Status) -> Self {
Self { status } Self {
status,
connect_clicked: false,
}
} }
pub fn set_status(&mut self, status: Status, cx: &mut ViewContext<Self>) { pub fn set_status(&mut self, status: Status, cx: &mut ViewContext<Self>) {
@ -143,6 +159,7 @@ impl CopilotCodeVerification {
} }
fn render_prompting_modal( fn render_prompting_modal(
connect_clicked: bool,
data: &PromptUserDeviceFlow, data: &PromptUserDeviceFlow,
style: &theme::Copilot, style: &theme::Copilot,
cx: &mut gpui::RenderContext<Self>, cx: &mut gpui::RenderContext<Self>,
@ -189,13 +206,20 @@ impl CopilotCodeVerification {
.with_style(style.auth.prompting.hint.container.clone()) .with_style(style.auth.prompting.hint.container.clone())
.boxed(), .boxed(),
theme::ui::cta_button_with_click( theme::ui::cta_button_with_click(
"Connect to GitHub", if connect_clicked {
"Waiting for connection..."
} else {
"Connect to GitHub"
},
style.auth.content_width, style.auth.content_width,
&style.auth.cta_button, &style.auth.cta_button,
cx, cx,
{ {
let verification_uri = data.verification_uri.clone(); let verification_uri = data.verification_uri.clone();
move |_, cx| cx.platform().open_url(&verification_uri) move |_, cx| {
cx.platform().open_url(&verification_uri);
cx.dispatch_action(ClickedConnect)
}
}, },
) )
.boxed(), .boxed(),
@ -343,9 +367,20 @@ impl View for CopilotCodeVerification {
match &self.status { match &self.status {
Status::SigningIn { Status::SigningIn {
prompt: Some(prompt), prompt: Some(prompt),
} => Self::render_prompting_modal(&prompt, &style.copilot, cx), } => Self::render_prompting_modal(
Status::Unauthorized => Self::render_unauthorized_modal(&style.copilot, cx), self.connect_clicked,
Status::Authorized => Self::render_enabled_modal(&style.copilot, cx), &prompt,
&style.copilot,
cx,
),
Status::Unauthorized => {
self.connect_clicked = false;
Self::render_unauthorized_modal(&style.copilot, cx)
}
Status::Authorized => {
self.connect_clicked = false;
Self::render_enabled_modal(&style.copilot, cx)
}
_ => Empty::new().boxed(), _ => Empty::new().boxed(),
}, },
]) ])

View File

@ -24,6 +24,15 @@ const COPILOT_ERROR_TOAST_ID: usize = 1338;
#[derive(Clone, PartialEq)] #[derive(Clone, PartialEq)]
pub struct DeployCopilotMenu; pub struct DeployCopilotMenu;
#[derive(Clone, PartialEq)]
pub struct DeployCopilotStartMenu;
#[derive(Clone, PartialEq)]
pub struct HideCopilot;
#[derive(Clone, PartialEq)]
pub struct InitiateSignIn;
#[derive(Clone, PartialEq)] #[derive(Clone, PartialEq)]
pub struct ToggleCopilotForLanguage { pub struct ToggleCopilotForLanguage {
language: Arc<str>, language: Arc<str>,
@ -40,6 +49,9 @@ impl_internal_actions!(
copilot, copilot,
[ [
DeployCopilotMenu, DeployCopilotMenu,
DeployCopilotStartMenu,
HideCopilot,
InitiateSignIn,
DeployCopilotModal, DeployCopilotModal,
ToggleCopilotForLanguage, ToggleCopilotForLanguage,
ToggleCopilotGlobally, ToggleCopilotGlobally,
@ -48,17 +60,19 @@ impl_internal_actions!(
pub fn init(cx: &mut AppContext) { pub fn init(cx: &mut AppContext) {
cx.add_action(CopilotButton::deploy_copilot_menu); cx.add_action(CopilotButton::deploy_copilot_menu);
cx.add_action(CopilotButton::deploy_copilot_start_menu);
cx.add_action( cx.add_action(
|_: &mut CopilotButton, action: &ToggleCopilotForLanguage, cx| { |_: &mut CopilotButton, action: &ToggleCopilotForLanguage, cx| {
let language = action.language.to_owned(); let language = action.language.clone();
let show_copilot_suggestions = cx
let current_langauge = cx.global::<Settings>().copilot_on(Some(&language)); .global::<Settings>()
.show_copilot_suggestions(Some(&language));
SettingsFile::update(cx, move |file_contents| { SettingsFile::update(cx, move |file_contents| {
file_contents.languages.insert( file_contents.languages.insert(
language.to_owned(), language,
settings::EditorSettings { settings::EditorSettings {
copilot: Some((!current_langauge).into()), show_copilot_suggestions: Some((!show_copilot_suggestions).into()),
..Default::default() ..Default::default()
}, },
); );
@ -67,12 +81,63 @@ pub fn init(cx: &mut AppContext) {
); );
cx.add_action(|_: &mut CopilotButton, _: &ToggleCopilotGlobally, cx| { cx.add_action(|_: &mut CopilotButton, _: &ToggleCopilotGlobally, cx| {
let copilot_on = cx.global::<Settings>().copilot_on(None); let show_copilot_suggestions = cx.global::<Settings>().show_copilot_suggestions(None);
SettingsFile::update(cx, move |file_contents| { SettingsFile::update(cx, move |file_contents| {
file_contents.editor.copilot = Some((!copilot_on).into()) file_contents.editor.show_copilot_suggestions = Some((!show_copilot_suggestions).into())
}) })
}); });
cx.add_action(|_: &mut CopilotButton, _: &HideCopilot, cx| {
SettingsFile::update(cx, move |file_contents| {
file_contents.features.copilot = Some(false)
})
});
cx.add_action(|_: &mut CopilotButton, _: &InitiateSignIn, cx| {
let Some(copilot) = Copilot::global(cx) else {
return;
};
let status = copilot.read(cx).status();
match status {
Status::Starting { task } => {
cx.dispatch_action(workspace::Toast::new(
COPILOT_STARTING_TOAST_ID,
"Copilot is starting...",
));
let window_id = cx.window_id();
let task = task.to_owned();
cx.spawn(|handle, mut cx| async move {
task.await;
cx.update(|cx| {
if let Some(copilot) = Copilot::global(cx) {
let status = copilot.read(cx).status();
match status {
Status::Authorized => cx.dispatch_action_at(
window_id,
handle.id(),
workspace::Toast::new(
COPILOT_STARTING_TOAST_ID,
"Copilot has started!",
),
),
_ => {
cx.dispatch_action_at(
window_id,
handle.id(),
DismissToast::new(COPILOT_STARTING_TOAST_ID),
);
cx.dispatch_action_at(window_id, handle.id(), SignIn)
}
}
}
})
})
.detach();
}
_ => cx.dispatch_action(SignIn),
}
})
} }
pub struct CopilotButton { pub struct CopilotButton {
@ -94,7 +159,7 @@ impl View for CopilotButton {
fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox { fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox {
let settings = cx.global::<Settings>(); let settings = cx.global::<Settings>();
if !settings.enable_copilot_integration { if !settings.features.copilot {
return Empty::new().boxed(); return Empty::new().boxed();
} }
@ -105,9 +170,9 @@ impl View for CopilotButton {
}; };
let status = copilot.read(cx).status(); let status = copilot.read(cx).status();
let enabled = self.editor_enabled.unwrap_or(settings.copilot_on(None)); let enabled = self
.editor_enabled
let view_id = cx.view_id(); .unwrap_or(settings.show_copilot_suggestions(None));
Stack::new() Stack::new()
.with_child( .with_child(
@ -155,48 +220,13 @@ impl View for CopilotButton {
let status = status.clone(); let status = status.clone();
move |_, cx| match status { move |_, cx| match status {
Status::Authorized => cx.dispatch_action(DeployCopilotMenu), Status::Authorized => cx.dispatch_action(DeployCopilotMenu),
Status::Starting { ref task } => {
cx.dispatch_action(workspace::Toast::new(
COPILOT_STARTING_TOAST_ID,
"Copilot is starting...",
));
let window_id = cx.window_id();
let task = task.to_owned();
cx.spawn(|mut cx| async move {
task.await;
cx.update(|cx| {
if let Some(copilot) = Copilot::global(cx) {
let status = copilot.read(cx).status();
match status {
Status::Authorized => cx.dispatch_action_at(
window_id,
view_id,
workspace::Toast::new(
COPILOT_STARTING_TOAST_ID,
"Copilot has started!",
),
),
_ => {
cx.dispatch_action_at(
window_id,
view_id,
DismissToast::new(COPILOT_STARTING_TOAST_ID),
);
cx.dispatch_global_action(SignIn)
}
}
}
})
})
.detach();
}
Status::Error(ref e) => cx.dispatch_action(workspace::Toast::new_action( Status::Error(ref e) => cx.dispatch_action(workspace::Toast::new_action(
COPILOT_ERROR_TOAST_ID, COPILOT_ERROR_TOAST_ID,
format!("Copilot can't be started: {}", e), format!("Copilot can't be started: {}", e),
"Reinstall Copilot", "Reinstall Copilot",
Reinstall, Reinstall,
)), )),
_ => cx.dispatch_action(SignIn), _ => cx.dispatch_action(DeployCopilotStartMenu),
} }
}) })
.with_tooltip::<Self, _>( .with_tooltip::<Self, _>(
@ -242,22 +272,38 @@ impl CopilotButton {
} }
} }
pub fn deploy_copilot_start_menu(
&mut self,
_: &DeployCopilotStartMenu,
cx: &mut ViewContext<Self>,
) {
let mut menu_options = Vec::with_capacity(2);
menu_options.push(ContextMenuItem::item("Sign In", InitiateSignIn));
menu_options.push(ContextMenuItem::item("Hide Copilot", HideCopilot));
self.popup_menu.update(cx, |menu, cx| {
menu.show(
Default::default(),
AnchorCorner::BottomRight,
menu_options,
cx,
);
});
}
pub fn deploy_copilot_menu(&mut self, _: &DeployCopilotMenu, cx: &mut ViewContext<Self>) { pub fn deploy_copilot_menu(&mut self, _: &DeployCopilotMenu, cx: &mut ViewContext<Self>) {
let settings = cx.global::<Settings>(); let settings = cx.global::<Settings>();
let mut menu_options = Vec::with_capacity(6); let mut menu_options = Vec::with_capacity(6);
if let Some(language) = &self.language { if let Some(language) = &self.language {
let language_enabled = settings.copilot_on(Some(language.as_ref())); let language_enabled = settings.show_copilot_suggestions(Some(language.as_ref()));
menu_options.push(ContextMenuItem::item( menu_options.push(ContextMenuItem::item(
format!( format!(
"{} Copilot for {}", "{} Suggestions for {}",
if language_enabled { if language_enabled { "Hide" } else { "Show" },
"Disable"
} else {
"Enable"
},
language language
), ),
ToggleCopilotForLanguage { ToggleCopilotForLanguage {
@ -266,12 +312,12 @@ impl CopilotButton {
)); ));
} }
let globally_enabled = cx.global::<Settings>().copilot_on(None); let globally_enabled = cx.global::<Settings>().show_copilot_suggestions(None);
menu_options.push(ContextMenuItem::item( menu_options.push(ContextMenuItem::item(
if globally_enabled { if globally_enabled {
"Disable Copilot Globally" "Hide Suggestions for All Files"
} else { } else {
"Enable Copilot Globally" "Show Suggestions for All Files"
}, },
ToggleCopilotGlobally, ToggleCopilotGlobally,
)); ));
@ -319,7 +365,7 @@ impl CopilotButton {
self.language = language_name.clone(); self.language = language_name.clone();
self.editor_enabled = Some(settings.copilot_on(language_name.as_deref())); self.editor_enabled = Some(settings.show_copilot_suggestions(language_name.as_deref()));
cx.notify() cx.notify()
} }

View File

@ -397,6 +397,7 @@ pub fn init(cx: &mut AppContext) {
cx.add_async_action(Editor::find_all_references); cx.add_async_action(Editor::find_all_references);
cx.add_action(Editor::next_copilot_suggestion); cx.add_action(Editor::next_copilot_suggestion);
cx.add_action(Editor::previous_copilot_suggestion); cx.add_action(Editor::previous_copilot_suggestion);
cx.add_action(Editor::copilot_suggest);
hover_popover::init(cx); hover_popover::init(cx);
link_go_to_definition::init(cx); link_go_to_definition::init(cx);
@ -1016,6 +1017,8 @@ impl CodeActionsMenu {
pub struct CopilotState { pub struct CopilotState {
excerpt_id: Option<ExcerptId>, excerpt_id: Option<ExcerptId>,
pending_refresh: Task<Option<()>>, pending_refresh: Task<Option<()>>,
pending_cycling_refresh: Task<Option<()>>,
cycled: bool,
completions: Vec<copilot::Completion>, completions: Vec<copilot::Completion>,
active_completion_index: usize, active_completion_index: usize,
} }
@ -1024,9 +1027,11 @@ impl Default for CopilotState {
fn default() -> Self { fn default() -> Self {
Self { Self {
excerpt_id: None, excerpt_id: None,
pending_cycling_refresh: Task::ready(Some(())),
pending_refresh: Task::ready(Some(())), pending_refresh: Task::ready(Some(())),
completions: Default::default(), completions: Default::default(),
active_completion_index: 0, active_completion_index: 0,
cycled: false,
} }
} }
} }
@ -1070,6 +1075,26 @@ impl CopilotState {
} }
} }
fn cycle_completions(&mut self, direction: Direction) {
match direction {
Direction::Prev => {
self.active_completion_index = if self.active_completion_index == 0 {
self.completions.len() - 1
} else {
self.active_completion_index - 1
};
}
Direction::Next => {
if self.completions.len() == 0 {
self.active_completion_index = 0
} else {
self.active_completion_index =
(self.active_completion_index + 1) % self.completions.len();
}
}
}
}
fn push_completion(&mut self, new_completion: copilot::Completion) { fn push_completion(&mut self, new_completion: copilot::Completion) {
for completion in &self.completions { for completion in &self.completions {
if *completion == new_completion { if *completion == new_completion {
@ -1267,7 +1292,7 @@ impl Editor {
cx.subscribe(&buffer, Self::on_buffer_event), cx.subscribe(&buffer, Self::on_buffer_event),
cx.observe(&display_map, Self::on_display_map_changed), cx.observe(&display_map, Self::on_display_map_changed),
cx.observe(&blink_manager, |_, _, cx| cx.notify()), cx.observe(&blink_manager, |_, _, cx| cx.notify()),
cx.observe_global::<Settings, _>(Self::on_settings_changed), cx.observe_global::<Settings, _>(Self::settings_changed),
], ],
}; };
this.end_selection(cx); this.end_selection(cx);
@ -2028,13 +2053,13 @@ impl Editor {
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections)); this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections));
if had_active_copilot_suggestion { if had_active_copilot_suggestion {
this.refresh_copilot_suggestions(cx); this.refresh_copilot_suggestions(true, cx);
if !this.has_active_copilot_suggestion(cx) { if !this.has_active_copilot_suggestion(cx) {
this.trigger_completion_on_input(&text, cx); this.trigger_completion_on_input(&text, cx);
} }
} else { } else {
this.trigger_completion_on_input(&text, cx); this.trigger_completion_on_input(&text, cx);
this.refresh_copilot_suggestions(cx); this.refresh_copilot_suggestions(true, cx);
} }
}); });
} }
@ -2116,7 +2141,7 @@ impl Editor {
.collect(); .collect();
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections)); this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections));
this.refresh_copilot_suggestions(cx); this.refresh_copilot_suggestions(true, cx);
}); });
} }
@ -2591,7 +2616,7 @@ impl Editor {
}); });
} }
this.refresh_copilot_suggestions(cx); this.refresh_copilot_suggestions(true, cx);
}); });
let project = self.project.clone()?; let project = self.project.clone()?;
@ -2884,10 +2909,14 @@ impl Editor {
None None
} }
fn refresh_copilot_suggestions(&mut self, cx: &mut ViewContext<Self>) -> Option<()> { fn refresh_copilot_suggestions(
&mut self,
debounce: bool,
cx: &mut ViewContext<Self>,
) -> Option<()> {
let copilot = Copilot::global(cx)?; let copilot = Copilot::global(cx)?;
if self.mode != EditorMode::Full || !copilot.read(cx).status().is_authorized() { if self.mode != EditorMode::Full || !copilot.read(cx).status().is_authorized() {
self.hide_copilot_suggestion(cx); self.clear_copilot_suggestions(cx);
return None; return None;
} }
self.update_visible_copilot_suggestion(cx); self.update_visible_copilot_suggestion(cx);
@ -2895,28 +2924,35 @@ impl Editor {
let snapshot = self.buffer.read(cx).snapshot(cx); let snapshot = self.buffer.read(cx).snapshot(cx);
let cursor = self.selections.newest_anchor().head(); let cursor = self.selections.newest_anchor().head();
let language_name = snapshot.language_at(cursor).map(|language| language.name()); let language_name = snapshot.language_at(cursor).map(|language| language.name());
if !cx.global::<Settings>().copilot_on(language_name.as_deref()) { if !cx
self.hide_copilot_suggestion(cx); .global::<Settings>()
.show_copilot_suggestions(language_name.as_deref())
{
self.clear_copilot_suggestions(cx);
return None; return None;
} }
let (buffer, buffer_position) = let (buffer, buffer_position) =
self.buffer.read(cx).text_anchor_for_position(cursor, cx)?; self.buffer.read(cx).text_anchor_for_position(cursor, cx)?;
self.copilot_state.pending_refresh = cx.spawn_weak(|this, mut cx| async move { self.copilot_state.pending_refresh = cx.spawn_weak(|this, mut cx| async move {
if debounce {
cx.background().timer(COPILOT_DEBOUNCE_TIMEOUT).await; cx.background().timer(COPILOT_DEBOUNCE_TIMEOUT).await;
let (completion, completions_cycling) = copilot.update(&mut cx, |copilot, cx| { }
(
copilot.completions(&buffer, buffer_position, cx), let completions = copilot
copilot.completions_cycling(&buffer, buffer_position, cx), .update(&mut cx, |copilot, cx| {
) copilot.completions(&buffer, buffer_position, cx)
}); })
.await
.log_err()
.into_iter()
.flatten()
.collect_vec();
let (completion, completions_cycling) = futures::join!(completion, completions_cycling);
let mut completions = Vec::new();
completions.extend(completion.log_err().into_iter().flatten());
completions.extend(completions_cycling.log_err().into_iter().flatten());
this.upgrade(&cx)?.update(&mut cx, |this, cx| { this.upgrade(&cx)?.update(&mut cx, |this, cx| {
if !completions.is_empty() { if !completions.is_empty() {
this.copilot_state.cycled = false;
this.copilot_state.pending_cycling_refresh = Task::ready(None);
this.copilot_state.completions.clear(); this.copilot_state.completions.clear();
this.copilot_state.active_completion_index = 0; this.copilot_state.active_completion_index = 0;
this.copilot_state.excerpt_id = Some(cursor.excerpt_id); this.copilot_state.excerpt_id = Some(cursor.excerpt_id);
@ -2933,34 +2969,73 @@ impl Editor {
Some(()) Some(())
} }
fn next_copilot_suggestion(&mut self, _: &copilot::NextSuggestion, cx: &mut ViewContext<Self>) { fn cycle_suggestions(
&mut self,
direction: Direction,
cx: &mut ViewContext<Self>,
) -> Option<()> {
let copilot = Copilot::global(cx)?;
if self.mode != EditorMode::Full || !copilot.read(cx).status().is_authorized() {
return None;
}
if self.copilot_state.cycled {
self.copilot_state.cycle_completions(direction);
self.update_visible_copilot_suggestion(cx);
} else {
let cursor = self.selections.newest_anchor().head();
let (buffer, buffer_position) =
self.buffer.read(cx).text_anchor_for_position(cursor, cx)?;
self.copilot_state.pending_cycling_refresh = cx.spawn_weak(|this, mut cx| async move {
let completions = copilot
.update(&mut cx, |copilot, cx| {
copilot.completions_cycling(&buffer, buffer_position, cx)
})
.await;
this.upgrade(&cx)?.update(&mut cx, |this, cx| {
this.copilot_state.cycled = true;
for completion in completions.log_err().into_iter().flatten() {
this.copilot_state.push_completion(completion);
}
this.copilot_state.cycle_completions(direction);
this.update_visible_copilot_suggestion(cx);
});
Some(())
});
}
Some(())
}
fn copilot_suggest(&mut self, _: &copilot::Suggest, cx: &mut ViewContext<Self>) {
if !self.has_active_copilot_suggestion(cx) { if !self.has_active_copilot_suggestion(cx) {
self.refresh_copilot_suggestions(cx); self.refresh_copilot_suggestions(false, cx);
return; return;
} }
self.copilot_state.active_completion_index =
(self.copilot_state.active_completion_index + 1) % self.copilot_state.completions.len();
self.update_visible_copilot_suggestion(cx); self.update_visible_copilot_suggestion(cx);
} }
fn next_copilot_suggestion(&mut self, _: &copilot::NextSuggestion, cx: &mut ViewContext<Self>) {
if self.has_active_copilot_suggestion(cx) {
self.cycle_suggestions(Direction::Next, cx);
} else {
self.refresh_copilot_suggestions(false, cx);
}
}
fn previous_copilot_suggestion( fn previous_copilot_suggestion(
&mut self, &mut self,
_: &copilot::PreviousSuggestion, _: &copilot::PreviousSuggestion,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) { ) {
if !self.has_active_copilot_suggestion(cx) { if self.has_active_copilot_suggestion(cx) {
self.refresh_copilot_suggestions(cx); self.cycle_suggestions(Direction::Prev, cx);
return;
}
self.copilot_state.active_completion_index =
if self.copilot_state.active_completion_index == 0 {
self.copilot_state.completions.len() - 1
} else { } else {
self.copilot_state.active_completion_index - 1 self.refresh_copilot_suggestions(false, cx);
}; }
self.update_visible_copilot_suggestion(cx);
} }
fn accept_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> bool { fn accept_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> bool {
@ -3002,11 +3077,11 @@ impl Editor {
.copilot_state .copilot_state
.text_for_active_completion(cursor, &snapshot) .text_for_active_completion(cursor, &snapshot)
{ {
self.display_map.update(cx, |map, cx| { self.display_map.update(cx, move |map, cx| {
map.replace_suggestion( map.replace_suggestion(
Some(Suggestion { Some(Suggestion {
position: cursor, position: cursor,
text: text.into(), text: text.trim_end().into(),
}), }),
cx, cx,
) )
@ -3017,6 +3092,11 @@ impl Editor {
} }
} }
fn clear_copilot_suggestions(&mut self, cx: &mut ViewContext<Self>) {
self.copilot_state = Default::default();
self.hide_copilot_suggestion(cx);
}
pub fn render_code_actions_indicator( pub fn render_code_actions_indicator(
&self, &self,
style: &EditorStyle, style: &EditorStyle,
@ -3302,7 +3382,7 @@ impl Editor {
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections)); this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
this.insert("", cx); this.insert("", cx);
this.refresh_copilot_suggestions(cx); this.refresh_copilot_suggestions(true, cx);
}); });
} }
@ -3318,7 +3398,7 @@ impl Editor {
}) })
}); });
this.insert("", cx); this.insert("", cx);
this.refresh_copilot_suggestions(cx); this.refresh_copilot_suggestions(true, cx);
}); });
} }
@ -3414,7 +3494,7 @@ impl Editor {
self.transact(cx, |this, cx| { self.transact(cx, |this, cx| {
this.buffer.update(cx, |b, cx| b.edit(edits, None, cx)); this.buffer.update(cx, |b, cx| b.edit(edits, None, cx));
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections)); this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
this.refresh_copilot_suggestions(cx); this.refresh_copilot_suggestions(true, cx);
}); });
} }
@ -4094,7 +4174,7 @@ impl Editor {
} }
self.request_autoscroll(Autoscroll::fit(), cx); self.request_autoscroll(Autoscroll::fit(), cx);
self.unmark_text(cx); self.unmark_text(cx);
self.refresh_copilot_suggestions(cx); self.refresh_copilot_suggestions(true, cx);
cx.emit(Event::Edited); cx.emit(Event::Edited);
} }
} }
@ -4109,7 +4189,7 @@ impl Editor {
} }
self.request_autoscroll(Autoscroll::fit(), cx); self.request_autoscroll(Autoscroll::fit(), cx);
self.unmark_text(cx); self.unmark_text(cx);
self.refresh_copilot_suggestions(cx); self.refresh_copilot_suggestions(true, cx);
cx.emit(Event::Edited); cx.emit(Event::Edited);
} }
} }
@ -6570,8 +6650,8 @@ impl Editor {
cx.notify(); cx.notify();
} }
fn on_settings_changed(&mut self, cx: &mut ViewContext<Self>) { fn settings_changed(&mut self, cx: &mut ViewContext<Self>) {
self.refresh_copilot_suggestions(cx); self.refresh_copilot_suggestions(true, cx);
} }
pub fn set_searchable(&mut self, searchable: bool) { pub fn set_searchable(&mut self, searchable: bool) {

View File

@ -28,11 +28,11 @@ pub use watched_json::watch_files;
#[derive(Clone)] #[derive(Clone)]
pub struct Settings { pub struct Settings {
pub features: Features,
pub buffer_font_family_name: String, pub buffer_font_family_name: String,
pub buffer_font_features: fonts::Features, pub buffer_font_features: fonts::Features,
pub buffer_font_family: FamilyId, pub buffer_font_family: FamilyId,
pub default_buffer_font_size: f32, pub default_buffer_font_size: f32,
pub enable_copilot_integration: bool,
pub buffer_font_size: f32, pub buffer_font_size: f32,
pub active_pane_magnification: f32, pub active_pane_magnification: f32,
pub cursor_blink: bool, pub cursor_blink: bool,
@ -177,43 +177,7 @@ pub struct EditorSettings {
pub ensure_final_newline_on_save: Option<bool>, pub ensure_final_newline_on_save: Option<bool>,
pub formatter: Option<Formatter>, pub formatter: Option<Formatter>,
pub enable_language_server: Option<bool>, pub enable_language_server: Option<bool>,
#[schemars(skip)] pub show_copilot_suggestions: Option<bool>,
pub copilot: Option<OnOff>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum OnOff {
On,
Off,
}
impl OnOff {
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 From<bool> for OnOff {
fn from(value: bool) -> OnOff {
OnOff::from_bool(value)
}
} }
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
@ -437,8 +401,7 @@ pub struct SettingsFileContent {
#[serde(default)] #[serde(default)]
pub base_keymap: Option<BaseKeymap>, pub base_keymap: Option<BaseKeymap>,
#[serde(default)] #[serde(default)]
#[schemars(skip)] pub features: FeaturesContent,
pub enable_copilot_integration: Option<bool>,
} }
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
@ -447,6 +410,18 @@ pub struct LspSettings {
pub initialization_options: Option<Value>, pub initialization_options: Option<Value>,
} }
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub struct Features {
pub copilot: bool,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub struct FeaturesContent {
pub copilot: Option<bool>,
}
impl Settings { impl Settings {
/// Fill out the settings corresponding to the default.json file, overrides will be set later /// Fill out the settings corresponding to the default.json file, overrides will be set later
pub fn defaults( pub fn defaults(
@ -500,7 +475,7 @@ impl Settings {
format_on_save: required(defaults.editor.format_on_save), format_on_save: required(defaults.editor.format_on_save),
formatter: required(defaults.editor.formatter), formatter: required(defaults.editor.formatter),
enable_language_server: required(defaults.editor.enable_language_server), enable_language_server: required(defaults.editor.enable_language_server),
copilot: required(defaults.editor.copilot), show_copilot_suggestions: required(defaults.editor.show_copilot_suggestions),
}, },
editor_overrides: Default::default(), editor_overrides: Default::default(),
git: defaults.git.unwrap(), git: defaults.git.unwrap(),
@ -517,7 +492,9 @@ impl Settings {
telemetry_overrides: Default::default(), telemetry_overrides: Default::default(),
auto_update: defaults.auto_update.unwrap(), auto_update: defaults.auto_update.unwrap(),
base_keymap: Default::default(), base_keymap: Default::default(),
enable_copilot_integration: defaults.enable_copilot_integration.unwrap(), features: Features {
copilot: defaults.features.copilot.unwrap(),
},
} }
} }
@ -569,10 +546,7 @@ impl Settings {
merge(&mut self.autosave, data.autosave); merge(&mut self.autosave, data.autosave);
merge(&mut self.default_dock_anchor, data.default_dock_anchor); merge(&mut self.default_dock_anchor, data.default_dock_anchor);
merge(&mut self.base_keymap, data.base_keymap); merge(&mut self.base_keymap, data.base_keymap);
merge( merge(&mut self.features.copilot, data.features.copilot);
&mut self.enable_copilot_integration,
data.enable_copilot_integration,
);
self.editor_overrides = data.editor; self.editor_overrides = data.editor;
self.git_overrides = data.git.unwrap_or_default(); self.git_overrides = data.git.unwrap_or_default();
@ -596,12 +570,15 @@ impl Settings {
self self
} }
pub fn copilot_on(&self, language: Option<&str>) -> bool { pub fn features(&self) -> &Features {
if self.enable_copilot_integration { &self.features
self.language_setting(language, |settings| settings.copilot.map(Into::into))
} else {
false
} }
pub fn show_copilot_suggestions(&self, language: Option<&str>) -> bool {
self.features.copilot
&& self.language_setting(language, |settings| {
settings.show_copilot_suggestions.map(Into::into)
})
} }
pub fn tab_size(&self, language: Option<&str>) -> NonZeroU32 { pub fn tab_size(&self, language: Option<&str>) -> NonZeroU32 {
@ -740,7 +717,7 @@ impl Settings {
format_on_save: Some(FormatOnSave::On), format_on_save: Some(FormatOnSave::On),
formatter: Some(Formatter::LanguageServer), formatter: Some(Formatter::LanguageServer),
enable_language_server: Some(true), enable_language_server: Some(true),
copilot: Some(OnOff::On), show_copilot_suggestions: Some(true),
}, },
editor_overrides: Default::default(), editor_overrides: Default::default(),
journal_defaults: Default::default(), journal_defaults: Default::default(),
@ -760,7 +737,7 @@ impl Settings {
telemetry_overrides: Default::default(), telemetry_overrides: Default::default(),
auto_update: true, auto_update: true,
base_keymap: Default::default(), base_keymap: Default::default(),
enable_copilot_integration: true, features: Features { copilot: true },
} }
} }
@ -1125,7 +1102,7 @@ mod tests {
{ {
"language_overrides": { "language_overrides": {
"JSON": { "JSON": {
"copilot": "off" "show_copilot_suggestions": false
} }
} }
} }
@ -1135,7 +1112,7 @@ mod tests {
settings.languages.insert( settings.languages.insert(
"Rust".into(), "Rust".into(),
EditorSettings { EditorSettings {
copilot: Some(OnOff::On), show_copilot_suggestions: Some(true),
..Default::default() ..Default::default()
}, },
); );
@ -1144,10 +1121,10 @@ mod tests {
{ {
"language_overrides": { "language_overrides": {
"Rust": { "Rust": {
"copilot": "on" "show_copilot_suggestions": true
}, },
"JSON": { "JSON": {
"copilot": "off" "show_copilot_suggestions": false
} }
} }
} }
@ -1163,21 +1140,21 @@ mod tests {
{ {
"languages": { "languages": {
"JSON": { "JSON": {
"copilot": "off" "show_copilot_suggestions": false
} }
} }
} }
"# "#
.unindent(), .unindent(),
|settings| { |settings| {
settings.editor.copilot = Some(OnOff::On); settings.editor.show_copilot_suggestions = Some(true);
}, },
r#" r#"
{ {
"copilot": "on", "show_copilot_suggestions": true,
"languages": { "languages": {
"JSON": { "JSON": {
"copilot": "off" "show_copilot_suggestions": false
} }
} }
} }
@ -1187,13 +1164,13 @@ mod tests {
} }
#[test] #[test]
fn test_update_langauge_copilot() { fn test_update_language_copilot() {
assert_new_settings( assert_new_settings(
r#" r#"
{ {
"languages": { "languages": {
"JSON": { "JSON": {
"copilot": "off" "show_copilot_suggestions": false
} }
} }
} }
@ -1203,7 +1180,7 @@ mod tests {
settings.languages.insert( settings.languages.insert(
"Rust".into(), "Rust".into(),
EditorSettings { EditorSettings {
copilot: Some(OnOff::On), show_copilot_suggestions: Some(true),
..Default::default() ..Default::default()
}, },
); );
@ -1212,10 +1189,10 @@ mod tests {
{ {
"languages": { "languages": {
"Rust": { "Rust": {
"copilot": "on" "show_copilot_suggestions": true
}, },
"JSON": { "JSON": {
"copilot": "off" "show_copilot_suggestions": false
} }
} }
} }

View File

@ -44,9 +44,7 @@ export default function editor(colorScheme: ColorScheme) {
activeLineBackground: withOpacity(background(layer, "on"), 0.75), activeLineBackground: withOpacity(background(layer, "on"), 0.75),
highlightedLineBackground: background(layer, "on"), highlightedLineBackground: background(layer, "on"),
// Inline autocomplete suggestions, Co-pilot suggestions, etc. // Inline autocomplete suggestions, Co-pilot suggestions, etc.
suggestion: { suggestion: syntax.predictive,
color: syntax.predictive.color,
},
codeActions: { codeActions: {
indicator: { indicator: {
color: foreground(layer, "variant"), color: foreground(layer, "variant"),

View File

@ -181,6 +181,7 @@ function buildDefaultSyntax(colorScheme: ColorScheme): Syntax {
}, },
predictive: { predictive: {
color: color.predictive, color: color.predictive,
italic: true,
}, },
emphasis: { emphasis: {
color: color.emphasis, color: color.emphasis,