diff options
-rw-r--r-- | .gitignore | 6 | ||||
-rw-r--r-- | alacritty.yml | 6 | ||||
-rw-r--r-- | alacritty_macos.yml | 7 | ||||
-rw-r--r-- | src/config.rs | 79 | ||||
-rw-r--r-- | src/event.rs | 32 | ||||
-rw-r--r-- | src/grid.rs | 114 | ||||
-rw-r--r-- | src/input.rs | 202 | ||||
-rw-r--r-- | src/term/mod.rs | 145 |
8 files changed, 567 insertions, 24 deletions
@@ -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<D>(deserializer: D) -> ::std::result::Result<Duration, D::Error> + 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<MouseBinding>, + #[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<Shell<'static>>, @@ -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<KeyBinding> { default_config().key_bindings } @@ -274,6 +331,10 @@ fn default_mouse_bindings() -> Vec<MouseBinding> { 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<Point> { + 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<N> { key_bindings: Vec<KeyBinding>, mouse_bindings: Vec<MouseBinding>, + mouse_config: config::Mouse, print_events: bool, notifier: N, mouse: Mouse, @@ -143,6 +168,7 @@ impl<N: Notify> Processor<N> { 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<N: Notify> Processor<N> { 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<N: Notify> Processor<N> { 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<index::Linear>; } +/// Bidirection iterator +pub trait BidirectionalIterator: Iterator { + fn prev(&mut self) -> Option<Self::Item>; +} + /// Represents the terminal display contents #[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)] pub struct Grid<T> { @@ -49,6 +54,11 @@ pub struct Grid<T> { lines: index::Line, } +pub struct GridIterator<'a, T: 'a> { + grid: &'a Grid<T>, + pub cur: Point, +} + impl<T: Clone> Grid<T> { pub fn new(lines: index::Line, cols: index::Column, template: &T) -> Grid<T> { let mut raw = Vec::with_capacity(*lines); @@ -139,6 +149,13 @@ impl<T> Grid<T> { } } + pub fn iter_from(&self, point: Point) -> GridIterator<T> { + 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<T> Grid<T> { } } +impl<'a, T> Iterator for GridIterator<'a, T> { + type Item = &'a T; + + fn next(&mut self) -> Option<Self::Item> { + 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<Self::Item> { + 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<T> Index<index::Line> for Grid<T> { type Output = Row<T>; @@ -464,8 +524,8 @@ clear_region_impl!(RangeFrom<index::Line>); #[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<Point>; } /// 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<B: Into<Cow<'static, [u8]>>>(&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<Point> { + 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<Cell> = 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<Cell> = 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 /// |