file_finder.rs

   1use editor::{scroll::autoscroll::Autoscroll, Bias, Editor};
   2use fuzzy::PathMatch;
   3use gpui::{
   4    actions, elements::*, AppContext, ModelHandle, MouseState, Task, ViewContext, WeakViewHandle,
   5};
   6use picker::{Picker, PickerDelegate};
   7use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
   8use std::{
   9    path::{Path, PathBuf},
  10    sync::{
  11        atomic::{self, AtomicBool},
  12        Arc,
  13    },
  14};
  15use text::Point;
  16use util::{paths::PathLikeWithPosition, post_inc, ResultExt};
  17use workspace::Workspace;
  18
  19pub type FileFinder = Picker<FileFinderDelegate>;
  20
  21pub struct FileFinderDelegate {
  22    workspace: WeakViewHandle<Workspace>,
  23    project: ModelHandle<Project>,
  24    search_count: usize,
  25    latest_search_id: usize,
  26    latest_search_did_cancel: bool,
  27    latest_search_query: Option<PathLikeWithPosition<FileSearchQuery>>,
  28    currently_opened_path: Option<FoundPath>,
  29    matches: Matches,
  30    selected_index: Option<usize>,
  31    cancel_flag: Arc<AtomicBool>,
  32    history_items: Vec<FoundPath>,
  33}
  34
  35#[derive(Debug)]
  36enum Matches {
  37    History(Vec<FoundPath>),
  38    Search(Vec<PathMatch>),
  39}
  40
  41#[derive(Debug)]
  42enum Match<'a> {
  43    History(&'a FoundPath),
  44    Search(&'a PathMatch),
  45}
  46
  47impl Matches {
  48    fn len(&self) -> usize {
  49        match self {
  50            Self::History(items) => items.len(),
  51            Self::Search(items) => items.len(),
  52        }
  53    }
  54
  55    fn get(&self, index: usize) -> Option<Match<'_>> {
  56        match self {
  57            Self::History(items) => items.get(index).map(Match::History),
  58            Self::Search(items) => items.get(index).map(Match::Search),
  59        }
  60    }
  61}
  62
  63impl Default for Matches {
  64    fn default() -> Self {
  65        Self::History(Vec::new())
  66    }
  67}
  68
  69#[derive(Debug, Clone, PartialEq, Eq)]
  70struct FoundPath {
  71    project: ProjectPath,
  72    absolute: Option<PathBuf>,
  73}
  74
  75impl FoundPath {
  76    fn new(project: ProjectPath, absolute: Option<PathBuf>) -> Self {
  77        Self { project, absolute }
  78    }
  79}
  80
  81actions!(file_finder, [Toggle]);
  82
  83pub fn init(cx: &mut AppContext) {
  84    cx.add_action(toggle_file_finder);
  85    FileFinder::init(cx);
  86}
  87
  88const MAX_RECENT_SELECTIONS: usize = 20;
  89
  90fn toggle_file_finder(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
  91    workspace.toggle_modal(cx, |workspace, cx| {
  92        let project = workspace.project().read(cx);
  93
  94        let currently_opened_path = workspace
  95            .active_item(cx)
  96            .and_then(|item| item.project_path(cx))
  97            .map(|project_path| {
  98                let abs_path = project
  99                    .worktree_for_id(project_path.worktree_id, cx)
 100                    .map(|worktree| worktree.read(cx).abs_path().join(&project_path.path));
 101                FoundPath::new(project_path, abs_path)
 102            });
 103
 104        // if exists, bubble the currently opened path to the top
 105        let history_items = currently_opened_path
 106            .clone()
 107            .into_iter()
 108            .chain(
 109                workspace
 110                    .recent_navigation_history(Some(MAX_RECENT_SELECTIONS), cx)
 111                    .into_iter()
 112                    .filter(|(history_path, _)| {
 113                        Some(history_path)
 114                            != currently_opened_path
 115                                .as_ref()
 116                                .map(|found_path| &found_path.project)
 117                    })
 118                    .filter(|(_, history_abs_path)| {
 119                        history_abs_path.as_ref()
 120                            != currently_opened_path
 121                                .as_ref()
 122                                .and_then(|found_path| found_path.absolute.as_ref())
 123                    })
 124                    .map(|(history_path, abs_path)| FoundPath::new(history_path, abs_path)),
 125            )
 126            .collect();
 127
 128        let project = workspace.project().clone();
 129        let workspace = cx.handle().downgrade();
 130        let finder = cx.add_view(|cx| {
 131            Picker::new(
 132                FileFinderDelegate::new(
 133                    workspace,
 134                    project,
 135                    currently_opened_path,
 136                    history_items,
 137                    cx,
 138                ),
 139                cx,
 140            )
 141        });
 142        finder
 143    });
 144}
 145
 146pub enum Event {
 147    Selected(ProjectPath),
 148    Dismissed,
 149}
 150
 151#[derive(Debug, Clone)]
 152struct FileSearchQuery {
 153    raw_query: String,
 154    file_query_end: Option<usize>,
 155}
 156
 157impl FileSearchQuery {
 158    fn path_query(&self) -> &str {
 159        match self.file_query_end {
 160            Some(file_path_end) => &self.raw_query[..file_path_end],
 161            None => &self.raw_query,
 162        }
 163    }
 164}
 165
 166impl FileFinderDelegate {
 167    fn new(
 168        workspace: WeakViewHandle<Workspace>,
 169        project: ModelHandle<Project>,
 170        currently_opened_path: Option<FoundPath>,
 171        history_items: Vec<FoundPath>,
 172        cx: &mut ViewContext<FileFinder>,
 173    ) -> Self {
 174        cx.observe(&project, |picker, _, cx| {
 175            picker.update_matches(picker.query(cx), cx);
 176        })
 177        .detach();
 178        Self {
 179            workspace,
 180            project,
 181            search_count: 0,
 182            latest_search_id: 0,
 183            latest_search_did_cancel: false,
 184            latest_search_query: None,
 185            currently_opened_path,
 186            matches: Matches::default(),
 187            selected_index: None,
 188            cancel_flag: Arc::new(AtomicBool::new(false)),
 189            history_items,
 190        }
 191    }
 192
 193    fn spawn_search(
 194        &mut self,
 195        query: PathLikeWithPosition<FileSearchQuery>,
 196        cx: &mut ViewContext<FileFinder>,
 197    ) -> Task<()> {
 198        let relative_to = self
 199            .currently_opened_path
 200            .as_ref()
 201            .map(|found_path| Arc::clone(&found_path.project.path));
 202        let worktrees = self
 203            .project
 204            .read(cx)
 205            .visible_worktrees(cx)
 206            .collect::<Vec<_>>();
 207        let include_root_name = worktrees.len() > 1;
 208        let candidate_sets = worktrees
 209            .into_iter()
 210            .map(|worktree| {
 211                let worktree = worktree.read(cx);
 212                PathMatchCandidateSet {
 213                    snapshot: worktree.snapshot(),
 214                    include_ignored: worktree
 215                        .root_entry()
 216                        .map_or(false, |entry| entry.is_ignored),
 217                    include_root_name,
 218                }
 219            })
 220            .collect::<Vec<_>>();
 221
 222        let search_id = util::post_inc(&mut self.search_count);
 223        self.cancel_flag.store(true, atomic::Ordering::Relaxed);
 224        self.cancel_flag = Arc::new(AtomicBool::new(false));
 225        let cancel_flag = self.cancel_flag.clone();
 226        cx.spawn(|picker, mut cx| async move {
 227            let matches = fuzzy::match_path_sets(
 228                candidate_sets.as_slice(),
 229                query.path_like.path_query(),
 230                relative_to,
 231                false,
 232                100,
 233                &cancel_flag,
 234                cx.background(),
 235            )
 236            .await;
 237            let did_cancel = cancel_flag.load(atomic::Ordering::Relaxed);
 238            picker
 239                .update(&mut cx, |picker, cx| {
 240                    picker
 241                        .delegate_mut()
 242                        .set_search_matches(search_id, did_cancel, query, matches, cx)
 243                })
 244                .log_err();
 245        })
 246    }
 247
 248    fn set_search_matches(
 249        &mut self,
 250        search_id: usize,
 251        did_cancel: bool,
 252        query: PathLikeWithPosition<FileSearchQuery>,
 253        matches: Vec<PathMatch>,
 254        cx: &mut ViewContext<FileFinder>,
 255    ) {
 256        if search_id >= self.latest_search_id {
 257            self.latest_search_id = search_id;
 258            if self.latest_search_did_cancel
 259                && Some(query.path_like.path_query())
 260                    == self
 261                        .latest_search_query
 262                        .as_ref()
 263                        .map(|query| query.path_like.path_query())
 264            {
 265                match &mut self.matches {
 266                    Matches::History(_) => self.matches = Matches::Search(matches),
 267                    Matches::Search(search_matches) => {
 268                        util::extend_sorted(search_matches, matches.into_iter(), 100, |a, b| {
 269                            b.cmp(a)
 270                        })
 271                    }
 272                }
 273            } else {
 274                self.matches = Matches::Search(matches);
 275            }
 276            self.latest_search_query = Some(query);
 277            self.latest_search_did_cancel = did_cancel;
 278            cx.notify();
 279        }
 280    }
 281
 282    fn labels_for_match(
 283        &self,
 284        path_match: Match,
 285        cx: &AppContext,
 286        ix: usize,
 287    ) -> (String, Vec<usize>, String, Vec<usize>) {
 288        let (file_name, file_name_positions, full_path, full_path_positions) = match path_match {
 289            Match::History(found_path) => {
 290                let worktree_id = found_path.project.worktree_id;
 291                let project_relative_path = &found_path.project.path;
 292                let has_worktree = self
 293                    .project
 294                    .read(cx)
 295                    .worktree_for_id(worktree_id, cx)
 296                    .is_some();
 297
 298                if !has_worktree {
 299                    if let Some(absolute_path) = &found_path.absolute {
 300                        return (
 301                            absolute_path
 302                                .file_name()
 303                                .map_or_else(
 304                                    || project_relative_path.to_string_lossy(),
 305                                    |file_name| file_name.to_string_lossy(),
 306                                )
 307                                .to_string(),
 308                            Vec::new(),
 309                            absolute_path.to_string_lossy().to_string(),
 310                            Vec::new(),
 311                        );
 312                    }
 313                }
 314
 315                let mut path = Arc::clone(project_relative_path);
 316                if project_relative_path.as_ref() == Path::new("") {
 317                    if let Some(absolute_path) = &found_path.absolute {
 318                        path = Arc::from(absolute_path.as_path());
 319                    }
 320                }
 321                self.labels_for_path_match(&PathMatch {
 322                    score: ix as f64,
 323                    positions: Vec::new(),
 324                    worktree_id: worktree_id.to_usize(),
 325                    path,
 326                    path_prefix: "".into(),
 327                    distance_to_relative_ancestor: usize::MAX,
 328                })
 329            }
 330            Match::Search(path_match) => self.labels_for_path_match(path_match),
 331        };
 332
 333        if file_name_positions.is_empty() {
 334            if let Some(user_home_path) = std::env::var("HOME").ok() {
 335                let user_home_path = user_home_path.trim();
 336                if !user_home_path.is_empty() {
 337                    if (&full_path).starts_with(user_home_path) {
 338                        return (
 339                            file_name,
 340                            file_name_positions,
 341                            full_path.replace(user_home_path, "~"),
 342                            full_path_positions,
 343                        );
 344                    }
 345                }
 346            }
 347        }
 348
 349        (
 350            file_name,
 351            file_name_positions,
 352            full_path,
 353            full_path_positions,
 354        )
 355    }
 356
 357    fn labels_for_path_match(
 358        &self,
 359        path_match: &PathMatch,
 360    ) -> (String, Vec<usize>, String, Vec<usize>) {
 361        let path = &path_match.path;
 362        let path_string = path.to_string_lossy();
 363        let full_path = [path_match.path_prefix.as_ref(), path_string.as_ref()].join("");
 364        let path_positions = path_match.positions.clone();
 365
 366        let file_name = path.file_name().map_or_else(
 367            || path_match.path_prefix.to_string(),
 368            |file_name| file_name.to_string_lossy().to_string(),
 369        );
 370        let file_name_start = path_match.path_prefix.chars().count() + path_string.chars().count()
 371            - file_name.chars().count();
 372        let file_name_positions = path_positions
 373            .iter()
 374            .filter_map(|pos| {
 375                if pos >= &file_name_start {
 376                    Some(pos - file_name_start)
 377                } else {
 378                    None
 379                }
 380            })
 381            .collect();
 382
 383        (file_name, file_name_positions, full_path, path_positions)
 384    }
 385}
 386
 387impl PickerDelegate for FileFinderDelegate {
 388    fn placeholder_text(&self) -> Arc<str> {
 389        "Search project files...".into()
 390    }
 391
 392    fn match_count(&self) -> usize {
 393        self.matches.len()
 394    }
 395
 396    fn selected_index(&self) -> usize {
 397        self.selected_index.unwrap_or(0)
 398    }
 399
 400    fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<FileFinder>) {
 401        self.selected_index = Some(ix);
 402        cx.notify();
 403    }
 404
 405    fn update_matches(&mut self, raw_query: String, cx: &mut ViewContext<FileFinder>) -> Task<()> {
 406        if raw_query.is_empty() {
 407            let project = self.project.read(cx);
 408            self.latest_search_id = post_inc(&mut self.search_count);
 409            self.matches = Matches::History(
 410                self.history_items
 411                    .iter()
 412                    .filter(|history_item| {
 413                        project
 414                            .worktree_for_id(history_item.project.worktree_id, cx)
 415                            .is_some()
 416                            || (project.is_local()
 417                                && history_item
 418                                    .absolute
 419                                    .as_ref()
 420                                    .filter(|abs_path| abs_path.exists())
 421                                    .is_some())
 422                    })
 423                    .cloned()
 424                    .collect(),
 425            );
 426            cx.notify();
 427            Task::ready(())
 428        } else {
 429            let raw_query = &raw_query;
 430            let query = PathLikeWithPosition::parse_str(raw_query, |path_like_str| {
 431                Ok::<_, std::convert::Infallible>(FileSearchQuery {
 432                    raw_query: raw_query.to_owned(),
 433                    file_query_end: if path_like_str == raw_query {
 434                        None
 435                    } else {
 436                        Some(path_like_str.len())
 437                    },
 438                })
 439            })
 440            .expect("infallible");
 441            self.spawn_search(query, cx)
 442        }
 443    }
 444
 445    fn confirm(&mut self, cx: &mut ViewContext<FileFinder>) {
 446        if let Some(m) = self.matches.get(self.selected_index()) {
 447            if let Some(workspace) = self.workspace.upgrade(cx) {
 448                let open_task = workspace.update(cx, |workspace, cx| match m {
 449                    Match::History(history_match) => {
 450                        let worktree_id = history_match.project.worktree_id;
 451                        if workspace
 452                            .project()
 453                            .read(cx)
 454                            .worktree_for_id(worktree_id, cx)
 455                            .is_some()
 456                        {
 457                            workspace.open_path(
 458                                ProjectPath {
 459                                    worktree_id,
 460                                    path: Arc::clone(&history_match.project.path),
 461                                },
 462                                None,
 463                                true,
 464                                cx,
 465                            )
 466                        } else {
 467                            match history_match.absolute.as_ref() {
 468                                Some(abs_path) => {
 469                                    workspace.open_abs_path(abs_path.to_path_buf(), false, cx)
 470                                }
 471                                None => workspace.open_path(
 472                                    ProjectPath {
 473                                        worktree_id,
 474                                        path: Arc::clone(&history_match.project.path),
 475                                    },
 476                                    None,
 477                                    true,
 478                                    cx,
 479                                ),
 480                            }
 481                        }
 482                    }
 483                    Match::Search(m) => workspace.open_path(
 484                        ProjectPath {
 485                            worktree_id: WorktreeId::from_usize(m.worktree_id),
 486                            path: m.path.clone(),
 487                        },
 488                        None,
 489                        true,
 490                        cx,
 491                    ),
 492                });
 493
 494                let row = self
 495                    .latest_search_query
 496                    .as_ref()
 497                    .and_then(|query| query.row)
 498                    .map(|row| row.saturating_sub(1));
 499                let col = self
 500                    .latest_search_query
 501                    .as_ref()
 502                    .and_then(|query| query.column)
 503                    .unwrap_or(0)
 504                    .saturating_sub(1);
 505                cx.spawn(|_, mut cx| async move {
 506                    let item = open_task.await.log_err()?;
 507                    if let Some(row) = row {
 508                        if let Some(active_editor) = item.downcast::<Editor>() {
 509                            active_editor
 510                                .downgrade()
 511                                .update(&mut cx, |editor, cx| {
 512                                    let snapshot = editor.snapshot(cx).display_snapshot;
 513                                    let point = snapshot
 514                                        .buffer_snapshot
 515                                        .clip_point(Point::new(row, col), Bias::Left);
 516                                    editor.change_selections(Some(Autoscroll::center()), cx, |s| {
 517                                        s.select_ranges([point..point])
 518                                    });
 519                                })
 520                                .log_err();
 521                        }
 522                    }
 523                    workspace
 524                        .downgrade()
 525                        .update(&mut cx, |workspace, cx| workspace.dismiss_modal(cx))
 526                        .log_err();
 527
 528                    Some(())
 529                })
 530                .detach();
 531            }
 532        }
 533    }
 534
 535    fn dismissed(&mut self, _: &mut ViewContext<FileFinder>) {}
 536
 537    fn render_match(
 538        &self,
 539        ix: usize,
 540        mouse_state: &mut MouseState,
 541        selected: bool,
 542        cx: &AppContext,
 543    ) -> AnyElement<Picker<Self>> {
 544        let path_match = self
 545            .matches
 546            .get(ix)
 547            .expect("Invalid matches state: no element for index {ix}");
 548        let theme = theme::current(cx);
 549        let style = theme.picker.item.in_state(selected).style_for(mouse_state);
 550        let (file_name, file_name_positions, full_path, full_path_positions) =
 551            self.labels_for_match(path_match, cx, ix);
 552        Flex::column()
 553            .with_child(
 554                Label::new(file_name, style.label.clone()).with_highlights(file_name_positions),
 555            )
 556            .with_child(
 557                Label::new(full_path, style.label.clone()).with_highlights(full_path_positions),
 558            )
 559            .flex(1., false)
 560            .contained()
 561            .with_style(style.container)
 562            .into_any_named("match")
 563    }
 564}
 565
 566#[cfg(test)]
 567mod tests {
 568    use std::{assert_eq, collections::HashMap, path::Path, time::Duration};
 569
 570    use super::*;
 571    use editor::Editor;
 572    use gpui::{TestAppContext, ViewHandle};
 573    use menu::{Confirm, SelectNext};
 574    use serde_json::json;
 575    use workspace::{AppState, Workspace};
 576
 577    #[ctor::ctor]
 578    fn init_logger() {
 579        if std::env::var("RUST_LOG").is_ok() {
 580            env_logger::init();
 581        }
 582    }
 583
 584    #[gpui::test]
 585    async fn test_matching_paths(cx: &mut TestAppContext) {
 586        let app_state = init_test(cx);
 587        app_state
 588            .fs
 589            .as_fake()
 590            .insert_tree(
 591                "/root",
 592                json!({
 593                    "a": {
 594                        "banana": "",
 595                        "bandana": "",
 596                    }
 597                }),
 598            )
 599            .await;
 600
 601        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
 602        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
 603        cx.dispatch_action(window_id, Toggle);
 604
 605        let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
 606        finder
 607            .update(cx, |finder, cx| {
 608                finder.delegate_mut().update_matches("bna".to_string(), cx)
 609            })
 610            .await;
 611        finder.read_with(cx, |finder, _| {
 612            assert_eq!(finder.delegate().matches.len(), 2);
 613        });
 614
 615        let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
 616        cx.dispatch_action(window_id, SelectNext);
 617        cx.dispatch_action(window_id, Confirm);
 618        active_pane
 619            .condition(cx, |pane, _| pane.active_item().is_some())
 620            .await;
 621        cx.read(|cx| {
 622            let active_item = active_pane.read(cx).active_item().unwrap();
 623            assert_eq!(
 624                active_item
 625                    .as_any()
 626                    .downcast_ref::<Editor>()
 627                    .unwrap()
 628                    .read(cx)
 629                    .title(cx),
 630                "bandana"
 631            );
 632        });
 633    }
 634
 635    #[gpui::test]
 636    async fn test_row_column_numbers_query_inside_file(cx: &mut TestAppContext) {
 637        let app_state = init_test(cx);
 638
 639        let first_file_name = "first.rs";
 640        let first_file_contents = "// First Rust file";
 641        app_state
 642            .fs
 643            .as_fake()
 644            .insert_tree(
 645                "/src",
 646                json!({
 647                    "test": {
 648                        first_file_name: first_file_contents,
 649                        "second.rs": "// Second Rust file",
 650                    }
 651                }),
 652            )
 653            .await;
 654
 655        let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
 656        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
 657        cx.dispatch_action(window_id, Toggle);
 658        let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
 659
 660        let file_query = &first_file_name[..3];
 661        let file_row = 1;
 662        let file_column = 3;
 663        assert!(file_column <= first_file_contents.len());
 664        let query_inside_file = format!("{file_query}:{file_row}:{file_column}");
 665        finder
 666            .update(cx, |finder, cx| {
 667                finder
 668                    .delegate_mut()
 669                    .update_matches(query_inside_file.to_string(), cx)
 670            })
 671            .await;
 672        finder.read_with(cx, |finder, _| {
 673            let finder = finder.delegate();
 674            assert_eq!(finder.matches.len(), 1);
 675            let latest_search_query = finder
 676                .latest_search_query
 677                .as_ref()
 678                .expect("Finder should have a query after the update_matches call");
 679            assert_eq!(latest_search_query.path_like.raw_query, query_inside_file);
 680            assert_eq!(
 681                latest_search_query.path_like.file_query_end,
 682                Some(file_query.len())
 683            );
 684            assert_eq!(latest_search_query.row, Some(file_row));
 685            assert_eq!(latest_search_query.column, Some(file_column as u32));
 686        });
 687
 688        let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
 689        cx.dispatch_action(window_id, SelectNext);
 690        cx.dispatch_action(window_id, Confirm);
 691        active_pane
 692            .condition(cx, |pane, _| pane.active_item().is_some())
 693            .await;
 694        let editor = cx.update(|cx| {
 695            let active_item = active_pane.read(cx).active_item().unwrap();
 696            active_item.downcast::<Editor>().unwrap()
 697        });
 698        cx.foreground().advance_clock(Duration::from_secs(2));
 699        cx.foreground().start_waiting();
 700        cx.foreground().finish_waiting();
 701        editor.update(cx, |editor, cx| {
 702            let all_selections = editor.selections.all_adjusted(cx);
 703            assert_eq!(
 704                all_selections.len(),
 705                1,
 706                "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}"
 707            );
 708            let caret_selection = all_selections.into_iter().next().unwrap();
 709            assert_eq!(caret_selection.start, caret_selection.end,
 710                "Caret selection should have its start and end at the same position");
 711            assert_eq!(file_row, caret_selection.start.row + 1,
 712                "Query inside file should get caret with the same focus row");
 713            assert_eq!(file_column, caret_selection.start.column as usize + 1,
 714                "Query inside file should get caret with the same focus column");
 715        });
 716    }
 717
 718    #[gpui::test]
 719    async fn test_row_column_numbers_query_outside_file(cx: &mut TestAppContext) {
 720        let app_state = init_test(cx);
 721
 722        let first_file_name = "first.rs";
 723        let first_file_contents = "// First Rust file";
 724        app_state
 725            .fs
 726            .as_fake()
 727            .insert_tree(
 728                "/src",
 729                json!({
 730                    "test": {
 731                        first_file_name: first_file_contents,
 732                        "second.rs": "// Second Rust file",
 733                    }
 734                }),
 735            )
 736            .await;
 737
 738        let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
 739        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
 740        cx.dispatch_action(window_id, Toggle);
 741        let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
 742
 743        let file_query = &first_file_name[..3];
 744        let file_row = 200;
 745        let file_column = 300;
 746        assert!(file_column > first_file_contents.len());
 747        let query_outside_file = format!("{file_query}:{file_row}:{file_column}");
 748        finder
 749            .update(cx, |finder, cx| {
 750                finder
 751                    .delegate_mut()
 752                    .update_matches(query_outside_file.to_string(), cx)
 753            })
 754            .await;
 755        finder.read_with(cx, |finder, _| {
 756            let finder = finder.delegate();
 757            assert_eq!(finder.matches.len(), 1);
 758            let latest_search_query = finder
 759                .latest_search_query
 760                .as_ref()
 761                .expect("Finder should have a query after the update_matches call");
 762            assert_eq!(latest_search_query.path_like.raw_query, query_outside_file);
 763            assert_eq!(
 764                latest_search_query.path_like.file_query_end,
 765                Some(file_query.len())
 766            );
 767            assert_eq!(latest_search_query.row, Some(file_row));
 768            assert_eq!(latest_search_query.column, Some(file_column as u32));
 769        });
 770
 771        let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
 772        cx.dispatch_action(window_id, SelectNext);
 773        cx.dispatch_action(window_id, Confirm);
 774        active_pane
 775            .condition(cx, |pane, _| pane.active_item().is_some())
 776            .await;
 777        let editor = cx.update(|cx| {
 778            let active_item = active_pane.read(cx).active_item().unwrap();
 779            active_item.downcast::<Editor>().unwrap()
 780        });
 781        cx.foreground().advance_clock(Duration::from_secs(2));
 782        cx.foreground().start_waiting();
 783        cx.foreground().finish_waiting();
 784        editor.update(cx, |editor, cx| {
 785            let all_selections = editor.selections.all_adjusted(cx);
 786            assert_eq!(
 787                all_selections.len(),
 788                1,
 789                "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}"
 790            );
 791            let caret_selection = all_selections.into_iter().next().unwrap();
 792            assert_eq!(caret_selection.start, caret_selection.end,
 793                "Caret selection should have its start and end at the same position");
 794            assert_eq!(0, caret_selection.start.row,
 795                "Excessive rows (as in query outside file borders) should get trimmed to last file row");
 796            assert_eq!(first_file_contents.len(), caret_selection.start.column as usize,
 797                "Excessive columns (as in query outside file borders) should get trimmed to selected row's last column");
 798        });
 799    }
 800
 801    #[gpui::test]
 802    async fn test_matching_cancellation(cx: &mut TestAppContext) {
 803        let app_state = init_test(cx);
 804        app_state
 805            .fs
 806            .as_fake()
 807            .insert_tree(
 808                "/dir",
 809                json!({
 810                    "hello": "",
 811                    "goodbye": "",
 812                    "halogen-light": "",
 813                    "happiness": "",
 814                    "height": "",
 815                    "hi": "",
 816                    "hiccup": "",
 817                }),
 818            )
 819            .await;
 820
 821        let project = Project::test(app_state.fs.clone(), ["/dir".as_ref()], cx).await;
 822        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
 823        let (_, finder) = cx.add_window(|cx| {
 824            Picker::new(
 825                FileFinderDelegate::new(
 826                    workspace.downgrade(),
 827                    workspace.read(cx).project().clone(),
 828                    None,
 829                    Vec::new(),
 830                    cx,
 831                ),
 832                cx,
 833            )
 834        });
 835
 836        let query = test_path_like("hi");
 837        finder
 838            .update(cx, |f, cx| f.delegate_mut().spawn_search(query.clone(), cx))
 839            .await;
 840        finder.read_with(cx, |f, _| assert_eq!(f.delegate().matches.len(), 5));
 841
 842        finder.update(cx, |finder, cx| {
 843            let delegate = finder.delegate_mut();
 844            let matches = match &delegate.matches {
 845                Matches::Search(path_matches) => path_matches,
 846                _ => panic!("Search matches expected"),
 847            }
 848            .clone();
 849
 850            // Simulate a search being cancelled after the time limit,
 851            // returning only a subset of the matches that would have been found.
 852            drop(delegate.spawn_search(query.clone(), cx));
 853            delegate.set_search_matches(
 854                delegate.latest_search_id,
 855                true, // did-cancel
 856                query.clone(),
 857                vec![matches[1].clone(), matches[3].clone()],
 858                cx,
 859            );
 860
 861            // Simulate another cancellation.
 862            drop(delegate.spawn_search(query.clone(), cx));
 863            delegate.set_search_matches(
 864                delegate.latest_search_id,
 865                true, // did-cancel
 866                query.clone(),
 867                vec![matches[0].clone(), matches[2].clone(), matches[3].clone()],
 868                cx,
 869            );
 870
 871            match &delegate.matches {
 872                Matches::Search(new_matches) => {
 873                    assert_eq!(new_matches.as_slice(), &matches[0..4])
 874                }
 875                _ => panic!("Search matches expected"),
 876            };
 877        });
 878    }
 879
 880    #[gpui::test]
 881    async fn test_ignored_files(cx: &mut TestAppContext) {
 882        let app_state = init_test(cx);
 883        app_state
 884            .fs
 885            .as_fake()
 886            .insert_tree(
 887                "/ancestor",
 888                json!({
 889                    ".gitignore": "ignored-root",
 890                    "ignored-root": {
 891                        "happiness": "",
 892                        "height": "",
 893                        "hi": "",
 894                        "hiccup": "",
 895                    },
 896                    "tracked-root": {
 897                        ".gitignore": "height",
 898                        "happiness": "",
 899                        "height": "",
 900                        "hi": "",
 901                        "hiccup": "",
 902                    },
 903                }),
 904            )
 905            .await;
 906
 907        let project = Project::test(
 908            app_state.fs.clone(),
 909            [
 910                "/ancestor/tracked-root".as_ref(),
 911                "/ancestor/ignored-root".as_ref(),
 912            ],
 913            cx,
 914        )
 915        .await;
 916        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
 917        let (_, finder) = cx.add_window(|cx| {
 918            Picker::new(
 919                FileFinderDelegate::new(
 920                    workspace.downgrade(),
 921                    workspace.read(cx).project().clone(),
 922                    None,
 923                    Vec::new(),
 924                    cx,
 925                ),
 926                cx,
 927            )
 928        });
 929        finder
 930            .update(cx, |f, cx| {
 931                f.delegate_mut().spawn_search(test_path_like("hi"), cx)
 932            })
 933            .await;
 934        finder.read_with(cx, |f, _| assert_eq!(f.delegate().matches.len(), 7));
 935    }
 936
 937    #[gpui::test]
 938    async fn test_single_file_worktrees(cx: &mut TestAppContext) {
 939        let app_state = init_test(cx);
 940        app_state
 941            .fs
 942            .as_fake()
 943            .insert_tree("/root", json!({ "the-parent-dir": { "the-file": "" } }))
 944            .await;
 945
 946        let project = Project::test(
 947            app_state.fs.clone(),
 948            ["/root/the-parent-dir/the-file".as_ref()],
 949            cx,
 950        )
 951        .await;
 952        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
 953        let (_, finder) = cx.add_window(|cx| {
 954            Picker::new(
 955                FileFinderDelegate::new(
 956                    workspace.downgrade(),
 957                    workspace.read(cx).project().clone(),
 958                    None,
 959                    Vec::new(),
 960                    cx,
 961                ),
 962                cx,
 963            )
 964        });
 965
 966        // Even though there is only one worktree, that worktree's filename
 967        // is included in the matching, because the worktree is a single file.
 968        finder
 969            .update(cx, |f, cx| {
 970                f.delegate_mut().spawn_search(test_path_like("thf"), cx)
 971            })
 972            .await;
 973        cx.read(|cx| {
 974            let finder = finder.read(cx);
 975            let delegate = finder.delegate();
 976            let matches = match &delegate.matches {
 977                Matches::Search(path_matches) => path_matches,
 978                _ => panic!("Search matches expected"),
 979            };
 980            assert_eq!(matches.len(), 1);
 981
 982            let (file_name, file_name_positions, full_path, full_path_positions) =
 983                delegate.labels_for_path_match(&matches[0]);
 984            assert_eq!(file_name, "the-file");
 985            assert_eq!(file_name_positions, &[0, 1, 4]);
 986            assert_eq!(full_path, "the-file");
 987            assert_eq!(full_path_positions, &[0, 1, 4]);
 988        });
 989
 990        // Since the worktree root is a file, searching for its name followed by a slash does
 991        // not match anything.
 992        finder
 993            .update(cx, |f, cx| {
 994                f.delegate_mut().spawn_search(test_path_like("thf/"), cx)
 995            })
 996            .await;
 997        finder.read_with(cx, |f, _| assert_eq!(f.delegate().matches.len(), 0));
 998    }
 999
1000    #[gpui::test]
1001    async fn test_multiple_matches_with_same_relative_path(cx: &mut TestAppContext) {
1002        let app_state = init_test(cx);
1003        app_state
1004            .fs
1005            .as_fake()
1006            .insert_tree(
1007                "/root",
1008                json!({
1009                    "dir1": { "a.txt": "" },
1010                    "dir2": { "a.txt": "" }
1011                }),
1012            )
1013            .await;
1014
1015        let project = Project::test(
1016            app_state.fs.clone(),
1017            ["/root/dir1".as_ref(), "/root/dir2".as_ref()],
1018            cx,
1019        )
1020        .await;
1021        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
1022
1023        let (_, finder) = cx.add_window(|cx| {
1024            Picker::new(
1025                FileFinderDelegate::new(
1026                    workspace.downgrade(),
1027                    workspace.read(cx).project().clone(),
1028                    None,
1029                    Vec::new(),
1030                    cx,
1031                ),
1032                cx,
1033            )
1034        });
1035
1036        // Run a search that matches two files with the same relative path.
1037        finder
1038            .update(cx, |f, cx| {
1039                f.delegate_mut().spawn_search(test_path_like("a.t"), cx)
1040            })
1041            .await;
1042
1043        // Can switch between different matches with the same relative path.
1044        finder.update(cx, |finder, cx| {
1045            let delegate = finder.delegate_mut();
1046            assert_eq!(delegate.matches.len(), 2);
1047            assert_eq!(delegate.selected_index(), 0);
1048            delegate.set_selected_index(1, cx);
1049            assert_eq!(delegate.selected_index(), 1);
1050            delegate.set_selected_index(0, cx);
1051            assert_eq!(delegate.selected_index(), 0);
1052        });
1053    }
1054
1055    #[gpui::test]
1056    async fn test_path_distance_ordering(cx: &mut TestAppContext) {
1057        let app_state = init_test(cx);
1058        app_state
1059            .fs
1060            .as_fake()
1061            .insert_tree(
1062                "/root",
1063                json!({
1064                    "dir1": { "a.txt": "" },
1065                    "dir2": {
1066                        "a.txt": "",
1067                        "b.txt": ""
1068                    }
1069                }),
1070            )
1071            .await;
1072
1073        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1074        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
1075        let worktree_id = cx.read(|cx| {
1076            let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1077            assert_eq!(worktrees.len(), 1);
1078            WorktreeId::from_usize(worktrees[0].id())
1079        });
1080
1081        // When workspace has an active item, sort items which are closer to that item
1082        // first when they have the same name. In this case, b.txt is closer to dir2's a.txt
1083        // so that one should be sorted earlier
1084        let b_path = Some(dummy_found_path(ProjectPath {
1085            worktree_id,
1086            path: Arc::from(Path::new("/root/dir2/b.txt")),
1087        }));
1088        let (_, finder) = cx.add_window(|cx| {
1089            Picker::new(
1090                FileFinderDelegate::new(
1091                    workspace.downgrade(),
1092                    workspace.read(cx).project().clone(),
1093                    b_path,
1094                    Vec::new(),
1095                    cx,
1096                ),
1097                cx,
1098            )
1099        });
1100
1101        finder
1102            .update(cx, |f, cx| {
1103                f.delegate_mut().spawn_search(test_path_like("a.txt"), cx)
1104            })
1105            .await;
1106
1107        finder.read_with(cx, |f, _| {
1108            let delegate = f.delegate();
1109            let matches = match &delegate.matches {
1110                Matches::Search(path_matches) => path_matches,
1111                _ => panic!("Search matches expected"),
1112            };
1113            assert_eq!(matches[0].path.as_ref(), Path::new("dir2/a.txt"));
1114            assert_eq!(matches[1].path.as_ref(), Path::new("dir1/a.txt"));
1115        });
1116    }
1117
1118    #[gpui::test]
1119    async fn test_search_worktree_without_files(cx: &mut TestAppContext) {
1120        let app_state = init_test(cx);
1121        app_state
1122            .fs
1123            .as_fake()
1124            .insert_tree(
1125                "/root",
1126                json!({
1127                    "dir1": {},
1128                    "dir2": {
1129                        "dir3": {}
1130                    }
1131                }),
1132            )
1133            .await;
1134
1135        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1136        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
1137        let (_, finder) = cx.add_window(|cx| {
1138            Picker::new(
1139                FileFinderDelegate::new(
1140                    workspace.downgrade(),
1141                    workspace.read(cx).project().clone(),
1142                    None,
1143                    Vec::new(),
1144                    cx,
1145                ),
1146                cx,
1147            )
1148        });
1149        finder
1150            .update(cx, |f, cx| {
1151                f.delegate_mut().spawn_search(test_path_like("dir"), cx)
1152            })
1153            .await;
1154        cx.read(|cx| {
1155            let finder = finder.read(cx);
1156            assert_eq!(finder.delegate().matches.len(), 0);
1157        });
1158    }
1159
1160    #[gpui::test]
1161    async fn test_query_history(
1162        deterministic: Arc<gpui::executor::Deterministic>,
1163        cx: &mut gpui::TestAppContext,
1164    ) {
1165        let app_state = init_test(cx);
1166
1167        app_state
1168            .fs
1169            .as_fake()
1170            .insert_tree(
1171                "/src",
1172                json!({
1173                    "test": {
1174                        "first.rs": "// First Rust file",
1175                        "second.rs": "// Second Rust file",
1176                        "third.rs": "// Third Rust file",
1177                    }
1178                }),
1179            )
1180            .await;
1181
1182        let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
1183        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
1184        let worktree_id = cx.read(|cx| {
1185            let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1186            assert_eq!(worktrees.len(), 1);
1187            WorktreeId::from_usize(worktrees[0].id())
1188        });
1189
1190        // Open and close panels, getting their history items afterwards.
1191        // Ensure history items get populated with opened items, and items are kept in a certain order.
1192        // The history lags one opened buffer behind, since it's updated in the search panel only on its reopen.
1193        //
1194        // TODO: without closing, the opened items do not propagate their history changes for some reason
1195        // it does work in real app though, only tests do not propagate.
1196
1197        let initial_history = open_close_queried_buffer(
1198            "fir",
1199            1,
1200            "first.rs",
1201            window_id,
1202            &workspace,
1203            &deterministic,
1204            cx,
1205        )
1206        .await;
1207        assert!(
1208            initial_history.is_empty(),
1209            "Should have no history before opening any files"
1210        );
1211
1212        let history_after_first = open_close_queried_buffer(
1213            "sec",
1214            1,
1215            "second.rs",
1216            window_id,
1217            &workspace,
1218            &deterministic,
1219            cx,
1220        )
1221        .await;
1222        assert_eq!(
1223            history_after_first,
1224            vec![FoundPath::new(
1225                ProjectPath {
1226                    worktree_id,
1227                    path: Arc::from(Path::new("test/first.rs")),
1228                },
1229                Some(PathBuf::from("/src/test/first.rs"))
1230            )],
1231            "Should show 1st opened item in the history when opening the 2nd item"
1232        );
1233
1234        let history_after_second = open_close_queried_buffer(
1235            "thi",
1236            1,
1237            "third.rs",
1238            window_id,
1239            &workspace,
1240            &deterministic,
1241            cx,
1242        )
1243        .await;
1244        assert_eq!(
1245            history_after_second,
1246            vec![
1247                FoundPath::new(
1248                    ProjectPath {
1249                        worktree_id,
1250                        path: Arc::from(Path::new("test/second.rs")),
1251                    },
1252                    Some(PathBuf::from("/src/test/second.rs"))
1253                ),
1254                FoundPath::new(
1255                    ProjectPath {
1256                        worktree_id,
1257                        path: Arc::from(Path::new("test/first.rs")),
1258                    },
1259                    Some(PathBuf::from("/src/test/first.rs"))
1260                ),
1261            ],
1262            "Should show 1st and 2nd opened items in the history when opening the 3rd item. \
12632nd item should be the first in the history, as the last opened."
1264        );
1265
1266        let history_after_third = open_close_queried_buffer(
1267            "sec",
1268            1,
1269            "second.rs",
1270            window_id,
1271            &workspace,
1272            &deterministic,
1273            cx,
1274        )
1275        .await;
1276        assert_eq!(
1277            history_after_third,
1278            vec![
1279                FoundPath::new(
1280                    ProjectPath {
1281                        worktree_id,
1282                        path: Arc::from(Path::new("test/third.rs")),
1283                    },
1284                    Some(PathBuf::from("/src/test/third.rs"))
1285                ),
1286                FoundPath::new(
1287                    ProjectPath {
1288                        worktree_id,
1289                        path: Arc::from(Path::new("test/second.rs")),
1290                    },
1291                    Some(PathBuf::from("/src/test/second.rs"))
1292                ),
1293                FoundPath::new(
1294                    ProjectPath {
1295                        worktree_id,
1296                        path: Arc::from(Path::new("test/first.rs")),
1297                    },
1298                    Some(PathBuf::from("/src/test/first.rs"))
1299                ),
1300            ],
1301            "Should show 1st, 2nd and 3rd opened items in the history when opening the 2nd item again. \
13023rd item should be the first in the history, as the last opened."
1303        );
1304
1305        let history_after_second_again = open_close_queried_buffer(
1306            "thi",
1307            1,
1308            "third.rs",
1309            window_id,
1310            &workspace,
1311            &deterministic,
1312            cx,
1313        )
1314        .await;
1315        assert_eq!(
1316            history_after_second_again,
1317            vec![
1318                FoundPath::new(
1319                    ProjectPath {
1320                        worktree_id,
1321                        path: Arc::from(Path::new("test/second.rs")),
1322                    },
1323                    Some(PathBuf::from("/src/test/second.rs"))
1324                ),
1325                FoundPath::new(
1326                    ProjectPath {
1327                        worktree_id,
1328                        path: Arc::from(Path::new("test/third.rs")),
1329                    },
1330                    Some(PathBuf::from("/src/test/third.rs"))
1331                ),
1332                FoundPath::new(
1333                    ProjectPath {
1334                        worktree_id,
1335                        path: Arc::from(Path::new("test/first.rs")),
1336                    },
1337                    Some(PathBuf::from("/src/test/first.rs"))
1338                ),
1339            ],
1340            "Should show 1st, 2nd and 3rd opened items in the history when opening the 3rd item again. \
13412nd item, as the last opened, 3rd item should go next as it was opened right before."
1342        );
1343    }
1344
1345    #[gpui::test]
1346    async fn test_external_files_history(
1347        deterministic: Arc<gpui::executor::Deterministic>,
1348        cx: &mut gpui::TestAppContext,
1349    ) {
1350        let app_state = init_test(cx);
1351
1352        app_state
1353            .fs
1354            .as_fake()
1355            .insert_tree(
1356                "/src",
1357                json!({
1358                    "test": {
1359                        "first.rs": "// First Rust file",
1360                        "second.rs": "// Second Rust file",
1361                    }
1362                }),
1363            )
1364            .await;
1365
1366        app_state
1367            .fs
1368            .as_fake()
1369            .insert_tree(
1370                "/external-src",
1371                json!({
1372                    "test": {
1373                        "third.rs": "// Third Rust file",
1374                        "fourth.rs": "// Fourth Rust file",
1375                    }
1376                }),
1377            )
1378            .await;
1379
1380        let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
1381        cx.update(|cx| {
1382            project.update(cx, |project, cx| {
1383                project.find_or_create_local_worktree("/external-src", false, cx)
1384            })
1385        })
1386        .detach();
1387        deterministic.run_until_parked();
1388
1389        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
1390        let worktree_id = cx.read(|cx| {
1391            let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1392            assert_eq!(worktrees.len(), 1,);
1393
1394            WorktreeId::from_usize(worktrees[0].id())
1395        });
1396        workspace
1397            .update(cx, |workspace, cx| {
1398                workspace.open_abs_path(PathBuf::from("/external-src/test/third.rs"), false, cx)
1399            })
1400            .detach();
1401        deterministic.run_until_parked();
1402        let external_worktree_id = cx.read(|cx| {
1403            let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1404            assert_eq!(
1405                worktrees.len(),
1406                2,
1407                "External file should get opened in a new worktree"
1408            );
1409
1410            WorktreeId::from_usize(
1411                worktrees
1412                    .into_iter()
1413                    .find(|worktree| worktree.id() != worktree_id.to_usize())
1414                    .expect("New worktree should have a different id")
1415                    .id(),
1416            )
1417        });
1418        close_active_item(&workspace, &deterministic, cx).await;
1419
1420        let initial_history_items = open_close_queried_buffer(
1421            "sec",
1422            1,
1423            "second.rs",
1424            window_id,
1425            &workspace,
1426            &deterministic,
1427            cx,
1428        )
1429        .await;
1430        assert_eq!(
1431            initial_history_items,
1432            vec![FoundPath::new(
1433                ProjectPath {
1434                    worktree_id: external_worktree_id,
1435                    path: Arc::from(Path::new("")),
1436                },
1437                Some(PathBuf::from("/external-src/test/third.rs"))
1438            )],
1439            "Should show external file with its full path in the history after it was open"
1440        );
1441
1442        let updated_history_items = open_close_queried_buffer(
1443            "fir",
1444            1,
1445            "first.rs",
1446            window_id,
1447            &workspace,
1448            &deterministic,
1449            cx,
1450        )
1451        .await;
1452        assert_eq!(
1453            updated_history_items,
1454            vec![
1455                FoundPath::new(
1456                    ProjectPath {
1457                        worktree_id,
1458                        path: Arc::from(Path::new("test/second.rs")),
1459                    },
1460                    Some(PathBuf::from("/src/test/second.rs"))
1461                ),
1462                FoundPath::new(
1463                    ProjectPath {
1464                        worktree_id: external_worktree_id,
1465                        path: Arc::from(Path::new("")),
1466                    },
1467                    Some(PathBuf::from("/external-src/test/third.rs"))
1468                ),
1469            ],
1470            "Should keep external file with history updates",
1471        );
1472    }
1473
1474    async fn open_close_queried_buffer(
1475        input: &str,
1476        expected_matches: usize,
1477        expected_editor_title: &str,
1478        window_id: usize,
1479        workspace: &ViewHandle<Workspace>,
1480        deterministic: &gpui::executor::Deterministic,
1481        cx: &mut gpui::TestAppContext,
1482    ) -> Vec<FoundPath> {
1483        cx.dispatch_action(window_id, Toggle);
1484        let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
1485        finder
1486            .update(cx, |finder, cx| {
1487                finder.delegate_mut().update_matches(input.to_string(), cx)
1488            })
1489            .await;
1490        let history_items = finder.read_with(cx, |finder, _| {
1491            assert_eq!(
1492                finder.delegate().matches.len(),
1493                expected_matches,
1494                "Unexpected number of matches found for query {input}"
1495            );
1496            finder.delegate().history_items.clone()
1497        });
1498
1499        let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
1500        cx.dispatch_action(window_id, SelectNext);
1501        cx.dispatch_action(window_id, Confirm);
1502        deterministic.run_until_parked();
1503        active_pane
1504            .condition(cx, |pane, _| pane.active_item().is_some())
1505            .await;
1506        cx.read(|cx| {
1507            let active_item = active_pane.read(cx).active_item().unwrap();
1508            let active_editor_title = active_item
1509                .as_any()
1510                .downcast_ref::<Editor>()
1511                .unwrap()
1512                .read(cx)
1513                .title(cx);
1514            assert_eq!(
1515                expected_editor_title, active_editor_title,
1516                "Unexpected editor title for query {input}"
1517            );
1518        });
1519
1520        close_active_item(workspace, deterministic, cx).await;
1521
1522        history_items
1523    }
1524
1525    async fn close_active_item(
1526        workspace: &ViewHandle<Workspace>,
1527        deterministic: &gpui::executor::Deterministic,
1528        cx: &mut TestAppContext,
1529    ) {
1530        let mut original_items = HashMap::new();
1531        cx.read(|cx| {
1532            for pane in workspace.read(cx).panes() {
1533                let pane_id = pane.id();
1534                let pane = pane.read(cx);
1535                let insertion_result = original_items.insert(pane_id, pane.items().count());
1536                assert!(insertion_result.is_none(), "Pane id {pane_id} collision");
1537            }
1538        });
1539
1540        let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
1541        active_pane
1542            .update(cx, |pane, cx| {
1543                pane.close_active_item(&workspace::CloseActiveItem, cx)
1544                    .unwrap()
1545            })
1546            .await
1547            .unwrap();
1548        deterministic.run_until_parked();
1549        cx.read(|cx| {
1550            for pane in workspace.read(cx).panes() {
1551                let pane_id = pane.id();
1552                let pane = pane.read(cx);
1553                match original_items.remove(&pane_id) {
1554                    Some(original_items) => {
1555                        assert_eq!(
1556                            pane.items().count(),
1557                            original_items.saturating_sub(1),
1558                            "Pane id {pane_id} should have item closed"
1559                        );
1560                    }
1561                    None => panic!("Pane id {pane_id} not found in original items"),
1562                }
1563            }
1564        });
1565        assert!(
1566            original_items.len() <= 1,
1567            "At most one panel should got closed"
1568        );
1569    }
1570
1571    fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
1572        cx.foreground().forbid_parking();
1573        cx.update(|cx| {
1574            let state = AppState::test(cx);
1575            theme::init((), cx);
1576            language::init(cx);
1577            super::init(cx);
1578            editor::init(cx);
1579            workspace::init_settings(cx);
1580            Project::init_settings(cx);
1581            state
1582        })
1583    }
1584
1585    fn test_path_like(test_str: &str) -> PathLikeWithPosition<FileSearchQuery> {
1586        PathLikeWithPosition::parse_str(test_str, |path_like_str| {
1587            Ok::<_, std::convert::Infallible>(FileSearchQuery {
1588                raw_query: test_str.to_owned(),
1589                file_query_end: if path_like_str == test_str {
1590                    None
1591                } else {
1592                    Some(path_like_str.len())
1593                },
1594            })
1595        })
1596        .unwrap()
1597    }
1598
1599    fn dummy_found_path(project_path: ProjectPath) -> FoundPath {
1600        FoundPath {
1601            project: project_path,
1602            absolute: None,
1603        }
1604    }
1605}