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
This commit is contained in:
Xiaoyu Yin 2017-01-14 17:53:48 -08:00 committed by Joe Wilm
parent 59295e4431
commit 92e1cec088
8 changed files with 567 additions and 24 deletions

6
.gitignore vendored
View File

@ -2,3 +2,9 @@ target
FlameGraph
.idea
*.iml
# vim temporary files
*.swp
# other ignores
*.DS_Store

View File

@ -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
#

View File

@ -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.

View File

@ -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

View File

@ -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();
}
}

View File

@ -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());
}
}

View File

@ -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 },

View File

@ -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
///