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