495 lines
17 KiB
Rust
495 lines
17 KiB
Rust
use gpui::{actions, impl_actions, ViewContext};
|
|
use search::{buffer_search, BufferSearchBar, SearchMode, SearchOptions};
|
|
use serde_derive::Deserialize;
|
|
use workspace::{searchable::Direction, Workspace};
|
|
|
|
use crate::{motion::Motion, normal::move_cursor, state::SearchState, Vim};
|
|
|
|
#[derive(Clone, Deserialize, PartialEq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub(crate) struct MoveToNext {
|
|
#[serde(default)]
|
|
partial_word: bool,
|
|
}
|
|
|
|
#[derive(Clone, Deserialize, PartialEq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub(crate) struct MoveToPrev {
|
|
#[serde(default)]
|
|
partial_word: bool,
|
|
}
|
|
|
|
#[derive(Clone, Deserialize, PartialEq)]
|
|
pub(crate) struct Search {
|
|
#[serde(default)]
|
|
backwards: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
|
pub struct FindCommand {
|
|
pub query: String,
|
|
pub backwards: bool,
|
|
}
|
|
|
|
#[derive(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!(vim, [SearchSubmit]);
|
|
impl_actions!(
|
|
vim,
|
|
[FindCommand, ReplaceCommand, Search, MoveToPrev, MoveToNext]
|
|
);
|
|
|
|
pub(crate) fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
|
|
workspace.register_action(move_to_next);
|
|
workspace.register_action(move_to_prev);
|
|
workspace.register_action(search);
|
|
workspace.register_action(search_submit);
|
|
workspace.register_action(search_deploy);
|
|
|
|
workspace.register_action(find_command);
|
|
workspace.register_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 Workspace, _: &buffer_search::Deploy, cx: &mut ViewContext<Workspace>) {
|
|
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");
|
|
// }
|
|
// }
|