Detailed changes
@@ -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
@@ -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<dyn Fn(FoldStatus, &mut WindowContext) -> 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<T: ToOffset>(
&mut self,
- ranges: impl IntoIterator<Item = Range<T>>,
+ ranges: impl IntoIterator<Item = (Range<T>, &'static str)>,
cx: &mut ModelContext<Self>,
) {
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<Item = Flap>,
+ cx: &mut ModelContext<Self>,
+ ) -> Vec<FlapId> {
+ let snapshot = self.buffer.read(cx).snapshot(cx);
+ self.flap_map.insert(flaps, &snapshot)
+ }
+
+ pub fn remove_flaps(
+ &mut self,
+ flap_ids: impl IntoIterator<Item = FlapId>,
+ cx: &mut ModelContext<Self>,
+ ) {
+ let snapshot = self.buffer.read(cx).snapshot(cx);
+ self.flap_map.remove(flap_ids, &snapshot)
+ }
+
pub fn insert_blocks(
&mut self,
blocks: impl IntoIterator<Item = BlockProperties<Anchor>>,
@@ -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<FoldStatus> {
- 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<Range<Point>> {
+ pub fn foldable_range(
+ &self,
+ buffer_row: MultiBufferRow,
+ ) -> Option<(Range<Point>, &'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, |_| {});
@@ -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<FlapId, Range<Anchor>>,
+}
+
+#[derive(Clone, Default)]
+pub struct FlapSnapshot {
+ flaps: SumTree<FlapItem>,
+}
+
+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::<ItemSummary>();
+ 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<Point>)> {
+ let mut cursor = self.flaps.cursor::<ItemSummary>();
+ 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<dyn Send + Sync + Fn(bool, &mut WindowContext)>,
+ &mut WindowContext,
+ ) -> AnyElement,
+>;
+type RenderTrailerFn =
+ Arc<dyn Send + Sync + Fn(MultiBufferRow, bool, &mut WindowContext) -> AnyElement>;
+
+#[derive(Clone)]
+pub struct Flap {
+ pub range: Range<Anchor>,
+ pub render_toggle: RenderToggleFn,
+ pub render_trailer: RenderTrailerFn,
+}
+
+impl Flap {
+ pub fn new<RenderToggle, ToggleElement, RenderTrailer, TrailerElement>(
+ range: Range<Anchor>,
+ render_toggle: RenderToggle,
+ render_trailer: RenderTrailer,
+ ) -> Self
+ where
+ RenderToggle: 'static
+ + Send
+ + Sync
+ + Fn(
+ MultiBufferRow,
+ bool,
+ Arc<dyn Send + Sync + Fn(bool, &mut WindowContext)>,
+ &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<Item = Flap>,
+ snapshot: &MultiBufferSnapshot,
+ ) -> Vec<FlapId> {
+ let mut new_ids = Vec::new();
+ self.snapshot.flaps = {
+ let mut new_flaps = SumTree::new();
+ let mut cursor = self.snapshot.flaps.cursor::<ItemSummary>();
+ 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<Item = FlapId>,
+ 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::<ItemSummary>();
+
+ 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<Anchor>,
+}
+
+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<Anchor>` to enable seeking within a `SumTree` of `FlapItem`s.
+impl SeekTarget<'_, ItemSummary, ItemSummary> for Range<Anchor> {
+ 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());
+ }
+}
@@ -75,12 +75,12 @@ pub(crate) struct FoldMapWriter<'a>(&'a mut FoldMap);
impl<'a> FoldMapWriter<'a> {
pub(crate) fn fold<T: ToOffset>(
&mut self,
- ranges: impl IntoIterator<Item = Range<T>>,
+ ranges: impl IntoIterator<Item = (Range<T>, &'static str)>,
) -> (FoldSnapshot, Vec<FoldEdit>) {
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<ElementId> 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<Range<usize>> {
+ fn merged_folds(&self) -> Vec<(Range<usize>, &'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![]);
@@ -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<Option<(FoldStatus, MultiBufferRow, bool)>>,
- _style: &EditorStyle,
- gutter_hovered: bool,
- _line_height: Pixels,
- _gutter_margin: Pixels,
- cx: &mut ViewContext<Self>,
- ) -> Vec<Option<AnyElement>> {
- 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::<Point>(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<T: ToOffset + Clone>(
&mut self,
- ranges: impl IntoIterator<Item = Range<T>>,
+ ranges: impl IntoIterator<Item = (Range<T>, &'static str)>,
auto_scroll: bool,
cx: &mut ViewContext<Self>,
) {
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<Item = Flap>,
+ cx: &mut ViewContext<Self>,
+ ) -> Vec<FlapId> {
+ self.display_map
+ .update(cx, |map, cx| map.insert_flaps(flaps, cx))
+ }
+
+ pub fn remove_flaps(
+ &mut self,
+ ids: impl IntoIterator<Item = FlapId>,
+ cx: &mut ViewContext<Self>,
+ ) {
+ 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<Editor>,
+ cx: &mut WindowContext,
+ ) -> Option<AnyElement> {
+ 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<AnyElement> {
+ 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 {
@@ -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<dyn Fn(bool, &mut WindowContext) + Send + Sync>,
+ }
+
+ 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<DisplayPoint> {
let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
point..point
@@ -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<Option<(FoldStatus, MultiBufferRow, bool)>>,
+ toggles: &mut [Option<AnyElement>],
line_height: Pixels,
gutter_dimensions: &GutterDimensions,
gutter_settings: crate::editor_settings::Gutter,
scroll_pixel_position: gpui::Point<Pixels>,
gutter_hitbox: &Hitbox,
cx: &mut WindowContext,
- ) -> Vec<Option<AnyElement>> {
- 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);
}
}
+ }
+
+ #[allow(clippy::too_many_arguments)]
+ fn prepaint_flap_trailers(
+ &self,
+ trailers: Vec<Option<AnyElement>>,
+ lines: &[LineWithInvisibles],
+ line_height: Pixels,
+ content_origin: gpui::Point<Pixels>,
+ scroll_pixel_position: gpui::Point<Pixels>,
+ em_width: Pixels,
+ cx: &mut WindowContext,
+ ) -> Vec<Option<FlapTrailerLayout>> {
+ 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);
- indicators
+ 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<Pixels>,
scroll_pixel_position: gpui::Point<Pixels>,
@@ -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<DisplayRow, bool>,
newest_selection_head: Option<DisplayPoint>,
snapshot: &EditorSnapshot,
- cx: &WindowContext,
- ) -> (
- Vec<Option<ShapedLine>>,
- Vec<Option<(FoldStatus, MultiBufferRow, bool)>>,
- ) {
+ cx: &mut WindowContext,
+ ) -> Vec<Option<ShapedLine>> {
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::<Point>(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<DisplayRow>,
+ buffer_rows: impl IntoIterator<Item = Option<MultiBufferRow>>,
+ active_rows: &BTreeMap<DisplayRow, bool>,
+ snapshot: &EditorSnapshot,
+ cx: &mut WindowContext,
+ ) -> Vec<Option<AnyElement>> {
+ 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<Item = Option<MultiBufferRow>>,
+ snapshot: &EditorSnapshot,
+ cx: &mut WindowContext,
+ ) -> Vec<Option<AnyElement>> {
+ 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<SelectionLayout>)>,
code_actions_indicator: Option<AnyElement>,
test_indicators: Vec<AnyElement>,
- fold_indicators: Vec<Option<AnyElement>>,
+ gutter_fold_toggles: Vec<Option<AnyElement>>,
+ flap_trailers: Vec<Option<FlapTrailerLayout>>,
mouse_context_menu: Option<AnyElement>,
tab_invisible: ShapedLine,
space_invisible: ShapedLine,
@@ -4554,6 +4660,11 @@ impl ScrollbarLayout {
}
}
+struct FlapTrailerLayout {
+ element: AnyElement,
+ bounds: Bounds<Pixels>,
+}
+
struct FoldLayout {
display_range: Range<DisplayPoint>,
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);
@@ -280,6 +280,14 @@ impl<V: Render> From<View<V>> 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<AnyElement>;
type PrepaintState = Option<AnyElement>;
@@ -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)]
@@ -49,6 +49,7 @@ where
&self.position
}
+ #[track_caller]
pub fn end(&self, cx: &<T::Summary as Summary>::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<T>> {
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<T>> {
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: &<T::Summary as Summary>::Context) {
self.prev_internal(|_| true, cx)
}
+ #[track_caller]
fn prev_internal<F>(&mut self, mut filter_node: F, cx: &<T::Summary as Summary>::Context)
where
F: FnMut(&T::Summary) -> bool,
@@ -238,10 +247,12 @@ where
}
}
+ #[track_caller]
pub fn next(&mut self, cx: &<T::Summary as Summary>::Context) {
self.next_internal(|_| true, cx)
}
+ #[track_caller]
fn next_internal<F>(&mut self, mut filter_node: F, cx: &<T::Summary as Summary>::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<Target>(
&mut self,
pos: &Target,
@@ -355,6 +368,7 @@ where
self.seek_internal(pos, bias, &mut (), cx)
}
+ #[track_caller]
pub fn seek_forward<Target>(
&mut self,
pos: &Target,
@@ -367,6 +381,7 @@ where
self.seek_internal(pos, bias, &mut (), cx)
}
+ #[track_caller]
pub fn slice<Target>(
&mut self,
end: &Target,
@@ -386,10 +401,12 @@ where
slice.tree
}
+ #[track_caller]
pub fn suffix(&mut self, cx: &<T::Summary as Summary>::Context) -> SumTree<T> {
self.slice(&End::new(), Bias::Right, cx)
}
+ #[track_caller]
pub fn summary<Target, Output>(
&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>,
@@ -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<Anchor>, buffer: &BufferSnapshot) -> Result<Ordering>;
+ fn cmp(&self, b: &Range<Anchor>, buffer: &BufferSnapshot) -> Ordering;
}
impl AnchorRangeExt for Range<Anchor> {
- fn cmp(&self, other: &Range<Anchor>, buffer: &BufferSnapshot) -> Result<Ordering> {
- Ok(match self.start.cmp(&other.start, buffer) {
+ fn cmp(&self, other: &Range<Anchor>, buffer: &BufferSnapshot) -> Ordering {
+ match self.start.cmp(&other.start, buffer) {
Ordering::Equal => other.end.cmp(&self.end, buffer),
ord => ord,
- })
+ }
}
}