863 lines · 29.3 KB · Rust
RawDownload
use std::{iter::Peekable, str::Chars, time::Duration}; use gpui::{actions, impl_actions, ViewContext};use language::Point;use search::{buffer_search, BufferSearchBar, SearchOptions};use serde_derive::Deserialize;use workspace::{notifications::NotifyResultExt, searchable::Direction, Workspace}; use crate::{    command::CommandRange,    motion::{search_motion, Motion},    normal::move_cursor,    state::{Mode, 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(crate) range: Option<CommandRange>,    pub(crate) replacement: Replacement,} #[derive(Debug, Default, PartialEq, Deserialize, Clone)]pub(crate) struct Replacement {    search: String,    replacement: String,    should_replace_all: bool,    is_case_sensitive: bool,} actions!(vim, [SearchSubmit, MoveToNextMatch, MoveToPrevMatch]);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(move_to_next_match);    workspace.register_action(move_to_prev_match);    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 move_to_next_match(    workspace: &mut Workspace,    _: &MoveToNextMatch,    cx: &mut ViewContext<Workspace>,) {    move_to_match_internal(workspace, Direction::Next, cx)} fn move_to_prev_match(    workspace: &mut Workspace,    _: &MoveToPrevMatch,    cx: &mut ViewContext<Workspace>,) {    move_to_match_internal(workspace, Direction::Prev, 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);        let prior_selections = vim.editor_selections(cx);        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::REGEX, cx);                    }                    vim.update_state(|state| {                        state.search = SearchState {                            direction,                            count,                            initial_query: query.clone(),                            prior_selections,                            prior_operator: state.operator_stack.last().cloned(),                            prior_mode: state.mode,                        }                    });                });            }        })    })} // 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.update_state(|state| state.search = Default::default())    });    cx.propagate();} fn search_submit(workspace: &mut Workspace, _: &SearchSubmit, cx: &mut ViewContext<Workspace>) {    let mut motion = None;    Vim::update(cx, |vim, cx| {        vim.store_visual_marks(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 (mut prior_selections, prior_mode, prior_operator) =                        vim.update_state(|state| {                            let mut count = state.search.count;                            let direction = state.search.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.search.initial_query)                                && state.search.direction == Direction::Next                            {                                count = count.saturating_sub(1)                            }                            state.search.count = 1;                            search_bar.select_match(direction, count, cx);                            search_bar.focus_editor(&Default::default(), cx);                             let prior_selections: Vec<_> =                                state.search.prior_selections.drain(..).collect();                            let prior_mode = state.search.prior_mode;                            let prior_operator = state.search.prior_operator.take();                            (prior_selections, prior_mode, prior_operator)                        });                     vim.workspace_state                        .registers                        .insert('/', search_bar.query(cx).into());                     let new_selections = vim.editor_selections(cx);                     // If the active editor has changed during a search, don't panic.                    if prior_selections.iter().any(|s| {                        vim.update_active_editor(cx, |_vim, editor, cx| {                            !s.start.is_valid(&editor.snapshot(cx).buffer_snapshot)                        })                        .unwrap_or(true)                    }) {                        prior_selections.clear();                    }                     if prior_mode != vim.state().mode {                        vim.switch_mode(prior_mode, true, cx);                    }                    if let Some(operator) = prior_operator {                        vim.push_operator(operator, cx);                    };                    motion = Some(Motion::ZedSearchResult {                        prior_selections,                        new_selections,                    });                });            }        });    });     if let Some(motion) = motion {        search_motion(motion, cx)    }} pub fn move_to_match_internal(    workspace: &mut Workspace,    direction: Direction,    cx: &mut ViewContext<Workspace>,) {    let mut motion = None;    Vim::update(cx, |vim, cx| {        let pane = workspace.active_pane().clone();        let count = vim.take_count(cx).unwrap_or(1);        let prior_selections = vim.editor_selections(cx);         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.has_active_match() || !search_bar.show(cx) {                        return;                    }                    search_bar.select_match(direction, count, cx);                     let new_selections = vim.editor_selections(cx);                    motion = Some(Motion::ZedSearchResult {                        prior_selections,                        new_selections,                    });                })            }        })    });    if let Some(motion) = motion {        search_motion(motion, 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);        let prior_selections = vim.editor_selections(cx);         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 options = SearchOptions::CASE_SENSITIVE | SearchOptions::REGEX;                    if !search_bar.show(cx) {                        return None;                    }                    let Some(query) = search_bar.query_suggestion(cx) else {                        vim.clear_operator(cx);                        drop(search_bar.search("", None, cx));                        return None;                    };                    let mut query = regex::escape(&query);                    if whole_word {                        query = format!(r"\<{}\>", query);                    }                    Some(search_bar.search(&query, Some(options), cx))                });                 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);                             let new_selections =                                Vim::update(cx, |vim, cx| vim.editor_selections(cx));                            search_motion(                                Motion::ZedSearchResult {                                    prior_selections,                                    new_selections,                                },                                cx,                            )                        })?;                        anyhow::Ok(())                    })                    .detach_and_log_err(cx);                }            }        });         if vim.state().mode.is_visual() {            vim.switch_mode(Mode::Normal, false, 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);                };                 Some(search_bar.search(                    &query,                    Some(SearchOptions::CASE_SENSITIVE | SearchOptions::REGEX),                    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 = action.replacement.clone();    let pane = workspace.active_pane().clone();    let editor = Vim::read(cx)        .active_editor        .as_ref()        .and_then(|editor| editor.upgrade());    if let Some(range) = &action.range {        if let Some(result) = Vim::update(cx, |vim, cx| {            vim.update_active_editor(cx, |vim, editor, cx| {                let range = range.buffer_range(vim, editor, cx)?;                let snapshot = &editor.snapshot(cx).buffer_snapshot;                let end_point = Point::new(range.end.0, snapshot.line_len(range.end));                let range = snapshot.anchor_before(Point::new(range.start.0, 0))                    ..snapshot.anchor_after(end_point);                editor.set_search_within_ranges(&[range], cx);                anyhow::Ok(())            })        }) {            result.notify_err(workspace, cx);        }    }    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::REGEX;            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);            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);                    if let Some(editor) = editor {                        cx.spawn(|_, mut cx| async move {                            cx.background_executor()                                .timer(Duration::from_millis(200))                                .await;                            editor                                .update(&mut cx, |editor, cx| editor.clear_search_within_ranges(cx))                                .ok();                        })                        .detach();                    }                    Vim::update(cx, |vim, cx| {                        move_cursor(                            vim,                            Motion::StartOfLine {                                display_lines: false,                            },                            None,                            cx,                        )                    })                }            })?;            anyhow::Ok(())        })        .detach_and_log_err(cx);    })} impl Replacement {    // 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.    pub(crate) fn parse(mut chars: Peekable<Chars>) -> Option<Replacement> {        let Some(delimiter) = chars            .next()            .filter(|c| !c.is_alphanumeric() && *c != '"' && *c != '|' && *c != '\'')        else {            return None;        };         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 != delimiter {                    buffer.push('\\')                }                buffer.push(c)            } else if c == '\\' {                escaped = true;            } else if c == delimiter {                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,                _ => {}

Lines 1–500 of 863.