From bdf48d478fc1362f6047d8b50ddc6c263a28d75a Mon Sep 17 00:00:00 2001 From: Anton-4 <17049058+Anton-4@users.noreply.github.com> Date: Mon, 11 Jan 2021 19:46:15 +0100 Subject: [PATCH] progress integrating text rope --- Cargo.lock | 10 +++ editor/Cargo.toml | 1 + editor/src/error.rs | 12 ++- editor/src/lang/mod.rs | 2 +- editor/src/lang/{file.rs => roc_file.rs} | 0 editor/src/lib.rs | 52 +++++++++-- editor/src/resources/strings.rs | 1 + editor/src/selection.rs | 4 +- editor/src/tea/app_model.rs | 7 ++ editor/src/tea/ed_model.rs | 14 +-- editor/src/tea/mod.rs | 1 + editor/src/tea/update.rs | 77 +++++++++-------- editor/src/text_buffer.rs | 86 +++++++++++++++++++ .../{Simple.roc => SimpleUnformatted.roc} | 0 editor/tests/modules/Storage.roc | 68 --------------- editor/tests/test_file.rs | 6 +- 16 files changed, 217 insertions(+), 124 deletions(-) rename editor/src/lang/{file.rs => roc_file.rs} (100%) create mode 100644 editor/src/resources/strings.rs create mode 100644 editor/src/tea/app_model.rs create mode 100644 editor/src/text_buffer.rs rename editor/tests/modules/{Simple.roc => SimpleUnformatted.roc} (100%) delete mode 100644 editor/tests/modules/Storage.roc diff --git a/Cargo.lock b/Cargo.lock index 0af4e03703..90c3bdc7bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2694,6 +2694,7 @@ dependencies = [ "roc_types", "roc_unify", "roc_uniq", + "ropey", "snafu", "target-lexicon", "ven_graph", @@ -3023,6 +3024,15 @@ dependencies = [ "roc_types", ] +[[package]] +name = "ropey" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f3ef16589fdbb3e8fbce3dca944c08e61f39c7f16064b21a257d68ea911a83" +dependencies = [ + "smallvec", +] + [[package]] name = "rust-argon2" version = "0.8.3" diff --git a/editor/Cargo.toml b/editor/Cargo.toml index fcfda19a8a..b0cb7578c2 100644 --- a/editor/Cargo.toml +++ b/editor/Cargo.toml @@ -68,6 +68,7 @@ snafu = { version = "0.6", features = ["backtraces"] } colored = "2" pest = "2.1" pest_derive = "2.1" +ropey = "1.2.0" [dependencies.bytemuck] diff --git a/editor/src/error.rs b/editor/src/error.rs index c3d2f53cbc..95f36bf6d0 100644 --- a/editor/src/error.rs +++ b/editor/src/error.rs @@ -19,13 +19,23 @@ pub enum EdError { vec_len: usize, backtrace: Backtrace, }, - #[snafu(display("InvalidSelection: {}", err_msg))] + #[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 {}, + #[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))] + TextBufReadFailed { + path_str: String, + err_msg: String + } } pub type EdResult = std::result::Result; diff --git a/editor/src/lang/mod.rs b/editor/src/lang/mod.rs index add841ec13..00a088b07f 100644 --- a/editor/src/lang/mod.rs +++ b/editor/src/lang/mod.rs @@ -1,7 +1,7 @@ pub mod ast; mod def; mod expr; -pub mod file; +pub mod roc_file; mod module; mod pattern; mod pool; diff --git a/editor/src/lang/file.rs b/editor/src/lang/roc_file.rs similarity index 100% rename from editor/src/lang/file.rs rename to editor/src/lang/roc_file.rs diff --git a/editor/src/lib.rs b/editor/src/lib.rs index 0324eadcbc..5ba72b4f76 100644 --- a/editor/src/lib.rs +++ b/editor/src/lib.rs @@ -28,6 +28,8 @@ use crate::graphics::style::CODE_TXT_XY; use crate::selection::create_selection_rects; use crate::tea::ed_model::EdModel; use crate::tea::{ed_model, update}; +use crate::tea::app_model::AppModel; +use crate::vec_result::get_res; use bumpalo::collections::Vec as BumpVec; use bumpalo::Bump; use cgmath::Vector2; @@ -51,24 +53,37 @@ mod selection; mod tea; mod util; mod vec_result; +mod text_buffer; /// The editor is actually launched from the CLI if you pass it zero arguments, /// or if you provide it 1 or more files or directories to open on launch. -pub fn launch(_filepaths: &[&Path]) -> io::Result<()> { - // TODO do any initialization here +pub fn launch(filepaths: &[&Path]) -> io::Result<()> { + //TODO support using multiple filepaths + let first_path_opt = if filepaths.len() > 0 { + match get_res(0, filepaths) { + Ok(path_ref_ref) => Some(*path_ref_ref), + Err(e) => { + eprintln!("{}", e); + None + } + } + } else { + None + }; - run_event_loop().expect("Error running event loop"); + run_event_loop(first_path_opt).expect("Error running event loop"); Ok(()) } -fn run_event_loop() -> Result<(), Box> { +fn run_event_loop(file_path_opt: Option<&Path>) -> Result<(), Box> { env_logger::init(); // Open window and create a surface let event_loop = winit::event_loop::EventLoop::new(); let window = winit::window::WindowBuilder::new() + .with_inner_size(PhysicalSize::new(1000.0, 800.0)) .build(&event_loop) .unwrap(); @@ -124,8 +139,31 @@ fn run_event_loop() -> Result<(), Box> { let mut glyph_brush = build_glyph_brush(&gpu_device, render_format)?; let is_animating = true; - let mut ed_model = ed_model::init_model(); - ed_model.glyph_dim_rect_opt = Some(example_code_glyph_rect(&mut glyph_brush)); + let mut ed_model_opt = + if let Some(file_path) = file_path_opt { + let ed_model_res = + ed_model::init_model(file_path); + + match ed_model_res { + Ok(ed_model) => { + ed_model.glyph_dim_rect_opt = + Some(example_code_glyph_rect(&mut glyph_brush)); + + Some(ed_model) + }, + Err(e) => { + print_err(&e); + None + } + } + } else { + None + }; + + let mut app_model = AppModel { + ed_model_opt + }; + let mut keyboard_modifiers = ModifiersState::empty(); let arena = Bump::new(); @@ -180,7 +218,7 @@ fn run_event_loop() -> Result<(), Box> { event: event::WindowEvent::ReceivedCharacter(ch), .. } => { - update::update_text_state(&mut ed_model, &ch); + update::handle_new_char(&mut app_model, &ch); } //Keyboard Input Event::WindowEvent { diff --git a/editor/src/resources/strings.rs b/editor/src/resources/strings.rs new file mode 100644 index 0000000000..56866557ca --- /dev/null +++ b/editor/src/resources/strings.rs @@ -0,0 +1 @@ +pub const NOTHING_OPENED: &str = "Execute `cargo run edit ` to open a file"; \ No newline at end of file diff --git a/editor/src/selection.rs b/editor/src/selection.rs index 85f60ab3fd..9909941082 100644 --- a/editor/src/selection.rs +++ b/editor/src/selection.rs @@ -9,10 +9,10 @@ use snafu::ensure; //using the "parse don't validate" pattern struct ValidSelection { - selection: RawSelection, + pub selection: RawSelection, } -fn validate_selection(selection: RawSelection) -> EdResult { +pub fn validate_selection(selection: RawSelection) -> EdResult { let RawSelection { start_pos, end_pos } = selection; ensure!( diff --git a/editor/src/tea/app_model.rs b/editor/src/tea/app_model.rs new file mode 100644 index 0000000000..69f6e26aa6 --- /dev/null +++ b/editor/src/tea/app_model.rs @@ -0,0 +1,7 @@ + +use crate::tea::ed_model::EdModel; + +#[derive(Debug)] +pub struct AppModel { + pub ed_model_opt: Option +} \ No newline at end of file diff --git a/editor/src/tea/ed_model.rs b/editor/src/tea/ed_model.rs index a864a2f698..352a92b652 100644 --- a/editor/src/tea/ed_model.rs +++ b/editor/src/tea/ed_model.rs @@ -1,21 +1,25 @@ use crate::graphics::primitives::rect::Rect; +use crate::text_buffer; +use crate::text_buffer::TextBuffer; +use crate::error::EdResult; +use std::path::Path; use std::cmp::Ordering; #[derive(Debug)] pub struct EdModel { - pub lines: Vec, + pub text_buf: TextBuffer, pub caret_pos: Position, pub selection_opt: Option, pub glyph_dim_rect_opt: Option, } -pub fn init_model() -> EdModel { - EdModel { - lines: vec![String::new()], +pub fn init_model(file_path: &Path) -> EdResult { + Ok(EdModel { + text_buf: text_buffer::from_path(file_path)?, caret_pos: Position { line: 0, column: 0 }, selection_opt: None, glyph_dim_rect_opt: None, - } + }) } //Is model.rs the right place for these structs? diff --git a/editor/src/tea/mod.rs b/editor/src/tea/mod.rs index 33bcc304d1..589e859a6b 100644 --- a/editor/src/tea/mod.rs +++ b/editor/src/tea/mod.rs @@ -1,2 +1,3 @@ pub mod ed_model; +pub mod app_model; pub mod update; diff --git a/editor/src/tea/update.rs b/editor/src/tea/update.rs index f83dd677a5..2bf39ae8b5 100644 --- a/editor/src/tea/update.rs +++ b/editor/src/tea/update.rs @@ -1,6 +1,7 @@ use super::ed_model::EdModel; use super::ed_model::{Position, RawSelection}; use crate::util::is_newline; +use crate::tea::app_model::AppModel; use std::cmp::{max, min}; pub fn move_caret_left( @@ -293,52 +294,54 @@ pub fn move_caret_down( (new_caret_pos, new_selection_opt) } -pub fn update_text_state(ed_model: &mut EdModel, received_char: &char) { - ed_model.selection_opt = None; +pub fn handle_new_char(app_model: &mut AppModel, received_char: &char) { + if let Some(ed_model) = app_model.ed_model_opt { + ed_model.selection_opt = None; - match received_char { - '\u{8}' | '\u{7f}' => { - // In Linux, we get a '\u{8}' when you press backspace, - // but in macOS we get '\u{7f}'. - if let Some(last_line) = ed_model.lines.last_mut() { - if !last_line.is_empty() { - last_line.pop(); - } else if ed_model.lines.len() > 1 { - ed_model.lines.pop(); + match received_char { + '\u{8}' | '\u{7f}' => { + // On Linux, '\u{8}' is backspace, + // on macOS '\u{7f}'. + if let Some(last_line) = ed_model.lines.last_mut() { + if !last_line.is_empty() { + last_line.pop(); + } else if ed_model.lines.len() > 1 { + ed_model.lines.pop(); + } + ed_model.caret_pos = + move_caret_left(ed_model.caret_pos, None, false, &ed_model.lines).0; } - ed_model.caret_pos = - move_caret_left(ed_model.caret_pos, None, false, &ed_model.lines).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 - } - ch if is_newline(ch) => { - if let Some(last_line) = ed_model.lines.last_mut() { - last_line.push(*received_char) + '\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.lines.push(String::new()); - ed_model.caret_pos = Position { - line: ed_model.caret_pos.line + 1, - column: 0, - }; - - ed_model.selection_opt = None; - } - _ => { - let nr_lines = ed_model.lines.len(); - - if let Some(last_line) = ed_model.lines.last_mut() { - last_line.push(*received_char); - + ch if is_newline(ch) => { + if let Some(last_line) = ed_model.lines.last_mut() { + last_line.push(*received_char) + } + ed_model.lines.push(String::new()); ed_model.caret_pos = Position { - line: nr_lines - 1, - column: last_line.len(), + line: ed_model.caret_pos.line + 1, + column: 0, }; ed_model.selection_opt = None; } + _ => { + let nr_lines = ed_model.lines.len(); + + if let Some(last_line) = ed_model.lines.last_mut() { + last_line.push(*received_char); + + ed_model.caret_pos = Position { + line: nr_lines - 1, + column: last_line.len(), + }; + + ed_model.selection_opt = None; + } + } } } } diff --git a/editor/src/text_buffer.rs b/editor/src/text_buffer.rs new file mode 100644 index 0000000000..6b228d5e84 --- /dev/null +++ b/editor/src/text_buffer.rs @@ -0,0 +1,86 @@ + +// Adapted from https://github.com/cessen/ropey by Nathan Vegdahl, licensed under the MIT license + +use crate::error::EdResult; +use crate::error::EdError::{TextBufReadFailed, FileOpenFailed}; +use crate::tea::ed_model::{Position, RawSelection}; +use crate::selection::{validate_selection}; +use std::fs::File; +use std::io; +use std::path::Path; +use std::ops::Range; +use ropey::{Rope}; + +#[derive(Debug)] +pub struct TextBuffer { + text_rope: Rope, + path_str: String, + dirty: bool // true if text has been changed, false if all actions following change have executed +} + +impl TextBuffer { + pub fn pop_char(&mut self, cursor_pos: Position) { + let char_indx = self.pos_to_char_indx(cursor_pos); + self.text_rope.remove(char_indx..char_indx); + } + + pub fn del_selection(&mut self, raw_sel: RawSelection) -> EdResult<()> { + let range = self.sel_to_range(raw_sel)?; + self.text_rope.remove(range); + Ok(()) + } + + fn pos_to_char_indx(&self, pos: Position) -> usize { + self.text_rope.line_to_char(pos.line) + pos.column + } + + fn sel_to_range(&self, raw_sel: RawSelection) -> EdResult> { + 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); + + Ok(start_char_indx..end_char_indx) + } + + pub fn from_path(&self, path: &Path) -> EdResult { + // TODO benchmark different file reading methods, see #886 + let text_rope = rope_from_path(path)?; + let path_str = path_to_string(path); + + Ok(TextBuffer { + text_rope, + path_str, + dirty: false + }) + } +} + +fn path_to_string(path: &Path) -> String { + let mut path_str = String::new(); + path_str.push_str(&path.to_string_lossy()); + + path_str +} + +fn rope_from_path(path: &Path) -> EdResult { + match File::open(path) { + Ok(file) => { + let mut buf_reader = &mut io::BufReader::new(file); + match Rope::from_reader(buf_reader) { + Ok(rope) => + Ok(rope), + Err(e) => + Err(TextBufReadFailed { + path_str: path_to_string(path), + err_msg: e.to_string() + }) + } + } + Err(e) => { + Err(FileOpenFailed { + path_str: path_to_string(path), + err_msg: e.to_string() + }) + } + } +} \ No newline at end of file diff --git a/editor/tests/modules/Simple.roc b/editor/tests/modules/SimpleUnformatted.roc similarity index 100% rename from editor/tests/modules/Simple.roc rename to editor/tests/modules/SimpleUnformatted.roc diff --git a/editor/tests/modules/Storage.roc b/editor/tests/modules/Storage.roc deleted file mode 100644 index 950d1d01f1..0000000000 --- a/editor/tests/modules/Storage.roc +++ /dev/null @@ -1,68 +0,0 @@ -interface Storage - exposes [ - Storage, - decoder, - get, - listener, - set - ] - imports [ - Map.{ Map }, - Json.Decode.{ Decoder } as Decode - Json.Encode as Encode - Ports.FromJs as FromJs - Ports.ToJs as ToJs - ] - - -################################################################################ -## TYPES ## -################################################################################ - - -Storage : [ - @Storage (Map Str Decode.Value) -] - - -################################################################################ -## API ## -################################################################################ - - -get : Storage, Str, Decoder a -> [ Ok a, NotInStorage, DecodeError Decode.Error ]* -get = \key, decoder, @Storage map -> - when Map.get map key is - Ok json -> - Decode.decodeValue decoder json - - Err NotFound -> - NotInStorage - - -set : Encode.Value, Str -> Effect {} -set json str = - ToJs.type "setStorage" - |> ToJs.setFields [ - Field "key" (Encode.str str), - Field "value" json - ] - |> ToJs.send - - -decoder : Decoder Storage -decoder = - Decode.mapType Decode.value - |> Decode.map \map -> @Storage map - - -################################################################################ -## PORTS INCOMING ## -################################################################################ - - -listener : (Storage -> msg) -> FromJs.Listener msg -listener toMsg = - FromJs.listen "storageUpdated" - (Decode.map decoder toMsg) - diff --git a/editor/tests/test_file.rs b/editor/tests/test_file.rs index 6fa0b0f26c..32b850e17b 100644 --- a/editor/tests/test_file.rs +++ b/editor/tests/test_file.rs @@ -6,17 +6,17 @@ extern crate indoc; #[cfg(test)] mod test_file { use bumpalo::Bump; - use roc_editor::lang::file::File; + use roc_editor::lang::roc_file::File; use std::path::Path; #[test] fn read_and_fmt_simple_roc_module() { - let simple_module_path = Path::new("./tests/modules/Simple.roc"); + let simple_module_path = Path::new("./tests/modules/SimpleUnformatted.roc"); let arena = Bump::new(); let file = File::read(simple_module_path, &arena) - .expect("Could not read Simple.roc in test_file test"); + .expect("Could not read SimpleUnformatted.roc in test_file test"); assert_eq!( file.fmt(),