use helix_term::application::Application; use super::*; mod insert; mod movement; mod write; #[tokio::test(flavor = "multi_thread")] async fn test_selection_duplication() -> anyhow::Result<()> { // Forward test(( indoc! {"\ #[lo|]#rem ipsum dolor "}, "CC", indoc! {"\ #(lo|)#rem #(ip|)#sum #[do|]#lor "}, )) .await?; // Backward test(( indoc! {"\ #[|lo]#rem ipsum dolor "}, "CC", indoc! {"\ #(|lo)#rem #(|ip)#sum #[|do]#lor "}, )) .await?; // Copy the selection to previous line, skipping the first line in the file test(( indoc! {"\ test #[testitem|]# "}, "", indoc! {"\ test #[testitem|]# "}, )) .await?; // Copy the selection to previous line, including the first line in the file test(( indoc! {"\ test #[test|]# "}, "", indoc! {"\ #[test|]# #(test|)# "}, )) .await?; // Copy the selection to next line, skipping the last line in the file test(( indoc! {"\ #[testitem|]# test "}, "C", indoc! {"\ #[testitem|]# test "}, )) .await?; // Copy the selection to next line, including the last line in the file test(( indoc! {"\ #[test|]# test "}, "C", indoc! {"\ #(test|)# #[test|]# "}, )) .await?; Ok(()) } #[tokio::test(flavor = "multi_thread")] async fn test_goto_file_impl() -> anyhow::Result<()> { let file = tempfile::NamedTempFile::new()?; fn match_paths(app: &Application, matches: Vec<&str>) -> usize { app.editor .documents() .filter_map(|d| d.path()?.file_name()) .filter(|n| matches.iter().any(|m| *m == n.to_string_lossy())) .count() } // Single selection test_key_sequence( &mut AppBuilder::new().with_file(file.path(), None).build()?, Some("ione.js%gf"), Some(&|app| { assert_eq!(1, match_paths(app, vec!["one.js"])); }), false, ) .await?; // Multiple selection test_key_sequence( &mut AppBuilder::new().with_file(file.path(), None).build()?, Some("ione.jstwo.js%gf"), Some(&|app| { assert_eq!(2, match_paths(app, vec!["one.js", "two.js"])); }), false, ) .await?; // Cursor on first quote test_key_sequence( &mut AppBuilder::new().with_file(file.path(), None).build()?, Some("iimport 'one.js'B;gf"), Some(&|app| { assert_eq!(1, match_paths(app, vec!["one.js"])); }), false, ) .await?; // Cursor on last quote test_key_sequence( &mut AppBuilder::new().with_file(file.path(), None).build()?, Some("iimport 'one.js'bgf"), Some(&|app| { assert_eq!(1, match_paths(app, vec!["one.js"])); }), false, ) .await?; // ';' is behind the path test_key_sequence( &mut AppBuilder::new().with_file(file.path(), None).build()?, Some("iimport 'one.js';B;gf"), Some(&|app| { assert_eq!(1, match_paths(app, vec!["one.js"])); }), false, ) .await?; // allow numeric values in path test_key_sequence( &mut AppBuilder::new().with_file(file.path(), None).build()?, Some("iimport 'one123.js'B;gf"), Some(&|app| { assert_eq!(1, match_paths(app, vec!["one123.js"])); }), false, ) .await?; Ok(()) } #[tokio::test(flavor = "multi_thread")] async fn test_multi_selection_paste() -> anyhow::Result<()> { test(( indoc! {"\ #[|lorem]# #(|ipsum)# #(|dolor)# "}, "yp", indoc! {"\ lorem#[|lorem]# ipsum#(|ipsum)# dolor#(|dolor)# "}, )) .await?; Ok(()) } #[tokio::test(flavor = "multi_thread")] async fn test_multi_selection_shell_commands() -> anyhow::Result<()> { // pipe test(( indoc! {"\ #[|lorem]# #(|ipsum)# #(|dolor)# "}, "|echo foo", indoc! {"\ #[|foo]# #(|foo)# #(|foo)#" }, )) .await?; // insert-output test(( indoc! {"\ #[|lorem]# #(|ipsum)# #(|dolor)# "}, "!echo foo", indoc! {"\ #[|foo]#lorem #(|foo)#ipsum #(|foo)#dolor "}, )) .await?; // append-output test(( indoc! {"\ #[|lorem]# #(|ipsum)# #(|dolor)# "}, "echo foo", indoc! {"\ lorem#[|foo]# ipsum#(|foo)# dolor#(|foo)# "}, )) .await?; Ok(()) } #[tokio::test(flavor = "multi_thread")] async fn test_undo_redo() -> anyhow::Result<()> { // A jumplist selection is created at a point which is undone. // // * 2[ Add two newlines at line start. We're now on line 3. // * Save the selection on line 3 in the jumplist. // * u Undo the two newlines. We're now on line 1. // * Jump forward an back again in the jumplist. This would panic // if the jumplist were not being updated correctly. test(( "#[|]#", "2[u", "#[|]#", LineFeedHandling::AsIs, )) .await?; // A jumplist selection is passed through an edit and then an undo and then a redo. // // * [ Add a newline at line start. We're now on line 2. // * Save the selection on line 2 in the jumplist. // * kd Delete line 1. The jumplist selection should be adjusted to the new line 1. // * uU Undo and redo the `kd` edit. // * Jump back in the jumplist. This would panic if the jumplist were not being // updated correctly. // * Jump forward to line 1. test(( "#[|]#", "[kduU", "#[|]#", LineFeedHandling::AsIs, )) .await?; // In this case we 'redo' manually to ensure that the transactions are composing correctly. test(( "#[|]#", "[u[u", "#[|]#", LineFeedHandling::AsIs, )) .await?; Ok(()) } #[tokio::test(flavor = "multi_thread")] async fn test_extend_line() -> anyhow::Result<()> { // extend with line selected then count test(( indoc! {"\ #[l|]#orem ipsum dolor "}, "x2x", indoc! {"\ #[lorem ipsum dolor\n|]# "}, )) .await?; // extend with count on partial selection test(( indoc! {"\ #[l|]#orem ipsum "}, "2x", indoc! {"\ #[lorem ipsum\n|]# "}, )) .await?; Ok(()) } #[tokio::test(flavor = "multi_thread")] async fn test_character_info() -> anyhow::Result<()> { // UTF-8, single byte test_key_sequence( &mut helpers::AppBuilder::new().build()?, Some("ihh:char"), Some(&|app| { assert_eq!( r#""h" (U+0068) Dec 104 Hex 68"#, app.editor.get_status().unwrap().0 ); }), false, ) .await?; // UTF-8, multi-byte test_key_sequence( &mut helpers::AppBuilder::new().build()?, Some("iëh:char"), Some(&|app| { assert_eq!( r#""ë" (U+0065 U+0308) Hex 65 + cc 88"#, app.editor.get_status().unwrap().0 ); }), false, ) .await?; // Multiple characters displayed as one, escaped characters test_key_sequence( &mut helpers::AppBuilder::new().build()?, Some(":lineending crlf:char"), Some(&|app| { assert_eq!( r#""\r\n" (U+000d U+000a) Hex 0d + 0a"#, app.editor.get_status().unwrap().0 ); }), false, ) .await?; // Non-UTF-8 test_key_sequence( &mut helpers::AppBuilder::new().build()?, Some(":encoding asciiihh:char"), Some(&|app| { assert_eq!(r#""h" Dec 104 Hex 68"#, app.editor.get_status().unwrap().0); }), false, ) .await?; Ok(()) } #[tokio::test(flavor = "multi_thread")] async fn test_delete_char_backward() -> anyhow::Result<()> { // don't panic when deleting overlapping ranges test(("#(x|)# #[x|]#", "c", "#[\n|]#")).await?; test(( "#( |)##( |)#a#( |)#axx#[x|]#a", "li", "#(a|)##(|a)#xx#[|a]#", )) .await?; Ok(()) } #[tokio::test(flavor = "multi_thread")] async fn test_delete_word_backward() -> anyhow::Result<()> { // don't panic when deleting overlapping ranges test(("fo#[o|]#ba#(r|)#", "a", "#[\n|]#")).await?; Ok(()) } #[tokio::test(flavor = "multi_thread")] async fn test_delete_word_forward() -> anyhow::Result<()> { // don't panic when deleting overlapping ranges test(("fo#[o|]#b#(|ar)#", "i", "fo#[\n|]#")).await?; Ok(()) } #[tokio::test(flavor = "multi_thread")] async fn test_delete_char_forward() -> anyhow::Result<()> { test(( indoc! {"\ #[abc|]#def #(abc|)#ef #(abc|)#f #(abc|)# "}, "a", indoc! {"\ #[abc|]#ef #(abc|)#f #(abc|)# #(abc|)# "}, )) .await?; Ok(()) } #[tokio::test(flavor = "multi_thread")] async fn test_insert_with_indent() -> anyhow::Result<()> { const INPUT: &str = indoc! { " #[f|]#n foo() { if let Some(_) = None { } } fn bar() { } " }; // insert_at_line_start test(( INPUT, ":lang rust%I", indoc! { " #[f|]#n foo() { #(i|)#f let Some(_) = None { #(\n|)# #(}|)# #( |)# #(}|)# #(\n|)# #(f|)#n bar() { #(\n|)# #(}|)# " }, )) .await?; // insert_at_line_end test(( INPUT, ":lang rust%A", indoc! { " fn foo() {#[\n|]# if let Some(_) = None {#(\n|)# #(\n|)# }#(\n|)# #(\n|)# }#(\n|)# #(\n|)# fn bar() {#(\n|)# #(\n|)# }#(\n|)# " }, )) .await?; Ok(()) } #[tokio::test(flavor = "multi_thread")] async fn test_join_selections() -> anyhow::Result<()> { // normal join test(( indoc! {"\ #[a|]#bc def "}, "J", indoc! {"\ #[a|]#bc def "}, )) .await?; // join with empty line test(( indoc! {"\ #[a|]#bc def "}, "JJ", indoc! {"\ #[a|]#bc def "}, )) .await?; // join with additional space in non-empty line test(( indoc! {"\ #[a|]#bc def "}, "JJ", indoc! {"\ #[a|]#bc def "}, )) .await?; Ok(()) } #[tokio::test(flavor = "multi_thread")] async fn test_join_selections_space() -> anyhow::Result<()> { // join with empty lines panic test(( indoc! {"\ #[a b c d e|]# "}, "", indoc! {"\ a#[ |]#b#( |)#c#( |)#d#( |)#e "}, )) .await?; // normal join test(( indoc! {"\ #[a|]#bc def "}, "", indoc! {"\ abc#[ |]#def "}, )) .await?; // join with empty line test(( indoc! {"\ #[a|]#bc def "}, "", indoc! {"\ #[a|]#bc def "}, )) .await?; // join with additional space in non-empty line test(( indoc! {"\ #[a|]#bc def "}, "", indoc! {"\ abc#[ |]#def "}, )) .await?; // join with retained trailing spaces test(( indoc! {"\ #[aaa bb c |]# "}, "", indoc! {"\ aaa #[ |]#bb #( |)#c "}, )) .await?; Ok(()) } #[tokio::test(flavor = "multi_thread")] async fn test_join_selections_comment() -> anyhow::Result<()> { test(( indoc! {"\ /// #[a|]#bc /// def "}, ":lang rustJ", indoc! {"\ /// #[a|]#bc def "}, )) .await?; // Only join if the comment token matches the previous line. test(( indoc! {"\ #[| // a // b /// c /// d e /// f // g]# "}, ":lang rustJ", indoc! {"\ #[| // a b /// c d e f // g]# "}, )) .await?; Ok(()) } #[tokio::test(flavor = "multi_thread")] async fn test_read_file() -> anyhow::Result<()> { let mut file = tempfile::NamedTempFile::new()?; let contents_to_read = "some contents"; let output_file = helpers::temp_file_with_contents(contents_to_read)?; test_key_sequence( &mut helpers::AppBuilder::new() .with_file(file.path(), None) .build()?, Some(&format!(":r {:?}:w", output_file.path())), Some(&|app| { assert!(!app.editor.is_err(), "error: {:?}", app.editor.get_status()); }), false, ) .await?; let expected_contents = LineFeedHandling::Native.apply(contents_to_read); helpers::assert_file_has_content(&mut file, &expected_contents)?; Ok(()) } #[tokio::test(flavor = "multi_thread")] async fn surround_delete() -> anyhow::Result<()> { // Test `surround_delete` when head < anchor test(("(#[| ]#)", "mdm", "#[| ]#")).await?; test(("(#[| ]#)", "md(", "#[| ]#")).await?; Ok(()) } #[tokio::test(flavor = "multi_thread")] async fn surround_replace_ts() -> anyhow::Result<()> { const INPUT: &str = r#"\ fn foo() { if let Some(_) = None { todo!("f#[|o]#o)"); } } "#; test(( INPUT, ":lang rustmrm'", r#"\ fn foo() { if let Some(_) = None { todo!('f#[|o]#o)'); } } "#, )) .await?; test(( INPUT, ":lang rust3mrm[", r#"\ fn foo() { if let Some(_) = None [ todo!("f#[|o]#o)"); ] } "#, )) .await?; test(( INPUT, ":lang rust2mrm{", r#"\ fn foo() { if let Some(_) = None { todo!{"f#[|o]#o)"}; } } "#, )) .await?; test(( indoc! {"\ #[a b c d e|]# f "}, "s\\nr,", "a#[,|]#b#(,|)#c#(,|)#d#(,|)#e\nf\n", )) .await?; Ok(()) }