Flesh out v1.0 of vim :
This commit is contained in:
parent
6ad1f19a21
commit
2d9db0fed1
16 changed files with 516 additions and 82 deletions
|
@ -1,9 +1,9 @@
|
|||
use gpui::{actions, impl_actions, AppContext, ViewContext};
|
||||
use search::{buffer_search, BufferSearchBar, SearchMode, SearchOptions};
|
||||
use serde_derive::Deserialize;
|
||||
use workspace::{searchable::Direction, Pane, Workspace};
|
||||
use workspace::{searchable::Direction, Pane, Toast, Workspace};
|
||||
|
||||
use crate::{state::SearchState, Vim};
|
||||
use crate::{motion::Motion, normal::move_cursor, state::SearchState, Vim};
|
||||
|
||||
#[derive(Clone, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
@ -25,7 +25,29 @@ pub(crate) struct Search {
|
|||
backwards: bool,
|
||||
}
|
||||
|
||||
impl_actions!(vim, [MoveToNext, MoveToPrev, Search]);
|
||||
#[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)]
|
||||
struct Replacement {
|
||||
search: String,
|
||||
replacement: String,
|
||||
should_replace_all: bool,
|
||||
is_case_sensitive: bool,
|
||||
}
|
||||
|
||||
impl_actions!(
|
||||
vim,
|
||||
[MoveToNext, MoveToPrev, Search, FindCommand, ReplaceCommand]
|
||||
);
|
||||
actions!(vim, [SearchSubmit]);
|
||||
|
||||
pub(crate) fn init(cx: &mut AppContext) {
|
||||
|
@ -34,6 +56,9 @@ pub(crate) fn init(cx: &mut AppContext) {
|
|||
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>) {
|
||||
|
@ -65,6 +90,7 @@ fn search(workspace: &mut Workspace, action: &Search, cx: &mut ViewContext<Works
|
|||
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);
|
||||
}
|
||||
|
@ -151,6 +177,170 @@ pub fn move_to_internal(
|
|||
});
|
||||
}
|
||||
|
||||
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();
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
search.await?;
|
||||
search_bar.update(&mut cx, |search_bar, cx| {
|
||||
search_bar.select_match(Direction::Next, 1, cx)
|
||||
})?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn replace_command(
|
||||
workspace: &mut Workspace,
|
||||
action: &ReplaceCommand,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
let replacement = match parse_replace_all(&action.query) {
|
||||
Ok(replacement) => replacement,
|
||||
Err(message) => {
|
||||
cx.handle().update(cx, |workspace, cx| {
|
||||
workspace.show_toast(Toast::new(1544, message), cx)
|
||||
});
|
||||
return;
|
||||
}
|
||||
};
|
||||
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 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);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_replace_all(query: &str) -> Result<Replacement, String> {
|
||||
let mut chars = query.chars();
|
||||
if Some('%') != chars.next() || Some('s') != chars.next() {
|
||||
return Err("unsupported pattern".to_string());
|
||||
}
|
||||
|
||||
let Some(delimeter) = chars.next() else {
|
||||
return Err("unsupported pattern".to_string());
|
||||
};
|
||||
if delimeter == '\\' || !delimeter.is_ascii_punctuation() {
|
||||
return Err(format!("cannot use {:?} as a search delimeter", delimeter));
|
||||
}
|
||||
|
||||
let mut search = String::new();
|
||||
let mut replacement = String::new();
|
||||
let mut flags = String::new();
|
||||
|
||||
let mut buffer = &mut search;
|
||||
|
||||
let mut escaped = false;
|
||||
let mut phase = 0;
|
||||
|
||||
for c in chars {
|
||||
if escaped {
|
||||
escaped = false;
|
||||
if phase == 1 && c.is_digit(10) {
|
||||
// help vim users discover zed regex syntax
|
||||
// (though we don't try and fix arbitrary patterns for them)
|
||||
buffer.push('$')
|
||||
} else if phase == 0 && c == '(' || c == ')' {
|
||||
// un-escape parens
|
||||
} 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 {
|
||||
return Err("trailing characters".to_string());
|
||||
}
|
||||
} else {
|
||||
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' => {} // defaults,
|
||||
'c' | 'n' => replacement.should_replace_all = false,
|
||||
'i' => replacement.is_case_sensitive = false,
|
||||
_ => return Err(format!("unsupported flag {:?}", c)),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(replacement)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::sync::Arc;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue