Re-add MultiWorkspace (#48800)

Release Notes:

- Added agent panel restoration. Now restarting your editor won't cause
your thread to be forgotten.

---------

Co-authored-by: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com>
Co-authored-by: Eric Holk <eric@zed.dev>
Co-authored-by: Danilo Leal <67129314+danilo-leal@users.noreply.github.com>
Co-authored-by: Anthony Eid <anthony@zed.dev>
Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
Co-authored-by: Cameron Mcloughlin <cameron.studdstreet@gmail.com>
This commit is contained in:
authored and GitHub committed 2026-02-12 01:06:23 +00:00
1 parent 4d00186953
commit a67197b3e2
133 files changed
+9290 -4100

No files matched your search

+89 -211
View File
@@ -2,19 +2,17 @@ use std::{
collections::{HashMap, HashSet},
fmt::Display,
path::{Path, PathBuf},
sync::Arc,
};
use gpui::AsyncWindowContext;
use node_runtime::NodeRuntime;
use serde::Deserialize;
use settings::{DevContainerConnection, Settings as _};
use settings::DevContainerConnection;
use smol::{fs, process::Command};
use util::rel_path::RelPath;
use workspace::Workspace;
use worktree::Snapshot;
use crate::{DevContainerFeature, DevContainerSettings, DevContainerTemplate};
use crate::{DevContainerContext, DevContainerFeature, DevContainerTemplate};
/// Represents a discovered devcontainer configuration
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -67,6 +65,31 @@ pub(crate) struct DevContainerConfigurationOutput {
configuration: DevContainerConfiguration,
}
pub(crate) struct DevContainerCli {
pub path: PathBuf,
node_runtime_path: Option<PathBuf>,
}
impl DevContainerCli {
fn command(&self, use_podman: bool) -> Command {
let mut command = if let Some(node_runtime_path) = &self.node_runtime_path {
let mut command = util::command::new_smol_command(
node_runtime_path.as_os_str().display().to_string(),
);
command.arg(self.path.display().to_string());
command
} else {
util::command::new_smol_command(self.path.display().to_string())
};
if use_podman {
command.arg("--docker-path");
command.arg("podman");
}
command
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DevContainerError {
DockerNotAvailable,
@@ -107,87 +130,23 @@ impl Display for DevContainerError {
}
}
pub(crate) async fn read_devcontainer_configuration_for_project(
cx: &mut AsyncWindowContext,
node_runtime: &NodeRuntime,
) -> Result<DevContainerConfigurationOutput, DevContainerError> {
let (path_to_devcontainer_cli, found_in_path) = ensure_devcontainer_cli(&node_runtime).await?;
let Some(directory) = project_directory(cx) else {
return Err(DevContainerError::NotInValidProject);
};
devcontainer_read_configuration(
&path_to_devcontainer_cli,
found_in_path,
node_runtime,
&directory,
None,
use_podman(cx),
)
.await
}
pub(crate) async fn apply_dev_container_template(
template: &DevContainerTemplate,
options_selected: &HashMap<String, String>,
features_selected: &HashSet<DevContainerFeature>,
cx: &mut AsyncWindowContext,
node_runtime: &NodeRuntime,
) -> Result<DevContainerApply, DevContainerError> {
let (path_to_devcontainer_cli, found_in_path) = ensure_devcontainer_cli(&node_runtime).await?;
let Some(directory) = project_directory(cx) else {
return Err(DevContainerError::NotInValidProject);
};
devcontainer_template_apply(
template,
options_selected,
features_selected,
&path_to_devcontainer_cli,
found_in_path,
node_runtime,
&directory,
false, // devcontainer template apply does not use --docker-path option
)
.await
}
fn use_podman(cx: &mut AsyncWindowContext) -> bool {
cx.update(|_, cx| DevContainerSettings::get_global(cx).use_podman)
.unwrap_or(false)
}
/// Finds all available devcontainer configurations in the project.
///
/// See [`find_configs_in_snapshot`] for the locations that are scanned.
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");
pub fn find_devcontainer_configs(workspace: &Workspace, cx: &gpui::App) -> Vec<DevContainerConfig> {
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 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);
find_configs_in_snapshot(worktree)
}) else {
log::debug!("find_devcontainer_configs: Failed to update workspace");
return Vec::new();
};
configs
let worktree = worktree.read(cx);
find_configs_in_snapshot(worktree)
}
/// Scans a worktree snapshot for devcontainer configurations.
@@ -280,60 +239,36 @@ pub fn find_configs_in_snapshot(snapshot: &Snapshot) -> Vec<DevContainerConfig>
}
pub async fn start_dev_container_with_config(
cx: &mut AsyncWindowContext,
node_runtime: NodeRuntime,
context: DevContainerContext,
config: Option<DevContainerConfig>,
) -> Result<(DevContainerConnection, String), DevContainerError> {
let use_podman = use_podman(cx);
check_for_docker(use_podman).await?;
check_for_docker(context.use_podman).await?;
let cli = ensure_devcontainer_cli(&context.node_runtime).await?;
let config_path = config.map(|c| context.project_directory.join(&c.config_path));
let (path_to_devcontainer_cli, found_in_path) = ensure_devcontainer_cli(&node_runtime).await?;
let Some(directory) = project_directory(cx) else {
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
{
match devcontainer_up(&context, &cli, config_path.as_deref()).await {
Ok(DevContainerUp {
container_id,
remote_workspace_folder,
remote_user,
..
}) => {
let project_name = match devcontainer_read_configuration(
&path_to_devcontainer_cli,
found_in_path,
&node_runtime,
&directory,
config_path.as_ref(),
use_podman,
)
.await
{
Ok(DevContainerConfigurationOutput {
configuration:
DevContainerConfiguration {
name: Some(project_name),
},
}) => project_name,
_ => get_backup_project_name(&remote_workspace_folder, &container_id),
};
let project_name =
match read_devcontainer_configuration(&context, &cli, config_path.as_deref()).await
{
Ok(DevContainerConfigurationOutput {
configuration:
DevContainerConfiguration {
name: Some(project_name),
},
}) => project_name,
_ => get_backup_project_name(&remote_workspace_folder, &container_id),
};
let connection = DevContainerConnection {
name: project_name,
container_id: container_id,
use_podman,
container_id,
use_podman: context.use_podman,
remote_user,
};
@@ -377,9 +312,9 @@ async fn check_for_docker(use_podman: bool) -> Result<(), DevContainerError> {
}
}
async fn ensure_devcontainer_cli(
pub(crate) async fn ensure_devcontainer_cli(
node_runtime: &NodeRuntime,
) -> Result<(PathBuf, bool), DevContainerError> {
) -> Result<DevContainerCli, DevContainerError> {
let mut command = util::command::new_smol_command(&dev_container_cli());
command.arg("--version");
@@ -417,7 +352,10 @@ async fn ensure_devcontainer_cli(
Ok(output) => {
if output.status.success() {
log::info!("Found devcontainer CLI in Data dir");
return Ok((datadir_cli_path.clone(), false));
return Ok(DevContainerCli {
path: datadir_cli_path.clone(),
node_runtime_path: Some(node_runtime_path.clone()),
});
} else {
log::error!(
"Could not run devcontainer CLI from data_dir. Will try once more to install. Output: {:?}",
@@ -457,32 +395,29 @@ async fn ensure_devcontainer_cli(
);
Err(DevContainerError::DevContainerCliNotAvailable)
} else {
Ok((datadir_cli_path, false))
Ok(DevContainerCli {
path: datadir_cli_path,
node_runtime_path: Some(node_runtime_path),
})
}
} else {
log::info!("Found devcontainer cli on $PATH, using it");
Ok((PathBuf::from(&dev_container_cli()), true))
Ok(DevContainerCli {
path: PathBuf::from(&dev_container_cli()),
node_runtime_path: None,
})
}
}
async fn devcontainer_up(
path_to_cli: &PathBuf,
found_in_path: bool,
node_runtime: &NodeRuntime,
path: Arc<Path>,
config_path: Option<PathBuf>,
use_podman: bool,
context: &DevContainerContext,
cli: &DevContainerCli,
config_path: Option<&Path>,
) -> Result<DevContainerUp, DevContainerError> {
let Ok(node_runtime_path) = node_runtime.binary_path().await else {
log::error!("Unable to find node runtime path");
return Err(DevContainerError::NodeRuntimeNotAvailable);
};
let mut command =
devcontainer_cli_command(path_to_cli, found_in_path, &node_runtime_path, use_podman);
let mut command = cli.command(context.use_podman);
command.arg("up");
command.arg("--workspace-folder");
command.arg(path.display().to_string());
command.arg(context.project_directory.display().to_string());
if let Some(config) = config_path {
command.arg("--config");
@@ -515,24 +450,15 @@ async fn devcontainer_up(
}
}
async fn devcontainer_read_configuration(
path_to_cli: &PathBuf,
found_in_path: bool,
node_runtime: &NodeRuntime,
path: &Arc<Path>,
config_path: Option<&PathBuf>,
use_podman: bool,
pub(crate) async fn read_devcontainer_configuration(
context: &DevContainerContext,
cli: &DevContainerCli,
config_path: Option<&Path>,
) -> Result<DevContainerConfigurationOutput, DevContainerError> {
let Ok(node_runtime_path) = node_runtime.binary_path().await else {
log::error!("Unable to find node runtime path");
return Err(DevContainerError::NodeRuntimeNotAvailable);
};
let mut command =
devcontainer_cli_command(path_to_cli, found_in_path, &node_runtime_path, use_podman);
let mut command = cli.command(context.use_podman);
command.arg("read-configuration");
command.arg("--workspace-folder");
command.arg(path.display().to_string());
command.arg(context.project_directory.display().to_string());
if let Some(config) = config_path {
command.arg("--config");
@@ -562,23 +488,14 @@ async fn devcontainer_read_configuration(
}
}
async fn devcontainer_template_apply(
pub(crate) async fn apply_dev_container_template(
template: &DevContainerTemplate,
template_options: &HashMap<String, String>,
features_selected: &HashSet<DevContainerFeature>,
path_to_cli: &PathBuf,
found_in_path: bool,
node_runtime: &NodeRuntime,
path: &Arc<Path>,
use_podman: bool,
context: &DevContainerContext,
cli: &DevContainerCli,
) -> Result<DevContainerApply, DevContainerError> {
let Ok(node_runtime_path) = node_runtime.binary_path().await else {
log::error!("Unable to find node runtime path");
return Err(DevContainerError::NodeRuntimeNotAvailable);
};
let mut command =
devcontainer_cli_command(path_to_cli, found_in_path, &node_runtime_path, use_podman);
let mut command = cli.command(context.use_podman);
let Ok(serialized_options) = serde_json::to_string(template_options) else {
log::error!("Unable to serialize options for {:?}", template_options);
@@ -588,7 +505,7 @@ async fn devcontainer_template_apply(
command.arg("templates");
command.arg("apply");
command.arg("--workspace-folder");
command.arg(path.display().to_string());
command.arg(context.project_directory.display().to_string());
command.arg("--template-id");
command.arg(format!(
"{}/{}",
@@ -652,28 +569,6 @@ fn parse_json_from_cli<T: serde::de::DeserializeOwned>(raw: &str) -> Result<T, D
})
}
fn devcontainer_cli_command(
path_to_cli: &PathBuf,
found_in_path: bool,
node_runtime_path: &PathBuf,
use_podman: bool,
) -> Command {
let mut command = if found_in_path {
util::command::new_smol_command(path_to_cli.display().to_string())
} else {
let mut command =
util::command::new_smol_command(node_runtime_path.as_os_str().display().to_string());
command.arg(path_to_cli.display().to_string());
command
};
if use_podman {
command.arg("--docker-path");
command.arg("podman");
}
command
}
fn get_backup_project_name(remote_workspace_folder: &str, container_id: &str) -> String {
Path::new(remote_workspace_folder)
.file_name()
@@ -682,22 +577,6 @@ fn get_backup_project_name(remote_workspace_folder: &str, container_id: &str) ->
.unwrap_or_else(|| container_id.to_string())
}
fn project_directory(cx: &mut AsyncWindowContext) -> Option<Arc<Path>> {
let Some(workspace) = cx.window_handle().downcast::<Workspace>() else {
return None;
};
match workspace.update(cx, |workspace, _, cx| {
workspace.project().read(cx).active_project_directory(cx)
}) {
Ok(dir) => dir,
Err(e) => {
log::error!("Error getting project directory from workspace: {:?}", e);
None
}
}
}
fn template_features_to_json(features_selected: &HashSet<DevContainerFeature>) -> String {
let features_map = features_selected
.iter()
@@ -725,6 +604,9 @@ fn template_features_to_json(features_selected: &HashSet<DevContainerFeature>) -
mod tests {
use std::path::PathBuf;
use crate::devcontainer_api::{
DevContainerConfig, DevContainerUp, find_configs_in_snapshot, parse_json_from_cli,
};
use fs::FakeFs;
use gpui::TestAppContext;
use project::Project;
@@ -732,10 +614,6 @@ mod tests {
use settings::SettingsStore;
use util::path;
use crate::devcontainer_api::{
DevContainerConfig, DevContainerUp, find_configs_in_snapshot, parse_json_from_cli,
};
fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);