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.
This commit is contained in:
Nathan Vegdahl 2020-02-11 20:24:34 +09:00
parent c3bfdf7a7f
commit 19adb08170
6 changed files with 107 additions and 235 deletions

View File

@ -29,26 +29,20 @@ pub trait LineFormatter {
/// 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.
fn dimensions<'a, T>(&'a self, g_iter: T) -> (usize, usize) fn dimensions(&self, g_iter: RopeGraphemes) -> (usize, usize);
where
T: Iterator<Item = RopeSlice<'a>>;
/// Converts a char index within a text into a visual 2d position. /// Converts a char index within a text into a visual 2d position.
/// The text to be formatted is passed as a grapheme iterator. /// 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) fn index_to_v2d(&self, g_iter: RopeGraphemes, char_idx: usize) -> (usize, usize);
where
T: Iterator<Item = RopeSlice<'a>>;
/// Converts a visual 2d position into a char index within a text. /// Converts a visual 2d position into a char index within a text.
/// The text to be formatted is passed as a grapheme iterator. /// The text to be formatted is passed as a grapheme iterator.
fn v2d_to_index<'a, T>( fn v2d_to_index(
&'a self, &self,
g_iter: T, g_iter: RopeGraphemes,
v2d: (usize, usize), v2d: (usize, usize),
rounding: (RoundingBehavior, RoundingBehavior), rounding: (RoundingBehavior, RoundingBehavior),
) -> usize ) -> usize;
where
T: Iterator<Item = RopeSlice<'a>>;
/// Converts from char index to the horizontal 2d char index. /// 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 {

View File

@ -16,7 +16,11 @@ pub fn is_line_ending(text: &str) -> bool {
} }
pub fn rope_slice_is_line_ending(text: &RopeSlice) -> 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 { pub fn is_whitespace(text: &str) -> bool {

View File

@ -1,11 +1,9 @@
use std::cmp::max; use std::{borrow::Cow, cmp::max};
use ropey::RopeSlice;
use crate::{ use crate::{
formatter::{LineFormatter, RoundingBehavior}, formatter::{LineFormatter, RoundingBehavior},
string_utils::{rope_slice_is_line_ending, rope_slice_is_whitespace}, string_utils::{is_line_ending, is_whitespace},
utils::grapheme_width, utils::{grapheme_width, RopeGraphemes},
}; };
pub enum WrapType { pub enum WrapType {
@ -49,27 +47,24 @@ impl ConsoleLineFormatter {
} }
} }
pub fn iter<'a, T>(&'a self, g_iter: T) -> ConsoleLineFormatterVisIter<'a, T> pub fn iter<'a>(&self, g_iter: RopeGraphemes<'a>) -> FormattingIter<'a> {
where FormattingIter {
T: Iterator<Item = RopeSlice<'a>>, grapheme_itr: g_iter,
{ wrap_width: match self.wrap_type {
ConsoleLineFormatterVisIter::<'a, T> { WrapType::WordWrap(w) => w,
grapheme_iter: g_iter, WrapType::CharWrap(w) => w,
f: self, WrapType::NoWrap => unreachable!(),
pos: (0, 0), },
indent: 0, tab_width: self.tab_width as usize,
indent_found: false,
word_buf: Vec::new(), word_buf: Vec::new(),
word_i: 0, word_i: 0,
pos: (0, 0),
} }
} }
} }
impl LineFormatter for ConsoleLineFormatter { impl LineFormatter for ConsoleLineFormatter {
fn dimensions<'a, T>(&'a self, g_iter: T) -> (usize, usize) fn dimensions(&self, g_iter: RopeGraphemes) -> (usize, usize) {
where
T: Iterator<Item = RopeSlice<'a>>,
{
let mut dim: (usize, usize) = (0, 0); let mut dim: (usize, usize) = (0, 0);
for (_, pos, width) in self.iter(g_iter) { for (_, pos, width) in self.iter(g_iter) {
@ -81,10 +76,7 @@ impl LineFormatter for ConsoleLineFormatter {
return dim; return dim;
} }
fn index_to_v2d<'a, T>(&'a self, g_iter: T, char_idx: usize) -> (usize, usize) fn index_to_v2d(&self, g_iter: RopeGraphemes, char_idx: usize) -> (usize, usize) {
where
T: Iterator<Item = RopeSlice<'a>>,
{
let mut pos = (0, 0); let mut pos = (0, 0);
let mut i = 0; let mut i = 0;
let mut last_width = 0; let mut last_width = 0;
@ -102,15 +94,12 @@ impl LineFormatter for ConsoleLineFormatter {
return (pos.0, pos.1 + last_width); return (pos.0, pos.1 + last_width);
} }
fn v2d_to_index<'a, T>( fn v2d_to_index(
&'a self, &self,
g_iter: T, g_iter: RopeGraphemes,
v2d: (usize, usize), v2d: (usize, usize),
_: (RoundingBehavior, RoundingBehavior), _: (RoundingBehavior, RoundingBehavior),
) -> usize ) -> usize {
where
T: Iterator<Item = RopeSlice<'a>>,
{
// TODO: handle rounding modes // TODO: handle rounding modes
let mut prev_i = 0; let mut prev_i = 0;
let mut 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<Item = RopeSlice<'a>>,
{
grapheme_iter: T,
f: &'a ConsoleLineFormatter,
pos: (usize, usize),
indent: usize, /// An iterator over the visual printable characters of a piece of text,
indent_found: bool, /// yielding the text of the character, its position in 2d space, and its
/// visial width.
word_buf: Vec<RopeSlice<'a>>, ///
word_i: usize, /// TODO: handle maintaining indent, etc.
} pub struct FormattingIter<'a> {
grapheme_itr: RopeGraphemes<'a>,
impl<'a, T> ConsoleLineFormatterVisIter<'a, T>
where
T: Iterator<Item = RopeSlice<'a>>,
{
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);
let pos = self.pos;
self.pos = (self.pos.0, self.pos.1 + width);
return Some((g, pos, width));
}
fn next_charwrap(
&mut self,
g: RopeSlice<'a>,
wrap_width: usize, wrap_width: usize,
) -> Option<(RopeSlice<'a>, (usize, usize), usize)> { tab_width: 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 { word_buf: Vec<(Cow<'a, str>, usize)>, // Printable character and its width.
if !self.indent_found { word_i: usize,
self.indent = 0;
self.indent_found = true; pos: (usize, usize),
} }
if self.f.maintain_indent { impl<'a> Iterator for FormattingIter<'a> {
let pos = (self.pos.0 + 1, self.indent + self.f.wrap_additional_indent); type Item = (Cow<'a, str>, (usize, usize), usize);
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; fn next(&mut self) -> Option<Self::Item> {
self.pos = (self.pos.0, self.pos.1 + width);
return Some((g, pos, width));
}
}
}
impl<'a, T> Iterator for ConsoleLineFormatterVisIter<'a, T>
where
T: Iterator<Item = RopeSlice<'a>>,
{
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 // Get next word if necessary
if self.word_i >= self.word_buf.len() { if self.word_i >= self.word_buf.len() {
let mut word_width = 0; let mut word_width = 0;
self.word_buf.truncate(0); self.word_buf.truncate(0);
while let Some(g) = self.grapheme_iter.next() {
self.word_buf.push(g); while let Some(g) = self.grapheme_itr.next().map(|g| Cow::<str>::from(g)) {
let width = grapheme_vis_width_at_vis_pos( let width =
g, grapheme_vis_width_at_vis_pos(&g, self.pos.1 + word_width, self.tab_width);
self.pos.1 + word_width, self.word_buf.push((g.clone(), width));
self.f.tab_width as usize,
);
word_width += width; word_width += width;
if rope_slice_is_whitespace(&g) {
if is_whitespace(&g) {
break; break;
} }
} }
if self.word_buf.len() == 0 { if self.word_buf.len() == 0 {
return None; 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 // Move to next line if necessary
if (self.pos.1 + word_width) > wrap_width { if (self.pos.1 + word_width) > self.wrap_width {
if !self.indent_found {
self.indent = 0;
self.indent_found = true;
}
if self.pos.1 > 0 { if self.pos.1 > 0 {
if self.f.maintain_indent { self.pos = (self.pos.0 + 1, 0);
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; self.word_i = 0;
} }
// Iterate over the word // Get next grapheme and width from the current word.
let g = self.word_buf[self.word_i]; let (g, g_width) = {
self.word_i += 1; let (ref g, mut width) = self.word_buf[self.word_i];
return self.next_charwrap(g, wrap_width); 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
// Helper functions // 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.1 += g_width;
// Increment index and return.
self.word_i += 1;
return Some((g.clone(), pos, g_width));
}
}
/// Returns the visual width of a grapheme given a starting /// Returns the visual width of a grapheme given a starting
/// position on a line. /// 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" { if g == "\t" {
let ending_pos = ((pos / tab_width) + 1) * tab_width; let ending_pos = ((pos / tab_width) + 1) * tab_width;
return ending_pos - pos; return ending_pos - pos;
} else if rope_slice_is_line_ending(&g) { } else if is_line_ending(g) {
return 1; return 1;
} else { } else {
return grapheme_width(&g); return grapheme_width(&g);
} }
} }
//--------------------------------------------------------------------------
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
#![allow(unused_imports)] #![allow(unused_imports)]

View File

@ -14,7 +14,7 @@ use crossterm::{
use crate::{ use crate::{
editor::Editor, editor::Editor,
formatter::{block_count, block_index_and_range, char_range_from_block_index, LineFormatter}, 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}, utils::{digit_count, RopeGraphemes, Timer},
}; };
@ -568,7 +568,7 @@ impl TermUI {
} }
// Actually print the character // Actually print the character
if rope_slice_is_line_ending(&g) { if is_line_ending(&g) {
if at_cursor { if at_cursor {
self.screen self.screen
.draw(px as usize, py as usize, " ", STYLE_CURSOR); .draw(px as usize, py as usize, " ", STYLE_CURSOR);
@ -587,19 +587,9 @@ impl TermUI {
} }
} else { } else {
if at_cursor { if at_cursor {
self.screen.draw_rope_slice( self.screen.draw(px as usize, py as usize, &g, STYLE_CURSOR);
px as usize,
py as usize,
&g,
STYLE_CURSOR,
);
} else { } else {
self.screen.draw_rope_slice( self.screen.draw(px as usize, py as usize, &g, STYLE_MAIN);
px as usize,
py as usize,
&g,
STYLE_MAIN,
);
} }
} }
} }

View File

@ -4,11 +4,9 @@ use std::io;
use std::io::{BufWriter, Write}; use std::io::{BufWriter, Write};
use crossterm::{self, execute, queue}; use crossterm::{self, execute, queue};
use ropey::RopeSlice;
use unicode_segmentation::UnicodeSegmentation; use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
use crate::utils::{grapheme_width, RopeGraphemes}; use crate::utils::grapheme_width;
use super::smallstring::SmallString; use super::smallstring::SmallString;
@ -135,7 +133,7 @@ impl Screen {
let mut buf = self.buf.borrow_mut(); let mut buf = self.buf.borrow_mut();
let mut x = x; let mut x = x;
for g in UnicodeSegmentation::graphemes(text, true) { for g in UnicodeSegmentation::graphemes(text, true) {
let width = UnicodeWidthStr::width(g); let width = grapheme_width(g);
if width > 0 { if width > 0 {
if x < self.w { if x < self.w {
buf[y * self.w + x] = Some((style, g.into())); 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) { pub(crate) fn hide_cursor(&self) {
let mut out = self.out.borrow_mut(); let mut out = self.out.borrow_mut();
execute!(out, crossterm::cursor::Hide).unwrap(); execute!(out, crossterm::cursor::Hide).unwrap();

View File

@ -16,14 +16,8 @@ pub fn digit_count(mut n: u32, b: u32) -> u32 {
//============================================================= //=============================================================
pub fn grapheme_width(slice: &RopeSlice) -> usize { pub fn grapheme_width(g: &str) -> usize {
use crate::term_ui::smallstring::SmallString; UnicodeWidthStr::width(g)
if let Some(text) = slice.as_str() {
return UnicodeWidthStr::width(text);
} else {
let text = SmallString::from_rope_slice(slice);
return UnicodeWidthStr::width(&text[..]);
}
} }
/// Finds the previous grapheme boundary before the given char position. /// Finds the previous grapheme boundary before the given char position.