dev_container_suggest.rs

  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}