ZIm/crates/vim2/src/normal/search.rs
2023-12-11 10:45:27 -07:00

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");
// }
// }