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