Detailed changes
@@ -209,6 +209,10 @@
"ctrl-e": [
"vim::Scroll",
"LineDown"
+ ],
+ "r": [
+ "vim::PushOperator",
+ "Replace"
]
}
},
@@ -294,7 +298,11 @@
"d": "vim::VisualDelete",
"x": "vim::VisualDelete",
"y": "vim::VisualYank",
- "p": "vim::VisualPaste"
+ "p": "vim::VisualPaste",
+ "r": [
+ "vim::PushOperator",
+ "Replace"
+ ]
}
},
{
@@ -352,6 +352,29 @@ pub fn surrounding_word(map: &DisplaySnapshot, position: DisplayPoint) -> Range<
start..end
}
+pub fn split_display_range_by_lines(
+ map: &DisplaySnapshot,
+ range: Range<DisplayPoint>,
+) -> Vec<Range<DisplayPoint>> {
+ let mut result = Vec::new();
+
+ let mut start = range.start;
+ // Loop over all the covered rows until the one containing the range end
+ for row in range.start.row()..range.end.row() {
+ let row_end_column = map.line_len(row);
+ let end = map.clip_point(DisplayPoint::new(row, row_end_column), Bias::Left);
+ if start != end {
+ result.push(start..end);
+ }
+ start = map.clip_point(DisplayPoint::new(row + 1, 0), Bias::Left);
+ }
+
+ // Add the final range from the start of the last end to the original range end.
+ result.push(start..range.end);
+
+ result
+}
+
#[cfg(test)]
mod tests {
use super::*;
@@ -3,7 +3,7 @@ use editor::{
display_map::{DisplaySnapshot, ToDisplayPoint},
movement, Bias, CharKind, DisplayPoint,
};
-use gpui::{actions, impl_actions, keymap_matcher::KeyPressed, MutableAppContext};
+use gpui::{actions, impl_actions, MutableAppContext};
use language::{Point, Selection, SelectionGoal};
use serde::Deserialize;
use workspace::Workspace;
@@ -109,27 +109,6 @@ pub fn init(cx: &mut MutableAppContext) {
&PreviousWordStart { ignore_punctuation }: &PreviousWordStart,
cx: _| { motion(Motion::PreviousWordStart { ignore_punctuation }, cx) },
);
- cx.add_action(
- |_: &mut Workspace, KeyPressed { keystroke }: &KeyPressed, cx| match Vim::read(cx)
- .active_operator()
- {
- Some(Operator::FindForward { before }) => motion(
- Motion::FindForward {
- before,
- character: keystroke.key.chars().next().unwrap(),
- },
- cx,
- ),
- Some(Operator::FindBackward { after }) => motion(
- Motion::FindBackward {
- after,
- character: keystroke.key.chars().next().unwrap(),
- },
- cx,
- ),
- _ => cx.propagate_action(),
- },
- )
}
pub(crate) fn motion(motion: Motion, cx: &mut MutableAppContext) {
@@ -424,6 +424,53 @@ fn scroll(editor: &mut Editor, amount: &ScrollAmount, cx: &mut ViewContext<Edito
}
}
+pub(crate) fn normal_replace(text: &str, cx: &mut MutableAppContext) {
+ Vim::update(cx, |vim, cx| {
+ vim.update_active_editor(cx, |editor, cx| {
+ editor.transact(cx, |editor, cx| {
+ editor.set_clip_at_line_ends(false, cx);
+ let (map, display_selections) = editor.selections.all_display(cx);
+ // Selections are biased right at the start. So we need to store
+ // anchors that are biased left so that we can restore the selections
+ // after the change
+ let stable_anchors = editor
+ .selections
+ .disjoint_anchors()
+ .into_iter()
+ .map(|selection| {
+ let start = selection.start.bias_left(&map.buffer_snapshot);
+ start..start
+ })
+ .collect::<Vec<_>>();
+
+ let edits = display_selections
+ .into_iter()
+ .map(|selection| {
+ let mut range = selection.range();
+ *range.end.column_mut() += 1;
+ range.end = map.clip_point(range.end, Bias::Right);
+
+ (
+ range.start.to_offset(&map, Bias::Left)
+ ..range.end.to_offset(&map, Bias::Left),
+ text,
+ )
+ })
+ .collect::<Vec<_>>();
+
+ editor.buffer().update(cx, |buffer, cx| {
+ buffer.edit(edits, None, cx);
+ });
+ editor.set_clip_at_line_ends(true, cx);
+ editor.change_selections(None, cx, |s| {
+ s.select_anchor_ranges(stable_anchors);
+ });
+ });
+ });
+ vim.pop_operator(cx)
+ });
+}
+
#[cfg(test)]
mod test {
use indoc::indoc;
@@ -468,6 +515,16 @@ mod test {
.await;
}
+ // #[gpui::test]
+ // async fn test_enter(cx: &mut gpui::TestAppContext) {
+ // let mut cx = NeovimBackedTestContext::new(cx).await.binding(["enter"]);
+ // cx.assert_all(indoc! {"
+ // หThe qหuick broหwn
+ // หfox jumps"
+ // })
+ // .await;
+ // }
+
#[gpui::test]
async fn test_k(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await.binding(["k"]);
@@ -28,6 +28,7 @@ pub enum Operator {
Change,
Delete,
Yank,
+ Replace,
Object { around: bool },
FindForward { before: bool },
FindBackward { after: bool },
@@ -117,6 +118,7 @@ impl Operator {
Operator::Change => "c",
Operator::Delete => "d",
Operator::Yank => "y",
+ Operator::Replace => "r",
Operator::FindForward { before: false } => "f",
Operator::FindForward { before: true } => "t",
Operator::FindBackward { after: false } => "F",
@@ -127,7 +129,9 @@ impl Operator {
pub fn context_flags(&self) -> &'static [&'static str] {
match self {
Operator::Object { .. } => &["VimObject"],
- Operator::FindForward { .. } | Operator::FindBackward { .. } => &["VimWaiting"],
+ Operator::FindForward { .. } | Operator::FindBackward { .. } | Operator::Replace => {
+ &["VimWaiting"]
+ }
_ => &[],
}
}
@@ -13,11 +13,18 @@ mod visual;
use collections::HashMap;
use command_palette::CommandPaletteFilter;
use editor::{Bias, Cancel, Editor};
-use gpui::{impl_actions, MutableAppContext, Subscription, ViewContext, WeakViewHandle};
+use gpui::{
+ impl_actions,
+ keymap_matcher::{KeyPressed, Keystroke},
+ MutableAppContext, Subscription, ViewContext, WeakViewHandle,
+};
use language::CursorShape;
+use motion::Motion;
+use normal::normal_replace;
use serde::Deserialize;
use settings::Settings;
use state::{Mode, Operator, VimState};
+use visual::visual_replace;
use workspace::{self, Workspace};
#[derive(Clone, Deserialize, PartialEq)]
@@ -51,6 +58,11 @@ pub fn init(cx: &mut MutableAppContext) {
cx.add_action(|_: &mut Workspace, n: &Number, cx: _| {
Vim::update(cx, |vim, cx| vim.push_number(n, cx));
});
+ cx.add_action(
+ |_: &mut Workspace, KeyPressed { keystroke }: &KeyPressed, cx| {
+ Vim::key_pressed(keystroke, cx);
+ },
+ );
// Editor Actions
cx.add_action(|_: &mut Editor, _: &Cancel, cx| {
@@ -208,6 +220,27 @@ impl Vim {
self.state.operator_stack.last().copied()
}
+ fn key_pressed(keystroke: &Keystroke, cx: &mut ViewContext<Workspace>) {
+ match Vim::read(cx).active_operator() {
+ Some(Operator::FindForward { before }) => {
+ if let Some(character) = keystroke.key.chars().next() {
+ motion::motion(Motion::FindForward { before, character }, cx)
+ }
+ }
+ Some(Operator::FindBackward { after }) => {
+ if let Some(character) = keystroke.key.chars().next() {
+ motion::motion(Motion::FindBackward { after, character }, cx)
+ }
+ }
+ Some(Operator::Replace) => match Vim::read(cx).state.mode {
+ Mode::Normal => normal_replace(&keystroke.key, cx),
+ Mode::Visual { line } => visual_replace(&keystroke.key, line, cx),
+ _ => Vim::update(cx, |vim, cx| vim.clear_operator(cx)),
+ },
+ _ => cx.propagate_action(),
+ }
+ }
+
fn set_enabled(&mut self, enabled: bool, cx: &mut MutableAppContext) {
if self.enabled != enabled {
self.enabled = enabled;
@@ -2,7 +2,7 @@ use std::borrow::Cow;
use collections::HashMap;
use editor::{
- display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Bias, ClipboardSelection,
+ display_map::ToDisplayPoint, movement, scroll::autoscroll::Autoscroll, Bias, ClipboardSelection,
};
use gpui::{actions, MutableAppContext, ViewContext};
use language::{AutoindentMode, SelectionGoal};
@@ -313,6 +313,55 @@ pub fn paste(_: &mut Workspace, _: &VisualPaste, cx: &mut ViewContext<Workspace>
});
}
+pub(crate) fn visual_replace(text: &str, line: bool, cx: &mut MutableAppContext) {
+ Vim::update(cx, |vim, cx| {
+ vim.update_active_editor(cx, |editor, cx| {
+ editor.transact(cx, |editor, cx| {
+ let (display_map, selections) = editor.selections.all_adjusted_display(cx);
+
+ // Selections are biased right at the start. So we need to store
+ // anchors that are biased left so that we can restore the selections
+ // after the change
+ let stable_anchors = editor
+ .selections
+ .disjoint_anchors()
+ .into_iter()
+ .map(|selection| {
+ let start = selection.start.bias_left(&display_map.buffer_snapshot);
+ start..start
+ })
+ .collect::<Vec<_>>();
+
+ let mut edits = Vec::new();
+ for selection in selections.iter() {
+ let mut selection = selection.clone();
+ if !line && !selection.reversed {
+ // Head is at the end of the selection. Adjust the end position to
+ // to include the character under the cursor.
+ *selection.end.column_mut() = selection.end.column() + 1;
+ selection.end = display_map.clip_point(selection.end, Bias::Right);
+ }
+
+ for row_range in
+ movement::split_display_range_by_lines(&display_map, selection.range())
+ {
+ let range = row_range.start.to_offset(&display_map, Bias::Right)
+ ..row_range.end.to_offset(&display_map, Bias::Right);
+ let text = text.repeat(range.len());
+ edits.push((range, text));
+ }
+ }
+
+ editor.buffer().update(cx, |buffer, cx| {
+ buffer.edit(edits, None, cx);
+ });
+ editor.change_selections(None, cx, |s| s.select_ranges(stable_anchors));
+ });
+ });
+ vim.switch_mode(Mode::Normal, false, cx);
+ });
+}
+
#[cfg(test)]
mod test {
use indoc::indoc;