terminal: Normalize path-like targets with leading `..` (#47289)

Thanh Nguyen and Ben Kunkle created

Fixes #28339

## What
- Normalize cwd-relative path-like targets that include leading `..`
before worktree lookup.
- Update issue #28339 tests to expect `WorktreeExact` for local and
remote resolution.

## Why
- Prevent terminal links like `../foo/bar.txt` from creating invalid
entries; resolve to the correct worktree file instead.

## How to test
- `cargo test -p terminal_view issue_28339 -- --nocapture`
- Manual: open a workspace, run `echo ../foo/bar.txt` from a subdir in
the terminal, then cmd/ctrl-click and confirm the correct file opens.

## Risk
- Low: only changes terminal path-like resolution for `..` paths;
expected to match normalized behavior.



## Checklist
- [x] Tests run (`cargo test -p terminal_view issue_28339 --
--nocapture`)
- [x] Docs updated (not needed)
- [x] Backwards compatibility considered

Release Notes:
- Fixed an issue where relative paths starting with `..` would not resolve correctly when clicking the link in the terminal

---------

Co-authored-by: Ben Kunkle <ben.kunkle@gmail.com>

Change summary

crates/terminal_view/src/terminal_path_like_target.rs | 45 ++++++++----
1 file changed, 28 insertions(+), 17 deletions(-)

Detailed changes

crates/terminal_view/src/terminal_path_like_target.rs 🔗

@@ -8,7 +8,7 @@ use std::path::PathBuf;
 use terminal::PathLikeTarget;
 use util::{
     ResultExt, debug_panic,
-    paths::{PathStyle, PathWithPosition},
+    paths::{PathStyle, PathWithPosition, normalize_lexically},
     rel_path::RelPath,
 };
 use workspace::{OpenOptions, OpenVisible, Workspace};
@@ -222,17 +222,33 @@ fn possible_open_target(
                 }
             };
 
-            if let Ok(relative_path_to_check) =
-                RelPath::new(&path_to_check.path, PathStyle::local())
-                && !worktree.read(cx).is_single_file()
-                && let Some(entry) = relative_cwd
-                    .clone()
-                    .and_then(|relative_cwd| {
-                        worktree
-                            .read(cx)
-                            .entry_for_path(&relative_cwd.join(&relative_path_to_check))
+            // Normalize the path by joining with cwd if available (handles `.` and `..` segments)
+            let normalized_path = if path_to_check.path.is_relative() {
+                relative_cwd.as_ref().and_then(|relative_cwd| {
+                    let joined = relative_cwd
+                        .as_ref()
+                        .as_std_path()
+                        .join(&path_to_check.path);
+                    normalize_lexically(&joined).ok().and_then(|p| {
+                        RelPath::new(&p, PathStyle::local())
+                            .ok()
+                            .map(std::borrow::Cow::into_owned)
+                    })
+                })
+            } else {
+                None
+            };
+            let original_path = RelPath::new(&path_to_check.path, PathStyle::local()).ok();
+
+            if !worktree.read(cx).is_single_file()
+                && let Some(entry) = normalized_path
+                    .as_ref()
+                    .and_then(|p| worktree.read(cx).entry_for_path(p))
+                    .or_else(|| {
+                        original_path
+                            .as_ref()
+                            .and_then(|p| worktree.read(cx).entry_for_path(p.as_ref()))
                     })
-                    .or_else(|| worktree.read(cx).entry_for_path(&relative_path_to_check))
             {
                 open_target = Some(OpenTarget::Worktree(
                     PathWithPosition {
@@ -999,8 +1015,6 @@ mod tests {
         }
 
         // 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!(
@@ -1051,17 +1065,14 @@ mod tests {
                         "../foo/bar.txt",
                         "/tmp/issue28339/foo/bar.txt",
                         "/tmp/issue28339/foo",
-                        FileSystemBackground
+                        WorktreeExact
                     );
                 }
             )
         }
 
         // 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,