diff options
Diffstat (limited to 'alacritty_terminal')
-rw-r--r-- | alacritty_terminal/src/config/colors.rs | 2 | ||||
-rw-r--r-- | alacritty_terminal/src/config/mod.rs | 32 | ||||
-rw-r--r-- | alacritty_terminal/src/grid/mod.rs | 12 | ||||
-rw-r--r-- | alacritty_terminal/src/index.rs | 41 | ||||
-rw-r--r-- | alacritty_terminal/src/lib.rs | 1 | ||||
-rw-r--r-- | alacritty_terminal/src/selection.rs | 106 | ||||
-rw-r--r-- | alacritty_terminal/src/term/mod.rs | 456 | ||||
-rw-r--r-- | alacritty_terminal/src/vi_mode.rs | 799 |
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))); + } +} |