Show inline completions inside the completion menu if both are available (#22093)

Thorsten Ball , Bennet , and Danilo created

Screenshot:

![screenshot-2024-12-17-11 53
41@2x](https://github.com/user-attachments/assets/bace3d20-7175-4833-9326-7b859166c0e8)

Demo:


https://github.com/user-attachments/assets/70197042-4785-4e45-80fd-29d12e68333f



(Note for Joseph/Peter: this supersedes
https://github.com/zed-industries/zed/pull/22069)

Release Notes:
- Changed inline completions to show up inside the normal completions in
case LSP and inline-completions are available. In that case, the inline
completion will be the first entry in the menu and can be selected with
`<tab>`.

---------

Co-authored-by: Bennet <bennet@zed.dev>
Co-authored-by: Danilo <danilo@zed.dev>

Change summary

assets/keymaps/default-linux.json                       |  11 
assets/keymaps/default-macos.json                       |  11 
crates/copilot/src/copilot_completion_provider.rs       |  40 
crates/editor/src/code_context_menus.rs                 | 352 ++++++----
crates/editor/src/editor.rs                             | 233 ++++++-
crates/editor/src/editor_tests.rs                       | 201 +++++
crates/editor/src/element.rs                            | 232 ------
crates/editor/src/inline_completion_tests.rs            |   4 
crates/inline_completion/src/inline_completion.rs       |   6 
crates/supermaven/src/supermaven_completion_provider.rs |   4 
crates/zeta/src/zeta.rs                                 |   4 
11 files changed, 665 insertions(+), 433 deletions(-)

Detailed changes

assets/keymaps/default-linux.json πŸ”—

@@ -471,22 +471,13 @@
     }
   },
   {
-    "context": "Editor && !inline_completion && showing_completions",
+    "context": "Editor && showing_completions",
     "use_key_equivalents": true,
     "bindings": {
       "enter": "editor::ConfirmCompletion",
       "tab": "editor::ComposeCompletion"
     }
   },
-  {
-    "context": "Editor && inline_completion && showing_completions",
-    "use_key_equivalents": true,
-    "bindings": {
-      "enter": "editor::ConfirmCompletion",
-      "tab": "editor::ComposeCompletion",
-      "shift-tab": "editor::AcceptInlineCompletion"
-    }
-  },
   {
     "context": "Editor && inline_completion && !showing_completions",
     "use_key_equivalents": true,

assets/keymaps/default-macos.json πŸ”—

@@ -542,22 +542,13 @@
     }
   },
   {
-    "context": "Editor && !inline_completion && showing_completions",
+    "context": "Editor && showing_completions",
     "use_key_equivalents": true,
     "bindings": {
       "enter": "editor::ConfirmCompletion",
       "tab": "editor::ComposeCompletion"
     }
   },
-  {
-    "context": "Editor && inline_completion && showing_completions",
-    "use_key_equivalents": true,
-    "bindings": {
-      "enter": "editor::ConfirmCompletion",
-      "tab": "editor::ComposeCompletion",
-      "shift-tab": "editor::AcceptInlineCompletion"
-    }
-  },
   {
     "context": "Editor && inline_completion && !showing_completions",
     "use_key_equivalents": true,

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

@@ -55,6 +55,10 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
         "copilot"
     }
 
+    fn display_name() -> &'static str {
+        "Copilot"
+    }
+
     fn is_enabled(
         &self,
         buffer: &Model<Buffer>,
@@ -324,10 +328,15 @@ mod tests {
         cx.update_editor(|editor, cx| {
             // We want to show both: the inline completion and the completion menu
             assert!(editor.context_menu_visible());
+            assert!(editor.context_menu_contains_inline_completion());
             assert!(editor.has_active_inline_completion());
+            // Since we have both, the copilot suggestion is not shown inline
+            assert_eq!(editor.text(cx), "one.\ntwo\nthree\n");
+            assert_eq!(editor.display_text(cx), "one.\ntwo\nthree\n");
 
-            // Confirming a completion inserts it and hides the context menu, without showing
+            // Confirming a non-copilot completion inserts it and hides the context menu, without showing
             // the copilot suggestion afterwards.
+            editor.context_menu_next(&Default::default(), cx);
             editor
                 .confirm_completion(&Default::default(), cx)
                 .unwrap()
@@ -338,13 +347,14 @@ mod tests {
             assert_eq!(editor.display_text(cx), "one.completion_a\ntwo\nthree\n");
         });
 
-        // Reset editor and test that accepting completions works
+        // Reset editor and only return copilot suggestions
         cx.set_state(indoc! {"
             oneˇ
             two
             three
         "});
         cx.simulate_keystroke(".");
+
         drop(handle_completion_request(
             &mut cx,
             indoc! {"
@@ -352,7 +362,7 @@ mod tests {
                 two
                 three
             "},
-            vec!["completion_a", "completion_b"],
+            vec![],
         ));
         handle_copilot_completion_request(
             &copilot_lsp,
@@ -365,16 +375,15 @@ mod tests {
         );
         executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
         cx.update_editor(|editor, cx| {
-            assert!(editor.context_menu_visible());
+            assert!(!editor.context_menu_visible());
             assert!(editor.has_active_inline_completion());
+            // Since only the copilot is available, it's shown inline
             assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
             assert_eq!(editor.text(cx), "one.\ntwo\nthree\n");
         });
 
         // Ensure existing inline completion is interpolated when inserting again.
         cx.simulate_keystroke("c");
-        // We still request a normal LSP completion, but we interpolate the
-        // existing inline completion.
         drop(handle_completion_request(
             &mut cx,
             indoc! {"
@@ -382,13 +391,16 @@ mod tests {
                 two
                 three
             "},
-            vec!["ompletion_a", "ompletion_b"],
+            vec!["completion_a", "completion_b"],
         ));
         executor.run_until_parked();
         cx.update_editor(|editor, cx| {
-            assert!(!editor.context_menu_visible());
+            // Since we have an LSP completion too, the inline completion is
+            // shown in the menu now
+            assert!(editor.context_menu_visible());
+            assert!(editor.context_menu_contains_inline_completion());
             assert!(editor.has_active_inline_completion());
-            assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
+            assert_eq!(editor.display_text(cx), "one.c\ntwo\nthree\n");
             assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
         });
 
@@ -404,6 +416,14 @@ mod tests {
         );
         executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
         cx.update_editor(|editor, cx| {
+            assert!(editor.context_menu_visible());
+            assert!(editor.has_active_inline_completion());
+            assert!(editor.context_menu_contains_inline_completion());
+            assert_eq!(editor.display_text(cx), "one.c\ntwo\nthree\n");
+            assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
+
+            // Canceling should first hide the menu and make Copilot suggestion visible.
+            editor.cancel(&Default::default(), cx);
             assert!(!editor.context_menu_visible());
             assert!(editor.has_active_inline_completion());
             assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
@@ -908,8 +928,8 @@ mod tests {
         executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
         cx.update_editor(|editor, cx| {
             assert!(editor.context_menu_visible());
+            assert!(editor.context_menu_contains_inline_completion());
             assert!(editor.has_active_inline_completion(),);
-            assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
             assert_eq!(editor.text(cx), "one\ntwo.\nthree\n");
         });
     }

crates/editor/src/code_context_menus.rs πŸ”—

@@ -28,6 +28,7 @@ use crate::{
     render_parsed_markdown, split_words, styled_runs_for_code_label, CodeActionProvider,
     CompletionId, CompletionProvider, DisplayRow, Editor, EditorStyle, ResolvedTasks,
 };
+use crate::{AcceptInlineCompletion, InlineCompletionMenuHint, InlineCompletionText};
 
 pub enum CodeContextMenu {
     Completions(CompletionsMenu),
@@ -141,7 +142,7 @@ pub struct CompletionsMenu {
     pub buffer: Model<Buffer>,
     pub completions: Rc<RefCell<Box<[Completion]>>>,
     match_candidates: Rc<[StringMatchCandidate]>,
-    pub matches: Rc<[StringMatch]>,
+    pub entries: Rc<[CompletionEntry]>,
     pub selected_item: usize,
     scroll_handle: UniformListScrollHandle,
     resolve_completions: bool,
@@ -149,6 +150,12 @@ pub struct CompletionsMenu {
     show_completion_documentation: bool,
 }
 
+#[derive(Clone, Debug)]
+pub(crate) enum CompletionEntry {
+    Match(StringMatch),
+    InlineCompletionHint(InlineCompletionMenuHint),
+}
+
 impl CompletionsMenu {
     pub fn new(
         id: CompletionId,
@@ -173,7 +180,7 @@ impl CompletionsMenu {
             show_completion_documentation,
             completions: RefCell::new(completions).into(),
             match_candidates,
-            matches: Vec::new().into(),
+            entries: Vec::new().into(),
             selected_item: 0,
             scroll_handle: UniformListScrollHandle::new(),
             resolve_completions: true,
@@ -210,14 +217,16 @@ impl CompletionsMenu {
             .enumerate()
             .map(|(id, completion)| StringMatchCandidate::new(id, &completion))
             .collect();
-        let matches = choices
+        let entries = choices
             .iter()
             .enumerate()
-            .map(|(id, completion)| StringMatch {
-                candidate_id: id,
-                score: 1.,
-                positions: vec![],
-                string: completion.clone(),
+            .map(|(id, completion)| {
+                CompletionEntry::Match(StringMatch {
+                    candidate_id: id,
+                    score: 1.,
+                    positions: vec![],
+                    string: completion.clone(),
+                })
             })
             .collect();
         Self {
@@ -227,7 +236,7 @@ impl CompletionsMenu {
             buffer,
             completions: RefCell::new(completions).into(),
             match_candidates,
-            matches,
+            entries,
             selected_item: 0,
             scroll_handle: UniformListScrollHandle::new(),
             resolve_completions: false,
@@ -256,7 +265,7 @@ impl CompletionsMenu {
         if self.selected_item > 0 {
             self.selected_item -= 1;
         } else {
-            self.selected_item = self.matches.len() - 1;
+            self.selected_item = self.entries.len() - 1;
         }
         self.scroll_handle
             .scroll_to_item(self.selected_item, ScrollStrategy::Top);
@@ -269,7 +278,7 @@ impl CompletionsMenu {
         provider: Option<&dyn CompletionProvider>,
         cx: &mut ViewContext<Editor>,
     ) {
-        if self.selected_item + 1 < self.matches.len() {
+        if self.selected_item + 1 < self.entries.len() {
             self.selected_item += 1;
         } else {
             self.selected_item = 0;
@@ -285,13 +294,33 @@ impl CompletionsMenu {
         provider: Option<&dyn CompletionProvider>,
         cx: &mut ViewContext<Editor>,
     ) {
-        self.selected_item = self.matches.len() - 1;
+        self.selected_item = self.entries.len() - 1;
         self.scroll_handle
             .scroll_to_item(self.selected_item, ScrollStrategy::Top);
         self.resolve_selected_completion(provider, cx);
         cx.notify();
     }
 
+    pub fn show_inline_completion_hint(&mut self, hint: InlineCompletionMenuHint) {
+        let hint = CompletionEntry::InlineCompletionHint(hint);
+
+        self.entries = match self.entries.first() {
+            Some(CompletionEntry::InlineCompletionHint { .. }) => {
+                let mut entries = Vec::from(&*self.entries);
+                entries[0] = hint;
+                entries
+            }
+            _ => {
+                let mut entries = Vec::with_capacity(self.entries.len() + 1);
+                entries.push(hint);
+                entries.extend_from_slice(&self.entries);
+                entries
+            }
+        }
+        .into();
+        self.selected_item = 0;
+    }
+
     pub fn resolve_selected_completion(
         &mut self,
         provider: Option<&dyn CompletionProvider>,
@@ -304,24 +333,29 @@ impl CompletionsMenu {
             return;
         };
 
-        let completion_index = self.matches[self.selected_item].candidate_id;
-        let resolve_task = provider.resolve_completions(
-            self.buffer.clone(),
-            vec![completion_index],
-            self.completions.clone(),
-            cx,
-        );
-
-        cx.spawn(move |editor, mut cx| async move {
-            if let Some(true) = resolve_task.await.log_err() {
-                editor.update(&mut cx, |_, cx| cx.notify()).ok();
+        match &self.entries[self.selected_item] {
+            CompletionEntry::Match(entry) => {
+                let completion_index = entry.candidate_id;
+                let resolve_task = provider.resolve_completions(
+                    self.buffer.clone(),
+                    vec![completion_index],
+                    self.completions.clone(),
+                    cx,
+                );
+
+                cx.spawn(move |editor, mut cx| async move {
+                    if let Some(true) = resolve_task.await.log_err() {
+                        editor.update(&mut cx, |_, cx| cx.notify()).ok();
+                    }
+                })
+                .detach();
             }
-        })
-        .detach();
+            CompletionEntry::InlineCompletionHint { .. } => {}
+        }
     }
 
-    fn visible(&self) -> bool {
-        !self.matches.is_empty()
+    pub fn visible(&self) -> bool {
+        !self.entries.is_empty()
     }
 
     fn origin(&self, cursor_position: DisplayPoint) -> ContextMenuOrigin {
@@ -340,21 +374,27 @@ impl CompletionsMenu {
         let completions = self.completions.borrow_mut();
         let show_completion_documentation = self.show_completion_documentation;
         let widest_completion_ix = self
-            .matches
+            .entries
             .iter()
             .enumerate()
-            .max_by_key(|(_, mat)| {
-                let completion = &completions[mat.candidate_id];
-                let documentation = &completion.documentation;
-
-                let mut len = completion.label.text.chars().count();
-                if let Some(Documentation::SingleLine(text)) = documentation {
-                    if show_completion_documentation {
-                        len += text.chars().count();
+            .max_by_key(|(_, mat)| match mat {
+                CompletionEntry::Match(mat) => {
+                    let completion = &completions[mat.candidate_id];
+                    let documentation = &completion.documentation;
+
+                    let mut len = completion.label.text.chars().count();
+                    if let Some(Documentation::SingleLine(text)) = documentation {
+                        if show_completion_documentation {
+                            len += text.chars().count();
+                        }
                     }
-                }
 
-                len
+                    len
+                }
+                CompletionEntry::InlineCompletionHint(InlineCompletionMenuHint {
+                    provider_name,
+                    ..
+                }) => provider_name.len(),
             })
             .map(|(ix, _)| ix);
 
@@ -362,24 +402,36 @@ impl CompletionsMenu {
         let style = style.clone();
 
         let multiline_docs = if show_completion_documentation {
-            let mat = &self.matches[selected_item];
-            match &completions[mat.candidate_id].documentation {
-                Some(Documentation::MultiLinePlainText(text)) => {
-                    Some(div().child(SharedString::from(text.clone())))
-                }
-                Some(Documentation::MultiLineMarkdown(parsed)) if !parsed.text.is_empty() => {
-                    Some(div().child(render_parsed_markdown(
-                        "completions_markdown",
-                        parsed,
-                        &style,
-                        workspace,
-                        cx,
-                    )))
-                }
-                Some(Documentation::Undocumented) if self.aside_was_displayed.get() => {
-                    Some(div().child("No documentation"))
-                }
-                _ => None,
+            match &self.entries[selected_item] {
+                CompletionEntry::Match(mat) => match &completions[mat.candidate_id].documentation {
+                    Some(Documentation::MultiLinePlainText(text)) => {
+                        Some(div().child(SharedString::from(text.clone())))
+                    }
+                    Some(Documentation::MultiLineMarkdown(parsed)) if !parsed.text.is_empty() => {
+                        Some(div().child(render_parsed_markdown(
+                            "completions_markdown",
+                            parsed,
+                            &style,
+                            workspace,
+                            cx,
+                        )))
+                    }
+                    Some(Documentation::Undocumented) if self.aside_was_displayed.get() => {
+                        Some(div().child("No documentation"))
+                    }
+                    _ => None,
+                },
+                CompletionEntry::InlineCompletionHint(hint) => Some(match &hint.text {
+                    InlineCompletionText::Edit { text, highlights } => div()
+                        .my_1()
+                        .rounded_md()
+                        .bg(cx.theme().colors().editor_background)
+                        .child(
+                            gpui::StyledText::new(text.clone())
+                                .with_highlights(&style.text, highlights.clone()),
+                        ),
+                    InlineCompletionText::Move(text) => div().child(text.clone()),
+                }),
             }
         } else {
             None
@@ -409,7 +461,7 @@ impl CompletionsMenu {
 
         drop(completions);
         let completions = self.completions.clone();
-        let matches = self.matches.clone();
+        let matches = self.entries.clone();
         let list = uniform_list(
             cx.view().clone(),
             "completions",
@@ -423,82 +475,111 @@ impl CompletionsMenu {
                     .enumerate()
                     .map(|(ix, mat)| {
                         let item_ix = start_ix + ix;
-                        let candidate_id = mat.candidate_id;
-                        let completion = &completions_guard[candidate_id];
-
-                        let documentation = if show_completion_documentation {
-                            &completion.documentation
-                        } else {
-                            &None
-                        };
-
-                        let filter_start = completion.label.filter_range.start;
-                        let highlights = gpui::combine_highlights(
-                            mat.ranges().map(|range| {
-                                (
-                                    filter_start + range.start..filter_start + range.end,
-                                    FontWeight::BOLD.into(),
-                                )
-                            }),
-                            styled_runs_for_code_label(&completion.label, &style.syntax).map(
-                                |(range, mut highlight)| {
-                                    // Ignore font weight for syntax highlighting, as we'll use it
-                                    // for fuzzy matches.
-                                    highlight.font_weight = None;
-
-                                    if completion.lsp_completion.deprecated.unwrap_or(false) {
-                                        highlight.strikethrough = Some(StrikethroughStyle {
-                                            thickness: 1.0.into(),
-                                            ..Default::default()
-                                        });
-                                        highlight.color = Some(cx.theme().colors().text_muted);
-                                    }
-
-                                    (range, highlight)
-                                },
-                            ),
-                        );
-                        let completion_label = StyledText::new(completion.label.text.clone())
-                            .with_highlights(&style.text, highlights);
-                        let documentation_label =
-                            if let Some(Documentation::SingleLine(text)) = documentation {
-                                if text.trim().is_empty() {
-                                    None
-                                } else {
-                                    Some(
-                                        Label::new(text.clone())
-                                            .ml_4()
-                                            .size(LabelSize::Small)
-                                            .color(Color::Muted),
-                                    )
-                                }
-                            } else {
-                                None
-                            };
-
-                        let color_swatch = completion
-                            .color()
-                            .map(|color| div().size_4().bg(color).rounded_sm());
+                        match mat {
+                            CompletionEntry::Match(mat) => {
+                                let candidate_id = mat.candidate_id;
+                                let completion = &completions_guard[candidate_id];
 
-                        div().min_w(px(220.)).max_w(px(540.)).child(
-                            ListItem::new(mat.candidate_id)
-                                .inset(true)
-                                .toggle_state(item_ix == selected_item)
-                                .on_click(cx.listener(move |editor, _event, cx| {
-                                    cx.stop_propagation();
-                                    if let Some(task) = editor.confirm_completion(
-                                        &ConfirmCompletion {
-                                            item_ix: Some(item_ix),
-                                        },
-                                        cx,
-                                    ) {
-                                        task.detach_and_log_err(cx)
-                                    }
-                                }))
-                                .start_slot::<Div>(color_swatch)
-                                .child(h_flex().overflow_hidden().child(completion_label))
-                                .end_slot::<Label>(documentation_label),
-                        )
+                                let documentation = if show_completion_documentation {
+                                    &completion.documentation
+                                } else {
+                                    &None
+                                };
+
+                                let filter_start = completion.label.filter_range.start;
+                                let highlights = gpui::combine_highlights(
+                                    mat.ranges().map(|range| {
+                                        (
+                                            filter_start + range.start..filter_start + range.end,
+                                            FontWeight::BOLD.into(),
+                                        )
+                                    }),
+                                    styled_runs_for_code_label(&completion.label, &style.syntax)
+                                        .map(|(range, mut highlight)| {
+                                            // Ignore font weight for syntax highlighting, as we'll use it
+                                            // for fuzzy matches.
+                                            highlight.font_weight = None;
+
+                                            if completion.lsp_completion.deprecated.unwrap_or(false)
+                                            {
+                                                highlight.strikethrough =
+                                                    Some(StrikethroughStyle {
+                                                        thickness: 1.0.into(),
+                                                        ..Default::default()
+                                                    });
+                                                highlight.color =
+                                                    Some(cx.theme().colors().text_muted);
+                                            }
+
+                                            (range, highlight)
+                                        }),
+                                );
+                                let completion_label =
+                                    StyledText::new(completion.label.text.clone())
+                                        .with_highlights(&style.text, highlights);
+                                let documentation_label =
+                                    if let Some(Documentation::SingleLine(text)) = documentation {
+                                        if text.trim().is_empty() {
+                                            None
+                                        } else {
+                                            Some(
+                                                Label::new(text.clone())
+                                                    .ml_4()
+                                                    .size(LabelSize::Small)
+                                                    .color(Color::Muted),
+                                            )
+                                        }
+                                    } else {
+                                        None
+                                    };
+
+                                let color_swatch = completion
+                                    .color()
+                                    .map(|color| div().size_4().bg(color).rounded_sm());
+
+                                div().min_w(px(220.)).max_w(px(540.)).child(
+                                    ListItem::new(mat.candidate_id)
+                                        .inset(true)
+                                        .toggle_state(item_ix == selected_item)
+                                        .on_click(cx.listener(move |editor, _event, cx| {
+                                            cx.stop_propagation();
+                                            if let Some(task) = editor.confirm_completion(
+                                                &ConfirmCompletion {
+                                                    item_ix: Some(item_ix),
+                                                },
+                                                cx,
+                                            ) {
+                                                task.detach_and_log_err(cx)
+                                            }
+                                        }))
+                                        .start_slot::<Div>(color_swatch)
+                                        .child(h_flex().overflow_hidden().child(completion_label))
+                                        .end_slot::<Label>(documentation_label),
+                                )
+                            }
+                            CompletionEntry::InlineCompletionHint(InlineCompletionMenuHint {
+                                provider_name,
+                                ..
+                            }) => div()
+                                .min_w(px(250.))
+                                .max_w(px(500.))
+                                .pb_1()
+                                .border_b_1()
+                                .border_color(cx.theme().colors().border_variant)
+                                .child(
+                                    ListItem::new("inline-completion")
+                                        .inset(true)
+                                        .toggle_state(item_ix == selected_item)
+                                        .on_click(cx.listener(move |editor, _event, cx| {
+                                            cx.stop_propagation();
+                                            editor.accept_inline_completion(
+                                                &AcceptInlineCompletion {},
+                                                cx,
+                                            );
+                                        }))
+                                        .child(Label::new(SharedString::new_static(provider_name))),
+                                ),
+                        }
                     })
                     .collect()
             },
@@ -611,7 +692,12 @@ impl CompletionsMenu {
         }
         drop(completions);
 
-        self.matches = matches.into();
+        let mut new_entries: Vec<_> = matches.into_iter().map(CompletionEntry::Match).collect();
+        if let Some(CompletionEntry::InlineCompletionHint(hint)) = self.entries.first() {
+            new_entries.insert(0, CompletionEntry::InlineCompletionHint(hint.clone()));
+        }
+
+        self.entries = new_entries.into();
         self.selected_item = 0;
     }
 }

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

@@ -73,7 +73,7 @@ use fuzzy::StringMatchCandidate;
 
 use code_context_menus::{
     AvailableCodeAction, CodeActionContents, CodeActionsItem, CodeActionsMenu, CodeContextMenu,
-    CompletionsMenu, ContextMenuOrigin,
+    CompletionEntry, CompletionsMenu, ContextMenuOrigin,
 };
 use git::blame::GitBlame;
 use gpui::{
@@ -457,6 +457,21 @@ pub fn make_suggestion_styles(cx: &WindowContext) -> InlineCompletionStyles {
 
 type CompletionId = usize;
 
+#[derive(Debug, Clone)]
+struct InlineCompletionMenuHint {
+    provider_name: &'static str,
+    text: InlineCompletionText,
+}
+
+#[derive(Clone, Debug)]
+enum InlineCompletionText {
+    Move(SharedString),
+    Edit {
+        text: SharedString,
+        highlights: Vec<(Range<usize>, HighlightStyle)>,
+    },
+}
+
 enum InlineCompletion {
     Edit(Vec<(Range<Anchor>, String)>),
     Move(Anchor),
@@ -2458,6 +2473,9 @@ impl Editor {
         }
 
         if self.hide_context_menu(cx).is_some() {
+            if self.has_active_inline_completion() {
+                self.update_visible_inline_completion(cx);
+            }
             return true;
         }
 
@@ -3704,21 +3722,17 @@ impl Editor {
                         completions.into(),
                         aside_was_displayed,
                     );
+
                     menu.filter(query.as_deref(), cx.background_executor().clone())
                         .await;
 
-                    if menu.matches.is_empty() {
-                        None
-                    } else {
-                        Some(menu)
-                    }
+                    menu.visible().then_some(menu)
                 } else {
                     None
                 };
 
                 editor.update(&mut cx, |editor, cx| {
-                    let mut context_menu = editor.context_menu.borrow_mut();
-                    match context_menu.as_ref() {
+                    match editor.context_menu.borrow().as_ref() {
                         None => {}
                         Some(CodeContextMenu::Completions(prev_menu)) => {
                             if prev_menu.id > id {
@@ -3731,14 +3745,20 @@ impl Editor {
                     if editor.focus_handle.is_focused(cx) && menu.is_some() {
                         let mut menu = menu.unwrap();
                         menu.resolve_selected_completion(editor.completion_provider.as_deref(), cx);
-                        *context_menu = Some(CodeContextMenu::Completions(menu));
-                        drop(context_menu);
+
+                        if let Some(hint) = editor.inline_completion_menu_hint(cx) {
+                            editor.hide_active_inline_completion(cx);
+                            menu.show_inline_completion_hint(hint);
+                        }
+
+                        *editor.context_menu.borrow_mut() =
+                            Some(CodeContextMenu::Completions(menu));
+
                         cx.notify();
                     } else if editor.completion_tasks.len() <= 1 {
                         // If there are no more completion tasks and the last menu was
                         // empty, we should hide it. If it was already hidden, we should
                         // also show the copilot completion when available.
-                        drop(context_menu);
                         editor.hide_context_menu(cx);
                     }
                 })?;
@@ -3775,7 +3795,6 @@ impl Editor {
     ) -> Option<Task<std::result::Result<(), anyhow::Error>>> {
         use language::ToOffset as _;
 
-        self.discard_inline_completion(true, cx);
         let completions_menu =
             if let CodeContextMenu::Completions(menu) = self.hide_context_menu(cx)? {
                 menu
@@ -3784,8 +3803,21 @@ impl Editor {
             };
 
         let mat = completions_menu
-            .matches
+            .entries
             .get(item_ix.unwrap_or(completions_menu.selected_item))?;
+
+        let mat = match mat {
+            CompletionEntry::InlineCompletionHint { .. } => {
+                self.accept_inline_completion(&AcceptInlineCompletion, cx);
+                cx.stop_propagation();
+                return Some(Task::ready(Ok(())));
+            }
+            CompletionEntry::Match(mat) => {
+                self.discard_inline_completion(true, cx);
+                mat
+            }
+        };
+
         let buffer_handle = completions_menu.buffer;
         let completions = completions_menu.completions.borrow_mut();
         let completion = completions.get(mat.candidate_id)?;
@@ -4668,6 +4700,17 @@ impl Editor {
         Some(active_inline_completion.completion)
     }
 
+    fn hide_active_inline_completion(&mut self, cx: &mut ViewContext<Self>) {
+        if let Some(active_inline_completion) = self.active_inline_completion.as_ref() {
+            self.splice_inlays(
+                active_inline_completion.inlay_ids.clone(),
+                Default::default(),
+                cx,
+            );
+            self.clear_highlights::<InlineCompletionHighlight>(cx);
+        }
+    }
+
     fn update_visible_inline_completion(&mut self, cx: &mut ViewContext<Self>) -> Option<()> {
         let selection = self.selections.newest_anchor();
         let cursor = selection.head();
@@ -4739,32 +4782,34 @@ impl Editor {
             invalidation_row_range = edit_start_row..cursor_row;
             completion = InlineCompletion::Move(first_edit_start);
         } else {
-            if edits
-                .iter()
-                .all(|(range, _)| range.to_offset(&multibuffer).is_empty())
-            {
-                let mut inlays = Vec::new();
-                for (range, new_text) in &edits {
-                    let inlay = Inlay::inline_completion(
-                        post_inc(&mut self.next_inlay_id),
-                        range.start,
-                        new_text.as_str(),
+            if !self.has_active_completions_menu() {
+                if edits
+                    .iter()
+                    .all(|(range, _)| range.to_offset(&multibuffer).is_empty())
+                {
+                    let mut inlays = Vec::new();
+                    for (range, new_text) in &edits {
+                        let inlay = Inlay::inline_completion(
+                            post_inc(&mut self.next_inlay_id),
+                            range.start,
+                            new_text.as_str(),
+                        );
+                        inlay_ids.push(inlay.id);
+                        inlays.push(inlay);
+                    }
+
+                    self.splice_inlays(vec![], inlays, cx);
+                } else {
+                    let background_color = cx.theme().status().deleted_background;
+                    self.highlight_text::<InlineCompletionHighlight>(
+                        edits.iter().map(|(range, _)| range.clone()).collect(),
+                        HighlightStyle {
+                            background_color: Some(background_color),
+                            ..Default::default()
+                        },
+                        cx,
                     );
-                    inlay_ids.push(inlay.id);
-                    inlays.push(inlay);
                 }
-
-                self.splice_inlays(vec![], inlays, cx);
-            } else {
-                let background_color = cx.theme().status().deleted_background;
-                self.highlight_text::<InlineCompletionHighlight>(
-                    edits.iter().map(|(range, _)| range.clone()).collect(),
-                    HighlightStyle {
-                        background_color: Some(background_color),
-                        ..Default::default()
-                    },
-                    cx,
-                );
             }
 
             invalidation_row_range = edit_start_row..edit_end_row;
@@ -4783,11 +4828,54 @@ impl Editor {
             completion,
             invalidation_range,
         });
+
+        if self.has_active_completions_menu() {
+            if let Some(hint) = self.inline_completion_menu_hint(cx) {
+                match self.context_menu.borrow_mut().as_mut() {
+                    Some(CodeContextMenu::Completions(menu)) => {
+                        menu.show_inline_completion_hint(hint);
+                    }
+                    _ => {}
+                }
+            }
+        }
+
         cx.notify();
 
         Some(())
     }
 
+    fn inline_completion_menu_hint(
+        &mut self,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<InlineCompletionMenuHint> {
+        if self.has_active_inline_completion() {
+            let provider_name = self.inline_completion_provider()?.display_name();
+            let editor_snapshot = self.snapshot(cx);
+
+            let text = match &self.active_inline_completion.as_ref()?.completion {
+                InlineCompletion::Edit(edits) => {
+                    inline_completion_edit_text(&editor_snapshot, edits, cx)
+                }
+                InlineCompletion::Move(target) => {
+                    let target_point =
+                        target.to_point(&editor_snapshot.display_snapshot.buffer_snapshot);
+                    let target_line = target_point.row + 1;
+                    InlineCompletionText::Move(
+                        format!("Jump to edit in line {}", target_line).into(),
+                    )
+                }
+            };
+
+            Some(InlineCompletionMenuHint {
+                provider_name,
+                text,
+            })
+        } else {
+            None
+        }
+    }
+
     fn inline_completion_provider(&self) -> Option<Arc<dyn InlineCompletionProviderHandle>> {
         Some(self.inline_completion_provider.as_ref()?.provider.clone())
     }
@@ -5002,6 +5090,19 @@ impl Editor {
             .map_or(false, |menu| menu.visible())
     }
 
+    #[cfg(feature = "test-support")]
+    pub fn context_menu_contains_inline_completion(&self) -> bool {
+        self.context_menu
+            .borrow()
+            .as_ref()
+            .map_or(false, |menu| match menu {
+                CodeContextMenu::Completions(menu) => menu.entries.first().map_or(false, |entry| {
+                    matches!(entry, CompletionEntry::InlineCompletionHint(_))
+                }),
+                CodeContextMenu::CodeActions(_) => false,
+            })
+    }
+
     fn context_menu_origin(&self, cursor_position: DisplayPoint) -> Option<ContextMenuOrigin> {
         self.context_menu
             .borrow()
@@ -14491,6 +14592,64 @@ pub fn diagnostic_block_renderer(
     })
 }
 
+fn inline_completion_edit_text(
+    editor_snapshot: &EditorSnapshot,
+    edits: &Vec<(Range<Anchor>, String)>,
+    cx: &WindowContext,
+) -> InlineCompletionText {
+    let edit_start = edits
+        .first()
+        .unwrap()
+        .0
+        .start
+        .to_display_point(editor_snapshot);
+
+    let mut text = String::new();
+    let mut offset = DisplayPoint::new(edit_start.row(), 0).to_offset(editor_snapshot, Bias::Left);
+    let mut highlights = Vec::new();
+    for (old_range, new_text) in edits {
+        let old_offset_range = old_range.to_offset(&editor_snapshot.buffer_snapshot);
+        text.extend(
+            editor_snapshot
+                .buffer_snapshot
+                .chunks(offset..old_offset_range.start, false)
+                .map(|chunk| chunk.text),
+        );
+        offset = old_offset_range.end;
+
+        let start = text.len();
+        text.push_str(new_text);
+        let end = text.len();
+        highlights.push((
+            start..end,
+            HighlightStyle {
+                background_color: Some(cx.theme().status().created_background),
+                ..Default::default()
+            },
+        ));
+    }
+
+    let edit_end = edits
+        .last()
+        .unwrap()
+        .0
+        .end
+        .to_display_point(editor_snapshot);
+    let end_of_line = DisplayPoint::new(edit_end.row(), editor_snapshot.line_len(edit_end.row()))
+        .to_offset(editor_snapshot, Bias::Right);
+    text.extend(
+        editor_snapshot
+            .buffer_snapshot
+            .chunks(offset..end_of_line, false)
+            .map(|chunk| chunk.text),
+    );
+
+    InlineCompletionText::Edit {
+        text: text.into(),
+        highlights,
+    }
+}
+
 pub fn highlight_diagnostic_message(
     diagnostic: &Diagnostic,
     mut max_message_rows: Option<u8>,

crates/editor/src/editor_tests.rs πŸ”—

@@ -8470,10 +8470,7 @@ async fn test_completion_page_up_down_keys(cx: &mut gpui::TestAppContext) {
     cx.update_editor(|editor, _| {
         if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
         {
-            assert_eq!(
-                menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
-                &["first", "last"]
-            );
+            assert_eq!(completion_menu_entries(&menu.entries), &["first", "last"]);
         } else {
             panic!("expected completion menu to be open");
         }
@@ -8566,7 +8563,7 @@ async fn test_completion_sort(cx: &mut gpui::TestAppContext) {
         if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
         {
             assert_eq!(
-                menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
+                completion_menu_entries(&menu.entries),
                 &["r", "ret", "Range", "return"]
             );
         } else {
@@ -10962,11 +10959,7 @@ async fn test_completions_default_resolve_data_handling(cx: &mut gpui::TestAppCo
         match menu.as_ref().expect("should have the completions menu") {
             CodeContextMenu::Completions(completions_menu) => {
                 assert_eq!(
-                    completions_menu
-                        .matches
-                        .iter()
-                        .map(|c| c.string.as_str())
-                        .collect::<Vec<_>>(),
+                    completion_menu_entries(&completions_menu.entries),
                     vec!["Some(2)", "vec![2]"]
                 );
             }
@@ -11066,7 +11059,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui:
         if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
         {
             assert_eq!(
-                menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
+                completion_menu_entries(&menu.entries),
                 &["bg-red", "bg-blue", "bg-yellow"]
             );
         } else {
@@ -11080,7 +11073,7 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui:
         if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
         {
             assert_eq!(
-                menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
+                completion_menu_entries(&menu.entries),
                 &["bg-blue", "bg-yellow"]
             );
         } else {
@@ -11096,16 +11089,23 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui:
     cx.update_editor(|editor, _| {
         if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
         {
-            assert_eq!(
-                menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
-                &["bg-yellow"]
-            );
+            assert_eq!(completion_menu_entries(&menu.entries), &["bg-yellow"]);
         } else {
             panic!("expected completion menu to be open");
         }
     });
 }
 
+fn completion_menu_entries(entries: &[CompletionEntry]) -> Vec<&str> {
+    entries
+        .iter()
+        .flat_map(|e| match e {
+            CompletionEntry::Match(mat) => Some(mat.string.as_str()),
+            _ => None,
+        })
+        .collect()
+}
+
 #[gpui::test]
 async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
     init_test(cx, |settings| {
@@ -14363,6 +14363,175 @@ async fn test_multi_buffer_with_single_excerpt_folding(cx: &mut gpui::TestAppCon
     );
 }
 
+#[gpui::test]
+fn test_inline_completion_text(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    // Test case 1: Simple insertion
+    {
+        let window = cx.add_window(|cx| {
+            let buffer = MultiBuffer::build_simple("Hello, world!", cx);
+            Editor::new(EditorMode::Full, buffer, None, true, cx)
+        });
+        let cx = &mut VisualTestContext::from_window(*window, cx);
+
+        window
+            .update(cx, |editor, cx| {
+                let snapshot = editor.snapshot(cx);
+                let edit_range = snapshot.buffer_snapshot.anchor_after(Point::new(0, 6))
+                    ..snapshot.buffer_snapshot.anchor_before(Point::new(0, 6));
+                let edits = vec![(edit_range, " beautiful".to_string())];
+
+                let InlineCompletionText::Edit { text, highlights } =
+                    inline_completion_edit_text(&snapshot, &edits, cx)
+                else {
+                    panic!("Failed to generate inline completion text");
+                };
+
+                assert_eq!(text, "Hello, beautiful world!");
+                assert_eq!(highlights.len(), 1);
+                assert_eq!(highlights[0].0, 6..16);
+                assert_eq!(
+                    highlights[0].1.background_color,
+                    Some(cx.theme().status().created_background)
+                );
+            })
+            .unwrap();
+    }
+
+    // Test case 2: Replacement
+    {
+        let window = cx.add_window(|cx| {
+            let buffer = MultiBuffer::build_simple("This is a test.", cx);
+            Editor::new(EditorMode::Full, buffer, None, true, cx)
+        });
+        let cx = &mut VisualTestContext::from_window(*window, cx);
+
+        window
+            .update(cx, |editor, cx| {
+                let snapshot = editor.snapshot(cx);
+                let edits = vec![(
+                    snapshot.buffer_snapshot.anchor_after(Point::new(0, 0))
+                        ..snapshot.buffer_snapshot.anchor_before(Point::new(0, 4)),
+                    "That".to_string(),
+                )];
+
+                let InlineCompletionText::Edit { text, highlights } =
+                    inline_completion_edit_text(&snapshot, &edits, cx)
+                else {
+                    panic!("Failed to generate inline completion text");
+                };
+
+                assert_eq!(text, "That is a test.");
+                assert_eq!(highlights.len(), 1);
+                assert_eq!(highlights[0].0, 0..4);
+                assert_eq!(
+                    highlights[0].1.background_color,
+                    Some(cx.theme().status().created_background)
+                );
+            })
+            .unwrap();
+    }
+
+    // Test case 3: Multiple edits
+    {
+        let window = cx.add_window(|cx| {
+            let buffer = MultiBuffer::build_simple("Hello, world!", cx);
+            Editor::new(EditorMode::Full, buffer, None, true, cx)
+        });
+        let cx = &mut VisualTestContext::from_window(*window, cx);
+
+        window
+            .update(cx, |editor, cx| {
+                let snapshot = editor.snapshot(cx);
+                let edits = vec![
+                    (
+                        snapshot.buffer_snapshot.anchor_after(Point::new(0, 0))
+                            ..snapshot.buffer_snapshot.anchor_before(Point::new(0, 5)),
+                        "Greetings".into(),
+                    ),
+                    (
+                        snapshot.buffer_snapshot.anchor_after(Point::new(0, 12))
+                            ..snapshot.buffer_snapshot.anchor_before(Point::new(0, 12)),
+                        " and universe".into(),
+                    ),
+                ];
+
+                let InlineCompletionText::Edit { text, highlights } =
+                    inline_completion_edit_text(&snapshot, &edits, cx)
+                else {
+                    panic!("Failed to generate inline completion text");
+                };
+
+                assert_eq!(text, "Greetings, world and universe!");
+                assert_eq!(highlights.len(), 2);
+                assert_eq!(highlights[0].0, 0..9);
+                assert_eq!(highlights[1].0, 16..29);
+                assert_eq!(
+                    highlights[0].1.background_color,
+                    Some(cx.theme().status().created_background)
+                );
+                assert_eq!(
+                    highlights[1].1.background_color,
+                    Some(cx.theme().status().created_background)
+                );
+            })
+            .unwrap();
+    }
+
+    // Test case 4: Multiple lines with edits
+    {
+        let window = cx.add_window(|cx| {
+            let buffer =
+                MultiBuffer::build_simple("First line\nSecond line\nThird line\nFourth line", cx);
+            Editor::new(EditorMode::Full, buffer, None, true, cx)
+        });
+        let cx = &mut VisualTestContext::from_window(*window, cx);
+
+        window
+            .update(cx, |editor, cx| {
+                let snapshot = editor.snapshot(cx);
+                let edits = vec![
+                    (
+                        snapshot.buffer_snapshot.anchor_before(Point::new(1, 7))
+                            ..snapshot.buffer_snapshot.anchor_before(Point::new(1, 11)),
+                        "modified".to_string(),
+                    ),
+                    (
+                        snapshot.buffer_snapshot.anchor_before(Point::new(2, 0))
+                            ..snapshot.buffer_snapshot.anchor_before(Point::new(2, 10)),
+                        "New third line".to_string(),
+                    ),
+                    (
+                        snapshot.buffer_snapshot.anchor_before(Point::new(3, 6))
+                            ..snapshot.buffer_snapshot.anchor_before(Point::new(3, 6)),
+                        " updated".to_string(),
+                    ),
+                ];
+
+                let InlineCompletionText::Edit { text, highlights } =
+                    inline_completion_edit_text(&snapshot, &edits, cx)
+                else {
+                    panic!("Failed to generate inline completion text");
+                };
+
+                assert_eq!(text, "Second modified\nNew third line\nFourth updated line");
+                assert_eq!(highlights.len(), 3);
+                assert_eq!(highlights[0].0, 7..15); // "modified"
+                assert_eq!(highlights[1].0, 16..30); // "New third line"
+                assert_eq!(highlights[2].0, 37..45); // " updated"
+
+                for highlight in &highlights {
+                    assert_eq!(
+                        highlight.1.background_color,
+                        Some(cx.theme().status().created_background)
+                    );
+                }
+            })
+            .unwrap();
+    }
+}
+
 fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
     let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
     point..point

crates/editor/src/element.rs πŸ”—

@@ -33,11 +33,11 @@ use gpui::{
     anchored, deferred, div, fill, outline, point, px, quad, relative, size, svg,
     transparent_black, Action, AnyElement, AvailableSpace, Bounds, ClickEvent, ClipboardItem,
     ContentMask, Corner, Corners, CursorStyle, DispatchPhase, Edges, Element, ElementInputHandler,
-    Entity, FontId, GlobalElementId, HighlightStyle, Hitbox, Hsla, InteractiveElement, IntoElement,
-    Length, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
-    PaintQuad, ParentElement, Pixels, ScrollDelta, ScrollWheelEvent, ShapedLine, SharedString,
-    Size, StatefulInteractiveElement, Style, Styled, Subscription, TextRun, TextStyleRefinement,
-    View, ViewContext, WeakView, WindowContext,
+    Entity, FontId, GlobalElementId, Hitbox, Hsla, InteractiveElement, IntoElement, Length,
+    ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad,
+    ParentElement, Pixels, ScrollDelta, ScrollWheelEvent, ShapedLine, SharedString, Size,
+    StatefulInteractiveElement, Style, Styled, Subscription, TextRun, TextStyleRefinement, View,
+    ViewContext, WeakView, WindowContext,
 };
 use itertools::Itertools;
 use language::{
@@ -2967,6 +2967,10 @@ impl EditorElement {
                 }
             }
             InlineCompletion::Edit(edits) => {
+                if self.editor.read(cx).has_active_completions_menu() {
+                    return None;
+                }
+
                 let edit_start = edits
                     .first()
                     .unwrap()
@@ -2990,7 +2994,11 @@ impl EditorElement {
                     return None;
                 }
 
-                let (text, highlights) = inline_completion_popover_text(editor_snapshot, edits, cx);
+                let crate::InlineCompletionText::Edit { text, highlights } =
+                    crate::inline_completion_edit_text(editor_snapshot, edits, cx)
+                else {
+                    return None;
+                };
                 let line_count = text.lines().count() + 1;
 
                 let longest_row =
@@ -3010,7 +3018,7 @@ impl EditorElement {
                 };
 
                 let styled_text =
-                    gpui::StyledText::new(text).with_highlights(&style.text, highlights);
+                    gpui::StyledText::new(text.clone()).with_highlights(&style.text, highlights);
 
                 let mut element = div()
                     .bg(cx.theme().colors().editor_background)
@@ -4519,61 +4527,6 @@ fn jump_data(
     }
 }
 
-fn inline_completion_popover_text(
-    editor_snapshot: &EditorSnapshot,
-    edits: &Vec<(Range<Anchor>, String)>,
-    cx: &WindowContext,
-) -> (String, Vec<(Range<usize>, HighlightStyle)>) {
-    let edit_start = edits
-        .first()
-        .unwrap()
-        .0
-        .start
-        .to_display_point(editor_snapshot);
-
-    let mut text = String::new();
-    let mut offset = DisplayPoint::new(edit_start.row(), 0).to_offset(editor_snapshot, Bias::Left);
-    let mut highlights = Vec::new();
-    for (old_range, new_text) in edits {
-        let old_offset_range = old_range.to_offset(&editor_snapshot.buffer_snapshot);
-        text.extend(
-            editor_snapshot
-                .buffer_snapshot
-                .chunks(offset..old_offset_range.start, false)
-                .map(|chunk| chunk.text),
-        );
-        offset = old_offset_range.end;
-
-        let start = text.len();
-        text.push_str(new_text);
-        let end = text.len();
-        highlights.push((
-            start..end,
-            HighlightStyle {
-                background_color: Some(cx.theme().status().created_background),
-                ..Default::default()
-            },
-        ));
-    }
-
-    let edit_end = edits
-        .last()
-        .unwrap()
-        .0
-        .end
-        .to_display_point(editor_snapshot);
-    let end_of_line = DisplayPoint::new(edit_end.row(), editor_snapshot.line_len(edit_end.row()))
-        .to_offset(editor_snapshot, Bias::Right);
-    text.extend(
-        editor_snapshot
-            .buffer_snapshot
-            .chunks(offset..end_of_line, false)
-            .map(|chunk| chunk.text),
-    );
-
-    (text, highlights)
-}
-
 fn all_edits_insertions_or_deletions(
     edits: &Vec<(Range<Anchor>, String)>,
     snapshot: &MultiBufferSnapshot,
@@ -7323,161 +7276,6 @@ mod tests {
         }
     }
 
-    #[gpui::test]
-    fn test_inline_completion_popover_text(cx: &mut TestAppContext) {
-        init_test(cx, |_| {});
-
-        // Test case 1: Simple insertion
-        {
-            let window = cx.add_window(|cx| {
-                let buffer = MultiBuffer::build_simple("Hello, world!", cx);
-                Editor::new(EditorMode::Full, buffer, None, true, cx)
-            });
-            let cx = &mut VisualTestContext::from_window(*window, cx);
-
-            window
-                .update(cx, |editor, cx| {
-                    let snapshot = editor.snapshot(cx);
-                    let edit_range = snapshot.buffer_snapshot.anchor_after(Point::new(0, 6))
-                        ..snapshot.buffer_snapshot.anchor_before(Point::new(0, 6));
-                    let edits = vec![(edit_range, " beautiful".to_string())];
-
-                    let (text, highlights) = inline_completion_popover_text(&snapshot, &edits, cx);
-
-                    assert_eq!(text, "Hello, beautiful world!");
-                    assert_eq!(highlights.len(), 1);
-                    assert_eq!(highlights[0].0, 6..16);
-                    assert_eq!(
-                        highlights[0].1.background_color,
-                        Some(cx.theme().status().created_background)
-                    );
-                })
-                .unwrap();
-        }
-
-        // Test case 2: Replacement
-        {
-            let window = cx.add_window(|cx| {
-                let buffer = MultiBuffer::build_simple("This is a test.", cx);
-                Editor::new(EditorMode::Full, buffer, None, true, cx)
-            });
-            let cx = &mut VisualTestContext::from_window(*window, cx);
-
-            window
-                .update(cx, |editor, cx| {
-                    let snapshot = editor.snapshot(cx);
-                    let edits = vec![(
-                        snapshot.buffer_snapshot.anchor_after(Point::new(0, 0))
-                            ..snapshot.buffer_snapshot.anchor_before(Point::new(0, 4)),
-                        "That".to_string(),
-                    )];
-
-                    let (text, highlights) = inline_completion_popover_text(&snapshot, &edits, cx);
-
-                    assert_eq!(text, "That is a test.");
-                    assert_eq!(highlights.len(), 1);
-                    assert_eq!(highlights[0].0, 0..4);
-                    assert_eq!(
-                        highlights[0].1.background_color,
-                        Some(cx.theme().status().created_background)
-                    );
-                })
-                .unwrap();
-        }
-
-        // Test case 3: Multiple edits
-        {
-            let window = cx.add_window(|cx| {
-                let buffer = MultiBuffer::build_simple("Hello, world!", cx);
-                Editor::new(EditorMode::Full, buffer, None, true, cx)
-            });
-            let cx = &mut VisualTestContext::from_window(*window, cx);
-
-            window
-                .update(cx, |editor, cx| {
-                    let snapshot = editor.snapshot(cx);
-                    let edits = vec![
-                        (
-                            snapshot.buffer_snapshot.anchor_after(Point::new(0, 0))
-                                ..snapshot.buffer_snapshot.anchor_before(Point::new(0, 5)),
-                            "Greetings".into(),
-                        ),
-                        (
-                            snapshot.buffer_snapshot.anchor_after(Point::new(0, 12))
-                                ..snapshot.buffer_snapshot.anchor_before(Point::new(0, 12)),
-                            " and universe".into(),
-                        ),
-                    ];
-
-                    let (text, highlights) = inline_completion_popover_text(&snapshot, &edits, cx);
-
-                    assert_eq!(text, "Greetings, world and universe!");
-                    assert_eq!(highlights.len(), 2);
-                    assert_eq!(highlights[0].0, 0..9);
-                    assert_eq!(highlights[1].0, 16..29);
-                    assert_eq!(
-                        highlights[0].1.background_color,
-                        Some(cx.theme().status().created_background)
-                    );
-                    assert_eq!(
-                        highlights[1].1.background_color,
-                        Some(cx.theme().status().created_background)
-                    );
-                })
-                .unwrap();
-        }
-
-        // Test case 4: Multiple lines with edits
-        {
-            let window = cx.add_window(|cx| {
-                let buffer = MultiBuffer::build_simple(
-                    "First line\nSecond line\nThird line\nFourth line",
-                    cx,
-                );
-                Editor::new(EditorMode::Full, buffer, None, true, cx)
-            });
-            let cx = &mut VisualTestContext::from_window(*window, cx);
-
-            window
-                .update(cx, |editor, cx| {
-                    let snapshot = editor.snapshot(cx);
-                    let edits = vec![
-                        (
-                            snapshot.buffer_snapshot.anchor_before(Point::new(1, 7))
-                                ..snapshot.buffer_snapshot.anchor_before(Point::new(1, 11)),
-                            "modified".to_string(),
-                        ),
-                        (
-                            snapshot.buffer_snapshot.anchor_before(Point::new(2, 0))
-                                ..snapshot.buffer_snapshot.anchor_before(Point::new(2, 10)),
-                            "New third line".to_string(),
-                        ),
-                        (
-                            snapshot.buffer_snapshot.anchor_before(Point::new(3, 6))
-                                ..snapshot.buffer_snapshot.anchor_before(Point::new(3, 6)),
-                            " updated".to_string(),
-                        ),
-                    ];
-
-                    let (text, highlights) = inline_completion_popover_text(&snapshot, &edits, cx);
-
-                    assert_eq!(text, "Second modified\nNew third line\nFourth updated line");
-                    assert_eq!(highlights.len(), 3);
-                    assert_eq!(highlights[0].0, 7..15); // "modified"
-                    assert_eq!(highlights[1].0, 16..30); // "New third line"
-                    assert_eq!(highlights[2].0, 37..45); // " updated"
-
-                    for highlight in &highlights {
-                        assert_eq!(
-                            highlight.1.background_color,
-                            Some(cx.theme().status().created_background)
-                        );
-                    }
-                })
-                .unwrap();
-        }
-    }
-
     fn collect_invisibles_from_new_editor(
         cx: &mut TestAppContext,
         editor_mode: EditorMode,

crates/editor/src/inline_completion_tests.rs πŸ”—

@@ -317,6 +317,10 @@ impl InlineCompletionProvider for FakeInlineCompletionProvider {
         "fake-completion-provider"
     }
 
+    fn display_name() -> &'static str {
+        "Fake Completion Provider"
+    }
+
     fn is_enabled(
         &self,
         _buffer: &gpui::Model<language::Buffer>,

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

@@ -19,6 +19,7 @@ pub struct InlineCompletion {
 
 pub trait InlineCompletionProvider: 'static + Sized {
     fn name() -> &'static str;
+    fn display_name() -> &'static str;
     fn is_enabled(
         &self,
         buffer: &Model<Buffer>,
@@ -51,6 +52,7 @@ pub trait InlineCompletionProvider: 'static + Sized {
 
 pub trait InlineCompletionProviderHandle {
     fn name(&self) -> &'static str;
+    fn display_name(&self) -> &'static str;
     fn is_enabled(
         &self,
         buffer: &Model<Buffer>,
@@ -89,6 +91,10 @@ where
         T::name()
     }
 
+    fn display_name(&self) -> &'static str {
+        T::display_name()
+    }
+
     fn is_enabled(
         &self,
         buffer: &Model<Buffer>,

crates/supermaven/src/supermaven_completion_provider.rs πŸ”—

@@ -98,6 +98,10 @@ impl InlineCompletionProvider for SupermavenCompletionProvider {
         "supermaven"
     }
 
+    fn display_name() -> &'static str {
+        "Supermaven"
+    }
+
     fn is_enabled(&self, buffer: &Model<Buffer>, cursor_position: Anchor, cx: &AppContext) -> bool {
         if !self.supermaven.read(cx).is_enabled() {
             return false;

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

@@ -930,6 +930,10 @@ impl inline_completion::InlineCompletionProvider for ZetaInlineCompletionProvide
         "zeta"
     }
 
+    fn display_name() -> &'static str {
+        "Zeta"
+    }
+
     fn is_enabled(
         &self,
         buffer: &Model<Buffer>,