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
|
@ -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<_>>();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue