feat(remote): reopen a local project in its dev container (#1843)

Refs #1083 (step B). A local project with a devcontainer.json can be reopened in its dev container (File › Reopen in Container, Find Action): the Dev Containers CLI brings the container up and the window connects to openidea-server inside it, as for WSL, so terminal, language servers, run, debug and git work in the container. The project's identity stays the local folder, so reopening from Recent Projects finds the same container. Missing CLI gives install guidance; trust of the local project carries over. Pure CLI output parsing lives in ide-remote; a failed connection no longer closes the current backend early, saved locations keep the container configuration, and Docker context precedence follows the CLI.

Co-authored-by: andreabasile <[email protected]>
Цей коміт міститься в:
Andrea Basileтаandreabasile authored and GitHub committed 2026-11-08 01:43:03 +08:00
1 джерело 7479b7ad9d
коміт 60d7dcdca0
29 files changed
+908 -69

No files matched your search

+34 -7
Переглянути файл
@@ -36,6 +36,9 @@ pub enum RemoteTarget {
Ssh { config: String },
/// A WSL distribution on this machine.
Wsl { distro: String },
/// The dev container of local folder `folder`, built and started with the Dev Containers CLI;
/// `config` is its `devcontainer.json`, relative to the folder (empty = the default one).
DevContainer { folder: String, config: String },
}
/// Where the server is installed on the remote host (per version).
@@ -66,7 +69,7 @@ pub struct Connection {
/// connection with its parent).
channels: Channels,
next_channel: std::sync::atomic::AtomicU64,
/// Keeps the SSH connection alive; `None` for WSL.
/// Keeps the SSH connection alive; `None` for WSL and dev containers.
session: Option<Arc<ide_ssh::Session>>,
/// Remote ports forwarded to this machine (Ports tool window), by remote port.
forwards: Mutex<Vec<(u16, ide_ssh::Tunnel)>>,
@@ -110,7 +113,11 @@ impl Connections {
self.0
.lock()
.iter()
.find(|(_, c)| c.target == *target && c.root.trim_end_matches('/') == root)
// A dev container is one project: its folder there is only known once connected.
.find(|(_, c)| {
c.target == *target
&& (matches!(target, RemoteTarget::DevContainer { .. }) || c.root.trim_end_matches('/') == root)
})
.map(|(label, _)| label.clone())
}
@@ -131,6 +138,8 @@ impl Connections {
#[serde(rename_all = "camelCase")]
pub enum RemoteStage {
Connecting,
/// `devcontainer up`: building or starting the container.
Container,
Checking,
Downloading,
Uploading,
@@ -403,15 +412,16 @@ fn build_id(path: &std::path::Path) -> IdeResult<String> {
Ok(id)
}
/// File › Remote Development: opens `path` on `target` (an SSH host or a WSL distribution)
/// in this window (installs / starts the server as needed).
/// File › Remote Development: opens `path` on `target` (an SSH host, a WSL distribution, or a
/// dev container, whose project folder is the container's own) in this window (installs /
/// starts the server as needed).
#[tauri::command]
#[specta::specta]
pub async fn remote_connect(
window: Window,
state: State<'_, AppState>,
target: RemoteTarget,
path: String,
mut path: String,
on_progress: Channel<RemoteProgress>,
) -> IdeResult<RemoteProject> {
let progress: Arc<Progress> = Arc::new(move |stage, fraction| {
@@ -419,8 +429,6 @@ pub async fn remote_connect(
});
progress(RemoteStage::Connecting, None);
let label = AppState::owner_window(window.label()).to_owned();
state.remote_dev.disconnect(&label);
state.host.close_project(&label);
let channels: Channels = Arc::default();
let handler =
Arc::new(ClientHandler { app: window.app_handle().clone(), window: label.clone(), channels: channels.clone() });
@@ -444,6 +452,15 @@ pub async fn remote_connect(
let peer = crate::remote_wsl::spawn(distro, &bin, handler, &tail)?;
(peer, None, distro.clone(), format!("WSL: {distro}"))
}
RemoteTarget::DevContainer { folder, config } => {
let (peer, root) =
crate::remote_container::connect(&state, folder, config, handler, &tail, &progress).await?;
path = root;
let name = std::path::Path::new(folder)
.file_name()
.map_or_else(|| folder.clone(), |n| n.to_string_lossy().into_owned());
(peer, None, name.clone(), format!("Dev Container: {name}"))
}
};
// A server that dies on start-up says why on stderr (`wsl.exe` prints its own errors too).
let with_output = |e: IdeError| match tail.text() {
@@ -490,6 +507,8 @@ pub async fn remote_connect(
session,
forwards: Mutex::new(Vec::new()),
});
state.remote_dev.disconnect(&label);
state.host.close_project(&label);
state.remote_dev.0.lock().insert(label.clone(), connection.clone());
let plugins = state.plugins.clone();
tauri::async_runtime::spawn(async move {
@@ -767,4 +786,12 @@ mod tests {
assert_eq!(platform("Darwin arm64"), ("macos".into(), "aarch64".into()));
assert_eq!(platform("Linux aarch64"), ("linux".into(), "aarch64".into()));
}
#[test]
fn a_dev_container_target_is_its_local_folder_and_configuration() {
let target = RemoteTarget::DevContainer { folder: "/Users/me/app".into(), config: String::new() };
let json = serde_json::to_value(&target).unwrap();
assert_eq!(json, serde_json::json!({ "kind": "devContainer", "folder": "/Users/me/app", "config": "" }));
assert_eq!(serde_json::from_value::<RemoteTarget>(json).unwrap(), target);
}
}