From 19adb081700ea471ab4f4cb10dee6d7834f65ea2 Mon Sep 17 00:00:00 2001 From: Nathan Vegdahl Date: Tue, 11 Feb 2020 20:24:34 +0900 Subject: [PATCH] Reworking the text formatting code. It's a mess of indirection and over-abstraction, and this commit is the first step at cleaning that up. In addition to making the code easier to follow, it's also notably faster. The only downside is we've (temporarily) lost indentation continuation on line wrapping. But that can be added back without too much trouble later. --- src/formatter.rs | 18 +-- src/string_utils.rs | 6 +- src/term_ui/formatter.rs | 262 +++++++++++++-------------------------- src/term_ui/mod.rs | 18 +-- src/term_ui/screen.rs | 28 +---- src/utils.rs | 10 +- 6 files changed, 107 insertions(+), 235 deletions(-) diff --git a/src/formatter.rs b/src/formatter.rs index e6fba44..077deb9 100644 --- a/src/formatter.rs +++ b/src/formatter.rs @@ -29,26 +29,20 @@ pub trait LineFormatter { /// 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. - fn dimensions<'a, T>(&'a self, g_iter: T) -> (usize, usize) - where - T: Iterator>; + fn dimensions(&self, g_iter: RopeGraphemes) -> (usize, usize); /// Converts a char index within a text into a visual 2d position. /// The text to be formatted is passed as a grapheme iterator. - fn index_to_v2d<'a, T>(&'a self, g_iter: T, char_idx: usize) -> (usize, usize) - where - T: Iterator>; + fn index_to_v2d(&self, g_iter: RopeGraphemes, char_idx: usize) -> (usize, usize); /// Converts a visual 2d position into a char index within a text. /// The text to be formatted is passed as a grapheme iterator. - fn v2d_to_index<'a, T>( - &'a self, - g_iter: T, + fn v2d_to_index( + &self, + g_iter: RopeGraphemes, v2d: (usize, usize), rounding: (RoundingBehavior, RoundingBehavior), - ) -> usize - where - T: Iterator>; + ) -> usize; /// Converts from char index to the horizontal 2d char index. fn index_to_horizontal_v2d(&self, buf: &Buffer, char_idx: usize) -> usize { diff --git a/src/string_utils.rs b/src/string_utils.rs index 26b3d73..eeabb8e 100644 --- a/src/string_utils.rs +++ b/src/string_utils.rs @@ -16,7 +16,11 @@ pub fn is_line_ending(text: &str) -> bool { } pub fn rope_slice_is_line_ending(text: &RopeSlice) -> bool { - rope_slice_to_line_ending(text) != LineEnding::None + match text.char(0) { + c if (c >= '\u{000A}' && c <= '\u{000D}') => true, + '\u{0085}' | '\u{2028}' | '\u{2029}' => true, + _ => false, + } } pub fn is_whitespace(text: &str) -> bool { diff --git a/src/term_ui/formatter.rs b/src/term_ui/formatter.rs index 211b0db..d91d9ae 100644 --- a/src/term_ui/formatter.rs +++ b/src/term_ui/formatter.rs @@ -1,11 +1,9 @@ -use std::cmp::max; - -use ropey::RopeSlice; +use std::{borrow::Cow, cmp::max}; use crate::{ formatter::{LineFormatter, RoundingBehavior}, - string_utils::{rope_slice_is_line_ending, rope_slice_is_whitespace}, - utils::grapheme_width, + string_utils::{is_line_ending, is_whitespace}, + utils::{grapheme_width, RopeGraphemes}, }; pub enum WrapType { @@ -49,27 +47,24 @@ impl ConsoleLineFormatter { } } - pub fn iter<'a, T>(&'a self, g_iter: T) -> ConsoleLineFormatterVisIter<'a, T> - where - T: Iterator>, - { - ConsoleLineFormatterVisIter::<'a, T> { - grapheme_iter: g_iter, - f: self, - pos: (0, 0), - indent: 0, - indent_found: false, + pub fn iter<'a>(&self, g_iter: RopeGraphemes<'a>) -> FormattingIter<'a> { + FormattingIter { + grapheme_itr: g_iter, + wrap_width: match self.wrap_type { + WrapType::WordWrap(w) => w, + WrapType::CharWrap(w) => w, + WrapType::NoWrap => unreachable!(), + }, + tab_width: self.tab_width as usize, word_buf: Vec::new(), word_i: 0, + pos: (0, 0), } } } impl LineFormatter for ConsoleLineFormatter { - fn dimensions<'a, T>(&'a self, g_iter: T) -> (usize, usize) - where - T: Iterator>, - { + fn dimensions(&self, g_iter: RopeGraphemes) -> (usize, usize) { let mut dim: (usize, usize) = (0, 0); for (_, pos, width) in self.iter(g_iter) { @@ -81,10 +76,7 @@ impl LineFormatter for ConsoleLineFormatter { return dim; } - fn index_to_v2d<'a, T>(&'a self, g_iter: T, char_idx: usize) -> (usize, usize) - where - T: Iterator>, - { + fn index_to_v2d(&self, g_iter: RopeGraphemes, char_idx: usize) -> (usize, usize) { let mut pos = (0, 0); let mut i = 0; let mut last_width = 0; @@ -102,15 +94,12 @@ impl LineFormatter for ConsoleLineFormatter { return (pos.0, pos.1 + last_width); } - fn v2d_to_index<'a, T>( - &'a self, - g_iter: T, + fn v2d_to_index( + &self, + g_iter: RopeGraphemes, v2d: (usize, usize), _: (RoundingBehavior, RoundingBehavior), - ) -> usize - where - T: Iterator>, - { + ) -> usize { // TODO: handle rounding modes let mut prev_i = 0; let mut i = 0; @@ -131,172 +120,97 @@ impl LineFormatter for ConsoleLineFormatter { } } -// =================================================================== -// An iterator that iterates over the graphemes in a line in a -// manner consistent with the ConsoleFormatter. -// =================================================================== -pub struct ConsoleLineFormatterVisIter<'a, T> -where - T: Iterator>, -{ - grapheme_iter: T, - f: &'a ConsoleLineFormatter, - pos: (usize, usize), +//-------------------------------------------------------------------------- - indent: usize, - indent_found: bool, +/// An iterator over the visual printable characters of a piece of text, +/// yielding the text of the character, its position in 2d space, and its +/// visial width. +/// +/// TODO: handle maintaining indent, etc. +pub struct FormattingIter<'a> { + grapheme_itr: RopeGraphemes<'a>, + wrap_width: usize, + tab_width: usize, - word_buf: Vec>, + word_buf: Vec<(Cow<'a, str>, usize)>, // Printable character and its width. word_i: usize, + + pos: (usize, usize), } -impl<'a, T> ConsoleLineFormatterVisIter<'a, T> -where - T: Iterator>, -{ - fn next_nowrap(&mut self, g: RopeSlice<'a>) -> Option<(RopeSlice<'a>, (usize, usize), usize)> { - let width = grapheme_vis_width_at_vis_pos(g, self.pos.1, self.f.tab_width as usize); +impl<'a> Iterator for FormattingIter<'a> { + type Item = (Cow<'a, str>, (usize, usize), usize); + fn next(&mut self) -> Option { + // Get next word if necessary + if self.word_i >= self.word_buf.len() { + let mut word_width = 0; + self.word_buf.truncate(0); + + while let Some(g) = self.grapheme_itr.next().map(|g| Cow::::from(g)) { + let width = + grapheme_vis_width_at_vis_pos(&g, self.pos.1 + word_width, self.tab_width); + self.word_buf.push((g.clone(), width)); + word_width += width; + + if is_whitespace(&g) { + break; + } + } + + if self.word_buf.len() == 0 { + return None; + } + + // Move to next line if necessary + if (self.pos.1 + word_width) > self.wrap_width { + if self.pos.1 > 0 { + self.pos = (self.pos.0 + 1, 0); + } + } + + self.word_i = 0; + } + + // Get next grapheme and width from the current word. + let (g, g_width) = { + let (ref g, mut width) = self.word_buf[self.word_i]; + if g == "\t" { + width = grapheme_vis_width_at_vis_pos(&g, self.pos.1, self.tab_width); + } + (g, width) + }; + + // Get our character's position and update the position for the next + // grapheme. + if (self.pos.1 + g_width) > self.wrap_width && self.pos.1 > 0 { + self.pos.0 += 1; + self.pos.1 = 0; + } let pos = self.pos; - self.pos = (self.pos.0, self.pos.1 + width); - return Some((g, pos, width)); - } + self.pos.1 += g_width; - fn next_charwrap( - &mut self, - g: RopeSlice<'a>, - wrap_width: usize, - ) -> Option<(RopeSlice<'a>, (usize, usize), usize)> { - let width = grapheme_vis_width_at_vis_pos(g, self.pos.1, self.f.tab_width as usize); - - if (self.pos.1 + width) > wrap_width { - if !self.indent_found { - self.indent = 0; - self.indent_found = true; - } - - if self.f.maintain_indent { - let pos = (self.pos.0 + 1, self.indent + self.f.wrap_additional_indent); - self.pos = ( - self.pos.0 + 1, - self.indent + self.f.wrap_additional_indent + width, - ); - return Some((g, pos, width)); - } else { - 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 { - if !self.indent_found { - if rope_slice_is_whitespace(&g) { - self.indent += width; - } else { - self.indent_found = true; - } - } - - let pos = self.pos; - self.pos = (self.pos.0, self.pos.1 + width); - return Some((g, pos, width)); - } + // Increment index and return. + self.word_i += 1; + return Some((g.clone(), pos, g_width)); } } -impl<'a, T> Iterator for ConsoleLineFormatterVisIter<'a, T> -where - T: Iterator>, -{ - type Item = (RopeSlice<'a>, (usize, usize), usize); - - fn next(&mut self) -> Option<(RopeSlice<'a>, (usize, usize), usize)> { - match self.f.wrap_type { - WrapType::NoWrap => { - if let Some(g) = self.grapheme_iter.next() { - return self.next_nowrap(g); - } else { - return None; - } - } - - WrapType::CharWrap(wrap_width) => { - if let Some(g) = self.grapheme_iter.next() { - return self.next_charwrap(g, wrap_width); - } else { - return None; - } - } - - WrapType::WordWrap(wrap_width) => { - // Get next word if necessary - if self.word_i >= self.word_buf.len() { - let mut word_width = 0; - self.word_buf.truncate(0); - while let Some(g) = self.grapheme_iter.next() { - self.word_buf.push(g); - let width = grapheme_vis_width_at_vis_pos( - g, - self.pos.1 + word_width, - self.f.tab_width as usize, - ); - word_width += width; - if rope_slice_is_whitespace(&g) { - break; - } - } - - if self.word_buf.len() == 0 { - return None; - } else if !self.indent_found && !rope_slice_is_whitespace(&self.word_buf[0]) { - self.indent_found = true; - } - - // Move to next line if necessary - if (self.pos.1 + word_width) > wrap_width { - if !self.indent_found { - self.indent = 0; - self.indent_found = true; - } - - if self.pos.1 > 0 { - if self.f.maintain_indent { - self.pos = - (self.pos.0 + 1, self.indent + self.f.wrap_additional_indent); - } else { - self.pos = (self.pos.0 + 1, self.f.wrap_additional_indent); - } - } - } - - self.word_i = 0; - } - - // Iterate over the word - let g = self.word_buf[self.word_i]; - self.word_i += 1; - return self.next_charwrap(g, wrap_width); - } - } - } -} - -// =================================================================== -// Helper functions -// =================================================================== - /// Returns the visual width of a grapheme given a starting /// position on a line. -fn grapheme_vis_width_at_vis_pos(g: RopeSlice, pos: usize, tab_width: usize) -> usize { +fn grapheme_vis_width_at_vis_pos(g: &str, pos: usize, tab_width: usize) -> usize { if g == "\t" { let ending_pos = ((pos / tab_width) + 1) * tab_width; return ending_pos - pos; - } else if rope_slice_is_line_ending(&g) { + } else if is_line_ending(g) { return 1; } else { return grapheme_width(&g); } } +//-------------------------------------------------------------------------- + #[cfg(test)] mod tests { #![allow(unused_imports)] diff --git a/src/term_ui/mod.rs b/src/term_ui/mod.rs index ac0744a..4d69942 100644 --- a/src/term_ui/mod.rs +++ b/src/term_ui/mod.rs @@ -14,7 +14,7 @@ use crossterm::{ use crate::{ editor::Editor, 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::{is_line_ending, line_ending_to_str, LineEnding}, utils::{digit_count, RopeGraphemes, Timer}, }; @@ -568,7 +568,7 @@ impl TermUI { } // Actually print the character - if rope_slice_is_line_ending(&g) { + if is_line_ending(&g) { if at_cursor { self.screen .draw(px as usize, py as usize, " ", STYLE_CURSOR); @@ -587,19 +587,9 @@ impl TermUI { } } else { if at_cursor { - self.screen.draw_rope_slice( - px as usize, - py as usize, - &g, - STYLE_CURSOR, - ); + self.screen.draw(px as usize, py as usize, &g, STYLE_CURSOR); } else { - self.screen.draw_rope_slice( - px as usize, - py as usize, - &g, - STYLE_MAIN, - ); + self.screen.draw(px as usize, py as usize, &g, STYLE_MAIN); } } } diff --git a/src/term_ui/screen.rs b/src/term_ui/screen.rs index 7409716..0b845cd 100644 --- a/src/term_ui/screen.rs +++ b/src/term_ui/screen.rs @@ -4,11 +4,9 @@ use std::io; use std::io::{BufWriter, Write}; use crossterm::{self, execute, queue}; -use ropey::RopeSlice; use unicode_segmentation::UnicodeSegmentation; -use unicode_width::UnicodeWidthStr; -use crate::utils::{grapheme_width, RopeGraphemes}; +use crate::utils::grapheme_width; use super::smallstring::SmallString; @@ -135,7 +133,7 @@ impl Screen { let mut buf = self.buf.borrow_mut(); let mut x = x; for g in UnicodeSegmentation::graphemes(text, true) { - let width = UnicodeWidthStr::width(g); + let width = grapheme_width(g); if width > 0 { if x < self.w { buf[y * self.w + x] = Some((style, g.into())); @@ -152,28 +150,6 @@ impl Screen { } } - pub(crate) fn draw_rope_slice(&self, x: usize, y: usize, text: &RopeSlice, style: Style) { - if y < self.h { - let mut buf = self.buf.borrow_mut(); - let mut x = x; - for g in RopeGraphemes::new(&text) { - let width = grapheme_width(&g); - if width > 0 { - if x < self.w { - buf[y * self.w + x] = Some((style, SmallString::from_rope_slice(&g))); - } - x += 1; - for _ in 0..(width - 1) { - if x < self.w { - buf[y * self.w + x] = None; - } - x += 1; - } - } - } - } - } - pub(crate) fn hide_cursor(&self) { let mut out = self.out.borrow_mut(); execute!(out, crossterm::cursor::Hide).unwrap(); diff --git a/src/utils.rs b/src/utils.rs index 4bf7bff..37c135e 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -16,14 +16,8 @@ pub fn digit_count(mut n: u32, b: u32) -> u32 { //============================================================= -pub fn grapheme_width(slice: &RopeSlice) -> usize { - use crate::term_ui::smallstring::SmallString; - if let Some(text) = slice.as_str() { - return UnicodeWidthStr::width(text); - } else { - let text = SmallString::from_rope_slice(slice); - return UnicodeWidthStr::width(&text[..]); - } +pub fn grapheme_width(g: &str) -> usize { + UnicodeWidthStr::width(g) } /// Finds the previous grapheme boundary before the given char position.