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