search.rs

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