mirror of
https://github.com/zed-industries/zed.git
synced 2026-06-01 03:14:56 +07:00
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:
parent
18a3b0c53a
commit
13a06e673b
4 changed files with 542 additions and 241 deletions
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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")]
|
||||
|
|
|
|||
Loading…
Reference in a new issue