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::{Path, PathBuf};
8use terminal::PathLikeTarget;
9use util::{ResultExt, debug_panic, paths::PathWithPosition};
10use workspace::{OpenOptions, OpenVisible, Workspace};
11
12/// The way we found the open target. This is important to have for test assertions.
13/// For example, remote projects never look in the file system.
14#[cfg(test)]
15#[derive(Debug, Clone, Copy, Eq, PartialEq)]
16enum OpenTargetFoundBy {
17 WorktreeExact,
18 WorktreeScan,
19 FileSystemBackground,
20}
21
22#[cfg(test)]
23#[derive(Debug, Clone, Copy, Eq, PartialEq)]
24enum BackgroundFsChecks {
25 Enabled,
26 Disabled,
27}
28
29#[derive(Debug, Clone)]
30enum OpenTarget {
31 Worktree(PathWithPosition, Entry, #[cfg(test)] OpenTargetFoundBy),
32 File(PathWithPosition, Metadata),
33}
34
35impl OpenTarget {
36 fn is_file(&self) -> bool {
37 match self {
38 OpenTarget::Worktree(_, entry, ..) => entry.is_file(),
39 OpenTarget::File(_, metadata) => !metadata.is_dir,
40 }
41 }
42
43 fn is_dir(&self) -> bool {
44 match self {
45 OpenTarget::Worktree(_, entry, ..) => entry.is_dir(),
46 OpenTarget::File(_, metadata) => metadata.is_dir,
47 }
48 }
49
50 fn path(&self) -> &PathWithPosition {
51 match self {
52 OpenTarget::Worktree(path, ..) => path,
53 OpenTarget::File(path, _) => path,
54 }
55 }
56
57 #[cfg(test)]
58 fn found_by(&self) -> OpenTargetFoundBy {
59 match self {
60 OpenTarget::Worktree(.., found_by) => *found_by,
61 OpenTarget::File(..) => OpenTargetFoundBy::FileSystemBackground,
62 }
63 }
64}
65
66pub(super) fn hover_path_like_target(
67 workspace: &WeakEntity<Workspace>,
68 hovered_word: HoveredWord,
69 path_like_target: &PathLikeTarget,
70 cx: &mut Context<TerminalView>,
71) -> Task<()> {
72 #[cfg(not(test))]
73 {
74 possible_hover_target(workspace, hovered_word, path_like_target, cx)
75 }
76 #[cfg(test)]
77 {
78 possible_hover_target(
79 workspace,
80 hovered_word,
81 path_like_target,
82 cx,
83 BackgroundFsChecks::Enabled,
84 )
85 }
86}
87
88fn possible_hover_target(
89 workspace: &WeakEntity<Workspace>,
90 hovered_word: HoveredWord,
91 path_like_target: &PathLikeTarget,
92 cx: &mut Context<TerminalView>,
93 #[cfg(test)] background_fs_checks: BackgroundFsChecks,
94) -> Task<()> {
95 let file_to_open_task = possible_open_target(
96 workspace,
97 path_like_target,
98 cx,
99 #[cfg(test)]
100 background_fs_checks,
101 );
102 cx.spawn(async move |terminal_view, cx| {
103 let file_to_open = file_to_open_task.await;
104 terminal_view
105 .update(cx, |terminal_view, _| match file_to_open {
106 Some(OpenTarget::File(path, _) | OpenTarget::Worktree(path, ..)) => {
107 terminal_view.hover = Some(HoverTarget {
108 tooltip: path.to_string(|path| path.to_string_lossy().to_string()),
109 hovered_word,
110 });
111 }
112 None => {
113 terminal_view.hover = None;
114 }
115 })
116 .ok();
117 })
118}
119
120fn possible_open_target(
121 workspace: &WeakEntity<Workspace>,
122 path_like_target: &PathLikeTarget,
123 cx: &App,
124 #[cfg(test)] background_fs_checks: BackgroundFsChecks,
125) -> Task<Option<OpenTarget>> {
126 let Some(workspace) = workspace.upgrade() else {
127 return Task::ready(None);
128 };
129 // We have to check for both paths, as on Unix, certain paths with positions are valid file paths too.
130 // We can be on FS remote part, without real FS, so cannot canonicalize or check for existence the path right away.
131 let mut potential_paths = Vec::new();
132 let cwd = path_like_target.terminal_dir.as_ref();
133 let maybe_path = &path_like_target.maybe_path;
134 let original_path = PathWithPosition::from_path(PathBuf::from(maybe_path));
135 let path_with_position = PathWithPosition::parse_str(maybe_path);
136 let worktree_candidates = workspace
137 .read(cx)
138 .worktrees(cx)
139 .sorted_by_key(|worktree| {
140 let worktree_root = worktree.read(cx).abs_path();
141 match cwd.and_then(|cwd| worktree_root.strip_prefix(cwd).ok()) {
142 Some(cwd_child) => cwd_child.components().count(),
143 None => usize::MAX,
144 }
145 })
146 .collect::<Vec<_>>();
147 // Since we do not check paths via FS and joining, we need to strip off potential `./`, `a/`, `b/` prefixes out of it.
148 const GIT_DIFF_PATH_PREFIXES: &[&str] = &["a", "b"];
149 for prefix_str in GIT_DIFF_PATH_PREFIXES.iter().chain(std::iter::once(&".")) {
150 if let Some(stripped) = original_path.path.strip_prefix(prefix_str).ok() {
151 potential_paths.push(PathWithPosition {
152 path: stripped.to_owned(),
153 row: original_path.row,
154 column: original_path.column,
155 });
156 }
157 if let Some(stripped) = path_with_position.path.strip_prefix(prefix_str).ok() {
158 potential_paths.push(PathWithPosition {
159 path: stripped.to_owned(),
160 row: path_with_position.row,
161 column: path_with_position.column,
162 });
163 }
164 }
165
166 let insert_both_paths = original_path != path_with_position;
167 potential_paths.insert(0, original_path);
168 if insert_both_paths {
169 potential_paths.insert(1, path_with_position);
170 }
171
172 // If we won't find paths "easily", we can traverse the entire worktree to look what ends with the potential path suffix.
173 // That will be slow, though, so do the fast checks first.
174 let mut worktree_paths_to_check = Vec::new();
175 let mut is_cwd_in_worktree = false;
176 let mut open_target = None;
177 'worktree_loop: for worktree in &worktree_candidates {
178 let worktree_root = worktree.read(cx).abs_path();
179 let mut paths_to_check = Vec::with_capacity(potential_paths.len());
180 let relative_cwd = cwd
181 .and_then(|cwd| cwd.strip_prefix(&worktree_root).ok())
182 .and_then(|cwd_stripped| {
183 (cwd_stripped != Path::new("")).then(|| {
184 is_cwd_in_worktree = true;
185 cwd_stripped
186 })
187 });
188
189 for path_with_position in &potential_paths {
190 let path_to_check = if worktree_root.ends_with(&path_with_position.path) {
191 let root_path_with_position = PathWithPosition {
192 path: worktree_root.to_path_buf(),
193 row: path_with_position.row,
194 column: path_with_position.column,
195 };
196 match worktree.read(cx).root_entry() {
197 Some(root_entry) => {
198 open_target = Some(OpenTarget::Worktree(
199 root_path_with_position,
200 root_entry.clone(),
201 #[cfg(test)]
202 OpenTargetFoundBy::WorktreeExact,
203 ));
204 break 'worktree_loop;
205 }
206 None => root_path_with_position,
207 }
208 } else {
209 PathWithPosition {
210 path: path_with_position
211 .path
212 .strip_prefix(&worktree_root)
213 .unwrap_or(&path_with_position.path)
214 .to_owned(),
215 row: path_with_position.row,
216 column: path_with_position.column,
217 }
218 };
219
220 if path_to_check.path.is_relative()
221 && !worktree.read(cx).is_single_file()
222 && let Some(entry) = relative_cwd
223 .and_then(|relative_cwd| {
224 worktree
225 .read(cx)
226 .entry_for_path(&relative_cwd.join(&path_to_check.path))
227 })
228 .or_else(|| worktree.read(cx).entry_for_path(&path_to_check.path))
229 {
230 open_target = Some(OpenTarget::Worktree(
231 PathWithPosition {
232 path: worktree_root.join(&entry.path),
233 row: path_to_check.row,
234 column: path_to_check.column,
235 },
236 entry.clone(),
237 #[cfg(test)]
238 OpenTargetFoundBy::WorktreeExact,
239 ));
240 break 'worktree_loop;
241 }
242
243 paths_to_check.push(path_to_check);
244 }
245
246 if !paths_to_check.is_empty() {
247 worktree_paths_to_check.push((worktree.clone(), paths_to_check));
248 }
249 }
250
251 #[cfg(not(test))]
252 let enable_background_fs_checks = workspace.read(cx).project().read(cx).is_local();
253 #[cfg(test)]
254 let enable_background_fs_checks = background_fs_checks == BackgroundFsChecks::Enabled;
255
256 if open_target.is_some() {
257 // We we want to prefer open targets found via background fs checks over worktree matches,
258 // however we can return early if either:
259 // - This is a remote project, or
260 // - If the terminal working directory is inside of at least one worktree
261 if !enable_background_fs_checks || is_cwd_in_worktree {
262 return Task::ready(open_target);
263 }
264 }
265
266 // Before entire worktree traversal(s), make an attempt to do FS checks if available.
267 let fs_paths_to_check =
268 if enable_background_fs_checks {
269 let fs_cwd_paths_to_check = cwd
270 .iter()
271 .flat_map(|cwd| {
272 let mut paths_to_check = Vec::new();
273 for path_to_check in &potential_paths {
274 let maybe_path = &path_to_check.path;
275 if path_to_check.path.is_relative() {
276 paths_to_check.push(PathWithPosition {
277 path: cwd.join(&maybe_path),
278 row: path_to_check.row,
279 column: path_to_check.column,
280 });
281 }
282 }
283 paths_to_check
284 })
285 .collect::<Vec<_>>();
286 fs_cwd_paths_to_check
287 .into_iter()
288 .chain(
289 potential_paths
290 .into_iter()
291 .flat_map(|path_to_check| {
292 let mut paths_to_check = Vec::new();
293 let maybe_path = &path_to_check.path;
294 if maybe_path.starts_with("~") {
295 if let Some(home_path) = maybe_path.strip_prefix("~").ok().and_then(
296 |stripped_maybe_path| {
297 Some(dirs::home_dir()?.join(stripped_maybe_path))
298 },
299 ) {
300 paths_to_check.push(PathWithPosition {
301 path: home_path,
302 row: path_to_check.row,
303 column: path_to_check.column,
304 });
305 }
306 } else {
307 paths_to_check.push(PathWithPosition {
308 path: maybe_path.clone(),
309 row: path_to_check.row,
310 column: path_to_check.column,
311 });
312 if maybe_path.is_relative() {
313 for worktree in &worktree_candidates {
314 if !worktree.read(cx).is_single_file() {
315 paths_to_check.push(PathWithPosition {
316 path: worktree.read(cx).abs_path().join(maybe_path),
317 row: path_to_check.row,
318 column: path_to_check.column,
319 });
320 }
321 }
322 }
323 }
324 paths_to_check
325 })
326 .collect::<Vec<_>>(),
327 )
328 .collect()
329 } else {
330 Vec::new()
331 };
332
333 let fs = workspace.read(cx).project().read(cx).fs().clone();
334 let background_fs_checks_task = cx.background_spawn(async move {
335 for mut path_to_check in fs_paths_to_check {
336 if let Some(fs_path_to_check) = fs.canonicalize(&path_to_check.path).await.ok()
337 && let Some(metadata) = fs.metadata(&fs_path_to_check).await.ok().flatten()
338 {
339 if open_target
340 .as_ref()
341 .map(|open_target| open_target.path().path != fs_path_to_check)
342 .unwrap_or(true)
343 {
344 path_to_check.path = fs_path_to_check;
345 return Some(OpenTarget::File(path_to_check, metadata));
346 }
347
348 break;
349 }
350 }
351
352 open_target
353 });
354
355 cx.spawn(async move |cx| {
356 background_fs_checks_task.await.or_else(|| {
357 for (worktree, worktree_paths_to_check) in worktree_paths_to_check {
358 let found_entry = worktree
359 .update(cx, |worktree, _| -> Option<OpenTarget> {
360 let worktree_root = worktree.abs_path();
361 let traversal = worktree.traverse_from_path(true, true, false, "".as_ref());
362 for entry in traversal {
363 if let Some(path_in_worktree) = worktree_paths_to_check
364 .iter()
365 .find(|path_to_check| entry.path.ends_with(&path_to_check.path))
366 {
367 return Some(OpenTarget::Worktree(
368 PathWithPosition {
369 path: worktree_root.join(&entry.path),
370 row: path_in_worktree.row,
371 column: path_in_worktree.column,
372 },
373 entry.clone(),
374 #[cfg(test)]
375 OpenTargetFoundBy::WorktreeScan,
376 ));
377 }
378 }
379 None
380 })
381 .ok()?;
382 if let Some(found_entry) = found_entry {
383 return Some(found_entry);
384 }
385 }
386 None
387 })
388 })
389}
390
391pub(super) fn open_path_like_target(
392 workspace: &WeakEntity<Workspace>,
393 terminal_view: &mut TerminalView,
394 path_like_target: &PathLikeTarget,
395 window: &mut Window,
396 cx: &mut Context<TerminalView>,
397) {
398 #[cfg(not(test))]
399 {
400 possibly_open_target(workspace, terminal_view, path_like_target, window, cx)
401 .detach_and_log_err(cx)
402 }
403 #[cfg(test)]
404 {
405 possibly_open_target(
406 workspace,
407 terminal_view,
408 path_like_target,
409 window,
410 cx,
411 BackgroundFsChecks::Enabled,
412 )
413 .detach_and_log_err(cx)
414 }
415}
416
417fn possibly_open_target(
418 workspace: &WeakEntity<Workspace>,
419 terminal_view: &mut TerminalView,
420 path_like_target: &PathLikeTarget,
421 window: &mut Window,
422 cx: &mut Context<TerminalView>,
423 #[cfg(test)] background_fs_checks: BackgroundFsChecks,
424) -> Task<Result<Option<OpenTarget>>> {
425 if terminal_view.hover.is_none() {
426 return Task::ready(Ok(None));
427 }
428 let workspace = workspace.clone();
429 let path_like_target = path_like_target.clone();
430 cx.spawn_in(window, async move |terminal_view, cx| {
431 let Some(open_target) = terminal_view
432 .update(cx, |_, cx| {
433 possible_open_target(
434 &workspace,
435 &path_like_target,
436 cx,
437 #[cfg(test)]
438 background_fs_checks,
439 )
440 })?
441 .await
442 else {
443 return Ok(None);
444 };
445
446 let path_to_open = open_target.path();
447 let opened_items = workspace
448 .update_in(cx, |workspace, window, cx| {
449 workspace.open_paths(
450 vec![path_to_open.path.clone()],
451 OpenOptions {
452 visible: Some(OpenVisible::OnlyDirectories),
453 ..Default::default()
454 },
455 None,
456 window,
457 cx,
458 )
459 })
460 .context("workspace update")?
461 .await;
462 if opened_items.len() != 1 {
463 debug_panic!(
464 "Received {} items for one path {path_to_open:?}",
465 opened_items.len(),
466 );
467 }
468
469 if let Some(opened_item) = opened_items.first() {
470 if open_target.is_file() {
471 if let Some(Ok(opened_item)) = opened_item {
472 if let Some(row) = path_to_open.row {
473 let col = path_to_open.column.unwrap_or(0);
474 if let Some(active_editor) = opened_item.downcast::<Editor>() {
475 active_editor
476 .downgrade()
477 .update_in(cx, |editor, window, cx| {
478 editor.go_to_singleton_buffer_point(
479 language::Point::new(
480 row.saturating_sub(1),
481 col.saturating_sub(1),
482 ),
483 window,
484 cx,
485 )
486 })
487 .log_err();
488 }
489 }
490 return Ok(Some(open_target));
491 }
492 } else if open_target.is_dir() {
493 workspace.update(cx, |workspace, cx| {
494 workspace.project().update(cx, |_, cx| {
495 cx.emit(project::Event::ActivateProjectPanel);
496 })
497 })?;
498 return Ok(Some(open_target));
499 }
500 }
501 Ok(None)
502 })
503}
504
505#[cfg(test)]
506mod tests {
507 use super::*;
508 use gpui::TestAppContext;
509 use project::Project;
510 use serde_json::json;
511 use std::path::{Path, PathBuf};
512 use terminal::{HoveredWord, alacritty_terminal::index::Point as AlacPoint};
513 use util::path;
514 use workspace::AppState;
515
516 async fn init_test(
517 app_cx: &mut TestAppContext,
518 trees: impl IntoIterator<Item = (&str, serde_json::Value)>,
519 worktree_roots: impl IntoIterator<Item = &str>,
520 ) -> impl AsyncFnMut(
521 HoveredWord,
522 PathLikeTarget,
523 BackgroundFsChecks,
524 ) -> (Option<HoverTarget>, Option<OpenTarget>) {
525 let fs = app_cx.update(AppState::test).fs.as_fake().clone();
526
527 app_cx.update(|cx| {
528 terminal::init(cx);
529 theme::init(theme::LoadThemes::JustBase, cx);
530 Project::init_settings(cx);
531 language::init(cx);
532 editor::init(cx);
533 });
534
535 for (path, tree) in trees {
536 fs.insert_tree(path, tree).await;
537 }
538
539 let project = Project::test(
540 fs.clone(),
541 worktree_roots.into_iter().map(Path::new),
542 app_cx,
543 )
544 .await;
545
546 let (workspace, cx) =
547 app_cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
548
549 let cwd = std::env::current_dir().expect("Failed to get working directory");
550 let terminal = project
551 .update(cx, |project: &mut Project, cx| {
552 project.create_terminal_shell(Some(cwd), cx)
553 })
554 .await
555 .expect("Failed to create a terminal");
556
557 let workspace_a = workspace.clone();
558 let (terminal_view, cx) = app_cx.add_window_view(|window, cx| {
559 TerminalView::new(
560 terminal,
561 workspace_a.downgrade(),
562 None,
563 project.downgrade(),
564 window,
565 cx,
566 )
567 });
568
569 async move |hovered_word: HoveredWord,
570 path_like_target: PathLikeTarget,
571 background_fs_checks: BackgroundFsChecks|
572 -> (Option<HoverTarget>, Option<OpenTarget>) {
573 let workspace_a = workspace.clone();
574 terminal_view
575 .update(cx, |_, cx| {
576 possible_hover_target(
577 &workspace_a.downgrade(),
578 hovered_word,
579 &path_like_target,
580 cx,
581 background_fs_checks,
582 )
583 })
584 .await;
585
586 let hover_target =
587 terminal_view.read_with(cx, |terminal_view, _| terminal_view.hover.clone());
588
589 let open_target = terminal_view
590 .update_in(cx, |terminal_view, window, cx| {
591 possibly_open_target(
592 &workspace.downgrade(),
593 terminal_view,
594 &path_like_target,
595 window,
596 cx,
597 background_fs_checks,
598 )
599 })
600 .await
601 .expect("Failed to possibly open target");
602
603 (hover_target, open_target)
604 }
605 }
606
607 async fn test_path_like_simple(
608 test_path_like: &mut impl AsyncFnMut(
609 HoveredWord,
610 PathLikeTarget,
611 BackgroundFsChecks,
612 ) -> (Option<HoverTarget>, Option<OpenTarget>),
613 maybe_path: &str,
614 tooltip: &str,
615 terminal_dir: Option<PathBuf>,
616 background_fs_checks: BackgroundFsChecks,
617 mut open_target_found_by: OpenTargetFoundBy,
618 file: &str,
619 line: u32,
620 ) {
621 let (hover_target, open_target) = test_path_like(
622 HoveredWord {
623 word: maybe_path.to_string(),
624 word_match: AlacPoint::default()..=AlacPoint::default(),
625 id: 0,
626 },
627 PathLikeTarget {
628 maybe_path: maybe_path.to_string(),
629 terminal_dir,
630 },
631 background_fs_checks,
632 )
633 .await;
634
635 let Some(hover_target) = hover_target else {
636 assert!(
637 hover_target.is_some(),
638 "Hover target should not be `None` at {file}:{line}:"
639 );
640 return;
641 };
642
643 assert_eq!(
644 hover_target.tooltip, tooltip,
645 "Tooltip mismatch at {file}:{line}:"
646 );
647 assert_eq!(
648 hover_target.hovered_word.word, maybe_path,
649 "Hovered word mismatch at {file}:{line}:"
650 );
651
652 let Some(open_target) = open_target else {
653 assert!(
654 open_target.is_some(),
655 "Open target should not be `None` at {file}:{line}:"
656 );
657 return;
658 };
659
660 assert_eq!(
661 open_target.path().path,
662 Path::new(tooltip),
663 "Open target path mismatch at {file}:{line}:"
664 );
665
666 if background_fs_checks == BackgroundFsChecks::Disabled
667 && open_target_found_by == OpenTargetFoundBy::FileSystemBackground
668 {
669 open_target_found_by = OpenTargetFoundBy::WorktreeScan;
670 }
671
672 assert_eq!(
673 open_target.found_by(),
674 open_target_found_by,
675 "Open target found by mismatch at {file}:{line}:"
676 );
677 }
678
679 macro_rules! none_or_some_pathbuf {
680 (None) => {
681 None
682 };
683 ($cwd:literal) => {
684 Some($crate::PathBuf::from(path!($cwd)))
685 };
686 }
687
688 macro_rules! test_path_like {
689 (
690 $test_path_like:expr,
691 $maybe_path:literal,
692 $tooltip:literal,
693 $cwd:tt,
694 $found_by:expr
695 ) => {{
696 test_path_like!(
697 $test_path_like,
698 $maybe_path,
699 $tooltip,
700 $cwd,
701 BackgroundFsChecks::Enabled,
702 $found_by
703 );
704 test_path_like!(
705 $test_path_like,
706 $maybe_path,
707 $tooltip,
708 $cwd,
709 BackgroundFsChecks::Disabled,
710 $found_by
711 );
712 }};
713
714 (
715 $test_path_like:expr,
716 $maybe_path:literal,
717 $tooltip:literal,
718 $cwd:tt,
719 $background_fs_checks:path,
720 $found_by:expr
721 ) => {
722 test_path_like_simple(
723 &mut $test_path_like,
724 path!($maybe_path),
725 path!($tooltip),
726 none_or_some_pathbuf!($cwd),
727 $background_fs_checks,
728 $found_by,
729 std::file!(),
730 std::line!(),
731 )
732 .await
733 };
734 }
735
736 // Note the arms of `test`, `test_local`, and `test_remote` should be collapsed once macro
737 // metavariable expressions (#![feature(macro_metavar_expr)]) are stabilized.
738 // See https://github.com/rust-lang/rust/issues/83527
739 #[doc = "test_path_likes!(<cx>, <trees>, <worktrees>, { $(<tests>;)+ })"]
740 macro_rules! test_path_likes {
741 ($cx:expr, $trees:expr, $worktrees:expr, { $($tests:expr;)+ }) => { {
742 let mut test_path_like = init_test($cx, $trees, $worktrees).await;
743 #[doc ="test!(<hovered maybe_path>, <expected tooltip>, <terminal cwd> "]
744 #[doc ="\\[, found by \\])"]
745 #[allow(unused_macros)]
746 macro_rules! test {
747 ($maybe_path:literal, $tooltip:literal, $cwd:tt) => {
748 test_path_like!(
749 test_path_like,
750 $maybe_path,
751 $tooltip,
752 $cwd,
753 OpenTargetFoundBy::WorktreeExact
754 )
755 };
756 ($maybe_path:literal, $tooltip:literal, $cwd:tt, $found_by:ident) => {
757 test_path_like!(
758 test_path_like,
759 $maybe_path,
760 $tooltip,
761 $cwd,
762 OpenTargetFoundBy::$found_by
763 )
764 }
765 }
766 #[doc ="test_local!(<hovered maybe_path>, <expected tooltip>, <terminal cwd> "]
767 #[doc ="\\[, found by \\])"]
768 #[allow(unused_macros)]
769 macro_rules! test_local {
770 ($maybe_path:literal, $tooltip:literal, $cwd:tt) => {
771 test_path_like!(
772 test_path_like,
773 $maybe_path,
774 $tooltip,
775 $cwd,
776 BackgroundFsChecks::Enabled,
777 OpenTargetFoundBy::WorktreeExact
778 )
779 };
780 ($maybe_path:literal, $tooltip:literal, $cwd:tt, $found_by:ident) => {
781 test_path_like!(
782 test_path_like,
783 $maybe_path,
784 $tooltip,
785 $cwd,
786 BackgroundFsChecks::Enabled,
787 OpenTargetFoundBy::$found_by
788 )
789 }
790 }
791 #[doc ="test_remote!(<hovered maybe_path>, <expected tooltip>, <terminal cwd> "]
792 #[doc ="\\[, found by \\])"]
793 #[allow(unused_macros)]
794 macro_rules! test_remote {
795 ($maybe_path:literal, $tooltip:literal, $cwd:tt) => {
796 test_path_like!(
797 test_path_like,
798 $maybe_path,
799 $tooltip,
800 $cwd,
801 BackgroundFsChecks::Disabled,
802 OpenTargetFoundBy::WorktreeExact
803 )
804 };
805 ($maybe_path:literal, $tooltip:literal, $cwd:tt, $found_by:ident) => {
806 test_path_like!(
807 test_path_like,
808 $maybe_path,
809 $tooltip,
810 $cwd,
811 BackgroundFsChecks::Disabled,
812 OpenTargetFoundBy::$found_by
813 )
814 }
815 }
816 $($tests);+
817 } }
818 }
819
820 #[gpui::test]
821 async fn one_folder_worktree(cx: &mut TestAppContext) {
822 test_path_likes!(
823 cx,
824 vec![(
825 path!("/test"),
826 json!({
827 "lib.rs": "",
828 "test.rs": "",
829 }),
830 )],
831 vec![path!("/test")],
832 {
833 test!("lib.rs", "/test/lib.rs", None);
834 test!("/test/lib.rs", "/test/lib.rs", None);
835 test!("test.rs", "/test/test.rs", None);
836 test!("/test/test.rs", "/test/test.rs", None);
837 }
838 )
839 }
840
841 #[gpui::test]
842 async fn mixed_worktrees(cx: &mut TestAppContext) {
843 test_path_likes!(
844 cx,
845 vec![
846 (
847 path!("/"),
848 json!({
849 "file.txt": "",
850 }),
851 ),
852 (
853 path!("/test"),
854 json!({
855 "lib.rs": "",
856 "test.rs": "",
857 "file.txt": "",
858 }),
859 ),
860 ],
861 vec![path!("/file.txt"), path!("/test")],
862 {
863 test!("file.txt", "/file.txt", "/");
864 test!("/file.txt", "/file.txt", "/");
865
866 test!("lib.rs", "/test/lib.rs", "/test");
867 test!("test.rs", "/test/test.rs", "/test");
868 test!("file.txt", "/test/file.txt", "/test");
869
870 test!("/test/lib.rs", "/test/lib.rs", "/test");
871 test!("/test/test.rs", "/test/test.rs", "/test");
872 test!("/test/file.txt", "/test/file.txt", "/test");
873 }
874 )
875 }
876
877 #[gpui::test]
878 async fn worktree_file_preferred(cx: &mut TestAppContext) {
879 test_path_likes!(
880 cx,
881 vec![
882 (
883 path!("/"),
884 json!({
885 "file.txt": "",
886 }),
887 ),
888 (
889 path!("/test"),
890 json!({
891 "file.txt": "",
892 }),
893 ),
894 ],
895 vec![path!("/test")],
896 {
897 test!("file.txt", "/test/file.txt", "/test");
898 }
899 )
900 }
901
902 mod issues {
903 use super::*;
904
905 // https://github.com/zed-industries/zed/issues/28407
906 #[gpui::test]
907 async fn issue_28407_siblings(cx: &mut TestAppContext) {
908 test_path_likes!(
909 cx,
910 vec![(
911 path!("/dir1"),
912 json!({
913 "dir 2": {
914 "C.py": ""
915 },
916 "dir 3": {
917 "C.py": ""
918 },
919 }),
920 )],
921 vec![path!("/dir1")],
922 {
923 test!("C.py", "/dir1/dir 2/C.py", "/dir1", WorktreeScan);
924 test!("C.py", "/dir1/dir 2/C.py", "/dir1/dir 2");
925 test!("C.py", "/dir1/dir 3/C.py", "/dir1/dir 3");
926 }
927 )
928 }
929
930 // https://github.com/zed-industries/zed/issues/28407
931 // See https://github.com/zed-industries/zed/issues/34027
932 // See https://github.com/zed-industries/zed/issues/33498
933 #[gpui::test]
934 async fn issue_28407_nesting(cx: &mut TestAppContext) {
935 test_path_likes!(
936 cx,
937 vec![(
938 path!("/project"),
939 json!({
940 "lib": {
941 "src": {
942 "main.rs": "",
943 "only_in_lib.rs": ""
944 },
945 },
946 "src": {
947 "main.rs": ""
948 },
949 }),
950 )],
951 vec![path!("/project")],
952 {
953 test!("main.rs", "/project/src/main.rs", "/project/src");
954 test!("main.rs", "/project/lib/src/main.rs", "/project/lib/src");
955
956 test!("src/main.rs", "/project/src/main.rs", "/project");
957 test!("src/main.rs", "/project/src/main.rs", "/project/src");
958 test!("src/main.rs", "/project/lib/src/main.rs", "/project/lib");
959
960 test!("lib/src/main.rs", "/project/lib/src/main.rs", "/project");
961 test!(
962 "lib/src/main.rs",
963 "/project/lib/src/main.rs",
964 "/project/src"
965 );
966 test!(
967 "lib/src/main.rs",
968 "/project/lib/src/main.rs",
969 "/project/lib"
970 );
971 test!(
972 "lib/src/main.rs",
973 "/project/lib/src/main.rs",
974 "/project/lib/src"
975 );
976 test!(
977 "src/only_in_lib.rs",
978 "/project/lib/src/only_in_lib.rs",
979 "/project/lib/src",
980 WorktreeScan
981 );
982 }
983 )
984 }
985
986 // https://github.com/zed-industries/zed/issues/28339
987 // Note: These could all be found by WorktreeExact if we used
988 // `fs::normalize_path(&maybe_path)`
989 #[gpui::test]
990 async fn issue_28339(cx: &mut TestAppContext) {
991 test_path_likes!(
992 cx,
993 vec![(
994 path!("/tmp"),
995 json!({
996 "issue28339": {
997 "foo": {
998 "bar.txt": ""
999 },
1000 },
1001 }),
1002 )],
1003 vec![path!("/tmp")],
1004 {
1005 test_local!(
1006 "foo/./bar.txt",
1007 "/tmp/issue28339/foo/bar.txt",
1008 "/tmp/issue28339"
1009 );
1010 test_local!(
1011 "foo/../foo/bar.txt",
1012 "/tmp/issue28339/foo/bar.txt",
1013 "/tmp/issue28339",
1014 FileSystemBackground
1015 );
1016 test_local!(
1017 "foo/..///foo/bar.txt",
1018 "/tmp/issue28339/foo/bar.txt",
1019 "/tmp/issue28339",
1020 FileSystemBackground
1021 );
1022 test_local!(
1023 "issue28339/../issue28339/foo/../foo/bar.txt",
1024 "/tmp/issue28339/foo/bar.txt",
1025 "/tmp/issue28339",
1026 FileSystemBackground
1027 );
1028 test_local!(
1029 "./bar.txt",
1030 "/tmp/issue28339/foo/bar.txt",
1031 "/tmp/issue28339/foo"
1032 );
1033 test_local!(
1034 "../foo/bar.txt",
1035 "/tmp/issue28339/foo/bar.txt",
1036 "/tmp/issue28339/foo",
1037 FileSystemBackground
1038 );
1039 }
1040 )
1041 }
1042
1043 // https://github.com/zed-industries/zed/issues/28339
1044 // Note: These could all be found by WorktreeExact if we used
1045 // `fs::normalize_path(&maybe_path)`
1046 #[gpui::test]
1047 #[should_panic(expected = "Hover target should not be `None`")]
1048 async fn issue_28339_remote(cx: &mut TestAppContext) {
1049 test_path_likes!(
1050 cx,
1051 vec![(
1052 path!("/tmp"),
1053 json!({
1054 "issue28339": {
1055 "foo": {
1056 "bar.txt": ""
1057 },
1058 },
1059 }),
1060 )],
1061 vec![path!("/tmp")],
1062 {
1063 test_remote!(
1064 "foo/./bar.txt",
1065 "/tmp/issue28339/foo/bar.txt",
1066 "/tmp/issue28339"
1067 );
1068 test_remote!(
1069 "foo/../foo/bar.txt",
1070 "/tmp/issue28339/foo/bar.txt",
1071 "/tmp/issue28339"
1072 );
1073 test_remote!(
1074 "foo/..///foo/bar.txt",
1075 "/tmp/issue28339/foo/bar.txt",
1076 "/tmp/issue28339"
1077 );
1078 test_remote!(
1079 "issue28339/../issue28339/foo/../foo/bar.txt",
1080 "/tmp/issue28339/foo/bar.txt",
1081 "/tmp/issue28339"
1082 );
1083 test_remote!(
1084 "./bar.txt",
1085 "/tmp/issue28339/foo/bar.txt",
1086 "/tmp/issue28339/foo"
1087 );
1088 test_remote!(
1089 "../foo/bar.txt",
1090 "/tmp/issue28339/foo/bar.txt",
1091 "/tmp/issue28339/foo"
1092 );
1093 }
1094 )
1095 }
1096
1097 // https://github.com/zed-industries/zed/issues/34027
1098 #[gpui::test]
1099 async fn issue_34027(cx: &mut TestAppContext) {
1100 test_path_likes!(
1101 cx,
1102 vec![(
1103 path!("/tmp/issue34027"),
1104 json!({
1105 "test.txt": "",
1106 "foo": {
1107 "test.txt": "",
1108 }
1109 }),
1110 ),],
1111 vec![path!("/tmp/issue34027")],
1112 {
1113 test!("test.txt", "/tmp/issue34027/test.txt", "/tmp/issue34027");
1114 test!(
1115 "test.txt",
1116 "/tmp/issue34027/foo/test.txt",
1117 "/tmp/issue34027/foo"
1118 );
1119 }
1120 )
1121 }
1122
1123 // https://github.com/zed-industries/zed/issues/34027
1124 #[gpui::test]
1125 async fn issue_34027_siblings(cx: &mut TestAppContext) {
1126 test_path_likes!(
1127 cx,
1128 vec![(
1129 path!("/test"),
1130 json!({
1131 "sub1": {
1132 "file.txt": "",
1133 },
1134 "sub2": {
1135 "file.txt": "",
1136 }
1137 }),
1138 ),],
1139 vec![path!("/test")],
1140 {
1141 test!("file.txt", "/test/sub1/file.txt", "/test/sub1");
1142 test!("file.txt", "/test/sub2/file.txt", "/test/sub2");
1143 test!("sub1/file.txt", "/test/sub1/file.txt", "/test/sub1");
1144 test!("sub2/file.txt", "/test/sub2/file.txt", "/test/sub2");
1145 test!("sub1/file.txt", "/test/sub1/file.txt", "/test/sub2");
1146 test!("sub2/file.txt", "/test/sub2/file.txt", "/test/sub1");
1147 }
1148 )
1149 }
1150
1151 // https://github.com/zed-industries/zed/issues/34027
1152 #[gpui::test]
1153 async fn issue_34027_nesting(cx: &mut TestAppContext) {
1154 test_path_likes!(
1155 cx,
1156 vec![(
1157 path!("/test"),
1158 json!({
1159 "sub1": {
1160 "file.txt": "",
1161 "subsub1": {
1162 "file.txt": "",
1163 }
1164 },
1165 "sub2": {
1166 "file.txt": "",
1167 "subsub1": {
1168 "file.txt": "",
1169 }
1170 }
1171 }),
1172 ),],
1173 vec![path!("/test")],
1174 {
1175 test!(
1176 "file.txt",
1177 "/test/sub1/subsub1/file.txt",
1178 "/test/sub1/subsub1"
1179 );
1180 test!(
1181 "file.txt",
1182 "/test/sub2/subsub1/file.txt",
1183 "/test/sub2/subsub1"
1184 );
1185 test!(
1186 "subsub1/file.txt",
1187 "/test/sub1/subsub1/file.txt",
1188 "/test",
1189 WorktreeScan
1190 );
1191 test!(
1192 "subsub1/file.txt",
1193 "/test/sub1/subsub1/file.txt",
1194 "/test",
1195 WorktreeScan
1196 );
1197 test!(
1198 "subsub1/file.txt",
1199 "/test/sub1/subsub1/file.txt",
1200 "/test/sub1"
1201 );
1202 test!(
1203 "subsub1/file.txt",
1204 "/test/sub2/subsub1/file.txt",
1205 "/test/sub2"
1206 );
1207 test!(
1208 "subsub1/file.txt",
1209 "/test/sub1/subsub1/file.txt",
1210 "/test/sub1/subsub1",
1211 WorktreeScan
1212 );
1213 }
1214 )
1215 }
1216
1217 // https://github.com/zed-industries/zed/issues/34027
1218 #[gpui::test]
1219 async fn issue_34027_non_worktree_local_file(cx: &mut TestAppContext) {
1220 test_path_likes!(
1221 cx,
1222 vec![
1223 (
1224 path!("/"),
1225 json!({
1226 "file.txt": "",
1227 }),
1228 ),
1229 (
1230 path!("/test"),
1231 json!({
1232 "file.txt": "",
1233 }),
1234 ),
1235 ],
1236 vec![path!("/test")],
1237 {
1238 // Note: Opening a non-worktree file adds that file as a single file worktree.
1239 test_local!("file.txt", "/file.txt", "/", FileSystemBackground);
1240 }
1241 )
1242 }
1243
1244 // https://github.com/zed-industries/zed/issues/34027
1245 #[gpui::test]
1246 async fn issue_34027_non_worktree_remote_file(cx: &mut TestAppContext) {
1247 test_path_likes!(
1248 cx,
1249 vec![
1250 (
1251 path!("/"),
1252 json!({
1253 "file.txt": "",
1254 }),
1255 ),
1256 (
1257 path!("/test"),
1258 json!({
1259 "file.txt": "",
1260 }),
1261 ),
1262 ],
1263 vec![path!("/test")],
1264 {
1265 // Note: Opening a non-worktree file adds that file as a single file worktree.
1266 test_remote!("file.txt", "/test/file.txt", "/");
1267 test_remote!("/test/file.txt", "/test/file.txt", "/");
1268 }
1269 )
1270 }
1271
1272 // See https://github.com/zed-industries/zed/issues/34027
1273 #[gpui::test]
1274 #[should_panic(expected = "Tooltip mismatch")]
1275 async fn issue_34027_gaps(cx: &mut TestAppContext) {
1276 test_path_likes!(
1277 cx,
1278 vec![(
1279 path!("/project"),
1280 json!({
1281 "lib": {
1282 "src": {
1283 "main.rs": ""
1284 },
1285 },
1286 "src": {
1287 "main.rs": ""
1288 },
1289 }),
1290 )],
1291 vec![path!("/project")],
1292 {
1293 test!("main.rs", "/project/src/main.rs", "/project");
1294 test!("main.rs", "/project/lib/src/main.rs", "/project/lib");
1295 }
1296 )
1297 }
1298
1299 // See https://github.com/zed-industries/zed/issues/34027
1300 #[gpui::test]
1301 #[should_panic(expected = "Tooltip mismatch")]
1302 async fn issue_34027_overlap(cx: &mut TestAppContext) {
1303 test_path_likes!(
1304 cx,
1305 vec![(
1306 path!("/project"),
1307 json!({
1308 "lib": {
1309 "src": {
1310 "main.rs": ""
1311 },
1312 },
1313 "src": {
1314 "main.rs": ""
1315 },
1316 }),
1317 )],
1318 vec![path!("/project")],
1319 {
1320 // Finds "/project/src/main.rs"
1321 test!(
1322 "src/main.rs",
1323 "/project/lib/src/main.rs",
1324 "/project/lib/src"
1325 );
1326 }
1327 )
1328 }
1329 }
1330}