pub mod word;
use crate::editor::document::{Document, indent};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default, Hash)]
pub struct Position {
pub line: usize,
pub col: usize,
}
impl Position {
pub const ZERO: Self = Self { line: 0, col: 0 };
#[must_use]
pub const fn new(line: usize, col: usize) -> Self {
Self { line, col }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Cursor {
pub head: Position,
pub anchor: Position,
goal_col: Option<usize>,
}
impl Cursor {
#[must_use]
pub const fn at(pos: Position) -> Self {
Self {
head: pos,
anchor: pos,
goal_col: None,
}
}
pub fn collapse(&mut self) {
self.anchor = self.head;
}
pub fn anchor_here(&mut self) {
self.anchor = self.head;
}
pub fn move_to(&mut self, pos: Position, extend: bool) {
self.head = pos;
self.goal_col = None;
if !extend {
self.anchor = pos;
}
}
#[must_use]
pub fn goal_col(&self) -> usize {
self.goal_col.unwrap_or(self.head.col)
}
pub fn set_goal_col(&mut self, col: usize) {
self.goal_col = Some(col);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Motion {
Left,
Right,
Up(usize),
Down(usize),
WordForward,
WordBackward,
WordEnd,
LineStart,
LineFirstNonBlank,
LineEnd,
DocStart,
DocEnd,
ToLine(usize),
}
impl Cursor {
pub fn apply(&mut self, motion: Motion, doc: &Document, extend: bool, allow_eol: bool) {
match motion {
Motion::Up(n) => self.move_vertical(doc, n, true, extend, allow_eol),
Motion::Down(n) => self.move_vertical(doc, n, false, extend, allow_eol),
Motion::Left => {
let target = self.left_of(doc);
self.move_to(doc.clamp(target, allow_eol), extend);
}
Motion::Right => {
let target = self.right_of(doc, allow_eol);
self.move_to(doc.clamp(target, allow_eol), extend);
}
Motion::WordForward => {
let target = word::next_word_start(doc, self.head);
self.move_to(doc.clamp(target, allow_eol), extend);
}
Motion::WordBackward => {
let target = word::prev_word_start(doc, self.head);
self.move_to(doc.clamp(target, allow_eol), extend);
}
Motion::WordEnd => {
let target = word::word_end(doc, self.head);
self.move_to(doc.clamp(target, allow_eol), extend);
}
Motion::LineStart => {
self.move_to(Position::new(self.head.line, 0), extend);
}
Motion::LineFirstNonBlank => {
let col = indent::first_non_blank(doc, self.head.line);
let target = Position::new(self.head.line, col);
self.move_to(doc.clamp(target, allow_eol), extend);
}
Motion::LineEnd => {
let target = Position::new(self.head.line, usize::MAX);
self.move_to(doc.clamp(target, allow_eol), extend);
}
Motion::DocStart => self.move_to(Position::ZERO, extend),
Motion::DocEnd => {
let target = Position::new(doc.last_line(), usize::MAX);
self.move_to(doc.clamp(target, allow_eol), extend);
}
Motion::ToLine(line) => {
let line = line.min(doc.last_line());
let target = Position::new(line, indent::first_non_blank(doc, line));
self.move_to(doc.clamp(target, allow_eol), extend);
}
}
}
fn move_vertical(
&mut self,
doc: &Document,
count: usize,
up: bool,
extend: bool,
allow_eol: bool,
) {
let goal = self.goal_col();
let line = if up {
self.head.line.saturating_sub(count)
} else {
self.head.line.saturating_add(count).min(doc.last_line())
};
self.head = doc.clamp(Position::new(line, goal), allow_eol);
if !extend {
self.anchor = self.head;
}
self.set_goal_col(goal);
}
fn left_of(&self, doc: &Document) -> Position {
if self.head.col > 0 {
Position::new(self.head.line, self.head.col - 1)
} else if self.head.line > 0 {
let line = self.head.line - 1;
Position::new(line, doc.line_len(line))
} else {
self.head
}
}
fn right_of(&self, doc: &Document, allow_eol: bool) -> Position {
let max_col = doc
.clamp(Position::new(self.head.line, usize::MAX), allow_eol)
.col;
if self.head.col < max_col {
Position::new(self.head.line, self.head.col + 1)
} else if self.head.line < doc.last_line() {
Position::new(self.head.line + 1, 0)
} else {
self.head
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn doc(text: &str) -> Document {
Document::from_text(text, None)
}
fn moved(text: &str, from: Position, motion: Motion) -> Position {
let document = doc(text);
let mut cursor = Cursor::at(from);
cursor.apply(motion, &document, false, false);
cursor.head
}
#[test]
fn horizontal_movement_wraps_between_lines() {
assert_eq!(
moved("ab\ncd", Position::new(0, 1), Motion::Right),
Position::new(1, 0)
);
assert_eq!(
moved("ab\ncd", Position::new(1, 0), Motion::Left),
Position::new(0, 1)
);
}
#[test]
fn wrapping_left_in_insert_mode_lands_past_the_last_character() {
let document = doc("ab\ncd");
let mut cursor = Cursor::at(Position::new(1, 0));
cursor.apply(Motion::Left, &document, false, true);
assert_eq!(cursor.head, Position::new(0, 2));
}
#[test]
fn movement_stops_at_the_document_edges() {
assert_eq!(
moved("ab", Position::new(0, 0), Motion::Left),
Position::new(0, 0)
);
assert_eq!(
moved("ab", Position::new(0, 1), Motion::Right),
Position::new(0, 1)
);
}
#[test]
fn goal_column_survives_a_short_line() {
let document = doc("abcdef\nx\nabcdef");
let mut cursor = Cursor::at(Position::new(0, 5));
cursor.apply(Motion::Down(1), &document, false, false);
assert_eq!(cursor.head, Position::new(1, 0));
cursor.apply(Motion::Down(1), &document, false, false);
assert_eq!(cursor.head, Position::new(2, 5));
}
#[test]
fn horizontal_movement_resets_the_goal_column() {
let document = doc("abcdef\nx\nabcdef");
let mut cursor = Cursor::at(Position::new(0, 5));
cursor.apply(Motion::Down(1), &document, false, false);
cursor.apply(Motion::Left, &document, false, false);
assert_eq!(cursor.goal_col, None);
}
#[test]
fn extending_keeps_the_anchor_in_place() {
let document = doc("abcdef");
let mut cursor = Cursor::at(Position::new(0, 1));
cursor.apply(Motion::Right, &document, true, false);
assert_eq!(cursor.anchor, Position::new(0, 1));
assert_ne!(cursor.head, cursor.anchor);
}
#[test]
fn word_motions_walk_over_runs() {
let text = "let mut x = 1;";
assert_eq!(
moved(text, Position::new(0, 0), Motion::WordForward),
Position::new(0, 4)
);
assert_eq!(
moved(text, Position::new(0, 5), Motion::WordBackward),
Position::new(0, 4)
);
assert_eq!(
moved(text, Position::new(0, 0), Motion::WordEnd),
Position::new(0, 2)
);
}
#[test]
fn line_motions_use_the_first_non_blank() {
let text = " indented";
assert_eq!(
moved(text, Position::new(0, 8), Motion::LineFirstNonBlank),
Position::new(0, 4)
);
assert_eq!(
moved(text, Position::new(0, 8), Motion::LineStart),
Position::new(0, 0)
);
assert_eq!(
moved(text, Position::new(0, 0), Motion::LineEnd),
Position::new(0, 11)
);
}
}