search.rs

   1use editor::{Editor, EditorSettings, VimFlavor};
   2use gpui::{Action, Context, Window, actions};
   3
   4use language::Point;
   5use schemars::JsonSchema;
   6use search::{BufferSearchBar, SearchOptions, buffer_search};
   7use serde::Deserialize;
   8use settings::Settings;
   9use std::{iter::Peekable, str::Chars};
  10use util::serde::default_true;
  11use workspace::{notifications::NotifyResultExt, searchable::Direction};
  12
  13use crate::{
  14    Vim,
  15    command::CommandRange,
  16    motion::Motion,
  17    state::{Mode, SearchState},
  18};
  19
  20/// Moves to the next search match.
  21#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Action)]
  22#[action(namespace = vim)]
  23#[serde(deny_unknown_fields)]
  24pub(crate) struct MoveToNext {
  25    #[serde(default = "default_true")]
  26    case_sensitive: bool,
  27    #[serde(default)]
  28    partial_word: bool,
  29    #[serde(default = "default_true")]
  30    regex: bool,
  31}
  32
  33/// Moves to the previous search match.
  34#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Action)]
  35#[action(namespace = vim)]
  36#[serde(deny_unknown_fields)]
  37pub(crate) struct MoveToPrevious {
  38    #[serde(default = "default_true")]
  39    case_sensitive: bool,
  40    #[serde(default)]
  41    partial_word: bool,
  42    #[serde(default = "default_true")]
  43    regex: bool,
  44}
  45
  46/// Initiates a search operation with the specified parameters.
  47#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Action)]
  48#[action(namespace = vim)]
  49#[serde(deny_unknown_fields)]
  50pub(crate) struct Search {
  51    #[serde(default)]
  52    backwards: bool,
  53    #[serde(default = "default_true")]
  54    regex: bool,
  55}
  56
  57/// Executes a find command to search for patterns in the buffer.
  58#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Action)]
  59#[action(namespace = vim)]
  60#[serde(deny_unknown_fields)]
  61pub struct FindCommand {
  62    pub query: String,
  63    pub backwards: bool,
  64}
  65
  66/// Executes a search and replace command within the specified range.
  67#[derive(Clone, Debug, PartialEq, Action)]
  68#[action(namespace = vim, no_json, no_register)]
  69pub struct ReplaceCommand {
  70    pub(crate) range: CommandRange,
  71    pub(crate) replacement: Replacement,
  72}
  73
  74#[derive(Clone, Debug, PartialEq)]
  75pub struct Replacement {
  76    search: String,
  77    replacement: String,
  78    case_sensitive: Option<bool>,
  79    flag_n: bool,
  80    flag_g: bool,
  81    flag_c: bool,
  82}
  83
  84actions!(
  85    vim,
  86    [
  87        /// Submits the current search query.
  88        SearchSubmit,
  89        /// Moves to the next search match.
  90        MoveToNextMatch,
  91        /// Moves to the previous search match.
  92        MoveToPreviousMatch
  93    ]
  94);
  95
  96pub(crate) fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
  97    Vim::action(editor, cx, Vim::move_to_next);
  98    Vim::action(editor, cx, Vim::move_to_previous);
  99    Vim::action(editor, cx, Vim::move_to_next_match);
 100    Vim::action(editor, cx, Vim::move_to_previous_match);
 101    Vim::action(editor, cx, Vim::search);
 102    Vim::action(editor, cx, Vim::search_deploy);
 103    Vim::action(editor, cx, Vim::find_command);
 104    Vim::action(editor, cx, Vim::replace_command);
 105}
 106
 107impl Vim {
 108    fn move_to_next(&mut self, action: &MoveToNext, window: &mut Window, cx: &mut Context<Self>) {
 109        self.move_to_internal(
 110            Direction::Next,
 111            action.case_sensitive,
 112            !action.partial_word,
 113            action.regex,
 114            window,
 115            cx,
 116        )
 117    }
 118
 119    fn move_to_previous(
 120        &mut self,
 121        action: &MoveToPrevious,
 122        window: &mut Window,
 123        cx: &mut Context<Self>,
 124    ) {
 125        self.move_to_internal(
 126            Direction::Prev,
 127            action.case_sensitive,
 128            !action.partial_word,
 129            action.regex,
 130            window,
 131            cx,
 132        )
 133    }
 134
 135    fn move_to_next_match(
 136        &mut self,
 137        _: &MoveToNextMatch,
 138        window: &mut Window,
 139        cx: &mut Context<Self>,
 140    ) {
 141        self.move_to_match_internal(self.search.direction, window, cx)
 142    }
 143
 144    fn move_to_previous_match(
 145        &mut self,
 146        _: &MoveToPreviousMatch,
 147        window: &mut Window,
 148        cx: &mut Context<Self>,
 149    ) {
 150        self.move_to_match_internal(self.search.direction.opposite(), window, cx)
 151    }
 152
 153    fn search(&mut self, action: &Search, window: &mut Window, cx: &mut Context<Self>) {
 154        let Some(pane) = self.pane(window, cx) else {
 155            return;
 156        };
 157        let direction = if action.backwards {
 158            Direction::Prev
 159        } else {
 160            Direction::Next
 161        };
 162        let count = Vim::take_count(cx).unwrap_or(1);
 163        Vim::take_forced_motion(cx);
 164        let prior_selections = self.editor_selections(window, cx);
 165        pane.update(cx, |pane, cx| {
 166            if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
 167                search_bar.update(cx, |search_bar, cx| {
 168                    if !search_bar.show(window, cx) {
 169                        return;
 170                    }
 171
 172                    search_bar.select_query(window, cx);
 173                    cx.focus_self(window);
 174
 175                    search_bar.set_replacement(None, cx);
 176                    let mut options = SearchOptions::NONE;
 177                    if action.regex {
 178                        options |= SearchOptions::REGEX;
 179                    }
 180                    if action.backwards {
 181                        options |= SearchOptions::BACKWARDS;
 182                    }
 183                    if EditorSettings::get_global(cx).search.case_sensitive {
 184                        options |= SearchOptions::CASE_SENSITIVE;
 185                    }
 186                    search_bar.set_search_options(options, cx);
 187                    let prior_mode = if self.temp_mode {
 188                        Mode::Insert
 189                    } else {
 190                        self.mode
 191                    };
 192
 193                    self.search = SearchState {
 194                        direction,
 195                        count,
 196                        prior_selections,
 197                        prior_operator: self.operator_stack.last().cloned(),
 198                        prior_mode,
 199                        is_helix_regex_search: false,
 200                    }
 201                });
 202            }
 203        })
 204    }
 205
 206    // hook into the existing to clear out any vim search state on cmd+f or edit -> find.
 207    fn search_deploy(&mut self, _: &buffer_search::Deploy, _: &mut Window, cx: &mut Context<Self>) {
 208        // Preserve the current mode when resetting search state
 209        let current_mode = self.mode;
 210        self.search = Default::default();
 211        self.search.prior_mode = current_mode;
 212        cx.propagate();
 213    }
 214
 215    pub fn search_submit(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 216        self.store_visual_marks(window, cx);
 217        let Some(pane) = self.pane(window, cx) else {
 218            return;
 219        };
 220        let new_selections = self.editor_selections(window, cx);
 221        let result = pane.update(cx, |pane, cx| {
 222            let search_bar = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>()?;
 223            if self.search.is_helix_regex_search {
 224                search_bar.update(cx, |search_bar, cx| {
 225                    search_bar.select_all_matches(&Default::default(), window, cx)
 226                });
 227                return None;
 228            }
 229            search_bar.update(cx, |search_bar, cx| {
 230                let mut count = self.search.count;
 231                let direction = self.search.direction;
 232                search_bar.has_active_match();
 233                let new_head = new_selections.last()?.start;
 234                let is_different_head = self
 235                    .search
 236                    .prior_selections
 237                    .last()
 238                    .is_none_or(|range| range.start != new_head);
 239
 240                if is_different_head {
 241                    count = count.saturating_sub(1)
 242                }
 243                self.search.count = 1;
 244                let collapse = !self.mode.is_helix();
 245                search_bar.select_match(direction, count, collapse, window, cx);
 246                search_bar.focus_editor(&Default::default(), window, cx);
 247
 248                let prior_selections: Vec<_> = self.search.prior_selections.drain(..).collect();
 249                let prior_mode = self.search.prior_mode;
 250                let prior_operator = self.search.prior_operator.take();
 251
 252                let query = search_bar.query(cx).into();
 253                Vim::globals(cx).registers.insert('/', query);
 254                Some((prior_selections, prior_mode, prior_operator))
 255            })
 256        });
 257
 258        let Some((mut prior_selections, prior_mode, prior_operator)) = result else {
 259            return;
 260        };
 261
 262        let new_selections = self.editor_selections(window, cx);
 263
 264        // If the active editor has changed during a search, don't panic.
 265        if prior_selections.iter().any(|s| {
 266            self.update_editor(cx, |_, editor, cx| {
 267                !s.start
 268                    .is_valid(&editor.snapshot(window, cx).buffer_snapshot())
 269            })
 270            .unwrap_or(true)
 271        }) {
 272            prior_selections.clear();
 273        }
 274
 275        if prior_mode != self.mode {
 276            self.switch_mode(prior_mode, true, window, cx);
 277        }
 278        if let Some(operator) = prior_operator {
 279            self.push_operator(operator, window, cx);
 280        };
 281        self.search_motion(
 282            Motion::ZedSearchResult {
 283                prior_selections,
 284                new_selections,
 285            },
 286            window,
 287            cx,
 288        );
 289    }
 290
 291    pub fn move_to_match_internal(
 292        &mut self,
 293        direction: Direction,
 294        window: &mut Window,
 295        cx: &mut Context<Self>,
 296    ) {
 297        let Some(pane) = self.pane(window, cx) else {
 298            return;
 299        };
 300        let count = Vim::take_count(cx).unwrap_or(1);
 301        Vim::take_forced_motion(cx);
 302        let prior_selections = self.editor_selections(window, cx);
 303
 304        let success = pane.update(cx, |pane, cx| {
 305            let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() else {
 306                return false;
 307            };
 308            search_bar.update(cx, |search_bar, cx| {
 309                if !search_bar.has_active_match() || !search_bar.show(window, cx) {
 310                    return false;
 311                }
 312                let collapse = !self.mode.is_helix();
 313                search_bar.select_match(direction, count, collapse, window, cx);
 314                true
 315            })
 316        });
 317        if !success {
 318            return;
 319        }
 320
 321        let new_selections = self.editor_selections(window, cx);
 322
 323        self.search_motion(
 324            Motion::ZedSearchResult {
 325                prior_selections,
 326                new_selections,
 327            },
 328            window,
 329            cx,
 330        );
 331    }
 332
 333    pub fn move_to_internal(
 334        &mut self,
 335        direction: Direction,
 336        case_sensitive: bool,
 337        whole_word: bool,
 338        regex: bool,
 339        window: &mut Window,
 340        cx: &mut Context<Self>,
 341    ) {
 342        let Some(pane) = self.pane(window, cx) else {
 343            return;
 344        };
 345        let count = Vim::take_count(cx).unwrap_or(1);
 346        Vim::take_forced_motion(cx);
 347        let prior_selections = self.editor_selections(window, cx);
 348        let cursor_word = self.editor_cursor_word(window, cx);
 349        let vim = cx.entity();
 350
 351        let searched = pane.update(cx, |pane, cx| {
 352            self.search.direction = direction;
 353            let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() else {
 354                return false;
 355            };
 356            let search = search_bar.update(cx, |search_bar, cx| {
 357                let mut options = SearchOptions::NONE;
 358                if case_sensitive {
 359                    options |= SearchOptions::CASE_SENSITIVE;
 360                }
 361                if regex {
 362                    options |= SearchOptions::REGEX;
 363                }
 364                if whole_word {
 365                    options |= SearchOptions::WHOLE_WORD;
 366                }
 367                if !search_bar.show(window, cx) {
 368                    return None;
 369                }
 370                let Some(query) = search_bar
 371                    .query_suggestion(window, cx)
 372                    .or_else(|| cursor_word)
 373                else {
 374                    drop(search_bar.search("", None, false, window, cx));
 375                    return None;
 376                };
 377
 378                let query = regex::escape(&query);
 379                Some(search_bar.search(&query, Some(options), true, window, cx))
 380            });
 381
 382            let Some(search) = search else { return false };
 383
 384            let search_bar = search_bar.downgrade();
 385            cx.spawn_in(window, async move |_, cx| {
 386                search.await?;
 387                search_bar.update_in(cx, |search_bar, window, cx| {
 388                    let collapse = editor::vim_flavor(cx) == Some(VimFlavor::Vim);
 389                    search_bar.select_match(direction, count, collapse, window, cx);
 390
 391                    vim.update(cx, |vim, cx| {
 392                        let new_selections = vim.editor_selections(window, cx);
 393                        vim.search_motion(
 394                            Motion::ZedSearchResult {
 395                                prior_selections,
 396                                new_selections,
 397                            },
 398                            window,
 399                            cx,
 400                        )
 401                    });
 402                })?;
 403                anyhow::Ok(())
 404            })
 405            .detach_and_log_err(cx);
 406            true
 407        });
 408        if !searched {
 409            self.clear_operator(window, cx)
 410        }
 411
 412        if self.mode.is_visual() {
 413            self.switch_mode(Mode::Normal, false, window, cx)
 414        }
 415    }
 416
 417    fn find_command(&mut self, action: &FindCommand, window: &mut Window, cx: &mut Context<Self>) {
 418        let Some(pane) = self.pane(window, cx) else {
 419            return;
 420        };
 421        pane.update(cx, |pane, cx| {
 422            if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::<BufferSearchBar>() {
 423                let search = search_bar.update(cx, |search_bar, cx| {
 424                    if !search_bar.show(window, cx) {
 425                        return None;
 426                    }
 427                    let mut query = action.query.clone();
 428                    if query.is_empty() {
 429                        query = search_bar.query(cx);
 430                    };
 431
 432                    let mut options = SearchOptions::REGEX | SearchOptions::CASE_SENSITIVE;
 433                    if search_bar.should_use_smartcase_search(cx) {
 434                        options.set(
 435                            SearchOptions::CASE_SENSITIVE,
 436                            search_bar.is_contains_uppercase(&query),
 437                        );
 438                    }
 439
 440                    Some(search_bar.search(&query, Some(options), true, window, cx))
 441                });
 442                let Some(search) = search else { return };
 443                let search_bar = search_bar.downgrade();
 444                let direction = if action.backwards {
 445                    Direction::Prev
 446                } else {
 447                    Direction::Next
 448                };
 449                cx.spawn_in(window, async move |_, cx| {
 450                    search.await?;
 451                    search_bar.update_in(cx, |search_bar, window, cx| {
 452                        search_bar.select_match(direction, 1, true, window, cx)
 453                    })?;
 454                    anyhow::Ok(())
 455                })
 456                .detach_and_log_err(cx);
 457            }
 458        })
 459    }
 460
 461    fn replace_command(
 462        &mut self,
 463        action: &ReplaceCommand,
 464        window: &mut Window,
 465        cx: &mut Context<Self>,
 466    ) {
 467        let replacement = action.replacement.clone();
 468        let Some(((pane, workspace), editor)) = self
 469            .pane(window, cx)
 470            .zip(self.workspace(window))
 471            .zip(self.editor())
 472        else {
 473            return;
 474        };
 475        if let Some(result) = self.update_editor(cx, |vim, editor, cx| {
 476            let range = action.range.buffer_range(vim, editor, window, cx)?;
 477            let snapshot = editor.snapshot(window, cx);
 478            let snapshot = snapshot.buffer_snapshot();
 479            let end_point = Point::new(range.end.0, snapshot.line_len(range.end));
 480            let range = snapshot.anchor_before(Point::new(range.start.0, 0))
 481                ..snapshot.anchor_after(end_point);
 482            editor.set_search_within_ranges(&[range], cx);
 483            anyhow::Ok(())
 484        }) {
 485            workspace.update(cx, |workspace, cx| {
 486                result.notify_err(workspace, cx);
 487            })
 488        }
 489        let Some(search_bar) = pane.update(cx, |pane, cx| {
 490            pane.toolbar().read(cx).item_of_type::<BufferSearchBar>()
 491        }) else {
 492            return;
 493        };
 494        let mut options = SearchOptions::REGEX;
 495        let search = search_bar.update(cx, |search_bar, cx| {
 496            if !search_bar.show(window, cx) {
 497                return None;
 498            }
 499
 500            let search = if replacement.search.is_empty() {
 501                search_bar.query(cx)
 502            } else {
 503                replacement.search
 504            };
 505
 506            if let Some(case) = replacement.case_sensitive {
 507                options.set(SearchOptions::CASE_SENSITIVE, case)
 508            } else if search_bar.should_use_smartcase_search(cx) {
 509                options.set(
 510                    SearchOptions::CASE_SENSITIVE,
 511                    search_bar.is_contains_uppercase(&search),
 512                );
 513            } else {
 514                options.set(SearchOptions::CASE_SENSITIVE, false)
 515            }
 516
 517            if !replacement.flag_g {
 518                options.set(SearchOptions::ONE_MATCH_PER_LINE, true);
 519            }
 520
 521            search_bar.set_replacement(Some(&replacement.replacement), cx);
 522            if replacement.flag_c {
 523                search_bar.focus_replace(window, cx);
 524            }
 525            Some(search_bar.search(&search, Some(options), true, window, cx))
 526        });
 527        if replacement.flag_n {
 528            self.move_cursor(
 529                Motion::StartOfLine {
 530                    display_lines: false,
 531                },
 532                None,
 533                window,
 534                cx,
 535            );
 536            return;
 537        }
 538        let Some(search) = search else { return };
 539        let search_bar = search_bar.downgrade();
 540        cx.spawn_in(window, async move |vim, cx| {
 541            search.await?;
 542            search_bar.update_in(cx, |search_bar, window, cx| {
 543                if replacement.flag_c {
 544                    search_bar.select_first_match(window, cx);
 545                    return;
 546                }
 547                search_bar.select_last_match(window, cx);
 548                search_bar.replace_all(&Default::default(), window, cx);
 549                editor.update(cx, |editor, cx| editor.clear_search_within_ranges(cx));
 550                let _ = search_bar.search(&search_bar.query(cx), None, false, window, cx);
 551                vim.update(cx, |vim, cx| {
 552                    vim.move_cursor(
 553                        Motion::StartOfLine {
 554                            display_lines: false,
 555                        },
 556                        None,
 557                        window,
 558                        cx,
 559                    )
 560                })
 561                .ok();
 562
 563                // Disable the `ONE_MATCH_PER_LINE` search option when finished, as
 564                // this is not properly supported outside of vim mode, and
 565                // not disabling it makes the "Replace All Matches" button
 566                // actually replace only the first match on each line.
 567                options.set(SearchOptions::ONE_MATCH_PER_LINE, false);
 568                search_bar.set_search_options(options, cx);
 569            })
 570        })
 571        .detach_and_log_err(cx);
 572    }
 573}
 574
 575impl Replacement {
 576    // convert a vim query into something more usable by zed.
 577    // we don't attempt to fully convert between the two regex syntaxes,
 578    // but we do flip \( and \) to ( and ) (and vice-versa) in the pattern,
 579    // and convert \0..\9 to $0..$9 in the replacement so that common idioms work.
 580    pub(crate) fn parse(mut chars: Peekable<Chars>) -> Option<Replacement> {
 581        let delimiter = chars
 582            .next()
 583            .filter(|c| !c.is_alphanumeric() && *c != '"' && *c != '|' && *c != '\'')?;
 584
 585        let mut search = String::new();
 586        let mut replacement = String::new();
 587        let mut flags = String::new();
 588
 589        let mut buffer = &mut search;
 590
 591        let mut escaped = false;
 592        // 0 - parsing search
 593        // 1 - parsing replacement
 594        // 2 - parsing flags
 595        let mut phase = 0;
 596
 597        for c in chars {
 598            if escaped {
 599                escaped = false;
 600                if phase == 1 && c.is_ascii_digit() {
 601                    buffer.push('$')
 602                // unescape escaped parens
 603                } else if phase == 0 && (c == '(' || c == ')') {
 604                } else if c != delimiter {
 605                    buffer.push('\\')
 606                }
 607                buffer.push(c)
 608            } else if c == '\\' {
 609                escaped = true;
 610            } else if c == delimiter {
 611                if phase == 0 {
 612                    buffer = &mut replacement;
 613                    phase = 1;
 614                } else if phase == 1 {
 615                    buffer = &mut flags;
 616                    phase = 2;
 617                } else {
 618                    break;
 619                }
 620            } else {
 621                // escape unescaped parens
 622                if phase == 0 && (c == '(' || c == ')') {
 623                    buffer.push('\\')
 624                }
 625                buffer.push(c)
 626            }
 627        }
 628
 629        let mut replacement = Replacement {
 630            search,
 631            replacement,
 632            case_sensitive: None,
 633            flag_g: false,
 634            flag_n: false,
 635            flag_c: false,
 636        };
 637
 638        for c in flags.chars() {
 639            match c {
 640                'g' => replacement.flag_g = true,
 641                'n' => replacement.flag_n = true,
 642                'c' => replacement.flag_c = true,
 643                'i' => replacement.case_sensitive = Some(false),
 644                'I' => replacement.case_sensitive = Some(true),
 645                _ => {}
 646            }
 647        }
 648
 649        Some(replacement)
 650    }
 651}
 652
 653#[cfg(test)]
 654mod test {
 655    use std::time::Duration;
 656
 657    use crate::{
 658        state::Mode,
 659        test::{NeovimBackedTestContext, VimTestContext},
 660    };
 661    use editor::{DisplayPoint, display_map::DisplayRow};
 662
 663    use indoc::indoc;
 664    use search::BufferSearchBar;
 665    use settings::SettingsStore;
 666
 667    #[gpui::test]
 668    async fn test_move_to_next(cx: &mut gpui::TestAppContext) {
 669        let mut cx = VimTestContext::new(cx, true).await;
 670        cx.set_state("ˇhi\nhigh\nhi\n", Mode::Normal);
 671
 672        cx.simulate_keystrokes("*");
 673        cx.run_until_parked();
 674        cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
 675
 676        cx.simulate_keystrokes("*");
 677        cx.run_until_parked();
 678        cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
 679
 680        cx.simulate_keystrokes("#");
 681        cx.run_until_parked();
 682        cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
 683
 684        cx.simulate_keystrokes("#");
 685        cx.run_until_parked();
 686        cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
 687
 688        cx.simulate_keystrokes("2 *");
 689        cx.run_until_parked();
 690        cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
 691
 692        cx.simulate_keystrokes("g *");
 693        cx.run_until_parked();
 694        cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
 695
 696        cx.simulate_keystrokes("n");
 697        cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
 698
 699        cx.simulate_keystrokes("g #");
 700        cx.run_until_parked();
 701        cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
 702    }
 703
 704    #[gpui::test]
 705    async fn test_move_to_next_with_no_search_wrap(cx: &mut gpui::TestAppContext) {
 706        let mut cx = VimTestContext::new(cx, true).await;
 707
 708        cx.update_global(|store: &mut SettingsStore, cx| {
 709            store.update_user_settings(cx, |s| s.editor.search_wrap = Some(false));
 710        });
 711
 712        cx.set_state("ˇhi\nhigh\nhi\n", Mode::Normal);
 713
 714        cx.simulate_keystrokes("*");
 715        cx.run_until_parked();
 716        cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
 717
 718        cx.simulate_keystrokes("*");
 719        cx.run_until_parked();
 720        cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
 721
 722        cx.simulate_keystrokes("#");
 723        cx.run_until_parked();
 724        cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
 725
 726        cx.simulate_keystrokes("3 *");
 727        cx.run_until_parked();
 728        cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal);
 729
 730        cx.simulate_keystrokes("g *");
 731        cx.run_until_parked();
 732        cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
 733
 734        cx.simulate_keystrokes("n");
 735        cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal);
 736
 737        cx.simulate_keystrokes("g #");
 738        cx.run_until_parked();
 739        cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal);
 740    }
 741
 742    #[gpui::test]
 743    async fn test_search(cx: &mut gpui::TestAppContext) {
 744        let mut cx = VimTestContext::new(cx, true).await;
 745
 746        cx.set_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal);
 747        cx.simulate_keystrokes("/ c c");
 748
 749        let search_bar = cx.workspace(|workspace, _, cx| {
 750            workspace
 751                .active_pane()
 752                .read(cx)
 753                .toolbar()
 754                .read(cx)
 755                .item_of_type::<BufferSearchBar>()
 756                .expect("Buffer search bar should be deployed")
 757        });
 758
 759        cx.update_entity(search_bar, |bar, _window, cx| {
 760            assert_eq!(bar.query(cx), "cc");
 761        });
 762
 763        cx.run_until_parked();
 764
 765        cx.update_editor(|editor, window, cx| {
 766            let highlights = editor.all_text_background_highlights(window, cx);
 767            assert_eq!(3, highlights.len());
 768            assert_eq!(
 769                DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 2),
 770                highlights[0].0
 771            )
 772        });
 773
 774        cx.simulate_keystrokes("enter");
 775        cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
 776
 777        // n to go to next/N to go to previous
 778        cx.simulate_keystrokes("n");
 779        cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal);
 780        cx.simulate_keystrokes("shift-n");
 781        cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
 782
 783        // ?<enter> to go to previous
 784        cx.simulate_keystrokes("? enter");
 785        cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal);
 786        cx.simulate_keystrokes("? enter");
 787        cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal);
 788
 789        // /<enter> to go to next
 790        cx.simulate_keystrokes("/ enter");
 791        cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal);
 792
 793        // ?{search}<enter> to search backwards
 794        cx.simulate_keystrokes("? b enter");
 795        cx.assert_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal);
 796
 797        // works with counts
 798        cx.simulate_keystrokes("4 / c");
 799        cx.simulate_keystrokes("enter");
 800        cx.assert_state("aa\nbb\ncc\ncˇc\ncc\n", Mode::Normal);
 801
 802        // check that searching resumes from cursor, not previous match
 803        cx.set_state("ˇaa\nbb\ndd\ncc\nbb\n", Mode::Normal);
 804        cx.simulate_keystrokes("/ d");
 805        cx.simulate_keystrokes("enter");
 806        cx.assert_state("aa\nbb\nˇdd\ncc\nbb\n", Mode::Normal);
 807        cx.update_editor(|editor, window, cx| {
 808            editor.move_to_beginning(&Default::default(), window, cx)
 809        });
 810        cx.assert_state("ˇaa\nbb\ndd\ncc\nbb\n", Mode::Normal);
 811        cx.simulate_keystrokes("/ b");
 812        cx.simulate_keystrokes("enter");
 813        cx.assert_state("aa\nˇbb\ndd\ncc\nbb\n", Mode::Normal);
 814
 815        // check that searching switches to normal mode if in visual mode
 816        cx.set_state("ˇone two one", Mode::Normal);
 817        cx.simulate_keystrokes("v l l");
 818        cx.assert_editor_state("«oneˇ» two one");
 819        cx.simulate_keystrokes("*");
 820        cx.assert_state("one two ˇone", Mode::Normal);
 821
 822        // check that a backward search after last match works correctly
 823        cx.set_state("aa\naa\nbbˇ", Mode::Normal);
 824        cx.simulate_keystrokes("? a a");
 825        cx.simulate_keystrokes("enter");
 826        cx.assert_state("aa\nˇaa\nbb", Mode::Normal);
 827
 828        // check that searching with unable search wrap
 829        cx.update_global(|store: &mut SettingsStore, cx| {
 830            store.update_user_settings(cx, |s| s.editor.search_wrap = Some(false));
 831        });
 832        cx.set_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal);
 833        cx.simulate_keystrokes("/ c c enter");
 834
 835        cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
 836
 837        // n to go to next/N to go to previous
 838        cx.simulate_keystrokes("n");
 839        cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal);
 840        cx.simulate_keystrokes("shift-n");
 841        cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
 842
 843        // ?<enter> to go to previous
 844        cx.simulate_keystrokes("? enter");
 845        cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
 846        cx.simulate_keystrokes("? enter");
 847        cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal);
 848    }
 849
 850    #[gpui::test]
 851    async fn test_non_vim_search(cx: &mut gpui::TestAppContext) {
 852        let mut cx = VimTestContext::new(cx, false).await;
 853        cx.cx.set_state("ˇone one one one");
 854        cx.run_until_parked();
 855        cx.simulate_keystrokes("cmd-f");
 856        cx.run_until_parked();
 857
 858        cx.assert_editor_state("«oneˇ» one one one");
 859        cx.simulate_keystrokes("enter");
 860        cx.assert_editor_state("one «oneˇ» one one");
 861        cx.simulate_keystrokes("shift-enter");
 862        cx.assert_editor_state("«oneˇ» one one one");
 863    }
 864
 865    #[gpui::test]
 866    async fn test_visual_star_hash(cx: &mut gpui::TestAppContext) {
 867        let mut cx = NeovimBackedTestContext::new(cx).await;
 868
 869        cx.set_shared_state("ˇa.c. abcd a.c. abcd").await;
 870        cx.simulate_shared_keystrokes("v 3 l *").await;
 871        cx.shared_state().await.assert_eq("a.c. abcd ˇa.c. abcd");
 872    }
 873
 874    #[gpui::test]
 875    async fn test_d_search(cx: &mut gpui::TestAppContext) {
 876        let mut cx = NeovimBackedTestContext::new(cx).await;
 877
 878        cx.set_shared_state("ˇa.c. abcd a.c. abcd").await;
 879        cx.simulate_shared_keystrokes("d / c d").await;
 880        cx.simulate_shared_keystrokes("enter").await;
 881        cx.shared_state().await.assert_eq("ˇcd a.c. abcd");
 882    }
 883
 884    #[gpui::test]
 885    async fn test_backwards_n(cx: &mut gpui::TestAppContext) {
 886        let mut cx = NeovimBackedTestContext::new(cx).await;
 887
 888        cx.set_shared_state("ˇa b a b a b a").await;
 889        cx.simulate_shared_keystrokes("*").await;
 890        cx.simulate_shared_keystrokes("n").await;
 891        cx.shared_state().await.assert_eq("a b a b ˇa b a");
 892        cx.simulate_shared_keystrokes("#").await;
 893        cx.shared_state().await.assert_eq("a b ˇa b a b a");
 894        cx.simulate_shared_keystrokes("n").await;
 895        cx.shared_state().await.assert_eq("ˇa b a b a b a");
 896    }
 897
 898    #[gpui::test]
 899    async fn test_v_search(cx: &mut gpui::TestAppContext) {
 900        let mut cx = NeovimBackedTestContext::new(cx).await;
 901
 902        cx.set_shared_state("ˇa.c. abcd a.c. abcd").await;
 903        cx.simulate_shared_keystrokes("v / c d").await;
 904        cx.simulate_shared_keystrokes("enter").await;
 905        cx.shared_state().await.assert_eq("«a.c. abcˇ»d a.c. abcd");
 906
 907        cx.set_shared_state("a a aˇ a a a").await;
 908        cx.simulate_shared_keystrokes("v / a").await;
 909        cx.simulate_shared_keystrokes("enter").await;
 910        cx.shared_state().await.assert_eq("a a a« aˇ» a a");
 911        cx.simulate_shared_keystrokes("/ enter").await;
 912        cx.shared_state().await.assert_eq("a a a« a aˇ» a");
 913        cx.simulate_shared_keystrokes("? enter").await;
 914        cx.shared_state().await.assert_eq("a a a« aˇ» a a");
 915        cx.simulate_shared_keystrokes("? enter").await;
 916        cx.shared_state().await.assert_eq("a a «ˇa »a a a");
 917        cx.simulate_shared_keystrokes("/ enter").await;
 918        cx.shared_state().await.assert_eq("a a a« aˇ» a a");
 919        cx.simulate_shared_keystrokes("/ enter").await;
 920        cx.shared_state().await.assert_eq("a a a« a aˇ» a");
 921    }
 922
 923    #[gpui::test]
 924    async fn test_v_search_aa(cx: &mut gpui::TestAppContext) {
 925        let mut cx = NeovimBackedTestContext::new(cx).await;
 926
 927        cx.set_shared_state("ˇaa aa").await;
 928        cx.simulate_shared_keystrokes("v / a a").await;
 929        cx.simulate_shared_keystrokes("enter").await;
 930        cx.shared_state().await.assert_eq("«aa aˇ»a");
 931    }
 932
 933    #[gpui::test]
 934    async fn test_visual_block_search(cx: &mut gpui::TestAppContext) {
 935        let mut cx = NeovimBackedTestContext::new(cx).await;
 936
 937        cx.set_shared_state(indoc! {
 938            "ˇone two
 939             three four
 940             five six
 941             "
 942        })
 943        .await;
 944        cx.simulate_shared_keystrokes("ctrl-v j / f").await;
 945        cx.simulate_shared_keystrokes("enter").await;
 946        cx.shared_state().await.assert_eq(indoc! {
 947            "«one twoˇ»
 948             «three fˇ»our
 949             five six
 950             "
 951        });
 952    }
 953
 954    #[gpui::test]
 955    async fn test_replace_with_range_at_start(cx: &mut gpui::TestAppContext) {
 956        let mut cx = NeovimBackedTestContext::new(cx).await;
 957
 958        cx.set_shared_state(indoc! {
 959            "ˇa
 960            a
 961            a
 962            a
 963            a
 964            a
 965            a
 966             "
 967        })
 968        .await;
 969        cx.simulate_shared_keystrokes(": 2 , 5 s / ^ / b").await;
 970        cx.simulate_shared_keystrokes("enter").await;
 971        cx.shared_state().await.assert_eq(indoc! {
 972            "a
 973            ba
 974            ba
 975            ba
 976            ˇba
 977            a
 978            a
 979             "
 980        });
 981
 982        cx.simulate_shared_keystrokes("/ a").await;
 983        cx.simulate_shared_keystrokes("enter").await;
 984        cx.shared_state().await.assert_eq(indoc! {
 985            "a
 986                ba
 987                ba
 988                ba
 989                bˇa
 990                a
 991                a
 992                 "
 993        });
 994    }
 995
 996    #[gpui::test]
 997    async fn test_search_skipping(cx: &mut gpui::TestAppContext) {
 998        let mut cx = NeovimBackedTestContext::new(cx).await;
 999        cx.set_shared_state(indoc! {
1000            "ˇaa aa aa"
1001        })
1002        .await;
1003
1004        cx.simulate_shared_keystrokes("/ a a").await;
1005        cx.simulate_shared_keystrokes("enter").await;
1006
1007        cx.shared_state().await.assert_eq(indoc! {
1008            "aa ˇaa aa"
1009        });
1010
1011        cx.simulate_shared_keystrokes("left / a a").await;
1012        cx.simulate_shared_keystrokes("enter").await;
1013
1014        cx.shared_state().await.assert_eq(indoc! {
1015            "aa ˇaa aa"
1016        });
1017    }
1018
1019    #[gpui::test]
1020    async fn test_replace_n(cx: &mut gpui::TestAppContext) {
1021        let mut cx = NeovimBackedTestContext::new(cx).await;
1022        cx.set_shared_state(indoc! {
1023            "ˇaa
1024            bb
1025            aa"
1026        })
1027        .await;
1028
1029        cx.simulate_shared_keystrokes(": s / b b / d d / n").await;
1030        cx.simulate_shared_keystrokes("enter").await;
1031
1032        cx.shared_state().await.assert_eq(indoc! {
1033            "ˇaa
1034            bb
1035            aa"
1036        });
1037
1038        let search_bar = cx.update_workspace(|workspace, _, cx| {
1039            workspace.active_pane().update(cx, |pane, cx| {
1040                pane.toolbar()
1041                    .read(cx)
1042                    .item_of_type::<BufferSearchBar>()
1043                    .unwrap()
1044            })
1045        });
1046        cx.update_entity(search_bar, |search_bar, _, cx| {
1047            assert!(!search_bar.is_dismissed());
1048            assert_eq!(search_bar.query(cx), "bb".to_string());
1049            assert_eq!(search_bar.replacement(cx), "dd".to_string());
1050        })
1051    }
1052
1053    #[gpui::test]
1054    async fn test_replace_g(cx: &mut gpui::TestAppContext) {
1055        let mut cx = NeovimBackedTestContext::new(cx).await;
1056        cx.set_shared_state(indoc! {
1057            "ˇaa aa aa aa
1058            aa
1059            aa"
1060        })
1061        .await;
1062
1063        cx.simulate_shared_keystrokes(": s / a a / b b").await;
1064        cx.simulate_shared_keystrokes("enter").await;
1065        cx.shared_state().await.assert_eq(indoc! {
1066            "ˇbb aa aa aa
1067            aa
1068            aa"
1069        });
1070        cx.simulate_shared_keystrokes(": s / a a / b b / g").await;
1071        cx.simulate_shared_keystrokes("enter").await;
1072        cx.shared_state().await.assert_eq(indoc! {
1073            "ˇbb bb bb bb
1074            aa
1075            aa"
1076        });
1077    }
1078
1079    #[gpui::test]
1080    async fn test_replace_c(cx: &mut gpui::TestAppContext) {
1081        let mut cx = VimTestContext::new(cx, true).await;
1082        cx.set_state(
1083            indoc! {
1084                "ˇaa
1085            aa
1086            aa"
1087            },
1088            Mode::Normal,
1089        );
1090
1091        cx.simulate_keystrokes("v j : s / a a / d d / c");
1092        cx.simulate_keystrokes("enter");
1093
1094        cx.assert_state(
1095            indoc! {
1096                "ˇaa
1097            aa
1098            aa"
1099            },
1100            Mode::Normal,
1101        );
1102
1103        cx.simulate_keystrokes("enter");
1104
1105        cx.assert_state(
1106            indoc! {
1107                "dd
1108            ˇaa
1109            aa"
1110            },
1111            Mode::Normal,
1112        );
1113
1114        cx.simulate_keystrokes("enter");
1115        cx.assert_state(
1116            indoc! {
1117                "dd
1118            ddˇ
1119            aa"
1120            },
1121            Mode::Normal,
1122        );
1123        cx.simulate_keystrokes("enter");
1124        cx.assert_state(
1125            indoc! {
1126                "dd
1127            ddˇ
1128            aa"
1129            },
1130            Mode::Normal,
1131        );
1132    }
1133
1134    #[gpui::test]
1135    async fn test_replace_with_range(cx: &mut gpui::TestAppContext) {
1136        let mut cx = NeovimBackedTestContext::new(cx).await;
1137
1138        cx.set_shared_state(indoc! {
1139            "ˇa
1140            a
1141            a
1142            a
1143            a
1144            a
1145            a
1146             "
1147        })
1148        .await;
1149        cx.simulate_shared_keystrokes(": 2 , 5 s / a / b").await;
1150        cx.simulate_shared_keystrokes("enter").await;
1151        cx.shared_state().await.assert_eq(indoc! {
1152            "a
1153            b
1154            b
1155            b
1156            ˇb
1157            a
1158            a
1159             "
1160        });
1161        cx.executor().advance_clock(Duration::from_millis(250));
1162        cx.run_until_parked();
1163
1164        cx.simulate_shared_keystrokes("/ a enter").await;
1165        cx.shared_state().await.assert_eq(indoc! {
1166            "a
1167                b
1168                b
1169                b
1170                b
1171                ˇa
1172                a
1173                 "
1174        });
1175    }
1176}