1
0
зеркало из https://github.com/glebtv/tui-textarea.git synced 2026-09-08 08:25:51 +03:00
* select, second go using new multiline code

* implemented internal delete_str, added tests

* delete_str_internal must not put non deleted strings into history. Ie ctrlc should not change history

* delete_str was taking lines out in the non delete case

* corrected comment

* fixes after feedback. Plus multichar bugs. change modal sample
Этот коммит содержится в:
pm100
2023-11-10 04:10:04 -08:00
коммит произвёл GitHub
родитель 9ec2f32edf
Коммит 5a2649abe9
10 изменённых файлов: 955 добавлений и 68 удалений

Просмотреть файл

@@ -262,6 +262,7 @@ impl<'a> Editor<'a> {
key: Key::Char('g' | 'n'), key: Key::Char('g' | 'n'),
ctrl: true, ctrl: true,
alt: false, alt: false,
..
} }
| Input { key: Key::Down, .. } => { | Input { key: Key::Down, .. } => {
if !textarea.search_forward(false) { if !textarea.search_forward(false) {
@@ -272,11 +273,13 @@ impl<'a> Editor<'a> {
key: Key::Char('g'), key: Key::Char('g'),
ctrl: false, ctrl: false,
alt: true, alt: true,
..
} }
| Input { | Input {
key: Key::Char('p'), key: Key::Char('p'),
ctrl: true, ctrl: true,
alt: false, alt: false,
..
} }
| Input { key: Key::Up, .. } => { | Input { key: Key::Up, .. } => {
if !textarea.search_back(false) { if !textarea.search_back(false) {

Просмотреть файл

@@ -60,7 +60,7 @@ fn main() -> io::Result<()> {
} else { } else {
TextArea::default() TextArea::default()
}; };
textarea.set_selection_style(Style::default().bg(Color::Red));
let mut mode = Mode::Normal; let mut mode = Mode::Normal;
loop { loop {
// Show help message and current mode in title of the block // Show help message and current mode in title of the block
@@ -75,32 +75,41 @@ fn main() -> io::Result<()> {
term.draw(|f| f.render_widget(textarea.widget(), f.size()))?; term.draw(|f| f.render_widget(textarea.widget(), f.size()))?;
let input = crossterm::event::read()?.into(); let input: Input = crossterm::event::read()?.into();
// since this sample doesnt use input a lot of the time
// it must call should_select
textarea.should_select(&input);
match mode { match mode {
Mode::Normal => match input { Mode::Normal => match input {
// Mappings in normal mode // Mappings in normal mode
// note that the navigation keys can be pressed with or without shift
// if the user is selecting text
// so we have to look for 'h' and 'H' etc.
Input { Input {
key: Key::Char('h'), key: Key::Char('h' | 'H'),
.. ..
} => textarea.move_cursor(CursorMove::Back), } => textarea.move_cursor(CursorMove::Back),
Input { Input {
key: Key::Char('j'), key: Key::Char('j' | 'J'),
.. ..
} => textarea.move_cursor(CursorMove::Down), } => textarea.move_cursor(CursorMove::Down),
Input { Input {
key: Key::Char('k'), key: Key::Char('k' | 'K'),
.. ..
} => textarea.move_cursor(CursorMove::Up), } => textarea.move_cursor(CursorMove::Up),
Input { Input {
key: Key::Char('l'), key: Key::Char('l' | 'L'),
.. ..
} => textarea.move_cursor(CursorMove::Forward), } => textarea.move_cursor(CursorMove::Forward),
Input { Input {
key: Key::Char('w'), key: Key::Char('w' | 'W'),
.. ..
} => textarea.move_cursor(CursorMove::WordForward), } => textarea.move_cursor(CursorMove::WordForward),
Input { Input {
key: Key::Char('b'), key: Key::Char('b' | 'B'),
ctrl: false, ctrl: false,
.. ..
} => textarea.move_cursor(CursorMove::WordBack), } => textarea.move_cursor(CursorMove::WordBack),
@@ -131,6 +140,13 @@ fn main() -> io::Result<()> {
} => { } => {
textarea.paste(); textarea.paste();
} }
Input {
key: Key::Char('y'),
ctrl: false,
..
} => {
textarea.copy();
}
Input { Input {
key: Key::Char('u'), key: Key::Char('u'),
ctrl: false, ctrl: false,
@@ -166,6 +182,8 @@ fn main() -> io::Result<()> {
key: Key::Char('A'), key: Key::Char('A'),
.. ..
} => { } => {
// stop selection is called to prevent 'A' being interpreted as selecting
textarea.stop_selection();
textarea.move_cursor(CursorMove::End); textarea.move_cursor(CursorMove::End);
mode = Mode::Insert; mode = Mode::Insert;
} }
@@ -181,6 +199,8 @@ fn main() -> io::Result<()> {
key: Key::Char('O'), key: Key::Char('O'),
.. ..
} => { } => {
// stop selection is called to prevent 'O' being interpreted as selecting
textarea.stop_selection();
textarea.move_cursor(CursorMove::Head); textarea.move_cursor(CursorMove::Head);
textarea.insert_newline(); textarea.insert_newline();
textarea.move_cursor(CursorMove::Up); textarea.move_cursor(CursorMove::Up);
@@ -190,6 +210,7 @@ fn main() -> io::Result<()> {
key: Key::Char('I'), key: Key::Char('I'),
.. ..
} => { } => {
textarea.stop_selection();
textarea.move_cursor(CursorMove::Head); textarea.move_cursor(CursorMove::Head);
mode = Mode::Insert; mode = Mode::Insert;
} }

Просмотреть файл

@@ -264,6 +264,7 @@ impl<'a> Editor<'a> {
key: Key::Char('g' | 'n'), key: Key::Char('g' | 'n'),
ctrl: true, ctrl: true,
alt: false, alt: false,
..
} }
| Input { key: Key::Down, .. } => { | Input { key: Key::Down, .. } => {
if !textarea.search_forward(false) { if !textarea.search_forward(false) {
@@ -274,11 +275,13 @@ impl<'a> Editor<'a> {
key: Key::Char('g'), key: Key::Char('g'),
ctrl: false, ctrl: false,
alt: true, alt: true,
..
} }
| Input { | Input {
key: Key::Char('p'), key: Key::Char('p'),
ctrl: true, ctrl: true,
alt: false, alt: false,
..
} }
| Input { key: Key::Up, .. } => { | Input { key: Key::Up, .. } => {
if !textarea.search_back(false) { if !textarea.search_back(false) {

Просмотреть файл

@@ -10,10 +10,12 @@ use std::iter;
use tui::text::Spans as Line; use tui::text::Spans as Line;
use unicode_width::UnicodeWidthChar as _; use unicode_width::UnicodeWidthChar as _;
#[derive(Debug)]
enum Boundary { enum Boundary {
Cursor(Style), Cursor(Style),
#[cfg(feature = "search")] #[cfg(feature = "search")]
Search(Style), Search(Style),
Select(Style),
End, End,
} }
@@ -21,9 +23,10 @@ impl Boundary {
fn cmp(&self, other: &Boundary) -> Ordering { fn cmp(&self, other: &Boundary) -> Ordering {
fn rank(b: &Boundary) -> u8 { fn rank(b: &Boundary) -> u8 {
match b { match b {
Boundary::Cursor(_) => 2, Boundary::Cursor(_) => 3,
#[cfg(feature = "search")] #[cfg(feature = "search")]
Boundary::Search(_) => 1, Boundary::Search(_) => 1,
Boundary::Select(_) => 2,
Boundary::End => 0, Boundary::End => 0,
} }
} }
@@ -36,6 +39,7 @@ impl Boundary {
#[cfg(feature = "search")] #[cfg(feature = "search")]
Boundary::Search(s) => Some(*s), Boundary::Search(s) => Some(*s),
Boundary::End => None, Boundary::End => None,
Boundary::Select(s) => Some(*s),
} }
} }
} }
@@ -94,16 +98,24 @@ impl DisplayTextBuilder {
pub struct LineHighlighter<'a> { pub struct LineHighlighter<'a> {
line: &'a str, line: &'a str,
spans: Vec<Span<'a>>, spans: Vec<Span<'a>>,
// reminder - the usize is the start of a section, in BYTES, not chars
boundaries: Vec<(Boundary, usize)>, // TODO: Consider smallvec boundaries: Vec<(Boundary, usize)>, // TODO: Consider smallvec
style_begin: Style, style_begin: Style,
cursor_at_end: bool, cursor_at_end: bool,
cursor_style: Style, cursor_style: Style,
tab_len: u8, tab_len: u8,
mask: Option<char>, mask: Option<char>,
select_style: Style,
} }
impl<'a> LineHighlighter<'a> { impl<'a> LineHighlighter<'a> {
pub fn new(line: &'a str, cursor_style: Style, tab_len: u8, mask: Option<char>) -> Self { pub fn new(
line: &'a str,
cursor_style: Style,
tab_len: u8,
mask: Option<char>,
select_style: Style,
) -> Self {
Self { Self {
line, line,
spans: vec![], spans: vec![],
@@ -113,6 +125,7 @@ impl<'a> LineHighlighter<'a> {
cursor_style, cursor_style,
tab_len, tab_len,
mask, mask,
select_style,
} }
} }
@@ -122,6 +135,11 @@ impl<'a> LineHighlighter<'a> {
.push(Span::styled(format!("{}{} ", pad, row + 1), style)); .push(Span::styled(format!("{}{} ", pad, row + 1), style));
} }
pub fn select(&mut self, start: usize, end: usize, style: Style) {
self.boundaries.push((Boundary::Select(style), start));
self.boundaries.push((Boundary::End, end));
}
pub fn cursor_line(&mut self, cursor_col: usize, style: Style) { pub fn cursor_line(&mut self, cursor_col: usize, style: Style) {
if let Some((start, c)) = self.line.char_indices().nth(cursor_col) { if let Some((start, c)) = self.line.char_indices().nth(cursor_col) {
self.boundaries self.boundaries
@@ -133,6 +151,60 @@ impl<'a> LineHighlighter<'a> {
self.style_begin = style; self.style_begin = style;
} }
pub(crate) fn select_line(
&mut self,
row: usize,
line: &str,
start: (usize, usize),
end: (usize, usize),
) {
// is this row selected?
// note that the input coordinates here are in char offsets not bytes
// so a lot of this code is converting from char offsets to byte offsets
if start.0 <= row && end.0 >= row {
let mut indices = line.char_indices();
let llen = line.len();
match (start.0 == row, end.0 == row) {
(true, true) => {
// only line
self.select(
indices.nth(start.1).unwrap_or((llen, 'x')).0,
indices.nth(end.1 - start.1).unwrap_or((llen, 'x')).0,
self.select_style,
);
}
(true, false) => {
// a line in the start of selection
// the +1 extends the highlight one beyond end, to show
// that the newline is included
// same done for middle rows below
self.select(
indices.nth(start.1).unwrap_or((llen, 'x')).0,
llen + 1,
self.select_style,
);
}
(false, true) => {
// last line
if end.1 > 0 {
self.select(
0,
indices.nth(end.1).unwrap_or((llen, 'x')).0,
self.select_style,
);
}
}
(false, false) => {
// in the middle of selection
self.select(0, llen + 1, self.select_style);
}
}
}
}
#[cfg(feature = "search")] #[cfg(feature = "search")]
pub fn search(&mut self, matches: impl Iterator<Item = (usize, usize)>, style: Style) { pub fn search(&mut self, matches: impl Iterator<Item = (usize, usize)>, style: Style) {
for (start, end) in matches { for (start, end) in matches {
@@ -153,6 +225,7 @@ impl<'a> LineHighlighter<'a> {
cursor_style, cursor_style,
cursor_at_end, cursor_at_end,
mask, mask,
select_style,
} = self; } = self;
let mut builder = DisplayTextBuilder::new(tab_len, mask); let mut builder = DisplayTextBuilder::new(tab_len, mask);
@@ -172,11 +245,19 @@ impl<'a> LineHighlighter<'a> {
let mut style = style_begin; let mut style = style_begin;
let mut start = 0; let mut start = 0;
let mut stack = vec![]; let mut stack = vec![];
let mut dont_add_cursor = false;
for (next_boundary, end) in boundaries { for (next_boundary, end) in boundaries {
if start < end { if start < end {
// add extra select space at line end to indicate
// that the \n will be deleted / included
if end > line.len() && style == select_style {
spans.push(Span::styled(builder.build(&line[start..end - 1]), style));
spans.push(Span::styled(" ", style));
dont_add_cursor = true;
} else {
spans.push(Span::styled(builder.build(&line[start..end]), style)); spans.push(Span::styled(builder.build(&line[start..end]), style));
} }
}
style = if let Some(s) = next_boundary.style() { style = if let Some(s) = next_boundary.style() {
stack.push(style); stack.push(style);
@@ -186,13 +267,13 @@ impl<'a> LineHighlighter<'a> {
}; };
start = end; start = end;
} }
if start < line.len() {
if start != line.len() {
spans.push(Span::styled(builder.build(&line[start..]), style)); spans.push(Span::styled(builder.build(&line[start..]), style));
} }
if cursor_at_end { if cursor_at_end && !dont_add_cursor {
spans.push(Span::styled(" ", cursor_style)); spans.push(Span::styled(" ", cursor_style));
} }
Line::from(spans) Line::from(spans)
} }
} }

Просмотреть файл

@@ -49,9 +49,15 @@ impl From<KeyEvent> for Input {
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL); let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
let alt = key.modifiers.contains(KeyModifiers::ALT); let alt = key.modifiers.contains(KeyModifiers::ALT);
let shift = key.modifiers.contains(KeyModifiers::SHIFT);
let key = Key::from(key.code); let key = Key::from(key.code);
Self { key, ctrl, alt } Self {
key,
ctrl,
alt,
shift,
}
} }
} }
@@ -72,7 +78,14 @@ impl From<MouseEvent> for Input {
let key = Key::from(mouse.kind); let key = Key::from(mouse.kind);
let ctrl = mouse.modifiers.contains(KeyModifiers::CONTROL); let ctrl = mouse.modifiers.contains(KeyModifiers::CONTROL);
let alt = mouse.modifiers.contains(KeyModifiers::ALT); let alt = mouse.modifiers.contains(KeyModifiers::ALT);
Self { key, ctrl, alt } let shift = mouse.modifiers.contains(KeyModifiers::SHIFT);
Self {
key,
ctrl,
alt,
shift,
}
} }
} }

Просмотреть файл

@@ -90,7 +90,7 @@ impl Default for Key {
/// textarea.input(Input { /// textarea.input(Input {
/// key: Key::Char('a'), /// key: Key::Char('a'),
/// ctrl: true, /// ctrl: true,
/// alt: false, /// alt: false,shift:false
/// }); /// });
/// ``` /// ```
#[derive(Debug, Clone, Default, PartialEq, Hash)] #[derive(Debug, Clone, Default, PartialEq, Hash)]
@@ -102,6 +102,8 @@ pub struct Input {
pub ctrl: bool, pub ctrl: bool,
/// Alt modifier key. `true` means Alt key was pressed. /// Alt modifier key. `true` means Alt key was pressed.
pub alt: bool, pub alt: bool,
// Shift key. `true` means Shift key was pressed.
pub shift: bool,
} }
#[cfg(test)] #[cfg(test)]
@@ -110,7 +112,12 @@ mod tests {
#[allow(dead_code)] #[allow(dead_code)]
pub(crate) fn input(key: Key, ctrl: bool, alt: bool) -> Input { pub(crate) fn input(key: Key, ctrl: bool, alt: bool) -> Input {
Input { key, ctrl, alt } Input {
key,
ctrl,
alt,
shift: false,
}
} }
#[test] #[test]

Просмотреть файл

@@ -17,6 +17,7 @@ impl From<KeyEvent> for Input {
fn from(key: KeyEvent) -> Self { fn from(key: KeyEvent) -> Self {
let mut ctrl = false; let mut ctrl = false;
let mut alt = false; let mut alt = false;
let shift = false;
let key = match key { let key = match key {
KeyEvent::Char('\n' | '\r') => Key::Enter, KeyEvent::Char('\n' | '\r') => Key::Enter,
KeyEvent::Char(c) => Key::Char(c), KeyEvent::Char(c) => Key::Char(c),
@@ -44,7 +45,12 @@ impl From<KeyEvent> for Input {
_ => Key::Null, _ => Key::Null,
}; };
Input { key, ctrl, alt } Input {
key,
ctrl,
alt,
shift,
}
} }
} }
@@ -71,6 +77,7 @@ impl From<MouseEvent> for Input {
key, key,
ctrl: false, ctrl: false,
alt: false, alt: false,
shift: false,
} }
} }
} }

Просмотреть файл

@@ -46,8 +46,13 @@ impl From<KeyEvent> for Input {
let key = Key::from(key); let key = Key::from(key);
let ctrl = modifiers.contains(Modifiers::CTRL); let ctrl = modifiers.contains(Modifiers::CTRL);
let alt = modifiers.contains(Modifiers::ALT); let alt = modifiers.contains(Modifiers::ALT);
let shift = modifiers.contains(Modifiers::SHIFT);
Self { key, ctrl, alt } Self {
key,
ctrl,
alt,
shift,
}
} }
} }
@@ -77,8 +82,13 @@ impl From<MouseEvent> for Input {
let key = Key::from(mouse_buttons); let key = Key::from(mouse_buttons);
let ctrl = modifiers.contains(Modifiers::CTRL); let ctrl = modifiers.contains(Modifiers::CTRL);
let alt = modifiers.contains(Modifiers::ALT); let alt = modifiers.contains(Modifiers::ALT);
let shift = modifiers.contains(Modifiers::SHIFT);
Self { key, ctrl, alt } Self {
key,
ctrl,
alt,
shift,
}
} }
} }
@@ -94,8 +104,13 @@ impl From<PixelMouseEvent> for Input {
let key = Key::from(mouse_buttons); let key = Key::from(mouse_buttons);
let ctrl = modifiers.contains(Modifiers::CTRL); let ctrl = modifiers.contains(Modifiers::CTRL);
let alt = modifiers.contains(Modifiers::ALT); let alt = modifiers.contains(Modifiers::ALT);
let shift = modifiers.contains(Modifiers::SHIFT);
Self { key, ctrl, alt } Self {
key,
ctrl,
alt,
shift,
}
} }
} }

Просмотреть файл

@@ -1,3 +1,5 @@
use std::cell::Cell;
use crate::cursor::CursorMove; use crate::cursor::CursorMove;
use crate::highlight::LineHighlighter; use crate::highlight::LineHighlighter;
use crate::history::{Edit, EditKind, History}; use crate::history::{Edit, EditKind, History};
@@ -55,7 +57,7 @@ impl ToString for YankText {
/// let mut textarea = TextArea::default(); /// let mut textarea = TextArea::default();
/// ///
/// // Input 'a' /// // Input 'a'
/// let input = Input { key: Key::Char('a'), ctrl: false, alt: false }; /// let input = Input { key: Key::Char('a'), ctrl: false, alt: false , shift:false};
/// textarea.input(input); /// textarea.input(input);
/// ///
/// // Get widget to render. /// // Get widget to render.
@@ -84,6 +86,11 @@ pub struct TextArea<'a> {
pub(crate) placeholder: String, pub(crate) placeholder: String,
pub(crate) placeholder_style: Style, pub(crate) placeholder_style: Style,
mask: Option<char>, mask: Option<char>,
// these are Cells so that should_end_selection can be called
// in a non mutable function call - ie self.widget()
select_start: Cell<Option<(usize, usize)>>,
pending_end_selection: Cell<bool>,
select_style: Style,
} }
/// Convert any iterator whose elements can be converted into [`String`] into [`TextArea`]. Each [`String`] element is /// Convert any iterator whose elements can be converted into [`String`] into [`TextArea`]. Each [`String`] element is
@@ -184,6 +191,9 @@ impl<'a> TextArea<'a> {
placeholder: String::new(), placeholder: String::new(),
placeholder_style: Style::default().fg(Color::DarkGray), placeholder_style: Style::default().fg(Color::DarkGray),
mask: None, mask: None,
select_start: Default::default(),
select_style: Style::default().bg(Color::LightBlue),
pending_end_selection: Cell::new(false),
} }
} }
@@ -225,16 +235,19 @@ impl<'a> TextArea<'a> {
/// ``` /// ```
pub fn input(&mut self, input: impl Into<Input>) -> bool { pub fn input(&mut self, input: impl Into<Input>) -> bool {
let input = input.into(); let input = input.into();
self.should_select(&input);
let modified = match input { let modified = match input {
Input { Input {
key: Key::Char('m'), key: Key::Char('m'),
ctrl: true, ctrl: true,
alt: false, alt: false,
..
} }
| Input { | Input {
key: Key::Char('\n' | '\r'), key: Key::Char('\n' | '\r'),
ctrl: false, ctrl: false,
alt: false, alt: false,
..
} }
| Input { | Input {
key: Key::Enter, .. key: Key::Enter, ..
@@ -246,6 +259,7 @@ impl<'a> TextArea<'a> {
key: Key::Char(c), key: Key::Char(c),
ctrl: false, ctrl: false,
alt: false, alt: false,
..
} => { } => {
self.insert_char(c); self.insert_char(c);
true true
@@ -254,124 +268,146 @@ impl<'a> TextArea<'a> {
key: Key::Tab, key: Key::Tab,
ctrl: false, ctrl: false,
alt: false, alt: false,
..
} => self.insert_tab(), } => self.insert_tab(),
Input { Input {
key: Key::Char('h'), key: Key::Char('h'),
ctrl: true, ctrl: true,
alt: false, alt: false,
..
} }
| Input { | Input {
key: Key::Backspace, key: Key::Backspace,
ctrl: false, ctrl: false,
alt: false, alt: false,
..
} => self.delete_char(), } => self.delete_char(),
Input { Input {
key: Key::Char('d'), key: Key::Char('d'),
ctrl: true, ctrl: true,
alt: false, alt: false,
..
} }
| Input { | Input {
key: Key::Delete, key: Key::Delete,
ctrl: false, ctrl: false,
alt: false, alt: false,
..
} => self.delete_next_char(), } => self.delete_next_char(),
Input { Input {
key: Key::Char('k'), key: Key::Char('k'),
ctrl: true, ctrl: true,
alt: false, alt: false,
..
} => self.delete_line_by_end(), } => self.delete_line_by_end(),
Input { Input {
key: Key::Char('j'), key: Key::Char('j'),
ctrl: true, ctrl: true,
alt: false, alt: false,
..
} => self.delete_line_by_head(), } => self.delete_line_by_head(),
Input { Input {
key: Key::Char('w'), key: Key::Char('w'),
ctrl: true, ctrl: true,
alt: false, alt: false,
..
} }
| Input { | Input {
key: Key::Char('h'), key: Key::Char('h'),
ctrl: false, ctrl: false,
alt: true, alt: true,
..
} }
| Input { | Input {
key: Key::Backspace, key: Key::Backspace,
ctrl: false, ctrl: false,
alt: true, alt: true,
..
} => self.delete_word(), } => self.delete_word(),
Input { Input {
key: Key::Delete, key: Key::Delete,
ctrl: false, ctrl: false,
alt: true, alt: true,
..
} }
| Input { | Input {
key: Key::Char('d'), key: Key::Char('d'),
ctrl: false, ctrl: false,
alt: true, alt: true,
..
} => self.delete_next_word(), } => self.delete_next_word(),
Input { Input {
key: Key::Char('n'), key: Key::Char('n' | 'N'),
ctrl: true, ctrl: true,
alt: false, alt: false,
..
} }
| Input { | Input {
key: Key::Down, key: Key::Down,
ctrl: false, ctrl: false,
alt: false, alt: false,
..
} => { } => {
self.move_cursor(CursorMove::Down); self.move_cursor(CursorMove::Down);
false false
} }
Input { Input {
key: Key::Char('p'), key: Key::Char('p' | 'P'),
ctrl: true, ctrl: true,
alt: false, alt: false,
..
} }
| Input { | Input {
key: Key::Up, key: Key::Up,
ctrl: false, ctrl: false,
alt: false, alt: false,
..
} => { } => {
self.move_cursor(CursorMove::Up); self.move_cursor(CursorMove::Up);
false false
} }
Input { Input {
key: Key::Char('f'), key: Key::Char('f' | 'F'),
ctrl: true, ctrl: true,
alt: false, alt: false,
..
} }
| Input { | Input {
key: Key::Right, key: Key::Right,
ctrl: false, ctrl: false,
alt: false, alt: false,
..
} => { } => {
self.move_cursor(CursorMove::Forward); self.move_cursor(CursorMove::Forward);
false false
} }
Input { Input {
key: Key::Char('b'), key: Key::Char('b' | 'B'),
ctrl: true, ctrl: true,
alt: false, alt: false,
..
} }
| Input { | Input {
key: Key::Left, key: Key::Left,
ctrl: false, ctrl: false,
alt: false, alt: false,
..
} => { } => {
self.move_cursor(CursorMove::Back); self.move_cursor(CursorMove::Back);
false false
} }
Input { Input {
key: Key::Char('a'), key: Key::Char('a' | 'A'),
ctrl: true, ctrl: true,
alt: false, alt: false,
..
} }
| Input { key: Key::Home, .. } | Input { key: Key::Home, .. }
| Input { | Input {
key: Key::Left | Key::Char('b'), key: Key::Left | Key::Char('b' | 'B'),
ctrl: true, ctrl: true,
alt: true, alt: true,
..
} => { } => {
self.move_cursor(CursorMove::Head); self.move_cursor(CursorMove::Head);
false false
@@ -380,12 +416,14 @@ impl<'a> TextArea<'a> {
key: Key::Char('e'), key: Key::Char('e'),
ctrl: true, ctrl: true,
alt: false, alt: false,
..
} }
| Input { key: Key::End, .. } | Input { key: Key::End, .. }
| Input { | Input {
key: Key::Right | Key::Char('f'), key: Key::Right | Key::Char('f' | 'F'),
ctrl: true, ctrl: true,
alt: true, alt: true,
..
} => { } => {
self.move_cursor(CursorMove::End); self.move_cursor(CursorMove::End);
false false
@@ -394,11 +432,13 @@ impl<'a> TextArea<'a> {
key: Key::Char('<'), key: Key::Char('<'),
ctrl: false, ctrl: false,
alt: true, alt: true,
..
} }
| Input { | Input {
key: Key::Up | Key::Char('p'), key: Key::Up | Key::Char('p' | 'P'),
ctrl: true, ctrl: true,
alt: true, alt: true,
..
} => { } => {
self.move_cursor(CursorMove::Top); self.move_cursor(CursorMove::Top);
false false
@@ -407,37 +447,43 @@ impl<'a> TextArea<'a> {
key: Key::Char('>'), key: Key::Char('>'),
ctrl: false, ctrl: false,
alt: true, alt: true,
..
} }
| Input { | Input {
key: Key::Down | Key::Char('n'), key: Key::Down | Key::Char('n' | 'N'),
ctrl: true, ctrl: true,
alt: true, alt: true,
..
} => { } => {
self.move_cursor(CursorMove::Bottom); self.move_cursor(CursorMove::Bottom);
false false
} }
Input { Input {
key: Key::Char('f'), key: Key::Char('f' | 'F'),
ctrl: false, ctrl: false,
alt: true, alt: true,
..
} }
| Input { | Input {
key: Key::Right, key: Key::Right,
ctrl: true, ctrl: true,
alt: false, alt: false,
..
} => { } => {
self.move_cursor(CursorMove::WordForward); self.move_cursor(CursorMove::WordForward);
false false
} }
Input { Input {
key: Key::Char('b'), key: Key::Char('b' | 'B'),
ctrl: false, ctrl: false,
alt: true, alt: true,
..
} }
| Input { | Input {
key: Key::Left, key: Key::Left,
ctrl: true, ctrl: true,
alt: false, alt: false,
..
} => { } => {
self.move_cursor(CursorMove::WordBack); self.move_cursor(CursorMove::WordBack);
false false
@@ -446,16 +492,19 @@ impl<'a> TextArea<'a> {
key: Key::Char(']'), key: Key::Char(']'),
ctrl: false, ctrl: false,
alt: true, alt: true,
..
} }
| Input { | Input {
key: Key::Char('n'), key: Key::Char('n' | 'N'),
ctrl: false, ctrl: false,
alt: true, alt: true,
..
} }
| Input { | Input {
key: Key::Down, key: Key::Down,
ctrl: true, ctrl: true,
alt: false, alt: false,
..
} => { } => {
self.move_cursor(CursorMove::ParagraphForward); self.move_cursor(CursorMove::ParagraphForward);
false false
@@ -464,16 +513,19 @@ impl<'a> TextArea<'a> {
key: Key::Char('['), key: Key::Char('['),
ctrl: false, ctrl: false,
alt: true, alt: true,
..
} }
| Input { | Input {
key: Key::Char('p'), key: Key::Char('p' | 'P'),
ctrl: false, ctrl: false,
alt: true, alt: true,
..
} }
| Input { | Input {
key: Key::Up, key: Key::Up,
ctrl: true, ctrl: true,
alt: false, alt: false,
..
} => { } => {
self.move_cursor(CursorMove::ParagraphBack); self.move_cursor(CursorMove::ParagraphBack);
false false
@@ -482,21 +534,43 @@ impl<'a> TextArea<'a> {
key: Key::Char('u'), key: Key::Char('u'),
ctrl: true, ctrl: true,
alt: false, alt: false,
..
} => self.undo(), } => self.undo(),
Input { Input {
key: Key::Char('r'), key: Key::Char('r'),
ctrl: true, ctrl: true,
alt: false, alt: false,
..
} => self.redo(), } => self.redo(),
Input { Input {
key: Key::Char('y'), key: Key::Char('y'),
ctrl: true, ctrl: true,
alt: false, alt: false,
..
} => self.paste(), } => self.paste(),
Input { Input {
key: Key::Char('v'), key: Key::Char('c'),
ctrl: true, ctrl: true,
alt: false, alt: false,
..
} => {
self.copy();
false
}
Input {
key: Key::Char('x'),
ctrl: true,
alt: false,
..
} => {
self.cut();
false
}
Input {
key: Key::Char('v' | 'V'),
ctrl: true,
alt: false,
..
} }
| Input { | Input {
key: Key::PageDown, .. key: Key::PageDown, ..
@@ -505,9 +579,10 @@ impl<'a> TextArea<'a> {
false false
} }
Input { Input {
key: Key::Char('v'), key: Key::Char('v' | 'V'),
ctrl: false, ctrl: false,
alt: true, alt: true,
..
} }
| Input { | Input {
key: Key::PageUp, .. key: Key::PageUp, ..
@@ -531,7 +606,6 @@ impl<'a> TextArea<'a> {
} }
_ => false, _ => false,
}; };
// Check invariants // Check invariants
debug_assert!(!self.lines.is_empty(), "no line after {:?}", input); debug_assert!(!self.lines.is_empty(), "no line after {:?}", input);
let (r, c) = self.cursor; let (r, c) = self.cursor;
@@ -550,7 +624,7 @@ impl<'a> TextArea<'a> {
self.lines[r], self.lines[r],
input, input,
); );
self.should_end_selection();
modified modified
} }
@@ -567,11 +641,14 @@ impl<'a> TextArea<'a> {
/// This method is useful when you want to define your own key mappings and don't want default key mappings. /// This method is useful when you want to define your own key mappings and don't want default key mappings.
/// See 'Define your own key mappings' section in [the module document](./index.html). /// See 'Define your own key mappings' section in [the module document](./index.html).
pub fn input_without_shortcuts(&mut self, input: impl Into<Input>) -> bool { pub fn input_without_shortcuts(&mut self, input: impl Into<Input>) -> bool {
match input.into() { let input = input.into();
self.should_select(&input);
let modified = match input {
Input { Input {
key: Key::Char(c), key: Key::Char(c),
ctrl: false, ctrl: false,
alt: false, alt: false,
..
} => { } => {
self.insert_char(c); self.insert_char(c);
true true
@@ -580,6 +657,7 @@ impl<'a> TextArea<'a> {
key: Key::Tab, key: Key::Tab,
ctrl: false, ctrl: false,
alt: false, alt: false,
..
} => self.insert_tab(), } => self.insert_tab(),
Input { Input {
key: Key::Backspace, key: Key::Backspace,
@@ -609,7 +687,9 @@ impl<'a> TextArea<'a> {
false false
} }
_ => false, _ => false,
} };
self.should_end_selection();
modified
} }
fn push_history(&mut self, kind: EditKind, cursor_before: (usize, usize)) { fn push_history(&mut self, kind: EditKind, cursor_before: (usize, usize)) {
@@ -627,6 +707,8 @@ impl<'a> TextArea<'a> {
/// assert_eq!(textarea.lines(), ["a"]); /// assert_eq!(textarea.lines(), ["a"]);
/// ``` /// ```
pub fn insert_char(&mut self, c: char) { pub fn insert_char(&mut self, c: char) {
self.delete_selection(false);
let (row, col) = self.cursor; let (row, col) = self.cursor;
let line = &mut self.lines[row]; let line = &mut self.lines[row];
let i = line let i = line
@@ -652,6 +734,7 @@ impl<'a> TextArea<'a> {
/// assert_eq!(textarea.lines(), ["hello, world", "goodbye, world"]); /// assert_eq!(textarea.lines(), ["hello, world", "goodbye, world"]);
/// ``` /// ```
pub fn insert_str<S: AsRef<str>>(&mut self, s: S) -> bool { pub fn insert_str<S: AsRef<str>>(&mut self, s: S) -> bool {
self.end_selection();
let mut lines: Vec<_> = s.as_ref().lines().map(ToString::to_string).collect(); let mut lines: Vec<_> = s.as_ref().lines().map(ToString::to_string).collect();
match lines.len() { match lines.len() {
0 => false, 0 => false,
@@ -727,6 +810,14 @@ impl<'a> TextArea<'a> {
/// assert_eq!(textarea.lines(), ["🐱", "🐮"]); /// assert_eq!(textarea.lines(), ["🐱", "🐮"]);
/// ``` /// ```
pub fn delete_str(&mut self, chars: usize) -> bool { pub fn delete_str(&mut self, chars: usize) -> bool {
self.delete_str_internal(chars, true, true)
}
// grabs the specified (maybe multi line string)
// sometimes yanked
// sometimes deleted from lines array and placed in history
fn delete_str_internal(&mut self, chars: usize, yank: bool, delete: bool) -> bool {
if chars == 0 { if chars == 0 {
return false; return false;
} }
@@ -759,14 +850,22 @@ impl<'a> TextArea<'a> {
// First line // First line
if let Some(offset) = find_end(&line[first_start..]) { if let Some(offset) = find_end(&line[first_start..]) {
let removed = self.lines[row] let removed = if delete {
self.lines[row]
.drain(first_start..first_start + offset) .drain(first_start..first_start + offset)
.as_str() .as_str()
.to_string(); .to_string()
} else {
self.lines[row][first_start..first_start + offset].to_string()
};
if yank {
self.yank = removed.clone().into(); self.yank = removed.clone().into();
};
if delete {
self.push_history(EditKind::DeleteStr(removed, first_start), self.cursor); self.push_history(EditKind::DeleteStr(removed, first_start), self.cursor);
return true;
} }
return true;
};
let mut r = row + 1; let mut r = row + 1;
let mut i = 0; let mut i = 0;
@@ -780,23 +879,41 @@ impl<'a> TextArea<'a> {
r += 1; r += 1;
} }
let mut deleted = vec![self.lines[row].drain(first_start..).as_str().to_string()]; let mut deleted = if delete {
vec![self.lines[row].drain(first_start..).as_str().to_string()]
} else {
vec![self.lines[row][first_start..].to_string()]
};
if delete {
deleted.extend(self.lines.drain(row + 1..r)); deleted.extend(self.lines.drain(row + 1..r));
} else {
deleted.extend(self.lines[row + 1..r].iter().map(|s| s.to_string()));
}
if row + 1 < self.lines.len() { if row + 1 < self.lines.len() {
let mut last_line = self.lines.remove(row + 1); let mut last_line = if delete {
self.lines[row].push_str(&last_line[i..]); let ll = self.lines.remove(row + 1);
self.lines[row].push_str(&ll[i..]);
ll
} else {
self.lines[row + 1].clone()
};
last_line.truncate(i); last_line.truncate(i);
deleted.push(last_line); deleted.push(last_line);
} }
if yank {
self.yank = YankText::Chunk(deleted.clone()); self.yank = YankText::Chunk(deleted.clone());
};
if delete {
let edit = if deleted.len() == 1 { let edit = if deleted.len() == 1 {
EditKind::DeleteStr(deleted.remove(0), first_start) EditKind::DeleteStr(deleted.remove(0), first_start)
} else { } else {
EditKind::DeleteChunk(deleted, row, first_start) EditKind::DeleteChunk(deleted, row, first_start)
}; };
self.push_history(edit, self.cursor); self.push_history(edit, self.cursor);
}
true true
} }
@@ -871,6 +988,7 @@ impl<'a> TextArea<'a> {
/// assert_eq!(textarea.lines(), ["h", "i"]); /// assert_eq!(textarea.lines(), ["h", "i"]);
/// ``` /// ```
pub fn insert_newline(&mut self) { pub fn insert_newline(&mut self) {
self.delete_selection(false);
let (row, col) = self.cursor; let (row, col) = self.cursor;
let line = &mut self.lines[row]; let line = &mut self.lines[row];
let idx = line let idx = line
@@ -925,6 +1043,9 @@ impl<'a> TextArea<'a> {
/// assert_eq!(textarea.lines(), ["bc"]); /// assert_eq!(textarea.lines(), ["bc"]);
/// ``` /// ```
pub fn delete_char(&mut self) -> bool { pub fn delete_char(&mut self) -> bool {
if self.select_start.get().is_some() {
return self.delete_selection(false);
}
let (row, col) = self.cursor; let (row, col) = self.cursor;
if col == 0 { if col == 0 {
return self.delete_newline(); return self.delete_newline();
@@ -953,6 +1074,9 @@ impl<'a> TextArea<'a> {
/// assert_eq!(textarea.lines(), ["ac"]); /// assert_eq!(textarea.lines(), ["ac"]);
/// ``` /// ```
pub fn delete_next_char(&mut self) -> bool { pub fn delete_next_char(&mut self) -> bool {
if self.select_start.get().is_some() {
return self.delete_selection(false);
}
let before = self.cursor; let before = self.cursor;
self.move_cursor(CursorMove::Forward); self.move_cursor(CursorMove::Forward);
if before == self.cursor { if before == self.cursor {
@@ -1080,6 +1204,7 @@ impl<'a> TextArea<'a> {
/// assert_eq!(textarea.lines(), [" bbb cccaaa"]); /// assert_eq!(textarea.lines(), [" bbb cccaaa"]);
/// ``` /// ```
pub fn paste(&mut self) -> bool { pub fn paste(&mut self) -> bool {
self.delete_selection(false);
match self.yank.clone() { match self.yank.clone() {
YankText::Piece(s) => self.insert_piece(s), YankText::Piece(s) => self.insert_piece(s),
YankText::Chunk(c) => self.insert_chunk(c), YankText::Chunk(c) => self.insert_chunk(c),
@@ -1097,7 +1222,7 @@ impl<'a> TextArea<'a> {
/// assert_eq!(textarea.cursor(), (0, 1)); /// assert_eq!(textarea.cursor(), (0, 1));
/// textarea.move_cursor(CursorMove::Down); /// textarea.move_cursor(CursorMove::Down);
/// assert_eq!(textarea.cursor(), (1, 1)); /// assert_eq!(textarea.cursor(), (1, 1));
/// ```
pub fn move_cursor(&mut self, m: CursorMove) { pub fn move_cursor(&mut self, m: CursorMove) {
if let Some(cursor) = m.next_cursor(self.cursor, &self.lines, &self.viewport) { if let Some(cursor) = m.next_cursor(self.cursor, &self.lines, &self.viewport) {
self.cursor = cursor; self.cursor = cursor;
@@ -1147,7 +1272,13 @@ impl<'a> TextArea<'a> {
} }
pub(crate) fn line_spans<'b>(&'b self, line: &'b str, row: usize, lnum_len: u8) -> Line<'b> { pub(crate) fn line_spans<'b>(&'b self, line: &'b str, row: usize, lnum_len: u8) -> Line<'b> {
let mut hl = LineHighlighter::new(line, self.cursor_style, self.tab_len, self.mask); let mut hl = LineHighlighter::new(
line,
self.cursor_style,
self.tab_len,
self.mask,
self.select_style,
);
if let Some(style) = self.line_number_style { if let Some(style) = self.line_number_style {
hl.line_number(row, lnum_len, style); hl.line_number(row, lnum_len, style);
@@ -1162,6 +1293,12 @@ impl<'a> TextArea<'a> {
hl.search(matches, self.search.style); hl.search(matches, self.search.style);
} }
// any selected text to highlight?
if self.select_start.get().is_some() {
let (start, end, _) = self.normalize_selection();
hl.select_line(row, line, start, end);
}
hl.into_spans() hl.into_spans()
} }
@@ -1192,6 +1329,7 @@ impl<'a> TextArea<'a> {
/// } /// }
/// ``` /// ```
pub fn widget(&'a self) -> impl Widget + 'a { pub fn widget(&'a self) -> impl Widget + 'a {
self.should_end_selection();
Renderer::new(self) Renderer::new(self)
} }
@@ -1253,7 +1391,7 @@ impl<'a> TextArea<'a> {
/// use tui_textarea::{TextArea, Input, Key}; /// use tui_textarea::{TextArea, Input, Key};
/// ///
/// let mut textarea = TextArea::default(); /// let mut textarea = TextArea::default();
/// let tab_input = Input { key: Key::Tab, ctrl: false, alt: false }; /// let tab_input = Input { key: Key::Tab, ctrl: false, alt:false, shift: false };
/// ///
/// textarea.set_tab_length(8); /// textarea.set_tab_length(8);
/// textarea.input(tab_input.clone()); /// textarea.input(tab_input.clone());
@@ -1879,10 +2017,226 @@ impl<'a> TextArea<'a> {
scrolling.into().scroll(&mut self.viewport); scrolling.into().scroll(&mut self.viewport);
self.move_cursor(CursorMove::InViewport); self.move_cursor(CursorMove::InViewport);
} }
// public functions for select
/// Sets the style used for selected text
/// the default is LightBlue
///```
/// use tui_textarea::TextArea;
/// use ratatui::style::{Style, Color};
/// let mut textarea = TextArea::default();
/// // change the selection color from the default to Red
/// textarea.set_selection_style(Style::default().bg(Color::Red));
/// assert_eq!(textarea.get_selection_style(), Style::default().bg(Color::Red));
/// ```
pub fn set_selection_style(&mut self, style: Style) {
self.select_style = style;
}
/// Gets the style used for selected text
///
///```
/// use tui_textarea::TextArea;
/// use ratatui::style::{Style, Color};
/// let mut textarea = TextArea::default();
///
///
/// assert_eq!(textarea.get_selection_style(), Style::default().bg(Color::LightBlue));
/// ```
pub fn get_selection_style(&mut self) -> Style {
self.select_style
}
/// Copies the current selection to the yank buffer
///
///```
/// use tui_textarea::{TextArea, Key, Input};
///
///
/// let mut textarea = TextArea::default();
/// // load some text
/// textarea.insert_str("Hello World");
/// // select the word "World" by doing a ctrl-left with the shift key down
/// textarea.input(Input{key:Key::Left, ctrl:true, alt:false, shift:true});
///
/// textarea.copy();
/// assert_eq!(textarea.yank_text(), "World");
/// assert_eq!(textarea.lines(), ["Hello World"]);
///```
pub fn copy(&mut self) {
if self.select_start.get().is_some() {
let (start, end, swapped) = self.normalize_selection();
self.cursor = start;
self.delete_range(start, end, true, false);
self.cursor = if swapped { start } else { end };
}
} }
/// Deletes the current selection and places it in the yank buffer
///```
/// use tui_textarea::{TextArea, Key, Input};
///
///
/// let mut textarea = TextArea::default();
/// // load some text
/// textarea.insert_str("Hello World");
/// // select the word "World" by doing a ctrl-left with the shift key down
/// textarea.input(Input{key:Key::Left, ctrl:true, alt:false, shift:true});
///
/// textarea.cut();
/// assert_eq!(textarea.yank_text(), "World");
/// assert_eq!(textarea.lines(), ["Hello "]);
///```
///
pub fn cut(&mut self) {
self.delete_selection(true);
}
/// Must be called before calling textarea functions if you are
/// not calling [`TextArea::input`] or [`TextArea::input_without_shortcuts`] to dispatch the keys.
/// It is used to detect whether or not a selection should be started, continued or ended
///
/// Handled automatically when inputs are passed to [`TextArea::input`] or [`TextArea::input_without_shortcuts`]
///
/// Note: it is harmless to call this function even if you have some logic paths that call [`TextArea::input`]
/// The state of the shift key (or whatever you want to signal start selection) should be passed in.
/// If you need to stop an upper case letter being treated as a selection character
/// then call this function with false
///
/// Look at the modal example for a demonstration of its use
pub fn should_select(&mut self, input: &Input) {
if input.key == Key::Null {
return;
}
// Should we start selecting text, stop the current selection, or do nothing?
// the end is handled after the ending keystroke
self.pending_end_selection =
Cell::new(match (self.select_start.get().is_some(), input.shift) {
(true, true) => {
// continue select
false
}
(true, false) => {
// end select
true
}
(false, true) => {
// start select
self.start_selection();
false
}
(false, false) => {
// ignore
false
}
});
}
/// used in conjunction with code that needs to use [`TextArea::should_select`]
/// Used to force the end selection mode
///
/// Needed if a simple keystroke is upper case and you want to stop it being treated as a selection
/// Look at the modal example for a demonstration of its use - look at the 'a' and 'A' keys
///
pub fn stop_selection(&mut self) {
self.end_selection();
}
// functions for select
fn should_end_selection(&self) {
// Should we end the current selection?
if self.pending_end_selection.take() {
self.end_selection();
}
}
fn start_selection(&mut self) {
self.select_start = Cell::new(Some(self.cursor));
}
fn end_selection(&self) {
self.select_start.set(Default::default());
}
fn normalize_selection(&self) -> ((usize, usize), (usize, usize), bool) {
// selection can be from right to left or left to right
// rest of the code always wants the leftmost part first
// bool indicates if swap was done
let start = self.select_start.get().unwrap();
let end = self.cursor;
if start.0 > end.0 || (start.0 == end.0 && start.1 > end.1) {
(end, start, true)
} else {
(start, end, false)
}
}
// delete the currect selection, sometimes yank it
fn delete_selection(&mut self, yank: bool) -> bool {
let deleted = match self.select_start.get() {
Some(_) => {
let (start, end, _) = self.normalize_selection();
if start != end {
self.delete_range(start, end, yank, true);
true
} else {
false
}
}
None => false,
};
self.select_start = Default::default();
deleted
}
fn line_length(&self, row: usize) -> usize {
self.lines[row].chars().count()
}
// erase a range of text, start and end must be normalized
// erased text may be put in history and yank buffer (by delete_str)
// the end range column is not included
fn delete_range(
&mut self,
start: (usize, usize),
end: (usize, usize),
yank: bool,
delete: bool,
) {
// calculate length of range
let (start_row, start_col) = start;
let (end_row, end_col) = end;
let mut col = start_col;
let mut len = 0;
// need +1 because the row range is inclusive
for r in start_row..end_row + 1 {
// we only take some of the first line
len += self.line_length(r) - col;
// but maybe all of subsequent lines
col = 0;
}
// we didnt take the whole last line
// so subtract the bit we didnt take
len -= self.line_length(end_row) - end_col;
// plus add the number of linefeeds
len += end_row - start_row;
self.cursor = start;
self.delete_str_internal(len, yank, delete);
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
// Seaparate tests for tui-rs support // Seaparate tests for tui-rs support

Просмотреть файл

@@ -798,3 +798,386 @@ fn test_delete_str_multiple_lines() {
assert_eq!(t.cursor(), (row, col), "cursor after undo: {:?}", test); assert_eq!(t.cursor(), (row, col), "cursor after undo: {:?}", test);
} }
} }
//
// selection tests
//
use tui_textarea::{Input, Key};
/*
each test block is a tuple of
- text to be loaded into textarea
- vec of key strokes to be sent
- expected value in yank
- expected text left in textarea
the text is inserted into a new textarea
cursor is moved to (0,0)
strokes are sent
yank and remainder are checked
*/
#[test]
fn select_copy() {
for test in [
// plain ascii
(
"hello world",
vec![
(Key::Home, false, false, false),
(Key::Right, false, false, true),
(Key::Right, false, false, true),
(Key::Char('c'), true, false, false),
],
"he",
"hello world",
),
// plain ascii backwards
(
"hello world",
vec![
(Key::End, false, false, false),
(Key::Left, false, false, true),
(Key::Left, false, false, true),
(Key::Char('c'), true, false, false),
],
"ld",
"hello world",
),
// utf8
(
"あい",
vec![
(Key::Home, false, false, false),
(Key::Right, false, false, true),
(Key::Char('c'), true, false, false),
],
"",
"あい",
),
// multi line - all
(
"hello\nworld",
vec![
(Key::Char('P'), true, true, false),
(Key::Home, false, false, false),
(Key::Char('N'), true, true, true),
(Key::End, false, false, true),
(Key::Char('c'), true, false, false),
],
"hello\nworld",
"hello\nworld",
),
// multi line - some
(
"hello\nworld",
vec![
(Key::Char('P'), true, true, false),
(Key::Home, false, false, false),
(Key::Right, false, false, false),
(Key::Char('N'), true, true, true),
(Key::End, false, false, true),
(Key::Left, false, false, true),
(Key::Char('c'), true, false, false),
],
"ello\nworl",
"hello\nworld",
),
// multi - line utf8
(
"あい\nうえ",
vec![
(Key::Char('P'), true, true, false),
(Key::Home, false, false, false),
(Key::Char('N'), true, true, true),
(Key::End, false, false, true),
(Key::Char('c'), true, false, false),
],
"あい\nうえ",
"あい\nうえ",
),
// multi-line utf8 - some
(
"あい\nうえ",
vec![
(Key::Char('P'), true, true, false),
(Key::Home, false, false, false),
(Key::Right, false, false, false),
(Key::Char('N'), true, true, true),
(Key::End, false, false, true),
(Key::Left, false, false, true),
(Key::Char('c'), true, false, false),
],
"\n",
"あい\nうえ",
),
] {
let (text, strokes, yank, remaining) = test;
one_select_test(text, &strokes, yank, remaining);
}
}
#[test]
fn select_cut() {
for test in [
// plain ascii
(
"hello world",
vec![
(Key::Home, false, false, false),
(Key::Right, false, false, true),
(Key::Right, false, false, true),
(Key::Char('x'), true, false, false),
],
"he",
"llo world",
),
// plain ascii backwards
(
"hello world",
vec![
(Key::End, false, false, false),
(Key::Left, false, false, true),
(Key::Left, false, false, true),
(Key::Char('x'), true, false, false),
],
"ld",
"hello wor",
),
// utf8
(
"あい",
vec![
(Key::Home, false, false, false),
(Key::Right, false, false, true),
(Key::Char('x'), true, false, false),
],
"",
"",
),
// multi line - all
(
"hello\nworld",
vec![
(Key::Char('P'), true, true, false),
(Key::Home, false, false, false),
(Key::Char('N'), true, true, true),
(Key::End, false, false, true),
(Key::Char('x'), true, false, false),
],
"hello\nworld",
"",
),
// multi line - some
(
"hello\nworld",
vec![
(Key::Char('P'), true, true, false),
(Key::Home, false, false, false),
(Key::Right, false, false, false),
(Key::Char('N'), true, true, true),
(Key::End, false, false, true),
(Key::Left, false, false, true),
(Key::Char('x'), true, false, false),
],
"ello\nworl",
"hd",
),
// multi - line utf8
(
"あい\nうえ",
vec![
(Key::Char('P'), true, true, false),
(Key::Home, false, false, false),
(Key::Char('N'), true, true, true),
(Key::End, false, false, true),
(Key::Char('x'), true, false, false),
],
"あい\nうえ",
"",
),
// multi-line utf8 - some
(
"あい\nうえ",
vec![
(Key::Char('P'), true, true, false),
(Key::Home, false, false, false),
(Key::Right, false, false, false),
(Key::Char('N'), true, true, true),
(Key::End, false, false, true),
(Key::Left, false, false, true),
(Key::Char('x'), true, false, false),
],
"\n",
"あえ",
),
] {
let (text, strokes, yank, remaining) = test;
one_select_test(text, &strokes, yank, remaining);
}
}
#[test]
fn select_paste() {
for test in [
// plain ascii
(
"hello world",
vec![
(Key::Home, false, false, false),
(Key::Right, false, false, true),
(Key::Right, false, false, true),
(Key::Char('c'), true, false, false),
(Key::End, false, false, false),
(Key::Char('y'), true, false, false),
],
"he",
"hello worldhe",
),
(
"hello world",
vec![
(Key::Home, false, false, false),
(Key::Right, false, false, true),
(Key::Right, false, false, true),
(Key::Char('c'), true, false, false),
(Key::End, false, false, false),
(Key::Left, false, false, true),
(Key::Left, false, false, true),
(Key::Char('y'), true, false, false),
],
"he",
"hello worhe",
),
(
"hello\nworld",
vec![
(Key::Home, false, false, false),
(Key::Right, false, false, false),
(Key::Right, false, false, false),
(Key::Down, false, false, true),
(Key::Char('c'), true, false, false),
(Key::End, false, false, false),
(Key::Enter, false, false, false),
(Key::Char('y'), true, false, false),
],
"llo\nwo",
"hello\nworld\nllo\nwo",
),
] {
let (text, strokes, yank, remaining) = test;
one_select_test(text, &strokes, yank, remaining);
}
}
#[test]
fn select_all_keys() {
for test in [
(
// enter key erases selection
"hello world",
vec![
(Key::End, false, false, true),
(Key::Enter, false, false, true),
],
"",
"\n",
),
(
// word forward select
"hello world",
vec![
(Key::Char('f'), false, true, true),
(Key::Char('c'), true, false, false),
],
"hello ",
"hello world",
),
(
// word back select
"hello world",
vec![
(Key::End, false, false, false),
(Key::Char('b'), false, true, true),
(Key::Char('c'), true, false, false),
],
"world",
"hello world",
),
(
// para forward select
"hello\nworld\n\nhow\nare\nyou",
vec![
(Key::Char('n'), false, true, true),
(Key::Char('x'), true, false, true),
],
"hello\nworld\n\n",
"how\nare\nyou",
),
(
// para back select
"hello\nworld\n\nhow\nare\nyou",
vec![
// goto end
(Key::Char('n'), true, true, false),
(Key::End, false, false, false),
(Key::Char('p'), false, true, true),
(Key::Char('x'), true, false, true),
],
"\nare\nyou",
"hello\nworld\n\nhow",
),
(
// undo
"hello\nworld\n\nhow\nare\nyou",
vec![
// goto end
(Key::Char('n'), true, true, false),
(Key::End, false, false, false),
(Key::Char('p'), false, true, true),
(Key::Char('x'), true, false, false),
(Key::Char('u'), true, false, false),
],
"\nare\nyou",
"hello\nworld\n\nhow\nare\nyou",
),
] {
let (text, strokes, yank, remaining) = test;
one_select_test(text, &strokes, yank, remaining);
}
}
fn one_select_test(
text: &str,
strokes: &Vec<(Key, bool, bool, bool)>,
yank: &str,
remaining: &str,
) {
let mut ta = TextArea::default();
ta.insert_str(text);
// cursor home
ta.input(Input {
key: Key::Up,
ctrl: true,
alt: true,
shift: false,
});
ta.input(Input {
key: Key::Home,
ctrl: true,
alt: true,
shift: false,
});
for k in strokes {
let input = Input {
key: k.0,
ctrl: k.1,
alt: k.2,
shift: k.3,
};
ta.input(input);
}
assert_eq!(ta.yank_text(), yank);
assert_eq!(ta.lines().join("\n"), remaining);
}