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
|
@ -471,22 +471,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Editor && !inline_completion && showing_completions",
|
"context": "Editor && showing_completions",
|
||||||
"use_key_equivalents": true,
|
"use_key_equivalents": true,
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"enter": "editor::ConfirmCompletion",
|
"enter": "editor::ConfirmCompletion",
|
||||||
"tab": "editor::ComposeCompletion"
|
"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",
|
"context": "Editor && inline_completion && !showing_completions",
|
||||||
"use_key_equivalents": true,
|
"use_key_equivalents": true,
|
||||||
|
|
|
@ -542,22 +542,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"context": "Editor && !inline_completion && showing_completions",
|
"context": "Editor && showing_completions",
|
||||||
"use_key_equivalents": true,
|
"use_key_equivalents": true,
|
||||||
"bindings": {
|
"bindings": {
|
||||||
"enter": "editor::ConfirmCompletion",
|
"enter": "editor::ConfirmCompletion",
|
||||||
"tab": "editor::ComposeCompletion"
|
"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",
|
"context": "Editor && inline_completion && !showing_completions",
|
||||||
"use_key_equivalents": true,
|
"use_key_equivalents": true,
|
||||||
|
|
|
@ -55,6 +55,10 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
|
||||||
"copilot"
|
"copilot"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn display_name() -> &'static str {
|
||||||
|
"Copilot"
|
||||||
|
}
|
||||||
|
|
||||||
fn is_enabled(
|
fn is_enabled(
|
||||||
&self,
|
&self,
|
||||||
buffer: &Model<Buffer>,
|
buffer: &Model<Buffer>,
|
||||||
|
@ -324,10 +328,15 @@ mod tests {
|
||||||
cx.update_editor(|editor, cx| {
|
cx.update_editor(|editor, cx| {
|
||||||
// We want to show both: the inline completion and the completion menu
|
// We want to show both: the inline completion and the completion menu
|
||||||
assert!(editor.context_menu_visible());
|
assert!(editor.context_menu_visible());
|
||||||
|
assert!(editor.context_menu_contains_inline_completion());
|
||||||
assert!(editor.has_active_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.
|
// the copilot suggestion afterwards.
|
||||||
|
editor.context_menu_next(&Default::default(), cx);
|
||||||
editor
|
editor
|
||||||
.confirm_completion(&Default::default(), cx)
|
.confirm_completion(&Default::default(), cx)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
@ -338,13 +347,14 @@ mod tests {
|
||||||
assert_eq!(editor.display_text(cx), "one.completion_a\ntwo\nthree\n");
|
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! {"
|
cx.set_state(indoc! {"
|
||||||
oneˇ
|
oneˇ
|
||||||
two
|
two
|
||||||
three
|
three
|
||||||
"});
|
"});
|
||||||
cx.simulate_keystroke(".");
|
cx.simulate_keystroke(".");
|
||||||
|
|
||||||
drop(handle_completion_request(
|
drop(handle_completion_request(
|
||||||
&mut cx,
|
&mut cx,
|
||||||
indoc! {"
|
indoc! {"
|
||||||
|
@ -352,7 +362,7 @@ mod tests {
|
||||||
two
|
two
|
||||||
three
|
three
|
||||||
"},
|
"},
|
||||||
vec!["completion_a", "completion_b"],
|
vec![],
|
||||||
));
|
));
|
||||||
handle_copilot_completion_request(
|
handle_copilot_completion_request(
|
||||||
&copilot_lsp,
|
&copilot_lsp,
|
||||||
|
@ -365,16 +375,15 @@ mod tests {
|
||||||
);
|
);
|
||||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||||
cx.update_editor(|editor, cx| {
|
cx.update_editor(|editor, cx| {
|
||||||
assert!(editor.context_menu_visible());
|
assert!(!editor.context_menu_visible());
|
||||||
assert!(editor.has_active_inline_completion());
|
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.display_text(cx), "one.copilot1\ntwo\nthree\n");
|
||||||
assert_eq!(editor.text(cx), "one.\ntwo\nthree\n");
|
assert_eq!(editor.text(cx), "one.\ntwo\nthree\n");
|
||||||
});
|
});
|
||||||
|
|
||||||
// Ensure existing inline completion is interpolated when inserting again.
|
// Ensure existing inline completion is interpolated when inserting again.
|
||||||
cx.simulate_keystroke("c");
|
cx.simulate_keystroke("c");
|
||||||
// We still request a normal LSP completion, but we interpolate the
|
|
||||||
// existing inline completion.
|
|
||||||
drop(handle_completion_request(
|
drop(handle_completion_request(
|
||||||
&mut cx,
|
&mut cx,
|
||||||
indoc! {"
|
indoc! {"
|
||||||
|
@ -382,13 +391,16 @@ mod tests {
|
||||||
two
|
two
|
||||||
three
|
three
|
||||||
"},
|
"},
|
||||||
vec!["ompletion_a", "ompletion_b"],
|
vec!["completion_a", "completion_b"],
|
||||||
));
|
));
|
||||||
executor.run_until_parked();
|
executor.run_until_parked();
|
||||||
cx.update_editor(|editor, cx| {
|
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!(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");
|
assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -404,6 +416,14 @@ mod tests {
|
||||||
);
|
);
|
||||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||||
cx.update_editor(|editor, cx| {
|
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.context_menu_visible());
|
||||||
assert!(editor.has_active_inline_completion());
|
assert!(editor.has_active_inline_completion());
|
||||||
assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
|
assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
|
||||||
|
@ -908,8 +928,8 @@ mod tests {
|
||||||
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
executor.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
|
||||||
cx.update_editor(|editor, cx| {
|
cx.update_editor(|editor, cx| {
|
||||||
assert!(editor.context_menu_visible());
|
assert!(editor.context_menu_visible());
|
||||||
|
assert!(editor.context_menu_contains_inline_completion());
|
||||||
assert!(editor.has_active_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");
|
assert_eq!(editor.text(cx), "one\ntwo.\nthree\n");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,7 @@ use crate::{
|
||||||
render_parsed_markdown, split_words, styled_runs_for_code_label, CodeActionProvider,
|
render_parsed_markdown, split_words, styled_runs_for_code_label, CodeActionProvider,
|
||||||
CompletionId, CompletionProvider, DisplayRow, Editor, EditorStyle, ResolvedTasks,
|
CompletionId, CompletionProvider, DisplayRow, Editor, EditorStyle, ResolvedTasks,
|
||||||
};
|
};
|
||||||
|
use crate::{AcceptInlineCompletion, InlineCompletionMenuHint, InlineCompletionText};
|
||||||
|
|
||||||
pub enum CodeContextMenu {
|
pub enum CodeContextMenu {
|
||||||
Completions(CompletionsMenu),
|
Completions(CompletionsMenu),
|
||||||
|
@ -141,7 +142,7 @@ pub struct CompletionsMenu {
|
||||||
pub buffer: Model<Buffer>,
|
pub buffer: Model<Buffer>,
|
||||||
pub completions: Rc<RefCell<Box<[Completion]>>>,
|
pub completions: Rc<RefCell<Box<[Completion]>>>,
|
||||||
match_candidates: Rc<[StringMatchCandidate]>,
|
match_candidates: Rc<[StringMatchCandidate]>,
|
||||||
pub matches: Rc<[StringMatch]>,
|
pub entries: Rc<[CompletionEntry]>,
|
||||||
pub selected_item: usize,
|
pub selected_item: usize,
|
||||||
scroll_handle: UniformListScrollHandle,
|
scroll_handle: UniformListScrollHandle,
|
||||||
resolve_completions: bool,
|
resolve_completions: bool,
|
||||||
|
@ -149,6 +150,12 @@ pub struct CompletionsMenu {
|
||||||
show_completion_documentation: bool,
|
show_completion_documentation: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub(crate) enum CompletionEntry {
|
||||||
|
Match(StringMatch),
|
||||||
|
InlineCompletionHint(InlineCompletionMenuHint),
|
||||||
|
}
|
||||||
|
|
||||||
impl CompletionsMenu {
|
impl CompletionsMenu {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
id: CompletionId,
|
id: CompletionId,
|
||||||
|
@ -173,7 +180,7 @@ impl CompletionsMenu {
|
||||||
show_completion_documentation,
|
show_completion_documentation,
|
||||||
completions: RefCell::new(completions).into(),
|
completions: RefCell::new(completions).into(),
|
||||||
match_candidates,
|
match_candidates,
|
||||||
matches: Vec::new().into(),
|
entries: Vec::new().into(),
|
||||||
selected_item: 0,
|
selected_item: 0,
|
||||||
scroll_handle: UniformListScrollHandle::new(),
|
scroll_handle: UniformListScrollHandle::new(),
|
||||||
resolve_completions: true,
|
resolve_completions: true,
|
||||||
|
@ -210,14 +217,16 @@ impl CompletionsMenu {
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(id, completion)| StringMatchCandidate::new(id, &completion))
|
.map(|(id, completion)| StringMatchCandidate::new(id, &completion))
|
||||||
.collect();
|
.collect();
|
||||||
let matches = choices
|
let entries = choices
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(id, completion)| StringMatch {
|
.map(|(id, completion)| {
|
||||||
candidate_id: id,
|
CompletionEntry::Match(StringMatch {
|
||||||
score: 1.,
|
candidate_id: id,
|
||||||
positions: vec![],
|
score: 1.,
|
||||||
string: completion.clone(),
|
positions: vec![],
|
||||||
|
string: completion.clone(),
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
Self {
|
Self {
|
||||||
|
@ -227,7 +236,7 @@ impl CompletionsMenu {
|
||||||
buffer,
|
buffer,
|
||||||
completions: RefCell::new(completions).into(),
|
completions: RefCell::new(completions).into(),
|
||||||
match_candidates,
|
match_candidates,
|
||||||
matches,
|
entries,
|
||||||
selected_item: 0,
|
selected_item: 0,
|
||||||
scroll_handle: UniformListScrollHandle::new(),
|
scroll_handle: UniformListScrollHandle::new(),
|
||||||
resolve_completions: false,
|
resolve_completions: false,
|
||||||
|
@ -256,7 +265,7 @@ impl CompletionsMenu {
|
||||||
if self.selected_item > 0 {
|
if self.selected_item > 0 {
|
||||||
self.selected_item -= 1;
|
self.selected_item -= 1;
|
||||||
} else {
|
} else {
|
||||||
self.selected_item = self.matches.len() - 1;
|
self.selected_item = self.entries.len() - 1;
|
||||||
}
|
}
|
||||||
self.scroll_handle
|
self.scroll_handle
|
||||||
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
|
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
|
||||||
|
@ -269,7 +278,7 @@ impl CompletionsMenu {
|
||||||
provider: Option<&dyn CompletionProvider>,
|
provider: Option<&dyn CompletionProvider>,
|
||||||
cx: &mut ViewContext<Editor>,
|
cx: &mut ViewContext<Editor>,
|
||||||
) {
|
) {
|
||||||
if self.selected_item + 1 < self.matches.len() {
|
if self.selected_item + 1 < self.entries.len() {
|
||||||
self.selected_item += 1;
|
self.selected_item += 1;
|
||||||
} else {
|
} else {
|
||||||
self.selected_item = 0;
|
self.selected_item = 0;
|
||||||
|
@ -285,13 +294,33 @@ impl CompletionsMenu {
|
||||||
provider: Option<&dyn CompletionProvider>,
|
provider: Option<&dyn CompletionProvider>,
|
||||||
cx: &mut ViewContext<Editor>,
|
cx: &mut ViewContext<Editor>,
|
||||||
) {
|
) {
|
||||||
self.selected_item = self.matches.len() - 1;
|
self.selected_item = self.entries.len() - 1;
|
||||||
self.scroll_handle
|
self.scroll_handle
|
||||||
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
|
.scroll_to_item(self.selected_item, ScrollStrategy::Top);
|
||||||
self.resolve_selected_completion(provider, cx);
|
self.resolve_selected_completion(provider, cx);
|
||||||
cx.notify();
|
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(
|
pub fn resolve_selected_completion(
|
||||||
&mut self,
|
&mut self,
|
||||||
provider: Option<&dyn CompletionProvider>,
|
provider: Option<&dyn CompletionProvider>,
|
||||||
|
@ -304,24 +333,29 @@ impl CompletionsMenu {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let completion_index = self.matches[self.selected_item].candidate_id;
|
match &self.entries[self.selected_item] {
|
||||||
let resolve_task = provider.resolve_completions(
|
CompletionEntry::Match(entry) => {
|
||||||
self.buffer.clone(),
|
let completion_index = entry.candidate_id;
|
||||||
vec![completion_index],
|
let resolve_task = provider.resolve_completions(
|
||||||
self.completions.clone(),
|
self.buffer.clone(),
|
||||||
cx,
|
vec![completion_index],
|
||||||
);
|
self.completions.clone(),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
|
||||||
cx.spawn(move |editor, mut cx| async move {
|
cx.spawn(move |editor, mut cx| async move {
|
||||||
if let Some(true) = resolve_task.await.log_err() {
|
if let Some(true) = resolve_task.await.log_err() {
|
||||||
editor.update(&mut cx, |_, cx| cx.notify()).ok();
|
editor.update(&mut cx, |_, cx| cx.notify()).ok();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
}
|
}
|
||||||
})
|
CompletionEntry::InlineCompletionHint { .. } => {}
|
||||||
.detach();
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn visible(&self) -> bool {
|
pub fn visible(&self) -> bool {
|
||||||
!self.matches.is_empty()
|
!self.entries.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn origin(&self, cursor_position: DisplayPoint) -> ContextMenuOrigin {
|
fn origin(&self, cursor_position: DisplayPoint) -> ContextMenuOrigin {
|
||||||
|
@ -340,21 +374,27 @@ impl CompletionsMenu {
|
||||||
let completions = self.completions.borrow_mut();
|
let completions = self.completions.borrow_mut();
|
||||||
let show_completion_documentation = self.show_completion_documentation;
|
let show_completion_documentation = self.show_completion_documentation;
|
||||||
let widest_completion_ix = self
|
let widest_completion_ix = self
|
||||||
.matches
|
.entries
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.max_by_key(|(_, mat)| {
|
.max_by_key(|(_, mat)| match mat {
|
||||||
let completion = &completions[mat.candidate_id];
|
CompletionEntry::Match(mat) => {
|
||||||
let documentation = &completion.documentation;
|
let completion = &completions[mat.candidate_id];
|
||||||
|
let documentation = &completion.documentation;
|
||||||
|
|
||||||
let mut len = completion.label.text.chars().count();
|
let mut len = completion.label.text.chars().count();
|
||||||
if let Some(Documentation::SingleLine(text)) = documentation {
|
if let Some(Documentation::SingleLine(text)) = documentation {
|
||||||
if show_completion_documentation {
|
if show_completion_documentation {
|
||||||
len += text.chars().count();
|
len += text.chars().count();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
len
|
len
|
||||||
|
}
|
||||||
|
CompletionEntry::InlineCompletionHint(InlineCompletionMenuHint {
|
||||||
|
provider_name,
|
||||||
|
..
|
||||||
|
}) => provider_name.len(),
|
||||||
})
|
})
|
||||||
.map(|(ix, _)| ix);
|
.map(|(ix, _)| ix);
|
||||||
|
|
||||||
|
@ -362,24 +402,36 @@ impl CompletionsMenu {
|
||||||
let style = style.clone();
|
let style = style.clone();
|
||||||
|
|
||||||
let multiline_docs = if show_completion_documentation {
|
let multiline_docs = if show_completion_documentation {
|
||||||
let mat = &self.matches[selected_item];
|
match &self.entries[selected_item] {
|
||||||
match &completions[mat.candidate_id].documentation {
|
CompletionEntry::Match(mat) => match &completions[mat.candidate_id].documentation {
|
||||||
Some(Documentation::MultiLinePlainText(text)) => {
|
Some(Documentation::MultiLinePlainText(text)) => {
|
||||||
Some(div().child(SharedString::from(text.clone())))
|
Some(div().child(SharedString::from(text.clone())))
|
||||||
}
|
}
|
||||||
Some(Documentation::MultiLineMarkdown(parsed)) if !parsed.text.is_empty() => {
|
Some(Documentation::MultiLineMarkdown(parsed)) if !parsed.text.is_empty() => {
|
||||||
Some(div().child(render_parsed_markdown(
|
Some(div().child(render_parsed_markdown(
|
||||||
"completions_markdown",
|
"completions_markdown",
|
||||||
parsed,
|
parsed,
|
||||||
&style,
|
&style,
|
||||||
workspace,
|
workspace,
|
||||||
cx,
|
cx,
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
Some(Documentation::Undocumented) if self.aside_was_displayed.get() => {
|
Some(Documentation::Undocumented) if self.aside_was_displayed.get() => {
|
||||||
Some(div().child("No documentation"))
|
Some(div().child("No documentation"))
|
||||||
}
|
}
|
||||||
_ => None,
|
_ => 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 {
|
} else {
|
||||||
None
|
None
|
||||||
|
@ -409,7 +461,7 @@ impl CompletionsMenu {
|
||||||
|
|
||||||
drop(completions);
|
drop(completions);
|
||||||
let completions = self.completions.clone();
|
let completions = self.completions.clone();
|
||||||
let matches = self.matches.clone();
|
let matches = self.entries.clone();
|
||||||
let list = uniform_list(
|
let list = uniform_list(
|
||||||
cx.view().clone(),
|
cx.view().clone(),
|
||||||
"completions",
|
"completions",
|
||||||
|
@ -423,82 +475,111 @@ impl CompletionsMenu {
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(ix, mat)| {
|
.map(|(ix, mat)| {
|
||||||
let item_ix = start_ix + ix;
|
let item_ix = start_ix + ix;
|
||||||
let candidate_id = mat.candidate_id;
|
match mat {
|
||||||
let completion = &completions_guard[candidate_id];
|
CompletionEntry::Match(mat) => {
|
||||||
|
let candidate_id = mat.candidate_id;
|
||||||
|
let completion = &completions_guard[candidate_id];
|
||||||
|
|
||||||
let documentation = if show_completion_documentation {
|
let documentation = if show_completion_documentation {
|
||||||
&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 {
|
} else {
|
||||||
Some(
|
&None
|
||||||
Label::new(text.clone())
|
};
|
||||||
.ml_4()
|
|
||||||
.size(LabelSize::Small)
|
|
||||||
.color(Color::Muted),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let color_swatch = completion
|
let filter_start = completion.label.filter_range.start;
|
||||||
.color()
|
let highlights = gpui::combine_highlights(
|
||||||
.map(|color| div().size_4().bg(color).rounded_sm());
|
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;
|
||||||
|
|
||||||
div().min_w(px(220.)).max_w(px(540.)).child(
|
if completion.lsp_completion.deprecated.unwrap_or(false)
|
||||||
ListItem::new(mat.candidate_id)
|
{
|
||||||
.inset(true)
|
highlight.strikethrough =
|
||||||
.toggle_state(item_ix == selected_item)
|
Some(StrikethroughStyle {
|
||||||
.on_click(cx.listener(move |editor, _event, cx| {
|
thickness: 1.0.into(),
|
||||||
cx.stop_propagation();
|
..Default::default()
|
||||||
if let Some(task) = editor.confirm_completion(
|
});
|
||||||
&ConfirmCompletion {
|
highlight.color =
|
||||||
item_ix: Some(item_ix),
|
Some(cx.theme().colors().text_muted);
|
||||||
},
|
}
|
||||||
cx,
|
|
||||||
) {
|
(range, highlight)
|
||||||
task.detach_and_log_err(cx)
|
}),
|
||||||
}
|
);
|
||||||
}))
|
let completion_label =
|
||||||
.start_slot::<Div>(color_swatch)
|
StyledText::new(completion.label.text.clone())
|
||||||
.child(h_flex().overflow_hidden().child(completion_label))
|
.with_highlights(&style.text, highlights);
|
||||||
.end_slot::<Label>(documentation_label),
|
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()
|
.collect()
|
||||||
},
|
},
|
||||||
|
@ -611,7 +692,12 @@ impl CompletionsMenu {
|
||||||
}
|
}
|
||||||
drop(completions);
|
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;
|
self.selected_item = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,7 +73,7 @@ use fuzzy::StringMatchCandidate;
|
||||||
|
|
||||||
use code_context_menus::{
|
use code_context_menus::{
|
||||||
AvailableCodeAction, CodeActionContents, CodeActionsItem, CodeActionsMenu, CodeContextMenu,
|
AvailableCodeAction, CodeActionContents, CodeActionsItem, CodeActionsMenu, CodeContextMenu,
|
||||||
CompletionsMenu, ContextMenuOrigin,
|
CompletionEntry, CompletionsMenu, ContextMenuOrigin,
|
||||||
};
|
};
|
||||||
use git::blame::GitBlame;
|
use git::blame::GitBlame;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
|
@ -457,6 +457,21 @@ pub fn make_suggestion_styles(cx: &WindowContext) -> InlineCompletionStyles {
|
||||||
|
|
||||||
type CompletionId = usize;
|
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 {
|
enum InlineCompletion {
|
||||||
Edit(Vec<(Range<Anchor>, String)>),
|
Edit(Vec<(Range<Anchor>, String)>),
|
||||||
Move(Anchor),
|
Move(Anchor),
|
||||||
|
@ -2458,6 +2473,9 @@ impl Editor {
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.hide_context_menu(cx).is_some() {
|
if self.hide_context_menu(cx).is_some() {
|
||||||
|
if self.has_active_inline_completion() {
|
||||||
|
self.update_visible_inline_completion(cx);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3704,21 +3722,17 @@ impl Editor {
|
||||||
completions.into(),
|
completions.into(),
|
||||||
aside_was_displayed,
|
aside_was_displayed,
|
||||||
);
|
);
|
||||||
|
|
||||||
menu.filter(query.as_deref(), cx.background_executor().clone())
|
menu.filter(query.as_deref(), cx.background_executor().clone())
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
if menu.matches.is_empty() {
|
menu.visible().then_some(menu)
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(menu)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
editor.update(&mut cx, |editor, cx| {
|
editor.update(&mut cx, |editor, cx| {
|
||||||
let mut context_menu = editor.context_menu.borrow_mut();
|
match editor.context_menu.borrow().as_ref() {
|
||||||
match context_menu.as_ref() {
|
|
||||||
None => {}
|
None => {}
|
||||||
Some(CodeContextMenu::Completions(prev_menu)) => {
|
Some(CodeContextMenu::Completions(prev_menu)) => {
|
||||||
if prev_menu.id > id {
|
if prev_menu.id > id {
|
||||||
|
@ -3731,14 +3745,20 @@ impl Editor {
|
||||||
if editor.focus_handle.is_focused(cx) && menu.is_some() {
|
if editor.focus_handle.is_focused(cx) && menu.is_some() {
|
||||||
let mut menu = menu.unwrap();
|
let mut menu = menu.unwrap();
|
||||||
menu.resolve_selected_completion(editor.completion_provider.as_deref(), cx);
|
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();
|
cx.notify();
|
||||||
} else if editor.completion_tasks.len() <= 1 {
|
} else if editor.completion_tasks.len() <= 1 {
|
||||||
// If there are no more completion tasks and the last menu was
|
// If there are no more completion tasks and the last menu was
|
||||||
// empty, we should hide it. If it was already hidden, we should
|
// empty, we should hide it. If it was already hidden, we should
|
||||||
// also show the copilot completion when available.
|
// also show the copilot completion when available.
|
||||||
drop(context_menu);
|
|
||||||
editor.hide_context_menu(cx);
|
editor.hide_context_menu(cx);
|
||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
|
@ -3775,7 +3795,6 @@ impl Editor {
|
||||||
) -> Option<Task<std::result::Result<(), anyhow::Error>>> {
|
) -> Option<Task<std::result::Result<(), anyhow::Error>>> {
|
||||||
use language::ToOffset as _;
|
use language::ToOffset as _;
|
||||||
|
|
||||||
self.discard_inline_completion(true, cx);
|
|
||||||
let completions_menu =
|
let completions_menu =
|
||||||
if let CodeContextMenu::Completions(menu) = self.hide_context_menu(cx)? {
|
if let CodeContextMenu::Completions(menu) = self.hide_context_menu(cx)? {
|
||||||
menu
|
menu
|
||||||
|
@ -3784,8 +3803,21 @@ impl Editor {
|
||||||
};
|
};
|
||||||
|
|
||||||
let mat = completions_menu
|
let mat = completions_menu
|
||||||
.matches
|
.entries
|
||||||
.get(item_ix.unwrap_or(completions_menu.selected_item))?;
|
.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 buffer_handle = completions_menu.buffer;
|
||||||
let completions = completions_menu.completions.borrow_mut();
|
let completions = completions_menu.completions.borrow_mut();
|
||||||
let completion = completions.get(mat.candidate_id)?;
|
let completion = completions.get(mat.candidate_id)?;
|
||||||
|
@ -4668,6 +4700,17 @@ impl Editor {
|
||||||
Some(active_inline_completion.completion)
|
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<()> {
|
fn update_visible_inline_completion(&mut self, cx: &mut ViewContext<Self>) -> Option<()> {
|
||||||
let selection = self.selections.newest_anchor();
|
let selection = self.selections.newest_anchor();
|
||||||
let cursor = selection.head();
|
let cursor = selection.head();
|
||||||
|
@ -4739,32 +4782,34 @@ impl Editor {
|
||||||
invalidation_row_range = edit_start_row..cursor_row;
|
invalidation_row_range = edit_start_row..cursor_row;
|
||||||
completion = InlineCompletion::Move(first_edit_start);
|
completion = InlineCompletion::Move(first_edit_start);
|
||||||
} else {
|
} else {
|
||||||
if edits
|
if !self.has_active_completions_menu() {
|
||||||
.iter()
|
if edits
|
||||||
.all(|(range, _)| range.to_offset(&multibuffer).is_empty())
|
.iter()
|
||||||
{
|
.all(|(range, _)| range.to_offset(&multibuffer).is_empty())
|
||||||
let mut inlays = Vec::new();
|
{
|
||||||
for (range, new_text) in &edits {
|
let mut inlays = Vec::new();
|
||||||
let inlay = Inlay::inline_completion(
|
for (range, new_text) in &edits {
|
||||||
post_inc(&mut self.next_inlay_id),
|
let inlay = Inlay::inline_completion(
|
||||||
range.start,
|
post_inc(&mut self.next_inlay_id),
|
||||||
new_text.as_str(),
|
range.start,
|
||||||
);
|
new_text.as_str(),
|
||||||
inlay_ids.push(inlay.id);
|
);
|
||||||
inlays.push(inlay);
|
inlay_ids.push(inlay.id);
|
||||||
}
|
inlays.push(inlay);
|
||||||
|
}
|
||||||
|
|
||||||
self.splice_inlays(vec![], inlays, cx);
|
self.splice_inlays(vec![], inlays, cx);
|
||||||
} else {
|
} else {
|
||||||
let background_color = cx.theme().status().deleted_background;
|
let background_color = cx.theme().status().deleted_background;
|
||||||
self.highlight_text::<InlineCompletionHighlight>(
|
self.highlight_text::<InlineCompletionHighlight>(
|
||||||
edits.iter().map(|(range, _)| range.clone()).collect(),
|
edits.iter().map(|(range, _)| range.clone()).collect(),
|
||||||
HighlightStyle {
|
HighlightStyle {
|
||||||
background_color: Some(background_color),
|
background_color: Some(background_color),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
invalidation_row_range = edit_start_row..edit_end_row;
|
invalidation_row_range = edit_start_row..edit_end_row;
|
||||||
|
@ -4783,11 +4828,54 @@ impl Editor {
|
||||||
completion,
|
completion,
|
||||||
invalidation_range,
|
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();
|
cx.notify();
|
||||||
|
|
||||||
Some(())
|
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>> {
|
fn inline_completion_provider(&self) -> Option<Arc<dyn InlineCompletionProviderHandle>> {
|
||||||
Some(self.inline_completion_provider.as_ref()?.provider.clone())
|
Some(self.inline_completion_provider.as_ref()?.provider.clone())
|
||||||
}
|
}
|
||||||
|
@ -5002,6 +5090,19 @@ impl Editor {
|
||||||
.map_or(false, |menu| menu.visible())
|
.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> {
|
fn context_menu_origin(&self, cursor_position: DisplayPoint) -> Option<ContextMenuOrigin> {
|
||||||
self.context_menu
|
self.context_menu
|
||||||
.borrow()
|
.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(
|
pub fn highlight_diagnostic_message(
|
||||||
diagnostic: &Diagnostic,
|
diagnostic: &Diagnostic,
|
||||||
mut max_message_rows: Option<u8>,
|
mut max_message_rows: Option<u8>,
|
||||||
|
|
|
@ -8470,10 +8470,7 @@ async fn test_completion_page_up_down_keys(cx: &mut gpui::TestAppContext) {
|
||||||
cx.update_editor(|editor, _| {
|
cx.update_editor(|editor, _| {
|
||||||
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
|
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
|
||||||
{
|
{
|
||||||
assert_eq!(
|
assert_eq!(completion_menu_entries(&menu.entries), &["first", "last"]);
|
||||||
menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
|
|
||||||
&["first", "last"]
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
panic!("expected completion menu to be open");
|
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()
|
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
|
||||||
{
|
{
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
|
completion_menu_entries(&menu.entries),
|
||||||
&["r", "ret", "Range", "return"]
|
&["r", "ret", "Range", "return"]
|
||||||
);
|
);
|
||||||
} else {
|
} 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") {
|
match menu.as_ref().expect("should have the completions menu") {
|
||||||
CodeContextMenu::Completions(completions_menu) => {
|
CodeContextMenu::Completions(completions_menu) => {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
completions_menu
|
completion_menu_entries(&completions_menu.entries),
|
||||||
.matches
|
|
||||||
.iter()
|
|
||||||
.map(|c| c.string.as_str())
|
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
vec!["Some(2)", "vec![2]"]
|
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()
|
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
|
||||||
{
|
{
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
|
completion_menu_entries(&menu.entries),
|
||||||
&["bg-red", "bg-blue", "bg-yellow"]
|
&["bg-red", "bg-blue", "bg-yellow"]
|
||||||
);
|
);
|
||||||
} else {
|
} 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()
|
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
|
||||||
{
|
{
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
|
completion_menu_entries(&menu.entries),
|
||||||
&["bg-blue", "bg-yellow"]
|
&["bg-blue", "bg-yellow"]
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
@ -11096,16 +11089,23 @@ async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui:
|
||||||
cx.update_editor(|editor, _| {
|
cx.update_editor(|editor, _| {
|
||||||
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
|
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
|
||||||
{
|
{
|
||||||
assert_eq!(
|
assert_eq!(completion_menu_entries(&menu.entries), &["bg-yellow"]);
|
||||||
menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
|
|
||||||
&["bg-yellow"]
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
panic!("expected completion menu to be open");
|
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]
|
#[gpui::test]
|
||||||
async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
|
async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
|
||||||
init_test(cx, |settings| {
|
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> {
|
fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
|
||||||
let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
|
let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
|
||||||
point..point
|
point..point
|
||||||
|
|
|
@ -33,11 +33,11 @@ use gpui::{
|
||||||
anchored, deferred, div, fill, outline, point, px, quad, relative, size, svg,
|
anchored, deferred, div, fill, outline, point, px, quad, relative, size, svg,
|
||||||
transparent_black, Action, AnyElement, AvailableSpace, Bounds, ClickEvent, ClipboardItem,
|
transparent_black, Action, AnyElement, AvailableSpace, Bounds, ClickEvent, ClipboardItem,
|
||||||
ContentMask, Corner, Corners, CursorStyle, DispatchPhase, Edges, Element, ElementInputHandler,
|
ContentMask, Corner, Corners, CursorStyle, DispatchPhase, Edges, Element, ElementInputHandler,
|
||||||
Entity, FontId, GlobalElementId, HighlightStyle, Hitbox, Hsla, InteractiveElement, IntoElement,
|
Entity, FontId, GlobalElementId, Hitbox, Hsla, InteractiveElement, IntoElement, Length,
|
||||||
Length, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
|
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad,
|
||||||
PaintQuad, ParentElement, Pixels, ScrollDelta, ScrollWheelEvent, ShapedLine, SharedString,
|
ParentElement, Pixels, ScrollDelta, ScrollWheelEvent, ShapedLine, SharedString, Size,
|
||||||
Size, StatefulInteractiveElement, Style, Styled, Subscription, TextRun, TextStyleRefinement,
|
StatefulInteractiveElement, Style, Styled, Subscription, TextRun, TextStyleRefinement, View,
|
||||||
View, ViewContext, WeakView, WindowContext,
|
ViewContext, WeakView, WindowContext,
|
||||||
};
|
};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use language::{
|
use language::{
|
||||||
|
@ -2967,6 +2967,10 @@ impl EditorElement {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
InlineCompletion::Edit(edits) => {
|
InlineCompletion::Edit(edits) => {
|
||||||
|
if self.editor.read(cx).has_active_completions_menu() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
let edit_start = edits
|
let edit_start = edits
|
||||||
.first()
|
.first()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
|
@ -2990,7 +2994,11 @@ impl EditorElement {
|
||||||
return None;
|
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 line_count = text.lines().count() + 1;
|
||||||
|
|
||||||
let longest_row =
|
let longest_row =
|
||||||
|
@ -3010,7 +3018,7 @@ impl EditorElement {
|
||||||
};
|
};
|
||||||
|
|
||||||
let styled_text =
|
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()
|
let mut element = div()
|
||||||
.bg(cx.theme().colors().editor_background)
|
.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(
|
fn all_edits_insertions_or_deletions(
|
||||||
edits: &Vec<(Range<Anchor>, String)>,
|
edits: &Vec<(Range<Anchor>, String)>,
|
||||||
snapshot: &MultiBufferSnapshot,
|
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(
|
fn collect_invisibles_from_new_editor(
|
||||||
cx: &mut TestAppContext,
|
cx: &mut TestAppContext,
|
||||||
editor_mode: EditorMode,
|
editor_mode: EditorMode,
|
||||||
|
|
|
@ -317,6 +317,10 @@ impl InlineCompletionProvider for FakeInlineCompletionProvider {
|
||||||
"fake-completion-provider"
|
"fake-completion-provider"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn display_name() -> &'static str {
|
||||||
|
"Fake Completion Provider"
|
||||||
|
}
|
||||||
|
|
||||||
fn is_enabled(
|
fn is_enabled(
|
||||||
&self,
|
&self,
|
||||||
_buffer: &gpui::Model<language::Buffer>,
|
_buffer: &gpui::Model<language::Buffer>,
|
||||||
|
|
|
@ -19,6 +19,7 @@ pub struct InlineCompletion {
|
||||||
|
|
||||||
pub trait InlineCompletionProvider: 'static + Sized {
|
pub trait InlineCompletionProvider: 'static + Sized {
|
||||||
fn name() -> &'static str;
|
fn name() -> &'static str;
|
||||||
|
fn display_name() -> &'static str;
|
||||||
fn is_enabled(
|
fn is_enabled(
|
||||||
&self,
|
&self,
|
||||||
buffer: &Model<Buffer>,
|
buffer: &Model<Buffer>,
|
||||||
|
@ -51,6 +52,7 @@ pub trait InlineCompletionProvider: 'static + Sized {
|
||||||
|
|
||||||
pub trait InlineCompletionProviderHandle {
|
pub trait InlineCompletionProviderHandle {
|
||||||
fn name(&self) -> &'static str;
|
fn name(&self) -> &'static str;
|
||||||
|
fn display_name(&self) -> &'static str;
|
||||||
fn is_enabled(
|
fn is_enabled(
|
||||||
&self,
|
&self,
|
||||||
buffer: &Model<Buffer>,
|
buffer: &Model<Buffer>,
|
||||||
|
@ -89,6 +91,10 @@ where
|
||||||
T::name()
|
T::name()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn display_name(&self) -> &'static str {
|
||||||
|
T::display_name()
|
||||||
|
}
|
||||||
|
|
||||||
fn is_enabled(
|
fn is_enabled(
|
||||||
&self,
|
&self,
|
||||||
buffer: &Model<Buffer>,
|
buffer: &Model<Buffer>,
|
||||||
|
|
|
@ -98,6 +98,10 @@ impl InlineCompletionProvider for SupermavenCompletionProvider {
|
||||||
"supermaven"
|
"supermaven"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn display_name() -> &'static str {
|
||||||
|
"Supermaven"
|
||||||
|
}
|
||||||
|
|
||||||
fn is_enabled(&self, buffer: &Model<Buffer>, cursor_position: Anchor, cx: &AppContext) -> bool {
|
fn is_enabled(&self, buffer: &Model<Buffer>, cursor_position: Anchor, cx: &AppContext) -> bool {
|
||||||
if !self.supermaven.read(cx).is_enabled() {
|
if !self.supermaven.read(cx).is_enabled() {
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -930,6 +930,10 @@ impl inline_completion::InlineCompletionProvider for ZetaInlineCompletionProvide
|
||||||
"zeta"
|
"zeta"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn display_name() -> &'static str {
|
||||||
|
"Zeta"
|
||||||
|
}
|
||||||
|
|
||||||
fn is_enabled(
|
fn is_enabled(
|
||||||
&self,
|
&self,
|
||||||
buffer: &Model<Buffer>,
|
buffer: &Model<Buffer>,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue