Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

implement select #45

Merged
merged 7 commits into from
Nov 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions examples/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ impl<'a> Editor<'a> {
key: Key::Char('g' | 'n'),
ctrl: true,
alt: false,
..
}
| Input { key: Key::Down, .. } => {
if !textarea.search_forward(false) {
Expand All @@ -275,11 +276,13 @@ impl<'a> Editor<'a> {
key: Key::Char('g'),
ctrl: false,
alt: true,
..
}
| Input {
key: Key::Char('p'),
ctrl: true,
alt: false,
..
}
| Input { key: Key::Up, .. } => {
if !textarea.search_back(false) {
Expand Down
37 changes: 29 additions & 8 deletions examples/modal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -75,32 +75,41 @@ fn main() -> io::Result<()> {

term.draw(|f| f.render_widget(textarea.widget(), f.size()))?;

let input = crossterm::event::read()?.into();
let input: Input = crossterm::event::read()?.into();

// since this sample doesnt use input a lot of the time
// it must call should_select

textarea.should_select(&input);
match mode {
Mode::Normal => match input {
// Mappings in normal mode

// note that the navigation keys can be pressed with or without shift
// if the user is selecting text
// so we have to look for 'h' and 'H' etc.
Input {
key: Key::Char('h'),
key: Key::Char('h' | 'H'),
..
} => textarea.move_cursor(CursorMove::Back),
Input {
key: Key::Char('j'),
key: Key::Char('j' | 'J'),
..
} => textarea.move_cursor(CursorMove::Down),
Input {
key: Key::Char('k'),
key: Key::Char('k' | 'K'),
..
} => textarea.move_cursor(CursorMove::Up),
Input {
key: Key::Char('l'),
key: Key::Char('l' | 'L'),
..
} => textarea.move_cursor(CursorMove::Forward),
Input {
key: Key::Char('w'),
key: Key::Char('w' | 'W'),
..
} => textarea.move_cursor(CursorMove::WordForward),
Input {
key: Key::Char('b'),
key: Key::Char('b' | 'B'),
ctrl: false,
..
} => textarea.move_cursor(CursorMove::WordBack),
Expand Down Expand Up @@ -131,6 +140,13 @@ fn main() -> io::Result<()> {
} => {
textarea.paste();
}
Input {
key: Key::Char('y'),
ctrl: false,
..
} => {
textarea.copy();
}
Input {
key: Key::Char('u'),
ctrl: false,
Expand Down Expand Up @@ -166,6 +182,8 @@ fn main() -> io::Result<()> {
key: Key::Char('A'),
..
} => {
// stop selection is called to prevent 'A' being interpreted as selecting
textarea.stop_selection();
textarea.move_cursor(CursorMove::End);
mode = Mode::Insert;
}
Expand All @@ -181,6 +199,8 @@ fn main() -> io::Result<()> {
key: Key::Char('O'),
..
} => {
// stop selection is called to prevent 'O' being interpreted as selecting
textarea.stop_selection();
textarea.move_cursor(CursorMove::Head);
textarea.insert_newline();
textarea.move_cursor(CursorMove::Up);
Expand All @@ -190,6 +210,7 @@ fn main() -> io::Result<()> {
key: Key::Char('I'),
..
} => {
textarea.stop_selection();
textarea.move_cursor(CursorMove::Head);
mode = Mode::Insert;
}
Expand Down
3 changes: 3 additions & 0 deletions examples/tuirs_editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,7 @@ impl<'a> Editor<'a> {
key: Key::Char('g' | 'n'),
ctrl: true,
alt: false,
..
}
| Input { key: Key::Down, .. } => {
if !textarea.search_forward(false) {
Expand All @@ -277,11 +278,13 @@ impl<'a> Editor<'a> {
key: Key::Char('g'),
ctrl: false,
alt: true,
..
}
| Input {
key: Key::Char('p'),
ctrl: true,
alt: false,
..
}
| Input { key: Key::Up, .. } => {
if !textarea.search_back(false) {
Expand Down
95 changes: 88 additions & 7 deletions src/highlight.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,23 @@ use std::iter;
use tui::text::Spans as Line;
use unicode_width::UnicodeWidthChar as _;

#[derive(Debug)]
enum Boundary {
Cursor(Style),
#[cfg(feature = "search")]
Search(Style),
Select(Style),
End,
}

impl Boundary {
fn cmp(&self, other: &Boundary) -> Ordering {
fn rank(b: &Boundary) -> u8 {
match b {
Boundary::Cursor(_) => 2,
Boundary::Cursor(_) => 3,
#[cfg(feature = "search")]
Boundary::Search(_) => 1,
Boundary::Select(_) => 2,
Boundary::End => 0,
}
}
Expand All @@ -36,6 +39,7 @@ impl Boundary {
#[cfg(feature = "search")]
Boundary::Search(s) => Some(*s),
Boundary::End => None,
Boundary::Select(s) => Some(*s),
}
}
}
Expand Down Expand Up @@ -94,16 +98,24 @@ impl DisplayTextBuilder {
pub struct LineHighlighter<'a> {
line: &'a str,
spans: Vec<Span<'a>>,
// reminder - the usize is the start of a section, in BYTES, not chars
boundaries: Vec<(Boundary, usize)>, // TODO: Consider smallvec
style_begin: Style,
cursor_at_end: bool,
cursor_style: Style,
tab_len: u8,
mask: Option<char>,
select_style: Style,
}

impl<'a> LineHighlighter<'a> {
pub fn new(line: &'a str, cursor_style: Style, tab_len: u8, mask: Option<char>) -> Self {
pub fn new(
line: &'a str,
cursor_style: Style,
tab_len: u8,
mask: Option<char>,
select_style: Style,
) -> Self {
Self {
line,
spans: vec![],
Expand All @@ -113,6 +125,7 @@ impl<'a> LineHighlighter<'a> {
cursor_style,
tab_len,
mask,
select_style,
}
}

Expand All @@ -122,6 +135,11 @@ impl<'a> LineHighlighter<'a> {
.push(Span::styled(format!("{}{} ", pad, row + 1), style));
}

pub fn select(&mut self, start: usize, end: usize, style: Style) {
self.boundaries.push((Boundary::Select(style), start));
self.boundaries.push((Boundary::End, end));
}

pub fn cursor_line(&mut self, cursor_col: usize, style: Style) {
if let Some((start, c)) = self.line.char_indices().nth(cursor_col) {
self.boundaries
Expand All @@ -133,6 +151,60 @@ impl<'a> LineHighlighter<'a> {
self.style_begin = style;
}

pub(crate) fn select_line(
&mut self,
row: usize,
line: &str,
start: (usize, usize),
end: (usize, usize),
) {
// is this row selected?

// note that the input coordinates here are in char offsets not bytes
// so a lot of this code is converting from char offsets to byte offsets
if start.0 <= row && end.0 >= row {
let mut indices = line.char_indices();
let llen = line.len();
match (start.0 == row, end.0 == row) {
(true, true) => {
// only line
self.select(
indices.nth(start.1).unwrap_or((llen, 'x')).0,
indices.nth(end.1 - start.1).unwrap_or((llen, 'x')).0,
self.select_style,
);
}
(true, false) => {
// a line in the start of selection

// the +1 extends the highlight one beyond end, to show
// that the newline is included
// same done for middle rows below

self.select(
indices.nth(start.1).unwrap_or((llen, 'x')).0,
llen + 1,
self.select_style,
);
}
(false, true) => {
// last line
if end.1 > 0 {
self.select(
0,
indices.nth(end.1).unwrap_or((llen, 'x')).0,
self.select_style,
);
}
}
(false, false) => {
// in the middle of selection
self.select(0, llen + 1, self.select_style);
}
}
}
}

#[cfg(feature = "search")]
pub fn search(&mut self, matches: impl Iterator<Item = (usize, usize)>, style: Style) {
for (start, end) in matches {
Expand All @@ -153,6 +225,7 @@ impl<'a> LineHighlighter<'a> {
cursor_style,
cursor_at_end,
mask,
select_style,
} = self;
let mut builder = DisplayTextBuilder::new(tab_len, mask);

Expand All @@ -172,10 +245,18 @@ impl<'a> LineHighlighter<'a> {
let mut style = style_begin;
let mut start = 0;
let mut stack = vec![];

let mut dont_add_cursor = false;
for (next_boundary, end) in boundaries {
if start < end {
spans.push(Span::styled(builder.build(&line[start..end]), style));
// add extra select space at line end to indicate
// that the \n will be deleted / included
if end > line.len() && style == select_style {
spans.push(Span::styled(builder.build(&line[start..end - 1]), style));
spans.push(Span::styled(" ", style));
dont_add_cursor = true;
} else {
spans.push(Span::styled(builder.build(&line[start..end]), style));
}
}

style = if let Some(s) = next_boundary.style() {
Expand All @@ -186,13 +267,13 @@ impl<'a> LineHighlighter<'a> {
};
start = end;
}

if start != line.len() {
if start < line.len() {
spans.push(Span::styled(builder.build(&line[start..]), style));
}
if cursor_at_end {
if cursor_at_end && !dont_add_cursor {
spans.push(Span::styled(" ", cursor_style));
}

Line::from(spans)
}
}
Expand Down
17 changes: 15 additions & 2 deletions src/input/crossterm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,15 @@ impl From<KeyEvent> for Input {

let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
let alt = key.modifiers.contains(KeyModifiers::ALT);
let shift = key.modifiers.contains(KeyModifiers::SHIFT);
let key = Key::from(key.code);

Self { key, ctrl, alt }
Self {
key,
ctrl,
alt,
shift,
}
}
}

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

Self {
key,
ctrl,
alt,
shift,
}
}
}

Expand Down
11 changes: 9 additions & 2 deletions src/input/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ impl Default for Key {
/// textarea.input(Input {
/// key: Key::Char('a'),
/// ctrl: true,
/// alt: false,
/// alt: false,shift:false
/// });
/// ```
#[derive(Debug, Clone, Default, PartialEq, Hash)]
Expand All @@ -102,6 +102,8 @@ pub struct Input {
pub ctrl: bool,
/// Alt modifier key. `true` means Alt key was pressed.
pub alt: bool,
// Shift key. `true` means Shift key was pressed.
pub shift: bool,
}

#[cfg(test)]
Expand All @@ -110,7 +112,12 @@ mod tests {

#[allow(dead_code)]
pub(crate) fn input(key: Key, ctrl: bool, alt: bool) -> Input {
Input { key, ctrl, alt }
Input {
key,
ctrl,
alt,
shift: false,
}
}

#[test]
Expand Down
Loading