From ebfedab58cb0b72c02f584f3ca674075dac4325c Mon Sep 17 00:00:00 2001 From: Nathan Vegdahl Date: Sat, 1 Feb 2020 22:32:09 +0900 Subject: [PATCH] Change how line chunking works, to favor breaking on whitespace. --- src/editor/mod.rs | 10 ++- src/formatter.rs | 148 ++++++++++++++++++++++++++------------- src/term_ui/formatter.rs | 81 +++++++++++++-------- src/term_ui/mod.rs | 44 +++++------- 4 files changed, 174 insertions(+), 109 deletions(-) diff --git a/src/editor/mod.rs b/src/editor/mod.rs index a08d5f9..a60b7ac 100644 --- a/src/editor/mod.rs +++ b/src/editor/mod.rs @@ -553,7 +553,7 @@ impl Editor { pub fn cursor_up(&mut self, n: usize) { 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( &self.buffer, @@ -589,7 +589,7 @@ impl Editor { pub fn cursor_down(&mut self, n: usize) { 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( &self.buffer, @@ -624,8 +624,7 @@ impl Editor { } pub fn page_up(&mut self) { - let move_amount = - self.view_dim.0 - max(self.view_dim.0 / 8, self.formatter.single_line_height()); + let move_amount = self.view_dim.0 - max(self.view_dim.0 / 8, 1); self.view_pos.0 = self.formatter.index_offset_vertical_v2d( &self.buffer, self.view_pos.0, @@ -640,8 +639,7 @@ impl Editor { } pub fn page_down(&mut self) { - let move_amount = - self.view_dim.0 - max(self.view_dim.0 / 8, self.formatter.single_line_height()); + let move_amount = self.view_dim.0 - max(self.view_dim.0 / 8, 1); self.view_pos.0 = self.formatter.index_offset_vertical_v2d( &self.buffer, self.view_pos.0, diff --git a/src/formatter.rs b/src/formatter.rs index d5d2209..79f62c6 100644 --- a/src/formatter.rs +++ b/src/formatter.rs @@ -1,17 +1,23 @@ -#![allow(dead_code)] - use std::cmp::min; 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 // could slow down the editor arbitrarily for arbitrarily long // 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)] pub enum RoundingBehavior { Round, @@ -20,8 +26,6 @@ pub enum RoundingBehavior { } pub trait LineFormatter { - fn single_line_height(&self) -> usize; - /// Returns the 2d visual dimensions of the given text when formatted /// by the formatter. /// The text to be formatted is passed as a grapheme iterator. @@ -46,17 +50,17 @@ pub trait LineFormatter { where T: Iterator>; + /// Converts from char index to the horizontal 2d char index. 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 = buf.get_line(line_i); // 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 - let a = line_block * LINE_BLOCK_LENGTH; - let b = min(line.len_chars(), (line_block + 1) * LINE_BLOCK_LENGTH); - let g_iter = RopeGraphemes::new(&line.slice(a..b)); + let g_iter = RopeGraphemes::new(&line.slice(block_range.0..block_range.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 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 - 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( - RopeGraphemes::new(&line.slice( - (line_block * LINE_BLOCK_LENGTH) - ..min(line.len_chars(), (line_block + 1) * LINE_BLOCK_LENGTH), - )), + RopeGraphemes::new(&line.slice(block_range.0..block_range.1)), col_i_adjusted, ); @@ -93,17 +95,15 @@ pub trait LineFormatter { let mut block_index: usize = line_block; loop { line = buf.get_line(line_i); - let (h, _) = self.dimensions(RopeGraphemes::new(&line.slice( - (block_index * LINE_BLOCK_LENGTH) - ..min(line.len_chars(), (block_index + 1) * LINE_BLOCK_LENGTH), - ))); + let (block_start, block_end) = char_range_from_block_index(&line, block_index); + let (h, _) = self.dimensions(RopeGraphemes::new(&line.slice(block_start..block_end))); if new_y >= 0 && new_y < h as isize { y = new_y as usize; break; } else { 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 if is_last_block && (line_i + 1) >= buf.line_count() { @@ -126,14 +126,13 @@ pub trait LineFormatter { if block_index == 0 { line_i -= 1; line = buf.get_line(line_i); - block_index = last_block_index(line.len_chars()); + block_index = block_count(&line) - 1; } else { block_index -= 1; } - let (h, _) = self.dimensions(RopeGraphemes::new(&line.slice( - (block_index * LINE_BLOCK_LENGTH) - ..min(line.len_chars(), (block_index + 1) * LINE_BLOCK_LENGTH), - ))); + let (block_start, block_end) = char_range_from_block_index(&line, block_index); + let (h, _) = + self.dimensions(RopeGraphemes::new(&line.slice(block_start..block_end))); new_y += h as isize; } else { unreachable!(); @@ -143,15 +142,13 @@ pub trait LineFormatter { // Next, convert the resulting coordinates back into buffer-wide // coordinates. - let block_slice = line.slice( - (block_index * LINE_BLOCK_LENGTH) - ..min(line.len_chars(), (block_index + 1) * LINE_BLOCK_LENGTH), - ); + let (block_start, block_end) = char_range_from_block_index(&line, block_index); + let block_slice = line.slice(block_start..block_end); let block_col_i = min( self.v2d_to_index(RopeGraphemes::new(&block_slice), (y, x), rounding), 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)); } @@ -170,21 +167,20 @@ pub trait LineFormatter { let line = buf.get_line(line_i); // 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 start_index = line_block * LINE_BLOCK_LENGTH; - let end_index = min(line.len_chars(), start_index + LINE_BLOCK_LENGTH); + let (_, block_range) = block_index_and_range(&line, col_i); + let col_i_adjusted = col_i - block_range.0; // Calculate the horizontal position 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, ); 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), (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 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) { - (index / LINE_BLOCK_LENGTH, index % LINE_BLOCK_LENGTH) -} +// Finds the best break at or before the given char index, bounded by +// 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()); -pub fn last_block_index(gc: usize) -> usize { - let mut block_count = gc / LINE_BLOCK_LENGTH; - if (gc % LINE_BLOCK_LENGTH) > 0 { - block_count += 1; + // 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; + } + + // Previous char is not whitespace. + _ => { + i -= 1; + } + } } - if block_count > 0 { - return block_count - 1; + // Otherwise, at least try to find a grapheme break. + let mut i = char_idx; + while i > lower_limit && !is_grapheme_boundary(slice, i) { + i -= 1; + } + + if i > lower_limit { + i } 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 +} diff --git a/src/term_ui/formatter.rs b/src/term_ui/formatter.rs index 513b43b..211b0db 100644 --- a/src/term_ui/formatter.rs +++ b/src/term_ui/formatter.rs @@ -66,10 +66,6 @@ impl ConsoleLineFormatter { } impl LineFormatter for ConsoleLineFormatter { - fn single_line_height(&self) -> usize { - return 1; - } - fn dimensions<'a, T>(&'a self, g_iter: T) -> (usize, usize) where T: Iterator>, @@ -80,7 +76,7 @@ impl LineFormatter for ConsoleLineFormatter { dim = (max(dim.0, pos.0), max(dim.1, pos.1 + width)); } - dim.0 += self.single_line_height(); + dim.0 += 1; return dim; } @@ -180,24 +176,15 @@ where } if self.f.maintain_indent { - let pos = ( - self.pos.0 + self.f.single_line_height(), - self.indent + self.f.wrap_additional_indent, - ); + let pos = (self.pos.0 + 1, self.indent + self.f.wrap_additional_indent); self.pos = ( - self.pos.0 + self.f.single_line_height(), + self.pos.0 + 1, self.indent + self.f.wrap_additional_indent + width, ); return Some((g, pos, width)); } else { - let pos = ( - self.pos.0 + self.f.single_line_height(), - self.f.wrap_additional_indent, - ); - self.pos = ( - self.pos.0 + self.f.single_line_height(), - self.f.wrap_additional_indent + width, - ); + let pos = (self.pos.0 + 1, self.f.wrap_additional_indent); + self.pos = (self.pos.0 + 1, self.f.wrap_additional_indent + width); return Some((g, pos, width)); } } else { @@ -273,15 +260,10 @@ where if self.pos.1 > 0 { if self.f.maintain_indent { - self.pos = ( - self.pos.0 + self.f.single_line_height(), - self.indent + self.f.wrap_additional_indent, - ); + self.pos = + (self.pos.0 + 1, self.indent + self.f.wrap_additional_indent); } else { - self.pos = ( - self.pos.0 + self.f.single_line_height(), - self.f.wrap_additional_indent, - ); + self.pos = (self.pos.0 + 1, self.f.wrap_additional_indent); } } } @@ -321,8 +303,8 @@ mod tests { use ropey::Rope; use crate::buffer::Buffer; + use crate::formatter::LineFormatter; use crate::formatter::RoundingBehavior::{Ceiling, Floor, Round}; - use crate::formatter::{LineFormatter, LINE_BLOCK_LENGTH}; use crate::utils::RopeGraphemes; use super::*; @@ -355,6 +337,19 @@ mod tests { #[test] 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 let text = Rope::from_str( "税マイミ文末\ @@ -379,7 +374,7 @@ mod tests { } #[test] - fn dimensions_4() { + fn dimensions_5() { // 55 graphemes long let text = Rope::from_str( "税マイミ文末\ @@ -672,6 +667,36 @@ mod tests { 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] 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 diff --git a/src/term_ui/mod.rs b/src/term_ui/mod.rs index 5e85a59..ae9d701 100644 --- a/src/term_ui/mod.rs +++ b/src/term_ui/mod.rs @@ -13,7 +13,7 @@ use crossterm::{ use crate::{ 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}, utils::{digit_count, RopeGraphemes}, }; @@ -487,19 +487,11 @@ impl TermUI { // Calculate all the starting info 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 (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 (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( - RopeGraphemes::new(&temp_line.slice( - (line_block_index * LINE_BLOCK_LENGTH) - ..min( - temp_line.len_chars(), - (line_block_index + 1) * LINE_BLOCK_LENGTH, - ), - )), + RopeGraphemes::new(&temp_line.slice(block_range.0..block_range.1)), editor.view_pos.0 - char_index, ); @@ -527,16 +519,16 @@ impl TermUI { // Loop through the graphemes of the line and print them to // the screen. - let mut line_g_index: usize = 0; - let mut last_pos_y = 0; - let mut lines_traversed: usize = 0; - let line_len = line.len_chars(); + let max_block_index = block_count(&line) - 1; + let block_range = char_range_from_block_index(&line, line_block_index); 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 { - 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 { lines_traversed += pos_y - last_pos_y; @@ -600,19 +592,17 @@ impl TermUI { } char_index += g.chars().count(); - line_g_index += 1; - } else { - break; } - if line_g_index >= LINE_BLOCK_LENGTH { - line_block_index += 1; - line_g_index = 0; - let line_len = line.len_chars(); + // Move on to the next block. + line_block_index += 1; + if line_block_index <= max_block_index { + let block_range = char_range_from_block_index(&line, line_block_index); 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; } }