dev_container_suggest.rs

  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}