diff --git a/src/action.rs b/src/action.rs index 8c23f83..daea92f 100644 --- a/src/action.rs +++ b/src/action.rs @@ -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(); - - self.clamp_screen_to_cursor(window_size); + // 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(); + + 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(); - - self.clamp_screen_to_cursor(window_size); + fn move_byte_down(&mut self, window_size: WindowSize) { + let max_contents_index = self.max_contents_index(); + + 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(); - - self.clamp_screen_to_cursor(window_size); + fn move_byte_left(&mut self, window_size: WindowSize) { + self.primary_cursor.move_byte_left(); + + 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(); - - self.clamp_screen_to_cursor(window_size); + fn move_byte_right(&mut self, window_size: WindowSize) { + let max_contents_index = self.max_contents_index(); + + 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(); - } - - 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); - } - - 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); + let max_contents_index = self.max_contents_index(); - self.cursor.collapse(); - self.clamp_screen_to_cursor(window_size); + 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(); + } + + 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); + } + + fn goto_file_end(&mut self, window_size: WindowSize) { + let max_contents_index = self.max_contents_index(); + + 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); - } - - const fn extend_previous_word_start(&mut self, window_size: WindowSize) { - self.cursor.extend_to_previous_beginning(); - self.clamp_screen_to_cursor(window_size); - } - - const fn collapse_selection(&mut self) { - self.cursor.collapse(); - } - - fn extend_line_below(&mut self) { - if self.cursor.tail > self.cursor.head { - swap(&mut self.cursor.head, &mut self.cursor.tail); - } + let max_contents_index = self.max_contents_index(); - 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); + 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); + } + + 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); + } + + fn collapse_selection(&mut self) { + self.primary_cursor.collapse(); + + 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(); - 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); + 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); + } + + 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) - } -} diff --git a/src/buffer/mod.rs b/src/buffer/mod.rs index 1cb6004..4eb5ec6 100644 --- a/src/buffer/mod.rs +++ b/src/buffer/mod.rs @@ -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, pub scroll_position: usize, - pub cursor: Cursor, + pub primary_cursor: Cursor, + pub cursors: Vec, pub mode: Mode, pub partial_action: Option, @@ -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 { diff --git a/src/buffer/widget.rs b/src/buffer/widget.rs index 369f7da..1c1422d 100644 --- a/src/buffer/widget.rs +++ b/src/buffer/widget.rs @@ -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() } diff --git a/src/cursor.rs b/src/cursor.rs index 444028b..ebb3e1f 100644 --- a/src/cursor.rs +++ b/src/cursor.rs @@ -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 { + self.lower_bound()..=self.upper_bound() + } + pub const fn contains(&self, index: usize) -> Option { 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 { + 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; } } -} - -mod tests { - use crate::cursor::Cursor; - impl Cursor { - #[allow(dead_code)] - const fn at(index: usize) -> Self { - Self { head: index, tail: index } + 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; + #[test] fn next_word() { // [a]bcd efgh -> abcd [e]fgh diff --git a/src/edit_action.rs b/src/edit_action.rs index b8ee973..29494ec 100644 --- a/src/edit_action.rs +++ b/src/edit_action.rs @@ -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 + primary_cursor: Cursor, + cursors: Vec, + + primary_old_data: Vec, + old_data: Vec> }, Replace { - cursor: Cursor, - old_data: Vec, + primary_cursor: Cursor, + cursors: Vec, + + primary_old_data: Vec, + old_data: Vec>, + new_byte: u8 }, // Insert { - // cursor: Cursor, + // primary_cursor: Cursor, + // cursors: Vec, // which side of cursor? append/insert // new_data: Vec // } @@ -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::len() is unstable/nonexistant :/ + bytes_deleted_so_far += range.end() - range.start() + 1; + } + + 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, cursor: Cursor, old_data: &[u8]) { - let cursor_start = min(cursor.head, cursor.tail); + fn undo_delete_at( + &mut self, + primary_cursor: Cursor, + cursors: &[Cursor], + primary_old_data: &[u8], + old_data: &[Vec] + ) { + let primary_cursor_start = primary_cursor.lower_bound(); self.contents.splice( - cursor_start..cursor_start, - old_data.iter().copied() + primary_cursor_start..primary_cursor_start, + primary_old_data.iter().copied() ); - self.cursor = cursor; - } - - fn replace_at_with(&mut self, cursor: Cursor, new_byte: u8) { - self.contents[cursor.range()].fill(new_byte); + 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; + self.primary_cursor = primary_cursor; + self.cursors = cursors.to_vec(); } - fn undo_replace_at_with(&mut self, cursor: Cursor, old_data: &[u8]) { + 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.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] + ) { self.contents.splice( - cursor.range(), - old_data.iter().copied() + primary_cursor.range(), + primary_old_data.iter().copied() ); - self.cursor = cursor; + for (cursor, old_data) in cursors.iter().zip(old_data) { + self.contents.splice( + cursor.range(), + old_data.iter().copied() + ); + } + + self.primary_cursor = primary_cursor; + self.cursors = cursors.to_vec(); } } + +fn cursors_in_order( + primary_cursor: Cursor, + cursors: &[Cursor] +) -> impl Iterator { + 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)) +} diff --git a/src/main.rs b/src/main.rs index c6a5f75..cb0f17e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ #![warn(clippy::pedantic, clippy::nursery)] #![allow(clippy::cast_possible_truncation)] +#![feature(get_disjoint_mut_helpers)] use app::App;