diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index f01b392b0b..864567d353 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -19,6 +19,7 @@ test-support = [ "gpui/test-support", "multi_buffer/test-support", "project/test-support", + "theme/test-support", "util/test-support", "workspace/test-support", "tree-sitter-rust", @@ -86,6 +87,7 @@ release_channel.workspace = true rand.workspace = true settings = { workspace = true, features = ["test-support"] } text = { workspace = true, features = ["test-support"] } +theme = { workspace = true, features = ["test-support"] } tree-sitter-html.workspace = true tree-sitter-rust.workspace = true tree-sitter-typescript.workspace = true diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index c4b047b428..9ac9c7c83c 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -18,6 +18,7 @@ //! [EditorElement]: crate::element::EditorElement mod block_map; +mod flap_map; mod fold_map; mod inlay_map; mod tab_map; @@ -28,7 +29,9 @@ use crate::{EditorStyle, RowExt}; pub use block_map::{BlockMap, BlockPoint}; use collections::{HashMap, HashSet}; use fold_map::FoldMap; -use gpui::{Font, HighlightStyle, Hsla, LineLayout, Model, ModelContext, Pixels, UnderlineStyle}; +use gpui::{ + AnyElement, Font, HighlightStyle, Hsla, LineLayout, Model, ModelContext, Pixels, UnderlineStyle, +}; use inlay_map::InlayMap; use language::{ language_settings::language_settings, OffsetUtf16, Point, Subscription as BufferSubscription, @@ -42,6 +45,7 @@ use serde::Deserialize; use std::{any::TypeId, borrow::Cow, fmt::Debug, num::NonZeroU32, ops::Range, sync::Arc}; use sum_tree::{Bias, TreeMap}; use tab_map::TabMap; +use ui::WindowContext; use wrap_map::WrapMap; @@ -49,10 +53,15 @@ pub use block_map::{ BlockBufferRows, BlockChunks as DisplayChunks, BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock, TransformBlock, }; +pub use flap_map::*; -use self::block_map::BlockRow; +use self::block_map::{BlockRow, BlockSnapshot}; +use self::fold_map::FoldSnapshot; pub use self::fold_map::{Fold, FoldId, FoldPoint}; +use self::inlay_map::InlaySnapshot; pub use self::inlay_map::{InlayOffset, InlayPoint}; +use self::tab_map::TabSnapshot; +use self::wrap_map::WrapSnapshot; pub(crate) use inlay_map::Inlay; #[derive(Copy, Clone, Debug, PartialEq, Eq)] @@ -61,6 +70,8 @@ pub enum FoldStatus { Foldable, } +pub type RenderFoldToggle = Arc AnyElement>; + const UNNECESSARY_CODE_FADE: f32 = 0.3; pub trait ToDisplayPoint { @@ -92,6 +103,8 @@ pub struct DisplayMap { text_highlights: TextHighlights, /// Regions of inlays that should be highlighted. inlay_highlights: InlayHighlights, + /// A container for explicitly foldable ranges, which supersede indentation based fold range suggestions. + flap_map: FlapMap, pub clip_at_line_ends: bool, } @@ -113,7 +126,9 @@ impl DisplayMap { let (tab_map, snapshot) = TabMap::new(snapshot, tab_size); let (wrap_map, snapshot) = WrapMap::new(snapshot, font, font_size, wrap_width, cx); let block_map = BlockMap::new(snapshot, buffer_header_height, excerpt_header_height); + let flap_map = FlapMap::default(); cx.observe(&wrap_map, |_, _, cx| cx.notify()).detach(); + DisplayMap { buffer, buffer_subscription, @@ -122,6 +137,7 @@ impl DisplayMap { tab_map, wrap_map, block_map, + flap_map, text_highlights: Default::default(), inlay_highlights: Default::default(), clip_at_line_ends: false, @@ -147,6 +163,7 @@ impl DisplayMap { tab_snapshot, wrap_snapshot, block_snapshot, + flap_snapshot: self.flap_map.snapshot(), text_highlights: self.text_highlights.clone(), inlay_highlights: self.inlay_highlights.clone(), clip_at_line_ends: self.clip_at_line_ends, @@ -157,14 +174,14 @@ impl DisplayMap { self.fold( other .folds_in_range(0..other.buffer_snapshot.len()) - .map(|fold| fold.range.to_offset(&other.buffer_snapshot)), + .map(|fold| (fold.range.to_offset(&other.buffer_snapshot), fold.text)), cx, ); } pub fn fold( &mut self, - ranges: impl IntoIterator>, + ranges: impl IntoIterator, &'static str)>, cx: &mut ModelContext, ) { let snapshot = self.buffer.read(cx).snapshot(cx); @@ -209,6 +226,24 @@ impl DisplayMap { self.block_map.read(snapshot, edits); } + pub fn insert_flaps( + &mut self, + flaps: impl IntoIterator, + cx: &mut ModelContext, + ) -> Vec { + let snapshot = self.buffer.read(cx).snapshot(cx); + self.flap_map.insert(flaps, &snapshot) + } + + pub fn remove_flaps( + &mut self, + flap_ids: impl IntoIterator, + cx: &mut ModelContext, + ) { + let snapshot = self.buffer.read(cx).snapshot(cx); + self.flap_map.remove(flap_ids, &snapshot) + } + pub fn insert_blocks( &mut self, blocks: impl IntoIterator>, @@ -367,11 +402,12 @@ pub struct HighlightedChunk<'a> { #[derive(Clone)] pub struct DisplaySnapshot { pub buffer_snapshot: MultiBufferSnapshot, - pub fold_snapshot: fold_map::FoldSnapshot, - inlay_snapshot: inlay_map::InlaySnapshot, - tab_snapshot: tab_map::TabSnapshot, - wrap_snapshot: wrap_map::WrapSnapshot, - block_snapshot: block_map::BlockSnapshot, + pub fold_snapshot: FoldSnapshot, + pub flap_snapshot: FlapSnapshot, + inlay_snapshot: InlaySnapshot, + tab_snapshot: TabSnapshot, + wrap_snapshot: WrapSnapshot, + block_snapshot: BlockSnapshot, text_highlights: TextHighlights, inlay_highlights: InlayHighlights, clip_at_line_ends: bool, @@ -833,17 +869,7 @@ impl DisplaySnapshot { DisplayRow(self.block_snapshot.longest_row()) } - pub fn fold_for_line(&self, buffer_row: MultiBufferRow) -> Option { - if self.is_line_folded(buffer_row) { - Some(FoldStatus::Folded) - } else if self.is_foldable(buffer_row) { - Some(FoldStatus::Foldable) - } else { - None - } - } - - pub fn is_foldable(&self, buffer_row: MultiBufferRow) -> bool { + pub fn starts_indent(&self, buffer_row: MultiBufferRow) -> bool { let max_row = self.buffer_snapshot.max_buffer_row(); if buffer_row >= max_row { return false; @@ -867,9 +893,17 @@ impl DisplaySnapshot { false } - pub fn foldable_range(&self, buffer_row: MultiBufferRow) -> Option> { + pub fn foldable_range( + &self, + buffer_row: MultiBufferRow, + ) -> Option<(Range, &'static str)> { let start = MultiBufferPoint::new(buffer_row.0, self.buffer_snapshot.line_len(buffer_row)); - if self.is_foldable(MultiBufferRow(start.row)) + if let Some(flap) = self + .flap_snapshot + .query_row(buffer_row, &self.buffer_snapshot) + { + Some((flap.range.to_point(&self.buffer_snapshot), "")) + } else if self.starts_indent(MultiBufferRow(start.row)) && !self.is_line_folded(MultiBufferRow(start.row)) { let (start_indent, _) = self.line_indent_for_buffer_row(buffer_row); @@ -888,7 +922,7 @@ impl DisplaySnapshot { } } let end = end.unwrap_or(max_point); - Some(start..end) + Some((start..end, "⋯")) } else { None } @@ -1165,7 +1199,7 @@ pub mod tests { } else { log::info!("folding ranges: {:?}", ranges); map.update(cx, |map, cx| { - map.fold(ranges, cx); + map.fold(ranges.into_iter().map(|range| (range, "⋯")), cx); }); } } @@ -1513,7 +1547,10 @@ pub mod tests { map.update(cx, |map, cx| { map.fold( - vec![MultiBufferPoint::new(0, 6)..MultiBufferPoint::new(3, 2)], + vec![( + MultiBufferPoint::new(0, 6)..MultiBufferPoint::new(3, 2), + "⋯", + )], cx, ) }); @@ -1595,7 +1632,10 @@ pub mod tests { map.update(cx, |map, cx| { map.fold( - vec![MultiBufferPoint::new(0, 6)..MultiBufferPoint::new(3, 2)], + vec![( + MultiBufferPoint::new(0, 6)..MultiBufferPoint::new(3, 2), + "⋯", + )], cx, ) }); @@ -1754,6 +1794,33 @@ pub mod tests { assert("aˇαˇ", cx); } + #[gpui::test] + fn test_flaps(cx: &mut gpui::AppContext) { + init_test(cx, |_| {}); + + let text = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll"; + let buffer = MultiBuffer::build_simple(text, cx); + let font_size = px(14.0); + cx.new_model(|cx| { + let mut map = + DisplayMap::new(buffer.clone(), font("Helvetica"), font_size, None, 1, 1, cx); + let snapshot = map.buffer.read(cx).snapshot(cx); + let range = + snapshot.anchor_before(Point::new(2, 0))..snapshot.anchor_after(Point::new(3, 3)); + + map.flap_map.insert( + [Flap::new( + range, + |_row, _status, _toggle, _cx| div(), + |_row, _status, _cx| div(), + )], + &map.buffer.read(cx).snapshot(cx), + ); + + map + }); + } + #[gpui::test] fn test_tabs_with_multibyte_chars(cx: &mut gpui::AppContext) { init_test(cx, |_| {}); diff --git a/crates/editor/src/display_map/flap_map.rs b/crates/editor/src/display_map/flap_map.rs new file mode 100644 index 0000000000..c2a6731672 --- /dev/null +++ b/crates/editor/src/display_map/flap_map.rs @@ -0,0 +1,292 @@ +use collections::HashMap; +use gpui::{AnyElement, IntoElement}; +use multi_buffer::{Anchor, AnchorRangeExt, MultiBufferRow, MultiBufferSnapshot, ToPoint}; +use std::{cmp::Ordering, ops::Range, sync::Arc}; +use sum_tree::{Bias, SeekTarget, SumTree}; +use text::Point; +use ui::WindowContext; + +#[derive(Copy, Clone, Default, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)] +pub struct FlapId(usize); + +#[derive(Default)] +pub struct FlapMap { + snapshot: FlapSnapshot, + next_id: FlapId, + id_to_range: HashMap>, +} + +#[derive(Clone, Default)] +pub struct FlapSnapshot { + flaps: SumTree, +} + +impl FlapSnapshot { + /// Returns the first Flap starting on the specified buffer row. + pub fn query_row<'a>( + &'a self, + row: MultiBufferRow, + snapshot: &'a MultiBufferSnapshot, + ) -> Option<&'a Flap> { + let start = snapshot.anchor_before(Point::new(row.0, 0)); + let mut cursor = self.flaps.cursor::(); + cursor.seek(&start, Bias::Left, snapshot); + while let Some(item) = cursor.item() { + match Ord::cmp(&item.flap.range.start.to_point(snapshot).row, &row.0) { + Ordering::Less => cursor.next(snapshot), + Ordering::Equal => return Some(&item.flap), + Ordering::Greater => break, + } + } + return None; + } + + pub fn flap_items_with_offsets( + &self, + snapshot: &MultiBufferSnapshot, + ) -> Vec<(FlapId, Range)> { + let mut cursor = self.flaps.cursor::(); + let mut results = Vec::new(); + + cursor.next(snapshot); + while let Some(item) = cursor.item() { + let start_point = item.flap.range.start.to_point(snapshot); + let end_point = item.flap.range.end.to_point(snapshot); + results.push((item.id, start_point..end_point)); + cursor.next(snapshot); + } + + results + } +} + +type RenderToggleFn = Arc< + dyn Send + + Sync + + Fn( + MultiBufferRow, + bool, + Arc, + &mut WindowContext, + ) -> AnyElement, +>; +type RenderTrailerFn = + Arc AnyElement>; + +#[derive(Clone)] +pub struct Flap { + pub range: Range, + pub render_toggle: RenderToggleFn, + pub render_trailer: RenderTrailerFn, +} + +impl Flap { + pub fn new( + range: Range, + render_toggle: RenderToggle, + render_trailer: RenderTrailer, + ) -> Self + where + RenderToggle: 'static + + Send + + Sync + + Fn( + MultiBufferRow, + bool, + Arc, + &mut WindowContext, + ) -> ToggleElement + + 'static, + ToggleElement: IntoElement, + RenderTrailer: 'static + + Send + + Sync + + Fn(MultiBufferRow, bool, &mut WindowContext) -> TrailerElement + + 'static, + TrailerElement: IntoElement, + { + Flap { + range, + render_toggle: Arc::new(move |row, folded, toggle, cx| { + render_toggle(row, folded, toggle, cx).into_any_element() + }), + render_trailer: Arc::new(move |row, folded, cx| { + render_trailer(row, folded, cx).into_any_element() + }), + } + } +} + +impl std::fmt::Debug for Flap { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Flap").field("range", &self.range).finish() + } +} + +#[derive(Clone, Debug)] +struct FlapItem { + id: FlapId, + flap: Flap, +} + +impl FlapMap { + pub fn snapshot(&self) -> FlapSnapshot { + self.snapshot.clone() + } + + pub fn insert( + &mut self, + flaps: impl IntoIterator, + snapshot: &MultiBufferSnapshot, + ) -> Vec { + let mut new_ids = Vec::new(); + self.snapshot.flaps = { + let mut new_flaps = SumTree::new(); + let mut cursor = self.snapshot.flaps.cursor::(); + for flap in flaps { + new_flaps.append(cursor.slice(&flap.range, Bias::Left, snapshot), snapshot); + + let id = self.next_id; + self.next_id.0 += 1; + self.id_to_range.insert(id, flap.range.clone()); + new_flaps.push(FlapItem { flap, id }, snapshot); + new_ids.push(id); + } + new_flaps.append(cursor.suffix(snapshot), snapshot); + new_flaps + }; + new_ids + } + + pub fn remove( + &mut self, + ids: impl IntoIterator, + snapshot: &MultiBufferSnapshot, + ) { + let mut removals = Vec::new(); + for id in ids { + if let Some(range) = self.id_to_range.remove(&id) { + removals.push((id, range.clone())); + } + } + removals.sort_unstable_by(|(a_id, a_range), (b_id, b_range)| { + AnchorRangeExt::cmp(a_range, b_range, snapshot).then(b_id.cmp(&a_id)) + }); + + self.snapshot.flaps = { + let mut new_flaps = SumTree::new(); + let mut cursor = self.snapshot.flaps.cursor::(); + + for (id, range) in removals { + new_flaps.append(cursor.slice(&range, Bias::Left, snapshot), snapshot); + while let Some(item) = cursor.item() { + cursor.next(snapshot); + if item.id == id { + break; + } else { + new_flaps.push(item.clone(), snapshot); + } + } + } + + new_flaps.append(cursor.suffix(snapshot), snapshot); + new_flaps + }; + } +} + +#[derive(Debug, Clone)] +pub struct ItemSummary { + range: Range, +} + +impl Default for ItemSummary { + fn default() -> Self { + Self { + range: Anchor::min()..Anchor::min(), + } + } +} + +impl sum_tree::Summary for ItemSummary { + type Context = MultiBufferSnapshot; + + fn add_summary(&mut self, other: &Self, _snapshot: &MultiBufferSnapshot) { + self.range = other.range.clone(); + } +} + +impl sum_tree::Item for FlapItem { + type Summary = ItemSummary; + + fn summary(&self) -> Self::Summary { + ItemSummary { + range: self.flap.range.clone(), + } + } +} + +/// Implements `SeekTarget` for `Range` to enable seeking within a `SumTree` of `FlapItem`s. +impl SeekTarget<'_, ItemSummary, ItemSummary> for Range { + fn cmp(&self, cursor_location: &ItemSummary, snapshot: &MultiBufferSnapshot) -> Ordering { + AnchorRangeExt::cmp(self, &cursor_location.range, snapshot) + } +} + +impl SeekTarget<'_, ItemSummary, ItemSummary> for Anchor { + fn cmp(&self, other: &ItemSummary, snapshot: &MultiBufferSnapshot) -> Ordering { + self.cmp(&other.range.start, snapshot) + } +} + +#[cfg(test)] +mod test { + use super::*; + use gpui::{div, AppContext}; + use multi_buffer::MultiBuffer; + + #[gpui::test] + fn test_insert_and_remove_flaps(cx: &mut AppContext) { + let text = "line1\nline2\nline3\nline4\nline5"; + let buffer = MultiBuffer::build_simple(text, cx); + let snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx)); + let mut flap_map = FlapMap::default(); + + // Insert flaps + let flaps = [ + Flap::new( + snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(1, 5)), + |_row, _folded, _toggle, _cx| div(), + |_row, _folded, _cx| div(), + ), + Flap::new( + snapshot.anchor_before(Point::new(3, 0))..snapshot.anchor_after(Point::new(3, 5)), + |_row, _folded, _toggle, _cx| div(), + |_row, _folded, _cx| div(), + ), + ]; + let flap_ids = flap_map.insert(flaps, &snapshot); + assert_eq!(flap_ids.len(), 2); + + // Verify flaps are inserted + let flap_snapshot = flap_map.snapshot(); + assert!(flap_snapshot + .query_row(MultiBufferRow(1), &snapshot) + .is_some()); + assert!(flap_snapshot + .query_row(MultiBufferRow(3), &snapshot) + .is_some()); + + // Remove flaps + flap_map.remove(flap_ids, &snapshot); + + // Verify flaps are removed + let flap_snapshot = flap_map.snapshot(); + assert!(flap_snapshot + .query_row(MultiBufferRow(1), &snapshot) + .is_none()); + assert!(flap_snapshot + .query_row(MultiBufferRow(3), &snapshot) + .is_none()); + } +} diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index 337395bacd..d85be31cec 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -75,12 +75,12 @@ pub(crate) struct FoldMapWriter<'a>(&'a mut FoldMap); impl<'a> FoldMapWriter<'a> { pub(crate) fn fold( &mut self, - ranges: impl IntoIterator>, + ranges: impl IntoIterator, &'static str)>, ) -> (FoldSnapshot, Vec) { let mut edits = Vec::new(); let mut folds = Vec::new(); let snapshot = self.0.snapshot.inlay_snapshot.clone(); - for range in ranges.into_iter() { + for (range, fold_text) in ranges.into_iter() { let buffer = &snapshot.buffer; let range = range.start.to_offset(&buffer)..range.end.to_offset(&buffer); @@ -99,6 +99,7 @@ impl<'a> FoldMapWriter<'a> { folds.push(Fold { id: FoldId(post_inc(&mut self.0.next_fold_id.0)), range: fold_range, + text: fold_text, }); let inlay_range = @@ -324,11 +325,14 @@ impl FoldMap { let mut folds = iter::from_fn({ let inlay_snapshot = &inlay_snapshot; move || { - let item = folds_cursor.item().map(|f| { - let buffer_start = f.range.start.to_offset(&inlay_snapshot.buffer); - let buffer_end = f.range.end.to_offset(&inlay_snapshot.buffer); - inlay_snapshot.to_inlay_offset(buffer_start) - ..inlay_snapshot.to_inlay_offset(buffer_end) + let item = folds_cursor.item().map(|fold| { + let buffer_start = fold.range.start.to_offset(&inlay_snapshot.buffer); + let buffer_end = fold.range.end.to_offset(&inlay_snapshot.buffer); + ( + inlay_snapshot.to_inlay_offset(buffer_start) + ..inlay_snapshot.to_inlay_offset(buffer_end), + fold.text, + ) }); folds_cursor.next(&inlay_snapshot.buffer); item @@ -336,25 +340,27 @@ impl FoldMap { }) .peekable(); - while folds.peek().map_or(false, |fold| fold.start < edit.new.end) { - let mut fold = folds.next().unwrap(); + while folds + .peek() + .map_or(false, |(fold_range, _)| fold_range.start < edit.new.end) + { + let (mut fold_range, fold_text) = folds.next().unwrap(); let sum = new_transforms.summary(); - assert!(fold.start.0 >= sum.input.len); + assert!(fold_range.start.0 >= sum.input.len); - while folds - .peek() - .map_or(false, |next_fold| next_fold.start <= fold.end) - { - let next_fold = folds.next().unwrap(); - if next_fold.end > fold.end { - fold.end = next_fold.end; + while folds.peek().map_or(false, |(next_fold_range, _)| { + next_fold_range.start <= fold_range.end + }) { + let (next_fold_range, _) = folds.next().unwrap(); + if next_fold_range.end > fold_range.end { + fold_range.end = next_fold_range.end; } } - if fold.start.0 > sum.input.len { + if fold_range.start.0 > sum.input.len { let text_summary = inlay_snapshot - .text_summary_for_range(InlayOffset(sum.input.len)..fold.start); + .text_summary_for_range(InlayOffset(sum.input.len)..fold_range.start); new_transforms.push( Transform { summary: TransformSummary { @@ -367,16 +373,15 @@ impl FoldMap { ); } - if fold.end > fold.start { - let output_text = "⋯"; + if fold_range.end > fold_range.start { new_transforms.push( Transform { summary: TransformSummary { - output: TextSummary::from(output_text), + output: TextSummary::from(fold_text), input: inlay_snapshot - .text_summary_for_range(fold.start..fold.end), + .text_summary_for_range(fold_range.start..fold_range.end), }, - output_text: Some(output_text), + output_text: Some(fold_text), }, &(), ); @@ -853,6 +858,7 @@ impl Into for FoldId { pub struct Fold { pub id: FoldId, pub range: FoldRange, + pub text: &'static str, } #[derive(Clone, Debug, Eq, PartialEq)] @@ -948,7 +954,7 @@ impl<'a> sum_tree::Dimension<'a, FoldSummary> for FoldRange { impl<'a> sum_tree::SeekTarget<'a, FoldSummary, FoldRange> for FoldRange { fn cmp(&self, other: &Self, buffer: &MultiBufferSnapshot) -> Ordering { - self.0.cmp(&other.0, buffer) + AnchorRangeExt::cmp(&self.0, &other.0, buffer) } } @@ -1159,8 +1165,8 @@ mod tests { let (mut writer, _, _) = map.write(inlay_snapshot, vec![]); let (snapshot2, edits) = writer.fold(vec![ - Point::new(0, 2)..Point::new(2, 2), - Point::new(2, 4)..Point::new(4, 1), + (Point::new(0, 2)..Point::new(2, 2), "⋯"), + (Point::new(2, 4)..Point::new(4, 1), "⋯"), ]); assert_eq!(snapshot2.text(), "aa⋯cc⋯eeeee"); assert_eq!( @@ -1239,19 +1245,19 @@ mod tests { let mut map = FoldMap::new(inlay_snapshot.clone()).0; let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]); - writer.fold(vec![5..8]); + writer.fold(vec![(5..8, "⋯")]); let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]); assert_eq!(snapshot.text(), "abcde⋯ijkl"); // Create an fold adjacent to the start of the first fold. let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]); - writer.fold(vec![0..1, 2..5]); + writer.fold(vec![(0..1, "⋯"), (2..5, "⋯")]); let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]); assert_eq!(snapshot.text(), "⋯b⋯ijkl"); // Create an fold adjacent to the end of the first fold. let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]); - writer.fold(vec![11..11, 8..10]); + writer.fold(vec![(11..11, "⋯"), (8..10, "⋯")]); let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]); assert_eq!(snapshot.text(), "⋯b⋯kl"); } @@ -1261,7 +1267,7 @@ mod tests { // Create two adjacent folds. let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]); - writer.fold(vec![0..2, 2..5]); + writer.fold(vec![(0..2, "⋯"), (2..5, "⋯")]); let (snapshot, _) = map.read(inlay_snapshot, vec![]); assert_eq!(snapshot.text(), "⋯fghijkl"); @@ -1285,10 +1291,10 @@ mod tests { let mut map = FoldMap::new(inlay_snapshot.clone()).0; let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]); writer.fold(vec![ - Point::new(0, 2)..Point::new(2, 2), - Point::new(0, 4)..Point::new(1, 0), - Point::new(1, 2)..Point::new(3, 2), - Point::new(3, 1)..Point::new(4, 1), + (Point::new(0, 2)..Point::new(2, 2), "⋯"), + (Point::new(0, 4)..Point::new(1, 0), "⋯"), + (Point::new(1, 2)..Point::new(3, 2), "⋯"), + (Point::new(3, 1)..Point::new(4, 1), "⋯"), ]); let (snapshot, _) = map.read(inlay_snapshot, vec![]); assert_eq!(snapshot.text(), "aa⋯eeeee"); @@ -1305,8 +1311,8 @@ mod tests { let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]); writer.fold(vec![ - Point::new(0, 2)..Point::new(2, 2), - Point::new(3, 1)..Point::new(4, 1), + (Point::new(0, 2)..Point::new(2, 2), "⋯"), + (Point::new(3, 1)..Point::new(4, 1), "⋯"), ]); let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]); assert_eq!(snapshot.text(), "aa⋯cccc\nd⋯eeeee"); @@ -1330,10 +1336,10 @@ mod tests { let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]); writer.fold(vec![ - Point::new(0, 2)..Point::new(2, 2), - Point::new(0, 4)..Point::new(1, 0), - Point::new(1, 2)..Point::new(3, 2), - Point::new(3, 1)..Point::new(4, 1), + (Point::new(0, 2)..Point::new(2, 2), "⋯"), + (Point::new(0, 4)..Point::new(1, 0), "⋯"), + (Point::new(1, 2)..Point::new(3, 2), "⋯"), + (Point::new(3, 1)..Point::new(4, 1), "⋯"), ]); let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]); let fold_ranges = snapshot @@ -1408,10 +1414,10 @@ mod tests { snapshot_edits.push((snapshot.clone(), edits)); let mut expected_text: String = inlay_snapshot.text().to_string(); - for fold_range in map.merged_fold_ranges().into_iter().rev() { + for (fold_range, fold_text) in map.merged_folds().into_iter().rev() { let fold_inlay_start = inlay_snapshot.to_inlay_offset(fold_range.start); let fold_inlay_end = inlay_snapshot.to_inlay_offset(fold_range.end); - expected_text.replace_range(fold_inlay_start.0..fold_inlay_end.0, "⋯"); + expected_text.replace_range(fold_inlay_start.0..fold_inlay_end.0, fold_text); } assert_eq!(snapshot.text(), expected_text); @@ -1423,7 +1429,7 @@ mod tests { let mut prev_row = 0; let mut expected_buffer_rows = Vec::new(); - for fold_range in map.merged_fold_ranges().into_iter() { + for (fold_range, _fold_text) in map.merged_folds().into_iter() { let fold_start = inlay_snapshot .to_point(inlay_snapshot.to_inlay_offset(fold_range.start)) .row(); @@ -1535,11 +1541,11 @@ mod tests { } let folded_buffer_rows = map - .merged_fold_ranges() + .merged_folds() .iter() - .flat_map(|range| { - let start_row = range.start.to_point(&buffer_snapshot).row; - let end = range.end.to_point(&buffer_snapshot); + .flat_map(|(fold_range, _)| { + let start_row = fold_range.start.to_point(&buffer_snapshot).row; + let end = fold_range.end.to_point(&buffer_snapshot); if end.column == 0 { start_row..end.row } else { @@ -1634,8 +1640,8 @@ mod tests { let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]); writer.fold(vec![ - Point::new(0, 2)..Point::new(2, 2), - Point::new(3, 1)..Point::new(4, 1), + (Point::new(0, 2)..Point::new(2, 2), "⋯"), + (Point::new(3, 1)..Point::new(4, 1), "⋯"), ]); let (snapshot, _) = map.read(inlay_snapshot, vec![]); @@ -1653,34 +1659,39 @@ mod tests { } impl FoldMap { - fn merged_fold_ranges(&self) -> Vec> { + fn merged_folds(&self) -> Vec<(Range, &'static str)> { let inlay_snapshot = self.snapshot.inlay_snapshot.clone(); let buffer = &inlay_snapshot.buffer; let mut folds = self.snapshot.folds.items(buffer); // Ensure sorting doesn't change how folds get merged and displayed. folds.sort_by(|a, b| a.range.cmp(&b.range, buffer)); - let mut fold_ranges = folds + let mut folds = folds .iter() - .map(|fold| fold.range.start.to_offset(buffer)..fold.range.end.to_offset(buffer)) + .map(|fold| { + ( + fold.range.start.to_offset(buffer)..fold.range.end.to_offset(buffer), + fold.text, + ) + }) .peekable(); - let mut merged_ranges = Vec::new(); - while let Some(mut fold_range) = fold_ranges.next() { - while let Some(next_range) = fold_ranges.peek() { + let mut merged_folds = Vec::new(); + while let Some((mut fold_range, fold_text)) = folds.next() { + while let Some((next_range, _)) = folds.peek() { if fold_range.end >= next_range.start { if next_range.end > fold_range.end { fold_range.end = next_range.end; } - fold_ranges.next(); + folds.next(); } else { break; } } if fold_range.end > fold_range.start { - merged_ranges.push(fold_range); + merged_folds.push((fold_range, fold_text)); } } - merged_ranges + merged_folds } pub fn randomly_mutate( @@ -1698,10 +1709,11 @@ mod tests { let start = buffer.clip_offset(rng.gen_range(0..=end), Left); to_unfold.push(start..end); } - log::info!("unfolding {:?}", to_unfold); + let inclusive = rng.gen(); + log::info!("unfolding {:?} (inclusive: {})", to_unfold, inclusive); let (mut writer, snapshot, edits) = self.write(inlay_snapshot, vec![]); snapshot_edits.push((snapshot, edits)); - let (snapshot, edits) = writer.fold(to_unfold); + let (snapshot, edits) = writer.unfold(to_unfold, inclusive); snapshot_edits.push((snapshot, edits)); } _ => { @@ -1711,7 +1723,8 @@ mod tests { for _ in 0..rng.gen_range(1..=2) { let end = buffer.clip_offset(rng.gen_range(0..=buffer.len()), Right); let start = buffer.clip_offset(rng.gen_range(0..=end), Left); - to_fold.push(start..end); + let text = if rng.gen() { "⋯" } else { "" }; + to_fold.push((start..end, text)); } log::info!("folding {:?}", to_fold); let (mut writer, snapshot, edits) = self.write(inlay_snapshot, vec![]); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index cfdac1b7f9..687965ad5a 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -523,6 +523,7 @@ pub struct EditorSnapshot { scroll_anchor: ScrollAnchor, ongoing_scroll: OngoingScroll, current_line_highlight: CurrentLineHighlight, + gutter_hovered: bool, } const GIT_BLAME_GUTTER_WIDTH_CHARS: f32 = 53.; @@ -1886,6 +1887,7 @@ impl Editor { placeholder_text: self.placeholder_text.clone(), is_focused: self.focus_handle.is_focused(cx), current_line_highlight: self.current_line_highlight, + gutter_hovered: self.gutter_hovered, } } @@ -4639,44 +4641,6 @@ impl Editor { })) } - pub fn render_fold_indicators( - &mut self, - fold_data: Vec>, - _style: &EditorStyle, - gutter_hovered: bool, - _line_height: Pixels, - _gutter_margin: Pixels, - cx: &mut ViewContext, - ) -> Vec> { - fold_data - .iter() - .enumerate() - .map(|(ix, fold_data)| { - fold_data - .map(|(fold_status, buffer_row, active)| { - (active || gutter_hovered || fold_status == FoldStatus::Folded).then(|| { - IconButton::new(ix, ui::IconName::ChevronDown) - .on_click(cx.listener(move |this, _e, cx| match fold_status { - FoldStatus::Folded => { - this.unfold_at(&UnfoldAt { buffer_row }, cx); - } - FoldStatus::Foldable => { - this.fold_at(&FoldAt { buffer_row }, cx); - } - })) - .icon_color(ui::Color::Muted) - .icon_size(ui::IconSize::Small) - .selected(fold_status == FoldStatus::Folded) - .selected_icon(ui::IconName::ChevronRight) - .size(ui::ButtonSize::None) - .into_any_element() - }) - }) - .flatten() - }) - .collect() - } - pub fn context_menu_visible(&self) -> bool { self.context_menu .read() @@ -5830,7 +5794,7 @@ impl Editor { let mut end = fold.range.end.to_point(&buffer); start.row -= row_delta; end.row -= row_delta; - refold_ranges.push(start..end); + refold_ranges.push((start..end, fold.text)); } } } @@ -5924,7 +5888,7 @@ impl Editor { let mut end = fold.range.end.to_point(&buffer); start.row += row_delta; end.row += row_delta; - refold_ranges.push(start..end); + refold_ranges.push((start..end, fold.text)); } } } @@ -9282,11 +9246,11 @@ impl Editor { let buffer_start_row = range.start.row; for row in (0..=range.end.row).rev() { - let fold_range = display_map.foldable_range(MultiBufferRow(row)); - - if let Some(fold_range) = fold_range { - if fold_range.end.row >= buffer_start_row { - fold_ranges.push(fold_range); + if let Some((foldable_range, fold_text)) = + display_map.foldable_range(MultiBufferRow(row)) + { + if foldable_range.end.row >= buffer_start_row { + fold_ranges.push((foldable_range, fold_text)); if row <= range.start.row { break; } @@ -9302,14 +9266,14 @@ impl Editor { let buffer_row = fold_at.buffer_row; let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - if let Some(fold_range) = display_map.foldable_range(buffer_row) { + if let Some((fold_range, fold_text)) = display_map.foldable_range(buffer_row) { let autoscroll = self .selections .all::(cx) .iter() .any(|selection| fold_range.overlaps(&selection.range())); - self.fold_ranges(std::iter::once(fold_range), autoscroll, cx); + self.fold_ranges([(fold_range, fold_text)], autoscroll, cx); } } @@ -9363,9 +9327,9 @@ impl Editor { .buffer_snapshot .line_len(MultiBufferRow(s.end.row)), ); - start..end + (start..end, "⋯") } else { - s.start..s.end + (s.start..s.end, "⋯") } }); self.fold_ranges(ranges, true, cx); @@ -9373,18 +9337,20 @@ impl Editor { pub fn fold_ranges( &mut self, - ranges: impl IntoIterator>, + ranges: impl IntoIterator, &'static str)>, auto_scroll: bool, cx: &mut ViewContext, ) { let mut fold_ranges = Vec::new(); let mut buffers_affected = HashMap::default(); let multi_buffer = self.buffer().read(cx); - for range in ranges { - if let Some((_, buffer, _)) = multi_buffer.excerpt_containing(range.start.clone(), cx) { + for (fold_range, fold_text) in ranges { + if let Some((_, buffer, _)) = + multi_buffer.excerpt_containing(fold_range.start.clone(), cx) + { buffers_affected.insert(buffer.read(cx).remote_id(), buffer); }; - fold_ranges.push(range); + fold_ranges.push((fold_range, fold_text)); } let mut ranges = fold_ranges.into_iter().peekable(); @@ -9500,6 +9466,24 @@ impl Editor { } } + pub fn insert_flaps( + &mut self, + flaps: impl IntoIterator, + cx: &mut ViewContext, + ) -> Vec { + self.display_map + .update(cx, |map, cx| map.insert_flaps(flaps, cx)) + } + + pub fn remove_flaps( + &mut self, + ids: impl IntoIterator, + cx: &mut ViewContext, + ) { + self.display_map + .update(cx, |map, cx| map.remove_flaps(ids, cx)); + } + pub fn longest_row(&self, cx: &mut AppContext) -> DisplayRow { self.display_map .update(cx, |map, cx| map.snapshot(cx)) @@ -11098,6 +11082,76 @@ impl EditorSnapshot { git_blame_entries_width, } } + + pub fn render_fold_toggle( + &self, + buffer_row: MultiBufferRow, + row_contains_cursor: bool, + editor: View, + cx: &mut WindowContext, + ) -> Option { + let folded = self.is_line_folded(buffer_row); + + if let Some(flap) = self + .flap_snapshot + .query_row(buffer_row, &self.buffer_snapshot) + { + let toggle_callback = Arc::new(move |folded, cx: &mut WindowContext| { + if folded { + editor.update(cx, |editor, cx| { + editor.fold_at(&crate::FoldAt { buffer_row }, cx) + }); + } else { + editor.update(cx, |editor, cx| { + editor.unfold_at(&crate::UnfoldAt { buffer_row }, cx) + }); + } + }); + + Some((flap.render_toggle)( + buffer_row, + folded, + toggle_callback, + cx, + )) + } else if folded + || (self.starts_indent(buffer_row) && (row_contains_cursor || self.gutter_hovered)) + { + Some( + IconButton::new( + ("indent-fold-indicator", buffer_row.0), + ui::IconName::ChevronDown, + ) + .on_click(cx.listener_for(&editor, move |this, _e, cx| { + if folded { + this.unfold_at(&UnfoldAt { buffer_row }, cx); + } else { + this.fold_at(&FoldAt { buffer_row }, cx); + } + })) + .icon_color(ui::Color::Muted) + .icon_size(ui::IconSize::Small) + .selected(folded) + .selected_icon(ui::IconName::ChevronRight) + .size(ui::ButtonSize::None) + .into_any_element(), + ) + } else { + None + } + } + + pub fn render_flap_trailer( + &self, + buffer_row: MultiBufferRow, + cx: &mut WindowContext, + ) -> Option { + let folded = self.is_line_folded(buffer_row); + let flap = self + .flap_snapshot + .query_row(buffer_row, &self.buffer_snapshot)?; + Some((flap.render_trailer)(buffer_row, folded, cx)) + } } impl Deref for EditorSnapshot { diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 61d7b17a2d..fb19424fc4 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -494,8 +494,8 @@ fn test_clone(cx: &mut TestAppContext) { editor.change_selections(None, cx, |s| s.select_ranges(selection_ranges.clone())); editor.fold_ranges( [ - Point::new(1, 0)..Point::new(2, 0), - Point::new(3, 0)..Point::new(4, 0), + (Point::new(1, 0)..Point::new(2, 0), "⋯"), + (Point::new(3, 0)..Point::new(4, 0), "⋯"), ], true, cx, @@ -903,9 +903,9 @@ fn test_move_cursor_multibyte(cx: &mut TestAppContext) { _ = view.update(cx, |view, cx| { view.fold_ranges( vec![ - Point::new(0, 6)..Point::new(0, 12), - Point::new(1, 2)..Point::new(1, 4), - Point::new(2, 4)..Point::new(2, 8), + (Point::new(0, 6)..Point::new(0, 12), "⋯"), + (Point::new(1, 2)..Point::new(1, 4), "⋯"), + (Point::new(2, 4)..Point::new(2, 8), "⋯"), ], true, cx, @@ -3407,9 +3407,9 @@ fn test_move_line_up_down(cx: &mut TestAppContext) { _ = view.update(cx, |view, cx| { view.fold_ranges( vec![ - Point::new(0, 2)..Point::new(1, 2), - Point::new(2, 3)..Point::new(4, 1), - Point::new(7, 0)..Point::new(8, 4), + (Point::new(0, 2)..Point::new(1, 2), "⋯"), + (Point::new(2, 3)..Point::new(4, 1), "⋯"), + (Point::new(7, 0)..Point::new(8, 4), "⋯"), ], true, cx, @@ -3891,9 +3891,9 @@ fn test_split_selection_into_lines(cx: &mut TestAppContext) { _ = view.update(cx, |view, cx| { view.fold_ranges( vec![ - Point::new(0, 2)..Point::new(1, 2), - Point::new(2, 3)..Point::new(4, 1), - Point::new(7, 0)..Point::new(8, 4), + (Point::new(0, 2)..Point::new(1, 2), "⋯"), + (Point::new(2, 3)..Point::new(4, 1), "⋯"), + (Point::new(7, 0)..Point::new(8, 4), "⋯"), ], true, cx, @@ -4548,8 +4548,8 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { _ = view.update(cx, |view, cx| { view.fold_ranges( vec![ - Point::new(0, 21)..Point::new(0, 24), - Point::new(3, 20)..Point::new(3, 22), + (Point::new(0, 21)..Point::new(0, 24), "⋯"), + (Point::new(3, 20)..Point::new(3, 22), "⋯"), ], true, cx, @@ -11448,6 +11448,67 @@ async fn test_multiple_expanded_hunks_merge( ); } +#[gpui::test] +fn test_flap_insertion_and_rendering(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let editor = cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx); + build_editor(buffer, cx) + }); + + let render_args = Arc::new(Mutex::new(None)); + let snapshot = editor + .update(cx, |editor, cx| { + let snapshot = editor.buffer().read(cx).snapshot(cx); + let range = + snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6)); + + struct RenderArgs { + row: MultiBufferRow, + folded: bool, + callback: Arc, + } + + let flap = Flap::new( + range, + { + let toggle_callback = render_args.clone(); + move |row, folded, callback, _cx| { + *toggle_callback.lock() = Some(RenderArgs { + row, + folded, + callback, + }); + div() + } + }, + |_row, _folded, _cx| div(), + ); + + editor.insert_flaps(Some(flap), cx); + let snapshot = editor.snapshot(cx); + let _div = snapshot.render_fold_toggle(MultiBufferRow(1), false, cx.view().clone(), cx); + snapshot + }) + .unwrap(); + + let render_args = render_args.lock().take().unwrap(); + assert_eq!(render_args.row, MultiBufferRow(1)); + assert_eq!(render_args.folded, false); + assert!(!snapshot.is_line_folded(MultiBufferRow(1))); + + cx.update_window(*editor, |_, cx| (render_args.callback)(true, cx)) + .unwrap(); + let snapshot = editor.update(cx, |editor, cx| editor.snapshot(cx)).unwrap(); + assert!(snapshot.is_line_folded(MultiBufferRow(1))); + + cx.update_window(*editor, |_, cx| (render_args.callback)(false, cx)) + .unwrap(); + let snapshot = editor.update(cx, |editor, cx| editor.snapshot(cx)).unwrap(); + assert!(!snapshot.is_line_folded(MultiBufferRow(1))); +} + fn empty_range(row: usize, column: usize) -> Range { let point = DisplayPoint::new(DisplayRow(row as u32), column as u32); point..point diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 37e8bf46a9..d919405c84 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1,8 +1,7 @@ use crate::{ blame_entry_tooltip::{blame_entry_relative_timestamp, BlameEntryTooltip}, display_map::{ - BlockContext, BlockStyle, DisplaySnapshot, FoldStatus, HighlightedChunk, ToDisplayPoint, - TransformBlock, + BlockContext, BlockStyle, DisplaySnapshot, HighlightedChunk, ToDisplayPoint, TransformBlock, }, editor_settings::{ CurrentLineHighlight, DoubleClickInMultibuffer, MultiCursorModifier, ShowScrollbar, @@ -51,7 +50,7 @@ use smallvec::SmallVec; use std::{ any::TypeId, borrow::Cow, - cmp::{self, max, Ordering}, + cmp::{self, Ordering}, fmt::Write, iter, mem, ops::{Deref, Range}, @@ -869,6 +868,11 @@ impl EditorElement { snapshot .folds_in_range(visible_anchor_range.clone()) .filter_map(|fold| { + // Skip folds that have no text. + if fold.text.is_empty() { + return None; + } + let fold_range = fold.range.clone(); let display_range = fold.range.start.to_display_point(&snapshot) ..fold.range.end.to_display_point(&snapshot); @@ -1163,28 +1167,17 @@ impl EditorElement { } #[allow(clippy::too_many_arguments)] - fn layout_gutter_fold_indicators( + fn prepaint_gutter_fold_toggles( &self, - fold_statuses: Vec>, + toggles: &mut [Option], line_height: Pixels, gutter_dimensions: &GutterDimensions, gutter_settings: crate::editor_settings::Gutter, scroll_pixel_position: gpui::Point, gutter_hitbox: &Hitbox, cx: &mut WindowContext, - ) -> Vec> { - let mut indicators = self.editor.update(cx, |editor, cx| { - editor.render_fold_indicators( - fold_statuses, - &self.style, - editor.gutter_hovered, - line_height, - gutter_dimensions.margin, - cx, - ) - }); - - for (ix, fold_indicator) in indicators.iter_mut().enumerate() { + ) { + for (ix, fold_indicator) in toggles.iter_mut().enumerate() { if let Some(fold_indicator) = fold_indicator { debug_assert!(gutter_settings.folds); let available_space = size( @@ -1207,8 +1200,49 @@ impl EditorElement { fold_indicator.prepaint_as_root(origin, available_space, cx); } } + } - indicators + #[allow(clippy::too_many_arguments)] + fn prepaint_flap_trailers( + &self, + trailers: Vec>, + lines: &[LineWithInvisibles], + line_height: Pixels, + content_origin: gpui::Point, + scroll_pixel_position: gpui::Point, + em_width: Pixels, + cx: &mut WindowContext, + ) -> Vec> { + trailers + .into_iter() + .enumerate() + .map(|(ix, element)| { + let mut element = element?; + let available_space = size( + AvailableSpace::MinContent, + AvailableSpace::Definite(line_height), + ); + let size = element.layout_as_root(available_space, cx); + + let line = &lines[ix].line; + let padding = if line.width == Pixels::ZERO { + Pixels::ZERO + } else { + 4. * em_width + }; + let position = point( + scroll_pixel_position.x + line.width + padding, + ix as f32 * line_height - (scroll_pixel_position.y % line_height), + ); + let centering_offset = point(px(0.), (line_height - size.height) / 2.); + let origin = content_origin + position + centering_offset; + element.prepaint_as_root(origin, available_space, cx); + Some(FlapTrailerLayout { + element, + bounds: Bounds::new(origin, size), + }) + }) + .collect() } // Folds contained in a hunk are ignored apart from shrinking visual size @@ -1292,6 +1326,7 @@ impl EditorElement { display_row: DisplayRow, display_snapshot: &DisplaySnapshot, line_layout: &LineWithInvisibles, + flap_trailer: Option<&FlapTrailerLayout>, em_width: Pixels, content_origin: gpui::Point, scroll_pixel_position: gpui::Point, @@ -1331,17 +1366,22 @@ impl EditorElement { let start_x = { const INLINE_BLAME_PADDING_EM_WIDTHS: f32 = 6.; - let padded_line_width = - line_layout.line.width + (em_width * INLINE_BLAME_PADDING_EM_WIDTHS); + let line_end = if let Some(flap_trailer) = flap_trailer { + flap_trailer.bounds.right() + } else { + content_origin.x - scroll_pixel_position.x + line_layout.line.width + }; + let padded_line_end = line_end + em_width * INLINE_BLAME_PADDING_EM_WIDTHS; - let min_column = ProjectSettings::get_global(cx) + let min_column_in_pixels = ProjectSettings::get_global(cx) .git .inline_blame .and_then(|settings| settings.min_column) .map(|col| self.column_pixels(col as usize, cx)) .unwrap_or(px(0.)); + let min_start = content_origin.x - scroll_pixel_position.x + min_column_in_pixels; - (content_origin.x - scroll_pixel_position.x) + max(padded_line_width, min_column) + cmp::max(padded_line_end, min_start) }; let absolute_offset = point(start_x, start_y); @@ -1580,13 +1620,9 @@ impl EditorElement { active_rows: &BTreeMap, newest_selection_head: Option, snapshot: &EditorSnapshot, - cx: &WindowContext, - ) -> ( - Vec>, - Vec>, - ) { + cx: &mut WindowContext, + ) -> Vec> { let editor = self.editor.read(cx); - let is_singleton = editor.is_singleton(cx); let newest_selection_head = newest_selection_head.unwrap_or_else(|| { let newest = editor.selections.newest::(cx); SelectionLayout::new( @@ -1603,10 +1639,7 @@ impl EditorElement { let font_size = self.style.text.font_size.to_pixels(cx.rem_size()); let include_line_numbers = EditorSettings::get_global(cx).gutter.line_numbers && snapshot.mode == EditorMode::Full; - let include_fold_statuses = - EditorSettings::get_global(cx).gutter.folds && snapshot.mode == EditorMode::Full; let mut shaped_line_numbers = Vec::with_capacity(rows.len()); - let mut fold_statuses = Vec::with_capacity(rows.len()); let mut line_number = String::new(); let is_relative = EditorSettings::get_global(cx).relative_line_numbers; let relative_to = if is_relative { @@ -1619,10 +1652,10 @@ impl EditorElement { for (ix, row) in buffer_rows.into_iter().enumerate() { let display_row = DisplayRow(rows.start.0 + ix as u32); - let (active, color) = if active_rows.contains_key(&display_row) { - (true, cx.theme().colors().editor_active_line_number) + let color = if active_rows.contains_key(&display_row) { + cx.theme().colors().editor_active_line_number } else { - (false, cx.theme().colors().editor_line_number) + cx.theme().colors().editor_line_number }; if let Some(multibuffer_row) = row { if include_line_numbers { @@ -1646,24 +1679,65 @@ impl EditorElement { .unwrap(); shaped_line_numbers.push(Some(shaped_line)); } - if include_fold_statuses { - fold_statuses.push( - is_singleton - .then(|| { - snapshot - .fold_for_line(multibuffer_row) - .map(|fold_status| (fold_status, multibuffer_row, active)) - }) - .flatten(), - ) - } } else { - fold_statuses.push(None); shaped_line_numbers.push(None); } } - (shaped_line_numbers, fold_statuses) + shaped_line_numbers + } + + fn layout_gutter_fold_toggles( + &self, + rows: Range, + buffer_rows: impl IntoIterator>, + active_rows: &BTreeMap, + snapshot: &EditorSnapshot, + cx: &mut WindowContext, + ) -> Vec> { + let include_fold_statuses = EditorSettings::get_global(cx).gutter.folds + && snapshot.mode == EditorMode::Full + && self.editor.read(cx).is_singleton(cx); + if include_fold_statuses { + buffer_rows + .into_iter() + .enumerate() + .map(|(ix, row)| { + if let Some(multibuffer_row) = row { + let display_row = DisplayRow(rows.start.0 + ix as u32); + let active = active_rows.contains_key(&display_row); + snapshot.render_fold_toggle( + multibuffer_row, + active, + self.editor.clone(), + cx, + ) + } else { + None + } + }) + .collect() + } else { + Vec::new() + } + } + + fn layout_flap_trailers( + &self, + buffer_rows: impl IntoIterator>, + snapshot: &EditorSnapshot, + cx: &mut WindowContext, + ) -> Vec> { + buffer_rows + .into_iter() + .map(|row| { + if let Some(multibuffer_row) = row { + snapshot.render_flap_trailer(multibuffer_row, cx) + } else { + None + } + }) + .collect() } fn layout_lines( @@ -2465,8 +2539,8 @@ impl EditorElement { } cx.paint_layer(layout.gutter_hitbox.bounds, |cx| { - cx.with_element_namespace("gutter_fold_indicators", |cx| { - for fold_indicator in layout.fold_indicators.iter_mut().flatten() { + cx.with_element_namespace("gutter_fold_toggles", |cx| { + for fold_indicator in layout.gutter_fold_toggles.iter_mut().flatten() { fold_indicator.paint(cx); } }); @@ -2646,6 +2720,11 @@ impl EditorElement { self.paint_redactions(layout, cx); self.paint_cursors(layout, cx); self.paint_inline_blame(layout, cx); + cx.with_element_namespace("flap_trailers", |cx| { + for trailer in layout.flap_trailers.iter_mut().flatten() { + trailer.element.paint(cx); + } + }); }, ) } @@ -3992,15 +4071,29 @@ impl Element for EditorElement { cx, ); - let (line_numbers, fold_statuses) = self.layout_line_numbers( + let line_numbers = self.layout_line_numbers( start_row..end_row, - buffer_rows.clone().into_iter(), + buffer_rows.iter().copied(), &active_rows, newest_selection_head, &snapshot, cx, ); + let mut gutter_fold_toggles = + cx.with_element_namespace("gutter_fold_toggles", |cx| { + self.layout_gutter_fold_toggles( + start_row..end_row, + buffer_rows.iter().copied(), + &active_rows, + &snapshot, + cx, + ) + }); + let flap_trailers = cx.with_element_namespace("flap_trailers", |cx| { + self.layout_flap_trailers(buffer_rows.iter().copied(), &snapshot, cx) + }); + let display_hunks = self.layout_git_gutters( line_height, &gutter_hitbox, @@ -4046,15 +4139,30 @@ impl Element for EditorElement { scroll_position.y * line_height, ); + let flap_trailers = cx.with_element_namespace("flap_trailers", |cx| { + self.prepaint_flap_trailers( + flap_trailers, + &line_layouts, + line_height, + content_origin, + scroll_pixel_position, + em_width, + cx, + ) + }); + let mut inline_blame = None; if let Some(newest_selection_head) = newest_selection_head { let display_row = newest_selection_head.row(); if (start_row..end_row).contains(&display_row) { - let line_layout = &line_layouts[display_row.minus(start_row) as usize]; + let line_ix = display_row.minus(start_row) as usize; + let line_layout = &line_layouts[line_ix]; + let flap_trailer_layout = flap_trailers[line_ix].as_ref(); inline_blame = self.layout_inline_blame( display_row, &snapshot.display_snapshot, line_layout, + flap_trailer_layout, em_width, content_origin, scroll_pixel_position, @@ -4226,21 +4334,17 @@ impl Element for EditorElement { let mouse_context_menu = self.layout_mouse_context_menu(cx); - let fold_indicators = if gutter_settings.folds { - cx.with_element_namespace("gutter_fold_indicators", |cx| { - self.layout_gutter_fold_indicators( - fold_statuses, - line_height, - &gutter_dimensions, - gutter_settings, - scroll_pixel_position, - &gutter_hitbox, - cx, - ) - }) - } else { - Vec::new() - }; + cx.with_element_namespace("gutter_fold_toggles", |cx| { + self.prepaint_gutter_fold_toggles( + &mut gutter_fold_toggles, + line_height, + &gutter_dimensions, + gutter_settings, + scroll_pixel_position, + &gutter_hitbox, + cx, + ) + }); let invisible_symbol_font_size = font_size / 2.; let tab_invisible = cx @@ -4310,7 +4414,8 @@ impl Element for EditorElement { mouse_context_menu, test_indicators, code_actions_indicator, - fold_indicators, + gutter_fold_toggles, + flap_trailers, tab_invisible, space_invisible, } @@ -4430,7 +4535,8 @@ pub struct EditorLayout { selections: Vec<(PlayerColor, Vec)>, code_actions_indicator: Option, test_indicators: Vec, - fold_indicators: Vec>, + gutter_fold_toggles: Vec>, + flap_trailers: Vec>, mouse_context_menu: Option, tab_invisible: ShapedLine, space_invisible: ShapedLine, @@ -4554,6 +4660,11 @@ impl ScrollbarLayout { } } +struct FlapTrailerLayout { + element: AnyElement, + bounds: Bounds, +} + struct FoldLayout { display_range: Range, hover_element: AnyElement, @@ -4972,16 +5083,14 @@ mod tests { let layouts = cx .update_window(*window, |_, cx| { - element - .layout_line_numbers( - DisplayRow(0)..DisplayRow(6), - (0..6).map(MultiBufferRow).map(Some), - &Default::default(), - Some(DisplayPoint::new(DisplayRow(0), 0)), - &snapshot, - cx, - ) - .0 + element.layout_line_numbers( + DisplayRow(0)..DisplayRow(6), + (0..6).map(MultiBufferRow).map(Some), + &Default::default(), + Some(DisplayPoint::new(DisplayRow(0), 0)), + &snapshot, + cx, + ) }) .unwrap(); assert_eq!(layouts.len(), 6); diff --git a/crates/gpui/src/view.rs b/crates/gpui/src/view.rs index fb96f75a80..7f10eb25c3 100644 --- a/crates/gpui/src/view.rs +++ b/crates/gpui/src/view.rs @@ -280,6 +280,14 @@ impl From> for AnyView { } } +impl PartialEq for AnyView { + fn eq(&self, other: &Self) -> bool { + self.model == other.model + } +} + +impl Eq for AnyView {} + impl Element for AnyView { type RequestLayoutState = Option; type PrepaintState = Option; diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 4f038aa2cc..2c6b2da264 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -4591,6 +4591,12 @@ impl From<(&'static str, u64)> for ElementId { } } +impl From<(&'static str, u32)> for ElementId { + fn from((name, id): (&'static str, u32)) -> Self { + ElementId::NamedInteger(name.into(), id as usize) + } +} + /// A rectangle to be rendered in the window at the given position and size. /// Passed as an argument [`WindowContext::paint_quad`]. #[derive(Clone)] diff --git a/crates/sum_tree/src/cursor.rs b/crates/sum_tree/src/cursor.rs index 4604d9e9ff..1e8519114d 100644 --- a/crates/sum_tree/src/cursor.rs +++ b/crates/sum_tree/src/cursor.rs @@ -49,6 +49,7 @@ where &self.position } + #[track_caller] pub fn end(&self, cx: &::Context) -> D { if let Some(item_summary) = self.item_summary() { let mut end = self.start().clone(); @@ -59,6 +60,7 @@ where } } + #[track_caller] pub fn item(&self) -> Option<&'a T> { self.assert_did_seek(); if let Some(entry) = self.stack.last() { @@ -77,6 +79,7 @@ where } } + #[track_caller] pub fn item_summary(&self) -> Option<&'a T::Summary> { self.assert_did_seek(); if let Some(entry) = self.stack.last() { @@ -97,6 +100,7 @@ where } } + #[track_caller] pub fn next_item(&self) -> Option<&'a T> { self.assert_did_seek(); if let Some(entry) = self.stack.last() { @@ -119,6 +123,7 @@ where } } + #[track_caller] fn next_leaf(&self) -> Option<&'a SumTree> { for entry in self.stack.iter().rev().skip(1) { if entry.index < entry.tree.0.child_trees().len() - 1 { @@ -133,6 +138,7 @@ where None } + #[track_caller] pub fn prev_item(&self) -> Option<&'a T> { self.assert_did_seek(); if let Some(entry) = self.stack.last() { @@ -155,6 +161,7 @@ where } } + #[track_caller] fn prev_leaf(&self) -> Option<&'a SumTree> { for entry in self.stack.iter().rev().skip(1) { if entry.index != 0 { @@ -169,10 +176,12 @@ where None } + #[track_caller] pub fn prev(&mut self, cx: &::Context) { self.prev_internal(|_| true, cx) } + #[track_caller] fn prev_internal(&mut self, mut filter_node: F, cx: &::Context) where F: FnMut(&T::Summary) -> bool, @@ -238,10 +247,12 @@ where } } + #[track_caller] pub fn next(&mut self, cx: &::Context) { self.next_internal(|_| true, cx) } + #[track_caller] fn next_internal(&mut self, mut filter_node: F, cx: &::Context) where F: FnMut(&T::Summary) -> bool, @@ -329,6 +340,7 @@ where debug_assert!(self.stack.is_empty() || self.stack.last().unwrap().tree.0.is_leaf()); } + #[track_caller] fn assert_did_seek(&self) { assert!( self.did_seek, @@ -342,6 +354,7 @@ where T: Item, D: Dimension<'a, T::Summary>, { + #[track_caller] pub fn seek( &mut self, pos: &Target, @@ -355,6 +368,7 @@ where self.seek_internal(pos, bias, &mut (), cx) } + #[track_caller] pub fn seek_forward( &mut self, pos: &Target, @@ -367,6 +381,7 @@ where self.seek_internal(pos, bias, &mut (), cx) } + #[track_caller] pub fn slice( &mut self, end: &Target, @@ -386,10 +401,12 @@ where slice.tree } + #[track_caller] pub fn suffix(&mut self, cx: &::Context) -> SumTree { self.slice(&End::new(), Bias::Right, cx) } + #[track_caller] pub fn summary( &mut self, end: &Target, @@ -406,6 +423,7 @@ where } /// Returns whether we found the item you where seeking for + #[track_caller] fn seek_internal( &mut self, target: &dyn SeekTarget<'a, T::Summary, D>, diff --git a/crates/text/src/anchor.rs b/crates/text/src/anchor.rs index 3a1ab499fb..ee833326f5 100644 --- a/crates/text/src/anchor.rs +++ b/crates/text/src/anchor.rs @@ -2,7 +2,6 @@ use crate::{ locator::Locator, BufferId, BufferSnapshot, Point, PointUtf16, TextDimension, ToOffset, ToPoint, ToPointUtf16, }; -use anyhow::Result; use std::{cmp::Ordering, fmt::Debug, ops::Range}; use sum_tree::Bias; @@ -136,14 +135,14 @@ where } pub trait AnchorRangeExt { - fn cmp(&self, b: &Range, buffer: &BufferSnapshot) -> Result; + fn cmp(&self, b: &Range, buffer: &BufferSnapshot) -> Ordering; } impl AnchorRangeExt for Range { - fn cmp(&self, other: &Range, buffer: &BufferSnapshot) -> Result { - Ok(match self.start.cmp(&other.start, buffer) { + fn cmp(&self, other: &Range, buffer: &BufferSnapshot) -> Ordering { + match self.start.cmp(&other.start, buffer) { Ordering::Equal => other.end.cmp(&self.end, buffer), ord => ord, - }) + } } }