Show inline completions inside the completion menu if both are available (#22093)
Screenshot:  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>
This commit is contained in:
parent
cc56ed7a88
commit
95334cb0ad
11 changed files with 661 additions and 429 deletions
|
@ -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(),
|
||||
);
|
||||
inlay_ids.push(inlay.id);
|
||||
inlays.push(inlay);
|
||||
}
|
||||
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,
|
||||
);
|
||||
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>,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue