search.rs

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