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.rs819
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(())
}