helix-mods/helix-term/tests/test/movement.rs
Skyler Hawthorne f5991657f4
Factor out line ending handling in integration tests (#9921)
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.
2024-03-31 07:12:17 -05:00

685 lines
16 KiB
Rust

use super::*;
#[tokio::test(flavor = "multi_thread")]
async fn insert_mode_cursor_position() -> anyhow::Result<()> {
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),
line_feed_handling: LineFeedHandling::AsIs,
})
.await?;
test(("#[\n|]#", "i", "#[|\n]#")).await?;
test(("#[\n|]#", "i<esc>", "#[|\n]#")).await?;
test(("#[\n|]#", "i<esc>i", "#[|\n]#")).await?;
Ok(())
}
/// Range direction is preserved when escaping insert mode to normal
#[tokio::test(flavor = "multi_thread")]
async fn insert_to_normal_mode_cursor_position() -> anyhow::Result<()> {
test(("#[f|]#oo\n", "vll<A-;><esc>", "#[|foo]#\n")).await?;
test((
indoc! {"\
#[f|]#oo
#(b|)#ar"
},
"vll<A-;><esc>",
indoc! {"\
#[|foo]#
#(|bar)#"
},
))
.await?;
test((
indoc! {"\
#[f|]#oo
#(b|)#ar"
},
"a",
indoc! {"\
#[fo|]#o
#(ba|)#r"
},
))
.await?;
test((
indoc! {"\
#[f|]#oo
#(b|)#ar"
},
"a<esc>",
indoc! {"\
#[f|]#oo
#(b|)#ar"
},
))
.await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn surround_by_character() -> anyhow::Result<()> {
// Only pairs matching the passed character count
test((
"(so [many {go#[o|]#d} text] here)",
"mi{",
"(so [many {#[good|]#} text] here)",
))
.await?;
test((
"(so [many {go#[o|]#d} text] here)",
"mi[",
"(so [#[many {good} text|]#] here)",
))
.await?;
test((
"(so [many {go#[o|]#d} text] here)",
"mi(",
"(#[so [many {good} text] here|]#)",
))
.await?;
// Works with characters that aren't pairs too
test((
"'so 'many 'go#[o|]#d' text' here'",
"mi'",
"'so 'many '#[good|]#' text' here'",
))
.await?;
test((
"'so 'many 'go#[o|]#d' text' here'",
"2mi'",
"'so '#[many 'good' text|]#' here'",
))
.await?;
test((
"'so \"many 'go#[o|]#d' text\" here'",
"mi\"",
"'so \"#[many 'good' text|]#\" here'",
))
.await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn surround_inside_pair() -> anyhow::Result<()> {
// Works at first character of buffer
// TODO: Adjust test when opening pair failure is fixed
test(("#[(|]#something)", "mim", "#[(|]#something)")).await?;
// Inside a valid pair selects pair
test(("some (#[t|]#ext) here", "mim", "some (#[text|]#) here")).await?;
// On pair character selects pair
// TODO: Opening pair character is a known failure case that needs addressing
// test(("some #[(|]#text) here", "mim", "some (#[text|]#) here")).await?;
test(("some (text#[)|]# here", "mim", "some (#[text|]#) here")).await?;
// No valid pair does nothing
test(("so#[m|]#e (text) here", "mim", "so#[m|]#e (text) here")).await?;
// Count skips to outer pairs
test((
"(so (many (go#[o|]#d) text) here)",
"1mim",
"(so (many (#[good|]#) text) here)",
))
.await?;
test((
"(so (many (go#[o|]#d) text) here)",
"2mim",
"(so (#[many (good) text|]#) here)",
))
.await?;
test((
"(so (many (go#[o|]#d) text) here)",
"3mim",
"(#[so (many (good) text) here|]#)",
))
.await?;
// Matching pairs outside selection don't match
test((
"((so)((many) go#[o|]#d (text))(here))",
"mim",
"((so)(#[(many) good (text)|]#)(here))",
))
.await?;
test((
"((so)((many) go#[o|]#d (text))(here))",
"2mim",
"(#[(so)((many) good (text))(here)|]#)",
))
.await?;
// Works with mixed braces
test((
"(so [many {go#[o|]#d} text] here)",
"mim",
"(so [many {#[good|]#} text] here)",
))
.await?;
test((
"(so [many {go#[o|]#d} text] here)",
"2mim",
"(so [#[many {good} text|]#] here)",
))
.await?;
test((
"(so [many {go#[o|]#d} text] here)",
"3mim",
"(#[so [many {good} text] here|]#)",
))
.await?;
// Selection direction is preserved
test((
"(so [many {go#[|od]#} text] here)",
"mim",
"(so [many {#[|good]#} text] here)",
))
.await?;
test((
"(so [many {go#[|od]#} text] here)",
"2mim",
"(so [#[|many {good} text]#] here)",
))
.await?;
test((
"(so [many {go#[|od]#} text] here)",
"3mim",
"(#[|so [many {good} text] here]#)",
))
.await?;
// Only pairs outside of full selection range are considered
test((
"(so (many (go#[od) |]#text) here)",
"mim",
"(so (#[many (good) text|]#) here)",
))
.await?;
test((
"(so (many#[ (go|]#od) text) here)",
"mim",
"(so (#[many (good) text|]#) here)",
))
.await?;
test((
"(so#[ (many (go|]#od) text) here)",
"mim",
"(#[so (many (good) text) here|]#)",
))
.await?;
test((
"(so (many (go#[od) text) |]#here)",
"mim",
"(#[so (many (good) text) here|]#)",
))
.await?;
// Works with multiple cursors
test((
"(so (many (good) text) #[he|]#re\nso (many (good) text) #(|he)#re)",
"mim",
"(#[so (many (good) text) here\nso (many (good) text) here|]#)",
))
.await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn surround_around_pair() -> anyhow::Result<()> {
// Works at first character of buffer
// TODO: Adjust test when opening pair failure is fixed
test(("#[(|]#something)", "mam", "#[(|]#something)")).await?;
// Inside a valid pair selects pair
test(("some (#[t|]#ext) here", "mam", "some #[(text)|]# here")).await?;
// On pair character selects pair
// TODO: Opening pair character is a known failure case that needs addressing
// test(("some #[(|]#text) here", "mam", "some #[(text)|]# here")).await?;
test(("some (text#[)|]# here", "mam", "some #[(text)|]# here")).await?;
// No valid pair does nothing
test(("so#[m|]#e (text) here", "mam", "so#[m|]#e (text) here")).await?;
// Count skips to outer pairs
test((
"(so (many (go#[o|]#d) text) here)",
"1mam",
"(so (many #[(good)|]# text) here)",
))
.await?;
test((
"(so (many (go#[o|]#d) text) here)",
"2mam",
"(so #[(many (good) text)|]# here)",
))
.await?;
test((
"(so (many (go#[o|]#d) text) here)",
"3mam",
"#[(so (many (good) text) here)|]#",
))
.await?;
// Matching pairs outside selection don't match
test((
"((so)((many) go#[o|]#d (text))(here))",
"mam",
"((so)#[((many) good (text))|]#(here))",
))
.await?;
test((
"((so)((many) go#[o|]#d (text))(here))",
"2mam",
"#[((so)((many) good (text))(here))|]#",
))
.await?;
// Works with mixed braces
test((
"(so [many {go#[o|]#d} text] here)",
"mam",
"(so [many #[{good}|]# text] here)",
))
.await?;
test((
"(so [many {go#[o|]#d} text] here)",
"2mam",
"(so #[[many {good} text]|]# here)",
))
.await?;
test((
"(so [many {go#[o|]#d} text] here)",
"3mam",
"#[(so [many {good} text] here)|]#",
))
.await?;
// Selection direction is preserved
test((
"(so [many {go#[|od]#} text] here)",
"mam",
"(so [many #[|{good}]# text] here)",
))
.await?;
test((
"(so [many {go#[|od]#} text] here)",
"2mam",
"(so #[|[many {good} text]]# here)",
))
.await?;
test((
"(so [many {go#[|od]#} text] here)",
"3mam",
"#[|(so [many {good} text] here)]#",
))
.await?;
// Only pairs outside of full selection range are considered
test((
"(so (many (go#[od) |]#text) here)",
"mam",
"(so #[(many (good) text)|]# here)",
))
.await?;
test((
"(so (many#[ (go|]#od) text) here)",
"mam",
"(so #[(many (good) text)|]# here)",
))
.await?;
test((
"(so#[ (many (go|]#od) text) here)",
"mam",
"#[(so (many (good) text) here)|]#",
))
.await?;
test((
"(so (many (go#[od) text) |]#here)",
"mam",
"#[(so (many (good) text) here)|]#",
))
.await?;
// Works with multiple cursors
test((
"(so (many (good) text) #[he|]#re\nso (many (good) text) #(|he)#re)",
"mam",
"#[(so (many (good) text) here\nso (many (good) text) here)|]#",
))
.await?;
Ok(())
}
/// Ensure the very initial cursor in an opened file is the width of
/// the first grapheme
#[tokio::test(flavor = "multi_thread")]
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 = helpers::AppBuilder::new()
.with_file(file.path(), None)
.build()?;
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))?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn cursor_position_append_eof() -> anyhow::Result<()> {
// Selection is forwards
test(("#[foo|]#", "abar<esc>", "#[foobar|]#\n")).await?;
// Selection is backwards
test(("#[|foo]#", "abar<esc>", "#[foobar|]#\n")).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn select_mode_tree_sitter_next_function_is_union_of_objects() -> anyhow::Result<()> {
test_with_config(
AppBuilder::new().with_file("foo.rs", None),
(
indoc! {"\
#[/|]#// Increments
fn inc(x: usize) -> usize { x + 1 }
/// Decrements
fn dec(x: usize) -> usize { x - 1 }
"},
"]fv]f",
indoc! {"\
/// Increments
#[fn inc(x: usize) -> usize { x + 1 }
/// Decrements
fn dec(x: usize) -> usize { x - 1 }|]#
"},
),
)
.await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn select_mode_tree_sitter_prev_function_unselects_object() -> anyhow::Result<()> {
test_with_config(
AppBuilder::new().with_file("foo.rs", None),
(
indoc! {"\
/// Increments
#[fn inc(x: usize) -> usize { x + 1 }
/// Decrements
fn dec(x: usize) -> usize { x - 1 }|]#
"},
"v[f",
indoc! {"\
/// Increments
#[fn inc(x: usize) -> usize { x + 1 }|]#
/// Decrements
fn dec(x: usize) -> usize { x - 1 }
"},
),
)
.await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn select_mode_tree_sitter_prev_function_goes_backwards_to_object() -> anyhow::Result<()> {
// Note: the anchor stays put and the head moves back.
test_with_config(
AppBuilder::new().with_file("foo.rs", None),
(
indoc! {"\
/// Increments
fn inc(x: usize) -> usize { x + 1 }
/// Decrements
fn dec(x: usize) -> usize { x - 1 }
/// Identity
#[fn ident(x: usize) -> usize { x }|]#
"},
"v[f",
indoc! {"\
/// Increments
fn inc(x: usize) -> usize { x + 1 }
/// Decrements
#[|fn dec(x: usize) -> usize { x - 1 }
/// Identity
]#fn ident(x: usize) -> usize { x }
"},
),
)
.await?;
test_with_config(
AppBuilder::new().with_file("foo.rs", None),
(
indoc! {"\
/// Increments
fn inc(x: usize) -> usize { x + 1 }
/// Decrements
fn dec(x: usize) -> usize { x - 1 }
/// Identity
#[fn ident(x: usize) -> usize { x }|]#
"},
"v[f[f",
indoc! {"\
/// Increments
#[|fn inc(x: usize) -> usize { x + 1 }
/// Decrements
fn dec(x: usize) -> usize { x - 1 }
/// Identity
]#fn ident(x: usize) -> usize { x }
"},
),
)
.await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn find_char_line_ending() -> anyhow::Result<()> {
test((
indoc! {
"\
one
#[|t]#wo
three"
},
"T<ret>gll2f<ret>",
indoc! {
"\
one
two#[
|]#three"
},
))
.await?;
test((
indoc! {
"\
#[|o]#ne
two
three"
},
"f<ret>2t<ret>ghT<ret>F<ret>",
indoc! {
"\
one#[|
t]#wo
three"
},
))
.await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_surround_replace() -> anyhow::Result<()> {
test((
indoc! {"\
(#[|a]#)
"},
"mrm{",
indoc! {"\
{#[|a]#}
"},
))
.await?;
test((
indoc! {"\
(#[a|]#)
"},
"mrm{",
indoc! {"\
{#[a|]#}
"},
))
.await?;
test((
indoc! {"\
{{
#(}|)#
#[}|]#
"},
"mrm)",
indoc! {"\
((
#()|)#
#[)|]#
"},
))
.await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_surround_delete() -> anyhow::Result<()> {
test((
indoc! {"\
(#[|a]#)
"},
"mdm",
indoc! {"\
#[|a]#
"},
))
.await?;
test((
indoc! {"\
(#[a|]#)
"},
"mdm",
indoc! {"\
#[a|]#
"},
))
.await?;
test((
indoc! {"\
{{
#(}|)#
#[}|]#
"},
"mdm",
"\n\n#(\n|)##[\n|]#",
))
.await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn tree_sitter_motions_work_across_injections() -> anyhow::Result<()> {
test_with_config(
AppBuilder::new().with_file("foo.html", None),
(
"<script>let #[|x]# = 1;</script>",
"<A-o>",
"<script>let #[|x = 1]#;</script>",
),
)
.await?;
// When the full injected layer is selected, expand_selection jumps to
// a more shallow layer.
test_with_config(
AppBuilder::new().with_file("foo.html", None),
(
"<script>#[|let x = 1;]#</script>",
"<A-o>",
"#[|<script>let x = 1;</script>]#",
),
)
.await?;
test_with_config(
AppBuilder::new().with_file("foo.html", None),
(
"<script>let #[|x = 1]#;</script>",
"<A-i>",
"<script>let #[|x]# = 1;</script>",
),
)
.await?;
test_with_config(
AppBuilder::new().with_file("foo.html", None),
(
"<script>let #[|x]# = 1;</script>",
"<A-n>",
"<script>let x #[|=]# 1;</script>",
),
)
.await?;
test_with_config(
AppBuilder::new().with_file("foo.html", None),
(
"<script>let #[|x]# = 1;</script>",
"<A-p>",
"<script>#[|let]# x = 1;</script>",
),
)
.await?;
Ok(())
}