зеркало из
https://github.com/glebtv/tui-textarea.git
synced 2026-09-03 14:26:17 +03:00
implement select (#45)
* 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
Этот коммит содержится в:
@@ -262,6 +262,7 @@ impl<'a> Editor<'a> {
|
||||
key: Key::Char('g' | 'n'),
|
||||
ctrl: true,
|
||||
alt: false,
|
||||
..
|
||||
}
|
||||
| Input { key: Key::Down, .. } => {
|
||||
if !textarea.search_forward(false) {
|
||||
@@ -272,11 +273,13 @@ impl<'a> Editor<'a> {
|
||||
key: Key::Char('g'),
|
||||
ctrl: false,
|
||||
alt: true,
|
||||
..
|
||||
}
|
||||
| Input {
|
||||
key: Key::Char('p'),
|
||||
ctrl: true,
|
||||
alt: false,
|
||||
..
|
||||
}
|
||||
| Input { key: Key::Up, .. } => {
|
||||
if !textarea.search_back(false) {
|
||||
|
||||
@@ -60,7 +60,7 @@ fn main() -> io::Result<()> {
|
||||
} else {
|
||||
TextArea::default()
|
||||
};
|
||||
|
||||
textarea.set_selection_style(Style::default().bg(Color::Red));
|
||||
let mut mode = Mode::Normal;
|
||||
loop {
|
||||
// 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()))?;
|
||||
|
||||
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 {
|
||||
Mode::Normal => match input {
|
||||
// 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 {
|
||||
key: Key::Char('h'),
|
||||
key: Key::Char('h' | 'H'),
|
||||
..
|
||||
} => textarea.move_cursor(CursorMove::Back),
|
||||
Input {
|
||||
key: Key::Char('j'),
|
||||
key: Key::Char('j' | 'J'),
|
||||
..
|
||||
} => textarea.move_cursor(CursorMove::Down),
|
||||
Input {
|
||||
key: Key::Char('k'),
|
||||
key: Key::Char('k' | 'K'),
|
||||
..
|
||||
} => textarea.move_cursor(CursorMove::Up),
|
||||
Input {
|
||||
key: Key::Char('l'),
|
||||
key: Key::Char('l' | 'L'),
|
||||
..
|
||||
} => textarea.move_cursor(CursorMove::Forward),
|
||||
Input {
|
||||
key: Key::Char('w'),
|
||||
key: Key::Char('w' | 'W'),
|
||||
..
|
||||
} => textarea.move_cursor(CursorMove::WordForward),
|
||||
Input {
|
||||
key: Key::Char('b'),
|
||||
key: Key::Char('b' | 'B'),
|
||||
ctrl: false,
|
||||
..
|
||||
} => textarea.move_cursor(CursorMove::WordBack),
|
||||
@@ -131,6 +140,13 @@ fn main() -> io::Result<()> {
|
||||
} => {
|
||||
textarea.paste();
|
||||
}
|
||||
Input {
|
||||
key: Key::Char('y'),
|
||||
ctrl: false,
|
||||
..
|
||||
} => {
|
||||
textarea.copy();
|
||||
}
|
||||
Input {
|
||||
key: Key::Char('u'),
|
||||
ctrl: false,
|
||||
@@ -166,6 +182,8 @@ fn main() -> io::Result<()> {
|
||||
key: Key::Char('A'),
|
||||
..
|
||||
} => {
|
||||
// stop selection is called to prevent 'A' being interpreted as selecting
|
||||
textarea.stop_selection();
|
||||
textarea.move_cursor(CursorMove::End);
|
||||
mode = Mode::Insert;
|
||||
}
|
||||
@@ -181,6 +199,8 @@ fn main() -> io::Result<()> {
|
||||
key: Key::Char('O'),
|
||||
..
|
||||
} => {
|
||||
// stop selection is called to prevent 'O' being interpreted as selecting
|
||||
textarea.stop_selection();
|
||||
textarea.move_cursor(CursorMove::Head);
|
||||
textarea.insert_newline();
|
||||
textarea.move_cursor(CursorMove::Up);
|
||||
@@ -190,6 +210,7 @@ fn main() -> io::Result<()> {
|
||||
key: Key::Char('I'),
|
||||
..
|
||||
} => {
|
||||
textarea.stop_selection();
|
||||
textarea.move_cursor(CursorMove::Head);
|
||||
mode = Mode::Insert;
|
||||
}
|
||||
|
||||
@@ -264,6 +264,7 @@ impl<'a> Editor<'a> {
|
||||
key: Key::Char('g' | 'n'),
|
||||
ctrl: true,
|
||||
alt: false,
|
||||
..
|
||||
}
|
||||
| Input { key: Key::Down, .. } => {
|
||||
if !textarea.search_forward(false) {
|
||||
@@ -274,11 +275,13 @@ impl<'a> Editor<'a> {
|
||||
key: Key::Char('g'),
|
||||
ctrl: false,
|
||||
alt: true,
|
||||
..
|
||||
}
|
||||
| Input {
|
||||
key: Key::Char('p'),
|
||||
ctrl: true,
|
||||
alt: false,
|
||||
..
|
||||
}
|
||||
| Input { key: Key::Up, .. } => {
|
||||
if !textarea.search_back(false) {
|
||||
|
||||
@@ -10,10 +10,12 @@ use std::iter;
|
||||
use tui::text::Spans as Line;
|
||||
use unicode_width::UnicodeWidthChar as _;
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Boundary {
|
||||
Cursor(Style),
|
||||
#[cfg(feature = "search")]
|
||||
Search(Style),
|
||||
Select(Style),
|
||||
End,
|
||||
}
|
||||
|
||||
@@ -21,9 +23,10 @@ impl Boundary {
|
||||
fn cmp(&self, other: &Boundary) -> Ordering {
|
||||
fn rank(b: &Boundary) -> u8 {
|
||||
match b {
|
||||
Boundary::Cursor(_) => 2,
|
||||
Boundary::Cursor(_) => 3,
|
||||
#[cfg(feature = "search")]
|
||||
Boundary::Search(_) => 1,
|
||||
Boundary::Select(_) => 2,
|
||||
Boundary::End => 0,
|
||||
}
|
||||
}
|
||||
@@ -36,6 +39,7 @@ impl Boundary {
|
||||
#[cfg(feature = "search")]
|
||||
Boundary::Search(s) => Some(*s),
|
||||
Boundary::End => None,
|
||||
Boundary::Select(s) => Some(*s),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -94,16 +98,24 @@ impl DisplayTextBuilder {
|
||||
pub struct LineHighlighter<'a> {
|
||||
line: &'a str,
|
||||
spans: Vec<Span<'a>>,
|
||||
// reminder - the usize is the start of a section, in BYTES, not chars
|
||||
boundaries: Vec<(Boundary, usize)>, // TODO: Consider smallvec
|
||||
style_begin: Style,
|
||||
cursor_at_end: bool,
|
||||
cursor_style: Style,
|
||||
tab_len: u8,
|
||||
mask: Option<char>,
|
||||
select_style: Style,
|
||||
}
|
||||
|
||||
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 {
|
||||
line,
|
||||
spans: vec![],
|
||||
@@ -113,6 +125,7 @@ impl<'a> LineHighlighter<'a> {
|
||||
cursor_style,
|
||||
tab_len,
|
||||
mask,
|
||||
select_style,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,6 +135,11 @@ impl<'a> LineHighlighter<'a> {
|
||||
.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) {
|
||||
if let Some((start, c)) = self.line.char_indices().nth(cursor_col) {
|
||||
self.boundaries
|
||||
@@ -133,6 +151,60 @@ impl<'a> LineHighlighter<'a> {
|
||||
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")]
|
||||
pub fn search(&mut self, matches: impl Iterator<Item = (usize, usize)>, style: Style) {
|
||||
for (start, end) in matches {
|
||||
@@ -153,6 +225,7 @@ impl<'a> LineHighlighter<'a> {
|
||||
cursor_style,
|
||||
cursor_at_end,
|
||||
mask,
|
||||
select_style,
|
||||
} = self;
|
||||
let mut builder = DisplayTextBuilder::new(tab_len, mask);
|
||||
|
||||
@@ -172,10 +245,18 @@ impl<'a> LineHighlighter<'a> {
|
||||
let mut style = style_begin;
|
||||
let mut start = 0;
|
||||
let mut stack = vec![];
|
||||
|
||||
let mut dont_add_cursor = false;
|
||||
for (next_boundary, end) in boundaries {
|
||||
if start < end {
|
||||
spans.push(Span::styled(builder.build(&line[start..end]), style));
|
||||
// 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));
|
||||
}
|
||||
}
|
||||
|
||||
style = if let Some(s) = next_boundary.style() {
|
||||
@@ -186,13 +267,13 @@ impl<'a> LineHighlighter<'a> {
|
||||
};
|
||||
start = end;
|
||||
}
|
||||
|
||||
if start != line.len() {
|
||||
if start < line.len() {
|
||||
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));
|
||||
}
|
||||
|
||||
Line::from(spans)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,9 +49,15 @@ impl From<KeyEvent> for Input {
|
||||
|
||||
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
|
||||
let alt = key.modifiers.contains(KeyModifiers::ALT);
|
||||
let shift = key.modifiers.contains(KeyModifiers::SHIFT);
|
||||
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 ctrl = mouse.modifiers.contains(KeyModifiers::CONTROL);
|
||||
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 {
|
||||
/// key: Key::Char('a'),
|
||||
/// ctrl: true,
|
||||
/// alt: false,
|
||||
/// alt: false,shift:false
|
||||
/// });
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default, PartialEq, Hash)]
|
||||
@@ -102,6 +102,8 @@ pub struct Input {
|
||||
pub ctrl: bool,
|
||||
/// Alt modifier key. `true` means Alt key was pressed.
|
||||
pub alt: bool,
|
||||
// Shift key. `true` means Shift key was pressed.
|
||||
pub shift: bool,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -110,7 +112,12 @@ mod tests {
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn input(key: Key, ctrl: bool, alt: bool) -> Input {
|
||||
Input { key, ctrl, alt }
|
||||
Input {
|
||||
key,
|
||||
ctrl,
|
||||
alt,
|
||||
shift: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -17,6 +17,7 @@ impl From<KeyEvent> for Input {
|
||||
fn from(key: KeyEvent) -> Self {
|
||||
let mut ctrl = false;
|
||||
let mut alt = false;
|
||||
let shift = false;
|
||||
let key = match key {
|
||||
KeyEvent::Char('\n' | '\r') => Key::Enter,
|
||||
KeyEvent::Char(c) => Key::Char(c),
|
||||
@@ -44,7 +45,12 @@ impl From<KeyEvent> for Input {
|
||||
_ => Key::Null,
|
||||
};
|
||||
|
||||
Input { key, ctrl, alt }
|
||||
Input {
|
||||
key,
|
||||
ctrl,
|
||||
alt,
|
||||
shift,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,6 +77,7 @@ impl From<MouseEvent> for Input {
|
||||
key,
|
||||
ctrl: false,
|
||||
alt: false,
|
||||
shift: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,8 +46,13 @@ impl From<KeyEvent> for Input {
|
||||
let key = Key::from(key);
|
||||
let ctrl = modifiers.contains(Modifiers::CTRL);
|
||||
let alt = modifiers.contains(Modifiers::ALT);
|
||||
|
||||
Self { key, ctrl, alt }
|
||||
let shift = modifiers.contains(Modifiers::SHIFT);
|
||||
Self {
|
||||
key,
|
||||
ctrl,
|
||||
alt,
|
||||
shift,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,8 +82,13 @@ impl From<MouseEvent> for Input {
|
||||
let key = Key::from(mouse_buttons);
|
||||
let ctrl = modifiers.contains(Modifiers::CTRL);
|
||||
let alt = modifiers.contains(Modifiers::ALT);
|
||||
|
||||
Self { key, ctrl, alt }
|
||||
let shift = modifiers.contains(Modifiers::SHIFT);
|
||||
Self {
|
||||
key,
|
||||
ctrl,
|
||||
alt,
|
||||
shift,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,8 +104,13 @@ impl From<PixelMouseEvent> for Input {
|
||||
let key = Key::from(mouse_buttons);
|
||||
let ctrl = modifiers.contains(Modifiers::CTRL);
|
||||
let alt = modifiers.contains(Modifiers::ALT);
|
||||
|
||||
Self { key, ctrl, alt }
|
||||
let shift = modifiers.contains(Modifiers::SHIFT);
|
||||
Self {
|
||||
key,
|
||||
ctrl,
|
||||
alt,
|
||||
shift,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
438
src/textarea.rs
438
src/textarea.rs
@@ -1,3 +1,5 @@
|
||||
use std::cell::Cell;
|
||||
|
||||
use crate::cursor::CursorMove;
|
||||
use crate::highlight::LineHighlighter;
|
||||
use crate::history::{Edit, EditKind, History};
|
||||
@@ -55,7 +57,7 @@ impl ToString for YankText {
|
||||
/// let mut textarea = TextArea::default();
|
||||
///
|
||||
/// // 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);
|
||||
///
|
||||
/// // Get widget to render.
|
||||
@@ -84,6 +86,11 @@ pub struct TextArea<'a> {
|
||||
pub(crate) placeholder: String,
|
||||
pub(crate) placeholder_style: Style,
|
||||
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
|
||||
@@ -184,6 +191,9 @@ impl<'a> TextArea<'a> {
|
||||
placeholder: String::new(),
|
||||
placeholder_style: Style::default().fg(Color::DarkGray),
|
||||
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 {
|
||||
let input = input.into();
|
||||
self.should_select(&input);
|
||||
let modified = match input {
|
||||
Input {
|
||||
key: Key::Char('m'),
|
||||
ctrl: true,
|
||||
alt: false,
|
||||
..
|
||||
}
|
||||
| Input {
|
||||
key: Key::Char('\n' | '\r'),
|
||||
ctrl: false,
|
||||
alt: false,
|
||||
..
|
||||
}
|
||||
| Input {
|
||||
key: Key::Enter, ..
|
||||
@@ -246,6 +259,7 @@ impl<'a> TextArea<'a> {
|
||||
key: Key::Char(c),
|
||||
ctrl: false,
|
||||
alt: false,
|
||||
..
|
||||
} => {
|
||||
self.insert_char(c);
|
||||
true
|
||||
@@ -254,124 +268,146 @@ impl<'a> TextArea<'a> {
|
||||
key: Key::Tab,
|
||||
ctrl: false,
|
||||
alt: false,
|
||||
..
|
||||
} => self.insert_tab(),
|
||||
Input {
|
||||
key: Key::Char('h'),
|
||||
ctrl: true,
|
||||
alt: false,
|
||||
..
|
||||
}
|
||||
| Input {
|
||||
key: Key::Backspace,
|
||||
ctrl: false,
|
||||
alt: false,
|
||||
..
|
||||
} => self.delete_char(),
|
||||
Input {
|
||||
key: Key::Char('d'),
|
||||
ctrl: true,
|
||||
alt: false,
|
||||
..
|
||||
}
|
||||
| Input {
|
||||
key: Key::Delete,
|
||||
ctrl: false,
|
||||
alt: false,
|
||||
..
|
||||
} => self.delete_next_char(),
|
||||
Input {
|
||||
key: Key::Char('k'),
|
||||
ctrl: true,
|
||||
alt: false,
|
||||
..
|
||||
} => self.delete_line_by_end(),
|
||||
Input {
|
||||
key: Key::Char('j'),
|
||||
ctrl: true,
|
||||
alt: false,
|
||||
..
|
||||
} => self.delete_line_by_head(),
|
||||
Input {
|
||||
key: Key::Char('w'),
|
||||
ctrl: true,
|
||||
alt: false,
|
||||
..
|
||||
}
|
||||
| Input {
|
||||
key: Key::Char('h'),
|
||||
ctrl: false,
|
||||
alt: true,
|
||||
..
|
||||
}
|
||||
| Input {
|
||||
key: Key::Backspace,
|
||||
ctrl: false,
|
||||
alt: true,
|
||||
..
|
||||
} => self.delete_word(),
|
||||
Input {
|
||||
key: Key::Delete,
|
||||
ctrl: false,
|
||||
alt: true,
|
||||
..
|
||||
}
|
||||
| Input {
|
||||
key: Key::Char('d'),
|
||||
ctrl: false,
|
||||
alt: true,
|
||||
..
|
||||
} => self.delete_next_word(),
|
||||
Input {
|
||||
key: Key::Char('n'),
|
||||
key: Key::Char('n' | 'N'),
|
||||
ctrl: true,
|
||||
alt: false,
|
||||
..
|
||||
}
|
||||
| Input {
|
||||
key: Key::Down,
|
||||
ctrl: false,
|
||||
alt: false,
|
||||
..
|
||||
} => {
|
||||
self.move_cursor(CursorMove::Down);
|
||||
false
|
||||
}
|
||||
Input {
|
||||
key: Key::Char('p'),
|
||||
key: Key::Char('p' | 'P'),
|
||||
ctrl: true,
|
||||
alt: false,
|
||||
..
|
||||
}
|
||||
| Input {
|
||||
key: Key::Up,
|
||||
ctrl: false,
|
||||
alt: false,
|
||||
..
|
||||
} => {
|
||||
self.move_cursor(CursorMove::Up);
|
||||
false
|
||||
}
|
||||
Input {
|
||||
key: Key::Char('f'),
|
||||
key: Key::Char('f' | 'F'),
|
||||
ctrl: true,
|
||||
alt: false,
|
||||
..
|
||||
}
|
||||
| Input {
|
||||
key: Key::Right,
|
||||
ctrl: false,
|
||||
alt: false,
|
||||
..
|
||||
} => {
|
||||
self.move_cursor(CursorMove::Forward);
|
||||
false
|
||||
}
|
||||
Input {
|
||||
key: Key::Char('b'),
|
||||
key: Key::Char('b' | 'B'),
|
||||
ctrl: true,
|
||||
alt: false,
|
||||
..
|
||||
}
|
||||
| Input {
|
||||
key: Key::Left,
|
||||
ctrl: false,
|
||||
alt: false,
|
||||
..
|
||||
} => {
|
||||
self.move_cursor(CursorMove::Back);
|
||||
false
|
||||
}
|
||||
Input {
|
||||
key: Key::Char('a'),
|
||||
key: Key::Char('a' | 'A'),
|
||||
ctrl: true,
|
||||
alt: false,
|
||||
..
|
||||
}
|
||||
| Input { key: Key::Home, .. }
|
||||
| Input {
|
||||
key: Key::Left | Key::Char('b'),
|
||||
key: Key::Left | Key::Char('b' | 'B'),
|
||||
ctrl: true,
|
||||
alt: true,
|
||||
..
|
||||
} => {
|
||||
self.move_cursor(CursorMove::Head);
|
||||
false
|
||||
@@ -380,12 +416,14 @@ impl<'a> TextArea<'a> {
|
||||
key: Key::Char('e'),
|
||||
ctrl: true,
|
||||
alt: false,
|
||||
..
|
||||
}
|
||||
| Input { key: Key::End, .. }
|
||||
| Input {
|
||||
key: Key::Right | Key::Char('f'),
|
||||
key: Key::Right | Key::Char('f' | 'F'),
|
||||
ctrl: true,
|
||||
alt: true,
|
||||
..
|
||||
} => {
|
||||
self.move_cursor(CursorMove::End);
|
||||
false
|
||||
@@ -394,11 +432,13 @@ impl<'a> TextArea<'a> {
|
||||
key: Key::Char('<'),
|
||||
ctrl: false,
|
||||
alt: true,
|
||||
..
|
||||
}
|
||||
| Input {
|
||||
key: Key::Up | Key::Char('p'),
|
||||
key: Key::Up | Key::Char('p' | 'P'),
|
||||
ctrl: true,
|
||||
alt: true,
|
||||
..
|
||||
} => {
|
||||
self.move_cursor(CursorMove::Top);
|
||||
false
|
||||
@@ -407,37 +447,43 @@ impl<'a> TextArea<'a> {
|
||||
key: Key::Char('>'),
|
||||
ctrl: false,
|
||||
alt: true,
|
||||
..
|
||||
}
|
||||
| Input {
|
||||
key: Key::Down | Key::Char('n'),
|
||||
key: Key::Down | Key::Char('n' | 'N'),
|
||||
ctrl: true,
|
||||
alt: true,
|
||||
..
|
||||
} => {
|
||||
self.move_cursor(CursorMove::Bottom);
|
||||
false
|
||||
}
|
||||
Input {
|
||||
key: Key::Char('f'),
|
||||
key: Key::Char('f' | 'F'),
|
||||
ctrl: false,
|
||||
alt: true,
|
||||
..
|
||||
}
|
||||
| Input {
|
||||
key: Key::Right,
|
||||
ctrl: true,
|
||||
alt: false,
|
||||
..
|
||||
} => {
|
||||
self.move_cursor(CursorMove::WordForward);
|
||||
false
|
||||
}
|
||||
Input {
|
||||
key: Key::Char('b'),
|
||||
key: Key::Char('b' | 'B'),
|
||||
ctrl: false,
|
||||
alt: true,
|
||||
..
|
||||
}
|
||||
| Input {
|
||||
key: Key::Left,
|
||||
ctrl: true,
|
||||
alt: false,
|
||||
..
|
||||
} => {
|
||||
self.move_cursor(CursorMove::WordBack);
|
||||
false
|
||||
@@ -446,16 +492,19 @@ impl<'a> TextArea<'a> {
|
||||
key: Key::Char(']'),
|
||||
ctrl: false,
|
||||
alt: true,
|
||||
..
|
||||
}
|
||||
| Input {
|
||||
key: Key::Char('n'),
|
||||
key: Key::Char('n' | 'N'),
|
||||
ctrl: false,
|
||||
alt: true,
|
||||
..
|
||||
}
|
||||
| Input {
|
||||
key: Key::Down,
|
||||
ctrl: true,
|
||||
alt: false,
|
||||
..
|
||||
} => {
|
||||
self.move_cursor(CursorMove::ParagraphForward);
|
||||
false
|
||||
@@ -464,16 +513,19 @@ impl<'a> TextArea<'a> {
|
||||
key: Key::Char('['),
|
||||
ctrl: false,
|
||||
alt: true,
|
||||
..
|
||||
}
|
||||
| Input {
|
||||
key: Key::Char('p'),
|
||||
key: Key::Char('p' | 'P'),
|
||||
ctrl: false,
|
||||
alt: true,
|
||||
..
|
||||
}
|
||||
| Input {
|
||||
key: Key::Up,
|
||||
ctrl: true,
|
||||
alt: false,
|
||||
..
|
||||
} => {
|
||||
self.move_cursor(CursorMove::ParagraphBack);
|
||||
false
|
||||
@@ -482,21 +534,43 @@ impl<'a> TextArea<'a> {
|
||||
key: Key::Char('u'),
|
||||
ctrl: true,
|
||||
alt: false,
|
||||
..
|
||||
} => self.undo(),
|
||||
Input {
|
||||
key: Key::Char('r'),
|
||||
ctrl: true,
|
||||
alt: false,
|
||||
..
|
||||
} => self.redo(),
|
||||
Input {
|
||||
key: Key::Char('y'),
|
||||
ctrl: true,
|
||||
alt: false,
|
||||
..
|
||||
} => self.paste(),
|
||||
Input {
|
||||
key: Key::Char('v'),
|
||||
key: Key::Char('c'),
|
||||
ctrl: true,
|
||||
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 {
|
||||
key: Key::PageDown, ..
|
||||
@@ -505,9 +579,10 @@ impl<'a> TextArea<'a> {
|
||||
false
|
||||
}
|
||||
Input {
|
||||
key: Key::Char('v'),
|
||||
key: Key::Char('v' | 'V'),
|
||||
ctrl: false,
|
||||
alt: true,
|
||||
..
|
||||
}
|
||||
| Input {
|
||||
key: Key::PageUp, ..
|
||||
@@ -531,7 +606,6 @@ impl<'a> TextArea<'a> {
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
|
||||
// Check invariants
|
||||
debug_assert!(!self.lines.is_empty(), "no line after {:?}", input);
|
||||
let (r, c) = self.cursor;
|
||||
@@ -550,7 +624,7 @@ impl<'a> TextArea<'a> {
|
||||
self.lines[r],
|
||||
input,
|
||||
);
|
||||
|
||||
self.should_end_selection();
|
||||
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.
|
||||
/// 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 {
|
||||
match input.into() {
|
||||
let input = input.into();
|
||||
self.should_select(&input);
|
||||
let modified = match input {
|
||||
Input {
|
||||
key: Key::Char(c),
|
||||
ctrl: false,
|
||||
alt: false,
|
||||
..
|
||||
} => {
|
||||
self.insert_char(c);
|
||||
true
|
||||
@@ -580,6 +657,7 @@ impl<'a> TextArea<'a> {
|
||||
key: Key::Tab,
|
||||
ctrl: false,
|
||||
alt: false,
|
||||
..
|
||||
} => self.insert_tab(),
|
||||
Input {
|
||||
key: Key::Backspace,
|
||||
@@ -609,7 +687,9 @@ impl<'a> TextArea<'a> {
|
||||
false
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
};
|
||||
self.should_end_selection();
|
||||
modified
|
||||
}
|
||||
|
||||
fn push_history(&mut self, kind: EditKind, cursor_before: (usize, usize)) {
|
||||
@@ -627,6 +707,8 @@ impl<'a> TextArea<'a> {
|
||||
/// assert_eq!(textarea.lines(), ["a"]);
|
||||
/// ```
|
||||
pub fn insert_char(&mut self, c: char) {
|
||||
self.delete_selection(false);
|
||||
|
||||
let (row, col) = self.cursor;
|
||||
let line = &mut self.lines[row];
|
||||
let i = line
|
||||
@@ -652,6 +734,7 @@ impl<'a> TextArea<'a> {
|
||||
/// assert_eq!(textarea.lines(), ["hello, world", "goodbye, world"]);
|
||||
/// ```
|
||||
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();
|
||||
match lines.len() {
|
||||
0 => false,
|
||||
@@ -727,6 +810,14 @@ impl<'a> TextArea<'a> {
|
||||
/// assert_eq!(textarea.lines(), ["🐱", "🐮"]);
|
||||
/// ```
|
||||
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 {
|
||||
return false;
|
||||
}
|
||||
@@ -759,14 +850,22 @@ impl<'a> TextArea<'a> {
|
||||
|
||||
// First line
|
||||
if let Some(offset) = find_end(&line[first_start..]) {
|
||||
let removed = self.lines[row]
|
||||
.drain(first_start..first_start + offset)
|
||||
.as_str()
|
||||
.to_string();
|
||||
self.yank = removed.clone().into();
|
||||
self.push_history(EditKind::DeleteStr(removed, first_start), self.cursor);
|
||||
let removed = if delete {
|
||||
self.lines[row]
|
||||
.drain(first_start..first_start + offset)
|
||||
.as_str()
|
||||
.to_string()
|
||||
} else {
|
||||
self.lines[row][first_start..first_start + offset].to_string()
|
||||
};
|
||||
if yank {
|
||||
self.yank = removed.clone().into();
|
||||
};
|
||||
if delete {
|
||||
self.push_history(EditKind::DeleteStr(removed, first_start), self.cursor);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
let mut r = row + 1;
|
||||
let mut i = 0;
|
||||
@@ -780,23 +879,41 @@ impl<'a> TextArea<'a> {
|
||||
r += 1;
|
||||
}
|
||||
|
||||
let mut deleted = vec![self.lines[row].drain(first_start..).as_str().to_string()];
|
||||
deleted.extend(self.lines.drain(row + 1..r));
|
||||
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));
|
||||
} else {
|
||||
deleted.extend(self.lines[row + 1..r].iter().map(|s| s.to_string()));
|
||||
}
|
||||
|
||||
if row + 1 < self.lines.len() {
|
||||
let mut last_line = self.lines.remove(row + 1);
|
||||
self.lines[row].push_str(&last_line[i..]);
|
||||
let mut last_line = if delete {
|
||||
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);
|
||||
deleted.push(last_line);
|
||||
}
|
||||
|
||||
self.yank = YankText::Chunk(deleted.clone());
|
||||
|
||||
let edit = if deleted.len() == 1 {
|
||||
EditKind::DeleteStr(deleted.remove(0), first_start)
|
||||
} else {
|
||||
EditKind::DeleteChunk(deleted, row, first_start)
|
||||
if yank {
|
||||
self.yank = YankText::Chunk(deleted.clone());
|
||||
};
|
||||
self.push_history(edit, self.cursor);
|
||||
if delete {
|
||||
let edit = if deleted.len() == 1 {
|
||||
EditKind::DeleteStr(deleted.remove(0), first_start)
|
||||
} else {
|
||||
EditKind::DeleteChunk(deleted, row, first_start)
|
||||
};
|
||||
self.push_history(edit, self.cursor);
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
@@ -871,6 +988,7 @@ impl<'a> TextArea<'a> {
|
||||
/// assert_eq!(textarea.lines(), ["h", "i"]);
|
||||
/// ```
|
||||
pub fn insert_newline(&mut self) {
|
||||
self.delete_selection(false);
|
||||
let (row, col) = self.cursor;
|
||||
let line = &mut self.lines[row];
|
||||
let idx = line
|
||||
@@ -925,6 +1043,9 @@ impl<'a> TextArea<'a> {
|
||||
/// assert_eq!(textarea.lines(), ["bc"]);
|
||||
/// ```
|
||||
pub fn delete_char(&mut self) -> bool {
|
||||
if self.select_start.get().is_some() {
|
||||
return self.delete_selection(false);
|
||||
}
|
||||
let (row, col) = self.cursor;
|
||||
if col == 0 {
|
||||
return self.delete_newline();
|
||||
@@ -953,6 +1074,9 @@ impl<'a> TextArea<'a> {
|
||||
/// assert_eq!(textarea.lines(), ["ac"]);
|
||||
/// ```
|
||||
pub fn delete_next_char(&mut self) -> bool {
|
||||
if self.select_start.get().is_some() {
|
||||
return self.delete_selection(false);
|
||||
}
|
||||
let before = self.cursor;
|
||||
self.move_cursor(CursorMove::Forward);
|
||||
if before == self.cursor {
|
||||
@@ -1080,6 +1204,7 @@ impl<'a> TextArea<'a> {
|
||||
/// assert_eq!(textarea.lines(), [" bbb cccaaa"]);
|
||||
/// ```
|
||||
pub fn paste(&mut self) -> bool {
|
||||
self.delete_selection(false);
|
||||
match self.yank.clone() {
|
||||
YankText::Piece(s) => self.insert_piece(s),
|
||||
YankText::Chunk(c) => self.insert_chunk(c),
|
||||
@@ -1097,7 +1222,7 @@ impl<'a> TextArea<'a> {
|
||||
/// assert_eq!(textarea.cursor(), (0, 1));
|
||||
/// textarea.move_cursor(CursorMove::Down);
|
||||
/// assert_eq!(textarea.cursor(), (1, 1));
|
||||
/// ```
|
||||
|
||||
pub fn move_cursor(&mut self, m: CursorMove) {
|
||||
if let Some(cursor) = m.next_cursor(self.cursor, &self.lines, &self.viewport) {
|
||||
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> {
|
||||
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 {
|
||||
hl.line_number(row, lnum_len, style);
|
||||
@@ -1162,6 +1293,12 @@ impl<'a> TextArea<'a> {
|
||||
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()
|
||||
}
|
||||
|
||||
@@ -1192,6 +1329,7 @@ impl<'a> TextArea<'a> {
|
||||
/// }
|
||||
/// ```
|
||||
pub fn widget(&'a self) -> impl Widget + 'a {
|
||||
self.should_end_selection();
|
||||
Renderer::new(self)
|
||||
}
|
||||
|
||||
@@ -1253,7 +1391,7 @@ impl<'a> TextArea<'a> {
|
||||
/// use tui_textarea::{TextArea, Input, Key};
|
||||
///
|
||||
/// 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.input(tab_input.clone());
|
||||
@@ -1879,10 +2017,226 @@ impl<'a> TextArea<'a> {
|
||||
scrolling.into().scroll(&mut self.viewport);
|
||||
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)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// 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);
|
||||
}
|
||||
|
||||
Ссылка в новой задаче
Block a user