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}