From 13a06e673bea8d9585a10e17ce35dd266757f98e Mon Sep 17 00:00:00 2001 From: Caio Piccirillo <34453935+caiopiccirillo@users.noreply.github.com> Date: Tue, 3 Feb 2026 14:05:40 -0300 Subject: [PATCH] Add detection of devcontainers in subfolders (#47411) Release Notes: - Add detection of devcontainers in subfolders --------- Co-authored-by: KyleBarton --- crates/dev_container/src/devcontainer_api.rs | 150 +++++ crates/dev_container/src/lib.rs | 5 +- crates/recent_projects/src/recent_projects.rs | 69 ++- crates/recent_projects/src/remote_servers.rs | 559 +++++++++++------- 4 files changed, 542 insertions(+), 241 deletions(-) diff --git a/crates/dev_container/src/devcontainer_api.rs b/crates/dev_container/src/devcontainer_api.rs index 6e7a2a336bfa231ed668df5bb6046c7b05af9636..3c822aa139001e3ccb2d6350a2380b36e8925d1b 100644 --- a/crates/dev_container/src/devcontainer_api.rs +++ b/crates/dev_container/src/devcontainer_api.rs @@ -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//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 { + let Some(workspace) = cx.window_handle().downcast::() 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, ) -> 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, + config_path: Option, use_podman: bool, ) -> Result { 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, + config_path: Option<&PathBuf>, use_podman: bool, ) -> Result { 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() { diff --git a/crates/dev_container/src/lib.rs b/crates/dev_container/src/lib.rs index e670b0e8339ea2ca10575f99529d6e86c702980e..1168bdc6cfb56463697d2bd5b26e4ce95387003b 100644 --- a/crates/dev_container/src/lib.rs +++ b/crates/dev_container/src/lib.rs @@ -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 { diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs index c5b668720a9a657a5bf5264dbdc991cd30b4fbb7..2693266b927b4dd1c2cdeab594a801b37df0079f 100644 --- a/crates/recent_projects/src/recent_projects.rs +++ b/crates/recent_projects/src/recent_projects.rs @@ -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::(); 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) - }); }); }); diff --git a/crates/recent_projects/src/remote_servers.rs b/crates/recent_projects/src/remote_servers.rs index 3d420893d637b926d75b33b0257121edb74d7e8b..3fd9603ad4793635da0fabd3f52d3868ee90521b 100644 --- a/crates/recent_projects/src/remote_servers.rs +++ b/crates/recent_projects/src/remote_servers.rs @@ -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, create_new_window: bool, + dev_container_picker: Option>>, _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) -> Self { - let entries = std::array::from_fn(|_| NavigableEntry::focusable(cx)); - entries[0].focus_handle.focus(window, cx); + fn new(progress: DevContainerCreationProgress, cx: &mut Context) -> 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, } +struct DevContainerPickerDelegate { + selected_index: usize, + candidates: Vec, + matching_candidates: Vec, + parent_modal: WeakEntity, +} +impl DevContainerPickerDelegate { + fn new( + candidates: Vec, + parent_modal: WeakEntity, + ) -> 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>, + ) { + self.selected_index = ix; + } + + fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { + "Select Dev Container Configuration".into() + } + + fn update_matches( + &mut self, + query: String, + _window: &mut Window, + _cx: &mut Context>, + ) -> 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>) { + 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>) { + 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>, + ) -> Option { + 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>, + ) -> Option { + 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, cx: &mut Context, ) -> 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.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) { + fn edit_in_dev_container_json( + &mut self, + config: Option, + window: &mut Window, + cx: &mut Context, + ) { 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) { + fn init_dev_container_mode(&mut self, window: &mut Window, cx: &mut Context) { + 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, + window: &mut Window, + cx: &mut Context, + ) { 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, + ) -> 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")]