summaryrefslogtreecommitdiff
path: root/alacritty_terminal/src/vi_mode.rs
diff options
context:
space:
mode:
authorChristian Duerr <contact@christianduerr.com>2020-03-18 02:35:08 +0000
committerGitHub <noreply@github.com>2020-03-18 02:35:08 +0000
commit1a8cd172e520e493bacc9c6a2ae6f80de086eaa3 (patch)
tree0b837f1f52f72fe00e258afc34094d60b5d18f04 /alacritty_terminal/src/vi_mode.rs
parent64db7d3daaed4e06fb8292227622bbc4cdaa2cf0 (diff)
downloadalacritty-1a8cd172e520e493bacc9c6a2ae6f80de086eaa3.tar.gz
alacritty-1a8cd172e520e493bacc9c6a2ae6f80de086eaa3.zip
Add modal keyboard motion mode
This implements a basic mode for navigating inside of Alacritty's history with keyboard bindings. They're bound by default to vi's motion shortcuts but are fully customizable. Since this relies on key bindings only single key bindings are currently supported (so no `ge`, or repetition). Other than navigating the history and moving the viewport, this mode should enable making use of all available selection modes to copy content to the clipboard and launch URLs below the cursor. This also changes the rendering of the block cursor at the side of selections, since previously it could be inverted to be completely invisible. Since that would have caused some troubles with this keyboard selection mode, the block cursor now is no longer inverted when it is at the edges of a selection. Fixes #262.
Diffstat (limited to 'alacritty_terminal/src/vi_mode.rs')
-rw-r--r--alacritty_terminal/src/vi_mode.rs799
1 files changed, 799 insertions, 0 deletions
diff --git a/alacritty_terminal/src/vi_mode.rs b/alacritty_terminal/src/vi_mode.rs
new file mode 100644
index 00000000..196193e8
--- /dev/null
+++ b/alacritty_terminal/src/vi_mode.rs
@@ -0,0 +1,799 @@
+use std::cmp::{max, min};
+
+use serde::Deserialize;
+
+use crate::event::EventListener;
+use crate::grid::{GridCell, Scroll};
+use crate::index::{Column, Line, Point};
+use crate::term::cell::Flags;
+use crate::term::{Search, Term};
+
+/// Possible vi mode motion movements.
+#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize)]
+pub enum ViMotion {
+ /// Move up.
+ Up,
+ /// Move down.
+ Down,
+ /// Move left.
+ Left,
+ /// Move right.
+ Right,
+ /// Move to start of line.
+ First,
+ /// Move to end of line.
+ Last,
+ /// Move to the first non-empty cell.
+ FirstOccupied,
+ /// Move to top of screen.
+ High,
+ /// Move to center of screen.
+ Middle,
+ /// Move to bottom of screen.
+ Low,
+ /// Move to start of semantically separated word.
+ SemanticLeft,
+ /// Move to start of next semantically separated word.
+ SemanticRight,
+ /// Move to end of previous semantically separated word.
+ SemanticLeftEnd,
+ /// Move to end of semantically separated word.
+ SemanticRightEnd,
+ /// Move to start of whitespace separated word.
+ WordLeft,
+ /// Move to start of next whitespace separated word.
+ WordRight,
+ /// Move to end of previous whitespace separated word.
+ WordLeftEnd,
+ /// Move to end of whitespace separated word.
+ WordRightEnd,
+ /// Move to opposing bracket.
+ Bracket,
+}
+
+/// Cursor tracking vi mode position.
+#[derive(Default, Copy, Clone)]
+pub struct ViModeCursor {
+ pub point: Point,
+}
+
+impl ViModeCursor {
+ pub fn new(point: Point) -> Self {
+ Self { point }
+ }
+
+ /// Move vi mode cursor.
+ #[must_use = "this returns the result of the operation, without modifying the original"]
+ pub fn motion<T: EventListener>(mut self, term: &mut Term<T>, motion: ViMotion) -> Self {
+ let display_offset = term.grid().display_offset();
+ let lines = term.grid().num_lines();
+ let cols = term.grid().num_cols();
+
+ let mut buffer_point = term.visible_to_buffer(self.point);
+
+ match motion {
+ ViMotion::Up => {
+ if buffer_point.line + 1 < term.grid().len() {
+ buffer_point.line += 1;
+ }
+ },
+ ViMotion::Down => buffer_point.line = buffer_point.line.saturating_sub(1),
+ ViMotion::Left => {
+ buffer_point = expand_wide(term, buffer_point, true);
+ let wrap_point = Point::new(buffer_point.line + 1, cols - 1);
+ if buffer_point.col.0 == 0
+ && buffer_point.line + 1 < term.grid().len()
+ && is_wrap(term, wrap_point)
+ {
+ buffer_point = wrap_point;
+ } else {
+ buffer_point.col = Column(buffer_point.col.saturating_sub(1));
+ }
+ },
+ ViMotion::Right => {
+ buffer_point = expand_wide(term, buffer_point, false);
+ if is_wrap(term, buffer_point) {
+ buffer_point = Point::new(buffer_point.line - 1, Column(0));
+ } else {
+ buffer_point.col = min(buffer_point.col + 1, cols - 1);
+ }
+ },
+ ViMotion::First => {
+ buffer_point = expand_wide(term, buffer_point, true);
+ while buffer_point.col.0 == 0
+ && buffer_point.line + 1 < term.grid().len()
+ && is_wrap(term, Point::new(buffer_point.line + 1, cols - 1))
+ {
+ buffer_point.line += 1;
+ }
+ buffer_point.col = Column(0);
+ },
+ ViMotion::Last => buffer_point = last(term, buffer_point),
+ ViMotion::FirstOccupied => buffer_point = first_occupied(term, buffer_point),
+ ViMotion::High => {
+ let line = display_offset + lines.0 - 1;
+ let col = first_occupied_in_line(term, line).unwrap_or_default().col;
+ buffer_point = Point::new(line, col);
+ },
+ ViMotion::Middle => {
+ let line = display_offset + lines.0 / 2;
+ let col = first_occupied_in_line(term, line).unwrap_or_default().col;
+ buffer_point = Point::new(line, col);
+ },
+ ViMotion::Low => {
+ let line = display_offset;
+ let col = first_occupied_in_line(term, line).unwrap_or_default().col;
+ buffer_point = Point::new(line, col);
+ },
+ ViMotion::SemanticLeft => buffer_point = semantic(term, buffer_point, true, true),
+ ViMotion::SemanticRight => buffer_point = semantic(term, buffer_point, false, true),
+ ViMotion::SemanticLeftEnd => buffer_point = semantic(term, buffer_point, true, false),
+ ViMotion::SemanticRightEnd => buffer_point = semantic(term, buffer_point, false, false),
+ ViMotion::WordLeft => buffer_point = word(term, buffer_point, true, true),
+ ViMotion::WordRight => buffer_point = word(term, buffer_point, false, true),
+ ViMotion::WordLeftEnd => buffer_point = word(term, buffer_point, true, false),
+ ViMotion::WordRightEnd => buffer_point = word(term, buffer_point, false, false),
+ ViMotion::Bracket => {
+ buffer_point = term.bracket_search(buffer_point).unwrap_or(buffer_point);
+ },
+ }
+
+ scroll_to_point(term, buffer_point);
+ self.point = term.grid().clamp_buffer_to_visible(buffer_point);
+
+ self
+ }
+
+ /// Get target cursor point for vim-like page movement.
+ #[must_use = "this returns the result of the operation, without modifying the original"]
+ pub fn scroll<T: EventListener>(mut self, term: &Term<T>, lines: isize) -> Self {
+ // Check number of lines the cursor needs to be moved
+ let overscroll = if lines > 0 {
+ let max_scroll = term.grid().history_size() - term.grid().display_offset();
+ max(0, lines - max_scroll as isize)
+ } else {
+ let max_scroll = term.grid().display_offset();
+ min(0, lines + max_scroll as isize)
+ };
+
+ // Clamp movement to within visible region
+ let mut line = self.point.line.0 as isize;
+ line -= overscroll;
+ line = max(0, min(term.grid().num_lines().0 as isize - 1, line));
+
+ // Find the first occupied cell after scrolling has been performed
+ let buffer_point = term.visible_to_buffer(self.point);
+ let mut target_line = buffer_point.line as isize + lines;
+ target_line = max(0, min(term.grid().len() as isize - 1, target_line));
+ let col = first_occupied_in_line(term, target_line as usize).unwrap_or_default().col;
+
+ // Move cursor
+ self.point = Point::new(Line(line as usize), col);
+
+ self
+ }
+}
+
+/// Scroll display if point is outside of viewport.
+fn scroll_to_point<T: EventListener>(term: &mut Term<T>, point: Point<usize>) {
+ let display_offset = term.grid().display_offset();
+ let lines = term.grid().num_lines();
+
+ // Scroll once the top/bottom has been reached
+ if point.line >= display_offset + lines.0 {
+ let lines = point.line.saturating_sub(display_offset + lines.0 - 1);
+ term.scroll_display(Scroll::Lines(lines as isize));
+ } else if point.line < display_offset {
+ let lines = display_offset.saturating_sub(point.line);
+ term.scroll_display(Scroll::Lines(-(lines as isize)));
+ };
+}
+
+/// Find next end of line to move to.
+fn last<T>(term: &Term<T>, mut point: Point<usize>) -> Point<usize> {
+ let cols = term.grid().num_cols();
+
+ // Expand across wide cells
+ point = expand_wide(term, point, false);
+
+ // Find last non-empty cell in the current line
+ let occupied = last_occupied_in_line(term, point.line).unwrap_or_default();
+
+ if point.col < occupied.col {
+ // Jump to last occupied cell when not already at or beyond it
+ occupied
+ } else if is_wrap(term, point) {
+ // Jump to last occupied cell across linewraps
+ while point.line > 0 && is_wrap(term, point) {
+ point.line -= 1;
+ }
+
+ last_occupied_in_line(term, point.line).unwrap_or(point)
+ } else {
+ // Jump to last column when beyond the last occupied cell
+ Point::new(point.line, cols - 1)
+ }
+}
+
+/// Find next non-empty cell to move to.
+fn first_occupied<T>(term: &Term<T>, mut point: Point<usize>) -> Point<usize> {
+ let cols = term.grid().num_cols();
+
+ // Expand left across wide chars, since we're searching lines left to right
+ point = expand_wide(term, point, true);
+
+ // Find first non-empty cell in current line
+ let occupied = first_occupied_in_line(term, point.line)
+ .unwrap_or_else(|| Point::new(point.line, cols - 1));
+
+ // Jump across wrapped lines if we're already at this line's first occupied cell
+ if point == occupied {
+ let mut occupied = None;
+
+ // Search for non-empty cell in previous lines
+ for line in (point.line + 1)..term.grid().len() {
+ if !is_wrap(term, Point::new(line, cols - 1)) {
+ break;
+ }
+
+ occupied = first_occupied_in_line(term, line).or(occupied);
+ }
+
+ // Fallback to the next non-empty cell
+ let mut line = point.line;
+ occupied.unwrap_or_else(|| loop {
+ if let Some(occupied) = first_occupied_in_line(term, line) {
+ break occupied;
+ }
+
+ let last_cell = Point::new(line, cols - 1);
+ if line == 0 || !is_wrap(term, last_cell) {
+ break last_cell;
+ }
+
+ line -= 1;
+ })
+ } else {
+ occupied
+ }
+}
+
+/// Move by semantically separated word, like w/b/e/ge in vi.
+fn semantic<T: EventListener>(
+ term: &mut Term<T>,
+ mut point: Point<usize>,
+ left: bool,
+ start: bool,
+) -> Point<usize> {
+ // Expand semantically based on movement direction
+ let expand_semantic = |point: Point<usize>| {
+ // Do not expand when currently on a semantic escape char
+ let cell = term.grid()[point.line][point.col];
+ if term.semantic_escape_chars().contains(cell.c)
+ && !cell.flags.contains(Flags::WIDE_CHAR_SPACER)
+ {
+ point
+ } else if left {
+ term.semantic_search_left(point)
+ } else {
+ term.semantic_search_right(point)
+ }
+ };
+
+ // Make sure we jump above wide chars
+ point = expand_wide(term, point, left);
+
+ // Move to word boundary
+ if left != start && !is_boundary(term, point, left) {
+ point = expand_semantic(point);
+ }
+
+ // Skip whitespace
+ let mut next_point = advance(term, point, left);
+ while !is_boundary(term, point, left) && is_space(term, next_point) {
+ point = next_point;
+ next_point = advance(term, point, left);
+ }
+
+ // Assure minimum movement of one cell
+ if !is_boundary(term, point, left) {
+ point = advance(term, point, left);
+ }
+
+ // Move to word boundary
+ if left == start && !is_boundary(term, point, left) {
+ point = expand_semantic(point);
+ }
+
+ point
+}
+
+/// Move by whitespace separated word, like W/B/E/gE in vi.
+fn word<T: EventListener>(
+ term: &mut Term<T>,
+ mut point: Point<usize>,
+ left: bool,
+ start: bool,
+) -> Point<usize> {
+ // Make sure we jump above wide chars
+ point = expand_wide(term, point, left);
+
+ if left == start {
+ // Skip whitespace until right before a word
+ let mut next_point = advance(term, point, left);
+ while !is_boundary(term, point, left) && is_space(term, next_point) {
+ point = next_point;
+ next_point = advance(term, point, left);
+ }
+
+ // Skip non-whitespace until right inside word boundary
+ let mut next_point = advance(term, point, left);
+ while !is_boundary(term, point, left) && !is_space(term, next_point) {
+ point = next_point;
+ next_point = advance(term, point, left);
+ }
+ }
+
+ if left != start {
+ // Skip non-whitespace until just beyond word
+ while !is_boundary(term, point, left) && !is_space(term, point) {
+ point = advance(term, point, left);
+ }
+
+ // Skip whitespace until right inside word boundary
+ while !is_boundary(term, point, left) && is_space(term, point) {
+ point = advance(term, point, left);
+ }
+ }
+
+ point
+}
+
+/// Jump to the end of a wide cell.
+fn expand_wide<T, P>(term: &Term<T>, point: P, left: bool) -> Point<usize>
+where
+ P: Into<Point<usize>>,
+{
+ let mut point = point.into();
+ let cell = term.grid()[point.line][point.col];
+
+ if cell.flags.contains(Flags::WIDE_CHAR) && !left {
+ point.col += 1;
+ } else if cell.flags.contains(Flags::WIDE_CHAR_SPACER)
+ && term.grid()[point.line][point.col - 1].flags.contains(Flags::WIDE_CHAR)
+ && left
+ {
+ point.col -= 1;
+ }
+
+ point
+}
+
+/// Find first non-empty cell in line.
+fn first_occupied_in_line<T>(term: &Term<T>, line: usize) -> Option<Point<usize>> {
+ (0..term.grid().num_cols().0)
+ .map(|col| Point::new(line, Column(col)))
+ .find(|&point| !is_space(term, point))
+}
+
+/// Find last non-empty cell in line.
+fn last_occupied_in_line<T>(term: &Term<T>, line: usize) -> Option<Point<usize>> {
+ (0..term.grid().num_cols().0)
+ .map(|col| Point::new(line, Column(col)))
+ .rfind(|&point| !is_space(term, point))
+}
+
+/// Advance point based on direction.
+fn advance<T>(term: &Term<T>, point: Point<usize>, left: bool) -> Point<usize> {
+ let cols = term.grid().num_cols();
+ if left {
+ point.sub_absolute(cols.0, 1)
+ } else {
+ point.add_absolute(cols.0, 1)
+ }
+}
+
+/// Check if cell at point contains whitespace.
+fn is_space<T>(term: &Term<T>, point: Point<usize>) -> bool {
+ let cell = term.grid()[point.line][point.col];
+ cell.c == ' ' || cell.c == '\t' && !cell.flags().contains(Flags::WIDE_CHAR_SPACER)
+}
+
+fn is_wrap<T>(term: &Term<T>, point: Point<usize>) -> bool {
+ term.grid()[point.line][point.col].flags.contains(Flags::WRAPLINE)
+}
+
+/// Check if point is at screen boundary.
+fn is_boundary<T>(term: &Term<T>, point: Point<usize>, left: bool) -> bool {
+ (point.line == 0 && point.col + 1 >= term.grid().num_cols() && !left)
+ || (point.line + 1 >= term.grid().len() && point.col.0 == 0 && left)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ use crate::clipboard::Clipboard;
+ use crate::config::MockConfig;
+ use crate::event::Event;
+ use crate::index::{Column, Line};
+ use crate::term::{SizeInfo, Term};
+
+ struct Mock;
+ impl EventListener for Mock {
+ fn send_event(&self, _event: Event) {}
+ }
+
+ fn term() -> Term<Mock> {
+ let size = SizeInfo {
+ width: 20.,
+ height: 20.,
+ cell_width: 1.0,
+ cell_height: 1.0,
+ padding_x: 0.0,
+ padding_y: 0.0,
+ dpr: 1.0,
+ };
+ Term::new(&MockConfig::default(), &size, Clipboard::new_nop(), Mock)
+ }
+
+ #[test]
+ fn motion_simple() {
+ let mut term = term();
+
+ let mut cursor = ViModeCursor::new(Point::new(Line(0), Column(0)));
+
+ cursor = cursor.motion(&mut term, ViMotion::Right);
+ assert_eq!(cursor.point, Point::new(Line(0), Column(1)));
+
+ cursor = cursor.motion(&mut term, ViMotion::Left);
+ assert_eq!(cursor.point, Point::new(Line(0), Column(0)));
+
+ cursor = cursor.motion(&mut term, ViMotion::Down);
+ assert_eq!(cursor.point, Point::new(Line(1), Column(0)));
+
+ cursor = cursor.motion(&mut term, ViMotion::Up);
+ assert_eq!(cursor.point, Point::new(Line(0), Column(0)));
+ }
+
+ #[test]
+ fn simple_wide() {
+ let mut term = term();
+ term.grid_mut()[Line(0)][Column(0)].c = 'a';
+ term.grid_mut()[Line(0)][Column(1)].c = '汉';
+ term.grid_mut()[Line(0)][Column(1)].flags.insert(Flags::WIDE_CHAR);
+ term.grid_mut()[Line(0)][Column(2)].c = ' ';
+ term.grid_mut()[Line(0)][Column(2)].flags.insert(Flags::WIDE_CHAR_SPACER);
+ term.grid_mut()[Line(0)][Column(3)].c = 'a';
+
+ let mut cursor = ViModeCursor::new(Point::new(Line(0), Column(1)));
+ cursor = cursor.motion(&mut term, ViMotion::Right);
+ assert_eq!(cursor.point, Point::new(Line(0), Column(3)));
+
+ let mut cursor = ViModeCursor::new(Point::new(Line(0), Column(2)));
+ cursor = cursor.motion(&mut term, ViMotion::Left);
+ assert_eq!(cursor.point, Point::new(Line(0), Column(0)));
+ }
+
+ #[test]
+ fn motion_start_end() {
+ let mut term = term();
+
+ let mut cursor = ViModeCursor::new(Point::new(Line(0), Column(0)));
+
+ cursor = cursor.motion(&mut term, ViMotion::Last);
+ assert_eq!(cursor.point, Point::new(Line(0), Column(19)));
+
+ cursor = cursor.motion(&mut term, ViMotion::First);
+ assert_eq!(cursor.point, Point::new(Line(0), Column(0)));
+ }
+
+ #[test]
+ fn motion_first_occupied() {
+ let mut term = term();
+ term.grid_mut()[Line(0)][Column(0)].c = ' ';
+ term.grid_mut()[Line(0)][Column(1)].c = 'x';
+ term.grid_mut()[Line(0)][Column(2)].c = ' ';
+ term.grid_mut()[Line(0)][Column(3)].c = 'y';
+ term.grid_mut()[Line(0)][Column(19)].flags.insert(Flags::WRAPLINE);
+ term.grid_mut()[Line(1)][Column(19)].flags.insert(Flags::WRAPLINE);
+ term.grid_mut()[Line(2)][Column(0)].c = 'z';
+ term.grid_mut()[Line(2)][Column(1)].c = ' ';
+
+ let mut cursor = ViModeCursor::new(Point::new(Line(2), Column(1)));
+
+ cursor = cursor.motion(&mut term, ViMotion::FirstOccupied);
+ assert_eq!(cursor.point, Point::new(Line(2), Column(0)));
+
+ cursor = cursor.motion(&mut term, ViMotion::FirstOccupied);
+ assert_eq!(cursor.point, Point::new(Line(0), Column(1)));
+ }
+
+ #[test]
+ fn motion_high_middle_low() {
+ let mut term = term();
+
+ let mut cursor = ViModeCursor::new(Point::new(Line(0), Column(0)));
+
+ cursor = cursor.motion(&mut term, ViMotion::High);
+ assert_eq!(cursor.point, Point::new(Line(0), Column(0)));
+
+ cursor = cursor.motion(&mut term, ViMotion::Middle);
+ assert_eq!(cursor.point, Point::new(Line(9), Column(0)));
+
+ cursor = cursor.motion(&mut term, ViMotion::Low);
+ assert_eq!(cursor.point, Point::new(Line(19), Column(0)));
+ }
+
+ #[test]
+ fn motion_bracket() {
+ let mut term = term();
+ term.grid_mut()[Line(0)][Column(0)].c = '(';
+ term.grid_mut()[Line(0)][Column(1)].c = 'x';
+ term.grid_mut()[Line(0)][Column(2)].c = ')';
+
+ let mut cursor = ViModeCursor::new(Point::new(Line(0), Column(0)));
+
+ cursor = cursor.motion(&mut term, ViMotion::Bracket);
+ assert_eq!(cursor.point, Point::new(Line(0), Column(2)));
+
+ cursor = cursor.motion(&mut term, ViMotion::Bracket);
+ assert_eq!(cursor.point, Point::new(Line(0), Column(0)));
+ }
+
+ fn motion_semantic_term() -> Term<Mock> {
+ let mut term = term();
+
+ term.grid_mut()[Line(0)][Column(0)].c = 'x';
+ term.grid_mut()[Line(0)][Column(1)].c = ' ';
+ term.grid_mut()[Line(0)][Column(2)].c = 'x';
+ term.grid_mut()[Line(0)][Column(3)].c = 'x';
+ term.grid_mut()[Line(0)][Column(4)].c = ' ';
+ term.grid_mut()[Line(0)][Column(5)].c = ' ';
+ term.grid_mut()[Line(0)][Column(6)].c = ':';
+ term.grid_mut()[Line(0)][Column(7)].c = ' ';
+ term.grid_mut()[Line(0)][Column(8)].c = 'x';
+ term.grid_mut()[Line(0)][Column(9)].c = ':';
+ term.grid_mut()[Line(0)][Column(10)].c = 'x';
+ term.grid_mut()[Line(0)][Column(11)].c = ' ';
+ term.grid_mut()[Line(0)][Column(12)].c = ' ';
+ term.grid_mut()[Line(0)][Column(13)].c = ':';
+ term.grid_mut()[Line(0)][Column(14)].c = ' ';
+ term.grid_mut()[Line(0)][Column(15)].c = 'x';
+
+ term
+ }
+
+ #[test]
+ fn motion_semantic_right_end() {
+ let mut term = motion_semantic_term();
+
+ let mut cursor = ViModeCursor::new(Point::new(Line(0), Column(0)));
+
+ cursor = cursor.motion(&mut term, ViMotion::SemanticRightEnd);
+ assert_eq!(cursor.point, Point::new(Line(0), Column(3)));
+
+ cursor = cursor.motion(&mut term, ViMotion::SemanticRightEnd);
+ assert_eq!(cursor.point, Point::new(Line(0), Column(6)));
+
+ cursor = cursor.motion(&mut term, ViMotion::SemanticRightEnd);
+ assert_eq!(cursor.point, Point::new(Line(0), Column(8)));
+
+ cursor = cursor.motion(&mut term, ViMotion::SemanticRightEnd);
+ assert_eq!(cursor.point, Point::new(Line(0), Column(9)));
+
+ cursor = cursor.motion(&mut term, ViMotion::SemanticRightEnd);
+ assert_eq!(cursor.point, Point::new(Line(0), Column(10)));
+
+ cursor = cursor.motion(&mut term, ViMotion::SemanticRightEnd);
+ assert_eq!(cursor.point, Point::new(Line(0), Column(13)));
+
+ cursor = cursor.motion(&mut term, ViMotion::SemanticRightEnd);
+ assert_eq!(cursor.point, Point::new(Line(0), Column(15)));
+ }
+
+ #[test]
+ fn motion_semantic_left_start() {
+ let mut term = motion_semantic_term();
+
+ let mut cursor = ViModeCursor::new(Point::new(Line(0), Column(15)));
+
+ cursor = cursor.motion(&mut term, ViMotion::SemanticLeft);
+ assert_eq!(cursor.point, Point::new(Line(0), Column(13)));
+
+ cursor = cursor.motion(&mut term, ViMotion::SemanticLeft);
+ assert_eq!(cursor.point, Point::new(Line(0), Column(10)));
+
+ cursor = cursor.motion(&mut term, ViMotion::SemanticLeft);
+ assert_eq!(cursor.point, Point::new(Line(0), Column(9)));
+
+ cursor = cursor.motion(&mut term, ViMotion::SemanticLeft);
+ assert_eq!(cursor.point, Point::new(Line(0), Column(8)));
+
+ cursor = cursor.motion(&mut term, ViMotion::SemanticLeft);
+ assert_eq!(cursor.point, Point::new(Line(0), Column(6)));
+
+ cursor = cursor.motion(&mut term, ViMotion::SemanticLeft);
+ assert_eq!(cursor.point, Point::new(Line(0), Column(2)));
+
+ cursor = cursor.motion(&mut term, ViMotion::SemanticLeft);
+ assert_eq!(cursor.point, Point::new(Line(0), Column(0)));
+ }
+
+ #[test]
+ fn motion_semantic_right_start() {
+ let mut term = motion_semantic_term();
+
+ let mut cursor = ViModeCursor::new(Point::new(Line(0), Column(0)));
+
+ cursor = cursor.motion(&mut term, ViMotion::SemanticRight);
+ assert_eq!(cursor.point, Point::new(Line(0), Column(2)));
+
+ cursor = cursor.motion(&mut term, ViMotion::SemanticRight);
+ assert_eq!(cursor.point, Point::new(Line(0), Column(6)));
+
+ cursor = cursor.motion(&mut term, ViMotion::SemanticRight);
+ assert_eq!(cursor.point, Point::new(Line(0), Column(8)));
+
+ cursor = cursor.motion(&mut term, ViMotion::SemanticRight);
+ assert_eq!(cursor.point, Point::new(Line(0), Column(9)));
+
+ cursor = cursor.motion(&mut term, ViMotion::SemanticRight);
+ assert_eq!(cursor.point, Point::new(Line(0), Column(10)));
+
+ cursor = cursor.motion(&mut term, ViMotion::SemanticRight);
+ assert_eq!(cursor.point, Point::new(Line(0), Column(13)));
+
+ cursor = cursor.motion(&mut term, ViMotion::SemanticRight);
+ assert_eq!(cursor.point, Point::new(Line(0), Column(15)));
+ }
+
+ #[test]
+ fn motion_semantic_left_end() {
+ let mut term = motion_semantic_term();
+
+ let mut cursor = ViModeCursor::new(Point::new(Line(0), Column(15)));
+
+ cursor = cursor.motion(&mut term, ViMotion::SemanticLeftEnd);
+ assert_eq!(cursor.point, Point::new(Line(0), Column(13)));
+
+ cursor = cursor.motion(&mut term, ViMotion::SemanticLeftEnd);
+ assert_eq!(cursor.point, Point::new(Line(0), Column(10)));
+
+ cursor = cursor.motion(&mut term, ViMotion::SemanticLeftEnd);
+ assert_eq!(cursor.point, Point::new(Line(0), Column(9)));
+
+ cursor = cursor.motion(&mut term, ViMotion::SemanticLeftEnd);
+ assert_eq!(cursor.point, Point::new(Line(0), Column(8)));
+
+ cursor = cursor.motion(&mut term, ViMotion::SemanticLeftEnd);
+ assert_eq!(cursor.point, Point::new(Line(0), Column(6)));
+
+ cursor = cursor.motion(&mut term, ViMotion::SemanticLeftEnd);
+ assert_eq!(cursor.point, Point::new(Line(0), Column(3)));
+
+ cursor = cursor.motion(&mut term, ViMotion::SemanticLeftEnd);
+ assert_eq!(cursor.point, Point::new(Line(0), Column(0)));
+ }
+
+ #[test]
+ fn scroll_semantic() {
+ let mut term = term();
+ term.grid_mut().scroll_up(&(Line(0)..Line(20)), Line(5), &Default::default());
+
+ let mut cursor = ViModeCursor::new(Point::new(Line(0), Column(0)));
+
+ cursor = cursor.motion(&mut term, ViMotion::SemanticLeft);
+ assert_eq!(cursor.point, Point::new(Line(0), Column(0)));
+ assert_eq!(term.grid().display_offset(), 5);
+
+ cursor = cursor.motion(&mut term, ViMotion::SemanticRight);
+ assert_eq!(cursor.point, Point::new(Line(19), Column(19)));
+ assert_eq!(term.grid().display_offset(), 0);
+
+ cursor = cursor.motion(&mut term, ViMotion::SemanticLeftEnd);
+ assert_eq!(cursor.point, Point::new(Line(0), Column(0)));
+ assert_eq!(term.grid().display_offset(), 5);
+
+ cursor = cursor.motion(&mut term, ViMotion::SemanticRightEnd);
+ assert_eq!(cursor.point, Point::new(Line(19), Column(19)));
+ assert_eq!(term.grid().display_offset(), 0);
+ }
+
+ #[test]
+ fn semantic_wide() {
+ let mut term = term();
+ term.grid_mut()[Line(0)][Column(0)].c = 'a';
+ term.grid_mut()[Line(0)][Column(1)].c = ' ';
+ term.grid_mut()[Line(0)][Column(2)].c = '汉';
+ term.grid_mut()[Line(0)][Column(2)].flags.insert(Flags::WIDE_CHAR);
+ term.grid_mut()[Line(0)][Column(3)].c = ' ';
+ term.grid_mut()[Line(0)][Column(3)].flags.insert(Flags::WIDE_CHAR_SPACER);
+ term.grid_mut()[Line(0)][Column(4)].c = ' ';
+ term.grid_mut()[Line(0)][Column(5)].c = 'a';
+
+ let mut cursor = ViModeCursor::new(Point::new(Line(0), Column(2)));
+ cursor = cursor.motion(&mut term, ViMotion::SemanticRight);
+ assert_eq!(cursor.point, Point::new(Line(0), Column(5)));
+
+ let mut cursor = ViModeCursor::new(Point::new(Line(0), Column(3)));
+ cursor = cursor.motion(&mut term, ViMotion::SemanticLeft);
+ assert_eq!(cursor.point, Point::new(Line(0), Column(0)));
+ }
+
+ #[test]
+ fn motion_word() {
+ let mut term = term();
+ term.grid_mut()[Line(0)][Column(0)].c = 'a';
+ term.grid_mut()[Line(0)][Column(1)].c = ';';
+ term.grid_mut()[Line(0)][Column(2)].c = ' ';
+ term.grid_mut()[Line(0)][Column(3)].c = ' ';
+ term.grid_mut()[Line(0)][Column(4)].c = 'a';
+ term.grid_mut()[Line(0)][Column(5)].c = ';';
+
+ let mut cursor = ViModeCursor::new(Point::new(Line(0), Column(0)));
+
+ cursor = cursor.motion(&mut term, ViMotion::WordRightEnd);
+ assert_eq!(cursor.point, Point::new(Line(0), Column(1)));
+
+ cursor = cursor.motion(&mut term, ViMotion::WordRightEnd);
+ assert_eq!(cursor.point, Point::new(Line(0), Column(5)));
+
+ cursor = cursor.motion(&mut term, ViMotion::WordLeft);
+ assert_eq!(cursor.point, Point::new(Line(0), Column(4)));
+
+ cursor = cursor.motion(&mut term, ViMotion::WordLeft);
+ assert_eq!(cursor.point, Point::new(Line(0), Column(0)));
+
+ cursor = cursor.motion(&mut term, ViMotion::WordRight);
+ assert_eq!(cursor.point, Point::new(Line(0), Column(4)));
+
+ cursor = cursor.motion(&mut term, ViMotion::WordLeftEnd);
+ assert_eq!(cursor.point, Point::new(Line(0), Column(1)));
+ }
+
+ #[test]
+ fn scroll_word() {
+ let mut term = term();
+ term.grid_mut().scroll_up(&(Line(0)..Line(20)), Line(5), &Default::default());
+
+ let mut cursor = ViModeCursor::new(Point::new(Line(0), Column(0)));
+
+ cursor = cursor.motion(&mut term, ViMotion::WordLeft);
+ assert_eq!(cursor.point, Point::new(Line(0), Column(0)));
+ assert_eq!(term.grid().display_offset(), 5);
+
+ cursor = cursor.motion(&mut term, ViMotion::WordRight);
+ assert_eq!(cursor.point, Point::new(Line(19), Column(19)));
+ assert_eq!(term.grid().display_offset(), 0);
+
+ cursor = cursor.motion(&mut term, ViMotion::WordLeftEnd);
+ assert_eq!(cursor.point, Point::new(Line(0), Column(0)));
+ assert_eq!(term.grid().display_offset(), 5);
+
+ cursor = cursor.motion(&mut term, ViMotion::WordRightEnd);
+ assert_eq!(cursor.point, Point::new(Line(19), Column(19)));
+ assert_eq!(term.grid().display_offset(), 0);
+ }
+
+ #[test]
+ fn word_wide() {
+ let mut term = term();
+ term.grid_mut()[Line(0)][Column(0)].c = 'a';
+ term.grid_mut()[Line(0)][Column(1)].c = ' ';
+ term.grid_mut()[Line(0)][Column(2)].c = '汉';
+ term.grid_mut()[Line(0)][Column(2)].flags.insert(Flags::WIDE_CHAR);
+ term.grid_mut()[Line(0)][Column(3)].c = ' ';
+ term.grid_mut()[Line(0)][Column(3)].flags.insert(Flags::WIDE_CHAR_SPACER);
+ term.grid_mut()[Line(0)][Column(4)].c = ' ';
+ term.grid_mut()[Line(0)][Column(5)].c = 'a';
+
+ let mut cursor = ViModeCursor::new(Point::new(Line(0), Column(2)));
+ cursor = cursor.motion(&mut term, ViMotion::WordRight);
+ assert_eq!(cursor.point, Point::new(Line(0), Column(5)));
+
+ let mut cursor = ViModeCursor::new(Point::new(Line(0), Column(3)));
+ cursor = cursor.motion(&mut term, ViMotion::WordLeft);
+ assert_eq!(cursor.point, Point::new(Line(0), Column(0)));
+ }
+}