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. Load more
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, _ => {}