Switch to new backend buffer for text, cursor, and undo management.
This commit is contained in:
parent
6077be2dfd
commit
c4fa72405f
1488
src/buffer/mod.rs
1488
src/buffer/mod.rs
File diff suppressed because it is too large
Load Diff
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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();
|
||||||
|
|
|
@ -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
212
src/graphemes.rs
Normal 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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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))
|
||||||
};
|
};
|
||||||
|
|
|
@ -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;
|
||||||
|
|
194
src/utils.rs
194
src/utils.rs
|
@ -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,
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user