Merge remote-tracking branch 'origin/trunk' into nullable-tags

This commit is contained in:
Folkert 2021-01-17 02:37:57 +01:00
commit 8cd744342b
54 changed files with 1208 additions and 724 deletions

10
Cargo.lock generated
View file

@ -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"

View file

@ -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,

View file

@ -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,

View file

@ -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 *");

View file

@ -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)

View file

@ -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,

View file

@ -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};

View file

@ -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();

View file

@ -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);
}

View file

@ -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);

View file

@ -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
);
}
}

View file

@ -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);
}
};
}

View file

@ -47,7 +47,7 @@ pub fn helper<'a>(
arena,
filename,
&module_src,
stdlib,
&stdlib,
src_dir,
exposed_types,
8,

View file

@ -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,

View file

@ -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,

View file

@ -59,6 +59,7 @@ pub enum LowLevel {
NumAcos,
NumAsin,
NumBitwiseAnd,
NumBitwiseXor,
Eq,
NotEq,
And,

View file

@ -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

View file

@ -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 => {

View file

@ -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

View file

@ -58,7 +58,7 @@ mod test_mono {
arena,
filename,
&module_src,
stdlib,
&stdlib,
src_dir,
exposed_types,
8,

View file

@ -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,

View file

@ -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)

View file

@ -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]

View file

@ -3,7 +3,7 @@
Run the following from the roc folder:
```
cargo run edit
cargo run edit examples/hello-world/Hello.roc
```
## Troubleshooting

View file

@ -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>;

View file

@ -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]
}

View file

@ -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 {

View file

@ -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,
},
],
};

View file

@ -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),
}

View file

@ -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,
}
}

View file

@ -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;
}

View file

@ -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);

View file

@ -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);

View file

@ -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,

View file

@ -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;

View file

@ -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);
}

View file

@ -0,0 +1,6 @@
use super::ed_model::EdModel;
#[derive(Debug)]
pub struct AppModel {
pub ed_model_opt: Option<EdModel>,
}

View file

@ -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
View 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
View 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
View 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(())
}
}

View file

@ -0,0 +1 @@
pub mod strings;

View file

@ -0,0 +1 @@
pub const NOTHING_OPENED: &str = "Execute `cargo run edit <filename>` to open a file.";

View 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) => {

View file

@ -1,2 +0,0 @@
pub mod ed_model;
pub mod update;

View file

@ -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
View 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(),
}),
}
}

View file

@ -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)

View file

@ -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)

View file

@ -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(),