From 1a8cd172e520e493bacc9c6a2ae6f80de086eaa3 Mon Sep 17 00:00:00 2001 From: Christian Duerr Date: Wed, 18 Mar 2020 02:35:08 +0000 Subject: 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. --- alacritty_terminal/src/config/colors.rs | 2 + alacritty_terminal/src/config/mod.rs | 32 +- alacritty_terminal/src/grid/mod.rs | 12 +- alacritty_terminal/src/index.rs | 41 +- alacritty_terminal/src/lib.rs | 1 + alacritty_terminal/src/selection.rs | 106 +++-- alacritty_terminal/src/term/mod.rs | 456 +++++++++++------- alacritty_terminal/src/vi_mode.rs | 799 ++++++++++++++++++++++++++++++++ 8 files changed, 1215 insertions(+), 234 deletions(-) create mode 100644 alacritty_terminal/src/vi_mode.rs (limited to 'alacritty_terminal') 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 Config { self.dynamic_title.0 } - /// Cursor foreground color + /// Cursor foreground color. #[inline] pub fn cursor_text_color(&self) -> Option { self.colors.cursor.text } - /// Cursor background color + /// Cursor background color. #[inline] - pub fn cursor_cursor_color(&self) -> Option { - self.colors.cursor.cursor.map(|_| Color::Named(NamedColor::Cursor)) + pub fn cursor_cursor_color(&self) -> Option { + self.colors.cursor.cursor.map(|_| NamedColor::Cursor) + } + + /// Vi mode cursor foreground color. + #[inline] + pub fn vi_mode_cursor_text_color(&self) -> Option { + self.colors.vi_mode_cursor.text + } + + /// Vi mode cursor background color. + #[inline] + pub fn vi_mode_cursor_cursor_color(&self) -> Option { + 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, #[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 Grid { let mut new_empty_lines = 0; let mut reversed: Vec> = 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 Grid { } 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 { @@ -49,7 +58,7 @@ impl Point { L: Copy + Default + Into + Add + Sub, { 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 Point { #[must_use = "this returns the result of the operation, without modifying the original"] pub fn add(mut self, num_cols: usize, rhs: usize) -> Point where - L: Add + Sub, + L: Copy + Default + Into + Add + Sub, { 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 + where + L: Copy + Default + Into + Add + Sub, + { + 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 + where + L: Copy + Default + Into + Add + Sub, + { + 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, side: Side, } @@ -67,7 +67,7 @@ impl SelectionRange { /// 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, - ty: SelectionType, } impl Selection { - pub fn simple(location: Point, side: Side) -> Selection { + pub fn new(ty: SelectionType, location: Point, 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, side: Side) -> Selection { - Self { - region: Range { start: Anchor::new(location, side), end: Anchor::new(location, side) }, - ty: SelectionType::Block, - } - } - - pub fn semantic(location: Point) -> Selection { - Self { - region: Range { - start: Anchor::new(location, Side::Left), - end: Anchor::new(location, Side::Right), - }, - ty: SelectionType::Semantic, - } - } - - pub fn lines(location: Point) -> 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, side: Side) { - self.region.end.point = location; - self.region.end.side = side; + pub fn update(&mut self, point: Point, 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(&self, term: &Term) -> Option { 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 Search for Term { } } -/// 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, + cursor_color: Option, + 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, - cursor: &'a Point, - cursor_offset: usize, - cursor_key: Option, - cursor_style: CursorStyle, + cursor: RenderableCursor, config: &'a Config, colors: &'a color::List, selection: Option>, @@ -216,12 +224,10 @@ impl<'a, C> RenderableCellsIter<'a, C> { term: &'b Term, config: &'b Config, selection: Option, - 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 { 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 { - /// Terminal focus + /// Terminal focus. pub is_focused: bool, - /// The grid + /// The grid. grid: Grid, /// Tracks if the next call to input will need to first handle wrapping. @@ -744,23 +808,25 @@ pub struct Term { /// 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, - /// 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 { 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, - /// 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, + /// 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 { title_stack: Vec>, } -/// 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 Term { pub fn selection(&self) -> &Option { &self.grid.selection @@ -920,6 +931,7 @@ impl Term { 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 Term { 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 Term { 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 Term { pub fn renderable_cells<'b, C>(&'b self, config: &'b Config) -> 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 Term { 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 Term { 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 Term { } 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 Term { 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 Term { cell.c = self.cursor.charsets[self.active_charset].map(c); cell } + + /// Get rendering information about the active cursor. + fn renderable_cursor(&self, config: &Config) -> 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 TermInfo for Term { @@ -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(mut self, term: &mut Term, 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(mut self, term: &Term, 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(term: &mut Term, point: Point) { + 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(term: &Term, mut point: Point) -> Point { + 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(term: &Term, mut point: Point) -> Point { + 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( + term: &mut Term, + mut point: Point, + left: bool, + start: bool, +) -> Point { + // Expand semantically based on movement direction + let expand_semantic = |point: Point| { + // 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( + term: &mut Term, + mut point: Point, + left: bool, + start: bool, +) -> Point { + // 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(term: &Term, point: P, left: bool) -> Point +where + P: Into>, +{ + 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(term: &Term, line: usize) -> Option> { + (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(term: &Term, line: usize) -> Option> { + (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(term: &Term, point: Point, left: bool) -> Point { + 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(term: &Term, point: Point) -> bool { + let cell = term.grid()[point.line][point.col]; + cell.c == ' ' || cell.c == '\t' && !cell.flags().contains(Flags::WIDE_CHAR_SPACER) +} + +fn is_wrap(term: &Term, point: Point) -> bool { + term.grid()[point.line][point.col].flags.contains(Flags::WRAPLINE) +} + +/// Check if point is at screen boundary. +fn is_boundary(term: &Term, point: Point, 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 { + 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 { + 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))); + } +} -- cgit v1.2.3-54-g00ecf