1use db::kvp::KeyValueStore;
2use dev_container::find_configs_in_snapshot;
3use gpui::{SharedString, Window};
4use project::{Project, WorktreeId};
5use std::sync::LazyLock;
6use ui::prelude::*;
7use util::ResultExt;
8use util::rel_path::RelPath;
9use workspace::Workspace;
10use workspace::notifications::NotificationId;
11use workspace::notifications::simple_message_notification::MessageNotification;
12use worktree::UpdatedEntriesSet;
13
14const DEV_CONTAINER_SUGGEST_KEY: &str = "dev_container_suggest_dismissed";
15
16fn devcontainer_dir_path() -> &'static RelPath {
17 static PATH: LazyLock<&'static RelPath> =
18 LazyLock::new(|| RelPath::unix(".devcontainer").expect("valid path"));
19 *PATH
20}
21
22fn devcontainer_json_path() -> &'static RelPath {
23 static PATH: LazyLock<&'static RelPath> =
24 LazyLock::new(|| RelPath::unix(".devcontainer.json").expect("valid path"));
25 *PATH
26}
27
28fn project_devcontainer_key(project_path: &str) -> String {
29 format!("{}_{}", DEV_CONTAINER_SUGGEST_KEY, project_path)
30}
31
32pub fn suggest_on_worktree_updated(
33 worktree_id: WorktreeId,
34 updated_entries: &UpdatedEntriesSet,
35 project: &gpui::Entity<Project>,
36 window: &mut Window,
37 cx: &mut Context<Workspace>,
38) {
39 let devcontainer_updated = updated_entries.iter().any(|(path, _, _)| {
40 path.as_ref() == devcontainer_dir_path() || path.as_ref() == devcontainer_json_path()
41 });
42
43 if !devcontainer_updated {
44 return;
45 }
46
47 let Some(worktree) = project.read(cx).worktree_for_id(worktree_id, cx) else {
48 return;
49 };
50
51 let worktree = worktree.read(cx);
52
53 if !worktree.is_local() {
54 return;
55 }
56
57 if find_configs_in_snapshot(worktree).is_empty() {
58 return;
59 }
60
61 let abs_path = worktree.abs_path();
62 let project_path = abs_path.to_string_lossy().to_string();
63 let key_for_dismiss = project_devcontainer_key(&project_path);
64
65 let already_dismissed = KeyValueStore::global(cx)
66 .read_kvp(&key_for_dismiss)
67 .ok()
68 .flatten()
69 .is_some();
70
71 if already_dismissed {
72 return;
73 }
74
75 cx.on_next_frame(window, move |workspace, _window, cx| {
76 struct DevContainerSuggestionNotification;
77
78 let notification_id = NotificationId::composite::<DevContainerSuggestionNotification>(
79 SharedString::from(project_path.clone()),
80 );
81
82 workspace.show_notification(notification_id, cx, |cx| {
83 cx.new(move |cx| {
84 MessageNotification::new(
85 "This project contains a Dev Container configuration file. Would you like to re-open it in a container?",
86 cx,
87 )
88 .primary_message("Yes, Open in Container")
89 .primary_icon(IconName::Check)
90 .primary_icon_color(Color::Success)
91 .primary_on_click({
92 move |window, cx| {
93 window.dispatch_action(Box::new(zed_actions::OpenDevContainer), cx);
94 }
95 })
96 .secondary_message("Don't Show Again")
97 .secondary_icon(IconName::Close)
98 .secondary_icon_color(Color::Error)
99 .secondary_on_click({
100 move |_window, cx| {
101 let key = key_for_dismiss.clone();
102 let kvp = KeyValueStore::global(cx);
103 cx.background_spawn(async move {
104 kvp.write_kvp(key, "dismissed".to_string())
105 .await
106 .log_err();
107 })
108 .detach();
109 }
110 })
111 })
112 });
113 });
114}