@@ -34,6 +34,7 @@ pub use remote_connections::RemoteSettings;
pub use remote_servers::RemoteServerProjects;
use settings::{Settings, WorktreeId};
+use dev_container::{DevContainerContext, find_devcontainer_configs};
use ui::{
ContextMenu, Divider, KeyBinding, ListItem, ListItemSpacing, ListSubHeader, PopoverMenu,
PopoverMenuHandle, TintColor, Tooltip, prelude::*,
@@ -352,9 +353,20 @@ pub fn init(cx: &mut App) {
}
let fs = workspace.project().read(cx).fs().clone();
+ let configs = find_devcontainer_configs(workspace, cx);
+ let app_state = workspace.app_state().clone();
+ let dev_container_context = DevContainerContext::from_workspace(workspace, cx);
let handle = cx.entity().downgrade();
workspace.toggle_modal(window, cx, |window, cx| {
- RemoteServerProjects::new_dev_container(fs, window, handle, cx)
+ RemoteServerProjects::new_dev_container(
+ fs,
+ configs,
+ app_state,
+ dev_container_context,
+ window,
+ handle,
+ cx,
+ )
});
});
});
@@ -1621,6 +1633,121 @@ mod tests {
.unwrap()
}
+ #[gpui::test]
+ async fn test_open_dev_container_action_with_single_config(cx: &mut TestAppContext) {
+ let app_state = init_test(cx);
+
+ app_state
+ .fs
+ .as_fake()
+ .insert_tree(
+ path!("/project"),
+ json!({
+ ".devcontainer": {
+ "devcontainer.json": "{}"
+ },
+ "src": {
+ "main.rs": "fn main() {}"
+ }
+ }),
+ )
+ .await;
+
+ cx.update(|cx| {
+ open_paths(
+ &[PathBuf::from(path!("/project"))],
+ app_state,
+ workspace::OpenOptions::default(),
+ cx,
+ )
+ })
+ .await
+ .unwrap();
+
+ assert_eq!(cx.update(|cx| cx.windows().len()), 1);
+ let multi_workspace = cx.update(|cx| cx.windows()[0].downcast::<MultiWorkspace>().unwrap());
+
+ cx.run_until_parked();
+
+ // This dispatch triggers with_active_or_new_workspace -> MultiWorkspace::update
+ // -> Workspace::update -> toggle_modal -> new_dev_container.
+ // Before the fix, this panicked with "cannot read workspace::Workspace while
+ // it is already being updated" because new_dev_container and open_dev_container
+ // tried to read the Workspace entity through a WeakEntity handle while it was
+ // already leased by the outer update.
+ cx.dispatch_action(*multi_workspace, OpenDevContainer);
+
+ multi_workspace
+ .update(cx, |multi_workspace, _, cx| {
+ let modal = multi_workspace
+ .workspace()
+ .read(cx)
+ .active_modal::<RemoteServerProjects>(cx);
+ assert!(
+ modal.is_some(),
+ "Dev container modal should be open after dispatching OpenDevContainer"
+ );
+ })
+ .unwrap();
+ }
+
+ #[gpui::test]
+ async fn test_open_dev_container_action_with_multiple_configs(cx: &mut TestAppContext) {
+ let app_state = init_test(cx);
+
+ app_state
+ .fs
+ .as_fake()
+ .insert_tree(
+ path!("/project"),
+ json!({
+ ".devcontainer": {
+ "rust": {
+ "devcontainer.json": "{}"
+ },
+ "python": {
+ "devcontainer.json": "{}"
+ }
+ },
+ "src": {
+ "main.rs": "fn main() {}"
+ }
+ }),
+ )
+ .await;
+
+ cx.update(|cx| {
+ open_paths(
+ &[PathBuf::from(path!("/project"))],
+ app_state,
+ workspace::OpenOptions::default(),
+ cx,
+ )
+ })
+ .await
+ .unwrap();
+
+ assert_eq!(cx.update(|cx| cx.windows().len()), 1);
+ let multi_workspace = cx.update(|cx| cx.windows()[0].downcast::<MultiWorkspace>().unwrap());
+
+ cx.run_until_parked();
+
+ cx.dispatch_action(*multi_workspace, OpenDevContainer);
+
+ multi_workspace
+ .update(cx, |multi_workspace, _, cx| {
+ let modal = multi_workspace
+ .workspace()
+ .read(cx)
+ .active_modal::<RemoteServerProjects>(cx);
+ assert!(
+ modal.is_some(),
+ "Dev container modal should be open after dispatching OpenDevContainer with multiple configs"
+ );
+ })
+ .unwrap();
+ }
+
fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
cx.update(|cx| {
let state = AppState::test(cx);
@@ -53,7 +53,7 @@ use util::{
rel_path::RelPath,
};
use workspace::{
- ModalView, MultiWorkspace, OpenLog, OpenOptions, Toast, Workspace,
+ AppState, ModalView, MultiWorkspace, OpenLog, OpenOptions, Toast, Workspace,
notifications::{DetachAndPromptErr, NotificationId},
open_remote_project_with_existing_connection,
};
@@ -258,9 +258,20 @@ impl PickerDelegate for DevContainerPickerDelegate {
.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);
+ } else if let Some((app_state, context)) = modal
+ .workspace
+ .read_with(cx, |workspace, cx| {
+ let app_state = workspace.app_state().clone();
+ let context = DevContainerContext::from_workspace(workspace, cx)?;
+ Some((app_state, context))
+ })
+ .ok()
+ .flatten()
+ {
+ modal.open_dev_container(selected_config, app_state, context, window, cx);
modal.view_in_progress_dev_container(window, cx);
+ } else {
+ log::error!("No active project directory for Dev Container");
}
})
.ok();
@@ -807,14 +818,13 @@ impl RemoteServerProjects {
/// Used when suggesting dev container connection from toast notification.
pub fn new_dev_container(
fs: Arc<dyn Fs>,
+ configs: Vec<DevContainerConfig>,
+ app_state: Arc<AppState>,
+ dev_container_context: Option<DevContainerContext>,
window: &mut Window,
workspace: WeakEntity<Workspace>,
cx: &mut Context<Self>,
) -> Self {
- let configs = workspace
- .read_with(cx, |workspace, cx| find_devcontainer_configs(workspace, cx))
- .unwrap_or_default();
-
let initial_mode = if configs.len() > 1 {
DevContainerCreationProgress::SelectingConfig
} else {
@@ -834,10 +844,12 @@ impl RemoteServerProjects {
let delegate = DevContainerPickerDelegate::new(configs, cx.weak_entity());
this.dev_container_picker =
Some(cx.new(|cx| Picker::uniform_list(delegate, window, cx).modal(false)));
- } else {
+ } else if let Some(context) = dev_container_context {
let config = configs.into_iter().next();
- this.open_dev_container(config, window, cx);
+ this.open_dev_container(config, app_state, context, window, cx);
this.view_in_progress_dev_container(window, cx);
+ } else {
+ log::error!("No active project directory for Dev Container");
}
this
@@ -1809,33 +1821,32 @@ impl RemoteServerProjects {
CreateRemoteDevContainer::new(DevContainerCreationProgress::SelectingConfig, cx);
self.mode = Mode::CreateRemoteDevContainer(state);
cx.notify();
- } else {
+ } else if let Some((app_state, context)) = self
+ .workspace
+ .read_with(cx, |workspace, cx| {
+ let app_state = workspace.app_state().clone();
+ let context = DevContainerContext::from_workspace(workspace, cx)?;
+ Some((app_state, context))
+ })
+ .ok()
+ .flatten()
+ {
let config = configs.into_iter().next();
- self.open_dev_container(config, window, cx);
+ self.open_dev_container(config, app_state, context, window, cx);
self.view_in_progress_dev_container(window, cx);
+ } else {
+ log::error!("No active project directory for Dev Container");
}
}
fn open_dev_container(
&self,
config: Option<DevContainerConfig>,
+ app_state: Arc<AppState>,
+ context: DevContainerContext,
window: &mut Window,
cx: &mut Context<Self>,
) {
- let Some((app_state, context)) = self
- .workspace
- .read_with(cx, |workspace, cx| {
- let app_state = workspace.app_state().clone();
- let context = DevContainerContext::from_workspace(workspace, cx)?;
- Some((app_state, context))
- })
- .log_err()
- .flatten()
- else {
- log::error!("No active project directory for Dev Container");
- return;
- };
-
let replace_window = window.window_handle().downcast::<MultiWorkspace>();
cx.spawn_in(window, async move |entity, cx| {