773 lines · 26.3 KB · Rust
RawDownload
use std::{ops::Range, sync::OnceLock, time::Duration}; use gpui::{actions, impl_actions, ViewContext};use language::Point;use regex::Regex;use search::{buffer_search, BufferSearchBar, SearchOptions};use serde_derive::Deserialize;use workspace::{searchable::Direction, Workspace}; use crate::{    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 query: String,} #[derive(Debug, Default)]struct Replacement {    search: String,    replacement: String,    should_replace_all: bool,    is_case_sensitive: bool,    range: Option<Range<usize>>,} actions!(vim, [SearchSubmit, MoveToNextMatch, MoveToPrevMatch]);impl_actions!(    vim,    [FindCommand, ReplaceCommand, Search, MoveToPrev, MoveToNext]); static RANGE_REGEX: OnceLock<Regex> = OnceLock::new();pub(crate) fn range_regex() -> &'static Regex {    RANGE_REGEX.get_or_init(|| Regex::new(r"^(\d+),(\d+)s(.*)").unwrap())} 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| {        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);                        let _ = 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 = parse_replace_all(&action.query);    let pane = workspace.active_pane().clone();    let mut editor = Vim::read(cx)        .active_editor        .as_ref()        .and_then(|editor| editor.upgrade());    if let Some(range) = &replacement.range {        if let Some(editor) = editor.as_mut() {            editor.update(cx, |editor, cx| {                let snapshot = &editor.snapshot(cx).buffer_snapshot;                let range = snapshot                    .anchor_before(Point::new(range.start.saturating_sub(1) as u32, 0))                    ..snapshot.anchor_before(Point::new(range.end as u32, 0));                editor.set_search_within_ranges(&[range], 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);    })} // 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();    let mut range = None;    let maybe_line_range_and_rest: Option<(Range<usize>, &str)> =        range_regex().captures(query).map(|captures| {            (                captures.get(1).unwrap().as_str().parse().unwrap()                    ..captures.get(2).unwrap().as_str().parse().unwrap(),                captures.get(3).unwrap().as_str(),            )        });    if maybe_line_range_and_rest.is_some() {        let (line_range, rest) = maybe_line_range_and_rest.unwrap();        range = Some(line_range);        chars = rest.chars();    } else if Some('%') != chars.next() || Some('s') != chars.next() {        return Replacement::default();    }     let Some(delimiter) = 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 != 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)        }    }

Lines 1–500 of 773.