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::{ResultExt, debug_panic, paths::PathWithPosition};
 10use workspace::{OpenOptions, OpenVisible, Workspace};
 11
 12#[derive(Debug, Clone)]
 13enum OpenTarget {
 14    Worktree(PathWithPosition, Entry),
 15    File(PathWithPosition, Metadata),
 16}
 17
 18impl OpenTarget {
 19    fn is_file(&self) -> bool {
 20        match self {
 21            OpenTarget::Worktree(_, entry) => entry.is_file(),
 22            OpenTarget::File(_, metadata) => !metadata.is_dir,
 23        }
 24    }
 25
 26    fn is_dir(&self) -> bool {
 27        match self {
 28            OpenTarget::Worktree(_, entry) => entry.is_dir(),
 29            OpenTarget::File(_, metadata) => metadata.is_dir,
 30        }
 31    }
 32
 33    fn path(&self) -> &PathWithPosition {
 34        match self {
 35            OpenTarget::Worktree(path, _) => path,
 36            OpenTarget::File(path, _) => path,
 37        }
 38    }
 39}
 40
 41pub(super) fn hover_path_like_target(
 42    workspace: &WeakEntity<Workspace>,
 43    hovered_word: HoveredWord,
 44    path_like_target: &PathLikeTarget,
 45    cx: &mut Context<TerminalView>,
 46) -> Task<()> {
 47    let file_to_open_task = possible_open_target(workspace, path_like_target, cx);
 48    cx.spawn(async move |terminal_view, cx| {
 49        let file_to_open = file_to_open_task.await;
 50        terminal_view
 51            .update(cx, |terminal_view, _| match file_to_open {
 52                Some(OpenTarget::File(path, _) | OpenTarget::Worktree(path, _)) => {
 53                    terminal_view.hover = Some(HoverTarget {
 54                        tooltip: path.to_string(|path| path.to_string_lossy().to_string()),
 55                        hovered_word,
 56                    });
 57                }
 58                None => {
 59                    terminal_view.hover = None;
 60                }
 61            })
 62            .ok();
 63    })
 64}
 65
 66fn possible_open_target(
 67    workspace: &WeakEntity<Workspace>,
 68    path_like_target: &PathLikeTarget,
 69    cx: &App,
 70) -> Task<Option<OpenTarget>> {
 71    let Some(workspace) = workspace.upgrade() else {
 72        return Task::ready(None);
 73    };
 74    // We have to check for both paths, as on Unix, certain paths with positions are valid file paths too.
 75    // We can be on FS remote part, without real FS, so cannot canonicalize or check for existence the path right away.
 76    let mut potential_paths = Vec::new();
 77    let cwd = path_like_target.terminal_dir.as_ref();
 78    let maybe_path = &path_like_target.maybe_path;
 79    let original_path = PathWithPosition::from_path(PathBuf::from(maybe_path));
 80    let path_with_position = PathWithPosition::parse_str(maybe_path);
 81    let worktree_candidates = workspace
 82        .read(cx)
 83        .worktrees(cx)
 84        .sorted_by_key(|worktree| {
 85            let worktree_root = worktree.read(cx).abs_path();
 86            match cwd.and_then(|cwd| worktree_root.strip_prefix(cwd).ok()) {
 87                Some(cwd_child) => cwd_child.components().count(),
 88                None => usize::MAX,
 89            }
 90        })
 91        .collect::<Vec<_>>();
 92    // Since we do not check paths via FS and joining, we need to strip off potential `./`, `a/`, `b/` prefixes out of it.
 93    const GIT_DIFF_PATH_PREFIXES: &[&str] = &["a", "b"];
 94    for prefix_str in GIT_DIFF_PATH_PREFIXES.iter().chain(std::iter::once(&".")) {
 95        if let Some(stripped) = original_path.path.strip_prefix(prefix_str).ok() {
 96            potential_paths.push(PathWithPosition {
 97                path: stripped.to_owned(),
 98                row: original_path.row,
 99                column: original_path.column,
100            });
101        }
102        if let Some(stripped) = path_with_position.path.strip_prefix(prefix_str).ok() {
103            potential_paths.push(PathWithPosition {
104                path: stripped.to_owned(),
105                row: path_with_position.row,
106                column: path_with_position.column,
107            });
108        }
109    }
110
111    let insert_both_paths = original_path != path_with_position;
112    potential_paths.insert(0, original_path);
113    if insert_both_paths {
114        potential_paths.insert(1, path_with_position);
115    }
116
117    // If we won't find paths "easily", we can traverse the entire worktree to look what ends with the potential path suffix.
118    // That will be slow, though, so do the fast checks first.
119    let mut worktree_paths_to_check = Vec::new();
120    for worktree in &worktree_candidates {
121        let worktree_root = worktree.read(cx).abs_path();
122        let mut paths_to_check = Vec::with_capacity(potential_paths.len());
123
124        for path_with_position in &potential_paths {
125            let path_to_check = if worktree_root.ends_with(&path_with_position.path) {
126                let root_path_with_position = PathWithPosition {
127                    path: worktree_root.to_path_buf(),
128                    row: path_with_position.row,
129                    column: path_with_position.column,
130                };
131                match worktree.read(cx).root_entry() {
132                    Some(root_entry) => {
133                        return Task::ready(Some(OpenTarget::Worktree(
134                            root_path_with_position,
135                            root_entry.clone(),
136                        )));
137                    }
138                    None => root_path_with_position,
139                }
140            } else {
141                PathWithPosition {
142                    path: path_with_position
143                        .path
144                        .strip_prefix(&worktree_root)
145                        .unwrap_or(&path_with_position.path)
146                        .to_owned(),
147                    row: path_with_position.row,
148                    column: path_with_position.column,
149                }
150            };
151
152            if path_to_check.path.is_relative()
153                && let Some(entry) = worktree.read(cx).entry_for_path(&path_to_check.path)
154            {
155                return Task::ready(Some(OpenTarget::Worktree(
156                    PathWithPosition {
157                        path: worktree_root.join(&entry.path),
158                        row: path_to_check.row,
159                        column: path_to_check.column,
160                    },
161                    entry.clone(),
162                )));
163            }
164
165            paths_to_check.push(path_to_check);
166        }
167
168        if !paths_to_check.is_empty() {
169            worktree_paths_to_check.push((worktree.clone(), paths_to_check));
170        }
171    }
172
173    // Before entire worktree traversal(s), make an attempt to do FS checks if available.
174    let fs_paths_to_check = if workspace.read(cx).project().read(cx).is_local() {
175        potential_paths
176            .into_iter()
177            .flat_map(|path_to_check| {
178                let mut paths_to_check = Vec::new();
179                let maybe_path = &path_to_check.path;
180                if maybe_path.starts_with("~") {
181                    if let Some(home_path) =
182                        maybe_path
183                            .strip_prefix("~")
184                            .ok()
185                            .and_then(|stripped_maybe_path| {
186                                Some(dirs::home_dir()?.join(stripped_maybe_path))
187                            })
188                    {
189                        paths_to_check.push(PathWithPosition {
190                            path: home_path,
191                            row: path_to_check.row,
192                            column: path_to_check.column,
193                        });
194                    }
195                } else {
196                    paths_to_check.push(PathWithPosition {
197                        path: maybe_path.clone(),
198                        row: path_to_check.row,
199                        column: path_to_check.column,
200                    });
201                    if maybe_path.is_relative() {
202                        if let Some(cwd) = &cwd {
203                            paths_to_check.push(PathWithPosition {
204                                path: cwd.join(maybe_path),
205                                row: path_to_check.row,
206                                column: path_to_check.column,
207                            });
208                        }
209                        for worktree in &worktree_candidates {
210                            paths_to_check.push(PathWithPosition {
211                                path: worktree.read(cx).abs_path().join(maybe_path),
212                                row: path_to_check.row,
213                                column: path_to_check.column,
214                            });
215                        }
216                    }
217                }
218                paths_to_check
219            })
220            .collect()
221    } else {
222        Vec::new()
223    };
224
225    let worktree_check_task = cx.spawn(async move |cx| {
226        for (worktree, worktree_paths_to_check) in worktree_paths_to_check {
227            let found_entry = worktree
228                .update(cx, |worktree, _| {
229                    let worktree_root = worktree.abs_path();
230                    let traversal = worktree.traverse_from_path(true, true, false, "".as_ref());
231                    for entry in traversal {
232                        if let Some(path_in_worktree) = worktree_paths_to_check
233                            .iter()
234                            .find(|path_to_check| entry.path.ends_with(&path_to_check.path))
235                        {
236                            return Some(OpenTarget::Worktree(
237                                PathWithPosition {
238                                    path: worktree_root.join(&entry.path),
239                                    row: path_in_worktree.row,
240                                    column: path_in_worktree.column,
241                                },
242                                entry.clone(),
243                            ));
244                        }
245                    }
246                    None
247                })
248                .ok()?;
249            if let Some(found_entry) = found_entry {
250                return Some(found_entry);
251            }
252        }
253        None
254    });
255
256    let fs = workspace.read(cx).project().read(cx).fs().clone();
257    cx.background_spawn(async move {
258        for mut path_to_check in fs_paths_to_check {
259            if let Some(fs_path_to_check) = fs.canonicalize(&path_to_check.path).await.ok()
260                && let Some(metadata) = fs.metadata(&fs_path_to_check).await.ok().flatten()
261            {
262                path_to_check.path = fs_path_to_check;
263                return Some(OpenTarget::File(path_to_check, metadata));
264            }
265        }
266
267        worktree_check_task.await
268    })
269}
270
271pub(super) fn open_path_like_target(
272    workspace: &WeakEntity<Workspace>,
273    terminal_view: &mut TerminalView,
274    path_like_target: &PathLikeTarget,
275    window: &mut Window,
276    cx: &mut Context<TerminalView>,
277) {
278    possibly_open_target(workspace, terminal_view, path_like_target, window, cx)
279        .detach_and_log_err(cx)
280}
281
282fn possibly_open_target(
283    workspace: &WeakEntity<Workspace>,
284    terminal_view: &mut TerminalView,
285    path_like_target: &PathLikeTarget,
286    window: &mut Window,
287    cx: &mut Context<TerminalView>,
288) -> Task<Result<Option<OpenTarget>>> {
289    if terminal_view.hover.is_none() {
290        return Task::ready(Ok(None));
291    }
292    let workspace = workspace.clone();
293    let path_like_target = path_like_target.clone();
294    cx.spawn_in(window, async move |terminal_view, cx| {
295        let Some(open_target) = terminal_view
296            .update(cx, |_, cx| {
297                possible_open_target(&workspace, &path_like_target, cx)
298            })?
299            .await
300        else {
301            return Ok(None);
302        };
303
304        let path_to_open = open_target.path();
305        let opened_items = workspace
306            .update_in(cx, |workspace, window, cx| {
307                workspace.open_paths(
308                    vec![path_to_open.path.clone()],
309                    OpenOptions {
310                        visible: Some(OpenVisible::OnlyDirectories),
311                        ..Default::default()
312                    },
313                    None,
314                    window,
315                    cx,
316                )
317            })
318            .context("workspace update")?
319            .await;
320        if opened_items.len() != 1 {
321            debug_panic!(
322                "Received {} items for one path {path_to_open:?}",
323                opened_items.len(),
324            );
325        }
326
327        if let Some(opened_item) = opened_items.first() {
328            if open_target.is_file() {
329                if let Some(Ok(opened_item)) = opened_item {
330                    if let Some(row) = path_to_open.row {
331                        let col = path_to_open.column.unwrap_or(0);
332                        if let Some(active_editor) = opened_item.downcast::<Editor>() {
333                            active_editor
334                                .downgrade()
335                                .update_in(cx, |editor, window, cx| {
336                                    editor.go_to_singleton_buffer_point(
337                                        language::Point::new(
338                                            row.saturating_sub(1),
339                                            col.saturating_sub(1),
340                                        ),
341                                        window,
342                                        cx,
343                                    )
344                                })
345                                .log_err();
346                        }
347                    }
348                    return Ok(Some(open_target));
349                }
350            } else if open_target.is_dir() {
351                workspace.update(cx, |workspace, cx| {
352                    workspace.project().update(cx, |_, cx| {
353                        cx.emit(project::Event::ActivateProjectPanel);
354                    })
355                })?;
356                return Ok(Some(open_target));
357            }
358        }
359        Ok(None)
360    })
361}
362
363#[cfg(test)]
364mod tests {
365    use super::*;
366    use gpui::TestAppContext;
367    use project::Project;
368    use serde_json::json;
369    use std::path::{Path, PathBuf};
370    use terminal::{HoveredWord, alacritty_terminal::index::Point as AlacPoint};
371    use util::path;
372    use workspace::AppState;
373
374    async fn init_test(
375        app_cx: &mut TestAppContext,
376        trees: impl IntoIterator<Item = (&str, serde_json::Value)>,
377        worktree_roots: impl IntoIterator<Item = &str>,
378    ) -> impl AsyncFnMut(HoveredWord, PathLikeTarget) -> (Option<HoverTarget>, Option<OpenTarget>)
379    {
380        let fs = app_cx.update(AppState::test).fs.as_fake().clone();
381
382        app_cx.update(|cx| {
383            terminal::init(cx);
384            theme::init(theme::LoadThemes::JustBase, cx);
385            Project::init_settings(cx);
386            language::init(cx);
387            editor::init(cx);
388        });
389
390        for (path, tree) in trees {
391            fs.insert_tree(path, tree).await;
392        }
393
394        let project = Project::test(
395            fs.clone(),
396            worktree_roots
397                .into_iter()
398                .map(Path::new)
399                .collect::<Vec<_>>(),
400            app_cx,
401        )
402        .await;
403
404        let (workspace, cx) =
405            app_cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
406
407        let terminal = project
408            .update(cx, |project: &mut Project, cx| {
409                project.create_terminal_shell(None, cx)
410            })
411            .await
412            .expect("Failed to create a terminal");
413
414        let workspace_a = workspace.clone();
415        let (terminal_view, cx) = app_cx.add_window_view(|window, cx| {
416            TerminalView::new(
417                terminal,
418                workspace_a.downgrade(),
419                None,
420                project.downgrade(),
421                window,
422                cx,
423            )
424        });
425
426        async move |hovered_word: HoveredWord,
427                    path_like_target: PathLikeTarget|
428                    -> (Option<HoverTarget>, Option<OpenTarget>) {
429            let workspace_a = workspace.clone();
430            terminal_view
431                .update(cx, |_, cx| {
432                    hover_path_like_target(
433                        &workspace_a.downgrade(),
434                        hovered_word,
435                        &path_like_target,
436                        cx,
437                    )
438                })
439                .await;
440
441            let hover_target =
442                terminal_view.read_with(cx, |terminal_view, _| terminal_view.hover.clone());
443
444            let open_target = terminal_view
445                .update_in(cx, |terminal_view, window, cx| {
446                    possibly_open_target(
447                        &workspace.downgrade(),
448                        terminal_view,
449                        &path_like_target,
450                        window,
451                        cx,
452                    )
453                })
454                .await
455                .expect("Failed to possibly open target");
456
457            (hover_target, open_target)
458        }
459    }
460
461    async fn test_path_like_simple(
462        test_path_like: &mut impl AsyncFnMut(
463            HoveredWord,
464            PathLikeTarget,
465        ) -> (Option<HoverTarget>, Option<OpenTarget>),
466        maybe_path: &str,
467        tooltip: &str,
468        terminal_dir: Option<PathBuf>,
469        file: &str,
470        line: u32,
471    ) {
472        let (hover_target, open_target) = test_path_like(
473            HoveredWord {
474                word: maybe_path.to_string(),
475                word_match: AlacPoint::default()..=AlacPoint::default(),
476                id: 0,
477            },
478            PathLikeTarget {
479                maybe_path: maybe_path.to_string(),
480                terminal_dir,
481            },
482        )
483        .await;
484
485        let Some(hover_target) = hover_target else {
486            assert!(
487                hover_target.is_some(),
488                "Hover target should not be `None` at {file}:{line}:"
489            );
490            return;
491        };
492
493        assert_eq!(
494            hover_target.tooltip, tooltip,
495            "Tooltip mismatch at {file}:{line}:"
496        );
497        assert_eq!(
498            hover_target.hovered_word.word, maybe_path,
499            "Hovered word mismatch at {file}:{line}:"
500        );
501
502        let Some(open_target) = open_target else {
503            assert!(
504                open_target.is_some(),
505                "Open target should not be `None` at {file}:{line}:"
506            );
507            return;
508        };
509
510        assert_eq!(
511            open_target.path().path,
512            Path::new(tooltip),
513            "Open target path mismatch at {file}:{line}:"
514        );
515    }
516
517    macro_rules! none_or_some {
518        () => {
519            None
520        };
521        ($some:expr) => {
522            Some($some)
523        };
524    }
525
526    macro_rules! test_path_like {
527        ($test_path_like:expr, $maybe_path:literal, $tooltip:literal $(, $cwd:literal)?) => {
528            test_path_like_simple(
529                &mut $test_path_like,
530                path!($maybe_path),
531                path!($tooltip),
532                none_or_some!($($crate::PathBuf::from(path!($cwd)))?),
533                std::file!(),
534                std::line!(),
535            )
536            .await
537        };
538    }
539
540    #[doc = "test_path_likes!(<cx>, <trees>, <worktrees>, { $(<tests>;)+ })"]
541    macro_rules! test_path_likes {
542        ($cx:expr, $trees:expr, $worktrees:expr, { $($tests:expr;)+ }) => { {
543            let mut test_path_like = init_test($cx, $trees, $worktrees).await;
544            #[doc ="test!(<hovered maybe_path>, <expected tooltip>, <terminal cwd>)"]
545            macro_rules! test {
546                ($maybe_path:literal, $tooltip:literal) => {
547                    test_path_like!(test_path_like, $maybe_path, $tooltip)
548                };
549                ($maybe_path:literal, $tooltip:literal, $cwd:literal) => {
550                    test_path_like!(test_path_like, $maybe_path, $tooltip, $cwd)
551                }
552            }
553            $($tests);+
554        } }
555    }
556
557    #[gpui::test]
558    async fn one_folder_worktree(cx: &mut TestAppContext) {
559        test_path_likes!(
560            cx,
561            vec![(
562                path!("/test"),
563                json!({
564                    "lib.rs": "",
565                    "test.rs": "",
566                }),
567            )],
568            vec![path!("/test")],
569            {
570                test!("lib.rs", "/test/lib.rs");
571                test!("test.rs", "/test/test.rs");
572            }
573        )
574    }
575
576    #[gpui::test]
577    async fn mixed_worktrees(cx: &mut TestAppContext) {
578        test_path_likes!(
579            cx,
580            vec![
581                (
582                    path!("/"),
583                    json!({
584                        "file.txt": "",
585                    }),
586                ),
587                (
588                    path!("/test"),
589                    json!({
590                        "lib.rs": "",
591                        "test.rs": "",
592                        "file.txt": "",
593                    }),
594                ),
595            ],
596            vec![path!("/file.txt"), path!("/test")],
597            {
598                test!("file.txt", "/file.txt", "/");
599                test!("lib.rs", "/test/lib.rs", "/test");
600                test!("test.rs", "/test/test.rs", "/test");
601                test!("file.txt", "/test/file.txt", "/test");
602            }
603        )
604    }
605
606    #[gpui::test]
607    async fn worktree_file_preferred(cx: &mut TestAppContext) {
608        test_path_likes!(
609            cx,
610            vec![
611                (
612                    path!("/"),
613                    json!({
614                        "file.txt": "",
615                    }),
616                ),
617                (
618                    path!("/test"),
619                    json!({
620                        "file.txt": "",
621                    }),
622                ),
623            ],
624            vec![path!("/test")],
625            {
626                test!("file.txt", "/test/file.txt", "/test");
627            }
628        )
629    }
630
631    mod issues {
632        use super::*;
633
634        // https://github.com/zed-industries/zed/issues/28407
635        #[gpui::test]
636        async fn issue_28407_siblings(cx: &mut TestAppContext) {
637            test_path_likes!(
638                cx,
639                vec![(
640                    path!("/dir1"),
641                    json!({
642                        "dir 2": {
643                            "C.py": ""
644                        },
645                        "dir 3": {
646                            "C.py": ""
647                        },
648                    }),
649                )],
650                vec![path!("/dir1")],
651                {
652                    test!("C.py", "/dir1/dir 2/C.py", "/dir1");
653                    test!("C.py", "/dir1/dir 2/C.py", "/dir1/dir 2");
654                    test!("C.py", "/dir1/dir 3/C.py", "/dir1/dir 3");
655                }
656            )
657        }
658
659        // https://github.com/zed-industries/zed/issues/28407
660        // See https://github.com/zed-industries/zed/issues/34027
661        // See https://github.com/zed-industries/zed/issues/33498
662        #[gpui::test]
663        #[should_panic(expected = "Tooltip mismatch")]
664        async fn issue_28407_nesting(cx: &mut TestAppContext) {
665            test_path_likes!(
666                cx,
667                vec![(
668                    path!("/project"),
669                    json!({
670                        "lib": {
671                            "src": {
672                                "main.rs": ""
673                            },
674                        },
675                        "src": {
676                            "main.rs": ""
677                        },
678                    }),
679                )],
680                vec![path!("/project")],
681                {
682                    // Failing currently
683                    test!("main.rs", "/project/src/main.rs", "/project");
684                    test!("main.rs", "/project/src/main.rs", "/project/src");
685                    test!("main.rs", "/project/lib/src/main.rs", "/project/lib");
686                    test!("main.rs", "/project/lib/src/main.rs", "/project/lib/src");
687
688                    test!("src/main.rs", "/project/src/main.rs", "/project");
689                    test!("src/main.rs", "/project/src/main.rs", "/project/src");
690                    // Failing currently
691                    test!("src/main.rs", "/project/lib/src/main.rs", "/project/lib");
692                    // Failing currently
693                    test!(
694                        "src/main.rs",
695                        "/project/lib/src/main.rs",
696                        "/project/lib/src"
697                    );
698
699                    test!("lib/src/main.rs", "/project/lib/src/main.rs", "/project");
700                    test!(
701                        "lib/src/main.rs",
702                        "/project/lib/src/main.rs",
703                        "/project/src"
704                    );
705                    test!(
706                        "lib/src/main.rs",
707                        "/project/lib/src/main.rs",
708                        "/project/lib"
709                    );
710                    test!(
711                        "lib/src/main.rs",
712                        "/project/lib/src/main.rs",
713                        "/project/lib/src"
714                    );
715                }
716            )
717        }
718
719        // https://github.com/zed-industries/zed/issues/28339
720        #[gpui::test]
721        async fn issue_28339(cx: &mut TestAppContext) {
722            test_path_likes!(
723                cx,
724                vec![(
725                    path!("/tmp"),
726                    json!({
727                        "issue28339": {
728                            "foo": {
729                                "bar.txt": ""
730                            },
731                        },
732                    }),
733                )],
734                vec![path!("/tmp")],
735                {
736                    test!(
737                        "foo/./bar.txt",
738                        "/tmp/issue28339/foo/bar.txt",
739                        "/tmp/issue28339"
740                    );
741                    test!(
742                        "foo/../foo/bar.txt",
743                        "/tmp/issue28339/foo/bar.txt",
744                        "/tmp/issue28339"
745                    );
746                    test!(
747                        "foo/..///foo/bar.txt",
748                        "/tmp/issue28339/foo/bar.txt",
749                        "/tmp/issue28339"
750                    );
751                    test!(
752                        "issue28339/../issue28339/foo/../foo/bar.txt",
753                        "/tmp/issue28339/foo/bar.txt",
754                        "/tmp/issue28339"
755                    );
756                    test!(
757                        "./bar.txt",
758                        "/tmp/issue28339/foo/bar.txt",
759                        "/tmp/issue28339/foo"
760                    );
761                    test!(
762                        "../foo/bar.txt",
763                        "/tmp/issue28339/foo/bar.txt",
764                        "/tmp/issue28339/foo"
765                    );
766                }
767            )
768        }
769
770        // https://github.com/zed-industries/zed/issues/34027
771        #[gpui::test]
772        #[should_panic(expected = "Tooltip mismatch")]
773        async fn issue_34027(cx: &mut TestAppContext) {
774            test_path_likes!(
775                cx,
776                vec![(
777                    path!("/tmp/issue34027"),
778                    json!({
779                        "test.txt": "",
780                        "foo": {
781                            "test.txt": "",
782                        }
783                    }),
784                ),],
785                vec![path!("/tmp/issue34027")],
786                {
787                    test!("test.txt", "/tmp/issue34027/test.txt", "/tmp/issue34027");
788                    test!(
789                        "test.txt",
790                        "/tmp/issue34027/foo/test.txt",
791                        "/tmp/issue34027/foo"
792                    );
793                }
794            )
795        }
796
797        // https://github.com/zed-industries/zed/issues/34027
798        #[gpui::test]
799        #[should_panic(expected = "Tooltip mismatch")]
800        async fn issue_34027_non_worktree_file(cx: &mut TestAppContext) {
801            test_path_likes!(
802                cx,
803                vec![
804                    (
805                        path!("/"),
806                        json!({
807                            "file.txt": "",
808                        }),
809                    ),
810                    (
811                        path!("/test"),
812                        json!({
813                            "file.txt": "",
814                        }),
815                    ),
816                ],
817                vec![path!("/test")],
818                {
819                    test!("file.txt", "/file.txt", "/");
820                    test!("file.txt", "/test/file.txt", "/test");
821                }
822            )
823        }
824    }
825}