Introduce custom fold placeholders (#12214)

Antonio Scandurra , Nathan , and Conrad created

This pull request replaces the static `⋯` character we used to insert
when folding a range with a custom render function that return an
`AnyElement`. We plan to use this in the assistant, but for now this
should be behavior-preserving.

Release Notes:

- N/A

---------

Co-authored-by: Nathan <nathan@zed.dev>
Co-authored-by: Conrad <conrad@zed.dev>

Change summary

crates/assistant/src/assistant.rs          |   2 
crates/assistant/src/assistant_panel.rs    |   7 
crates/assistant2/src/assistant2.rs        |   1 
crates/editor/src/display_map.rs           | 189 +++++--
crates/editor/src/display_map/block_map.rs |   2 
crates/editor/src/display_map/flap_map.rs  |   7 
crates/editor/src/display_map/fold_map.rs  | 214 +++++---
crates/editor/src/display_map/inlay_map.rs |   2 
crates/editor/src/display_map/tab_map.rs   |   4 
crates/editor/src/display_map/wrap_map.rs  |   4 
crates/editor/src/editor.rs                |  58 ++
crates/editor/src/editor_tests.rs          |  33 
crates/editor/src/element.rs               | 588 ++++++++++++++----------
crates/editor/src/movement.rs              |  30 +
crates/editor/src/scroll/autoscroll.rs     |   2 
crates/editor/src/test.rs                  |  17 
crates/gpui/src/elements/text.rs           |  14 
crates/gpui/src/text_system/line_layout.rs |   2 
crates/language/src/buffer.rs              |  27 +
19 files changed, 770 insertions(+), 433 deletions(-)

Detailed changes

crates/assistant/src/assistant.rs 🔗

@@ -189,7 +189,7 @@ pub struct LanguageModelChoiceDelta {
 struct MessageMetadata {
     role: Role,
     status: MessageStatus,
-    // todo!("delete this")
+    // TODO: Delete this
     #[serde(skip)]
     ambient_context: AmbientContextSnapshot,
 }

crates/assistant/src/assistant_panel.rs 🔗

@@ -19,6 +19,7 @@ use crate::{
 use anyhow::{anyhow, Result};
 use client::telemetry::Telemetry;
 use collections::{hash_map, HashMap, HashSet, VecDeque};
+use editor::FoldPlaceholder;
 use editor::{
     actions::{FoldAt, MoveDown, MoveUp},
     display_map::{
@@ -34,7 +35,7 @@ use fs::Fs;
 use futures::StreamExt;
 use gpui::{
     canvas, div, point, relative, rems, uniform_list, Action, AnyElement, AnyView, AppContext,
-    AsyncAppContext, AsyncWindowContext, AvailableSpace, ClipboardItem, Context, Entity,
+    AsyncAppContext, AsyncWindowContext, AvailableSpace, ClipboardItem, Context, Empty, Entity,
     EventEmitter, FocusHandle, FocusableView, FontStyle, FontWeight, HighlightStyle,
     InteractiveElement, IntoElement, Model, ModelContext, ParentElement, Pixels, Render,
     SharedString, StatefulInteractiveElement, Styled, Subscription, Task, TextStyle,
@@ -2943,6 +2944,10 @@ impl ConversationEditor {
                         .insert_flaps(
                             [Flap::new(
                                 start..end,
+                                FoldPlaceholder {
+                                    render: Arc::new(|_, _, _| Empty.into_any()),
+                                    constrain_width: false,
+                                },
                                 render_slash_command_output_toggle,
                                 render_slash_command_output_trailer,
                             )],

crates/assistant2/src/assistant2.rs 🔗

@@ -869,7 +869,6 @@ impl AssistantChat {
                             crate::ui::ChatMessage::new(
                                 *id,
                                 UserOrAssistant::User(self.user_store.read(cx).current_user()),
-                                // todo!(): clean up the vec usage
                                 vec![
                                     body.clone().into_any_element(),
                                     h_flex()

crates/editor/src/display_map.rs 🔗

@@ -24,17 +24,27 @@ mod inlay_map;
 mod tab_map;
 mod wrap_map;
 
-use crate::{hover_links::InlayHighlight, movement::TextLayoutDetails, InlayId};
-use crate::{EditorStyle, RowExt};
-pub use block_map::{BlockMap, BlockPoint};
+use crate::{
+    hover_links::InlayHighlight, movement::TextLayoutDetails, EditorStyle, InlayId, RowExt,
+};
+pub use block_map::{
+    BlockBufferRows, BlockChunks as DisplayChunks, BlockContext, BlockDisposition, BlockId,
+    BlockMap, BlockPoint, BlockProperties, BlockStyle, RenderBlock, TransformBlock,
+};
+use block_map::{BlockRow, BlockSnapshot};
 use collections::{HashMap, HashSet};
-use fold_map::FoldMap;
+pub use flap_map::*;
+pub use fold_map::{Fold, FoldId, FoldPlaceholder, FoldPoint};
+use fold_map::{FoldMap, FoldSnapshot};
 use gpui::{
-    AnyElement, Font, HighlightStyle, Hsla, LineLayout, Model, ModelContext, Pixels, UnderlineStyle,
+    AnyElement, Font, HighlightStyle, LineLayout, Model, ModelContext, Pixels, UnderlineStyle,
 };
-use inlay_map::InlayMap;
+pub(crate) use inlay_map::Inlay;
+use inlay_map::{InlayMap, InlaySnapshot};
+pub use inlay_map::{InlayOffset, InlayPoint};
 use language::{
-    language_settings::language_settings, OffsetUtf16, Point, Subscription as BufferSubscription,
+    language_settings::language_settings, ChunkRenderer, OffsetUtf16, Point,
+    Subscription as BufferSubscription,
 };
 use lsp::DiagnosticSeverity;
 use multi_buffer::{
@@ -42,27 +52,12 @@ use multi_buffer::{
     ToOffset, ToPoint,
 };
 use serde::Deserialize;
+use std::ops::Add;
 use std::{any::TypeId, borrow::Cow, fmt::Debug, num::NonZeroU32, ops::Range, sync::Arc};
 use sum_tree::{Bias, TreeMap};
-use tab_map::TabMap;
+use tab_map::{TabMap, TabSnapshot};
 use ui::WindowContext;
-
-use wrap_map::WrapMap;
-
-pub use block_map::{
-    BlockBufferRows, BlockChunks as DisplayChunks, BlockContext, BlockDisposition, BlockId,
-    BlockProperties, BlockStyle, RenderBlock, TransformBlock,
-};
-pub use flap_map::*;
-
-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;
+use wrap_map::{WrapMap, WrapSnapshot};
 
 #[derive(Copy, Clone, Debug, PartialEq, Eq)]
 pub enum FoldStatus {
@@ -105,10 +100,12 @@ pub struct DisplayMap {
     inlay_highlights: InlayHighlights,
     /// A container for explicitly foldable ranges, which supersede indentation based fold range suggestions.
     flap_map: FlapMap,
+    fold_placeholder: FoldPlaceholder,
     pub clip_at_line_ends: bool,
 }
 
 impl DisplayMap {
+    #[allow(clippy::too_many_arguments)]
     pub fn new(
         buffer: Model<MultiBuffer>,
         font: Font,
@@ -116,6 +113,7 @@ impl DisplayMap {
         wrap_width: Option<Pixels>,
         buffer_header_height: u8,
         excerpt_header_height: u8,
+        fold_placeholder: FoldPlaceholder,
         cx: &mut ModelContext<Self>,
     ) -> Self {
         let buffer_subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
@@ -138,6 +136,7 @@ impl DisplayMap {
             wrap_map,
             block_map,
             flap_map,
+            fold_placeholder,
             text_highlights: Default::default(),
             inlay_highlights: Default::default(),
             clip_at_line_ends: false,
@@ -167,6 +166,7 @@ impl DisplayMap {
             text_highlights: self.text_highlights.clone(),
             inlay_highlights: self.inlay_highlights.clone(),
             clip_at_line_ends: self.clip_at_line_ends,
+            fold_placeholder: self.fold_placeholder.clone(),
         }
     }
 
@@ -174,14 +174,19 @@ impl DisplayMap {
         self.fold(
             other
                 .folds_in_range(0..other.buffer_snapshot.len())
-                .map(|fold| (fold.range.to_offset(&other.buffer_snapshot), fold.text)),
+                .map(|fold| {
+                    (
+                        fold.range.to_offset(&other.buffer_snapshot),
+                        fold.placeholder.clone(),
+                    )
+                }),
             cx,
         );
     }
 
     pub fn fold<T: ToOffset>(
         &mut self,
-        ranges: impl IntoIterator<Item = (Range<T>, &'static str)>,
+        ranges: impl IntoIterator<Item = (Range<T>, FoldPlaceholder)>,
         cx: &mut ModelContext<Self>,
     ) {
         let snapshot = self.buffer.read(cx).snapshot(cx);
@@ -324,10 +329,6 @@ impl DisplayMap {
             .update(cx, |map, cx| map.set_font_with_size(font, font_size, cx))
     }
 
-    pub fn set_fold_ellipses_color(&mut self, color: Hsla) -> bool {
-        self.fold_map.set_ellipses_color(color)
-    }
-
     pub fn set_wrap_width(&self, width: Option<Pixels>, cx: &mut ModelContext<Self>) -> bool {
         self.wrap_map
             .update(cx, |map, cx| map.set_wrap_width(width, cx))
@@ -394,9 +395,10 @@ pub struct HighlightStyles {
 }
 
 pub struct HighlightedChunk<'a> {
-    pub chunk: &'a str,
+    pub text: &'a str,
     pub style: Option<HighlightStyle>,
     pub is_tab: bool,
+    pub renderer: Option<ChunkRenderer>,
 }
 
 #[derive(Clone)]
@@ -411,6 +413,7 @@ pub struct DisplaySnapshot {
     text_highlights: TextHighlights,
     inlay_highlights: InlayHighlights,
     clip_at_line_ends: bool,
+    pub(crate) fold_placeholder: FoldPlaceholder,
 }
 
 impl DisplaySnapshot {
@@ -648,9 +651,10 @@ impl DisplaySnapshot {
             }
 
             HighlightedChunk {
-                chunk: chunk.text,
+                text: chunk.text,
                 style: highlight_style,
                 is_tab: chunk.is_tab,
+                renderer: chunk.renderer,
             }
         })
     }
@@ -672,7 +676,7 @@ impl DisplaySnapshot {
 
         let range = display_row..display_row.next_row();
         for chunk in self.highlighted_chunks(range, false, &editor_style) {
-            line.push_str(chunk.chunk);
+            line.push_str(chunk.text);
 
             let text_style = if let Some(style) = chunk.style {
                 Cow::Owned(editor_style.text.clone().highlight(style))
@@ -680,7 +684,7 @@ impl DisplaySnapshot {
                 Cow::Borrowed(&editor_style.text)
             };
 
-            runs.push(text_style.to_run(chunk.chunk.len()))
+            runs.push(text_style.to_run(chunk.text.len()))
         }
 
         if line.ends_with('\n') {
@@ -883,13 +887,16 @@ impl DisplaySnapshot {
     pub fn foldable_range(
         &self,
         buffer_row: MultiBufferRow,
-    ) -> Option<(Range<Point>, &'static str)> {
+    ) -> Option<(Range<Point>, FoldPlaceholder)> {
         let start = MultiBufferPoint::new(buffer_row.0, self.buffer_snapshot.line_len(buffer_row));
         if let Some(flap) = self
             .flap_snapshot
             .query_row(buffer_row, &self.buffer_snapshot)
         {
-            Some((flap.range.to_point(&self.buffer_snapshot), ""))
+            Some((
+                flap.range.to_point(&self.buffer_snapshot),
+                flap.placeholder.clone(),
+            ))
         } else if self.starts_indent(MultiBufferRow(start.row))
             && !self.is_line_folded(MultiBufferRow(start.row))
         {
@@ -909,7 +916,7 @@ impl DisplaySnapshot {
                 }
             }
             let end = end.unwrap_or(max_point);
-            Some((start..end, "⋯"))
+            Some((start..end, self.fold_placeholder.clone()))
         } else {
             None
         }
@@ -950,6 +957,14 @@ impl Debug for DisplayPoint {
 #[serde(transparent)]
 pub struct DisplayRow(pub u32);
 
+impl Add for DisplayRow {
+    type Output = Self;
+
+    fn add(self, other: Self) -> Self::Output {
+        DisplayRow(self.0 + other.0)
+    }
+}
+
 impl DisplayPoint {
     pub fn new(row: DisplayRow, column: u32) -> Self {
         Self(BlockPoint(Point::new(row.0, column)))
@@ -1083,6 +1098,7 @@ pub mod tests {
                 wrap_width,
                 buffer_start_excerpt_header_height,
                 excerpt_header_height,
+                FoldPlaceholder::test(),
                 cx,
             )
         });
@@ -1186,7 +1202,12 @@ pub mod tests {
                     } else {
                         log::info!("folding ranges: {:?}", ranges);
                         map.update(cx, |map, cx| {
-                            map.fold(ranges.into_iter().map(|range| (range, "⋯")), cx);
+                            map.fold(
+                                ranges
+                                    .into_iter()
+                                    .map(|range| (range, FoldPlaceholder::test())),
+                                cx,
+                            );
                         });
                     }
                 }
@@ -1323,6 +1344,7 @@ pub mod tests {
                     wrap_width,
                     1,
                     1,
+                    FoldPlaceholder::test(),
                     cx,
                 )
             });
@@ -1424,7 +1446,16 @@ pub mod tests {
 
         let font_size = px(14.0);
         let map = cx.new_model(|cx| {
-            DisplayMap::new(buffer.clone(), font("Helvetica"), font_size, None, 1, 1, cx)
+            DisplayMap::new(
+                buffer.clone(),
+                font("Helvetica"),
+                font_size,
+                None,
+                1,
+                1,
+                FoldPlaceholder::test(),
+                cx,
+            )
         });
 
         buffer.update(cx, |buffer, cx| {
@@ -1510,8 +1541,18 @@ pub mod tests {
 
         let font_size = px(14.0);
 
-        let map = cx
-            .new_model(|cx| DisplayMap::new(buffer, font("Helvetica"), font_size, None, 1, 1, cx));
+        let map = cx.new_model(|cx| {
+            DisplayMap::new(
+                buffer,
+                font("Helvetica"),
+                font_size,
+                None,
+                1,
+                1,
+                FoldPlaceholder::test(),
+                cx,
+            )
+        });
         assert_eq!(
             cx.update(|cx| syntax_chunks(DisplayRow(0)..DisplayRow(5), &map, &theme, cx)),
             vec![
@@ -1536,7 +1577,7 @@ pub mod tests {
             map.fold(
                 vec![(
                     MultiBufferPoint::new(0, 6)..MultiBufferPoint::new(3, 2),
-                    "⋯",
+                    FoldPlaceholder::test(),
                 )],
                 cx,
             )
@@ -1602,7 +1643,16 @@ pub mod tests {
         let font_size = px(16.0);
 
         let map = cx.new_model(|cx| {
-            DisplayMap::new(buffer, font("Courier"), font_size, Some(px(40.0)), 1, 1, cx)
+            DisplayMap::new(
+                buffer,
+                font("Courier"),
+                font_size,
+                Some(px(40.0)),
+                1,
+                1,
+                FoldPlaceholder::test(),
+                cx,
+            )
         });
         assert_eq!(
             cx.update(|cx| syntax_chunks(DisplayRow(0)..DisplayRow(5), &map, &theme, cx)),
@@ -1621,7 +1671,7 @@ pub mod tests {
             map.fold(
                 vec![(
                     MultiBufferPoint::new(0, 6)..MultiBufferPoint::new(3, 2),
-                    "⋯",
+                    FoldPlaceholder::test(),
                 )],
                 cx,
             )
@@ -1674,8 +1724,18 @@ pub mod tests {
         let buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
 
         let font_size = px(16.0);
-        let map =
-            cx.new_model(|cx| DisplayMap::new(buffer, font("Courier"), font_size, None, 1, 1, cx));
+        let map = cx.new_model(|cx| {
+            DisplayMap::new(
+                buffer,
+                font("Courier"),
+                font_size,
+                None,
+                1,
+                1,
+                FoldPlaceholder::test(),
+                cx,
+            )
+        });
 
         enum MyType {}
 
@@ -1789,8 +1849,16 @@ pub mod tests {
         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 mut map = DisplayMap::new(
+                buffer.clone(),
+                font("Helvetica"),
+                font_size,
+                None,
+                1,
+                1,
+                FoldPlaceholder::test(),
+                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));
@@ -1798,6 +1866,7 @@ pub mod tests {
             map.flap_map.insert(
                 [Flap::new(
                     range,
+                    FoldPlaceholder::test(),
                     |_row, _status, _toggle, _cx| div(),
                     |_row, _status, _cx| div(),
                 )],
@@ -1817,7 +1886,16 @@ pub mod tests {
         let font_size = px(14.0);
 
         let map = cx.new_model(|cx| {
-            DisplayMap::new(buffer.clone(), font("Helvetica"), font_size, None, 1, 1, cx)
+            DisplayMap::new(
+                buffer.clone(),
+                font("Helvetica"),
+                font_size,
+                None,
+                1,
+                1,
+                FoldPlaceholder::test(),
+                cx,
+            )
         });
         let map = map.update(cx, |map, cx| map.snapshot(cx));
         assert_eq!(map.text(), "✅       α\nβ   \n🏀β      γ");
@@ -1883,7 +1961,16 @@ pub mod tests {
         let buffer = MultiBuffer::build_simple("aaa\n\t\tbbb", cx);
         let font_size = px(14.0);
         let map = cx.new_model(|cx| {
-            DisplayMap::new(buffer.clone(), font("Helvetica"), font_size, None, 1, 1, cx)
+            DisplayMap::new(
+                buffer.clone(),
+                font("Helvetica"),
+                font_size,
+                None,
+                1,
+                1,
+                FoldPlaceholder::test(),
+                cx,
+            )
         });
         assert_eq!(
             map.update(cx, |map, cx| map.snapshot(cx)).max_point(),

crates/editor/src/display_map/flap_map.rs 🔗

@@ -6,6 +6,8 @@ use sum_tree::{Bias, SeekTarget, SumTree};
 use text::Point;
 use ui::WindowContext;
 
+use crate::FoldPlaceholder;
+
 #[derive(Copy, Clone, Default, Debug, Eq, PartialEq, PartialOrd, Ord, Hash)]
 pub struct FlapId(usize);
 
@@ -76,6 +78,7 @@ type RenderTrailerFn =
 #[derive(Clone)]
 pub struct Flap {
     pub range: Range<Anchor>,
+    pub placeholder: FoldPlaceholder,
     pub render_toggle: RenderToggleFn,
     pub render_trailer: RenderTrailerFn,
 }
@@ -83,6 +86,7 @@ pub struct Flap {
 impl Flap {
     pub fn new<RenderToggle, ToggleElement, RenderTrailer, TrailerElement>(
         range: Range<Anchor>,
+        placeholder: FoldPlaceholder,
         render_toggle: RenderToggle,
         render_trailer: RenderTrailer,
     ) -> Self
@@ -107,6 +111,7 @@ impl Flap {
     {
         Flap {
             range,
+            placeholder,
             render_toggle: Arc::new(move |row, folded, toggle, cx| {
                 render_toggle(row, folded, toggle, cx).into_any_element()
             }),
@@ -256,11 +261,13 @@ mod test {
         let flaps = [
             Flap::new(
                 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(1, 5)),
+                FoldPlaceholder::test(),
                 |_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)),
+                FoldPlaceholder::test(),
                 |_row, _folded, _toggle, _cx| div(),
                 |_row, _folded, _cx| div(),
             ),

crates/editor/src/display_map/fold_map.rs 🔗

@@ -2,17 +2,54 @@ use super::{
     inlay_map::{InlayBufferRows, InlayChunks, InlayEdit, InlayOffset, InlayPoint, InlaySnapshot},
     Highlights,
 };
-use gpui::{ElementId, HighlightStyle, Hsla};
-use language::{Chunk, Edit, Point, TextSummary};
+use gpui::{AnyElement, ElementId, WindowContext};
+use language::{Chunk, ChunkRenderer, Edit, Point, TextSummary};
 use multi_buffer::{Anchor, AnchorRangeExt, MultiBufferRow, MultiBufferSnapshot, ToOffset};
 use std::{
     cmp::{self, Ordering},
-    iter,
+    fmt, iter,
     ops::{Add, AddAssign, Deref, DerefMut, Range, Sub},
+    sync::Arc,
 };
 use sum_tree::{Bias, Cursor, FilterCursor, SumTree};
 use util::post_inc;
 
+#[derive(Clone)]
+pub struct FoldPlaceholder {
+    /// Creates an element to represent this fold's placeholder.
+    pub render: Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut WindowContext) -> AnyElement>,
+    /// If true, the element is constrained to the shaped width of an ellipsis.
+    pub constrain_width: bool,
+}
+
+impl FoldPlaceholder {
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn test() -> Self {
+        use gpui::IntoElement;
+
+        Self {
+            render: Arc::new(|_id, _range, _cx| gpui::Empty.into_any_element()),
+            constrain_width: true,
+        }
+    }
+}
+
+impl fmt::Debug for FoldPlaceholder {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        f.debug_struct("FoldPlaceholder")
+            .field("constrain_width", &self.constrain_width)
+            .finish()
+    }
+}
+
+impl Eq for FoldPlaceholder {}
+
+impl PartialEq for FoldPlaceholder {
+    fn eq(&self, other: &Self) -> bool {
+        Arc::ptr_eq(&self.render, &other.render) && self.constrain_width == other.constrain_width
+    }
+}
+
 #[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
 pub struct FoldPoint(pub Point);
 
@@ -54,7 +91,7 @@ impl FoldPoint {
         let mut offset = cursor.start().1.output.len;
         if !overshoot.is_zero() {
             let transform = cursor.item().expect("display point out of range");
-            assert!(transform.output_text.is_none());
+            assert!(transform.placeholder.is_none());
             let end_inlay_offset = snapshot
                 .inlay_snapshot
                 .to_offset(InlayPoint(cursor.start().1.input.lines + overshoot));
@@ -75,7 +112,7 @@ 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>, &'static str)>,
+        ranges: impl IntoIterator<Item = (Range<T>, FoldPlaceholder)>,
     ) -> (FoldSnapshot, Vec<FoldEdit>) {
         let mut edits = Vec::new();
         let mut folds = Vec::new();
@@ -99,7 +136,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,
+                placeholder: fold_text,
             });
 
             let inlay_range =
@@ -183,7 +220,6 @@ impl<'a> FoldMapWriter<'a> {
 /// See the [`display_map` module documentation](crate::display_map) for more information.
 pub(crate) struct FoldMap {
     snapshot: FoldSnapshot,
-    ellipses_color: Option<Hsla>,
     next_fold_id: FoldId,
 }
 
@@ -198,15 +234,13 @@ impl FoldMap {
                             input: inlay_snapshot.text_summary(),
                             output: inlay_snapshot.text_summary(),
                         },
-                        output_text: None,
+                        placeholder: None,
                     },
                     &(),
                 ),
                 inlay_snapshot: inlay_snapshot.clone(),
                 version: 0,
-                ellipses_color: None,
             },
-            ellipses_color: None,
             next_fold_id: FoldId::default(),
         };
         let snapshot = this.snapshot.clone();
@@ -232,15 +266,6 @@ impl FoldMap {
         (FoldMapWriter(self), snapshot, edits)
     }
 
-    pub fn set_ellipses_color(&mut self, color: Hsla) -> bool {
-        if self.ellipses_color == Some(color) {
-            false
-        } else {
-            self.ellipses_color = Some(color);
-            true
-        }
-    }
-
     fn check_invariants(&self) {
         if cfg!(test) {
             assert_eq!(
@@ -329,9 +354,9 @@ impl FoldMap {
                             let buffer_start = fold.range.start.to_offset(&inlay_snapshot.buffer);
                             let buffer_end = fold.range.end.to_offset(&inlay_snapshot.buffer);
                             (
+                                fold.clone(),
                                 inlay_snapshot.to_inlay_offset(buffer_start)
                                     ..inlay_snapshot.to_inlay_offset(buffer_end),
-                                fold.text,
                             )
                         });
                         folds_cursor.next(&inlay_snapshot.buffer);
@@ -342,17 +367,17 @@ impl FoldMap {
 
                 while folds
                     .peek()
-                    .map_or(false, |(fold_range, _)| fold_range.start < edit.new.end)
+                    .map_or(false, |(_, fold_range)| fold_range.start < edit.new.end)
                 {
-                    let (mut fold_range, fold_text) = folds.next().unwrap();
+                    let (fold, mut fold_range) = folds.next().unwrap();
                     let sum = new_transforms.summary();
 
                     assert!(fold_range.start.0 >= sum.input.len);
 
-                    while folds.peek().map_or(false, |(next_fold_range, _)| {
+                    while folds.peek().map_or(false, |(_, next_fold_range)| {
                         next_fold_range.start <= fold_range.end
                     }) {
-                        let (next_fold_range, _) = folds.next().unwrap();
+                        let (_, next_fold_range) = folds.next().unwrap();
                         if next_fold_range.end > fold_range.end {
                             fold_range.end = next_fold_range.end;
                         }
@@ -367,21 +392,36 @@ impl FoldMap {
                                     output: text_summary.clone(),
                                     input: text_summary,
                                 },
-                                output_text: None,
+                                placeholder: None,
                             },
                             &(),
                         );
                     }
 
                     if fold_range.end > fold_range.start {
+                        const ELLIPSIS: &'static str = "⋯";
+
+                        let fold_id = fold.id;
                         new_transforms.push(
                             Transform {
                                 summary: TransformSummary {
-                                    output: TextSummary::from(fold_text),
+                                    output: TextSummary::from(ELLIPSIS),
                                     input: inlay_snapshot
                                         .text_summary_for_range(fold_range.start..fold_range.end),
                                 },
-                                output_text: Some(fold_text),
+                                placeholder: Some(TransformPlaceholder {
+                                    text: ELLIPSIS,
+                                    renderer: ChunkRenderer {
+                                        render: Arc::new(move |cx| {
+                                            (fold.placeholder.render)(
+                                                fold_id,
+                                                fold.range.0.clone(),
+                                                cx,
+                                            )
+                                        }),
+                                        constrain_width: fold.placeholder.constrain_width,
+                                    },
+                                }),
                             },
                             &(),
                         );
@@ -398,7 +438,7 @@ impl FoldMap {
                                 output: text_summary.clone(),
                                 input: text_summary,
                             },
-                            output_text: None,
+                            placeholder: None,
                         },
                         &(),
                     );
@@ -414,7 +454,7 @@ impl FoldMap {
                             output: text_summary.clone(),
                             input: text_summary,
                         },
-                        output_text: None,
+                        placeholder: None,
                     },
                     &(),
                 );
@@ -484,7 +524,6 @@ pub struct FoldSnapshot {
     folds: SumTree<Fold>,
     pub inlay_snapshot: InlaySnapshot,
     pub version: usize,
-    pub ellipses_color: Option<Hsla>,
 }
 
 impl FoldSnapshot {
@@ -508,9 +547,9 @@ impl FoldSnapshot {
         if let Some(transform) = cursor.item() {
             let start_in_transform = range.start.0 - cursor.start().0 .0;
             let end_in_transform = cmp::min(range.end, cursor.end(&()).0).0 - cursor.start().0 .0;
-            if let Some(output_text) = transform.output_text {
+            if let Some(placeholder) = transform.placeholder.as_ref() {
                 summary = TextSummary::from(
-                    &output_text
+                    &placeholder.text
                         [start_in_transform.column as usize..end_in_transform.column as usize],
                 );
             } else {
@@ -533,8 +572,9 @@ impl FoldSnapshot {
                 .output;
             if let Some(transform) = cursor.item() {
                 let end_in_transform = range.end.0 - cursor.start().0 .0;
-                if let Some(output_text) = transform.output_text {
-                    summary += TextSummary::from(&output_text[..end_in_transform.column as usize]);
+                if let Some(placeholder) = transform.placeholder.as_ref() {
+                    summary +=
+                        TextSummary::from(&placeholder.text[..end_in_transform.column as usize]);
                 } else {
                     let inlay_start = self.inlay_snapshot.to_offset(cursor.start().1);
                     let inlay_end = self
@@ -631,7 +671,7 @@ impl FoldSnapshot {
         let inlay_offset = self.inlay_snapshot.to_inlay_offset(buffer_offset);
         let mut cursor = self.transforms.cursor::<InlayOffset>();
         cursor.seek(&inlay_offset, Bias::Right, &());
-        cursor.item().map_or(false, |t| t.output_text.is_some())
+        cursor.item().map_or(false, |t| t.placeholder.is_some())
     }
 
     pub fn is_line_folded(&self, buffer_row: MultiBufferRow) -> bool {
@@ -646,7 +686,7 @@ impl FoldSnapshot {
                     let buffer_point = self.inlay_snapshot.to_buffer_point(inlay_point);
                     if buffer_point.row != buffer_row.0 {
                         return false;
-                    } else if transform.output_text.is_some() {
+                    } else if transform.placeholder.is_some() {
                         return true;
                     }
                 }
@@ -693,7 +733,6 @@ impl FoldSnapshot {
             inlay_offset: inlay_start,
             output_offset: range.start.0,
             max_output_offset: range.end.0,
-            ellipses_color: self.ellipses_color,
         }
     }
 
@@ -720,7 +759,7 @@ impl FoldSnapshot {
         cursor.seek(&point, Bias::Right, &());
         if let Some(transform) = cursor.item() {
             let transform_start = cursor.start().0 .0;
-            if transform.output_text.is_some() {
+            if transform.placeholder.is_some() {
                 if point.0 == transform_start || matches!(bias, Bias::Left) {
                     FoldPoint(transform_start)
                 } else {
@@ -810,15 +849,21 @@ fn consolidate_fold_edits(edits: &mut Vec<FoldEdit>) {
     }
 }
 
-#[derive(Clone, Debug, Default, Eq, PartialEq)]
+#[derive(Clone, Debug, Default)]
 struct Transform {
     summary: TransformSummary,
-    output_text: Option<&'static str>,
+    placeholder: Option<TransformPlaceholder>,
+}
+
+#[derive(Clone, Debug)]
+struct TransformPlaceholder {
+    text: &'static str,
+    renderer: ChunkRenderer,
 }
 
 impl Transform {
     fn is_fold(&self) -> bool {
-        self.output_text.is_some()
+        self.placeholder.is_some()
     }
 }
 
@@ -858,7 +903,7 @@ impl Into<ElementId> for FoldId {
 pub struct Fold {
     pub id: FoldId,
     pub range: FoldRange,
-    pub text: &'static str,
+    pub placeholder: FoldPlaceholder,
 }
 
 #[derive(Clone, Debug, Eq, PartialEq)]
@@ -1004,7 +1049,6 @@ pub struct FoldChunks<'a> {
     inlay_offset: InlayOffset,
     output_offset: usize,
     max_output_offset: usize,
-    ellipses_color: Option<Hsla>,
 }
 
 impl<'a> Iterator for FoldChunks<'a> {
@@ -1019,7 +1063,7 @@ impl<'a> Iterator for FoldChunks<'a> {
 
         // If we're in a fold, then return the fold's display text and
         // advance the transform and buffer cursors to the end of the fold.
-        if let Some(output_text) = transform.output_text {
+        if let Some(placeholder) = transform.placeholder.as_ref() {
             self.inlay_chunk.take();
             self.inlay_offset += InlayOffset(transform.summary.input.len);
             self.inlay_chunks.seek(self.inlay_offset);
@@ -1030,13 +1074,10 @@ impl<'a> Iterator for FoldChunks<'a> {
                 self.transform_cursor.next(&());
             }
 
-            self.output_offset += output_text.len();
+            self.output_offset += placeholder.text.len();
             return Some(Chunk {
-                text: output_text,
-                highlight_style: self.ellipses_color.map(|color| HighlightStyle {
-                    color: Some(color),
-                    ..Default::default()
-                }),
+                text: placeholder.text,
+                renderer: Some(placeholder.renderer.clone()),
                 ..Default::default()
             });
         }
@@ -1048,7 +1089,7 @@ impl<'a> Iterator for FoldChunks<'a> {
         }
 
         // Otherwise, take a chunk from the buffer's text.
-        if let Some((buffer_chunk_start, mut chunk)) = self.inlay_chunk {
+        if let Some((buffer_chunk_start, mut chunk)) = self.inlay_chunk.clone() {
             let buffer_chunk_end = buffer_chunk_start + InlayOffset(chunk.text.len());
             let transform_end = self.transform_cursor.end(&()).1;
             let chunk_end = buffer_chunk_end.min(transform_end);
@@ -1165,8 +1206,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), FoldPlaceholder::test()),
+            (Point::new(2, 4)..Point::new(4, 1), FoldPlaceholder::test()),
         ]);
         assert_eq!(snapshot2.text(), "aa⋯cc⋯eeeee");
         assert_eq!(
@@ -1245,19 +1286,25 @@ 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, FoldPlaceholder::test())]);
             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, FoldPlaceholder::test()),
+                (2..5, FoldPlaceholder::test()),
+            ]);
             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, FoldPlaceholder::test()),
+                (8..10, FoldPlaceholder::test()),
+            ]);
             let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]);
             assert_eq!(snapshot.text(), "⋯b⋯kl");
         }
@@ -1267,7 +1314,10 @@ 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, FoldPlaceholder::test()),
+                (2..5, FoldPlaceholder::test()),
+            ]);
             let (snapshot, _) = map.read(inlay_snapshot, vec![]);
             assert_eq!(snapshot.text(), "⋯fghijkl");
 
@@ -1291,10 +1341,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), FoldPlaceholder::test()),
+            (Point::new(0, 4)..Point::new(1, 0), FoldPlaceholder::test()),
+            (Point::new(1, 2)..Point::new(3, 2), FoldPlaceholder::test()),
+            (Point::new(3, 1)..Point::new(4, 1), FoldPlaceholder::test()),
         ]);
         let (snapshot, _) = map.read(inlay_snapshot, vec![]);
         assert_eq!(snapshot.text(), "aa⋯eeeee");
@@ -1311,8 +1361,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), FoldPlaceholder::test()),
+            (Point::new(3, 1)..Point::new(4, 1), FoldPlaceholder::test()),
         ]);
         let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]);
         assert_eq!(snapshot.text(), "aa⋯cccc\nd⋯eeeee");
@@ -1336,10 +1386,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), FoldPlaceholder::test()),
+            (Point::new(0, 4)..Point::new(1, 0), FoldPlaceholder::test()),
+            (Point::new(1, 2)..Point::new(3, 2), FoldPlaceholder::test()),
+            (Point::new(3, 1)..Point::new(4, 1), FoldPlaceholder::test()),
         ]);
         let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]);
         let fold_ranges = snapshot
@@ -1414,10 +1464,10 @@ mod tests {
             snapshot_edits.push((snapshot.clone(), edits));
 
             let mut expected_text: String = inlay_snapshot.text().to_string();
-            for (fold_range, fold_text) in map.merged_folds().into_iter().rev() {
+            for fold_range 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, fold_text);
+                expected_text.replace_range(fold_inlay_start.0..fold_inlay_end.0, "⋯");
             }
 
             assert_eq!(snapshot.text(), expected_text);
@@ -1429,7 +1479,7 @@ mod tests {
 
             let mut prev_row = 0;
             let mut expected_buffer_rows = Vec::new();
-            for (fold_range, _fold_text) in map.merged_folds().into_iter() {
+            for fold_range in map.merged_folds() {
                 let fold_start = inlay_snapshot
                     .to_point(inlay_snapshot.to_inlay_offset(fold_range.start))
                     .row();
@@ -1543,7 +1593,7 @@ mod tests {
             let folded_buffer_rows = map
                 .merged_folds()
                 .iter()
-                .flat_map(|(fold_range, _)| {
+                .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 {
@@ -1640,8 +1690,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), FoldPlaceholder::test()),
+            (Point::new(3, 1)..Point::new(4, 1), FoldPlaceholder::test()),
         ]);
 
         let (snapshot, _) = map.read(inlay_snapshot, vec![]);
@@ -1659,7 +1709,7 @@ mod tests {
     }
 
     impl FoldMap {
-        fn merged_folds(&self) -> Vec<(Range<usize>, &'static str)> {
+        fn merged_folds(&self) -> Vec<Range<usize>> {
             let inlay_snapshot = self.snapshot.inlay_snapshot.clone();
             let buffer = &inlay_snapshot.buffer;
             let mut folds = self.snapshot.folds.items(buffer);
@@ -1667,17 +1717,12 @@ mod tests {
             folds.sort_by(|a, b| a.range.cmp(&b.range, buffer));
             let mut folds = folds
                 .iter()
-                .map(|fold| {
-                    (
-                        fold.range.start.to_offset(buffer)..fold.range.end.to_offset(buffer),
-                        fold.text,
-                    )
-                })
+                .map(|fold| fold.range.start.to_offset(buffer)..fold.range.end.to_offset(buffer))
                 .peekable();
 
             let mut merged_folds = Vec::new();
-            while let Some((mut fold_range, fold_text)) = folds.next() {
-                while let Some((next_range, _)) = folds.peek() {
+            while let Some(mut fold_range) = 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;
@@ -1688,7 +1733,7 @@ mod tests {
                     }
                 }
                 if fold_range.end > fold_range.start {
-                    merged_folds.push((fold_range, fold_text));
+                    merged_folds.push(fold_range);
                 }
             }
             merged_folds
@@ -1723,8 +1768,7 @@ 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);
-                        let text = if rng.gen() { "⋯" } else { "" };
-                        to_fold.push((start..end, text));
+                        to_fold.push((start..end, FoldPlaceholder::test()));
                     }
                     log::info!("folding {:?}", to_fold);
                     let (mut writer, snapshot, edits) = self.write(inlay_snapshot, vec![]);

crates/editor/src/display_map/inlay_map.rs 🔗

@@ -284,7 +284,7 @@ impl<'a> Iterator for InlayChunks<'a> {
                 self.output_offset.0 += prefix.len();
                 let mut prefix = Chunk {
                     text: prefix,
-                    ..*chunk
+                    ..chunk.clone()
                 };
                 if !self.active_highlights.is_empty() {
                     let mut highlight_style = HighlightStyle::default();

crates/editor/src/display_map/tab_map.rs 🔗

@@ -508,7 +508,7 @@ impl<'a> Iterator for TabChunks<'a> {
                         self.chunk.text = suffix;
                         return Some(Chunk {
                             text: prefix,
-                            ..self.chunk
+                            ..self.chunk.clone()
                         });
                     } else {
                         self.chunk.text = &self.chunk.text[1..];
@@ -529,7 +529,7 @@ impl<'a> Iterator for TabChunks<'a> {
                         return Some(Chunk {
                             text: &SPACES[..len as usize],
                             is_tab: true,
-                            ..self.chunk
+                            ..self.chunk.clone()
                         });
                     }
                 }

crates/editor/src/display_map/wrap_map.rs 🔗

@@ -812,7 +812,7 @@ impl<'a> Iterator for WrapChunks<'a> {
             self.transforms.next(&());
             return Some(Chunk {
                 text: &display_text[start_ix..end_ix],
-                ..self.input_chunk
+                ..self.input_chunk.clone()
             });
         }
 
@@ -842,7 +842,7 @@ impl<'a> Iterator for WrapChunks<'a> {
         self.input_chunk.text = suffix;
         Some(Chunk {
             text: prefix,
-            ..self.input_chunk
+            ..self.input_chunk.clone()
         })
     }
 }

crates/editor/src/editor.rs 🔗

@@ -52,8 +52,8 @@ use clock::ReplicaId;
 use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque};
 use convert_case::{Case, Casing};
 use debounced_delay::DebouncedDelay;
-pub use display_map::DisplayPoint;
 use display_map::*;
+pub use display_map::{DisplayPoint, FoldPlaceholder};
 use editor_settings::CurrentLineHighlight;
 pub use editor_settings::EditorSettings;
 use element::LineWithInvisibles;
@@ -1577,8 +1577,48 @@ impl Editor {
     ) -> Self {
         let style = cx.text_style();
         let font_size = style.font_size.to_pixels(cx.rem_size());
+        let editor = cx.view().downgrade();
+        let fold_placeholder = FoldPlaceholder {
+            constrain_width: true,
+            render: Arc::new(move |fold_id, fold_range, cx| {
+                let editor = editor.clone();
+                div()
+                    .id(fold_id)
+                    .bg(cx.theme().colors().ghost_element_background)
+                    .hover(|style| style.bg(cx.theme().colors().ghost_element_hover))
+                    .active(|style| style.bg(cx.theme().colors().ghost_element_active))
+                    .rounded_sm()
+                    .size_full()
+                    .cursor_pointer()
+                    .child("⋯")
+                    .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
+                    .on_click(move |_, cx| {
+                        editor
+                            .update(cx, |editor, cx| {
+                                editor.unfold_ranges(
+                                    [fold_range.start..fold_range.end],
+                                    true,
+                                    false,
+                                    cx,
+                                );
+                                cx.stop_propagation();
+                            })
+                            .ok();
+                    })
+                    .into_any()
+            }),
+        };
         let display_map = cx.new_model(|cx| {
-            DisplayMap::new(buffer.clone(), style.font(), font_size, None, 2, 1, cx)
+            DisplayMap::new(
+                buffer.clone(),
+                style.font(),
+                font_size,
+                None,
+                2,
+                1,
+                fold_placeholder,
+                cx,
+            )
         });
 
         let selections = SelectionsCollection::new(display_map.clone(), buffer.clone());
@@ -5826,7 +5866,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, fold.text));
+                        refold_ranges.push((start..end, fold.placeholder.clone()));
                     }
                 }
             }
@@ -5920,7 +5960,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, fold.text));
+                        refold_ranges.push((start..end, fold.placeholder.clone()));
                     }
                 }
             }
@@ -9298,14 +9338,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, fold_text)) = display_map.foldable_range(buffer_row) {
+        if let Some((fold_range, placeholder)) = display_map.foldable_range(buffer_row) {
             let autoscroll = self
                 .selections
                 .all::<Point>(cx)
                 .iter()
                 .any(|selection| fold_range.overlaps(&selection.range()));
 
-            self.fold_ranges([(fold_range, fold_text)], autoscroll, cx);
+            self.fold_ranges([(fold_range, placeholder)], autoscroll, cx);
         }
     }
 
@@ -9359,9 +9399,9 @@ impl Editor {
                         .buffer_snapshot
                         .line_len(MultiBufferRow(s.end.row)),
                 );
-                (start..end, "⋯")
+                (start..end, display_map.fold_placeholder.clone())
             } else {
-                (s.start..s.end, "⋯")
+                (s.start..s.end, display_map.fold_placeholder.clone())
             }
         });
         self.fold_ranges(ranges, true, cx);
@@ -9369,7 +9409,7 @@ impl Editor {
 
     pub fn fold_ranges<T: ToOffset + Clone>(
         &mut self,
-        ranges: impl IntoIterator<Item = (Range<T>, &'static str)>,
+        ranges: impl IntoIterator<Item = (Range<T>, FoldPlaceholder)>,
         auto_scroll: bool,
         cx: &mut ViewContext<Self>,
     ) {

crates/editor/src/editor_tests.rs 🔗

@@ -496,8 +496,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), FoldPlaceholder::test()),
+                (Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
             ],
             true,
             cx,
@@ -905,9 +905,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), FoldPlaceholder::test()),
+                (Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
+                (Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
             ],
             true,
             cx,
@@ -3409,9 +3409,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), FoldPlaceholder::test()),
+                (Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
+                (Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
             ],
             true,
             cx,
@@ -3893,9 +3893,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), FoldPlaceholder::test()),
+                (Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
+                (Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
             ],
             true,
             cx,
@@ -4550,8 +4550,14 @@ 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),
+                    FoldPlaceholder::test(),
+                ),
+                (
+                    Point::new(3, 20)..Point::new(3, 22),
+                    FoldPlaceholder::test(),
+                ),
             ],
             true,
             cx,
@@ -11973,6 +11979,7 @@ fn test_flap_insertion_and_rendering(cx: &mut TestAppContext) {
 
             let flap = Flap::new(
                 range,
+                FoldPlaceholder::test(),
                 {
                     let toggle_callback = render_args.clone();
                     move |row, folded, callback, _cx| {

crates/editor/src/element.rs 🔗

@@ -23,7 +23,6 @@ use crate::{
     LineDown, LineUp, OpenExcerpts, PageDown, PageUp, Point, RowExt, RowRangeExt, SelectPhase,
     Selection, SoftWrap, ToPoint, CURSORS_VISIBLE_FOR, MAX_LINE_LEN,
 };
-use anyhow::Result;
 use client::ParticipantIndex;
 use collections::{BTreeMap, HashMap};
 use git::{blame::BlameEntry, diff::DiffHunkStatus, Oid};
@@ -31,11 +30,11 @@ use gpui::{
     anchored, deferred, div, fill, outline, point, px, quad, relative, size, svg,
     transparent_black, Action, AnchorCorner, AnyElement, AvailableSpace, Bounds, ClipboardItem,
     ContentMask, Corners, CursorStyle, DispatchPhase, Edges, Element, ElementInputHandler, Entity,
-    GlobalElementId, Hitbox, Hsla, InteractiveElement, IntoElement, ModifiersChangedEvent,
+    FontId, GlobalElementId, Hitbox, Hsla, InteractiveElement, IntoElement, ModifiersChangedEvent,
     MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels,
-    ScrollDelta, ScrollWheelEvent, ShapedLine, SharedString, Size, Stateful,
-    StatefulInteractiveElement, Style, Styled, TextRun, TextStyle, TextStyleRefinement, View,
-    ViewContext, WeakView, WindowContext,
+    ScrollDelta, ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement,
+    Style, Styled, TextRun, TextStyle, TextStyleRefinement, View, ViewContext, WeakView,
+    WindowContext,
 };
 use itertools::Itertools;
 use language::language_settings::{
@@ -48,12 +47,12 @@ use project::{
     ProjectPath,
 };
 use settings::Settings;
-use smallvec::SmallVec;
+use smallvec::{smallvec, SmallVec};
 use std::{
     any::TypeId,
     borrow::Cow,
     cmp::{self, Ordering},
-    fmt::Write,
+    fmt::{self, Write},
     iter, mem,
     ops::{Deref, Range},
     sync::Arc,
@@ -857,76 +856,6 @@ impl EditorElement {
         (selections, active_rows, newest_selection_head)
     }
 
-    #[allow(clippy::too_many_arguments)]
-    fn layout_folds(
-        &self,
-        snapshot: &EditorSnapshot,
-        content_origin: gpui::Point<Pixels>,
-        visible_anchor_range: Range<Anchor>,
-        visible_display_row_range: Range<DisplayRow>,
-        scroll_pixel_position: gpui::Point<Pixels>,
-        line_height: Pixels,
-        line_layouts: &[LineWithInvisibles],
-        cx: &mut WindowContext,
-    ) -> Vec<FoldLayout> {
-        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);
-                debug_assert_eq!(display_range.start.row(), display_range.end.row());
-                let row = display_range.start.row();
-                debug_assert!(row < visible_display_row_range.end);
-                let line_layout = line_layouts
-                    .get(row.minus(visible_display_row_range.start) as usize)
-                    .map(|l| &l.line)?;
-
-                let start_x = content_origin.x
-                    + line_layout.x_for_index(display_range.start.column() as usize)
-                    - scroll_pixel_position.x;
-                let start_y =
-                    content_origin.y + row.as_f32() * line_height - scroll_pixel_position.y;
-                let end_x = content_origin.x
-                    + line_layout.x_for_index(display_range.end.column() as usize)
-                    - scroll_pixel_position.x;
-
-                let fold_bounds = Bounds {
-                    origin: point(start_x, start_y),
-                    size: size(end_x - start_x, line_height),
-                };
-
-                let mut hover_element = div()
-                    .id(fold.id)
-                    .size_full()
-                    .cursor_pointer()
-                    .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
-                    .on_click(
-                        cx.listener_for(&self.editor, move |editor: &mut Editor, _, cx| {
-                            editor.unfold_ranges(
-                                [fold_range.start..fold_range.end],
-                                true,
-                                false,
-                                cx,
-                            );
-                            cx.stop_propagation();
-                        }),
-                    )
-                    .into_any();
-                hover_element.prepaint_as_root(fold_bounds.origin, fold_bounds.size.into(), cx);
-                Some(FoldLayout {
-                    display_range,
-                    hover_element,
-                })
-            })
-            .collect()
-    }
-
     fn collect_cursors(
         &self,
         snapshot: &EditorSnapshot,
@@ -994,8 +923,7 @@ impl EditorElement {
                     }
 
                     let cursor_row_layout = &line_layouts
-                        [cursor_position.row().minus(visible_display_row_range.start) as usize]
-                        .line;
+                        [cursor_position.row().minus(visible_display_row_range.start) as usize];
                     let cursor_column = cursor_position.column() as usize;
 
                     let cursor_character_x = cursor_row_layout.x_for_index(cursor_column);
@@ -1228,7 +1156,7 @@ impl EditorElement {
                 );
                 let size = element.layout_as_root(available_space, cx);
 
-                let line = &lines[ix].line;
+                let line = &lines[ix];
                 let padding = if line.width == Pixels::ZERO {
                     Pixels::ZERO
                 } else {
@@ -1373,7 +1301,7 @@ impl EditorElement {
             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
+                content_origin.x - scroll_pixel_position.x + line_layout.width
             };
             let padded_line_end = line_end + em_width * INLINE_BLAME_PADDING_EM_WIDTHS;
 
@@ -1860,7 +1788,7 @@ impl EditorElement {
         rows: Range<DisplayRow>,
         line_number_layouts: &[Option<ShapedLine>],
         snapshot: &EditorSnapshot,
-        cx: &WindowContext,
+        cx: &mut WindowContext,
     ) -> Vec<LineWithInvisibles> {
         if rows.start >= rows.end {
             return Vec::new();
@@ -1894,8 +1822,11 @@ impl EditorElement {
                         .log_err()
                 })
                 .map(|line| LineWithInvisibles {
-                    line,
+                    width: line.width,
+                    len: line.len,
+                    fragments: smallvec![LineFragment::Text(line)],
                     invisibles: Vec::new(),
+                    font_size,
                 })
                 .collect()
         } else {
@@ -1912,6 +1843,30 @@ impl EditorElement {
         }
     }
 
+    fn prepaint_lines(
+        &self,
+        start_row: DisplayRow,
+        line_layouts: &mut [LineWithInvisibles],
+        line_height: Pixels,
+        scroll_pixel_position: gpui::Point<Pixels>,
+        content_origin: gpui::Point<Pixels>,
+        cx: &mut WindowContext,
+    ) -> SmallVec<[AnyElement; 1]> {
+        let mut line_elements = SmallVec::new();
+        for (ix, line) in line_layouts.iter_mut().enumerate() {
+            let row = start_row + DisplayRow(ix as u32);
+            line.prepaint(
+                line_height,
+                scroll_pixel_position,
+                row,
+                content_origin,
+                &mut line_elements,
+                cx,
+            );
+        }
+        line_elements
+    }
+
     #[allow(clippy::too_many_arguments)]
     fn build_blocks(
         &self,
@@ -1949,11 +1904,9 @@ impl EditorElement {
                     let anchor_x = text_x
                         + if rows.contains(&align_to.row()) {
                             line_layouts[align_to.row().minus(rows.start) as usize]
-                                .line
                                 .x_for_index(align_to.column() as usize)
                         } else {
                             layout_line(align_to.row(), snapshot, &self.style, cx)
-                                .unwrap()
                                 .x_for_index(align_to.column() as usize)
                         };
 
@@ -2310,7 +2263,7 @@ impl EditorElement {
 
         let (x, y) = match position {
             crate::ContextMenuOrigin::EditorPoint(point) => {
-                let cursor_row_layout = &line_layouts[point.row().minus(start_row) as usize].line;
+                let cursor_row_layout = &line_layouts[point.row().minus(start_row) as usize];
                 let x = cursor_row_layout.x_for_index(point.column() as usize)
                     - scroll_pixel_position.x;
                 let y = point.row().next_row().as_f32() * line_height - scroll_pixel_position.y;
@@ -2406,7 +2359,7 @@ impl EditorElement {
 
         // This is safe because we check on layout whether the required row is available
         let hovered_row_layout =
-            &line_layouts[position.row().minus(visible_display_row_range.start) as usize].line;
+            &line_layouts[position.row().minus(visible_display_row_range.start) as usize];
 
         // Compute Hovered Point
         let x =
@@ -2920,7 +2873,6 @@ impl EditorElement {
                 };
                 cx.set_cursor_style(cursor_style, &layout.text_hitbox);
 
-                cx.with_element_namespace("folds", |cx| self.paint_folds(layout, cx));
                 let invisible_display_ranges = self.paint_highlights(layout, cx);
                 self.paint_lines(&invisible_display_ranges, layout, cx);
                 self.paint_redactions(layout, cx);
@@ -2979,7 +2931,7 @@ impl EditorElement {
     fn paint_lines(
         &mut self,
         invisible_display_ranges: &[Range<DisplayPoint>],
-        layout: &EditorLayout,
+        layout: &mut EditorLayout,
         cx: &mut WindowContext,
     ) {
         let whitespace_setting = self
@@ -3001,6 +2953,10 @@ impl EditorElement {
                 cx,
             )
         }
+
+        for line_element in &mut layout.line_elements {
+            line_element.paint(cx);
+        }
     }
 
     fn paint_redactions(&mut self, layout: &EditorLayout, cx: &mut WindowContext) {
@@ -3378,7 +3334,7 @@ impl EditorElement {
                     .iter_rows()
                     .map(|row| {
                         let line_layout =
-                            &layout.position_map.line_layouts[row.minus(start_row) as usize].line;
+                            &layout.position_map.line_layouts[row.minus(start_row) as usize];
                         HighlightedRangeLine {
                             start_x: if row == range.start.row() {
                                 layout.content_origin.x
@@ -3405,37 +3361,6 @@ impl EditorElement {
         }
     }
 
-    fn paint_folds(&mut self, layout: &mut EditorLayout, cx: &mut WindowContext) {
-        if layout.folds.is_empty() {
-            return;
-        }
-
-        cx.paint_layer(layout.text_hitbox.bounds, |cx| {
-            let fold_corner_radius = 0.15 * layout.position_map.line_height;
-            for mut fold in mem::take(&mut layout.folds) {
-                fold.hover_element.paint(cx);
-
-                let hover_element = fold.hover_element.downcast_mut::<Stateful<Div>>().unwrap();
-                let fold_background = if hover_element.interactivity().active.unwrap() {
-                    cx.theme().colors().ghost_element_active
-                } else if hover_element.interactivity().hovered.unwrap() {
-                    cx.theme().colors().ghost_element_hover
-                } else {
-                    cx.theme().colors().ghost_element_background
-                };
-
-                self.paint_highlighted_range(
-                    fold.display_range.clone(),
-                    fold_background,
-                    fold_corner_radius,
-                    fold_corner_radius * 2.,
-                    layout,
-                    cx,
-                );
-            }
-        })
-    }
-
     fn paint_inline_blame(&mut self, layout: &mut EditorLayout, cx: &mut WindowContext) {
         if let Some(mut inline_blame) = layout.inline_blame.take() {
             cx.paint_layer(layout.text_hitbox.bounds, |cx| {
@@ -3813,8 +3738,34 @@ fn deploy_blame_entry_context_menu(
 
 #[derive(Debug)]
 pub(crate) struct LineWithInvisibles {
-    pub line: ShapedLine,
+    fragments: SmallVec<[LineFragment; 1]>,
     invisibles: Vec<Invisible>,
+    len: usize,
+    width: Pixels,
+    font_size: Pixels,
+}
+
+#[allow(clippy::large_enum_variant)]
+enum LineFragment {
+    Text(ShapedLine),
+    Element {
+        element: Option<AnyElement>,
+        width: Pixels,
+        len: usize,
+    },
+}
+
+impl fmt::Debug for LineFragment {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        match self {
+            LineFragment::Text(shaped_line) => f.debug_tuple("Text").field(shaped_line).finish(),
+            LineFragment::Element { width, len, .. } => f
+                .debug_struct("Element")
+                .field("width", width)
+                .field("len", len)
+                .finish(),
+        }
+    }
 }
 
 impl LineWithInvisibles {
@@ -3825,100 +3776,161 @@ impl LineWithInvisibles {
         max_line_count: usize,
         line_number_layouts: &[Option<ShapedLine>],
         editor_mode: EditorMode,
-        cx: &WindowContext,
+        cx: &mut WindowContext,
     ) -> Vec<Self> {
         let mut layouts = Vec::with_capacity(max_line_count);
+        let mut fragments: SmallVec<[LineFragment; 1]> = SmallVec::new();
         let mut line = String::new();
         let mut invisibles = Vec::new();
+        let mut width = Pixels::ZERO;
+        let mut len = 0;
         let mut styles = Vec::new();
         let mut non_whitespace_added = false;
         let mut row = 0;
         let mut line_exceeded_max_len = false;
         let font_size = text_style.font_size.to_pixels(cx.rem_size());
 
+        let ellipsis = SharedString::from("⋯");
+
         for highlighted_chunk in chunks.chain([HighlightedChunk {
-            chunk: "\n",
+            text: "\n",
             style: None,
             is_tab: false,
+            renderer: None,
         }]) {
-            for (ix, mut line_chunk) in highlighted_chunk.chunk.split('\n').enumerate() {
-                if ix > 0 {
+            if let Some(renderer) = highlighted_chunk.renderer {
+                if !line.is_empty() {
                     let shaped_line = cx
                         .text_system()
                         .shape_line(line.clone().into(), font_size, &styles)
                         .unwrap();
-                    layouts.push(Self {
-                        line: shaped_line,
-                        invisibles: std::mem::take(&mut invisibles),
-                    });
-
+                    width += shaped_line.width;
+                    len += shaped_line.len;
+                    fragments.push(LineFragment::Text(shaped_line));
                     line.clear();
                     styles.clear();
-                    row += 1;
-                    line_exceeded_max_len = false;
-                    non_whitespace_added = false;
-                    if row == max_line_count {
-                        return layouts;
-                    }
                 }
 
-                if !line_chunk.is_empty() && !line_exceeded_max_len {
-                    let text_style = if let Some(style) = highlighted_chunk.style {
-                        Cow::Owned(text_style.clone().highlight(style))
+                let available_width = if renderer.constrain_width {
+                    let chunk = if highlighted_chunk.text == ellipsis.as_ref() {
+                        ellipsis.clone()
                     } else {
-                        Cow::Borrowed(text_style)
+                        SharedString::from(Arc::from(highlighted_chunk.text))
                     };
+                    let shaped_line = cx
+                        .text_system()
+                        .shape_line(
+                            chunk,
+                            font_size,
+                            &[text_style.to_run(highlighted_chunk.text.len())],
+                        )
+                        .unwrap();
+                    AvailableSpace::Definite(shaped_line.width)
+                } else {
+                    AvailableSpace::MinContent
+                };
+
+                let mut element = (renderer.render)(cx);
+                let line_height = text_style.line_height_in_pixels(cx.rem_size());
+                let size = element.layout_as_root(
+                    size(available_width, AvailableSpace::Definite(line_height)),
+                    cx,
+                );
+
+                width += size.width;
+                len += highlighted_chunk.text.len();
+                fragments.push(LineFragment::Element {
+                    element: Some(element),
+                    width: size.width,
+                    len: highlighted_chunk.text.len(),
+                });
+            } else {
+                for (ix, mut line_chunk) in highlighted_chunk.text.split('\n').enumerate() {
+                    if ix > 0 {
+                        let shaped_line = cx
+                            .text_system()
+                            .shape_line(line.clone().into(), font_size, &styles)
+                            .unwrap();
+                        width += shaped_line.width;
+                        len += shaped_line.len;
+                        fragments.push(LineFragment::Text(shaped_line));
+                        layouts.push(Self {
+                            width: mem::take(&mut width),
+                            len: mem::take(&mut len),
+                            fragments: mem::take(&mut fragments),
+                            invisibles: std::mem::take(&mut invisibles),
+                            font_size,
+                        });
 
-                    if line.len() + line_chunk.len() > max_line_len {
-                        let mut chunk_len = max_line_len - line.len();
-                        while !line_chunk.is_char_boundary(chunk_len) {
-                            chunk_len -= 1;
+                        line.clear();
+                        styles.clear();
+                        row += 1;
+                        line_exceeded_max_len = false;
+                        non_whitespace_added = false;
+                        if row == max_line_count {
+                            return layouts;
                         }
-                        line_chunk = &line_chunk[..chunk_len];
-                        line_exceeded_max_len = true;
                     }
 
-                    styles.push(TextRun {
-                        len: line_chunk.len(),
-                        font: text_style.font(),
-                        color: text_style.color,
-                        background_color: text_style.background_color,
-                        underline: text_style.underline,
-                        strikethrough: text_style.strikethrough,
-                    });
+                    if !line_chunk.is_empty() && !line_exceeded_max_len {
+                        let text_style = if let Some(style) = highlighted_chunk.style {
+                            Cow::Owned(text_style.clone().highlight(style))
+                        } else {
+                            Cow::Borrowed(text_style)
+                        };
 
-                    if editor_mode == EditorMode::Full {
-                        // Line wrap pads its contents with fake whitespaces,
-                        // avoid printing them
-                        let inside_wrapped_string = line_number_layouts
-                            .get(row)
-                            .and_then(|layout| layout.as_ref())
-                            .is_none();
-                        if highlighted_chunk.is_tab {
-                            if non_whitespace_added || !inside_wrapped_string {
-                                invisibles.push(Invisible::Tab {
-                                    line_start_offset: line.len(),
-                                });
+                        if line.len() + line_chunk.len() > max_line_len {
+                            let mut chunk_len = max_line_len - line.len();
+                            while !line_chunk.is_char_boundary(chunk_len) {
+                                chunk_len -= 1;
+                            }
+                            line_chunk = &line_chunk[..chunk_len];
+                            line_exceeded_max_len = true;
+                        }
+
+                        styles.push(TextRun {
+                            len: line_chunk.len(),
+                            font: text_style.font(),
+                            color: text_style.color,
+                            background_color: text_style.background_color,
+                            underline: text_style.underline,
+                            strikethrough: text_style.strikethrough,
+                        });
+
+                        if editor_mode == EditorMode::Full {
+                            // Line wrap pads its contents with fake whitespaces,
+                            // avoid printing them
+                            let inside_wrapped_string = line_number_layouts
+                                .get(row)
+                                .and_then(|layout| layout.as_ref())
+                                .is_none();
+                            if highlighted_chunk.is_tab {
+                                if non_whitespace_added || !inside_wrapped_string {
+                                    invisibles.push(Invisible::Tab {
+                                        line_start_offset: line.len(),
+                                    });
+                                }
+                            } else {
+                                invisibles.extend(
+                                    line_chunk
+                                        .bytes()
+                                        .enumerate()
+                                        .filter(|(_, line_byte)| {
+                                            let is_whitespace =
+                                                (*line_byte as char).is_whitespace();
+                                            non_whitespace_added |= !is_whitespace;
+                                            is_whitespace
+                                                && (non_whitespace_added || !inside_wrapped_string)
+                                        })
+                                        .map(|(whitespace_index, _)| Invisible::Whitespace {
+                                            line_offset: line.len() + whitespace_index,
+                                        }),
+                                )
                             }
-                        } else {
-                            invisibles.extend(
-                                line_chunk
-                                    .bytes()
-                                    .enumerate()
-                                    .filter(|(_, line_char)| {
-                                        let is_whitespace = (*line_char as char).is_whitespace();
-                                        non_whitespace_added |= !is_whitespace;
-                                        is_whitespace
-                                            && (non_whitespace_added || !inside_wrapped_string)
-                                    })
-                                    .map(|(whitespace_index, _)| Invisible::Whitespace {
-                                        line_offset: line.len() + whitespace_index,
-                                    }),
-                            )
                         }
-                    }
 
-                    line.push_str(line_chunk);
+                        line.push_str(line_chunk);
+                    }
                 }
             }
         }
@@ -3926,6 +3938,34 @@ impl LineWithInvisibles {
         layouts
     }
 
+    fn prepaint(
+        &mut self,
+        line_height: Pixels,
+        scroll_pixel_position: gpui::Point<Pixels>,
+        row: DisplayRow,
+        content_origin: gpui::Point<Pixels>,
+        line_elements: &mut SmallVec<[AnyElement; 1]>,
+        cx: &mut WindowContext,
+    ) {
+        let line_y = line_height * (row.as_f32() - scroll_pixel_position.y / line_height);
+        let mut fragment_origin = content_origin + gpui::point(-scroll_pixel_position.x, line_y);
+        for fragment in &mut self.fragments {
+            match fragment {
+                LineFragment::Text(line) => {
+                    fragment_origin.x += line.width;
+                }
+                LineFragment::Element { element, width, .. } => {
+                    let mut element = element
+                        .take()
+                        .expect("you can't prepaint LineWithInvisibles twice");
+                    element.prepaint_at(fragment_origin, cx);
+                    line_elements.push(element);
+                    fragment_origin.x += *width;
+                }
+            }
+        }
+    }
+
     fn draw(
         &self,
         layout: &EditorLayout,
@@ -3939,9 +3979,20 @@ impl LineWithInvisibles {
         let line_y = line_height
             * (row.as_f32() - layout.position_map.scroll_pixel_position.y / line_height);
 
-        let line_origin =
+        let mut fragment_origin =
             content_origin + gpui::point(-layout.position_map.scroll_pixel_position.x, line_y);
-        self.line.paint(line_origin, line_height, cx).log_err();
+
+        for fragment in &self.fragments {
+            match fragment {
+                LineFragment::Text(line) => {
+                    line.paint(fragment_origin, line_height, cx).log_err();
+                    fragment_origin.x += line.width;
+                }
+                LineFragment::Element { width, .. } => {
+                    fragment_origin.x += *width;
+                }
+            }
+        }
 
         self.draw_invisibles(
             &selection_ranges,
@@ -3979,7 +4030,7 @@ impl LineWithInvisibles {
                 Invisible::Whitespace { line_offset } => (line_offset, &layout.space_invisible),
             };
 
-            let x_offset = self.line.x_for_index(token_offset);
+            let x_offset = self.x_for_index(token_offset);
             let invisible_offset =
                 (layout.position_map.em_width - invisible_symbol.width).max(Pixels::ZERO) / 2.0;
             let origin = content_origin
@@ -4000,6 +4051,90 @@ impl LineWithInvisibles {
             invisible_symbol.paint(origin, line_height, cx).log_err();
         }
     }
+
+    pub fn x_for_index(&self, index: usize) -> Pixels {
+        let mut fragment_start_x = Pixels::ZERO;
+        let mut fragment_start_index = 0;
+
+        for fragment in &self.fragments {
+            match fragment {
+                LineFragment::Text(shaped_line) => {
+                    let fragment_end_index = fragment_start_index + shaped_line.len;
+                    if index < fragment_end_index {
+                        return fragment_start_x
+                            + shaped_line.x_for_index(index - fragment_start_index);
+                    }
+                    fragment_start_x += shaped_line.width;
+                    fragment_start_index = fragment_end_index;
+                }
+                LineFragment::Element { len, width, .. } => {
+                    let fragment_end_index = fragment_start_index + len;
+                    if index < fragment_end_index {
+                        return fragment_start_x;
+                    }
+                    fragment_start_x += *width;
+                    fragment_start_index = fragment_end_index;
+                }
+            }
+        }
+
+        fragment_start_x
+    }
+
+    pub fn index_for_x(&self, x: Pixels) -> Option<usize> {
+        let mut fragment_start_x = Pixels::ZERO;
+        let mut fragment_start_index = 0;
+
+        for fragment in &self.fragments {
+            match fragment {
+                LineFragment::Text(shaped_line) => {
+                    let fragment_end_x = fragment_start_x + shaped_line.width;
+                    if x < fragment_end_x {
+                        return Some(
+                            fragment_start_index + shaped_line.index_for_x(x - fragment_start_x)?,
+                        );
+                    }
+                    fragment_start_x = fragment_end_x;
+                    fragment_start_index += shaped_line.len;
+                }
+                LineFragment::Element { len, width, .. } => {
+                    let fragment_end_x = fragment_start_x + *width;
+                    if x < fragment_end_x {
+                        return Some(fragment_start_index);
+                    }
+                    fragment_start_index += len;
+                    fragment_start_x = fragment_end_x;
+                }
+            }
+        }
+
+        None
+    }
+
+    pub fn font_id_for_index(&self, index: usize) -> Option<FontId> {
+        let mut fragment_start_index = 0;
+
+        for fragment in &self.fragments {
+            match fragment {
+                LineFragment::Text(shaped_line) => {
+                    let fragment_end_index = fragment_start_index + shaped_line.len;
+                    if index < fragment_end_index {
+                        return shaped_line.font_id_for_index(index - fragment_start_index);
+                    }
+                    fragment_start_index = fragment_end_index;
+                }
+                LineFragment::Element { len, .. } => {
+                    let fragment_end_index = fragment_start_index + len;
+                    if index < fragment_end_index {
+                        return None;
+                    }
+                    fragment_start_index = fragment_end_index;
+                }
+            }
+        }
+
+        None
+    }
 }
 
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -4309,18 +4444,16 @@ impl Element for EditorElement {
                     );
 
                     let mut max_visible_line_width = Pixels::ZERO;
-                    let line_layouts =
+                    let mut line_layouts =
                         self.layout_lines(start_row..end_row, &line_numbers, &snapshot, cx);
                     for line_with_invisibles in &line_layouts {
-                        if line_with_invisibles.line.width > max_visible_line_width {
-                            max_visible_line_width = line_with_invisibles.line.width;
+                        if line_with_invisibles.width > max_visible_line_width {
+                            max_visible_line_width = line_with_invisibles.width;
                         }
                     }
 
                     let longest_line_width =
-                        layout_line(snapshot.longest_row(), &snapshot, &style, cx)
-                            .unwrap()
-                            .width;
+                        layout_line(snapshot.longest_row(), &snapshot, &style, cx).width;
                     let mut scroll_width =
                         longest_line_width.max(max_visible_line_width) + overscroll.width;
 
@@ -4430,6 +4563,15 @@ impl Element for EditorElement {
                         }
                     });
 
+                    let line_elements = self.prepaint_lines(
+                        start_row,
+                        &mut line_layouts,
+                        line_height,
+                        scroll_pixel_position,
+                        content_origin,
+                        cx,
+                    );
+
                     cx.with_element_namespace("blocks", |cx| {
                         self.layout_blocks(
                             &mut blocks,
@@ -4470,19 +4612,6 @@ impl Element for EditorElement {
                         cx,
                     );
 
-                    let folds = cx.with_element_namespace("folds", |cx| {
-                        self.layout_folds(
-                            &snapshot,
-                            content_origin,
-                            start_anchor..end_anchor,
-                            start_row..end_row,
-                            scroll_pixel_position,
-                            line_height,
-                            &line_layouts,
-                            cx,
-                        )
-                    });
-
                     let gutter_settings = EditorSettings::get_global(cx).gutter;
 
                     let mut context_menu_visible = false;
@@ -4628,11 +4757,11 @@ impl Element for EditorElement {
                         highlighted_rows,
                         highlighted_ranges,
                         redacted_ranges,
+                        line_elements,
                         line_numbers,
                         display_hunks,
                         blamed_display_rows,
                         inline_blame,
-                        folds,
                         blocks,
                         cursors,
                         visible_cursors,
@@ -4750,11 +4879,11 @@ pub struct EditorLayout {
     visible_display_row_range: Range<DisplayRow>,
     active_rows: BTreeMap<DisplayRow, bool>,
     highlighted_rows: BTreeMap<DisplayRow, Hsla>,
+    line_elements: SmallVec<[AnyElement; 1]>,
     line_numbers: Vec<Option<ShapedLine>>,
     display_hunks: Vec<(DisplayDiffHunk, Option<Hitbox>)>,
     blamed_display_rows: Option<Vec<AnyElement>>,
     inline_blame: Option<AnyElement>,
-    folds: Vec<FoldLayout>,
     blocks: Vec<BlockLayout>,
     highlighted_ranges: Vec<(Range<DisplayPoint>, Hsla)>,
     redacted_ranges: Vec<Range<DisplayPoint>>,
@@ -4893,11 +5022,6 @@ struct FlapTrailerLayout {
     bounds: Bounds<Pixels>,
 }
 
-struct FoldLayout {
-    display_range: Range<DisplayPoint>,
-    hover_element: AnyElement,
-}
-
 struct PositionMap {
     size: Size<Pixels>,
     line_height: Pixels,
@@ -4942,7 +5066,6 @@ impl PositionMap {
         let (column, x_overshoot_after_line_end) = if let Some(line) = self
             .line_layouts
             .get(row as usize - scroll_position.y as usize)
-            .map(|LineWithInvisibles { line, .. }| line)
         {
             if let Some(ix) = line.index_for_x(x) {
                 (ix as u32, px(0.))
@@ -4979,37 +5102,12 @@ fn layout_line(
     row: DisplayRow,
     snapshot: &EditorSnapshot,
     style: &EditorStyle,
-    cx: &WindowContext,
-) -> Result<ShapedLine> {
-    let mut line = snapshot.line(row);
-
-    let len = {
-        let line_len = line.len();
-        if line_len > MAX_LINE_LEN {
-            let mut len = MAX_LINE_LEN;
-            while !line.is_char_boundary(len) {
-                len -= 1;
-            }
-
-            line.truncate(len);
-            len
-        } else {
-            line_len
-        }
-    };
-
-    cx.text_system().shape_line(
-        line.into(),
-        style.text.font_size.to_pixels(cx.rem_size()),
-        &[TextRun {
-            len,
-            font: style.text.font(),
-            color: Hsla::default(),
-            background_color: None,
-            underline: None,
-            strikethrough: None,
-        }],
-    )
+    cx: &mut WindowContext,
+) -> LineWithInvisibles {
+    let chunks = snapshot.highlighted_chunks(row..row + DisplayRow(1), true, style);
+    LineWithInvisibles::from_chunks(chunks, &style.text, MAX_LINE_LEN, 1, &[], snapshot.mode, cx)
+        .pop()
+        .unwrap()
 }
 
 #[derive(Debug)]

crates/editor/src/movement.rs 🔗

@@ -577,7 +577,7 @@ mod tests {
     use crate::{
         display_map::Inlay,
         test::{editor_test_context::EditorTestContext, marked_display_snapshot},
-        Buffer, DisplayMap, DisplayRow, ExcerptRange, InlayId, MultiBuffer,
+        Buffer, DisplayMap, DisplayRow, ExcerptRange, FoldPlaceholder, InlayId, MultiBuffer,
     };
     use gpui::{font, Context as _};
     use language::Capability;
@@ -695,8 +695,18 @@ mod tests {
         let font_size = px(14.0);
         let buffer = MultiBuffer::build_simple(input_text, cx);
         let buffer_snapshot = buffer.read(cx).snapshot(cx);
-        let display_map =
-            cx.new_model(|cx| DisplayMap::new(buffer, font, font_size, None, 1, 1, cx));
+        let display_map = cx.new_model(|cx| {
+            DisplayMap::new(
+                buffer,
+                font,
+                font_size,
+                None,
+                1,
+                1,
+                FoldPlaceholder::test(),
+                cx,
+            )
+        });
 
         // add all kinds of inlays between two word boundaries: we should be able to cross them all, when looking for another boundary
         let mut id = 0;
@@ -901,8 +911,18 @@ mod tests {
                 );
                 multibuffer
             });
-            let display_map =
-                cx.new_model(|cx| DisplayMap::new(multibuffer, font, px(14.0), None, 2, 2, cx));
+            let display_map = cx.new_model(|cx| {
+                DisplayMap::new(
+                    multibuffer,
+                    font,
+                    px(14.0),
+                    None,
+                    2,
+                    2,
+                    FoldPlaceholder::test(),
+                    cx,
+                )
+            });
             let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
 
             assert_eq!(snapshot.text(), "\n\nabc\ndefg\n\n\nhijkl\nmn");

crates/editor/src/scroll/autoscroll.rs 🔗

@@ -251,12 +251,10 @@ impl Editor {
                     let end_column = cmp::min(display_map.line_len(head.row()), head.column() + 3);
                     target_left = target_left.min(
                         layouts[head.row().minus(start_row) as usize]
-                            .line
                             .x_for_index(start_column as usize),
                     );
                     target_right = target_right.max(
                         layouts[head.row().minus(start_row) as usize]
-                            .line
                             .x_for_index(end_column as usize)
                             + max_glyph_width,
                     );

crates/editor/src/test.rs 🔗

@@ -3,11 +3,9 @@ pub mod editor_test_context;
 
 use crate::{
     display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint},
-    DisplayPoint, Editor, EditorMode, MultiBuffer,
+    DisplayPoint, Editor, EditorMode, FoldPlaceholder, MultiBuffer,
 };
-
 use gpui::{Context, Font, FontFeatures, FontStyle, FontWeight, Model, Pixels, ViewContext};
-
 use project::Project;
 use util::test::{marked_text_offsets, marked_text_ranges};
 
@@ -35,7 +33,18 @@ pub fn marked_display_snapshot(
     let font_size: Pixels = 14usize.into();
 
     let buffer = MultiBuffer::build_simple(&unmarked_text, cx);
-    let display_map = cx.new_model(|cx| DisplayMap::new(buffer, font, font_size, None, 1, 1, cx));
+    let display_map = cx.new_model(|cx| {
+        DisplayMap::new(
+            buffer,
+            font,
+            font_size,
+            None,
+            1,
+            1,
+            FoldPlaceholder::test(),
+            cx,
+        )
+    });
     let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
     let markers = markers
         .into_iter()

crates/gpui/src/elements/text.rs 🔗

@@ -143,7 +143,7 @@ impl StyledText {
         }
     }
 
-    /// todo!()
+    /// Get the layout for this element. This can be used to map indices to pixels and vice versa.
     pub fn layout(&self) -> &TextLayout {
         &self.layout
     }
@@ -232,7 +232,7 @@ impl IntoElement for StyledText {
     }
 }
 
-/// todo!()
+/// The Layout for TextElement. This can be used to map indices to pixels and vice versa.
 #[derive(Default, Clone)]
 pub struct TextLayout(Arc<Mutex<Option<TextLayoutInner>>>);
 
@@ -358,7 +358,7 @@ impl TextLayout {
         }
     }
 
-    /// todo!()
+    /// Get the byte index into the input of the pixel position.
     pub fn index_for_position(&self, mut position: Point<Pixels>) -> Result<usize, usize> {
         let element_state = self.lock();
         let element_state = element_state
@@ -392,7 +392,7 @@ impl TextLayout {
         Err(line_start_ix.saturating_sub(1))
     }
 
-    /// todo!()
+    /// Get the pixel position for the given byte index.
     pub fn position_for_index(&self, index: usize) -> Option<Point<Pixels>> {
         let element_state = self.lock();
         let element_state = element_state
@@ -423,17 +423,17 @@ impl TextLayout {
         None
     }
 
-    /// todo!()
+    /// The bounds of this layout.
     pub fn bounds(&self) -> Bounds<Pixels> {
         self.0.lock().as_ref().unwrap().bounds.unwrap()
     }
 
-    /// todo!()
+    /// The line height for this layout.
     pub fn line_height(&self) -> Pixels {
         self.0.lock().as_ref().unwrap().line_height
     }
 
-    /// todo!()
+    /// The text for this layout.
     pub fn text(&self) -> String {
         self.0
             .lock()

crates/gpui/src/text_system/line_layout.rs 🔗

@@ -302,7 +302,7 @@ impl WrappedLineLayout {
         }
     }
 
-    /// todo!()
+    /// Returns the pixel position for the given byte index.
     pub fn position_for_index(&self, index: usize, line_height: Pixels) -> Option<Point<Pixels>> {
         let mut line_start_ix = 0;
         let mut line_end_indices = self

crates/language/src/buffer.rs 🔗

@@ -19,7 +19,10 @@ use crate::{
 use anyhow::{anyhow, Context, Result};
 pub use clock::ReplicaId;
 use futures::channel::oneshot;
-use gpui::{AppContext, EventEmitter, HighlightStyle, ModelContext, Task, TaskLabel};
+use gpui::{
+    AnyElement, AppContext, EventEmitter, HighlightStyle, ModelContext, Task, TaskLabel,
+    WindowContext,
+};
 use lazy_static::lazy_static;
 use lsp::LanguageServerId;
 use parking_lot::Mutex;
@@ -31,6 +34,7 @@ use std::{
     cmp::{self, Ordering},
     collections::BTreeMap,
     ffi::OsStr,
+    fmt,
     future::Future,
     iter::{self, Iterator, Peekable},
     mem,
@@ -461,7 +465,7 @@ pub struct BufferChunks<'a> {
 
 /// A chunk of a buffer's text, along with its syntax highlight and
 /// diagnostic status.
-#[derive(Clone, Copy, Debug, Default)]
+#[derive(Clone, Debug, Default)]
 pub struct Chunk<'a> {
     /// The text of the chunk.
     pub text: &'a str,
@@ -476,6 +480,25 @@ pub struct Chunk<'a> {
     pub is_unnecessary: bool,
     /// Whether this chunk of text was originally a tab character.
     pub is_tab: bool,
+    /// An optional recipe for how the chunk should be presented.
+    pub renderer: Option<ChunkRenderer>,
+}
+
+/// A recipe for how the chunk should be presented.
+#[derive(Clone)]
+pub struct ChunkRenderer {
+    /// creates a custom element to represent this chunk.
+    pub render: Arc<dyn Send + Sync + Fn(&mut WindowContext) -> AnyElement>,
+    /// If true, the element is constrained to the shaped width of the text.
+    pub constrain_width: bool,
+}
+
+impl fmt::Debug for ChunkRenderer {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        f.debug_struct("ChunkRenderer")
+            .field("constrain_width", &self.constrain_width)
+            .finish()
+    }
 }
 
 /// A set of edits to a given version of a buffer, computed asynchronously.