From 92e1cec0880313d962d80bf16eca60cebb509eab Mon Sep 17 00:00:00 2001 From: Xiaoyu Yin Date: Sat, 14 Jan 2017 17:53:48 -0800 Subject: [PATCH] Semantic Selection Fix tests and add line select Refactor BidirectionalIter to remove if blocks Allow for cells tagged with WRAPLINE to continue expanding the selection Reorganize config into structs Add test coverage that callbacks are called Cleanup mouse config - Uses Duration type for ClickHandler::threshold - Removes `action` property from ClickHandler--this can be added in a backwards compatible way later on - Renames ClickState::DblClick to DoubleClick fixup! Cleanup mouse config --- .gitignore | 6 ++ alacritty.yml | 6 ++ alacritty_macos.yml | 7 ++ src/config.rs | 79 ++++++++++++++++- src/event.rs | 32 ++++++- src/grid.rs | 114 ++++++++++++++++++++++++- src/input.rs | 202 +++++++++++++++++++++++++++++++++++++++++--- src/term/mod.rs | 145 +++++++++++++++++++++++++++++-- 8 files changed, 567 insertions(+), 24 deletions(-) diff --git a/.gitignore b/.gitignore index db0a5014..46831eaf 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,9 @@ target FlameGraph .idea *.iml + +# vim temporary files +*.swp + +# other ignores +*.DS_Store diff --git a/alacritty.yml b/alacritty.yml index 3ffd59a4..9a3a3711 100644 --- a/alacritty.yml +++ b/alacritty.yml @@ -212,6 +212,12 @@ key_bindings: mouse_bindings: - { mouse: Middle, action: PasteSelection } +mouse: + double_click: { threshold: 300 } + triple_click: { threshold: 300 } + +selection: + semantic_escape_chars: ",│`|:\"' ()[]{}<>" # Shell # diff --git a/alacritty_macos.yml b/alacritty_macos.yml index d6b89f77..160b85a0 100644 --- a/alacritty_macos.yml +++ b/alacritty_macos.yml @@ -212,6 +212,13 @@ key_bindings: mouse_bindings: - { mouse: Middle, action: PasteSelection } +mouse: + double_click: { threshold: 300 } + triple_click: { threshold: 300 } + +selection: + semantic_escape_chars: ",│`|:\"' ()[]{}<>" + # Shell # # You can set shell.program to the path of your favorite shell, e.g. /bin/fish. diff --git a/src/config.rs b/src/config.rs index 362fe645..c610f419 100644 --- a/src/config.rs +++ b/src/config.rs @@ -3,21 +3,22 @@ //! Alacritty reads from a config file at startup to determine various runtime //! parameters including font family and style, font size, etc. In the future, //! the config file will also hold user and platform specific keybindings. +use std::borrow::Cow; use std::env; use std::fmt; +use std::fs::File; use std::fs; use std::io::{self, Read, Write}; +use std::ops::{Index, IndexMut}; use std::path::{Path, PathBuf}; use std::str::FromStr; use std::sync::mpsc; -use std::ops::{Index, IndexMut}; -use std::fs::File; -use std::borrow::Cow; +use std::time::Duration; use ::Rgb; use font::Size; use serde_yaml; -use serde::{self, de}; +use serde::{self, de, Deserialize}; use serde::de::Error as SerdeError; use serde::de::{Visitor, MapVisitor, Unexpected}; use notify::{Watcher as WatcherApi, RecommendedWatcher as FileWatcher, op}; @@ -32,6 +33,52 @@ fn true_bool() -> bool { true } +#[derive(Clone, Debug, Deserialize)] +pub struct Selection { + pub semantic_escape_chars: String, +} + +impl Default for Selection { + fn default() -> Selection { + Selection { + semantic_escape_chars: String::new() + } + } +} + +#[derive(Clone, Debug, Deserialize)] +pub struct ClickHandler { + #[serde(deserialize_with="deserialize_duration_ms")] + pub threshold: Duration, +} + +fn deserialize_duration_ms(deserializer: D) -> ::std::result::Result + where D: de::Deserializer +{ + let threshold_ms = u64::deserialize(deserializer)?; + Ok(Duration::from_millis(threshold_ms)) +} + + +#[derive(Clone, Debug, Deserialize)] +pub struct Mouse { + pub double_click: ClickHandler, + pub triple_click: ClickHandler, +} + +impl Default for Mouse { + fn default() -> Mouse { + Mouse { + double_click: ClickHandler { + threshold: Duration::from_millis(300), + }, + triple_click: ClickHandler { + threshold: Duration::from_millis(300), + } + } + } +} + /// List of indexed colors /// /// The first 16 entries are the standard ansi named colors. Items 16..232 are @@ -248,6 +295,12 @@ pub struct Config { #[serde(default="default_mouse_bindings")] mouse_bindings: Vec, + #[serde(default="default_selection")] + selection: Selection, + + #[serde(default="default_mouse")] + mouse: Mouse, + /// Path to a shell program to run on startup #[serde(default)] shell: Option>, @@ -266,6 +319,10 @@ fn default_config() -> Config { .expect("default config is valid") } +fn default_selection() -> Selection { + default_config().selection +} + fn default_key_bindings() -> Vec { default_config().key_bindings } @@ -274,6 +331,10 @@ fn default_mouse_bindings() -> Vec { default_config().mouse_bindings } +fn default_mouse() -> Mouse { + default_config().mouse +} + impl Default for Config { fn default() -> Config { Config { @@ -286,6 +347,8 @@ impl Default for Config { colors: Default::default(), key_bindings: Vec::new(), mouse_bindings: Vec::new(), + selection: Default::default(), + mouse: Default::default(), shell: None, config_path: None, } @@ -964,6 +1027,14 @@ impl Config { &self.mouse_bindings[..] } + pub fn mouse(&self) -> &Mouse { + &self.mouse + } + + pub fn selection(&self) -> &Selection { + &self.selection + } + #[inline] pub fn draw_bold_text_with_bright_colors(&self) -> bool { self.draw_bold_text_with_bright_colors diff --git a/src/event.rs b/src/event.rs index 295cb6b1..f643c115 100644 --- a/src/event.rs +++ b/src/event.rs @@ -3,13 +3,14 @@ use std::borrow::Cow; use std::fs::File; use std::io::Write; use std::sync::mpsc; +use std::time::{Instant}; use serde_json as json; use parking_lot::MutexGuard; use glutin::{self, ElementState}; use copypasta::{Clipboard, Load, Store}; -use config::Config; +use config::{self, Config}; use cli::Options; use display::OnResize; use index::{Line, Column, Side, Point}; @@ -71,17 +72,38 @@ impl<'a, N: Notify + 'a> input::ActionContext for ActionContext<'a, N> { self.selection.update(point, side); } + fn semantic_selection(&mut self, point: Point) { + self.terminal.semantic_selection(&mut self.selection, point) + } + + fn line_selection(&mut self, point: Point) { + self.terminal.line_selection(&mut self.selection, point) + } + + fn mouse_coords(&self) -> Option { + self.terminal.pixels_to_coords(self.mouse.x as usize, self.mouse.y as usize) + } + #[inline] fn mouse_mut(&mut self) -> &mut Mouse { self.mouse } } +pub enum ClickState { + None, + Click, + DoubleClick, + TripleClick, +} + /// State of the mouse pub struct Mouse { pub x: u32, pub y: u32, pub left_button_state: ElementState, + pub last_click_timestamp: Instant, + pub click_state: ClickState, pub scroll_px: i32, pub line: Line, pub column: Column, @@ -93,7 +115,9 @@ impl Default for Mouse { Mouse { x: 0, y: 0, + last_click_timestamp: Instant::now(), left_button_state: ElementState::Released, + click_state: ClickState::None, scroll_px: 0, line: Line(0), column: Column(0), @@ -109,6 +133,7 @@ impl Default for Mouse { pub struct Processor { key_bindings: Vec, mouse_bindings: Vec, + mouse_config: config::Mouse, print_events: bool, notifier: N, mouse: Mouse, @@ -143,6 +168,7 @@ impl Processor { Processor { key_bindings: config.key_bindings().to_vec(), mouse_bindings: config.mouse_bindings().to_vec(), + mouse_config: config.mouse().to_owned(), print_events: options.print_events, notifier: notifier, resize_tx: resize_tx, @@ -263,8 +289,9 @@ impl Processor { processor = input::Processor { ctx: context, + mouse_config: &self.mouse_config, key_bindings: &self.key_bindings[..], - mouse_bindings: &self.mouse_bindings[..] + mouse_bindings: &self.mouse_bindings[..], }; process!(event); @@ -284,5 +311,6 @@ impl Processor { pub fn update_config(&mut self, config: &Config) { self.key_bindings = config.key_bindings().to_vec(); self.mouse_bindings = config.mouse_bindings().to_vec(); + self.mouse_config = config.mouse().to_owned(); } } diff --git a/src/grid.rs b/src/grid.rs index d913e640..c886807b 100644 --- a/src/grid.rs +++ b/src/grid.rs @@ -26,13 +26,18 @@ use std::iter::IntoIterator; use std::ops::{Deref, DerefMut, Range, RangeTo, RangeFrom, RangeFull, Index, IndexMut}; use std::slice::{self, Iter, IterMut}; -use index::{self, Point, IndexRange, RangeInclusive}; +use index::{self, Point, Line, Column, IndexRange, RangeInclusive}; /// Convert a type to a linear index range. pub trait ToRange { fn to_range(&self, columns: index::Column) -> RangeInclusive; } +/// Bidirection iterator +pub trait BidirectionalIterator: Iterator { + fn prev(&mut self) -> Option; +} + /// Represents the terminal display contents #[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)] pub struct Grid { @@ -49,6 +54,11 @@ pub struct Grid { lines: index::Line, } +pub struct GridIterator<'a, T: 'a> { + grid: &'a Grid, + pub cur: Point, +} + impl Grid { pub fn new(lines: index::Line, cols: index::Column, template: &T) -> Grid { let mut raw = Vec::with_capacity(*lines); @@ -139,6 +149,13 @@ impl Grid { } } + pub fn iter_from(&self, point: Point) -> GridIterator { + GridIterator { + grid: self, + cur: point, + } + } + #[inline] pub fn contains(&self, point: &Point) -> bool { self.lines > point.line && self.cols > point.col @@ -193,6 +210,49 @@ impl Grid { } } +impl<'a, T> Iterator for GridIterator<'a, T> { + type Item = &'a T; + + fn next(&mut self) -> Option { + let last_line = self.grid.num_lines() - Line(1); + let last_col = self.grid.num_cols() - Column(1); + match self.cur { + Point { line, col } if + (line == last_line) && + (col == last_col) => None, + Point { col, .. } if + (col == last_col) => { + self.cur.line += 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]) + } + } + } +} + +impl<'a, T> BidirectionalIterator for GridIterator<'a, T> { + fn prev(&mut self) -> Option { + let num_cols = self.grid.num_cols(); + + match self.cur { + Point { line: Line(0), col: Column(0) } => None, + Point { col: Column(0), .. } => { + self.cur.line -= 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]) + } + } + } +} + impl Index for Grid { type Output = Row; @@ -464,8 +524,8 @@ clear_region_impl!(RangeFrom); #[cfg(test)] mod tests { - use super::Grid; - use index::{Line, Column}; + use super::{Grid, BidirectionalIterator}; + use index::{Point, Line, Column}; #[test] fn grid_swap_lines_ok() { let mut grid = Grid::new(Line(10), Column(1), &0); @@ -588,4 +648,52 @@ mod tests { assert_eq!(grid[Line(i)][Column(0)], other[Line(i)][Column(0)]); } } + + // Test that GridIterator works + #[test] + fn test_iter() { + info!(""); + + let mut grid = Grid::new(Line(5), Column(5), &0); + for i in 0..5 { + for j in 0..5 { + grid[Line(i)][Column(j)] = i*5 + j; + } + } + + info!("grid: {:?}", grid); + + let mut iter = grid.iter_from(Point { + line: Line(0), + col: Column(0), + }); + + assert_eq!(None, iter.prev()); + assert_eq!(Some(&1), iter.next()); + assert_eq!(Column(1), iter.cur.col); + assert_eq!(Line(0), iter.cur.line); + + assert_eq!(Some(&2), iter.next()); + assert_eq!(Some(&3), iter.next()); + assert_eq!(Some(&4), iter.next()); + + // test linewrapping + assert_eq!(Some(&5), iter.next()); + assert_eq!(Column(0), iter.cur.col); + assert_eq!(Line(1), iter.cur.line); + + assert_eq!(Some(&4), iter.prev()); + assert_eq!(Column(4), iter.cur.col); + assert_eq!(Line(0), iter.cur.line); + + + // test that iter ends at end of grid + let mut final_iter = grid.iter_from(Point { + line: Line(4), + col: Column(4), + }); + assert_eq!(None, final_iter.next()); + assert_eq!(Some(&23), final_iter.prev()); + } + } diff --git a/src/input.rs b/src/input.rs index e6dc3993..e2b96c60 100644 --- a/src/input.rs +++ b/src/input.rs @@ -20,16 +20,18 @@ //! determine what to do when a non-modifier key is pressed. use std::borrow::Cow; use std::mem; +use std::time::Instant; use copypasta::{Clipboard, Load, Buffer}; use glutin::{ElementState, VirtualKeyCode, MouseButton}; use glutin::{Mods, mods}; use glutin::{TouchPhase, MouseScrollDelta}; -use event::{Mouse}; +use config; +use event::{ClickState, Mouse}; use index::{Line, Column, Side, Point}; -use term::mode::{self, TermMode}; use term::SizeInfo; +use term::mode::{self, TermMode}; use util::fmt::Red; /// Processes input from glutin. @@ -41,6 +43,7 @@ use util::fmt::Red; pub struct Processor<'a, A: 'a> { pub key_bindings: &'a [KeyBinding], pub mouse_bindings: &'a [MouseBinding], + pub mouse_config: &'a config::Mouse, pub ctx: A, } @@ -51,7 +54,10 @@ pub trait ActionContext { fn copy_selection(&self, Buffer); fn clear_selection(&mut self); fn update_selection(&mut self, Point, Side); + fn semantic_selection(&mut self, Point); + fn line_selection(&mut self, Point); fn mouse_mut(&mut self) -> &mut Mouse; + fn mouse_coords(&self) -> Option; } /// Describes a state and action to take in that state @@ -266,13 +272,43 @@ impl<'a, A: ActionContext + 'a> Processor<'a, A> { } } - pub fn on_mouse_press(&mut self) { - if self.ctx.terminal_mode().intersects(mode::MOUSE_REPORT_CLICK | mode::MOUSE_MOTION) { - self.mouse_report(0); - return; + pub fn on_mouse_double_click(&mut self) { + if let Some(point) = self.ctx.mouse_coords() { + self.ctx.semantic_selection(point); } + } - self.ctx.clear_selection(); + pub fn on_mouse_triple_click(&mut self) { + if let Some(point) = self.ctx.mouse_coords() { + self.ctx.line_selection(point); + } + } + + pub fn on_mouse_press(&mut self) { + let now = Instant::now(); + let elapsed = self.ctx.mouse_mut().last_click_timestamp.elapsed(); + self.ctx.mouse_mut().last_click_timestamp = now; + + self.ctx.mouse_mut().click_state = match self.ctx.mouse_mut().click_state { + ClickState::Click if elapsed < self.mouse_config.double_click.threshold => { + self.on_mouse_double_click(); + ClickState::DoubleClick + }, + ClickState::DoubleClick if elapsed < self.mouse_config.triple_click.threshold => { + self.on_mouse_triple_click(); + ClickState::TripleClick + }, + _ => { + let report_modes = mode::MOUSE_REPORT_CLICK | mode::MOUSE_MOTION; + if self.ctx.terminal_mode().intersects(report_modes) { + self.mouse_report(0); + return; + } + + self.ctx.clear_selection(); + ClickState::Click + } + }; } pub fn on_mouse_release(&mut self) { @@ -422,14 +458,136 @@ impl<'a, A: ActionContext + 'a> Processor<'a, A> { #[cfg(test)] mod tests { - use glutin::{mods, VirtualKeyCode}; + use std::borrow::Cow; + use std::time::Duration; - use term::mode; + use glutin::{mods, VirtualKeyCode, Event, ElementState, MouseButton}; - use super::{Action, Binding}; + use term::{SizeInfo, Term, TermMode, mode}; + use event::{Mouse, ClickState}; + use config::{self, Config, ClickHandler}; + use selection::Selection; + use index::{Point, Side}; + + use super::{Action, Binding, Processor}; const KEY: VirtualKeyCode = VirtualKeyCode::Key0; + #[derive(PartialEq)] + enum MultiClick { + DoubleClick, + TripleClick, + None, + } + + struct ActionContext<'a> { + pub terminal: &'a mut Term, + pub selection: &'a mut Selection, + pub size_info: &'a SizeInfo, + pub mouse: &'a mut Mouse, + pub last_action: MultiClick, + } + + impl <'a>super::ActionContext for ActionContext<'a> { + fn write_to_pty>>(&mut self, _val: B) { + // STUBBED + } + + fn terminal_mode(&self) -> TermMode { + *self.terminal.mode() + } + + fn size_info(&self) -> SizeInfo { + *self.size_info + } + + fn copy_selection(&self, _buffer: ::copypasta::Buffer) { + // STUBBED + } + + fn clear_selection(&mut self) { } + + fn update_selection(&mut self, point: Point, side: Side) { + self.selection.update(point, side); + } + + fn semantic_selection(&mut self, _point: Point) { + // set something that we can check for here + self.last_action = MultiClick::DoubleClick; + } + + fn line_selection(&mut self, _point: Point) { + self.last_action = MultiClick::TripleClick; + } + + fn mouse_coords(&self) -> Option { + self.terminal.pixels_to_coords(self.mouse.x as usize, self.mouse.y as usize) + } + + #[inline] + fn mouse_mut(&mut self) -> &mut Mouse { + self.mouse + } + } + + macro_rules! test_clickstate { + { + name: $name:ident, + initial_state: $initial_state:expr, + input: $input:expr, + end_state: $end_state:pat, + last_action: $last_action:expr + } => { + #[test] + fn $name() { + let config = Config::default(); + let size = SizeInfo { + width: 21.0, + height: 51.0, + cell_width: 3.0, + cell_height: 3.0, + }; + + let mut terminal = Term::new(&config, size); + + let mut mouse = Mouse::default(); + let mut selection = Selection::new(); + mouse.click_state = $initial_state; + + let context = ActionContext { + terminal: &mut terminal, + selection: &mut selection, + mouse: &mut mouse, + size_info: &size, + last_action: MultiClick::None, + }; + + let mut processor = Processor { + ctx: context, + mouse_config: &config::Mouse { + double_click: ClickHandler { + threshold: Duration::from_millis(1000), + }, + triple_click: ClickHandler { + threshold: Duration::from_millis(1000), + } + }, + key_bindings: &config.key_bindings()[..], + mouse_bindings: &config.mouse_bindings()[..], + }; + + if let Event::MouseInput(state, input) = $input { + processor.mouse_input(state, input); + }; + + assert!(match mouse.click_state { + $end_state => processor.ctx.last_action == $last_action, + _ => false + }); + } + } + } + macro_rules! test_process_binding { { name: $name:ident, @@ -449,6 +607,30 @@ mod tests { } } + test_clickstate! { + name: single_click, + initial_state: ClickState::None, + input: Event::MouseInput(ElementState::Pressed, MouseButton::Left), + end_state: ClickState::Click, + last_action: MultiClick::None + } + + test_clickstate! { + name: double_click, + initial_state: ClickState::Click, + input: Event::MouseInput(ElementState::Pressed, MouseButton::Left), + end_state: ClickState::DoubleClick, + last_action: MultiClick::DoubleClick + } + + test_clickstate! { + name: triple_click, + initial_state: ClickState::DoubleClick, + input: Event::MouseInput(ElementState::Pressed, MouseButton::Left), + end_state: ClickState::TripleClick, + last_action: MultiClick::TripleClick + } + test_process_binding! { name: process_binding_nomode_shiftmod_require_shift, binding: Binding { trigger: KEY, mods: mods::SHIFT, action: Action::from("\x1b[1;2D"), mode: mode::NONE, notmode: mode::NONE }, diff --git a/src/term/mod.rs b/src/term/mod.rs index 0dc5532c..b0ca2a59 100644 --- a/src/term/mod.rs +++ b/src/term/mod.rs @@ -20,8 +20,8 @@ use std::cmp::min; use std::io; use ansi::{self, Color, NamedColor, Attr, Handler, CharsetIndex, StandardCharset}; -use grid::{Grid, ClearRegion, ToRange}; -use index::{self, Point, Column, Line, Linear, IndexRange, Contains, RangeInclusive}; +use grid::{BidirectionalIterator, Grid, ClearRegion, ToRange}; +use index::{self, Point, Column, Line, Linear, IndexRange, Contains, RangeInclusive, Side}; use selection::{Span, Selection}; use config::{Config}; @@ -352,6 +352,8 @@ pub struct Term { /// Saved cursor from alt grid cursor_save_alt: Cursor, + + semantic_escape_chars: String, } /// Terminal size info @@ -436,11 +438,13 @@ impl Term { size_info: size, empty_cell: template, custom_cursor_colors: config.custom_cursor_colors(), + semantic_escape_chars: config.selection().semantic_escape_chars.clone(), } } pub fn update_config(&mut self, config: &Config) { - self.custom_cursor_colors = config.custom_cursor_colors() + self.custom_cursor_colors = config.custom_cursor_colors(); + self.semantic_escape_chars = config.selection().semantic_escape_chars.clone(); } #[inline] @@ -448,6 +452,64 @@ impl Term { self.dirty } + pub fn line_selection(&self, selection: &mut Selection, point: Point) { + selection.clear(); + selection.update(Point { + line: point.line, + col: Column(0), + }, Side::Left); + selection.update(Point { + line: point.line, + col: self.grid.num_cols() - Column(1), + }, Side::Right); + } + + pub fn semantic_selection(&self, selection: &mut Selection, point: Point) { + let mut side_left = Point { + line: point.line, + col: point.col + }; + let mut side_right = Point { + line: point.line, + col: point.col + }; + + let mut left_iter = self.grid.iter_from(point); + let mut right_iter = self.grid.iter_from(point); + + let last_col = self.grid.num_cols() - Column(1); + + while let Some(cell) = left_iter.prev() { + if self.semantic_escape_chars.contains(cell.c) { + break; + } + + if left_iter.cur.col == last_col && !cell.flags.contains(cell::WRAPLINE) { + break; // cut off if on new line or hit escape char + } + + side_left.col = left_iter.cur.col; + side_left.line = left_iter.cur.line; + } + + while let Some(cell) = right_iter.next() { + if self.semantic_escape_chars.contains(cell.c) { + break; + } + + side_right.col = right_iter.cur.col; + side_right.line = right_iter.cur.line; + + if right_iter.cur.col == last_col && !cell.flags.contains(cell::WRAPLINE) { + break; // cut off if on new line or hit escape char + } + } + + selection.clear(); + selection.update(side_left, Side::Left); + selection.update(side_right, Side::Right); + } + pub fn string_from_selection(&self, span: &Span) -> String { /// Need a generic push() for the Append trait trait PushChar { @@ -1283,12 +1345,85 @@ impl ansi::Handler for Term { mod tests { extern crate serde_json; - use super::{Term, limit, SizeInfo}; + use super::{Cell, Term, limit, SizeInfo}; + use term::cell; use grid::Grid; use index::{Point, Line, Column}; - use term::{Cell}; use ansi::{Handler, CharsetIndex, StandardCharset}; + use selection::Selection; + use std::mem; + + #[test] + fn semantic_selection_works() { + let size = SizeInfo { + width: 21.0, + height: 51.0, + cell_width: 3.0, + cell_height: 3.0, + }; + let mut term = Term::new(&Default::default(), size); + let mut grid: Grid = Grid::new(Line(3), Column(5), &Cell::default()); + for i in 0..5 { + for j in 0..2 { + grid[Line(j)][Column(i)].c = 'a'; + } + } + grid[Line(0)][Column(0)].c = '"'; + grid[Line(0)][Column(3)].c = '"'; + grid[Line(1)][Column(2)].c = '"'; + grid[Line(0)][Column(4)].flags.insert(cell::WRAPLINE); + + let mut escape_chars = String::from("\""); + + mem::swap(&mut term.grid, &mut grid); + mem::swap(&mut term.semantic_escape_chars, &mut escape_chars); + + { + let mut selection = Selection::new(); + term.semantic_selection(&mut selection, Point { line: Line(0), col: Column(1) }); + assert_eq!(term.string_from_selection(&selection.span().unwrap()), "aa"); + } + + { + let mut selection = Selection::new(); + term.semantic_selection(&mut selection, Point { line: Line(0), col: Column(4) }); + assert_eq!(term.string_from_selection(&selection.span().unwrap()), "aaa"); + } + + { + let mut selection = Selection::new(); + term.semantic_selection(&mut selection, Point { line: Line(1), col: Column(1) }); + assert_eq!(term.string_from_selection(&selection.span().unwrap()), "aaa"); + } + } + + #[test] + fn line_selection_works() { + let size = SizeInfo { + width: 21.0, + height: 51.0, + cell_width: 3.0, + cell_height: 3.0, + }; + let mut term = Term::new(&Default::default(), size); + let mut grid: Grid = Grid::new(Line(1), Column(5), &Cell::default()); + for i in 0..5 { + grid[Line(0)][Column(i)].c = 'a'; + } + grid[Line(0)][Column(0)].c = '"'; + grid[Line(0)][Column(3)].c = '"'; + + + mem::swap(&mut term.grid, &mut grid); + + let mut selection = Selection::new(); + term.line_selection(&mut selection, Point { line: Line(0), col: Column(3) }); + match selection.span() { + Some(span) => assert_eq!(term.string_from_selection(&span), "\"aa\"a"), + _ => () + } + } /// Check that the grid can be serialized back and forth losslessly ///