copilot: Add the option to disable Next Edit Suggestions (#47438)

AndrΓ© Eriksson created

Adds a new setting to GitHub Copilot to toggle the Next Edit Suggestions
feature, it is enabled by default.

## Motivations
Due to some current usability issues with this feature, see #46880, and
some personal anecdotes of using it, it is currently rough to utilize,
so this gives the option to disable it.

## Related
- #47071 
- #30124
- #44486
## Release Notes
- Adds the ability to disable GitHub Copilot's Next Edit Suggestions
feature.
## User Interface

![image](https://github.com/user-attachments/assets/5a3d7166-68dd-4f5b-a220-0a9bd9282cd5)
## Text Example
The text example will be adding a `z` variable to a `Point3D` class in
TypeScript.
### With Next Edit Suggestions
In this example I am able to just press auto-complete (press TAB) 3x.
```ts
class Point3D {
    x: number;
    y: number;
    z: number; // <-- Cursor before z: suggested

    constructor(x: number, 
                y: number
                , z: number // <-- Next Suggestion
                ) { 
        this.x = x;
        this.y = y;
        this.z = z; // <-- Last Suggestion
    }
}
```
### Without Next Edit Suggestions
```ts
class Point3D {
    x: number;
    y: number;
    z: number; // <-- Cursor before z: the only suggestion

    constructor(x: number, y: number) {
        this.x = x;
        this.y = y;
    }
}
```

Change summary

assets/settings/default.json                            |   2 
crates/copilot/src/copilot.rs                           | 149 ++++++----
crates/edit_prediction_ui/src/edit_prediction_button.rs |  26 +
crates/language/src/language_settings.rs                |   3 
crates/settings_content/src/language.rs                 |   4 
5 files changed, 122 insertions(+), 62 deletions(-)

Detailed changes

assets/settings/default.json πŸ”—

@@ -1528,11 +1528,13 @@
     //   "enterprise_uri": "",
     //   "proxy": "",
     //   "proxy_no_verify": false
+    //   "enabled_next_edit_suggestions": true
     // },
     "copilot": {
       "enterprise_uri": null,
       "proxy": null,
       "proxy_no_verify": null,
+      "enabled_next_edit_suggestions": true,
     },
     "codestral": {
       "api_url": "https://codestral.mistral.ai",

crates/copilot/src/copilot.rs πŸ”—

@@ -9,12 +9,13 @@ use ::fs::Fs;
 use anyhow::{Context as _, Result, anyhow};
 use collections::{HashMap, HashSet};
 use command_palette_hooks::CommandPaletteFilter;
+use futures::future;
 use futures::{Future, FutureExt, TryFutureExt, channel::oneshot, future::Shared, select_biased};
 use gpui::{
     App, AppContext as _, AsyncApp, Context, Entity, EntityId, EventEmitter, Global, Task,
     WeakEntity, actions,
 };
-use language::language_settings::CopilotSettings;
+use language::language_settings::{AllLanguageSettings, CopilotSettings};
 use language::{
     Anchor, Bias, Buffer, BufferSnapshot, Language, PointUtf16, ToPointUtf16,
     language_settings::{EditPredictionProvider, all_language_settings},
@@ -928,21 +929,63 @@ impl Copilot {
         let hard_tabs = settings.hard_tabs;
         drop(settings);
 
+        let nes_enabled = AllLanguageSettings::get_global(cx)
+            .edit_predictions
+            .copilot
+            .enabled_next_edit_suggestions
+            .unwrap_or(true);
+
         cx.background_spawn(async move {
             let (version, snapshot) = pending_snapshot.await?;
             let lsp_position = point_to_lsp(position);
 
-            let nes_request = lsp
-                .request::<NextEditSuggestions>(request::NextEditSuggestionsParams {
+            let nes_fut = if nes_enabled {
+                lsp.request::<NextEditSuggestions>(request::NextEditSuggestionsParams {
                     text_document: lsp::VersionedTextDocumentIdentifier {
                         uri: uri.clone(),
                         version,
                     },
                     position: lsp_position,
                 })
-                .fuse();
+                .map(|resp| {
+                    resp.into_response()
+                        .ok()
+                        .map(|result| {
+                            result
+                                .edits
+                                .into_iter()
+                                .map(|completion| {
+                                    let start = snapshot.clip_point_utf16(
+                                        point_from_lsp(completion.range.start),
+                                        Bias::Left,
+                                    );
+                                    let end = snapshot.clip_point_utf16(
+                                        point_from_lsp(completion.range.end),
+                                        Bias::Left,
+                                    );
+                                    CopilotEditPrediction {
+                                        buffer: buffer_entity.clone(),
+                                        range: snapshot.anchor_before(start)
+                                            ..snapshot.anchor_after(end),
+                                        text: completion.text,
+                                        command: completion.command,
+                                        snapshot: snapshot.clone(),
+                                        source: CompletionSource::NextEditSuggestion,
+                                    }
+                                })
+                                .collect::<Vec<_>>()
+                        })
+                        .unwrap_or_default()
+                })
+                .left_future()
+                .fuse()
+            } else {
+                future::ready(Vec::<CopilotEditPrediction>::new())
+                    .right_future()
+                    .fuse()
+            };
 
-            let inline_request = lsp
+            let inline_fut = lsp
                 .request::<InlineCompletions>(request::InlineCompletionsParams {
                     text_document: lsp::VersionedTextDocumentIdentifier {
                         uri: uri.clone(),
@@ -957,74 +1000,56 @@ impl Copilot {
                         insert_spaces: !hard_tabs,
                     }),
                 })
-                .fuse();
-
-            futures::pin_mut!(nes_request, inline_request);
-
-            let convert_nes =
-                |result: request::NextEditSuggestionsResult| -> Vec<CopilotEditPrediction> {
-                    result
-                        .edits
-                        .into_iter()
-                        .map(|completion| {
-                            let start = snapshot.clip_point_utf16(
-                                point_from_lsp(completion.range.start),
-                                Bias::Left,
-                            );
-                            let end = snapshot
-                                .clip_point_utf16(point_from_lsp(completion.range.end), Bias::Left);
-                            CopilotEditPrediction {
-                                buffer: buffer_entity.clone(),
-                                range: snapshot.anchor_before(start)..snapshot.anchor_after(end),
-                                text: completion.text,
-                                command: completion.command,
-                                snapshot: snapshot.clone(),
-                                source: CompletionSource::NextEditSuggestion,
-                            }
+                .map(|resp| {
+                    resp.into_response()
+                        .ok()
+                        .map(|result| {
+                            result
+                                .items
+                                .into_iter()
+                                .map(|item| {
+                                    let start = snapshot.clip_point_utf16(
+                                        point_from_lsp(item.range.start),
+                                        Bias::Left,
+                                    );
+                                    let end = snapshot.clip_point_utf16(
+                                        point_from_lsp(item.range.end),
+                                        Bias::Left,
+                                    );
+                                    CopilotEditPrediction {
+                                        buffer: buffer_entity.clone(),
+                                        range: snapshot.anchor_before(start)
+                                            ..snapshot.anchor_after(end),
+                                        text: item.insert_text,
+                                        command: item.command,
+                                        snapshot: snapshot.clone(),
+                                        source: CompletionSource::InlineCompletion,
+                                    }
+                                })
+                                .collect::<Vec<_>>()
                         })
-                        .collect()
-                };
+                        .unwrap_or_default()
+                })
+                .fuse();
 
-            let convert_inline =
-                |result: request::InlineCompletionsResult| -> Vec<CopilotEditPrediction> {
-                    result
-                        .items
-                        .into_iter()
-                        .map(|item| {
-                            let start = snapshot
-                                .clip_point_utf16(point_from_lsp(item.range.start), Bias::Left);
-                            let end = snapshot
-                                .clip_point_utf16(point_from_lsp(item.range.end), Bias::Left);
-                            CopilotEditPrediction {
-                                buffer: buffer_entity.clone(),
-                                range: snapshot.anchor_before(start)..snapshot.anchor_after(end),
-                                text: item.insert_text,
-                                command: item.command,
-                                snapshot: snapshot.clone(),
-                                source: CompletionSource::InlineCompletion,
-                            }
-                        })
-                        .collect()
-                };
+            futures::pin_mut!(nes_fut, inline_fut);
 
             let mut nes_result: Option<Vec<CopilotEditPrediction>> = None;
             let mut inline_result: Option<Vec<CopilotEditPrediction>> = None;
 
             loop {
                 select_biased! {
-                    nes = nes_request => {
-                        let completions = nes.into_response().ok().map(convert_nes).unwrap_or_default();
-                        if !completions.is_empty() {
-                            return Ok(completions);
+                    nes = nes_fut => {
+                        if !nes.is_empty() {
+                            return Ok(nes);
                         }
-                        nes_result = Some(completions);
+                        nes_result = Some(nes);
                     }
-                    inline = inline_request => {
-                        let completions = inline.into_response().ok().map(convert_inline).unwrap_or_default();
-                        if !completions.is_empty() {
-                            return Ok(completions);
+                    inline = inline_fut => {
+                        if !inline.is_empty() {
+                            return Ok(inline);
                         }
-                        inline_result = Some(completions);
+                        inline_result = Some(inline);
                     }
                     complete => break,
                 }

crates/edit_prediction_ui/src/edit_prediction_button.rs πŸ”—

@@ -942,6 +942,11 @@ impl EditPredictionButton {
         cx: &mut Context<Self>,
     ) -> Entity<ContextMenu> {
         let all_language_settings = all_language_settings(None, cx);
+        let next_edit_suggestions = all_language_settings
+            .edit_predictions
+            .copilot
+            .enabled_next_edit_suggestions
+            .unwrap_or(true);
         let copilot_config = copilot_chat::CopilotChatConfiguration {
             enterprise_uri: all_language_settings
                 .edit_predictions
@@ -957,6 +962,27 @@ impl EditPredictionButton {
                 self.add_provider_switching_section(menu, EditPredictionProvider::Copilot, cx);
 
             menu.separator()
+                .item(
+                    ContextMenuEntry::new("Copilot: Next Edit Suggestions")
+                        .toggleable(IconPosition::Start, next_edit_suggestions)
+                        .handler({
+                            let fs = self.fs.clone();
+                            move |_, cx| {
+                                update_settings_file(fs.clone(), cx, move |settings, _| {
+                                    settings
+                                        .project
+                                        .all_languages
+                                        .edit_predictions
+                                        .get_or_insert_default()
+                                        .copilot
+                                        .get_or_insert_default()
+                                        .enabled_next_edit_suggestions =
+                                        Some(!next_edit_suggestions);
+                                });
+                            }
+                        }),
+                )
+                .separator()
                 .link(
                     "Go to Copilot Settings",
                     OpenBrowser { url: settings_url }.boxed_clone(),

crates/language/src/language_settings.rs πŸ”—

@@ -483,6 +483,8 @@ pub struct CopilotSettings {
     pub proxy_no_verify: Option<bool>,
     /// Enterprise URI for Copilot.
     pub enterprise_uri: Option<String>,
+    /// Whether the Copilot Next Edit Suggestions feature is enabled.
+    pub enabled_next_edit_suggestions: Option<bool>,
 }
 
 #[derive(Clone, Debug, Default)]
@@ -741,6 +743,7 @@ impl settings::Settings for AllLanguageSettings {
             proxy: copilot.proxy,
             proxy_no_verify: copilot.proxy_no_verify,
             enterprise_uri: copilot.enterprise_uri,
+            enabled_next_edit_suggestions: copilot.enabled_next_edit_suggestions,
         };
 
         let codestral = edit_predictions.codestral.unwrap();

crates/settings_content/src/language.rs πŸ”—

@@ -210,6 +210,10 @@ pub struct CopilotSettingsContent {
     ///
     /// Default: none
     pub enterprise_uri: Option<String>,
+    /// Whether the Copilot Next Edit Suggestions feature is enabled.
+    ///
+    /// Default: true
+    pub enabled_next_edit_suggestions: Option<bool>,
 }
 
 #[with_fallible_options]