multiple cursors with primary separate
This commit is contained in:
+275
-126
@@ -1,4 +1,4 @@
|
||||
use std::{cmp::min, fs::File, io::Write, mem::{replace, swap}};
|
||||
use std::{cmp::min, fs::File, io::Write, mem::replace};
|
||||
use crate::{BYTES_PER_LINE, app::WindowSize, buffer::{Buffer, Mode, PartialAction}, edit_action::EditAction};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
@@ -119,8 +119,8 @@ impl Buffer {
|
||||
|
||||
Action::CollapseSelection => self.collapse_selection(),
|
||||
|
||||
Action::ExtendLineBelow => self.extend_line_below(),
|
||||
Action::ExtendLineAbove => self.extend_line_above(),
|
||||
Action::ExtendLineBelow => self.extend_line_below(window_size),
|
||||
Action::ExtendLineAbove => self.extend_line_above(window_size),
|
||||
|
||||
Action::Delete => self.delete(),
|
||||
|
||||
@@ -162,95 +162,159 @@ impl Buffer {
|
||||
self.partial_action = Some(PartialAction::Space);
|
||||
}
|
||||
|
||||
const fn move_byte_up(&mut self, window_size: WindowSize) {
|
||||
if self.cursor.head >= BYTES_PER_LINE {
|
||||
self.cursor.head -= BYTES_PER_LINE;
|
||||
self.cursor.collapse();
|
||||
// TODO: all these move/extend-cursor operations could be DRYed together
|
||||
fn move_byte_up(&mut self, window_size: WindowSize) {
|
||||
self.primary_cursor.move_byte_up();
|
||||
|
||||
self.clamp_screen_to_cursor(window_size);
|
||||
for cursor in &mut self.cursors {
|
||||
cursor.move_byte_up();
|
||||
}
|
||||
self.cursors.sort_by_key(|cursor| cursor.head);
|
||||
|
||||
self.combine_cursors_if_overlapping();
|
||||
self.clamp_screen_to_primary_cursor(window_size);
|
||||
}
|
||||
|
||||
const fn move_byte_down(&mut self, window_size: WindowSize) {
|
||||
if self.max_contents_index() - self.cursor.head >= BYTES_PER_LINE {
|
||||
self.cursor.head += BYTES_PER_LINE;
|
||||
self.cursor.collapse();
|
||||
fn move_byte_down(&mut self, window_size: WindowSize) {
|
||||
let max_contents_index = self.max_contents_index();
|
||||
|
||||
self.clamp_screen_to_cursor(window_size);
|
||||
self.primary_cursor.move_byte_down(max_contents_index);
|
||||
|
||||
for cursor in &mut self.cursors {
|
||||
cursor.move_byte_down(max_contents_index);
|
||||
}
|
||||
self.cursors.sort_by_key(|cursor| cursor.head);
|
||||
|
||||
self.combine_cursors_if_overlapping();
|
||||
self.clamp_screen_to_primary_cursor(window_size);
|
||||
}
|
||||
|
||||
const fn move_byte_left(&mut self, window_size: WindowSize) {
|
||||
if self.cursor.head >= 1 {
|
||||
self.cursor.head -= 1;
|
||||
self.cursor.collapse();
|
||||
fn move_byte_left(&mut self, window_size: WindowSize) {
|
||||
self.primary_cursor.move_byte_left();
|
||||
|
||||
self.clamp_screen_to_cursor(window_size);
|
||||
for cursor in &mut self.cursors {
|
||||
cursor.move_byte_left();
|
||||
}
|
||||
self.cursors.sort_by_key(|cursor| cursor.head);
|
||||
|
||||
self.combine_cursors_if_overlapping();
|
||||
self.clamp_screen_to_primary_cursor(window_size);
|
||||
}
|
||||
|
||||
const fn move_byte_right(&mut self, window_size: WindowSize) {
|
||||
if self.max_contents_index() - self.cursor.head >= 1 {
|
||||
self.cursor.head += 1;
|
||||
self.cursor.collapse();
|
||||
fn move_byte_right(&mut self, window_size: WindowSize) {
|
||||
let max_contents_index = self.max_contents_index();
|
||||
|
||||
self.clamp_screen_to_cursor(window_size);
|
||||
self.primary_cursor.move_byte_right(max_contents_index);
|
||||
|
||||
for cursor in &mut self.cursors {
|
||||
cursor.move_byte_right(max_contents_index);
|
||||
}
|
||||
self.cursors.sort_by_key(|cursor| cursor.head);
|
||||
|
||||
self.combine_cursors_if_overlapping();
|
||||
self.clamp_screen_to_primary_cursor(window_size);
|
||||
}
|
||||
|
||||
const fn extend_byte_up(&mut self, window_size: WindowSize) {
|
||||
if self.cursor.head >= BYTES_PER_LINE {
|
||||
self.cursor.head -= BYTES_PER_LINE;
|
||||
self.clamp_screen_to_cursor(window_size);
|
||||
fn extend_byte_up(&mut self, window_size: WindowSize) {
|
||||
self.primary_cursor.extend_byte_up();
|
||||
|
||||
for cursor in &mut self.cursors {
|
||||
cursor.extend_byte_up();
|
||||
}
|
||||
self.cursors.sort_by_key(|cursor| cursor.head);
|
||||
|
||||
self.combine_cursors_if_overlapping();
|
||||
self.clamp_screen_to_primary_cursor(window_size);
|
||||
}
|
||||
|
||||
const fn extend_byte_down(&mut self, window_size: WindowSize) {
|
||||
if self.max_contents_index() - self.cursor.head >= BYTES_PER_LINE {
|
||||
self.cursor.head += BYTES_PER_LINE;
|
||||
self.clamp_screen_to_cursor(window_size);
|
||||
fn extend_byte_down(&mut self, window_size: WindowSize) {
|
||||
let max_contents_index = self.max_contents_index();
|
||||
|
||||
self.primary_cursor.extend_byte_down(max_contents_index);
|
||||
|
||||
for cursor in &mut self.cursors {
|
||||
cursor.extend_byte_down(max_contents_index);
|
||||
}
|
||||
self.cursors.sort_by_key(|cursor| cursor.head);
|
||||
|
||||
self.combine_cursors_if_overlapping();
|
||||
self.clamp_screen_to_primary_cursor(window_size);
|
||||
}
|
||||
|
||||
const fn extend_byte_left(&mut self, window_size: WindowSize) {
|
||||
if self.cursor.head >= 1 {
|
||||
self.cursor.head -= 1;
|
||||
self.clamp_screen_to_cursor(window_size);
|
||||
fn extend_byte_left(&mut self, window_size: WindowSize) {
|
||||
self.primary_cursor.extend_byte_left();
|
||||
|
||||
for cursor in &mut self.cursors {
|
||||
cursor.extend_byte_left();
|
||||
}
|
||||
self.cursors.sort_by_key(|cursor| cursor.head);
|
||||
|
||||
self.combine_cursors_if_overlapping();
|
||||
self.clamp_screen_to_primary_cursor(window_size);
|
||||
}
|
||||
|
||||
const fn extend_byte_right(&mut self, window_size: WindowSize) {
|
||||
if self.max_contents_index() - self.cursor.head >= 1 {
|
||||
self.cursor.head += 1;
|
||||
self.clamp_screen_to_cursor(window_size);
|
||||
fn extend_byte_right(&mut self, window_size: WindowSize) {
|
||||
let max_contents_index = self.max_contents_index();
|
||||
|
||||
self.primary_cursor.extend_byte_right(max_contents_index);
|
||||
|
||||
for cursor in &mut self.cursors {
|
||||
cursor.extend_byte_right(max_contents_index);
|
||||
}
|
||||
self.cursors.sort_by_key(|cursor| cursor.head);
|
||||
|
||||
self.combine_cursors_if_overlapping();
|
||||
self.clamp_screen_to_primary_cursor(window_size);
|
||||
}
|
||||
|
||||
const fn goto_line_start(&mut self) {
|
||||
self.cursor.head -= self.cursor.head % BYTES_PER_LINE;
|
||||
self.cursor.collapse();
|
||||
fn goto_line_start(&mut self) {
|
||||
self.primary_cursor.goto_line_start();
|
||||
|
||||
for cursor in &mut self.cursors {
|
||||
cursor.goto_line_start();
|
||||
}
|
||||
self.cursors.sort_by_key(|cursor| cursor.head);
|
||||
|
||||
self.combine_cursors_if_overlapping();
|
||||
}
|
||||
|
||||
fn goto_line_end(&mut self) {
|
||||
self.cursor.head = min(
|
||||
self.cursor.head + BYTES_PER_LINE - 1 - (self.cursor.head % BYTES_PER_LINE),
|
||||
self.max_contents_index()
|
||||
);
|
||||
self.cursor.collapse();
|
||||
let max_contents_index = self.max_contents_index();
|
||||
|
||||
self.primary_cursor.goto_line_end(max_contents_index);
|
||||
|
||||
for cursor in &mut self.cursors {
|
||||
cursor.goto_line_end(max_contents_index);
|
||||
}
|
||||
self.cursors.sort_by_key(|cursor| cursor.head);
|
||||
|
||||
self.combine_cursors_if_overlapping();
|
||||
}
|
||||
|
||||
const fn goto_file_start(&mut self, window_size: WindowSize) {
|
||||
self.cursor.head %= BYTES_PER_LINE;
|
||||
self.cursor.collapse();
|
||||
self.clamp_screen_to_cursor(window_size);
|
||||
fn goto_file_start(&mut self, window_size: WindowSize) {
|
||||
self.primary_cursor.goto_file_start();
|
||||
|
||||
for cursor in &mut self.cursors {
|
||||
cursor.goto_file_start();
|
||||
}
|
||||
self.cursors.sort_by_key(|cursor| cursor.head);
|
||||
|
||||
self.combine_cursors_if_overlapping();
|
||||
self.clamp_screen_to_primary_cursor(window_size);
|
||||
}
|
||||
|
||||
const fn goto_file_end(&mut self, window_size: WindowSize) {
|
||||
self.cursor.head = previous_multiple_of(BYTES_PER_LINE, self.contents.len()) +
|
||||
(self.cursor.head % BYTES_PER_LINE);
|
||||
fn goto_file_end(&mut self, window_size: WindowSize) {
|
||||
let max_contents_index = self.max_contents_index();
|
||||
|
||||
self.cursor.collapse();
|
||||
self.clamp_screen_to_cursor(window_size);
|
||||
self.primary_cursor.goto_file_end(max_contents_index);
|
||||
|
||||
for cursor in &mut self.cursors {
|
||||
cursor.goto_file_end(max_contents_index);
|
||||
}
|
||||
self.cursors.sort_by_key(|cursor| cursor.head);
|
||||
|
||||
self.combine_cursors_if_overlapping();
|
||||
self.clamp_screen_to_primary_cursor(window_size);
|
||||
}
|
||||
|
||||
fn scroll_down(&mut self, window_size: WindowSize) {
|
||||
@@ -260,39 +324,72 @@ impl Buffer {
|
||||
self.scroll_position + BYTES_PER_LINE,
|
||||
self.contents.len() - (5 * BYTES_PER_LINE)
|
||||
);
|
||||
self.cursor.clamp(self.scroll_position, window_size.visible_byte_count());
|
||||
|
||||
self.primary_cursor.clamp(self.scroll_position, window_size.visible_byte_count());
|
||||
self.combine_cursors_if_overlapping();
|
||||
}
|
||||
|
||||
fn scroll_up(&mut self, window_size: WindowSize) {
|
||||
self.scroll_position = self.scroll_position.saturating_sub(BYTES_PER_LINE);
|
||||
self.cursor.clamp(self.scroll_position, window_size.visible_byte_count());
|
||||
self.primary_cursor.clamp(self.scroll_position, window_size.visible_byte_count());
|
||||
self.combine_cursors_if_overlapping();
|
||||
}
|
||||
|
||||
fn page_cursor_half_down(&mut self, window_size: WindowSize) {
|
||||
if self.contents.len() <= 5 * BYTES_PER_LINE { return; }
|
||||
|
||||
let head_offset = self.cursor.head - self.scroll_position;
|
||||
let tail_offset = self.cursor.tail - self.scroll_position;
|
||||
let old_scroll_position = self.scroll_position;
|
||||
|
||||
self.scroll_position = min(
|
||||
self.scroll_position + (window_size.visible_byte_count() / 2).next_multiple_of(BYTES_PER_LINE),
|
||||
self.contents.len() - (5 * BYTES_PER_LINE)
|
||||
);
|
||||
|
||||
self.cursor.head = (self.scroll_position + head_offset).min(self.max_contents_index());
|
||||
self.cursor.tail = (self.scroll_position + tail_offset).min(self.max_contents_index());
|
||||
let scroll_position_change = self.scroll_position - old_scroll_position;
|
||||
let max_contents_index = self.max_contents_index();
|
||||
|
||||
self.primary_cursor.head = min(
|
||||
self.primary_cursor.head + scroll_position_change,
|
||||
max_contents_index
|
||||
);
|
||||
self.primary_cursor.tail = min(
|
||||
self.primary_cursor.tail + scroll_position_change,
|
||||
max_contents_index
|
||||
);
|
||||
|
||||
for cursor in &mut self.cursors {
|
||||
cursor.head = (cursor.head + scroll_position_change).min(max_contents_index);
|
||||
cursor.tail = (cursor.tail + scroll_position_change).min(max_contents_index);
|
||||
}
|
||||
|
||||
self.combine_cursors_if_overlapping();
|
||||
}
|
||||
|
||||
fn page_cursor_half_up(&mut self, window_size: WindowSize) {
|
||||
let head_offset = self.cursor.head - self.scroll_position;
|
||||
let tail_offset = self.cursor.tail - self.scroll_position;
|
||||
let old_scroll_position = self.scroll_position;
|
||||
|
||||
self.scroll_position = self.scroll_position.saturating_sub(
|
||||
(window_size.visible_byte_count() / 2).next_multiple_of(BYTES_PER_LINE)
|
||||
);
|
||||
|
||||
self.cursor.head = (self.scroll_position + head_offset).min(self.max_contents_index());
|
||||
self.cursor.tail = (self.scroll_position + tail_offset).min(self.max_contents_index());
|
||||
let scroll_position_change = old_scroll_position - self.scroll_position;
|
||||
let max_contents_index = self.max_contents_index();
|
||||
|
||||
self.primary_cursor.head = min(
|
||||
self.primary_cursor.head - scroll_position_change,
|
||||
max_contents_index
|
||||
);
|
||||
self.primary_cursor.tail = min(
|
||||
self.primary_cursor.tail - scroll_position_change,
|
||||
max_contents_index
|
||||
);
|
||||
|
||||
for cursor in &mut self.cursors {
|
||||
cursor.head = (cursor.head - scroll_position_change).min(max_contents_index);
|
||||
cursor.tail = (cursor.tail - scroll_position_change).min(max_contents_index);
|
||||
}
|
||||
|
||||
self.combine_cursors_if_overlapping();
|
||||
}
|
||||
|
||||
fn page_down(&mut self, window_size: WindowSize) {
|
||||
@@ -302,90 +399,147 @@ impl Buffer {
|
||||
self.scroll_position + window_size.visible_byte_count(),
|
||||
self.contents.len() - (5 * BYTES_PER_LINE)
|
||||
);
|
||||
self.cursor.clamp(self.scroll_position, window_size.visible_byte_count());
|
||||
|
||||
self.primary_cursor.clamp(self.scroll_position, window_size.visible_byte_count());
|
||||
self.combine_cursors_if_overlapping();
|
||||
}
|
||||
|
||||
fn page_up(&mut self, window_size: WindowSize) {
|
||||
self.scroll_position = self.scroll_position.saturating_sub(
|
||||
window_size.visible_byte_count()
|
||||
);
|
||||
self.cursor.clamp(self.scroll_position, window_size.visible_byte_count());
|
||||
|
||||
self.primary_cursor.clamp(self.scroll_position, window_size.visible_byte_count());
|
||||
self.combine_cursors_if_overlapping();
|
||||
}
|
||||
|
||||
fn move_next_word_start(&mut self, window_size: WindowSize) {
|
||||
self.cursor.move_to_next_word(self.max_contents_index());
|
||||
self.clamp_screen_to_cursor(window_size);
|
||||
let max_contents_index = self.max_contents_index();
|
||||
|
||||
self.primary_cursor.move_to_next_word(max_contents_index);
|
||||
|
||||
for cursor in &mut self.cursors {
|
||||
cursor.move_to_next_word(max_contents_index);
|
||||
}
|
||||
self.cursors.sort_by_key(|cursor| cursor.head);
|
||||
|
||||
self.combine_cursors_if_overlapping();
|
||||
self.clamp_screen_to_primary_cursor(window_size);
|
||||
}
|
||||
|
||||
fn move_next_word_end(&mut self, window_size: WindowSize) {
|
||||
self.cursor.move_to_next_end(self.max_contents_index());
|
||||
self.clamp_screen_to_cursor(window_size);
|
||||
let max_contents_index = self.max_contents_index();
|
||||
|
||||
self.primary_cursor.move_to_next_end(max_contents_index);
|
||||
|
||||
for cursor in &mut self.cursors {
|
||||
cursor.move_to_next_end(max_contents_index);
|
||||
}
|
||||
self.cursors.sort_by_key(|cursor| cursor.head);
|
||||
|
||||
self.combine_cursors_if_overlapping();
|
||||
self.clamp_screen_to_primary_cursor(window_size);
|
||||
}
|
||||
|
||||
const fn move_previous_word_start(&mut self, window_size: WindowSize) {
|
||||
self.cursor.move_to_previous_beginning();
|
||||
self.clamp_screen_to_cursor(window_size);
|
||||
fn move_previous_word_start(&mut self, window_size: WindowSize) {
|
||||
self.primary_cursor.move_to_previous_beginning();
|
||||
|
||||
for cursor in &mut self.cursors {
|
||||
cursor.move_to_previous_beginning();
|
||||
}
|
||||
self.cursors.sort_by_key(|cursor| cursor.head);
|
||||
|
||||
self.combine_cursors_if_overlapping();
|
||||
self.clamp_screen_to_primary_cursor(window_size);
|
||||
}
|
||||
|
||||
fn extend_next_word_start(&mut self, window_size: WindowSize) {
|
||||
self.cursor.extend_to_next_word(self.max_contents_index());
|
||||
self.clamp_screen_to_cursor(window_size);
|
||||
let max_contents_index = self.max_contents_index();
|
||||
|
||||
self.primary_cursor.extend_to_next_word(max_contents_index);
|
||||
|
||||
for cursor in &mut self.cursors {
|
||||
cursor.extend_to_next_word(max_contents_index);
|
||||
}
|
||||
self.cursors.sort_by_key(|cursor| cursor.head);
|
||||
|
||||
self.combine_cursors_if_overlapping();
|
||||
self.clamp_screen_to_primary_cursor(window_size);
|
||||
}
|
||||
|
||||
fn extend_next_word_end(&mut self, window_size: WindowSize) {
|
||||
self.cursor.extend_to_next_end(self.max_contents_index());
|
||||
self.clamp_screen_to_cursor(window_size);
|
||||
let max_contents_index = self.max_contents_index();
|
||||
|
||||
self.primary_cursor.extend_to_next_end(max_contents_index);
|
||||
|
||||
for cursor in &mut self.cursors {
|
||||
cursor.extend_to_next_end(max_contents_index);
|
||||
}
|
||||
self.cursors.sort_by_key(|cursor| cursor.head);
|
||||
|
||||
self.combine_cursors_if_overlapping();
|
||||
self.clamp_screen_to_primary_cursor(window_size);
|
||||
}
|
||||
|
||||
const fn extend_previous_word_start(&mut self, window_size: WindowSize) {
|
||||
self.cursor.extend_to_previous_beginning();
|
||||
self.clamp_screen_to_cursor(window_size);
|
||||
fn extend_previous_word_start(&mut self, window_size: WindowSize) {
|
||||
self.primary_cursor.extend_to_previous_beginning();
|
||||
|
||||
for cursor in &mut self.cursors {
|
||||
cursor.extend_to_previous_beginning();
|
||||
}
|
||||
self.cursors.sort_by_key(|cursor| cursor.head);
|
||||
|
||||
self.combine_cursors_if_overlapping();
|
||||
self.clamp_screen_to_primary_cursor(window_size);
|
||||
}
|
||||
|
||||
const fn collapse_selection(&mut self) {
|
||||
self.cursor.collapse();
|
||||
}
|
||||
fn collapse_selection(&mut self) {
|
||||
self.primary_cursor.collapse();
|
||||
|
||||
fn extend_line_below(&mut self) {
|
||||
if self.cursor.tail > self.cursor.head {
|
||||
swap(&mut self.cursor.head, &mut self.cursor.tail);
|
||||
}
|
||||
|
||||
if self.cursor.tail.is_multiple_of(BYTES_PER_LINE) &&
|
||||
self.cursor.head % BYTES_PER_LINE == BYTES_PER_LINE - 1
|
||||
{
|
||||
self.cursor.head = min(
|
||||
self.cursor.head + BYTES_PER_LINE,
|
||||
self.max_contents_index()
|
||||
);
|
||||
} else {
|
||||
self.cursor.tail -= self.cursor.tail % BYTES_PER_LINE;
|
||||
self.cursor.head += BYTES_PER_LINE - 1 - (self.cursor.head % BYTES_PER_LINE);
|
||||
for cursor in &mut self.cursors {
|
||||
cursor.collapse();
|
||||
}
|
||||
}
|
||||
|
||||
const fn extend_line_above(&mut self) {
|
||||
if self.cursor.head > self.cursor.tail {
|
||||
swap(&mut self.cursor.head, &mut self.cursor.tail);
|
||||
fn extend_line_below(&mut self, window_size: WindowSize) {
|
||||
let max_contents_index = self.max_contents_index();
|
||||
|
||||
self.primary_cursor.extend_line_below(max_contents_index);
|
||||
|
||||
for cursor in &mut self.cursors {
|
||||
cursor.extend_line_below(max_contents_index);
|
||||
}
|
||||
self.cursors.sort_by_key(|cursor| cursor.head);
|
||||
|
||||
self.combine_cursors_if_overlapping();
|
||||
self.clamp_screen_to_primary_cursor(window_size);
|
||||
}
|
||||
|
||||
if self.cursor.head.is_multiple_of(BYTES_PER_LINE) &&
|
||||
(self.cursor.tail % BYTES_PER_LINE == BYTES_PER_LINE - 1 ||
|
||||
self.cursor.tail == self.max_contents_index())
|
||||
{
|
||||
self.cursor.head = self.cursor.head.saturating_sub(BYTES_PER_LINE);
|
||||
} else {
|
||||
self.cursor.head -= self.cursor.head % BYTES_PER_LINE;
|
||||
self.cursor.tail += BYTES_PER_LINE - 1 - (self.cursor.tail % BYTES_PER_LINE);
|
||||
fn extend_line_above(&mut self, window_size: WindowSize) {
|
||||
let max_contents_index = self.max_contents_index();
|
||||
|
||||
self.primary_cursor.extend_line_above(max_contents_index);
|
||||
|
||||
for cursor in &mut self.cursors {
|
||||
cursor.extend_line_above(max_contents_index);
|
||||
}
|
||||
self.cursors.sort_by_key(|cursor| cursor.head);
|
||||
|
||||
self.combine_cursors_if_overlapping();
|
||||
self.clamp_screen_to_primary_cursor(window_size);
|
||||
}
|
||||
|
||||
fn delete(&mut self) {
|
||||
if !self.contents.is_empty() {
|
||||
self.execute_and_add(
|
||||
EditAction::Delete {
|
||||
cursor: self.cursor,
|
||||
old_data: self.contents[self.cursor.range()].into()
|
||||
primary_cursor: self.primary_cursor,
|
||||
cursors: self.cursors.clone(),
|
||||
primary_old_data: self.contents[self.primary_cursor.range()].into(),
|
||||
old_data: self.cursors
|
||||
.iter()
|
||||
.map(|cursor| self.contents[cursor.range()].to_vec())
|
||||
.collect(),
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -446,20 +600,15 @@ impl Buffer {
|
||||
|
||||
// helpers
|
||||
impl Buffer {
|
||||
const fn clamp_screen_to_cursor(&mut self, window_size: WindowSize) {
|
||||
if self.cursor.head < self.scroll_position {
|
||||
self.scroll_position -= (self.scroll_position - self.cursor.head).next_multiple_of(BYTES_PER_LINE);
|
||||
} else if self.cursor.head > self.scroll_position + window_size.visible_byte_count() - 1 {
|
||||
let screen_edge_offset_to_cursor = self.cursor.head - (self.scroll_position + window_size.visible_byte_count() - 1);
|
||||
const fn clamp_screen_to_primary_cursor(&mut self, window_size: WindowSize) {
|
||||
if self.primary_cursor.head < self.scroll_position {
|
||||
self.scroll_position -= (self.scroll_position - self.primary_cursor.head)
|
||||
.next_multiple_of(BYTES_PER_LINE);
|
||||
} else if self.primary_cursor.head > self.scroll_position + window_size.visible_byte_count() - 1 {
|
||||
let screen_edge_offset_to_cursor = self.primary_cursor.head - (
|
||||
self.scroll_position + window_size.visible_byte_count() - 1
|
||||
);
|
||||
self.scroll_position += screen_edge_offset_to_cursor.next_multiple_of(BYTES_PER_LINE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const fn previous_multiple_of(multiple: usize, number: usize) -> usize {
|
||||
if number == 0 {
|
||||
0
|
||||
} else {
|
||||
(number - 1) - ((number - 1) % multiple)
|
||||
}
|
||||
}
|
||||
|
||||
+38
-4
@@ -1,3 +1,4 @@
|
||||
use core::slice::GetDisjointMutIndex;
|
||||
use std::{fs::File, io::Read, path::PathBuf};
|
||||
use crossterm::event::KeyEvent;
|
||||
use ratatui::{style::Color, text::Span};
|
||||
@@ -12,7 +13,8 @@ pub struct Buffer {
|
||||
pub contents: Vec<u8>,
|
||||
|
||||
pub scroll_position: usize,
|
||||
pub cursor: Cursor,
|
||||
pub primary_cursor: Cursor,
|
||||
pub cursors: Vec<Cursor>,
|
||||
|
||||
pub mode: Mode,
|
||||
pub partial_action: Option<PartialAction>,
|
||||
@@ -79,7 +81,8 @@ impl Buffer {
|
||||
contents,
|
||||
|
||||
scroll_position: 0,
|
||||
cursor: Cursor::default(),
|
||||
primary_cursor: Cursor::default(),
|
||||
cursors: Vec::new(),
|
||||
|
||||
mode: Mode::Normal,
|
||||
partial_action: None,
|
||||
@@ -110,8 +113,13 @@ impl Buffer {
|
||||
if let Some(partial_replace) = self.partial_replace.take() {
|
||||
self.execute_and_add(
|
||||
EditAction::Replace {
|
||||
cursor: self.cursor,
|
||||
old_data: self.contents[self.cursor.range()].into(),
|
||||
primary_cursor: self.primary_cursor,
|
||||
cursors: self.cursors.clone(),
|
||||
primary_old_data: self.contents[self.primary_cursor.range()].to_vec(),
|
||||
old_data: self.cursors
|
||||
.iter()
|
||||
.map(|cursor| self.contents[cursor.range()].to_vec())
|
||||
.collect(),
|
||||
new_byte: partial_replace << 4 | nybble
|
||||
}
|
||||
);
|
||||
@@ -161,6 +169,32 @@ impl Buffer {
|
||||
pub const fn max_contents_index(&self) -> usize {
|
||||
self.contents.len().saturating_sub(1)
|
||||
}
|
||||
|
||||
pub fn combine_cursors_if_overlapping(&mut self) {
|
||||
if self.cursors.is_empty() { return; }
|
||||
|
||||
let mut index = 0;
|
||||
|
||||
while index < self.cursors.len() - 1 {
|
||||
while index < self.cursors.len() - 1 &&
|
||||
self.cursors[index].range().is_overlapping(
|
||||
&self.cursors[index + 1].range())
|
||||
{
|
||||
let next_cursor = self.cursors[index + 1];
|
||||
self.cursors[index].combine_with(next_cursor);
|
||||
self.cursors.remove(index + 1);
|
||||
}
|
||||
|
||||
if self.primary_cursor.range()
|
||||
.is_overlapping(&self.cursors[index].range())
|
||||
{
|
||||
self.primary_cursor.combine_with(self.cursors[index]);
|
||||
self.cursors.remove(index);
|
||||
}
|
||||
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn nybble_from_hex(hex: char) -> Option<u8> {
|
||||
|
||||
+22
-8
@@ -82,7 +82,7 @@ mod address {
|
||||
}
|
||||
|
||||
mod hex {
|
||||
use std::{borrow::Cow, mem};
|
||||
use std::{borrow::Cow, iter, mem};
|
||||
use itertools::{Itertools, repeat_n};
|
||||
use ratatui::{style::{Color, Style, Stylize}, text::Span};
|
||||
|
||||
@@ -199,7 +199,9 @@ mod hex {
|
||||
byte: u8
|
||||
) -> Span<'static> {
|
||||
if self.partial_action == Some(PartialAction::Replace) &&
|
||||
self.cursor.contains(address).is_some()
|
||||
iter::once(&self.primary_cursor)
|
||||
.chain(&self.cursors)
|
||||
.any(|cursor| cursor.contains(address).is_some())
|
||||
{
|
||||
let replaced_byte = self.partial_replace.unwrap_or(0) << 4;
|
||||
|
||||
@@ -224,7 +226,10 @@ mod hex {
|
||||
_ => Color::Gray
|
||||
};
|
||||
|
||||
match self.cursor.contains(address) {
|
||||
match iter::once(&self.primary_cursor)
|
||||
.chain(&self.cursors)
|
||||
.find_map(|cursor| cursor.contains(address))
|
||||
{
|
||||
Some(InCursor::Head) => span.bg(head_color),
|
||||
Some(InCursor::Rest) => span.bg(Color::select_grey()),
|
||||
None => span,
|
||||
@@ -232,7 +237,10 @@ mod hex {
|
||||
}
|
||||
|
||||
fn render_large_space_before(&self, address: usize) -> Span<'static> {
|
||||
if self.cursor.contains_space_before(address) {
|
||||
if iter::once(&self.primary_cursor)
|
||||
.chain(&self.cursors)
|
||||
.any(|cursor| cursor.contains_space_before(address))
|
||||
{
|
||||
Span {
|
||||
style: Style::new().bg(Color::select_grey()),
|
||||
content: " ".into()
|
||||
@@ -243,7 +251,10 @@ mod hex {
|
||||
}
|
||||
|
||||
fn render_space_before(&self, address: usize) -> Span<'static> {
|
||||
if self.cursor.contains_space_before(address) {
|
||||
if iter::once(&self.primary_cursor)
|
||||
.chain(&self.cursors)
|
||||
.any(|cursor| cursor.contains_space_before(address))
|
||||
{
|
||||
Span {
|
||||
style: Style::new().bg(Color::select_grey()),
|
||||
content: " ".into()
|
||||
@@ -306,7 +317,7 @@ mod hex {
|
||||
}
|
||||
|
||||
mod character_panel {
|
||||
use std::{borrow::Cow, mem};
|
||||
use std::{borrow::Cow, iter, mem};
|
||||
use ratatui::{style::{Color, Style, Stylize}, text::Span};
|
||||
use crate::{buffer::Buffer, cardinality::HasCardinality, cursor::InCursor, custom_greys::CustomGreys, empty_span::empty_span};
|
||||
|
||||
@@ -332,7 +343,10 @@ mod character_panel {
|
||||
|
||||
let span = SPAN_FOR_BYTE[byte as usize].clone();
|
||||
|
||||
match self.cursor.contains(address) {
|
||||
match iter::once(&self.primary_cursor)
|
||||
.chain(&self.cursors)
|
||||
.find_map(|cursor| cursor.contains(address))
|
||||
{
|
||||
Some(InCursor::Head) => span.bg(Color::select_grey()),
|
||||
Some(InCursor::Rest) => span.on_dark_gray(),
|
||||
None => span,
|
||||
@@ -434,7 +448,7 @@ mod extra_statuses {
|
||||
format!("{partial_action} ").into()
|
||||
} else {
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
let percentage = self.cursor.head as f64 / self.max_contents_index() as f64 * 100.0;
|
||||
let percentage = self.primary_cursor.head as f64 / self.max_contents_index() as f64 * 100.0;
|
||||
|
||||
format!("{partial_action} {percentage:.0}% ").into()
|
||||
}
|
||||
|
||||
+141
-11
@@ -1,4 +1,6 @@
|
||||
use std::ops::RangeInclusive;
|
||||
use std::{cmp::{max, min}, mem::swap, ops::RangeInclusive};
|
||||
|
||||
use crate::BYTES_PER_LINE;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub struct Cursor {
|
||||
@@ -12,6 +14,22 @@ pub enum InCursor {
|
||||
}
|
||||
|
||||
impl Cursor {
|
||||
pub const fn at(index: usize) -> Self {
|
||||
Self { head: index, tail: index }
|
||||
}
|
||||
|
||||
pub fn lower_bound(&self) -> usize {
|
||||
min(self.head, self.tail)
|
||||
}
|
||||
|
||||
pub fn upper_bound(&self) -> usize {
|
||||
max(self.head, self.tail)
|
||||
}
|
||||
|
||||
pub fn range(&self) -> RangeInclusive<usize> {
|
||||
self.lower_bound()..=self.upper_bound()
|
||||
}
|
||||
|
||||
pub const fn contains(&self, index: usize) -> Option<InCursor> {
|
||||
if index == self.head {
|
||||
Some(InCursor::Head)
|
||||
@@ -41,14 +59,93 @@ impl Cursor {
|
||||
self.tail = self.tail.clamp(scroll_position, max_row);
|
||||
}
|
||||
|
||||
pub const fn range(&self) -> RangeInclusive<usize> {
|
||||
pub fn combine_with(&mut self, other: Self) {
|
||||
if self.head < self.tail {
|
||||
self.head..=self.tail
|
||||
self.head = min(self.head, other.head);
|
||||
self.tail = max(self.tail, other.tail);
|
||||
} else {
|
||||
self.tail..=self.head
|
||||
self.head = max(self.head, other.head);
|
||||
self.tail = min(self.tail, other.tail);
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn move_byte_up(&mut self) {
|
||||
if self.head >= BYTES_PER_LINE {
|
||||
self.head -= BYTES_PER_LINE;
|
||||
self.collapse();
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn move_byte_down(&mut self, max: usize) {
|
||||
if max - self.head >= BYTES_PER_LINE {
|
||||
self.head += BYTES_PER_LINE;
|
||||
self.collapse();
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn move_byte_left(&mut self) {
|
||||
if self.head >= 1 {
|
||||
self.head -= 1;
|
||||
self.collapse();
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn move_byte_right(&mut self, max: usize) {
|
||||
if max - self.head >= 1 {
|
||||
self.head += 1;
|
||||
self.collapse();
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn extend_byte_up(&mut self) {
|
||||
if self.head >= BYTES_PER_LINE {
|
||||
self.head -= BYTES_PER_LINE;
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn extend_byte_down(&mut self, max: usize) {
|
||||
if max - self.head >= BYTES_PER_LINE {
|
||||
self.head += BYTES_PER_LINE;
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn extend_byte_left(&mut self) {
|
||||
if self.head >= 1 {
|
||||
self.head -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn extend_byte_right(&mut self, max: usize) {
|
||||
if max - self.head >= 1 {
|
||||
self.head += 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn goto_line_start(&mut self) {
|
||||
self.head -= self.head % BYTES_PER_LINE;
|
||||
self.collapse();
|
||||
}
|
||||
|
||||
pub fn goto_line_end(&mut self, max: usize) {
|
||||
self.head = min(
|
||||
self.head + BYTES_PER_LINE - 1 - (self.head % BYTES_PER_LINE),
|
||||
max
|
||||
);
|
||||
self.collapse();
|
||||
}
|
||||
|
||||
pub const fn goto_file_start(&mut self) {
|
||||
self.head %= BYTES_PER_LINE;
|
||||
self.collapse();
|
||||
}
|
||||
|
||||
pub const fn goto_file_end(&mut self, max: usize) {
|
||||
self.head = previous_multiple_of(BYTES_PER_LINE, max + 1) +
|
||||
(self.head % BYTES_PER_LINE);
|
||||
|
||||
self.collapse();
|
||||
}
|
||||
|
||||
pub fn move_to_next_word(&mut self, max: usize) {
|
||||
if self.head == max { return; }
|
||||
|
||||
@@ -113,18 +210,51 @@ impl Cursor {
|
||||
self.head -= self.head % 4;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn extend_line_below(&mut self, max: usize) {
|
||||
if self.tail > self.head {
|
||||
swap(&mut self.head, &mut self.tail);
|
||||
}
|
||||
|
||||
if self.tail.is_multiple_of(BYTES_PER_LINE) &&
|
||||
self.head % BYTES_PER_LINE == BYTES_PER_LINE - 1
|
||||
{
|
||||
self.head = min(self.head + BYTES_PER_LINE, max);
|
||||
} else {
|
||||
self.tail -= self.tail % BYTES_PER_LINE;
|
||||
self.head += BYTES_PER_LINE - 1 - (self.head % BYTES_PER_LINE);
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn extend_line_above(&mut self, max: usize) {
|
||||
if self.head > self.tail {
|
||||
swap(&mut self.head, &mut self.tail);
|
||||
}
|
||||
|
||||
if self.head.is_multiple_of(BYTES_PER_LINE) &&
|
||||
(self.tail % BYTES_PER_LINE == BYTES_PER_LINE - 1 ||
|
||||
self.tail == max)
|
||||
{
|
||||
self.head = self.head.saturating_sub(BYTES_PER_LINE);
|
||||
} else {
|
||||
self.head -= self.head % BYTES_PER_LINE;
|
||||
self.tail += BYTES_PER_LINE - 1 - (self.tail % BYTES_PER_LINE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const fn previous_multiple_of(multiple: usize, number: usize) -> usize {
|
||||
if number == 0 {
|
||||
0
|
||||
} else {
|
||||
(number - 1) - ((number - 1) % multiple)
|
||||
}
|
||||
}
|
||||
|
||||
mod tests {
|
||||
#[allow(unused_imports)]
|
||||
use crate::cursor::Cursor;
|
||||
|
||||
impl Cursor {
|
||||
#[allow(dead_code)]
|
||||
const fn at(index: usize) -> Self {
|
||||
Self { head: index, tail: index }
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn next_word() {
|
||||
// [a]bcd efgh -> abcd [e]fgh
|
||||
|
||||
+115
-25
@@ -1,4 +1,4 @@
|
||||
use std::cmp::min;
|
||||
use std::{cmp::min, convert::identity, iter};
|
||||
use crate::{buffer::Buffer, cursor::Cursor};
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -10,16 +10,24 @@ pub enum EditAction {
|
||||
// then swap it back in once the undo/redo is done
|
||||
Placeholder,
|
||||
Delete {
|
||||
cursor: Cursor,
|
||||
old_data: Vec<u8>
|
||||
primary_cursor: Cursor,
|
||||
cursors: Vec<Cursor>,
|
||||
|
||||
primary_old_data: Vec<u8>,
|
||||
old_data: Vec<Vec<u8>>
|
||||
},
|
||||
Replace {
|
||||
cursor: Cursor,
|
||||
old_data: Vec<u8>,
|
||||
primary_cursor: Cursor,
|
||||
cursors: Vec<Cursor>,
|
||||
|
||||
primary_old_data: Vec<u8>,
|
||||
old_data: Vec<Vec<u8>>,
|
||||
|
||||
new_byte: u8
|
||||
},
|
||||
// Insert {
|
||||
// cursor: Cursor,
|
||||
// primary_cursor: Cursor,
|
||||
// cursors: Vec<Cursor>,
|
||||
// which side of cursor? append/insert
|
||||
// new_data: Vec<u8>
|
||||
// }
|
||||
@@ -46,53 +54,135 @@ impl Buffer {
|
||||
pub fn execute_edit(&mut self, edit_action: &EditAction) {
|
||||
match edit_action {
|
||||
EditAction::Placeholder => unreachable!(),
|
||||
EditAction::Delete { cursor, .. } => self.delete_at(*cursor),
|
||||
EditAction::Delete { primary_cursor, cursors, .. } => self.delete_at(*primary_cursor, cursors),
|
||||
EditAction::Replace {
|
||||
cursor, old_data: _, new_byte
|
||||
} => self.replace_at_with(*cursor, *new_byte),
|
||||
primary_cursor, cursors, primary_old_data: _, old_data: _, new_byte
|
||||
} => self.replace_at_with(*primary_cursor, cursors, *new_byte),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn undo_edit(&mut self, edit_action: &EditAction) {
|
||||
match edit_action {
|
||||
EditAction::Placeholder => unreachable!(),
|
||||
EditAction::Delete { cursor, old_data } => self.undo_delete_at(*cursor, old_data),
|
||||
EditAction::Delete {
|
||||
primary_cursor, cursors, primary_old_data, old_data
|
||||
} => self.undo_delete_at(*primary_cursor, cursors, primary_old_data, old_data),
|
||||
EditAction::Replace {
|
||||
cursor, old_data, ..
|
||||
} => self.undo_replace_at_with(*cursor, old_data),
|
||||
primary_cursor, cursors, primary_old_data, old_data, ..
|
||||
} => self.undo_replace_at_with(*primary_cursor, cursors, primary_old_data, old_data),
|
||||
}
|
||||
}
|
||||
|
||||
fn delete_at(&mut self, cursor: Cursor) {
|
||||
self.contents.drain(cursor.range());
|
||||
fn delete_at(
|
||||
&mut self,
|
||||
primary_cursor: Cursor,
|
||||
cursors: &[Cursor]
|
||||
) {
|
||||
let mut bytes_deleted_so_far = 0;
|
||||
|
||||
self.cursor.head = min(min(cursor.head, cursor.tail), self.max_contents_index());
|
||||
self.cursor.collapse();
|
||||
for cursor in cursors_in_order(primary_cursor, cursors) {
|
||||
let range = cursor.range();
|
||||
|
||||
self.contents.drain(
|
||||
(range.start() - bytes_deleted_so_far)..=(range.end() - bytes_deleted_so_far)
|
||||
);
|
||||
|
||||
// RangeInclusive<usize>::len() is unstable/nonexistant :/
|
||||
bytes_deleted_so_far += range.end() - range.start() + 1;
|
||||
}
|
||||
|
||||
fn undo_delete_at(&mut self, cursor: Cursor, old_data: &[u8]) {
|
||||
let cursor_start = min(cursor.head, cursor.tail);
|
||||
self.primary_cursor.head = min(
|
||||
min(primary_cursor.head, primary_cursor.tail),
|
||||
self.max_contents_index()
|
||||
);
|
||||
self.primary_cursor.collapse();
|
||||
|
||||
self.cursors = cursors
|
||||
.iter()
|
||||
.map(|cursor| Cursor::at(min(cursor.lower_bound(), self.max_contents_index())))
|
||||
.collect();
|
||||
|
||||
self.combine_cursors_if_overlapping();
|
||||
}
|
||||
|
||||
fn undo_delete_at(
|
||||
&mut self,
|
||||
primary_cursor: Cursor,
|
||||
cursors: &[Cursor],
|
||||
primary_old_data: &[u8],
|
||||
old_data: &[Vec<u8>]
|
||||
) {
|
||||
let primary_cursor_start = primary_cursor.lower_bound();
|
||||
|
||||
self.contents.splice(
|
||||
primary_cursor_start..primary_cursor_start,
|
||||
primary_old_data.iter().copied()
|
||||
);
|
||||
|
||||
for (cursor, old_data) in cursors.iter().zip(old_data) {
|
||||
let cursor_start = cursor.lower_bound();
|
||||
|
||||
self.contents.splice(
|
||||
cursor_start..cursor_start,
|
||||
old_data.iter().copied()
|
||||
);
|
||||
|
||||
self.cursor = cursor;
|
||||
}
|
||||
|
||||
fn replace_at_with(&mut self, cursor: Cursor, new_byte: u8) {
|
||||
self.primary_cursor = primary_cursor;
|
||||
self.cursors = cursors.to_vec();
|
||||
}
|
||||
|
||||
fn replace_at_with(
|
||||
&mut self,
|
||||
primary_cursor: Cursor,
|
||||
cursors: &[Cursor],
|
||||
new_byte: u8
|
||||
) {
|
||||
self.contents[primary_cursor.range()].fill(new_byte);
|
||||
|
||||
for cursor in cursors {
|
||||
self.contents[cursor.range()].fill(new_byte);
|
||||
|
||||
self.cursor = cursor;
|
||||
}
|
||||
|
||||
fn undo_replace_at_with(&mut self, cursor: Cursor, old_data: &[u8]) {
|
||||
self.primary_cursor = primary_cursor;
|
||||
self.cursors = cursors.to_vec();
|
||||
}
|
||||
|
||||
fn undo_replace_at_with(
|
||||
&mut self,
|
||||
primary_cursor: Cursor,
|
||||
cursors: &[Cursor],
|
||||
primary_old_data: &[u8],
|
||||
old_data: &[Vec<u8>]
|
||||
) {
|
||||
self.contents.splice(
|
||||
primary_cursor.range(),
|
||||
primary_old_data.iter().copied()
|
||||
);
|
||||
|
||||
for (cursor, old_data) in cursors.iter().zip(old_data) {
|
||||
self.contents.splice(
|
||||
cursor.range(),
|
||||
old_data.iter().copied()
|
||||
);
|
||||
}
|
||||
|
||||
self.cursor = cursor;
|
||||
self.primary_cursor = primary_cursor;
|
||||
self.cursors = cursors.to_vec();
|
||||
}
|
||||
}
|
||||
|
||||
fn cursors_in_order(
|
||||
primary_cursor: Cursor,
|
||||
cursors: &[Cursor]
|
||||
) -> impl Iterator<Item=Cursor> {
|
||||
let primary_cursor_index = cursors
|
||||
.binary_search_by_key(&primary_cursor.head, |cursor| cursor.head)
|
||||
.unwrap_or_else(identity);
|
||||
|
||||
cursors.iter()
|
||||
.copied()
|
||||
.take(primary_cursor_index)
|
||||
.chain(iter::once(primary_cursor))
|
||||
.chain(cursors.iter().copied().skip(primary_cursor_index))
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#![warn(clippy::pedantic, clippy::nursery)]
|
||||
#![allow(clippy::cast_possible_truncation)]
|
||||
#![feature(get_disjoint_mut_helpers)]
|
||||
|
||||
use app::App;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user