From 164cafa57d3eff2c1bf71ae8595f5a69eaff96b8 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 27 Jul 2021 18:09:26 +0200 Subject: [PATCH] Preserve indentation when soft-wrapping Co-Authored-By: Nathan Sobo --- zed/src/editor/display_map.rs | 78 ++++++------- zed/src/editor/display_map/line_wrapper.rs | 107 ++++++++++++----- zed/src/editor/display_map/wrap_map.rs | 128 +++++++++++++-------- zed/src/editor/movement.rs | 4 +- 4 files changed, 200 insertions(+), 117 deletions(-) diff --git a/zed/src/editor/display_map.rs b/zed/src/editor/display_map.rs index 7b13d1f05ee2a8c5be80854b2331beef88da67da..d99cbfaa384a837d0ab065cdbea5b6c291a503e3 100644 --- a/zed/src/editor/display_map.rs +++ b/zed/src/editor/display_map.rs @@ -111,22 +111,26 @@ impl DisplayMapSnapshot { DisplayPoint(self.wraps_snapshot.max_point()) } - pub fn chunks_at(&self, point: DisplayPoint) -> wrap_map::Chunks { - self.wraps_snapshot.chunks_at(point.0) + pub fn chunks_at(&self, display_row: u32) -> wrap_map::Chunks { + self.wraps_snapshot.chunks_at(display_row) } - pub fn highlighted_chunks_for_rows(&mut self, rows: Range) -> wrap_map::HighlightedChunks { - self.wraps_snapshot.highlighted_chunks_for_rows(rows) + pub fn highlighted_chunks_for_rows( + &mut self, + display_rows: Range, + ) -> wrap_map::HighlightedChunks { + self.wraps_snapshot + .highlighted_chunks_for_rows(display_rows) } - pub fn chars_at<'a>(&'a self, point: DisplayPoint) -> impl Iterator + 'a { - self.chunks_at(point).flat_map(str::chars) + pub fn chars_at<'a>(&'a self, display_row: u32) -> impl Iterator + 'a { + self.chunks_at(display_row).flat_map(str::chars) } pub fn column_to_chars(&self, display_row: u32, target: u32) -> u32 { let mut count = 0; let mut column = 0; - for c in self.chars_at(DisplayPoint::new(display_row, 0)) { + for c in self.chars_at(display_row) { if column >= target { break; } @@ -139,7 +143,7 @@ impl DisplayMapSnapshot { pub fn column_from_chars(&self, display_row: u32, char_count: u32) -> u32 { let mut count = 0; let mut column = 0; - for c in self.chars_at(DisplayPoint::new(display_row, 0)) { + for c in self.chars_at(display_row) { if c == '\n' || count >= char_count { break; } @@ -174,12 +178,12 @@ impl DisplayMapSnapshot { } pub fn text(&self) -> String { - self.chunks_at(DisplayPoint::zero()).collect() + self.chunks_at(0).collect() } pub fn line(&self, display_row: u32) -> String { let mut result = String::new(); - for chunk in self.chunks_at(DisplayPoint::new(display_row, 0)) { + for chunk in self.chunks_at(display_row) { if let Some(ix) = chunk.find('\n') { result.push_str(&chunk[0..ix]); break; @@ -193,7 +197,7 @@ impl DisplayMapSnapshot { pub fn line_indent(&self, display_row: u32) -> (u32, bool) { let mut indent = 0; let mut is_blank = true; - for c in self.chars_at(DisplayPoint::new(display_row, 0)) { + for c in self.chars_at(display_row) { if c == ' ' { indent += 1; } else { @@ -378,10 +382,8 @@ mod tests { let snapshot = map.update(&mut cx, |map, cx| map.snapshot(cx)); assert_eq!( - snapshot - .chunks_at(DisplayPoint::new(0, 3)) - .collect::(), - " two \nthree four \nfive\nsix seven \neight" + snapshot.chunks_at(0).collect::(), + "one two \nthree four \nfive\nsix seven \neight" ); assert_eq!( snapshot.clip_point(DisplayPoint::new(0, 8), Bias::Left), @@ -399,9 +401,7 @@ mod tests { let snapshot = map.update(&mut cx, |map, cx| map.snapshot(cx)); assert_eq!( - snapshot - .chunks_at(DisplayPoint::new(1, 0)) - .collect::(), + snapshot.chunks_at(1).collect::(), "three four \nfive\nsix and \nseven eight" ); } @@ -431,22 +431,20 @@ mod tests { }); assert_eq!( - &map.update(cx, |map, cx| map.snapshot(cx)) - .chunks_at(DisplayPoint::new(1, 0)) - .collect::()[0..10], - " b bb" - ); - assert_eq!( - &map.update(cx, |map, cx| map.snapshot(cx)) - .chunks_at(DisplayPoint::new(1, 2)) - .collect::()[0..10], - " b bbbb" + map.update(cx, |map, cx| map.snapshot(cx)) + .chunks_at(1) + .collect::() + .lines() + .next(), + Some(" b bbbbb") ); assert_eq!( - &map.update(cx, |map, cx| map.snapshot(cx)) - .chunks_at(DisplayPoint::new(1, 6)) - .collect::()[0..13], - " bbbbb\nc c" + map.update(cx, |map, cx| map.snapshot(cx)) + .chunks_at(2) + .collect::() + .lines() + .next(), + Some("c ccccc") ); } @@ -676,6 +674,12 @@ mod tests { }); let map = map.update(cx, |map, cx| map.snapshot(cx)); assert_eq!(map.text(), "✅ α\nβ \n🏀β γ"); + assert_eq!( + map.chunks_at(0).collect::(), + "✅ α\nβ \n🏀β γ" + ); + assert_eq!(map.chunks_at(1).collect::(), "β \n🏀β γ"); + assert_eq!(map.chunks_at(2).collect::(), "🏀β γ"); let point = Point::new(0, "✅\t\t".len() as u32); let display_point = DisplayPoint::new(0, "✅ ".len() as u32); @@ -701,11 +705,6 @@ mod tests { DisplayPoint::new(0, "✅ ".len() as u32).to_buffer_point(&map, Bias::Left), Point::new(0, "✅\t".len() as u32), ); - assert_eq!( - map.chunks_at(DisplayPoint::new(0, "✅ ".len() as u32)) - .collect::(), - " α\nβ \n🏀β γ" - ); assert_eq!( DisplayPoint::new(0, "✅ ".len() as u32).to_buffer_point(&map, Bias::Right), Point::new(0, "✅\t".len() as u32), @@ -714,11 +713,6 @@ mod tests { DisplayPoint::new(0, "✅ ".len() as u32).to_buffer_point(&map, Bias::Left), Point::new(0, "✅".len() as u32), ); - assert_eq!( - map.chunks_at(DisplayPoint::new(0, "✅ ".len() as u32)) - .collect::(), - " α\nβ \n🏀β γ" - ); // Clipping display points inside of multi-byte characters assert_eq!( diff --git a/zed/src/editor/display_map/line_wrapper.rs b/zed/src/editor/display_map/line_wrapper.rs index 87836454d7a6ffe6dc8ae1869cecbda402794f83..97ac2769e72b231e85d9f55c3fc5e763e674b36c 100644 --- a/zed/src/editor/display_map/line_wrapper.rs +++ b/zed/src/editor/display_map/line_wrapper.rs @@ -3,6 +3,7 @@ use gpui::{fonts::FontId, FontCache, FontSystem}; use std::{ cell::RefCell, collections::HashMap, + iter, ops::{Deref, DerefMut}, sync::Arc, }; @@ -11,6 +12,18 @@ thread_local! { static WRAPPERS: RefCell> = Default::default(); } +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct Boundary { + pub ix: usize, + pub next_indent: u32, +} + +impl Boundary { + fn new(ix: usize, next_indent: u32) -> Self { + Self { ix, next_indent } + } +} + pub struct LineWrapper { font_system: Arc, font_id: FontId, @@ -20,6 +33,8 @@ pub struct LineWrapper { } impl LineWrapper { + pub const MAX_INDENT: u32 = 256; + pub fn thread_local( font_system: Arc, font_cache: &FontCache, @@ -60,33 +75,44 @@ impl LineWrapper { } } - #[cfg(test)] - pub fn wrap_line_with_shaping(&self, line: &str, wrap_width: f32) -> Vec { - self.font_system - .wrap_line(line, self.font_id, self.font_size, wrap_width) - } - pub fn wrap_line<'a>( &'a mut self, line: &'a str, wrap_width: f32, - ) -> impl Iterator + 'a { + ) -> impl Iterator + 'a { let mut width = 0.0; + let mut first_non_whitespace_ix = None; + let mut indent = None; let mut last_candidate_ix = 0; let mut last_candidate_width = 0.0; let mut last_wrap_ix = 0; let mut prev_c = '\0'; - let char_indices = line.char_indices(); - char_indices.filter_map(move |(ix, c)| { - if c != '\n' { - if self.is_boundary(prev_c, c) { + let mut char_indices = line.char_indices(); + iter::from_fn(move || { + while let Some((ix, c)) = char_indices.next() { + if c == '\n' { + continue; + } + + if self.is_boundary(prev_c, c) && first_non_whitespace_ix.is_some() { last_candidate_ix = ix; last_candidate_width = width; } + if c != ' ' && first_non_whitespace_ix.is_none() { + first_non_whitespace_ix = Some(ix); + } + let char_width = self.width_for_char(c); width += char_width; if width > wrap_width && ix > last_wrap_ix { + if let (None, Some(first_non_whitespace_ix)) = (indent, first_non_whitespace_ix) + { + indent = Some( + Self::MAX_INDENT.min((first_non_whitespace_ix - last_wrap_ix) as u32), + ); + } + if last_candidate_ix > 0 { last_wrap_ix = last_candidate_ix; width -= last_candidate_width; @@ -95,7 +121,12 @@ impl LineWrapper { last_wrap_ix = ix; width = char_width; } - return Some(last_wrap_ix); + + let indent_width = + indent.map(|indent| indent as f32 * self.width_for_char(' ')); + width += indent_width.unwrap_or(0.); + + return Some(Boundary::new(last_wrap_ix, indent.unwrap_or(0))); } prev_c = c; } @@ -105,10 +136,7 @@ impl LineWrapper { } fn is_boundary(&self, prev: char, next: char) -> bool { - if prev == ' ' || next == ' ' { - return true; - } - false + (prev == ' ') && (next != ' ') } #[inline(always)] @@ -184,27 +212,54 @@ mod tests { }; let mut wrapper = LineWrapper::new(font_system, &font_cache, settings); - assert_eq!( - wrapper.wrap_line_with_shaping("aa bbb cccc ddddd eeee", 72.0), - &[7, 12, 18], + wrapper + .wrap_line("aa bbb cccc ddddd eeee", 72.0) + .collect::>(), + &[ + Boundary::new(7, 0), + Boundary::new(12, 0), + Boundary::new(18, 0) + ], ); assert_eq!( wrapper - .wrap_line("aa bbb cccc ddddd eeee", 72.0) + .wrap_line("aaa aaaaaaaaaaaaaaaaaa", 72.0) .collect::>(), - &[7, 12, 18], + &[ + Boundary::new(4, 0), + Boundary::new(11, 0), + Boundary::new(18, 0) + ], ); - assert_eq!( - wrapper.wrap_line_with_shaping("aaa aaaaaaaaaaaaaaaaaa", 72.0), - &[4, 11, 18], + wrapper.wrap_line(" aaaaaaa", 72.).collect::>(), + &[ + Boundary::new(7, 5), + Boundary::new(9, 5), + Boundary::new(11, 5), + ] ); assert_eq!( wrapper - .wrap_line("aaa aaaaaaaaaaaaaaaaaa", 72.0) + .wrap_line(" ", 72.) + .collect::>(), + &[ + Boundary::new(7, 0), + Boundary::new(14, 0), + Boundary::new(21, 0) + ] + ); + assert_eq!( + wrapper + .wrap_line(" aaaaaaaaaaaaaa", 72.) .collect::>(), - &[4, 11, 18], + &[ + Boundary::new(7, 0), + Boundary::new(14, 3), + Boundary::new(18, 3), + Boundary::new(22, 3), + ] ); } } diff --git a/zed/src/editor/display_map/wrap_map.rs b/zed/src/editor/display_map/wrap_map.rs index e3ee1c566e506c956db1074ae288948bcbf1d95a..c129bc3363882dee924bf2f4ff6443e3c639574a 100644 --- a/zed/src/editor/display_map/wrap_map.rs +++ b/zed/src/editor/display_map/wrap_map.rs @@ -11,6 +11,7 @@ use crate::{ Settings, }; use gpui::{Entity, ModelContext, Task}; +use lazy_static::lazy_static; use smol::future::yield_now; use std::{collections::VecDeque, ops::Range, time::Duration}; @@ -391,11 +392,11 @@ impl Snapshot { } let mut prev_boundary_ix = 0; - for boundary_ix in line_wrapper.wrap_line(&line, wrap_width) { - let wrapped = &line[prev_boundary_ix..boundary_ix]; + for boundary in line_wrapper.wrap_line(&line, wrap_width) { + let wrapped = &line[prev_boundary_ix..boundary.ix]; push_isomorphic(&mut edit_transforms, TextSummary::from(wrapped)); - edit_transforms.push(Transform::newline()); - prev_boundary_ix = boundary_ix; + edit_transforms.push(Transform::wrap(boundary.next_indent)); + prev_boundary_ix = boundary.ix; } if prev_boundary_ix < line.len() { @@ -453,11 +454,14 @@ impl Snapshot { self.check_invariants(); } - pub fn chunks_at(&self, point: WrapPoint) -> Chunks { + pub fn chunks_at(&self, wrap_row: u32) -> Chunks { + let point = WrapPoint::new(wrap_row, 0); let mut transforms = self.transforms.cursor::(); transforms.seek(&point, Bias::Right, &()); - let input_position = - TabPoint(transforms.sum_start().0 + (point.0 - transforms.seek_start().0)); + let mut input_position = TabPoint(transforms.sum_start().0); + if transforms.item().map_or(false, |t| t.is_isomorphic()) { + input_position.0 += point.0 - transforms.seek_start().0; + } let input_chunks = self.tab_snapshot.chunks_at(input_position); Chunks { input_chunks, @@ -472,8 +476,10 @@ impl Snapshot { let output_end = WrapPoint::new(rows.end, 0); let mut transforms = self.transforms.cursor::(); transforms.seek(&output_start, Bias::Right, &()); - let input_start = - TabPoint(transforms.sum_start().0 + (output_start.0 - transforms.seek_start().0)); + let mut input_start = TabPoint(transforms.sum_start().0); + if transforms.item().map_or(false, |t| t.is_isomorphic()) { + input_start.0 += output_start.0 - transforms.seek_start().0; + } let input_end = self .to_tab_point(output_end) .min(self.tab_snapshot.max_point()); @@ -493,7 +499,7 @@ impl Snapshot { pub fn line_len(&self, row: u32) -> u32 { let mut len = 0; - for chunk in self.chunks_at(WrapPoint::new(row, 0)) { + for chunk in self.chunks_at(row) { if let Some(newline_ix) = chunk.find('\n') { len += newline_ix; break; @@ -511,7 +517,10 @@ impl Snapshot { pub fn buffer_rows(&self, start_row: u32) -> BufferRows { let mut transforms = self.transforms.cursor::(); transforms.seek(&WrapPoint::new(start_row, 0), Bias::Right, &()); - let input_row = transforms.sum_start().row() + (start_row - transforms.seek_start().row()); + let mut input_row = transforms.sum_start().row(); + if transforms.item().map_or(false, |t| t.is_isomorphic()) { + input_row += start_row - transforms.seek_start().row(); + } let mut input_buffer_rows = self.tab_snapshot.buffer_rows(input_row); let input_buffer_row = input_buffer_rows.next().unwrap(); BufferRows { @@ -526,7 +535,11 @@ impl Snapshot { pub fn to_tab_point(&self, point: WrapPoint) -> TabPoint { let mut cursor = self.transforms.cursor::(); cursor.seek(&point, Bias::Right, &()); - TabPoint(cursor.sum_start().0 + (point.0 - cursor.seek_start().0)) + let mut tab_point = cursor.sum_start().0; + if cursor.item().map_or(false, |t| t.is_isomorphic()) { + tab_point += point.0 - cursor.seek_start().0; + } + TabPoint(tab_point) } pub fn to_wrap_point(&self, point: TabPoint) -> WrapPoint { @@ -539,8 +552,8 @@ impl Snapshot { if bias == Bias::Left { let mut cursor = self.transforms.cursor::(); cursor.seek(&point, Bias::Right, &()); - let transform = cursor.item().expect("invalid point"); - if !transform.is_isomorphic() { + if cursor.item().map_or(false, |t| !t.is_isomorphic()) { + point = *cursor.seek_start(); *point.column_mut() -= 1; } } @@ -559,11 +572,9 @@ impl Snapshot { { let mut transforms = self.transforms.cursor::<(), ()>().peekable(); while let Some(transform) = transforms.next() { - let next_transform = transforms.peek(); - assert!( - !transform.is_isomorphic() - || next_transform.map_or(true, |t| !t.is_isomorphic()) - ); + if let Some(next_transform) = transforms.peek() { + assert!(transform.is_isomorphic() != next_transform.is_isomorphic()); + } } } } @@ -576,9 +587,15 @@ impl<'a> Iterator for Chunks<'a> { fn next(&mut self) -> Option { let transform = self.transforms.item()?; if let Some(display_text) = transform.display_text { - self.output_position.0 += transform.summary.output.lines; - self.transforms.next(&()); - return Some(display_text); + if self.output_position > *self.transforms.seek_start() { + self.output_position.0.column += transform.summary.output.lines.column; + self.transforms.next(&()); + return Some(&display_text[1..]); + } else { + self.output_position.0 += transform.summary.output.lines; + self.transforms.next(&()); + return Some(display_text); + } } if self.input_chunk.is_empty() { @@ -619,9 +636,23 @@ impl<'a> Iterator for HighlightedChunks<'a> { let transform = self.transforms.item()?; if let Some(display_text) = transform.display_text { - self.output_position.0 += transform.summary.output.lines; + let mut start_ix = 0; + let mut end_ix = display_text.len(); + let mut summary = transform.summary.output.lines; + + if self.output_position > *self.transforms.seek_start() { + // Exclude newline starting prior to the desired row. + start_ix = 1; + summary.row = 0; + } else if self.output_position.row() + 1 >= self.max_output_row { + // Exclude soft indentation ending after the desired row. + end_ix = 1; + summary.column = 0; + } + + self.output_position.0 += summary; self.transforms.next(&()); - return Some((display_text, self.style_id)); + return Some((&display_text[start_ix..end_ix], self.style_id)); } if self.input_chunk.is_empty() { @@ -688,19 +719,28 @@ impl Transform { } } - fn newline() -> Self { + fn wrap(indent: u32) -> Self { + lazy_static! { + static ref WRAP_TEXT: String = { + let mut wrap_text = String::new(); + wrap_text.push('\n'); + wrap_text.extend((0..LineWrapper::MAX_INDENT as usize).map(|_| ' ')); + wrap_text + }; + } + Self { summary: TransformSummary { input: TextSummary::default(), output: TextSummary { - lines: Point::new(1, 0), + lines: Point::new(1, indent), first_line_chars: 0, - last_line_chars: 0, - longest_row: 0, - longest_row_chars: 0, + last_line_chars: indent, + longest_row: 1, + longest_row_chars: indent, }, }, - display_text: Some("\n"), + display_text: Some(&WRAP_TEXT[..1 + indent as usize]), } } @@ -753,11 +793,6 @@ impl WrapPoint { Self(super::Point::new(row, column)) } - #[cfg(test)] - pub fn zero() -> Self { - Self::new(0, 0) - } - pub fn row(self) -> u32 { self.0.row } @@ -918,7 +953,7 @@ mod tests { map.sync(tabs_snapshot.clone(), edits, cx) }); snapshot.check_invariants(); - interpolated_snapshot.verify_chunks(&mut rng); + snapshot.verify_chunks(&mut rng); if wrap_map.read_with(&cx, |map, _| map.is_rewrapping()) && rng.gen_bool(0.4) { log::info!("Waiting for wrapping to finish"); @@ -928,18 +963,17 @@ mod tests { } if !wrap_map.read_with(&cx, |map, _| map.is_rewrapping()) { - log::info!("Wrapping finished"); snapshot = wrap_map.update(&mut cx, |map, cx| map.sync(tabs_snapshot, Vec::new(), cx)); - snapshot.check_invariants(); - interpolated_snapshot.verify_chunks(&mut rng); let actual_text = snapshot.text(); + log::info!("Wrapping finished: {:?}", actual_text); + snapshot.check_invariants(); + snapshot.verify_chunks(&mut rng); assert_eq!( actual_text, expected_text, "unwrapped text is: {:?}", unwrapped_text ); - log::info!("New wrapped text: {:?}", actual_text); interpolated_snapshot = snapshot.clone(); } } @@ -959,10 +993,11 @@ mod tests { } let mut prev_ix = 0; - for ix in line_wrapper.wrap_line(line, wrap_width) { - wrapped_text.push_str(&line[prev_ix..ix]); + for boundary in line_wrapper.wrap_line(line, wrap_width) { + wrapped_text.push_str(&line[prev_ix..boundary.ix]); wrapped_text.push('\n'); - prev_ix = ix; + wrapped_text.push_str(&" ".repeat(boundary.next_indent as usize)); + prev_ix = boundary.ix; } wrapped_text.push_str(&line[prev_ix..]); } @@ -974,7 +1009,7 @@ mod tests { impl Snapshot { fn text(&self) -> String { - self.chunks_at(WrapPoint::zero()).collect() + self.chunks_at(0).collect() } fn verify_chunks(&mut self, rng: &mut impl Rng) { @@ -983,9 +1018,7 @@ mod tests { let start_row = rng.gen_range(0..=end_row); end_row += 1; - let mut expected_text = self - .chunks_at(WrapPoint::new(start_row, 0)) - .collect::(); + let mut expected_text = self.chunks_at(start_row).collect::(); if expected_text.ends_with("\n") { expected_text.push('\n'); } @@ -997,6 +1030,7 @@ mod tests { if end_row <= self.max_point().row() { expected_text.push('\n'); } + let actual_text = self .highlighted_chunks_for_rows(start_row..end_row) .map(|c| c.0) diff --git a/zed/src/editor/movement.rs b/zed/src/editor/movement.rs index 1a0f01c14520151c932f6756153159d22682f4e6..7b1cfc35974d64f348033eeaaffaaf8952359810 100644 --- a/zed/src/editor/movement.rs +++ b/zed/src/editor/movement.rs @@ -94,7 +94,7 @@ pub fn prev_word_boundary(map: &DisplayMapSnapshot, point: DisplayPoint) -> Resu let mut boundary = DisplayPoint::new(point.row(), 0); let mut column = 0; let mut prev_c = None; - for c in map.chars_at(boundary) { + for c in map.chars_at(point.row()) { if column >= point.column() { break; } @@ -115,7 +115,7 @@ pub fn next_word_boundary( mut point: DisplayPoint, ) -> Result { let mut prev_c = None; - for c in map.chars_at(point) { + for c in map.chars_at(point.row()).skip(point.column() as usize) { if prev_c.is_some() && (c == '\n' || char_kind(prev_c.unwrap()) != char_kind(c)) { break; }