244 lines
7.5 KiB
Rust
244 lines
7.5 KiB
Rust
use std::ops::Range;
|
|
|
|
use super::buffer::Buffer;
|
|
|
|
/// A cursor and the text it holds: `head` moves, `anchor` stays; `goal` keeps the column a vertical move aims for.
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub struct Selection {
|
|
pub anchor: usize,
|
|
pub head: usize,
|
|
pub goal: Option<usize>,
|
|
}
|
|
|
|
impl Selection {
|
|
pub fn caret(at: usize) -> Self {
|
|
Self {
|
|
anchor: at,
|
|
head: at,
|
|
goal: None,
|
|
}
|
|
}
|
|
|
|
pub fn range(&self) -> Range<usize> {
|
|
self.anchor.min(self.head)..self.anchor.max(self.head)
|
|
}
|
|
|
|
pub fn is_empty(&self) -> bool {
|
|
self.anchor == self.head
|
|
}
|
|
|
|
/// Moves the head, keeping the anchor when `extend`, or collapsing onto the head.
|
|
fn to(self, head: usize, extend: bool) -> Self {
|
|
Self {
|
|
anchor: if extend { self.anchor } else { head },
|
|
head,
|
|
goal: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Sorted by where they start, overlapping selections joined into one; the last given, the primary, stays last.
|
|
pub(crate) fn merged(mut all: Vec<Selection>) -> Vec<Selection> {
|
|
let primary = all
|
|
.last()
|
|
.expect("an editor keeps at least one cursor")
|
|
.range();
|
|
all.sort_by_key(|selection| selection.range().start);
|
|
let mut out: Vec<Selection> = Vec::with_capacity(all.len());
|
|
for next in all {
|
|
match out.last_mut() {
|
|
Some(last)
|
|
if next.range().start < last.range().end
|
|
|| (next.range() == last.range() && next.is_empty()) =>
|
|
{
|
|
let range = last.range().start..last.range().end.max(next.range().end);
|
|
let forward = last.head >= last.anchor;
|
|
*last = Selection {
|
|
anchor: if forward { range.start } else { range.end },
|
|
head: if forward { range.end } else { range.start },
|
|
goal: None,
|
|
};
|
|
}
|
|
_ => out.push(next),
|
|
}
|
|
}
|
|
let lead = out
|
|
.iter()
|
|
.position(|selection| {
|
|
let range = selection.range();
|
|
range.start <= primary.start && primary.end <= range.end
|
|
})
|
|
.expect("a merge keeps every cursor inside one");
|
|
let lead = out.remove(lead);
|
|
out.push(lead);
|
|
out
|
|
}
|
|
|
|
/// Where a cursor goes.
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub(crate) enum Motion {
|
|
Left,
|
|
Right,
|
|
Up,
|
|
Down,
|
|
WordLeft,
|
|
WordRight,
|
|
LineStart,
|
|
LineEnd,
|
|
Start,
|
|
End,
|
|
/// Down or up a page of this many lines.
|
|
Page(isize),
|
|
}
|
|
|
|
/// A selection after a motion. Left and Right close a selection to its edge first.
|
|
pub(crate) fn moved(
|
|
buffer: &Buffer,
|
|
selection: Selection,
|
|
motion: Motion,
|
|
extend: bool,
|
|
) -> Selection {
|
|
let head = selection.head;
|
|
if !extend && !selection.is_empty() {
|
|
match motion {
|
|
Motion::Left => return Selection::caret(selection.range().start),
|
|
Motion::Right => return Selection::caret(selection.range().end),
|
|
_ => {}
|
|
}
|
|
}
|
|
let vertical = |lines: isize| {
|
|
let (line, column) = buffer.point(head);
|
|
let goal = selection.goal.unwrap_or(column);
|
|
let target = line as isize + lines;
|
|
let offset = if target < 0 {
|
|
0
|
|
} else if target >= buffer.lines() as isize {
|
|
buffer.len()
|
|
} else {
|
|
buffer.offset(target as usize, goal)
|
|
};
|
|
Selection {
|
|
goal: Some(goal),
|
|
..selection.to(offset, extend)
|
|
}
|
|
};
|
|
match motion {
|
|
Motion::Left => selection.to(buffer.previous(head), extend),
|
|
Motion::Right => selection.to(buffer.next(head), extend),
|
|
Motion::Up => vertical(-1),
|
|
Motion::Down => vertical(1),
|
|
Motion::Page(lines) => vertical(lines),
|
|
Motion::WordLeft => selection.to(buffer.word_start(head), extend),
|
|
Motion::WordRight => selection.to(buffer.word_end(head), extend),
|
|
Motion::LineStart => {
|
|
let line = buffer.line_of(head);
|
|
let start = buffer.line_range(line).start;
|
|
let text = buffer.line_range(line).start + buffer.indent(line);
|
|
selection.to(if head == text { start } else { text }, extend)
|
|
}
|
|
Motion::LineEnd => selection.to(buffer.line_range(buffer.line_of(head)).end, extend),
|
|
Motion::Start => selection.to(0, extend),
|
|
Motion::End => selection.to(buffer.len(), extend),
|
|
}
|
|
}
|
|
|
|
/// Replaces each selection's range with its text, all at once, and returns a caret after each insert.
|
|
pub(crate) fn replace_each(
|
|
buffer: &mut Buffer,
|
|
edits: Vec<(Range<usize>, String)>,
|
|
) -> Vec<Selection> {
|
|
let mut order: Vec<usize> = (0..edits.len()).collect();
|
|
order.sort_by_key(|ix| edits[*ix].0.start);
|
|
assert!(
|
|
order
|
|
.windows(2)
|
|
.all(|pair| edits[pair[0]].0.end <= edits[pair[1]].0.start),
|
|
"edits do not overlap"
|
|
);
|
|
let mut carets = vec![Selection::caret(0); edits.len()];
|
|
let mut shift: isize = 0;
|
|
for ix in &order {
|
|
let (range, text) = &edits[*ix];
|
|
let start = (range.start as isize + shift) as usize;
|
|
carets[*ix] = Selection::caret(start + text.len());
|
|
shift += text.len() as isize - range.len() as isize;
|
|
}
|
|
for ix in order.iter().rev() {
|
|
let (range, text) = &edits[*ix];
|
|
buffer.replace(range.clone(), text);
|
|
}
|
|
carets
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn overlapping_selections_join_and_carets_on_one_spot_become_one() {
|
|
let joined = merged(vec![
|
|
Selection {
|
|
anchor: 4,
|
|
head: 8,
|
|
goal: None,
|
|
},
|
|
Selection {
|
|
anchor: 6,
|
|
head: 10,
|
|
goal: None,
|
|
},
|
|
Selection::caret(12),
|
|
Selection::caret(12),
|
|
]);
|
|
assert_eq!(joined.len(), 2);
|
|
assert_eq!(joined[0].range(), 4..10);
|
|
assert_eq!(joined[1], Selection::caret(12));
|
|
}
|
|
|
|
#[test]
|
|
fn vertical_moves_keep_their_column_through_short_lines() {
|
|
let buffer = Buffer::new("abcdef\nab\nabcdef");
|
|
let start = Selection::caret(5);
|
|
let down = moved(&buffer, start, Motion::Down, false);
|
|
assert_eq!(
|
|
buffer.point(down.head),
|
|
(1, 2),
|
|
"held to the short line's end"
|
|
);
|
|
let again = moved(&buffer, down, Motion::Down, false);
|
|
assert_eq!(
|
|
buffer.point(again.head),
|
|
(2, 5),
|
|
"back to the column it aimed for"
|
|
);
|
|
let selected = moved(&buffer, start, Motion::End, true);
|
|
assert_eq!(selected.range(), 5..buffer.len());
|
|
assert_eq!(
|
|
moved(&buffer, selected, Motion::Left, false),
|
|
Selection::caret(5)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn home_toggles_between_the_text_and_the_line_start() {
|
|
let buffer = Buffer::new(" let x;");
|
|
let first = moved(&buffer, Selection::caret(9), Motion::LineStart, false);
|
|
assert_eq!(first.head, 4);
|
|
assert_eq!(moved(&buffer, first, Motion::LineStart, false).head, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn edits_at_many_cursors_land_where_each_one_was() {
|
|
let mut buffer = Buffer::new("a1\nb2\nc3");
|
|
let carets = replace_each(
|
|
&mut buffer,
|
|
vec![(7..7, "!".into()), (1..1, "!".into()), (4..4, "!".into())],
|
|
);
|
|
assert_eq!(buffer.text(), "a!1\nb!2\nc!3");
|
|
assert_eq!(
|
|
carets.iter().map(|caret| caret.head).collect::<Vec<_>>(),
|
|
[10, 2, 6]
|
|
);
|
|
}
|
|
}
|