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