on master help sign in
add node boundary movement
commit93acb538121cab36712f40f26fa287df93817de5 authorSkyler Hawthorne <skyler@dead10ck.com> dateTue 16 Aug 2022, 21:28 (4 years ago) parent1d702ea
5 files changed+329−1
helix-core/src/movement.rs+80−1 helix-term/src/commands.rs+44 helix-term/src/keymap/default.rs+5 helix-term/tests/test/commands.rs+1 helix-term/tests/test/commands/movement.rs+199
helix-core/src/movement.rs+80−1
@@ -16,7 +16,7 @@ use crate::{
1616 syntax::LanguageConfiguration,
1717 text_annotations::TextAnnotations,
1818 textobject::TextObject,
19− visual_offset_from_block, Range, RopeSlice,
19+ visual_offset_from_block, Range, RopeSlice, Selection, Syntax,
2020};
2121
2222#[derive(Debug, Copy, Clone, PartialEq, Eq)]
@@ -556,6 +556,85 @@ pub fn goto_treesitter_object(
556556 last_range
557557}
558558
559+fn find_parent_start(mut node: Node) -> Option<Node> {
560+ let start = node.start_byte();
561+
562+ while node.start_byte() >= start || !node.is_named() {
563+ node = node.parent()?;
564+ }
565+
566+ Some(node)
567+}
568+
569+pub fn move_parent_node_end(
570+ syntax: &Syntax,
571+ text: RopeSlice,
572+ selection: Selection,
573+ dir: Direction,
574+ movement: Movement,
575+) -> Selection {
576+ let tree = syntax.tree();
577+
578+ selection.transform(|range| {
579+ let start_from = text.char_to_byte(range.from());
580+ let start_to = text.char_to_byte(range.to());
581+
582+ let mut node = match tree
583+ .root_node()
584+ .named_descendant_for_byte_range(start_from, start_to)
585+ {
586+ Some(node) => node,
587+ None => {
588+ log::debug!(
589+ "no descendant found for byte range: {} - {}",
590+ start_from,
591+ start_to
592+ );
593+ return range;
594+ }
595+ };
596+
597+ let mut end_head = match dir {
598+ // moving forward, we always want to move one past the end of the
599+ // current node, so use the end byte of the current node, which is an exclusive
600+ // end of the range
601+ Direction::Forward => text.byte_to_char(node.end_byte()),
602+
603+ // moving backward, we want the cursor to land on the start char of
604+ // the current node, or if it is already at the start of a node, to traverse up to
605+ // the parent
606+ Direction::Backward => {
607+ let end_head = text.byte_to_char(node.start_byte());
608+
609+ // if we're already on the beginning, look up to the parent
610+ if end_head == range.cursor(text) {
611+ node = find_parent_start(node).unwrap_or(node);
612+ text.byte_to_char(node.start_byte())
613+ } else {
614+ end_head
615+ }
616+ }
617+ };
618+
619+ if movement == Movement::Move {
620+ // preserve direction of original range
621+ if range.direction() == Direction::Forward {
622+ Range::new(end_head, end_head + 1)
623+ } else {
624+ Range::new(end_head + 1, end_head)
625+ }
626+ } else {
627+ // if we end up with a forward range, then adjust it to be one past
628+ // where we want
629+ if end_head >= range.anchor {
630+ end_head += 1;
631+ }
632+
633+ Range::new(range.anchor, end_head)
634+ }
635+ })
636+}
637+
559638#[cfg(test)]
560639mod test {
561640 use ropey::Rope;
helix-term/src/commands.rs+44
@@ -247,6 +247,8 @@ impl MappableCommand {
247247 move_prev_long_word_start, "Move to start of previous long word",
248248 move_next_long_word_end, "Move to end of next long word",
249249 move_prev_long_word_end, "Move to end of previous long word",
250+ move_parent_node_end, "Move to end of the parent node",
251+ move_parent_node_start, "Move to beginning of the parent node",
250252 extend_next_word_start, "Extend to start of next word",
251253 extend_prev_word_start, "Extend to start of previous word",
252254 extend_next_word_end, "Extend to end of next word",
@@ -255,6 +257,8 @@ impl MappableCommand {
255257 extend_prev_long_word_start, "Extend to start of previous long word",
256258 extend_next_long_word_end, "Extend to end of next long word",
257259 extend_prev_long_word_end, "Extend to end of prev long word",
260+ extend_parent_node_end, "Extend to end of the parent node",
261+ extend_parent_node_start, "Extend to beginning of the parent node",
258262 find_till_char, "Move till next occurrence of char",
259263 find_next_char, "Move to next occurrence of char",
260264 extend_till_char, "Extend till next occurrence of char",
@@ -4605,6 +4609,46 @@ fn select_prev_sibling(cx: &mut Context) {
46054609 select_sibling_impl(cx, &|node| Node::prev_sibling(&node))
46064610}
46074611
4612+fn move_node_bound_impl(cx: &mut Context, dir: Direction, movement: Movement) {
4613+ let motion = move |editor: &mut Editor| {
4614+ let (view, doc) = current!(editor);
4615+
4616+ if let Some(syntax) = doc.syntax() {
4617+ let text = doc.text().slice(..);
4618+ let current_selection = doc.selection(view.id);
4619+
4620+ let selection = movement::move_parent_node_end(
4621+ syntax,
4622+ text,
4623+ current_selection.clone(),
4624+ dir,
4625+ movement,
4626+ );
4627+
4628+ doc.set_selection(view.id, selection);
4629+ }
4630+ };
4631+
4632+ motion(cx.editor);
4633+ cx.editor.last_motion = Some(Motion(Box::new(motion)));
4634+}
4635+
4636+pub fn move_parent_node_end(cx: &mut Context) {
4637+ move_node_bound_impl(cx, Direction::Forward, Movement::Move)
4638+}
4639+
4640+pub fn move_parent_node_start(cx: &mut Context) {
4641+ move_node_bound_impl(cx, Direction::Backward, Movement::Move)
4642+}
4643+
4644+pub fn extend_parent_node_end(cx: &mut Context) {
4645+ move_node_bound_impl(cx, Direction::Forward, Movement::Extend)
4646+}
4647+
4648+pub fn extend_parent_node_start(cx: &mut Context) {
4649+ move_node_bound_impl(cx, Direction::Backward, Movement::Extend)
4650+}
4651+
46084652fn match_brackets(cx: &mut Context) {
46094653 let (view, doc) = current!(cx.editor);
46104654 let is_select = cx.editor.mode == Mode::Select;
helix-term/src/keymap/default.rs+5
@@ -88,6 +88,8 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
8888 "A-i" | "A-down" => shrink_selection,
8989 "A-p" | "A-left" => select_prev_sibling,
9090 "A-n" | "A-right" => select_next_sibling,
91+ "A-e" => move_parent_node_end,
92+ "A-b" => move_parent_node_start,
9193
9294 "%" => select_all,
9395 "x" => extend_line_below,
@@ -336,6 +338,9 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
336338 "B" => extend_prev_long_word_start,
337339 "E" => extend_next_long_word_end,
338340
341+ "A-e" => extend_parent_node_end,
342+ "A-b" => extend_parent_node_start,
343+
339344 "n" => extend_search_next,
340345 "N" => extend_search_prev,
341346
helix-term/tests/test/commands.rs+1
@@ -2,6 +2,7 @@ use helix_term::application::Application;
22
33use super::*;
44
5+mod movement;
56mod write;
67
78#[tokio::test(flavor = "multi_thread")]
helix-term/tests/test/commands/movement.rsnew file+199
@@ -0,0 +1,199 @@
1+use super::*;
2+
3+#[tokio::test(flavor = "multi_thread")]
4+async fn test_move_parent_node_end() -> anyhow::Result<()> {
5+ let tests = vec![
6+ // single cursor stays single cursor, first goes to end of current
7+ // node, then parent
8+ (
9+ helpers::platform_line(indoc! {r##"
10+ fn foo() {
11+ let result = if true {
12+ "yes"
13+ } else {
14+ "no#["|]#
15+ }
16+ }
17+ "##}),
18+ "<A-e>",
19+ helpers::platform_line(indoc! {"\
20+ fn foo() {
21+ let result = if true {
22+ \"yes\"
23+ } else {
24+ \"no\"#[\n|]#
25+ }
26+ }
27+ "}),
28+ ),
29+ (
30+ helpers::platform_line(indoc! {"\
31+ fn foo() {
32+ let result = if true {
33+ \"yes\"
34+ } else {
35+ \"no\"#[\n|]#
36+ }
37+ }
38+ "}),
39+ "<A-e>",
40+ helpers::platform_line(indoc! {"\
41+ fn foo() {
42+ let result = if true {
43+ \"yes\"
44+ } else {
45+ \"no\"
46+ }#[\n|]#
47+ }
48+ "}),
49+ ),
50+ // select mode extends
51+ (
52+ helpers::platform_line(indoc! {r##"
53+ fn foo() {
54+ let result = if true {
55+ "yes"
56+ } else {
57+ #["no"|]#
58+ }
59+ }
60+ "##}),
61+ "v<A-e><A-e>",
62+ helpers::platform_line(indoc! {"\
63+ fn foo() {
64+ let result = if true {
65+ \"yes\"
66+ } else {
67+ #[\"no\"
68+ }\n|]#
69+ }
70+ "}),
71+ ),
72+ ];
73+
74+ for test in tests {
75+ test_with_config(AppBuilder::new().with_file("foo.rs", None), test).await?;
76+ }
77+
78+ Ok(())
79+}
80+
81+#[tokio::test(flavor = "multi_thread")]
82+async fn test_move_parent_node_start() -> anyhow::Result<()> {
83+ let tests = vec![
84+ // single cursor stays single cursor, first goes to end of current
85+ // node, then parent
86+ (
87+ helpers::platform_line(indoc! {r##"
88+ fn foo() {
89+ let result = if true {
90+ "yes"
91+ } else {
92+ "no#["|]#
93+ }
94+ }
95+ "##}),
96+ "<A-b>",
97+ helpers::platform_line(indoc! {"\
98+ fn foo() {
99+ let result = if true {
100+ \"yes\"
101+ } else {
102+ #[\"|]#no\"
103+ }
104+ }
105+ "}),
106+ ),
107+ (
108+ helpers::platform_line(indoc! {"\
109+ fn foo() {
110+ let result = if true {
111+ \"yes\"
112+ } else {
113+ \"no\"#[\n|]#
114+ }
115+ }
116+ "}),
117+ "<A-b>",
118+ helpers::platform_line(indoc! {"\
119+ fn foo() {
120+ let result = if true {
121+ \"yes\"
122+ } else #[{|]#
123+ \"no\"
124+ }
125+ }
126+ "}),
127+ ),
128+ (
129+ helpers::platform_line(indoc! {"\
130+ fn foo() {
131+ let result = if true {
132+ \"yes\"
133+ } else #[{|]#
134+ \"no\"
135+ }
136+ }
137+ "}),
138+ "<A-b>",
139+ helpers::platform_line(indoc! {"\
140+ fn foo() {
141+ let result = if true {
142+ \"yes\"
143+ } #[e|]#lse {
144+ \"no\"
145+ }
146+ }
147+ "}),
148+ ),
149+ // select mode extends
150+ (
151+ helpers::platform_line(indoc! {r##"
152+ fn foo() {
153+ let result = if true {
154+ "yes"
155+ } else {
156+ #["no"|]#
157+ }
158+ }
159+ "##}),
160+ "v<A-b><A-b>",
161+ helpers::platform_line(indoc! {"\
162+ fn foo() {
163+ let result = if true {
164+ \"yes\"
165+ } else #[|{
166+ ]#\"no\"
167+ }
168+ }
169+ "}),
170+ ),
171+ (
172+ helpers::platform_line(indoc! {r##"
173+ fn foo() {
174+ let result = if true {
175+ "yes"
176+ } else {
177+ #["no"|]#
178+ }
179+ }
180+ "##}),
181+ "v<A-b><A-b><A-b>",
182+ helpers::platform_line(indoc! {"\
183+ fn foo() {
184+ let result = if true {
185+ \"yes\"
186+ } #[|else {
187+ ]#\"no\"
188+ }
189+ }
190+ "}),
191+ ),
192+ ];
193+
194+ for test in tests {
195+ test_with_config(AppBuilder::new().with_file("foo.rs", None), test).await?;
196+ }
197+
198+ Ok(())
199+}