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