Add detection of devcontainers in subfolders (#47411)

Release Notes:

- Add detection of devcontainers in subfolders

---------

Co-authored-by: KyleBarton <kjb@initialcapacity.io>
This commit is contained in:
Caio Piccirillo 2026-02-03 14:05:40 -03:00 committed by GitHub
parent 18a3b0c53a
commit 13a06e673b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 542 additions and 241 deletions

View file

@ -10,10 +10,29 @@ use node_runtime::NodeRuntime;
use serde::Deserialize;
use settings::{DevContainerConnection, Settings as _};
use smol::{fs, process::Command};
use util::rel_path::RelPath;
use workspace::Workspace;
use crate::{DevContainerFeature, DevContainerSettings, DevContainerTemplate};
/// Represents a discovered devcontainer configuration
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DevContainerConfig {
/// Display name for the configuration (subfolder name or "default")
pub name: String,
/// Relative path to the devcontainer.json file from the project root
pub config_path: PathBuf,
}
impl DevContainerConfig {
pub fn default_config() -> Self {
Self {
name: "default".to_string(),
config_path: PathBuf::from(".devcontainer/devcontainer.json"),
}
}
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct DevContainerUp {
@ -95,6 +114,7 @@ pub(crate) async fn read_devcontainer_configuration_for_project(
found_in_path,
node_runtime,
&directory,
None,
use_podman(cx),
)
.await
@ -131,9 +151,123 @@ fn use_podman(cx: &mut AsyncWindowContext) -> bool {
.unwrap_or(false)
}
/// Finds all available devcontainer configurations in the project.
///
/// This function scans for:
/// 1. `.devcontainer/devcontainer.json` (the default location)
/// 2. `.devcontainer/<subfolder>/devcontainer.json` (named configurations)
///
/// Returns a list of found configurations, or an empty list if none are found.
pub fn find_devcontainer_configs(cx: &mut AsyncWindowContext) -> Vec<DevContainerConfig> {
let Some(workspace) = cx.window_handle().downcast::<Workspace>() else {
log::debug!("find_devcontainer_configs: No workspace found");
return Vec::new();
};
let Ok(configs) = workspace.update(cx, |workspace, _, cx| {
let project = workspace.project().read(cx);
let worktree = project
.visible_worktrees(cx)
.find_map(|tree| tree.read(cx).root_entry()?.is_dir().then_some(tree));
let Some(worktree) = worktree else {
log::debug!("find_devcontainer_configs: No worktree found");
return Vec::new();
};
let worktree = worktree.read(cx);
let mut configs = Vec::new();
let devcontainer_path = RelPath::unix(".devcontainer").expect("valid path");
let Some(devcontainer_entry) = worktree.entry_for_path(devcontainer_path) else {
log::debug!("find_devcontainer_configs: .devcontainer directory not found in worktree");
return Vec::new();
};
if !devcontainer_entry.is_dir() {
log::debug!("find_devcontainer_configs: .devcontainer is not a directory");
return Vec::new();
}
log::debug!("find_devcontainer_configs: Scanning .devcontainer directory");
let devcontainer_json_path =
RelPath::unix(".devcontainer/devcontainer.json").expect("valid path");
for entry in worktree.child_entries(devcontainer_path) {
log::debug!(
"find_devcontainer_configs: Found entry: {:?}, is_file: {}, is_dir: {}",
entry.path.as_unix_str(),
entry.is_file(),
entry.is_dir()
);
if entry.is_file() && entry.path.as_ref() == devcontainer_json_path {
log::debug!("find_devcontainer_configs: Found default devcontainer.json");
configs.push(DevContainerConfig::default_config());
} else if entry.is_dir() {
let subfolder_name = entry
.path
.file_name()
.map(|n| n.to_string())
.unwrap_or_default();
let config_json_path = format!("{}/devcontainer.json", entry.path.as_unix_str());
if let Ok(rel_config_path) = RelPath::unix(&config_json_path) {
if worktree.entry_for_path(rel_config_path).is_some() {
log::debug!(
"find_devcontainer_configs: Found config in subfolder: {}",
subfolder_name
);
configs.push(DevContainerConfig {
name: subfolder_name,
config_path: PathBuf::from(&config_json_path),
});
} else {
log::debug!(
"find_devcontainer_configs: Subfolder {} has no devcontainer.json",
subfolder_name
);
}
}
}
}
log::info!(
"find_devcontainer_configs: Found {} configurations",
configs.len()
);
configs.sort_by(|a, b| {
if a.name == "default" {
std::cmp::Ordering::Less
} else if b.name == "default" {
std::cmp::Ordering::Greater
} else {
a.name.cmp(&b.name)
}
});
configs
}) else {
log::debug!("find_devcontainer_configs: Failed to update workspace");
return Vec::new();
};
configs
}
pub async fn start_dev_container(
cx: &mut AsyncWindowContext,
node_runtime: NodeRuntime,
) -> Result<(DevContainerConnection, String), DevContainerError> {
start_dev_container_with_config(cx, node_runtime, None).await
}
pub async fn start_dev_container_with_config(
cx: &mut AsyncWindowContext,
node_runtime: NodeRuntime,
config: Option<DevContainerConfig>,
) -> Result<(DevContainerConnection, String), DevContainerError> {
let use_podman = use_podman(cx);
check_for_docker(use_podman).await?;
@ -144,11 +278,14 @@ pub async fn start_dev_container(
return Err(DevContainerError::NotInValidProject);
};
let config_path = config.map(|c| directory.join(&c.config_path));
match devcontainer_up(
&path_to_devcontainer_cli,
found_in_path,
&node_runtime,
directory.clone(),
config_path.clone(),
use_podman,
)
.await
@ -164,6 +301,7 @@ pub async fn start_dev_container(
found_in_path,
&node_runtime,
&directory,
config_path.as_ref(),
use_podman,
)
.await
@ -313,6 +451,7 @@ async fn devcontainer_up(
found_in_path: bool,
node_runtime: &NodeRuntime,
path: Arc<Path>,
config_path: Option<PathBuf>,
use_podman: bool,
) -> Result<DevContainerUp, DevContainerError> {
let Ok(node_runtime_path) = node_runtime.binary_path().await else {
@ -326,6 +465,11 @@ async fn devcontainer_up(
command.arg("--workspace-folder");
command.arg(path.display().to_string());
if let Some(config) = config_path {
command.arg("--config");
command.arg(config.display().to_string());
}
log::info!("Running full devcontainer up command: {:?}", command);
match command.output().await {
@ -357,6 +501,7 @@ async fn devcontainer_read_configuration(
found_in_path: bool,
node_runtime: &NodeRuntime,
path: &Arc<Path>,
config_path: Option<&PathBuf>,
use_podman: bool,
) -> Result<DevContainerConfigurationOutput, DevContainerError> {
let Ok(node_runtime_path) = node_runtime.binary_path().await else {
@ -370,6 +515,11 @@ async fn devcontainer_read_configuration(
command.arg("--workspace-folder");
command.arg(path.display().to_string());
if let Some(config) = config_path {
command.arg("--config");
command.arg(config.display().to_string());
}
match command.output().await {
Ok(output) => {
if output.status.success() {

View file

@ -46,7 +46,10 @@ use devcontainer_api::read_devcontainer_configuration_for_project;
use crate::devcontainer_api::DevContainerError;
use crate::devcontainer_api::apply_dev_container_template;
pub use devcontainer_api::start_dev_container;
pub use devcontainer_api::{
DevContainerConfig, find_devcontainer_configs, start_dev_container,
start_dev_container_with_config,
};
#[derive(RegisterSetting)]
struct DevContainerSettings {

View file

@ -9,7 +9,7 @@ use std::path::PathBuf;
#[cfg(target_os = "windows")]
mod wsl_picker;
use dev_container::start_dev_container;
use dev_container::{find_devcontainer_configs, start_dev_container_with_config};
use remote::RemoteConnectionOptions;
pub use remote_connection::{RemoteConnectionModal, connect};
pub use remote_connections::open_remote_project;
@ -239,7 +239,7 @@ pub fn init(cx: &mut App) {
let replace_window = window.window_handle().downcast::<Workspace>();
let is_local = workspace.project().read(cx).is_local();
cx.spawn_in(window, async move |_, mut cx| {
cx.spawn_in(window, async move |_, cx| {
if !is_local {
cx.prompt(
gpui::PromptLevel::Critical,
@ -251,22 +251,47 @@ pub fn init(cx: &mut App) {
.ok();
return;
}
let (connection, starting_dir) =
match start_dev_container(&mut cx, app_state.node_runtime.clone()).await {
Ok((c, s)) => (Connection::DevContainer(c), s),
Err(e) => {
log::error!("Failed to start Dev Container: {:?}", e);
cx.prompt(
gpui::PromptLevel::Critical,
"Failed to start Dev Container",
Some(&format!("{:?}", e)),
&["Ok"],
)
.await
.ok();
return;
}
};
let configs = find_devcontainer_configs(cx);
if configs.len() > 1 {
// Multiple configs found - show modal for selection
cx.update(|_, cx| {
with_active_or_new_workspace(cx, move |workspace, window, cx| {
let fs = workspace.project().read(cx).fs().clone();
let handle = cx.entity().downgrade();
workspace.toggle_modal(window, cx, |window, cx| {
RemoteServerProjects::new_dev_container(fs, window, handle, cx)
});
});
})
.log_err();
return;
}
// Single or no config - proceed with opening directly
let config = configs.into_iter().next();
let (connection, starting_dir) = match start_dev_container_with_config(
cx,
app_state.node_runtime.clone(),
config,
)
.await
{
Ok((c, s)) => (Connection::DevContainer(c), s),
Err(e) => {
log::error!("Failed to start Dev Container: {:?}", e);
cx.prompt(
gpui::PromptLevel::Critical,
"Failed to start Dev Container",
Some(&format!("{:?}", e)),
&["Ok"],
)
.await
.ok();
return;
}
};
let result = open_remote_project(
connection.into(),
@ -276,7 +301,7 @@ pub fn init(cx: &mut App) {
replace_window,
..OpenOptions::default()
},
&mut cx,
cx,
)
.await;
@ -293,12 +318,6 @@ pub fn init(cx: &mut App) {
}
})
.detach();
let fs = workspace.project().read(cx).fs().clone();
let handle = cx.entity().downgrade();
workspace.toggle_modal(window, cx, |window, cx| {
RemoteServerProjects::new_dev_container(fs, window, handle, cx)
});
});
});

View file

@ -5,20 +5,22 @@ use crate::{
},
ssh_config::parse_ssh_config_hosts,
};
use dev_container::start_dev_container;
use dev_container::{
DevContainerConfig, find_devcontainer_configs, start_dev_container_with_config,
};
use editor::Editor;
use futures::{FutureExt, channel::oneshot, future::Shared};
use gpui::{
AnyElement, App, ClickEvent, ClipboardItem, Context, DismissEvent, Entity, EventEmitter,
FocusHandle, Focusable, PromptLevel, ScrollHandle, Subscription, Task, WeakEntity, Window,
canvas,
Action, AnyElement, App, ClickEvent, ClipboardItem, Context, DismissEvent, Entity,
EventEmitter, FocusHandle, Focusable, PromptLevel, ScrollHandle, Subscription, Task,
WeakEntity, Window, canvas,
};
use language::Point;
use log::{debug, info};
use open_path_prompt::OpenPathDelegate;
use paths::{global_ssh_config_file, user_ssh_config_file};
use picker::Picker;
use picker::{Picker, PickerDelegate};
use project::{Fs, Project};
use remote::{
RemoteClient, RemoteConnectionOptions, SshConnectionOptions, WslConnectionOptions,
@ -62,6 +64,7 @@ pub struct RemoteServerProjects {
ssh_config_updates: Task<()>,
ssh_config_servers: BTreeSet<SharedString>,
create_new_window: bool,
dev_container_picker: Option<Entity<Picker<DevContainerPickerDelegate>>>,
_subscription: Subscription,
}
@ -89,35 +92,25 @@ impl CreateRemoteServer {
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
enum DevContainerCreationProgress {
Initial,
SelectingConfig,
Creating,
Error(String),
}
#[derive(Clone)]
struct CreateRemoteDevContainer {
// 3 Navigable Options
// - Create from devcontainer.json
// - Edit devcontainer.json
// - Go back
entries: [NavigableEntry; 3],
back_entry: NavigableEntry,
progress: DevContainerCreationProgress,
}
impl CreateRemoteDevContainer {
fn new(window: &mut Window, cx: &mut Context<RemoteServerProjects>) -> Self {
let entries = std::array::from_fn(|_| NavigableEntry::focusable(cx));
entries[0].focus_handle.focus(window, cx);
fn new(progress: DevContainerCreationProgress, cx: &mut Context<RemoteServerProjects>) -> Self {
let back_entry = NavigableEntry::focusable(cx);
Self {
entries,
progress: DevContainerCreationProgress::Initial,
back_entry,
progress,
}
}
fn progress(&mut self, progress: DevContainerCreationProgress) -> Self {
self.progress = progress;
self.clone()
}
}
#[cfg(target_os = "windows")]
@ -182,6 +175,164 @@ struct EditNicknameState {
editor: Entity<Editor>,
}
struct DevContainerPickerDelegate {
selected_index: usize,
candidates: Vec<DevContainerConfig>,
matching_candidates: Vec<DevContainerConfig>,
parent_modal: WeakEntity<RemoteServerProjects>,
}
impl DevContainerPickerDelegate {
fn new(
candidates: Vec<DevContainerConfig>,
parent_modal: WeakEntity<RemoteServerProjects>,
) -> Self {
Self {
selected_index: 0,
matching_candidates: candidates.clone(),
candidates,
parent_modal,
}
}
}
impl PickerDelegate for DevContainerPickerDelegate {
type ListItem = AnyElement;
fn match_count(&self) -> usize {
self.matching_candidates.len()
}
fn selected_index(&self) -> usize {
self.selected_index
}
fn set_selected_index(
&mut self,
ix: usize,
_window: &mut Window,
_cx: &mut Context<Picker<Self>>,
) {
self.selected_index = ix;
}
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
"Select Dev Container Configuration".into()
}
fn update_matches(
&mut self,
query: String,
_window: &mut Window,
_cx: &mut Context<Picker<Self>>,
) -> Task<()> {
let query_lower = query.to_lowercase();
self.matching_candidates = self
.candidates
.iter()
.filter(|c| {
c.name.to_lowercase().contains(&query_lower)
|| c.config_path
.to_string_lossy()
.to_lowercase()
.contains(&query_lower)
})
.cloned()
.collect();
self.selected_index = std::cmp::min(
self.selected_index,
self.matching_candidates.len().saturating_sub(1),
);
Task::ready(())
}
fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
let selected_config = self.matching_candidates.get(self.selected_index).cloned();
self.parent_modal
.update(cx, move |modal, cx| {
if secondary {
modal.edit_in_dev_container_json(selected_config.clone(), window, cx);
} else {
modal.open_dev_container(selected_config, window, cx);
modal.view_in_progress_dev_container(window, cx);
}
})
.ok();
}
fn dismissed(&mut self, window: &mut Window, cx: &mut Context<Picker<Self>>) {
self.parent_modal
.update(cx, |modal, cx| {
modal.cancel(&menu::Cancel, window, cx);
})
.ok();
}
fn render_match(
&self,
ix: usize,
selected: bool,
_window: &mut Window,
_cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
let candidate = self.matching_candidates.get(ix)?;
let config_path = candidate.config_path.display().to_string();
Some(
ListItem::new(SharedString::from(format!("li-devcontainer-config-{}", ix)))
.inset(true)
.spacing(ui::ListItemSpacing::Sparse)
.toggle_state(selected)
.start_slot(Icon::new(IconName::FileToml).color(Color::Muted))
.child(
v_flex().child(Label::new(candidate.name.clone())).child(
Label::new(config_path)
.size(ui::LabelSize::Small)
.color(Color::Muted),
),
)
.into_any_element(),
)
}
fn render_footer(
&self,
_window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<AnyElement> {
Some(
h_flex()
.w_full()
.p_1p5()
.gap_1()
.justify_start()
.border_t_1()
.border_color(cx.theme().colors().border_variant)
.child(
Button::new("run-action", "Start Dev Container")
.key_binding(
KeyBinding::for_action(&menu::Confirm, cx)
.map(|kb| kb.size(rems_from_px(12.))),
)
.on_click(|_, window, cx| {
window.dispatch_action(menu::Confirm.boxed_clone(), cx)
}),
)
.child(
Button::new("run-action-secondary", "Open devcontainer.json")
.key_binding(
KeyBinding::for_action(&menu::SecondaryConfirm, cx)
.map(|kb| kb.size(rems_from_px(12.))),
)
.on_click(|_, window, cx| {
window.dispatch_action(menu::SecondaryConfirm.boxed_clone(), cx)
}),
)
.into_any_element(),
)
}
}
impl EditNicknameState {
fn new(index: SshServerIndex, window: &mut Window, cx: &mut App) -> Self {
let this = Self {
@ -654,17 +805,49 @@ impl RemoteServerProjects {
workspace: WeakEntity<Workspace>,
cx: &mut Context<Self>,
) -> Self {
Self::new_inner(
Mode::CreateRemoteDevContainer(
CreateRemoteDevContainer::new(window, cx)
.progress(DevContainerCreationProgress::Creating),
),
let this = Self::new_inner(
Mode::CreateRemoteDevContainer(CreateRemoteDevContainer::new(
DevContainerCreationProgress::Creating,
cx,
)),
false,
fs,
window,
workspace,
cx,
)
);
// Spawn a task to scan for configs and then start the container
cx.spawn_in(window, async move |entity, cx| {
let configs = find_devcontainer_configs(cx);
entity
.update_in(cx, |this, window, cx| {
if configs.len() > 1 {
// Multiple configs found - show selection UI
let delegate = DevContainerPickerDelegate::new(configs, cx.weak_entity());
this.dev_container_picker = Some(
cx.new(|cx| Picker::uniform_list(delegate, window, cx).modal(false)),
);
let state = CreateRemoteDevContainer::new(
DevContainerCreationProgress::SelectingConfig,
cx,
);
this.mode = Mode::CreateRemoteDevContainer(state);
cx.notify();
} else {
// Single or no config - proceed with opening
let config = configs.into_iter().next();
this.open_dev_container(config, window, cx);
this.view_in_progress_dev_container(window, cx);
}
})
.log_err();
})
.detach();
this
}
pub fn popover(
@ -725,6 +908,7 @@ impl RemoteServerProjects {
ssh_config_updates,
ssh_config_servers: BTreeSet::new(),
create_new_window,
dev_container_picker: None,
_subscription,
}
}
@ -946,10 +1130,10 @@ impl RemoteServerProjects {
}
fn view_in_progress_dev_container(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.mode = Mode::CreateRemoteDevContainer(
CreateRemoteDevContainer::new(window, cx)
.progress(DevContainerCreationProgress::Creating),
);
self.mode = Mode::CreateRemoteDevContainer(CreateRemoteDevContainer::new(
DevContainerCreationProgress::Creating,
cx,
));
self.focus_handle(cx).focus(window, cx);
cx.notify();
}
@ -1549,13 +1733,22 @@ impl RemoteServerProjects {
});
}
fn edit_in_dev_container_json(&mut self, window: &mut Window, cx: &mut Context<Self>) {
fn edit_in_dev_container_json(
&mut self,
config: Option<DevContainerConfig>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(workspace) = self.workspace.upgrade() else {
cx.emit(DismissEvent);
cx.notify();
return;
};
let config_path = config
.map(|c| c.config_path)
.unwrap_or_else(|| PathBuf::from(".devcontainer/devcontainer.json"));
workspace.update(cx, |workspace, cx| {
let project = workspace.project().clone();
@ -1566,7 +1759,18 @@ impl RemoteServerProjects {
if let Some(worktree) = worktree {
let tree_id = worktree.read(cx).id();
let devcontainer_path = RelPath::unix(".devcontainer/devcontainer.json").unwrap();
let devcontainer_path =
match RelPath::new(&config_path, util::paths::PathStyle::Posix) {
Ok(path) => path.into_owned(),
Err(error) => {
log::error!(
"Invalid devcontainer path: {} - {}",
config_path.display(),
error
);
return;
}
};
cx.spawn_in(window, async move |workspace, cx| {
workspace
.update_in(cx, |workspace, window, cx| {
@ -1589,7 +1793,34 @@ impl RemoteServerProjects {
cx.notify();
}
fn open_dev_container(&self, window: &mut Window, cx: &mut Context<Self>) {
fn init_dev_container_mode(&mut self, window: &mut Window, cx: &mut Context<Self>) {
cx.spawn_in(window, async move |entity, cx| {
let configs = find_devcontainer_configs(cx);
entity
.update_in(cx, |this, window, cx| {
let delegate = DevContainerPickerDelegate::new(configs, cx.weak_entity());
this.dev_container_picker =
Some(cx.new(|cx| Picker::uniform_list(delegate, window, cx).modal(false)));
let state = CreateRemoteDevContainer::new(
DevContainerCreationProgress::SelectingConfig,
cx,
);
this.mode = Mode::CreateRemoteDevContainer(state);
cx.notify();
})
.log_err();
})
.detach();
}
fn open_dev_container(
&self,
config: Option<DevContainerConfig>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(app_state) = self
.workspace
.read_with(cx, |workspace, _| workspace.app_state().clone())
@ -1602,19 +1833,29 @@ impl RemoteServerProjects {
cx.spawn_in(window, async move |entity, cx| {
let (connection, starting_dir) =
match start_dev_container(cx, app_state.node_runtime.clone()).await {
match start_dev_container_with_config(cx, app_state.node_runtime.clone(), config)
.await
{
Ok((c, s)) => (Connection::DevContainer(c), s),
Err(e) => {
log::error!("Failed to start dev container: {:?}", e);
entity
.update_in(cx, |remote_server_projects, window, cx| {
remote_server_projects.mode = Mode::CreateRemoteDevContainer(
CreateRemoteDevContainer::new(window, cx).progress(
.update_in(cx, |remote_server_projects, _window, cx| {
remote_server_projects.mode =
Mode::CreateRemoteDevContainer(CreateRemoteDevContainer::new(
DevContainerCreationProgress::Error(format!("{:?}", e)),
),
);
cx,
));
})
.log_err();
cx.prompt(
gpui::PromptLevel::Critical,
"Failed to start Dev Container",
Some(&format!("{:?}", e)),
&["Ok"],
)
.await
.ok();
return;
}
};
@ -1659,7 +1900,7 @@ impl RemoteServerProjects {
match &state.progress {
DevContainerCreationProgress::Error(message) => {
self.focus_handle(cx).focus(window, cx);
return div()
div()
.track_focus(&self.focus_handle(cx))
.size_full()
.child(
@ -1678,7 +1919,7 @@ impl RemoteServerProjects {
.child(
div()
.id("devcontainer-go-back")
.track_focus(&state.entries[0].focus_handle)
.track_focus(&state.back_entry.focus_handle)
.on_action(cx.listener(
|this, _: &menu::Confirm, window, cx| {
this.mode =
@ -1690,7 +1931,8 @@ impl RemoteServerProjects {
.child(
ListItem::new("li-devcontainer-go-back")
.toggle_state(
state.entries[0]
state
.back_entry
.focus_handle
.contains_focused(window, cx),
)
@ -1709,180 +1951,73 @@ impl RemoteServerProjects {
.size(rems_from_px(12.)),
)
.on_click(cx.listener(|this, _, window, cx| {
let state =
CreateRemoteDevContainer::new(window, cx);
this.mode = Mode::CreateRemoteDevContainer(state);
this.mode = Mode::default_mode(
&this.ssh_config_servers,
cx,
);
cx.focus_self(window);
cx.notify();
})),
),
),
)
.into_any_element();
.into_any_element()
}
_ => {}
DevContainerCreationProgress::SelectingConfig => {
self.render_config_selection(window, cx).into_any_element()
}
DevContainerCreationProgress::Creating => {
self.focus_handle(cx).focus(window, cx);
div()
.track_focus(&self.focus_handle(cx))
.size_full()
.child(
v_flex()
.pb_1()
.child(
ModalHeader::new().child(
Headline::new("Dev Containers").size(HeadlineSize::XSmall),
),
)
.child(ListSeparator)
.child(
ListItem::new("creating")
.inset(true)
.spacing(ui::ListItemSpacing::Sparse)
.disabled(true)
.start_slot(
Icon::new(IconName::ArrowCircle)
.color(Color::Muted)
.with_rotate_animation(2),
)
.child(
h_flex()
.opacity(0.6)
.gap_1()
.child(Label::new("Creating Dev Container"))
.child(LoadingLabel::new("")),
),
),
)
.into_any_element()
}
}
}
fn render_config_selection(
&self,
window: &mut Window,
cx: &mut Context<Self>,
) -> impl IntoElement {
let Some(picker) = &self.dev_container_picker else {
return div().into_any_element();
};
let mut view = Navigable::new(
div()
.track_focus(&self.focus_handle(cx))
.size_full()
.child(
v_flex()
.pb_1()
.child(
ModalHeader::new()
.child(Headline::new("Dev Containers").size(HeadlineSize::XSmall)),
)
.child(ListSeparator)
.child(
div()
.id("confirm-create-from-devcontainer-json")
.track_focus(&state.entries[0].focus_handle)
.on_action(cx.listener({
move |this, _: &menu::Confirm, window, cx| {
this.open_dev_container(window, cx);
this.view_in_progress_dev_container(window, cx);
}
}))
.map(|this| {
if state.progress == DevContainerCreationProgress::Creating {
this.child(
ListItem::new("creating")
.inset(true)
.spacing(ui::ListItemSpacing::Sparse)
.disabled(true)
.start_slot(
Icon::new(IconName::ArrowCircle)
.color(Color::Muted)
.with_rotate_animation(2),
)
.child(
h_flex()
.opacity(0.6)
.gap_1()
.child(Label::new("Creating From"))
.child(
Label::new("devcontainer.json")
.buffer_font(cx),
)
.child(LoadingLabel::new("")),
),
)
} else {
this.child(
ListItem::new(
"li-confirm-create-from-devcontainer-json",
)
.toggle_state(
state.entries[0]
.focus_handle
.contains_focused(window, cx),
)
.inset(true)
.spacing(ui::ListItemSpacing::Sparse)
.start_slot(
Icon::new(IconName::Plus).color(Color::Muted),
)
.child(
h_flex()
.gap_1()
.child(Label::new("Open or Create New From"))
.child(
Label::new("devcontainer.json")
.buffer_font(cx),
),
)
.on_click(
cx.listener({
move |this, _, window, cx| {
this.open_dev_container(window, cx);
this.view_in_progress_dev_container(
window, cx,
);
cx.notify();
}
}),
),
)
}
}),
)
.child(
div()
.id("edit-devcontainer-json")
.track_focus(&state.entries[1].focus_handle)
.on_action(cx.listener(|this, _: &menu::Confirm, window, cx| {
this.edit_in_dev_container_json(window, cx);
}))
.child(
ListItem::new("li-edit-devcontainer-json")
.toggle_state(
state.entries[1]
.focus_handle
.contains_focused(window, cx),
)
.inset(true)
.spacing(ui::ListItemSpacing::Sparse)
.start_slot(Icon::new(IconName::Pencil).color(Color::Muted))
.child(
h_flex().gap_1().child(Label::new("Edit")).child(
Label::new("devcontainer.json").buffer_font(cx),
),
)
.on_click(cx.listener(move |this, _, window, cx| {
this.edit_in_dev_container_json(window, cx);
})),
),
)
.child(ListSeparator)
.child(
div()
.id("devcontainer-go-back")
.track_focus(&state.entries[2].focus_handle)
.on_action(cx.listener(|this, _: &menu::Confirm, window, cx| {
this.mode = Mode::default_mode(&this.ssh_config_servers, cx);
cx.focus_self(window);
cx.notify();
}))
.child(
ListItem::new("li-devcontainer-go-back")
.toggle_state(
state.entries[2]
.focus_handle
.contains_focused(window, cx),
)
.inset(true)
.spacing(ui::ListItemSpacing::Sparse)
.start_slot(
Icon::new(IconName::ArrowLeft).color(Color::Muted),
)
.child(Label::new("Go Back"))
.end_slot(
KeyBinding::for_action_in(
&menu::Cancel,
&self.focus_handle,
cx,
)
.size(rems_from_px(12.)),
)
.on_click(cx.listener(|this, _, window, cx| {
this.mode =
Mode::default_mode(&this.ssh_config_servers, cx);
cx.focus_self(window);
cx.notify()
})),
),
),
)
.into_any_element(),
);
let content = v_flex().pb_1().child(picker.clone().into_any_element());
view = view.entry(state.entries[0].clone());
view = view.entry(state.entries[1].clone());
view = view.entry(state.entries[2].clone());
picker.focus_handle(cx).focus(window, cx);
view.render(window, cx).into_any_element()
content.into_any_element()
}
fn render_create_remote_server(
@ -2469,17 +2604,11 @@ impl RemoteServerProjects {
.start_slot(Icon::new(IconName::Plus).color(Color::Muted))
.child(Label::new("Connect Dev Container"))
.on_click(cx.listener(|this, _, window, cx| {
let state = CreateRemoteDevContainer::new(window, cx);
this.mode = Mode::CreateRemoteDevContainer(state);
cx.notify();
this.init_dev_container_mode(window, cx);
})),
)
.on_action(cx.listener(|this, _: &menu::Confirm, window, cx| {
let state = CreateRemoteDevContainer::new(window, cx);
this.mode = Mode::CreateRemoteDevContainer(state);
cx.notify();
this.init_dev_container_mode(window, cx);
}));
#[cfg(target_os = "windows")]