Preserve indentation when soft-wrapping

Co-Authored-By: Nathan Sobo <nathan@zed.dev>
This commit is contained in:
Antonio Scandurra 2021-07-27 18:09:26 +02:00 committed by Max Brunsfeld
parent 2f1a5c48d3
commit 164cafa57d
4 changed files with 202 additions and 119 deletions

View File

@ -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<u32>) -> wrap_map::HighlightedChunks {
self.wraps_snapshot.highlighted_chunks_for_rows(rows)
pub fn highlighted_chunks_for_rows(
&mut self,
display_rows: Range<u32>,
) -> wrap_map::HighlightedChunks {
self.wraps_snapshot
.highlighted_chunks_for_rows(display_rows)
}
pub fn chars_at<'a>(&'a self, point: DisplayPoint) -> impl Iterator<Item = char> + 'a {
self.chunks_at(point).flat_map(str::chars)
pub fn chars_at<'a>(&'a self, display_row: u32) -> impl Iterator<Item = char> + '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::<String>(),
" two \nthree four \nfive\nsix seven \neight"
snapshot.chunks_at(0).collect::<String>(),
"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::<String>(),
snapshot.chunks_at(1).collect::<String>(),
"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::<String>()[0..10],
" b bb"
map.update(cx, |map, cx| map.snapshot(cx))
.chunks_at(1)
.collect::<String>()
.lines()
.next(),
Some(" b bbbbb")
);
assert_eq!(
&map.update(cx, |map, cx| map.snapshot(cx))
.chunks_at(DisplayPoint::new(1, 2))
.collect::<String>()[0..10],
" b bbbb"
);
assert_eq!(
&map.update(cx, |map, cx| map.snapshot(cx))
.chunks_at(DisplayPoint::new(1, 6))
.collect::<String>()[0..13],
" bbbbb\nc c"
map.update(cx, |map, cx| map.snapshot(cx))
.chunks_at(2)
.collect::<String>()
.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::<String>(),
"α\nβ \n🏀β γ"
);
assert_eq!(map.chunks_at(1).collect::<String>(), "β \n🏀β γ");
assert_eq!(map.chunks_at(2).collect::<String>(), "🏀β γ");
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::<String>(),
" α\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::<String>(),
" α\nβ \n🏀β γ"
);
// Clipping display points inside of multi-byte characters
assert_eq!(

View File

@ -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<Vec<LineWrapper>> = 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<dyn FontSystem>,
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<dyn FontSystem>,
font_cache: &FontCache,
@ -60,33 +75,44 @@ impl LineWrapper {
}
}
#[cfg(test)]
pub fn wrap_line_with_shaping(&self, line: &str, wrap_width: f32) -> Vec<usize> {
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<Item = usize> + 'a {
) -> impl Iterator<Item = Boundary> + '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],
);
assert_eq!(
wrapper
.wrap_line("aa bbb cccc ddddd eeee", 72.0)
.collect::<Vec<_>>(),
&[7, 12, 18],
);
assert_eq!(
wrapper.wrap_line_with_shaping("aaa aaaaaaaaaaaaaaaaaa", 72.0),
&[4, 11, 18],
&[
Boundary::new(7, 0),
Boundary::new(12, 0),
Boundary::new(18, 0)
],
);
assert_eq!(
wrapper
.wrap_line("aaa aaaaaaaaaaaaaaaaaa", 72.0)
.collect::<Vec<_>>(),
&[4, 11, 18],
&[
Boundary::new(4, 0),
Boundary::new(11, 0),
Boundary::new(18, 0)
],
);
assert_eq!(
wrapper.wrap_line(" aaaaaaa", 72.).collect::<Vec<_>>(),
&[
Boundary::new(7, 5),
Boundary::new(9, 5),
Boundary::new(11, 5),
]
);
assert_eq!(
wrapper
.wrap_line(" ", 72.)
.collect::<Vec<_>>(),
&[
Boundary::new(7, 0),
Boundary::new(14, 0),
Boundary::new(21, 0)
]
);
assert_eq!(
wrapper
.wrap_line(" aaaaaaaaaaaaaa", 72.)
.collect::<Vec<_>>(),
&[
Boundary::new(7, 0),
Boundary::new(14, 3),
Boundary::new(18, 3),
Boundary::new(22, 3),
]
);
}
}

View File

@ -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::<WrapPoint, TabPoint>();
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::<WrapPoint, TabPoint>();
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::<WrapPoint, TabPoint>();
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::<WrapPoint, TabPoint>();
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::<WrapPoint, ()>();
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<Self::Item> {
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::<String>();
let mut expected_text = self.chunks_at(start_row).collect::<String>();
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)

View File

@ -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<DisplayPoint> {
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;
}