Extract tab expansion logic into `TabMap`

Antonio Scandurra and Nathan Sobo created

Co-Authored-By: Nathan Sobo <nathan@zed.dev>

Change summary

zed/src/editor/display_map.rs          | 270 ++-------------------
zed/src/editor/display_map/fold_map.rs |   2 
zed/src/editor/display_map/tab_map.rs  | 344 ++++++++++++++++++++++++++++
3 files changed, 371 insertions(+), 245 deletions(-)

Detailed changes

zed/src/editor/display_map.rs šŸ”—

@@ -1,19 +1,20 @@
 mod fold_map;
+mod tab_map;
 // mod wrap_map;
 
 use super::{buffer, Anchor, Bias, Buffer, Point, ToOffset, ToPoint};
-use crate::settings::StyleId;
 use fold_map::FoldMap;
 pub use fold_map::InputRows;
 use gpui::{AppContext, ModelHandle};
-use std::{mem, ops::Range};
+use std::ops::Range;
+use tab_map::TabMap;
 // use wrap_map::WrapMap;
 
 pub struct DisplayMap {
     buffer: ModelHandle<Buffer>,
     fold_map: FoldMap,
+    tab_map: TabMap,
     // wrap_map: WrapMap,
-    tab_size: usize,
 }
 
 impl DisplayMap {
@@ -21,23 +22,25 @@ impl DisplayMap {
         let fold_map = FoldMap::new(buffer.clone(), cx);
         let (snapshot, edits) = fold_map.read(cx);
         assert_eq!(edits.len(), 0);
+        let tab_map = TabMap::new(snapshot, tab_size);
         // TODO: take `wrap_width` as a parameter.
         // let config = { todo!() };
         // let wrap_map = WrapMap::new(snapshot, config, cx);
         DisplayMap {
             buffer,
             fold_map,
+            tab_map,
             // wrap_map,
-            tab_size,
         }
     }
 
     pub fn snapshot(&self, cx: &AppContext) -> DisplayMapSnapshot {
         let (folds_snapshot, edits) = self.fold_map.read(cx);
+        let (tabs_snapshot, edits) = self.tab_map.sync(folds_snapshot.clone(), edits);
         DisplayMapSnapshot {
             buffer_snapshot: self.buffer.read(cx).snapshot(),
             folds_snapshot,
-            tab_size: self.tab_size,
+            tabs_snapshot,
         }
     }
 
@@ -63,7 +66,7 @@ impl DisplayMap {
 pub struct DisplayMapSnapshot {
     buffer_snapshot: buffer::Snapshot,
     folds_snapshot: fold_map::Snapshot,
-    tab_size: usize,
+    tabs_snapshot: tab_map::Snapshot,
 }
 
 impl DisplayMapSnapshot {
@@ -72,35 +75,15 @@ impl DisplayMapSnapshot {
     }
 
     pub fn max_point(&self) -> DisplayPoint {
-        self.expand_tabs(self.folds_snapshot.max_point())
-    }
-
-    pub fn chunks_at(&self, point: DisplayPoint) -> Chunks {
-        let (point, expanded_char_column, to_next_stop) = self.collapse_tabs(point, Bias::Left);
-        let fold_chunks = self
-            .folds_snapshot
-            .chunks_at(self.folds_snapshot.to_output_offset(point));
-        Chunks {
-            fold_chunks,
-            column: expanded_char_column,
-            tab_size: self.tab_size,
-            chunk: &SPACES[0..to_next_stop],
-            skip_leading_tab: to_next_stop > 0,
-        }
+        DisplayPoint(self.tabs_snapshot.max_point())
     }
 
-    pub fn highlighted_chunks_for_rows(&mut self, rows: Range<u32>) -> HighlightedChunks {
-        let start = DisplayPoint::new(rows.start, 0);
-        let start = self.folds_snapshot.to_output_offset(start.0);
-        let end = DisplayPoint::new(rows.end, 0).min(self.max_point());
-        let end = self.folds_snapshot.to_output_offset(end.0);
-        HighlightedChunks {
-            fold_chunks: self.folds_snapshot.highlighted_chunks(start..end),
-            column: 0,
-            tab_size: self.tab_size,
-            chunk: "",
-            style_id: Default::default(),
-        }
+    pub fn chunks_at(&self, point: DisplayPoint) -> tab_map::Chunks {
+        self.tabs_snapshot.chunks_at(point.0)
+    }
+
+    pub fn highlighted_chunks_for_rows(&mut self, rows: Range<u32>) -> tab_map::HighlightedChunks {
+        self.tabs_snapshot.highlighted_chunks_for_rows(rows)
     }
 
     pub fn chars_at<'a>(&'a self, point: DisplayPoint) -> impl Iterator<Item = char> + 'a {
@@ -134,10 +117,7 @@ impl DisplayMapSnapshot {
     }
 
     pub fn clip_point(&self, point: DisplayPoint, bias: Bias) -> DisplayPoint {
-        self.expand_tabs(
-            self.folds_snapshot
-                .clip_point(self.collapse_tabs(point, bias).0, bias),
-        )
+        DisplayPoint(self.tabs_snapshot.clip_point(point.0, bias))
     }
 
     pub fn folds_in_range<'a, T>(
@@ -190,15 +170,11 @@ impl DisplayMapSnapshot {
     }
 
     pub fn line_len(&self, row: u32) -> u32 {
-        self.expand_tabs(fold_map::OutputPoint::new(
-            row,
-            self.folds_snapshot.line_len(row),
-        ))
-        .column()
+        self.tabs_snapshot.line_len(row)
     }
 
     pub fn longest_row(&self) -> u32 {
-        self.folds_snapshot.longest_row()
+        self.tabs_snapshot.longest_row()
     }
 
     pub fn anchor_before(&self, point: DisplayPoint, bias: Bias) -> Anchor {
@@ -210,40 +186,14 @@ impl DisplayMapSnapshot {
         self.buffer_snapshot
             .anchor_after(point.to_buffer_point(self, bias))
     }
-
-    fn expand_tabs(&self, point: fold_map::OutputPoint) -> DisplayPoint {
-        let chars = self
-            .folds_snapshot
-            .chars_at(fold_map::OutputPoint::new(point.row(), 0));
-        let expanded = expand_tabs(chars, point.column() as usize, self.tab_size);
-        DisplayPoint::new(point.row(), expanded as u32)
-    }
-
-    fn collapse_tabs(
-        &self,
-        point: DisplayPoint,
-        bias: Bias,
-    ) -> (fold_map::OutputPoint, usize, usize) {
-        let chars = self
-            .folds_snapshot
-            .chars_at(fold_map::OutputPoint::new(point.row(), 0));
-        let expanded = point.column() as usize;
-        let (collapsed, expanded_char_column, to_next_stop) =
-            collapse_tabs(chars, expanded, bias, self.tab_size);
-        (
-            fold_map::OutputPoint::new(point.row(), collapsed as u32),
-            expanded_char_column,
-            to_next_stop,
-        )
-    }
 }
 
 #[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
-pub struct DisplayPoint(fold_map::OutputPoint);
+pub struct DisplayPoint(tab_map::OutputPoint);
 
 impl DisplayPoint {
     pub fn new(row: u32, column: u32) -> Self {
-        Self(fold_map::OutputPoint::new(row, column))
+        Self(tab_map::OutputPoint::new(row, column))
     }
 
     pub fn zero() -> Self {
@@ -268,24 +218,20 @@ impl DisplayPoint {
 
     pub fn to_buffer_point(self, map: &DisplayMapSnapshot, bias: Bias) -> Point {
         map.folds_snapshot
-            .to_input_point(map.collapse_tabs(self, bias).0)
+            .to_input_point(map.tabs_snapshot.to_input_point(self.0, bias).0)
     }
 
     pub fn to_buffer_offset(self, map: &DisplayMapSnapshot, bias: Bias) -> usize {
         map.folds_snapshot
-            .to_input_offset(map.collapse_tabs(self, bias).0)
+            .to_input_offset(map.tabs_snapshot.to_input_point(self.0, bias).0)
     }
 }
 
 impl Point {
     pub fn to_display_point(self, map: &DisplayMapSnapshot) -> DisplayPoint {
-        let folded_point = map.folds_snapshot.to_output_point(self);
-        let chars = map
-            .folds_snapshot
-            .chars_at(fold_map::OutputPoint::new(folded_point.row(), 0));
-        DisplayPoint::new(
-            folded_point.row(),
-            expand_tabs(chars, folded_point.column() as usize, map.tab_size) as u32,
+        DisplayPoint(
+            map.tabs_snapshot
+                .to_output_point(map.folds_snapshot.to_output_point(self)),
         )
     }
 }
@@ -296,163 +242,6 @@ impl Anchor {
     }
 }
 
-// Handles a tab width <= 16
-const SPACES: &'static str = "                ";
-
-pub struct Chunks<'a> {
-    fold_chunks: fold_map::Chunks<'a>,
-    chunk: &'a str,
-    column: usize,
-    tab_size: usize,
-    skip_leading_tab: bool,
-}
-
-impl<'a> Iterator for Chunks<'a> {
-    type Item = &'a str;
-
-    fn next(&mut self) -> Option<Self::Item> {
-        if self.chunk.is_empty() {
-            if let Some(chunk) = self.fold_chunks.next() {
-                self.chunk = chunk;
-                if self.skip_leading_tab {
-                    self.chunk = &self.chunk[1..];
-                    self.skip_leading_tab = false;
-                }
-            } else {
-                return None;
-            }
-        }
-
-        for (ix, c) in self.chunk.char_indices() {
-            match c {
-                '\t' => {
-                    if ix > 0 {
-                        let (prefix, suffix) = self.chunk.split_at(ix);
-                        self.chunk = suffix;
-                        return Some(prefix);
-                    } else {
-                        self.chunk = &self.chunk[1..];
-                        let len = self.tab_size - self.column % self.tab_size;
-                        self.column += len;
-                        return Some(&SPACES[0..len]);
-                    }
-                }
-                '\n' => self.column = 0,
-                _ => self.column += 1,
-            }
-        }
-
-        let result = Some(self.chunk);
-        self.chunk = "";
-        result
-    }
-}
-
-pub struct HighlightedChunks<'a> {
-    fold_chunks: fold_map::HighlightedChunks<'a>,
-    chunk: &'a str,
-    style_id: StyleId,
-    column: usize,
-    tab_size: usize,
-}
-
-impl<'a> Iterator for HighlightedChunks<'a> {
-    type Item = (&'a str, StyleId);
-
-    fn next(&mut self) -> Option<Self::Item> {
-        if self.chunk.is_empty() {
-            if let Some((chunk, style_id)) = self.fold_chunks.next() {
-                self.chunk = chunk;
-                self.style_id = style_id;
-            } else {
-                return None;
-            }
-        }
-
-        for (ix, c) in self.chunk.char_indices() {
-            match c {
-                '\t' => {
-                    if ix > 0 {
-                        let (prefix, suffix) = self.chunk.split_at(ix);
-                        self.chunk = suffix;
-                        return Some((prefix, self.style_id));
-                    } else {
-                        self.chunk = &self.chunk[1..];
-                        let len = self.tab_size - self.column % self.tab_size;
-                        self.column += len;
-                        return Some((&SPACES[0..len], self.style_id));
-                    }
-                }
-                '\n' => self.column = 0,
-                _ => self.column += 1,
-            }
-        }
-
-        Some((mem::take(&mut self.chunk), mem::take(&mut self.style_id)))
-    }
-}
-
-pub fn expand_tabs(chars: impl Iterator<Item = char>, column: usize, tab_size: usize) -> usize {
-    let mut expanded_chars = 0;
-    let mut expanded_bytes = 0;
-    let mut collapsed_bytes = 0;
-    for c in chars {
-        if collapsed_bytes == column {
-            break;
-        }
-        if c == '\t' {
-            let tab_len = tab_size - expanded_chars % tab_size;
-            expanded_bytes += tab_len;
-            expanded_chars += tab_len;
-        } else {
-            expanded_bytes += c.len_utf8();
-            expanded_chars += 1;
-        }
-        collapsed_bytes += c.len_utf8();
-    }
-    expanded_bytes
-}
-
-pub fn collapse_tabs(
-    mut chars: impl Iterator<Item = char>,
-    column: usize,
-    bias: Bias,
-    tab_size: usize,
-) -> (usize, usize, usize) {
-    let mut expanded_bytes = 0;
-    let mut expanded_chars = 0;
-    let mut collapsed_bytes = 0;
-    while let Some(c) = chars.next() {
-        if expanded_bytes >= column {
-            break;
-        }
-
-        if c == '\t' {
-            let tab_len = tab_size - (expanded_chars % tab_size);
-            expanded_chars += tab_len;
-            expanded_bytes += tab_len;
-            if expanded_bytes > column {
-                expanded_chars -= expanded_bytes - column;
-                return match bias {
-                    Bias::Left => (collapsed_bytes, expanded_chars, expanded_bytes - column),
-                    Bias::Right => (collapsed_bytes + 1, expanded_chars, 0),
-                };
-            }
-        } else {
-            expanded_chars += 1;
-            expanded_bytes += c.len_utf8();
-        }
-
-        if expanded_bytes > column && matches!(bias, Bias::Left) {
-            expanded_chars -= 1;
-            break;
-        }
-
-        collapsed_bytes += c.len_utf8();
-    }
-    (collapsed_bytes, expanded_chars, 0)
-}
-
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -640,13 +429,6 @@ mod tests {
         }
     }
 
-    #[test]
-    fn test_expand_tabs() {
-        assert_eq!(expand_tabs("\t".chars(), 0, 4), 0);
-        assert_eq!(expand_tabs("\t".chars(), 1, 4), 4);
-        assert_eq!(expand_tabs("\ta".chars(), 2, 4), 5);
-    }
-
     #[gpui::test]
     fn test_tabs_with_multibyte_chars(cx: &mut gpui::MutableAppContext) {
         let text = "āœ…\t\tα\nβ\t\nšŸ€Ī²\t\tγ";

zed/src/editor/display_map/fold_map.rs šŸ”—

@@ -1041,7 +1041,7 @@ impl<'a> sum_tree::Dimension<'a, TransformSummary> for OutputPoint {
 pub struct OutputOffset(pub usize);
 
 impl OutputOffset {
-    pub fn to_output_point(&self, snapshot: &Snapshot) -> OutputPoint {
+    pub fn to_point(&self, snapshot: &Snapshot) -> OutputPoint {
         let mut cursor = snapshot
             .transforms
             .cursor::<OutputOffset, TransformSummary>();

zed/src/editor/display_map/tab_map.rs šŸ”—

@@ -0,0 +1,344 @@
+use parking_lot::Mutex;
+
+use super::fold_map::{
+    Chunks as InputChunks, Edit as InputEdit, HighlightedChunks as InputHighlightedChunks,
+    OutputOffset as InputOffset, OutputPoint as InputPoint, Snapshot as InputSnapshot,
+};
+use crate::{settings::StyleId, util::Bias};
+use std::{mem, ops::Range};
+
+pub struct TabMap(Mutex<Snapshot>);
+
+impl TabMap {
+    pub fn new(input: InputSnapshot, tab_size: usize) -> Self {
+        Self(Mutex::new(Snapshot { input, tab_size }))
+    }
+
+    pub fn sync(
+        &self,
+        snapshot: InputSnapshot,
+        input_edits: Vec<InputEdit>,
+    ) -> (Snapshot, Vec<Edit>) {
+        let mut old_snapshot = self.0.lock();
+        let new_snapshot = Snapshot {
+            input: snapshot,
+            tab_size: old_snapshot.tab_size,
+        };
+
+        let mut output_edits = Vec::with_capacity(input_edits.len());
+        for input_edit in input_edits {
+            output_edits.push(Edit {
+                old_bytes: old_snapshot.to_output_offset(input_edit.old_bytes.start)
+                    ..old_snapshot.to_output_offset(input_edit.old_bytes.end),
+                new_bytes: new_snapshot.to_output_offset(input_edit.new_bytes.start)
+                    ..new_snapshot.to_output_offset(input_edit.new_bytes.end),
+            });
+        }
+
+        *old_snapshot = new_snapshot;
+        (old_snapshot.clone(), output_edits)
+    }
+}
+
+#[derive(Clone)]
+pub struct Snapshot {
+    input: InputSnapshot,
+    tab_size: usize,
+}
+
+impl Snapshot {
+    pub fn chunks_at(&self, point: OutputPoint) -> Chunks {
+        let (point, expanded_char_column, to_next_stop) = self.to_input_point(point, Bias::Left);
+        let fold_chunks = self.input.chunks_at(self.input.to_output_offset(point));
+        Chunks {
+            fold_chunks,
+            column: expanded_char_column,
+            tab_size: self.tab_size,
+            chunk: &SPACES[0..to_next_stop],
+            skip_leading_tab: to_next_stop > 0,
+        }
+    }
+
+    pub fn highlighted_chunks_for_rows(&mut self, rows: Range<u32>) -> HighlightedChunks {
+        let start = self.input.to_output_offset(InputPoint::new(rows.start, 0));
+        let end = self
+            .input
+            .to_output_offset(InputPoint::new(rows.end, 0).min(self.input.max_point()));
+        HighlightedChunks {
+            input_chunks: self.input.highlighted_chunks(start..end),
+            column: 0,
+            tab_size: self.tab_size,
+            chunk: "",
+            style_id: Default::default(),
+        }
+    }
+
+    pub fn line_len(&self, row: u32) -> u32 {
+        self.to_output_point(InputPoint::new(row, self.input.line_len(row)))
+            .column()
+    }
+
+    pub fn longest_row(&self) -> u32 {
+        // TODO: Account for tab expansion.
+        self.input.longest_row()
+    }
+
+    pub fn max_point(&self) -> OutputPoint {
+        self.to_output_point(self.input.max_point())
+    }
+
+    pub fn clip_point(&self, point: OutputPoint, bias: Bias) -> OutputPoint {
+        self.to_output_point(
+            self.input
+                .clip_point(self.to_input_point(point, bias).0, bias),
+        )
+    }
+
+    pub fn to_output_offset(&self, input_offset: InputOffset) -> OutputOffset {
+        let input_point = input_offset.to_point(&self.input);
+        let input_row_start_offset = self
+            .input
+            .to_output_offset(InputPoint::new(input_point.row(), 0));
+        let output_point = self.to_output_point(input_point);
+        OutputOffset(input_row_start_offset.0 + output_point.column() as usize)
+    }
+
+    pub fn to_output_point(&self, input: InputPoint) -> OutputPoint {
+        let chars = self.input.chars_at(InputPoint::new(input.row(), 0));
+        let expanded = Self::expand_tabs(chars, input.column() as usize, self.tab_size);
+        OutputPoint::new(input.row(), expanded as u32)
+    }
+
+    pub fn to_input_point(&self, output: OutputPoint, bias: Bias) -> (InputPoint, usize, usize) {
+        let chars = self.input.chars_at(InputPoint::new(output.row(), 0));
+        let expanded = output.column() as usize;
+        let (collapsed, expanded_char_column, to_next_stop) =
+            Self::collapse_tabs(chars, expanded, bias, self.tab_size);
+        (
+            InputPoint::new(output.row(), collapsed as u32),
+            expanded_char_column,
+            to_next_stop,
+        )
+    }
+
+    fn expand_tabs(chars: impl Iterator<Item = char>, column: usize, tab_size: usize) -> usize {
+        let mut expanded_chars = 0;
+        let mut expanded_bytes = 0;
+        let mut collapsed_bytes = 0;
+        for c in chars {
+            if collapsed_bytes == column {
+                break;
+            }
+            if c == '\t' {
+                let tab_len = tab_size - expanded_chars % tab_size;
+                expanded_bytes += tab_len;
+                expanded_chars += tab_len;
+            } else {
+                expanded_bytes += c.len_utf8();
+                expanded_chars += 1;
+            }
+            collapsed_bytes += c.len_utf8();
+        }
+        expanded_bytes
+    }
+
+    fn collapse_tabs(
+        mut chars: impl Iterator<Item = char>,
+        column: usize,
+        bias: Bias,
+        tab_size: usize,
+    ) -> (usize, usize, usize) {
+        let mut expanded_bytes = 0;
+        let mut expanded_chars = 0;
+        let mut collapsed_bytes = 0;
+        while let Some(c) = chars.next() {
+            if expanded_bytes >= column {
+                break;
+            }
+
+            if c == '\t' {
+                let tab_len = tab_size - (expanded_chars % tab_size);
+                expanded_chars += tab_len;
+                expanded_bytes += tab_len;
+                if expanded_bytes > column {
+                    expanded_chars -= expanded_bytes - column;
+                    return match bias {
+                        Bias::Left => (collapsed_bytes, expanded_chars, expanded_bytes - column),
+                        Bias::Right => (collapsed_bytes + 1, expanded_chars, 0),
+                    };
+                }
+            } else {
+                expanded_chars += 1;
+                expanded_bytes += c.len_utf8();
+            }
+
+            if expanded_bytes > column && matches!(bias, Bias::Left) {
+                expanded_chars -= 1;
+                break;
+            }
+
+            collapsed_bytes += c.len_utf8();
+        }
+        (collapsed_bytes, expanded_chars, 0)
+    }
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+pub struct OutputOffset(usize);
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct Edit {
+    pub old_bytes: Range<OutputOffset>,
+    pub new_bytes: Range<OutputOffset>,
+}
+
+impl Edit {
+    pub fn delta(&self) -> isize {
+        self.inserted_bytes() as isize - self.deleted_bytes() as isize
+    }
+
+    pub fn deleted_bytes(&self) -> usize {
+        self.old_bytes.end.0 - self.old_bytes.start.0
+    }
+
+    pub fn inserted_bytes(&self) -> usize {
+        self.new_bytes.end.0 - self.new_bytes.start.0
+    }
+}
+
+#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
+pub struct OutputPoint(super::Point);
+
+impl OutputPoint {
+    pub fn new(row: u32, column: u32) -> Self {
+        Self(super::Point::new(row, column))
+    }
+
+    pub fn zero() -> Self {
+        Self::new(0, 0)
+    }
+
+    pub fn row(self) -> u32 {
+        self.0.row
+    }
+
+    pub fn column(self) -> u32 {
+        self.0.column
+    }
+
+    pub fn row_mut(&mut self) -> &mut u32 {
+        &mut self.0.row
+    }
+
+    pub fn column_mut(&mut self) -> &mut u32 {
+        &mut self.0.column
+    }
+}
+
+// Handles a tab width <= 16
+const SPACES: &'static str = "                ";
+
+pub struct Chunks<'a> {
+    fold_chunks: InputChunks<'a>,
+    chunk: &'a str,
+    column: usize,
+    tab_size: usize,
+    skip_leading_tab: bool,
+}
+
+impl<'a> Iterator for Chunks<'a> {
+    type Item = &'a str;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        if self.chunk.is_empty() {
+            if let Some(chunk) = self.fold_chunks.next() {
+                self.chunk = chunk;
+                if self.skip_leading_tab {
+                    self.chunk = &self.chunk[1..];
+                    self.skip_leading_tab = false;
+                }
+            } else {
+                return None;
+            }
+        }
+
+        for (ix, c) in self.chunk.char_indices() {
+            match c {
+                '\t' => {
+                    if ix > 0 {
+                        let (prefix, suffix) = self.chunk.split_at(ix);
+                        self.chunk = suffix;
+                        return Some(prefix);
+                    } else {
+                        self.chunk = &self.chunk[1..];
+                        let len = self.tab_size - self.column % self.tab_size;
+                        self.column += len;
+                        return Some(&SPACES[0..len]);
+                    }
+                }
+                '\n' => self.column = 0,
+                _ => self.column += 1,
+            }
+        }
+
+        let result = Some(self.chunk);
+        self.chunk = "";
+        result
+    }
+}
+
+pub struct HighlightedChunks<'a> {
+    input_chunks: InputHighlightedChunks<'a>,
+    chunk: &'a str,
+    style_id: StyleId,
+    column: usize,
+    tab_size: usize,
+}
+
+impl<'a> Iterator for HighlightedChunks<'a> {
+    type Item = (&'a str, StyleId);
+
+    fn next(&mut self) -> Option<Self::Item> {
+        if self.chunk.is_empty() {
+            if let Some((chunk, style_id)) = self.input_chunks.next() {
+                self.chunk = chunk;
+                self.style_id = style_id;
+            } else {
+                return None;
+            }
+        }
+
+        for (ix, c) in self.chunk.char_indices() {
+            match c {
+                '\t' => {
+                    if ix > 0 {
+                        let (prefix, suffix) = self.chunk.split_at(ix);
+                        self.chunk = suffix;
+                        return Some((prefix, self.style_id));
+                    } else {
+                        self.chunk = &self.chunk[1..];
+                        let len = self.tab_size - self.column % self.tab_size;
+                        self.column += len;
+                        return Some((&SPACES[0..len], self.style_id));
+                    }
+                }
+                '\n' => self.column = 0,
+                _ => self.column += 1,
+            }
+        }
+
+        Some((mem::take(&mut self.chunk), mem::take(&mut self.style_id)))
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn test_expand_tabs() {
+        assert_eq!(Snapshot::expand_tabs("\t".chars(), 0, 4), 0);
+        assert_eq!(Snapshot::expand_tabs("\t".chars(), 1, 4), 4);
+        assert_eq!(Snapshot::expand_tabs("\ta".chars(), 2, 4), 5);
+    }
+}