From 0875a86c695d34522971ef9d58abd40c455adc41 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 21 Jul 2021 17:18:15 +0200 Subject: [PATCH] Account for the impact of edits on tab expansion Tab characters are expanded differently based on the column on which they appear, which edits can affect. Thus, `TabMap::sync` will now expand edits to the first tab that appears on the line in which the edit occurred. Co-Authored-By: Nathan Sobo --- zed/src/editor/display_map/tab_map.rs | 90 ++++++++++++++++++++------ zed/src/editor/display_map/wrap_map.rs | 34 +++++----- 2 files changed, 89 insertions(+), 35 deletions(-) diff --git a/zed/src/editor/display_map/tab_map.rs b/zed/src/editor/display_map/tab_map.rs index 0356312457..9df8a63863 100644 --- a/zed/src/editor/display_map/tab_map.rs +++ b/zed/src/editor/display_map/tab_map.rs @@ -5,7 +5,7 @@ use super::fold_map::{ OutputOffset as InputOffset, OutputPoint as InputPoint, Snapshot as InputSnapshot, }; use crate::{editor::rope, settings::StyleId, util::Bias}; -use std::{mem, ops::Range}; +use std::{cmp, mem, ops::Range}; pub struct TabMap(Mutex); @@ -18,7 +18,7 @@ impl TabMap { pub fn sync( &self, snapshot: InputSnapshot, - input_edits: Vec, + mut input_edits: Vec, ) -> (Snapshot, Vec) { let mut old_snapshot = self.0.lock(); let new_snapshot = Snapshot { @@ -27,6 +27,39 @@ impl TabMap { }; let mut output_edits = Vec::with_capacity(input_edits.len()); + for input_edit in &mut input_edits { + let mut delta = 0; + for chunk in old_snapshot.input.chunks_at(input_edit.old_bytes.end) { + let patterns: &[_] = &['\t', '\n']; + if let Some(ix) = chunk.find(patterns) { + if &chunk[ix..ix + 1] == "\t" { + input_edit.old_bytes.end.0 += delta + ix + old_snapshot.tab_size; + input_edit.new_bytes.end.0 += delta + ix + new_snapshot.tab_size; + } + + break; + } + + delta += chunk.len(); + } + input_edit.old_bytes.end = cmp::min(input_edit.old_bytes.end, old_snapshot.input.len()); + input_edit.new_bytes.end = cmp::min(input_edit.new_bytes.end, new_snapshot.input.len()); + } + + let mut ix = 1; + while ix < input_edits.len() { + let (prev_edits, next_edits) = input_edits.split_at_mut(ix); + let prev_edit = prev_edits.last_mut().unwrap(); + let edit = &next_edits[0]; + if prev_edit.old_bytes.end >= edit.old_bytes.start { + prev_edit.old_bytes.end = edit.old_bytes.end; + prev_edit.new_bytes.end = edit.new_bytes.end; + input_edits.remove(ix); + } else { + ix += 1; + } + } + for input_edit in input_edits { let old_start = input_edit.old_bytes.start.to_point(&old_snapshot.input); let old_end = input_edit.old_bytes.end.to_point(&old_snapshot.input); @@ -53,28 +86,45 @@ pub struct Snapshot { impl Snapshot { pub fn text_summary(&self) -> TextSummary { - // TODO: expand tabs on first and last line, ignoring the longest row. - let summary = self.input.text_summary(); - TextSummary { - lines: summary.lines, - first_line_chars: summary.first_line_chars, - last_line_chars: summary.last_line_chars, - longest_row: summary.longest_row, - longest_row_chars: summary.longest_row_chars, - } + self.text_summary_for_range(OutputPoint::zero()..self.max_point()) } pub fn text_summary_for_range(&self, range: Range) -> TextSummary { - // TODO: expand tabs on first and last line, ignoring the longest row. - let start = self.to_input_point(range.start, Bias::Left).0; - let end = self.to_input_point(range.end, Bias::Right).0; - let summary = self.input.text_summary_for_range(start..end); + let input_start = self.to_input_point(range.start, Bias::Left).0; + let input_end = self.to_input_point(range.end, Bias::Right).0; + let input_summary = self.input.text_summary_for_range(input_start..input_end); + + let mut first_line_chars = 0; + let mut first_line_bytes = 0; + for c in self.chunks_at(range.start).flat_map(|chunk| chunk.chars()) { + if c == '\n' + || (range.start.row() == range.end.row() && first_line_bytes == range.end.column()) + { + break; + } + first_line_chars += 1; + first_line_bytes += c.len_utf8() as u32; + } + + let mut last_line_chars = 0; + let mut last_line_bytes = 0; + for c in self + .chunks_at(OutputPoint::new(range.end.row(), 0).max(range.start)) + .flat_map(|chunk| chunk.chars()) + { + if last_line_bytes == range.end.column() { + break; + } + last_line_chars += 1; + last_line_bytes += c.len_utf8() as u32; + } + TextSummary { - lines: summary.lines, - first_line_chars: summary.first_line_chars, - last_line_chars: summary.last_line_chars, - longest_row: summary.longest_row, - longest_row_chars: summary.longest_row_chars, + lines: range.end.0 - range.start.0, + first_line_chars, + last_line_chars, + longest_row: input_summary.longest_row, + longest_row_chars: input_summary.longest_row_chars, } } diff --git a/zed/src/editor/display_map/wrap_map.rs b/zed/src/editor/display_map/wrap_map.rs index ba0fbc75af..fa345b40c9 100644 --- a/zed/src/editor/display_map/wrap_map.rs +++ b/zed/src/editor/display_map/wrap_map.rs @@ -755,28 +755,30 @@ mod tests { for seed in seed_range { dbg!(seed); let mut rng = StdRng::seed_from_u64(seed); - - let buffer = cx.add_model(|cx| { - let len = rng.gen_range(0..10); - let text = RandomCharIter::new(&mut rng).take(len).collect::(); - log::info!("Initial buffer text: {:?}", text); - Buffer::new(0, text, cx) - }); - let (fold_map, folds_snapshot) = FoldMap::new(buffer.clone(), cx.as_ref()); - let (tab_map, tabs_snapshot) = - TabMap::new(folds_snapshot.clone(), rng.gen_range(1..=4)); let font_cache = cx.font_cache().clone(); let font_system = cx.platform().fonts(); let wrap_width = rng.gen_range(100.0..=1000.0); let settings = Settings { - tab_size: 4, + tab_size: rng.gen_range(1..=4), buffer_font_family: font_cache.load_family(&["Helvetica"]).unwrap(), buffer_font_size: 14.0, ..Settings::new(&font_cache).unwrap() }; + log::info!("Tab size: {}", settings.tab_size); + log::info!("Wrap width: {}", wrap_width); + let font_id = font_cache .select_font(settings.buffer_font_family, &Default::default()) .unwrap(); + + let buffer = cx.add_model(|cx| { + let len = rng.gen_range(0..10); + let text = RandomCharIter::new(&mut rng).take(len).collect::(); + log::info!("Initial buffer text: {:?} (len: {})", text, text.len()); + Buffer::new(0, text, cx) + }); + let (fold_map, folds_snapshot) = FoldMap::new(buffer.clone(), cx.as_ref()); + let (tab_map, tabs_snapshot) = TabMap::new(folds_snapshot.clone(), settings.tab_size); let mut wrapper = BackgroundWrapper::new( Snapshot::new(tabs_snapshot.clone()), settings.clone(), @@ -818,10 +820,8 @@ mod tests { wrap_text(&unwrapped_text, wrap_width, font_id, font_system.as_ref()); wrapper.sync(snapshot, edits); wrapper.snapshot.check_invariants(); - let actual_text = wrapper - .snapshot - .chunks_at(OutputPoint::zero()) - .collect::(); + + let actual_text = wrapper.snapshot.text(); assert_eq!( actual_text, expected_text, "unwrapped text is: {:?}", @@ -857,6 +857,10 @@ mod tests { } impl Snapshot { + fn text(&self) -> String { + self.chunks_at(OutputPoint::zero()).collect() + } + fn check_invariants(&self) { assert_eq!( InputPoint::from(self.transforms.summary().input.lines),