mirror of
https://github.com/zed-industries/zed.git
synced 2024-10-25 16:09:44 +00:00
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 <nathan@zed.dev>
This commit is contained in:
parent
62ad97a728
commit
0875a86c69
2 changed files with 89 additions and 35 deletions
|
@ -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<Snapshot>);
|
||||
|
||||
|
@ -18,7 +18,7 @@ impl TabMap {
|
|||
pub fn sync(
|
||||
&self,
|
||||
snapshot: InputSnapshot,
|
||||
input_edits: Vec<InputEdit>,
|
||||
mut input_edits: Vec<InputEdit>,
|
||||
) -> (Snapshot, Vec<Edit>) {
|
||||
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<OutputPoint>) -> 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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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::<String>();
|
||||
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::<String>();
|
||||
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::<String>();
|
||||
|
||||
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),
|
||||
|
|
Loading…
Reference in a new issue