diff --git a/Cargo.lock b/Cargo.lock index a4621269a8..17ace44f5a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1994,6 +1994,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "nonempty" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fa586da3e43cc7df44aae0e21ed2e743218b876de3f38035683d30bd8a3828e" + [[package]] name = "num-traits" version = "0.2.14" @@ -3035,6 +3041,7 @@ dependencies = [ "libc", "log", "maplit", + "nonempty", "page_size", "palette", "pest", diff --git a/cli/src/lib.rs b/cli/src/lib.rs index 1204909733..c15257487b 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -151,10 +151,7 @@ pub fn build(target: &Triple, matches: &ArgMatches, run_after_build: bool) -> io .expect("TODO gracefully handle block_on failing"); } } - Err(LoadingProblem::ParsingFailedReport(report)) => { - print!("{}", report); - } - Err(LoadingProblem::NoPlatform(report)) => { + Err(LoadingProblem::FormattedReport(report)) => { print!("{}", report); } Err(other) => { diff --git a/cli/src/repl/gen.rs b/cli/src/repl/gen.rs index cf171e8563..84d5d1704b 100644 --- a/cli/src/repl/gen.rs +++ b/cli/src/repl/gen.rs @@ -57,7 +57,7 @@ pub fn gen_and_eval<'a>( let mut loaded = match loaded { Ok(v) => v, - Err(LoadingProblem::ParsingFailedReport(report)) => { + Err(LoadingProblem::FormattedReport(report)) => { return Ok(ReplOutput::Problems(vec![report])); } Err(e) => { diff --git a/compiler/builtins/docs/Dict.roc b/compiler/builtins/docs/Dict.roc index 94d37dc0b1..9486764194 100644 --- a/compiler/builtins/docs/Dict.roc +++ b/compiler/builtins/docs/Dict.roc @@ -8,15 +8,14 @@ isEmpty : Dict * * -> Bool ## Convert each key and value in the #Dict to something new, by calling a conversion ## function on each of them. Then return a new #Map of the converted keys and values. -## +## ## >>> Dict.map {{ 3.14 => "pi", 1.0 => "one" }} \{ key, value } -> { key: -## +## ## >>> Dict.map {[ "", "a", "bc" ]} Str.isEmpty -## +## ## `map` functions like this are common in Roc, and they all work similarly. ## See for example #Result.map, #List.map, and #Set.map. map : Dict beforeKey beforeValue, - ({ key: beforeKey, value: beforeValue } -> - { key: afterKey, value: afterValue } - ) -> Dict afterKey afterValue + ({ key: beforeKey, value: beforeValue } -> { key: afterKey, value: afterValue }) + -> Dict afterKey afterValue diff --git a/compiler/load/src/file.rs b/compiler/load/src/file.rs index 14a0b78a39..e9474fcd84 100644 --- a/compiler/load/src/file.rs +++ b/compiler/load/src/file.rs @@ -784,6 +784,10 @@ enum Msg<'a> { }, FailedToParse(ParseProblem<'a, SyntaxError<'a>>), + FailedToReadFile { + filename: PathBuf, + error: io::ErrorKind, + }, } #[derive(Debug)] @@ -996,18 +1000,16 @@ pub enum LoadingProblem<'a> { FileProblem { filename: PathBuf, error: io::ErrorKind, - msg: &'static str, }, ParsingFailed(ParseProblem<'a, SyntaxError<'a>>), UnexpectedHeader(String), - /// there is no platform (likely running an Interface module) - NoPlatform(String), MsgChannelDied, ErrJoiningWorkerThreads, TriedToImportAppModule, - /// a formatted report of parsing failure - ParsingFailedReport(String), + + /// a formatted report + FormattedReport(String), } pub enum Phases { @@ -1399,6 +1401,14 @@ where Err(LoadingProblem::ParsingFailed(problem)) => { msg_tx.send(Msg::FailedToParse(problem)).unwrap(); } + Err(LoadingProblem::FileProblem { + filename, + error, + }) => { + msg_tx + .send(Msg::FailedToReadFile { filename, error }) + .unwrap(); + } Err(other) => { return Err(other); } @@ -1457,6 +1467,16 @@ where let worker_listeners = worker_listeners.into_bump_slice(); let msg_tx = msg_tx.clone(); + macro_rules! shut_down_worker_threads { + () => { + for listener in worker_listeners { + listener + .send(WorkerMsg::Shutdown) + .map_err(|_| LoadingProblem::MsgChannelDied)?; + } + }; + } + // The root module will have already queued up messages to process, // and processing those messages will in turn queue up more messages. for msg in msg_rx.iter() { @@ -1490,12 +1510,7 @@ where // We're done! There should be no more messages pending. debug_assert!(msg_rx.is_empty()); - // Shut down all the worker threads. - for listener in worker_listeners { - listener - .send(WorkerMsg::Shutdown) - .map_err(|_| LoadingProblem::MsgChannelDied)?; - } + shut_down_worker_threads!(); return Ok(LoadResult::Monomorphized(finish_specialization( state, @@ -1503,50 +1518,29 @@ where exposed_to_host, )?)); } + Msg::FailedToReadFile { filename, error } => { + shut_down_worker_threads!(); + + let buf = to_file_problem_report(&filename, error); + return Err(LoadingProblem::FormattedReport(buf)); + } + Msg::FailedToParse(problem) => { - // Shut down all the worker threads. - for listener in worker_listeners { - listener - .send(WorkerMsg::Shutdown) - .map_err(|_| LoadingProblem::MsgChannelDied)?; - } + shut_down_worker_threads!(); - use roc_reporting::report::{ - parse_problem, RocDocAllocator, DEFAULT_PALETTE, - }; - - // TODO this is not in fact safe - let src = unsafe { from_utf8_unchecked(problem.bytes) }; - let src_lines: Vec<&str> = src.split('\n').collect(); - - let palette = DEFAULT_PALETTE; - - let mut module_ids = Arc::try_unwrap(state.arc_modules) + let module_ids = Arc::try_unwrap(state.arc_modules) .unwrap_or_else(|_| { panic!("There were still outstanding Arc references to module_ids") }) .into_inner() .into_module_ids(); - let module_id = - module_ids.get_or_insert(&"find module name somehow?".into()); - - let interns = Interns { + let buf = to_parse_problem_report( + problem, module_ids, - all_ident_ids: state.constrained_ident_ids, - }; - - // Report parsing and canonicalization problems - let alloc = RocDocAllocator::new(&src_lines, module_id, &interns); - - let starting_line = 0; - let report = - parse_problem(&alloc, problem.filename.clone(), starting_line, problem); - let mut buf = String::new(); - - report.render_color_terminal(&mut buf, &alloc, &palette); - - return Err(LoadingProblem::ParsingFailedReport(buf)); + state.constrained_ident_ids, + ); + return Err(LoadingProblem::FormattedReport(buf)); } msg => { // This is where most of the main thread's work gets done. @@ -2086,6 +2080,9 @@ fn update<'a>( Msg::FailedToParse(_) => { unreachable!(); } + Msg::FailedToReadFile { .. } => { + unreachable!(); + } } } @@ -2142,72 +2139,8 @@ fn finish_specialization( } Valid(To::NewPackage(p_or_p)) => p_or_p, other => { - use roc_reporting::report::{Report, RocDocAllocator, DEFAULT_PALETTE}; - use ven_pretty::DocAllocator; - - let module_id = state.root_id; - - let palette = DEFAULT_PALETTE; - - // Report parsing and canonicalization problems - let alloc = RocDocAllocator::new(&[], module_id, &interns); - - let report = { - match other { - Valid(_) => unreachable!(), - NotSpecified => { - let doc = alloc.stack(vec![ - alloc.reflow("I could not find a platform based on your input file."), - alloc.reflow(r"Does the module header contain an entry that looks like this:"), - alloc - .parser_suggestion(" packages { base: \"platform\" }") - .indent(4), - alloc.reflow("See also TODO."), - ]); - - Report { - filename: "UNKNOWN.roc".into(), - doc, - title: "NO PLATFORM".to_string(), - } - } - RootIsInterface => { - let doc = alloc.stack(vec![ - alloc.reflow(r"The input file is a interface file, but only app modules can be ran."), - alloc.concat(vec![ - alloc.reflow(r"I will still parse and typecheck the input file and its dependencies,"), - alloc.reflow(r"but won't output any executable."), - ]) - ]); - - Report { - filename: "UNKNOWN.roc".into(), - doc, - title: "NO PLATFORM".to_string(), - } - } - RootIsPkgConfig => { - let doc = alloc.stack(vec![ - alloc.reflow(r"The input file is a package config file, but only app modules can be ran."), - alloc.concat(vec![ - alloc.reflow(r"I will still parse and typecheck the input file and its dependencies,"), - alloc.reflow(r"but won't output any executable."), - ]) - ]); - - Report { - filename: "UNKNOWN.roc".into(), - doc, - title: "NO PLATFORM".to_string(), - } - } - } - }; - let mut buf = String::new(); - - report.render_color_terminal(&mut buf, &alloc, &palette); - - return Err(LoadingProblem::NoPlatform(buf)); + let buf = to_missing_platform_report(state.root_id, other); + return Err(LoadingProblem::FormattedReport(buf)); } }; @@ -2360,7 +2293,7 @@ fn load_pkg_config<'a>( Ok(Msg::Many(vec![effects_module_msg, pkg_config_module_msg])) } Err(fail) => Err(LoadingProblem::ParsingFailed( - SyntaxError::Header(fail).into_parse_problem(filename, bytes), + SyntaxError::Header(fail).into_parse_problem(filename, "", bytes), )), } } @@ -2368,7 +2301,6 @@ fn load_pkg_config<'a>( Err(err) => Err(LoadingProblem::FileProblem { filename, error: err.kind(), - msg: "while reading a Pkg-Config.roc file", }), } } @@ -2633,7 +2565,7 @@ fn parse_header<'a>( module_timing, )), Err(fail) => Err(LoadingProblem::ParsingFailed( - SyntaxError::Header(fail).into_parse_problem(filename, src_bytes), + SyntaxError::Header(fail).into_parse_problem(filename, "", src_bytes), )), } } @@ -2670,7 +2602,6 @@ fn load_filename<'a>( Err(err) => Err(LoadingProblem::FileProblem { filename, error: err.kind(), - msg: "in `load_filename`", }), } } @@ -3683,9 +3614,11 @@ fn parse<'a>(arena: &'a Bump, header: ModuleHeader<'a>) -> Result, Loadi let parsed_defs = match module_defs().parse(&arena, parse_state) { Ok((_, success, _state)) => success, Err((_, fail, _)) => { - return Err(LoadingProblem::ParsingFailed( - fail.into_parse_problem(header.module_path, source), - )); + return Err(LoadingProblem::ParsingFailed(fail.into_parse_problem( + header.module_path, + header.header_src, + source, + ))); } }; @@ -4180,3 +4113,179 @@ where Ok(()) } + +fn to_file_problem_report(filename: &PathBuf, error: io::ErrorKind) -> String { + use roc_reporting::report::{Report, RocDocAllocator, DEFAULT_PALETTE}; + use ven_pretty::DocAllocator; + + let src_lines: Vec<&str> = Vec::new(); + + let mut module_ids = ModuleIds::default(); + + let module_id = module_ids.get_or_insert(&"find module name somehow?".into()); + + let interns = Interns::default(); + + // Report parsing and canonicalization problems + let alloc = RocDocAllocator::new(&src_lines, module_id, &interns); + + let report = match error { + io::ErrorKind::NotFound => { + let doc = alloc.stack(vec![ + alloc.reflow(r"I am looking for this file, but it's not there:"), + alloc + .parser_suggestion(filename.to_str().unwrap()) + .indent(4), + alloc.concat(vec![ + alloc.reflow(r"Is the file supposed to be there? "), + alloc.reflow("Maybe there is a typo in the file name?"), + ]), + ]); + + Report { + filename: "UNKNOWN.roc".into(), + doc, + title: "FILE NOT FOUND".to_string(), + } + } + io::ErrorKind::PermissionDenied => { + let doc = alloc.stack(vec![ + alloc.reflow(r"I don't have the required permissions to read this file:"), + alloc + .parser_suggestion(filename.to_str().unwrap()) + .indent(4), + alloc.concat(vec![ + alloc.reflow(r"Is it the right file? Maybe change its permissions?") + ]), + ]); + + Report { + filename: "UNKNOWN.roc".into(), + doc, + title: "PERMISSION DENIED".to_string(), + } + } + _ => { + let error = std::io::Error::from(error); + let formatted = format!("{}", error); + let doc = alloc.concat(vec![ + alloc.reflow(r"I tried to read this file, but ran into a "), + alloc.text(formatted), + alloc.reflow(r" problem."), + ]); + + Report { + filename: "UNKNOWN.roc".into(), + doc, + title: "FILE PROBLEM".to_string(), + } + } + }; + + let mut buf = String::new(); + let palette = DEFAULT_PALETTE; + report.render_color_terminal(&mut buf, &alloc, &palette); + + buf +} + +fn to_parse_problem_report<'a>( + problem: ParseProblem<'a, SyntaxError<'a>>, + mut module_ids: ModuleIds, + all_ident_ids: MutMap, +) -> String { + use roc_reporting::report::{parse_problem, RocDocAllocator, DEFAULT_PALETTE}; + + // TODO this is not in fact safe + let src = unsafe { from_utf8_unchecked(problem.bytes) }; + let mut src_lines: Vec<&str> = problem.prefix.lines().collect(); + src_lines.extend(src.lines().skip(1)); + + let module_id = module_ids.get_or_insert(&"find module name somehow?".into()); + + let interns = Interns { + module_ids, + all_ident_ids, + }; + + // Report parsing and canonicalization problems + let alloc = RocDocAllocator::new(&src_lines, module_id, &interns); + + let starting_line = 0; + let report = parse_problem(&alloc, problem.filename.clone(), starting_line, problem); + + let mut buf = String::new(); + let palette = DEFAULT_PALETTE; + + report.render_color_terminal(&mut buf, &alloc, &palette); + + buf +} + +fn to_missing_platform_report(module_id: ModuleId, other: PlatformPath) -> String { + use roc_reporting::report::{Report, RocDocAllocator, DEFAULT_PALETTE}; + use ven_pretty::DocAllocator; + use PlatformPath::*; + + // Report parsing and canonicalization problems + let interns = Interns::default(); + let alloc = RocDocAllocator::new(&[], module_id, &interns); + + let report = { + match other { + Valid(_) => unreachable!(), + NotSpecified => { + let doc = alloc.stack(vec![ + alloc.reflow("I could not find a platform based on your input file."), + alloc.reflow(r"Does the module header contain an entry that looks like this:"), + alloc + .parser_suggestion(" packages { base: \"platform\" }") + .indent(4), + alloc.reflow("See also TODO."), + ]); + + Report { + filename: "UNKNOWN.roc".into(), + doc, + title: "NO PLATFORM".to_string(), + } + } + RootIsInterface => { + let doc = alloc.stack(vec![ + alloc.reflow(r"The input file is a interface file, but only app modules can be ran."), + alloc.concat(vec![ + alloc.reflow(r"I will still parse and typecheck the input file and its dependencies,"), + alloc.reflow(r"but won't output any executable."), + ]) + ]); + + Report { + filename: "UNKNOWN.roc".into(), + doc, + title: "NO PLATFORM".to_string(), + } + } + RootIsPkgConfig => { + let doc = alloc.stack(vec![ + alloc.reflow(r"The input file is a package config file, but only app modules can be ran."), + alloc.concat(vec![ + alloc.reflow(r"I will still parse and typecheck the input file and its dependencies,"), + alloc.reflow(r"but won't output any executable."), + ]) + ]); + + Report { + filename: "UNKNOWN.roc".into(), + doc, + title: "NO PLATFORM".to_string(), + } + } + } + }; + + let palette = DEFAULT_PALETTE; + let mut buf = String::new(); + report.render_color_terminal(&mut buf, &alloc, &palette); + + buf +} diff --git a/compiler/load/tests/fixtures/build/no_deps/MissingDep.roc b/compiler/load/tests/fixtures/build/no_deps/MissingDep.roc new file mode 100644 index 0000000000..c67fcb7403 --- /dev/null +++ b/compiler/load/tests/fixtures/build/no_deps/MissingDep.roc @@ -0,0 +1,8 @@ +interface MissingDep + exposes [ unit ] + imports [ ThisFileIsMissing ] + +Unit : [ Unit ] + +unit : Unit +unit = Unit diff --git a/compiler/load/tests/test_load.rs b/compiler/load/tests/test_load.rs index 5eff2b2a9d..71ca201ef0 100644 --- a/compiler/load/tests/test_load.rs +++ b/compiler/load/tests/test_load.rs @@ -32,87 +32,94 @@ mod test_load { // HELPERS - fn multiple_modules(files: Vec<(&str, &str)>) -> LoadedModule { - multiple_modules_help(files).unwrap() + fn multiple_modules(files: Vec<(&str, &str)>) -> Result { + use roc_load::file::LoadingProblem; + + let arena = Bump::new(); + let arena = &arena; + + match multiple_modules_help(arena, files) { + Err(io_error) => panic!("IO trouble: {:?}", io_error), + Ok(Err(LoadingProblem::FormattedReport(buf))) => Err(buf), + Ok(Err(loading_problem)) => Err(format!("{:?}", loading_problem)), + Ok(Ok(mut loaded_module)) => { + let home = loaded_module.module_id; + + assert_eq!( + loaded_module.can_problems.remove(&home).unwrap_or_default(), + Vec::new() + ); + assert_eq!( + loaded_module + .type_problems + .remove(&home) + .unwrap_or_default(), + Vec::new() + ); + + Ok(loaded_module) + } + } } - fn multiple_modules_help(mut files: Vec<(&str, &str)>) -> Result { + fn multiple_modules_help<'a>( + arena: &'a Bump, + mut files: Vec<(&str, &str)>, + ) -> Result>, std::io::Error> { use std::fs::File; use std::io::Write; use std::path::PathBuf; use tempfile::tempdir; - let arena = Bump::new(); - let arena = &arena; - let stdlib = roc_builtins::std::standard_stdlib(); let mut file_handles: Vec<_> = Vec::new(); let exposed_types = MutMap::default(); - let loaded = { - // create a temporary directory - let dir = tempdir()?; - let app_module = files.pop().unwrap(); - let interfaces = files; + // create a temporary directory + let dir = tempdir()?; - debug_assert!( - app_module.1.starts_with("app"), - "The final module should be the application module" - ); + let app_module = files.pop().unwrap(); + let interfaces = files; - for (name, source) in interfaces { - let mut filename = PathBuf::from(name); - filename.set_extension("roc"); - let file_path = dir.path().join(filename.clone()); - let mut file = File::create(file_path)?; - writeln!(file, "{}", source)?; - file_handles.push(file); - } + debug_assert!( + app_module.1.starts_with("app"), + "The final module should be the application module" + ); - let result = { - let (name, source) = app_module; + for (name, source) in interfaces { + let mut filename = PathBuf::from(name); + filename.set_extension("roc"); + let file_path = dir.path().join(filename.clone()); + let mut file = File::create(file_path)?; + writeln!(file, "{}", source)?; + file_handles.push(file); + } - let filename = PathBuf::from(name); - let file_path = dir.path().join(filename); - let full_file_path = file_path.clone(); - let mut file = File::create(file_path)?; - writeln!(file, "{}", source)?; - file_handles.push(file); + let result = { + let (name, source) = app_module; - roc_load::file::load_and_typecheck( - arena, - full_file_path, - &stdlib, - dir.path(), - exposed_types, - 8, - builtin_defs_map, - ) - }; + let filename = PathBuf::from(name); + let file_path = dir.path().join(filename); + let full_file_path = file_path.clone(); + let mut file = File::create(file_path)?; + writeln!(file, "{}", source)?; + file_handles.push(file); - dir.close()?; - - result + roc_load::file::load_and_typecheck( + arena, + full_file_path, + arena.alloc(stdlib), + dir.path(), + exposed_types, + 8, + builtin_defs_map, + ) }; - let mut loaded_module = loaded.expect("failed to load module"); + dir.close()?; - let home = loaded_module.module_id; - - assert_eq!( - loaded_module.can_problems.remove(&home).unwrap_or_default(), - Vec::new() - ); - assert_eq!( - loaded_module - .type_problems - .remove(&home) - .unwrap_or_default(), - Vec::new() - ); - - Ok(loaded_module) + Ok(result) } fn load_fixture( @@ -134,9 +141,9 @@ mod test_load { ); let mut loaded_module = match loaded { Ok(x) => x, - Err(roc_load::file::LoadingProblem::ParsingFailedReport(report)) => { + Err(roc_load::file::LoadingProblem::FormattedReport(report)) => { println!("{}", report); - panic!(); + panic!("{}", report); } Err(e) => panic!("{:?}", e), }; @@ -285,7 +292,8 @@ mod test_load { ), ), ]; - multiple_modules(modules); + + assert!(multiple_modules(modules).is_ok()); } #[test] @@ -517,61 +525,69 @@ mod test_load { ); } - // #[test] - // fn load_records() { - // use roc::types::{ErrorType, Mismatch, Problem, TypeExt}; + #[test] + fn parse_problem() { + let modules = vec![( + "Main", + indoc!( + r#" + app "test-app" packages { blah: "./blah" } provides [ main ] to blah - // let subs_by_module = MutMap::default(); - // let loaded_module = - // load_fixture("interface_with_deps", "Records", subs_by_module); + main = [ + "# + ), + )]; - // // NOTE: `a` here is unconstrained, so unifies with - // let expected_types = hashmap! { - // "Records.intVal" => "a", - // }; + match multiple_modules(modules) { + Err(report) => assert_eq!( + report, + indoc!( + " + \u{1b}[36m── UNFINISHED LIST ─────────────────────────────────────────────────────────────\u{1b}[0m + + I cannot find the end of this list: - // let a = ErrorType::FlexVar("a".into()); + \u{1b}[36m3\u{1b}[0m\u{1b}[36m│\u{1b}[0m \u{1b}[37mmain = [\u{1b}[0m + \u{1b}[31m^\u{1b}[0m - // let mut record = SendMap::default(); - // record.insert("x".into(), a); + You could change it to something like \u{1b}[33m[ 1, 2, 3 ]\u{1b}[0m or even just \u{1b}[33m[]\u{1b}[0m. + Anything where there is an open and a close square bracket, and where + the elements of the list are separated by commas. - // let problem = Problem::Mismatch( - // Mismatch::TypeMismatch, - // ErrorType::Record(SendMap::default(), TypeExt::Closed), - // ErrorType::Record(record, TypeExt::FlexOpen("b".into())), - // ); + \u{1b}[4mNote\u{1b}[0m: I may be confused by indentation" + ) + ), + Ok(_) => unreachable!("we expect failure here"), + } + } - // assert_eq!(loaded_module.problems, vec![problem]); - // assert_eq!(expected_types.len(), loaded_module.declarations.len()); + #[test] + #[should_panic( + expected = "FileProblem { filename: \"tests/fixtures/build/interface_with_deps/invalid$name.roc\", error: NotFound }" + )] + fn file_not_found() { + let subs_by_module = MutMap::default(); + let loaded_module = load_fixture("interface_with_deps", "invalid$name", subs_by_module); - // let mut subs = loaded_module.solved.into_inner(); + expect_types( + loaded_module, + hashmap! { + "str" => "Str", + }, + ); + } - // for decl in loaded_module.declarations { - // let def = match decl { - // Declare(def) => def, - // rec_decl @ DeclareRec(_) => { - // panic!( - // "Unexpected recursive def in module declarations: {:?}", - // rec_decl - // ); - // } - // cycle @ InvalidCycle(_, _) => { - // panic!("Unexpected cyclic def in module declarations: {:?}", cycle); - // } - // }; + #[test] + #[should_panic(expected = "FILE NOT FOUND")] + fn imported_file_not_found() { + let subs_by_module = MutMap::default(); + let loaded_module = load_fixture("no_deps", "MissingDep", subs_by_module); - // for (symbol, expr_var) in def.pattern_vars { - // let content = subs.get(expr_var).content; - - // name_all_type_vars(expr_var, &mut subs); - - // let actual_str = content_to_string(content, &mut subs); - // let expected_type = expected_types.get(symbol.as_str()).unwrap_or_else(|| { - // panic!("Defs included an unexpected symbol: {:?}", symbol) - // }); - - // assert_eq!((&symbol, expected_type), (&symbol, &actual_str.as_str())); - // } - // } - // } + expect_types( + loaded_module, + hashmap! { + "str" => "Str", + }, + ); + } } diff --git a/compiler/mono/tests/test_mono.rs b/compiler/mono/tests/test_mono.rs index 70ad80e504..e476947adf 100644 --- a/compiler/mono/tests/test_mono.rs +++ b/compiler/mono/tests/test_mono.rs @@ -69,7 +69,7 @@ mod test_mono { let mut loaded = match loaded { Ok(x) => x, - Err(roc_load::file::LoadingProblem::ParsingFailedReport(report)) => { + Err(roc_load::file::LoadingProblem::FormattedReport(report)) => { println!("{}", report); panic!(); } diff --git a/compiler/parse/src/parser.rs b/compiler/parse/src/parser.rs index 624555e07c..a6df64b54b 100644 --- a/compiler/parse/src/parser.rs +++ b/compiler/parse/src/parser.rs @@ -360,6 +360,7 @@ impl<'a> SyntaxError<'a> { pub fn into_parse_problem( self, filename: std::path::PathBuf, + prefix: &'a str, bytes: &'a [u8], ) -> ParseProblem<'a, SyntaxError<'a>> { ParseProblem { @@ -368,6 +369,7 @@ impl<'a> SyntaxError<'a> { problem: self, filename, bytes, + prefix, } } } @@ -679,6 +681,8 @@ pub struct ParseProblem<'a, T> { pub problem: T, pub filename: std::path::PathBuf, pub bytes: &'a [u8], + /// prefix is usually the header (for parse problems in the body), or empty + pub prefix: &'a str, } pub trait Parser<'a, Output, Error> { diff --git a/compiler/reporting/tests/test_reporting.rs b/compiler/reporting/tests/test_reporting.rs index ee2af9e6bf..33f31714f4 100644 --- a/compiler/reporting/tests/test_reporting.rs +++ b/compiler/reporting/tests/test_reporting.rs @@ -129,7 +129,7 @@ mod test_reporting { let alloc = RocDocAllocator::new(&src_lines, home, &interns); - let problem = fail.into_parse_problem(filename.clone(), src.as_bytes()); + let problem = fail.into_parse_problem(filename.clone(), "", src.as_bytes()); let doc = parse_problem(&alloc, filename, 0, problem); callback(doc.pretty(&alloc).append(alloc.line()), buf) @@ -190,8 +190,11 @@ mod test_reporting { let alloc = RocDocAllocator::new(&src_lines, home, &interns); use roc_parse::parser::SyntaxError; - let problem = - SyntaxError::Header(fail).into_parse_problem(filename.clone(), src.as_bytes()); + let problem = SyntaxError::Header(fail).into_parse_problem( + filename.clone(), + "", + src.as_bytes(), + ); let doc = parse_problem(&alloc, filename, 0, problem); callback(doc.pretty(&alloc).append(alloc.line()), buf) diff --git a/compiler/test_gen/src/helpers/eval.rs b/compiler/test_gen/src/helpers/eval.rs index f4d9dc5d4e..3302ce5fcb 100644 --- a/compiler/test_gen/src/helpers/eval.rs +++ b/compiler/test_gen/src/helpers/eval.rs @@ -74,7 +74,7 @@ pub fn helper<'a>( let mut loaded = match loaded { Ok(x) => x, - Err(roc_load::file::LoadingProblem::ParsingFailedReport(report)) => { + Err(roc_load::file::LoadingProblem::FormattedReport(report)) => { println!("{}", report); panic!(); } diff --git a/docs/src/lib.rs b/docs/src/lib.rs index cf6983344e..76369e0d1b 100644 --- a/docs/src/lib.rs +++ b/docs/src/lib.rs @@ -122,7 +122,7 @@ pub fn files_to_documentations( builtin_defs_map, ) { Ok(mut loaded) => files_docs.extend(loaded.documentation.drain().map(|x| x.1)), - Err(LoadingProblem::ParsingFailedReport(report)) => { + Err(LoadingProblem::FormattedReport(report)) => { println!("{}", report); panic!(); } diff --git a/docs/tests/test_docs.rs b/docs/tests/test_docs.rs index da99e07b08..1099d88b69 100644 --- a/docs/tests/test_docs.rs +++ b/docs/tests/test_docs.rs @@ -1,5 +1,5 @@ use roc_docs::{documentation_to_template_data, files_to_documentations, ModuleEntry}; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; #[cfg(test)] mod test_docs { diff --git a/editor/Cargo.toml b/editor/Cargo.toml index 704462fa1c..0fa795d25c 100644 --- a/editor/Cargo.toml +++ b/editor/Cargo.toml @@ -48,6 +48,7 @@ confy = { git = 'https://github.com/rust-cli/confy', features = [ "yaml_conf" ], default-features = false } serde = { version = "1.0.123", features = ["derive"] } +nonempty = "0.6.0" [dependencies.bytemuck] version = "1.4" diff --git a/editor/src/editor/code_lines.rs b/editor/src/editor/code_lines.rs new file mode 100644 index 0000000000..28acf257d5 --- /dev/null +++ b/editor/src/editor/code_lines.rs @@ -0,0 +1,76 @@ +use crate::ui::text::lines::Lines; +use crate::ui::ui_error::UIResult; +use crate::ui::util::slice_get; +use bumpalo::collections::String as BumpString; +use bumpalo::Bump; + +#[derive(Debug)] +pub struct CodeLines { + pub lines: Vec, + pub nr_of_chars: usize, +} + +impl CodeLines { + pub fn from_str(code_str: &str) -> CodeLines { + CodeLines { + lines: split_inclusive(code_str), + nr_of_chars: code_str.len(), + } + } +} + +//TODO use rust's split_inclusive once it's no longer unstable +fn split_inclusive(code_str: &str) -> Vec { + let mut split_vec: Vec = Vec::new(); + let mut temp_str = String::new(); + + for token in code_str.chars() { + if token != '\n' { + temp_str.push(token); + } else { + split_vec.push(temp_str); + temp_str = String::new(); + temp_str.push(token); + } + } + + if !temp_str.is_empty() { + split_vec.push(temp_str); + } + + split_vec +} + +impl Lines for CodeLines { + fn get_line(&self, line_nr: usize) -> UIResult<&str> { + let line_string = slice_get(line_nr, &self.lines)?; + + Ok(&line_string) + } + + fn line_len(&self, line_nr: usize) -> UIResult { + self.get_line(line_nr).map(|line| line.len()) + } + + fn nr_of_lines(&self) -> usize { + self.lines.len() + } + + fn nr_of_chars(&self) -> usize { + self.nr_of_chars + } + + fn all_lines<'a>(&self, arena: &'a Bump) -> BumpString<'a> { + let mut lines = BumpString::with_capacity_in(self.nr_of_chars(), arena); + + for line in &self.lines { + lines.push_str(&line); + } + + lines + } + + fn last_char(&self, line_nr: usize) -> UIResult> { + Ok(self.get_line(line_nr)?.chars().last()) + } +} diff --git a/editor/src/editor/keyboard_input.rs b/editor/src/editor/keyboard_input.rs index 7f8941f7cf..115cbc600e 100644 --- a/editor/src/editor/keyboard_input.rs +++ b/editor/src/editor/keyboard_input.rs @@ -3,7 +3,6 @@ use crate::editor::mvc::app_model::AppModel; use crate::editor::mvc::app_update::{ handle_copy, handle_cut, handle_paste, pass_keydown_to_focused, }; -use crate::editor::slow_pool::SlowPool; use winit::event::VirtualKeyCode::*; use winit::event::{ElementState, ModifiersState, VirtualKeyCode}; @@ -12,7 +11,6 @@ pub fn handle_keydown( virtual_keycode: VirtualKeyCode, modifiers: ModifiersState, app_model: &mut AppModel, - markup_node_pool: &mut SlowPool, ) -> EdResult<()> { if let ElementState::Released = elem_state { return Ok(()); @@ -20,7 +18,7 @@ pub fn handle_keydown( match virtual_keycode { Left | Up | Right | Down => { - pass_keydown_to_focused(&modifiers, virtual_keycode, app_model, markup_node_pool)? + pass_keydown_to_focused(&modifiers, virtual_keycode, app_model)? } Copy => handle_copy(app_model)?, @@ -42,9 +40,7 @@ pub fn handle_keydown( } } - A | Home | End => { - pass_keydown_to_focused(&modifiers, virtual_keycode, app_model, markup_node_pool)? - } + A | Home | End => pass_keydown_to_focused(&modifiers, virtual_keycode, app_model)?, _ => (), } diff --git a/editor/src/editor/main.rs b/editor/src/editor/main.rs index ec06a8052e..4d9ba84c02 100644 --- a/editor/src/editor/main.rs +++ b/editor/src/editor/main.rs @@ -1,6 +1,5 @@ use super::keyboard_input; use super::style::CODE_TXT_XY; -use super::util::slice_get; use crate::editor::ed_error::print_ui_err; use crate::editor::resources::strings::NOTHING_OPENED; use crate::editor::slow_pool::SlowPool; @@ -21,6 +20,7 @@ use crate::graphics::{ use crate::lang::expr::Env; use crate::lang::pool::Pool; use crate::ui::ui_error::UIError::FileOpenFailed; +use crate::ui::util::slice_get; use bumpalo::collections::String as BumpString; use bumpalo::Bump; use cgmath::Vector2; @@ -153,21 +153,33 @@ fn run_event_loop(file_path_opt: Option<&Path>) -> Result<(), Box> { let mut code_str = BumpString::from_str_in("", &code_arena); - if let Some(file_path) = file_path_opt { + let file_path = if let Some(file_path) = file_path_opt { match std::fs::read_to_string(file_path) { Ok(file_as_str) => { code_str = BumpString::from_str_in(&file_as_str, &code_arena); + file_path } - Err(e) => print_ui_err(&FileOpenFailed { - path_str: file_path.to_string_lossy().to_string(), - err_msg: e.to_string(), - }), + Err(e) => { + print_ui_err(&FileOpenFailed { + path_str: file_path.to_string_lossy().to_string(), + err_msg: e.to_string(), + }); + Path::new("") + } } - } + } else { + Path::new("") + }; let ed_model_opt = { - let ed_model_res = ed_model::init_model(&code_str, env, &code_arena, &mut markup_node_pool); + let ed_model_res = ed_model::init_model( + &code_str, + file_path, + env, + &code_arena, + &mut markup_node_pool, + ); match ed_model_res { Ok(mut ed_model) => { @@ -256,7 +268,6 @@ fn run_event_loop(file_path_opt: Option<&Path>) -> Result<(), Box> { virtual_keycode, keyboard_modifiers, &mut app_model, - &mut markup_node_pool, ); if let Err(e) = keydown_res { diff --git a/editor/src/editor/markup/caret.rs b/editor/src/editor/markup/caret.rs deleted file mode 100644 index 0eff39e55a..0000000000 --- a/editor/src/editor/markup/caret.rs +++ /dev/null @@ -1,130 +0,0 @@ -use crate::editor::mvc::ed_model::LeafIndex; -use crate::editor::{ - ed_error::{CaretNotFound, EdResult, NodeWithoutAttributes}, - markup::attribute::Caret, - markup::nodes::MarkupNode, - slow_pool::{SlowNodeId, SlowPool}, -}; -use snafu::ensure; - -// Returns id of node that has Caret attribute -pub fn set_caret_at_start( - markup_node_id: SlowNodeId, - markup_node_pool: &mut SlowPool, -) -> EdResult { - let markup_node = markup_node_pool.get_mut(markup_node_id); - - match markup_node { - MarkupNode::Nested { - ast_node_id: _, - children_ids: _, - parent_id_opt: _, - } => NodeWithoutAttributes {}.fail(), - MarkupNode::Text { - content: _, - ast_node_id: _, - syn_high_style: _, - attributes, - parent_id_opt: _, - } => { - attributes.add(Caret::new_attr(0)); - Ok(markup_node_id) - } - MarkupNode::Blank { - ast_node_id: _, - attributes, - syn_high_style: _, - parent_id_opt: _, - } => { - attributes.add(Caret::new_attr(0)); - Ok(markup_node_id) - } - } -} - -// Returns nodes containing the carets after the move, as well as its position in a DFS ordered list of leaves. -pub fn move_carets_right_for_node( - node_with_caret_id: SlowNodeId, - caret_id_leaf_index: LeafIndex, - next_leaf_id_opt: Option, - markup_node_pool: &mut SlowPool, -) -> EdResult> { - let carets = get_carets(node_with_caret_id, markup_node_pool)?; - let node_content = markup_node_pool.get(node_with_caret_id).get_content()?; - - ensure!( - !carets.is_empty(), - CaretNotFound { - node_id: node_with_caret_id - } - ); - - let mut new_nodes_w_carets = Vec::new(); - - for caret in carets { - let (new_node, new_leaf_index) = if caret.offset_col + 1 < node_content.len() { - increase_caret_offset(node_with_caret_id, caret.offset_col, markup_node_pool)?; - - (node_with_caret_id, caret_id_leaf_index) - } else if let Some(next_child_id) = next_leaf_id_opt { - delete_caret(node_with_caret_id, caret.offset_col, markup_node_pool)?; - - let next_child = markup_node_pool.get_mut(next_child_id); - let child_attrs = next_child.get_mut_attributes()?; - child_attrs.add_caret(0); - - (next_child_id, caret_id_leaf_index + 1) - } else if caret.offset_col + 1 == node_content.len() { - // For last char in editor. - // In other places we jump to start of next node instead of going to end of - // this node, otherwise it would be like there is a space between every node. - increase_caret_offset(node_with_caret_id, caret.offset_col, markup_node_pool)?; - (node_with_caret_id, caret_id_leaf_index) - } else { - // Caret is at its end, keep it here. - (node_with_caret_id, caret_id_leaf_index) - }; - - new_nodes_w_carets.push((new_node, new_leaf_index)); - } - - Ok(new_nodes_w_carets) -} - -fn get_carets(node_with_caret_id: SlowNodeId, markup_node_pool: &SlowPool) -> EdResult> { - let curr_node = markup_node_pool.get(node_with_caret_id); - let attributes = curr_node.get_attributes()?; - - Ok(attributes.get_carets()) -} - -// this method assumes the current caret position is checked and increasing it will not lead to an invalid caret position -fn increase_caret_offset( - node_id: SlowNodeId, - offset_col: usize, - markup_node_pool: &mut SlowPool, -) -> EdResult<()> { - let node = markup_node_pool.get_mut(node_id); - let attrs = node.get_mut_attributes()?; - let mut carets = attrs.get_mut_carets(); - - for caret in carets.iter_mut() { - if caret.offset_col == offset_col { - caret.offset_col += 1; - } - } - - Ok(()) -} - -fn delete_caret( - node_id: SlowNodeId, - offset_col: usize, - markup_node_pool: &mut SlowPool, -) -> EdResult<()> { - let node = markup_node_pool.get_mut(node_id); - let attrs = node.get_mut_attributes()?; - attrs.delete_caret(offset_col, node_id)?; - - Ok(()) -} diff --git a/editor/src/editor/markup/mod.rs b/editor/src/editor/markup/mod.rs index 2aabfb9f65..ecae51c6d2 100644 --- a/editor/src/editor/markup/mod.rs +++ b/editor/src/editor/markup/mod.rs @@ -1,3 +1,2 @@ pub mod attribute; -pub mod caret; pub mod nodes; diff --git a/editor/src/editor/markup/nodes.rs b/editor/src/editor/markup/nodes.rs index bb071283dc..9b1d9be4a9 100644 --- a/editor/src/editor/markup/nodes.rs +++ b/editor/src/editor/markup/nodes.rs @@ -1,18 +1,13 @@ use super::attribute::Attributes; -use crate::editor::ed_error::GetContentOnNestedNode; -use crate::editor::ed_error::NodeWithoutAttributes; -use crate::editor::{ - ed_error::EdResult, - slow_pool::{SlowNodeId, SlowPool}, - syntax_highlight::HighlightStyle, -}; +use crate::editor::slow_pool::SlowNodeId; +use crate::editor::slow_pool::SlowPool; +use crate::editor::syntax_highlight::HighlightStyle; use crate::lang::{ ast::Expr2, expr::Env, pool::{NodeId, PoolStr}, }; use bumpalo::Bump; -use snafu::OptionExt; #[derive(Debug)] pub enum MarkupNode { @@ -38,117 +33,6 @@ pub enum MarkupNode { pub const BLANK_PLACEHOLDER: &str = " "; -impl MarkupNode { - pub fn get_children_ids(&self) -> Vec { - match self { - MarkupNode::Nested { - ast_node_id: _, - children_ids, - parent_id_opt: _, - } => children_ids.to_vec(), - MarkupNode::Text { - content: _, - ast_node_id: _, - syn_high_style: _, - attributes: _, - parent_id_opt: _, - } => Vec::new(), - MarkupNode::Blank { - ast_node_id: _, - attributes: _, - syn_high_style: _, - parent_id_opt: _, - } => Vec::new(), - } - } - - // can't be &str, this creates borrowing issues - pub fn get_content(&self) -> EdResult { - match self { - MarkupNode::Nested { - ast_node_id: _, - children_ids: _, - parent_id_opt: _, - } => GetContentOnNestedNode {}.fail(), - MarkupNode::Text { - content, - ast_node_id: _, - syn_high_style: _, - attributes: _, - parent_id_opt: _, - } => Ok(content.clone()), - MarkupNode::Blank { - ast_node_id: _, - attributes: _, - syn_high_style: _, - parent_id_opt: _, - } => Ok(BLANK_PLACEHOLDER.to_owned()), - } - } - - // Do Depth First Search and return SlowNodeId's in order of encounter - // The returning vec is used for caret movement - pub fn get_dfs_leaves( - &self, - node_id: SlowNodeId, - markup_node_pool: &SlowPool, - ordered_leaves: &mut Vec, - ) { - let children_ids = self.get_children_ids(); - - if children_ids.is_empty() { - ordered_leaves.push(node_id); - } else { - for child_id in self.get_children_ids() { - let child = markup_node_pool.get(child_id); - child.get_dfs_leaves(child_id, markup_node_pool, ordered_leaves); - } - } - } - - pub fn get_mut_attributes(&mut self) -> EdResult<&mut Attributes> { - let attrs_ref = match self { - MarkupNode::Nested { .. } => None, - MarkupNode::Text { - content: _, - ast_node_id: _, - syn_high_style: _, - attributes, - parent_id_opt: _, - } => Some(attributes), - MarkupNode::Blank { - ast_node_id: _, - attributes, - syn_high_style: _, - parent_id_opt: _, - } => Some(attributes), - }; - - attrs_ref.with_context(|| NodeWithoutAttributes {}) - } - - pub fn get_attributes(&self) -> EdResult<&Attributes> { - let attrs_ref = match self { - MarkupNode::Nested { .. } => None, - MarkupNode::Text { - content: _, - ast_node_id: _, - syn_high_style: _, - attributes, - parent_id_opt: _, - } => Some(attributes), - MarkupNode::Blank { - ast_node_id: _, - attributes, - syn_high_style: _, - parent_id_opt: _, - } => Some(attributes), - }; - - attrs_ref.with_context(|| NodeWithoutAttributes {}) - } -} - fn get_string<'a>(env: &Env<'a>, pool_str: &PoolStr) -> String { pool_str.as_str(env.pool).to_owned() } diff --git a/editor/src/editor/mod.rs b/editor/src/editor/mod.rs index c0ac3ad56e..b06ae4fc99 100644 --- a/editor/src/editor/mod.rs +++ b/editor/src/editor/mod.rs @@ -1,3 +1,4 @@ +mod code_lines; mod config; mod ed_error; mod keyboard_input; diff --git a/editor/src/editor/mvc/app_update.rs b/editor/src/editor/mvc/app_update.rs index ee934c80d4..a6d41572c9 100644 --- a/editor/src/editor/mvc/app_update.rs +++ b/editor/src/editor/mvc/app_update.rs @@ -1,6 +1,6 @@ use super::app_model::AppModel; use crate::editor::ed_error::EdResult; -use crate::editor::slow_pool::SlowPool; +use crate::ui::text::lines::SelectableLines; use crate::window::keyboard_input::from_winit; use winit::event::{ModifiersState, VirtualKeyCode}; @@ -38,13 +38,12 @@ pub fn pass_keydown_to_focused( modifiers_winit: &ModifiersState, virtual_keycode: VirtualKeyCode, app_model: &mut AppModel, - markup_node_pool: &mut SlowPool, ) -> EdResult<()> { let modifiers = from_winit(modifiers_winit); if let Some(ref mut ed_model) = app_model.ed_model_opt { if ed_model.has_focus { - ed_model.handle_key_down(&modifiers, virtual_keycode, markup_node_pool)?; + ed_model.handle_key_down(&modifiers, virtual_keycode)?; } } diff --git a/editor/src/editor/mvc/ed_model.rs b/editor/src/editor/mvc/ed_model.rs index 2784f8fb1c..7db649567a 100644 --- a/editor/src/editor/mvc/ed_model.rs +++ b/editor/src/editor/mvc/ed_model.rs @@ -1,39 +1,48 @@ +use crate::editor::code_lines::CodeLines; use crate::editor::slow_pool::{SlowNodeId, SlowPool}; use crate::editor::syntax_highlight::HighlightStyle; use crate::editor::{ ed_error::EdError::ParseError, ed_error::EdResult, markup::attribute::{Attributes, Caret}, - markup::caret::{move_carets_right_for_node, set_caret_at_start}, markup::nodes::{expr2_to_markup, set_parent_for_all, MarkupNode}, }; use crate::graphics::primitives::rect::Rect; use crate::lang::ast::Expr2; use crate::lang::expr::{str_to_expr2, Env}; use crate::lang::scope::Scope; +use crate::ui::text::caret_w_select::CaretWSelect; +use crate::ui::text::lines::MoveCaretFun; +use crate::ui::text::selection::validate_raw_sel; +use crate::ui::text::selection::RawSelection; +use crate::ui::text::selection::Selection; +use crate::ui::text::text_pos::TextPos; +use crate::ui::text::{lines, lines::Lines, lines::SelectableLines}; +use crate::ui::ui_error::UIResult; use crate::window::keyboard_input::Modifiers; use bumpalo::collections::String as BumpString; use bumpalo::Bump; +use nonempty::NonEmpty; use roc_region::all::Region; -use std::collections::HashSet; +use std::path::Path; use winit::event::VirtualKeyCode; - -pub type LeafIndex = usize; +use VirtualKeyCode::*; #[derive(Debug)] pub struct EdModel<'a> { pub module: EdModule<'a>, - pub code_as_str: &'a str, + pub file_path: &'a Path, + pub code_lines: CodeLines, pub markup_root_id: SlowNodeId, pub glyph_dim_rect_opt: Option, pub has_focus: bool, - // This HashSet may have less elements than there are carets. There can be multiple carets for a single node. - caret_nodes: HashSet<(SlowNodeId, LeafIndex)>, - dfs_ordered_leaves: Vec, + // Option: MarkupNode that corresponds to caret position, Option because this SlowNodeId is only calculated when it needs to be used. + pub caret_w_select_vec: NonEmpty<(CaretWSelect, Option)>, } pub fn init_model<'a>( code_str: &'a BumpString, + file_path: &'a Path, env: Env<'a>, code_arena: &'a Bump, markup_node_pool: &mut SlowPool, @@ -65,62 +74,178 @@ pub fn init_model<'a>( temp_markup_root_id }; - let mut dfs_ordered_leaves = Vec::new(); - markup_node_pool.get(markup_root_id).get_dfs_leaves( - markup_root_id, - markup_node_pool, - &mut dfs_ordered_leaves, - ); - - // unwrap because it's not possible to only have a single Nested node without children. - let first_leaf_id = dfs_ordered_leaves.first().unwrap(); - let node_w_caret_id = set_caret_at_start(*first_leaf_id, markup_node_pool)?; - Ok(EdModel { module, - code_as_str: code_str, + file_path, + code_lines: CodeLines::from_str(code_str), markup_root_id, glyph_dim_rect_opt: None, has_focus: true, - caret_nodes: vec![(node_w_caret_id, 0)].into_iter().collect(), - dfs_ordered_leaves, + caret_w_select_vec: NonEmpty::new((CaretWSelect::default(), None)), }) } impl<'a> EdModel<'a> { - pub fn handle_key_down( + pub fn move_caret( &mut self, - _modifiers: &Modifiers, - virtual_keycode: VirtualKeyCode, - markup_node_pool: &mut SlowPool, - ) -> EdResult<()> { - match virtual_keycode { - VirtualKeyCode::Right => { - let mut new_caret_nodes: Vec<(SlowNodeId, LeafIndex)> = Vec::new(); + move_fun: MoveCaretFun, + modifiers: &Modifiers, + ) -> UIResult<()> { + for caret_tup in self.caret_w_select_vec.iter_mut() { + caret_tup.0 = move_fun(&self.code_lines, caret_tup.0, modifiers)?; + caret_tup.1 = None; + } - for (caret_node_id_ref, leaf_index) in self.caret_nodes.iter() { - let caret_node_id = *caret_node_id_ref; - let next_leaf_id_opt = self.get_next_leaf(*leaf_index); + Ok(()) + } +} - new_caret_nodes.extend(move_carets_right_for_node( - caret_node_id, - *leaf_index, - next_leaf_id_opt, - markup_node_pool, - )?); - } +impl<'a> SelectableLines for EdModel<'a> { + fn get_caret(self) -> TextPos { + self.caret_w_select_vec.first().0.caret_pos + } - self.caret_nodes = new_caret_nodes.into_iter().collect(); - } - VirtualKeyCode::Left => unimplemented!("TODO"), - _ => (), - }; + // keeps active selection + fn set_caret(&mut self, caret_pos: TextPos) { + let caret_tup = self.caret_w_select_vec.first_mut(); + caret_tup.0.caret_pos = caret_pos; + caret_tup.1 = None; + } + + fn move_caret_left(&mut self, modifiers: &Modifiers) -> UIResult<()> { + let move_fun: MoveCaretFun = lines::move_caret_left; + EdModel::move_caret(self, move_fun, modifiers)?; Ok(()) } - pub fn get_next_leaf(&self, index: usize) -> Option { - self.dfs_ordered_leaves.get(index + 1).copied() + fn move_caret_right(&mut self, modifiers: &Modifiers) -> UIResult<()> { + let move_fun: MoveCaretFun = lines::move_caret_right; + EdModel::move_caret(self, move_fun, modifiers)?; + + Ok(()) + } + + fn move_caret_up(&mut self, modifiers: &Modifiers) -> UIResult<()> { + let move_fun: MoveCaretFun = lines::move_caret_up; + EdModel::move_caret(self, move_fun, modifiers)?; + + Ok(()) + } + + fn move_caret_down(&mut self, modifiers: &Modifiers) -> UIResult<()> { + let move_fun: MoveCaretFun = lines::move_caret_down; + EdModel::move_caret(self, move_fun, modifiers)?; + + Ok(()) + } + + fn move_caret_home(&mut self, modifiers: &Modifiers) -> UIResult<()> { + let move_fun: MoveCaretFun = lines::move_caret_home; + EdModel::move_caret(self, move_fun, modifiers)?; + + Ok(()) + } + + fn move_caret_end(&mut self, modifiers: &Modifiers) -> UIResult<()> { + let move_fun: MoveCaretFun = lines::move_caret_end; + EdModel::move_caret(self, move_fun, modifiers)?; + + Ok(()) + } + + fn get_selection(&self) -> Option { + self.caret_w_select_vec.first().0.selection_opt + } + + fn is_selection_active(&self) -> bool { + self.get_selection().is_some() + } + + fn get_selected_str(&self) -> UIResult> { + if let Some(selection) = self.get_selection() { + let start_line_index = selection.start_pos.line; + let start_col = selection.start_pos.column; + let end_line_index = selection.end_pos.line; + let end_col = selection.end_pos.column; + + if start_line_index == end_line_index { + let line_ref = self.code_lines.get_line(start_line_index)?; + + Ok(Some(line_ref[start_col..end_col].to_string())) + } else { + let full_str = String::new(); + + // TODO + Ok(Some(full_str)) + } + } else { + Ok(None) + } + } + + fn set_raw_sel(&mut self, raw_sel: RawSelection) -> UIResult<()> { + self.caret_w_select_vec.first_mut().0.selection_opt = Some(validate_raw_sel(raw_sel)?); + + Ok(()) + } + + fn set_sel_none(&mut self) { + self.caret_w_select_vec.first_mut().0.selection_opt = None; + } + + fn set_caret_w_sel(&mut self, caret_w_sel: CaretWSelect) { + self.caret_w_select_vec.first_mut().0 = caret_w_sel; + } + + fn select_all(&mut self) -> UIResult<()> { + if self.code_lines.nr_of_chars() > 0 { + let last_pos = self.last_text_pos()?; + + self.set_raw_sel(RawSelection { + start_pos: TextPos { line: 0, column: 0 }, + end_pos: last_pos, + })?; + + self.set_caret(last_pos); + } + + Ok(()) + } + + fn last_text_pos(&self) -> UIResult { + let nr_of_lines = self.code_lines.lines.len(); + let last_line_index = nr_of_lines - 1; + let last_line = self.code_lines.get_line(last_line_index)?; + + Ok(TextPos { + line: self.code_lines.lines.len() - 1, + column: last_line.len(), + }) + } + + fn handle_key_down( + &mut self, + modifiers: &Modifiers, + virtual_keycode: VirtualKeyCode, + ) -> UIResult<()> { + match virtual_keycode { + Left => self.move_caret_left(modifiers), + Up => self.move_caret_up(modifiers), + Right => self.move_caret_right(modifiers), + Down => self.move_caret_down(modifiers), + + A => { + if modifiers.ctrl { + self.select_all() + } else { + Ok(()) + } + } + Home => self.move_caret_home(modifiers), + End => self.move_caret_end(modifiers), + _ => Ok(()), + } } } diff --git a/editor/src/editor/mvc/ed_view.rs b/editor/src/editor/mvc/ed_view.rs index 5833b639a1..baaff2d91b 100644 --- a/editor/src/editor/mvc/ed_view.rs +++ b/editor/src/editor/mvc/ed_view.rs @@ -1,9 +1,12 @@ use super::ed_model::EdModel; +use crate::editor::code_lines::CodeLines; use crate::editor::config::Config; use crate::editor::ed_error::EdResult; use crate::editor::render_ast::build_code_graphics; use crate::editor::slow_pool::SlowPool; use crate::graphics::primitives::rect::Rect; +use crate::ui::text::caret_w_select::make_caret_rect; +use crate::ui::text::caret_w_select::CaretWSelect; use crate::ui::ui_error::MissingGlyphDims; use cgmath::Vector2; use snafu::OptionExt; @@ -19,12 +22,62 @@ pub fn model_to_wgpu<'a>( ) -> EdResult<(wgpu_glyph::Section<'a>, Vec)> { let glyph_dim_rect = ed_model.glyph_dim_rect_opt.context(MissingGlyphDims {})?; - build_code_graphics( + let (section, mut rects) = build_code_graphics( markup_node_pool.get(ed_model.markup_root_id), size, txt_coords, config, glyph_dim_rect, markup_node_pool, - ) + )?; + + let mut all_code_string = String::new(); + + for txt in section.text.iter() { + all_code_string.push_str(txt.text); + } + + ed_model.code_lines = CodeLines::from_str(&all_code_string); + + let caret_w_sel_vec = ed_model + .caret_w_select_vec + .iter() + .map(|(caret_w_sel, _)| *caret_w_sel) + .collect(); + + let mut sel_rects = + build_selection_graphics(caret_w_sel_vec, txt_coords, config, glyph_dim_rect)?; + + rects.append(&mut sel_rects); + + Ok((section, rects)) +} + +pub fn build_selection_graphics( + caret_w_select_vec: Vec, + txt_coords: Vector2, + config: &Config, + glyph_dim_rect: Rect, +) -> EdResult> { + let mut rects = Vec::new(); + let char_width = glyph_dim_rect.width; + let char_height = glyph_dim_rect.height; + + for caret_w_sel in caret_w_select_vec { + let caret_row = caret_w_sel.caret_pos.line as f32; + let caret_col = caret_w_sel.caret_pos.column as f32; + + let top_left_x = txt_coords.x + caret_col * char_width; + + let top_left_y = txt_coords.y + caret_row * char_height; + + rects.push(make_caret_rect( + top_left_x, + top_left_y, + &glyph_dim_rect, + &config.ed_theme.ui_theme, + )) + } + + Ok(rects) } diff --git a/editor/src/editor/render_ast.rs b/editor/src/editor/render_ast.rs index a48a2b1b46..f1e26786c7 100644 --- a/editor/src/editor/render_ast.rs +++ b/editor/src/editor/render_ast.rs @@ -1,10 +1,8 @@ -use super::markup::attribute::{Attribute, Attributes}; use super::markup::nodes::{MarkupNode, BLANK_PLACEHOLDER}; use crate::editor::slow_pool::SlowPool; use crate::editor::{ed_error::EdResult, theme::EdTheme, util::map_get}; use crate::graphics::primitives::rect::Rect; use crate::graphics::primitives::text as gr_text; -use crate::ui::text::caret_w_select::make_caret_rect; use cgmath::Vector2; use winit::dpi::PhysicalSize; @@ -67,40 +65,6 @@ fn markup_to_wgpu<'a>( Ok((wgpu_texts, rects)) } -fn draw_attributes( - attributes: &Attributes, - txt_row_col: &(usize, usize), - code_style: &CodeStyle, -) -> Vec { - let char_width = code_style.glyph_dim_rect.width; - - attributes - .all - .iter() - .map(|attr| match attr { - Attribute::Caret { caret } => { - let caret_col = caret.offset_col as f32; - - let top_left_x = code_style.txt_coords.x - + (txt_row_col.1 as f32) * char_width - + caret_col * char_width; - - let top_left_y = code_style.txt_coords.y - + (txt_row_col.0 as f32) * char_width - + char_width * 0.2; - - make_caret_rect( - top_left_x, - top_left_y, - &code_style.glyph_dim_rect, - &code_style.ed_theme.ui_theme, - ) - } - rest => todo!("implement draw_attributes for {:?}", rest), - }) - .collect() -} - // TODO use text_row fn markup_to_wgpu_helper<'a>( markup_node: &'a MarkupNode, @@ -132,7 +96,7 @@ fn markup_to_wgpu_helper<'a>( content, ast_node_id: _, syn_high_style, - attributes, + attributes: _, parent_id_opt: _, } => { let highlight_color = map_get(&code_style.ed_theme.syntax_high_map, &syn_high_style)?; @@ -141,13 +105,12 @@ fn markup_to_wgpu_helper<'a>( .with_color(colors::to_slice(*highlight_color)) .with_scale(code_style.font_size); - rects.extend(draw_attributes(attributes, txt_row_col, code_style)); txt_row_col.1 += content.len(); wgpu_texts.push(glyph_text); } MarkupNode::Blank { ast_node_id: _, - attributes, + attributes: _, syn_high_style, parent_id_opt: _, } => { @@ -171,8 +134,6 @@ fn markup_to_wgpu_helper<'a>( }; rects.push(hole_rect); - rects.extend(draw_attributes(attributes, txt_row_col, code_style)); - txt_row_col.1 += BLANK_PLACEHOLDER.len(); wgpu_texts.push(glyph_text); } diff --git a/editor/src/editor/util.rs b/editor/src/editor/util.rs index 1e7fa31c65..6138fd8442 100644 --- a/editor/src/editor/util.rs +++ b/editor/src/editor/util.rs @@ -1,18 +1,6 @@ -use super::ed_error::{EdResult, KeyNotFound, OutOfBounds}; +use super::ed_error::{EdResult, KeyNotFound}; use snafu::OptionExt; use std::collections::HashMap; -use std::slice::SliceIndex; - -// replace vec methods that return Option with ones that return Result and proper Error -pub fn slice_get(index: usize, slice: &[T]) -> EdResult<&>::Output> { - let elt_ref = slice.get(index).context(OutOfBounds { - index, - collection_name: "Slice", - len: slice.len(), - })?; - - Ok(elt_ref) -} // replace HashMap method that returns Option with one that returns Result and proper Error pub fn map_get<'a, K: ::std::fmt::Debug + std::hash::Hash + std::cmp::Eq, V>( diff --git a/editor/src/ui/mod.rs b/editor/src/ui/mod.rs index e55ec08073..0f4852cdf8 100644 --- a/editor/src/ui/mod.rs +++ b/editor/src/ui/mod.rs @@ -1,4 +1,4 @@ pub mod text; pub mod theme; pub mod ui_error; -mod util; +pub mod util; diff --git a/editor/src/ui/text/big_selectable_text.rs b/editor/src/ui/text/big_text_area.rs similarity index 89% rename from editor/src/ui/text/big_selectable_text.rs rename to editor/src/ui/text/big_text_area.rs index 5b37e06886..22ba0f0da5 100644 --- a/editor/src/ui/text/big_selectable_text.rs +++ b/editor/src/ui/text/big_text_area.rs @@ -4,8 +4,9 @@ use crate::ui::text::{ caret_w_select::CaretWSelect, + lines, lines::{Lines, MutSelectableLines, SelectableLines}, - selection::{validate_raw_sel, validate_selection, RawSelection, Selection}, + selection::{validate_raw_sel, RawSelection, Selection}, text_pos::TextPos, }; use crate::ui::ui_error::{ @@ -19,23 +20,17 @@ use bumpalo::collections::String as BumpString; use bumpalo::Bump; use ropey::Rope; use snafu::ensure; -use std::{ - cmp::{max, min}, - fmt, - fs::File, - io, - path::Path, -}; +use std::{fmt, fs::File, io, path::Path}; use winit::event::{VirtualKeyCode, VirtualKeyCode::*}; -pub struct BigSelectableText { +pub struct BigTextArea { pub caret_w_select: CaretWSelect, text_rope: Rope, pub path_str: String, arena: Bump, } -impl BigSelectableText { +impl BigTextArea { fn check_bounds(&self, char_indx: usize) -> UIResult<()> { ensure!( char_indx <= self.text_rope.len_chars(), @@ -71,17 +66,13 @@ impl BigSelectableText { } } -fn validate_sel_opt(start_pos: TextPos, end_pos: TextPos) -> UIResult> { - Ok(Some(validate_selection(start_pos, end_pos)?)) -} - -impl Lines for BigSelectableText { +impl Lines for BigTextArea { fn get_line(&self, line_nr: usize) -> UIResult<&str> { ensure!( line_nr < self.nr_of_lines(), OutOfBounds { index: line_nr, - collection_name: "BigSelectableText", + collection_name: "BigTextArea", len: self.nr_of_lines(), } ); @@ -121,9 +112,13 @@ impl Lines for BigSelectableText { lines } + + fn last_char(&self, line_nr: usize) -> UIResult> { + Ok(self.get_line(line_nr)?.chars().last()) + } } -impl SelectableLines for BigSelectableText { +impl SelectableLines for BigTextArea { fn get_caret(self) -> TextPos { self.caret_w_select.caret_pos } @@ -133,347 +128,39 @@ impl SelectableLines for BigSelectableText { } fn move_caret_left(&mut self, modifiers: &Modifiers) -> UIResult<()> { - let old_selection_opt = self.get_selection(); - let old_caret_pos = self.caret_w_select.caret_pos; - let old_line_nr = old_caret_pos.line; - let old_col_nr = old_caret_pos.column; - - let shift_pressed = modifiers.shift; - - 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 { - let curr_line_len = self.line_len(old_line_nr - 1)?; - - (old_line_nr - 1, curr_line_len - 1) - } - } else { - (old_line_nr, old_col_nr - 1) - }; - - let new_caret_pos = TextPos { - 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 { - validate_sel_opt(old_selection.start_pos, new_caret_pos)? - } - } else { - validate_sel_opt( - TextPos { - line: line_nr, - column: col_nr, - }, - old_selection.end_pos, - )? - } - } else if !(old_line_nr == line_nr && old_col_nr == col_nr) { - validate_sel_opt( - TextPos { - line: line_nr, - column: col_nr, - }, - TextPos { - line: old_line_nr, - column: old_col_nr, - }, - )? - } else { - None - } - } else { - None - }; - - self.caret_w_select = CaretWSelect::new(new_caret_pos, new_selection_opt); + self.caret_w_select = lines::move_caret_left(self, self.caret_w_select, modifiers)?; Ok(()) } fn move_caret_right(&mut self, modifiers: &Modifiers) -> UIResult<()> { - let old_selection_opt = self.get_selection(); - let old_caret_pos = self.caret_w_select.caret_pos; - let old_line_nr = old_caret_pos.line; - let old_col_nr = old_caret_pos.column; - - let shift_pressed = modifiers.shift; - - 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 { - let curr_line = self.get_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) - } - }; - - let new_caret_pos = TextPos { - 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 { - validate_sel_opt(new_caret_pos, old_selection.end_pos)? - } - } else { - validate_sel_opt( - old_selection.start_pos, - TextPos { - line: line_nr, - column: col_nr, - }, - )? - } - } else if !(old_line_nr == line_nr && old_col_nr == col_nr) { - validate_sel_opt( - TextPos { - line: old_line_nr, - column: old_col_nr, - }, - TextPos { - line: line_nr, - column: col_nr, - }, - )? - } else { - None - } - } else { - None - }; - - self.caret_w_select = CaretWSelect::new(new_caret_pos, new_selection_opt); + self.caret_w_select = lines::move_caret_right(self, self.caret_w_select, modifiers)?; Ok(()) } fn move_caret_up(&mut self, modifiers: &Modifiers) -> UIResult<()> { - let old_selection_opt = self.get_selection(); - let old_caret_pos = self.caret_w_select.caret_pos; - let old_line_nr = old_caret_pos.line; - let old_col_nr = old_caret_pos.column; - - let shift_pressed = modifiers.shift; - - 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 { - let prev_line_len = self.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) - } - }; - - let new_caret_pos = TextPos { - 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 { - validate_sel_opt( - min(old_selection.start_pos, new_caret_pos), - max(old_selection.start_pos, new_caret_pos), - )? - } - } else { - validate_sel_opt(new_caret_pos, old_selection.end_pos)? - } - } else if !(old_line_nr == line_nr && old_col_nr == col_nr) { - validate_sel_opt( - min(old_caret_pos, new_caret_pos), - max(old_caret_pos, new_caret_pos), - )? - } else { - None - } - } else { - None - }; - - self.caret_w_select = CaretWSelect::new(new_caret_pos, new_selection_opt); + self.caret_w_select = lines::move_caret_up(self, self.caret_w_select, modifiers)?; Ok(()) } fn move_caret_down(&mut self, modifiers: &Modifiers) -> UIResult<()> { - let old_selection_opt = self.get_selection(); - let old_caret_pos = self.caret_w_select.caret_pos; - let old_line_nr = old_caret_pos.line; - let old_col_nr = old_caret_pos.column; - - let shift_pressed = modifiers.shift; - - 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 >= self.nr_of_lines() { - let curr_line_len = self.line_len(old_line_nr)?; - - (old_line_nr, curr_line_len) - } else { - let next_line = self.get_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) - } - }; - - let new_caret_pos = TextPos { - 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 { - validate_sel_opt( - min(old_selection.end_pos, new_caret_pos), - max(old_selection.end_pos, new_caret_pos), - )? - } - } else { - validate_sel_opt(old_selection.start_pos, new_caret_pos)? - } - } else if !(old_line_nr == line_nr && old_col_nr == col_nr) { - validate_sel_opt( - min(old_caret_pos, new_caret_pos), - max(old_caret_pos, new_caret_pos), - )? - } else { - None - } - } else { - None - }; - - self.caret_w_select = CaretWSelect::new(new_caret_pos, new_selection_opt); + self.caret_w_select = lines::move_caret_down(self, self.caret_w_select, modifiers)?; Ok(()) } fn move_caret_home(&mut self, modifiers: &Modifiers) -> UIResult<()> { - let curr_line_nr = self.caret_w_select.caret_pos.line; - let old_col_nr = self.caret_w_select.caret_pos.column; + self.caret_w_select = lines::move_caret_home(self, self.caret_w_select, modifiers)?; - let curr_line_str = self.get_line(curr_line_nr)?; - let line_char_iter = curr_line_str.chars(); - - let mut first_no_space_char_col = 0; - let mut non_space_found = false; - - for c in line_char_iter { - if !c.is_whitespace() { - non_space_found = true; - break; - } else { - first_no_space_char_col += 1; - } - } - - if !non_space_found { - first_no_space_char_col = 0; - } - - let new_col_nr = if first_no_space_char_col == old_col_nr { - 0 - } else { - first_no_space_char_col - }; - - self.caret_w_select.move_caret_w_mods( - TextPos { - line: curr_line_nr, - column: new_col_nr, - }, - modifiers, - ) + Ok(()) } fn move_caret_end(&mut self, modifiers: &Modifiers) -> UIResult<()> { - let curr_line_nr = self.caret_w_select.caret_pos.line; - let curr_line_len = self.line_len(curr_line_nr)?; + self.caret_w_select = lines::move_caret_end(self, self.caret_w_select, modifiers)?; - let new_col = if let Some(last_char) = self.last_char(curr_line_nr)? { - if is_newline(&last_char) { - curr_line_len - 1 - } else { - curr_line_len - } - } else { - 0 - }; - - let new_pos = TextPos { - line: curr_line_nr, - column: new_col, - }; - - self.caret_w_select.move_caret_w_mods(new_pos, modifiers) + Ok(()) } fn get_selection(&self) -> Option { @@ -484,7 +171,7 @@ impl SelectableLines for BigSelectableText { self.get_selection().is_some() } - fn get_selected_str(&self) -> UIResult> { + fn get_selected_str(&self) -> UIResult> { if let Some(val_sel) = self.caret_w_select.selection_opt { let (start_char_indx, end_char_indx) = self.sel_to_tup(val_sel); @@ -493,12 +180,9 @@ impl SelectableLines for BigSelectableText { let rope_slice = self.text_rope.slice(start_char_indx..end_char_indx); if let Some(line_str_ref) = rope_slice.as_str() { - Ok(Some(line_str_ref)) + Ok(Some(line_str_ref.to_string())) } else { - // happens very rarely - let line_str = rope_slice.chunks().collect::(); - let arena_str_ref = self.arena.alloc(line_str); - Ok(Some(arena_str_ref)) + Ok(Some(rope_slice.chunks().collect::())) } } else { Ok(None) @@ -511,13 +195,17 @@ impl SelectableLines for BigSelectableText { Ok(()) } + fn set_caret_w_sel(&mut self, caret_w_sel: CaretWSelect) { + self.caret_w_select = caret_w_sel; + } + fn set_sel_none(&mut self) { self.caret_w_select.selection_opt = None; } fn select_all(&mut self) -> UIResult<()> { if self.nr_of_chars() > 0 { - let last_pos = self.last_text_pos(); + let last_pos = self.last_text_pos()?; self.set_raw_sel(RawSelection { start_pos: TextPos { line: 0, column: 0 }, @@ -530,12 +218,8 @@ impl SelectableLines for BigSelectableText { Ok(()) } - fn last_text_pos(&self) -> TextPos { - self.char_indx_to_pos(self.nr_of_chars()) - } - - fn last_char(&self, line_nr: usize) -> UIResult> { - Ok(self.get_line(line_nr)?.chars().last()) + fn last_text_pos(&self) -> UIResult { + Ok(self.char_indx_to_pos(self.nr_of_chars())) } fn handle_key_down( @@ -563,7 +247,7 @@ impl SelectableLines for BigSelectableText { } } -impl MutSelectableLines for BigSelectableText { +impl MutSelectableLines for BigTextArea { fn insert_char(&mut self, new_char: &char) -> UIResult<()> { if self.is_selection_active() { self.del_selection()?; @@ -658,7 +342,7 @@ impl MutSelectableLines for BigSelectableText { } } -impl Default for BigSelectableText { +impl Default for BigTextArea { fn default() -> Self { let caret_w_select = CaretWSelect::default(); let text_rope = Rope::from_str(""); @@ -674,11 +358,11 @@ impl Default for BigSelectableText { } } -pub fn from_path(path: &Path) -> UIResult { +pub fn from_path(path: &Path) -> UIResult { let text_rope = rope_from_path(path)?; let path_str = path_to_string(path); - Ok(BigSelectableText { + Ok(BigTextArea { text_rope, path_str, ..Default::default() @@ -687,10 +371,10 @@ pub fn from_path(path: &Path) -> UIResult { #[allow(dead_code)] // used by tests but will also be used in the future -pub fn from_str(text: &str) -> BigSelectableText { +pub fn from_str(text: &str) -> BigTextArea { let text_rope = Rope::from_str(text); - BigSelectableText { + BigTextArea { text_rope, ..Default::default() } @@ -723,9 +407,9 @@ fn rope_from_path(path: &Path) -> UIResult { } // need to explicitly omit arena -impl fmt::Debug for BigSelectableText { +impl fmt::Debug for BigTextArea { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("BigSelectableText") + f.debug_struct("BigTextArea") .field("caret_w_select", &self.caret_w_select) .field("text_rope", &self.text_rope) .field("path_str", &self.path_str) @@ -736,8 +420,8 @@ impl fmt::Debug for BigSelectableText { #[cfg(test)] pub mod test_big_sel_text { use crate::ui::text::{ - big_selectable_text::from_str, - big_selectable_text::BigSelectableText, + big_text_area::from_str, + big_text_area::BigTextArea, caret_w_select::CaretWSelect, lines::{Lines, MutSelectableLines, SelectableLines}, selection::validate_selection, @@ -843,7 +527,7 @@ pub mod test_big_sel_text { Ok(elt_ref) } - pub fn big_text_from_dsl_str(lines: &[String]) -> BigSelectableText { + pub fn big_text_from_dsl_str(lines: &[String]) -> BigTextArea { from_str( &lines .iter() @@ -853,7 +537,7 @@ pub mod test_big_sel_text { ) } - pub fn all_lines_vec(big_sel_text: &BigSelectableText) -> Vec { + pub fn all_lines_vec(big_sel_text: &BigTextArea) -> Vec { let mut lines: Vec = Vec::new(); for i in 0..big_sel_text.nr_of_lines() { @@ -963,7 +647,7 @@ pub mod test_big_sel_text { } } - pub fn gen_big_text(lines: &[&str]) -> Result { + pub fn gen_big_text(lines: &[&str]) -> Result { let lines_string_slice: Vec = lines.iter().map(|l| l.to_string()).collect(); let mut big_text = big_text_from_dsl_str(&lines_string_slice); let caret_w_select = convert_dsl_to_selection(&lines_string_slice).unwrap(); @@ -1141,7 +825,7 @@ pub mod test_big_sel_text { Ok(()) } - type MoveCaretFun = fn(&mut BigSelectableText, &Modifiers) -> UIResult<()>; + type MoveCaretFun = fn(&mut BigTextArea, &Modifiers) -> UIResult<()>; // Convert nice string representations and compare results fn assert_move( @@ -1720,7 +1404,7 @@ pub mod test_big_sel_text { #[test] fn move_home() -> Result<(), String> { - let move_caret_home = BigSelectableText::move_caret_home; + let move_caret_home = BigTextArea::move_caret_home; assert_move(&["|"], &["|"], &no_mods(), move_caret_home)?; assert_move(&["a|"], &["|a"], &no_mods(), move_caret_home)?; assert_move(&["|a"], &["|a"], &no_mods(), move_caret_home)?; @@ -1834,7 +1518,7 @@ pub mod test_big_sel_text { #[test] fn move_end() -> Result<(), String> { - let move_caret_end = BigSelectableText::move_caret_end; + let move_caret_end = BigTextArea::move_caret_end; assert_move(&["|"], &["|"], &no_mods(), move_caret_end)?; assert_move(&["|a"], &["a|"], &no_mods(), move_caret_end)?; assert_move(&["a|"], &["a|"], &no_mods(), move_caret_end)?; @@ -2467,7 +2151,7 @@ pub mod test_big_sel_text { #[test] fn start_selection_home() -> Result<(), String> { - let move_caret_home = BigSelectableText::move_caret_home; + let move_caret_home = BigTextArea::move_caret_home; assert_move(&["|"], &["|"], &shift_pressed(), move_caret_home)?; assert_move(&["|a"], &["|a"], &shift_pressed(), move_caret_home)?; assert_move(&["a|"], &["|[a]"], &shift_pressed(), move_caret_home)?; @@ -2587,7 +2271,7 @@ pub mod test_big_sel_text { #[test] fn start_selection_end() -> Result<(), String> { - let move_caret_end = BigSelectableText::move_caret_end; + let move_caret_end = BigTextArea::move_caret_end; assert_move(&["|"], &["|"], &shift_pressed(), move_caret_end)?; assert_move(&["|a"], &["[a]|"], &shift_pressed(), move_caret_end)?; assert_move(&["a|"], &["a|"], &shift_pressed(), move_caret_end)?; @@ -3253,7 +2937,7 @@ pub mod test_big_sel_text { #[test] fn end_selection_home() -> Result<(), String> { - let move_caret_home = BigSelectableText::move_caret_home; + let move_caret_home = BigTextArea::move_caret_home; assert_move(&["[a]|"], &["|a"], &no_mods(), move_caret_home)?; assert_move(&["|[a]"], &["|a"], &no_mods(), move_caret_home)?; assert_move(&[" |[a]"], &["| a"], &no_mods(), move_caret_home)?; @@ -3318,7 +3002,7 @@ pub mod test_big_sel_text { #[test] fn end_selection_end() -> Result<(), String> { - let move_caret_end = BigSelectableText::move_caret_end; + let move_caret_end = BigTextArea::move_caret_end; assert_move(&["|[a]"], &["a|"], &no_mods(), move_caret_end)?; assert_move(&["[a]|"], &["a|"], &no_mods(), move_caret_end)?; assert_move(&[" a|[ ]"], &[" a |"], &no_mods(), move_caret_end)?; diff --git a/editor/src/ui/text/caret_w_select.rs b/editor/src/ui/text/caret_w_select.rs index 1a08830507..d9bbf15892 100644 --- a/editor/src/ui/text/caret_w_select.rs +++ b/editor/src/ui/text/caret_w_select.rs @@ -37,7 +37,7 @@ impl CaretWSelect { } } - pub fn move_caret_w_mods(&mut self, new_pos: TextPos, mods: &Modifiers) -> UIResult<()> { + pub fn move_caret_w_mods(&self, new_pos: TextPos, mods: &Modifiers) -> UIResult { let old_caret_pos = self.caret_pos; // one does not simply move the caret @@ -75,10 +75,7 @@ impl CaretWSelect { None }; - self.caret_pos = new_pos; - self.selection_opt = valid_sel_opt; - - Ok(()) + Ok(CaretWSelect::new(new_pos, valid_sel_opt)) } } diff --git a/editor/src/ui/text/lines.rs b/editor/src/ui/text/lines.rs index 25641e7c20..0ec1389667 100644 --- a/editor/src/ui/text/lines.rs +++ b/editor/src/ui/text/lines.rs @@ -1,13 +1,18 @@ // Adapted from https://github.com/cessen/ropey by Nathan Vegdahl, licensed under the MIT license +use crate::ui::text::caret_w_select::CaretWSelect; +use crate::ui::text::selection::validate_sel_opt; use crate::ui::text::{ selection::{RawSelection, Selection}, text_pos::TextPos, }; use crate::ui::ui_error::UIResult; +use crate::ui::util::is_newline; use crate::window::keyboard_input::Modifiers; use bumpalo::collections::String as BumpString; use bumpalo::Bump; +use std::cmp::max; +use std::cmp::min; use winit::event::VirtualKeyCode; pub trait Lines { @@ -19,8 +24,9 @@ pub trait Lines { fn nr_of_chars(&self) -> usize; - // TODO use pool allocation here fn all_lines<'a>(&self, arena: &'a Bump) -> BumpString<'a>; + + fn last_char(&self, line_nr: usize) -> UIResult>; } pub trait SelectableLines { @@ -44,17 +50,17 @@ pub trait SelectableLines { fn is_selection_active(&self) -> bool; - fn get_selected_str(&self) -> UIResult>; + fn get_selected_str(&self) -> UIResult>; fn set_raw_sel(&mut self, raw_sel: RawSelection) -> UIResult<()>; fn set_sel_none(&mut self); + fn set_caret_w_sel(&mut self, caret_w_sel: CaretWSelect); + fn select_all(&mut self) -> UIResult<()>; - fn last_text_pos(&self) -> TextPos; - - fn last_char(&self, line_nr: usize) -> UIResult>; + fn last_text_pos(&self) -> UIResult; fn handle_key_down( &mut self, @@ -75,3 +81,362 @@ pub trait MutSelectableLines { fn del_selection(&mut self) -> UIResult<()>; } + +// T: Lines +pub type MoveCaretFun = fn(&T, CaretWSelect, &Modifiers) -> UIResult; + +pub fn move_caret_left( + lines: &T, + caret_w_select: CaretWSelect, + modifiers: &Modifiers, +) -> UIResult { + let old_selection_opt = caret_w_select.selection_opt; + let old_caret_pos = caret_w_select.caret_pos; + let old_line_nr = old_caret_pos.line; + let old_col_nr = old_caret_pos.column; + + let shift_pressed = modifiers.shift; + + 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 { + let curr_line_len = lines.line_len(old_line_nr - 1)?; + + (old_line_nr - 1, curr_line_len - 1) + } + } else { + (old_line_nr, old_col_nr - 1) + }; + + let new_caret_pos = TextPos { + 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 { + validate_sel_opt(old_selection.start_pos, new_caret_pos)? + } + } else { + validate_sel_opt( + TextPos { + line: line_nr, + column: col_nr, + }, + old_selection.end_pos, + )? + } + } else if !(old_line_nr == line_nr && old_col_nr == col_nr) { + validate_sel_opt( + TextPos { + line: line_nr, + column: col_nr, + }, + TextPos { + line: old_line_nr, + column: old_col_nr, + }, + )? + } else { + None + } + } else { + None + }; + + Ok(CaretWSelect::new(new_caret_pos, new_selection_opt)) +} + +pub fn move_caret_right( + lines: &T, + caret_w_select: CaretWSelect, + modifiers: &Modifiers, +) -> UIResult { + let old_selection_opt = caret_w_select.selection_opt; + let old_caret_pos = caret_w_select.caret_pos; + let old_line_nr = old_caret_pos.line; + let old_col_nr = old_caret_pos.column; + + let shift_pressed = modifiers.shift; + + 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 { + let curr_line = lines.get_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) + } + }; + + let new_caret_pos = TextPos { + 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 { + validate_sel_opt(new_caret_pos, old_selection.end_pos)? + } + } else { + validate_sel_opt( + old_selection.start_pos, + TextPos { + line: line_nr, + column: col_nr, + }, + )? + } + } else if !(old_line_nr == line_nr && old_col_nr == col_nr) { + validate_sel_opt( + TextPos { + line: old_line_nr, + column: old_col_nr, + }, + TextPos { + line: line_nr, + column: col_nr, + }, + )? + } else { + None + } + } else { + None + }; + + Ok(CaretWSelect::new(new_caret_pos, new_selection_opt)) +} + +pub fn move_caret_up( + lines: &T, + caret_w_select: CaretWSelect, + modifiers: &Modifiers, +) -> UIResult { + let old_selection_opt = caret_w_select.selection_opt; + let old_caret_pos = caret_w_select.caret_pos; + let old_line_nr = old_caret_pos.line; + let old_col_nr = old_caret_pos.column; + + let shift_pressed = modifiers.shift; + + 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 { + let prev_line_len = lines.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) + } + }; + + let new_caret_pos = TextPos { + 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 { + validate_sel_opt( + min(old_selection.start_pos, new_caret_pos), + max(old_selection.start_pos, new_caret_pos), + )? + } + } else { + validate_sel_opt(new_caret_pos, old_selection.end_pos)? + } + } else if !(old_line_nr == line_nr && old_col_nr == col_nr) { + validate_sel_opt( + min(old_caret_pos, new_caret_pos), + max(old_caret_pos, new_caret_pos), + )? + } else { + None + } + } else { + None + }; + + Ok(CaretWSelect::new(new_caret_pos, new_selection_opt)) +} + +pub fn move_caret_down( + lines: &T, + caret_w_select: CaretWSelect, + modifiers: &Modifiers, +) -> UIResult { + let old_selection_opt = caret_w_select.selection_opt; + let old_caret_pos = caret_w_select.caret_pos; + let old_line_nr = old_caret_pos.line; + let old_col_nr = old_caret_pos.column; + + let shift_pressed = modifiers.shift; + + 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.nr_of_lines() { + let curr_line_len = lines.line_len(old_line_nr)?; + + (old_line_nr, curr_line_len) + } else { + let next_line = lines.get_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) + } + }; + + let new_caret_pos = TextPos { + 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 { + validate_sel_opt( + min(old_selection.end_pos, new_caret_pos), + max(old_selection.end_pos, new_caret_pos), + )? + } + } else { + validate_sel_opt(old_selection.start_pos, new_caret_pos)? + } + } else if !(old_line_nr == line_nr && old_col_nr == col_nr) { + validate_sel_opt( + min(old_caret_pos, new_caret_pos), + max(old_caret_pos, new_caret_pos), + )? + } else { + None + } + } else { + None + }; + + Ok(CaretWSelect::new(new_caret_pos, new_selection_opt)) +} + +pub fn move_caret_home( + lines: &T, + caret_w_select: CaretWSelect, + modifiers: &Modifiers, +) -> UIResult { + let curr_line_nr = caret_w_select.caret_pos.line; + let old_col_nr = caret_w_select.caret_pos.column; + + let curr_line_str = lines.get_line(curr_line_nr)?; + let line_char_iter = curr_line_str.chars(); + + let mut first_no_space_char_col = 0; + let mut non_space_found = false; + + for c in line_char_iter { + if !c.is_whitespace() { + non_space_found = true; + break; + } else { + first_no_space_char_col += 1; + } + } + + if !non_space_found { + first_no_space_char_col = 0; + } + + let new_col_nr = if first_no_space_char_col == old_col_nr { + 0 + } else { + first_no_space_char_col + }; + + caret_w_select.move_caret_w_mods( + TextPos { + line: curr_line_nr, + column: new_col_nr, + }, + modifiers, + ) +} + +pub fn move_caret_end( + lines: &T, + caret_w_select: CaretWSelect, + modifiers: &Modifiers, +) -> UIResult { + let curr_line_nr = caret_w_select.caret_pos.line; + let curr_line_len = lines.line_len(curr_line_nr)?; + + let new_col = if let Some(last_char) = lines.last_char(curr_line_nr)? { + if is_newline(&last_char) { + curr_line_len - 1 + } else { + curr_line_len + } + } else { + 0 + }; + + let new_pos = TextPos { + line: curr_line_nr, + column: new_col, + }; + + caret_w_select.move_caret_w_mods(new_pos, modifiers) +} diff --git a/editor/src/ui/text/mod.rs b/editor/src/ui/text/mod.rs index 5dad412667..39616a47cd 100644 --- a/editor/src/ui/text/mod.rs +++ b/editor/src/ui/text/mod.rs @@ -1,4 +1,4 @@ -pub mod big_selectable_text; +pub mod big_text_area; pub mod caret_w_select; pub mod lines; pub mod selection; diff --git a/editor/src/ui/text/selection.rs b/editor/src/ui/text/selection.rs index 20a95f422e..c86fcb84fa 100644 --- a/editor/src/ui/text/selection.rs +++ b/editor/src/ui/text/selection.rs @@ -34,6 +34,10 @@ pub fn validate_raw_sel(raw_sel: RawSelection) -> UIResult { validate_selection(raw_sel.start_pos, raw_sel.end_pos) } +pub fn validate_sel_opt(start_pos: TextPos, end_pos: TextPos) -> UIResult> { + Ok(Some(validate_selection(start_pos, end_pos)?)) +} + pub fn validate_selection(start_pos: TextPos, end_pos: TextPos) -> UIResult { ensure!( start_pos.line <= end_pos.line, diff --git a/editor/src/ui/util.rs b/editor/src/ui/util.rs index 8fba081a08..cf0636112d 100644 --- a/editor/src/ui/util.rs +++ b/editor/src/ui/util.rs @@ -1,5 +1,20 @@ +use super::ui_error::{OutOfBounds, UIResult}; +use snafu::OptionExt; +use std::slice::SliceIndex; + pub fn is_newline(char_ref: &char) -> bool { let newline_codes = vec!['\u{d}', '\n']; newline_codes.contains(char_ref) } + +// replace vec method that return Option with one that return Result and proper Error +pub fn slice_get(index: usize, slice: &[T]) -> UIResult<&>::Output> { + let elt_ref = slice.get(index).context(OutOfBounds { + index, + collection_name: "Slice", + len: slice.len(), + })?; + + Ok(elt_ref) +}