add node boundary movement

This commit is contained in:
Skyler Hawthorne 2022-08-16 21:28:41 -04:00 committed by Michael Davis
parent 1d702ea191
commit 93acb53812
5 changed files with 329 additions and 1 deletions

View File

@ -16,7 +16,7 @@ use crate::{
syntax::LanguageConfiguration,
text_annotations::TextAnnotations,
textobject::TextObject,
visual_offset_from_block, Range, RopeSlice,
visual_offset_from_block, Range, RopeSlice, Selection, Syntax,
};
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
@ -556,6 +556,85 @@ pub fn goto_treesitter_object(
last_range
}
fn find_parent_start(mut node: Node) -> Option<Node> {
let start = node.start_byte();
while node.start_byte() >= start || !node.is_named() {
node = node.parent()?;
}
Some(node)
}
pub fn move_parent_node_end(
syntax: &Syntax,
text: RopeSlice,
selection: Selection,
dir: Direction,
movement: Movement,
) -> Selection {
let tree = syntax.tree();
selection.transform(|range| {
let start_from = text.char_to_byte(range.from());
let start_to = text.char_to_byte(range.to());
let mut node = match tree
.root_node()
.named_descendant_for_byte_range(start_from, start_to)
{
Some(node) => node,
None => {
log::debug!(
"no descendant found for byte range: {} - {}",
start_from,
start_to
);
return range;
}
};
let mut end_head = match dir {
// moving forward, we always want to move one past the end of the
// current node, so use the end byte of the current node, which is an exclusive
// end of the range
Direction::Forward => text.byte_to_char(node.end_byte()),
// moving backward, we want the cursor to land on the start char of
// the current node, or if it is already at the start of a node, to traverse up to
// the parent
Direction::Backward => {
let end_head = text.byte_to_char(node.start_byte());
// if we're already on the beginning, look up to the parent
if end_head == range.cursor(text) {
node = find_parent_start(node).unwrap_or(node);
text.byte_to_char(node.start_byte())
} else {
end_head
}
}
};
if movement == Movement::Move {
// preserve direction of original range
if range.direction() == Direction::Forward {
Range::new(end_head, end_head + 1)
} else {
Range::new(end_head + 1, end_head)
}
} else {
// if we end up with a forward range, then adjust it to be one past
// where we want
if end_head >= range.anchor {
end_head += 1;
}
Range::new(range.anchor, end_head)
}
})
}
#[cfg(test)]
mod test {
use ropey::Rope;

View File

@ -247,6 +247,8 @@ impl MappableCommand {
move_prev_long_word_start, "Move to start of previous long word",
move_next_long_word_end, "Move to end of next long word",
move_prev_long_word_end, "Move to end of previous long word",
move_parent_node_end, "Move to end of the parent node",
move_parent_node_start, "Move to beginning of the parent node",
extend_next_word_start, "Extend to start of next word",
extend_prev_word_start, "Extend to start of previous word",
extend_next_word_end, "Extend to end of next word",
@ -255,6 +257,8 @@ impl MappableCommand {
extend_prev_long_word_start, "Extend to start of previous long word",
extend_next_long_word_end, "Extend to end of next long word",
extend_prev_long_word_end, "Extend to end of prev long word",
extend_parent_node_end, "Extend to end of the parent node",
extend_parent_node_start, "Extend to beginning of the parent node",
find_till_char, "Move till next occurrence of char",
find_next_char, "Move to next occurrence of char",
extend_till_char, "Extend till next occurrence of char",
@ -4605,6 +4609,46 @@ fn select_prev_sibling(cx: &mut Context) {
select_sibling_impl(cx, &|node| Node::prev_sibling(&node))
}
fn move_node_bound_impl(cx: &mut Context, dir: Direction, movement: Movement) {
let motion = move |editor: &mut Editor| {
let (view, doc) = current!(editor);
if let Some(syntax) = doc.syntax() {
let text = doc.text().slice(..);
let current_selection = doc.selection(view.id);
let selection = movement::move_parent_node_end(
syntax,
text,
current_selection.clone(),
dir,
movement,
);
doc.set_selection(view.id, selection);
}
};
motion(cx.editor);
cx.editor.last_motion = Some(Motion(Box::new(motion)));
}
pub fn move_parent_node_end(cx: &mut Context) {
move_node_bound_impl(cx, Direction::Forward, Movement::Move)
}
pub fn move_parent_node_start(cx: &mut Context) {
move_node_bound_impl(cx, Direction::Backward, Movement::Move)
}
pub fn extend_parent_node_end(cx: &mut Context) {
move_node_bound_impl(cx, Direction::Forward, Movement::Extend)
}
pub fn extend_parent_node_start(cx: &mut Context) {
move_node_bound_impl(cx, Direction::Backward, Movement::Extend)
}
fn match_brackets(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
let is_select = cx.editor.mode == Mode::Select;

View File

@ -88,6 +88,8 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
"A-i" | "A-down" => shrink_selection,
"A-p" | "A-left" => select_prev_sibling,
"A-n" | "A-right" => select_next_sibling,
"A-e" => move_parent_node_end,
"A-b" => move_parent_node_start,
"%" => select_all,
"x" => extend_line_below,
@ -336,6 +338,9 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
"B" => extend_prev_long_word_start,
"E" => extend_next_long_word_end,
"A-e" => extend_parent_node_end,
"A-b" => extend_parent_node_start,
"n" => extend_search_next,
"N" => extend_search_prev,

View File

@ -2,6 +2,7 @@ use helix_term::application::Application;
use super::*;
mod movement;
mod write;
#[tokio::test(flavor = "multi_thread")]

View File

@ -0,0 +1,199 @@
use super::*;
#[tokio::test(flavor = "multi_thread")]
async fn test_move_parent_node_end() -> anyhow::Result<()> {
let tests = vec![
// single cursor stays single cursor, first goes to end of current
// node, then parent
(
helpers::platform_line(indoc! {r##"
fn foo() {
let result = if true {
"yes"
} else {
"no#["|]#
}
}
"##}),
"<A-e>",
helpers::platform_line(indoc! {"\
fn foo() {
let result = if true {
\"yes\"
} else {
\"no\"#[\n|]#
}
}
"}),
),
(
helpers::platform_line(indoc! {"\
fn foo() {
let result = if true {
\"yes\"
} else {
\"no\"#[\n|]#
}
}
"}),
"<A-e>",
helpers::platform_line(indoc! {"\
fn foo() {
let result = if true {
\"yes\"
} else {
\"no\"
}#[\n|]#
}
"}),
),
// select mode extends
(
helpers::platform_line(indoc! {r##"
fn foo() {
let result = if true {
"yes"
} else {
#["no"|]#
}
}
"##}),
"v<A-e><A-e>",
helpers::platform_line(indoc! {"\
fn foo() {
let result = if true {
\"yes\"
} else {
#[\"no\"
}\n|]#
}
"}),
),
];
for test in tests {
test_with_config(AppBuilder::new().with_file("foo.rs", None), test).await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_move_parent_node_start() -> anyhow::Result<()> {
let tests = vec![
// single cursor stays single cursor, first goes to end of current
// node, then parent
(
helpers::platform_line(indoc! {r##"
fn foo() {
let result = if true {
"yes"
} else {
"no#["|]#
}
}
"##}),
"<A-b>",
helpers::platform_line(indoc! {"\
fn foo() {
let result = if true {
\"yes\"
} else {
#[\"|]#no\"
}
}
"}),
),
(
helpers::platform_line(indoc! {"\
fn foo() {
let result = if true {
\"yes\"
} else {
\"no\"#[\n|]#
}
}
"}),
"<A-b>",
helpers::platform_line(indoc! {"\
fn foo() {
let result = if true {
\"yes\"
} else #[{|]#
\"no\"
}
}
"}),
),
(
helpers::platform_line(indoc! {"\
fn foo() {
let result = if true {
\"yes\"
} else #[{|]#
\"no\"
}
}
"}),
"<A-b>",
helpers::platform_line(indoc! {"\
fn foo() {
let result = if true {
\"yes\"
} #[e|]#lse {
\"no\"
}
}
"}),
),
// select mode extends
(
helpers::platform_line(indoc! {r##"
fn foo() {
let result = if true {
"yes"
} else {
#["no"|]#
}
}
"##}),
"v<A-b><A-b>",
helpers::platform_line(indoc! {"\
fn foo() {
let result = if true {
\"yes\"
} else #[|{
]#\"no\"
}
}
"}),
),
(
helpers::platform_line(indoc! {r##"
fn foo() {
let result = if true {
"yes"
} else {
#["no"|]#
}
}
"##}),
"v<A-b><A-b><A-b>",
helpers::platform_line(indoc! {"\
fn foo() {
let result = if true {
\"yes\"
} #[|else {
]#\"no\"
}
}
"}),
),
];
for test in tests {
test_with_config(AppBuilder::new().with_file("foo.rs", None), test).await?;
}
Ok(())
}