@@ -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() {
@@ -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")]