multi-cursor actions

This commit is contained in:
alice pellerin
2026-03-20 23:44:10 -05:00
parent 9906e76ab5
commit f81d220d93
4 changed files with 114 additions and 17 deletions
+80 -2
View File
@@ -1,5 +1,5 @@
use std::{cmp::min, fs::File, io::Write, mem::replace}; use std::{cmp::min, convert::identity, fs::File, io::Write, iter, mem::{replace, swap}};
use crate::{BYTES_PER_LINE, app::WindowSize, buffer::{Buffer, Mode, PartialAction}, edit_action::EditAction}; use crate::{BYTES_PER_LINE, app::WindowSize, buffer::{Buffer, Mode, PartialAction}, cursor::Cursor, edit_action::EditAction};
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub enum Action { pub enum Action {
@@ -60,6 +60,13 @@ pub enum Action {
PreviousBuffer, PreviousBuffer,
NextBuffer, NextBuffer,
CopySelectionOnNextLine,
RotateSelectionsBackward,
RotateSelectionsForward,
KeepPrimarySelection,
} }
// actions that act on the app as a whole, not just one buffer // actions that act on the app as a whole, not just one buffer
@@ -131,6 +138,13 @@ impl Buffer {
Action::PreviousBuffer => return Some(AppAction::PreviousBuffer), Action::PreviousBuffer => return Some(AppAction::PreviousBuffer),
Action::NextBuffer => return Some(AppAction::NextBuffer), Action::NextBuffer => return Some(AppAction::NextBuffer),
Action::CopySelectionOnNextLine => self.copy_selection_on_next_line(),
Action::RotateSelectionsBackward => self.rotate_selections_backward(),
Action::RotateSelectionsForward => self.rotate_selections_forward(),
Action::KeepPrimarySelection => self.keep_primary_selection(),
} }
None None
@@ -596,6 +610,70 @@ impl Buffer {
self.time_traveling.unwrap_or(self.edit_history.len()) self.time_traveling.unwrap_or(self.edit_history.len())
); );
} }
fn copy_selection_on_next_line(&mut self) {
let new_cursors: Vec<Cursor> = iter::once(&self.primary_cursor)
.chain(&self.cursors)
.filter_map(|cursor| {
let number_of_lines_tall = (cursor.upper_bound() - cursor.lower_bound()) / BYTES_PER_LINE;
let offset_to_add = (number_of_lines_tall + 1) * BYTES_PER_LINE;
if cursor.lower_bound() + offset_to_add < self.contents.len() {
Some(
Cursor {
head: min(cursor.head + offset_to_add, self.max_contents_index()),
tail: min(cursor.tail + offset_to_add, self.max_contents_index())
}
)
} else {
None
}
})
.collect();
self.cursors.extend(new_cursors);
self.cursors.sort_by_key(|cursor| cursor.head);
self.combine_cursors_if_overlapping();
}
fn rotate_selections_backward(&mut self) {
if self.cursors.is_empty() { return; }
let next_cursor_index = self.cursors
.binary_search_by_key(&self.primary_cursor.head, |cursor| cursor.head)
.unwrap_or_else(identity);
if next_cursor_index == 0 {
let cursor_count = self.cursors.len();
swap(&mut self.primary_cursor, &mut self.cursors[cursor_count - 1]);
self.cursors.sort_by_key(|cursor| cursor.head);
} else {
swap(&mut self.primary_cursor, &mut self.cursors[next_cursor_index - 1]);
}
}
fn rotate_selections_forward(&mut self) {
if self.cursors.is_empty() { return; }
let next_cursor_index = self.cursors
.binary_search_by_key(&self.primary_cursor.head, |cursor| cursor.head)
.unwrap_or_else(identity);
if next_cursor_index == self.cursors.len() {
swap(&mut self.primary_cursor, &mut self.cursors[0]);
self.cursors.sort_by_key(|cursor| cursor.head);
} else {
swap(&mut self.primary_cursor, &mut self.cursors[next_cursor_index]);
}
}
fn keep_primary_selection(&mut self) {
self.cursors.clear();
}
} }
// helpers // helpers
+1 -3
View File
@@ -171,11 +171,9 @@ impl Buffer {
} }
pub fn combine_cursors_if_overlapping(&mut self) { pub fn combine_cursors_if_overlapping(&mut self) {
if self.cursors.is_empty() { return; }
let mut index = 0; let mut index = 0;
while index < self.cursors.len() - 1 { while !self.cursors.is_empty() && index < self.cursors.len() - 1 {
while index < self.cursors.len() - 1 && while index < self.cursors.len() - 1 &&
self.cursors[index].range().is_overlapping( self.cursors[index].range().is_overlapping(
&self.cursors[index + 1].range()) &self.cursors[index + 1].range())
+19 -12
View File
@@ -221,18 +221,25 @@ mod hex {
let span = SPAN_FOR_BYTE[byte as usize].clone(); let span = SPAN_FOR_BYTE[byte as usize].clone();
let head_color = match self.mode { if let Some(place_in_cursor) = self.primary_cursor.contains(address) {
Mode::Select => Color::Yellow, let head_color = match self.mode {
_ => Color::Gray Mode::Select => Color::Yellow,
}; _ => Color::Gray
};
match iter::once(&self.primary_cursor)
.chain(&self.cursors) match place_in_cursor {
.find_map(|cursor| cursor.contains(address)) InCursor::Head => span.bg(head_color),
{ InCursor::Rest => span.bg(Color::select_grey()),
Some(InCursor::Head) => span.bg(head_color), }
Some(InCursor::Rest) => span.bg(Color::select_grey()), } else {
None => span, match self.cursors
.iter()
.find_map(|cursor| cursor.contains(address))
{
Some(InCursor::Head) => span.on_gray(),
Some(InCursor::Rest) => span.bg(Color::select_grey()),
None => span,
}
} }
} }
+14
View File
@@ -141,6 +141,13 @@ impl Default for Config {
("C-j".try_into().unwrap(), Action::PreviousBuffer), ("C-j".try_into().unwrap(), Action::PreviousBuffer),
("C-l".try_into().unwrap(), Action::NextBuffer), ("C-l".try_into().unwrap(), Action::NextBuffer),
("C".try_into().unwrap(), Action::CopySelectionOnNextLine),
("(".try_into().unwrap(), Action::RotateSelectionsBackward),
(")".try_into().unwrap(), Action::RotateSelectionsForward),
(",".try_into().unwrap(), Action::KeepPrimarySelection),
].into()), ].into()),
(Some(PartialAction::Goto), [ (Some(PartialAction::Goto), [
("j".try_into().unwrap(), Action::GotoLineStart), ("j".try_into().unwrap(), Action::GotoLineStart),
@@ -191,6 +198,13 @@ impl Default for Config {
("u".try_into().unwrap(), Action::Undo), ("u".try_into().unwrap(), Action::Undo),
("U".try_into().unwrap(), Action::Redo), ("U".try_into().unwrap(), Action::Redo),
("C".try_into().unwrap(), Action::CopySelectionOnNextLine),
("(".try_into().unwrap(), Action::RotateSelectionsBackward),
(")".try_into().unwrap(), Action::RotateSelectionsForward),
(",".try_into().unwrap(), Action::KeepPrimarySelection),
].into()), ].into()),
(Some(PartialAction::Space), [ (Some(PartialAction::Space), [
("w".try_into().unwrap(), Action::Save), ("w".try_into().unwrap(), Action::Save),