From 0623a72599f5ffe7adfd0ebe5445ad5eaa26ff91 Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Sun, 24 Apr 2022 16:01:45 -0400 Subject: [PATCH 01/31] move config parsing back into main --- helix-term/src/application.rs | 21 +-------------------- helix-term/src/main.rs | 25 +++++++++++++++++++++++-- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 0c8de2ab..e8fbb7cc 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -56,29 +56,10 @@ pub struct Application { } impl Application { - pub fn new(args: Args) -> Result { + pub fn new(args: Args, config: Config) -> Result { use helix_view::editor::Action; let config_dir = helix_loader::config_dir(); - if !config_dir.exists() { - std::fs::create_dir_all(&config_dir).ok(); - } - - let config = match std::fs::read_to_string(config_dir.join("config.toml")) { - Ok(config) => toml::from_str(&config) - .map(crate::keymap::merge_keys) - .unwrap_or_else(|err| { - eprintln!("Bad config: {}", err); - eprintln!("Press to continue with default config"); - use std::io::Read; - // This waits for an enter press. - let _ = std::io::stdin().read(&mut []); - Config::default() - }), - Err(err) if err.kind() == std::io::ErrorKind::NotFound => Config::default(), - Err(err) => return Err(Error::new(err)), - }; - let theme_loader = std::sync::Arc::new(theme::Loader::new( &config_dir, &helix_loader::runtime_dir(), diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs index 58a90131..cd0b364b 100644 --- a/helix-term/src/main.rs +++ b/helix-term/src/main.rs @@ -1,6 +1,7 @@ -use anyhow::{Context, Result}; +use anyhow::{Context, Error, Result}; use helix_term::application::Application; use helix_term::args::Args; +use helix_term::config::Config; use std::path::PathBuf; fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> { @@ -110,8 +111,28 @@ FLAGS: setup_logging(logpath, args.verbosity).context("failed to initialize logging")?; + let config_dir = helix_loader::config_dir(); + if !config_dir.exists() { + std::fs::create_dir_all(&config_dir).ok(); + } + + let config = match std::fs::read_to_string(config_dir.join("config.toml")) { + Ok(config) => toml::from_str(&config) + .map(helix_term::keymap::merge_keys) + .unwrap_or_else(|err| { + eprintln!("Bad config: {}", err); + eprintln!("Press to continue with default config"); + use std::io::Read; + // This waits for an enter press. + let _ = std::io::stdin().read(&mut []); + Config::default() + }), + Err(err) if err.kind() == std::io::ErrorKind::NotFound => Config::default(), + Err(err) => return Err(Error::new(err)), + }; + // TODO: use the thread local executor to spawn the application task separately from the work pool - let mut app = Application::new(args).context("unable to create new application")?; + let mut app = Application::new(args, config).context("unable to create new application")?; let exit_code = app.run().await?; From adb6cd537628308a23fe6ea86f1c7b419c4d8c68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Tue, 26 Oct 2021 18:02:36 +0900 Subject: [PATCH 02/31] Simplify handle_terminal_events signature --- helix-term/src/application.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index e8fbb7cc..075b9c35 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -213,7 +213,7 @@ impl Application { tokio::select! { biased; - event = reader.next() => { + Some(event) = reader.next() => { self.handle_terminal_events(event) } Some(signal) = self.signals.next() => { @@ -351,7 +351,7 @@ impl Application { } } - pub fn handle_terminal_events(&mut self, event: Option>) { + pub fn handle_terminal_events(&mut self, event: Result) { let mut cx = crate::compositor::Context { editor: &mut self.editor, jobs: &mut self.jobs, @@ -359,15 +359,14 @@ impl Application { }; // Handle key events let should_redraw = match event { - Some(Ok(Event::Resize(width, height))) => { + Ok(Event::Resize(width, height)) => { self.compositor.resize(width, height); self.compositor .handle_event(Event::Resize(width, height), &mut cx) } - Some(Ok(event)) => self.compositor.handle_event(event, &mut cx), - Some(Err(x)) => panic!("{}", x), - None => panic!(), + Ok(event) => self.compositor.handle_event(event, &mut cx), + Err(x) => panic!("{}", x), }; if should_redraw && !self.editor.should_close() { From 308cab3e5cd5e8d5a8c37498e725f51ab101a908 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Tue, 26 Oct 2021 18:03:03 +0900 Subject: [PATCH 03/31] Integration testing harness --- helix-term/src/application.rs | 4 ++-- helix-term/tests/integration.rs | 24 ++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 helix-term/tests/integration.rs diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 075b9c35..09b7836f 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -41,7 +41,7 @@ type Signals = futures_util::stream::Empty<()>; pub struct Application { compositor: Compositor, - editor: Editor, + pub editor: Editor, config: Arc>, @@ -193,7 +193,7 @@ impl Application { scroll: None, }; - self.compositor.render(&mut cx); + // self.compositor.render(&mut cx); } pub async fn event_loop(&mut self) { diff --git a/helix-term/tests/integration.rs b/helix-term/tests/integration.rs new file mode 100644 index 00000000..1ef618f7 --- /dev/null +++ b/helix-term/tests/integration.rs @@ -0,0 +1,24 @@ +use helix_term::{application::Application, args::Args, config::Config}; +use helix_view::current; + +use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers}; + +#[tokio::test] +async fn it_works() { + let args = Args::default(); + let config = Config::default(); + let mut app = Application::new(args, config).unwrap(); + + let inputs = &['i', 'h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd']; + + for input in inputs { + // TODO: use input.parse:: + app.handle_terminal_events(Ok(Event::Key(KeyEvent { + code: KeyCode::Char(*input), + modifiers: KeyModifiers::NONE, + }))); + } + + let (_, doc) = current!(app.editor); + assert_eq!(doc.text(), "hello world\n"); +} From 502d3290fb88d8a871b0824adc7987a98104933d Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Mon, 17 Jan 2022 19:04:40 -0500 Subject: [PATCH 04/31] improve test harness * Use new macro syntax for encoding sequences of keys * Make convenience helpers for common test pattern * Use indoc for inline indented raw strings * Add feature flag for integration testing to disable rendering --- Cargo.lock | 285 +++++++++++++++++++++----------- helix-core/Cargo.toml | 1 + helix-loader/src/lib.rs | 10 +- helix-term/Cargo.toml | 5 + helix-term/src/application.rs | 20 ++- helix-term/tests/integration.rs | 163 ++++++++++++++++-- helix-view/src/editor.rs | 1 + 7 files changed, 359 insertions(+), 126 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ddf26790..39afd141 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,9 +13,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.57" +version = "1.0.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc" +checksum = "94a45b455c14666b85fc40a019e8ab9eb75e3a124e05494f5397122bc9eb06e0" [[package]] name = "arc-swap" @@ -25,9 +25,9 @@ checksum = "c5d78ce20460b82d3fa150275ed9d55e21064fc7951177baacf86a145c4a4b1f" [[package]] name = "autocfg" -version = "1.1.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" [[package]] name = "bitflags" @@ -66,9 +66,9 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" [[package]] name = "cc" -version = "1.0.73" +version = "1.0.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" +checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee" [[package]] name = "cfg-if" @@ -121,9 +121,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.8" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38" +checksum = "b5e5bed1f1c269533fa816a0a5492b3545209a205ca1a54842be180eb63a16a6" dependencies = [ "cfg-if", "lazy_static", @@ -131,15 +131,15 @@ dependencies = [ [[package]] name = "crossterm" -version = "0.23.2" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2102ea4f781910f8a5b98dd061f4c2023f479ce7bb1236330099ceb5a93cf17" +checksum = "77b75a27dc8d220f1f8521ea69cd55a34d720a200ebb3a624d9aa19193d3b432" dependencies = [ "bitflags", "crossterm_winapi", "futures-core", "libc", - "mio", + "mio 0.7.14", "parking_lot", "signal-hook", "signal-hook-mio", @@ -184,9 +184,9 @@ checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" [[package]] name = "encoding_rs" -version = "0.8.31" +version = "0.8.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b" +checksum = "7896dc8abb250ffdda33912550faa54c88ec8b998dec0b2c55ab224921ce11df" dependencies = [ "cfg-if", ] @@ -293,9 +293,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.6" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad" +checksum = "418d37c8b1d42553c93648be529cb70f920d3baf8ef469b74b9638df426e0b4c" dependencies = [ "cfg-if", "libc", @@ -452,6 +452,7 @@ dependencies = [ "helix-tui", "helix-view", "ignore", + "indoc", "log", "once_cell", "pulldown-cmark", @@ -460,6 +461,7 @@ dependencies = [ "serde_json", "signal-hook", "signal-hook-tokio", + "smallvec", "tokio", "tokio-stream", "toml", @@ -544,11 +546,20 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "indoc" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a75aeaaef0ce18b58056d306c27b07436fbb34b8816c53094b76dd81803136" +dependencies = [ + "unindent", +] + [[package]] name = "itoa" -version = "1.0.2" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" +checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" [[package]] name = "lazy_static" @@ -558,9 +569,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.126" +version = "0.2.125" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" +checksum = "5916d2ae698f6de9bfb891ad7a8d65c09d232dc58cc4ac433c7da3b2fd84bc2b" [[package]] name = "libloading" @@ -574,19 +585,18 @@ dependencies = [ [[package]] name = "lock_api" -version = "0.4.7" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" +checksum = "88943dd7ef4a2e5a4bfa2753aaab3013e34ce2533d1996fb18ef591e315e2b3b" dependencies = [ - "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.17" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" dependencies = [ "cfg-if", ] @@ -612,9 +622,9 @@ checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" [[package]] name = "memchr" -version = "2.5.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" [[package]] name = "memmap2" @@ -625,6 +635,19 @@ dependencies = [ "libc", ] +[[package]] +name = "mio" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc" +dependencies = [ + "libc", + "log", + "miow", + "ntapi", + "winapi", +] + [[package]] name = "mio" version = "0.8.3" @@ -634,14 +657,32 @@ dependencies = [ "libc", "log", "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys", + "windows-sys 0.36.1", +] + +[[package]] +name = "miow" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" +dependencies = [ + "winapi", +] + +[[package]] +name = "ntapi" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" +dependencies = [ + "winapi", ] [[package]] name = "num-integer" -version = "0.1.45" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" dependencies = [ "autocfg", "num-traits", @@ -649,9 +690,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.15" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" dependencies = [ "autocfg", ] @@ -684,15 +725,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.3" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929" +checksum = "28141e0cc4143da2443301914478dc976a61ffdb3f043058310c70df2fed8954" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-sys", + "windows-sys 0.32.0", ] [[package]] @@ -703,9 +744,9 @@ checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" [[package]] name = "pin-project-lite" -version = "0.2.9" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" +checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c" [[package]] name = "pin-utils" @@ -715,11 +756,11 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "proc-macro2" -version = "1.0.39" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f" +checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" dependencies = [ - "unicode-ident", + "unicode-xid", ] [[package]] @@ -744,18 +785,18 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.18" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" +checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145" dependencies = [ "proc-macro2", ] [[package]] name = "rand" -version = "0.8.5" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8" dependencies = [ "rand_core", ] @@ -771,29 +812,28 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.2.13" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" +checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" dependencies = [ "bitflags", ] [[package]] name = "redox_users" -version = "0.4.3" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" dependencies = [ "getrandom", "redox_syscall", - "thiserror", ] [[package]] name = "regex" -version = "1.5.6" +version = "1.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1" +checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" dependencies = [ "aho-corasick", "memchr", @@ -808,15 +848,15 @@ checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" [[package]] name = "regex-syntax" -version = "0.6.26" +version = "0.6.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" [[package]] name = "retain_mut" -version = "0.1.9" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4389f1d5789befaf6029ebd9f7dac4af7f7e3d61b69d4f30e2ac02b57e7712b0" +checksum = "8c31b5c4033f8fdde8700e4657be2c497e7288f01515be52168c631e2e4d4086" [[package]] name = "ropey" @@ -830,9 +870,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.10" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695" +checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" [[package]] name = "same-file" @@ -851,18 +891,18 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "serde" -version = "1.0.137" +version = "1.0.136" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1" +checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.137" +version = "1.0.136" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be" +checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" dependencies = [ "proc-macro2", "quote", @@ -871,9 +911,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.81" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c" +checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95" dependencies = [ "itoa", "ryu", @@ -882,9 +922,9 @@ dependencies = [ [[package]] name = "serde_repr" -version = "0.1.8" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2ad84e47328a31223de7fed7a4f5087f2d6ddfe586cf3ca25b7a165bc0a5aed" +checksum = "98d0516900518c29efa217c298fa1f4e6c6ffc85ae29fd7f4ee48f176e1a9ed5" dependencies = [ "proc-macro2", "quote", @@ -903,12 +943,12 @@ dependencies = [ [[package]] name = "signal-hook-mio" -version = "0.2.3" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +checksum = "29fd5867f1c4f2c5be079aee7a2adf1152ebb04a4bc4d341f504b7dece607ed4" dependencies = [ "libc", - "mio", + "mio 0.7.14", "signal-hook", ] @@ -941,9 +981,9 @@ checksum = "2e24979f63a11545f5f2c60141afe249d4f19f84581ea2138065e400941d83d3" [[package]] name = "slab" -version = "0.4.6" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32" +checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5" [[package]] name = "slotmap" @@ -995,9 +1035,9 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "str-buf" -version = "1.0.6" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0" +checksum = "d44a3643b4ff9caf57abcee9c2c621d6c03d9135e0d8b589bd9afb5992cb176a" [[package]] name = "str_indices" @@ -1007,13 +1047,13 @@ checksum = "9d9199fa80c817e074620be84374a520062ebac833f358d74b37060ce4a0f2c0" [[package]] name = "syn" -version = "1.0.95" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbaf6116ab8924f39d52792136fb74fd60a80194cf1b1c6ffa6453eef1c3f942" +checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b" dependencies = [ "proc-macro2", "quote", - "unicode-ident", + "unicode-xid", ] [[package]] @@ -1029,18 +1069,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.31" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" +checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.31" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" +checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" dependencies = [ "proc-macro2", "quote", @@ -1067,9 +1107,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.6.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2" dependencies = [ "tinyvec_macros", ] @@ -1089,7 +1129,7 @@ dependencies = [ "bytes", "libc", "memchr", - "mio", + "mio 0.8.3", "num_cpus", "once_cell", "parking_lot", @@ -1133,9 +1173,9 @@ dependencies = [ [[package]] name = "tree-sitter" -version = "0.20.6" +version = "0.20.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b3b781640108d29892e8b9684642d2cda5ea05951fd58f0fea1db9edeb9b71" +checksum = "4e34327f8eac545e3f037382471b2b19367725a242bba7bc45edb9efb49fe39a" dependencies = [ "cc", "regex", @@ -1152,9 +1192,9 @@ dependencies = [ [[package]] name = "unicode-bidi" -version = "0.3.8" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" +checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f" [[package]] name = "unicode-general-category" @@ -1162,12 +1202,6 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1218098468b8085b19a2824104c70d976491d247ce194bbd9dc77181150cdfd6" -[[package]] -name = "unicode-ident" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee" - [[package]] name = "unicode-linebreak" version = "0.1.2" @@ -1198,6 +1232,18 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "unindent" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f14ee04d9415b52b3aeab06258a3f07093182b88ba0f9b8d203f211a7a7d41c7" + [[package]] name = "url" version = "2.2.2" @@ -1242,9 +1288,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "which" -version = "4.2.5" +version = "4.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c4fb54e6113b6a8772ee41c3404fb0301ac79604489467e0a9ce1f3e97c24ae" +checksum = "2a5a7e487e921cf220206864a94a89b6c6905bfc19f1057fa26a4cb360e5c1d2" dependencies = [ "either", "lazy_static", @@ -1282,43 +1328,86 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-sys" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3df6e476185f92a12c072be4a189a0210dcdcf512a1891d6dff9edb874deadc6" +dependencies = [ + "windows_aarch64_msvc 0.32.0", + "windows_i686_gnu 0.32.0", + "windows_i686_msvc 0.32.0", + "windows_x86_64_gnu 0.32.0", + "windows_x86_64_msvc 0.32.0", +] + [[package]] name = "windows-sys" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" dependencies = [ - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_msvc", + "windows_aarch64_msvc 0.36.1", + "windows_i686_gnu 0.36.1", + "windows_i686_msvc 0.36.1", + "windows_x86_64_gnu 0.36.1", + "windows_x86_64_msvc 0.36.1", ] +[[package]] +name = "windows_aarch64_msvc" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8e92753b1c443191654ec532f14c199742964a061be25d77d7a96f09db20bf5" + [[package]] name = "windows_aarch64_msvc" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" +[[package]] +name = "windows_i686_gnu" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a711c68811799e017b6038e0922cb27a5e2f43a2ddb609fe0b6f3eeda9de615" + [[package]] name = "windows_i686_gnu" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" +[[package]] +name = "windows_i686_msvc" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c11bb1a02615db74680b32a68e2d61f553cc24c4eb5b4ca10311740e44172" + [[package]] name = "windows_i686_msvc" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" +[[package]] +name = "windows_x86_64_gnu" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c912b12f7454c6620635bbff3450962753834be2a594819bd5e945af18ec64bc" + [[package]] name = "windows_x86_64_gnu" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" +[[package]] +name = "windows_x86_64_msvc" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "504a2476202769977a040c6364301a3f65d0cc9e3fb08600b2bda150a0488316" + [[package]] name = "windows_x86_64_msvc" version = "0.36.1" diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml index 6574d144..5eb3b621 100644 --- a/helix-core/Cargo.toml +++ b/helix-core/Cargo.toml @@ -12,6 +12,7 @@ include = ["src/**/*", "README.md"] [features] unicode-lines = ["ropey/unicode_lines"] +integration = [] [dependencies] helix-loader = { version = "0.6", path = "../helix-loader" } diff --git a/helix-loader/src/lib.rs b/helix-loader/src/lib.rs index 767bff7a..595ac7aa 100644 --- a/helix-loader/src/lib.rs +++ b/helix-loader/src/lib.rs @@ -11,17 +11,17 @@ pub fn runtime_dir() -> std::path::PathBuf { return dir.into(); } + if let Ok(dir) = std::env::var("CARGO_MANIFEST_DIR") { + // this is the directory of the crate being run by cargo, we need the workspace path so we take the parent + return std::path::PathBuf::from(dir).parent().unwrap().join(RT_DIR); + } + const RT_DIR: &str = "runtime"; let conf_dir = config_dir().join(RT_DIR); if conf_dir.exists() { return conf_dir; } - if let Ok(dir) = std::env::var("CARGO_MANIFEST_DIR") { - // this is the directory of the crate being run by cargo, we need the workspace path so we take the parent - return std::path::PathBuf::from(dir).parent().unwrap().join(RT_DIR); - } - // fallback to location of the executable being run std::env::current_exe() .ok() diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index 0f80c416..05f8eed4 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -17,6 +17,7 @@ app = true [features] unicode-lines = ["helix-core/unicode-lines"] +integration = [] [[bin]] name = "hx" @@ -73,3 +74,7 @@ signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] } [build-dependencies] helix-loader = { version = "0.6", path = "../helix-loader" } + +[dev-dependencies] +smallvec = "1.8" +indoc = "1.0.3" diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 09b7836f..21595eae 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -187,13 +187,21 @@ impl Application { } fn render(&mut self) { - let mut cx = crate::compositor::Context { - editor: &mut self.editor, - jobs: &mut self.jobs, - scroll: None, - }; + #[cfg(feature = "integration")] + return; + + #[allow(unreachable_code)] + { + let compositor = &mut self.compositor; - // self.compositor.render(&mut cx); + let mut cx = crate::compositor::Context { + editor: &mut self.editor, + jobs: &mut self.jobs, + scroll: None, + }; + + compositor.render(&mut cx); + } } pub async fn event_loop(&mut self) { diff --git a/helix-term/tests/integration.rs b/helix-term/tests/integration.rs index 1ef618f7..31a0d218 100644 --- a/helix-term/tests/integration.rs +++ b/helix-term/tests/integration.rs @@ -1,24 +1,153 @@ -use helix_term::{application::Application, args::Args, config::Config}; -use helix_view::current; +#[cfg(feature = "integration")] +mod integration { + use std::path::PathBuf; -use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers}; + use helix_core::{syntax::AutoPairConfig, Position, Selection, Tendril, Transaction}; + use helix_term::{application::Application, args::Args, config::Config}; + use helix_view::{current, doc, input::parse_macro}; -#[tokio::test] -async fn it_works() { - let args = Args::default(); - let config = Config::default(); - let mut app = Application::new(args, config).unwrap(); + use crossterm::event::{Event, KeyEvent}; + use indoc::indoc; - let inputs = &['i', 'h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd']; + pub struct TestCase { + pub in_text: String, + pub in_selection: Selection, + pub in_keys: String, + pub out_text: String, + pub out_selection: Selection, + } + + fn test_key_sequence( + app: Option, + test_case: &TestCase, + test_fn: &dyn Fn(&mut Application), + ) -> anyhow::Result<()> { + let mut app = + app.unwrap_or_else(|| Application::new(Args::default(), Config::default()).unwrap()); + + let (view, doc) = current!(app.editor); + + doc.apply( + &Transaction::insert( + doc.text(), + &Selection::single(1, 0), + Tendril::from(&test_case.in_text), + ) + .with_selection(test_case.in_selection.clone()), + view.id, + ); + + let input_keys = parse_macro(&test_case.in_keys)? + .into_iter() + .map(|key_event| Event::Key(KeyEvent::from(key_event))); + + for key in input_keys { + app.handle_terminal_events(Ok(key)); + } + + test_fn(&mut app); + + Ok(()) + } + + /// Use this for very simple test cases where there is one input + /// document, selection, and sequence of key presses, and you just + /// want to verify the resulting document and selection. + fn test_key_sequence_text_result( + args: Args, + config: Config, + test_case: TestCase, + ) -> anyhow::Result<()> { + let app = Application::new(args, config).unwrap(); + + test_key_sequence(Some(app), &test_case, &|app| { + let doc = doc!(app.editor); + assert_eq!(&test_case.out_text, doc.text()); - for input in inputs { - // TODO: use input.parse:: - app.handle_terminal_events(Ok(Event::Key(KeyEvent { - code: KeyCode::Char(*input), - modifiers: KeyModifiers::NONE, - }))); + let mut selections: Vec<_> = doc.selections().values().cloned().collect(); + assert_eq!(1, selections.len()); + + let sel = selections.pop().unwrap(); + assert_eq!(test_case.out_selection, sel); + })?; + + Ok(()) + } + + #[tokio::test] + async fn hello_world() -> anyhow::Result<()> { + test_key_sequence_text_result( + Args::default(), + Config::default(), + TestCase { + in_text: String::new(), + in_selection: Selection::single(0, 1), + // TODO: fix incorrect selection on new doc + in_keys: String::from("ihello worldhl"), + out_text: String::from("hello world\n"), + out_selection: Selection::single(11, 12), + }, + )?; + + Ok(()) } - let (_, doc) = current!(app.editor); - assert_eq!(doc.text(), "hello world\n"); + #[tokio::test] + async fn auto_pairs_basic() -> anyhow::Result<()> { + test_key_sequence_text_result( + Args::default(), + Config::default(), + TestCase { + in_text: String::new(), + in_selection: Selection::single(0, 1), + in_keys: String::from("i(hl"), + out_text: String::from("()\n"), + out_selection: Selection::single(1, 2), + }, + )?; + + test_key_sequence_text_result( + Args::default(), + Config { + editor: helix_view::editor::Config { + auto_pairs: AutoPairConfig::Enable(false), + ..Default::default() + }, + ..Default::default() + }, + TestCase { + in_text: String::new(), + in_selection: Selection::single(0, 1), + in_keys: String::from("i(hl"), + out_text: String::from("(\n"), + out_selection: Selection::single(1, 2), + }, + )?; + + Ok(()) + } + + #[tokio::test] + async fn auto_indent_rs() -> anyhow::Result<()> { + test_key_sequence_text_result( + Args { + files: vec![(PathBuf::from("foo.c"), Position::default())], + ..Default::default() + }, + Config::default(), + TestCase { + in_text: String::from("void foo() {}"), + in_selection: Selection::single(12, 13), + in_keys: String::from("i"), + out_text: String::from(indoc! {r#" + void foo() { + + } + "#}), + out_selection: Selection::single(15, 16), + }, + )?; + + Ok(()) + } } diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index ac19def1..76ac0b51 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -769,6 +769,7 @@ impl Editor { Ok(self.new_file_from_document(action, Document::from(rope, Some(encoding)))) } + // ??? pub fn open(&mut self, path: PathBuf, action: Action) -> Result { let path = helix_core::path::get_canonicalized_path(&path)?; let id = self.document_by_path(&path).map(|doc| doc.id); From 0f3c10a021bbe79e20bde1f55b87465edeec476d Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Wed, 16 Mar 2022 23:34:21 -0400 Subject: [PATCH 05/31] Fix initial selection of Document in new view When a new View of a Document is created, a default cursor of 0, 0 is created, and it does not get normalized to a single width cursor until at least one movement of the cursor happens. This appears to have no practical negative effect that I could find, but it makes tests difficult to work with, since the initial selection is not what you expect it to be. This changes the initial selection of a new View to be the width of the first grapheme in the text. --- .gitignore | 1 - helix-core/src/auto_pairs.rs | 14 +--- helix-core/src/selection.rs | 10 ++- helix-loader/Cargo.toml | 1 - helix-loader/src/lib.rs | 4 +- helix-term/src/application.rs | 21 ++++++ helix-term/src/commands.rs | 17 +++-- helix-term/tests/integration.rs | 112 ++++++++++++++++++++++++-------- helix-view/src/document.rs | 34 +++++++++- helix-view/src/editor.rs | 24 ++----- 10 files changed, 173 insertions(+), 65 deletions(-) diff --git a/.gitignore b/.gitignore index 346d0946..6a6fc782 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ target .direnv helix-term/rustfmt.toml -helix-syntax/languages/ result runtime/grammars diff --git a/helix-core/src/auto_pairs.rs b/helix-core/src/auto_pairs.rs index bcd47356..1131178e 100644 --- a/helix-core/src/auto_pairs.rs +++ b/helix-core/src/auto_pairs.rs @@ -1,9 +1,7 @@ //! When typing the opening character of one of the possible pairs defined below, //! this module provides the functionality to insert the paired closing character. -use crate::{ - graphemes, movement::Direction, Range, Rope, RopeGraphemes, Selection, Tendril, Transaction, -}; +use crate::{graphemes, movement::Direction, Range, Rope, Selection, Tendril, Transaction}; use std::collections::HashMap; use log::debug; @@ -149,14 +147,6 @@ fn prev_char(doc: &Rope, pos: usize) -> Option { doc.get_char(pos - 1) } -fn is_single_grapheme(doc: &Rope, range: &Range) -> bool { - let mut graphemes = RopeGraphemes::new(doc.slice(range.from()..range.to())); - let first = graphemes.next(); - let second = graphemes.next(); - debug!("first: {:#?}, second: {:#?}", first, second); - first.is_some() && second.is_none() -} - /// calculate what the resulting range should be for an auto pair insertion fn get_next_range( doc: &Rope, @@ -189,8 +179,8 @@ fn get_next_range( ); } - let single_grapheme = is_single_grapheme(doc, start_range); let doc_slice = doc.slice(..); + let single_grapheme = start_range.is_single_grapheme(doc_slice); // just skip over graphemes if len_inserted == 0 { diff --git a/helix-core/src/selection.rs b/helix-core/src/selection.rs index 1b2416f5..83bab5e3 100644 --- a/helix-core/src/selection.rs +++ b/helix-core/src/selection.rs @@ -8,7 +8,7 @@ use crate::{ prev_grapheme_boundary, }, movement::Direction, - Assoc, ChangeSet, RopeSlice, + Assoc, ChangeSet, RopeGraphemes, RopeSlice, }; use smallvec::{smallvec, SmallVec}; use std::borrow::Cow; @@ -339,6 +339,14 @@ impl Range { pub fn cursor_line(&self, text: RopeSlice) -> usize { text.char_to_line(self.cursor(text)) } + + /// Returns true if this Range covers a single grapheme in the given text + pub fn is_single_grapheme(&self, doc: RopeSlice) -> bool { + let mut graphemes = RopeGraphemes::new(doc.slice(self.from()..self.to())); + let first = graphemes.next(); + let second = graphemes.next(); + first.is_some() && second.is_none() + } } impl From<(usize, usize)> for Range { diff --git a/helix-loader/Cargo.toml b/helix-loader/Cargo.toml index 20384472..3d8a697c 100644 --- a/helix-loader/Cargo.toml +++ b/helix-loader/Cargo.toml @@ -20,7 +20,6 @@ toml = "0.5" etcetera = "0.4" tree-sitter = "0.20" once_cell = "1.12" - log = "0.4" # TODO: these two should be on !wasm32 only diff --git a/helix-loader/src/lib.rs b/helix-loader/src/lib.rs index 595ac7aa..ff4414b2 100644 --- a/helix-loader/src/lib.rs +++ b/helix-loader/src/lib.rs @@ -13,7 +13,9 @@ pub fn runtime_dir() -> std::path::PathBuf { if let Ok(dir) = std::env::var("CARGO_MANIFEST_DIR") { // this is the directory of the crate being run by cargo, we need the workspace path so we take the parent - return std::path::PathBuf::from(dir).parent().unwrap().join(RT_DIR); + let path = std::path::PathBuf::from(dir).parent().unwrap().join(RT_DIR); + log::debug!("runtime dir: {}", path.to_string_lossy()); + return path; } const RT_DIR: &str = "runtime"; diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 21595eae..146194bf 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -55,8 +55,29 @@ pub struct Application { lsp_progress: LspProgressMap, } +#[cfg(feature = "integration")] +fn setup_integration_logging() { + // Separate file config so we can include year, month and day in file logs + let _ = fern::Dispatch::new() + .format(|out, message, record| { + out.finish(format_args!( + "{} {} [{}] {}", + chrono::Local::now().format("%Y-%m-%dT%H:%M:%S%.3f"), + record.target(), + record.level(), + message + )) + }) + .level(log::LevelFilter::Debug) + .chain(std::io::stdout()) + .apply(); +} + impl Application { pub fn new(args: Args, config: Config) -> Result { + #[cfg(feature = "integration")] + setup_integration_logging(); + use helix_view::editor::Action; let config_dir = helix_loader::config_dir(); diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index c9c8e6a9..6b01cbe3 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -2094,10 +2094,17 @@ fn insert_mode(cx: &mut Context) { let (view, doc) = current!(cx.editor); enter_insert_mode(doc); - let selection = doc - .selection(view.id) - .clone() - .transform(|range| Range::new(range.to(), range.from())); + log::trace!( + "entering insert mode with sel: {:?}, text: {:?}", + doc.selection(view.id), + doc.text().to_string() + ); + + let selection = doc.selection(view.id).clone().transform(|range| { + let new_range = Range::new(range.to(), range.from()); + new_range + }); + doc.set_selection(view.id, selection); } @@ -2444,8 +2451,8 @@ fn normal_mode(cx: &mut Context) { graphemes::prev_grapheme_boundary(text, range.to()), ) }); - doc.set_selection(view.id, selection); + doc.set_selection(view.id, selection); doc.restore_cursor = false; } } diff --git a/helix-term/tests/integration.rs b/helix-term/tests/integration.rs index 31a0d218..58883d40 100644 --- a/helix-term/tests/integration.rs +++ b/helix-term/tests/integration.rs @@ -2,9 +2,9 @@ mod integration { use std::path::PathBuf; - use helix_core::{syntax::AutoPairConfig, Position, Selection, Tendril, Transaction}; + use helix_core::{syntax::AutoPairConfig, Position, Selection, Transaction}; use helix_term::{application::Application, args::Args, config::Config}; - use helix_view::{current, doc, input::parse_macro}; + use helix_view::{doc, input::parse_macro}; use crossterm::event::{Event, KeyEvent}; use indoc::indoc; @@ -25,14 +25,14 @@ mod integration { let mut app = app.unwrap_or_else(|| Application::new(Args::default(), Config::default()).unwrap()); - let (view, doc) = current!(app.editor); + let (view, doc) = helix_view::current!(app.editor); + let sel = doc.selection(view.id).clone(); + // replace the initial text with the input text doc.apply( - &Transaction::insert( - doc.text(), - &Selection::single(1, 0), - Tendril::from(&test_case.in_text), - ) + &Transaction::change_by_selection(&doc.text(), &sel, |_| { + (0, doc.text().len_chars(), Some((&test_case.in_text).into())) + }) .with_selection(test_case.in_selection.clone()), view.id, ); @@ -80,12 +80,12 @@ mod integration { Args::default(), Config::default(), TestCase { - in_text: String::new(), + in_text: "\n".into(), in_selection: Selection::single(0, 1), // TODO: fix incorrect selection on new doc - in_keys: String::from("ihello worldhl"), - out_text: String::from("hello world\n"), - out_selection: Selection::single(11, 12), + in_keys: "ihello world".into(), + out_text: "hello world\n".into(), + out_selection: Selection::single(12, 11), }, )?; @@ -93,16 +93,74 @@ mod integration { } #[tokio::test] - async fn auto_pairs_basic() -> anyhow::Result<()> { + async fn insert_mode_cursor_position() -> anyhow::Result<()> { test_key_sequence_text_result( Args::default(), Config::default(), TestCase { in_text: String::new(), + in_selection: Selection::single(0, 0), + in_keys: "i".into(), + out_text: String::new(), + out_selection: Selection::single(0, 0), + }, + )?; + + test_key_sequence_text_result( + Args::default(), + Config::default(), + TestCase { + in_text: "\n".into(), + in_selection: Selection::single(0, 1), + in_keys: "i".into(), + out_text: "\n".into(), + out_selection: Selection::single(1, 0), + }, + )?; + + test_key_sequence_text_result( + Args::default(), + Config::default(), + TestCase { + in_text: "\n".into(), in_selection: Selection::single(0, 1), - in_keys: String::from("i(hl"), - out_text: String::from("()\n"), - out_selection: Selection::single(1, 2), + in_keys: "ii".into(), + out_text: "\n".into(), + out_selection: Selection::single(1, 0), + }, + )?; + + Ok(()) + } + + #[tokio::test] + async fn insert_to_normal_mode_cursor_position() -> anyhow::Result<()> { + test_key_sequence_text_result( + Args::default(), + Config::default(), + TestCase { + in_text: "\n".into(), + in_selection: Selection::single(0, 1), + in_keys: "i".into(), + out_text: "\n".into(), + out_selection: Selection::single(1, 0), + }, + )?; + + Ok(()) + } + + #[tokio::test] + async fn auto_pairs_basic() -> anyhow::Result<()> { + test_key_sequence_text_result( + Args::default(), + Config::default(), + TestCase { + in_text: "\n".into(), + in_selection: Selection::single(0, 1), + in_keys: "i(".into(), + out_text: "()\n".into(), + out_selection: Selection::single(2, 1), }, )?; @@ -116,11 +174,11 @@ mod integration { ..Default::default() }, TestCase { - in_text: String::new(), + in_text: "\n".into(), in_selection: Selection::single(0, 1), - in_keys: String::from("i(hl"), - out_text: String::from("(\n"), - out_selection: Selection::single(1, 2), + in_keys: "i(".into(), + out_text: "(\n".into(), + out_selection: Selection::single(2, 1), }, )?; @@ -136,15 +194,17 @@ mod integration { }, Config::default(), TestCase { - in_text: String::from("void foo() {}"), - in_selection: Selection::single(12, 13), - in_keys: String::from("i"), - out_text: String::from(indoc! {r#" + in_text: "void foo() {}\n".into(), + in_selection: Selection::single(13, 12), + in_keys: "i".into(), + out_text: indoc! {r#" void foo() { } - "#}), - out_selection: Selection::single(15, 16), + "#} + .trim_start() + .into(), + out_selection: Selection::single(16, 15), }, )?; diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index a2d2af77..00adaa1a 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -1,5 +1,6 @@ use anyhow::{anyhow, bail, Context, Error}; use helix_core::auto_pairs::AutoPairs; +use helix_core::Range; use serde::de::{self, Deserialize, Deserializer}; use serde::Serialize; use std::cell::Cell; @@ -83,7 +84,7 @@ impl Serialize for Mode { pub struct Document { pub(crate) id: DocumentId, text: Rope, - pub(crate) selections: HashMap, + selections: HashMap, path: Option, encoding: &'static encoding::Encoding, @@ -637,6 +638,37 @@ impl Document { .insert(view_id, selection.ensure_invariants(self.text().slice(..))); } + /// Find the origin selection of the text in a document, i.e. where + /// a single cursor would go if it were on the first grapheme. If + /// the text is empty, returns (0, 0). + pub fn origin(&self) -> Range { + if self.text().len_chars() == 0 { + return Range::new(0, 0); + } + + Range::new(0, 1).grapheme_aligned(self.text().slice(..)) + } + + /// Reset the view's selection on this document to the + /// [origin](Document::origin) cursor. + pub fn reset_selection(&mut self, view_id: ViewId) { + let origin = self.origin(); + self.set_selection(view_id, Selection::single(origin.anchor, origin.head)); + } + + /// Initializes a new selection for the given view if it does not + /// already have one. + pub fn ensure_view_init(&mut self, view_id: ViewId) { + if self.selections.get(&view_id).is_none() { + self.reset_selection(view_id); + } + } + + /// Remove a view's selection from this document. + pub fn remove_view(&mut self, view_id: ViewId) { + self.selections.remove(&view_id); + } + /// Apply a [`Transaction`] to the [`Document`] to change its text. fn apply_impl(&mut self, transaction: &Transaction, view_id: ViewId) -> bool { let old_doc = self.text().clone(); diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 76ac0b51..8607c65a 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -32,12 +32,12 @@ use anyhow::{bail, Error}; pub use helix_core::diagnostic::Severity; pub use helix_core::register::Registers; +use helix_core::Position; use helix_core::{ auto_pairs::AutoPairs, syntax::{self, AutoPairConfig}, Change, }; -use helix_core::{Position, Selection}; use helix_dap as dap; use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize, Serializer}; @@ -645,11 +645,8 @@ impl Editor { view.offset = Position::default(); let doc = self.documents.get_mut(&doc_id).unwrap(); + doc.ensure_view_init(view.id); - // initialize selection for view - doc.selections - .entry(view.id) - .or_insert_with(|| Selection::point(0)); // TODO: reuse align_view let pos = doc .selection(view.id) @@ -719,9 +716,7 @@ impl Editor { Action::Load => { let view_id = view!(self).id; let doc = self.documents.get_mut(&id).unwrap(); - if doc.selections().is_empty() { - doc.set_selection(view_id, Selection::point(0)); - } + doc.ensure_view_init(view_id); return; } Action::HorizontalSplit | Action::VerticalSplit => { @@ -736,7 +731,7 @@ impl Editor { ); // initialize selection for view let doc = self.documents.get_mut(&id).unwrap(); - doc.set_selection(view_id, Selection::point(0)); + doc.ensure_view_init(view_id); } } @@ -769,7 +764,7 @@ impl Editor { Ok(self.new_file_from_document(action, Document::from(rope, Some(encoding)))) } - // ??? + // ??? possible use for integration tests pub fn open(&mut self, path: PathBuf, action: Action) -> Result { let path = helix_core::path::get_canonicalized_path(&path)?; let id = self.document_by_path(&path).map(|doc| doc.id); @@ -791,12 +786,7 @@ impl Editor { pub fn close(&mut self, id: ViewId) { let view = self.tree.get(self.tree.focus); // remove selection - self.documents - .get_mut(&view.doc) - .unwrap() - .selections - .remove(&id); - + self.documents.get_mut(&view.doc).unwrap().remove_view(id); self.tree.remove(id); self._refresh(); } @@ -871,7 +861,7 @@ impl Editor { let view = View::new(doc_id, self.config().gutters.clone()); let view_id = self.tree.insert(view); let doc = self.documents.get_mut(&doc_id).unwrap(); - doc.set_selection(view_id, Selection::point(0)); + doc.ensure_view_init(view_id); } self._refresh(); From 84bbe6b8f3aa23f3f9f1d8b38844efba6af17b41 Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Sat, 16 Apr 2022 22:05:45 -0400 Subject: [PATCH 06/31] refactor helpers, use new test helpers --- helix-term/tests/integration.rs | 198 +++++++++--------------- helix-term/tests/integration/helpers.rs | 87 +++++++++++ 2 files changed, 162 insertions(+), 123 deletions(-) create mode 100644 helix-term/tests/integration/helpers.rs diff --git a/helix-term/tests/integration.rs b/helix-term/tests/integration.rs index 58883d40..a32eebf5 100644 --- a/helix-term/tests/integration.rs +++ b/helix-term/tests/integration.rs @@ -1,92 +1,22 @@ #[cfg(feature = "integration")] mod integration { + mod helpers; + use std::path::PathBuf; - use helix_core::{syntax::AutoPairConfig, Position, Selection, Transaction}; - use helix_term::{application::Application, args::Args, config::Config}; - use helix_view::{doc, input::parse_macro}; + use helix_core::{syntax::AutoPairConfig, Position, Selection}; + use helix_term::{args::Args, config::Config}; - use crossterm::event::{Event, KeyEvent}; use indoc::indoc; - pub struct TestCase { - pub in_text: String, - pub in_selection: Selection, - pub in_keys: String, - pub out_text: String, - pub out_selection: Selection, - } - - fn test_key_sequence( - app: Option, - test_case: &TestCase, - test_fn: &dyn Fn(&mut Application), - ) -> anyhow::Result<()> { - let mut app = - app.unwrap_or_else(|| Application::new(Args::default(), Config::default()).unwrap()); - - let (view, doc) = helix_view::current!(app.editor); - let sel = doc.selection(view.id).clone(); - - // replace the initial text with the input text - doc.apply( - &Transaction::change_by_selection(&doc.text(), &sel, |_| { - (0, doc.text().len_chars(), Some((&test_case.in_text).into())) - }) - .with_selection(test_case.in_selection.clone()), - view.id, - ); - - let input_keys = parse_macro(&test_case.in_keys)? - .into_iter() - .map(|key_event| Event::Key(KeyEvent::from(key_event))); - - for key in input_keys { - app.handle_terminal_events(Ok(key)); - } - - test_fn(&mut app); - - Ok(()) - } - - /// Use this for very simple test cases where there is one input - /// document, selection, and sequence of key presses, and you just - /// want to verify the resulting document and selection. - fn test_key_sequence_text_result( - args: Args, - config: Config, - test_case: TestCase, - ) -> anyhow::Result<()> { - let app = Application::new(args, config).unwrap(); - - test_key_sequence(Some(app), &test_case, &|app| { - let doc = doc!(app.editor); - assert_eq!(&test_case.out_text, doc.text()); - - let mut selections: Vec<_> = doc.selections().values().cloned().collect(); - assert_eq!(1, selections.len()); - - let sel = selections.pop().unwrap(); - assert_eq!(test_case.out_selection, sel); - })?; - - Ok(()) - } + use self::helpers::*; #[tokio::test] async fn hello_world() -> anyhow::Result<()> { test_key_sequence_text_result( Args::default(), Config::default(), - TestCase { - in_text: "\n".into(), - in_selection: Selection::single(0, 1), - // TODO: fix incorrect selection on new doc - in_keys: "ihello world".into(), - out_text: "hello world\n".into(), - out_selection: Selection::single(12, 11), - }, + ("#[\n|]#", "ihello world", "hello world#[|\n]#"), )?; Ok(()) @@ -109,42 +39,79 @@ mod integration { test_key_sequence_text_result( Args::default(), Config::default(), - TestCase { - in_text: "\n".into(), - in_selection: Selection::single(0, 1), - in_keys: "i".into(), - out_text: "\n".into(), - out_selection: Selection::single(1, 0), - }, + ("#[\n|]#", "i", "#[|\n]#"), )?; test_key_sequence_text_result( Args::default(), Config::default(), - TestCase { - in_text: "\n".into(), - in_selection: Selection::single(0, 1), - in_keys: "ii".into(), - out_text: "\n".into(), - out_selection: Selection::single(1, 0), - }, + ("#[\n|]#", "i", "#[|\n]#"), + )?; + + test_key_sequence_text_result( + Args::default(), + Config::default(), + ("#[\n|]#", "ii", "#[|\n]#"), )?; Ok(()) } + /// Range direction is preserved when escaping insert mode to normal #[tokio::test] async fn insert_to_normal_mode_cursor_position() -> anyhow::Result<()> { test_key_sequence_text_result( Args::default(), Config::default(), - TestCase { - in_text: "\n".into(), - in_selection: Selection::single(0, 1), - in_keys: "i".into(), - out_text: "\n".into(), - out_selection: Selection::single(1, 0), - }, + ("#[f|]#oo\n", "vll", "#[|foo]#\n"), + )?; + + test_key_sequence_text_result( + Args::default(), + Config::default(), + ( + indoc! {"\ + #[f|]#oo + #(b|)#ar" + }, + "vll", + indoc! {"\ + #[|foo]# + #(|bar)#" + }, + ), + )?; + + test_key_sequence_text_result( + Args::default(), + Config::default(), + ( + indoc! {"\ + #[f|]#oo + #(b|)#ar" + }, + "a", + indoc! {"\ + #[fo|]#o + #(ba|)#r" + }, + ), + )?; + + test_key_sequence_text_result( + Args::default(), + Config::default(), + ( + indoc! {"\ + #[f|]#oo + #(b|)#ar" + }, + "a", + indoc! {"\ + #[f|]#oo + #(b|)#ar" + }, + ), )?; Ok(()) @@ -155,13 +122,7 @@ mod integration { test_key_sequence_text_result( Args::default(), Config::default(), - TestCase { - in_text: "\n".into(), - in_selection: Selection::single(0, 1), - in_keys: "i(".into(), - out_text: "()\n".into(), - out_selection: Selection::single(2, 1), - }, + ("#[\n|]#", "i(", "(#[|)]#\n"), )?; test_key_sequence_text_result( @@ -173,39 +134,30 @@ mod integration { }, ..Default::default() }, - TestCase { - in_text: "\n".into(), - in_selection: Selection::single(0, 1), - in_keys: "i(".into(), - out_text: "(\n".into(), - out_selection: Selection::single(2, 1), - }, + ("#[\n|]#", "i(", "(#[|\n]#"), )?; Ok(()) } #[tokio::test] - async fn auto_indent_rs() -> anyhow::Result<()> { + async fn auto_indent_c() -> anyhow::Result<()> { test_key_sequence_text_result( Args { files: vec![(PathBuf::from("foo.c"), Position::default())], ..Default::default() }, Config::default(), - TestCase { - in_text: "void foo() {}\n".into(), - in_selection: Selection::single(13, 12), - in_keys: "i".into(), - out_text: indoc! {r#" + // switches to append mode? + ( + "void foo() {#[|}]#\n", + "i", + indoc! {"\ void foo() { - + #[|\n]#\ } - "#} - .trim_start() - .into(), - out_selection: Selection::single(16, 15), - }, + "}, + ), )?; Ok(()) diff --git a/helix-term/tests/integration/helpers.rs b/helix-term/tests/integration/helpers.rs new file mode 100644 index 00000000..d22bcc3c --- /dev/null +++ b/helix-term/tests/integration/helpers.rs @@ -0,0 +1,87 @@ +use crossterm::event::{Event, KeyEvent}; +use helix_core::{test, Selection, Transaction}; +use helix_term::{application::Application, args::Args, config::Config}; +use helix_view::{doc, input::parse_macro}; + +#[derive(Clone, Debug)] +pub struct TestCase { + pub in_text: String, + pub in_selection: Selection, + pub in_keys: String, + pub out_text: String, + pub out_selection: Selection, +} + +impl> From<(S, S, S)> for TestCase { + fn from((input, keys, output): (S, S, S)) -> Self { + let (in_text, in_selection) = test::print(&input.into()); + let (out_text, out_selection) = test::print(&output.into()); + + TestCase { + in_text, + in_selection, + in_keys: keys.into(), + out_text, + out_selection, + } + } +} + +pub fn test_key_sequence>( + app: Option, + test_case: T, + test_fn: &dyn Fn(&mut Application), +) -> anyhow::Result<()> { + let test_case = test_case.into(); + let mut app = + app.unwrap_or_else(|| Application::new(Args::default(), Config::default()).unwrap()); + + let (view, doc) = helix_view::current!(app.editor); + let sel = doc.selection(view.id).clone(); + + // replace the initial text with the input text + doc.apply( + &Transaction::change_by_selection(&doc.text(), &sel, |_| { + (0, doc.text().len_chars(), Some((&test_case.in_text).into())) + }) + .with_selection(test_case.in_selection.clone()), + view.id, + ); + + let input_keys = parse_macro(&test_case.in_keys)? + .into_iter() + .map(|key_event| Event::Key(KeyEvent::from(key_event))); + + for key in input_keys { + app.handle_terminal_events(Ok(key)); + } + + test_fn(&mut app); + + Ok(()) +} + +/// Use this for very simple test cases where there is one input +/// document, selection, and sequence of key presses, and you just +/// want to verify the resulting document and selection. +pub fn test_key_sequence_text_result>( + args: Args, + config: Config, + test_case: T, +) -> anyhow::Result<()> { + let test_case = test_case.into(); + let app = Application::new(args, config).unwrap(); + + test_key_sequence(Some(app), test_case.clone(), &|app| { + let doc = doc!(app.editor); + assert_eq!(&test_case.out_text, doc.text()); + + let mut selections: Vec<_> = doc.selections().values().cloned().collect(); + assert_eq!(1, selections.len()); + + let sel = selections.pop().unwrap(); + assert_eq!(test_case.out_selection, sel); + })?; + + Ok(()) +} From 267605d147587e120d765fa62333dd986a3cb5e6 Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Sun, 17 Apr 2022 00:19:22 -0400 Subject: [PATCH 07/31] reorganize tests into groups --- helix-term/src/application.rs | 2 +- helix-term/tests/integration.rs | 143 +------------------- helix-term/tests/integration/auto_indent.rs | 24 ++++ helix-term/tests/integration/auto_pairs.rs | 24 ++++ helix-term/tests/integration/movement.rs | 96 +++++++++++++ 5 files changed, 148 insertions(+), 141 deletions(-) create mode 100644 helix-term/tests/integration/auto_indent.rs create mode 100644 helix-term/tests/integration/auto_pairs.rs create mode 100644 helix-term/tests/integration/movement.rs diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 146194bf..44025ea0 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -68,7 +68,7 @@ fn setup_integration_logging() { message )) }) - .level(log::LevelFilter::Debug) + .level(log::LevelFilter::Info) .chain(std::io::stdout()) .apply(); } diff --git a/helix-term/tests/integration.rs b/helix-term/tests/integration.rs index a32eebf5..a388cf6b 100644 --- a/helix-term/tests/integration.rs +++ b/helix-term/tests/integration.rs @@ -22,144 +22,7 @@ mod integration { Ok(()) } - #[tokio::test] - async fn insert_mode_cursor_position() -> anyhow::Result<()> { - test_key_sequence_text_result( - Args::default(), - Config::default(), - TestCase { - in_text: String::new(), - in_selection: Selection::single(0, 0), - in_keys: "i".into(), - out_text: String::new(), - out_selection: Selection::single(0, 0), - }, - )?; - - test_key_sequence_text_result( - Args::default(), - Config::default(), - ("#[\n|]#", "i", "#[|\n]#"), - )?; - - test_key_sequence_text_result( - Args::default(), - Config::default(), - ("#[\n|]#", "i", "#[|\n]#"), - )?; - - test_key_sequence_text_result( - Args::default(), - Config::default(), - ("#[\n|]#", "ii", "#[|\n]#"), - )?; - - Ok(()) - } - - /// Range direction is preserved when escaping insert mode to normal - #[tokio::test] - async fn insert_to_normal_mode_cursor_position() -> anyhow::Result<()> { - test_key_sequence_text_result( - Args::default(), - Config::default(), - ("#[f|]#oo\n", "vll", "#[|foo]#\n"), - )?; - - test_key_sequence_text_result( - Args::default(), - Config::default(), - ( - indoc! {"\ - #[f|]#oo - #(b|)#ar" - }, - "vll", - indoc! {"\ - #[|foo]# - #(|bar)#" - }, - ), - )?; - - test_key_sequence_text_result( - Args::default(), - Config::default(), - ( - indoc! {"\ - #[f|]#oo - #(b|)#ar" - }, - "a", - indoc! {"\ - #[fo|]#o - #(ba|)#r" - }, - ), - )?; - - test_key_sequence_text_result( - Args::default(), - Config::default(), - ( - indoc! {"\ - #[f|]#oo - #(b|)#ar" - }, - "a", - indoc! {"\ - #[f|]#oo - #(b|)#ar" - }, - ), - )?; - - Ok(()) - } - - #[tokio::test] - async fn auto_pairs_basic() -> anyhow::Result<()> { - test_key_sequence_text_result( - Args::default(), - Config::default(), - ("#[\n|]#", "i(", "(#[|)]#\n"), - )?; - - test_key_sequence_text_result( - Args::default(), - Config { - editor: helix_view::editor::Config { - auto_pairs: AutoPairConfig::Enable(false), - ..Default::default() - }, - ..Default::default() - }, - ("#[\n|]#", "i(", "(#[|\n]#"), - )?; - - Ok(()) - } - - #[tokio::test] - async fn auto_indent_c() -> anyhow::Result<()> { - test_key_sequence_text_result( - Args { - files: vec![(PathBuf::from("foo.c"), Position::default())], - ..Default::default() - }, - Config::default(), - // switches to append mode? - ( - "void foo() {#[|}]#\n", - "i", - indoc! {"\ - void foo() { - #[|\n]#\ - } - "}, - ), - )?; - - Ok(()) - } + mod auto_indent; + mod auto_pairs; + mod movement; } diff --git a/helix-term/tests/integration/auto_indent.rs b/helix-term/tests/integration/auto_indent.rs new file mode 100644 index 00000000..18138cca --- /dev/null +++ b/helix-term/tests/integration/auto_indent.rs @@ -0,0 +1,24 @@ +use super::*; + +#[tokio::test] +async fn auto_indent_c() -> anyhow::Result<()> { + test_key_sequence_text_result( + Args { + files: vec![(PathBuf::from("foo.c"), Position::default())], + ..Default::default() + }, + Config::default(), + // switches to append mode? + ( + "void foo() {#[|}]#\n", + "i", + indoc! {"\ + void foo() { + #[|\n]#\ + } + "}, + ), + )?; + + Ok(()) +} diff --git a/helix-term/tests/integration/auto_pairs.rs b/helix-term/tests/integration/auto_pairs.rs new file mode 100644 index 00000000..4da44d45 --- /dev/null +++ b/helix-term/tests/integration/auto_pairs.rs @@ -0,0 +1,24 @@ +use super::*; + +#[tokio::test] +async fn auto_pairs_basic() -> anyhow::Result<()> { + test_key_sequence_text_result( + Args::default(), + Config::default(), + ("#[\n|]#", "i(", "(#[|)]#\n"), + )?; + + test_key_sequence_text_result( + Args::default(), + Config { + editor: helix_view::editor::Config { + auto_pairs: AutoPairConfig::Enable(false), + ..Default::default() + }, + ..Default::default() + }, + ("#[\n|]#", "i(", "(#[|\n]#"), + )?; + + Ok(()) +} diff --git a/helix-term/tests/integration/movement.rs b/helix-term/tests/integration/movement.rs new file mode 100644 index 00000000..d2e01e71 --- /dev/null +++ b/helix-term/tests/integration/movement.rs @@ -0,0 +1,96 @@ +use super::*; + +#[tokio::test] +async fn insert_mode_cursor_position() -> anyhow::Result<()> { + test_key_sequence_text_result( + Args::default(), + Config::default(), + TestCase { + in_text: String::new(), + in_selection: Selection::single(0, 0), + in_keys: "i".into(), + out_text: String::new(), + out_selection: Selection::single(0, 0), + }, + )?; + + test_key_sequence_text_result( + Args::default(), + Config::default(), + ("#[\n|]#", "i", "#[|\n]#"), + )?; + + test_key_sequence_text_result( + Args::default(), + Config::default(), + ("#[\n|]#", "i", "#[|\n]#"), + )?; + + test_key_sequence_text_result( + Args::default(), + Config::default(), + ("#[\n|]#", "ii", "#[|\n]#"), + )?; + + Ok(()) +} + +/// Range direction is preserved when escaping insert mode to normal +#[tokio::test] +async fn insert_to_normal_mode_cursor_position() -> anyhow::Result<()> { + test_key_sequence_text_result( + Args::default(), + Config::default(), + ("#[f|]#oo\n", "vll", "#[|foo]#\n"), + )?; + + test_key_sequence_text_result( + Args::default(), + Config::default(), + ( + indoc! {"\ + #[f|]#oo + #(b|)#ar" + }, + "vll", + indoc! {"\ + #[|foo]# + #(|bar)#" + }, + ), + )?; + + test_key_sequence_text_result( + Args::default(), + Config::default(), + ( + indoc! {"\ + #[f|]#oo + #(b|)#ar" + }, + "a", + indoc! {"\ + #[fo|]#o + #(ba|)#r" + }, + ), + )?; + + test_key_sequence_text_result( + Args::default(), + Config::default(), + ( + indoc! {"\ + #[f|]#oo + #(b|)#ar" + }, + "a", + indoc! {"\ + #[f|]#oo + #(b|)#ar" + }, + ), + )?; + + Ok(()) +} From 36e5809f638028644d8a51e1ed2467ea402de170 Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Sun, 17 Apr 2022 21:04:59 -0400 Subject: [PATCH 08/31] add test for ensuring the initial cursor on a newly opened file --- Cargo.lock | 42 ++++++++++++++++++++++++ helix-term/Cargo.toml | 1 + helix-term/tests/integration.rs | 1 + helix-term/tests/integration/helpers.rs | 13 ++++++++ helix-term/tests/integration/movement.rs | 30 +++++++++++++++++ 5 files changed, 87 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 39afd141..6b49f722 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -221,6 +221,15 @@ dependencies = [ "thiserror", ] +[[package]] +name = "fastrand" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" +dependencies = [ + "instant", +] + [[package]] name = "fern" version = "0.6.1" @@ -462,6 +471,7 @@ dependencies = [ "signal-hook", "signal-hook-tokio", "smallvec", + "tempfile", "tokio", "tokio-stream", "toml", @@ -555,6 +565,15 @@ dependencies = [ "unindent", ] +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + [[package]] name = "itoa" version = "1.0.1" @@ -852,6 +871,15 @@ version = "0.6.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + [[package]] name = "retain_mut" version = "0.1.7" @@ -1056,6 +1084,20 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "tempfile" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +dependencies = [ + "cfg-if", + "fastrand", + "libc", + "redox_syscall", + "remove_dir_all", + "winapi", +] + [[package]] name = "textwrap" version = "0.15.0" diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index 05f8eed4..f1903f04 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -78,3 +78,4 @@ helix-loader = { version = "0.6", path = "../helix-loader" } [dev-dependencies] smallvec = "1.8" indoc = "1.0.3" +tempfile = "3.3.0" diff --git a/helix-term/tests/integration.rs b/helix-term/tests/integration.rs index a388cf6b..4b0a2346 100644 --- a/helix-term/tests/integration.rs +++ b/helix-term/tests/integration.rs @@ -25,4 +25,5 @@ mod integration { mod auto_indent; mod auto_pairs; mod movement; + mod write; } diff --git a/helix-term/tests/integration/helpers.rs b/helix-term/tests/integration/helpers.rs index d22bcc3c..5a853ad1 100644 --- a/helix-term/tests/integration/helpers.rs +++ b/helix-term/tests/integration/helpers.rs @@ -1,3 +1,5 @@ +use std::io::Write; + use crossterm::event::{Event, KeyEvent}; use helix_core::{test, Selection, Transaction}; use helix_term::{application::Application, args::Args, config::Config}; @@ -85,3 +87,14 @@ pub fn test_key_sequence_text_result>( Ok(()) } + +pub fn temp_file_with_contents>(content: S) -> tempfile::NamedTempFile { + let mut temp_file = tempfile::NamedTempFile::new().unwrap(); + temp_file + .as_file_mut() + .write_all(content.as_ref().as_bytes()) + .unwrap(); + temp_file.flush().unwrap(); + temp_file.as_file_mut().sync_all().unwrap(); + temp_file +} diff --git a/helix-term/tests/integration/movement.rs b/helix-term/tests/integration/movement.rs index d2e01e71..fc2583c1 100644 --- a/helix-term/tests/integration/movement.rs +++ b/helix-term/tests/integration/movement.rs @@ -1,3 +1,5 @@ +use helix_term::application::Application; + use super::*; #[tokio::test] @@ -94,3 +96,31 @@ async fn insert_to_normal_mode_cursor_position() -> anyhow::Result<()> { Ok(()) } + +/// Ensure the very initial cursor in an opened file is the width of +/// the first grapheme +#[tokio::test] +async fn cursor_position_newly_opened_file() -> anyhow::Result<()> { + let test = |content: &str, expected_sel: Selection| { + let file = helpers::temp_file_with_contents(content); + + let mut app = Application::new( + Args { + files: vec![(file.path().to_path_buf(), Position::default())], + ..Default::default() + }, + Config::default(), + ) + .unwrap(); + + let (view, doc) = helix_view::current!(app.editor); + let sel = doc.selection(view.id).clone(); + assert_eq!(expected_sel, sel); + }; + + test("foo", Selection::single(0, 1)); + test("๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ foo", Selection::single(0, 7)); + test("", Selection::single(0, 0)); + + Ok(()) +} From ee705dcb3363aeb197f6125ab2f8285782333010 Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Tue, 19 Apr 2022 01:21:31 -0400 Subject: [PATCH 09/31] use main application event loop Use the Application's main event loop to allow LSP, file writes, etc --- helix-core/src/auto_pairs.rs | 15 ++--- helix-term/src/application.rs | 30 ++++++--- helix-term/src/job.rs | 1 + helix-term/src/main.rs | 3 +- helix-term/tests/integration.rs | 3 +- helix-term/tests/integration/auto_indent.rs | 3 +- helix-term/tests/integration/auto_pairs.rs | 6 +- helix-term/tests/integration/helpers.rs | 64 +++++++++++++------ helix-term/tests/integration/movement.rs | 24 ++++--- helix-term/tests/integration/write.rs | 69 +++++++++++++++++++++ 10 files changed, 169 insertions(+), 49 deletions(-) create mode 100644 helix-term/tests/integration/write.rs diff --git a/helix-core/src/auto_pairs.rs b/helix-core/src/auto_pairs.rs index 1131178e..ff680a77 100644 --- a/helix-core/src/auto_pairs.rs +++ b/helix-core/src/auto_pairs.rs @@ -4,7 +4,6 @@ use crate::{graphemes, movement::Direction, Range, Rope, Selection, Tendril, Transaction}; use std::collections::HashMap; -use log::debug; use smallvec::SmallVec; // Heavily based on https://github.com/codemirror/closebrackets/ @@ -123,7 +122,7 @@ impl Default for AutoPairs { #[must_use] pub fn hook(doc: &Rope, selection: &Selection, ch: char, pairs: &AutoPairs) -> Option { - debug!("autopairs hook selection: {:#?}", selection); + log::trace!("autopairs hook selection: {:#?}", selection); if let Some(pair) = pairs.get(ch) { if pair.same() { @@ -225,9 +224,11 @@ fn get_next_range( // other end of the grapheme to get to where the new characters // are inserted, then move the head to where it should be let prev_bound = graphemes::prev_grapheme_boundary(doc_slice, start_range.head); - debug!( + log::trace!( "prev_bound: {}, offset: {}, len_inserted: {}", - prev_bound, offset, len_inserted + prev_bound, + offset, + len_inserted ); prev_bound + offset + len_inserted }; @@ -302,7 +303,7 @@ fn handle_open(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction { }); let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index())); - debug!("auto pair transaction: {:#?}", t); + log::debug!("auto pair transaction: {:#?}", t); t } @@ -334,7 +335,7 @@ fn handle_close(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction { }); let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index())); - debug!("auto pair transaction: {:#?}", t); + log::debug!("auto pair transaction: {:#?}", t); t } @@ -374,7 +375,7 @@ fn handle_same(doc: &Rope, selection: &Selection, pair: &Pair) -> Transaction { }); let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index())); - debug!("auto pair transaction: {:#?}", t); + log::debug!("auto pair transaction: {:#?}", t); t } diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 44025ea0..15026bb6 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -1,4 +1,5 @@ use arc_swap::{access::Map, ArcSwap}; +use futures_util::Stream; use helix_core::{ config::{default_syntax_loader, user_syntax_loader}, pos_at_coords, syntax, Selection, @@ -27,7 +28,7 @@ use std::{ use anyhow::Error; use crossterm::{ - event::{DisableMouseCapture, EnableMouseCapture, Event, EventStream}, + event::{DisableMouseCapture, EnableMouseCapture, Event}, execute, terminal, tty::IsTty, }; @@ -68,7 +69,7 @@ fn setup_integration_logging() { message )) }) - .level(log::LevelFilter::Info) + .level(log::LevelFilter::Debug) .chain(std::io::stdout()) .apply(); } @@ -225,8 +226,10 @@ impl Application { } } - pub async fn event_loop(&mut self) { - let mut reader = EventStream::new(); + pub async fn event_loop(&mut self, input_stream: &mut S) + where + S: Stream> + Unpin, + { let mut last_render = Instant::now(); let deadline = Duration::from_secs(1) / 60; @@ -242,7 +245,7 @@ impl Application { tokio::select! { biased; - Some(event) = reader.next() => { + Some(event) = input_stream.next() => { self.handle_terminal_events(event) } Some(signal) = self.signals.next() => { @@ -749,7 +752,10 @@ impl Application { Ok(()) } - pub async fn run(&mut self) -> Result { + pub async fn run(&mut self, input_stream: &mut S) -> Result + where + S: Stream> + Unpin, + { self.claim_term().await?; // Exit the alternate screen and disable raw mode before panicking @@ -764,16 +770,20 @@ impl Application { hook(info); })); - self.event_loop().await; + self.event_loop(input_stream).await; + self.close().await?; + self.restore_term()?; + + Ok(self.editor.exit_code) + } + pub async fn close(&mut self) -> anyhow::Result<()> { self.jobs.finish().await; if self.editor.close_language_servers(None).await.is_err() { log::error!("Timed out waiting for language servers to shutdown"); }; - self.restore_term()?; - - Ok(self.editor.exit_code) + Ok(()) } } diff --git a/helix-term/src/job.rs b/helix-term/src/job.rs index a6a77021..d21099f7 100644 --- a/helix-term/src/job.rs +++ b/helix-term/src/job.rs @@ -95,6 +95,7 @@ impl Jobs { /// Blocks until all the jobs that need to be waited on are done. pub async fn finish(&mut self) { let wait_futures = std::mem::take(&mut self.wait_futures); + log::debug!("waiting on jobs..."); wait_futures.for_each(|_| future::ready(())).await } } diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs index cd0b364b..7b26fb11 100644 --- a/helix-term/src/main.rs +++ b/helix-term/src/main.rs @@ -1,4 +1,5 @@ use anyhow::{Context, Error, Result}; +use crossterm::event::EventStream; use helix_term::application::Application; use helix_term::args::Args; use helix_term::config::Config; @@ -134,7 +135,7 @@ FLAGS: // TODO: use the thread local executor to spawn the application task separately from the work pool let mut app = Application::new(args, config).context("unable to create new application")?; - let exit_code = app.run().await?; + let exit_code = app.run(&mut EventStream::new()).await?; Ok(exit_code) } diff --git a/helix-term/tests/integration.rs b/helix-term/tests/integration.rs index 4b0a2346..b2b78e63 100644 --- a/helix-term/tests/integration.rs +++ b/helix-term/tests/integration.rs @@ -17,7 +17,8 @@ mod integration { Args::default(), Config::default(), ("#[\n|]#", "ihello world", "hello world#[|\n]#"), - )?; + ) + .await?; Ok(()) } diff --git a/helix-term/tests/integration/auto_indent.rs b/helix-term/tests/integration/auto_indent.rs index 18138cca..74d1ac58 100644 --- a/helix-term/tests/integration/auto_indent.rs +++ b/helix-term/tests/integration/auto_indent.rs @@ -18,7 +18,8 @@ async fn auto_indent_c() -> anyhow::Result<()> { } "}, ), - )?; + ) + .await?; Ok(()) } diff --git a/helix-term/tests/integration/auto_pairs.rs b/helix-term/tests/integration/auto_pairs.rs index 4da44d45..52fee55e 100644 --- a/helix-term/tests/integration/auto_pairs.rs +++ b/helix-term/tests/integration/auto_pairs.rs @@ -6,7 +6,8 @@ async fn auto_pairs_basic() -> anyhow::Result<()> { Args::default(), Config::default(), ("#[\n|]#", "i(", "(#[|)]#\n"), - )?; + ) + .await?; test_key_sequence_text_result( Args::default(), @@ -18,7 +19,8 @@ async fn auto_pairs_basic() -> anyhow::Result<()> { ..Default::default() }, ("#[\n|]#", "i(", "(#[|\n]#"), - )?; + ) + .await?; Ok(()) } diff --git a/helix-term/tests/integration/helpers.rs b/helix-term/tests/integration/helpers.rs index 5a853ad1..df662f07 100644 --- a/helix-term/tests/integration/helpers.rs +++ b/helix-term/tests/integration/helpers.rs @@ -1,9 +1,12 @@ -use std::io::Write; +use std::{io::Write, time::Duration}; +use anyhow::bail; use crossterm::event::{Event, KeyEvent}; use helix_core::{test, Selection, Transaction}; use helix_term::{application::Application, args::Args, config::Config}; use helix_view::{doc, input::parse_macro}; +use tokio::time::timeout; +use tokio_stream::wrappers::UnboundedReceiverStream; #[derive(Clone, Debug)] pub struct TestCase { @@ -29,10 +32,44 @@ impl> From<(S, S, S)> for TestCase { } } -pub fn test_key_sequence>( +pub async fn test_key_sequence( + app: &mut Application, + in_keys: &str, + test_fn: Option<&dyn Fn(&Application)>, +) -> anyhow::Result<()> { + let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); + + for key_event in parse_macro(&in_keys)?.into_iter() { + tx.send(Ok(Event::Key(KeyEvent::from(key_event))))?; + } + + let mut rx_stream = UnboundedReceiverStream::new(rx); + let event_loop = app.event_loop(&mut rx_stream); + let result = timeout(Duration::from_millis(500), event_loop).await; + + if result.is_ok() { + bail!("application exited before test function could run"); + } + + if let Some(test) = test_fn { + test(app); + }; + + for key_event in parse_macro(":q!")?.into_iter() { + tx.send(Ok(Event::Key(KeyEvent::from(key_event))))?; + } + + let event_loop = app.event_loop(&mut rx_stream); + timeout(Duration::from_millis(5000), event_loop).await?; + app.close().await?; + + Ok(()) +} + +pub async fn test_key_sequence_with_input_text>( app: Option, test_case: T, - test_fn: &dyn Fn(&mut Application), + test_fn: &dyn Fn(&Application), ) -> anyhow::Result<()> { let test_case = test_case.into(); let mut app = @@ -50,23 +87,13 @@ pub fn test_key_sequence>( view.id, ); - let input_keys = parse_macro(&test_case.in_keys)? - .into_iter() - .map(|key_event| Event::Key(KeyEvent::from(key_event))); - - for key in input_keys { - app.handle_terminal_events(Ok(key)); - } - - test_fn(&mut app); - - Ok(()) + test_key_sequence(&mut app, &test_case.in_keys, Some(test_fn)).await } /// Use this for very simple test cases where there is one input /// document, selection, and sequence of key presses, and you just /// want to verify the resulting document and selection. -pub fn test_key_sequence_text_result>( +pub async fn test_key_sequence_text_result>( args: Args, config: Config, test_case: T, @@ -74,7 +101,7 @@ pub fn test_key_sequence_text_result>( let test_case = test_case.into(); let app = Application::new(args, config).unwrap(); - test_key_sequence(Some(app), test_case.clone(), &|app| { + test_key_sequence_with_input_text(Some(app), test_case.clone(), &|app| { let doc = doc!(app.editor); assert_eq!(&test_case.out_text, doc.text()); @@ -83,9 +110,8 @@ pub fn test_key_sequence_text_result>( let sel = selections.pop().unwrap(); assert_eq!(test_case.out_selection, sel); - })?; - - Ok(()) + }) + .await } pub fn temp_file_with_contents>(content: S) -> tempfile::NamedTempFile { diff --git a/helix-term/tests/integration/movement.rs b/helix-term/tests/integration/movement.rs index fc2583c1..cac10852 100644 --- a/helix-term/tests/integration/movement.rs +++ b/helix-term/tests/integration/movement.rs @@ -14,25 +14,29 @@ async fn insert_mode_cursor_position() -> anyhow::Result<()> { out_text: String::new(), out_selection: Selection::single(0, 0), }, - )?; + ) + .await?; test_key_sequence_text_result( Args::default(), Config::default(), ("#[\n|]#", "i", "#[|\n]#"), - )?; + ) + .await?; test_key_sequence_text_result( Args::default(), Config::default(), ("#[\n|]#", "i", "#[|\n]#"), - )?; + ) + .await?; test_key_sequence_text_result( Args::default(), Config::default(), ("#[\n|]#", "ii", "#[|\n]#"), - )?; + ) + .await?; Ok(()) } @@ -44,7 +48,8 @@ async fn insert_to_normal_mode_cursor_position() -> anyhow::Result<()> { Args::default(), Config::default(), ("#[f|]#oo\n", "vll", "#[|foo]#\n"), - )?; + ) + .await?; test_key_sequence_text_result( Args::default(), @@ -60,7 +65,8 @@ async fn insert_to_normal_mode_cursor_position() -> anyhow::Result<()> { #(|bar)#" }, ), - )?; + ) + .await?; test_key_sequence_text_result( Args::default(), @@ -76,7 +82,8 @@ async fn insert_to_normal_mode_cursor_position() -> anyhow::Result<()> { #(ba|)#r" }, ), - )?; + ) + .await?; test_key_sequence_text_result( Args::default(), @@ -92,7 +99,8 @@ async fn insert_to_normal_mode_cursor_position() -> anyhow::Result<()> { #(b|)#ar" }, ), - )?; + ) + .await?; Ok(()) } diff --git a/helix-term/tests/integration/write.rs b/helix-term/tests/integration/write.rs new file mode 100644 index 00000000..47e56288 --- /dev/null +++ b/helix-term/tests/integration/write.rs @@ -0,0 +1,69 @@ +use std::{ + io::{Read, Write}, + ops::RangeInclusive, +}; + +use helix_term::application::Application; + +use super::*; + +#[tokio::test] +async fn test_write() -> anyhow::Result<()> { + let mut file = tempfile::NamedTempFile::new().unwrap(); + + test_key_sequence( + &mut Application::new( + Args { + files: vec![(file.path().to_path_buf(), Position::default())], + ..Default::default() + }, + Config::default(), + )?, + "ii can eat glass, it will not hurt me:w", + None, + ) + .await?; + + file.as_file_mut().flush()?; + file.as_file_mut().sync_all()?; + + let mut file_content = String::new(); + file.as_file_mut().read_to_string(&mut file_content)?; + assert_eq!("i can eat glass, it will not hurt me\n", file_content); + + Ok(()) +} + +#[tokio::test] +async fn test_write_concurrent() -> anyhow::Result<()> { + let mut file = tempfile::NamedTempFile::new().unwrap(); + let mut command = String::new(); + const RANGE: RangeInclusive = 1..=1000; + + for i in RANGE { + let cmd = format!("%c{}:w", i); + command.push_str(&cmd); + } + + test_key_sequence( + &mut Application::new( + Args { + files: vec![(file.path().to_path_buf(), Position::default())], + ..Default::default() + }, + Config::default(), + )?, + &command, + None, + ) + .await?; + + file.as_file_mut().flush()?; + file.as_file_mut().sync_all()?; + + let mut file_content = String::new(); + file.as_file_mut().read_to_string(&mut file_content)?; + assert_eq!(RANGE.end().to_string(), file_content); + + Ok(()) +} From 07fc80aece221233b4a986b0c5a03e2056cc1307 Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Sat, 30 Apr 2022 20:44:54 -0400 Subject: [PATCH 10/31] tests for serialized writes --- helix-term/tests/integration.rs | 2 + helix-term/tests/integration/auto_indent.rs | 1 + helix-term/tests/integration/auto_pairs.rs | 2 + helix-term/tests/integration/commands.rs | 25 +++++++ helix-term/tests/integration/helpers.rs | 69 +++++++++++------- helix-term/tests/integration/movement.rs | 8 +++ helix-term/tests/integration/write.rs | 79 ++++++++++++++++----- helix-view/src/editor.rs | 5 ++ 8 files changed, 147 insertions(+), 44 deletions(-) create mode 100644 helix-term/tests/integration/commands.rs diff --git a/helix-term/tests/integration.rs b/helix-term/tests/integration.rs index b2b78e63..54364e12 100644 --- a/helix-term/tests/integration.rs +++ b/helix-term/tests/integration.rs @@ -17,6 +17,7 @@ mod integration { Args::default(), Config::default(), ("#[\n|]#", "ihello world", "hello world#[|\n]#"), + None, ) .await?; @@ -25,6 +26,7 @@ mod integration { mod auto_indent; mod auto_pairs; + mod commands; mod movement; mod write; } diff --git a/helix-term/tests/integration/auto_indent.rs b/helix-term/tests/integration/auto_indent.rs index 74d1ac58..fdfc7dfb 100644 --- a/helix-term/tests/integration/auto_indent.rs +++ b/helix-term/tests/integration/auto_indent.rs @@ -18,6 +18,7 @@ async fn auto_indent_c() -> anyhow::Result<()> { } "}, ), + None, ) .await?; diff --git a/helix-term/tests/integration/auto_pairs.rs b/helix-term/tests/integration/auto_pairs.rs index 52fee55e..d34cd0fd 100644 --- a/helix-term/tests/integration/auto_pairs.rs +++ b/helix-term/tests/integration/auto_pairs.rs @@ -6,6 +6,7 @@ async fn auto_pairs_basic() -> anyhow::Result<()> { Args::default(), Config::default(), ("#[\n|]#", "i(", "(#[|)]#\n"), + None, ) .await?; @@ -19,6 +20,7 @@ async fn auto_pairs_basic() -> anyhow::Result<()> { ..Default::default() }, ("#[\n|]#", "i(", "(#[|\n]#"), + None, ) .await?; diff --git a/helix-term/tests/integration/commands.rs b/helix-term/tests/integration/commands.rs new file mode 100644 index 00000000..ec60ac96 --- /dev/null +++ b/helix-term/tests/integration/commands.rs @@ -0,0 +1,25 @@ +use helix_core::diagnostic::Severity; +use helix_term::application::Application; + +use super::*; + +#[tokio::test] +async fn test_write_quit_fail() -> anyhow::Result<()> { + test_key_sequence( + &mut Application::new( + Args { + files: vec![(PathBuf::from("/foo"), Position::default())], + ..Default::default() + }, + Config::default(), + )?, + "ihello:wq", + Some(&|app| { + assert_eq!(&Severity::Error, app.editor.get_status().unwrap().1); + }), + None, + ) + .await?; + + Ok(()) +} diff --git a/helix-term/tests/integration/helpers.rs b/helix-term/tests/integration/helpers.rs index df662f07..60bfa331 100644 --- a/helix-term/tests/integration/helpers.rs +++ b/helix-term/tests/integration/helpers.rs @@ -5,7 +5,6 @@ use crossterm::event::{Event, KeyEvent}; use helix_core::{test, Selection, Transaction}; use helix_term::{application::Application, args::Args, config::Config}; use helix_view::{doc, input::parse_macro}; -use tokio::time::timeout; use tokio_stream::wrappers::UnboundedReceiverStream; #[derive(Clone, Debug)] @@ -32,35 +31,48 @@ impl> From<(S, S, S)> for TestCase { } } +#[inline] pub async fn test_key_sequence( app: &mut Application, in_keys: &str, test_fn: Option<&dyn Fn(&Application)>, + timeout: Option, ) -> anyhow::Result<()> { + test_key_sequences(app, vec![(in_keys, test_fn)], timeout).await +} + +pub async fn test_key_sequences( + app: &mut Application, + inputs: Vec<(&str, Option<&dyn Fn(&Application)>)>, + timeout: Option, +) -> anyhow::Result<()> { + let timeout = timeout.unwrap_or(Duration::from_millis(500)); let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); + let mut rx_stream = UnboundedReceiverStream::new(rx); - for key_event in parse_macro(&in_keys)?.into_iter() { - tx.send(Ok(Event::Key(KeyEvent::from(key_event))))?; - } + for (in_keys, test_fn) in inputs { + for key_event in parse_macro(&in_keys)?.into_iter() { + tx.send(Ok(Event::Key(KeyEvent::from(key_event))))?; + } - let mut rx_stream = UnboundedReceiverStream::new(rx); - let event_loop = app.event_loop(&mut rx_stream); - let result = timeout(Duration::from_millis(500), event_loop).await; + let event_loop = app.event_loop(&mut rx_stream); + let result = tokio::time::timeout(timeout, event_loop).await; - if result.is_ok() { - bail!("application exited before test function could run"); - } + if result.is_ok() { + bail!("application exited before test function could run"); + } - if let Some(test) = test_fn { - test(app); - }; + if let Some(test) = test_fn { + test(app); + }; + } for key_event in parse_macro(":q!")?.into_iter() { tx.send(Ok(Event::Key(KeyEvent::from(key_event))))?; } let event_loop = app.event_loop(&mut rx_stream); - timeout(Duration::from_millis(5000), event_loop).await?; + tokio::time::timeout(timeout, event_loop).await?; app.close().await?; Ok(()) @@ -70,6 +82,7 @@ pub async fn test_key_sequence_with_input_text>( app: Option, test_case: T, test_fn: &dyn Fn(&Application), + timeout: Option, ) -> anyhow::Result<()> { let test_case = test_case.into(); let mut app = @@ -87,7 +100,7 @@ pub async fn test_key_sequence_with_input_text>( view.id, ); - test_key_sequence(&mut app, &test_case.in_keys, Some(test_fn)).await + test_key_sequence(&mut app, &test_case.in_keys, Some(test_fn), timeout).await } /// Use this for very simple test cases where there is one input @@ -97,20 +110,26 @@ pub async fn test_key_sequence_text_result>( args: Args, config: Config, test_case: T, + timeout: Option, ) -> anyhow::Result<()> { let test_case = test_case.into(); let app = Application::new(args, config).unwrap(); - test_key_sequence_with_input_text(Some(app), test_case.clone(), &|app| { - let doc = doc!(app.editor); - assert_eq!(&test_case.out_text, doc.text()); - - let mut selections: Vec<_> = doc.selections().values().cloned().collect(); - assert_eq!(1, selections.len()); - - let sel = selections.pop().unwrap(); - assert_eq!(test_case.out_selection, sel); - }) + test_key_sequence_with_input_text( + Some(app), + test_case.clone(), + &|app| { + let doc = doc!(app.editor); + assert_eq!(&test_case.out_text, doc.text()); + + let mut selections: Vec<_> = doc.selections().values().cloned().collect(); + assert_eq!(1, selections.len()); + + let sel = selections.pop().unwrap(); + assert_eq!(test_case.out_selection, sel); + }, + timeout, + ) .await } diff --git a/helix-term/tests/integration/movement.rs b/helix-term/tests/integration/movement.rs index cac10852..a1d294c6 100644 --- a/helix-term/tests/integration/movement.rs +++ b/helix-term/tests/integration/movement.rs @@ -14,6 +14,7 @@ async fn insert_mode_cursor_position() -> anyhow::Result<()> { out_text: String::new(), out_selection: Selection::single(0, 0), }, + None, ) .await?; @@ -21,6 +22,7 @@ async fn insert_mode_cursor_position() -> anyhow::Result<()> { Args::default(), Config::default(), ("#[\n|]#", "i", "#[|\n]#"), + None, ) .await?; @@ -28,6 +30,7 @@ async fn insert_mode_cursor_position() -> anyhow::Result<()> { Args::default(), Config::default(), ("#[\n|]#", "i", "#[|\n]#"), + None, ) .await?; @@ -35,6 +38,7 @@ async fn insert_mode_cursor_position() -> anyhow::Result<()> { Args::default(), Config::default(), ("#[\n|]#", "ii", "#[|\n]#"), + None, ) .await?; @@ -48,6 +52,7 @@ async fn insert_to_normal_mode_cursor_position() -> anyhow::Result<()> { Args::default(), Config::default(), ("#[f|]#oo\n", "vll", "#[|foo]#\n"), + None, ) .await?; @@ -65,6 +70,7 @@ async fn insert_to_normal_mode_cursor_position() -> anyhow::Result<()> { #(|bar)#" }, ), + None, ) .await?; @@ -82,6 +88,7 @@ async fn insert_to_normal_mode_cursor_position() -> anyhow::Result<()> { #(ba|)#r" }, ), + None, ) .await?; @@ -99,6 +106,7 @@ async fn insert_to_normal_mode_cursor_position() -> anyhow::Result<()> { #(b|)#ar" }, ), + None, ) .await?; diff --git a/helix-term/tests/integration/write.rs b/helix-term/tests/integration/write.rs index 47e56288..27f97a45 100644 --- a/helix-term/tests/integration/write.rs +++ b/helix-term/tests/integration/write.rs @@ -1,9 +1,11 @@ use std::{ io::{Read, Write}, - ops::RangeInclusive, + time::Duration, }; +use helix_core::diagnostic::Severity; use helix_term::application::Application; +use helix_view::doc; use super::*; @@ -21,6 +23,7 @@ async fn test_write() -> anyhow::Result<()> { )?, "ii can eat glass, it will not hurt me:w", None, + Some(Duration::from_millis(1000)), ) .await?; @@ -35,35 +38,73 @@ async fn test_write() -> anyhow::Result<()> { } #[tokio::test] -async fn test_write_concurrent() -> anyhow::Result<()> { - let mut file = tempfile::NamedTempFile::new().unwrap(); - let mut command = String::new(); - const RANGE: RangeInclusive = 1..=1000; - - for i in RANGE { - let cmd = format!("%c{}:w", i); - command.push_str(&cmd); - } - - test_key_sequence( +async fn test_write_fail_mod_flag() -> anyhow::Result<()> { + test_key_sequences( &mut Application::new( Args { - files: vec![(file.path().to_path_buf(), Position::default())], + files: vec![(PathBuf::from("/foo"), Position::default())], ..Default::default() }, Config::default(), )?, - &command, + vec![ + ( + "", + Some(&|app| { + let doc = doc!(app.editor); + assert!(!doc.is_modified()); + }), + ), + ( + "ihello", + Some(&|app| { + let doc = doc!(app.editor); + assert!(doc.is_modified()); + }), + ), + ( + ":w", + Some(&|app| { + assert_eq!(&Severity::Error, app.editor.get_status().unwrap().1); + + let doc = doc!(app.editor); + assert!(doc.is_modified()); + }), + ), + ], None, ) .await?; - file.as_file_mut().flush()?; - file.as_file_mut().sync_all()?; + Ok(()) +} - let mut file_content = String::new(); - file.as_file_mut().read_to_string(&mut file_content)?; - assert_eq!(RANGE.end().to_string(), file_content); +#[tokio::test] +#[ignore] +async fn test_write_fail_new_path() -> anyhow::Result<()> { + test_key_sequences( + &mut Application::new(Args::default(), Config::default())?, + vec![ + ( + "", + Some(&|app| { + let doc = doc!(app.editor); + assert_eq!(None, app.editor.get_status()); + assert_eq!(None, doc.path()); + }), + ), + ( + ":w /foo", + Some(&|app| { + let doc = doc!(app.editor); + assert_eq!(&Severity::Error, app.editor.get_status().unwrap().1); + assert_eq!(None, doc.path()); + }), + ), + ], + Some(Duration::from_millis(1000)), + ) + .await?; Ok(()) } diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 8607c65a..d828f9ec 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -571,6 +571,11 @@ impl Editor { self.status_msg = Some((error.into(), Severity::Error)); } + #[inline] + pub fn get_status(&self) -> Option<(&Cow<'static, str>, &Severity)> { + self.status_msg.as_ref().map(|(status, sev)| (status, sev)) + } + pub fn set_theme(&mut self, theme: Theme) { // `ui.selection` is the only scope required to be able to render a theme. if theme.find_scope_index("ui.selection").is_none() { From 40120967e9ba48d2e8b5fb4976a6ca1ce8993704 Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Sat, 30 Apr 2022 20:47:53 -0400 Subject: [PATCH 11/31] tests for buffer-close --- helix-term/tests/integration/commands.rs | 80 +++++++++++++++++++++++- helix-term/tests/integration/helpers.rs | 12 ++-- helix-term/tests/integration/write.rs | 48 ++++++++++++-- helix-view/src/editor.rs | 9 +++ 4 files changed, 137 insertions(+), 12 deletions(-) diff --git a/helix-term/tests/integration/commands.rs b/helix-term/tests/integration/commands.rs index ec60ac96..7da180b9 100644 --- a/helix-term/tests/integration/commands.rs +++ b/helix-term/tests/integration/commands.rs @@ -1,3 +1,9 @@ +use std::{ + io::{Read, Write}, + ops::RangeInclusive, + time::Duration, +}; + use helix_core::diagnostic::Severity; use helix_term::application::Application; @@ -13,7 +19,7 @@ async fn test_write_quit_fail() -> anyhow::Result<()> { }, Config::default(), )?, - "ihello:wq", + Some("ihello:wq"), Some(&|app| { assert_eq!(&Severity::Error, app.editor.get_status().unwrap().1); }), @@ -23,3 +29,75 @@ async fn test_write_quit_fail() -> anyhow::Result<()> { Ok(()) } + +#[tokio::test] +async fn test_buffer_close() -> anyhow::Result<()> { + test_key_sequences( + &mut Application::new(Args::default(), Config::default())?, + vec![ + ( + None, + Some(&|app| { + assert_eq!(1, app.editor.documents().count()); + assert!(!app.editor.is_err()); + }), + ), + ( + Some("ihello:new"), + Some(&|app| { + assert_eq!(2, app.editor.documents().count()); + assert!(!app.editor.is_err()); + }), + ), + ( + Some(":bufferclose"), + Some(&|app| { + assert_eq!(1, app.editor.documents().count()); + assert!(!app.editor.is_err()); + }), + ), + ], + None, + ) + .await?; + + // verify if writes are queued up, it finishes them before closing the buffer + let mut file = tempfile::NamedTempFile::new().unwrap(); + let mut command = String::new(); + const RANGE: RangeInclusive = 1..=10; + + for i in RANGE { + let cmd = format!("%c{}:w", i); + command.push_str(&cmd); + } + + command.push_str(":bufferclose"); + + test_key_sequence( + &mut Application::new( + Args { + files: vec![(file.path().to_path_buf(), Position::default())], + ..Default::default() + }, + Config::default(), + )?, + Some(&command), + Some(&|app| { + assert!(!app.editor.is_err(), "error: {:?}", app.editor.get_status()); + + let doc = app.editor.document_by_path(file.path()); + assert!(doc.is_none(), "found doc: {:?}", doc); + }), + Some(Duration::from_millis(5000)), + ) + .await?; + + file.as_file_mut().flush()?; + file.as_file_mut().sync_all()?; + + let mut file_content = String::new(); + file.as_file_mut().read_to_string(&mut file_content)?; + assert_eq!(RANGE.end().to_string(), file_content); + + Ok(()) +} diff --git a/helix-term/tests/integration/helpers.rs b/helix-term/tests/integration/helpers.rs index 60bfa331..18a3517c 100644 --- a/helix-term/tests/integration/helpers.rs +++ b/helix-term/tests/integration/helpers.rs @@ -34,7 +34,7 @@ impl> From<(S, S, S)> for TestCase { #[inline] pub async fn test_key_sequence( app: &mut Application, - in_keys: &str, + in_keys: Option<&str>, test_fn: Option<&dyn Fn(&Application)>, timeout: Option, ) -> anyhow::Result<()> { @@ -43,7 +43,7 @@ pub async fn test_key_sequence( pub async fn test_key_sequences( app: &mut Application, - inputs: Vec<(&str, Option<&dyn Fn(&Application)>)>, + inputs: Vec<(Option<&str>, Option<&dyn Fn(&Application)>)>, timeout: Option, ) -> anyhow::Result<()> { let timeout = timeout.unwrap_or(Duration::from_millis(500)); @@ -51,8 +51,10 @@ pub async fn test_key_sequences( let mut rx_stream = UnboundedReceiverStream::new(rx); for (in_keys, test_fn) in inputs { - for key_event in parse_macro(&in_keys)?.into_iter() { - tx.send(Ok(Event::Key(KeyEvent::from(key_event))))?; + if let Some(in_keys) = in_keys { + for key_event in parse_macro(&in_keys)?.into_iter() { + tx.send(Ok(Event::Key(KeyEvent::from(key_event))))?; + } } let event_loop = app.event_loop(&mut rx_stream); @@ -100,7 +102,7 @@ pub async fn test_key_sequence_with_input_text>( view.id, ); - test_key_sequence(&mut app, &test_case.in_keys, Some(test_fn), timeout).await + test_key_sequence(&mut app, Some(&test_case.in_keys), Some(test_fn), timeout).await } /// Use this for very simple test cases where there is one input diff --git a/helix-term/tests/integration/write.rs b/helix-term/tests/integration/write.rs index 27f97a45..365e6b8d 100644 --- a/helix-term/tests/integration/write.rs +++ b/helix-term/tests/integration/write.rs @@ -1,5 +1,6 @@ use std::{ io::{Read, Write}, + ops::RangeInclusive, time::Duration, }; @@ -21,7 +22,7 @@ async fn test_write() -> anyhow::Result<()> { }, Config::default(), )?, - "ii can eat glass, it will not hurt me:w", + Some("ii can eat glass, it will not hurt me:w"), None, Some(Duration::from_millis(1000)), ) @@ -37,6 +38,41 @@ async fn test_write() -> anyhow::Result<()> { Ok(()) } +#[tokio::test] +async fn test_write_concurrent() -> anyhow::Result<()> { + let mut file = tempfile::NamedTempFile::new().unwrap(); + let mut command = String::new(); + const RANGE: RangeInclusive = 1..=5000; + + for i in RANGE { + let cmd = format!("%c{}:w", i); + command.push_str(&cmd); + } + + test_key_sequence( + &mut Application::new( + Args { + files: vec![(file.path().to_path_buf(), Position::default())], + ..Default::default() + }, + Config::default(), + )?, + Some(&command), + None, + Some(Duration::from_millis(10000)), + ) + .await?; + + file.as_file_mut().flush()?; + file.as_file_mut().sync_all()?; + + let mut file_content = String::new(); + file.as_file_mut().read_to_string(&mut file_content)?; + assert_eq!(RANGE.end().to_string(), file_content); + + Ok(()) +} + #[tokio::test] async fn test_write_fail_mod_flag() -> anyhow::Result<()> { test_key_sequences( @@ -49,21 +85,21 @@ async fn test_write_fail_mod_flag() -> anyhow::Result<()> { )?, vec![ ( - "", + None, Some(&|app| { let doc = doc!(app.editor); assert!(!doc.is_modified()); }), ), ( - "ihello", + Some("ihello"), Some(&|app| { let doc = doc!(app.editor); assert!(doc.is_modified()); }), ), ( - ":w", + Some(":w"), Some(&|app| { assert_eq!(&Severity::Error, app.editor.get_status().unwrap().1); @@ -86,7 +122,7 @@ async fn test_write_fail_new_path() -> anyhow::Result<()> { &mut Application::new(Args::default(), Config::default())?, vec![ ( - "", + None, Some(&|app| { let doc = doc!(app.editor); assert_eq!(None, app.editor.get_status()); @@ -94,7 +130,7 @@ async fn test_write_fail_new_path() -> anyhow::Result<()> { }), ), ( - ":w /foo", + Some(":w /foo"), Some(&|app| { let doc = doc!(app.editor); assert_eq!(&Severity::Error, app.editor.get_status().unwrap().1); diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index d828f9ec..e8603221 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -576,6 +576,15 @@ impl Editor { self.status_msg.as_ref().map(|(status, sev)| (status, sev)) } + /// Returns true if the current status is an error + #[inline] + pub fn is_err(&self) -> bool { + self.status_msg + .as_ref() + .map(|(_, sev)| *sev == Severity::Error) + .unwrap_or(false) + } + pub fn set_theme(&mut self, theme: Theme) { // `ui.selection` is the only scope required to be able to render a theme. if theme.find_scope_index("ui.selection").is_none() { From 2386c81ebc118860107094591b76ef3864e120a8 Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Sat, 23 Apr 2022 21:49:31 -0400 Subject: [PATCH 12/31] use idle timer instead of fixed timeout --- helix-term/src/application.rs | 37 +++++++++++++++--- helix-term/tests/integration.rs | 1 - helix-term/tests/integration/auto_indent.rs | 1 - helix-term/tests/integration/auto_pairs.rs | 2 - helix-term/tests/integration/commands.rs | 4 -- helix-term/tests/integration/helpers.rs | 42 ++++++++------------- helix-term/tests/integration/movement.rs | 8 ---- helix-term/tests/integration/write.rs | 5 --- 8 files changed, 46 insertions(+), 54 deletions(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 15026bb6..3b96c45a 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -40,6 +40,8 @@ use { #[cfg(windows)] type Signals = futures_util::stream::Empty<()>; +const LSP_DEADLINE: Duration = Duration::from_millis(16); + pub struct Application { compositor: Compositor, pub editor: Editor, @@ -54,6 +56,7 @@ pub struct Application { signals: Signals, jobs: Jobs, lsp_progress: LspProgressMap, + last_render: Instant, } #[cfg(feature = "integration")] @@ -203,6 +206,7 @@ impl Application { signals, jobs: Jobs::new(), lsp_progress: LspProgressMap::new(), + last_render: Instant::now(), }; Ok(app) @@ -230,58 +234,79 @@ impl Application { where S: Stream> + Unpin, { - let mut last_render = Instant::now(); - let deadline = Duration::from_secs(1) / 60; - self.render(); + self.last_render = Instant::now(); loop { if self.editor.should_close() { break; } + self.event_loop_until_idle(input_stream).await; + } + } + + pub async fn event_loop_until_idle(&mut self, input_stream: &mut S) -> bool + where + S: Stream> + Unpin, + { + loop { + if self.editor.should_close() { + return false; + } + use futures_util::StreamExt; tokio::select! { biased; Some(event) = input_stream.next() => { - self.handle_terminal_events(event) + self.handle_terminal_events(event); + self.editor.reset_idle_timer(); } Some(signal) = self.signals.next() => { self.handle_signals(signal).await; + self.editor.reset_idle_timer(); } Some((id, call)) = self.editor.language_servers.incoming.next() => { self.handle_language_server_message(call, id).await; // limit render calls for fast language server messages let last = self.editor.language_servers.incoming.is_empty(); - if last || last_render.elapsed() > deadline { + + if last || self.last_render.elapsed() > LSP_DEADLINE { self.render(); - last_render = Instant::now(); + self.last_render = Instant::now(); } + + self.editor.reset_idle_timer(); } Some(payload) = self.editor.debugger_events.next() => { let needs_render = self.editor.handle_debugger_message(payload).await; if needs_render { self.render(); } + self.editor.reset_idle_timer(); } Some(config_event) = self.editor.config_events.1.recv() => { self.handle_config_events(config_event); self.render(); + self.editor.reset_idle_timer(); } Some(callback) = self.jobs.futures.next() => { self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback); self.render(); + self.editor.reset_idle_timer(); } Some(callback) = self.jobs.wait_futures.next() => { self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback); self.render(); + self.editor.reset_idle_timer(); } _ = &mut self.editor.idle_timer => { // idle timeout self.editor.clear_idle_timer(); self.handle_idle_timeout(); + return true; } } } diff --git a/helix-term/tests/integration.rs b/helix-term/tests/integration.rs index 54364e12..061bd8ff 100644 --- a/helix-term/tests/integration.rs +++ b/helix-term/tests/integration.rs @@ -17,7 +17,6 @@ mod integration { Args::default(), Config::default(), ("#[\n|]#", "ihello world", "hello world#[|\n]#"), - None, ) .await?; diff --git a/helix-term/tests/integration/auto_indent.rs b/helix-term/tests/integration/auto_indent.rs index fdfc7dfb..74d1ac58 100644 --- a/helix-term/tests/integration/auto_indent.rs +++ b/helix-term/tests/integration/auto_indent.rs @@ -18,7 +18,6 @@ async fn auto_indent_c() -> anyhow::Result<()> { } "}, ), - None, ) .await?; diff --git a/helix-term/tests/integration/auto_pairs.rs b/helix-term/tests/integration/auto_pairs.rs index d34cd0fd..52fee55e 100644 --- a/helix-term/tests/integration/auto_pairs.rs +++ b/helix-term/tests/integration/auto_pairs.rs @@ -6,7 +6,6 @@ async fn auto_pairs_basic() -> anyhow::Result<()> { Args::default(), Config::default(), ("#[\n|]#", "i(", "(#[|)]#\n"), - None, ) .await?; @@ -20,7 +19,6 @@ async fn auto_pairs_basic() -> anyhow::Result<()> { ..Default::default() }, ("#[\n|]#", "i(", "(#[|\n]#"), - None, ) .await?; diff --git a/helix-term/tests/integration/commands.rs b/helix-term/tests/integration/commands.rs index 7da180b9..4ab87b9b 100644 --- a/helix-term/tests/integration/commands.rs +++ b/helix-term/tests/integration/commands.rs @@ -1,7 +1,6 @@ use std::{ io::{Read, Write}, ops::RangeInclusive, - time::Duration, }; use helix_core::diagnostic::Severity; @@ -23,7 +22,6 @@ async fn test_write_quit_fail() -> anyhow::Result<()> { Some(&|app| { assert_eq!(&Severity::Error, app.editor.get_status().unwrap().1); }), - None, ) .await?; @@ -57,7 +55,6 @@ async fn test_buffer_close() -> anyhow::Result<()> { }), ), ], - None, ) .await?; @@ -88,7 +85,6 @@ async fn test_buffer_close() -> anyhow::Result<()> { let doc = app.editor.document_by_path(file.path()); assert!(doc.is_none(), "found doc: {:?}", doc); }), - Some(Duration::from_millis(5000)), ) .await?; diff --git a/helix-term/tests/integration/helpers.rs b/helix-term/tests/integration/helpers.rs index 18a3517c..29cb8cd8 100644 --- a/helix-term/tests/integration/helpers.rs +++ b/helix-term/tests/integration/helpers.rs @@ -36,17 +36,15 @@ pub async fn test_key_sequence( app: &mut Application, in_keys: Option<&str>, test_fn: Option<&dyn Fn(&Application)>, - timeout: Option, ) -> anyhow::Result<()> { - test_key_sequences(app, vec![(in_keys, test_fn)], timeout).await + test_key_sequences(app, vec![(in_keys, test_fn)]).await } pub async fn test_key_sequences( app: &mut Application, inputs: Vec<(Option<&str>, Option<&dyn Fn(&Application)>)>, - timeout: Option, ) -> anyhow::Result<()> { - let timeout = timeout.unwrap_or(Duration::from_millis(500)); + const TIMEOUT: Duration = Duration::from_millis(500); let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); let mut rx_stream = UnboundedReceiverStream::new(rx); @@ -57,10 +55,7 @@ pub async fn test_key_sequences( } } - let event_loop = app.event_loop(&mut rx_stream); - let result = tokio::time::timeout(timeout, event_loop).await; - - if result.is_ok() { + if !app.event_loop_until_idle(&mut rx_stream).await { bail!("application exited before test function could run"); } @@ -74,7 +69,7 @@ pub async fn test_key_sequences( } let event_loop = app.event_loop(&mut rx_stream); - tokio::time::timeout(timeout, event_loop).await?; + tokio::time::timeout(TIMEOUT, event_loop).await?; app.close().await?; Ok(()) @@ -84,7 +79,6 @@ pub async fn test_key_sequence_with_input_text>( app: Option, test_case: T, test_fn: &dyn Fn(&Application), - timeout: Option, ) -> anyhow::Result<()> { let test_case = test_case.into(); let mut app = @@ -102,7 +96,7 @@ pub async fn test_key_sequence_with_input_text>( view.id, ); - test_key_sequence(&mut app, Some(&test_case.in_keys), Some(test_fn), timeout).await + test_key_sequence(&mut app, Some(&test_case.in_keys), Some(test_fn)).await } /// Use this for very simple test cases where there is one input @@ -112,26 +106,20 @@ pub async fn test_key_sequence_text_result>( args: Args, config: Config, test_case: T, - timeout: Option, ) -> anyhow::Result<()> { let test_case = test_case.into(); let app = Application::new(args, config).unwrap(); - test_key_sequence_with_input_text( - Some(app), - test_case.clone(), - &|app| { - let doc = doc!(app.editor); - assert_eq!(&test_case.out_text, doc.text()); - - let mut selections: Vec<_> = doc.selections().values().cloned().collect(); - assert_eq!(1, selections.len()); - - let sel = selections.pop().unwrap(); - assert_eq!(test_case.out_selection, sel); - }, - timeout, - ) + test_key_sequence_with_input_text(Some(app), test_case.clone(), &|app| { + let doc = doc!(app.editor); + assert_eq!(&test_case.out_text, doc.text()); + + let mut selections: Vec<_> = doc.selections().values().cloned().collect(); + assert_eq!(1, selections.len()); + + let sel = selections.pop().unwrap(); + assert_eq!(test_case.out_selection, sel); + }) .await } diff --git a/helix-term/tests/integration/movement.rs b/helix-term/tests/integration/movement.rs index a1d294c6..cac10852 100644 --- a/helix-term/tests/integration/movement.rs +++ b/helix-term/tests/integration/movement.rs @@ -14,7 +14,6 @@ async fn insert_mode_cursor_position() -> anyhow::Result<()> { out_text: String::new(), out_selection: Selection::single(0, 0), }, - None, ) .await?; @@ -22,7 +21,6 @@ async fn insert_mode_cursor_position() -> anyhow::Result<()> { Args::default(), Config::default(), ("#[\n|]#", "i", "#[|\n]#"), - None, ) .await?; @@ -30,7 +28,6 @@ async fn insert_mode_cursor_position() -> anyhow::Result<()> { Args::default(), Config::default(), ("#[\n|]#", "i", "#[|\n]#"), - None, ) .await?; @@ -38,7 +35,6 @@ async fn insert_mode_cursor_position() -> anyhow::Result<()> { Args::default(), Config::default(), ("#[\n|]#", "ii", "#[|\n]#"), - None, ) .await?; @@ -52,7 +48,6 @@ async fn insert_to_normal_mode_cursor_position() -> anyhow::Result<()> { Args::default(), Config::default(), ("#[f|]#oo\n", "vll", "#[|foo]#\n"), - None, ) .await?; @@ -70,7 +65,6 @@ async fn insert_to_normal_mode_cursor_position() -> anyhow::Result<()> { #(|bar)#" }, ), - None, ) .await?; @@ -88,7 +82,6 @@ async fn insert_to_normal_mode_cursor_position() -> anyhow::Result<()> { #(ba|)#r" }, ), - None, ) .await?; @@ -106,7 +99,6 @@ async fn insert_to_normal_mode_cursor_position() -> anyhow::Result<()> { #(b|)#ar" }, ), - None, ) .await?; diff --git a/helix-term/tests/integration/write.rs b/helix-term/tests/integration/write.rs index 365e6b8d..0cc41dbc 100644 --- a/helix-term/tests/integration/write.rs +++ b/helix-term/tests/integration/write.rs @@ -1,7 +1,6 @@ use std::{ io::{Read, Write}, ops::RangeInclusive, - time::Duration, }; use helix_core::diagnostic::Severity; @@ -24,7 +23,6 @@ async fn test_write() -> anyhow::Result<()> { )?, Some("ii can eat glass, it will not hurt me:w"), None, - Some(Duration::from_millis(1000)), ) .await?; @@ -59,7 +57,6 @@ async fn test_write_concurrent() -> anyhow::Result<()> { )?, Some(&command), None, - Some(Duration::from_millis(10000)), ) .await?; @@ -108,7 +105,6 @@ async fn test_write_fail_mod_flag() -> anyhow::Result<()> { }), ), ], - None, ) .await?; @@ -138,7 +134,6 @@ async fn test_write_fail_new_path() -> anyhow::Result<()> { }), ), ], - Some(Duration::from_millis(1000)), ) .await?; From 2fbf83363028179fe8d3908b5d9911d8595163b1 Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Mon, 25 Apr 2022 19:18:27 -0400 Subject: [PATCH 13/31] add integration feature to github tests --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1368d1bc..d87d4a3b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -64,7 +64,7 @@ jobs: uses: actions-rs/cargo@v1 with: command: test - args: --workspace + args: --workspace --features integration strategy: matrix: From 1533f489340fb63eee31c12122d6233cb5f6abaf Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Mon, 25 Apr 2022 20:25:16 -0400 Subject: [PATCH 14/31] use Results in integration tests for more error context --- helix-term/tests/integration/commands.rs | 2 +- helix-term/tests/integration/helpers.rs | 25 ++++++++++++++---------- helix-term/tests/integration/movement.rs | 15 +++++++------- helix-term/tests/integration/write.rs | 5 ++--- 4 files changed, 26 insertions(+), 21 deletions(-) diff --git a/helix-term/tests/integration/commands.rs b/helix-term/tests/integration/commands.rs index 4ab87b9b..0d2e14fd 100644 --- a/helix-term/tests/integration/commands.rs +++ b/helix-term/tests/integration/commands.rs @@ -59,7 +59,7 @@ async fn test_buffer_close() -> anyhow::Result<()> { .await?; // verify if writes are queued up, it finishes them before closing the buffer - let mut file = tempfile::NamedTempFile::new().unwrap(); + let mut file = tempfile::NamedTempFile::new()?; let mut command = String::new(); const RANGE: RangeInclusive = 1..=10; diff --git a/helix-term/tests/integration/helpers.rs b/helix-term/tests/integration/helpers.rs index 29cb8cd8..2a542404 100644 --- a/helix-term/tests/integration/helpers.rs +++ b/helix-term/tests/integration/helpers.rs @@ -81,8 +81,10 @@ pub async fn test_key_sequence_with_input_text>( test_fn: &dyn Fn(&Application), ) -> anyhow::Result<()> { let test_case = test_case.into(); - let mut app = - app.unwrap_or_else(|| Application::new(Args::default(), Config::default()).unwrap()); + let mut app = match app { + Some(app) => app, + None => Application::new(Args::default(), Config::default())?, + }; let (view, doc) = helix_view::current!(app.editor); let sel = doc.selection(view.id).clone(); @@ -108,7 +110,7 @@ pub async fn test_key_sequence_text_result>( test_case: T, ) -> anyhow::Result<()> { let test_case = test_case.into(); - let app = Application::new(args, config).unwrap(); + let app = Application::new(args, config)?; test_key_sequence_with_input_text(Some(app), test_case.clone(), &|app| { let doc = doc!(app.editor); @@ -123,13 +125,16 @@ pub async fn test_key_sequence_text_result>( .await } -pub fn temp_file_with_contents>(content: S) -> tempfile::NamedTempFile { - let mut temp_file = tempfile::NamedTempFile::new().unwrap(); +pub fn temp_file_with_contents>( + content: S, +) -> anyhow::Result { + let mut temp_file = tempfile::NamedTempFile::new()?; + temp_file .as_file_mut() - .write_all(content.as_ref().as_bytes()) - .unwrap(); - temp_file.flush().unwrap(); - temp_file.as_file_mut().sync_all().unwrap(); - temp_file + .write_all(content.as_ref().as_bytes())?; + + temp_file.flush()?; + temp_file.as_file_mut().sync_all()?; + Ok(temp_file) } diff --git a/helix-term/tests/integration/movement.rs b/helix-term/tests/integration/movement.rs index cac10852..e0bfc3bf 100644 --- a/helix-term/tests/integration/movement.rs +++ b/helix-term/tests/integration/movement.rs @@ -109,8 +109,8 @@ async fn insert_to_normal_mode_cursor_position() -> anyhow::Result<()> { /// the first grapheme #[tokio::test] async fn cursor_position_newly_opened_file() -> anyhow::Result<()> { - let test = |content: &str, expected_sel: Selection| { - let file = helpers::temp_file_with_contents(content); + let test = |content: &str, expected_sel: Selection| -> anyhow::Result<()> { + let file = helpers::temp_file_with_contents(content)?; let mut app = Application::new( Args { @@ -118,17 +118,18 @@ async fn cursor_position_newly_opened_file() -> anyhow::Result<()> { ..Default::default() }, Config::default(), - ) - .unwrap(); + )?; let (view, doc) = helix_view::current!(app.editor); let sel = doc.selection(view.id).clone(); assert_eq!(expected_sel, sel); + + Ok(()) }; - test("foo", Selection::single(0, 1)); - test("๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ foo", Selection::single(0, 7)); - test("", Selection::single(0, 0)); + test("foo", Selection::single(0, 1))?; + test("๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ foo", Selection::single(0, 7))?; + test("", Selection::single(0, 0))?; Ok(()) } diff --git a/helix-term/tests/integration/write.rs b/helix-term/tests/integration/write.rs index 0cc41dbc..4f8f0eb5 100644 --- a/helix-term/tests/integration/write.rs +++ b/helix-term/tests/integration/write.rs @@ -11,7 +11,7 @@ use super::*; #[tokio::test] async fn test_write() -> anyhow::Result<()> { - let mut file = tempfile::NamedTempFile::new().unwrap(); + let mut file = tempfile::NamedTempFile::new()?; test_key_sequence( &mut Application::new( @@ -38,7 +38,7 @@ async fn test_write() -> anyhow::Result<()> { #[tokio::test] async fn test_write_concurrent() -> anyhow::Result<()> { - let mut file = tempfile::NamedTempFile::new().unwrap(); + let mut file = tempfile::NamedTempFile::new()?; let mut command = String::new(); const RANGE: RangeInclusive = 1..=5000; @@ -112,7 +112,6 @@ async fn test_write_fail_mod_flag() -> anyhow::Result<()> { } #[tokio::test] -#[ignore] async fn test_write_fail_new_path() -> anyhow::Result<()> { test_key_sequences( &mut Application::new(Args::default(), Config::default())?, From ed950fcc56c480dc5a54c7e07918dca9192db200 Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Tue, 26 Apr 2022 19:18:20 -0400 Subject: [PATCH 15/31] Add more context; Editor::open doesn't need to own path --- helix-term/src/application.rs | 17 ++++++++++------- helix-term/src/commands.rs | 4 ++-- helix-term/src/commands/lsp.rs | 6 +++--- helix-term/src/commands/typed.rs | 12 ++++++------ helix-term/src/compositor.rs | 6 +++--- helix-term/src/ui/mod.rs | 2 +- helix-view/src/editor.rs | 4 ++-- helix-view/src/handlers/dap.rs | 2 +- 8 files changed, 28 insertions(+), 25 deletions(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 3b96c45a..65cf4b2e 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -25,7 +25,7 @@ use std::{ time::{Duration, Instant}, }; -use anyhow::Error; +use anyhow::{Context, Error}; use crossterm::{ event::{DisableMouseCapture, EnableMouseCapture, Event}, @@ -122,7 +122,7 @@ impl Application { }); let syn_loader = std::sync::Arc::new(syntax::Loader::new(syn_loader_conf)); - let mut compositor = Compositor::new()?; + let mut compositor = Compositor::new().context("build compositor")?; let config = Arc::new(ArcSwap::from_pointee(config)); let mut editor = Editor::new( compositor.size(), @@ -141,26 +141,28 @@ impl Application { if args.load_tutor { let path = helix_loader::runtime_dir().join("tutor.txt"); - editor.open(path, Action::VerticalSplit)?; + editor.open(&path, Action::VerticalSplit)?; // Unset path to prevent accidentally saving to the original tutor file. doc_mut!(editor).set_path(None)?; } else if !args.files.is_empty() { let first = &args.files[0].0; // we know it's not empty if first.is_dir() { - std::env::set_current_dir(&first)?; + std::env::set_current_dir(&first).context("set current dir")?; editor.new_file(Action::VerticalSplit); let picker = ui::file_picker(".".into(), &config.load().editor); compositor.push(Box::new(overlayed(picker))); } else { let nr_of_files = args.files.len(); - editor.open(first.to_path_buf(), Action::VerticalSplit)?; + editor.open(first, Action::VerticalSplit)?; for (file, pos) in args.files { if file.is_dir() { return Err(anyhow::anyhow!( "expected a path to file, found a directory. (to open a directory pass it as first argument)" )); } else { - let doc_id = editor.open(file, Action::Load)?; + let doc_id = editor + .open(&file, Action::Load) + .context(format!("open '{}'", file.to_string_lossy()))?; // with Action::Load all documents have the same view let view_id = editor.tree.focus; let doc = editor.document_mut(doc_id).unwrap(); @@ -192,7 +194,8 @@ impl Application { #[cfg(windows)] let signals = futures_util::stream::empty(); #[cfg(not(windows))] - let signals = Signals::new(&[signal::SIGTSTP, signal::SIGCONT])?; + let signals = + Signals::new(&[signal::SIGTSTP, signal::SIGCONT]).context("build signal handler")?; let app = Self { compositor, diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 6b01cbe3..85dbfd56 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1024,7 +1024,7 @@ fn goto_file_impl(cx: &mut Context, action: Action) { for sel in paths { let p = sel.trim(); if !p.is_empty() { - if let Err(e) = cx.editor.open(PathBuf::from(p), action) { + if let Err(e) = cx.editor.open(&PathBuf::from(p), action) { cx.editor.set_error(format!("Open file failed: {:?}", e)); } } @@ -1849,7 +1849,7 @@ fn global_search(cx: &mut Context) { } }, move |cx, (line_num, path), action| { - match cx.editor.open(path.into(), action) { + match cx.editor.open(path, action) { Ok(_) => {} Err(e) => { cx.editor.set_error(format!( diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index b6bea8d6..ff61ee63 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -61,7 +61,7 @@ fn jump_to_location( return; } }; - let _id = editor.open(path, action).expect("editor.open failed"); + let _id = editor.open(&path, action).expect("editor.open failed"); let (view, doc) = current!(editor); let definition_pos = location.range.start; // TODO: convert inside server @@ -114,7 +114,7 @@ fn sym_picker( return; } }; - if let Err(err) = cx.editor.open(path, action) { + if let Err(err) = cx.editor.open(&path, action) { let err = format!("failed to open document: {}: {}", uri, err); log::error!("{}", err); cx.editor.set_error(err); @@ -385,7 +385,7 @@ pub fn apply_workspace_edit( }; let current_view_id = view!(editor).id; - let doc_id = match editor.open(path, Action::Load) { + let doc_id = match editor.open(&path, Action::Load) { Ok(doc_id) => doc_id, Err(err) => { let err = format!("failed to open document: {}: {}", uri, err); diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index ae3e63af..3c88b0ce 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -50,7 +50,7 @@ fn open( ensure!(!args.is_empty(), "wrong argument count"); for arg in args { let (path, pos) = args::parse_file(arg); - let _ = cx.editor.open(path, Action::Replace)?; + let _ = cx.editor.open(&path, Action::Replace)?; let (view, doc) = current!(cx.editor); let pos = Selection::point(pos_at_coords(doc.text().slice(..), pos, true)); doc.set_selection(view.id, pos); @@ -819,7 +819,7 @@ fn vsplit( } else { for arg in args { cx.editor - .open(PathBuf::from(arg.as_ref()), Action::VerticalSplit)?; + .open(&PathBuf::from(arg.as_ref()), Action::VerticalSplit)?; } } @@ -838,7 +838,7 @@ fn hsplit( } else { for arg in args { cx.editor - .open(PathBuf::from(arg.as_ref()), Action::HorizontalSplit)?; + .open(&PathBuf::from(arg.as_ref()), Action::HorizontalSplit)?; } } @@ -923,7 +923,7 @@ fn tutor( _event: PromptEvent, ) -> anyhow::Result<()> { let path = helix_loader::runtime_dir().join("tutor.txt"); - cx.editor.open(path, Action::Replace)?; + cx.editor.open(&path, Action::Replace)?; // Unset path to prevent accidentally saving to the original tutor file. doc_mut!(cx.editor).set_path(None)?; Ok(()) @@ -1150,7 +1150,7 @@ fn open_config( _event: PromptEvent, ) -> anyhow::Result<()> { cx.editor - .open(helix_loader::config_file(), Action::Replace)?; + .open(&helix_loader::config_file(), Action::Replace)?; Ok(()) } @@ -1159,7 +1159,7 @@ fn open_log( _args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { - cx.editor.open(helix_loader::log_file(), Action::Replace)?; + cx.editor.open(&helix_loader::log_file(), Action::Replace)?; Ok(()) } diff --git a/helix-term/src/compositor.rs b/helix-term/src/compositor.rs index e3cec643..1d421213 100644 --- a/helix-term/src/compositor.rs +++ b/helix-term/src/compositor.rs @@ -63,7 +63,7 @@ pub trait Component: Any + AnyComponent { } } -use anyhow::Error; +use anyhow::Context as AnyhowContext; use std::io::stdout; use tui::backend::{Backend, CrosstermBackend}; type Terminal = tui::terminal::Terminal>; @@ -76,9 +76,9 @@ pub struct Compositor { } impl Compositor { - pub fn new() -> Result { + pub fn new() -> anyhow::Result { let backend = CrosstermBackend::new(stdout()); - let terminal = Terminal::new(backend)?; + let terminal = Terminal::new(backend).context("build terminal")?; Ok(Self { layers: Vec::new(), terminal, diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 23d0dca0..76ddaf89 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -175,7 +175,7 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi path.strip_prefix(&root).unwrap_or(path).to_string_lossy() }, move |cx, path: &PathBuf, action| { - if let Err(e) = cx.editor.open(path.into(), action) { + if let Err(e) = cx.editor.open(path, action) { let err = if let Some(err) = e.source() { format!("{}", err) } else { diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index e8603221..8ef4413e 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -779,8 +779,8 @@ impl Editor { } // ??? possible use for integration tests - pub fn open(&mut self, path: PathBuf, action: Action) -> Result { - let path = helix_core::path::get_canonicalized_path(&path)?; + pub fn open(&mut self, path: &Path, action: Action) -> Result { + let path = helix_core::path::get_canonicalized_path(path)?; let id = self.document_by_path(&path).map(|doc| doc.id); let id = if let Some(id) = id { diff --git a/helix-view/src/handlers/dap.rs b/helix-view/src/handlers/dap.rs index b17ca353..ae1ae64c 100644 --- a/helix-view/src/handlers/dap.rs +++ b/helix-view/src/handlers/dap.rs @@ -62,7 +62,7 @@ pub fn jump_to_stack_frame(editor: &mut Editor, frame: &helix_dap::StackFrame) { return; }; - if let Err(e) = editor.open(path, Action::Replace) { + if let Err(e) = editor.open(&path, Action::Replace) { editor.set_error(format!("Unable to jump to stack frame: {}", e)); return; } From 652cdda8338bee55eeff58066cd20e68bb0b5a44 Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Wed, 27 Apr 2022 00:07:59 -0400 Subject: [PATCH 16/31] use test terminal backend for integration tests --- helix-term/src/application.rs | 20 +++++++------------- helix-term/src/commands.rs | 8 ++++---- helix-term/src/compositor.rs | 20 +++++++++++++++++++- 3 files changed, 30 insertions(+), 18 deletions(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 65cf4b2e..886b531b 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -216,21 +216,15 @@ impl Application { } fn render(&mut self) { - #[cfg(feature = "integration")] - return; - - #[allow(unreachable_code)] - { - let compositor = &mut self.compositor; + let compositor = &mut self.compositor; - let mut cx = crate::compositor::Context { - editor: &mut self.editor, - jobs: &mut self.jobs, - scroll: None, - }; + let mut cx = crate::compositor::Context { + editor: &mut self.editor, + jobs: &mut self.jobs, + scroll: None, + }; - compositor.render(&mut cx); - } + compositor.render(&mut cx); } pub async fn event_loop(&mut self, input_stream: &mut S) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 85dbfd56..4dfa6ec8 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -2100,10 +2100,10 @@ fn insert_mode(cx: &mut Context) { doc.text().to_string() ); - let selection = doc.selection(view.id).clone().transform(|range| { - let new_range = Range::new(range.to(), range.from()); - new_range - }); + let selection = doc + .selection(view.id) + .clone() + .transform(|range| Range::new(range.to(), range.from())); doc.set_selection(view.id, selection); } diff --git a/helix-term/src/compositor.rs b/helix-term/src/compositor.rs index 1d421213..61a3bfaf 100644 --- a/helix-term/src/compositor.rs +++ b/helix-term/src/compositor.rs @@ -5,6 +5,9 @@ use helix_core::Position; use helix_view::graphics::{CursorKind, Rect}; use crossterm::event::Event; + +#[cfg(feature = "integration")] +use tui::backend::TestBackend; use tui::buffer::Buffer as Surface; pub type Callback = Box; @@ -64,10 +67,20 @@ pub trait Component: Any + AnyComponent { } use anyhow::Context as AnyhowContext; +use tui::backend::Backend; + +#[cfg(not(feature = "integration"))] +use tui::backend::CrosstermBackend; + +#[cfg(not(feature = "integration"))] use std::io::stdout; -use tui::backend::{Backend, CrosstermBackend}; + +#[cfg(not(feature = "integration"))] type Terminal = tui::terminal::Terminal>; +#[cfg(feature = "integration")] +type Terminal = tui::terminal::Terminal; + pub struct Compositor { layers: Vec>, terminal: Terminal, @@ -77,7 +90,12 @@ pub struct Compositor { impl Compositor { pub fn new() -> anyhow::Result { + #[cfg(not(feature = "integration"))] let backend = CrosstermBackend::new(stdout()); + + #[cfg(feature = "integration")] + let backend = TestBackend::new(120, 150); + let terminal = Terminal::new(backend).context("build terminal")?; Ok(Self { layers: Vec::new(), From cb0440be85338b2669a8341dee2861ea53da7ef7 Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Wed, 27 Apr 2022 00:42:18 -0400 Subject: [PATCH 17/31] use env var for integration test log level --- .github/workflows/build.yml | 3 +++ helix-term/src/application.rs | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d87d4a3b..50829caa 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -38,6 +38,9 @@ jobs: test: name: Test Suite runs-on: ${{ matrix.os }} + env: + RUST_BACKTRACE: 1 + HELIX_LOG_LEVEL: info steps: - name: Checkout sources uses: actions/checkout@v3 diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 886b531b..f3aa955f 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -61,6 +61,10 @@ pub struct Application { #[cfg(feature = "integration")] fn setup_integration_logging() { + let level = std::env::var("HELIX_LOG_LEVEL") + .map(|lvl| lvl.parse().unwrap()) + .unwrap_or(log::LevelFilter::Info); + // Separate file config so we can include year, month and day in file logs let _ = fern::Dispatch::new() .format(|out, message, record| { @@ -72,7 +76,7 @@ fn setup_integration_logging() { message )) }) - .level(log::LevelFilter::Debug) + .level(level) .chain(std::io::stdout()) .apply(); } From 4e34ee7d2e9dcf9b166abce20f5b2dd083ad2006 Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Wed, 27 Apr 2022 00:48:17 -0400 Subject: [PATCH 18/31] don't read from stdin for integration tests --- helix-term/src/application.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index f3aa955f..c611a691 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -180,7 +180,7 @@ impl Application { let (view, doc) = current!(editor); align_view(doc, view, Align::Center); } - } else if stdin().is_tty() { + } else if stdin().is_tty() || cfg!(feature = "integration") { editor.new_file(Action::VerticalSplit); } else if cfg!(target_os = "macos") { // On Linux and Windows, we allow the output of a command to be piped into the new buffer. From 28e94fb2613fbedeef56c1bc6e21830277bb35bb Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Wed, 27 Apr 2022 08:44:20 -0400 Subject: [PATCH 19/31] need the full languages config for integration tests --- .github/workflows/build.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 50829caa..9d0383a7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -53,9 +53,6 @@ jobs: - uses: Swatinem/rust-cache@v1 - - name: Copy minimal languages config - run: cp .github/workflows/languages.toml ./languages.toml - - name: Cache test tree-sitter grammar uses: actions/cache@v3 with: From ef8fe5a5ce536c65f34e479db79b94c8435aa3b2 Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Fri, 29 Apr 2022 16:27:35 -0400 Subject: [PATCH 20/31] use system's appropriate line ending --- helix-term/tests/integration/auto_indent.rs | 7 ++++--- helix-term/tests/integration/helpers.rs | 17 +++++++++++++++++ helix-term/tests/integration/write.rs | 5 ++++- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/helix-term/tests/integration/auto_indent.rs b/helix-term/tests/integration/auto_indent.rs index 74d1ac58..8933cb6a 100644 --- a/helix-term/tests/integration/auto_indent.rs +++ b/helix-term/tests/integration/auto_indent.rs @@ -10,13 +10,14 @@ async fn auto_indent_c() -> anyhow::Result<()> { Config::default(), // switches to append mode? ( - "void foo() {#[|}]#\n", + helpers::platform_line("void foo() {#[|}]#").as_ref(), "i", - indoc! {"\ + helpers::platform_line(indoc! {"\ void foo() { #[|\n]#\ } - "}, + "}) + .as_ref(), ), ) .await?; diff --git a/helix-term/tests/integration/helpers.rs b/helix-term/tests/integration/helpers.rs index 2a542404..706e1afb 100644 --- a/helix-term/tests/integration/helpers.rs +++ b/helix-term/tests/integration/helpers.rs @@ -138,3 +138,20 @@ pub fn temp_file_with_contents>( temp_file.as_file_mut().sync_all()?; Ok(temp_file) } + +/// Replaces all LF chars with the system's appropriate line feed +/// character, and if one doesn't exist already, appends the system's +/// appropriate line ending to the end of a string. +pub fn platform_line(input: &str) -> String { + let line_end = helix_core::DEFAULT_LINE_ENDING.as_str(); + + // we can assume that the source files in this code base will always + // be LF, so indoc strings will always insert LF + let mut output = input.replace("\n", line_end); + + if !output.ends_with(line_end) { + output.push_str(line_end); + } + + output +} diff --git a/helix-term/tests/integration/write.rs b/helix-term/tests/integration/write.rs index 4f8f0eb5..06af9dd8 100644 --- a/helix-term/tests/integration/write.rs +++ b/helix-term/tests/integration/write.rs @@ -31,7 +31,10 @@ async fn test_write() -> anyhow::Result<()> { let mut file_content = String::new(); file.as_file_mut().read_to_string(&mut file_content)?; - assert_eq!("i can eat glass, it will not hurt me\n", file_content); + assert_eq!( + helpers::platform_line("i can eat glass, it will not hurt me"), + file_content + ); Ok(()) } From acf931709a56e5af0ac101276fcfb3ba45f159f2 Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Sat, 30 Apr 2022 00:35:00 -0400 Subject: [PATCH 21/31] use a read only file to ensure write failure --- helix-term/tests/integration/commands.rs | 4 +++- helix-term/tests/integration/helpers.rs | 12 ++++++++++++ helix-term/tests/integration/write.rs | 18 ++++++++++++++---- 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/helix-term/tests/integration/commands.rs b/helix-term/tests/integration/commands.rs index 0d2e14fd..1ff7cc90 100644 --- a/helix-term/tests/integration/commands.rs +++ b/helix-term/tests/integration/commands.rs @@ -10,10 +10,12 @@ use super::*; #[tokio::test] async fn test_write_quit_fail() -> anyhow::Result<()> { + let file = helpers::new_readonly_tempfile()?; + test_key_sequence( &mut Application::new( Args { - files: vec![(PathBuf::from("/foo"), Position::default())], + files: vec![(file.path().to_path_buf(), Position::default())], ..Default::default() }, Config::default(), diff --git a/helix-term/tests/integration/helpers.rs b/helix-term/tests/integration/helpers.rs index 706e1afb..3fe1934f 100644 --- a/helix-term/tests/integration/helpers.rs +++ b/helix-term/tests/integration/helpers.rs @@ -5,6 +5,7 @@ use crossterm::event::{Event, KeyEvent}; use helix_core::{test, Selection, Transaction}; use helix_term::{application::Application, args::Args, config::Config}; use helix_view::{doc, input::parse_macro}; +use tempfile::NamedTempFile; use tokio_stream::wrappers::UnboundedReceiverStream; #[derive(Clone, Debug)] @@ -155,3 +156,14 @@ pub fn platform_line(input: &str) -> String { output } + +/// Creates a new temporary file that is set to read only. Useful for +/// testing write failures. +pub fn new_readonly_tempfile() -> anyhow::Result { + let mut file = tempfile::NamedTempFile::new()?; + let metadata = file.as_file().metadata()?; + let mut perms = metadata.permissions(); + perms.set_readonly(true); + file.as_file_mut().set_permissions(perms)?; + Ok(file) +} diff --git a/helix-term/tests/integration/write.rs b/helix-term/tests/integration/write.rs index 06af9dd8..f3abbd91 100644 --- a/helix-term/tests/integration/write.rs +++ b/helix-term/tests/integration/write.rs @@ -75,10 +75,12 @@ async fn test_write_concurrent() -> anyhow::Result<()> { #[tokio::test] async fn test_write_fail_mod_flag() -> anyhow::Result<()> { + let file = helpers::new_readonly_tempfile()?; + test_key_sequences( &mut Application::new( Args { - files: vec![(PathBuf::from("/foo"), Position::default())], + files: vec![(file.path().to_path_buf(), Position::default())], ..Default::default() }, Config::default(), @@ -116,6 +118,8 @@ async fn test_write_fail_mod_flag() -> anyhow::Result<()> { #[tokio::test] async fn test_write_fail_new_path() -> anyhow::Result<()> { + let file = helpers::new_readonly_tempfile()?; + test_key_sequences( &mut Application::new(Args::default(), Config::default())?, vec![ @@ -123,15 +127,21 @@ async fn test_write_fail_new_path() -> anyhow::Result<()> { None, Some(&|app| { let doc = doc!(app.editor); - assert_eq!(None, app.editor.get_status()); + assert_ne!( + Some(&Severity::Error), + app.editor.get_status().map(|status| status.1) + ); assert_eq!(None, doc.path()); }), ), ( - Some(":w /foo"), + Some(&format!(":w {}", file.path().to_string_lossy())), Some(&|app| { let doc = doc!(app.editor); - assert_eq!(&Severity::Error, app.editor.get_status().unwrap().1); + assert_eq!( + Some(&Severity::Error), + app.editor.get_status().map(|status| status.1) + ); assert_eq!(None, doc.path()); }), ), From 8d8d389536d1f948f25a38c33f278a5e2f8d1b28 Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Sat, 30 Apr 2022 09:22:20 -0400 Subject: [PATCH 22/31] rename top level module to satisfy cargo fmt --- helix-term/tests/integration.rs | 2 +- helix-term/tests/{integration => test}/auto_indent.rs | 0 helix-term/tests/{integration => test}/auto_pairs.rs | 0 helix-term/tests/{integration => test}/commands.rs | 0 helix-term/tests/{integration => test}/helpers.rs | 0 helix-term/tests/{integration => test}/movement.rs | 0 helix-term/tests/{integration => test}/write.rs | 0 7 files changed, 1 insertion(+), 1 deletion(-) rename helix-term/tests/{integration => test}/auto_indent.rs (100%) rename helix-term/tests/{integration => test}/auto_pairs.rs (100%) rename helix-term/tests/{integration => test}/commands.rs (100%) rename helix-term/tests/{integration => test}/helpers.rs (100%) rename helix-term/tests/{integration => test}/movement.rs (100%) rename helix-term/tests/{integration => test}/write.rs (100%) diff --git a/helix-term/tests/integration.rs b/helix-term/tests/integration.rs index 061bd8ff..376bc88b 100644 --- a/helix-term/tests/integration.rs +++ b/helix-term/tests/integration.rs @@ -1,5 +1,5 @@ #[cfg(feature = "integration")] -mod integration { +mod test { mod helpers; use std::path::PathBuf; diff --git a/helix-term/tests/integration/auto_indent.rs b/helix-term/tests/test/auto_indent.rs similarity index 100% rename from helix-term/tests/integration/auto_indent.rs rename to helix-term/tests/test/auto_indent.rs diff --git a/helix-term/tests/integration/auto_pairs.rs b/helix-term/tests/test/auto_pairs.rs similarity index 100% rename from helix-term/tests/integration/auto_pairs.rs rename to helix-term/tests/test/auto_pairs.rs diff --git a/helix-term/tests/integration/commands.rs b/helix-term/tests/test/commands.rs similarity index 100% rename from helix-term/tests/integration/commands.rs rename to helix-term/tests/test/commands.rs diff --git a/helix-term/tests/integration/helpers.rs b/helix-term/tests/test/helpers.rs similarity index 100% rename from helix-term/tests/integration/helpers.rs rename to helix-term/tests/test/helpers.rs diff --git a/helix-term/tests/integration/movement.rs b/helix-term/tests/test/movement.rs similarity index 100% rename from helix-term/tests/integration/movement.rs rename to helix-term/tests/test/movement.rs diff --git a/helix-term/tests/integration/write.rs b/helix-term/tests/test/write.rs similarity index 100% rename from helix-term/tests/integration/write.rs rename to helix-term/tests/test/write.rs From 374724f5ac23b4b99022b4de58bcb17a1a3f99d5 Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Sat, 30 Apr 2022 22:34:52 -0400 Subject: [PATCH 23/31] ignore failing write path tests until fixes are merged --- helix-term/tests/test/commands.rs | 6 ++++-- helix-term/tests/test/write.rs | 3 +++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/helix-term/tests/test/commands.rs b/helix-term/tests/test/commands.rs index 1ff7cc90..cea31e25 100644 --- a/helix-term/tests/test/commands.rs +++ b/helix-term/tests/test/commands.rs @@ -9,6 +9,7 @@ use helix_term::application::Application; use super::*; #[tokio::test] +#[ignore] async fn test_write_quit_fail() -> anyhow::Result<()> { let file = helpers::new_readonly_tempfile()?; @@ -31,7 +32,8 @@ async fn test_write_quit_fail() -> anyhow::Result<()> { } #[tokio::test] -async fn test_buffer_close() -> anyhow::Result<()> { +#[ignore] +async fn test_buffer_close_concurrent() -> anyhow::Result<()> { test_key_sequences( &mut Application::new(Args::default(), Config::default())?, vec![ @@ -63,7 +65,7 @@ async fn test_buffer_close() -> anyhow::Result<()> { // verify if writes are queued up, it finishes them before closing the buffer let mut file = tempfile::NamedTempFile::new()?; let mut command = String::new(); - const RANGE: RangeInclusive = 1..=10; + const RANGE: RangeInclusive = 1..=1000; for i in RANGE { let cmd = format!("%c{}:w", i); diff --git a/helix-term/tests/test/write.rs b/helix-term/tests/test/write.rs index f3abbd91..3d724af5 100644 --- a/helix-term/tests/test/write.rs +++ b/helix-term/tests/test/write.rs @@ -40,6 +40,7 @@ async fn test_write() -> anyhow::Result<()> { } #[tokio::test] +#[ignore] async fn test_write_concurrent() -> anyhow::Result<()> { let mut file = tempfile::NamedTempFile::new()?; let mut command = String::new(); @@ -74,6 +75,7 @@ async fn test_write_concurrent() -> anyhow::Result<()> { } #[tokio::test] +#[ignore] async fn test_write_fail_mod_flag() -> anyhow::Result<()> { let file = helpers::new_readonly_tempfile()?; @@ -117,6 +119,7 @@ async fn test_write_fail_mod_flag() -> anyhow::Result<()> { } #[tokio::test] +#[ignore] async fn test_write_fail_new_path() -> anyhow::Result<()> { let file = helpers::new_readonly_tempfile()?; From 526c9be8cadf99519e8f6a9911b3784ab7f2e142 Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Mon, 16 May 2022 22:28:12 -0400 Subject: [PATCH 24/31] consolidate idle timer logic, make conditional --- helix-term/src/application.rs | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index c611a691..15b44a85 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -251,6 +251,9 @@ impl Application { where S: Stream> + Unpin, { + #[cfg(feature = "integration")] + let mut idle_handled = false; + loop { if self.editor.should_close() { return false; @@ -263,11 +266,9 @@ impl Application { Some(event) = input_stream.next() => { self.handle_terminal_events(event); - self.editor.reset_idle_timer(); } Some(signal) = self.signals.next() => { self.handle_signals(signal).await; - self.editor.reset_idle_timer(); } Some((id, call)) = self.editor.language_servers.incoming.next() => { self.handle_language_server_message(call, id).await; @@ -278,37 +279,46 @@ impl Application { self.render(); self.last_render = Instant::now(); } - - self.editor.reset_idle_timer(); } Some(payload) = self.editor.debugger_events.next() => { let needs_render = self.editor.handle_debugger_message(payload).await; if needs_render { self.render(); } - self.editor.reset_idle_timer(); } Some(config_event) = self.editor.config_events.1.recv() => { self.handle_config_events(config_event); self.render(); - self.editor.reset_idle_timer(); } Some(callback) = self.jobs.futures.next() => { self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback); self.render(); - self.editor.reset_idle_timer(); } Some(callback) = self.jobs.wait_futures.next() => { self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback); self.render(); - self.editor.reset_idle_timer(); } _ = &mut self.editor.idle_timer => { // idle timeout self.editor.clear_idle_timer(); self.handle_idle_timeout(); + + #[cfg(feature = "integration")] + { + idle_handled = true; + } + } + } + + // for integration tests only, reset the idle timer after every + // event to make a signal when test events are done processing + #[cfg(feature = "integration")] + { + if idle_handled { return true; } + + self.editor.reset_idle_timer(); } } } From 7c0bca186cdacf070355c1a4ab82121d6a4d2e27 Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Sun, 22 May 2022 13:29:52 -0400 Subject: [PATCH 25/31] rename test helpers --- helix-term/tests/integration.rs | 8 +-- helix-term/tests/test/auto_indent.rs | 2 +- helix-term/tests/test/auto_pairs.rs | 9 +-- helix-term/tests/test/helpers.rs | 13 ++-- helix-term/tests/test/movement.rs | 103 +++++++++------------------ 5 files changed, 45 insertions(+), 90 deletions(-) diff --git a/helix-term/tests/integration.rs b/helix-term/tests/integration.rs index 376bc88b..11bc4e4c 100644 --- a/helix-term/tests/integration.rs +++ b/helix-term/tests/integration.rs @@ -13,13 +13,7 @@ mod test { #[tokio::test] async fn hello_world() -> anyhow::Result<()> { - test_key_sequence_text_result( - Args::default(), - Config::default(), - ("#[\n|]#", "ihello world", "hello world#[|\n]#"), - ) - .await?; - + test(("#[\n|]#", "ihello world", "hello world#[|\n]#")).await?; Ok(()) } diff --git a/helix-term/tests/test/auto_indent.rs b/helix-term/tests/test/auto_indent.rs index 8933cb6a..2f638893 100644 --- a/helix-term/tests/test/auto_indent.rs +++ b/helix-term/tests/test/auto_indent.rs @@ -2,7 +2,7 @@ use super::*; #[tokio::test] async fn auto_indent_c() -> anyhow::Result<()> { - test_key_sequence_text_result( + test_with_config( Args { files: vec![(PathBuf::from("foo.c"), Position::default())], ..Default::default() diff --git a/helix-term/tests/test/auto_pairs.rs b/helix-term/tests/test/auto_pairs.rs index 52fee55e..ec47a5b4 100644 --- a/helix-term/tests/test/auto_pairs.rs +++ b/helix-term/tests/test/auto_pairs.rs @@ -2,14 +2,9 @@ use super::*; #[tokio::test] async fn auto_pairs_basic() -> anyhow::Result<()> { - test_key_sequence_text_result( - Args::default(), - Config::default(), - ("#[\n|]#", "i(", "(#[|)]#\n"), - ) - .await?; + test(("#[\n|]#", "i(", "(#[|)]#\n")).await?; - test_key_sequence_text_result( + test_with_config( Args::default(), Config { editor: helix_view::editor::Config { diff --git a/helix-term/tests/test/helpers.rs b/helix-term/tests/test/helpers.rs index 3fe1934f..2bebe31b 100644 --- a/helix-term/tests/test/helpers.rs +++ b/helix-term/tests/test/helpers.rs @@ -41,6 +41,7 @@ pub async fn test_key_sequence( test_key_sequences(app, vec![(in_keys, test_fn)]).await } +#[allow(clippy::type_complexity)] pub async fn test_key_sequences( app: &mut Application, inputs: Vec<(Option<&str>, Option<&dyn Fn(&Application)>)>, @@ -51,7 +52,7 @@ pub async fn test_key_sequences( for (in_keys, test_fn) in inputs { if let Some(in_keys) = in_keys { - for key_event in parse_macro(&in_keys)?.into_iter() { + for key_event in parse_macro(in_keys)?.into_iter() { tx.send(Ok(Event::Key(KeyEvent::from(key_event))))?; } } @@ -92,7 +93,7 @@ pub async fn test_key_sequence_with_input_text>( // replace the initial text with the input text doc.apply( - &Transaction::change_by_selection(&doc.text(), &sel, |_| { + &Transaction::change_by_selection(doc.text(), &sel, |_| { (0, doc.text().len_chars(), Some((&test_case.in_text).into())) }) .with_selection(test_case.in_selection.clone()), @@ -105,7 +106,7 @@ pub async fn test_key_sequence_with_input_text>( /// Use this for very simple test cases where there is one input /// document, selection, and sequence of key presses, and you just /// want to verify the resulting document and selection. -pub async fn test_key_sequence_text_result>( +pub async fn test_with_config>( args: Args, config: Config, test_case: T, @@ -126,6 +127,10 @@ pub async fn test_key_sequence_text_result>( .await } +pub async fn test>(test_case: T) -> anyhow::Result<()> { + test_with_config(Args::default(), Config::default(), test_case).await +} + pub fn temp_file_with_contents>( content: S, ) -> anyhow::Result { @@ -148,7 +153,7 @@ pub fn platform_line(input: &str) -> String { // we can assume that the source files in this code base will always // be LF, so indoc strings will always insert LF - let mut output = input.replace("\n", line_end); + let mut output = input.replace('\n', line_end); if !output.ends_with(line_end) { output.push_str(line_end); diff --git a/helix-term/tests/test/movement.rs b/helix-term/tests/test/movement.rs index e0bfc3bf..5fb2ce25 100644 --- a/helix-term/tests/test/movement.rs +++ b/helix-term/tests/test/movement.rs @@ -4,39 +4,18 @@ use super::*; #[tokio::test] async fn insert_mode_cursor_position() -> anyhow::Result<()> { - test_key_sequence_text_result( - Args::default(), - Config::default(), - TestCase { - in_text: String::new(), - in_selection: Selection::single(0, 0), - in_keys: "i".into(), - out_text: String::new(), - out_selection: Selection::single(0, 0), - }, - ) - .await?; - - test_key_sequence_text_result( - Args::default(), - Config::default(), - ("#[\n|]#", "i", "#[|\n]#"), - ) - .await?; - - test_key_sequence_text_result( - Args::default(), - Config::default(), - ("#[\n|]#", "i", "#[|\n]#"), - ) + test(TestCase { + in_text: String::new(), + in_selection: Selection::single(0, 0), + in_keys: "i".into(), + out_text: String::new(), + out_selection: Selection::single(0, 0), + }) .await?; - test_key_sequence_text_result( - Args::default(), - Config::default(), - ("#[\n|]#", "ii", "#[|\n]#"), - ) - .await?; + test(("#[\n|]#", "i", "#[|\n]#")).await?; + test(("#[\n|]#", "i", "#[|\n]#")).await?; + test(("#[\n|]#", "ii", "#[|\n]#")).await?; Ok(()) } @@ -44,62 +23,44 @@ async fn insert_mode_cursor_position() -> anyhow::Result<()> { /// Range direction is preserved when escaping insert mode to normal #[tokio::test] async fn insert_to_normal_mode_cursor_position() -> anyhow::Result<()> { - test_key_sequence_text_result( - Args::default(), - Config::default(), - ("#[f|]#oo\n", "vll", "#[|foo]#\n"), - ) - .await?; - - test_key_sequence_text_result( - Args::default(), - Config::default(), - ( - indoc! {"\ + test(("#[f|]#oo\n", "vll", "#[|foo]#\n")).await?; + test(( + indoc! {"\ #[f|]#oo #(b|)#ar" - }, - "vll", - indoc! {"\ + }, + "vll", + indoc! {"\ #[|foo]# #(|bar)#" - }, - ), - ) + }, + )) .await?; - test_key_sequence_text_result( - Args::default(), - Config::default(), - ( - indoc! {"\ + test(( + indoc! {"\ #[f|]#oo #(b|)#ar" - }, - "a", - indoc! {"\ + }, + "a", + indoc! {"\ #[fo|]#o #(ba|)#r" - }, - ), - ) + }, + )) .await?; - test_key_sequence_text_result( - Args::default(), - Config::default(), - ( - indoc! {"\ + test(( + indoc! {"\ #[f|]#oo #(b|)#ar" - }, - "a", - indoc! {"\ + }, + "a", + indoc! {"\ #[f|]#oo #(b|)#ar" - }, - ), - ) + }, + )) .await?; Ok(()) From fac36bc5eab804e823ddef01e50d1e36495c7967 Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Mon, 23 May 2022 00:24:18 -0400 Subject: [PATCH 26/31] add test for write-quit happy path --- helix-term/tests/test/commands.rs | 3 ++ helix-term/tests/test/helpers.rs | 63 ++++++++++++++++++++++--------- helix-term/tests/test/write.rs | 37 ++++++++++++++++++ 3 files changed, 85 insertions(+), 18 deletions(-) diff --git a/helix-term/tests/test/commands.rs b/helix-term/tests/test/commands.rs index cea31e25..27b4da58 100644 --- a/helix-term/tests/test/commands.rs +++ b/helix-term/tests/test/commands.rs @@ -25,6 +25,7 @@ async fn test_write_quit_fail() -> anyhow::Result<()> { Some(&|app| { assert_eq!(&Severity::Error, app.editor.get_status().unwrap().1); }), + false, ) .await?; @@ -59,6 +60,7 @@ async fn test_buffer_close_concurrent() -> anyhow::Result<()> { }), ), ], + false, ) .await?; @@ -89,6 +91,7 @@ async fn test_buffer_close_concurrent() -> anyhow::Result<()> { let doc = app.editor.document_by_path(file.path()); assert!(doc.is_none(), "found doc: {:?}", doc); }), + false, ) .await?; diff --git a/helix-term/tests/test/helpers.rs b/helix-term/tests/test/helpers.rs index 2bebe31b..894fb674 100644 --- a/helix-term/tests/test/helpers.rs +++ b/helix-term/tests/test/helpers.rs @@ -37,41 +37,56 @@ pub async fn test_key_sequence( app: &mut Application, in_keys: Option<&str>, test_fn: Option<&dyn Fn(&Application)>, + should_exit: bool, ) -> anyhow::Result<()> { - test_key_sequences(app, vec![(in_keys, test_fn)]).await + test_key_sequences(app, vec![(in_keys, test_fn)], should_exit).await } #[allow(clippy::type_complexity)] pub async fn test_key_sequences( app: &mut Application, inputs: Vec<(Option<&str>, Option<&dyn Fn(&Application)>)>, + should_exit: bool, ) -> anyhow::Result<()> { const TIMEOUT: Duration = Duration::from_millis(500); let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); let mut rx_stream = UnboundedReceiverStream::new(rx); + let num_inputs = inputs.len(); - for (in_keys, test_fn) in inputs { + for (i, (in_keys, test_fn)) in inputs.into_iter().enumerate() { if let Some(in_keys) = in_keys { for key_event in parse_macro(in_keys)?.into_iter() { tx.send(Ok(Event::Key(KeyEvent::from(key_event))))?; } } - if !app.event_loop_until_idle(&mut rx_stream).await { + let app_exited = !app.event_loop_until_idle(&mut rx_stream).await; + + // the app should not exit from any test until the last one + if i < num_inputs - 1 && app_exited { bail!("application exited before test function could run"); } + // verify if it exited on the last iteration if it should have and + // the inverse + if i == num_inputs - 1 && app_exited != should_exit { + bail!("expected app to exit: {} != {}", app_exited, should_exit); + } + if let Some(test) = test_fn { test(app); }; } - for key_event in parse_macro(":q!")?.into_iter() { - tx.send(Ok(Event::Key(KeyEvent::from(key_event))))?; + if !should_exit { + for key_event in parse_macro(":q!")?.into_iter() { + tx.send(Ok(Event::Key(KeyEvent::from(key_event))))?; + } + + let event_loop = app.event_loop(&mut rx_stream); + tokio::time::timeout(TIMEOUT, event_loop).await?; } - let event_loop = app.event_loop(&mut rx_stream); - tokio::time::timeout(TIMEOUT, event_loop).await?; app.close().await?; Ok(()) @@ -81,6 +96,7 @@ pub async fn test_key_sequence_with_input_text>( app: Option, test_case: T, test_fn: &dyn Fn(&Application), + should_exit: bool, ) -> anyhow::Result<()> { let test_case = test_case.into(); let mut app = match app { @@ -100,7 +116,13 @@ pub async fn test_key_sequence_with_input_text>( view.id, ); - test_key_sequence(&mut app, Some(&test_case.in_keys), Some(test_fn)).await + test_key_sequence( + &mut app, + Some(&test_case.in_keys), + Some(test_fn), + should_exit, + ) + .await } /// Use this for very simple test cases where there is one input @@ -114,16 +136,21 @@ pub async fn test_with_config>( let test_case = test_case.into(); let app = Application::new(args, config)?; - test_key_sequence_with_input_text(Some(app), test_case.clone(), &|app| { - let doc = doc!(app.editor); - assert_eq!(&test_case.out_text, doc.text()); - - let mut selections: Vec<_> = doc.selections().values().cloned().collect(); - assert_eq!(1, selections.len()); - - let sel = selections.pop().unwrap(); - assert_eq!(test_case.out_selection, sel); - }) + test_key_sequence_with_input_text( + Some(app), + test_case.clone(), + &|app| { + let doc = doc!(app.editor); + assert_eq!(&test_case.out_text, doc.text()); + + let mut selections: Vec<_> = doc.selections().values().cloned().collect(); + assert_eq!(1, selections.len()); + + let sel = selections.pop().unwrap(); + assert_eq!(test_case.out_selection, sel); + }, + false, + ) .await } diff --git a/helix-term/tests/test/write.rs b/helix-term/tests/test/write.rs index 3d724af5..d22b3125 100644 --- a/helix-term/tests/test/write.rs +++ b/helix-term/tests/test/write.rs @@ -23,6 +23,7 @@ async fn test_write() -> anyhow::Result<()> { )?, Some("ii can eat glass, it will not hurt me:w"), None, + false, ) .await?; @@ -31,6 +32,39 @@ async fn test_write() -> anyhow::Result<()> { let mut file_content = String::new(); file.as_file_mut().read_to_string(&mut file_content)?; + + assert_eq!( + helpers::platform_line("i can eat glass, it will not hurt me"), + file_content + ); + + Ok(()) +} + +#[tokio::test] +async fn test_write_quit() -> anyhow::Result<()> { + let mut file = tempfile::NamedTempFile::new()?; + + test_key_sequence( + &mut Application::new( + Args { + files: vec![(file.path().to_path_buf(), Position::default())], + ..Default::default() + }, + Config::default(), + )?, + Some("ii can eat glass, it will not hurt me:wq"), + None, + true, + ) + .await?; + + file.as_file_mut().flush()?; + file.as_file_mut().sync_all()?; + + let mut file_content = String::new(); + file.as_file_mut().read_to_string(&mut file_content)?; + assert_eq!( helpers::platform_line("i can eat glass, it will not hurt me"), file_content @@ -61,6 +95,7 @@ async fn test_write_concurrent() -> anyhow::Result<()> { )?, Some(&command), None, + false, ) .await?; @@ -112,6 +147,7 @@ async fn test_write_fail_mod_flag() -> anyhow::Result<()> { }), ), ], + false, ) .await?; @@ -149,6 +185,7 @@ async fn test_write_fail_new_path() -> anyhow::Result<()> { }), ), ], + false, ) .await?; From 41bf1d581137855596e00ad7702e8827325714b0 Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Thu, 31 Mar 2022 10:58:50 -0400 Subject: [PATCH 27/31] fix(command): write-quit: do not quit if write fails During write-quit, if the file fails to be written for any reason, helix will still quit without saving the changes. This fixes this behavior by introducing fallibility to the asynchronous job queues. This will also benefit all contexts which may depend on these job queues. Fixes #1575 --- helix-term/src/application.rs | 2 +- helix-term/src/commands/typed.rs | 2 ++ helix-term/src/job.rs | 20 ++++++++++++++++---- helix-term/tests/test/commands.rs | 1 - 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 15b44a85..2790c9a4 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -814,7 +814,7 @@ impl Application { } pub async fn close(&mut self) -> anyhow::Result<()> { - self.jobs.finish().await; + self.jobs.finish().await?; if self.editor.close_language_servers(None).await.is_err() { log::error!("Timed out waiting for language servers to shutdown"); diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 3c88b0ce..5b48ca48 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -233,6 +233,7 @@ fn write_impl( doc.detect_language(cx.editor.syn_loader.clone()); let _ = cx.editor.refresh_language_server(id); } + Ok(()) } @@ -422,6 +423,7 @@ fn write_quit( event: PromptEvent, ) -> anyhow::Result<()> { write_impl(cx, args.first(), false)?; + helix_lsp::block_on(cx.jobs.finish())?; quit(cx, &[], event) } diff --git a/helix-term/src/job.rs b/helix-term/src/job.rs index d21099f7..e5147992 100644 --- a/helix-term/src/job.rs +++ b/helix-term/src/job.rs @@ -2,7 +2,7 @@ use helix_view::Editor; use crate::compositor::Compositor; -use futures_util::future::{self, BoxFuture, Future, FutureExt}; +use futures_util::future::{BoxFuture, Future, FutureExt}; use futures_util::stream::{FuturesUnordered, StreamExt}; pub type Callback = Box; @@ -93,9 +93,21 @@ impl Jobs { } /// Blocks until all the jobs that need to be waited on are done. - pub async fn finish(&mut self) { - let wait_futures = std::mem::take(&mut self.wait_futures); + pub async fn finish(&mut self) -> anyhow::Result<()> { log::debug!("waiting on jobs..."); - wait_futures.for_each(|_| future::ready(())).await + let mut wait_futures = std::mem::take(&mut self.wait_futures); + while let (Some(job), tail) = wait_futures.into_future().await { + match job { + Ok(_) => { + wait_futures = tail; + } + Err(e) => { + self.wait_futures = tail; + return Err(e); + } + } + } + + Ok(()) } } diff --git a/helix-term/tests/test/commands.rs b/helix-term/tests/test/commands.rs index 27b4da58..01f13c5c 100644 --- a/helix-term/tests/test/commands.rs +++ b/helix-term/tests/test/commands.rs @@ -9,7 +9,6 @@ use helix_term::application::Application; use super::*; #[tokio::test] -#[ignore] async fn test_write_quit_fail() -> anyhow::Result<()> { let file = helpers::new_readonly_tempfile()?; From 086b63ab1b5f004400721ef876b6f1441783f104 Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Thu, 2 Jun 2022 00:13:08 -0400 Subject: [PATCH 28/31] add integration-test cargo alias --- .cargo/config | 1 + .github/workflows/build.yml | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.cargo/config b/.cargo/config index 35049cbc..5d615566 100644 --- a/.cargo/config +++ b/.cargo/config @@ -1,2 +1,3 @@ [alias] xtask = "run --package xtask --" +integration-test = "test --features integration --workspace --test integration" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9d0383a7..b24cdb8c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -64,7 +64,12 @@ jobs: uses: actions-rs/cargo@v1 with: command: test - args: --workspace --features integration + args: --workspace + + - name: Run cargo integration-test + uses: actions-rs/cargo@v1 + with: + command: integration-test strategy: matrix: From 65bf6836b71e309a43d39b75a3fac7446604592b Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Sat, 4 Jun 2022 16:08:34 -0400 Subject: [PATCH 29/31] update docs for integration tests --- docs/CONTRIBUTING.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index bdd771aa..6da50fdd 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -35,3 +35,10 @@ to `cargo install` anything either). [architecture.md]: ./architecture.md [docs]: https://docs.helix-editor.com/ [xtask]: https://github.com/matklad/cargo-xtask + +# Integration tests + +Integration tests for helix-term can be run with `cargo integration-test`. Code +contributors are strongly encouraged to write integration tests for their code. +Existing tests can be used as examples. Helpers can be found in +[helpers.rs][../helix-term/tests/test/helpers.rs]. From 665286c199b344c0bd65772156b5e460ff11d768 Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Fri, 10 Jun 2022 23:35:34 -0400 Subject: [PATCH 30/31] factor new Application with file arg to function --- helix-term/src/application.rs | 4 +--- helix-term/tests/test/commands.rs | 16 ++-------------- helix-term/tests/test/helpers.rs | 14 +++++++++++++- helix-term/tests/test/movement.rs | 11 +---------- helix-term/tests/test/write.rs | 32 ++++--------------------------- 5 files changed, 21 insertions(+), 56 deletions(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 2790c9a4..48e9c275 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -239,11 +239,9 @@ impl Application { self.last_render = Instant::now(); loop { - if self.editor.should_close() { + if !self.event_loop_until_idle(input_stream).await { break; } - - self.event_loop_until_idle(input_stream).await; } } diff --git a/helix-term/tests/test/commands.rs b/helix-term/tests/test/commands.rs index 01f13c5c..0cd79bc7 100644 --- a/helix-term/tests/test/commands.rs +++ b/helix-term/tests/test/commands.rs @@ -13,13 +13,7 @@ async fn test_write_quit_fail() -> anyhow::Result<()> { let file = helpers::new_readonly_tempfile()?; test_key_sequence( - &mut Application::new( - Args { - files: vec![(file.path().to_path_buf(), Position::default())], - ..Default::default() - }, - Config::default(), - )?, + &mut helpers::app_with_file(file.path())?, Some("ihello:wq"), Some(&|app| { assert_eq!(&Severity::Error, app.editor.get_status().unwrap().1); @@ -76,13 +70,7 @@ async fn test_buffer_close_concurrent() -> anyhow::Result<()> { command.push_str(":bufferclose"); test_key_sequence( - &mut Application::new( - Args { - files: vec![(file.path().to_path_buf(), Position::default())], - ..Default::default() - }, - Config::default(), - )?, + &mut helpers::app_with_file(file.path())?, Some(&command), Some(&|app| { assert!(!app.editor.is_err(), "error: {:?}", app.editor.get_status()); diff --git a/helix-term/tests/test/helpers.rs b/helix-term/tests/test/helpers.rs index 894fb674..8f2501e6 100644 --- a/helix-term/tests/test/helpers.rs +++ b/helix-term/tests/test/helpers.rs @@ -1,4 +1,4 @@ -use std::{io::Write, time::Duration}; +use std::{io::Write, path::PathBuf, time::Duration}; use anyhow::bail; use crossterm::event::{Event, KeyEvent}; @@ -199,3 +199,15 @@ pub fn new_readonly_tempfile() -> anyhow::Result { file.as_file_mut().set_permissions(perms)?; Ok(file) } + +/// Creates a new Application with default config that opens the given file +/// path +pub fn app_with_file>(path: P) -> anyhow::Result { + Application::new( + Args { + files: vec![(path.into(), helix_core::Position::default())], + ..Default::default() + }, + Config::default(), + ) +} diff --git a/helix-term/tests/test/movement.rs b/helix-term/tests/test/movement.rs index 5fb2ce25..088685df 100644 --- a/helix-term/tests/test/movement.rs +++ b/helix-term/tests/test/movement.rs @@ -1,5 +1,3 @@ -use helix_term::application::Application; - use super::*; #[tokio::test] @@ -72,14 +70,7 @@ async fn insert_to_normal_mode_cursor_position() -> anyhow::Result<()> { async fn cursor_position_newly_opened_file() -> anyhow::Result<()> { let test = |content: &str, expected_sel: Selection| -> anyhow::Result<()> { let file = helpers::temp_file_with_contents(content)?; - - let mut app = Application::new( - Args { - files: vec![(file.path().to_path_buf(), Position::default())], - ..Default::default() - }, - Config::default(), - )?; + let mut app = helpers::app_with_file(file.path())?; let (view, doc) = helix_view::current!(app.editor); let sel = doc.selection(view.id).clone(); diff --git a/helix-term/tests/test/write.rs b/helix-term/tests/test/write.rs index d22b3125..39efa2ce 100644 --- a/helix-term/tests/test/write.rs +++ b/helix-term/tests/test/write.rs @@ -14,13 +14,7 @@ async fn test_write() -> anyhow::Result<()> { let mut file = tempfile::NamedTempFile::new()?; test_key_sequence( - &mut Application::new( - Args { - files: vec![(file.path().to_path_buf(), Position::default())], - ..Default::default() - }, - Config::default(), - )?, + &mut helpers::app_with_file(file.path())?, Some("ii can eat glass, it will not hurt me:w"), None, false, @@ -46,13 +40,7 @@ async fn test_write_quit() -> anyhow::Result<()> { let mut file = tempfile::NamedTempFile::new()?; test_key_sequence( - &mut Application::new( - Args { - files: vec![(file.path().to_path_buf(), Position::default())], - ..Default::default() - }, - Config::default(), - )?, + &mut helpers::app_with_file(file.path())?, Some("ii can eat glass, it will not hurt me:wq"), None, true, @@ -86,13 +74,7 @@ async fn test_write_concurrent() -> anyhow::Result<()> { } test_key_sequence( - &mut Application::new( - Args { - files: vec![(file.path().to_path_buf(), Position::default())], - ..Default::default() - }, - Config::default(), - )?, + &mut helpers::app_with_file(file.path())?, Some(&command), None, false, @@ -115,13 +97,7 @@ async fn test_write_fail_mod_flag() -> anyhow::Result<()> { let file = helpers::new_readonly_tempfile()?; test_key_sequences( - &mut Application::new( - Args { - files: vec![(file.path().to_path_buf(), Position::default())], - ..Default::default() - }, - Config::default(), - )?, + &mut helpers::app_with_file(file.path())?, vec![ ( None, From 5f7c247430998fabceb55d4689118dd75e2bdfb1 Mon Sep 17 00:00:00 2001 From: Skyler Hawthorne Date: Mon, 13 Jun 2022 22:18:17 -0400 Subject: [PATCH 31/31] replace phrase in tests --- helix-term/tests/test/write.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/helix-term/tests/test/write.rs b/helix-term/tests/test/write.rs index 39efa2ce..8869d881 100644 --- a/helix-term/tests/test/write.rs +++ b/helix-term/tests/test/write.rs @@ -15,7 +15,7 @@ async fn test_write() -> anyhow::Result<()> { test_key_sequence( &mut helpers::app_with_file(file.path())?, - Some("ii can eat glass, it will not hurt me:w"), + Some("ithe gostak distims the doshes:w"), None, false, ) @@ -28,7 +28,7 @@ async fn test_write() -> anyhow::Result<()> { file.as_file_mut().read_to_string(&mut file_content)?; assert_eq!( - helpers::platform_line("i can eat glass, it will not hurt me"), + helpers::platform_line("the gostak distims the doshes"), file_content ); @@ -41,7 +41,7 @@ async fn test_write_quit() -> anyhow::Result<()> { test_key_sequence( &mut helpers::app_with_file(file.path())?, - Some("ii can eat glass, it will not hurt me:wq"), + Some("ithe gostak distims the doshes:wq"), None, true, ) @@ -54,7 +54,7 @@ async fn test_write_quit() -> anyhow::Result<()> { file.as_file_mut().read_to_string(&mut file_content)?; assert_eq!( - helpers::platform_line("i can eat glass, it will not hurt me"), + helpers::platform_line("the gostak distims the doshes"), file_content );