terminal_path_like_target.rs

   1use super::{HoverTarget, HoveredWord, TerminalView};
   2use anyhow::{Context as _, Result};
   3use editor::Editor;
   4use gpui::{App, AppContext, Context, Task, WeakEntity, Window};
   5use itertools::Itertools;
   6use project::{Entry, Metadata};
   7use std::path::{Path, PathBuf};
   8use terminal::PathLikeTarget;
   9use util::{ResultExt, debug_panic, paths::PathWithPosition};
  10use workspace::{OpenOptions, OpenVisible, Workspace};
  11
  12/// The way we found the open target. This is important to have for test assertions.
  13/// For example, remote projects never look in the file system.
  14#[cfg(test)]
  15#[derive(Debug, Clone, Copy, Eq, PartialEq)]
  16enum OpenTargetFoundBy {
  17    WorktreeExact,
  18    WorktreeScan,
  19    FileSystemBackground,
  20}
  21
  22#[cfg(test)]
  23#[derive(Debug, Clone, Copy, Eq, PartialEq)]
  24enum BackgroundFsChecks {
  25    Enabled,
  26    Disabled,
  27}
  28
  29#[derive(Debug, Clone)]
  30enum OpenTarget {
  31    Worktree(PathWithPosition, Entry, #[cfg(test)] OpenTargetFoundBy),
  32    File(PathWithPosition, Metadata),
  33}
  34
  35impl OpenTarget {
  36    fn is_file(&self) -> bool {
  37        match self {
  38            OpenTarget::Worktree(_, entry, ..) => entry.is_file(),
  39            OpenTarget::File(_, metadata) => !metadata.is_dir,
  40        }
  41    }
  42
  43    fn is_dir(&self) -> bool {
  44        match self {
  45            OpenTarget::Worktree(_, entry, ..) => entry.is_dir(),
  46            OpenTarget::File(_, metadata) => metadata.is_dir,
  47        }
  48    }
  49
  50    fn path(&self) -> &PathWithPosition {
  51        match self {
  52            OpenTarget::Worktree(path, ..) => path,
  53            OpenTarget::File(path, _) => path,
  54        }
  55    }
  56
  57    #[cfg(test)]
  58    fn found_by(&self) -> OpenTargetFoundBy {
  59        match self {
  60            OpenTarget::Worktree(.., found_by) => *found_by,
  61            OpenTarget::File(..) => OpenTargetFoundBy::FileSystemBackground,
  62        }
  63    }
  64}
  65
  66pub(super) fn hover_path_like_target(
  67    workspace: &WeakEntity<Workspace>,
  68    hovered_word: HoveredWord,
  69    path_like_target: &PathLikeTarget,
  70    cx: &mut Context<TerminalView>,
  71) -> Task<()> {
  72    #[cfg(not(test))]
  73    {
  74        possible_hover_target(workspace, hovered_word, path_like_target, cx)
  75    }
  76    #[cfg(test)]
  77    {
  78        possible_hover_target(
  79            workspace,
  80            hovered_word,
  81            path_like_target,
  82            cx,
  83            BackgroundFsChecks::Enabled,
  84        )
  85    }
  86}
  87
  88fn possible_hover_target(
  89    workspace: &WeakEntity<Workspace>,
  90    hovered_word: HoveredWord,
  91    path_like_target: &PathLikeTarget,
  92    cx: &mut Context<TerminalView>,
  93    #[cfg(test)] background_fs_checks: BackgroundFsChecks,
  94) -> Task<()> {
  95    let file_to_open_task = possible_open_target(
  96        workspace,
  97        path_like_target,
  98        cx,
  99        #[cfg(test)]
 100        background_fs_checks,
 101    );
 102    cx.spawn(async move |terminal_view, cx| {
 103        let file_to_open = file_to_open_task.await;
 104        terminal_view
 105            .update(cx, |terminal_view, _| match file_to_open {
 106                Some(OpenTarget::File(path, _) | OpenTarget::Worktree(path, ..)) => {
 107                    terminal_view.hover = Some(HoverTarget {
 108                        tooltip: path.to_string(|path| path.to_string_lossy().to_string()),
 109                        hovered_word,
 110                    });
 111                }
 112                None => {
 113                    terminal_view.hover = None;
 114                }
 115            })
 116            .ok();
 117    })
 118}
 119
 120fn possible_open_target(
 121    workspace: &WeakEntity<Workspace>,
 122    path_like_target: &PathLikeTarget,
 123    cx: &App,
 124    #[cfg(test)] background_fs_checks: BackgroundFsChecks,
 125) -> Task<Option<OpenTarget>> {
 126    let Some(workspace) = workspace.upgrade() else {
 127        return Task::ready(None);
 128    };
 129    // We have to check for both paths, as on Unix, certain paths with positions are valid file paths too.
 130    // We can be on FS remote part, without real FS, so cannot canonicalize or check for existence the path right away.
 131    let mut potential_paths = Vec::new();
 132    let cwd = path_like_target.terminal_dir.as_ref();
 133    let maybe_path = &path_like_target.maybe_path;
 134    let original_path = PathWithPosition::from_path(PathBuf::from(maybe_path));
 135    let path_with_position = PathWithPosition::parse_str(maybe_path);
 136    let worktree_candidates = workspace
 137        .read(cx)
 138        .worktrees(cx)
 139        .sorted_by_key(|worktree| {
 140            let worktree_root = worktree.read(cx).abs_path();
 141            match cwd.and_then(|cwd| worktree_root.strip_prefix(cwd).ok()) {
 142                Some(cwd_child) => cwd_child.components().count(),
 143                None => usize::MAX,
 144            }
 145        })
 146        .collect::<Vec<_>>();
 147    // Since we do not check paths via FS and joining, we need to strip off potential `./`, `a/`, `b/` prefixes out of it.
 148    const GIT_DIFF_PATH_PREFIXES: &[&str] = &["a", "b"];
 149    for prefix_str in GIT_DIFF_PATH_PREFIXES.iter().chain(std::iter::once(&".")) {
 150        if let Some(stripped) = original_path.path.strip_prefix(prefix_str).ok() {
 151            potential_paths.push(PathWithPosition {
 152                path: stripped.to_owned(),
 153                row: original_path.row,
 154                column: original_path.column,
 155            });
 156        }
 157        if let Some(stripped) = path_with_position.path.strip_prefix(prefix_str).ok() {
 158            potential_paths.push(PathWithPosition {
 159                path: stripped.to_owned(),
 160                row: path_with_position.row,
 161                column: path_with_position.column,
 162            });
 163        }
 164    }
 165
 166    let insert_both_paths = original_path != path_with_position;
 167    potential_paths.insert(0, original_path);
 168    if insert_both_paths {
 169        potential_paths.insert(1, path_with_position);
 170    }
 171
 172    // If we won't find paths "easily", we can traverse the entire worktree to look what ends with the potential path suffix.
 173    // That will be slow, though, so do the fast checks first.
 174    let mut worktree_paths_to_check = Vec::new();
 175    let mut is_cwd_in_worktree = false;
 176    let mut open_target = None;
 177    'worktree_loop: for worktree in &worktree_candidates {
 178        let worktree_root = worktree.read(cx).abs_path();
 179        let mut paths_to_check = Vec::with_capacity(potential_paths.len());
 180        let relative_cwd = cwd
 181            .and_then(|cwd| cwd.strip_prefix(&worktree_root).ok())
 182            .and_then(|cwd_stripped| {
 183                (cwd_stripped != Path::new("")).then(|| {
 184                    is_cwd_in_worktree = true;
 185                    cwd_stripped
 186                })
 187            });
 188
 189        for path_with_position in &potential_paths {
 190            let path_to_check = if worktree_root.ends_with(&path_with_position.path) {
 191                let root_path_with_position = PathWithPosition {
 192                    path: worktree_root.to_path_buf(),
 193                    row: path_with_position.row,
 194                    column: path_with_position.column,
 195                };
 196                match worktree.read(cx).root_entry() {
 197                    Some(root_entry) => {
 198                        open_target = Some(OpenTarget::Worktree(
 199                            root_path_with_position,
 200                            root_entry.clone(),
 201                            #[cfg(test)]
 202                            OpenTargetFoundBy::WorktreeExact,
 203                        ));
 204                        break 'worktree_loop;
 205                    }
 206                    None => root_path_with_position,
 207                }
 208            } else {
 209                PathWithPosition {
 210                    path: path_with_position
 211                        .path
 212                        .strip_prefix(&worktree_root)
 213                        .unwrap_or(&path_with_position.path)
 214                        .to_owned(),
 215                    row: path_with_position.row,
 216                    column: path_with_position.column,
 217                }
 218            };
 219
 220            if path_to_check.path.is_relative()
 221                && !worktree.read(cx).is_single_file()
 222                && let Some(entry) = relative_cwd
 223                    .and_then(|relative_cwd| {
 224                        worktree
 225                            .read(cx)
 226                            .entry_for_path(&relative_cwd.join(&path_to_check.path))
 227                    })
 228                    .or_else(|| worktree.read(cx).entry_for_path(&path_to_check.path))
 229            {
 230                open_target = Some(OpenTarget::Worktree(
 231                    PathWithPosition {
 232                        path: worktree_root.join(&entry.path),
 233                        row: path_to_check.row,
 234                        column: path_to_check.column,
 235                    },
 236                    entry.clone(),
 237                    #[cfg(test)]
 238                    OpenTargetFoundBy::WorktreeExact,
 239                ));
 240                break 'worktree_loop;
 241            }
 242
 243            paths_to_check.push(path_to_check);
 244        }
 245
 246        if !paths_to_check.is_empty() {
 247            worktree_paths_to_check.push((worktree.clone(), paths_to_check));
 248        }
 249    }
 250
 251    #[cfg(not(test))]
 252    let enable_background_fs_checks = workspace.read(cx).project().read(cx).is_local();
 253    #[cfg(test)]
 254    let enable_background_fs_checks = background_fs_checks == BackgroundFsChecks::Enabled;
 255
 256    if open_target.is_some() {
 257        // We we want to prefer open targets found via background fs checks over worktree matches,
 258        // however we can return early if either:
 259        //   - This is a remote project, or
 260        //   - If the terminal working directory is inside of at least one worktree
 261        if !enable_background_fs_checks || is_cwd_in_worktree {
 262            return Task::ready(open_target);
 263        }
 264    }
 265
 266    // Before entire worktree traversal(s), make an attempt to do FS checks if available.
 267    let fs_paths_to_check =
 268        if enable_background_fs_checks {
 269            let fs_cwd_paths_to_check = cwd
 270                .iter()
 271                .flat_map(|cwd| {
 272                    let mut paths_to_check = Vec::new();
 273                    for path_to_check in &potential_paths {
 274                        let maybe_path = &path_to_check.path;
 275                        if path_to_check.path.is_relative() {
 276                            paths_to_check.push(PathWithPosition {
 277                                path: cwd.join(&maybe_path),
 278                                row: path_to_check.row,
 279                                column: path_to_check.column,
 280                            });
 281                        }
 282                    }
 283                    paths_to_check
 284                })
 285                .collect::<Vec<_>>();
 286            fs_cwd_paths_to_check
 287                .into_iter()
 288                .chain(
 289                    potential_paths
 290                        .into_iter()
 291                        .flat_map(|path_to_check| {
 292                            let mut paths_to_check = Vec::new();
 293                            let maybe_path = &path_to_check.path;
 294                            if maybe_path.starts_with("~") {
 295                                if let Some(home_path) = maybe_path.strip_prefix("~").ok().and_then(
 296                                    |stripped_maybe_path| {
 297                                        Some(dirs::home_dir()?.join(stripped_maybe_path))
 298                                    },
 299                                ) {
 300                                    paths_to_check.push(PathWithPosition {
 301                                        path: home_path,
 302                                        row: path_to_check.row,
 303                                        column: path_to_check.column,
 304                                    });
 305                                }
 306                            } else {
 307                                paths_to_check.push(PathWithPosition {
 308                                    path: maybe_path.clone(),
 309                                    row: path_to_check.row,
 310                                    column: path_to_check.column,
 311                                });
 312                                if maybe_path.is_relative() {
 313                                    for worktree in &worktree_candidates {
 314                                        if !worktree.read(cx).is_single_file() {
 315                                            paths_to_check.push(PathWithPosition {
 316                                                path: worktree.read(cx).abs_path().join(maybe_path),
 317                                                row: path_to_check.row,
 318                                                column: path_to_check.column,
 319                                            });
 320                                        }
 321                                    }
 322                                }
 323                            }
 324                            paths_to_check
 325                        })
 326                        .collect::<Vec<_>>(),
 327                )
 328                .collect()
 329        } else {
 330            Vec::new()
 331        };
 332
 333    let fs = workspace.read(cx).project().read(cx).fs().clone();
 334    let background_fs_checks_task = cx.background_spawn(async move {
 335        for mut path_to_check in fs_paths_to_check {
 336            if let Some(fs_path_to_check) = fs.canonicalize(&path_to_check.path).await.ok()
 337                && let Some(metadata) = fs.metadata(&fs_path_to_check).await.ok().flatten()
 338            {
 339                if open_target
 340                    .as_ref()
 341                    .map(|open_target| open_target.path().path != fs_path_to_check)
 342                    .unwrap_or(true)
 343                {
 344                    path_to_check.path = fs_path_to_check;
 345                    return Some(OpenTarget::File(path_to_check, metadata));
 346                }
 347
 348                break;
 349            }
 350        }
 351
 352        open_target
 353    });
 354
 355    cx.spawn(async move |cx| {
 356        background_fs_checks_task.await.or_else(|| {
 357            for (worktree, worktree_paths_to_check) in worktree_paths_to_check {
 358                let found_entry = worktree
 359                    .update(cx, |worktree, _| -> Option<OpenTarget> {
 360                        let worktree_root = worktree.abs_path();
 361                        let traversal = worktree.traverse_from_path(true, true, false, "".as_ref());
 362                        for entry in traversal {
 363                            if let Some(path_in_worktree) = worktree_paths_to_check
 364                                .iter()
 365                                .find(|path_to_check| entry.path.ends_with(&path_to_check.path))
 366                            {
 367                                return Some(OpenTarget::Worktree(
 368                                    PathWithPosition {
 369                                        path: worktree_root.join(&entry.path),
 370                                        row: path_in_worktree.row,
 371                                        column: path_in_worktree.column,
 372                                    },
 373                                    entry.clone(),
 374                                    #[cfg(test)]
 375                                    OpenTargetFoundBy::WorktreeScan,
 376                                ));
 377                            }
 378                        }
 379                        None
 380                    })
 381                    .ok()?;
 382                if let Some(found_entry) = found_entry {
 383                    return Some(found_entry);
 384                }
 385            }
 386            None
 387        })
 388    })
 389}
 390
 391pub(super) fn open_path_like_target(
 392    workspace: &WeakEntity<Workspace>,
 393    terminal_view: &mut TerminalView,
 394    path_like_target: &PathLikeTarget,
 395    window: &mut Window,
 396    cx: &mut Context<TerminalView>,
 397) {
 398    #[cfg(not(test))]
 399    {
 400        possibly_open_target(workspace, terminal_view, path_like_target, window, cx)
 401            .detach_and_log_err(cx)
 402    }
 403    #[cfg(test)]
 404    {
 405        possibly_open_target(
 406            workspace,
 407            terminal_view,
 408            path_like_target,
 409            window,
 410            cx,
 411            BackgroundFsChecks::Enabled,
 412        )
 413        .detach_and_log_err(cx)
 414    }
 415}
 416
 417fn possibly_open_target(
 418    workspace: &WeakEntity<Workspace>,
 419    terminal_view: &mut TerminalView,
 420    path_like_target: &PathLikeTarget,
 421    window: &mut Window,
 422    cx: &mut Context<TerminalView>,
 423    #[cfg(test)] background_fs_checks: BackgroundFsChecks,
 424) -> Task<Result<Option<OpenTarget>>> {
 425    if terminal_view.hover.is_none() {
 426        return Task::ready(Ok(None));
 427    }
 428    let workspace = workspace.clone();
 429    let path_like_target = path_like_target.clone();
 430    cx.spawn_in(window, async move |terminal_view, cx| {
 431        let Some(open_target) = terminal_view
 432            .update(cx, |_, cx| {
 433                possible_open_target(
 434                    &workspace,
 435                    &path_like_target,
 436                    cx,
 437                    #[cfg(test)]
 438                    background_fs_checks,
 439                )
 440            })?
 441            .await
 442        else {
 443            return Ok(None);
 444        };
 445
 446        let path_to_open = open_target.path();
 447        let opened_items = workspace
 448            .update_in(cx, |workspace, window, cx| {
 449                workspace.open_paths(
 450                    vec![path_to_open.path.clone()],
 451                    OpenOptions {
 452                        visible: Some(OpenVisible::OnlyDirectories),
 453                        ..Default::default()
 454                    },
 455                    None,
 456                    window,
 457                    cx,
 458                )
 459            })
 460            .context("workspace update")?
 461            .await;
 462        if opened_items.len() != 1 {
 463            debug_panic!(
 464                "Received {} items for one path {path_to_open:?}",
 465                opened_items.len(),
 466            );
 467        }
 468
 469        if let Some(opened_item) = opened_items.first() {
 470            if open_target.is_file() {
 471                if let Some(Ok(opened_item)) = opened_item {
 472                    if let Some(row) = path_to_open.row {
 473                        let col = path_to_open.column.unwrap_or(0);
 474                        if let Some(active_editor) = opened_item.downcast::<Editor>() {
 475                            active_editor
 476                                .downgrade()
 477                                .update_in(cx, |editor, window, cx| {
 478                                    editor.go_to_singleton_buffer_point(
 479                                        language::Point::new(
 480                                            row.saturating_sub(1),
 481                                            col.saturating_sub(1),
 482                                        ),
 483                                        window,
 484                                        cx,
 485                                    )
 486                                })
 487                                .log_err();
 488                        }
 489                    }
 490                    return Ok(Some(open_target));
 491                }
 492            } else if open_target.is_dir() {
 493                workspace.update(cx, |workspace, cx| {
 494                    workspace.project().update(cx, |_, cx| {
 495                        cx.emit(project::Event::ActivateProjectPanel);
 496                    })
 497                })?;
 498                return Ok(Some(open_target));
 499            }
 500        }
 501        Ok(None)
 502    })
 503}
 504
 505#[cfg(test)]
 506mod tests {
 507    use super::*;
 508    use gpui::TestAppContext;
 509    use project::Project;
 510    use serde_json::json;
 511    use std::path::{Path, PathBuf};
 512    use terminal::{HoveredWord, alacritty_terminal::index::Point as AlacPoint};
 513    use util::path;
 514    use workspace::AppState;
 515
 516    async fn init_test(
 517        app_cx: &mut TestAppContext,
 518        trees: impl IntoIterator<Item = (&str, serde_json::Value)>,
 519        worktree_roots: impl IntoIterator<Item = &str>,
 520    ) -> impl AsyncFnMut(
 521        HoveredWord,
 522        PathLikeTarget,
 523        BackgroundFsChecks,
 524    ) -> (Option<HoverTarget>, Option<OpenTarget>) {
 525        let fs = app_cx.update(AppState::test).fs.as_fake().clone();
 526
 527        app_cx.update(|cx| {
 528            terminal::init(cx);
 529            theme::init(theme::LoadThemes::JustBase, cx);
 530            Project::init_settings(cx);
 531            language::init(cx);
 532            editor::init(cx);
 533        });
 534
 535        for (path, tree) in trees {
 536            fs.insert_tree(path, tree).await;
 537        }
 538
 539        let project = Project::test(
 540            fs.clone(),
 541            worktree_roots.into_iter().map(Path::new),
 542            app_cx,
 543        )
 544        .await;
 545
 546        let (workspace, cx) =
 547            app_cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
 548
 549        let cwd = std::env::current_dir().expect("Failed to get working directory");
 550        let terminal = project
 551            .update(cx, |project: &mut Project, cx| {
 552                project.create_terminal_shell(Some(cwd), cx)
 553            })
 554            .await
 555            .expect("Failed to create a terminal");
 556
 557        let workspace_a = workspace.clone();
 558        let (terminal_view, cx) = app_cx.add_window_view(|window, cx| {
 559            TerminalView::new(
 560                terminal,
 561                workspace_a.downgrade(),
 562                None,
 563                project.downgrade(),
 564                window,
 565                cx,
 566            )
 567        });
 568
 569        async move |hovered_word: HoveredWord,
 570                    path_like_target: PathLikeTarget,
 571                    background_fs_checks: BackgroundFsChecks|
 572                    -> (Option<HoverTarget>, Option<OpenTarget>) {
 573            let workspace_a = workspace.clone();
 574            terminal_view
 575                .update(cx, |_, cx| {
 576                    possible_hover_target(
 577                        &workspace_a.downgrade(),
 578                        hovered_word,
 579                        &path_like_target,
 580                        cx,
 581                        background_fs_checks,
 582                    )
 583                })
 584                .await;
 585
 586            let hover_target =
 587                terminal_view.read_with(cx, |terminal_view, _| terminal_view.hover.clone());
 588
 589            let open_target = terminal_view
 590                .update_in(cx, |terminal_view, window, cx| {
 591                    possibly_open_target(
 592                        &workspace.downgrade(),
 593                        terminal_view,
 594                        &path_like_target,
 595                        window,
 596                        cx,
 597                        background_fs_checks,
 598                    )
 599                })
 600                .await
 601                .expect("Failed to possibly open target");
 602
 603            (hover_target, open_target)
 604        }
 605    }
 606
 607    async fn test_path_like_simple(
 608        test_path_like: &mut impl AsyncFnMut(
 609            HoveredWord,
 610            PathLikeTarget,
 611            BackgroundFsChecks,
 612        ) -> (Option<HoverTarget>, Option<OpenTarget>),
 613        maybe_path: &str,
 614        tooltip: &str,
 615        terminal_dir: Option<PathBuf>,
 616        background_fs_checks: BackgroundFsChecks,
 617        mut open_target_found_by: OpenTargetFoundBy,
 618        file: &str,
 619        line: u32,
 620    ) {
 621        let (hover_target, open_target) = test_path_like(
 622            HoveredWord {
 623                word: maybe_path.to_string(),
 624                word_match: AlacPoint::default()..=AlacPoint::default(),
 625                id: 0,
 626            },
 627            PathLikeTarget {
 628                maybe_path: maybe_path.to_string(),
 629                terminal_dir,
 630            },
 631            background_fs_checks,
 632        )
 633        .await;
 634
 635        let Some(hover_target) = hover_target else {
 636            assert!(
 637                hover_target.is_some(),
 638                "Hover target should not be `None` at {file}:{line}:"
 639            );
 640            return;
 641        };
 642
 643        assert_eq!(
 644            hover_target.tooltip, tooltip,
 645            "Tooltip mismatch at {file}:{line}:"
 646        );
 647        assert_eq!(
 648            hover_target.hovered_word.word, maybe_path,
 649            "Hovered word mismatch at {file}:{line}:"
 650        );
 651
 652        let Some(open_target) = open_target else {
 653            assert!(
 654                open_target.is_some(),
 655                "Open target should not be `None` at {file}:{line}:"
 656            );
 657            return;
 658        };
 659
 660        assert_eq!(
 661            open_target.path().path,
 662            Path::new(tooltip),
 663            "Open target path mismatch at {file}:{line}:"
 664        );
 665
 666        if background_fs_checks == BackgroundFsChecks::Disabled
 667            && open_target_found_by == OpenTargetFoundBy::FileSystemBackground
 668        {
 669            open_target_found_by = OpenTargetFoundBy::WorktreeScan;
 670        }
 671
 672        assert_eq!(
 673            open_target.found_by(),
 674            open_target_found_by,
 675            "Open target found by mismatch at {file}:{line}:"
 676        );
 677    }
 678
 679    macro_rules! none_or_some_pathbuf {
 680        (None) => {
 681            None
 682        };
 683        ($cwd:literal) => {
 684            Some($crate::PathBuf::from(path!($cwd)))
 685        };
 686    }
 687
 688    macro_rules! test_path_like {
 689        (
 690            $test_path_like:expr,
 691            $maybe_path:literal,
 692            $tooltip:literal,
 693            $cwd:tt,
 694            $found_by:expr
 695        ) => {{
 696            test_path_like!(
 697                $test_path_like,
 698                $maybe_path,
 699                $tooltip,
 700                $cwd,
 701                BackgroundFsChecks::Enabled,
 702                $found_by
 703            );
 704            test_path_like!(
 705                $test_path_like,
 706                $maybe_path,
 707                $tooltip,
 708                $cwd,
 709                BackgroundFsChecks::Disabled,
 710                $found_by
 711            );
 712        }};
 713
 714        (
 715            $test_path_like:expr,
 716            $maybe_path:literal,
 717            $tooltip:literal,
 718            $cwd:tt,
 719            $background_fs_checks:path,
 720            $found_by:expr
 721        ) => {
 722            test_path_like_simple(
 723                &mut $test_path_like,
 724                path!($maybe_path),
 725                path!($tooltip),
 726                none_or_some_pathbuf!($cwd),
 727                $background_fs_checks,
 728                $found_by,
 729                std::file!(),
 730                std::line!(),
 731            )
 732            .await
 733        };
 734    }
 735
 736    // Note the arms of `test`, `test_local`, and `test_remote` should be collapsed once macro
 737    // metavariable expressions (#![feature(macro_metavar_expr)]) are stabilized.
 738    // See https://github.com/rust-lang/rust/issues/83527
 739    #[doc = "test_path_likes!(<cx>, <trees>, <worktrees>, { $(<tests>;)+ })"]
 740    macro_rules! test_path_likes {
 741        ($cx:expr, $trees:expr, $worktrees:expr, { $($tests:expr;)+ }) => { {
 742            let mut test_path_like = init_test($cx, $trees, $worktrees).await;
 743            #[doc ="test!(<hovered maybe_path>, <expected tooltip>, <terminal cwd> "]
 744            #[doc ="\\[, found by \\])"]
 745            #[allow(unused_macros)]
 746            macro_rules! test {
 747                ($maybe_path:literal, $tooltip:literal, $cwd:tt) => {
 748                    test_path_like!(
 749                        test_path_like,
 750                        $maybe_path,
 751                        $tooltip,
 752                        $cwd,
 753                        OpenTargetFoundBy::WorktreeExact
 754                    )
 755                };
 756                ($maybe_path:literal, $tooltip:literal, $cwd:tt, $found_by:ident) => {
 757                    test_path_like!(
 758                        test_path_like,
 759                        $maybe_path,
 760                        $tooltip,
 761                        $cwd,
 762                        OpenTargetFoundBy::$found_by
 763                    )
 764                }
 765            }
 766            #[doc ="test_local!(<hovered maybe_path>, <expected tooltip>, <terminal cwd> "]
 767            #[doc ="\\[, found by \\])"]
 768            #[allow(unused_macros)]
 769            macro_rules! test_local {
 770                ($maybe_path:literal, $tooltip:literal, $cwd:tt) => {
 771                    test_path_like!(
 772                        test_path_like,
 773                        $maybe_path,
 774                        $tooltip,
 775                        $cwd,
 776                        BackgroundFsChecks::Enabled,
 777                        OpenTargetFoundBy::WorktreeExact
 778                    )
 779                };
 780                ($maybe_path:literal, $tooltip:literal, $cwd:tt, $found_by:ident) => {
 781                    test_path_like!(
 782                        test_path_like,
 783                        $maybe_path,
 784                        $tooltip,
 785                        $cwd,
 786                        BackgroundFsChecks::Enabled,
 787                        OpenTargetFoundBy::$found_by
 788                    )
 789                }
 790            }
 791            #[doc ="test_remote!(<hovered maybe_path>, <expected tooltip>, <terminal cwd> "]
 792            #[doc ="\\[, found by \\])"]
 793            #[allow(unused_macros)]
 794            macro_rules! test_remote {
 795                ($maybe_path:literal, $tooltip:literal, $cwd:tt) => {
 796                    test_path_like!(
 797                        test_path_like,
 798                        $maybe_path,
 799                        $tooltip,
 800                        $cwd,
 801                        BackgroundFsChecks::Disabled,
 802                        OpenTargetFoundBy::WorktreeExact
 803                    )
 804                };
 805                ($maybe_path:literal, $tooltip:literal, $cwd:tt, $found_by:ident) => {
 806                    test_path_like!(
 807                        test_path_like,
 808                        $maybe_path,
 809                        $tooltip,
 810                        $cwd,
 811                        BackgroundFsChecks::Disabled,
 812                        OpenTargetFoundBy::$found_by
 813                    )
 814                }
 815            }
 816            $($tests);+
 817        } }
 818    }
 819
 820    #[gpui::test]
 821    async fn one_folder_worktree(cx: &mut TestAppContext) {
 822        test_path_likes!(
 823            cx,
 824            vec![(
 825                path!("/test"),
 826                json!({
 827                    "lib.rs": "",
 828                    "test.rs": "",
 829                }),
 830            )],
 831            vec![path!("/test")],
 832            {
 833                test!("lib.rs", "/test/lib.rs", None);
 834                test!("/test/lib.rs", "/test/lib.rs", None);
 835                test!("test.rs", "/test/test.rs", None);
 836                test!("/test/test.rs", "/test/test.rs", None);
 837            }
 838        )
 839    }
 840
 841    #[gpui::test]
 842    async fn mixed_worktrees(cx: &mut TestAppContext) {
 843        test_path_likes!(
 844            cx,
 845            vec![
 846                (
 847                    path!("/"),
 848                    json!({
 849                        "file.txt": "",
 850                    }),
 851                ),
 852                (
 853                    path!("/test"),
 854                    json!({
 855                        "lib.rs": "",
 856                        "test.rs": "",
 857                        "file.txt": "",
 858                    }),
 859                ),
 860            ],
 861            vec![path!("/file.txt"), path!("/test")],
 862            {
 863                test!("file.txt", "/file.txt", "/");
 864                test!("/file.txt", "/file.txt", "/");
 865
 866                test!("lib.rs", "/test/lib.rs", "/test");
 867                test!("test.rs", "/test/test.rs", "/test");
 868                test!("file.txt", "/test/file.txt", "/test");
 869
 870                test!("/test/lib.rs", "/test/lib.rs", "/test");
 871                test!("/test/test.rs", "/test/test.rs", "/test");
 872                test!("/test/file.txt", "/test/file.txt", "/test");
 873            }
 874        )
 875    }
 876
 877    #[gpui::test]
 878    async fn worktree_file_preferred(cx: &mut TestAppContext) {
 879        test_path_likes!(
 880            cx,
 881            vec![
 882                (
 883                    path!("/"),
 884                    json!({
 885                        "file.txt": "",
 886                    }),
 887                ),
 888                (
 889                    path!("/test"),
 890                    json!({
 891                        "file.txt": "",
 892                    }),
 893                ),
 894            ],
 895            vec![path!("/test")],
 896            {
 897                test!("file.txt", "/test/file.txt", "/test");
 898            }
 899        )
 900    }
 901
 902    mod issues {
 903        use super::*;
 904
 905        // https://github.com/zed-industries/zed/issues/28407
 906        #[gpui::test]
 907        async fn issue_28407_siblings(cx: &mut TestAppContext) {
 908            test_path_likes!(
 909                cx,
 910                vec![(
 911                    path!("/dir1"),
 912                    json!({
 913                        "dir 2": {
 914                            "C.py": ""
 915                        },
 916                        "dir 3": {
 917                            "C.py": ""
 918                        },
 919                    }),
 920                )],
 921                vec![path!("/dir1")],
 922                {
 923                    test!("C.py", "/dir1/dir 2/C.py", "/dir1", WorktreeScan);
 924                    test!("C.py", "/dir1/dir 2/C.py", "/dir1/dir 2");
 925                    test!("C.py", "/dir1/dir 3/C.py", "/dir1/dir 3");
 926                }
 927            )
 928        }
 929
 930        // https://github.com/zed-industries/zed/issues/28407
 931        // See https://github.com/zed-industries/zed/issues/34027
 932        // See https://github.com/zed-industries/zed/issues/33498
 933        #[gpui::test]
 934        async fn issue_28407_nesting(cx: &mut TestAppContext) {
 935            test_path_likes!(
 936                cx,
 937                vec![(
 938                    path!("/project"),
 939                    json!({
 940                        "lib": {
 941                            "src": {
 942                                "main.rs": "",
 943                                "only_in_lib.rs": ""
 944                            },
 945                        },
 946                        "src": {
 947                            "main.rs": ""
 948                        },
 949                    }),
 950                )],
 951                vec![path!("/project")],
 952                {
 953                    test!("main.rs", "/project/src/main.rs", "/project/src");
 954                    test!("main.rs", "/project/lib/src/main.rs", "/project/lib/src");
 955
 956                    test!("src/main.rs", "/project/src/main.rs", "/project");
 957                    test!("src/main.rs", "/project/src/main.rs", "/project/src");
 958                    test!("src/main.rs", "/project/lib/src/main.rs", "/project/lib");
 959
 960                    test!("lib/src/main.rs", "/project/lib/src/main.rs", "/project");
 961                    test!(
 962                        "lib/src/main.rs",
 963                        "/project/lib/src/main.rs",
 964                        "/project/src"
 965                    );
 966                    test!(
 967                        "lib/src/main.rs",
 968                        "/project/lib/src/main.rs",
 969                        "/project/lib"
 970                    );
 971                    test!(
 972                        "lib/src/main.rs",
 973                        "/project/lib/src/main.rs",
 974                        "/project/lib/src"
 975                    );
 976                    test!(
 977                        "src/only_in_lib.rs",
 978                        "/project/lib/src/only_in_lib.rs",
 979                        "/project/lib/src",
 980                        WorktreeScan
 981                    );
 982                }
 983            )
 984        }
 985
 986        // https://github.com/zed-industries/zed/issues/28339
 987        // Note: These could all be found by WorktreeExact if we used
 988        // `fs::normalize_path(&maybe_path)`
 989        #[gpui::test]
 990        async fn issue_28339(cx: &mut TestAppContext) {
 991            test_path_likes!(
 992                cx,
 993                vec![(
 994                    path!("/tmp"),
 995                    json!({
 996                        "issue28339": {
 997                            "foo": {
 998                                "bar.txt": ""
 999                            },
1000                        },
1001                    }),
1002                )],
1003                vec![path!("/tmp")],
1004                {
1005                    test_local!(
1006                        "foo/./bar.txt",
1007                        "/tmp/issue28339/foo/bar.txt",
1008                        "/tmp/issue28339"
1009                    );
1010                    test_local!(
1011                        "foo/../foo/bar.txt",
1012                        "/tmp/issue28339/foo/bar.txt",
1013                        "/tmp/issue28339",
1014                        FileSystemBackground
1015                    );
1016                    test_local!(
1017                        "foo/..///foo/bar.txt",
1018                        "/tmp/issue28339/foo/bar.txt",
1019                        "/tmp/issue28339",
1020                        FileSystemBackground
1021                    );
1022                    test_local!(
1023                        "issue28339/../issue28339/foo/../foo/bar.txt",
1024                        "/tmp/issue28339/foo/bar.txt",
1025                        "/tmp/issue28339",
1026                        FileSystemBackground
1027                    );
1028                    test_local!(
1029                        "./bar.txt",
1030                        "/tmp/issue28339/foo/bar.txt",
1031                        "/tmp/issue28339/foo"
1032                    );
1033                    test_local!(
1034                        "../foo/bar.txt",
1035                        "/tmp/issue28339/foo/bar.txt",
1036                        "/tmp/issue28339/foo",
1037                        FileSystemBackground
1038                    );
1039                }
1040            )
1041        }
1042
1043        // https://github.com/zed-industries/zed/issues/28339
1044        // Note: These could all be found by WorktreeExact if we used
1045        // `fs::normalize_path(&maybe_path)`
1046        #[gpui::test]
1047        #[should_panic(expected = "Hover target should not be `None`")]
1048        async fn issue_28339_remote(cx: &mut TestAppContext) {
1049            test_path_likes!(
1050                cx,
1051                vec![(
1052                    path!("/tmp"),
1053                    json!({
1054                        "issue28339": {
1055                            "foo": {
1056                                "bar.txt": ""
1057                            },
1058                        },
1059                    }),
1060                )],
1061                vec![path!("/tmp")],
1062                {
1063                    test_remote!(
1064                        "foo/./bar.txt",
1065                        "/tmp/issue28339/foo/bar.txt",
1066                        "/tmp/issue28339"
1067                    );
1068                    test_remote!(
1069                        "foo/../foo/bar.txt",
1070                        "/tmp/issue28339/foo/bar.txt",
1071                        "/tmp/issue28339"
1072                    );
1073                    test_remote!(
1074                        "foo/..///foo/bar.txt",
1075                        "/tmp/issue28339/foo/bar.txt",
1076                        "/tmp/issue28339"
1077                    );
1078                    test_remote!(
1079                        "issue28339/../issue28339/foo/../foo/bar.txt",
1080                        "/tmp/issue28339/foo/bar.txt",
1081                        "/tmp/issue28339"
1082                    );
1083                    test_remote!(
1084                        "./bar.txt",
1085                        "/tmp/issue28339/foo/bar.txt",
1086                        "/tmp/issue28339/foo"
1087                    );
1088                    test_remote!(
1089                        "../foo/bar.txt",
1090                        "/tmp/issue28339/foo/bar.txt",
1091                        "/tmp/issue28339/foo"
1092                    );
1093                }
1094            )
1095        }
1096
1097        // https://github.com/zed-industries/zed/issues/34027
1098        #[gpui::test]
1099        async fn issue_34027(cx: &mut TestAppContext) {
1100            test_path_likes!(
1101                cx,
1102                vec![(
1103                    path!("/tmp/issue34027"),
1104                    json!({
1105                        "test.txt": "",
1106                        "foo": {
1107                            "test.txt": "",
1108                        }
1109                    }),
1110                ),],
1111                vec![path!("/tmp/issue34027")],
1112                {
1113                    test!("test.txt", "/tmp/issue34027/test.txt", "/tmp/issue34027");
1114                    test!(
1115                        "test.txt",
1116                        "/tmp/issue34027/foo/test.txt",
1117                        "/tmp/issue34027/foo"
1118                    );
1119                }
1120            )
1121        }
1122
1123        // https://github.com/zed-industries/zed/issues/34027
1124        #[gpui::test]
1125        async fn issue_34027_siblings(cx: &mut TestAppContext) {
1126            test_path_likes!(
1127                cx,
1128                vec![(
1129                    path!("/test"),
1130                    json!({
1131                        "sub1": {
1132                            "file.txt": "",
1133                        },
1134                        "sub2": {
1135                            "file.txt": "",
1136                        }
1137                    }),
1138                ),],
1139                vec![path!("/test")],
1140                {
1141                    test!("file.txt", "/test/sub1/file.txt", "/test/sub1");
1142                    test!("file.txt", "/test/sub2/file.txt", "/test/sub2");
1143                    test!("sub1/file.txt", "/test/sub1/file.txt", "/test/sub1");
1144                    test!("sub2/file.txt", "/test/sub2/file.txt", "/test/sub2");
1145                    test!("sub1/file.txt", "/test/sub1/file.txt", "/test/sub2");
1146                    test!("sub2/file.txt", "/test/sub2/file.txt", "/test/sub1");
1147                }
1148            )
1149        }
1150
1151        // https://github.com/zed-industries/zed/issues/34027
1152        #[gpui::test]
1153        async fn issue_34027_nesting(cx: &mut TestAppContext) {
1154            test_path_likes!(
1155                cx,
1156                vec![(
1157                    path!("/test"),
1158                    json!({
1159                        "sub1": {
1160                            "file.txt": "",
1161                            "subsub1": {
1162                                "file.txt": "",
1163                            }
1164                        },
1165                        "sub2": {
1166                            "file.txt": "",
1167                            "subsub1": {
1168                                "file.txt": "",
1169                            }
1170                        }
1171                    }),
1172                ),],
1173                vec![path!("/test")],
1174                {
1175                    test!(
1176                        "file.txt",
1177                        "/test/sub1/subsub1/file.txt",
1178                        "/test/sub1/subsub1"
1179                    );
1180                    test!(
1181                        "file.txt",
1182                        "/test/sub2/subsub1/file.txt",
1183                        "/test/sub2/subsub1"
1184                    );
1185                    test!(
1186                        "subsub1/file.txt",
1187                        "/test/sub1/subsub1/file.txt",
1188                        "/test",
1189                        WorktreeScan
1190                    );
1191                    test!(
1192                        "subsub1/file.txt",
1193                        "/test/sub1/subsub1/file.txt",
1194                        "/test",
1195                        WorktreeScan
1196                    );
1197                    test!(
1198                        "subsub1/file.txt",
1199                        "/test/sub1/subsub1/file.txt",
1200                        "/test/sub1"
1201                    );
1202                    test!(
1203                        "subsub1/file.txt",
1204                        "/test/sub2/subsub1/file.txt",
1205                        "/test/sub2"
1206                    );
1207                    test!(
1208                        "subsub1/file.txt",
1209                        "/test/sub1/subsub1/file.txt",
1210                        "/test/sub1/subsub1",
1211                        WorktreeScan
1212                    );
1213                }
1214            )
1215        }
1216
1217        // https://github.com/zed-industries/zed/issues/34027
1218        #[gpui::test]
1219        async fn issue_34027_non_worktree_local_file(cx: &mut TestAppContext) {
1220            test_path_likes!(
1221                cx,
1222                vec![
1223                    (
1224                        path!("/"),
1225                        json!({
1226                            "file.txt": "",
1227                        }),
1228                    ),
1229                    (
1230                        path!("/test"),
1231                        json!({
1232                            "file.txt": "",
1233                        }),
1234                    ),
1235                ],
1236                vec![path!("/test")],
1237                {
1238                    // Note: Opening a non-worktree file adds that file as a single file worktree.
1239                    test_local!("file.txt", "/file.txt", "/", FileSystemBackground);
1240                }
1241            )
1242        }
1243
1244        // https://github.com/zed-industries/zed/issues/34027
1245        #[gpui::test]
1246        async fn issue_34027_non_worktree_remote_file(cx: &mut TestAppContext) {
1247            test_path_likes!(
1248                cx,
1249                vec![
1250                    (
1251                        path!("/"),
1252                        json!({
1253                            "file.txt": "",
1254                        }),
1255                    ),
1256                    (
1257                        path!("/test"),
1258                        json!({
1259                            "file.txt": "",
1260                        }),
1261                    ),
1262                ],
1263                vec![path!("/test")],
1264                {
1265                    // Note: Opening a non-worktree file adds that file as a single file worktree.
1266                    test_remote!("file.txt", "/test/file.txt", "/");
1267                    test_remote!("/test/file.txt", "/test/file.txt", "/");
1268                }
1269            )
1270        }
1271
1272        // See https://github.com/zed-industries/zed/issues/34027
1273        #[gpui::test]
1274        #[should_panic(expected = "Tooltip mismatch")]
1275        async fn issue_34027_gaps(cx: &mut TestAppContext) {
1276            test_path_likes!(
1277                cx,
1278                vec![(
1279                    path!("/project"),
1280                    json!({
1281                        "lib": {
1282                            "src": {
1283                                "main.rs": ""
1284                            },
1285                        },
1286                        "src": {
1287                            "main.rs": ""
1288                        },
1289                    }),
1290                )],
1291                vec![path!("/project")],
1292                {
1293                    test!("main.rs", "/project/src/main.rs", "/project");
1294                    test!("main.rs", "/project/lib/src/main.rs", "/project/lib");
1295                }
1296            )
1297        }
1298
1299        // See https://github.com/zed-industries/zed/issues/34027
1300        #[gpui::test]
1301        #[should_panic(expected = "Tooltip mismatch")]
1302        async fn issue_34027_overlap(cx: &mut TestAppContext) {
1303            test_path_likes!(
1304                cx,
1305                vec![(
1306                    path!("/project"),
1307                    json!({
1308                        "lib": {
1309                            "src": {
1310                                "main.rs": ""
1311                            },
1312                        },
1313                        "src": {
1314                            "main.rs": ""
1315                        },
1316                    }),
1317                )],
1318                vec![path!("/project")],
1319                {
1320                    // Finds "/project/src/main.rs"
1321                    test!(
1322                        "src/main.rs",
1323                        "/project/lib/src/main.rs",
1324                        "/project/lib/src"
1325                    );
1326                }
1327            )
1328        }
1329    }
1330}