Draft invisibles' tabs display

Kirill Bulatov created

Change summary

crates/editor/src/element.rs     |  6 +++
crates/gpui/src/elements/text.rs | 45 +++++++++++++++++++++++++++------
crates/gpui/src/text_layout.rs   | 35 ++++++++++++++++++++++++-
3 files changed, 75 insertions(+), 11 deletions(-)

Detailed changes

crates/editor/src/element.rs 🔗

@@ -1359,7 +1359,11 @@ impl EditorElement {
                         highlight_style = Some(diagnostic_highlight);
                     }
 
-                    (chunk.text, highlight_style)
+                    HighlightedChunk {
+                        chunk: chunk.text,
+                        style: highlight_style,
+                        is_tab: chunk.is_tab,
+                    }
                 });
             layout_highlighted_chunks(
                 chunks,

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

@@ -6,7 +6,7 @@ use crate::{
         vector::{vec2f, Vector2F},
     },
     json::{ToJson, Value},
-    text_layout::{Line, RunStyle, ShapedBoundary},
+    text_layout::{Invisible, Line, RunStyle, ShapedBoundary},
     AppContext, Element, FontCache, LayoutContext, SceneBuilder, SizeConstraint, TextLayoutCache,
     View, ViewContext,
 };
@@ -114,7 +114,11 @@ impl<V: View> Element<V> for Text {
             } else {
                 result = None;
             }
-            result
+            result.map(|(chunk, style)| HighlightedChunk {
+                chunk,
+                style,
+                is_tab: false,
+            })
         });
 
         // Perform shaping on these highlighted chunks
@@ -337,9 +341,25 @@ impl<V: View> Element<V> for Text {
     }
 }
 
+pub struct HighlightedChunk<'a> {
+    pub chunk: &'a str,
+    pub style: Option<HighlightStyle>,
+    pub is_tab: bool,
+}
+
+impl<'a> HighlightedChunk<'a> {
+    fn plain_str(str_symbols: &'a str) -> Self {
+        Self {
+            chunk: str_symbols,
+            style: None,
+            is_tab: str_symbols == "\t",
+        }
+    }
+}
+
 /// Perform text layout on a series of highlighted chunks of text.
 pub fn layout_highlighted_chunks<'a>(
-    chunks: impl Iterator<Item = (&'a str, Option<HighlightStyle>)>,
+    chunks: impl Iterator<Item = HighlightedChunk<'a>>,
     text_style: &TextStyle,
     text_layout_cache: &TextLayoutCache,
     font_cache: &Arc<FontCache>,
@@ -348,13 +368,17 @@ pub fn layout_highlighted_chunks<'a>(
 ) -> Vec<Line> {
     let mut layouts = Vec::with_capacity(max_line_count);
     let mut line = String::new();
+    let mut invisibles = Vec::new();
     let mut styles = Vec::new();
     let mut row = 0;
     let mut line_exceeded_max_len = false;
-    for (chunk, highlight_style) in chunks.chain([("\n", Default::default())]) {
-        for (ix, mut line_chunk) in chunk.split('\n').enumerate() {
+    for highlighted_chunk in chunks.chain(std::iter::once(HighlightedChunk::plain_str("\n"))) {
+        for (ix, mut line_chunk) in highlighted_chunk.chunk.split('\n').enumerate() {
             if ix > 0 {
-                layouts.push(text_layout_cache.layout_str(&line, text_style.font_size, &styles));
+                let mut laid_out_line =
+                    text_layout_cache.layout_str(&line, text_style.font_size, &styles);
+                laid_out_line.invisibles.extend(invisibles.drain(..));
+                layouts.push(laid_out_line);
                 line.clear();
                 styles.clear();
                 row += 1;
@@ -365,7 +389,7 @@ pub fn layout_highlighted_chunks<'a>(
             }
 
             if !line_chunk.is_empty() && !line_exceeded_max_len {
-                let text_style = if let Some(style) = highlight_style {
+                let text_style = if let Some(style) = highlighted_chunk.style {
                     text_style
                         .clone()
                         .highlight(style, font_cache)
@@ -384,7 +408,6 @@ pub fn layout_highlighted_chunks<'a>(
                     line_exceeded_max_len = true;
                 }
 
-                line.push_str(line_chunk);
                 styles.push((
                     line_chunk.len(),
                     RunStyle {
@@ -393,6 +416,12 @@ pub fn layout_highlighted_chunks<'a>(
                         underline: text_style.underline,
                     },
                 ));
+                if highlighted_chunk.is_tab {
+                    invisibles.push(Invisible::Tab {
+                        range: line.len()..line.len() + line_chunk.len(),
+                    });
+                }
+                line.push_str(line_chunk);
             }
         }
     }

crates/gpui/src/text_layout.rs 🔗

@@ -11,6 +11,7 @@ use crate::{
     window::WindowContext,
     SceneBuilder,
 };
+use itertools::Itertools;
 use ordered_float::OrderedFloat;
 use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard};
 use smallvec::SmallVec;
@@ -178,6 +179,7 @@ impl<'a> Hash for CacheKeyRef<'a> {
 pub struct Line {
     layout: Arc<LineLayout>,
     style_runs: SmallVec<[StyleRun; 32]>,
+    pub invisibles: SmallVec<[Invisible; 32]>,
 }
 
 #[derive(Debug, Clone, Copy)]
@@ -211,6 +213,12 @@ pub struct Glyph {
     pub is_emoji: bool,
 }
 
+#[derive(Debug, Clone)]
+pub enum Invisible {
+    Tab { range: std::ops::Range<usize> },
+    Whitespace { range: std::ops::Range<usize> },
+}
+
 impl Line {
     fn new(layout: Arc<LineLayout>, runs: &[(usize, RunStyle)]) -> Self {
         let mut style_runs = SmallVec::new();
@@ -221,7 +229,11 @@ impl Line {
                 underline: style.underline,
             });
         }
-        Self { layout, style_runs }
+        Self {
+            layout,
+            style_runs,
+            invisibles: SmallVec::new(),
+        }
     }
 
     pub fn runs(&self) -> &[Run] {
@@ -298,6 +310,16 @@ impl Line {
         let mut color = Color::black();
         let mut underline = None;
 
+        let tab_ranges = self
+            .invisibles
+            .iter()
+            .filter_map(|invisible| match invisible {
+                Invisible::Tab { range } => Some(range),
+                Invisible::Whitespace { .. } => None,
+            })
+            .sorted_by(|tab_range_1, tab_range_2| tab_range_1.start.cmp(&tab_range_2.start))
+            .collect::<Vec<_>>();
+
         for run in &self.layout.runs {
             let max_glyph_width = cx
                 .font_cache
@@ -364,10 +386,19 @@ impl Line {
                         origin: glyph_origin,
                     });
                 } else {
+                    let id = if tab_ranges.iter().any(|tab_range| {
+                        tab_range.start <= glyph.index && glyph.index < tab_range.end
+                    }) {
+                        // TODO kb get a proper (cached) glyph
+                        glyph.id + 100
+                    } else {
+                        glyph.id
+                    };
+
                     scene.push_glyph(scene::Glyph {
                         font_id: run.font_id,
                         font_size: self.layout.font_size,
-                        id: glyph.id,
+                        id,
                         origin: glyph_origin,
                         color,
                     });