зеркало из
https://github.com/glebtv/tui-textarea.git
synced 2026-09-04 14:55:51 +03:00
refactor text selection and add more tests (fix #6)
Этот коммит содержится в:
@@ -15,6 +15,7 @@ Multi-line text editor can be easily put as part of your TUI application.
|
|||||||
- Line number
|
- Line number
|
||||||
- Cursor line highlight
|
- Cursor line highlight
|
||||||
- Search with regular expressions
|
- Search with regular expressions
|
||||||
|
- Text selection
|
||||||
- Mouse scrolling
|
- Mouse scrolling
|
||||||
- Yank support. Paste text deleted with `C-k`, `C-j`, ...
|
- Yank support. Paste text deleted with `C-k`, `C-j`, ...
|
||||||
- Backend agnostic. [crossterm][], [termion][], [termwiz][], and your own backend are all supported
|
- Backend agnostic. [crossterm][], [termion][], [termwiz][], and your own backend are all supported
|
||||||
@@ -250,6 +251,8 @@ Default key mappings are as follows:
|
|||||||
| `Alt+D`, `Alt+Delete` | Delete one word next to cursor |
|
| `Alt+D`, `Alt+Delete` | Delete one word next to cursor |
|
||||||
| `Ctrl+U` | Undo |
|
| `Ctrl+U` | Undo |
|
||||||
| `Ctrl+R` | Redo |
|
| `Ctrl+R` | Redo |
|
||||||
|
| `Ctrl+C` | Copy selected text |
|
||||||
|
| `Ctrl+X` | Cut selected text |
|
||||||
| `Ctrl+Y` | Paste yanked text |
|
| `Ctrl+Y` | Paste yanked text |
|
||||||
| `Ctrl+F`, `→` | Move cursor forward by one character |
|
| `Ctrl+F`, `→` | Move cursor forward by one character |
|
||||||
| `Ctrl+B`, `←` | Move cursor backward by one character |
|
| `Ctrl+B`, `←` | Move cursor backward by one character |
|
||||||
@@ -480,7 +483,11 @@ notify how to move the cursor.
|
|||||||
| `textarea.delete_next_word()` | Delete one word next to cursor |
|
| `textarea.delete_next_word()` | Delete one word next to cursor |
|
||||||
| `textarea.undo()` | Undo |
|
| `textarea.undo()` | Undo |
|
||||||
| `textarea.redo()` | Redo |
|
| `textarea.redo()` | Redo |
|
||||||
|
| `textarea.copy()` | Copy selected text |
|
||||||
|
| `textarea.cut()` | Cut selected text |
|
||||||
| `textarea.paste()` | Paste yanked text |
|
| `textarea.paste()` | Paste yanked text |
|
||||||
|
| `textarea.start_selection()` | Start text selection |
|
||||||
|
| `textarea.cancel_selection()` | Cancel text selection |
|
||||||
| `textarea.move_cursor(CursorMove::Forward)` | Move cursor forward by one character |
|
| `textarea.move_cursor(CursorMove::Forward)` | Move cursor forward by one character |
|
||||||
| `textarea.move_cursor(CursorMove::Back)` | Move cursor backward by one character |
|
| `textarea.move_cursor(CursorMove::Back)` | Move cursor backward by one character |
|
||||||
| `textarea.move_cursor(CursorMove::Up)` | Move cursor up by one line |
|
| `textarea.move_cursor(CursorMove::Up)` | Move cursor up by one line |
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ impl<'a> Editor<'a> {
|
|||||||
Span::raw(" to save, "),
|
Span::raw(" to save, "),
|
||||||
Span::styled("^G", Style::default().add_modifier(Modifier::BOLD)),
|
Span::styled("^G", Style::default().add_modifier(Modifier::BOLD)),
|
||||||
Span::raw(" to search, "),
|
Span::raw(" to search, "),
|
||||||
Span::styled("^X", Style::default().add_modifier(Modifier::BOLD)),
|
Span::styled("^T", Style::default().add_modifier(Modifier::BOLD)),
|
||||||
Span::raw(" to switch buffer"),
|
Span::raw(" to switch buffer"),
|
||||||
])
|
])
|
||||||
};
|
};
|
||||||
@@ -314,7 +314,7 @@ impl<'a> Editor<'a> {
|
|||||||
..
|
..
|
||||||
} => break,
|
} => break,
|
||||||
Input {
|
Input {
|
||||||
key: Key::Char('x'),
|
key: Key::Char('t'),
|
||||||
ctrl: true,
|
ctrl: true,
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
|
|||||||
@@ -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,41 +75,32 @@ 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: Input = crossterm::event::read()?.into();
|
let 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' | 'H'),
|
key: Key::Char('h'),
|
||||||
..
|
..
|
||||||
} => textarea.move_cursor(CursorMove::Back),
|
} => textarea.move_cursor(CursorMove::Back),
|
||||||
Input {
|
Input {
|
||||||
key: Key::Char('j' | 'J'),
|
key: Key::Char('j'),
|
||||||
..
|
..
|
||||||
} => textarea.move_cursor(CursorMove::Down),
|
} => textarea.move_cursor(CursorMove::Down),
|
||||||
Input {
|
Input {
|
||||||
key: Key::Char('k' | 'K'),
|
key: Key::Char('k'),
|
||||||
..
|
..
|
||||||
} => textarea.move_cursor(CursorMove::Up),
|
} => textarea.move_cursor(CursorMove::Up),
|
||||||
Input {
|
Input {
|
||||||
key: Key::Char('l' | 'L'),
|
key: Key::Char('l'),
|
||||||
..
|
..
|
||||||
} => textarea.move_cursor(CursorMove::Forward),
|
} => textarea.move_cursor(CursorMove::Forward),
|
||||||
Input {
|
Input {
|
||||||
key: Key::Char('w' | 'W'),
|
key: Key::Char('w'),
|
||||||
..
|
..
|
||||||
} => textarea.move_cursor(CursorMove::WordForward),
|
} => textarea.move_cursor(CursorMove::WordForward),
|
||||||
Input {
|
Input {
|
||||||
key: Key::Char('b' | 'B'),
|
key: Key::Char('b'),
|
||||||
ctrl: false,
|
ctrl: false,
|
||||||
..
|
..
|
||||||
} => textarea.move_cursor(CursorMove::WordBack),
|
} => textarea.move_cursor(CursorMove::WordBack),
|
||||||
@@ -140,13 +131,6 @@ 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,
|
||||||
@@ -182,8 +166,6 @@ 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;
|
||||||
}
|
}
|
||||||
@@ -199,8 +181,6 @@ 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);
|
||||||
@@ -210,7 +190,6 @@ 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;
|
||||||
}
|
}
|
||||||
@@ -248,6 +227,33 @@ fn main() -> io::Result<()> {
|
|||||||
ctrl: true,
|
ctrl: true,
|
||||||
..
|
..
|
||||||
} => textarea.scroll(Scrolling::PageUp),
|
} => textarea.scroll(Scrolling::PageUp),
|
||||||
|
Input {
|
||||||
|
key: Key::Char('v'),
|
||||||
|
ctrl: false,
|
||||||
|
..
|
||||||
|
} => textarea.start_selection(),
|
||||||
|
Input {
|
||||||
|
key: Key::Char('V'),
|
||||||
|
ctrl: false,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
textarea.move_cursor(CursorMove::Head);
|
||||||
|
textarea.start_selection();
|
||||||
|
textarea.move_cursor(CursorMove::End);
|
||||||
|
}
|
||||||
|
Input { key: Key::Esc, .. } => textarea.cancel_selection(),
|
||||||
|
Input {
|
||||||
|
key: Key::Char('y'),
|
||||||
|
ctrl: false,
|
||||||
|
..
|
||||||
|
} if textarea.is_selecting() => textarea.copy(),
|
||||||
|
Input {
|
||||||
|
key: Key::Char('d'),
|
||||||
|
ctrl: false,
|
||||||
|
..
|
||||||
|
} if textarea.is_selecting() => {
|
||||||
|
textarea.cut();
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
Mode::Insert => match input {
|
Mode::Insert => match input {
|
||||||
|
|||||||
@@ -250,7 +250,7 @@ impl<'a> Editor<'a> {
|
|||||||
Span::raw(" to save, "),
|
Span::raw(" to save, "),
|
||||||
Span::styled("^G", Style::default().add_modifier(Modifier::BOLD)),
|
Span::styled("^G", Style::default().add_modifier(Modifier::BOLD)),
|
||||||
Span::raw(" to search, "),
|
Span::raw(" to search, "),
|
||||||
Span::styled("^X", Style::default().add_modifier(Modifier::BOLD)),
|
Span::styled("^T", Style::default().add_modifier(Modifier::BOLD)),
|
||||||
Span::raw(" to switch buffer"),
|
Span::raw(" to switch buffer"),
|
||||||
])
|
])
|
||||||
};
|
};
|
||||||
@@ -316,7 +316,7 @@ impl<'a> Editor<'a> {
|
|||||||
..
|
..
|
||||||
} => break,
|
} => break,
|
||||||
Input {
|
Input {
|
||||||
key: Key::Char('x'),
|
key: Key::Char('t'),
|
||||||
ctrl: true,
|
ctrl: true,
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
|
|||||||
386
src/highlight.rs
386
src/highlight.rs
@@ -10,12 +10,11 @@ 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),
|
||||||
|
Select(Style),
|
||||||
#[cfg(feature = "search")]
|
#[cfg(feature = "search")]
|
||||||
Search(Style),
|
Search(Style),
|
||||||
Select(Style),
|
|
||||||
End,
|
End,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,8 +24,8 @@ impl Boundary {
|
|||||||
match b {
|
match b {
|
||||||
Boundary::Cursor(_) => 3,
|
Boundary::Cursor(_) => 3,
|
||||||
#[cfg(feature = "search")]
|
#[cfg(feature = "search")]
|
||||||
Boundary::Search(_) => 1,
|
Boundary::Search(_) => 2,
|
||||||
Boundary::Select(_) => 2,
|
Boundary::Select(_) => 1,
|
||||||
Boundary::End => 0,
|
Boundary::End => 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -36,10 +35,10 @@ impl Boundary {
|
|||||||
fn style(&self) -> Option<Style> {
|
fn style(&self) -> Option<Style> {
|
||||||
match self {
|
match self {
|
||||||
Boundary::Cursor(s) => Some(*s),
|
Boundary::Cursor(s) => Some(*s),
|
||||||
|
Boundary::Select(s) => Some(*s),
|
||||||
#[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),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -98,13 +97,13 @@ 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_at_end: bool,
|
||||||
select_style: Style,
|
select_style: Style,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,6 +124,7 @@ impl<'a> LineHighlighter<'a> {
|
|||||||
cursor_style,
|
cursor_style,
|
||||||
tab_len,
|
tab_len,
|
||||||
mask,
|
mask,
|
||||||
|
select_at_end: false,
|
||||||
select_style,
|
select_style,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -135,11 +135,6 @@ 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
|
||||||
@@ -151,60 +146,6 @@ 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 {
|
||||||
@@ -215,6 +156,36 @@ impl<'a> LineHighlighter<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn selection(
|
||||||
|
&mut self,
|
||||||
|
row: usize,
|
||||||
|
start_row: usize,
|
||||||
|
start_off: usize,
|
||||||
|
end_row: usize,
|
||||||
|
end_off: usize,
|
||||||
|
) {
|
||||||
|
let (start, end) = if row == start_row {
|
||||||
|
if start_row == end_row {
|
||||||
|
(start_off, end_off)
|
||||||
|
} else {
|
||||||
|
self.select_at_end = true;
|
||||||
|
(start_off, self.line.len())
|
||||||
|
}
|
||||||
|
} else if row == end_row {
|
||||||
|
(0, end_off)
|
||||||
|
} else if start_row < row && row < end_row {
|
||||||
|
self.select_at_end = true;
|
||||||
|
(0, self.line.len())
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if start != end {
|
||||||
|
self.boundaries
|
||||||
|
.push((Boundary::Select(self.select_style), start));
|
||||||
|
self.boundaries.push((Boundary::End, end));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn into_spans(self) -> Line<'a> {
|
pub fn into_spans(self) -> Line<'a> {
|
||||||
let Self {
|
let Self {
|
||||||
line,
|
line,
|
||||||
@@ -225,14 +196,20 @@ impl<'a> LineHighlighter<'a> {
|
|||||||
cursor_style,
|
cursor_style,
|
||||||
cursor_at_end,
|
cursor_at_end,
|
||||||
mask,
|
mask,
|
||||||
|
select_at_end,
|
||||||
select_style,
|
select_style,
|
||||||
} = self;
|
} = self;
|
||||||
let mut builder = DisplayTextBuilder::new(tab_len, mask);
|
let mut builder = DisplayTextBuilder::new(tab_len, mask);
|
||||||
|
|
||||||
if boundaries.is_empty() {
|
if boundaries.is_empty() {
|
||||||
spans.push(Span::styled(builder.build(line), style_begin));
|
let built = builder.build(line);
|
||||||
|
if !built.is_empty() {
|
||||||
|
spans.push(Span::styled(built, style_begin));
|
||||||
|
}
|
||||||
if cursor_at_end {
|
if cursor_at_end {
|
||||||
spans.push(Span::styled(" ", cursor_style));
|
spans.push(Span::styled(" ", cursor_style));
|
||||||
|
} else if select_at_end {
|
||||||
|
spans.push(Span::styled(" ", select_style));
|
||||||
}
|
}
|
||||||
return Line::from(spans);
|
return Line::from(spans);
|
||||||
}
|
}
|
||||||
@@ -245,18 +222,10 @@ 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
|
spans.push(Span::styled(builder.build(&line[start..end]), style));
|
||||||
// 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() {
|
style = if let Some(s) = next_boundary.style() {
|
||||||
@@ -267,20 +236,27 @@ 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 && !dont_add_cursor {
|
|
||||||
|
if cursor_at_end {
|
||||||
spans.push(Span::styled(" ", cursor_style));
|
spans.push(Span::styled(" ", cursor_style));
|
||||||
|
} else if select_at_end {
|
||||||
|
spans.push(Span::styled(" ", select_style));
|
||||||
}
|
}
|
||||||
|
|
||||||
Line::from(spans)
|
Line::from(spans)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
// Tests for spans don't work with tui-rs
|
||||||
|
#[cfg(all(test, feature = "ratatui"))]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::ratatui::style::Color;
|
||||||
|
use std::fmt::Debug;
|
||||||
use unicode_width::UnicodeWidthStr as _;
|
use unicode_width::UnicodeWidthStr as _;
|
||||||
|
|
||||||
fn build(text: &'static str, tab: u8, mask: Option<char>) -> Cow<'static, str> {
|
fn build(text: &'static str, tab: u8, mask: Option<char>) -> Cow<'static, str> {
|
||||||
@@ -374,5 +350,255 @@ mod tests {
|
|||||||
assert_eq!(&build_with_offset(2, "あ\tあ\t", 4), "あ あ ");
|
assert_eq!(&build_with_offset(2, "あ\tあ\t", 4), "あ あ ");
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Add tests for LineHighlighter
|
fn assert_spans<T: Debug>(lh: LineHighlighter, want: &[(&str, Style)], context: T) {
|
||||||
|
let line = lh.into_spans();
|
||||||
|
let have = line
|
||||||
|
.spans
|
||||||
|
.iter()
|
||||||
|
.map(|s| (s.content.as_ref(), s.style))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
assert_eq!(&have, want, "Test case: {context:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT: Style = Style::new();
|
||||||
|
const CUR: Style = Style::new().bg(Color::Red); // Cursor
|
||||||
|
#[allow(unused)]
|
||||||
|
const SEARCH: Style = Style::new().bg(Color::Green);
|
||||||
|
const SEL: Style = Style::new().bg(Color::Blue);
|
||||||
|
const LINE: Style = Style::new().bg(Color::Gray);
|
||||||
|
const LNUM: Style = Style::new().bg(Color::Yellow);
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn into_spans_normal_line() {
|
||||||
|
let tests = [
|
||||||
|
("", &[][..]),
|
||||||
|
("abc", &[("abc", DEFAULT)][..]),
|
||||||
|
("a\tb\tc", &[("a b c", DEFAULT)][..]),
|
||||||
|
];
|
||||||
|
for test in tests {
|
||||||
|
let (line, want) = test;
|
||||||
|
let lh = LineHighlighter::new(line, CUR, 4, None, SEL);
|
||||||
|
assert_spans(lh, want, test);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn into_spans_cursor_line() {
|
||||||
|
let tests = [
|
||||||
|
("", 0, &[(" ", CUR)][..]),
|
||||||
|
("a", 0, &[("a", CUR)][..]),
|
||||||
|
("a", 1, &[("a", LINE), (" ", CUR)][..]),
|
||||||
|
("あいう", 0, &[("あ", CUR), ("いう", LINE)][..]),
|
||||||
|
("あいう", 1, &[("あ", LINE), ("い", CUR), ("う", LINE)][..]),
|
||||||
|
("あいう", 2, &[("あい", LINE), ("う", CUR)][..]),
|
||||||
|
("a\tb", 1, &[("a", LINE), (" ", CUR), ("b", LINE)][..]),
|
||||||
|
];
|
||||||
|
|
||||||
|
for test in tests {
|
||||||
|
let (line, col, want) = test;
|
||||||
|
let mut lh = LineHighlighter::new(line, CUR, 4, None, SEL);
|
||||||
|
lh.cursor_line(col, LINE);
|
||||||
|
assert_spans(lh, want, test);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn into_spans_line_number() {
|
||||||
|
let tests = [
|
||||||
|
(0, 1, &[(" 1 ", LNUM)][..]),
|
||||||
|
(123, 3, &[(" 124 ", LNUM)][..]),
|
||||||
|
(123, 5, &[(" 124 ", LNUM)][..]),
|
||||||
|
];
|
||||||
|
for test in tests {
|
||||||
|
let (row, len, want) = test;
|
||||||
|
let mut lh = LineHighlighter::new("", CUR, 4, None, SEL);
|
||||||
|
lh.line_number(row, len, LNUM);
|
||||||
|
assert_spans(lh, want, test);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "search")]
|
||||||
|
#[test]
|
||||||
|
fn into_spans_search() {
|
||||||
|
let tests = [
|
||||||
|
("abcde", &[(0, 5)][..], &[("abcde", SEARCH)][..]),
|
||||||
|
(
|
||||||
|
"abcde",
|
||||||
|
&[(0, 1), (2, 3), (4, 5)][..],
|
||||||
|
&[
|
||||||
|
("a", SEARCH),
|
||||||
|
("b", DEFAULT),
|
||||||
|
("c", SEARCH),
|
||||||
|
("d", DEFAULT),
|
||||||
|
("e", SEARCH),
|
||||||
|
][..],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"abcde",
|
||||||
|
&[(1, 2), (3, 4)][..],
|
||||||
|
&[
|
||||||
|
("a", DEFAULT),
|
||||||
|
("b", SEARCH),
|
||||||
|
("c", DEFAULT),
|
||||||
|
("d", SEARCH),
|
||||||
|
("e", DEFAULT),
|
||||||
|
][..],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"abcde",
|
||||||
|
&[(0, 2), (2, 4), (4, 5)][..],
|
||||||
|
&[("ab", SEARCH), ("cd", SEARCH), ("e", SEARCH)][..],
|
||||||
|
),
|
||||||
|
("abcde", &[(1, 1)][..], &[("abcde", DEFAULT)][..]),
|
||||||
|
(
|
||||||
|
"あいうえお",
|
||||||
|
&[(0, 3), (6, 9), (12, 15)][..],
|
||||||
|
&[
|
||||||
|
("あ", SEARCH),
|
||||||
|
("い", DEFAULT),
|
||||||
|
("う", SEARCH),
|
||||||
|
("え", DEFAULT),
|
||||||
|
("お", SEARCH),
|
||||||
|
][..],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"\ta\tb\t",
|
||||||
|
&[(0, 1), (2, 3), (3, 4)][..],
|
||||||
|
&[
|
||||||
|
(" ", SEARCH),
|
||||||
|
("a", DEFAULT),
|
||||||
|
(" ", SEARCH),
|
||||||
|
("b", SEARCH),
|
||||||
|
(" ", DEFAULT),
|
||||||
|
][..],
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
for test in tests {
|
||||||
|
let (line, matches, want) = test;
|
||||||
|
let mut lh = LineHighlighter::new(line, CUR, 4, None, SEL);
|
||||||
|
lh.search(matches.iter().copied(), SEARCH);
|
||||||
|
assert_spans(lh, want, test);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn into_spans_selection() {
|
||||||
|
let tests = [
|
||||||
|
// (line, (row, start_row, start_off, end_row, end_off), want)
|
||||||
|
("abc", (0, 1, 0, 2, 0), &[("abc", DEFAULT)][..]),
|
||||||
|
("abc", (1, 1, 0, 1, 1), &[("a", SEL), ("bc", DEFAULT)][..]),
|
||||||
|
("abc", (1, 1, 2, 1, 3), &[("ab", DEFAULT), ("c", SEL)][..]),
|
||||||
|
("abc", (1, 1, 0, 1, 3), &[("abc", SEL)][..]),
|
||||||
|
("abc", (1, 1, 0, 2, 0), &[("abc", SEL), (" ", SEL)][..]),
|
||||||
|
(
|
||||||
|
"abc",
|
||||||
|
(1, 1, 2, 2, 0),
|
||||||
|
&[("ab", DEFAULT), ("c", SEL), (" ", SEL)][..],
|
||||||
|
),
|
||||||
|
("abc", (1, 1, 3, 2, 0), &[("abc", DEFAULT), (" ", SEL)][..]),
|
||||||
|
("abc", (2, 1, 0, 3, 0), &[("abc", SEL), (" ", SEL)][..]),
|
||||||
|
("abc", (2, 1, 0, 2, 0), &[("abc", DEFAULT)][..]),
|
||||||
|
("abc", (2, 1, 0, 2, 2), &[("ab", SEL), ("c", DEFAULT)][..]),
|
||||||
|
("abc", (2, 1, 0, 2, 3), &[("abc", SEL)][..]),
|
||||||
|
(
|
||||||
|
"ab\t",
|
||||||
|
(1, 1, 2, 2, 0),
|
||||||
|
&[("ab", DEFAULT), (" ", SEL), (" ", SEL)][..],
|
||||||
|
),
|
||||||
|
("a\tb", (2, 1, 0, 3, 0), &[("a b", SEL), (" ", SEL)][..]),
|
||||||
|
(
|
||||||
|
"a\tb",
|
||||||
|
(2, 1, 0, 2, 2),
|
||||||
|
&[("a ", SEL), ("b", DEFAULT)][..],
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
for test in tests {
|
||||||
|
let (line, (row, start_row, start_off, end_row, end_off), want) = test;
|
||||||
|
let mut lh = LineHighlighter::new(line, CUR, 4, None, SEL);
|
||||||
|
lh.selection(row, start_row, start_off, end_row, end_off);
|
||||||
|
assert_spans(lh, want, test);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn into_spans_mixed_highlights() {
|
||||||
|
let tests = [
|
||||||
|
(
|
||||||
|
"cursor on selection",
|
||||||
|
{
|
||||||
|
let mut lh = LineHighlighter::new("abcde", CUR, 4, None, SEL);
|
||||||
|
lh.cursor_line(2, LINE);
|
||||||
|
lh.selection(0, 0, 1, 0, 4);
|
||||||
|
lh
|
||||||
|
},
|
||||||
|
&[("a", LINE), ("b", SEL), ("c", CUR), ("d", SEL), ("e", LINE)][..],
|
||||||
|
),
|
||||||
|
#[cfg(feature = "search")]
|
||||||
|
(
|
||||||
|
"cursor + selection + search",
|
||||||
|
{
|
||||||
|
let mut lh = LineHighlighter::new("abcdefg", CUR, 4, None, SEL);
|
||||||
|
lh.cursor_line(3, LINE);
|
||||||
|
lh.selection(0, 0, 2, 0, 5);
|
||||||
|
lh.search([(1, 2), (5, 6)].into_iter(), SEARCH);
|
||||||
|
lh
|
||||||
|
},
|
||||||
|
&[
|
||||||
|
("a", LINE),
|
||||||
|
("b", SEARCH),
|
||||||
|
("c", SEL),
|
||||||
|
("d", CUR),
|
||||||
|
("e", SEL),
|
||||||
|
("f", SEARCH),
|
||||||
|
("g", LINE),
|
||||||
|
][..],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"selection + cursor at end",
|
||||||
|
{
|
||||||
|
let mut lh = LineHighlighter::new("ab", CUR, 4, None, SEL);
|
||||||
|
lh.cursor_line(2, LINE);
|
||||||
|
lh.selection(0, 0, 1, 2, 0);
|
||||||
|
lh
|
||||||
|
},
|
||||||
|
&[("a", LINE), ("b", SEL), (" ", CUR)][..],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"cursor at start of selection",
|
||||||
|
{
|
||||||
|
let mut lh = LineHighlighter::new("abcd", CUR, 4, None, SEL);
|
||||||
|
lh.cursor_line(1, LINE);
|
||||||
|
lh.selection(0, 0, 1, 0, 3);
|
||||||
|
lh
|
||||||
|
},
|
||||||
|
&[("a", LINE), ("b", CUR), ("c", SEL), ("d", LINE)][..],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"cursor at end of selection",
|
||||||
|
{
|
||||||
|
let mut lh = LineHighlighter::new("abcd", CUR, 4, None, SEL);
|
||||||
|
lh.cursor_line(2, LINE);
|
||||||
|
lh.selection(0, 0, 1, 0, 3);
|
||||||
|
lh
|
||||||
|
},
|
||||||
|
&[("a", LINE), ("b", SEL), ("c", CUR), ("d", LINE)][..],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"cursor covers selection",
|
||||||
|
{
|
||||||
|
let mut lh = LineHighlighter::new("abc", CUR, 4, None, SEL);
|
||||||
|
lh.cursor_line(1, LINE);
|
||||||
|
lh.selection(0, 0, 1, 0, 2);
|
||||||
|
lh
|
||||||
|
},
|
||||||
|
&[("a", LINE), ("b", CUR), ("c", LINE)][..],
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (what, lh, want) in tests {
|
||||||
|
assert_spans(lh, want, what);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,7 +79,6 @@ impl From<MouseEvent> for Input {
|
|||||||
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);
|
||||||
let shift = mouse.modifiers.contains(KeyModifiers::SHIFT);
|
let shift = mouse.modifiers.contains(KeyModifiers::SHIFT);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
key,
|
key,
|
||||||
ctrl,
|
ctrl,
|
||||||
@@ -118,27 +117,34 @@ mod tests {
|
|||||||
for (from, to) in [
|
for (from, to) in [
|
||||||
(
|
(
|
||||||
key_event(KeyCode::Char('a'), KeyModifiers::empty()),
|
key_event(KeyCode::Char('a'), KeyModifiers::empty()),
|
||||||
input(Key::Char('a'), false, false),
|
input(Key::Char('a'), false, false, false),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
key_event(KeyCode::Enter, KeyModifiers::empty()),
|
key_event(KeyCode::Enter, KeyModifiers::empty()),
|
||||||
input(Key::Enter, false, false),
|
input(Key::Enter, false, false, false),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
key_event(KeyCode::Left, KeyModifiers::CONTROL),
|
key_event(KeyCode::Left, KeyModifiers::CONTROL),
|
||||||
input(Key::Left, true, false),
|
input(Key::Left, true, false, false),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
key_event(KeyCode::Right, KeyModifiers::SHIFT),
|
||||||
|
input(Key::Right, false, false, true),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
key_event(KeyCode::Home, KeyModifiers::ALT),
|
key_event(KeyCode::Home, KeyModifiers::ALT),
|
||||||
input(Key::Home, false, true),
|
input(Key::Home, false, true, false),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
key_event(KeyCode::F(1), KeyModifiers::ALT | KeyModifiers::CONTROL),
|
key_event(
|
||||||
input(Key::F(1), true, true),
|
KeyCode::F(1),
|
||||||
|
KeyModifiers::ALT | KeyModifiers::CONTROL | KeyModifiers::SHIFT,
|
||||||
|
),
|
||||||
|
input(Key::F(1), true, true, true),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
key_event(KeyCode::NumLock, KeyModifiers::CONTROL),
|
key_event(KeyCode::NumLock, KeyModifiers::CONTROL),
|
||||||
input(Key::Null, true, false),
|
input(Key::Null, true, false, false),
|
||||||
),
|
),
|
||||||
] {
|
] {
|
||||||
assert_eq!(Input::from(from), to, "{:?} -> {:?}", from, to);
|
assert_eq!(Input::from(from), to, "{:?} -> {:?}", from, to);
|
||||||
@@ -150,26 +156,30 @@ mod tests {
|
|||||||
for (from, to) in [
|
for (from, to) in [
|
||||||
(
|
(
|
||||||
mouse_event(MouseEventKind::ScrollDown, KeyModifiers::empty()),
|
mouse_event(MouseEventKind::ScrollDown, KeyModifiers::empty()),
|
||||||
input(Key::MouseScrollDown, false, false),
|
input(Key::MouseScrollDown, false, false, false),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
mouse_event(MouseEventKind::ScrollUp, KeyModifiers::CONTROL),
|
mouse_event(MouseEventKind::ScrollUp, KeyModifiers::CONTROL),
|
||||||
input(Key::MouseScrollUp, true, false),
|
input(Key::MouseScrollUp, true, false, false),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
mouse_event(MouseEventKind::ScrollUp, KeyModifiers::SHIFT),
|
||||||
|
input(Key::MouseScrollUp, false, false, true),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
mouse_event(MouseEventKind::ScrollDown, KeyModifiers::ALT),
|
mouse_event(MouseEventKind::ScrollDown, KeyModifiers::ALT),
|
||||||
input(Key::MouseScrollDown, false, true),
|
input(Key::MouseScrollDown, false, true, false),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
mouse_event(
|
mouse_event(
|
||||||
MouseEventKind::ScrollUp,
|
MouseEventKind::ScrollUp,
|
||||||
KeyModifiers::CONTROL | KeyModifiers::ALT,
|
KeyModifiers::CONTROL | KeyModifiers::ALT,
|
||||||
),
|
),
|
||||||
input(Key::MouseScrollUp, true, true),
|
input(Key::MouseScrollUp, true, true, false),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
mouse_event(MouseEventKind::Moved, KeyModifiers::CONTROL),
|
mouse_event(MouseEventKind::Moved, KeyModifiers::CONTROL),
|
||||||
input(Key::Null, true, false),
|
input(Key::Null, true, false, false),
|
||||||
),
|
),
|
||||||
] {
|
] {
|
||||||
assert_eq!(Input::from(from), to, "{:?} -> {:?}", from, to);
|
assert_eq!(Input::from(from), to, "{:?} -> {:?}", from, to);
|
||||||
@@ -181,16 +191,16 @@ mod tests {
|
|||||||
for (from, to) in [
|
for (from, to) in [
|
||||||
(
|
(
|
||||||
Event::Key(key_event(KeyCode::Char('a'), KeyModifiers::empty())),
|
Event::Key(key_event(KeyCode::Char('a'), KeyModifiers::empty())),
|
||||||
input(Key::Char('a'), false, false),
|
input(Key::Char('a'), false, false, false),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
Event::Mouse(mouse_event(
|
Event::Mouse(mouse_event(
|
||||||
MouseEventKind::ScrollDown,
|
MouseEventKind::ScrollDown,
|
||||||
KeyModifiers::empty(),
|
KeyModifiers::empty(),
|
||||||
)),
|
)),
|
||||||
input(Key::MouseScrollDown, false, false),
|
input(Key::MouseScrollDown, false, false, false),
|
||||||
),
|
),
|
||||||
(Event::FocusGained, input(Key::Null, false, false)),
|
(Event::FocusGained, input(Key::Null, false, false, false)),
|
||||||
] {
|
] {
|
||||||
assert_eq!(Input::from(from.clone()), to, "{:?} -> {:?}", from, to);
|
assert_eq!(Input::from(from.clone()), to, "{:?} -> {:?}", from, to);
|
||||||
}
|
}
|
||||||
@@ -201,7 +211,7 @@ mod tests {
|
|||||||
fn ignore_key_release_event() {
|
fn ignore_key_release_event() {
|
||||||
let mut from = key_event(KeyCode::Char('a'), KeyModifiers::empty());
|
let mut from = key_event(KeyCode::Char('a'), KeyModifiers::empty());
|
||||||
from.kind = KeyEventKind::Release;
|
from.kind = KeyEventKind::Release;
|
||||||
let to = input(Key::Null, false, false);
|
let to = input(Key::Null, false, false, false);
|
||||||
assert_eq!(Input::from(from), to, "{:?} -> {:?}", from, to);
|
assert_eq!(Input::from(from), to, "{:?} -> {:?}", from, to);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,7 +90,8 @@ impl Default for Key {
|
|||||||
/// textarea.input(Input {
|
/// textarea.input(Input {
|
||||||
/// key: Key::Char('a'),
|
/// key: Key::Char('a'),
|
||||||
/// ctrl: true,
|
/// ctrl: true,
|
||||||
/// alt: false,shift:false
|
/// alt: false,
|
||||||
|
/// shift: false,
|
||||||
/// });
|
/// });
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(Debug, Clone, Default, PartialEq, Hash)]
|
#[derive(Debug, Clone, Default, PartialEq, Hash)]
|
||||||
@@ -102,7 +103,7 @@ 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.
|
/// Shift modifier key. `true` means Alt key was pressed.
|
||||||
pub shift: bool,
|
pub shift: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,12 +112,12 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[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, shift: bool) -> Input {
|
||||||
Input {
|
Input {
|
||||||
key,
|
key,
|
||||||
ctrl,
|
ctrl,
|
||||||
alt,
|
alt,
|
||||||
shift: false,
|
shift,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ 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),
|
||||||
@@ -49,7 +48,7 @@ impl From<KeyEvent> for Input {
|
|||||||
key,
|
key,
|
||||||
ctrl,
|
ctrl,
|
||||||
alt,
|
alt,
|
||||||
shift,
|
shift: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -90,14 +89,23 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn key_to_input() {
|
fn key_to_input() {
|
||||||
for (from, to) in [
|
for (from, to) in [
|
||||||
(KeyEvent::Char('a'), input(Key::Char('a'), false, false)),
|
(
|
||||||
(KeyEvent::Ctrl('a'), input(Key::Char('a'), true, false)),
|
KeyEvent::Char('a'),
|
||||||
(KeyEvent::Alt('a'), input(Key::Char('a'), false, true)),
|
input(Key::Char('a'), false, false, false),
|
||||||
(KeyEvent::Char('\n'), input(Key::Enter, false, false)),
|
),
|
||||||
(KeyEvent::Char('\r'), input(Key::Enter, false, false)),
|
(
|
||||||
(KeyEvent::F(1), input(Key::F(1), false, false)),
|
KeyEvent::Ctrl('a'),
|
||||||
(KeyEvent::BackTab, input(Key::Tab, false, false)),
|
input(Key::Char('a'), true, false, false),
|
||||||
(KeyEvent::Null, input(Key::Null, false, false)),
|
),
|
||||||
|
(
|
||||||
|
KeyEvent::Alt('a'),
|
||||||
|
input(Key::Char('a'), false, true, false),
|
||||||
|
),
|
||||||
|
(KeyEvent::Char('\n'), input(Key::Enter, false, false, false)),
|
||||||
|
(KeyEvent::Char('\r'), input(Key::Enter, false, false, false)),
|
||||||
|
(KeyEvent::F(1), input(Key::F(1), false, false, false)),
|
||||||
|
(KeyEvent::BackTab, input(Key::Tab, false, false, false)),
|
||||||
|
(KeyEvent::Null, input(Key::Null, false, false, false)),
|
||||||
] {
|
] {
|
||||||
assert_eq!(Input::from(from), to, "{:?} -> {:?}", from, to);
|
assert_eq!(Input::from(from), to, "{:?} -> {:?}", from, to);
|
||||||
}
|
}
|
||||||
@@ -108,18 +116,24 @@ mod tests {
|
|||||||
for (from, to) in [
|
for (from, to) in [
|
||||||
(
|
(
|
||||||
MouseEvent::Press(MouseButton::WheelDown, 1, 1),
|
MouseEvent::Press(MouseButton::WheelDown, 1, 1),
|
||||||
input(Key::MouseScrollDown, false, false),
|
input(Key::MouseScrollDown, false, false, false),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
MouseEvent::Press(MouseButton::WheelUp, 1, 1),
|
MouseEvent::Press(MouseButton::WheelUp, 1, 1),
|
||||||
input(Key::MouseScrollUp, false, false),
|
input(Key::MouseScrollUp, false, false, false),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
MouseEvent::Press(MouseButton::Left, 1, 1),
|
MouseEvent::Press(MouseButton::Left, 1, 1),
|
||||||
input(Key::Null, false, false),
|
input(Key::Null, false, false, false),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
MouseEvent::Release(1, 1),
|
||||||
|
input(Key::Null, false, false, false),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
MouseEvent::Hold(1, 1),
|
||||||
|
input(Key::Null, false, false, false),
|
||||||
),
|
),
|
||||||
(MouseEvent::Release(1, 1), input(Key::Null, false, false)),
|
|
||||||
(MouseEvent::Hold(1, 1), input(Key::Null, false, false)),
|
|
||||||
] {
|
] {
|
||||||
assert_eq!(Input::from(from), to, "{:?} -> {:?}", from, to);
|
assert_eq!(Input::from(from), to, "{:?} -> {:?}", from, to);
|
||||||
}
|
}
|
||||||
@@ -130,13 +144,16 @@ mod tests {
|
|||||||
for (from, to) in [
|
for (from, to) in [
|
||||||
(
|
(
|
||||||
Event::Key(KeyEvent::Char('a')),
|
Event::Key(KeyEvent::Char('a')),
|
||||||
input(Key::Char('a'), false, false),
|
input(Key::Char('a'), false, false, false),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
Event::Mouse(MouseEvent::Press(MouseButton::WheelDown, 1, 1)),
|
Event::Mouse(MouseEvent::Press(MouseButton::WheelDown, 1, 1)),
|
||||||
input(Key::MouseScrollDown, false, false),
|
input(Key::MouseScrollDown, false, false, false),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
Event::Unsupported(vec![]),
|
||||||
|
input(Key::Null, false, false, false),
|
||||||
),
|
),
|
||||||
(Event::Unsupported(vec![]), input(Key::Null, false, false)),
|
|
||||||
] {
|
] {
|
||||||
assert_eq!(Input::from(from.clone()), to, "{:?} -> {:?}", from, to);
|
assert_eq!(Input::from(from.clone()), to, "{:?} -> {:?}", from, to);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ impl From<KeyEvent> for Input {
|
|||||||
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);
|
let shift = modifiers.contains(Modifiers::SHIFT);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
key,
|
key,
|
||||||
ctrl,
|
ctrl,
|
||||||
@@ -83,6 +84,7 @@ impl From<MouseEvent> for Input {
|
|||||||
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);
|
let shift = modifiers.contains(Modifiers::SHIFT);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
key,
|
key,
|
||||||
ctrl,
|
ctrl,
|
||||||
@@ -105,6 +107,7 @@ impl From<PixelMouseEvent> for Input {
|
|||||||
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);
|
let shift = modifiers.contains(Modifiers::SHIFT);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
key,
|
key,
|
||||||
ctrl,
|
ctrl,
|
||||||
@@ -146,27 +149,34 @@ mod tests {
|
|||||||
for (from, to) in [
|
for (from, to) in [
|
||||||
(
|
(
|
||||||
key_event(KeyCode::Char('a'), Modifiers::empty()),
|
key_event(KeyCode::Char('a'), Modifiers::empty()),
|
||||||
input(Key::Char('a'), false, false),
|
input(Key::Char('a'), false, false, false),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
key_event(KeyCode::Enter, Modifiers::empty()),
|
key_event(KeyCode::Enter, Modifiers::empty()),
|
||||||
input(Key::Enter, false, false),
|
input(Key::Enter, false, false, false),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
key_event(KeyCode::LeftArrow, Modifiers::CTRL),
|
key_event(KeyCode::LeftArrow, Modifiers::CTRL),
|
||||||
input(Key::Left, true, false),
|
input(Key::Left, true, false, false),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
key_event(KeyCode::RightArrow, Modifiers::SHIFT),
|
||||||
|
input(Key::Right, false, false, true),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
key_event(KeyCode::Home, Modifiers::ALT),
|
key_event(KeyCode::Home, Modifiers::ALT),
|
||||||
input(Key::Home, false, true),
|
input(Key::Home, false, true, false),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
key_event(KeyCode::Function(1), Modifiers::ALT | Modifiers::CTRL),
|
key_event(
|
||||||
input(Key::F(1), true, true),
|
KeyCode::Function(1),
|
||||||
|
Modifiers::ALT | Modifiers::CTRL | Modifiers::SHIFT,
|
||||||
|
),
|
||||||
|
input(Key::F(1), true, true, true),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
key_event(KeyCode::NumLock, Modifiers::CTRL),
|
key_event(KeyCode::NumLock, Modifiers::CTRL),
|
||||||
input(Key::Null, true, false),
|
input(Key::Null, true, false, false),
|
||||||
),
|
),
|
||||||
] {
|
] {
|
||||||
assert_eq!(Input::from(from.clone()), to, "{:?} -> {:?}", from, to);
|
assert_eq!(Input::from(from.clone()), to, "{:?} -> {:?}", from, to);
|
||||||
@@ -178,30 +188,37 @@ mod tests {
|
|||||||
for (from, to) in [
|
for (from, to) in [
|
||||||
(
|
(
|
||||||
mouse_event(MouseButtons::VERT_WHEEL, Modifiers::empty()),
|
mouse_event(MouseButtons::VERT_WHEEL, Modifiers::empty()),
|
||||||
input(Key::MouseScrollDown, false, false),
|
input(Key::MouseScrollDown, false, false, false),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
mouse_event(
|
mouse_event(
|
||||||
MouseButtons::VERT_WHEEL | MouseButtons::WHEEL_POSITIVE,
|
MouseButtons::VERT_WHEEL | MouseButtons::WHEEL_POSITIVE,
|
||||||
Modifiers::empty(),
|
Modifiers::empty(),
|
||||||
),
|
),
|
||||||
input(Key::MouseScrollUp, false, false),
|
input(Key::MouseScrollUp, false, false, false),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
mouse_event(MouseButtons::VERT_WHEEL, Modifiers::CTRL),
|
mouse_event(MouseButtons::VERT_WHEEL, Modifiers::CTRL),
|
||||||
input(Key::MouseScrollDown, true, false),
|
input(Key::MouseScrollDown, true, false, false),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
mouse_event(MouseButtons::VERT_WHEEL, Modifiers::SHIFT),
|
||||||
|
input(Key::MouseScrollDown, false, false, true),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
mouse_event(MouseButtons::VERT_WHEEL, Modifiers::ALT),
|
mouse_event(MouseButtons::VERT_WHEEL, Modifiers::ALT),
|
||||||
input(Key::MouseScrollDown, false, true),
|
input(Key::MouseScrollDown, false, true, false),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
mouse_event(MouseButtons::VERT_WHEEL, Modifiers::CTRL | Modifiers::ALT),
|
mouse_event(
|
||||||
input(Key::MouseScrollDown, true, true),
|
MouseButtons::VERT_WHEEL,
|
||||||
|
Modifiers::CTRL | Modifiers::ALT | Modifiers::SHIFT,
|
||||||
|
),
|
||||||
|
input(Key::MouseScrollDown, true, true, true),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
mouse_event(MouseButtons::LEFT, Modifiers::empty()),
|
mouse_event(MouseButtons::LEFT, Modifiers::empty()),
|
||||||
input(Key::Null, false, false),
|
input(Key::Null, false, false, false),
|
||||||
),
|
),
|
||||||
] {
|
] {
|
||||||
assert_eq!(Input::from(from.clone()), to, "{:?} -> {:?}", from, to);
|
assert_eq!(Input::from(from.clone()), to, "{:?} -> {:?}", from, to);
|
||||||
@@ -216,22 +233,22 @@ mod tests {
|
|||||||
for (from, to) in [
|
for (from, to) in [
|
||||||
(
|
(
|
||||||
InputEvent::Key(key_event(KeyCode::Char('a'), Modifiers::empty())),
|
InputEvent::Key(key_event(KeyCode::Char('a'), Modifiers::empty())),
|
||||||
input(Key::Char('a'), false, false),
|
input(Key::Char('a'), false, false, false),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
InputEvent::Mouse(mouse_event(MouseButtons::VERT_WHEEL, Modifiers::empty())),
|
InputEvent::Mouse(mouse_event(MouseButtons::VERT_WHEEL, Modifiers::empty())),
|
||||||
input(Key::MouseScrollDown, false, false),
|
input(Key::MouseScrollDown, false, false, false),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
InputEvent::PixelMouse(pixel_mouse_event(
|
InputEvent::PixelMouse(pixel_mouse_event(
|
||||||
MouseButtons::VERT_WHEEL,
|
MouseButtons::VERT_WHEEL,
|
||||||
Modifiers::empty(),
|
Modifiers::empty(),
|
||||||
)),
|
)),
|
||||||
input(Key::MouseScrollDown, false, false),
|
input(Key::MouseScrollDown, false, false, false),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
InputEvent::Paste("x".into()),
|
InputEvent::Paste("x".into()),
|
||||||
input(Key::Null, false, false),
|
input(Key::Null, false, false, false),
|
||||||
),
|
),
|
||||||
] {
|
] {
|
||||||
assert_eq!(Input::from(from.clone()), to, "{:?} -> {:?}", from, to);
|
assert_eq!(Input::from(from.clone()), to, "{:?} -> {:?}", from, to);
|
||||||
|
|||||||
793
src/textarea.rs
793
src/textarea.rs
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
@@ -1,3 +1,4 @@
|
|||||||
|
use std::cmp;
|
||||||
use tui_textarea::{CursorMove, TextArea};
|
use tui_textarea::{CursorMove, TextArea};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -799,385 +800,507 @@ fn test_delete_str_multiple_lines() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
|
||||||
// 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]
|
#[test]
|
||||||
fn select_copy() {
|
fn test_copy_single_line() {
|
||||||
for test in [
|
for i in 0..="abc".len() {
|
||||||
// plain ascii
|
for j in i.."abc".len() {
|
||||||
(
|
let mut t = TextArea::from(["abc"]);
|
||||||
"hello world",
|
|
||||||
vec![
|
t.move_cursor(CursorMove::Jump(0, i as u16));
|
||||||
(Key::Home, false, false, false),
|
t.start_selection();
|
||||||
(Key::Right, false, false, true),
|
t.move_cursor(CursorMove::Jump(0, j as u16));
|
||||||
(Key::Right, false, false, true),
|
t.copy();
|
||||||
(Key::Char('c'), true, false, false),
|
|
||||||
],
|
assert_eq!(t.yank_text(), &"abc"[i..j], "from {i} to {j}");
|
||||||
"he",
|
assert_eq!(t.lines(), ["abc"], "from {i} to {j}");
|
||||||
"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]
|
#[test]
|
||||||
fn select_paste() {
|
fn test_cut_single_line() {
|
||||||
for test in [
|
for i in 0.."abc".len() {
|
||||||
// plain ascii
|
for j in i + 1.."abc".len() {
|
||||||
(
|
let mut t = TextArea::from(["abc"]);
|
||||||
"hello world",
|
|
||||||
vec![
|
t.move_cursor(CursorMove::Jump(0, i as u16));
|
||||||
(Key::Home, false, false, false),
|
t.start_selection();
|
||||||
(Key::Right, false, false, true),
|
t.move_cursor(CursorMove::Jump(0, j as u16));
|
||||||
(Key::Right, false, false, true),
|
t.cut();
|
||||||
(Key::Char('c'), true, false, false),
|
|
||||||
(Key::End, false, false, false),
|
assert_eq!(t.yank_text(), &"abc"[i..j], "from {i} to {j}");
|
||||||
(Key::Char('y'), true, false, false),
|
|
||||||
],
|
let mut after = "abc".to_string();
|
||||||
"he",
|
after.replace_range(i..j, "");
|
||||||
"hello worldhe",
|
assert_eq!(t.lines(), [after], "from {i} to {j}");
|
||||||
),
|
assert_eq!(t.cursor(), (0, i));
|
||||||
(
|
|
||||||
"hello world",
|
t.paste();
|
||||||
vec![
|
assert_eq!(t.lines(), ["abc"], "from {i} to {j}");
|
||||||
(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]
|
#[test]
|
||||||
fn select_all_keys() {
|
fn test_copy_cut_empty() {
|
||||||
for test in [
|
for row in 0..=2 {
|
||||||
|
for col in 0..=2 {
|
||||||
|
let check = |f: fn(&mut TextArea<'_>)| {
|
||||||
|
let mut t = TextArea::from(["ab", "cd", "ef"]);
|
||||||
|
t.move_cursor(CursorMove::Jump(row, col));
|
||||||
|
t.start_selection();
|
||||||
|
t.move_cursor(CursorMove::Jump(row, col));
|
||||||
|
f(&mut t);
|
||||||
|
assert!(!t.is_selecting());
|
||||||
|
assert_eq!(t.cursor(), (row as _, col as _));
|
||||||
|
assert_eq!(t.lines(), ["ab", "cd", "ef"]);
|
||||||
|
};
|
||||||
|
|
||||||
|
check(|t| {
|
||||||
|
assert!(!t.cut());
|
||||||
|
});
|
||||||
|
check(|t| t.copy());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_copy_cut_paste_multi_lines() {
|
||||||
|
#[rustfmt::skip]
|
||||||
|
let tests = [
|
||||||
(
|
(
|
||||||
// enter key erases selection
|
// Initial text
|
||||||
"hello world",
|
&[
|
||||||
vec![
|
"ab",
|
||||||
(Key::End, false, false, true),
|
"cd",
|
||||||
(Key::Enter, false, false, true),
|
"ef",
|
||||||
],
|
][..],
|
||||||
"",
|
// Start position of selection
|
||||||
|
(0, 0),
|
||||||
|
// End position of selection
|
||||||
|
(1, 0),
|
||||||
|
// Expected yanked text
|
||||||
|
"ab\n",
|
||||||
|
// Text buffer after cut
|
||||||
|
&[
|
||||||
|
"cd",
|
||||||
|
"ef",
|
||||||
|
][..]
|
||||||
|
),
|
||||||
|
(
|
||||||
|
&[
|
||||||
|
"ab",
|
||||||
|
"cd",
|
||||||
|
"ef",
|
||||||
|
][..],
|
||||||
|
(0, 0),
|
||||||
|
(1, 1),
|
||||||
|
"ab\nc",
|
||||||
|
&[
|
||||||
|
"d",
|
||||||
|
"ef",
|
||||||
|
][..]
|
||||||
|
),
|
||||||
|
(
|
||||||
|
&[
|
||||||
|
"ab",
|
||||||
|
"cd",
|
||||||
|
"ef",
|
||||||
|
][..],
|
||||||
|
(0, 0),
|
||||||
|
(1, 2),
|
||||||
|
"ab\ncd",
|
||||||
|
&[
|
||||||
|
"",
|
||||||
|
"ef",
|
||||||
|
][..]
|
||||||
|
),
|
||||||
|
(
|
||||||
|
&[
|
||||||
|
"ab",
|
||||||
|
"cd",
|
||||||
|
"ef",
|
||||||
|
][..],
|
||||||
|
(0, 0),
|
||||||
|
(2, 0),
|
||||||
|
"ab\ncd\n",
|
||||||
|
&[
|
||||||
|
"ef",
|
||||||
|
][..]
|
||||||
|
),
|
||||||
|
(
|
||||||
|
&[
|
||||||
|
"ab",
|
||||||
|
"cd",
|
||||||
|
"ef",
|
||||||
|
][..],
|
||||||
|
(0, 0),
|
||||||
|
(2, 1),
|
||||||
|
"ab\ncd\ne",
|
||||||
|
&[
|
||||||
|
"f",
|
||||||
|
][..]
|
||||||
|
),
|
||||||
|
(
|
||||||
|
&[
|
||||||
|
"ab",
|
||||||
|
"cd",
|
||||||
|
"ef",
|
||||||
|
][..],
|
||||||
|
(0, 0),
|
||||||
|
(2, 2),
|
||||||
|
"ab\ncd\nef",
|
||||||
|
&[
|
||||||
|
"",
|
||||||
|
][..]
|
||||||
|
),
|
||||||
|
(
|
||||||
|
&[
|
||||||
|
"ab",
|
||||||
|
"cd",
|
||||||
|
"ef",
|
||||||
|
][..],
|
||||||
|
(0, 1),
|
||||||
|
(1, 1),
|
||||||
|
"b\nc",
|
||||||
|
&[
|
||||||
|
"ad",
|
||||||
|
"ef",
|
||||||
|
][..]
|
||||||
|
),
|
||||||
|
(
|
||||||
|
&[
|
||||||
|
"ab",
|
||||||
|
"cd",
|
||||||
|
"ef",
|
||||||
|
][..],
|
||||||
|
(0, 2),
|
||||||
|
(1, 1),
|
||||||
|
"\nc",
|
||||||
|
&[
|
||||||
|
"abd",
|
||||||
|
"ef",
|
||||||
|
][..]
|
||||||
|
),
|
||||||
|
(
|
||||||
|
&[
|
||||||
|
"ab",
|
||||||
|
"cd",
|
||||||
|
"ef",
|
||||||
|
][..],
|
||||||
|
(1, 0),
|
||||||
|
(2, 1),
|
||||||
|
"cd\ne",
|
||||||
|
&[
|
||||||
|
"ab",
|
||||||
|
"f",
|
||||||
|
][..]
|
||||||
|
),
|
||||||
|
(
|
||||||
|
&[
|
||||||
|
"ab",
|
||||||
|
"cd",
|
||||||
|
"ef",
|
||||||
|
][..],
|
||||||
|
(0, 2),
|
||||||
|
(1, 0),
|
||||||
"\n",
|
"\n",
|
||||||
|
&[
|
||||||
|
"abcd",
|
||||||
|
"ef",
|
||||||
|
][..]
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
// word forward select
|
&[
|
||||||
"hello world",
|
"ab",
|
||||||
vec![
|
"cd",
|
||||||
(Key::Char('f'), false, true, true),
|
"ef",
|
||||||
(Key::Char('c'), true, false, false),
|
][..],
|
||||||
],
|
(0, 2),
|
||||||
"hello ",
|
(2, 0),
|
||||||
"hello world",
|
"\ncd\n",
|
||||||
|
&[
|
||||||
|
"abef",
|
||||||
|
][..]
|
||||||
|
),
|
||||||
|
// Multi-byte characters
|
||||||
|
(
|
||||||
|
&[
|
||||||
|
"あい",
|
||||||
|
"うえ",
|
||||||
|
"おか",
|
||||||
|
][..],
|
||||||
|
(0, 0),
|
||||||
|
(2, 2),
|
||||||
|
"あい\nうえ\nおか",
|
||||||
|
&[
|
||||||
|
"",
|
||||||
|
][..]
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
// word back select
|
&[
|
||||||
"hello world",
|
"あい",
|
||||||
vec![
|
"うえ",
|
||||||
(Key::End, false, false, false),
|
"おか",
|
||||||
(Key::Char('b'), false, true, true),
|
][..],
|
||||||
(Key::Char('c'), true, false, false),
|
(0, 1),
|
||||||
],
|
(2, 1),
|
||||||
"world",
|
"い\nうえ\nお",
|
||||||
"hello world",
|
&[
|
||||||
|
"あか",
|
||||||
|
][..]
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
// para forward select
|
&[
|
||||||
"hello\nworld\n\nhow\nare\nyou",
|
"あい",
|
||||||
vec![
|
"うえ",
|
||||||
(Key::Char('n'), false, true, true),
|
"おか",
|
||||||
(Key::Char('x'), true, false, true),
|
][..],
|
||||||
],
|
(0, 2),
|
||||||
"hello\nworld\n\n",
|
(2, 0),
|
||||||
"how\nare\nyou",
|
"\nうえ\n",
|
||||||
|
&[
|
||||||
|
"あいおか",
|
||||||
|
][..]
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
// para back select
|
&[
|
||||||
"hello\nworld\n\nhow\nare\nyou",
|
"あい",
|
||||||
vec![
|
"うえ",
|
||||||
// goto end
|
"おか",
|
||||||
(Key::Char('n'), true, true, false),
|
][..],
|
||||||
(Key::End, false, false, false),
|
(0, 2),
|
||||||
(Key::Char('p'), false, true, true),
|
(1, 2),
|
||||||
(Key::Char('x'), true, false, true),
|
"\nうえ",
|
||||||
],
|
&[
|
||||||
"\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),
|
(0, 2),
|
||||||
(Key::Char('p'), false, true, true),
|
(1, 1),
|
||||||
(Key::Char('x'), true, false, false),
|
"\nう",
|
||||||
(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,
|
(0, 2),
|
||||||
strokes: &Vec<(Key, bool, bool, bool)>,
|
(1, 0),
|
||||||
yank: &str,
|
"\n",
|
||||||
remaining: &str,
|
&[
|
||||||
) {
|
"あいうえ",
|
||||||
let mut ta = TextArea::default();
|
"おか",
|
||||||
ta.insert_str(text);
|
][..]
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
// cursor home
|
for test in tests {
|
||||||
ta.input(Input {
|
let (init_text, (srow, scol), (erow, ecol), yanked, after_cut) = test;
|
||||||
key: Key::Up,
|
|
||||||
ctrl: true,
|
{
|
||||||
alt: true,
|
let mut t = TextArea::from(init_text.iter().map(|s| s.to_string()));
|
||||||
shift: false,
|
t.move_cursor(CursorMove::Jump(srow as _, scol as _));
|
||||||
});
|
t.start_selection();
|
||||||
ta.input(Input {
|
t.move_cursor(CursorMove::Jump(erow as _, ecol as _));
|
||||||
key: Key::Home,
|
t.copy();
|
||||||
ctrl: true,
|
|
||||||
alt: true,
|
assert_eq!(t.cursor(), (erow, ecol), "{test:?}");
|
||||||
shift: false,
|
assert_eq!(t.yank_text(), yanked, "{test:?}");
|
||||||
});
|
assert_eq!(t.lines(), init_text, "{test:?}");
|
||||||
for k in strokes {
|
}
|
||||||
let input = Input {
|
|
||||||
key: k.0,
|
{
|
||||||
ctrl: k.1,
|
let mut t = TextArea::from(init_text.iter().map(|s| s.to_string()));
|
||||||
alt: k.2,
|
t.move_cursor(CursorMove::Jump(srow as _, scol as _));
|
||||||
shift: k.3,
|
t.start_selection();
|
||||||
};
|
t.move_cursor(CursorMove::Jump(erow as _, ecol as _));
|
||||||
ta.input(input);
|
t.cut();
|
||||||
|
|
||||||
|
assert_eq!(t.cursor(), (srow, scol), "{test:?}");
|
||||||
|
assert_eq!(t.yank_text(), yanked, "{test:?}");
|
||||||
|
assert_eq!(t.lines(), after_cut, "{test:?}");
|
||||||
|
|
||||||
|
t.paste();
|
||||||
|
assert_eq!(t.lines(), init_text, "{test:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reverse positions
|
||||||
|
{
|
||||||
|
let mut t = TextArea::from(init_text.iter().map(|s| s.to_string()));
|
||||||
|
t.move_cursor(CursorMove::Jump(erow as _, ecol as _));
|
||||||
|
t.start_selection();
|
||||||
|
t.move_cursor(CursorMove::Jump(srow as _, scol as _));
|
||||||
|
t.copy();
|
||||||
|
|
||||||
|
assert_eq!(t.cursor(), (srow, scol), "{test:?}");
|
||||||
|
assert_eq!(t.yank_text(), yanked, "{test:?}");
|
||||||
|
assert_eq!(t.lines(), init_text, "{test:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut t = TextArea::from(init_text.iter().map(|s| s.to_string()));
|
||||||
|
t.move_cursor(CursorMove::Jump(erow as _, ecol as _));
|
||||||
|
t.start_selection();
|
||||||
|
t.move_cursor(CursorMove::Jump(srow as _, scol as _));
|
||||||
|
t.cut();
|
||||||
|
|
||||||
|
assert_eq!(t.cursor(), (srow, scol), "{test:?}");
|
||||||
|
assert_eq!(t.yank_text(), yanked, "{test:?}");
|
||||||
|
assert_eq!(t.lines(), after_cut, "{test:?}");
|
||||||
|
|
||||||
|
t.paste();
|
||||||
|
assert_eq!(t.lines(), init_text, "{test:?}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
assert_eq!(ta.yank_text(), yank);
|
}
|
||||||
assert_eq!(ta.lines().join("\n"), remaining);
|
|
||||||
|
#[test]
|
||||||
|
fn test_delete_selection_on_delete_operations() {
|
||||||
|
macro_rules! test_case {
|
||||||
|
($name:ident($($args:expr),*)) => {
|
||||||
|
(
|
||||||
|
stringify!($name),
|
||||||
|
(|t| t.$name($($args),*)) as fn(&mut TextArea) -> bool,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let tests = [
|
||||||
|
test_case!(delete_char()),
|
||||||
|
test_case!(delete_next_char()),
|
||||||
|
test_case!(delete_line_by_end()),
|
||||||
|
test_case!(delete_line_by_head()),
|
||||||
|
test_case!(delete_word()),
|
||||||
|
test_case!(delete_next_word()),
|
||||||
|
test_case!(delete_str(3)),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (n, f) in tests {
|
||||||
|
let mut t = TextArea::from(["ab", "cd", "ef"]);
|
||||||
|
t.move_cursor(CursorMove::Jump(0, 1));
|
||||||
|
t.start_selection();
|
||||||
|
t.move_cursor(CursorMove::Jump(2, 1));
|
||||||
|
|
||||||
|
let modified = f(&mut t);
|
||||||
|
assert!(modified, "{n}");
|
||||||
|
assert_eq!(t.lines(), ["af"], "{n}");
|
||||||
|
assert_eq!(t.cursor(), (0, 1), "{n}");
|
||||||
|
|
||||||
|
t.undo();
|
||||||
|
assert_eq!(t.lines(), ["ab", "cd", "ef"], "{n}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_delete_selection_on_delete_edge_cases() {
|
||||||
|
macro_rules! test_case {
|
||||||
|
($name:ident($($args:expr),*), $pos:expr) => {
|
||||||
|
(
|
||||||
|
stringify!($name),
|
||||||
|
(|t| t.$name($($args),*)) as fn(&mut TextArea) -> bool,
|
||||||
|
$pos,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// When deleting nothing and deleting newline
|
||||||
|
let tests = [
|
||||||
|
test_case!(delete_char(), (0, 0)),
|
||||||
|
test_case!(delete_char(), (1, 0)),
|
||||||
|
test_case!(delete_next_char(), (2, 2)),
|
||||||
|
test_case!(delete_next_char(), (1, 2)),
|
||||||
|
test_case!(delete_line_by_end(), (0, 2)),
|
||||||
|
test_case!(delete_line_by_end(), (2, 2)),
|
||||||
|
test_case!(delete_line_by_head(), (0, 0)),
|
||||||
|
test_case!(delete_line_by_head(), (1, 0)),
|
||||||
|
test_case!(delete_word(), (0, 0)),
|
||||||
|
test_case!(delete_word(), (1, 0)),
|
||||||
|
test_case!(delete_next_word(), (2, 2)),
|
||||||
|
test_case!(delete_next_word(), (1, 2)),
|
||||||
|
test_case!(delete_str(0), (0, 0)),
|
||||||
|
test_case!(delete_str(100), (2, 2)),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (n, f, pos) in tests {
|
||||||
|
let mut t = TextArea::from(["ab", "cd", "ef"]);
|
||||||
|
t.move_cursor(CursorMove::Jump(1, 1));
|
||||||
|
t.start_selection();
|
||||||
|
t.move_cursor(CursorMove::Jump(pos.0 as _, pos.1 as _));
|
||||||
|
|
||||||
|
assert!(f(&mut t), "{n}, {pos:?}");
|
||||||
|
assert_eq!(t.cursor(), cmp::min(pos, (1, 1)), "{n}, {pos:?}");
|
||||||
|
|
||||||
|
t.undo();
|
||||||
|
assert_eq!(t.lines(), ["ab", "cd", "ef"], "{n}, {pos:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_delete_selection_before_insert() {
|
||||||
|
macro_rules! test_case {
|
||||||
|
($name:ident($($args:expr),*), $want:expr) => {
|
||||||
|
(
|
||||||
|
stringify!($name),
|
||||||
|
(|t| {
|
||||||
|
t.$name($($args),*);
|
||||||
|
}) as fn(&mut TextArea),
|
||||||
|
&$want as &[_],
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let tests = [
|
||||||
|
test_case!(insert_newline(), ["a", "f"]),
|
||||||
|
test_case!(insert_char('x'), ["axf"]),
|
||||||
|
test_case!(insert_tab(), ["a f"]), // Default tab is 4 spaces
|
||||||
|
test_case!(insert_str("xyz"), ["axyzf"]),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (n, f, after) in tests {
|
||||||
|
let mut t = TextArea::from(["ab", "cd", "ef"]);
|
||||||
|
t.move_cursor(CursorMove::Jump(0, 1));
|
||||||
|
t.start_selection();
|
||||||
|
t.move_cursor(CursorMove::Jump(2, 1));
|
||||||
|
|
||||||
|
f(&mut t);
|
||||||
|
assert_eq!(t.lines(), after, "{n}");
|
||||||
|
|
||||||
|
// XXX: Deleting selection and inserting text are separate undo units for now
|
||||||
|
t.undo();
|
||||||
|
t.undo();
|
||||||
|
assert_eq!(t.lines(), ["ab", "cd", "ef"], "{n}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_undo_redo_stop_selection() {
|
||||||
|
fn check(t: &mut TextArea, f: fn(&mut TextArea) -> bool) {
|
||||||
|
t.move_cursor(CursorMove::Jump(0, 0));
|
||||||
|
t.start_selection();
|
||||||
|
t.move_cursor(CursorMove::Jump(0, 1));
|
||||||
|
assert!(t.is_selecting());
|
||||||
|
assert!(f(t));
|
||||||
|
assert!(!t.is_selecting());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut t = TextArea::default();
|
||||||
|
t.insert_char('a');
|
||||||
|
|
||||||
|
check(&mut t, |t| t.undo());
|
||||||
|
assert_eq!(t.lines(), [""]);
|
||||||
|
check(&mut t, |t| t.redo());
|
||||||
|
assert_eq!(t.lines(), ["a"]);
|
||||||
}
|
}
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user