diff options
author | Tim Oram <dev@mitmaro.ca> | 2022-03-19 03:23:05 -0230 |
---|---|---|
committer | Tim Oram <dev@mitmaro.ca> | 2022-03-19 03:57:28 -0230 |
commit | e12c2eb2ff7ddde82c65931e1135384bd525f6b1 (patch) | |
tree | cbaf981bead90cb5c101f29f8ddc8ea3e9f2e3c0 | |
parent | ced9c585fb5dfb711f4b594ba4f8809632884a2e (diff) |
Extract editable line from edit component
The edit component contains the functionality to edit a line, which is
useful outside the context of that component. This extracts that
functionality into a shared location.
-rw-r--r-- | src/core/src/components/edit/mod.rs | 148 | ||||
-rw-r--r-- | src/core/src/components/edit/tests.rs | 472 | ||||
-rw-r--r-- | src/core/src/components/mod.rs | 1 | ||||
-rw-r--r-- | src/core/src/components/shared/editable_line.rs | 598 | ||||
-rw-r--r-- | src/core/src/components/shared/mod.rs | 3 |
5 files changed, 633 insertions, 589 deletions
diff --git a/src/core/src/components/edit/mod.rs b/src/core/src/components/edit/mod.rs index b1dc884..1436651 100644 --- a/src/core/src/components/edit/mod.rs +++ b/src/core/src/components/edit/mod.rs @@ -4,18 +4,22 @@ mod tests; use display::DisplayColor; use input::{Event, InputOptions, KeyCode, KeyEvent, KeyModifiers}; use lazy_static::lazy_static; -use unicode_segmentation::UnicodeSegmentation; use view::{LineSegment, ViewData, ViewDataUpdater, ViewLine}; +use crate::components::shared::EditableLine; + lazy_static! { pub static ref INPUT_OPTIONS: InputOptions = InputOptions::RESIZE; } +const FINISH_EVENT: Event = Event::Key(KeyEvent { + code: KeyCode::Enter, + modifiers: KeyModifiers::NONE, +}); + pub(crate) struct Edit { - content: String, - cursor_position: usize, + editable_line: EditableLine, finished: bool, - label: Option<String>, view_data: ViewData, } @@ -25,10 +29,8 @@ impl Edit { updater.set_show_title(true); }); Self { - content: String::from(""), - cursor_position: 0, + editable_line: EditableLine::new(), finished: false, - label: None, view_data, } } @@ -38,48 +40,15 @@ impl Edit { F: FnOnce(&mut ViewDataUpdater<'_>), G: FnOnce(&mut ViewDataUpdater<'_>), { - let line = self.content.as_str(); - let pointer = self.cursor_position; - - let graphemes = UnicodeSegmentation::graphemes(line, true); - - let start = graphemes.clone().take(pointer).collect::<String>(); - let indicator = graphemes.clone().skip(pointer).take(1).collect::<String>(); - let end = graphemes.skip(pointer + 1).collect::<String>(); - - let mut segments = vec![]; - if let Some(label) = self.label.as_ref() { - segments.push(LineSegment::new_with_color_and_style( - label.as_str(), - DisplayColor::Normal, - true, - false, - false, - )); - } - if !start.is_empty() { - segments.push(LineSegment::new(start.as_str())); - } - segments.push( - if indicator.is_empty() { - LineSegment::new_with_color_and_style(" ", DisplayColor::Normal, false, true, false) - } - else { - LineSegment::new_with_color_and_style(indicator.as_str(), DisplayColor::Normal, false, true, false) - }, - ); - if !end.is_empty() { - segments.push(LineSegment::new(end.as_str())); - } self.view_data.update_view_data(|updater| { updater.clear(); before_build(updater); - updater.push_line(ViewLine::from(segments)); + updater.push_line(ViewLine::from(self.editable_line.line_segments())); updater.push_trailing_line(ViewLine::new_pinned(vec![LineSegment::new_with_color( "Enter to finish", DisplayColor::IndicatorColor, )])); - updater.ensure_column_visible(pointer); + updater.ensure_column_visible(self.editable_line.cursor_position()); updater.ensure_line_visible(0); after_build(updater); }); @@ -91,95 +60,30 @@ impl Edit { } pub(crate) fn handle_event(&mut self, event: Event) { - match event { - Event::Key(KeyEvent { - code: KeyCode::Backspace, - modifiers: KeyModifiers::NONE, - }) => { - if self.cursor_position != 0 { - let start = UnicodeSegmentation::graphemes(self.content.as_str(), true) - .take(self.cursor_position - 1) - .collect::<String>(); - let end = UnicodeSegmentation::graphemes(self.content.as_str(), true) - .skip(self.cursor_position) - .collect::<String>(); - self.content = format!("{}{}", start, end); - self.cursor_position -= 1; - } - }, - Event::Key(KeyEvent { - code: KeyCode::Delete, - modifiers: KeyModifiers::NONE, - }) => { - let length = UnicodeSegmentation::graphemes(self.content.as_str(), true).count(); - if self.cursor_position != length { - let start = UnicodeSegmentation::graphemes(self.content.as_str(), true) - .take(self.cursor_position) - .collect::<String>(); - let end = UnicodeSegmentation::graphemes(self.content.as_str(), true) - .skip(self.cursor_position + 1) - .collect::<String>(); - self.content = format!("{}{}", start, end); - } - }, - Event::Key(KeyEvent { - code: KeyCode::Home, - modifiers: KeyModifiers::NONE, - }) => self.cursor_position = 0, - Event::Key(KeyEvent { - code: KeyCode::End, - modifiers: KeyModifiers::NONE, - }) => self.cursor_position = UnicodeSegmentation::graphemes(self.content.as_str(), true).count(), - Event::Key(KeyEvent { - code: KeyCode::Right, - modifiers: KeyModifiers::NONE, - }) => { - let length = UnicodeSegmentation::graphemes(self.content.as_str(), true).count(); - if self.cursor_position < length { - self.cursor_position += 1; - } - }, - Event::Key(KeyEvent { - code: KeyCode::Left, - modifiers: KeyModifiers::NONE, - }) => { - if self.cursor_position != 0 { - self.cursor_position -= 1; - } - }, - Event::Key(KeyEvent { - code: KeyCode::Enter, - modifiers: KeyModifiers::NONE, - }) => self.finished = true, - Event::Key(KeyEvent { - code: KeyCode::Char(c), - modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT, - }) => { - let start = UnicodeSegmentation::graphemes(self.content.as_str(), true) - .take(self.cursor_position) - .collect::<String>(); - let end = UnicodeSegmentation::graphemes(self.content.as_str(), true) - .skip(self.cursor_position) - .collect::<String>(); - self.content = format!("{}{}{}", start, c, end); - self.cursor_position += 1; - }, - _ => {}, + if event == FINISH_EVENT { + self.finished = true; + } + else { + self.editable_line.handle_event(event); } } pub(crate) fn set_label(&mut self, label: &str) { - self.label = Some(String::from(label)); + self.editable_line.set_label(LineSegment::new_with_color_and_style( + label, + DisplayColor::Normal, + true, + false, + false, + )); } pub(crate) fn set_content(&mut self, content: &str) { - self.content = String::from(content); - self.cursor_position = UnicodeSegmentation::graphemes(content, true).count(); + self.editable_line.set_content(content); } pub(crate) fn clear(&mut self) { - self.content.clear(); - self.cursor_position = 0; + self.editable_line.clear(); self.finished = false; } @@ -188,6 +92,6 @@ impl Edit { } pub(crate) fn get_content(&self) -> String { - self.content.clone() + self.editable_line.get_content() } } diff --git a/src/core/src/components/edit/tests.rs b/src/core/src/components/edit/tests.rs index 9fb5e79..f491211 100644 --- a/src/core/src/components/edit/tests.rs +++ b/src/core/src/components/edit/tests.rs @@ -2,29 +2,6 @@ use view::assert_rendered_output; use super::*; -fn handle_events(module: &mut Edit, events: &[Event]) { - for event in events { - module.handle_event(*event); - } -} - -#[test] -fn with_label() { - let mut module = Edit::new(); - module.set_content("foobar"); - module.set_label("Label: "); - let view_data = module.get_view_data(); - assert_rendered_output!( - Options AssertRenderOptions::INCLUDE_TRAILING_WHITESPACE, - view_data, - "{TITLE}", - "{BODY}", - "{Normal,Dimmed}Label: {Normal}foobar{Normal,Underline} ", - "{TRAILING}", - "{IndicatorColor}Enter to finish" - ); -} - #[test] fn with_before_and_after_build() { let mut module = Edit::new(); @@ -51,473 +28,35 @@ fn with_before_and_after_build() { } #[test] -fn move_cursor_end() { - let mut module = Edit::new(); - module.set_content("foobar"); - module.handle_event(Event::from(KeyCode::Right)); - let view_data = module.get_view_data(); - assert_rendered_output!( - Options AssertRenderOptions::INCLUDE_TRAILING_WHITESPACE, - view_data, - "{TITLE}", - "{BODY}", - "{Normal}foobar{Normal,Underline} ", - "{TRAILING}", - "{IndicatorColor}Enter to finish" - ); -} - -#[test] -fn move_cursor_1_left() { +fn edit_event() { let mut module = Edit::new(); module.set_content("foobar"); module.handle_event(Event::from(KeyCode::Left)); let view_data = module.get_view_data(); - assert_rendered_output!( - view_data, - "{TITLE}", - "{BODY}", - "{Normal}fooba{Normal,Underline}r", - "{TRAILING}", - "{IndicatorColor}Enter to finish" - ); -} - -#[test] -fn move_cursor_2_from_start() { - let mut module = Edit::new(); - module.set_content("foobar"); - handle_events(&mut module, &[Event::from(KeyCode::Left); 2]); - let view_data = module.get_view_data(); - assert_rendered_output!( - view_data, - "{TITLE}", - "{BODY}", - "{Normal}foob{Normal,Underline}a{Normal}r", - "{TRAILING}", - "{IndicatorColor}Enter to finish" - ); -} - -#[test] -fn move_cursor_1_from_start() { - let mut module = Edit::new(); - module.set_content("foobar"); - handle_events(&mut module, &[Event::from(KeyCode::Left); 5]); - let view_data = module.get_view_data(); - assert_rendered_output!( - view_data, - "{TITLE}", - "{BODY}", - "{Normal}f{Normal,Underline}o{Normal}obar", - "{TRAILING}", - "{IndicatorColor}Enter to finish" - ); -} - -#[test] -fn move_cursor_to_start() { - let mut module = Edit::new(); - module.set_content("foobar"); - handle_events(&mut module, &[Event::from(KeyCode::Left); 6]); - let view_data = module.get_view_data(); - assert_rendered_output!( - view_data, - "{TITLE}", - "{BODY}", - "{Normal,Underline}f{Normal}oobar", - "{TRAILING}", - "{IndicatorColor}Enter to finish" - ); -} - -#[test] -fn move_cursor_to_home() { - let mut module = Edit::new(); - module.set_content("foobar"); - module.handle_event(Event::from(KeyCode::Home)); - let view_data = module.get_view_data(); - assert_rendered_output!( - view_data, - "{TITLE}", - "{BODY}", - "{Normal,Underline}f{Normal}oobar", - "{TRAILING}", - "{IndicatorColor}Enter to finish" - ); -} - -#[test] -fn move_cursor_to_end() { - let mut module = Edit::new(); - module.set_content("foobar"); - handle_events(&mut module, &[ - Event::from(KeyCode::Left), - Event::from(KeyCode::Left), - Event::from(KeyCode::Left), - Event::from(KeyCode::End), - ]); - let view_data = module.get_view_data(); - assert_rendered_output!( - Options AssertRenderOptions::INCLUDE_TRAILING_WHITESPACE, - view_data, - "{TITLE}", - "{BODY}", - "{Normal}foobar{Normal,Underline} ", - "{TRAILING}", - "{IndicatorColor}Enter to finish" - ); -} -#[test] -fn move_cursor_on_empty_content() { - let mut module = Edit::new(); - handle_events(&mut module, &[ - Event::from(KeyCode::Left), - Event::from(KeyCode::Right), - Event::from(KeyCode::End), - Event::from(KeyCode::Home), - ]); - let view_data = module.get_view_data(); assert_rendered_output!( Options AssertRenderOptions::INCLUDE_TRAILING_WHITESPACE, view_data, "{TITLE}", "{BODY}", - "{Normal,Underline} ", - "{TRAILING}", - "{IndicatorColor}Enter to finish" - ); -} - -#[test] -fn move_cursor_attempt_past_start() { - let mut module = Edit::new(); - module.set_content("foobar"); - handle_events(&mut module, &[Event::from(KeyCode::Left); 10]); - let view_data = module.get_view_data(); - assert_rendered_output!( - view_data, - "{TITLE}", - "{BODY}", - "{Normal,Underline}f{Normal}oobar", + "{Normal}fooba{Normal,Underline}r", "{TRAILING}", "{IndicatorColor}Enter to finish" ); } #[test] -fn move_cursor_attempt_past_end() { +fn finish_event() { let mut module = Edit::new(); module.set_content("foobar"); - handle_events(&mut module, &[Event::from(KeyCode::Right); 10]); - let view_data = module.get_view_data(); - assert_rendered_output!( - Options AssertRenderOptions::INCLUDE_TRAILING_WHITESPACE, - view_data, - "{TITLE}", - "{BODY}", - "{Normal}foobar{Normal,Underline} ", - "{TRAILING}", - "{IndicatorColor}Enter to finish" - ); -} - -#[test] -fn multiple_width_unicode_single_width() { - let mut module = Edit::new(); - module.set_content("a🗳b"); - handle_events(&mut module, &[Event::from(KeyCode::Left); 2]); - let view_data = module.get_view_data(); - assert_rendered_output!( - view_data, - "{TITLE}", - "{BODY}", - "{Normal}a{Normal,Underline}🗳{Normal}b", - "{TRAILING}", - "{IndicatorColor}Enter to finish" - ); -} - -#[test] -fn multiple_width_unicode_emoji() { - let mut module = Edit::new(); - module.set_content("a😀b"); - handle_events(&mut module, &[Event::from(KeyCode::Left); 2]); - let view_data = module.get_view_data(); - assert_rendered_output!( - view_data, - "{TITLE}", - "{BODY}", - "{Normal}a{Normal,Underline}😀{Normal}b", - "{TRAILING}", - "{IndicatorColor}Enter to finish" - ); -} - -#[test] -fn add_character_end() { - let mut module = Edit::new(); - module.set_content("abcd"); - module.handle_event(Event::from('x')); - let view_data = module.get_view_data(); - assert_rendered_output!( - Options AssertRenderOptions::INCLUDE_TRAILING_WHITESPACE, - view_data, - "{TITLE}", - "{BODY}", - "{Normal}abcdx{Normal,Underline} ", - "{TRAILING}", - "{IndicatorColor}Enter to finish" - ); -} - -#[test] -fn add_character_one_from_end() { - let mut module = Edit::new(); - module.set_content("abcd"); - handle_events(&mut module, &[Event::from(KeyCode::Left), Event::from('x')]); - let view_data = module.get_view_data(); - assert_rendered_output!( - view_data, - "{TITLE}", - "{BODY}", - "{Normal}abcx{Normal,Underline}d", - "{TRAILING}", - "{IndicatorColor}Enter to finish" - ); -} - -#[test] -fn add_character_one_from_start() { - let mut module = Edit::new(); - module.set_content("abcd"); - handle_events(&mut module, &[ - Event::from(KeyCode::Left), - Event::from(KeyCode::Left), - Event::from(KeyCode::Left), - Event::from('x'), - ]); - let view_data = module.get_view_data(); - assert_rendered_output!( - view_data, - "{TITLE}", - "{BODY}", - "{Normal}ax{Normal,Underline}b{Normal}cd", - "{TRAILING}", - "{IndicatorColor}Enter to finish" - ); -} - -#[test] -fn add_character_at_start() { - let mut module = Edit::new(); - module.set_content("abcd"); - handle_events(&mut module, &[ - Event::from(KeyCode::Left), - Event::from(KeyCode::Left), - Event::from(KeyCode::Left), - Event::from(KeyCode::Left), - Event::from('x'), - ]); - let view_data = module.get_view_data(); - assert_rendered_output!( - view_data, - "{TITLE}", - "{BODY}", - "{Normal}x{Normal,Underline}a{Normal}bcd", - "{TRAILING}", - "{IndicatorColor}Enter to finish" - ); -} - -#[test] -fn add_character_uppercase() { - let mut module = Edit::new(); - module.set_content("abcd"); - module.handle_event(Event::Key(KeyEvent { - code: input::KeyCode::Char('X'), - modifiers: input::KeyModifiers::SHIFT, - })); - let view_data = module.get_view_data(); - assert_rendered_output!( - Options AssertRenderOptions::INCLUDE_TRAILING_WHITESPACE, - view_data, - "{TITLE}", - "{BODY}", - "{Normal}abcdX{Normal,Underline} ", - "{TRAILING}", - "{IndicatorColor}Enter to finish" - ); -} - -#[test] -fn backspace_at_end() { - let mut module = Edit::new(); - module.set_content("abcd"); - module.handle_event(Event::from(KeyCode::Backspace)); - let view_data = module.get_view_data(); - assert_rendered_output!( - Options AssertRenderOptions::INCLUDE_TRAILING_WHITESPACE, - view_data, - "{TITLE}", - "{BODY}", - "{Normal}abc{Normal,Underline} ", - "{TRAILING}", - "{IndicatorColor}Enter to finish" - ); -} - -#[test] -fn backspace_one_from_end() { - let mut module = Edit::new(); - module.set_content("abcd"); - handle_events(&mut module, &[ - Event::from(KeyCode::Left), - Event::from(KeyCode::Backspace), - ]); - let view_data = module.get_view_data(); - assert_rendered_output!( - view_data, - "{TITLE}", - "{BODY}", - "{Normal}ab{Normal,Underline}d", - "{TRAILING}", - "{IndicatorColor}Enter to finish" - ); -} - -#[test] -fn backspace_one_from_start() { - let mut module = Edit::new(); - module.set_content("abcd"); - handle_events(&mut module, &[ - Event::from(KeyCode::Left), - Event::from(KeyCode::Left), - Event::from(KeyCode::Left), - Event::from(KeyCode::Backspace), - ]); - let view_data = module.get_view_data(); - assert_rendered_output!( - view_data, - "{TITLE}", - "{BODY}", - "{Normal,Underline}b{Normal}cd", - "{TRAILING}", - "{IndicatorColor}Enter to finish" - ); -} - -#[test] -fn backspace_at_start() { - let mut module = Edit::new(); - module.set_content("abcd"); - handle_events(&mut module, &[ - Event::from(KeyCode::Left), - Event::from(KeyCode::Left), - Event::from(KeyCode::Left), - Event::from(KeyCode::Left), - Event::from(KeyCode::Backspace), - ]); - let view_data = module.get_view_data(); - assert_rendered_output!( - view_data, - "{TITLE}", - "{BODY}", - "{Normal,Underline}a{Normal}bcd", - "{TRAILING}", - "{IndicatorColor}Enter to finish" - ); -} - -#[test] -fn delete_at_end() { - let mut module = Edit::new(); - module.set_content("abcd"); - module.handle_event(Event::from(KeyCode::Delete)); - let view_data = module.get_view_data(); - assert_rendered_output!( - Options AssertRenderOptions::INCLUDE_TRAILING_WHITESPACE, - view_data, - "{TITLE}", - "{BODY}", - "{Normal}abcd{Normal,Underline} ", - "{TRAILING}", - "{IndicatorColor}Enter to finish" - ); -} - -#[test] -fn delete_last_character() { - let mut module = Edit::new(); - module.set_content("abcd"); - handle_events(&mut module, &[Event::from(KeyCode::Left), Event::from(KeyCode::Delete)]); - let view_data = module.get_view_data(); - assert_rendered_output!( - Options AssertRenderOptions::INCLUDE_TRAILING_WHITESPACE, - view_data, - "{TITLE}", - "{BODY}", - "{Normal}abc{Normal,Underline} ", - "{TRAILING}", - "{IndicatorColor}Enter to finish" - ); -} - -#[test] -fn delete_second_character() { - let mut module = Edit::new(); - module.set_content("abcd"); - handle_events(&mut module, &[ - Event::from(KeyCode::Left), - Event::from(KeyCode::Left), - Event::from(KeyCode::Left), - Event::from(KeyCode::Delete), - ]); - let view_data = module.get_view_data(); - assert_rendered_output!( - view_data, - "{TITLE}", - "{BODY}", - "{Normal}a{Normal,Underline}c{Normal}d", - "{TRAILING}", - "{IndicatorColor}Enter to finish" - ); -} - -#[test] -fn delete_first_character() { - let mut module = Edit::new(); - module.set_content("abcd"); - handle_events(&mut module, &[ - Event::from(KeyCode::Left), - Event::from(KeyCode::Left), - Event::from(KeyCode::Left), - Event::from(KeyCode::Left), - Event::from(KeyCode::Delete), - ]); - let view_data = module.get_view_data(); - assert_rendered_output!( - view_data, - "{TITLE}", - "{BODY}", - "{Normal,Underline}b{Normal}cd", - "{TRAILING}", - "{IndicatorColor}Enter to finish" - ); -} - -#[test] -fn ignore_other_input() { - let mut module = Edit::new(); - module.handle_event(Event::from(KeyCode::Null)); + module.handle_event(Event::from(KeyCode::Enter)); + assert!(module.is_finished()); } #[test] fn set_get_content() { let mut module = Edit::new(); module.set_content("abcd"); - assert_eq!(module.cursor_position, 4); assert_eq!(module.get_content(), "abcd"); } @@ -526,6 +65,5 @@ fn clear_content() { let mut module = Edit::new(); module.set_content("abcd"); module.clear(); - assert_eq!(module.cursor_position, 0); assert_eq!(module.get_content(), ""); } diff --git a/src/core/src/components/mod.rs b/src/core/src/components/mod.rs index 1020e77..6627ff9 100644 --- a/src/core/src/components/mod.rs +++ b/src/core/src/components/mod.rs @@ -2,3 +2,4 @@ pub(crate) mod choice; pub(crate) mod confirm; pub(crate) mod edit; pub(crate) mod help; +mod shared; diff --git a/src/core/src/components/shared/editable_line.rs b/src/core/src/components/shared/editable_line.rs new file mode 100644 index 0000000..1a39f13 --- /dev/null +++ b/src/core/src/components/shared/editable_line.rs @@ -0,0 +1,598 @@ +use display::DisplayColor; +use input::{Event, KeyCode, KeyEvent, KeyModifiers}; +use unicode_segmentation::UnicodeSegmentation; +use view::LineSegment; + +pub(crate) struct EditableLine { + content: String, + cursor_position: usize, + label: Option<LineSegment>, +} + +impl EditableLine { + pub(crate) fn new() -> Self { + Self { + content: String::from(""), + cursor_position: 0, + label: None, + } + } + + pub(crate) fn set_label(&mut self, label: LineSegment) { + self.label = Some(label); + } + + pub(crate) fn set_content(&mut self, content: &str) { + self.content = String::from(content); + self.cursor_position = UnicodeSegmentation::graphemes(content, true).count(); + } + + pub(crate) fn clear(&mut self) { + self.content.clear(); + self.cursor_position = 0; + } + + pub(crate) fn get_content(&self) -> String { + self.content.clone() + } + + pub(crate) const fn cursor_position(&self) -> usize { + self.cursor_position + } + + pub(crate) fn line_segments(&self) -> Vec<LineSegment> { + let line = self.content.as_str(); + let pointer = self.cursor_position; + + let graphemes = UnicodeSegmentation::graphemes(line, true); + + let start = graphemes.clone().take(pointer).collect::<String>(); + let indicator = graphemes.clone().skip(pointer).take(1).collect::<String>(); + let end = graphemes.skip(pointer + 1).collect::<String>(); + + let mut segments = vec![]; + if let Some(label) = self.label.as_ref() { + segments.push(label.clone()); + } + if !start.is_empty() { + segments.push(LineSegment::new(start.as_str())); + } + segments.push( + if indicator.is_empty() { + LineSegment::new_with_color_and_style(" ", DisplayColor::Normal, false, true, false) + } + else { + LineSegment::new_with_color_and_style(indicator.as_str(), DisplayColor::Normal, false, true, false) + }, + ); + if !end.is_empty() { + segments.push(LineSegment::new(end.as_str())); + } + + segments + } + + pub(crate) fn handle_event(&mut self, event: Event) { + match event { + Event::Key(KeyEvent { + code: KeyCode::Backspace, + modifiers: KeyModifiers::NONE, + }) => { + if self.cursor_position != 0 { + let start = UnicodeSegmentation::graphemes(self.content.as_str(), true) + .take(self.cursor_position - 1) + .collect::<String>(); + let end = UnicodeSegmentation::graphemes(self.content.as_str(), true) + .skip(self.cursor_position) + .collect::<String>(); + self.content = format!("{}{}", start, end); + self.cursor_position -= 1; + } + }, + Event::Key(KeyEvent { + code: KeyCode::Delete, + modifiers: KeyModifiers::NONE, + }) => { + let length = UnicodeSegmentation::graphemes(self.content.as_str(), true).count(); + if self.cursor_position != length { + let start = UnicodeSegmentation::graphemes(self.content.as_str(), true) + .take(self.cursor_position) + .collect::<String>(); + let end = UnicodeSegmentation::graphemes(self.content.as_str(), true) + .skip(self.cursor_position + 1) + .collect::<String>(); + self.content = format!("{}{}", start, end); + } + }, + Event::Key(KeyEvent { + code: KeyCode::Home, + modifiers: KeyModifiers::NONE, + }) => self.cursor_position = 0, + Event::Key(KeyEvent { + code: KeyCode::End, + modifiers: KeyModifiers::NONE, + }) => self.cursor_position = UnicodeSegmentation::graphemes(self.content.as_str(), true).count(), + Event::Key(KeyEvent { + code: KeyCode::Right, + modifiers: KeyModifiers::NONE, + }) => { + let length = UnicodeSegmentation::graphemes(self.content.as_str(), true).count(); + if self.cursor_position < length { + self.cursor_position += 1; + } + }, + Event::Key(KeyEvent { + code: KeyCode::Left, + modifiers: KeyModifiers::NONE, + }) => { + if self.cursor_position != 0 { + self.cursor_position -= 1; + } + }, + Event::Key(KeyEvent { + code: KeyCode::Char(c), + modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT, + }) => { + let start = UnicodeSegmentation::graphemes(self.content.as_str(), true) + .take(self.cursor_position) + .collect::<String>(); + let end = UnicodeSegmentation::graphemes(self.content.as_str(), true) + .skip(self.cursor_position) + .collect::<String>(); + self.content = format!("{}{}{}", start, c, end); + self.cursor_position += 1; + }, + _ => {}, + } + } +} + +#[cfg(test)] +mod tests { + use view::{assert_rendered_output, ViewData, ViewLine}; + + use super::*; + + macro_rules! view_data_from_editable_line { + ($editable_line:expr) => {{ + let segments = $editable_line.line_segments(); + &ViewData::new(|updater| updater.push_line(ViewLine::from(segments))) + }}; + } + + fn handle_events(module: &mut EditableLine, events: &[Event]) { + for event in events { + module.handle_event(*event); + } + } + + #[test] + fn with_label() { + let mut editable_line = EditableLine::new(); + editable_line.set_content("foobar"); + editable_line.set_label(LineSegment::new("Label: ")); + assert_rendered_output!( + Options AssertRenderOptions::INCLUDE_TRAILING_WHITESPACE, + view_data_from_editable_line!(&editable_line), + "{BODY}", + "{Normal}Label: {Normal}foobar{Normal,Underline} " + ); + } + + #[test] + fn move_cursor_end() { + let mut editable_line = EditableLine::new(); + editable_line.set_content("foobar"); + editable_line.handle_event(Event::from(KeyCode::Right)); + assert_rendered_output!( + Options AssertRenderOptions::INCLUDE_TRAILING_WHITESPACE, + view_data_from_editable_line!(&editable_line), + "{BODY}", + "{Normal}foobar{Normal,Underline} " + ); + } + + #[test] + fn move_cursor_1_left() { + let mut editable_line = EditableLine::new(); + editable_line.set_content("foobar"); + editable_line.handle_event(Event::from(KeyCode::Left)); + assert_rendered_output!( + Options AssertRenderOptions::INCLUDE_TRAILING_WHITESPACE, + view_data_from_editable_line!(&editable_line), + "{BODY}", + "{Normal}fooba{Normal,Underline}r" + ); + } + + #[test] + fn move_cursor_2_from_start() { + let mut editable_line = EditableLine::new(); + editable_line.set_content("foobar"); + handle_events(&mut editable_line, &[Event::from(KeyCode::Left); 2]); + assert_rendered_output!( + Options AssertRenderOptions::INCLUDE_TRAILING_WHITESPACE, + view_data_from_editable_line!(&editable_line), + "{BODY}", + "{Normal}foob{Normal,Underline}a{Normal}r" + ); + } + + #[test] + fn move_cursor_1_from_start() { + let mut editable_line = EditableLine::new(); + editable_line.set_content("foobar"); + handle_events(&mut editable_line, &[Event::from(KeyCode::Left); 5]); + assert_rendered_output!( + Options AssertRenderOptions::INCLUDE_TRAILING_WHITESPACE, + view_data_from_editable_line!(&editable_line), + "{BODY}", + "{Normal}f{Normal,Underline}o{Normal}obar" + ); + } + + #[test] + fn move_cursor_to_start() { + let mut editable_line = EditableLine::new(); + editable_line.set_content("foobar"); + handle_events(&mut editable_line, &[Event::from(KeyCode::Left); 6]); + assert_rendered_output!( + Options AssertRenderOptions::INCLUDE_TRAILING_WHITESPACE, + view_data_from_editable_line!(&editable_line), + "{BODY}", + "{Normal,Underline}f{Normal}oobar" + ); + } + + #[test] + fn move_cursor_to_home() { + let mut editable_line = EditableLine::new(); + editable_line.set_content("foobar"); + editable_line.handle_event(Event::from(KeyCode::Home)); + assert_rendered_output!( + Options AssertRenderOptions::INCLUDE_TRAILING_WHITESPACE, + view_data_from_editable_line!(&editable_line), + "{BODY}", + "{Normal,Underline}f{Normal}oobar" + ); + } + + #[test] + fn move_cursor_right() { + let mut editable_line = EditableLine::new(); + editable_line.set_content("foobar"); + handle_events(&mut editable_line, &[ + Event::from(KeyCode::Left), + Event::from(KeyCode::Left), + Event::from(KeyCode::Left), + Event::from(KeyCode::Right), + ]); + assert_rendered_output!( + Options AssertRenderOptions::INCLUDE_TRAILING_WHITESPACE, + view_data_from_editable_line!(&editable_line), + "{BODY}", + "{Normal}foob{Normal,Underline}a{Normal}r" + ); + } + + #[test] + fn move_cursor_to_end() { + let mut editable_line = EditableLine::new(); + editable_line.set_content("foobar"); + handle_events(&mut editable_line, &[ + Event::from(KeyCode::Left), + Event::from(KeyCode::Left), + Event::from(KeyCode::Left), + Event::from(KeyCode::End), + ]); + assert_rendered_output!( + Options AssertRenderOptions::INCLUDE_TRAILING_WHITESPACE, + view_data_from_editable_line!(&editable_line), + "{BODY}", + "{Normal}foobar{Normal,Underline} " + ); + } + + #[test] + fn move_cursor_on_empty_content() { + let mut editable_line = EditableLine::new(); + handle_events(&mut editable_line, &[ + Event::from(KeyCode::Left), + Event::from(KeyCode::Right), + Event::from(KeyCode::End), + Event::from(KeyCode::Home), + ]); + assert_rendered_output!( + Options AssertRenderOptions::INCLUDE_TRAILING_WHITESPACE, + view_data_from_editable_line!(&editable_line), + "{BODY}", + "{Normal,Underline} " + ); + } + + #[test] + fn move_cursor_attempt_past_start() { + let mut editable_line = EditableLine::new(); + editable_line.set_content("foobar"); + handle_events(&mut editable_line, &[Event::from(KeyCode::Left); 10]); + assert_rendered_output!( + Options AssertRenderOptions::INCLUDE_TRAILING_WHITESPACE, + view_data_from_editable_line!(&editable_line), + "{BODY}", + "{Normal,Underline}f{Normal}oobar" + ); + } + + #[test] + fn move_cursor_attempt_past_end() { + let mut editable_line = EditableLine::new(); + editable_line.set_content("foobar"); + handle_events(&mut editable_line, &[Event::from(KeyCode::Right); 10]); + assert_rendered_output!( + Options AssertRenderOptions::INCLUDE_TRAILING_WHITESPACE, + view_data_from_editable_line!(&editable_line), + "{BODY}", + "{Normal}foobar{Normal,Underline} " + ); + } + + #[test] + fn multiple_width_unicode_single_width() { + let mut editable_line = EditableLine::new(); + editable_line.set_content("a🗳b"); + handle_events(&mut editable_line, &[Event::from(KeyCode::Left); 2]); + assert_rendered_output!( + Options AssertRenderOptions::INCLUDE_TRAILING_WHITESPACE, + view_data_from_editable_line!(&editable_line), + "{BODY}", + "{Normal}a{Normal,Underline}🗳{Normal}b" + ); + } + + #[test] + fn multiple_width_unicode_emoji() { + let mut editable_line = EditableLine::new(); + editable_line.set_content("a😀b"); + handle_events(&mut editable_line, &[Event::from(KeyCode::Left); 2]); + assert_rendered_output!( + Options AssertRenderOptions::INCLUDE_TRAILING_WHITESPACE, + view_data_from_editable_line!(&editable_line), + "{BODY}", + "{Normal}a{Normal,Underline}😀{Normal}b" + ); + } + + #[test] + fn add_character_end() { + let mut editable_line = EditableLine::new(); + editable_line.set_content("abcd"); + editable_line.handle_event(Event::from('x')); + assert_rendered_output!( + Options AssertRenderOptions::INCLUDE_TRAILING_WHITESPACE, + view_data_from_editable_line!(&editable_line), |