diff --git a/Cargo.lock b/Cargo.lock index 3d70d53b05..64f13c6540 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1622,6 +1622,8 @@ dependencies = [ "anyhow", "buffer", "clock", + "ctor", + "env_logger", "gpui", "language", "lazy_static", diff --git a/crates/buffer/src/anchor.rs b/crates/buffer/src/anchor.rs index bb0e7b386a..4fdd387be0 100644 --- a/crates/buffer/src/anchor.rs +++ b/crates/buffer/src/anchor.rs @@ -194,6 +194,66 @@ impl AnchorRangeMap { .iter() .map(|(range, value)| (range.start.0..range.end.0, value)) } + + pub fn min_by_key<'a, C, D, F, K>( + &self, + content: C, + mut extract_key: F, + ) -> Option<(Range, &T)> + where + C: Into>, + D: 'a + TextDimension<'a>, + F: FnMut(&T) -> K, + K: Ord, + { + let content = content.into(); + self.entries + .iter() + .min_by_key(|(_, value)| extract_key(value)) + .map(|(range, value)| (self.resolve_range(range, &content), value)) + } + + pub fn max_by_key<'a, C, D, F, K>( + &self, + content: C, + mut extract_key: F, + ) -> Option<(Range, &T)> + where + C: Into>, + D: 'a + TextDimension<'a>, + F: FnMut(&T) -> K, + K: Ord, + { + let content = content.into(); + self.entries + .iter() + .max_by_key(|(_, value)| extract_key(value)) + .map(|(range, value)| (self.resolve_range(range, &content), value)) + } + + fn resolve_range<'a, D>( + &self, + range: &Range<(FullOffset, Bias)>, + content: &Content<'a>, + ) -> Range + where + D: 'a + TextDimension<'a>, + { + let (start, start_bias) = range.start; + let mut anchor = Anchor { + full_offset: start, + bias: start_bias, + version: self.version.clone(), + }; + let start = content.summary_for_anchor(&anchor); + + let (end, end_bias) = range.end; + anchor.full_offset = end; + anchor.bias = end_bias; + let end = content.summary_for_anchor(&anchor); + + start..end + } } impl PartialEq for AnchorRangeMap { @@ -354,6 +414,38 @@ impl AnchorRangeMultimap { .cursor::<()>() .map(|entry| (entry.range.start..entry.range.end, &entry.value)) } + + pub fn filter<'a, O, F>( + &'a self, + content: Content<'a>, + mut f: F, + ) -> impl 'a + Iterator, &T)> + where + O: FromAnchor, + F: 'a + FnMut(&'a T) -> bool, + { + let mut endpoint = Anchor { + full_offset: FullOffset(0), + bias: Bias::Left, + version: self.version.clone(), + }; + self.entries + .cursor::<()>() + .enumerate() + .filter_map(move |(ix, entry)| { + if f(&entry.value) { + endpoint.full_offset = entry.range.start; + endpoint.bias = self.start_bias; + let start = O::from_anchor(&endpoint, &content); + endpoint.full_offset = entry.range.end; + endpoint.bias = self.end_bias; + let end = O::from_anchor(&endpoint, &content); + Some((ix, start..end, &entry.value)) + } else { + None + } + }) + } } impl sum_tree::Item for AnchorRangeMultimapEntry { @@ -435,6 +527,7 @@ impl<'a> sum_tree::SeekTarget<'a, AnchorRangeMultimapSummary, FullOffsetRange> f pub trait AnchorRangeExt { fn cmp<'a>(&self, b: &Range, buffer: impl Into>) -> Result; + fn to_offset<'a>(&self, content: impl Into>) -> Range; } impl AnchorRangeExt for Range { @@ -445,4 +538,9 @@ impl AnchorRangeExt for Range { ord @ _ => ord, }) } + + fn to_offset<'a>(&self, content: impl Into>) -> Range { + let content = content.into(); + self.start.to_offset(&content)..self.end.to_offset(&content) + } } diff --git a/crates/buffer/src/point.rs b/crates/buffer/src/point.rs index 5e62176956..93cc2c6007 100644 --- a/crates/buffer/src/point.rs +++ b/crates/buffer/src/point.rs @@ -23,6 +23,15 @@ impl Point { Point::new(0, 0) } + pub fn from_str(s: &str) -> Self { + let mut point = Self::zero(); + for (row, line) in s.split('\n').enumerate() { + point.row = row as u32; + point.column = line.len() as u32; + } + point + } + pub fn is_zero(&self) -> bool { self.row == 0 && self.column == 0 } diff --git a/crates/buffer/src/rope.rs b/crates/buffer/src/rope.rs index 3cf43bd160..71a906fe5d 100644 --- a/crates/buffer/src/rope.rs +++ b/crates/buffer/src/rope.rs @@ -3,7 +3,7 @@ use crate::PointUtf16; use super::Point; use arrayvec::ArrayString; use smallvec::SmallVec; -use std::{cmp, ops::Range, str}; +use std::{cmp, fmt, mem, ops::Range, str}; use sum_tree::{Bias, Dimension, SumTree}; #[cfg(test)] @@ -38,6 +38,16 @@ impl Rope { self.check_invariants(); } + pub fn replace(&mut self, range: Range, text: &str) { + let mut new_rope = Rope::new(); + let mut cursor = self.cursor(0); + new_rope.append(cursor.slice(range.start)); + cursor.seek_forward(range.end); + new_rope.push(text); + new_rope.append(cursor.suffix()); + *self = new_rope; + } + pub fn push(&mut self, text: &str) { let mut new_chunks = SmallVec::<[_; 16]>::new(); let mut new_chunk = ArrayString::new(); @@ -79,6 +89,11 @@ impl Rope { self.check_invariants(); } + pub fn push_front(&mut self, text: &str) { + let suffix = mem::replace(self, Rope::from(text)); + self.append(suffix); + } + fn check_invariants(&self) { #[cfg(test)] { @@ -139,7 +154,9 @@ impl Rope { } pub fn offset_to_point(&self, offset: usize) -> Point { - assert!(offset <= self.summary().bytes); + if offset >= self.summary().bytes { + return self.summary().lines; + } let mut cursor = self.chunks.cursor::<(usize, Point)>(); cursor.seek(&offset, Bias::Left, &()); let overshoot = offset - cursor.start().0; @@ -150,7 +167,9 @@ impl Rope { } pub fn offset_to_point_utf16(&self, offset: usize) -> PointUtf16 { - assert!(offset <= self.summary().bytes); + if offset >= self.summary().bytes { + return self.summary().lines_utf16; + } let mut cursor = self.chunks.cursor::<(usize, PointUtf16)>(); cursor.seek(&offset, Bias::Left, &()); let overshoot = offset - cursor.start().0; @@ -161,7 +180,9 @@ impl Rope { } pub fn point_to_offset(&self, point: Point) -> usize { - assert!(point <= self.summary().lines); + if point >= self.summary().lines { + return self.summary().bytes; + } let mut cursor = self.chunks.cursor::<(Point, usize)>(); cursor.seek(&point, Bias::Left, &()); let overshoot = point - cursor.start().0; @@ -172,7 +193,9 @@ impl Rope { } pub fn point_utf16_to_offset(&self, point: PointUtf16) -> usize { - assert!(point <= self.summary().lines_utf16); + if point >= self.summary().lines_utf16 { + return self.summary().bytes; + } let mut cursor = self.chunks.cursor::<(PointUtf16, usize)>(); cursor.seek(&point, Bias::Left, &()); let overshoot = point - cursor.start().0; @@ -226,6 +249,11 @@ impl Rope { self.summary().lines_utf16 } } + + pub fn line_len(&self, row: u32) -> u32 { + self.clip_point(Point::new(row, u32::MAX), Bias::Left) + .column + } } impl<'a> From<&'a str> for Rope { @@ -236,9 +264,12 @@ impl<'a> From<&'a str> for Rope { } } -impl Into for Rope { - fn into(self) -> String { - self.chunks().collect() +impl fmt::Display for Rope { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for chunk in self.chunks() { + write!(f, "{}", chunk)?; + } + Ok(()) } } @@ -303,7 +334,7 @@ impl<'a> Cursor<'a> { if let Some(start_chunk) = self.chunks.item() { let start_ix = self.offset - self.chunks.start(); let end_ix = cmp::min(end_offset, self.chunks.end(&())) - self.chunks.start(); - summary.add_assign(&D::from_summary(&TextSummary::from( + summary.add_assign(&D::from_text_summary(&TextSummary::from( &start_chunk.0[start_ix..end_ix], ))); } @@ -313,7 +344,9 @@ impl<'a> Cursor<'a> { summary.add_assign(&self.chunks.summary(&end_offset, Bias::Right, &())); if let Some(end_chunk) = self.chunks.item() { let end_ix = end_offset - self.chunks.start(); - summary.add_assign(&D::from_summary(&TextSummary::from(&end_chunk.0[..end_ix]))); + summary.add_assign(&D::from_text_summary(&TextSummary::from( + &end_chunk.0[..end_ix], + ))); } } @@ -634,13 +667,16 @@ impl std::ops::AddAssign for TextSummary { } pub trait TextDimension<'a>: Dimension<'a, TextSummary> { - fn from_summary(summary: &TextSummary) -> Self; + fn from_text_summary(summary: &TextSummary) -> Self; fn add_assign(&mut self, other: &Self); } impl<'a, D1: TextDimension<'a>, D2: TextDimension<'a>> TextDimension<'a> for (D1, D2) { - fn from_summary(summary: &TextSummary) -> Self { - (D1::from_summary(summary), D2::from_summary(summary)) + fn from_text_summary(summary: &TextSummary) -> Self { + ( + D1::from_text_summary(summary), + D2::from_text_summary(summary), + ) } fn add_assign(&mut self, other: &Self) { @@ -650,7 +686,7 @@ impl<'a, D1: TextDimension<'a>, D2: TextDimension<'a>> TextDimension<'a> for (D1 } impl<'a> TextDimension<'a> for TextSummary { - fn from_summary(summary: &TextSummary) -> Self { + fn from_text_summary(summary: &TextSummary) -> Self { summary.clone() } @@ -666,7 +702,7 @@ impl<'a> sum_tree::Dimension<'a, TextSummary> for usize { } impl<'a> TextDimension<'a> for usize { - fn from_summary(summary: &TextSummary) -> Self { + fn from_text_summary(summary: &TextSummary) -> Self { summary.bytes } @@ -682,7 +718,7 @@ impl<'a> sum_tree::Dimension<'a, TextSummary> for Point { } impl<'a> TextDimension<'a> for Point { - fn from_summary(summary: &TextSummary) -> Self { + fn from_text_summary(summary: &TextSummary) -> Self { summary.lines } @@ -698,7 +734,7 @@ impl<'a> sum_tree::Dimension<'a, TextSummary> for PointUtf16 { } impl<'a> TextDimension<'a> for PointUtf16 { - fn from_summary(summary: &TextSummary) -> Self { + fn from_text_summary(summary: &TextSummary) -> Self { summary.lines_utf16 } @@ -731,7 +767,7 @@ mod tests { use super::*; use crate::random_char_iter::RandomCharIter; use rand::prelude::*; - use std::env; + use std::{cmp::Ordering, env}; use Bias::{Left, Right}; #[test] @@ -778,7 +814,7 @@ mod tests { } #[gpui::test(iterations = 100)] - fn test_random(mut rng: StdRng) { + fn test_random_rope(mut rng: StdRng) { let operations = env::var("OPERATIONS") .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) .unwrap_or(10); @@ -862,6 +898,38 @@ mod tests { TextSummary::from(&expected[start_ix..end_ix]) ); } + + let mut expected_longest_rows = Vec::new(); + let mut longest_line_len = -1_isize; + for (row, line) in expected.split('\n').enumerate() { + let row = row as u32; + assert_eq!( + actual.line_len(row), + line.len() as u32, + "invalid line len for row {}", + row + ); + + let line_char_count = line.chars().count() as isize; + match line_char_count.cmp(&longest_line_len) { + Ordering::Less => {} + Ordering::Equal => expected_longest_rows.push(row), + Ordering::Greater => { + longest_line_len = line_char_count; + expected_longest_rows.clear(); + expected_longest_rows.push(row); + } + } + } + + let longest_row = actual.summary().longest_row; + assert!( + expected_longest_rows.contains(&longest_row), + "incorrect longest row {}. expected {:?} with length {}", + longest_row, + expected_longest_rows, + longest_line_len, + ); } } diff --git a/crates/buffer/src/selection.rs b/crates/buffer/src/selection.rs index dfd4522368..b90a6fa105 100644 --- a/crates/buffer/src/selection.rs +++ b/crates/buffer/src/selection.rs @@ -116,4 +116,36 @@ impl SelectionSet { goal: state.goal, }) } + + pub fn oldest_selection<'a, D, C>(&'a self, content: C) -> Option> + where + D: 'a + TextDimension<'a>, + C: 'a + Into>, + { + self.selections + .min_by_key(content, |selection| selection.id) + .map(|(range, state)| Selection { + id: state.id, + start: range.start, + end: range.end, + reversed: state.reversed, + goal: state.goal, + }) + } + + pub fn newest_selection<'a, D, C>(&'a self, content: C) -> Option> + where + D: 'a + TextDimension<'a>, + C: 'a + Into>, + { + self.selections + .max_by_key(content, |selection| selection.id) + .map(|(range, state)| Selection { + id: state.id, + start: range.start, + end: range.end, + reversed: state.reversed, + goal: state.goal, + }) + } } diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index 59ed90d460..7f5ffec97e 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -31,6 +31,8 @@ smol = "1.2" buffer = { path = "../buffer", features = ["test-support"] } language = { path = "../language", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } +ctor = "0.1" +env_logger = "0.8" rand = "0.8" unindent = "0.1.7" tree-sitter = "0.19" diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 596dc9507f..14fa4a9146 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -1,18 +1,29 @@ +mod block_map; mod fold_map; +mod patch; mod tab_map; mod wrap_map; +pub use block_map::{BlockDisposition, BlockId, BlockProperties, BufferRows, Chunks}; +use block_map::{BlockMap, BlockPoint}; +use buffer::Rope; use fold_map::{FoldMap, ToFoldPoint as _}; -use gpui::{fonts::FontId, Entity, ModelContext, ModelHandle}; +use gpui::{ + fonts::{FontId, HighlightStyle}, + AppContext, Entity, ModelContext, ModelHandle, +}; use language::{Anchor, Buffer, Point, ToOffset, ToPoint}; -use std::ops::Range; +use std::{ + collections::{HashMap, HashSet}, + ops::Range, +}; use sum_tree::Bias; use tab_map::TabMap; +use theme::{BlockStyle, SyntaxTheme}; use wrap_map::WrapMap; -pub use wrap_map::{BufferRows, HighlightedChunks}; pub trait ToDisplayPoint { - fn to_display_point(&self, map: &DisplayMapSnapshot, bias: Bias) -> DisplayPoint; + fn to_display_point(&self, map: &DisplayMapSnapshot) -> DisplayPoint; } pub struct DisplayMap { @@ -20,6 +31,7 @@ pub struct DisplayMap { fold_map: FoldMap, tab_map: TabMap, wrap_map: ModelHandle, + block_map: BlockMap, } impl Entity for DisplayMap { @@ -37,28 +49,32 @@ impl DisplayMap { ) -> Self { let (fold_map, snapshot) = FoldMap::new(buffer.clone(), cx); let (tab_map, snapshot) = TabMap::new(snapshot, tab_size); - let wrap_map = - cx.add_model(|cx| WrapMap::new(snapshot, font_id, font_size, wrap_width, cx)); + let (wrap_map, snapshot) = WrapMap::new(snapshot, font_id, font_size, wrap_width, cx); + let block_map = BlockMap::new(buffer.clone(), snapshot); cx.observe(&wrap_map, |_, _, cx| cx.notify()).detach(); DisplayMap { buffer, fold_map, tab_map, wrap_map, + block_map, } } pub fn snapshot(&self, cx: &mut ModelContext) -> DisplayMapSnapshot { let (folds_snapshot, edits) = self.fold_map.read(cx); let (tabs_snapshot, edits) = self.tab_map.sync(folds_snapshot.clone(), edits); - let wraps_snapshot = self + let (wraps_snapshot, edits) = self .wrap_map .update(cx, |map, cx| map.sync(tabs_snapshot.clone(), edits, cx)); + let blocks_snapshot = self.block_map.read(wraps_snapshot.clone(), edits, cx); + DisplayMapSnapshot { buffer_snapshot: self.buffer.read(cx).snapshot(), folds_snapshot, tabs_snapshot, wraps_snapshot, + blocks_snapshot, } } @@ -69,12 +85,16 @@ impl DisplayMap { ) { let (mut fold_map, snapshot, edits) = self.fold_map.write(cx); let (snapshot, edits) = self.tab_map.sync(snapshot, edits); - self.wrap_map + let (snapshot, edits) = self + .wrap_map .update(cx, |map, cx| map.sync(snapshot, edits, cx)); + self.block_map.read(snapshot, edits, cx); let (snapshot, edits) = fold_map.fold(ranges, cx); let (snapshot, edits) = self.tab_map.sync(snapshot, edits); - self.wrap_map + let (snapshot, edits) = self + .wrap_map .update(cx, |map, cx| map.sync(snapshot, edits, cx)); + self.block_map.read(snapshot, edits, cx); } pub fn unfold( @@ -84,12 +104,52 @@ impl DisplayMap { ) { let (mut fold_map, snapshot, edits) = self.fold_map.write(cx); let (snapshot, edits) = self.tab_map.sync(snapshot, edits); - self.wrap_map + let (snapshot, edits) = self + .wrap_map .update(cx, |map, cx| map.sync(snapshot, edits, cx)); + self.block_map.read(snapshot, edits, cx); let (snapshot, edits) = fold_map.unfold(ranges, cx); let (snapshot, edits) = self.tab_map.sync(snapshot, edits); - self.wrap_map + let (snapshot, edits) = self + .wrap_map .update(cx, |map, cx| map.sync(snapshot, edits, cx)); + self.block_map.read(snapshot, edits, cx); + } + + pub fn insert_blocks( + &mut self, + blocks: impl IntoIterator>, + cx: &mut ModelContext, + ) -> Vec + where + P: ToOffset + Clone, + T: Into + Clone, + { + let (snapshot, edits) = self.fold_map.read(cx); + let (snapshot, edits) = self.tab_map.sync(snapshot, edits); + let (snapshot, edits) = self + .wrap_map + .update(cx, |map, cx| map.sync(snapshot, edits, cx)); + let mut block_map = self.block_map.write(snapshot, edits, cx); + block_map.insert(blocks, cx) + } + + pub fn restyle_blocks(&mut self, styles: HashMap, Option)>) + where + F1: 'static + Fn(&AppContext) -> Vec<(usize, HighlightStyle)>, + F2: 'static + Fn(&AppContext) -> BlockStyle, + { + self.block_map.restyle(styles); + } + + pub fn remove_blocks(&mut self, ids: HashSet, cx: &mut ModelContext) { + let (snapshot, edits) = self.fold_map.read(cx); + let (snapshot, edits) = self.tab_map.sync(snapshot, edits); + let (snapshot, edits) = self + .wrap_map + .update(cx, |map, cx| map.sync(snapshot, edits, cx)); + let mut block_map = self.block_map.write(snapshot, edits, cx); + block_map.remove(ids, cx); } pub fn set_font(&self, font_id: FontId, font_size: f32, cx: &mut ModelContext) { @@ -113,6 +173,7 @@ pub struct DisplayMapSnapshot { folds_snapshot: fold_map::Snapshot, tabs_snapshot: tab_map::Snapshot, wraps_snapshot: wrap_map::Snapshot, + blocks_snapshot: block_map::BlockSnapshot, } impl DisplayMapSnapshot { @@ -125,8 +186,8 @@ impl DisplayMapSnapshot { self.buffer_snapshot.len() == 0 } - pub fn buffer_rows(&self, start_row: u32) -> BufferRows { - self.wraps_snapshot.buffer_rows(start_row) + pub fn buffer_rows<'a>(&'a self, start_row: u32, cx: Option<&'a AppContext>) -> BufferRows<'a> { + self.blocks_snapshot.buffer_rows(start_row, cx) } pub fn buffer_row_count(&self) -> u32 { @@ -136,9 +197,9 @@ impl DisplayMapSnapshot { pub fn prev_row_boundary(&self, mut display_point: DisplayPoint) -> (DisplayPoint, Point) { loop { *display_point.column_mut() = 0; - let mut point = display_point.to_buffer_point(self, Bias::Left); + let mut point = display_point.to_point(self); point.column = 0; - let next_display_point = point.to_display_point(self, Bias::Left); + let next_display_point = self.point_to_display_point(point, Bias::Left); if next_display_point == display_point { return (display_point, point); } @@ -149,9 +210,9 @@ impl DisplayMapSnapshot { pub fn next_row_boundary(&self, mut display_point: DisplayPoint) -> (DisplayPoint, Point) { loop { *display_point.column_mut() = self.line_len(display_point.row()); - let mut point = display_point.to_buffer_point(self, Bias::Right); + let mut point = display_point.to_point(self); point.column = self.buffer_snapshot.line_len(point.row); - let next_display_point = point.to_display_point(self, Bias::Right); + let next_display_point = self.point_to_display_point(point, Bias::Right); if next_display_point == display_point { return (display_point, point); } @@ -159,25 +220,46 @@ impl DisplayMapSnapshot { } } + fn point_to_display_point(&self, point: Point, bias: Bias) -> DisplayPoint { + DisplayPoint( + self.blocks_snapshot.to_block_point( + self.wraps_snapshot.from_tab_point( + self.tabs_snapshot + .to_tab_point(point.to_fold_point(&self.folds_snapshot, bias)), + ), + ), + ) + } + + fn display_point_to_point(&self, point: DisplayPoint, bias: Bias) -> Point { + let unblocked_point = self.blocks_snapshot.to_wrap_point(point.0); + let unwrapped_point = self.wraps_snapshot.to_tab_point(unblocked_point); + let unexpanded_point = self.tabs_snapshot.to_fold_point(unwrapped_point, bias).0; + unexpanded_point.to_buffer_point(&self.folds_snapshot) + } + pub fn max_point(&self) -> DisplayPoint { - DisplayPoint(self.wraps_snapshot.max_point()) + DisplayPoint(self.blocks_snapshot.max_point()) } - pub fn chunks_at(&self, display_row: u32) -> wrap_map::Chunks { - self.wraps_snapshot.chunks_at(display_row) + pub fn text_chunks(&self, display_row: u32) -> impl Iterator { + self.blocks_snapshot + .chunks(display_row..self.max_point().row() + 1, None, None) + .map(|h| h.text) } - pub fn highlighted_chunks_for_rows( - &mut self, + pub fn chunks<'a>( + &'a self, display_rows: Range, - ) -> wrap_map::HighlightedChunks { - self.wraps_snapshot - .highlighted_chunks_for_rows(display_rows) + theme: Option<&'a SyntaxTheme>, + cx: &'a AppContext, + ) -> block_map::Chunks<'a> { + self.blocks_snapshot.chunks(display_rows, theme, Some(cx)) } pub fn chars_at<'a>(&'a self, point: DisplayPoint) -> impl Iterator + 'a { let mut column = 0; - let mut chars = self.chunks_at(point.row()).flat_map(str::chars); + let mut chars = self.text_chunks(point.row()).flat_map(str::chars); while column < point.column() { if let Some(c) = chars.next() { column += c.len_utf8() as u32; @@ -215,7 +297,7 @@ impl DisplayMapSnapshot { } pub fn clip_point(&self, point: DisplayPoint, bias: Bias) -> DisplayPoint { - DisplayPoint(self.wraps_snapshot.clip_point(point.0, bias)) + DisplayPoint(self.blocks_snapshot.clip_point(point.0, bias)) } pub fn folds_in_range<'a, T>( @@ -233,22 +315,31 @@ impl DisplayMapSnapshot { } pub fn is_line_folded(&self, display_row: u32) -> bool { - let wrap_point = DisplayPoint::new(display_row, 0).0; - let row = self.wraps_snapshot.to_tab_point(wrap_point).row(); - self.folds_snapshot.is_line_folded(row) + let block_point = BlockPoint(Point::new(display_row, 0)); + let wrap_point = self.blocks_snapshot.to_wrap_point(block_point); + let tab_point = self.wraps_snapshot.to_tab_point(wrap_point); + self.folds_snapshot.is_line_folded(tab_point.row()) + } + + pub fn is_block_line(&self, display_row: u32) -> bool { + self.blocks_snapshot.is_block_line(display_row) } pub fn soft_wrap_indent(&self, display_row: u32) -> Option { - self.wraps_snapshot.soft_wrap_indent(display_row) + let wrap_row = self + .blocks_snapshot + .to_wrap_point(BlockPoint::new(display_row, 0)) + .row(); + self.wraps_snapshot.soft_wrap_indent(wrap_row) } pub fn text(&self) -> String { - self.chunks_at(0).collect() + self.text_chunks(0).collect() } pub fn line(&self, display_row: u32) -> String { let mut result = String::new(); - for chunk in self.chunks_at(display_row) { + for chunk in self.text_chunks(display_row) { if let Some(ix) = chunk.find('\n') { result.push_str(&chunk[0..ix]); break; @@ -274,30 +365,20 @@ impl DisplayMapSnapshot { } pub fn line_len(&self, row: u32) -> u32 { - self.wraps_snapshot.line_len(row) + self.blocks_snapshot.line_len(row) } pub fn longest_row(&self) -> u32 { - self.wraps_snapshot.longest_row() - } - - pub fn anchor_before(&self, point: DisplayPoint, bias: Bias) -> Anchor { - self.buffer_snapshot - .anchor_before(point.to_buffer_point(self, bias)) - } - - pub fn anchor_after(&self, point: DisplayPoint, bias: Bias) -> Anchor { - self.buffer_snapshot - .anchor_after(point.to_buffer_point(self, bias)) + self.blocks_snapshot.longest_row() } } #[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)] -pub struct DisplayPoint(wrap_map::WrapPoint); +pub struct DisplayPoint(BlockPoint); impl DisplayPoint { pub fn new(row: u32, column: u32) -> Self { - Self(wrap_map::WrapPoint::new(row, column)) + Self(BlockPoint(Point::new(row, column))) } pub fn zero() -> Self { @@ -310,50 +391,52 @@ impl DisplayPoint { } pub fn row(self) -> u32 { - self.0.row() + self.0.row } pub fn column(self) -> u32 { - self.0.column() + self.0.column } pub fn row_mut(&mut self) -> &mut u32 { - self.0.row_mut() + &mut self.0.row } pub fn column_mut(&mut self) -> &mut u32 { - self.0.column_mut() + &mut self.0.column } - pub fn to_buffer_point(self, map: &DisplayMapSnapshot, bias: Bias) -> Point { - let unwrapped_point = map.wraps_snapshot.to_tab_point(self.0); - let unexpanded_point = map.tabs_snapshot.to_fold_point(unwrapped_point, bias).0; - unexpanded_point.to_buffer_point(&map.folds_snapshot) + pub fn to_point(self, map: &DisplayMapSnapshot) -> Point { + map.display_point_to_point(self, Bias::Left) } - pub fn to_buffer_offset(self, map: &DisplayMapSnapshot, bias: Bias) -> usize { - let unwrapped_point = map.wraps_snapshot.to_tab_point(self.0); + pub fn to_offset(self, map: &DisplayMapSnapshot, bias: Bias) -> usize { + let unblocked_point = map.blocks_snapshot.to_wrap_point(self.0); + let unwrapped_point = map.wraps_snapshot.to_tab_point(unblocked_point); let unexpanded_point = map.tabs_snapshot.to_fold_point(unwrapped_point, bias).0; unexpanded_point.to_buffer_offset(&map.folds_snapshot) } } impl ToDisplayPoint for Point { - fn to_display_point(&self, map: &DisplayMapSnapshot, bias: Bias) -> DisplayPoint { - let fold_point = self.to_fold_point(&map.folds_snapshot, bias); - let tab_point = map.tabs_snapshot.to_tab_point(fold_point); - let wrap_point = map.wraps_snapshot.to_wrap_point(tab_point); - DisplayPoint(wrap_point) + fn to_display_point(&self, map: &DisplayMapSnapshot) -> DisplayPoint { + map.point_to_display_point(*self, Bias::Left) } } impl ToDisplayPoint for Anchor { - fn to_display_point(&self, map: &DisplayMapSnapshot, bias: Bias) -> DisplayPoint { - self.to_point(&map.buffer_snapshot) - .to_display_point(map, bias) + fn to_display_point(&self, map: &DisplayMapSnapshot) -> DisplayPoint { + self.to_point(&map.buffer_snapshot).to_display_point(map) } } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum DisplayRow { + Buffer(u32), + Block(BlockId, Option), + Wrap, +} + #[cfg(test)] mod tests { use super::*; @@ -472,28 +555,28 @@ mod tests { assert_eq!( prev_display_bound, - prev_buffer_bound.to_display_point(&snapshot, Left), + prev_buffer_bound.to_display_point(&snapshot), "row boundary before {:?}. reported buffer row boundary: {:?}", point, prev_buffer_bound ); assert_eq!( next_display_bound, - next_buffer_bound.to_display_point(&snapshot, Right), + next_buffer_bound.to_display_point(&snapshot), "display row boundary after {:?}. reported buffer row boundary: {:?}", point, next_buffer_bound ); assert_eq!( prev_buffer_bound, - prev_display_bound.to_buffer_point(&snapshot, Left), + prev_display_bound.to_point(&snapshot), "row boundary before {:?}. reported display row boundary: {:?}", point, prev_display_bound ); assert_eq!( next_buffer_bound, - next_display_bound.to_buffer_point(&snapshot, Right), + next_display_bound.to_point(&snapshot), "row boundary after {:?}. reported display row boundary: {:?}", point, next_display_bound @@ -559,7 +642,7 @@ mod tests { let snapshot = map.update(cx, |map, cx| map.snapshot(cx)); assert_eq!( - snapshot.chunks_at(0).collect::(), + snapshot.text_chunks(0).collect::(), "one two \nthree four \nfive\nsix seven \neight" ); assert_eq!( @@ -608,7 +691,7 @@ mod tests { let snapshot = map.update(cx, |map, cx| map.snapshot(cx)); assert_eq!( - snapshot.chunks_at(1).collect::(), + snapshot.text_chunks(1).collect::(), "three four \nfive\nsix and \nseven eight" ); @@ -617,13 +700,13 @@ mod tests { let snapshot = map.update(cx, |map, cx| map.snapshot(cx)); assert_eq!( - snapshot.chunks_at(1).collect::(), + snapshot.text_chunks(1).collect::(), "three \nfour five\nsix and \nseven \neight" ) } #[gpui::test] - fn test_chunks_at(cx: &mut gpui::MutableAppContext) { + fn test_text_chunks(cx: &mut gpui::MutableAppContext) { let text = sample_text(6, 6); let buffer = cx.add_model(|cx| Buffer::new(0, text, cx)); let tab_size = 4; @@ -650,7 +733,7 @@ mod tests { assert_eq!( map.update(cx, |map, cx| map.snapshot(cx)) - .chunks_at(1) + .text_chunks(1) .collect::() .lines() .next(), @@ -658,7 +741,7 @@ mod tests { ); assert_eq!( map.update(cx, |map, cx| map.snapshot(cx)) - .chunks_at(2) + .text_chunks(2) .collect::() .lines() .next(), @@ -667,7 +750,7 @@ mod tests { } #[gpui::test] - async fn test_highlighted_chunks_at(mut cx: gpui::TestAppContext) { + async fn test_chunks(mut cx: gpui::TestAppContext) { use unindent::Unindent as _; let text = r#" @@ -679,8 +762,8 @@ mod tests { .unindent(); let theme = SyntaxTheme::new(vec![ - ("mod.body".to_string(), Color::from_u32(0xff0000ff).into()), - ("fn.name".to_string(), Color::from_u32(0x00ff00ff).into()), + ("mod.body".to_string(), Color::red().into()), + ("fn.name".to_string(), Color::blue().into()), ]); let lang = Arc::new( Language::new( @@ -716,22 +799,22 @@ mod tests { let map = cx.add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, cx)); assert_eq!( - cx.update(|cx| highlighted_chunks(0..5, &map, &theme, cx)), + cx.update(|cx| chunks(0..5, &map, &theme, cx)), vec![ ("fn ".to_string(), None), - ("outer".to_string(), Some("fn.name")), + ("outer".to_string(), Some(Color::blue())), ("() {}\n\nmod module ".to_string(), None), - ("{\n fn ".to_string(), Some("mod.body")), - ("inner".to_string(), Some("fn.name")), - ("() {}\n}".to_string(), Some("mod.body")), + ("{\n fn ".to_string(), Some(Color::red())), + ("inner".to_string(), Some(Color::blue())), + ("() {}\n}".to_string(), Some(Color::red())), ] ); assert_eq!( - cx.update(|cx| highlighted_chunks(3..5, &map, &theme, cx)), + cx.update(|cx| chunks(3..5, &map, &theme, cx)), vec![ - (" fn ".to_string(), Some("mod.body")), - ("inner".to_string(), Some("fn.name")), - ("() {}\n}".to_string(), Some("mod.body")), + (" fn ".to_string(), Some(Color::red())), + ("inner".to_string(), Some(Color::blue())), + ("() {}\n}".to_string(), Some(Color::red())), ] ); @@ -739,20 +822,20 @@ mod tests { map.fold(vec![Point::new(0, 6)..Point::new(3, 2)], cx) }); assert_eq!( - cx.update(|cx| highlighted_chunks(0..2, &map, &theme, cx)), + cx.update(|cx| chunks(0..2, &map, &theme, cx)), vec![ ("fn ".to_string(), None), - ("out".to_string(), Some("fn.name")), + ("out".to_string(), Some(Color::blue())), ("…".to_string(), None), - (" fn ".to_string(), Some("mod.body")), - ("inner".to_string(), Some("fn.name")), - ("() {}\n}".to_string(), Some("mod.body")), + (" fn ".to_string(), Some(Color::red())), + ("inner".to_string(), Some(Color::blue())), + ("() {}\n}".to_string(), Some(Color::red())), ] ); } #[gpui::test] - async fn test_highlighted_chunks_with_soft_wrapping(mut cx: gpui::TestAppContext) { + async fn test_chunks_with_soft_wrapping(mut cx: gpui::TestAppContext) { use unindent::Unindent as _; cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX); @@ -766,8 +849,8 @@ mod tests { .unindent(); let theme = SyntaxTheme::new(vec![ - ("mod.body".to_string(), Color::from_u32(0xff0000ff).into()), - ("fn.name".to_string(), Color::from_u32(0x00ff00ff).into()), + ("mod.body".to_string(), Color::red().into()), + ("fn.name".to_string(), Color::blue().into()), ]); let lang = Arc::new( Language::new( @@ -804,15 +887,15 @@ mod tests { let map = cx .add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, Some(40.0), cx)); assert_eq!( - cx.update(|cx| highlighted_chunks(0..5, &map, &theme, cx)), + cx.update(|cx| chunks(0..5, &map, &theme, cx)), [ ("fn \n".to_string(), None), - ("oute\nr".to_string(), Some("fn.name")), + ("oute\nr".to_string(), Some(Color::blue())), ("() \n{}\n\n".to_string(), None), ] ); assert_eq!( - cx.update(|cx| highlighted_chunks(3..5, &map, &theme, cx)), + cx.update(|cx| chunks(3..5, &map, &theme, cx)), [("{}\n\n".to_string(), None)] ); @@ -820,12 +903,12 @@ mod tests { map.fold(vec![Point::new(0, 6)..Point::new(3, 2)], cx) }); assert_eq!( - cx.update(|cx| highlighted_chunks(1..4, &map, &theme, cx)), + cx.update(|cx| chunks(1..4, &map, &theme, cx)), [ - ("out".to_string(), Some("fn.name")), + ("out".to_string(), Some(Color::blue())), ("…\n".to_string(), None), - (" \nfn ".to_string(), Some("mod.body")), - ("i\n".to_string(), Some("fn.name")) + (" \nfn ".to_string(), Some(Color::red())), + ("i\n".to_string(), Some(Color::blue())) ] ); } @@ -895,42 +978,34 @@ mod tests { let map = map.update(cx, |map, cx| map.snapshot(cx)); assert_eq!(map.text(), "✅ α\nβ \n🏀β γ"); assert_eq!( - map.chunks_at(0).collect::(), + map.text_chunks(0).collect::(), "✅ α\nβ \n🏀β γ" ); - assert_eq!(map.chunks_at(1).collect::(), "β \n🏀β γ"); - assert_eq!(map.chunks_at(2).collect::(), "🏀β γ"); + assert_eq!(map.text_chunks(1).collect::(), "β \n🏀β γ"); + assert_eq!(map.text_chunks(2).collect::(), "🏀β γ"); let point = Point::new(0, "✅\t\t".len() as u32); let display_point = DisplayPoint::new(0, "✅ ".len() as u32); - assert_eq!(point.to_display_point(&map, Left), display_point); - assert_eq!(display_point.to_buffer_point(&map, Left), point,); + assert_eq!(point.to_display_point(&map), display_point); + assert_eq!(display_point.to_point(&map), point); let point = Point::new(1, "β\t".len() as u32); let display_point = DisplayPoint::new(1, "β ".len() as u32); - assert_eq!(point.to_display_point(&map, Left), display_point); - assert_eq!(display_point.to_buffer_point(&map, Left), point,); + assert_eq!(point.to_display_point(&map), display_point); + assert_eq!(display_point.to_point(&map), point,); let point = Point::new(2, "🏀β\t\t".len() as u32); let display_point = DisplayPoint::new(2, "🏀β ".len() as u32); - assert_eq!(point.to_display_point(&map, Left), display_point); - assert_eq!(display_point.to_buffer_point(&map, Left), point,); + assert_eq!(point.to_display_point(&map), display_point); + assert_eq!(display_point.to_point(&map), point,); // Display points inside of expanded tabs assert_eq!( - DisplayPoint::new(0, "✅ ".len() as u32).to_buffer_point(&map, Right), - Point::new(0, "✅\t\t".len() as u32), - ); - assert_eq!( - DisplayPoint::new(0, "✅ ".len() as u32).to_buffer_point(&map, Left), + DisplayPoint::new(0, "✅ ".len() as u32).to_point(&map), Point::new(0, "✅\t".len() as u32), ); assert_eq!( - DisplayPoint::new(0, "✅ ".len() as u32).to_buffer_point(&map, Right), - Point::new(0, "✅\t".len() as u32), - ); - assert_eq!( - DisplayPoint::new(0, "✅ ".len() as u32).to_buffer_point(&map, Left), + DisplayPoint::new(0, "✅ ".len() as u32).to_point(&map), Point::new(0, "✅".len() as u32), ); @@ -964,24 +1039,24 @@ mod tests { ) } - fn highlighted_chunks<'a>( + fn chunks<'a>( rows: Range, map: &ModelHandle, theme: &'a SyntaxTheme, cx: &mut MutableAppContext, - ) -> Vec<(String, Option<&'a str>)> { - let mut snapshot = map.update(cx, |map, cx| map.snapshot(cx)); - let mut chunks: Vec<(String, Option<&str>)> = Vec::new(); - for chunk in snapshot.highlighted_chunks_for_rows(rows) { - let style_name = chunk.highlight_id.name(theme); - if let Some((last_chunk, last_style_name)) = chunks.last_mut() { - if style_name == *last_style_name { + ) -> Vec<(String, Option)> { + let snapshot = map.update(cx, |map, cx| map.snapshot(cx)); + let mut chunks: Vec<(String, Option)> = Vec::new(); + for chunk in snapshot.chunks(rows, Some(theme), cx) { + let color = chunk.highlight_style.map(|s| s.color); + if let Some((last_chunk, last_color)) = chunks.last_mut() { + if color == *last_color { last_chunk.push_str(chunk.text); } else { - chunks.push((chunk.text.to_string(), style_name)); + chunks.push((chunk.text.to_string(), color)); } } else { - chunks.push((chunk.text.to_string(), style_name)); + chunks.push((chunk.text.to_string(), color)); } } chunks diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs new file mode 100644 index 0000000000..b45274d691 --- /dev/null +++ b/crates/editor/src/display_map/block_map.rs @@ -0,0 +1,1600 @@ +use super::{ + wrap_map::{self, Edit as WrapEdit, Snapshot as WrapSnapshot, WrapPoint}, + BlockStyle, DisplayRow, +}; +use buffer::{rope, Anchor, Bias, Edit, Point, Rope, ToOffset, ToPoint as _}; +use gpui::{fonts::HighlightStyle, AppContext, ModelHandle}; +use language::{Buffer, Chunk}; +use parking_lot::Mutex; +use std::{ + cmp::{self, Ordering}, + collections::{HashMap, HashSet}, + fmt::Debug, + iter, + ops::{Deref, Range}, + sync::{ + atomic::{AtomicUsize, Ordering::SeqCst}, + Arc, + }, + vec, +}; +use sum_tree::SumTree; +use theme::SyntaxTheme; + +pub struct BlockMap { + buffer: ModelHandle, + next_block_id: AtomicUsize, + wrap_snapshot: Mutex, + blocks: Vec>, + transforms: Mutex>, +} + +pub struct BlockMapWriter<'a>(&'a mut BlockMap); + +pub struct BlockSnapshot { + wrap_snapshot: WrapSnapshot, + transforms: SumTree, +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct BlockId(usize); + +#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)] +pub struct BlockPoint(pub super::Point); + +#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)] +struct BlockRow(u32); + +#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)] +struct WrapRow(u32); + +pub struct Block { + id: BlockId, + position: Anchor, + text: Rope, + build_runs: Mutex Vec<(usize, HighlightStyle)>>>>, + build_style: Mutex BlockStyle>>>, + disposition: BlockDisposition, +} + +#[derive(Clone)] +pub struct BlockProperties +where + P: Clone, + T: Clone, +{ + pub position: P, + pub text: T, + pub build_runs: Option Vec<(usize, HighlightStyle)>>>, + pub build_style: Option BlockStyle>>, + pub disposition: BlockDisposition, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum BlockDisposition { + Above, + Below, +} + +#[derive(Clone, Debug)] +struct Transform { + summary: TransformSummary, + block: Option, +} + +#[derive(Clone, Debug)] +struct AlignedBlock { + block: Arc, + column: u32, +} + +#[derive(Clone, Debug, Default)] +struct TransformSummary { + input_rows: u32, + output_rows: u32, + longest_row_in_block: u32, + longest_row_in_block_chars: u32, +} + +pub struct Chunks<'a> { + transforms: sum_tree::Cursor<'a, Transform, (BlockRow, WrapRow)>, + input_chunks: wrap_map::Chunks<'a>, + input_chunk: Chunk<'a>, + block_chunks: Option>, + output_row: u32, + max_output_row: u32, + cx: Option<&'a AppContext>, +} + +struct BlockChunks<'a> { + chunks: rope::Chunks<'a>, + runs: iter::Peekable>, + chunk: Option<&'a str>, + remaining_padding: u32, + padding_column: u32, + run_start: usize, + offset: usize, +} + +pub struct BufferRows<'a> { + transforms: sum_tree::Cursor<'a, Transform, (BlockRow, WrapRow)>, + input_buffer_rows: wrap_map::BufferRows<'a>, + output_row: u32, + cx: Option<&'a AppContext>, + started: bool, +} + +impl BlockMap { + pub fn new(buffer: ModelHandle, wrap_snapshot: WrapSnapshot) -> Self { + Self { + buffer, + next_block_id: AtomicUsize::new(0), + blocks: Vec::new(), + transforms: Mutex::new(SumTree::from_item( + Transform::isomorphic(wrap_snapshot.text_summary().lines.row + 1), + &(), + )), + wrap_snapshot: Mutex::new(wrap_snapshot), + } + } + + pub fn read( + &self, + wrap_snapshot: WrapSnapshot, + edits: Vec, + cx: &AppContext, + ) -> BlockSnapshot { + self.sync(&wrap_snapshot, edits, cx); + *self.wrap_snapshot.lock() = wrap_snapshot.clone(); + BlockSnapshot { + wrap_snapshot, + transforms: self.transforms.lock().clone(), + } + } + + pub fn write( + &mut self, + wrap_snapshot: WrapSnapshot, + edits: Vec, + cx: &AppContext, + ) -> BlockMapWriter { + self.sync(&wrap_snapshot, edits, cx); + *self.wrap_snapshot.lock() = wrap_snapshot; + BlockMapWriter(self) + } + + fn sync(&self, wrap_snapshot: &WrapSnapshot, edits: Vec, cx: &AppContext) { + if edits.is_empty() { + return; + } + + let buffer = self.buffer.read(cx); + let mut transforms = self.transforms.lock(); + let mut new_transforms = SumTree::new(); + let old_row_count = transforms.summary().input_rows; + let new_row_count = wrap_snapshot.max_point().row() + 1; + let mut cursor = transforms.cursor::(); + let mut last_block_ix = 0; + let mut blocks_in_edit = Vec::new(); + let mut edits = edits.into_iter().peekable(); + + while let Some(edit) = edits.next() { + // Preserve any old transforms that precede this edit. + let old_start = WrapRow(edit.old.start); + let new_start = WrapRow(edit.new.start); + new_transforms.push_tree(cursor.slice(&old_start, Bias::Left, &()), &()); + if let Some(transform) = cursor.item() { + if transform.is_isomorphic() && old_start == cursor.end(&()) { + new_transforms.push(transform.clone(), &()); + cursor.next(&()); + while let Some(transform) = cursor.item() { + if transform + .block + .as_ref() + .map_or(false, |b| b.disposition.is_below()) + { + new_transforms.push(transform.clone(), &()); + cursor.next(&()); + } else { + break; + } + } + } + } + + // Preserve any portion of an old transform that precedes this edit. + let extent_before_edit = old_start.0 - cursor.start().0; + push_isomorphic(&mut new_transforms, extent_before_edit); + + // Skip over any old transforms that intersect this edit. + let mut old_end = WrapRow(edit.old.end); + let mut new_end = WrapRow(edit.new.end); + cursor.seek(&old_end, Bias::Left, &()); + cursor.next(&()); + if old_end == *cursor.start() { + while let Some(transform) = cursor.item() { + if transform + .block + .as_ref() + .map_or(false, |b| b.disposition.is_below()) + { + cursor.next(&()); + } else { + break; + } + } + } + + // Combine this edit with any subsequent edits that intersect the same transform. + while let Some(next_edit) = edits.peek() { + if next_edit.old.start <= cursor.start().0 { + old_end = WrapRow(next_edit.old.end); + new_end = WrapRow(next_edit.new.end); + cursor.seek(&old_end, Bias::Left, &()); + cursor.next(&()); + if old_end == *cursor.start() { + while let Some(transform) = cursor.item() { + if transform + .block + .as_ref() + .map_or(false, |b| b.disposition.is_below()) + { + cursor.next(&()); + } else { + break; + } + } + } + edits.next(); + } else { + break; + } + } + + // Find the blocks within this edited region. + let new_start = wrap_snapshot.to_point(WrapPoint::new(new_start.0, 0), Bias::Left); + let start_anchor = buffer.anchor_before(new_start); + let start_block_ix = match self.blocks[last_block_ix..].binary_search_by(|probe| { + probe + .position + .cmp(&start_anchor, buffer) + .unwrap() + .then(Ordering::Greater) + }) { + Ok(ix) | Err(ix) => last_block_ix + ix, + }; + let end_block_ix = if new_end.0 > wrap_snapshot.max_point().row() { + self.blocks.len() + } else { + let new_end = wrap_snapshot.to_point(WrapPoint::new(new_end.0, 0), Bias::Left); + let end_anchor = buffer.anchor_before(new_end); + match self.blocks[start_block_ix..].binary_search_by(|probe| { + probe + .position + .cmp(&end_anchor, buffer) + .unwrap() + .then(Ordering::Greater) + }) { + Ok(ix) | Err(ix) => start_block_ix + ix, + } + }; + last_block_ix = end_block_ix; + blocks_in_edit.clear(); + blocks_in_edit.extend( + self.blocks[start_block_ix..end_block_ix] + .iter() + .map(|block| { + let mut position = block.position.to_point(buffer); + let column = wrap_snapshot.from_point(position, Bias::Left).column(); + match block.disposition { + BlockDisposition::Above => position.column = 0, + BlockDisposition::Below => { + position.column = buffer.line_len(position.row) + } + } + let position = wrap_snapshot.from_point(position, Bias::Left); + (position.row(), column, block) + }), + ); + blocks_in_edit + .sort_unstable_by_key(|(row, _, block)| (*row, block.disposition, block.id)); + + // For each of these blocks, insert a new isomorphic transform preceding the block, + // and then insert the block itself. + for (block_row, column, block) in blocks_in_edit.iter().copied() { + let insertion_row = match block.disposition { + BlockDisposition::Above => block_row, + BlockDisposition::Below => block_row + 1, + }; + let extent_before_block = insertion_row - new_transforms.summary().input_rows; + push_isomorphic(&mut new_transforms, extent_before_block); + new_transforms.push(Transform::block(block.clone(), column), &()); + } + + old_end = WrapRow(old_end.0.min(old_row_count)); + new_end = WrapRow(new_end.0.min(new_row_count)); + + // Insert an isomorphic transform after the final block. + let extent_after_last_block = new_end.0 - new_transforms.summary().input_rows; + push_isomorphic(&mut new_transforms, extent_after_last_block); + + // Preserve any portion of the old transform after this edit. + let extent_after_edit = cursor.start().0 - old_end.0; + push_isomorphic(&mut new_transforms, extent_after_edit); + } + + new_transforms.push_tree(cursor.suffix(&()), &()); + debug_assert_eq!( + new_transforms.summary().input_rows, + wrap_snapshot.max_point().row() + 1 + ); + + drop(cursor); + *transforms = new_transforms; + } + + pub fn restyle(&mut self, mut styles: HashMap, Option)>) + where + F1: 'static + Fn(&AppContext) -> Vec<(usize, HighlightStyle)>, + F2: 'static + Fn(&AppContext) -> BlockStyle, + { + for block in &self.blocks { + if let Some((build_runs, build_style)) = styles.remove(&block.id) { + *block.build_runs.lock() = build_runs.map(|build_runs| { + Arc::new(build_runs) as Arc Vec<(usize, HighlightStyle)>> + }); + *block.build_style.lock() = build_style.map(|build_style| { + Arc::new(build_style) as Arc BlockStyle> + }); + } + } + } +} + +fn push_isomorphic(tree: &mut SumTree, rows: u32) { + if rows == 0 { + return; + } + + let mut extent = Some(rows); + tree.update_last( + |last_transform| { + if last_transform.is_isomorphic() { + let extent = extent.take().unwrap(); + last_transform.summary.input_rows += extent; + last_transform.summary.output_rows += extent; + } + }, + &(), + ); + if let Some(extent) = extent { + tree.push(Transform::isomorphic(extent), &()); + } +} + +impl BlockPoint { + pub fn new(row: u32, column: u32) -> Self { + Self(Point::new(row, column)) + } +} + +impl Deref for BlockPoint { + type Target = Point; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl std::ops::DerefMut for BlockPoint { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl<'a> BlockMapWriter<'a> { + pub fn insert( + &mut self, + blocks: impl IntoIterator>, + cx: &AppContext, + ) -> Vec + where + P: ToOffset + Clone, + T: Into + Clone, + { + let buffer = self.0.buffer.read(cx); + let mut ids = Vec::new(); + let mut edits = Vec::>::new(); + let wrap_snapshot = &*self.0.wrap_snapshot.lock(); + + for block in blocks { + let id = BlockId(self.0.next_block_id.fetch_add(1, SeqCst)); + ids.push(id); + + let position = buffer.anchor_after(block.position); + let point = position.to_point(buffer); + let start_row = wrap_snapshot + .from_point(Point::new(point.row, 0), Bias::Left) + .row(); + let end_row = if point.row == buffer.max_point().row { + wrap_snapshot.max_point().row() + 1 + } else { + wrap_snapshot + .from_point(Point::new(point.row + 1, 0), Bias::Left) + .row() + }; + + let block_ix = match self + .0 + .blocks + .binary_search_by(|probe| probe.position.cmp(&position, buffer).unwrap()) + { + Ok(ix) | Err(ix) => ix, + }; + self.0.blocks.insert( + block_ix, + Arc::new(Block { + id, + position, + text: block.text.into(), + build_runs: Mutex::new(block.build_runs), + build_style: Mutex::new(block.build_style), + disposition: block.disposition, + }), + ); + + if let Err(edit_ix) = edits.binary_search_by_key(&start_row, |edit| edit.old.start) { + edits.insert( + edit_ix, + Edit { + old: start_row..end_row, + new: start_row..end_row, + }, + ); + } + } + + self.0.sync(wrap_snapshot, edits, cx); + ids + } + + pub fn remove(&mut self, block_ids: HashSet, cx: &AppContext) { + let buffer = self.0.buffer.read(cx); + let wrap_snapshot = &*self.0.wrap_snapshot.lock(); + let mut edits = Vec::new(); + let mut last_block_buffer_row = None; + self.0.blocks.retain(|block| { + if block_ids.contains(&block.id) { + let buffer_row = block.position.to_point(buffer).row; + if last_block_buffer_row != Some(buffer_row) { + last_block_buffer_row = Some(buffer_row); + let start_row = wrap_snapshot + .from_point(Point::new(buffer_row, 0), Bias::Left) + .row(); + let end_row = wrap_snapshot + .from_point( + Point::new(buffer_row, buffer.line_len(buffer_row)), + Bias::Left, + ) + .row() + + 1; + edits.push(Edit { + old: start_row..end_row, + new: start_row..end_row, + }) + } + false + } else { + true + } + }); + self.0.sync(wrap_snapshot, edits, cx); + } +} + +impl BlockSnapshot { + #[cfg(test)] + fn text(&mut self) -> String { + self.chunks(0..self.transforms.summary().output_rows, None, None) + .map(|chunk| chunk.text) + .collect() + } + + pub fn chunks<'a>( + &'a self, + rows: Range, + theme: Option<&'a SyntaxTheme>, + cx: Option<&'a AppContext>, + ) -> Chunks<'a> { + let max_output_row = cmp::min(rows.end, self.transforms.summary().output_rows); + let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(); + let input_end = { + cursor.seek(&BlockRow(rows.end), Bias::Right, &()); + let overshoot = if cursor + .item() + .map_or(false, |transform| transform.is_isomorphic()) + { + rows.end - cursor.start().0 .0 + } else { + 0 + }; + cursor.start().1 .0 + overshoot + }; + let input_start = { + cursor.seek(&BlockRow(rows.start), Bias::Right, &()); + let overshoot = if cursor + .item() + .map_or(false, |transform| transform.is_isomorphic()) + { + rows.start - cursor.start().0 .0 + } else { + 0 + }; + cursor.start().1 .0 + overshoot + }; + Chunks { + input_chunks: self.wrap_snapshot.chunks(input_start..input_end, theme), + input_chunk: Default::default(), + block_chunks: None, + transforms: cursor, + output_row: rows.start, + max_output_row, + cx, + } + } + + pub fn buffer_rows<'a>(&'a self, start_row: u32, cx: Option<&'a AppContext>) -> BufferRows<'a> { + let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(); + cursor.seek(&BlockRow(start_row), Bias::Right, &()); + let (output_start, input_start) = cursor.start(); + let overshoot = if cursor.item().map_or(false, |t| t.is_isomorphic()) { + start_row - output_start.0 + } else { + 0 + }; + let input_start_row = input_start.0 + overshoot; + BufferRows { + cx, + transforms: cursor, + input_buffer_rows: self.wrap_snapshot.buffer_rows(input_start_row), + output_row: start_row, + started: false, + } + } + + pub fn max_point(&self) -> BlockPoint { + let row = self.transforms.summary().output_rows - 1; + BlockPoint::new(row, self.line_len(row)) + } + + pub fn longest_row(&self) -> u32 { + let input_row = self.wrap_snapshot.longest_row(); + let input_row_chars = self.wrap_snapshot.line_char_count(input_row); + let TransformSummary { + longest_row_in_block: block_row, + longest_row_in_block_chars: block_row_chars, + .. + } = &self.transforms.summary(); + + if *block_row_chars > input_row_chars { + *block_row + } else { + self.to_block_point(WrapPoint::new(input_row, 0)).row + } + } + + pub fn line_len(&self, row: u32) -> u32 { + let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(); + cursor.seek(&BlockRow(row), Bias::Right, &()); + if let Some(transform) = cursor.item() { + let (output_start, input_start) = cursor.start(); + let overshoot = row - output_start.0; + if let Some(block) = &transform.block { + let mut len = block.text.line_len(overshoot); + if len > 0 { + len += block.column; + } + len + } else { + self.wrap_snapshot.line_len(input_start.0 + overshoot) + } + } else { + panic!("row out of range"); + } + } + + pub fn is_block_line(&self, row: u32) -> bool { + let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(); + cursor.seek(&BlockRow(row), Bias::Right, &()); + cursor.item().map_or(false, |t| t.block.is_some()) + } + + pub fn clip_point(&self, point: BlockPoint, bias: Bias) -> BlockPoint { + let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(); + cursor.seek(&BlockRow(point.row), Bias::Right, &()); + + let max_input_row = WrapRow(self.transforms.summary().input_rows); + let search_left = + (bias == Bias::Left && cursor.start().1 .0 > 0) || cursor.end(&()).1 == max_input_row; + + loop { + if let Some(transform) = cursor.item() { + if transform.is_isomorphic() { + let (output_start_row, input_start_row) = cursor.start(); + let (output_end_row, input_end_row) = cursor.end(&()); + + if point.row >= output_end_row.0 { + return BlockPoint::new( + output_end_row.0 - 1, + self.wrap_snapshot.line_len(input_end_row.0 - 1), + ); + } + + let output_start = Point::new(output_start_row.0, 0); + if point.0 > output_start { + let output_overshoot = point.0 - output_start; + let input_start = Point::new(input_start_row.0, 0); + let input_point = self + .wrap_snapshot + .clip_point(WrapPoint(input_start + output_overshoot), bias); + let input_overshoot = input_point.0 - input_start; + return BlockPoint(output_start + input_overshoot); + } else { + return BlockPoint(output_start); + } + } else if search_left { + cursor.prev(&()); + } else { + cursor.next(&()); + } + } else { + return self.max_point(); + } + } + } + + pub fn to_block_point(&self, wrap_point: WrapPoint) -> BlockPoint { + let mut cursor = self.transforms.cursor::<(WrapRow, BlockRow)>(); + cursor.seek(&WrapRow(wrap_point.row()), Bias::Right, &()); + if let Some(transform) = cursor.item() { + debug_assert!(transform.is_isomorphic()); + } else { + return self.max_point(); + } + + let (input_start_row, output_start_row) = cursor.start(); + let input_start = Point::new(input_start_row.0, 0); + let output_start = Point::new(output_start_row.0, 0); + let input_overshoot = wrap_point.0 - input_start; + BlockPoint(output_start + input_overshoot) + } + + pub fn to_wrap_point(&self, block_point: BlockPoint) -> WrapPoint { + let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(); + cursor.seek(&BlockRow(block_point.row), Bias::Right, &()); + if let Some(transform) = cursor.item() { + match transform.block.as_ref().map(|b| b.disposition) { + Some(BlockDisposition::Above) => WrapPoint::new(cursor.start().1 .0, 0), + Some(BlockDisposition::Below) => { + let wrap_row = cursor.start().1 .0 - 1; + WrapPoint::new(wrap_row, self.wrap_snapshot.line_len(wrap_row)) + } + None => { + let overshoot = block_point.row - cursor.start().0 .0; + let wrap_row = cursor.start().1 .0 + overshoot; + WrapPoint::new(wrap_row, block_point.column) + } + } + } else { + self.wrap_snapshot.max_point() + } + } +} + +impl Transform { + fn isomorphic(rows: u32) -> Self { + Self { + summary: TransformSummary { + input_rows: rows, + output_rows: rows, + longest_row_in_block: 0, + longest_row_in_block_chars: 0, + }, + block: None, + } + } + + fn block(block: Arc, column: u32) -> Self { + let text_summary = block.text.summary(); + Self { + summary: TransformSummary { + input_rows: 0, + output_rows: text_summary.lines.row + 1, + longest_row_in_block: text_summary.longest_row, + longest_row_in_block_chars: column + text_summary.longest_row_chars, + }, + block: Some(AlignedBlock { block, column }), + } + } + + fn is_isomorphic(&self) -> bool { + self.block.is_none() + } +} + +impl<'a> Iterator for Chunks<'a> { + type Item = Chunk<'a>; + + fn next(&mut self) -> Option { + if self.output_row >= self.max_output_row { + return None; + } + + if let Some(block_chunks) = self.block_chunks.as_mut() { + if let Some(block_chunk) = block_chunks.next() { + self.output_row += block_chunk.text.matches('\n').count() as u32; + return Some(block_chunk); + } else { + self.block_chunks.take(); + self.output_row += 1; + if self.output_row < self.max_output_row { + return Some(Chunk { + text: "\n", + ..Default::default() + }); + } else { + return None; + } + } + } + + let transform = self.transforms.item()?; + if let Some(block) = transform.block.as_ref() { + let block_start = self.transforms.start().0 .0; + let block_end = self.transforms.end(&()).0 .0; + let start_in_block = self.output_row - block_start; + let end_in_block = cmp::min(self.max_output_row, block_end) - block_start; + self.transforms.next(&()); + self.block_chunks = Some(BlockChunks::new( + block, + start_in_block..end_in_block, + self.cx, + )); + return self.next(); + } + + if self.input_chunk.text.is_empty() { + if let Some(input_chunk) = self.input_chunks.next() { + self.input_chunk = input_chunk; + } else { + self.output_row += 1; + if self.output_row < self.max_output_row { + self.transforms.next(&()); + return Some(Chunk { + text: "\n", + ..Default::default() + }); + } else { + return None; + } + } + } + + let transform_end = self.transforms.end(&()).0 .0; + let (prefix_rows, prefix_bytes) = + offset_for_row(self.input_chunk.text, transform_end - self.output_row); + self.output_row += prefix_rows; + let (prefix, suffix) = self.input_chunk.text.split_at(prefix_bytes); + self.input_chunk.text = suffix; + if self.output_row == transform_end { + self.transforms.next(&()); + } + + Some(Chunk { + text: prefix, + ..self.input_chunk + }) + } +} + +impl<'a> BlockChunks<'a> { + fn new(block: &'a AlignedBlock, rows: Range, cx: Option<&'a AppContext>) -> Self { + let offset_range = block.text.point_to_offset(Point::new(rows.start, 0)) + ..block.text.point_to_offset(Point::new(rows.end, 0)); + + let mut runs = block + .build_runs + .lock() + .as_ref() + .zip(cx) + .map(|(build_runs, cx)| build_runs(cx)) + .unwrap_or_default() + .into_iter() + .peekable(); + let mut run_start = 0; + while let Some((run_len, _)) = runs.peek() { + let run_end = run_start + run_len; + if run_end <= offset_range.start { + run_start = run_end; + runs.next(); + } else { + break; + } + } + + Self { + chunk: None, + run_start, + padding_column: block.column, + remaining_padding: block.column, + chunks: block.text.chunks_in_range(offset_range.clone()), + runs, + offset: offset_range.start, + } + } +} + +impl<'a> Iterator for BlockChunks<'a> { + type Item = Chunk<'a>; + + fn next(&mut self) -> Option { + if self.chunk.is_none() { + self.chunk = self.chunks.next(); + } + + let chunk = self.chunk?; + + if chunk.starts_with('\n') { + self.remaining_padding = 0; + } + + if self.remaining_padding > 0 { + const PADDING: &'static str = " "; + let padding_len = self.remaining_padding.min(PADDING.len() as u32); + self.remaining_padding -= padding_len; + return Some(Chunk { + text: &PADDING[..padding_len as usize], + ..Default::default() + }); + } + + let mut chunk_len = if let Some(ix) = chunk.find('\n') { + ix + 1 + } else { + chunk.len() + }; + + let mut highlight_style = None; + if let Some((run_len, style)) = self.runs.peek() { + highlight_style = Some(style.clone()); + let run_end_in_chunk = self.run_start + run_len - self.offset; + if run_end_in_chunk <= chunk_len { + chunk_len = run_end_in_chunk; + self.run_start += run_len; + self.runs.next(); + } + } + + self.offset += chunk_len; + let (chunk, suffix) = chunk.split_at(chunk_len); + + if chunk.ends_with('\n') { + self.remaining_padding = self.padding_column; + } + + self.chunk = if suffix.is_empty() { + None + } else { + Some(suffix) + }; + + Some(Chunk { + text: chunk, + highlight_style, + diagnostic: None, + }) + } +} + +impl<'a> Iterator for BufferRows<'a> { + type Item = DisplayRow; + + fn next(&mut self) -> Option { + if self.started { + self.output_row += 1; + } else { + self.started = true; + } + + if self.output_row >= self.transforms.end(&()).0 .0 { + self.transforms.next(&()); + } + + let transform = self.transforms.item()?; + if let Some(block) = &transform.block { + let style = self + .cx + .and_then(|cx| block.build_style.lock().as_ref().map(|f| f(cx))); + Some(DisplayRow::Block(block.id, style)) + } else { + Some(self.input_buffer_rows.next().unwrap()) + } + } +} + +impl sum_tree::Item for Transform { + type Summary = TransformSummary; + + fn summary(&self) -> Self::Summary { + self.summary.clone() + } +} + +impl sum_tree::Summary for TransformSummary { + type Context = (); + + fn add_summary(&mut self, summary: &Self, _: &()) { + if summary.longest_row_in_block_chars > self.longest_row_in_block_chars { + self.longest_row_in_block_chars = summary.longest_row_in_block_chars; + self.longest_row_in_block = self.output_rows + summary.longest_row_in_block; + } + + self.input_rows += summary.input_rows; + self.output_rows += summary.output_rows; + } +} + +impl<'a> sum_tree::Dimension<'a, TransformSummary> for WrapRow { + fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) { + self.0 += summary.input_rows; + } +} + +impl<'a> sum_tree::Dimension<'a, TransformSummary> for BlockRow { + fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) { + self.0 += summary.output_rows; + } +} + +impl BlockDisposition { + fn is_below(&self) -> bool { + matches!(self, BlockDisposition::Below) + } +} + +impl Deref for AlignedBlock { + type Target = Block; + + fn deref(&self) -> &Self::Target { + self.block.as_ref() + } +} + +impl Debug for Block { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Block") + .field("id", &self.id) + .field("position", &self.position) + .field("text", &self.text) + .field("disposition", &self.disposition) + .finish() + } +} + +// Count the number of bytes prior to a target point. If the string doesn't contain the target +// point, return its total extent. Otherwise return the target point itself. +fn offset_for_row(s: &str, target: u32) -> (u32, usize) { + let mut row = 0; + let mut offset = 0; + for (ix, line) in s.split('\n').enumerate() { + if ix > 0 { + row += 1; + offset += 1; + } + if row >= target { + break; + } + offset += line.len() as usize; + } + (row, offset) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::display_map::{fold_map::FoldMap, tab_map::TabMap, wrap_map::WrapMap}; + use buffer::RandomCharIter; + use gpui::color::Color; + use language::Buffer; + use rand::prelude::*; + use std::env; + + #[gpui::test] + fn test_offset_for_row() { + assert_eq!(offset_for_row("", 0), (0, 0)); + assert_eq!(offset_for_row("", 1), (0, 0)); + assert_eq!(offset_for_row("abcd", 0), (0, 0)); + assert_eq!(offset_for_row("abcd", 1), (0, 4)); + assert_eq!(offset_for_row("\n", 0), (0, 0)); + assert_eq!(offset_for_row("\n", 1), (1, 1)); + assert_eq!(offset_for_row("abc\ndef\nghi", 0), (0, 0)); + assert_eq!(offset_for_row("abc\ndef\nghi", 1), (1, 4)); + assert_eq!(offset_for_row("abc\ndef\nghi", 2), (2, 8)); + assert_eq!(offset_for_row("abc\ndef\nghi", 3), (2, 11)); + } + + #[gpui::test] + fn test_block_chunks(cx: &mut gpui::MutableAppContext) { + let red = Color::red(); + let blue = Color::blue(); + let clear = Color::default(); + + let block = AlignedBlock { + column: 5, + block: Arc::new(Block { + id: BlockId(0), + position: Anchor::min(), + text: "one!\ntwo three\nfour".into(), + build_style: Mutex::new(None), + build_runs: Mutex::new(Some(Arc::new(move |_| { + vec![(3, red.into()), (6, Default::default()), (5, blue.into())] + }))), + disposition: BlockDisposition::Above, + }), + }; + + assert_eq!( + colored_chunks(&block, 0..3, cx), + &[ + (" ", clear), + ("one", red), + ("!\n", clear), + (" ", clear), + ("two ", clear), + ("three", blue), + ("\n", clear), + (" ", clear), + ("four", clear) + ] + ); + assert_eq!( + colored_chunks(&block, 0..1, cx), + &[ + (" ", clear), // + ("one", red), + ("!\n", clear), + ] + ); + assert_eq!( + colored_chunks(&block, 1..3, cx), + &[ + (" ", clear), + ("two ", clear), + ("three", blue), + ("\n", clear), + (" ", clear), + ("four", clear) + ] + ); + + fn colored_chunks<'a>( + block: &'a AlignedBlock, + row_range: Range, + cx: &'a AppContext, + ) -> Vec<(&'a str, Color)> { + BlockChunks::new(block, row_range, Some(cx)) + .map(|c| { + ( + c.text, + c.highlight_style.map_or(Color::default(), |s| s.color), + ) + }) + .collect() + } + } + + #[gpui::test] + fn test_basic_blocks(cx: &mut gpui::MutableAppContext) { + let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap(); + let font_id = cx + .font_cache() + .select_font(family_id, &Default::default()) + .unwrap(); + + let text = "aaa\nbbb\nccc\nddd"; + + let buffer = cx.add_model(|cx| Buffer::new(0, text, cx)); + let (fold_map, folds_snapshot) = FoldMap::new(buffer.clone(), cx); + let (tab_map, tabs_snapshot) = TabMap::new(folds_snapshot.clone(), 1); + let (wrap_map, wraps_snapshot) = WrapMap::new(tabs_snapshot, font_id, 14.0, None, cx); + let mut block_map = BlockMap::new(buffer.clone(), wraps_snapshot.clone()); + + let mut writer = block_map.write(wraps_snapshot.clone(), vec![], cx); + let block_ids = writer.insert( + vec![ + BlockProperties { + position: Point::new(1, 0), + text: "BLOCK 1", + disposition: BlockDisposition::Above, + build_runs: None, + build_style: None, + }, + BlockProperties { + position: Point::new(1, 2), + text: "BLOCK 2", + disposition: BlockDisposition::Above, + build_runs: None, + build_style: None, + }, + BlockProperties { + position: Point::new(3, 2), + text: "BLOCK 3", + disposition: BlockDisposition::Below, + build_runs: None, + build_style: None, + }, + ], + cx, + ); + + let mut snapshot = block_map.read(wraps_snapshot, vec![], cx); + assert_eq!( + snapshot.text(), + "aaa\nBLOCK 1\n BLOCK 2\nbbb\nccc\nddd\n BLOCK 3" + ); + assert_eq!( + snapshot.to_block_point(WrapPoint::new(0, 3)), + BlockPoint::new(0, 3) + ); + assert_eq!( + snapshot.to_block_point(WrapPoint::new(1, 0)), + BlockPoint::new(3, 0) + ); + assert_eq!( + snapshot.to_block_point(WrapPoint::new(3, 3)), + BlockPoint::new(5, 3) + ); + + assert_eq!( + snapshot.to_wrap_point(BlockPoint::new(0, 3)), + WrapPoint::new(0, 3) + ); + assert_eq!( + snapshot.to_wrap_point(BlockPoint::new(1, 0)), + WrapPoint::new(1, 0) + ); + assert_eq!( + snapshot.to_wrap_point(BlockPoint::new(3, 0)), + WrapPoint::new(1, 0) + ); + assert_eq!( + snapshot.to_wrap_point(BlockPoint::new(6, 0)), + WrapPoint::new(3, 3) + ); + + assert_eq!( + snapshot.clip_point(BlockPoint::new(1, 0), Bias::Left), + BlockPoint::new(0, 3) + ); + assert_eq!( + snapshot.clip_point(BlockPoint::new(1, 0), Bias::Right), + BlockPoint::new(3, 0) + ); + assert_eq!( + snapshot.clip_point(BlockPoint::new(1, 1), Bias::Left), + BlockPoint::new(0, 3) + ); + assert_eq!( + snapshot.clip_point(BlockPoint::new(1, 1), Bias::Right), + BlockPoint::new(3, 0) + ); + assert_eq!( + snapshot.clip_point(BlockPoint::new(3, 0), Bias::Left), + BlockPoint::new(3, 0) + ); + assert_eq!( + snapshot.clip_point(BlockPoint::new(3, 0), Bias::Right), + BlockPoint::new(3, 0) + ); + assert_eq!( + snapshot.clip_point(BlockPoint::new(5, 3), Bias::Left), + BlockPoint::new(5, 3) + ); + assert_eq!( + snapshot.clip_point(BlockPoint::new(5, 3), Bias::Right), + BlockPoint::new(5, 3) + ); + assert_eq!( + snapshot.clip_point(BlockPoint::new(6, 0), Bias::Left), + BlockPoint::new(5, 3) + ); + assert_eq!( + snapshot.clip_point(BlockPoint::new(6, 0), Bias::Right), + BlockPoint::new(5, 3) + ); + + assert_eq!( + snapshot.buffer_rows(0, None).collect::>(), + &[ + DisplayRow::Buffer(0), + DisplayRow::Block(block_ids[0], None), + DisplayRow::Block(block_ids[1], None), + DisplayRow::Buffer(1), + DisplayRow::Buffer(2), + DisplayRow::Buffer(3), + DisplayRow::Block(block_ids[2], None) + ] + ); + + // Insert a line break, separating two block decorations into separate + // lines. + buffer.update(cx, |buffer, cx| { + buffer.edit([Point::new(1, 1)..Point::new(1, 1)], "!!!\n", cx) + }); + + let (folds_snapshot, fold_edits) = fold_map.read(cx); + let (tabs_snapshot, tab_edits) = tab_map.sync(folds_snapshot, fold_edits); + let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| { + wrap_map.sync(tabs_snapshot, tab_edits, cx) + }); + let mut snapshot = block_map.read(wraps_snapshot, wrap_edits, cx); + assert_eq!( + snapshot.text(), + "aaa\nBLOCK 1\nb!!!\n BLOCK 2\nbb\nccc\nddd\n BLOCK 3" + ); + } + + #[gpui::test] + fn test_blocks_on_wrapped_lines(cx: &mut gpui::MutableAppContext) { + let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap(); + let font_id = cx + .font_cache() + .select_font(family_id, &Default::default()) + .unwrap(); + + let text = "one two three\nfour five six\nseven eight"; + + let buffer = cx.add_model(|cx| Buffer::new(0, text, cx)); + let (_, folds_snapshot) = FoldMap::new(buffer.clone(), cx); + let (_, tabs_snapshot) = TabMap::new(folds_snapshot.clone(), 1); + let (_, wraps_snapshot) = WrapMap::new(tabs_snapshot, font_id, 14.0, Some(60.), cx); + let mut block_map = BlockMap::new(buffer.clone(), wraps_snapshot.clone()); + + let mut writer = block_map.write(wraps_snapshot.clone(), vec![], cx); + writer.insert( + vec![ + BlockProperties { + position: Point::new(1, 12), + text: "BLOCK 2", + disposition: BlockDisposition::Below, + build_runs: None, + build_style: None, + }, + ], + cx, + ); + + // Blocks with an 'above' disposition go above their corresponding buffer line. + // Blocks with a 'below' disposition go below their corresponding buffer line. + let mut snapshot = block_map.read(wraps_snapshot, vec![], cx); + assert_eq!( + snapshot.text(), + "one two \nthree\n BLOCK 2\nseven \neight" + ); + } + + #[gpui::test(iterations = 100)] + fn test_random_blocks(cx: &mut gpui::MutableAppContext, mut rng: StdRng) { + let operations = env::var("OPERATIONS") + .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) + .unwrap_or(10); + + let wrap_width = if rng.gen_bool(0.2) { + None + } else { + Some(rng.gen_range(0.0..=100.0)) + }; + let tab_size = 1; + let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap(); + let font_id = cx + .font_cache() + .select_font(family_id, &Default::default()) + .unwrap(); + let font_size = 14.0; + + log::info!("Wrap width: {:?}", wrap_width); + + let buffer = cx.add_model(|cx| { + let len = rng.gen_range(0..10); + let text = RandomCharIter::new(&mut rng).take(len).collect::(); + log::info!("initial buffer text: {:?}", text); + Buffer::new(0, text, cx) + }); + let (fold_map, folds_snapshot) = FoldMap::new(buffer.clone(), cx); + let (tab_map, tabs_snapshot) = TabMap::new(folds_snapshot.clone(), tab_size); + let (wrap_map, wraps_snapshot) = + WrapMap::new(tabs_snapshot, font_id, font_size, wrap_width, cx); + let mut block_map = BlockMap::new(buffer.clone(), wraps_snapshot); + let mut expected_blocks = Vec::new(); + + for _ in 0..operations { + match rng.gen_range(0..=100) { + 0..=19 => { + let wrap_width = if rng.gen_bool(0.2) { + None + } else { + Some(rng.gen_range(0.0..=100.0)) + }; + log::info!("Setting wrap width to {:?}", wrap_width); + wrap_map.update(cx, |map, cx| map.set_wrap_width(wrap_width, cx)); + } + 20..=39 => { + let block_count = rng.gen_range(1..=1); + let block_properties = (0..block_count) + .map(|_| { + let buffer = buffer.read(cx); + let position = buffer.anchor_after( + buffer.clip_offset(rng.gen_range(0..=buffer.len()), Bias::Left), + ); + + let len = rng.gen_range(0..10); + let mut text = Rope::from( + RandomCharIter::new(&mut rng) + .take(len) + .collect::() + .to_uppercase() + .as_str(), + ); + let disposition = if rng.gen() { + text.push_front("<"); + BlockDisposition::Above + } else { + text.push_front(">"); + BlockDisposition::Below + }; + log::info!( + "inserting block {:?} {:?} with text {:?}", + disposition, + position.to_point(buffer), + text.to_string() + ); + BlockProperties { + position, + text, + disposition, + build_runs: None, + build_style: None, + } + }) + .collect::>(); + + let (folds_snapshot, fold_edits) = fold_map.read(cx); + let (tabs_snapshot, tab_edits) = tab_map.sync(folds_snapshot, fold_edits); + let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| { + wrap_map.sync(tabs_snapshot, tab_edits, cx) + }); + let mut block_map = block_map.write(wraps_snapshot, wrap_edits, cx); + let block_ids = block_map.insert(block_properties.clone(), cx); + for (block_id, props) in block_ids.into_iter().zip(block_properties) { + expected_blocks.push((block_id, props)); + } + } + 40..=59 if !expected_blocks.is_empty() => { + let block_count = rng.gen_range(1..=4.min(expected_blocks.len())); + let block_ids_to_remove = (0..block_count) + .map(|_| { + expected_blocks + .remove(rng.gen_range(0..expected_blocks.len())) + .0 + }) + .collect(); + + let (folds_snapshot, fold_edits) = fold_map.read(cx); + let (tabs_snapshot, tab_edits) = tab_map.sync(folds_snapshot, fold_edits); + let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| { + wrap_map.sync(tabs_snapshot, tab_edits, cx) + }); + let mut block_map = block_map.write(wraps_snapshot, wrap_edits, cx); + block_map.remove(block_ids_to_remove, cx); + } + _ => { + buffer.update(cx, |buffer, _| { + buffer.randomly_edit(&mut rng, 1); + log::info!("buffer text: {:?}", buffer.text()); + }); + } + } + + let (folds_snapshot, fold_edits) = fold_map.read(cx); + let (tabs_snapshot, tab_edits) = tab_map.sync(folds_snapshot, fold_edits); + let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| { + wrap_map.sync(tabs_snapshot, tab_edits, cx) + }); + let mut blocks_snapshot = block_map.read(wraps_snapshot.clone(), wrap_edits, cx); + assert_eq!( + blocks_snapshot.transforms.summary().input_rows, + wraps_snapshot.max_point().row() + 1 + ); + log::info!("blocks text: {:?}", blocks_snapshot.text()); + + let buffer = buffer.read(cx); + let mut sorted_blocks = expected_blocks + .iter() + .cloned() + .map(|(id, block)| { + let mut position = block.position.to_point(buffer); + let column = wraps_snapshot.from_point(position, Bias::Left).column(); + match block.disposition { + BlockDisposition::Above => { + position.column = 0; + } + BlockDisposition::Below => { + position.column = buffer.line_len(position.row); + } + }; + let row = wraps_snapshot.from_point(position, Bias::Left).row(); + ( + id, + BlockProperties { + position: BlockPoint::new(row, column), + text: block.text, + build_runs: block.build_runs.clone(), + build_style: None, + disposition: block.disposition, + }, + ) + }) + .collect::>(); + sorted_blocks + .sort_unstable_by_key(|(id, block)| (block.position.row, block.disposition, *id)); + let mut sorted_blocks = sorted_blocks.into_iter().peekable(); + + let mut expected_buffer_rows = Vec::new(); + let mut expected_text = String::new(); + let input_text = wraps_snapshot.text(); + for (row, input_line) in input_text.split('\n').enumerate() { + let row = row as u32; + if row > 0 { + expected_text.push('\n'); + } + + let buffer_row = wraps_snapshot + .to_point(WrapPoint::new(row, 0), Bias::Left) + .row; + + while let Some((block_id, block)) = sorted_blocks.peek() { + if block.position.row == row && block.disposition == BlockDisposition::Above { + let text = block.text.to_string(); + let padding = " ".repeat(block.position.column as usize); + for line in text.split('\n') { + if !line.is_empty() { + expected_text.push_str(&padding); + expected_text.push_str(line); + } + expected_text.push('\n'); + expected_buffer_rows.push(DisplayRow::Block(*block_id, None)); + } + sorted_blocks.next(); + } else { + break; + } + } + + let soft_wrapped = wraps_snapshot.to_tab_point(WrapPoint::new(row, 0)).column() > 0; + expected_buffer_rows.push(if soft_wrapped { + DisplayRow::Wrap + } else { + DisplayRow::Buffer(buffer_row) + }); + expected_text.push_str(input_line); + + while let Some((block_id, block)) = sorted_blocks.peek() { + if block.position.row == row && block.disposition == BlockDisposition::Below { + let text = block.text.to_string(); + let padding = " ".repeat(block.position.column as usize); + for line in text.split('\n') { + expected_text.push('\n'); + if !line.is_empty() { + expected_text.push_str(&padding); + expected_text.push_str(line); + } + expected_buffer_rows.push(DisplayRow::Block(*block_id, None)); + } + sorted_blocks.next(); + } else { + break; + } + } + } + + let expected_lines = expected_text.split('\n').collect::>(); + let expected_row_count = expected_lines.len(); + for start_row in 0..expected_row_count { + let expected_text = expected_lines[start_row..].join("\n"); + let actual_text = blocks_snapshot + .chunks(start_row as u32..expected_row_count as u32, None, None) + .map(|chunk| chunk.text) + .collect::(); + assert_eq!( + actual_text, expected_text, + "incorrect text starting from row {}", + start_row + ); + assert_eq!( + blocks_snapshot + .buffer_rows(start_row as u32, None) + .collect::>(), + &expected_buffer_rows[start_row..] + ); + } + + let mut expected_longest_rows = Vec::new(); + let mut longest_line_len = -1_isize; + for (row, line) in expected_lines.iter().enumerate() { + let row = row as u32; + + assert_eq!( + blocks_snapshot.line_len(row), + line.len() as u32, + "invalid line len for row {}", + row + ); + + let line_char_count = line.chars().count() as isize; + match line_char_count.cmp(&longest_line_len) { + Ordering::Less => {} + Ordering::Equal => expected_longest_rows.push(row), + Ordering::Greater => { + longest_line_len = line_char_count; + expected_longest_rows.clear(); + expected_longest_rows.push(row); + } + } + } + + log::info!("getting longest row >>>>>>>>>>>>>>>>>>>>>>>>"); + let longest_row = blocks_snapshot.longest_row(); + assert!( + expected_longest_rows.contains(&longest_row), + "incorrect longest row {}. expected {:?} with length {}", + longest_row, + expected_longest_rows, + longest_line_len, + ); + + for row in 0..=blocks_snapshot.wrap_snapshot.max_point().row() { + let wrap_point = WrapPoint::new(row, 0); + let block_point = blocks_snapshot.to_block_point(wrap_point); + assert_eq!(blocks_snapshot.to_wrap_point(block_point), wrap_point); + } + + let mut block_point = BlockPoint::new(0, 0); + for c in expected_text.chars() { + let left_point = blocks_snapshot.clip_point(block_point, Bias::Left); + let right_point = blocks_snapshot.clip_point(block_point, Bias::Right); + + assert_eq!( + blocks_snapshot.to_block_point(blocks_snapshot.to_wrap_point(left_point)), + left_point + ); + assert_eq!( + blocks_snapshot.to_block_point(blocks_snapshot.to_wrap_point(right_point)), + right_point + ); + + if c == '\n' { + block_point.0 += Point::new(1, 0); + } else { + block_point.column += c.len_utf8() as u32; + } + } + } + } +} diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index 8a5b4c5584..26d3ff3a7d 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -1,8 +1,5 @@ use gpui::{AppContext, ModelHandle}; -use language::{ - Anchor, AnchorRangeExt, Buffer, HighlightId, HighlightedChunk, Point, PointUtf16, TextSummary, - ToOffset, -}; +use language::{Anchor, AnchorRangeExt, Buffer, Chunk, Point, PointUtf16, TextSummary, ToOffset}; use parking_lot::Mutex; use std::{ cmp::{self, Ordering}, @@ -11,6 +8,7 @@ use std::{ sync::atomic::{AtomicUsize, Ordering::SeqCst}, }; use sum_tree::{Bias, Cursor, FilterCursor, SumTree}; +use theme::SyntaxTheme; pub trait ToFoldPoint { fn to_fold_point(&self, snapshot: &Snapshot, bias: Bias) -> FoldPoint; @@ -499,7 +497,9 @@ pub struct Snapshot { impl Snapshot { #[cfg(test)] pub fn text(&self) -> String { - self.chunks_at(FoldOffset(0)).collect() + self.chunks(FoldOffset(0)..self.len(), None) + .map(|c| c.text) + .collect() } #[cfg(test)] @@ -551,7 +551,6 @@ impl Snapshot { summary } - #[cfg(test)] pub fn len(&self) -> FoldOffset { FoldOffset(self.transforms.summary().output.bytes) } @@ -628,21 +627,17 @@ impl Snapshot { false } - pub fn chunks_at(&self, offset: FoldOffset) -> Chunks { - let mut transform_cursor = self.transforms.cursor::<(FoldOffset, usize)>(); - transform_cursor.seek(&offset, Bias::Right, &()); - let overshoot = offset.0 - transform_cursor.start().0 .0; - let buffer_offset = transform_cursor.start().1 + overshoot; - Chunks { - transform_cursor, - buffer_offset, - buffer_chunks: self - .buffer_snapshot - .text_for_range(buffer_offset..self.buffer_snapshot.len()), - } + pub fn chars_at(&self, start: FoldPoint) -> impl '_ + Iterator { + let start = start.to_offset(self); + self.chunks(start..self.len(), None) + .flat_map(|chunk| chunk.text.chars()) } - pub fn highlighted_chunks(&mut self, range: Range) -> HighlightedChunks { + pub fn chunks<'a>( + &'a self, + range: Range, + theme: Option<&'a SyntaxTheme>, + ) -> Chunks<'a> { let mut transform_cursor = self.transforms.cursor::<(FoldOffset, usize)>(); transform_cursor.seek(&range.end, Bias::Right, &()); @@ -653,21 +648,16 @@ impl Snapshot { let overshoot = range.start.0 - transform_cursor.start().0 .0; let buffer_start = transform_cursor.start().1 + overshoot; - HighlightedChunks { + Chunks { transform_cursor, - buffer_offset: buffer_start, - buffer_chunks: self - .buffer_snapshot - .highlighted_text_for_range(buffer_start..buffer_end), + buffer_chunks: self.buffer_snapshot.chunks(buffer_start..buffer_end, theme), buffer_chunk: None, + buffer_offset: buffer_start, + output_offset: range.start.0, + max_output_offset: range.end.0, } } - pub fn chars_at<'a>(&'a self, point: FoldPoint) -> impl Iterator + 'a { - let offset = point.to_offset(self); - self.chunks_at(offset).flat_map(str::chars) - } - #[cfg(test)] pub fn clip_offset(&self, offset: FoldOffset, bias: Bias) -> FoldOffset { let mut cursor = self.transforms.cursor::<(FoldOffset, usize)>(); @@ -948,68 +938,21 @@ impl<'a> Iterator for BufferRows<'a> { pub struct Chunks<'a> { transform_cursor: Cursor<'a, Transform, (FoldOffset, usize)>, - buffer_chunks: buffer::Chunks<'a>, + buffer_chunks: language::Chunks<'a>, + buffer_chunk: Option<(usize, Chunk<'a>)>, buffer_offset: usize, + output_offset: usize, + max_output_offset: usize, } impl<'a> Iterator for Chunks<'a> { - type Item = &'a str; + type Item = Chunk<'a>; fn next(&mut self) -> Option { - let transform = if let Some(item) = self.transform_cursor.item() { - item - } else { + if self.output_offset >= self.max_output_offset { return None; - }; - - // If we're in a fold, then return the fold's display text and - // advance the transform and buffer cursors to the end of the fold. - if let Some(output_text) = transform.output_text { - self.buffer_offset += transform.summary.input.bytes; - self.buffer_chunks.seek(self.buffer_offset); - - while self.buffer_offset >= self.transform_cursor.end(&()).1 - && self.transform_cursor.item().is_some() - { - self.transform_cursor.next(&()); - } - - return Some(output_text); } - // Otherwise, take a chunk from the buffer's text. - if let Some(mut chunk) = self.buffer_chunks.peek() { - let offset_in_chunk = self.buffer_offset - self.buffer_chunks.offset(); - chunk = &chunk[offset_in_chunk..]; - - // Truncate the chunk so that it ends at the next fold. - let region_end = self.transform_cursor.end(&()).1 - self.buffer_offset; - if chunk.len() >= region_end { - chunk = &chunk[0..region_end]; - self.transform_cursor.next(&()); - } else { - self.buffer_chunks.next(); - } - - self.buffer_offset += chunk.len(); - return Some(chunk); - } - - None - } -} - -pub struct HighlightedChunks<'a> { - transform_cursor: Cursor<'a, Transform, (FoldOffset, usize)>, - buffer_chunks: language::HighlightedChunks<'a>, - buffer_chunk: Option<(usize, HighlightedChunk<'a>)>, - buffer_offset: usize, -} - -impl<'a> Iterator for HighlightedChunks<'a> { - type Item = HighlightedChunk<'a>; - - fn next(&mut self) -> Option { let transform = if let Some(item) = self.transform_cursor.item() { item } else { @@ -1029,9 +972,10 @@ impl<'a> Iterator for HighlightedChunks<'a> { self.transform_cursor.next(&()); } - return Some(HighlightedChunk { + self.output_offset += output_text.len(); + return Some(Chunk { text: output_text, - highlight_id: HighlightId::default(), + highlight_style: None, diagnostic: None, }); } @@ -1057,6 +1001,7 @@ impl<'a> Iterator for HighlightedChunks<'a> { } self.buffer_offset += chunk.text.len(); + self.output_offset += chunk.text.len(); return Some(chunk); } @@ -1352,7 +1297,7 @@ mod tests { } let buffer = map.buffer.read(cx).snapshot(); - let mut expected_text: String = buffer.text().into(); + let mut expected_text: String = buffer.text().to_string(); let mut expected_buffer_rows = Vec::new(); let mut next_row = buffer.max_point().row; for fold_range in map.merged_fold_ranges(cx.as_ref()).into_iter().rev() { @@ -1428,11 +1373,22 @@ mod tests { } for _ in 0..5 { - let offset = snapshot + let mut start = snapshot + .clip_offset(FoldOffset(rng.gen_range(0..=snapshot.len().0)), Bias::Left); + let mut end = snapshot .clip_offset(FoldOffset(rng.gen_range(0..=snapshot.len().0)), Bias::Right); + if start > end { + mem::swap(&mut start, &mut end); + } + + let text = &expected_text[start.0..end.0]; + log::info!("slicing {:?}..{:?} (text: {:?})", start, end, text); assert_eq!( - snapshot.chunks_at(offset).collect::(), - &expected_text[offset.0..], + snapshot + .chunks(start..end, None) + .map(|c| c.text) + .collect::(), + text, ); } diff --git a/crates/editor/src/display_map/patch.rs b/crates/editor/src/display_map/patch.rs new file mode 100644 index 0000000000..1fa940d91c --- /dev/null +++ b/crates/editor/src/display_map/patch.rs @@ -0,0 +1,511 @@ +use std::{cmp, mem}; + +type Edit = buffer::Edit; + +#[derive(Default, Debug, PartialEq, Eq)] +pub struct Patch(Vec); + +impl Patch { + pub unsafe fn new_unchecked(edits: Vec) -> Self { + Self(edits) + } + + pub fn into_inner(self) -> Vec { + self.0 + } + + pub fn compose(&self, other: &Self) -> Self { + let mut old_edits_iter = self.0.iter().cloned().peekable(); + let mut new_edits_iter = other.0.iter().cloned().peekable(); + let mut composed = Patch(Vec::new()); + + let mut old_start = 0; + let mut new_start = 0; + loop { + let old_edit = old_edits_iter.peek_mut(); + let new_edit = new_edits_iter.peek_mut(); + + // Push the old edit if its new end is before the new edit's old start. + if let Some(old_edit) = old_edit.as_ref() { + let new_edit = new_edit.as_ref(); + if new_edit.map_or(true, |new_edit| old_edit.new.end < new_edit.old.start) { + let catchup = old_edit.old.start - old_start; + old_start += catchup; + new_start += catchup; + + let old_end = old_start + old_edit.old.len() as u32; + let new_end = new_start + old_edit.new.len() as u32; + composed.push(Edit { + old: old_start..old_end, + new: new_start..new_end, + }); + old_start = old_end; + new_start = new_end; + old_edits_iter.next(); + continue; + } + } + + // Push the new edit if its old end is before the old edit's new start. + if let Some(new_edit) = new_edit.as_ref() { + let old_edit = old_edit.as_ref(); + if old_edit.map_or(true, |old_edit| new_edit.old.end < old_edit.new.start) { + let catchup = new_edit.new.start - new_start; + old_start += catchup; + new_start += catchup; + + let old_end = old_start + new_edit.old.len() as u32; + let new_end = new_start + new_edit.new.len() as u32; + composed.push(Edit { + old: old_start..old_end, + new: new_start..new_end, + }); + old_start = old_end; + new_start = new_end; + new_edits_iter.next(); + continue; + } + } + + // If we still have edits by this point then they must intersect, so we compose them. + if let Some((old_edit, new_edit)) = old_edit.zip(new_edit) { + if old_edit.new.start < new_edit.old.start { + let catchup = old_edit.old.start - old_start; + old_start += catchup; + new_start += catchup; + + let overshoot = new_edit.old.start - old_edit.new.start; + let old_end = cmp::min(old_start + overshoot, old_edit.old.end); + let new_end = new_start + overshoot; + composed.push(Edit { + old: old_start..old_end, + new: new_start..new_end, + }); + + old_edit.old.start += overshoot; + old_edit.new.start += overshoot; + old_start = old_end; + new_start = new_end; + } else { + let catchup = new_edit.new.start - new_start; + old_start += catchup; + new_start += catchup; + + let overshoot = old_edit.new.start - new_edit.old.start; + let old_end = old_start + overshoot; + let new_end = cmp::min(new_start + overshoot, new_edit.new.end); + composed.push(Edit { + old: old_start..old_end, + new: new_start..new_end, + }); + + new_edit.old.start += overshoot; + new_edit.new.start += overshoot; + old_start = old_end; + new_start = new_end; + } + + if old_edit.new.end > new_edit.old.end { + let old_end = + old_start + cmp::min(old_edit.old.len() as u32, new_edit.old.len() as u32); + let new_end = new_start + new_edit.new.len() as u32; + composed.push(Edit { + old: old_start..old_end, + new: new_start..new_end, + }); + + old_edit.old.start = old_end; + old_edit.new.start = new_edit.old.end; + old_start = old_end; + new_start = new_end; + new_edits_iter.next(); + } else { + let old_end = old_start + old_edit.old.len() as u32; + let new_end = + new_start + cmp::min(old_edit.new.len() as u32, new_edit.new.len() as u32); + composed.push(Edit { + old: old_start..old_end, + new: new_start..new_end, + }); + + new_edit.old.start = old_edit.new.end; + new_edit.new.start = new_end; + old_start = old_end; + new_start = new_end; + old_edits_iter.next(); + } + } else { + break; + } + } + + composed + } + + pub fn invert(&mut self) -> &mut Self { + for edit in &mut self.0 { + mem::swap(&mut edit.old, &mut edit.new); + } + self + } + + pub fn clear(&mut self) { + self.0.clear(); + } + + fn push(&mut self, edit: Edit) { + if edit.old.len() == 0 && edit.new.len() == 0 { + return; + } + + if let Some(last) = self.0.last_mut() { + if last.old.end >= edit.old.start { + last.old.end = edit.old.end; + last.new.end = edit.new.end; + } else { + self.0.push(edit); + } + } else { + self.0.push(edit); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rand::prelude::*; + use std::env; + + #[gpui::test] + fn test_one_disjoint_edit() { + assert_patch_composition( + Patch(vec![Edit { + old: 1..3, + new: 1..4, + }]), + Patch(vec![Edit { + old: 0..0, + new: 0..4, + }]), + Patch(vec![ + Edit { + old: 0..0, + new: 0..4, + }, + Edit { + old: 1..3, + new: 5..8, + }, + ]), + ); + + assert_patch_composition( + Patch(vec![Edit { + old: 1..3, + new: 1..4, + }]), + Patch(vec![Edit { + old: 5..9, + new: 5..7, + }]), + Patch(vec![ + Edit { + old: 1..3, + new: 1..4, + }, + Edit { + old: 4..8, + new: 5..7, + }, + ]), + ); + } + + #[gpui::test] + fn test_one_overlapping_edit() { + assert_patch_composition( + Patch(vec![Edit { + old: 1..3, + new: 1..4, + }]), + Patch(vec![Edit { + old: 3..5, + new: 3..6, + }]), + Patch(vec![Edit { + old: 1..4, + new: 1..6, + }]), + ); + } + + #[gpui::test] + fn test_two_disjoint_and_overlapping() { + assert_patch_composition( + Patch(vec![ + Edit { + old: 1..3, + new: 1..4, + }, + Edit { + old: 8..12, + new: 9..11, + }, + ]), + Patch(vec![ + Edit { + old: 0..0, + new: 0..4, + }, + Edit { + old: 3..10, + new: 7..9, + }, + ]), + Patch(vec![ + Edit { + old: 0..0, + new: 0..4, + }, + Edit { + old: 1..12, + new: 5..10, + }, + ]), + ); + } + + #[gpui::test] + fn test_two_new_edits_overlapping_one_old_edit() { + assert_patch_composition( + Patch(vec![Edit { + old: 0..0, + new: 0..3, + }]), + Patch(vec![ + Edit { + old: 0..0, + new: 0..1, + }, + Edit { + old: 1..2, + new: 2..2, + }, + ]), + Patch(vec![Edit { + old: 0..0, + new: 0..3, + }]), + ); + + assert_patch_composition( + Patch(vec![Edit { + old: 2..3, + new: 2..4, + }]), + Patch(vec![ + Edit { + old: 0..2, + new: 0..1, + }, + Edit { + old: 3..3, + new: 2..5, + }, + ]), + Patch(vec![Edit { + old: 0..3, + new: 0..6, + }]), + ); + + assert_patch_composition( + Patch(vec![Edit { + old: 0..0, + new: 0..2, + }]), + Patch(vec![ + Edit { + old: 0..0, + new: 0..2, + }, + Edit { + old: 2..5, + new: 4..4, + }, + ]), + Patch(vec![Edit { + old: 0..3, + new: 0..4, + }]), + ); + } + + // #[test] + // fn test_compose_edits() { + // assert_eq!( + // compose_edits( + // &Edit { + // old: 3..3, + // new: 3..6, + // }, + // &Edit { + // old: 2..7, + // new: 2..4, + // }, + // ), + // Edit { + // old: 2..4, + // new: 2..4 + // } + // ); + // } + + #[gpui::test] + fn test_two_new_edits_touching_one_old_edit() { + assert_patch_composition( + Patch(vec![ + Edit { + old: 2..3, + new: 2..4, + }, + Edit { + old: 7..7, + new: 8..11, + }, + ]), + Patch(vec![ + Edit { + old: 2..3, + new: 2..2, + }, + Edit { + old: 4..4, + new: 3..4, + }, + ]), + Patch(vec![ + Edit { + old: 2..3, + new: 2..4, + }, + Edit { + old: 7..7, + new: 8..11, + }, + ]), + ); + } + + #[gpui::test(iterations = 100)] + fn test_random_patch_compositions(mut rng: StdRng) { + let operations = env::var("OPERATIONS") + .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) + .unwrap_or(20); + + let initial_chars = (0..rng.gen_range(0..=100)) + .map(|_| rng.gen_range(b'a'..=b'z') as char) + .collect::>(); + log::info!("initial chars: {:?}", initial_chars); + + // Generate two sequential patches + let mut patches = Vec::new(); + let mut expected_chars = initial_chars.clone(); + for i in 0..2 { + log::info!("patch {}:", i); + + let mut delta = 0i32; + let mut last_edit_end = 0; + let mut edits = Vec::new(); + + for _ in 0..operations { + if last_edit_end >= expected_chars.len() { + break; + } + + let end = rng.gen_range(last_edit_end..=expected_chars.len()); + let start = rng.gen_range(last_edit_end..=end); + let old_len = end - start; + + let mut new_len = rng.gen_range(0..=3); + if start == end && new_len == 0 { + new_len += 1; + } + + last_edit_end = start + new_len + 1; + + let new_chars = (0..new_len) + .map(|_| rng.gen_range(b'A'..=b'Z') as char) + .collect::>(); + log::info!( + " editing {:?}: {:?}", + start..end, + new_chars.iter().collect::() + ); + edits.push(Edit { + old: (start as i32 - delta) as u32..(end as i32 - delta) as u32, + new: start as u32..(start + new_len) as u32, + }); + expected_chars.splice(start..end, new_chars); + + delta += new_len as i32 - old_len as i32; + } + + patches.push(Patch(edits)); + } + + log::info!("old patch: {:?}", &patches[0]); + log::info!("new patch: {:?}", &patches[1]); + log::info!("initial chars: {:?}", initial_chars); + log::info!("final chars: {:?}", expected_chars); + + // Compose the patches, and verify that it has the same effect as applying the + // two patches separately. + let composed = patches[0].compose(&patches[1]); + log::info!("composed patch: {:?}", &composed); + + let mut actual_chars = initial_chars.clone(); + for edit in composed.0 { + actual_chars.splice( + edit.new.start as usize..edit.new.start as usize + edit.old.len(), + expected_chars[edit.new.start as usize..edit.new.end as usize] + .iter() + .copied(), + ); + } + + assert_eq!(actual_chars, expected_chars); + } + + #[track_caller] + fn assert_patch_composition(old: Patch, new: Patch, composed: Patch) { + let original = ('a'..'z').collect::>(); + let inserted = ('A'..'Z').collect::>(); + + let mut expected = original.clone(); + apply_patch(&mut expected, &old, &inserted); + apply_patch(&mut expected, &new, &inserted); + + let mut actual = original.clone(); + apply_patch(&mut actual, &composed, &expected); + assert_eq!( + actual.into_iter().collect::(), + expected.into_iter().collect::(), + "expected patch is incorrect" + ); + + assert_eq!(old.compose(&new), composed); + } + + fn apply_patch(text: &mut Vec, patch: &Patch, new_text: &[char]) { + for edit in patch.0.iter().rev() { + text.splice( + edit.old.start as usize..edit.old.end as usize, + new_text[edit.new.start as usize..edit.new.end as usize] + .iter() + .copied(), + ); + } + } +} diff --git a/crates/editor/src/display_map/tab_map.rs b/crates/editor/src/display_map/tab_map.rs index 93fae6d6b2..675ce9132e 100644 --- a/crates/editor/src/display_map/tab_map.rs +++ b/crates/editor/src/display_map/tab_map.rs @@ -1,8 +1,10 @@ -use super::fold_map::{self, FoldEdit, FoldPoint, Snapshot as FoldSnapshot}; -use language::{rope, HighlightedChunk}; +use super::fold_map::{self, FoldEdit, FoldPoint, Snapshot as FoldSnapshot, ToFoldPoint}; +use buffer::Point; +use language::{rope, Chunk}; use parking_lot::Mutex; -use std::{mem, ops::Range}; +use std::{cmp, mem, ops::Range}; use sum_tree::Bias; +use theme::SyntaxTheme; pub struct TabMap(Mutex); @@ -21,6 +23,7 @@ impl TabMap { mut fold_edits: Vec, ) -> (Snapshot, Vec) { let mut old_snapshot = self.0.lock(); + let max_offset = old_snapshot.fold_snapshot.len(); let new_snapshot = Snapshot { fold_snapshot, tab_size: old_snapshot.tab_size, @@ -31,11 +34,11 @@ impl TabMap { let mut delta = 0; for chunk in old_snapshot .fold_snapshot - .chunks_at(fold_edit.old_bytes.end) + .chunks(fold_edit.old_bytes.end..max_offset, None) { let patterns: &[_] = &['\t', '\n']; - if let Some(ix) = chunk.find(patterns) { - if &chunk[ix..ix + 1] == "\t" { + if let Some(ix) = chunk.text.find(patterns) { + if &chunk.text[ix..ix + 1] == "\t" { fold_edit.old_bytes.end.0 += delta + ix + 1; fold_edit.new_bytes.end.0 += delta + ix + 1; } @@ -43,7 +46,7 @@ impl TabMap { break; } - delta += chunk.len(); + delta += chunk.text.len(); } } @@ -108,28 +111,31 @@ impl Snapshot { .text_summary_for_range(input_start..input_end); let mut first_line_chars = 0; - let mut first_line_bytes = 0; - for c in self.chunks_at(range.start).flat_map(|chunk| chunk.chars()) { - if c == '\n' - || (range.start.row() == range.end.row() && first_line_bytes == range.end.column()) - { + let line_end = if range.start.row() == range.end.row() { + range.end + } else { + self.max_point() + }; + for c in self + .chunks(range.start..line_end, None) + .flat_map(|chunk| chunk.text.chars()) + { + if c == '\n' { break; } first_line_chars += 1; - first_line_bytes += c.len_utf8() as u32; } let mut last_line_chars = 0; - let mut last_line_bytes = 0; - for c in self - .chunks_at(TabPoint::new(range.end.row(), 0).max(range.start)) - .flat_map(|chunk| chunk.chars()) - { - if last_line_bytes == range.end.column() { - break; + if range.start.row() == range.end.row() { + last_line_chars = first_line_chars; + } else { + for _ in self + .chunks(TabPoint::new(range.end.row(), 0)..range.end, None) + .flat_map(|chunk| chunk.text.chars()) + { + last_line_chars += 1; } - last_line_chars += 1; - last_line_bytes += c.len_utf8() as u32; } TextSummary { @@ -145,21 +151,11 @@ impl Snapshot { self.fold_snapshot.version } - pub fn chunks_at(&self, point: TabPoint) -> Chunks { - let (point, expanded_char_column, to_next_stop) = self.to_fold_point(point, Bias::Left); - let fold_chunks = self - .fold_snapshot - .chunks_at(point.to_offset(&self.fold_snapshot)); - Chunks { - fold_chunks, - column: expanded_char_column, - tab_size: self.tab_size, - chunk: &SPACES[0..to_next_stop], - skip_leading_tab: to_next_stop > 0, - } - } - - pub fn highlighted_chunks(&mut self, range: Range) -> HighlightedChunks { + pub fn chunks<'a>( + &'a self, + range: Range, + theme: Option<&'a SyntaxTheme>, + ) -> Chunks<'a> { let (input_start, expanded_char_column, to_next_stop) = self.to_fold_point(range.start, Bias::Left); let input_start = input_start.to_offset(&self.fold_snapshot); @@ -167,13 +163,19 @@ impl Snapshot { .to_fold_point(range.end, Bias::Right) .0 .to_offset(&self.fold_snapshot); - HighlightedChunks { - fold_chunks: self - .fold_snapshot - .highlighted_chunks(input_start..input_end), + let to_next_stop = if range.start.0 + Point::new(0, to_next_stop as u32) > range.end.0 { + (range.end.column() - range.start.column()) as usize + } else { + to_next_stop + }; + + Chunks { + fold_chunks: self.fold_snapshot.chunks(input_start..input_end, theme), column: expanded_char_column, + output_position: range.start.0, + max_output_position: range.end.0, tab_size: self.tab_size, - chunk: HighlightedChunk { + chunk: Chunk { text: &SPACES[0..to_next_stop], ..Default::default() }, @@ -187,7 +189,9 @@ impl Snapshot { #[cfg(test)] pub fn text(&self) -> String { - self.chunks_at(Default::default()).collect() + self.chunks(TabPoint::zero()..self.max_point(), None) + .map(|chunk| chunk.text) + .collect() } pub fn max_point(&self) -> TabPoint { @@ -207,6 +211,10 @@ impl Snapshot { TabPoint::new(input.row(), expanded as u32) } + pub fn from_point(&self, point: Point, bias: Bias) -> TabPoint { + self.to_tab_point(point.to_fold_point(&self.fold_snapshot, bias)) + } + pub fn to_fold_point(&self, output: TabPoint, bias: Bias) -> (FoldPoint, usize, usize) { let chars = self.fold_snapshot.chars_at(FoldPoint::new(output.row(), 0)); let expanded = output.column() as usize; @@ -219,6 +227,12 @@ impl Snapshot { ) } + pub fn to_point(&self, point: TabPoint, bias: Bias) -> Point { + self.to_fold_point(point, bias) + .0 + .to_buffer_point(&self.fold_snapshot) + } + fn expand_tabs(chars: impl Iterator, column: usize, tab_size: usize) -> usize { let mut expanded_chars = 0; let mut expanded_bytes = 0; @@ -368,63 +382,16 @@ const SPACES: &'static str = " "; pub struct Chunks<'a> { fold_chunks: fold_map::Chunks<'a>, - chunk: &'a str, + chunk: Chunk<'a>, column: usize, + output_position: Point, + max_output_position: Point, tab_size: usize, skip_leading_tab: bool, } impl<'a> Iterator for Chunks<'a> { - type Item = &'a str; - - fn next(&mut self) -> Option { - if self.chunk.is_empty() { - if let Some(chunk) = self.fold_chunks.next() { - self.chunk = chunk; - if self.skip_leading_tab { - self.chunk = &self.chunk[1..]; - self.skip_leading_tab = false; - } - } else { - return None; - } - } - - for (ix, c) in self.chunk.char_indices() { - match c { - '\t' => { - if ix > 0 { - let (prefix, suffix) = self.chunk.split_at(ix); - self.chunk = suffix; - return Some(prefix); - } else { - self.chunk = &self.chunk[1..]; - let len = self.tab_size - self.column % self.tab_size; - self.column += len; - return Some(&SPACES[0..len]); - } - } - '\n' => self.column = 0, - _ => self.column += 1, - } - } - - let result = Some(self.chunk); - self.chunk = ""; - result - } -} - -pub struct HighlightedChunks<'a> { - fold_chunks: fold_map::HighlightedChunks<'a>, - chunk: HighlightedChunk<'a>, - column: usize, - tab_size: usize, - skip_leading_tab: bool, -} - -impl<'a> Iterator for HighlightedChunks<'a> { - type Item = HighlightedChunk<'a>; + type Item = Chunk<'a>; fn next(&mut self) -> Option { if self.chunk.text.is_empty() { @@ -445,22 +412,34 @@ impl<'a> Iterator for HighlightedChunks<'a> { if ix > 0 { let (prefix, suffix) = self.chunk.text.split_at(ix); self.chunk.text = suffix; - return Some(HighlightedChunk { + return Some(Chunk { text: prefix, ..self.chunk }); } else { self.chunk.text = &self.chunk.text[1..]; - let len = self.tab_size - self.column % self.tab_size; + let mut len = self.tab_size - self.column % self.tab_size; + let next_output_position = cmp::min( + self.output_position + Point::new(0, len as u32), + self.max_output_position, + ); + len = (next_output_position.column - self.output_position.column) as usize; self.column += len; - return Some(HighlightedChunk { + self.output_position = next_output_position; + return Some(Chunk { text: &SPACES[0..len], ..self.chunk }); } } - '\n' => self.column = 0, - _ => self.column += 1, + '\n' => { + self.column = 0; + self.output_position += Point::new(1, 0); + } + _ => { + self.column += 1; + self.output_position.column += c.len_utf8() as u32; + } } } @@ -471,6 +450,10 @@ impl<'a> Iterator for HighlightedChunks<'a> { #[cfg(test)] mod tests { use super::*; + use crate::display_map::fold_map::FoldMap; + use buffer::{RandomCharIter, Rope}; + use language::Buffer; + use rand::{prelude::StdRng, Rng}; #[test] fn test_expand_tabs() { @@ -478,4 +461,62 @@ mod tests { assert_eq!(Snapshot::expand_tabs("\t".chars(), 1, 4), 4); assert_eq!(Snapshot::expand_tabs("\ta".chars(), 2, 4), 5); } + + #[gpui::test(iterations = 100)] + fn test_random(cx: &mut gpui::MutableAppContext, mut rng: StdRng) { + let tab_size = rng.gen_range(1..=4); + let buffer = cx.add_model(|cx| { + let len = rng.gen_range(0..30); + let text = RandomCharIter::new(&mut rng).take(len).collect::(); + Buffer::new(0, text, cx) + }); + log::info!("Buffer text: {:?}", buffer.read(cx).text()); + + let (mut fold_map, _) = FoldMap::new(buffer.clone(), cx); + fold_map.randomly_mutate(&mut rng, cx); + let (folds_snapshot, _) = fold_map.read(cx); + log::info!("FoldMap text: {:?}", folds_snapshot.text()); + + let (_, tabs_snapshot) = TabMap::new(folds_snapshot.clone(), tab_size); + let text = Rope::from(tabs_snapshot.text().as_str()); + log::info!( + "TabMap text (tab size: {}): {:?}", + tab_size, + tabs_snapshot.text(), + ); + + for _ in 0..5 { + let end_row = rng.gen_range(0..=text.max_point().row); + let end_column = rng.gen_range(0..=text.line_len(end_row)); + let mut end = TabPoint(text.clip_point(Point::new(end_row, end_column), Bias::Right)); + let start_row = rng.gen_range(0..=text.max_point().row); + let start_column = rng.gen_range(0..=text.line_len(start_row)); + let mut start = + TabPoint(text.clip_point(Point::new(start_row, start_column), Bias::Left)); + if start > end { + mem::swap(&mut start, &mut end); + } + + let expected_text = text + .chunks_in_range(text.point_to_offset(start.0)..text.point_to_offset(end.0)) + .collect::(); + let expected_summary = TextSummary::from(expected_text.as_str()); + log::info!("slicing {:?}..{:?} (text: {:?})", start, end, text); + assert_eq!( + expected_text, + tabs_snapshot + .chunks(start..end, None) + .map(|c| c.text) + .collect::() + ); + + let mut actual_summary = tabs_snapshot.text_summary_for_range(start..end); + if tab_size > 1 && folds_snapshot.text().contains('\t') { + actual_summary.longest_row = expected_summary.longest_row; + actual_summary.longest_row_chars = expected_summary.longest_row_chars; + } + + assert_eq!(actual_summary, expected_summary,); + } + } } diff --git a/crates/editor/src/display_map/wrap_map.rs b/crates/editor/src/display_map/wrap_map.rs index a62c67dbce..d84b25936a 100644 --- a/crates/editor/src/display_map/wrap_map.rs +++ b/crates/editor/src/display_map/wrap_map.rs @@ -1,17 +1,28 @@ use super::{ fold_map, - tab_map::{self, Edit as TabEdit, Snapshot as TabSnapshot, TabPoint, TextSummary}, + patch::Patch, + tab_map::{self, Edit as TabEdit, Snapshot as TabSnapshot, TabPoint}, + DisplayRow, }; -use gpui::{fonts::FontId, text_layout::LineWrapper, Entity, ModelContext, Task}; -use language::{HighlightedChunk, Point}; +use gpui::{ + fonts::FontId, text_layout::LineWrapper, Entity, ModelContext, ModelHandle, MutableAppContext, + Task, +}; +use language::{Chunk, Point}; use lazy_static::lazy_static; use smol::future::yield_now; -use std::{collections::VecDeque, ops::Range, time::Duration}; +use std::{collections::VecDeque, mem, ops::Range, time::Duration}; use sum_tree::{Bias, Cursor, SumTree}; +use theme::SyntaxTheme; + +pub use super::tab_map::TextSummary; +pub type Edit = buffer::Edit; pub struct WrapMap { snapshot: Snapshot, pending_edits: VecDeque<(TabSnapshot, Vec)>, + interpolated_edits: Patch, + edits_since_sync: Patch, wrap_width: Option, background_task: Option>, font: (FontId, f32), @@ -41,18 +52,11 @@ struct TransformSummary { } #[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)] -pub struct WrapPoint(super::Point); +pub struct WrapPoint(pub super::Point); pub struct Chunks<'a> { input_chunks: tab_map::Chunks<'a>, - input_chunk: &'a str, - output_position: WrapPoint, - transforms: Cursor<'a, Transform, (WrapPoint, TabPoint)>, -} - -pub struct HighlightedChunks<'a> { - input_chunks: tab_map::HighlightedChunks<'a>, - input_chunk: HighlightedChunk<'a>, + input_chunk: Chunk<'a>, output_position: WrapPoint, max_output_row: u32, transforms: Cursor<'a, Transform, (WrapPoint, TabPoint)>, @@ -73,18 +77,24 @@ impl WrapMap { font_id: FontId, font_size: f32, wrap_width: Option, - cx: &mut ModelContext, - ) -> Self { - let mut this = Self { - font: (font_id, font_size), - wrap_width: None, - pending_edits: Default::default(), - snapshot: Snapshot::new(tab_snapshot), - background_task: None, - }; - this.set_wrap_width(wrap_width, cx); - - this + cx: &mut MutableAppContext, + ) -> (ModelHandle, Snapshot) { + let handle = cx.add_model(|cx| { + let mut this = Self { + font: (font_id, font_size), + wrap_width: None, + pending_edits: Default::default(), + interpolated_edits: Default::default(), + edits_since_sync: Default::default(), + snapshot: Snapshot::new(tab_snapshot), + background_task: None, + }; + this.set_wrap_width(wrap_width, cx); + mem::take(&mut this.edits_since_sync); + this + }); + let snapshot = handle.read(cx).snapshot.clone(); + (handle, snapshot) } #[cfg(test)] @@ -97,10 +107,13 @@ impl WrapMap { tab_snapshot: TabSnapshot, edits: Vec, cx: &mut ModelContext, - ) -> Snapshot { + ) -> (Snapshot, Vec) { self.pending_edits.push_back((tab_snapshot, edits)); self.flush_edits(cx); - self.snapshot.clone() + ( + self.snapshot.clone(), + mem::take(&mut self.edits_since_sync).into_inner(), + ) } pub fn set_font(&mut self, font_id: FontId, font_size: f32, cx: &mut ModelContext) { @@ -122,6 +135,8 @@ impl WrapMap { fn rewrap(&mut self, cx: &mut ModelContext) { self.background_task.take(); + self.interpolated_edits.clear(); + self.pending_edits.clear(); if let Some(wrap_width) = self.wrap_width { let mut new_snapshot = self.snapshot.clone(); @@ -131,7 +146,7 @@ impl WrapMap { let mut line_wrapper = font_cache.line_wrapper(font_id, font_size); let tab_snapshot = new_snapshot.tab_snapshot.clone(); let range = TabPoint::zero()..tab_snapshot.max_point(); - new_snapshot + let edits = new_snapshot .update( tab_snapshot, &[TabEdit { @@ -142,22 +157,27 @@ impl WrapMap { &mut line_wrapper, ) .await; - new_snapshot + (new_snapshot, edits) }); match cx .background() .block_with_timeout(Duration::from_millis(5), task) { - Ok(snapshot) => { + Ok((snapshot, edits)) => { self.snapshot = snapshot; + self.edits_since_sync = self.edits_since_sync.compose(&edits); cx.notify(); } Err(wrap_task) => { self.background_task = Some(cx.spawn(|this, mut cx| async move { - let snapshot = wrap_task.await; + let (snapshot, edits) = wrap_task.await; this.update(&mut cx, |this, cx| { this.snapshot = snapshot; + this.edits_since_sync = this + .edits_since_sync + .compose(mem::take(&mut this.interpolated_edits).invert()) + .compose(&edits); this.background_task = None; this.flush_edits(cx); cx.notify(); @@ -166,6 +186,7 @@ impl WrapMap { } } } else { + let old_rows = self.snapshot.transforms.summary().output.lines.row + 1; self.snapshot.transforms = SumTree::new(); let summary = self.snapshot.tab_snapshot.text_summary(); if !summary.lines.is_zero() { @@ -173,6 +194,14 @@ impl WrapMap { .transforms .push(Transform::isomorphic(summary), &()); } + let new_rows = self.snapshot.transforms.summary().output.lines.row + 1; + self.snapshot.interpolated = false; + self.edits_since_sync = self.edits_since_sync.compose(&unsafe { + Patch::new_unchecked(vec![Edit { + old: 0..old_rows, + new: 0..new_rows, + }]) + }); } } @@ -202,26 +231,33 @@ impl WrapMap { let update_task = cx.background().spawn(async move { let mut line_wrapper = font_cache.line_wrapper(font_id, font_size); - for (tab_snapshot, edits) in pending_edits { - snapshot - .update(tab_snapshot, &edits, wrap_width, &mut line_wrapper) + let mut edits = Patch::default(); + for (tab_snapshot, tab_edits) in pending_edits { + let wrap_edits = snapshot + .update(tab_snapshot, &tab_edits, wrap_width, &mut line_wrapper) .await; + edits = edits.compose(&wrap_edits); } - snapshot + (snapshot, edits) }); match cx .background() .block_with_timeout(Duration::from_millis(1), update_task) { - Ok(snapshot) => { + Ok((snapshot, output_edits)) => { self.snapshot = snapshot; + self.edits_since_sync = self.edits_since_sync.compose(&output_edits); } Err(update_task) => { self.background_task = Some(cx.spawn(|this, mut cx| async move { - let snapshot = update_task.await; + let (snapshot, edits) = update_task.await; this.update(&mut cx, |this, cx| { this.snapshot = snapshot; + this.edits_since_sync = this + .edits_since_sync + .compose(mem::take(&mut this.interpolated_edits).invert()) + .compose(&edits); this.background_task = None; this.flush_edits(cx); cx.notify(); @@ -238,7 +274,9 @@ impl WrapMap { if tab_snapshot.version() <= self.snapshot.tab_snapshot.version() { to_remove_len += 1; } else { - self.snapshot.interpolate(tab_snapshot.clone(), &edits); + let interpolated_edits = self.snapshot.interpolate(tab_snapshot.clone(), &edits); + self.edits_since_sync = self.edits_since_sync.compose(&interpolated_edits); + self.interpolated_edits = self.interpolated_edits.compose(&interpolated_edits); } } @@ -262,17 +300,21 @@ impl Snapshot { } } - fn interpolate(&mut self, new_tab_snapshot: TabSnapshot, edits: &[TabEdit]) { + fn interpolate(&mut self, new_tab_snapshot: TabSnapshot, tab_edits: &[TabEdit]) -> Patch { let mut new_transforms; - if edits.is_empty() { + if tab_edits.is_empty() { new_transforms = self.transforms.clone(); } else { let mut old_cursor = self.transforms.cursor::(); - let mut edits = edits.into_iter().peekable(); - new_transforms = - old_cursor.slice(&edits.peek().unwrap().old_lines.start, Bias::Right, &()); - while let Some(edit) = edits.next() { + let mut tab_edits_iter = tab_edits.iter().peekable(); + new_transforms = old_cursor.slice( + &tab_edits_iter.peek().unwrap().old_lines.start, + Bias::Right, + &(), + ); + + while let Some(edit) = tab_edits_iter.next() { if edit.new_lines.start > TabPoint::from(new_transforms.summary().input.lines) { let summary = new_tab_snapshot.text_summary_for_range( TabPoint::from(new_transforms.summary().input.lines)..edit.new_lines.start, @@ -287,7 +329,7 @@ impl Snapshot { } old_cursor.seek_forward(&edit.old_lines.end, Bias::Right, &()); - if let Some(next_edit) = edits.peek() { + if let Some(next_edit) = tab_edits_iter.peek() { if next_edit.old_lines.start > old_cursor.end(&()) { if old_cursor.end(&()) > edit.old_lines.end { let summary = self @@ -295,6 +337,7 @@ impl Snapshot { .text_summary_for_range(edit.old_lines.end..old_cursor.end(&())); new_transforms.push_or_extend(Transform::isomorphic(summary)); } + old_cursor.next(&()); new_transforms.push_tree( old_cursor.slice(&next_edit.old_lines.start, Bias::Right, &()), @@ -314,38 +357,44 @@ impl Snapshot { } } - self.transforms = new_transforms; - self.tab_snapshot = new_tab_snapshot; - self.interpolated = true; + let old_snapshot = mem::replace( + self, + Snapshot { + tab_snapshot: new_tab_snapshot, + transforms: new_transforms, + interpolated: true, + }, + ); self.check_invariants(); + old_snapshot.compute_edits(tab_edits, self) } async fn update( &mut self, new_tab_snapshot: TabSnapshot, - edits: &[TabEdit], + tab_edits: &[TabEdit], wrap_width: f32, line_wrapper: &mut LineWrapper, - ) { + ) -> Patch { #[derive(Debug)] struct RowEdit { old_rows: Range, new_rows: Range, } - let mut edits = edits.into_iter().peekable(); + let mut tab_edits_iter = tab_edits.into_iter().peekable(); let mut row_edits = Vec::new(); - while let Some(edit) = edits.next() { + while let Some(edit) = tab_edits_iter.next() { let mut row_edit = RowEdit { old_rows: edit.old_lines.start.row()..edit.old_lines.end.row() + 1, new_rows: edit.new_lines.start.row()..edit.new_lines.end.row() + 1, }; - while let Some(next_edit) = edits.peek() { + while let Some(next_edit) = tab_edits_iter.peek() { if next_edit.old_lines.start.row() <= row_edit.old_rows.end { row_edit.old_rows.end = next_edit.old_lines.end.row() + 1; row_edit.new_rows.end = next_edit.new_lines.end.row() + 1; - edits.next(); + tab_edits_iter.next(); } else { break; } @@ -370,7 +419,7 @@ impl Snapshot { while let Some(edit) = row_edits.next() { if edit.new_rows.start > new_transforms.summary().input.lines.row { let summary = new_tab_snapshot.text_summary_for_range( - TabPoint::new(new_transforms.summary().input.lines.row, 0) + TabPoint(new_transforms.summary().input.lines) ..TabPoint::new(edit.new_rows.start, 0), ); new_transforms.push_or_extend(Transform::isomorphic(summary)); @@ -378,10 +427,15 @@ impl Snapshot { let mut line = String::new(); let mut remaining = None; - let mut chunks = new_tab_snapshot.chunks_at(TabPoint::new(edit.new_rows.start, 0)); + let mut chunks = new_tab_snapshot.chunks( + TabPoint::new(edit.new_rows.start, 0)..new_tab_snapshot.max_point(), + None, + ); let mut edit_transforms = Vec::::new(); for _ in edit.new_rows.start..edit.new_rows.end { - while let Some(chunk) = remaining.take().or_else(|| chunks.next()) { + while let Some(chunk) = + remaining.take().or_else(|| chunks.next().map(|c| c.text)) + { if let Some(ix) = chunk.find('\n') { line.push_str(&chunk[..ix + 1]); remaining = Some(&chunk[ix + 1..]); @@ -452,30 +506,60 @@ impl Snapshot { } } - self.transforms = new_transforms; - self.tab_snapshot = new_tab_snapshot; - self.interpolated = false; + let old_snapshot = mem::replace( + self, + Snapshot { + tab_snapshot: new_tab_snapshot, + transforms: new_transforms, + interpolated: false, + }, + ); self.check_invariants(); + old_snapshot.compute_edits(tab_edits, self) } - pub fn chunks_at(&self, wrap_row: u32) -> Chunks { - let point = WrapPoint::new(wrap_row, 0); - let mut transforms = self.transforms.cursor::<(WrapPoint, TabPoint)>(); - transforms.seek(&point, Bias::Right, &()); - let mut input_position = TabPoint(transforms.start().1 .0); - if transforms.item().map_or(false, |t| t.is_isomorphic()) { - input_position.0 += point.0 - transforms.start().0 .0; - } - let input_chunks = self.tab_snapshot.chunks_at(input_position); - Chunks { - input_chunks, - transforms, - output_position: point, - input_chunk: "", + fn compute_edits(&self, tab_edits: &[TabEdit], new_snapshot: &Snapshot) -> Patch { + let mut wrap_edits = Vec::new(); + let mut old_cursor = self.transforms.cursor::(); + let mut new_cursor = new_snapshot.transforms.cursor::(); + for mut tab_edit in tab_edits.iter().cloned() { + tab_edit.old_lines.start.0.column = 0; + tab_edit.old_lines.end.0 += Point::new(1, 0); + tab_edit.new_lines.start.0.column = 0; + tab_edit.new_lines.end.0 += Point::new(1, 0); + + old_cursor.seek(&tab_edit.old_lines.start, Bias::Right, &()); + let mut old_start = old_cursor.start().output.lines; + old_start += tab_edit.old_lines.start.0 - old_cursor.start().input.lines; + + old_cursor.seek(&tab_edit.old_lines.end, Bias::Right, &()); + let mut old_end = old_cursor.start().output.lines; + old_end += tab_edit.old_lines.end.0 - old_cursor.start().input.lines; + + new_cursor.seek(&tab_edit.new_lines.start, Bias::Right, &()); + let mut new_start = new_cursor.start().output.lines; + new_start += tab_edit.new_lines.start.0 - new_cursor.start().input.lines; + + new_cursor.seek(&tab_edit.new_lines.end, Bias::Right, &()); + let mut new_end = new_cursor.start().output.lines; + new_end += tab_edit.new_lines.end.0 - new_cursor.start().input.lines; + + wrap_edits.push(Edit { + old: old_start.row..old_end.row, + new: new_start.row..new_end.row, + }); } + + consolidate_wrap_edits(&mut wrap_edits); + unsafe { Patch::new_unchecked(wrap_edits) } } - pub fn highlighted_chunks_for_rows(&mut self, rows: Range) -> HighlightedChunks { + pub fn text_chunks(&self, wrap_row: u32) -> impl Iterator { + self.chunks(wrap_row..self.max_point().row() + 1, None) + .map(|h| h.text) + } + + pub fn chunks<'a>(&'a self, rows: Range, theme: Option<&'a SyntaxTheme>) -> Chunks<'a> { let output_start = WrapPoint::new(rows.start, 0); let output_end = WrapPoint::new(rows.end, 0); let mut transforms = self.transforms.cursor::<(WrapPoint, TabPoint)>(); @@ -487,8 +571,8 @@ impl Snapshot { let input_end = self .to_tab_point(output_end) .min(self.tab_snapshot.max_point()); - HighlightedChunks { - input_chunks: self.tab_snapshot.highlighted_chunks(input_start..input_end), + Chunks { + input_chunks: self.tab_snapshot.chunks(input_start..input_end, theme), input_chunk: Default::default(), output_position: output_start, max_output_row: rows.end, @@ -496,13 +580,17 @@ impl Snapshot { } } + pub fn text_summary(&self) -> TextSummary { + self.transforms.summary().output + } + pub fn max_point(&self) -> WrapPoint { - self.to_wrap_point(self.tab_snapshot.max_point()) + WrapPoint(self.transforms.summary().output.lines) } pub fn line_len(&self, row: u32) -> u32 { let mut len = 0; - for chunk in self.chunks_at(row) { + for chunk in self.text_chunks(row) { if let Some(newline_ix) = chunk.find('\n') { len += newline_ix; break; @@ -513,6 +601,13 @@ impl Snapshot { len as u32 } + pub fn line_char_count(&self, row: u32) -> u32 { + self.text_chunks(row) + .flat_map(|c| c.chars()) + .take_while(|c| *c != '\n') + .count() as u32 + } + pub fn soft_wrap_indent(&self, row: u32) -> Option { let mut cursor = self.transforms.cursor::(); cursor.seek(&WrapPoint::new(row + 1, 0), Bias::Right, &()); @@ -559,7 +654,15 @@ impl Snapshot { TabPoint(tab_point) } - pub fn to_wrap_point(&self, point: TabPoint) -> WrapPoint { + pub fn to_point(&self, point: WrapPoint, bias: Bias) -> Point { + self.tab_snapshot.to_point(self.to_tab_point(point), bias) + } + + pub fn from_point(&self, point: Point, bias: Bias) -> WrapPoint { + self.from_tab_point(self.tab_snapshot.from_point(point, bias)) + } + + pub fn from_tab_point(&self, point: TabPoint) -> WrapPoint { let mut cursor = self.transforms.cursor::<(TabPoint, WrapPoint)>(); cursor.seek(&point, Bias::Right, &()); WrapPoint(cursor.start().1 .0 + (point.0 - cursor.start().0 .0)) @@ -575,7 +678,7 @@ impl Snapshot { } } - self.to_wrap_point(self.tab_snapshot.clip_point(self.to_tab_point(point), bias)) + self.from_tab_point(self.tab_snapshot.clip_point(self.to_tab_point(point), bias)) } fn check_invariants(&self) { @@ -610,7 +713,11 @@ impl Snapshot { prev_tab_row = tab_point.row(); soft_wrapped = false; } - expected_buffer_rows.push((buffer_row, soft_wrapped)); + expected_buffer_rows.push(if soft_wrapped { + DisplayRow::Wrap + } else { + DisplayRow::Buffer(buffer_row) + }); } for start_display_row in 0..expected_buffer_rows.len() { @@ -627,52 +734,7 @@ impl Snapshot { } impl<'a> Iterator for Chunks<'a> { - type Item = &'a str; - - fn next(&mut self) -> Option { - let transform = self.transforms.item()?; - if let Some(display_text) = transform.display_text { - if self.output_position > self.transforms.start().0 { - self.output_position.0.column += transform.summary.output.lines.column; - self.transforms.next(&()); - return Some(&display_text[1..]); - } else { - self.output_position.0 += transform.summary.output.lines; - self.transforms.next(&()); - return Some(display_text); - } - } - - if self.input_chunk.is_empty() { - self.input_chunk = self.input_chunks.next().unwrap(); - } - - let mut input_len = 0; - let transform_end = self.transforms.end(&()).0; - for c in self.input_chunk.chars() { - let char_len = c.len_utf8(); - input_len += char_len; - if c == '\n' { - *self.output_position.row_mut() += 1; - *self.output_position.column_mut() = 0; - } else { - *self.output_position.column_mut() += char_len as u32; - } - - if self.output_position >= transform_end { - self.transforms.next(&()); - break; - } - } - - let (prefix, suffix) = self.input_chunk.split_at(input_len); - self.input_chunk = suffix; - Some(prefix) - } -} - -impl<'a> Iterator for HighlightedChunks<'a> { - type Item = HighlightedChunk<'a>; + type Item = Chunk<'a>; fn next(&mut self) -> Option { if self.output_position.row() >= self.max_output_row { @@ -697,7 +759,7 @@ impl<'a> Iterator for HighlightedChunks<'a> { self.output_position.0 += summary; self.transforms.next(&()); - return Some(HighlightedChunk { + return Some(Chunk { text: &display_text[start_ix..end_ix], ..self.input_chunk }); @@ -727,7 +789,7 @@ impl<'a> Iterator for HighlightedChunks<'a> { let (prefix, suffix) = self.input_chunk.text.split_at(input_len); self.input_chunk.text = suffix; - Some(HighlightedChunk { + Some(Chunk { text: prefix, ..self.input_chunk }) @@ -735,7 +797,7 @@ impl<'a> Iterator for HighlightedChunks<'a> { } impl<'a> Iterator for BufferRows<'a> { - type Item = (u32, bool); + type Item = DisplayRow; fn next(&mut self) -> Option { if self.output_row > self.max_output_row { @@ -755,7 +817,11 @@ impl<'a> Iterator for BufferRows<'a> { self.soft_wrapped = true; } - Some((buffer_row, soft_wrapped)) + Some(if soft_wrapped { + DisplayRow::Wrap + } else { + DisplayRow::Buffer(buffer_row) + }) } } @@ -851,23 +917,18 @@ impl WrapPoint { Self(super::Point::new(row, column)) } - #[cfg(test)] - pub fn is_zero(&self) -> bool { - self.0.is_zero() - } - pub fn row(self) -> u32 { self.0.row } - pub fn column(self) -> u32 { - self.0.column - } - pub fn row_mut(&mut self) -> &mut u32 { &mut self.0.row } + pub fn column(&self) -> u32 { + self.0.column + } + pub fn column_mut(&mut self) -> &mut u32 { &mut self.0.column } @@ -888,12 +949,33 @@ impl<'a> sum_tree::Dimension<'a, TransformSummary> for TabPoint { } } +impl<'a> sum_tree::SeekTarget<'a, TransformSummary, TransformSummary> for TabPoint { + fn cmp(&self, cursor_location: &TransformSummary, _: &()) -> std::cmp::Ordering { + Ord::cmp(&self.0, &cursor_location.input.lines) + } +} + impl<'a> sum_tree::Dimension<'a, TransformSummary> for WrapPoint { fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) { self.0 += summary.output.lines; } } +fn consolidate_wrap_edits(edits: &mut Vec) { + let mut i = 1; + while i < edits.len() { + let edit = edits[i].clone(); + let prev_edit = &mut edits[i - 1]; + if prev_edit.old.end >= edit.old.start { + prev_edit.old.end = edit.old.end; + prev_edit.new.end = edit.new.end; + edits.remove(i); + continue; + } + i += 1; + } +} + #[cfg(test)] mod tests { use super::*; @@ -901,9 +983,10 @@ mod tests { display_map::{fold_map::FoldMap, tab_map::TabMap}, test::Observer, }; + use buffer::Rope; use language::{Buffer, RandomCharIter}; use rand::prelude::*; - use std::env; + use std::{cmp, env}; #[gpui::test(iterations = 100)] async fn test_random_wraps(mut cx: gpui::TestAppContext, mut rng: StdRng) { @@ -951,17 +1034,20 @@ mod tests { let unwrapped_text = tabs_snapshot.text(); let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper); - let wrap_map = cx.add_model(|cx| { - WrapMap::new(tabs_snapshot.clone(), font_id, font_size, wrap_width, cx) - }); + let (wrap_map, _) = + cx.update(|cx| WrapMap::new(tabs_snapshot.clone(), font_id, font_size, wrap_width, cx)); let (_observer, notifications) = Observer::new(&wrap_map, &mut cx); if wrap_map.read_with(&cx, |map, _| map.is_rewrapping()) { notifications.recv().await.unwrap(); } - let snapshot = wrap_map.update(&mut cx, |map, cx| map.sync(tabs_snapshot, Vec::new(), cx)); - let actual_text = snapshot.text(); + let (initial_snapshot, _) = wrap_map.update(&mut cx, |map, cx| { + assert!(!map.is_rewrapping()); + map.sync(tabs_snapshot.clone(), Vec::new(), cx) + }); + + let actual_text = initial_snapshot.text(); assert_eq!( actual_text, expected_text, "unwrapped text is: {:?}", @@ -969,7 +1055,10 @@ mod tests { ); log::info!("Wrapped text: {:?}", actual_text); + let mut edits = Vec::new(); for _i in 0..operations { + log::info!("{} ==============================================", _i); + match rng.gen_range(0..=100) { 0..=19 => { wrap_width = if rng.gen_bool(0.2) { @@ -981,14 +1070,15 @@ mod tests { wrap_map.update(&mut cx, |map, cx| map.set_wrap_width(wrap_width, cx)); } 20..=39 => { - for (folds_snapshot, edits) in + for (folds_snapshot, fold_edits) in cx.read(|cx| fold_map.randomly_mutate(&mut rng, cx)) { - let (tabs_snapshot, edits) = tab_map.sync(folds_snapshot, edits); - let mut snapshot = - wrap_map.update(&mut cx, |map, cx| map.sync(tabs_snapshot, edits, cx)); + let (tabs_snapshot, tab_edits) = tab_map.sync(folds_snapshot, fold_edits); + let (mut snapshot, wrap_edits) = wrap_map + .update(&mut cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx)); snapshot.check_invariants(); snapshot.verify_chunks(&mut rng); + edits.push((snapshot, wrap_edits)); } } _ => { @@ -1000,21 +1090,22 @@ mod tests { "Unwrapped text (no folds): {:?}", buffer.read_with(&cx, |buf, _| buf.text()) ); - let (folds_snapshot, edits) = cx.read(|cx| fold_map.read(cx)); + let (folds_snapshot, fold_edits) = cx.read(|cx| fold_map.read(cx)); log::info!( "Unwrapped text (unexpanded tabs): {:?}", folds_snapshot.text() ); - let (tabs_snapshot, edits) = tab_map.sync(folds_snapshot, edits); + let (tabs_snapshot, tab_edits) = tab_map.sync(folds_snapshot, fold_edits); log::info!("Unwrapped text (expanded tabs): {:?}", tabs_snapshot.text()); let unwrapped_text = tabs_snapshot.text(); let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper); - let mut snapshot = wrap_map.update(&mut cx, |map, cx| { - map.sync(tabs_snapshot.clone(), edits, cx) + let (mut snapshot, wrap_edits) = wrap_map.update(&mut cx, |map, cx| { + map.sync(tabs_snapshot.clone(), tab_edits, cx) }); snapshot.check_invariants(); snapshot.verify_chunks(&mut rng); + edits.push((snapshot, wrap_edits)); if wrap_map.read_with(&cx, |map, _| map.is_rewrapping()) && rng.gen_bool(0.4) { log::info!("Waiting for wrapping to finish"); @@ -1024,19 +1115,84 @@ mod tests { } if !wrap_map.read_with(&cx, |map, _| map.is_rewrapping()) { - let mut wrapped_snapshot = + let (mut wrapped_snapshot, wrap_edits) = wrap_map.update(&mut cx, |map, cx| map.sync(tabs_snapshot, Vec::new(), cx)); let actual_text = wrapped_snapshot.text(); + let actual_longest_row = wrapped_snapshot.longest_row(); log::info!("Wrapping finished: {:?}", actual_text); wrapped_snapshot.check_invariants(); wrapped_snapshot.verify_chunks(&mut rng); + edits.push((wrapped_snapshot.clone(), wrap_edits)); assert_eq!( actual_text, expected_text, "unwrapped text is: {:?}", unwrapped_text ); + + let mut summary = TextSummary::default(); + for (ix, item) in wrapped_snapshot + .transforms + .items(&()) + .into_iter() + .enumerate() + { + summary += &item.summary.output; + log::info!("{} summary: {:?}", ix, item.summary.output,); + } + + if tab_size == 1 + || !wrapped_snapshot + .tab_snapshot + .fold_snapshot + .text() + .contains('\t') + { + let mut expected_longest_rows = Vec::new(); + let mut longest_line_len = -1; + for (row, line) in expected_text.split('\n').enumerate() { + let line_char_count = line.chars().count() as isize; + if line_char_count > longest_line_len { + expected_longest_rows.clear(); + longest_line_len = line_char_count; + } + if line_char_count >= longest_line_len { + expected_longest_rows.push(row as u32); + } + } + + assert!( + expected_longest_rows.contains(&actual_longest_row), + "incorrect longest row {}. expected {:?} with length {}", + actual_longest_row, + expected_longest_rows, + longest_line_len, + ) + } } } + + let mut initial_text = Rope::from(initial_snapshot.text().as_str()); + for (snapshot, patch) in edits { + let snapshot_text = Rope::from(snapshot.text().as_str()); + for edit in &patch { + let old_start = initial_text.point_to_offset(Point::new(edit.new.start, 0)); + let old_end = initial_text.point_to_offset(cmp::min( + Point::new(edit.new.start + edit.old.len() as u32, 0), + initial_text.max_point(), + )); + let new_start = snapshot_text.point_to_offset(Point::new(edit.new.start, 0)); + let new_end = snapshot_text.point_to_offset(cmp::min( + Point::new(edit.new.end, 0), + snapshot_text.max_point(), + )); + let new_text = snapshot_text + .chunks_in_range(new_start..new_end) + .collect::(); + + initial_text.replace(old_start..old_end, &new_text); + } + assert_eq!(initial_text.to_string(), snapshot_text.to_string()); + } } fn wrap_text( @@ -1067,8 +1223,8 @@ mod tests { } impl Snapshot { - fn text(&self) -> String { - self.chunks_at(0).collect() + pub fn text(&self) -> String { + self.text_chunks(0).collect() } fn verify_chunks(&mut self, rng: &mut impl Rng) { @@ -1077,7 +1233,7 @@ mod tests { let start_row = rng.gen_range(0..=end_row); end_row += 1; - let mut expected_text = self.chunks_at(start_row).collect::(); + let mut expected_text = self.text_chunks(start_row).collect::(); if expected_text.ends_with("\n") { expected_text.push('\n'); } @@ -1091,7 +1247,7 @@ mod tests { } let actual_text = self - .highlighted_chunks_for_rows(start_row..end_row) + .chunks(start_row..end_row, None) .map(|c| c.text) .collect::(); assert_eq!( diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index f538f3f4cb..8927f9efdd 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1,6 +1,6 @@ use super::{ - DisplayPoint, Editor, EditorMode, EditorSettings, EditorStyle, Input, Scroll, Select, - SelectPhase, Snapshot, MAX_LINE_LEN, + DisplayPoint, DisplayRow, Editor, EditorMode, EditorSettings, EditorStyle, Input, Scroll, + Select, SelectPhase, Snapshot, MAX_LINE_LEN, }; use clock::ReplicaId; use gpui::{ @@ -17,7 +17,7 @@ use gpui::{ MutableAppContext, PaintContext, Quad, Scene, SizeConstraint, ViewContext, WeakViewHandle, }; use json::json; -use language::{DiagnosticSeverity, HighlightedChunk}; +use language::Chunk; use smallvec::SmallVec; use std::{ cmp::{self, Ordering}, @@ -25,6 +25,7 @@ use std::{ fmt::Write, ops::Range, }; +use theme::BlockStyle; pub struct EditorElement { view: WeakViewHandle, @@ -195,6 +196,7 @@ impl EditorElement { ) { let bounds = gutter_bounds.union_rect(text_bounds); let scroll_top = layout.snapshot.scroll_position().y() * layout.line_height; + let start_row = layout.snapshot.scroll_position().y() as u32; let editor = self.view(cx.app); let style = &self.settings.style; cx.scene.push_quad(Quad { @@ -239,6 +241,51 @@ impl EditorElement { } } } + + // Draw block backgrounds + for (ixs, block_style) in &layout.block_layouts { + let row = start_row + ixs.start; + let offset = vec2f(0., row as f32 * layout.line_height - scroll_top); + let height = ixs.len() as f32 * layout.line_height; + cx.scene.push_quad(Quad { + bounds: RectF::new( + text_bounds.origin() + offset, + vec2f(text_bounds.width(), height), + ), + background: block_style.background, + border: block_style + .border + .map_or(Default::default(), |color| Border { + width: 1., + color, + overlay: true, + top: true, + right: false, + bottom: true, + left: false, + }), + corner_radius: 0., + }); + cx.scene.push_quad(Quad { + bounds: RectF::new( + gutter_bounds.origin() + offset, + vec2f(gutter_bounds.width(), height), + ), + background: block_style.gutter_background, + border: block_style + .gutter_border + .map_or(Default::default(), |color| Border { + width: 1., + color, + overlay: true, + top: true, + right: false, + bottom: true, + left: false, + }), + corner_radius: 0., + }); + } } fn paint_gutter( @@ -401,18 +448,24 @@ impl EditorElement { .width() } - fn layout_line_numbers( + fn layout_rows( &self, rows: Range, active_rows: &BTreeMap, snapshot: &Snapshot, cx: &LayoutContext, - ) -> Vec> { + ) -> ( + Vec>, + Vec<(Range, BlockStyle)>, + ) { let style = &self.settings.style; - let mut layouts = Vec::with_capacity(rows.len()); + let include_line_numbers = snapshot.mode == EditorMode::Full; + let mut last_block_id = None; + let mut blocks = Vec::<(Range, BlockStyle)>::new(); + let mut line_number_layouts = Vec::with_capacity(rows.len()); let mut line_number = String::new(); - for (ix, (buffer_row, soft_wrapped)) in snapshot - .buffer_rows(rows.start) + for (ix, row) in snapshot + .buffer_rows(rows.start, cx) .take((rows.end - rows.start) as usize) .enumerate() { @@ -422,27 +475,46 @@ impl EditorElement { } else { style.line_number }; - if soft_wrapped { - layouts.push(None); - } else { - line_number.clear(); - write!(&mut line_number, "{}", buffer_row + 1).unwrap(); - layouts.push(Some(cx.text_layout_cache.layout_str( - &line_number, - style.text.font_size, - &[( - line_number.len(), - RunStyle { - font_id: style.text.font_id, - color, - underline: None, - }, - )], - ))); + match row { + DisplayRow::Buffer(buffer_row) => { + if include_line_numbers { + line_number.clear(); + write!(&mut line_number, "{}", buffer_row + 1).unwrap(); + line_number_layouts.push(Some(cx.text_layout_cache.layout_str( + &line_number, + style.text.font_size, + &[( + line_number.len(), + RunStyle { + font_id: style.text.font_id, + color, + underline: None, + }, + )], + ))); + } + last_block_id = None; + } + DisplayRow::Block(block_id, style) => { + let ix = ix as u32; + if last_block_id == Some(block_id) { + if let Some((row_range, _)) = blocks.last_mut() { + row_range.end += 1; + } + } else if let Some(style) = style { + blocks.push((ix..ix + 1, style)); + } + line_number_layouts.push(None); + last_block_id = Some(block_id); + } + DisplayRow::Wrap => { + line_number_layouts.push(None); + last_block_id = None; + } } } - layouts + (line_number_layouts, blocks) } fn layout_lines( @@ -493,9 +565,9 @@ impl EditorElement { let mut styles = Vec::new(); let mut row = rows.start; let mut line_exceeded_max_len = false; - let chunks = snapshot.highlighted_chunks_for_rows(rows.clone()); + let chunks = snapshot.chunks(rows.clone(), Some(&style.syntax), cx); - let newline_chunk = HighlightedChunk { + let newline_chunk = Chunk { text: "\n", ..Default::default() }; @@ -517,10 +589,8 @@ impl EditorElement { } if !line_chunk.is_empty() && !line_exceeded_max_len { - let highlight_style = chunk - .highlight_id - .style(&style.syntax) - .unwrap_or(style.text.clone().into()); + let highlight_style = + chunk.highlight_style.unwrap_or(style.text.clone().into()); // Avoid a lookup if the font properties match the previous ones. let font_id = if highlight_style.font_properties == prev_font_properties { prev_font_id @@ -543,13 +613,7 @@ impl EditorElement { } let underline = if let Some(severity) = chunk.diagnostic { - match severity { - DiagnosticSeverity::ERROR => Some(style.error_underline), - DiagnosticSeverity::WARNING => Some(style.warning_underline), - DiagnosticSeverity::INFORMATION => Some(style.information_underline), - DiagnosticSeverity::HINT => Some(style.hint_underline), - _ => highlight_style.underline, - } + Some(super::diagnostic_style(severity, true, style).text) } else { highlight_style.underline }; @@ -677,11 +741,8 @@ impl Element for EditorElement { } }); - let line_number_layouts = if snapshot.mode == EditorMode::Full { - self.layout_line_numbers(start_row..end_row, &active_rows, &snapshot, cx) - } else { - Vec::new() - }; + let (line_number_layouts, block_layouts) = + self.layout_rows(start_row..end_row, &active_rows, &snapshot, cx); let mut max_visible_line_width = 0.0; let line_layouts = self.layout_lines(start_row..end_row, &mut snapshot, cx); @@ -703,6 +764,7 @@ impl Element for EditorElement { active_rows, line_layouts, line_number_layouts, + block_layouts, line_height, em_width, selections, @@ -825,6 +887,7 @@ pub struct LayoutState { active_rows: BTreeMap, line_layouts: Vec, line_number_layouts: Vec>, + block_layouts: Vec<(Range, BlockStyle)>, line_height: f32, em_width: f32, selections: HashMap>>, @@ -1079,11 +1142,11 @@ mod tests { }); let element = EditorElement::new(editor.downgrade(), settings); - let layouts = editor.update(cx, |editor, cx| { + let (layouts, _) = editor.update(cx, |editor, cx| { let snapshot = editor.snapshot(cx); let mut presenter = cx.build_presenter(window_id, 30.); let mut layout_cx = presenter.build_layout_context(false, cx); - element.layout_line_numbers(0..6, &Default::default(), &snapshot, &mut layout_cx) + element.layout_rows(0..6, &Default::default(), &snapshot, &mut layout_cx) }); assert_eq!(layouts.len(), 6); } diff --git a/crates/editor/src/lib.rs b/crates/editor/src/lib.rs index 4a1f6392e4..09ca22f760 100644 --- a/crates/editor/src/lib.rs +++ b/crates/editor/src/lib.rs @@ -7,13 +7,15 @@ mod test; use buffer::rope::TextDimension; use clock::ReplicaId; -pub use display_map::DisplayPoint; use display_map::*; +pub use display_map::{DisplayPoint, DisplayRow}; pub use element::*; use gpui::{ - action, geometry::vector::Vector2F, keymap::Binding, text_layout, AppContext, ClipboardItem, - Element, ElementBox, Entity, ModelHandle, MutableAppContext, RenderContext, View, ViewContext, - WeakViewHandle, + action, + geometry::vector::{vec2f, Vector2F}, + keymap::Binding, + text_layout, AppContext, ClipboardItem, Element, ElementBox, Entity, ModelHandle, + MutableAppContext, RenderContext, View, ViewContext, WeakViewHandle, }; use language::*; use serde::{Deserialize, Serialize}; @@ -22,6 +24,7 @@ use smol::Timer; use std::{ cell::RefCell, cmp::{self, Ordering}, + collections::HashMap, iter, mem, ops::{Range, RangeInclusive}, rc::Rc, @@ -29,7 +32,7 @@ use std::{ time::Duration, }; use sum_tree::Bias; -use theme::EditorStyle; +use theme::{DiagnosticStyle, EditorStyle, SyntaxTheme}; use util::post_inc; const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); @@ -83,6 +86,7 @@ action!(AddSelectionBelow); action!(SelectLargerSyntaxNode); action!(SelectSmallerSyntaxNode); action!(MoveToEnclosingBracket); +action!(ShowNextDiagnostic); action!(PageUp); action!(PageDown); action!(Fold); @@ -184,6 +188,7 @@ pub fn init(cx: &mut MutableAppContext) { Binding::new("ctrl-w", SelectLargerSyntaxNode, Some("Editor")), Binding::new("alt-down", SelectSmallerSyntaxNode, Some("Editor")), Binding::new("ctrl-shift-W", SelectSmallerSyntaxNode, Some("Editor")), + Binding::new("f8", ShowNextDiagnostic, Some("Editor")), Binding::new("ctrl-m", MoveToEnclosingBracket, Some("Editor")), Binding::new("pageup", PageUp, Some("Editor")), Binding::new("pagedown", PageDown, Some("Editor")), @@ -242,6 +247,7 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(Editor::select_larger_syntax_node); cx.add_action(Editor::select_smaller_syntax_node); cx.add_action(Editor::move_to_enclosing_bracket); + cx.add_action(Editor::show_next_diagnostic); cx.add_action(Editor::page_up); cx.add_action(Editor::page_down); cx.add_action(Editor::fold); @@ -299,6 +305,7 @@ pub struct Editor { add_selections_state: Option, autoclose_stack: Vec, select_larger_syntax_node_stack: Vec]>>, + active_diagnostics: Option, scroll_position: Vector2F, scroll_top_anchor: Anchor, autoscroll_requested: bool, @@ -331,6 +338,14 @@ struct BracketPairState { pair: BracketPair, } +#[derive(Debug)] +struct ActiveDiagnosticGroup { + primary_range: Range, + primary_message: String, + blocks: HashMap, + is_valid: bool, +} + #[derive(Serialize, Deserialize)] struct ClipboardSelection { len: usize, @@ -418,6 +433,7 @@ impl Editor { add_selections_state: None, autoclose_stack: Default::default(), select_larger_syntax_node_stack: Vec::new(), + active_diagnostics: None, build_settings, scroll_position: Vector2F::zero(), scroll_top_anchor: Anchor::min(), @@ -466,16 +482,24 @@ impl Editor { cx.notify(); } - fn set_scroll_position(&mut self, mut scroll_position: Vector2F, cx: &mut ViewContext) { + fn set_scroll_position(&mut self, scroll_position: Vector2F, cx: &mut ViewContext) { let map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let scroll_top_buffer_offset = - DisplayPoint::new(scroll_position.y() as u32, 0).to_buffer_offset(&map, Bias::Right); + DisplayPoint::new(scroll_position.y() as u32, 0).to_offset(&map, Bias::Right); self.scroll_top_anchor = self .buffer .read(cx) .anchor_at(scroll_top_buffer_offset, Bias::Right); - scroll_position.set_y(scroll_position.y().fract()); - self.scroll_position = scroll_position; + self.scroll_position = vec2f( + scroll_position.x(), + scroll_position.y() - self.scroll_top_anchor.to_display_point(&map).row() as f32, + ); + + debug_assert_eq!( + compute_scroll_position(&map, self.scroll_position, &self.scroll_top_anchor), + scroll_position + ); + cx.notify(); } @@ -519,13 +543,13 @@ impl Editor { .peek() .unwrap() .head() - .to_display_point(&display_map, Bias::Left) + .to_display_point(&display_map) .row() as f32; let last_cursor_bottom = selections .last() .unwrap() .head() - .to_display_point(&display_map, Bias::Right) + .to_display_point(&display_map) .row() as f32 + 1.0; @@ -570,7 +594,7 @@ impl Editor { let mut target_left = std::f32::INFINITY; let mut target_right = 0.0_f32; for selection in selections { - let head = selection.head().to_display_point(&display_map, Bias::Left); + let head = selection.head().to_display_point(&display_map); let start_column = head.column().saturating_sub(3); let end_column = cmp::min(display_map.line_len(head.row()), head.column() + 3); target_left = target_left @@ -620,7 +644,7 @@ impl Editor { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let buffer = self.buffer.read(cx); - let cursor = buffer.anchor_before(position.to_buffer_point(&display_map, Bias::Left)); + let cursor = buffer.anchor_before(position.to_point(&display_map)); let selection = Selection { id: post_inc(&mut self.next_selection_id), start: cursor.clone(), @@ -646,7 +670,7 @@ impl Editor { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); if let Some(pending_selection) = self.pending_selection.as_mut() { let buffer = self.buffer.read(cx); - let cursor = buffer.anchor_before(position.to_buffer_point(&display_map, Bias::Left)); + let cursor = buffer.anchor_before(position.to_point(&display_map)); if cursor.cmp(&pending_selection.tail(), buffer).unwrap() < Ordering::Equal { if !pending_selection.reversed { pending_selection.end = pending_selection.start.clone(); @@ -681,7 +705,9 @@ impl Editor { } pub fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { - if let Some(pending_selection) = self.pending_selection.take() { + if self.active_diagnostics.is_some() { + self.dismiss_diagnostics(cx); + } else if let Some(pending_selection) = self.pending_selection.take() { let buffer = self.buffer.read(cx); let pending_selection = Selection { id: pending_selection.id, @@ -694,16 +720,8 @@ impl Editor { self.update_selections(vec![pending_selection], true, cx); } } else { - let selections = self.selections::(cx); - let mut selection_count = 0; - let mut oldest_selection = selections - .min_by_key(|s| { - selection_count += 1; - s.id - }) - .unwrap() - .clone(); - if selection_count == 1 { + let mut oldest_selection = self.oldest_selection::(cx); + if self.selection_count(cx) == 1 { oldest_selection.start = oldest_selection.head().clone(); oldest_selection.end = oldest_selection.head().clone(); } @@ -763,8 +781,8 @@ impl Editor { }; Selection { id: post_inc(&mut self.next_selection_id), - start: start.to_buffer_point(&display_map, Bias::Left), - end: end.to_buffer_point(&display_map, Bias::Left), + start: start.to_point(&display_map), + end: end.to_point(&display_map), reversed, goal: SelectionGoal::None, } @@ -1052,10 +1070,10 @@ impl Editor { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); for selection in &mut selections { if selection.is_empty() { - let head = selection.head().to_display_point(&display_map, Bias::Left); + let head = selection.head().to_display_point(&display_map); let cursor = movement::left(&display_map, head) .unwrap() - .to_buffer_point(&display_map, Bias::Left); + .to_point(&display_map); selection.set_head(cursor); selection.goal = SelectionGoal::None; } @@ -1071,10 +1089,10 @@ impl Editor { let mut selections = self.selections::(cx).collect::>(); for selection in &mut selections { if selection.is_empty() { - let head = selection.head().to_display_point(&display_map, Bias::Left); + let head = selection.head().to_display_point(&display_map); let cursor = movement::right(&display_map, head) .unwrap() - .to_buffer_point(&display_map, Bias::Right); + .to_point(&display_map); selection.set_head(cursor); selection.goal = SelectionGoal::None; } @@ -1138,10 +1156,7 @@ impl Editor { let mut selections = selections.iter().peekable(); while let Some(selection) = selections.next() { let mut rows = selection.spanned_rows(false, &display_map).buffer_rows; - let goal_display_column = selection - .head() - .to_display_point(&display_map, Bias::Left) - .column(); + let goal_display_column = selection.head().to_display_point(&display_map).column(); // Accumulate contiguous regions of rows that we want to delete. while let Some(next_selection) = selections.peek() { @@ -1170,16 +1185,13 @@ impl Editor { cursor_buffer_row = rows.start.saturating_sub(1); } - let mut cursor = Point::new(cursor_buffer_row - row_delta, 0) - .to_display_point(&display_map, Bias::Left); + let mut cursor = + Point::new(cursor_buffer_row - row_delta, 0).to_display_point(&display_map); *cursor.column_mut() = cmp::min(goal_display_column, display_map.line_len(cursor.row())); row_delta += rows.len() as u32; - new_cursors.push(( - selection.id, - cursor.to_buffer_point(&display_map, Bias::Left), - )); + new_cursors.push((selection.id, cursor.to_point(&display_map))); edit_ranges.push(edit_start..edit_end); } @@ -1566,15 +1578,15 @@ impl Editor { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let mut selections = self.selections::(cx).collect::>(); for selection in &mut selections { - let start = selection.start.to_display_point(&display_map, Bias::Left); - let end = selection.end.to_display_point(&display_map, Bias::Left); + let start = selection.start.to_display_point(&display_map); + let end = selection.end.to_display_point(&display_map); if start != end { selection.end = selection.start.clone(); } else { let cursor = movement::left(&display_map, start) .unwrap() - .to_buffer_point(&display_map, Bias::Left); + .to_point(&display_map); selection.start = cursor.clone(); selection.end = cursor; } @@ -1588,10 +1600,10 @@ impl Editor { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let mut selections = self.selections::(cx).collect::>(); for selection in &mut selections { - let head = selection.head().to_display_point(&display_map, Bias::Left); + let head = selection.head().to_display_point(&display_map); let cursor = movement::left(&display_map, head) .unwrap() - .to_buffer_point(&display_map, Bias::Left); + .to_point(&display_map); selection.set_head(cursor); selection.goal = SelectionGoal::None; } @@ -1602,15 +1614,15 @@ impl Editor { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let mut selections = self.selections::(cx).collect::>(); for selection in &mut selections { - let start = selection.start.to_display_point(&display_map, Bias::Left); - let end = selection.end.to_display_point(&display_map, Bias::Left); + let start = selection.start.to_display_point(&display_map); + let end = selection.end.to_display_point(&display_map); if start != end { selection.start = selection.end.clone(); } else { let cursor = movement::right(&display_map, end) .unwrap() - .to_buffer_point(&display_map, Bias::Right); + .to_point(&display_map); selection.start = cursor; selection.end = cursor; } @@ -1624,10 +1636,10 @@ impl Editor { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let mut selections = self.selections::(cx).collect::>(); for selection in &mut selections { - let head = selection.head().to_display_point(&display_map, Bias::Left); + let head = selection.head().to_display_point(&display_map); let cursor = movement::right(&display_map, head) .unwrap() - .to_buffer_point(&display_map, Bias::Right); + .to_point(&display_map); selection.set_head(cursor); selection.goal = SelectionGoal::None; } @@ -1643,14 +1655,14 @@ impl Editor { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let mut selections = self.selections::(cx).collect::>(); for selection in &mut selections { - let start = selection.start.to_display_point(&display_map, Bias::Left); - let end = selection.end.to_display_point(&display_map, Bias::Left); + let start = selection.start.to_display_point(&display_map); + let end = selection.end.to_display_point(&display_map); if start != end { selection.goal = SelectionGoal::None; } let (start, goal) = movement::up(&display_map, start, selection.goal).unwrap(); - let cursor = start.to_buffer_point(&display_map, Bias::Left); + let cursor = start.to_point(&display_map); selection.start = cursor; selection.end = cursor; selection.goal = goal; @@ -1663,9 +1675,9 @@ impl Editor { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let mut selections = self.selections::(cx).collect::>(); for selection in &mut selections { - let head = selection.head().to_display_point(&display_map, Bias::Left); + let head = selection.head().to_display_point(&display_map); let (head, goal) = movement::up(&display_map, head, selection.goal).unwrap(); - let cursor = head.to_buffer_point(&display_map, Bias::Left); + let cursor = head.to_point(&display_map); selection.set_head(cursor); selection.goal = goal; } @@ -1681,14 +1693,14 @@ impl Editor { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let mut selections = self.selections::(cx).collect::>(); for selection in &mut selections { - let start = selection.start.to_display_point(&display_map, Bias::Left); - let end = selection.end.to_display_point(&display_map, Bias::Left); + let start = selection.start.to_display_point(&display_map); + let end = selection.end.to_display_point(&display_map); if start != end { selection.goal = SelectionGoal::None; } let (start, goal) = movement::down(&display_map, end, selection.goal).unwrap(); - let cursor = start.to_buffer_point(&display_map, Bias::Right); + let cursor = start.to_point(&display_map); selection.start = cursor; selection.end = cursor; selection.goal = goal; @@ -1701,9 +1713,9 @@ impl Editor { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let mut selections = self.selections::(cx).collect::>(); for selection in &mut selections { - let head = selection.head().to_display_point(&display_map, Bias::Left); + let head = selection.head().to_display_point(&display_map); let (head, goal) = movement::down(&display_map, head, selection.goal).unwrap(); - let cursor = head.to_buffer_point(&display_map, Bias::Right); + let cursor = head.to_point(&display_map); selection.set_head(cursor); selection.goal = goal; } @@ -1718,9 +1730,9 @@ impl Editor { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let mut selections = self.selections::(cx).collect::>(); for selection in &mut selections { - let head = selection.head().to_display_point(&display_map, Bias::Left); + let head = selection.head().to_display_point(&display_map); let new_head = movement::prev_word_boundary(&display_map, head).unwrap(); - let cursor = new_head.to_buffer_point(&display_map, Bias::Left); + let cursor = new_head.to_point(&display_map); selection.start = cursor.clone(); selection.end = cursor; selection.reversed = false; @@ -1737,9 +1749,9 @@ impl Editor { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let mut selections = self.selections::(cx).collect::>(); for selection in &mut selections { - let head = selection.head().to_display_point(&display_map, Bias::Left); + let head = selection.head().to_display_point(&display_map); let new_head = movement::prev_word_boundary(&display_map, head).unwrap(); - let cursor = new_head.to_buffer_point(&display_map, Bias::Left); + let cursor = new_head.to_point(&display_map); selection.set_head(cursor); selection.goal = SelectionGoal::None; } @@ -1756,9 +1768,9 @@ impl Editor { let mut selections = self.selections::(cx).collect::>(); for selection in &mut selections { if selection.is_empty() { - let head = selection.head().to_display_point(&display_map, Bias::Left); + let head = selection.head().to_display_point(&display_map); let new_head = movement::prev_word_boundary(&display_map, head).unwrap(); - let cursor = new_head.to_buffer_point(&display_map, Bias::Right); + let cursor = new_head.to_point(&display_map); selection.set_head(cursor); selection.goal = SelectionGoal::None; } @@ -1776,9 +1788,9 @@ impl Editor { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let mut selections = self.selections::(cx).collect::>(); for selection in &mut selections { - let head = selection.head().to_display_point(&display_map, Bias::Left); + let head = selection.head().to_display_point(&display_map); let new_head = movement::next_word_boundary(&display_map, head).unwrap(); - let cursor = new_head.to_buffer_point(&display_map, Bias::Left); + let cursor = new_head.to_point(&display_map); selection.start = cursor; selection.end = cursor; selection.reversed = false; @@ -1795,9 +1807,9 @@ impl Editor { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let mut selections = self.selections::(cx).collect::>(); for selection in &mut selections { - let head = selection.head().to_display_point(&display_map, Bias::Left); + let head = selection.head().to_display_point(&display_map); let new_head = movement::next_word_boundary(&display_map, head).unwrap(); - let cursor = new_head.to_buffer_point(&display_map, Bias::Left); + let cursor = new_head.to_point(&display_map); selection.set_head(cursor); selection.goal = SelectionGoal::None; } @@ -1814,9 +1826,9 @@ impl Editor { let mut selections = self.selections::(cx).collect::>(); for selection in &mut selections { if selection.is_empty() { - let head = selection.head().to_display_point(&display_map, Bias::Left); + let head = selection.head().to_display_point(&display_map); let new_head = movement::next_word_boundary(&display_map, head).unwrap(); - let cursor = new_head.to_buffer_point(&display_map, Bias::Right); + let cursor = new_head.to_point(&display_map); selection.set_head(cursor); selection.goal = SelectionGoal::None; } @@ -1834,9 +1846,9 @@ impl Editor { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let mut selections = self.selections::(cx).collect::>(); for selection in &mut selections { - let head = selection.head().to_display_point(&display_map, Bias::Left); + let head = selection.head().to_display_point(&display_map); let new_head = movement::line_beginning(&display_map, head, true).unwrap(); - let cursor = new_head.to_buffer_point(&display_map, Bias::Left); + let cursor = new_head.to_point(&display_map); selection.start = cursor; selection.end = cursor; selection.reversed = false; @@ -1853,9 +1865,9 @@ impl Editor { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let mut selections = self.selections::(cx).collect::>(); for selection in &mut selections { - let head = selection.head().to_display_point(&display_map, Bias::Left); + let head = selection.head().to_display_point(&display_map); let new_head = movement::line_beginning(&display_map, head, *toggle_indent).unwrap(); - selection.set_head(new_head.to_buffer_point(&display_map, Bias::Left)); + selection.set_head(new_head.to_point(&display_map)); selection.goal = SelectionGoal::None; } self.update_selections(selections, true, cx); @@ -1877,9 +1889,9 @@ impl Editor { let mut selections = self.selections::(cx).collect::>(); { for selection in &mut selections { - let head = selection.head().to_display_point(&display_map, Bias::Left); + let head = selection.head().to_display_point(&display_map); let new_head = movement::line_end(&display_map, head).unwrap(); - let anchor = new_head.to_buffer_point(&display_map, Bias::Left); + let anchor = new_head.to_point(&display_map); selection.start = anchor.clone(); selection.end = anchor; selection.reversed = false; @@ -1893,9 +1905,9 @@ impl Editor { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let mut selections = self.selections::(cx).collect::>(); for selection in &mut selections { - let head = selection.head().to_display_point(&display_map, Bias::Left); + let head = selection.head().to_display_point(&display_map); let new_head = movement::line_end(&display_map, head).unwrap(); - selection.set_head(new_head.to_buffer_point(&display_map, Bias::Left)); + selection.set_head(new_head.to_point(&display_map)); selection.goal = SelectionGoal::None; } self.update_selections(selections, true, cx); @@ -2205,6 +2217,197 @@ impl Editor { self.update_selections(selections, true, cx); } + pub fn show_next_diagnostic(&mut self, _: &ShowNextDiagnostic, cx: &mut ViewContext) { + let selection = self.newest_selection::(cx); + let buffer = self.buffer.read(cx.as_ref()); + let active_primary_range = self.active_diagnostics.as_ref().map(|active_diagnostics| { + active_diagnostics + .primary_range + .to_offset(buffer) + .to_inclusive() + }); + let mut search_start = if let Some(active_primary_range) = active_primary_range.as_ref() { + if active_primary_range.contains(&selection.head()) { + *active_primary_range.end() + } else { + selection.head() + } + } else { + selection.head() + }; + + loop { + let next_group = buffer + .diagnostics_in_range::<_, usize>(search_start..buffer.len()) + .find_map(|(range, diagnostic)| { + if diagnostic.is_primary + && !range.is_empty() + && Some(range.end) != active_primary_range.as_ref().map(|r| *r.end()) + { + Some((range, diagnostic.group_id)) + } else { + None + } + }); + + if let Some((primary_range, group_id)) = next_group { + self.activate_diagnostics(group_id, cx); + self.update_selections( + vec![Selection { + id: selection.id, + start: primary_range.start, + end: primary_range.start, + reversed: false, + goal: SelectionGoal::None, + }], + true, + cx, + ); + break; + } else if search_start == 0 { + break; + } else { + // Cycle around to the start of the buffer. + search_start = 0; + } + } + } + + fn refresh_active_diagnostics(&mut self, cx: &mut ViewContext) { + if let Some(active_diagnostics) = self.active_diagnostics.as_mut() { + let buffer = self.buffer.read(cx); + let primary_range_start = active_diagnostics.primary_range.start.to_offset(buffer); + let is_valid = buffer + .diagnostics_in_range::<_, usize>(active_diagnostics.primary_range.clone()) + .any(|(range, diagnostic)| { + diagnostic.is_primary + && !range.is_empty() + && range.start == primary_range_start + && diagnostic.message == active_diagnostics.primary_message + }); + + if is_valid != active_diagnostics.is_valid { + active_diagnostics.is_valid = is_valid; + let mut new_styles = HashMap::new(); + for (block_id, diagnostic) in &active_diagnostics.blocks { + let severity = diagnostic.severity; + let message_len = diagnostic.message.len(); + new_styles.insert( + *block_id, + ( + Some({ + let build_settings = self.build_settings.clone(); + move |cx: &AppContext| { + let settings = build_settings.borrow()(cx); + vec![( + message_len, + diagnostic_style(severity, is_valid, &settings.style) + .text + .into(), + )] + } + }), + Some({ + let build_settings = self.build_settings.clone(); + move |cx: &AppContext| { + let settings = build_settings.borrow()(cx); + diagnostic_style(severity, is_valid, &settings.style).block + } + }), + ), + ); + } + self.display_map + .update(cx, |display_map, _| display_map.restyle_blocks(new_styles)); + } + } + } + + fn activate_diagnostics(&mut self, group_id: usize, cx: &mut ViewContext) { + self.dismiss_diagnostics(cx); + self.active_diagnostics = self.display_map.update(cx, |display_map, cx| { + let buffer = self.buffer.read(cx); + + let mut primary_range = None; + let mut primary_message = None; + let mut group_end = Point::zero(); + let diagnostic_group = buffer + .diagnostic_group::(group_id) + .map(|(range, diagnostic)| { + if range.end > group_end { + group_end = range.end; + } + if diagnostic.is_primary { + primary_range = Some(range.clone()); + primary_message = Some(diagnostic.message.clone()); + } + (range, diagnostic.clone()) + }) + .collect::>(); + let primary_range = primary_range.unwrap(); + let primary_message = primary_message.unwrap(); + let primary_range = + buffer.anchor_after(primary_range.start)..buffer.anchor_before(primary_range.end); + + let blocks = display_map + .insert_blocks( + diagnostic_group.iter().map(|(range, diagnostic)| { + let build_settings = self.build_settings.clone(); + let message_len = diagnostic.message.len(); + let severity = diagnostic.severity; + BlockProperties { + position: range.start, + text: diagnostic.message.as_str(), + build_runs: Some(Arc::new({ + let build_settings = build_settings.clone(); + move |cx| { + let settings = build_settings.borrow()(cx); + vec![( + message_len, + diagnostic_style(severity, true, &settings.style) + .text + .into(), + )] + } + })), + build_style: Some(Arc::new({ + let build_settings = build_settings.clone(); + move |cx| { + let settings = build_settings.borrow()(cx); + diagnostic_style(severity, true, &settings.style).block + } + })), + disposition: BlockDisposition::Below, + } + }), + cx, + ) + .into_iter() + .zip( + diagnostic_group + .into_iter() + .map(|(_, diagnostic)| diagnostic), + ) + .collect(); + + Some(ActiveDiagnosticGroup { + primary_range, + primary_message, + blocks, + is_valid: true, + }) + }); + } + + fn dismiss_diagnostics(&mut self, cx: &mut ViewContext) { + if let Some(active_diagnostic_group) = self.active_diagnostics.take() { + self.display_map.update(cx, |display_map, cx| { + display_map.remove_blocks(active_diagnostic_group.blocks.into_keys().collect(), cx); + }); + cx.notify(); + } + } + fn build_columnar_selection( &mut self, display_map: &DisplayMapSnapshot, @@ -2219,8 +2422,8 @@ impl Editor { let end = DisplayPoint::new(row, cmp::min(columns.end, line_len)); Some(Selection { id: post_inc(&mut self.next_selection_id), - start: start.to_buffer_point(display_map, Bias::Left), - end: end.to_buffer_point(display_map, Bias::Left), + start: start.to_point(display_map), + end: end.to_point(display_map), reversed, goal: SelectionGoal::ColumnRange { start: columns.start, @@ -2254,17 +2457,16 @@ impl Editor { ) -> impl 'a + Iterator> { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let buffer = self.buffer.read(cx); - let selections = buffer - .selection_set(set_id) - .unwrap() + let selections = self + .selection_set(cx) .selections::(buffer) .collect::>(); - let start = range.start.to_buffer_point(&display_map, Bias::Left); + let start = range.start.to_point(&display_map); let start_index = self.selection_insertion_index(&selections, start); let pending_selection = if set_id.replica_id == self.buffer.read(cx).replica_id() { self.pending_selection.as_ref().and_then(|pending| { - let mut selection_start = pending.start.to_display_point(&display_map, Bias::Left); - let mut selection_end = pending.end.to_display_point(&display_map, Bias::Left); + let mut selection_start = pending.start.to_display_point(&display_map); + let mut selection_end = pending.end.to_display_point(&display_map); if pending.reversed { mem::swap(&mut selection_start, &mut selection_end); } @@ -2303,18 +2505,8 @@ impl Editor { D: 'a + TextDimension<'a> + Ord, { let buffer = self.buffer.read(cx); - let mut selections = buffer - .selection_set(self.selection_set_id) - .unwrap() - .selections::(buffer) - .peekable(); - let mut pending_selection = self.pending_selection.clone().map(|selection| Selection { - id: selection.id, - start: selection.start.summary::(buffer), - end: selection.end.summary::(buffer), - reversed: selection.reversed, - goal: selection.goal, - }); + let mut selections = self.selection_set(cx).selections::(buffer).peekable(); + let mut pending_selection = self.pending_selection(cx); iter::from_fn(move || { if let Some(pending) = pending_selection.as_mut() { while let Some(next_selection) = selections.peek() { @@ -2340,6 +2532,56 @@ impl Editor { }) } + fn pending_selection<'a, D>(&self, cx: &'a AppContext) -> Option> + where + D: 'a + TextDimension<'a>, + { + let buffer = self.buffer.read(cx); + self.pending_selection.as_ref().map(|selection| Selection { + id: selection.id, + start: selection.start.summary::(buffer), + end: selection.end.summary::(buffer), + reversed: selection.reversed, + goal: selection.goal, + }) + } + + fn selection_count<'a>(&self, cx: &'a AppContext) -> usize { + let mut selection_count = self.selection_set(cx).len(); + if self.pending_selection.is_some() { + selection_count += 1; + } + selection_count + } + + pub fn oldest_selection<'a, T>(&self, cx: &'a AppContext) -> Selection + where + T: 'a + TextDimension<'a>, + { + let buffer = self.buffer.read(cx); + self.selection_set(cx) + .oldest_selection(buffer) + .or_else(|| self.pending_selection(cx)) + .unwrap() + } + + pub fn newest_selection<'a, T>(&self, cx: &'a AppContext) -> Selection + where + T: 'a + TextDimension<'a>, + { + let buffer = self.buffer.read(cx); + self.pending_selection(cx) + .or_else(|| self.selection_set(cx).newest_selection(buffer)) + .unwrap() + } + + fn selection_set<'a>(&self, cx: &'a AppContext) -> &'a SelectionSet { + self.buffer + .read(cx) + .selection_set(self.selection_set_id) + .unwrap() + } + fn update_selections( &mut self, mut selections: Vec>, @@ -2438,7 +2680,7 @@ impl Editor { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); for selection in selections { let range = selection.display_range(&display_map).sorted(); - let buffer_start_row = range.start.to_buffer_point(&display_map, Bias::Left).row; + let buffer_start_row = range.start.to_point(&display_map).row; for row in (0..=range.end.row()).rev() { if self.is_line_foldable(&display_map, row) && !display_map.is_line_folded(row) { @@ -2464,8 +2706,8 @@ impl Editor { .iter() .map(|s| { let range = s.display_range(&display_map).sorted(); - let mut start = range.start.to_buffer_point(&display_map, Bias::Left); - let mut end = range.end.to_buffer_point(&display_map, Bias::Left); + let mut start = range.start.to_point(&display_map); + let mut end = range.end.to_point(&display_map); start.column = 0; end.column = buffer.line_len(end.row); start..end @@ -2513,8 +2755,7 @@ impl Editor { } let end = end.unwrap_or(max_point); - return start.to_buffer_point(display_map, Bias::Left) - ..end.to_buffer_point(display_map, Bias::Left); + return start.to_point(display_map)..end.to_point(display_map); } pub fn fold_selected_ranges(&mut self, _: &FoldSelectedRanges, cx: &mut ViewContext) { @@ -2624,6 +2865,7 @@ impl Editor { } fn on_buffer_changed(&mut self, _: ModelHandle, cx: &mut ViewContext) { + self.refresh_active_diagnostics(cx); cx.notify(); } @@ -2666,16 +2908,17 @@ impl Snapshot { self.display_snapshot.buffer_row_count() } - pub fn buffer_rows(&self, start_row: u32) -> BufferRows { - self.display_snapshot.buffer_rows(start_row) + pub fn buffer_rows<'a>(&'a self, start_row: u32, cx: &'a AppContext) -> BufferRows<'a> { + self.display_snapshot.buffer_rows(start_row, Some(cx)) } - pub fn highlighted_chunks_for_rows( - &mut self, + pub fn chunks<'a>( + &'a self, display_rows: Range, - ) -> display_map::HighlightedChunks { - self.display_snapshot - .highlighted_chunks_for_rows(display_rows) + theme: Option<&'a SyntaxTheme>, + cx: &'a AppContext, + ) -> display_map::Chunks<'a> { + self.display_snapshot.chunks(display_rows, theme, cx) } pub fn scroll_position(&self) -> Vector2F { @@ -2743,10 +2986,14 @@ impl EditorSettings { selection: Default::default(), guest_selections: Default::default(), syntax: Default::default(), - error_underline: Default::default(), - warning_underline: Default::default(), - information_underline: Default::default(), - hint_underline: Default::default(), + error_diagnostic: Default::default(), + invalid_error_diagnostic: Default::default(), + warning_diagnostic: Default::default(), + invalid_warning_diagnostic: Default::default(), + information_diagnostic: Default::default(), + invalid_information_diagnostic: Default::default(), + hint_diagnostic: Default::default(), + invalid_hint_diagnostic: Default::default(), } }, } @@ -2758,9 +3005,7 @@ fn compute_scroll_position( mut scroll_position: Vector2F, scroll_top_anchor: &Anchor, ) -> Vector2F { - let scroll_top = scroll_top_anchor - .to_display_point(snapshot, Bias::Left) - .row() as f32; + let scroll_top = scroll_top_anchor.to_display_point(snapshot).row() as f32; scroll_position.set_y(scroll_top + scroll_position.y()); scroll_position } @@ -2838,8 +3083,8 @@ impl View for Editor { impl SelectionExt for Selection { fn display_range(&self, map: &DisplayMapSnapshot) -> Range { - let start = self.start.to_display_point(map, Bias::Left); - let end = self.end.to_display_point(map, Bias::Left); + let start = self.start.to_display_point(map); + let end = self.end.to_display_point(map); if self.reversed { end..start } else { @@ -2852,8 +3097,8 @@ impl SelectionExt for Selection { include_end_if_at_line_start: bool, map: &DisplayMapSnapshot, ) -> SpannedRows { - let display_start = self.start.to_display_point(map, Bias::Left); - let mut display_end = self.end.to_display_point(map, Bias::Right); + let display_start = self.start.to_display_point(map); + let mut display_end = self.end.to_display_point(map); if !include_end_if_at_line_start && display_end.row() != map.max_point().row() && display_start.row() != display_end.row() @@ -2872,6 +3117,24 @@ impl SelectionExt for Selection { } } +pub fn diagnostic_style( + severity: DiagnosticSeverity, + valid: bool, + style: &EditorStyle, +) -> DiagnosticStyle { + match (severity, valid) { + (DiagnosticSeverity::ERROR, true) => style.error_diagnostic, + (DiagnosticSeverity::ERROR, false) => style.invalid_error_diagnostic, + (DiagnosticSeverity::WARNING, true) => style.warning_diagnostic, + (DiagnosticSeverity::WARNING, false) => style.invalid_warning_diagnostic, + (DiagnosticSeverity::INFORMATION, true) => style.information_diagnostic, + (DiagnosticSeverity::INFORMATION, false) => style.invalid_information_diagnostic, + (DiagnosticSeverity::HINT, true) => style.hint_diagnostic, + (DiagnosticSeverity::HINT, false) => style.invalid_hint_diagnostic, + _ => Default::default(), + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 7ad1374f8e..c6190fec29 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -33,11 +33,17 @@ pub fn up( map.column_to_chars(point.row(), point.column()) }; - if point.row() > 0 { - *point.row_mut() -= 1; - *point.column_mut() = map.column_from_chars(point.row(), goal_column); - } else { - point = DisplayPoint::new(0, 0); + loop { + if point.row() > 0 { + *point.row_mut() -= 1; + *point.column_mut() = map.column_from_chars(point.row(), goal_column); + if !map.is_block_line(point.row()) { + break; + } + } else { + point = DisplayPoint::new(0, 0); + break; + } } let clip_bias = if point.column() == map.line_len(point.row()) { @@ -64,11 +70,17 @@ pub fn down( map.column_to_chars(point.row(), point.column()) }; - if point.row() < max_point.row() { - *point.row_mut() += 1; - *point.column_mut() = map.column_from_chars(point.row(), goal_column); - } else { - point = max_point; + loop { + if point.row() < max_point.row() { + *point.row_mut() += 1; + *point.column_mut() = map.column_from_chars(point.row(), goal_column); + if !map.is_block_line(point.row()) { + break; + } + } else { + point = max_point; + break; + } } let clip_bias = if point.column() == map.line_len(point.row()) { diff --git a/crates/editor/src/test.rs b/crates/editor/src/test.rs index 97e7f5a08c..26f29364fd 100644 --- a/crates/editor/src/test.rs +++ b/crates/editor/src/test.rs @@ -2,6 +2,13 @@ use gpui::{Entity, ModelHandle}; use smol::channel; use std::marker::PhantomData; +#[cfg(test)] +#[ctor::ctor] +fn init_logger() { + // std::env::set_var("RUST_LOG", "info"); + env_logger::init(); +} + pub fn sample_text(rows: usize, cols: usize) -> String { let mut text = String::new(); for row in 0..rows { diff --git a/crates/gpui/src/fonts.rs b/crates/gpui/src/fonts.rs index b1aae4c9be..3dbfd66034 100644 --- a/crates/gpui/src/fonts.rs +++ b/crates/gpui/src/fonts.rs @@ -30,7 +30,7 @@ pub struct TextStyle { pub underline: Option, } -#[derive(Clone, Debug, Default)] +#[derive(Copy, Clone, Debug, Default)] pub struct HighlightStyle { pub color: Color, pub font_properties: Properties, diff --git a/crates/gpui/src/platform/mac/event.rs b/crates/gpui/src/platform/mac/event.rs index 04712d599f..f205420dde 100644 --- a/crates/gpui/src/platform/mac/event.rs +++ b/crates/gpui/src/platform/mac/event.rs @@ -1,10 +1,4 @@ use crate::{geometry::vector::vec2f, keymap::Keystroke, platform::Event}; -use cocoa::appkit::{ - NSDeleteFunctionKey as DELETE_KEY, NSDownArrowFunctionKey as ARROW_DOWN_KEY, - NSLeftArrowFunctionKey as ARROW_LEFT_KEY, NSPageDownFunctionKey as PAGE_DOWN_KEY, - NSPageUpFunctionKey as PAGE_UP_KEY, NSRightArrowFunctionKey as ARROW_RIGHT_KEY, - NSUpArrowFunctionKey as ARROW_UP_KEY, -}; use cocoa::{ appkit::{NSEvent, NSEventModifierFlags, NSEventType}, base::{id, nil, YES}, @@ -12,11 +6,6 @@ use cocoa::{ }; use std::{ffi::CStr, os::raw::c_char}; -const BACKSPACE_KEY: u16 = 0x7f; -const ENTER_KEY: u16 = 0x0d; -const ESCAPE_KEY: u16 = 0x1b; -const TAB_KEY: u16 = 0x09; - impl Event { pub unsafe fn from_native(native_event: id, window_height: Option) -> Option { let event_type = native_event.eventType(); @@ -39,18 +28,39 @@ impl Event { .unwrap(); let unmodified_chars = if let Some(first_char) = unmodified_chars.chars().next() { + use cocoa::appkit::*; + const BACKSPACE_KEY: u16 = 0x7f; + const ENTER_KEY: u16 = 0x0d; + const ESCAPE_KEY: u16 = 0x1b; + const TAB_KEY: u16 = 0x09; + + #[allow(non_upper_case_globals)] match first_char as u16 { - ARROW_UP_KEY => "up", - ARROW_DOWN_KEY => "down", - ARROW_LEFT_KEY => "left", - ARROW_RIGHT_KEY => "right", - PAGE_UP_KEY => "pageup", - PAGE_DOWN_KEY => "pagedown", BACKSPACE_KEY => "backspace", ENTER_KEY => "enter", - DELETE_KEY => "delete", ESCAPE_KEY => "escape", TAB_KEY => "tab", + + NSUpArrowFunctionKey => "up", + NSDownArrowFunctionKey => "down", + NSLeftArrowFunctionKey => "left", + NSRightArrowFunctionKey => "right", + NSPageUpFunctionKey => "pageup", + NSPageDownFunctionKey => "pagedown", + NSDeleteFunctionKey => "delete", + NSF1FunctionKey => "f1", + NSF2FunctionKey => "f2", + NSF3FunctionKey => "f3", + NSF4FunctionKey => "f4", + NSF5FunctionKey => "f5", + NSF6FunctionKey => "f6", + NSF7FunctionKey => "f7", + NSF8FunctionKey => "f8", + NSF9FunctionKey => "f9", + NSF10FunctionKey => "f10", + NSF11FunctionKey => "f11", + NSF12FunctionKey => "f12", + _ => unmodified_chars, } } else { diff --git a/crates/language/src/lib.rs b/crates/language/src/lib.rs index 58f0ecad64..0755ed9571 100644 --- a/crates/language/src/lib.rs +++ b/crates/language/src/lib.rs @@ -12,7 +12,7 @@ use anyhow::{anyhow, Result}; pub use buffer::{Buffer as TextBuffer, Operation as _, *}; use clock::ReplicaId; use futures::FutureExt as _; -use gpui::{AppContext, Entity, ModelContext, MutableAppContext, Task}; +use gpui::{fonts::HighlightStyle, AppContext, Entity, ModelContext, MutableAppContext, Task}; use lazy_static::lazy_static; use lsp::LanguageServer; use parking_lot::Mutex; @@ -34,6 +34,7 @@ use std::{ time::{Duration, Instant, SystemTime, UNIX_EPOCH}, vec, }; +use theme::SyntaxTheme; use tree_sitter::{InputEdit, Parser, QueryCursor, Tree}; use util::{post_inc, TryFutureExt as _}; @@ -78,13 +79,14 @@ pub struct Snapshot { diagnostics: AnchorRangeMultimap, is_parsing: bool, language: Option>, - query_cursor: QueryCursorHandle, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct Diagnostic { pub severity: DiagnosticSeverity, pub message: String, + pub group_id: usize, + pub is_primary: bool, } struct LanguageServerState { @@ -190,11 +192,13 @@ struct Highlights<'a> { next_capture: Option<(tree_sitter::QueryMatch<'a, 'a>, usize)>, stack: Vec<(usize, HighlightId)>, highlight_map: HighlightMap, + theme: &'a SyntaxTheme, + _query_cursor: QueryCursorHandle, } -pub struct HighlightedChunks<'a> { +pub struct Chunks<'a> { range: Range, - chunks: Chunks<'a>, + chunks: rope::Chunks<'a>, diagnostic_endpoints: Peekable>, error_depth: usize, warning_depth: usize, @@ -204,9 +208,9 @@ pub struct HighlightedChunks<'a> { } #[derive(Clone, Copy, Debug, Default)] -pub struct HighlightedChunk<'a> { +pub struct Chunk<'a> { pub text: &'a str, - pub highlight_id: HighlightId, + pub highlight_style: Option, pub diagnostic: Option, } @@ -341,7 +345,6 @@ impl Buffer { diagnostics: self.diagnostics.clone(), is_parsing: self.parsing_in_background, language: self.language.clone(), - query_cursor: QueryCursorHandle::new(), } } @@ -438,7 +441,7 @@ impl Buffer { uri, Default::default(), snapshot.version as i32, - snapshot.buffer_snapshot.text().into(), + snapshot.buffer_snapshot.text().to_string(), ), }, ) @@ -699,6 +702,7 @@ impl Buffer { } else { self.content() }; + let abs_path = self.file.as_ref().and_then(|f| f.abs_path()); let empty_set = HashSet::new(); let disk_based_sources = self @@ -714,56 +718,82 @@ impl Buffer { .peekable(); let mut last_edit_old_end = PointUtf16::zero(); let mut last_edit_new_end = PointUtf16::zero(); + let mut group_ids_by_diagnostic_range = HashMap::new(); + let mut diagnostics_by_group_id = HashMap::new(); + let mut next_group_id = 0; + 'outer: for diagnostic in &diagnostics { + let mut start = diagnostic.range.start.to_point_utf16(); + let mut end = diagnostic.range.end.to_point_utf16(); + let source = diagnostic.source.as_ref(); + let code = diagnostic.code.as_ref(); + let group_id = diagnostic_ranges(&diagnostic, abs_path.as_deref()) + .find_map(|range| group_ids_by_diagnostic_range.get(&(source, code, range))) + .copied() + .unwrap_or_else(|| { + let group_id = post_inc(&mut next_group_id); + for range in diagnostic_ranges(&diagnostic, abs_path.as_deref()) { + group_ids_by_diagnostic_range.insert((source, code, range), group_id); + } + group_id + }); + + if diagnostic + .source + .as_ref() + .map_or(false, |source| disk_based_sources.contains(source)) + { + while let Some(edit) = edits_since_save.peek() { + if edit.old.end <= start { + last_edit_old_end = edit.old.end; + last_edit_new_end = edit.new.end; + edits_since_save.next(); + } else if edit.old.start <= end && edit.old.end >= start { + continue 'outer; + } else { + break; + } + } + + start = last_edit_new_end + (start - last_edit_old_end); + end = last_edit_new_end + (end - last_edit_old_end); + } + + let mut range = content.clip_point_utf16(start, Bias::Left) + ..content.clip_point_utf16(end, Bias::Right); + if range.start == range.end { + range.end.column += 1; + range.end = content.clip_point_utf16(range.end, Bias::Right); + if range.start == range.end && range.end.column > 0 { + range.start.column -= 1; + range.start = content.clip_point_utf16(range.start, Bias::Left); + } + } + + diagnostics_by_group_id + .entry(group_id) + .or_insert(Vec::new()) + .push(( + range, + Diagnostic { + severity: diagnostic.severity.unwrap_or(DiagnosticSeverity::ERROR), + message: diagnostic.message.clone(), + group_id, + is_primary: false, + }, + )); + } content.anchor_range_multimap( Bias::Left, Bias::Right, - diagnostics.into_iter().filter_map(|diagnostic| { - let mut start = PointUtf16::new( - diagnostic.range.start.line, - diagnostic.range.start.character, - ); - let mut end = - PointUtf16::new(diagnostic.range.end.line, diagnostic.range.end.character); - if diagnostic - .source - .as_ref() - .map_or(false, |source| disk_based_sources.contains(source)) - { - while let Some(edit) = edits_since_save.peek() { - if edit.old.end <= start { - last_edit_old_end = edit.old.end; - last_edit_new_end = edit.new.end; - edits_since_save.next(); - } else if edit.old.start <= end && edit.old.end >= start { - return None; - } else { - break; - } - } - - start = last_edit_new_end + (start - last_edit_old_end); - end = last_edit_new_end + (end - last_edit_old_end); - } - - let mut range = content.clip_point_utf16(start, Bias::Left) - ..content.clip_point_utf16(end, Bias::Right); - if range.start == range.end { - range.end.column += 1; - range.end = content.clip_point_utf16(range.end, Bias::Right); - if range.start == range.end && range.end.column > 0 { - range.start.column -= 1; - range.start = content.clip_point_utf16(range.start, Bias::Left); - } - } - Some(( - range, - Diagnostic { - severity: diagnostic.severity.unwrap_or(DiagnosticSeverity::ERROR), - message: diagnostic.message, - }, - )) - }), + diagnostics_by_group_id + .into_values() + .flat_map(|mut diagnostics| { + let primary_diagnostic = + diagnostics.iter_mut().min_by_key(|d| d.1.severity).unwrap(); + primary_diagnostic.1.is_primary = true; + diagnostics + }), ) }; @@ -786,7 +816,7 @@ impl Buffer { pub fn diagnostics_in_range<'a, T, O>( &'a self, - range: Range, + search_range: Range, ) -> impl Iterator, &Diagnostic)> + 'a where T: 'a + ToOffset, @@ -794,7 +824,20 @@ impl Buffer { { let content = self.content(); self.diagnostics - .intersecting_ranges(range, content, true) + .intersecting_ranges(search_range, content, true) + .map(move |(_, range, diagnostic)| (range, diagnostic)) + } + + pub fn diagnostic_group<'a, O>( + &'a self, + group_id: usize, + ) -> impl Iterator, &Diagnostic)> + 'a + where + O: 'a + FromAnchor, + { + let content = self.content(); + self.diagnostics + .filter(content, move |diagnostic| diagnostic.group_id == group_id) .map(move |(_, range, diagnostic)| (range, diagnostic)) } @@ -1608,51 +1651,61 @@ impl Snapshot { .all(|chunk| chunk.matches(|c: char| !c.is_whitespace()).next().is_none()) } - pub fn highlighted_text_for_range( - &mut self, + pub fn chunks<'a, T: ToOffset>( + &'a self, range: Range, - ) -> HighlightedChunks { + theme: Option<&'a SyntaxTheme>, + ) -> Chunks<'a> { let range = range.start.to_offset(&*self)..range.end.to_offset(&*self); + let mut highlights = None; let mut diagnostic_endpoints = Vec::::new(); - for (_, range, diagnostic) in - self.diagnostics - .intersecting_ranges(range.clone(), self.content(), true) - { - diagnostic_endpoints.push(DiagnosticEndpoint { - offset: range.start, - is_start: true, - severity: diagnostic.severity, - }); - diagnostic_endpoints.push(DiagnosticEndpoint { - offset: range.end, - is_start: false, - severity: diagnostic.severity, - }); - } - diagnostic_endpoints.sort_unstable_by_key(|endpoint| (endpoint.offset, !endpoint.is_start)); - let diagnostic_endpoints = diagnostic_endpoints.into_iter().peekable(); + if let Some(theme) = theme { + for (_, range, diagnostic) in + self.diagnostics + .intersecting_ranges(range.clone(), self.content(), true) + { + diagnostic_endpoints.push(DiagnosticEndpoint { + offset: range.start, + is_start: true, + severity: diagnostic.severity, + }); + diagnostic_endpoints.push(DiagnosticEndpoint { + offset: range.end, + is_start: false, + severity: diagnostic.severity, + }); + } + diagnostic_endpoints + .sort_unstable_by_key(|endpoint| (endpoint.offset, !endpoint.is_start)); - let chunks = self.text.as_rope().chunks_in_range(range.clone()); - let highlights = if let Some((language, tree)) = self.language.as_ref().zip(self.tree.as_ref()) { - let captures = self.query_cursor.set_byte_range(range.clone()).captures( + let mut query_cursor = QueryCursorHandle::new(); + + // TODO - add a Tree-sitter API to remove the need for this. + let cursor = unsafe { + std::mem::transmute::<_, &'static mut QueryCursor>(query_cursor.deref_mut()) + }; + let captures = cursor.set_byte_range(range.clone()).captures( &language.highlights_query, tree.root_node(), TextProvider(self.text.as_rope()), ); - - Some(Highlights { + highlights = Some(Highlights { captures, next_capture: None, stack: Default::default(), highlight_map: language.highlight_map(), + _query_cursor: query_cursor, + theme, }) - } else { - None - }; + } + } - HighlightedChunks { + let diagnostic_endpoints = diagnostic_endpoints.into_iter().peekable(); + let chunks = self.text.as_rope().chunks_in_range(range.clone()); + + Chunks { range, chunks, diagnostic_endpoints, @@ -1673,7 +1726,6 @@ impl Clone for Snapshot { diagnostics: self.diagnostics.clone(), is_parsing: self.is_parsing, language: self.language.clone(), - query_cursor: QueryCursorHandle::new(), } } } @@ -1704,7 +1756,9 @@ impl<'a> Iterator for ByteChunks<'a> { } } -impl<'a> HighlightedChunks<'a> { +unsafe impl<'a> Send for Chunks<'a> {} + +impl<'a> Chunks<'a> { pub fn seek(&mut self, offset: usize) { self.range.start = offset; self.chunks.seek(self.range.start); @@ -1763,8 +1817,8 @@ impl<'a> HighlightedChunks<'a> { } } -impl<'a> Iterator for HighlightedChunks<'a> { - type Item = HighlightedChunk<'a>; +impl<'a> Iterator for Chunks<'a> { + type Item = Chunk<'a>; fn next(&mut self) -> Option { let mut next_capture_start = usize::MAX; @@ -1813,12 +1867,12 @@ impl<'a> Iterator for HighlightedChunks<'a> { let mut chunk_end = (self.chunks.offset() + chunk.len()) .min(next_capture_start) .min(next_diagnostic_endpoint); - let mut highlight_id = HighlightId::default(); - if let Some((parent_capture_end, parent_highlight_id)) = - self.highlights.as_ref().and_then(|h| h.stack.last()) - { - chunk_end = chunk_end.min(*parent_capture_end); - highlight_id = *parent_highlight_id; + let mut highlight_style = None; + if let Some(highlights) = self.highlights.as_ref() { + if let Some((parent_capture_end, parent_highlight_id)) = highlights.stack.last() { + chunk_end = chunk_end.min(*parent_capture_end); + highlight_style = parent_highlight_id.style(highlights.theme); + } } let slice = @@ -1828,9 +1882,9 @@ impl<'a> Iterator for HighlightedChunks<'a> { self.chunks.next().unwrap(); } - Some(HighlightedChunk { + Some(Chunk { text: slice, - highlight_id, + highlight_style, diagnostic: self.current_diagnostic_severity(), }) } else { @@ -1888,6 +1942,44 @@ impl ToTreeSitterPoint for Point { } } +trait ToPointUtf16 { + fn to_point_utf16(self) -> PointUtf16; +} + +impl ToPointUtf16 for lsp::Position { + fn to_point_utf16(self) -> PointUtf16 { + PointUtf16::new(self.line, self.character) + } +} + +fn diagnostic_ranges<'a>( + diagnostic: &'a lsp::Diagnostic, + abs_path: Option<&'a Path>, +) -> impl 'a + Iterator> { + diagnostic + .related_information + .iter() + .flatten() + .filter_map(move |info| { + if info.location.uri.to_file_path().ok()? == abs_path? { + let info_start = PointUtf16::new( + info.location.range.start.line, + info.location.range.start.character, + ); + let info_end = PointUtf16::new( + info.location.range.end.line, + info.location.range.end.character, + ); + Some(info_start..info_end) + } else { + None + } + }) + .chain(Some( + diagnostic.range.start.to_point_utf16()..diagnostic.range.end.to_point_utf16(), + )) +} + fn contiguous_ranges( values: impl IntoIterator, max_len: usize, diff --git a/crates/language/src/proto.rs b/crates/language/src/proto.rs index 4e6a8316c2..6a259e0726 100644 --- a/crates/language/src/proto.rs +++ b/crates/language/src/proto.rs @@ -141,6 +141,8 @@ pub fn serialize_diagnostics(map: &AnchorRangeMultimap) -> proto::Di DiagnosticSeverity::HINT => proto::diagnostic::Severity::Hint, _ => proto::diagnostic::Severity::None, } as i32, + group_id: diagnostic.group_id as u64, + is_primary: diagnostic.is_primary, }) .collect(), } @@ -308,6 +310,8 @@ pub fn deserialize_diagnostics(message: proto::DiagnosticSet) -> AnchorRangeMult proto::diagnostic::Severity::None => return None, }, message: diagnostic.message, + group_id: diagnostic.group_id as usize, + is_primary: diagnostic.is_primary, }, )) }), diff --git a/crates/language/src/tests.rs b/crates/language/src/tests.rs index b83fb4c671..671755195d 100644 --- a/crates/language/src/tests.rs +++ b/crates/language/src/tests.rs @@ -482,14 +482,18 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) { Point::new(3, 9)..Point::new(3, 11), &Diagnostic { severity: DiagnosticSeverity::ERROR, - message: "undefined variable 'BB'".to_string() + message: "undefined variable 'BB'".to_string(), + group_id: 1, + is_primary: true, }, ), ( Point::new(4, 9)..Point::new(4, 12), &Diagnostic { severity: DiagnosticSeverity::ERROR, - message: "undefined variable 'CCC'".to_string() + message: "undefined variable 'CCC'".to_string(), + group_id: 2, + is_primary: true, } ) ] @@ -545,14 +549,18 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) { Point::new(2, 9)..Point::new(2, 12), &Diagnostic { severity: DiagnosticSeverity::WARNING, - message: "unreachable statement".to_string() + message: "unreachable statement".to_string(), + group_id: 1, + is_primary: true, } ), ( Point::new(2, 9)..Point::new(2, 10), &Diagnostic { severity: DiagnosticSeverity::ERROR, - message: "undefined variable 'A'".to_string() + message: "undefined variable 'A'".to_string(), + group_id: 0, + is_primary: true, }, ) ] @@ -620,14 +628,18 @@ async fn test_diagnostics(mut cx: gpui::TestAppContext) { Point::new(2, 21)..Point::new(2, 22), &Diagnostic { severity: DiagnosticSeverity::ERROR, - message: "undefined variable 'A'".to_string() + message: "undefined variable 'A'".to_string(), + group_id: 0, + is_primary: true, } ), ( Point::new(3, 9)..Point::new(3, 11), &Diagnostic { severity: DiagnosticSeverity::ERROR, - message: "undefined variable 'BB'".to_string() + message: "undefined variable 'BB'".to_string(), + group_id: 1, + is_primary: true, }, ) ] @@ -694,12 +706,223 @@ async fn test_empty_diagnostic_ranges(mut cx: gpui::TestAppContext) { }); } -fn chunks_with_diagnostics( +#[gpui::test] +async fn test_grouped_diagnostics(mut cx: gpui::TestAppContext) { + cx.add_model(|cx| { + let text = " + fn foo(mut v: Vec) { + for x in &v { + v.push(1); + } + } + " + .unindent(); + + let file = FakeFile::new("/example.rs"); + let mut buffer = Buffer::from_file(0, text, Box::new(file.clone()), cx); + buffer.set_language(Some(Arc::new(rust_lang())), None, cx); + let diagnostics = vec![ + lsp::Diagnostic { + range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)), + severity: Some(DiagnosticSeverity::WARNING), + message: "error 1".to_string(), + related_information: Some(vec![lsp::DiagnosticRelatedInformation { + location: lsp::Location { + uri: lsp::Url::from_file_path(&file.abs_path).unwrap(), + range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)), + }, + message: "error 1 hint 1".to_string(), + }]), + ..Default::default() + }, + lsp::Diagnostic { + range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)), + severity: Some(DiagnosticSeverity::HINT), + message: "error 1 hint 1".to_string(), + related_information: Some(vec![lsp::DiagnosticRelatedInformation { + location: lsp::Location { + uri: lsp::Url::from_file_path(&file.abs_path).unwrap(), + range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)), + }, + message: "original diagnostic".to_string(), + }]), + ..Default::default() + }, + lsp::Diagnostic { + range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)), + severity: Some(DiagnosticSeverity::ERROR), + message: "error 2".to_string(), + related_information: Some(vec![ + lsp::DiagnosticRelatedInformation { + location: lsp::Location { + uri: lsp::Url::from_file_path(&file.abs_path).unwrap(), + range: lsp::Range::new( + lsp::Position::new(1, 13), + lsp::Position::new(1, 15), + ), + }, + message: "error 2 hint 1".to_string(), + }, + lsp::DiagnosticRelatedInformation { + location: lsp::Location { + uri: lsp::Url::from_file_path(&file.abs_path).unwrap(), + range: lsp::Range::new( + lsp::Position::new(1, 13), + lsp::Position::new(1, 15), + ), + }, + message: "error 2 hint 2".to_string(), + }, + ]), + ..Default::default() + }, + lsp::Diagnostic { + range: lsp::Range::new(lsp::Position::new(1, 13), lsp::Position::new(1, 15)), + severity: Some(DiagnosticSeverity::HINT), + message: "error 2 hint 1".to_string(), + related_information: Some(vec![lsp::DiagnosticRelatedInformation { + location: lsp::Location { + uri: lsp::Url::from_file_path(&file.abs_path).unwrap(), + range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)), + }, + message: "original diagnostic".to_string(), + }]), + ..Default::default() + }, + lsp::Diagnostic { + range: lsp::Range::new(lsp::Position::new(1, 13), lsp::Position::new(1, 15)), + severity: Some(DiagnosticSeverity::HINT), + message: "error 2 hint 2".to_string(), + related_information: Some(vec![lsp::DiagnosticRelatedInformation { + location: lsp::Location { + uri: lsp::Url::from_file_path(&file.abs_path).unwrap(), + range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)), + }, + message: "original diagnostic".to_string(), + }]), + ..Default::default() + }, + ]; + buffer.update_diagnostics(None, diagnostics, cx).unwrap(); + assert_eq!( + buffer + .diagnostics_in_range::<_, Point>(0..buffer.len()) + .collect::>(), + &[ + ( + Point::new(1, 8)..Point::new(1, 9), + &Diagnostic { + severity: DiagnosticSeverity::WARNING, + message: "error 1".to_string(), + group_id: 0, + is_primary: true, + } + ), + ( + Point::new(1, 8)..Point::new(1, 9), + &Diagnostic { + severity: DiagnosticSeverity::HINT, + message: "error 1 hint 1".to_string(), + group_id: 0, + is_primary: false, + } + ), + ( + Point::new(1, 13)..Point::new(1, 15), + &Diagnostic { + severity: DiagnosticSeverity::HINT, + message: "error 2 hint 1".to_string(), + group_id: 1, + is_primary: false, + } + ), + ( + Point::new(1, 13)..Point::new(1, 15), + &Diagnostic { + severity: DiagnosticSeverity::HINT, + message: "error 2 hint 2".to_string(), + group_id: 1, + is_primary: false, + } + ), + ( + Point::new(2, 8)..Point::new(2, 17), + &Diagnostic { + severity: DiagnosticSeverity::ERROR, + message: "error 2".to_string(), + group_id: 1, + is_primary: true, + } + ) + ] + ); + + assert_eq!( + buffer.diagnostic_group(0).collect::>(), + &[ + ( + Point::new(1, 8)..Point::new(1, 9), + &Diagnostic { + severity: DiagnosticSeverity::WARNING, + message: "error 1".to_string(), + group_id: 0, + is_primary: true, + } + ), + ( + Point::new(1, 8)..Point::new(1, 9), + &Diagnostic { + severity: DiagnosticSeverity::HINT, + message: "error 1 hint 1".to_string(), + group_id: 0, + is_primary: false, + } + ), + ] + ); + assert_eq!( + buffer.diagnostic_group(1).collect::>(), + &[ + ( + Point::new(1, 13)..Point::new(1, 15), + &Diagnostic { + severity: DiagnosticSeverity::HINT, + message: "error 2 hint 1".to_string(), + group_id: 1, + is_primary: false, + } + ), + ( + Point::new(1, 13)..Point::new(1, 15), + &Diagnostic { + severity: DiagnosticSeverity::HINT, + message: "error 2 hint 2".to_string(), + group_id: 1, + is_primary: false, + } + ), + ( + Point::new(2, 8)..Point::new(2, 17), + &Diagnostic { + severity: DiagnosticSeverity::ERROR, + message: "error 2".to_string(), + group_id: 1, + is_primary: true, + } + ) + ] + ); + + buffer + }); +} + +fn chunks_with_diagnostics( buffer: &Buffer, range: Range, ) -> Vec<(String, Option)> { let mut chunks: Vec<(String, Option)> = Vec::new(); - for chunk in buffer.snapshot().highlighted_text_for_range(range) { + for chunk in buffer.snapshot().chunks(range, Some(&Default::default())) { if chunks .last() .map_or(false, |prev_chunk| prev_chunk.1 == chunk.diagnostic) @@ -765,3 +988,80 @@ fn rust_lang() -> Language { fn empty(point: Point) -> Range { point..point } + +#[derive(Clone)] +struct FakeFile { + abs_path: PathBuf, +} + +impl FakeFile { + fn new(abs_path: impl Into) -> Self { + Self { + abs_path: abs_path.into(), + } + } +} + +impl File for FakeFile { + fn worktree_id(&self) -> usize { + todo!() + } + + fn entry_id(&self) -> Option { + todo!() + } + + fn mtime(&self) -> SystemTime { + SystemTime::now() + } + + fn path(&self) -> &Arc { + todo!() + } + + fn abs_path(&self) -> Option { + Some(self.abs_path.clone()) + } + + fn full_path(&self) -> PathBuf { + todo!() + } + + fn file_name(&self) -> Option { + todo!() + } + + fn is_deleted(&self) -> bool { + todo!() + } + + fn save( + &self, + _: u64, + _: Rope, + _: clock::Global, + _: &mut MutableAppContext, + ) -> Task> { + todo!() + } + + fn load_local(&self, _: &AppContext) -> Option>> { + todo!() + } + + fn buffer_updated(&self, _: u64, _: super::Operation, _: &mut MutableAppContext) { + todo!() + } + + fn buffer_removed(&self, _: u64, _: &mut MutableAppContext) { + todo!() + } + + fn boxed_clone(&self) -> Box { + todo!() + } + + fn as_any(&self) -> &dyn Any { + todo!() + } +} diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 10025b3054..e6b444b548 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -3633,7 +3633,9 @@ mod tests { Point::new(0, 9)..Point::new(0, 10), &Diagnostic { severity: lsp::DiagnosticSeverity::ERROR, - message: "undefined variable 'A'".to_string() + message: "undefined variable 'A'".to_string(), + group_id: 0, + is_primary: true } )] ) diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 8753f27dba..950178be34 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -256,6 +256,8 @@ message Diagnostic { uint64 end = 2; Severity severity = 3; string message = 4; + uint64 group_id = 5; + bool is_primary = 6; enum Severity { None = 0; Error = 1; diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index aebebc5891..0c60ba3cbd 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -1713,15 +1713,19 @@ mod tests { ( Point::new(0, 4)..Point::new(0, 7), &Diagnostic { + group_id: 0, message: "message 1".to_string(), severity: lsp::DiagnosticSeverity::ERROR, + is_primary: true } ), ( Point::new(0, 10)..Point::new(0, 13), &Diagnostic { + group_id: 1, severity: lsp::DiagnosticSeverity::WARNING, - message: "message 2".to_string() + message: "message 2".to_string(), + is_primary: true } ) ] diff --git a/crates/sum_tree/src/lib.rs b/crates/sum_tree/src/lib.rs index eeef956324..8b4a45519f 100644 --- a/crates/sum_tree/src/lib.rs +++ b/crates/sum_tree/src/lib.rs @@ -31,6 +31,12 @@ pub trait Summary: Default + Clone + fmt::Debug { pub trait Dimension<'a, S: Summary>: Clone + fmt::Debug + Default { fn add_summary(&mut self, _summary: &'a S, _: &S::Context); + + fn from_summary(summary: &'a S, cx: &S::Context) -> Self { + let mut dimension = Self::default(); + dimension.add_summary(summary, cx); + dimension + } } impl<'a, T: Summary> Dimension<'a, T> for T { diff --git a/crates/theme/src/lib.rs b/crates/theme/src/lib.rs index c4fbcc3031..133475426c 100644 --- a/crates/theme/src/lib.rs +++ b/crates/theme/src/lib.rs @@ -227,12 +227,21 @@ pub struct EditorStyle { pub line_number_active: Color, pub guest_selections: Vec, pub syntax: Arc, - pub error_underline: Color, - pub warning_underline: Color, - #[serde(default)] - pub information_underline: Color, - #[serde(default)] - pub hint_underline: Color, + pub error_diagnostic: DiagnosticStyle, + pub invalid_error_diagnostic: DiagnosticStyle, + pub warning_diagnostic: DiagnosticStyle, + pub invalid_warning_diagnostic: DiagnosticStyle, + pub information_diagnostic: DiagnosticStyle, + pub invalid_information_diagnostic: DiagnosticStyle, + pub hint_diagnostic: DiagnosticStyle, + pub invalid_hint_diagnostic: DiagnosticStyle, +} + +#[derive(Copy, Clone, Deserialize, Default)] +pub struct DiagnosticStyle { + pub text: Color, + #[serde(flatten)] + pub block: BlockStyle, } #[derive(Clone, Copy, Default, Deserialize)] @@ -251,6 +260,14 @@ pub struct InputEditorStyle { pub selection: SelectionStyle, } +#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq)] +pub struct BlockStyle { + pub background: Option, + pub border: Option, + pub gutter_background: Option, + pub gutter_border: Option, +} + impl EditorStyle { pub fn placeholder_text(&self) -> &TextStyle { self.placeholder_text.as_ref().unwrap_or(&self.text) @@ -273,10 +290,14 @@ impl InputEditorStyle { line_number_active: Default::default(), guest_selections: Default::default(), syntax: Default::default(), - error_underline: Default::default(), - warning_underline: Default::default(), - information_underline: Default::default(), - hint_underline: Default::default(), + error_diagnostic: Default::default(), + invalid_error_diagnostic: Default::default(), + warning_diagnostic: Default::default(), + invalid_warning_diagnostic: Default::default(), + information_diagnostic: Default::default(), + invalid_information_diagnostic: Default::default(), + hint_diagnostic: Default::default(), + invalid_hint_diagnostic: Default::default(), } } } diff --git a/crates/workspace/src/items.rs b/crates/workspace/src/items.rs index 9a14379c7a..58358cdf47 100644 --- a/crates/workspace/src/items.rs +++ b/crates/workspace/src/items.rs @@ -258,15 +258,12 @@ impl DiagnosticMessage { fn update(&mut self, editor: ViewHandle, cx: &mut ViewContext) { let editor = editor.read(cx); - let cursor_position = editor - .selections::(cx) - .max_by_key(|selection| selection.id) - .unwrap() - .head(); + let cursor_position = editor.newest_selection(cx).head(); let new_diagnostic = editor .buffer() .read(cx) .diagnostics_in_range::(cursor_position..cursor_position) + .filter(|(range, _)| !range.is_empty()) .min_by_key(|(range, diagnostic)| (diagnostic.severity, range.len())) .map(|(_, diagnostic)| diagnostic.clone()); if new_diagnostic != self.diagnostic { diff --git a/crates/zed/assets/themes/_base.toml b/crates/zed/assets/themes/_base.toml index 77167df8b0..16861b74b6 100644 --- a/crates/zed/assets/themes/_base.toml +++ b/crates/zed/assets/themes/_base.toml @@ -173,7 +173,7 @@ corner_radius = 6 [project_panel] extends = "$panel" -padding.top = 6 # ($workspace.tab.height - $project_panel.entry.height) / 2 +padding.top = 6 # ($workspace.tab.height - $project_panel.entry.height) / 2 [project_panel.entry] text = "$text.1" @@ -235,7 +235,12 @@ line_number = "$text.2.color" line_number_active = "$text.0.color" selection = "$selection.host" guest_selections = "$selection.guests" -error_underline = "$status.bad" -warning_underline = "$status.warn" -info_underline = "$status.info" -hint_underline = "$status.info" +error_color = "$status.bad" +error_diagnostic = { text = "$status.bad" } +invalid_error_diagnostic = { text = "$text.3.color" } +warning_diagnostic = { text = "$status.warn" } +invalid_warning_diagnostic = { text = "$text.3.color" } +information_diagnostic = { text = "$status.info" } +invalid_information_diagnostic = { text = "$text.3.color" } +hint_diagnostic = { text = "$status.info" } +invalid_hint_diagnostic = { text = "$text.3.color" }