A bunch more work on refactoring how formatting works.

This compiles, but does not yet work correctly again. Tons of bugs.
This commit is contained in:
Nathan Vegdahl 2020-02-16 17:03:37 +09:00
parent 8c1778f674
commit bfeaee602c
9 changed files with 1075 additions and 1055 deletions

View File

@ -23,7 +23,7 @@ use self::undo_stack::{Operation::*, UndoStack};
/// A text buffer /// A text buffer
pub struct Buffer { pub struct Buffer {
text: Rope, pub text: Rope,
file_path: Option<PathBuf>, file_path: Option<PathBuf>,
undo_stack: UndoStack, undo_stack: UndoStack,
} }

View File

@ -28,8 +28,8 @@ impl Cursor {
} }
} }
pub fn update_vis_start<T: LineFormatter>(&mut self, buf: &Buffer, f: &T) { pub fn update_vis_start(&mut self, buf: &Buffer, f: &LineFormatter) {
self.vis_start = f.index_to_horizontal_v2d(buf, self.range.0); self.vis_start = f.get_horizontal(buf, self.range.0);
} }
} }

View File

@ -11,16 +11,15 @@ use std::{
use crate::{ use crate::{
buffer::Buffer, buffer::Buffer,
formatter::LineFormatter, formatter::LineFormatter,
formatter::RoundingBehavior::*,
string_utils::{char_count, rope_slice_to_line_ending, LineEnding}, string_utils::{char_count, rope_slice_to_line_ending, LineEnding},
utils::{digit_count, RopeGraphemes}, utils::{digit_count, RopeGraphemes},
}; };
use self::cursor::CursorSet; use self::cursor::CursorSet;
pub struct Editor<T: LineFormatter> { pub struct Editor {
pub buffer: Buffer, pub buffer: Buffer,
pub formatter: T, pub formatter: LineFormatter,
pub file_path: PathBuf, pub file_path: PathBuf,
pub line_ending_type: LineEnding, pub line_ending_type: LineEnding,
pub soft_tabs: bool, pub soft_tabs: bool,
@ -39,9 +38,9 @@ pub struct Editor<T: LineFormatter> {
pub cursors: CursorSet, pub cursors: CursorSet,
} }
impl<T: LineFormatter> Editor<T> { impl Editor {
/// Create a new blank editor /// Create a new blank editor
pub fn new(formatter: T) -> Editor<T> { pub fn new(formatter: LineFormatter) -> Editor {
Editor { Editor {
buffer: Buffer::new(), buffer: Buffer::new(),
formatter: formatter, formatter: formatter,
@ -57,7 +56,7 @@ impl<T: LineFormatter> Editor<T> {
} }
} }
pub fn new_from_file(formatter: T, path: &Path) -> Editor<T> { pub fn new_from_file(formatter: LineFormatter, path: &Path) -> Editor {
let buf = match Buffer::new_from_file(path) { let buf = match Buffer::new_from_file(path) {
Ok(b) => b, Ok(b) => b,
// TODO: handle un-openable file better // TODO: handle un-openable file better
@ -307,28 +306,24 @@ impl<T: LineFormatter> Editor<T> {
// the closest cursor. // the closest cursor.
// Find the first and last char index visible within the editor. // Find the first and last char index visible within the editor.
let c_first = let c_first = self
.formatter
.set_horizontal(&self.buffer, self.view_pos.0, 0);
let mut c_last =
self.formatter self.formatter
.index_set_horizontal_v2d(&self.buffer, self.view_pos.0, 0, Floor); .offset_vertical(&self.buffer, c_first, self.view_dim.0 as isize);
let mut c_last = self.formatter.index_offset_vertical_v2d( c_last = self
&self.buffer, .formatter
c_first, .set_horizontal(&self.buffer, c_last, self.view_dim.1);
self.view_dim.0 as isize,
(Floor, Floor),
);
c_last =
self.formatter
.index_set_horizontal_v2d(&self.buffer, c_last, self.view_dim.1, Floor);
// Adjust the view depending on where the cursor is // Adjust the view depending on where the cursor is
if self.cursors[0].range.0 < c_first { if self.cursors[0].range.0 < c_first {
self.view_pos.0 = self.cursors[0].range.0; self.view_pos.0 = self.cursors[0].range.0;
} else if self.cursors[0].range.0 > c_last { } else if self.cursors[0].range.0 > c_last {
self.view_pos.0 = self.formatter.index_offset_vertical_v2d( self.view_pos.0 = self.formatter.offset_vertical(
&self.buffer, &self.buffer,
self.cursors[0].range.0, self.cursors[0].range.0,
-(self.view_dim.0 as isize), -(self.view_dim.0 as isize),
(Floor, Floor),
); );
} }
} }
@ -369,9 +364,7 @@ impl<T: LineFormatter> Editor<T> {
c.range.1 += offset; c.range.1 += offset;
// Figure out how many spaces to insert // Figure out how many spaces to insert
let vis_pos = self let vis_pos = self.formatter.get_horizontal(&self.buffer, c.range.0);
.formatter
.index_to_horizontal_v2d(&self.buffer, c.range.0);
// TODO: handle tab settings // TODO: handle tab settings
let next_tab_stop = let next_tab_stop =
((vis_pos / self.soft_tab_width as usize) + 1) * self.soft_tab_width as usize; ((vis_pos / self.soft_tab_width as usize) + 1) * self.soft_tab_width as usize;
@ -555,18 +548,12 @@ impl<T: LineFormatter> Editor<T> {
for c in self.cursors.iter_mut() { for c in self.cursors.iter_mut() {
let vmove = -1 * n as isize; let vmove = -1 * n as isize;
let mut temp_index = self.formatter.index_offset_vertical_v2d( let mut temp_index = self
&self.buffer, .formatter
c.range.0, .offset_vertical(&self.buffer, c.range.0, vmove);
vmove, temp_index = self
(Round, Round), .formatter
); .set_horizontal(&self.buffer, temp_index, c.vis_start);
temp_index = self.formatter.index_set_horizontal_v2d(
&self.buffer,
temp_index,
c.vis_start,
Round,
);
if !self.buffer.is_grapheme(temp_index) { if !self.buffer.is_grapheme(temp_index) {
temp_index = self.buffer.nth_prev_grapheme(temp_index, 1); temp_index = self.buffer.nth_prev_grapheme(temp_index, 1);
@ -591,18 +578,12 @@ impl<T: LineFormatter> Editor<T> {
for c in self.cursors.iter_mut() { for c in self.cursors.iter_mut() {
let vmove = n as isize; let vmove = n as isize;
let mut temp_index = self.formatter.index_offset_vertical_v2d( let mut temp_index = self
&self.buffer, .formatter
c.range.0, .offset_vertical(&self.buffer, c.range.0, vmove);
vmove, temp_index = self
(Round, Round), .formatter
); .set_horizontal(&self.buffer, temp_index, c.vis_start);
temp_index = self.formatter.index_set_horizontal_v2d(
&self.buffer,
temp_index,
c.vis_start,
Round,
);
if !self.buffer.is_grapheme(temp_index) { if !self.buffer.is_grapheme(temp_index) {
temp_index = self.buffer.nth_prev_grapheme(temp_index, 1); temp_index = self.buffer.nth_prev_grapheme(temp_index, 1);
@ -625,11 +606,10 @@ impl<T: LineFormatter> Editor<T> {
pub fn page_up(&mut self) { pub fn page_up(&mut self) {
let move_amount = self.view_dim.0 - max(self.view_dim.0 / 8, 1); 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.view_pos.0 = self.formatter.offset_vertical(
&self.buffer, &self.buffer,
self.view_pos.0, self.view_pos.0,
-1 * move_amount as isize, -1 * move_amount as isize,
(Round, Round),
); );
self.cursor_up(move_amount); self.cursor_up(move_amount);
@ -640,12 +620,9 @@ impl<T: LineFormatter> Editor<T> {
pub fn page_down(&mut self) { pub fn page_down(&mut self) {
let move_amount = self.view_dim.0 - max(self.view_dim.0 / 8, 1); 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.view_pos.0 =
&self.buffer, self.formatter
self.view_pos.0, .offset_vertical(&self.buffer, self.view_pos.0, move_amount as isize);
move_amount as isize,
(Round, Round),
);
self.cursor_down(move_amount); self.cursor_down(move_amount);
@ -656,12 +633,9 @@ impl<T: LineFormatter> Editor<T> {
pub fn jump_to_line(&mut self, n: usize) { pub fn jump_to_line(&mut self, n: usize) {
let pos = self.buffer.line_col_to_index((n, 0)); let pos = self.buffer.line_col_to_index((n, 0));
self.cursors.truncate(1); self.cursors.truncate(1);
self.cursors[0].range.0 = self.formatter.index_set_horizontal_v2d( self.cursors[0].range.0 =
&self.buffer, self.formatter
pos, .set_horizontal(&self.buffer, pos, self.cursors[0].vis_start);
self.cursors[0].vis_start,
Round,
);
self.cursors[0].range.1 = self.cursors[0].range.0; self.cursors[0].range.1 = self.cursors[0].range.0;
// Adjust view // Adjust view

View File

@ -1,10 +1,12 @@
use std::cmp::min; use std::borrow::Cow;
use ropey::RopeSlice; use ropey::{Rope, RopeSlice};
use crate::{ use crate::{
buffer::Buffer, buffer::Buffer,
utils::{is_grapheme_boundary, prev_grapheme_boundary, RopeGraphemes}, string_utils::char_count,
string_utils::{is_line_ending, str_is_whitespace},
utils::{grapheme_width, is_grapheme_boundary, prev_grapheme_boundary, RopeGraphemes},
}; };
// Maximum chars in a line before a soft line break is forced. // Maximum chars in a line before a soft line break is forced.
@ -17,176 +19,457 @@ const LINE_BLOCK_LENGTH: usize = 1 << 12;
// breaks. // breaks.
const LINE_BLOCK_FUDGE: usize = 32; const LINE_BLOCK_FUDGE: usize = 32;
#[allow(dead_code)] //--------------------------------------------------------------------------
#[derive(Copy, Clone, PartialEq)]
pub enum RoundingBehavior { #[derive(Clone)]
Round, pub struct LineFormatter {
Floor, pub tab_width: usize,
Ceiling, pub wrap_width: usize,
pub maintain_indent: bool,
pub wrap_extra_indent: usize,
} }
pub trait LineFormatter { impl LineFormatter {
/// Returns the 2d visual dimensions of the given text when formatted pub fn new(tab_width: usize) -> LineFormatter {
/// by the formatter. LineFormatter {
/// The text to be formatted is passed as a grapheme iterator. tab_width: tab_width,
fn dimensions(&self, g_iter: RopeGraphemes) -> (usize, usize); wrap_width: 80,
maintain_indent: true,
wrap_extra_indent: 2,
}
}
/// Converts a char index within a text into a visual 2d position. /// Returns an iterator over the blocks of the buffer, starting at the
/// The text to be formatted is passed as a grapheme iterator. /// block containing the given char. Also returns the offset of that char
fn index_to_v2d(&self, g_iter: RopeGraphemes, char_idx: usize) -> (usize, usize); /// relative to the start of the first block.
pub fn iter<'b>(&'b self, buf: &'b Buffer, char_idx: usize) -> (Blocks<'b>, usize) {
/// Converts a visual 2d position into a char index within a text. // Get the line.
/// The text to be formatted is passed as a grapheme iterator.
fn v2d_to_index(
&self,
g_iter: RopeGraphemes,
v2d: (usize, usize),
rounding: (RoundingBehavior, RoundingBehavior),
) -> usize;
/// 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_i, col_i) = buf.index_to_line_col(char_idx);
let line = buf.get_line(line_i); let line = buf.get_line(line_i);
// Find the right block in the line, and the index within that block // Find the right block in the line, and the index within that block
let (_, block_range) = block_index_and_range(&line, col_i); let (block_index, block_range) = block_index_and_range(&line, col_i);
let col_i_adjusted = col_i - block_range.0; let col_i_adjusted = col_i - block_range.0;
// Get an iter into the right block (
let g_iter = RopeGraphemes::new(&line.slice(block_range.0..block_range.1)); Blocks {
return self.index_to_v2d(g_iter, col_i_adjusted).1; formatter: self,
buf: &buf.text,
line_idx: line_i,
line_block_count: block_count(&line),
block_idx: block_index,
},
col_i_adjusted,
)
} }
/// Takes a char index and a visual vertical offset, and returns the char /// Converts from char index to the horizontal 2d char index.
/// index after that visual offset is applied. pub fn get_horizontal(&self, buf: &Buffer, char_idx: usize) -> usize {
fn index_offset_vertical_v2d( let (_, vis_iter, char_offset) = self.block_vis_iter_and_char_offset(buf, char_idx);
&self,
buf: &Buffer,
char_idx: usize,
offset: isize,
rounding: (RoundingBehavior, RoundingBehavior),
) -> usize {
// TODO: handle rounding modes
// TODO: do this with bidirectional line iterator
// Get the line and char index within that line. // Traverse the iterator and find the horizontal position of the char
let (mut line_i, mut col_i) = buf.index_to_line_col(char_idx); // index.
let mut line = buf.get_line(line_i); let mut hpos = 0;
let mut i = 0;
let mut last_width = 0;
// Get the block information for the char offset in the line. for (g, pos, width) in vis_iter {
let (line_block, block_range) = block_index_and_range(&line, col_i); hpos = pos.1;
let col_i_adjusted = col_i - block_range.0; last_width = width;
i += char_count(&g);
// Get the 2d coordinates within the block. if i > char_offset {
let (mut y, x) = self.index_to_v2d( return hpos;
RopeGraphemes::new(&line.slice(block_range.0..block_range.1)),
col_i_adjusted,
);
// First, find the right line while keeping track of the vertical offset
let mut new_y = y as isize + offset;
let mut block_index: usize = line_block;
loop {
line = buf.get_line(line_i);
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 >= (block_count(&line) - 1);
// Check for off-the-end
if is_last_block && (line_i + 1) >= buf.line_count() {
return buf.char_count();
}
if is_last_block {
line_i += 1;
block_index = 0;
} else {
block_index += 1;
}
new_y -= h as isize;
} else if new_y < 0 {
// Check for off-the-end
if block_index == 0 && line_i == 0 {
return 0;
}
if block_index == 0 {
line_i -= 1;
line = buf.get_line(line_i);
block_index = block_count(&line) - 1;
} else {
block_index -= 1;
}
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!();
}
} }
} }
// Next, convert the resulting coordinates back into buffer-wide // If we went off the end, calculate the position of the end of the
// coordinates. // block.
let (block_start, block_end) = char_range_from_block_index(&line, block_index); return hpos + last_width;
let block_len = block_end - block_start;
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),
block_len.saturating_sub(1),
);
col_i = block_start + block_col_i;
return buf.line_col_to_index((line_i, col_i));
} }
/// Takes a char index and a desired visual horizontal position, and /// Takes a char index and a desired visual horizontal position, and
/// returns a char index on the same visual line as the given index, /// returns a char index on the same visual line as the given index,
/// but offset to have the desired horizontal position. /// but offset to have the desired horizontal position (or as close as is
fn index_set_horizontal_v2d( /// possible.
pub fn set_horizontal(&self, buf: &Buffer, char_idx: usize, horizontal: usize) -> usize {
let (_, vis_iter, char_offset) = self.block_vis_iter_and_char_offset(buf, char_idx);
let mut hpos_char_idx = None;
let mut i = 0;
let mut last_i = 0;
let mut last_pos = (0, 0);
for (g, pos, width) in vis_iter {
// Check if we moved to the next line.
if pos.0 > last_pos.0 {
// If we did, but we're already passed the given char_idx,
// that means the target was on the previous line but the line
// wasn't long enough, so return the index of the last grapheme
// of the previous line.
if i > char_offset {
return last_i;
}
// Otherwise reset and keep going.
hpos_char_idx = None;
}
// Check if we found the horizontal position on this line,
// and set it if so.
if hpos_char_idx == None && horizontal < (pos.1 + width) {
hpos_char_idx = Some(i);
}
// Check if we've found the horizontal position _and_ the passed
// char_idx on the same line, and return if so.
if i >= char_offset && hpos_char_idx != None {
return hpos_char_idx.unwrap();
}
last_pos = pos;
last_i = i;
i += char_count(&g);
}
// If we reached the end of the text, return the last char index.
return i;
}
/// Takes a char index and a visual vertical offset, and returns the char
/// index after that visual offset is applied.
pub fn offset_vertical(&self, buf: &Buffer, char_idx: usize, v_offset: isize) -> usize {
let mut char_idx = char_idx;
let mut v_offset = v_offset;
while v_offset != 0 {
// Get our block and the offset of the char inside it.
let (block, block_vis_iter, char_offset) =
self.block_vis_iter_and_char_offset(buf, char_idx);
// Get the vertical size of the block and the vertical
// position of the char_idx within it.
let block_v_dim = (block_vis_iter.clone().last().unwrap().1).0 + 1;
let char_v_pos = block_vis_iter.clone().vpos(char_offset);
// Get the char's vertical position within the block after offset
// by v_offset.
let offset_char_v_pos = char_v_pos as isize + v_offset;
// Check if the offset position is within the block or not,
// and handle appropriately.
if offset_char_v_pos < 0 {
// If we're off the start of the block.
if char_idx == 0 {
// We reached the start of the whole buffer.
break;
} else {
// Set our variables appropriately for the next iteration.
char_idx -= char_offset + 1;
v_offset += char_v_pos as isize + 1;
}
} else if offset_char_v_pos >= block_v_dim as isize {
// If we're off the end of the block.
if char_idx >= buf.text.len_chars() {
// We reached the end of the whole buffer.
char_idx = buf.text.len_chars();
break;
} else {
// Set our variables appropriately for the next iteration.
char_idx += block.len_chars() - char_offset;
v_offset -= block_v_dim as isize - char_v_pos as isize;
}
} else {
// If the vertical offset is within this block, calculate an
// appropriate char index and return.
let mut i = 0;
for (g, pos, _) in block_vis_iter {
if pos.0 == offset_char_v_pos as usize {
break;
}
i += char_count(&g);
}
char_idx += block.len_chars() - char_offset + i;
v_offset = 0;
}
}
return char_idx;
}
//----------------------------------------------------
// Helper methods
/// Returns the amount of indentation to use for soft-line wrapping
/// given the start of a line.
fn get_line_indent(&self, line: &RopeSlice) -> usize {
if !self.maintain_indent {
return 0;
}
let mut indent = 0;
for c in line.chars() {
match c {
' ' => {
indent += 1;
}
'\t' => {
indent = tab_stop_from_vis_pos(indent, self.tab_width);
}
_ => break,
}
// If the indent is too long for the wrap width, do no indentation.
if (indent + self.wrap_extra_indent + 2) > self.wrap_width {
return 0;
}
}
indent
}
/// Returns the appropriate BlockVisIter containing the given char, and the
/// char's offset within that iter.
fn block_vis_iter_and_char_offset<'b>(
&self, &self,
buf: &Buffer, buf: &'b Buffer,
char_idx: usize, char_idx: usize,
horizontal: usize, ) -> (RopeSlice<'b>, BlockVisIter<'b>, usize) {
rounding: RoundingBehavior,
) -> usize {
// Get the line info.
let (line_i, col_i) = buf.index_to_line_col(char_idx); let (line_i, col_i) = buf.index_to_line_col(char_idx);
let line = buf.get_line(line_i); let line = buf.get_line(line_i);
// Get the right block within the line. // Find the right block in the line, and the index within that block
let (block_i, block_range) = block_index_and_range(&line, col_i); let (block_index, block_range) = block_index_and_range(&line, col_i);
let col_i_adjusted = col_i - block_range.0; let col_i_adjusted = col_i - block_range.0;
// Calculate the horizontal position. // Get the right block and an iter into it.
let (v, _) = self.index_to_v2d( let block = line.slice(block_range.0..block_range.1);
RopeGraphemes::new(&line.slice(block_range.0..block_range.1)), let g_iter = RopeGraphemes::new(&block);
col_i_adjusted,
);
let block_col_i = self.v2d_to_index(
RopeGraphemes::new(&line.slice(block_range.0..block_range.1)),
(v, horizontal),
(RoundingBehavior::Floor, rounding),
);
let new_col_i = if (line_i + 1) < buf.line_count() || (block_i + 1) < block_count(&line) {
min(block_range.0 + block_col_i, block_range.1.saturating_sub(1))
} else {
min(block_range.0 + block_col_i, block_range.1)
};
return (char_idx + new_col_i) - col_i; // Get an appropriate visual block iter.
let vis_iter = BlockVisIter::new(
g_iter,
self.wrap_width,
self.tab_width,
block_index == 0,
if block_index == 0 {
0
} else {
self.get_line_indent(&line)
},
self.wrap_extra_indent,
);
(block, vis_iter, col_i_adjusted)
} }
} }
//--------------------------------------------------------------------------
#[derive(Clone)]
pub struct Blocks<'a> {
formatter: &'a LineFormatter,
buf: &'a Rope,
line_idx: usize,
line_block_count: usize,
block_idx: usize,
}
impl<'a> Iterator for Blocks<'a> {
type Item = (BlockVisIter<'a>, bool);
fn next(&mut self) -> Option<Self::Item> {
// Check if we're done already.
if self.line_idx >= self.buf.len_lines() {
return None;
}
// Get our return values.
let (iter, is_line_start) = {
let line = self.buf.line(self.line_idx);
let (start, end) = char_range_from_block_index(&line, self.block_idx);
let block = line.slice(start..end);
let iter = BlockVisIter::new(
RopeGraphemes::new(&block),
self.formatter.wrap_width,
self.formatter.tab_width,
self.block_idx == 0,
if self.block_idx == 0 {
0
} else {
self.formatter.get_line_indent(&line)
},
self.formatter.wrap_extra_indent,
);
(iter, self.block_idx == 0)
};
// Progress the values of the iterator.
self.block_idx += 1;
if self.block_idx >= self.line_block_count {
self.line_idx += 1;
self.block_idx = 0;
if self.line_idx < self.buf.len_lines() {
self.line_block_count = block_count(&self.buf.line(self.line_idx));
}
}
// Return.
Some((iter, is_line_start))
}
}
//--------------------------------------------------------------------------
/// An iterator over the visual printable characters of a block of text,
/// yielding the text of the character, its position in 2d space, and its
/// visial width.
#[derive(Clone)]
pub struct BlockVisIter<'a> {
grapheme_itr: RopeGraphemes<'a>,
wrap_width: usize,
tab_width: usize,
indent: usize, // Size of soft indent to use.
wrap_extra_indent: usize, // Additional amount to indent soft-wrapped lines.
finding_indent: bool,
word_buf: Vec<(Cow<'a, str>, usize)>, // Printable character and its width.
word_i: usize,
pos: (usize, usize),
}
impl<'a> BlockVisIter<'a> {
fn new(
grapheme_itr: RopeGraphemes<'a>,
wrap_width: usize,
tab_width: usize,
find_indent: bool,
starting_indent: usize,
wrap_extra_indent: usize,
) -> BlockVisIter<'a> {
BlockVisIter {
grapheme_itr: grapheme_itr,
wrap_width: wrap_width,
tab_width: tab_width,
indent: starting_indent,
wrap_extra_indent: wrap_extra_indent,
finding_indent: find_indent,
word_buf: Vec::new(),
word_i: 0,
pos: (0, 0),
}
}
pub fn vpos(&mut self, char_offset: usize) -> usize {
let mut vpos = 0;
let mut i = 0;
for (g, pos, _) in self {
vpos = pos.0;
i += char_count(&g);
if i > char_offset {
break;
}
}
vpos
}
}
impl<'a> Iterator for BlockVisIter<'a> {
type Item = (Cow<'a, str>, (usize, usize), usize);
fn next(&mut self) -> Option<Self::Item> {
if self.pos == (0, 0) {
self.pos = (0, self.indent);
}
// 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 str_is_whitespace(&g) {
if self.finding_indent && (g.as_bytes()[0] == 0x09 || g.as_bytes()[0] == 0x20) {
if (self.indent + self.wrap_extra_indent + width + 2) > self.wrap_width {
// Cancel indentation if it's too long for the screen.
self.indent = 0;
self.wrap_extra_indent = 0;
self.finding_indent = false;
} else {
self.indent += width;
}
}
break;
} else {
self.finding_indent = false;
}
}
if self.word_buf.len() == 0 {
return None;
}
// Move to next line if necessary
if (self.pos.1 + word_width) > self.wrap_width && (self.pos.1 > self.indent) {
if self.pos.1 > 0 {
self.pos = (self.pos.0 + 1, self.indent + self.wrap_extra_indent);
}
}
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 = self.indent + self.wrap_extra_indent;
}
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: &str, pos: usize, tab_width: usize) -> usize {
if g == "\t" {
return tab_stop_from_vis_pos(pos, tab_width) - pos;
} else if is_line_ending(g) {
return 1;
} else {
return grapheme_width(&g);
}
}
fn tab_stop_from_vis_pos(pos: usize, tab_width: usize) -> usize {
((pos / tab_width) + 1) * tab_width
}
//--------------------------------------------------------------------------
// Finds the best break at or before the given char index, bounded by // Finds the best break at or before the given char index, bounded by
// the given `lower_limit`. // the given `lower_limit`.
pub fn find_good_break(slice: &RopeSlice, lower_limit: usize, char_idx: usize) -> usize { pub fn find_good_break(slice: &RopeSlice, lower_limit: usize, char_idx: usize) -> usize {

View File

@ -2,7 +2,7 @@ use std::{io::Write, path::Path};
use clap::{App, Arg}; use clap::{App, Arg};
use editor::Editor; use editor::Editor;
use term_ui::formatter::ConsoleLineFormatter; use formatter::LineFormatter;
use term_ui::TermUI; use term_ui::TermUI;
mod buffer; mod buffer;
@ -29,9 +29,9 @@ fn main() {
// Load file, if specified // Load file, if specified
let editor = if let Some(filepath) = args.value_of("file") { let editor = if let Some(filepath) = args.value_of("file") {
Editor::new_from_file(ConsoleLineFormatter::new(4), &Path::new(&filepath[..])) Editor::new_from_file(LineFormatter::new(4), &Path::new(&filepath[..]))
} else { } else {
Editor::new(ConsoleLineFormatter::new(4)) Editor::new(LineFormatter::new(4))
}; };
// Holds stderr output in an internal buffer, and prints it when dropped. // Holds stderr output in an internal buffer, and prints it when dropped.

View File

@ -10,28 +10,36 @@ pub fn is_line_ending(text: &str) -> bool {
} }
} }
pub fn is_whitespace(text: &str) -> bool { pub fn str_is_whitespace(text: &str) -> bool {
if let Some(c) = text.chars().nth(0) {
is_whitespace(c)
} else {
false
}
}
pub fn is_whitespace(c: char) -> bool {
// TODO: this is a naive categorization of whitespace characters. // TODO: this is a naive categorization of whitespace characters.
// For better categorization these should be split up into groups // For better categorization these should be split up into groups
// based on e.g. breaking vs non-breaking spaces, among other things. // based on e.g. breaking vs non-breaking spaces, among other things.
match text.chars().nth(0) { match c {
//Some('\u{1680}') | // OGHAM SPACE MARK (here for completeness, but usually displayed as a dash, not as whitespace) //'\u{1680}' | // OGHAM SPACE MARK (here for completeness, but usually displayed as a dash, not as whitespace)
Some('\u{0009}') | // CHARACTER TABULATION '\u{0009}' | // CHARACTER TABULATION
Some('\u{0020}') | // SPACE '\u{0020}' | // SPACE
Some('\u{00A0}') | // NO-BREAK SPACE '\u{00A0}' | // NO-BREAK SPACE
Some('\u{180E}') | // MONGOLIAN VOWEL SEPARATOR '\u{180E}' | // MONGOLIAN VOWEL SEPARATOR
Some('\u{202F}') | // NARROW NO-BREAK SPACE '\u{202F}' | // NARROW NO-BREAK SPACE
Some('\u{205F}') | // MEDIUM MATHEMATICAL SPACE '\u{205F}' | // MEDIUM MATHEMATICAL SPACE
Some('\u{3000}') | // IDEOGRAPHIC SPACE '\u{3000}' | // IDEOGRAPHIC SPACE
Some('\u{FEFF}') // ZERO WIDTH NO-BREAK SPACE '\u{FEFF}' // ZERO WIDTH NO-BREAK SPACE
=> true, => true,
// EN QUAD, EM QUAD, EN SPACE, EM SPACE, THREE-PER-EM SPACE, // EN QUAD, EM QUAD, EN SPACE, EM SPACE, THREE-PER-EM SPACE,
// FOUR-PER-EM SPACE, SIX-PER-EM SPACE, FIGURE SPACE, // FOUR-PER-EM SPACE, SIX-PER-EM SPACE, FIGURE SPACE,
// PUNCTUATION SPACE, THIN SPACE, HAIR SPACE, ZERO WIDTH SPACE. // PUNCTUATION SPACE, THIN SPACE, HAIR SPACE, ZERO WIDTH SPACE.
Some(c) if c >= '\u{2000}' && c <= '\u{200B}' => true, c if c >= '\u{2000}' && c <= '\u{200B}' => true,
// None, or not a matching whitespace character. // Not a matching whitespace character.
_ => false, _ => false,
} }
} }
@ -108,9 +116,11 @@ mod tests {
#[test] #[test]
fn char_count_1() { fn char_count_1() {
let text_0 = "";
let text_1 = "Hello world!"; let text_1 = "Hello world!";
let text_2 = "今日はみんなさん!"; let text_2 = "今日はみんなさん!";
assert_eq!(0, char_count(text_0));
assert_eq!(12, char_count(text_1)); assert_eq!(12, char_count(text_1));
assert_eq!(9, char_count(text_2)); assert_eq!(9, char_count(text_2));
} }

File diff suppressed because it is too large Load Diff

View File

@ -13,15 +13,12 @@ use crossterm::{
use crate::{ use crate::{
editor::Editor, editor::Editor,
formatter::{block_count, block_index_and_range, char_range_from_block_index, LineFormatter}, formatter::LineFormatter,
string_utils::{is_line_ending, line_ending_to_str, LineEnding}, string_utils::{char_count, is_line_ending, line_ending_to_str, LineEnding},
utils::{digit_count, RopeGraphemes, Timer}, utils::{digit_count, Timer},
}; };
use self::{ use self::screen::{Screen, Style};
formatter::ConsoleLineFormatter,
screen::{Screen, Style},
};
const EMPTY_MOD: KeyModifiers = KeyModifiers::empty(); const EMPTY_MOD: KeyModifiers = KeyModifiers::empty();
const UPDATE_TICK_MS: u64 = 10; const UPDATE_TICK_MS: u64 = 10;
@ -159,10 +156,7 @@ macro_rules! ui_loop {
$term_ui $term_ui
.editor .editor
.update_dim($term_ui.height - 1, $term_ui.width); .update_dim($term_ui.height - 1, $term_ui.width);
$term_ui $term_ui.editor.formatter.wrap_width = $term_ui.editor.view_dim.1;
.editor
.formatter
.set_wrap_width($term_ui.editor.view_dim.1);
// Draw! // Draw!
{ {
@ -176,7 +170,7 @@ macro_rules! ui_loop {
pub struct TermUI { pub struct TermUI {
screen: Screen, screen: Screen,
editor: Editor<ConsoleLineFormatter>, editor: Editor,
width: usize, width: usize,
height: usize, height: usize,
quit: bool, quit: bool,
@ -190,10 +184,10 @@ enum LoopStatus {
impl TermUI { impl TermUI {
pub fn new() -> TermUI { pub fn new() -> TermUI {
TermUI::new_from_editor(Editor::new(ConsoleLineFormatter::new(4))) TermUI::new_from_editor(Editor::new(LineFormatter::new(4)))
} }
pub fn new_from_editor(ed: Editor<ConsoleLineFormatter>) -> TermUI { pub fn new_from_editor(ed: Editor) -> TermUI {
let (w, h) = crossterm::terminal::size().unwrap(); let (w, h) = crossterm::terminal::size().unwrap();
let mut editor = ed; let mut editor = ed;
editor.update_dim(h as usize - 1, w as usize); editor.update_dim(h as usize - 1, w as usize);
@ -216,7 +210,7 @@ impl TermUI {
self.width = w as usize; self.width = w as usize;
self.height = h as usize; self.height = h as usize;
self.editor.update_dim(self.height - 1, self.width); self.editor.update_dim(self.height - 1, self.width);
self.editor.formatter.set_wrap_width(self.editor.view_dim.1); self.editor.formatter.wrap_width = self.editor.view_dim.1;
self.screen.resize(w as usize, h as usize); self.screen.resize(w as usize, h as usize);
// Start the UI // Start the UI
@ -444,12 +438,7 @@ impl TermUI {
} }
} }
fn draw_editor( fn draw_editor(&self, editor: &Editor, c1: (usize, usize), c2: (usize, usize)) {
&self,
editor: &Editor<ConsoleLineFormatter>,
c1: (usize, usize),
c2: (usize, usize),
) {
// Fill in top row with info line color // Fill in top row with info line color
for i in c1.1..(c2.1 + 1) { for i in c1.1..(c2.1 + 1) {
self.screen.draw(i, c1.0, " ", STYLE_INFO); self.screen.draw(i, c1.0, " ", STYLE_INFO);
@ -502,23 +491,20 @@ impl TermUI {
self.draw_editor_text(editor, (c1.0 + 1, c1.1), c2); self.draw_editor_text(editor, (c1.0 + 1, c1.1), c2);
} }
fn draw_editor_text( fn draw_editor_text(&self, editor: &Editor, c1: (usize, usize), c2: (usize, usize)) {
&self,
editor: &Editor<ConsoleLineFormatter>,
c1: (usize, usize),
c2: (usize, usize),
) {
// Calculate all the starting info // Calculate all the starting info
let gutter_width = editor.editor_dim.1 - editor.view_dim.1; let gutter_width = editor.editor_dim.1 - editor.view_dim.1;
let blank_gutter = &" "[..gutter_width - 1]; let blank_gutter = &" "[..gutter_width - 1];
let (line_index, col_i) = editor.buffer.index_to_line_col(editor.view_pos.0); let line_index = editor.buffer.text.char_to_line(editor.view_pos.0);
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 (blocks_iter, char_offset) = editor.formatter.iter(&editor.buffer, editor.view_pos.0);
let mut char_index = editor.buffer.line_col_to_index((line_index, block_range.0));
let (vis_line_offset, _) = editor.formatter.index_to_v2d( let vis_line_offset = blocks_iter
RopeGraphemes::new(&temp_line.slice(block_range.0..block_range.1)), .clone()
editor.view_pos.0 - char_index, .next()
); .unwrap()
.0
.vpos(editor.view_pos.0);
let mut screen_line = c1.0 as isize - vis_line_offset as isize; let mut screen_line = c1.0 as isize - vis_line_offset as isize;
let screen_col = c1.1 as isize + gutter_width as isize; let screen_col = c1.1 as isize + gutter_width as isize;
@ -528,8 +514,16 @@ impl TermUI {
self.screen.draw(c1.1, y, blank_gutter, STYLE_GUTTER_ODD); self.screen.draw(c1.1, y, blank_gutter, STYLE_GUTTER_ODD);
} }
// Loop through the blocks, printing them to the screen.
let mut is_first_loop = true;
let mut line_num = line_index + 1; let mut line_num = line_index + 1;
for line in editor.buffer.line_iter_at_index(line_index) { let mut char_index = editor.view_pos.0 - char_offset;
for (block_vis_iter, is_line_start) in blocks_iter {
if is_line_start && !is_first_loop {
line_num += 1;
}
is_first_loop = false;
let gutter_style = if (line_num % 2) == 0 { let gutter_style = if (line_num % 2) == 0 {
STYLE_GUTTER_EVEN STYLE_GUTTER_EVEN
} else { } else {
@ -537,7 +531,7 @@ impl TermUI {
}; };
// Print line number // Print line number
if line_block_index == 0 { if is_line_start {
let lnx = c1.1; let lnx = c1.1;
let lny = screen_line as usize; let lny = screen_line as usize;
if lny >= c1.0 && lny <= c2.0 { if lny >= c1.0 && lny <= c2.0 {
@ -555,97 +549,63 @@ impl TermUI {
} }
} }
// Loop through the graphemes of the line and print them to // Loop through the graphemes of the block and print them to
// the screen. // the screen.
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(block_range.0..block_range.1),
));
let mut last_pos_y = 0; let mut last_pos_y = 0;
let mut lines_traversed: usize = 0; for (g, (pos_y, pos_x), width) in block_vis_iter {
loop { // Calculate the cell coordinates at which to draw the grapheme
for (g, (pos_y, pos_x), width) in g_iter { if pos_y > last_pos_y {
let do_gutter = screen_line += 1;
last_pos_y != pos_y || (lines_traversed == 0 && line_block_index != 0); last_pos_y = pos_y;
if last_pos_y != pos_y { }
if last_pos_y < pos_y { let px = pos_x as isize + screen_col - editor.view_pos.1 as isize;
lines_traversed += pos_y - last_pos_y; let py = screen_line;
// If we're off the bottom, we're done
if py > c2.0 as isize {
return;
}
// Draw the grapheme to the screen if it's in bounds
if (px >= c1.1 as isize) && (py >= c1.0 as isize) && (px <= c2.1 as isize) {
// Check if the character is within a cursor
let mut at_cursor = false;
for c in editor.cursors.iter() {
if char_index >= c.range.0 && char_index <= c.range.1 {
at_cursor = true;
self.screen.set_cursor(px as usize, py as usize);
} }
last_pos_y = pos_y;
}
// Calculate the cell coordinates at which to draw the grapheme
let px = pos_x as isize + screen_col - editor.view_pos.1 as isize;
let py = lines_traversed as isize + screen_line;
// If we're off the bottom, we're done
if py > c2.0 as isize {
return;
} }
if do_gutter { // Actually print the character
self.screen if is_line_ending(&g) {
.draw(c1.1, py as usize, blank_gutter, gutter_style); if at_cursor {
} self.screen
.draw(px as usize, py as usize, " ", STYLE_CURSOR);
// Draw the grapheme to the screen if it's in bounds }
if (px >= c1.1 as isize) && (py >= c1.0 as isize) && (px <= c2.1 as isize) { } else if g == "\t" {
// Check if the character is within a cursor for i in 0..width {
let mut at_cursor = false; let tpx = px as usize + i;
for c in editor.cursors.iter() { if tpx <= c2.1 {
if char_index >= c.range.0 && char_index <= c.range.1 { self.screen.draw(tpx as usize, py as usize, " ", STYLE_MAIN);
at_cursor = true;
self.screen.set_cursor(px as usize, py as usize);
} }
} }
// Actually print the character if at_cursor {
if is_line_ending(&g) { self.screen
if at_cursor { .draw(px as usize, py as usize, " ", STYLE_CURSOR);
self.screen }
.draw(px as usize, py as usize, " ", STYLE_CURSOR); } else {
} if at_cursor {
} else if g == "\t" { self.screen.draw(px as usize, py as usize, &g, STYLE_CURSOR);
for i in 0..width {
let tpx = px as usize + i;
if tpx <= c2.1 {
self.screen.draw(tpx as usize, py as usize, " ", STYLE_MAIN);
}
}
if at_cursor {
self.screen
.draw(px as usize, py as usize, " ", STYLE_CURSOR);
}
} else { } else {
if at_cursor { self.screen.draw(px as usize, py as usize, &g, STYLE_MAIN);
self.screen.draw(px as usize, py as usize, &g, STYLE_CURSOR);
} else {
self.screen.draw(px as usize, py as usize, &g, STYLE_MAIN);
}
} }
} }
char_index += g.chars().count();
} }
// Move on to the next block. char_index += char_count(&g);
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(block_range.0..block_range.1),
));
lines_traversed += 1;
} else {
break;
}
} }
line_block_index = 0;
screen_line += lines_traversed as isize + 1;
line_num += 1;
} }
// If we get here, it means we reached the end of the text buffer // If we get here, it means we reached the end of the text buffer
@ -664,7 +624,7 @@ impl TermUI {
// Calculate the cell coordinates at which to draw the cursor // Calculate the cell coordinates at which to draw the cursor
let pos_x = editor let pos_x = editor
.formatter .formatter
.index_to_horizontal_v2d(&self.editor.buffer, self.editor.buffer.char_count()); .get_horizontal(&self.editor.buffer, self.editor.buffer.char_count());
let mut px = pos_x as isize + screen_col - editor.view_pos.1 as isize; let mut px = pos_x as isize + screen_col - editor.view_pos.1 as isize;
let mut py = screen_line - 1; let mut py = screen_line - 1;
if px > c2.1 as isize { if px > c2.1 as isize {

View File

@ -122,6 +122,7 @@ pub fn is_grapheme_boundary(slice: &RopeSlice, char_idx: usize) -> bool {
} }
/// An iterator over the graphemes of a RopeSlice. /// An iterator over the graphemes of a RopeSlice.
#[derive(Clone)]
pub struct RopeGraphemes<'a> { pub struct RopeGraphemes<'a> {
text: RopeSlice<'a>, text: RopeSlice<'a>,
chunks: Chunks<'a>, chunks: Chunks<'a>,