Switch to new backend buffer for text, cursor, and undo management.

This commit is contained in:
Nathan Vegdahl 2020-02-23 19:56:08 +09:00
parent 6077be2dfd
commit c4fa72405f
11 changed files with 501 additions and 2037 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,49 +0,0 @@
use std::collections::LinkedList;
/// A text editing operation
#[derive(Clone)]
pub enum Operation {
InsertText(String, usize),
RemoveTextBefore(String, usize),
RemoveTextAfter(String, usize),
MoveText(usize, usize, usize),
CompositeOp(Vec<Operation>),
}
/// An undo/redo stack of text editing operations
pub struct UndoStack {
stack_a: LinkedList<Operation>,
stack_b: LinkedList<Operation>,
}
impl UndoStack {
pub fn new() -> UndoStack {
UndoStack {
stack_a: LinkedList::new(),
stack_b: LinkedList::new(),
}
}
pub fn push(&mut self, op: Operation) {
self.stack_a.push_back(op);
self.stack_b.clear();
}
pub fn prev(&mut self) -> Option<Operation> {
if let Some(op) = self.stack_a.pop_back() {
self.stack_b.push_back(op.clone());
return Some(op);
} else {
return None;
}
}
pub fn next(&mut self) -> Option<Operation> {
if let Some(op) = self.stack_b.pop_back() {
self.stack_a.push_back(op.clone());
return Some(op);
} else {
return None;
}
}
}

View File

@ -1,21 +1,25 @@
#![allow(dead_code)] #![allow(dead_code)]
mod cursor;
use std::{ use std::{
cmp::{max, min}, cmp::{max, min},
collections::HashMap, collections::HashMap,
fs::File,
io::{self, BufReader, BufWriter, Write},
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
use crate::{ use ropey::Rope;
buffer::Buffer,
formatter::LineFormatter,
string_utils::{char_count, rope_slice_to_line_ending, LineEnding},
utils::{digit_count, RopeGraphemes},
};
use self::cursor::CursorSet; use backend::{buffer::Buffer, marks::Mark};
use crate::{
formatter::LineFormatter,
graphemes::{
is_grapheme_boundary, nth_next_grapheme_boundary, nth_prev_grapheme_boundary, RopeGraphemes,
},
string_utils::{rope_slice_to_line_ending, LineEnding},
utils::digit_count,
};
pub struct Editor { pub struct Editor {
pub buffer: Buffer, pub buffer: Buffer,
@ -24,7 +28,6 @@ pub struct Editor {
pub line_ending_type: LineEnding, pub line_ending_type: LineEnding,
pub soft_tabs: bool, pub soft_tabs: bool,
pub soft_tab_width: u8, pub soft_tab_width: u8,
pub dirty: bool,
// The dimensions of the total editor in screen space, including the // The dimensions of the total editor in screen space, including the
// header, gutter, etc. // header, gutter, etc.
@ -32,76 +35,90 @@ pub struct Editor {
// The dimensions and position of just the text view portion of the editor // The dimensions and position of just the text view portion of the editor
pub view_dim: (usize, usize), // (height, width) pub view_dim: (usize, usize), // (height, width)
pub view_pos: (usize, usize), // (char index, visual horizontal offset)
// The editing cursor position // Indices into the mark sets of the buffer.
pub cursors: CursorSet, pub v_msi: usize, // View position MarkSet index.
pub c_msi: usize, // Cursors MarkSet index.
} }
impl Editor { impl Editor {
/// Create a new blank editor /// Create a new blank editor
pub fn new(formatter: LineFormatter) -> Editor { pub fn new(formatter: LineFormatter) -> Editor {
let (buffer, v_msi, c_msi) = {
let mut buffer = Buffer::new("".into());
let view_idx = buffer.add_mark_set();
let cursors_idx = buffer.add_mark_set();
buffer.mark_sets[view_idx].add_mark(Mark::new(0, 0));
buffer.mark_sets[cursors_idx].add_mark(Mark::new(0, 0));
(buffer, view_idx, cursors_idx)
};
Editor { Editor {
buffer: Buffer::new(), buffer: buffer,
formatter: formatter, formatter: formatter,
file_path: PathBuf::new(), file_path: PathBuf::new(),
line_ending_type: LineEnding::LF, line_ending_type: LineEnding::LF,
soft_tabs: false, soft_tabs: false,
soft_tab_width: 4, soft_tab_width: 4,
dirty: false,
editor_dim: (0, 0), editor_dim: (0, 0),
view_dim: (0, 0), view_dim: (0, 0),
view_pos: (0, 0), v_msi: v_msi,
cursors: CursorSet::new(), c_msi: c_msi,
} }
} }
pub fn new_from_file(formatter: LineFormatter, path: &Path) -> Editor { pub fn new_from_file(formatter: LineFormatter, path: &Path) -> io::Result<Editor> {
let buf = match Buffer::new_from_file(path) { let (buffer, v_msi, c_msi) = {
Ok(b) => b, let mut buffer = Buffer::new(Rope::from_reader(BufReader::new(File::open(path)?))?);
// TODO: handle un-openable file better let view_idx = buffer.add_mark_set();
_ => panic!("Could not open file!"), let cursors_idx = buffer.add_mark_set();
buffer.mark_sets[view_idx].add_mark(Mark::new(0, 0));
buffer.mark_sets[cursors_idx].add_mark(Mark::new(0, 0));
(buffer, view_idx, cursors_idx)
}; };
let mut ed = Editor { let mut ed = Editor {
buffer: buf, buffer: buffer,
formatter: formatter, formatter: formatter,
file_path: path.to_path_buf(), file_path: path.to_path_buf(),
line_ending_type: LineEnding::LF, line_ending_type: LineEnding::LF,
soft_tabs: false, soft_tabs: false,
soft_tab_width: 4, soft_tab_width: 4,
dirty: false,
editor_dim: (0, 0), editor_dim: (0, 0),
view_dim: (0, 0), view_dim: (0, 0),
view_pos: (0, 0), v_msi: v_msi,
cursors: CursorSet::new(), c_msi: c_msi,
}; };
// For multiple-cursor testing
// let mut cur = Cursor::new();
// cur.range.0 = 30;
// cur.range.1 = 30;
// cur.update_vis_start(&(ed.buffer), &(ed.formatter));
// ed.cursors.add_cursor(cur);
ed.auto_detect_line_ending(); ed.auto_detect_line_ending();
ed.auto_detect_indentation_style(); ed.auto_detect_indentation_style();
return ed; Ok(ed)
} }
pub fn save_if_dirty(&mut self) { pub fn save_if_dirty(&mut self) -> io::Result<()> {
if self.dirty && self.file_path != PathBuf::new() { if self.buffer.is_dirty && self.file_path != PathBuf::new() {
let _ = self.buffer.save_to_file(&self.file_path); let mut f = BufWriter::new(File::create(&self.file_path)?);
self.dirty = false;
for c in self.buffer.text.chunks() {
f.write(c.as_bytes())?;
}
self.buffer.is_dirty = false;
} }
Ok(())
} }
pub fn auto_detect_line_ending(&mut self) { pub fn auto_detect_line_ending(&mut self) {
let mut line_ending_histogram: [usize; 8] = [0, 0, 0, 0, 0, 0, 0, 0]; let mut line_ending_histogram: [usize; 8] = [0, 0, 0, 0, 0, 0, 0, 0];
// Collect statistics on the first 100 lines // Collect statistics on the first 100 lines
for line in self.buffer.line_iter().take(100) { for line in self.buffer.text.lines().take(100) {
// Get the line ending // Get the line ending
let ending = if line.len_chars() == 1 { let ending = if line.len_chars() == 1 {
let g = RopeGraphemes::new(&line.slice((line.len_chars() - 1)..)) let g = RopeGraphemes::new(&line.slice((line.len_chars() - 1)..))
@ -181,7 +198,7 @@ impl Editor {
let mut last_indent = (false, 0usize); // (was_tabs, indent_count) let mut last_indent = (false, 0usize); // (was_tabs, indent_count)
// Collect statistics on the first 1000 lines // Collect statistics on the first 1000 lines
for line in self.buffer.line_iter().take(1000) { for line in self.buffer.text.lines().take(1000) {
let mut c_iter = line.chars(); let mut c_iter = line.chars();
match c_iter.next() { match c_iter.next() {
Some('\t') => { Some('\t') => {
@ -254,7 +271,7 @@ impl Editor {
/// Updates the view dimensions. /// Updates the view dimensions.
pub fn update_dim(&mut self, h: usize, w: usize) { pub fn update_dim(&mut self, h: usize, w: usize) {
let line_count_digits = digit_count(self.buffer.line_count() as u32, 10) as usize; let line_count_digits = digit_count(self.buffer.text.len_lines() as u32, 10) as usize;
self.editor_dim = (h, w); self.editor_dim = (h, w);
// Minus 1 vertically for the header, minus two more than the digits in // Minus 1 vertically for the header, minus two more than the digits in
@ -267,332 +284,258 @@ impl Editor {
pub fn undo(&mut self) { pub fn undo(&mut self) {
// TODO: handle multiple cursors properly // TODO: handle multiple cursors properly
if let Some(pos) = self.buffer.undo() { if let Some((_start, end)) = self.buffer.undo() {
self.cursors.truncate(1); self.buffer.mark_sets[self.c_msi].reduce_to_main();
self.cursors[0].range.0 = pos; self.buffer.mark_sets[self.c_msi][0].head = end;
self.cursors[0].range.1 = pos; self.buffer.mark_sets[self.c_msi][0].tail = end;
self.cursors[0].update_vis_start(&(self.buffer), &(self.formatter)); self.buffer.mark_sets[self.c_msi][0].hh_pos = None;
self.move_view_to_cursor(); self.move_view_to_cursor();
self.dirty = true;
self.cursors.make_consistent();
} }
} }
pub fn redo(&mut self) { pub fn redo(&mut self) {
// TODO: handle multiple cursors properly // TODO: handle multiple cursors properly
if let Some(pos) = self.buffer.redo() { if let Some((_start, end)) = self.buffer.redo() {
self.cursors.truncate(1); self.buffer.mark_sets[self.c_msi].reduce_to_main();
self.cursors[0].range.0 = pos; self.buffer.mark_sets[self.c_msi][0].head = end;
self.cursors[0].range.1 = pos; self.buffer.mark_sets[self.c_msi][0].tail = end;
self.cursors[0].update_vis_start(&(self.buffer), &(self.formatter)); self.buffer.mark_sets[self.c_msi][0].hh_pos = None;
self.move_view_to_cursor(); self.move_view_to_cursor();
self.dirty = true;
self.cursors.make_consistent();
} }
} }
/// Moves the editor's view the minimum amount to show the cursor /// Moves the editor's view the minimum amount to show the cursor
pub fn move_view_to_cursor(&mut self) { pub fn move_view_to_cursor(&mut self) {
// 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 = self let c_first = self.formatter.set_horizontal(
.formatter &self.buffer.text,
.set_horizontal(&self.buffer, self.view_pos.0, 0); self.buffer.mark_sets[self.v_msi][0].head,
let mut c_last = 0,
self.formatter );
.offset_vertical(&self.buffer, c_first, self.view_dim.0 as isize - 1); let mut c_last = self.formatter.offset_vertical(
&self.buffer.text,
c_first,
self.view_dim.0 as isize - 1,
);
c_last = self c_last = self
.formatter .formatter
.set_horizontal(&self.buffer, c_last, self.view_dim.1); .set_horizontal(&self.buffer.text, c_last, self.view_dim.1);
// 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 { let cursor_head = self.buffer.mark_sets[self.c_msi].main().unwrap().head;
self.view_pos.0 = self.cursors[0].range.0; if cursor_head < c_first {
} else if self.cursors[0].range.0 > c_last { self.buffer.mark_sets[self.v_msi][0].head = cursor_head;
self.view_pos.0 = self.formatter.offset_vertical( } else if cursor_head > c_last {
&self.buffer, self.buffer.mark_sets[self.v_msi][0].head = self.formatter.offset_vertical(
self.cursors[0].range.0, &self.buffer.text,
cursor_head,
-(self.view_dim.0 as isize), -(self.view_dim.0 as isize),
); );
} }
} }
pub fn insert_text_at_cursor(&mut self, text: &str) { pub fn insert_text_at_cursor(&mut self, text: &str) {
self.cursors.make_consistent(); // TODO: handle multiple cursors.
let mark = self.buffer.mark_sets[self.c_msi].main().unwrap();
let range = mark.range();
let str_len = char_count(text); self.buffer.edit((range.start, range.end), text);
let mut offset = 0;
for c in self.cursors.iter_mut() {
// Insert text
self.buffer.insert_text(text, c.range.0 + offset);
self.dirty = true;
// Move cursor
c.range.0 += str_len + offset;
c.range.1 += str_len + offset;
c.update_vis_start(&(self.buffer), &(self.formatter));
// Update offset
offset += str_len;
}
// Adjust view // Adjust view
self.move_view_to_cursor(); self.move_view_to_cursor();
} }
pub fn insert_tab_at_cursor(&mut self) { pub fn insert_tab_at_cursor(&mut self) {
self.cursors.make_consistent(); // TODO: handle multiple cursors.
let mark = self.buffer.mark_sets[self.c_msi].main().unwrap();
let range = mark.range();
if self.soft_tabs { if self.soft_tabs {
let mut offset = 0; // Figure out how many spaces to insert
let vis_pos = self
.formatter
.get_horizontal(&self.buffer.text, range.start);
// TODO: handle tab settings
let next_tab_stop =
((vis_pos / self.soft_tab_width as usize) + 1) * self.soft_tab_width as usize;
let space_count = min(next_tab_stop - vis_pos, 8);
for c in self.cursors.iter_mut() { // Insert spaces
// Update cursor with offset let space_strs = [
c.range.0 += offset; "", " ", " ", " ", " ", " ", " ", " ", " ",
c.range.1 += offset; ];
self.buffer
// Figure out how many spaces to insert .edit((range.start, range.end), space_strs[space_count]);
let vis_pos = self.formatter.get_horizontal(&self.buffer, c.range.0);
// TODO: handle tab settings
let next_tab_stop =
((vis_pos / self.soft_tab_width as usize) + 1) * self.soft_tab_width as usize;
let space_count = min(next_tab_stop - vis_pos, 8);
// Insert spaces
let space_strs = [
"", " ", " ", " ", " ", " ", " ", " ", " ",
];
self.buffer.insert_text(space_strs[space_count], c.range.0);
self.dirty = true;
// Move cursor
c.range.0 += space_count;
c.range.1 += space_count;
c.update_vis_start(&(self.buffer), &(self.formatter));
// Update offset
offset += space_count;
}
// Adjust view
self.move_view_to_cursor();
} else { } else {
self.insert_text_at_cursor("\t"); self.buffer.edit((range.start, range.end), "\t");
} }
}
pub fn backspace_at_cursor(&mut self) { // Adjust view
self.remove_text_behind_cursor(1); self.move_view_to_cursor();
} }
pub fn remove_text_behind_cursor(&mut self, grapheme_count: usize) { pub fn remove_text_behind_cursor(&mut self, grapheme_count: usize) {
self.cursors.make_consistent(); // TODO: handle multiple cursors.
let mark = self.buffer.mark_sets[self.c_msi].main().unwrap();
let range = mark.range();
let mut offset = 0; // Do nothing if there's nothing to delete.
if range.start == 0 {
for c in self.cursors.iter_mut() { return;
// Update cursor with offset
c.range.0 -= offset;
c.range.1 -= offset;
// Do nothing if there's nothing to delete.
if c.range.0 == 0 {
continue;
}
let len = c.range.0 - self.buffer.nth_prev_grapheme(c.range.0, grapheme_count);
// Remove text
self.buffer.remove_text_before(c.range.0, len);
self.dirty = true;
// Move cursor
c.range.0 -= len;
c.range.1 -= len;
c.update_vis_start(&(self.buffer), &(self.formatter));
// Update offset
offset += len;
} }
self.cursors.make_consistent(); let pre =
nth_prev_grapheme_boundary(&self.buffer.text.slice(..), range.start, grapheme_count);
// Remove text
self.buffer.edit((pre, range.start), "");
// Adjust view // Adjust view
self.move_view_to_cursor(); self.move_view_to_cursor();
} }
pub fn remove_text_in_front_of_cursor(&mut self, grapheme_count: usize) { pub fn remove_text_in_front_of_cursor(&mut self, grapheme_count: usize) {
self.cursors.make_consistent(); // TODO: handle multiple cursors.
let mark = self.buffer.mark_sets[self.c_msi].main().unwrap();
let range = mark.range();
let mut offset = 0; // Do nothing if there's nothing to delete.
if range.end == self.buffer.text.len_chars() {
for c in self.cursors.iter_mut() { return;
// Update cursor with offset
c.range.0 -= min(c.range.0, offset);
c.range.1 -= min(c.range.1, offset);
// Do nothing if there's nothing to delete.
if c.range.1 == self.buffer.char_count() {
return;
}
let len = self.buffer.nth_next_grapheme(c.range.1, grapheme_count) - c.range.1;
// Remove text
self.buffer.remove_text_after(c.range.1, len);
self.dirty = true;
// Move cursor
c.update_vis_start(&(self.buffer), &(self.formatter));
// Update offset
offset += len;
} }
self.cursors.make_consistent(); let post =
nth_next_grapheme_boundary(&self.buffer.text.slice(..), range.end, grapheme_count);
// Remove text
self.buffer.edit((range.end, post), "");
// Adjust view // Adjust view
self.move_view_to_cursor(); self.move_view_to_cursor();
} }
pub fn remove_text_inside_cursor(&mut self) { pub fn remove_text_inside_cursor(&mut self) {
self.cursors.make_consistent(); // TODO: handle multiple cursors.
let mark = self.buffer.mark_sets[self.c_msi].main().unwrap();
let range = mark.range();
let mut offset = 0; if range.start < range.end {
self.buffer.edit((range.start, range.end), "");
for c in self.cursors.iter_mut() {
// Update cursor with offset
c.range.0 -= min(c.range.0, offset);
c.range.1 -= min(c.range.1, offset);
// If selection, remove text
if c.range.0 < c.range.1 {
let len = c.range.1 - c.range.0;
self.buffer
.remove_text_before(c.range.0, c.range.1 - c.range.0);
self.dirty = true;
// Move cursor
c.range.1 = c.range.0;
// Update offset
offset += len;
}
c.update_vis_start(&(self.buffer), &(self.formatter));
} }
self.cursors.make_consistent();
// Adjust view // Adjust view
self.move_view_to_cursor(); self.move_view_to_cursor();
} }
pub fn cursor_to_beginning_of_buffer(&mut self) { pub fn cursor_to_beginning_of_buffer(&mut self) {
self.cursors = CursorSet::new(); self.buffer.mark_sets[self.c_msi].clear();
self.buffer.mark_sets[self.c_msi].add_mark(Mark::new(0, 0));
self.cursors[0].range = (0, 0); // Adjust view.
self.cursors[0].update_vis_start(&(self.buffer), &(self.formatter));
// Adjust view
self.move_view_to_cursor(); self.move_view_to_cursor();
} }
pub fn cursor_to_end_of_buffer(&mut self) { pub fn cursor_to_end_of_buffer(&mut self) {
let end = self.buffer.char_count(); let end = self.buffer.text.len_chars();
self.cursors = CursorSet::new(); self.buffer.mark_sets[self.c_msi].clear();
self.cursors[0].range = (end, end); self.buffer.mark_sets[self.c_msi].add_mark(Mark::new(end, end));
self.cursors[0].update_vis_start(&(self.buffer), &(self.formatter));
// Adjust view // Adjust view.
self.move_view_to_cursor(); self.move_view_to_cursor();
} }
pub fn cursor_left(&mut self, n: usize) { pub fn cursor_left(&mut self, n: usize) {
for c in self.cursors.iter_mut() { for mark in self.buffer.mark_sets[self.c_msi].iter_mut() {
c.range.0 = self.buffer.nth_prev_grapheme(c.range.0, n); mark.head = nth_prev_grapheme_boundary(&self.buffer.text.slice(..), mark.head, n);
c.range.1 = c.range.0; mark.tail = mark.head;
c.update_vis_start(&(self.buffer), &(self.formatter)); mark.hh_pos = None;
} }
self.buffer.mark_sets[self.c_msi].make_consistent();
// Adjust view // Adjust view
self.move_view_to_cursor(); self.move_view_to_cursor();
} }
pub fn cursor_right(&mut self, n: usize) { pub fn cursor_right(&mut self, n: usize) {
for c in self.cursors.iter_mut() { for mark in self.buffer.mark_sets[self.c_msi].iter_mut() {
c.range.1 = self.buffer.nth_next_grapheme(c.range.1, n); mark.head = nth_next_grapheme_boundary(&self.buffer.text.slice(..), mark.head, n);
c.range.0 = c.range.1; mark.tail = mark.head;
c.update_vis_start(&(self.buffer), &(self.formatter)); mark.hh_pos = None;
} }
self.buffer.mark_sets[self.c_msi].make_consistent();
// Adjust view // Adjust view
self.move_view_to_cursor(); self.move_view_to_cursor();
} }
pub fn cursor_up(&mut self, n: usize) { pub fn cursor_up(&mut self, n: usize) {
for c in self.cursors.iter_mut() { for mark in self.buffer.mark_sets[self.c_msi].iter_mut() {
if mark.hh_pos == None {
mark.hh_pos = Some(self.formatter.get_horizontal(&self.buffer.text, mark.head));
}
let vmove = -1 * n as isize; let vmove = -1 * n as isize;
let mut temp_index = self let mut temp_index =
.formatter self.formatter
.offset_vertical(&self.buffer, c.range.0, vmove); .offset_vertical(&self.buffer.text, mark.head, vmove);
temp_index = self temp_index =
.formatter self.formatter
.set_horizontal(&self.buffer, temp_index, c.vis_start); .set_horizontal(&self.buffer.text, temp_index, mark.hh_pos.unwrap());
if !self.buffer.is_grapheme(temp_index) { if !is_grapheme_boundary(&self.buffer.text.slice(..), temp_index) {
temp_index = self.buffer.nth_prev_grapheme(temp_index, 1); temp_index = nth_prev_grapheme_boundary(&self.buffer.text.slice(..), temp_index, 1);
} }
if temp_index == c.range.0 { if temp_index == mark.head {
// We were already at the top. // We were already at the top.
c.range.0 = 0; mark.head = 0;
c.range.1 = 0; mark.tail = 0;
c.update_vis_start(&(self.buffer), &(self.formatter)); mark.hh_pos = None;
} else { } else {
c.range.0 = temp_index; mark.head = temp_index;
c.range.1 = temp_index; mark.tail = temp_index;
} }
} }
self.buffer.mark_sets[self.c_msi].make_consistent();
// Adjust view // Adjust view
self.move_view_to_cursor(); self.move_view_to_cursor();
} }
pub fn cursor_down(&mut self, n: usize) { pub fn cursor_down(&mut self, n: usize) {
for c in self.cursors.iter_mut() { for mark in self.buffer.mark_sets[self.c_msi].iter_mut() {
if mark.hh_pos == None {
mark.hh_pos = Some(self.formatter.get_horizontal(&self.buffer.text, mark.head));
}
let vmove = n as isize; let vmove = n as isize;
let mut temp_index = self let mut temp_index =
.formatter self.formatter
.offset_vertical(&self.buffer, c.range.0, vmove); .offset_vertical(&self.buffer.text, mark.head, vmove);
temp_index = self temp_index =
.formatter self.formatter
.set_horizontal(&self.buffer, temp_index, c.vis_start); .set_horizontal(&self.buffer.text, temp_index, mark.hh_pos.unwrap());
if !self.buffer.is_grapheme(temp_index) { if !is_grapheme_boundary(&self.buffer.text.slice(..), temp_index) {
temp_index = self.buffer.nth_prev_grapheme(temp_index, 1); temp_index = nth_prev_grapheme_boundary(&self.buffer.text.slice(..), temp_index, 1);
} }
if temp_index == c.range.0 { if temp_index == mark.head {
// We were already at the bottom. // We were already at the bottom.
c.range.0 = self.buffer.char_count(); mark.head = self.buffer.text.len_chars();
c.range.1 = self.buffer.char_count(); mark.tail = self.buffer.text.len_chars();
c.update_vis_start(&(self.buffer), &(self.formatter)); mark.hh_pos = None;
} else { } else {
c.range.0 = temp_index; mark.head = temp_index;
c.range.1 = temp_index; mark.tail = temp_index;
} }
} }
self.buffer.mark_sets[self.c_msi].make_consistent();
// Adjust view // Adjust view
self.move_view_to_cursor(); self.move_view_to_cursor();
@ -600,9 +543,9 @@ impl Editor {
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.offset_vertical( self.buffer.mark_sets[self.v_msi][0].head = self.formatter.offset_vertical(
&self.buffer, &self.buffer.text,
self.view_pos.0, self.buffer.mark_sets[self.v_msi][0].head,
-1 * move_amount as isize, -1 * move_amount as isize,
); );
@ -614,9 +557,11 @@ impl Editor {
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.buffer.mark_sets[self.v_msi][0].head = self.formatter.offset_vertical(
self.formatter &self.buffer.text,
.offset_vertical(&self.buffer, self.view_pos.0, move_amount as isize); self.buffer.mark_sets[self.v_msi][0].head,
move_amount as isize,
);
self.cursor_down(move_amount); self.cursor_down(move_amount);
@ -625,12 +570,23 @@ impl Editor {
} }
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)); self.buffer.mark_sets[self.c_msi].reduce_to_main();
self.cursors.truncate(1); if self.buffer.mark_sets[self.c_msi][0].hh_pos == None {
self.cursors[0].range.0 = self.buffer.mark_sets[self.c_msi][0].hh_pos = Some(
self.formatter self.formatter
.set_horizontal(&self.buffer, pos, self.cursors[0].vis_start); .get_horizontal(&self.buffer.text, self.buffer.mark_sets[self.c_msi][0].head),
self.cursors[0].range.1 = self.cursors[0].range.0; );
}
let pos = self.buffer.text.line_to_char(n);
let pos = self.formatter.set_horizontal(
&self.buffer.text,
pos,
self.buffer.mark_sets[self.c_msi][0].hh_pos.unwrap(),
);
self.buffer.mark_sets[self.c_msi][0].head = pos;
self.buffer.mark_sets[self.c_msi][0].tail = pos;
// Adjust view // Adjust view
self.move_view_to_cursor(); self.move_view_to_cursor();

View File

@ -3,10 +3,9 @@ use std::borrow::Cow;
use ropey::{Rope, RopeSlice}; use ropey::{Rope, RopeSlice};
use crate::{ use crate::{
buffer::Buffer, graphemes::{grapheme_width, is_grapheme_boundary, prev_grapheme_boundary, RopeGraphemes},
string_utils::char_count, string_utils::char_count,
string_utils::str_is_whitespace, string_utils::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.
@ -42,10 +41,14 @@ impl LineFormatter {
/// Returns an iterator over the blocks of the buffer, starting at the /// Returns an iterator over the blocks of the buffer, starting at the
/// block containing the given char. Also returns the offset of that char /// block containing the given char. Also returns the offset of that char
/// relative to the start of the first block. /// relative to the start of the first block.
pub fn iter<'b>(&'b self, buf: &'b Buffer, char_idx: usize) -> (Blocks<'b>, usize) { pub fn iter<'b>(&'b self, buf: &'b Rope, char_idx: usize) -> (Blocks<'b>, usize) {
// Get the line. // Get the line.
let (line_i, col_i) = buf.index_to_line_col(char_idx); let (line_i, col_i) = {
let line = buf.get_line(line_i); let line_idx = buf.char_to_line(char_idx);
let col_idx = char_idx - buf.line_to_char(line_idx);
(line_idx, col_idx)
};
let line = buf.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_index, block_range) = block_index_and_range(&line, col_i); let (block_index, block_range) = block_index_and_range(&line, col_i);
@ -54,7 +57,7 @@ impl LineFormatter {
( (
Blocks { Blocks {
formatter: self, formatter: self,
buf: &buf.text, buf: buf,
line_idx: line_i, line_idx: line_i,
line_block_count: block_count(&line), line_block_count: block_count(&line),
block_idx: block_index, block_idx: block_index,
@ -63,8 +66,8 @@ impl LineFormatter {
) )
} }
/// Converts from char index to the horizontal 2d char index. /// Converts from char index to its formatted horizontal 2d position.
pub fn get_horizontal(&self, buf: &Buffer, char_idx: usize) -> usize { pub fn get_horizontal(&self, buf: &Rope, char_idx: usize) -> usize {
let (_, vis_iter, char_offset) = self.block_vis_iter_and_char_offset(buf, char_idx); let (_, vis_iter, char_offset) = self.block_vis_iter_and_char_offset(buf, char_idx);
// Traverse the iterator and find the horizontal position of the char // Traverse the iterator and find the horizontal position of the char
@ -92,7 +95,7 @@ impl LineFormatter {
/// 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 (or as close as is /// but offset to have the desired horizontal position (or as close as is
/// possible. /// possible.
pub fn set_horizontal(&self, buf: &Buffer, char_idx: usize, horizontal: usize) -> usize { pub fn set_horizontal(&self, buf: &Rope, char_idx: usize, horizontal: usize) -> usize {
let (_, vis_iter, char_offset) = self.block_vis_iter_and_char_offset(buf, char_idx); let (_, vis_iter, char_offset) = self.block_vis_iter_and_char_offset(buf, char_idx);
let mut hpos_char_idx = None; let mut hpos_char_idx = None;
@ -134,7 +137,7 @@ impl LineFormatter {
// If we reached the end of the text, return the last char index. // If we reached the end of the text, return the last char index.
let end_i = char_idx - char_offset + i; let end_i = char_idx - char_offset + i;
let end_last_i = char_idx - char_offset + last_i; let end_last_i = char_idx - char_offset + last_i;
if buf.text.len_chars() == end_i { if buf.len_chars() == end_i {
return end_i; return end_i;
} else { } else {
return end_last_i; return end_last_i;
@ -143,7 +146,7 @@ impl LineFormatter {
/// Takes a char index and a visual vertical offset, and returns the char /// Takes a char index and a visual vertical offset, and returns the char
/// index after that visual offset is applied. /// index after that visual offset is applied.
pub fn offset_vertical(&self, buf: &Buffer, char_idx: usize, v_offset: isize) -> usize { pub fn offset_vertical(&self, buf: &Rope, char_idx: usize, v_offset: isize) -> usize {
let mut char_idx = char_idx; let mut char_idx = char_idx;
let mut v_offset = v_offset; let mut v_offset = v_offset;
while v_offset != 0 { while v_offset != 0 {
@ -171,9 +174,9 @@ impl LineFormatter {
} }
} else if offset_char_v_pos >= block_v_dim as isize { } else if offset_char_v_pos >= block_v_dim as isize {
// If we're off the end of the block. // If we're off the end of the block.
char_idx = (char_idx + block.len_chars() - char_offset).min(buf.text.len_chars()); char_idx = (char_idx + block.len_chars() - char_offset).min(buf.len_chars());
v_offset -= block_v_dim as isize - char_v_pos as isize; v_offset -= block_v_dim as isize - char_v_pos as isize;
if char_idx == buf.text.len_chars() { if char_idx == buf.len_chars() {
break; break;
} }
} else { } else {
@ -230,13 +233,13 @@ impl LineFormatter {
/// char's offset within that iter. /// char's offset within that iter.
fn block_vis_iter_and_char_offset<'b>( fn block_vis_iter_and_char_offset<'b>(
&self, &self,
buf: &'b Buffer, buf: &'b Rope,
char_idx: usize, char_idx: usize,
) -> (RopeSlice<'b>, BlockVisIter<'b>, usize) { ) -> (RopeSlice<'b>, BlockVisIter<'b>, usize) {
let line_i = buf.text.char_to_line(char_idx); let line_i = buf.char_to_line(char_idx);
let line_start = buf.text.line_to_char(line_i); let line_start = buf.line_to_char(line_i);
let line_end = buf.text.line_to_char(line_i + 1); let line_end = buf.line_to_char(line_i + 1);
let line = buf.text.slice(line_start..line_end); let line = buf.slice(line_start..line_end);
// 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_index, block_range) = block_index_and_range(&line, char_idx - line_start); let (block_index, block_range) = block_index_and_range(&line, char_idx - line_start);

212
src/graphemes.rs Normal file
View File

@ -0,0 +1,212 @@
use ropey::{iter::Chunks, str_utils::byte_to_char_idx, RopeSlice};
use unicode_segmentation::{GraphemeCursor, GraphemeIncomplete};
use unicode_width::UnicodeWidthStr;
pub fn grapheme_width(g: &str) -> usize {
if g.as_bytes()[0] <= 127 {
// Fast-path ascii.
// Point 1: theoretically, ascii control characters should have zero
// width, but in our case we actually want them to have width: if they
// show up in text, we want to treat them as textual elements that can
// be editied. So we can get away with making all ascii single width
// here.
// Point 2: we're only examining the first codepoint here, which means
// we're ignoring graphemes formed with combining characters. However,
// if it starts with ascii, it's going to be a single-width grapeheme
// regardless, so, again, we can get away with that here.
// Point 3: we're only examining the first _byte_. But for utf8, when
// checking for ascii range values only, that works.
1
} else {
// We use max(1) here because all grapeheme clusters--even illformed
// ones--should have at least some width so they can be edited
// properly.
UnicodeWidthStr::width(g).max(1)
}
}
pub fn nth_prev_grapheme_boundary(slice: &RopeSlice, char_idx: usize, n: usize) -> usize {
// TODO: implement this more efficiently. This has to do a lot of
// re-scanning of rope chunks. Probably move the main implementation here,
// and have prev_grapheme_boundary call this instead.
let mut char_idx = char_idx;
for _ in 0..n {
char_idx = prev_grapheme_boundary(slice, char_idx);
}
char_idx
}
/// Finds the previous grapheme boundary before the given char position.
pub fn prev_grapheme_boundary(slice: &RopeSlice, char_idx: usize) -> usize {
// Bounds check
debug_assert!(char_idx <= slice.len_chars());
// We work with bytes for this, so convert.
let byte_idx = slice.char_to_byte(char_idx);
// Get the chunk with our byte index in it.
let (mut chunk, mut chunk_byte_idx, mut chunk_char_idx, _) = slice.chunk_at_byte(byte_idx);
// Set up the grapheme cursor.
let mut gc = GraphemeCursor::new(byte_idx, slice.len_bytes(), true);
// Find the previous grapheme cluster boundary.
loop {
match gc.prev_boundary(chunk, chunk_byte_idx) {
Ok(None) => return 0,
Ok(Some(n)) => {
let tmp = byte_to_char_idx(chunk, n - chunk_byte_idx);
return chunk_char_idx + tmp;
}
Err(GraphemeIncomplete::PrevChunk) => {
let (a, b, c, _) = slice.chunk_at_byte(chunk_byte_idx - 1);
chunk = a;
chunk_byte_idx = b;
chunk_char_idx = c;
}
Err(GraphemeIncomplete::PreContext(n)) => {
let ctx_chunk = slice.chunk_at_byte(n - 1).0;
gc.provide_context(ctx_chunk, n - ctx_chunk.len());
}
_ => unreachable!(),
}
}
}
pub fn nth_next_grapheme_boundary(slice: &RopeSlice, char_idx: usize, n: usize) -> usize {
// TODO: implement this more efficiently. This has to do a lot of
// re-scanning of rope chunks. Probably move the main implementation here,
// and have next_grapheme_boundary call this instead.
let mut char_idx = char_idx;
for _ in 0..n {
char_idx = next_grapheme_boundary(slice, char_idx);
}
char_idx
}
/// Finds the next grapheme boundary after the given char position.
pub fn next_grapheme_boundary(slice: &RopeSlice, char_idx: usize) -> usize {
// Bounds check
debug_assert!(char_idx <= slice.len_chars());
// We work with bytes for this, so convert.
let byte_idx = slice.char_to_byte(char_idx);
// Get the chunk with our byte index in it.
let (mut chunk, mut chunk_byte_idx, mut chunk_char_idx, _) = slice.chunk_at_byte(byte_idx);
// Set up the grapheme cursor.
let mut gc = GraphemeCursor::new(byte_idx, slice.len_bytes(), true);
// Find the next grapheme cluster boundary.
loop {
match gc.next_boundary(chunk, chunk_byte_idx) {
Ok(None) => return slice.len_chars(),
Ok(Some(n)) => {
let tmp = byte_to_char_idx(chunk, n - chunk_byte_idx);
return chunk_char_idx + tmp;
}
Err(GraphemeIncomplete::NextChunk) => {
chunk_byte_idx += chunk.len();
let (a, _, c, _) = slice.chunk_at_byte(chunk_byte_idx);
chunk = a;
chunk_char_idx = c;
}
Err(GraphemeIncomplete::PreContext(n)) => {
let ctx_chunk = slice.chunk_at_byte(n - 1).0;
gc.provide_context(ctx_chunk, n - ctx_chunk.len());
}
_ => unreachable!(),
}
}
}
/// Returns whether the given char position is a grapheme boundary.
pub fn is_grapheme_boundary(slice: &RopeSlice, char_idx: usize) -> bool {
// Bounds check
debug_assert!(char_idx <= slice.len_chars());
// We work with bytes for this, so convert.
let byte_idx = slice.char_to_byte(char_idx);
// Get the chunk with our byte index in it.
let (chunk, chunk_byte_idx, _, _) = slice.chunk_at_byte(byte_idx);
// Set up the grapheme cursor.
let mut gc = GraphemeCursor::new(byte_idx, slice.len_bytes(), true);
// Determine if the given position is a grapheme cluster boundary.
loop {
match gc.is_boundary(chunk, chunk_byte_idx) {
Ok(n) => return n,
Err(GraphemeIncomplete::PreContext(n)) => {
let (ctx_chunk, ctx_byte_start, _, _) = slice.chunk_at_byte(n - 1);
gc.provide_context(ctx_chunk, ctx_byte_start);
}
_ => unreachable!(),
}
}
}
/// An iterator over the graphemes of a RopeSlice.
#[derive(Clone)]
pub struct RopeGraphemes<'a> {
text: RopeSlice<'a>,
chunks: Chunks<'a>,
cur_chunk: &'a str,
cur_chunk_start: usize,
cursor: GraphemeCursor,
}
impl<'a> RopeGraphemes<'a> {
pub fn new<'b>(slice: &RopeSlice<'b>) -> RopeGraphemes<'b> {
let mut chunks = slice.chunks();
let first_chunk = chunks.next().unwrap_or("");
RopeGraphemes {
text: *slice,
chunks: chunks,
cur_chunk: first_chunk,
cur_chunk_start: 0,
cursor: GraphemeCursor::new(0, slice.len_bytes(), true),
}
}
}
impl<'a> Iterator for RopeGraphemes<'a> {
type Item = RopeSlice<'a>;
fn next(&mut self) -> Option<RopeSlice<'a>> {
let a = self.cursor.cur_cursor();
let b;
loop {
match self
.cursor
.next_boundary(self.cur_chunk, self.cur_chunk_start)
{
Ok(None) => {
return None;
}
Ok(Some(n)) => {
b = n;
break;
}
Err(GraphemeIncomplete::NextChunk) => {
self.cur_chunk_start += self.cur_chunk.len();
self.cur_chunk = self.chunks.next().unwrap_or("");
}
_ => unreachable!(),
}
}
if a < self.cur_chunk_start {
let a_char = self.text.byte_to_char(a);
let b_char = self.text.byte_to_char(b);
Some(self.text.slice(a_char..b_char))
} else {
let a2 = a - self.cur_chunk_start;
let b2 = b - self.cur_chunk_start;
Some((&self.cur_chunk[a2..b2]).into())
}
}
}

View File

@ -5,9 +5,9 @@ use editor::Editor;
use formatter::LineFormatter; use formatter::LineFormatter;
use term_ui::TermUI; use term_ui::TermUI;
mod buffer;
mod editor; mod editor;
mod formatter; mod formatter;
mod graphemes;
mod string_utils; mod string_utils;
mod term_ui; mod term_ui;
mod utils; mod utils;
@ -30,6 +30,7 @@ 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(LineFormatter::new(4), &Path::new(&filepath[..])) Editor::new_from_file(LineFormatter::new(4), &Path::new(&filepath[..]))
.expect(&format!("Couldn't open file '{}'.", filepath))
} else { } else {
Editor::new(LineFormatter::new(4)) Editor::new(LineFormatter::new(4))
}; };

View File

@ -243,7 +243,7 @@ impl TermUI {
code: KeyCode::Char('s'), code: KeyCode::Char('s'),
modifiers: KeyModifiers::CONTROL, modifiers: KeyModifiers::CONTROL,
} => { } => {
self.editor.save_if_dirty(); self.editor.save_if_dirty().expect("For some reason the file couldn't be saved. Also, TODO: this code path shouldn't panic.");
} }
KeyEvent { KeyEvent {
@ -342,7 +342,7 @@ impl TermUI {
code: KeyCode::Backspace, code: KeyCode::Backspace,
modifiers: EMPTY_MOD, modifiers: EMPTY_MOD,
} => { } => {
self.editor.backspace_at_cursor(); self.editor.remove_text_behind_cursor(1);
} }
KeyEvent { KeyEvent {
@ -464,16 +464,17 @@ impl TermUI {
// Filename and dirty marker // Filename and dirty marker
let filename = editor.file_path.display(); let filename = editor.file_path.display();
let dirty_char = if editor.dirty { "*" } else { "" }; let dirty_char = if editor.buffer.is_dirty { "*" } else { "" };
let name = format!("{}{}", filename, dirty_char); let name = format!("{}{}", filename, dirty_char);
self.screen.draw(c1.1 + 1, c1.0, &name[..], STYLE_INFO); self.screen.draw(c1.1 + 1, c1.0, &name[..], STYLE_INFO);
// Percentage position in document // Percentage position in document
// TODO: use view instead of cursor for calculation if there is more // TODO: use view instead of cursor for calculation if there is more
// than one cursor. // than one cursor.
let percentage: usize = if editor.buffer.char_count() > 0 { let percentage: usize = if editor.buffer.text.len_chars() > 0 {
(((editor.cursors[0].range.0 as f32) / (editor.buffer.char_count() as f32)) * 100.0) (((editor.buffer.mark_sets[editor.c_msi].main().unwrap().head as f32)
as usize / (editor.buffer.text.len_chars() as f32))
* 100.0) as usize
} else { } else {
100 100
}; };
@ -510,12 +511,15 @@ impl TermUI {
} }
fn draw_editor_text(&self, editor: &Editor, c1: (usize, usize), c2: (usize, usize)) { fn draw_editor_text(&self, editor: &Editor, c1: (usize, usize), c2: (usize, usize)) {
let view_pos = editor.buffer.mark_sets[editor.v_msi][0].head;
let cursors = &editor.buffer.mark_sets[editor.c_msi];
// 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 = editor.buffer.text.char_to_line(editor.view_pos.0); let line_index = editor.buffer.text.char_to_line(view_pos);
let (blocks_iter, char_offset) = editor.formatter.iter(&editor.buffer, editor.view_pos.0); let (blocks_iter, char_offset) = editor.formatter.iter(&editor.buffer.text, view_pos);
let vis_line_offset = blocks_iter.clone().next().unwrap().0.vpos(char_offset); let vis_line_offset = blocks_iter.clone().next().unwrap().0.vpos(char_offset);
@ -537,7 +541,7 @@ impl TermUI {
// Loop through the blocks, printing them to the screen. // Loop through the blocks, printing them to the screen.
let mut is_first_loop = true; let mut is_first_loop = true;
let mut line_num = line_index + 1; let mut line_num = line_index + 1;
let mut char_index = editor.view_pos.0 - char_offset; let mut char_index = view_pos - char_offset;
for (block_vis_iter, is_line_start) in blocks_iter { for (block_vis_iter, is_line_start) in blocks_iter {
if is_line_start && !is_first_loop { if is_line_start && !is_first_loop {
line_num += 1; line_num += 1;
@ -578,7 +582,7 @@ impl TermUI {
screen_line += 1; screen_line += 1;
last_pos_y = pos_y; last_pos_y = pos_y;
} }
let px = pos_x as isize + screen_col - editor.view_pos.1 as isize; let px = pos_x as isize + screen_col;
let py = screen_line; let py = screen_line;
// If we're off the bottom, we're done // If we're off the bottom, we're done
@ -590,8 +594,8 @@ impl TermUI {
if (px >= c1.1 as isize) && (py >= c1.0 as isize) && (px <= c2.1 as isize) { if (px >= c1.1 as isize) && (py >= c1.0 as isize) && (px <= c2.1 as isize) {
// Check if the character is within a cursor // Check if the character is within a cursor
let mut at_cursor = false; let mut at_cursor = false;
for c in editor.cursors.iter() { for c in cursors.iter() {
if char_index >= c.range.0 && char_index <= c.range.1 { if char_index >= c.range().start && char_index <= c.range().end {
at_cursor = true; at_cursor = true;
self.screen.set_cursor(px as usize, py as usize); self.screen.set_cursor(px as usize, py as usize);
} }
@ -636,18 +640,19 @@ impl TermUI {
// Check if the character is within a cursor // Check if the character is within a cursor
let mut at_cursor = false; let mut at_cursor = false;
for c in editor.cursors.iter() { for c in cursors.iter() {
if char_index >= c.range.0 && char_index <= c.range.1 { if char_index >= c.range().start && char_index <= c.range().end {
at_cursor = true; at_cursor = true;
} }
} }
if at_cursor { if at_cursor {
// 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.get_horizontal(
.formatter &self.editor.buffer.text,
.get_horizontal(&self.editor.buffer, self.editor.buffer.char_count()); self.editor.buffer.text.len_chars(),
let mut px = pos_x as isize + screen_col - editor.view_pos.1 as isize; );
let mut px = pos_x as isize + screen_col;
let mut py = screen_line - 1; let mut py = screen_line - 1;
if px > c2.1 as isize { if px > c2.1 as isize {
px = c1.1 as isize + screen_col; px = c1.1 as isize + screen_col;

View File

@ -1,7 +1,4 @@
use ropey::{iter::Chunks, str_utils::byte_to_char_idx, RopeSlice};
use time::Instant; use time::Instant;
use unicode_segmentation::{GraphemeCursor, GraphemeIncomplete};
use unicode_width::UnicodeWidthStr;
pub fn digit_count(mut n: u32, b: u32) -> u32 { pub fn digit_count(mut n: u32, b: u32) -> u32 {
let mut d = 0; let mut d = 0;
@ -14,197 +11,6 @@ pub fn digit_count(mut n: u32, b: u32) -> u32 {
} }
} }
//=============================================================
pub fn grapheme_width(g: &str) -> usize {
if g.as_bytes()[0] <= 127 {
// Fast-path ascii.
// Point 1: theoretically, ascii control characters should have zero
// width, but in our case we actually want them to have width: if they
// show up in text, we want to treat them as textual elements that can
// be editied. So we can get away with making all ascii single width
// here.
// Point 2: we're only examining the first codepoint here, which means
// we're ignoring graphemes formed with combining characters. However,
// if it starts with ascii, it's going to be a single-width grapeheme
// regardless, so, again, we can get away with that here.
// Point 3: we're only examining the first _byte_. But for utf8, when
// checking for ascii range values only, that works.
1
} else {
// We use max(1) here because all grapeheme clusters--even illformed
// ones--should have at least some width so they can be edited
// properly.
UnicodeWidthStr::width(g).max(1)
}
}
/// Finds the previous grapheme boundary before the given char position.
pub fn prev_grapheme_boundary(slice: &RopeSlice, char_idx: usize) -> usize {
// Bounds check
debug_assert!(char_idx <= slice.len_chars());
// We work with bytes for this, so convert.
let byte_idx = slice.char_to_byte(char_idx);
// Get the chunk with our byte index in it.
let (mut chunk, mut chunk_byte_idx, mut chunk_char_idx, _) = slice.chunk_at_byte(byte_idx);
// Set up the grapheme cursor.
let mut gc = GraphemeCursor::new(byte_idx, slice.len_bytes(), true);
// Find the previous grapheme cluster boundary.
loop {
match gc.prev_boundary(chunk, chunk_byte_idx) {
Ok(None) => return 0,
Ok(Some(n)) => {
let tmp = byte_to_char_idx(chunk, n - chunk_byte_idx);
return chunk_char_idx + tmp;
}
Err(GraphemeIncomplete::PrevChunk) => {
let (a, b, c, _) = slice.chunk_at_byte(chunk_byte_idx - 1);
chunk = a;
chunk_byte_idx = b;
chunk_char_idx = c;
}
Err(GraphemeIncomplete::PreContext(n)) => {
let ctx_chunk = slice.chunk_at_byte(n - 1).0;
gc.provide_context(ctx_chunk, n - ctx_chunk.len());
}
_ => unreachable!(),
}
}
}
/// Finds the next grapheme boundary after the given char position.
pub fn next_grapheme_boundary(slice: &RopeSlice, char_idx: usize) -> usize {
// Bounds check
debug_assert!(char_idx <= slice.len_chars());
// We work with bytes for this, so convert.
let byte_idx = slice.char_to_byte(char_idx);
// Get the chunk with our byte index in it.
let (mut chunk, mut chunk_byte_idx, mut chunk_char_idx, _) = slice.chunk_at_byte(byte_idx);
// Set up the grapheme cursor.
let mut gc = GraphemeCursor::new(byte_idx, slice.len_bytes(), true);
// Find the next grapheme cluster boundary.
loop {
match gc.next_boundary(chunk, chunk_byte_idx) {
Ok(None) => return slice.len_chars(),
Ok(Some(n)) => {
let tmp = byte_to_char_idx(chunk, n - chunk_byte_idx);
return chunk_char_idx + tmp;
}
Err(GraphemeIncomplete::NextChunk) => {
chunk_byte_idx += chunk.len();
let (a, _, c, _) = slice.chunk_at_byte(chunk_byte_idx);
chunk = a;
chunk_char_idx = c;
}
Err(GraphemeIncomplete::PreContext(n)) => {
let ctx_chunk = slice.chunk_at_byte(n - 1).0;
gc.provide_context(ctx_chunk, n - ctx_chunk.len());
}
_ => unreachable!(),
}
}
}
/// Returns whether the given char position is a grapheme boundary.
pub fn is_grapheme_boundary(slice: &RopeSlice, char_idx: usize) -> bool {
// Bounds check
debug_assert!(char_idx <= slice.len_chars());
// We work with bytes for this, so convert.
let byte_idx = slice.char_to_byte(char_idx);
// Get the chunk with our byte index in it.
let (chunk, chunk_byte_idx, _, _) = slice.chunk_at_byte(byte_idx);
// Set up the grapheme cursor.
let mut gc = GraphemeCursor::new(byte_idx, slice.len_bytes(), true);
// Determine if the given position is a grapheme cluster boundary.
loop {
match gc.is_boundary(chunk, chunk_byte_idx) {
Ok(n) => return n,
Err(GraphemeIncomplete::PreContext(n)) => {
let (ctx_chunk, ctx_byte_start, _, _) = slice.chunk_at_byte(n - 1);
gc.provide_context(ctx_chunk, ctx_byte_start);
}
_ => unreachable!(),
}
}
}
/// An iterator over the graphemes of a RopeSlice.
#[derive(Clone)]
pub struct RopeGraphemes<'a> {
text: RopeSlice<'a>,
chunks: Chunks<'a>,
cur_chunk: &'a str,
cur_chunk_start: usize,
cursor: GraphemeCursor,
}
impl<'a> RopeGraphemes<'a> {
pub fn new<'b>(slice: &RopeSlice<'b>) -> RopeGraphemes<'b> {
let mut chunks = slice.chunks();
let first_chunk = chunks.next().unwrap_or("");
RopeGraphemes {
text: *slice,
chunks: chunks,
cur_chunk: first_chunk,
cur_chunk_start: 0,
cursor: GraphemeCursor::new(0, slice.len_bytes(), true),
}
}
}
impl<'a> Iterator for RopeGraphemes<'a> {
type Item = RopeSlice<'a>;
fn next(&mut self) -> Option<RopeSlice<'a>> {
let a = self.cursor.cur_cursor();
let b;
loop {
match self
.cursor
.next_boundary(self.cur_chunk, self.cur_chunk_start)
{
Ok(None) => {
return None;
}
Ok(Some(n)) => {
b = n;
break;
}
Err(GraphemeIncomplete::NextChunk) => {
self.cur_chunk_start += self.cur_chunk.len();
self.cur_chunk = self.chunks.next().unwrap_or("");
}
_ => unreachable!(),
}
}
if a < self.cur_chunk_start {
let a_char = self.text.byte_to_char(a);
let b_char = self.text.byte_to_char(b);
Some(self.text.slice(a_char..b_char))
} else {
let a2 = a - self.cur_chunk_start;
let b2 = b - self.cur_chunk_start;
Some((&self.cur_chunk[a2..b2]).into())
}
}
}
//=============================================================
#[derive(Copy, Clone)] #[derive(Copy, Clone)]
pub struct Timer { pub struct Timer {
last_instant: Instant, last_instant: Instant,

View File

@ -61,7 +61,7 @@ impl Buffer {
*mark = mark.edit((start, end), post_len); *mark = mark.edit((start, end), post_len);
} }
mark_set.merge_touching(); mark_set.make_consistent();
} }
// Do removal if needed. // Do removal if needed.
@ -92,7 +92,7 @@ impl Buffer {
*mark = mark.edit((start, end), post_len); *mark = mark.edit((start, end), post_len);
} }
mark_set.merge_touching(); mark_set.make_consistent();
} }
// Do removal if needed. // Do removal if needed.
@ -128,7 +128,7 @@ impl Buffer {
*mark = mark.edit((start, end), post_len); *mark = mark.edit((start, end), post_len);
} }
mark_set.merge_touching(); mark_set.make_consistent();
} }
// Do removal if needed. // Do removal if needed.

View File

@ -15,6 +15,7 @@ impl History {
pub fn push_edit(&mut self, edit: Edit) { pub fn push_edit(&mut self, edit: Edit) {
self.edits.truncate(self.position); self.edits.truncate(self.position);
self.edits.push(edit); self.edits.push(edit);
self.position += 1;
} }
pub fn undo(&mut self) -> Option<&Edit> { pub fn undo(&mut self) -> Option<&Edit> {

View File

@ -111,7 +111,7 @@ impl Mark {
/// and code that modifies a MarkSet should ensure that the invariants remain /// and code that modifies a MarkSet should ensure that the invariants remain
/// true. /// true.
/// ///
/// The `merge_touching` method will ensure that all expected invariants hold, /// The `make_consistent` method will ensure that all expected invariants hold,
/// modifying the set to meet the invariants if needed. /// modifying the set to meet the invariants if needed.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct MarkSet { pub struct MarkSet {
@ -133,6 +133,23 @@ impl MarkSet {
self.marks.clear(); self.marks.clear();
} }
pub fn truncate(&mut self, len: usize) {
self.marks.truncate(len);
self.main_mark_idx = self.main_mark_idx.min(self.marks.len().saturating_sub(1));
}
/// Returns the main mark, if it exists.
pub fn main(&self) -> Option<Mark> {
self.marks.get(self.main_mark_idx).map(|m| *m)
}
/// Removes all marks except the main one.
pub fn reduce_to_main(&mut self) {
self.marks.swap(0, self.main_mark_idx);
self.main_mark_idx = 0;
self.marks.truncate(1);
}
/// Adds a new mark to the set, inserting it into its sorted position, and /// Adds a new mark to the set, inserting it into its sorted position, and
/// returns the index where it was inserted. /// returns the index where it was inserted.
/// ///
@ -140,7 +157,7 @@ impl MarkSet {
/// range. /// range.
/// ///
/// This does *not* preserve disjointedness. You should call /// This does *not* preserve disjointedness. You should call
/// `merge_touching` after you have added all the marks you want. /// `make_consistent` after you have added all the marks you want.
/// ///
/// Runs in O(N + log N) time worst-case, but when the new mark is /// Runs in O(N + log N) time worst-case, but when the new mark is
/// inserted at the end of the set it is amortized O(1). /// inserted at the end of the set it is amortized O(1).
@ -185,7 +202,7 @@ impl MarkSet {
/// into one. /// into one.
/// ///
/// Runs in O(N) time. /// Runs in O(N) time.
pub fn merge_touching(&mut self) { pub fn make_consistent(&mut self) {
let mut i1 = 0; let mut i1 = 0;
let mut i2 = 1; let mut i2 = 1;