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)]
|
||||
|
||||
mod cursor;
|
||||
|
||||
use std::{
|
||||
cmp::{max, min},
|
||||
collections::HashMap,
|
||||
fs::File,
|
||||
io::{self, BufReader, BufWriter, Write},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
buffer::Buffer,
|
||||
formatter::LineFormatter,
|
||||
string_utils::{char_count, rope_slice_to_line_ending, LineEnding},
|
||||
utils::{digit_count, RopeGraphemes},
|
||||
};
|
||||
use ropey::Rope;
|
||||
|
||||
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 buffer: Buffer,
|
||||
|
@ -24,7 +28,6 @@ pub struct Editor {
|
|||
pub line_ending_type: LineEnding,
|
||||
pub soft_tabs: bool,
|
||||
pub soft_tab_width: u8,
|
||||
pub dirty: bool,
|
||||
|
||||
// The dimensions of the total editor in screen space, including the
|
||||
// header, gutter, etc.
|
||||
|
@ -32,76 +35,90 @@ pub struct Editor {
|
|||
|
||||
// The dimensions and position of just the text view portion of the editor
|
||||
pub view_dim: (usize, usize), // (height, width)
|
||||
pub view_pos: (usize, usize), // (char index, visual horizontal offset)
|
||||
|
||||
// The editing cursor position
|
||||
pub cursors: CursorSet,
|
||||
// Indices into the mark sets of the buffer.
|
||||
pub v_msi: usize, // View position MarkSet index.
|
||||
pub c_msi: usize, // Cursors MarkSet index.
|
||||
}
|
||||
|
||||
impl Editor {
|
||||
/// Create a new blank 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 {
|
||||
buffer: Buffer::new(),
|
||||
buffer: buffer,
|
||||
formatter: formatter,
|
||||
file_path: PathBuf::new(),
|
||||
line_ending_type: LineEnding::LF,
|
||||
soft_tabs: false,
|
||||
soft_tab_width: 4,
|
||||
dirty: false,
|
||||
editor_dim: (0, 0),
|
||||
view_dim: (0, 0),
|
||||
view_pos: (0, 0),
|
||||
cursors: CursorSet::new(),
|
||||
v_msi: v_msi,
|
||||
c_msi: c_msi,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_from_file(formatter: LineFormatter, path: &Path) -> Editor {
|
||||
let buf = match Buffer::new_from_file(path) {
|
||||
Ok(b) => b,
|
||||
// TODO: handle un-openable file better
|
||||
_ => panic!("Could not open file!"),
|
||||
pub fn new_from_file(formatter: LineFormatter, path: &Path) -> io::Result<Editor> {
|
||||
let (buffer, v_msi, c_msi) = {
|
||||
let mut buffer = Buffer::new(Rope::from_reader(BufReader::new(File::open(path)?))?);
|
||||
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)
|
||||
};
|
||||
|
||||
let mut ed = Editor {
|
||||
buffer: buf,
|
||||
buffer: buffer,
|
||||
formatter: formatter,
|
||||
file_path: path.to_path_buf(),
|
||||
line_ending_type: LineEnding::LF,
|
||||
soft_tabs: false,
|
||||
soft_tab_width: 4,
|
||||
dirty: false,
|
||||
editor_dim: (0, 0),
|
||||
view_dim: (0, 0),
|
||||
view_pos: (0, 0),
|
||||
cursors: CursorSet::new(),
|
||||
v_msi: v_msi,
|
||||
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_indentation_style();
|
||||
|
||||
return ed;
|
||||
Ok(ed)
|
||||
}
|
||||
|
||||
pub fn save_if_dirty(&mut self) {
|
||||
if self.dirty && self.file_path != PathBuf::new() {
|
||||
let _ = self.buffer.save_to_file(&self.file_path);
|
||||
self.dirty = false;
|
||||
pub fn save_if_dirty(&mut self) -> io::Result<()> {
|
||||
if self.buffer.is_dirty && self.file_path != PathBuf::new() {
|
||||
let mut f = BufWriter::new(File::create(&self.file_path)?);
|
||||
|
||||
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) {
|
||||
let mut line_ending_histogram: [usize; 8] = [0, 0, 0, 0, 0, 0, 0, 0];
|
||||
|
||||
// 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
|
||||
let ending = if 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)
|
||||
|
||||
// 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();
|
||||
match c_iter.next() {
|
||||
Some('\t') => {
|
||||
|
@ -254,7 +271,7 @@ impl Editor {
|
|||
|
||||
/// Updates the view dimensions.
|
||||
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);
|
||||
|
||||
// Minus 1 vertically for the header, minus two more than the digits in
|
||||
|
@ -267,332 +284,258 @@ impl Editor {
|
|||
|
||||
pub fn undo(&mut self) {
|
||||
// TODO: handle multiple cursors properly
|
||||
if let Some(pos) = self.buffer.undo() {
|
||||
self.cursors.truncate(1);
|
||||
self.cursors[0].range.0 = pos;
|
||||
self.cursors[0].range.1 = pos;
|
||||
self.cursors[0].update_vis_start(&(self.buffer), &(self.formatter));
|
||||
if let Some((_start, end)) = self.buffer.undo() {
|
||||
self.buffer.mark_sets[self.c_msi].reduce_to_main();
|
||||
self.buffer.mark_sets[self.c_msi][0].head = end;
|
||||
self.buffer.mark_sets[self.c_msi][0].tail = end;
|
||||
self.buffer.mark_sets[self.c_msi][0].hh_pos = None;
|
||||
|
||||
self.move_view_to_cursor();
|
||||
|
||||
self.dirty = true;
|
||||
|
||||
self.cursors.make_consistent();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn redo(&mut self) {
|
||||
// TODO: handle multiple cursors properly
|
||||
if let Some(pos) = self.buffer.redo() {
|
||||
self.cursors.truncate(1);
|
||||
self.cursors[0].range.0 = pos;
|
||||
self.cursors[0].range.1 = pos;
|
||||
self.cursors[0].update_vis_start(&(self.buffer), &(self.formatter));
|
||||
if let Some((_start, end)) = self.buffer.redo() {
|
||||
self.buffer.mark_sets[self.c_msi].reduce_to_main();
|
||||
self.buffer.mark_sets[self.c_msi][0].head = end;
|
||||
self.buffer.mark_sets[self.c_msi][0].tail = end;
|
||||
self.buffer.mark_sets[self.c_msi][0].hh_pos = None;
|
||||
|
||||
self.move_view_to_cursor();
|
||||
|
||||
self.dirty = true;
|
||||
|
||||
self.cursors.make_consistent();
|
||||
}
|
||||
}
|
||||
|
||||
/// Moves the editor's view the minimum amount to show the cursor
|
||||
pub fn move_view_to_cursor(&mut self) {
|
||||
// Find the first and last char index visible within the editor.
|
||||
let c_first = self
|
||||
.formatter
|
||||
.set_horizontal(&self.buffer, self.view_pos.0, 0);
|
||||
let mut c_last =
|
||||
self.formatter
|
||||
.offset_vertical(&self.buffer, c_first, self.view_dim.0 as isize - 1);
|
||||
let c_first = self.formatter.set_horizontal(
|
||||
&self.buffer.text,
|
||||
self.buffer.mark_sets[self.v_msi][0].head,
|
||||
0,
|
||||
);
|
||||
let mut c_last = self.formatter.offset_vertical(
|
||||
&self.buffer.text,
|
||||
c_first,
|
||||
self.view_dim.0 as isize - 1,
|
||||
);
|
||||
c_last = self
|
||||
.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
|
||||
if self.cursors[0].range.0 < c_first {
|
||||
self.view_pos.0 = self.cursors[0].range.0;
|
||||
} else if self.cursors[0].range.0 > c_last {
|
||||
self.view_pos.0 = self.formatter.offset_vertical(
|
||||
&self.buffer,
|
||||
self.cursors[0].range.0,
|
||||
let cursor_head = self.buffer.mark_sets[self.c_msi].main().unwrap().head;
|
||||
if cursor_head < c_first {
|
||||
self.buffer.mark_sets[self.v_msi][0].head = cursor_head;
|
||||
} else if cursor_head > c_last {
|
||||
self.buffer.mark_sets[self.v_msi][0].head = self.formatter.offset_vertical(
|
||||
&self.buffer.text,
|
||||
cursor_head,
|
||||
-(self.view_dim.0 as isize),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
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;
|
||||
}
|
||||
self.buffer.edit((range.start, range.end), text);
|
||||
|
||||
// Adjust view
|
||||
self.move_view_to_cursor();
|
||||
}
|
||||
|
||||
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 {
|
||||
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() {
|
||||
// Update cursor with offset
|
||||
c.range.0 += offset;
|
||||
c.range.1 += offset;
|
||||
|
||||
// Figure out how many spaces to insert
|
||||
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();
|
||||
// Insert spaces
|
||||
let space_strs = [
|
||||
"", " ", " ", " ", " ", " ", " ", " ", " ",
|
||||
];
|
||||
self.buffer
|
||||
.edit((range.start, range.end), space_strs[space_count]);
|
||||
} else {
|
||||
self.insert_text_at_cursor("\t");
|
||||
self.buffer.edit((range.start, range.end), "\t");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn backspace_at_cursor(&mut self) {
|
||||
self.remove_text_behind_cursor(1);
|
||||
// Adjust view
|
||||
self.move_view_to_cursor();
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
for c in self.cursors.iter_mut() {
|
||||
// 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;
|
||||
// Do nothing if there's nothing to delete.
|
||||
if range.start == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
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
|
||||
self.move_view_to_cursor();
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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);
|
||||
|
||||
// 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;
|
||||
// Do nothing if there's nothing to delete.
|
||||
if range.end == self.buffer.text.len_chars() {
|
||||
return;
|
||||
}
|
||||
|
||||
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
|
||||
self.move_view_to_cursor();
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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));
|
||||
if range.start < range.end {
|
||||
self.buffer.edit((range.start, range.end), "");
|
||||
}
|
||||
|
||||
self.cursors.make_consistent();
|
||||
|
||||
// Adjust view
|
||||
self.move_view_to_cursor();
|
||||
}
|
||||
|
||||
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);
|
||||
self.cursors[0].update_vis_start(&(self.buffer), &(self.formatter));
|
||||
|
||||
// Adjust view
|
||||
// Adjust view.
|
||||
self.move_view_to_cursor();
|
||||
}
|
||||
|
||||
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.cursors[0].range = (end, end);
|
||||
self.cursors[0].update_vis_start(&(self.buffer), &(self.formatter));
|
||||
self.buffer.mark_sets[self.c_msi].clear();
|
||||
self.buffer.mark_sets[self.c_msi].add_mark(Mark::new(end, end));
|
||||
|
||||
// Adjust view
|
||||
// Adjust view.
|
||||
self.move_view_to_cursor();
|
||||
}
|
||||
|
||||
pub fn cursor_left(&mut self, n: usize) {
|
||||
for c in self.cursors.iter_mut() {
|
||||
c.range.0 = self.buffer.nth_prev_grapheme(c.range.0, n);
|
||||
c.range.1 = c.range.0;
|
||||
c.update_vis_start(&(self.buffer), &(self.formatter));
|
||||
for mark in self.buffer.mark_sets[self.c_msi].iter_mut() {
|
||||
mark.head = nth_prev_grapheme_boundary(&self.buffer.text.slice(..), mark.head, n);
|
||||
mark.tail = mark.head;
|
||||
mark.hh_pos = None;
|
||||
}
|
||||
self.buffer.mark_sets[self.c_msi].make_consistent();
|
||||
|
||||
// Adjust view
|
||||
self.move_view_to_cursor();
|
||||
}
|
||||
|
||||
pub fn cursor_right(&mut self, n: usize) {
|
||||
for c in self.cursors.iter_mut() {
|
||||
c.range.1 = self.buffer.nth_next_grapheme(c.range.1, n);
|
||||
c.range.0 = c.range.1;
|
||||
c.update_vis_start(&(self.buffer), &(self.formatter));
|
||||
for mark in self.buffer.mark_sets[self.c_msi].iter_mut() {
|
||||
mark.head = nth_next_grapheme_boundary(&self.buffer.text.slice(..), mark.head, n);
|
||||
mark.tail = mark.head;
|
||||
mark.hh_pos = None;
|
||||
}
|
||||
self.buffer.mark_sets[self.c_msi].make_consistent();
|
||||
|
||||
// Adjust view
|
||||
self.move_view_to_cursor();
|
||||
}
|
||||
|
||||
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 mut temp_index = self
|
||||
.formatter
|
||||
.offset_vertical(&self.buffer, c.range.0, vmove);
|
||||
temp_index = self
|
||||
.formatter
|
||||
.set_horizontal(&self.buffer, temp_index, c.vis_start);
|
||||
let mut temp_index =
|
||||
self.formatter
|
||||
.offset_vertical(&self.buffer.text, mark.head, vmove);
|
||||
temp_index =
|
||||
self.formatter
|
||||
.set_horizontal(&self.buffer.text, temp_index, mark.hh_pos.unwrap());
|
||||
|
||||
if !self.buffer.is_grapheme(temp_index) {
|
||||
temp_index = self.buffer.nth_prev_grapheme(temp_index, 1);
|
||||
if !is_grapheme_boundary(&self.buffer.text.slice(..), temp_index) {
|
||||
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.
|
||||
c.range.0 = 0;
|
||||
c.range.1 = 0;
|
||||
c.update_vis_start(&(self.buffer), &(self.formatter));
|
||||
mark.head = 0;
|
||||
mark.tail = 0;
|
||||
mark.hh_pos = None;
|
||||
} else {
|
||||
c.range.0 = temp_index;
|
||||
c.range.1 = temp_index;
|
||||
mark.head = temp_index;
|
||||
mark.tail = temp_index;
|
||||
}
|
||||
}
|
||||
self.buffer.mark_sets[self.c_msi].make_consistent();
|
||||
|
||||
// Adjust view
|
||||
self.move_view_to_cursor();
|
||||
}
|
||||
|
||||
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 mut temp_index = self
|
||||
.formatter
|
||||
.offset_vertical(&self.buffer, c.range.0, vmove);
|
||||
temp_index = self
|
||||
.formatter
|
||||
.set_horizontal(&self.buffer, temp_index, c.vis_start);
|
||||
let mut temp_index =
|
||||
self.formatter
|
||||
.offset_vertical(&self.buffer.text, mark.head, vmove);
|
||||
temp_index =
|
||||
self.formatter
|
||||
.set_horizontal(&self.buffer.text, temp_index, mark.hh_pos.unwrap());
|
||||
|
||||
if !self.buffer.is_grapheme(temp_index) {
|
||||
temp_index = self.buffer.nth_prev_grapheme(temp_index, 1);
|
||||
if !is_grapheme_boundary(&self.buffer.text.slice(..), temp_index) {
|
||||
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.
|
||||
c.range.0 = self.buffer.char_count();
|
||||
c.range.1 = self.buffer.char_count();
|
||||
c.update_vis_start(&(self.buffer), &(self.formatter));
|
||||
mark.head = self.buffer.text.len_chars();
|
||||
mark.tail = self.buffer.text.len_chars();
|
||||
mark.hh_pos = None;
|
||||
} else {
|
||||
c.range.0 = temp_index;
|
||||
c.range.1 = temp_index;
|
||||
mark.head = temp_index;
|
||||
mark.tail = temp_index;
|
||||
}
|
||||
}
|
||||
self.buffer.mark_sets[self.c_msi].make_consistent();
|
||||
|
||||
// Adjust view
|
||||
self.move_view_to_cursor();
|
||||
|
@ -600,9 +543,9 @@ impl Editor {
|
|||
|
||||
pub fn page_up(&mut self) {
|
||||
let move_amount = self.view_dim.0 - max(self.view_dim.0 / 8, 1);
|
||||
self.view_pos.0 = self.formatter.offset_vertical(
|
||||
&self.buffer,
|
||||
self.view_pos.0,
|
||||
self.buffer.mark_sets[self.v_msi][0].head = self.formatter.offset_vertical(
|
||||
&self.buffer.text,
|
||||
self.buffer.mark_sets[self.v_msi][0].head,
|
||||
-1 * move_amount as isize,
|
||||
);
|
||||
|
||||
|
@ -614,9 +557,11 @@ impl Editor {
|
|||
|
||||
pub fn page_down(&mut self) {
|
||||
let move_amount = self.view_dim.0 - max(self.view_dim.0 / 8, 1);
|
||||
self.view_pos.0 =
|
||||
self.formatter
|
||||
.offset_vertical(&self.buffer, self.view_pos.0, move_amount as isize);
|
||||
self.buffer.mark_sets[self.v_msi][0].head = self.formatter.offset_vertical(
|
||||
&self.buffer.text,
|
||||
self.buffer.mark_sets[self.v_msi][0].head,
|
||||
move_amount as isize,
|
||||
);
|
||||
|
||||
self.cursor_down(move_amount);
|
||||
|
||||
|
@ -625,12 +570,23 @@ impl Editor {
|
|||
}
|
||||
|
||||
pub fn jump_to_line(&mut self, n: usize) {
|
||||
let pos = self.buffer.line_col_to_index((n, 0));
|
||||
self.cursors.truncate(1);
|
||||
self.cursors[0].range.0 =
|
||||
self.formatter
|
||||
.set_horizontal(&self.buffer, pos, self.cursors[0].vis_start);
|
||||
self.cursors[0].range.1 = self.cursors[0].range.0;
|
||||
self.buffer.mark_sets[self.c_msi].reduce_to_main();
|
||||
if self.buffer.mark_sets[self.c_msi][0].hh_pos == None {
|
||||
self.buffer.mark_sets[self.c_msi][0].hh_pos = Some(
|
||||
self.formatter
|
||||
.get_horizontal(&self.buffer.text, self.buffer.mark_sets[self.c_msi][0].head),
|
||||
);
|
||||
}
|
||||
|
||||
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
|
||||
self.move_view_to_cursor();
|
||||
|
|
|
@ -3,10 +3,9 @@ use std::borrow::Cow;
|
|||
use ropey::{Rope, RopeSlice};
|
||||
|
||||
use crate::{
|
||||
buffer::Buffer,
|
||||
graphemes::{grapheme_width, is_grapheme_boundary, prev_grapheme_boundary, RopeGraphemes},
|
||||
string_utils::char_count,
|
||||
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.
|
||||
|
@ -42,10 +41,14 @@ impl LineFormatter {
|
|||
/// Returns an iterator over the blocks of the buffer, starting at the
|
||||
/// block containing the given char. Also returns the offset of that char
|
||||
/// 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.
|
||||
let (line_i, col_i) = buf.index_to_line_col(char_idx);
|
||||
let line = buf.get_line(line_i);
|
||||
let (line_i, col_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
|
||||
let (block_index, block_range) = block_index_and_range(&line, col_i);
|
||||
|
@ -54,7 +57,7 @@ impl LineFormatter {
|
|||
(
|
||||
Blocks {
|
||||
formatter: self,
|
||||
buf: &buf.text,
|
||||
buf: buf,
|
||||
line_idx: line_i,
|
||||
line_block_count: block_count(&line),
|
||||
block_idx: block_index,
|
||||
|
@ -63,8 +66,8 @@ impl LineFormatter {
|
|||
)
|
||||
}
|
||||
|
||||
/// Converts from char index to the horizontal 2d char index.
|
||||
pub fn get_horizontal(&self, buf: &Buffer, char_idx: usize) -> usize {
|
||||
/// Converts from char index to its formatted horizontal 2d position.
|
||||
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);
|
||||
|
||||
// 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,
|
||||
/// but offset to have the desired horizontal position (or as close as is
|
||||
/// 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 mut hpos_char_idx = None;
|
||||
|
@ -134,7 +137,7 @@ impl LineFormatter {
|
|||
// If we reached the end of the text, return the last char index.
|
||||
let end_i = char_idx - char_offset + 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;
|
||||
} else {
|
||||
return end_last_i;
|
||||
|
@ -143,7 +146,7 @@ impl LineFormatter {
|
|||
|
||||
/// Takes a char index and a visual vertical offset, and returns the char
|
||||
/// index after that visual offset is applied.
|
||||
pub fn offset_vertical(&self, buf: &Buffer, char_idx: usize, v_offset: isize) -> usize {
|
||||
pub fn offset_vertical(&self, buf: &Rope, char_idx: usize, v_offset: isize) -> usize {
|
||||
let mut char_idx = char_idx;
|
||||
let mut v_offset = v_offset;
|
||||
while v_offset != 0 {
|
||||
|
@ -171,9 +174,9 @@ impl LineFormatter {
|
|||
}
|
||||
} else if offset_char_v_pos >= block_v_dim as isize {
|
||||
// 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;
|
||||
if char_idx == buf.text.len_chars() {
|
||||
if char_idx == buf.len_chars() {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
|
@ -230,13 +233,13 @@ impl LineFormatter {
|
|||
/// char's offset within that iter.
|
||||
fn block_vis_iter_and_char_offset<'b>(
|
||||
&self,
|
||||
buf: &'b Buffer,
|
||||
buf: &'b Rope,
|
||||
char_idx: usize,
|
||||
) -> (RopeSlice<'b>, BlockVisIter<'b>, usize) {
|
||||
let line_i = buf.text.char_to_line(char_idx);
|
||||
let line_start = buf.text.line_to_char(line_i);
|
||||
let line_end = buf.text.line_to_char(line_i + 1);
|
||||
let line = buf.text.slice(line_start..line_end);
|
||||
let line_i = buf.char_to_line(char_idx);
|
||||
let line_start = buf.line_to_char(line_i);
|
||||
let line_end = buf.line_to_char(line_i + 1);
|
||||
let line = buf.slice(line_start..line_end);
|
||||
|
||||
// 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);
|
||||
|
|
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 term_ui::TermUI;
|
||||
|
||||
mod buffer;
|
||||
mod editor;
|
||||
mod formatter;
|
||||
mod graphemes;
|
||||
mod string_utils;
|
||||
mod term_ui;
|
||||
mod utils;
|
||||
|
@ -30,6 +30,7 @@ fn main() {
|
|||
// Load file, if specified
|
||||
let editor = if let Some(filepath) = args.value_of("file") {
|
||||
Editor::new_from_file(LineFormatter::new(4), &Path::new(&filepath[..]))
|
||||
.expect(&format!("Couldn't open file '{}'.", filepath))
|
||||
} else {
|
||||
Editor::new(LineFormatter::new(4))
|
||||
};
|
||||
|
|
|
@ -243,7 +243,7 @@ impl TermUI {
|
|||
code: KeyCode::Char('s'),
|
||||
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 {
|
||||
|
@ -342,7 +342,7 @@ impl TermUI {
|
|||
code: KeyCode::Backspace,
|
||||
modifiers: EMPTY_MOD,
|
||||
} => {
|
||||
self.editor.backspace_at_cursor();
|
||||
self.editor.remove_text_behind_cursor(1);
|
||||
}
|
||||
|
||||
KeyEvent {
|
||||
|
@ -464,16 +464,17 @@ impl TermUI {
|
|||
|
||||
// Filename and dirty marker
|
||||
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);
|
||||
self.screen.draw(c1.1 + 1, c1.0, &name[..], STYLE_INFO);
|
||||
|
||||
// Percentage position in document
|
||||
// TODO: use view instead of cursor for calculation if there is more
|
||||
// than one cursor.
|
||||
let percentage: usize = if editor.buffer.char_count() > 0 {
|
||||
(((editor.cursors[0].range.0 as f32) / (editor.buffer.char_count() as f32)) * 100.0)
|
||||
as usize
|
||||
let percentage: usize = if editor.buffer.text.len_chars() > 0 {
|
||||
(((editor.buffer.mark_sets[editor.c_msi].main().unwrap().head as f32)
|
||||
/ (editor.buffer.text.len_chars() as f32))
|
||||
* 100.0) as usize
|
||||
} else {
|
||||
100
|
||||
};
|
||||
|
@ -510,12 +511,15 @@ impl TermUI {
|
|||
}
|
||||
|
||||
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
|
||||
let gutter_width = editor.editor_dim.1 - editor.view_dim.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);
|
||||
|
||||
|
@ -537,7 +541,7 @@ impl TermUI {
|
|||
// Loop through the blocks, printing them to the screen.
|
||||
let mut is_first_loop = true;
|
||||
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 {
|
||||
if is_line_start && !is_first_loop {
|
||||
line_num += 1;
|
||||
|
@ -578,7 +582,7 @@ impl TermUI {
|
|||
screen_line += 1;
|
||||
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;
|
||||
|
||||
// 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) {
|
||||
// Check if the character is within a cursor
|
||||
let mut at_cursor = false;
|
||||
for c in editor.cursors.iter() {
|
||||
if char_index >= c.range.0 && char_index <= c.range.1 {
|
||||
for c in cursors.iter() {
|
||||
if char_index >= c.range().start && char_index <= c.range().end {
|
||||
at_cursor = true;
|
||||
self.screen.set_cursor(px as usize, py as usize);
|
||||
}
|
||||
|
@ -636,18 +640,19 @@ impl TermUI {
|
|||
|
||||
// Check if the character is within a cursor
|
||||
let mut at_cursor = false;
|
||||
for c in editor.cursors.iter() {
|
||||
if char_index >= c.range.0 && char_index <= c.range.1 {
|
||||
for c in cursors.iter() {
|
||||
if char_index >= c.range().start && char_index <= c.range().end {
|
||||
at_cursor = true;
|
||||
}
|
||||
}
|
||||
|
||||
if at_cursor {
|
||||
// Calculate the cell coordinates at which to draw the cursor
|
||||
let pos_x = editor
|
||||
.formatter
|
||||
.get_horizontal(&self.editor.buffer, self.editor.buffer.char_count());
|
||||
let mut px = pos_x as isize + screen_col - editor.view_pos.1 as isize;
|
||||
let pos_x = editor.formatter.get_horizontal(
|
||||
&self.editor.buffer.text,
|
||||
self.editor.buffer.text.len_chars(),
|
||||
);
|
||||
let mut px = pos_x as isize + screen_col;
|
||||
let mut py = screen_line - 1;
|
||||
if px > c2.1 as isize {
|
||||
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 unicode_segmentation::{GraphemeCursor, GraphemeIncomplete};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
pub fn digit_count(mut n: u32, b: u32) -> u32 {
|
||||
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)]
|
||||
pub struct Timer {
|
||||
last_instant: Instant,
|
||||
|
|
|
@ -61,7 +61,7 @@ impl Buffer {
|
|||
*mark = mark.edit((start, end), post_len);
|
||||
}
|
||||
|
||||
mark_set.merge_touching();
|
||||
mark_set.make_consistent();
|
||||
}
|
||||
|
||||
// Do removal if needed.
|
||||
|
@ -92,7 +92,7 @@ impl Buffer {
|
|||
*mark = mark.edit((start, end), post_len);
|
||||
}
|
||||
|
||||
mark_set.merge_touching();
|
||||
mark_set.make_consistent();
|
||||
}
|
||||
|
||||
// Do removal if needed.
|
||||
|
@ -128,7 +128,7 @@ impl Buffer {
|
|||
*mark = mark.edit((start, end), post_len);
|
||||
}
|
||||
|
||||
mark_set.merge_touching();
|
||||
mark_set.make_consistent();
|
||||
}
|
||||
|
||||
// Do removal if needed.
|
||||
|
|
|
@ -15,6 +15,7 @@ impl History {
|
|||
pub fn push_edit(&mut self, edit: Edit) {
|
||||
self.edits.truncate(self.position);
|
||||
self.edits.push(edit);
|
||||
self.position += 1;
|
||||
}
|
||||
|
||||
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
|
||||
/// 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.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MarkSet {
|
||||
|
@ -133,6 +133,23 @@ impl MarkSet {
|
|||
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
|
||||
/// returns the index where it was inserted.
|
||||
///
|
||||
|
@ -140,7 +157,7 @@ impl MarkSet {
|
|||
/// range.
|
||||
///
|
||||
/// 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
|
||||
/// inserted at the end of the set it is amortized O(1).
|
||||
|
@ -185,7 +202,7 @@ impl MarkSet {
|
|||
/// into one.
|
||||
///
|
||||
/// Runs in O(N) time.
|
||||
pub fn merge_touching(&mut self) {
|
||||
pub fn make_consistent(&mut self) {
|
||||
let mut i1 = 0;
|
||||
let mut i2 = 1;
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user