summaryrefslogtreecommitdiffstats
path: root/alacritty_terminal/src/term/mod.rs
diff options
context:
space:
mode:
Diffstat (limited to 'alacritty_terminal/src/term/mod.rs')
-rw-r--r--alacritty_terminal/src/term/mod.rs677
1 files changed, 394 insertions, 283 deletions
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
- l