Implement Indent & Outdent as operators (#12430)
Release Notes: - Fixes [#9697](https://github.com/zed-industries/zed/issues/9697). Implements `>` and `<` with motions and text objects. Works with repeat action `.`
This commit is contained in:
parent
25050e8027
commit
8a659b0c60
7 changed files with 150 additions and 3 deletions
|
@ -379,8 +379,8 @@
|
||||||
"r": ["vim::PushOperator", "Replace"],
|
"r": ["vim::PushOperator", "Replace"],
|
||||||
"s": "vim::Substitute",
|
"s": "vim::Substitute",
|
||||||
"shift-s": "vim::SubstituteLine",
|
"shift-s": "vim::SubstituteLine",
|
||||||
"> >": "vim::Indent",
|
">": ["vim::PushOperator", "Indent"],
|
||||||
"< <": "vim::Outdent",
|
"<": ["vim::PushOperator", "Outdent"],
|
||||||
"ctrl-pagedown": "pane::ActivateNextItem",
|
"ctrl-pagedown": "pane::ActivateNextItem",
|
||||||
"ctrl-pageup": "pane::ActivatePrevItem",
|
"ctrl-pageup": "pane::ActivatePrevItem",
|
||||||
// tree-sitter related commands
|
// tree-sitter related commands
|
||||||
|
@ -459,6 +459,18 @@
|
||||||
"s": "vim::CurrentLine"
|
"s": "vim::CurrentLine"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"context": "Editor && vim_operator == >",
|
||||||
|
"bindings": {
|
||||||
|
">": "vim::CurrentLine"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"context": "Editor && vim_operator == <",
|
||||||
|
"bindings": {
|
||||||
|
"<": "vim::CurrentLine"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"context": "Editor && VimObject",
|
"context": "Editor && VimObject",
|
||||||
"bindings": {
|
"bindings": {
|
||||||
|
|
|
@ -304,6 +304,14 @@ impl KeyBindingContextPredicate {
|
||||||
source,
|
source,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
_ if is_vim_operator_char(next) => {
|
||||||
|
let (operator, rest) = source.split_at(1);
|
||||||
|
source = skip_whitespace(rest);
|
||||||
|
Ok((
|
||||||
|
KeyBindingContextPredicate::Identifier(operator.to_string().into()),
|
||||||
|
source,
|
||||||
|
))
|
||||||
|
}
|
||||||
_ => Err(anyhow!("unexpected character {next:?}")),
|
_ => Err(anyhow!("unexpected character {next:?}")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -347,6 +355,10 @@ fn is_identifier_char(c: char) -> bool {
|
||||||
c.is_alphanumeric() || c == '_' || c == '-'
|
c.is_alphanumeric() || c == '_' || c == '-'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_vim_operator_char(c: char) -> bool {
|
||||||
|
c == '>' || c == '<'
|
||||||
|
}
|
||||||
|
|
||||||
fn skip_whitespace(source: &str) -> &str {
|
fn skip_whitespace(source: &str) -> &str {
|
||||||
let len = source
|
let len = source
|
||||||
.find(|c: char| !c.is_whitespace())
|
.find(|c: char| !c.is_whitespace())
|
||||||
|
|
|
@ -2,6 +2,7 @@ mod case;
|
||||||
mod change;
|
mod change;
|
||||||
mod delete;
|
mod delete;
|
||||||
mod increment;
|
mod increment;
|
||||||
|
mod indent;
|
||||||
pub(crate) mod mark;
|
pub(crate) mod mark;
|
||||||
mod paste;
|
mod paste;
|
||||||
pub(crate) mod repeat;
|
pub(crate) mod repeat;
|
||||||
|
@ -32,6 +33,7 @@ use self::{
|
||||||
case::{change_case, convert_to_lower_case, convert_to_upper_case},
|
case::{change_case, convert_to_lower_case, convert_to_upper_case},
|
||||||
change::{change_motion, change_object},
|
change::{change_motion, change_object},
|
||||||
delete::{delete_motion, delete_object},
|
delete::{delete_motion, delete_object},
|
||||||
|
indent::{indent_motion, indent_object, IndentDirection},
|
||||||
yank::{yank_motion, yank_object},
|
yank::{yank_motion, yank_object},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -182,6 +184,8 @@ pub fn normal_motion(
|
||||||
Some(Operator::Delete) => delete_motion(vim, motion, times, cx),
|
Some(Operator::Delete) => delete_motion(vim, motion, times, cx),
|
||||||
Some(Operator::Yank) => yank_motion(vim, motion, times, cx),
|
Some(Operator::Yank) => yank_motion(vim, motion, times, cx),
|
||||||
Some(Operator::AddSurrounds { target: None }) => {}
|
Some(Operator::AddSurrounds { target: None }) => {}
|
||||||
|
Some(Operator::Indent) => indent_motion(vim, motion, times, IndentDirection::In, cx),
|
||||||
|
Some(Operator::Outdent) => indent_motion(vim, motion, times, IndentDirection::Out, cx),
|
||||||
Some(operator) => {
|
Some(operator) => {
|
||||||
// Can't do anything for text objects, Ignoring
|
// Can't do anything for text objects, Ignoring
|
||||||
error!("Unexpected normal mode motion operator: {:?}", operator)
|
error!("Unexpected normal mode motion operator: {:?}", operator)
|
||||||
|
@ -198,6 +202,12 @@ pub fn normal_object(object: Object, cx: &mut WindowContext) {
|
||||||
Some(Operator::Change) => change_object(vim, object, around, cx),
|
Some(Operator::Change) => change_object(vim, object, around, cx),
|
||||||
Some(Operator::Delete) => delete_object(vim, object, around, cx),
|
Some(Operator::Delete) => delete_object(vim, object, around, cx),
|
||||||
Some(Operator::Yank) => yank_object(vim, object, around, cx),
|
Some(Operator::Yank) => yank_object(vim, object, around, cx),
|
||||||
|
Some(Operator::Indent) => {
|
||||||
|
indent_object(vim, object, around, IndentDirection::In, cx)
|
||||||
|
}
|
||||||
|
Some(Operator::Outdent) => {
|
||||||
|
indent_object(vim, object, around, IndentDirection::Out, cx)
|
||||||
|
}
|
||||||
Some(Operator::AddSurrounds { target: None }) => {
|
Some(Operator::AddSurrounds { target: None }) => {
|
||||||
waiting_operator = Some(Operator::AddSurrounds {
|
waiting_operator = Some(Operator::AddSurrounds {
|
||||||
target: Some(SurroundsType::Object(object)),
|
target: Some(SurroundsType::Object(object)),
|
||||||
|
|
78
crates/vim/src/normal/indent.rs
Normal file
78
crates/vim/src/normal/indent.rs
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
use crate::{motion::Motion, object::Object, Vim};
|
||||||
|
use collections::HashMap;
|
||||||
|
use editor::{display_map::ToDisplayPoint, Bias};
|
||||||
|
use gpui::WindowContext;
|
||||||
|
use language::SelectionGoal;
|
||||||
|
|
||||||
|
#[derive(PartialEq, Eq)]
|
||||||
|
pub(super) enum IndentDirection {
|
||||||
|
In,
|
||||||
|
Out,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn indent_motion(
|
||||||
|
vim: &mut Vim,
|
||||||
|
motion: Motion,
|
||||||
|
times: Option<usize>,
|
||||||
|
dir: IndentDirection,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) {
|
||||||
|
vim.stop_recording();
|
||||||
|
vim.update_active_editor(cx, |_, editor, cx| {
|
||||||
|
let text_layout_details = editor.text_layout_details(cx);
|
||||||
|
editor.transact(cx, |editor, cx| {
|
||||||
|
let mut selection_starts: HashMap<_, _> = Default::default();
|
||||||
|
editor.change_selections(None, cx, |s| {
|
||||||
|
s.move_with(|map, selection| {
|
||||||
|
let anchor = map.display_point_to_anchor(selection.head(), Bias::Right);
|
||||||
|
selection_starts.insert(selection.id, anchor);
|
||||||
|
motion.expand_selection(map, selection, times, false, &text_layout_details);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if dir == IndentDirection::In {
|
||||||
|
editor.indent(&Default::default(), cx);
|
||||||
|
} else {
|
||||||
|
editor.outdent(&Default::default(), cx);
|
||||||
|
}
|
||||||
|
editor.change_selections(None, cx, |s| {
|
||||||
|
s.move_with(|map, selection| {
|
||||||
|
let anchor = selection_starts.remove(&selection.id).unwrap();
|
||||||
|
selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn indent_object(
|
||||||
|
vim: &mut Vim,
|
||||||
|
object: Object,
|
||||||
|
around: bool,
|
||||||
|
dir: IndentDirection,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) {
|
||||||
|
vim.stop_recording();
|
||||||
|
vim.update_active_editor(cx, |_, editor, cx| {
|
||||||
|
editor.transact(cx, |editor, cx| {
|
||||||
|
let mut original_positions: HashMap<_, _> = Default::default();
|
||||||
|
editor.change_selections(None, cx, |s| {
|
||||||
|
s.move_with(|map, selection| {
|
||||||
|
let anchor = map.display_point_to_anchor(selection.head(), Bias::Right);
|
||||||
|
original_positions.insert(selection.id, anchor);
|
||||||
|
object.expand_selection(map, selection, around);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if dir == IndentDirection::In {
|
||||||
|
editor.indent(&Default::default(), cx);
|
||||||
|
} else {
|
||||||
|
editor.outdent(&Default::default(), cx);
|
||||||
|
}
|
||||||
|
editor.change_selections(None, cx, |s| {
|
||||||
|
s.move_with(|map, selection| {
|
||||||
|
let anchor = original_positions.remove(&selection.id).unwrap();
|
||||||
|
selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
|
@ -61,6 +61,8 @@ pub enum Operator {
|
||||||
DeleteSurrounds,
|
DeleteSurrounds,
|
||||||
Mark,
|
Mark,
|
||||||
Jump { line: bool },
|
Jump { line: bool },
|
||||||
|
Indent,
|
||||||
|
Outdent,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Clone)]
|
#[derive(Default, Clone)]
|
||||||
|
@ -266,6 +268,8 @@ impl Operator {
|
||||||
Operator::Mark => "m",
|
Operator::Mark => "m",
|
||||||
Operator::Jump { line: true } => "'",
|
Operator::Jump { line: true } => "'",
|
||||||
Operator::Jump { line: false } => "`",
|
Operator::Jump { line: false } => "`",
|
||||||
|
Operator::Indent => ">",
|
||||||
|
Operator::Outdent => "<",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -180,6 +180,33 @@ async fn test_indent_outdent(cx: &mut gpui::TestAppContext) {
|
||||||
// works in visual mode
|
// works in visual mode
|
||||||
cx.simulate_keystrokes("shift-v down >");
|
cx.simulate_keystrokes("shift-v down >");
|
||||||
cx.assert_editor_state("aa\n bb\n cˇc");
|
cx.assert_editor_state("aa\n bb\n cˇc");
|
||||||
|
|
||||||
|
// works as operator
|
||||||
|
cx.set_state("aa\nbˇb\ncc\n", Mode::Normal);
|
||||||
|
cx.simulate_keystrokes("> j");
|
||||||
|
cx.assert_editor_state("aa\n bˇb\n cc\n");
|
||||||
|
cx.simulate_keystrokes("< k");
|
||||||
|
cx.assert_editor_state("aa\nbˇb\n cc\n");
|
||||||
|
cx.simulate_keystrokes("> i p");
|
||||||
|
cx.assert_editor_state(" aa\n bˇb\n cc\n");
|
||||||
|
cx.simulate_keystrokes("< i p");
|
||||||
|
cx.assert_editor_state("aa\nbˇb\n cc\n");
|
||||||
|
cx.simulate_keystrokes("< i p");
|
||||||
|
cx.assert_editor_state("aa\nbˇb\ncc\n");
|
||||||
|
|
||||||
|
cx.set_state("ˇaa\nbb\ncc\n", Mode::Normal);
|
||||||
|
cx.simulate_keystrokes("> 2 j");
|
||||||
|
cx.assert_editor_state(" ˇaa\n bb\n cc\n");
|
||||||
|
|
||||||
|
cx.set_state("aa\nbb\nˇcc\n", Mode::Normal);
|
||||||
|
cx.simulate_keystrokes("> 2 k");
|
||||||
|
cx.assert_editor_state(" aa\n bb\n ˇcc\n");
|
||||||
|
|
||||||
|
cx.set_state("a\nb\nccˇc\n", Mode::Normal);
|
||||||
|
cx.simulate_keystrokes("> 2 k");
|
||||||
|
cx.assert_editor_state(" a\n b\n ccˇc\n");
|
||||||
|
cx.simulate_keystrokes(".");
|
||||||
|
cx.assert_editor_state(" a\n b\n ccˇc\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
|
|
|
@ -534,7 +534,11 @@ impl Vim {
|
||||||
fn push_operator(&mut self, operator: Operator, cx: &mut WindowContext) {
|
fn push_operator(&mut self, operator: Operator, cx: &mut WindowContext) {
|
||||||
if matches!(
|
if matches!(
|
||||||
operator,
|
operator,
|
||||||
Operator::Change | Operator::Delete | Operator::Replace
|
Operator::Change
|
||||||
|
| Operator::Delete
|
||||||
|
| Operator::Replace
|
||||||
|
| Operator::Indent
|
||||||
|
| Operator::Outdent
|
||||||
) {
|
) {
|
||||||
self.start_recording(cx)
|
self.start_recording(cx)
|
||||||
};
|
};
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue