vim . to replay (#2936)
Release Notes: - vim: Add `.` to replay ([#946](https://github.com/zed-industries/community/issues/946)) - vim: Fix `J` in visual mode, and with counts.
This commit is contained in:
@@ -34,6 +34,7 @@ fn focused(EditorFocused(editor): &EditorFocused, cx: &mut AppContext) {
|
||||
fn blurred(EditorBlurred(editor): &EditorBlurred, cx: &mut AppContext) {
|
||||
editor.window().update(cx, |cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.workspace_state.recording = false;
|
||||
if let Some(previous_editor) = vim.active_editor.clone() {
|
||||
if previous_editor == editor.clone() {
|
||||
vim.active_editor = None;
|
||||
|
||||
@@ -11,8 +11,9 @@ pub fn init(cx: &mut AppContext) {
|
||||
}
|
||||
|
||||
fn normal_before(_: &mut Workspace, _: &NormalBefore, cx: &mut ViewContext<Workspace>) {
|
||||
Vim::update(cx, |state, cx| {
|
||||
state.update_active_editor(cx, |editor, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.stop_recording();
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
s.move_cursors_with(|map, mut cursor, _| {
|
||||
*cursor.column_mut() = cursor.column().saturating_sub(1);
|
||||
@@ -20,7 +21,7 @@ fn normal_before(_: &mut Workspace, _: &NormalBefore, cx: &mut ViewContext<Works
|
||||
});
|
||||
});
|
||||
});
|
||||
state.switch_mode(Mode::Normal, false, cx);
|
||||
vim.switch_mode(Mode::Normal, false, cx);
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -65,9 +65,9 @@ struct PreviousWordStart {
|
||||
|
||||
#[derive(Clone, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct Up {
|
||||
pub(crate) struct Up {
|
||||
#[serde(default)]
|
||||
display_lines: bool,
|
||||
pub(crate) display_lines: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, PartialEq)]
|
||||
@@ -93,9 +93,9 @@ struct EndOfLine {
|
||||
|
||||
#[derive(Clone, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct StartOfLine {
|
||||
pub struct StartOfLine {
|
||||
#[serde(default)]
|
||||
display_lines: bool,
|
||||
pub(crate) display_lines: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, PartialEq)]
|
||||
|
||||
@@ -2,6 +2,7 @@ mod case;
|
||||
mod change;
|
||||
mod delete;
|
||||
mod paste;
|
||||
mod repeat;
|
||||
mod scroll;
|
||||
mod search;
|
||||
pub mod substitute;
|
||||
@@ -34,6 +35,7 @@ actions!(
|
||||
vim,
|
||||
[
|
||||
InsertAfter,
|
||||
InsertBefore,
|
||||
InsertFirstNonWhitespace,
|
||||
InsertEndOfLine,
|
||||
InsertLineAbove,
|
||||
@@ -44,32 +46,42 @@ actions!(
|
||||
DeleteToEndOfLine,
|
||||
Yank,
|
||||
ChangeCase,
|
||||
JoinLines,
|
||||
]
|
||||
);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
paste::init(cx);
|
||||
repeat::init(cx);
|
||||
scroll::init(cx);
|
||||
search::init(cx);
|
||||
substitute::init(cx);
|
||||
|
||||
cx.add_action(insert_after);
|
||||
cx.add_action(insert_before);
|
||||
cx.add_action(insert_first_non_whitespace);
|
||||
cx.add_action(insert_end_of_line);
|
||||
cx.add_action(insert_line_above);
|
||||
cx.add_action(insert_line_below);
|
||||
cx.add_action(change_case);
|
||||
substitute::init(cx);
|
||||
search::init(cx);
|
||||
|
||||
cx.add_action(|_: &mut Workspace, _: &DeleteLeft, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.record_current_action(cx);
|
||||
let times = vim.pop_number_operator(cx);
|
||||
delete_motion(vim, Motion::Left, times, cx);
|
||||
})
|
||||
});
|
||||
cx.add_action(|_: &mut Workspace, _: &DeleteRight, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.record_current_action(cx);
|
||||
let times = vim.pop_number_operator(cx);
|
||||
delete_motion(vim, Motion::Right, times, cx);
|
||||
})
|
||||
});
|
||||
cx.add_action(|_: &mut Workspace, _: &ChangeToEndOfLine, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.start_recording(cx);
|
||||
let times = vim.pop_number_operator(cx);
|
||||
change_motion(
|
||||
vim,
|
||||
@@ -83,6 +95,7 @@ pub fn init(cx: &mut AppContext) {
|
||||
});
|
||||
cx.add_action(|_: &mut Workspace, _: &DeleteToEndOfLine, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.record_current_action(cx);
|
||||
let times = vim.pop_number_operator(cx);
|
||||
delete_motion(
|
||||
vim,
|
||||
@@ -94,8 +107,26 @@ pub fn init(cx: &mut AppContext) {
|
||||
);
|
||||
})
|
||||
});
|
||||
scroll::init(cx);
|
||||
paste::init(cx);
|
||||
cx.add_action(|_: &mut Workspace, _: &JoinLines, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.record_current_action(cx);
|
||||
let mut times = vim.pop_number_operator(cx).unwrap_or(1);
|
||||
if vim.state().mode.is_visual() {
|
||||
times = 1;
|
||||
} else if times > 1 {
|
||||
// 2J joins two lines together (same as J or 1J)
|
||||
times -= 1;
|
||||
}
|
||||
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.transact(cx, |editor, cx| {
|
||||
for _ in 0..times {
|
||||
editor.join_lines(&Default::default(), cx)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn normal_motion(
|
||||
@@ -151,6 +182,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>) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.start_recording(cx);
|
||||
vim.switch_mode(Mode::Insert, false, cx);
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
@@ -162,12 +194,20 @@ fn insert_after(_: &mut Workspace, _: &InsertAfter, cx: &mut ViewContext<Workspa
|
||||
});
|
||||
}
|
||||
|
||||
fn insert_before(_: &mut Workspace, _: &InsertBefore, cx: &mut ViewContext<Workspace>) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.start_recording(cx);
|
||||
vim.switch_mode(Mode::Insert, false, cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn insert_first_non_whitespace(
|
||||
_: &mut Workspace,
|
||||
_: &InsertFirstNonWhitespace,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.start_recording(cx);
|
||||
vim.switch_mode(Mode::Insert, false, cx);
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
@@ -184,6 +224,7 @@ fn insert_first_non_whitespace(
|
||||
|
||||
fn insert_end_of_line(_: &mut Workspace, _: &InsertEndOfLine, cx: &mut ViewContext<Workspace>) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.start_recording(cx);
|
||||
vim.switch_mode(Mode::Insert, false, cx);
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
|
||||
@@ -197,6 +238,7 @@ fn insert_end_of_line(_: &mut Workspace, _: &InsertEndOfLine, cx: &mut ViewConte
|
||||
|
||||
fn insert_line_above(_: &mut Workspace, _: &InsertLineAbove, cx: &mut ViewContext<Workspace>) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.start_recording(cx);
|
||||
vim.switch_mode(Mode::Insert, false, cx);
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.transact(cx, |editor, cx| {
|
||||
@@ -229,6 +271,7 @@ fn insert_line_above(_: &mut Workspace, _: &InsertLineAbove, cx: &mut ViewContex
|
||||
|
||||
fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContext<Workspace>) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.start_recording(cx);
|
||||
vim.switch_mode(Mode::Insert, false, cx);
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.transact(cx, |editor, cx| {
|
||||
@@ -260,6 +303,7 @@ fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContex
|
||||
|
||||
pub(crate) fn normal_replace(text: Arc<str>, cx: &mut WindowContext) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.stop_recording();
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.transact(cx, |editor, cx| {
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
|
||||
@@ -7,6 +7,7 @@ use crate::{normal::ChangeCase, state::Mode, Vim};
|
||||
|
||||
pub fn change_case(_: &mut Workspace, _: &ChangeCase, cx: &mut ViewContext<Workspace>) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.record_current_action(cx);
|
||||
let count = vim.pop_number_operator(cx).unwrap_or(1) as u32;
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
let mut ranges = Vec::new();
|
||||
@@ -21,10 +22,16 @@ pub fn change_case(_: &mut Workspace, _: &ChangeCase, cx: &mut ViewContext<Works
|
||||
ranges.push(start..end);
|
||||
cursor_positions.push(start..start);
|
||||
}
|
||||
Mode::Visual | Mode::VisualBlock => {
|
||||
Mode::Visual => {
|
||||
ranges.push(selection.start..selection.end);
|
||||
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 => {
|
||||
let start = selection.start;
|
||||
let mut end = start;
|
||||
@@ -96,6 +103,11 @@ mod test {
|
||||
cx.simulate_shared_keystrokes(["shift-v", "~"]).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)
|
||||
cx.set_state("aˇßcdˇe\n", Mode::Normal);
|
||||
cx.simulate_keystroke("~");
|
||||
|
||||
@@ -4,6 +4,7 @@ use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Bias};
|
||||
use gpui::WindowContext;
|
||||
|
||||
pub fn delete_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
|
||||
vim.stop_recording();
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.transact(cx, |editor, cx| {
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
@@ -37,6 +38,7 @@ pub fn delete_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &m
|
||||
}
|
||||
|
||||
pub fn delete_object(vim: &mut Vim, object: Object, around: bool, cx: &mut WindowContext) {
|
||||
vim.stop_recording();
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.transact(cx, |editor, cx| {
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
|
||||
@@ -28,6 +28,7 @@ pub(crate) fn init(cx: &mut AppContext) {
|
||||
|
||||
fn paste(_: &mut Workspace, action: &Paste, cx: &mut ViewContext<Workspace>) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.record_current_action(cx);
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.transact(cx, |editor, cx| {
|
||||
editor.set_clip_at_line_ends(false, cx);
|
||||
|
||||
@@ -0,0 +1,427 @@
|
||||
use crate::{
|
||||
motion::Motion,
|
||||
state::{Mode, RecordedSelection, ReplayableAction},
|
||||
visual::visual_motion,
|
||||
Vim,
|
||||
};
|
||||
use gpui::{actions, Action, AppContext};
|
||||
use workspace::Workspace;
|
||||
|
||||
actions!(vim, [Repeat, EndRepeat,]);
|
||||
|
||||
fn should_replay(action: &Box<dyn Action>) -> bool {
|
||||
// skip so that we don't leave the character palette open
|
||||
if editor::ShowCharacterPalette.id() == action.id() {
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub(crate) fn init(cx: &mut AppContext) {
|
||||
cx.add_action(|_: &mut Workspace, _: &EndRepeat, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.workspace_state.replaying = false;
|
||||
vim.update_active_editor(cx, |editor, _| {
|
||||
editor.show_local_selections = true;
|
||||
});
|
||||
vim.switch_mode(Mode::Normal, false, cx)
|
||||
});
|
||||
});
|
||||
|
||||
cx.add_action(|_: &mut Workspace, _: &Repeat, cx| {
|
||||
let Some((actions, editor, selection)) = Vim::update(cx, |vim, cx| {
|
||||
let actions = vim.workspace_state.recorded_actions.clone();
|
||||
let Some(editor) = vim.active_editor.clone() else {
|
||||
return None;
|
||||
};
|
||||
let count = vim.pop_number_operator(cx);
|
||||
|
||||
vim.workspace_state.replaying = true;
|
||||
|
||||
let selection = vim.workspace_state.recorded_selection.clone();
|
||||
match selection {
|
||||
RecordedSelection::SingleLine { .. } | RecordedSelection::Visual { .. } => {
|
||||
vim.workspace_state.recorded_count = None;
|
||||
vim.switch_mode(Mode::Visual, false, cx)
|
||||
}
|
||||
RecordedSelection::VisualLine { .. } => {
|
||||
vim.workspace_state.recorded_count = None;
|
||||
vim.switch_mode(Mode::VisualLine, false, cx)
|
||||
}
|
||||
RecordedSelection::VisualBlock { .. } => {
|
||||
vim.workspace_state.recorded_count = None;
|
||||
vim.switch_mode(Mode::VisualBlock, false, cx)
|
||||
}
|
||||
RecordedSelection::None => {
|
||||
if let Some(count) = count {
|
||||
vim.workspace_state.recorded_count = Some(count);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(editor) = editor.upgrade(cx) {
|
||||
editor.update(cx, |editor, _| {
|
||||
editor.show_local_selections = false;
|
||||
})
|
||||
} 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) => {
|
||||
if should_replay(&action) {
|
||||
window
|
||||
.dispatch_action(editor.id(), action.as_ref(), &mut cx)
|
||||
.ok_or_else(|| anyhow::anyhow!("window was closed"))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::sync::Arc;
|
||||
|
||||
use editor::test::editor_lsp_test_context::EditorLspTestContext;
|
||||
use futures::StreamExt;
|
||||
use indoc::indoc;
|
||||
|
||||
use gpui::{executor::Deterministic, View};
|
||||
|
||||
use crate::{
|
||||
state::Mode,
|
||||
test::{NeovimBackedTestContext, VimTestContext},
|
||||
};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_dot_repeat(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
// "o"
|
||||
cx.set_shared_state("ˇhello").await;
|
||||
cx.simulate_shared_keystrokes(["o", "w", "o", "r", "l", "d", "escape"])
|
||||
.await;
|
||||
cx.assert_shared_state("hello\nworlˇd").await;
|
||||
cx.simulate_shared_keystrokes(["."]).await;
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_shared_state("hello\nworld\nworlˇd").await;
|
||||
|
||||
// "d"
|
||||
cx.simulate_shared_keystrokes(["^", "d", "f", "o"]).await;
|
||||
cx.simulate_shared_keystrokes(["g", "g", "."]).await;
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_shared_state("ˇ\nworld\nrld").await;
|
||||
|
||||
// "p" (note that it pastes the current clipboard)
|
||||
cx.simulate_shared_keystrokes(["j", "y", "y", "p"]).await;
|
||||
cx.simulate_shared_keystrokes(["shift-g", "y", "y", "."])
|
||||
.await;
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_shared_state("\nworld\nworld\nrld\nˇrld").await;
|
||||
|
||||
// "~" (note that counts apply to the action taken, not . itself)
|
||||
cx.set_shared_state("ˇthe quick brown fox").await;
|
||||
cx.simulate_shared_keystrokes(["2", "~", "."]).await;
|
||||
deterministic.run_until_parked();
|
||||
cx.set_shared_state("THE ˇquick brown fox").await;
|
||||
cx.simulate_shared_keystrokes(["3", "."]).await;
|
||||
deterministic.run_until_parked();
|
||||
cx.set_shared_state("THE QUIˇck brown fox").await;
|
||||
deterministic.run_until_parked();
|
||||
cx.simulate_shared_keystrokes(["."]).await;
|
||||
deterministic.run_until_parked();
|
||||
cx.set_shared_state("THE QUICK ˇbrown fox").await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_repeat_ime(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
cx.set_state("hˇllo", Mode::Normal);
|
||||
cx.simulate_keystrokes(["i"]);
|
||||
|
||||
// simulate brazilian input for ä.
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor.replace_and_mark_text_in_range(None, "\"", Some(1..1), cx);
|
||||
editor.replace_text_in_range(None, "ä", cx);
|
||||
});
|
||||
cx.simulate_keystrokes(["escape"]);
|
||||
cx.assert_state("hˇällo", Mode::Normal);
|
||||
cx.simulate_keystrokes(["."]);
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_state("hˇäällo", Mode::Normal);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_repeat_completion(
|
||||
deterministic: Arc<Deterministic>,
|
||||
cx: &mut gpui::TestAppContext,
|
||||
) {
|
||||
let cx = EditorLspTestContext::new_rust(
|
||||
lsp::ServerCapabilities {
|
||||
completion_provider: Some(lsp::CompletionOptions {
|
||||
trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
|
||||
resolve_provider: Some(true),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
let mut cx = VimTestContext::new_with_lsp(cx, true);
|
||||
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
onˇe
|
||||
two
|
||||
three
|
||||
"},
|
||||
Mode::Normal,
|
||||
);
|
||||
|
||||
let mut request =
|
||||
cx.handle_request::<lsp::request::Completion, _, _>(move |_, params, _| async move {
|
||||
let position = params.text_document_position.position;
|
||||
Ok(Some(lsp::CompletionResponse::Array(vec![
|
||||
lsp::CompletionItem {
|
||||
label: "first".to_string(),
|
||||
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
|
||||
range: lsp::Range::new(position.clone(), position.clone()),
|
||||
new_text: "first".to_string(),
|
||||
})),
|
||||
..Default::default()
|
||||
},
|
||||
lsp::CompletionItem {
|
||||
label: "second".to_string(),
|
||||
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
|
||||
range: lsp::Range::new(position.clone(), position.clone()),
|
||||
new_text: "second".to_string(),
|
||||
})),
|
||||
..Default::default()
|
||||
},
|
||||
])))
|
||||
});
|
||||
cx.simulate_keystrokes(["a", "."]);
|
||||
request.next().await;
|
||||
cx.condition(|editor, _| editor.context_menu_visible())
|
||||
.await;
|
||||
cx.simulate_keystrokes(["down", "enter", "!", "escape"]);
|
||||
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
one.secondˇ!
|
||||
two
|
||||
three
|
||||
"},
|
||||
Mode::Normal,
|
||||
);
|
||||
cx.simulate_keystrokes(["j", "."]);
|
||||
deterministic.run_until_parked();
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
one.second!
|
||||
two.secondˇ!
|
||||
three
|
||||
"},
|
||||
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) {
|
||||
cx.add_action(|_: &mut Workspace, _: &Substitute, cx| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.start_recording(cx);
|
||||
let count = vim.pop_number_operator(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| {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.start_recording(cx);
|
||||
if matches!(vim.state().mode, Mode::VisualBlock | Mode::Visual) {
|
||||
vim.switch_mode(Mode::VisualLine, false, cx)
|
||||
}
|
||||
|
||||
+54
-1
@@ -1,4 +1,6 @@
|
||||
use gpui::keymap_matcher::KeymapContext;
|
||||
use std::{ops::Range, sync::Arc};
|
||||
|
||||
use gpui::{keymap_matcher::KeymapContext, Action};
|
||||
use language::CursorShape;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use workspace::searchable::Direction;
|
||||
@@ -48,10 +50,61 @@ pub struct EditorState {
|
||||
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)]
|
||||
pub struct WorkspaceState {
|
||||
pub search: SearchState,
|
||||
pub last_find: Option<Motion>,
|
||||
|
||||
pub recording: bool,
|
||||
pub stop_recording_after_next_action: bool,
|
||||
pub replaying: bool,
|
||||
pub recorded_count: Option<usize>,
|
||||
pub recorded_actions: Vec<ReplayableAction>,
|
||||
pub recorded_selection: RecordedSelection,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ReplayableAction {
|
||||
Action(Box<dyn Action>),
|
||||
Insertion {
|
||||
text: Arc<str>,
|
||||
utf16_range_to_replace: Option<Range<isize>>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Clone for ReplayableAction {
|
||||
fn clone(&self) -> Self {
|
||||
match self {
|
||||
Self::Action(action) => Self::Action(action.boxed_clone()),
|
||||
Self::Insertion {
|
||||
text,
|
||||
utf16_range_to_replace,
|
||||
} => Self::Insertion {
|
||||
text: text.clone(),
|
||||
utf16_range_to_replace: utf16_range_to_replace.clone(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
|
||||
@@ -286,6 +286,55 @@ async fn test_word_characters(cx: &mut gpui::TestAppContext) {
|
||||
)
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_join_lines(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state(indoc! {"
|
||||
ˇone
|
||||
two
|
||||
three
|
||||
four
|
||||
five
|
||||
six
|
||||
"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["shift-j"]).await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
oneˇ two
|
||||
three
|
||||
four
|
||||
five
|
||||
six
|
||||
"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["3", "shift-j"]).await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
one two threeˇ four
|
||||
five
|
||||
six
|
||||
"})
|
||||
.await;
|
||||
|
||||
cx.set_shared_state(indoc! {"
|
||||
ˇone
|
||||
two
|
||||
three
|
||||
four
|
||||
five
|
||||
six
|
||||
"})
|
||||
.await;
|
||||
cx.simulate_shared_keystrokes(["j", "v", "3", "j", "shift-j"])
|
||||
.await;
|
||||
cx.assert_shared_state(indoc! {"
|
||||
one
|
||||
two three fourˇ five
|
||||
six
|
||||
"})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_wrapped_lines(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
@@ -3,7 +3,9 @@ use std::ops::{Deref, DerefMut};
|
||||
use editor::test::{
|
||||
editor_lsp_test_context::EditorLspTestContext, editor_test_context::EditorTestContext,
|
||||
};
|
||||
use futures::Future;
|
||||
use gpui::ContextHandle;
|
||||
use lsp::request;
|
||||
use search::{BufferSearchBar, ProjectSearchBar};
|
||||
|
||||
use crate::{state::Operator, *};
|
||||
@@ -124,6 +126,19 @@ impl<'a> VimTestContext<'a> {
|
||||
assert_eq!(self.mode(), mode_after, "{}", self.assertion_context());
|
||||
assert_eq!(self.active_operator(), None, "{}", self.assertion_context());
|
||||
}
|
||||
|
||||
pub fn handle_request<T, F, Fut>(
|
||||
&self,
|
||||
handler: F,
|
||||
) -> futures::channel::mpsc::UnboundedReceiver<()>
|
||||
where
|
||||
T: 'static + request::Request,
|
||||
T::Params: 'static + Send,
|
||||
F: 'static + Send + FnMut(lsp::Url, T::Params, gpui::AsyncAppContext) -> Fut,
|
||||
Fut: 'static + Send + Future<Output = Result<T::Result>>,
|
||||
{
|
||||
self.cx.handle_request::<T, F, Fut>(handler)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Deref for VimTestContext<'a> {
|
||||
|
||||
+121
-3
@@ -18,17 +18,19 @@ use gpui::{
|
||||
actions, impl_actions, keymap_matcher::KeymapContext, keymap_matcher::MatchResult, AppContext,
|
||||
Subscription, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
|
||||
};
|
||||
use language::{CursorShape, Selection, SelectionGoal};
|
||||
use language::{CursorShape, Point, Selection, SelectionGoal};
|
||||
pub use mode_indicator::ModeIndicator;
|
||||
use motion::Motion;
|
||||
use normal::normal_replace;
|
||||
use serde::Deserialize;
|
||||
use settings::{Setting, SettingsStore};
|
||||
use state::{EditorState, Mode, Operator, WorkspaceState};
|
||||
use std::sync::Arc;
|
||||
use state::{EditorState, Mode, Operator, RecordedSelection, WorkspaceState};
|
||||
use std::{ops::Range, sync::Arc};
|
||||
use visual::{visual_block_motion, visual_replace};
|
||||
use workspace::{self, Workspace};
|
||||
|
||||
use crate::state::ReplayableAction;
|
||||
|
||||
struct VimModeSetting(bool);
|
||||
|
||||
#[derive(Clone, Deserialize, PartialEq)]
|
||||
@@ -102,6 +104,19 @@ pub fn observe_keystrokes(cx: &mut WindowContext) {
|
||||
return true;
|
||||
}
|
||||
if let Some(handled_by) = handled_by {
|
||||
Vim::update(cx, |vim, _| {
|
||||
if vim.workspace_state.recording {
|
||||
vim.workspace_state
|
||||
.recorded_actions
|
||||
.push(ReplayableAction::Action(handled_by.boxed_clone()));
|
||||
|
||||
if vim.workspace_state.stop_recording_after_next_action {
|
||||
vim.workspace_state.recording = false;
|
||||
vim.workspace_state.stop_recording_after_next_action = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Keystroke is handled by the vim system, so continue forward
|
||||
if handled_by.namespace() == "vim" {
|
||||
return true;
|
||||
@@ -156,7 +171,12 @@ impl Vim {
|
||||
}
|
||||
Event::InputIgnored { text } => {
|
||||
Vim::active_editor_input_ignored(text.clone(), cx);
|
||||
Vim::record_insertion(text, None, cx)
|
||||
}
|
||||
Event::InputHandled {
|
||||
text,
|
||||
utf16_range_to_replace: range_to_replace,
|
||||
} => Vim::record_insertion(text, range_to_replace.clone(), cx),
|
||||
_ => {}
|
||||
}));
|
||||
|
||||
@@ -176,6 +196,27 @@ impl Vim {
|
||||
self.sync_vim_settings(cx);
|
||||
}
|
||||
|
||||
fn record_insertion(
|
||||
text: &Arc<str>,
|
||||
range_to_replace: Option<Range<isize>>,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
Vim::update(cx, |vim, _| {
|
||||
if vim.workspace_state.recording {
|
||||
vim.workspace_state
|
||||
.recorded_actions
|
||||
.push(ReplayableAction::Insertion {
|
||||
text: text.clone(),
|
||||
utf16_range_to_replace: range_to_replace,
|
||||
});
|
||||
if vim.workspace_state.stop_recording_after_next_action {
|
||||
vim.workspace_state.recording = false;
|
||||
vim.workspace_state.stop_recording_after_next_action = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn update_active_editor<S>(
|
||||
&self,
|
||||
cx: &mut WindowContext,
|
||||
@@ -184,6 +225,71 @@ impl Vim {
|
||||
let editor = self.active_editor.clone()?.upgrade(cx)?;
|
||||
Some(editor.update(cx, update))
|
||||
}
|
||||
// ~, shift-j, x, shift-x, p
|
||||
// shift-c, shift-d, shift-i, i, a, o, shift-o, s
|
||||
// c, d
|
||||
// r
|
||||
|
||||
// TODO: shift-j?
|
||||
//
|
||||
pub fn start_recording(&mut self, cx: &mut WindowContext) {
|
||||
if !self.workspace_state.replaying {
|
||||
self.workspace_state.recording = true;
|
||||
self.workspace_state.recorded_actions = Default::default();
|
||||
self.workspace_state.recorded_count =
|
||||
if let Some(Operator::Number(number)) = self.active_operator() {
|
||||
Some(number)
|
||||
} else {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stop_recording(&mut self) {
|
||||
if self.workspace_state.recording {
|
||||
self.workspace_state.stop_recording_after_next_action = true;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn record_current_action(&mut self, cx: &mut WindowContext) {
|
||||
self.start_recording(cx);
|
||||
self.stop_recording();
|
||||
}
|
||||
|
||||
fn switch_mode(&mut self, mode: Mode, leave_selections: bool, cx: &mut WindowContext) {
|
||||
let state = self.state();
|
||||
@@ -247,6 +353,12 @@ impl Vim {
|
||||
}
|
||||
|
||||
fn push_operator(&mut self, operator: Operator, cx: &mut WindowContext) {
|
||||
if matches!(
|
||||
operator,
|
||||
Operator::Change | Operator::Delete | Operator::Replace
|
||||
) {
|
||||
self.start_recording(cx)
|
||||
};
|
||||
self.update_state(|state| state.operator_stack.push(operator));
|
||||
self.sync_vim_settings(cx);
|
||||
}
|
||||
@@ -272,6 +384,12 @@ impl Vim {
|
||||
}
|
||||
|
||||
fn pop_number_operator(&mut self, cx: &mut WindowContext) -> Option<usize> {
|
||||
if self.workspace_state.replaying {
|
||||
if let Some(number) = self.workspace_state.recorded_count {
|
||||
return Some(number);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(Operator::Number(number)) = self.active_operator() {
|
||||
self.pop_operator(cx);
|
||||
return Some(number);
|
||||
|
||||
@@ -277,6 +277,7 @@ pub fn other_end(_: &mut Workspace, _: &OtherEnd, cx: &mut ViewContext<Workspace
|
||||
|
||||
pub fn delete(_: &mut Workspace, _: &VisualDelete, cx: &mut ViewContext<Workspace>) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.record_current_action(cx);
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
let mut original_columns: HashMap<_, _> = Default::default();
|
||||
let line_mode = editor.selections.line_mode;
|
||||
@@ -339,6 +340,7 @@ pub fn yank(_: &mut Workspace, _: &VisualYank, cx: &mut ViewContext<Workspace>)
|
||||
|
||||
pub(crate) fn visual_replace(text: Arc<str>, cx: &mut WindowContext) {
|
||||
Vim::update(cx, |vim, cx| {
|
||||
vim.stop_recording();
|
||||
vim.update_active_editor(cx, |editor, cx| {
|
||||
editor.transact(cx, |editor, cx| {
|
||||
let (display_map, selections) = editor.selections.all_adjusted_display(cx);
|
||||
|
||||
Reference in New Issue
Block a user