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