Detailed changes
@@ -379,8 +379,8 @@
"r": ["vim::PushOperator", "Replace"],
"s": "vim::Substitute",
"shift-s": "vim::SubstituteLine",
- "> >": "vim::Indent",
- "< <": "vim::Outdent",
+ ">": ["vim::PushOperator", "Indent"],
+ "<": ["vim::PushOperator", "Outdent"],
"ctrl-pagedown": "pane::ActivateNextItem",
"ctrl-pageup": "pane::ActivatePrevItem",
// tree-sitter related commands
@@ -459,6 +459,18 @@
"s": "vim::CurrentLine"
}
},
+ {
+ "context": "Editor && vim_operator == >",
+ "bindings": {
+ ">": "vim::CurrentLine"
+ }
+ },
+ {
+ "context": "Editor && vim_operator == <",
+ "bindings": {
+ "<": "vim::CurrentLine"
+ }
+ },
{
"context": "Editor && VimObject",
"bindings": {
@@ -304,6 +304,14 @@ impl KeyBindingContextPredicate {
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:?}")),
}
}
@@ -347,6 +355,10 @@ fn is_identifier_char(c: char) -> bool {
c.is_alphanumeric() || c == '_' || c == '-'
}
+fn is_vim_operator_char(c: char) -> bool {
+ c == '>' || c == '<'
+}
+
fn skip_whitespace(source: &str) -> &str {
let len = source
.find(|c: char| !c.is_whitespace())
@@ -2,6 +2,7 @@ mod case;
mod change;
mod delete;
mod increment;
+mod indent;
pub(crate) mod mark;
mod paste;
pub(crate) mod repeat;
@@ -32,6 +33,7 @@ use self::{
case::{change_case, convert_to_lower_case, convert_to_upper_case},
change::{change_motion, change_object},
delete::{delete_motion, delete_object},
+ indent::{indent_motion, indent_object, IndentDirection},
yank::{yank_motion, yank_object},
};
@@ -182,6 +184,8 @@ pub fn normal_motion(
Some(Operator::Delete) => delete_motion(vim, motion, times, cx),
Some(Operator::Yank) => yank_motion(vim, motion, times, cx),
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) => {
// Can't do anything for text objects, Ignoring
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::Delete) => delete_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 }) => {
waiting_operator = Some(Operator::AddSurrounds {
target: Some(SurroundsType::Object(object)),
@@ -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,
Mark,
Jump { line: bool },
+ Indent,
+ Outdent,
}
#[derive(Default, Clone)]
@@ -266,6 +268,8 @@ impl Operator {
Operator::Mark => "m",
Operator::Jump { line: true } => "'",
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
cx.simulate_keystrokes("shift-v down >");
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]
@@ -534,7 +534,11 @@ impl Vim {
fn push_operator(&mut self, operator: Operator, cx: &mut WindowContext) {
if matches!(
operator,
- Operator::Change | Operator::Delete | Operator::Replace
+ Operator::Change
+ | Operator::Delete
+ | Operator::Replace
+ | Operator::Indent
+ | Operator::Outdent
) {
self.start_recording(cx)
};