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