dev_container_suggest.rs

  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}