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
/// 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<Item = RopeSlice<'a>>;
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<Item = RopeSlice<'a>>;
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<Item = RopeSlice<'a>>;
) -> usize;
/// Converts from char index to the horizontal 2d char index.
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 {
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 {

View File

@ -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<Item = RopeSlice<'a>>,
{
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<Item = RopeSlice<'a>>,
{
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<Item = RopeSlice<'a>>,
{
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<Item = RopeSlice<'a>>,
{
) -> 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<Item = RopeSlice<'a>>,
{
grapheme_iter: T,
f: &'a ConsoleLineFormatter,
pos: (usize, usize),
//--------------------------------------------------------------------------
indent: usize,
indent_found: bool,
word_buf: Vec<RopeSlice<'a>>,
word_i: usize,
}
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>,
/// 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,
) -> Option<(RopeSlice<'a>, (usize, usize), usize)> {
let width = grapheme_vis_width_at_vis_pos(g, self.pos.1, self.f.tab_width as usize);
tab_width: usize,
if (self.pos.1 + width) > wrap_width {
if !self.indent_found {
self.indent = 0;
self.indent_found = true;
word_buf: Vec<(Cow<'a, str>, usize)>, // Printable character and its width.
word_i: usize,
pos: (usize, usize),
}
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;
}
}
impl<'a> Iterator for FormattingIter<'a> {
type Item = (Cow<'a, str>, (usize, usize), usize);
let pos = self.pos;
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) => {
fn next(&mut self) -> Option<Self::Item> {
// 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,
);
while let Some(g) = self.grapheme_itr.next().map(|g| Cow::<str>::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 rope_slice_is_whitespace(&g) {
if 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 + word_width) > self.wrap_width {
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.pos = (self.pos.0 + 1, 0);
}
}
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);
}
}
}
// 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)
};
// ===================================================================
// Helper functions
// ===================================================================
// 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.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
/// 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)]

View File

@ -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);
}
}
}

View File

@ -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();

View File

@ -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.