Change how line chunking works, to favor breaking on whitespace.

This commit is contained in:
Nathan Vegdahl 2020-02-01 22:32:09 +09:00
parent 18920d9e87
commit ebfedab58c
4 changed files with 174 additions and 109 deletions

View File

@ -553,7 +553,7 @@ impl<T: LineFormatter> Editor<T> {
pub fn cursor_up(&mut self, n: usize) { pub fn cursor_up(&mut self, n: usize) {
for c in self.cursors.iter_mut() { for c in self.cursors.iter_mut() {
let vmove = -1 * (n * self.formatter.single_line_height()) as isize; let vmove = -1 * n as isize;
let mut temp_index = self.formatter.index_offset_vertical_v2d( let mut temp_index = self.formatter.index_offset_vertical_v2d(
&self.buffer, &self.buffer,
@ -589,7 +589,7 @@ impl<T: LineFormatter> Editor<T> {
pub fn cursor_down(&mut self, n: usize) { pub fn cursor_down(&mut self, n: usize) {
for c in self.cursors.iter_mut() { for c in self.cursors.iter_mut() {
let vmove = (n * self.formatter.single_line_height()) as isize; let vmove = n as isize;
let mut temp_index = self.formatter.index_offset_vertical_v2d( let mut temp_index = self.formatter.index_offset_vertical_v2d(
&self.buffer, &self.buffer,
@ -624,8 +624,7 @@ impl<T: LineFormatter> Editor<T> {
} }
pub fn page_up(&mut self) { pub fn page_up(&mut self) {
let move_amount = let move_amount = self.view_dim.0 - max(self.view_dim.0 / 8, 1);
self.view_dim.0 - max(self.view_dim.0 / 8, self.formatter.single_line_height());
self.view_pos.0 = self.formatter.index_offset_vertical_v2d( self.view_pos.0 = self.formatter.index_offset_vertical_v2d(
&self.buffer, &self.buffer,
self.view_pos.0, self.view_pos.0,
@ -640,8 +639,7 @@ impl<T: LineFormatter> Editor<T> {
} }
pub fn page_down(&mut self) { pub fn page_down(&mut self) {
let move_amount = let move_amount = self.view_dim.0 - max(self.view_dim.0 / 8, 1);
self.view_dim.0 - max(self.view_dim.0 / 8, self.formatter.single_line_height());
self.view_pos.0 = self.formatter.index_offset_vertical_v2d( self.view_pos.0 = self.formatter.index_offset_vertical_v2d(
&self.buffer, &self.buffer,
self.view_pos.0, self.view_pos.0,

View File

@ -1,17 +1,23 @@
#![allow(dead_code)]
use std::cmp::min; use std::cmp::min;
use ropey::RopeSlice; use ropey::RopeSlice;
use crate::{buffer::Buffer, utils::RopeGraphemes}; use crate::{
buffer::Buffer,
utils::{is_grapheme_boundary, RopeGraphemes},
};
// Maximum graphemes in a line before a soft line break is forced. // Maximum chars in a line before a soft line break is forced.
// This is necessary to prevent pathological formatting cases which // This is necessary to prevent pathological formatting cases which
// could slow down the editor arbitrarily for arbitrarily long // could slow down the editor arbitrarily for arbitrarily long
// lines. // lines.
pub const LINE_BLOCK_LENGTH: usize = 1 << 12; const LINE_BLOCK_LENGTH: usize = 1 << 12;
// A fudge-factor for the above block length, to allow looking for natural
// breaks.
const LINE_BLOCK_FUDGE: usize = 32;
#[allow(dead_code)]
#[derive(Copy, Clone, PartialEq)] #[derive(Copy, Clone, PartialEq)]
pub enum RoundingBehavior { pub enum RoundingBehavior {
Round, Round,
@ -20,8 +26,6 @@ pub enum RoundingBehavior {
} }
pub trait LineFormatter { pub trait LineFormatter {
fn single_line_height(&self) -> usize;
/// Returns the 2d visual dimensions of the given text when formatted /// Returns the 2d visual dimensions of the given text when formatted
/// by the formatter. /// by the formatter.
/// The text to be formatted is passed as a grapheme iterator. /// The text to be formatted is passed as a grapheme iterator.
@ -46,17 +50,17 @@ pub trait LineFormatter {
where where
T: Iterator<Item = RopeSlice<'a>>; T: Iterator<Item = RopeSlice<'a>>;
/// Converts from char index to the horizontal 2d char index.
fn index_to_horizontal_v2d(&self, buf: &Buffer, char_idx: usize) -> usize { fn index_to_horizontal_v2d(&self, buf: &Buffer, char_idx: usize) -> usize {
let (line_i, col_i) = buf.index_to_line_col(char_idx); let (line_i, col_i) = buf.index_to_line_col(char_idx);
let line = buf.get_line(line_i); let line = buf.get_line(line_i);
// Find the right block in the line, and the index within that block // Find the right block in the line, and the index within that block
let (line_block, col_i_adjusted) = block_index_and_offset(col_i); let (_, block_range) = block_index_and_range(&line, col_i);
let col_i_adjusted = col_i - dbg!(block_range).0;
// Get an iter into the right block // Get an iter into the right block
let a = line_block * LINE_BLOCK_LENGTH; let g_iter = RopeGraphemes::new(&line.slice(block_range.0..block_range.1));
let b = min(line.len_chars(), (line_block + 1) * LINE_BLOCK_LENGTH);
let g_iter = RopeGraphemes::new(&line.slice(a..b));
return self.index_to_v2d(g_iter, col_i_adjusted).1; return self.index_to_v2d(g_iter, col_i_adjusted).1;
} }
@ -74,16 +78,14 @@ pub trait LineFormatter {
// Get the line and block index of the given index // Get the line and block index of the given index
let (mut line_i, mut col_i) = buf.index_to_line_col(char_idx); let (mut line_i, mut col_i) = buf.index_to_line_col(char_idx);
let mut line = buf.get_line(line_i);
// Find the right block in the line, and the index within that block // Find the right block in the line, and the index within that block
let (line_block, col_i_adjusted) = block_index_and_offset(col_i); let (line_block, block_range) = block_index_and_range(&line, col_i);
let col_i_adjusted = col_i - block_range.0;
let mut line = buf.get_line(line_i);
let (mut y, x) = self.index_to_v2d( let (mut y, x) = self.index_to_v2d(
RopeGraphemes::new(&line.slice( RopeGraphemes::new(&line.slice(block_range.0..block_range.1)),
(line_block * LINE_BLOCK_LENGTH)
..min(line.len_chars(), (line_block + 1) * LINE_BLOCK_LENGTH),
)),
col_i_adjusted, col_i_adjusted,
); );
@ -93,17 +95,15 @@ pub trait LineFormatter {
let mut block_index: usize = line_block; let mut block_index: usize = line_block;
loop { loop {
line = buf.get_line(line_i); line = buf.get_line(line_i);
let (h, _) = self.dimensions(RopeGraphemes::new(&line.slice( let (block_start, block_end) = char_range_from_block_index(&line, block_index);
(block_index * LINE_BLOCK_LENGTH) let (h, _) = self.dimensions(RopeGraphemes::new(&line.slice(block_start..block_end)));
..min(line.len_chars(), (block_index + 1) * LINE_BLOCK_LENGTH),
)));
if new_y >= 0 && new_y < h as isize { if new_y >= 0 && new_y < h as isize {
y = new_y as usize; y = new_y as usize;
break; break;
} else { } else {
if new_y > 0 { if new_y > 0 {
let is_last_block = block_index >= last_block_index(line.len_chars()); let is_last_block = block_index >= (block_count(&line) - 1);
// Check for off-the-end // Check for off-the-end
if is_last_block && (line_i + 1) >= buf.line_count() { if is_last_block && (line_i + 1) >= buf.line_count() {
@ -126,14 +126,13 @@ pub trait LineFormatter {
if block_index == 0 { if block_index == 0 {
line_i -= 1; line_i -= 1;
line = buf.get_line(line_i); line = buf.get_line(line_i);
block_index = last_block_index(line.len_chars()); block_index = block_count(&line) - 1;
} else { } else {
block_index -= 1; block_index -= 1;
} }
let (h, _) = self.dimensions(RopeGraphemes::new(&line.slice( let (block_start, block_end) = char_range_from_block_index(&line, block_index);
(block_index * LINE_BLOCK_LENGTH) let (h, _) =
..min(line.len_chars(), (block_index + 1) * LINE_BLOCK_LENGTH), self.dimensions(RopeGraphemes::new(&line.slice(block_start..block_end)));
)));
new_y += h as isize; new_y += h as isize;
} else { } else {
unreachable!(); unreachable!();
@ -143,15 +142,13 @@ pub trait LineFormatter {
// Next, convert the resulting coordinates back into buffer-wide // Next, convert the resulting coordinates back into buffer-wide
// coordinates. // coordinates.
let block_slice = line.slice( let (block_start, block_end) = char_range_from_block_index(&line, block_index);
(block_index * LINE_BLOCK_LENGTH) let block_slice = line.slice(block_start..block_end);
..min(line.len_chars(), (block_index + 1) * LINE_BLOCK_LENGTH),
);
let block_col_i = min( let block_col_i = min(
self.v2d_to_index(RopeGraphemes::new(&block_slice), (y, x), rounding), self.v2d_to_index(RopeGraphemes::new(&block_slice), (y, x), rounding),
LINE_BLOCK_LENGTH - 1, LINE_BLOCK_LENGTH - 1,
); );
col_i = (block_index * LINE_BLOCK_LENGTH) + block_col_i; col_i = block_start + block_col_i;
return buf.line_col_to_index((line_i, col_i)); return buf.line_col_to_index((line_i, col_i));
} }
@ -170,21 +167,20 @@ pub trait LineFormatter {
let line = buf.get_line(line_i); let line = buf.get_line(line_i);
// Find the right block in the line, and the index within that block // Find the right block in the line, and the index within that block
let (line_block, col_i_adjusted) = block_index_and_offset(col_i); let (_, block_range) = block_index_and_range(&line, col_i);
let start_index = line_block * LINE_BLOCK_LENGTH; let col_i_adjusted = col_i - block_range.0;
let end_index = min(line.len_chars(), start_index + LINE_BLOCK_LENGTH);
// Calculate the horizontal position // Calculate the horizontal position
let (v, _) = self.index_to_v2d( let (v, _) = self.index_to_v2d(
RopeGraphemes::new(&line.slice(start_index..end_index)), RopeGraphemes::new(&line.slice(block_range.0..block_range.1)),
col_i_adjusted, col_i_adjusted,
); );
let block_col_i = self.v2d_to_index( let block_col_i = self.v2d_to_index(
RopeGraphemes::new(&line.slice(start_index..end_index)), RopeGraphemes::new(&line.slice(block_range.0..block_range.1)),
(v, horizontal), (v, horizontal),
(RoundingBehavior::Floor, rounding), (RoundingBehavior::Floor, rounding),
); );
let mut new_col_i = start_index + min(block_col_i, LINE_BLOCK_LENGTH - 1); let mut new_col_i = block_range.0 + min(block_col_i, LINE_BLOCK_LENGTH - 1);
// Make sure we're not pushing the index off the end of the line // Make sure we're not pushing the index off the end of the line
if (line_i + 1) < buf.line_count() && new_col_i >= line.len_chars() && line.len_chars() > 0 if (line_i + 1) < buf.line_count() && new_col_i >= line.len_chars() && line.len_chars() > 0
@ -196,19 +192,75 @@ pub trait LineFormatter {
} }
} }
pub fn block_index_and_offset(index: usize) -> (usize, usize) { // Finds the best break at or before the given char index, bounded by
(index / LINE_BLOCK_LENGTH, index % LINE_BLOCK_LENGTH) // the given `lower_limit`.
pub fn find_good_break(slice: &RopeSlice, lower_limit: usize, char_idx: usize) -> usize {
let char_idx = char_idx.min(slice.len_chars());
let lower_limit = lower_limit.min(slice.len_chars());
// Find a whitespace break, if any.
let mut i = char_idx;
while i > lower_limit {
match slice.char(i - 1) {
// Previous char is whitespace.
'\u{0009}' | '\u{0020}' | '\u{00A0}' | '\u{2000}' | '\u{2001}' | '\u{2002}'
| '\u{2003}' | '\u{2004}' | '\u{2005}' | '\u{2006}' | '\u{2007}' | '\u{2008}'
| '\u{2009}' | '\u{200A}' | '\u{202F}' | '\u{205F}' | '\u{3000}' => {
return i;
} }
pub fn last_block_index(gc: usize) -> usize { // Previous char is not whitespace.
let mut block_count = gc / LINE_BLOCK_LENGTH; _ => {
if (gc % LINE_BLOCK_LENGTH) > 0 { i -= 1;
block_count += 1; }
}
} }
if block_count > 0 { // Otherwise, at least try to find a grapheme break.
return block_count - 1; let mut i = char_idx;
while i > lower_limit && !is_grapheme_boundary(slice, i) {
i -= 1;
}
if i > lower_limit {
i
} else { } else {
return 0; char_idx
} }
} }
pub fn char_range_from_block_index(slice: &RopeSlice, block_idx: usize) -> (usize, usize) {
let start = {
let initial = LINE_BLOCK_LENGTH * block_idx;
find_good_break(slice, initial - LINE_BLOCK_FUDGE.min(initial), initial)
};
let end = {
let initial = LINE_BLOCK_LENGTH * (block_idx + 1);
find_good_break(slice, initial - LINE_BLOCK_FUDGE.min(initial), initial)
};
(start, end)
}
pub fn block_index_and_range(slice: &RopeSlice, char_idx: usize) -> (usize, (usize, usize)) {
let mut block_index = char_idx / LINE_BLOCK_LENGTH;
let mut range = char_range_from_block_index(slice, block_index);
if char_idx >= range.1 && range.1 < slice.len_chars() {
block_index += 1;
range = char_range_from_block_index(slice, block_index);
}
(block_index, range)
}
pub fn block_count(slice: &RopeSlice) -> usize {
let char_count = slice.len_chars();
let mut last_idx = (char_count - 1.min(char_count)) / LINE_BLOCK_LENGTH;
let range = char_range_from_block_index(slice, last_idx + 1);
if range.0 < range.1 {
last_idx += 1;
}
last_idx + 1
}

View File

@ -66,10 +66,6 @@ impl ConsoleLineFormatter {
} }
impl LineFormatter for ConsoleLineFormatter { impl LineFormatter for ConsoleLineFormatter {
fn single_line_height(&self) -> usize {
return 1;
}
fn dimensions<'a, T>(&'a self, g_iter: T) -> (usize, usize) fn dimensions<'a, T>(&'a self, g_iter: T) -> (usize, usize)
where where
T: Iterator<Item = RopeSlice<'a>>, T: Iterator<Item = RopeSlice<'a>>,
@ -80,7 +76,7 @@ impl LineFormatter for ConsoleLineFormatter {
dim = (max(dim.0, pos.0), max(dim.1, pos.1 + width)); dim = (max(dim.0, pos.0), max(dim.1, pos.1 + width));
} }
dim.0 += self.single_line_height(); dim.0 += 1;
return dim; return dim;
} }
@ -180,24 +176,15 @@ where
} }
if self.f.maintain_indent { if self.f.maintain_indent {
let pos = ( let pos = (self.pos.0 + 1, self.indent + self.f.wrap_additional_indent);
self.pos.0 + self.f.single_line_height(),
self.indent + self.f.wrap_additional_indent,
);
self.pos = ( self.pos = (
self.pos.0 + self.f.single_line_height(), self.pos.0 + 1,
self.indent + self.f.wrap_additional_indent + width, self.indent + self.f.wrap_additional_indent + width,
); );
return Some((g, pos, width)); return Some((g, pos, width));
} else { } else {
let pos = ( let pos = (self.pos.0 + 1, self.f.wrap_additional_indent);
self.pos.0 + self.f.single_line_height(), self.pos = (self.pos.0 + 1, self.f.wrap_additional_indent + width);
self.f.wrap_additional_indent,
);
self.pos = (
self.pos.0 + self.f.single_line_height(),
self.f.wrap_additional_indent + width,
);
return Some((g, pos, width)); return Some((g, pos, width));
} }
} else { } else {
@ -273,15 +260,10 @@ where
if self.pos.1 > 0 { if self.pos.1 > 0 {
if self.f.maintain_indent { if self.f.maintain_indent {
self.pos = ( self.pos =
self.pos.0 + self.f.single_line_height(), (self.pos.0 + 1, self.indent + self.f.wrap_additional_indent);
self.indent + self.f.wrap_additional_indent,
);
} else { } else {
self.pos = ( self.pos = (self.pos.0 + 1, self.f.wrap_additional_indent);
self.pos.0 + self.f.single_line_height(),
self.f.wrap_additional_indent,
);
} }
} }
} }
@ -321,8 +303,8 @@ mod tests {
use ropey::Rope; use ropey::Rope;
use crate::buffer::Buffer; use crate::buffer::Buffer;
use crate::formatter::LineFormatter;
use crate::formatter::RoundingBehavior::{Ceiling, Floor, Round}; use crate::formatter::RoundingBehavior::{Ceiling, Floor, Round};
use crate::formatter::{LineFormatter, LINE_BLOCK_LENGTH};
use crate::utils::RopeGraphemes; use crate::utils::RopeGraphemes;
use super::*; use super::*;
@ -355,6 +337,19 @@ mod tests {
#[test] #[test]
fn dimensions_3() { fn dimensions_3() {
let text = Rope::from_str("Hello there, stranger! How are you doing this fine day?"); // 56 graphemes long
let mut f = ConsoleLineFormatter::new(4);
f.wrap_type = WrapType::WordWrap(0);
f.maintain_indent = false;
f.wrap_additional_indent = 0;
f.set_wrap_width(12);
assert_eq!(f.dimensions(RopeGraphemes::new(&text.slice(..))), (6, 12));
}
#[test]
fn dimensions_4() {
// 55 graphemes long // 55 graphemes long
let text = Rope::from_str( let text = Rope::from_str(
"税マイミ文末\ "税マイミ文末\
@ -379,7 +374,7 @@ mod tests {
} }
#[test] #[test]
fn dimensions_4() { fn dimensions_5() {
// 55 graphemes long // 55 graphemes long
let text = Rope::from_str( let text = Rope::from_str(
"税マイミ文末\ "税マイミ文末\
@ -672,6 +667,36 @@ mod tests {
assert_eq!(f.index_to_horizontal_v2d(&b, 56), 8); assert_eq!(f.index_to_horizontal_v2d(&b, 56), 8);
} }
#[test]
fn index_to_horizontal_v2d_3() {
let b = Buffer::new_from_str("Hello there, stranger!\nHow are you doing this fine day?"); // 55 graphemes long
let mut f = ConsoleLineFormatter::new(4);
f.wrap_type = WrapType::WordWrap(0);
f.maintain_indent = false;
f.wrap_additional_indent = 0;
f.set_wrap_width(12);
assert_eq!(f.index_to_horizontal_v2d(&b, 0), 0);
assert_eq!(f.index_to_horizontal_v2d(&b, 5), 5);
assert_eq!(f.index_to_horizontal_v2d(&b, 6), 0);
assert_eq!(f.index_to_horizontal_v2d(&b, 12), 6);
assert_eq!(f.index_to_horizontal_v2d(&b, 13), 0);
assert_eq!(f.index_to_horizontal_v2d(&b, 22), 9);
assert_eq!(f.index_to_horizontal_v2d(&b, 23), 0);
assert_eq!(f.index_to_horizontal_v2d(&b, 34), 11);
assert_eq!(f.index_to_horizontal_v2d(&b, 35), 0);
assert_eq!(f.index_to_horizontal_v2d(&b, 45), 10);
assert_eq!(f.index_to_horizontal_v2d(&b, 46), 0);
assert_eq!(f.index_to_horizontal_v2d(&b, 55), 9);
assert_eq!(f.index_to_horizontal_v2d(&b, 56), 9);
}
#[test] #[test]
fn index_set_horizontal_v2d_1() { fn index_set_horizontal_v2d_1() {
let b = Buffer::new_from_str("Hello there, stranger!\nHow are you doing this fine day?"); // 55 graphemes long let b = Buffer::new_from_str("Hello there, stranger!\nHow are you doing this fine day?"); // 55 graphemes long

View File

@ -13,7 +13,7 @@ use crossterm::{
use crate::{ use crate::{
editor::Editor, editor::Editor,
formatter::{block_index_and_offset, LineFormatter, LINE_BLOCK_LENGTH}, formatter::{block_count, block_index_and_range, char_range_from_block_index, LineFormatter},
string_utils::{line_ending_to_str, rope_slice_is_line_ending, LineEnding}, string_utils::{line_ending_to_str, rope_slice_is_line_ending, LineEnding},
utils::{digit_count, RopeGraphemes}, utils::{digit_count, RopeGraphemes},
}; };
@ -487,19 +487,11 @@ impl TermUI {
// Calculate all the starting info // Calculate all the starting info
let gutter_width = editor.editor_dim.1 - editor.view_dim.1; let gutter_width = editor.editor_dim.1 - editor.view_dim.1;
let (line_index, col_i) = editor.buffer.index_to_line_col(editor.view_pos.0); let (line_index, col_i) = editor.buffer.index_to_line_col(editor.view_pos.0);
let (mut line_block_index, _) = block_index_and_offset(col_i);
let mut char_index = editor
.buffer
.line_col_to_index((line_index, line_block_index * LINE_BLOCK_LENGTH));
let temp_line = editor.buffer.get_line(line_index); let temp_line = editor.buffer.get_line(line_index);
let (mut line_block_index, block_range) = block_index_and_range(&temp_line, col_i);
let mut char_index = editor.buffer.line_col_to_index((line_index, block_range.0));
let (vis_line_offset, _) = editor.formatter.index_to_v2d( let (vis_line_offset, _) = editor.formatter.index_to_v2d(
RopeGraphemes::new(&temp_line.slice( RopeGraphemes::new(&temp_line.slice(block_range.0..block_range.1)),
(line_block_index * LINE_BLOCK_LENGTH)
..min(
temp_line.len_chars(),
(line_block_index + 1) * LINE_BLOCK_LENGTH,
),
)),
editor.view_pos.0 - char_index, editor.view_pos.0 - char_index,
); );
@ -527,16 +519,16 @@ impl TermUI {
// Loop through the graphemes of the line and print them to // Loop through the graphemes of the line and print them to
// the screen. // the screen.
let mut line_g_index: usize = 0; let max_block_index = block_count(&line) - 1;
let mut last_pos_y = 0; let block_range = char_range_from_block_index(&line, line_block_index);
let mut lines_traversed: usize = 0;
let line_len = line.len_chars();
let mut g_iter = editor.formatter.iter(RopeGraphemes::new( let mut g_iter = editor.formatter.iter(RopeGraphemes::new(
&line.slice((line_block_index * LINE_BLOCK_LENGTH)..line_len), &line.slice(block_range.0..block_range.1),
)); ));
let mut last_pos_y = 0;
let mut lines_traversed: usize = 0;
loop { loop {
if let Some((g, (pos_y, pos_x), width)) = g_iter.next() { for (g, (pos_y, pos_x), width) in g_iter {
if last_pos_y != pos_y { if last_pos_y != pos_y {
if last_pos_y < pos_y { if last_pos_y < pos_y {
lines_traversed += pos_y - last_pos_y; lines_traversed += pos_y - last_pos_y;
@ -600,19 +592,17 @@ impl TermUI {
} }
char_index += g.chars().count(); char_index += g.chars().count();
line_g_index += 1;
} else {
break;
} }
if line_g_index >= LINE_BLOCK_LENGTH { // Move on to the next block.
line_block_index += 1; line_block_index += 1;
line_g_index = 0; if line_block_index <= max_block_index {
let line_len = line.len_chars(); let block_range = char_range_from_block_index(&line, line_block_index);
g_iter = editor.formatter.iter(RopeGraphemes::new( g_iter = editor.formatter.iter(RopeGraphemes::new(
&line.slice((line_block_index * LINE_BLOCK_LENGTH)..line_len), &line.slice(block_range.0..block_range.1),
)); ));
lines_traversed += 1; } else {
break;
} }
} }