diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index e401f850319375e6e6c841b665a40d3c2be1bea9..fe7895cba556895b168bce2e125438853617e0dd 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -275,7 +275,7 @@ impl DisplaySnapshot { pub fn text_chunks(&self, display_row: u32) -> impl Iterator { self.blocks_snapshot - .chunks(display_row..self.max_point().row() + 1, false) + .chunks(display_row..self.max_point().row() + 1, false, None) .map(|h| h.text) } @@ -284,7 +284,8 @@ impl DisplaySnapshot { display_rows: Range, language_aware: bool, ) -> DisplayChunks<'a> { - self.blocks_snapshot.chunks(display_rows, language_aware) + self.blocks_snapshot + .chunks(display_rows, language_aware, Some(&self.text_highlights)) } pub fn chars_at<'a>(&'a self, point: DisplayPoint) -> impl Iterator + 'a { diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index 7b90179c8ffbfbf1903ec0097708e891c3a94f9a..770ebc8fbef6ce852400e18632419e03717a9b93 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -1,4 +1,7 @@ -use super::wrap_map::{self, WrapEdit, WrapPoint, WrapSnapshot}; +use super::{ + wrap_map::{self, WrapEdit, WrapPoint, WrapSnapshot}, + TextHighlights, +}; use crate::{Anchor, ToPoint as _}; use collections::{Bound, HashMap, HashSet}; use gpui::{AppContext, ElementBox}; @@ -555,12 +558,17 @@ impl<'a> BlockMapWriter<'a> { impl BlockSnapshot { #[cfg(test)] pub fn text(&self) -> String { - self.chunks(0..self.transforms.summary().output_rows, false) + self.chunks(0..self.transforms.summary().output_rows, false, None) .map(|chunk| chunk.text) .collect() } - pub fn chunks<'a>(&'a self, rows: Range, language_aware: bool) -> BlockChunks<'a> { + pub fn chunks<'a>( + &'a self, + rows: Range, + language_aware: bool, + text_highlights: Option<&'a TextHighlights>, + ) -> BlockChunks<'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 = { @@ -588,9 +596,11 @@ impl BlockSnapshot { cursor.start().1 .0 + overshoot }; BlockChunks { - input_chunks: self - .wrap_snapshot - .chunks(input_start..input_end, language_aware), + input_chunks: self.wrap_snapshot.chunks( + input_start..input_end, + language_aware, + text_highlights, + ), input_chunk: Default::default(), transforms: cursor, output_row: rows.start, @@ -1436,7 +1446,11 @@ mod tests { 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..blocks_snapshot.max_point().row + 1, false) + .chunks( + start_row as u32..blocks_snapshot.max_point().row + 1, + false, + None, + ) .map(|chunk| chunk.text) .collect::(); assert_eq!( diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index facb5ab2a7f1ee86d67e69129d024ab011fe2b2c..295b7710efd186f7048e1dc4a55dbb86ec268322 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -2,16 +2,22 @@ use crate::{ multi_buffer::MultiBufferRows, Anchor, AnchorRangeExt, MultiBufferChunks, MultiBufferSnapshot, ToOffset, }; +use collections::BTreeMap; +use gpui::fonts::HighlightStyle; use language::{Chunk, Edit, Point, PointUtf16, TextSummary}; use parking_lot::Mutex; use std::{ + any::TypeId, cmp::{self, Ordering}, - iter, + iter::{self, Peekable}, ops::{Range, Sub}, sync::atomic::{AtomicUsize, Ordering::SeqCst}, + vec, }; use sum_tree::{Bias, Cursor, FilterCursor, SumTree}; +use super::TextHighlights; + pub trait ToFoldPoint { fn to_fold_point(&self, snapshot: &FoldSnapshot, bias: Bias) -> FoldPoint; } @@ -95,6 +101,12 @@ impl ToFoldPoint for Point { } } +impl<'a> sum_tree::Dimension<'a, TransformSummary> for FoldPoint { + fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) { + self.0 += &summary.output.lines; + } +} + pub struct FoldMapWriter<'a>(&'a mut FoldMap); impl<'a> FoldMapWriter<'a> { @@ -500,7 +512,7 @@ impl FoldSnapshot { #[cfg(test)] pub fn text(&self) -> String { - self.chunks(FoldOffset(0)..self.len(), false) + self.chunks(FoldOffset(0)..self.len(), false, None) .map(|c| c.text) .collect() } @@ -640,20 +652,96 @@ impl FoldSnapshot { pub fn chars_at(&self, start: FoldPoint) -> impl '_ + Iterator { let start = start.to_offset(self); - self.chunks(start..self.len(), false) + self.chunks(start..self.len(), false, None) .flat_map(|chunk| chunk.text.chars()) } - pub fn chunks<'a>(&'a self, range: Range, language_aware: bool) -> FoldChunks<'a> { + pub fn chunks<'a>( + &'a self, + range: Range, + language_aware: bool, + text_highlights: Option<&'a TextHighlights>, + ) -> FoldChunks<'a> { + let mut highlight_endpoints = Vec::new(); let mut transform_cursor = self.transforms.cursor::<(FoldOffset, usize)>(); - transform_cursor.seek(&range.end, Bias::Right, &()); - let overshoot = range.end.0 - transform_cursor.start().0 .0; - let buffer_end = transform_cursor.start().1 + overshoot; + let buffer_end = { + transform_cursor.seek(&range.end, Bias::Right, &()); + let overshoot = range.end.0 - transform_cursor.start().0 .0; + transform_cursor.start().1 + overshoot + }; - transform_cursor.seek(&range.start, Bias::Right, &()); - let overshoot = range.start.0 - transform_cursor.start().0 .0; - let buffer_start = transform_cursor.start().1 + overshoot; + let buffer_start = { + transform_cursor.seek(&range.start, Bias::Right, &()); + let overshoot = range.start.0 - transform_cursor.start().0 .0; + transform_cursor.start().1 + overshoot + }; + + if let Some(text_highlights) = text_highlights { + if !text_highlights.is_empty() { + while transform_cursor.start().0 < range.end { + if !transform_cursor.item().unwrap().is_fold() { + let transform_start = self + .buffer_snapshot + .anchor_after(cmp::max(buffer_start, transform_cursor.start().1)); + + let transform_end = { + let overshoot = range.end.0 - transform_cursor.start().0 .0; + self.buffer_snapshot.anchor_before(cmp::min( + transform_cursor.end(&()).1, + transform_cursor.start().1 + overshoot, + )) + }; + + for (tag, highlights) in text_highlights.iter() { + let style = highlights.0; + let ranges = &highlights.1; + + let start_ix = match ranges.binary_search_by(|probe| { + let cmp = probe + .end + .cmp(&transform_start, &self.buffer_snapshot()) + .unwrap(); + if cmp.is_gt() { + Ordering::Greater + } else { + Ordering::Less + } + }) { + Ok(i) | Err(i) => i, + }; + for range in &ranges[start_ix..] { + if range + .start + .cmp(&transform_end, &self.buffer_snapshot) + .unwrap() + .is_ge() + { + break; + } + + highlight_endpoints.push(HighlightEndpoint { + offset: range.start.to_offset(&self.buffer_snapshot), + is_start: true, + tag: *tag, + style, + }); + highlight_endpoints.push(HighlightEndpoint { + offset: range.end.to_offset(&self.buffer_snapshot), + is_start: false, + tag: *tag, + style, + }); + } + } + } + + transform_cursor.next(&()); + } + highlight_endpoints.sort(); + transform_cursor.seek(&range.start, Bias::Right, &()); + } + } FoldChunks { transform_cursor, @@ -664,6 +752,8 @@ impl FoldSnapshot { buffer_offset: buffer_start, output_offset: range.start.0, max_output_offset: range.end.0, + highlight_endpoints: highlight_endpoints.into_iter().peekable(), + active_highlights: Default::default(), } } @@ -952,6 +1042,8 @@ pub struct FoldChunks<'a> { buffer_offset: usize, output_offset: usize, max_output_offset: usize, + highlight_endpoints: Peekable>, + active_highlights: BTreeMap, HighlightStyle>, } impl<'a> Iterator for FoldChunks<'a> { @@ -990,6 +1082,21 @@ impl<'a> Iterator for FoldChunks<'a> { }); } + let mut next_highlight_endpoint = usize::MAX; + while let Some(endpoint) = self.highlight_endpoints.peek().copied() { + if endpoint.offset <= self.buffer_offset { + if endpoint.is_start { + self.active_highlights.insert(endpoint.tag, endpoint.style); + } else { + self.active_highlights.remove(&endpoint.tag); + } + self.highlight_endpoints.next(); + } else { + next_highlight_endpoint = endpoint.offset; + break; + } + } + // Retrieve a chunk from the current location in the buffer. if self.buffer_chunk.is_none() { let chunk_offset = self.buffer_chunks.offset(); @@ -997,20 +1104,31 @@ impl<'a> Iterator for FoldChunks<'a> { } // Otherwise, take a chunk from the buffer's text. - if let Some((chunk_offset, mut chunk)) = self.buffer_chunk { - let offset_in_chunk = self.buffer_offset - chunk_offset; - chunk.text = &chunk.text[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.text.len() >= region_end { - chunk.text = &chunk.text[0..region_end]; + if let Some((buffer_chunk_start, mut chunk)) = self.buffer_chunk { + let buffer_chunk_end = buffer_chunk_start + chunk.text.len(); + let transform_end = self.transform_cursor.end(&()).1; + let chunk_end = buffer_chunk_end + .min(transform_end) + .min(next_highlight_endpoint); + + chunk.text = &chunk.text + [self.buffer_offset - buffer_chunk_start..chunk_end - buffer_chunk_start]; + + if !self.active_highlights.is_empty() { + let mut highlight_style = HighlightStyle::default(); + for active_highlight in self.active_highlights.values() { + highlight_style.highlight(*active_highlight); + } + chunk.highlight_style = Some(highlight_style); + } + + if chunk_end == transform_end { self.transform_cursor.next(&()); - } else { + } else if chunk_end == buffer_chunk_end { self.buffer_chunk.take(); } - self.buffer_offset += chunk.text.len(); + self.buffer_offset = chunk_end; self.output_offset += chunk.text.len(); return Some(chunk); } @@ -1019,9 +1137,25 @@ impl<'a> Iterator for FoldChunks<'a> { } } -impl<'a> sum_tree::Dimension<'a, TransformSummary> for FoldPoint { - fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) { - self.0 += &summary.output.lines; +#[derive(Copy, Clone, Eq, PartialEq)] +struct HighlightEndpoint { + offset: usize, + is_start: bool, + tag: Option, + style: HighlightStyle, +} + +impl PartialOrd for HighlightEndpoint { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for HighlightEndpoint { + fn cmp(&self, other: &Self) -> Ordering { + self.offset + .cmp(&other.offset) + .then_with(|| self.is_start.cmp(&other.is_start)) } } @@ -1078,7 +1212,8 @@ mod tests { use super::*; use crate::{MultiBuffer, ToPoint}; use rand::prelude::*; - use std::{env, mem}; + use std::{cmp::Reverse, env, mem, sync::Arc}; + use sum_tree::TreeMap; use text::RandomCharIter; use util::test::sample_text; use Bias::{Left, Right}; @@ -1283,6 +1418,25 @@ mod tests { let (mut initial_snapshot, _) = map.read(buffer_snapshot.clone(), vec![]); let mut snapshot_edits = Vec::new(); + let mut highlights = TreeMap::default(); + let highlight_count = rng.gen_range(0_usize..10); + let mut highlight_ranges = (0..highlight_count) + .map(|_| buffer_snapshot.random_byte_range(0, &mut rng)) + .collect::>(); + highlight_ranges.sort_by_key(|range| (range.start, Reverse(range.end))); + log::info!("highlighting ranges {:?}", highlight_ranges); + let highlight_ranges = highlight_ranges + .into_iter() + .map(|range| { + buffer_snapshot.anchor_before(range.start)..buffer_snapshot.anchor_after(range.end) + }) + .collect::>(); + + highlights.insert( + Some(TypeId::of::<()>()), + Arc::new((HighlightStyle::default(), highlight_ranges)), + ); + for _ in 0..operations { log::info!("text: {:?}", buffer_snapshot.text()); let mut buffer_edits = Vec::new(); @@ -1407,7 +1561,7 @@ mod tests { let text = &expected_text[start.0..end.0]; assert_eq!( snapshot - .chunks(start..end, false) + .chunks(start..end, false, Some(&highlights)) .map(|c| c.text) .collect::(), text, diff --git a/crates/editor/src/display_map/tab_map.rs b/crates/editor/src/display_map/tab_map.rs index e2239e76715278ba84e0ade5d3b56c7bc9e0f082..696b50577aacee56a2482e95e7ca168729b0a401 100644 --- a/crates/editor/src/display_map/tab_map.rs +++ b/crates/editor/src/display_map/tab_map.rs @@ -1,4 +1,7 @@ -use super::fold_map::{self, FoldEdit, FoldPoint, FoldSnapshot, ToFoldPoint}; +use super::{ + fold_map::{self, FoldEdit, FoldPoint, FoldSnapshot, ToFoldPoint}, + TextHighlights, +}; use crate::MultiBufferSnapshot; use language::{rope, Chunk}; use parking_lot::Mutex; @@ -32,9 +35,10 @@ impl TabMap { let mut tab_edits = Vec::with_capacity(fold_edits.len()); for fold_edit in &mut fold_edits { let mut delta = 0; - for chunk in old_snapshot - .fold_snapshot - .chunks(fold_edit.old.end..max_offset, false) + for chunk in + old_snapshot + .fold_snapshot + .chunks(fold_edit.old.end..max_offset, false, None) { let patterns: &[_] = &['\t', '\n']; if let Some(ix) = chunk.text.find(patterns) { @@ -109,7 +113,7 @@ impl TabSnapshot { self.max_point() }; for c in self - .chunks(range.start..line_end, false) + .chunks(range.start..line_end, false, None) .flat_map(|chunk| chunk.text.chars()) { if c == '\n' { @@ -123,7 +127,7 @@ impl TabSnapshot { last_line_chars = first_line_chars; } else { for _ in self - .chunks(TabPoint::new(range.end.row(), 0)..range.end, false) + .chunks(TabPoint::new(range.end.row(), 0)..range.end, false, None) .flat_map(|chunk| chunk.text.chars()) { last_line_chars += 1; @@ -143,7 +147,12 @@ impl TabSnapshot { self.fold_snapshot.version } - pub fn chunks<'a>(&'a self, range: Range, language_aware: bool) -> TabChunks<'a> { + pub fn chunks<'a>( + &'a self, + range: Range, + language_aware: bool, + text_highlights: Option<&'a TextHighlights>, + ) -> TabChunks<'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); @@ -158,9 +167,11 @@ impl TabSnapshot { }; TabChunks { - fold_chunks: self - .fold_snapshot - .chunks(input_start..input_end, language_aware), + fold_chunks: self.fold_snapshot.chunks( + input_start..input_end, + language_aware, + text_highlights, + ), column: expanded_char_column, output_position: range.start.0, max_output_position: range.end.0, @@ -179,7 +190,7 @@ impl TabSnapshot { #[cfg(test)] pub fn text(&self) -> String { - self.chunks(TabPoint::zero()..self.max_point(), false) + self.chunks(TabPoint::zero()..self.max_point(), false, None) .map(|chunk| chunk.text) .collect() } @@ -492,7 +503,7 @@ mod tests { assert_eq!( expected_text, tabs_snapshot - .chunks(start..end, false) + .chunks(start..end, false, None) .map(|c| c.text) .collect::(), "chunks({:?}..{:?})", diff --git a/crates/editor/src/display_map/wrap_map.rs b/crates/editor/src/display_map/wrap_map.rs index 7858197902dcd90a6f98e7739991744ed8639af6..2fab37fb30997ce867994b922f89add56e47d5d9 100644 --- a/crates/editor/src/display_map/wrap_map.rs +++ b/crates/editor/src/display_map/wrap_map.rs @@ -1,6 +1,7 @@ use super::{ fold_map, tab_map::{self, TabEdit, TabPoint, TabSnapshot}, + TextHighlights, }; use crate::{MultiBufferSnapshot, Point}; use gpui::{ @@ -433,6 +434,7 @@ impl WrapSnapshot { let mut chunks = new_tab_snapshot.chunks( TabPoint::new(edit.new_rows.start, 0)..new_tab_snapshot.max_point(), false, + None, ); let mut edit_transforms = Vec::::new(); for _ in edit.new_rows.start..edit.new_rows.end { @@ -558,11 +560,16 @@ impl WrapSnapshot { } pub fn text_chunks(&self, wrap_row: u32) -> impl Iterator { - self.chunks(wrap_row..self.max_point().row() + 1, false) + self.chunks(wrap_row..self.max_point().row() + 1, false, None) .map(|h| h.text) } - pub fn chunks<'a>(&'a self, rows: Range, language_aware: bool) -> WrapChunks<'a> { + pub fn chunks<'a>( + &'a self, + rows: Range, + language_aware: bool, + text_highlights: Option<&'a TextHighlights>, + ) -> WrapChunks<'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)>(); @@ -575,9 +582,11 @@ impl WrapSnapshot { .to_tab_point(output_end) .min(self.tab_snapshot.max_point()); WrapChunks { - input_chunks: self - .tab_snapshot - .chunks(input_start..input_end, language_aware), + input_chunks: self.tab_snapshot.chunks( + input_start..input_end, + language_aware, + text_highlights, + ), input_chunk: Default::default(), output_position: output_start, max_output_row: rows.end, @@ -1280,7 +1289,7 @@ mod tests { } let actual_text = self - .chunks(start_row..end_row, true) + .chunks(start_row..end_row, true, None) .map(|c| c.text) .collect::(); assert_eq!( diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index ded484e549675de172764b453bc1bfc8b35d608b..64009448570bb56f60fdd053ca9e3188b2f69e25 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -4398,16 +4398,16 @@ impl Editor { None, cx, ); - editor.highlight_background::( - vec![Anchor::min()..Anchor::max()], - style.diff_background_inserted, - cx, - ); editor }); - this.highlight_background::( + this.highlight_text::( vec![range.clone()], - style.diff_background_deleted, + HighlightStyle { + color: Color::transparent_black(), + font_properties: todo!(), + underline: todo!(), + fade_out: todo!(), + }, cx, ); this.update_selections( diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 64683faa962229a77ba6384881eed7ca4ea87d1d..93f67d0cc7ee5dc8a7e771fa6efbd866cb6e542e 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -213,25 +213,6 @@ impl MultiBuffer { this } - #[cfg(any(test, feature = "test-support"))] - pub fn build_simple(text: &str, cx: &mut gpui::MutableAppContext) -> ModelHandle { - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx)); - cx.add_model(|cx| Self::singleton(buffer, cx)) - } - - #[cfg(any(test, feature = "test-support"))] - pub fn build_random( - rng: &mut impl rand::Rng, - cx: &mut gpui::MutableAppContext, - ) -> ModelHandle { - cx.add_model(|cx| { - let mut multibuffer = MultiBuffer::new(0); - let mutation_count = rng.gen_range(1..=5); - multibuffer.randomly_edit_excerpts(rng, mutation_count, cx); - multibuffer - }) - } - pub fn replica_id(&self) -> ReplicaId { self.replica_id } @@ -1170,6 +1151,23 @@ impl MultiBuffer { #[cfg(any(test, feature = "test-support"))] impl MultiBuffer { + pub fn build_simple(text: &str, cx: &mut gpui::MutableAppContext) -> ModelHandle { + let buffer = cx.add_model(|cx| Buffer::new(0, text, cx)); + cx.add_model(|cx| Self::singleton(buffer, cx)) + } + + pub fn build_random( + rng: &mut impl rand::Rng, + cx: &mut gpui::MutableAppContext, + ) -> ModelHandle { + cx.add_model(|cx| { + let mut multibuffer = MultiBuffer::new(0); + let mutation_count = rng.gen_range(1..=5); + multibuffer.randomly_edit_excerpts(rng, mutation_count, cx); + multibuffer + }) + } + pub fn randomly_edit( &mut self, rng: &mut impl rand::Rng, @@ -2300,6 +2298,15 @@ impl MultiBufferSnapshot { } } +#[cfg(any(test, feature = "test-support"))] +impl MultiBufferSnapshot { + pub fn random_byte_range(&self, start_offset: usize, rng: &mut impl rand::Rng) -> Range { + let end = self.clip_offset(rng.gen_range(start_offset..=self.len()), Bias::Right); + let start = self.clip_offset(rng.gen_range(start_offset..=end), Bias::Right); + start..end + } +} + impl History { fn start_transaction(&mut self, now: Instant) -> Option { self.transaction_depth += 1; diff --git a/crates/gpui/src/color.rs b/crates/gpui/src/color.rs index 24e917a7779c21b5a50d11ed8a127eee8e064297..1057cbe7aa3dc690c1539fb6f91ad342b0198217 100644 --- a/crates/gpui/src/color.rs +++ b/crates/gpui/src/color.rs @@ -50,8 +50,9 @@ impl Color { } pub fn blend(source: Color, dest: Color) -> Color { - if dest.a == 255 { - return dest; + // If source is fully opaque, don't blend. + if source.a == 255 { + return source; } let source = source.0.to_f32(); diff --git a/crates/sum_tree/src/tree_map.rs b/crates/sum_tree/src/tree_map.rs index 1de6b2f589470e11fbd436d76c77604ca1219a34..80143aad6971d9cde4164236e77d142060a24b14 100644 --- a/crates/sum_tree/src/tree_map.rs +++ b/crates/sum_tree/src/tree_map.rs @@ -31,6 +31,10 @@ impl TreeMap { Self(tree) } + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + pub fn get<'a>(&self, key: &'a K) -> Option<&V> { let mut cursor = self.0.cursor::>(); cursor.seek(&MapKeyRef(Some(key)), Bias::Left, &());