Add visual area repeating
This commit is contained in:
parent
f22d53eef9
commit
1b1d7f22cc
13 changed files with 393 additions and 64 deletions
|
@ -446,12 +446,10 @@
|
||||||
],
|
],
|
||||||
"s": "vim::Substitute",
|
"s": "vim::Substitute",
|
||||||
"shift-s": "vim::SubstituteLine",
|
"shift-s": "vim::SubstituteLine",
|
||||||
|
"shift-r": "vim::SubstituteLine",
|
||||||
"c": "vim::Substitute",
|
"c": "vim::Substitute",
|
||||||
"~": "vim::ChangeCase",
|
"~": "vim::ChangeCase",
|
||||||
"shift-i": [
|
"shift-i": "vim::InsertBefore",
|
||||||
"vim::SwitchMode",
|
|
||||||
"Insert"
|
|
||||||
],
|
|
||||||
"shift-a": "vim::InsertAfter",
|
"shift-a": "vim::InsertAfter",
|
||||||
"r": [
|
"r": [
|
||||||
"vim::PushOperator",
|
"vim::PushOperator",
|
||||||
|
|
|
@ -572,7 +572,7 @@ pub struct Editor {
|
||||||
project: Option<ModelHandle<Project>>,
|
project: Option<ModelHandle<Project>>,
|
||||||
focused: bool,
|
focused: bool,
|
||||||
blink_manager: ModelHandle<BlinkManager>,
|
blink_manager: ModelHandle<BlinkManager>,
|
||||||
show_local_selections: bool,
|
pub show_local_selections: bool,
|
||||||
mode: EditorMode,
|
mode: EditorMode,
|
||||||
replica_id_mapping: Option<HashMap<ReplicaId, ReplicaId>>,
|
replica_id_mapping: Option<HashMap<ReplicaId, ReplicaId>>,
|
||||||
show_gutter: bool,
|
show_gutter: bool,
|
||||||
|
|
|
@ -65,9 +65,9 @@ struct PreviousWordStart {
|
||||||
|
|
||||||
#[derive(Clone, Deserialize, PartialEq)]
|
#[derive(Clone, Deserialize, PartialEq)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct Up {
|
pub(crate) struct Up {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
display_lines: bool,
|
pub(crate) display_lines: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Deserialize, PartialEq)]
|
#[derive(Clone, Deserialize, PartialEq)]
|
||||||
|
@ -93,9 +93,9 @@ struct EndOfLine {
|
||||||
|
|
||||||
#[derive(Clone, Deserialize, PartialEq)]
|
#[derive(Clone, Deserialize, PartialEq)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
struct StartOfLine {
|
pub struct StartOfLine {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
display_lines: bool,
|
pub(crate) display_lines: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Deserialize, PartialEq)]
|
#[derive(Clone, Deserialize, PartialEq)]
|
||||||
|
|
|
@ -66,21 +66,21 @@ pub fn init(cx: &mut AppContext) {
|
||||||
|
|
||||||
cx.add_action(|_: &mut Workspace, _: &DeleteLeft, cx| {
|
cx.add_action(|_: &mut Workspace, _: &DeleteLeft, cx| {
|
||||||
Vim::update(cx, |vim, cx| {
|
Vim::update(cx, |vim, cx| {
|
||||||
vim.record_current_action();
|
vim.record_current_action(cx);
|
||||||
let times = vim.pop_number_operator(cx);
|
let times = vim.pop_number_operator(cx);
|
||||||
delete_motion(vim, Motion::Left, times, cx);
|
delete_motion(vim, Motion::Left, times, cx);
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
cx.add_action(|_: &mut Workspace, _: &DeleteRight, cx| {
|
cx.add_action(|_: &mut Workspace, _: &DeleteRight, cx| {
|
||||||
Vim::update(cx, |vim, cx| {
|
Vim::update(cx, |vim, cx| {
|
||||||
vim.record_current_action();
|
vim.record_current_action(cx);
|
||||||
let times = vim.pop_number_operator(cx);
|
let times = vim.pop_number_operator(cx);
|
||||||
delete_motion(vim, Motion::Right, times, cx);
|
delete_motion(vim, Motion::Right, times, cx);
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
cx.add_action(|_: &mut Workspace, _: &ChangeToEndOfLine, cx| {
|
cx.add_action(|_: &mut Workspace, _: &ChangeToEndOfLine, cx| {
|
||||||
Vim::update(cx, |vim, cx| {
|
Vim::update(cx, |vim, cx| {
|
||||||
vim.start_recording();
|
vim.start_recording(cx);
|
||||||
let times = vim.pop_number_operator(cx);
|
let times = vim.pop_number_operator(cx);
|
||||||
change_motion(
|
change_motion(
|
||||||
vim,
|
vim,
|
||||||
|
@ -94,7 +94,7 @@ pub fn init(cx: &mut AppContext) {
|
||||||
});
|
});
|
||||||
cx.add_action(|_: &mut Workspace, _: &DeleteToEndOfLine, cx| {
|
cx.add_action(|_: &mut Workspace, _: &DeleteToEndOfLine, cx| {
|
||||||
Vim::update(cx, |vim, cx| {
|
Vim::update(cx, |vim, cx| {
|
||||||
vim.record_current_action();
|
vim.record_current_action(cx);
|
||||||
let times = vim.pop_number_operator(cx);
|
let times = vim.pop_number_operator(cx);
|
||||||
delete_motion(
|
delete_motion(
|
||||||
vim,
|
vim,
|
||||||
|
@ -161,7 +161,7 @@ fn move_cursor(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &mut Win
|
||||||
|
|
||||||
fn insert_after(_: &mut Workspace, _: &InsertAfter, cx: &mut ViewContext<Workspace>) {
|
fn insert_after(_: &mut Workspace, _: &InsertAfter, cx: &mut ViewContext<Workspace>) {
|
||||||
Vim::update(cx, |vim, cx| {
|
Vim::update(cx, |vim, cx| {
|
||||||
vim.start_recording();
|
vim.start_recording(cx);
|
||||||
vim.switch_mode(Mode::Insert, false, cx);
|
vim.switch_mode(Mode::Insert, false, cx);
|
||||||
vim.update_active_editor(cx, |editor, cx| {
|
vim.update_active_editor(cx, |editor, cx| {
|
||||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||||
|
@ -175,7 +175,7 @@ fn insert_after(_: &mut Workspace, _: &InsertAfter, cx: &mut ViewContext<Workspa
|
||||||
|
|
||||||
fn insert_before(_: &mut Workspace, _: &InsertBefore, cx: &mut ViewContext<Workspace>) {
|
fn insert_before(_: &mut Workspace, _: &InsertBefore, cx: &mut ViewContext<Workspace>) {
|
||||||
Vim::update(cx, |vim, cx| {
|
Vim::update(cx, |vim, cx| {
|
||||||
vim.start_recording();
|
vim.start_recording(cx);
|
||||||
vim.switch_mode(Mode::Insert, false, cx);
|
vim.switch_mode(Mode::Insert, false, cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -186,7 +186,7 @@ fn insert_first_non_whitespace(
|
||||||
cx: &mut ViewContext<Workspace>,
|
cx: &mut ViewContext<Workspace>,
|
||||||
) {
|
) {
|
||||||
Vim::update(cx, |vim, cx| {
|
Vim::update(cx, |vim, cx| {
|
||||||
vim.start_recording();
|
vim.start_recording(cx);
|
||||||
vim.switch_mode(Mode::Insert, false, cx);
|
vim.switch_mode(Mode::Insert, false, cx);
|
||||||
vim.update_active_editor(cx, |editor, cx| {
|
vim.update_active_editor(cx, |editor, cx| {
|
||||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||||
|
@ -203,7 +203,7 @@ fn insert_first_non_whitespace(
|
||||||
|
|
||||||
fn insert_end_of_line(_: &mut Workspace, _: &InsertEndOfLine, cx: &mut ViewContext<Workspace>) {
|
fn insert_end_of_line(_: &mut Workspace, _: &InsertEndOfLine, cx: &mut ViewContext<Workspace>) {
|
||||||
Vim::update(cx, |vim, cx| {
|
Vim::update(cx, |vim, cx| {
|
||||||
vim.start_recording();
|
vim.start_recording(cx);
|
||||||
vim.switch_mode(Mode::Insert, false, cx);
|
vim.switch_mode(Mode::Insert, false, cx);
|
||||||
vim.update_active_editor(cx, |editor, cx| {
|
vim.update_active_editor(cx, |editor, cx| {
|
||||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||||
|
@ -217,7 +217,7 @@ fn insert_end_of_line(_: &mut Workspace, _: &InsertEndOfLine, cx: &mut ViewConte
|
||||||
|
|
||||||
fn insert_line_above(_: &mut Workspace, _: &InsertLineAbove, cx: &mut ViewContext<Workspace>) {
|
fn insert_line_above(_: &mut Workspace, _: &InsertLineAbove, cx: &mut ViewContext<Workspace>) {
|
||||||
Vim::update(cx, |vim, cx| {
|
Vim::update(cx, |vim, cx| {
|
||||||
vim.start_recording();
|
vim.start_recording(cx);
|
||||||
vim.switch_mode(Mode::Insert, false, cx);
|
vim.switch_mode(Mode::Insert, false, cx);
|
||||||
vim.update_active_editor(cx, |editor, cx| {
|
vim.update_active_editor(cx, |editor, cx| {
|
||||||
editor.transact(cx, |editor, cx| {
|
editor.transact(cx, |editor, cx| {
|
||||||
|
@ -250,7 +250,7 @@ fn insert_line_above(_: &mut Workspace, _: &InsertLineAbove, cx: &mut ViewContex
|
||||||
|
|
||||||
fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContext<Workspace>) {
|
fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContext<Workspace>) {
|
||||||
Vim::update(cx, |vim, cx| {
|
Vim::update(cx, |vim, cx| {
|
||||||
vim.start_recording();
|
vim.start_recording(cx);
|
||||||
vim.switch_mode(Mode::Insert, false, cx);
|
vim.switch_mode(Mode::Insert, false, cx);
|
||||||
vim.update_active_editor(cx, |editor, cx| {
|
vim.update_active_editor(cx, |editor, cx| {
|
||||||
editor.transact(cx, |editor, cx| {
|
editor.transact(cx, |editor, cx| {
|
||||||
|
|
|
@ -7,7 +7,7 @@ use crate::{normal::ChangeCase, state::Mode, Vim};
|
||||||
|
|
||||||
pub fn change_case(_: &mut Workspace, _: &ChangeCase, cx: &mut ViewContext<Workspace>) {
|
pub fn change_case(_: &mut Workspace, _: &ChangeCase, cx: &mut ViewContext<Workspace>) {
|
||||||
Vim::update(cx, |vim, cx| {
|
Vim::update(cx, |vim, cx| {
|
||||||
vim.record_current_action();
|
vim.record_current_action(cx);
|
||||||
let count = vim.pop_number_operator(cx).unwrap_or(1) as u32;
|
let count = vim.pop_number_operator(cx).unwrap_or(1) as u32;
|
||||||
vim.update_active_editor(cx, |editor, cx| {
|
vim.update_active_editor(cx, |editor, cx| {
|
||||||
let mut ranges = Vec::new();
|
let mut ranges = Vec::new();
|
||||||
|
@ -22,10 +22,16 @@ pub fn change_case(_: &mut Workspace, _: &ChangeCase, cx: &mut ViewContext<Works
|
||||||
ranges.push(start..end);
|
ranges.push(start..end);
|
||||||
cursor_positions.push(start..start);
|
cursor_positions.push(start..start);
|
||||||
}
|
}
|
||||||
Mode::Visual | Mode::VisualBlock => {
|
Mode::Visual => {
|
||||||
ranges.push(selection.start..selection.end);
|
ranges.push(selection.start..selection.end);
|
||||||
cursor_positions.push(selection.start..selection.start);
|
cursor_positions.push(selection.start..selection.start);
|
||||||
}
|
}
|
||||||
|
Mode::VisualBlock => {
|
||||||
|
ranges.push(selection.start..selection.end);
|
||||||
|
if cursor_positions.len() == 0 {
|
||||||
|
cursor_positions.push(selection.start..selection.start);
|
||||||
|
}
|
||||||
|
}
|
||||||
Mode::Insert | Mode::Normal => {
|
Mode::Insert | Mode::Normal => {
|
||||||
let start = selection.start;
|
let start = selection.start;
|
||||||
let mut end = start;
|
let mut end = start;
|
||||||
|
@ -97,6 +103,11 @@ mod test {
|
||||||
cx.simulate_shared_keystrokes(["shift-v", "~"]).await;
|
cx.simulate_shared_keystrokes(["shift-v", "~"]).await;
|
||||||
cx.assert_shared_state("ˇABc\n").await;
|
cx.assert_shared_state("ˇABc\n").await;
|
||||||
|
|
||||||
|
// works in visual block mode
|
||||||
|
cx.set_shared_state("ˇaa\nbb\ncc").await;
|
||||||
|
cx.simulate_shared_keystrokes(["ctrl-v", "j", "~"]).await;
|
||||||
|
cx.assert_shared_state("ˇAa\nBb\ncc").await;
|
||||||
|
|
||||||
// works with multiple cursors (zed only)
|
// works with multiple cursors (zed only)
|
||||||
cx.set_state("aˇßcdˇe\n", Mode::Normal);
|
cx.set_state("aˇßcdˇe\n", Mode::Normal);
|
||||||
cx.simulate_keystroke("~");
|
cx.simulate_keystroke("~");
|
||||||
|
|
|
@ -28,7 +28,7 @@ pub(crate) fn init(cx: &mut AppContext) {
|
||||||
|
|
||||||
fn paste(_: &mut Workspace, action: &Paste, cx: &mut ViewContext<Workspace>) {
|
fn paste(_: &mut Workspace, action: &Paste, cx: &mut ViewContext<Workspace>) {
|
||||||
Vim::update(cx, |vim, cx| {
|
Vim::update(cx, |vim, cx| {
|
||||||
vim.record_current_action();
|
vim.record_current_action(cx);
|
||||||
vim.update_active_editor(cx, |editor, cx| {
|
vim.update_active_editor(cx, |editor, cx| {
|
||||||
editor.transact(cx, |editor, cx| {
|
editor.transact(cx, |editor, cx| {
|
||||||
editor.set_clip_at_line_ends(false, cx);
|
editor.set_clip_at_line_ends(false, cx);
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
state::{Mode, ReplayableAction},
|
motion::Motion,
|
||||||
|
state::{Mode, RecordedSelection, ReplayableAction},
|
||||||
|
visual::visual_motion,
|
||||||
Vim,
|
Vim,
|
||||||
};
|
};
|
||||||
use gpui::{actions, AppContext};
|
use gpui::{actions, AppContext};
|
||||||
|
@ -11,47 +13,127 @@ pub(crate) fn init(cx: &mut AppContext) {
|
||||||
cx.add_action(|_: &mut Workspace, _: &EndRepeat, cx| {
|
cx.add_action(|_: &mut Workspace, _: &EndRepeat, cx| {
|
||||||
Vim::update(cx, |vim, cx| {
|
Vim::update(cx, |vim, cx| {
|
||||||
vim.workspace_state.replaying = false;
|
vim.workspace_state.replaying = false;
|
||||||
|
vim.update_active_editor(cx, |editor, _| {
|
||||||
|
editor.show_local_selections = true;
|
||||||
|
});
|
||||||
vim.switch_mode(Mode::Normal, false, cx)
|
vim.switch_mode(Mode::Normal, false, cx)
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.add_action(|_: &mut Workspace, _: &Repeat, cx| {
|
cx.add_action(|_: &mut Workspace, _: &Repeat, cx| {
|
||||||
Vim::update(cx, |vim, cx| {
|
let Some((actions, editor, selection)) = Vim::update(cx, |vim, cx| {
|
||||||
let actions = vim.workspace_state.repeat_actions.clone();
|
let actions = vim.workspace_state.recorded_actions.clone();
|
||||||
let Some(editor) = vim.active_editor.clone() else {
|
let Some(editor) = vim.active_editor.clone() else {
|
||||||
return;
|
return None;
|
||||||
};
|
};
|
||||||
if let Some(new_count) = vim.pop_number_operator(cx) {
|
let count = vim.pop_number_operator(cx);
|
||||||
vim.workspace_state.recorded_count = Some(new_count);
|
|
||||||
}
|
|
||||||
vim.workspace_state.replaying = true;
|
vim.workspace_state.replaying = true;
|
||||||
|
|
||||||
let window = cx.window();
|
let selection = vim.workspace_state.recorded_selection.clone();
|
||||||
cx.app_context()
|
match selection {
|
||||||
.spawn(move |mut cx| async move {
|
RecordedSelection::SingleLine { .. } | RecordedSelection::Visual { .. } => {
|
||||||
for action in actions {
|
vim.workspace_state.recorded_count = None;
|
||||||
match action {
|
vim.switch_mode(Mode::Visual, false, cx)
|
||||||
ReplayableAction::Action(action) => window
|
}
|
||||||
.dispatch_action(editor.id(), action.as_ref(), &mut cx)
|
RecordedSelection::VisualLine { .. } => {
|
||||||
.ok_or_else(|| anyhow::anyhow!("window was closed")),
|
vim.workspace_state.recorded_count = None;
|
||||||
ReplayableAction::Insertion {
|
vim.switch_mode(Mode::VisualLine, false, cx)
|
||||||
text,
|
}
|
||||||
utf16_range_to_replace,
|
RecordedSelection::VisualBlock { .. } => {
|
||||||
} => editor.update(&mut cx, |editor, cx| {
|
vim.workspace_state.recorded_count = None;
|
||||||
editor.replay_insert_event(
|
vim.switch_mode(Mode::VisualBlock, false, cx)
|
||||||
&text,
|
}
|
||||||
utf16_range_to_replace.clone(),
|
RecordedSelection::None => {
|
||||||
cx,
|
if let Some(count) = count {
|
||||||
)
|
vim.workspace_state.recorded_count = Some(count);
|
||||||
}),
|
|
||||||
}?
|
|
||||||
}
|
}
|
||||||
window
|
}
|
||||||
.dispatch_action(editor.id(), &EndRepeat, &mut cx)
|
}
|
||||||
.ok_or_else(|| anyhow::anyhow!("window was closed"))
|
|
||||||
|
if let Some(editor) = editor.upgrade(cx) {
|
||||||
|
editor.update(cx, |editor, _| {
|
||||||
|
editor.show_local_selections = false;
|
||||||
})
|
})
|
||||||
.detach_and_log_err(cx);
|
} else {
|
||||||
});
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some((actions, editor, selection))
|
||||||
|
}) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
match selection {
|
||||||
|
RecordedSelection::SingleLine { cols } => {
|
||||||
|
if cols > 1 {
|
||||||
|
visual_motion(Motion::Right, Some(cols as usize - 1), cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RecordedSelection::Visual { rows, cols } => {
|
||||||
|
visual_motion(
|
||||||
|
Motion::Down {
|
||||||
|
display_lines: false,
|
||||||
|
},
|
||||||
|
Some(rows as usize),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
visual_motion(
|
||||||
|
Motion::StartOfLine {
|
||||||
|
display_lines: false,
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
if cols > 1 {
|
||||||
|
visual_motion(Motion::Right, Some(cols as usize - 1), cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RecordedSelection::VisualBlock { rows, cols } => {
|
||||||
|
visual_motion(
|
||||||
|
Motion::Down {
|
||||||
|
display_lines: false,
|
||||||
|
},
|
||||||
|
Some(rows as usize),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
if cols > 1 {
|
||||||
|
visual_motion(Motion::Right, Some(cols as usize - 1), cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RecordedSelection::VisualLine { rows } => {
|
||||||
|
visual_motion(
|
||||||
|
Motion::Down {
|
||||||
|
display_lines: false,
|
||||||
|
},
|
||||||
|
Some(rows as usize),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
RecordedSelection::None => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
let window = cx.window();
|
||||||
|
cx.app_context()
|
||||||
|
.spawn(move |mut cx| async move {
|
||||||
|
for action in actions {
|
||||||
|
match action {
|
||||||
|
ReplayableAction::Action(action) => window
|
||||||
|
.dispatch_action(editor.id(), action.as_ref(), &mut cx)
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("window was closed")),
|
||||||
|
ReplayableAction::Insertion {
|
||||||
|
text,
|
||||||
|
utf16_range_to_replace,
|
||||||
|
} => editor.update(&mut cx, |editor, cx| {
|
||||||
|
editor.replay_insert_event(&text, utf16_range_to_replace.clone(), cx)
|
||||||
|
}),
|
||||||
|
}?
|
||||||
|
}
|
||||||
|
window
|
||||||
|
.dispatch_action(editor.id(), &EndRepeat, &mut cx)
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("window was closed"))
|
||||||
|
})
|
||||||
|
.detach_and_log_err(cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -204,4 +286,128 @@ mod test {
|
||||||
Mode::Normal,
|
Mode::Normal,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_repeat_visual(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
|
||||||
|
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||||
|
|
||||||
|
// single-line (3 columns)
|
||||||
|
cx.set_shared_state(indoc! {
|
||||||
|
"ˇthe quick brown
|
||||||
|
fox jumps over
|
||||||
|
the lazy dog"
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
cx.simulate_shared_keystrokes(["v", "i", "w", "s", "o", "escape"])
|
||||||
|
.await;
|
||||||
|
cx.assert_shared_state(indoc! {
|
||||||
|
"ˇo quick brown
|
||||||
|
fox jumps over
|
||||||
|
the lazy dog"
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
cx.simulate_shared_keystrokes(["j", "w", "."]).await;
|
||||||
|
deterministic.run_until_parked();
|
||||||
|
cx.assert_shared_state(indoc! {
|
||||||
|
"o quick brown
|
||||||
|
fox ˇops over
|
||||||
|
the lazy dog"
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
cx.simulate_shared_keystrokes(["f", "r", "."]).await;
|
||||||
|
deterministic.run_until_parked();
|
||||||
|
cx.assert_shared_state(indoc! {
|
||||||
|
"o quick brown
|
||||||
|
fox ops oveˇothe lazy dog"
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// visual
|
||||||
|
cx.set_shared_state(indoc! {
|
||||||
|
"the ˇquick brown
|
||||||
|
fox jumps over
|
||||||
|
fox jumps over
|
||||||
|
fox jumps over
|
||||||
|
the lazy dog"
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
cx.simulate_shared_keystrokes(["v", "j", "x"]).await;
|
||||||
|
cx.assert_shared_state(indoc! {
|
||||||
|
"the ˇumps over
|
||||||
|
fox jumps over
|
||||||
|
fox jumps over
|
||||||
|
the lazy dog"
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
cx.simulate_shared_keystrokes(["."]).await;
|
||||||
|
deterministic.run_until_parked();
|
||||||
|
cx.assert_shared_state(indoc! {
|
||||||
|
"the ˇumps over
|
||||||
|
fox jumps over
|
||||||
|
the lazy dog"
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
cx.simulate_shared_keystrokes(["w", "."]).await;
|
||||||
|
deterministic.run_until_parked();
|
||||||
|
cx.assert_shared_state(indoc! {
|
||||||
|
"the umps ˇumps over
|
||||||
|
the lazy dog"
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
cx.simulate_shared_keystrokes(["j", "."]).await;
|
||||||
|
deterministic.run_until_parked();
|
||||||
|
cx.assert_shared_state(indoc! {
|
||||||
|
"the umps umps over
|
||||||
|
the ˇog"
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// block mode (3 rows)
|
||||||
|
cx.set_shared_state(indoc! {
|
||||||
|
"ˇthe quick brown
|
||||||
|
fox jumps over
|
||||||
|
the lazy dog"
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
cx.simulate_shared_keystrokes(["ctrl-v", "j", "j", "shift-i", "o", "escape"])
|
||||||
|
.await;
|
||||||
|
cx.assert_shared_state(indoc! {
|
||||||
|
"ˇothe quick brown
|
||||||
|
ofox jumps over
|
||||||
|
othe lazy dog"
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
cx.simulate_shared_keystrokes(["j", "4", "l", "."]).await;
|
||||||
|
deterministic.run_until_parked();
|
||||||
|
cx.assert_shared_state(indoc! {
|
||||||
|
"othe quick brown
|
||||||
|
ofoxˇo jumps over
|
||||||
|
otheo lazy dog"
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// line mode
|
||||||
|
cx.set_shared_state(indoc! {
|
||||||
|
"ˇthe quick brown
|
||||||
|
fox jumps over
|
||||||
|
the lazy dog"
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
cx.simulate_shared_keystrokes(["shift-v", "shift-r", "o", "escape"])
|
||||||
|
.await;
|
||||||
|
cx.assert_shared_state(indoc! {
|
||||||
|
"ˇo
|
||||||
|
fox jumps over
|
||||||
|
the lazy dog"
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
cx.simulate_shared_keystrokes(["j", "."]).await;
|
||||||
|
deterministic.run_until_parked();
|
||||||
|
cx.assert_shared_state(indoc! {
|
||||||
|
"o
|
||||||
|
ˇo
|
||||||
|
the lazy dog"
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ actions!(vim, [Substitute, SubstituteLine]);
|
||||||
pub(crate) fn init(cx: &mut AppContext) {
|
pub(crate) fn init(cx: &mut AppContext) {
|
||||||
cx.add_action(|_: &mut Workspace, _: &Substitute, cx| {
|
cx.add_action(|_: &mut Workspace, _: &Substitute, cx| {
|
||||||
Vim::update(cx, |vim, cx| {
|
Vim::update(cx, |vim, cx| {
|
||||||
|
vim.start_recording(cx);
|
||||||
let count = vim.pop_number_operator(cx);
|
let count = vim.pop_number_operator(cx);
|
||||||
substitute(vim, count, vim.state().mode == Mode::VisualLine, cx);
|
substitute(vim, count, vim.state().mode == Mode::VisualLine, cx);
|
||||||
})
|
})
|
||||||
|
@ -17,6 +18,7 @@ pub(crate) fn init(cx: &mut AppContext) {
|
||||||
|
|
||||||
cx.add_action(|_: &mut Workspace, _: &SubstituteLine, cx| {
|
cx.add_action(|_: &mut Workspace, _: &SubstituteLine, cx| {
|
||||||
Vim::update(cx, |vim, cx| {
|
Vim::update(cx, |vim, cx| {
|
||||||
|
vim.start_recording(cx);
|
||||||
if matches!(vim.state().mode, Mode::VisualBlock | Mode::Visual) {
|
if matches!(vim.state().mode, Mode::VisualBlock | Mode::Visual) {
|
||||||
vim.switch_mode(Mode::VisualLine, false, cx)
|
vim.switch_mode(Mode::VisualLine, false, cx)
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,6 +50,26 @@ pub struct EditorState {
|
||||||
pub operator_stack: Vec<Operator>,
|
pub operator_stack: Vec<Operator>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Clone, Debug)]
|
||||||
|
pub enum RecordedSelection {
|
||||||
|
#[default]
|
||||||
|
None,
|
||||||
|
Visual {
|
||||||
|
rows: u32,
|
||||||
|
cols: u32,
|
||||||
|
},
|
||||||
|
SingleLine {
|
||||||
|
cols: u32,
|
||||||
|
},
|
||||||
|
VisualBlock {
|
||||||
|
rows: u32,
|
||||||
|
cols: u32,
|
||||||
|
},
|
||||||
|
VisualLine {
|
||||||
|
rows: u32,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Default, Clone)]
|
#[derive(Default, Clone)]
|
||||||
pub struct WorkspaceState {
|
pub struct WorkspaceState {
|
||||||
pub search: SearchState,
|
pub search: SearchState,
|
||||||
|
@ -59,7 +79,8 @@ pub struct WorkspaceState {
|
||||||
pub stop_recording_after_next_action: bool,
|
pub stop_recording_after_next_action: bool,
|
||||||
pub replaying: bool,
|
pub replaying: bool,
|
||||||
pub recorded_count: Option<usize>,
|
pub recorded_count: Option<usize>,
|
||||||
pub repeat_actions: Vec<ReplayableAction>,
|
pub recorded_actions: Vec<ReplayableAction>,
|
||||||
|
pub recorded_selection: RecordedSelection,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|
|
@ -18,13 +18,13 @@ use gpui::{
|
||||||
actions, impl_actions, keymap_matcher::KeymapContext, keymap_matcher::MatchResult, AppContext,
|
actions, impl_actions, keymap_matcher::KeymapContext, keymap_matcher::MatchResult, AppContext,
|
||||||
Subscription, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
|
Subscription, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
|
||||||
};
|
};
|
||||||
use language::{CursorShape, Selection, SelectionGoal};
|
use language::{CursorShape, Point, Selection, SelectionGoal};
|
||||||
pub use mode_indicator::ModeIndicator;
|
pub use mode_indicator::ModeIndicator;
|
||||||
use motion::Motion;
|
use motion::Motion;
|
||||||
use normal::normal_replace;
|
use normal::normal_replace;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use settings::{Setting, SettingsStore};
|
use settings::{Setting, SettingsStore};
|
||||||
use state::{EditorState, Mode, Operator, WorkspaceState};
|
use state::{EditorState, Mode, Operator, RecordedSelection, WorkspaceState};
|
||||||
use std::{ops::Range, sync::Arc};
|
use std::{ops::Range, sync::Arc};
|
||||||
use visual::{visual_block_motion, visual_replace};
|
use visual::{visual_block_motion, visual_replace};
|
||||||
use workspace::{self, Workspace};
|
use workspace::{self, Workspace};
|
||||||
|
@ -107,7 +107,7 @@ pub fn observe_keystrokes(cx: &mut WindowContext) {
|
||||||
Vim::update(cx, |vim, _| {
|
Vim::update(cx, |vim, _| {
|
||||||
if vim.workspace_state.recording {
|
if vim.workspace_state.recording {
|
||||||
vim.workspace_state
|
vim.workspace_state
|
||||||
.repeat_actions
|
.recorded_actions
|
||||||
.push(ReplayableAction::Action(handled_by.boxed_clone()));
|
.push(ReplayableAction::Action(handled_by.boxed_clone()));
|
||||||
|
|
||||||
if vim.workspace_state.stop_recording_after_next_action {
|
if vim.workspace_state.stop_recording_after_next_action {
|
||||||
|
@ -204,7 +204,7 @@ impl Vim {
|
||||||
Vim::update(cx, |vim, _| {
|
Vim::update(cx, |vim, _| {
|
||||||
if vim.workspace_state.recording {
|
if vim.workspace_state.recording {
|
||||||
vim.workspace_state
|
vim.workspace_state
|
||||||
.repeat_actions
|
.recorded_actions
|
||||||
.push(ReplayableAction::Insertion {
|
.push(ReplayableAction::Insertion {
|
||||||
text: text.clone(),
|
text: text.clone(),
|
||||||
utf16_range_to_replace: range_to_replace,
|
utf16_range_to_replace: range_to_replace,
|
||||||
|
@ -232,16 +232,51 @@ impl Vim {
|
||||||
|
|
||||||
// TODO: shift-j?
|
// TODO: shift-j?
|
||||||
//
|
//
|
||||||
pub fn start_recording(&mut self) {
|
pub fn start_recording(&mut self, cx: &mut WindowContext) {
|
||||||
if !self.workspace_state.replaying {
|
if !self.workspace_state.replaying {
|
||||||
self.workspace_state.recording = true;
|
self.workspace_state.recording = true;
|
||||||
self.workspace_state.repeat_actions = Default::default();
|
self.workspace_state.recorded_actions = Default::default();
|
||||||
self.workspace_state.recorded_count =
|
self.workspace_state.recorded_count =
|
||||||
if let Some(Operator::Number(number)) = self.active_operator() {
|
if let Some(Operator::Number(number)) = self.active_operator() {
|
||||||
Some(number)
|
Some(number)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let selections = self
|
||||||
|
.active_editor
|
||||||
|
.and_then(|editor| editor.upgrade(cx))
|
||||||
|
.map(|editor| {
|
||||||
|
let editor = editor.read(cx);
|
||||||
|
(
|
||||||
|
editor.selections.oldest::<Point>(cx),
|
||||||
|
editor.selections.newest::<Point>(cx),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some((oldest, newest)) = selections {
|
||||||
|
self.workspace_state.recorded_selection = match self.state().mode {
|
||||||
|
Mode::Visual if newest.end.row == newest.start.row => {
|
||||||
|
RecordedSelection::SingleLine {
|
||||||
|
cols: newest.end.column - newest.start.column,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Mode::Visual => RecordedSelection::Visual {
|
||||||
|
rows: newest.end.row - newest.start.row,
|
||||||
|
cols: newest.end.column,
|
||||||
|
},
|
||||||
|
Mode::VisualLine => RecordedSelection::VisualLine {
|
||||||
|
rows: newest.end.row - newest.start.row,
|
||||||
|
},
|
||||||
|
Mode::VisualBlock => RecordedSelection::VisualBlock {
|
||||||
|
rows: newest.end.row.abs_diff(oldest.start.row),
|
||||||
|
cols: newest.end.column.abs_diff(oldest.start.column),
|
||||||
|
},
|
||||||
|
_ => RecordedSelection::None,
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
self.workspace_state.recorded_selection = RecordedSelection::None;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -251,8 +286,8 @@ impl Vim {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn record_current_action(&mut self) {
|
pub fn record_current_action(&mut self, cx: &mut WindowContext) {
|
||||||
self.start_recording();
|
self.start_recording(cx);
|
||||||
self.stop_recording();
|
self.stop_recording();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -322,7 +357,7 @@ impl Vim {
|
||||||
operator,
|
operator,
|
||||||
Operator::Change | Operator::Delete | Operator::Replace
|
Operator::Change | Operator::Delete | Operator::Replace
|
||||||
) {
|
) {
|
||||||
self.start_recording()
|
self.start_recording(cx)
|
||||||
};
|
};
|
||||||
self.update_state(|state| state.operator_stack.push(operator));
|
self.update_state(|state| state.operator_stack.push(operator));
|
||||||
self.sync_vim_settings(cx);
|
self.sync_vim_settings(cx);
|
||||||
|
|
|
@ -277,7 +277,7 @@ pub fn other_end(_: &mut Workspace, _: &OtherEnd, cx: &mut ViewContext<Workspace
|
||||||
|
|
||||||
pub fn delete(_: &mut Workspace, _: &VisualDelete, cx: &mut ViewContext<Workspace>) {
|
pub fn delete(_: &mut Workspace, _: &VisualDelete, cx: &mut ViewContext<Workspace>) {
|
||||||
Vim::update(cx, |vim, cx| {
|
Vim::update(cx, |vim, cx| {
|
||||||
vim.record_current_action();
|
vim.record_current_action(cx);
|
||||||
vim.update_active_editor(cx, |editor, cx| {
|
vim.update_active_editor(cx, |editor, cx| {
|
||||||
let mut original_columns: HashMap<_, _> = Default::default();
|
let mut original_columns: HashMap<_, _> = Default::default();
|
||||||
let line_mode = editor.selections.line_mode;
|
let line_mode = editor.selections.line_mode;
|
||||||
|
|
|
@ -16,3 +16,8 @@
|
||||||
{"Key":"shift-v"}
|
{"Key":"shift-v"}
|
||||||
{"Key":"~"}
|
{"Key":"~"}
|
||||||
{"Get":{"state":"ˇABc\n","mode":"Normal"}}
|
{"Get":{"state":"ˇABc\n","mode":"Normal"}}
|
||||||
|
{"Put":{"state":"ˇaa\nbb\ncc"}}
|
||||||
|
{"Key":"ctrl-v"}
|
||||||
|
{"Key":"j"}
|
||||||
|
{"Key":"~"}
|
||||||
|
{"Get":{"state":"ˇAa\nBb\ncc","mode":"Normal"}}
|
||||||
|
|
51
crates/vim/test_data/test_repeat_visual.json
Normal file
51
crates/vim/test_data/test_repeat_visual.json
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
{"Put":{"state":"ˇthe quick brown\nfox jumps over\nthe lazy dog"}}
|
||||||
|
{"Key":"v"}
|
||||||
|
{"Key":"i"}
|
||||||
|
{"Key":"w"}
|
||||||
|
{"Key":"s"}
|
||||||
|
{"Key":"o"}
|
||||||
|
{"Key":"escape"}
|
||||||
|
{"Get":{"state":"ˇo quick brown\nfox jumps over\nthe lazy dog","mode":"Normal"}}
|
||||||
|
{"Key":"j"}
|
||||||
|
{"Key":"w"}
|
||||||
|
{"Key":"."}
|
||||||
|
{"Get":{"state":"o quick brown\nfox ˇops over\nthe lazy dog","mode":"Normal"}}
|
||||||
|
{"Key":"f"}
|
||||||
|
{"Key":"r"}
|
||||||
|
{"Key":"."}
|
||||||
|
{"Get":{"state":"o quick brown\nfox ops oveˇothe lazy dog","mode":"Normal"}}
|
||||||
|
{"Put":{"state":"the ˇquick brown\nfox jumps over\nfox jumps over\nfox jumps over\nthe lazy dog"}}
|
||||||
|
{"Key":"v"}
|
||||||
|
{"Key":"j"}
|
||||||
|
{"Key":"x"}
|
||||||
|
{"Get":{"state":"the ˇumps over\nfox jumps over\nfox jumps over\nthe lazy dog","mode":"Normal"}}
|
||||||
|
{"Key":"."}
|
||||||
|
{"Get":{"state":"the ˇumps over\nfox jumps over\nthe lazy dog","mode":"Normal"}}
|
||||||
|
{"Key":"w"}
|
||||||
|
{"Key":"."}
|
||||||
|
{"Get":{"state":"the umps ˇumps over\nthe lazy dog","mode":"Normal"}}
|
||||||
|
{"Key":"j"}
|
||||||
|
{"Key":"."}
|
||||||
|
{"Get":{"state":"the umps umps over\nthe ˇog","mode":"Normal"}}
|
||||||
|
{"Put":{"state":"ˇthe quick brown\nfox jumps over\nthe lazy dog"}}
|
||||||
|
{"Key":"ctrl-v"}
|
||||||
|
{"Key":"j"}
|
||||||
|
{"Key":"j"}
|
||||||
|
{"Key":"shift-i"}
|
||||||
|
{"Key":"o"}
|
||||||
|
{"Key":"escape"}
|
||||||
|
{"Get":{"state":"ˇothe quick brown\nofox jumps over\nothe lazy dog","mode":"Normal"}}
|
||||||
|
{"Key":"j"}
|
||||||
|
{"Key":"4"}
|
||||||
|
{"Key":"l"}
|
||||||
|
{"Key":"."}
|
||||||
|
{"Get":{"state":"othe quick brown\nofoxˇo jumps over\notheo lazy dog","mode":"Normal"}}
|
||||||
|
{"Put":{"state":"ˇthe quick brown\nfox jumps over\nthe lazy dog"}}
|
||||||
|
{"Key":"shift-v"}
|
||||||
|
{"Key":"shift-r"}
|
||||||
|
{"Key":"o"}
|
||||||
|
{"Key":"escape"}
|
||||||
|
{"Get":{"state":"ˇo\nfox jumps over\nthe lazy dog","mode":"Normal"}}
|
||||||
|
{"Key":"j"}
|
||||||
|
{"Key":"."}
|
||||||
|
{"Get":{"state":"o\nˇo\nthe lazy dog","mode":"Normal"}}
|
Loading…
Add table
Add a link
Reference in a new issue