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