зеркало из
https://github.com/glebtv/tui-textarea.git
synced 2026-09-03 06:16:17 +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
|
||||
- Cursor line highlight
|
||||
- Search with regular expressions
|
||||
- Text selection
|
||||
- Mouse scrolling
|
||||
- Yank support. Paste text deleted with `C-k`, `C-j`, ...
|
||||
- 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 |
|
||||
| `Ctrl+U` | Undo |
|
||||
| `Ctrl+R` | Redo |
|
||||
| `Ctrl+C` | Copy selected text |
|
||||
| `Ctrl+X` | Cut selected text |
|
||||
| `Ctrl+Y` | Paste yanked text |
|
||||
| `Ctrl+F`, `→` | Move cursor forward 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.undo()` | Undo |
|
||||
| `textarea.redo()` | Redo |
|
||||
| `textarea.copy()` | Copy selected text |
|
||||
| `textarea.cut()` | Cut selected 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::Back)` | Move cursor backward by one character |
|
||||
| `textarea.move_cursor(CursorMove::Up)` | Move cursor up by one line |
|
||||
|
||||
@@ -248,7 +248,7 @@ impl<'a> Editor<'a> {
|
||||
Span::raw(" to save, "),
|
||||
Span::styled("^G", Style::default().add_modifier(Modifier::BOLD)),
|
||||
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"),
|
||||
])
|
||||
};
|
||||
@@ -314,7 +314,7 @@ impl<'a> Editor<'a> {
|
||||
..
|
||||
} => break,
|
||||
Input {
|
||||
key: Key::Char('x'),
|
||||
key: Key::Char('t'),
|
||||
ctrl: true,
|
||||
..
|
||||
} => {
|
||||
|
||||
@@ -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,41 +75,32 @@ fn main() -> io::Result<()> {
|
||||
|
||||
term.draw(|f| f.render_widget(textarea.widget(), f.size()))?;
|
||||
|
||||
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);
|
||||
let input = crossterm::event::read()?.into();
|
||||
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' | 'H'),
|
||||
key: Key::Char('h'),
|
||||
..
|
||||
} => textarea.move_cursor(CursorMove::Back),
|
||||
Input {
|
||||
key: Key::Char('j' | 'J'),
|
||||
key: Key::Char('j'),
|
||||
..
|
||||
} => textarea.move_cursor(CursorMove::Down),
|
||||
Input {
|
||||
key: Key::Char('k' | 'K'),
|
||||
key: Key::Char('k'),
|
||||
..
|
||||
} => textarea.move_cursor(CursorMove::Up),
|
||||
Input {
|
||||
key: Key::Char('l' | 'L'),
|
||||
key: Key::Char('l'),
|
||||
..
|
||||
} => textarea.move_cursor(CursorMove::Forward),
|
||||
Input {
|
||||
key: Key::Char('w' | 'W'),
|
||||
key: Key::Char('w'),
|
||||
..
|
||||
} => textarea.move_cursor(CursorMove::WordForward),
|
||||
Input {
|
||||
key: Key::Char('b' | 'B'),
|
||||
key: Key::Char('b'),
|
||||
ctrl: false,
|
||||
..
|
||||
} => textarea.move_cursor(CursorMove::WordBack),
|
||||
@@ -140,13 +131,6 @@ fn main() -> io::Result<()> {
|
||||
} => {
|
||||
textarea.paste();
|
||||
}
|
||||
Input {
|
||||
key: Key::Char('y'),
|
||||
ctrl: false,
|
||||
..
|
||||
} => {
|
||||
textarea.copy();
|
||||
}
|
||||
Input {
|
||||
key: Key::Char('u'),
|
||||
ctrl: false,
|
||||
@@ -182,8 +166,6 @@ 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;
|
||||
}
|
||||
@@ -199,8 +181,6 @@ 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);
|
||||
@@ -210,7 +190,6 @@ fn main() -> io::Result<()> {
|
||||
key: Key::Char('I'),
|
||||
..
|
||||
} => {
|
||||
textarea.stop_selection();
|
||||
textarea.move_cursor(CursorMove::Head);
|
||||
mode = Mode::Insert;
|
||||
}
|
||||
@@ -248,6 +227,33 @@ fn main() -> io::Result<()> {
|
||||
ctrl: true,
|
||||
..
|
||||
} => 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 {
|
||||
|
||||
@@ -250,7 +250,7 @@ impl<'a> Editor<'a> {
|
||||
Span::raw(" to save, "),
|
||||
Span::styled("^G", Style::default().add_modifier(Modifier::BOLD)),
|
||||
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"),
|
||||
])
|
||||
};
|
||||
@@ -316,7 +316,7 @@ impl<'a> Editor<'a> {
|
||||
..
|
||||
} => break,
|
||||
Input {
|
||||
key: Key::Char('x'),
|
||||
key: Key::Char('t'),
|
||||
ctrl: true,
|
||||
..
|
||||
} => {
|
||||
|
||||
386
src/highlight.rs
386
src/highlight.rs
@@ -10,12 +10,11 @@ use std::iter;
|
||||
use tui::text::Spans as Line;
|
||||
use unicode_width::UnicodeWidthChar as _;
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Boundary {
|
||||
Cursor(Style),
|
||||
Select(Style),
|
||||
#[cfg(feature = "search")]
|
||||
Search(Style),
|
||||
Select(Style),
|
||||
End,
|
||||
}
|
||||
|
||||
@@ -25,8 +24,8 @@ impl Boundary {
|
||||
match b {
|
||||
Boundary::Cursor(_) => 3,
|
||||
#[cfg(feature = "search")]
|
||||
Boundary::Search(_) => 1,
|
||||
Boundary::Select(_) => 2,
|
||||
Boundary::Search(_) => 2,
|
||||
Boundary::Select(_) => 1,
|
||||
Boundary::End => 0,
|
||||
}
|
||||
}
|
||||
@@ -36,10 +35,10 @@ impl Boundary {
|
||||
fn style(&self) -> Option<Style> {
|
||||
match self {
|
||||
Boundary::Cursor(s) => Some(*s),
|
||||
Boundary::Select(s) => Some(*s),
|
||||
#[cfg(feature = "search")]
|
||||
Boundary::Search(s) => Some(*s),
|
||||
Boundary::End => None,
|
||||
Boundary::Select(s) => Some(*s),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -98,13 +97,13 @@ 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_at_end: bool,
|
||||
select_style: Style,
|
||||
}
|
||||
|
||||
@@ -125,6 +124,7 @@ impl<'a> LineHighlighter<'a> {
|
||||
cursor_style,
|
||||
tab_len,
|
||||
mask,
|
||||
select_at_end: false,
|
||||
select_style,
|
||||
}
|
||||
}
|
||||
@@ -135,11 +135,6 @@ 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
|
||||
@@ -151,60 +146,6 @@ 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 {
|
||||
@@ -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> {
|
||||
let Self {
|
||||
line,
|
||||
@@ -225,14 +196,20 @@ impl<'a> LineHighlighter<'a> {
|
||||
cursor_style,
|
||||
cursor_at_end,
|
||||
mask,
|
||||
select_at_end,
|
||||
select_style,
|
||||
} = self;
|
||||
let mut builder = DisplayTextBuilder::new(tab_len, mask);
|
||||
|
||||
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 {
|
||||
spans.push(Span::styled(" ", cursor_style));
|
||||
} else if select_at_end {
|
||||
spans.push(Span::styled(" ", select_style));
|
||||
}
|
||||
return Line::from(spans);
|
||||
}
|
||||
@@ -245,18 +222,10 @@ 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 {
|
||||
// add extra select space at line end to indicate
|
||||
// that the \n will be deleted / included
|
||||
if end > line.len() && style == select_style {
|
||||
spans.push(Span::styled(builder.build(&line[start..end - 1]), style));
|
||||
spans.push(Span::styled(" ", style));
|
||||
dont_add_cursor = true;
|
||||
} else {
|
||||
spans.push(Span::styled(builder.build(&line[start..end]), style));
|
||||
}
|
||||
spans.push(Span::styled(builder.build(&line[start..end]), style));
|
||||
}
|
||||
|
||||
style = if let Some(s) = next_boundary.style() {
|
||||
@@ -267,20 +236,27 @@ 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 && !dont_add_cursor {
|
||||
|
||||
if cursor_at_end {
|
||||
spans.push(Span::styled(" ", cursor_style));
|
||||
} else if select_at_end {
|
||||
spans.push(Span::styled(" ", select_style));
|
||||
}
|
||||
|
||||
Line::from(spans)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
// Tests for spans don't work with tui-rs
|
||||
#[cfg(all(test, feature = "ratatui"))]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::ratatui::style::Color;
|
||||
use std::fmt::Debug;
|
||||
use unicode_width::UnicodeWidthStr as _;
|
||||
|
||||
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), "あ あ ");
|
||||
}
|
||||
|
||||
// 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 alt = mouse.modifiers.contains(KeyModifiers::ALT);
|
||||
let shift = mouse.modifiers.contains(KeyModifiers::SHIFT);
|
||||
|
||||
Self {
|
||||
key,
|
||||
ctrl,
|
||||
@@ -118,27 +117,34 @@ mod tests {
|
||||
for (from, to) in [
|
||||
(
|
||||
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()),
|
||||
input(Key::Enter, false, false),
|
||||
input(Key::Enter, false, false, false),
|
||||
),
|
||||
(
|
||||
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),
|
||||
input(Key::Home, false, true),
|
||||
input(Key::Home, false, true, false),
|
||||
),
|
||||
(
|
||||
key_event(KeyCode::F(1), KeyModifiers::ALT | KeyModifiers::CONTROL),
|
||||
input(Key::F(1), true, true),
|
||||
key_event(
|
||||
KeyCode::F(1),
|
||||
KeyModifiers::ALT | KeyModifiers::CONTROL | KeyModifiers::SHIFT,
|
||||
),
|
||||
input(Key::F(1), true, true, true),
|
||||
),
|
||||
(
|
||||
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);
|
||||
@@ -150,26 +156,30 @@ mod tests {
|
||||
for (from, to) in [
|
||||
(
|
||||
mouse_event(MouseEventKind::ScrollDown, KeyModifiers::empty()),
|
||||
input(Key::MouseScrollDown, false, false),
|
||||
input(Key::MouseScrollDown, false, false, false),
|
||||
),
|
||||
(
|
||||
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),
|
||||
input(Key::MouseScrollDown, false, true),
|
||||
input(Key::MouseScrollDown, false, true, false),
|
||||
),
|
||||
(
|
||||
mouse_event(
|
||||
MouseEventKind::ScrollUp,
|
||||
KeyModifiers::CONTROL | KeyModifiers::ALT,
|
||||
),
|
||||
input(Key::MouseScrollUp, true, true),
|
||||
input(Key::MouseScrollUp, true, true, false),
|
||||
),
|
||||
(
|
||||
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);
|
||||
@@ -181,16 +191,16 @@ mod tests {
|
||||
for (from, to) in [
|
||||
(
|
||||
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(
|
||||
MouseEventKind::ScrollDown,
|
||||
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);
|
||||
}
|
||||
@@ -201,7 +211,7 @@ mod tests {
|
||||
fn ignore_key_release_event() {
|
||||
let mut from = key_event(KeyCode::Char('a'), KeyModifiers::empty());
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +90,8 @@ impl Default for Key {
|
||||
/// textarea.input(Input {
|
||||
/// key: Key::Char('a'),
|
||||
/// ctrl: true,
|
||||
/// alt: false,shift:false
|
||||
/// alt: false,
|
||||
/// shift: false,
|
||||
/// });
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Default, PartialEq, Hash)]
|
||||
@@ -102,7 +103,7 @@ 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.
|
||||
/// Shift modifier key. `true` means Alt key was pressed.
|
||||
pub shift: bool,
|
||||
}
|
||||
|
||||
@@ -111,12 +112,12 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[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 {
|
||||
key,
|
||||
ctrl,
|
||||
alt,
|
||||
shift: false,
|
||||
shift,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ 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),
|
||||
@@ -49,7 +48,7 @@ impl From<KeyEvent> for Input {
|
||||
key,
|
||||
ctrl,
|
||||
alt,
|
||||
shift,
|
||||
shift: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -90,14 +89,23 @@ mod tests {
|
||||
#[test]
|
||||
fn key_to_input() {
|
||||
for (from, to) in [
|
||||
(KeyEvent::Char('a'), input(Key::Char('a'), false, false)),
|
||||
(KeyEvent::Ctrl('a'), input(Key::Char('a'), true, false)),
|
||||
(KeyEvent::Alt('a'), input(Key::Char('a'), false, true)),
|
||||
(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::BackTab, input(Key::Tab, false, false)),
|
||||
(KeyEvent::Null, input(Key::Null, false, false)),
|
||||
(
|
||||
KeyEvent::Char('a'),
|
||||
input(Key::Char('a'), false, false, false),
|
||||
),
|
||||
(
|
||||
KeyEvent::Ctrl('a'),
|
||||
input(Key::Char('a'), true, 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);
|
||||
}
|
||||
@@ -108,18 +116,24 @@ mod tests {
|
||||
for (from, to) in [
|
||||
(
|
||||
MouseEvent::Press(MouseButton::WheelDown, 1, 1),
|
||||
input(Key::MouseScrollDown, false, false),
|
||||
input(Key::MouseScrollDown, false, false, false),
|
||||
),
|
||||
(
|
||||
MouseEvent::Press(MouseButton::WheelUp, 1, 1),
|
||||
input(Key::MouseScrollUp, false, false),
|
||||
input(Key::MouseScrollUp, false, false, false),
|
||||
),
|
||||
(
|
||||
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);
|
||||
}
|
||||
@@ -130,13 +144,16 @@ mod tests {
|
||||
for (from, to) in [
|
||||
(
|
||||
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)),
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ impl From<KeyEvent> for Input {
|
||||
let ctrl = modifiers.contains(Modifiers::CTRL);
|
||||
let alt = modifiers.contains(Modifiers::ALT);
|
||||
let shift = modifiers.contains(Modifiers::SHIFT);
|
||||
|
||||
Self {
|
||||
key,
|
||||
ctrl,
|
||||
@@ -83,6 +84,7 @@ impl From<MouseEvent> for Input {
|
||||
let ctrl = modifiers.contains(Modifiers::CTRL);
|
||||
let alt = modifiers.contains(Modifiers::ALT);
|
||||
let shift = modifiers.contains(Modifiers::SHIFT);
|
||||
|
||||
Self {
|
||||
key,
|
||||
ctrl,
|
||||
@@ -105,6 +107,7 @@ impl From<PixelMouseEvent> for Input {
|
||||
let ctrl = modifiers.contains(Modifiers::CTRL);
|
||||
let alt = modifiers.contains(Modifiers::ALT);
|
||||
let shift = modifiers.contains(Modifiers::SHIFT);
|
||||
|
||||
Self {
|
||||
key,
|
||||
ctrl,
|
||||
@@ -146,27 +149,34 @@ mod tests {
|
||||
for (from, to) in [
|
||||
(
|
||||
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()),
|
||||
input(Key::Enter, false, false),
|
||||
input(Key::Enter, false, false, false),
|
||||
),
|
||||
(
|
||||
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),
|
||||
input(Key::Home, false, true),
|
||||
input(Key::Home, false, true, false),
|
||||
),
|
||||
(
|
||||
key_event(KeyCode::Function(1), Modifiers::ALT | Modifiers::CTRL),
|
||||
input(Key::F(1), true, true),
|
||||
key_event(
|
||||
KeyCode::Function(1),
|
||||
Modifiers::ALT | Modifiers::CTRL | Modifiers::SHIFT,
|
||||
),
|
||||
input(Key::F(1), true, true, true),
|
||||
),
|
||||
(
|
||||
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);
|
||||
@@ -178,30 +188,37 @@ mod tests {
|
||||
for (from, to) in [
|
||||
(
|
||||
mouse_event(MouseButtons::VERT_WHEEL, Modifiers::empty()),
|
||||
input(Key::MouseScrollDown, false, false),
|
||||
input(Key::MouseScrollDown, false, false, false),
|
||||
),
|
||||
(
|
||||
mouse_event(
|
||||
MouseButtons::VERT_WHEEL | MouseButtons::WHEEL_POSITIVE,
|
||||
Modifiers::empty(),
|
||||
),
|
||||
input(Key::MouseScrollUp, false, false),
|
||||
input(Key::MouseScrollUp, false, false, false),
|
||||
),
|
||||
(
|
||||
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),
|
||||
input(Key::MouseScrollDown, false, true),
|
||||
input(Key::MouseScrollDown, false, true, false),
|
||||
),
|
||||
(
|
||||
mouse_event(MouseButtons::VERT_WHEEL, Modifiers::CTRL | Modifiers::ALT),
|
||||
input(Key::MouseScrollDown, true, true),
|
||||
mouse_event(
|
||||
MouseButtons::VERT_WHEEL,
|
||||
Modifiers::CTRL | Modifiers::ALT | Modifiers::SHIFT,
|
||||
),
|
||||
input(Key::MouseScrollDown, true, true, true),
|
||||
),
|
||||
(
|
||||
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);
|
||||
@@ -216,22 +233,22 @@ mod tests {
|
||||
for (from, to) in [
|
||||
(
|
||||
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())),
|
||||
input(Key::MouseScrollDown, false, false),
|
||||
input(Key::MouseScrollDown, false, false, false),
|
||||
),
|
||||
(
|
||||
InputEvent::PixelMouse(pixel_mouse_event(
|
||||
MouseButtons::VERT_WHEEL,
|
||||
Modifiers::empty(),
|
||||
)),
|
||||
input(Key::MouseScrollDown, false, false),
|
||||
input(Key::MouseScrollDown, false, false, false),
|
||||
),
|
||||
(
|
||||
InputEvent::Paste("x".into()),
|
||||
input(Key::Null, false, false),
|
||||
input(Key::Null, false, false, false),
|
||||
),
|
||||
] {
|
||||
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};
|
||||
|
||||
#[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]
|
||||
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);
|
||||
fn test_copy_single_line() {
|
||||
for i in 0..="abc".len() {
|
||||
for j in i.."abc".len() {
|
||||
let mut t = TextArea::from(["abc"]);
|
||||
|
||||
t.move_cursor(CursorMove::Jump(0, i as u16));
|
||||
t.start_selection();
|
||||
t.move_cursor(CursorMove::Jump(0, j as u16));
|
||||
t.copy();
|
||||
|
||||
assert_eq!(t.yank_text(), &"abc"[i..j], "from {i} to {j}");
|
||||
assert_eq!(t.lines(), ["abc"], "from {i} to {j}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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);
|
||||
fn test_cut_single_line() {
|
||||
for i in 0.."abc".len() {
|
||||
for j in i + 1.."abc".len() {
|
||||
let mut t = TextArea::from(["abc"]);
|
||||
|
||||
t.move_cursor(CursorMove::Jump(0, i as u16));
|
||||
t.start_selection();
|
||||
t.move_cursor(CursorMove::Jump(0, j as u16));
|
||||
t.cut();
|
||||
|
||||
assert_eq!(t.yank_text(), &"abc"[i..j], "from {i} to {j}");
|
||||
|
||||
let mut after = "abc".to_string();
|
||||
after.replace_range(i..j, "");
|
||||
assert_eq!(t.lines(), [after], "from {i} to {j}");
|
||||
assert_eq!(t.cursor(), (0, i));
|
||||
|
||||
t.paste();
|
||||
assert_eq!(t.lines(), ["abc"], "from {i} to {j}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_all_keys() {
|
||||
for test in [
|
||||
fn test_copy_cut_empty() {
|
||||
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
|
||||
"hello world",
|
||||
vec![
|
||||
(Key::End, false, false, true),
|
||||
(Key::Enter, false, false, true),
|
||||
],
|
||||
"",
|
||||
// Initial text
|
||||
&[
|
||||
"ab",
|
||||
"cd",
|
||||
"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",
|
||||
&[
|
||||
"abcd",
|
||||
"ef",
|
||||
][..]
|
||||
),
|
||||
(
|
||||
// word forward select
|
||||
"hello world",
|
||||
vec![
|
||||
(Key::Char('f'), false, true, true),
|
||||
(Key::Char('c'), true, false, false),
|
||||
],
|
||||
"hello ",
|
||||
"hello world",
|
||||
&[
|
||||
"ab",
|
||||
"cd",
|
||||
"ef",
|
||||
][..],
|
||||
(0, 2),
|
||||
(2, 0),
|
||||
"\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),
|
||||
],
|
||||
"world",
|
||||
"hello world",
|
||||
&[
|
||||
"あい",
|
||||
"うえ",
|
||||
"おか",
|
||||
][..],
|
||||
(0, 1),
|
||||
(2, 1),
|
||||
"い\nうえ\nお",
|
||||
&[
|
||||
"あか",
|
||||
][..]
|
||||
),
|
||||
(
|
||||
// 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",
|
||||
&[
|
||||
"あい",
|
||||
"うえ",
|
||||
"おか",
|
||||
][..],
|
||||
(0, 2),
|
||||
(2, 0),
|
||||
"\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),
|
||||
(Key::Char('p'), false, true, true),
|
||||
(Key::Char('x'), true, false, true),
|
||||
],
|
||||
"\nare\nyou",
|
||||
"hello\nworld\n\nhow",
|
||||
&[
|
||||
"あい",
|
||||
"うえ",
|
||||
"おか",
|
||||
][..],
|
||||
(0, 2),
|
||||
(1, 2),
|
||||
"\nうえ",
|
||||
&[
|
||||
"あい",
|
||||
"おか",
|
||||
][..]
|
||||
),
|
||||
(
|
||||
// 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",
|
||||
&[
|
||||
"あい",
|
||||
"うえ",
|
||||
"おか",
|
||||
][..],
|
||||
(0, 2),
|
||||
(1, 1),
|
||||
"\nう",
|
||||
&[
|
||||
"あいえ",
|
||||
"おか",
|
||||
][..]
|
||||
),
|
||||
] {
|
||||
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);
|
||||
(
|
||||
&[
|
||||
"あい",
|
||||
"うえ",
|
||||
"おか",
|
||||
][..],
|
||||
(0, 2),
|
||||
(1, 0),
|
||||
"\n",
|
||||
&[
|
||||
"あいうえ",
|
||||
"おか",
|
||||
][..]
|
||||
),
|
||||
];
|
||||
|
||||
// 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);
|
||||
for test in tests {
|
||||
let (init_text, (srow, scol), (erow, ecol), yanked, after_cut) = test;
|
||||
|
||||
{
|
||||
let mut t = TextArea::from(init_text.iter().map(|s| s.to_string()));
|
||||
t.move_cursor(CursorMove::Jump(srow as _, scol as _));
|
||||
t.start_selection();
|
||||
t.move_cursor(CursorMove::Jump(erow as _, ecol as _));
|
||||
t.copy();
|
||||
|
||||
assert_eq!(t.cursor(), (erow, ecol), "{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(srow as _, scol as _));
|
||||
t.start_selection();
|
||||
t.move_cursor(CursorMove::Jump(erow as _, ecol 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:?}");
|
||||
}
|
||||
|
||||
// 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