inline completion: Add syntax highlighting for edit prediction (#23361)

Closes #ISSUE

Release Notes:

- N/A

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Agus <agus@zed.dev>
This commit is contained in:
Bennet Bo Fenner 2025-01-23 18:32:43 +01:00 committed by GitHub
parent 75ae4dada4
commit 3dee32c43d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 796 additions and 485 deletions

View file

@ -26,7 +26,7 @@ use fs::MTime;
use futures::channel::oneshot;
use gpui::{
AnyElement, AppContext, Context as _, EventEmitter, HighlightStyle, Model, ModelContext,
Pixels, Task, TaskLabel, WindowContext,
Pixels, SharedString, Task, TaskLabel, WindowContext,
};
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};
@ -603,6 +603,162 @@ impl IndentGuide {
}
}
#[derive(Clone)]
pub struct EditPreview {
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: &AppContext,
) -> HighlightedEdits {
let mut text = String::new();
let mut highlights = Vec::new();
let Some(range) = self.compute_visible_range(edits, current_snapshot) else {
return HighlightedEdits::default();
};
let mut offset = range.start;
let mut delta = 0isize;
let status_colors = cx.theme().status();
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();
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 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,
&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;
}
self.highlight_text(
offset..(range.end as isize + delta) as usize,
&mut text,
&mut highlights,
None,
cx,
);
HighlightedEdits {
text: text.into(),
highlights,
}
}
fn highlight_text(
&self,
range: Range<usize>,
text: &mut String,
highlights: &mut Vec<(Range<usize>, HighlightStyle)>,
override_style: Option<HighlightStyle>,
cx: &AppContext,
) {
for chunk in self.highlighted_chunks(range) {
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(&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,
)
}
fn compute_visible_range(
&self,
edits: &[(Range<Anchor>, String)],
snapshot: &BufferSnapshot,
) -> Option<Range<usize>> {
let (first, _) = edits.first()?;
let (last, _) = edits.last()?;
let start = first.start.to_point(snapshot);
let end = last.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));
Some(range.to_offset(&snapshot))
}
}
impl Buffer {
/// Create a new buffer with the given base text.
pub fn local<T: Into<String>>(base_text: T, cx: &ModelContext<Self>) -> Self {
@ -825,6 +981,33 @@ impl Buffer {
})
}
pub fn preview_edits(
&self,
edits: Arc<[(Range<Anchor>, String)]>,
cx: &AppContext,
) -> Task<EditPreview> {
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);
if let Some(language) = language {
syntax_snapshot.reparse(&snapshot, registry, language);
}
}
EditPreview {
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.
///

View file

@ -2718,6 +2718,148 @@ 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 text = indoc! {r#"
struct Person {
first_name: String,
}
impl Person {
fn last_name(&self) -> &String {
&self.last_name
}
}"#
};
let language = Arc::new(Language::new(
LanguageConfig::default(),
Some(tree_sitter_rust::LANGUAGE.into()),
));
let buffer = cx.new_model(|cx| Buffer::local(text, cx).with_language(language, cx));
let highlighted_edits = preview_edits(
&buffer,
cx,
[
(Point::new(5, 7)..Point::new(5, 11), "first"),
(Point::new(6, 14)..Point::new(6, 18), "first"),
],
)
.await;
assert_eq!(
highlighted_edits.text,
" fn lastfirst_name(&self) -> &String {\n &self.lastfirst_name"
);
async fn preview_edits(
buffer: &Model<Buffer>,
cx: &mut TestAppContext,
edits: impl IntoIterator<Item = (Range<Point>, &'static str)>,
) -> HighlightedEdits {
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;
cx.read(|cx| edit_preview.highlight_edits(&buffer.read(cx).snapshot(), &edits, true, cx))
}
}
#[gpui::test]
async fn test_preview_edits_interpolate(cx: &mut TestAppContext) {
use theme::ActiveTheme;
cx.update(|cx| {
init_settings(cx, |_| {});
theme::init(theme::LoadThemes::JustBase, cx);
});
let text = indoc! {r#"
struct Person {
_name: String
}"#
};
let language = Arc::new(Language::new(
LanguageConfig::default(),
Some(tree_sitter_rust::LANGUAGE.into()),
));
let buffer = cx.new_model(|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))
.await;
let highlighted_edits =
cx.read(|cx| edit_preview.highlight_edits(&buffer.read(cx).snapshot(), &edits, false, cx));
let created_background = cx.read(|cx| cx.theme().status().created_background);
assert_eq!(highlighted_edits.text, " first_name: String");
assert_eq!(highlighted_edits.highlights.len(), 1);
assert_eq!(highlighted_edits.highlights[0].0, 4..9);
assert_eq!(
highlighted_edits.highlights[0].1.background_color,
Some(created_background)
);
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);
})
});
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));
assert_eq!(highlighted_edits.text, " first_name: String");
assert_eq!(highlighted_edits.highlights.len(), 1);
assert_eq!(highlighted_edits.highlights[0].0, (5..9));
assert_eq!(
highlighted_edits.highlights[0].1.background_color,
Some(created_background)
);
fn construct_edits(
buffer: &Model<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()
}
}
#[gpui::test(iterations = 100)]
fn test_random_collaboration(cx: &mut AppContext, mut rng: StdRng) {
let min_peers = env::var("MIN_PEERS")

View file

@ -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<_>>();