Introduce staff-only inline completion provider (#21739)
Release Notes: - N/A --------- Co-authored-by: Thorsten Ball <mrnugget@gmail.com> Co-authored-by: Bennet <bennet@zed.dev> Co-authored-by: Thorsten <thorsten@zed.dev>
This commit is contained in:
parent
39e8944dcc
commit
77b8296fbb
39 changed files with 2890 additions and 356 deletions
|
@ -1125,6 +1125,12 @@ impl DisplaySnapshot {
|
|||
DisplayRow(self.block_snapshot.longest_row())
|
||||
}
|
||||
|
||||
pub fn longest_row_in_range(&self, range: Range<DisplayRow>) -> DisplayRow {
|
||||
let block_range = BlockRow(range.start.0)..BlockRow(range.end.0);
|
||||
let longest_row = self.block_snapshot.longest_row_in_range(block_range);
|
||||
DisplayRow(longest_row.0)
|
||||
}
|
||||
|
||||
pub fn starts_indent(&self, buffer_row: MultiBufferRow) -> bool {
|
||||
let max_row = self.buffer_snapshot.max_row();
|
||||
if buffer_row >= max_row {
|
||||
|
|
|
@ -1339,6 +1339,57 @@ impl BlockSnapshot {
|
|||
self.transforms.summary().longest_row
|
||||
}
|
||||
|
||||
pub fn longest_row_in_range(&self, range: Range<BlockRow>) -> BlockRow {
|
||||
let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&());
|
||||
cursor.seek(&range.start, Bias::Right, &());
|
||||
|
||||
let mut longest_row = range.start;
|
||||
let mut longest_row_chars = 0;
|
||||
if let Some(transform) = cursor.item() {
|
||||
if transform.block.is_none() {
|
||||
let (output_start, input_start) = cursor.start();
|
||||
let overshoot = range.start.0 - output_start.0;
|
||||
let wrap_start_row = input_start.0 + overshoot;
|
||||
let wrap_end_row = cmp::min(
|
||||
input_start.0 + (range.end.0 - output_start.0),
|
||||
cursor.end(&()).1 .0,
|
||||
);
|
||||
let summary = self
|
||||
.wrap_snapshot
|
||||
.text_summary_for_range(wrap_start_row..wrap_end_row);
|
||||
longest_row = BlockRow(range.start.0 + summary.longest_row);
|
||||
longest_row_chars = summary.longest_row_chars;
|
||||
}
|
||||
cursor.next(&());
|
||||
}
|
||||
|
||||
let cursor_start_row = cursor.start().0;
|
||||
if range.end > cursor_start_row {
|
||||
let summary = cursor.summary::<_, TransformSummary>(&range.end, Bias::Right, &());
|
||||
if summary.longest_row_chars > longest_row_chars {
|
||||
longest_row = BlockRow(cursor_start_row.0 + summary.longest_row);
|
||||
longest_row_chars = summary.longest_row_chars;
|
||||
}
|
||||
|
||||
if let Some(transform) = cursor.item() {
|
||||
if transform.block.is_none() {
|
||||
let (output_start, input_start) = cursor.start();
|
||||
let overshoot = range.end.0 - output_start.0;
|
||||
let wrap_start_row = input_start.0;
|
||||
let wrap_end_row = input_start.0 + overshoot;
|
||||
let summary = self
|
||||
.wrap_snapshot
|
||||
.text_summary_for_range(wrap_start_row..wrap_end_row);
|
||||
if summary.longest_row_chars > longest_row_chars {
|
||||
longest_row = BlockRow(output_start.0 + summary.longest_row);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
longest_row
|
||||
}
|
||||
|
||||
pub(super) fn line_len(&self, row: BlockRow) -> u32 {
|
||||
let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&());
|
||||
cursor.seek(&BlockRow(row.0), Bias::Right, &());
|
||||
|
@ -2705,6 +2756,40 @@ mod tests {
|
|||
longest_line_len,
|
||||
);
|
||||
|
||||
for _ in 0..10 {
|
||||
let end_row = rng.gen_range(1..=expected_lines.len());
|
||||
let start_row = rng.gen_range(0..end_row);
|
||||
|
||||
let mut expected_longest_rows_in_range = vec![];
|
||||
let mut longest_line_len_in_range = 0;
|
||||
|
||||
let mut row = start_row as u32;
|
||||
for line in &expected_lines[start_row..end_row] {
|
||||
let line_char_count = line.chars().count() as isize;
|
||||
match line_char_count.cmp(&longest_line_len_in_range) {
|
||||
Ordering::Less => {}
|
||||
Ordering::Equal => expected_longest_rows_in_range.push(row),
|
||||
Ordering::Greater => {
|
||||
longest_line_len_in_range = line_char_count;
|
||||
expected_longest_rows_in_range.clear();
|
||||
expected_longest_rows_in_range.push(row);
|
||||
}
|
||||
}
|
||||
row += 1;
|
||||
}
|
||||
|
||||
let longest_row_in_range = blocks_snapshot
|
||||
.longest_row_in_range(BlockRow(start_row as u32)..BlockRow(end_row as u32));
|
||||
assert!(
|
||||
expected_longest_rows_in_range.contains(&longest_row_in_range.0),
|
||||
"incorrect longest row {} in range {:?}. expected {:?} with length {}",
|
||||
longest_row,
|
||||
start_row..end_row,
|
||||
expected_longest_rows_in_range,
|
||||
longest_line_len_in_range,
|
||||
);
|
||||
}
|
||||
|
||||
// Ensure that conversion between block points and wrap points is stable.
|
||||
for row in 0..=blocks_snapshot.wrap_snapshot.max_point().row() {
|
||||
let wrap_point = WrapPoint::new(row, 0);
|
||||
|
|
|
@ -42,6 +42,8 @@ pub mod tasks;
|
|||
|
||||
#[cfg(test)]
|
||||
mod editor_tests;
|
||||
#[cfg(test)]
|
||||
mod inline_completion_tests;
|
||||
mod signature_help;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub mod test;
|
||||
|
@ -87,7 +89,7 @@ use hunk_diff::{diff_hunk_to_display, DiffMap, DiffMapSnapshot};
|
|||
use indent_guides::ActiveIndentGuidesState;
|
||||
use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy};
|
||||
pub use inline_completion::Direction;
|
||||
use inline_completion::{InlayProposal, InlineCompletionProvider, InlineCompletionProviderHandle};
|
||||
use inline_completion::{InlineCompletionProvider, InlineCompletionProviderHandle};
|
||||
pub use items::MAX_TAB_TITLE_LEN;
|
||||
use itertools::Itertools;
|
||||
use language::{
|
||||
|
@ -438,22 +440,19 @@ pub fn make_inlay_hints_style(cx: &WindowContext) -> HighlightStyle {
|
|||
|
||||
type CompletionId = usize;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct CompletionState {
|
||||
// render_inlay_ids represents the inlay hints that are inserted
|
||||
// for rendering the inline completions. They may be discontinuous
|
||||
// in the event that the completion provider returns some intersection
|
||||
// with the existing content.
|
||||
render_inlay_ids: Vec<InlayId>,
|
||||
// text is the resulting rope that is inserted when the user accepts a completion.
|
||||
text: Rope,
|
||||
// position is the position of the cursor when the completion was triggered.
|
||||
position: multi_buffer::Anchor,
|
||||
// delete_range is the range of text that this completion state covers.
|
||||
// if the completion is accepted, this range should be deleted.
|
||||
delete_range: Option<Range<multi_buffer::Anchor>>,
|
||||
enum InlineCompletion {
|
||||
Edit(Vec<(Range<Anchor>, String)>),
|
||||
Move(Anchor),
|
||||
}
|
||||
|
||||
struct InlineCompletionState {
|
||||
inlay_ids: Vec<InlayId>,
|
||||
completion: InlineCompletion,
|
||||
invalidation_range: Range<Anchor>,
|
||||
}
|
||||
|
||||
enum InlineCompletionHighlight {}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Debug, Default)]
|
||||
struct EditorActionId(usize);
|
||||
|
||||
|
@ -619,7 +618,7 @@ pub struct Editor {
|
|||
hovered_link_state: Option<HoveredLinkState>,
|
||||
inline_completion_provider: Option<RegisteredInlineCompletionProvider>,
|
||||
code_action_providers: Vec<Arc<dyn CodeActionProvider>>,
|
||||
active_inline_completion: Option<CompletionState>,
|
||||
active_inline_completion: Option<InlineCompletionState>,
|
||||
// enable_inline_completions is a switch that Vim can use to disable
|
||||
// inline completions based on its mode.
|
||||
enable_inline_completions: bool,
|
||||
|
@ -2250,7 +2249,7 @@ impl Editor {
|
|||
key_context.set("extension", extension.to_string());
|
||||
}
|
||||
|
||||
if self.has_active_inline_completion(cx) {
|
||||
if self.has_active_inline_completion() {
|
||||
key_context.add("copilot_suggestion");
|
||||
key_context.add("inline_completion");
|
||||
}
|
||||
|
@ -2760,7 +2759,7 @@ impl Editor {
|
|||
self.refresh_code_actions(cx);
|
||||
self.refresh_document_highlights(cx);
|
||||
refresh_matching_bracket_highlights(self, cx);
|
||||
self.discard_inline_completion(false, cx);
|
||||
self.update_visible_inline_completion(cx);
|
||||
linked_editing_ranges::refresh_linked_ranges(self, cx);
|
||||
if self.git_blame_inline_enabled {
|
||||
self.start_inline_blame_timer(cx);
|
||||
|
@ -3651,7 +3650,7 @@ impl Editor {
|
|||
);
|
||||
}
|
||||
|
||||
let had_active_inline_completion = this.has_active_inline_completion(cx);
|
||||
let had_active_inline_completion = this.has_active_inline_completion();
|
||||
this.change_selections_inner(Some(Autoscroll::fit()), false, cx, |s| {
|
||||
s.select(new_selections)
|
||||
});
|
||||
|
@ -4386,7 +4385,7 @@ impl Editor {
|
|||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.display_map.update(cx, |display_map, cx| {
|
||||
display_map.splice_inlays(to_remove, to_insert, cx);
|
||||
display_map.splice_inlays(to_remove, to_insert, cx)
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
|
@ -5243,7 +5242,8 @@ impl Editor {
|
|||
|
||||
if !user_requested
|
||||
&& (!self.enable_inline_completions
|
||||
|| !self.should_show_inline_completions(&buffer, cursor_buffer_position, cx))
|
||||
|| !self.should_show_inline_completions(&buffer, cursor_buffer_position, cx)
|
||||
|| !self.is_focused(cx))
|
||||
{
|
||||
self.discard_inline_completion(false, cx);
|
||||
return None;
|
||||
|
@ -5276,7 +5276,7 @@ impl Editor {
|
|||
}
|
||||
|
||||
pub fn show_inline_completion(&mut self, _: &ShowInlineCompletion, cx: &mut ViewContext<Self>) {
|
||||
if !self.has_active_inline_completion(cx) {
|
||||
if !self.has_active_inline_completion() {
|
||||
self.refresh_inline_completion(false, true, cx);
|
||||
return;
|
||||
}
|
||||
|
@ -5303,7 +5303,7 @@ impl Editor {
|
|||
}
|
||||
|
||||
pub fn next_inline_completion(&mut self, _: &NextInlineCompletion, cx: &mut ViewContext<Self>) {
|
||||
if self.has_active_inline_completion(cx) {
|
||||
if self.has_active_inline_completion() {
|
||||
self.cycle_inline_completion(Direction::Next, cx);
|
||||
} else {
|
||||
let is_copilot_disabled = self.refresh_inline_completion(false, true, cx).is_none();
|
||||
|
@ -5318,7 +5318,7 @@ impl Editor {
|
|||
_: &PreviousInlineCompletion,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if self.has_active_inline_completion(cx) {
|
||||
if self.has_active_inline_completion() {
|
||||
self.cycle_inline_completion(Direction::Prev, cx);
|
||||
} else {
|
||||
let is_copilot_disabled = self.refresh_inline_completion(false, true, cx).is_none();
|
||||
|
@ -5333,24 +5333,43 @@ impl Editor {
|
|||
_: &AcceptInlineCompletion,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
let Some(completion) = self.take_active_inline_completion(cx) else {
|
||||
let Some(active_inline_completion) = self.active_inline_completion.as_ref() else {
|
||||
return;
|
||||
};
|
||||
if let Some(provider) = self.inline_completion_provider() {
|
||||
provider.accept(cx);
|
||||
}
|
||||
|
||||
cx.emit(EditorEvent::InputHandled {
|
||||
utf16_range_to_replace: None,
|
||||
text: completion.text.to_string().into(),
|
||||
});
|
||||
self.report_inline_completion_event(true, cx);
|
||||
|
||||
if let Some(range) = completion.delete_range {
|
||||
self.change_selections(None, cx, |s| s.select_ranges([range]))
|
||||
match &active_inline_completion.completion {
|
||||
InlineCompletion::Move(position) => {
|
||||
let position = *position;
|
||||
self.change_selections(Some(Autoscroll::newest()), cx, |selections| {
|
||||
selections.select_anchor_ranges([position..position]);
|
||||
});
|
||||
}
|
||||
InlineCompletion::Edit(edits) => {
|
||||
if let Some(provider) = self.inline_completion_provider() {
|
||||
provider.accept(cx);
|
||||
}
|
||||
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
let last_edit_end = edits.last().unwrap().0.end.bias_right(&snapshot);
|
||||
|
||||
self.buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit(edits.iter().cloned(), None, cx)
|
||||
});
|
||||
|
||||
self.change_selections(None, cx, |s| {
|
||||
s.select_anchor_ranges([last_edit_end..last_edit_end])
|
||||
});
|
||||
|
||||
self.update_visible_inline_completion(cx);
|
||||
if self.active_inline_completion.is_none() {
|
||||
self.refresh_inline_completion(true, true, cx);
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
self.insert_with_autoindent_mode(&completion.text.to_string(), None, cx);
|
||||
self.refresh_inline_completion(true, true, cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn accept_partial_inline_completion(
|
||||
|
@ -5358,35 +5377,48 @@ impl Editor {
|
|||
_: &AcceptPartialInlineCompletion,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if self.selections.count() == 1 && self.has_active_inline_completion(cx) {
|
||||
if let Some(completion) = self.take_active_inline_completion(cx) {
|
||||
let mut partial_completion = completion
|
||||
.text
|
||||
.chars()
|
||||
.by_ref()
|
||||
.take_while(|c| c.is_alphabetic())
|
||||
.collect::<String>();
|
||||
if partial_completion.is_empty() {
|
||||
partial_completion = completion
|
||||
.text
|
||||
let Some(active_inline_completion) = self.active_inline_completion.as_ref() else {
|
||||
return;
|
||||
};
|
||||
if self.selections.count() != 1 {
|
||||
return;
|
||||
}
|
||||
|
||||
self.report_inline_completion_event(true, cx);
|
||||
|
||||
match &active_inline_completion.completion {
|
||||
InlineCompletion::Move(position) => {
|
||||
let position = *position;
|
||||
self.change_selections(Some(Autoscroll::newest()), cx, |selections| {
|
||||
selections.select_anchor_ranges([position..position]);
|
||||
});
|
||||
}
|
||||
InlineCompletion::Edit(edits) => {
|
||||
if edits.len() == 1 && edits[0].0.start == edits[0].0.end {
|
||||
let text = edits[0].1.as_str();
|
||||
let mut partial_completion = text
|
||||
.chars()
|
||||
.by_ref()
|
||||
.take_while(|c| c.is_whitespace() || !c.is_alphabetic())
|
||||
.take_while(|c| c.is_alphabetic())
|
||||
.collect::<String>();
|
||||
if partial_completion.is_empty() {
|
||||
partial_completion = text
|
||||
.chars()
|
||||
.by_ref()
|
||||
.take_while(|c| c.is_whitespace() || !c.is_alphabetic())
|
||||
.collect::<String>();
|
||||
}
|
||||
|
||||
cx.emit(EditorEvent::InputHandled {
|
||||
utf16_range_to_replace: None,
|
||||
text: partial_completion.clone().into(),
|
||||
});
|
||||
|
||||
self.insert_with_autoindent_mode(&partial_completion, None, cx);
|
||||
|
||||
self.refresh_inline_completion(true, true, cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
cx.emit(EditorEvent::InputHandled {
|
||||
utf16_range_to_replace: None,
|
||||
text: partial_completion.clone().into(),
|
||||
});
|
||||
|
||||
if let Some(range) = completion.delete_range {
|
||||
self.change_selections(None, cx, |s| s.select_ranges([range]))
|
||||
}
|
||||
self.insert_with_autoindent_mode(&partial_completion, None, cx);
|
||||
|
||||
self.refresh_inline_completion(true, true, cx);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5396,106 +5428,178 @@ impl Editor {
|
|||
should_report_inline_completion_event: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> bool {
|
||||
if should_report_inline_completion_event {
|
||||
self.report_inline_completion_event(false, cx);
|
||||
}
|
||||
|
||||
if let Some(provider) = self.inline_completion_provider() {
|
||||
provider.discard(should_report_inline_completion_event, cx);
|
||||
provider.discard(cx);
|
||||
}
|
||||
|
||||
self.take_active_inline_completion(cx).is_some()
|
||||
}
|
||||
|
||||
pub fn has_active_inline_completion(&self, cx: &AppContext) -> bool {
|
||||
if let Some(completion) = self.active_inline_completion.as_ref() {
|
||||
let buffer = self.buffer.read(cx).read(cx);
|
||||
completion.position.is_valid(&buffer)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
fn report_inline_completion_event(&self, accepted: bool, cx: &AppContext) {
|
||||
let Some(provider) = self.inline_completion_provider() else {
|
||||
return;
|
||||
};
|
||||
let Some(project) = self.project.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let Some((_, buffer, _)) = self
|
||||
.buffer
|
||||
.read(cx)
|
||||
.excerpt_containing(self.selections.newest_anchor().head(), cx)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let project = project.read(cx);
|
||||
let extension = buffer
|
||||
.read(cx)
|
||||
.file()
|
||||
.and_then(|file| Some(file.path().extension()?.to_string_lossy().to_string()));
|
||||
project.client().telemetry().report_inline_completion_event(
|
||||
provider.name().into(),
|
||||
accepted,
|
||||
extension,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn has_active_inline_completion(&self) -> bool {
|
||||
self.active_inline_completion.is_some()
|
||||
}
|
||||
|
||||
fn take_active_inline_completion(
|
||||
&mut self,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<CompletionState> {
|
||||
let completion = self.active_inline_completion.take()?;
|
||||
let render_inlay_ids = completion.render_inlay_ids.clone();
|
||||
self.display_map.update(cx, |map, cx| {
|
||||
map.splice_inlays(render_inlay_ids, Default::default(), cx);
|
||||
});
|
||||
let buffer = self.buffer.read(cx).read(cx);
|
||||
|
||||
if completion.position.is_valid(&buffer) {
|
||||
Some(completion)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
) -> Option<InlineCompletion> {
|
||||
let active_inline_completion = self.active_inline_completion.take()?;
|
||||
self.splice_inlays(active_inline_completion.inlay_ids, Default::default(), cx);
|
||||
self.clear_highlights::<InlineCompletionHighlight>(cx);
|
||||
Some(active_inline_completion.completion)
|
||||
}
|
||||
|
||||
fn update_visible_inline_completion(&mut self, cx: &mut ViewContext<Self>) {
|
||||
fn update_visible_inline_completion(&mut self, cx: &mut ViewContext<Self>) -> Option<()> {
|
||||
let selection = self.selections.newest_anchor();
|
||||
let cursor = selection.head();
|
||||
|
||||
let multibuffer = self.buffer.read(cx).snapshot(cx);
|
||||
let offset_selection = selection.map(|endpoint| endpoint.to_offset(&multibuffer));
|
||||
let excerpt_id = cursor.excerpt_id;
|
||||
|
||||
if self.context_menu.read().is_none()
|
||||
&& self.completion_tasks.is_empty()
|
||||
&& selection.start == selection.end
|
||||
if self.context_menu.read().is_some()
|
||||
|| (!self.completion_tasks.is_empty() && !self.has_active_inline_completion())
|
||||
|| !offset_selection.is_empty()
|
||||
|| self
|
||||
.active_inline_completion
|
||||
.as_ref()
|
||||
.map_or(false, |completion| {
|
||||
let invalidation_range = completion.invalidation_range.to_offset(&multibuffer);
|
||||
let invalidation_range = invalidation_range.start..=invalidation_range.end;
|
||||
!invalidation_range.contains(&offset_selection.head())
|
||||
})
|
||||
{
|
||||
if let Some(provider) = self.inline_completion_provider() {
|
||||
if let Some((buffer, cursor_buffer_position)) =
|
||||
self.buffer.read(cx).text_anchor_for_position(cursor, cx)
|
||||
{
|
||||
if let Some(proposal) =
|
||||
provider.active_completion_text(&buffer, cursor_buffer_position, cx)
|
||||
{
|
||||
let mut to_remove = Vec::new();
|
||||
if let Some(completion) = self.active_inline_completion.take() {
|
||||
to_remove.extend(completion.render_inlay_ids.iter());
|
||||
}
|
||||
|
||||
let to_add = proposal
|
||||
.inlays
|
||||
.iter()
|
||||
.filter_map(|inlay| {
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
let id = post_inc(&mut self.next_inlay_id);
|
||||
match inlay {
|
||||
InlayProposal::Hint(position, hint) => {
|
||||
let position =
|
||||
snapshot.anchor_in_excerpt(excerpt_id, *position)?;
|
||||
Some(Inlay::hint(id, position, hint))
|
||||
}
|
||||
InlayProposal::Suggestion(position, text) => {
|
||||
let position =
|
||||
snapshot.anchor_in_excerpt(excerpt_id, *position)?;
|
||||
Some(Inlay::suggestion(id, position, text.clone()))
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect_vec();
|
||||
|
||||
self.active_inline_completion = Some(CompletionState {
|
||||
position: cursor,
|
||||
text: proposal.text,
|
||||
delete_range: proposal.delete_range.and_then(|range| {
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
let start = snapshot.anchor_in_excerpt(excerpt_id, range.start);
|
||||
let end = snapshot.anchor_in_excerpt(excerpt_id, range.end);
|
||||
Some(start?..end?)
|
||||
}),
|
||||
render_inlay_ids: to_add.iter().map(|i| i.id).collect(),
|
||||
});
|
||||
|
||||
self.display_map
|
||||
.update(cx, move |map, cx| map.splice_inlays(to_remove, to_add, cx));
|
||||
|
||||
cx.notify();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.discard_inline_completion(false, cx);
|
||||
return None;
|
||||
}
|
||||
|
||||
self.discard_inline_completion(false, cx);
|
||||
self.take_active_inline_completion(cx);
|
||||
let provider = self.inline_completion_provider()?;
|
||||
|
||||
let (buffer, cursor_buffer_position) =
|
||||
self.buffer.read(cx).text_anchor_for_position(cursor, cx)?;
|
||||
|
||||
let completion = provider.suggest(&buffer, cursor_buffer_position, cx)?;
|
||||
let edits = completion
|
||||
.edits
|
||||
.into_iter()
|
||||
.map(|(range, new_text)| {
|
||||
(
|
||||
multibuffer
|
||||
.anchor_in_excerpt(excerpt_id, range.start)
|
||||
.unwrap()
|
||||
..multibuffer
|
||||
.anchor_in_excerpt(excerpt_id, range.end)
|
||||
.unwrap(),
|
||||
new_text,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if edits.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let first_edit_start = edits.first().unwrap().0.start;
|
||||
let edit_start_row = first_edit_start
|
||||
.to_point(&multibuffer)
|
||||
.row
|
||||
.saturating_sub(2);
|
||||
|
||||
let last_edit_end = edits.last().unwrap().0.end;
|
||||
let edit_end_row = cmp::min(
|
||||
multibuffer.max_point().row,
|
||||
last_edit_end.to_point(&multibuffer).row + 2,
|
||||
);
|
||||
|
||||
let cursor_row = cursor.to_point(&multibuffer).row;
|
||||
|
||||
let mut inlay_ids = Vec::new();
|
||||
let invalidation_row_range;
|
||||
let completion;
|
||||
if cursor_row < edit_start_row {
|
||||
invalidation_row_range = cursor_row..edit_end_row;
|
||||
completion = InlineCompletion::Move(first_edit_start);
|
||||
} else if cursor_row > edit_end_row {
|
||||
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::suggestion(
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
invalidation_row_range = edit_start_row..edit_end_row;
|
||||
completion = InlineCompletion::Edit(edits);
|
||||
};
|
||||
|
||||
let invalidation_range = multibuffer
|
||||
.anchor_before(Point::new(invalidation_row_range.start, 0))
|
||||
..multibuffer.anchor_after(Point::new(
|
||||
invalidation_row_range.end,
|
||||
multibuffer.line_len(MultiBufferRow(invalidation_row_range.end)),
|
||||
));
|
||||
|
||||
self.active_inline_completion = Some(InlineCompletionState {
|
||||
inlay_ids,
|
||||
completion,
|
||||
invalidation_range,
|
||||
});
|
||||
cx.notify();
|
||||
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn inline_completion_provider(&self) -> Option<Arc<dyn InlineCompletionProviderHandle>> {
|
||||
|
@ -12617,7 +12721,7 @@ impl Editor {
|
|||
self.active_indent_guides_state.dirty = true;
|
||||
self.refresh_active_diagnostics(cx);
|
||||
self.refresh_code_actions(cx);
|
||||
if self.has_active_inline_completion(cx) {
|
||||
if self.has_active_inline_completion() {
|
||||
self.update_visible_inline_completion(cx);
|
||||
}
|
||||
cx.emit(EditorEvent::BufferEdited);
|
||||
|
@ -13310,10 +13414,10 @@ impl Editor {
|
|||
}
|
||||
|
||||
pub fn display_to_pixel_point(
|
||||
&mut self,
|
||||
&self,
|
||||
source: DisplayPoint,
|
||||
editor_snapshot: &EditorSnapshot,
|
||||
cx: &mut ViewContext<Self>,
|
||||
cx: &WindowContext,
|
||||
) -> Option<gpui::Point<Pixels>> {
|
||||
let line_height = self.style()?.text.line_height_in_pixels(cx.rem_size());
|
||||
let text_layout_details = self.text_layout_details(cx);
|
||||
|
|
|
@ -19,10 +19,10 @@ use crate::{
|
|||
BlockId, ChunkReplacement, CodeActionsMenu, CursorShape, CustomBlockId, DisplayPoint,
|
||||
DisplayRow, DocumentHighlightRead, DocumentHighlightWrite, Editor, EditorMode, EditorSettings,
|
||||
EditorSnapshot, EditorStyle, ExpandExcerpts, FocusedBlock, GutterDimensions, HalfPageDown,
|
||||
HalfPageUp, HandleInput, HoveredCursor, HoveredHunk, JumpData, LineDown, LineUp, OpenExcerpts,
|
||||
PageDown, PageUp, Point, RowExt, RowRangeExt, SelectPhase, Selection, SoftWrap, ToPoint,
|
||||
CURSORS_VISIBLE_FOR, FILE_HEADER_HEIGHT, GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, MAX_LINE_LEN,
|
||||
MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
|
||||
HalfPageUp, HandleInput, HoveredCursor, HoveredHunk, InlineCompletion, JumpData, LineDown,
|
||||
LineUp, OpenExcerpts, PageDown, PageUp, Point, RowExt, RowRangeExt, SelectPhase, Selection,
|
||||
SoftWrap, ToPoint, CURSORS_VISIBLE_FOR, FILE_HEADER_HEIGHT,
|
||||
GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, MAX_LINE_LEN, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT,
|
||||
};
|
||||
use client::ParticipantIndex;
|
||||
use collections::{BTreeMap, HashMap, HashSet};
|
||||
|
@ -31,7 +31,7 @@ use gpui::{
|
|||
anchored, deferred, div, fill, outline, point, px, quad, relative, size, svg,
|
||||
transparent_black, Action, AnchorCorner, AnyElement, AvailableSpace, Bounds, ClipboardItem,
|
||||
ContentMask, Corners, CursorStyle, DispatchPhase, Edges, Element, ElementInputHandler, Entity,
|
||||
FontId, GlobalElementId, Hitbox, Hsla, InteractiveElement, IntoElement, Length,
|
||||
FontId, GlobalElementId, HighlightStyle, Hitbox, Hsla, InteractiveElement, IntoElement, Length,
|
||||
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad,
|
||||
ParentElement, Pixels, ScrollDelta, ScrollWheelEvent, ShapedLine, SharedString, Size,
|
||||
StatefulInteractiveElement, Style, Styled, TextRun, TextStyleRefinement, View, ViewContext,
|
||||
|
@ -47,7 +47,10 @@ use language::{
|
|||
ChunkRendererContext,
|
||||
};
|
||||
use lsp::DiagnosticSeverity;
|
||||
use multi_buffer::{Anchor, ExcerptId, ExpandExcerptDirection, MultiBufferPoint, MultiBufferRow};
|
||||
use multi_buffer::{
|
||||
Anchor, AnchorRangeExt, ExcerptId, ExpandExcerptDirection, MultiBufferPoint, MultiBufferRow,
|
||||
MultiBufferSnapshot,
|
||||
};
|
||||
use project::{
|
||||
project_settings::{GitGutterSetting, ProjectSettings},
|
||||
ProjectPath,
|
||||
|
@ -2720,6 +2723,157 @@ impl EditorElement {
|
|||
true
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_inline_completion_popover(
|
||||
&self,
|
||||
text_bounds: &Bounds<Pixels>,
|
||||
editor_snapshot: &EditorSnapshot,
|
||||
visible_row_range: Range<DisplayRow>,
|
||||
scroll_top: f32,
|
||||
scroll_bottom: f32,
|
||||
line_layouts: &[LineWithInvisibles],
|
||||
line_height: Pixels,
|
||||
scroll_pixel_position: gpui::Point<Pixels>,
|
||||
editor_width: Pixels,
|
||||
style: &EditorStyle,
|
||||
cx: &mut WindowContext,
|
||||
) -> Option<AnyElement> {
|
||||
const PADDING_X: Pixels = Pixels(25.);
|
||||
const PADDING_Y: Pixels = Pixels(2.);
|
||||
|
||||
let active_inline_completion = self.editor.read(cx).active_inline_completion.as_ref()?;
|
||||
|
||||
match &active_inline_completion.completion {
|
||||
InlineCompletion::Move(target_position) => {
|
||||
let container_element = div()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.rounded_md()
|
||||
.px_1();
|
||||
|
||||
let target_display_point = target_position.to_display_point(editor_snapshot);
|
||||
if target_display_point.row().as_f32() < scroll_top {
|
||||
let mut element = container_element
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(Icon::new(IconName::Tab))
|
||||
.child(Label::new("Jump to Edit"))
|
||||
.child(Icon::new(IconName::ArrowUp)),
|
||||
)
|
||||
.into_any();
|
||||
let size = element.layout_as_root(AvailableSpace::min_size(), cx);
|
||||
let offset = point((text_bounds.size.width - size.width) / 2., PADDING_Y);
|
||||
element.prepaint_at(text_bounds.origin + offset, cx);
|
||||
Some(element)
|
||||
} else if (target_display_point.row().as_f32() + 1.) > scroll_bottom {
|
||||
let mut element = container_element
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(Icon::new(IconName::Tab))
|
||||
.child(Label::new("Jump to Edit"))
|
||||
.child(Icon::new(IconName::ArrowDown)),
|
||||
)
|
||||
.into_any();
|
||||
let size = element.layout_as_root(AvailableSpace::min_size(), cx);
|
||||
let offset = point(
|
||||
(text_bounds.size.width - size.width) / 2.,
|
||||
text_bounds.size.height - size.height - PADDING_Y,
|
||||
);
|
||||
element.prepaint_at(text_bounds.origin + offset, cx);
|
||||
Some(element)
|
||||
} else {
|
||||
let mut element = container_element
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(Icon::new(IconName::Tab))
|
||||
.child(Label::new("Jump to Edit")),
|
||||
)
|
||||
.into_any();
|
||||
|
||||
let target_line_end = DisplayPoint::new(
|
||||
target_display_point.row(),
|
||||
editor_snapshot.line_len(target_display_point.row()),
|
||||
);
|
||||
let origin = self.editor.update(cx, |editor, cx| {
|
||||
editor.display_to_pixel_point(target_line_end, editor_snapshot, cx)
|
||||
})?;
|
||||
element.prepaint_as_root(
|
||||
text_bounds.origin + origin + point(PADDING_X, px(0.)),
|
||||
AvailableSpace::min_size(),
|
||||
cx,
|
||||
);
|
||||
Some(element)
|
||||
}
|
||||
}
|
||||
InlineCompletion::Edit(edits) => {
|
||||
let edit_start = edits
|
||||
.first()
|
||||
.unwrap()
|
||||
.0
|
||||
.start
|
||||
.to_display_point(editor_snapshot);
|
||||
let edit_end = edits
|
||||
.last()
|
||||
.unwrap()
|
||||
.0
|
||||
.end
|
||||
.to_display_point(editor_snapshot);
|
||||
|
||||
let is_visible = visible_row_range.contains(&edit_start.row())
|
||||
|| visible_row_range.contains(&edit_end.row());
|
||||
if !is_visible {
|
||||
return None;
|
||||
}
|
||||
|
||||
if all_edits_insertions_or_deletions(edits, &editor_snapshot.buffer_snapshot) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let (text, highlights) =
|
||||
inline_completion_popover_text(edit_start, editor_snapshot, edits, cx);
|
||||
|
||||
let longest_row =
|
||||
editor_snapshot.longest_row_in_range(edit_start.row()..edit_end.row() + 1);
|
||||
let longest_line_width = if visible_row_range.contains(&longest_row) {
|
||||
line_layouts[(longest_row.0 - visible_row_range.start.0) as usize].width
|
||||
} else {
|
||||
layout_line(
|
||||
longest_row,
|
||||
editor_snapshot,
|
||||
style,
|
||||
editor_width,
|
||||
|_| false,
|
||||
cx,
|
||||
)
|
||||
.width
|
||||
};
|
||||
|
||||
let text = gpui::StyledText::new(text).with_highlights(&style.text, highlights);
|
||||
|
||||
let mut element = div()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.rounded_md()
|
||||
.px_1()
|
||||
.child(text)
|
||||
.into_any();
|
||||
|
||||
let origin = text_bounds.origin
|
||||
+ point(
|
||||
longest_line_width + PADDING_X - scroll_pixel_position.x,
|
||||
edit_start.row().as_f32() * line_height - scroll_pixel_position.y,
|
||||
);
|
||||
element.prepaint_as_root(origin, AvailableSpace::min_size(), cx);
|
||||
Some(element)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn layout_mouse_context_menu(
|
||||
&self,
|
||||
editor_snapshot: &EditorSnapshot,
|
||||
|
@ -3942,6 +4096,16 @@ impl EditorElement {
|
|||
}
|
||||
}
|
||||
|
||||
fn paint_inline_completion_popover(
|
||||
&mut self,
|
||||
layout: &mut EditorLayout,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
if let Some(inline_completion_popover) = layout.inline_completion_popover.as_mut() {
|
||||
inline_completion_popover.paint(cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_mouse_context_menu(&mut self, layout: &mut EditorLayout, cx: &mut WindowContext) {
|
||||
if let Some(mouse_context_menu) = layout.mouse_context_menu.as_mut() {
|
||||
mouse_context_menu.paint(cx);
|
||||
|
@ -4134,6 +4298,67 @@ impl EditorElement {
|
|||
}
|
||||
}
|
||||
|
||||
fn inline_completion_popover_text(
|
||||
edit_start: DisplayPoint,
|
||||
editor_snapshot: &EditorSnapshot,
|
||||
edits: &Vec<(Range<Anchor>, String)>,
|
||||
cx: &WindowContext,
|
||||
) -> (String, Vec<(Range<usize>, HighlightStyle)>) {
|
||||
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()
|
||||
},
|
||||
));
|
||||
}
|
||||
(text, highlights)
|
||||
}
|
||||
|
||||
fn all_edits_insertions_or_deletions(
|
||||
edits: &Vec<(Range<Anchor>, String)>,
|
||||
snapshot: &MultiBufferSnapshot,
|
||||
) -> bool {
|
||||
let mut all_insertions = true;
|
||||
let mut all_deletions = true;
|
||||
|
||||
for (range, new_text) in edits.iter() {
|
||||
let range_is_empty = range.to_offset(&snapshot).is_empty();
|
||||
let text_is_empty = new_text.is_empty();
|
||||
|
||||
if range_is_empty != text_is_empty {
|
||||
if range_is_empty {
|
||||
all_deletions = false;
|
||||
} else {
|
||||
all_insertions = false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
if !all_insertions && !all_deletions {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
all_insertions || all_deletions
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn prepaint_gutter_button(
|
||||
button: IconButton,
|
||||
|
@ -5566,6 +5791,20 @@ impl Element for EditorElement {
|
|||
);
|
||||
}
|
||||
|
||||
let inline_completion_popover = self.layout_inline_completion_popover(
|
||||
&text_hitbox.bounds,
|
||||
&snapshot,
|
||||
start_row..end_row,
|
||||
scroll_position.y,
|
||||
scroll_position.y + height_in_lines,
|
||||
&line_layouts,
|
||||
line_height,
|
||||
scroll_pixel_position,
|
||||
editor_width,
|
||||
&style,
|
||||
cx,
|
||||
);
|
||||
|
||||
let mouse_context_menu = self.layout_mouse_context_menu(
|
||||
&snapshot,
|
||||
start_row..end_row,
|
||||
|
@ -5652,6 +5891,7 @@ impl Element for EditorElement {
|
|||
cursors,
|
||||
visible_cursors,
|
||||
selections,
|
||||
inline_completion_popover,
|
||||
mouse_context_menu,
|
||||
test_indicators,
|
||||
code_actions_indicator,
|
||||
|
@ -5741,6 +5981,7 @@ impl Element for EditorElement {
|
|||
}
|
||||
|
||||
self.paint_scrollbar(layout, cx);
|
||||
self.paint_inline_completion_popover(layout, cx);
|
||||
self.paint_mouse_context_menu(layout, cx);
|
||||
});
|
||||
})
|
||||
|
@ -5796,6 +6037,7 @@ pub struct EditorLayout {
|
|||
test_indicators: Vec<AnyElement>,
|
||||
crease_toggles: Vec<Option<AnyElement>>,
|
||||
crease_trailers: Vec<Option<CreaseTrailerLayout>>,
|
||||
inline_completion_popover: Option<AnyElement>,
|
||||
mouse_context_menu: Option<AnyElement>,
|
||||
tab_invisible: ShapedLine,
|
||||
space_invisible: ShapedLine,
|
||||
|
@ -6837,6 +7079,169 @@ 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 edit_start = DisplayPoint::new(DisplayRow(0), 6);
|
||||
let edits = vec![(edit_range, " beautiful".to_string())];
|
||||
|
||||
let (text, highlights) =
|
||||
inline_completion_popover_text(edit_start, &snapshot, &edits, cx);
|
||||
|
||||
assert_eq!(text, "Hello, beautiful");
|
||||
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 edit_start = DisplayPoint::new(DisplayRow(0), 0);
|
||||
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(edit_start, &snapshot, &edits, cx);
|
||||
|
||||
assert_eq!(text, "That");
|
||||
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 edit_start = DisplayPoint::new(DisplayRow(0), 0);
|
||||
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, 13)),
|
||||
" and universe".into(),
|
||||
),
|
||||
];
|
||||
|
||||
let (text, highlights) =
|
||||
inline_completion_popover_text(edit_start, &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 edit_start = DisplayPoint::new(DisplayRow(1), 0);
|
||||
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(edit_start, &snapshot, &edits, cx);
|
||||
|
||||
assert_eq!(text, "Second modified\nNew third line\nFourth updated");
|
||||
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,
|
||||
|
|
360
crates/editor/src/inline_completion_tests.rs
Normal file
360
crates/editor/src/inline_completion_tests.rs
Normal file
|
@ -0,0 +1,360 @@
|
|||
use gpui::Model;
|
||||
use indoc::indoc;
|
||||
use inline_completion::InlineCompletionProvider;
|
||||
use multi_buffer::{Anchor, MultiBufferSnapshot, ToPoint};
|
||||
use std::ops::Range;
|
||||
use text::{Point, ToOffset};
|
||||
use ui::Context;
|
||||
|
||||
use crate::{
|
||||
editor_tests::init_test, test::editor_test_context::EditorTestContext, InlineCompletion,
|
||||
};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_inline_completion_insert(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
let provider = cx.new_model(|_| FakeInlineCompletionProvider::default());
|
||||
assign_editor_completion_provider(provider.clone(), &mut cx);
|
||||
cx.set_state("let absolute_zero_celsius = ˇ;");
|
||||
|
||||
propose_edits(&provider, vec![(28..28, "-273.15")], &mut cx);
|
||||
cx.update_editor(|editor, cx| editor.update_visible_inline_completion(cx));
|
||||
|
||||
assert_editor_active_edit_completion(&mut cx, |_, edits| {
|
||||
assert_eq!(edits.len(), 1);
|
||||
assert_eq!(edits[0].1.as_str(), "-273.15");
|
||||
});
|
||||
|
||||
accept_completion(&mut cx);
|
||||
|
||||
cx.assert_editor_state("let absolute_zero_celsius = -273.15ˇ;")
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_inline_completion_modification(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
let provider = cx.new_model(|_| FakeInlineCompletionProvider::default());
|
||||
assign_editor_completion_provider(provider.clone(), &mut cx);
|
||||
cx.set_state("let pi = ˇ\"foo\";");
|
||||
|
||||
propose_edits(&provider, vec![(9..14, "3.14159")], &mut cx);
|
||||
cx.update_editor(|editor, cx| editor.update_visible_inline_completion(cx));
|
||||
|
||||
assert_editor_active_edit_completion(&mut cx, |_, edits| {
|
||||
assert_eq!(edits.len(), 1);
|
||||
assert_eq!(edits[0].1.as_str(), "3.14159");
|
||||
});
|
||||
|
||||
accept_completion(&mut cx);
|
||||
|
||||
cx.assert_editor_state("let pi = 3.14159ˇ;")
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_inline_completion_jump_button(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
let provider = cx.new_model(|_| FakeInlineCompletionProvider::default());
|
||||
assign_editor_completion_provider(provider.clone(), &mut cx);
|
||||
|
||||
// Cursor is 2+ lines above the proposed edit
|
||||
cx.set_state(indoc! {"
|
||||
line 0
|
||||
line ˇ1
|
||||
line 2
|
||||
line 3
|
||||
line
|
||||
"});
|
||||
|
||||
propose_edits(
|
||||
&provider,
|
||||
vec![(Point::new(4, 3)..Point::new(4, 3), " 4")],
|
||||
&mut cx,
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, cx| editor.update_visible_inline_completion(cx));
|
||||
assert_editor_active_move_completion(&mut cx, |snapshot, move_target| {
|
||||
assert_eq!(move_target.to_point(&snapshot), Point::new(4, 3));
|
||||
});
|
||||
|
||||
// When accepting, cursor is moved to the proposed location
|
||||
accept_completion(&mut cx);
|
||||
cx.assert_editor_state(indoc! {"
|
||||
line 0
|
||||
line 1
|
||||
line 2
|
||||
line 3
|
||||
linˇe
|
||||
"});
|
||||
|
||||
// Cursor is 2+ lines below the proposed edit
|
||||
cx.set_state(indoc! {"
|
||||
line 0
|
||||
line
|
||||
line 2
|
||||
line 3
|
||||
line ˇ4
|
||||
"});
|
||||
|
||||
propose_edits(
|
||||
&provider,
|
||||
vec![(Point::new(1, 3)..Point::new(1, 3), " 1")],
|
||||
&mut cx,
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, cx| editor.update_visible_inline_completion(cx));
|
||||
assert_editor_active_move_completion(&mut cx, |snapshot, move_target| {
|
||||
assert_eq!(move_target.to_point(&snapshot), Point::new(1, 3));
|
||||
});
|
||||
|
||||
// When accepting, cursor is moved to the proposed location
|
||||
accept_completion(&mut cx);
|
||||
cx.assert_editor_state(indoc! {"
|
||||
line 0
|
||||
linˇe
|
||||
line 2
|
||||
line 3
|
||||
line 4
|
||||
"});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_inline_completion_invalidation_range(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
let provider = cx.new_model(|_| FakeInlineCompletionProvider::default());
|
||||
assign_editor_completion_provider(provider.clone(), &mut cx);
|
||||
|
||||
// Cursor is 3+ lines above the proposed edit
|
||||
cx.set_state(indoc! {"
|
||||
line 0
|
||||
line ˇ1
|
||||
line 2
|
||||
line 3
|
||||
line 4
|
||||
line
|
||||
"});
|
||||
let edit_location = Point::new(5, 3);
|
||||
|
||||
propose_edits(
|
||||
&provider,
|
||||
vec![(edit_location..edit_location, " 5")],
|
||||
&mut cx,
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, cx| editor.update_visible_inline_completion(cx));
|
||||
assert_editor_active_move_completion(&mut cx, |snapshot, move_target| {
|
||||
assert_eq!(move_target.to_point(&snapshot), edit_location);
|
||||
});
|
||||
|
||||
// If we move *towards* the completion, it stays active
|
||||
cx.set_selections_state(indoc! {"
|
||||
line 0
|
||||
line 1
|
||||
line ˇ2
|
||||
line 3
|
||||
line 4
|
||||
line
|
||||
"});
|
||||
assert_editor_active_move_completion(&mut cx, |snapshot, move_target| {
|
||||
assert_eq!(move_target.to_point(&snapshot), edit_location);
|
||||
});
|
||||
|
||||
// If we move *away* from the completion, it is discarded
|
||||
cx.set_selections_state(indoc! {"
|
||||
line ˇ0
|
||||
line 1
|
||||
line 2
|
||||
line 3
|
||||
line 4
|
||||
line
|
||||
"});
|
||||
cx.editor(|editor, _| {
|
||||
assert!(editor.active_inline_completion.is_none());
|
||||
});
|
||||
|
||||
// Cursor is 3+ lines below the proposed edit
|
||||
cx.set_state(indoc! {"
|
||||
line
|
||||
line 1
|
||||
line 2
|
||||
line 3
|
||||
line ˇ4
|
||||
line 5
|
||||
"});
|
||||
let edit_location = Point::new(0, 3);
|
||||
|
||||
propose_edits(
|
||||
&provider,
|
||||
vec![(edit_location..edit_location, " 0")],
|
||||
&mut cx,
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, cx| editor.update_visible_inline_completion(cx));
|
||||
assert_editor_active_move_completion(&mut cx, |snapshot, move_target| {
|
||||
assert_eq!(move_target.to_point(&snapshot), edit_location);
|
||||
});
|
||||
|
||||
// If we move *towards* the completion, it stays active
|
||||
cx.set_selections_state(indoc! {"
|
||||
line
|
||||
line 1
|
||||
line 2
|
||||
line ˇ3
|
||||
line 4
|
||||
line 5
|
||||
"});
|
||||
assert_editor_active_move_completion(&mut cx, |snapshot, move_target| {
|
||||
assert_eq!(move_target.to_point(&snapshot), edit_location);
|
||||
});
|
||||
|
||||
// If we move *away* from the completion, it is discarded
|
||||
cx.set_selections_state(indoc! {"
|
||||
line
|
||||
line 1
|
||||
line 2
|
||||
line 3
|
||||
line 4
|
||||
line ˇ5
|
||||
"});
|
||||
cx.editor(|editor, _| {
|
||||
assert!(editor.active_inline_completion.is_none());
|
||||
});
|
||||
}
|
||||
|
||||
fn assert_editor_active_edit_completion(
|
||||
cx: &mut EditorTestContext,
|
||||
assert: impl FnOnce(MultiBufferSnapshot, &Vec<(Range<Anchor>, String)>),
|
||||
) {
|
||||
cx.editor(|editor, cx| {
|
||||
let completion_state = editor
|
||||
.active_inline_completion
|
||||
.as_ref()
|
||||
.expect("editor has no active completion");
|
||||
|
||||
if let InlineCompletion::Edit(edits) = &completion_state.completion {
|
||||
assert(editor.buffer().read(cx).snapshot(cx), edits);
|
||||
} else {
|
||||
panic!("expected edit completion");
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn assert_editor_active_move_completion(
|
||||
cx: &mut EditorTestContext,
|
||||
assert: impl FnOnce(MultiBufferSnapshot, Anchor),
|
||||
) {
|
||||
cx.editor(|editor, cx| {
|
||||
let completion_state = editor
|
||||
.active_inline_completion
|
||||
.as_ref()
|
||||
.expect("editor has no active completion");
|
||||
|
||||
if let InlineCompletion::Move(anchor) = &completion_state.completion {
|
||||
assert(editor.buffer().read(cx).snapshot(cx), *anchor);
|
||||
} else {
|
||||
panic!("expected move completion");
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn accept_completion(cx: &mut EditorTestContext) {
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor.accept_inline_completion(&crate::AcceptInlineCompletion, cx)
|
||||
})
|
||||
}
|
||||
|
||||
fn propose_edits<T: ToOffset>(
|
||||
provider: &Model<FakeInlineCompletionProvider>,
|
||||
edits: Vec<(Range<T>, &str)>,
|
||||
cx: &mut EditorTestContext,
|
||||
) {
|
||||
let snapshot = cx.buffer_snapshot();
|
||||
let edits = edits.into_iter().map(|(range, text)| {
|
||||
let range = snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end);
|
||||
(range, text.into())
|
||||
});
|
||||
|
||||
cx.update(|cx| {
|
||||
provider.update(cx, |provider, _| {
|
||||
provider.set_inline_completion(Some(inline_completion::InlineCompletion {
|
||||
edits: edits.collect(),
|
||||
}))
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
fn assign_editor_completion_provider(
|
||||
provider: Model<FakeInlineCompletionProvider>,
|
||||
cx: &mut EditorTestContext,
|
||||
) {
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor.set_inline_completion_provider(Some(provider), cx);
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Default, Clone)]
|
||||
struct FakeInlineCompletionProvider {
|
||||
completion: Option<inline_completion::InlineCompletion>,
|
||||
}
|
||||
|
||||
impl FakeInlineCompletionProvider {
|
||||
pub fn set_inline_completion(
|
||||
&mut self,
|
||||
completion: Option<inline_completion::InlineCompletion>,
|
||||
) {
|
||||
self.completion = completion;
|
||||
}
|
||||
}
|
||||
|
||||
impl InlineCompletionProvider for FakeInlineCompletionProvider {
|
||||
fn name() -> &'static str {
|
||||
"fake-completion-provider"
|
||||
}
|
||||
|
||||
fn is_enabled(
|
||||
&self,
|
||||
_buffer: &gpui::Model<language::Buffer>,
|
||||
_cursor_position: language::Anchor,
|
||||
_cx: &gpui::AppContext,
|
||||
) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn refresh(
|
||||
&mut self,
|
||||
_buffer: gpui::Model<language::Buffer>,
|
||||
_cursor_position: language::Anchor,
|
||||
_debounce: bool,
|
||||
_cx: &mut gpui::ModelContext<Self>,
|
||||
) {
|
||||
}
|
||||
|
||||
fn cycle(
|
||||
&mut self,
|
||||
_buffer: gpui::Model<language::Buffer>,
|
||||
_cursor_position: language::Anchor,
|
||||
_direction: inline_completion::Direction,
|
||||
_cx: &mut gpui::ModelContext<Self>,
|
||||
) {
|
||||
}
|
||||
|
||||
fn accept(&mut self, _cx: &mut gpui::ModelContext<Self>) {}
|
||||
|
||||
fn discard(&mut self, _cx: &mut gpui::ModelContext<Self>) {}
|
||||
|
||||
fn suggest<'a>(
|
||||
&mut self,
|
||||
_buffer: &gpui::Model<language::Buffer>,
|
||||
_cursor_position: language::Anchor,
|
||||
_cx: &mut gpui::ModelContext<Self>,
|
||||
) -> Option<inline_completion::InlineCompletion> {
|
||||
self.completion.clone()
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue