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