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

Mikayla Maki created

Get copilot ready to ship

Change summary

assets/keymaps/default.json                 |   2 
assets/settings/default.json                |  13 +
crates/copilot/src/copilot.rs               |  13 +
crates/copilot/src/sign_in.rs               |  47 +++++
crates/copilot_button/src/copilot_button.rs | 162 ++++++++++++++-------
crates/editor/src/editor.rs                 | 168 ++++++++++++++++------
crates/settings/src/settings.rs             | 111 ++++++---------
styles/src/styleTree/editor.ts              |   4 
styles/src/themes/common/syntax.ts          |   1 
9 files changed, 333 insertions(+), 188 deletions(-)

Detailed changes

assets/keymaps/default.json 🔗

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

assets/settings/default.json 🔗

@@ -1,6 +1,11 @@
 {
     // The name of the Zed theme to use for the UI
     "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
     "buffer_font_family": "Zed Mono",
     // 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
     // which gives the same size as all other panes.
     "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
     "vim_mode": false,
     // 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
     // explicitly requesting it.
     "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.
     "show_call_status_icon": true,
     // Whether to use language servers to provide code intelligence.

crates/copilot/src/copilot.rs 🔗

@@ -29,7 +29,10 @@ const COPILOT_AUTH_NAMESPACE: &'static str = "copilot_auth";
 actions!(copilot_auth, [SignIn, SignOut]);
 
 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) {
     // Disable Copilot for stable releases.
@@ -172,7 +175,7 @@ impl Copilot {
             let http = http.clone();
             let node_runtime = node_runtime.clone();
             move |this, cx| {
-                if cx.global::<Settings>().enable_copilot_integration {
+                if cx.global::<Settings>().features.copilot {
                     if matches!(this.server, CopilotServer::Disabled) {
                         let start_task = cx
                             .spawn({
@@ -194,12 +197,14 @@ impl Copilot {
         })
         .detach();
 
-        if cx.global::<Settings>().enable_copilot_integration {
+        if cx.global::<Settings>().features.copilot {
             let start_task = cx
                 .spawn({
                     let http = http.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();
 

crates/copilot/src/sign_in.rs 🔗

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

crates/copilot_button/src/copilot_button.rs 🔗

@@ -24,6 +24,15 @@ const COPILOT_ERROR_TOAST_ID: usize = 1338;
 #[derive(Clone, PartialEq)]
 pub struct DeployCopilotMenu;
 
+#[derive(Clone, PartialEq)]
+pub struct DeployCopilotStartMenu;
+
+#[derive(Clone, PartialEq)]
+pub struct HideCopilot;
+
+#[derive(Clone, PartialEq)]
+pub struct InitiateSignIn;
+
 #[derive(Clone, PartialEq)]
 pub struct ToggleCopilotForLanguage {
     language: Arc<str>,
@@ -40,6 +49,9 @@ impl_internal_actions!(
     copilot,
     [
         DeployCopilotMenu,
+        DeployCopilotStartMenu,
+        HideCopilot,
+        InitiateSignIn,
         DeployCopilotModal,
         ToggleCopilotForLanguage,
         ToggleCopilotGlobally,
@@ -48,17 +60,19 @@ impl_internal_actions!(
 
 pub fn init(cx: &mut AppContext) {
     cx.add_action(CopilotButton::deploy_copilot_menu);
+    cx.add_action(CopilotButton::deploy_copilot_start_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));
+            let language = action.language.clone();
+            let show_copilot_suggestions = cx
+                .global::<Settings>()
+                .show_copilot_suggestions(Some(&language));
 
             SettingsFile::update(cx, move |file_contents| {
                 file_contents.languages.insert(
-                    language.to_owned(),
+                    language,
                     settings::EditorSettings {
-                        copilot: Some((!current_langauge).into()),
+                        show_copilot_suggestions: Some((!show_copilot_suggestions).into()),
                         ..Default::default()
                     },
                 );
@@ -67,12 +81,63 @@ pub fn init(cx: &mut AppContext) {
     );
 
     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| {
+            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.editor.copilot = Some((!copilot_on).into())
+            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 {
@@ -94,7 +159,7 @@ impl View for CopilotButton {
     fn render(&mut self, cx: &mut RenderContext<'_, Self>) -> ElementBox {
         let settings = cx.global::<Settings>();
 
-        if !settings.enable_copilot_integration {
+        if !settings.features.copilot {
             return Empty::new().boxed();
         }
 
@@ -105,9 +170,9 @@ impl View for CopilotButton {
         };
         let status = copilot.read(cx).status();
 
-        let enabled = self.editor_enabled.unwrap_or(settings.copilot_on(None));
-
-        let view_id = cx.view_id();
+        let enabled = self
+            .editor_enabled
+            .unwrap_or(settings.show_copilot_suggestions(None));
 
         Stack::new()
             .with_child(
@@ -155,48 +220,13 @@ impl View for CopilotButton {
                     let status = status.clone();
                     move |_, cx| match status {
                         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(
                             COPILOT_ERROR_TOAST_ID,
                             format!("Copilot can't be started: {}", e),
                             "Reinstall Copilot",
                             Reinstall,
                         )),
-                        _ => cx.dispatch_action(SignIn),
+                        _ => cx.dispatch_action(DeployCopilotStartMenu),
                     }
                 })
                 .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>) {
         let settings = cx.global::<Settings>();
 
         let mut menu_options = Vec::with_capacity(6);
 
         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(
                 format!(
-                    "{} Copilot for {}",
-                    if language_enabled {
-                        "Disable"
-                    } else {
-                        "Enable"
-                    },
+                    "{} Suggestions for {}",
+                    if language_enabled { "Hide" } else { "Show" },
                     language
                 ),
                 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(
             if globally_enabled {
-                "Disable Copilot Globally"
+                "Hide Suggestions for All Files"
             } else {
-                "Enable Copilot Globally"
+                "Show Suggestions for All Files"
             },
             ToggleCopilotGlobally,
         ));
@@ -319,7 +365,7 @@ impl CopilotButton {
 
         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()
     }

crates/editor/src/editor.rs 🔗

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

crates/settings/src/settings.rs 🔗

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

styles/src/styleTree/editor.ts 🔗

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

styles/src/themes/common/syntax.ts 🔗

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