mirror of
https://github.com/helix-editor/helix.git
synced 2024-11-22 17:36:19 +04:00
add node boundary movement
This commit is contained in:
parent
1d702ea191
commit
93acb53812
@ -16,7 +16,7 @@
|
|||||||
syntax::LanguageConfiguration,
|
syntax::LanguageConfiguration,
|
||||||
text_annotations::TextAnnotations,
|
text_annotations::TextAnnotations,
|
||||||
textobject::TextObject,
|
textobject::TextObject,
|
||||||
visual_offset_from_block, Range, RopeSlice,
|
visual_offset_from_block, Range, RopeSlice, Selection, Syntax,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||||
@ -556,6 +556,85 @@ pub fn goto_treesitter_object(
|
|||||||
last_range
|
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)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use ropey::Rope;
|
use ropey::Rope;
|
||||||
|
@ -247,6 +247,8 @@ pub fn doc(&self) -> &str {
|
|||||||
move_prev_long_word_start, "Move to start of previous long word",
|
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_next_long_word_end, "Move to end of next long word",
|
||||||
move_prev_long_word_end, "Move to end of previous 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_next_word_start, "Extend to start of next word",
|
||||||
extend_prev_word_start, "Extend to start of previous word",
|
extend_prev_word_start, "Extend to start of previous word",
|
||||||
extend_next_word_end, "Extend to end of next word",
|
extend_next_word_end, "Extend to end of next word",
|
||||||
@ -255,6 +257,8 @@ pub fn doc(&self) -> &str {
|
|||||||
extend_prev_long_word_start, "Extend to start of previous long word",
|
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_next_long_word_end, "Extend to end of next long word",
|
||||||
extend_prev_long_word_end, "Extend to end of prev 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_till_char, "Move till next occurrence of char",
|
||||||
find_next_char, "Move to next occurrence of char",
|
find_next_char, "Move to next occurrence of char",
|
||||||
extend_till_char, "Extend till 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))
|
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) {
|
fn match_brackets(cx: &mut Context) {
|
||||||
let (view, doc) = current!(cx.editor);
|
let (view, doc) = current!(cx.editor);
|
||||||
let is_select = cx.editor.mode == Mode::Select;
|
let is_select = cx.editor.mode == Mode::Select;
|
||||||
|
@ -88,6 +88,8 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
|
|||||||
"A-i" | "A-down" => shrink_selection,
|
"A-i" | "A-down" => shrink_selection,
|
||||||
"A-p" | "A-left" => select_prev_sibling,
|
"A-p" | "A-left" => select_prev_sibling,
|
||||||
"A-n" | "A-right" => select_next_sibling,
|
"A-n" | "A-right" => select_next_sibling,
|
||||||
|
"A-e" => move_parent_node_end,
|
||||||
|
"A-b" => move_parent_node_start,
|
||||||
|
|
||||||
"%" => select_all,
|
"%" => select_all,
|
||||||
"x" => extend_line_below,
|
"x" => extend_line_below,
|
||||||
@ -336,6 +338,9 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
|
|||||||
"B" => extend_prev_long_word_start,
|
"B" => extend_prev_long_word_start,
|
||||||
"E" => extend_next_long_word_end,
|
"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_next,
|
||||||
"N" => extend_search_prev,
|
"N" => extend_search_prev,
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
mod movement;
|
||||||
mod write;
|
mod write;
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
|
199
helix-term/tests/test/commands/movement.rs
Normal file
199
helix-term/tests/test/commands/movement.rs
Normal 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(())
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user