From e0a286515f12c6ceed53c74df1c10123cb0b550d Mon Sep 17 00:00:00 2001 From: Christian Duerr Date: Thu, 20 Jun 2019 15:56:09 +0000 Subject: [PATCH] Add block selection This implements a block selection mode which can be triggered by holding Control before starting a selection. If text is copied using this block selection, newlines will be automatically added to the end of the lines. This fixes #526. --- CHANGELOG.md | 4 + alacritty_terminal/src/event.rs | 6 + alacritty_terminal/src/input.rs | 9 +- alacritty_terminal/src/selection.rs | 323 ++++++++++++++++++---------- alacritty_terminal/src/term/mod.rs | 94 ++++---- 5 files changed, 270 insertions(+), 166 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 08c7e77..cceb5d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Block selection mode when Control is held while starting a selection + ### Fixed - GUI programs launched by Alacritty starting in the background on X11 diff --git a/alacritty_terminal/src/event.rs b/alacritty_terminal/src/event.rs index f844bf6..171f3ce 100644 --- a/alacritty_terminal/src/event.rs +++ b/alacritty_terminal/src/event.rs @@ -102,6 +102,12 @@ impl<'a, N: Notify + 'a> input::ActionContext for ActionContext<'a, N> { self.terminal.dirty = true; } + fn block_selection(&mut self, point: Point, side: Side) { + let point = self.terminal.visible_to_buffer(point); + *self.terminal.selection_mut() = Some(Selection::block(point, side)); + self.terminal.dirty = true; + } + fn semantic_selection(&mut self, point: Point) { let point = self.terminal.visible_to_buffer(point); *self.terminal.selection_mut() = Some(Selection::semantic(point)); diff --git a/alacritty_terminal/src/input.rs b/alacritty_terminal/src/input.rs index 17d427c..b4ea591 100644 --- a/alacritty_terminal/src/input.rs +++ b/alacritty_terminal/src/input.rs @@ -66,6 +66,7 @@ pub trait ActionContext { fn clear_selection(&mut self); fn update_selection(&mut self, point: Point, side: Side); fn simple_selection(&mut self, point: Point, side: Side); + fn block_selection(&mut self, point: Point, side: Side); fn semantic_selection(&mut self, point: Point); fn line_selection(&mut self, point: Point); fn selection_is_empty(&self) -> bool; @@ -612,7 +613,11 @@ impl<'a, A: ActionContext + 'a> Processor<'a, A> { // Start new empty selection let side = self.ctx.mouse().cell_side; if let Some(point) = point { - self.ctx.simple_selection(point, side); + if modifiers.ctrl { + self.ctx.block_selection(point, side); + } else { + self.ctx.simple_selection(point, side); + } } let report_modes = @@ -991,6 +996,8 @@ mod tests { fn simple_selection(&mut self, _point: Point, _side: Side) {} + fn block_selection(&mut self, _point: Point, _side: Side) {} + fn copy_selection(&mut self, _: ClipboardType) {} fn clear_selection(&mut self) {} diff --git a/alacritty_terminal/src/selection.rs b/alacritty_terminal/src/selection.rs index 7dd0be7..132c691 100644 --- a/alacritty_terminal/src/selection.rs +++ b/alacritty_terminal/src/selection.rs @@ -20,7 +20,7 @@ //! also be cleared if the user clicks off of the selection. use std::ops::Range; -use crate::index::{Column, Point, Side}; +use crate::index::{Column, Line, Point, Side}; use crate::term::cell::Flags; use crate::term::{Search, Term}; @@ -45,6 +45,10 @@ pub enum Selection { /// The region representing start and end of cursor movement region: Range, }, + Block { + /// The region representing start and end of cursor movement + region: Range, + }, Semantic { /// The region representing start and end of cursor movement region: Range>, @@ -52,10 +56,6 @@ pub enum Selection { Lines { /// The region representing start and end of cursor movement region: Range>, - - /// The line under the initial point. This is always selected regardless - /// of which way the cursor is moved. - initial_line: isize, }, } @@ -79,6 +79,19 @@ pub trait Dimensions { } impl Selection { + pub fn rotate(&mut self, offset: isize) { + match *self { + Selection::Simple { ref mut region } | Selection::Block { ref mut region } => { + region.start.point.line += offset; + region.end.point.line += offset; + }, + Selection::Semantic { ref mut region } | Selection::Lines { ref mut region } => { + region.start.line += offset; + region.end.line += offset; + }, + } + } + pub fn simple(location: Point, side: Side) -> Selection { Selection::Simple { region: Range { @@ -88,20 +101,11 @@ impl Selection { } } - pub fn rotate(&mut self, offset: isize) { - match *self { - Selection::Simple { ref mut region } => { - region.start.point.line += offset; - region.end.point.line += offset; - }, - Selection::Semantic { ref mut region } => { - region.start.line += offset; - region.end.line += offset; - }, - Selection::Lines { ref mut region, ref mut initial_line } => { - region.start.line += offset; - region.end.line += offset; - *initial_line += offset; + pub fn block(location: Point, side: Side) -> Selection { + Selection::Block { + region: Range { + start: Anchor::new(location.into(), side), + end: Anchor::new(location.into(), side), }, } } @@ -111,29 +115,49 @@ impl Selection { } pub fn lines(point: Point) -> Selection { - Selection::Lines { - region: Range { start: point.into(), end: point.into() }, - initial_line: point.line as isize, - } + Selection::Lines { region: Range { start: point.into(), end: point.into() } } } pub fn update(&mut self, location: Point, side: Side) { // Always update the `end`; can normalize later during span generation. match *self { - Selection::Simple { ref mut region } => { + Selection::Simple { ref mut region } | Selection::Block { ref mut region } => { region.end = Anchor::new(location.into(), side); }, - Selection::Semantic { ref mut region } | Selection::Lines { ref mut region, .. } => { + Selection::Semantic { ref mut region } | Selection::Lines { ref mut region } => { region.end = location.into(); }, } } + pub fn is_empty(&self) -> bool { + match *self { + Selection::Simple { ref region } | Selection::Block { ref region } => { + let (start, end) = + if Selection::points_need_swap(region.start.point, region.end.point) { + (®ion.end, ®ion.start) + } else { + (®ion.start, ®ion.end) + }; + + // Empty when single cell with identical sides or two cell with right+left sides + start == end + || (start.side == Side::Left + && end.side == Side::Right + && start.point.line == end.point.line + && start.point.col == end.point.col + 1) + }, + Selection::Semantic { .. } | Selection::Lines { .. } => false, + } + } + pub fn to_span(&self, term: &Term) -> Option { // Get both sides of the selection let (mut start, mut end) = match *self { - Selection::Simple { ref region } => (region.start.point, region.end.point), - Selection::Semantic { ref region } | Selection::Lines { ref region, .. } => { + Selection::Simple { ref region } | Selection::Block { ref region } => { + (region.start.point, region.end.point) + }, + Selection::Semantic { ref region } | Selection::Lines { ref region } => { (region.start, region.end) }, }; @@ -150,11 +174,23 @@ impl Selection { let (start, end) = Selection::grid_clamp(start, end, lines, cols)?; let span = match *self { - Selection::Simple { ref region } if needs_swap => { - Selection::span_simple(term, start, end, region.end.side, region.start.side) - }, Selection::Simple { ref region } => { - Selection::span_simple(term, start, end, region.start.side, region.end.side) + let (start_side, end_side) = if needs_swap { + (region.end.side, region.start.side) + } else { + (region.start.side, region.end.side) + }; + + self.span_simple(term, start, end, start_side, end_side) + }, + Selection::Block { ref region } => { + let (start_side, end_side) = if needs_swap { + (region.end.side, region.start.side) + } else { + (region.start.side, region.end.side) + }; + + self.span_block(start, end, start_side, end_side) }, Selection::Semantic { .. } => Selection::span_semantic(term, start, end), Selection::Lines { .. } => Selection::span_lines(term, start, end), @@ -180,85 +216,9 @@ impl Selection { }) } - pub fn is_empty(&self) -> bool { - match *self { - Selection::Simple { ref region } => { - region.start == region.end && region.start.side == region.end.side - }, - Selection::Semantic { .. } | Selection::Lines { .. } => false, - } - } - - fn span_semantic(term: &T, start: Point, end: Point) -> Option - where - T: Search + Dimensions, - { - let (start, end) = if start == end { - if let Some(end) = term.bracket_search(start.into()) { - (start.into(), end) - } else { - (term.semantic_search_right(start.into()), term.semantic_search_left(end.into())) - } - } else { - (term.semantic_search_right(start.into()), term.semantic_search_left(end.into())) - }; - - Some(Span { start, end }) - } - - fn span_lines(term: &T, mut start: Point, mut end: Point) -> Option - where - T: Dimensions, - { - start.col = term.dimensions().col - 1; - end.col = Column(0); - - Some(Span { start: start.into(), end: end.into() }) - } - - fn span_simple( - term: &T, - mut start: Point, - mut end: Point, - start_side: Side, - end_side: Side, - ) -> Option - where - T: Dimensions, - { - // No selection for single cell with identical sides or two cell with right+left sides - if (start == end && start_side == end_side) - || (end_side == Side::Right - && start_side == Side::Left - && start.line == end.line - && start.col == end.col + 1) - { - return None; - } - - // Remove last cell if selection ends to the left of a cell - if start_side == Side::Left && start != end { - // Special case when selection starts to left of first cell - if start.col == Column(0) { - start.col = term.dimensions().col - 1; - start.line += 1; - } else { - start.col -= 1; - } - } - - // Remove first cell if selection starts at the right of a cell - if end_side == Side::Right && start != end { - end.col += 1; - } - - // Return the selection with all cells inclusive - Some(Span { start: start.into(), end: end.into() }) - } - // Bring start and end points in the correct order fn points_need_swap(start: Point, end: Point) -> bool { - start.line > end.line || start.line == end.line && start.col <= end.col + start.line > end.line || start.line == end.line && start.col < end.col } // Clamp selection inside the grid to prevent out of bounds errors @@ -292,15 +252,129 @@ impl Selection { Some((start, end)) } + + fn span_semantic(term: &T, start: Point, end: Point) -> Option + where + T: Search + Dimensions, + { + let (start, end) = if start == end { + if let Some(end) = term.bracket_search(start.into()) { + (start.into(), end) + } else { + (term.semantic_search_right(start.into()), term.semantic_search_left(end.into())) + } + } else { + (term.semantic_search_right(start.into()), term.semantic_search_left(end.into())) + }; + + Some(Span { start, end, is_block: false }) + } + + fn span_lines(term: &T, mut start: Point, mut end: Point) -> Option + where + T: Dimensions, + { + start.col = term.dimensions().col - 1; + end.col = Column(0); + + Some(Span { start: start.into(), end: end.into(), is_block: false }) + } + + fn span_simple( + &self, + term: &T, + mut start: Point, + mut end: Point, + start_side: Side, + end_side: Side, + ) -> Option + where + T: Dimensions, + { + if self.is_empty() { + return None; + } + + // Remove last cell if selection ends to the left of a cell + if start_side == Side::Left && start != end { + // Special case when selection starts to left of first cell + if start.col == Column(0) { + start.col = term.dimensions().col - 1; + start.line += 1; + } else { + start.col -= 1; + } + } + + // Remove first cell if selection starts at the right of a cell + if end_side == Side::Right && start != end { + end.col += 1; + } + + // Return the selection with all cells inclusive + Some(Span { start: start.into(), end: end.into(), is_block: false }) + } + + fn span_block( + &self, + mut start: Point, + mut end: Point, + mut start_side: Side, + mut end_side: Side, + ) -> Option { + if self.is_empty() { + return None; + } + + // Always go bottom-right -> top-left + if start.col < end.col { + std::mem::swap(&mut start_side, &mut end_side); + std::mem::swap(&mut start.col, &mut end.col); + } + + // Remove last cell if selection ends to the left of a cell + if start_side == Side::Left && start != end && start.col.0 > 0 { + start.col -= 1; + } + + // Remove first cell if selection starts at the right of a cell + if end_side == Side::Right && start != end { + end.col += 1; + } + + // Return the selection with all cells inclusive + Some(Span { start: start.into(), end: end.into(), is_block: true }) + } } /// Represents a span of selected cells -#[derive(Debug, Eq, PartialEq)] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] pub struct Span { /// Start point from bottom of buffer pub start: Point, /// End point towards top of buffer pub end: Point, + /// Whether this selection is a block selection + pub is_block: bool, +} + +pub struct SelectionRange { + start: Point, + end: Point, + is_block: bool, +} + +impl SelectionRange { + pub fn new(start: Point, end: Point, is_block: bool) -> Self { + Self { start, end, is_block } + } + + pub fn contains(&self, col: Column, line: Line) -> bool { + self.start.line <= line + && self.end.line >= line + && (self.start.col <= col || (self.start.line != line && !self.is_block)) + && (self.end.col >= col || (self.end.line != line && !self.is_block)) + } } /// Tests for selection @@ -350,7 +424,8 @@ mod test { assert_eq!(selection.to_span(&term(1, 1)).unwrap(), Span { start: location, - end: location + end: location, + is_block: false, }); } @@ -367,7 +442,8 @@ mod test { assert_eq!(selection.to_span(&term(1, 1)).unwrap(), Span { start: location, - end: location + end: location, + is_block: false, }); } @@ -414,6 +490,7 @@ mod test { assert_eq!(selection.to_span(&term(5, 2)).unwrap(), Span { start: Point::new(0, Column(1)), end: Point::new(1, Column(2)), + is_block: false, }); } @@ -437,11 +514,12 @@ mod test { assert_eq!(selection.to_span(&term(5, 2)).unwrap(), Span { start: Point::new(0, Column(1)), end: Point::new(1, Column(1)), + is_block: false, }); } #[test] - fn alt_screen_lines() { + fn line_selection() { let mut selection = Selection::lines(Point::new(0, Column(0))); selection.update(Point::new(5, Column(3)), Side::Right); selection.rotate(-3); @@ -449,11 +527,12 @@ mod test { assert_eq!(selection.to_span(&term(5, 10)).unwrap(), Span { start: Point::new(0, Column(4)), end: Point::new(2, Column(0)), + is_block: false, }); } #[test] - fn alt_screen_semantic() { + fn semantic_selection() { let mut selection = Selection::semantic(Point::new(0, Column(0))); selection.update(Point::new(5, Column(3)), Side::Right); selection.rotate(-3); @@ -461,11 +540,12 @@ mod test { assert_eq!(selection.to_span(&term(5, 10)).unwrap(), Span { start: Point::new(0, Column(4)), end: Point::new(2, Column(3)), + is_block: false, }); } #[test] - fn alt_screen_simple() { + fn simple_selection() { let mut selection = Selection::simple(Point::new(0, Column(0)), Side::Right); selection.update(Point::new(5, Column(3)), Side::Right); selection.rotate(-3); @@ -473,6 +553,20 @@ mod test { assert_eq!(selection.to_span(&term(5, 10)).unwrap(), Span { start: Point::new(0, Column(4)), end: Point::new(2, Column(4)), + is_block: false, + }); + } + + #[test] + fn block_selection() { + let mut selection = Selection::block(Point::new(0, Column(0)), Side::Right); + selection.update(Point::new(5, Column(3)), Side::Right); + selection.rotate(-3); + + assert_eq!(selection.to_span(&term(5, 10)).unwrap(), Span { + start: Point::new(0, Column(4)), + end: Point::new(2, Column(4)), + is_block: true, }); } @@ -492,6 +586,7 @@ mod test { assert_eq!(selection.to_span(&term).unwrap(), Span { start: Point::new(0, Column(9)), end: Point::new(0, Column(0)), + is_block: false, }); } } diff --git a/alacritty_terminal/src/term/mod.rs b/alacritty_terminal/src/term/mod.rs index f936b08..378b315 100644 --- a/alacritty_terminal/src/term/mod.rs +++ b/alacritty_terminal/src/term/mod.rs @@ -35,7 +35,7 @@ use crate::grid::{ use crate::index::{self, Column, Contains, IndexRange, Line, Linear, Point}; use crate::input::FONT_SIZE_STEP; use crate::message_bar::MessageBuffer; -use crate::selection::{self, Selection, Span}; +use crate::selection::{self, Selection, SelectionRange, Span}; use crate::term::cell::{Cell, Flags, LineLength}; use crate::term::color::Rgb; use crate::url::{Url, UrlParser}; @@ -215,7 +215,7 @@ pub struct RenderableCellsIter<'a> { cursor_style: CursorStyle, config: &'a Config, colors: &'a color::List, - selection: Option>, + selection: Option, url_highlight: &'a Option>, } @@ -240,19 +240,23 @@ impl<'a> RenderableCellsIter<'a> { let start_line = grid.buffer_line_to_visible(span.start.line); let end_line = grid.buffer_line_to_visible(span.end.line); + // Limit block selection columns to within start/end points + let (limit_start, limit_end) = + if span.is_block { (span.start.col, span.end.col) } else { (Column(0), Column(0)) }; + // Get start/end locations based on what part of selection is on screen let locations = match (start_line, end_line) { (ViewportPosition::Visible(start_line), ViewportPosition::Visible(end_line)) => { Some((start_line, span.start.col, end_line, span.end.col)) }, (ViewportPosition::Visible(start_line), ViewportPosition::Above) => { - Some((start_line, span.start.col, Line(0), Column(0))) + Some((start_line, span.start.col, Line(0), limit_end)) }, (ViewportPosition::Below, ViewportPosition::Visible(end_line)) => { - Some((grid.num_lines(), Column(0), end_line, span.end.col)) + Some((grid.num_lines(), limit_start, end_line, span.end.col)) }, (ViewportPosition::Below, ViewportPosition::Above) => { - Some((grid.num_lines(), Column(0), Line(0), Column(0))) + Some((grid.num_lines(), limit_start, Line(0), limit_end)) }, _ => None, }; @@ -267,11 +271,7 @@ impl<'a> RenderableCellsIter<'a> { ::std::mem::swap(&mut start, &mut end); } - let cols = grid.num_cols(); - let start = Linear::from_point(cols, start.into()); - let end = Linear::from_point(cols, end.into()); - - RangeInclusive::new(start, end) + SelectionRange::new(start, end, span.is_block) }) }); @@ -425,9 +425,11 @@ impl<'a> Iterator for RenderableCellsIter<'a> { fn next(&mut self) -> Option { loop { if self.cursor_offset == self.inner.offset() && self.inner.column() == self.cursor.col { - let index = Linear::new(self.grid.num_cols(), self.cursor.col, self.cursor.line); - let selected = - self.selection.as_ref().map(|range| range.contains_(index)).unwrap_or(false); + let selected = self + .selection + .as_ref() + .map(|range| range.contains(self.cursor.col, self.cursor.line)) + .unwrap_or(false); // Handle cursor if let Some(cursor_key) = self.cursor_key.take() { @@ -464,12 +466,14 @@ impl<'a> Iterator for RenderableCellsIter<'a> { } else { let mut cell = self.inner.next()?; - let index = Linear::new(self.grid.num_cols(), cell.column, cell.line); - - let selected = - self.selection.as_ref().map(|range| range.contains_(index)).unwrap_or(false); + let selected = self + .selection + .as_ref() + .map(|range| range.contains(cell.column, cell.line)) + .unwrap_or(false); // Underline URL highlights + let index = Linear::new(self.grid.num_cols(), cell.column, cell.line); if self.url_highlight.as_ref().map(|range| range.contains_(index)).unwrap_or(false) { cell.inner.flags.insert(Flags::UNDERLINE); @@ -987,28 +991,10 @@ impl Term { } pub fn selection_to_string(&self) -> Option { - /// Need a generic push() for the Append trait - trait PushChar { - fn push_char(&mut self, c: char); - fn maybe_newline(&mut self, grid: &Grid, line: usize, ending: Column) { - if ending != Column(0) - && !grid[line][ending - 1].flags.contains(cell::Flags::WRAPLINE) - { - self.push_char('\n'); - } - } - } - - impl PushChar for String { - #[inline] - fn push_char(&mut self, c: char) { - self.push(c); - } - } - - trait Append: PushChar { + trait Append { fn append( &mut self, + append_newline: bool, grid: &Grid, tabs: &TabStops, line: usize, @@ -1019,6 +1005,7 @@ impl Term { impl Append for String { fn append( &mut self, + append_newline: bool, grid: &Grid, tabs: &TabStops, mut line: usize, @@ -1031,9 +1018,7 @@ impl Term { let line_length = grid_line.line_length(); let line_end = min(line_length, cols.end + 1); - if line_end.0 == 0 && cols.end >= grid.num_cols() - 1 { - self.push('\n'); - } else if cols.start < line_end { + if cols.start < line_end { let mut tab_mode = false; for col in IndexRange::from(cols.start..line_end) { @@ -1059,16 +1044,20 @@ impl Term { tab_mode = true; } } + } - if cols.end >= grid.num_cols() - 1 { - self.maybe_newline(grid, line, line_end); - } + if append_newline + || (cols.end >= grid.num_cols() - 1 + && (line_end == Column(0) + || !grid[line][line_end - 1].flags.contains(cell::Flags::WRAPLINE))) + { + self.push('\n'); } } } let selection = self.grid.selection.clone()?; - let Span { mut start, mut end } = selection.to_span(self)?; + let Span { mut start, mut end, is_block } = selection.to_span(self)?; let mut res = String::new(); @@ -1077,35 +1066,38 @@ impl Term { } let line_count = end.line - start.line; - let max_col = Column(usize::max_value() - 1); + + // Setup block selection start/end point limits + let (limit_start, limit_end) = + if is_block { (end.col, start.col) } else { (Column(0), self.grid.num_cols()) }; match line_count { // Selection within single line 0 => { - res.append(&self.grid, &self.tabs, start.line, start.col..end.col); + res.append(false, &self.grid, &self.tabs, start.line, start.col..end.col); }, // Selection ends on line following start 1 => { // Ending line - res.append(&self.grid, &self.tabs, end.line, end.col..max_col); + res.append(is_block, &self.grid, &self.tabs, end.line, end.col..limit_end); // Starting line - res.append(&self.grid, &self.tabs, start.line, Column(0)..start.col); + res.append(false, &self.grid, &self.tabs, start.line, limit_start..start.col); }, // Multi line selection _ => { // Ending line - res.append(&self.grid, &self.tabs, end.line, end.col..max_col); + res.append(is_block, &self.grid, &self.tabs, end.line, end.col..limit_end); let middle_range = (start.line + 1)..(end.line); for line in middle_range.rev() { - res.append(&self.grid, &self.tabs, line, Column(0)..max_col); + res.append(is_block, &self.grid, &self.tabs, line, limit_start..limit_end); } // Starting line - res.append(&self.grid, &self.tabs, start.line, Column(0)..start.col); + res.append(false, &self.grid, &self.tabs, start.line, limit_start..start.col); }, }