@@ -185,6 +185,7 @@ impl Vim {
Some(Operator::ReplaceWithRegister) => {
self.replace_with_register_motion(motion, times, window, cx)
}
+ Some(Operator::Exchange) => self.exchange_motion(motion, times, window, cx),
Some(operator) => {
// Can't do anything for text objects, Ignoring
error!("Unexpected normal mode motion operator: {:?}", operator)
@@ -234,6 +235,7 @@ impl Vim {
Some(Operator::ReplaceWithRegister) => {
self.replace_with_register_object(object, around, window, cx)
}
+ Some(Operator::Exchange) => self.exchange_object(object, around, window, cx),
_ => {
// Can't do anything for namespace operators. Ignoring
}
@@ -1,11 +1,15 @@
use crate::{
- motion::{self},
+ motion::{self, Motion},
+ object::Object,
state::Mode,
Vim,
};
-use editor::{display_map::ToDisplayPoint, Bias, Editor, ToPoint};
+use editor::{
+ display_map::ToDisplayPoint, scroll::Autoscroll, Anchor, Bias, Editor, EditorSnapshot,
+ ToOffset, ToPoint,
+};
use gpui::{actions, Context, Window};
-use language::Point;
+use language::{Point, SelectionGoal};
use std::ops::Range;
use std::sync::Arc;
@@ -27,6 +31,8 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
});
}
+struct VimExchange;
+
impl Vim {
pub(crate) fn multi_replace(
&mut self,
@@ -124,6 +130,139 @@ impl Vim {
});
});
}
+
+ pub fn exchange_object(
+ &mut self,
+ object: Object,
+ around: bool,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ self.stop_recording(cx);
+ self.update_editor(window, cx, |vim, editor, window, cx| {
+ editor.set_clip_at_line_ends(false, cx);
+ let mut selection = editor.selections.newest_display(cx);
+ let snapshot = editor.snapshot(window, cx);
+ object.expand_selection(&snapshot, &mut selection, around);
+ let start = snapshot
+ .buffer_snapshot
+ .anchor_before(selection.start.to_point(&snapshot));
+ let end = snapshot
+ .buffer_snapshot
+ .anchor_before(selection.end.to_point(&snapshot));
+ let new_range = start..end;
+ vim.exchange_impl(new_range, editor, &snapshot, window, cx);
+ editor.set_clip_at_line_ends(true, cx);
+ });
+ }
+
+ pub fn exchange_visual(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+ self.stop_recording(cx);
+ self.update_editor(window, cx, |vim, editor, window, cx| {
+ let selection = editor.selections.newest_anchor();
+ let new_range = selection.start..selection.end;
+ let snapshot = editor.snapshot(window, cx);
+ vim.exchange_impl(new_range, editor, &snapshot, window, cx);
+ });
+ self.switch_mode(Mode::Normal, false, window, cx);
+ }
+
+ pub fn clear_exchange(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+ self.stop_recording(cx);
+ self.update_editor(window, cx, |_, editor, _, cx| {
+ editor.clear_highlights::<VimExchange>(cx);
+ });
+ }
+
+ pub fn exchange_motion(
+ &mut self,
+ motion: Motion,
+ times: Option<usize>,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ self.stop_recording(cx);
+ self.update_editor(window, cx, |vim, editor, window, cx| {
+ editor.set_clip_at_line_ends(false, cx);
+ let text_layout_details = editor.text_layout_details(window);
+ let mut selection = editor.selections.newest_display(cx);
+ let snapshot = editor.snapshot(window, cx);
+ motion.expand_selection(
+ &snapshot,
+ &mut selection,
+ times,
+ false,
+ &text_layout_details,
+ );
+ let start = snapshot
+ .buffer_snapshot
+ .anchor_before(selection.start.to_point(&snapshot));
+ let end = snapshot
+ .buffer_snapshot
+ .anchor_before(selection.end.to_point(&snapshot));
+ let new_range = start..end;
+ vim.exchange_impl(new_range, editor, &snapshot, window, cx);
+ editor.set_clip_at_line_ends(true, cx);
+ });
+ }
+
+ pub fn exchange_impl(
+ &self,
+ new_range: Range<Anchor>,
+ editor: &mut Editor,
+ snapshot: &EditorSnapshot,
+ window: &mut Window,
+ cx: &mut Context<Editor>,
+ ) {
+ if let Some((_, ranges)) = editor.clear_background_highlights::<VimExchange>(cx) {
+ let previous_range = ranges[0].clone();
+
+ let new_range_start = new_range.start.to_offset(&snapshot.buffer_snapshot);
+ let new_range_end = new_range.end.to_offset(&snapshot.buffer_snapshot);
+ let previous_range_end = previous_range.end.to_offset(&snapshot.buffer_snapshot);
+ let previous_range_start = previous_range.start.to_offset(&snapshot.buffer_snapshot);
+
+ let text_for = |range: Range<Anchor>| {
+ snapshot
+ .buffer_snapshot
+ .text_for_range(range)
+ .collect::<String>()
+ };
+
+ let mut final_cursor_position = None;
+
+ if previous_range_end < new_range_start || new_range_end < previous_range_start {
+ let previous_text = text_for(previous_range.clone());
+ let new_text = text_for(new_range.clone());
+ final_cursor_position = Some(new_range.start.to_display_point(snapshot));
+
+ editor.edit([(previous_range, new_text), (new_range, previous_text)], cx);
+ } else if new_range_start <= previous_range_start && new_range_end >= previous_range_end
+ {
+ final_cursor_position = Some(new_range.start.to_display_point(snapshot));
+ editor.edit([(new_range, text_for(previous_range))], cx);
+ } else if previous_range_start <= new_range_start && previous_range_end >= new_range_end
+ {
+ final_cursor_position = Some(previous_range.start.to_display_point(snapshot));
+ editor.edit([(previous_range, text_for(new_range))], cx);
+ }
+
+ if let Some(position) = final_cursor_position {
+ editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ s.move_with(|_map, selection| {
+ selection.collapse_to(position, SelectionGoal::None);
+ });
+ })
+ }
+ } else {
+ let ranges = [new_range];
+ editor.highlight_background::<VimExchange>(
+ &ranges,
+ |theme| theme.editor_document_highlight_read_background,
+ cx,
+ );
+ }
+ }
}
#[cfg(test)]
@@ -311,4 +450,37 @@ mod test {
cx.simulate_keystrokes("0 shift-r b b b escape u");
cx.assert_state("ˇaaaa", Mode::Normal);
}
+
+ #[gpui::test]
+ async fn test_exchange_separate_range(cx: &mut gpui::TestAppContext) {
+ let mut cx = VimTestContext::new(cx, true).await;
+
+ cx.set_state("ˇhello world", Mode::Normal);
+ cx.simulate_keystrokes("c x i w w c x i w");
+ cx.assert_state("world ˇhello", Mode::Normal);
+ }
+
+ #[gpui::test]
+ async fn test_exchange_complete_overlap(cx: &mut gpui::TestAppContext) {
+ let mut cx = VimTestContext::new(cx, true).await;
+
+ cx.set_state("ˇhello world", Mode::Normal);
+ cx.simulate_keystrokes("c x x w c x i w");
+ cx.assert_state("ˇworld", Mode::Normal);
+
+ // the focus should still be at the start of the word if we reverse the
+ // order of selections (smaller -> larger)
+ cx.set_state("ˇhello world", Mode::Normal);
+ cx.simulate_keystrokes("c x i w c x x");
+ cx.assert_state("ˇhello", Mode::Normal);
+ }
+
+ #[gpui::test]
+ async fn test_exchange_partial_overlap(cx: &mut gpui::TestAppContext) {
+ let mut cx = VimTestContext::new(cx, true).await;
+
+ cx.set_state("ˇhello world", Mode::Normal);
+ cx.simulate_keystrokes("c x t r w c x i w");
+ cx.assert_state("hello ˇworld", Mode::Normal);
+ }
}
@@ -110,6 +110,7 @@ pub enum Operator {
ReplayRegister,
ToggleComments,
ReplaceWithRegister,
+ Exchange,
}
#[derive(Default, Clone, Debug)]
@@ -501,6 +502,7 @@ impl Operator {
Operator::ShellCommand => "sh",
Operator::Rewrap => "gq",
Operator::ReplaceWithRegister => "gr",
+ Operator::Exchange => "cx",
Operator::Outdent => "<",
Operator::Uppercase => "gU",
Operator::Lowercase => "gu",
@@ -554,6 +556,7 @@ impl Operator {
| Operator::Lowercase
| Operator::Uppercase
| Operator::ReplaceWithRegister
+ | Operator::Exchange
| Operator::Object { .. }
| Operator::ChangeSurrounds { target: None }
| Operator::OppositeCase
@@ -126,6 +126,7 @@ actions!(
SwitchToVisualBlockMode,
SwitchToHelixNormalMode,
ClearOperators,
+ ClearExchange,
Tab,
Enter,
InnerObject,
@@ -138,6 +139,7 @@ actions!(
ResizePaneDown,
PushChange,
PushDelete,
+ Exchange,
PushYank,
PushReplace,
PushDeleteSurrounds,
@@ -637,6 +639,18 @@ impl Vim {
},
);
+ Vim::action(editor, cx, |vim, _: &Exchange, window, cx| {
+ if vim.mode.is_visual() {
+ vim.exchange_visual(window, cx)
+ } else {
+ vim.push_operator(Operator::Exchange, window, cx)
+ }
+ });
+
+ Vim::action(editor, cx, |vim, _: &ClearExchange, window, cx| {
+ vim.clear_exchange(window, cx)
+ });
+
Vim::action(editor, cx, |vim, _: &PushToggleComments, window, cx| {
vim.push_operator(Operator::ToggleComments, window, cx)
});