Compare commits
3 Commits
edit-diffs
...
fix-highli
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0529a4f125 | ||
|
|
92d16eaa5d | ||
|
|
949562fed9 |
@@ -4,7 +4,7 @@ use gpui::{App, Context, Entity, EntityId, Task};
|
||||
use inline_completion::{Direction, InlineCompletion, InlineCompletionProvider};
|
||||
use language::{
|
||||
language_settings::{all_language_settings, AllLanguageSettings},
|
||||
Buffer, OffsetRangeExt, ToOffset,
|
||||
Buffer, OffsetRangeExt, PlainTextEdit, ToOffset,
|
||||
};
|
||||
use settings::Settings;
|
||||
use std::{path::Path, time::Duration};
|
||||
@@ -255,8 +255,11 @@ impl InlineCompletionProvider for CopilotCompletionProvider {
|
||||
} else {
|
||||
let position = cursor_position.bias_right(buffer);
|
||||
Some(InlineCompletion {
|
||||
edits: vec![(position..position, completion_text.into())],
|
||||
edit_preview: None,
|
||||
edits: vec![PlainTextEdit {
|
||||
old_range: position..position,
|
||||
new_text: completion_text.into(),
|
||||
}
|
||||
.into()],
|
||||
})
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -96,9 +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, EditPreview, HighlightedEdits, IndentKind, IndentSize,
|
||||
Language, OffsetRangeExt, Point, Selection, SelectionGoal, TextObject, TransactionId,
|
||||
TreeSitterOptions,
|
||||
CursorShape, Diagnostic, Documentation, HighlightedEdits, IndentKind, IndentSize, Language,
|
||||
OffsetRangeExt, Point, Selection, SelectionGoal, TextEdit, TextEditWithNewHighlights,
|
||||
TextObject, TransactionId, TreeSitterOptions,
|
||||
};
|
||||
use language::{point_to_lsp, BufferRow, CharClassifier, Runnable, RunnableRange};
|
||||
use linked_editing_ranges::refresh_linked_ranges;
|
||||
@@ -499,8 +499,7 @@ pub(crate) enum EditDisplayMode {
|
||||
|
||||
enum InlineCompletion {
|
||||
Edit {
|
||||
edits: Vec<(Range<Anchor>, String)>,
|
||||
edit_preview: Option<EditPreview>,
|
||||
edits: Vec<TextEditWithNewHighlights<Anchor>>,
|
||||
display_mode: EditDisplayMode,
|
||||
snapshot: BufferSnapshot,
|
||||
},
|
||||
@@ -4854,10 +4853,16 @@ impl Editor {
|
||||
}
|
||||
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
let last_edit_end = edits.last().unwrap().0.end.bias_right(&snapshot);
|
||||
let last_edit_end = edits.last().unwrap().old_range().end.bias_right(&snapshot);
|
||||
|
||||
self.buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit(edits.iter().cloned(), None, cx)
|
||||
buffer.edit(
|
||||
edits
|
||||
.iter()
|
||||
.map(|edit| (edit.old_range().clone(), edit.new_text().clone())),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
self.change_selections(None, window, cx, |s| {
|
||||
@@ -4900,10 +4905,10 @@ impl Editor {
|
||||
// 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();
|
||||
let insertion = edits.iter().find_map(|(range, text)| {
|
||||
let range = range.to_offset(&snapshot);
|
||||
let insertion = edits.iter().find_map(|edit| {
|
||||
let range = edit.old_range().to_offset(&snapshot);
|
||||
if range.is_empty() && range.start == cursor_offset {
|
||||
Some(text)
|
||||
Some(edit.new_text())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -5039,21 +5044,19 @@ impl Editor {
|
||||
let edits = inline_completion
|
||||
.edits
|
||||
.into_iter()
|
||||
.flat_map(|(range, new_text)| {
|
||||
let start = multibuffer.anchor_in_excerpt(excerpt_id, range.start)?;
|
||||
let end = multibuffer.anchor_in_excerpt(excerpt_id, range.end)?;
|
||||
Some((start..end, new_text))
|
||||
.flat_map(|edit| {
|
||||
edit.maybe_map_position(|anchor| multibuffer.anchor_in_excerpt(excerpt_id, anchor))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if edits.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let first_edit_start = edits.first().unwrap().0.start;
|
||||
let first_edit_start = edits.first().unwrap().old_range().start;
|
||||
let first_edit_start_point = first_edit_start.to_point(&multibuffer);
|
||||
let edit_start_row = first_edit_start_point.row.saturating_sub(2);
|
||||
|
||||
let last_edit_end = edits.last().unwrap().0.end;
|
||||
let last_edit_end = edits.last().unwrap().old_range().end;
|
||||
let last_edit_end_point = last_edit_end.to_point(&multibuffer);
|
||||
let edit_end_row = cmp::min(multibuffer.max_point().row, last_edit_end_point.row + 2);
|
||||
|
||||
@@ -5070,14 +5073,14 @@ impl Editor {
|
||||
} else {
|
||||
if edits
|
||||
.iter()
|
||||
.all(|(range, _)| range.to_offset(&multibuffer).is_empty())
|
||||
.all(|edit| edit.old_range().to_offset(&multibuffer).is_empty())
|
||||
{
|
||||
let mut inlays = Vec::new();
|
||||
for (range, new_text) in &edits {
|
||||
for edit in &edits {
|
||||
let inlay = Inlay::inline_completion(
|
||||
post_inc(&mut self.next_inlay_id),
|
||||
range.start,
|
||||
new_text.as_str(),
|
||||
edit.old_range().start,
|
||||
edit.new_text().as_str(),
|
||||
);
|
||||
inlay_ids.push(inlay.id);
|
||||
inlays.push(inlay);
|
||||
@@ -5087,7 +5090,7 @@ impl Editor {
|
||||
} else {
|
||||
let background_color = cx.theme().status().deleted_background;
|
||||
self.highlight_text::<InlineCompletionHighlight>(
|
||||
edits.iter().map(|(range, _)| range.clone()).collect(),
|
||||
edits.iter().map(|edit| edit.old_range().clone()).collect(),
|
||||
HighlightStyle {
|
||||
background_color: Some(background_color),
|
||||
..Default::default()
|
||||
@@ -5101,7 +5104,7 @@ impl Editor {
|
||||
let display_mode = if all_edits_insertions_or_deletions(&edits, &multibuffer) {
|
||||
if provider.show_tab_accept_marker()
|
||||
&& first_edit_start_point.row == last_edit_end_point.row
|
||||
&& !edits.iter().any(|(_, edit)| edit.contains('\n'))
|
||||
&& !edits.iter().any(|edit| edit.new_text().contains('\n'))
|
||||
{
|
||||
EditDisplayMode::TabAccept
|
||||
} else {
|
||||
@@ -5115,7 +5118,6 @@ impl Editor {
|
||||
|
||||
InlineCompletion::Edit {
|
||||
edits,
|
||||
edit_preview: inline_completion.edit_preview,
|
||||
display_mode,
|
||||
snapshot,
|
||||
}
|
||||
@@ -5162,14 +5164,9 @@ impl Editor {
|
||||
let text = match &self.active_inline_completion.as_ref()?.completion {
|
||||
InlineCompletion::Edit {
|
||||
edits,
|
||||
edit_preview,
|
||||
display_mode: _,
|
||||
snapshot,
|
||||
} => edit_preview
|
||||
.as_ref()
|
||||
.and_then(|edit_preview| {
|
||||
inline_completion_edit_text(&snapshot, &edits, edit_preview, true, cx)
|
||||
})
|
||||
} => inline_completion_edit_text(&snapshot, &edits, true, cx)
|
||||
.map(InlineCompletionText::Edit),
|
||||
InlineCompletion::Move(target) => {
|
||||
let target_point =
|
||||
@@ -15822,22 +15819,21 @@ pub fn diagnostic_block_renderer(
|
||||
|
||||
fn inline_completion_edit_text(
|
||||
current_snapshot: &BufferSnapshot,
|
||||
edits: &[(Range<Anchor>, String)],
|
||||
edit_preview: &EditPreview,
|
||||
edits: &[TextEditWithNewHighlights<Anchor>],
|
||||
include_deletions: bool,
|
||||
cx: &App,
|
||||
) -> Option<HighlightedEdits> {
|
||||
let edits = edits
|
||||
.iter()
|
||||
.map(|(anchor, text)| {
|
||||
(
|
||||
anchor.start.text_anchor..anchor.end.text_anchor,
|
||||
text.clone(),
|
||||
)
|
||||
})
|
||||
.map(|edit| edit.clone().map_position(|anchor| anchor.text_anchor))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Some(edit_preview.highlight_edits(current_snapshot, &edits, include_deletions, cx))
|
||||
Some(HighlightedEdits::highlight(
|
||||
current_snapshot,
|
||||
&edits,
|
||||
include_deletions,
|
||||
cx,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn highlight_diagnostic_message(
|
||||
@@ -16096,15 +16092,15 @@ impl Global for KillRing {}
|
||||
const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50);
|
||||
|
||||
fn all_edits_insertions_or_deletions(
|
||||
edits: &Vec<(Range<Anchor>, String)>,
|
||||
edits: &Vec<TextEditWithNewHighlights<Anchor>>,
|
||||
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();
|
||||
for edit in edits.iter() {
|
||||
let range_is_empty = edit.old_range().to_offset(&snapshot).is_empty();
|
||||
let text_is_empty = edit.new_text().is_empty();
|
||||
|
||||
if range_is_empty != text_is_empty {
|
||||
if range_is_empty {
|
||||
|
||||
@@ -20,7 +20,7 @@ use language::{
|
||||
BracketPairConfig,
|
||||
Capability::ReadWrite,
|
||||
FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher, LanguageName,
|
||||
Override, ParsedMarkdown, Point,
|
||||
Override, ParsedMarkdown, PlainTextEdit, Point,
|
||||
};
|
||||
use language_settings::{Formatter, FormatterList, IndentGuideSettings};
|
||||
use multi_buffer::IndentGuide;
|
||||
@@ -15421,39 +15421,45 @@ async fn assert_highlighted_edits(
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let edits = edits
|
||||
let text_anchor_edits = edits
|
||||
.into_iter()
|
||||
.map(|(range, edit)| {
|
||||
(
|
||||
snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
|
||||
edit,
|
||||
)
|
||||
.map(|(range, insertion)| PlainTextEdit {
|
||||
old_range: snapshot.anchor_after(range.start).text_anchor
|
||||
..snapshot.anchor_before(range.end).text_anchor,
|
||||
new_text: insertion,
|
||||
})
|
||||
.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| {
|
||||
let edits = cx
|
||||
.update(|_window, cx| {
|
||||
buffer
|
||||
.read(cx)
|
||||
.as_singleton()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.preview_edits(text_anchor_edits.into(), cx)
|
||||
.highlight_edit_insertions(text_anchor_edits, cx.background_executor())
|
||||
})
|
||||
.unwrap()
|
||||
.await;
|
||||
|
||||
let edits = cx.update(|_window, cx| {
|
||||
let multibuffer_snapshot = buffer.read(cx).snapshot(cx);
|
||||
let excerpt_id = multibuffer_snapshot.excerpts().next().unwrap().0;
|
||||
edits
|
||||
.into_iter()
|
||||
.map(|edit| {
|
||||
edit.map_position(|anchor| {
|
||||
multibuffer_snapshot
|
||||
.anchor_in_excerpt(excerpt_id, anchor)
|
||||
.unwrap()
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
cx.update(|_window, cx| {
|
||||
let highlighted_edits = inline_completion_edit_text(
|
||||
&snapshot.as_singleton().unwrap().2,
|
||||
&edits,
|
||||
&edit_preview,
|
||||
include_deletions,
|
||||
cx,
|
||||
)
|
||||
|
||||
@@ -44,7 +44,7 @@ use language::{
|
||||
IndentGuideBackgroundColoring, IndentGuideColoring, IndentGuideSettings,
|
||||
ShowWhitespaceSetting,
|
||||
},
|
||||
ChunkRendererContext,
|
||||
ChunkRendererContext, TextEdit,
|
||||
};
|
||||
use lsp::DiagnosticSeverity;
|
||||
use multi_buffer::{
|
||||
@@ -3432,7 +3432,6 @@ impl EditorElement {
|
||||
}
|
||||
InlineCompletion::Edit {
|
||||
edits,
|
||||
edit_preview,
|
||||
display_mode,
|
||||
snapshot,
|
||||
} => {
|
||||
@@ -3443,13 +3442,13 @@ impl EditorElement {
|
||||
let edit_start = edits
|
||||
.first()
|
||||
.unwrap()
|
||||
.0
|
||||
.old_range()
|
||||
.start
|
||||
.to_display_point(editor_snapshot);
|
||||
let edit_end = edits
|
||||
.last()
|
||||
.unwrap()
|
||||
.0
|
||||
.old_range()
|
||||
.end
|
||||
.to_display_point(editor_snapshot);
|
||||
|
||||
@@ -3461,7 +3460,7 @@ impl EditorElement {
|
||||
|
||||
match display_mode {
|
||||
EditDisplayMode::TabAccept => {
|
||||
let range = &edits.first()?.0;
|
||||
let range = &edits.first()?.old_range();
|
||||
let target_display_point = range.end.to_display_point(editor_snapshot);
|
||||
|
||||
let target_line_end = DisplayPoint::new(
|
||||
@@ -3487,9 +3486,8 @@ impl EditorElement {
|
||||
EditDisplayMode::DiffPopover => {}
|
||||
}
|
||||
|
||||
let highlighted_edits = edit_preview.as_ref().and_then(|edit_preview| {
|
||||
crate::inline_completion_edit_text(&snapshot, edits, edit_preview, false, cx)
|
||||
})?;
|
||||
let highlighted_edits =
|
||||
crate::inline_completion_edit_text(&snapshot, edits, false, cx)?;
|
||||
|
||||
let line_count = highlighted_edits.text.lines().count() + 1;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use gpui::{prelude::*, Entity};
|
||||
use indoc::indoc;
|
||||
use inline_completion::InlineCompletionProvider;
|
||||
use language::{Language, LanguageConfig};
|
||||
use language::{Language, LanguageConfig, PlainTextEdit, TextEdit, TextEditWithNewHighlights};
|
||||
use multi_buffer::{Anchor, MultiBufferSnapshot, ToPoint};
|
||||
use std::{num::NonZeroU32, ops::Range, sync::Arc};
|
||||
use text::{Point, ToOffset};
|
||||
@@ -24,7 +24,7 @@ async fn test_inline_completion_insert(cx: &mut gpui::TestAppContext) {
|
||||
|
||||
assert_editor_active_edit_completion(&mut cx, |_, edits| {
|
||||
assert_eq!(edits.len(), 1);
|
||||
assert_eq!(edits[0].1.as_str(), "-273.15");
|
||||
assert_eq!(edits[0].new_text().as_str(), "-273.15");
|
||||
});
|
||||
|
||||
accept_completion(&mut cx);
|
||||
@@ -46,7 +46,7 @@ async fn test_inline_completion_modification(cx: &mut gpui::TestAppContext) {
|
||||
|
||||
assert_editor_active_edit_completion(&mut cx, |_, edits| {
|
||||
assert_eq!(edits.len(), 1);
|
||||
assert_eq!(edits[0].1.as_str(), "3.14159");
|
||||
assert_eq!(edits[0].new_text().as_str(), "3.14159");
|
||||
});
|
||||
|
||||
accept_completion(&mut cx);
|
||||
@@ -158,7 +158,7 @@ async fn test_indentation(cx: &mut gpui::TestAppContext) {
|
||||
|
||||
assert_editor_active_edit_completion(&mut cx, |_, edits| {
|
||||
assert_eq!(edits.len(), 1);
|
||||
assert_eq!(edits[0].1.as_str(), " const function()");
|
||||
assert_eq!(edits[0].new_text().as_str(), " const function()");
|
||||
});
|
||||
|
||||
// When the cursor is before the suggested indentation level, accepting a
|
||||
@@ -278,7 +278,7 @@ async fn test_inline_completion_invalidation_range(cx: &mut gpui::TestAppContext
|
||||
|
||||
fn assert_editor_active_edit_completion(
|
||||
cx: &mut EditorTestContext,
|
||||
assert: impl FnOnce(MultiBufferSnapshot, &Vec<(Range<Anchor>, String)>),
|
||||
assert: impl FnOnce(MultiBufferSnapshot, &Vec<TextEditWithNewHighlights<Anchor>>),
|
||||
) {
|
||||
cx.editor(|editor, _, cx| {
|
||||
let completion_state = editor
|
||||
@@ -324,16 +324,18 @@ fn propose_edits<T: ToOffset>(
|
||||
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())
|
||||
let edits = edits.into_iter().map(|edit| {
|
||||
PlainTextEdit {
|
||||
old_range: snapshot.anchor_after(edit.0.start)..snapshot.anchor_before(edit.0.end),
|
||||
new_text: edit.1.to_string(),
|
||||
}
|
||||
.into()
|
||||
});
|
||||
|
||||
cx.update(|_, cx| {
|
||||
provider.update(cx, |provider, _| {
|
||||
provider.set_inline_completion(Some(inline_completion::InlineCompletion {
|
||||
edits: edits.collect(),
|
||||
edit_preview: None,
|
||||
}))
|
||||
})
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use gpui::{App, Context, Entity};
|
||||
use language::Buffer;
|
||||
use std::ops::Range;
|
||||
use language::{Buffer, TextEditWithNewHighlights};
|
||||
|
||||
// TODO: Find a better home for `Direction`.
|
||||
//
|
||||
@@ -14,8 +13,7 @@ pub enum Direction {
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct InlineCompletion {
|
||||
pub edits: Vec<(Range<language::Anchor>, String)>,
|
||||
pub edit_preview: Option<language::EditPreview>,
|
||||
pub edits: Vec<TextEditWithNewHighlights<language::Anchor>>,
|
||||
}
|
||||
|
||||
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,
|
||||
SharedString, Task, TaskLabel, Window,
|
||||
AnyElement, App, AppContext as _, BackgroundExecutor, Context, Entity, EventEmitter,
|
||||
HighlightStyle, Pixels, SharedString, Task, TaskLabel, Window,
|
||||
};
|
||||
use lsp::LanguageServerId;
|
||||
use parking_lot::Mutex;
|
||||
@@ -588,10 +588,128 @@ pub struct Runnable {
|
||||
pub buffer: BufferId,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct EditPreview {
|
||||
applied_edits_snapshot: text::BufferSnapshot,
|
||||
syntax_snapshot: SyntaxSnapshot,
|
||||
pub trait TextEdit<T: Clone> {
|
||||
fn old_range(&self) -> &Range<T>;
|
||||
fn mut_old_range(&mut self) -> &mut Range<T>;
|
||||
fn new_text(&self) -> &String;
|
||||
fn with_prefix_dropped(&self, prefix_length: usize) -> Self;
|
||||
|
||||
// TODO: replace uses of tuples with PlainTextEdit.
|
||||
fn to_tuple(&self) -> (Range<T>, String) {
|
||||
(self.old_range().clone(), self.new_text().clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct PlainTextEdit<T> {
|
||||
pub old_range: Range<T>,
|
||||
pub new_text: String,
|
||||
}
|
||||
|
||||
impl<T: Clone> TextEdit<T> for PlainTextEdit<T> {
|
||||
fn old_range(&self) -> &Range<T> {
|
||||
&self.old_range
|
||||
}
|
||||
|
||||
fn mut_old_range(&mut self) -> &mut Range<T> {
|
||||
&mut self.old_range
|
||||
}
|
||||
|
||||
fn new_text(&self) -> &String {
|
||||
&self.new_text
|
||||
}
|
||||
|
||||
fn with_prefix_dropped(&self, prefix_length: usize) -> Self {
|
||||
PlainTextEdit {
|
||||
old_range: self.old_range.clone(),
|
||||
new_text: self.new_text[prefix_length..].to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> PlainTextEdit<T> {
|
||||
pub fn map_position<O>(self, f: impl Fn(T) -> O) -> PlainTextEdit<O> {
|
||||
PlainTextEdit {
|
||||
old_range: f(self.old_range.start)..f(self.old_range.end),
|
||||
new_text: self.new_text.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn maybe_map_position<O>(self, f: impl Fn(T) -> Option<O>) -> Option<PlainTextEdit<O>> {
|
||||
Some(PlainTextEdit {
|
||||
old_range: f(self.old_range.start)?..f(self.old_range.end)?,
|
||||
new_text: self.new_text.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct TextEditWithNewHighlights<T> {
|
||||
pub text_edit: PlainTextEdit<T>,
|
||||
pub new_highlights: Vec<(Range<usize>, HighlightId)>,
|
||||
}
|
||||
|
||||
impl<T: Clone> TextEdit<T> for TextEditWithNewHighlights<T> {
|
||||
fn old_range(&self) -> &Range<T> {
|
||||
&self.text_edit.old_range
|
||||
}
|
||||
|
||||
fn mut_old_range(&mut self) -> &mut Range<T> {
|
||||
&mut self.text_edit.old_range
|
||||
}
|
||||
|
||||
fn new_text(&self) -> &String {
|
||||
&self.text_edit.new_text
|
||||
}
|
||||
|
||||
fn with_prefix_dropped(&self, prefix_length: usize) -> Self {
|
||||
TextEditWithNewHighlights {
|
||||
text_edit: self.text_edit.with_prefix_dropped(prefix_length),
|
||||
new_highlights: self.new_highlights.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> TextEditWithNewHighlights<T> {
|
||||
pub fn map_edit<O>(
|
||||
self,
|
||||
f: impl FnOnce(PlainTextEdit<T>) -> PlainTextEdit<O>,
|
||||
) -> TextEditWithNewHighlights<O> {
|
||||
TextEditWithNewHighlights {
|
||||
text_edit: f(self.text_edit),
|
||||
new_highlights: self.new_highlights,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn maybe_map_edit<O>(
|
||||
self,
|
||||
f: impl FnOnce(PlainTextEdit<T>) -> Option<PlainTextEdit<O>>,
|
||||
) -> Option<TextEditWithNewHighlights<O>> {
|
||||
Some(TextEditWithNewHighlights {
|
||||
text_edit: f(self.text_edit)?,
|
||||
new_highlights: self.new_highlights,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn map_position<O>(self, f: impl Fn(T) -> O) -> TextEditWithNewHighlights<O> {
|
||||
self.map_edit(|edit| edit.map_position(f))
|
||||
}
|
||||
|
||||
pub fn maybe_map_position<O>(
|
||||
self,
|
||||
f: impl Fn(T) -> Option<O>,
|
||||
) -> Option<TextEditWithNewHighlights<O>> {
|
||||
self.maybe_map_edit(|edit| edit.maybe_map_position(f))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<PlainTextEdit<T>> for TextEditWithNewHighlights<T> {
|
||||
fn from(text_edit: PlainTextEdit<T>) -> Self {
|
||||
TextEditWithNewHighlights {
|
||||
text_edit,
|
||||
new_highlights: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Debug)]
|
||||
@@ -600,76 +718,102 @@ pub struct HighlightedEdits {
|
||||
pub highlights: Vec<(Range<usize>, HighlightStyle)>,
|
||||
}
|
||||
|
||||
impl EditPreview {
|
||||
pub fn highlight_edits(
|
||||
&self,
|
||||
impl HighlightedEdits {
|
||||
pub fn highlight(
|
||||
current_snapshot: &BufferSnapshot,
|
||||
edits: &[(Range<Anchor>, String)],
|
||||
edits: &[TextEditWithNewHighlights<Anchor>],
|
||||
include_deletions: bool,
|
||||
cx: &App,
|
||||
) -> HighlightedEdits {
|
||||
let mut text = String::new();
|
||||
let mut highlights = Vec::new();
|
||||
let Some(range) = self.compute_visible_range(edits, current_snapshot) else {
|
||||
|
||||
let Some(visible_range) = Self::compute_visible_range(edits, current_snapshot) else {
|
||||
return HighlightedEdits::default();
|
||||
};
|
||||
let mut offset = range.start;
|
||||
let mut delta = 0isize;
|
||||
|
||||
let mut start_of_unchanged = visible_range.start;
|
||||
|
||||
let syntax_theme = cx.theme().syntax();
|
||||
let status_colors = cx.theme().status();
|
||||
let deleted_highlight = HighlightStyle {
|
||||
background_color: Some(status_colors.deleted_background),
|
||||
..Default::default()
|
||||
};
|
||||
let created_highlight = HighlightStyle {
|
||||
background_color: Some(status_colors.created_background),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
for (range, edit_text) in edits {
|
||||
let edit_range = range.to_offset(current_snapshot);
|
||||
let new_edit_start = (edit_range.start as isize + delta) as usize;
|
||||
let new_edit_range = new_edit_start..new_edit_start + edit_text.len();
|
||||
for edit in edits {
|
||||
let edit_range = edit.text_edit.old_range.to_offset(current_snapshot);
|
||||
let unchanged_range = start_of_unchanged..edit_range.start;
|
||||
start_of_unchanged = edit_range.end;
|
||||
|
||||
let prev_range = offset..new_edit_start;
|
||||
|
||||
if !prev_range.is_empty() {
|
||||
let start = text.len();
|
||||
self.highlight_text(prev_range, &mut text, &mut highlights, None, cx);
|
||||
offset += text.len() - start;
|
||||
if !unchanged_range.is_empty() {
|
||||
Self::add_highlighted_text_from(
|
||||
unchanged_range,
|
||||
¤t_snapshot,
|
||||
None,
|
||||
syntax_theme,
|
||||
&mut text,
|
||||
&mut highlights,
|
||||
);
|
||||
}
|
||||
|
||||
if include_deletions && !edit_range.is_empty() {
|
||||
let start = text.len();
|
||||
text.extend(current_snapshot.text_for_range(edit_range.clone()));
|
||||
let end = text.len();
|
||||
|
||||
highlights.push((
|
||||
start..end,
|
||||
HighlightStyle {
|
||||
background_color: Some(status_colors.deleted_background),
|
||||
..Default::default()
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
if !edit_text.is_empty() {
|
||||
self.highlight_text(
|
||||
new_edit_range,
|
||||
Self::add_highlighted_text_from(
|
||||
edit_range,
|
||||
¤t_snapshot,
|
||||
Some(deleted_highlight),
|
||||
syntax_theme,
|
||||
&mut text,
|
||||
&mut highlights,
|
||||
Some(HighlightStyle {
|
||||
background_color: Some(status_colors.created_background),
|
||||
..Default::default()
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
|
||||
offset += edit_text.len();
|
||||
}
|
||||
|
||||
delta += edit_text.len() as isize - edit_range.len() as isize;
|
||||
let new_text = &edit.text_edit.new_text;
|
||||
if !new_text.is_empty() {
|
||||
let delta = text.len();
|
||||
text.push_str(&new_text);
|
||||
let mut offset = delta;
|
||||
for (highlight_range, highlight_id) in edit.new_highlights.iter() {
|
||||
let start = highlight_range.start + delta;
|
||||
let end = highlight_range.end + delta;
|
||||
let unhighlighted_range = offset..start;
|
||||
if !unhighlighted_range.is_empty() {
|
||||
highlights.push((unhighlighted_range, created_highlight));
|
||||
}
|
||||
highlights.push((
|
||||
start..end,
|
||||
highlight_id.style(syntax_theme).map_or(
|
||||
created_highlight,
|
||||
|mut highlight_style| {
|
||||
highlight_style.highlight(created_highlight);
|
||||
highlight_style
|
||||
},
|
||||
),
|
||||
));
|
||||
offset = end;
|
||||
}
|
||||
let unhighlighted_range = offset..text.len();
|
||||
if !unhighlighted_range.is_empty() {
|
||||
highlights.push((unhighlighted_range, created_highlight));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.highlight_text(
|
||||
offset..(range.end as isize + delta) as usize,
|
||||
&mut text,
|
||||
&mut highlights,
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
let unchanged_range = start_of_unchanged..visible_range.end;
|
||||
if !unchanged_range.is_empty() {
|
||||
Self::add_highlighted_text_from(
|
||||
unchanged_range,
|
||||
¤t_snapshot,
|
||||
None,
|
||||
syntax_theme,
|
||||
&mut text,
|
||||
&mut highlights,
|
||||
);
|
||||
}
|
||||
|
||||
HighlightedEdits {
|
||||
text: text.into(),
|
||||
@@ -677,22 +821,22 @@ impl EditPreview {
|
||||
}
|
||||
}
|
||||
|
||||
fn highlight_text(
|
||||
&self,
|
||||
fn add_highlighted_text_from(
|
||||
range: Range<usize>,
|
||||
snapshot: &BufferSnapshot,
|
||||
override_style: Option<HighlightStyle>,
|
||||
syntax_theme: &SyntaxTheme,
|
||||
text: &mut String,
|
||||
highlights: &mut Vec<(Range<usize>, HighlightStyle)>,
|
||||
override_style: Option<HighlightStyle>,
|
||||
cx: &App,
|
||||
) {
|
||||
for chunk in self.highlighted_chunks(range) {
|
||||
for chunk in snapshot.chunks(range, true) {
|
||||
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()))
|
||||
.and_then(|id| id.style(syntax_theme))
|
||||
{
|
||||
if let Some(override_style) = override_style {
|
||||
highlight_style.highlight(override_style);
|
||||
@@ -704,38 +848,17 @@ impl EditPreview {
|
||||
}
|
||||
}
|
||||
|
||||
fn highlighted_chunks(&self, range: Range<usize>) -> BufferChunks {
|
||||
let captures =
|
||||
self.syntax_snapshot
|
||||
.captures(range.clone(), &self.applied_edits_snapshot, |grammar| {
|
||||
grammar.highlights_query.as_ref()
|
||||
});
|
||||
|
||||
let highlight_maps = captures
|
||||
.grammars()
|
||||
.iter()
|
||||
.map(|grammar| grammar.highlight_map())
|
||||
.collect();
|
||||
|
||||
BufferChunks::new(
|
||||
self.applied_edits_snapshot.as_rope(),
|
||||
range,
|
||||
Some((captures, highlight_maps)),
|
||||
false,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
/// Offset range in `current_snapshot` that contains all edits, expanded to start at the
|
||||
/// beginning of the first line and end at the end of the last line.
|
||||
fn compute_visible_range(
|
||||
&self,
|
||||
edits: &[(Range<Anchor>, String)],
|
||||
edits: &[TextEditWithNewHighlights<Anchor>],
|
||||
snapshot: &BufferSnapshot,
|
||||
) -> Option<Range<usize>> {
|
||||
let (first, _) = edits.first()?;
|
||||
let (last, _) = edits.last()?;
|
||||
let first_edit = edits.first()?;
|
||||
let last_edit = edits.last()?;
|
||||
|
||||
let start = first.start.to_point(snapshot);
|
||||
let end = last.end.to_point(snapshot);
|
||||
let start = first_edit.old_range().start.to_point(snapshot);
|
||||
let end = last_edit.old_range().end.to_point(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, snapshot.line_len(end.row));
|
||||
@@ -996,30 +1119,66 @@ impl Buffer {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn preview_edits(
|
||||
pub fn highlight_edit_insertions(
|
||||
&self,
|
||||
edits: Arc<[(Range<Anchor>, String)]>,
|
||||
cx: &App,
|
||||
) -> Task<EditPreview> {
|
||||
edits: Vec<PlainTextEdit<Anchor>>,
|
||||
background_executor: &BackgroundExecutor,
|
||||
) -> Task<Vec<TextEditWithNewHighlights<Anchor>>> {
|
||||
let registry = self.language_registry();
|
||||
let language = self.language().cloned();
|
||||
|
||||
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);
|
||||
background_executor.spawn(async move {
|
||||
if edits.is_empty() {
|
||||
return vec![];
|
||||
}
|
||||
branch_buffer.edit(edits.iter().map(|edit| edit.to_tuple()));
|
||||
let snapshot = branch_buffer.snapshot();
|
||||
syntax_snapshot.interpolate(&snapshot);
|
||||
if let Some(language) = language {
|
||||
syntax_snapshot.reparse(&snapshot, registry, language);
|
||||
}
|
||||
|
||||
if let Some(language) = language {
|
||||
syntax_snapshot.reparse(&snapshot, registry, language);
|
||||
}
|
||||
}
|
||||
EditPreview {
|
||||
applied_edits_snapshot: branch_buffer.snapshot(),
|
||||
syntax_snapshot,
|
||||
}
|
||||
edits
|
||||
.iter()
|
||||
.map(|text_edit| {
|
||||
let range = text_edit.old_range().to_offset(&snapshot);
|
||||
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();
|
||||
|
||||
let chunks = BufferChunks::new(
|
||||
snapshot.as_rope(),
|
||||
range,
|
||||
Some((captures, highlight_maps)),
|
||||
false,
|
||||
None,
|
||||
);
|
||||
|
||||
let mut new_highlights = Vec::new();
|
||||
let mut offset = 0;
|
||||
for chunk in chunks {
|
||||
let start = offset;
|
||||
let end = start + chunk.text.len();
|
||||
offset = end;
|
||||
if let Some(highlight_id) = chunk.syntax_highlight_id {
|
||||
new_highlights.push((start..end, highlight_id));
|
||||
}
|
||||
}
|
||||
|
||||
TextEditWithNewHighlights {
|
||||
text_edit: text_edit.clone(),
|
||||
new_highlights,
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -2674,20 +2674,18 @@ async fn test_preview_edits(cx: &mut TestAppContext) {
|
||||
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(),
|
||||
)
|
||||
.map(|(range, text)| PlainTextEdit {
|
||||
old_range: buffer.anchor_before(range.start)..buffer.anchor_after(range.end),
|
||||
new_text: text.to_string(),
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
let edit_preview = buffer
|
||||
let edits = buffer
|
||||
.read_with(cx, |buffer, cx| {
|
||||
buffer.preview_edits(edits.clone().into(), cx)
|
||||
buffer.highlight_edit_insertions(edits, cx.background_executor())
|
||||
})
|
||||
.await;
|
||||
cx.read(|cx| edit_preview.highlight_edits(&buffer.read(cx).snapshot(), &edits, true, cx))
|
||||
cx.read(|cx| HighlightedEdits::highlight(&buffer.read(cx).snapshot(), &edits, true, cx))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2712,12 +2710,20 @@ async fn test_preview_edits_interpolate(cx: &mut TestAppContext) {
|
||||
let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
|
||||
|
||||
let edits = construct_edits(&buffer, [(Point::new(1, 4)..Point::new(1, 4), "first")], cx);
|
||||
let edit_preview = buffer
|
||||
.read_with(cx, |buffer, cx| buffer.preview_edits(edits.clone(), cx))
|
||||
let edits_with_new_highlights = buffer
|
||||
.read_with(cx, |buffer, cx| {
|
||||
buffer.highlight_edit_insertions(edits, cx.background_executor())
|
||||
})
|
||||
.await;
|
||||
|
||||
let highlighted_edits =
|
||||
cx.read(|cx| edit_preview.highlight_edits(&buffer.read(cx).snapshot(), &edits, false, cx));
|
||||
let highlighted_edits = cx.read(|cx| {
|
||||
HighlightedEdits::highlight(
|
||||
&buffer.read(cx).snapshot(),
|
||||
&edits_with_new_highlights,
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let created_background = cx.read(|cx| cx.theme().status().created_background);
|
||||
|
||||
@@ -2732,13 +2738,24 @@ async fn test_preview_edits_interpolate(cx: &mut TestAppContext) {
|
||||
let edits = construct_edits(&buffer, [(Point::new(1, 4)..Point::new(1, 4), "f")], cx);
|
||||
cx.update(|cx| {
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit(edits.iter().cloned(), None, cx);
|
||||
buffer.edit(edits.iter().map(|edit| edit.to_tuple()), None, cx);
|
||||
})
|
||||
});
|
||||
|
||||
let edits = construct_edits(&buffer, [(Point::new(1, 5)..Point::new(1, 5), "irst")], cx);
|
||||
let highlighted_edits =
|
||||
cx.read(|cx| edit_preview.highlight_edits(&buffer.read(cx).snapshot(), &edits, false, cx));
|
||||
let edits_with_new_highlights = buffer
|
||||
.read_with(cx, |buffer, cx| {
|
||||
buffer.highlight_edit_insertions(edits.clone(), cx.background_executor())
|
||||
})
|
||||
.await;
|
||||
let highlighted_edits = cx.read(|cx| {
|
||||
HighlightedEdits::highlight(
|
||||
&buffer.read(cx).snapshot(),
|
||||
&edits_with_new_highlights,
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
assert_eq!(highlighted_edits.text, " first_name: String");
|
||||
assert_eq!(highlighted_edits.highlights.len(), 1);
|
||||
@@ -2752,20 +2769,16 @@ async fn test_preview_edits_interpolate(cx: &mut TestAppContext) {
|
||||
buffer: &Entity<Buffer>,
|
||||
edits: impl IntoIterator<Item = (Range<Point>, &'static str)>,
|
||||
cx: &mut TestAppContext,
|
||||
) -> Arc<[(Range<Anchor>, String)]> {
|
||||
buffer
|
||||
.read_with(cx, |buffer, _| {
|
||||
edits
|
||||
.into_iter()
|
||||
.map(|(range, text)| {
|
||||
(
|
||||
buffer.anchor_after(range.start)..buffer.anchor_before(range.end),
|
||||
text.to_string(),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.into()
|
||||
) -> Vec<PlainTextEdit<Anchor>> {
|
||||
buffer.read_with(cx, |buffer, _| {
|
||||
edits
|
||||
.into_iter()
|
||||
.map(|(range, text)| PlainTextEdit {
|
||||
old_range: buffer.anchor_after(range.start)..buffer.anchor_before(range.end),
|
||||
new_text: text.to_string(),
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,10 @@ use anyhow::Result;
|
||||
use futures::StreamExt as _;
|
||||
use gpui::{App, Context, Entity, EntityId, Task};
|
||||
use inline_completion::{Direction, InlineCompletion, InlineCompletionProvider};
|
||||
use language::{language_settings::all_language_settings, Anchor, Buffer, BufferSnapshot};
|
||||
use language::{
|
||||
language_settings::all_language_settings, Anchor, Buffer, BufferSnapshot, PlainTextEdit,
|
||||
TextEditWithNewHighlights,
|
||||
};
|
||||
use std::{
|
||||
ops::{AddAssign, Range},
|
||||
path::Path,
|
||||
@@ -48,7 +51,7 @@ fn completion_from_diff(
|
||||
.text_for_range(delete_range.clone())
|
||||
.collect::<String>();
|
||||
|
||||
let mut edits: Vec<(Range<language::Anchor>, String)> = Vec::new();
|
||||
let mut edits: Vec<TextEditWithNewHighlights<Anchor>> = Vec::new();
|
||||
|
||||
let completion_graphemes: Vec<&str> = completion_text.graphemes(true).collect();
|
||||
let buffer_graphemes: Vec<&str> = buffer_text.graphemes(true).collect();
|
||||
@@ -67,8 +70,11 @@ fn completion_from_diff(
|
||||
if k != 0 {
|
||||
let offset = snapshot.anchor_after(offset);
|
||||
// the range from the current position to item is an inlay.
|
||||
let edit = (offset..offset, completion_graphemes[i..i + k].join(""));
|
||||
edits.push(edit);
|
||||
let edit = PlainTextEdit {
|
||||
old_range: offset..offset,
|
||||
new_text: completion_graphemes[i..i + k].join(""),
|
||||
};
|
||||
edits.push(edit.into());
|
||||
}
|
||||
i += k + 1;
|
||||
j += 1;
|
||||
@@ -85,15 +91,16 @@ fn completion_from_diff(
|
||||
if j == buffer_graphemes.len() && i < completion_graphemes.len() {
|
||||
let offset = snapshot.anchor_after(offset);
|
||||
// there is leftover completion text, so drop it as an inlay.
|
||||
let edit_range = offset..offset;
|
||||
let edit_text = completion_graphemes[i..].join("");
|
||||
edits.push((edit_range, edit_text));
|
||||
edits.push(
|
||||
PlainTextEdit {
|
||||
old_range: offset..offset,
|
||||
new_text: completion_graphemes[i..].join(""),
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
InlineCompletion {
|
||||
edits,
|
||||
edit_preview: None,
|
||||
}
|
||||
InlineCompletion { edits }
|
||||
}
|
||||
|
||||
impl InlineCompletionProvider for SupermavenCompletionProvider {
|
||||
|
||||
@@ -5,7 +5,7 @@ use gpui::{
|
||||
point, prelude::*, quad, size, AnyElement, App, Bounds, Corners, Edges, HighlightStyle, Hsla,
|
||||
StyledText, TextLayout, TextStyle,
|
||||
};
|
||||
use language::OffsetRangeExt;
|
||||
use language::{OffsetRangeExt, TextEdit};
|
||||
use settings::Settings;
|
||||
use theme::ThemeSettings;
|
||||
use ui::prelude::*;
|
||||
@@ -26,8 +26,9 @@ impl CompletionDiffElement {
|
||||
let mut cursor_offset_in_diff = None;
|
||||
let mut delta = 0;
|
||||
let mut diff_highlights = Vec::new();
|
||||
for (old_range, new_text) in completion.edits.iter() {
|
||||
let old_range = old_range.to_offset(&completion.snapshot);
|
||||
for edit in completion.edits.iter() {
|
||||
let old_range = edit.old_range().to_offset(&completion.snapshot);
|
||||
let new_text = &edit.new_text();
|
||||
|
||||
if cursor_offset_in_diff.is_none() && completion.cursor_offset <= old_range.end {
|
||||
cursor_offset_in_diff =
|
||||
@@ -51,7 +52,7 @@ impl CompletionDiffElement {
|
||||
}
|
||||
|
||||
if !new_text.is_empty() {
|
||||
diff.insert_str(old_end_in_diff, new_text);
|
||||
diff.insert_str(old_end_in_diff, new_text.as_str());
|
||||
diff_highlights.push((
|
||||
old_end_in_diff..old_end_in_diff + new_text.len(),
|
||||
HighlightStyle {
|
||||
|
||||
@@ -14,8 +14,8 @@ use gpui::{
|
||||
};
|
||||
use http_client::{HttpClient, Method};
|
||||
use language::{
|
||||
language_settings::all_language_settings, Anchor, Buffer, BufferSnapshot, EditPreview,
|
||||
OffsetRangeExt, Point, ToOffset, ToPoint,
|
||||
language_settings::all_language_settings, Anchor, Buffer, BufferSnapshot, OffsetRangeExt,
|
||||
PlainTextEdit, Point, TextEdit, TextEditWithNewHighlights, ToOffset, ToPoint,
|
||||
};
|
||||
use language_models::LlmApiToken;
|
||||
use rpc::{PredictEditsParams, PredictEditsResponse, EXPIRED_LLM_TOKEN_HEADER_NAME};
|
||||
@@ -74,9 +74,8 @@ pub struct InlineCompletion {
|
||||
path: Arc<Path>,
|
||||
excerpt_range: Range<usize>,
|
||||
cursor_offset: usize,
|
||||
edits: Arc<[(Range<Anchor>, String)]>,
|
||||
edits: Arc<[TextEditWithNewHighlights<Anchor>]>,
|
||||
snapshot: BufferSnapshot,
|
||||
edit_preview: EditPreview,
|
||||
input_outline: Arc<str>,
|
||||
input_events: Arc<str>,
|
||||
input_excerpt: Arc<str>,
|
||||
@@ -91,22 +90,30 @@ impl InlineCompletion {
|
||||
.duration_since(self.request_sent_at)
|
||||
}
|
||||
|
||||
fn interpolate(&self, new_snapshot: &BufferSnapshot) -> Option<Vec<(Range<Anchor>, String)>> {
|
||||
interpolate(&self.snapshot, new_snapshot, self.edits.clone())
|
||||
fn interpolate(
|
||||
&self,
|
||||
new_snapshot: &BufferSnapshot,
|
||||
) -> Option<Vec<TextEditWithNewHighlights<Anchor>>> {
|
||||
interpolate(&self.snapshot, new_snapshot, &self.edits)
|
||||
}
|
||||
}
|
||||
|
||||
fn interpolate(
|
||||
fn interpolate<E>(
|
||||
old_snapshot: &BufferSnapshot,
|
||||
new_snapshot: &BufferSnapshot,
|
||||
current_edits: Arc<[(Range<Anchor>, String)]>,
|
||||
) -> Option<Vec<(Range<Anchor>, String)>> {
|
||||
current_edits: &[E],
|
||||
) -> Option<Vec<E>>
|
||||
where
|
||||
E: Clone + TextEdit<Anchor>,
|
||||
{
|
||||
let mut edits = Vec::new();
|
||||
|
||||
let mut user_edits = new_snapshot
|
||||
.edits_since::<usize>(&old_snapshot.version)
|
||||
.peekable();
|
||||
for (model_old_range, model_new_text) in current_edits.iter() {
|
||||
for model_edit in current_edits.iter() {
|
||||
let model_old_range = model_edit.old_range();
|
||||
let model_new_text = model_edit.new_text();
|
||||
let model_offset_range = model_old_range.to_offset(old_snapshot);
|
||||
while let Some(next_user_edit) = user_edits.peek() {
|
||||
if next_user_edit.old.end < model_offset_range.start {
|
||||
@@ -118,19 +125,19 @@ fn interpolate(
|
||||
|
||||
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()));
|
||||
edits.push(model_edit.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(),
|
||||
));
|
||||
if model_new_text.starts_with(&user_new_text) {
|
||||
if user_new_text.len() < model_new_text.len() {
|
||||
let mut modified_edit = model_edit.with_prefix_dropped(user_new_text.len());
|
||||
*modified_edit.mut_old_range() = new_snapshot
|
||||
.anchor_after(user_edit.new.end)
|
||||
..new_snapshot.anchor_before(user_edit.new.end);
|
||||
edits.push(modified_edit);
|
||||
}
|
||||
|
||||
user_edits.next();
|
||||
@@ -141,7 +148,7 @@ fn interpolate(
|
||||
return None;
|
||||
}
|
||||
} else {
|
||||
edits.push((model_old_range.clone(), model_new_text.clone()));
|
||||
edits.push(model_edit.clone());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -606,7 +613,7 @@ and then another
|
||||
cx.spawn(|cx| async move {
|
||||
let output_excerpt: Arc<str> = output_excerpt.into();
|
||||
|
||||
let edits: Arc<[(Range<Anchor>, String)]> = cx
|
||||
let edits: Vec<PlainTextEdit<Anchor>> = cx
|
||||
.background_executor()
|
||||
.spawn({
|
||||
let output_excerpt = output_excerpt.clone();
|
||||
@@ -614,23 +621,23 @@ and then another
|
||||
let snapshot = snapshot.clone();
|
||||
async move { Self::parse_edits(output_excerpt, excerpt_range, &snapshot) }
|
||||
})
|
||||
.await?
|
||||
.into();
|
||||
.await?;
|
||||
|
||||
let (edits, snapshot, edit_preview) = buffer.read_with(&cx, {
|
||||
let edits = edits.clone();
|
||||
|buffer, cx| {
|
||||
let (edits, snapshot) = buffer.read_with(&cx, {
|
||||
move |buffer, cx| {
|
||||
let new_snapshot = buffer.snapshot();
|
||||
let edits: Arc<[(Range<Anchor>, String)]> =
|
||||
interpolate(&snapshot, &new_snapshot, edits)
|
||||
.context("Interpolated edits are empty")?
|
||||
.into();
|
||||
let edits: Vec<PlainTextEdit<Anchor>> =
|
||||
interpolate(&snapshot, &new_snapshot, &edits)
|
||||
.context("Interpolated edits are empty")?;
|
||||
|
||||
anyhow::Ok((edits.clone(), new_snapshot, buffer.preview_edits(edits, cx)))
|
||||
anyhow::Ok((
|
||||
buffer.highlight_edit_insertions(edits, cx.background_executor()),
|
||||
new_snapshot,
|
||||
))
|
||||
}
|
||||
})??;
|
||||
|
||||
let edit_preview = edit_preview.await;
|
||||
let edits = edits.await.into();
|
||||
|
||||
Ok(InlineCompletion {
|
||||
id: InlineCompletionId::new(),
|
||||
@@ -638,7 +645,6 @@ and then another
|
||||
excerpt_range,
|
||||
cursor_offset,
|
||||
edits,
|
||||
edit_preview,
|
||||
snapshot,
|
||||
input_outline: input_outline.into(),
|
||||
input_events: input_events.into(),
|
||||
@@ -654,7 +660,7 @@ and then another
|
||||
output_excerpt: Arc<str>,
|
||||
excerpt_range: Range<usize>,
|
||||
snapshot: &BufferSnapshot,
|
||||
) -> Result<Vec<(Range<Anchor>, String)>> {
|
||||
) -> Result<Vec<PlainTextEdit<Anchor>>> {
|
||||
let content = output_excerpt.replace(CURSOR_MARKER, "");
|
||||
|
||||
let start_markers = content
|
||||
@@ -712,7 +718,7 @@ and then another
|
||||
new_text: &str,
|
||||
offset: usize,
|
||||
snapshot: &BufferSnapshot,
|
||||
) -> Vec<(Range<Anchor>, String)> {
|
||||
) -> Vec<PlainTextEdit<Anchor>> {
|
||||
let diff = similar::TextDiff::from_words(old_text.as_str(), new_text);
|
||||
|
||||
let mut edits: Vec<(Range<usize>, String)> = Vec::new();
|
||||
@@ -765,10 +771,11 @@ 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),
|
||||
PlainTextEdit {
|
||||
old_range: snapshot.anchor_after(old_range.start)
|
||||
..snapshot.anchor_before(old_range.end),
|
||||
new_text,
|
||||
)
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -1024,9 +1031,9 @@ impl CurrentInlineCompletion {
|
||||
};
|
||||
|
||||
if old_edits.len() == 1 && new_edits.len() == 1 {
|
||||
let (old_range, old_text) = &old_edits[0];
|
||||
let (new_range, new_text) = &new_edits[0];
|
||||
new_range == old_range && new_text.starts_with(old_text)
|
||||
let old = &old_edits[0];
|
||||
let new = &new_edits[0];
|
||||
new.old_range() == old.old_range() && new.new_text().starts_with(old.new_text())
|
||||
} else {
|
||||
true
|
||||
}
|
||||
@@ -1226,17 +1233,20 @@ impl inline_completion::InlineCompletionProvider for ZetaInlineCompletionProvide
|
||||
};
|
||||
|
||||
let cursor_row = cursor_position.to_point(buffer).row;
|
||||
let (closest_edit_ix, (closest_edit_range, _)) =
|
||||
edits.iter().enumerate().min_by_key(|(_, (range, _))| {
|
||||
let distance_from_start = cursor_row.abs_diff(range.start.to_point(buffer).row);
|
||||
let distance_from_end = cursor_row.abs_diff(range.end.to_point(buffer).row);
|
||||
let (closest_edit_ix, closest_edit) =
|
||||
edits.iter().enumerate().min_by_key(|(_, edit)| {
|
||||
let distance_from_start =
|
||||
cursor_row.abs_diff(edit.old_range().start.to_point(buffer).row);
|
||||
let distance_from_end =
|
||||
cursor_row.abs_diff(edit.old_range().end.to_point(buffer).row);
|
||||
cmp::min(distance_from_start, distance_from_end)
|
||||
})?;
|
||||
let closest_edit_range = &closest_edit.old_range();
|
||||
|
||||
let mut edit_start_ix = closest_edit_ix;
|
||||
for (range, _) in edits[..edit_start_ix].iter().rev() {
|
||||
let distance_from_closest_edit =
|
||||
closest_edit_range.start.to_point(buffer).row - range.end.to_point(buffer).row;
|
||||
for edit in edits[..edit_start_ix].iter().rev() {
|
||||
let distance_from_closest_edit = closest_edit_range.start.to_point(buffer).row
|
||||
- edit.old_range().end.to_point(buffer).row;
|
||||
if distance_from_closest_edit <= 1 {
|
||||
edit_start_ix -= 1;
|
||||
} else {
|
||||
@@ -1245,9 +1255,9 @@ impl inline_completion::InlineCompletionProvider for ZetaInlineCompletionProvide
|
||||
}
|
||||
|
||||
let mut edit_end_ix = closest_edit_ix + 1;
|
||||
for (range, _) in &edits[edit_end_ix..] {
|
||||
let distance_from_closest_edit =
|
||||
range.start.to_point(buffer).row - closest_edit_range.end.to_point(buffer).row;
|
||||
for edit in &edits[edit_end_ix..] {
|
||||
let distance_from_closest_edit = edit.old_range().start.to_point(buffer).row
|
||||
- closest_edit_range.end.to_point(buffer).row;
|
||||
if distance_from_closest_edit <= 1 {
|
||||
edit_end_ix += 1;
|
||||
} else {
|
||||
@@ -1257,7 +1267,6 @@ 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()),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1278,22 +1287,25 @@ mod tests {
|
||||
#[gpui::test]
|
||||
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| {
|
||||
let edits: Vec<PlainTextEdit<Anchor>> = 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 edits = cx
|
||||
.read(|cx| {
|
||||
buffer
|
||||
.read(cx)
|
||||
.highlight_edit_insertions(edits.clone(), cx.background_executor())
|
||||
})
|
||||
.await
|
||||
.into();
|
||||
|
||||
let completion = InlineCompletion {
|
||||
edits,
|
||||
edit_preview,
|
||||
path: Path::new("").into(),
|
||||
snapshot: cx.read(|cx| buffer.read(cx).snapshot()),
|
||||
id: InlineCompletionId::new(),
|
||||
@@ -1444,7 +1456,11 @@ mod tests {
|
||||
|
||||
let completion = completion_task.await.unwrap();
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit(completion.edits.iter().cloned(), None, cx)
|
||||
buffer.edit(
|
||||
completion.edits.iter().map(|edit| edit.to_tuple()),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
assert_eq!(
|
||||
buffer.read_with(cx, |buffer, _| buffer.text()),
|
||||
@@ -1456,32 +1472,29 @@ mod tests {
|
||||
iterator: impl IntoIterator<Item = (Range<usize>, String)>,
|
||||
buffer: &Entity<Buffer>,
|
||||
cx: &App,
|
||||
) -> Vec<(Range<Anchor>, String)> {
|
||||
) -> Vec<PlainTextEdit<Anchor>> {
|
||||
let buffer = buffer.read(cx);
|
||||
iterator
|
||||
.into_iter()
|
||||
.map(|(range, text)| {
|
||||
(
|
||||
buffer.anchor_after(range.start)..buffer.anchor_before(range.end),
|
||||
text,
|
||||
)
|
||||
.map(|(range, text)| PlainTextEdit {
|
||||
old_range: buffer.anchor_after(range.start)..buffer.anchor_before(range.end),
|
||||
new_text: text,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn from_completion_edits(
|
||||
editor_edits: &[(Range<Anchor>, String)],
|
||||
editor_edits: &[TextEditWithNewHighlights<Anchor>],
|
||||
buffer: &Entity<Buffer>,
|
||||
cx: &App,
|
||||
) -> Vec<(Range<usize>, String)> {
|
||||
let buffer = buffer.read(cx);
|
||||
editor_edits
|
||||
.iter()
|
||||
.map(|(range, text)| {
|
||||
(
|
||||
range.start.to_offset(buffer)..range.end.to_offset(buffer),
|
||||
text.clone(),
|
||||
)
|
||||
.map(|edit| {
|
||||
edit.clone()
|
||||
.map_position(|position| position.to_offset(buffer))
|
||||
.to_tuple()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user