summaryrefslogtreecommitdiff
path: root/alacritty_terminal
diff options
context:
space:
mode:
Diffstat (limited to 'alacritty_terminal')
-rw-r--r--alacritty_terminal/Cargo.toml1
-rw-r--r--alacritty_terminal/src/ansi.rs34
-rw-r--r--alacritty_terminal/src/config/colors.rs124
-rw-r--r--alacritty_terminal/src/config/mod.rs27
-rw-r--r--alacritty_terminal/src/grid/mod.rs209
-rw-r--r--alacritty_terminal/src/grid/resize.rs26
-rw-r--r--alacritty_terminal/src/grid/storage.rs4
-rw-r--r--alacritty_terminal/src/grid/tests.rs19
-rw-r--r--alacritty_terminal/src/index.rs177
-rw-r--r--alacritty_terminal/src/selection.rs79
-rw-r--r--alacritty_terminal/src/term/cell.rs28
-rw-r--r--alacritty_terminal/src/term/color.rs87
-rw-r--r--alacritty_terminal/src/term/mod.rs677
-rw-r--r--alacritty_terminal/src/term/search.rs794
-rw-r--r--alacritty_terminal/src/vi_mode.rs182
-rw-r--r--alacritty_terminal/tests/ref.rs9
16 files changed, 1797 insertions, 680 deletions
diff --git a/alacritty_terminal/Cargo.toml b/alacritty_terminal/Cargo.toml
index 65a12c6f..9345a76f 100644
--- a/alacritty_terminal/Cargo.toml
+++ b/alacritty_terminal/Cargo.toml
@@ -22,6 +22,7 @@ log = "0.4"
unicode-width = "0.1"
base64 = "0.12.0"
terminfo = "0.7.1"
+regex-automata = "0.1.9"
[target.'cfg(unix)'.dependencies]
nix = "0.17.0"
diff --git a/alacritty_terminal/src/ansi.rs b/alacritty_terminal/src/ansi.rs
index 8240ff00..5f24dcff 100644
--- a/alacritty_terminal/src/ansi.rs
+++ b/alacritty_terminal/src/ansi.rs
@@ -89,13 +89,13 @@ struct ProcessorState {
///
/// Processor creates a Performer when running advance and passes the Performer
/// to `vte::Parser`.
-struct Performer<'a, H: Handler + TermInfo, W: io::Write> {
+struct Performer<'a, H: Handler, W: io::Write> {
state: &'a mut ProcessorState,
handler: &'a mut H,
writer: &'a mut W,
}
-impl<'a, H: Handler + TermInfo + 'a, W: io::Write> Performer<'a, H, W> {
+impl<'a, H: Handler + 'a, W: io::Write> Performer<'a, H, W> {
/// Create a performer.
#[inline]
pub fn new<'b>(
@@ -121,7 +121,7 @@ impl Processor {
#[inline]
pub fn advance<H, W>(&mut self, handler: &mut H, byte: u8, writer: &mut W)
where
- H: Handler + TermInfo,
+ H: Handler,
W: io::Write,
{
let mut performer = Performer::new(&mut self.state, handler, writer);
@@ -129,12 +129,6 @@ impl Processor {
}
}
-/// Trait that provides properties of terminal.
-pub trait TermInfo {
- fn lines(&self) -> Line;
- fn cols(&self) -> Column;
-}
-
/// Type that handles actions from the parser.
///
/// XXX Should probably not provide default impls for everything, but it makes
@@ -278,7 +272,7 @@ pub trait Handler {
fn unset_mode(&mut self, _: Mode) {}
/// DECSTBM - Set the terminal scrolling region.
- fn set_scrolling_region(&mut self, _top: usize, _bottom: usize) {}
+ fn set_scrolling_region(&mut self, _top: usize, _bottom: Option<usize>) {}
/// DECKPAM - Set keypad to applications mode (ESCape instead of digits).
fn set_keypad_application_mode(&mut self) {}
@@ -731,7 +725,7 @@ impl StandardCharset {
impl<'a, H, W> vte::Perform for Performer<'a, H, W>
where
- H: Handler + TermInfo + 'a,
+ H: Handler + 'a,
W: io::Write + 'a,
{
#[inline]
@@ -945,9 +939,7 @@ where
macro_rules! arg_or_default {
(idx: $idx:expr, default: $default:expr) => {
- args.get($idx)
- .and_then(|v| if *v == 0 { None } else { Some(*v) })
- .unwrap_or($default)
+ args.get($idx).copied().filter(|&v| v != 0).unwrap_or($default)
};
}
@@ -1099,7 +1091,7 @@ where
},
('r', None) => {
let top = arg_or_default!(idx: 0, default: 1) as usize;
- let bottom = arg_or_default!(idx: 1, default: handler.lines().0 as _) as usize;
+ let bottom = args.get(1).map(|&b| b as usize).filter(|&b| b != 0);
handler.set_scrolling_region(top, bottom);
},
@@ -1391,9 +1383,7 @@ pub mod C0 {
mod tests {
use super::{
parse_number, xparse_color, Attr, CharsetIndex, Color, Handler, Processor, StandardCharset,
- TermInfo,
};
- use crate::index::{Column, Line};
use crate::term::color::Rgb;
use std::io;
@@ -1427,16 +1417,6 @@ mod tests {
}
}
- impl TermInfo for MockHandler {
- fn lines(&self) -> Line {
- Line(200)
- }
-
- fn cols(&self) -> Column {
- Column(90)
- }
- }
-
impl Default for MockHandler {
fn default() -> MockHandler {
MockHandler {
diff --git a/alacritty_terminal/src/config/colors.rs b/alacritty_terminal/src/config/colors.rs
index ccea9536..13a30bef 100644
--- a/alacritty_terminal/src/config/colors.rs
+++ b/alacritty_terminal/src/config/colors.rs
@@ -1,8 +1,9 @@
use log::error;
use serde::{Deserialize, Deserializer};
+use serde_yaml::Value;
use crate::config::{failure_default, LOG_TARGET_CONFIG};
-use crate::term::color::Rgb;
+use crate::term::color::{CellRgb, Rgb};
#[serde(default)]
#[derive(Deserialize, Clone, Debug, Default, PartialEq, Eq)]
@@ -23,6 +24,8 @@ pub struct Colors {
pub dim: Option<AnsiColors>,
#[serde(deserialize_with = "failure_default")]
pub indexed_colors: Vec<IndexedColor>,
+ #[serde(deserialize_with = "failure_default")]
+ pub search: SearchColors,
}
impl Colors {
@@ -33,6 +36,32 @@ impl Colors {
pub fn bright(&self) -> &AnsiColors {
&self.bright.0
}
+
+ pub fn search_bar_foreground(&self) -> Rgb {
+ self.search.bar.foreground.unwrap_or(self.primary.background)
+ }
+
+ pub fn search_bar_background(&self) -> Rgb {
+ self.search.bar.background.unwrap_or(self.primary.foreground)
+ }
+}
+
+#[derive(Deserialize, Copy, Clone, Debug, PartialEq, Eq)]
+struct DefaultForegroundCellRgb(CellRgb);
+
+impl Default for DefaultForegroundCellRgb {
+ fn default() -> Self {
+ Self(CellRgb::CellForeground)
+ }
+}
+
+#[derive(Deserialize, Copy, Clone, Debug, PartialEq, Eq)]
+struct DefaultBackgroundCellRgb(CellRgb);
+
+impl Default for DefaultBackgroundCellRgb {
+ fn default() -> Self {
+ Self(CellRgb::CellBackground)
+ }
}
#[serde(default)]
@@ -44,11 +73,11 @@ pub struct IndexedColor {
pub color: Rgb,
}
-fn deserialize_color_index<'a, D>(deserializer: D) -> ::std::result::Result<u8, D::Error>
+fn deserialize_color_index<'a, D>(deserializer: D) -> Result<u8, D::Error>
where
D: Deserializer<'a>,
{
- let value = serde_yaml::Value::deserialize(deserializer)?;
+ let value = Value::deserialize(deserializer)?;
match u8::deserialize(value) {
Ok(index) => {
if index < 16 {
@@ -78,26 +107,91 @@ where
#[derive(Deserialize, Debug, Copy, Clone, Default, PartialEq, Eq)]
pub struct CursorColors {
#[serde(deserialize_with = "failure_default")]
- pub text: Option<Rgb>,
+ text: DefaultBackgroundCellRgb,
#[serde(deserialize_with = "failure_default")]
- pub cursor: Option<Rgb>,
+ cursor: DefaultForegroundCellRgb,
+}
+
+impl CursorColors {
+ pub fn text(self) -> CellRgb {
+ self.text.0
+ }
+
+ pub fn cursor(self) -> CellRgb {
+ self.cursor.0
+ }
}
#[serde(default)]
#[derive(Deserialize, Debug, Copy, Clone, Default, PartialEq, Eq)]
pub struct SelectionColors {
#[serde(deserialize_with = "failure_default")]
- pub text: Option<Rgb>,
+ text: DefaultBackgroundCellRgb,
+ #[serde(deserialize_with = "failure_default")]
+ background: DefaultForegroundCellRgb,
+}
+
+impl SelectionColors {
+ pub fn text(self) -> CellRgb {
+ self.text.0
+ }
+
+ pub fn background(self) -> CellRgb {
+ self.background.0
+ }
+}
+
+#[serde(default)]
+#[derive(Deserialize, Debug, Copy, Clone, Default, PartialEq, Eq)]
+pub struct SearchColors {
+ #[serde(deserialize_with = "failure_default")]
+ pub matches: MatchColors,
#[serde(deserialize_with = "failure_default")]
- pub background: Option<Rgb>,
+ bar: BarColors,
+}
+
+#[serde(default)]
+#[derive(Deserialize, Debug, Copy, Clone, PartialEq, Eq)]
+pub struct MatchColors {
+ #[serde(deserialize_with = "failure_default")]
+ pub foreground: CellRgb,
+ #[serde(deserialize_with = "deserialize_match_background")]
+ pub background: CellRgb,
+}
+
+impl Default for MatchColors {
+ fn default() -> Self {
+ Self { foreground: CellRgb::default(), background: default_match_background() }
+ }
+}
+
+fn deserialize_match_background<'a, D>(deserializer: D) -> Result<CellRgb, D::Error>
+where
+ D: Deserializer<'a>,
+{
+ let value = Value::deserialize(deserializer)?;
+ Ok(CellRgb::deserialize(value).unwrap_or_else(|_| default_match_background()))
+}
+
+fn default_match_background() -> CellRgb {
+ CellRgb::Rgb(Rgb { r: 0xff, g: 0xff, b: 0xff })
+}
+
+#[serde(default)]
+#[derive(Deserialize, Debug, Copy, Clone, Default, PartialEq, Eq)]
+pub struct BarColors {
+ #[serde(deserialize_with = "failure_default")]
+ foreground: Option<Rgb>,
+ #[serde(deserialize_with = "failure_default")]
+ background: Option<Rgb>,
}
#[serde(default)]
#[derive(Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct PrimaryColors {
- #[serde(default = "default_background", deserialize_with = "failure_default")]
+ #[serde(deserialize_with = "failure_default")]
pub background: Rgb,
- #[serde(default = "default_foreground", deserialize_with = "failure_default")]
+ #[serde(deserialize_with = "failure_default")]
pub foreground: Rgb,
#[serde(deserialize_with = "failure_default")]
pub bright_foreground: Option<Rgb>,
@@ -108,22 +202,14 @@ pub struct PrimaryColors {
impl Default for PrimaryColors {
fn default() -> Self {
PrimaryColors {
- background: default_background(),
- foreground: default_foreground(),
+ background: Rgb { r: 0x1d, g: 0x1f, b: 0x21 },
+ foreground: Rgb { r: 0xc5, g: 0xc8, b: 0xc6 },
bright_foreground: Default::default(),
dim_foreground: Default::default(),
}
}
}
-fn default_background() -> Rgb {
- Rgb { r: 0x1d, g: 0x1f, b: 0x21 }
-}
-
-fn default_foreground() -> Rgb {
- Rgb { r: 0xc5, g: 0xc8, b: 0xc6 }
-}
-
/// The 8-colors sections of config.
#[derive(Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct AnsiColors {
diff --git a/alacritty_terminal/src/config/mod.rs b/alacritty_terminal/src/config/mod.rs
index c3936c0c..e3d72fda 100644
--- a/alacritty_terminal/src/config/mod.rs
+++ b/alacritty_terminal/src/config/mod.rs
@@ -13,7 +13,7 @@ mod scrolling;
mod visual_bell;
mod window;
-use crate::ansi::{CursorStyle, NamedColor};
+use crate::ansi::CursorStyle;
pub use crate::config::colors::Colors;
pub use crate::config::debug::Debug;
@@ -21,7 +21,6 @@ pub use crate::config::font::{Font, FontDescription};
pub use crate::config::scrolling::Scrolling;
pub use crate::config::visual_bell::{VisualBellAnimation, VisualBellConfig};
pub use crate::config::window::{Decorations, Dimensions, StartupMode, WindowConfig, DEFAULT_NAME};
-use crate::term::color::Rgb;
pub const LOG_TARGET_CONFIG: &str = "alacritty_config";
const MAX_SCROLLBACK_LINES: u32 = 100_000;
@@ -156,30 +155,6 @@ impl<T> Config<T> {
self.dynamic_title.0
}
- /// Cursor foreground color.
- #[inline]
- pub fn cursor_text_color(&self) -> Option<Rgb> {
- self.colors.cursor.text
- }
-
- /// Cursor background color.
- #[inline]
- 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]
pub fn set_dynamic_title(&mut self, dynamic_title: bool) {
self.dynamic_title.0 = dynamic_title;
diff --git a/alacritty_terminal/src/grid/mod.rs b/alacritty_terminal/src/grid/mod.rs
index 5ad7e8d6..c1f980e8 100644
--- a/alacritty_terminal/src/grid/mod.rs
+++ b/alacritty_terminal/src/grid/mod.rs
@@ -160,7 +160,7 @@ pub struct Grid<T> {
#[derive(Debug, Copy, Clone)]
pub enum Scroll {
- Lines(isize),
+ Delta(isize),
PageUp,
PageDown,
Top,
@@ -180,23 +180,6 @@ impl<T: GridCell + Default + PartialEq + Copy> Grid<T> {
}
}
- /// Clamp a buffer point to the visible region.
- pub fn clamp_buffer_to_visible(&self, point: Point<usize>) -> Point {
- if point.line < self.display_offset {
- Point::new(self.lines - 1, self.cols - 1)
- } else if point.line >= self.display_offset + self.lines.0 {
- Point::new(Line(0), Column(0))
- } else {
- // Since edge-cases are handled, conversion is identical as visible to buffer.
- self.visible_to_buffer(point.into()).into()
- }
- }
-
- /// Convert viewport relative point to global buffer indexing.
- pub fn visible_to_buffer(&self, point: Point) -> Point<usize> {
- Point { line: self.lines.0 + self.display_offset - point.line.0 - 1, col: point.col }
- }
-
/// Update the size of the scrollback history.
pub fn update_history(&mut self, history_size: usize) {
let current_history_size = self.history_size();
@@ -208,22 +191,16 @@ impl<T: GridCell + Default + PartialEq + Copy> Grid<T> {
}
pub fn scroll_display(&mut self, scroll: Scroll) {
- match scroll {
- Scroll::Lines(count) => {
- self.display_offset = min(
- max((self.display_offset as isize) + count, 0isize) as usize,
- self.history_size(),
- );
- },
- Scroll::PageUp => {
- self.display_offset = min(self.display_offset + self.lines.0, self.history_size());
- },
- Scroll::PageDown => {
- self.display_offset -= min(self.display_offset, self.lines.0);
- },
- Scroll::Top => self.display_offset = self.history_size(),
- Scroll::Bottom => self.display_offset = 0,
- }
+ self.display_offset = match scroll {
+ Scroll::Delta(count) => min(
+ max((self.display_offset as isize) + count, 0isize) as usize,
+ self.history_size(),
+ ),
+ Scroll::PageUp => min(self.display_offset + self.lines.0, self.history_size()),
+ Scroll::PageDown => self.display_offset.saturating_sub(self.lines.0),
+ Scroll::Top => self.history_size(),
+ Scroll::Bottom => 0,
+ };
}
fn increase_scroll_limit(&mut self, count: usize, template: T) {
@@ -279,7 +256,7 @@ impl<T: GridCell + Default + PartialEq + Copy> Grid<T> {
///
/// This is the performance-sensitive part of scrolling.
pub fn scroll_up(&mut self, region: &Range<Line>, positions: Line, template: T) {
- let num_lines = self.num_lines().0;
+ let num_lines = self.screen_lines().0;
if region.start == Line(0) {
// Update display offset when not pinned to active area.
@@ -324,7 +301,7 @@ impl<T: GridCell + Default + PartialEq + Copy> Grid<T> {
pub fn clear_viewport(&mut self, template: T) {
// Determine how many lines to scroll up by.
- let end = Point { line: 0, col: self.num_cols() };
+ let end = Point { line: 0, col: self.cols() };
let mut iter = self.iter_from(end);
while let Some(cell) = iter.prev() {
if !cell.is_empty() || iter.cur.line >= *self.lines {
@@ -333,7 +310,7 @@ impl<T: GridCell + Default + PartialEq + Copy> Grid<T> {
}
debug_assert!(iter.cur.line <= *self.lines);
let positions = self.lines - iter.cur.line;
- let region = Line(0)..self.num_lines();
+ let region = Line(0)..self.screen_lines();
// Reset display offset.
self.display_offset = 0;
@@ -364,19 +341,27 @@ impl<T: GridCell + Default + PartialEq + Copy> Grid<T> {
#[allow(clippy::len_without_is_empty)]
impl<T> Grid<T> {
- #[inline]
- pub fn num_lines(&self) -> Line {
- self.lines
+ /// Clamp a buffer point to the visible region.
+ pub fn clamp_buffer_to_visible(&self, point: Point<usize>) -> Point {
+ if point.line < self.display_offset {
+ Point::new(self.lines - 1, self.cols - 1)
+ } else if point.line >= self.display_offset + self.lines.0 {
+ Point::new(Line(0), Column(0))
+ } else {
+ // Since edgecases are handled, conversion is identical as visible to buffer.
+ self.visible_to_buffer(point.into()).into()
+ }
}
+ /// Convert viewport relative point to global buffer indexing.
#[inline]
- pub fn display_iter(&self) -> DisplayIter<'_, T> {
- DisplayIter::new(self)
+ pub fn visible_to_buffer(&self, point: Point) -> Point<usize> {
+ Point { line: self.lines.0 + self.display_offset - point.line.0 - 1, col: point.col }
}
#[inline]
- pub fn num_cols(&self) -> Column {
- self.cols
+ pub fn display_iter(&self) -> DisplayIter<'_, T> {
+ DisplayIter::new(self)
}
#[inline]
@@ -385,17 +370,6 @@ impl<T> Grid<T> {
self.raw.shrink_lines(self.history_size());
}
- /// Total number of lines in the buffer, this includes scrollback + visible lines.
- #[inline]
- pub fn len(&self) -> usize {
- self.raw.len()
- }
-
- #[inline]
- pub fn history_size(&self) -> usize {
- self.raw.len() - *self.lines
- }
-
/// This is used only for initializing after loading ref-tests.
#[inline]
pub fn initialize_all(&mut self, template: T)
@@ -432,6 +406,56 @@ impl<T> Grid<T> {
}
}
+/// Grid dimensions.
+pub trait Dimensions {
+ /// Total number of lines in the buffer, this includes scrollback and visible lines.
+ fn total_lines(&self) -> usize;
+
+ /// Height of the viewport in lines.
+ fn screen_lines(&self) -> Line;
+
+ /// Width of the terminal in columns.
+ fn cols(&self) -> Column;
+
+ /// Number of invisible lines part of the scrollback history.
+ #[inline]
+ fn history_size(&self) -> usize {
+ self.total_lines() - self.screen_lines().0
+ }
+}
+
+impl<G> Dimensions for Grid<G> {
+ #[inline]
+ fn total_lines(&self) -> usize {
+ self.raw.len()
+ }
+
+ #[inline]
+ fn screen_lines(&self) -> Line {
+ self.lines
+ }
+
+ #[inline]
+ fn cols(&self) -> Column {
+ self.cols
+ }
+}
+
+#[cfg(test)]
+impl Dimensions for (Line, Column) {
+ fn total_lines(&self) -> usize {
+ *self.0
+ }
+
+ fn screen_lines(&self) -> Line {
+ self.0
+ }
+
+ fn cols(&self) -> Column {
+ self.1
+ }
+}
+
pub struct GridIterator<'a, T> {
/// Immutable grid reference.
grid: &'a Grid<T>,
@@ -446,7 +470,7 @@ impl<'a, T> GridIterator<'a, T> {
}
pub fn cell(&self) -> &'a T {
- &self.grid[self.cur.line][self.cur.col]
+ &self.grid[self.cur]
}
}
@@ -454,38 +478,35 @@ impl<'a, T> Iterator for GridIterator<'a, T> {
type Item = &'a T;
fn next(&mut self) -> Option<Self::Item> {
- let last_col = self.grid.num_cols() - Column(1);
+ let last_col = self.grid.cols() - 1;
+
match self.cur {
- Point { line, col } if line == 0 && col == last_col => None,
+ Point { line, col } if line == 0 && col == last_col => return None,
Point { col, .. } if (col == last_col) => {
self.cur.line -= 1;
self.cur.col = Column(0);
- Some(&self.grid[self.cur.line][self.cur.col])
- },
- _ => {
- self.cur.col += Column(1);
- Some(&self.grid[self.cur.line][self.cur.col])
},
+ _ => self.cur.col += Column(1),
}
+
+ Some(&self.grid[self.cur])
}
}
impl<'a, T> BidirectionalIterator for GridIterator<'a, T> {
fn prev(&mut self) -> Option<Self::Item> {
- let num_cols = self.grid.num_cols();
+ let last_col = self.grid.cols() - 1;
match self.cur {
- Point { line, col: Column(0) } if line == self.grid.len() - 1 => None,
+ Point { line, col: Column(0) } if line == self.grid.total_lines() - 1 => return None,
Point { col: Column(0), .. } => {
self.cur.line += 1;
- self.cur.col = num_cols - Column(1);
- Some(&self.grid[self.cur.line][self.cur.col])
- },
- _ => {
- self.cur.col -= Column(1);
- Some(&self.grid[self.cur.line][self.cur.col])
+ self.cur.col = last_col;
},
+ _ => self.cur.col -= Column(1),
}
+
+ Some(&self.grid[self.cur])
}
}
@@ -539,6 +560,22 @@ impl<'point, T> IndexMut<&'point Point> for Grid<T> {
}
}
+impl<T> Index<Point<usize>> for Grid<T> {
+ type Output = T;
+
+ #[inline]
+ fn index(&self, point: Point<usize>) -> &T {
+ &self[point.line][point.col]
+ }
+}
+
+impl<T> IndexMut<Point<usize>> for Grid<T> {
+ #[inline]
+ fn index_mut(&mut self, point: Point<usize>) -> &mut T {
+ &mut self[point.line][point.col]
+ }
+}
+
/// A subset of lines in the grid.
///
/// May be constructed using Grid::region(..).
@@ -578,15 +615,15 @@ pub trait IndexRegion<I, T> {
impl<T> IndexRegion<Range<Line>, T> for Grid<T> {
fn region(&self, index: Range<Line>) -> Region<'_, T> {
- assert!(index.start < self.num_lines());
- assert!(index.end <= self.num_lines());
+ assert!(index.start < self.screen_lines());
+ assert!(index.end <= self.screen_lines());
assert!(index.start <= index.end);
Region { start: index.start, end: index.end, raw: &self.raw }
}
fn region_mut(&mut self, index: Range<Line>) -> RegionMut<'_, T> {
- assert!(index.start < self.num_lines());
- assert!(index.end <= self.num_lines());
+ assert!(index.start < self.screen_lines());
+ assert!(index.end <= self.screen_lines());
assert!(index.start <= index.end);
RegionMut { start: index.start, end: index.end, raw: &mut self.raw }
}
@@ -594,35 +631,35 @@ impl<T> IndexRegion<Range<Line>, T> for Grid<T> {
impl<T> IndexRegion<RangeTo<Line>, T> for Grid<T> {
fn region(&self, index: RangeTo<Line>) -> Region<'_, T> {
- assert!(index.end <= self.num_lines());
+ assert!(index.end <= self.screen_lines());
Region { start: Line(0), end: index.end, raw: &self.raw }
}
fn region_mut(&mut self, index: RangeTo<Line>) -> RegionMut<'_, T> {
- assert!(index.end <= self.num_lines());
+ assert!(index.end <= self.screen_lines());
RegionMut { start: Line(0), end: index.end, raw: &mut self.raw }
}
}
impl<T> IndexRegion<RangeFrom<Line>, T> for Grid<T> {
fn region(&self, index: RangeFrom<Line>) -> Region<'_, T> {
- assert!(index.start < self.num_lines());
- Region { start: index.start, end: self.num_lines(), raw: &self.raw }
+ assert!(index.start < self.screen_lines());
+ Region { start: index.start, end: self.screen_lines(), raw: &self.raw }
}
fn region_mut(&mut self, index: RangeFrom<Line>) -> RegionMut<'_, T> {
- assert!(index.start < self.num_lines());
- RegionMut { start: index.start, end: self.num_lines(), raw: &mut self.raw }
+ assert!(index.start < self.screen_lines());
+ RegionMut { start: index.start, end: self.screen_lines(), raw: &mut self.raw }
}
}
impl<T> IndexRegion<RangeFull, T> for Grid<T> {
fn region(&self, _: RangeFull) -> Region<'_, T> {
- Region { start: Line(0), end: self.num_lines(), raw: &self.raw }
+ Region { start: Line(0), end: self.screen_lines(), raw: &self.raw }
}
fn region_mut(&mut self, _: RangeFull) -> RegionMut<'_, T> {
- RegionMut { start: Line(0), end: self.num_lines(), raw: &mut self.raw }
+ RegionMut { start: Line(0), end: self.screen_lines(), raw: &mut self.raw }
}
}
@@ -695,7 +732,7 @@ pub struct DisplayIter<'a, T> {
impl<'a, T: 'a> DisplayIter<'a, T> {
pub fn new(grid: &'a Grid<T>) -> DisplayIter<'a, T> {
- let offset = grid.display_offset + *grid.num_lines() - 1;
+ let offset = grid.display_offset + *grid.screen_lines() - 1;
let limit = grid.display_offset;
let col = Column(0);
let line = Line(0);
@@ -722,7 +759,7 @@ impl<'a, T: Copy + 'a> Iterator for DisplayIter<'a, T> {
#[inline]
fn next(&mut self) -> Option<Self::Item> {
// Return None if we've reached the end.
- if self.offset == self.limit && self.grid.num_cols() == self.col {
+ if self.offset == self.limit && self.grid.cols() == self.col {
return None;
}
@@ -735,7 +772,7 @@ impl<'a, T: Copy + 'a> Iterator for DisplayIter<'a, T> {
// Update line/col to point to next item.
self.col += 1;
- if self.col == self.grid.num_cols() && self.offset != self.limit {
+ if self.col == self.grid.cols() && self.offset != self.limit {
self.offset -= 1;
self.col = Column(0);
diff --git a/alacritty_terminal/src/grid/resize.rs b/alacritty_terminal/src/grid/resize.rs
index a0493fc0..079fcf19 100644
--- a/alacritty_terminal/src/grid/resize.rs
+++ b/alacritty_terminal/src/grid/resize.rs
@@ -6,7 +6,7 @@ use crate::index::{Column, Line};
use crate::term::cell::Flags;
use crate::grid::row::Row;
-use crate::grid::{Grid, GridCell};
+use crate::grid::{Dimensions, Grid, GridCell};
impl<T: GridCell + Default + PartialEq + Copy> Grid<T> {
/// Resize the grid's width and/or height.
@@ -18,8 +18,8 @@ impl<T: GridCell + Default + PartialEq + Copy> Grid<T> {
}
match self.cols.cmp(&cols) {
- Ordering::Less => self.grow_cols(cols, reflow),
- Ordering::Greater => self.shrink_cols(cols, reflow),
+ Ordering::Less => self.grow_cols(reflow, cols),
+ Ordering::Greater => self.shrink_cols(reflow, cols),
Ordering::Equal => (),
}
}
@@ -79,7 +79,7 @@ impl<T: GridCell + Default + PartialEq + Copy> Grid<T> {
}
/// Grow number of columns in each row, reflowing if necessary.
- fn grow_cols(&mut self, cols: Column, reflow: bool) {
+ fn grow_cols(&mut self, reflow: bool, cols: Column) {
// Check if a row needs to be wrapped.
let should_reflow = |row: &Row<T>| -> bool {
let len = Column(row.len());
@@ -116,9 +116,8 @@ impl<T: GridCell + Default + PartialEq + Copy> Grid<T> {
// Remove leading spacers when reflowing wide char to the previous line.
let mut last_len = last_row.len();
- if last_len >= 2
- && !last_row[Column(last_len - 2)].flags().contains(Flags::WIDE_CHAR)
- && last_row[Column(last_len - 1)].flags().contains(Flags::WIDE_CHAR_SPACER)
+ if last_len >= 1
+ && last_row[Column(last_len - 1)].flags().contains(Flags::LEADING_WIDE_CHAR_SPACER)
{
last_row.shrink(Column(last_len - 1));
last_len -= 1;
@@ -135,7 +134,7 @@ impl<T: GridCell + Default + PartialEq + Copy> Grid<T> {
let mut cells = row.front_split_off(len - 1);
let mut spacer = T::default();
- spacer.flags_mut().insert(Flags::WIDE_CHAR_SPACER);
+ spacer.flags_mut().insert(Flags::LEADING_WIDE_CHAR_SPACER);
cells.push(spacer);
cells
@@ -143,7 +142,7 @@ impl<T: GridCell + Default + PartialEq + Copy> Grid<T> {
row.front_split_off(len)
};
- // Reflow cells to previous row.
+ // Add removed cells to previous row and reflow content.
last_row.append(&mut cells);
let cursor_buffer_line = (self.lines - self.cursor.point.line - 1).0;
@@ -219,7 +218,7 @@ impl<T: GridCell + Default + PartialEq + Copy> Grid<T> {
}
/// Shrink number of columns in each row, reflowing if necessary.
- fn shrink_cols(&mut self, cols: Column, reflow: bool) {
+ fn shrink_cols(&mut self, reflow: bool, cols: Column) {
self.cols = cols;
// Remove the linewrap special case, by moving the cursor outside of the grid.
@@ -268,17 +267,14 @@ impl<T: GridCell + Default + PartialEq + Copy> Grid<T> {
wrapped.insert(0, row[cols - 1]);
let mut spacer = T::default();
- spacer.flags_mut().insert(Flags::WIDE_CHAR_SPACER);
+ spacer.flags_mut().insert(Flags::LEADING_WIDE_CHAR_SPACER);
row[cols - 1] = spacer;
}
// Remove wide char spacer before shrinking.
let len = wrapped.len();
- if (len == 1 || (len >= 2 && !wrapped[len - 2].flags().contains(Flags::WIDE_CHAR)))
- && wrapped[len - 1].flags().contains(Flags::WIDE_CHAR_SPACER)
- {
+ if wrapped[len - 1].flags().contains(Flags::LEADING_WIDE_CHAR_SPACER) {
if len == 1 {
- // Delete the wrapped content if it contains only a leading spacer.
row[cols - 1].flags_mut().insert(Flags::WRAPLINE);
new_raw.push(row);
break;
diff --git a/alacritty_terminal/src/grid/storage.rs b/alacritty_terminal/src/grid/storage.rs
index 4b7ca41a..a025a99c 100644
--- a/alacritty_terminal/src/grid/storage.rs
+++ b/alacritty_terminal/src/grid/storage.rs
@@ -232,7 +232,9 @@ impl<T> Storage<T> {
/// Rotate the grid up, moving all existing lines down in history.
///
- /// This is a faster, specialized version of [`rotate`].
+ /// This is a faster, specialized version of [`rotate_left`].
+ ///
+ /// [`rotate_left`]: https://doc.rust-lang.org/std/vec/struct.Vec.html#method.rotate_left
#[inline]
pub fn rotate_up(&mut self, count: usize) {
self.zero = (self.zero + count) % self.inner.len();
diff --git a/alacritty_terminal/src/grid/tests.rs b/alacritty_terminal/src/grid/tests.rs
index dbe5f1fc..1ed279a0 100644
--- a/alacritty_terminal/src/grid/tests.rs
+++ b/alacritty_terminal/src/grid/tests.rs
@@ -1,7 +1,6 @@
//! Tests for the Grid.
-use super::{BidirectionalIterator, Grid};
-use crate::grid::GridCell;
+use super::{BidirectionalIterator, Dimensions, Grid, GridCell};
use crate::index::{Column, Line, Point};
use crate::term::cell::{Cell, Flags};
@@ -171,7 +170,7 @@ fn shrink_reflow() {
grid.resize(true, Line(1), Column(2));
- assert_eq!(grid.len(), 3);
+ assert_eq!(grid.total_lines(), 3);
assert_eq!(grid[2].len(), 2);
assert_eq!(grid[2][Column(0)], cell('1'));
@@ -198,7 +197,7 @@ fn shrink_reflow_twice() {
grid.resize(true, Line(1), Column(4));
grid.resize(true, Line(1), Column(2));
- assert_eq!(grid.len(), 3);
+ assert_eq!(grid.total_lines(), 3);
assert_eq!(grid[2].len(), 2);
assert_eq!(grid[2][Column(0)], cell('1'));
@@ -224,7 +223,7 @@ fn shrink_reflow_empty_cell_inside_line() {
grid.resize(true, Line(1), Column(2));
- assert_eq!(grid.len(), 2);
+ assert_eq!(grid.total_lines(), 2);
assert_eq!(grid[1].len(), 2);
assert_eq!(grid[1][Column(0)], cell('1'));
@@ -236,7 +235,7 @@ fn shrink_reflow_empty_cell_inside_line() {
grid.resize(true, Line(1), Column(1));
- assert_eq!(grid.len(), 4);
+ assert_eq!(grid.total_lines(), 4);
assert_eq!(grid[3].len(), 1);
assert_eq!(grid[3][Column(0)], wrap_cell('1'));
@@ -261,7 +260,7 @@ fn grow_reflow() {
grid.resize(true, Line(2), Column(3));
- assert_eq!(grid.len(), 2);
+ assert_eq!(grid.total_lines(), 2);
assert_eq!(grid[1].len(), 3);
assert_eq!(grid[1][Column(0)], cell('1'));
@@ -287,7 +286,7 @@ fn grow_reflow_multiline() {
grid.resize(true, Line(3), Column(6));
- assert_eq!(grid.len(), 3);
+ assert_eq!(grid.total_lines(), 3);
assert_eq!(grid[2].len(), 6);
assert_eq!(grid[2][Column(0)], cell('1'));
@@ -318,7 +317,7 @@ fn grow_reflow_disabled() {
grid.resize(false, Line(2), Column(3));
- assert_eq!(grid.len(), 2);
+ assert_eq!(grid.total_lines(), 2);
assert_eq!(grid[1].len(), 3);
assert_eq!(grid[1][Column(0)], cell('1'));
@@ -342,7 +341,7 @@ fn shrink_reflow_disabled() {
grid.resize(false, Line(1), Column(2));
- assert_eq!(grid.len(), 1);
+ assert_eq!(grid.total_lines(), 1);
assert_eq!(grid[0].len(), 2);
assert_eq!(grid[0][Column(0)], cell('1'));
diff --git a/alacritty_terminal/src/index.rs b/alacritty_terminal/src/index.rs
index 019de83b..baed323e 100644
--- a/alacritty_terminal/src/index.rs
+++ b/alacritty_terminal/src/index.rs
@@ -7,16 +7,20 @@ use std::ops::{self, Add, AddAssign, Deref, Range, Sub, SubAssign};
use serde::{Deserialize, Serialize};
+use crate::grid::Dimensions;
use crate::term::RenderableCell;
/// The side of a cell.
+pub type Side = Direction;
+
+/// Horizontal direction.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
-pub enum Side {
+pub enum Direction {
Left,
Right,
}
-impl Side {
+impl Direction {
pub fn opposite(self) -> Self {
match self {
Side::Right => Side::Left,
@@ -25,8 +29,23 @@ impl Side {
}
}
+/// Behavior for handling grid boundaries.
+pub enum Boundary {
+ /// Clamp to grid boundaries.
+ ///
+ /// When an operation exceeds the grid boundaries, the last point will be returned no matter
+ /// how far the boundaries were exceeded.
+ Clamp,
+
+ /// Wrap around grid bondaries.
+ ///
+ /// When an operation exceeds the grid boundaries, the point will wrap around the entire grid
+ /// history and continue at the other side.
+ Wrap,
+}
+
/// Index in the grid using row, column notation.
-#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Serialize, Deserialize, PartialOrd)]
+#[derive(Debug, Clone, Copy, Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct Point<L = Line> {
pub line: L,
pub col: Column,
@@ -65,43 +84,84 @@ impl<L> Point<L> {
self.col = Column((self.col.0 + rhs) % num_cols);
self
}
+}
+impl Point<usize> {
#[inline]
#[must_use = "this returns the result of the operation, without modifying the original"]
- pub fn sub_absolute(mut self, num_cols: Column, rhs: usize) -> Point<L>
+ pub fn sub_absolute<D>(mut self, dimensions: &D, boundary: Boundary, rhs: usize) -> Point<usize>
where
- L: Copy + Default + Into<Line> + Add<usize, Output = L> + Sub<usize, Output = L>,
+ D: Dimensions,
{
- let num_cols = num_cols.0;
- self.line = self.line + ((rhs + num_cols - 1).saturating_sub(self.col.0) / num_cols);
+ let total_lines = dimensions.total_lines();
+ let num_cols = dimensions.cols().0;
+
+ self.line += (rhs + num_cols - 1).saturating_sub(self.col.0) / num_cols;
self.col = Column((num_cols + self.col.0 - rhs % num_cols) % num_cols);
- self
+
+ if self.line >= total_lines {
+ match boundary {
+ Boundary::Clamp => Point::new(total_lines - 1, Column(0)),
+ Boundary::Wrap => Point::new(self.line - total_lines, self.col),
+ }
+ } else {
+ self
+ }
}
#[inline]
#[must_use = "this returns the result of the operation, without modifying the original"]
- pub fn add_absolute(mut self, num_cols: Column, rhs: usize) -> Point<L>
+ pub fn add_absolute<D>(mut self, dimensions: &D, boundary: Boundary, rhs: usize) -> Point<usize>
where
- L: Copy + Default + Into<Line> + Add<usize, Output = L> + Sub<usize, Output = L>,
+ D: Dimensions,
{
- let line_changes = (rhs + self.col.0) / num_cols.0;
- if self.line.into() >= Line(line_changes) {
- self.line = self.line - line_changes;
+ let num_cols = dimensions.cols();
+
+ let line_delta = (rhs + self.col.0) / num_cols.0;
+
+ if self.line >= line_delta {
+ self.line -= line_delta;
self.col = Column((self.col.0 + rhs) % num_cols.0);
self
} else {
- Point::new(L::default(), num_cols - 1)
+ match boundary {
+ Boundary::Clamp => Point::new(0, num_cols - 1),
+ Boundary::Wrap => {
+ let col = Column((self.col.0 + rhs) % num_cols.0);
+ let line = dimensions.total_lines() + self.line - line_delta;
+ Point::new(line, col)
+ },
+ }
}
}
}
+impl PartialOrd for Point {
+ fn partial_cmp(&self, other: &Point) -> Option<Ordering> {
+ Some(self.cmp(other))
+ }
+}
+
impl Ord for Point {
fn cmp(&self, other: &Point) -> Ordering {
match (self.line.cmp(&other.line), self.col.cmp(&other.col)) {
- (Ordering::Equal, Ordering::Equal) => Ordering::Equal,
- (Ordering::Equal, ord) | (ord, Ordering::Equal) => ord,
- (Ordering::Less, _) => Ordering::Less,
- (Ordering::Greater, _) => Ordering::Greater,
+ (Ordering::Equal, ord) | (ord, _) => ord,
+ }
+ }
+}
+
+impl PartialOrd for Point<usize> {
+ fn partial_cmp(&self, other: &Point<usize>) -> Option<Ordering> {
+ Some(self.cmp(other))
+ }
+}
+
+impl Ord for Point<usize> {
+ fn cmp(&self, other: &Point<usize>) -> Ordering {
+ match (self.line.cmp(&other.line), self.col.cmp(&other.col)) {
+ (Ordering::Equal, ord) => ord,
+ (Ordering::Less, _) => Ordering::Greater,
+ (Ordering::Greater, _) => Ordering::Less,
}
}
}
@@ -429,7 +489,7 @@ ops!(Linear, Linear);
#[cfg(test)]
mod tests {
- use super::{Column, Line, Point};
+ use super::*;
#[test]
fn location_ordering() {
@@ -493,51 +553,100 @@ mod tests {
#[test]
fn add_absolute() {
- let num_cols = Column(42);
let point = Point::new(0, Column(13));
- let result = point.add_absolute(num_cols, 1);
+ let result = point.add_absolute(&(Line(1), Column(42)), Boundary::Clamp, 1);
assert_eq!(result, Point::new(0, point.col + 1));
}
#[test]
- fn add_absolute_wrap() {
- let num_cols = Column(42);
- let point = Point::new(1, num_cols - 1);
+ fn add_absolute_wrapline() {
+ let point = Point::new(1, Column(41));
- let result = point.add_absolute(num_cols, 1);
+ let result = point.add_absolute(&(Line(2), Column(42)), Boundary::Clamp, 1);
+
+ assert_eq!(result, Point::new(0, Column(0)));
+ }
+
+ #[test]
+ fn add_absolute_multiline_wrapline() {
+ let point = Point::new(2, Column(9));
+
+ let result = point.add_absolute(&(Line(3), Column(10)), Boundary::Clamp, 11);
assert_eq!(result, Point::new(0, Column(0)));
}
#[test]
fn add_absolute_clamp() {
- let num_cols = Column(42);
- let point = Point::new(0, num_cols - 1);
+ let point = Point::new(0, Column(41));
- let result = point.add_absolute(num_cols, 1);
+ let result = point.add_absolute(&(Line(1), Column(42)), Boundary::Clamp, 1);
assert_eq!(result, point);
}
#[test]
+ fn add_absolute_wrap() {
+ let point = Point::new(0, Column(41));
+
+ let result = point.add_absolute(&(Line(3), Column(42)), Boundary::Wrap, 1);
+
+ assert_eq!(result, Point::new(2, Column(0)));
+ }
+
+ #[test]
+ fn add_absolute_multiline_wrap() {
+ let point = Point::new(0, Column(9));
+
+ let result = point.add_absolute(&(Line(3), Column(10)), Boundary::Wrap, 11);
+
+ assert_eq!(result, Point::new(1, Column(0)));
+ }
+
+ #[test]
fn sub_absolute() {
- let num_cols = Column(42);
let point = Point::new(0, Column(13));
- let result = point.sub_absolute(num_cols, 1);
+ let result = point.sub_absolute(&(Line(1), Column(42)), Boundary::Clamp, 1);
assert_eq!(result, Point::new(0, point.col - 1));
}
#[test]
- fn sub_absolute_wrap() {
- let num_cols = Column(42);
+ fn sub_absolute_wrapline() {
let point = Point::new(0, Column(0));
- let result = point.sub_absolute(num_cols, 1);
+ let result = point.sub_absolute(&(Line(2), Column(42)), Boundary::Clamp, 1);
+
+ assert_eq!(result, Point::new(1, Column(41)));
+ }
+
+ #[test]
+ fn sub_absolute_multiline_wrapline() {
+ let point = Point::new(0, Column(0));
+
+ let result = point.sub_absolute(&(Line(3), Column(10)), Boundary::Clamp, 11);
+
+ assert_eq!(result, Point::new(2, Column(9)));
+ }
+
+ #[test]
+ fn sub_absolute_wrap() {
+ let point = Point::new(2, Column(0));
+
+ let result = point.sub_absolute(&(Line(3), Column(42)), Boundary::Wrap, 1);
+
+ assert_eq!(result, Point::new(0, Column(41)));
+ }
+
+ #[test]
+ fn sub_absolute_multiline_wrap() {
+ let point = Point::new(2, Column(0));
+
+ let result = point.sub_absolute(&(Line(3), Column(10)), Boundary::Wrap, 11);
- assert_eq!(result, Point::new(1, num_cols - 1));
+ assert_eq!(result, Point::new(1, Column(9)));
}
}
diff --git a/alacritty_terminal/src/selection.rs b/alacritty_terminal/src/selection.rs
index dbd11592..83dea824 100644
--- a/alacritty_terminal/src/selection.rs
+++ b/alacritty_terminal/src/selection.rs
@@ -9,8 +9,9 @@ use std::convert::TryFrom;
use std::mem;
use std::ops::{Bound, Range, RangeBounds};
+use crate::grid::Dimensions;
use crate::index::{Column, Line, Point, Side};
-use crate::term::{Search, Term};
+use crate::term::Term;
/// A Point and side within that point.
#[derive(Debug, Copy, Clone, PartialEq)]
@@ -98,20 +99,19 @@ impl Selection {
self.region.end = Anchor::new(point, side);
}
- pub fn rotate(
+ pub fn rotate<D: Dimensions>(
mut self,
- num_lines: Line,
- num_cols: Column,
+ dimensions: &D,
range: &Range<Line>,
delta: isize,
) -> Option<Selection> {
+ let num_lines = dimensions.screen_lines().0;
+ let num_cols = dimensions.cols().0;
let range_bottom = range.start.0;
let range_top = range.end.0;
- let num_lines = num_lines.0;
- let num_cols = num_cols.0;
let (mut start, mut end) = (&mut self.region.start, &mut self.region.end);
- if Self::points_need_swap(start.point, end.point) {
+ if Selection::points_need_swap(start.point, end.point) {
mem::swap(&mut start, &mut end);
}
@@ -238,7 +238,7 @@ impl Selection {
/// Convert selection to grid coordinates.
pub fn to_range<T>(&self, term: &Term<T>) -> Option<SelectionRange> {
let grid = term.grid();
- let num_cols = grid.num_cols();
+ let num_cols = grid.cols();
// Order start above the end.
let mut start = self.region.start;
@@ -250,7 +250,7 @@ impl Selection {
// Clamp to inside the grid buffer.
let is_block = self.ty == SelectionType::Block;
- let (start, end) = Self::grid_clamp(start, end, is_block, grid.len()).ok()?;
+ let (start, end) = Self::grid_clamp(start, end, is_block, grid.total_lines()).ok()?;
match self.ty {
SelectionType::Simple => self.range_simple(start, end, num_cols),
@@ -408,7 +408,7 @@ mod tests {
fn send_event(&self, _event: Event) {}
}
- fn term(width: usize, height: usize) -> Term<Mock> {
+ fn term(height: usize, width: usize) -> Term<Mock> {
let size = SizeInfo {
width: width as f32,
height: height as f32,
@@ -468,7 +468,7 @@ mod tests {
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);
+ assert_eq!(selection.to_range(&term(1, 2)), None);
}
/// Test adjacent cell selection from right to left.
@@ -482,7 +482,7 @@ mod tests {
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);
+ assert_eq!(selection.to_range(&term(1, 2)), None);
}
/// Test selection across adjacent lines.
@@ -499,7 +499,7 @@ mod tests {
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 {
+ assert_eq!(selection.to_range(&term(2, 5)).unwrap(), SelectionRange {
start: Point::new(1, Column(2)),
end: Point::new(0, Column(1)),
is_block: false,
@@ -523,7 +523,7 @@ mod tests {
selection.update(Point::new(1, Column(1)), Side::Right);
selection.update(Point::new(1, Column(0)), Side::Right);
- assert_eq!(selection.to_range(&term(5, 2)).unwrap(), SelectionRange {
+ assert_eq!(selection.to_range(&term(2, 5)).unwrap(), SelectionRange {
start: Point::new(1, Column(1)),
end: Point::new(0, Column(1)),
is_block: false,
@@ -532,14 +532,13 @@ mod tests {
#[test]
fn line_selection() {
- let num_lines = Line(10);
- let num_cols = Column(5);
+ let size = (Line(10), Column(5));
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)..num_lines), 7).unwrap();
+ selection = selection.rotate(&size, &(Line(0)..size.0), 7).unwrap();
- assert_eq!(selection.to_range(&term(num_cols.0, num_lines.0)).unwrap(), SelectionRange {
+ assert_eq!(selection.to_range(&term(*size.0, *size.1)).unwrap(), SelectionRange {
start: Point::new(9, Column(0)),
end: Point::new(7, Column(4)),
is_block: false,
@@ -548,14 +547,13 @@ mod tests {
#[test]
fn semantic_selection() {
- let num_lines = Line(10);
- let num_cols = Column(5);
+ let size = (Line(10), Column(5));
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)..num_lines), 7).unwrap();
+ selection = selection.rotate(&size, &(Line(0)..size.0), 7).unwrap();
- assert_eq!(selection.to_range(&term(num_cols.0, num_lines.0)).unwrap(), SelectionRange {
+ assert_eq!(selection.to_range(&term(*size.0, *size.1)).unwrap(), SelectionRange {
start: Point::new(9, Column(0)),
end: Point::new(7, Column(3)),
is_block: false,
@@ -564,14 +562,13 @@ mod tests {
#[test]
fn simple_selection() {
- let num_lines = Line(10);
- let num_cols = Column(5);
+ let size = (Line(10), Column(5));
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)..num_lines), 7).unwrap();
+ selection = selection.rotate(&size, &(Line(0)..size.0), 7).unwrap();
- assert_eq!(selection.to_range(&term(num_cols.0, num_lines.0)).unwrap(), SelectionRange {
+ assert_eq!(selection.to_range(&term(*size.0, *size.1)).unwrap(), SelectionRange {
start: Point::new(9, Column(0)),
end: Point::new(7, Column(3)),
is_block: false,
@@ -580,14 +577,13 @@ mod tests {
#[test]
fn block_selection() {
- let num_lines = Line(10);
- let num_cols = Column(5);
+ let size = (Line(10), Column(5));
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)..num_lines), 7).unwrap();
+ selection = selection.rotate(&size, &(Line(0)..size.0), 7).unwrap();
- assert_eq!(selection.to_range(&term(num_cols.0, num_lines.0)).unwrap(), SelectionRange {
+ assert_eq!(selection.to_range(&term(*size.0, *size.1)).unwrap(), SelectionRange {
start: Point::new(9, Column(2)),
end: Point::new(7, Column(3)),
is_block: true
@@ -624,14 +620,13 @@ mod tests {
#[test]
fn rotate_in_region_up() {
- let num_lines = Line(10);
- let num_cols = Column(5);
+ let size = (Line(10), Column(5));
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)..(num_lines - 1)), 4).unwrap();
+ selection = selection.rotate(&size, &(Line(1)..(size.0 - 1)), 4).unwrap();
- assert_eq!(selection.to_range(&term(num_cols.0, num_lines.0)).unwrap(), SelectionRange {
+ assert_eq!(selection.to_range(&term(*size.0, *size.1)).unwrap(), SelectionRange {
start: Point::new(8, Column(0)),
end: Point::new(6, Column(3)),
is_block: false,
@@ -640,30 +635,28 @@ mod tests {
#[test]
fn rotate_in_region_down() {
- let num_lines = Line(10);
- let num_cols = Column(5);
+ let size = (Line(10), Column(5));
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)..(num_lines - 1)), -5).unwrap();
+ selection = selection.rotate(&size, &(Line(1)..(size.0 - 1)), -5).unwrap();
- assert_eq!(selection.to_range(&term(num_cols.0, num_lines.0)).unwrap(), SelectionRange {
+ assert_eq!(selection.to_range(&term(*size.0, *size.1)).unwrap(), SelectionRange {
start: Point::new(3, Column(1)),
- end: Point::new(1, num_cols - 1),
+ end: Point::new(1, size.1 - 1),
is_block: false,
});
}
#[test]
fn rotate_in_region_up_block() {
- let num_lines = Line(10);
- let num_cols = Column(5);
+ let size = (Line(10), Column(5));
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)..(num_lines - 1)), 4).unwrap();
+ selection = selection.rotate(&size, &(Line(1)..(size.0 - 1)), 4).unwrap();
- assert_eq!(selection.to_range(&term(num_cols.0, num_lines.0)).unwrap(), SelectionRange {
+ assert_eq!(selection.to_range(&term(*size.0, *size.1)).unwrap(), SelectionRange {
start: Point::new(8, Column(2)),
end: Point::new(6, Column(3)),
is_block: true,
diff --git a/alacritty_terminal/src/term/cell.rs b/alacritty_terminal/src/term/cell.rs
index 5f948b19..3fdd8cea 100644
--- a/alacritty_terminal/src/term/cell.rs
+++ b/alacritty_terminal/src/term/cell.rs
@@ -12,18 +12,19 @@ pub const MAX_ZEROWIDTH_CHARS: usize = 5;
bitflags! {
#[derive(Serialize, Deserialize)]
pub struct Flags: u16 {
- const INVERSE = 0b00_0000_0001;
- const BOLD = 0b00_0000_0010;
- const ITALIC = 0b00_0000_0100;
- const BOLD_ITALIC = 0b00_0000_0110;
- const UNDERLINE = 0b00_0000_1000;
- const WRAPLINE = 0b00_0001_0000;
- const WIDE_CHAR = 0b00_0010_0000;
- const WIDE_CHAR_SPACER = 0b00_0100_0000;
- const DIM = 0b00_1000_0000;
- const DIM_BOLD = 0b00_1000_0010;
- const HIDDEN = 0b01_0000_0000;
- const STRIKEOUT = 0b10_0000_0000;
+ const INVERSE = 0b000_0000_0001;
+ const BOLD = 0b000_0000_0010;
+ const ITALIC = 0b000_0000_0100;
+ const BOLD_ITALIC = 0b000_0000_0110;
+ const UNDERLINE = 0b000_0000_1000;
+ const WRAPLINE = 0b000_0001_0000;
+ const WIDE_CHAR = 0b000_0010_0000;
+ const WIDE_CHAR_SPACER = 0b000_0100_0000;
+ const DIM = 0b000_1000_0000;
+ const DIM_BOLD = 0b000_1000_0010;
+ const HIDDEN = 0b001_0000_0000;
+ const STRIKEOUT = 0b010_0000_0000;
+ const LEADING_WIDE_CHAR_SPACER = 0b100_0000_0000;
}
}
@@ -59,7 +60,8 @@ impl GridCell for Cell {
| Flags::UNDERLINE
| Flags::STRIKEOUT
| Flags::WRAPLINE
- | Flags::WIDE_CHAR_SPACER,
+ | Flags::WIDE_CHAR_SPACER
+ | Flags::LEADING_WIDE_CHAR_SPACER,
)
}
diff --git a/alacritty_terminal/src/term/color.rs b/alacritty_terminal/src/term/color.rs
index ef2c2402..f20601d6 100644
--- a/alacritty_terminal/src/term/color.rs
+++ b/alacritty_terminal/src/term/color.rs
@@ -2,12 +2,13 @@ use std::fmt;
use std::ops::{Index, IndexMut, Mul};
use std::str::FromStr;
-use log::{error, trace};
-use serde::de::Visitor;
+use log::trace;
+use serde::de::{Error as _, Visitor};
use serde::{Deserialize, Deserializer, Serialize};
+use serde_yaml::Value;
use crate::ansi;
-use crate::config::{Colors, LOG_TARGET_CONFIG};
+use crate::config::Colors;
pub const COUNT: usize = 269;
@@ -67,7 +68,7 @@ impl<'de> Deserialize<'de> for Rgb {
f.write_str("hex color like #ff00ff")
}
- fn visit_str<E>(self, value: &str) -> ::std::result::Result<Rgb, E>
+ fn visit_str<E>(self, value: &str) -> Result<Rgb, E>
where
E: serde::de::Error,
{
@@ -81,7 +82,7 @@ impl<'de> Deserialize<'de> for Rgb {
}
// Return an error if the syntax is incorrect.
- let value = serde_yaml::Value::deserialize(deserializer)?;
+ let value = Value::deserialize(deserializer)?;
// Attempt to deserialize from struct form.
if let Ok(RgbDerivedDeser { r, g, b }) = RgbDerivedDeser::deserialize(value.clone()) {
@@ -89,23 +90,14 @@ impl<'de> Deserialize<'de> for Rgb {
}
// Deserialize from hex notation (either 0xff00ff or #ff00ff).
- match value.deserialize_str(RgbVisitor) {
- Ok(rgb) => Ok(rgb),
- Err(err) => {
- error!(
- target: LOG_TARGET_CONFIG,
- "Problem with config: {}; using color #000000", err
- );
- Ok(Rgb::default())
- },
- }
+ value.deserialize_str(RgbVisitor).map_err(D::Error::custom)
}
}
impl FromStr for Rgb {
type Err = ();
- fn from_str(s: &str) -> std::result::Result<Rgb, ()> {
+ fn from_str(s: &str) -> Result<Rgb, ()> {
let chars = if s.starts_with("0x") && s.len() == 8 {
&s[2..]
} else if s.starts_with('#') && s.len() == 7 {
@@ -128,6 +120,66 @@ impl FromStr for Rgb {
}
}
+/// RGB color optionally referencing the cell's foreground or background.
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+pub enum CellRgb {
+ CellForeground,
+ CellBackground,
+ Rgb(Rgb),
+}
+
+impl CellRgb {
+ pub fn color(self, foreground: Rgb, background: Rgb) -> Rgb {
+ match self {
+ Self::CellForeground => foreground,
+ Self::CellBackground => background,
+ Self::Rgb(rgb) => rgb,
+ }
+ }
+}
+
+impl Default for CellRgb {
+ fn default() -> Self {
+ Self::Rgb(Rgb::default())
+ }
+}
+
+impl<'de> Deserialize<'de> for CellRgb {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ const EXPECTING: &str = "CellForeground, CellBackground, or hex color like #ff00ff";
+
+ struct CellRgbVisitor;
+ impl<'a> Visitor<'a> for CellRgbVisitor {
+ type Value = CellRgb;
+
+ fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ f.write_str(EXPECTING)
+ }
+
+ fn visit_str<E>(self, value: &str) -> Result<CellRgb, E>
+ where
+ E: serde::de::Error,
+ {
+ // Attempt to deserialize as enum constants.
+ match value {
+ "CellForeground" => return Ok(CellRgb::CellForeground),
+ "CellBackground" => return Ok(CellRgb::CellBackground),
+ _ => (),
+ }
+
+ Rgb::from_str(&value[..]).map(CellRgb::Rgb).map_err(|_| {
+ E::custom(format!("failed to parse color {}; expected {}", value, EXPECTING))
+ })
+ }
+ }
+
+ deserializer.deserialize_str(CellRgbVisitor).map_err(D::Error::custom)
+ }
+}
+
/// List of indexed colors.
///
/// The first 16 entries are the standard ansi named colors. Items 16..232 are
@@ -179,9 +231,6 @@ impl List {
self[ansi::NamedColor::Foreground] = colors.primary.foreground;
self[ansi::NamedColor::Background] = colors.primary.background;
- // Background for custom cursor colors.
- self[ansi::NamedColor::Cursor] = colors.cursor.cursor.unwrap_or_else(Rgb::default);
-
// Dims.
self[ansi::NamedColor::DimForeground] =
colors.primary.dim_foreground.unwrap_or(colors.primary.foreground * DIM_FACTOR);
diff --git a/alacritty_terminal/src/term/mod.rs b/alacritty_terminal/src/term/mod.rs
index 996f6809..d59838d4 100644
--- a/alacritty_terminal/src/term/mod.rs
+++ b/alacritty_terminal/src/term/mod.rs
@@ -1,195 +1,121 @@
//! Exports the `Term` type which is a high-level API for the Grid.
use std::cmp::{max, min};
-use std::ops::{Index, IndexMut, Range};
+use std::iter::Peekable;
+use std::ops::{Index, IndexMut, Range, RangeInclusive};
use std::sync::Arc;
use std::time::{Duration, Instant};
-use std::{io, mem, ptr, str};
+use std::{io, iter, mem, ptr, str};
use log::{debug, trace};
use serde::{Deserialize, Serialize};
use unicode_width::UnicodeWidthChar;
use crate::ansi::{
- self, Attr, CharsetIndex, Color, CursorStyle, Handler, NamedColor, StandardCharset, TermInfo,
+ self, Attr, CharsetIndex, Color, CursorStyle, Handler, NamedColor, StandardCharset,
};
use crate::config::{Config, VisualBellAnimation};
use crate::event::{Event, EventListener};
-use crate::grid::{
- BidirectionalIterator, DisplayIter, Grid, GridCell, IndexRegion, Indexed, Scroll,
-};
-use crate::index::{self, Column, IndexRange, Line, Point, Side};
+use crate::grid::{Dimensions, DisplayIter, Grid, IndexRegion, Indexed, Scroll};
+use crate::index::{self, Boundary, Column, Direction, IndexRange, Line, Point, Side};
use crate::selection::{Selection, SelectionRange};
use crate::term::cell::{Cell, Flags, LineLength};
-use crate::term::color::{Rgb, DIM_FACTOR};
+use crate::term::color::{CellRgb, Rgb, DIM_FACTOR};
+use crate::term::search::{RegexIter, RegexSearch};
use crate::vi_mode::{ViModeCursor, ViMotion};
pub mod cell;
pub mod color;
-
-/// Used to match equal brackets, when performing a bracket-pair selection.
-const BRACKET_PAIRS: [(char, char); 4] = [('(', ')'), ('[', ']'), ('{', '}'), ('<', '>')];
+mod search;
/// Max size of the window title stack.
const TITLE_STACK_MAX_DEPTH: usize = 4096;
+/// Maximum number of linewraps followed outside of the viewport during search highlighting.
+const MAX_SEARCH_LINES: usize = 100;
+
/// Default tab interval, corresponding to terminfo `it` value.
const INITIAL_TABSTOPS: usize = 8;
/// Minimum number of columns and lines.
const MIN_SIZE: usize = 2;
-/// A type that can expand a given point to a region.
-///
-/// Usually this is implemented for some 2-D array type since
-/// points are two dimensional indices.
-pub trait Search {
- /// Find the nearest semantic boundary _to the left_ of provided point.
- fn semantic_search_left(&self, _: Point<usize>) -> Point<usize>;
- /// Find the nearest semantic boundary _to the point_ of provided point.
- fn semantic_search_right(&self, _: Point<usize>) -> Point<usize>;
- /// Find the beginning of a line, following line wraps.
- fn line_search_left(&self, _: Point<usize>) -> Point<usize>;
- /// Find the end of a line, following line wraps.
- fn line_search_right(&self, _: Point<usize>) -> Point<usize>;
- /// Find the nearest matching bracket.
- fn bracket_search(&self, _: Point<usize>) -> Option<Point<usize>>;
+/// Cursor storing all information relevant for rendering.
+#[derive(Debug, Eq, PartialEq, Copy, Clone, Deserialize)]
+struct RenderableCursor {
+ text_color: CellRgb,
+ cursor_color: CellRgb,
+ key: CursorKey,
+ point: Point,
+ rendered: bool,
}
-impl<T> Search for Term<T> {
- fn semantic_search_left(&self, mut point: Point<usize>) -> Point<usize> {
- // Limit the starting point to the last line in the history.
- point.line = min(point.line, self.grid.len() - 1);
-
- let mut iter = self.grid.iter_from(point);
- let last_col = self.grid.num_cols() - Column(1);
-
- while let Some(cell) = iter.prev() {
- if !cell.flags.intersects(Flags::WIDE_CHAR | Flags::WIDE_CHAR_SPACER)
- && self.semantic_escape_chars.contains(cell.c)
- {
- break;
- }
-
- if iter.point().col == last_col && !cell.flags.contains(Flags::WRAPLINE) {
- // Cut off if on new line or hit escape char.
- break;
- }
-
- point = iter.point();
- }
-
- point
- }
-
- fn semantic_search_right(&self, mut point: Point<usize>) -> Point<usize> {
- // Limit the starting point to the last line in the history.
- point.line = min(point.line, self.grid.len() - 1);
-
- let mut iter = self.grid.iter_from(point);
- let last_col = self.grid.num_cols() - 1;
+/// A key for caching cursor glyphs.
+#[derive(Debug, Eq, PartialEq, Copy, Clone, Hash, Deserialize)]
+pub struct CursorKey {
+ pub style: CursorStyle,
+ pub is_wide: bool,
+}
- while let Some(cell) = iter.next() {
- if !cell.flags.intersects(Flags::WIDE_CHAR | Flags::WIDE_CHAR_SPACER)
- && self.semantic_escape_chars.contains(cell.c)
- {
- break;
- }
+type MatchIter<'a> = Box<dyn Iterator<Item = RangeInclusive<Point<usize>>> + 'a>;
- point = iter.point();
+/// Regex search highlight tracking.
+pub struct RenderableSearch<'a> {
+ iter: Peekable<MatchIter<'a>>,
+}
- if point.col == last_col && !cell.flags.contains(Flags::WRAPLINE) {
- // Cut off if on new line or hit escape char.
- break;
+impl<'a> RenderableSearch<'a> {
+ /// Create a new renderable search iterator.
+ fn new<T>(term: &'a Term<T>) -> Self {
+ let viewport_end = term.grid().display_offset();
+ let viewport_start = viewport_end + term.grid().screen_lines().0 - 1;
+
+ // Compute start of the first and end of the last line.
+ let start_point = Point::new(viewport_start, Column(0));
+ let mut start = term.line_search_left(start_point);
+ let end_point = Point::new(viewport_end, term.grid().cols() - 1);
+ let mut end = term.line_search_right(end_point);
+
+ // Set upper bound on search before/after the viewport to prevent excessive blocking.
+ if start.line > viewport_start + MAX_SEARCH_LINES {
+ if start.line == 0 {
+ // Do not highlight anything if this line is the last.
+ let iter: MatchIter<'a> = Box::new(iter::empty());
+ return Self { iter: iter.peekable() };
+ } else {
+ // Start at next line if this one is too long.
+ start.line -= 1;
}
}
+ end.line = max(end.line, viewport_end.saturating_sub(MAX_SEARCH_LINES));
- point
- }
-
- fn line_search_left(&self, mut point: Point<usize>) -> Point<usize> {
- while point.line + 1 < self.grid.len()
- && self.grid[point.line + 1][self.grid.num_cols() - 1].flags.contains(Flags::WRAPLINE)
- {
- point.line += 1;
- }
-
- point.col = Column(0);
+ // Create an iterater for the current regex search for all visible matches.
+ let iter: MatchIter<'a> = Box::new(
+ RegexIter::new(start, end, Direction::Right, &term)
+ .skip_while(move |rm| rm.end().line > viewport_start)
+ .take_while(move |rm| rm.start().line >= viewport_end),
+ );
- point
+ Self { iter: iter.peekable() }
}
- fn line_search_right(&self, mut point: Point<usize>) -> Point<usize> {
- while self.grid[point.line][self.grid.num_cols() - 1].flags.contains(Flags::WRAPLINE) {
- point.line -= 1;
- }
-
- point.col = self.grid.num_cols() - 1;
-
- point
- }
-
- fn bracket_search(&self, point: Point<usize>) -> Option<Point<usize>> {
- let start_char = self.grid[point.line][point.col].c;
-
- // Find the matching bracket we're looking for.
- let (forwards, end_char) = BRACKET_PAIRS.iter().find_map(|(open, close)| {
- if open == &start_char {
- Some((true, *close))
- } else if close == &start_char {
- Some((false, *open))
+ /// Advance the search tracker to the next point.
+ ///
+ /// This will return `true` if the point passed is part of a search match.
+ fn advance(&mut self, point: Point<usize>) -> bool {
+ while let Some(regex_match) = &self.iter.peek() {
+ if regex_match.start() > &point {
+ break;
+ } else if regex_match.end() < &point {
+ let _ = self.iter.next();
} else {
- None
- }
- })?;
-
- let mut iter = self.grid.iter_from(point);
-
- // For every character match that equals the starting bracket, we
- // ignore one bracket of the opposite type.
- let mut skip_pairs = 0;
-
- loop {
- // Check the next cell.
- let cell = if forwards { iter.next() } else { iter.prev() };
-
- // Break if there are no more cells.
- let c = match cell {
- Some(cell) => cell.c,
- None => break,
- };
-
- // Check if the bracket matches.
- if c == end_char && skip_pairs == 0 {
- return Some(iter.point());
- } else if c == start_char {
- skip_pairs += 1;
- } else if c == end_char {
- skip_pairs -= 1;
+ return true;
}
}
-
- None
+ false
}
}
-/// 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,
- pub is_wide: bool,
-}
-
/// Iterator that yields cells needing render.
///
/// Yields cells that require work to be displayed (that is, not a an empty
@@ -205,6 +131,7 @@ pub struct RenderableCellsIter<'a, C> {
config: &'a Config<C>,
colors: &'a color::List,
selection: Option<SelectionRange<Line>>,
+ search: RenderableSearch<'a>,
}
impl<'a, C> RenderableCellsIter<'a, C> {
@@ -212,26 +139,24 @@ impl<'a, C> RenderableCellsIter<'a, C> {
///
/// The cursor and terminal mode are required for properly displaying the
/// cursor.
- fn new<'b, T>(
- term: &'b Term<T>,
- config: &'b Config<C>,
+ fn new<T>(
+ term: &'a Term<T>,
+ config: &'a Config<C>,
selection: Option<SelectionRange>,
- ) -> RenderableCellsIter<'b, C> {
+ ) -> RenderableCellsIter<'a, C> {
let grid = &term.grid;
- let inner = grid.display_iter();
-
let selection_range = selection.and_then(|span| {
let (limit_start, limit_end) = if span.is_block {
(span.start.col, span.end.col)
} else {
- (Column(0), grid.num_cols() - 1)
+ (Column(0), grid.cols() - 1)
};
// Do not render completely offscreen selection.
- let viewport_start = grid.display_offset();
- let viewport_end = viewport_start + grid.num_lines().0;
- if span.end.line >= viewport_end || span.start.line < viewport_start {
+ let viewport_end = grid.display_offset();
+ let viewport_start = viewport_end + grid.screen_lines().0 - 1;
+ if span.end.line > viewport_start || span.start.line < viewport_end {
return None;
}
@@ -249,10 +174,11 @@ impl<'a, C> RenderableCellsIter<'a, C> {
RenderableCellsIter {
cursor: term.renderable_cursor(config),
grid,
- inner,
+ inner: grid.display_iter(),
selection: selection_range,
config,
colors: &term.colors,
+ search: RenderableSearch::new(term),
}
}
@@ -280,20 +206,18 @@ impl<'a, C> RenderableCellsIter<'a, C> {
return true;
}
- let num_cols = self.grid.num_cols();
+ let num_cols = self.grid.cols();
let cell = self.grid[&point];
// Check if wide char's spacers are selected.
if cell.flags.contains(Flags::WIDE_CHAR) {
- let prevprev = point.sub(num_cols, 2);
let prev = point.sub(num_cols, 1);
let next = point.add(num_cols, 1);
// Check trailing spacer.
selection.contains(next.col, next.line)
// Check line-wrapping, leading spacer.
- || (self.grid[&prev].flags.contains(Flags::WIDE_CHAR_SPACER)
- && !self.grid[&prevprev].flags.contains(Flags::WIDE_CHAR)
+ || (self.grid[&prev].flags.contains(Flags::LEADING_WIDE_CHAR_SPACER)
&& selection.contains(prev.col, prev.line))
} else if cell.flags.contains(Flags::WIDE_CHAR_SPACER) {
// Check if spacer's wide char is selected.
@@ -312,7 +236,7 @@ impl<'a, C> RenderableCellsIter<'a, C> {
}
}
-#[derive(Copy, Clone, Debug)]
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum RenderableCellContent {
Chars([char; cell::MAX_ZEROWIDTH_CHARS + 1]),
Cursor(CursorKey),
@@ -331,38 +255,44 @@ pub struct RenderableCell {
}
impl RenderableCell {
- fn new<C>(
- config: &Config<C>,
- colors: &color::List,
- cell: Indexed<Cell>,
- selected: bool,
- ) -> Self {
+ fn new<'a, C>(iter: &mut RenderableCellsIter<'a, C>, cell: Indexed<Cell>) -> Self {
+ let point = Point::new(cell.line, cell.column);
+
// Lookup RGB values.
- let mut fg_rgb = Self::compute_fg_rgb(config, colors, cell.fg, cell.flags);
- let mut bg_rgb = Self::compute_bg_rgb(colors, cell.bg);
- let mut bg_alpha = Self::compute_bg_alpha(cell.bg);
-
- let selection_background = config.colors.selection.background;
- if let (true, Some(col)) = (selected, selection_background) {
- // Override selection background with config colors.
- bg_rgb = col;
- bg_alpha = 1.0;
- } else if selected ^ cell.inverse() {
+ let mut fg_rgb = Self::compute_fg_rgb(iter.config, iter.colors, cell.fg, cell.flags);
+ let mut bg_rgb = Self::compute_bg_rgb(iter.colors, cell.bg);
+
+ let mut bg_alpha = if cell.inverse() {
+ mem::swap(&mut fg_rgb, &mut bg_rgb);
+ 1.0
+ } else {
+ Self::compute_bg_alpha(cell.bg)
+ };
+
+ if iter.is_selected(point) {
+ let config_bg = iter.config.colors.selection.background();
+ let selected_fg = iter.config.colors.selection.text().color(fg_rgb, bg_rgb);
+ bg_rgb = config_bg.color(fg_rgb, bg_rgb);
+ fg_rgb = selected_fg;
+
if fg_rgb == bg_rgb && !cell.flags.contains(Flags::HIDDEN) {
// Reveal inversed text when fg/bg is the same.
- fg_rgb = colors[NamedColor::Background];
- bg_rgb = colors[NamedColor::Foreground];
- } else {
- // Invert cell fg and bg colors.
- mem::swap(&mut fg_rgb, &mut bg_rgb);
+ fg_rgb = iter.colors[NamedColor::Background];
+ bg_rgb = iter.colors[NamedColor::Foreground];
+ bg_alpha = 1.0;
+ } else if config_bg != CellRgb::CellBackground {
+ bg_alpha = 1.0;
+ }
+ } else if iter.search.advance(iter.grid.visible_to_buffer(point)) {
+ // Highlight the cell if it is part of a search match.
+ let config_bg = iter.config.colors.search.matches.background;
+ let matched_fg = iter.config.colors.search.matches.foreground.color(fg_rgb, bg_rgb);
+ bg_rgb = config_bg.color(fg_rgb, bg_rgb);
+ fg_rgb = matched_fg;
+
+ if config_bg != CellRgb::CellBackground {
+ bg_alpha = 1.0;
}
-
- bg_alpha = 1.0;
- }
-
- // Override selection text with config colors.
- if let (true, Some(col)) = (selected, config.colors.selection.text) {
- fg_rgb = col;
}
RenderableCell {
@@ -376,6 +306,12 @@ impl RenderableCell {
}
}
+ fn is_empty(&self) -> bool {
+ self.bg_alpha == 0.
+ && !self.flags.intersects(Flags::UNDERLINE | Flags::STRIKEOUT)
+ && self.inner == RenderableCellContent::Chars([' '; cell::MAX_ZEROWIDTH_CHARS + 1])
+ }
+
fn compute_fg_rgb<C>(config: &Config<C>, colors: &color::List, fg: Color, flags: Flags) -> Rgb {
match fg {
Color::Spec(rgb) => match flags & Flags::DIM {
@@ -416,6 +352,11 @@ impl RenderableCell {
}
}
+ /// Compute background alpha based on cell's original color.
+ ///
+ /// Since an RGB color matching the background should not be transparent, this is computed
+ /// using the named input color, rather than checking the RGB of the background after its color
+ /// is computed.
#[inline]
fn compute_bg_alpha(bg: Color) -> f32 {
if bg == Color::Named(NamedColor::Background) {
@@ -448,19 +389,13 @@ impl<'a, C> Iterator for RenderableCellsIter<'a, C> {
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);
+ let cell = self.inner.next()?;
+ let mut cell = RenderableCell::new(self, cell);
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;
- }
+ cell.fg = self.cursor.text_color.color(cell.fg, cell.bg);
}
return Some(cell);
@@ -475,24 +410,18 @@ impl<'a, C> Iterator for RenderableCellsIter<'a, C> {
line: self.cursor.point.line,
};
- let mut renderable_cell =
- RenderableCell::new(self.config, self.colors, cell, selected);
-
- renderable_cell.inner = RenderableCellContent::Cursor(self.cursor.key);
-
- if let Some(color) = self.cursor.cursor_color {
- renderable_cell.fg = color;
- }
+ let mut cell = RenderableCell::new(self, cell);
+ cell.inner = RenderableCellContent::Cursor(self.cursor.key);
+ cell.fg = self.cursor.cursor_color.color(cell.fg, cell.bg);
- return Some(renderable_cell);
+ return Some(cell);
}
} else {
let cell = self.inner.next()?;
+ let cell = RenderableCell::new(self, cell);
- let selected = self.is_selected(Point::new(cell.line, cell.column));
-
- if !cell.is_empty() || selected {
- return Some(RenderableCell::new(self.config, self.colors, cell, selected));
+ if !cell.is_empty() {
+ return Some(cell);
}
}
}
@@ -802,6 +731,9 @@ pub struct Term<T> {
/// Stack of saved window titles. When a title is popped from this stack, the `title` for the
/// term is set, and the Glutin window's title attribute is changed through the event listener.
title_stack: Vec<Option<String>>,
+
+ /// Current forwards and backwards buffer search regexes.
+ regex_search: Option<RegexSearch>,
}
impl<T> Term<T> {
@@ -810,8 +742,8 @@ impl<T> Term<T> {
where
T: EventListener,
{
- self.event_proxy.send_event(Event::MouseCursorDirty);
self.grid.scroll_display(scroll);
+ self.event_proxy.send_event(Event::MouseCursorDirty);
self.dirty = true;
}
@@ -823,9 +755,9 @@ impl<T> Term<T> {
let grid = Grid::new(num_lines, num_cols, history_size, Cell::default());
let alt = Grid::new(num_lines, num_cols, 0 /* scroll history */, Cell::default());
- let tabs = TabStops::new(grid.num_cols());
+ let tabs = TabStops::new(grid.cols());
- let scroll_region = Line(0)..grid.num_lines();
+ let scroll_region = Line(0)..grid.screen_lines();
let colors = color::List::from(&config.colors);
@@ -853,6 +785,7 @@ impl<T> Term<T> {
default_title: config.window.title.clone(),
title_stack: Vec::new(),
selection: None,
+ regex_search: None,
}
}
@@ -964,7 +897,7 @@ impl<T> Term<T> {
tab_mode = true;
}
- if !cell.flags.contains(Flags::WIDE_CHAR_SPACER) {
+ if !cell.flags.intersects(Flags::WIDE_CHAR_SPACER | Flags::LEADING_WIDE_CHAR_SPACER) {
// Push cells primary character.
text.push(cell.c);
@@ -983,10 +916,9 @@ impl<T> Term<T> {
}
// If wide char is not part of the selection, but leading spacer is, include it.
- if line_length == self.grid.num_cols()
+ if line_length == self.cols()
&& line_length.0 >= 2
- && grid_line[line_length - 1].flags.contains(Flags::WIDE_CHAR_SPACER)
- && !grid_line[line_length - 2].flags.contains(Flags::WIDE_CHAR)
+ && grid_line[line_length - 1].flags.contains(Flags::LEADING_WIDE_CHAR_SPACER)
&& include_wrapped_wide
{
text.push(self.grid[line - 1][Column(0)].c);
@@ -1026,8 +958,8 @@ impl<T> Term<T> {
/// Resize terminal to new dimensions.
pub fn resize(&mut self, size: &SizeInfo) {
- let old_cols = self.grid.num_cols();
- let old_lines = self.grid.num_lines();
+ let old_cols = self.cols();
+ let old_lines = self.screen_lines();
let num_cols = max(size.cols(), Column(MIN_SIZE));
let num_lines = max(size.lines(), Line(MIN_SIZE));
@@ -1038,6 +970,23 @@ impl<T> Term<T> {
debug!("New num_cols is {} and num_lines is {}", num_cols, num_lines);
+ // Invalidate selection and tabs only when necessary.
+ if old_cols != num_cols {
+ self.selection = None;
+
+ // Recreate tabs list.
+ self.tabs.resize(num_cols);
+ } else if let Some(selection) = self.selection.take() {
+ // Move the selection if only number of lines changed.
+ let delta = if num_lines > old_lines {
+ (num_lines - old_lines.0).saturating_sub(self.grid.history_size()) as isize
+ } else {
+ let cursor_line = self.grid.cursor.point.line;
+ -(min(old_lines - cursor_line - 1, old_lines - num_lines).0 as isize)
+ };
+ self.selection = selection.rotate(self, &(Line(0)..num_lines), delta);
+ }
+
let is_alt = self.mode.contains(TermMode::ALT_SCREEN);
self.grid.resize(!is_alt, num_lines, num_cols);
@@ -1047,14 +996,11 @@ impl<T> Term<T> {
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());
-
- // Reset scrolling region and selection.
- self.scroll_region = Line(0)..self.grid.num_lines();
- self.selection = None;
+ // Reset scrolling region.
+ self.scroll_region = Line(0)..self.screen_lines();
}
+ /// Active terminal modes.
#[inline]
pub fn mode(&self) -> &TermMode {
&self.mode
@@ -1087,8 +1033,7 @@ impl<T> Term<T> {
fn scroll_down_relative(&mut self, origin: Line, mut lines: Line) {
trace!("Scrolling down relative: origin={}, lines={}", origin, lines);
- let num_lines = self.grid.num_lines();
- let num_cols = self.grid.num_cols();
+ let num_lines = self.screen_lines();
lines = min(lines, self.scroll_region.end - self.scroll_region.start);
lines = min(lines, self.scroll_region.end - origin);
@@ -1100,7 +1045,7 @@ impl<T> Term<T> {
self.selection = self
.selection
.take()
- .and_then(|s| s.rotate(num_lines, num_cols, &absolute_region, -(lines.0 as isize)));
+ .and_then(|s| s.rotate(self, &absolute_region, -(lines.0 as isize)));
// Scroll between origin and bottom
let template = Cell { bg: self.grid.cursor.template.bg, ..Cell::default() };
@@ -1114,8 +1059,8 @@ impl<T> Term<T> {
#[inline]
fn scroll_up_relative(&mut self, origin: Line, mut lines: Line) {
trace!("Scrolling up relative: origin={}, lines={}", origin, lines);
- let num_lines = self.grid.num_lines();
- let num_cols = self.grid.num_cols();
+
+ let num_lines = self.screen_lines();
lines = min(lines, self.scroll_region.end - self.scroll_region.start);
@@ -1123,10 +1068,8 @@ impl<T> Term<T> {
let absolute_region = (num_lines - region.end)..(num_lines - region.start);
// Scroll selection.
- self.selection = self
- .selection
- .take()
- .and_then(|s| s.rotate(num_lines, num_cols, &absolute_region, lines.0 as isize));
+ self.selection =
+ self.selection.take().and_then(|s| s.rotate(self, &absolute_region, lines.0 as isize));
// Scroll from origin to bottom less number of lines.
let template = Cell { bg: self.grid.cursor.template.bg, ..Cell::default() };
@@ -1139,7 +1082,7 @@ impl<T> Term<T> {
{
// Setting 132 column font makes no sense, but run the other side effects.
// Clear scrolling region.
- self.set_scrolling_region(1, self.grid.num_lines().0);
+ self.set_scrolling_region(1, None);
// Clear grid.
let template = self.grid.cursor.template;
@@ -1163,18 +1106,33 @@ impl<T> Term<T> {
#[inline]
pub fn toggle_vi_mode(&mut self) {
self.mode ^= TermMode::VI;
- self.selection = None;
- // Reset vi mode cursor position to match primary cursor.
- if self.mode.contains(TermMode::VI) {
+ let vi_mode = self.mode.contains(TermMode::VI);
+
+ // Do not clear selection when entering search.
+ if self.regex_search.is_none() || !vi_mode {
+ self.selection = None;
+ }
+
+ if vi_mode {
+ // Reset vi mode cursor position to match primary cursor.
let cursor = self.grid.cursor.point;
- let line = min(cursor.line + self.grid.display_offset(), self.lines() - 1);
+ let line = min(cursor.line + self.grid.display_offset(), self.screen_lines() - 1);
self.vi_mode_cursor = ViModeCursor::new(Point::new(line, cursor.col));
+ } else {
+ self.cancel_search();
}
self.dirty = true;
}
+ /// Start vi mode without moving the cursor.
+ #[inline]
+ pub fn set_vi_mode(&mut self) {
+ self.mode.insert(TermMode::VI);
+ self.dirty = true;
+ }
+
/// Move vi mode cursor.
#[inline]
pub fn vi_motion(&mut self, motion: ViMotion)
@@ -1188,18 +1146,89 @@ impl<T> Term<T> {
// Move cursor.
self.vi_mode_cursor = self.vi_mode_cursor.motion(self, motion);
+ self.vi_mode_recompute_selection();
+
+ self.dirty = true;
+ }
+
+ /// Move vi cursor to absolute point in grid.
+ #[inline]
+ pub fn vi_goto_point(&mut self, point: Point<usize>)
+ where
+ T: EventListener,
+ {
+ // Move viewport to make point visible.
+ self.scroll_to_point(point);
+
+ // Move vi cursor to the point.
+ self.vi_mode_cursor.point = self.grid.clamp_buffer_to_visible(point);
+
+ self.vi_mode_recompute_selection();
+
+ self.dirty = true;
+ }
+
+ /// Update the active selection to match the vi mode cursor position.
+ #[inline]
+ fn vi_mode_recompute_selection(&mut self) {
+ // Require vi mode to be active.
+ if !self.mode.contains(TermMode::VI) {
+ return;
+ }
- // Update selection if one is active.
let viewport_point = self.visible_to_buffer(self.vi_mode_cursor.point);
- if let Some(selection) = &mut self.selection {
- // Do not extend empty selections started by a single mouse click.
- if !selection.is_empty() {
- selection.update(viewport_point, Side::Left);
- selection.include_all();
- }
+
+ // Update only if non-empty selection is present.
+ let selection = match &mut self.selection {
+ Some(selection) if !selection.is_empty() => selection,
+ _ => return,
+ };
+
+ selection.update(viewport_point, Side::Left);
+ selection.include_all();
+ }
+
+ /// Scroll display to point if it is outside of viewport.
+ pub fn scroll_to_point(&mut self, point: Point<usize>)
+ where
+ T: EventListener,
+ {
+ let display_offset = self.grid.display_offset();
+ let num_lines = self.screen_lines().0;
+
+ if point.line >= display_offset + num_lines {
+ let lines = point.line.saturating_sub(display_offset + num_lines - 1);
+ self.scroll_display(Scroll::Delta(lines as isize));
+ } else if point.line < display_offset {
+ let lines = display_offset.saturating_sub(point.line);
+ self.scroll_display(Scroll::Delta(-(lines as isize)));
}
+ }
- self.dirty = true;
+ /// Jump to the end of a wide cell.
+ pub fn expand_wide(&self, mut point: Point<usize>, direction: Direction) -> Point<usize> {
+ let flags = self.grid[point.line][point.col].flags;
+
+ match direction {
+ Direction::Right if flags.contains(Flags::LEADING_WIDE_CHAR_SPACER) => {
+ point.col = Column(1);
+ point.line -= 1;
+ },
+ Direction::Right if flags.contains(Flags::WIDE_CHAR) => point.col += 1,
+ Direction::Left if flags.intersects(Flags::WIDE_CHAR | Flags::WIDE_CHAR_SPACER) => {
+ if flags.contains(Flags::WIDE_CHAR_SPACER) {
+ point.col -= 1;
+ }
+
+ let prev = point.sub_absolute(self, Boundary::Clamp, 1);
+ if self.grid[prev].flags.contains(Flags::LEADING_WIDE_CHAR_SPACER) {
+ point = prev;
+ }
+ },
+ _ => (),
+ }
+
+ point
}
#[inline]
@@ -1260,7 +1289,8 @@ impl<T> Term<T> {
};
// Cursor shape.
- let hidden = !self.mode.contains(TermMode::SHOW_CURSOR) || point.line >= self.lines();
+ let hidden =
+ !self.mode.contains(TermMode::SHOW_CURSOR) || point.line >= self.screen_lines();
let cursor_style = if hidden && !vi_mode {
point.line = Line(0);
CursorStyle::Hidden
@@ -1277,19 +1307,18 @@ impl<T> Term<T> {
};
// Cursor colors.
- let (text_color, cursor_color) = if vi_mode {
- (config.vi_mode_cursor_text_color(), config.vi_mode_cursor_cursor_color())
+ let color = if vi_mode { config.colors.vi_mode_cursor } else { config.colors.cursor };
+ let cursor_color = if self.color_modified[NamedColor::Cursor as usize] {
+ CellRgb::Rgb(self.colors[NamedColor::Cursor])
} else {
- let cursor_cursor_color = config.cursor_cursor_color().map(|c| self.colors[c]);
- (config.cursor_text_color(), cursor_cursor_color)
+ color.cursor()
};
+ let text_color = color.text();
// 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)
- {
+ let is_wide = if cell.flags.contains(Flags::WIDE_CHAR_SPACER) {
point.col -= 1;
true
} else {
@@ -1306,15 +1335,20 @@ impl<T> Term<T> {
}
}
-impl<T> TermInfo for Term<T> {
+impl<T> Dimensions for Term<T> {
#[inline]
- fn lines(&self) -> Line {
- self.grid.num_lines()
+ fn cols(&self) -> Column {
+ self.grid.cols()
}
#[inline]
- fn cols(&self) -> Column {
- self.grid.num_cols()
+ fn screen_lines(&self) -> Line {
+ self.grid.screen_lines()
+ }
+
+ #[inline]
+ fn total_lines(&self) -> usize {
+ self.grid.total_lines()
}
}
@@ -1344,7 +1378,7 @@ impl<T: EventListener> Handler for Term<T> {
self.wrapline();
}
- let num_cols = self.grid.num_cols();
+ let num_cols = self.cols();
// If in insert mode, first shift cells to the right.
if self.mode.contains(TermMode::INSERT) && self.grid.cursor.point.col + width < num_cols {
@@ -1365,7 +1399,7 @@ impl<T: EventListener> Handler for Term<T> {
if self.grid.cursor.point.col + 1 >= num_cols {
if self.mode.contains(TermMode::LINE_WRAP) {
// Insert placeholder before wide char if glyph does not fit in this row.
- self.write_at_cursor(' ').flags.insert(Flags::WIDE_CHAR_SPACER);
+ self.write_at_cursor(' ').flags.insert(Flags::LEADING_WIDE_CHAR_SPACER);
self.wrapline();
} else {
// Prevent out of bounds crash when linewrapping is disabled.
@@ -1403,11 +1437,11 @@ impl<T: EventListener> Handler for Term<T> {
let (y_offset, max_y) = if self.mode.contains(TermMode::ORIGIN) {
(self.scroll_region.start, self.scroll_region.end - 1)
} else {
- (Line(0), self.grid.num_lines() - 1)
+ (Line(0), self.screen_lines() - 1)
};
self.grid.cursor.point.line = min(line + y_offset, max_y);
- self.grid.cursor.point.col = min(col, self.grid.num_cols() - 1);
+ self.grid.cursor.point.col = min(col, self.cols() - 1);
self.grid.cursor.input_needs_wrap = false;
}
@@ -1428,11 +1462,11 @@ impl<T: EventListener> Handler for Term<T> {
let cursor = self.grid.cursor;
// Ensure inserting within terminal bounds
- let count = min(count, self.grid.num_cols() - cursor.point.col);
+ let count = min(count, self.cols() - cursor.point.col);
let source = cursor.point.col;
let destination = cursor.point.col + count;
- let num_cells = (self.grid.num_cols() - destination).0;
+ let num_cells = (self.cols() - destination).0;
let line = &mut self.grid[cursor.point.line];
@@ -1467,7 +1501,7 @@ impl<T: EventListener> Handler for Term<T> {
#[inline]
fn move_forward(&mut self, cols: Column) {
trace!("Moving forward: {}", cols);
- let num_cols = self.grid.num_cols();
+ let num_cols = self.cols();
self.grid.cursor.point.col = min(self.grid.cursor.point.col + cols, num_cols - 1);
self.grid.cursor.input_needs_wrap = false;
}
@@ -1524,7 +1558,7 @@ impl<T: EventListener> Handler for Term<T> {
return;
}
- while self.grid.cursor.point.col < self.grid.num_cols() && count != 0 {
+ while self.grid.cursor.point.col < self.cols() && count != 0 {
count -= 1;
let c = self.grid.cursor.charsets[self.active_charset].map('\t');
@@ -1534,7 +1568,7 @@ impl<T: EventListener> Handler for Term<T> {
}
loop {
- if (self.grid.cursor.point.col + 1) == self.grid.num_cols() {
+ if (self.grid.cursor.point.col + 1) == self.cols() {
break;
}
@@ -1573,7 +1607,7 @@ impl<T: EventListener> Handler for Term<T> {
let next = self.grid.cursor.point.line + 1;
if next == self.scroll_region.end {
self.scroll_up(Line(1));
- } else if next < self.grid.num_lines() {
+ } else if next < self.screen_lines() {
self.grid.cursor.point.line += 1;
}
}
@@ -1653,7 +1687,7 @@ impl<T: EventListener> Handler for Term<T> {
#[inline]
fn delete_lines(&mut self, lines: Line) {
let origin = self.grid.cursor.point.line;
- let lines = min(self.lines() - origin, lines);
+ let lines = min(self.screen_lines() - origin, lines);
trace!("Deleting {} lines", lines);
@@ -1669,7 +1703,7 @@ impl<T: EventListener> Handler for Term<T> {
trace!("Erasing chars: count={}, col={}", count, cursor.point.col);
let start = cursor.point.col;
- let end = min(start + count, self.grid.num_cols());
+ let end = min(start + count, self.cols());
// Cleared cells have current background color set.
let row = &mut self.grid[cursor.point.line];
@@ -1680,7 +1714,7 @@ impl<T: EventListener> Handler for Term<T> {
#[inline]
fn delete_chars(&mut self, count: Column) {
- let cols = self.grid.num_cols();
+ let cols = self.cols();
let cursor = self.grid.cursor;
// Ensure deleting within terminal bounds.
@@ -1768,7 +1802,7 @@ impl<T: EventListener> Handler for Term<T> {
},
}
- let cursor_buffer_line = (self.grid.num_lines() - self.grid.cursor.point.line - 1).0;
+ let cursor_buffer_line = (self.grid.screen_lines() - self.grid.cursor.point.line - 1).0;
self.selection = self
.selection
.take()
@@ -1850,7 +1884,7 @@ impl<T: EventListener> Handler for Term<T> {
trace!("Clearing screen: {:?}", mode);
let template = self.grid.cursor.template;
- let num_lines = self.grid.num_lines().0;
+ let num_lines = self.screen_lines().0;
let cursor_buffer_line = num_lines - self.grid.cursor.point.line.0 - 1;
match mode {
@@ -1864,7 +1898,7 @@ impl<T: EventListener> Handler for Term<T> {
}
// Clear up to the current column in the current line.
- let end = min(cursor.col + 1, self.grid.num_cols());
+ let end = min(cursor.col + 1, self.cols());
for cell in &mut self.grid[cursor.line][..end] {
cell.reset(&template);
}
@@ -1933,17 +1967,18 @@ impl<T: EventListener> Handler for Term<T> {
self.cursor_style = None;
self.grid.reset(Cell::default());
self.inactive_grid.reset(Cell::default());
- self.scroll_region = Line(0)..self.grid.num_lines();
- self.tabs = TabStops::new(self.grid.num_cols());
+ self.scroll_region = Line(0)..self.screen_lines();
+ self.tabs = TabStops::new(self.cols());
self.title_stack = Vec::new();
self.title = None;
self.selection = None;
+ self.regex_search = None;
}
#[inline]
fn reverse_index(&mut self) {
trace!("Reversing index");
-
+ // If cursor is at the top.
if self.grid.cursor.point.line == self.scroll_region.start {
self.scroll_down(Line(1));
} else {
@@ -2074,7 +2109,10 @@ impl<T: EventListener> Handler for Term<T> {
}
#[inline]
- fn set_scrolling_region(&mut self, top: usize, bottom: usize) {
+ fn set_scrolling_region(&mut self, top: usize, bottom: Option<usize>) {
+ // Fallback to the last line as default.
+ let bottom = bottom.unwrap_or_else(|| self.screen_lines().0);
+
if top >= bottom {
debug!("Invalid scrolling region: ({};{})", top, bottom);
return;
@@ -2089,8 +2127,8 @@ impl<T: EventListener> Handler for Term<T> {
trace!("Setting scrolling region: ({};{})", start, end);
- self.scroll_region.start = min(start, self.grid.num_lines());
- self.scroll_region.end = min(end, self.grid.num_lines());
+ self.scroll_region.start = min(start, self.screen_lines());
+ self.scroll_region.end = min(end, self.screen_lines());
self.goto(Line(0), Column(0));
}
@@ -2216,6 +2254,79 @@ impl IndexMut<Column> for TabStops {
}
}
+/// Terminal test helpers.
+pub mod test {
+ use super::*;
+
+ use unicode_width::UnicodeWidthChar;
+
+ use crate::config::Config;
+ use crate::index::Column;
+
+ /// Construct a terminal from its content as string.
+ ///
+ /// A `\n` will break line and `\r\n` will break line without wrapping.
+ ///
+ /// # Examples
+ ///
+ /// ```rust
+ /// use alacritty_terminal::term::test::mock_term;
+ ///
+ /// // Create a terminal with the following cells:
+ /// //
+ /// // [h][e][l][l][o] <- WRAPLINE flag set
+ /// // [:][)][ ][ ][ ]
+ /// // [t][e][s][t][ ]
+ /// mock_term(
+ /// "\
+ /// hello\n:)\r\ntest",
+ /// );
+ /// ```
+ pub fn mock_term(content: &str) -> Term<()> {
+ let lines: Vec<&str> = content.split('\n').collect();
+ let num_cols = lines
+ .iter()
+ .map(|line| line.chars().filter(|c| *c != '\r').map(|c| c.width().unwrap()).sum())
+ .max()
+ .unwrap_or(0);
+
+ // Create terminal with the appropriate dimensions.
+ let size = SizeInfo {
+ width: num_cols as f32,
+ height: lines.len() as f32,
+ cell_width: 1.,
+ cell_height: 1.,
+ padding_x: 0.,
+ padding_y: 0.,
+ dpr: 1.,
+ };
+ let mut term = Term::new(&Config::<()>::default(), &size, ());
+
+ // Fill terminal with content.
+ for (line, text) in lines.iter().rev().enumerate() {
+ if !text.ends_with('\r') && line != 0 {
+ term.grid[line][Column(num_cols - 1)].flags.insert(Flags::WRAPLINE);
+ }
+
+ let mut index = 0;
+ for c in text.chars().take_while(|c| *c != '\r') {
+ term.grid[line][Column(index)].c = c;
+
+ // Handle fullwidth characters.
+ let width = c.width().unwrap();
+ if width == 2 {
+ term.grid[line][Column(index)].flags.insert(Flags::WIDE_CHAR);
+ term.grid[line][Column(index + 1)].flags.insert(Flags::WIDE_CHAR_SPACER);
+ }
+
+ index += width;
+ }
+ }
+
+ term
+ }
+}
+
#[cfg(test)]
mod tests {
use super::*;
diff --git a/alacritty_terminal/src/term/search.rs b/alacritty_terminal/src/term/search.rs
new file mode 100644
index 00000000..b1766b05
--- /dev/null
+++ b/alacritty_terminal/src/term/search.rs
@@ -0,0 +1,794 @@
+use std::cmp::min;
+use std::mem;
+use std::ops::RangeInclusive;
+
+use regex_automata::{dense, DenseDFA, Error as RegexError, DFA};
+
+use crate::grid::{BidirectionalIterator, Dimensions, GridIterator};
+use crate::index::{Boundary, Column, Direction, Point, Side};
+use crate::term::cell::{Cell, Flags};
+use crate::term::Term;
+
+/// Used to match equal brackets, when performing a bracket-pair selection.
+const BRACKET_PAIRS: [(char, char); 4] = [('(', ')'), ('[', ']'), ('{', '}'), ('<', '>')];
+
+pub type Match = RangeInclusive<Point<usize>>;
+
+/// Terminal regex search state.
+pub struct RegexSearch {
+ /// Locate end of match searching right.
+ right_fdfa: DenseDFA<Vec<usize>, usize>,
+ /// Locate start of match searching right.
+ right_rdfa: DenseDFA<Vec<usize>, usize>,
+
+ /// Locate start of match searching left.
+ left_fdfa: DenseDFA<Vec<usize>, usize>,
+ /// Locate end of match searching left.
+ left_rdfa: DenseDFA<Vec<usize>, usize>,
+}
+
+impl RegexSearch {
+ /// Build the forwards and backwards search DFAs.
+ pub fn new(search: &str) -> Result<RegexSearch, RegexError> {
+ // Check case info for smart case
+ let has_uppercase = search.chars().any(|c| c.is_uppercase());
+
+ // Create Regex DFAs for all search directions.
+ let mut builder = dense::Builder::new();
+ let builder = builder.case_insensitive(!has_uppercase);
+
+ let left_fdfa = builder.clone().reverse(true).build(search)?;
+ let left_rdfa = builder.clone().anchored(true).longest_match(true).build(search)?;
+
+ let right_fdfa = builder.clone().build(search)?;
+ let right_rdfa = builder.anchored(true).longest_match(true).reverse(true).build(search)?;
+
+ Ok(RegexSearch { right_fdfa, right_rdfa, left_fdfa, left_rdfa })
+ }
+}
+
+impl<T> Term<T> {
+ /// Enter terminal buffer search mode.
+ #[inline]
+ pub fn start_search(&mut self, search: &str) {
+ self.regex_search = RegexSearch::new(search).ok();
+ self.dirty = true;
+ }
+
+ /// Cancel active terminal buffer search.
+ #[inline]
+ pub fn cancel_search(&mut self) {
+ self.regex_search = None;
+ self.dirty = true;
+ }
+
+ /// Get next search match in the specified direction.
+ pub fn search_next(
+ &self,
+ mut origin: Point<usize>,
+ direction: Direction,
+ side: Side,
+ mut max_lines: Option<usize>,
+ ) -> Option<Match> {
+ origin = self.expand_wide(origin, direction);
+
+ max_lines = max_lines.filter(|max_lines| max_lines + 1 < self.total_lines());
+
+ match direction {
+ Direction::Right => self.next_match_right(origin, side, max_lines),
+ Direction::Left => self.next_match_left(origin, side, max_lines),
+ }
+ }
+
+ /// Find the next match to the right of the origin.
+ fn next_match_right(
+ &self,
+ origin: Point<usize>,
+ side: Side,
+ max_lines: Option<usize>,
+ ) -> Option<Match> {
+ // Skip origin itself to exclude it from the search results.
+ let origin = origin.add_absolute(self, Boundary::Wrap, 1);
+ let start = self.line_search_left(origin);
+ let mut end = start;
+
+ // Limit maximum number of lines searched.
+ let total_lines = self.total_lines();
+ end = match max_lines {
+ Some(max_lines) => {
+ let line = (start.line + total_lines - max_lines) % total_lines;
+ Point::new(line, self.cols() - 1)
+ },
+ _ => end.sub_absolute(self, Boundary::Wrap, 1),
+ };
+
+ let mut regex_iter = RegexIter::new(start, end, Direction::Right, &self).peekable();
+
+ // Check if there's any match at all.
+ let first_match = regex_iter.peek()?.clone();
+
+ let regex_match = regex_iter
+ .find(|regex_match| {
+ let match_point = Self::match_side(&regex_match, side);
+
+ // If the match's point is beyond the origin, we're done.
+ match_point.line > start.line
+ || match_point.line < origin.line
+ || (match_point.line == origin.line && match_point.col >= origin.col)
+ })
+ .unwrap_or(first_match);
+
+ Some(regex_match)
+ }
+
+ /// Find the next match to the left of the origin.
+ fn next_match_left(
+ &self,
+ origin: Point<usize>,
+ side: Side,
+ max_lines: Option<usize>,
+ ) -> Option<Match> {
+ // Skip origin itself to exclude it from the search results.
+ let origin = origin.sub_absolute(self, Boundary::Wrap, 1);
+ let start = self.line_search_right(origin);
+ let mut end = start;
+
+ // Limit maximum number of lines searched.
+ end = match max_lines {
+ Some(max_lines) => Point::new((start.line + max_lines) % self.total_lines(), Column(0)),
+ _ => end.add_absolute(self, Boundary::Wrap, 1),
+ };
+
+ let mut regex_iter = RegexIter::new(start, end, Direction::Left, &self).peekable();
+
+ // Check if there's any match at all.
+ let first_match = regex_iter.peek()?.clone();
+
+ let regex_match = regex_iter
+ .find(|regex_match| {
+ let match_point = Self::match_side(&regex_match, side);
+
+ // If the match's point is beyond the origin, we're done.
+ match_point.line < start.line
+ || match_point.line > origin.line
+ || (match_point.line == origin.line && match_point.col <= origin.col)
+ })
+ .unwrap_or(first_match);
+
+ Some(regex_match)
+ }
+
+ /// Get the side of a match.
+ fn match_side(regex_match: &Match, side: Side) -> Point<usize> {
+ match side {
+ Side::Right => *regex_match.end(),
+ Side::Left => *regex_match.start(),
+ }
+ }
+
+ /// Find the next regex match to the left of the origin point.
+ ///
+ /// The origin is always included in the regex.
+ pub fn regex_search_left(&self, start: Point<usize>, end: Point<usize>) -> Option<Match> {
+ let RegexSearch { left_fdfa: fdfa, left_rdfa: rdfa, .. } = self.regex_search.as_ref()?;
+
+ // Find start and end of match.
+ let match_start = self.regex_search(start, end, Direction::Left, &fdfa)?;
+ let match_end = self.regex_search(match_start, start, Direction::Right, &rdfa)?;
+
+ Some(match_start..=match_end)
+ }
+
+ /// Find the next regex match to the right of the origin point.
+ ///
+ /// The origin is always included in the regex.
+ pub fn regex_search_right(&self, start: Point<usize>, end: Point<usize>) -> Option<Match> {
+ let RegexSearch { right_fdfa: fdfa, right_rdfa: rdfa, .. } = self.regex_search.as_ref()?;
+
+ // Find start and end of match.
+ let match_end = self.regex_search(start, end, Direction::Right, &fdfa)?;
+ let match_start = self.regex_search(match_end, start, Direction::Left, &rdfa)?;
+
+ Some(match_start..=match_end)
+ }
+
+ /// Find the next regex match.
+ ///
+ /// This will always return the side of the first match which is farthest from the start point.
+ fn regex_search(
+ &self,
+ start: Point<usize>,
+ end: Point<usize>,
+ direction: Direction,
+ dfa: &impl DFA,
+ ) -> Option<Point<usize>> {
+ let last_line = self.total_lines() - 1;
+ let last_col = self.cols() - 1;
+
+ // Advance the iterator.
+ let next = match direction {
+ Direction::Right => GridIterator::next,
+ Direction::Left => GridIterator::prev,
+ };
+
+ let mut iter = self.grid.iter_from(start);
+ let mut state = dfa.start_state();
+ let mut regex_match = None;
+
+ let mut cell = *iter.cell();
+ self.skip_fullwidth(&mut iter, &mut cell, direction);
+ let mut point = iter.point();
+
+ loop {
+ // Convert char to array of bytes.
+ let mut buf = [0; 4];
+ let utf8_len = cell.c.encode_utf8(&mut buf).len();
+
+ // Pass char to DFA as individual bytes.
+ for i in 0..utf8_len {
+ // Inverse byte order when going left.
+ let byte = match direction {
+ Direction::Right => buf[i],
+ Direction::Left => buf[utf8_len - i - 1],
+ };
+
+ // Since we get the state from the DFA, it doesn't need to be checked.
+ state = unsafe { dfa.next_state_unchecked(state, byte) };
+ }
+
+ // Handle regex state changes.
+ if dfa.is_match_or_dead_state(state) {
+ if dfa.is_dead_state(state) {
+ break;
+ } else {
+ regex_match = Some(point);
+ }
+ }
+
+ // Stop once we've reached the target point.
+ if point == end {
+ break;
+ }
+
+ // Advance grid cell iterator.
+ let mut new_cell = match next(&mut iter) {
+ Some(&cell) => cell,
+ None => {
+ // Wrap around to other end of the scrollback buffer.
+ let start = Point::new(last_line - point.line, last_col - point.col);
+ iter = self.grid.iter_from(start);
+ *iter.cell()
+ },
+ };
+ self.skip_fullwidth(&mut iter, &mut new_cell, direction);
+ let last_point = mem::replace(&mut point, iter.point());
+ let last_cell = mem::replace(&mut cell, new_cell);
+
+ // Handle linebreaks.
+ if (last_point.col == last_col
+ && point.col == Column(0)
+ && !last_cell.flags.contains(Flags::WRAPLINE))
+ || (last_point.col == Column(0)
+ && point.col == last_col
+ && !cell.flags.contains(Flags::WRAPLINE))
+ {
+ match regex_match {
+ Some(_) => break,
+ None => state = dfa.start_state(),
+ }
+ }
+ }
+
+ regex_match
+ }
+
+ /// Advance a grid iterator over fullwidth characters.
+ fn skip_fullwidth(
+ &self,
+ iter: &mut GridIterator<'_, Cell>,
+ cell: &mut Cell,
+ direction: Direction,
+ ) {
+ match direction {
+ Direction::Right if cell.flags.contains(Flags::WIDE_CHAR) => {
+ iter.next();
+ },
+ Direction::Right if cell.flags.contains(Flags::LEADING_WIDE_CHAR_SPACER) => {
+ if let Some(new_cell) = iter.next() {
+ *cell = *new_cell;
+ }
+ iter.next();
+ },
+ Direction::Left if cell.flags.contains(Flags::WIDE_CHAR_SPACER) => {
+ if let Some(new_cell) = iter.prev() {
+ *cell = *new_cell;
+ }
+
+ let prev = iter.point().sub_absolute(self, Boundary::Clamp, 1);
+ if self.grid[prev].flags.contains(Flags::LEADING_WIDE_CHAR_SPACER) {
+ iter.prev();
+ }
+ },
+ _ => (),
+ }
+ }
+
+ /// Find next matching bracket.
+ pub fn bracket_search(&self, point: Point<usize>) -> Option<Point<usize>> {
+ let start_char = self.grid[point.line][point.col].c;
+
+ // Find the matching bracket we're looking for
+ let (forwards, end_char) = BRACKET_PAIRS.iter().find_map(|(open, close)| {
+ if open == &start_char {
+ Some((true, *close))
+ } else if close == &start_char {
+ Some((false, *open))
+ } else {
+ None
+ }
+ })?;
+
+ let mut iter = self.grid.iter_from(point);
+
+ // For every character match that equals the starting bracket, we
+ // ignore one bracket of the opposite type.
+ let mut skip_pairs = 0;
+
+ loop {
+ // Check the next cell
+ let cell = if forwards { iter.next() } else { iter.prev() };
+
+ // Break if there are no more cells
+ let c = match cell {
+ Some(cell) => cell.c,
+ None => break,
+ };
+
+ // Check if the bracket matches
+ if c == end_char && skip_pairs == 0 {
+ return Some(iter.point());
+ } else if c == start_char {
+ skip_pairs += 1;
+ } else if c == end_char {
+ skip_pairs -= 1;
+ }
+ }
+
+ None
+ }
+
+ /// Find left end of semantic block.
+ pub fn semantic_search_left(&self, mut point: Point<usize>) -> Point<usize> {
+ // Limit the starting point to the last line in the history
+ point.line = min(point.line, self.total_lines() - 1);
+
+ let mut iter = self.grid.iter_from(point);
+ let last_col = self.cols() - Column(1);
+
+ let wide = Flags::WIDE_CHAR | Flags::WIDE_CHAR_SPACER | Flags::LEADING_WIDE_CHAR_SPACER;
+ while let Some(cell) = iter.prev() {
+ if !cell.flags.intersects(wide) && self.semantic_escape_chars.contains(cell.c) {
+ break;
+ }
+
+ if iter.point().col == last_col && !cell.flags.contains(Flags::WRAPLINE) {
+ break; // cut off if on new line or hit escape char
+ }
+
+ point = iter.point();
+ }
+
+ point
+ }
+
+ /// Find right end of semantic block.
+ pub fn semantic_search_right(&self, mut point: Point<usize>) -> Point<usize> {
+ // Limit the starting point to the last line in the history
+ point.line = min(point.line, self.total_lines() - 1);
+
+ let mut iter = self.grid.iter_from(point);
+ let last_col = self.cols() - 1;
+
+ let wide = Flags::WIDE_CHAR | Flags::WIDE_CHAR_SPACER | Flags::LEADING_WIDE_CHAR_SPACER;
+ while let Some(cell) = iter.next() {
+ if !cell.flags.intersects(wide) && self.semantic_escape_chars.contains(cell.c) {
+ break;
+ }
+
+ point = iter.point();
+
+ if point.col == last_col && !cell.flags.contains(Flags::WRAPLINE) {
+ break; // cut off if on new line or hit escape char
+ }
+ }
+
+ point
+ }
+
+ /// Find the beginning of the current line across linewraps.
+ pub fn line_search_left(&self, mut point: Point<usize>) -> Point<usize> {
+ while point.line + 1 < self.total_lines()
+ && self.grid[point.line + 1][self.cols() - 1].flags.contains(Flags::WRAPLINE)
+ {
+ point.line += 1;
+ }
+
+ point.col = Column(0);
+
+ point
+ }
+
+ /// Find the end of the current line across linewraps.
+ pub fn line_search_right(&self, mut point: Point<usize>) -> Point<usize> {
+ while self.grid[point.line][self.cols() - 1].flags.contains(Flags::WRAPLINE) {
+ point.line -= 1;
+ }
+
+ point.col = self.cols() - 1;
+
+ point
+ }
+}
+
+/// Iterator over regex matches.
+pub struct RegexIter<'a, T> {
+ point: Point<usize>,
+ end: Point<usize>,
+ direction: Direction,
+ term: &'a Term<T>,
+ done: bool,
+}
+
+impl<'a, T> RegexIter<'a, T> {
+ pub fn new(
+ start: Point<usize>,
+ end: Point<usize>,
+ direction: Direction,
+ term: &'a Term<T>,
+ ) -> Self {
+ Self { point: start, done: false, end, direction, term }
+ }
+
+ /// Skip one cell, advancing the origin point to the next one.
+ fn skip(&mut self) {
+ self.point = self.term.expand_wide(self.point, self.direction);
+
+ self.point = match self.direction {
+ Direction::Right => self.point.add_absolute(self.term, Boundary::Wrap, 1),
+ Direction::Left => self.point.sub_absolute(self.term, Boundary::Wrap, 1),
+ };
+ }
+
+ /// Get the next match in the specified direction.
+ fn next_match(&self) -> Option<Match> {
+ match self.direction {
+ Direction::Right => self.term.regex_search_right(self.point, self.end),
+ Direction::Left => self.term.regex_search_left(self.point, self.end),
+ }
+ }
+}
+
+impl<'a, T> Iterator for RegexIter<'a, T> {
+ type Item = Match;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ if self.point == self.end {
+ self.done = true;
+ } else if self.done {
+ return None;
+ }
+
+ let regex_match = self.next_match()?;
+
+ self.point = *regex_match.end();
+ self.skip();
+
+ Some(regex_match)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ use crate::index::Column;
+ use crate::term::test::mock_term;
+
+ #[test]
+ fn regex_right() {
+ #[rustfmt::skip]
+ let mut term = mock_term("\
+ testing66\r\n\
+ Alacritty\n\
+ 123\r\n\
+ Alacritty\r\n\
+ 123\
+ ");
+
+ // Check regex across wrapped and unwrapped lines.
+ term.regex_search = Some(RegexSearch::new("Ala.*123").unwrap());
+ let start = Point::new(3, Column(0));
+ let end = Point::new(0, Column(2));
+ let match_start = Point::new(3, Column(0));
+ let match_end = Point::new(2, Column(2));
+ assert_eq!(term.regex_search_right(start, end), Some(match_start..=match_end));
+ }
+
+ #[test]
+ fn regex_left() {
+ #[rustfmt::skip]
+ let mut term = mock_term("\
+ testing66\r\n\
+ Alacritty\n\
+ 123\r\n\
+ Alacritty\r\n\
+ 123\
+ ");
+
+ // Check regex across wrapped and unwrapped lines.
+ term.regex_search = Some(RegexSearch::new("Ala.*123").unwrap());
+ let start = Point::new(0, Column(2));
+ let end = Point::new(3, Column(0));
+ let match_start = Point::new(3, Column(0));
+ let match_end = Point::new(2, Column(2));
+ assert_eq!(term.regex_search_left(start, end), Some(match_start..=match_end));
+ }
+
+ #[test]
+ fn nested_regex() {
+ #[rustfmt::skip]
+ let mut term = mock_term("\
+ Ala -> Alacritty -> critty\r\n\
+ critty\
+ ");
+
+ // Greedy stopped at linebreak.
+ term.regex_search = Some(RegexSearch::new("Ala.*critty").unwrap());
+ let start = Point::new(1, Column(0));
+ let end = Point::new(1, Column(25));
+ assert_eq!(term.regex_search_right(start, end), Some(start..=end));
+
+ // Greedy stopped at dead state.
+ term.regex_search = Some(RegexSearch::new("Ala[^y]*critty").unwrap());
+ let start = Point::new(1, Column(0));
+ let end = Point::new(1, Column(15));
+ assert_eq!(term.regex_search_right(start, end), Some(start..=end));
+ }
+
+ #[test]
+ fn no_match_right() {
+ #[rustfmt::skip]
+ let mut term = mock_term("\
+ first line\n\
+ broken second\r\n\
+ third\
+ ");
+
+ term.regex_search = Some(RegexSearch::new("nothing").unwrap());
+ let start = Point::new(2, Column(0));
+ let end = Point::new(0, Column(4));
+ assert_eq!(term.regex_search_right(start, end), None);
+ }
+
+ #[test]
+ fn no_match_left() {
+ #[rustfmt::skip]
+ let mut term = mock_term("\
+ first line\n\
+ broken second\r\n\
+ third\
+ ");
+
+ term.regex_search = Some(RegexSearch::new("nothing").unwrap());
+ let start = Point::new(0, Column(4));
+ let end = Point::new(2, Column(0));
+ assert_eq!(term.regex_search_left(start, end), None);
+ }
+
+ #[test]
+ fn include_linebreak_left() {
+ #[rustfmt::skip]
+ let mut term = mock_term("\
+ testing123\r\n\
+ xxx\
+ ");
+
+ // Make sure the cell containing the linebreak is not skipped.
+ term.regex_search = Some(RegexSearch::new("te.*123").unwrap());
+ let start = Point::new(0, Column(0));
+ let end = Point::new(1, Column(0));
+ let match_start = Point::new(1, Column(0));
+ let match_end = Point::new(1, Column(9));
+ assert_eq!(term.regex_search_left(start, end), Some(match_start..=match_end));
+ }
+
+ #[test]
+ fn include_linebreak_right() {
+ #[rustfmt::skip]
+ let mut term = mock_term("\
+ xxx\r\n\
+ testing123\
+ ");
+
+ // Make sure the cell containing the linebreak is not skipped.
+ term.regex_search = Some(RegexSearch::new("te.*123").unwrap());
+ let start = Point::new(1, Column(2));
+ let end = Point::new(0, Column(9));
+ let match_start = Point::new(0, Column(0));
+ assert_eq!(term.regex_search_right(start, end), Some(match_start..=end));
+ }
+
+ #[test]
+ fn skip_dead_cell() {
+ let mut term = mock_term("alacritty");
+
+ // Make sure dead state cell is skipped when reversing.
+ term.regex_search = Some(RegexSearch::new("alacrit").unwrap());
+ let start = Point::new(0, Column(0));
+ let end = Point::new(0, Column(6));
+ assert_eq!(term.regex_search_right(start, end), Some(start..=end));
+ }
+
+ #[test]
+ fn reverse_search_dead_recovery() {
+ let mut term = mock_term("zooo lense");
+
+ // Make sure the reverse DFA operates the same as a forwards DFA.
+ term.regex_search = Some(RegexSearch::new("zoo").unwrap());
+ let start = Point::new(0, Column(9));
+ let end = Point::new(0, Column(0));
+ let match_start = Point::new(0, Column(0));
+ let match_end = Point::new(0, Column(2));
+ assert_eq!(term.regex_search_left(start, end), Some(match_start..=match_end));
+ }
+
+ #[test]
+ fn multibyte_unicode() {
+ let mut term = mock_term("testвосибing");
+
+ term.regex_search = Some(RegexSearch::new("te.*ing").unwrap());
+ let start = Point::new(0, Column(0));
+ let end = Point::new(0, Column(11));
+ assert_eq!(term.regex_search_right(start, end), Some(start..=end));
+
+ term.regex_search = Some(RegexSearch::new("te.*ing").unwrap());
+ let start = Point::new(0, Column(11));
+ let end = Point::new(0, Column(0));
+ assert_eq!(term.regex_search_left(start, end), Some(end..=start));
+ }
+
+ #[test]
+ fn fullwidth() {
+ let mut term = mock_term("a🦇x🦇");
+
+ term.regex_search = Some(RegexSearch::new("[^ ]*").unwrap());
+ let start = Point::new(0, Column(0));
+ let end = Point::new(0, Column(5));
+ assert_eq!(term.regex_search_right(start, end), Some(start..=end));
+
+ term.regex_search = Some(RegexSearch::new("[^ ]*").unwrap());
+ let start = Point::new(0, Column(5));
+ let end = Point::new(0, Column(0));
+ assert_eq!(term.regex_search_left(start, end), Some(end..=start));
+ }
+
+ #[test]
+ fn singlecell_fullwidth() {
+ let mut term = mock_term("🦇");
+
+ term.regex_search = Some(RegexSearch::new("🦇").unwrap());
+ let start = Point::new(0, Column(0));
+ let end = Point::new(0, Column(1));
+ assert_eq!(term.regex_search_right(start, end), Some(start..=end));
+
+ term.regex_search = Some(RegexSearch::new("🦇").unwrap());
+ let start = Point::new(0, Column(1));
+ let end = Point::new(0, Column(0));
+ assert_eq!(term.regex_search_left(start, end), Some(end..=start));
+ }
+
+ #[test]
+ fn wrapping() {
+ #[rustfmt::skip]
+ let mut term = mock_term("\
+ xxx\r\n\
+ xxx\
+ ");
+
+ term.regex_search = Some(RegexSearch::new("xxx").unwrap());
+ let start = Point::new(0, Column(2));
+ let end = Point::new(1, Column(2));
+ let match_start = Point::new(1, Column(0));
+ assert_eq!(term.regex_search_right(start, end), Some(match_start..=end));
+
+ term.regex_search = Some(RegexSearch::new("xxx").unwrap());
+ let start = Point::new(1, Column(0));
+ let end = Point::new(0, Column(0));
+ let match_end = Point::new(0, Column(2));
+ assert_eq!(term.regex_search_left(start, end), Some(end..=match_end));
+ }
+
+ #[test]
+ fn wrapping_into_fullwidth() {
+ #[rustfmt::skip]
+ let mut term = mock_term("\
+ 🦇xx\r\n\
+ xx🦇\
+ ");
+
+ term.regex_search = Some(RegexSearch::new("🦇x").unwrap());
+ let start = Point::new(0, Column(0));
+ let end = Point::new(1, Column(3));
+ let match_start = Point::new(1, Column(0));
+ let match_end = Point::new(1, Column(2));
+ assert_eq!(term.regex_search_right(start, end), Some(match_start..=match_end));
+
+ term.regex_search = Some(RegexSearch::new("x🦇").unwrap());
+ let start = Point::new(1, Column(2));
+ let end = Point::new(0, Column(0));
+ let match_start = Point::new(0, Column(1));
+ let match_end = Point::new(0, Column(3));
+ assert_eq!(term.regex_search_left(start, end), Some(match_start..=match_end));
+ }
+
+ #[test]
+ fn leading_spacer() {
+ #[rustfmt::skip]
+ let mut term = mock_term("\
+ xxx \n\
+ 🦇xx\
+ ");
+ term.grid[1][Column(3)].flags.insert(Flags::LEADING_WIDE_CHAR_SPACER);
+
+ term.regex_search = Some(RegexSearch::new("🦇x").unwrap());
+ let start = Point::new(1, Column(0));
+ let end = Point::new(0, Column(3));
+ let match_start = Point::new(1, Column(3));
+ let match_end = Point::new(0, Column(2));
+ assert_eq!(term.regex_search_right(start, end), Some(match_start..=match_end));
+
+ term.regex_search = Some(RegexSearch::new("🦇x").unwrap());
+ let start = Point::new(0, Column(3));
+ let end = Point::new(1, Column(0));
+ let match_start = Point::new(1, Column(3));
+ let match_end = Point::new(0, Column(2));
+ assert_eq!(term.regex_search_left(start, end), Some(match_start..=match_end));
+
+ term.regex_search = Some(RegexSearch::new("x🦇").unwrap());
+ let start = Point::new(1, Column(0));
+ let end = Point::new(0, Column(3));
+ let match_start = Point::new(1, Column(2));
+ let match_end = Point::new(0, Column(1));
+ assert_eq!(term.regex_search_right(start, end), Some(match_start..=match_end));
+
+ term.regex_search = Some(RegexSearch::new("x🦇").unwrap());
+ let start = Point::new(0, Column(3));
+ let end = Point::new(1, Column(0));
+ let match_start = Point::new(1, Column(2));
+ let match_end = Point::new(0, Column(1));
+ assert_eq!(term.regex_search_left(start, end), Some(match_start..=match_end));
+ }
+}
+
+#[cfg(all(test, feature = "bench"))]
+mod benches {
+ extern crate test;
+
+ use super::*;
+
+ use crate::term::test::mock_term;
+
+ #[bench]
+ fn regex_search(b: &mut test::Bencher) {
+ let input = format!("{:^10000}", "Alacritty");
+ let mut term = mock_term(&input);
+ term.regex_search = Some(RegexSearch::new(" Alacritty ").unwrap());
+ let start = Point::new(0, Column(0));
+ let end = Point::new(0, Column(input.len() - 1));
+
+ b.iter(|| {
+ test::black_box(term.regex_search_right(start, end));
+ test::black_box(term.regex_search_left(end, start));
+ });
+ }
+}
diff --git a/alacritty_terminal/src/vi_mode.rs b/alacritty_terminal/src/vi_mode.rs
index 6621eda5..985d5455 100644
--- a/alacritty_terminal/src/vi_mode.rs
+++ b/alacritty_terminal/src/vi_mode.rs
@@ -3,10 +3,10 @@ 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::grid::{Dimensions, GridCell};
+use crate::index::{Boundary, Column, Direction, Line, Point, Side};
use crate::term::cell::Flags;
-use crate::term::{Search, Term};
+use crate::term::Term;
/// Possible vi mode motion movements.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize)]
@@ -66,23 +66,23 @@ impl ViModeCursor {
#[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 lines = term.grid().screen_lines();
+ let cols = term.grid().cols();
let mut buffer_point = term.visible_to_buffer(self.point);
match motion {
ViMotion::Up => {
- if buffer_point.line + 1 < term.grid().len() {
+ if buffer_point.line + 1 < term.grid().total_lines() {
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);
+ buffer_point = term.expand_wide(buffer_point, Direction::Left);
let wrap_point = Point::new(buffer_point.line + 1, cols - 1);
if buffer_point.col.0 == 0
- && buffer_point.line + 1 < term.grid().len()
+ && buffer_point.line + 1 < term.grid().total_lines()
&& is_wrap(term, wrap_point)
{
buffer_point = wrap_point;
@@ -91,7 +91,7 @@ impl ViModeCursor {
}
},
ViMotion::Right => {
- buffer_point = expand_wide(term, buffer_point, false);
+ buffer_point = term.expand_wide(buffer_point, Direction::Right);
if is_wrap(term, buffer_point) {
buffer_point = Point::new(buffer_point.line - 1, Column(0));
} else {
@@ -99,9 +99,9 @@ impl ViModeCursor {
}
},
ViMotion::First => {
- buffer_point = expand_wide(term, buffer_point, true);
+ buffer_point = term.expand_wide(buffer_point, Direction::Left);
while buffer_point.col.0 == 0
- && buffer_point.line + 1 < term.grid().len()
+ && buffer_point.line + 1 < term.grid().total_lines()
&& is_wrap(term, Point::new(buffer_point.line + 1, cols - 1))
{
buffer_point.line += 1;
@@ -125,20 +125,36 @@ impl ViModeCursor {
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::SemanticLeft => {
+ buffer_point = semantic(term, buffer_point, Direction::Left, Side::Left);
+ },
+ ViMotion::SemanticRight => {
+ buffer_point = semantic(term, buffer_point, Direction::Right, Side::Left);
+ },
+ ViMotion::SemanticLeftEnd => {
+ buffer_point = semantic(term, buffer_point, Direction::Left, Side::Right);
+ },
+ ViMotion::SemanticRightEnd => {
+ buffer_point = semantic(term, buffer_point, Direction::Right, Side::Right);
+ },
+ ViMotion::WordLeft => {
+ buffer_point = word(term, buffer_point, Direction::Left, Side::Left);
+ },
+ ViMotion::WordRight => {
+ buffer_point = word(term, buffer_point, Direction::Right, Side::Left);
+ },
+ ViMotion::WordLeftEnd => {
+ buffer_point = word(term, buffer_point, Direction::Left, Side::Right);
+ },
+ ViMotion::WordRightEnd => {
+ buffer_point = word(term, buffer_point, Direction::Right, Side::Right);
+ },
ViMotion::Bracket => {
buffer_point = term.bracket_search(buffer_point).unwrap_or(buffer_point);
},
}
- scroll_to_point(term, buffer_point);
+ term.scroll_to_point(buffer_point);
self.point = term.grid().clamp_buffer_to_visible(buffer_point);
self
@@ -159,12 +175,12 @@ impl ViModeCursor {
// 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));
+ line = max(0, min(term.grid().screen_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));
+ target_line = max(0, min(term.grid().total_lines() as isize - 1, target_line));
let col = first_occupied_in_line(term, target_line as usize).unwrap_or_default().col;
// Move cursor.
@@ -174,27 +190,12 @@ impl ViModeCursor {
}
}
-/// 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();
+ let cols = term.grid().cols();
// Expand across wide cells.
- point = expand_wide(term, point, false);
+ point = term.expand_wide(point, Direction::Right);
// Find last non-empty cell in the current line.
let occupied = last_occupied_in_line(term, point.line).unwrap_or_default();
@@ -217,10 +218,10 @@ fn last<T>(term: &Term<T>, mut point: Point<usize>) -> Point<usize> {
/// 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();
+ let cols = term.grid().cols();
// Expand left across wide chars, since we're searching lines left to right.
- point = expand_wide(term, point, true);
+ point = term.expand_wide(point, Direction::Left);
// Find first non-empty cell in current line.
let occupied = first_occupied_in_line(term, point.line)
@@ -231,7 +232,7 @@ fn first_occupied<T>(term: &Term<T>, mut point: Point<usize>) -> Point<usize> {
let mut occupied = None;
// Search for non-empty cell in previous lines.
- for line in (point.line + 1)..term.grid().len() {
+ for line in (point.line + 1)..term.grid().total_lines() {
if !is_wrap(term, Point::new(line, cols - 1)) {
break;
}
@@ -262,18 +263,18 @@ fn first_occupied<T>(term: &Term<T>, mut point: Point<usize>) -> Point<usize> {
fn semantic<T: EventListener>(
term: &mut Term<T>,
mut point: Point<usize>,
- left: bool,
- start: bool,
+ direction: Direction,
+ side: Side,
) -> 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)
+ && !cell.flags.intersects(Flags::WIDE_CHAR_SPACER | Flags::LEADING_WIDE_CHAR_SPACER)
{
point
- } else if left {
+ } else if direction == Direction::Left {
term.semantic_search_left(point)
} else {
term.semantic_search_right(point)
@@ -281,27 +282,27 @@ fn semantic<T: EventListener>(
};
// Make sure we jump above wide chars.
- point = expand_wide(term, point, left);
+ point = term.expand_wide(point, direction);
// Move to word boundary.
- if left != start && !is_boundary(term, point, left) {
+ if direction != side && !is_boundary(term, point, direction) {
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) {
+ let mut next_point = advance(term, point, direction);
+ while !is_boundary(term, point, direction) && is_space(term, next_point) {
point = next_point;
- next_point = advance(term, point, left);
+ next_point = advance(term, point, direction);
}
// Assure minimum movement of one cell.
- if !is_boundary(term, point, left) {
- point = advance(term, point, left);
+ if !is_boundary(term, point, direction) {
+ point = advance(term, point, direction);
}
// Move to word boundary.
- if left == start && !is_boundary(term, point, left) {
+ if direction == side && !is_boundary(term, point, direction) {
point = expand_semantic(point);
}
@@ -312,90 +313,71 @@ fn semantic<T: EventListener>(
fn word<T: EventListener>(
term: &mut Term<T>,
mut point: Point<usize>,
- left: bool,
- start: bool,
+ direction: Direction,
+ side: Side,
) -> Point<usize> {
// Make sure we jump above wide chars.
- point = expand_wide(term, point, left);
+ point = term.expand_wide(point, direction);
- if left == start {
+ if direction == side {
// 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) {
+ let mut next_point = advance(term, point, direction);
+ while !is_boundary(term, point, direction) && is_space(term, next_point) {
point = next_point;
- next_point = advance(term, point, left);
+ next_point = advance(term, point, direction);
}
// 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) {
+ let mut next_point = advance(term, point, direction);
+ while !is_boundary(term, point, direction) && !is_space(term, next_point) {
point = next_point;
- next_point = advance(term, point, left);
+ next_point = advance(term, point, direction);
}
}
- if left != start {
+ if direction != side {
// Skip non-whitespace until just beyond word.
- while !is_boundary(term, point, left) && !is_space(term, point) {
- point = advance(term, point, left);
+ while !is_boundary(term, point, direction) && !is_space(term, point) {
+ point = advance(term, point, direction);
}
// Skip whitespace until right inside word boundary.
- while !is_boundary(term, point, left) && is_space(term, point) {
- point = advance(term, point, left);
+ while !is_boundary(term, point, direction) && is_space(term, point) {
+ point = advance(term, point, direction);
}
}
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)
+ (0..term.grid().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)
+ (0..term.grid().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> {
- if left {
- point.sub_absolute(term.grid().num_cols(), 1)
+fn advance<T>(term: &Term<T>, point: Point<usize>, direction: Direction) -> Point<usize> {
+ if direction == Direction::Left {
+ point.sub_absolute(term, Boundary::Clamp, 1)
} else {
- point.add_absolute(term.grid().num_cols(), 1)
+ point.add_absolute(term, Boundary::Clamp, 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)
+ !cell.flags().intersects(Flags::WIDE_CHAR_SPACER | Flags::LEADING_WIDE_CHAR_SPACER)
+ && (cell.c == ' ' || cell.c == '\t')
}
fn is_wrap<T>(term: &Term<T>, point: Point<usize>) -> bool {
@@ -403,9 +385,11 @@ fn is_wrap<T>(term: &Term<T>, point: Point<usize>) -> bool {
}
/// 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)
+fn is_boundary<T>(term: &Term<T>, point: Point<usize>, direction: Direction) -> bool {
+ let total_lines = term.grid().total_lines();
+ let num_cols = term.grid().cols();
+ (point.line + 1 >= total_lines && point.col.0 == 0 && direction == Direction::Left)
+ || (point.line == 0 && point.col + 1 >= num_cols && direction == Direction::Right)
}
#[cfg(test)]
diff --git a/alacritty_terminal/tests/ref.rs b/alacritty_terminal/tests/ref.rs
index 9c5bbda5..62439775 100644
--- a/alacritty_terminal/tests/ref.rs
+++ b/alacritty_terminal/tests/ref.rs
@@ -8,11 +8,10 @@ use std::path::Path;
use alacritty_terminal::ansi;
use alacritty_terminal::config::MockConfig;
use alacritty_terminal::event::{Event, EventListener};
+use alacritty_terminal::grid::{Dimensions, Grid};
use alacritty_terminal::index::Column;
use alacritty_terminal::term::cell::Cell;
-use alacritty_terminal::term::SizeInfo;
-use alacritty_terminal::Grid;
-use alacritty_terminal::Term;
+use alacritty_terminal::term::{SizeInfo, Term};
macro_rules! ref_tests {
($($name:ident)*) => {
@@ -114,8 +113,8 @@ fn ref_test(dir: &Path) {
term_grid.truncate();
if grid != term_grid {
- for i in 0..grid.len() {
- for j in 0..grid.num_cols().0 {
+ for i in 0..grid.total_lines() {
+ for j in 0..grid.cols().0 {
let cell = term_grid[i][Column(j)];
let original_cell = grid[i][Column(j)];
if original_cell != cell {