mirror of
https://github.com/helix-editor/helix.git
synced 2024-11-22 09:26:19 +04:00
add node boundary movement
This commit is contained in:
parent
1d702ea191
commit
93acb53812
@ -16,7 +16,7 @@
|
||||
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;
|
||||
|
@ -247,6 +247,8 @@ pub fn doc(&self) -> &str {
|
||||
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 @@ pub fn doc(&self) -> &str {
|
||||
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;
|
||||
|
@ -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,
|
||||
|
||||
|
@ -2,6 +2,7 @@
|
||||
|
||||
use super::*;
|
||||
|
||||
mod movement;
|
||||
mod write;
|
||||
|
||||
#[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