tab_map.rs

  1use super::{
  2    fold_map::{self, FoldEdit, FoldPoint, FoldSnapshot},
  3    TextHighlights,
  4};
  5use crate::MultiBufferSnapshot;
  6use language::{rope, Chunk};
  7use parking_lot::Mutex;
  8use std::{cmp, mem, num::NonZeroU32, ops::Range};
  9use sum_tree::Bias;
 10use text::Point;
 11
 12pub struct TabMap(Mutex<TabSnapshot>);
 13
 14impl TabMap {
 15    pub fn new(input: FoldSnapshot, tab_size: NonZeroU32) -> (Self, TabSnapshot) {
 16        let snapshot = TabSnapshot {
 17            fold_snapshot: input,
 18            tab_size,
 19        };
 20        (Self(Mutex::new(snapshot.clone())), snapshot)
 21    }
 22
 23    pub fn sync(
 24        &self,
 25        fold_snapshot: FoldSnapshot,
 26        mut fold_edits: Vec<FoldEdit>,
 27        tab_size: NonZeroU32,
 28    ) -> (TabSnapshot, Vec<TabEdit>) {
 29        let mut old_snapshot = self.0.lock();
 30        let new_snapshot = TabSnapshot {
 31            fold_snapshot,
 32            tab_size,
 33        };
 34
 35        if old_snapshot.tab_size != new_snapshot.tab_size {
 36            let edits = vec![TabEdit {
 37                old: TabPoint::zero()..old_snapshot.max_point(),
 38                new: TabPoint::zero()..new_snapshot.max_point(),
 39            }];
 40            return (new_snapshot, edits);
 41        }
 42
 43        let old_max_offset = old_snapshot.fold_snapshot.len();
 44        let mut tab_edits = Vec::with_capacity(fold_edits.len());
 45        for fold_edit in &mut fold_edits {
 46            let mut delta = 0;
 47            for chunk in
 48                old_snapshot
 49                    .fold_snapshot
 50                    .chunks(fold_edit.old.end..old_max_offset, false, None)
 51            {
 52                let patterns: &[_] = &['\t', '\n'];
 53                if let Some(ix) = chunk.text.find(patterns) {
 54                    if &chunk.text[ix..ix + 1] == "\t" {
 55                        fold_edit.old.end.0 += delta + ix + 1;
 56                        fold_edit.new.end.0 += delta + ix + 1;
 57                    }
 58
 59                    break;
 60                }
 61
 62                delta += chunk.text.len();
 63            }
 64        }
 65
 66        let mut ix = 1;
 67        while ix < fold_edits.len() {
 68            let (prev_edits, next_edits) = fold_edits.split_at_mut(ix);
 69            let prev_edit = prev_edits.last_mut().unwrap();
 70            let edit = &next_edits[0];
 71            if prev_edit.old.end >= edit.old.start {
 72                prev_edit.old.end = edit.old.end;
 73                prev_edit.new.end = edit.new.end;
 74                fold_edits.remove(ix);
 75            } else {
 76                ix += 1;
 77            }
 78        }
 79
 80        for fold_edit in fold_edits {
 81            let old_start = fold_edit.old.start.to_point(&old_snapshot.fold_snapshot);
 82            let old_end = fold_edit.old.end.to_point(&old_snapshot.fold_snapshot);
 83            let new_start = fold_edit.new.start.to_point(&new_snapshot.fold_snapshot);
 84            let new_end = fold_edit.new.end.to_point(&new_snapshot.fold_snapshot);
 85            tab_edits.push(TabEdit {
 86                old: old_snapshot.to_tab_point(old_start)..old_snapshot.to_tab_point(old_end),
 87                new: new_snapshot.to_tab_point(new_start)..new_snapshot.to_tab_point(new_end),
 88            });
 89        }
 90
 91        *old_snapshot = new_snapshot;
 92        (old_snapshot.clone(), tab_edits)
 93    }
 94}
 95
 96#[derive(Clone)]
 97pub struct TabSnapshot {
 98    pub fold_snapshot: FoldSnapshot,
 99    pub tab_size: NonZeroU32,
100}
101
102impl TabSnapshot {
103    pub fn buffer_snapshot(&self) -> &MultiBufferSnapshot {
104        self.fold_snapshot.buffer_snapshot()
105    }
106
107    pub fn line_len(&self, row: u32) -> u32 {
108        let max_point = self.max_point();
109        if row < max_point.row() {
110            self.chunks(
111                TabPoint::new(row, 0)..TabPoint::new(row + 1, 0),
112                false,
113                None,
114            )
115            .map(|chunk| chunk.text.len() as u32)
116            .sum::<u32>()
117                - 1
118        } else {
119            max_point.column()
120        }
121    }
122
123    pub fn text_summary(&self) -> TextSummary {
124        self.text_summary_for_range(TabPoint::zero()..self.max_point())
125    }
126
127    pub fn text_summary_for_range(&self, range: Range<TabPoint>) -> TextSummary {
128        let input_start = self.to_fold_point(range.start, Bias::Left).0;
129        let input_end = self.to_fold_point(range.end, Bias::Right).0;
130        let input_summary = self
131            .fold_snapshot
132            .text_summary_for_range(input_start..input_end);
133
134        let mut first_line_chars = 0;
135        let line_end = if range.start.row() == range.end.row() {
136            range.end
137        } else {
138            self.max_point()
139        };
140        for c in self
141            .chunks(range.start..line_end, false, None)
142            .flat_map(|chunk| chunk.text.chars())
143        {
144            if c == '\n' {
145                break;
146            }
147            first_line_chars += 1;
148        }
149
150        let mut last_line_chars = 0;
151        if range.start.row() == range.end.row() {
152            last_line_chars = first_line_chars;
153        } else {
154            for _ in self
155                .chunks(TabPoint::new(range.end.row(), 0)..range.end, false, None)
156                .flat_map(|chunk| chunk.text.chars())
157            {
158                last_line_chars += 1;
159            }
160        }
161
162        TextSummary {
163            lines: range.end.0 - range.start.0,
164            first_line_chars,
165            last_line_chars,
166            longest_row: input_summary.longest_row,
167            longest_row_chars: input_summary.longest_row_chars,
168        }
169    }
170
171    pub fn version(&self) -> usize {
172        self.fold_snapshot.version
173    }
174
175    pub fn chunks<'a>(
176        &'a self,
177        range: Range<TabPoint>,
178        language_aware: bool,
179        text_highlights: Option<&'a TextHighlights>,
180    ) -> TabChunks<'a> {
181        let (input_start, expanded_char_column, to_next_stop) =
182            self.to_fold_point(range.start, Bias::Left);
183        let input_start = input_start.to_offset(&self.fold_snapshot);
184        let input_end = self
185            .to_fold_point(range.end, Bias::Right)
186            .0
187            .to_offset(&self.fold_snapshot);
188        let to_next_stop = if range.start.0 + Point::new(0, to_next_stop as u32) > range.end.0 {
189            (range.end.column() - range.start.column()) as usize
190        } else {
191            to_next_stop
192        };
193
194        TabChunks {
195            fold_chunks: self.fold_snapshot.chunks(
196                input_start..input_end,
197                language_aware,
198                text_highlights,
199            ),
200            column: expanded_char_column,
201            output_position: range.start.0,
202            max_output_position: range.end.0,
203            tab_size: self.tab_size,
204            chunk: Chunk {
205                text: &SPACES[0..to_next_stop],
206                ..Default::default()
207            },
208            skip_leading_tab: to_next_stop > 0,
209        }
210    }
211
212    pub fn buffer_rows(&self, row: u32) -> fold_map::FoldBufferRows {
213        self.fold_snapshot.buffer_rows(row)
214    }
215
216    #[cfg(test)]
217    pub fn text(&self) -> String {
218        self.chunks(TabPoint::zero()..self.max_point(), false, None)
219            .map(|chunk| chunk.text)
220            .collect()
221    }
222
223    pub fn max_point(&self) -> TabPoint {
224        self.to_tab_point(self.fold_snapshot.max_point())
225    }
226
227    pub fn clip_point(&self, point: TabPoint, bias: Bias) -> TabPoint {
228        self.to_tab_point(
229            self.fold_snapshot
230                .clip_point(self.to_fold_point(point, bias).0, bias),
231        )
232    }
233
234    pub fn to_tab_point(&self, input: FoldPoint) -> TabPoint {
235        let chars = self.fold_snapshot.chars_at(FoldPoint::new(input.row(), 0));
236        let expanded = Self::expand_tabs(chars, input.column() as usize, self.tab_size);
237        TabPoint::new(input.row(), expanded as u32)
238    }
239
240    pub fn to_fold_point(&self, output: TabPoint, bias: Bias) -> (FoldPoint, usize, usize) {
241        let chars = self.fold_snapshot.chars_at(FoldPoint::new(output.row(), 0));
242        let expanded = output.column() as usize;
243        let (collapsed, expanded_char_column, to_next_stop) =
244            Self::collapse_tabs(chars, expanded, bias, self.tab_size);
245        (
246            FoldPoint::new(output.row(), collapsed as u32),
247            expanded_char_column,
248            to_next_stop,
249        )
250    }
251
252    pub fn from_point(&self, point: Point, bias: Bias) -> TabPoint {
253        self.to_tab_point(self.fold_snapshot.to_fold_point(point, bias))
254    }
255
256    pub fn to_point(&self, point: TabPoint, bias: Bias) -> Point {
257        self.to_fold_point(point, bias)
258            .0
259            .to_buffer_point(&self.fold_snapshot)
260    }
261
262    fn expand_tabs(
263        chars: impl Iterator<Item = char>,
264        column: usize,
265        tab_size: NonZeroU32,
266    ) -> usize {
267        let mut expanded_chars = 0;
268        let mut expanded_bytes = 0;
269        let mut collapsed_bytes = 0;
270        for c in chars {
271            if collapsed_bytes == column {
272                break;
273            }
274            if c == '\t' {
275                let tab_size = tab_size.get() as usize;
276                let tab_len = tab_size - expanded_chars % tab_size;
277                expanded_bytes += tab_len;
278                expanded_chars += tab_len;
279            } else {
280                expanded_bytes += c.len_utf8();
281                expanded_chars += 1;
282            }
283            collapsed_bytes += c.len_utf8();
284        }
285        expanded_bytes
286    }
287
288    fn collapse_tabs(
289        mut chars: impl Iterator<Item = char>,
290        column: usize,
291        bias: Bias,
292        tab_size: NonZeroU32,
293    ) -> (usize, usize, usize) {
294        let mut expanded_bytes = 0;
295        let mut expanded_chars = 0;
296        let mut collapsed_bytes = 0;
297        while let Some(c) = chars.next() {
298            if expanded_bytes >= column {
299                break;
300            }
301
302            if c == '\t' {
303                let tab_size = tab_size.get() as usize;
304                let tab_len = tab_size - (expanded_chars % tab_size);
305                expanded_chars += tab_len;
306                expanded_bytes += tab_len;
307                if expanded_bytes > column {
308                    expanded_chars -= expanded_bytes - column;
309                    return match bias {
310                        Bias::Left => (collapsed_bytes, expanded_chars, expanded_bytes - column),
311                        Bias::Right => (collapsed_bytes + 1, expanded_chars, 0),
312                    };
313                }
314            } else {
315                expanded_chars += 1;
316                expanded_bytes += c.len_utf8();
317            }
318
319            if expanded_bytes > column && matches!(bias, Bias::Left) {
320                expanded_chars -= 1;
321                break;
322            }
323
324            collapsed_bytes += c.len_utf8();
325        }
326        (collapsed_bytes, expanded_chars, 0)
327    }
328}
329
330#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
331pub struct TabPoint(pub super::Point);
332
333impl TabPoint {
334    pub fn new(row: u32, column: u32) -> Self {
335        Self(super::Point::new(row, column))
336    }
337
338    pub fn zero() -> Self {
339        Self::new(0, 0)
340    }
341
342    pub fn row(self) -> u32 {
343        self.0.row
344    }
345
346    pub fn column(self) -> u32 {
347        self.0.column
348    }
349}
350
351impl From<super::Point> for TabPoint {
352    fn from(point: super::Point) -> Self {
353        Self(point)
354    }
355}
356
357pub type TabEdit = text::Edit<TabPoint>;
358
359#[derive(Clone, Debug, Default, Eq, PartialEq)]
360pub struct TextSummary {
361    pub lines: super::Point,
362    pub first_line_chars: u32,
363    pub last_line_chars: u32,
364    pub longest_row: u32,
365    pub longest_row_chars: u32,
366}
367
368impl<'a> From<&'a str> for TextSummary {
369    fn from(text: &'a str) -> Self {
370        let sum = rope::TextSummary::from(text);
371
372        TextSummary {
373            lines: sum.lines,
374            first_line_chars: sum.first_line_chars,
375            last_line_chars: sum.last_line_chars,
376            longest_row: sum.longest_row,
377            longest_row_chars: sum.longest_row_chars,
378        }
379    }
380}
381
382impl<'a> std::ops::AddAssign<&'a Self> for TextSummary {
383    fn add_assign(&mut self, other: &'a Self) {
384        let joined_chars = self.last_line_chars + other.first_line_chars;
385        if joined_chars > self.longest_row_chars {
386            self.longest_row = self.lines.row;
387            self.longest_row_chars = joined_chars;
388        }
389        if other.longest_row_chars > self.longest_row_chars {
390            self.longest_row = self.lines.row + other.longest_row;
391            self.longest_row_chars = other.longest_row_chars;
392        }
393
394        if self.lines.row == 0 {
395            self.first_line_chars += other.first_line_chars;
396        }
397
398        if other.lines.row == 0 {
399            self.last_line_chars += other.first_line_chars;
400        } else {
401            self.last_line_chars = other.last_line_chars;
402        }
403
404        self.lines += &other.lines;
405    }
406}
407
408// Handles a tab width <= 16
409const SPACES: &'static str = "                ";
410
411pub struct TabChunks<'a> {
412    fold_chunks: fold_map::FoldChunks<'a>,
413    chunk: Chunk<'a>,
414    column: usize,
415    output_position: Point,
416    max_output_position: Point,
417    tab_size: NonZeroU32,
418    skip_leading_tab: bool,
419}
420
421impl<'a> Iterator for TabChunks<'a> {
422    type Item = Chunk<'a>;
423
424    fn next(&mut self) -> Option<Self::Item> {
425        if self.chunk.text.is_empty() {
426            if let Some(chunk) = self.fold_chunks.next() {
427                self.chunk = chunk;
428                if self.skip_leading_tab {
429                    self.chunk.text = &self.chunk.text[1..];
430                    self.skip_leading_tab = false;
431                }
432            } else {
433                return None;
434            }
435        }
436
437        for (ix, c) in self.chunk.text.char_indices() {
438            match c {
439                '\t' => {
440                    if ix > 0 {
441                        let (prefix, suffix) = self.chunk.text.split_at(ix);
442                        self.chunk.text = suffix;
443                        return Some(Chunk {
444                            text: prefix,
445                            ..self.chunk
446                        });
447                    } else {
448                        self.chunk.text = &self.chunk.text[1..];
449                        let tab_size = self.tab_size.get() as u32;
450                        let mut len = tab_size - self.column as u32 % tab_size;
451                        let next_output_position = cmp::min(
452                            self.output_position + Point::new(0, len),
453                            self.max_output_position,
454                        );
455                        len = next_output_position.column - self.output_position.column;
456                        self.column += len as usize;
457                        self.output_position = next_output_position;
458                        return Some(Chunk {
459                            text: &SPACES[0..len as usize],
460                            ..self.chunk
461                        });
462                    }
463                }
464                '\n' => {
465                    self.column = 0;
466                    self.output_position += Point::new(1, 0);
467                }
468                _ => {
469                    self.column += 1;
470                    self.output_position.column += c.len_utf8() as u32;
471                }
472            }
473        }
474
475        Some(mem::take(&mut self.chunk))
476    }
477}
478
479#[cfg(test)]
480mod tests {
481    use super::*;
482    use crate::{display_map::fold_map::FoldMap, MultiBuffer};
483    use rand::{prelude::StdRng, Rng};
484    use text::{RandomCharIter, Rope};
485
486    #[test]
487    fn test_expand_tabs() {
488        assert_eq!(
489            TabSnapshot::expand_tabs("\t".chars(), 0, 4.try_into().unwrap()),
490            0
491        );
492        assert_eq!(
493            TabSnapshot::expand_tabs("\t".chars(), 1, 4.try_into().unwrap()),
494            4
495        );
496        assert_eq!(
497            TabSnapshot::expand_tabs("\ta".chars(), 2, 4.try_into().unwrap()),
498            5
499        );
500    }
501
502    #[gpui::test(iterations = 100)]
503    fn test_random_tabs(cx: &mut gpui::MutableAppContext, mut rng: StdRng) {
504        let tab_size = NonZeroU32::new(rng.gen_range(1..=4)).unwrap();
505        let len = rng.gen_range(0..30);
506        let buffer = if rng.gen() {
507            let text = RandomCharIter::new(&mut rng).take(len).collect::<String>();
508            MultiBuffer::build_simple(&text, cx)
509        } else {
510            MultiBuffer::build_random(&mut rng, cx)
511        };
512        let buffer_snapshot = buffer.read(cx).snapshot(cx);
513        log::info!("Buffer text: {:?}", buffer_snapshot.text());
514
515        let (mut fold_map, _) = FoldMap::new(buffer_snapshot.clone());
516        fold_map.randomly_mutate(&mut rng);
517        let (folds_snapshot, _) = fold_map.read(buffer_snapshot.clone(), vec![]);
518        log::info!("FoldMap text: {:?}", folds_snapshot.text());
519
520        let (_, tabs_snapshot) = TabMap::new(folds_snapshot.clone(), tab_size);
521        let text = Rope::from(tabs_snapshot.text().as_str());
522        log::info!(
523            "TabMap text (tab size: {}): {:?}",
524            tab_size,
525            tabs_snapshot.text(),
526        );
527
528        for _ in 0..5 {
529            let end_row = rng.gen_range(0..=text.max_point().row);
530            let end_column = rng.gen_range(0..=text.line_len(end_row));
531            let mut end = TabPoint(text.clip_point(Point::new(end_row, end_column), Bias::Right));
532            let start_row = rng.gen_range(0..=text.max_point().row);
533            let start_column = rng.gen_range(0..=text.line_len(start_row));
534            let mut start =
535                TabPoint(text.clip_point(Point::new(start_row, start_column), Bias::Left));
536            if start > end {
537                mem::swap(&mut start, &mut end);
538            }
539
540            let expected_text = text
541                .chunks_in_range(text.point_to_offset(start.0)..text.point_to_offset(end.0))
542                .collect::<String>();
543            let expected_summary = TextSummary::from(expected_text.as_str());
544            assert_eq!(
545                expected_text,
546                tabs_snapshot
547                    .chunks(start..end, false, None)
548                    .map(|c| c.text)
549                    .collect::<String>(),
550                "chunks({:?}..{:?})",
551                start,
552                end
553            );
554
555            let mut actual_summary = tabs_snapshot.text_summary_for_range(start..end);
556            if tab_size.get() > 1 && folds_snapshot.text().contains('\t') {
557                actual_summary.longest_row = expected_summary.longest_row;
558                actual_summary.longest_row_chars = expected_summary.longest_row_chars;
559            }
560            assert_eq!(actual_summary, expected_summary);
561        }
562
563        for row in 0..=text.max_point().row {
564            assert_eq!(tabs_snapshot.line_len(row), text.line_len(row));
565        }
566    }
567}