mirror of https://github.com/andrewkdinh/via.git
489 lines
18 KiB
Rust
489 lines
18 KiB
Rust
// use unicode_segmentation::UnicodeSegmentation;
|
|
|
|
/// The main structure for storing text
|
|
pub(crate) struct PieceTable {
|
|
/// The main table, contains `TableEntry`'s
|
|
table: Vec<TableEntry>,
|
|
/// Original buffer
|
|
original_buffer: String,
|
|
/// Add buffer
|
|
add_buffer: String,
|
|
/// All active text. Only to be used when `text_up_to_date == true`
|
|
text: String,
|
|
/// Length of text if it was up to date (`text` may not not be up to date)
|
|
text_len: usize,
|
|
/// Whether `text` is up to date
|
|
text_up_to_date: bool,
|
|
/// List of actions, which are lists of table entries' indices
|
|
actions: Vec<Vec<usize>>,
|
|
/// Where in `self.actions` we are currently at
|
|
///
|
|
/// **NOTE**: A value of 0 means no actions have been taken
|
|
actions_index: usize,
|
|
}
|
|
|
|
impl PieceTable {
|
|
/// Initializes a piece table
|
|
pub(crate) fn new() -> PieceTable {
|
|
PieceTable {
|
|
table: Vec::new(),
|
|
original_buffer: String::new(),
|
|
add_buffer: String::new(),
|
|
text: String::new(),
|
|
text_len: 0,
|
|
text_up_to_date: true,
|
|
actions: Vec::new(),
|
|
actions_index: 0,
|
|
}
|
|
}
|
|
|
|
/// Returns if any actions have been taken
|
|
pub(crate) fn actions_taken(&self) -> bool {
|
|
!self.actions.is_empty()
|
|
}
|
|
|
|
/// Append text to the original buffer and add a table entry
|
|
pub(crate) fn update_original_buffer(&mut self, text: String) {
|
|
let org_buffer_len = self.original_buffer.len();
|
|
self.original_buffer.push_str(&text);
|
|
self.table.push(TableEntry::new(false, org_buffer_len, org_buffer_len + text.len()));
|
|
self.text_len += text.len();
|
|
self.text_up_to_date = false;
|
|
}
|
|
|
|
/// Add text at a certain index
|
|
pub(crate) fn add_text(&mut self, text: String, index: usize) {
|
|
let text_len = text.len();
|
|
let add_buffer_len = self.add_buffer.len();
|
|
if index > self.text_len {
|
|
panic!("index ({}) is a greater value than text len ({})", index, self.text_len);
|
|
}
|
|
let mut curr_pos = 0;
|
|
let mut action: Vec<usize> = Vec::new();
|
|
let mut add_table: Vec<TableEntry> = Vec::with_capacity(3);
|
|
let mut add_table_indices: Vec<usize> = Vec::with_capacity(3);
|
|
if index == 0 {
|
|
self.table.insert(0, TableEntry::new(true, add_buffer_len, add_buffer_len + text.len()));
|
|
action.push(0);
|
|
} else if index == self.text_len {
|
|
self.table.push(TableEntry::new(true, add_buffer_len, add_buffer_len + text.len()));
|
|
action.push(self.table.len());
|
|
} else {
|
|
for (i, table_entry) in self.table.iter_mut().enumerate() {
|
|
if table_entry.active {
|
|
let len = table_entry.end_index - table_entry.start_index;
|
|
if curr_pos == index {
|
|
add_table.push(TableEntry::new(true, add_buffer_len, add_buffer_len + text.len()));
|
|
add_table_indices.push(i);
|
|
break
|
|
} else if curr_pos + len > index {
|
|
// Split into 2 parts and disable original [ab] + [c] -> [a][c][b]
|
|
let split_point = index - curr_pos;
|
|
|
|
table_entry.active = false;
|
|
action.push(i);
|
|
|
|
add_table.push(TableEntry::new(table_entry.is_add_buffer, table_entry.start_index, table_entry.start_index + split_point));
|
|
action.push(i + 1);
|
|
add_table.push(TableEntry::new(true, add_buffer_len, add_buffer_len + text.len()));
|
|
action.push(i + 2);
|
|
add_table.push(TableEntry::new(table_entry.is_add_buffer, table_entry.start_index + split_point, table_entry.end_index));
|
|
action.push(i + 3);
|
|
|
|
add_table_indices.push(i);
|
|
add_table_indices.push(i + 1);
|
|
add_table_indices.push(i + 2);
|
|
break
|
|
}
|
|
curr_pos += len;
|
|
}
|
|
}
|
|
|
|
for (i, table_entry) in add_table_indices.iter().zip(add_table) {
|
|
self.table.insert(*i, table_entry);
|
|
}
|
|
}
|
|
|
|
self.add_buffer.push_str(&text);
|
|
self.add_action(action);
|
|
self.text_len += text_len;
|
|
self.text_up_to_date = false;
|
|
}
|
|
|
|
/// Delete text from `start` to `end`
|
|
pub(crate) fn delete_text(&mut self, start: usize, end: usize) {
|
|
if start >= end || end == 0 || end > self.text_len {
|
|
panic!("Can't delete from start ({}) to end ({}) of text size {}", start, end, self.text_len);
|
|
}
|
|
let mut curr_pos = 0;
|
|
let mut action: Vec<usize> = Vec::new();
|
|
let mut add_table: Vec<TableEntry> = Vec::with_capacity(4);
|
|
let mut add_table_indices: Vec<usize> = Vec::with_capacity(4);
|
|
|
|
for (i, table_entry) in self.table.iter_mut().enumerate() {
|
|
if curr_pos == end {
|
|
break
|
|
}
|
|
if table_entry.active {
|
|
let len = table_entry.end_index - table_entry.start_index;
|
|
if start <= curr_pos && end >= curr_pos + len {
|
|
// At table entry to continue/end at
|
|
// OR start & end is this exact table entry
|
|
table_entry.active = false;
|
|
action.push(i + add_table.len());
|
|
} else if start > curr_pos && start < curr_pos + len && end >= curr_pos + len {
|
|
// At table entry to start at (split table entry in two)
|
|
// Only occurs once, and will be the first
|
|
let split_point = start - curr_pos;
|
|
|
|
table_entry.active = false;
|
|
action.push(i);
|
|
|
|
add_table.push(TableEntry::new(table_entry.is_add_buffer, table_entry.start_index, table_entry.start_index + split_point));
|
|
add_table_indices.push(i + 1);
|
|
action.push(i + 1);
|
|
} else if start <= curr_pos && end > curr_pos && end < curr_pos + len {
|
|
// At table entry to end at (split table entry in two)
|
|
let split_point = end - curr_pos;
|
|
|
|
table_entry.active = false;
|
|
action.push(i + add_table.len());
|
|
|
|
add_table.push(TableEntry::new(table_entry.is_add_buffer, table_entry.start_index + split_point, table_entry.end_index));
|
|
add_table_indices.push(i + add_table.len());
|
|
action.push(i + add_table.len());
|
|
break
|
|
} else if start > curr_pos && end < curr_pos + len {
|
|
// At table entry to split into 3 [abc] -> [a](b)[c]
|
|
// Only occurs once, and will be the first/last
|
|
let first_split = start - curr_pos;
|
|
let second_split = end - curr_pos;
|
|
|
|
table_entry.active = false;
|
|
action.push(i);
|
|
|
|
add_table.push(TableEntry::new(table_entry.is_add_buffer, table_entry.start_index, table_entry.start_index + first_split));
|
|
add_table_indices.push(i + 1);
|
|
action.push(i + 1);
|
|
|
|
add_table.push(TableEntry::new(table_entry.is_add_buffer, table_entry.start_index + second_split, table_entry.end_index));
|
|
add_table_indices.push(i + 2);
|
|
action.push(i + 2);
|
|
break
|
|
}
|
|
curr_pos += len;
|
|
}
|
|
}
|
|
|
|
for (i, table_entry) in add_table_indices.iter().zip(add_table) {
|
|
self.table.insert(*i, table_entry);
|
|
}
|
|
|
|
self.add_action(action);
|
|
self.text_len -= end - start;
|
|
self.text_up_to_date = false;
|
|
}
|
|
|
|
/// Add a table entry to actions
|
|
fn add_action(&mut self, action: Vec<usize>) {
|
|
// Remove actions after current index
|
|
// TODO: Remove all unecessary TableEntry's
|
|
self.actions = self.actions[..self.actions_index].to_vec();
|
|
self.actions.push(action);
|
|
self.actions_index = self.actions.len();
|
|
}
|
|
|
|
/// Undo an action. Errors if no actions to undo
|
|
fn undo(&mut self) {
|
|
if self.actions.is_empty() {
|
|
panic!("Unable to undo");
|
|
}
|
|
|
|
for index in self.actions.get(self.actions_index - 1).unwrap() {
|
|
let table_entry = self.table.get_mut(*index).unwrap();
|
|
table_entry.switch();
|
|
if table_entry.active {
|
|
self.text_len += table_entry.end_index - table_entry.start_index;
|
|
} else {
|
|
self.text_len -= table_entry.end_index - table_entry.start_index;
|
|
}
|
|
}
|
|
|
|
self.text_up_to_date = false;
|
|
self.actions_index -= 1;
|
|
}
|
|
|
|
/// Redo an action. Errors if no actions to redo
|
|
fn redo(&mut self) {
|
|
if self.actions.is_empty() || self.actions.len() <= self.actions_index {
|
|
panic!("Unable to redo");
|
|
}
|
|
for index in self.actions.get(self.actions_index).unwrap() {
|
|
let table_entry = self.table.get_mut(*index).unwrap();
|
|
table_entry.switch();
|
|
if table_entry.active {
|
|
self.text_len += table_entry.end_index - table_entry.start_index;
|
|
} else {
|
|
self.text_len -= table_entry.end_index - table_entry.start_index;
|
|
}
|
|
}
|
|
self.text_up_to_date = false;
|
|
self.actions_index += 1;
|
|
}
|
|
|
|
/// Returns the text represented by a table entry
|
|
fn table_entry_text(&self, table_entry: &TableEntry) -> &str {
|
|
let buffer = if table_entry.is_add_buffer {&self.add_buffer} else {&self.original_buffer};
|
|
buffer.get(table_entry.start_index..table_entry.end_index).unwrap()
|
|
}
|
|
|
|
/// Returns length of text
|
|
pub(crate) fn text_len(&self) -> usize {
|
|
self.text_len
|
|
}
|
|
|
|
/// Returns all visible text
|
|
///
|
|
/// If you want to get the length of the text, use `text_len(&self)` instead
|
|
pub(crate) fn text(&mut self) -> &str {
|
|
self.update_text();
|
|
&self.text
|
|
}
|
|
|
|
/// Updates all text for the piece table
|
|
fn update_text(&mut self) {
|
|
if self.text_up_to_date {
|
|
return;
|
|
}
|
|
|
|
let mut text = String::new();
|
|
for table_entry in &self.table {
|
|
if table_entry.active {
|
|
text.push_str(self.table_entry_text(table_entry));
|
|
}
|
|
}
|
|
|
|
self.text = text;
|
|
self.text_up_to_date = true;
|
|
}
|
|
|
|
/// Insert a table entry to a specific index
|
|
fn insert(&mut self, index: usize, table_entry: TableEntry) {
|
|
self.table.insert(index, table_entry);
|
|
self.text_up_to_date = false;
|
|
}
|
|
|
|
/// Add a table entry to the end of the table
|
|
fn push(&mut self, table_entry: TableEntry) {
|
|
self.table.push(table_entry);
|
|
self.text_up_to_date = false;
|
|
}
|
|
|
|
/// Return the index-th line
|
|
/// TODO: Remove this and do something better regarding lines
|
|
pub(crate) fn line(&mut self, index: usize) -> Result<&str, &str> {
|
|
for (i, line) in self.text().lines().enumerate() {
|
|
if i == index {
|
|
return Ok(line)
|
|
}
|
|
}
|
|
Err("Invalid line index")
|
|
}
|
|
}
|
|
|
|
/// An entry in PieceTable's table
|
|
struct TableEntry {
|
|
/// Whether this table entry points to the add buffer
|
|
is_add_buffer: bool,
|
|
/// Start index
|
|
start_index: usize,
|
|
/// End index
|
|
end_index: usize,
|
|
/// Whether this table is visible
|
|
active: bool,
|
|
}
|
|
|
|
impl TableEntry {
|
|
/// Initalize a table entry
|
|
pub(crate) fn new(is_add_buffer: bool, start_index: usize, end_index: usize) -> TableEntry {
|
|
TableEntry {
|
|
is_add_buffer: is_add_buffer,
|
|
start_index: start_index,
|
|
end_index: end_index,
|
|
active: true,
|
|
}
|
|
}
|
|
|
|
/// Change from active to deactivated and visa versa
|
|
fn switch(&mut self) {
|
|
self.active = !self.active;
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn add_text() {
|
|
let mut piece_table = PieceTable::new();
|
|
let mut want_str = "a";
|
|
piece_table.add_text("a".to_string(), 0);
|
|
assert_eq!(piece_table.text_len, want_str.len());
|
|
assert_eq!(piece_table.text(), want_str);
|
|
|
|
piece_table.add_text(" b".to_string(), 1);
|
|
want_str = "a b";
|
|
assert_eq!(piece_table.text_len, want_str.len());
|
|
assert_eq!(piece_table.text(), want_str);
|
|
|
|
piece_table.add_text("c ".to_string(), 2);
|
|
want_str = "a c b";
|
|
assert_eq!(piece_table.text_len, want_str.len());
|
|
assert_eq!(piece_table.text(), want_str);
|
|
|
|
piece_table.add_text("d ".to_string(), 0);
|
|
want_str = "d a c b";
|
|
assert_eq!(piece_table.text_len, want_str.len());
|
|
assert_eq!(piece_table.text(), want_str);
|
|
}
|
|
|
|
#[test]
|
|
fn delete_text() {
|
|
let mut piece_table = PieceTable::new();
|
|
piece_table.add_text("abc".to_string(), 0);
|
|
piece_table.delete_text(0, 1);
|
|
let mut want_str = "bc";
|
|
assert_eq!(piece_table.text_len, want_str.len());
|
|
assert_eq!(piece_table.text(), want_str);
|
|
|
|
piece_table = PieceTable::new();
|
|
piece_table.add_text("abc".to_string(), 0);
|
|
piece_table.delete_text(1, 2);
|
|
want_str = "ac";
|
|
assert_eq!(piece_table.text_len, want_str.len());
|
|
assert_eq!(piece_table.text(), want_str);
|
|
|
|
piece_table = PieceTable::new();
|
|
piece_table.add_text("abc".to_string(), 0);
|
|
piece_table.delete_text(2, 3);
|
|
want_str = "ab";
|
|
assert_eq!(piece_table.text_len, want_str.len());
|
|
assert_eq!(piece_table.text(), want_str);
|
|
|
|
piece_table = PieceTable::new();
|
|
piece_table.add_text("abc".to_string(), 0);
|
|
piece_table.delete_text(2, 3);
|
|
want_str = "ab";
|
|
assert_eq!(piece_table.text_len, want_str.len());
|
|
assert_eq!(piece_table.text(), want_str);
|
|
|
|
piece_table = PieceTable::new();
|
|
piece_table.add_text("ab".to_string(), 0);
|
|
piece_table.add_text("cd".to_string(), 2);
|
|
piece_table.delete_text(2, 3);
|
|
want_str = "abd";
|
|
assert_eq!(piece_table.text_len, want_str.len());
|
|
assert_eq!(piece_table.text(), want_str);
|
|
|
|
piece_table = PieceTable::new();
|
|
piece_table.add_text("ab".to_string(), 0);
|
|
piece_table.add_text("cd".to_string(), 2);
|
|
piece_table.add_text("ef".to_string(), 4);
|
|
piece_table.delete_text(1, 5);
|
|
want_str = "af";
|
|
assert_eq!(piece_table.text_len, want_str.len());
|
|
assert_eq!(piece_table.text(), want_str);
|
|
|
|
piece_table = PieceTable::new();
|
|
piece_table.add_text("ab".to_string(), 0);
|
|
piece_table.add_text("cd".to_string(), 2);
|
|
piece_table.add_text("ef".to_string(), 4);
|
|
piece_table.delete_text(1, 6);
|
|
want_str = "a";
|
|
assert_eq!(piece_table.text_len, want_str.len());
|
|
assert_eq!(piece_table.text(), want_str);
|
|
|
|
piece_table = PieceTable::new();
|
|
piece_table.add_text("ab".to_string(), 0);
|
|
piece_table.add_text("cd".to_string(), 2);
|
|
piece_table.add_text("ef".to_string(), 4);
|
|
piece_table.delete_text(0, 6);
|
|
want_str = "";
|
|
assert_eq!(piece_table.text_len, want_str.len());
|
|
assert_eq!(piece_table.text(), want_str);
|
|
}
|
|
|
|
#[test]
|
|
fn undo_redo() {
|
|
let mut piece_table = PieceTable::new();
|
|
piece_table.add_text("abc".to_string(), 0);
|
|
piece_table.delete_text(0, 1);
|
|
let mut want_str = "abc";
|
|
piece_table.undo();
|
|
assert_eq!(piece_table.text_len, want_str.len());
|
|
assert_eq!(piece_table.text(), want_str);
|
|
|
|
piece_table.redo();
|
|
want_str = "bc";
|
|
assert_eq!(piece_table.text_len, want_str.len());
|
|
assert_eq!(piece_table.text(), want_str);
|
|
|
|
piece_table = PieceTable::new();
|
|
piece_table.add_text("abc".to_string(), 0);
|
|
piece_table.add_text("d".to_string(), 3); // "abcd"
|
|
piece_table.delete_text(0, 2); // "cd"
|
|
piece_table.undo();
|
|
want_str = "abcd";
|
|
assert_eq!(piece_table.text_len, want_str.len());
|
|
assert_eq!(piece_table.text(), want_str);
|
|
piece_table.undo();
|
|
want_str = "abc";
|
|
assert_eq!(piece_table.text_len, want_str.len());
|
|
assert_eq!(piece_table.text(), want_str);
|
|
piece_table.redo();
|
|
want_str = "abcd";
|
|
assert_eq!(piece_table.text_len, want_str.len());
|
|
assert_eq!(piece_table.text(), want_str);
|
|
piece_table.redo();
|
|
want_str = "cd";
|
|
assert_eq!(piece_table.text_len, want_str.len());
|
|
assert_eq!(piece_table.text(), want_str);
|
|
}
|
|
|
|
#[test]
|
|
fn edge_cases() {
|
|
let mut piece_table = PieceTable::new();
|
|
piece_table.add_text("\n".to_string(), 0);
|
|
let mut want_str = "\n";
|
|
assert_eq!(want_str.len(), 1);
|
|
assert_eq!(piece_table.text_len, want_str.len());
|
|
assert_eq!(piece_table.text(), want_str);
|
|
|
|
piece_table = PieceTable::new();
|
|
piece_table.add_text("\n\n\n\n".to_string(), 0);
|
|
want_str = "\n\n\n\n";
|
|
assert_eq!(want_str.len(), 4);
|
|
assert_eq!(piece_table.text_len, want_str.len());
|
|
assert_eq!(piece_table.text(), want_str);
|
|
|
|
// TODO: Add support for graphemes
|
|
// https://stackoverflow.com/a/46290728
|
|
piece_table = PieceTable::new();
|
|
piece_table.add_text("😀".to_string(), 0);
|
|
want_str = "😀";
|
|
assert_eq!(want_str.len(), 4);
|
|
// assert_eq!(want_str.graphemes(true).count(), 1);
|
|
assert_eq!(piece_table.text_len, want_str.len());
|
|
assert_eq!(piece_table.text(), want_str);
|
|
|
|
piece_table = PieceTable::new();
|
|
piece_table.add_text("é".to_string(), 0);
|
|
want_str = "é";
|
|
assert_eq!(want_str.len(), 3);
|
|
assert_eq!(want_str.chars().count(), 2);
|
|
// assert_eq!(want_str.graphemes(true).count(), 1);
|
|
assert_eq!(piece_table.text_len, want_str.len());
|
|
assert_eq!(piece_table.text(), want_str);
|
|
}
|
|
} |