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