use crate::{
core_editor::{
graphemes::{next_grapheme_boundary, prev_grapheme_boundary},
line, word, CaretGeometry, Cursor,
},
enums::{Direction, MotionTarget, WordEdge},
FindStop,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct ResolvedMotion {
pub(crate) head: usize,
pub(crate) op_end: usize,
}
pub(crate) fn operator_span(
buf: &str,
origin: usize,
target: MotionTarget,
geometry: CaretGeometry,
) -> Cursor {
Cursor::new(origin, resolve_motion(buf, origin, target, geometry).op_end)
}
pub(crate) fn resolve_motion(
buf: &str,
origin: usize,
target: MotionTarget,
geometry: CaretGeometry,
) -> ResolvedMotion {
let block = geometry.is_inclusive();
let span = |head: usize, inclusive: bool| ResolvedMotion {
head,
op_end: if inclusive {
next_grapheme_boundary(buf, head)
} else {
head
},
};
match target {
MotionTarget::Grapheme(Direction::Forward) => {
span(next_grapheme_boundary(buf, origin), false)
}
MotionTarget::Grapheme(Direction::Backward) => {
span(prev_grapheme_boundary(buf, origin), false)
}
MotionTarget::Word {
kind,
edge,
direction,
} => {
let forward = direction == Direction::Forward;
let on_grapheme = block && forward && edge == WordEdge::End;
let head = if on_grapheme {
let probe = next_grapheme_boundary(buf, origin);
prev_grapheme_boundary(buf, word::locate_word(buf, probe, kind, edge, direction))
} else {
word::locate_word(buf, origin, kind, edge, direction)
};
span(head, on_grapheme)
}
MotionTarget::Offset(n) => span(n.min(buf.len()), false),
MotionTarget::BufferEdge(Direction::Backward) => span(0, false),
MotionTarget::BufferEdge(Direction::Forward) => span(buf.len(), false),
MotionTarget::LineEdge(Direction::Backward) => {
span(line::start_of_line(buf, origin), false)
}
MotionTarget::LineEdge(Direction::Forward) => span(line::end_of_line(buf, origin), false),
MotionTarget::Line(Direction::Forward) => {
let head = line::start_of_next_line(buf, origin).unwrap_or(origin);
span(head, false)
}
MotionTarget::Line(Direction::Backward) => {
let line_start = line::start_of_line(buf, origin);
let head = if line_start == 0 {
origin
} else {
line::start_of_line(buf, line_start - 1)
};
span(head, false)
}
MotionTarget::Find {
ch,
direction,
stop,
} => {
let hit = find_char(buf, origin, ch, direction, stop);
let inclusive = hit.is_some() && direction == Direction::Forward;
span(hit.unwrap_or(origin), inclusive)
}
}
}
fn find_char(
buf: &str,
origin: usize,
ch: char,
direction: Direction,
stop: FindStop,
) -> Option<usize> {
let hit = match direction {
Direction::Forward => {
let start = next_grapheme_boundary(buf, origin);
buf[start..].find(ch).map(|rel| start + rel)
}
Direction::Backward => buf[..origin].rfind(ch),
}?;
Some(match (direction, stop) {
(_, FindStop::On) => hit,
(Direction::Forward, FindStop::Before) => prev_grapheme_boundary(buf, hit),
(Direction::Backward, FindStop::Before) => next_grapheme_boundary(buf, hit),
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::WordKind;
use rstest::rstest;
fn word(edge: WordEdge, direction: Direction) -> MotionTarget {
MotionTarget::Word {
kind: WordKind::Word,
edge,
direction,
}
}
#[test]
fn resolve_motion_marks_forward_word_end_inclusive() {
let m = resolve_motion(
"foo bar",
0,
word(WordEdge::End, Direction::Forward),
CaretGeometry::Block,
);
assert_eq!(m, ResolvedMotion { head: 2, op_end: 3 });
let m = resolve_motion(
"foo bar",
0,
word(WordEdge::Start, Direction::Forward),
CaretGeometry::Block,
);
assert_eq!(m.op_end, m.head);
let m = resolve_motion(
"foo bar",
7,
word(WordEdge::End, Direction::Backward),
CaretGeometry::Block,
);
assert_eq!(m.op_end, m.head);
}
#[test]
fn resolve_motion_handles_line_and_buffer_edges() {
let buf = "ab\ncd\nef";
assert_eq!(
resolve_motion(
buf,
4,
MotionTarget::LineEdge(Direction::Backward),
CaretGeometry::Block
)
.head,
3
);
assert_eq!(
resolve_motion(
buf,
4,
MotionTarget::LineEdge(Direction::Forward),
CaretGeometry::Block
)
.head,
5
);
assert_eq!(
resolve_motion(
buf,
4,
MotionTarget::BufferEdge(Direction::Backward),
CaretGeometry::Block
)
.head,
0
);
assert_eq!(
resolve_motion(
buf,
4,
MotionTarget::BufferEdge(Direction::Forward),
CaretGeometry::Block
)
.head,
8
);
}
use crate::enums::FindStop;
fn find(ch: char, direction: Direction, stop: FindStop) -> MotionTarget {
MotionTarget::Find {
ch,
direction,
stop,
}
}
#[test]
fn resolve_motion_find_forward_on_lands_on_char() {
assert_eq!(
resolve_motion(
"foo bar",
0,
find('b', Direction::Forward, FindStop::On),
CaretGeometry::Block
),
ResolvedMotion { head: 4, op_end: 5 } );
}
#[test]
fn resolve_motion_find_forward_before_stops_short() {
assert_eq!(
resolve_motion(
"foo bar",
0,
find('b', Direction::Forward, FindStop::Before),
CaretGeometry::Block
),
ResolvedMotion { head: 3, op_end: 4 } );
}
#[test]
fn resolve_motion_find_backward_on_lands_on_char() {
assert_eq!(
resolve_motion(
"foo bar",
6,
find('f', Direction::Backward, FindStop::On),
CaretGeometry::Block
),
ResolvedMotion { head: 0, op_end: 0 } );
}
#[test]
fn resolve_motion_find_backward_before_stops_short() {
assert_eq!(
resolve_motion(
"foo bar",
6,
find('f', Direction::Backward, FindStop::Before),
CaretGeometry::Block
),
ResolvedMotion { head: 1, op_end: 1 } );
}
#[test]
fn resolve_motion_find_searches_strictly_past_origin() {
assert_eq!(
resolve_motion(
"foo bar",
4,
find('b', Direction::Forward, FindStop::On),
CaretGeometry::Block
)
.head,
4
);
}
#[test]
fn resolve_motion_find_before_replay_from_landing_spot_is_stuck() {
let t = find('x', Direction::Forward, FindStop::Before);
assert_eq!(resolve_motion("axbxc", 0, t, CaretGeometry::Block).head, 0);
assert_eq!(resolve_motion("axbxc", 2, t, CaretGeometry::Block).head, 2);
}
#[test]
fn resolve_motion_find_absent_char_stays_put() {
assert_eq!(
resolve_motion(
"foo bar",
3,
find('z', Direction::Forward, FindStop::On),
CaretGeometry::Block
),
ResolvedMotion { head: 3, op_end: 3 } );
}
#[test]
fn resolve_motion_find_before_respects_grapheme_boundaries() {
assert_eq!(
resolve_motion(
"a→b",
0,
find('b', Direction::Forward, FindStop::Before),
CaretGeometry::Block
)
.head,
1
);
assert_eq!(
resolve_motion(
"a→b",
4,
find('a', Direction::Backward, FindStop::Before),
CaretGeometry::Block
)
.head,
1
);
}
#[test]
fn resolve_motion_find_backward_finds_adjacent_char() {
assert_eq!(
resolve_motion(
"fab",
2,
find('a', Direction::Backward, FindStop::On),
CaretGeometry::Block
)
.head,
1
);
}
#[test]
fn resolve_motion_find_backward_searches_strictly_before_origin() {
assert_eq!(
resolve_motion(
"bab",
0,
find('b', Direction::Backward, FindStop::On),
CaretGeometry::Block
)
.head,
0
);
}
#[test]
fn resolve_motion_line_edge_forward_stops_at_newline() {
assert_eq!(
resolve_motion(
"ab\ncd",
0,
MotionTarget::LineEdge(Direction::Forward),
CaretGeometry::Block
),
ResolvedMotion { head: 2, op_end: 2 } );
}
#[test]
fn resolve_motion_line_edge_forward_stops_before_crlf() {
assert_eq!(
resolve_motion(
"ab\r\ncd",
0,
MotionTarget::LineEdge(Direction::Forward),
CaretGeometry::Block
)
.head,
2
);
}
#[test]
fn resolve_motion_line_edge_backward_stops_at_line_start() {
assert_eq!(
resolve_motion(
"ab\ncd",
4,
MotionTarget::LineEdge(Direction::Backward),
CaretGeometry::Block
)
.head,
3
);
}
#[test]
fn resolve_motion_buffer_edge_spans_whole_buffer() {
assert_eq!(
resolve_motion(
"ab\ncd",
0,
MotionTarget::BufferEdge(Direction::Forward),
CaretGeometry::Block
)
.head,
5
);
assert_eq!(
resolve_motion(
"ab\ncd",
4,
MotionTarget::BufferEdge(Direction::Backward),
CaretGeometry::Block
)
.head,
0
);
}
#[test]
fn resolve_motion_line_targets_the_adjacent_line() {
let buf = "ab\ncd\nef"; assert_eq!(
resolve_motion(
buf,
4,
MotionTarget::Line(Direction::Forward),
CaretGeometry::Block
)
.head,
6
);
assert_eq!(
resolve_motion(
buf,
4,
MotionTarget::Line(Direction::Backward),
CaretGeometry::Block
)
.head,
0
);
assert_eq!(
resolve_motion(
buf,
7,
MotionTarget::Line(Direction::Forward),
CaretGeometry::Block
)
.head,
7
);
assert_eq!(
resolve_motion(
buf,
1,
MotionTarget::Line(Direction::Backward),
CaretGeometry::Block
)
.head,
1
);
}
#[rstest]
#[case("abc def ghi", 0, 2)]
#[case("abc-def ghi", 0, 2)]
#[case("abc.def ghi", 0, 6)]
#[case("abc", 1, 2)]
#[case("abc", 2, 2)]
#[case("abc def", 2, 6)]
fn locate_unicode_word_right_end(
#[case] input: &str,
#[case] position: usize,
#[case] expected: usize,
) {
let head = resolve_motion(
input,
position,
MotionTarget::Word {
kind: WordKind::Unicode,
edge: WordEdge::End,
direction: Direction::Forward,
},
CaretGeometry::Block,
)
.head;
assert_eq!(head, expected);
}
#[rstest]
#[case("abc def ghi", 0, 2)]
#[case("abc-def ghi", 0, 6)]
#[case("abc-def ghi", 5, 6)]
#[case("abc-def ghi", 6, 10)]
#[case("abc.def ghi", 0, 6)]
#[case("abc", 1, 2)]
#[case("abc", 2, 2)]
#[case("abc def", 2, 6)]
#[case("abc-def", 6, 6)]
fn locate_long_word_right_end(
#[case] input: &str,
#[case] position: usize,
#[case] expected: usize,
) {
let head = resolve_motion(
input,
position,
MotionTarget::Word {
kind: WordKind::LongWord,
edge: WordEdge::End,
direction: Direction::Forward,
},
CaretGeometry::Block,
)
.head;
assert_eq!(head, expected);
}
#[rstest]
#[case("", 0, 0)] #[case("word", 0, 3)] #[case("word and another one", 0, 3)]
#[case("word and another one", 3, 7)] #[case("word and another one", 4, 7)] #[case("word\nline two", 0, 3)] #[case("word\nline two", 3, 8)] #[case("weirdö characters", 0, 5)] #[case("weirdö characters", 5, 17)] #[case("weirdö", 0, 5)] #[case("weirdö", 5, 5)] #[case("word😇 with emoji", 0, 3)] #[case("word😇 with emoji", 3, 4)] #[case("😇", 0, 0)] fn locate_unicode_word_right_end_multibyte(
#[case] input: &str,
#[case] in_location: usize,
#[case] expected: usize,
) {
let head = resolve_motion(
input,
in_location,
MotionTarget::Word {
kind: WordKind::Unicode,
edge: WordEdge::End,
direction: Direction::Forward,
},
CaretGeometry::Block,
)
.head;
assert_eq!(head, expected);
}
}