edit prediction: Add syntax highlighting for diff popover (#23899)
Co-Authored-by: Antonio <antonio@zed.dev> Release Notes: - N/A --------- Co-authored-by: Antonio <antonio@zed.dev>
This commit is contained in:
parent
41de83fe1f
commit
5e449c84fe
12 changed files with 802 additions and 479 deletions
|
@ -256,6 +256,7 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
|
|||
let position = cursor_position.bias_right(buffer);
|
||||
Some(InlineCompletion {
|
||||
edits: vec![(position..position, completion_text.into())],
|
||||
edit_preview: None,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -728,13 +728,13 @@ impl CompletionsMenu {
|
|||
}
|
||||
CompletionEntry::InlineCompletionHint(InlineCompletionMenuHint::Loaded { text }) => {
|
||||
match text {
|
||||
InlineCompletionText::Edit { text, highlights } => div()
|
||||
InlineCompletionText::Edit(highlighted_edits) => div()
|
||||
.mx_1()
|
||||
.rounded_md()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.child(
|
||||
gpui::StyledText::new(text.clone())
|
||||
.with_highlights(&style.text, highlights.clone()),
|
||||
gpui::StyledText::new(highlighted_edits.text.clone())
|
||||
.with_highlights(&style.text, highlighted_edits.highlights.clone()),
|
||||
),
|
||||
InlineCompletionText::Move(text) => div().child(text.clone()),
|
||||
}
|
||||
|
|
|
@ -96,8 +96,9 @@ use itertools::Itertools;
|
|||
use language::{
|
||||
language_settings::{self, all_language_settings, language_settings, InlayHintSettings},
|
||||
markdown, point_from_lsp, AutoindentMode, BracketPair, Buffer, Capability, CharKind, CodeLabel,
|
||||
CursorShape, Diagnostic, Documentation, IndentKind, IndentSize, Language, OffsetRangeExt,
|
||||
Point, Selection, SelectionGoal, TextObject, TransactionId, TreeSitterOptions,
|
||||
CursorShape, Diagnostic, Documentation, EditPreview, HighlightedEdits, IndentKind, IndentSize,
|
||||
Language, OffsetRangeExt, Point, Selection, SelectionGoal, TextObject, TransactionId,
|
||||
TreeSitterOptions,
|
||||
};
|
||||
use language::{point_to_lsp, BufferRow, CharClassifier, Runnable, RunnableRange};
|
||||
use linked_editing_ranges::refresh_linked_ranges;
|
||||
|
@ -116,6 +117,7 @@ use lsp::{
|
|||
LanguageServerId, LanguageServerName,
|
||||
};
|
||||
|
||||
use language::BufferSnapshot;
|
||||
use movement::TextLayoutDetails;
|
||||
pub use multi_buffer::{
|
||||
Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, RowInfo,
|
||||
|
@ -486,10 +488,7 @@ impl InlineCompletionMenuHint {
|
|||
#[derive(Clone, Debug)]
|
||||
enum InlineCompletionText {
|
||||
Move(SharedString),
|
||||
Edit {
|
||||
text: SharedString,
|
||||
highlights: Vec<(Range<usize>, HighlightStyle)>,
|
||||
},
|
||||
Edit(HighlightedEdits),
|
||||
}
|
||||
|
||||
pub(crate) enum EditDisplayMode {
|
||||
|
@ -501,7 +500,9 @@ pub(crate) enum EditDisplayMode {
|
|||
enum InlineCompletion {
|
||||
Edit {
|
||||
edits: Vec<(Range<Anchor>, String)>,
|
||||
edit_preview: Option<EditPreview>,
|
||||
display_mode: EditDisplayMode,
|
||||
snapshot: BufferSnapshot,
|
||||
},
|
||||
Move(Anchor),
|
||||
}
|
||||
|
@ -4847,10 +4848,7 @@ impl Editor {
|
|||
selections.select_anchor_ranges([position..position]);
|
||||
});
|
||||
}
|
||||
InlineCompletion::Edit {
|
||||
edits,
|
||||
display_mode: _,
|
||||
} => {
|
||||
InlineCompletion::Edit { edits, .. } => {
|
||||
if let Some(provider) = self.inline_completion_provider() {
|
||||
provider.accept(cx);
|
||||
}
|
||||
|
@ -4898,10 +4896,7 @@ impl Editor {
|
|||
selections.select_anchor_ranges([position..position]);
|
||||
});
|
||||
}
|
||||
InlineCompletion::Edit {
|
||||
edits,
|
||||
display_mode: _,
|
||||
} => {
|
||||
InlineCompletion::Edit { edits, .. } => {
|
||||
// Find an insertion that starts at the cursor position.
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
let cursor_offset = self.selections.newest::<usize>(cx).head();
|
||||
|
@ -5040,8 +5035,8 @@ impl Editor {
|
|||
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
|
||||
let inline_completion = provider.suggest(&buffer, cursor_buffer_position, cx)?;
|
||||
let edits = inline_completion
|
||||
.edits
|
||||
.into_iter()
|
||||
.flat_map(|(range, new_text)| {
|
||||
|
@ -5066,13 +5061,12 @@ impl Editor {
|
|||
|
||||
let mut inlay_ids = Vec::new();
|
||||
let invalidation_row_range;
|
||||
let completion;
|
||||
if cursor_row < edit_start_row {
|
||||
let completion = if cursor_row < edit_start_row {
|
||||
invalidation_row_range = cursor_row..edit_end_row;
|
||||
completion = InlineCompletion::Move(first_edit_start);
|
||||
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);
|
||||
InlineCompletion::Move(first_edit_start)
|
||||
} else {
|
||||
if edits
|
||||
.iter()
|
||||
|
@ -5117,10 +5111,14 @@ impl Editor {
|
|||
EditDisplayMode::DiffPopover
|
||||
};
|
||||
|
||||
completion = InlineCompletion::Edit {
|
||||
let snapshot = multibuffer.buffer_for_excerpt(excerpt_id).cloned()?;
|
||||
|
||||
InlineCompletion::Edit {
|
||||
edits,
|
||||
edit_preview: inline_completion.edit_preview,
|
||||
display_mode,
|
||||
};
|
||||
snapshot,
|
||||
}
|
||||
};
|
||||
|
||||
let invalidation_range = multibuffer
|
||||
|
@ -5164,19 +5162,26 @@ impl Editor {
|
|||
let text = match &self.active_inline_completion.as_ref()?.completion {
|
||||
InlineCompletion::Edit {
|
||||
edits,
|
||||
edit_preview,
|
||||
display_mode: _,
|
||||
} => inline_completion_edit_text(&editor_snapshot, edits, true, cx),
|
||||
snapshot,
|
||||
} => edit_preview
|
||||
.as_ref()
|
||||
.and_then(|edit_preview| {
|
||||
inline_completion_edit_text(&snapshot, &edits, edit_preview, true, cx)
|
||||
})
|
||||
.map(InlineCompletionText::Edit),
|
||||
InlineCompletion::Move(target) => {
|
||||
let target_point =
|
||||
target.to_point(&editor_snapshot.display_snapshot.buffer_snapshot);
|
||||
let target_line = target_point.row + 1;
|
||||
InlineCompletionText::Move(
|
||||
Some(InlineCompletionText::Move(
|
||||
format!("Jump to edit in line {}", target_line).into(),
|
||||
)
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
Some(InlineCompletionMenuHint::Loaded { text })
|
||||
Some(InlineCompletionMenuHint::Loaded { text: text? })
|
||||
} else if provider.is_refreshing(cx) {
|
||||
Some(InlineCompletionMenuHint::Loading)
|
||||
} else if provider.needs_terms_acceptance(cx) {
|
||||
|
@ -15829,74 +15834,23 @@ pub fn diagnostic_block_renderer(
|
|||
}
|
||||
|
||||
fn inline_completion_edit_text(
|
||||
editor_snapshot: &EditorSnapshot,
|
||||
edits: &Vec<(Range<Anchor>, String)>,
|
||||
current_snapshot: &BufferSnapshot,
|
||||
edits: &[(Range<Anchor>, String)],
|
||||
edit_preview: &EditPreview,
|
||||
include_deletions: bool,
|
||||
cx: &App,
|
||||
) -> InlineCompletionText {
|
||||
let edit_start = edits
|
||||
.first()
|
||||
.unwrap()
|
||||
.0
|
||||
.start
|
||||
.to_display_point(editor_snapshot);
|
||||
) -> Option<HighlightedEdits> {
|
||||
let edits = edits
|
||||
.iter()
|
||||
.map(|(anchor, text)| {
|
||||
(
|
||||
anchor.start.text_anchor..anchor.end.text_anchor,
|
||||
text.clone(),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
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();
|
||||
let color = if include_deletions && new_text.is_empty() {
|
||||
text.extend(
|
||||
editor_snapshot
|
||||
.buffer_snapshot
|
||||
.chunks(old_offset_range.start..offset, false)
|
||||
.map(|chunk| chunk.text),
|
||||
);
|
||||
cx.theme().status().deleted_background
|
||||
} else {
|
||||
text.push_str(new_text);
|
||||
cx.theme().status().created_background
|
||||
};
|
||||
let end = text.len();
|
||||
|
||||
highlights.push((
|
||||
start..end,
|
||||
HighlightStyle {
|
||||
background_color: Some(color),
|
||||
..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,
|
||||
}
|
||||
Some(edit_preview.highlight_edits(current_snapshot, &edits, include_deletions, cx))
|
||||
}
|
||||
|
||||
pub fn highlight_diagnostic_message(
|
||||
|
|
|
@ -15257,241 +15257,205 @@ async fn test_multi_buffer_with_single_excerpt_folding(cx: &mut gpui::TestAppCon
|
|||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_inline_completion_text(cx: &mut TestAppContext) {
|
||||
async fn test_inline_completion_text(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
// Simple insertion
|
||||
{
|
||||
let window = cx.add_window(|window, cx| {
|
||||
let buffer = MultiBuffer::build_simple("Hello, world!", cx);
|
||||
Editor::new(EditorMode::Full, buffer, None, true, window, cx)
|
||||
});
|
||||
let cx = &mut VisualTestContext::from_window(*window, cx);
|
||||
|
||||
window
|
||||
.update(cx, |editor, window, cx| {
|
||||
let snapshot = editor.snapshot(window, 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, false, 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();
|
||||
}
|
||||
assert_highlighted_edits(
|
||||
"Hello, world!",
|
||||
vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
|
||||
true,
|
||||
cx,
|
||||
|highlighted_edits, cx| {
|
||||
assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
|
||||
assert_eq!(highlighted_edits.highlights.len(), 1);
|
||||
assert_eq!(highlighted_edits.highlights[0].0, 6..16);
|
||||
assert_eq!(
|
||||
highlighted_edits.highlights[0].1.background_color,
|
||||
Some(cx.theme().status().created_background)
|
||||
);
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
// Replacement
|
||||
{
|
||||
let window = cx.add_window(|window, cx| {
|
||||
let buffer = MultiBuffer::build_simple("This is a test.", cx);
|
||||
Editor::new(EditorMode::Full, buffer, None, true, window, cx)
|
||||
});
|
||||
let cx = &mut VisualTestContext::from_window(*window, cx);
|
||||
|
||||
window
|
||||
.update(cx, |editor, window, cx| {
|
||||
let snapshot = editor.snapshot(window, 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, false, 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();
|
||||
}
|
||||
assert_highlighted_edits(
|
||||
"This is a test.",
|
||||
vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
|
||||
false,
|
||||
cx,
|
||||
|highlighted_edits, cx| {
|
||||
assert_eq!(highlighted_edits.text, "That is a test.");
|
||||
assert_eq!(highlighted_edits.highlights.len(), 1);
|
||||
assert_eq!(highlighted_edits.highlights[0].0, 0..4);
|
||||
assert_eq!(
|
||||
highlighted_edits.highlights[0].1.background_color,
|
||||
Some(cx.theme().status().created_background)
|
||||
);
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
// Multiple edits
|
||||
{
|
||||
let window = cx.add_window(|window, cx| {
|
||||
let buffer = MultiBuffer::build_simple("Hello, world!", cx);
|
||||
Editor::new(EditorMode::Full, buffer, None, true, window, cx)
|
||||
});
|
||||
let cx = &mut VisualTestContext::from_window(*window, cx);
|
||||
|
||||
window
|
||||
.update(cx, |editor, window, cx| {
|
||||
let snapshot = editor.snapshot(window, 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, false, 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();
|
||||
}
|
||||
assert_highlighted_edits(
|
||||
"Hello, world!",
|
||||
vec![
|
||||
(Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
|
||||
(Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
|
||||
],
|
||||
false,
|
||||
cx,
|
||||
|highlighted_edits, cx| {
|
||||
assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
|
||||
assert_eq!(highlighted_edits.highlights.len(), 2);
|
||||
assert_eq!(highlighted_edits.highlights[0].0, 0..9);
|
||||
assert_eq!(highlighted_edits.highlights[1].0, 16..29);
|
||||
assert_eq!(
|
||||
highlighted_edits.highlights[0].1.background_color,
|
||||
Some(cx.theme().status().created_background)
|
||||
);
|
||||
assert_eq!(
|
||||
highlighted_edits.highlights[1].1.background_color,
|
||||
Some(cx.theme().status().created_background)
|
||||
);
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
// Multiple lines with edits
|
||||
{
|
||||
let window = cx.add_window(|window, cx| {
|
||||
let buffer =
|
||||
MultiBuffer::build_simple("First line\nSecond line\nThird line\nFourth line", cx);
|
||||
Editor::new(EditorMode::Full, buffer, None, true, window, cx)
|
||||
});
|
||||
let cx = &mut VisualTestContext::from_window(*window, cx);
|
||||
|
||||
window
|
||||
.update(cx, |editor, window, cx| {
|
||||
let snapshot = editor.snapshot(window, 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, false, 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();
|
||||
}
|
||||
assert_highlighted_edits(
|
||||
"First line\nSecond line\nThird line\nFourth line",
|
||||
vec![
|
||||
(Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
|
||||
(
|
||||
Point::new(2, 0)..Point::new(2, 10),
|
||||
"New third line".to_string(),
|
||||
),
|
||||
(Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
|
||||
],
|
||||
false,
|
||||
cx,
|
||||
|highlighted_edits, cx| {
|
||||
assert_eq!(
|
||||
highlighted_edits.text,
|
||||
"Second modified\nNew third line\nFourth updated line"
|
||||
);
|
||||
assert_eq!(highlighted_edits.highlights.len(), 3);
|
||||
assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
|
||||
assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
|
||||
assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
|
||||
for highlight in &highlighted_edits.highlights {
|
||||
assert_eq!(
|
||||
highlight.1.background_color,
|
||||
Some(cx.theme().status().created_background)
|
||||
);
|
||||
}
|
||||
},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
|
||||
async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
// Deletion
|
||||
{
|
||||
let window = cx.add_window(|window, cx| {
|
||||
let buffer = MultiBuffer::build_simple("Hello, world!", cx);
|
||||
Editor::new(EditorMode::Full, buffer, None, true, window, cx)
|
||||
});
|
||||
let cx = &mut VisualTestContext::from_window(*window, cx);
|
||||
|
||||
window
|
||||
.update(cx, |editor, window, cx| {
|
||||
let snapshot = editor.snapshot(window, cx);
|
||||
let edit_range = snapshot.buffer_snapshot.anchor_after(Point::new(0, 5))
|
||||
..snapshot.buffer_snapshot.anchor_before(Point::new(0, 11));
|
||||
let edits = vec![(edit_range, "".to_string())];
|
||||
|
||||
let InlineCompletionText::Edit { text, highlights } =
|
||||
inline_completion_edit_text(&snapshot, &edits, true, cx)
|
||||
else {
|
||||
panic!("Failed to generate inline completion text");
|
||||
};
|
||||
|
||||
assert_eq!(text, "Hello, world!");
|
||||
assert_eq!(highlights.len(), 1);
|
||||
assert_eq!(highlights[0].0, 5..11);
|
||||
assert_eq!(
|
||||
highlights[0].1.background_color,
|
||||
Some(cx.theme().status().deleted_background)
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
assert_highlighted_edits(
|
||||
"Hello, world!",
|
||||
vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
|
||||
true,
|
||||
cx,
|
||||
|highlighted_edits, cx| {
|
||||
assert_eq!(highlighted_edits.text, "Hello, world!");
|
||||
assert_eq!(highlighted_edits.highlights.len(), 1);
|
||||
assert_eq!(highlighted_edits.highlights[0].0, 5..11);
|
||||
assert_eq!(
|
||||
highlighted_edits.highlights[0].1.background_color,
|
||||
Some(cx.theme().status().deleted_background)
|
||||
);
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
// Insertion
|
||||
{
|
||||
let window = cx.add_window(|window, cx| {
|
||||
let buffer = MultiBuffer::build_simple("Hello, world!", cx);
|
||||
Editor::new(EditorMode::Full, buffer, None, true, window, cx)
|
||||
});
|
||||
let cx = &mut VisualTestContext::from_window(*window, cx);
|
||||
assert_highlighted_edits(
|
||||
"Hello, world!",
|
||||
vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
|
||||
true,
|
||||
cx,
|
||||
|highlighted_edits, cx| {
|
||||
assert_eq!(highlighted_edits.highlights.len(), 1);
|
||||
assert_eq!(highlighted_edits.highlights[0].0, 6..14);
|
||||
assert_eq!(
|
||||
highlighted_edits.highlights[0].1.background_color,
|
||||
Some(cx.theme().status().created_background)
|
||||
);
|
||||
},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
window
|
||||
.update(cx, |editor, window, cx| {
|
||||
let snapshot = editor.snapshot(window, 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, " digital".to_string())];
|
||||
async fn assert_highlighted_edits(
|
||||
text: &str,
|
||||
edits: Vec<(Range<Point>, String)>,
|
||||
include_deletions: bool,
|
||||
cx: &mut TestAppContext,
|
||||
assertion_fn: impl Fn(HighlightedEdits, &App),
|
||||
) {
|
||||
let window = cx.add_window(|window, cx| {
|
||||
let buffer = MultiBuffer::build_simple(text, cx);
|
||||
Editor::new(EditorMode::Full, buffer, None, true, window, cx)
|
||||
});
|
||||
let cx = &mut VisualTestContext::from_window(*window, cx);
|
||||
|
||||
let InlineCompletionText::Edit { text, highlights } =
|
||||
inline_completion_edit_text(&snapshot, &edits, true, cx)
|
||||
else {
|
||||
panic!("Failed to generate inline completion text");
|
||||
};
|
||||
let (buffer, snapshot) = window
|
||||
.update(cx, |editor, _window, cx| {
|
||||
(
|
||||
editor.buffer().clone(),
|
||||
editor.buffer().read(cx).snapshot(cx),
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(text, "Hello, digital world!");
|
||||
assert_eq!(highlights.len(), 1);
|
||||
assert_eq!(highlights[0].0, 6..14);
|
||||
assert_eq!(
|
||||
highlights[0].1.background_color,
|
||||
Some(cx.theme().status().created_background)
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
let edits = edits
|
||||
.into_iter()
|
||||
.map(|(range, edit)| {
|
||||
(
|
||||
snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
|
||||
edit,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let text_anchor_edits = edits
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let edit_preview = window
|
||||
.update(cx, |_, _window, cx| {
|
||||
buffer
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.preview_edits(text_anchor_edits.into(), cx)
|
||||
})
|
||||
.unwrap()
|
||||
.await;
|
||||
|
||||
cx.update(|_window, cx| {
|
||||
let highlighted_edits = inline_completion_edit_text(
|
||||
&snapshot.as_singleton().unwrap().2,
|
||||
&edits,
|
||||
&edit_preview,
|
||||
include_deletions,
|
||||
cx,
|
||||
)
|
||||
.expect("Missing highlighted edits");
|
||||
assertion_fn(highlighted_edits, cx)
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
|
|
|
@ -3468,7 +3468,9 @@ impl EditorElement {
|
|||
}
|
||||
InlineCompletion::Edit {
|
||||
edits,
|
||||
edit_preview,
|
||||
display_mode,
|
||||
snapshot,
|
||||
} => {
|
||||
if self.editor.read(cx).has_active_completions_menu() {
|
||||
return None;
|
||||
|
@ -3521,13 +3523,11 @@ impl EditorElement {
|
|||
EditDisplayMode::DiffPopover => {}
|
||||
}
|
||||
|
||||
let crate::InlineCompletionText::Edit { text, highlights } =
|
||||
crate::inline_completion_edit_text(editor_snapshot, edits, false, cx)
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
let highlighted_edits = edit_preview.as_ref().and_then(|edit_preview| {
|
||||
crate::inline_completion_edit_text(&snapshot, edits, edit_preview, false, cx)
|
||||
})?;
|
||||
|
||||
let line_count = text.lines().count() + 1;
|
||||
let line_count = highlighted_edits.text.lines().count() + 1;
|
||||
|
||||
let longest_row =
|
||||
editor_snapshot.longest_row_in_range(edit_start.row()..edit_end.row() + 1);
|
||||
|
@ -3546,15 +3546,14 @@ impl EditorElement {
|
|||
.width
|
||||
};
|
||||
|
||||
let styled_text =
|
||||
gpui::StyledText::new(text.clone()).with_highlights(&style.text, highlights);
|
||||
let styled_text = gpui::StyledText::new(highlighted_edits.text.clone())
|
||||
.with_highlights(&style.text, highlighted_edits.highlights);
|
||||
|
||||
let mut element = div()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.rounded_md()
|
||||
.px_1()
|
||||
.child(styled_text)
|
||||
.into_any();
|
||||
|
||||
|
|
|
@ -333,6 +333,7 @@ fn propose_edits<T: ToOffset>(
|
|||
provider.update(cx, |provider, _| {
|
||||
provider.set_inline_completion(Some(inline_completion::InlineCompletion {
|
||||
edits: edits.collect(),
|
||||
edit_preview: None,
|
||||
}))
|
||||
})
|
||||
});
|
||||
|
|
|
@ -15,6 +15,7 @@ pub enum Direction {
|
|||
#[derive(Clone)]
|
||||
pub struct InlineCompletion {
|
||||
pub edits: Vec<(Range<language::Anchor>, String)>,
|
||||
pub edit_preview: Option<language::EditPreview>,
|
||||
}
|
||||
|
||||
pub trait InlineCompletionProvider: 'static + Sized {
|
||||
|
|
|
@ -25,8 +25,8 @@ use collections::HashMap;
|
|||
use fs::MTime;
|
||||
use futures::channel::oneshot;
|
||||
use gpui::{
|
||||
AnyElement, App, AppContext as _, Context, Entity, EventEmitter, HighlightStyle, Pixels, Task,
|
||||
TaskLabel, Window,
|
||||
AnyElement, App, AppContext as _, Context, Entity, EventEmitter, HighlightStyle, Pixels,
|
||||
SharedString, Task, TaskLabel, Window,
|
||||
};
|
||||
use lsp::LanguageServerId;
|
||||
use parking_lot::Mutex;
|
||||
|
@ -65,7 +65,7 @@ pub use text::{
|
|||
Subscription, TextDimension, TextSummary, ToOffset, ToOffsetUtf16, ToPoint, ToPointUtf16,
|
||||
Transaction, TransactionId, Unclipped,
|
||||
};
|
||||
use theme::SyntaxTheme;
|
||||
use theme::{ActiveTheme as _, SyntaxTheme};
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
use util::RandomCharIter;
|
||||
use util::{debug_panic, maybe, RangeExt};
|
||||
|
@ -588,6 +588,183 @@ pub struct Runnable {
|
|||
pub buffer: BufferId,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct EditPreview {
|
||||
old_snapshot: text::BufferSnapshot,
|
||||
applied_edits_snapshot: text::BufferSnapshot,
|
||||
syntax_snapshot: SyntaxSnapshot,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub struct HighlightedEdits {
|
||||
pub text: SharedString,
|
||||
pub highlights: Vec<(Range<usize>, HighlightStyle)>,
|
||||
}
|
||||
|
||||
impl EditPreview {
|
||||
pub fn highlight_edits(
|
||||
&self,
|
||||
current_snapshot: &BufferSnapshot,
|
||||
edits: &[(Range<Anchor>, String)],
|
||||
include_deletions: bool,
|
||||
cx: &App,
|
||||
) -> HighlightedEdits {
|
||||
let Some(visible_range_in_preview_snapshot) = self.compute_visible_range(edits) else {
|
||||
return HighlightedEdits::default();
|
||||
};
|
||||
|
||||
let mut text = String::new();
|
||||
let mut highlights = Vec::new();
|
||||
|
||||
let mut offset_in_preview_snapshot = visible_range_in_preview_snapshot.start;
|
||||
|
||||
let insertion_highlight_style = HighlightStyle {
|
||||
background_color: Some(cx.theme().status().created_background),
|
||||
..Default::default()
|
||||
};
|
||||
let deletion_highlight_style = HighlightStyle {
|
||||
background_color: Some(cx.theme().status().deleted_background),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
for (range, edit_text) in edits {
|
||||
let edit_new_end_in_preview_snapshot = range
|
||||
.end
|
||||
.bias_right(&self.old_snapshot)
|
||||
.to_offset(&self.applied_edits_snapshot);
|
||||
let edit_start_in_preview_snapshot = edit_new_end_in_preview_snapshot - edit_text.len();
|
||||
|
||||
let unchanged_range_in_preview_snapshot =
|
||||
offset_in_preview_snapshot..edit_start_in_preview_snapshot;
|
||||
if !unchanged_range_in_preview_snapshot.is_empty() {
|
||||
Self::highlight_text(
|
||||
unchanged_range_in_preview_snapshot.clone(),
|
||||
&mut text,
|
||||
&mut highlights,
|
||||
None,
|
||||
&self.applied_edits_snapshot,
|
||||
&self.syntax_snapshot,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
let range_in_current_snapshot = range.to_offset(current_snapshot);
|
||||
if include_deletions && !range_in_current_snapshot.is_empty() {
|
||||
Self::highlight_text(
|
||||
range_in_current_snapshot.clone(),
|
||||
&mut text,
|
||||
&mut highlights,
|
||||
Some(deletion_highlight_style),
|
||||
¤t_snapshot.text,
|
||||
¤t_snapshot.syntax,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
if !edit_text.is_empty() {
|
||||
Self::highlight_text(
|
||||
edit_start_in_preview_snapshot..edit_new_end_in_preview_snapshot,
|
||||
&mut text,
|
||||
&mut highlights,
|
||||
Some(insertion_highlight_style),
|
||||
&self.applied_edits_snapshot,
|
||||
&self.syntax_snapshot,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
offset_in_preview_snapshot = edit_new_end_in_preview_snapshot;
|
||||
}
|
||||
|
||||
Self::highlight_text(
|
||||
offset_in_preview_snapshot..visible_range_in_preview_snapshot.end,
|
||||
&mut text,
|
||||
&mut highlights,
|
||||
None,
|
||||
&self.applied_edits_snapshot,
|
||||
&self.syntax_snapshot,
|
||||
cx,
|
||||
);
|
||||
|
||||
HighlightedEdits {
|
||||
text: text.into(),
|
||||
highlights,
|
||||
}
|
||||
}
|
||||
|
||||
fn highlight_text(
|
||||
range: Range<usize>,
|
||||
text: &mut String,
|
||||
highlights: &mut Vec<(Range<usize>, HighlightStyle)>,
|
||||
override_style: Option<HighlightStyle>,
|
||||
snapshot: &text::BufferSnapshot,
|
||||
syntax_snapshot: &SyntaxSnapshot,
|
||||
cx: &App,
|
||||
) {
|
||||
for chunk in Self::highlighted_chunks(range, snapshot, syntax_snapshot) {
|
||||
let start = text.len();
|
||||
text.push_str(chunk.text);
|
||||
let end = text.len();
|
||||
|
||||
if let Some(mut highlight_style) = chunk
|
||||
.syntax_highlight_id
|
||||
.and_then(|id| id.style(cx.theme().syntax()))
|
||||
{
|
||||
if let Some(override_style) = override_style {
|
||||
highlight_style.highlight(override_style);
|
||||
}
|
||||
highlights.push((start..end, highlight_style));
|
||||
} else if let Some(override_style) = override_style {
|
||||
highlights.push((start..end, override_style));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn highlighted_chunks<'a>(
|
||||
range: Range<usize>,
|
||||
snapshot: &'a text::BufferSnapshot,
|
||||
syntax_snapshot: &'a SyntaxSnapshot,
|
||||
) -> BufferChunks<'a> {
|
||||
let captures = syntax_snapshot.captures(range.clone(), snapshot, |grammar| {
|
||||
grammar.highlights_query.as_ref()
|
||||
});
|
||||
|
||||
let highlight_maps = captures
|
||||
.grammars()
|
||||
.iter()
|
||||
.map(|grammar| grammar.highlight_map())
|
||||
.collect();
|
||||
|
||||
BufferChunks::new(
|
||||
snapshot.as_rope(),
|
||||
range,
|
||||
Some((captures, highlight_maps)),
|
||||
false,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
fn compute_visible_range(&self, edits: &[(Range<Anchor>, String)]) -> Option<Range<usize>> {
|
||||
let (first, _) = edits.first()?;
|
||||
let (last, _) = edits.last()?;
|
||||
|
||||
let start = first
|
||||
.start
|
||||
.bias_left(&self.old_snapshot)
|
||||
.to_point(&self.applied_edits_snapshot);
|
||||
let end = last
|
||||
.end
|
||||
.bias_right(&self.old_snapshot)
|
||||
.to_point(&self.applied_edits_snapshot);
|
||||
|
||||
// Ensure that the first line of the first edit and the last line of the last edit are always fully visible
|
||||
let range = Point::new(start.row, 0)
|
||||
..Point::new(end.row, self.applied_edits_snapshot.line_len(end.row));
|
||||
|
||||
Some(range.to_offset(&self.applied_edits_snapshot))
|
||||
}
|
||||
}
|
||||
|
||||
impl Buffer {
|
||||
/// Create a new buffer with the given base text.
|
||||
pub fn local<T: Into<String>>(base_text: T, cx: &Context<Self>) -> Self {
|
||||
|
@ -840,6 +1017,34 @@ impl Buffer {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn preview_edits(
|
||||
&self,
|
||||
edits: Arc<[(Range<Anchor>, String)]>,
|
||||
cx: &App,
|
||||
) -> Task<EditPreview> {
|
||||
let registry = self.language_registry();
|
||||
let language = self.language().cloned();
|
||||
let old_snapshot = self.text.snapshot();
|
||||
let mut branch_buffer = self.text.branch();
|
||||
let mut syntax_snapshot = self.syntax_map.lock().snapshot();
|
||||
cx.background_executor().spawn(async move {
|
||||
if !edits.is_empty() {
|
||||
branch_buffer.edit(edits.iter().cloned());
|
||||
let snapshot = branch_buffer.snapshot();
|
||||
syntax_snapshot.interpolate(&snapshot);
|
||||
|
||||
if let Some(language) = language {
|
||||
syntax_snapshot.reparse(&snapshot, registry, language);
|
||||
}
|
||||
}
|
||||
EditPreview {
|
||||
old_snapshot,
|
||||
applied_edits_snapshot: branch_buffer.snapshot(),
|
||||
syntax_snapshot,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Applies all of the changes in this buffer that intersect any of the
|
||||
/// given `ranges` to its base buffer.
|
||||
///
|
||||
|
|
|
@ -6,8 +6,8 @@ use crate::Buffer;
|
|||
use clock::ReplicaId;
|
||||
use collections::BTreeMap;
|
||||
use futures::FutureExt as _;
|
||||
use gpui::TestAppContext;
|
||||
use gpui::{App, AppContext as _, BorrowAppContext, Entity};
|
||||
use gpui::{HighlightStyle, TestAppContext};
|
||||
use indoc::indoc;
|
||||
use proto::deserialize_operation;
|
||||
use rand::prelude::*;
|
||||
|
@ -23,6 +23,7 @@ use syntax_map::TreeSitterOptions;
|
|||
use text::network::Network;
|
||||
use text::{BufferId, LineEnding};
|
||||
use text::{Point, ToPoint};
|
||||
use theme::ActiveTheme;
|
||||
use unindent::Unindent as _;
|
||||
use util::{assert_set_eq, post_inc, test::marked_text_ranges, RandomCharIter};
|
||||
|
||||
|
@ -2627,6 +2628,143 @@ fn test_undo_after_merge_into_base(cx: &mut TestAppContext) {
|
|||
branch.read_with(cx, |branch, _| assert_eq!(branch.text(), "ABCdefgHIjk"));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_preview_edits(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
init_settings(cx, |_| {});
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
});
|
||||
|
||||
let insertion_style = HighlightStyle {
|
||||
background_color: Some(cx.read(|cx| cx.theme().status().created_background)),
|
||||
..Default::default()
|
||||
};
|
||||
let deletion_style = HighlightStyle {
|
||||
background_color: Some(cx.read(|cx| cx.theme().status().deleted_background)),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// no edits
|
||||
assert_preview_edits(
|
||||
indoc! {"
|
||||
fn test_empty() -> bool {
|
||||
false
|
||||
}"
|
||||
},
|
||||
vec![],
|
||||
true,
|
||||
cx,
|
||||
|hl| {
|
||||
assert!(hl.text.is_empty());
|
||||
assert!(hl.highlights.is_empty());
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
// only insertions
|
||||
assert_preview_edits(
|
||||
indoc! {"
|
||||
fn calculate_area(: f64) -> f64 {
|
||||
std::f64::consts::PI * .powi(2)
|
||||
}"
|
||||
},
|
||||
vec![
|
||||
(Point::new(0, 18)..Point::new(0, 18), "radius"),
|
||||
(Point::new(1, 27)..Point::new(1, 27), "radius"),
|
||||
],
|
||||
true,
|
||||
cx,
|
||||
|hl| {
|
||||
assert_eq!(
|
||||
hl.text,
|
||||
indoc! {"
|
||||
fn calculate_area(radius: f64) -> f64 {
|
||||
std::f64::consts::PI * radius.powi(2)"
|
||||
}
|
||||
);
|
||||
|
||||
assert_eq!(hl.highlights.len(), 2);
|
||||
assert_eq!(hl.highlights[0], ((18..24), insertion_style));
|
||||
assert_eq!(hl.highlights[1], ((67..73), insertion_style));
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
// insertions & deletions
|
||||
assert_preview_edits(
|
||||
indoc! {"
|
||||
struct Person {
|
||||
first_name: String,
|
||||
}
|
||||
|
||||
impl Person {
|
||||
fn first_name(&self) -> &String {
|
||||
&self.first_name
|
||||
}
|
||||
}"
|
||||
},
|
||||
vec![
|
||||
(Point::new(1, 4)..Point::new(1, 9), "last"),
|
||||
(Point::new(5, 7)..Point::new(5, 12), "last"),
|
||||
(Point::new(6, 14)..Point::new(6, 19), "last"),
|
||||
],
|
||||
true,
|
||||
cx,
|
||||
|hl| {
|
||||
assert_eq!(
|
||||
hl.text,
|
||||
indoc! {"
|
||||
firstlast_name: String,
|
||||
}
|
||||
|
||||
impl Person {
|
||||
fn firstlast_name(&self) -> &String {
|
||||
&self.firstlast_name"
|
||||
}
|
||||
);
|
||||
|
||||
assert_eq!(hl.highlights.len(), 6);
|
||||
assert_eq!(hl.highlights[0], ((4..9), deletion_style));
|
||||
assert_eq!(hl.highlights[1], ((9..13), insertion_style));
|
||||
assert_eq!(hl.highlights[2], ((52..57), deletion_style));
|
||||
assert_eq!(hl.highlights[3], ((57..61), insertion_style));
|
||||
assert_eq!(hl.highlights[4], ((101..106), deletion_style));
|
||||
assert_eq!(hl.highlights[5], ((106..110), insertion_style));
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
async fn assert_preview_edits(
|
||||
text: &str,
|
||||
edits: Vec<(Range<Point>, &str)>,
|
||||
include_deletions: bool,
|
||||
cx: &mut TestAppContext,
|
||||
assert_fn: impl Fn(HighlightedEdits),
|
||||
) {
|
||||
let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx));
|
||||
let edits = buffer.read_with(cx, |buffer, _| {
|
||||
edits
|
||||
.into_iter()
|
||||
.map(|(range, text)| {
|
||||
(
|
||||
buffer.anchor_before(range.start)..buffer.anchor_after(range.end),
|
||||
text.to_string(),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
let edit_preview = buffer
|
||||
.read_with(cx, |buffer, cx| {
|
||||
buffer.preview_edits(edits.clone().into(), cx)
|
||||
})
|
||||
.await;
|
||||
let highlighted_edits = cx.read(|cx| {
|
||||
edit_preview.highlight_edits(&buffer.read(cx).snapshot(), &edits, include_deletions, cx)
|
||||
});
|
||||
assert_fn(highlighted_edits);
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
fn test_random_collaboration(cx: &mut App, mut rng: StdRng) {
|
||||
let min_peers = env::var("MIN_PEERS")
|
||||
|
|
|
@ -263,7 +263,7 @@ impl SyntaxSnapshot {
|
|||
self.layers.is_empty()
|
||||
}
|
||||
|
||||
fn interpolate(&mut self, text: &BufferSnapshot) {
|
||||
pub fn interpolate(&mut self, text: &BufferSnapshot) {
|
||||
let edits = text
|
||||
.anchored_edits_since::<(usize, Point)>(&self.interpolated_version)
|
||||
.collect::<Vec<_>>();
|
||||
|
|
|
@ -90,7 +90,10 @@ fn completion_from_diff(
|
|||
edits.push((edit_range, edit_text));
|
||||
}
|
||||
|
||||
InlineCompletion { edits }
|
||||
InlineCompletion {
|
||||
edits,
|
||||
edit_preview: None,
|
||||
}
|
||||
}
|
||||
|
||||
impl InlineCompletionProvider for SupermavenCompletionProvider {
|
||||
|
|
|
@ -15,8 +15,8 @@ use gpui::{
|
|||
};
|
||||
use http_client::{HttpClient, Method};
|
||||
use language::{
|
||||
language_settings::all_language_settings, Anchor, Buffer, BufferSnapshot, OffsetRangeExt,
|
||||
Point, ToOffset, ToPoint,
|
||||
language_settings::all_language_settings, Anchor, Buffer, BufferSnapshot, EditPreview,
|
||||
OffsetRangeExt, Point, ToOffset, ToPoint,
|
||||
};
|
||||
use language_models::LlmApiToken;
|
||||
use rpc::{PredictEditsParams, PredictEditsResponse, EXPIRED_LLM_TOKEN_HEADER_NAME};
|
||||
|
@ -101,6 +101,7 @@ pub struct InlineCompletion {
|
|||
cursor_offset: usize,
|
||||
edits: Arc<[(Range<Anchor>, String)]>,
|
||||
snapshot: BufferSnapshot,
|
||||
edit_preview: EditPreview,
|
||||
input_outline: Arc<str>,
|
||||
input_events: Arc<str>,
|
||||
input_excerpt: Arc<str>,
|
||||
|
@ -116,55 +117,57 @@ impl InlineCompletion {
|
|||
}
|
||||
|
||||
fn interpolate(&self, new_snapshot: &BufferSnapshot) -> Option<Vec<(Range<Anchor>, String)>> {
|
||||
let mut edits = Vec::new();
|
||||
interpolate(&self.snapshot, new_snapshot, self.edits.clone())
|
||||
}
|
||||
}
|
||||
|
||||
let mut user_edits = new_snapshot
|
||||
.edits_since::<usize>(&self.snapshot.version)
|
||||
.peekable();
|
||||
for (model_old_range, model_new_text) in self.edits.iter() {
|
||||
let model_offset_range = model_old_range.to_offset(&self.snapshot);
|
||||
while let Some(next_user_edit) = user_edits.peek() {
|
||||
if next_user_edit.old.end < model_offset_range.start {
|
||||
user_edits.next();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
fn interpolate(
|
||||
old_snapshot: &BufferSnapshot,
|
||||
new_snapshot: &BufferSnapshot,
|
||||
current_edits: Arc<[(Range<Anchor>, String)]>,
|
||||
) -> Option<Vec<(Range<Anchor>, String)>> {
|
||||
let mut edits = Vec::new();
|
||||
|
||||
if let Some(user_edit) = user_edits.peek() {
|
||||
if user_edit.old.start > model_offset_range.end {
|
||||
edits.push((model_old_range.clone(), model_new_text.clone()));
|
||||
} else if user_edit.old == model_offset_range {
|
||||
let user_new_text = new_snapshot
|
||||
.text_for_range(user_edit.new.clone())
|
||||
.collect::<String>();
|
||||
|
||||
if let Some(model_suffix) = model_new_text.strip_prefix(&user_new_text) {
|
||||
if !model_suffix.is_empty() {
|
||||
edits.push((
|
||||
new_snapshot.anchor_after(user_edit.new.end)
|
||||
..new_snapshot.anchor_before(user_edit.new.end),
|
||||
model_suffix.into(),
|
||||
));
|
||||
}
|
||||
|
||||
user_edits.next();
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
} else {
|
||||
let mut model_edits = current_edits.into_iter().peekable();
|
||||
for user_edit in new_snapshot.edits_since::<usize>(&old_snapshot.version) {
|
||||
while let Some((model_old_range, _)) = model_edits.peek() {
|
||||
let model_old_range = model_old_range.to_offset(old_snapshot);
|
||||
if model_old_range.end < user_edit.old.start {
|
||||
let (model_old_range, model_new_text) = model_edits.next().unwrap();
|
||||
edits.push((model_old_range.clone(), model_new_text.clone()));
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if edits.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(edits)
|
||||
if let Some((model_old_range, model_new_text)) = model_edits.peek() {
|
||||
let model_old_offset_range = model_old_range.to_offset(old_snapshot);
|
||||
if user_edit.old == model_old_offset_range {
|
||||
let user_new_text = new_snapshot
|
||||
.text_for_range(user_edit.new.clone())
|
||||
.collect::<String>();
|
||||
|
||||
if let Some(model_suffix) = model_new_text.strip_prefix(&user_new_text) {
|
||||
if !model_suffix.is_empty() {
|
||||
let anchor = old_snapshot.anchor_after(user_edit.old.end);
|
||||
edits.push((anchor..anchor, model_suffix.to_string()));
|
||||
}
|
||||
|
||||
model_edits.next();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return None;
|
||||
}
|
||||
|
||||
edits.extend(model_edits.cloned());
|
||||
|
||||
if edits.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(edits)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -324,7 +327,8 @@ impl Zeta {
|
|||
F: FnOnce(Arc<Client>, LlmApiToken, bool, PredictEditsParams) -> R + 'static,
|
||||
R: Future<Output = Result<PredictEditsResponse>> + Send + 'static,
|
||||
{
|
||||
let snapshot = self.report_changes_for_buffer(buffer, cx);
|
||||
let buffer = buffer.clone();
|
||||
let snapshot = self.report_changes_for_buffer(&buffer, cx);
|
||||
let cursor_point = cursor.to_point(&snapshot);
|
||||
let cursor_offset = cursor_point.to_offset(&snapshot);
|
||||
let events = self.events.clone();
|
||||
|
@ -375,6 +379,7 @@ impl Zeta {
|
|||
|
||||
Self::process_completion_response(
|
||||
output_excerpt,
|
||||
buffer,
|
||||
&snapshot,
|
||||
excerpt_range,
|
||||
cursor_offset,
|
||||
|
@ -606,6 +611,7 @@ and then another
|
|||
#[allow(clippy::too_many_arguments)]
|
||||
fn process_completion_response(
|
||||
output_excerpt: String,
|
||||
buffer: Entity<Buffer>,
|
||||
snapshot: &BufferSnapshot,
|
||||
excerpt_range: Range<usize>,
|
||||
cursor_offset: usize,
|
||||
|
@ -617,52 +623,110 @@ and then another
|
|||
cx: &AsyncApp,
|
||||
) -> Task<Result<Option<InlineCompletion>>> {
|
||||
let snapshot = snapshot.clone();
|
||||
cx.background_executor().spawn(async move {
|
||||
let content = output_excerpt.replace(CURSOR_MARKER, "");
|
||||
cx.spawn(|cx| async move {
|
||||
let output_excerpt: Arc<str> = output_excerpt.into();
|
||||
|
||||
let start_markers = content
|
||||
.match_indices(EDITABLE_REGION_START_MARKER)
|
||||
.collect::<Vec<_>>();
|
||||
anyhow::ensure!(
|
||||
start_markers.len() == 1,
|
||||
"expected exactly one start marker, found {}",
|
||||
start_markers.len()
|
||||
);
|
||||
let edits: Arc<[(Range<Anchor>, String)]> = cx
|
||||
.background_executor()
|
||||
.spawn({
|
||||
let output_excerpt = output_excerpt.clone();
|
||||
let excerpt_range = excerpt_range.clone();
|
||||
let snapshot = snapshot.clone();
|
||||
async move { Self::parse_edits(output_excerpt, excerpt_range, &snapshot) }
|
||||
})
|
||||
.await?
|
||||
.into();
|
||||
|
||||
let codefence_start = start_markers[0].0;
|
||||
let content = &content[codefence_start..];
|
||||
let Some((edits, snapshot, edit_preview)) = buffer.read_with(&cx, {
|
||||
let edits = edits.clone();
|
||||
|buffer, cx| {
|
||||
let new_snapshot = buffer.snapshot();
|
||||
let edits: Arc<[(Range<Anchor>, String)]> =
|
||||
interpolate(&snapshot, &new_snapshot, edits)?.into();
|
||||
Some((edits.clone(), new_snapshot, buffer.preview_edits(edits, cx)))
|
||||
}
|
||||
})?
|
||||
else {
|
||||
return anyhow::Ok(None);
|
||||
};
|
||||
|
||||
let newline_ix = content.find('\n').context("could not find newline")?;
|
||||
let content = &content[newline_ix + 1..];
|
||||
|
||||
let codefence_end = content
|
||||
.rfind(&format!("\n{EDITABLE_REGION_END_MARKER}"))
|
||||
.context("could not find end marker")?;
|
||||
let new_text = &content[..codefence_end];
|
||||
|
||||
let old_text = snapshot
|
||||
.text_for_range(excerpt_range.clone())
|
||||
.collect::<String>();
|
||||
|
||||
let edits = Self::compute_edits(old_text, new_text, excerpt_range.start, &snapshot);
|
||||
let edit_preview = edit_preview.await;
|
||||
|
||||
Ok(Some(InlineCompletion {
|
||||
id: InlineCompletionId::new(),
|
||||
path,
|
||||
excerpt_range,
|
||||
cursor_offset,
|
||||
edits: edits.into(),
|
||||
snapshot: snapshot.clone(),
|
||||
edits,
|
||||
edit_preview,
|
||||
snapshot,
|
||||
input_outline: input_outline.into(),
|
||||
input_events: input_events.into(),
|
||||
input_excerpt: input_excerpt.into(),
|
||||
output_excerpt: output_excerpt.into(),
|
||||
output_excerpt,
|
||||
request_sent_at,
|
||||
response_received_at: Instant::now(),
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_edits(
|
||||
output_excerpt: Arc<str>,
|
||||
excerpt_range: Range<usize>,
|
||||
snapshot: &BufferSnapshot,
|
||||
) -> Result<Vec<(Range<Anchor>, String)>> {
|
||||
let content = output_excerpt.replace(CURSOR_MARKER, "");
|
||||
|
||||
let start_markers = content
|
||||
.match_indices(EDITABLE_REGION_START_MARKER)
|
||||
.collect::<Vec<_>>();
|
||||
anyhow::ensure!(
|
||||
start_markers.len() == 1,
|
||||
"expected exactly one start marker, found {}",
|
||||
start_markers.len()
|
||||
);
|
||||
|
||||
let end_markers = content
|
||||
.match_indices(EDITABLE_REGION_END_MARKER)
|
||||
.collect::<Vec<_>>();
|
||||
anyhow::ensure!(
|
||||
end_markers.len() == 1,
|
||||
"expected exactly one end marker, found {}",
|
||||
end_markers.len()
|
||||
);
|
||||
|
||||
let sof_markers = content
|
||||
.match_indices(START_OF_FILE_MARKER)
|
||||
.collect::<Vec<_>>();
|
||||
anyhow::ensure!(
|
||||
sof_markers.len() <= 1,
|
||||
"expected at most one start-of-file marker, found {}",
|
||||
sof_markers.len()
|
||||
);
|
||||
|
||||
let codefence_start = start_markers[0].0;
|
||||
let content = &content[codefence_start..];
|
||||
|
||||
let newline_ix = content.find('\n').context("could not find newline")?;
|
||||
let content = &content[newline_ix + 1..];
|
||||
|
||||
let codefence_end = content
|
||||
.rfind(&format!("\n{EDITABLE_REGION_END_MARKER}"))
|
||||
.context("could not find end marker")?;
|
||||
let new_text = &content[..codefence_end];
|
||||
|
||||
let old_text = snapshot
|
||||
.text_for_range(excerpt_range.clone())
|
||||
.collect::<String>();
|
||||
|
||||
Ok(Self::compute_edits(
|
||||
old_text,
|
||||
new_text,
|
||||
excerpt_range.start,
|
||||
&snapshot,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn compute_edits(
|
||||
old_text: String,
|
||||
new_text: &str,
|
||||
|
@ -721,10 +785,13 @@ and then another
|
|||
old_range.end = old_range.end.saturating_sub(suffix_len);
|
||||
|
||||
let new_text = new_text[prefix_len..new_text.len() - suffix_len].to_string();
|
||||
(
|
||||
snapshot.anchor_after(old_range.start)..snapshot.anchor_before(old_range.end),
|
||||
new_text,
|
||||
)
|
||||
let range = if old_range.is_empty() {
|
||||
let anchor = snapshot.anchor_after(old_range.start);
|
||||
anchor..anchor
|
||||
} else {
|
||||
snapshot.anchor_after(old_range.start)..snapshot.anchor_before(old_range.end)
|
||||
};
|
||||
(range, new_text)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
@ -1434,6 +1501,7 @@ impl inline_completion::InlineCompletionProvider for ZetaInlineCompletionProvide
|
|||
|
||||
Some(inline_completion::InlineCompletion {
|
||||
edits: edits[edit_start_ix..edit_end_ix].to_vec(),
|
||||
edit_preview: Some(completion.edit_preview.clone()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -1452,18 +1520,24 @@ mod tests {
|
|||
use super::*;
|
||||
|
||||
#[gpui::test]
|
||||
fn test_inline_completion_basic_interpolation(cx: &mut TestAppContext) {
|
||||
async fn test_inline_completion_basic_interpolation(cx: &mut TestAppContext) {
|
||||
let buffer = cx.new(|cx| Buffer::local("Lorem ipsum dolor", cx));
|
||||
let edits: Arc<[(Range<Anchor>, String)]> = cx.update(|cx| {
|
||||
to_completion_edits(
|
||||
[(2..5, "REM".to_string()), (9..11, "".to_string())],
|
||||
&buffer,
|
||||
cx,
|
||||
)
|
||||
.into()
|
||||
});
|
||||
|
||||
let edit_preview = cx
|
||||
.read(|cx| buffer.read(cx).preview_edits(edits.clone(), cx))
|
||||
.await;
|
||||
|
||||
let completion = InlineCompletion {
|
||||
edits: cx
|
||||
.read(|cx| {
|
||||
to_completion_edits(
|
||||
[(2..5, "REM".to_string()), (9..11, "".to_string())],
|
||||
&buffer,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.into(),
|
||||
edits,
|
||||
edit_preview,
|
||||
path: Path::new("").into(),
|
||||
snapshot: cx.read(|cx| buffer.read(cx).snapshot()),
|
||||
id: InlineCompletionId::new(),
|
||||
|
@ -1477,106 +1551,89 @@ mod tests {
|
|||
response_received_at: Instant::now(),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
cx.read(|cx| {
|
||||
cx.update(|cx| {
|
||||
assert_eq!(
|
||||
from_completion_edits(
|
||||
&completion.interpolate(&buffer.read(cx).snapshot()).unwrap(),
|
||||
&buffer,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
vec![(2..5, "REM".to_string()), (9..11, "".to_string())]
|
||||
);
|
||||
cx
|
||||
),
|
||||
vec![(2..5, "REM".to_string()), (9..11, "".to_string())]
|
||||
);
|
||||
|
||||
buffer.update(cx, |buffer, cx| buffer.edit([(2..5, "")], None, cx));
|
||||
assert_eq!(
|
||||
cx.read(|cx| {
|
||||
buffer.update(cx, |buffer, cx| buffer.edit([(2..5, "")], None, cx));
|
||||
assert_eq!(
|
||||
from_completion_edits(
|
||||
&completion.interpolate(&buffer.read(cx).snapshot()).unwrap(),
|
||||
&buffer,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
vec![(2..2, "REM".to_string()), (6..8, "".to_string())]
|
||||
);
|
||||
cx
|
||||
),
|
||||
vec![(2..2, "REM".to_string()), (6..8, "".to_string())]
|
||||
);
|
||||
|
||||
buffer.update(cx, |buffer, cx| buffer.undo(cx));
|
||||
assert_eq!(
|
||||
cx.read(|cx| {
|
||||
buffer.update(cx, |buffer, cx| buffer.undo(cx));
|
||||
assert_eq!(
|
||||
from_completion_edits(
|
||||
&completion.interpolate(&buffer.read(cx).snapshot()).unwrap(),
|
||||
&buffer,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
vec![(2..5, "REM".to_string()), (9..11, "".to_string())]
|
||||
);
|
||||
cx
|
||||
),
|
||||
vec![(2..5, "REM".to_string()), (9..11, "".to_string())]
|
||||
);
|
||||
|
||||
buffer.update(cx, |buffer, cx| buffer.edit([(2..5, "R")], None, cx));
|
||||
assert_eq!(
|
||||
cx.read(|cx| {
|
||||
buffer.update(cx, |buffer, cx| buffer.edit([(2..5, "R")], None, cx));
|
||||
assert_eq!(
|
||||
from_completion_edits(
|
||||
&completion.interpolate(&buffer.read(cx).snapshot()).unwrap(),
|
||||
&buffer,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
vec![(3..3, "EM".to_string()), (7..9, "".to_string())]
|
||||
);
|
||||
cx
|
||||
),
|
||||
vec![(3..3, "EM".to_string()), (7..9, "".to_string())]
|
||||
);
|
||||
|
||||
buffer.update(cx, |buffer, cx| buffer.edit([(3..3, "E")], None, cx));
|
||||
assert_eq!(
|
||||
cx.read(|cx| {
|
||||
buffer.update(cx, |buffer, cx| buffer.edit([(3..3, "E")], None, cx));
|
||||
assert_eq!(
|
||||
from_completion_edits(
|
||||
&completion.interpolate(&buffer.read(cx).snapshot()).unwrap(),
|
||||
&buffer,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
vec![(4..4, "M".to_string()), (8..10, "".to_string())]
|
||||
);
|
||||
cx
|
||||
),
|
||||
vec![(4..4, "M".to_string()), (8..10, "".to_string())]
|
||||
);
|
||||
|
||||
buffer.update(cx, |buffer, cx| buffer.edit([(4..4, "M")], None, cx));
|
||||
assert_eq!(
|
||||
cx.read(|cx| {
|
||||
buffer.update(cx, |buffer, cx| buffer.edit([(4..4, "M")], None, cx));
|
||||
assert_eq!(
|
||||
from_completion_edits(
|
||||
&completion.interpolate(&buffer.read(cx).snapshot()).unwrap(),
|
||||
&buffer,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
vec![(9..11, "".to_string())]
|
||||
);
|
||||
cx
|
||||
),
|
||||
vec![(9..11, "".to_string())]
|
||||
);
|
||||
|
||||
buffer.update(cx, |buffer, cx| buffer.edit([(4..5, "")], None, cx));
|
||||
assert_eq!(
|
||||
cx.read(|cx| {
|
||||
buffer.update(cx, |buffer, cx| buffer.edit([(4..5, "")], None, cx));
|
||||
assert_eq!(
|
||||
from_completion_edits(
|
||||
&completion.interpolate(&buffer.read(cx).snapshot()).unwrap(),
|
||||
&buffer,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
vec![(4..4, "M".to_string()), (8..10, "".to_string())]
|
||||
);
|
||||
cx
|
||||
),
|
||||
vec![(4..4, "M".to_string()), (8..10, "".to_string())]
|
||||
);
|
||||
|
||||
buffer.update(cx, |buffer, cx| buffer.edit([(8..10, "")], None, cx));
|
||||
assert_eq!(
|
||||
cx.read(|cx| {
|
||||
buffer.update(cx, |buffer, cx| buffer.edit([(8..10, "")], None, cx));
|
||||
assert_eq!(
|
||||
from_completion_edits(
|
||||
&completion.interpolate(&buffer.read(cx).snapshot()).unwrap(),
|
||||
&buffer,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
vec![(4..4, "M".to_string())]
|
||||
);
|
||||
cx
|
||||
),
|
||||
vec![(4..4, "M".to_string())]
|
||||
);
|
||||
|
||||
buffer.update(cx, |buffer, cx| buffer.edit([(4..6, "")], None, cx));
|
||||
assert_eq!(
|
||||
cx.read(|cx| completion.interpolate(&buffer.read(cx).snapshot())),
|
||||
None
|
||||
);
|
||||
buffer.update(cx, |buffer, cx| buffer.edit([(4..6, "")], None, cx));
|
||||
assert_eq!(completion.interpolate(&buffer.read(cx).snapshot()), None);
|
||||
})
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue