diff --git a/src/core_editor/editor.rs b/src/core_editor/editor.rs index 5bff2856..3075dcfb 100644 --- a/src/core_editor/editor.rs +++ b/src/core_editor/editor.rs @@ -533,6 +533,18 @@ impl Editor { fn reset_selection(&mut self) { self.selection_anchor = None; } + + /// If a selection is active returns the selected range, otherwise None. + /// The range is guaranteed to be ascending. + pub fn get_selection(&self) -> Option<(usize, usize)> { + self.selection_anchor.map(|selection_anchor| { + if self.insertion_point() > selection_anchor { + (selection_anchor, self.insertion_point()) + } else { + (self.insertion_point(), selection_anchor) + } + }) + } } #[cfg(test)] diff --git a/src/engine.rs b/src/engine.rs index 43555eec..6ec73d3b 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -1,6 +1,7 @@ use std::path::PathBuf; use itertools::Itertools; +use nu_ansi_term::{Color, Style}; use crate::{enums::ReedlineRawEvent, CursorConfig}; #[cfg(feature = "bashisms")] @@ -127,6 +128,9 @@ pub struct Reedline { // Highlight the edit buffer highlighter: Box, + // Style used for visual selection + visual_selection_style: Style, + // Showcase hints based on various strategies (history, language-completion, spellcheck, etc) hinter: Option>, hide_hints: bool, @@ -183,6 +187,7 @@ impl Reedline { let history = Box::::default(); let painter = Painter::new(std::io::BufWriter::new(std::io::stderr())); let buffer_highlighter = Box::::default(); + let visual_selection_style = Style::new().on(Color::LightGray); let completer = Box::::default(); let hinter = None; let validator = None; @@ -209,6 +214,7 @@ impl Reedline { quick_completions: false, partial_completions: false, highlighter: buffer_highlighter, + visual_selection_style, hinter, hide_hints: false, validator, @@ -371,6 +377,13 @@ impl Reedline { self } + /// A builder that configures the style used for visual selection + #[must_use] + pub fn with_visual_selection_style(mut self, style: Style) -> Self { + self.visual_selection_style = style; + self + } + /// A builder which configures the history for your instance of the Reedline engine /// # Example /// ```rust,no_run @@ -1638,14 +1651,18 @@ impl Reedline { let cursor_position_in_buffer = self.editor.insertion_point(); let buffer_to_paint = self.editor.get_buffer(); - let (before_cursor, after_cursor) = self + let mut styled_text = self .highlighter - .highlight(buffer_to_paint, cursor_position_in_buffer) - .render_around_insertion_point( - cursor_position_in_buffer, - prompt, - self.use_ansi_coloring, - ); + .highlight(buffer_to_paint, cursor_position_in_buffer); + if let Some((from, to)) = self.editor.get_selection() { + styled_text.style_range(from, to, self.visual_selection_style); + } + + let (before_cursor, after_cursor) = styled_text.render_around_insertion_point( + cursor_position_in_buffer, + prompt, + self.use_ansi_coloring, + ); let hint: String = if self.hints_active() { self.hinter.as_mut().map_or_else(String::new, |hinter| { diff --git a/src/painting/styled_text.rs b/src/painting/styled_text.rs index eeb88071..f6a5afa0 100644 --- a/src/painting/styled_text.rs +++ b/src/painting/styled_text.rs @@ -5,6 +5,7 @@ use crate::Prompt; use super::utils::strip_ansi; /// A representation of a buffer with styling, used for doing syntax highlighting +#[derive(Clone)] pub struct StyledText { /// The component, styled parts of the text pub buffer: Vec<(Style, String)>, @@ -27,6 +28,67 @@ impl StyledText { self.buffer.push(styled_string); } + /// Style range with the provided style + pub fn style_range(&mut self, from: usize, to: usize, new_style: Style) { + let (from, to) = if from > to { (to, from) } else { (from, to) }; + let mut current_idx = 0; + let mut pair_idx = 0; + while pair_idx < self.buffer.len() { + let pair = &mut self.buffer[pair_idx]; + let end_idx = current_idx + pair.1.len(); + enum Position { + Before, + In, + After, + } + let start_position = if current_idx < from { + Position::Before + } else if current_idx >= to { + Position::After + } else { + Position::In + }; + let end_position = if end_idx < from { + Position::Before + } else if end_idx > to { + Position::After + } else { + Position::In + }; + match (start_position, end_position) { + (Position::Before, Position::After) => { + let mut in_range = pair.1.split_off(from - current_idx); + let after_range = in_range.split_off(to - current_idx - from); + let in_range = (new_style, in_range); + let after_range = (pair.0, after_range); + self.buffer.insert(pair_idx + 1, in_range); + self.buffer.insert(pair_idx + 2, after_range); + break; + } + (Position::Before, Position::In) => { + let in_range = pair.1.split_off(from - current_idx); + pair_idx += 1; // Additional increment for the split pair, since the new insertion is already correctly styled and can be skipped next iteration + self.buffer.insert(pair_idx, (new_style, in_range)); + } + (Position::In, Position::After) => { + let after_range = pair.1.split_off(to - current_idx); + let old_style = pair.0; + pair.0 = new_style; + if !after_range.is_empty() { + self.buffer.insert(pair_idx + 1, (old_style, after_range)); + } + break; + } + (Position::In, Position::In) => pair.0 = new_style, + + (Position::After, _) => break, + _ => (), + } + current_idx = end_idx; + pair_idx += 1; + } + } + /// Render the styled string. We use the insertion point to render around so that /// we can properly write out the styled string to the screen and find the correct /// place to put the cursor. This assumes a logic that prints the first part of the @@ -109,3 +171,88 @@ fn render_as_string( } rendered } + +#[cfg(test)] +mod test { + use nu_ansi_term::{Color, Style}; + + use crate::StyledText; + + fn get_styled_text_template() -> (super::StyledText, Style, Style) { + let before_style = Style::new().on(Color::Black); + let after_style = Style::new().on(Color::Red); + ( + super::StyledText { + buffer: vec![ + (before_style, "aaa".into()), + (before_style, "bbb".into()), + (before_style, "ccc".into()), + ], + }, + before_style, + after_style, + ) + } + #[test] + fn style_range_partial_update_one_part() { + let (styled_text_template, before_style, after_style) = get_styled_text_template(); + let mut styled_text = styled_text_template.clone(); + styled_text.style_range(0, 1, after_style); + assert_eq!(styled_text.buffer[0], (after_style, "a".into())); + assert_eq!(styled_text.buffer[1], (before_style, "aa".into())); + assert_eq!(styled_text.buffer[2], (before_style, "bbb".into())); + assert_eq!(styled_text.buffer[3], (before_style, "ccc".into())); + } + #[test] + fn style_range_complete_update_one_part() { + let (styled_text_template, before_style, after_style) = get_styled_text_template(); + let mut styled_text = styled_text_template.clone(); + styled_text.style_range(0, 3, after_style); + assert_eq!(styled_text.buffer[0], (after_style, "aaa".into())); + assert_eq!(styled_text.buffer[1], (before_style, "bbb".into())); + assert_eq!(styled_text.buffer[2], (before_style, "ccc".into())); + assert_eq!(styled_text.buffer.len(), 3); + } + #[test] + fn style_range_update_over_boundary() { + let (styled_text_template, before_style, after_style) = get_styled_text_template(); + let mut styled_text = styled_text_template; + styled_text.style_range(0, 5, after_style); + assert_eq!(styled_text.buffer[0], (after_style, "aaa".into())); + assert_eq!(styled_text.buffer[1], (after_style, "bb".into())); + assert_eq!(styled_text.buffer[2], (before_style, "b".into())); + assert_eq!(styled_text.buffer[3], (before_style, "ccc".into())); + } + #[test] + fn style_range_update_over_part() { + let (styled_text_template, before_style, after_style) = get_styled_text_template(); + let mut styled_text = styled_text_template; + styled_text.style_range(1, 7, after_style); + assert_eq!(styled_text.buffer[0], (before_style, "a".into())); + assert_eq!(styled_text.buffer[1], (after_style, "aa".into())); + assert_eq!(styled_text.buffer[2], (after_style, "bbb".into())); + assert_eq!(styled_text.buffer[3], (after_style, "c".into())); + assert_eq!(styled_text.buffer[4], (before_style, "cc".into())); + } + #[test] + fn style_range_last_letter() { + let (_, before_style, after_style) = get_styled_text_template(); + let mut styled_text = StyledText { + buffer: vec![(before_style, "asdf".into())], + }; + styled_text.style_range(3, 4, after_style); + assert_eq!(styled_text.buffer[0], (before_style, "asd".into())); + assert_eq!(styled_text.buffer[1], (after_style, "f".into())); + } + #[test] + fn style_range_from_second_to_last() { + let (_, before_style, after_style) = get_styled_text_template(); + let mut styled_text = StyledText { + buffer: vec![(before_style, "asdf".into())], + }; + styled_text.style_range(2, 3, after_style); + assert_eq!(styled_text.buffer[0], (before_style, "as".into())); + assert_eq!(styled_text.buffer[1], (after_style, "d".into())); + assert_eq!(styled_text.buffer[2], (before_style, "f".into())); + } +}