f5991657f4
Prior to this change, every integration test which wanted its line endings to be handled transparently across platforms, i.e. test with the same input that has its platform's line feed characters, converting the line endings was up to each individual test by calling the `platform_line` helper function. This significantly increases the amount of boilerplate one has to copy between all the tests. However, there are some test cases that need to exert strict control over the exact input text without being manipulated behind the scenes by the test framework. So, with this change, the line feed conversions are factored into the `TestCase` struct. By default, line endings of the input text are converted to the platform's native line feed ending, but one can explicitly specify in their test case when the input text should be left alone and tested as is.
620 lines
13 KiB
Rust
620 lines
13 KiB
Rust
use helix_term::application::Application;
|
|
|
|
use super::*;
|
|
|
|
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|]#
|
|
"},
|
|
"<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|]#
|
|
"},
|
|
))
|
|
.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<esc>%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.js<ret>two.js<esc>%<A-s>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'<esc>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'<esc>bgf"),
|
|
Some(&|app| {
|
|
assert_eq!(1, match_paths(app, vec!["one.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<ret>",
|
|
indoc! {"\
|
|
#[|foo\n]#
|
|
|
|
#(|foo\n)#
|
|
|
|
#(|foo\n)#
|
|
|
|
"},
|
|
))
|
|
.await?;
|
|
|
|
// insert-output
|
|
test((
|
|
indoc! {"\
|
|
#[|lorem]#
|
|
#(|ipsum)#
|
|
#(|dolor)#
|
|
"},
|
|
"!echo foo<ret>",
|
|
indoc! {"\
|
|
#[|foo\n]#
|
|
lorem
|
|
#(|foo\n)#
|
|
ipsum
|
|
#(|foo\n)#
|
|
dolor
|
|
"},
|
|
))
|
|
.await?;
|
|
|
|
// append-output
|
|
test((
|
|
indoc! {"\
|
|
#[|lorem]#
|
|
#(|ipsum)#
|
|
#(|dolor)#
|
|
"},
|
|
"<A-!>echo foo<ret>",
|
|
indoc! {"\
|
|
lorem#[|foo\n]#
|
|
|
|
ipsum#(|foo\n)#
|
|
|
|
dolor#(|foo\n)#
|
|
|
|
"},
|
|
))
|
|
.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[<space> Add two newlines at line start. We're now on line 3.
|
|
// * <C-s> Save the selection on line 3 in the jumplist.
|
|
// * 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?;
|
|
|
|
// A jumplist selection is passed through an edit and then an undo and then a redo.
|
|
//
|
|
// * [<space> Add a newline at line start. We're now on line 2.
|
|
// * <C-s> 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.
|
|
// * <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?;
|
|
|
|
// 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(())
|
|
}
|