Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-term/tests/test/commands.rs')
| -rw-r--r-- | helix-term/tests/test/commands.rs | 819 |
1 files changed, 143 insertions, 676 deletions
diff --git a/helix-term/tests/test/commands.rs b/helix-term/tests/test/commands.rs index 90ff4cf0..95bd95b7 100644 --- a/helix-term/tests/test/commands.rs +++ b/helix-term/tests/test/commands.rs @@ -1,30 +1,97 @@ +use std::ops::RangeInclusive; + +use helix_core::diagnostic::Severity; use helix_term::application::Application; use super::*; -mod insert; -mod movement; -mod reverse_selection_contents; -mod rotate_selection_contents; -mod write; +#[tokio::test(flavor = "multi_thread")] +async fn test_write_quit_fail() -> anyhow::Result<()> { + let file = helpers::new_readonly_tempfile()?; + let mut app = helpers::AppBuilder::new() + .with_file(file.path(), None) + .build()?; + + test_key_sequence( + &mut app, + Some("ihello<esc>:wq<ret>"), + Some(&|app| { + let mut docs: Vec<_> = app.editor.documents().collect(); + assert_eq!(1, docs.len()); + + let doc = docs.pop().unwrap(); + assert_eq!(Some(file.path()), doc.path().map(PathBuf::as_path)); + assert_eq!(&Severity::Error, app.editor.get_status().unwrap().1); + }), + false, + ) + .await?; + + Ok(()) +} #[tokio::test(flavor = "multi_thread")] -async fn search_selection_detect_word_boundaries_at_eof() -> anyhow::Result<()> { - // <https://github.com/helix-editor/helix/issues/12609> - test(( - indoc! {"\ - #[o|]#ne - two - three"}, - "gej*h", - indoc! {"\ - one - two - three#[ - |]#"}, - )) +async fn test_buffer_close_concurrent() -> anyhow::Result<()> { + test_key_sequences( + &mut helpers::AppBuilder::new().build()?, + vec![ + ( + None, + Some(&|app| { + assert_eq!(1, app.editor.documents().count()); + assert!(!app.editor.is_err()); + }), + ), + ( + Some("ihello<esc>:new<ret>"), + Some(&|app| { + assert_eq!(2, app.editor.documents().count()); + assert!(!app.editor.is_err()); + }), + ), + ( + Some(":buffer<minus>close<ret>"), + Some(&|app| { + assert_eq!(1, app.editor.documents().count()); + assert!(!app.editor.is_err()); + }), + ), + ], + false, + ) + .await?; + + // 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<i32> = 1..=1000; + + for i in RANGE { + let cmd = format!("%c{}<esc>:w<ret>", i); + command.push_str(&cmd); + } + + command.push_str(":buffer<minus>close<ret>"); + + let mut app = helpers::AppBuilder::new() + .with_file(file.path(), None) + .build()?; + + test_key_sequence( + &mut app, + 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); + }), + false, + ) .await?; + helpers::assert_file_has_content(file.as_file_mut(), &RANGE.end().to_string())?; + Ok(()) } @@ -32,89 +99,37 @@ async fn search_selection_detect_word_boundaries_at_eof() -> anyhow::Result<()> async fn test_selection_duplication() -> anyhow::Result<()> { // Forward test(( - indoc! {"\ + platform_line(indoc! {"\ #[lo|]#rem ipsum dolor - "}, + "}) + .as_str(), "CC", - indoc! {"\ + platform_line(indoc! {"\ #(lo|)#rem #(ip|)#sum #[do|]#lor - "}, + "}) + .as_str(), )) .await?; // Backward test(( - indoc! {"\ + platform_line(indoc! {"\ #[|lo]#rem ipsum dolor - "}, + "}) + .as_str(), "CC", - indoc! {"\ + platform_line(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|]# - "}, - "<A-C>", - indoc! {"\ - test - #[testitem|]# - "}, - )) - .await?; - - // Copy the selection to previous line, including the first line in the file - test(( - indoc! {"\ - test - #[test|]# - "}, - "<A-C>", - 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|]# - "}, + "}) + .as_str(), )) .await?; Ok(()) @@ -176,45 +191,25 @@ async fn test_goto_file_impl() -> anyhow::Result<()> { ) .await?; - // ';' is behind the path - test_key_sequence( - &mut AppBuilder::new().with_file(file.path(), None).build()?, - Some("iimport 'one.js';<esc>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'<esc>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! {"\ + platform_line(indoc! {"\ #[|lorem]# #(|ipsum)# #(|dolor)# - "}, + "}) + .as_str(), "yp", - indoc! {"\ + platform_line(indoc! {"\ lorem#[|lorem]# ipsum#(|ipsum)# dolor#(|dolor)# - "}, + "}) + .as_str(), )) .await?; @@ -225,49 +220,64 @@ async fn test_multi_selection_paste() -> anyhow::Result<()> { async fn test_multi_selection_shell_commands() -> anyhow::Result<()> { // pipe test(( - indoc! {"\ + platform_line(indoc! {"\ #[|lorem]# #(|ipsum)# #(|dolor)# - "}, + "}) + .as_str(), "|echo foo<ret>", - indoc! {"\ - #[|foo]# - #(|foo)# - #(|foo)#" - }, + platform_line(indoc! {"\ + #[|foo + ]# + #(|foo + )# + #(|foo + )# + "}) + .as_str(), )) .await?; // insert-output test(( - indoc! {"\ + platform_line(indoc! {"\ #[|lorem]# #(|ipsum)# #(|dolor)# - "}, + "}) + .as_str(), "!echo foo<ret>", - indoc! {"\ - #[|foo]#lorem - #(|foo)#ipsum - #(|foo)#dolor - "}, + platform_line(indoc! {"\ + #[|foo + ]#lorem + #(|foo + )#ipsum + #(|foo + )#dolor + "}) + .as_str(), )) .await?; // append-output test(( - indoc! {"\ + platform_line(indoc! {"\ #[|lorem]# #(|ipsum)# #(|dolor)# - "}, + "}) + .as_str(), "<A-!>echo foo<ret>", - indoc! {"\ - lorem#[|foo]# - ipsum#(|foo)# - dolor#(|foo)# - "}, + platform_line(indoc! {"\ + lorem#[|foo + ]# + ipsum#(|foo + )# + dolor#(|foo + )# + "}) + .as_str(), )) .await?; @@ -283,13 +293,7 @@ async fn test_undo_redo() -> anyhow::Result<()> { // * u Undo the two newlines. We're now on line 1. // * <C-o><C-i> Jump forward an back again in the jumplist. This would panic // if the jumplist were not being updated correctly. - test(( - "#[|]#", - "2[<space><C-s>u<C-o><C-i>", - "#[|]#", - LineFeedHandling::AsIs, - )) - .await?; + test(("#[|]#", "2[<space><C-s>u<C-o><C-i>", "#[|]#")).await?; // A jumplist selection is passed through an edit and then an undo and then a redo. // @@ -300,547 +304,10 @@ async fn test_undo_redo() -> anyhow::Result<()> { // * <C-o> Jump back in the jumplist. This would panic if the jumplist were not being // updated correctly. // * <C-i> Jump forward to line 1. - test(( - "#[|]#", - "[<space><C-s>kduU<C-o><C-i>", - "#[|]#", - LineFeedHandling::AsIs, - )) - .await?; + test(("#[|]#", "[<space><C-s>kduU<C-o><C-i>", "#[|]#")).await?; // In this case we 'redo' manually to ensure that the transactions are composing correctly. - test(( - "#[|]#", - "[<space>u[<space>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("ih<esc>h:char<ret>"), - 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ë<esc>h:char<ret>"), - 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(":line<minus>ending crlf<ret>:char<ret>"), - 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 ascii<ret>ih<esc>h:char<ret>"), - 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<space><backspace><esc>", "#[\n|]#")).await?; - test(( - "#( |)##( |)#a#( |)#axx#[x|]#a", - "li<backspace><esc>", - "#(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<C-w><esc>", "#[\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<A-d><esc>", "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<del><esc>", - 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<ret>%<A-s>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<ret>%<A-s>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|]# - "}, - "<A-J>", - indoc! {"\ - a#[ |]#b#( |)#c#( |)#d#( |)#e - "}, - )) - .await?; - - // normal join - test(( - indoc! {"\ - #[a|]#bc - def - "}, - "<A-J>", - indoc! {"\ - abc#[ |]#def - "}, - )) - .await?; - - // join with empty line - test(( - indoc! {"\ - #[a|]#bc - - def - "}, - "<A-J>", - indoc! {"\ - #[a|]#bc - def - "}, - )) - .await?; - - // join with additional space in non-empty line - test(( - indoc! {"\ - #[a|]#bc - - def - "}, - "<A-J><A-J>", - indoc! {"\ - abc#[ |]#def - "}, - )) - .await?; - - // join with retained trailing spaces - test(( - indoc! {"\ - #[aaa - - bb - - c |]# - "}, - "<A-J>", - 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 rust<ret>J", - 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 rust<ret>J", - indoc! {"\ - #[| // a b /// c d e f // g]# - "}, - )) - .await?; - - test(( - "#[|\t// Join comments -\t// with indent]#", - ":lang go<ret>J", - "#[|\t// Join comments with indent]#", - )) - .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 {:?}<ret><esc>:w<ret>", 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 { - testing!("f#[|o]#o)"); - } -} -"#; - test(( - INPUT, - ":lang rust<ret>mrm'", - r#"\ -fn foo() { - if let Some(_) = None { - testing!('f#[|o]#o)'); - } -} -"#, - )) - .await?; - - test(( - INPUT, - ":lang rust<ret>3mrm[", - r#"\ -fn foo() { - if let Some(_) = None [ - testing!("f#[|o]#o)"); - ] -} -"#, - )) - .await?; - - test(( - INPUT, - ":lang rust<ret>2mrm{", - r#"\ -fn foo() { - if let Some(_) = None { - testing!{"f#[|o]#o)"}; - } -} -"#, - )) - .await?; - - test(( - indoc! {"\ - #[a - b - c - d - e|]# - f - "}, - "s\\n<ret>r,", - "a#[,|]#b#(,|)#c#(,|)#d#(,|)#e\nf\n", - )) - .await?; - - Ok(()) -} - -#[tokio::test(flavor = "multi_thread")] -async fn macro_play_within_macro_record() -> anyhow::Result<()> { - // <https://github.com/helix-editor/helix/issues/12697> - // - // * `"aQihello<esc>Q` record a macro to register 'a' which inserts "hello" - // * `Q"aq<space>world<esc>Q` record a macro to the default macro register which plays the - // macro in register 'a' and then inserts " world" - // * `%d` clear the buffer - // * `q` replay the macro in the default macro register - // * `i<ret>` add a newline at the end - // - // The inner macro in register 'a' should replay within the outer macro exactly once to insert - // "hello world". - test(( - indoc! {"\ - #[|]# - "}, - r#""aQihello<esc>QQ"aqi<space>world<esc>Q%dqi<ret>"#, - indoc! {"\ - hello world - #[|]#"}, - )) - .await?; - - Ok(()) -} - -#[tokio::test(flavor = "multi_thread")] -async fn global_search_with_multibyte_chars() -> anyhow::Result<()> { - // Assert that `helix_term::commands::global_search` handles multibyte characters correctly. - test(( - indoc! {"\ - // Hello world! - // #[| - ]# - "}, - // start global search - " /«十分に長い マルチバイトキャラクター列» で検索<ret><esc>", - indoc! {"\ - // Hello world! - // #[| - ]# - "}, - )) - .await?; + test(("#[|]#", "[<space>u[<space>u", "#[|]#")).await?; Ok(()) } |