vim2 compiling (but mostly commented out)

This commit is contained in:
Conrad Irwin 2023-12-08 18:47:14 +00:00
parent 7a9f764aa0
commit 32837d67be
146 changed files with 22013 additions and 10 deletions

View file

@ -0,0 +1,116 @@
use editor::scroll::autoscroll::Autoscroll;
use gpui::ViewContext;
use language::{Bias, Point};
use workspace::Workspace;
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.take_count(cx).unwrap_or(1) as u32;
vim.update_active_editor(cx, |editor, cx| {
let mut ranges = Vec::new();
let mut cursor_positions = Vec::new();
let snapshot = editor.buffer().read(cx).snapshot(cx);
for selection in editor.selections.all::<Point>(cx) {
match vim.state().mode {
Mode::VisualLine => {
let start = Point::new(selection.start.row, 0);
let end =
Point::new(selection.end.row, snapshot.line_len(selection.end.row));
ranges.push(start..end);
cursor_positions.push(start..start);
}
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;
for _ in 0..count {
end = snapshot.clip_point(end + Point::new(0, 1), Bias::Right);
}
ranges.push(start..end);
if end.column == snapshot.line_len(end.row) {
end = snapshot.clip_point(end - Point::new(0, 1), Bias::Left);
}
cursor_positions.push(end..end)
}
}
}
editor.transact(cx, |editor, cx| {
for range in ranges.into_iter().rev() {
let snapshot = editor.buffer().read(cx).snapshot(cx);
editor.buffer().update(cx, |buffer, cx| {
let text = snapshot
.text_for_range(range.start..range.end)
.flat_map(|s| s.chars())
.flat_map(|c| {
if c.is_lowercase() {
c.to_uppercase().collect::<Vec<char>>()
} else {
c.to_lowercase().collect::<Vec<char>>()
}
})
.collect::<String>();
buffer.edit([(range, text)], None, cx)
})
}
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select_ranges(cursor_positions)
})
});
});
vim.switch_mode(Mode::Normal, true, cx)
})
}
// #[cfg(test)]
// mod test {
// use crate::{state::Mode, test::NeovimBackedTestContext};
// #[gpui::test]
// async fn test_change_case(cx: &mut gpui::TestAppContext) {
// let mut cx = NeovimBackedTestContext::new(cx).await;
// cx.set_shared_state("ˇabC\n").await;
// cx.simulate_shared_keystrokes(["~"]).await;
// cx.assert_shared_state("AˇbC\n").await;
// cx.simulate_shared_keystrokes(["2", "~"]).await;
// cx.assert_shared_state("ABˇc\n").await;
// // works in visual mode
// cx.set_shared_state("a😀C«dÉ1*fˇ»\n").await;
// cx.simulate_shared_keystrokes(["~"]).await;
// cx.assert_shared_state("a😀CˇDé1*F\n").await;
// // works with multibyte characters
// cx.simulate_shared_keystrokes(["~"]).await;
// cx.set_shared_state("aˇC😀é1*F\n").await;
// cx.simulate_shared_keystrokes(["4", "~"]).await;
// cx.assert_shared_state("ac😀É1ˇ*F\n").await;
// // works with line selections
// cx.set_shared_state("abˇC\n").await;
// 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("~");
// cx.assert_state("aSSˇcdˇE\n", Mode::Normal);
// }
// }

View file

@ -0,0 +1,502 @@
use crate::{motion::Motion, object::Object, state::Mode, utils::copy_selections_content, Vim};
use editor::{
char_kind,
display_map::DisplaySnapshot,
movement::{self, FindRange, TextLayoutDetails},
scroll::autoscroll::Autoscroll,
CharKind, DisplayPoint,
};
use gpui::WindowContext;
use language::Selection;
pub fn change_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
// Some motions ignore failure when switching to normal mode
let mut motion_succeeded = matches!(
motion,
Motion::Left
| Motion::Right
| Motion::EndOfLine { .. }
| Motion::Backspace
| Motion::StartOfLine { .. }
);
vim.update_active_editor(cx, |editor, cx| {
let text_layout_details = editor.text_layout_details(cx);
editor.transact(cx, |editor, cx| {
// We are swapping to insert mode anyway. Just set the line end clipping behavior now
editor.set_clip_at_line_ends(false, cx);
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_with(|map, selection| {
motion_succeeded |= if let Motion::NextWordStart { ignore_punctuation } = motion
{
expand_changed_word_selection(
map,
selection,
times,
ignore_punctuation,
&text_layout_details,
)
} else {
motion.expand_selection(map, selection, times, false, &text_layout_details)
};
});
});
copy_selections_content(editor, motion.linewise(), cx);
editor.insert("", cx);
});
});
if motion_succeeded {
vim.switch_mode(Mode::Insert, false, cx)
} else {
vim.switch_mode(Mode::Normal, false, cx)
}
}
pub fn change_object(vim: &mut Vim, object: Object, around: bool, cx: &mut WindowContext) {
let mut objects_found = false;
vim.update_active_editor(cx, |editor, cx| {
// We are swapping to insert mode anyway. Just set the line end clipping behavior now
editor.set_clip_at_line_ends(false, cx);
editor.transact(cx, |editor, cx| {
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_with(|map, selection| {
objects_found |= object.expand_selection(map, selection, around);
});
});
if objects_found {
copy_selections_content(editor, false, cx);
editor.insert("", cx);
}
});
});
if objects_found {
vim.switch_mode(Mode::Insert, false, cx);
} else {
vim.switch_mode(Mode::Normal, false, cx);
}
}
// From the docs https://vimdoc.sourceforge.net/htmldoc/motion.html
// Special case: "cw" and "cW" are treated like "ce" and "cE" if the cursor is
// on a non-blank. This is because "cw" is interpreted as change-word, and a
// word does not include the following white space. {Vi: "cw" when on a blank
// followed by other blanks changes only the first blank; this is probably a
// bug, because "dw" deletes all the blanks}
fn expand_changed_word_selection(
map: &DisplaySnapshot,
selection: &mut Selection<DisplayPoint>,
times: Option<usize>,
ignore_punctuation: bool,
text_layout_details: &TextLayoutDetails,
) -> bool {
if times.is_none() || times.unwrap() == 1 {
let scope = map
.buffer_snapshot
.language_scope_at(selection.start.to_point(map));
let in_word = map
.chars_at(selection.head())
.next()
.map(|(c, _)| char_kind(&scope, c) != CharKind::Whitespace)
.unwrap_or_default();
if in_word {
selection.end =
movement::find_boundary(map, selection.end, FindRange::MultiLine, |left, right| {
let left_kind = char_kind(&scope, left).coerce_punctuation(ignore_punctuation);
let right_kind =
char_kind(&scope, right).coerce_punctuation(ignore_punctuation);
left_kind != right_kind && left_kind != CharKind::Whitespace
});
true
} else {
Motion::NextWordStart { ignore_punctuation }.expand_selection(
map,
selection,
None,
false,
&text_layout_details,
)
}
} else {
Motion::NextWordStart { ignore_punctuation }.expand_selection(
map,
selection,
times,
false,
&text_layout_details,
)
}
}
// #[cfg(test)]
// mod test {
// use indoc::indoc;
// use crate::test::NeovimBackedTestContext;
// #[gpui::test]
// async fn test_change_h(cx: &mut gpui::TestAppContext) {
// let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "h"]);
// cx.assert("Teˇst").await;
// cx.assert("Tˇest").await;
// cx.assert("ˇTest").await;
// cx.assert(indoc! {"
// Test
// ˇtest"})
// .await;
// }
// #[gpui::test]
// async fn test_change_backspace(cx: &mut gpui::TestAppContext) {
// let mut cx = NeovimBackedTestContext::new(cx)
// .await
// .binding(["c", "backspace"]);
// cx.assert("Teˇst").await;
// cx.assert("Tˇest").await;
// cx.assert("ˇTest").await;
// cx.assert(indoc! {"
// Test
// ˇtest"})
// .await;
// }
// #[gpui::test]
// async fn test_change_l(cx: &mut gpui::TestAppContext) {
// let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "l"]);
// cx.assert("Teˇst").await;
// cx.assert("Tesˇt").await;
// }
// #[gpui::test]
// async fn test_change_w(cx: &mut gpui::TestAppContext) {
// let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "w"]);
// cx.assert("Teˇst").await;
// cx.assert("Tˇest test").await;
// cx.assert("Testˇ test").await;
// cx.assert(indoc! {"
// Test teˇst
// test"})
// .await;
// cx.assert(indoc! {"
// Test tesˇt
// test"})
// .await;
// cx.assert(indoc! {"
// Test test
// ˇ
// test"})
// .await;
// let mut cx = cx.binding(["c", "shift-w"]);
// cx.assert("Test teˇst-test test").await;
// }
// #[gpui::test]
// async fn test_change_e(cx: &mut gpui::TestAppContext) {
// let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "e"]);
// cx.assert("Teˇst Test").await;
// cx.assert("Tˇest test").await;
// cx.assert(indoc! {"
// Test teˇst
// test"})
// .await;
// cx.assert(indoc! {"
// Test tesˇt
// test"})
// .await;
// cx.assert(indoc! {"
// Test test
// ˇ
// test"})
// .await;
// let mut cx = cx.binding(["c", "shift-e"]);
// cx.assert("Test teˇst-test test").await;
// }
// #[gpui::test]
// async fn test_change_b(cx: &mut gpui::TestAppContext) {
// let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "b"]);
// cx.assert("Teˇst Test").await;
// cx.assert("Test ˇtest").await;
// cx.assert("Test1 test2 ˇtest3").await;
// cx.assert(indoc! {"
// Test test
// ˇtest"})
// .await;
// cx.assert(indoc! {"
// Test test
// ˇ
// test"})
// .await;
// let mut cx = cx.binding(["c", "shift-b"]);
// cx.assert("Test test-test ˇtest").await;
// }
// #[gpui::test]
// async fn test_change_end_of_line(cx: &mut gpui::TestAppContext) {
// let mut cx = NeovimBackedTestContext::new(cx).await.binding(["c", "$"]);
// cx.assert(indoc! {"
// The qˇuick
// brown fox"})
// .await;
// cx.assert(indoc! {"
// The quick
// ˇ
// brown fox"})
// .await;
// }
// #[gpui::test]
// async fn test_change_0(cx: &mut gpui::TestAppContext) {
// let mut cx = NeovimBackedTestContext::new(cx).await;
// cx.assert_neovim_compatible(
// indoc! {"
// The qˇuick
// brown fox"},
// ["c", "0"],
// )
// .await;
// cx.assert_neovim_compatible(
// indoc! {"
// The quick
// ˇ
// brown fox"},
// ["c", "0"],
// )
// .await;
// }
// #[gpui::test]
// async fn test_change_k(cx: &mut gpui::TestAppContext) {
// let mut cx = NeovimBackedTestContext::new(cx).await;
// cx.assert_neovim_compatible(
// indoc! {"
// The quick
// brown ˇfox
// jumps over"},
// ["c", "k"],
// )
// .await;
// cx.assert_neovim_compatible(
// indoc! {"
// The quick
// brown fox
// jumps ˇover"},
// ["c", "k"],
// )
// .await;
// cx.assert_neovim_compatible(
// indoc! {"
// The qˇuick
// brown fox
// jumps over"},
// ["c", "k"],
// )
// .await;
// cx.assert_neovim_compatible(
// indoc! {"
// ˇ
// brown fox
// jumps over"},
// ["c", "k"],
// )
// .await;
// }
// #[gpui::test]
// async fn test_change_j(cx: &mut gpui::TestAppContext) {
// let mut cx = NeovimBackedTestContext::new(cx).await;
// cx.assert_neovim_compatible(
// indoc! {"
// The quick
// brown ˇfox
// jumps over"},
// ["c", "j"],
// )
// .await;
// cx.assert_neovim_compatible(
// indoc! {"
// The quick
// brown fox
// jumps ˇover"},
// ["c", "j"],
// )
// .await;
// cx.assert_neovim_compatible(
// indoc! {"
// The qˇuick
// brown fox
// jumps over"},
// ["c", "j"],
// )
// .await;
// cx.assert_neovim_compatible(
// indoc! {"
// The quick
// brown fox
// ˇ"},
// ["c", "j"],
// )
// .await;
// }
// #[gpui::test]
// async fn test_change_end_of_document(cx: &mut gpui::TestAppContext) {
// let mut cx = NeovimBackedTestContext::new(cx).await;
// cx.assert_neovim_compatible(
// indoc! {"
// The quick
// brownˇ fox
// jumps over
// the lazy"},
// ["c", "shift-g"],
// )
// .await;
// cx.assert_neovim_compatible(
// indoc! {"
// The quick
// brownˇ fox
// jumps over
// the lazy"},
// ["c", "shift-g"],
// )
// .await;
// cx.assert_neovim_compatible(
// indoc! {"
// The quick
// brown fox
// jumps over
// the lˇazy"},
// ["c", "shift-g"],
// )
// .await;
// cx.assert_neovim_compatible(
// indoc! {"
// The quick
// brown fox
// jumps over
// ˇ"},
// ["c", "shift-g"],
// )
// .await;
// }
// #[gpui::test]
// async fn test_change_gg(cx: &mut gpui::TestAppContext) {
// let mut cx = NeovimBackedTestContext::new(cx).await;
// cx.assert_neovim_compatible(
// indoc! {"
// The quick
// brownˇ fox
// jumps over
// the lazy"},
// ["c", "g", "g"],
// )
// .await;
// cx.assert_neovim_compatible(
// indoc! {"
// The quick
// brown fox
// jumps over
// the lˇazy"},
// ["c", "g", "g"],
// )
// .await;
// cx.assert_neovim_compatible(
// indoc! {"
// The qˇuick
// brown fox
// jumps over
// the lazy"},
// ["c", "g", "g"],
// )
// .await;
// cx.assert_neovim_compatible(
// indoc! {"
// ˇ
// brown fox
// jumps over
// the lazy"},
// ["c", "g", "g"],
// )
// .await;
// }
// #[gpui::test]
// async fn test_repeated_cj(cx: &mut gpui::TestAppContext) {
// let mut cx = NeovimBackedTestContext::new(cx).await;
// for count in 1..=5 {
// cx.assert_binding_matches_all(
// ["c", &count.to_string(), "j"],
// indoc! {"
// ˇThe quˇickˇ browˇn
// ˇ
// ˇfox ˇjumpsˇ-ˇoˇver
// ˇthe lazy dog
// "},
// )
// .await;
// }
// }
// #[gpui::test]
// async fn test_repeated_cl(cx: &mut gpui::TestAppContext) {
// let mut cx = NeovimBackedTestContext::new(cx).await;
// for count in 1..=5 {
// cx.assert_binding_matches_all(
// ["c", &count.to_string(), "l"],
// indoc! {"
// ˇThe quˇickˇ browˇn
// ˇ
// ˇfox ˇjumpsˇ-ˇoˇver
// ˇthe lazy dog
// "},
// )
// .await;
// }
// }
// #[gpui::test]
// async fn test_repeated_cb(cx: &mut gpui::TestAppContext) {
// let mut cx = NeovimBackedTestContext::new(cx).await;
// for count in 1..=5 {
// for marked_text in cx.each_marked_position(indoc! {"
// ˇThe quˇickˇ browˇn
// ˇ
// ˇfox ˇjumpsˇ-ˇoˇver
// ˇthe lazy dog
// "})
// {
// cx.assert_neovim_compatible(&marked_text, ["c", &count.to_string(), "b"])
// .await;
// }
// }
// }
// #[gpui::test]
// async fn test_repeated_ce(cx: &mut gpui::TestAppContext) {
// let mut cx = NeovimBackedTestContext::new(cx).await;
// for count in 1..=5 {
// cx.assert_binding_matches_all(
// ["c", &count.to_string(), "e"],
// indoc! {"
// ˇThe quˇickˇ browˇn
// ˇ
// ˇfox ˇjumpsˇ-ˇoˇver
// ˇthe lazy dog
// "},
// )
// .await;
// }
// }
// }

View file

@ -0,0 +1,475 @@
use crate::{motion::Motion, object::Object, utils::copy_selections_content, Vim};
use collections::{HashMap, HashSet};
use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Bias};
use gpui::WindowContext;
use language::Point;
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| {
let text_layout_details = editor.text_layout_details(cx);
editor.transact(cx, |editor, cx| {
editor.set_clip_at_line_ends(false, cx);
let mut original_columns: HashMap<_, _> = Default::default();
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_with(|map, selection| {
let original_head = selection.head();
original_columns.insert(selection.id, original_head.column());
motion.expand_selection(map, selection, times, true, &text_layout_details);
// Motion::NextWordStart on an empty line should delete it.
if let Motion::NextWordStart {
ignore_punctuation: _,
} = motion
{
if selection.is_empty()
&& map
.buffer_snapshot
.line_len(selection.start.to_point(&map).row)
== 0
{
selection.end = map
.buffer_snapshot
.clip_point(
Point::new(selection.start.to_point(&map).row + 1, 0),
Bias::Left,
)
.to_display_point(map)
}
}
});
});
copy_selections_content(editor, motion.linewise(), cx);
editor.insert("", cx);
// Fixup cursor position after the deletion
editor.set_clip_at_line_ends(true, cx);
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_with(|map, selection| {
let mut cursor = selection.head();
if motion.linewise() {
if let Some(column) = original_columns.get(&selection.id) {
*cursor.column_mut() = *column
}
}
cursor = map.clip_point(cursor, Bias::Left);
selection.collapse_to(cursor, selection.goal)
});
});
});
});
}
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);
// Emulates behavior in vim where if we expanded backwards to include a newline
// the cursor gets set back to the start of the line
let mut should_move_to_start: HashSet<_> = Default::default();
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_with(|map, selection| {
object.expand_selection(map, selection, around);
let offset_range = selection.map(|p| p.to_offset(map, Bias::Left)).range();
let contains_only_newlines = map
.chars_at(selection.start)
.take_while(|(_, p)| p < &selection.end)
.all(|(char, _)| char == '\n')
&& !offset_range.is_empty();
let end_at_newline = map
.chars_at(selection.end)
.next()
.map(|(c, _)| c == '\n')
.unwrap_or(false);
// If expanded range contains only newlines and
// the object is around or sentence, expand to include a newline
// at the end or start
if (around || object == Object::Sentence) && contains_only_newlines {
if end_at_newline {
selection.end =
(offset_range.end + '\n'.len_utf8()).to_display_point(map);
} else if selection.start.row() > 0 {
should_move_to_start.insert(selection.id);
selection.start =
(offset_range.start - '\n'.len_utf8()).to_display_point(map);
}
}
});
});
copy_selections_content(editor, false, cx);
editor.insert("", cx);
// Fixup cursor position after the deletion
editor.set_clip_at_line_ends(true, cx);
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.move_with(|map, selection| {
let mut cursor = selection.head();
if should_move_to_start.contains(&selection.id) {
*cursor.column_mut() = 0;
}
cursor = map.clip_point(cursor, Bias::Left);
selection.collapse_to(cursor, selection.goal)
});
});
});
});
}
// #[cfg(test)]
// mod test {
// use indoc::indoc;
// use crate::{
// state::Mode,
// test::{ExemptionFeatures, NeovimBackedTestContext, VimTestContext},
// };
// #[gpui::test]
// async fn test_delete_h(cx: &mut gpui::TestAppContext) {
// let mut cx = NeovimBackedTestContext::new(cx).await.binding(["d", "h"]);
// cx.assert("Teˇst").await;
// cx.assert("Tˇest").await;
// cx.assert("ˇTest").await;
// cx.assert(indoc! {"
// Test
// ˇtest"})
// .await;
// }
// #[gpui::test]
// async fn test_delete_l(cx: &mut gpui::TestAppContext) {
// let mut cx = NeovimBackedTestContext::new(cx).await.binding(["d", "l"]);
// cx.assert("ˇTest").await;
// cx.assert("Teˇst").await;
// cx.assert("Tesˇt").await;
// cx.assert(indoc! {"
// Tesˇt
// test"})
// .await;
// }
// #[gpui::test]
// async fn test_delete_w(cx: &mut gpui::TestAppContext) {
// let mut cx = NeovimBackedTestContext::new(cx).await;
// cx.assert_neovim_compatible(
// indoc! {"
// Test tesˇt
// test"},
// ["d", "w"],
// )
// .await;
// cx.assert_neovim_compatible("Teˇst", ["d", "w"]).await;
// cx.assert_neovim_compatible("Tˇest test", ["d", "w"]).await;
// cx.assert_neovim_compatible(
// indoc! {"
// Test teˇst
// test"},
// ["d", "w"],
// )
// .await;
// cx.assert_neovim_compatible(
// indoc! {"
// Test tesˇt
// test"},
// ["d", "w"],
// )
// .await;
// cx.assert_neovim_compatible(
// indoc! {"
// Test test
// ˇ
// test"},
// ["d", "w"],
// )
// .await;
// let mut cx = cx.binding(["d", "shift-w"]);
// cx.assert_neovim_compatible("Test teˇst-test test", ["d", "shift-w"])
// .await;
// }
// #[gpui::test]
// async fn test_delete_next_word_end(cx: &mut gpui::TestAppContext) {
// let mut cx = NeovimBackedTestContext::new(cx).await.binding(["d", "e"]);
// // cx.assert("Teˇst Test").await;
// // cx.assert("Tˇest test").await;
// cx.assert(indoc! {"
// Test teˇst
// test"})
// .await;
// cx.assert(indoc! {"
// Test tesˇt
// test"})
// .await;
// cx.assert_exempted(
// indoc! {"
// Test test
// ˇ
// test"},
// ExemptionFeatures::OperatorLastNewlineRemains,
// )
// .await;
// let mut cx = cx.binding(["d", "shift-e"]);
// cx.assert("Test teˇst-test test").await;
// }
// #[gpui::test]
// async fn test_delete_b(cx: &mut gpui::TestAppContext) {
// let mut cx = NeovimBackedTestContext::new(cx).await.binding(["d", "b"]);
// cx.assert("Teˇst Test").await;
// cx.assert("Test ˇtest").await;
// cx.assert("Test1 test2 ˇtest3").await;
// cx.assert(indoc! {"
// Test test
// ˇtest"})
// .await;
// cx.assert(indoc! {"
// Test test
// ˇ
// test"})
// .await;
// let mut cx = cx.binding(["d", "shift-b"]);
// cx.assert("Test test-test ˇtest").await;
// }
// #[gpui::test]
// async fn test_delete_end_of_line(cx: &mut gpui::TestAppContext) {
// let mut cx = NeovimBackedTestContext::new(cx).await.binding(["d", "$"]);
// cx.assert(indoc! {"
// The qˇuick
// brown fox"})
// .await;
// cx.assert(indoc! {"
// The quick
// ˇ
// brown fox"})
// .await;
// }
// #[gpui::test]
// async fn test_delete_0(cx: &mut gpui::TestAppContext) {
// let mut cx = NeovimBackedTestContext::new(cx).await.binding(["d", "0"]);
// cx.assert(indoc! {"
// The qˇuick
// brown fox"})
// .await;
// cx.assert(indoc! {"
// The quick
// ˇ
// brown fox"})
// .await;
// }
// #[gpui::test]
// async fn test_delete_k(cx: &mut gpui::TestAppContext) {
// let mut cx = NeovimBackedTestContext::new(cx).await.binding(["d", "k"]);
// cx.assert(indoc! {"
// The quick
// brown ˇfox
// jumps over"})
// .await;
// cx.assert(indoc! {"
// The quick
// brown fox
// jumps ˇover"})
// .await;
// cx.assert(indoc! {"
// The qˇuick
// brown fox
// jumps over"})
// .await;
// cx.assert(indoc! {"
// ˇbrown fox
// jumps over"})
// .await;
// }
// #[gpui::test]
// async fn test_delete_j(cx: &mut gpui::TestAppContext) {
// let mut cx = NeovimBackedTestContext::new(cx).await.binding(["d", "j"]);
// cx.assert(indoc! {"
// The quick
// brown ˇfox
// jumps over"})
// .await;
// cx.assert(indoc! {"
// The quick
// brown fox
// jumps ˇover"})
// .await;
// cx.assert(indoc! {"
// The qˇuick
// brown fox
// jumps over"})
// .await;
// cx.assert(indoc! {"
// The quick
// brown fox
// ˇ"})
// .await;
// }
// #[gpui::test]
// async fn test_delete_end_of_document(cx: &mut gpui::TestAppContext) {
// let mut cx = NeovimBackedTestContext::new(cx).await;
// cx.assert_neovim_compatible(
// indoc! {"
// The quick
// brownˇ fox
// jumps over
// the lazy"},
// ["d", "shift-g"],
// )
// .await;
// cx.assert_neovim_compatible(
// indoc! {"
// The quick
// brownˇ fox
// jumps over
// the lazy"},
// ["d", "shift-g"],
// )
// .await;
// cx.assert_neovim_compatible(
// indoc! {"
// The quick
// brown fox
// jumps over
// the lˇazy"},
// ["d", "shift-g"],
// )
// .await;
// cx.assert_neovim_compatible(
// indoc! {"
// The quick
// brown fox
// jumps over
// ˇ"},
// ["d", "shift-g"],
// )
// .await;
// }
// #[gpui::test]
// async fn test_delete_gg(cx: &mut gpui::TestAppContext) {
// let mut cx = NeovimBackedTestContext::new(cx)
// .await
// .binding(["d", "g", "g"]);
// cx.assert_neovim_compatible(
// indoc! {"
// The quick
// brownˇ fox
// jumps over
// the lazy"},
// ["d", "g", "g"],
// )
// .await;
// cx.assert_neovim_compatible(
// indoc! {"
// The quick
// brown fox
// jumps over
// the lˇazy"},
// ["d", "g", "g"],
// )
// .await;
// cx.assert_neovim_compatible(
// indoc! {"
// The qˇuick
// brown fox
// jumps over
// the lazy"},
// ["d", "g", "g"],
// )
// .await;
// cx.assert_neovim_compatible(
// indoc! {"
// ˇ
// brown fox
// jumps over
// the lazy"},
// ["d", "g", "g"],
// )
// .await;
// }
// #[gpui::test]
// async fn test_cancel_delete_operator(cx: &mut gpui::TestAppContext) {
// let mut cx = VimTestContext::new(cx, true).await;
// cx.set_state(
// indoc! {"
// The quick brown
// fox juˇmps over
// the lazy dog"},
// Mode::Normal,
// );
// // Canceling operator twice reverts to normal mode with no active operator
// cx.simulate_keystrokes(["d", "escape", "k"]);
// assert_eq!(cx.active_operator(), None);
// assert_eq!(cx.mode(), Mode::Normal);
// cx.assert_editor_state(indoc! {"
// The quˇick brown
// fox jumps over
// the lazy dog"});
// }
// #[gpui::test]
// async fn test_unbound_command_cancels_pending_operator(cx: &mut gpui::TestAppContext) {
// let mut cx = VimTestContext::new(cx, true).await;
// cx.set_state(
// indoc! {"
// The quick brown
// fox juˇmps over
// the lazy dog"},
// Mode::Normal,
// );
// // Canceling operator twice reverts to normal mode with no active operator
// cx.simulate_keystrokes(["d", "y"]);
// assert_eq!(cx.active_operator(), None);
// assert_eq!(cx.mode(), Mode::Normal);
// }
// #[gpui::test]
// async fn test_delete_with_counts(cx: &mut gpui::TestAppContext) {
// let mut cx = NeovimBackedTestContext::new(cx).await;
// cx.set_shared_state(indoc! {"
// The ˇquick brown
// fox jumps over
// the lazy dog"})
// .await;
// cx.simulate_shared_keystrokes(["d", "2", "d"]).await;
// cx.assert_shared_state(indoc! {"
// the ˇlazy dog"})
// .await;
// cx.set_shared_state(indoc! {"
// The ˇquick brown
// fox jumps over
// the lazy dog"})
// .await;
// cx.simulate_shared_keystrokes(["2", "d", "d"]).await;
// cx.assert_shared_state(indoc! {"
// the ˇlazy dog"})
// .await;
// cx.set_shared_state(indoc! {"
// The ˇquick brown
// fox jumps over
// the moon,
// a star, and
// the lazy dog"})
// .await;
// cx.simulate_shared_keystrokes(["2", "d", "2", "d"]).await;
// cx.assert_shared_state(indoc! {"
// the ˇlazy dog"})
// .await;
// }
// }

View file

@ -0,0 +1,277 @@
use std::ops::Range;
use editor::{scroll::autoscroll::Autoscroll, MultiBufferSnapshot, ToOffset, ToPoint};
use gpui::{Action, AppContext, WindowContext};
use language::{Bias, Point};
use serde::Deserialize;
use workspace::Workspace;
use crate::{state::Mode, Vim};
#[derive(Action, Clone, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
struct Increment {
#[serde(default)]
step: bool,
}
#[derive(Action, Clone, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
struct Decrement {
#[serde(default)]
step: bool,
}
pub fn init(cx: &mut AppContext) {
// todo!();
// cx.add_action(|_: &mut Workspace, action: &Increment, cx| {
// Vim::update(cx, |vim, cx| {
// vim.record_current_action(cx);
// let count = vim.take_count(cx).unwrap_or(1);
// let step = if action.step { 1 } else { 0 };
// increment(vim, count as i32, step, cx)
// })
// });
// cx.add_action(|_: &mut Workspace, action: &Decrement, cx| {
// Vim::update(cx, |vim, cx| {
// vim.record_current_action(cx);
// let count = vim.take_count(cx).unwrap_or(1);
// let step = if action.step { -1 } else { 0 };
// increment(vim, count as i32 * -1, step, cx)
// })
// });
}
fn increment(vim: &mut Vim, mut delta: i32, step: i32, cx: &mut WindowContext) {
vim.update_active_editor(cx, |editor, cx| {
let mut edits = Vec::new();
let mut new_anchors = Vec::new();
let snapshot = editor.buffer().read(cx).snapshot(cx);
for selection in editor.selections.all_adjusted(cx) {
if !selection.is_empty() {
if vim.state().mode != Mode::VisualBlock || new_anchors.is_empty() {
new_anchors.push((true, snapshot.anchor_before(selection.start)))
}
}
for row in selection.start.row..=selection.end.row {
let start = if row == selection.start.row {
selection.start
} else {
Point::new(row, 0)
};
if let Some((range, num, radix)) = find_number(&snapshot, start) {
if let Ok(val) = i32::from_str_radix(&num, radix) {
let result = val + delta;
delta += step;
let replace = match radix {
10 => format!("{}", result),
16 => {
if num.to_ascii_lowercase() == num {
format!("{:x}", result)
} else {
format!("{:X}", result)
}
}
2 => format!("{:b}", result),
_ => unreachable!(),
};
edits.push((range.clone(), replace));
}
if selection.is_empty() {
new_anchors.push((false, snapshot.anchor_after(range.end)))
}
} else {
if selection.is_empty() {
new_anchors.push((true, snapshot.anchor_after(start)))
}
}
}
}
editor.transact(cx, |editor, cx| {
editor.edit(edits, cx);
let snapshot = editor.buffer().read(cx).snapshot(cx);
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
let mut new_ranges = Vec::new();
for (visual, anchor) in new_anchors.iter() {
let mut point = anchor.to_point(&snapshot);
if !*visual && point.column > 0 {
point.column -= 1;
point = snapshot.clip_point(point, Bias::Left)
}
new_ranges.push(point..point);
}
s.select_ranges(new_ranges)
})
});
});
vim.switch_mode(Mode::Normal, true, cx)
}
fn find_number(
snapshot: &MultiBufferSnapshot,
start: Point,
) -> Option<(Range<Point>, String, u32)> {
let mut offset = start.to_offset(snapshot);
// go backwards to the start of any number the selection is within
for ch in snapshot.reversed_chars_at(offset) {
if ch.is_ascii_digit() || ch == '-' || ch == 'b' || ch == 'x' {
offset -= ch.len_utf8();
continue;
}
break;
}
let mut begin = None;
let mut end = None;
let mut num = String::new();
let mut radix = 10;
let mut chars = snapshot.chars_at(offset).peekable();
// find the next number on the line (may start after the original cursor position)
while let Some(ch) = chars.next() {
if num == "0" && ch == 'b' && chars.peek().is_some() && chars.peek().unwrap().is_digit(2) {
radix = 2;
begin = None;
num = String::new();
}
if num == "0" && ch == 'x' && chars.peek().is_some() && chars.peek().unwrap().is_digit(16) {
radix = 16;
begin = None;
num = String::new();
}
if ch.is_digit(radix)
|| (begin.is_none()
&& ch == '-'
&& chars.peek().is_some()
&& chars.peek().unwrap().is_digit(radix))
{
if begin.is_none() {
begin = Some(offset);
}
num.push(ch);
} else {
if begin.is_some() {
end = Some(offset);
break;
} else if ch == '\n' {
break;
}
}
offset += ch.len_utf8();
}
if let Some(begin) = begin {
let end = end.unwrap_or(offset);
Some((begin.to_point(snapshot)..end.to_point(snapshot), num, radix))
} else {
None
}
}
// #[cfg(test)]
// mod test {
// use indoc::indoc;
// use crate::test::NeovimBackedTestContext;
// #[gpui::test]
// async fn test_increment(cx: &mut gpui::TestAppContext) {
// let mut cx = NeovimBackedTestContext::new(cx).await;
// cx.set_shared_state(indoc! {"
// 1ˇ2
// "})
// .await;
// cx.simulate_shared_keystrokes(["ctrl-a"]).await;
// cx.assert_shared_state(indoc! {"
// 1ˇ3
// "})
// .await;
// cx.simulate_shared_keystrokes(["ctrl-x"]).await;
// cx.assert_shared_state(indoc! {"
// 1ˇ2
// "})
// .await;
// cx.simulate_shared_keystrokes(["9", "9", "ctrl-a"]).await;
// cx.assert_shared_state(indoc! {"
// 11ˇ1
// "})
// .await;
// cx.simulate_shared_keystrokes(["1", "1", "1", "ctrl-x"])
// .await;
// cx.assert_shared_state(indoc! {"
// ˇ0
// "})
// .await;
// cx.simulate_shared_keystrokes(["."]).await;
// cx.assert_shared_state(indoc! {"
// -11ˇ1
// "})
// .await;
// }
// #[gpui::test]
// async fn test_increment_radix(cx: &mut gpui::TestAppContext) {
// let mut cx = NeovimBackedTestContext::new(cx).await;
// cx.assert_matches_neovim("ˇ total: 0xff", ["ctrl-a"], " total: 0x10ˇ0")
// .await;
// cx.assert_matches_neovim("ˇ total: 0xff", ["ctrl-x"], " total: 0xfˇe")
// .await;
// cx.assert_matches_neovim("ˇ total: 0xFF", ["ctrl-x"], " total: 0xFˇE")
// .await;
// cx.assert_matches_neovim("(ˇ0b10f)", ["ctrl-a"], "(0b1ˇ1f)")
// .await;
// cx.assert_matches_neovim("ˇ-1", ["ctrl-a"], "ˇ0").await;
// cx.assert_matches_neovim("banˇana", ["ctrl-a"], "banˇana")
// .await;
// }
// #[gpui::test]
// async fn test_increment_steps(cx: &mut gpui::TestAppContext) {
// let mut cx = NeovimBackedTestContext::new(cx).await;
// cx.set_shared_state(indoc! {"
// ˇ1
// 1
// 1 2
// 1
// 1"})
// .await;
// cx.simulate_shared_keystrokes(["j", "v", "shift-g", "g", "ctrl-a"])
// .await;
// cx.assert_shared_state(indoc! {"
// 1
// ˇ2
// 3 2
// 4
// 5"})
// .await;
// cx.simulate_shared_keystrokes(["shift-g", "ctrl-v", "g", "g"])
// .await;
// cx.assert_shared_state(indoc! {"
// «1ˇ»
// «2ˇ»
// «3ˇ» 2
// «4ˇ»
// «5ˇ»"})
// .await;
// cx.simulate_shared_keystrokes(["g", "ctrl-x"]).await;
// cx.assert_shared_state(indoc! {"
// ˇ0
// 0
// 0 2
// 0
// 0"})
// .await;
// }
// }

View file

@ -0,0 +1,475 @@
use std::{borrow::Cow, cmp};
use editor::{
display_map::ToDisplayPoint, movement, scroll::autoscroll::Autoscroll, ClipboardSelection,
DisplayPoint,
};
use gpui::{Action, AppContext, ViewContext};
use language::{Bias, SelectionGoal};
use serde::Deserialize;
use workspace::Workspace;
use crate::{state::Mode, utils::copy_selections_content, Vim};
#[derive(Action, Clone, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
struct Paste {
#[serde(default)]
before: bool,
#[serde(default)]
preserve_clipboard: bool,
}
pub(crate) fn init(cx: &mut AppContext) {
// todo!()
// cx.add_action(paste);
}
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| {
let text_layout_details = editor.text_layout_details(cx);
editor.transact(cx, |editor, cx| {
editor.set_clip_at_line_ends(false, cx);
let Some(item) = cx.read_from_clipboard() else {
return;
};
let clipboard_text = Cow::Borrowed(item.text());
if clipboard_text.is_empty() {
return;
}
if !action.preserve_clipboard && vim.state().mode.is_visual() {
copy_selections_content(editor, vim.state().mode == Mode::VisualLine, cx);
}
// if we are copying from multi-cursor (of visual block mode), we want
// to
let clipboard_selections =
item.metadata::<Vec<ClipboardSelection>>()
.filter(|clipboard_selections| {
clipboard_selections.len() > 1 && vim.state().mode != Mode::VisualLine
});
let (display_map, current_selections) = editor.selections.all_adjusted_display(cx);
// unlike zed, if you have a multi-cursor selection from vim block mode,
// pasting it will paste it on subsequent lines, even if you don't yet
// have a cursor there.
let mut selections_to_process = Vec::new();
let mut i = 0;
while i < current_selections.len() {
selections_to_process
.push((current_selections[i].start..current_selections[i].end, true));
i += 1;
}
if let Some(clipboard_selections) = clipboard_selections.as_ref() {
let left = current_selections
.iter()
.map(|selection| cmp::min(selection.start.column(), selection.end.column()))
.min()
.unwrap();
let mut row = current_selections.last().unwrap().end.row() + 1;
while i < clipboard_selections.len() {
let cursor =
display_map.clip_point(DisplayPoint::new(row, left), Bias::Left);
selections_to_process.push((cursor..cursor, false));
i += 1;
row += 1;
}
}
let first_selection_indent_column =
clipboard_selections.as_ref().and_then(|zed_selections| {
zed_selections
.first()
.map(|selection| selection.first_line_indent)
});
let before = action.before || vim.state().mode == Mode::VisualLine;
let mut edits = Vec::new();
let mut new_selections = Vec::new();
let mut original_indent_columns = Vec::new();
let mut start_offset = 0;
for (ix, (selection, preserve)) in selections_to_process.iter().enumerate() {
let (mut to_insert, original_indent_column) =
if let Some(clipboard_selections) = &clipboard_selections {
if let Some(clipboard_selection) = clipboard_selections.get(ix) {
let end_offset = start_offset + clipboard_selection.len;
let text = clipboard_text[start_offset..end_offset].to_string();
start_offset = end_offset + 1;
(text, Some(clipboard_selection.first_line_indent))
} else {
("".to_string(), first_selection_indent_column)
}
} else {
(clipboard_text.to_string(), first_selection_indent_column)
};
let line_mode = to_insert.ends_with("\n");
let is_multiline = to_insert.contains("\n");
if line_mode && !before {
if selection.is_empty() {
to_insert =
"\n".to_owned() + &to_insert[..to_insert.len() - "\n".len()];
} else {
to_insert = "\n".to_owned() + &to_insert;
}
} else if !line_mode && vim.state().mode == Mode::VisualLine {
to_insert = to_insert + "\n";
}
let display_range = if !selection.is_empty() {
selection.start..selection.end
} else if line_mode {
let point = if before {
movement::line_beginning(&display_map, selection.start, false)
} else {
movement::line_end(&display_map, selection.start, false)
};
point..point
} else {
let point = if before {
selection.start
} else {
movement::saturating_right(&display_map, selection.start)
};
point..point
};
let point_range = display_range.start.to_point(&display_map)
..display_range.end.to_point(&display_map);
let anchor = if is_multiline || vim.state().mode == Mode::VisualLine {
display_map.buffer_snapshot.anchor_before(point_range.start)
} else {
display_map.buffer_snapshot.anchor_after(point_range.end)
};
if *preserve {
new_selections.push((anchor, line_mode, is_multiline));
}
edits.push((point_range, to_insert));
original_indent_columns.extend(original_indent_column);
}
editor.edit_with_block_indent(edits, original_indent_columns, cx);
// in line_mode vim will insert the new text on the next (or previous if before) line
// and put the cursor on the first non-blank character of the first inserted line (or at the end if the first line is blank).
// otherwise vim will insert the next text at (or before) the current cursor position,
// the cursor will go to the last (or first, if is_multiline) inserted character.
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.replace_cursors_with(|map| {
let mut cursors = Vec::new();
for (anchor, line_mode, is_multiline) in &new_selections {
let mut cursor = anchor.to_display_point(map);
if *line_mode {
if !before {
cursor = movement::down(
map,
cursor,
SelectionGoal::None,
false,
&text_layout_details,
)
.0;
}
cursor = movement::indented_line_beginning(map, cursor, true);
} else if !is_multiline {
cursor = movement::saturating_left(map, cursor)
}
cursors.push(cursor);
if vim.state().mode == Mode::VisualBlock {
break;
}
}
cursors
});
})
});
});
vim.switch_mode(Mode::Normal, true, cx);
});
}
// #[cfg(test)]
// mod test {
// use crate::{
// state::Mode,
// test::{NeovimBackedTestContext, VimTestContext},
// };
// use indoc::indoc;
// #[gpui::test]
// async fn test_paste(cx: &mut gpui::TestAppContext) {
// let mut cx = NeovimBackedTestContext::new(cx).await;
// // single line
// cx.set_shared_state(indoc! {"
// The quick brown
// fox ˇjumps over
// the lazy dog"})
// .await;
// cx.simulate_shared_keystrokes(["v", "w", "y"]).await;
// cx.assert_shared_clipboard("jumps o").await;
// cx.set_shared_state(indoc! {"
// The quick brown
// fox jumps oveˇr
// the lazy dog"})
// .await;
// cx.simulate_shared_keystroke("p").await;
// cx.assert_shared_state(indoc! {"
// The quick brown
// fox jumps overjumps ˇo
// the lazy dog"})
// .await;
// cx.set_shared_state(indoc! {"
// The quick brown
// fox jumps oveˇr
// the lazy dog"})
// .await;
// cx.simulate_shared_keystroke("shift-p").await;
// cx.assert_shared_state(indoc! {"
// The quick brown
// fox jumps ovejumps ˇor
// the lazy dog"})
// .await;
// // line mode
// cx.set_shared_state(indoc! {"
// The quick brown
// fox juˇmps over
// the lazy dog"})
// .await;
// cx.simulate_shared_keystrokes(["d", "d"]).await;
// cx.assert_shared_clipboard("fox jumps over\n").await;
// cx.assert_shared_state(indoc! {"
// The quick brown
// the laˇzy dog"})
// .await;
// cx.simulate_shared_keystroke("p").await;
// cx.assert_shared_state(indoc! {"
// The quick brown
// the lazy dog
// ˇfox jumps over"})
// .await;
// cx.simulate_shared_keystrokes(["k", "shift-p"]).await;
// cx.assert_shared_state(indoc! {"
// The quick brown
// ˇfox jumps over
// the lazy dog
// fox jumps over"})
// .await;
// // multiline, cursor to first character of pasted text.
// cx.set_shared_state(indoc! {"
// The quick brown
// fox jumps ˇover
// the lazy dog"})
// .await;
// cx.simulate_shared_keystrokes(["v", "j", "y"]).await;
// cx.assert_shared_clipboard("over\nthe lazy do").await;
// cx.simulate_shared_keystroke("p").await;
// cx.assert_shared_state(indoc! {"
// The quick brown
// fox jumps oˇover
// the lazy dover
// the lazy dog"})
// .await;
// cx.simulate_shared_keystrokes(["u", "shift-p"]).await;
// cx.assert_shared_state(indoc! {"
// The quick brown
// fox jumps ˇover
// the lazy doover
// the lazy dog"})
// .await;
// }
// #[gpui::test]
// async fn test_paste_visual(cx: &mut gpui::TestAppContext) {
// let mut cx = NeovimBackedTestContext::new(cx).await;
// // copy in visual mode
// cx.set_shared_state(indoc! {"
// The quick brown
// fox jˇumps over
// the lazy dog"})
// .await;
// cx.simulate_shared_keystrokes(["v", "i", "w", "y"]).await;
// cx.assert_shared_state(indoc! {"
// The quick brown
// fox ˇjumps over
// the lazy dog"})
// .await;
// // paste in visual mode
// cx.simulate_shared_keystrokes(["w", "v", "i", "w", "p"])
// .await;
// cx.assert_shared_state(indoc! {"
// The quick brown
// fox jumps jumpˇs
// the lazy dog"})
// .await;
// cx.assert_shared_clipboard("over").await;
// // paste in visual line mode
// cx.simulate_shared_keystrokes(["up", "shift-v", "shift-p"])
// .await;
// cx.assert_shared_state(indoc! {"
// ˇover
// fox jumps jumps
// the lazy dog"})
// .await;
// cx.assert_shared_clipboard("over").await;
// // paste in visual block mode
// cx.simulate_shared_keystrokes(["ctrl-v", "down", "down", "p"])
// .await;
// cx.assert_shared_state(indoc! {"
// oveˇrver
// overox jumps jumps
// overhe lazy dog"})
// .await;
// // copy in visual line mode
// cx.set_shared_state(indoc! {"
// The quick brown
// fox juˇmps over
// the lazy dog"})
// .await;
// cx.simulate_shared_keystrokes(["shift-v", "d"]).await;
// cx.assert_shared_state(indoc! {"
// The quick brown
// the laˇzy dog"})
// .await;
// // paste in visual mode
// cx.simulate_shared_keystrokes(["v", "i", "w", "p"]).await;
// cx.assert_shared_state(
// &indoc! {"
// The quick brown
// the_
// ˇfox jumps over
// _dog"}
// .replace("_", " "), // Hack for trailing whitespace
// )
// .await;
// cx.assert_shared_clipboard("lazy").await;
// cx.set_shared_state(indoc! {"
// The quick brown
// fox juˇmps over
// the lazy dog"})
// .await;
// cx.simulate_shared_keystrokes(["shift-v", "d"]).await;
// cx.assert_shared_state(indoc! {"
// The quick brown
// the laˇzy dog"})
// .await;
// // paste in visual line mode
// cx.simulate_shared_keystrokes(["k", "shift-v", "p"]).await;
// cx.assert_shared_state(indoc! {"
// ˇfox jumps over
// the lazy dog"})
// .await;
// cx.assert_shared_clipboard("The quick brown\n").await;
// }
// #[gpui::test]
// async fn test_paste_visual_block(cx: &mut gpui::TestAppContext) {
// let mut cx = NeovimBackedTestContext::new(cx).await;
// // copy in visual block mode
// cx.set_shared_state(indoc! {"
// The ˇquick brown
// fox jumps over
// the lazy dog"})
// .await;
// cx.simulate_shared_keystrokes(["ctrl-v", "2", "j", "y"])
// .await;
// cx.assert_shared_clipboard("q\nj\nl").await;
// cx.simulate_shared_keystrokes(["p"]).await;
// cx.assert_shared_state(indoc! {"
// The qˇquick brown
// fox jjumps over
// the llazy dog"})
// .await;
// cx.simulate_shared_keystrokes(["v", "i", "w", "shift-p"])
// .await;
// cx.assert_shared_state(indoc! {"
// The ˇq brown
// fox jjjumps over
// the lllazy dog"})
// .await;
// cx.simulate_shared_keystrokes(["v", "i", "w", "shift-p"])
// .await;
// cx.set_shared_state(indoc! {"
// The ˇquick brown
// fox jumps over
// the lazy dog"})
// .await;
// cx.simulate_shared_keystrokes(["ctrl-v", "j", "y"]).await;
// cx.assert_shared_clipboard("q\nj").await;
// cx.simulate_shared_keystrokes(["l", "ctrl-v", "2", "j", "shift-p"])
// .await;
// cx.assert_shared_state(indoc! {"
// The qˇqick brown
// fox jjmps over
// the lzy dog"})
// .await;
// cx.simulate_shared_keystrokes(["shift-v", "p"]).await;
// cx.assert_shared_state(indoc! {"
// ˇq
// j
// fox jjmps over
// the lzy dog"})
// .await;
// }
// #[gpui::test]
// async fn test_paste_indent(cx: &mut gpui::TestAppContext) {
// let mut cx = VimTestContext::new_typescript(cx).await;
// cx.set_state(
// indoc! {"
// class A {ˇ
// }
// "},
// Mode::Normal,
// );
// cx.simulate_keystrokes(["o", "a", "(", ")", "{", "escape"]);
// cx.assert_state(
// indoc! {"
// class A {
// a()ˇ{}
// }
// "},
// Mode::Normal,
// );
// // cursor goes to the first non-blank character in the line;
// cx.simulate_keystrokes(["y", "y", "p"]);
// cx.assert_state(
// indoc! {"
// class A {
// a(){}
// ˇa(){}
// }
// "},
// Mode::Normal,
// );
// // indentation is preserved when pasting
// cx.simulate_keystrokes(["u", "shift-v", "up", "y", "shift-p"]);
// cx.assert_state(
// indoc! {"
// ˇclass A {
// a(){}
// class A {
// a(){}
// }
// "},
// Mode::Normal,
// );
// }
// }

View file

@ -0,0 +1,524 @@
use crate::{
insert::NormalBefore,
motion::Motion,
state::{Mode, RecordedSelection, ReplayableAction},
visual::visual_motion,
Vim,
};
use gpui::{actions, Action, AppContext, WindowContext};
use workspace::Workspace;
actions!(Repeat, EndRepeat);
fn should_replay(action: &Box<dyn Action>) -> bool {
// skip so that we don't leave the character palette open
if editor::ShowCharacterPalette.partial_eq(&**action) {
return false;
}
true
}
fn repeatable_insert(action: &ReplayableAction) -> Option<Box<dyn Action>> {
match action {
ReplayableAction::Action(action) => {
if super::InsertBefore.partial_eq(&**action)
|| super::InsertAfter.partial_eq(&**action)
|| super::InsertFirstNonWhitespace.partial_eq(&**action)
|| super::InsertEndOfLine.partial_eq(&**action)
{
Some(super::InsertBefore.boxed_clone())
} else if super::InsertLineAbove.partial_eq(&**action)
|| super::InsertLineBelow.partial_eq(&**action)
{
Some(super::InsertLineBelow.boxed_clone())
} else {
None
}
}
ReplayableAction::Insertion { .. } => None,
}
}
pub(crate) fn init(cx: &mut AppContext) {
// todo!()
// cx.add_action(|_: &mut Workspace, _: &EndRepeat, cx| {
// Vim::update(cx, |vim, cx| {
// vim.workspace_state.replaying = false;
// vim.switch_mode(Mode::Normal, false, cx)
// });
// });
// cx.add_action(|_: &mut Workspace, _: &Repeat, cx| repeat(cx, false));
}
pub(crate) fn repeat(cx: &mut WindowContext, from_insert_mode: bool) {
let Some((mut actions, editor, selection)) = Vim::update(cx, |vim, cx| {
let actions = vim.workspace_state.recorded_actions.clone();
if actions.is_empty() {
return None;
}
let Some(editor) = vim.active_editor.clone() else {
return None;
};
let count = vim.take_count(cx);
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);
}
}
}
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 => {}
}
// insert internally uses repeat to handle counts
// vim doesn't treat 3a1 as though you literally repeated a1
// 3 times, instead it inserts the content thrice at the insert position.
if let Some(to_repeat) = repeatable_insert(&actions[0]) {
if let Some(ReplayableAction::Action(action)) = actions.last() {
if NormalBefore.partial_eq(&**action) {
actions.pop();
}
}
let mut new_actions = actions.clone();
actions[0] = ReplayableAction::Action(to_repeat.boxed_clone());
let mut count = Vim::read(cx).workspace_state.recorded_count.unwrap_or(1);
// if we came from insert mode we're just doing repititions 2 onwards.
if from_insert_mode {
count -= 1;
new_actions[0] = actions[0].clone();
}
for _ in 1..count {
new_actions.append(actions.clone().as_mut());
}
new_actions.push(ReplayableAction::Action(NormalBefore.boxed_clone()));
actions = new_actions;
}
Vim::update(cx, |vim, _| vim.workspace_state.replaying = true);
let window = cx.window_handle();
cx.spawn(move |mut cx| async move {
editor.update(&mut cx, |editor, _| {
editor.show_local_selections = false;
})?;
for action in actions {
match action {
ReplayableAction::Action(action) => {
if should_replay(&action) {
window.update(&mut cx, |_, cx| cx.dispatch_action(action))
} 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)
}),
}?
}
editor.update(&mut cx, |editor, _| {
editor.show_local_selections = true;
})?;
window.update(&mut cx, |_, cx| cx.dispatch_action(EndRepeat.boxed_clone()))
})
.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.assert_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;
// }
// #[gpui::test]
// async fn test_repeat_motion_counts(
// deterministic: Arc<Deterministic>,
// cx: &mut gpui::TestAppContext,
// ) {
// let mut cx = NeovimBackedTestContext::new(cx).await;
// cx.set_shared_state(indoc! {
// "ˇthe quick brown
// fox jumps over
// the lazy dog"
// })
// .await;
// cx.simulate_shared_keystrokes(["3", "d", "3", "l"]).await;
// cx.assert_shared_state(indoc! {
// "ˇ brown
// fox jumps over
// the lazy dog"
// })
// .await;
// cx.simulate_shared_keystrokes(["j", "."]).await;
// deterministic.run_until_parked();
// cx.assert_shared_state(indoc! {
// " brown
// ˇ over
// the lazy dog"
// })
// .await;
// cx.simulate_shared_keystrokes(["j", "2", "."]).await;
// deterministic.run_until_parked();
// cx.assert_shared_state(indoc! {
// " brown
// over
// ˇe lazy dog"
// })
// .await;
// }
// #[gpui::test]
// async fn test_record_interrupted(
// deterministic: Arc<Deterministic>,
// cx: &mut gpui::TestAppContext,
// ) {
// let mut cx = VimTestContext::new(cx, true).await;
// cx.set_state("ˇhello\n", Mode::Normal);
// cx.simulate_keystrokes(["4", "i", "j", "cmd-shift-p", "escape", "escape"]);
// deterministic.run_until_parked();
// cx.assert_state("ˇjhello\n", Mode::Normal);
// }
// }

View file

@ -0,0 +1,227 @@
use crate::Vim;
use editor::{
display_map::ToDisplayPoint,
scroll::{scroll_amount::ScrollAmount, VERTICAL_SCROLL_MARGIN},
DisplayPoint, Editor,
};
use gpui::{actions, AppContext, ViewContext};
use language::Bias;
use workspace::Workspace;
actions!(LineUp, LineDown, ScrollUp, ScrollDown, PageUp, PageDown,);
pub fn init(cx: &mut AppContext) {
// todo!()
// cx.add_action(|_: &mut Workspace, _: &LineDown, cx| {
// scroll(cx, false, |c| ScrollAmount::Line(c.unwrap_or(1.)))
// });
// cx.add_action(|_: &mut Workspace, _: &LineUp, cx| {
// scroll(cx, false, |c| ScrollAmount::Line(-c.unwrap_or(1.)))
// });
// cx.add_action(|_: &mut Workspace, _: &PageDown, cx| {
// scroll(cx, false, |c| ScrollAmount::Page(c.unwrap_or(1.)))
// });
// cx.add_action(|_: &mut Workspace, _: &PageUp, cx| {
// scroll(cx, false, |c| ScrollAmount::Page(-c.unwrap_or(1.)))
// });
// cx.add_action(|_: &mut Workspace, _: &ScrollDown, cx| {
// scroll(cx, true, |c| {
// if let Some(c) = c {
// ScrollAmount::Line(c)
// } else {
// ScrollAmount::Page(0.5)
// }
// })
// });
// cx.add_action(|_: &mut Workspace, _: &ScrollUp, cx| {
// scroll(cx, true, |c| {
// if let Some(c) = c {
// ScrollAmount::Line(-c)
// } else {
// ScrollAmount::Page(-0.5)
// }
// })
// });
}
fn scroll(
cx: &mut ViewContext<Workspace>,
move_cursor: bool,
by: fn(c: Option<f32>) -> ScrollAmount,
) {
Vim::update(cx, |vim, cx| {
let amount = by(vim.take_count(cx).map(|c| c as f32));
vim.update_active_editor(cx, |editor, cx| {
scroll_editor(editor, move_cursor, &amount, cx)
});
})
}
fn scroll_editor(
editor: &mut Editor,
preserve_cursor_position: bool,
amount: &ScrollAmount,
cx: &mut ViewContext<Editor>,
) {
let should_move_cursor = editor.newest_selection_on_screen(cx).is_eq();
let old_top_anchor = editor.scroll_manager.anchor().anchor;
editor.scroll_screen(amount, cx);
if should_move_cursor {
let visible_rows = if let Some(visible_rows) = editor.visible_line_count() {
visible_rows as u32
} else {
return;
};
let top_anchor = editor.scroll_manager.anchor().anchor;
editor.change_selections(None, cx, |s| {
s.move_with(|map, selection| {
let mut head = selection.head();
let top = top_anchor.to_display_point(map);
if preserve_cursor_position {
let old_top = old_top_anchor.to_display_point(map);
let new_row = top.row() + selection.head().row() - old_top.row();
head = map.clip_point(DisplayPoint::new(new_row, head.column()), Bias::Left)
}
let min_row = top.row() + VERTICAL_SCROLL_MARGIN as u32;
let max_row = top.row() + visible_rows - VERTICAL_SCROLL_MARGIN as u32 - 1;
let new_head = if head.row() < min_row {
map.clip_point(DisplayPoint::new(min_row, head.column()), Bias::Left)
} else if head.row() > max_row {
map.clip_point(DisplayPoint::new(max_row, head.column()), Bias::Left)
} else {
head
};
if selection.is_empty() {
selection.collapse_to(new_head, selection.goal)
} else {
selection.set_head(new_head, selection.goal)
};
})
});
}
}
// #[cfg(test)]
// mod test {
// use crate::{
// state::Mode,
// test::{NeovimBackedTestContext, VimTestContext},
// };
// use gpui::geometry::vector::vec2f;
// use indoc::indoc;
// use language::Point;
// #[gpui::test]
// async fn test_scroll(cx: &mut gpui::TestAppContext) {
// let mut cx = VimTestContext::new(cx, true).await;
// let window = cx.window;
// let line_height = cx.editor(|editor, cx| editor.style().text.line_height(cx.font_cache()));
// window.simulate_resize(vec2f(1000., 8.0 * line_height - 1.0), &mut cx);
// cx.set_state(
// indoc!(
// "ˇone
// two
// three
// four
// five
// six
// seven
// eight
// nine
// ten
// eleven
// twelve
// "
// ),
// Mode::Normal,
// );
// cx.update_editor(|editor, cx| {
// assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 0.))
// });
// cx.simulate_keystrokes(["ctrl-e"]);
// cx.update_editor(|editor, cx| {
// assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 1.))
// });
// cx.simulate_keystrokes(["2", "ctrl-e"]);
// cx.update_editor(|editor, cx| {
// assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.))
// });
// cx.simulate_keystrokes(["ctrl-y"]);
// cx.update_editor(|editor, cx| {
// assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 2.))
// });
// // does not select in normal mode
// cx.simulate_keystrokes(["g", "g"]);
// cx.update_editor(|editor, cx| {
// assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 0.))
// });
// cx.simulate_keystrokes(["ctrl-d"]);
// cx.update_editor(|editor, cx| {
// assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.0));
// assert_eq!(
// editor.selections.newest(cx).range(),
// Point::new(6, 0)..Point::new(6, 0)
// )
// });
// // does select in visual mode
// cx.simulate_keystrokes(["g", "g"]);
// cx.update_editor(|editor, cx| {
// assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 0.))
// });
// cx.simulate_keystrokes(["v", "ctrl-d"]);
// cx.update_editor(|editor, cx| {
// assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.0));
// assert_eq!(
// editor.selections.newest(cx).range(),
// Point::new(0, 0)..Point::new(6, 1)
// )
// });
// }
// #[gpui::test]
// async fn test_ctrl_d_u(cx: &mut gpui::TestAppContext) {
// let mut cx = NeovimBackedTestContext::new(cx).await;
// cx.set_scroll_height(10).await;
// pub fn sample_text(rows: usize, cols: usize, start_char: char) -> String {
// let mut text = String::new();
// for row in 0..rows {
// let c: char = (start_char as u32 + row as u32) as u8 as char;
// let mut line = c.to_string().repeat(cols);
// if row < rows - 1 {
// line.push('\n');
// }
// text += &line;
// }
// text
// }
// let content = "ˇ".to_owned() + &sample_text(26, 2, 'a');
// cx.set_shared_state(&content).await;
// // skip over the scrolloff at the top
// // test ctrl-d
// cx.simulate_shared_keystrokes(["4", "j", "ctrl-d"]).await;
// cx.assert_state_matches().await;
// cx.simulate_shared_keystrokes(["ctrl-d"]).await;
// cx.assert_state_matches().await;
// cx.simulate_shared_keystrokes(["g", "g", "ctrl-d"]).await;
// cx.assert_state_matches().await;
// // test ctrl-u
// cx.simulate_shared_keystrokes(["ctrl-u"]).await;
// cx.assert_state_matches().await;
// cx.simulate_shared_keystrokes(["ctrl-d", "ctrl-d", "4", "j", "ctrl-u", "ctrl-u"])
// .await;
// cx.assert_state_matches().await;
// }
// }

View file

@ -0,0 +1,492 @@
use gpui::{actions, Action, AppContext, ViewContext};
use search::{buffer_search, BufferSearchBar, SearchMode, SearchOptions};
use serde_derive::Deserialize;
use workspace::{searchable::Direction, Pane, Workspace};
use crate::{motion::Motion, normal::move_cursor, state::SearchState, Vim};
#[derive(Action, Clone, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub(crate) struct MoveToNext {
#[serde(default)]
partial_word: bool,
}
#[derive(Action, Clone, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub(crate) struct MoveToPrev {
#[serde(default)]
partial_word: bool,
}
#[derive(Action, Clone, Deserialize, PartialEq)]
pub(crate) struct Search {
#[serde(default)]
backwards: bool,
}
#[derive(Action, Debug, Clone, PartialEq, Deserialize)]
pub struct FindCommand {
pub query: String,
pub backwards: bool,
}
#[derive(Action, Debug, Clone, PartialEq, Deserialize)]
pub struct ReplaceCommand {
pub query: String,
}
#[derive(Debug, Default)]
struct Replacement {
search: String,
replacement: String,
should_replace_all: bool,
is_case_sensitive: bool,
}
actions!(SearchSubmit);
pub(crate) fn init(cx: &mut AppContext) {
// todo!()
// cx.add_action(move_to_next);
// cx.add_action(move_to_prev);
// cx.add_action(search);
// cx.add_action(search_submit);
// cx.add_action(search_deploy);
// cx.add_action(find_command);
// cx.add_action(replace_command);
}
fn move_to_next(workspace: &mut Workspace, action: &MoveToNext, cx: &mut ViewContext<Workspace>) {
move_to_internal(workspace, Direction::Next, !action.partial_word, cx)
}
fn move_to_prev(workspace: &mut Workspace, action: &MoveToPrev, cx: &mut ViewContext<Workspace>) {
move_to_internal(workspace, Direction::Prev, !action.partial_word, cx)
}
fn search(workspace: &mut Workspace, action: &Search, cx: &mut ViewContext<Workspace>) {
let pane = workspace.active_pane().clone();
let direction = if action.backwards {
Direction::Prev
} else {
Direction::Next
};
Vim::update(cx, |vim, cx| {
let count = vim.take_count(cx).unwrap_or(1);
pane.update(cx, |pane, cx| {
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
search_bar.update(cx, |search_bar, cx| {
if !search_bar.show(cx) {
return;
}
let query = search_bar.query(cx);
search_bar.select_query(cx);
cx.focus_self();
if query.is_empty() {
search_bar.set_replacement(None, cx);
search_bar.set_search_options(SearchOptions::CASE_SENSITIVE, cx);
search_bar.activate_search_mode(SearchMode::Regex, cx);
}
vim.workspace_state.search = SearchState {
direction,
count,
initial_query: query.clone(),
};
});
}
})
})
}
// hook into the existing to clear out any vim search state on cmd+f or edit -> find.
fn search_deploy(_: &mut Pane, _: &buffer_search::Deploy, cx: &mut ViewContext<Pane>) {
Vim::update(cx, |vim, _| vim.workspace_state.search = Default::default());
cx.propagate();
}
fn search_submit(workspace: &mut Workspace, _: &SearchSubmit, cx: &mut ViewContext<Workspace>) {
Vim::update(cx, |vim, cx| {
let pane = workspace.active_pane().clone();
pane.update(cx, |pane, cx| {
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
search_bar.update(cx, |search_bar, cx| {
let state = &mut vim.workspace_state.search;
let mut count = state.count;
let direction = state.direction;
// in the case that the query has changed, the search bar
// will have selected the next match already.
if (search_bar.query(cx) != state.initial_query)
&& state.direction == Direction::Next
{
count = count.saturating_sub(1)
}
state.count = 1;
search_bar.select_match(direction, count, cx);
search_bar.focus_editor(&Default::default(), cx);
});
}
});
})
}
pub fn move_to_internal(
workspace: &mut Workspace,
direction: Direction,
whole_word: bool,
cx: &mut ViewContext<Workspace>,
) {
Vim::update(cx, |vim, cx| {
let pane = workspace.active_pane().clone();
let count = vim.take_count(cx).unwrap_or(1);
pane.update(cx, |pane, cx| {
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
let search = search_bar.update(cx, |search_bar, cx| {
let mut options = SearchOptions::CASE_SENSITIVE;
options.set(SearchOptions::WHOLE_WORD, whole_word);
if search_bar.show(cx) {
search_bar
.query_suggestion(cx)
.map(|query| search_bar.search(&query, Some(options), cx))
} else {
None
}
});
if let Some(search) = search {
let search_bar = search_bar.downgrade();
cx.spawn(|_, mut cx| async move {
search.await?;
search_bar.update(&mut cx, |search_bar, cx| {
search_bar.select_match(direction, count, cx)
})?;
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
}
});
vim.clear_operator(cx);
});
}
fn find_command(workspace: &mut Workspace, action: &FindCommand, cx: &mut ViewContext<Workspace>) {
let pane = workspace.active_pane().clone();
pane.update(cx, |pane, cx| {
if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
let search = search_bar.update(cx, |search_bar, cx| {
if !search_bar.show(cx) {
return None;
}
let mut query = action.query.clone();
if query == "" {
query = search_bar.query(cx);
};
search_bar.activate_search_mode(SearchMode::Regex, cx);
Some(search_bar.search(&query, Some(SearchOptions::CASE_SENSITIVE), cx))
});
let Some(search) = search else { return };
let search_bar = search_bar.downgrade();
let direction = if action.backwards {
Direction::Prev
} else {
Direction::Next
};
cx.spawn(|_, mut cx| async move {
search.await?;
search_bar.update(&mut cx, |search_bar, cx| {
search_bar.select_match(direction, 1, cx)
})?;
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
})
}
fn replace_command(
workspace: &mut Workspace,
action: &ReplaceCommand,
cx: &mut ViewContext<Workspace>,
) {
let replacement = parse_replace_all(&action.query);
let pane = workspace.active_pane().clone();
pane.update(cx, |pane, cx| {
let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() else {
return;
};
let search = search_bar.update(cx, |search_bar, cx| {
if !search_bar.show(cx) {
return None;
}
let mut options = SearchOptions::default();
if replacement.is_case_sensitive {
options.set(SearchOptions::CASE_SENSITIVE, true)
}
let search = if replacement.search == "" {
search_bar.query(cx)
} else {
replacement.search
};
search_bar.set_replacement(Some(&replacement.replacement), cx);
search_bar.activate_search_mode(SearchMode::Regex, cx);
Some(search_bar.search(&search, Some(options), cx))
});
let Some(search) = search else { return };
let search_bar = search_bar.downgrade();
cx.spawn(|_, mut cx| async move {
search.await?;
search_bar.update(&mut cx, |search_bar, cx| {
if replacement.should_replace_all {
search_bar.select_last_match(cx);
search_bar.replace_all(&Default::default(), cx);
Vim::update(cx, |vim, cx| {
move_cursor(
vim,
Motion::StartOfLine {
display_lines: false,
},
None,
cx,
)
})
}
})?;
anyhow::Ok(())
})
.detach_and_log_err(cx);
})
}
// convert a vim query into something more usable by zed.
// we don't attempt to fully convert between the two regex syntaxes,
// but we do flip \( and \) to ( and ) (and vice-versa) in the pattern,
// and convert \0..\9 to $0..$9 in the replacement so that common idioms work.
fn parse_replace_all(query: &str) -> Replacement {
let mut chars = query.chars();
if Some('%') != chars.next() || Some('s') != chars.next() {
return Replacement::default();
}
let Some(delimeter) = chars.next() else {
return Replacement::default();
};
let mut search = String::new();
let mut replacement = String::new();
let mut flags = String::new();
let mut buffer = &mut search;
let mut escaped = false;
// 0 - parsing search
// 1 - parsing replacement
// 2 - parsing flags
let mut phase = 0;
for c in chars {
if escaped {
escaped = false;
if phase == 1 && c.is_digit(10) {
buffer.push('$')
// unescape escaped parens
} else if phase == 0 && c == '(' || c == ')' {
} else if c != delimeter {
buffer.push('\\')
}
buffer.push(c)
} else if c == '\\' {
escaped = true;
} else if c == delimeter {
if phase == 0 {
buffer = &mut replacement;
phase = 1;
} else if phase == 1 {
buffer = &mut flags;
phase = 2;
} else {
break;
}
} else {
// escape unescaped parens
if phase == 0 && c == '(' || c == ')' {
buffer.push('\\')
}
buffer.push(c)
}
}
let mut replacement = Replacement {
search,
replacement,
should_replace_all: true,
is_case_sensitive: true,
};
for c in flags.chars() {
match c {
'g' | 'I' => {}
'c' | 'n' => replacement.should_replace_all = false,
'i' => replacement.is_case_sensitive = false,
_ => {}
}
}
replacement
}
// #[cfg(test)]
// mod test {
// use std::sync::Arc;
// use editor::DisplayPoint;
// use search::BufferSearchBar;
// use crate::{state::Mode, test::VimTestContext};
// #[gpui::test]
// async fn test_move_to_next(
// cx: &mut gpui::TestAppContext,
// deterministic: Arc<gpui::executor::Deterministic>,
// ) {
// let mut cx = VimTestContext::new(cx, true).await;
// cx.set_state("ˇhi\nhigh\nhi\n", Mode::Normal);
// cx.simulate_keystrokes(["*"]);
// deterministic.run_until_parked();
// cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
// cx.simulate_keystrokes(["*"]);
// deterministic.run_until_parked();
// cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
// cx.simulate_keystrokes(["#"]);
// deterministic.run_until_parked();
// cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
// cx.simulate_keystrokes(["#"]);
// deterministic.run_until_parked();
// cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
// cx.simulate_keystrokes(["2", "*"]);
// deterministic.run_until_parked();
// cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
// cx.simulate_keystrokes(["g", "*"]);
// deterministic.run_until_parked();
// cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
// cx.simulate_keystrokes(["n"]);
// cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
// cx.simulate_keystrokes(["g", "#"]);
// deterministic.run_until_parked();
// cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
// }
// #[gpui::test]
// async fn test_search(
// cx: &mut gpui::TestAppContext,
// deterministic: Arc<gpui::executor::Deterministic>,
// ) {
// let mut cx = VimTestContext::new(cx, true).await;
// cx.set_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal);
// cx.simulate_keystrokes(["/", "c", "c"]);
// let search_bar = cx.workspace(|workspace, cx| {
// workspace
// .active_pane()
// .read(cx)
// .toolbar()
// .read(cx)
// .item_of_type::<BufferSearchBar>()
// .expect("Buffer search bar should be deployed")
// });
// search_bar.read_with(cx.cx, |bar, cx| {
// assert_eq!(bar.query(cx), "cc");
// });
// deterministic.run_until_parked();
// cx.update_editor(|editor, cx| {
// let highlights = editor.all_text_background_highlights(cx);
// assert_eq!(3, highlights.len());
// assert_eq!(
// DisplayPoint::new(2, 0)..DisplayPoint::new(2, 2),
// highlights[0].0
// )
// });
// cx.simulate_keystrokes(["enter"]);
// cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
// // n to go to next/N to go to previous
// cx.simulate_keystrokes(["n"]);
// cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal);
// cx.simulate_keystrokes(["shift-n"]);
// cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
// // ?<enter> to go to previous
// cx.simulate_keystrokes(["?", "enter"]);
// deterministic.run_until_parked();
// cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal);
// cx.simulate_keystrokes(["?", "enter"]);
// deterministic.run_until_parked();
// cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal);
// // /<enter> to go to next
// cx.simulate_keystrokes(["/", "enter"]);
// deterministic.run_until_parked();
// cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal);
// // ?{search}<enter> to search backwards
// cx.simulate_keystrokes(["?", "b", "enter"]);
// deterministic.run_until_parked();
// cx.assert_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal);
// // works with counts
// cx.simulate_keystrokes(["4", "/", "c"]);
// deterministic.run_until_parked();
// cx.simulate_keystrokes(["enter"]);
// cx.assert_state("aa\nbb\ncc\ncˇc\ncc\n", Mode::Normal);
// // check that searching resumes from cursor, not previous match
// cx.set_state("ˇaa\nbb\ndd\ncc\nbb\n", Mode::Normal);
// cx.simulate_keystrokes(["/", "d"]);
// deterministic.run_until_parked();
// cx.simulate_keystrokes(["enter"]);
// cx.assert_state("aa\nbb\nˇdd\ncc\nbb\n", Mode::Normal);
// cx.update_editor(|editor, cx| editor.move_to_beginning(&Default::default(), cx));
// cx.assert_state("ˇaa\nbb\ndd\ncc\nbb\n", Mode::Normal);
// cx.simulate_keystrokes(["/", "b"]);
// deterministic.run_until_parked();
// cx.simulate_keystrokes(["enter"]);
// cx.assert_state("aa\nˇbb\ndd\ncc\nbb\n", Mode::Normal);
// }
// #[gpui::test]
// async fn test_non_vim_search(
// cx: &mut gpui::TestAppContext,
// deterministic: Arc<gpui::executor::Deterministic>,
// ) {
// let mut cx = VimTestContext::new(cx, false).await;
// cx.set_state("ˇone one one one", Mode::Normal);
// cx.simulate_keystrokes(["cmd-f"]);
// deterministic.run_until_parked();
// cx.assert_editor_state("«oneˇ» one one one");
// cx.simulate_keystrokes(["enter"]);
// cx.assert_editor_state("one «oneˇ» one one");
// cx.simulate_keystrokes(["shift-enter"]);
// cx.assert_editor_state("«oneˇ» one one one");
// }
// }

View file

@ -0,0 +1,277 @@
use editor::movement;
use gpui::{actions, AppContext, WindowContext};
use language::Point;
use workspace::Workspace;
use crate::{motion::Motion, utils::copy_selections_content, Mode, Vim};
actions!(Substitute, SubstituteLine);
pub(crate) fn init(cx: &mut AppContext) {
// todo!()
// cx.add_action(|_: &mut Workspace, _: &Substitute, cx| {
// Vim::update(cx, |vim, cx| {
// vim.start_recording(cx);
// let count = vim.take_count(cx);
// substitute(vim, count, vim.state().mode == Mode::VisualLine, cx);
// })
// });
// 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)
// }
// let count = vim.take_count(cx);
// substitute(vim, count, true, cx)
// })
// });
}
pub fn substitute(vim: &mut Vim, count: Option<usize>, line_mode: bool, cx: &mut WindowContext) {
vim.update_active_editor(cx, |editor, cx| {
editor.set_clip_at_line_ends(false, cx);
editor.transact(cx, |editor, cx| {
let text_layout_details = editor.text_layout_details(cx);
editor.change_selections(None, cx, |s| {
s.move_with(|map, selection| {
if selection.start == selection.end {
Motion::Right.expand_selection(
map,
selection,
count,
true,
&text_layout_details,
);
}
if line_mode {
// in Visual mode when the selection contains the newline at the end
// of the line, we should exclude it.
if !selection.is_empty() && selection.end.column() == 0 {
selection.end = movement::left(map, selection.end);
}
Motion::CurrentLine.expand_selection(
map,
selection,
None,
false,
&text_layout_details,
);
if let Some((point, _)) = (Motion::FirstNonWhitespace {
display_lines: false,
})
.move_point(
map,
selection.start,
selection.goal,
None,
&text_layout_details,
) {
selection.start = point;
}
}
})
});
copy_selections_content(editor, line_mode, cx);
let selections = editor.selections.all::<Point>(cx).into_iter();
let edits = selections.map(|selection| (selection.start..selection.end, ""));
editor.edit(edits, cx);
});
});
vim.switch_mode(Mode::Insert, true, cx);
}
// #[cfg(test)]
// mod test {
// use crate::{
// state::Mode,
// test::{NeovimBackedTestContext, VimTestContext},
// };
// use indoc::indoc;
// #[gpui::test]
// async fn test_substitute(cx: &mut gpui::TestAppContext) {
// let mut cx = VimTestContext::new(cx, true).await;
// // supports a single cursor
// cx.set_state(indoc! {"ˇabc\n"}, Mode::Normal);
// cx.simulate_keystrokes(["s", "x"]);
// cx.assert_editor_state("xˇbc\n");
// // supports a selection
// cx.set_state(indoc! {"a«bcˇ»\n"}, Mode::Visual);
// cx.assert_editor_state("a«bcˇ»\n");
// cx.simulate_keystrokes(["s", "x"]);
// cx.assert_editor_state("axˇ\n");
// // supports counts
// cx.set_state(indoc! {"ˇabc\n"}, Mode::Normal);
// cx.simulate_keystrokes(["2", "s", "x"]);
// cx.assert_editor_state("xˇc\n");
// // supports multiple cursors
// cx.set_state(indoc! {"a«bcˇ»deˇffg\n"}, Mode::Normal);
// cx.simulate_keystrokes(["2", "s", "x"]);
// cx.assert_editor_state("axˇdexˇg\n");
// // does not read beyond end of line
// cx.set_state(indoc! {"ˇabc\n"}, Mode::Normal);
// cx.simulate_keystrokes(["5", "s", "x"]);
// cx.assert_editor_state("xˇ\n");
// // it handles multibyte characters
// cx.set_state(indoc! {"ˇcàfé\n"}, Mode::Normal);
// cx.simulate_keystrokes(["4", "s"]);
// cx.assert_editor_state("ˇ\n");
// // should transactionally undo selection changes
// cx.simulate_keystrokes(["escape", "u"]);
// cx.assert_editor_state("ˇcàfé\n");
// // it handles visual line mode
// cx.set_state(
// indoc! {"
// alpha
// beˇta
// gamma"},
// Mode::Normal,
// );
// cx.simulate_keystrokes(["shift-v", "s"]);
// cx.assert_editor_state(indoc! {"
// alpha
// ˇ
// gamma"});
// }
// #[gpui::test]
// async fn test_visual_change(cx: &mut gpui::TestAppContext) {
// let mut cx = NeovimBackedTestContext::new(cx).await;
// cx.set_shared_state("The quick ˇbrown").await;
// cx.simulate_shared_keystrokes(["v", "w", "c"]).await;
// cx.assert_shared_state("The quick ˇ").await;
// cx.set_shared_state(indoc! {"
// The ˇquick brown
// fox jumps over
// the lazy dog"})
// .await;
// cx.simulate_shared_keystrokes(["v", "w", "j", "c"]).await;
// cx.assert_shared_state(indoc! {"
// The ˇver
// the lazy dog"})
// .await;
// let cases = cx.each_marked_position(indoc! {"
// The ˇquick brown
// fox jumps ˇover
// the ˇlazy dog"});
// for initial_state in cases {
// cx.assert_neovim_compatible(&initial_state, ["v", "w", "j", "c"])
// .await;
// cx.assert_neovim_compatible(&initial_state, ["v", "w", "k", "c"])
// .await;
// }
// }
// #[gpui::test]
// async fn test_visual_line_change(cx: &mut gpui::TestAppContext) {
// let mut cx = NeovimBackedTestContext::new(cx)
// .await
// .binding(["shift-v", "c"]);
// cx.assert(indoc! {"
// The quˇick brown
// fox jumps over
// the lazy dog"})
// .await;
// // Test pasting code copied on change
// cx.simulate_shared_keystrokes(["escape", "j", "p"]).await;
// cx.assert_state_matches().await;
// cx.assert_all(indoc! {"
// The quick brown
// fox juˇmps over
// the laˇzy dog"})
// .await;
// let mut cx = cx.binding(["shift-v", "j", "c"]);
// cx.assert(indoc! {"
// The quˇick brown
// fox jumps over
// the lazy dog"})
// .await;
// // Test pasting code copied on delete
// cx.simulate_shared_keystrokes(["escape", "j", "p"]).await;
// cx.assert_state_matches().await;
// cx.assert_all(indoc! {"
// The quick brown
// fox juˇmps over
// the laˇzy dog"})
// .await;
// }
// #[gpui::test]
// async fn test_substitute_line(cx: &mut gpui::TestAppContext) {
// let mut cx = NeovimBackedTestContext::new(cx).await;
// let initial_state = indoc! {"
// The quick brown
// fox juˇmps over
// the lazy dog
// "};
// // normal mode
// cx.set_shared_state(initial_state).await;
// cx.simulate_shared_keystrokes(["shift-s", "o"]).await;
// cx.assert_shared_state(indoc! {"
// The quick brown
// oˇ
// the lazy dog
// "})
// .await;
// // visual mode
// cx.set_shared_state(initial_state).await;
// cx.simulate_shared_keystrokes(["v", "k", "shift-s", "o"])
// .await;
// cx.assert_shared_state(indoc! {"
// oˇ
// the lazy dog
// "})
// .await;
// // visual block mode
// cx.set_shared_state(initial_state).await;
// cx.simulate_shared_keystrokes(["ctrl-v", "j", "shift-s", "o"])
// .await;
// cx.assert_shared_state(indoc! {"
// The quick brown
// oˇ
// "})
// .await;
// // visual mode including newline
// cx.set_shared_state(initial_state).await;
// cx.simulate_shared_keystrokes(["v", "$", "shift-s", "o"])
// .await;
// cx.assert_shared_state(indoc! {"
// The quick brown
// oˇ
// the lazy dog
// "})
// .await;
// // indentation
// cx.set_neovim_option("shiftwidth=4").await;
// cx.set_shared_state(initial_state).await;
// cx.simulate_shared_keystrokes([">", ">", "shift-s", "o"])
// .await;
// cx.assert_shared_state(indoc! {"
// The quick brown
// oˇ
// the lazy dog
// "})
// .await;
// }
// }

View file

@ -0,0 +1,50 @@
use crate::{motion::Motion, object::Object, utils::copy_selections_content, Vim};
use collections::HashMap;
use gpui::WindowContext;
pub fn yank_motion(vim: &mut Vim, motion: Motion, times: Option<usize>, cx: &mut WindowContext) {
vim.update_active_editor(cx, |editor, cx| {
let text_layout_details = editor.text_layout_details(cx);
editor.transact(cx, |editor, cx| {
editor.set_clip_at_line_ends(false, cx);
let mut original_positions: HashMap<_, _> = Default::default();
editor.change_selections(None, cx, |s| {
s.move_with(|map, selection| {
let original_position = (selection.head(), selection.goal);
original_positions.insert(selection.id, original_position);
motion.expand_selection(map, selection, times, true, &text_layout_details);
});
});
copy_selections_content(editor, motion.linewise(), cx);
editor.change_selections(None, cx, |s| {
s.move_with(|_, selection| {
let (head, goal) = original_positions.remove(&selection.id).unwrap();
selection.collapse_to(head, goal);
});
});
});
});
}
pub fn yank_object(vim: &mut Vim, object: Object, around: bool, cx: &mut WindowContext) {
vim.update_active_editor(cx, |editor, cx| {
editor.transact(cx, |editor, cx| {
editor.set_clip_at_line_ends(false, cx);
let mut original_positions: HashMap<_, _> = Default::default();
editor.change_selections(None, cx, |s| {
s.move_with(|map, selection| {
let original_position = (selection.head(), selection.goal);
object.expand_selection(map, selection, around);
original_positions.insert(selection.id, original_position);
});
});
copy_selections_content(editor, false, cx);
editor.change_selections(None, cx, |s| {
s.move_with(|_, selection| {
let (head, goal) = original_positions.remove(&selection.id).unwrap();
selection.collapse_to(head, goal);
});
});
});
});
}