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:
parent
c3bfdf7a7f
commit
19adb08170
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
/// 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<RopeSlice<'a>>,
|
||||
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<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);
|
||||
impl<'a> Iterator for FormattingIter<'a> {
|
||||
type Item = (Cow<'a, str>, (usize, usize), usize);
|
||||
|
||||
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_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 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<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
|
||||
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)]
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
10
src/utils.rs
10
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.
|
||||
|
|
Loading…
Reference in New Issue
Block a user