mirror of
https://github.com/roc-lang/roc.git
synced 2025-09-28 22:34:45 +00:00
Merge remote-tracking branch 'origin/trunk' into nullable-tags
This commit is contained in:
commit
8cd744342b
54 changed files with 1208 additions and 724 deletions
10
Cargo.lock
generated
10
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -42,7 +42,7 @@ pub fn gen_and_eval(src: &[u8], target: Triple, opt_level: OptLevel) -> Result<R
|
|||
&arena,
|
||||
filename,
|
||||
&module_src,
|
||||
stdlib,
|
||||
&stdlib,
|
||||
src_dir,
|
||||
exposed_types,
|
||||
ptr_bytes,
|
||||
|
|
|
@ -260,6 +260,17 @@ mod repl_eval {
|
|||
expect_success("Num.bitwiseAnd 200 0", "0 : Int *")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn num_bitwise_xor() {
|
||||
expect_success("Num.bitwiseXor 20 20", "0 : Int *");
|
||||
|
||||
expect_success("Num.bitwiseXor 15 14", "1 : Int *");
|
||||
|
||||
expect_success("Num.bitwiseXor 7 15", "8 : Int *");
|
||||
|
||||
expect_success("Num.bitwiseXor 200 0", "200 : Int *")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn num_add_wrap() {
|
||||
expect_success("Num.addWrap Num.maxInt 1", "-9223372036854775808 : Int *");
|
||||
|
|
|
@ -2,14 +2,15 @@ use std::fs::File;
|
|||
use std::io::prelude::Read;
|
||||
use std::vec::Vec;
|
||||
|
||||
pub fn get_bytes() -> Vec<u8> {
|
||||
// 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!(
|
||||
const PATH: &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");
|
||||
|
||||
pub fn get_bytes() -> Vec<u8> {
|
||||
// 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 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)
|
||||
|
|
|
@ -291,6 +291,15 @@ pub fn types() -> MutMap<Symbol, (SolvedType, Region)> {
|
|||
),
|
||||
);
|
||||
|
||||
// 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,
|
||||
|
|
|
@ -294,6 +294,15 @@ pub fn types() -> MutMap<Symbol, (SolvedType, Region)> {
|
|||
)
|
||||
});
|
||||
|
||||
// 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};
|
||||
|
|
|
@ -117,7 +117,8 @@ pub fn builtin_defs_map(symbol: Symbol, var_store: &mut VarStore) -> Option<Def>
|
|||
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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -47,7 +47,7 @@ pub fn helper<'a>(
|
|||
arena,
|
||||
filename,
|
||||
&module_src,
|
||||
stdlib,
|
||||
&stdlib,
|
||||
src_dir,
|
||||
exposed_types,
|
||||
8,
|
||||
|
|
|
@ -699,7 +699,7 @@ struct State<'a> {
|
|||
pub root_id: ModuleId,
|
||||
pub platform_id: Option<ModuleId>,
|
||||
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<To<'a>>,
|
||||
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -59,6 +59,7 @@ pub enum LowLevel {
|
|||
NumAcos,
|
||||
NumAsin,
|
||||
NumBitwiseAnd,
|
||||
NumBitwiseXor,
|
||||
Eq,
|
||||
NotEq,
|
||||
And,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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<u64>) -> MutMap<u64, u64> {
|
||||
let mut result = MutMap::default();
|
||||
count_targets_help(decision_tree, &mut result);
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn count_targets_help(decision_tree: &Decider<u64>, targets: &mut MutMap<u64, u64>) {
|
||||
fn count_targets(targets: &mut bumpalo::collections::Vec<u64>, initial: &Decider<u64>) {
|
||||
use Decider::*;
|
||||
|
||||
let mut stack = vec![initial];
|
||||
|
||||
while let Some(decision_tree) = stack.pop() {
|
||||
match decision_tree {
|
||||
Leaf(target) => match targets.get_mut(target) {
|
||||
None => {
|
||||
targets.insert(*target, 1);
|
||||
Leaf(target) => {
|
||||
targets[*target as usize] += 1;
|
||||
}
|
||||
Some(current) => {
|
||||
*current += 1;
|
||||
}
|
||||
},
|
||||
|
||||
Chain {
|
||||
success, failure, ..
|
||||
} => {
|
||||
count_targets_help(success, targets);
|
||||
count_targets_help(failure, targets);
|
||||
stack.push(success);
|
||||
stack.push(failure);
|
||||
}
|
||||
|
||||
FanOut {
|
||||
tests, fallback, ..
|
||||
} => {
|
||||
count_targets_help(fallback, targets);
|
||||
stack.push(fallback);
|
||||
|
||||
for (_, decider) in tests {
|
||||
count_targets_help(decider, targets);
|
||||
stack.push(decider);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1714,11 +1721,11 @@ fn count_targets_help(decision_tree: &Decider<u64>, targets: &mut MutMap<u64, u6
|
|||
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn create_choices<'a>(
|
||||
target_counts: &MutMap<u64, u64>,
|
||||
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
|
||||
|
|
|
@ -58,7 +58,7 @@ mod test_mono {
|
|||
arena,
|
||||
filename,
|
||||
&module_src,
|
||||
stdlib,
|
||||
&stdlib,
|
||||
src_dir,
|
||||
exposed_types,
|
||||
8,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
Run the following from the roc folder:
|
||||
|
||||
```
|
||||
cargo run edit
|
||||
cargo run edit examples/hello-world/Hello.roc
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
|
|
@ -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<T, E = EdError> = std::result::Result<T, E>;
|
||||
|
|
|
@ -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]
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -6,7 +6,7 @@ use cgmath::Vector2;
|
|||
pub struct Vertex {
|
||||
#[allow(dead_code)]
|
||||
pub position: Vector2<f32>,
|
||||
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,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
@ -5,5 +5,5 @@ pub struct Rect {
|
|||
pub top_left_coords: Vector2<f32>,
|
||||
pub width: f32,
|
||||
pub height: f32,
|
||||
pub color: [f32; 3],
|
||||
pub color: (f32, f32, f32, f32),
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
Binary file not shown.
|
@ -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);
|
||||
|
|
Binary file not shown.
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<dyn Error>> {
|
||||
fn run_event_loop(file_path_opt: Option<&Path>) -> Result<(), Box<dyn Error>> {
|
||||
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<dyn Error>> {
|
|||
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<dyn Error>> {
|
|||
let mut glyph_brush = build_glyph_brush(&gpu_device, render_format)?;
|
||||
|
||||
let is_animating = true;
|
||||
let mut ed_model = ed_model::init_model();
|
||||
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<dyn Error>> {
|
|||
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,14 +221,18 @@ fn run_event_loop() -> Result<(), Box<dyn Error>> {
|
|||
..
|
||||
} => {
|
||||
if let Some(virtual_keycode) = input.virtual_keycode {
|
||||
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,
|
||||
&mut ed_model,
|
||||
ed_model,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//Modifiers Changed
|
||||
Event::WindowEvent {
|
||||
event: event::WindowEvent::ModifiersChanged(modifiers),
|
||||
|
@ -216,17 +253,21 @@ fn run_event_loop() -> Result<(), Box<dyn Error>> {
|
|||
.expect("Failed to acquire next SwapChainFrame")
|
||||
.output;
|
||||
|
||||
queue_all_text(
|
||||
if let Some(ed_model) = &app_model.ed_model_opt {
|
||||
//TODO don't pass invisible lines
|
||||
queue_editor_text(
|
||||
&size,
|
||||
&ed_model.lines,
|
||||
&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,30 +313,15 @@ fn run_event_loop() -> Result<(), Box<dyn Error>> {
|
|||
}
|
||||
|
||||
fn draw_all_rects(
|
||||
ed_model: &EdModel,
|
||||
glyph_dim_rect_opt: &Option<Rect>,
|
||||
ed_model_opt: &Option<EdModel>,
|
||||
arena: &Bump,
|
||||
encoder: &mut CommandEncoder,
|
||||
texture_view: &TextureView,
|
||||
gpu_device: &wgpu::Device,
|
||||
rect_resources: &RectResources,
|
||||
) -> EdResult<()> {
|
||||
let mut all_rects: BumpVec<Rect> = BumpVec::new_in(arena);
|
||||
|
||||
let glyph_rect = if let Some(glyph_dim_rect) = glyph_dim_rect_opt {
|
||||
glyph_dim_rect
|
||||
} 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);
|
||||
}
|
||||
|
||||
all_rects.push(make_caret_rect(ed_model.caret_pos, glyph_rect)?);
|
||||
if let Some(ed_model) = ed_model_opt {
|
||||
let all_rects = ed_view::create_ed_rects(ed_model, arena)?;
|
||||
|
||||
let rect_buffers = create_rect_buffers(gpu_device, encoder, &all_rects);
|
||||
|
||||
|
@ -306,6 +332,10 @@ fn draw_all_rects(
|
|||
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 {
|
||||
// need to begin render pass to clear screen
|
||||
begin_render_pass(encoder, texture_view);
|
||||
}
|
||||
|
||||
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<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);
|
||||
|
||||
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<u32>,
|
||||
lines: &[String],
|
||||
editor_lines: &str,
|
||||
caret_pos: Position,
|
||||
code_coords: Vector2<f32>,
|
||||
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<u32>,
|
||||
text: &str,
|
||||
text_coords: Vector2<f32>,
|
||||
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);
|
||||
}
|
||||
|
|
6
editor/src/mvc/app_model.rs
Normal file
6
editor/src/mvc/app_model.rs
Normal file
|
@ -0,0 +1,6 @@
|
|||
use super::ed_model::EdModel;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AppModel {
|
||||
pub ed_model_opt: Option<EdModel>,
|
||||
}
|
|
@ -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<String>,
|
||||
pub text_buf: TextBuffer,
|
||||
pub caret_pos: Position,
|
||||
pub selection_opt: Option<RawSelection>,
|
||||
pub glyph_dim_rect_opt: Option<Rect>,
|
||||
pub has_focus: bool,
|
||||
}
|
||||
|
||||
pub fn init_model() -> EdModel {
|
||||
EdModel {
|
||||
lines: vec![String::new()],
|
||||
pub fn init_model(file_path: &Path) -> EdResult<EdModel> {
|
||||
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,
|
44
editor/src/mvc/ed_view.rs
Normal file
44
editor/src/mvc/ed_view.rs
Normal file
|
@ -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<BumpVec<'a, Rect>> {
|
||||
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<Rect> = 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,
|
||||
}
|
||||
}
|
4
editor/src/mvc/mod.rs
Normal file
4
editor/src/mvc/mod.rs
Normal file
|
@ -0,0 +1,4 @@
|
|||
pub mod app_model;
|
||||
pub mod ed_model;
|
||||
pub mod ed_view;
|
||||
pub mod update;
|
543
editor/src/mvc/update.rs
Normal file
543
editor/src/mvc/update.rs
Normal file
|
@ -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<RawSelection>, bool, &TextBuffer) -> (Position, Option<RawSelection>);
|
||||
|
||||
pub fn move_caret_left(
|
||||
old_caret_pos: Position,
|
||||
old_selection_opt: Option<RawSelection>,
|
||||
shift_pressed: bool,
|
||||
text_buf: &TextBuffer,
|
||||
) -> (Position, Option<RawSelection>) {
|
||||
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<RawSelection>,
|
||||
shift_pressed: bool,
|
||||
text_buf: &TextBuffer,
|
||||
) -> (Position, Option<RawSelection>) {
|
||||
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<RawSelection>,
|
||||
shift_pressed: bool,
|
||||
text_buf: &TextBuffer,
|
||||
) -> (Position, Option<RawSelection>) {
|
||||
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<RawSelection>,
|
||||
shift_pressed: bool,
|
||||
text_buf: &TextBuffer,
|
||||
) -> (Position, Option<RawSelection>) {
|
||||
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<RawSelection>, TextBuffer), String> {
|
||||
let lines_string_slice: Vec<String> = 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<RawSelection>,
|
||||
) -> 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(())
|
||||
}
|
||||
}
|
1
editor/src/resources/mod.rs
Normal file
1
editor/src/resources/mod.rs
Normal file
|
@ -0,0 +1 @@
|
|||
pub mod strings;
|
1
editor/src/resources/strings.rs
Normal file
1
editor/src/resources/strings.rs
Normal file
|
@ -0,0 +1 @@
|
|||
pub const NOTHING_OPENED: &str = "Execute `cargo run edit <filename>` to open a file.";
|
|
@ -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<ValidSelection> {
|
||||
pub fn validate_selection(selection: RawSelection) -> EdResult<ValidSelection> {
|
||||
let RawSelection { start_pos, end_pos } = selection;
|
||||
|
||||
ensure!(
|
||||
|
@ -43,7 +43,7 @@ fn validate_selection(selection: RawSelection) -> EdResult<ValidSelection> {
|
|||
|
||||
pub fn create_selection_rects<'a>(
|
||||
raw_sel: RawSelection,
|
||||
lines: &[String],
|
||||
text_buf: &TextBuffer,
|
||||
glyph_dim_rect: &Rect,
|
||||
arena: &'a Bump,
|
||||
) -> EdResult<BumpVec<'a, Rect>> {
|
||||
|
@ -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<RawSelection>,
|
||||
caret_pos: Position,
|
||||
lines: &mut [String],
|
||||
|
@ -199,13 +203,51 @@ mod test_parse {
|
|||
) -> EdResult<&mut <usize as SliceIndex<[T]>>::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::<Vec<String>>()
|
||||
.join(""),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn all_lines_vec(text_buf: &TextBuffer) -> Vec<String> {
|
||||
let mut lines: Vec<String> = 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<RawSelection>, Position), String> {
|
||||
let lines_str: String = lines.join("");
|
||||
|
@ -303,9 +345,6 @@ mod test_parse {
|
|||
}
|
||||
}
|
||||
|
||||
pub type MoveCaretFun =
|
||||
fn(Position, Option<RawSelection>, bool, &[String]) -> (Position, Option<RawSelection>);
|
||||
|
||||
// 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::<Vec<String>>();
|
||||
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) => {
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
pub mod ed_model;
|
||||
pub mod update;
|
|
@ -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<RawSelection>,
|
||||
shift_pressed: bool,
|
||||
lines: &[String],
|
||||
) -> (Position, Option<RawSelection>) {
|
||||
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<RawSelection>,
|
||||
shift_pressed: bool,
|
||||
lines: &[String],
|
||||
) -> (Position, Option<RawSelection>) {
|
||||
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<RawSelection>,
|
||||
shift_pressed: bool,
|
||||
lines: &[String],
|
||||
) -> (Position, Option<RawSelection>) {
|
||||
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<RawSelection>,
|
||||
shift_pressed: bool,
|
||||
lines: &[String],
|
||||
) -> (Position, Option<RawSelection>) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
153
editor/src/text_buffer.rs
Normal file
153
editor/src/text_buffer.rs
Normal file
|
@ -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<usize> {
|
||||
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<usize> {
|
||||
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<TextBuffer> {
|
||||
// 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<Rope> {
|
||||
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(),
|
||||
}),
|
||||
}
|
||||
}
|
|
@ -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<T>(index: usize, vec: &[T]) -> EdResult<&<usize as SliceIndex<[T]>>::Output> {
|
||||
let elt_ref = vec.get(index).context(OutOfBounds {
|
||||
pub fn get_res<T>(index: usize, slice: &[T]) -> EdResult<&<usize as SliceIndex<[T]>>::Output> {
|
||||
let elt_ref = slice.get(index).context(OutOfBounds {
|
||||
index,
|
||||
vec_len: vec.len(),
|
||||
collection_name: "Slice",
|
||||
len: slice.len(),
|
||||
})?;
|
||||
|
||||
Ok(elt_ref)
|
||||
|
|
|
@ -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)
|
||||
|
|
@ -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(),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue