search.rs

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