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 workspace: &mut Workspace,
34 worktree_id: WorktreeId,
35 updated_entries: &UpdatedEntriesSet,
36 project: &gpui::Entity<Project>,
37 window: &mut Window,
38 cx: &mut Context<Workspace>,
39) {
40 let cli_auto_open = workspace.open_in_dev_container();
41
42 let devcontainer_updated = updated_entries.iter().any(|(path, _, _)| {
43 path.as_ref() == devcontainer_dir_path() || path.as_ref() == devcontainer_json_path()
44 });
45
46 if !devcontainer_updated && !cli_auto_open {
47 return;
48 }
49
50 let Some(worktree) = project.read(cx).worktree_for_id(worktree_id, cx) else {
51 return;
52 };
53
54 let worktree = worktree.read(cx);
55
56 if !worktree.is_local() {
57 return;
58 }
59
60 let has_configs = !find_configs_in_snapshot(worktree).is_empty();
61
62 if cli_auto_open {
63 workspace.set_open_in_dev_container(false);
64 let task = cx.spawn_in(window, async move |workspace, cx| {
65 let scans_complete =
66 workspace.update(cx, |workspace, cx| workspace.worktree_scans_complete(cx))?;
67 scans_complete.await;
68
69 workspace.update_in(cx, |workspace, window, cx| {
70 let has_configs = workspace
71 .project()
72 .read(cx)
73 .worktrees(cx)
74 .any(|wt| !find_configs_in_snapshot(wt.read(cx)).is_empty());
75 if has_configs {
76 cx.on_next_frame(window, move |_workspace, window, cx| {
77 window.dispatch_action(Box::new(zed_actions::OpenDevContainer), cx);
78 });
79 } else {
80 log::warn!("--dev-container: no devcontainer configuration found in project");
81 }
82 })
83 });
84 workspace.set_dev_container_task(task);
85 return;
86 }
87
88 if !has_configs {
89 return;
90 }
91
92 let abs_path = worktree.abs_path();
93 let project_path = abs_path.to_string_lossy().to_string();
94 let key_for_dismiss = project_devcontainer_key(&project_path);
95
96 let already_dismissed = KeyValueStore::global(cx)
97 .read_kvp(&key_for_dismiss)
98 .ok()
99 .flatten()
100 .is_some();
101
102 if already_dismissed {
103 return;
104 }
105
106 cx.on_next_frame(window, move |workspace, _window, cx| {
107 struct DevContainerSuggestionNotification;
108
109 let notification_id = NotificationId::composite::<DevContainerSuggestionNotification>(
110 SharedString::from(project_path.clone()),
111 );
112
113 workspace.show_notification(notification_id, cx, |cx| {
114 cx.new(move |cx| {
115 MessageNotification::new(
116 "This project contains a Dev Container configuration file. Would you like to re-open it in a container?",
117 cx,
118 )
119 .primary_message("Yes, Open in Container")
120 .primary_icon(IconName::Check)
121 .primary_icon_color(Color::Success)
122 .primary_on_click({
123 move |window, cx| {
124 window.dispatch_action(Box::new(zed_actions::OpenDevContainer), cx);
125 }
126 })
127 .secondary_message("Don't Show Again")
128 .secondary_icon(IconName::Close)
129 .secondary_icon_color(Color::Error)
130 .secondary_on_click({
131 move |_window, cx| {
132 let key = key_for_dismiss.clone();
133 let kvp = KeyValueStore::global(cx);
134 cx.background_spawn(async move {
135 kvp.write_kvp(key, "dismissed".to_string())
136 .await
137 .log_err();
138 })
139 .detach();
140 }
141 })
142 })
143 });
144 });
145}