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