diff --git a/crates/terminal_view/src/terminal_path_like_target.rs b/crates/terminal_view/src/terminal_path_like_target.rs index 226a8f4c3d9bca398df778fa2043fb5872383b56..22d4de73a8716b41096b609674d40c1b64d554e2 100644 --- a/crates/terminal_view/src/terminal_path_like_target.rs +++ b/crates/terminal_view/src/terminal_path_like_target.rs @@ -4,38 +4,63 @@ use editor::Editor; use gpui::{App, AppContext, Context, Task, WeakEntity, Window}; use itertools::Itertools; use project::{Entry, Metadata}; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use terminal::PathLikeTarget; use util::{ResultExt, debug_panic, paths::PathWithPosition}; use workspace::{OpenOptions, OpenVisible, Workspace}; +/// The way we found the open target. This is important to have for test assertions. +/// For example, remote projects never look in the file system. +#[cfg(test)] +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +enum OpenTargetFoundBy { + WorktreeExact, + WorktreeScan, + FileSystemBackground, +} + +#[cfg(test)] +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +enum BackgroundFsChecks { + Enabled, + Disabled, +} + #[derive(Debug, Clone)] enum OpenTarget { - Worktree(PathWithPosition, Entry), + Worktree(PathWithPosition, Entry, #[cfg(test)] OpenTargetFoundBy), File(PathWithPosition, Metadata), } impl OpenTarget { fn is_file(&self) -> bool { match self { - OpenTarget::Worktree(_, entry) => entry.is_file(), + OpenTarget::Worktree(_, entry, ..) => entry.is_file(), OpenTarget::File(_, metadata) => !metadata.is_dir, } } fn is_dir(&self) -> bool { match self { - OpenTarget::Worktree(_, entry) => entry.is_dir(), + OpenTarget::Worktree(_, entry, ..) => entry.is_dir(), OpenTarget::File(_, metadata) => metadata.is_dir, } } fn path(&self) -> &PathWithPosition { match self { - OpenTarget::Worktree(path, _) => path, + OpenTarget::Worktree(path, ..) => path, OpenTarget::File(path, _) => path, } } + + #[cfg(test)] + fn found_by(&self) -> OpenTargetFoundBy { + match self { + OpenTarget::Worktree(.., found_by) => *found_by, + OpenTarget::File(..) => OpenTargetFoundBy::FileSystemBackground, + } + } } pub(super) fn hover_path_like_target( @@ -44,12 +69,41 @@ pub(super) fn hover_path_like_target( path_like_target: &PathLikeTarget, cx: &mut Context, ) -> Task<()> { - let file_to_open_task = possible_open_target(workspace, path_like_target, cx); + #[cfg(not(test))] + { + possible_hover_target(workspace, hovered_word, path_like_target, cx) + } + #[cfg(test)] + { + possible_hover_target( + workspace, + hovered_word, + path_like_target, + cx, + BackgroundFsChecks::Enabled, + ) + } +} + +fn possible_hover_target( + workspace: &WeakEntity, + hovered_word: HoveredWord, + path_like_target: &PathLikeTarget, + cx: &mut Context, + #[cfg(test)] background_fs_checks: BackgroundFsChecks, +) -> Task<()> { + let file_to_open_task = possible_open_target( + workspace, + path_like_target, + cx, + #[cfg(test)] + background_fs_checks, + ); cx.spawn(async move |terminal_view, cx| { let file_to_open = file_to_open_task.await; terminal_view .update(cx, |terminal_view, _| match file_to_open { - Some(OpenTarget::File(path, _) | OpenTarget::Worktree(path, _)) => { + Some(OpenTarget::File(path, _) | OpenTarget::Worktree(path, ..)) => { terminal_view.hover = Some(HoverTarget { tooltip: path.to_string(|path| path.to_string_lossy().to_string()), hovered_word, @@ -67,6 +121,7 @@ fn possible_open_target( workspace: &WeakEntity, path_like_target: &PathLikeTarget, cx: &App, + #[cfg(test)] background_fs_checks: BackgroundFsChecks, ) -> Task> { let Some(workspace) = workspace.upgrade() else { return Task::ready(None); @@ -117,9 +172,19 @@ fn possible_open_target( // If we won't find paths "easily", we can traverse the entire worktree to look what ends with the potential path suffix. // That will be slow, though, so do the fast checks first. let mut worktree_paths_to_check = Vec::new(); - for worktree in &worktree_candidates { + let mut is_cwd_in_worktree = false; + let mut open_target = None; + 'worktree_loop: for worktree in &worktree_candidates { let worktree_root = worktree.read(cx).abs_path(); let mut paths_to_check = Vec::with_capacity(potential_paths.len()); + let relative_cwd = cwd + .and_then(|cwd| cwd.strip_prefix(&worktree_root).ok()) + .and_then(|cwd_stripped| { + (cwd_stripped != Path::new("")).then(|| { + is_cwd_in_worktree = true; + cwd_stripped + }) + }); for path_with_position in &potential_paths { let path_to_check = if worktree_root.ends_with(&path_with_position.path) { @@ -130,10 +195,13 @@ fn possible_open_target( }; match worktree.read(cx).root_entry() { Some(root_entry) => { - return Task::ready(Some(OpenTarget::Worktree( + open_target = Some(OpenTarget::Worktree( root_path_with_position, root_entry.clone(), - ))); + #[cfg(test)] + OpenTargetFoundBy::WorktreeExact, + )); + break 'worktree_loop; } None => root_path_with_position, } @@ -150,16 +218,26 @@ fn possible_open_target( }; if path_to_check.path.is_relative() - && let Some(entry) = worktree.read(cx).entry_for_path(&path_to_check.path) + && !worktree.read(cx).is_single_file() + && let Some(entry) = relative_cwd + .and_then(|relative_cwd| { + worktree + .read(cx) + .entry_for_path(&relative_cwd.join(&path_to_check.path)) + }) + .or_else(|| worktree.read(cx).entry_for_path(&path_to_check.path)) { - return Task::ready(Some(OpenTarget::Worktree( + open_target = Some(OpenTarget::Worktree( PathWithPosition { path: worktree_root.join(&entry.path), row: path_to_check.row, column: path_to_check.column, }, entry.clone(), - ))); + #[cfg(test)] + OpenTargetFoundBy::WorktreeExact, + )); + break 'worktree_loop; } paths_to_check.push(path_to_check); @@ -170,101 +248,143 @@ fn possible_open_target( } } + #[cfg(not(test))] + let enable_background_fs_checks = workspace.read(cx).project().read(cx).is_local(); + #[cfg(test)] + let enable_background_fs_checks = background_fs_checks == BackgroundFsChecks::Enabled; + + if open_target.is_some() { + // We we want to prefer open targets found via background fs checks over worktree matches, + // however we can return early if either: + // - This is a remote project, or + // - If the terminal working directory is inside of at least one worktree + if !enable_background_fs_checks || is_cwd_in_worktree { + return Task::ready(open_target); + } + } + // Before entire worktree traversal(s), make an attempt to do FS checks if available. - let fs_paths_to_check = if workspace.read(cx).project().read(cx).is_local() { - potential_paths - .into_iter() - .flat_map(|path_to_check| { - let mut paths_to_check = Vec::new(); - let maybe_path = &path_to_check.path; - if maybe_path.starts_with("~") { - if let Some(home_path) = - maybe_path - .strip_prefix("~") - .ok() - .and_then(|stripped_maybe_path| { - Some(dirs::home_dir()?.join(stripped_maybe_path)) - }) - { - paths_to_check.push(PathWithPosition { - path: home_path, - row: path_to_check.row, - column: path_to_check.column, - }); - } - } else { - paths_to_check.push(PathWithPosition { - path: maybe_path.clone(), - row: path_to_check.row, - column: path_to_check.column, - }); - if maybe_path.is_relative() { - if let Some(cwd) = &cwd { + let fs_paths_to_check = + if enable_background_fs_checks { + let fs_cwd_paths_to_check = cwd + .iter() + .flat_map(|cwd| { + let mut paths_to_check = Vec::new(); + for path_to_check in &potential_paths { + let maybe_path = &path_to_check.path; + if path_to_check.path.is_relative() { paths_to_check.push(PathWithPosition { - path: cwd.join(maybe_path), + path: cwd.join(&maybe_path), row: path_to_check.row, column: path_to_check.column, }); } - for worktree in &worktree_candidates { - paths_to_check.push(PathWithPosition { - path: worktree.read(cx).abs_path().join(maybe_path), - row: path_to_check.row, - column: path_to_check.column, - }); - } - } - } - paths_to_check - }) - .collect() - } else { - Vec::new() - }; - - let worktree_check_task = cx.spawn(async move |cx| { - for (worktree, worktree_paths_to_check) in worktree_paths_to_check { - let found_entry = worktree - .update(cx, |worktree, _| { - let worktree_root = worktree.abs_path(); - let traversal = worktree.traverse_from_path(true, true, false, "".as_ref()); - for entry in traversal { - if let Some(path_in_worktree) = worktree_paths_to_check - .iter() - .find(|path_to_check| entry.path.ends_with(&path_to_check.path)) - { - return Some(OpenTarget::Worktree( - PathWithPosition { - path: worktree_root.join(&entry.path), - row: path_in_worktree.row, - column: path_in_worktree.column, - }, - entry.clone(), - )); - } } - None + paths_to_check }) - .ok()?; - if let Some(found_entry) = found_entry { - return Some(found_entry); - } - } - None - }); + .collect::>(); + fs_cwd_paths_to_check + .into_iter() + .chain( + potential_paths + .into_iter() + .flat_map(|path_to_check| { + let mut paths_to_check = Vec::new(); + let maybe_path = &path_to_check.path; + if maybe_path.starts_with("~") { + if let Some(home_path) = maybe_path.strip_prefix("~").ok().and_then( + |stripped_maybe_path| { + Some(dirs::home_dir()?.join(stripped_maybe_path)) + }, + ) { + paths_to_check.push(PathWithPosition { + path: home_path, + row: path_to_check.row, + column: path_to_check.column, + }); + } + } else { + paths_to_check.push(PathWithPosition { + path: maybe_path.clone(), + row: path_to_check.row, + column: path_to_check.column, + }); + if maybe_path.is_relative() { + for worktree in &worktree_candidates { + if !worktree.read(cx).is_single_file() { + paths_to_check.push(PathWithPosition { + path: worktree.read(cx).abs_path().join(maybe_path), + row: path_to_check.row, + column: path_to_check.column, + }); + } + } + } + } + paths_to_check + }) + .collect::>(), + ) + .collect() + } else { + Vec::new() + }; let fs = workspace.read(cx).project().read(cx).fs().clone(); - cx.background_spawn(async move { + let background_fs_checks_task = cx.background_spawn(async move { for mut path_to_check in fs_paths_to_check { if let Some(fs_path_to_check) = fs.canonicalize(&path_to_check.path).await.ok() && let Some(metadata) = fs.metadata(&fs_path_to_check).await.ok().flatten() { - path_to_check.path = fs_path_to_check; - return Some(OpenTarget::File(path_to_check, metadata)); + if open_target + .as_ref() + .map(|open_target| open_target.path().path != fs_path_to_check) + .unwrap_or(true) + { + path_to_check.path = fs_path_to_check; + return Some(OpenTarget::File(path_to_check, metadata)); + } + + break; } } - worktree_check_task.await + open_target + }); + + cx.spawn(async move |cx| { + background_fs_checks_task.await.or_else(|| { + for (worktree, worktree_paths_to_check) in worktree_paths_to_check { + let found_entry = worktree + .update(cx, |worktree, _| -> Option { + let worktree_root = worktree.abs_path(); + let traversal = worktree.traverse_from_path(true, true, false, "".as_ref()); + for entry in traversal { + if let Some(path_in_worktree) = worktree_paths_to_check + .iter() + .find(|path_to_check| entry.path.ends_with(&path_to_check.path)) + { + return Some(OpenTarget::Worktree( + PathWithPosition { + path: worktree_root.join(&entry.path), + row: path_in_worktree.row, + column: path_in_worktree.column, + }, + entry.clone(), + #[cfg(test)] + OpenTargetFoundBy::WorktreeScan, + )); + } + } + None + }) + .ok()?; + if let Some(found_entry) = found_entry { + return Some(found_entry); + } + } + None + }) }) } @@ -275,8 +395,23 @@ pub(super) fn open_path_like_target( window: &mut Window, cx: &mut Context, ) { - possibly_open_target(workspace, terminal_view, path_like_target, window, cx) + #[cfg(not(test))] + { + possibly_open_target(workspace, terminal_view, path_like_target, window, cx) + .detach_and_log_err(cx) + } + #[cfg(test)] + { + possibly_open_target( + workspace, + terminal_view, + path_like_target, + window, + cx, + BackgroundFsChecks::Enabled, + ) .detach_and_log_err(cx) + } } fn possibly_open_target( @@ -285,6 +420,7 @@ fn possibly_open_target( path_like_target: &PathLikeTarget, window: &mut Window, cx: &mut Context, + #[cfg(test)] background_fs_checks: BackgroundFsChecks, ) -> Task>> { if terminal_view.hover.is_none() { return Task::ready(Ok(None)); @@ -294,7 +430,13 @@ fn possibly_open_target( cx.spawn_in(window, async move |terminal_view, cx| { let Some(open_target) = terminal_view .update(cx, |_, cx| { - possible_open_target(&workspace, &path_like_target, cx) + possible_open_target( + &workspace, + &path_like_target, + cx, + #[cfg(test)] + background_fs_checks, + ) })? .await else { @@ -375,8 +517,11 @@ mod tests { app_cx: &mut TestAppContext, trees: impl IntoIterator, worktree_roots: impl IntoIterator, - ) -> impl AsyncFnMut(HoveredWord, PathLikeTarget) -> (Option, Option) - { + ) -> impl AsyncFnMut( + HoveredWord, + PathLikeTarget, + BackgroundFsChecks, + ) -> (Option, Option) { let fs = app_cx.update(AppState::test).fs.as_fake().clone(); app_cx.update(|cx| { @@ -393,10 +538,7 @@ mod tests { let project = Project::test( fs.clone(), - worktree_roots - .into_iter() - .map(Path::new) - .collect::>(), + worktree_roots.into_iter().map(Path::new), app_cx, ) .await; @@ -424,16 +566,18 @@ mod tests { }); async move |hovered_word: HoveredWord, - path_like_target: PathLikeTarget| + path_like_target: PathLikeTarget, + background_fs_checks: BackgroundFsChecks| -> (Option, Option) { let workspace_a = workspace.clone(); terminal_view .update(cx, |_, cx| { - hover_path_like_target( + possible_hover_target( &workspace_a.downgrade(), hovered_word, &path_like_target, cx, + background_fs_checks, ) }) .await; @@ -449,6 +593,7 @@ mod tests { &path_like_target, window, cx, + background_fs_checks, ) }) .await @@ -462,10 +607,13 @@ mod tests { test_path_like: &mut impl AsyncFnMut( HoveredWord, PathLikeTarget, + BackgroundFsChecks, ) -> (Option, Option), maybe_path: &str, tooltip: &str, terminal_dir: Option, + background_fs_checks: BackgroundFsChecks, + mut open_target_found_by: OpenTargetFoundBy, file: &str, line: u32, ) { @@ -479,6 +627,7 @@ mod tests { maybe_path: maybe_path.to_string(), terminal_dir, }, + background_fs_checks, ) .await; @@ -512,24 +661,70 @@ mod tests { Path::new(tooltip), "Open target path mismatch at {file}:{line}:" ); + + if background_fs_checks == BackgroundFsChecks::Disabled + && open_target_found_by == OpenTargetFoundBy::FileSystemBackground + { + open_target_found_by = OpenTargetFoundBy::WorktreeScan; + } + + assert_eq!( + open_target.found_by(), + open_target_found_by, + "Open target found by mismatch at {file}:{line}:" + ); } - macro_rules! none_or_some { - () => { + macro_rules! none_or_some_pathbuf { + (None) => { None }; - ($some:expr) => { - Some($some) + ($cwd:literal) => { + Some($crate::PathBuf::from(path!($cwd))) }; } macro_rules! test_path_like { - ($test_path_like:expr, $maybe_path:literal, $tooltip:literal $(, $cwd:literal)?) => { + ( + $test_path_like:expr, + $maybe_path:literal, + $tooltip:literal, + $cwd:tt, + $found_by:expr + ) => {{ + test_path_like!( + $test_path_like, + $maybe_path, + $tooltip, + $cwd, + BackgroundFsChecks::Enabled, + $found_by + ); + test_path_like!( + $test_path_like, + $maybe_path, + $tooltip, + $cwd, + BackgroundFsChecks::Disabled, + $found_by + ); + }}; + + ( + $test_path_like:expr, + $maybe_path:literal, + $tooltip:literal, + $cwd:tt, + $background_fs_checks:path, + $found_by:expr + ) => { test_path_like_simple( &mut $test_path_like, path!($maybe_path), path!($tooltip), - none_or_some!($($crate::PathBuf::from(path!($cwd)))?), + none_or_some_pathbuf!($cwd), + $background_fs_checks, + $found_by, std::file!(), std::line!(), ) @@ -537,17 +732,84 @@ mod tests { }; } + // Note the arms of `test`, `test_local`, and `test_remote` should be collapsed once macro + // metavariable expressions (#![feature(macro_metavar_expr)]) are stabilized. + // See https://github.com/rust-lang/rust/issues/83527 #[doc = "test_path_likes!(, , , { $(;)+ })"] macro_rules! test_path_likes { ($cx:expr, $trees:expr, $worktrees:expr, { $($tests:expr;)+ }) => { { let mut test_path_like = init_test($cx, $trees, $worktrees).await; - #[doc ="test!(, , )"] + #[doc ="test!(, , "] + #[doc ="\\[, found by \\])"] + #[allow(unused_macros)] macro_rules! test { - ($maybe_path:literal, $tooltip:literal) => { - test_path_like!(test_path_like, $maybe_path, $tooltip) + ($maybe_path:literal, $tooltip:literal, $cwd:tt) => { + test_path_like!( + test_path_like, + $maybe_path, + $tooltip, + $cwd, + OpenTargetFoundBy::WorktreeExact + ) + }; + ($maybe_path:literal, $tooltip:literal, $cwd:tt, $found_by:ident) => { + test_path_like!( + test_path_like, + $maybe_path, + $tooltip, + $cwd, + OpenTargetFoundBy::$found_by + ) + } + } + #[doc ="test_local!(, , "] + #[doc ="\\[, found by \\])"] + #[allow(unused_macros)] + macro_rules! test_local { + ($maybe_path:literal, $tooltip:literal, $cwd:tt) => { + test_path_like!( + test_path_like, + $maybe_path, + $tooltip, + $cwd, + BackgroundFsChecks::Enabled, + OpenTargetFoundBy::WorktreeExact + ) + }; + ($maybe_path:literal, $tooltip:literal, $cwd:tt, $found_by:ident) => { + test_path_like!( + test_path_like, + $maybe_path, + $tooltip, + $cwd, + BackgroundFsChecks::Enabled, + OpenTargetFoundBy::$found_by + ) + } + } + #[doc ="test_remote!(, , "] + #[doc ="\\[, found by \\])"] + #[allow(unused_macros)] + macro_rules! test_remote { + ($maybe_path:literal, $tooltip:literal, $cwd:tt) => { + test_path_like!( + test_path_like, + $maybe_path, + $tooltip, + $cwd, + BackgroundFsChecks::Disabled, + OpenTargetFoundBy::WorktreeExact + ) }; - ($maybe_path:literal, $tooltip:literal, $cwd:literal) => { - test_path_like!(test_path_like, $maybe_path, $tooltip, $cwd) + ($maybe_path:literal, $tooltip:literal, $cwd:tt, $found_by:ident) => { + test_path_like!( + test_path_like, + $maybe_path, + $tooltip, + $cwd, + BackgroundFsChecks::Disabled, + OpenTargetFoundBy::$found_by + ) } } $($tests);+ @@ -567,8 +829,10 @@ mod tests { )], vec![path!("/test")], { - test!("lib.rs", "/test/lib.rs"); - test!("test.rs", "/test/test.rs"); + test!("lib.rs", "/test/lib.rs", None); + test!("/test/lib.rs", "/test/lib.rs", None); + test!("test.rs", "/test/test.rs", None); + test!("/test/test.rs", "/test/test.rs", None); } ) } @@ -596,9 +860,15 @@ mod tests { vec![path!("/file.txt"), path!("/test")], { test!("file.txt", "/file.txt", "/"); + test!("/file.txt", "/file.txt", "/"); + test!("lib.rs", "/test/lib.rs", "/test"); test!("test.rs", "/test/test.rs", "/test"); test!("file.txt", "/test/file.txt", "/test"); + + test!("/test/lib.rs", "/test/lib.rs", "/test"); + test!("/test/test.rs", "/test/test.rs", "/test"); + test!("/test/file.txt", "/test/file.txt", "/test"); } ) } @@ -649,7 +919,7 @@ mod tests { )], vec![path!("/dir1")], { - test!("C.py", "/dir1/dir 2/C.py", "/dir1"); + test!("C.py", "/dir1/dir 2/C.py", "/dir1", WorktreeScan); test!("C.py", "/dir1/dir 2/C.py", "/dir1/dir 2"); test!("C.py", "/dir1/dir 3/C.py", "/dir1/dir 3"); } @@ -660,7 +930,6 @@ mod tests { // See https://github.com/zed-industries/zed/issues/34027 // See https://github.com/zed-industries/zed/issues/33498 #[gpui::test] - #[should_panic(expected = "Tooltip mismatch")] async fn issue_28407_nesting(cx: &mut TestAppContext) { test_path_likes!( cx, @@ -669,7 +938,8 @@ mod tests { json!({ "lib": { "src": { - "main.rs": "" + "main.rs": "", + "only_in_lib.rs": "" }, }, "src": { @@ -679,22 +949,12 @@ mod tests { )], vec![path!("/project")], { - // Failing currently - test!("main.rs", "/project/src/main.rs", "/project"); test!("main.rs", "/project/src/main.rs", "/project/src"); - test!("main.rs", "/project/lib/src/main.rs", "/project/lib"); test!("main.rs", "/project/lib/src/main.rs", "/project/lib/src"); test!("src/main.rs", "/project/src/main.rs", "/project"); test!("src/main.rs", "/project/src/main.rs", "/project/src"); - // Failing currently test!("src/main.rs", "/project/lib/src/main.rs", "/project/lib"); - // Failing currently - test!( - "src/main.rs", - "/project/lib/src/main.rs", - "/project/lib/src" - ); test!("lib/src/main.rs", "/project/lib/src/main.rs", "/project"); test!( @@ -712,11 +972,19 @@ mod tests { "/project/lib/src/main.rs", "/project/lib/src" ); + test!( + "src/only_in_lib.rs", + "/project/lib/src/only_in_lib.rs", + "/project/lib/src", + WorktreeScan + ); } ) } // https://github.com/zed-industries/zed/issues/28339 + // Note: These could all be found by WorktreeExact if we used + // `fs::normalize_path(&maybe_path)` #[gpui::test] async fn issue_28339(cx: &mut TestAppContext) { test_path_likes!( @@ -733,32 +1001,90 @@ mod tests { )], vec![path!("/tmp")], { - test!( + test_local!( "foo/./bar.txt", "/tmp/issue28339/foo/bar.txt", "/tmp/issue28339" ); - test!( + test_local!( + "foo/../foo/bar.txt", + "/tmp/issue28339/foo/bar.txt", + "/tmp/issue28339", + FileSystemBackground + ); + test_local!( + "foo/..///foo/bar.txt", + "/tmp/issue28339/foo/bar.txt", + "/tmp/issue28339", + FileSystemBackground + ); + test_local!( + "issue28339/../issue28339/foo/../foo/bar.txt", + "/tmp/issue28339/foo/bar.txt", + "/tmp/issue28339", + FileSystemBackground + ); + test_local!( + "./bar.txt", + "/tmp/issue28339/foo/bar.txt", + "/tmp/issue28339/foo" + ); + test_local!( + "../foo/bar.txt", + "/tmp/issue28339/foo/bar.txt", + "/tmp/issue28339/foo", + FileSystemBackground + ); + } + ) + } + + // https://github.com/zed-industries/zed/issues/28339 + // Note: These could all be found by WorktreeExact if we used + // `fs::normalize_path(&maybe_path)` + #[gpui::test] + #[should_panic(expected = "Hover target should not be `None`")] + async fn issue_28339_remote(cx: &mut TestAppContext) { + test_path_likes!( + cx, + vec![( + path!("/tmp"), + json!({ + "issue28339": { + "foo": { + "bar.txt": "" + }, + }, + }), + )], + vec![path!("/tmp")], + { + test_remote!( + "foo/./bar.txt", + "/tmp/issue28339/foo/bar.txt", + "/tmp/issue28339" + ); + test_remote!( "foo/../foo/bar.txt", "/tmp/issue28339/foo/bar.txt", "/tmp/issue28339" ); - test!( + test_remote!( "foo/..///foo/bar.txt", "/tmp/issue28339/foo/bar.txt", "/tmp/issue28339" ); - test!( + test_remote!( "issue28339/../issue28339/foo/../foo/bar.txt", "/tmp/issue28339/foo/bar.txt", "/tmp/issue28339" ); - test!( + test_remote!( "./bar.txt", "/tmp/issue28339/foo/bar.txt", "/tmp/issue28339/foo" ); - test!( + test_remote!( "../foo/bar.txt", "/tmp/issue28339/foo/bar.txt", "/tmp/issue28339/foo" @@ -769,7 +1095,6 @@ mod tests { // https://github.com/zed-industries/zed/issues/34027 #[gpui::test] - #[should_panic(expected = "Tooltip mismatch")] async fn issue_34027(cx: &mut TestAppContext) { test_path_likes!( cx, @@ -796,8 +1121,128 @@ mod tests { // https://github.com/zed-industries/zed/issues/34027 #[gpui::test] - #[should_panic(expected = "Tooltip mismatch")] - async fn issue_34027_non_worktree_file(cx: &mut TestAppContext) { + async fn issue_34027_siblings(cx: &mut TestAppContext) { + test_path_likes!( + cx, + vec![( + path!("/test"), + json!({ + "sub1": { + "file.txt": "", + }, + "sub2": { + "file.txt": "", + } + }), + ),], + vec![path!("/test")], + { + test!("file.txt", "/test/sub1/file.txt", "/test/sub1"); + test!("file.txt", "/test/sub2/file.txt", "/test/sub2"); + test!("sub1/file.txt", "/test/sub1/file.txt", "/test/sub1"); + test!("sub2/file.txt", "/test/sub2/file.txt", "/test/sub2"); + test!("sub1/file.txt", "/test/sub1/file.txt", "/test/sub2"); + test!("sub2/file.txt", "/test/sub2/file.txt", "/test/sub1"); + } + ) + } + + // https://github.com/zed-industries/zed/issues/34027 + #[gpui::test] + async fn issue_34027_nesting(cx: &mut TestAppContext) { + test_path_likes!( + cx, + vec![( + path!("/test"), + json!({ + "sub1": { + "file.txt": "", + "subsub1": { + "file.txt": "", + } + }, + "sub2": { + "file.txt": "", + "subsub1": { + "file.txt": "", + } + } + }), + ),], + vec![path!("/test")], + { + test!( + "file.txt", + "/test/sub1/subsub1/file.txt", + "/test/sub1/subsub1" + ); + test!( + "file.txt", + "/test/sub2/subsub1/file.txt", + "/test/sub2/subsub1" + ); + test!( + "subsub1/file.txt", + "/test/sub1/subsub1/file.txt", + "/test", + WorktreeScan + ); + test!( + "subsub1/file.txt", + "/test/sub1/subsub1/file.txt", + "/test", + WorktreeScan + ); + test!( + "subsub1/file.txt", + "/test/sub1/subsub1/file.txt", + "/test/sub1" + ); + test!( + "subsub1/file.txt", + "/test/sub2/subsub1/file.txt", + "/test/sub2" + ); + test!( + "subsub1/file.txt", + "/test/sub1/subsub1/file.txt", + "/test/sub1/subsub1", + WorktreeScan + ); + } + ) + } + + // https://github.com/zed-industries/zed/issues/34027 + #[gpui::test] + async fn issue_34027_non_worktree_local_file(cx: &mut TestAppContext) { + test_path_likes!( + cx, + vec![ + ( + path!("/"), + json!({ + "file.txt": "", + }), + ), + ( + path!("/test"), + json!({ + "file.txt": "", + }), + ), + ], + vec![path!("/test")], + { + // Note: Opening a non-worktree file adds that file as a single file worktree. + test_local!("file.txt", "/file.txt", "/", FileSystemBackground); + } + ) + } + + // https://github.com/zed-industries/zed/issues/34027 + #[gpui::test] + async fn issue_34027_non_worktree_remote_file(cx: &mut TestAppContext) { test_path_likes!( cx, vec![ @@ -816,8 +1261,67 @@ mod tests { ], vec![path!("/test")], { - test!("file.txt", "/file.txt", "/"); - test!("file.txt", "/test/file.txt", "/test"); + // Note: Opening a non-worktree file adds that file as a single file worktree. + test_remote!("file.txt", "/test/file.txt", "/"); + test_remote!("/test/file.txt", "/test/file.txt", "/"); + } + ) + } + + // See https://github.com/zed-industries/zed/issues/34027 + #[gpui::test] + #[should_panic(expected = "Tooltip mismatch")] + async fn issue_34027_gaps(cx: &mut TestAppContext) { + test_path_likes!( + cx, + vec![( + path!("/project"), + json!({ + "lib": { + "src": { + "main.rs": "" + }, + }, + "src": { + "main.rs": "" + }, + }), + )], + vec![path!("/project")], + { + test!("main.rs", "/project/src/main.rs", "/project"); + test!("main.rs", "/project/lib/src/main.rs", "/project/lib"); + } + ) + } + + // See https://github.com/zed-industries/zed/issues/34027 + #[gpui::test] + #[should_panic(expected = "Tooltip mismatch")] + async fn issue_34027_overlap(cx: &mut TestAppContext) { + test_path_likes!( + cx, + vec![( + path!("/project"), + json!({ + "lib": { + "src": { + "main.rs": "" + }, + }, + "src": { + "main.rs": "" + }, + }), + )], + vec![path!("/project")], + { + // Finds "/project/src/main.rs" + test!( + "src/main.rs", + "/project/lib/src/main.rs", + "/project/lib/src" + ); } ) }