file_finder.rs

   1use collections::HashMap;
   2use editor::{scroll::autoscroll::Autoscroll, Bias, Editor};
   3use fuzzy::{CharBag, PathMatch, PathMatchCandidate};
   4use gpui::{
   5    actions, div, AppContext, Component, Div, EventEmitter, InteractiveComponent, Model,
   6    ParentComponent, Render, Styled, Task, View, ViewContext, VisualContext, WeakView,
   7    WindowContext,
   8};
   9use picker::{Picker, PickerDelegate};
  10use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
  11use std::{
  12    path::{Path, PathBuf},
  13    sync::{
  14        atomic::{self, AtomicBool},
  15        Arc,
  16    },
  17};
  18use text::Point;
  19use theme::ActiveTheme;
  20use ui::{v_stack, HighlightedLabel, StyledExt};
  21use util::{paths::PathLikeWithPosition, post_inc, ResultExt};
  22use workspace::{Modal, ModalEvent, Workspace};
  23
  24actions!(Toggle);
  25
  26pub struct FileFinder {
  27    picker: View<Picker<FileFinderDelegate>>,
  28}
  29
  30pub fn init(cx: &mut AppContext) {
  31    cx.observe_new_views(FileFinder::register).detach();
  32}
  33
  34impl FileFinder {
  35    fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
  36        workspace.register_action(|workspace, _: &Toggle, cx| {
  37            let Some(file_finder) = workspace.current_modal::<Self>(cx) else {
  38                Self::open(workspace, cx);
  39                return;
  40            };
  41
  42            file_finder.update(cx, |file_finder, cx| {
  43                file_finder
  44                    .picker
  45                    .update(cx, |picker, cx| picker.cycle_selection(cx))
  46            });
  47        });
  48    }
  49
  50    fn open(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
  51        let project = workspace.project().read(cx);
  52
  53        let currently_opened_path = workspace
  54            .active_item(cx)
  55            .and_then(|item| item.project_path(cx))
  56            .map(|project_path| {
  57                let abs_path = project
  58                    .worktree_for_id(project_path.worktree_id, cx)
  59                    .map(|worktree| worktree.read(cx).abs_path().join(&project_path.path));
  60                FoundPath::new(project_path, abs_path)
  61            });
  62
  63        // if exists, bubble the currently opened path to the top
  64        let history_items = currently_opened_path
  65            .clone()
  66            .into_iter()
  67            .chain(
  68                workspace
  69                    .recent_navigation_history(Some(MAX_RECENT_SELECTIONS), cx)
  70                    .into_iter()
  71                    .filter(|(history_path, _)| {
  72                        Some(history_path)
  73                            != currently_opened_path
  74                                .as_ref()
  75                                .map(|found_path| &found_path.project)
  76                    })
  77                    .filter(|(_, history_abs_path)| {
  78                        history_abs_path.as_ref()
  79                            != currently_opened_path
  80                                .as_ref()
  81                                .and_then(|found_path| found_path.absolute.as_ref())
  82                    })
  83                    .filter(|(_, history_abs_path)| match history_abs_path {
  84                        Some(abs_path) => history_file_exists(abs_path),
  85                        None => true,
  86                    })
  87                    .map(|(history_path, abs_path)| FoundPath::new(history_path, abs_path)),
  88            )
  89            .collect();
  90
  91        let project = workspace.project().clone();
  92        let weak_workspace = cx.view().downgrade();
  93        workspace.toggle_modal(cx, |cx| {
  94            let delegate = FileFinderDelegate::new(
  95                cx.view().downgrade(),
  96                weak_workspace,
  97                project,
  98                currently_opened_path,
  99                history_items,
 100                cx,
 101            );
 102
 103            FileFinder::new(delegate, cx)
 104        });
 105    }
 106
 107    fn new(delegate: FileFinderDelegate, cx: &mut ViewContext<Self>) -> Self {
 108        Self {
 109            picker: cx.build_view(|cx| Picker::new(delegate, cx)),
 110        }
 111    }
 112}
 113
 114impl EventEmitter<ModalEvent> for FileFinder {}
 115impl Modal for FileFinder {
 116    fn focus(&self, cx: &mut WindowContext) {
 117        self.picker.update(cx, |picker, cx| picker.focus(cx))
 118    }
 119}
 120impl Render for FileFinder {
 121    type Element = Div<Self>;
 122
 123    fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
 124        v_stack().w_96().child(self.picker.clone())
 125    }
 126}
 127
 128pub struct FileFinderDelegate {
 129    file_finder: WeakView<FileFinder>,
 130    workspace: WeakView<Workspace>,
 131    project: Model<Project>,
 132    search_count: usize,
 133    latest_search_id: usize,
 134    latest_search_did_cancel: bool,
 135    latest_search_query: Option<PathLikeWithPosition<FileSearchQuery>>,
 136    currently_opened_path: Option<FoundPath>,
 137    matches: Matches,
 138    selected_index: Option<usize>,
 139    cancel_flag: Arc<AtomicBool>,
 140    history_items: Vec<FoundPath>,
 141}
 142
 143#[derive(Debug, Default)]
 144struct Matches {
 145    history: Vec<(FoundPath, Option<PathMatch>)>,
 146    search: Vec<PathMatch>,
 147}
 148
 149#[derive(Debug)]
 150enum Match<'a> {
 151    History(&'a FoundPath, Option<&'a PathMatch>),
 152    Search(&'a PathMatch),
 153}
 154
 155impl Matches {
 156    fn len(&self) -> usize {
 157        self.history.len() + self.search.len()
 158    }
 159
 160    fn get(&self, index: usize) -> Option<Match<'_>> {
 161        if index < self.history.len() {
 162            self.history
 163                .get(index)
 164                .map(|(path, path_match)| Match::History(path, path_match.as_ref()))
 165        } else {
 166            self.search
 167                .get(index - self.history.len())
 168                .map(Match::Search)
 169        }
 170    }
 171
 172    fn push_new_matches(
 173        &mut self,
 174        history_items: &Vec<FoundPath>,
 175        query: &PathLikeWithPosition<FileSearchQuery>,
 176        mut new_search_matches: Vec<PathMatch>,
 177        extend_old_matches: bool,
 178    ) {
 179        let matching_history_paths = matching_history_item_paths(history_items, query);
 180        new_search_matches
 181            .retain(|path_match| !matching_history_paths.contains_key(&path_match.path));
 182        let history_items_to_show = history_items
 183            .iter()
 184            .filter_map(|history_item| {
 185                Some((
 186                    history_item.clone(),
 187                    Some(
 188                        matching_history_paths
 189                            .get(&history_item.project.path)?
 190                            .clone(),
 191                    ),
 192                ))
 193            })
 194            .collect::<Vec<_>>();
 195        self.history = history_items_to_show;
 196        if extend_old_matches {
 197            self.search
 198                .retain(|path_match| !matching_history_paths.contains_key(&path_match.path));
 199            util::extend_sorted(
 200                &mut self.search,
 201                new_search_matches.into_iter(),
 202                100,
 203                |a, b| b.cmp(a),
 204            )
 205        } else {
 206            self.search = new_search_matches;
 207        }
 208    }
 209}
 210
 211fn matching_history_item_paths(
 212    history_items: &Vec<FoundPath>,
 213    query: &PathLikeWithPosition<FileSearchQuery>,
 214) -> HashMap<Arc<Path>, PathMatch> {
 215    let history_items_by_worktrees = history_items
 216        .iter()
 217        .filter_map(|found_path| {
 218            let candidate = PathMatchCandidate {
 219                path: &found_path.project.path,
 220                // Only match history items names, otherwise their paths may match too many queries, producing false positives.
 221                // E.g. `foo` would match both `something/foo/bar.rs` and `something/foo/foo.rs` and if the former is a history item,
 222                // it would be shown first always, despite the latter being a better match.
 223                char_bag: CharBag::from_iter(
 224                    found_path
 225                        .project
 226                        .path
 227                        .file_name()?
 228                        .to_string_lossy()
 229                        .to_lowercase()
 230                        .chars(),
 231                ),
 232            };
 233            Some((found_path.project.worktree_id, candidate))
 234        })
 235        .fold(
 236            HashMap::default(),
 237            |mut candidates, (worktree_id, new_candidate)| {
 238                candidates
 239                    .entry(worktree_id)
 240                    .or_insert_with(Vec::new)
 241                    .push(new_candidate);
 242                candidates
 243            },
 244        );
 245    let mut matching_history_paths = HashMap::default();
 246    for (worktree, candidates) in history_items_by_worktrees {
 247        let max_results = candidates.len() + 1;
 248        matching_history_paths.extend(
 249            fuzzy::match_fixed_path_set(
 250                candidates,
 251                worktree.to_usize(),
 252                query.path_like.path_query(),
 253                false,
 254                max_results,
 255            )
 256            .into_iter()
 257            .map(|path_match| (Arc::clone(&path_match.path), path_match)),
 258        );
 259    }
 260    matching_history_paths
 261}
 262
 263#[derive(Debug, Clone, PartialEq, Eq)]
 264struct FoundPath {
 265    project: ProjectPath,
 266    absolute: Option<PathBuf>,
 267}
 268
 269impl FoundPath {
 270    fn new(project: ProjectPath, absolute: Option<PathBuf>) -> Self {
 271        Self { project, absolute }
 272    }
 273}
 274
 275const MAX_RECENT_SELECTIONS: usize = 20;
 276
 277#[cfg(not(test))]
 278fn history_file_exists(abs_path: &PathBuf) -> bool {
 279    abs_path.exists()
 280}
 281
 282#[cfg(test)]
 283fn history_file_exists(abs_path: &PathBuf) -> bool {
 284    !abs_path.ends_with("nonexistent.rs")
 285}
 286
 287pub enum Event {
 288    Selected(ProjectPath),
 289    Dismissed,
 290}
 291
 292#[derive(Debug, Clone)]
 293struct FileSearchQuery {
 294    raw_query: String,
 295    file_query_end: Option<usize>,
 296}
 297
 298impl FileSearchQuery {
 299    fn path_query(&self) -> &str {
 300        match self.file_query_end {
 301            Some(file_path_end) => &self.raw_query[..file_path_end],
 302            None => &self.raw_query,
 303        }
 304    }
 305}
 306
 307impl FileFinderDelegate {
 308    fn new(
 309        file_finder: WeakView<FileFinder>,
 310        workspace: WeakView<Workspace>,
 311        project: Model<Project>,
 312        currently_opened_path: Option<FoundPath>,
 313        history_items: Vec<FoundPath>,
 314        cx: &mut ViewContext<FileFinder>,
 315    ) -> Self {
 316        cx.observe(&project, |file_finder, _, cx| {
 317            //todo!() We should probably not re-render on every project anything
 318            file_finder
 319                .picker
 320                .update(cx, |picker, cx| picker.refresh(cx))
 321        })
 322        .detach();
 323
 324        Self {
 325            file_finder,
 326            workspace,
 327            project,
 328            search_count: 0,
 329            latest_search_id: 0,
 330            latest_search_did_cancel: false,
 331            latest_search_query: None,
 332            currently_opened_path,
 333            matches: Matches::default(),
 334            selected_index: None,
 335            cancel_flag: Arc::new(AtomicBool::new(false)),
 336            history_items,
 337        }
 338    }
 339
 340    fn spawn_search(
 341        &mut self,
 342        query: PathLikeWithPosition<FileSearchQuery>,
 343        cx: &mut ViewContext<Picker<Self>>,
 344    ) -> Task<()> {
 345        let relative_to = self
 346            .currently_opened_path
 347            .as_ref()
 348            .map(|found_path| Arc::clone(&found_path.project.path));
 349        let worktrees = self
 350            .project
 351            .read(cx)
 352            .visible_worktrees(cx)
 353            .collect::<Vec<_>>();
 354        let include_root_name = worktrees.len() > 1;
 355        let candidate_sets = worktrees
 356            .into_iter()
 357            .map(|worktree| {
 358                let worktree = worktree.read(cx);
 359                PathMatchCandidateSet {
 360                    snapshot: worktree.snapshot(),
 361                    include_ignored: worktree
 362                        .root_entry()
 363                        .map_or(false, |entry| entry.is_ignored),
 364                    include_root_name,
 365                }
 366            })
 367            .collect::<Vec<_>>();
 368
 369        let search_id = util::post_inc(&mut self.search_count);
 370        self.cancel_flag.store(true, atomic::Ordering::Relaxed);
 371        self.cancel_flag = Arc::new(AtomicBool::new(false));
 372        let cancel_flag = self.cancel_flag.clone();
 373        cx.spawn(|picker, mut cx| async move {
 374            let matches = fuzzy::match_path_sets(
 375                candidate_sets.as_slice(),
 376                query.path_like.path_query(),
 377                relative_to,
 378                false,
 379                100,
 380                &cancel_flag,
 381                cx.background_executor().clone(),
 382            )
 383            .await;
 384            let did_cancel = cancel_flag.load(atomic::Ordering::Relaxed);
 385            picker
 386                .update(&mut cx, |picker, cx| {
 387                    picker
 388                        .delegate
 389                        .set_search_matches(search_id, did_cancel, query, matches, cx)
 390                })
 391                .log_err();
 392        })
 393    }
 394
 395    fn set_search_matches(
 396        &mut self,
 397        search_id: usize,
 398        did_cancel: bool,
 399        query: PathLikeWithPosition<FileSearchQuery>,
 400        matches: Vec<PathMatch>,
 401        cx: &mut ViewContext<Picker<Self>>,
 402    ) {
 403        if search_id >= self.latest_search_id {
 404            self.latest_search_id = search_id;
 405            let extend_old_matches = self.latest_search_did_cancel
 406                && Some(query.path_like.path_query())
 407                    == self
 408                        .latest_search_query
 409                        .as_ref()
 410                        .map(|query| query.path_like.path_query());
 411            self.matches
 412                .push_new_matches(&self.history_items, &query, matches, extend_old_matches);
 413            self.latest_search_query = Some(query);
 414            self.latest_search_did_cancel = did_cancel;
 415            cx.notify();
 416        }
 417    }
 418
 419    fn labels_for_match(
 420        &self,
 421        path_match: Match,
 422        cx: &AppContext,
 423        ix: usize,
 424    ) -> (String, Vec<usize>, String, Vec<usize>) {
 425        let (file_name, file_name_positions, full_path, full_path_positions) = match path_match {
 426            Match::History(found_path, found_path_match) => {
 427                let worktree_id = found_path.project.worktree_id;
 428                let project_relative_path = &found_path.project.path;
 429                let has_worktree = self
 430                    .project
 431                    .read(cx)
 432                    .worktree_for_id(worktree_id, cx)
 433                    .is_some();
 434
 435                if !has_worktree {
 436                    if let Some(absolute_path) = &found_path.absolute {
 437                        return (
 438                            absolute_path
 439                                .file_name()
 440                                .map_or_else(
 441                                    || project_relative_path.to_string_lossy(),
 442                                    |file_name| file_name.to_string_lossy(),
 443                                )
 444                                .to_string(),
 445                            Vec::new(),
 446                            absolute_path.to_string_lossy().to_string(),
 447                            Vec::new(),
 448                        );
 449                    }
 450                }
 451
 452                let mut path = Arc::clone(project_relative_path);
 453                if project_relative_path.as_ref() == Path::new("") {
 454                    if let Some(absolute_path) = &found_path.absolute {
 455                        path = Arc::from(absolute_path.as_path());
 456                    }
 457                }
 458
 459                let mut path_match = PathMatch {
 460                    score: ix as f64,
 461                    positions: Vec::new(),
 462                    worktree_id: worktree_id.to_usize(),
 463                    path,
 464                    path_prefix: "".into(),
 465                    distance_to_relative_ancestor: usize::MAX,
 466                };
 467                if let Some(found_path_match) = found_path_match {
 468                    path_match
 469                        .positions
 470                        .extend(found_path_match.positions.iter())
 471                }
 472
 473                self.labels_for_path_match(&path_match)
 474            }
 475            Match::Search(path_match) => self.labels_for_path_match(path_match),
 476        };
 477
 478        if file_name_positions.is_empty() {
 479            if let Some(user_home_path) = std::env::var("HOME").ok() {
 480                let user_home_path = user_home_path.trim();
 481                if !user_home_path.is_empty() {
 482                    if (&full_path).starts_with(user_home_path) {
 483                        return (
 484                            file_name,
 485                            file_name_positions,
 486                            full_path.replace(user_home_path, "~"),
 487                            full_path_positions,
 488                        );
 489                    }
 490                }
 491            }
 492        }
 493
 494        (
 495            file_name,
 496            file_name_positions,
 497            full_path,
 498            full_path_positions,
 499        )
 500    }
 501
 502    fn labels_for_path_match(
 503        &self,
 504        path_match: &PathMatch,
 505    ) -> (String, Vec<usize>, String, Vec<usize>) {
 506        let path = &path_match.path;
 507        let path_string = path.to_string_lossy();
 508        let full_path = [path_match.path_prefix.as_ref(), path_string.as_ref()].join("");
 509        let path_positions = path_match.positions.clone();
 510
 511        let file_name = path.file_name().map_or_else(
 512            || path_match.path_prefix.to_string(),
 513            |file_name| file_name.to_string_lossy().to_string(),
 514        );
 515        let file_name_start = path_match.path_prefix.chars().count() + path_string.chars().count()
 516            - file_name.chars().count();
 517        let file_name_positions = path_positions
 518            .iter()
 519            .filter_map(|pos| {
 520                if pos >= &file_name_start {
 521                    Some(pos - file_name_start)
 522                } else {
 523                    None
 524                }
 525            })
 526            .collect();
 527
 528        (file_name, file_name_positions, full_path, path_positions)
 529    }
 530}
 531
 532impl PickerDelegate for FileFinderDelegate {
 533    type ListItem = Div<Picker<Self>>;
 534
 535    fn placeholder_text(&self) -> Arc<str> {
 536        "Search project files...".into()
 537    }
 538
 539    fn match_count(&self) -> usize {
 540        self.matches.len()
 541    }
 542
 543    fn selected_index(&self) -> usize {
 544        self.selected_index.unwrap_or(0)
 545    }
 546
 547    fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
 548        self.selected_index = Some(ix);
 549        cx.notify();
 550    }
 551
 552    fn update_matches(
 553        &mut self,
 554        raw_query: String,
 555        cx: &mut ViewContext<Picker<Self>>,
 556    ) -> Task<()> {
 557        if raw_query.is_empty() {
 558            let project = self.project.read(cx);
 559            self.latest_search_id = post_inc(&mut self.search_count);
 560            self.matches = Matches {
 561                history: self
 562                    .history_items
 563                    .iter()
 564                    .filter(|history_item| {
 565                        project
 566                            .worktree_for_id(history_item.project.worktree_id, cx)
 567                            .is_some()
 568                            || (project.is_local() && history_item.absolute.is_some())
 569                    })
 570                    .cloned()
 571                    .map(|p| (p, None))
 572                    .collect(),
 573                search: Vec::new(),
 574            };
 575            cx.notify();
 576            Task::ready(())
 577        } else {
 578            let raw_query = &raw_query;
 579            let query = PathLikeWithPosition::parse_str(raw_query, |path_like_str| {
 580                Ok::<_, std::convert::Infallible>(FileSearchQuery {
 581                    raw_query: raw_query.to_owned(),
 582                    file_query_end: if path_like_str == raw_query {
 583                        None
 584                    } else {
 585                        Some(path_like_str.len())
 586                    },
 587                })
 588            })
 589            .expect("infallible");
 590            self.spawn_search(query, cx)
 591        }
 592    }
 593
 594    fn confirm(&mut self, secondary: bool, cx: &mut ViewContext<Picker<FileFinderDelegate>>) {
 595        if let Some(m) = self.matches.get(self.selected_index()) {
 596            if let Some(workspace) = self.workspace.upgrade() {
 597                let open_task = workspace.update(cx, move |workspace, cx| {
 598                    let split_or_open = |workspace: &mut Workspace, project_path, cx| {
 599                        if secondary {
 600                            workspace.split_path(project_path, cx)
 601                        } else {
 602                            workspace.open_path(project_path, None, true, cx)
 603                        }
 604                    };
 605                    match m {
 606                        Match::History(history_match, _) => {
 607                            let worktree_id = history_match.project.worktree_id;
 608                            if workspace
 609                                .project()
 610                                .read(cx)
 611                                .worktree_for_id(worktree_id, cx)
 612                                .is_some()
 613                            {
 614                                split_or_open(
 615                                    workspace,
 616                                    ProjectPath {
 617                                        worktree_id,
 618                                        path: Arc::clone(&history_match.project.path),
 619                                    },
 620                                    cx,
 621                                )
 622                            } else {
 623                                match history_match.absolute.as_ref() {
 624                                    Some(abs_path) => {
 625                                        if secondary {
 626                                            workspace.split_abs_path(
 627                                                abs_path.to_path_buf(),
 628                                                false,
 629                                                cx,
 630                                            )
 631                                        } else {
 632                                            workspace.open_abs_path(
 633                                                abs_path.to_path_buf(),
 634                                                false,
 635                                                cx,
 636                                            )
 637                                        }
 638                                    }
 639                                    None => split_or_open(
 640                                        workspace,
 641                                        ProjectPath {
 642                                            worktree_id,
 643                                            path: Arc::clone(&history_match.project.path),
 644                                        },
 645                                        cx,
 646                                    ),
 647                                }
 648                            }
 649                        }
 650                        Match::Search(m) => split_or_open(
 651                            workspace,
 652                            ProjectPath {
 653                                worktree_id: WorktreeId::from_usize(m.worktree_id),
 654                                path: m.path.clone(),
 655                            },
 656                            cx,
 657                        ),
 658                    }
 659                });
 660
 661                let row = self
 662                    .latest_search_query
 663                    .as_ref()
 664                    .and_then(|query| query.row)
 665                    .map(|row| row.saturating_sub(1));
 666                let col = self
 667                    .latest_search_query
 668                    .as_ref()
 669                    .and_then(|query| query.column)
 670                    .unwrap_or(0)
 671                    .saturating_sub(1);
 672                let finder = self.file_finder.clone();
 673
 674                cx.spawn(|_, mut cx| async move {
 675                    let item = open_task.await.log_err()?;
 676                    if let Some(row) = row {
 677                        if let Some(active_editor) = item.downcast::<Editor>() {
 678                            active_editor
 679                                .downgrade()
 680                                .update(&mut cx, |editor, cx| {
 681                                    let snapshot = editor.snapshot(cx).display_snapshot;
 682                                    let point = snapshot
 683                                        .buffer_snapshot
 684                                        .clip_point(Point::new(row, col), Bias::Left);
 685                                    editor.change_selections(Some(Autoscroll::center()), cx, |s| {
 686                                        s.select_ranges([point..point])
 687                                    });
 688                                })
 689                                .log_err();
 690                        }
 691                    }
 692                    finder
 693                        .update(&mut cx, |_, cx| cx.emit(ModalEvent::Dismissed))
 694                        .ok()?;
 695
 696                    Some(())
 697                })
 698                .detach();
 699            }
 700        }
 701    }
 702
 703    fn dismissed(&mut self, cx: &mut ViewContext<Picker<FileFinderDelegate>>) {
 704        self.file_finder
 705            .update(cx, |_, cx| cx.emit(ModalEvent::Dismissed))
 706            .log_err();
 707    }
 708
 709    fn render_match(
 710        &self,
 711        ix: usize,
 712        selected: bool,
 713        cx: &mut ViewContext<Picker<Self>>,
 714    ) -> Self::ListItem {
 715        let path_match = self
 716            .matches
 717            .get(ix)
 718            .expect("Invalid matches state: no element for index {ix}");
 719        let theme = cx.theme();
 720        let colors = theme.colors();
 721
 722        let (file_name, file_name_positions, full_path, full_path_positions) =
 723            self.labels_for_match(path_match, cx, ix);
 724
 725        div()
 726            .px_1()
 727            .text_color(colors.text)
 728            .text_ui()
 729            .bg(colors.ghost_element_background)
 730            .rounded_md()
 731            .when(selected, |this| this.bg(colors.ghost_element_selected))
 732            .hover(|this| this.bg(colors.ghost_element_hover))
 733            .child(
 734                v_stack()
 735                    .child(HighlightedLabel::new(file_name, file_name_positions))
 736                    .child(HighlightedLabel::new(full_path, full_path_positions)),
 737            )
 738    }
 739}
 740
 741// #[cfg(test)]
 742// mod tests {
 743//     use std::{assert_eq, collections::HashMap, path::Path, time::Duration};
 744
 745//     use super::*;
 746//     use editor::Editor;
 747//     use gpui::{Entity, TestAppContext, VisualTestContext};
 748//     use menu::{Confirm, SelectNext};
 749//     use serde_json::json;
 750//     use workspace::{AppState, Workspace};
 751
 752//     #[ctor::ctor]
 753//     fn init_logger() {
 754//         if std::env::var("RUST_LOG").is_ok() {
 755//             env_logger::init();
 756//         }
 757//     }
 758
 759//     #[gpui::test]
 760//     async fn test_matching_paths(cx: &mut TestAppContext) {
 761//         let app_state = init_test(cx);
 762//         app_state
 763//             .fs
 764//             .as_fake()
 765//             .insert_tree(
 766//                 "/root",
 767//                 json!({
 768//                     "a": {
 769//                         "banana": "",
 770//                         "bandana": "",
 771//                     }
 772//                 }),
 773//             )
 774//             .await;
 775
 776//         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
 777
 778//         let (picker, workspace, mut cx) = build_find_picker(project, cx);
 779//         let cx = &mut cx;
 780
 781//         picker
 782//             .update(cx, |picker, cx| {
 783//                 picker.delegate.update_matches("bna".to_string(), cx)
 784//             })
 785//             .await;
 786
 787//         picker.update(cx, |picker, _| {
 788//             assert_eq!(picker.delegate.matches.len(), 2);
 789//         });
 790
 791//         let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
 792//         cx.dispatch_action(SelectNext);
 793//         cx.dispatch_action(Confirm);
 794//         active_pane
 795//             .condition(cx, |pane, _| pane.active_item().is_some())
 796//             .await;
 797//         cx.read(|cx| {
 798//             let active_item = active_pane.read(cx).active_item().unwrap();
 799//             assert_eq!(
 800//                 active_item
 801//                     .to_any()
 802//                     .downcast::<Editor>()
 803//                     .unwrap()
 804//                     .read(cx)
 805//                     .title(cx),
 806//                 "bandana"
 807//             );
 808//         });
 809//     }
 810
 811//     #[gpui::test]
 812//     async fn test_row_column_numbers_query_inside_file(cx: &mut TestAppContext) {
 813//         let app_state = init_test(cx);
 814
 815//         let first_file_name = "first.rs";
 816//         let first_file_contents = "// First Rust file";
 817//         app_state
 818//             .fs
 819//             .as_fake()
 820//             .insert_tree(
 821//                 "/src",
 822//                 json!({
 823//                     "test": {
 824//                         first_file_name: first_file_contents,
 825//                         "second.rs": "// Second Rust file",
 826//                     }
 827//                 }),
 828//             )
 829//             .await;
 830
 831//         let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
 832
 833//         let (picker, workspace, mut cx) = build_find_picker(project, cx);
 834//         let cx = &mut cx;
 835
 836//         let file_query = &first_file_name[..3];
 837//         let file_row = 1;
 838//         let file_column = 3;
 839//         assert!(file_column <= first_file_contents.len());
 840//         let query_inside_file = format!("{file_query}:{file_row}:{file_column}");
 841//         picker
 842//             .update(cx, |finder, cx| {
 843//                 finder
 844//                     .delegate
 845//                     .update_matches(query_inside_file.to_string(), cx)
 846//             })
 847//             .await;
 848//         picker.update(cx, |finder, _| {
 849//             let finder = &finder.delegate;
 850//             assert_eq!(finder.matches.len(), 1);
 851//             let latest_search_query = finder
 852//                 .latest_search_query
 853//                 .as_ref()
 854//                 .expect("Finder should have a query after the update_matches call");
 855//             assert_eq!(latest_search_query.path_like.raw_query, query_inside_file);
 856//             assert_eq!(
 857//                 latest_search_query.path_like.file_query_end,
 858//                 Some(file_query.len())
 859//             );
 860//             assert_eq!(latest_search_query.row, Some(file_row));
 861//             assert_eq!(latest_search_query.column, Some(file_column as u32));
 862//         });
 863
 864//         let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
 865//         cx.dispatch_action(SelectNext);
 866//         cx.dispatch_action(Confirm);
 867//         active_pane
 868//             .condition(cx, |pane, _| pane.active_item().is_some())
 869//             .await;
 870//         let editor = cx.update(|cx| {
 871//             let active_item = active_pane.read(cx).active_item().unwrap();
 872//             active_item.downcast::<Editor>().unwrap()
 873//         });
 874//         cx.executor().advance_clock(Duration::from_secs(2));
 875
 876//         editor.update(cx, |editor, cx| {
 877//             let all_selections = editor.selections.all_adjusted(cx);
 878//             assert_eq!(
 879//                 all_selections.len(),
 880//                 1,
 881//                 "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}"
 882//             );
 883//             let caret_selection = all_selections.into_iter().next().unwrap();
 884//             assert_eq!(caret_selection.start, caret_selection.end,
 885//                 "Caret selection should have its start and end at the same position");
 886//             assert_eq!(file_row, caret_selection.start.row + 1,
 887//                 "Query inside file should get caret with the same focus row");
 888//             assert_eq!(file_column, caret_selection.start.column as usize + 1,
 889//                 "Query inside file should get caret with the same focus column");
 890//         });
 891//     }
 892
 893//     #[gpui::test]
 894//     async fn test_row_column_numbers_query_outside_file(cx: &mut TestAppContext) {
 895//         let app_state = init_test(cx);
 896
 897//         let first_file_name = "first.rs";
 898//         let first_file_contents = "// First Rust file";
 899//         app_state
 900//             .fs
 901//             .as_fake()
 902//             .insert_tree(
 903//                 "/src",
 904//                 json!({
 905//                     "test": {
 906//                         first_file_name: first_file_contents,
 907//                         "second.rs": "// Second Rust file",
 908//                     }
 909//                 }),
 910//             )
 911//             .await;
 912
 913//         let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
 914
 915//         let (picker, workspace, mut cx) = build_find_picker(project, cx);
 916//         let cx = &mut cx;
 917
 918//         let file_query = &first_file_name[..3];
 919//         let file_row = 200;
 920//         let file_column = 300;
 921//         assert!(file_column > first_file_contents.len());
 922//         let query_outside_file = format!("{file_query}:{file_row}:{file_column}");
 923//         picker
 924//             .update(cx, |picker, cx| {
 925//                 picker
 926//                     .delegate
 927//                     .update_matches(query_outside_file.to_string(), cx)
 928//             })
 929//             .await;
 930//         picker.update(cx, |finder, _| {
 931//             let delegate = &finder.delegate;
 932//             assert_eq!(delegate.matches.len(), 1);
 933//             let latest_search_query = delegate
 934//                 .latest_search_query
 935//                 .as_ref()
 936//                 .expect("Finder should have a query after the update_matches call");
 937//             assert_eq!(latest_search_query.path_like.raw_query, query_outside_file);
 938//             assert_eq!(
 939//                 latest_search_query.path_like.file_query_end,
 940//                 Some(file_query.len())
 941//             );
 942//             assert_eq!(latest_search_query.row, Some(file_row));
 943//             assert_eq!(latest_search_query.column, Some(file_column as u32));
 944//         });
 945
 946//         let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
 947//         cx.dispatch_action(SelectNext);
 948//         cx.dispatch_action(Confirm);
 949//         active_pane
 950//             .condition(cx, |pane, _| pane.active_item().is_some())
 951//             .await;
 952//         let editor = cx.update(|cx| {
 953//             let active_item = active_pane.read(cx).active_item().unwrap();
 954//             active_item.downcast::<Editor>().unwrap()
 955//         });
 956//         cx.executor().advance_clock(Duration::from_secs(2));
 957
 958//         editor.update(cx, |editor, cx| {
 959//             let all_selections = editor.selections.all_adjusted(cx);
 960//             assert_eq!(
 961//                 all_selections.len(),
 962//                 1,
 963//                 "Expected to have 1 selection (caret) after file finder confirm, but got: {all_selections:?}"
 964//             );
 965//             let caret_selection = all_selections.into_iter().next().unwrap();
 966//             assert_eq!(caret_selection.start, caret_selection.end,
 967//                 "Caret selection should have its start and end at the same position");
 968//             assert_eq!(0, caret_selection.start.row,
 969//                 "Excessive rows (as in query outside file borders) should get trimmed to last file row");
 970//             assert_eq!(first_file_contents.len(), caret_selection.start.column as usize,
 971//                 "Excessive columns (as in query outside file borders) should get trimmed to selected row's last column");
 972//         });
 973//     }
 974
 975//     #[gpui::test]
 976//     async fn test_matching_cancellation(cx: &mut TestAppContext) {
 977//         let app_state = init_test(cx);
 978//         app_state
 979//             .fs
 980//             .as_fake()
 981//             .insert_tree(
 982//                 "/dir",
 983//                 json!({
 984//                     "hello": "",
 985//                     "goodbye": "",
 986//                     "halogen-light": "",
 987//                     "happiness": "",
 988//                     "height": "",
 989//                     "hi": "",
 990//                     "hiccup": "",
 991//                 }),
 992//             )
 993//             .await;
 994
 995//         let project = Project::test(app_state.fs.clone(), ["/dir".as_ref()], cx).await;
 996
 997//         let (picker, _, mut cx) = build_find_picker(project, cx);
 998//         let cx = &mut cx;
 999
1000//         let query = test_path_like("hi");
1001//         picker
1002//             .update(cx, |picker, cx| {
1003//                 picker.delegate.spawn_search(query.clone(), cx)
1004//             })
1005//             .await;
1006
1007//         picker.update(cx, |picker, _cx| {
1008//             assert_eq!(picker.delegate.matches.len(), 5)
1009//         });
1010
1011//         picker.update(cx, |picker, cx| {
1012//             let delegate = &mut picker.delegate;
1013//             assert!(
1014//                 delegate.matches.history.is_empty(),
1015//                 "Search matches expected"
1016//             );
1017//             let matches = delegate.matches.search.clone();
1018
1019//             // Simulate a search being cancelled after the time limit,
1020//             // returning only a subset of the matches that would have been found.
1021//             drop(delegate.spawn_search(query.clone(), cx));
1022//             delegate.set_search_matches(
1023//                 delegate.latest_search_id,
1024//                 true, // did-cancel
1025//                 query.clone(),
1026//                 vec![matches[1].clone(), matches[3].clone()],
1027//                 cx,
1028//             );
1029
1030//             // Simulate another cancellation.
1031//             drop(delegate.spawn_search(query.clone(), cx));
1032//             delegate.set_search_matches(
1033//                 delegate.latest_search_id,
1034//                 true, // did-cancel
1035//                 query.clone(),
1036//                 vec![matches[0].clone(), matches[2].clone(), matches[3].clone()],
1037//                 cx,
1038//             );
1039
1040//             assert!(
1041//                 delegate.matches.history.is_empty(),
1042//                 "Search matches expected"
1043//             );
1044//             assert_eq!(delegate.matches.search.as_slice(), &matches[0..4]);
1045//         });
1046//     }
1047
1048//     #[gpui::test]
1049//     async fn test_ignored_files(cx: &mut TestAppContext) {
1050//         let app_state = init_test(cx);
1051//         app_state
1052//             .fs
1053//             .as_fake()
1054//             .insert_tree(
1055//                 "/ancestor",
1056//                 json!({
1057//                     ".gitignore": "ignored-root",
1058//                     "ignored-root": {
1059//                         "happiness": "",
1060//                         "height": "",
1061//                         "hi": "",
1062//                         "hiccup": "",
1063//                     },
1064//                     "tracked-root": {
1065//                         ".gitignore": "height",
1066//                         "happiness": "",
1067//                         "height": "",
1068//                         "hi": "",
1069//                         "hiccup": "",
1070//                     },
1071//                 }),
1072//             )
1073//             .await;
1074
1075//         let project = Project::test(
1076//             app_state.fs.clone(),
1077//             [
1078//                 "/ancestor/tracked-root".as_ref(),
1079//                 "/ancestor/ignored-root".as_ref(),
1080//             ],
1081//             cx,
1082//         )
1083//         .await;
1084
1085//         let (picker, _, mut cx) = build_find_picker(project, cx);
1086//         let cx = &mut cx;
1087
1088//         picker
1089//             .update(cx, |picker, cx| {
1090//                 picker.delegate.spawn_search(test_path_like("hi"), cx)
1091//             })
1092//             .await;
1093//         picker.update(cx, |picker, _| assert_eq!(picker.delegate.matches.len(), 7));
1094//     }
1095
1096// #[gpui::test]
1097// async fn test_single_file_worktrees(cx: &mut TestAppContext) {
1098//     let app_state = init_test(cx);
1099//     app_state
1100//         .fs
1101//         .as_fake()
1102//         .insert_tree("/root", json!({ "the-parent-dir": { "the-file": "" } }))
1103//         .await;
1104
1105//     let project = Project::test(
1106//         app_state.fs.clone(),
1107//         ["/root/the-parent-dir/the-file".as_ref()],
1108//         cx,
1109//     )
1110//     .await;
1111
1112//     let (picker, _, mut cx) = build_find_picker(project, cx);
1113//     let cx = &mut cx;
1114
1115//     // Even though there is only one worktree, that worktree's filename
1116//     // is included in the matching, because the worktree is a single file.
1117//     picker
1118//         .update(cx, |picker, cx| {
1119//             picker.delegate.spawn_search(test_path_like("thf"), cx)
1120//         })
1121//         .await;
1122//     cx.read(|cx| {
1123//         let picker = picker.read(cx);
1124//         let delegate = &picker.delegate;
1125//         assert!(
1126//             delegate.matches.history.is_empty(),
1127//             "Search matches expected"
1128//         );
1129//         let matches = delegate.matches.search.clone();
1130//         assert_eq!(matches.len(), 1);
1131
1132//         let (file_name, file_name_positions, full_path, full_path_positions) =
1133//             delegate.labels_for_path_match(&matches[0]);
1134//         assert_eq!(file_name, "the-file");
1135//         assert_eq!(file_name_positions, &[0, 1, 4]);
1136//         assert_eq!(full_path, "the-file");
1137//         assert_eq!(full_path_positions, &[0, 1, 4]);
1138//     });
1139
1140//     // Since the worktree root is a file, searching for its name followed by a slash does
1141//     // not match anything.
1142//     picker
1143//         .update(cx, |f, cx| {
1144//             f.delegate.spawn_search(test_path_like("thf/"), cx)
1145//         })
1146//         .await;
1147//     picker.update(cx, |f, _| assert_eq!(f.delegate.matches.len(), 0));
1148// }
1149
1150// #[gpui::test]
1151// async fn test_path_distance_ordering(cx: &mut TestAppContext) {
1152//     let app_state = init_test(cx);
1153//     app_state
1154//         .fs
1155//         .as_fake()
1156//         .insert_tree(
1157//             "/root",
1158//             json!({
1159//                 "dir1": { "a.txt": "" },
1160//                 "dir2": {
1161//                     "a.txt": "",
1162//                     "b.txt": ""
1163//                 }
1164//             }),
1165//         )
1166//         .await;
1167
1168//     let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1169//     let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
1170//     let cx = &mut cx;
1171
1172//     let worktree_id = cx.read(|cx| {
1173//         let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1174//         assert_eq!(worktrees.len(), 1);
1175//         WorktreeId::from_usize(worktrees[0].id())
1176//     });
1177
1178//     // When workspace has an active item, sort items which are closer to that item
1179//     // first when they have the same name. In this case, b.txt is closer to dir2's a.txt
1180//     // so that one should be sorted earlier
1181//     let b_path = Some(dummy_found_path(ProjectPath {
1182//         worktree_id,
1183//         path: Arc::from(Path::new("/root/dir2/b.txt")),
1184//     }));
1185//     cx.dispatch_action(Toggle);
1186
1187//     let finder = cx
1188//         .add_window(|cx| {
1189//             Picker::new(
1190//                 FileFinderDelegate::new(
1191//                     workspace.downgrade(),
1192//                     workspace.read(cx).project().clone(),
1193//                     b_path,
1194//                     Vec::new(),
1195//                     cx,
1196//                 ),
1197//                 cx,
1198//             )
1199//         })
1200//         .root(cx);
1201
1202//     finder
1203//         .update(cx, |f, cx| {
1204//             f.delegate.spawn_search(test_path_like("a.txt"), cx)
1205//         })
1206//         .await;
1207
1208//     finder.read_with(cx, |f, _| {
1209//         let delegate = &f.delegate;
1210//         assert!(
1211//             delegate.matches.history.is_empty(),
1212//             "Search matches expected"
1213//         );
1214//         let matches = delegate.matches.search.clone();
1215//         assert_eq!(matches[0].path.as_ref(), Path::new("dir2/a.txt"));
1216//         assert_eq!(matches[1].path.as_ref(), Path::new("dir1/a.txt"));
1217//     });
1218// }
1219
1220// #[gpui::test]
1221// async fn test_search_worktree_without_files(cx: &mut TestAppContext) {
1222//     let app_state = init_test(cx);
1223//     app_state
1224//         .fs
1225//         .as_fake()
1226//         .insert_tree(
1227//             "/root",
1228//             json!({
1229//                 "dir1": {},
1230//                 "dir2": {
1231//                     "dir3": {}
1232//                 }
1233//             }),
1234//         )
1235//         .await;
1236
1237//     let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
1238//     let workspace = cx
1239//         .add_window(|cx| Workspace::test_new(project, cx))
1240//         .root(cx);
1241//     let finder = cx
1242//         .add_window(|cx| {
1243//             Picker::new(
1244//                 FileFinderDelegate::new(
1245//                     workspace.downgrade(),
1246//                     workspace.read(cx).project().clone(),
1247//                     None,
1248//                     Vec::new(),
1249//                     cx,
1250//                 ),
1251//                 cx,
1252//             )
1253//         })
1254//         .root(cx);
1255//     finder
1256//         .update(cx, |f, cx| {
1257//             f.delegate.spawn_search(test_path_like("dir"), cx)
1258//         })
1259//         .await;
1260//     cx.read(|cx| {
1261//         let finder = finder.read(cx);
1262//         assert_eq!(finder.delegate.matches.len(), 0);
1263//     });
1264// }
1265
1266//     #[gpui::test]
1267//     async fn test_query_history(cx: &mut gpui::TestAppContext) {
1268//         let app_state = init_test(cx);
1269
1270//         app_state
1271//             .fs
1272//             .as_fake()
1273//             .insert_tree(
1274//                 "/src",
1275//                 json!({
1276//                     "test": {
1277//                         "first.rs": "// First Rust file",
1278//                         "second.rs": "// Second Rust file",
1279//                         "third.rs": "// Third Rust file",
1280//                     }
1281//                 }),
1282//             )
1283//             .await;
1284
1285//         let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
1286//         let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
1287//         let cx = &mut cx;
1288//         let worktree_id = cx.read(|cx| {
1289//             let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1290//             assert_eq!(worktrees.len(), 1);
1291//             WorktreeId::from_usize(worktrees[0].id())
1292//         });
1293
1294//         // Open and close panels, getting their history items afterwards.
1295//         // Ensure history items get populated with opened items, and items are kept in a certain order.
1296//         // The history lags one opened buffer behind, since it's updated in the search panel only on its reopen.
1297//         //
1298//         // TODO: without closing, the opened items do not propagate their history changes for some reason
1299//         // it does work in real app though, only tests do not propagate.
1300
1301//         let initial_history = open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1302//         assert!(
1303//             initial_history.is_empty(),
1304//             "Should have no history before opening any files"
1305//         );
1306
1307//         let history_after_first =
1308//             open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1309//         assert_eq!(
1310//             history_after_first,
1311//             vec![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//             "Should show 1st opened item in the history when opening the 2nd item"
1319//         );
1320
1321//         let history_after_second =
1322//             open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
1323//         assert_eq!(
1324//             history_after_second,
1325//             vec![
1326//                 FoundPath::new(
1327//                     ProjectPath {
1328//                         worktree_id,
1329//                         path: Arc::from(Path::new("test/second.rs")),
1330//                     },
1331//                     Some(PathBuf::from("/src/test/second.rs"))
1332//                 ),
1333//                 FoundPath::new(
1334//                     ProjectPath {
1335//                         worktree_id,
1336//                         path: Arc::from(Path::new("test/first.rs")),
1337//                     },
1338//                     Some(PathBuf::from("/src/test/first.rs"))
1339//                 ),
1340//             ],
1341//             "Should show 1st and 2nd opened items in the history when opening the 3rd item. \
1342// 2nd item should be the first in the history, as the last opened."
1343//         );
1344
1345//         let history_after_third =
1346//             open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1347//         assert_eq!(
1348//             history_after_third,
1349//             vec![
1350//                 FoundPath::new(
1351//                     ProjectPath {
1352//                         worktree_id,
1353//                         path: Arc::from(Path::new("test/third.rs")),
1354//                     },
1355//                     Some(PathBuf::from("/src/test/third.rs"))
1356//                 ),
1357//                 FoundPath::new(
1358//                     ProjectPath {
1359//                         worktree_id,
1360//                         path: Arc::from(Path::new("test/second.rs")),
1361//                     },
1362//                     Some(PathBuf::from("/src/test/second.rs"))
1363//                 ),
1364//                 FoundPath::new(
1365//                     ProjectPath {
1366//                         worktree_id,
1367//                         path: Arc::from(Path::new("test/first.rs")),
1368//                     },
1369//                     Some(PathBuf::from("/src/test/first.rs"))
1370//                 ),
1371//             ],
1372//             "Should show 1st, 2nd and 3rd opened items in the history when opening the 2nd item again. \
1373// 3rd item should be the first in the history, as the last opened."
1374//         );
1375
1376//         let history_after_second_again =
1377//             open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
1378//         assert_eq!(
1379//             history_after_second_again,
1380//             vec![
1381//                 FoundPath::new(
1382//                     ProjectPath {
1383//                         worktree_id,
1384//                         path: Arc::from(Path::new("test/second.rs")),
1385//                     },
1386//                     Some(PathBuf::from("/src/test/second.rs"))
1387//                 ),
1388//                 FoundPath::new(
1389//                     ProjectPath {
1390//                         worktree_id,
1391//                         path: Arc::from(Path::new("test/third.rs")),
1392//                     },
1393//                     Some(PathBuf::from("/src/test/third.rs"))
1394//                 ),
1395//                 FoundPath::new(
1396//                     ProjectPath {
1397//                         worktree_id,
1398//                         path: Arc::from(Path::new("test/first.rs")),
1399//                     },
1400//                     Some(PathBuf::from("/src/test/first.rs"))
1401//                 ),
1402//             ],
1403//             "Should show 1st, 2nd and 3rd opened items in the history when opening the 3rd item again. \
1404// 2nd item, as the last opened, 3rd item should go next as it was opened right before."
1405//         );
1406//     }
1407
1408// #[gpui::test]
1409// async fn test_external_files_history(cx: &mut gpui::TestAppContext) {
1410//     let app_state = init_test(cx);
1411
1412//     app_state
1413//         .fs
1414//         .as_fake()
1415//         .insert_tree(
1416//             "/src",
1417//             json!({
1418//                 "test": {
1419//                     "first.rs": "// First Rust file",
1420//                     "second.rs": "// Second Rust file",
1421//                 }
1422//             }),
1423//         )
1424//         .await;
1425
1426//     app_state
1427//         .fs
1428//         .as_fake()
1429//         .insert_tree(
1430//             "/external-src",
1431//             json!({
1432//                 "test": {
1433//                     "third.rs": "// Third Rust file",
1434//                     "fourth.rs": "// Fourth Rust file",
1435//                 }
1436//             }),
1437//         )
1438//         .await;
1439
1440//     let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
1441//     cx.update(|cx| {
1442//         project.update(cx, |project, cx| {
1443//             project.find_or_create_local_worktree("/external-src", false, cx)
1444//         })
1445//     })
1446//     .detach();
1447//     cx.background_executor.run_until_parked();
1448
1449//     let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
1450//     let cx = &mut cx;
1451//     let worktree_id = cx.read(|cx| {
1452//         let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1453//         assert_eq!(worktrees.len(), 1,);
1454
1455//         WorktreeId::from_usize(worktrees[0].id())
1456//     });
1457//     workspace
1458//         .update(cx, |workspace, cx| {
1459//             workspace.open_abs_path(PathBuf::from("/external-src/test/third.rs"), false, cx)
1460//         })
1461//         .detach();
1462//     cx.background_executor.run_until_parked();
1463//     let external_worktree_id = cx.read(|cx| {
1464//         let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1465//         assert_eq!(
1466//             worktrees.len(),
1467//             2,
1468//             "External file should get opened in a new worktree"
1469//         );
1470
1471//         WorktreeId::from_usize(
1472//             worktrees
1473//                 .into_iter()
1474//                 .find(|worktree| worktree.entity_id() != worktree_id.to_usize())
1475//                 .expect("New worktree should have a different id")
1476//                 .id(),
1477//         )
1478//     });
1479//     close_active_item(&workspace, cx).await;
1480
1481//     let initial_history_items =
1482//         open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1483//     assert_eq!(
1484//         initial_history_items,
1485//         vec![FoundPath::new(
1486//             ProjectPath {
1487//                 worktree_id: external_worktree_id,
1488//                 path: Arc::from(Path::new("")),
1489//             },
1490//             Some(PathBuf::from("/external-src/test/third.rs"))
1491//         )],
1492//         "Should show external file with its full path in the history after it was open"
1493//     );
1494
1495//     let updated_history_items =
1496//         open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1497//     assert_eq!(
1498//         updated_history_items,
1499//         vec![
1500//             FoundPath::new(
1501//                 ProjectPath {
1502//                     worktree_id,
1503//                     path: Arc::from(Path::new("test/second.rs")),
1504//                 },
1505//                 Some(PathBuf::from("/src/test/second.rs"))
1506//             ),
1507//             FoundPath::new(
1508//                 ProjectPath {
1509//                     worktree_id: external_worktree_id,
1510//                     path: Arc::from(Path::new("")),
1511//                 },
1512//                 Some(PathBuf::from("/external-src/test/third.rs"))
1513//             ),
1514//         ],
1515//         "Should keep external file with history updates",
1516//     );
1517// }
1518
1519// #[gpui::test]
1520// async fn test_toggle_panel_new_selections(cx: &mut gpui::TestAppContext) {
1521//     let app_state = init_test(cx);
1522
1523//     app_state
1524//         .fs
1525//         .as_fake()
1526//         .insert_tree(
1527//             "/src",
1528//             json!({
1529//                 "test": {
1530//                     "first.rs": "// First Rust file",
1531//                     "second.rs": "// Second Rust file",
1532//                     "third.rs": "// Third Rust file",
1533//                 }
1534//             }),
1535//         )
1536//         .await;
1537
1538//     let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
1539//     let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
1540//     let cx = &mut cx;
1541
1542//     // generate some history to select from
1543//     open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1544//     cx.executor().run_until_parked();
1545//     open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1546//     open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
1547//     let current_history = open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1548
1549//     for expected_selected_index in 0..current_history.len() {
1550//         cx.dispatch_action(Toggle);
1551//         let selected_index = workspace.update(cx, |workspace, cx| {
1552//             workspace
1553//                 .current_modal::<FileFinder>(cx)
1554//                 .unwrap()
1555//                 .read(cx)
1556//                 .picker
1557//                 .read(cx)
1558//                 .delegate
1559//                 .selected_index()
1560//         });
1561//         assert_eq!(
1562//             selected_index, expected_selected_index,
1563//             "Should select the next item in the history"
1564//         );
1565//     }
1566
1567//     cx.dispatch_action(Toggle);
1568//     let selected_index = workspace.update(cx, |workspace, cx| {
1569//         workspace
1570//             .current_modal::<FileFinder>(cx)
1571//             .unwrap()
1572//             .read(cx)
1573//             .picker
1574//             .read(cx)
1575//             .delegate
1576//             .selected_index()
1577//     });
1578//     assert_eq!(
1579//         selected_index, 0,
1580//         "Should wrap around the history and start all over"
1581//     );
1582// }
1583
1584// #[gpui::test]
1585// async fn test_search_preserves_history_items(cx: &mut gpui::TestAppContext) {
1586//     let app_state = init_test(cx);
1587
1588//     app_state
1589//         .fs
1590//         .as_fake()
1591//         .insert_tree(
1592//             "/src",
1593//             json!({
1594//                 "test": {
1595//                     "first.rs": "// First Rust file",
1596//                     "second.rs": "// Second Rust file",
1597//                     "third.rs": "// Third Rust file",
1598//                     "fourth.rs": "// Fourth Rust file",
1599//                 }
1600//             }),
1601//         )
1602//         .await;
1603
1604//     let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
1605//     let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
1606//     let cx = &mut cx;
1607//     let worktree_id = cx.read(|cx| {
1608//         let worktrees = workspace.read(cx).worktrees(cx).collect::<Vec<_>>();
1609//         assert_eq!(worktrees.len(), 1,);
1610
1611//         WorktreeId::from_usize(worktrees[0].entity_id())
1612//     });
1613
1614//     // generate some history to select from
1615//     open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1616//     open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1617//     open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
1618//     open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1619
1620//     cx.dispatch_action(Toggle);
1621//     let first_query = "f";
1622//     let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
1623//     finder
1624//         .update(cx, |finder, cx| {
1625//             finder.delegate.update_matches(first_query.to_string(), cx)
1626//         })
1627//         .await;
1628//     finder.read_with(cx, |finder, _| {
1629//         let delegate = &finder.delegate;
1630//         assert_eq!(delegate.matches.history.len(), 1, "Only one history item contains {first_query}, it should be present and others should be filtered out");
1631//         let history_match = delegate.matches.history.first().unwrap();
1632//         assert!(history_match.1.is_some(), "Should have path matches for history items after querying");
1633//         assert_eq!(history_match.0, FoundPath::new(
1634//             ProjectPath {
1635//                 worktree_id,
1636//                 path: Arc::from(Path::new("test/first.rs")),
1637//             },
1638//             Some(PathBuf::from("/src/test/first.rs"))
1639//         ));
1640//         assert_eq!(delegate.matches.search.len(), 1, "Only one non-history item contains {first_query}, it should be present");
1641//         assert_eq!(delegate.matches.search.first().unwrap().path.as_ref(), Path::new("test/fourth.rs"));
1642//     });
1643
1644//     let second_query = "fsdasdsa";
1645//     let finder = workspace.update(cx, |workspace, cx| {
1646//         workspace
1647//             .current_modal::<FileFinder>(cx)
1648//             .unwrap()
1649//             .read(cx)
1650//             .picker
1651//     });
1652//     finder
1653//         .update(cx, |finder, cx| {
1654//             finder.delegate.update_matches(second_query.to_string(), cx)
1655//         })
1656//         .await;
1657//     finder.update(cx, |finder, _| {
1658//         let delegate = &finder.delegate;
1659//         assert!(
1660//             delegate.matches.history.is_empty(),
1661//             "No history entries should match {second_query}"
1662//         );
1663//         assert!(
1664//             delegate.matches.search.is_empty(),
1665//             "No search entries should match {second_query}"
1666//         );
1667//     });
1668
1669//     let first_query_again = first_query;
1670
1671//     let finder = workspace.update(cx, |workspace, cx| {
1672//         workspace
1673//             .current_modal::<FileFinder>(cx)
1674//             .unwrap()
1675//             .read(cx)
1676//             .picker
1677//     });
1678//     finder
1679//         .update(cx, |finder, cx| {
1680//             finder
1681//                 .delegate
1682//                 .update_matches(first_query_again.to_string(), cx)
1683//         })
1684//         .await;
1685//     finder.read_with(cx, |finder, _| {
1686//         let delegate = &finder.delegate;
1687//         assert_eq!(delegate.matches.history.len(), 1, "Only one history item contains {first_query_again}, it should be present and others should be filtered out, even after non-matching query");
1688//         let history_match = delegate.matches.history.first().unwrap();
1689//         assert!(history_match.1.is_some(), "Should have path matches for history items after querying");
1690//         assert_eq!(history_match.0, FoundPath::new(
1691//             ProjectPath {
1692//                 worktree_id,
1693//                 path: Arc::from(Path::new("test/first.rs")),
1694//             },
1695//             Some(PathBuf::from("/src/test/first.rs"))
1696//         ));
1697//         assert_eq!(delegate.matches.search.len(), 1, "Only one non-history item contains {first_query_again}, it should be present, even after non-matching query");
1698//         assert_eq!(delegate.matches.search.first().unwrap().path.as_ref(), Path::new("test/fourth.rs"));
1699//     });
1700// }
1701
1702// #[gpui::test]
1703// async fn test_history_items_vs_very_good_external_match(cx: &mut gpui::TestAppContext) {
1704//     let app_state = init_test(cx);
1705
1706//     app_state
1707//         .fs
1708//         .as_fake()
1709//         .insert_tree(
1710//             "/src",
1711//             json!({
1712//                 "collab_ui": {
1713//                     "first.rs": "// First Rust file",
1714//                     "second.rs": "// Second Rust file",
1715//                     "third.rs": "// Third Rust file",
1716//                     "collab_ui.rs": "// Fourth Rust file",
1717//                 }
1718//             }),
1719//         )
1720//         .await;
1721
1722//     let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
1723//     let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
1724//     let cx = &mut cx;
1725//     // generate some history to select from
1726//     open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1727//     open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1728//     open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
1729//     open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
1730
1731//     cx.dispatch_action(Toggle);
1732//     let query = "collab_ui";
1733//     let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
1734//     finder
1735//         .update(cx, |finder, cx| {
1736//             finder.delegate.update_matches(query.to_string(), cx)
1737//         })
1738//         .await;
1739//     finder.read_with(cx, |finder, _| {
1740//         let delegate = &finder.delegate;
1741//         assert!(
1742//             delegate.matches.history.is_empty(),
1743//             "History items should not math query {query}, they should be matched by name only"
1744//         );
1745
1746//         let search_entries = delegate
1747//             .matches
1748//             .search
1749//             .iter()
1750//             .map(|path_match| path_match.path.to_path_buf())
1751//             .collect::<Vec<_>>();
1752//         assert_eq!(
1753//             search_entries,
1754//             vec![
1755//                 PathBuf::from("collab_ui/collab_ui.rs"),
1756//                 PathBuf::from("collab_ui/third.rs"),
1757//                 PathBuf::from("collab_ui/first.rs"),
1758//                 PathBuf::from("collab_ui/second.rs"),
1759//             ],
1760//             "Despite all search results having the same directory name, the most matching one should be on top"
1761//         );
1762//     });
1763// }
1764
1765// #[gpui::test]
1766// async fn test_nonexistent_history_items_not_shown(cx: &mut gpui::TestAppContext) {
1767//     let app_state = init_test(cx);
1768
1769//     app_state
1770//         .fs
1771//         .as_fake()
1772//         .insert_tree(
1773//             "/src",
1774//             json!({
1775//                 "test": {
1776//                     "first.rs": "// First Rust file",
1777//                     "nonexistent.rs": "// Second Rust file",
1778//                     "third.rs": "// Third Rust file",
1779//                 }
1780//             }),
1781//         )
1782//         .await;
1783
1784//     let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await;
1785//     let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
1786//     let cx = &mut cx;
1787//     // generate some history to select from
1788//     open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1789//     open_close_queried_buffer("non", 1, "nonexistent.rs", &workspace, cx).await;
1790//     open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
1791//     open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
1792
1793//     cx.dispatch_action(Toggle);
1794//     let query = "rs";
1795//     let finder = cx.read(|cx| workspace.read(cx).current_modal::<FileFinder>().unwrap());
1796//     finder
1797//         .update(cx, |finder, cx| {
1798//             finder.picker.update(cx, |picker, cx| {
1799//                 picker.delegate.update_matches(query.to_string(), cx)
1800//             })
1801//         })
1802//         .await;
1803//     finder.update(cx, |finder, _| {
1804//         let history_entries = finder.delegate
1805//             .matches
1806//             .history
1807//             .iter()
1808//             .map(|(_, path_match)| path_match.as_ref().expect("should have a path match").path.to_path_buf())
1809//             .collect::<Vec<_>>();
1810//         assert_eq!(
1811//             history_entries,
1812//             vec![
1813//                 PathBuf::from("test/first.rs"),
1814//                 PathBuf::from("test/third.rs"),
1815//             ],
1816//             "Should have all opened files in the history, except the ones that do not exist on disk"
1817//         );
1818//     });
1819// }
1820
1821//     async fn open_close_queried_buffer(
1822//         input: &str,
1823//         expected_matches: usize,
1824//         expected_editor_title: &str,
1825//         workspace: &View<Workspace>,
1826//         cx: &mut gpui::VisualTestContext<'_>,
1827//     ) -> Vec<FoundPath> {
1828//         cx.dispatch_action(Toggle);
1829//         let picker = workspace.update(cx, |workspace, cx| {
1830//             workspace
1831//                 .current_modal::<FileFinder>(cx)
1832//                 .unwrap()
1833//                 .read(cx)
1834//                 .picker
1835//                 .clone()
1836//         });
1837//         picker
1838//             .update(cx, |finder, cx| {
1839//                 finder.delegate.update_matches(input.to_string(), cx)
1840//             })
1841//             .await;
1842//         let history_items = picker.update(cx, |finder, _| {
1843//             assert_eq!(
1844//                 finder.delegate.matches.len(),
1845//                 expected_matches,
1846//                 "Unexpected number of matches found for query {input}"
1847//             );
1848//             finder.delegate.history_items.clone()
1849//         });
1850
1851//         let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
1852//         cx.dispatch_action(SelectNext);
1853//         cx.dispatch_action(Confirm);
1854//         cx.background_executor.run_until_parked();
1855//         active_pane
1856//             .condition(cx, |pane, _| pane.active_item().is_some())
1857//             .await;
1858//         cx.read(|cx| {
1859//             let active_item = active_pane.read(cx).active_item().unwrap();
1860//             let active_editor_title = active_item
1861//                 .to_any()
1862//                 .downcast::<Editor>()
1863//                 .unwrap()
1864//                 .read(cx)
1865//                 .title(cx);
1866//             assert_eq!(
1867//                 expected_editor_title, active_editor_title,
1868//                 "Unexpected editor title for query {input}"
1869//             );
1870//         });
1871
1872//         close_active_item(workspace, cx).await;
1873
1874//         history_items
1875//     }
1876
1877//     async fn close_active_item(workspace: &View<Workspace>, cx: &mut VisualTestContext<'_>) {
1878//         let mut original_items = HashMap::new();
1879//         cx.read(|cx| {
1880//             for pane in workspace.read(cx).panes() {
1881//                 let pane_id = pane.entity_id();
1882//                 let pane = pane.read(cx);
1883//                 let insertion_result = original_items.insert(pane_id, pane.items().count());
1884//                 assert!(insertion_result.is_none(), "Pane id {pane_id} collision");
1885//             }
1886//         });
1887
1888//         let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
1889//         active_pane
1890//             .update(cx, |pane, cx| {
1891//                 pane.close_active_item(&workspace::CloseActiveItem { save_intent: None }, cx)
1892//                     .unwrap()
1893//             })
1894//             .await
1895//             .unwrap();
1896//         cx.background_executor.run_until_parked();
1897//         cx.read(|cx| {
1898//             for pane in workspace.read(cx).panes() {
1899//                 let pane_id = pane.entity_id();
1900//                 let pane = pane.read(cx);
1901//                 match original_items.remove(&pane_id) {
1902//                     Some(original_items) => {
1903//                         assert_eq!(
1904//                             pane.items().count(),
1905//                             original_items.saturating_sub(1),
1906//                             "Pane id {pane_id} should have item closed"
1907//                         );
1908//                     }
1909//                     None => panic!("Pane id {pane_id} not found in original items"),
1910//                 }
1911//             }
1912//         });
1913//         assert!(
1914//             original_items.len() <= 1,
1915//             "At most one panel should got closed"
1916//         );
1917//     }
1918
1919//     fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
1920//         cx.update(|cx| {
1921//             let state = AppState::test(cx);
1922//             theme::init(cx);
1923//             language::init(cx);
1924//             super::init(cx);
1925//             editor::init(cx);
1926//             workspace::init_settings(cx);
1927//             Project::init_settings(cx);
1928//             state
1929//         })
1930//     }
1931
1932//     fn test_path_like(test_str: &str) -> PathLikeWithPosition<FileSearchQuery> {
1933//         PathLikeWithPosition::parse_str(test_str, |path_like_str| {
1934//             Ok::<_, std::convert::Infallible>(FileSearchQuery {
1935//                 raw_query: test_str.to_owned(),
1936//                 file_query_end: if path_like_str == test_str {
1937//                     None
1938//                 } else {
1939//                     Some(path_like_str.len())
1940//                 },
1941//             })
1942//         })
1943//         .unwrap()
1944//     }
1945
1946//     fn dummy_found_path(project_path: ProjectPath) -> FoundPath {
1947//         FoundPath {
1948//             project: project_path,
1949//             absolute: None,
1950//         }
1951//     }
1952
1953//     fn build_find_picker(
1954//         project: Model<Project>,
1955//         cx: &mut TestAppContext,
1956//     ) -> (
1957//         View<Picker<FileFinderDelegate>>,
1958//         View<Workspace>,
1959//         VisualTestContext,
1960//     ) {
1961//         let (workspace, mut cx) = cx.add_window_view(|cx| Workspace::test_new(project, cx));
1962//         cx.dispatch_action(Toggle);
1963//         let picker = workspace.update(&mut cx, |workspace, cx| {
1964//             workspace
1965//                 .current_modal::<FileFinder>(cx)
1966//                 .unwrap()
1967//                 .read(cx)
1968//                 .picker
1969//                 .clone()
1970//         });
1971//         (picker, workspace, cx)
1972//     }
1973// }