diff --git a/editor/src/lib.rs b/editor/src/lib.rs index 9df30fe881..ea4d15fdb0 100644 --- a/editor/src/lib.rs +++ b/editor/src/lib.rs @@ -79,7 +79,7 @@ fn run_event_loop(file_path_opt: Option<&Path>) -> Result<(), Box> { let event_loop = winit::event_loop::EventLoop::new(); let window = winit::window::WindowBuilder::new() - .with_inner_size(PhysicalSize::new(1000.0, 800.0)) + .with_inner_size(PhysicalSize::new(1200.0, 1000.0)) .build(&event_loop) .unwrap(); @@ -384,11 +384,11 @@ fn queue_all_text( }; let caret_pos_label = Text { - position: (30.0, (size.height as f32) - 45.0).into(), + position: ((size.width as f32) - 150.0, (size.height as f32) - 40.0).into(), area_bounds, color: TXT_COLOR.into(), text: format!("Ln {}, Col {}", caret_pos.line, caret_pos.column), - size: 30.0, + size: 25.0, ..Default::default() }; diff --git a/editor/src/mvc/update.rs b/editor/src/mvc/update.rs index 8340985d66..4187cfe4db 100644 --- a/editor/src/mvc/update.rs +++ b/editor/src/mvc/update.rs @@ -2,6 +2,7 @@ use super::ed_model::{Position, RawSelection}; use crate::text_buffer::TextBuffer; use crate::util::is_newline; use super::app_model::AppModel; +use super::ed_model::EdModel; use crate::error::EdResult; use std::cmp::{max, min}; @@ -298,9 +299,15 @@ pub fn move_caret_down( (new_caret_pos, new_selection_opt) } +fn del_selection(selection: RawSelection, ed_model: &mut EdModel) -> EdResult<()> { + ed_model.text_buf.del_selection(selection)?; + ed_model.caret_pos = selection.start_pos; + + Ok(()) +} + pub fn handle_new_char(app_model: &mut AppModel, received_char: &char) -> EdResult<()> { if let Some(ref mut ed_model) = app_model.ed_model_opt { - ed_model.selection_opt = None; let old_caret_pos = ed_model.caret_pos; match received_char { @@ -308,34 +315,46 @@ pub fn handle_new_char(app_model: &mut AppModel, received_char: &char) -> EdResu // On Linux, '\u{8}' is backspace, // on macOS '\u{7f}'. if let Some(selection) = ed_model.selection_opt { - ed_model.text_buf.del_selection(selection)?; - ed_model.caret_pos = selection.start_pos; + del_selection(selection, ed_model)?; } else { - ed_model.text_buf.pop_char(old_caret_pos); - ed_model.caret_pos = move_caret_left(old_caret_pos, None, false, &ed_model.text_buf).0; + + ed_model.text_buf.pop_char(old_caret_pos); } } ch if is_newline(ch) => { - ed_model.text_buf.insert_char(old_caret_pos, ch)?; - ed_model.caret_pos = Position { - line: old_caret_pos.line + 1, - column: 0, - }; + if let Some(selection) = ed_model.selection_opt { + del_selection(selection, ed_model)?; + ed_model.text_buf.insert_char(ed_model.caret_pos, &'\n')?; + } else { + ed_model.text_buf.insert_char(old_caret_pos, &'\n')?; + + ed_model.caret_pos = Position { + line: old_caret_pos.line + 1, + column: 0, + }; + } } '\u{e000}'..='\u{f8ff}' | '\u{f0000}'..='\u{ffffd}' | '\u{100000}'..='\u{10fffd}' => { // These are private use characters; ignore them. // See http://www.unicode.org/faq/private_use.html } _ => { - ed_model.text_buf.insert_char(old_caret_pos, received_char)?; + if let Some(selection) = ed_model.selection_opt { + del_selection(selection, ed_model)?; + ed_model.text_buf.insert_char(ed_model.caret_pos, received_char)?; - ed_model.caret_pos = Position { - line: old_caret_pos.line, - column: old_caret_pos.column + 1, - }; + ed_model.caret_pos = + move_caret_right(ed_model.caret_pos, None, false, &ed_model.text_buf).0; + } else { + ed_model.text_buf.insert_char(old_caret_pos, received_char)?; + ed_model.caret_pos = Position { + line: old_caret_pos.line, + column: old_caret_pos.column + 1, + }; + } } } @@ -344,3 +363,240 @@ pub fn handle_new_char(app_model: &mut AppModel, received_char: &char) -> EdResu Ok(()) } + + +#[cfg(test)] +mod test_update { + use crate::selection::test_selection::{convert_dsl_to_selection, convert_selection_to_dsl, text_buffer_from_dsl_str, all_lines_vec}; + use crate::mvc::ed_model::{Position, EdModel, RawSelection}; + use crate::mvc::app_model::AppModel; + use crate::mvc::update::{handle_new_char}; + use crate::text_buffer::TextBuffer; + + fn gen_caret_text_buf(lines: &[&str]) -> Result<(Position, Option, TextBuffer), String> { + let lines_string_slice: Vec = lines.iter().map(|l| l.to_string()).collect(); + let (selection_opt, caret_pos) = convert_dsl_to_selection(&lines_string_slice)?; + let text_buf = text_buffer_from_dsl_str(&lines_string_slice); + + Ok((caret_pos, selection_opt, text_buf)) + } + + fn mock_app_model(text_buf: TextBuffer, caret_pos: Position, selection_opt: Option) -> AppModel { + AppModel { + ed_model_opt: Some( + EdModel{ + text_buf, + caret_pos, + selection_opt, + glyph_dim_rect_opt: None, + has_focus: true + } + ) + } + } + + fn assert_insert( + pre_lines_str: &[&str], + expected_post_lines_str: &[&str], + new_char: char, + ) -> Result<(), String> { + let (caret_pos, selection_opt, pre_text_buf) = gen_caret_text_buf(pre_lines_str)?; + + let mut app_model = mock_app_model(pre_text_buf, caret_pos, selection_opt); + + if let Err(e) = handle_new_char(&mut app_model, &new_char) { + return Err(e.to_string()); + } + + if let Some(ed_model) = app_model.ed_model_opt { + let mut actual_lines = all_lines_vec(&ed_model.text_buf); + let dsl_slice = convert_selection_to_dsl(ed_model.selection_opt, ed_model.caret_pos, &mut actual_lines).unwrap(); + assert_eq!(dsl_slice, expected_post_lines_str); + } else { + panic!("Mock AppModel did not have an EdModel."); + } + + Ok(()) + } + + #[test] + fn insert_new_char_simple() -> Result<(), String> { + assert_insert( + &["|"], &["a|"], 'a' + )?; + assert_insert( + &["|"], &[" |"], ' ' + )?; + assert_insert( + &["a|"], &["aa|"], 'a' + )?; + assert_insert( + &["a|"], &["a |"], ' ' + )?; + assert_insert( + &["a|\n", ""], &["ab|\n", ""], 'b' + )?; + assert_insert( + &["a|\n", ""], &["ab|\n", ""], 'b' + )?; + assert_insert( + &["a\n", "|"], &["a\n", "b|"], 'b' + )?; + assert_insert( + &["a\n", "b\n", "c|"], &["a\n", "b\n", "cd|"], 'd' + )?; + + Ok(()) + } + + #[test] + fn insert_new_char_mid() -> Result<(), String> { + assert_insert( + &["ab|d"], &["abc|d"], 'c' + )?; + assert_insert( + &["a|cd"], &["ab|cd"], 'b' + )?; + assert_insert( + &["abc\n", "|e"], &["abc\n", "d|e"], 'd' + )?; + assert_insert( + &["abc\n", "def\n", "| "], &["abc\n", "def\n", "g| "], 'g' + )?; + assert_insert( + &["abc\n", "def\n", "| "], &["abc\n", "def\n", " | "], ' ' + )?; + + + Ok(()) + } + + #[test] + fn simple_backspace() -> Result<(), String> { + assert_insert( + &["|"], &["|"], '\u{8}' + )?; + assert_insert( + &[" |"], &["|"], '\u{8}' + )?; + assert_insert( + &["a|"], &["|"], '\u{8}' + )?; + assert_insert( + &["ab|"], &["a|"], '\u{8}' + )?; + assert_insert( + &["a|\n", ""], &["|\n", ""], '\u{8}' + )?; + assert_insert( + &["ab|\n", ""], &["a|\n", ""], '\u{8}' + )?; + assert_insert( + &["a\n", "|"], &["a|"], '\u{8}' + )?; + assert_insert( + &["a\n", "b\n", "c|"], &["a\n", "b\n", "|"], '\u{8}' + )?; + assert_insert( + &["a\n", "b\n", "|"], &["a\n", "b|"], '\u{8}' + )?; + + Ok(()) + } + + #[test] + fn selection_backspace() -> Result<(), String> { + assert_insert( + &["[a]|"], &["|"], '\u{8}' + )?; + assert_insert( + &["a[a]|"], &["a|"], '\u{8}' + )?; + assert_insert( + &["[aa]|"], &["|"], '\u{8}' + )?; + assert_insert( + &["a[b c]|"], &["a|"], '\u{8}' + )?; + assert_insert( + &["[abc]|\n", ""], &["|\n", ""], '\u{8}' + )?; + assert_insert( + &["a\n", "[abc]|"], &["a\n","|"], '\u{8}' + )?; + assert_insert( + &["[a\n", "abc]|"], &["|"], '\u{8}' + )?; + assert_insert( + &["a[b\n", "cdef ghij]|"], &["a|"], '\u{8}' + )?; + assert_insert( + &["[a\n", "b\n", "c]|"], &["|"], '\u{8}' + )?; + assert_insert( + &["a\n", "[b\n", "]|"], &["a\n", "|"], '\u{8}' + )?; + assert_insert( + &["abc\n", "d[ef\n", "ghi]|\n", "jkl"], &["abc\n", "d|\n", "jkl"], '\u{8}' + )?; + assert_insert( + &["abc\n", "[def\n", "ghi]|\n", "jkl"], &["abc\n", "|\n", "jkl"], '\u{8}' + )?; + assert_insert( + &["abc\n", "\n", "[def\n", "ghi]|\n", "jkl"], &["abc\n", "\n", "|\n", "jkl"], '\u{8}' + )?; + assert_insert( + &["[abc\n", "\n", "def\n", "ghi\n", "jkl]|"], &["|"], '\u{8}' + )?; + + Ok(()) + } + + #[test] + fn insert_with_selection() -> Result<(), String> { + assert_insert( + &["[a]|"], &["z|"], 'z' + )?; + assert_insert( + &["a[a]|"], &["az|"], 'z' + )?; + assert_insert( + &["[aa]|"], &["z|"], 'z' + )?; + assert_insert( + &["a[b c]|"], &["az|"], 'z' + )?; + assert_insert( + &["[abc]|\n", ""], &["z|\n", ""], 'z' + )?; + assert_insert( + &["a\n", "[abc]|"], &["a\n","z|"], 'z' + )?; + assert_insert( + &["[a\n", "abc]|"], &["z|"], 'z' + )?; + assert_insert( + &["a[b\n", "cdef ghij]|"], &["az|"], 'z' + )?; + assert_insert( + &["[a\n", "b\n", "c]|"], &["z|"], 'z' + )?; + assert_insert( + &["a\n", "[b\n", "]|"], &["a\n", "z|"], 'z' + )?; + assert_insert( + &["abc\n", "d[ef\n", "ghi]|\n", "jkl"], &["abc\n", "dz|\n", "jkl"], 'z' + )?; + assert_insert( + &["abc\n", "[def\n", "ghi]|\n", "jkl"], &["abc\n", "z|\n", "jkl"], 'z' + )?; + assert_insert( + &["abc\n", "\n", "[def\n", "ghi]|\n", "jkl"], &["abc\n", "\n", "z|\n", "jkl"], 'z' + )?; + assert_insert( + &["[abc\n", "\n", "def\n", "ghi\n", "jkl]|"], &["z|"], 'z' + )?; + + Ok(()) + } +} diff --git a/editor/src/selection.rs b/editor/src/selection.rs index 7d86dd1efd..1ff8f4c653 100644 --- a/editor/src/selection.rs +++ b/editor/src/selection.rs @@ -123,7 +123,7 @@ pub fn create_selection_rects<'a>( } #[cfg(test)] -mod test_parse { +pub mod test_selection { use crate::error::{EdResult, OutOfBounds}; use crate::mvc::ed_model::{Position, RawSelection}; use crate::mvc::update::{move_caret_down, move_caret_left, move_caret_right, move_caret_up, MoveCaretFun}; @@ -141,7 +141,7 @@ mod test_parse { pub struct LineParser; // show selection and caret position as symbols in lines for easy testing - fn convert_selection_to_dsl( + pub fn convert_selection_to_dsl( raw_sel_opt: Option, caret_pos: Position, lines: &mut [String], @@ -217,6 +217,16 @@ mod test_parse { } } + pub fn text_buffer_from_dsl_str(lines: &[String]) -> TextBuffer { + text_buffer_from_str( + &lines + .iter() + .map(|line| line.replace(&['[', ']', '|'][..], "")) + .collect::>() + .join("") + ) + } + pub fn all_lines_vec(text_buf: &TextBuffer) -> Vec { let mut lines: Vec = Vec::new(); @@ -235,7 +245,7 @@ mod test_parse { } // Retrieve selection and position from formatted string - fn convert_dsl_to_selection( + pub fn convert_dsl_to_selection( lines: &[String], ) -> Result<(Option, Position), String> { let lines_str: String = lines.join(""); @@ -349,13 +359,7 @@ mod test_parse { let (sel_opt, caret_pos) = convert_dsl_to_selection(&pre_lines)?; let clean_text_buf = - text_buffer_from_str( - &pre_lines - .into_iter() - .map(|line| line.replace(&['[', ']', '|'][..], "")) - .collect::>() - .join("") - ); + text_buffer_from_dsl_str(&pre_lines); let (new_caret_pos, new_sel_opt) = move_fun(caret_pos, sel_opt, shift_pressed, &clean_text_buf); diff --git a/editor/src/text_buffer.rs b/editor/src/text_buffer.rs index 5bb34902e6..bc7c52cc14 100644 --- a/editor/src/text_buffer.rs +++ b/editor/src/text_buffer.rs @@ -23,7 +23,7 @@ impl TextBuffer { pub fn insert_char(&mut self, caret_pos: Position, new_char: &char) -> EdResult<()> { let char_indx = self.pos_to_char_indx(caret_pos); - ensure!(char_indx > self.text_rope.len_chars(), OutOfBounds{ + ensure!(char_indx <= self.text_rope.len_chars(), OutOfBounds{ index: char_indx, collection_name: "Rope", len: self.text_rope.len_chars() @@ -35,16 +35,17 @@ impl TextBuffer { } pub fn pop_char(&mut self, caret_pos: Position) { - let char_indx = self.pos_to_char_indx(caret_pos) - 1; - if char_indx < self.text_rope.len_chars() { - self.text_rope.remove(char_indx..char_indx); + let char_indx = self.pos_to_char_indx(caret_pos); + + if (char_indx > 0) && char_indx <= self.text_rope.len_chars() { + self.text_rope.remove((char_indx - 1)..char_indx); } } pub fn del_selection(&mut self, raw_sel: RawSelection) -> EdResult<()> { let (start_char_indx, end_char_indx) = self.sel_to_tup(raw_sel)?; - ensure!(end_char_indx < self.text_rope.len_chars(), OutOfBounds{ + ensure!(end_char_indx <= self.text_rope.len_chars(), OutOfBounds{ index: end_char_indx, collection_name: "Rope", len: self.text_rope.len_chars() @@ -102,7 +103,7 @@ impl TextBuffer { fn sel_to_tup(&self, raw_sel: RawSelection) -> EdResult<(usize, usize)> { let valid_sel = validate_selection(raw_sel)?; let start_char_indx = self.pos_to_char_indx(valid_sel.selection.start_pos); - let end_char_indx = self.pos_to_char_indx(valid_sel.selection.start_pos); + let end_char_indx = self.pos_to_char_indx(valid_sel.selection.end_pos); Ok((start_char_indx, end_char_indx)) }