Merge branch 'main' into helix-match-fix to fix the tests
This commit is contained in:
commit
b893f99d4e
174 changed files with 12635 additions and 1996 deletions
|
@ -28,8 +28,8 @@ use std::{
|
|||
use task::{HideStrategy, RevealStrategy, SpawnInTerminal, TaskId};
|
||||
use ui::ActiveTheme;
|
||||
use util::ResultExt;
|
||||
use workspace::notifications::DetachAndPromptErr;
|
||||
use workspace::{Item, SaveIntent, notifications::NotifyResultExt};
|
||||
use workspace::{SplitDirection, notifications::DetachAndPromptErr};
|
||||
use zed_actions::{OpenDocs, RevealTarget};
|
||||
|
||||
use crate::{
|
||||
|
@ -175,6 +175,13 @@ struct VimSave {
|
|||
}
|
||||
|
||||
/// Deletes the specified marks from the editor.
|
||||
#[derive(Clone, PartialEq, Action)]
|
||||
#[action(namespace = vim, no_json, no_register)]
|
||||
struct VimSplit {
|
||||
pub vertical: bool,
|
||||
pub filename: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Action)]
|
||||
#[action(namespace = vim, no_json, no_register)]
|
||||
enum DeleteMarks {
|
||||
|
@ -323,6 +330,33 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
|
|||
});
|
||||
});
|
||||
|
||||
Vim::action(editor, cx, |vim, action: &VimSplit, window, cx| {
|
||||
let Some(workspace) = vim.workspace(window) else {
|
||||
return;
|
||||
};
|
||||
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
let project = workspace.project().clone();
|
||||
let Some(worktree) = project.read(cx).visible_worktrees(cx).next() else {
|
||||
return;
|
||||
};
|
||||
let project_path = ProjectPath {
|
||||
worktree_id: worktree.read(cx).id(),
|
||||
path: Arc::from(Path::new(&action.filename)),
|
||||
};
|
||||
|
||||
let direction = if action.vertical {
|
||||
SplitDirection::vertical(cx)
|
||||
} else {
|
||||
SplitDirection::horizontal(cx)
|
||||
};
|
||||
|
||||
workspace
|
||||
.split_path_preview(project_path, false, Some(direction), window, cx)
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
});
|
||||
|
||||
Vim::action(editor, cx, |vim, action: &DeleteMarks, window, cx| {
|
||||
fn err(s: String, window: &mut Window, cx: &mut Context<Editor>) {
|
||||
let _ = window.prompt(
|
||||
|
@ -998,8 +1032,24 @@ fn generate_commands(_: &App) -> Vec<VimCommand> {
|
|||
save_intent: Some(SaveIntent::Overwrite),
|
||||
}),
|
||||
VimCommand::new(("cq", "uit"), zed_actions::Quit),
|
||||
VimCommand::new(("sp", "lit"), workspace::SplitHorizontal),
|
||||
VimCommand::new(("vs", "plit"), workspace::SplitVertical),
|
||||
VimCommand::new(("sp", "lit"), workspace::SplitHorizontal).args(|_, args| {
|
||||
Some(
|
||||
VimSplit {
|
||||
vertical: false,
|
||||
filename: args,
|
||||
}
|
||||
.boxed_clone(),
|
||||
)
|
||||
}),
|
||||
VimCommand::new(("vs", "plit"), workspace::SplitVertical).args(|_, args| {
|
||||
Some(
|
||||
VimSplit {
|
||||
vertical: true,
|
||||
filename: args,
|
||||
}
|
||||
.boxed_clone(),
|
||||
)
|
||||
}),
|
||||
VimCommand::new(
|
||||
("bd", "elete"),
|
||||
workspace::CloseActiveItem {
|
||||
|
|
|
@ -371,40 +371,40 @@ mod test {
|
|||
cx.assert_state("aa\n«ˇ »bb", Mode::HelixNormal);
|
||||
}
|
||||
|
||||
// #[gpui::test]
|
||||
// async fn test_delete(cx: &mut gpui::TestAppContext) {
|
||||
// let mut cx = VimTestContext::new(cx, true).await;
|
||||
#[gpui::test]
|
||||
async fn test_delete(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
// // test delete a selection
|
||||
// cx.set_state(
|
||||
// indoc! {"
|
||||
// The qu«ick ˇ»brown
|
||||
// fox jumps over
|
||||
// the lazy dog."},
|
||||
// Mode::HelixNormal,
|
||||
// );
|
||||
// test delete a selection
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
The qu«ick ˇ»brown
|
||||
fox jumps over
|
||||
the lazy dog."},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
|
||||
// cx.simulate_keystrokes("d");
|
||||
cx.simulate_keystrokes("d");
|
||||
|
||||
// cx.assert_state(
|
||||
// indoc! {"
|
||||
// The quˇbrown
|
||||
// fox jumps over
|
||||
// the lazy dog."},
|
||||
// Mode::HelixNormal,
|
||||
// );
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
The quˇbrown
|
||||
fox jumps over
|
||||
the lazy dog."},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
|
||||
// // test deleting a single character
|
||||
// cx.simulate_keystrokes("d");
|
||||
// test deleting a single character
|
||||
cx.simulate_keystrokes("d");
|
||||
|
||||
// cx.assert_state(
|
||||
// indoc! {"
|
||||
// The quˇrown
|
||||
// fox jumps over
|
||||
// the lazy dog."},
|
||||
// Mode::HelixNormal,
|
||||
// );
|
||||
// }
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
The quˇrown
|
||||
fox jumps over
|
||||
the lazy dog."},
|
||||
Mode::HelixNormal,
|
||||
);
|
||||
}
|
||||
|
||||
// #[gpui::test]
|
||||
// async fn test_delete_character_end_of_line(cx: &mut gpui::TestAppContext) {
|
||||
|
|
|
@ -24,9 +24,9 @@ use crate::{
|
|||
};
|
||||
use collections::BTreeSet;
|
||||
use convert::ConvertTarget;
|
||||
use editor::Bias;
|
||||
use editor::Editor;
|
||||
use editor::{Anchor, SelectionEffects};
|
||||
use editor::{Bias, ToPoint};
|
||||
use editor::{display_map::ToDisplayPoint, movement};
|
||||
use gpui::{Context, Window, actions};
|
||||
use language::{Point, SelectionGoal};
|
||||
|
@ -90,6 +90,8 @@ actions!(
|
|||
Undo,
|
||||
/// Redoes the last undone change.
|
||||
Redo,
|
||||
/// Undoes all changes to the most recently changed line.
|
||||
UndoLastLine,
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -138,6 +140,7 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
|
|||
})
|
||||
});
|
||||
vim.visual_delete(false, window, cx);
|
||||
vim.switch_mode(Mode::HelixNormal, true, window, cx);
|
||||
});
|
||||
|
||||
Vim::action(editor, cx, |vim, _: &ChangeToEndOfLine, window, cx| {
|
||||
|
@ -194,6 +197,120 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
|
|||
}
|
||||
});
|
||||
});
|
||||
Vim::action(editor, cx, |vim, _: &UndoLastLine, window, cx| {
|
||||
Vim::take_forced_motion(cx);
|
||||
vim.update_editor(window, cx, |vim, editor, window, cx| {
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
let Some(last_change) = editor.change_list.last_before_grouping() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let anchors = last_change.iter().cloned().collect::<Vec<_>>();
|
||||
let mut last_row = None;
|
||||
let ranges: Vec<_> = anchors
|
||||
.iter()
|
||||
.filter_map(|anchor| {
|
||||
let point = anchor.to_point(&snapshot);
|
||||
if last_row == Some(point.row) {
|
||||
return None;
|
||||
}
|
||||
last_row = Some(point.row);
|
||||
let line_range = Point::new(point.row, 0)
|
||||
..Point::new(point.row, snapshot.line_len(MultiBufferRow(point.row)));
|
||||
Some((
|
||||
snapshot.anchor_before(line_range.start)
|
||||
..snapshot.anchor_after(line_range.end),
|
||||
line_range,
|
||||
))
|
||||
})
|
||||
.collect();
|
||||
|
||||
let edits = editor.buffer().update(cx, |buffer, cx| {
|
||||
let current_content = ranges
|
||||
.iter()
|
||||
.map(|(anchors, _)| {
|
||||
buffer
|
||||
.snapshot(cx)
|
||||
.text_for_range(anchors.clone())
|
||||
.collect::<String>()
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let mut content_before_undo = current_content.clone();
|
||||
let mut undo_count = 0;
|
||||
|
||||
loop {
|
||||
let undone_tx = buffer.undo(cx);
|
||||
undo_count += 1;
|
||||
let mut content_after_undo = Vec::new();
|
||||
|
||||
let mut line_changed = false;
|
||||
for ((anchors, _), text_before_undo) in
|
||||
ranges.iter().zip(content_before_undo.iter())
|
||||
{
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
let text_after_undo =
|
||||
snapshot.text_for_range(anchors.clone()).collect::<String>();
|
||||
|
||||
if &text_after_undo != text_before_undo {
|
||||
line_changed = true;
|
||||
}
|
||||
content_after_undo.push(text_after_undo);
|
||||
}
|
||||
|
||||
content_before_undo = content_after_undo;
|
||||
if !line_changed {
|
||||
break;
|
||||
}
|
||||
if undone_tx == vim.undo_last_line_tx {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let edits = ranges
|
||||
.into_iter()
|
||||
.zip(content_before_undo.into_iter().zip(current_content))
|
||||
.filter_map(|((_, mut points), (mut old_text, new_text))| {
|
||||
if new_text == old_text {
|
||||
return None;
|
||||
}
|
||||
let common_suffix_starts_at = old_text
|
||||
.char_indices()
|
||||
.rev()
|
||||
.zip(new_text.chars().rev())
|
||||
.find_map(
|
||||
|((i, a), b)| {
|
||||
if a != b { Some(i + a.len_utf8()) } else { None }
|
||||
},
|
||||
)
|
||||
.unwrap_or(old_text.len());
|
||||
points.end.column -= (old_text.len() - common_suffix_starts_at) as u32;
|
||||
old_text = old_text.split_at(common_suffix_starts_at).0.to_string();
|
||||
let common_prefix_len = old_text
|
||||
.char_indices()
|
||||
.zip(new_text.chars())
|
||||
.find_map(|((i, a), b)| if a != b { Some(i) } else { None })
|
||||
.unwrap_or(0);
|
||||
points.start.column = common_prefix_len as u32;
|
||||
old_text = old_text.split_at(common_prefix_len).1.to_string();
|
||||
|
||||
Some((points, old_text))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for _ in 0..undo_count {
|
||||
buffer.redo(cx);
|
||||
}
|
||||
edits
|
||||
});
|
||||
vim.undo_last_line_tx = editor.transact(window, cx, |editor, window, cx| {
|
||||
editor.change_list.invert_last_group();
|
||||
editor.edit(edits, cx);
|
||||
editor.change_selections(SelectionEffects::default(), window, cx, |s| {
|
||||
s.select_anchor_ranges(anchors.into_iter().map(|a| a..a));
|
||||
})
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
repeat::register(editor, cx);
|
||||
scroll::register(editor, cx);
|
||||
|
@ -1877,4 +1994,102 @@ mod test {
|
|||
cx.simulate_shared_keystrokes("ctrl-o").await;
|
||||
cx.shared_state().await.assert_matches();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_undo_last_line(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state(indoc! {"
|
||||
ˇfn a() { }
|
||||
fn a() { }
|
||||
fn a() { }
|
||||
"})
|
||||
.await;
|
||||
// do a jump to reset vim's undo grouping
|
||||
cx.simulate_shared_keystrokes("shift-g").await;
|
||||
cx.shared_state().await.assert_matches();
|
||||
cx.simulate_shared_keystrokes("r a").await;
|
||||
cx.shared_state().await.assert_matches();
|
||||
cx.simulate_shared_keystrokes("shift-u").await;
|
||||
cx.shared_state().await.assert_matches();
|
||||
cx.simulate_shared_keystrokes("shift-u").await;
|
||||
cx.shared_state().await.assert_matches();
|
||||
cx.simulate_shared_keystrokes("g g shift-u").await;
|
||||
cx.shared_state().await.assert_matches();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_undo_last_line_newline(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state(indoc! {"
|
||||
ˇfn a() { }
|
||||
fn a() { }
|
||||
fn a() { }
|
||||
"})
|
||||
.await;
|
||||
// do a jump to reset vim's undo grouping
|
||||
cx.simulate_shared_keystrokes("shift-g k").await;
|
||||
cx.shared_state().await.assert_matches();
|
||||
cx.simulate_shared_keystrokes("o h e l l o escape").await;
|
||||
cx.shared_state().await.assert_matches();
|
||||
cx.simulate_shared_keystrokes("shift-u").await;
|
||||
cx.shared_state().await.assert_matches();
|
||||
cx.simulate_shared_keystrokes("shift-u").await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_undo_last_line_newline_many_changes(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_shared_state(indoc! {"
|
||||
ˇfn a() { }
|
||||
fn a() { }
|
||||
fn a() { }
|
||||
"})
|
||||
.await;
|
||||
// do a jump to reset vim's undo grouping
|
||||
cx.simulate_shared_keystrokes("x shift-g k").await;
|
||||
cx.shared_state().await.assert_matches();
|
||||
cx.simulate_shared_keystrokes("x f a x f { x").await;
|
||||
cx.shared_state().await.assert_matches();
|
||||
cx.simulate_shared_keystrokes("shift-u").await;
|
||||
cx.shared_state().await.assert_matches();
|
||||
cx.simulate_shared_keystrokes("shift-u").await;
|
||||
cx.shared_state().await.assert_matches();
|
||||
cx.simulate_shared_keystrokes("shift-u").await;
|
||||
cx.shared_state().await.assert_matches();
|
||||
cx.simulate_shared_keystrokes("shift-u").await;
|
||||
cx.shared_state().await.assert_matches();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_undo_last_line_multicursor(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
ˇone two ˇone
|
||||
two ˇone two
|
||||
"},
|
||||
Mode::Normal,
|
||||
);
|
||||
cx.simulate_keystrokes("3 r a");
|
||||
cx.assert_state(
|
||||
indoc! {"
|
||||
aaˇa two aaˇa
|
||||
two aaˇa two
|
||||
"},
|
||||
Mode::Normal,
|
||||
);
|
||||
cx.simulate_keystrokes("escape escape");
|
||||
cx.simulate_keystrokes("shift-u");
|
||||
cx.set_state(
|
||||
indoc! {"
|
||||
onˇe two onˇe
|
||||
two onˇe two
|
||||
"},
|
||||
Mode::Normal,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ use editor::{
|
|||
use gpui::{Context, Window, actions};
|
||||
use language::Bias;
|
||||
use settings::Settings;
|
||||
use text::SelectionGoal;
|
||||
|
||||
actions!(
|
||||
vim,
|
||||
|
@ -26,7 +27,11 @@ actions!(
|
|||
/// Scrolls up by one page.
|
||||
PageUp,
|
||||
/// Scrolls down by one page.
|
||||
PageDown
|
||||
PageDown,
|
||||
/// Scrolls right by half a page's width.
|
||||
HalfPageRight,
|
||||
/// Scrolls left by half a page's width.
|
||||
HalfPageLeft,
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -51,6 +56,16 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
|
|||
Vim::action(editor, cx, |vim, _: &PageUp, window, cx| {
|
||||
vim.scroll(false, window, cx, |c| ScrollAmount::Page(-c.unwrap_or(1.)))
|
||||
});
|
||||
Vim::action(editor, cx, |vim, _: &HalfPageRight, window, cx| {
|
||||
vim.scroll(false, window, cx, |c| {
|
||||
ScrollAmount::PageWidth(c.unwrap_or(0.5))
|
||||
})
|
||||
});
|
||||
Vim::action(editor, cx, |vim, _: &HalfPageLeft, window, cx| {
|
||||
vim.scroll(false, window, cx, |c| {
|
||||
ScrollAmount::PageWidth(-c.unwrap_or(0.5))
|
||||
})
|
||||
});
|
||||
Vim::action(editor, cx, |vim, _: &ScrollDown, window, cx| {
|
||||
vim.scroll(true, window, cx, |c| {
|
||||
if let Some(c) = c {
|
||||
|
@ -123,6 +138,10 @@ fn scroll_editor(
|
|||
return;
|
||||
};
|
||||
|
||||
let Some(visible_column_count) = editor.visible_column_count() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let top_anchor = editor.scroll_manager.anchor().anchor;
|
||||
let vertical_scroll_margin = EditorSettings::get_global(cx).vertical_scroll_margin;
|
||||
|
||||
|
@ -132,8 +151,14 @@ fn scroll_editor(
|
|||
cx,
|
||||
|s| {
|
||||
s.move_with(|map, selection| {
|
||||
// TODO: Improve the logic and function calls below to be dependent on
|
||||
// the `amount`. If the amount is vertical, we don't care about
|
||||
// columns, while if it's horizontal, we don't care about rows,
|
||||
// so we don't need to calculate both and deal with logic for
|
||||
// both.
|
||||
let mut head = selection.head();
|
||||
let top = top_anchor.to_display_point(map);
|
||||
let max_point = map.max_point();
|
||||
let starting_column = head.column();
|
||||
|
||||
let vertical_scroll_margin =
|
||||
|
@ -163,9 +188,8 @@ fn scroll_editor(
|
|||
(visible_line_count as u32).saturating_sub(1 + vertical_scroll_margin),
|
||||
);
|
||||
// scroll off the end.
|
||||
let max_row = if top.row().0 + visible_line_count as u32 >= map.max_point().row().0
|
||||
{
|
||||
map.max_point().row()
|
||||
let max_row = if top.row().0 + visible_line_count as u32 >= max_point.row().0 {
|
||||
max_point.row()
|
||||
} else {
|
||||
DisplayRow(
|
||||
(top.row().0 + visible_line_count as u32)
|
||||
|
@ -185,13 +209,52 @@ fn scroll_editor(
|
|||
} else {
|
||||
head.row()
|
||||
};
|
||||
let new_head =
|
||||
map.clip_point(DisplayPoint::new(new_row, starting_column), Bias::Left);
|
||||
|
||||
// The minimum column position that the cursor position can be
|
||||
// at is either the scroll manager's anchor column, which is the
|
||||
// left-most column in the visible area, or the scroll manager's
|
||||
// old anchor column, in case the cursor position is being
|
||||
// preserved. This is necessary for motions like `ctrl-d` in
|
||||
// case there's not enough content to scroll half page down, in
|
||||
// which case the scroll manager's anchor column will be the
|
||||
// maximum column for the current line, so the minimum column
|
||||
// would end up being the same as the maximum column.
|
||||
let min_column = match preserve_cursor_position {
|
||||
true => old_top_anchor.to_display_point(map).column(),
|
||||
false => top.column(),
|
||||
};
|
||||
|
||||
// As for the maximum column position, that should be either the
|
||||
// right-most column in the visible area, which we can easily
|
||||
// calculate by adding the visible column count to the minimum
|
||||
// column position, or the right-most column in the current
|
||||
// line, seeing as the cursor might be in a short line, in which
|
||||
// case we don't want to go past its last column.
|
||||
let max_row_column = map.line_len(new_row);
|
||||
let max_column = match min_column + visible_column_count as u32 {
|
||||
max_column if max_column >= max_row_column => max_row_column,
|
||||
max_column => max_column,
|
||||
};
|
||||
|
||||
// Ensure that the cursor's column stays within the visible
|
||||
// area, otherwise clip it at either the left or right edge of
|
||||
// the visible area.
|
||||
let new_column = match (min_column, max_column) {
|
||||
(min_column, _) if starting_column < min_column => min_column,
|
||||
(_, max_column) if starting_column > max_column => max_column,
|
||||
_ => starting_column,
|
||||
};
|
||||
|
||||
let new_head = map.clip_point(DisplayPoint::new(new_row, new_column), Bias::Left);
|
||||
let goal = match amount {
|
||||
ScrollAmount::Column(_) | ScrollAmount::PageWidth(_) => SelectionGoal::None,
|
||||
_ => selection.goal,
|
||||
};
|
||||
|
||||
if selection.is_empty() {
|
||||
selection.collapse_to(new_head, selection.goal)
|
||||
selection.collapse_to(new_head, goal)
|
||||
} else {
|
||||
selection.set_head(new_head, selection.goal)
|
||||
selection.set_head(new_head, goal)
|
||||
};
|
||||
})
|
||||
},
|
||||
|
@ -472,4 +535,30 @@ mod test {
|
|||
cx.simulate_shared_keystrokes("ctrl-o").await;
|
||||
cx.shared_state().await.assert_matches();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_horizontal_scroll(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
||||
cx.set_scroll_height(20).await;
|
||||
cx.set_shared_wrap(12).await;
|
||||
cx.set_neovim_option("nowrap").await;
|
||||
|
||||
let content = "ˇ01234567890123456789";
|
||||
cx.set_shared_state(&content).await;
|
||||
|
||||
cx.simulate_shared_keystrokes("z shift-l").await;
|
||||
cx.shared_state().await.assert_eq("012345ˇ67890123456789");
|
||||
|
||||
// At this point, `z h` should not move the cursor as it should still be
|
||||
// visible within the 12 column width.
|
||||
cx.simulate_shared_keystrokes("z h").await;
|
||||
cx.shared_state().await.assert_eq("012345ˇ67890123456789");
|
||||
|
||||
let content = "ˇ01234567890123456789";
|
||||
cx.set_shared_state(&content).await;
|
||||
|
||||
cx.simulate_shared_keystrokes("z l").await;
|
||||
cx.shared_state().await.assert_eq("0ˇ1234567890123456789");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -71,11 +71,13 @@ pub struct ReplaceCommand {
|
|||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub(crate) struct Replacement {
|
||||
pub struct Replacement {
|
||||
search: String,
|
||||
replacement: String,
|
||||
should_replace_all: bool,
|
||||
is_case_sensitive: bool,
|
||||
case_sensitive: Option<bool>,
|
||||
flag_n: bool,
|
||||
flag_g: bool,
|
||||
flag_c: bool,
|
||||
}
|
||||
|
||||
actions!(
|
||||
|
@ -468,71 +470,89 @@ impl Vim {
|
|||
result.notify_err(workspace, cx);
|
||||
})
|
||||
}
|
||||
let vim = cx.entity().clone();
|
||||
pane.update(cx, |pane, cx| {
|
||||
let mut options = SearchOptions::REGEX;
|
||||
let Some(search_bar) = pane.update(cx, |pane, cx| {
|
||||
pane.toolbar().read(cx).item_of_type::<BufferSearchBar>()
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
let mut options = SearchOptions::REGEX;
|
||||
let search = search_bar.update(cx, |search_bar, cx| {
|
||||
if !search_bar.show(window, cx) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() else {
|
||||
return;
|
||||
let search = if replacement.search.is_empty() {
|
||||
search_bar.query(cx)
|
||||
} else {
|
||||
replacement.search
|
||||
};
|
||||
let search = search_bar.update(cx, |search_bar, cx| {
|
||||
if !search_bar.show(window, cx) {
|
||||
return None;
|
||||
}
|
||||
|
||||
if replacement.is_case_sensitive {
|
||||
options.set(SearchOptions::CASE_SENSITIVE, true)
|
||||
}
|
||||
let search = if replacement.search.is_empty() {
|
||||
search_bar.query(cx)
|
||||
} else {
|
||||
replacement.search
|
||||
};
|
||||
if search_bar.should_use_smartcase_search(cx) {
|
||||
options.set(
|
||||
SearchOptions::CASE_SENSITIVE,
|
||||
search_bar.is_contains_uppercase(&search),
|
||||
);
|
||||
}
|
||||
if let Some(case) = replacement.case_sensitive {
|
||||
options.set(SearchOptions::CASE_SENSITIVE, case)
|
||||
} else if search_bar.should_use_smartcase_search(cx) {
|
||||
options.set(
|
||||
SearchOptions::CASE_SENSITIVE,
|
||||
search_bar.is_contains_uppercase(&search),
|
||||
);
|
||||
} else {
|
||||
options.set(SearchOptions::CASE_SENSITIVE, false)
|
||||
}
|
||||
|
||||
if !replacement.should_replace_all {
|
||||
options.set(SearchOptions::ONE_MATCH_PER_LINE, true);
|
||||
if !replacement.flag_g {
|
||||
options.set(SearchOptions::ONE_MATCH_PER_LINE, true);
|
||||
}
|
||||
|
||||
search_bar.set_replacement(Some(&replacement.replacement), cx);
|
||||
if replacement.flag_c {
|
||||
search_bar.focus_replace(window, cx);
|
||||
}
|
||||
Some(search_bar.search(&search, Some(options), window, cx))
|
||||
});
|
||||
if replacement.flag_n {
|
||||
self.move_cursor(
|
||||
Motion::StartOfLine {
|
||||
display_lines: false,
|
||||
},
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
return;
|
||||
}
|
||||
let Some(search) = search else { return };
|
||||
let search_bar = search_bar.downgrade();
|
||||
cx.spawn_in(window, async move |vim, cx| {
|
||||
search.await?;
|
||||
search_bar.update_in(cx, |search_bar, window, cx| {
|
||||
if replacement.flag_c {
|
||||
search_bar.select_first_match(window, cx);
|
||||
return;
|
||||
}
|
||||
search_bar.select_last_match(window, cx);
|
||||
search_bar.replace_all(&Default::default(), window, cx);
|
||||
editor.update(cx, |editor, cx| editor.clear_search_within_ranges(cx));
|
||||
let _ = search_bar.search(&search_bar.query(cx), None, window, cx);
|
||||
vim.update(cx, |vim, cx| {
|
||||
vim.move_cursor(
|
||||
Motion::StartOfLine {
|
||||
display_lines: false,
|
||||
},
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.ok();
|
||||
|
||||
search_bar.set_replacement(Some(&replacement.replacement), cx);
|
||||
Some(search_bar.search(&search, Some(options), window, cx))
|
||||
});
|
||||
let Some(search) = search else { return };
|
||||
let search_bar = search_bar.downgrade();
|
||||
cx.spawn_in(window, async move |_, cx| {
|
||||
search.await?;
|
||||
search_bar.update_in(cx, |search_bar, window, cx| {
|
||||
search_bar.select_last_match(window, cx);
|
||||
search_bar.replace_all(&Default::default(), window, cx);
|
||||
editor.update(cx, |editor, cx| editor.clear_search_within_ranges(cx));
|
||||
let _ = search_bar.search(&search_bar.query(cx), None, window, cx);
|
||||
vim.update(cx, |vim, cx| {
|
||||
vim.move_cursor(
|
||||
Motion::StartOfLine {
|
||||
display_lines: false,
|
||||
},
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
// Disable the `ONE_MATCH_PER_LINE` search option when finished, as
|
||||
// this is not properly supported outside of vim mode, and
|
||||
// not disabling it makes the "Replace All Matches" button
|
||||
// actually replace only the first match on each line.
|
||||
options.set(SearchOptions::ONE_MATCH_PER_LINE, false);
|
||||
search_bar.set_search_options(options, cx);
|
||||
})?;
|
||||
anyhow::Ok(())
|
||||
// Disable the `ONE_MATCH_PER_LINE` search option when finished, as
|
||||
// this is not properly supported outside of vim mode, and
|
||||
// not disabling it makes the "Replace All Matches" button
|
||||
// actually replace only the first match on each line.
|
||||
options.set(SearchOptions::ONE_MATCH_PER_LINE, false);
|
||||
search_bar.set_search_options(options, cx);
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -593,16 +613,19 @@ impl Replacement {
|
|||
let mut replacement = Replacement {
|
||||
search,
|
||||
replacement,
|
||||
should_replace_all: false,
|
||||
is_case_sensitive: true,
|
||||
case_sensitive: None,
|
||||
flag_g: false,
|
||||
flag_n: false,
|
||||
flag_c: false,
|
||||
};
|
||||
|
||||
for c in flags.chars() {
|
||||
match c {
|
||||
'g' => replacement.should_replace_all = true,
|
||||
'c' | 'n' => replacement.should_replace_all = false,
|
||||
'i' => replacement.is_case_sensitive = false,
|
||||
'I' => replacement.is_case_sensitive = true,
|
||||
'g' => replacement.flag_g = true,
|
||||
'n' => replacement.flag_n = true,
|
||||
'c' => replacement.flag_c = true,
|
||||
'i' => replacement.case_sensitive = Some(false),
|
||||
'I' => replacement.case_sensitive = Some(true),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
@ -913,7 +936,6 @@ mod test {
|
|||
});
|
||||
}
|
||||
|
||||
// cargo test -p vim --features neovim test_replace_with_range_at_start
|
||||
#[gpui::test]
|
||||
async fn test_replace_with_range_at_start(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
@ -979,6 +1001,121 @@ mod test {
|
|||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_replace_n(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.set_shared_state(indoc! {
|
||||
"ˇaa
|
||||
bb
|
||||
aa"
|
||||
})
|
||||
.await;
|
||||
|
||||
cx.simulate_shared_keystrokes(": s / b b / d d / n").await;
|
||||
cx.simulate_shared_keystrokes("enter").await;
|
||||
|
||||
cx.shared_state().await.assert_eq(indoc! {
|
||||
"ˇaa
|
||||
bb
|
||||
aa"
|
||||
});
|
||||
|
||||
let search_bar = cx.update_workspace(|workspace, _, cx| {
|
||||
workspace.active_pane().update(cx, |pane, cx| {
|
||||
pane.toolbar()
|
||||
.read(cx)
|
||||
.item_of_type::<BufferSearchBar>()
|
||||
.unwrap()
|
||||
})
|
||||
});
|
||||
cx.update_entity(search_bar, |search_bar, _, cx| {
|
||||
assert!(!search_bar.is_dismissed());
|
||||
assert_eq!(search_bar.query(cx), "bb".to_string());
|
||||
assert_eq!(search_bar.replacement(cx), "dd".to_string());
|
||||
})
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_replace_g(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
cx.set_shared_state(indoc! {
|
||||
"ˇaa aa aa aa
|
||||
aa
|
||||
aa"
|
||||
})
|
||||
.await;
|
||||
|
||||
cx.simulate_shared_keystrokes(": s / a a / b b").await;
|
||||
cx.simulate_shared_keystrokes("enter").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {
|
||||
"ˇbb aa aa aa
|
||||
aa
|
||||
aa"
|
||||
});
|
||||
cx.simulate_shared_keystrokes(": s / a a / b b / g").await;
|
||||
cx.simulate_shared_keystrokes("enter").await;
|
||||
cx.shared_state().await.assert_eq(indoc! {
|
||||
"ˇbb bb bb bb
|
||||
aa
|
||||
aa"
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_replace_c(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = VimTestContext::new(cx, true).await;
|
||||
cx.set_state(
|
||||
indoc! {
|
||||
"ˇaa
|
||||
aa
|
||||
aa"
|
||||
},
|
||||
Mode::Normal,
|
||||
);
|
||||
|
||||
cx.simulate_keystrokes("v j : s / a a / d d / c");
|
||||
cx.simulate_keystrokes("enter");
|
||||
|
||||
cx.assert_state(
|
||||
indoc! {
|
||||
"ˇaa
|
||||
aa
|
||||
aa"
|
||||
},
|
||||
Mode::Normal,
|
||||
);
|
||||
|
||||
cx.simulate_keystrokes("enter");
|
||||
|
||||
cx.assert_state(
|
||||
indoc! {
|
||||
"dd
|
||||
ˇaa
|
||||
aa"
|
||||
},
|
||||
Mode::Normal,
|
||||
);
|
||||
|
||||
cx.simulate_keystrokes("enter");
|
||||
cx.assert_state(
|
||||
indoc! {
|
||||
"dd
|
||||
ddˇ
|
||||
aa"
|
||||
},
|
||||
Mode::Normal,
|
||||
);
|
||||
cx.simulate_keystrokes("enter");
|
||||
cx.assert_state(
|
||||
indoc! {
|
||||
"dd
|
||||
ddˇ
|
||||
aa"
|
||||
},
|
||||
Mode::Normal,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_replace_with_range(cx: &mut gpui::TestAppContext) {
|
||||
let mut cx = NeovimBackedTestContext::new(cx).await;
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use editor::test::editor_lsp_test_context::EditorLspTestContext;
|
||||
use gpui::{Context, Entity, SemanticVersion, UpdateGlobal, actions};
|
||||
use gpui::{Context, Entity, SemanticVersion, UpdateGlobal};
|
||||
use search::{BufferSearchBar, project_search::ProjectSearchBar};
|
||||
|
||||
use crate::{state::Operator, *};
|
||||
|
||||
actions!(agent, [Chat]);
|
||||
|
||||
pub struct VimTestContext {
|
||||
cx: EditorLspTestContext,
|
||||
}
|
||||
|
|
|
@ -377,6 +377,7 @@ pub(crate) struct Vim {
|
|||
pub(crate) current_tx: Option<TransactionId>,
|
||||
pub(crate) current_anchor: Option<Selection<Anchor>>,
|
||||
pub(crate) undo_modes: HashMap<TransactionId, Mode>,
|
||||
pub(crate) undo_last_line_tx: Option<TransactionId>,
|
||||
|
||||
selected_register: Option<char>,
|
||||
pub search: SearchState,
|
||||
|
@ -424,6 +425,7 @@ impl Vim {
|
|||
|
||||
stored_visual_mode: None,
|
||||
current_tx: None,
|
||||
undo_last_line_tx: None,
|
||||
current_anchor: None,
|
||||
undo_modes: HashMap::default(),
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue