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