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/cli/src/build.rs b/cli/src/build.rs index 41dc30b0fb..16a7bbed87 100644 --- a/cli/src/build.rs +++ b/cli/src/build.rs @@ -42,7 +42,7 @@ pub fn build_file( let loaded = roc_load::file::load_and_monomorphize( &arena, roc_file_path.clone(), - stdlib, + &stdlib, src_dir.as_path(), subs_by_module, ptr_bytes, diff --git a/cli/src/repl/gen.rs b/cli/src/repl/gen.rs index 3a674e87ba..f3642d5ca7 100644 --- a/cli/src/repl/gen.rs +++ b/cli/src/repl/gen.rs @@ -42,7 +42,7 @@ pub fn gen_and_eval(src: &[u8], target: Triple, opt_level: OptLevel) -> Result Vec { // In the build script for the builtins module, we compile the builtins bitcode and set // BUILTINS_BC to the path to the compiled output. - let path: &'static str = env!( - "BUILTINS_BC", - "Env var BUILTINS_BC not found. Is there a problem with the build script?" - ); - let mut builtins_bitcode = File::open(path).expect("Unable to find builtins bitcode source"); + let mut builtins_bitcode = File::open(PATH).expect("Unable to find builtins bitcode source"); let mut buffer = Vec::new(); builtins_bitcode .read_to_end(&mut buffer) diff --git a/compiler/builtins/src/std.rs b/compiler/builtins/src/std.rs index 2e211d7b18..cbd3a01fc1 100644 --- a/compiler/builtins/src/std.rs +++ b/compiler/builtins/src/std.rs @@ -291,6 +291,15 @@ pub fn types() -> MutMap { ), ); + // bitwiseXor : Int a, Int a -> Int a + add_type( + Symbol::NUM_BITWISE_XOR, + top_level_function( + vec![int_type(flex(TVAR1)), int_type(flex(TVAR1))], + Box::new(int_type(flex(TVAR1))), + ), + ); + // rem : Int a, Int a -> Result (Int a) [ DivByZero ]* add_type( Symbol::NUM_REM, diff --git a/compiler/builtins/src/unique.rs b/compiler/builtins/src/unique.rs index 317715cf03..23fd052e13 100644 --- a/compiler/builtins/src/unique.rs +++ b/compiler/builtins/src/unique.rs @@ -294,6 +294,15 @@ pub fn types() -> MutMap { ) }); + // bitwiseAnd : Attr * Int, Attr * Int -> Attr * Int + add_type(Symbol::NUM_BITWISE_XOR, { + let_tvars! { star1, star2, star3, int }; + unique_function( + vec![int_type(star1, int), int_type(star2, int)], + int_type(star3, int), + ) + }); + // divFloat : Float, Float -> Float add_type(Symbol::NUM_DIV_FLOAT, { let_tvars! { star1, star2, star3, star4, star5}; diff --git a/compiler/can/src/builtins.rs b/compiler/can/src/builtins.rs index 007c48bee8..96b2e1c21d 100644 --- a/compiler/can/src/builtins.rs +++ b/compiler/can/src/builtins.rs @@ -117,7 +117,8 @@ pub fn builtin_defs_map(symbol: Symbol, var_store: &mut VarStore) -> Option NUM_ASIN => num_asin, NUM_MAX_INT => num_max_int, NUM_MIN_INT => num_min_int, - NUM_BITWISE_AND => num_bitwise_and + NUM_BITWISE_AND => num_bitwise_and, + NUM_BITWISE_XOR => num_bitwise_xor } } @@ -1153,6 +1154,11 @@ fn num_bitwise_and(symbol: Symbol, var_store: &mut VarStore) -> Def { num_binop(symbol, var_store, LowLevel::NumBitwiseAnd) } +/// Num.bitwiseXor : Int, Int -> Int +fn num_bitwise_xor(symbol: Symbol, var_store: &mut VarStore) -> Def { + num_binop(symbol, var_store, LowLevel::NumBitwiseXor) +} + /// List.isEmpty : List * -> Bool fn list_is_empty(symbol: Symbol, var_store: &mut VarStore) -> Def { let list_var = var_store.fresh(); diff --git a/compiler/gen/src/llvm/build.rs b/compiler/gen/src/llvm/build.rs index 969a1796d1..58ce537667 100644 --- a/compiler/gen/src/llvm/build.rs +++ b/compiler/gen/src/llvm/build.rs @@ -3599,7 +3599,7 @@ fn run_low_level<'a, 'ctx, 'env>( build_num_binop(env, parent, lhs_arg, lhs_layout, rhs_arg, rhs_layout, op) } - NumBitwiseAnd => { + NumBitwiseAnd | NumBitwiseXor => { debug_assert_eq!(args.len(), 2); let (lhs_arg, lhs_layout) = load_symbol_and_layout(env, scope, &args[0]); @@ -3953,6 +3953,7 @@ fn build_int_binop<'a, 'ctx, 'env>( NumDivUnchecked => bd.build_int_signed_div(lhs, rhs, "div_int").into(), NumPowInt => call_bitcode_fn(env, &[lhs.into(), rhs.into()], &bitcode::NUM_POW_INT), NumBitwiseAnd => bd.build_and(lhs, rhs, "int_bitwise_and").into(), + NumBitwiseXor => bd.build_xor(lhs, rhs, "int_bitwise_xor").into(), _ => { unreachable!("Unrecognized int binary operation: {:?}", op); } diff --git a/compiler/gen/tests/gen_num.rs b/compiler/gen/tests/gen_num.rs index d041b17833..cdab29699e 100644 --- a/compiler/gen/tests/gen_num.rs +++ b/compiler/gen/tests/gen_num.rs @@ -757,6 +757,14 @@ mod gen_num { assert_evals_to!("Num.bitwiseAnd 200 0", 0, i64); } + #[test] + fn bitwise_xor() { + assert_evals_to!("Num.bitwiseXor 20 20", 0, i64); + assert_evals_to!("Num.bitwiseXor 15 14", 1, i64); + assert_evals_to!("Num.bitwiseXor 7 15", 8, i64); + assert_evals_to!("Num.bitwiseXor 200 0", 200, i64); + } + #[test] fn lt_i64() { assert_evals_to!("1 < 2", true, bool); diff --git a/compiler/gen/tests/gen_primitives.rs b/compiler/gen/tests/gen_primitives.rs index 22517e01e6..b146c17783 100644 --- a/compiler/gen/tests/gen_primitives.rs +++ b/compiler/gen/tests/gen_primitives.rs @@ -1901,6 +1901,26 @@ mod gen_primitives { ); } + #[test] + fn case_or_pattern() { + // the `0` branch body should only be generated once in the future + // it is currently duplicated + assert_evals_to!( + indoc!( + r#" + x : [ Red, Green, Blue ] + x = Red + + when x is + Red | Green -> 0 + Blue -> 1 + "# + ), + 0, + i64 + ); + } + #[test] #[ignore] fn rosetree_basic() { @@ -1932,4 +1952,29 @@ mod gen_primitives { bool ); } + + #[test] + fn case_jump() { + // the decision tree will generate a jump to the `1` branch here + assert_evals_to!( + indoc!( + r#" + app "test" provides [ main ] to "./platform" + + ConsList a : [ Cons a (ConsList a), Nil ] + + x : ConsList I64 + x = Nil + + main = + when Pair x x is + Pair Nil _ -> 1 + Pair _ Nil -> 2 + Pair (Cons a _) (Cons b _) -> a + b + 3 + "# + ), + 1, + i64 + ); + } } diff --git a/compiler/gen/tests/helpers/eval.rs b/compiler/gen/tests/helpers/eval.rs index 3fb2ff590e..76801146ad 100644 --- a/compiler/gen/tests/helpers/eval.rs +++ b/compiler/gen/tests/helpers/eval.rs @@ -19,7 +19,7 @@ fn promote_expr_to_module(src: &str) -> String { pub fn helper<'a>( arena: &'a bumpalo::Bump, src: &str, - stdlib: roc_builtins::std::StdLib, + stdlib: &'a roc_builtins::std::StdLib, leak: bool, context: &'a inkwell::context::Context, ) -> (&'static str, String, Library) { @@ -295,40 +295,6 @@ pub fn helper<'a>( (main_fn_name, delayed_errors.join("\n"), lib) } -// TODO this is almost all code duplication with assert_llvm_evals_to -// the only difference is that this calls uniq_expr instead of can_expr. -// Should extract the common logic into test helpers. -#[macro_export] -macro_rules! assert_opt_evals_to { - ($src:expr, $expected:expr, $ty:ty, $transform:expr, $leak:expr) => { - use bumpalo::Bump; - use inkwell::context::Context; - use roc_gen::run_jit_function; - - let arena = Bump::new(); - - let context = Context::create(); - - // don't use uniqueness types any more - // let stdlib = roc_builtins::unique::uniq_stdlib(); - let stdlib = roc_builtins::std::standard_stdlib(); - - let (main_fn_name, errors, lib) = - $crate::helpers::eval::helper(&arena, $src, stdlib, $leak, &context); - - let transform = |success| { - let expected = $expected; - let given = $transform(success); - assert_eq!(&given, &expected); - }; - run_jit_function!(lib, main_fn_name, $ty, transform, errors) - }; - - ($src:expr, $expected:expr, $ty:ty, $transform:expr) => { - assert_opt_evals_to!($src, $expected, $ty, $transform, true) - }; -} - #[macro_export] macro_rules! assert_llvm_evals_to { ($src:expr, $expected:expr, $ty:ty, $transform:expr, $leak:expr) => { @@ -337,9 +303,10 @@ macro_rules! assert_llvm_evals_to { use roc_gen::run_jit_function; let arena = Bump::new(); - let context = Context::create(); - let stdlib = roc_builtins::std::standard_stdlib(); + + // NOTE the stdlib must be in the arena; just taking a reference will segfault + let stdlib = arena.alloc(roc_builtins::std::standard_stdlib()); let (main_fn_name, errors, lib) = $crate::helpers::eval::helper(&arena, $src, stdlib, $leak, &context); @@ -377,7 +344,8 @@ macro_rules! assert_evals_to { assert_llvm_evals_to!($src, $expected, $ty, $transform, $leak); } { - assert_opt_evals_to!($src, $expected, $ty, $transform, $leak); + // NOTE at the moment, the optimized tests do the same thing + // assert_opt_evals_to!($src, $expected, $ty, $transform, $leak); } }; } diff --git a/compiler/gen_dev/tests/helpers/eval.rs b/compiler/gen_dev/tests/helpers/eval.rs index b464a0c10c..80c994569e 100644 --- a/compiler/gen_dev/tests/helpers/eval.rs +++ b/compiler/gen_dev/tests/helpers/eval.rs @@ -47,7 +47,7 @@ pub fn helper<'a>( arena, filename, &module_src, - stdlib, + &stdlib, src_dir, exposed_types, 8, diff --git a/compiler/load/src/file.rs b/compiler/load/src/file.rs index 4e28a4db4a..a114ade91b 100644 --- a/compiler/load/src/file.rs +++ b/compiler/load/src/file.rs @@ -699,7 +699,7 @@ struct State<'a> { pub root_id: ModuleId, pub platform_id: Option, pub goal_phase: Phase, - pub stdlib: StdLib, + pub stdlib: &'a StdLib, pub exposed_types: SubsByModule, pub output_path: Option<&'a str>, pub platform_path: Option>, @@ -944,7 +944,7 @@ fn enqueue_task<'a>( pub fn load_and_typecheck( arena: &Bump, filename: PathBuf, - stdlib: StdLib, + stdlib: &StdLib, src_dir: &Path, exposed_types: SubsByModule, ptr_bytes: u32, @@ -970,7 +970,7 @@ pub fn load_and_typecheck( pub fn load_and_monomorphize<'a>( arena: &'a Bump, filename: PathBuf, - stdlib: StdLib, + stdlib: &'a StdLib, src_dir: &Path, exposed_types: SubsByModule, ptr_bytes: u32, @@ -997,7 +997,7 @@ pub fn load_and_monomorphize_from_str<'a>( arena: &'a Bump, filename: PathBuf, src: &'a str, - stdlib: StdLib, + stdlib: &'a StdLib, src_dir: &Path, exposed_types: SubsByModule, ptr_bytes: u32, @@ -1146,7 +1146,7 @@ fn load<'a>( arena: &'a Bump, //filename: PathBuf, load_start: LoadStart<'a>, - stdlib: StdLib, + stdlib: &'a StdLib, src_dir: &Path, exposed_types: SubsByModule, goal_phase: Phase, diff --git a/compiler/load/tests/test_load.rs b/compiler/load/tests/test_load.rs index 966cf31c52..6e87ccdda3 100644 --- a/compiler/load/tests/test_load.rs +++ b/compiler/load/tests/test_load.rs @@ -82,7 +82,7 @@ mod test_load { roc_load::file::load_and_typecheck( arena, full_file_path, - stdlib, + &stdlib, dir.path(), exposed_types, 8, @@ -124,7 +124,7 @@ mod test_load { let loaded = roc_load::file::load_and_typecheck( &arena, filename, - roc_builtins::std::standard_stdlib(), + &roc_builtins::std::standard_stdlib(), src_dir.as_path(), subs_by_module, 8, @@ -287,7 +287,7 @@ mod test_load { let loaded = roc_load::file::load_and_typecheck( &arena, filename, - roc_builtins::std::standard_stdlib(), + &roc_builtins::std::standard_stdlib(), src_dir.as_path(), subs_by_module, 8, diff --git a/compiler/module/src/low_level.rs b/compiler/module/src/low_level.rs index 989c113da8..17c5dc62b0 100644 --- a/compiler/module/src/low_level.rs +++ b/compiler/module/src/low_level.rs @@ -59,6 +59,7 @@ pub enum LowLevel { NumAcos, NumAsin, NumBitwiseAnd, + NumBitwiseXor, Eq, NotEq, And, diff --git a/compiler/module/src/symbol.rs b/compiler/module/src/symbol.rs index c749068f1a..f7b87b9fa1 100644 --- a/compiler/module/src/symbol.rs +++ b/compiler/module/src/symbol.rs @@ -823,15 +823,16 @@ define_builtins! { 79 NUM_AT_BINARY32: "@Binary32" 80 NUM_BINARY32: "Binary32" imported 81 NUM_BITWISE_AND: "bitwiseAnd" - 82 NUM_SUB_WRAP: "subWrap" - 83 NUM_SUB_CHECKED: "subChecked" - 84 NUM_MUL_WRAP: "mulWrap" - 85 NUM_MUL_CHECKED: "mulChecked" - 86 NUM_INT: "Int" imported - 87 NUM_FLOAT: "Float" imported - 88 NUM_AT_NATURAL: "@Natural" - 89 NUM_NATURAL: "Natural" imported - 90 NUM_NAT: "Nat" imported + 82 NUM_BITWISE_XOR: "bitwiseXor" + 83 NUM_SUB_WRAP: "subWrap" + 84 NUM_SUB_CHECKED: "subChecked" + 85 NUM_MUL_WRAP: "mulWrap" + 86 NUM_MUL_CHECKED: "mulChecked" + 87 NUM_INT: "Int" imported + 88 NUM_FLOAT: "Float" imported + 89 NUM_AT_NATURAL: "@Natural" + 90 NUM_NATURAL: "Natural" imported + 91 NUM_NAT: "Nat" imported } 2 BOOL: "Bool" => { 0 BOOL_BOOL: "Bool" imported // the Bool.Bool type alias diff --git a/compiler/mono/src/borrow.rs b/compiler/mono/src/borrow.rs index 8fe05e3e7e..7d5312aca1 100644 --- a/compiler/mono/src/borrow.rs +++ b/compiler/mono/src/borrow.rs @@ -578,9 +578,8 @@ pub fn lowlevel_borrow_signature(arena: &Bump, op: LowLevel) -> &[bool] { Eq | NotEq | And | Or | NumAdd | NumAddWrap | NumAddChecked | NumSub | NumSubWrap | NumSubChecked | NumMul | NumMulWrap | NumMulChecked | NumGt | NumGte | NumLt | NumLte - | NumCompare | NumDivUnchecked | NumRemUnchecked | NumPow | NumPowInt | NumBitwiseAnd => { - arena.alloc_slice_copy(&[irrelevant, irrelevant]) - } + | NumCompare | NumDivUnchecked | NumRemUnchecked | NumPow | NumPowInt | NumBitwiseAnd + | NumBitwiseXor => arena.alloc_slice_copy(&[irrelevant, irrelevant]), NumAbs | NumNeg | NumSin | NumCos | NumSqrtUnchecked | NumRound | NumCeiling | NumFloor | NumToFloat | Not | NumIsFinite | NumAtan | NumAcos | NumAsin => { diff --git a/compiler/mono/src/decision_tree.rs b/compiler/mono/src/decision_tree.rs index 4cad47affe..67fb69e78d 100644 --- a/compiler/mono/src/decision_tree.rs +++ b/compiler/mono/src/decision_tree.rs @@ -905,7 +905,10 @@ pub fn optimize_when<'a>( let decision_tree = compile(patterns); let decider = tree_to_decider(decision_tree); - let target_counts = count_targets(&decider); + + // for each target (branch body), count in how many ways it can be reached + let mut target_counts = bumpalo::vec![in env.arena; 0; indexed_branches.len()]; + count_targets(&mut target_counts, &decider); let mut choices = MutMap::default(); let mut jumps = Vec::new(); @@ -913,8 +916,9 @@ pub fn optimize_when<'a>( for (index, branch) in indexed_branches.into_iter() { let ((branch_index, choice), opt_jump) = create_choices(&target_counts, index, branch); - if let Some(jump) = opt_jump { - jumps.push(jump); + if let Some((index, body)) = opt_jump { + let id = JoinPointId(env.unique_symbol()); + jumps.push((index, id, body)); } choices.insert(branch_index, choice); @@ -922,7 +926,7 @@ pub fn optimize_when<'a>( let choice_decider = insert_choices(&choices, decider); - decide_to_branching( + let mut stmt = decide_to_branching( env, procs, layout_cache, @@ -931,7 +935,18 @@ pub fn optimize_when<'a>( ret_layout, choice_decider, &jumps, - ) + ); + + for (_, id, body) in jumps.into_iter() { + stmt = Stmt::Join { + id, + parameters: &[], + continuation: env.arena.alloc(body), + remainder: env.arena.alloc(stmt), + }; + } + + stmt } #[derive(Debug)] @@ -1383,7 +1398,7 @@ fn decide_to_branching<'a>( cond_layout: Layout<'a>, ret_layout: Layout<'a>, decider: Decider<'a, Choice<'a>>, - jumps: &Vec<(u64, Stmt<'a>)>, + jumps: &Vec<(u64, JoinPointId, Stmt<'a>)>, ) -> Stmt<'a> { use Choice::*; use Decider::*; @@ -1392,12 +1407,11 @@ fn decide_to_branching<'a>( match decider { Leaf(Jump(label)) => { - // we currently inline the jumps: does fewer jumps but produces a larger artifact - let (_, expr) = jumps - .iter() - .find(|(l, _)| l == &label) + let index = jumps + .binary_search_by_key(&label, |ref r| r.0) .expect("jump not in list of jumps"); - expr.clone() + + Stmt::Jump(jumps[index].1, &[]) } Leaf(Inline(expr)) => expr, Chain { @@ -1674,39 +1688,32 @@ fn to_chain<'a>( /// If a target appears exactly once in a Decider, the corresponding expression /// can be inlined. Whether things are inlined or jumps is called a "choice". -fn count_targets(decision_tree: &Decider) -> MutMap { - let mut result = MutMap::default(); - count_targets_help(decision_tree, &mut result); - - result -} - -fn count_targets_help(decision_tree: &Decider, targets: &mut MutMap) { +fn count_targets(targets: &mut bumpalo::collections::Vec, initial: &Decider) { use Decider::*; - match decision_tree { - Leaf(target) => match targets.get_mut(target) { - None => { - targets.insert(*target, 1); + + let mut stack = vec![initial]; + + while let Some(decision_tree) = stack.pop() { + match decision_tree { + Leaf(target) => { + targets[*target as usize] += 1; } - Some(current) => { - *current += 1; + + Chain { + success, failure, .. + } => { + stack.push(success); + stack.push(failure); } - }, - Chain { - success, failure, .. - } => { - count_targets_help(success, targets); - count_targets_help(failure, targets); - } + FanOut { + tests, fallback, .. + } => { + stack.push(fallback); - FanOut { - tests, fallback, .. - } => { - count_targets_help(fallback, targets); - - for (_, decider) in tests { - count_targets_help(decider, targets); + for (_, decider) in tests { + stack.push(decider); + } } } } @@ -1714,11 +1721,11 @@ fn count_targets_help(decision_tree: &Decider, targets: &mut MutMap( - target_counts: &MutMap, + target_counts: &bumpalo::collections::Vec<'a, u64>, target: u64, branch: Stmt<'a>, ) -> ((u64, Choice<'a>), Option<(u64, Stmt<'a>)>) { - match target_counts.get(&target) { + match target_counts.get(target as usize) { None => unreachable!( "this should never happen: {:?} not in {:?}", target, target_counts diff --git a/compiler/mono/tests/test_mono.rs b/compiler/mono/tests/test_mono.rs index 0b14ca7e13..f7763da5ee 100644 --- a/compiler/mono/tests/test_mono.rs +++ b/compiler/mono/tests/test_mono.rs @@ -58,7 +58,7 @@ mod test_mono { arena, filename, &module_src, - stdlib, + &stdlib, src_dir, exposed_types, 8, diff --git a/compiler/solve/tests/solve_expr.rs b/compiler/solve/tests/solve_expr.rs index 792d6948f8..07d801402a 100644 --- a/compiler/solve/tests/solve_expr.rs +++ b/compiler/solve/tests/solve_expr.rs @@ -59,7 +59,7 @@ mod solve_expr { let result = roc_load::file::load_and_typecheck( arena, full_file_path, - stdlib, + &stdlib, dir.path(), exposed_types, 8, diff --git a/docs/src/main.rs b/docs/src/main.rs index 1089b54686..cb27432fc4 100644 --- a/docs/src/main.rs +++ b/docs/src/main.rs @@ -129,7 +129,7 @@ fn files_to_documentations( let mut loaded = roc_load::file::load_and_typecheck( &arena, filename, - std_lib.clone(), + &std_lib, src_dir, MutMap::default(), 8, // TODO: Is it okay to hardcode ptr_bytes here? I think it should be fine since we'er only type checking (also, 8 => 32bit system) diff --git a/editor/Cargo.toml b/editor/Cargo.toml index fcfda19a8a..61aec5ade5 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] @@ -83,9 +84,9 @@ quickcheck_macros = "0.8" # uncomment everything below if you have made changes to any shaders and # want to compile them to .spv -#[build-dependencies] -#rayon = "1.5.0" -#anyhow = "1.0" -#fs_extra = "1.1" -#glob = "0.3" -#shaderc = "0.6" +# [build-dependencies] +# rayon = "1.5.0" +# anyhow = "1.0" +# fs_extra = "1.1" +# glob = "0.3" +# shaderc = "0.6" diff --git a/editor/README.md b/editor/README.md index e92385db90..7009c372f8 100644 --- a/editor/README.md +++ b/editor/README.md @@ -3,7 +3,7 @@ Run the following from the roc folder: ``` -cargo run edit +cargo run edit examples/hello-world/Hello.roc ``` ## Troubleshooting diff --git a/editor/src/error.rs b/editor/src/error.rs index c3d2f53cbc..42f428f6fc 100644 --- a/editor/src/error.rs +++ b/editor/src/error.rs @@ -10,22 +10,32 @@ use snafu::{Backtrace, ErrorCompat, Snafu}; #[snafu(visibility(pub))] pub enum EdError { #[snafu(display( - "OutOfBounds: index {} was out of bounds for Vec with length {}.", + "OutOfBounds: index {} was out of bounds for {} with length {}.", index, - vec_len + collection_name, + len ))] OutOfBounds { index: usize, - vec_len: usize, + collection_name: String, + 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 {}, + 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))] + TextBufReadFailed { path_str: String, err_msg: String }, } pub type EdResult = std::result::Result; diff --git a/editor/src/graphics/colors.rs b/editor/src/graphics/colors.rs index cce601fe75..89867b8e14 100644 --- a/editor/src/graphics/colors.rs +++ b/editor/src/graphics/colors.rs @@ -1,5 +1,19 @@ -pub const WHITE: [f32; 3] = [1.0, 1.0, 1.0]; -pub const TXT_COLOR: [f32; 4] = [0.4666, 0.2, 1.0, 1.0]; -pub const CODE_COLOR: [f32; 4] = [0.0, 0.05, 0.46, 1.0]; -pub const CARET_COLOR: [f32; 3] = WHITE; -pub const SELECT_COLOR: [f32; 3] = [0.45, 0.61, 1.0]; +pub const WHITE: (f32, f32, f32, f32) = (1.0, 1.0, 1.0, 1.0); +pub const TXT_COLOR: (f32, f32, f32, f32) = (1.0, 1.0, 1.0, 1.0); +pub const CODE_COLOR: (f32, f32, f32, f32) = (0.21, 0.55, 0.83, 1.0); +pub const CARET_COLOR: (f32, f32, f32, f32) = WHITE; +pub const SELECT_COLOR: (f32, f32, f32, f32) = (0.45, 0.61, 1.0, 1.0); +pub const BG_COLOR: (f32, f32, f32, f32) = (0.11, 0.11, 0.13, 1.0); + +pub fn to_wgpu_color((r, g, b, a): (f32, f32, f32, f32)) -> wgpu::Color { + wgpu::Color { + r: r as f64, + g: g as f64, + b: b as f64, + a: a as f64, + } +} + +pub fn to_slice((r, g, b, a): (f32, f32, f32, f32)) -> [f32; 4] { + [r, g, b, a] +} diff --git a/editor/src/graphics/lowlevel/buffer.rs b/editor/src/graphics/lowlevel/buffer.rs index 0be9cfeb8c..a3e8fc0a42 100644 --- a/editor/src/graphics/lowlevel/buffer.rs +++ b/editor/src/graphics/lowlevel/buffer.rs @@ -1,6 +1,7 @@ // Adapted from https://github.com/sotrh/learn-wgpu // by Benjamin Hansen, licensed under the MIT license use super::vertex::Vertex; +use crate::graphics::colors::to_slice; use crate::graphics::primitives::rect::Rect; use bumpalo::collections::Vec as BumpVec; use wgpu::util::{BufferInitDescriptor, DeviceExt}; @@ -25,7 +26,7 @@ impl QuadBufferBuilder { coords.y, coords.x + rect.width, coords.y + rect.height, - rect.color, + to_slice(rect.color), ) } @@ -35,7 +36,7 @@ impl QuadBufferBuilder { min_y: f32, max_x: f32, max_y: f32, - color: [f32; 3], + color: [f32; 4], ) -> Self { self.vertex_data.extend(&[ Vertex { diff --git a/editor/src/graphics/lowlevel/vertex.rs b/editor/src/graphics/lowlevel/vertex.rs index a77b6a2a29..f7b1776f5d 100644 --- a/editor/src/graphics/lowlevel/vertex.rs +++ b/editor/src/graphics/lowlevel/vertex.rs @@ -6,7 +6,7 @@ use cgmath::Vector2; pub struct Vertex { #[allow(dead_code)] pub position: Vector2, - pub color: [f32; 3], + pub color: [f32; 4], } unsafe impl bytemuck::Pod for Vertex {} @@ -28,7 +28,7 @@ impl Vertex { wgpu::VertexAttributeDescriptor { offset: std::mem::size_of::<[f32; 2]>() as wgpu::BufferAddress, shader_location: 1, - format: wgpu::VertexFormat::Float3, + format: wgpu::VertexFormat::Float4, }, ], }; diff --git a/editor/src/graphics/primitives/rect.rs b/editor/src/graphics/primitives/rect.rs index 4fdfdcf730..7560c0d10a 100644 --- a/editor/src/graphics/primitives/rect.rs +++ b/editor/src/graphics/primitives/rect.rs @@ -5,5 +5,5 @@ pub struct Rect { pub top_left_coords: Vector2, pub width: f32, pub height: f32, - pub color: [f32; 3], + pub color: (f32, f32, f32, f32), } diff --git a/editor/src/graphics/primitives/text.rs b/editor/src/graphics/primitives/text.rs index b996daa5f8..27479708a7 100644 --- a/editor/src/graphics/primitives/text.rs +++ b/editor/src/graphics/primitives/text.rs @@ -2,7 +2,7 @@ // by Benjamin Hansen, licensed under the MIT license use super::rect::Rect; -use crate::graphics::colors::CODE_COLOR; +use crate::graphics::colors::{CODE_COLOR, WHITE}; use crate::graphics::style::{CODE_FONT_SIZE, CODE_TXT_XY}; use ab_glyph::{FontArc, Glyph, InvalidFont}; use cgmath::{Vector2, Vector4}; @@ -102,7 +102,7 @@ fn glyph_to_rect(glyph: &wgpu_glyph::SectionGlyph) -> Rect { top_left_coords: [position.x, top_y].into(), width, height, - color: [1.0, 1.0, 1.0], + color: WHITE, } } diff --git a/editor/src/graphics/shaders/rect.frag b/editor/src/graphics/shaders/rect.frag index 2246d723b6..c444a789ad 100644 --- a/editor/src/graphics/shaders/rect.frag +++ b/editor/src/graphics/shaders/rect.frag @@ -4,10 +4,10 @@ // Check build_shaders.rs on how to recompile shaders if you have made changes to this file -layout(location=0) in vec3 color; +layout(location=0) in vec4 color; layout(location=0) out vec4 fColor; void main() { - fColor = vec4(color, 1.0); + fColor = color; } \ No newline at end of file diff --git a/editor/src/graphics/shaders/rect.frag.spv b/editor/src/graphics/shaders/rect.frag.spv index ae9c49a4ad..d1981bc7ae 100644 Binary files a/editor/src/graphics/shaders/rect.frag.spv and b/editor/src/graphics/shaders/rect.frag.spv differ diff --git a/editor/src/graphics/shaders/rect.vert b/editor/src/graphics/shaders/rect.vert index 3f885aa159..5c4d85b11d 100644 --- a/editor/src/graphics/shaders/rect.vert +++ b/editor/src/graphics/shaders/rect.vert @@ -11,9 +11,9 @@ layout(set = 0, binding = 0) uniform Globals { } global; layout(location=0) in vec2 aPosition; -layout(location=1) in vec3 aColor; +layout(location=1) in vec4 aColor; -layout(location=0) out vec3 vColor; +layout(location=0) out vec4 vColor; void main() { gl_Position = global.ortho * vec4(aPosition, 0, 1); diff --git a/editor/src/graphics/shaders/rect.vert.spv b/editor/src/graphics/shaders/rect.vert.spv index 116f559047..ffd54e67a9 100644 Binary files a/editor/src/graphics/shaders/rect.vert.spv and b/editor/src/graphics/shaders/rect.vert.spv differ diff --git a/editor/src/graphics/style.rs b/editor/src/graphics/style.rs index a32deb6558..c166fd3808 100644 --- a/editor/src/graphics/style.rs +++ b/editor/src/graphics/style.rs @@ -1,2 +1,2 @@ -pub const CODE_FONT_SIZE: f32 = 40.0; +pub const CODE_FONT_SIZE: f32 = 30.0; pub const CODE_TXT_XY: (f32, f32) = (30.0, 90.0); diff --git a/editor/src/keyboard_input.rs b/editor/src/keyboard_input.rs index bbf3969886..40fd9e4a4c 100644 --- a/editor/src/keyboard_input.rs +++ b/editor/src/keyboard_input.rs @@ -1,12 +1,14 @@ -use crate::tea::ed_model::EdModel; -use crate::tea::update::{move_caret_down, move_caret_left, move_caret_right, move_caret_up}; +use crate::mvc::ed_model::EdModel; +use crate::mvc::update::{ + move_caret_down, move_caret_left, move_caret_right, move_caret_up, MoveCaretFun, +}; use winit::event::{ElementState, ModifiersState, VirtualKeyCode}; pub fn handle_keydown( elem_state: ElementState, virtual_keycode: VirtualKeyCode, modifiers: ModifiersState, - model: &mut EdModel, + ed_model: &mut EdModel, ) { use winit::event::VirtualKeyCode::*; @@ -15,46 +17,10 @@ pub fn handle_keydown( } match virtual_keycode { - Left => { - let (new_caret_pos, new_selection_opt) = move_caret_left( - model.caret_pos, - model.selection_opt, - modifiers.shift(), - &model.lines, - ); - model.caret_pos = new_caret_pos; - model.selection_opt = new_selection_opt; - } - Up => { - let (new_caret_pos, new_selection_opt) = move_caret_up( - model.caret_pos, - model.selection_opt, - modifiers.shift(), - &model.lines, - ); - model.caret_pos = new_caret_pos; - model.selection_opt = new_selection_opt; - } - Right => { - let (new_caret_pos, new_selection_opt) = move_caret_right( - model.caret_pos, - model.selection_opt, - modifiers.shift(), - &model.lines, - ); - model.caret_pos = new_caret_pos; - model.selection_opt = new_selection_opt; - } - Down => { - let (new_caret_pos, new_selection_opt) = move_caret_down( - model.caret_pos, - model.selection_opt, - modifiers.shift(), - &model.lines, - ); - model.caret_pos = new_caret_pos; - model.selection_opt = new_selection_opt; - } + Left => handle_arrow(move_caret_left, &modifiers, ed_model), + Up => handle_arrow(move_caret_up, &modifiers, ed_model), + Right => handle_arrow(move_caret_right, &modifiers, ed_model), + Down => handle_arrow(move_caret_down, &modifiers, ed_model), Copy => { todo!("copy"); } @@ -68,6 +34,17 @@ pub fn handle_keydown( } } +fn handle_arrow(move_caret_fun: MoveCaretFun, modifiers: &ModifiersState, ed_model: &mut EdModel) { + let (new_caret_pos, new_selection_opt) = move_caret_fun( + ed_model.caret_pos, + ed_model.selection_opt, + modifiers.shift(), + &ed_model.text_buf, + ); + ed_model.caret_pos = new_caret_pos; + ed_model.selection_opt = new_selection_opt; +} + // pub fn handle_text_input( // text_state: &mut String, // elem_state: ElementState, diff --git a/editor/src/lang/mod.rs b/editor/src/lang/mod.rs index add841ec13..a4025ace23 100644 --- a/editor/src/lang/mod.rs +++ b/editor/src/lang/mod.rs @@ -1,9 +1,9 @@ pub mod ast; mod def; mod expr; -pub mod file; mod module; mod pattern; mod pool; +pub mod roc_file; mod scope; mod types; 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..fbfffd29bb 100644 --- a/editor/src/lib.rs +++ b/editor/src/lib.rs @@ -13,22 +13,21 @@ extern crate pest; #[macro_use] extern crate pest_derive; -use crate::error::EdError::MissingGlyphDims; use crate::error::{print_err, EdResult}; -use crate::graphics::colors::{CARET_COLOR, CODE_COLOR, TXT_COLOR}; +use crate::graphics::colors::{CODE_COLOR, TXT_COLOR}; use crate::graphics::lowlevel::buffer::create_rect_buffers; use crate::graphics::lowlevel::ortho::update_ortho_buffer; use crate::graphics::lowlevel::pipelines; -use crate::graphics::primitives::rect::Rect; use crate::graphics::primitives::text::{ build_glyph_brush, example_code_glyph_rect, queue_text_draw, Text, }; use crate::graphics::style::CODE_FONT_SIZE; 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 bumpalo::collections::Vec as BumpVec; +use crate::mvc::app_model::AppModel; +use crate::mvc::ed_model::EdModel; +use crate::mvc::{ed_model, ed_view, update}; +use crate::resources::strings::NOTHING_OPENED; +use crate::vec_result::get_res; use bumpalo::Bump; use cgmath::Vector2; use ed_model::Position; @@ -47,28 +46,42 @@ pub mod error; pub mod graphics; mod keyboard_input; pub mod lang; +mod mvc; +mod resources; mod selection; -mod tea; +mod text_buffer; mod util; mod vec_result; /// 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.is_empty() { + 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(1200.0, 1000.0)) .build(&event_loop) .unwrap(); @@ -105,7 +118,7 @@ fn run_event_loop() -> Result<(), Box> { let local_spawner = local_pool.spawner(); // Prepare swap chain - let render_format = wgpu::TextureFormat::Bgra8UnormSrgb; + let render_format = wgpu::TextureFormat::Bgra8Unorm; let mut size = window.inner_size(); let swap_chain_descr = wgpu::SwapChainDescriptor { @@ -124,8 +137,26 @@ 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 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(mut 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 +211,9 @@ fn run_event_loop() -> Result<(), Box> { event: event::WindowEvent::ReceivedCharacter(ch), .. } => { - update::update_text_state(&mut ed_model, &ch); + if let Err(e) = update::handle_new_char(&mut app_model, &ch) { + print_err(&e) + } } //Keyboard Input Event::WindowEvent { @@ -188,12 +221,16 @@ fn run_event_loop() -> Result<(), Box> { .. } => { if let Some(virtual_keycode) = input.virtual_keycode { - keyboard_input::handle_keydown( - input.state, - virtual_keycode, - keyboard_modifiers, - &mut ed_model, - ); + if let Some(ref mut ed_model) = app_model.ed_model_opt { + if ed_model.has_focus { + keyboard_input::handle_keydown( + input.state, + virtual_keycode, + keyboard_modifiers, + ed_model, + ); + } + } } } //Modifiers Changed @@ -216,17 +253,21 @@ fn run_event_loop() -> Result<(), Box> { .expect("Failed to acquire next SwapChainFrame") .output; - queue_all_text( - &size, - &ed_model.lines, - ed_model.caret_pos, - CODE_TXT_XY.into(), - &mut glyph_brush, - ); + if let Some(ed_model) = &app_model.ed_model_opt { + //TODO don't pass invisible lines + queue_editor_text( + &size, + &ed_model.text_buf.all_lines(&arena), + ed_model.caret_pos, + CODE_TXT_XY.into(), + &mut glyph_brush, + ); + } else { + queue_no_file_text(&size, NOTHING_OPENED, CODE_TXT_XY.into(), &mut glyph_brush); + } match draw_all_rects( - &ed_model, - &ed_model.glyph_dim_rect_opt, + &app_model.ed_model_opt, &arena, &mut encoder, &frame.view, @@ -272,41 +313,30 @@ fn run_event_loop() -> Result<(), Box> { } fn draw_all_rects( - ed_model: &EdModel, - glyph_dim_rect_opt: &Option, + ed_model_opt: &Option, arena: &Bump, encoder: &mut CommandEncoder, texture_view: &TextureView, gpu_device: &wgpu::Device, rect_resources: &RectResources, ) -> EdResult<()> { - let mut all_rects: BumpVec = BumpVec::new_in(arena); + if let Some(ed_model) = ed_model_opt { + let all_rects = ed_view::create_ed_rects(ed_model, arena)?; - let glyph_rect = if let Some(glyph_dim_rect) = glyph_dim_rect_opt { - glyph_dim_rect + let rect_buffers = create_rect_buffers(gpu_device, encoder, &all_rects); + + let mut render_pass = begin_render_pass(encoder, texture_view); + + render_pass.set_pipeline(&rect_resources.pipeline); + render_pass.set_bind_group(0, &rect_resources.ortho.bind_group, &[]); + render_pass.set_vertex_buffer(0, rect_buffers.vertex_buffer.slice(..)); + render_pass.set_index_buffer(rect_buffers.index_buffer.slice(..)); + render_pass.draw_indexed(0..rect_buffers.num_rects, 0, 0..1); } else { - return Err(MissingGlyphDims {}); - }; - - if let Some(selection) = ed_model.selection_opt { - let mut selection_rects = - create_selection_rects(selection, &ed_model.lines, glyph_rect, &arena)?; - - all_rects.append(&mut selection_rects); + // need to begin render pass to clear screen + begin_render_pass(encoder, texture_view); } - all_rects.push(make_caret_rect(ed_model.caret_pos, glyph_rect)?); - - let rect_buffers = create_rect_buffers(gpu_device, encoder, &all_rects); - - let mut render_pass = begin_render_pass(encoder, texture_view); - - render_pass.set_pipeline(&rect_resources.pipeline); - render_pass.set_bind_group(0, &rect_resources.ortho.bind_group, &[]); - render_pass.set_vertex_buffer(0, rect_buffers.vertex_buffer.slice(..)); - render_pass.set_index_buffer(rect_buffers.index_buffer.slice(..)); - render_pass.draw_indexed(0..rect_buffers.num_rects, 0, 0..1); - Ok(()) } @@ -320,9 +350,9 @@ fn begin_render_pass<'a>( resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color { - r: 0.0, - g: 0.0, - b: 0.0, + r: 0.1, + g: 0.2, + b: 0.3, a: 1.0, }), store: true, @@ -332,61 +362,55 @@ fn begin_render_pass<'a>( }) } -fn make_caret_rect(caret_pos: Position, glyph_dim_rect: &Rect) -> EdResult { - let caret_y = - glyph_dim_rect.top_left_coords.y + (caret_pos.line as f32) * glyph_dim_rect.height; - - let caret_x = - glyph_dim_rect.top_left_coords.x + glyph_dim_rect.width * (caret_pos.column as f32); - - Ok(Rect { - top_left_coords: (caret_x, caret_y).into(), - height: glyph_dim_rect.height, - width: 2.0, - color: CARET_COLOR, - }) -} - // returns bounding boxes for every glyph -fn queue_all_text( +fn queue_editor_text( size: &PhysicalSize, - lines: &[String], + editor_lines: &str, caret_pos: Position, code_coords: Vector2, glyph_brush: &mut GlyphBrush<()>, ) { let area_bounds = (size.width as f32, size.height as f32).into(); - let main_label = Text { - position: (30.0, 30.0).into(), - area_bounds, - color: TXT_COLOR.into(), - text: String::from("Enter some text:"), - size: CODE_FONT_SIZE, - ..Default::default() - }; - let code_text = Text { position: code_coords, area_bounds, color: CODE_COLOR.into(), - text: lines.join(""), + text: editor_lines.to_owned(), size: CODE_FONT_SIZE, ..Default::default() }; 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() }; - queue_text_draw(&main_label, glyph_brush); - queue_text_draw(&caret_pos_label, glyph_brush); queue_text_draw(&code_text, glyph_brush); } + +fn queue_no_file_text( + size: &PhysicalSize, + text: &str, + text_coords: Vector2, + glyph_brush: &mut GlyphBrush<()>, +) { + let area_bounds = (size.width as f32, size.height as f32).into(); + + let code_text = Text { + position: text_coords, + area_bounds, + color: CODE_COLOR.into(), + text: text.to_owned(), + size: CODE_FONT_SIZE, + ..Default::default() + }; + + queue_text_draw(&code_text, glyph_brush); +} diff --git a/editor/src/mvc/app_model.rs b/editor/src/mvc/app_model.rs new file mode 100644 index 0000000000..b8f7fcc55d --- /dev/null +++ b/editor/src/mvc/app_model.rs @@ -0,0 +1,6 @@ +use super::ed_model::EdModel; + +#[derive(Debug)] +pub struct AppModel { + pub ed_model_opt: Option, +} diff --git a/editor/src/tea/ed_model.rs b/editor/src/mvc/ed_model.rs similarity index 75% rename from editor/src/tea/ed_model.rs rename to editor/src/mvc/ed_model.rs index a864a2f698..fc2cad09dd 100644 --- a/editor/src/tea/ed_model.rs +++ b/editor/src/mvc/ed_model.rs @@ -1,24 +1,29 @@ +use crate::error::EdResult; use crate::graphics::primitives::rect::Rect; +use crate::text_buffer; +use crate::text_buffer::TextBuffer; use std::cmp::Ordering; +use std::path::Path; #[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 has_focus: bool, } -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, - } + has_focus: true, + }) } -//Is model.rs the right place for these structs? #[derive(Debug, Copy, Clone)] pub struct Position { pub line: usize, diff --git a/editor/src/mvc/ed_view.rs b/editor/src/mvc/ed_view.rs new file mode 100644 index 0000000000..b62c040940 --- /dev/null +++ b/editor/src/mvc/ed_view.rs @@ -0,0 +1,44 @@ +use super::ed_model::{EdModel, Position}; +use crate::error::{EdResult, MissingGlyphDims}; +use crate::graphics::colors::CARET_COLOR; +use crate::graphics::primitives::rect::Rect; +use crate::selection::create_selection_rects; +use bumpalo::collections::Vec as BumpVec; +use bumpalo::Bump; +use snafu::ensure; + +//TODO add editor text here as well + +pub fn create_ed_rects<'a>(ed_model: &EdModel, arena: &'a Bump) -> EdResult> { + ensure!(ed_model.glyph_dim_rect_opt.is_some(), MissingGlyphDims {}); + + let glyph_rect = ed_model.glyph_dim_rect_opt.unwrap(); + + let mut all_rects: BumpVec = BumpVec::new_in(arena); + + if let Some(selection) = ed_model.selection_opt { + let mut selection_rects = + create_selection_rects(selection, &ed_model.text_buf, &glyph_rect, &arena)?; + + all_rects.append(&mut selection_rects); + } + + all_rects.push(make_caret_rect(ed_model.caret_pos, &glyph_rect)); + + Ok(all_rects) +} + +fn make_caret_rect(caret_pos: Position, glyph_dim_rect: &Rect) -> Rect { + let caret_y = + glyph_dim_rect.top_left_coords.y + (caret_pos.line as f32) * glyph_dim_rect.height; + + let caret_x = + glyph_dim_rect.top_left_coords.x + glyph_dim_rect.width * (caret_pos.column as f32); + + Rect { + top_left_coords: (caret_x, caret_y).into(), + height: glyph_dim_rect.height, + width: 2.0, + color: CARET_COLOR, + } +} diff --git a/editor/src/mvc/mod.rs b/editor/src/mvc/mod.rs new file mode 100644 index 0000000000..e401205005 --- /dev/null +++ b/editor/src/mvc/mod.rs @@ -0,0 +1,4 @@ +pub mod app_model; +pub mod ed_model; +pub mod ed_view; +pub mod update; diff --git a/editor/src/mvc/update.rs b/editor/src/mvc/update.rs new file mode 100644 index 0000000000..cb20ffc144 --- /dev/null +++ b/editor/src/mvc/update.rs @@ -0,0 +1,543 @@ +use super::app_model::AppModel; +use super::ed_model::EdModel; +use super::ed_model::{Position, RawSelection}; +use crate::error::EdResult; +use crate::text_buffer::TextBuffer; +use crate::util::is_newline; +use std::cmp::{max, min}; + +pub type MoveCaretFun = + fn(Position, Option, bool, &TextBuffer) -> (Position, Option); + +pub fn move_caret_left( + old_caret_pos: Position, + old_selection_opt: Option, + shift_pressed: bool, + text_buf: &TextBuffer, +) -> (Position, Option) { + let old_line_nr = old_caret_pos.line; + let old_col_nr = old_caret_pos.column; + + let (line_nr, col_nr) = if old_selection_opt.is_some() && !shift_pressed { + match old_selection_opt { + Some(old_selection) => (old_selection.start_pos.line, old_selection.start_pos.column), + None => unreachable!(), + } + } else if old_col_nr == 0 { + if old_line_nr == 0 { + (0, 0) + } else if let Some(curr_line_len) = text_buf.line_len(old_line_nr - 1) { + (old_line_nr - 1, curr_line_len - 1) + } else { + unreachable!() + } + } else { + (old_line_nr, old_col_nr - 1) + }; + + let new_caret_pos = Position { + line: line_nr, + column: col_nr, + }; + + let new_selection_opt = if shift_pressed { + if let Some(old_selection) = old_selection_opt { + if old_caret_pos >= old_selection.end_pos { + if new_caret_pos == old_selection.start_pos { + None + } else { + Some(RawSelection { + start_pos: old_selection.start_pos, + end_pos: new_caret_pos, + }) + } + } else { + Some(RawSelection { + start_pos: Position { + line: line_nr, + column: col_nr, + }, + end_pos: old_selection.end_pos, + }) + } + } else if !(old_line_nr == line_nr && old_col_nr == col_nr) { + Some(RawSelection { + start_pos: Position { + line: line_nr, + column: col_nr, + }, + end_pos: Position { + line: old_line_nr, + column: old_col_nr, + }, + }) + } else { + None + } + } else { + None + }; + + (new_caret_pos, new_selection_opt) +} + +pub fn move_caret_right( + old_caret_pos: Position, + old_selection_opt: Option, + shift_pressed: bool, + text_buf: &TextBuffer, +) -> (Position, Option) { + let old_line_nr = old_caret_pos.line; + let old_col_nr = old_caret_pos.column; + + let (line_nr, col_nr) = if old_selection_opt.is_some() && !shift_pressed { + match old_selection_opt { + Some(old_selection) => (old_selection.end_pos.line, old_selection.end_pos.column), + None => unreachable!(), + } + } else if let Some(curr_line) = text_buf.line(old_line_nr) { + if let Some(last_char) = curr_line.chars().last() { + if is_newline(&last_char) { + if old_col_nr + 1 > curr_line.len() - 1 { + (old_line_nr + 1, 0) + } else { + (old_line_nr, old_col_nr + 1) + } + } else if old_col_nr < curr_line.len() { + (old_line_nr, old_col_nr + 1) + } else { + (old_line_nr, old_col_nr) + } + } else { + (old_line_nr, old_col_nr) + } + } else { + unreachable!() + }; + + let new_caret_pos = Position { + line: line_nr, + column: col_nr, + }; + + let new_selection_opt = if shift_pressed { + if let Some(old_selection) = old_selection_opt { + if old_caret_pos <= old_selection.start_pos { + if new_caret_pos == old_selection.end_pos { + None + } else { + Some(RawSelection { + start_pos: new_caret_pos, + end_pos: old_selection.end_pos, + }) + } + } else { + Some(RawSelection { + start_pos: old_selection.start_pos, + end_pos: Position { + line: line_nr, + column: col_nr, + }, + }) + } + } else if !(old_line_nr == line_nr && old_col_nr == col_nr) { + Some(RawSelection { + start_pos: Position { + line: old_line_nr, + column: old_col_nr, + }, + end_pos: Position { + line: line_nr, + column: col_nr, + }, + }) + } else { + None + } + } else { + None + }; + + (new_caret_pos, new_selection_opt) +} + +pub fn move_caret_up( + old_caret_pos: Position, + old_selection_opt: Option, + shift_pressed: bool, + text_buf: &TextBuffer, +) -> (Position, Option) { + let old_line_nr = old_caret_pos.line; + let old_col_nr = old_caret_pos.column; + + let (line_nr, col_nr) = if old_selection_opt.is_some() && !shift_pressed { + match old_selection_opt { + Some(old_selection) => (old_selection.start_pos.line, old_selection.start_pos.column), + None => unreachable!(), + } + } else if old_line_nr == 0 { + (old_line_nr, 0) + } else if let Some(prev_line_len) = text_buf.line_len(old_line_nr - 1) { + if prev_line_len <= old_col_nr { + (old_line_nr - 1, prev_line_len - 1) + } else { + (old_line_nr - 1, old_col_nr) + } + } else { + unreachable!() + }; + + let new_caret_pos = Position { + line: line_nr, + column: col_nr, + }; + + let new_selection_opt = if shift_pressed { + if let Some(old_selection) = old_selection_opt { + if old_selection.end_pos <= old_caret_pos { + if new_caret_pos == old_selection.start_pos { + None + } else { + Some(RawSelection { + start_pos: min(old_selection.start_pos, new_caret_pos), + end_pos: max(old_selection.start_pos, new_caret_pos), + }) + } + } else { + Some(RawSelection { + start_pos: new_caret_pos, + end_pos: old_selection.end_pos, + }) + } + } else if !(old_line_nr == line_nr && old_col_nr == col_nr) { + Some(RawSelection { + start_pos: min(old_caret_pos, new_caret_pos), + end_pos: max(old_caret_pos, new_caret_pos), + }) + } else { + None + } + } else { + None + }; + + (new_caret_pos, new_selection_opt) +} + +pub fn move_caret_down( + old_caret_pos: Position, + old_selection_opt: Option, + shift_pressed: bool, + text_buf: &TextBuffer, +) -> (Position, Option) { + let old_line_nr = old_caret_pos.line; + let old_col_nr = old_caret_pos.column; + + let (line_nr, col_nr) = if old_selection_opt.is_some() && !shift_pressed { + match old_selection_opt { + Some(old_selection) => (old_selection.end_pos.line, old_selection.end_pos.column), + None => unreachable!(), + } + } else if old_line_nr + 1 >= text_buf.nr_of_lines() { + if let Some(curr_line_len) = text_buf.line_len(old_line_nr) { + (old_line_nr, curr_line_len) + } else { + unreachable!() + } + } else if let Some(next_line) = text_buf.line(old_line_nr + 1) { + if next_line.len() <= old_col_nr { + if let Some(last_char) = next_line.chars().last() { + if is_newline(&last_char) { + (old_line_nr + 1, next_line.len() - 1) + } else { + (old_line_nr + 1, next_line.len()) + } + } else { + (old_line_nr + 1, 0) + } + } else { + (old_line_nr + 1, old_col_nr) + } + } else { + unreachable!() + }; + + let new_caret_pos = Position { + line: line_nr, + column: col_nr, + }; + + let new_selection_opt = if shift_pressed { + if let Some(old_selection) = old_selection_opt { + if old_caret_pos <= old_selection.start_pos { + if new_caret_pos == old_selection.end_pos { + None + } else { + Some(RawSelection { + start_pos: min(old_selection.end_pos, new_caret_pos), + end_pos: max(old_selection.end_pos, new_caret_pos), + }) + } + } else { + Some(RawSelection { + start_pos: old_selection.start_pos, + end_pos: new_caret_pos, + }) + } + } else if !(old_line_nr == line_nr && old_col_nr == col_nr) { + Some(RawSelection { + start_pos: min(old_caret_pos, new_caret_pos), + end_pos: max(old_caret_pos, new_caret_pos), + }) + } else { + None + } + } else { + None + }; + + (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 { + let old_caret_pos = ed_model.caret_pos; + + match received_char { + '\u{8}' | '\u{7f}' => { + // On Linux, '\u{8}' is backspace, + // on macOS '\u{7f}'. + if let Some(selection) = ed_model.selection_opt { + del_selection(selection, ed_model)?; + } else { + 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) => { + 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 + } + _ => { + 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, + }; + } + } + } + + ed_model.selection_opt = None; + } + + Ok(()) +} + +#[cfg(test)] +mod test_update { + use crate::mvc::app_model::AppModel; + use crate::mvc::ed_model::{EdModel, Position, RawSelection}; + use crate::mvc::update::handle_new_char; + use crate::selection::test_selection::{ + all_lines_vec, convert_dsl_to_selection, convert_selection_to_dsl, text_buffer_from_dsl_str, + }; + 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/resources/mod.rs b/editor/src/resources/mod.rs new file mode 100644 index 0000000000..e8dfd788ff --- /dev/null +++ b/editor/src/resources/mod.rs @@ -0,0 +1 @@ +pub mod strings; diff --git a/editor/src/resources/strings.rs b/editor/src/resources/strings.rs new file mode 100644 index 0000000000..720fb4c89b --- /dev/null +++ b/editor/src/resources/strings.rs @@ -0,0 +1 @@ +pub const NOTHING_OPENED: &str = "Execute `cargo run edit ` to open a file."; diff --git a/editor/src/selection.rs b/editor/src/selection.rs index 85f60ab3fd..b15eab2691 100644 --- a/editor/src/selection.rs +++ b/editor/src/selection.rs @@ -1,18 +1,18 @@ use crate::error::{EdResult, InvalidSelection}; use crate::graphics::colors; use crate::graphics::primitives::rect::Rect; -use crate::tea::ed_model::RawSelection; -use crate::vec_result::get_res; +use crate::mvc::ed_model::RawSelection; +use crate::text_buffer::TextBuffer; use bumpalo::collections::Vec as BumpVec; use bumpalo::Bump; use snafu::ensure; //using the "parse don't validate" pattern -struct ValidSelection { - selection: RawSelection, +pub struct ValidSelection { + pub selection: RawSelection, } -fn validate_selection(selection: RawSelection) -> EdResult { +pub fn validate_selection(selection: RawSelection) -> EdResult { let RawSelection { start_pos, end_pos } = selection; ensure!( @@ -43,7 +43,7 @@ fn validate_selection(selection: RawSelection) -> EdResult { pub fn create_selection_rects<'a>( raw_sel: RawSelection, - lines: &[String], + text_buf: &TextBuffer, glyph_dim_rect: &Rect, arena: &'a Bump, ) -> EdResult> { @@ -71,7 +71,7 @@ pub fn create_selection_rects<'a>( Ok(all_rects) } else { // first line - let end_col = get_res(start_pos.line, lines)?.len(); + let end_col = text_buf.line_len_res(start_pos.line)?; let width = ((end_col as f32) * glyph_dim_rect.width) - ((start_pos.column as f32) * glyph_dim_rect.width); @@ -89,7 +89,7 @@ pub fn create_selection_rects<'a>( let first_mid_line = start_pos.line + 1; for i in first_mid_line..(first_mid_line + nr_mid_lines) { - let mid_line_len = get_res(i, lines)?.len(); + let mid_line_len = text_buf.line_len_res(i)?; let width = (mid_line_len as f32) * glyph_dim_rect.width; @@ -123,13 +123,17 @@ pub fn create_selection_rects<'a>( } #[cfg(test)] -mod test_parse { +pub mod test_selection { use crate::error::{EdResult, OutOfBounds}; - use crate::tea::ed_model::{Position, RawSelection}; - use crate::tea::update::{move_caret_down, move_caret_left, move_caret_right, move_caret_up}; + use crate::mvc::ed_model::{Position, RawSelection}; + use crate::mvc::update::{ + move_caret_down, move_caret_left, move_caret_right, move_caret_up, MoveCaretFun, + }; + use crate::text_buffer::TextBuffer; use crate::vec_result::get_res; use core::cmp::Ordering; use pest::Parser; + use ropey::Rope; use snafu::OptionExt; use std::collections::HashMap; use std::slice::SliceIndex; @@ -139,7 +143,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], @@ -199,13 +203,51 @@ mod test_parse { ) -> EdResult<&mut >::Output> { let vec_len = vec.len(); - let elt_ref = vec.get_mut(index).context(OutOfBounds { index, vec_len })?; + let elt_ref = vec.get_mut(index).context(OutOfBounds { + index, + collection_name: "Slice", + len: vec_len, + })?; Ok(elt_ref) } + fn text_buffer_from_str(lines_str: &str) -> TextBuffer { + TextBuffer { + text_rope: Rope::from_str(lines_str), + path_str: "".to_owned(), + } + } + + 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(); + + for line in text_buf.text_rope.lines() { + lines.push( + line + .as_str() + .expect( + "Failed to get str from RopeSlice. See https://docs.rs/ropey/1.2.0/ropey/struct.RopeSlice.html#method.as_str" + ) + .to_owned() + ); + } + + lines + } + // 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(""); @@ -303,9 +345,6 @@ mod test_parse { } } - pub type MoveCaretFun = - fn(Position, Option, bool, &[String]) -> (Position, Option); - // Convert nice string representations and compare results fn assert_move( pre_lines_str: &[&str], @@ -321,15 +360,13 @@ mod test_parse { let (sel_opt, caret_pos) = convert_dsl_to_selection(&pre_lines)?; - let mut clean_lines = pre_lines - .into_iter() - .map(|line| line.replace(&['[', ']', '|'][..], "")) - .collect::>(); + let clean_text_buf = text_buffer_from_dsl_str(&pre_lines); let (new_caret_pos, new_sel_opt) = - move_fun(caret_pos, sel_opt, shift_pressed, &clean_lines); + move_fun(caret_pos, sel_opt, shift_pressed, &clean_text_buf); - let post_lines_res = convert_selection_to_dsl(new_sel_opt, new_caret_pos, &mut clean_lines); + let mut lines_vec = all_lines_vec(&clean_text_buf); + let post_lines_res = convert_selection_to_dsl(new_sel_opt, new_caret_pos, &mut lines_vec); match post_lines_res { Ok(post_lines) => { diff --git a/editor/src/tea/mod.rs b/editor/src/tea/mod.rs deleted file mode 100644 index 33bcc304d1..0000000000 --- a/editor/src/tea/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod ed_model; -pub mod update; diff --git a/editor/src/tea/update.rs b/editor/src/tea/update.rs deleted file mode 100644 index f83dd677a5..0000000000 --- a/editor/src/tea/update.rs +++ /dev/null @@ -1,344 +0,0 @@ -use super::ed_model::EdModel; -use super::ed_model::{Position, RawSelection}; -use crate::util::is_newline; -use std::cmp::{max, min}; - -pub fn move_caret_left( - old_caret_pos: Position, - old_selection_opt: Option, - shift_pressed: bool, - lines: &[String], -) -> (Position, Option) { - let old_line_nr = old_caret_pos.line; - let old_col_nr = old_caret_pos.column; - - let (line_nr, col_nr) = if old_selection_opt.is_some() && !shift_pressed { - match old_selection_opt { - Some(old_selection) => (old_selection.start_pos.line, old_selection.start_pos.column), - None => unreachable!(), - } - } else if old_col_nr == 0 { - if old_line_nr == 0 { - (0, 0) - } else if let Some(curr_line) = lines.get(old_line_nr - 1) { - (old_line_nr - 1, curr_line.len() - 1) - } else { - unreachable!() - } - } else { - (old_line_nr, old_col_nr - 1) - }; - - let new_caret_pos = Position { - line: line_nr, - column: col_nr, - }; - - let new_selection_opt = if shift_pressed { - if let Some(old_selection) = old_selection_opt { - if old_caret_pos >= old_selection.end_pos { - if new_caret_pos == old_selection.start_pos { - None - } else { - Some(RawSelection { - start_pos: old_selection.start_pos, - end_pos: new_caret_pos, - }) - } - } else { - Some(RawSelection { - start_pos: Position { - line: line_nr, - column: col_nr, - }, - end_pos: old_selection.end_pos, - }) - } - } else if !(old_line_nr == line_nr && old_col_nr == col_nr) { - Some(RawSelection { - start_pos: Position { - line: line_nr, - column: col_nr, - }, - end_pos: Position { - line: old_line_nr, - column: old_col_nr, - }, - }) - } else { - None - } - } else { - None - }; - - (new_caret_pos, new_selection_opt) -} - -pub fn move_caret_right( - old_caret_pos: Position, - old_selection_opt: Option, - shift_pressed: bool, - lines: &[String], -) -> (Position, Option) { - let old_line_nr = old_caret_pos.line; - let old_col_nr = old_caret_pos.column; - - let (line_nr, col_nr) = if old_selection_opt.is_some() && !shift_pressed { - match old_selection_opt { - Some(old_selection) => (old_selection.end_pos.line, old_selection.end_pos.column), - None => unreachable!(), - } - } else if let Some(curr_line) = lines.get(old_line_nr) { - if let Some(last_char) = curr_line.chars().last() { - if is_newline(&last_char) { - if old_col_nr + 1 > curr_line.len() - 1 { - (old_line_nr + 1, 0) - } else { - (old_line_nr, old_col_nr + 1) - } - } else if old_col_nr < curr_line.len() { - (old_line_nr, old_col_nr + 1) - } else { - (old_line_nr, old_col_nr) - } - } else { - (old_line_nr, old_col_nr) - } - } else { - unreachable!() - }; - - let new_caret_pos = Position { - line: line_nr, - column: col_nr, - }; - - let new_selection_opt = if shift_pressed { - if let Some(old_selection) = old_selection_opt { - if old_caret_pos <= old_selection.start_pos { - if new_caret_pos == old_selection.end_pos { - None - } else { - Some(RawSelection { - start_pos: new_caret_pos, - end_pos: old_selection.end_pos, - }) - } - } else { - Some(RawSelection { - start_pos: old_selection.start_pos, - end_pos: Position { - line: line_nr, - column: col_nr, - }, - }) - } - } else if !(old_line_nr == line_nr && old_col_nr == col_nr) { - Some(RawSelection { - start_pos: Position { - line: old_line_nr, - column: old_col_nr, - }, - end_pos: Position { - line: line_nr, - column: col_nr, - }, - }) - } else { - None - } - } else { - None - }; - - (new_caret_pos, new_selection_opt) -} - -pub fn move_caret_up( - old_caret_pos: Position, - old_selection_opt: Option, - shift_pressed: bool, - lines: &[String], -) -> (Position, Option) { - let old_line_nr = old_caret_pos.line; - let old_col_nr = old_caret_pos.column; - - let (line_nr, col_nr) = if old_selection_opt.is_some() && !shift_pressed { - match old_selection_opt { - Some(old_selection) => (old_selection.start_pos.line, old_selection.start_pos.column), - None => unreachable!(), - } - } else if old_line_nr == 0 { - (old_line_nr, 0) - } else if let Some(prev_line) = lines.get(old_line_nr - 1) { - if prev_line.len() <= old_col_nr { - (old_line_nr - 1, prev_line.len() - 1) - } else { - (old_line_nr - 1, old_col_nr) - } - } else { - unreachable!() - }; - - let new_caret_pos = Position { - line: line_nr, - column: col_nr, - }; - - let new_selection_opt = if shift_pressed { - if let Some(old_selection) = old_selection_opt { - if old_selection.end_pos <= old_caret_pos { - if new_caret_pos == old_selection.start_pos { - None - } else { - Some(RawSelection { - start_pos: min(old_selection.start_pos, new_caret_pos), - end_pos: max(old_selection.start_pos, new_caret_pos), - }) - } - } else { - Some(RawSelection { - start_pos: new_caret_pos, - end_pos: old_selection.end_pos, - }) - } - } else if !(old_line_nr == line_nr && old_col_nr == col_nr) { - Some(RawSelection { - start_pos: min(old_caret_pos, new_caret_pos), - end_pos: max(old_caret_pos, new_caret_pos), - }) - } else { - None - } - } else { - None - }; - - (new_caret_pos, new_selection_opt) -} - -pub fn move_caret_down( - old_caret_pos: Position, - old_selection_opt: Option, - shift_pressed: bool, - lines: &[String], -) -> (Position, Option) { - let old_line_nr = old_caret_pos.line; - let old_col_nr = old_caret_pos.column; - - let (line_nr, col_nr) = if old_selection_opt.is_some() && !shift_pressed { - match old_selection_opt { - Some(old_selection) => (old_selection.end_pos.line, old_selection.end_pos.column), - None => unreachable!(), - } - } else if old_line_nr + 1 >= lines.len() { - if let Some(curr_line) = lines.get(old_line_nr) { - (old_line_nr, curr_line.len()) - } else { - unreachable!() - } - } else if let Some(next_line) = lines.get(old_line_nr + 1) { - if next_line.len() <= old_col_nr { - if let Some(last_char) = next_line.chars().last() { - if is_newline(&last_char) { - (old_line_nr + 1, next_line.len() - 1) - } else { - (old_line_nr + 1, next_line.len()) - } - } else { - (old_line_nr + 1, 0) - } - } else { - (old_line_nr + 1, old_col_nr) - } - } else { - unreachable!() - }; - - let new_caret_pos = Position { - line: line_nr, - column: col_nr, - }; - - let new_selection_opt = if shift_pressed { - if let Some(old_selection) = old_selection_opt { - if old_caret_pos <= old_selection.start_pos { - if new_caret_pos == old_selection.end_pos { - None - } else { - Some(RawSelection { - start_pos: min(old_selection.end_pos, new_caret_pos), - end_pos: max(old_selection.end_pos, new_caret_pos), - }) - } - } else { - Some(RawSelection { - start_pos: old_selection.start_pos, - end_pos: new_caret_pos, - }) - } - } else if !(old_line_nr == line_nr && old_col_nr == col_nr) { - Some(RawSelection { - start_pos: min(old_caret_pos, new_caret_pos), - end_pos: max(old_caret_pos, new_caret_pos), - }) - } else { - None - } - } else { - None - }; - - (new_caret_pos, new_selection_opt) -} - -pub fn update_text_state(ed_model: &mut EdModel, received_char: &char) { - 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(); - } - 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) - } - 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); - - 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..dc4fbdea1a --- /dev/null +++ b/editor/src/text_buffer.rs @@ -0,0 +1,153 @@ +// Adapted from https://github.com/cessen/ropey by Nathan Vegdahl, licensed under the MIT license + +use crate::error::EdError::{FileOpenFailed, TextBufReadFailed}; +use crate::error::EdResult; +use crate::error::OutOfBounds; +use crate::mvc::ed_model::{Position, RawSelection}; +use crate::selection::validate_selection; +use bumpalo::collections::String as BumpString; +use bumpalo::Bump; +use ropey::Rope; +use snafu::{ensure, OptionExt}; +use std::fs::File; +use std::io; +use std::path::Path; + +#[derive(Debug)] +pub struct TextBuffer { + pub text_rope: Rope, + pub path_str: String, +} + +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 { + index: char_indx, + collection_name: "Rope", + len: self.text_rope.len_chars() + } + ); + + self.text_rope.insert(char_indx, &new_char.to_string()); + + Ok(()) + } + + pub fn pop_char(&mut self, caret_pos: Position) { + 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 { + index: end_char_indx, + collection_name: "Rope", + len: self.text_rope.len_chars() + } + ); + + self.text_rope.remove(start_char_indx..end_char_indx); + + Ok(()) + } + + pub fn line(&self, line_nr: usize) -> Option<&str> { + if line_nr < self.text_rope.len_lines() { + self.text_rope.line(line_nr).as_str() + } else { + None + } + } + + pub fn line_len(&self, line_nr: usize) -> Option { + if line_nr < self.text_rope.len_lines() { + Some(self.text_rope.line(line_nr).len_chars()) + } else { + None + } + } + + pub fn line_len_res(&self, line_nr: usize) -> EdResult { + self.line_len(line_nr).context(OutOfBounds { + index: line_nr, + collection_name: "Rope", + len: self.text_rope.len_lines(), + }) + } + + pub fn nr_of_lines(&self) -> usize { + self.text_rope.len_lines() + } + + // expensive function, don't use it if it can be done with a specialized, more efficient function + // TODO use bump allocation here + pub fn all_lines<'a>(&self, arena: &'a Bump) -> BumpString<'a> { + let mut lines = BumpString::with_capacity_in(self.text_rope.len_chars(), arena); + + for line in self.text_rope.lines() { + lines.extend(line.as_str()); + } + + lines + } + + fn pos_to_char_indx(&self, pos: Position) -> usize { + self.text_rope.line_to_char(pos.line) + pos.column + } + + 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.end_pos); + + Ok((start_char_indx, end_char_indx)) + } +} + +pub fn from_path(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, + }) +} + +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 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(), + }), + } +} diff --git a/editor/src/vec_result.rs b/editor/src/vec_result.rs index 202af28a96..dddb2c0aaf 100644 --- a/editor/src/vec_result.rs +++ b/editor/src/vec_result.rs @@ -5,10 +5,11 @@ use std::slice::SliceIndex; // replace vec methods that return Option with ones that return Result and proper Error -pub fn get_res(index: usize, vec: &[T]) -> EdResult<&>::Output> { - let elt_ref = vec.get(index).context(OutOfBounds { +pub fn get_res(index: usize, slice: &[T]) -> EdResult<&>::Output> { + let elt_ref = slice.get(index).context(OutOfBounds { index, - vec_len: vec.len(), + collection_name: "Slice", + len: slice.len(), })?; Ok(elt_ref) 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(),