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