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