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 terminal = project
550 .update(cx, |project: &mut Project, cx| {
551 project.create_terminal_shell(None, cx)
552 })
553 .await
554 .expect("Failed to create a terminal");
555
556 let workspace_a = workspace.clone();
557 let (terminal_view, cx) = app_cx.add_window_view(|window, cx| {
558 TerminalView::new(
559 terminal,
560 workspace_a.downgrade(),
561 None,
562 project.downgrade(),
563 window,
564 cx,
565 )
566 });
567
568 async move |hovered_word: HoveredWord,
569 path_like_target: PathLikeTarget,
570 background_fs_checks: BackgroundFsChecks|
571 -> (Option<HoverTarget>, Option<OpenTarget>) {
572 let workspace_a = workspace.clone();
573 terminal_view
574 .update(cx, |_, cx| {
575 possible_hover_target(
576 &workspace_a.downgrade(),
577 hovered_word,
578 &path_like_target,
579 cx,
580 background_fs_checks,
581 )
582 })
583 .await;
584
585 let hover_target =
586 terminal_view.read_with(cx, |terminal_view, _| terminal_view.hover.clone());
587
588 let open_target = terminal_view
589 .update_in(cx, |terminal_view, window, cx| {
590 possibly_open_target(
591 &workspace.downgrade(),
592 terminal_view,
593 &path_like_target,
594 window,
595 cx,
596 background_fs_checks,
597 )
598 })
599 .await
600 .expect("Failed to possibly open target");
601
602 (hover_target, open_target)
603 }
604 }
605
606 async fn test_path_like_simple(
607 test_path_like: &mut impl AsyncFnMut(
608 HoveredWord,
609 PathLikeTarget,
610 BackgroundFsChecks,
611 ) -> (Option<HoverTarget>, Option<OpenTarget>),
612 maybe_path: &str,
613 tooltip: &str,
614 terminal_dir: Option<PathBuf>,
615 background_fs_checks: BackgroundFsChecks,
616 mut open_target_found_by: OpenTargetFoundBy,
617 file: &str,
618 line: u32,
619 ) {
620 let (hover_target, open_target) = test_path_like(
621 HoveredWord {
622 word: maybe_path.to_string(),
623 word_match: AlacPoint::default()..=AlacPoint::default(),
624 id: 0,
625 },
626 PathLikeTarget {
627 maybe_path: maybe_path.to_string(),
628 terminal_dir,
629 },
630 background_fs_checks,
631 )
632 .await;
633
634 let Some(hover_target) = hover_target else {
635 assert!(
636 hover_target.is_some(),
637 "Hover target should not be `None` at {file}:{line}:"
638 );
639 return;
640 };
641
642 assert_eq!(
643 hover_target.tooltip, tooltip,
644 "Tooltip mismatch at {file}:{line}:"
645 );
646 assert_eq!(
647 hover_target.hovered_word.word, maybe_path,
648 "Hovered word mismatch at {file}:{line}:"
649 );
650
651 let Some(open_target) = open_target else {
652 assert!(
653 open_target.is_some(),
654 "Open target should not be `None` at {file}:{line}:"
655 );
656 return;
657 };
658
659 assert_eq!(
660 open_target.path().path,
661 Path::new(tooltip),
662 "Open target path mismatch at {file}:{line}:"
663 );
664
665 if background_fs_checks == BackgroundFsChecks::Disabled
666 && open_target_found_by == OpenTargetFoundBy::FileSystemBackground
667 {
668 open_target_found_by = OpenTargetFoundBy::WorktreeScan;
669 }
670
671 assert_eq!(
672 open_target.found_by(),
673 open_target_found_by,
674 "Open target found by mismatch at {file}:{line}:"
675 );
676 }
677
678 macro_rules! none_or_some_pathbuf {
679 (None) => {
680 None
681 };
682 ($cwd:literal) => {
683 Some($crate::PathBuf::from(path!($cwd)))
684 };
685 }
686
687 macro_rules! test_path_like {
688 (
689 $test_path_like:expr,
690 $maybe_path:literal,
691 $tooltip:literal,
692 $cwd:tt,
693 $found_by:expr
694 ) => {{
695 test_path_like!(
696 $test_path_like,
697 $maybe_path,
698 $tooltip,
699 $cwd,
700 BackgroundFsChecks::Enabled,
701 $found_by
702 );
703 test_path_like!(
704 $test_path_like,
705 $maybe_path,
706 $tooltip,
707 $cwd,
708 BackgroundFsChecks::Disabled,
709 $found_by
710 );
711 }};
712
713 (
714 $test_path_like:expr,
715 $maybe_path:literal,
716 $tooltip:literal,
717 $cwd:tt,
718 $background_fs_checks:path,
719 $found_by:expr
720 ) => {
721 test_path_like_simple(
722 &mut $test_path_like,
723 path!($maybe_path),
724 path!($tooltip),
725 none_or_some_pathbuf!($cwd),
726 $background_fs_checks,
727 $found_by,
728 std::file!(),
729 std::line!(),
730 )
731 .await
732 };
733 }
734
735 // Note the arms of `test`, `test_local`, and `test_remote` should be collapsed once macro
736 // metavariable expressions (#![feature(macro_metavar_expr)]) are stabilized.
737 // See https://github.com/rust-lang/rust/issues/83527
738 #[doc = "test_path_likes!(<cx>, <trees>, <worktrees>, { $(<tests>;)+ })"]
739 macro_rules! test_path_likes {
740 ($cx:expr, $trees:expr, $worktrees:expr, { $($tests:expr;)+ }) => { {
741 let mut test_path_like = init_test($cx, $trees, $worktrees).await;
742 #[doc ="test!(<hovered maybe_path>, <expected tooltip>, <terminal cwd> "]
743 #[doc ="\\[, found by \\])"]
744 #[allow(unused_macros)]
745 macro_rules! test {
746 ($maybe_path:literal, $tooltip:literal, $cwd:tt) => {
747 test_path_like!(
748 test_path_like,
749 $maybe_path,
750 $tooltip,
751 $cwd,
752 OpenTargetFoundBy::WorktreeExact
753 )
754 };
755 ($maybe_path:literal, $tooltip:literal, $cwd:tt, $found_by:ident) => {
756 test_path_like!(
757 test_path_like,
758 $maybe_path,
759 $tooltip,
760 $cwd,
761 OpenTargetFoundBy::$found_by
762 )
763 }
764 }
765 #[doc ="test_local!(<hovered maybe_path>, <expected tooltip>, <terminal cwd> "]
766 #[doc ="\\[, found by \\])"]
767 #[allow(unused_macros)]
768 macro_rules! test_local {
769 ($maybe_path:literal, $tooltip:literal, $cwd:tt) => {
770 test_path_like!(
771 test_path_like,
772 $maybe_path,
773 $tooltip,
774 $cwd,
775 BackgroundFsChecks::Enabled,
776 OpenTargetFoundBy::WorktreeExact
777 )
778 };
779 ($maybe_path:literal, $tooltip:literal, $cwd:tt, $found_by:ident) => {
780 test_path_like!(
781 test_path_like,
782 $maybe_path,
783 $tooltip,
784 $cwd,
785 BackgroundFsChecks::Enabled,
786 OpenTargetFoundBy::$found_by
787 )
788 }
789 }
790 #[doc ="test_remote!(<hovered maybe_path>, <expected tooltip>, <terminal cwd> "]
791 #[doc ="\\[, found by \\])"]
792 #[allow(unused_macros)]
793 macro_rules! test_remote {
794 ($maybe_path:literal, $tooltip:literal, $cwd:tt) => {
795 test_path_like!(
796 test_path_like,
797 $maybe_path,
798 $tooltip,
799 $cwd,
800 BackgroundFsChecks::Disabled,
801 OpenTargetFoundBy::WorktreeExact
802 )
803 };
804 ($maybe_path:literal, $tooltip:literal, $cwd:tt, $found_by:ident) => {
805 test_path_like!(
806 test_path_like,
807 $maybe_path,
808 $tooltip,
809 $cwd,
810 BackgroundFsChecks::Disabled,
811 OpenTargetFoundBy::$found_by
812 )
813 }
814 }
815 $($tests);+
816 } }
817 }
818
819 #[gpui::test]
820 async fn one_folder_worktree(cx: &mut TestAppContext) {
821 test_path_likes!(
822 cx,
823 vec![(
824 path!("/test"),
825 json!({
826 "lib.rs": "",
827 "test.rs": "",
828 }),
829 )],
830 vec![path!("/test")],
831 {
832 test!("lib.rs", "/test/lib.rs", None);
833 test!("/test/lib.rs", "/test/lib.rs", None);
834 test!("test.rs", "/test/test.rs", None);
835 test!("/test/test.rs", "/test/test.rs", None);
836 }
837 )
838 }
839
840 #[gpui::test]
841 async fn mixed_worktrees(cx: &mut TestAppContext) {
842 test_path_likes!(
843 cx,
844 vec![
845 (
846 path!("/"),
847 json!({
848 "file.txt": "",
849 }),
850 ),
851 (
852 path!("/test"),
853 json!({
854 "lib.rs": "",
855 "test.rs": "",
856 "file.txt": "",
857 }),
858 ),
859 ],
860 vec![path!("/file.txt"), path!("/test")],
861 {
862 test!("file.txt", "/file.txt", "/");
863 test!("/file.txt", "/file.txt", "/");
864
865 test!("lib.rs", "/test/lib.rs", "/test");
866 test!("test.rs", "/test/test.rs", "/test");
867 test!("file.txt", "/test/file.txt", "/test");
868
869 test!("/test/lib.rs", "/test/lib.rs", "/test");
870 test!("/test/test.rs", "/test/test.rs", "/test");
871 test!("/test/file.txt", "/test/file.txt", "/test");
872 }
873 )
874 }
875
876 #[gpui::test]
877 async fn worktree_file_preferred(cx: &mut TestAppContext) {
878 test_path_likes!(
879 cx,
880 vec![
881 (
882 path!("/"),
883 json!({
884 "file.txt": "",
885 }),
886 ),
887 (
888 path!("/test"),
889 json!({
890 "file.txt": "",
891 }),
892 ),
893 ],
894 vec![path!("/test")],
895 {
896 test!("file.txt", "/test/file.txt", "/test");
897 }
898 )
899 }
900
901 mod issues {
902 use super::*;
903
904 // https://github.com/zed-industries/zed/issues/28407
905 #[gpui::test]
906 async fn issue_28407_siblings(cx: &mut TestAppContext) {
907 test_path_likes!(
908 cx,
909 vec![(
910 path!("/dir1"),
911 json!({
912 "dir 2": {
913 "C.py": ""
914 },
915 "dir 3": {
916 "C.py": ""
917 },
918 }),
919 )],
920 vec![path!("/dir1")],
921 {
922 test!("C.py", "/dir1/dir 2/C.py", "/dir1", WorktreeScan);
923 test!("C.py", "/dir1/dir 2/C.py", "/dir1/dir 2");
924 test!("C.py", "/dir1/dir 3/C.py", "/dir1/dir 3");
925 }
926 )
927 }
928
929 // https://github.com/zed-industries/zed/issues/28407
930 // See https://github.com/zed-industries/zed/issues/34027
931 // See https://github.com/zed-industries/zed/issues/33498
932 #[gpui::test]
933 async fn issue_28407_nesting(cx: &mut TestAppContext) {
934 test_path_likes!(
935 cx,
936 vec![(
937 path!("/project"),
938 json!({
939 "lib": {
940 "src": {
941 "main.rs": "",
942 "only_in_lib.rs": ""
943 },
944 },
945 "src": {
946 "main.rs": ""
947 },
948 }),
949 )],
950 vec![path!("/project")],
951 {
952 test!("main.rs", "/project/src/main.rs", "/project/src");
953 test!("main.rs", "/project/lib/src/main.rs", "/project/lib/src");
954
955 test!("src/main.rs", "/project/src/main.rs", "/project");
956 test!("src/main.rs", "/project/src/main.rs", "/project/src");
957 test!("src/main.rs", "/project/lib/src/main.rs", "/project/lib");
958
959 test!("lib/src/main.rs", "/project/lib/src/main.rs", "/project");
960 test!(
961 "lib/src/main.rs",
962 "/project/lib/src/main.rs",
963 "/project/src"
964 );
965 test!(
966 "lib/src/main.rs",
967 "/project/lib/src/main.rs",
968 "/project/lib"
969 );
970 test!(
971 "lib/src/main.rs",
972 "/project/lib/src/main.rs",
973 "/project/lib/src"
974 );
975 test!(
976 "src/only_in_lib.rs",
977 "/project/lib/src/only_in_lib.rs",
978 "/project/lib/src",
979 WorktreeScan
980 );
981 }
982 )
983 }
984
985 // https://github.com/zed-industries/zed/issues/28339
986 // Note: These could all be found by WorktreeExact if we used
987 // `fs::normalize_path(&maybe_path)`
988 #[gpui::test]
989 async fn issue_28339(cx: &mut TestAppContext) {
990 test_path_likes!(
991 cx,
992 vec![(
993 path!("/tmp"),
994 json!({
995 "issue28339": {
996 "foo": {
997 "bar.txt": ""
998 },
999 },
1000 }),
1001 )],
1002 vec![path!("/tmp")],
1003 {
1004 test_local!(
1005 "foo/./bar.txt",
1006 "/tmp/issue28339/foo/bar.txt",
1007 "/tmp/issue28339"
1008 );
1009 test_local!(
1010 "foo/../foo/bar.txt",
1011 "/tmp/issue28339/foo/bar.txt",
1012 "/tmp/issue28339",
1013 FileSystemBackground
1014 );
1015 test_local!(
1016 "foo/..///foo/bar.txt",
1017 "/tmp/issue28339/foo/bar.txt",
1018 "/tmp/issue28339",
1019 FileSystemBackground
1020 );
1021 test_local!(
1022 "issue28339/../issue28339/foo/../foo/bar.txt",
1023 "/tmp/issue28339/foo/bar.txt",
1024 "/tmp/issue28339",
1025 FileSystemBackground
1026 );
1027 test_local!(
1028 "./bar.txt",
1029 "/tmp/issue28339/foo/bar.txt",
1030 "/tmp/issue28339/foo"
1031 );
1032 test_local!(
1033 "../foo/bar.txt",
1034 "/tmp/issue28339/foo/bar.txt",
1035 "/tmp/issue28339/foo",
1036 FileSystemBackground
1037 );
1038 }
1039 )
1040 }
1041
1042 // https://github.com/zed-industries/zed/issues/28339
1043 // Note: These could all be found by WorktreeExact if we used
1044 // `fs::normalize_path(&maybe_path)`
1045 #[gpui::test]
1046 #[should_panic(expected = "Hover target should not be `None`")]
1047 async fn issue_28339_remote(cx: &mut TestAppContext) {
1048 test_path_likes!(
1049 cx,
1050 vec![(
1051 path!("/tmp"),
1052 json!({
1053 "issue28339": {
1054 "foo": {
1055 "bar.txt": ""
1056 },
1057 },
1058 }),
1059 )],
1060 vec![path!("/tmp")],
1061 {
1062 test_remote!(
1063 "foo/./bar.txt",
1064 "/tmp/issue28339/foo/bar.txt",
1065 "/tmp/issue28339"
1066 );
1067 test_remote!(
1068 "foo/../foo/bar.txt",
1069 "/tmp/issue28339/foo/bar.txt",
1070 "/tmp/issue28339"
1071 );
1072 test_remote!(
1073 "foo/..///foo/bar.txt",
1074 "/tmp/issue28339/foo/bar.txt",
1075 "/tmp/issue28339"
1076 );
1077 test_remote!(
1078 "issue28339/../issue28339/foo/../foo/bar.txt",
1079 "/tmp/issue28339/foo/bar.txt",
1080 "/tmp/issue28339"
1081 );
1082 test_remote!(
1083 "./bar.txt",
1084 "/tmp/issue28339/foo/bar.txt",
1085 "/tmp/issue28339/foo"
1086 );
1087 test_remote!(
1088 "../foo/bar.txt",
1089 "/tmp/issue28339/foo/bar.txt",
1090 "/tmp/issue28339/foo"
1091 );
1092 }
1093 )
1094 }
1095
1096 // https://github.com/zed-industries/zed/issues/34027
1097 #[gpui::test]
1098 async fn issue_34027(cx: &mut TestAppContext) {
1099 test_path_likes!(
1100 cx,
1101 vec![(
1102 path!("/tmp/issue34027"),
1103 json!({
1104 "test.txt": "",
1105 "foo": {
1106 "test.txt": "",
1107 }
1108 }),
1109 ),],
1110 vec![path!("/tmp/issue34027")],
1111 {
1112 test!("test.txt", "/tmp/issue34027/test.txt", "/tmp/issue34027");
1113 test!(
1114 "test.txt",
1115 "/tmp/issue34027/foo/test.txt",
1116 "/tmp/issue34027/foo"
1117 );
1118 }
1119 )
1120 }
1121
1122 // https://github.com/zed-industries/zed/issues/34027
1123 #[gpui::test]
1124 async fn issue_34027_siblings(cx: &mut TestAppContext) {
1125 test_path_likes!(
1126 cx,
1127 vec![(
1128 path!("/test"),
1129 json!({
1130 "sub1": {
1131 "file.txt": "",
1132 },
1133 "sub2": {
1134 "file.txt": "",
1135 }
1136 }),
1137 ),],
1138 vec![path!("/test")],
1139 {
1140 test!("file.txt", "/test/sub1/file.txt", "/test/sub1");
1141 test!("file.txt", "/test/sub2/file.txt", "/test/sub2");
1142 test!("sub1/file.txt", "/test/sub1/file.txt", "/test/sub1");
1143 test!("sub2/file.txt", "/test/sub2/file.txt", "/test/sub2");
1144 test!("sub1/file.txt", "/test/sub1/file.txt", "/test/sub2");
1145 test!("sub2/file.txt", "/test/sub2/file.txt", "/test/sub1");
1146 }
1147 )
1148 }
1149
1150 // https://github.com/zed-industries/zed/issues/34027
1151 #[gpui::test]
1152 async fn issue_34027_nesting(cx: &mut TestAppContext) {
1153 test_path_likes!(
1154 cx,
1155 vec![(
1156 path!("/test"),
1157 json!({
1158 "sub1": {
1159 "file.txt": "",
1160 "subsub1": {
1161 "file.txt": "",
1162 }
1163 },
1164 "sub2": {
1165 "file.txt": "",
1166 "subsub1": {
1167 "file.txt": "",
1168 }
1169 }
1170 }),
1171 ),],
1172 vec![path!("/test")],
1173 {
1174 test!(
1175 "file.txt",
1176 "/test/sub1/subsub1/file.txt",
1177 "/test/sub1/subsub1"
1178 );
1179 test!(
1180 "file.txt",
1181 "/test/sub2/subsub1/file.txt",
1182 "/test/sub2/subsub1"
1183 );
1184 test!(
1185 "subsub1/file.txt",
1186 "/test/sub1/subsub1/file.txt",
1187 "/test",
1188 WorktreeScan
1189 );
1190 test!(
1191 "subsub1/file.txt",
1192 "/test/sub1/subsub1/file.txt",
1193 "/test",
1194 WorktreeScan
1195 );
1196 test!(
1197 "subsub1/file.txt",
1198 "/test/sub1/subsub1/file.txt",
1199 "/test/sub1"
1200 );
1201 test!(
1202 "subsub1/file.txt",
1203 "/test/sub2/subsub1/file.txt",
1204 "/test/sub2"
1205 );
1206 test!(
1207 "subsub1/file.txt",
1208 "/test/sub1/subsub1/file.txt",
1209 "/test/sub1/subsub1",
1210 WorktreeScan
1211 );
1212 }
1213 )
1214 }
1215
1216 // https://github.com/zed-industries/zed/issues/34027
1217 #[gpui::test]
1218 async fn issue_34027_non_worktree_local_file(cx: &mut TestAppContext) {
1219 test_path_likes!(
1220 cx,
1221 vec![
1222 (
1223 path!("/"),
1224 json!({
1225 "file.txt": "",
1226 }),
1227 ),
1228 (
1229 path!("/test"),
1230 json!({
1231 "file.txt": "",
1232 }),
1233 ),
1234 ],
1235 vec![path!("/test")],
1236 {
1237 // Note: Opening a non-worktree file adds that file as a single file worktree.
1238 test_local!("file.txt", "/file.txt", "/", FileSystemBackground);
1239 }
1240 )
1241 }
1242
1243 // https://github.com/zed-industries/zed/issues/34027
1244 #[gpui::test]
1245 async fn issue_34027_non_worktree_remote_file(cx: &mut TestAppContext) {
1246 test_path_likes!(
1247 cx,
1248 vec![
1249 (
1250 path!("/"),
1251 json!({
1252 "file.txt": "",
1253 }),
1254 ),
1255 (
1256 path!("/test"),
1257 json!({
1258 "file.txt": "",
1259 }),
1260 ),
1261 ],
1262 vec![path!("/test")],
1263 {
1264 // Note: Opening a non-worktree file adds that file as a single file worktree.
1265 test_remote!("file.txt", "/test/file.txt", "/");
1266 test_remote!("/test/file.txt", "/test/file.txt", "/");
1267 }
1268 )
1269 }
1270
1271 // See https://github.com/zed-industries/zed/issues/34027
1272 #[gpui::test]
1273 #[should_panic(expected = "Tooltip mismatch")]
1274 async fn issue_34027_gaps(cx: &mut TestAppContext) {
1275 test_path_likes!(
1276 cx,
1277 vec![(
1278 path!("/project"),
1279 json!({
1280 "lib": {
1281 "src": {
1282 "main.rs": ""
1283 },
1284 },
1285 "src": {
1286 "main.rs": ""
1287 },
1288 }),
1289 )],
1290 vec![path!("/project")],
1291 {
1292 test!("main.rs", "/project/src/main.rs", "/project");
1293 test!("main.rs", "/project/lib/src/main.rs", "/project/lib");
1294 }
1295 )
1296 }
1297
1298 // See https://github.com/zed-industries/zed/issues/34027
1299 #[gpui::test]
1300 #[should_panic(expected = "Tooltip mismatch")]
1301 async fn issue_34027_overlap(cx: &mut TestAppContext) {
1302 test_path_likes!(
1303 cx,
1304 vec![(
1305 path!("/project"),
1306 json!({
1307 "lib": {
1308 "src": {
1309 "main.rs": ""
1310 },
1311 },
1312 "src": {
1313 "main.rs": ""
1314 },
1315 }),
1316 )],
1317 vec![path!("/project")],
1318 {
1319 // Finds "/project/src/main.rs"
1320 test!(
1321 "src/main.rs",
1322 "/project/lib/src/main.rs",
1323 "/project/lib/src"
1324 );
1325 }
1326 )
1327 }
1328 }
1329}