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