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, secondary: bool, 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, move |workspace, cx| {
 449                    let split_or_open = |workspace: &mut Workspace, project_path, cx| {
 450                        if secondary {
 451                            workspace.split_path(project_path, cx)
 452                        } else {
 453                            workspace.open_path(project_path, None, true, cx)
 454                        }
 455                    };
 456                    match m {
 457                        Match::History(history_match) => {
 458                            let worktree_id = history_match.project.worktree_id;
 459                            if workspace
 460                                .project()
 461                                .read(cx)
 462                                .worktree_for_id(worktree_id, cx)
 463                                .is_some()
 464                            {
 465                                split_or_open(
 466                                    workspace,
 467                                    ProjectPath {
 468                                        worktree_id,
 469                                        path: Arc::clone(&history_match.project.path),
 470                                    },
 471                                    cx,
 472                                )
 473                            } else {
 474                                match history_match.absolute.as_ref() {
 475                                    Some(abs_path) => {
 476                                        if secondary {
 477                                            workspace.split_abs_path(
 478                                                abs_path.to_path_buf(),
 479                                                false,
 480                                                cx,
 481                                            )
 482                                        } else {
 483                                            workspace.open_abs_path(
 484                                                abs_path.to_path_buf(),
 485                                                false,
 486                                                cx,
 487                                            )
 488                                        }
 489                                    }
 490                                    None => split_or_open(
 491                                        workspace,
 492                                        ProjectPath {
 493                                            worktree_id,
 494                                            path: Arc::clone(&history_match.project.path),
 495                                        },
 496                                        cx,
 497                                    ),
 498                                }
 499                            }
 500                        }
 501                        Match::Search(m) => split_or_open(
 502                            workspace,
 503                            ProjectPath {
 504                                worktree_id: WorktreeId::from_usize(m.worktree_id),
 505                                path: m.path.clone(),
 506                            },
 507                            cx,
 508                        ),
 509                    }
 510                });
 511
 512                let row = self
 513                    .latest_search_query
 514                    .as_ref()
 515                    .and_then(|query| query.row)
 516                    .map(|row| row.saturating_sub(1));
 517                let col = self
 518                    .latest_search_query
 519                    .as_ref()
 520                    .and_then(|query| query.column)
 521                    .unwrap_or(0)
 522                    .saturating_sub(1);
 523                cx.spawn(|_, mut cx| async move {
 524                    let item = open_task.await.log_err()?;
 525                    if let Some(row) = row {
 526                        if let Some(active_editor) = item.downcast::<Editor>() {
 527                            active_editor
 528                                .downgrade()
 529                                .update(&mut cx, |editor, cx| {
 530                                    let snapshot = editor.snapshot(cx).display_snapshot;
 531                                    let point = snapshot
 532                                        .buffer_snapshot
 533                                        .clip_point(Point::new(row, col), Bias::Left);
 534                                    editor.change_selections(Some(Autoscroll::center()), cx, |s| {
 535                                        s.select_ranges([point..point])
 536                                    });
 537                                })
 538                                .log_err();
 539                        }
 540                    }
 541                    workspace
 542                        .downgrade()
 543                        .update(&mut cx, |workspace, cx| workspace.dismiss_modal(cx))
 544                        .log_err();
 545
 546                    Some(())
 547                })
 548                .detach();
 549            }
 550        }
 551    }
 552
 553    fn dismissed(&mut self, _: &mut ViewContext<FileFinder>) {}
 554
 555    fn render_match(
 556        &self,
 557        ix: usize,
 558        mouse_state: &mut MouseState,
 559        selected: bool,
 560        cx: &AppContext,
 561    ) -> AnyElement<Picker<Self>> {
 562        let path_match = self
 563            .matches
 564            .get(ix)
 565            .expect("Invalid matches state: no element for index {ix}");
 566        let theme = theme::current(cx);
 567        let style = theme.picker.item.in_state(selected).style_for(mouse_state);
 568        let (file_name, file_name_positions, full_path, full_path_positions) =
 569            self.labels_for_match(path_match, cx, ix);
 570        Flex::column()
 571            .with_child(
 572                Label::new(file_name, style.label.clone()).with_highlights(file_name_positions),
 573            )
 574            .with_child(
 575                Label::new(full_path, style.label.clone()).with_highlights(full_path_positions),
 576            )
 577            .flex(1., false)
 578            .contained()
 579            .with_style(style.container)
 580            .into_any_named("match")
 581    }
 582}
 583
 584#[cfg(test)]
 585mod tests {
 586    use std::{assert_eq, collections::HashMap, path::Path, time::Duration};
 587
 588    use super::*;
 589    use editor::Editor;
 590    use gpui::{TestAppContext, ViewHandle};
 591    use menu::{Confirm, SelectNext};
 592    use serde_json::json;
 593    use workspace::{AppState, Workspace};
 594
 595    #[ctor::ctor]
 596    fn init_logger() {
 597        if std::env::var("RUST_LOG").is_ok() {
 598            env_logger::init();
 599        }
 600    }
 601
 602    #[gpui::test]
 603    async fn test_matching_paths(cx: &mut TestAppContext) {
 604        let app_state = init_test(cx);
 605        app_state
 606            .fs
 607            .as_fake()
 608            .insert_tree(
 609                "/root",
 610                json!({
 611                    "a": {
 612                        "banana": "",
 613                        "bandana": "",
 614                    }
 615                }),
 616            )
 617            .await;
 618
 619        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
 620        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
 621        cx.dispatch_action(window_id, Toggle);
 622
 623        let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
 624        finder
 625            .update(cx, |finder, cx| {
 626                finder.delegate_mut().update_matches("bna".to_string(), cx)
 627            })
 628            .await;
 629        finder.read_with(cx, |finder, _| {
 630            assert_eq!(finder.delegate().matches.len(), 2);
 631        });
 632
 633        let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
 634        cx.dispatch_action(window_id, SelectNext);
 635        cx.dispatch_action(window_id, Confirm);
 636        active_pane
 637            .condition(cx, |pane, _| pane.active_item().is_some())
 638            .await;
 639        cx.read(|cx| {
 640            let active_item = active_pane.read(cx).active_item().unwrap();
 641            assert_eq!(
 642                active_item
 643                    .as_any()
 644                    .downcast_ref::<Editor>()
 645                    .unwrap()
 646                    .read(cx)
 647                    .title(cx),
 648                "bandana"
 649            );
 650        });
 651    }
 652
 653    #[gpui::test]
 654    async fn test_row_column_numbers_query_inside_file(cx: &mut TestAppContext) {
 655        let app_state = init_test(cx);
 656
 657        let first_file_name = "first.rs";
 658        let first_file_contents = "// First Rust file";
 659        app_state
 660            .fs
 661            .as_fake()
 662            .insert_tree(
 663                "/src",
 664                json!({
 665                    "test": {
 666                        first_file_name: first_file_contents,
 667                        "second.rs": "// Second Rust file",
 668                    }
 669                }),
 670            )
 671            .await;
 672
 673        let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
 674        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
 675        cx.dispatch_action(window_id, Toggle);
 676        let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
 677
 678        let file_query = &first_file_name[..3];
 679        let file_row = 1;
 680        let file_column = 3;
 681        assert!(file_column <= first_file_contents.len());
 682        let query_inside_file = format!("{file_query}:{file_row}:{file_column}");
 683        finder
 684            .update(cx, |finder, cx| {
 685                finder
 686                    .delegate_mut()
 687                    .update_matches(query_inside_file.to_string(), cx)
 688            })
 689            .await;
 690        finder.read_with(cx, |finder, _| {
 691            let finder = finder.delegate();
 692            assert_eq!(finder.matches.len(), 1);
 693            let latest_search_query = finder
 694                .latest_search_query
 695                .as_ref()
 696                .expect("Finder should have a query after the update_matches call");
 697            assert_eq!(latest_search_query.path_like.raw_query, query_inside_file);
 698            assert_eq!(
 699                latest_search_query.path_like.file_query_end,
 700                Some(file_query.len())
 701            );
 702            assert_eq!(latest_search_query.row, Some(file_row));
 703            assert_eq!(latest_search_query.column, Some(file_column as u32));
 704        });
 705
 706        let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
 707        cx.dispatch_action(window_id, SelectNext);
 708        cx.dispatch_action(window_id, Confirm);
 709        active_pane
 710            .condition(cx, |pane, _| pane.active_item().is_some())
 711            .await;
 712        let editor = cx.update(|cx| {
 713            let active_item = active_pane.read(cx).active_item().unwrap();
 714            active_item.downcast::<Editor>().unwrap()
 715        });
 716        cx.foreground().advance_clock(Duration::from_secs(2));
 717        cx.foreground().start_waiting();
 718        cx.foreground().finish_waiting();
 719        editor.update(cx, |editor, cx| {
 720            let all_selections = editor.selections.all_adjusted(cx);
 721            assert_eq!(
 722                all_selections.len(),
 723                1,
 724                "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}"
 725            );
 726            let caret_selection = all_selections.into_iter().next().unwrap();
 727            assert_eq!(caret_selection.start, caret_selection.end,
 728                "Caret selection should have its start and end at the same position");
 729            assert_eq!(file_row, caret_selection.start.row + 1,
 730                "Query inside file should get caret with the same focus row");
 731            assert_eq!(file_column, caret_selection.start.column as usize + 1,
 732                "Query inside file should get caret with the same focus column");
 733        });
 734    }
 735
 736    #[gpui::test]
 737    async fn test_row_column_numbers_query_outside_file(cx: &mut TestAppContext) {
 738        let app_state = init_test(cx);
 739
 740        let first_file_name = "first.rs";
 741        let first_file_contents = "// First Rust file";
 742        app_state
 743            .fs
 744            .as_fake()
 745            .insert_tree(
 746                "/src",
 747                json!({
 748                    "test": {
 749                        first_file_name: first_file_contents,
 750                        "second.rs": "// Second Rust file",
 751                    }
 752                }),
 753            )
 754            .await;
 755
 756        let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
 757        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
 758        cx.dispatch_action(window_id, Toggle);
 759        let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
 760
 761        let file_query = &first_file_name[..3];
 762        let file_row = 200;
 763        let file_column = 300;
 764        assert!(file_column > first_file_contents.len());
 765        let query_outside_file = format!("{file_query}:{file_row}:{file_column}");
 766        finder
 767            .update(cx, |finder, cx| {
 768                finder
 769                    .delegate_mut()
 770                    .update_matches(query_outside_file.to_string(), cx)
 771            })
 772            .await;
 773        finder.read_with(cx, |finder, _| {
 774            let finder = finder.delegate();
 775            assert_eq!(finder.matches.len(), 1);
 776            let latest_search_query = finder
 777                .latest_search_query
 778                .as_ref()
 779                .expect("Finder should have a query after the update_matches call");
 780            assert_eq!(latest_search_query.path_like.raw_query, query_outside_file);
 781            assert_eq!(
 782                latest_search_query.path_like.file_query_end,
 783                Some(file_query.len())
 784            );
 785            assert_eq!(latest_search_query.row, Some(file_row));
 786            assert_eq!(latest_search_query.column, Some(file_column as u32));
 787        });
 788
 789        let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
 790        cx.dispatch_action(window_id, SelectNext);
 791        cx.dispatch_action(window_id, Confirm);
 792        active_pane
 793            .condition(cx, |pane, _| pane.active_item().is_some())
 794            .await;
 795        let editor = cx.update(|cx| {
 796            let active_item = active_pane.read(cx).active_item().unwrap();
 797            active_item.downcast::<Editor>().unwrap()
 798        });
 799        cx.foreground().advance_clock(Duration::from_secs(2));
 800        cx.foreground().start_waiting();
 801        cx.foreground().finish_waiting();
 802        editor.update(cx, |editor, cx| {
 803            let all_selections = editor.selections.all_adjusted(cx);
 804            assert_eq!(
 805                all_selections.len(),
 806                1,
 807                "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}"
 808            );
 809            let caret_selection = all_selections.into_iter().next().unwrap();
 810            assert_eq!(caret_selection.start, caret_selection.end,
 811                "Caret selection should have its start and end at the same position");
 812            assert_eq!(0, caret_selection.start.row,
 813                "Excessive rows (as in query outside file borders) should get trimmed to last file row");
 814            assert_eq!(first_file_contents.len(), caret_selection.start.column as usize,
 815                "Excessive columns (as in query outside file borders) should get trimmed to selected row's last column");
 816        });
 817    }
 818
 819    #[gpui::test]
 820    async fn test_matching_cancellation(cx: &mut TestAppContext) {
 821        let app_state = init_test(cx);
 822        app_state
 823            .fs
 824            .as_fake()
 825            .insert_tree(
 826                "/dir",
 827                json!({
 828                    "hello": "",
 829                    "goodbye": "",
 830                    "halogen-light": "",
 831                    "happiness": "",
 832                    "height": "",
 833                    "hi": "",
 834                    "hiccup": "",
 835                }),
 836            )
 837            .await;
 838
 839        let project = Project::test(app_state.fs.clone(), ["/dir".as_ref()], cx).await;
 840        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
 841        let (_, finder) = cx.add_window(|cx| {
 842            Picker::new(
 843                FileFinderDelegate::new(
 844                    workspace.downgrade(),
 845                    workspace.read(cx).project().clone(),
 846                    None,
 847                    Vec::new(),
 848                    cx,
 849                ),
 850                cx,
 851            )
 852        });
 853
 854        let query = test_path_like("hi");
 855        finder
 856            .update(cx, |f, cx| f.delegate_mut().spawn_search(query.clone(), cx))
 857            .await;
 858        finder.read_with(cx, |f, _| assert_eq!(f.delegate().matches.len(), 5));
 859
 860        finder.update(cx, |finder, cx| {
 861            let delegate = finder.delegate_mut();
 862            let matches = match &delegate.matches {
 863                Matches::Search(path_matches) => path_matches,
 864                _ => panic!("Search matches expected"),
 865            }
 866            .clone();
 867
 868            // Simulate a search being cancelled after the time limit,
 869            // returning only a subset of the matches that would have been found.
 870            drop(delegate.spawn_search(query.clone(), cx));
 871            delegate.set_search_matches(
 872                delegate.latest_search_id,
 873                true, // did-cancel
 874                query.clone(),
 875                vec![matches[1].clone(), matches[3].clone()],
 876                cx,
 877            );
 878
 879            // Simulate another cancellation.
 880            drop(delegate.spawn_search(query.clone(), cx));
 881            delegate.set_search_matches(
 882                delegate.latest_search_id,
 883                true, // did-cancel
 884                query.clone(),
 885                vec![matches[0].clone(), matches[2].clone(), matches[3].clone()],
 886                cx,
 887            );
 888
 889            match &delegate.matches {
 890                Matches::Search(new_matches) => {
 891                    assert_eq!(new_matches.as_slice(), &matches[0..4])
 892                }
 893                _ => panic!("Search matches expected"),
 894            };
 895        });
 896    }
 897
 898    #[gpui::test]
 899    async fn test_ignored_files(cx: &mut TestAppContext) {
 900        let app_state = init_test(cx);
 901        app_state
 902            .fs
 903            .as_fake()
 904            .insert_tree(
 905                "/ancestor",
 906                json!({
 907                    ".gitignore": "ignored-root",
 908                    "ignored-root": {
 909                        "happiness": "",
 910                        "height": "",
 911                        "hi": "",
 912                        "hiccup": "",
 913                    },
 914                    "tracked-root": {
 915                        ".gitignore": "height",
 916                        "happiness": "",
 917                        "height": "",
 918                        "hi": "",
 919                        "hiccup": "",
 920                    },
 921                }),
 922            )
 923            .await;
 924
 925        let project = Project::test(
 926            app_state.fs.clone(),
 927            [
 928                "/ancestor/tracked-root".as_ref(),
 929                "/ancestor/ignored-root".as_ref(),
 930            ],
 931            cx,
 932        )
 933        .await;
 934        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
 935        let (_, finder) = cx.add_window(|cx| {
 936            Picker::new(
 937                FileFinderDelegate::new(
 938                    workspace.downgrade(),
 939                    workspace.read(cx).project().clone(),
 940                    None,
 941                    Vec::new(),
 942                    cx,
 943                ),
 944                cx,
 945            )
 946        });
 947        finder
 948            .update(cx, |f, cx| {
 949                f.delegate_mut().spawn_search(test_path_like("hi"), cx)
 950            })
 951            .await;
 952        finder.read_with(cx, |f, _| assert_eq!(f.delegate().matches.len(), 7));
 953    }
 954
 955    #[gpui::test]
 956    async fn test_single_file_worktrees(cx: &mut TestAppContext) {
 957        let app_state = init_test(cx);
 958        app_state
 959            .fs
 960            .as_fake()
 961            .insert_tree("/root", json!({ "the-parent-dir": { "the-file": "" } }))
 962            .await;
 963
 964        let project = Project::test(
 965            app_state.fs.clone(),
 966            ["/root/the-parent-dir/the-file".as_ref()],
 967            cx,
 968        )
 969        .await;
 970        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
 971        let (_, finder) = cx.add_window(|cx| {
 972            Picker::new(
 973                FileFinderDelegate::new(
 974                    workspace.downgrade(),
 975                    workspace.read(cx).project().clone(),
 976                    None,
 977                    Vec::new(),
 978                    cx,
 979                ),
 980                cx,
 981            )
 982        });
 983
 984        // Even though there is only one worktree, that worktree's filename
 985        // is included in the matching, because the worktree is a single file.
 986        finder
 987            .update(cx, |f, cx| {
 988                f.delegate_mut().spawn_search(test_path_like("thf"), cx)
 989            })
 990            .await;
 991        cx.read(|cx| {
 992            let finder = finder.read(cx);
 993            let delegate = finder.delegate();
 994            let matches = match &delegate.matches {
 995                Matches::Search(path_matches) => path_matches,
 996                _ => panic!("Search matches expected"),
 997            };
 998            assert_eq!(matches.len(), 1);
 999
1000            let (file_name, file_name_positions, full_path, full_path_positions) =
1001                delegate.labels_for_path_match(&matches[0]);
1002            assert_eq!(file_name, "the-file");
1003            assert_eq!(file_name_positions, &[0, 1, 4]);
1004            assert_eq!(full_path, "the-file");
1005            assert_eq!(full_path_positions, &[0, 1, 4]);
1006        });
1007
1008        // Since the worktree root is a file, searching for its name followed by a slash does
1009        // not match anything.
1010        finder
1011            .update(cx, |f, cx| {
1012                f.delegate_mut().spawn_search(test_path_like("thf/"), cx)
1013            })
1014            .await;
1015        finder.read_with(cx, |f, _| assert_eq!(f.delegate().matches.len(), 0));
1016    }
1017
1018    #[gpui::test]
1019    async fn test_multiple_matches_with_same_relative_path(cx: &mut TestAppContext) {
1020        let app_state = init_test(cx);
1021        app_state
1022            .fs
1023            .as_fake()
1024            .insert_tree(
1025                "/root",
1026                json!({
1027                    "dir1": { "a.txt": "" },
1028                    "dir2": { "a.txt": "" }
1029                }),
1030            )
1031            .await;
1032
1033        let project = Project::test(
1034            app_state.fs.clone(),
1035            ["/root/dir1".as_ref(), "/root/dir2".as_ref()],
1036            cx,
1037        )
1038        .await;
1039        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
1040
1041        let (_, finder) = cx.add_window(|cx| {
1042            Picker::new(
1043                FileFinderDelegate::new(
1044                    workspace.downgrade(),
1045                    workspace.read(cx).project().clone(),
1046                    None,
1047                    Vec::new(),
1048                    cx,
1049                ),
1050                cx,
1051            )
1052        });
1053
1054        // Run a search that matches two files with the same relative path.
1055        finder
1056            .update(cx, |f, cx| {
1057                f.delegate_mut().spawn_search(test_path_like("a.t"), cx)
1058            })
1059            .await;
1060
1061        // Can switch between different matches with the same relative path.
1062        finder.update(cx, |finder, cx| {
1063            let delegate = finder.delegate_mut();
1064            assert_eq!(delegate.matches.len(), 2);
1065            assert_eq!(delegate.selected_index(), 0);
1066            delegate.set_selected_index(1, cx);
1067            assert_eq!(delegate.selected_index(), 1);
1068            delegate.set_selected_index(0, cx);
1069            assert_eq!(delegate.selected_index(), 0);
1070        });
1071    }
1072
1073    #[gpui::test]
1074    async fn test_path_distance_ordering(cx: &mut TestAppContext) {
1075        let app_state = init_test(cx);
1076        app_state
1077            .fs
1078            .as_fake()
1079            .insert_tree(
1080                "/root",
1081                json!({
1082                    "dir1": { "a.txt": "" },
1083                    "dir2": {
1084                        "a.txt": "",
1085                        "b.txt": ""
1086                    }
1087                }),
1088            )
1089            .await;
1090
1091        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1092        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
1093        let worktree_id = cx.read(|cx| {
1094            let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1095            assert_eq!(worktrees.len(), 1);
1096            WorktreeId::from_usize(worktrees[0].id())
1097        });
1098
1099        // When workspace has an active item, sort items which are closer to that item
1100        // first when they have the same name. In this case, b.txt is closer to dir2's a.txt
1101        // so that one should be sorted earlier
1102        let b_path = Some(dummy_found_path(ProjectPath {
1103            worktree_id,
1104            path: Arc::from(Path::new("/root/dir2/b.txt")),
1105        }));
1106        let (_, finder) = cx.add_window(|cx| {
1107            Picker::new(
1108                FileFinderDelegate::new(
1109                    workspace.downgrade(),
1110                    workspace.read(cx).project().clone(),
1111                    b_path,
1112                    Vec::new(),
1113                    cx,
1114                ),
1115                cx,
1116            )
1117        });
1118
1119        finder
1120            .update(cx, |f, cx| {
1121                f.delegate_mut().spawn_search(test_path_like("a.txt"), cx)
1122            })
1123            .await;
1124
1125        finder.read_with(cx, |f, _| {
1126            let delegate = f.delegate();
1127            let matches = match &delegate.matches {
1128                Matches::Search(path_matches) => path_matches,
1129                _ => panic!("Search matches expected"),
1130            };
1131            assert_eq!(matches[0].path.as_ref(), Path::new("dir2/a.txt"));
1132            assert_eq!(matches[1].path.as_ref(), Path::new("dir1/a.txt"));
1133        });
1134    }
1135
1136    #[gpui::test]
1137    async fn test_search_worktree_without_files(cx: &mut TestAppContext) {
1138        let app_state = init_test(cx);
1139        app_state
1140            .fs
1141            .as_fake()
1142            .insert_tree(
1143                "/root",
1144                json!({
1145                    "dir1": {},
1146                    "dir2": {
1147                        "dir3": {}
1148                    }
1149                }),
1150            )
1151            .await;
1152
1153        let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1154        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
1155        let (_, finder) = cx.add_window(|cx| {
1156            Picker::new(
1157                FileFinderDelegate::new(
1158                    workspace.downgrade(),
1159                    workspace.read(cx).project().clone(),
1160                    None,
1161                    Vec::new(),
1162                    cx,
1163                ),
1164                cx,
1165            )
1166        });
1167        finder
1168            .update(cx, |f, cx| {
1169                f.delegate_mut().spawn_search(test_path_like("dir"), cx)
1170            })
1171            .await;
1172        cx.read(|cx| {
1173            let finder = finder.read(cx);
1174            assert_eq!(finder.delegate().matches.len(), 0);
1175        });
1176    }
1177
1178    #[gpui::test]
1179    async fn test_query_history(
1180        deterministic: Arc<gpui::executor::Deterministic>,
1181        cx: &mut gpui::TestAppContext,
1182    ) {
1183        let app_state = init_test(cx);
1184
1185        app_state
1186            .fs
1187            .as_fake()
1188            .insert_tree(
1189                "/src",
1190                json!({
1191                    "test": {
1192                        "first.rs": "// First Rust file",
1193                        "second.rs": "// Second Rust file",
1194                        "third.rs": "// Third Rust file",
1195                    }
1196                }),
1197            )
1198            .await;
1199
1200        let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
1201        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
1202        let worktree_id = cx.read(|cx| {
1203            let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1204            assert_eq!(worktrees.len(), 1);
1205            WorktreeId::from_usize(worktrees[0].id())
1206        });
1207
1208        // Open and close panels, getting their history items afterwards.
1209        // Ensure history items get populated with opened items, and items are kept in a certain order.
1210        // The history lags one opened buffer behind, since it's updated in the search panel only on its reopen.
1211        //
1212        // TODO: without closing, the opened items do not propagate their history changes for some reason
1213        // it does work in real app though, only tests do not propagate.
1214
1215        let initial_history = open_close_queried_buffer(
1216            "fir",
1217            1,
1218            "first.rs",
1219            window_id,
1220            &workspace,
1221            &deterministic,
1222            cx,
1223        )
1224        .await;
1225        assert!(
1226            initial_history.is_empty(),
1227            "Should have no history before opening any files"
1228        );
1229
1230        let history_after_first = open_close_queried_buffer(
1231            "sec",
1232            1,
1233            "second.rs",
1234            window_id,
1235            &workspace,
1236            &deterministic,
1237            cx,
1238        )
1239        .await;
1240        assert_eq!(
1241            history_after_first,
1242            vec![FoundPath::new(
1243                ProjectPath {
1244                    worktree_id,
1245                    path: Arc::from(Path::new("test/first.rs")),
1246                },
1247                Some(PathBuf::from("/src/test/first.rs"))
1248            )],
1249            "Should show 1st opened item in the history when opening the 2nd item"
1250        );
1251
1252        let history_after_second = open_close_queried_buffer(
1253            "thi",
1254            1,
1255            "third.rs",
1256            window_id,
1257            &workspace,
1258            &deterministic,
1259            cx,
1260        )
1261        .await;
1262        assert_eq!(
1263            history_after_second,
1264            vec![
1265                FoundPath::new(
1266                    ProjectPath {
1267                        worktree_id,
1268                        path: Arc::from(Path::new("test/second.rs")),
1269                    },
1270                    Some(PathBuf::from("/src/test/second.rs"))
1271                ),
1272                FoundPath::new(
1273                    ProjectPath {
1274                        worktree_id,
1275                        path: Arc::from(Path::new("test/first.rs")),
1276                    },
1277                    Some(PathBuf::from("/src/test/first.rs"))
1278                ),
1279            ],
1280            "Should show 1st and 2nd opened items in the history when opening the 3rd item. \
12812nd item should be the first in the history, as the last opened."
1282        );
1283
1284        let history_after_third = open_close_queried_buffer(
1285            "sec",
1286            1,
1287            "second.rs",
1288            window_id,
1289            &workspace,
1290            &deterministic,
1291            cx,
1292        )
1293        .await;
1294        assert_eq!(
1295            history_after_third,
1296            vec![
1297                FoundPath::new(
1298                    ProjectPath {
1299                        worktree_id,
1300                        path: Arc::from(Path::new("test/third.rs")),
1301                    },
1302                    Some(PathBuf::from("/src/test/third.rs"))
1303                ),
1304                FoundPath::new(
1305                    ProjectPath {
1306                        worktree_id,
1307                        path: Arc::from(Path::new("test/second.rs")),
1308                    },
1309                    Some(PathBuf::from("/src/test/second.rs"))
1310                ),
1311                FoundPath::new(
1312                    ProjectPath {
1313                        worktree_id,
1314                        path: Arc::from(Path::new("test/first.rs")),
1315                    },
1316                    Some(PathBuf::from("/src/test/first.rs"))
1317                ),
1318            ],
1319            "Should show 1st, 2nd and 3rd opened items in the history when opening the 2nd item again. \
13203rd item should be the first in the history, as the last opened."
1321        );
1322
1323        let history_after_second_again = open_close_queried_buffer(
1324            "thi",
1325            1,
1326            "third.rs",
1327            window_id,
1328            &workspace,
1329            &deterministic,
1330            cx,
1331        )
1332        .await;
1333        assert_eq!(
1334            history_after_second_again,
1335            vec![
1336                FoundPath::new(
1337                    ProjectPath {
1338                        worktree_id,
1339                        path: Arc::from(Path::new("test/second.rs")),
1340                    },
1341                    Some(PathBuf::from("/src/test/second.rs"))
1342                ),
1343                FoundPath::new(
1344                    ProjectPath {
1345                        worktree_id,
1346                        path: Arc::from(Path::new("test/third.rs")),
1347                    },
1348                    Some(PathBuf::from("/src/test/third.rs"))
1349                ),
1350                FoundPath::new(
1351                    ProjectPath {
1352                        worktree_id,
1353                        path: Arc::from(Path::new("test/first.rs")),
1354                    },
1355                    Some(PathBuf::from("/src/test/first.rs"))
1356                ),
1357            ],
1358            "Should show 1st, 2nd and 3rd opened items in the history when opening the 3rd item again. \
13592nd item, as the last opened, 3rd item should go next as it was opened right before."
1360        );
1361    }
1362
1363    #[gpui::test]
1364    async fn test_external_files_history(
1365        deterministic: Arc<gpui::executor::Deterministic>,
1366        cx: &mut gpui::TestAppContext,
1367    ) {
1368        let app_state = init_test(cx);
1369
1370        app_state
1371            .fs
1372            .as_fake()
1373            .insert_tree(
1374                "/src",
1375                json!({
1376                    "test": {
1377                        "first.rs": "// First Rust file",
1378                        "second.rs": "// Second Rust file",
1379                    }
1380                }),
1381            )
1382            .await;
1383
1384        app_state
1385            .fs
1386            .as_fake()
1387            .insert_tree(
1388                "/external-src",
1389                json!({
1390                    "test": {
1391                        "third.rs": "// Third Rust file",
1392                        "fourth.rs": "// Fourth Rust file",
1393                    }
1394                }),
1395            )
1396            .await;
1397
1398        let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
1399        cx.update(|cx| {
1400            project.update(cx, |project, cx| {
1401                project.find_or_create_local_worktree("/external-src", false, cx)
1402            })
1403        })
1404        .detach();
1405        deterministic.run_until_parked();
1406
1407        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
1408        let worktree_id = cx.read(|cx| {
1409            let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1410            assert_eq!(worktrees.len(), 1,);
1411
1412            WorktreeId::from_usize(worktrees[0].id())
1413        });
1414        workspace
1415            .update(cx, |workspace, cx| {
1416                workspace.open_abs_path(PathBuf::from("/external-src/test/third.rs"), false, cx)
1417            })
1418            .detach();
1419        deterministic.run_until_parked();
1420        let external_worktree_id = cx.read(|cx| {
1421            let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1422            assert_eq!(
1423                worktrees.len(),
1424                2,
1425                "External file should get opened in a new worktree"
1426            );
1427
1428            WorktreeId::from_usize(
1429                worktrees
1430                    .into_iter()
1431                    .find(|worktree| worktree.id() != worktree_id.to_usize())
1432                    .expect("New worktree should have a different id")
1433                    .id(),
1434            )
1435        });
1436        close_active_item(&workspace, &deterministic, cx).await;
1437
1438        let initial_history_items = open_close_queried_buffer(
1439            "sec",
1440            1,
1441            "second.rs",
1442            window_id,
1443            &workspace,
1444            &deterministic,
1445            cx,
1446        )
1447        .await;
1448        assert_eq!(
1449            initial_history_items,
1450            vec![FoundPath::new(
1451                ProjectPath {
1452                    worktree_id: external_worktree_id,
1453                    path: Arc::from(Path::new("")),
1454                },
1455                Some(PathBuf::from("/external-src/test/third.rs"))
1456            )],
1457            "Should show external file with its full path in the history after it was open"
1458        );
1459
1460        let updated_history_items = open_close_queried_buffer(
1461            "fir",
1462            1,
1463            "first.rs",
1464            window_id,
1465            &workspace,
1466            &deterministic,
1467            cx,
1468        )
1469        .await;
1470        assert_eq!(
1471            updated_history_items,
1472            vec![
1473                FoundPath::new(
1474                    ProjectPath {
1475                        worktree_id,
1476                        path: Arc::from(Path::new("test/second.rs")),
1477                    },
1478                    Some(PathBuf::from("/src/test/second.rs"))
1479                ),
1480                FoundPath::new(
1481                    ProjectPath {
1482                        worktree_id: external_worktree_id,
1483                        path: Arc::from(Path::new("")),
1484                    },
1485                    Some(PathBuf::from("/external-src/test/third.rs"))
1486                ),
1487            ],
1488            "Should keep external file with history updates",
1489        );
1490    }
1491
1492    async fn open_close_queried_buffer(
1493        input: &str,
1494        expected_matches: usize,
1495        expected_editor_title: &str,
1496        window_id: usize,
1497        workspace: &ViewHandle<Workspace>,
1498        deterministic: &gpui::executor::Deterministic,
1499        cx: &mut gpui::TestAppContext,
1500    ) -> Vec<FoundPath> {
1501        cx.dispatch_action(window_id, Toggle);
1502        let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
1503        finder
1504            .update(cx, |finder, cx| {
1505                finder.delegate_mut().update_matches(input.to_string(), cx)
1506            })
1507            .await;
1508        let history_items = finder.read_with(cx, |finder, _| {
1509            assert_eq!(
1510                finder.delegate().matches.len(),
1511                expected_matches,
1512                "Unexpected number of matches found for query {input}"
1513            );
1514            finder.delegate().history_items.clone()
1515        });
1516
1517        let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
1518        cx.dispatch_action(window_id, SelectNext);
1519        cx.dispatch_action(window_id, Confirm);
1520        deterministic.run_until_parked();
1521        active_pane
1522            .condition(cx, |pane, _| pane.active_item().is_some())
1523            .await;
1524        cx.read(|cx| {
1525            let active_item = active_pane.read(cx).active_item().unwrap();
1526            let active_editor_title = active_item
1527                .as_any()
1528                .downcast_ref::<Editor>()
1529                .unwrap()
1530                .read(cx)
1531                .title(cx);
1532            assert_eq!(
1533                expected_editor_title, active_editor_title,
1534                "Unexpected editor title for query {input}"
1535            );
1536        });
1537
1538        close_active_item(workspace, deterministic, cx).await;
1539
1540        history_items
1541    }
1542
1543    async fn close_active_item(
1544        workspace: &ViewHandle<Workspace>,
1545        deterministic: &gpui::executor::Deterministic,
1546        cx: &mut TestAppContext,
1547    ) {
1548        let mut original_items = HashMap::new();
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                let insertion_result = original_items.insert(pane_id, pane.items().count());
1554                assert!(insertion_result.is_none(), "Pane id {pane_id} collision");
1555            }
1556        });
1557
1558        let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
1559        active_pane
1560            .update(cx, |pane, cx| {
1561                pane.close_active_item(&workspace::CloseActiveItem, cx)
1562                    .unwrap()
1563            })
1564            .await
1565            .unwrap();
1566        deterministic.run_until_parked();
1567        cx.read(|cx| {
1568            for pane in workspace.read(cx).panes() {
1569                let pane_id = pane.id();
1570                let pane = pane.read(cx);
1571                match original_items.remove(&pane_id) {
1572                    Some(original_items) => {
1573                        assert_eq!(
1574                            pane.items().count(),
1575                            original_items.saturating_sub(1),
1576                            "Pane id {pane_id} should have item closed"
1577                        );
1578                    }
1579                    None => panic!("Pane id {pane_id} not found in original items"),
1580                }
1581            }
1582        });
1583        assert!(
1584            original_items.len() <= 1,
1585            "At most one panel should got closed"
1586        );
1587    }
1588
1589    fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
1590        cx.foreground().forbid_parking();
1591        cx.update(|cx| {
1592            let state = AppState::test(cx);
1593            theme::init((), cx);
1594            language::init(cx);
1595            super::init(cx);
1596            editor::init(cx);
1597            workspace::init_settings(cx);
1598            Project::init_settings(cx);
1599            state
1600        })
1601    }
1602
1603    fn test_path_like(test_str: &str) -> PathLikeWithPosition<FileSearchQuery> {
1604        PathLikeWithPosition::parse_str(test_str, |path_like_str| {
1605            Ok::<_, std::convert::Infallible>(FileSearchQuery {
1606                raw_query: test_str.to_owned(),
1607                file_query_end: if path_like_str == test_str {
1608                    None
1609                } else {
1610                    Some(path_like_str.len())
1611                },
1612            })
1613        })
1614        .unwrap()
1615    }
1616
1617    fn dummy_found_path(project_path: ProjectPath) -> FoundPath {
1618        FoundPath {
1619            project: project_path,
1620            absolute: None,
1621        }
1622    }
1623}