Add is_tab field to chunks

Max Brunsfeld and Antonio Scandurra created

Co-authored-by: Antonio Scandurra <antonio@zed.dev>

Change summary

crates/editor/src/display_map/block_map.rs      |  5 -
crates/editor/src/display_map/fold_map.rs       |  4 -
crates/editor/src/display_map/suggestion_map.rs |  4 -
crates/editor/src/display_map/tab_map.rs        | 52 +++++++++++++++++++
crates/language/src/buffer.rs                   |  3 
5 files changed, 57 insertions(+), 11 deletions(-)

Detailed changes

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

@@ -833,10 +833,7 @@ impl<'a> Iterator for BlockChunks<'a> {
 
             return Some(Chunk {
                 text: unsafe { std::str::from_utf8_unchecked(&NEWLINES[..line_count as usize]) },
-                syntax_highlight_id: None,
-                highlight_style: None,
-                diagnostic_severity: None,
-                is_unnecessary: false,
+                ..Default::default()
             });
         }
 

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

@@ -1065,13 +1065,11 @@ impl<'a> Iterator for FoldChunks<'a> {
             self.output_offset += output_text.len();
             return Some(Chunk {
                 text: output_text,
-                syntax_highlight_id: None,
                 highlight_style: self.ellipses_color.map(|color| HighlightStyle {
                     color: Some(color),
                     ..Default::default()
                 }),
-                diagnostic_severity: None,
-                is_unnecessary: false,
+                ..Default::default()
             });
         }
 

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

@@ -531,10 +531,8 @@ impl<'a> Iterator for SuggestionChunks<'a> {
             if let Some(chunk) = chunks.next() {
                 return Some(Chunk {
                     text: chunk,
-                    syntax_highlight_id: None,
                     highlight_style: self.highlight_style,
-                    diagnostic_severity: None,
-                    is_unnecessary: false,
+                    ..Default::default()
                 });
             } else {
                 self.suggestion_chunks = None;

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

@@ -268,6 +268,7 @@ impl TabSnapshot {
             tab_size: self.tab_size,
             chunk: Chunk {
                 text: &SPACES[0..(to_next_stop as usize)],
+                is_tab: true,
                 ..Default::default()
             },
             inside_leading_tab: to_next_stop > 0,
@@ -545,6 +546,7 @@ impl<'a> Iterator for TabChunks<'a> {
                         self.output_position = next_output_position;
                         return Some(Chunk {
                             text: &SPACES[..len as usize],
+                            is_tab: true,
                             ..self.chunk
                         });
                     }
@@ -654,6 +656,56 @@ mod tests {
         assert_eq!(tab_snapshot.text(), input);
     }
 
+    #[gpui::test]
+    fn test_marking_tabs(cx: &mut gpui::AppContext) {
+        let input = "\t \thello";
+
+        let buffer = MultiBuffer::build_simple(&input, cx);
+        let buffer_snapshot = buffer.read(cx).snapshot(cx);
+        let (_, fold_snapshot) = FoldMap::new(buffer_snapshot.clone());
+        let (_, suggestion_snapshot) = SuggestionMap::new(fold_snapshot);
+        let (_, tab_snapshot) = TabMap::new(suggestion_snapshot, 4.try_into().unwrap());
+
+        assert_eq!(
+            chunks(&tab_snapshot, TabPoint::zero()),
+            vec![
+                ("    ".to_string(), true),
+                (" ".to_string(), false),
+                ("   ".to_string(), true),
+                ("hello".to_string(), false),
+            ]
+        );
+        assert_eq!(
+            chunks(&tab_snapshot, TabPoint::new(0, 2)),
+            vec![
+                ("  ".to_string(), true),
+                (" ".to_string(), false),
+                ("   ".to_string(), true),
+                ("hello".to_string(), false),
+            ]
+        );
+
+        fn chunks(snapshot: &TabSnapshot, start: TabPoint) -> Vec<(String, bool)> {
+            let mut chunks = Vec::new();
+            let mut was_tab = false;
+            let mut text = String::new();
+            for chunk in snapshot.chunks(start..snapshot.max_point(), false, None, None) {
+                if chunk.is_tab != was_tab {
+                    if !text.is_empty() {
+                        chunks.push((mem::take(&mut text), was_tab));
+                    }
+                    was_tab = chunk.is_tab;
+                }
+                text.push_str(chunk.text);
+            }
+
+            if !text.is_empty() {
+                chunks.push((text, was_tab));
+            }
+            chunks
+        }
+    }
+
     #[gpui::test(iterations = 100)]
     fn test_random_tabs(cx: &mut gpui::AppContext, mut rng: StdRng) {
         let tab_size = NonZeroU32::new(rng.gen_range(1..=4)).unwrap();

crates/language/src/buffer.rs 🔗

@@ -311,6 +311,7 @@ pub struct Chunk<'a> {
     pub highlight_style: Option<HighlightStyle>,
     pub diagnostic_severity: Option<DiagnosticSeverity>,
     pub is_unnecessary: bool,
+    pub is_tab: bool,
 }
 
 pub struct Diff {
@@ -2840,9 +2841,9 @@ impl<'a> Iterator for BufferChunks<'a> {
             Some(Chunk {
                 text: slice,
                 syntax_highlight_id: highlight_id,
-                highlight_style: None,
                 diagnostic_severity: self.current_diagnostic_severity(),
                 is_unnecessary: self.current_code_is_unnecessary(),
+                ..Default::default()
             })
         } else {
             None