Skip to content

Commit 5e1cabd

Browse files
committed
Add visual selection effect
1 parent b72d2e6 commit 5e1cabd

File tree

3 files changed

+183
-7
lines changed

3 files changed

+183
-7
lines changed

src/core_editor/editor.rs

+12
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,18 @@ impl Editor {
533533
fn reset_selection(&mut self) {
534534
self.selection_anchor = None;
535535
}
536+
537+
/// If a selection is active returns the selected range, otherwise None.
538+
/// The range is guaranteed to be ascending.
539+
pub fn get_selection(&self) -> Option<(usize, usize)> {
540+
self.selection_anchor.map(|selection_anchor| {
541+
if self.insertion_point() > selection_anchor {
542+
(selection_anchor, self.insertion_point())
543+
} else {
544+
(self.insertion_point(), selection_anchor)
545+
}
546+
})
547+
}
536548
}
537549

538550
#[cfg(test)]

src/engine.rs

+24-7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use std::path::PathBuf;
22

33
use itertools::Itertools;
4+
use nu_ansi_term::{Color, Style};
45

56
use crate::{enums::ReedlineRawEvent, CursorConfig};
67
#[cfg(feature = "bashisms")]
@@ -127,6 +128,9 @@ pub struct Reedline {
127128
// Highlight the edit buffer
128129
highlighter: Box<dyn Highlighter>,
129130

131+
// Style used for visual selection
132+
visual_selection_style: Style,
133+
130134
// Showcase hints based on various strategies (history, language-completion, spellcheck, etc)
131135
hinter: Option<Box<dyn Hinter>>,
132136
hide_hints: bool,
@@ -183,6 +187,7 @@ impl Reedline {
183187
let history = Box::<FileBackedHistory>::default();
184188
let painter = Painter::new(std::io::BufWriter::new(std::io::stderr()));
185189
let buffer_highlighter = Box::<ExampleHighlighter>::default();
190+
let visual_selection_style = Style::new().on(Color::LightGray);
186191
let completer = Box::<DefaultCompleter>::default();
187192
let hinter = None;
188193
let validator = None;
@@ -209,6 +214,7 @@ impl Reedline {
209214
quick_completions: false,
210215
partial_completions: false,
211216
highlighter: buffer_highlighter,
217+
visual_selection_style,
212218
hinter,
213219
hide_hints: false,
214220
validator,
@@ -371,6 +377,13 @@ impl Reedline {
371377
self
372378
}
373379

380+
/// A builder that configures the style used for visual selection
381+
#[must_use]
382+
pub fn with_visual_selection_style(mut self, style: Style) -> Self {
383+
self.visual_selection_style = style;
384+
self
385+
}
386+
374387
/// A builder which configures the history for your instance of the Reedline engine
375388
/// # Example
376389
/// ```rust,no_run
@@ -1638,14 +1651,18 @@ impl Reedline {
16381651
let cursor_position_in_buffer = self.editor.insertion_point();
16391652
let buffer_to_paint = self.editor.get_buffer();
16401653

1641-
let (before_cursor, after_cursor) = self
1654+
let mut styled_text = self
16421655
.highlighter
1643-
.highlight(buffer_to_paint, cursor_position_in_buffer)
1644-
.render_around_insertion_point(
1645-
cursor_position_in_buffer,
1646-
prompt,
1647-
self.use_ansi_coloring,
1648-
);
1656+
.highlight(buffer_to_paint, cursor_position_in_buffer);
1657+
if let Some((from, to)) = self.editor.get_selection() {
1658+
styled_text.style_range(from, to, self.visual_selection_style);
1659+
}
1660+
1661+
let (before_cursor, after_cursor) = styled_text.render_around_insertion_point(
1662+
cursor_position_in_buffer,
1663+
prompt,
1664+
self.use_ansi_coloring,
1665+
);
16491666

16501667
let hint: String = if self.hints_active() {
16511668
self.hinter.as_mut().map_or_else(String::new, |hinter| {

src/painting/styled_text.rs

+147
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use crate::Prompt;
55
use super::utils::strip_ansi;
66

77
/// A representation of a buffer with styling, used for doing syntax highlighting
8+
#[derive(Clone)]
89
pub struct StyledText {
910
/// The component, styled parts of the text
1011
pub buffer: Vec<(Style, String)>,
@@ -27,6 +28,67 @@ impl StyledText {
2728
self.buffer.push(styled_string);
2829
}
2930

31+
/// Style range with the provided style
32+
pub fn style_range(&mut self, from: usize, to: usize, new_style: Style) {
33+
let (from, to) = if from > to { (to, from) } else { (from, to) };
34+
let mut current_idx = 0;
35+
let mut pair_idx = 0;
36+
while pair_idx < self.buffer.len() {
37+
let pair = &mut self.buffer[pair_idx];
38+
let end_idx = current_idx + pair.1.len();
39+
enum Position {
40+
Before,
41+
In,
42+
After,
43+
}
44+
let start_position = if current_idx < from {
45+
Position::Before
46+
} else if current_idx >= to {
47+
Position::After
48+
} else {
49+
Position::In
50+
};
51+
let end_position = if end_idx < from {
52+
Position::Before
53+
} else if end_idx > to {
54+
Position::After
55+
} else {
56+
Position::In
57+
};
58+
match (start_position, end_position) {
59+
(Position::Before, Position::After) => {
60+
let mut in_range = pair.1.split_off(from - current_idx);
61+
let after_range = in_range.split_off(to - current_idx - from);
62+
let in_range = (new_style, in_range);
63+
let after_range = (pair.0, after_range);
64+
self.buffer.insert(pair_idx + 1, in_range);
65+
self.buffer.insert(pair_idx + 2, after_range);
66+
break;
67+
}
68+
(Position::Before, Position::In) => {
69+
let in_range = pair.1.split_off(from - current_idx);
70+
pair_idx += 1; // Additional increment for the split pair, since the new insertion is already correctly styled and can be skipped next iteration
71+
self.buffer.insert(pair_idx, (new_style, in_range));
72+
}
73+
(Position::In, Position::After) => {
74+
let after_range = pair.1.split_off(to - current_idx);
75+
let old_style = pair.0;
76+
pair.0 = new_style;
77+
if !after_range.is_empty() {
78+
self.buffer.insert(pair_idx + 1, (old_style, after_range));
79+
}
80+
break;
81+
}
82+
(Position::In, Position::In) => pair.0 = new_style,
83+
84+
(Position::After, _) => break,
85+
_ => (),
86+
}
87+
current_idx = end_idx;
88+
pair_idx += 1;
89+
}
90+
}
91+
3092
/// Render the styled string. We use the insertion point to render around so that
3193
/// we can properly write out the styled string to the screen and find the correct
3294
/// place to put the cursor. This assumes a logic that prints the first part of the
@@ -109,3 +171,88 @@ fn render_as_string(
109171
}
110172
rendered
111173
}
174+
175+
#[cfg(test)]
176+
mod test {
177+
use nu_ansi_term::{Color, Style};
178+
179+
use crate::StyledText;
180+
181+
fn get_styled_text_template() -> (super::StyledText, Style, Style) {
182+
let before_style = Style::new().on(Color::Black);
183+
let after_style = Style::new().on(Color::Red);
184+
(
185+
super::StyledText {
186+
buffer: vec![
187+
(before_style, "aaa".into()),
188+
(before_style, "bbb".into()),
189+
(before_style, "ccc".into()),
190+
],
191+
},
192+
before_style,
193+
after_style,
194+
)
195+
}
196+
#[test]
197+
fn style_range_partial_update_one_part() {
198+
let (styled_text_template, before_style, after_style) = get_styled_text_template();
199+
let mut styled_text = styled_text_template.clone();
200+
styled_text.style_range(0, 1, after_style);
201+
assert_eq!(styled_text.buffer[0], (after_style, "a".into()));
202+
assert_eq!(styled_text.buffer[1], (before_style, "aa".into()));
203+
assert_eq!(styled_text.buffer[2], (before_style, "bbb".into()));
204+
assert_eq!(styled_text.buffer[3], (before_style, "ccc".into()));
205+
}
206+
#[test]
207+
fn style_range_complete_update_one_part() {
208+
let (styled_text_template, before_style, after_style) = get_styled_text_template();
209+
let mut styled_text = styled_text_template.clone();
210+
styled_text.style_range(0, 3, after_style);
211+
assert_eq!(styled_text.buffer[0], (after_style, "aaa".into()));
212+
assert_eq!(styled_text.buffer[1], (before_style, "bbb".into()));
213+
assert_eq!(styled_text.buffer[2], (before_style, "ccc".into()));
214+
assert_eq!(styled_text.buffer.len(), 3);
215+
}
216+
#[test]
217+
fn style_range_update_over_boundary() {
218+
let (styled_text_template, before_style, after_style) = get_styled_text_template();
219+
let mut styled_text = styled_text_template;
220+
styled_text.style_range(0, 5, after_style);
221+
assert_eq!(styled_text.buffer[0], (after_style, "aaa".into()));
222+
assert_eq!(styled_text.buffer[1], (after_style, "bb".into()));
223+
assert_eq!(styled_text.buffer[2], (before_style, "b".into()));
224+
assert_eq!(styled_text.buffer[3], (before_style, "ccc".into()));
225+
}
226+
#[test]
227+
fn style_range_update_over_part() {
228+
let (styled_text_template, before_style, after_style) = get_styled_text_template();
229+
let mut styled_text = styled_text_template;
230+
styled_text.style_range(1, 7, after_style);
231+
assert_eq!(styled_text.buffer[0], (before_style, "a".into()));
232+
assert_eq!(styled_text.buffer[1], (after_style, "aa".into()));
233+
assert_eq!(styled_text.buffer[2], (after_style, "bbb".into()));
234+
assert_eq!(styled_text.buffer[3], (after_style, "c".into()));
235+
assert_eq!(styled_text.buffer[4], (before_style, "cc".into()));
236+
}
237+
#[test]
238+
fn style_range_last_letter() {
239+
let (_, before_style, after_style) = get_styled_text_template();
240+
let mut styled_text = StyledText {
241+
buffer: vec![(before_style, "asdf".into())],
242+
};
243+
styled_text.style_range(3, 4, after_style);
244+
assert_eq!(styled_text.buffer[0], (before_style, "asd".into()));
245+
assert_eq!(styled_text.buffer[1], (after_style, "f".into()));
246+
}
247+
#[test]
248+
fn style_range_from_second_to_last() {
249+
let (_, before_style, after_style) = get_styled_text_template();
250+
let mut styled_text = StyledText {
251+
buffer: vec![(before_style, "asdf".into())],
252+
};
253+
styled_text.style_range(2, 3, after_style);
254+
assert_eq!(styled_text.buffer[0], (before_style, "as".into()));
255+
assert_eq!(styled_text.buffer[1], (after_style, "d".into()));
256+
assert_eq!(styled_text.buffer[2], (before_style, "f".into()));
257+
}
258+
}

0 commit comments

Comments
 (0)