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