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