mirror of
https://github.com/roc-lang/roc.git
synced 2025-10-01 15:51:12 +00:00
restructuring for app<->editor, copy tests
This commit is contained in:
parent
fa2480ed4f
commit
a5ac077ec9
5 changed files with 228 additions and 102 deletions
|
@ -9,6 +9,34 @@ use snafu::{Backtrace, ErrorCompat, Snafu};
|
||||||
#[derive(Debug, Snafu)]
|
#[derive(Debug, Snafu)]
|
||||||
#[snafu(visibility(pub))]
|
#[snafu(visibility(pub))]
|
||||||
pub enum EdError {
|
pub enum EdError {
|
||||||
|
#[snafu(display("ClipboardReadFailed: could not get clipboard contents: {}", err_msg))]
|
||||||
|
ClipboardReadFailed { err_msg: String },
|
||||||
|
|
||||||
|
#[snafu(display("ClipboardWriteFailed: could not set clipboard contents: {}", err_msg))]
|
||||||
|
ClipboardWriteFailed { err_msg: String },
|
||||||
|
|
||||||
|
#[snafu(display("ClipboardInitFailed: could not initialize ClipboardContext: {}.", err_msg))]
|
||||||
|
ClipboardInitFailed { err_msg: String },
|
||||||
|
|
||||||
|
#[snafu(display("EdModelIsNone: editor model EdModel was never succesfully initiliazed."))]
|
||||||
|
EdModelIsNone { },
|
||||||
|
|
||||||
|
#[snafu(display(
|
||||||
|
"FileOpenFailed: failed to open file with path {} with the following error: {}.",
|
||||||
|
path_str,
|
||||||
|
err_msg
|
||||||
|
))]
|
||||||
|
FileOpenFailed { path_str: String, err_msg: String },
|
||||||
|
|
||||||
|
#[snafu(display("InvalidSelection: {}.", err_msg))]
|
||||||
|
InvalidSelection {
|
||||||
|
err_msg: String,
|
||||||
|
backtrace: Backtrace,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[snafu(display("MissingGlyphDims: glyph_dim_rect_opt was None for model. It needs to be set using the example_code_glyph_rect function."))]
|
||||||
|
MissingGlyphDims { backtrace: Backtrace },
|
||||||
|
|
||||||
#[snafu(display(
|
#[snafu(display(
|
||||||
"OutOfBounds: index {} was out of bounds for {} with length {}.",
|
"OutOfBounds: index {} was out of bounds for {} with length {}.",
|
||||||
index,
|
index,
|
||||||
|
@ -22,33 +50,8 @@ pub enum EdError {
|
||||||
backtrace: Backtrace,
|
backtrace: Backtrace,
|
||||||
},
|
},
|
||||||
|
|
||||||
#[snafu(display("InvalidSelection: {}.", err_msg))]
|
|
||||||
InvalidSelection {
|
|
||||||
err_msg: String,
|
|
||||||
backtrace: Backtrace,
|
|
||||||
},
|
|
||||||
|
|
||||||
#[snafu(display("MissingGlyphDims: glyph_dim_rect_opt was None for model. It needs to be set using the example_code_glyph_rect function."))]
|
|
||||||
MissingGlyphDims { backtrace: Backtrace },
|
|
||||||
|
|
||||||
#[snafu(display(
|
|
||||||
"FileOpenFailed: failed to open file with path {} with the following error: {}.",
|
|
||||||
path_str,
|
|
||||||
err_msg
|
|
||||||
))]
|
|
||||||
FileOpenFailed { path_str: String, err_msg: String },
|
|
||||||
|
|
||||||
#[snafu(display("TextBufReadFailed: the file {} could be opened but we encountered the following error while trying to read it: {}.", path_str, err_msg))]
|
#[snafu(display("TextBufReadFailed: the file {} could be opened but we encountered the following error while trying to read it: {}.", path_str, err_msg))]
|
||||||
TextBufReadFailed { path_str: String, err_msg: String },
|
TextBufReadFailed { path_str: String, err_msg: String },
|
||||||
|
|
||||||
#[snafu(display("ClipboardReadFailed: could not get clipboard contents: {}", err_msg))]
|
|
||||||
ClipboardReadFailed { err_msg: String },
|
|
||||||
|
|
||||||
#[snafu(display("ClipboardWriteFailed: could not set clipboard contents: {}", err_msg))]
|
|
||||||
ClipboardWriteFailed { err_msg: String },
|
|
||||||
|
|
||||||
#[snafu(display("ClipboardInitFailed: could not initialize ClipboardContext: {}.", err_msg))]
|
|
||||||
ClipboardInitFailed { err_msg: String },
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type EdResult<T, E = EdError> = std::result::Result<T, E>;
|
pub type EdResult<T, E = EdError> = std::result::Result<T, E>;
|
||||||
|
@ -101,3 +104,9 @@ fn contains_one_of(main_str: &str, contain_slice: &[&str]) -> bool {
|
||||||
|
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<EdError> for String {
|
||||||
|
fn from(ed_error: EdError) -> Self {
|
||||||
|
format!("{}", ed_error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -25,7 +25,7 @@ use crate::graphics::style::CODE_FONT_SIZE;
|
||||||
use crate::graphics::style::CODE_TXT_XY;
|
use crate::graphics::style::CODE_TXT_XY;
|
||||||
use crate::mvc::app_model::AppModel;
|
use crate::mvc::app_model::AppModel;
|
||||||
use crate::mvc::ed_model::EdModel;
|
use crate::mvc::ed_model::EdModel;
|
||||||
use crate::mvc::{ed_model, ed_view, ed_update};
|
use crate::mvc::{ed_model, ed_view, app_update};
|
||||||
use crate::resources::strings::NOTHING_OPENED;
|
use crate::resources::strings::NOTHING_OPENED;
|
||||||
use crate::vec_result::get_res;
|
use crate::vec_result::get_res;
|
||||||
use bumpalo::Bump;
|
use bumpalo::Bump;
|
||||||
|
@ -213,7 +213,7 @@ fn run_event_loop(file_path_opt: Option<&Path>) -> Result<(), Box<dyn Error>> {
|
||||||
event: event::WindowEvent::ReceivedCharacter(ch),
|
event: event::WindowEvent::ReceivedCharacter(ch),
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
if let Err(e) = ed_update::handle_new_char(&mut app_model, &ch) {
|
if let Err(e) = app_update::handle_new_char(&ch, &mut app_model) {
|
||||||
print_err(&e)
|
print_err(&e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use super::ed_model::EdModel;
|
use super::ed_model::EdModel;
|
||||||
use crate::error::{EdResult, print_err};
|
use crate::error::{EdResult, print_err};
|
||||||
use crate::error::EdError::{ClipboardReadFailed, ClipboardWriteFailed, ClipboardInitFailed};
|
use crate::error::EdError::{ClipboardReadFailed, ClipboardWriteFailed, ClipboardInitFailed, EdModelIsNone};
|
||||||
use clipboard::{ClipboardContext, ClipboardProvider};
|
use clipboard::{ClipboardContext, ClipboardProvider};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
|
@ -30,6 +30,22 @@ impl AppModel {
|
||||||
clipboard_opt
|
clipboard_opt
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_ed_model(&self) -> EdResult<&EdModel> {
|
||||||
|
if let Some(ref ed_model) = self.ed_model_opt {
|
||||||
|
Ok(ed_model)
|
||||||
|
} else {
|
||||||
|
Err(EdModelIsNone {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_ed_model_mut(&mut self) -> EdResult<& mut EdModel> {
|
||||||
|
if let Some(ref mut ed_model) = self.ed_model_opt {
|
||||||
|
Ok(ed_model)
|
||||||
|
} else {
|
||||||
|
Err(EdModelIsNone {})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Clipboard {
|
pub struct Clipboard {
|
||||||
|
|
|
@ -5,6 +5,7 @@ use crate::error::EdResult;
|
||||||
use crate::error::EdError::{ClipboardWriteFailed, ClipboardReadFailed};
|
use crate::error::EdError::{ClipboardWriteFailed, ClipboardReadFailed};
|
||||||
use winit::event::{ModifiersState, VirtualKeyCode};
|
use winit::event::{ModifiersState, VirtualKeyCode};
|
||||||
|
|
||||||
|
|
||||||
pub fn handle_copy(app_model: &mut AppModel) -> EdResult<()> {
|
pub fn handle_copy(app_model: &mut AppModel) -> EdResult<()> {
|
||||||
if let Some(ref mut ed_model) = app_model.ed_model_opt {
|
if let Some(ref mut ed_model) = app_model.ed_model_opt {
|
||||||
if ed_model.has_focus {
|
if ed_model.has_focus {
|
||||||
|
@ -89,4 +90,123 @@ pub fn pass_keydown_to_focused(
|
||||||
ed_update::handle_key_down(modifiers, virtual_keycode, ed_model);
|
ed_update::handle_key_down(modifiers, virtual_keycode, ed_model);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_new_char(received_char: &char, app_model: &mut AppModel) -> EdResult<()> {
|
||||||
|
if let Some(ref mut ed_model) = app_model.ed_model_opt {
|
||||||
|
if ed_model.has_focus {
|
||||||
|
ed_update::handle_new_char(received_char, ed_model)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub mod test_app_update {
|
||||||
|
use crate::mvc::app_model::{AppModel, Clipboard};
|
||||||
|
use crate::mvc::app_update::{handle_copy, handle_paste};
|
||||||
|
use crate::mvc::ed_update::test_ed_update::{gen_caret_text_buf};
|
||||||
|
use crate::mvc::ed_model::{EdModel, Position, RawSelection};
|
||||||
|
use crate::selection::test_selection::{convert_selection_to_dsl, all_lines_vec};
|
||||||
|
use crate::text_buffer::TextBuffer;
|
||||||
|
use crate::error::EdResult;
|
||||||
|
use crate::error::EdError::ClipboardInitFailed;
|
||||||
|
|
||||||
|
pub fn mock_app_model(
|
||||||
|
text_buf: TextBuffer,
|
||||||
|
caret_pos: Position,
|
||||||
|
selection_opt: Option<RawSelection>,
|
||||||
|
) -> AppModel {
|
||||||
|
AppModel::init(
|
||||||
|
Some(
|
||||||
|
EdModel {
|
||||||
|
text_buf,
|
||||||
|
caret_pos,
|
||||||
|
selection_opt,
|
||||||
|
glyph_dim_rect_opt: None,
|
||||||
|
has_focus: true,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_clipboard(app_model: &mut AppModel) -> EdResult<&mut Clipboard> {
|
||||||
|
if let Some(ref mut clipboard) = app_model.clipboard_opt {
|
||||||
|
Ok(clipboard)
|
||||||
|
} else {
|
||||||
|
Err(ClipboardInitFailed {
|
||||||
|
err_msg: "Clipboard was never initialized succesfully.".to_owned()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_copy(
|
||||||
|
pre_lines_str: &[&str],
|
||||||
|
expected_clipboard_content: &str,
|
||||||
|
) -> 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);
|
||||||
|
|
||||||
|
handle_copy(&mut app_model)?;
|
||||||
|
|
||||||
|
let clipboard = get_clipboard(&mut app_model)?;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
clipboard.get_content()?,
|
||||||
|
expected_clipboard_content
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_paste(
|
||||||
|
pre_lines_str: &[&str],
|
||||||
|
clipboard_content: &str,
|
||||||
|
expected_post_lines_str: &[&str],
|
||||||
|
) -> 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);
|
||||||
|
let clipboard = get_clipboard(&mut app_model)?;
|
||||||
|
clipboard.set_content(clipboard_content.to_owned())?;
|
||||||
|
|
||||||
|
handle_paste(&mut app_model)?;
|
||||||
|
|
||||||
|
let ed_model = app_model.get_ed_model()?;
|
||||||
|
let mut text_buf_lines = all_lines_vec(&ed_model.text_buf);
|
||||||
|
let post_lines_str = convert_selection_to_dsl(
|
||||||
|
ed_model.selection_opt,
|
||||||
|
ed_model.caret_pos,
|
||||||
|
&mut text_buf_lines
|
||||||
|
)?;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
post_lines_str,
|
||||||
|
expected_post_lines_str
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn copy() -> Result<(), String> {
|
||||||
|
assert_copy(&["[a]|"], "a")?;
|
||||||
|
assert_copy(&["|[b]"], "b")?;
|
||||||
|
assert_copy(&["a[ ]|"], " ")?;
|
||||||
|
assert_copy(&["[ ]|b"], " ")?;
|
||||||
|
assert_copy(&["a\n", "[b\n", "]|"], "b\n")?;
|
||||||
|
assert_copy(&["[a\n", " b\n", "]|"], "a\n b\n")?;
|
||||||
|
assert_copy(&["abc\n", "d[ef\n", "ghi]|\n", "jkl"], "ef\nghi")?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn paste() -> Result<(), String> {
|
||||||
|
assert_paste(&["|"], "", &["|"])?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -319,65 +319,63 @@ fn del_selection(selection: RawSelection, ed_model: &mut EdModel) -> EdResult<()
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_new_char(app_model: &mut AppModel, received_char: &char) -> EdResult<()> {
|
pub fn handle_new_char(received_char: &char, ed_model: &mut EdModel) -> EdResult<()> {
|
||||||
if let Some(ref mut ed_model) = app_model.ed_model_opt {
|
let old_caret_pos = ed_model.caret_pos;
|
||||||
let old_caret_pos = ed_model.caret_pos;
|
|
||||||
|
|
||||||
match received_char {
|
match received_char {
|
||||||
'\u{8}' | '\u{7f}' => {
|
'\u{8}' | '\u{7f}' => {
|
||||||
// On Linux, '\u{8}' is backspace,
|
// On Linux, '\u{8}' is backspace,
|
||||||
// on macOS '\u{7f}'.
|
// on macOS '\u{7f}'.
|
||||||
if let Some(selection) = ed_model.selection_opt {
|
if let Some(selection) = ed_model.selection_opt {
|
||||||
del_selection(selection, ed_model)?;
|
del_selection(selection, ed_model)?;
|
||||||
} else {
|
} else {
|
||||||
ed_model.caret_pos =
|
ed_model.caret_pos =
|
||||||
move_caret_left(old_caret_pos, None, false, &ed_model.text_buf).0;
|
move_caret_left(old_caret_pos, None, false, &ed_model.text_buf).0;
|
||||||
|
|
||||||
ed_model.text_buf.pop_char(old_caret_pos);
|
ed_model.text_buf.pop_char(old_caret_pos);
|
||||||
}
|
|
||||||
|
|
||||||
ed_model.selection_opt = None;
|
|
||||||
}
|
}
|
||||||
ch if is_newline(ch) => {
|
|
||||||
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 {
|
ed_model.selection_opt = None;
|
||||||
line: old_caret_pos.line + 1,
|
}
|
||||||
column: 0,
|
ch if is_newline(ch) => {
|
||||||
};
|
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.selection_opt = None;
|
ed_model.caret_pos = Position {
|
||||||
|
line: old_caret_pos.line + 1,
|
||||||
|
column: 0,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
'\u{3}' | '\u{16}' | '\u{30}' | '\u{e000}'..='\u{f8ff}' | '\u{f0000}'..='\u{ffffd}' | '\u{100000}'..='\u{10fffd}' => {
|
|
||||||
// chars that can be ignored
|
ed_model.selection_opt = None;
|
||||||
|
}
|
||||||
|
'\u{3}' | '\u{16}' | '\u{30}' | '\u{e000}'..='\u{f8ff}' | '\u{f0000}'..='\u{ffffd}' | '\u{100000}'..='\u{10fffd}' => {
|
||||||
|
// chars that can be ignored
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
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 =
|
||||||
|
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,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
_ => {
|
|
||||||
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 =
|
ed_model.selection_opt = None;
|
||||||
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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
ed_model.selection_opt = None;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -399,16 +397,16 @@ pub fn handle_key_down(
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test_update {
|
pub mod test_ed_update {
|
||||||
use crate::mvc::app_model::AppModel;
|
use crate::mvc::app_update::test_app_update::{mock_app_model};
|
||||||
use crate::mvc::ed_model::{EdModel, Position, RawSelection};
|
use crate::mvc::ed_model::{Position, RawSelection};
|
||||||
use crate::mvc::ed_update::handle_new_char;
|
use crate::mvc::ed_update::handle_new_char;
|
||||||
use crate::selection::test_selection::{
|
use crate::selection::test_selection::{
|
||||||
all_lines_vec, convert_dsl_to_selection, convert_selection_to_dsl, text_buffer_from_dsl_str,
|
all_lines_vec, convert_dsl_to_selection, convert_selection_to_dsl, text_buffer_from_dsl_str,
|
||||||
};
|
};
|
||||||
use crate::text_buffer::TextBuffer;
|
use crate::text_buffer::TextBuffer;
|
||||||
|
|
||||||
fn gen_caret_text_buf(
|
pub fn gen_caret_text_buf(
|
||||||
lines: &[&str],
|
lines: &[&str],
|
||||||
) -> Result<(Position, Option<RawSelection>, TextBuffer), String> {
|
) -> Result<(Position, Option<RawSelection>, TextBuffer), String> {
|
||||||
let lines_string_slice: Vec<String> = lines.iter().map(|l| l.to_string()).collect();
|
let lines_string_slice: Vec<String> = lines.iter().map(|l| l.to_string()).collect();
|
||||||
|
@ -418,24 +416,6 @@ mod test_update {
|
||||||
Ok((caret_pos, selection_opt, text_buf))
|
Ok((caret_pos, selection_opt, text_buf))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mock_app_model(
|
|
||||||
text_buf: TextBuffer,
|
|
||||||
caret_pos: Position,
|
|
||||||
selection_opt: Option<RawSelection>,
|
|
||||||
) -> AppModel {
|
|
||||||
AppModel::init(
|
|
||||||
Some(
|
|
||||||
EdModel {
|
|
||||||
text_buf,
|
|
||||||
caret_pos,
|
|
||||||
selection_opt,
|
|
||||||
glyph_dim_rect_opt: None,
|
|
||||||
has_focus: true,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn assert_insert(
|
fn assert_insert(
|
||||||
pre_lines_str: &[&str],
|
pre_lines_str: &[&str],
|
||||||
expected_post_lines_str: &[&str],
|
expected_post_lines_str: &[&str],
|
||||||
|
@ -444,8 +424,9 @@ mod test_update {
|
||||||
let (caret_pos, selection_opt, pre_text_buf) = gen_caret_text_buf(pre_lines_str)?;
|
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);
|
let mut app_model = mock_app_model(pre_text_buf, caret_pos, selection_opt);
|
||||||
|
let mut ed_model = app_model.get_ed_model_mut()?;
|
||||||
|
|
||||||
if let Err(e) = handle_new_char(&mut app_model, &new_char) {
|
if let Err(e) = handle_new_char(&new_char, ed_model) {
|
||||||
return Err(e.to_string());
|
return Err(e.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue