Partially addresses #11473 MVP of dev containers with the following capabilities: - If in a project with `.devcontainer/devcontainer.json`, a pop-up notification will ask if you want to open the project in a dev container. This can be dismissed: <img width="1478" height="1191" alt="Screenshot 2025-12-08 at 3 15 23 PM" src="https://github.com/user-attachments/assets/ec2e20d6-28ec-4495-8f23-4c1d48a9ce78" /> - Similarly, if a `devcontainer.json` file is in the project, you can open a devcontainer (or go the devcontainer.json file for further editing) via the `open remote` modal: https://github.com/user-attachments/assets/61f2fdaa-2808-4efc-994c-7b444a92c0b1 *Limitations* This is a first release, and comes with some limitations: - Zed extensions are not managed in `devcontainer.json` yet. They will need to be installed either on host or in the container. Host + Container sync their extensions, so there is not currently a concept of what is installed in the container vs what is installed on host: they come from the same list of manifests - This implementation uses the [devcontainer CLI](https://github.com/devcontainers/cli) for its control plane. Hence, it does not yet support the `forwardPorts` directive. A single port can be opened with `appPort`. See reference in docs [here](https://github.com/devcontainers/cli/tree/main/example-usage#how-the-tool-examples-work) - Editing devcontainer.json does not automatically cause the dev container to be rebuilt. So if you add features, change images, etc, you will need to `docker kill` the existing dev container before proceeding. - Currently takes a hard dependency on `docker` being available in the user's `PATH`. Release Notes: - Added ability to Open a project in a DevContainer, provided a `.devcontainer/devcontainer.json` is present --------- Co-authored-by: Danilo Leal <daniloleal09@gmail.com> Co-authored-by: Danilo Leal <67129314+danilo-leal@users.noreply.github.com>
107 lines
3.3 KiB
Rust
107 lines
3.3 KiB
Rust
use db::kvp::KEY_VALUE_STORE;
|
|
use gpui::{SharedString, Window};
|
|
use project::{Project, WorktreeId};
|
|
use std::sync::LazyLock;
|
|
use ui::prelude::*;
|
|
use util::rel_path::RelPath;
|
|
use workspace::Workspace;
|
|
use workspace::notifications::NotificationId;
|
|
use workspace::notifications::simple_message_notification::MessageNotification;
|
|
use worktree::UpdatedEntriesSet;
|
|
|
|
const DEV_CONTAINER_SUGGEST_KEY: &str = "dev_container_suggest_dismissed";
|
|
|
|
fn devcontainer_path() -> &'static RelPath {
|
|
static PATH: LazyLock<&'static RelPath> =
|
|
LazyLock::new(|| RelPath::unix(".devcontainer").expect("valid path"));
|
|
*PATH
|
|
}
|
|
|
|
fn project_devcontainer_key(project_path: &str) -> String {
|
|
format!("{}_{}", DEV_CONTAINER_SUGGEST_KEY, project_path)
|
|
}
|
|
|
|
pub fn suggest_on_worktree_updated(
|
|
worktree_id: WorktreeId,
|
|
updated_entries: &UpdatedEntriesSet,
|
|
project: &gpui::Entity<Project>,
|
|
window: &mut Window,
|
|
cx: &mut Context<Workspace>,
|
|
) {
|
|
let devcontainer_updated = updated_entries
|
|
.iter()
|
|
.any(|(path, _, _)| path.as_ref() == devcontainer_path());
|
|
|
|
if !devcontainer_updated {
|
|
return;
|
|
}
|
|
|
|
let Some(worktree) = project.read(cx).worktree_for_id(worktree_id, cx) else {
|
|
return;
|
|
};
|
|
|
|
let worktree = worktree.read(cx);
|
|
|
|
if !worktree.is_local() {
|
|
return;
|
|
}
|
|
|
|
let has_devcontainer = worktree
|
|
.entry_for_path(devcontainer_path())
|
|
.is_some_and(|entry| entry.is_dir());
|
|
|
|
if !has_devcontainer {
|
|
return;
|
|
}
|
|
|
|
let abs_path = worktree.abs_path();
|
|
let project_path = abs_path.to_string_lossy().to_string();
|
|
let key_for_dismiss = project_devcontainer_key(&project_path);
|
|
|
|
let already_dismissed = KEY_VALUE_STORE
|
|
.read_kvp(&key_for_dismiss)
|
|
.ok()
|
|
.flatten()
|
|
.is_some();
|
|
|
|
if already_dismissed {
|
|
return;
|
|
}
|
|
|
|
cx.on_next_frame(window, move |workspace, _window, cx| {
|
|
struct DevContainerSuggestionNotification;
|
|
|
|
let notification_id = NotificationId::composite::<DevContainerSuggestionNotification>(
|
|
SharedString::from(project_path.clone()),
|
|
);
|
|
|
|
workspace.show_notification(notification_id, cx, |cx| {
|
|
cx.new(move |cx| {
|
|
MessageNotification::new(
|
|
"This project contains a Dev Container configuration file. Would you like to re-open it in a container?",
|
|
cx,
|
|
)
|
|
.primary_message("Yes, Open in Container")
|
|
.primary_icon(IconName::Check)
|
|
.primary_icon_color(Color::Success)
|
|
.primary_on_click({
|
|
move |window, cx| {
|
|
window.dispatch_action(Box::new(zed_actions::OpenDevContainer), cx);
|
|
}
|
|
})
|
|
.secondary_message("Don't Show Again")
|
|
.secondary_icon(IconName::Close)
|
|
.secondary_icon_color(Color::Error)
|
|
.secondary_on_click({
|
|
move |_window, cx| {
|
|
let key = key_for_dismiss.clone();
|
|
db::write_and_log(cx, move || {
|
|
KEY_VALUE_STORE.write_kvp(key, "dismissed".to_string())
|
|
});
|
|
}
|
|
})
|
|
})
|
|
});
|
|
});
|
|
}
|