aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--helix-core/src/movement.rs81
-rw-r--r--helix-term/src/commands.rs44
-rw-r--r--helix-term/src/keymap/default.rs5
-rw-r--r--helix-term/tests/test/commands.rs1
-rw-r--r--helix-term/tests/test/commands/movement.rs199
5 files changed, 329 insertions, 1 deletions
diff --git a/helix-core/src/movement.rs b/helix-core/src/movement.rs
index 2b29f36d..6c4f3f53 100644
--- a/helix-core/src/movement.rs
+++ b/helix-core/src/movement.rs
@@ -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;
diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs
index bf60ad71..fc01eec7 100644
--- a/helix-term/src/commands.rs
+++ b/helix-term/src/commands.rs
@@ -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;
diff --git a/helix-term/src/keymap/default.rs b/helix-term/src/keymap/default.rs
index 37983352..419e376f 100644
--- a/helix-term/src/keymap/default.rs
+++ b/helix-term/src/keymap/default.rs
@@ -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,
diff --git a/helix-term/tests/test/commands.rs b/helix-term/tests/test/commands.rs
index b13c37bc..b3e13551 100644
--- a/helix-term/tests/test/commands.rs
+++ b/helix-term/tests/test/commands.rs
@@ -2,6 +2,7 @@ use helix_term::application::Application;
use super::*;
+mod movement;
mod write;
#[tokio::test(flavor = "multi_thread")]
diff --git a/helix-term/tests/test/commands/movement.rs b/helix-term/tests/test/commands/movement.rs
new file mode 100644
index 00000000..03dc7ba9
--- /dev/null
+++ b/helix-term/tests/test/commands/movement.rs
@@ -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(())
+}