summaryrefslogtreecommitdiff
path: root/alacritty_terminal
diff options
context:
space:
mode:
Diffstat (limited to 'alacritty_terminal')
-rw-r--r--alacritty_terminal/src/config/colors.rs2
-rw-r--r--alacritty_terminal/src/config/mod.rs32
-rw-r--r--alacritty_terminal/src/grid/mod.rs12
-rw-r--r--alacritty_terminal/src/index.rs41
-rw-r--r--alacritty_terminal/src/lib.rs1
-rw-r--r--alacritty_terminal/src/selection.rs106
-rw-r--r--alacritty_terminal/src/term/mod.rs456
-rw-r--r--alacritty_terminal/src/vi_mode.rs799
8 files changed, 1215 insertions, 234 deletions
diff --git a/alacritty_terminal/src/config/colors.rs b/alacritty_terminal/src/config/colors.rs
index 35c03684..5c057619 100644
--- a/alacritty_terminal/src/config/colors.rs
+++ b/alacritty_terminal/src/config/colors.rs
@@ -12,6 +12,8 @@ pub struct Colors {
#[serde(deserialize_with = "failure_default")]
pub cursor: CursorColors,
#[serde(deserialize_with = "failure_default")]
+ pub vi_mode_cursor: CursorColors,
+ #[serde(deserialize_with = "failure_default")]
pub selection: SelectionColors,
#[serde(deserialize_with = "failure_default")]
normal: NormalColors,
diff --git a/alacritty_terminal/src/config/mod.rs b/alacritty_terminal/src/config/mod.rs
index da95391c..df8d37bd 100644
--- a/alacritty_terminal/src/config/mod.rs
+++ b/alacritty_terminal/src/config/mod.rs
@@ -28,7 +28,7 @@ mod scrolling;
mod visual_bell;
mod window;
-use crate::ansi::{Color, CursorStyle, NamedColor};
+use crate::ansi::{CursorStyle, NamedColor};
pub use crate::config::colors::Colors;
pub use crate::config::debug::Debug;
@@ -170,16 +170,28 @@ impl<T> Config<T> {
self.dynamic_title.0
}
- /// Cursor foreground color
+ /// Cursor foreground color.
#[inline]
pub fn cursor_text_color(&self) -> Option<Rgb> {
self.colors.cursor.text
}
- /// Cursor background color
+ /// Cursor background color.
#[inline]
- pub fn cursor_cursor_color(&self) -> Option<Color> {
- self.colors.cursor.cursor.map(|_| Color::Named(NamedColor::Cursor))
+ pub fn cursor_cursor_color(&self) -> Option<NamedColor> {
+ self.colors.cursor.cursor.map(|_| NamedColor::Cursor)
+ }
+
+ /// Vi mode cursor foreground color.
+ #[inline]
+ pub fn vi_mode_cursor_text_color(&self) -> Option<Rgb> {
+ self.colors.vi_mode_cursor.text
+ }
+
+ /// Vi mode cursor background color.
+ #[inline]
+ pub fn vi_mode_cursor_cursor_color(&self) -> Option<Rgb> {
+ self.colors.vi_mode_cursor.cursor
}
#[inline]
@@ -230,20 +242,16 @@ impl Default for EscapeChars {
}
#[serde(default)]
-#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Eq)]
+#[derive(Deserialize, Copy, Clone, Debug, Default, PartialEq, Eq)]
pub struct Cursor {
#[serde(deserialize_with = "failure_default")]
pub style: CursorStyle,
+ #[serde(deserialize_with = "option_explicit_none")]
+ pub vi_mode_style: Option<CursorStyle>,
#[serde(deserialize_with = "failure_default")]
unfocused_hollow: DefaultTrueBool,
}
-impl Default for Cursor {
- fn default() -> Self {
- Self { style: Default::default(), unfocused_hollow: Default::default() }
- }
-}
-
impl Cursor {
pub fn unfocused_hollow(self) -> bool {
self.unfocused_hollow.0
diff --git a/alacritty_terminal/src/grid/mod.rs b/alacritty_terminal/src/grid/mod.rs
index 34d989db..37cf0eb6 100644
--- a/alacritty_terminal/src/grid/mod.rs
+++ b/alacritty_terminal/src/grid/mod.rs
@@ -264,11 +264,9 @@ impl<T: GridCell + PartialEq + Copy> Grid<T> {
let mut new_empty_lines = 0;
let mut reversed: Vec<Row<T>> = Vec::with_capacity(self.raw.len());
for (i, mut row) in self.raw.drain().enumerate().rev() {
- // FIXME: Rust 1.39.0+ allows moving in pattern guard here
// Check if reflowing shoud be performed
- let mut last_row = reversed.last_mut();
- let last_row = match last_row {
- Some(ref mut last_row) if should_reflow(last_row) => last_row,
+ let last_row = match reversed.last_mut() {
+ Some(last_row) if should_reflow(last_row) => last_row,
_ => {
reversed.push(row);
continue;
@@ -356,11 +354,9 @@ impl<T: GridCell + PartialEq + Copy> Grid<T> {
}
loop {
- // FIXME: Rust 1.39.0+ allows moving in pattern guard here
// Check if reflowing shoud be performed
- let wrapped = row.shrink(cols);
- let mut wrapped = match wrapped {
- Some(_) if reflow => wrapped.unwrap(),
+ let mut wrapped = match row.shrink(cols) {
+ Some(wrapped) if reflow => wrapped,
_ => {
new_raw.push(row);
break;
diff --git a/alacritty_terminal/src/index.rs b/alacritty_terminal/src/index.rs
index 56d32003..1334a74e 100644
--- a/alacritty_terminal/src/index.rs
+++ b/alacritty_terminal/src/index.rs
@@ -30,6 +30,15 @@ pub enum Side {
Right,
}
+impl Side {
+ pub fn opposite(self) -> Self {
+ match self {
+ Side::Right => Side::Left,
+ Side::Left => Side::Right,
+ }
+ }
+}
+
/// Index in the grid using row, column notation
#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Serialize, Deserialize, PartialOrd)]
pub struct Point<L = Line> {
@@ -49,7 +58,7 @@ impl<L> Point<L> {
L: Copy + Default + Into<Line> + Add<usize, Output = L> + Sub<usize, Output = L>,
{
let line_changes =
- f32::ceil(rhs.saturating_sub(self.col.0) as f32 / num_cols as f32) as usize;
+ (rhs.saturating_sub(self.col.0) as f32 / num_cols as f32).ceil() as usize;
if self.line.into() > Line(line_changes) {
self.line = self.line - line_changes;
} else {
@@ -63,12 +72,40 @@ impl<L> Point<L> {
#[must_use = "this returns the result of the operation, without modifying the original"]
pub fn add(mut self, num_cols: usize, rhs: usize) -> Point<L>
where
- L: Add<usize, Output = L> + Sub<usize, Output = L>,
+ L: Copy + Default + Into<Line> + Add<usize, Output = L> + Sub<usize, Output = L>,
{
self.line = self.line + (rhs + self.col.0) / num_cols;
self.col = Column((self.col.0 + rhs) % num_cols);
self
}
+
+ #[inline]
+ #[must_use = "this returns the result of the operation, without modifying the original"]
+ pub fn sub_absolute(mut self, num_cols: usize, rhs: usize) -> Point<L>
+ where
+ L: Copy + Default + Into<Line> + Add<usize, Output = L> + Sub<usize, Output = L>,
+ {
+ self.line =
+ self.line + (rhs.saturating_sub(self.col.0) as f32 / num_cols as f32).ceil() as usize;
+ self.col = Column((num_cols + self.col.0 - rhs % num_cols) % num_cols);
+ self
+ }
+
+ #[inline]
+ #[must_use = "this returns the result of the operation, without modifying the original"]
+ pub fn add_absolute(mut self, num_cols: usize, rhs: usize) -> Point<L>
+ where
+ L: Copy + Default + Into<Line> + Add<usize, Output = L> + Sub<usize, Output = L>,
+ {
+ let line_changes = (rhs + self.col.0) / num_cols;
+ if self.line.into() > Line(line_changes) {
+ self.line = self.line - line_changes;
+ } else {
+ self.line = Default::default();
+ }
+ self.col = Column((self.col.0 + rhs) % num_cols);
+ self
+ }
}
impl Ord for Point {
diff --git a/alacritty_terminal/src/lib.rs b/alacritty_terminal/src/lib.rs
index 039f2b81..6991ffdc 100644
--- a/alacritty_terminal/src/lib.rs
+++ b/alacritty_terminal/src/lib.rs
@@ -37,6 +37,7 @@ pub mod sync;
pub mod term;
pub mod tty;
pub mod util;
+pub mod vi_mode;
pub use crate::grid::Grid;
pub use crate::term::Term;
diff --git a/alacritty_terminal/src/selection.rs b/alacritty_terminal/src/selection.rs
index f663417f..369846cf 100644
--- a/alacritty_terminal/src/selection.rs
+++ b/alacritty_terminal/src/selection.rs
@@ -27,7 +27,7 @@ use crate::term::{Search, Term};
/// A Point and side within that point.
#[derive(Debug, Copy, Clone, PartialEq)]
-pub struct Anchor {
+struct Anchor {
point: Point<usize>,
side: Side,
}
@@ -67,7 +67,7 @@ impl<L> SelectionRange<L> {
/// Different kinds of selection.
#[derive(Debug, Copy, Clone, PartialEq)]
-enum SelectionType {
+pub enum SelectionType {
Simple,
Block,
Semantic,
@@ -94,48 +94,20 @@ enum SelectionType {
/// [`update`]: enum.Selection.html#method.update
#[derive(Debug, Clone, PartialEq)]
pub struct Selection {
+ pub ty: SelectionType,
region: Range<Anchor>,
- ty: SelectionType,
}
impl Selection {
- pub fn simple(location: Point<usize>, side: Side) -> Selection {
+ pub fn new(ty: SelectionType, location: Point<usize>, side: Side) -> Selection {
Self {
region: Range { start: Anchor::new(location, side), end: Anchor::new(location, side) },
- ty: SelectionType::Simple,
+ ty,
}
}
- pub fn block(location: Point<usize>, side: Side) -> Selection {
- Self {
- region: Range { start: Anchor::new(location, side), end: Anchor::new(location, side) },
- ty: SelectionType::Block,
- }
- }
-
- pub fn semantic(location: Point<usize>) -> Selection {
- Self {
- region: Range {
- start: Anchor::new(location, Side::Left),
- end: Anchor::new(location, Side::Right),
- },
- ty: SelectionType::Semantic,
- }
- }
-
- pub fn lines(location: Point<usize>) -> Selection {
- Self {
- region: Range {
- start: Anchor::new(location, Side::Left),
- end: Anchor::new(location, Side::Right),
- },
- ty: SelectionType::Lines,
- }
- }
-
- pub fn update(&mut self, location: Point<usize>, side: Side) {
- self.region.end.point = location;
- self.region.end.side = side;
+ pub fn update(&mut self, point: Point<usize>, side: Side) {
+ self.region.end = Anchor::new(point, side);
}
pub fn rotate(
@@ -233,6 +205,24 @@ impl Selection {
}
}
+ /// Expand selection sides to include all cells.
+ pub fn include_all(&mut self) {
+ let (start, end) = (self.region.start.point, self.region.end.point);
+ let (start_side, end_side) = match self.ty {
+ SelectionType::Block
+ if start.col > end.col || (start.col == end.col && start.line < end.line) =>
+ {
+ (Side::Right, Side::Left)
+ },
+ SelectionType::Block => (Side::Left, Side::Right),
+ _ if Self::points_need_swap(start, end) => (Side::Right, Side::Left),
+ _ => (Side::Left, Side::Right),
+ };
+
+ self.region.start.side = start_side;
+ self.region.end.side = end_side;
+ }
+
/// Convert selection to grid coordinates.
pub fn to_range<T>(&self, term: &Term<T>) -> Option<SelectionRange> {
let grid = term.grid();
@@ -392,7 +382,8 @@ impl Selection {
/// look like [ B] and [E ].
#[cfg(test)]
mod tests {
- use super::{Selection, SelectionRange};
+ use super::*;
+
use crate::clipboard::Clipboard;
use crate::config::MockConfig;
use crate::event::{Event, EventListener};
@@ -425,7 +416,7 @@ mod tests {
#[test]
fn single_cell_left_to_right() {
let location = Point { line: 0, col: Column(0) };
- let mut selection = Selection::simple(location, Side::Left);
+ let mut selection = Selection::new(SelectionType::Simple, location, Side::Left);
selection.update(location, Side::Right);
assert_eq!(selection.to_range(&term(1, 1)).unwrap(), SelectionRange {
@@ -443,7 +434,7 @@ mod tests {
#[test]
fn single_cell_right_to_left() {
let location = Point { line: 0, col: Column(0) };
- let mut selection = Selection::simple(location, Side::Right);
+ let mut selection = Selection::new(SelectionType::Simple, location, Side::Right);
selection.update(location, Side::Left);
assert_eq!(selection.to_range(&term(1, 1)).unwrap(), SelectionRange {
@@ -460,7 +451,8 @@ mod tests {
/// 3. [ B][E ]
#[test]
fn between_adjacent_cells_left_to_right() {
- let mut selection = Selection::simple(Point::new(0, Column(0)), Side::Right);
+ let mut selection =
+ Selection::new(SelectionType::Simple, Point::new(0, Column(0)), Side::Right);
selection.update(Point::new(0, Column(1)), Side::Left);
assert_eq!(selection.to_range(&term(2, 1)), None);
@@ -473,7 +465,8 @@ mod tests {
/// 3. [ E][B ]
#[test]
fn between_adjacent_cells_right_to_left() {
- let mut selection = Selection::simple(Point::new(0, Column(1)), Side::Left);
+ let mut selection =
+ Selection::new(SelectionType::Simple, Point::new(0, Column(1)), Side::Left);
selection.update(Point::new(0, Column(0)), Side::Right);
assert_eq!(selection.to_range(&term(2, 1)), None);
@@ -489,7 +482,8 @@ mod tests {
/// [XX][XE][ ][ ][ ]
#[test]
fn across_adjacent_lines_upward_final_cell_exclusive() {
- let mut selection = Selection::simple(Point::new(1, Column(1)), Side::Right);
+ let mut selection =
+ Selection::new(SelectionType::Simple, Point::new(1, Column(1)), Side::Right);
selection.update(Point::new(0, Column(1)), Side::Right);
assert_eq!(selection.to_range(&term(5, 2)).unwrap(), SelectionRange {
@@ -511,7 +505,8 @@ mod tests {
/// [XX][XB][ ][ ][ ]
#[test]
fn selection_bigger_then_smaller() {
- let mut selection = Selection::simple(Point::new(0, Column(1)), Side::Right);
+ let mut selection =
+ Selection::new(SelectionType::Simple, Point::new(0, Column(1)), Side::Right);
selection.update(Point::new(1, Column(1)), Side::Right);
selection.update(Point::new(1, Column(0)), Side::Right);
@@ -526,7 +521,8 @@ mod tests {
fn line_selection() {
let num_lines = 10;
let num_cols = 5;
- let mut selection = Selection::lines(Point::new(0, Column(1)));
+ let mut selection =
+ Selection::new(SelectionType::Lines, Point::new(0, Column(1)), Side::Left);
selection.update(Point::new(5, Column(1)), Side::Right);
selection = selection.rotate(num_lines, num_cols, &(Line(0)..Line(num_lines)), 7).unwrap();
@@ -541,7 +537,8 @@ mod tests {
fn semantic_selection() {
let num_lines = 10;
let num_cols = 5;
- let mut selection = Selection::semantic(Point::new(0, Column(3)));
+ let mut selection =
+ Selection::new(SelectionType::Semantic, Point::new(0, Column(3)), Side::Left);
selection.update(Point::new(5, Column(1)), Side::Right);
selection = selection.rotate(num_lines, num_cols, &(Line(0)..Line(num_lines)), 7).unwrap();
@@ -556,7 +553,8 @@ mod tests {
fn simple_selection() {
let num_lines = 10;
let num_cols = 5;
- let mut selection = Selection::simple(Point::new(0, Column(3)), Side::Right);
+ let mut selection =
+ Selection::new(SelectionType::Simple, Point::new(0, Column(3)), Side::Right);
selection.update(Point::new(5, Column(1)), Side::Right);
selection = selection.rotate(num_lines, num_cols, &(Line(0)..Line(num_lines)), 7).unwrap();
@@ -571,7 +569,8 @@ mod tests {
fn block_selection() {
let num_lines = 10;
let num_cols = 5;
- let mut selection = Selection::block(Point::new(0, Column(3)), Side::Right);
+ let mut selection =
+ Selection::new(SelectionType::Block, Point::new(0, Column(3)), Side::Right);
selection.update(Point::new(5, Column(1)), Side::Right);
selection = selection.rotate(num_lines, num_cols, &(Line(0)..Line(num_lines)), 7).unwrap();
@@ -584,7 +583,8 @@ mod tests {
#[test]
fn simple_is_empty() {
- let mut selection = Selection::simple(Point::new(0, Column(0)), Side::Right);
+ let mut selection =
+ Selection::new(SelectionType::Simple, Point::new(0, Column(0)), Side::Right);
assert!(selection.is_empty());
selection.update(Point::new(0, Column(1)), Side::Left);
assert!(selection.is_empty());
@@ -594,7 +594,8 @@ mod tests {
#[test]
fn block_is_empty() {
- let mut selection = Selection::block(Point::new(0, Column(0)), Side::Right);
+ let mut selection =
+ Selection::new(SelectionType::Block, Point::new(0, Column(0)), Side::Right);
assert!(selection.is_empty());
selection.update(Point::new(0, Column(1)), Side::Left);
assert!(selection.is_empty());
@@ -612,7 +613,8 @@ mod tests {
fn rotate_in_region_up() {
let num_lines = 10;
let num_cols = 5;
- let mut selection = Selection::simple(Point::new(2, Column(3)), Side::Right);
+ let mut selection =
+ Selection::new(SelectionType::Simple, Point::new(2, Column(3)), Side::Right);
selection.update(Point::new(5, Column(1)), Side::Right);
selection =
selection.rotate(num_lines, num_cols, &(Line(1)..Line(num_lines - 1)), 4).unwrap();
@@ -628,7 +630,8 @@ mod tests {
fn rotate_in_region_down() {
let num_lines = 10;
let num_cols = 5;
- let mut selection = Selection::simple(Point::new(5, Column(3)), Side::Right);
+ let mut selection =
+ Selection::new(SelectionType::Simple, Point::new(5, Column(3)), Side::Right);
selection.update(Point::new(8, Column(1)), Side::Left);
selection =
selection.rotate(num_lines, num_cols, &(Line(1)..Line(num_lines - 1)), -5).unwrap();
@@ -644,7 +647,8 @@ mod tests {
fn rotate_in_region_up_block() {
let num_lines = 10;
let num_cols = 5;
- let mut selection = Selection::block(Point::new(2, Column(3)), Side::Right);
+ let mut selection =
+ Selection::new(SelectionType::Block, Point::new(2, Column(3)), Side::Right);
selection.update(Point::new(5, Column(1)), Side::Right);
selection =
selection.rotate(num_lines, num_cols, &(Line(1)..Line(num_lines - 1)), 4).unwrap();
diff --git a/alacritty_terminal/src/term/mod.rs b/alacritty_terminal/src/term/mod.rs
index ac5e56b5..89c3723f 100644
--- a/alacritty_terminal/src/term/mod.rs
+++ b/alacritty_terminal/src/term/mod.rs
@@ -31,10 +31,11 @@ use crate::event::{Event, EventListener};
use crate::grid::{
BidirectionalIterator, DisplayIter, Grid, GridCell, IndexRegion, Indexed, Scroll,
};
-use crate::index::{self, Column, IndexRange, Line, Point};
+use crate::index::{self, Column, IndexRange, Line, Point, Side};
use crate::selection::{Selection, SelectionRange};
use crate::term::cell::{Cell, Flags, LineLength};
use crate::term::color::Rgb;
+use crate::vi_mode::{ViModeCursor, ViMotion};
pub mod cell;
pub mod color;
@@ -180,7 +181,17 @@ impl<T> Search for Term<T> {
}
}
-/// A key for caching cursor glyphs
+/// Cursor storing all information relevant for rendering.
+#[derive(Debug, Eq, PartialEq, Copy, Clone, Deserialize)]
+struct RenderableCursor {
+ text_color: Option<Rgb>,
+ cursor_color: Option<Rgb>,
+ key: CursorKey,
+ point: Point,
+ rendered: bool,
+}
+
+/// A key for caching cursor glyphs.
#[derive(Debug, Eq, PartialEq, Copy, Clone, Hash, Deserialize)]
pub struct CursorKey {
pub style: CursorStyle,
@@ -198,10 +209,7 @@ pub struct CursorKey {
pub struct RenderableCellsIter<'a, C> {
inner: DisplayIter<'a, Cell>,
grid: &'a Grid<Cell>,
- cursor: &'a Point,
- cursor_offset: usize,
- cursor_key: Option<CursorKey>,
- cursor_style: CursorStyle,
+ cursor: RenderableCursor,
config: &'a Config<C>,
colors: &'a color::List,
selection: Option<SelectionRange<Line>>,
@@ -216,12 +224,10 @@ impl<'a, C> RenderableCellsIter<'a, C> {
term: &'b Term<T>,
config: &'b Config<C>,
selection: Option<SelectionRange>,
- mut cursor_style: CursorStyle,
) -> RenderableCellsIter<'b, C> {
let grid = &term.grid;
let num_cols = grid.num_cols();
- let cursor_offset = grid.num_lines().0 - term.cursor.point.line.0 - 1;
let inner = grid.display_iter();
let selection_range = selection.and_then(|span| {
@@ -242,29 +248,13 @@ impl<'a, C> RenderableCellsIter<'a, C> {
Some(SelectionRange::new(start, end, span.is_block))
});
- // Load cursor glyph
- let cursor = &term.cursor.point;
- let cursor_visible = term.mode.contains(TermMode::SHOW_CURSOR) && grid.contains(cursor);
- let cursor_key = if cursor_visible {
- let is_wide =
- grid[cursor].flags.contains(Flags::WIDE_CHAR) && (cursor.col + 1) < num_cols;
- Some(CursorKey { style: cursor_style, is_wide })
- } else {
- // Use hidden cursor so text will not get inverted
- cursor_style = CursorStyle::Hidden;
- None
- };
-
RenderableCellsIter {
- cursor,
- cursor_offset,
+ cursor: term.renderable_cursor(config),
grid,
inner,
selection: selection_range,
config,
colors: &term.colors,
- cursor_key,
- cursor_style,
}
}
@@ -275,6 +265,18 @@ impl<'a, C> RenderableCellsIter<'a, C> {
None => return false,
};
+ // Do not invert block cursor at selection boundaries
+ if self.cursor.key.style == CursorStyle::Block
+ && self.cursor.point == point
+ && (selection.start == point
+ || selection.end == point
+ || (selection.is_block
+ && ((selection.start.line == point.line && selection.end.col == point.col)
+ || (selection.end.line == point.line && selection.start.col == point.col))))
+ {
+ return false;
+ }
+
// Point itself is selected
if selection.contains(point.col, point.line) {
return true;
@@ -442,43 +444,46 @@ impl<'a, C> Iterator for RenderableCellsIter<'a, C> {
#[inline]
fn next(&mut self) -> Option<Self::Item> {
loop {
- if self.cursor_offset == self.inner.offset() && self.inner.column() == self.cursor.col {
- let selected = self.is_selected(Point::new(self.cursor.line, self.cursor.col));
+ if self.cursor.point.line == self.inner.line()
+ && self.cursor.point.col == self.inner.column()
+ {
+ let selected = self.is_selected(self.cursor.point);
+
+ // Handle cell below cursor
+ if self.cursor.rendered {
+ let mut cell =
+ RenderableCell::new(self.config, self.colors, self.inner.next()?, selected);
- // Handle cursor
- if let Some(cursor_key) = self.cursor_key.take() {
+ if self.cursor.key.style == CursorStyle::Block {
+ mem::swap(&mut cell.bg, &mut cell.fg);
+
+ if let Some(color) = self.cursor.text_color {
+ cell.fg = color;
+ }
+ }
+
+ return Some(cell);
+ } else {
+ // Handle cursor
+ self.cursor.rendered = true;
+
+ let buffer_point = self.grid.visible_to_buffer(self.cursor.point);
let cell = Indexed {
- inner: self.grid[self.cursor],
- column: self.cursor.col,
- // Using `self.cursor.line` leads to inconsitent cursor position when
- // scrolling. See https://github.com/alacritty/alacritty/issues/2570 for more
- // info.
- line: self.inner.line(),
+ inner: self.grid[buffer_point.line][buffer_point.col],
+ column: self.cursor.point.col,
+ line: self.cursor.point.line,
};
let mut renderable_cell =
RenderableCell::new(self.config, self.colors, cell, selected);
- renderable_cell.inner = RenderableCellContent::Cursor(cursor_key);
+ renderable_cell.inner = RenderableCellContent::Cursor(self.cursor.key);
- if let Some(color) = self.config.cursor_cursor_color() {
- renderable_cell.fg = RenderableCell::compute_bg_rgb(self.colors, color);
+ if let Some(color) = self.cursor.cursor_color {
+ renderable_cell.fg = color;
}
return Some(renderable_cell);
- } else {
- let mut cell =
- RenderableCell::new(self.config, self.colors, self.inner.next()?, selected);
-
- if self.cursor_style == CursorStyle::Block {
- std::mem::swap(&mut cell.bg, &mut cell.fg);
-
- if let Some(color) = self.config.cursor_text_color() {
- cell.fg = color;
- }
- }
-
- return Some(cell);
}
} else {
let cell = self.inner.next()?;
@@ -497,26 +502,27 @@ pub mod mode {
use bitflags::bitflags;
bitflags! {
- pub struct TermMode: u16 {
- const SHOW_CURSOR = 0b0000_0000_0000_0001;
- const APP_CURSOR = 0b0000_0000_0000_0010;
- const APP_KEYPAD = 0b0000_0000_0000_0100;
- const MOUSE_REPORT_CLICK = 0b0000_0000_0000_1000;
- const BRACKETED_PASTE = 0b0000_0000_0001_0000;
- const SGR_MOUSE = 0b0000_0000_0010_0000;
- const MOUSE_MOTION = 0b0000_0000_0100_0000;
- const LINE_WRAP = 0b0000_0000_1000_0000;
- const LINE_FEED_NEW_LINE = 0b0000_0001_0000_0000;
- const ORIGIN = 0b0000_0010_0000_0000;
- const INSERT = 0b0000_0100_0000_0000;
- const FOCUS_IN_OUT = 0b0000_1000_0000_0000;
- const ALT_SCREEN = 0b0001_0000_0000_0000;
- const MOUSE_DRAG = 0b0010_0000_0000_0000;
- const MOUSE_MODE = 0b0010_0000_0100_1000;
- const UTF8_MOUSE = 0b0100_0000_0000_0000;
- const ALTERNATE_SCROLL = 0b1000_0000_0000_0000;
- const ANY = 0b1111_1111_1111_1111;
+ pub struct TermMode: u32 {
const NONE = 0;
+ const SHOW_CURSOR = 0b0000_0000_0000_0000_0001;
+ const APP_CURSOR = 0b0000_0000_0000_0000_0010;
+ const APP_KEYPAD = 0b0000_0000_0000_0000_0100;
+ const MOUSE_REPORT_CLICK = 0b0000_0000_0000_0000_1000;
+ const BRACKETED_PASTE = 0b0000_0000_0000_0001_0000;
+ const SGR_MOUSE = 0b0000_0000_0000_0010_0000;
+ const MOUSE_MOTION = 0b0000_0000_0000_0100_0000;
+ const LINE_WRAP = 0b0000_0000_0000_1000_0000;
+ const LINE_FEED_NEW_LINE = 0b0000_0000_0001_0000_0000;
+ const ORIGIN = 0b0000_0000_0010_0000_0000;
+ const INSERT = 0b0000_0000_0100_0000_0000;
+ const FOCUS_IN_OUT = 0b0000_0000_1000_0000_0000;
+ const ALT_SCREEN = 0b0000_0001_0000_0000_0000;
+ const MOUSE_DRAG = 0b0000_0010_0000_0000_0000;
+ const MOUSE_MODE = 0b0000_0010_0000_0100_1000;
+ const UTF8_MOUSE = 0b0000_0100_0000_0000_0000;
+ const ALTERNATE_SCROLL = 0b0000_1000_0000_0000_0000;
+ const VI = 0b0001_0000_0000_0000_0000;
+ const ANY = std::u32::MAX;
}
}
@@ -730,11 +736,69 @@ impl VisualBell {
}
}
+/// Terminal size info.
+#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq)]
+pub struct SizeInfo {
+ /// Terminal window width.
+ pub width: f32,
+
+ /// Terminal window height.
+ pub height: f32,
+
+ /// Width of individual cell.
+ pub cell_width: f32,
+
+ /// Height of individual cell.
+ pub cell_height: f32,
+
+ /// Horizontal window padding.
+ pub padding_x: f32,
+
+ /// Horizontal window padding.
+ pub padding_y: f32,
+
+ /// DPI factor of the current window.
+ #[serde(default)]
+ pub dpr: f64,
+}
+
+impl SizeInfo {
+ #[inline]
+ pub fn lines(&self) -> Line {
+ Line(((self.height - 2. * self.padding_y) / self.cell_height) as usize)
+ }
+
+ #[inline]
+ pub fn cols(&self) -> Column {
+ Column(((self.width - 2. * self.padding_x) / self.cell_width) as usize)
+ }
+
+ /// Check if coordinates are inside the terminal grid.
+ ///
+ /// The padding is not counted as part of the grid.
+ pub fn contains_point(&self, x: usize, y: usize) -> bool {
+ x < (self.width - self.padding_x) as usize
+ && x >= self.padding_x as usize
+ && y < (self.height - self.padding_y) as usize
+ && y >= self.padding_y as usize
+ }
+
+ pub fn pixels_to_coords(&self, x: usize, y: usize) -> Point {
+ let col = Column(x.saturating_sub(self.padding_x as usize) / (self.cell_width as usize));
+ let line = Line(y.saturating_sub(self.padding_y as usize) / (self.cell_height as usize));
+
+ Point {
+ line: min(line, Line(self.lines().saturating_sub(1))),
+ col: min(col, Column(self.cols().saturating_sub(1))),
+ }
+ }
+}
+
pub struct Term<T> {
- /// Terminal focus
+ /// Terminal focus.
pub is_focused: bool,
- /// The grid
+ /// The grid.
grid: Grid<Cell>,
/// Tracks if the next call to input will need to first handle wrapping.
@@ -744,23 +808,25 @@ pub struct Term<T> {
/// arrays. Without it we would have to sanitize cursor.col every time we used it.
input_needs_wrap: bool,
- /// Alternate grid
+ /// Alternate grid.
alt_grid: Grid<Cell>,
- /// Alt is active
+ /// Alt is active.
alt: bool,
- /// The cursor
+ /// The cursor.
cursor: Cursor,
- /// The graphic character set, out of `charsets`, which ASCII is currently
- /// being mapped to
+ /// Cursor location for vi mode.
+ pub vi_mode_cursor: ViModeCursor,
+
+ /// Index into `charsets`, pointing to what ASCII is currently being mapped to.
active_charset: CharsetIndex,
- /// Tabstops
+ /// Tabstops.
tabs: TabStops,
- /// Mode flags
+ /// Mode flags.
mode: TermMode,
/// Scroll region.
@@ -772,33 +838,36 @@ pub struct Term<T> {
pub visual_bell: VisualBell,
- /// Saved cursor from main grid
+ /// Saved cursor from main grid.
cursor_save: Cursor,
- /// Saved cursor from alt grid
+ /// Saved cursor from alt grid.
cursor_save_alt: Cursor,
semantic_escape_chars: String,
- /// Colors used for rendering
+ /// Colors used for rendering.
colors: color::List,
- /// Is color in `colors` modified or not
+ /// Is color in `colors` modified or not.
color_modified: [bool; color::COUNT],
- /// Original colors from config
+ /// Original colors from config.
original_colors: color::List,
- /// Current style of the cursor
+ /// Current style of the cursor.
cursor_style: Option<CursorStyle>,
- /// Default style for resetting the cursor
+ /// Default style for resetting the cursor.
default_cursor_style: CursorStyle,
+ /// Style of the vi mode cursor.
+ vi_mode_cursor_style: Option<CursorStyle>,
+
/// Clipboard access coupled to the active window
clipboard: Clipboard,
- /// Proxy for sending events to the event loop
+ /// Proxy for sending events to the event loop.
event_proxy: T,
/// Current title of the window.
@@ -815,64 +884,6 @@ pub struct Term<T> {
title_stack: Vec<Option<String>>,
}
-/// Terminal size info
-#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq)]
-pub struct SizeInfo {
- /// Terminal window width
- pub width: f32,
-
- /// Terminal window height
- pub height: f32,
-
- /// Width of individual cell
- pub cell_width: f32,
-
- /// Height of individual cell
- pub cell_height: f32,
-
- /// Horizontal window padding
- pub padding_x: f32,
-
- /// Horizontal window padding
- pub padding_y: f32,
-
- /// DPI factor of the current window
- #[serde(default)]
- pub dpr: f64,
-}
-
-impl SizeInfo {
- #[inline]
- pub fn lines(&self) -> Line {
- Line(((self.height - 2. * self.padding_y) / self.cell_height) as usize)
- }
-
- #[inline]
- pub fn cols(&self) -> Column {
- Column(((self.width - 2. * self.padding_x) / self.cell_width) as usize)
- }
-
- /// Check if coordinates are inside the terminal grid.
- ///
- /// The padding is not counted as part of the grid.
- pub fn contains_point(&self, x: usize, y: usize) -> bool {
- x < (self.width - self.padding_x) as usize
- && x >= self.padding_x as usize
- && y < (self.height - self.padding_y) as usize
- && y >= self.padding_y as usize
- }
-
- pub fn pixels_to_coords(&self, x: usize, y: usize) -> Point {
- let col = Column(x.saturating_sub(self.padding_x as usize) / (self.cell_width as usize));
- let line = Line(y.saturating_sub(self.padding_y as usize) / (self.cell_height as usize));
-
- Point {
- line: min(line, Line(self.lines().saturating_sub(1))),
- col: min(col, Column(self.cols().saturating_sub(1))),
- }
- }
-}
-
impl<T> Term<T> {
pub fn selection(&self) -> &Option<Selection> {
&self.grid.selection
@@ -920,6 +931,7 @@ impl<T> Term<T> {
alt: false,
active_charset: Default::default(),
cursor: Default::default(),
+ vi_mode_cursor: Default::default(),
cursor_save: Default::default(),
cursor_save_alt: Default::default(),
tabs,
@@ -931,6 +943,7 @@ impl<T> Term<T> {
semantic_escape_chars: config.selection.semantic_escape_chars().to_owned(),
cursor_style: None,
default_cursor_style: config.cursor.style,
+ vi_mode_cursor_style: config.cursor.vi_mode_style,
dynamic_title: config.dynamic_title(),
clipboard,
event_proxy,
@@ -959,6 +972,7 @@ impl<T> Term<T> {
self.mode.remove(TermMode::ALTERNATE_SCROLL);
}
self.default_cursor_style = config.cursor.style;
+ self.vi_mode_cursor_style = config.cursor.vi_mode_style;
self.default_title = config.window.title.clone();
self.dynamic_title = config.dynamic_title();
@@ -1105,13 +1119,7 @@ impl<T> Term<T> {
pub fn renderable_cells<'b, C>(&'b self, config: &'b Config<C>) -> RenderableCellsIter<'_, C> {
let selection = self.grid.selection.as_ref().and_then(|s| s.to_range(self));
- let cursor = if self.is_focused || !config.cursor.unfocused_hollow() {
- self.cursor_style.unwrap_or(self.default_cursor_style)
- } else {
- CursorStyle::HollowBlock
- };
-
- RenderableCellsIter::new(&self, config, selection, cursor)
+ RenderableCellsIter::new(&self, config, selection)
}
/// Resize terminal to new dimensions
@@ -1129,12 +1137,12 @@ impl<T> Term<T> {
self.grid.selection = None;
self.alt_grid.selection = None;
- // Should not allow less than 1 col, causes all sorts of checks to be required.
+ // Should not allow less than 2 cols, causes all sorts of checks to be required.
if num_cols <= Column(1) {
num_cols = Column(2);
}
- // Should not allow less than 1 line, causes all sorts of checks to be required.
+ // Should not allow less than 2 lines, causes all sorts of checks to be required.
if num_lines <= Line(1) {
num_lines = Line(2);
}
@@ -1178,6 +1186,8 @@ impl<T> Term<T> {
self.cursor_save.point.line = min(self.cursor_save.point.line, num_lines - 1);
self.cursor_save_alt.point.col = min(self.cursor_save_alt.point.col, num_cols - 1);
self.cursor_save_alt.point.line = min(self.cursor_save_alt.point.line, num_lines - 1);
+ self.vi_mode_cursor.point.col = min(self.vi_mode_cursor.point.col, num_cols - 1);
+ self.vi_mode_cursor.point.line = min(self.vi_mode_cursor.point.line, num_lines - 1);
// Recreate tabs list
self.tabs.resize(self.grid.num_cols());
@@ -1200,7 +1210,7 @@ impl<T> Term<T> {
}
self.alt = !self.alt;
- std::mem::swap(&mut self.grid, &mut self.alt_grid);
+ mem::swap(&mut self.grid, &mut self.alt_grid);
}
/// Scroll screen down
@@ -1258,10 +1268,58 @@ impl<T> Term<T> {
self.event_proxy.send_event(Event::Exit);
}
+ #[inline]
pub fn clipboard(&mut self) -> &mut Clipboard {
&mut self.clipboard
}
+ /// Toggle the vi mode.
+ #[inline]
+ pub fn toggle_vi_mode(&mut self) {
+ self.mode ^= TermMode::VI;
+ self.grid.selection = None;
+
+ // Reset vi mode cursor position to match primary cursor
+ if self.mode.contains(TermMode::VI) {
+ let line = min(self.cursor.point.line + self.grid.display_offset(), self.lines() - 1);
+ self.vi_mode_cursor = ViModeCursor::new(Point::new(line, self.cursor.point.col));
+ }
+
+ self.dirty = true;
+ }
+
+ /// Move vi mode cursor.
+ #[inline]
+ pub fn vi_motion(&mut self, motion: ViMotion)
+ where
+ T: EventListener,
+ {
+ // Require vi mode to be active
+ if !self.mode.contains(TermMode::VI) {
+ return;
+ }
+
+ // Move cursor
+ self.vi_mode_cursor = self.vi_mode_cursor.motion(self, motion);
+
+ // Update selection if one is active
+ let viewport_point = self.visible_to_buffer(self.vi_mode_cursor.point);
+ if let Some(selection) = &mut self.grid.selection {
+ // Do not extend empty selections started by single mouse click
+ if !selection.is_empty() {
+ selection.update(viewport_point, Side::Left);
+ selection.include_all();
+ }
+ }
+
+ self.dirty = true;
+ }
+
+ #[inline]
+ pub fn semantic_escape_chars(&self) -> &str {
+ &self.semantic_escape_chars
+ }
+
/// Insert a linebreak at the current cursor position.
#[inline]
fn wrapline(&mut self)
@@ -1297,6 +1355,65 @@ impl<T> Term<T> {
cell.c = self.cursor.charsets[self.active_charset].map(c);
cell
}
+
+ /// Get rendering information about the active cursor.
+ fn renderable_cursor<C>(&self, config: &Config<C>) -> RenderableCursor {
+ let vi_mode = self.mode.contains(TermMode::VI);
+
+ // Cursor position
+ let mut point = if vi_mode {
+ self.vi_mode_cursor.point
+ } else {
+ let mut point = self.cursor.point;
+ point.line += self.grid.display_offset();
+ point
+ };
+
+ // Cursor shape
+ let hidden = !self.mode.contains(TermMode::SHOW_CURSOR) || point.line >= self.lines();
+ let cursor_style = if hidden && !vi_mode {
+ point.line = Line(0);
+ CursorStyle::Hidden
+ } else if !self.is_focused && config.cursor.unfocused_hollow() {
+ CursorStyle::HollowBlock
+ } else {
+ let cursor_style = self.cursor_style.unwrap_or(self.default_cursor_style);
+
+ if vi_mode {
+ self.vi_mode_cursor_style.unwrap_or(cursor_style)
+ } else {
+ cursor_style
+ }
+ };
+
+ // Cursor colors
+ let (text_color, cursor_color) = if vi_mode {
+ (config.vi_mode_cursor_text_color(), config.vi_mode_cursor_cursor_color())
+ } else {
+ let cursor_cursor_color = config.cursor_cursor_color().map(|c| self.colors[c]);
+ (config.cursor_text_color(), cursor_cursor_color)
+ };
+
+ // Expand across wide cell when inside wide char or spacer
+ let buffer_point = self.visible_to_buffer(point);
+ let cell = self.grid[buffer_point.line][buffer_point.col];
+ let is_wide = if cell.flags.contains(Flags::WIDE_CHAR_SPACER)
+ && self.grid[buffer_point.line][buffer_point.col - 1].flags.contains(Flags::WIDE_CHAR)
+ {
+ point.col -= 1;
+ true
+ } else {
+ cell.flags.contains(Flags::WIDE_CHAR)
+ };
+
+ RenderableCursor {
+ text_color,
+ cursor_color,
+ key: CursorKey { style: cursor_style, is_wide },
+ point,
+ rendered: false,
+ }
+ }
}
impl<T> TermInfo for Term<T> {
@@ -2184,7 +2301,7 @@ mod tests {
use crate::event::{Event, EventListener};
use crate::grid::{Grid, Scroll};
use crate::index::{Column, Line, Point, Side};
- use crate::selection::Selection;
+ use crate::selection::{Selection, SelectionType};
use crate::term::cell::{Cell, Flags};
use crate::term::{SizeInfo, Term};
@@ -2222,17 +2339,29 @@ mod tests {
mem::swap(&mut term.semantic_escape_chars, &mut escape_chars);
{
- *term.selection_mut() = Some(Selection::semantic(Point { line: 2, col: Column(1) }));
+ *term.selection_mut() = Some(Selection::new(
+ SelectionType::Semantic,
+ Point { line: 2, col: Column(1) },
+ Side::Left,
+ ));
assert_eq!(term.selection_to_string(), Some(String::from("aa")));
}
{
- *term.selection_mut() = Some(Selection::semantic(Point { line: 2, col: Column(4) }));
+ *term.selection_mut() = Some(Selection::new(
+ SelectionType::Semantic,
+ Point { line: 2, col: Column(4) },
+ Side::Left,
+ ));
assert_eq!(term.selection_to_string(), Some(String::from("aaa")));
}
{
- *term.selection_mut() = Some(Selection::semantic(Point { line: 1, col: Column(1) }));
+ *term.selection_mut() = Some(Selection::new(
+ SelectionType::Semantic,
+ Point { line: 1, col: Column(1) },
+ Side::Left,
+ ));
assert_eq!(term.selection_to_string(), Some(String::from("aaa")));
}
}
@@ -2258,7 +2387,11 @@ mod tests {
mem::swap(&mut term.grid, &mut grid);
- *term.selection_mut() = Some(Selection::lines(Point { line: 0, col: Column(3) }));
+ *term.selection_mut() = Some(Selection::new(
+ SelectionType::Lines,
+ Point { line: 0, col: Column(3) },
+ Side::Left,
+ ));
assert_eq!(term.selection_to_string(), Some(String::from("\"aa\"a\n")));
}
@@ -2285,7 +2418,8 @@ mod tests {
mem::swap(&mut term.grid, &mut grid);
- let mut selection = Selection::simple(Point { line: 2, col: Column(0) }, Side::Left);
+ let mut selection =
+ Selection::new(SelectionType::Simple, Point { line: 2, col: Column(0) }, Side::Left);
selection.update(Point { line: 0, col: Column(2) }, Side::Right);
*term.selection_mut() = Some(selection);
assert_eq!(term.selection_to_string(), Some("aaa\n\naaa\n".into()));
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)));
+ }
+}