diff --git a/CHANGELOG.md b/CHANGELOG.md index f8d615b7..5570807d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -100,6 +100,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Unicode 13 support - Option to run command on bell which can be set in `bell.command` - Fallback to program specified in `$SHELL` variable on Linux/BSD if it is present +- Ability to make selections while search is active ### Changed @@ -120,6 +121,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - URLs are no longer highlighted without a clearly delimited scheme - Renamed config option `visual_bell` to `bell` - Moved config option `dynamic_title` to `window.dynamic_title` +- When searching without vi mode, matches are only selected once search is cancelled ### Fixed @@ -141,6 +143,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Ingoring of default FreeType properties - Alacritty crashing at startup when the configured font does not exist - Font size rounding error +- Opening URLs while search is active ### Removed diff --git a/alacritty.yml b/alacritty.yml index 7795136d..2c96d23e 100644 --- a/alacritty.yml +++ b/alacritty.yml @@ -227,6 +227,9 @@ #matches: # foreground: '#000000' # background: '#ffffff' + #focused_match: + # foreground: CellBackground + # background: CellForeground #bar: # background: '#c5c8c6' diff --git a/alacritty/src/display.rs b/alacritty/src/display.rs index 6d683336..af21001e 100644 --- a/alacritty/src/display.rs +++ b/alacritty/src/display.rs @@ -27,7 +27,7 @@ use crossfont::{self, Rasterize, Rasterizer}; use alacritty_terminal::event::{EventListener, OnResize}; use alacritty_terminal::index::{Column, Direction, Point}; use alacritty_terminal::selection::Selection; -use alacritty_terminal::term::{RenderableCell, SizeInfo, Term, TermMode}; +use alacritty_terminal::term::{SizeInfo, Term, TermMode}; use alacritty_terminal::term::{MIN_COLS, MIN_SCREEN_LINES}; use crate::config::font::Font; @@ -437,7 +437,13 @@ impl Display { mods: ModifiersState, search_state: &SearchState, ) { - let grid_cells: Vec = terminal.renderable_cells(config).collect(); + // Convert search match from viewport to absolute indexing. + let search_active = search_state.regex().is_some(); + let viewport_match = search_state + .focused_match() + .and_then(|focused_match| terminal.grid().clamp_buffer_range_to_visible(focused_match)); + + let grid_cells = terminal.renderable_cells(config, !search_active).collect::>(); let visual_bell_intensity = terminal.visual_bell.intensity(); let background_color = terminal.background_color(); let cursor_point = terminal.grid().cursor.point; @@ -471,7 +477,21 @@ impl Display { self.renderer.with_api(&config.ui_config, config.cursor, &size_info, |mut api| { // Iterate over all non-empty cells in the grid. - for cell in grid_cells { + for mut cell in grid_cells { + // Invert the active match in vi-less search. + let cell_point = Point::new(cell.line, cell.column); + if cell.is_match + && viewport_match + .as_ref() + .map_or(false, |viewport_match| viewport_match.contains(&cell_point)) + { + let colors = config.colors.search.focused_match; + let match_fg = colors.foreground().color(cell.fg, cell.bg); + cell.bg = colors.background().color(cell.fg, cell.bg); + cell.fg = match_fg; + cell.bg_alpha = 1.0; + } + // Update URL underlines. urls.update(size_info.cols(), &cell); @@ -525,7 +545,7 @@ impl Display { } if let Some(message) = message_buffer.message() { - let search_offset = if search_state.regex().is_some() { 1 } else { 0 }; + let search_offset = if search_active { 1 } else { 0 }; let text = message.text(&size_info); // Create a new rectangle for the background. diff --git a/alacritty/src/event.rs b/alacritty/src/event.rs index 20f087c3..c1f81300 100644 --- a/alacritty/src/event.rs +++ b/alacritty/src/event.rs @@ -9,6 +9,7 @@ use std::fs; use std::fs::File; use std::io::Write; use std::mem; +use std::ops::RangeInclusive; use std::path::PathBuf; #[cfg(not(any(target_os = "macos", windows)))] use std::sync::atomic::Ordering; @@ -94,6 +95,9 @@ pub struct SearchState { /// Search origin in viewport coordinates relative to original display offset. origin: Point, + + /// Focused match during active search. + focused_match: Option>>, } impl SearchState { @@ -110,6 +114,11 @@ impl SearchState { pub fn direction(&self) -> Direction { self.direction } + + /// Focused match during vi-less search. + pub fn focused_match(&self) -> Option<&RangeInclusive>> { + self.focused_match.as_ref() + } } impl Default for SearchState { @@ -118,6 +127,7 @@ impl Default for SearchState { direction: Direction::Right, display_offset_delta: 0, origin: Point::default(), + focused_match: None, regex: None, } } @@ -209,7 +219,7 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext for ActionCon selection.update(absolute_point, side); // Move vi cursor and expand selection. - if self.terminal.mode().contains(TermMode::VI) { + if self.terminal.mode().contains(TermMode::VI) && !self.search_active() { self.terminal.vi_mode_cursor.point = point; selection.include_all(); } @@ -385,6 +395,7 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext for ActionCon let num_lines = self.terminal.screen_lines(); let num_cols = self.terminal.cols(); + self.search_state.focused_match = None; self.search_state.regex = Some(String::new()); self.search_state.direction = direction; @@ -393,9 +404,6 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext for ActionCon self.search_state.origin = if self.terminal.mode().contains(TermMode::VI) { self.terminal.vi_mode_cursor.point } else { - // Clear search, since it is used as the active match. - self.terminal.selection = None; - match direction { Direction::Right => Point::new(Line(0), Column(0)), Direction::Left => Point::new(num_lines - 2, num_cols - 1), @@ -420,9 +428,15 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext for ActionCon fn cancel_search(&mut self) { self.terminal.cancel_search(); - // Recover pre-search state in vi mode. if self.terminal.mode().contains(TermMode::VI) { + // Recover pre-search state in vi mode. self.search_reset_state(); + } else if let Some(focused_match) = &self.search_state.focused_match { + // Create a selection for the focused match. + let start = self.terminal.grid().clamp_buffer_to_visible(*focused_match.start()); + let end = self.terminal.grid().clamp_buffer_to_visible(*focused_match.end()); + self.start_selection(SelectionType::Simple, start, Side::Left); + self.update_selection(end, Side::Right); } self.exit_search(); @@ -431,8 +445,8 @@ impl<'a, N: Notify + 'a, T: EventListener> input::ActionContext for ActionCon #[inline] fn push_search(&mut self, c: char) { if let Some(regex) = self.search_state.regex.as_mut() { - // Prevent previous search selections from sticking around when not in vi mode. if !self.terminal.mode().contains(TermMode::VI) { + // Clear selection so we do not obstruct any matches. self.terminal.selection = None; } @@ -533,8 +547,8 @@ impl<'a, N: Notify + 'a, T: EventListener> ActionContext<'a, N, T> { self.search_reset_state(); self.terminal.cancel_search(); - // Restart search without vi mode to clear the search origin. if !self.terminal.mode().contains(TermMode::VI) { + // Restart search without vi mode to clear the search origin. self.start_search(self.search_state.direction); } } else { @@ -554,6 +568,9 @@ impl<'a, N: Notify + 'a, T: EventListener> ActionContext<'a, N, T> { self.terminal.scroll_display(Scroll::Delta(self.search_state.display_offset_delta)); self.search_state.display_offset_delta = 0; + // Clear focused match. + self.search_state.focused_match = None; + // Reset vi mode cursor. let mut origin = self.search_state.origin; origin.line = min(origin.line, self.terminal.screen_lines() - 1); @@ -586,12 +603,11 @@ impl<'a, N: Notify + 'a, T: EventListener> ActionContext<'a, N, T> { } else { // Select the match when vi mode is not active. self.terminal.scroll_to_point(*regex_match.start()); - let start = self.terminal.grid().clamp_buffer_to_visible(*regex_match.start()); - let end = self.terminal.grid().clamp_buffer_to_visible(*regex_match.end()); - self.start_selection(SelectionType::Simple, start, Side::Left); - self.update_selection(end, Side::Right); } + // Update the focused match. + self.search_state.focused_match = Some(regex_match); + // Store number of lines the viewport had to be moved. let display_offset = self.terminal.grid().display_offset(); self.search_state.display_offset_delta += old_offset - display_offset as isize; @@ -611,13 +627,16 @@ impl<'a, N: Notify + 'a, T: EventListener> ActionContext<'a, N, T> { TimerId::DelayedSearch, ); } + + // Clear focused match. + self.search_state.focused_match = None; }, } self.search_state.regex = Some(regex); } - /// Close the search bar. + /// Cleanup the search state. fn exit_search(&mut self) { // Move vi cursor down if resize will pull content from history. if self.terminal.history_size() != 0 @@ -630,6 +649,9 @@ impl<'a, N: Notify + 'a, T: EventListener> ActionContext<'a, N, T> { self.display_update_pending.dirty = true; self.search_state.regex = None; self.terminal.dirty = true; + + // Clear focused match. + self.search_state.focused_match = None; } /// Get the absolute position of the search origin. diff --git a/alacritty/src/input.rs b/alacritty/src/input.rs index 9c75753a..348db610 100644 --- a/alacritty/src/input.rs +++ b/alacritty/src/input.rs @@ -360,14 +360,13 @@ impl<'a, T: EventListener, A: ActionContext> Processor<'a, T, A> { #[inline] pub fn mouse_moved(&mut self, position: PhysicalPosition) { - let search_active = self.ctx.search_active(); let size_info = self.ctx.size_info(); let (x, y) = position.into(); let lmb_pressed = self.ctx.mouse().left_button_state == ElementState::Pressed; let rmb_pressed = self.ctx.mouse().right_button_state == ElementState::Pressed; - if !self.ctx.selection_is_empty() && (lmb_pressed || rmb_pressed) && !search_active { + if !self.ctx.selection_is_empty() && (lmb_pressed || rmb_pressed) { self.update_selection_scrolling(y); } @@ -405,9 +404,7 @@ impl<'a, T: EventListener, A: ActionContext> Processor<'a, T, A> { // Don't launch URLs if mouse has moved. self.ctx.mouse_mut().block_url_launcher = true; - if (lmb_pressed || rmb_pressed) - && (self.ctx.modifiers().shift() || !self.ctx.mouse_mode()) - && !search_active + if (lmb_pressed || rmb_pressed) && (self.ctx.modifiers().shift() || !self.ctx.mouse_mode()) { self.ctx.update_selection(point, cell_side); } else if inside_text_area @@ -600,7 +597,7 @@ impl<'a, T: EventListener, A: ActionContext> Processor<'a, T, A> { self.ctx.update_selection(point, cell_side); // Move vi mode cursor to mouse click position. - if self.ctx.terminal().mode().contains(TermMode::VI) { + if self.ctx.terminal().mode().contains(TermMode::VI) && !self.ctx.search_active() { self.ctx.terminal_mut().vi_mode_cursor.point = point; } } @@ -635,7 +632,7 @@ impl<'a, T: EventListener, A: ActionContext> Processor<'a, T, A> { }; // Move vi mode cursor to mouse click position. - if self.ctx.terminal().mode().contains(TermMode::VI) { + if self.ctx.terminal().mode().contains(TermMode::VI) && !self.ctx.search_active() { self.ctx.terminal_mut().vi_mode_cursor.point = point; } } @@ -791,7 +788,7 @@ impl<'a, T: EventListener, A: ActionContext> Processor<'a, T, A> { }; self.ctx.window_mut().set_mouse_cursor(new_icon); - } else if !self.ctx.search_active() { + } else { match state { ElementState::Pressed => { self.process_mouse_bindings(button); @@ -963,6 +960,11 @@ impl<'a, T: EventListener, A: ActionContext> Processor<'a, T, A> { /// The provided mode, mods, and key must match what is allowed by a binding /// for its action to be executed. fn process_mouse_bindings(&mut self, button: MouseButton) { + // Ignore bindings while search is active. + if self.ctx.search_active() { + return; + } + let mods = *self.ctx.modifiers(); let mode = *self.ctx.terminal().mode(); let mouse_mode = self.ctx.mouse_mode(); diff --git a/alacritty/src/renderer/mod.rs b/alacritty/src/renderer/mod.rs index f628d24f..b347556d 100644 --- a/alacritty/src/renderer/mod.rs +++ b/alacritty/src/renderer/mod.rs @@ -958,6 +958,7 @@ impl<'a> RenderApi<'a> { bg_alpha, fg, bg: bg.unwrap_or(Rgb { r: 0, g: 0, b: 0 }), + is_match: false, }) .collect::>(); diff --git a/alacritty/src/url.rs b/alacritty/src/url.rs index f3f60dd3..5d35667d 100644 --- a/alacritty/src/url.rs +++ b/alacritty/src/url.rs @@ -207,6 +207,7 @@ mod tests { bg: Default::default(), bg_alpha: 0., flags: Flags::empty(), + is_match: false, }) .collect() } diff --git a/alacritty_terminal/src/config/colors.rs b/alacritty_terminal/src/config/colors.rs index 13a30bef..a292fde4 100644 --- a/alacritty_terminal/src/config/colors.rs +++ b/alacritty_terminal/src/config/colors.rs @@ -15,7 +15,7 @@ pub struct Colors { #[serde(deserialize_with = "failure_default")] pub vi_mode_cursor: CursorColors, #[serde(deserialize_with = "failure_default")] - pub selection: SelectionColors, + pub selection: InvertedCellColors, #[serde(deserialize_with = "failure_default")] normal: NormalColors, #[serde(deserialize_with = "failure_default")] @@ -124,16 +124,16 @@ impl CursorColors { #[serde(default)] #[derive(Deserialize, Debug, Copy, Clone, Default, PartialEq, Eq)] -pub struct SelectionColors { - #[serde(deserialize_with = "failure_default")] - text: DefaultBackgroundCellRgb, +pub struct InvertedCellColors { + #[serde(deserialize_with = "failure_default", alias = "text")] + foreground: DefaultBackgroundCellRgb, #[serde(deserialize_with = "failure_default")] background: DefaultForegroundCellRgb, } -impl SelectionColors { - pub fn text(self) -> CellRgb { - self.text.0 +impl InvertedCellColors { + pub fn foreground(self) -> CellRgb { + self.foreground.0 } pub fn background(self) -> CellRgb { @@ -147,6 +147,8 @@ pub struct SearchColors { #[serde(deserialize_with = "failure_default")] pub matches: MatchColors, #[serde(deserialize_with = "failure_default")] + pub focused_match: InvertedCellColors, + #[serde(deserialize_with = "failure_default")] bar: BarColors, } diff --git a/alacritty_terminal/src/grid/mod.rs b/alacritty_terminal/src/grid/mod.rs index 70dbc936..21e7e2f9 100644 --- a/alacritty_terminal/src/grid/mod.rs +++ b/alacritty_terminal/src/grid/mod.rs @@ -1,7 +1,7 @@ //! A specialized 2D grid implementation optimized for use in a terminal. use std::cmp::{max, min}; -use std::ops::{Deref, Index, IndexMut, Range, RangeFrom, RangeFull, RangeTo}; +use std::ops::{Deref, Index, IndexMut, Range, RangeFrom, RangeFull, RangeInclusive, RangeTo}; use serde::{Deserialize, Serialize}; @@ -368,6 +368,30 @@ impl Grid { } } + // Clamp a buffer point based range to the viewport. + // + // This will make sure the content within the range is visible and return `None` whenever the + // entire range is outside the visible region. + pub fn clamp_buffer_range_to_visible( + &self, + range: &RangeInclusive>, + ) -> Option> { + let start = range.start(); + let end = range.end(); + + // Check if the range is completely offscreen + let viewport_end = self.display_offset; + let viewport_start = viewport_end + self.lines.0 - 1; + if end.line > viewport_start || start.line < viewport_end { + return None; + } + + let start = self.clamp_buffer_to_visible(*start); + let end = self.clamp_buffer_to_visible(*end); + + Some(start..=end) + } + /// Convert viewport relative point to global buffer indexing. #[inline] pub fn visible_to_buffer(&self, point: Point) -> Point { @@ -759,12 +783,8 @@ impl<'a, T: 'a> DisplayIter<'a, T> { self.offset } - pub fn column(&self) -> Column { - self.col - } - - pub fn line(&self) -> Line { - self.line + pub fn point(&self) -> Point { + Point::new(self.line, self.col) } } diff --git a/alacritty_terminal/src/term/mod.rs b/alacritty_terminal/src/term/mod.rs index 6084d8b0..6e20f0e1 100644 --- a/alacritty_terminal/src/term/mod.rs +++ b/alacritty_terminal/src/term/mod.rs @@ -136,12 +136,44 @@ pub struct RenderableCellsIter<'a, C> { inner: DisplayIter<'a, Cell>, grid: &'a Grid, cursor: RenderableCursor, + show_cursor: bool, config: &'a Config, colors: &'a color::List, selection: Option>, search: RenderableSearch<'a>, } +impl<'a, C> Iterator for RenderableCellsIter<'a, C> { + type Item = RenderableCell; + + /// Gets the next renderable cell. + /// + /// Skips empty (background) cells and applies any flags to the cell state + /// (eg. invert fg and bg colors). + #[inline] + fn next(&mut self) -> Option { + loop { + if self.show_cursor && self.cursor.point == self.inner.point() { + // Handle cursor rendering. + if self.cursor.rendered { + return self.next_cursor_cell(); + } else { + return self.next_cursor(); + } + } else { + // Handle non-cursor cells. + let cell = self.inner.next()?; + let cell = RenderableCell::new(self, cell); + + // Skip empty cells. + if !cell.is_empty() { + return Some(cell); + } + } + } + } +} + impl<'a, C> RenderableCellsIter<'a, C> { /// Create the renderable cells iterator. /// @@ -150,46 +182,65 @@ impl<'a, C> RenderableCellsIter<'a, C> { fn new( term: &'a Term, config: &'a Config, - selection: Option, + show_cursor: bool, ) -> RenderableCellsIter<'a, C> { - let grid = &term.grid; - - 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.cols() - 1) - }; - - // Do not render completely offscreen selection. - 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; - } - - // Get on-screen lines of the selection's locations. - let mut start = grid.clamp_buffer_to_visible(span.start); - let mut end = grid.clamp_buffer_to_visible(span.end); - - // Trim start/end with partially visible block selection. - start.col = max(limit_start, start.col); - end.col = min(limit_end, end.col); - - Some(SelectionRange::new(start, end, span.is_block)) - }); - RenderableCellsIter { cursor: term.renderable_cursor(config), - grid, - inner: grid.display_iter(), - selection: selection_range, + show_cursor, + grid: &term.grid, + inner: term.grid.display_iter(), + selection: term.visible_selection(), config, colors: &term.colors, search: RenderableSearch::new(term), } } + /// Get the next renderable cell as the cell below the cursor. + fn next_cursor_cell(&mut self) -> Option { + // Handle cell below cursor. + let cell = self.inner.next()?; + let mut cell = RenderableCell::new(self, cell); + + if self.cursor.key.style == CursorStyle::Block { + cell.fg = match self.cursor.cursor_color { + // Apply cursor color, or invert the cursor if it has a fixed background + // close to the cell's background. + CellRgb::Rgb(col) if col.contrast(cell.bg) < MIN_CURSOR_CONTRAST => cell.bg, + _ => self.cursor.text_color.color(cell.fg, cell.bg), + }; + } + + Some(cell) + } + + /// Get the next renderable cell as the cursor. + fn next_cursor(&mut self) -> Option { + // Handle cursor. + self.cursor.rendered = true; + + let buffer_point = self.grid.visible_to_buffer(self.cursor.point); + let cell = Indexed { + inner: &self.grid[buffer_point.line][buffer_point.col], + column: self.cursor.point.col, + line: self.cursor.point.line, + }; + + let mut cell = RenderableCell::new(self, cell); + cell.inner = RenderableCellContent::Cursor(self.cursor.key); + + // Apply cursor color, or invert the cursor if it has a fixed background close + // to the cell's background. + if !matches!( + self.cursor.cursor_color, + CellRgb::Rgb(color) if color.contrast(cell.bg) < MIN_CURSOR_CONTRAST + ) { + cell.fg = self.cursor.cursor_color.color(cell.fg, cell.bg); + } + + Some(cell) + } + /// Check selection state of a cell. fn is_selected(&self, point: Point) -> bool { let selection = match self.selection { @@ -265,6 +316,7 @@ pub struct RenderableCell { pub bg: Rgb, pub bg_alpha: f32, pub flags: Flags, + pub is_match: bool, } impl RenderableCell { @@ -282,9 +334,11 @@ impl RenderableCell { Self::compute_bg_alpha(cell.bg) }; + let mut is_match = false; + 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); + let selected_fg = iter.config.colors.selection.foreground().color(fg_rgb, bg_rgb); bg_rgb = config_bg.color(fg_rgb, bg_rgb); fg_rgb = selected_fg; @@ -306,6 +360,8 @@ impl RenderableCell { if config_bg != CellRgb::CellBackground { bg_alpha = 1.0; } + + is_match = true; } let zerowidth = cell.zerowidth().map(|zerowidth| zerowidth.to_vec()); @@ -318,6 +374,7 @@ impl RenderableCell { bg: bg_rgb, bg_alpha, flags: cell.flags, + is_match, } } @@ -391,73 +448,6 @@ impl RenderableCell { } } -impl<'a, C> Iterator for RenderableCellsIter<'a, C> { - type Item = RenderableCell; - - /// Gets the next renderable cell. - /// - /// Skips empty (background) cells and applies any flags to the cell state - /// (eg. invert fg and bg colors). - #[inline] - fn next(&mut self) -> Option { - loop { - if self.cursor.point.line == self.inner.line() - && self.cursor.point.col == self.inner.column() - { - if self.cursor.rendered { - // Handle cell below cursor. - let cell = self.inner.next()?; - let mut cell = RenderableCell::new(self, cell); - - if self.cursor.key.style == CursorStyle::Block { - cell.fg = match self.cursor.cursor_color { - // Apply cursor color, or invert the cursor if it has a fixed background - // close to the cell's background. - CellRgb::Rgb(col) if col.contrast(cell.bg) < MIN_CURSOR_CONTRAST => { - cell.bg - }, - _ => self.cursor.text_color.color(cell.fg, cell.bg), - }; - } - - return Some(cell); - } else { - // Handle cursor. - self.cursor.rendered = true; - - let buffer_point = self.grid.visible_to_buffer(self.cursor.point); - let cell = Indexed { - inner: &self.grid[buffer_point.line][buffer_point.col], - column: self.cursor.point.col, - line: self.cursor.point.line, - }; - - let mut cell = RenderableCell::new(self, cell); - cell.inner = RenderableCellContent::Cursor(self.cursor.key); - - // Apply cursor color, or invert the cursor if it has a fixed background close - // to the cell's background. - if !matches!( - self.cursor.cursor_color, - CellRgb::Rgb(color) if color.contrast(cell.bg) < MIN_CURSOR_CONTRAST - ) { - cell.fg = self.cursor.cursor_color.color(cell.fg, cell.bg); - } - - return Some(cell); - } - } else { - let cell = self.inner.next()?; - let cell = RenderableCell::new(self, cell); - - if !cell.is_empty() { - return Some(cell); - } - } - } - } -} - pub mod mode { use bitflags::bitflags; @@ -1049,10 +1039,34 @@ impl Term { /// A renderable cell is any cell which has content other than the default /// background color. Cells with an alternate background color are /// considered renderable as are cells with any text content. - pub fn renderable_cells<'b, C>(&'b self, config: &'b Config) -> RenderableCellsIter<'_, C> { - let selection = self.selection.as_ref().and_then(|s| s.to_range(self)); + pub fn renderable_cells<'b, C>( + &'b self, + config: &'b Config, + show_cursor: bool, + ) -> RenderableCellsIter<'_, C> { + RenderableCellsIter::new(&self, config, show_cursor) + } - RenderableCellsIter::new(&self, config, selection) + /// Get the selection within the viewport. + pub fn visible_selection(&self) -> Option> { + let selection = self.selection.as_ref()?.to_range(self)?; + + // Set horizontal limits for block selection. + let (limit_start, limit_end) = if selection.is_block { + (selection.start.col, selection.end.col) + } else { + (Column(0), self.cols() - 1) + }; + + let range = self.grid.clamp_buffer_range_to_visible(&(selection.start..=selection.end))?; + let mut start = *range.start(); + let mut end = *range.end(); + + // Trim start/end with partially visible block selection. + start.col = max(limit_start, start.col); + end.col = min(limit_end, end.col); + + Some(SelectionRange::new(start, end, selection.is_block)) } /// Resize terminal to new dimensions. @@ -2836,7 +2850,7 @@ mod benches { mem::swap(&mut terminal.grid, &mut grid); b.iter(|| { - let iter = terminal.renderable_cells(&config); + let iter = terminal.renderable_cells(&config, true); for cell in iter { test::black_box(cell); }