search.rs

   1use editor::{Editor, EditorSettings};
   2use gpui::{Action, Context, Window, actions};
   3use language::Point;
   4use schemars::JsonSchema;
   5use search::{BufferSearchBar, SearchOptions, buffer_search};
   6use serde::Deserialize;
   7use settings::Settings;
   8use std::{iter::Peekable, str::Chars};
   9use util::serde::default_true;
  10use workspace::{notifications::NotifyResultExt, searchable::Direction};
  11
  12use crate::{
  13    Vim, VimSettings,
  14    command::CommandRange,
  15    motion::Motion,
  16    state::{Mode, SearchState},
  17};
  18
  19/// Moves to the next search match.
  20#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Action)]
  21#[action(namespace = vim)]
  22#[serde(deny_unknown_fields)]
  23pub(crate) struct MoveToNext {
  24    #[serde(default = "default_true")]
  25    case_sensitive: bool,
  26    #[serde(default)]
  27    partial_word: bool,
  28    #[serde(default = "default_true")]
  29    regex: bool,
  30}
  31
  32/// Moves to the previous search match.
  33#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Action)]
  34#[action(namespace = vim)]
  35#[serde(deny_unknown_fields)]
  36pub(crate) struct MoveToPrevious {
  37    #[serde(default = "default_true")]
  38    case_sensitive: bool,
  39    #[serde(default)]
  40    partial_word: bool,
  41    #[serde(default = "default_true")]
  42    regex: bool,
  43}
  44
  45/// Searches for the word under the cursor without moving.
  46#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Action)]
  47#[action(namespace = vim)]
  48#[serde(deny_unknown_fields)]
  49pub(crate) struct SearchUnderCursor {
  50    #[serde(default = "default_true")]
  51    case_sensitive: bool,
  52    #[serde(default)]
  53    partial_word: bool,
  54    #[serde(default = "default_true")]
  55    regex: bool,
  56}
  57
  58/// Searches for the word under the cursor without moving (backwards).
  59#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Action)]
  60#[action(namespace = vim)]
  61#[serde(deny_unknown_fields)]
  62pub(crate) struct SearchUnderCursorPrevious {
  63    #[serde(default = "default_true")]
  64    case_sensitive: bool,
  65    #[serde(default)]
  66    partial_word: bool,
  67    #[serde(default = "default_true")]
  68    regex: bool,
  69}
  70
  71/// Initiates a search operation with the specified parameters.
  72#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Action)]
  73#[action(namespace = vim)]
  74#[serde(deny_unknown_fields)]
  75pub(crate) struct Search {
  76    #[serde(default)]
  77    backwards: bool,
  78    #[serde(default = "default_true")]
  79    regex: bool,
  80}
  81
  82/// Executes a find command to search for patterns in the buffer.
  83#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Action)]
  84#[action(namespace = vim)]
  85#[serde(deny_unknown_fields)]
  86pub struct FindCommand {
  87    pub query: String,
  88    pub backwards: bool,
  89}
  90
  91/// Executes a search and replace command within the specified range.
  92#[derive(Clone, Debug, PartialEq, Action)]
  93#[action(namespace = vim, no_json, no_register)]
  94pub struct ReplaceCommand {
  95    pub(crate) range: CommandRange,
  96    pub(crate) replacement: Replacement,
  97}
  98
  99#[derive(Clone, Debug, PartialEq)]
 100pub struct Replacement {
 101    search: String,
 102    replacement: String,
 103    case_sensitive: Option<bool>,
 104    flag_n: bool,
 105    flag_g: bool,
 106    flag_c: bool,
 107}
 108
 109actions!(
 110    vim,
 111    [
 112        /// Submits the current search query.
 113        SearchSubmit,
 114        /// Moves to the next search match.
 115        MoveToNextMatch,
 116        /// Moves to the previous search match.
 117        MoveToPreviousMatch
 118    ]
 119);
 120
 121pub(crate) fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
 122    Vim::action(editor, cx, Vim::move_to_next);
 123    Vim::action(editor, cx, Vim::move_to_previous);
 124    Vim::action(editor, cx, Vim::search_under_cursor);
 125    Vim::action(editor, cx, Vim::search_under_cursor_previous);
 126    Vim::action(editor, cx, Vim::move_to_next_match);
 127    Vim::action(editor, cx, Vim::move_to_previous_match);
 128    Vim::action(editor, cx, Vim::search);
 129    Vim::action(editor, cx, Vim::search_deploy);
 130    Vim::action(editor, cx, Vim::find_command);
 131    Vim::action(editor, cx, Vim::replace_command);
 132}
 133
 134impl Vim {
 135    fn search_under_cursor(
 136        &mut self,
 137        action: &SearchUnderCursor,
 138        window: &mut Window,
 139        cx: &mut Context<Self>,
 140    ) {
 141        self.move_to_internal(
 142            Direction::Next,
 143            action.case_sensitive,
 144            !action.partial_word,
 145            action.regex,
 146            false,
 147            window,
 148            cx,
 149        )
 150    }
 151
 152    fn search_under_cursor_previous(
 153        &mut self,
 154        action: &SearchUnderCursorPrevious,
 155        window: &mut Window,
 156        cx: &mut Context<Self>,
 157    ) {
 158        self.move_to_internal(
 159            Direction::Prev,
 160            action.case_sensitive,
 161            !action.partial_word,
 162            action.regex,
 163            false,
 164            window,
 165            cx,
 166        )
 167    }
 168
 169    fn move_to_next(&mut self, action: &MoveToNext, window: &mut Window, cx: &mut Context<Self>) {
 170        self.move_to_internal(
 171            Direction::Next,
 172            action.case_sensitive,
 173            !action.partial_word,
 174            action.regex,
 175            true,
 176            window,
 177            cx,
 178        )
 179    }
 180
 181    fn move_to_previous(
 182        &mut self,
 183        action: &MoveToPrevious,
 184        window: &mut Window,
 185        cx: &mut Context<Self>,
 186    ) {
 187        self.move_to_internal(
 188            Direction::Prev,
 189            action.case_sensitive,
 190            !action.partial_word,
 191            action.regex,
 192            true,
 193            window,
 194            cx,
 195        )
 196    }
 197
 198    fn move_to_next_match(
 199        &mut self,
 200        _: &MoveToNextMatch,
 201        window: &mut Window,
 202        cx: &mut Context<Self>,
 203    ) {
 204        self.move_to_match_internal(self.search.direction, window, cx)
 205    }
 206
 207    fn move_to_previous_match(
 208        &mut self,
 209        _: &MoveToPreviousMatch,
 210        window: &mut Window,
 211        cx: &mut Context<Self>,
 212    ) {
 213        self.move_to_match_internal(self.search.direction.opposite(), window, cx)
 214    }
 215
 216    fn search(&mut self, action: &Search, window: &mut Window, cx: &mut Context<Self>) {
 217        let Some(pane) = self.pane(window, cx) else {
 218            return;
 219        };
 220        let direction = if action.backwards {
 221            Direction::Prev
 222        } else {
 223            Direction::Next
 224        };
 225        let count = Vim::take_count(cx).unwrap_or(1);
 226        Vim::take_forced_motion(cx);
 227        let prior_selections = self.editor_selections(window, cx);
 228
 229        let Some(search_bar) = pane
 230            .read(cx)
 231            .toolbar()
 232            .read(cx)
 233            .item_of_type::<BufferSearchBar>()
 234        else {
 235            return;
 236        };
 237
 238        let shown = search_bar.update(cx, |search_bar, cx| {
 239            if !search_bar.show(window, cx) {
 240                return false;
 241            }
 242
 243            search_bar.select_query(window, cx);
 244            cx.focus_self(window);
 245
 246            search_bar.set_replacement(None, cx);
 247            let mut options = SearchOptions::NONE;
 248            if action.regex && VimSettings::get_global(cx).use_regex_search {
 249                options |= SearchOptions::REGEX;
 250            }
 251            if action.backwards {
 252                options |= SearchOptions::BACKWARDS;
 253            }
 254            if EditorSettings::get_global(cx).search.case_sensitive {
 255                options |= SearchOptions::CASE_SENSITIVE;
 256            }
 257            search_bar.set_search_options(options, cx);
 258            true
 259        });
 260
 261        if !shown {
 262            return;
 263        }
 264
 265        let subscription = cx.subscribe_in(&search_bar, window, |vim, _, event, window, cx| {
 266            if let buffer_search::Event::Dismissed = event {
 267                if !vim.search.prior_selections.is_empty() {
 268                    let prior_selections: Vec<_> = vim.search.prior_selections.drain(..).collect();
 269                    vim.update_editor(cx, |_, editor, cx| {
 270                        editor.change_selections(Default::default(), window, cx, |s| {
 271                            s.select_ranges(prior_selections);
 272                        });
 273                    });
 274                }
 275            }
 276        });
 277
 278        let prior_mode = if self.temp_mode {
 279            Mode::Insert
 280        } else {
 281            self.mode
 282        };
 283
 284        self.search = SearchState {
 285            direction,
 286            count,
 287            cmd_f_search: false,
 288            prior_selections,
 289            prior_operator: self.operator_stack.last().cloned(),
 290            prior_mode,
 291            helix_select: false,
 292            _dismiss_subscription: Some(subscription),
 293        }
 294    }
 295
 296    // hook into the existing to clear out any vim search state on cmd+f or edit -> find.
 297    fn search_deploy(&mut self, _: &buffer_search::Deploy, _: &mut Window, cx: &mut Context<Self>) {
 298        // Preserve the current mode when resetting search state
 299        let current_mode = self.mode;
 300        self.search = Default::default();
 301        self.search.prior_mode = current_mode;
 302        self.search.cmd_f_search = true;
 303        cx.propagate();
 304    }
 305
 306    pub fn search_submit(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 307        self.store_visual_marks(window, cx);
 308        let Some(pane) = self.pane(window, cx) else {
 309            return;
 310        };
 311        let new_selections = self.editor_selections(window, cx);
 312        let result = pane.update(cx, |pane, cx| {
 313            let search_bar = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>()?;
 314            if self.search.helix_select {
 315                search_bar.update(cx, |search_bar, cx| {
 316                    search_bar.select_all_matches(&Default::default(), window, cx)
 317                });
 318                return None;
 319            }
 320            search_bar.update(cx, |search_bar, cx| {
 321                let mut count = self.search.count;
 322                let direction = self.search.direction;
 323                search_bar.has_active_match();
 324                let new_head = new_selections.last()?.start;
 325                let is_different_head = self
 326                    .search
 327                    .prior_selections
 328                    .last()
 329                    .is_none_or(|range| range.start != new_head);
 330
 331                if is_different_head {
 332                    count = count.saturating_sub(1)
 333                }
 334                self.search.count = 1;
 335                search_bar.select_match(direction, count, window, cx);
 336                search_bar.focus_editor(&Default::default(), window, cx);
 337
 338                let prior_selections: Vec<_> = self.search.prior_selections.drain(..).collect();
 339                let prior_mode = self.search.prior_mode;
 340                let prior_operator = self.search.prior_operator.take();
 341
 342                let query = search_bar.query(cx).into();
 343                Vim::globals(cx).registers.insert('/', query);
 344                Some((prior_selections, prior_mode, prior_operator))
 345            })
 346        });
 347
 348        let Some((mut prior_selections, prior_mode, prior_operator)) = result else {
 349            return;
 350        };
 351
 352        let new_selections = self.editor_selections(window, cx);
 353
 354        // If the active editor has changed during a search, don't panic.
 355        if prior_selections.iter().any(|s| {
 356            self.update_editor(cx, |_, editor, cx| {
 357                !s.start
 358                    .is_valid(&editor.snapshot(window, cx).buffer_snapshot())
 359            })
 360            .unwrap_or(true)
 361        }) {
 362            prior_selections.clear();
 363        }
 364
 365        if prior_mode != self.mode {
 366            self.switch_mode(prior_mode, true, window, cx);
 367        }
 368        if let Some(operator) = prior_operator {
 369            self.push_operator(operator, window, cx);
 370        };
 371        self.search_motion(
 372            Motion::ZedSearchResult {
 373                prior_selections,
 374                new_selections,
 375            },
 376            window,
 377            cx,
 378        );
 379    }
 380
 381    pub fn move_to_match_internal(
 382        &mut self,
 383        direction: Direction,
 384        window: &mut Window,
 385        cx: &mut Context<Self>,
 386    ) {
 387        let Some(pane) = self.pane(window, cx) else {
 388            return;
 389        };
 390        let count = Vim::take_count(cx).unwrap_or(1);
 391        Vim::take_forced_motion(cx);
 392        let prior_selections = self.editor_selections(window, cx);
 393
 394        let success = pane.update(cx, |pane, cx| {
 395            let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() else {
 396                return false;
 397            };
 398            search_bar.update(cx, |search_bar, cx| {
 399                if !search_bar.has_active_match() || !search_bar.show(window, cx) {
 400                    return false;
 401                }
 402                search_bar.select_match(direction, count, window, cx);
 403                true
 404            })
 405        });
 406        if !success {
 407            return;
 408        }
 409
 410        let new_selections = self.editor_selections(window, cx);
 411        self.search_motion(
 412            Motion::ZedSearchResult {
 413                prior_selections,
 414                new_selections,
 415            },
 416            window,
 417            cx,
 418        );
 419    }
 420
 421    pub fn move_to_internal(
 422        &mut self,
 423        direction: Direction,
 424        case_sensitive: bool,
 425        whole_word: bool,
 426        regex: bool,
 427        move_cursor: bool,
 428        window: &mut Window,
 429        cx: &mut Context<Self>,
 430    ) {
 431        let Some(pane) = self.pane(window, cx) else {
 432            return;
 433        };
 434        let count = Vim::take_count(cx).unwrap_or(1);
 435        Vim::take_forced_motion(cx);
 436        let prior_selections = self.editor_selections(window, cx);
 437        let vim = cx.entity();
 438
 439        let searched = pane.update(cx, |pane, cx| {
 440            self.search.direction = direction;
 441            let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() else {
 442                return false;
 443            };
 444            let search = search_bar.update(cx, |search_bar, cx| {
 445                let mut options = SearchOptions::NONE;
 446                if case_sensitive {
 447                    options |= SearchOptions::CASE_SENSITIVE;
 448                }
 449                if regex {
 450                    options |= SearchOptions::REGEX;
 451                }
 452                if whole_word {
 453                    options |= SearchOptions::WHOLE_WORD;
 454                }
 455                if !search_bar.show(window, cx) {
 456                    return None;
 457                }
 458                let Some(query) = search_bar.query_suggestion(true, window, cx) else {
 459                    drop(search_bar.search("", None, false, window, cx));
 460                    return None;
 461                };
 462
 463                let query = regex::escape(&query);
 464                Some(search_bar.search(&query, Some(options), true, window, cx))
 465            });
 466
 467            let Some(search) = search else { return false };
 468
 469            if move_cursor {
 470                let search_bar = search_bar.downgrade();
 471                cx.spawn_in(window, async move |_, cx| {
 472                    search.await?;
 473                    search_bar.update_in(cx, |search_bar, window, cx| {
 474                        search_bar.select_match(direction, count, window, cx);
 475
 476                        vim.update(cx, |vim, cx| {
 477                            let new_selections = vim.editor_selections(window, cx);
 478                            vim.search_motion(
 479                                Motion::ZedSearchResult {
 480                                    prior_selections,
 481                                    new_selections,
 482                                },
 483                                window,
 484                                cx,
 485                            )
 486                        });
 487                    })?;
 488                    anyhow::Ok(())
 489                })
 490                .detach_and_log_err(cx);
 491            }
 492            true
 493        });
 494        if !searched {
 495            self.clear_operator(window, cx)
 496        }
 497
 498        if self.mode.is_visual() {
 499            self.switch_mode(Mode::Normal, false, window, cx)
 500        }
 501    }
 502
 503    fn find_command(&mut self, action: &FindCommand, window: &mut Window, cx: &mut Context<Self>) {
 504        let Some(pane) = self.pane(window, cx) else {
 505            return;
 506        };
 507        pane.update(cx, |pane, cx| {
 508            if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
 509                let search = search_bar.update(cx, |search_bar, cx| {
 510                    if !search_bar.show(window, cx) {
 511                        return None;
 512                    }
 513                    let mut query = action.query.clone();
 514                    if query.is_empty() {
 515                        query = search_bar.query(cx);
 516                    };
 517
 518                    let mut options = SearchOptions::REGEX | SearchOptions::CASE_SENSITIVE;
 519                    if search_bar.should_use_smartcase_search(cx) {
 520                        options.set(
 521                            SearchOptions::CASE_SENSITIVE,
 522                            search_bar.is_contains_uppercase(&query),
 523                        );
 524                    }
 525
 526                    Some(search_bar.search(&query, Some(options), true, window, cx))
 527                });
 528                let Some(search) = search else { return };
 529                let search_bar = search_bar.downgrade();
 530                let direction = if action.backwards {
 531                    Direction::Prev
 532                } else {
 533                    Direction::Next
 534                };
 535                cx.spawn_in(window, async move |_, cx| {
 536                    search.await?;
 537                    search_bar.update_in(cx, |search_bar, window, cx| {
 538                        search_bar.select_match(direction, 1, window, cx)
 539                    })?;
 540                    anyhow::Ok(())
 541                })
 542                .detach_and_log_err(cx);
 543            }
 544        })
 545    }
 546
 547    fn replace_command(
 548        &mut self,
 549        action: &ReplaceCommand,
 550        window: &mut Window,
 551        cx: &mut Context<Self>,
 552    ) {
 553        let replacement = action.replacement.clone();
 554        let Some(((pane, workspace), editor)) = self
 555            .pane(window, cx)
 556            .zip(self.workspace(window, cx))
 557            .zip(self.editor())
 558        else {
 559            return;
 560        };
 561        if let Some(result) = self.update_editor(cx, |vim, editor, cx| {
 562            let range = action.range.buffer_range(vim, editor, window, cx)?;
 563            let snapshot = editor.snapshot(window, cx);
 564            let snapshot = snapshot.buffer_snapshot();
 565            let end_point = Point::new(range.end.0, snapshot.line_len(range.end));
 566            let range = snapshot.anchor_before(Point::new(range.start.0, 0))
 567                ..snapshot.anchor_after(end_point);
 568            editor.set_search_within_ranges(&[range], cx);
 569            anyhow::Ok(())
 570        }) {
 571            workspace.update(cx, |workspace, cx| {
 572                result.notify_err(workspace, cx);
 573            })
 574        }
 575        let Some(search_bar) = pane.update(cx, |pane, cx| {
 576            pane.toolbar().read(cx).item_of_type::<BufferSearchBar>()
 577        }) else {
 578            return;
 579        };
 580        let mut options = SearchOptions::REGEX;
 581        let search = search_bar.update(cx, |search_bar, cx| {
 582            if !search_bar.show(window, cx) {
 583                return None;
 584            }
 585
 586            let search = if replacement.search.is_empty() {
 587                search_bar.query(cx)
 588            } else {
 589                replacement.search
 590            };
 591
 592            if let Some(case) = replacement.case_sensitive {
 593                options.set(SearchOptions::CASE_SENSITIVE, case)
 594            } else if search_bar.should_use_smartcase_search(cx) {
 595                options.set(
 596                    SearchOptions::CASE_SENSITIVE,
 597                    search_bar.is_contains_uppercase(&search),
 598                );
 599            } else {
 600                // Fallback: no explicit i/I flags and smartcase disabled;
 601                // use global editor.search.case_sensitive.
 602                options.set(
 603                    SearchOptions::CASE_SENSITIVE,
 604                    EditorSettings::get_global(cx).search.case_sensitive,
 605                )
 606            }
 607
 608            // gdefault inverts the behavior of the 'g' flag.
 609            let replace_all = VimSettings::get_global(cx).gdefault != replacement.flag_g;
 610            if !replace_all {
 611                options.set(SearchOptions::ONE_MATCH_PER_LINE, true);
 612            }
 613
 614            search_bar.set_replacement(Some(&replacement.replacement), cx);
 615            if replacement.flag_c {
 616                search_bar.focus_replace(window, cx);
 617            }
 618            Some(search_bar.search(&search, Some(options), true, window, cx))
 619        });
 620        if replacement.flag_n {
 621            self.move_cursor(
 622                Motion::StartOfLine {
 623                    display_lines: false,
 624                },
 625                None,
 626                window,
 627                cx,
 628            );
 629            return;
 630        }
 631        let Some(search) = search else { return };
 632        let search_bar = search_bar.downgrade();
 633        cx.spawn_in(window, async move |vim, cx| {
 634            search.await?;
 635            search_bar.update_in(cx, |search_bar, window, cx| {
 636                if replacement.flag_c {
 637                    search_bar.select_first_match(window, cx);
 638                    return;
 639                }
 640                search_bar.select_last_match(window, cx);
 641                search_bar.replace_all(&Default::default(), window, cx);
 642                editor.update(cx, |editor, cx| editor.clear_search_within_ranges(cx));
 643                let _ = search_bar.search(&search_bar.query(cx), None, false, window, cx);
 644                vim.update(cx, |vim, cx| {
 645                    vim.move_cursor(
 646                        Motion::StartOfLine {
 647                            display_lines: false,
 648                        },
 649                        None,
 650                        window,
 651                        cx,
 652                    )
 653                })
 654                .ok();
 655
 656                // Disable the `ONE_MATCH_PER_LINE` search option when finished, as
 657                // this is not properly supported outside of vim mode, and
 658                // not disabling it makes the "Replace All Matches" button
 659                // actually replace only the first match on each line.
 660                options.set(SearchOptions::ONE_MATCH_PER_LINE, false);
 661                search_bar.set_search_options(options, cx);
 662            })
 663        })
 664        .detach_and_log_err(cx);
 665    }
 666}
 667
 668impl Replacement {
 669    // convert a vim query into something more usable by zed.
 670    // we don't attempt to fully convert between the two regex syntaxes,
 671    // but we do flip \( and \) to ( and ) (and vice-versa) in the pattern,
 672    // and convert \0..\9 to $0..$9 in the replacement so that common idioms work.
 673    pub(crate) fn parse(mut chars: Peekable<Chars>) -> Option<Replacement> {
 674        let delimiter = chars
 675            .next()
 676            .filter(|c| !c.is_alphanumeric() && *c != '"' && *c != '|' && *c != '\'')?;
 677
 678        let mut search = String::new();
 679        let mut replacement = String::new();
 680        let mut flags = String::new();
 681
 682        let mut buffer = &mut search;
 683
 684        let mut escaped = false;
 685        // 0 - parsing search
 686        // 1 - parsing replacement
 687        // 2 - parsing flags
 688        let mut phase = 0;
 689
 690        for c in chars {
 691            if escaped {
 692                escaped = false;
 693                if phase == 1 && c.is_ascii_digit() {
 694                    buffer.push('$')
 695                // unescape escaped parens
 696                } else if phase == 0 && (c == '(' || c == ')') {
 697                } else if c != delimiter {
 698                    buffer.push('\\')
 699                }
 700                buffer.push(c)
 701            } else if c == '\\' {
 702                escaped = true;
 703            } else if c == delimiter {
 704                if phase == 0 {
 705                    buffer = &mut replacement;
 706                    phase = 1;
 707                } else if phase == 1 {
 708                    buffer = &mut flags;
 709                    phase = 2;
 710                } else {
 711                    break;
 712                }
 713            } else {
 714                // escape unescaped parens
 715                if phase == 0 && (c == '(' || c == ')') {
 716                    buffer.push('\\')
 717                }
 718                buffer.push(c)
 719            }
 720        }
 721
 722        let mut replacement = Replacement {
 723            search,
 724            replacement,
 725            case_sensitive: None,
 726            flag_g: false,
 727            flag_n: false,
 728            flag_c: false,
 729        };
 730
 731        for c in flags.chars() {
 732            match c {
 733                'g' => replacement.flag_g = !replacement.flag_g,
 734                'n' => replacement.flag_n = true,
 735                'c' => replacement.flag_c = true,
 736                'i' => replacement.case_sensitive = Some(false),
 737                'I' => replacement.case_sensitive = Some(true),
 738                _ => {}
 739            }
 740        }
 741
 742        Some(replacement)
 743    }
 744}
 745
 746#[cfg(test)]
 747mod test {
 748    use std::time::Duration;
 749
 750    use crate::{
 751        state::Mode,
 752        test::{NeovimBackedTestContext, VimTestContext},
 753    };
 754    use editor::{DisplayPoint, display_map::DisplayRow};
 755
 756    use indoc::indoc;
 757    use search::BufferSearchBar;
 758    use settings::SettingsStore;
 759
 760    #[gpui::test]
 761    async fn test_move_to_next(cx: &mut gpui::TestAppContext) {
 762        let mut cx = VimTestContext::new(cx, true).await;
 763        cx.set_state("ˇhi\nhigh\nhi\n", Mode::Normal);
 764
 765        cx.simulate_keystrokes("*");
 766        cx.run_until_parked();
 767        cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
 768
 769        cx.simulate_keystrokes("*");
 770        cx.run_until_parked();
 771        cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
 772
 773        cx.simulate_keystrokes("#");
 774        cx.run_until_parked();
 775        cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
 776
 777        cx.simulate_keystrokes("#");
 778        cx.run_until_parked();
 779        cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
 780
 781        cx.simulate_keystrokes("2 *");
 782        cx.run_until_parked();
 783        cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
 784
 785        cx.simulate_keystrokes("g *");
 786        cx.run_until_parked();
 787        cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
 788
 789        cx.simulate_keystrokes("n");
 790        cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
 791
 792        cx.simulate_keystrokes("g #");
 793        cx.run_until_parked();
 794        cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
 795    }
 796
 797    #[gpui::test]
 798    async fn test_move_to_next_with_no_search_wrap(cx: &mut gpui::TestAppContext) {
 799        let mut cx = VimTestContext::new(cx, true).await;
 800
 801        cx.update_global(|store: &mut SettingsStore, cx| {
 802            store.update_user_settings(cx, |s| s.editor.search_wrap = Some(false));
 803        });
 804
 805        cx.set_state("ˇhi\nhigh\nhi\n", Mode::Normal);
 806
 807        cx.simulate_keystrokes("*");
 808        cx.run_until_parked();
 809        cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
 810
 811        cx.simulate_keystrokes("*");
 812        cx.run_until_parked();
 813        cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
 814
 815        cx.simulate_keystrokes("#");
 816        cx.run_until_parked();
 817        cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
 818
 819        cx.simulate_keystrokes("3 *");
 820        cx.run_until_parked();
 821        cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
 822
 823        cx.simulate_keystrokes("g *");
 824        cx.run_until_parked();
 825        cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
 826
 827        cx.simulate_keystrokes("n");
 828        cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
 829
 830        cx.simulate_keystrokes("g #");
 831        cx.run_until_parked();
 832        cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
 833    }
 834
 835    #[gpui::test]
 836    async fn test_search(cx: &mut gpui::TestAppContext) {
 837        let mut cx = VimTestContext::new(cx, true).await;
 838
 839        cx.set_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal);
 840        cx.simulate_keystrokes("/ c c");
 841
 842        let search_bar = cx.workspace(|workspace, _, cx| {
 843            workspace
 844                .active_pane()
 845                .read(cx)
 846                .toolbar()
 847                .read(cx)
 848                .item_of_type::<BufferSearchBar>()
 849                .expect("Buffer search bar should be deployed")
 850        });
 851
 852        cx.update_entity(search_bar, |bar, _window, cx| {
 853            assert_eq!(bar.query(cx), "cc");
 854        });
 855
 856        cx.run_until_parked();
 857
 858        cx.update_editor(|editor, window, cx| {
 859            let highlights = editor.all_text_background_highlights(window, cx);
 860            assert_eq!(3, highlights.len());
 861            assert_eq!(
 862                DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 2),
 863                highlights[0].0
 864            )
 865        });
 866
 867        cx.simulate_keystrokes("enter");
 868        cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
 869
 870        // n to go to next/N to go to previous
 871        cx.simulate_keystrokes("n");
 872        cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal);
 873        cx.simulate_keystrokes("shift-n");
 874        cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
 875
 876        // ?<enter> to go to previous
 877        cx.simulate_keystrokes("? enter");
 878        cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal);
 879        cx.simulate_keystrokes("? enter");
 880        cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal);
 881
 882        // /<enter> to go to next
 883        cx.simulate_keystrokes("/ enter");
 884        cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal);
 885
 886        // ?{search}<enter> to search backwards
 887        cx.simulate_keystrokes("? b enter");
 888        cx.assert_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal);
 889
 890        // works with counts
 891        cx.simulate_keystrokes("4 / c");
 892        cx.simulate_keystrokes("enter");
 893        cx.assert_state("aa\nbb\ncc\ncˇc\ncc\n", Mode::Normal);
 894
 895        // check that searching resumes from cursor, not previous match
 896        cx.set_state("ˇaa\nbb\ndd\ncc\nbb\n", Mode::Normal);
 897        cx.simulate_keystrokes("/ d");
 898        cx.simulate_keystrokes("enter");
 899        cx.assert_state("aa\nbb\nˇdd\ncc\nbb\n", Mode::Normal);
 900        cx.update_editor(|editor, window, cx| {
 901            editor.move_to_beginning(&Default::default(), window, cx)
 902        });
 903        cx.assert_state("ˇaa\nbb\ndd\ncc\nbb\n", Mode::Normal);
 904        cx.simulate_keystrokes("/ b");
 905        cx.simulate_keystrokes("enter");
 906        cx.assert_state("aa\nˇbb\ndd\ncc\nbb\n", Mode::Normal);
 907
 908        // check that searching switches to normal mode if in visual mode
 909        cx.set_state("ˇone two one", Mode::Normal);
 910        cx.simulate_keystrokes("v l l");
 911        cx.assert_editor_state("«oneˇ» two one");
 912        cx.simulate_keystrokes("*");
 913        cx.assert_state("one two ˇone", Mode::Normal);
 914
 915        // check that a backward search after last match works correctly
 916        cx.set_state("aa\naa\nbbˇ", Mode::Normal);
 917        cx.simulate_keystrokes("? a a");
 918        cx.simulate_keystrokes("enter");
 919        cx.assert_state("aa\nˇaa\nbb", Mode::Normal);
 920
 921        // check that searching with unable search wrap
 922        cx.update_global(|store: &mut SettingsStore, cx| {
 923            store.update_user_settings(cx, |s| s.editor.search_wrap = Some(false));
 924        });
 925        cx.set_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal);
 926        cx.simulate_keystrokes("/ c c enter");
 927
 928        cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
 929
 930        // n to go to next/N to go to previous
 931        cx.simulate_keystrokes("n");
 932        cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal);
 933        cx.simulate_keystrokes("shift-n");
 934        cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
 935
 936        // ?<enter> to go to previous
 937        cx.simulate_keystrokes("? enter");
 938        cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
 939        cx.simulate_keystrokes("? enter");
 940        cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
 941    }
 942
 943    #[gpui::test]
 944    async fn test_non_vim_search(cx: &mut gpui::TestAppContext) {
 945        let mut cx = VimTestContext::new(cx, false).await;
 946        cx.cx.set_state("ˇone one one one");
 947        cx.run_until_parked();
 948        cx.simulate_keystrokes("cmd-f");
 949        cx.run_until_parked();
 950
 951        cx.assert_editor_state("«oneˇ» one one one");
 952        cx.simulate_keystrokes("enter");
 953        cx.assert_editor_state("one «oneˇ» one one");
 954        cx.simulate_keystrokes("shift-enter");
 955        cx.assert_editor_state("«oneˇ» one one one");
 956    }
 957
 958    #[gpui::test]
 959    async fn test_non_vim_search_in_vim_mode(cx: &mut gpui::TestAppContext) {
 960        let mut cx = VimTestContext::new(cx, true).await;
 961        cx.cx.set_state("ˇone one one one");
 962        cx.run_until_parked();
 963        cx.simulate_keystrokes("cmd-f");
 964        cx.run_until_parked();
 965
 966        cx.assert_state("«oneˇ» one one one", Mode::Visual);
 967        cx.simulate_keystrokes("enter");
 968        cx.run_until_parked();
 969        cx.assert_state("one «oneˇ» one one", Mode::Visual);
 970        cx.simulate_keystrokes("shift-enter");
 971        cx.run_until_parked();
 972        cx.assert_state("«oneˇ» one one one", Mode::Visual);
 973
 974        cx.simulate_keystrokes("escape");
 975        cx.run_until_parked();
 976        cx.assert_state("«oneˇ» one one one", Mode::Visual);
 977    }
 978
 979    #[gpui::test]
 980    async fn test_non_vim_search_in_vim_insert_mode(cx: &mut gpui::TestAppContext) {
 981        let mut cx = VimTestContext::new(cx, true).await;
 982        cx.set_state("ˇone one one one", Mode::Insert);
 983        cx.run_until_parked();
 984        cx.simulate_keystrokes("cmd-f");
 985        cx.run_until_parked();
 986
 987        cx.assert_state("«oneˇ» one one one", Mode::Insert);
 988        cx.simulate_keystrokes("enter");
 989        cx.run_until_parked();
 990        cx.assert_state("one «oneˇ» one one", Mode::Insert);
 991
 992        cx.simulate_keystrokes("escape");
 993        cx.run_until_parked();
 994        cx.assert_state("one «oneˇ» one one", Mode::Insert);
 995    }
 996
 997    #[gpui::test]
 998    async fn test_visual_star_hash(cx: &mut gpui::TestAppContext) {
 999        let mut cx = NeovimBackedTestContext::new(cx).await;
1000
1001        cx.set_shared_state("ˇa.c. abcd a.c. abcd").await;
1002        cx.simulate_shared_keystrokes("v 3 l *").await;
1003        cx.shared_state().await.assert_eq("a.c. abcd ˇa.c. abcd");
1004    }
1005
1006    #[gpui::test]
1007    async fn test_d_search(cx: &mut gpui::TestAppContext) {
1008        let mut cx = NeovimBackedTestContext::new(cx).await;
1009
1010        cx.set_shared_state("ˇa.c. abcd a.c. abcd").await;
1011        cx.simulate_shared_keystrokes("d / c d").await;
1012        cx.simulate_shared_keystrokes("enter").await;
1013        cx.shared_state().await.assert_eq("ˇcd a.c. abcd");
1014    }
1015
1016    #[gpui::test]
1017    async fn test_backwards_n(cx: &mut gpui::TestAppContext) {
1018        let mut cx = NeovimBackedTestContext::new(cx).await;
1019
1020        cx.set_shared_state("ˇa b a b a b a").await;
1021        cx.simulate_shared_keystrokes("*").await;
1022        cx.simulate_shared_keystrokes("n").await;
1023        cx.shared_state().await.assert_eq("a b a b ˇa b a");
1024        cx.simulate_shared_keystrokes("#").await;
1025        cx.shared_state().await.assert_eq("a b ˇa b a b a");
1026        cx.simulate_shared_keystrokes("n").await;
1027        cx.shared_state().await.assert_eq("ˇa b a b a b a");
1028    }
1029
1030    #[gpui::test]
1031    async fn test_v_search(cx: &mut gpui::TestAppContext) {
1032        let mut cx = NeovimBackedTestContext::new(cx).await;
1033
1034        cx.set_shared_state("ˇa.c. abcd a.c. abcd").await;
1035        cx.simulate_shared_keystrokes("v / c d").await;
1036        cx.simulate_shared_keystrokes("enter").await;
1037        cx.shared_state().await.assert_eq("«a.c. abcˇ»d a.c. abcd");
1038
1039        cx.set_shared_state("a a aˇ a a a").await;
1040        cx.simulate_shared_keystrokes("v / a").await;
1041        cx.simulate_shared_keystrokes("enter").await;
1042        cx.shared_state().await.assert_eq("a a a« aˇ» a a");
1043        cx.simulate_shared_keystrokes("/ enter").await;
1044        cx.shared_state().await.assert_eq("a a a« a aˇ» a");
1045        cx.simulate_shared_keystrokes("? enter").await;
1046        cx.shared_state().await.assert_eq("a a a« aˇ» a a");
1047        cx.simulate_shared_keystrokes("? enter").await;
1048        cx.shared_state().await.assert_eq("a a «ˇa »a a a");
1049        cx.simulate_shared_keystrokes("/ enter").await;
1050        cx.shared_state().await.assert_eq("a a a« aˇ» a a");
1051        cx.simulate_shared_keystrokes("/ enter").await;
1052        cx.shared_state().await.assert_eq("a a a« a aˇ» a");
1053    }
1054
1055    #[gpui::test]
1056    async fn test_v_search_aa(cx: &mut gpui::TestAppContext) {
1057        let mut cx = NeovimBackedTestContext::new(cx).await;
1058
1059        cx.set_shared_state("ˇaa aa").await;
1060        cx.simulate_shared_keystrokes("v / a a").await;
1061        cx.simulate_shared_keystrokes("enter").await;
1062        cx.shared_state().await.assert_eq("«aa aˇ»a");
1063    }
1064
1065    #[gpui::test]
1066    async fn test_visual_block_search(cx: &mut gpui::TestAppContext) {
1067        let mut cx = NeovimBackedTestContext::new(cx).await;
1068
1069        cx.set_shared_state(indoc! {
1070            "ˇone two
1071             three four
1072             five six
1073             "
1074        })
1075        .await;
1076        cx.simulate_shared_keystrokes("ctrl-v j / f").await;
1077        cx.simulate_shared_keystrokes("enter").await;
1078        cx.shared_state().await.assert_eq(indoc! {
1079            "«one twoˇ»
1080             «three fˇ»our
1081             five six
1082             "
1083        });
1084    }
1085
1086    #[gpui::test]
1087    async fn test_replace_with_range_at_start(cx: &mut gpui::TestAppContext) {
1088        let mut cx = NeovimBackedTestContext::new(cx).await;
1089
1090        cx.set_shared_state(indoc! {
1091            "ˇa
1092            a
1093            a
1094            a
1095            a
1096            a
1097            a
1098             "
1099        })
1100        .await;
1101        cx.simulate_shared_keystrokes(": 2 , 5 s / ^ / b").await;
1102        cx.simulate_shared_keystrokes("enter").await;
1103        cx.shared_state().await.assert_eq(indoc! {
1104            "a
1105            ba
1106            ba
1107            ba
1108            ˇba
1109            a
1110            a
1111             "
1112        });
1113
1114        cx.simulate_shared_keystrokes("/ a").await;
1115        cx.simulate_shared_keystrokes("enter").await;
1116        cx.shared_state().await.assert_eq(indoc! {
1117            "a
1118                ba
1119                ba
1120                ba
1121                bˇa
1122                a
1123                a
1124                 "
1125        });
1126    }
1127
1128    #[gpui::test]
1129    async fn test_search_skipping(cx: &mut gpui::TestAppContext) {
1130        let mut cx = NeovimBackedTestContext::new(cx).await;
1131        cx.set_shared_state(indoc! {
1132            "ˇaa aa aa"
1133        })
1134        .await;
1135
1136        cx.simulate_shared_keystrokes("/ a a").await;
1137        cx.simulate_shared_keystrokes("enter").await;
1138
1139        cx.shared_state().await.assert_eq(indoc! {
1140            "aa ˇaa aa"
1141        });
1142
1143        cx.simulate_shared_keystrokes("left / a a").await;
1144        cx.simulate_shared_keystrokes("enter").await;
1145
1146        cx.shared_state().await.assert_eq(indoc! {
1147            "aa ˇaa aa"
1148        });
1149    }
1150
1151    #[gpui::test]
1152    async fn test_replace_n(cx: &mut gpui::TestAppContext) {
1153        let mut cx = NeovimBackedTestContext::new(cx).await;
1154        cx.set_shared_state(indoc! {
1155            "ˇaa
1156            bb
1157            aa"
1158        })
1159        .await;
1160
1161        cx.simulate_shared_keystrokes(": s / b b / d d / n").await;
1162        cx.simulate_shared_keystrokes("enter").await;
1163
1164        cx.shared_state().await.assert_eq(indoc! {
1165            "ˇaa
1166            bb
1167            aa"
1168        });
1169
1170        let search_bar = cx.update_workspace(|workspace, _, cx| {
1171            workspace.active_pane().update(cx, |pane, cx| {
1172                pane.toolbar()
1173                    .read(cx)
1174                    .item_of_type::<BufferSearchBar>()
1175                    .unwrap()
1176            })
1177        });
1178        cx.update_entity(search_bar, |search_bar, _, cx| {
1179            assert!(!search_bar.is_dismissed());
1180            assert_eq!(search_bar.query(cx), "bb".to_string());
1181            assert_eq!(search_bar.replacement(cx), "dd".to_string());
1182        })
1183    }
1184
1185    #[gpui::test]
1186    async fn test_replace_g(cx: &mut gpui::TestAppContext) {
1187        let mut cx = NeovimBackedTestContext::new(cx).await;
1188        cx.set_shared_state(indoc! {
1189            "ˇaa aa aa aa
1190            aa
1191            aa"
1192        })
1193        .await;
1194
1195        cx.simulate_shared_keystrokes(": s / a a / b b").await;
1196        cx.simulate_shared_keystrokes("enter").await;
1197        cx.shared_state().await.assert_eq(indoc! {
1198            "ˇbb aa aa aa
1199            aa
1200            aa"
1201        });
1202        cx.simulate_shared_keystrokes(": s / a a / b b / g").await;
1203        cx.simulate_shared_keystrokes("enter").await;
1204        cx.shared_state().await.assert_eq(indoc! {
1205            "ˇbb bb bb bb
1206            aa
1207            aa"
1208        });
1209    }
1210
1211    #[gpui::test]
1212    async fn test_replace_gdefault(cx: &mut gpui::TestAppContext) {
1213        let mut cx = NeovimBackedTestContext::new(cx).await;
1214
1215        // Set the `gdefault` option in both Zed and Neovim.
1216        cx.simulate_shared_keystrokes(": s e t space g d e f a u l t")
1217            .await;
1218        cx.simulate_shared_keystrokes("enter").await;
1219
1220        cx.set_shared_state(indoc! {
1221            "ˇaa aa aa aa
1222                aa
1223                aa"
1224        })
1225        .await;
1226
1227        // With gdefault on, :s/// replaces all matches (like :s///g normally).
1228        cx.simulate_shared_keystrokes(": s / a a / b b").await;
1229        cx.simulate_shared_keystrokes("enter").await;
1230        cx.shared_state().await.assert_eq(indoc! {
1231            "ˇbb bb bb bb
1232                aa
1233                aa"
1234        });
1235
1236        // With gdefault on, :s///g replaces only the first match.
1237        cx.simulate_shared_keystrokes(": s / b b / c c / g").await;
1238        cx.simulate_shared_keystrokes("enter").await;
1239        cx.shared_state().await.assert_eq(indoc! {
1240            "ˇcc bb bb bb
1241                aa
1242                aa"
1243        });
1244
1245        // Each successive `/g` flag should invert the one before it.
1246        cx.simulate_shared_keystrokes(": s / b b / d d / g g").await;
1247        cx.simulate_shared_keystrokes("enter").await;
1248        cx.shared_state().await.assert_eq(indoc! {
1249            "ˇcc dd dd dd
1250                aa
1251                aa"
1252        });
1253
1254        cx.simulate_shared_keystrokes(": s / c c / e e / g g g")
1255            .await;
1256        cx.simulate_shared_keystrokes("enter").await;
1257        cx.shared_state().await.assert_eq(indoc! {
1258            "ˇee dd dd dd
1259                aa
1260                aa"
1261        });
1262    }
1263
1264    #[gpui::test]
1265    async fn test_replace_c(cx: &mut gpui::TestAppContext) {
1266        let mut cx = VimTestContext::new(cx, true).await;
1267        cx.set_state(
1268            indoc! {
1269                "ˇaa
1270            aa
1271            aa"
1272            },
1273            Mode::Normal,
1274        );
1275
1276        cx.simulate_keystrokes("v j : s / a a / d d / c");
1277        cx.simulate_keystrokes("enter");
1278
1279        cx.assert_state(
1280            indoc! {
1281                "ˇaa
1282            aa
1283            aa"
1284            },
1285            Mode::Normal,
1286        );
1287
1288        cx.simulate_keystrokes("enter");
1289
1290        cx.assert_state(
1291            indoc! {
1292                "dd
1293            ˇaa
1294            aa"
1295            },
1296            Mode::Normal,
1297        );
1298
1299        cx.simulate_keystrokes("enter");
1300        cx.assert_state(
1301            indoc! {
1302                "dd
1303            ddˇ
1304            aa"
1305            },
1306            Mode::Normal,
1307        );
1308        cx.simulate_keystrokes("enter");
1309        cx.assert_state(
1310            indoc! {
1311                "dd
1312            ddˇ
1313            aa"
1314            },
1315            Mode::Normal,
1316        );
1317    }
1318
1319    #[gpui::test]
1320    async fn test_replace_with_range(cx: &mut gpui::TestAppContext) {
1321        let mut cx = NeovimBackedTestContext::new(cx).await;
1322
1323        cx.set_shared_state(indoc! {
1324            "ˇa
1325            a
1326            a
1327            a
1328            a
1329            a
1330            a
1331             "
1332        })
1333        .await;
1334        cx.simulate_shared_keystrokes(": 2 , 5 s / a / b").await;
1335        cx.simulate_shared_keystrokes("enter").await;
1336        cx.shared_state().await.assert_eq(indoc! {
1337            "a
1338            b
1339            b
1340            b
1341            ˇb
1342            a
1343            a
1344             "
1345        });
1346        cx.executor().advance_clock(Duration::from_millis(250));
1347        cx.run_until_parked();
1348
1349        cx.simulate_shared_keystrokes("/ a enter").await;
1350        cx.shared_state().await.assert_eq(indoc! {
1351            "a
1352                b
1353                b
1354                b
1355                b
1356                ˇa
1357                a
1358                 "
1359        });
1360    }
1361
1362    #[gpui::test]
1363    async fn test_search_dismiss_restores_cursor(cx: &mut gpui::TestAppContext) {
1364        let mut cx = VimTestContext::new(cx, true).await;
1365        cx.set_state("ˇhello world\nfoo bar\nhello again\n", Mode::Normal);
1366
1367        // Move cursor to line 2
1368        cx.simulate_keystrokes("j");
1369        cx.run_until_parked();
1370        cx.assert_state("hello world\nˇfoo bar\nhello again\n", Mode::Normal);
1371
1372        // Open search
1373        cx.simulate_keystrokes("/");
1374        cx.run_until_parked();
1375
1376        // Dismiss search with Escape - cursor should return to line 2
1377        cx.simulate_keystrokes("escape");
1378        cx.run_until_parked();
1379        // Cursor should be restored to line 2 where it was when search was opened
1380        cx.assert_state("hello world\nˇfoo bar\nhello again\n", Mode::Normal);
1381    }
1382
1383    #[gpui::test]
1384    async fn test_search_dismiss_restores_cursor_no_matches(cx: &mut gpui::TestAppContext) {
1385        let mut cx = VimTestContext::new(cx, true).await;
1386        cx.set_state("ˇapple\nbanana\ncherry\n", Mode::Normal);
1387
1388        // Move cursor to line 2
1389        cx.simulate_keystrokes("j");
1390        cx.run_until_parked();
1391        cx.assert_state("apple\nˇbanana\ncherry\n", Mode::Normal);
1392
1393        // Open search and type query for something that doesn't exist
1394        cx.simulate_keystrokes("/ n o n e x i s t e n t");
1395        cx.run_until_parked();
1396
1397        // Dismiss search with Escape - cursor should still be at original position
1398        cx.simulate_keystrokes("escape");
1399        cx.run_until_parked();
1400        cx.assert_state("apple\nˇbanana\ncherry\n", Mode::Normal);
1401    }
1402
1403    #[gpui::test]
1404    async fn test_search_dismiss_after_editor_focus_does_not_restore(
1405        cx: &mut gpui::TestAppContext,
1406    ) {
1407        let mut cx = VimTestContext::new(cx, true).await;
1408        cx.set_state("ˇhello world\nfoo bar\nhello again\n", Mode::Normal);
1409
1410        // Move cursor to line 2
1411        cx.simulate_keystrokes("j");
1412        cx.run_until_parked();
1413        cx.assert_state("hello world\nˇfoo bar\nhello again\n", Mode::Normal);
1414
1415        // Open search and type a query that matches line 3
1416        cx.simulate_keystrokes("/ a g a i n");
1417        cx.run_until_parked();
1418
1419        // Simulate the editor gaining focus while search is still open
1420        // This represents the user clicking in the editor
1421        cx.update_editor(|_, window, cx| cx.focus_self(window));
1422        cx.run_until_parked();
1423
1424        // Now dismiss the search bar directly
1425        cx.workspace(|workspace, window, cx| {
1426            let pane = workspace.active_pane().read(cx);
1427            if let Some(search_bar) = pane
1428                .toolbar()
1429                .read(cx)
1430                .item_of_type::<search::BufferSearchBar>()
1431            {
1432                search_bar.update(cx, |bar, cx| {
1433                    bar.dismiss(&search::buffer_search::Dismiss, window, cx)
1434                });
1435            }
1436        });
1437        cx.run_until_parked();
1438
1439        // Cursor should NOT be restored to line 2 (row 1) where search was opened.
1440        // Since the user "clicked" in the editor (by focusing it), prior_selections
1441        // was cleared, so dismiss should not restore the cursor.
1442        // The cursor should be at the match location on line 3 (row 2).
1443        cx.assert_state("hello world\nfoo bar\nhello ˇagain\n", Mode::Normal);
1444    }
1445
1446    #[gpui::test]
1447    async fn test_vim_search_respects_search_settings(cx: &mut gpui::TestAppContext) {
1448        let mut cx = VimTestContext::new(cx, true).await;
1449
1450        cx.update_global(|store: &mut SettingsStore, cx| {
1451            store.update_user_settings(cx, |settings| {
1452                settings.vim.get_or_insert_default().use_regex_search = Some(false);
1453            });
1454        });
1455
1456        cx.set_state("ˇcontent", Mode::Normal);
1457        cx.simulate_keystrokes("/");
1458        cx.run_until_parked();
1459
1460        // Verify search options are set from settings
1461        let search_bar = cx.workspace(|workspace, _, cx| {
1462            workspace
1463                .active_pane()
1464                .read(cx)
1465                .toolbar()
1466                .read(cx)
1467                .item_of_type::<BufferSearchBar>()
1468                .expect("Buffer search bar should be active")
1469        });
1470
1471        cx.update_entity(search_bar, |bar, _window, _cx| {
1472            assert!(
1473                !bar.has_search_option(search::SearchOptions::REGEX),
1474                "Vim search open without regex mode"
1475            );
1476        });
1477
1478        cx.simulate_keystrokes("escape");
1479        cx.run_until_parked();
1480
1481        cx.update_global(|store: &mut SettingsStore, cx| {
1482            store.update_user_settings(cx, |settings| {
1483                settings.vim.get_or_insert_default().use_regex_search = Some(true);
1484            });
1485        });
1486
1487        cx.simulate_keystrokes("/");
1488        cx.run_until_parked();
1489
1490        let search_bar = cx.workspace(|workspace, _, cx| {
1491            workspace
1492                .active_pane()
1493                .read(cx)
1494                .toolbar()
1495                .read(cx)
1496                .item_of_type::<BufferSearchBar>()
1497                .expect("Buffer search bar should be active")
1498        });
1499
1500        cx.update_entity(search_bar, |bar, _window, _cx| {
1501            assert!(
1502                bar.has_search_option(search::SearchOptions::REGEX),
1503                "Vim search opens with regex mode"
1504            );
1505        });
1506    }
1507}