use crate::buffer::Buffer;
use crate::coords::{Bias, Point};
use crate::display_map::{self, BufferRow, DisplayRow};
use crate::fold_map::FoldMap;
use crate::selection::SelectionSet;
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum Motion {
Left,
Right,
Up,
Down,
PageUp(u32),
PageDown(u32),
WordLeft,
WordRight,
LineStart,
LineEnd,
DocStart,
DocEnd,
}
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum Granularity {
Char,
Word,
Line,
}
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum ColumnDir {
Up,
Down,
Left,
Right,
}
pub fn move_selections(set: &mut SelectionSet, buffer: &Buffer, folds: &FoldMap, tab: u32, motion: Motion, extend: bool) {
set.map_each(|s| {
let (target, goal) = motion_target(buffer, folds, tab, s.head(), s.goal, motion);
if extend {
s.set_head(target);
s.goal = goal;
} else if !s.is_empty() && matches!(motion, Motion::Left | Motion::Right) {
let edge = if motion == Motion::Left { s.start() } else { s.end() };
s.move_to_caret(edge);
} else {
s.move_to_caret(target);
s.goal = goal;
}
});
}
fn motion_target(buffer: &Buffer, folds: &FoldMap, tab: u32, head: u32, goal: Option<u32>, motion: Motion) -> (u32, Option<u32>) {
match motion {
Motion::Left => (char_left(buffer, folds, head), None),
Motion::Right => (char_right(buffer, folds, head), None),
Motion::Up => vertical_by(buffer, folds, tab, head, goal, -1),
Motion::Down => vertical_by(buffer, folds, tab, head, goal, 1),
Motion::PageUp(rows) => vertical_by(buffer, folds, tab, head, goal, -(rows as i32)),
Motion::PageDown(rows) => vertical_by(buffer, folds, tab, head, goal, rows as i32),
Motion::WordLeft => (skip_fold_left(buffer, folds, word_left(buffer, head)), None),
Motion::WordRight => (skip_fold_right(buffer, folds, word_right(buffer, head)), None),
Motion::LineStart => (line_start_smart_folded(buffer, folds, head), None),
Motion::LineEnd => (line_end_folded(buffer, folds, head), None),
Motion::DocStart => (0, None),
Motion::DocEnd => (buffer.len(), None),
}
}
fn char_left(buffer: &Buffer, folds: &FoldMap, offset: u32) -> u32 {
if offset == 0 {
return 0;
}
let prev = buffer.char_before(offset).expect("offset > 0");
skip_fold_left(buffer, folds, offset - prev.len_utf8() as u32)
}
fn char_right(buffer: &Buffer, folds: &FoldMap, offset: u32) -> u32 {
if offset >= buffer.len() {
return buffer.len();
}
let next = buffer.char_at(offset).expect("offset < len");
skip_fold_right(buffer, folds, offset + next.len_utf8() as u32)
}
fn hidden_fold(buffer: &Buffer, folds: &FoldMap, off: u32) -> Option<(u32, u32)> {
let p = buffer.offset_to_point(off);
let (header, last) = folds.fold_containing(BufferRow(p.row))?;
if p.row == last.0 && p.col >= crate::row_layout::tail_start_col(&buffer.line(last.0)) {
return None;
}
Some((header.0, last.0))
}
fn inline_hidden(folds: &FoldMap, off: u32) -> Option<crate::fold_map::InlineFold> {
folds.inline_fold_before(off).filter(|f| f.hides_caret_at(off))
}
fn skip_fold_left(buffer: &Buffer, folds: &FoldMap, off: u32) -> u32 {
if let Some((header, _)) = hidden_fold(buffer, folds, off) {
return buffer.point_to_offset(Point::new(header, buffer.line_len(header)));
}
inline_hidden(folds, off).map_or(off, |f| f.left_edge())
}
fn skip_fold_right(buffer: &Buffer, folds: &FoldMap, off: u32) -> u32 {
if let Some((_, last)) = hidden_fold(buffer, folds, off) {
return buffer.point_to_offset(Point::new(last, crate::row_layout::tail_start_col(&buffer.line(last))));
}
inline_hidden(folds, off).map_or(off, |f| f.right_edge())
}
fn vertical_by(buffer: &Buffer, folds: &FoldMap, tab: u32, offset: u32, goal: Option<u32>, delta: i32) -> (u32, Option<u32>) {
let (row, cell) = match folds.display_position(buffer, offset, tab) {
Some(p) => (p.row, p.x.cells()),
None => {
let p = buffer.offset_to_point(offset);
(folds.to_display_row(BufferRow(p.row)), display_map::expand(&buffer.line(p.row), p.col, tab) as f32)
}
};
let goal_cell = goal.unwrap_or_else(|| cell.round() as u32);
let target = row.0 as i32 + delta;
if target < 0 {
return (0, Some(goal_cell)); }
let last_display = folds.display_row_count().saturating_sub(1) as i32;
if target > last_display {
return (buffer.len(), Some(goal_cell)); }
let new_row = folds.to_buffer_row(DisplayRow(target as u32));
let off = folds.hit_row(buffer, new_row, goal_cell as f32, Bias::Left, tab);
(off, Some(goal_cell))
}
pub(crate) fn caret_one_display_row(
buffer: &Buffer,
folds: &FoldMap,
tab: u32,
offset: u32,
delta: i32,
) -> Option<u32> {
let (off, _) = vertical_by(buffer, folds, tab, offset, None, delta);
let row_of = |o: u32| folds.to_display_row(BufferRow(buffer.offset_to_point(o).row));
(row_of(off) != row_of(offset)).then_some(off)
}
fn line_end(buffer: &Buffer, offset: u32) -> u32 {
let row = buffer.offset_to_point(offset).row;
buffer.point_to_offset(Point::new(row, buffer.line_len(row)))
}
fn line_end_folded(buffer: &Buffer, folds: &FoldMap, offset: u32) -> u32 {
let row = buffer.offset_to_point(offset).row;
if let Some(last) = folds.fold_at_header(BufferRow(row)) {
return buffer.point_to_offset(Point::new(last.0, buffer.line_len(last.0)));
}
line_end(buffer, offset)
}
fn line_start_smart(buffer: &Buffer, offset: u32) -> u32 {
let p = buffer.offset_to_point(offset);
line_start_smart_on(buffer, p.row, p.col)
}
fn line_start_smart_on(buffer: &Buffer, row: u32, col: u32) -> u32 {
let line = buffer.line(row);
let indent = crate::row_layout::tail_start_col(&line);
let line_start = buffer.point_to_offset(Point::new(row, 0));
if col == indent {
line_start } else {
line_start + indent
}
}
fn line_start_smart_folded(buffer: &Buffer, folds: &FoldMap, offset: u32) -> u32 {
let p = buffer.offset_to_point(offset);
if let Some(hdr) = folds.header_of_tail(BufferRow(p.row)) {
return line_start_smart_on(buffer, hdr.0, u32::MAX);
}
line_start_smart(buffer, offset)
}
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
enum CharKind {
Whitespace,
Punct,
Word,
}
pub(crate) fn is_word_char(c: char) -> bool {
c.is_alphanumeric() || c == '_'
}
fn char_kind(c: char) -> CharKind {
if c.is_whitespace() {
CharKind::Whitespace
} else if is_word_char(c) {
CharKind::Word
} else {
CharKind::Punct
}
}
fn word_left(buffer: &Buffer, offset: u32) -> u32 {
let prev = |o: usize| buffer.char_before(o as u32);
let mut o = offset as usize;
if prev(o) == Some('\n') {
o -= 1; if o == 0 || prev(o) == Some('\n') {
return o as u32; }
}
while let Some(c) = prev(o).filter(|&c| c != '\n' && c.is_whitespace()) {
o -= c.len_utf8();
}
if let Some(c) = prev(o).filter(|&c| c != '\n') {
let kind = char_kind(c);
while let Some(c) = prev(o).filter(|&c| c != '\n' && char_kind(c) == kind) {
o -= c.len_utf8();
}
}
o as u32
}
fn word_right(buffer: &Buffer, offset: u32) -> u32 {
let len = buffer.len() as usize;
let next = |o: usize| buffer.char_at(o as u32);
let mut o = offset as usize;
if next(o) == Some('\n') {
o += 1; if o == len || next(o) == Some('\n') {
return o as u32; }
}
while let Some(c) = next(o).filter(|&c| c != '\n' && c.is_whitespace()) {
o += c.len_utf8();
}
if let Some(c) = next(o).filter(|&c| c != '\n') {
let kind = char_kind(c);
while o < len {
match next(o).filter(|&c| c != '\n' && char_kind(c) == kind) {
Some(c) => o += c.len_utf8(),
None => break,
}
}
}
o as u32
}
pub(crate) fn word_delete_left(buffer: &Buffer, caret: u32) -> u32 {
let mut o = caret as usize;
let prev = |o: usize| buffer.char_before(o as u32);
if prev(o) == Some('\n') {
return (o - 1) as u32; }
let (mut ws, mut ws_chars) = (o, 0u32);
while let Some(c) = prev(ws).filter(|&c| c != '\n' && c.is_whitespace()) {
ws -= c.len_utf8();
ws_chars += 1;
}
if ws_chars >= 2 {
return ws as u32; }
o = ws;
if let Some(c) = prev(o).filter(|&c| c != '\n') {
let kind = char_kind(c);
while let Some(c) = prev(o).filter(|&c| c != '\n' && char_kind(c) == kind) {
o -= c.len_utf8();
}
}
o as u32
}
pub(crate) fn word_delete_right(buffer: &Buffer, caret: u32) -> u32 {
let len = buffer.len() as usize;
let mut o = caret as usize;
let next = |o: usize| buffer.char_at(o as u32);
if next(o) == Some('\n') {
return (o + 1) as u32; }
let (mut ws, mut ws_chars) = (o, 0u32);
while let Some(c) = next(ws).filter(|&c| c != '\n' && c.is_whitespace()) {
ws += c.len_utf8();
ws_chars += 1;
}
if ws_chars >= 2 {
return ws as u32; }
o = ws;
if let Some(c) = next(o).filter(|&c| c != '\n') {
let kind = char_kind(c);
while o < len {
match next(o).filter(|&c| c != '\n' && char_kind(c) == kind) {
Some(c) => o += c.len_utf8(),
None => break,
}
}
}
o as u32
}
pub(crate) fn surrounding_word(buffer: &Buffer, offset: u32) -> Option<(u32, u32)> {
let prev = |o: usize| buffer.char_before(o as u32);
let next = |o: usize| buffer.char_at(o as u32);
let o = offset as usize;
let kind = match (prev(o).map(char_kind), next(o).map(char_kind)) {
(Some(a), Some(b)) => a.max(b),
(Some(a), None) => a,
(None, Some(b)) => b,
(None, None) => return None,
};
if kind == CharKind::Whitespace {
return None;
}
let mut start = o;
while let Some(c) = prev(start) {
if char_kind(c) == kind {
start -= c.len_utf8();
} else {
break;
}
}
let mut end = o;
while let Some(c) = next(end) {
if char_kind(c) == kind {
end += c.len_utf8();
} else {
break;
}
}
Some((start as u32, end as u32))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::coords::Point;
fn buf(s: &str) -> Buffer {
Buffer::new(s).unwrap()
}
fn mv(set: &mut SelectionSet, b: &Buffer, motion: Motion, extend: bool) {
use crate::fold_map::{FoldMap, FoldSet};
move_selections(
set,
b,
&FoldMap::new(&FoldSet::new(), &crate::bracket::Brackets::default(), b),
display_map::default_tab_size(),
motion,
extend,
);
}
fn at(b: &Buffer, row: u32, col: u32) -> u32 {
b.point_to_offset(Point::new(row, col))
}
fn head_point(set: &SelectionSet, b: &Buffer) -> Point {
b.offset_to_point(set.all()[0].head())
}
#[test]
fn left_right_wrap_across_lines() {
let b = buf("ab\ncd");
let mut set = SelectionSet::new(at(&b, 1, 0)); mv(&mut set, &b, Motion::Left, false);
assert_eq!(head_point(&set, &b), Point::new(0, 2)); mv(&mut set, &b, Motion::Right, false);
assert_eq!(head_point(&set, &b), Point::new(1, 0)); }
#[test]
fn inline_hidden_binary_search_picks_the_right_fold() {
use crate::document::Document;
let text = "a[bcd]e\nf[ghi]j\nk[lmn]o\n";
let mut doc = Document::new(text).unwrap();
for open in [1u32, 9, 17] {
assert!(doc.toggle_fold_opener(open), "pair at {open} folds");
}
let fm = doc.fold_map();
assert_eq!(inline_hidden(&fm, 11).map(|f| f.open), Some(9));
assert_eq!(inline_hidden(&fm, 3).map(|f| f.open), Some(1));
assert_eq!(inline_hidden(&fm, 19).map(|f| f.open), Some(17));
assert_eq!(inline_hidden(&fm, 0), None);
assert_eq!(inline_hidden(&fm, 6), None);
assert_eq!(inline_hidden(&fm, 8), None);
}
#[test]
fn plain_horizontal_move_collapses_selection_to_edge() {
let b = buf("hello");
let mut set = SelectionSet::new(0);
set.set_single(crate::Selection::from_anchor(crate::SelectionId(0), 1, 4));
mv(&mut set, &b, Motion::Left, false);
assert!(set.all()[0].is_empty());
assert_eq!(set.all()[0].head(), 1); }
#[test]
fn vertical_move_preserves_goal_column_over_short_lines() {
let b = buf("0123456789\nab\nlongenough");
let mut set = SelectionSet::new(at(&b, 0, 5));
mv(&mut set, &b, Motion::Down, false);
assert_eq!(head_point(&set, &b), Point::new(1, 2)); mv(&mut set, &b, Motion::Down, false);
assert_eq!(head_point(&set, &b), Point::new(2, 5)); }
#[test]
fn page_moves_jump_by_rows_and_clamp_to_the_document_ends() {
let b = Buffer::new("l0\nl1\nl2\nl3\nl4").unwrap(); let mut set = SelectionSet::new(0); mv(&mut set, &b, Motion::PageDown(2), false);
assert_eq!(b.offset_to_point(set.newest().head()).row, 2);
mv(&mut set, &b, Motion::PageDown(10), false); assert_eq!(set.newest().head(), b.len(), "clamps to the document end");
mv(&mut set, &b, Motion::PageUp(10), false); assert_eq!(set.newest().head(), 0, "clamps to the document start");
}
#[test]
fn word_delete_targets_stop_at_newlines() {
let b = Buffer::new("foo bar\nbaz").unwrap(); assert_eq!(word_delete_left(&b, 11), 8); assert_eq!(word_delete_left(&b, 8), 7); assert_eq!(word_delete_right(&b, 0), 3); assert_eq!(word_delete_right(&b, 7), 8); }
#[test]
fn word_delete_eats_only_a_multi_space_run() {
let b = Buffer::new("foo bar").unwrap(); assert_eq!(word_delete_left(&b, 6), 3); assert_eq!(word_delete_right(&b, 3), 6); let b1 = Buffer::new("foo bar").unwrap();
assert_eq!(word_delete_left(&b1, 4), 0); assert_eq!(word_delete_right(&b1, 3), 7); let b2 = Buffer::new("foo ").unwrap();
assert_eq!(word_delete_left(&b2, 5), 3); }
#[test]
fn word_motion_stops_at_boundaries() {
let b = buf("foo bar_baz qux");
let mut set = SelectionSet::new(0);
mv(&mut set, &b, Motion::WordRight, false);
assert_eq!(set.all()[0].head(), 3); mv(&mut set, &b, Motion::WordRight, false);
assert_eq!(set.all()[0].head(), 11); mv(&mut set, &b, Motion::WordLeft, false);
assert_eq!(set.all()[0].head(), 4); }
#[test]
fn word_motion_crosses_one_line_boundary_then_finds_the_word() {
let b = buf("foo\nbar");
assert_eq!(word_left(&b, 4), 0); assert_eq!(word_right(&b, 3), 7); }
#[test]
fn word_motion_stops_on_a_blank_line() {
let b = buf("foo\n\nbar");
assert_eq!(word_left(&b, 5), 4); assert_eq!(word_right(&b, 3), 4); }
#[test]
fn smart_home_toggles() {
let b = buf(" indented");
let mut set = SelectionSet::new(at(&b, 0, 12)); mv(&mut set, &b, Motion::LineStart, false);
assert_eq!(head_point(&set, &b), Point::new(0, 4)); mv(&mut set, &b, Motion::LineStart, false);
assert_eq!(head_point(&set, &b), Point::new(0, 0)); mv(&mut set, &b, Motion::LineStart, false);
assert_eq!(head_point(&set, &b), Point::new(0, 4)); }
#[test]
fn extend_moves_head_keeps_anchor() {
let b = buf("hello world");
let mut set = SelectionSet::new(0);
mv(&mut set, &b, Motion::WordRight, true); mv(&mut set, &b, Motion::WordRight, true); let s = &set.all()[0];
assert_eq!((s.start(), s.end()), (0, 11));
assert!(!s.is_empty());
}
#[test]
fn surrounding_word_selects_the_word_at_the_caret() {
let b = buf("foo bar_baz qux");
assert_eq!(surrounding_word(&b, 6), Some((4, 11))); assert_eq!(surrounding_word(&b, 4), Some((4, 11))); assert_eq!(surrounding_word(&b, 11), Some((4, 11))); assert_eq!(surrounding_word(&b, 12), None); }
#[test]
fn surrounding_word_stops_at_newline_and_picks_punct() {
let b = buf("ab\n++");
assert_eq!(surrounding_word(&b, 2), Some((0, 2))); assert_eq!(surrounding_word(&b, 4), Some((3, 5))); }
#[test]
fn multi_cursor_moves_and_remerges() {
let b = buf("a b c");
let mut set = SelectionSet::new(0);
set.add_caret(2); set.add_caret(4); assert_eq!(set.len(), 3);
mv(&mut set, &b, Motion::Left, false);
assert_eq!(set.len(), 3);
mv(&mut set, &b, Motion::LineStart, false);
assert_eq!(set.len(), 1);
}
}