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