logiguard fork v3: full patch set on verified 8c74db0 tree
Includes prior-session patches (carry forward so the app compiles): - crates/gpui/build.rs: cross-compile manifest fix - crates/gpui/src/platform.rs: PlatformWindow::activate_with_token trait method - crates/gpui/src/window.rs: Window::activate_with_token public API - crates/gpui_linux/src/linux/wayland/window.rs: WaylandWindow::activate_with_token + activate() keyboard-serial fix Plus the focus-serial fix: - serial.rs: SerialKind::KeyboardEnter - client.rs: store wl_keyboard.enter serial; latest_serial_of() Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
754
crates/dev_container/src/devcontainer_api.rs
Normal file
754
crates/dev_container/src/devcontainer_api.rs
Normal file
@@ -0,0 +1,754 @@
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
fmt::Display,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use futures::TryFutureExt;
|
||||
use gpui::{AsyncWindowContext, Entity};
|
||||
use project::Worktree;
|
||||
use serde::Deserialize;
|
||||
use settings::{DevContainerConnection, infer_json_indent_size, replace_value_in_json_text};
|
||||
use util::rel_path::RelPath;
|
||||
use walkdir::WalkDir;
|
||||
use workspace::Workspace;
|
||||
use worktree::Snapshot;
|
||||
|
||||
use crate::{
|
||||
DevContainerContext, DevContainerFeature, DevContainerTemplate,
|
||||
devcontainer_json::DevContainer,
|
||||
devcontainer_manifest::{read_devcontainer_configuration, spawn_dev_container},
|
||||
devcontainer_templates_repository, get_latest_oci_manifest, get_oci_token, ghcr_registry,
|
||||
oci::download_oci_tarball,
|
||||
};
|
||||
|
||||
/// 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"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn root_config() -> Self {
|
||||
Self {
|
||||
name: "root".to_string(),
|
||||
config_path: PathBuf::from(".devcontainer.json"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct DevContainerUp {
|
||||
pub(crate) container_id: String,
|
||||
pub(crate) remote_user: String,
|
||||
pub(crate) remote_workspace_folder: String,
|
||||
#[serde(default)]
|
||||
pub(crate) extension_ids: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub(crate) remote_env: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct DevContainerApply {
|
||||
pub(crate) project_files: Vec<Arc<RelPath>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum DevContainerError {
|
||||
CommandFailed(String),
|
||||
DockerNotAvailable,
|
||||
ContainerNotValid(String),
|
||||
DevContainerTemplateApplyFailed(String),
|
||||
DevContainerScriptsFailed,
|
||||
DevContainerUpFailed(String),
|
||||
DevContainerNotFound,
|
||||
DevContainerParseFailed,
|
||||
DevContainerValidationFailed(String),
|
||||
FilesystemError,
|
||||
ResourceFetchFailed,
|
||||
NotInValidProject,
|
||||
/// Multiple existing containers match this project's identifying labels
|
||||
/// (`devcontainer.local_folder` + `devcontainer.config_file`). The spec
|
||||
/// expects those labels to be unique per project, so Zed can't choose
|
||||
/// which one to connect to. The user must remove the duplicate(s).
|
||||
MultipleMatchingContainers(Vec<String>),
|
||||
}
|
||||
|
||||
impl Display for DevContainerError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
DevContainerError::DockerNotAvailable =>
|
||||
"docker CLI not found on $PATH".to_string(),
|
||||
DevContainerError::ContainerNotValid(id) => format!(
|
||||
"docker image {id} did not have expected configuration for a dev container"
|
||||
),
|
||||
DevContainerError::DevContainerScriptsFailed =>
|
||||
"lifecycle scripts could not execute for dev container".to_string(),
|
||||
DevContainerError::DevContainerUpFailed(_) => {
|
||||
"DevContainer creation failed".to_string()
|
||||
}
|
||||
DevContainerError::DevContainerTemplateApplyFailed(_) => {
|
||||
"DevContainer template apply failed".to_string()
|
||||
}
|
||||
DevContainerError::DevContainerNotFound =>
|
||||
"No valid dev container definition found in project".to_string(),
|
||||
DevContainerError::DevContainerParseFailed =>
|
||||
"Failed to parse file .devcontainer/devcontainer.json".to_string(),
|
||||
DevContainerError::NotInValidProject => "Not within a valid project".to_string(),
|
||||
DevContainerError::CommandFailed(program) =>
|
||||
format!("Failure running external program {program}"),
|
||||
DevContainerError::FilesystemError =>
|
||||
"Error downloading resources locally".to_string(),
|
||||
DevContainerError::ResourceFetchFailed =>
|
||||
"Failed to fetch resources from template or feature repository".to_string(),
|
||||
DevContainerError::DevContainerValidationFailed(failure) => failure.to_string(),
|
||||
DevContainerError::MultipleMatchingContainers(ids) => format!(
|
||||
"Multiple containers match this project's dev container labels ({}). \
|
||||
Zed can't decide which to connect to. Stop and remove the stale one(s) with \
|
||||
`docker stop <id>` and `docker rm <id>`, then try again.",
|
||||
ids.join(", ")
|
||||
),
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn read_default_devcontainer_configuration(
|
||||
cx: &DevContainerContext,
|
||||
environment: HashMap<String, String>,
|
||||
) -> Result<DevContainer, DevContainerError> {
|
||||
let default_config = DevContainerConfig::default_config();
|
||||
|
||||
read_devcontainer_configuration(default_config, cx, environment)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
log::error!("Default configuration not found: {:?}", e);
|
||||
DevContainerError::DevContainerNotFound
|
||||
})
|
||||
}
|
||||
|
||||
/// Finds all available devcontainer configurations in the project.
|
||||
///
|
||||
/// See [`find_configs_in_snapshot`] for the locations that are scanned.
|
||||
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 worktree = worktree.read(cx);
|
||||
find_configs_in_snapshot(worktree)
|
||||
}
|
||||
|
||||
/// Scans a worktree snapshot for devcontainer configurations.
|
||||
///
|
||||
/// Scans for configurations in these locations:
|
||||
/// 1. `.devcontainer/devcontainer.json` (the default location)
|
||||
/// 2. `.devcontainer.json` in the project root
|
||||
/// 3. `.devcontainer/<subfolder>/devcontainer.json` (named configurations)
|
||||
///
|
||||
/// All found configurations are returned so the user can pick between them.
|
||||
pub fn find_configs_in_snapshot(snapshot: &Snapshot) -> Vec<DevContainerConfig> {
|
||||
let mut configs = Vec::new();
|
||||
|
||||
let devcontainer_dir_path = RelPath::unix(".devcontainer").expect("valid path");
|
||||
|
||||
if let Some(devcontainer_entry) = snapshot.entry_for_path(devcontainer_dir_path) {
|
||||
if devcontainer_entry.is_dir() {
|
||||
log::debug!("find_configs_in_snapshot: Scanning .devcontainer directory");
|
||||
let devcontainer_json_path =
|
||||
RelPath::unix(".devcontainer/devcontainer.json").expect("valid path");
|
||||
for entry in snapshot.child_entries(devcontainer_dir_path) {
|
||||
log::debug!(
|
||||
"find_configs_in_snapshot: 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_configs_in_snapshot: 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 snapshot.entry_for_path(rel_config_path).is_some() {
|
||||
log::debug!(
|
||||
"find_configs_in_snapshot: Found config in subfolder: {}",
|
||||
subfolder_name
|
||||
);
|
||||
configs.push(DevContainerConfig {
|
||||
name: subfolder_name,
|
||||
config_path: PathBuf::from(&config_json_path),
|
||||
});
|
||||
} else {
|
||||
log::debug!(
|
||||
"find_configs_in_snapshot: Subfolder {} has no devcontainer.json",
|
||||
subfolder_name
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Always include `.devcontainer.json` so the user can pick it from the UI
|
||||
// even when `.devcontainer/devcontainer.json` also exists.
|
||||
let root_config_path = RelPath::unix(".devcontainer.json").expect("valid path");
|
||||
if snapshot
|
||||
.entry_for_path(root_config_path)
|
||||
.is_some_and(|entry| entry.is_file())
|
||||
{
|
||||
log::debug!("find_configs_in_snapshot: Found .devcontainer.json in project root");
|
||||
configs.push(DevContainerConfig::root_config());
|
||||
}
|
||||
|
||||
log::info!(
|
||||
"find_configs_in_snapshot: Found {} configurations",
|
||||
configs.len()
|
||||
);
|
||||
|
||||
configs.sort_by(|a, b| {
|
||||
let a_is_primary = a.name == "default" || a.name == "root";
|
||||
let b_is_primary = b.name == "default" || b.name == "root";
|
||||
match (a_is_primary, b_is_primary) {
|
||||
(true, false) => std::cmp::Ordering::Less,
|
||||
(false, true) => std::cmp::Ordering::Greater,
|
||||
_ => a.name.cmp(&b.name),
|
||||
}
|
||||
});
|
||||
|
||||
configs
|
||||
}
|
||||
|
||||
pub async fn start_dev_container_with_config(
|
||||
context: DevContainerContext,
|
||||
config: Option<DevContainerConfig>,
|
||||
environment: HashMap<String, String>,
|
||||
) -> Result<(DevContainerConnection, String), DevContainerError> {
|
||||
check_for_docker(context.use_podman).await?;
|
||||
|
||||
let Some(actual_config) = config.clone() else {
|
||||
return Err(DevContainerError::NotInValidProject);
|
||||
};
|
||||
|
||||
match spawn_dev_container(
|
||||
&context,
|
||||
environment.clone(),
|
||||
actual_config.clone(),
|
||||
context.project_directory.clone().as_ref(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(DevContainerUp {
|
||||
container_id,
|
||||
remote_workspace_folder,
|
||||
remote_user,
|
||||
extension_ids,
|
||||
remote_env,
|
||||
..
|
||||
}) => {
|
||||
let project_name =
|
||||
match read_devcontainer_configuration(actual_config, &context, environment).await {
|
||||
Ok(DevContainer {
|
||||
name: Some(name), ..
|
||||
}) => name,
|
||||
_ => get_backup_project_name(&remote_workspace_folder, &container_id),
|
||||
};
|
||||
|
||||
let connection = DevContainerConnection {
|
||||
name: project_name,
|
||||
container_id,
|
||||
use_podman: context.use_podman,
|
||||
remote_user,
|
||||
extension_ids,
|
||||
remote_env: remote_env.into_iter().collect(),
|
||||
};
|
||||
|
||||
Ok((connection, remote_workspace_folder))
|
||||
}
|
||||
Err(err @ DevContainerError::MultipleMatchingContainers(_)) => Err(err),
|
||||
Err(err) => {
|
||||
let message = format!("Failed with nested error: {:?}", err);
|
||||
Err(DevContainerError::DevContainerUpFailed(message))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn check_for_docker(use_podman: bool) -> Result<(), DevContainerError> {
|
||||
let mut command = if use_podman {
|
||||
util::command::new_command("podman")
|
||||
} else {
|
||||
util::command::new_command("docker")
|
||||
};
|
||||
command.arg("--version");
|
||||
|
||||
match command.output().await {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => {
|
||||
log::error!("Unable to find docker in $PATH: {:?}", e);
|
||||
Err(DevContainerError::DockerNotAvailable)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn apply_devcontainer_template(
|
||||
worktree: Entity<Worktree>,
|
||||
template: &DevContainerTemplate,
|
||||
template_options: &HashMap<String, String>,
|
||||
features_selected: &HashSet<DevContainerFeature>,
|
||||
context: &DevContainerContext,
|
||||
cx: &mut AsyncWindowContext,
|
||||
) -> Result<DevContainerApply, DevContainerError> {
|
||||
let token = get_oci_token(
|
||||
ghcr_registry(),
|
||||
devcontainer_templates_repository(),
|
||||
&context.http_client,
|
||||
)
|
||||
.map_err(|e| {
|
||||
log::error!("Failed to get OCI auth token: {e}");
|
||||
DevContainerError::ResourceFetchFailed
|
||||
})
|
||||
.await?;
|
||||
let manifest = get_latest_oci_manifest(
|
||||
&token.token,
|
||||
ghcr_registry(),
|
||||
devcontainer_templates_repository(),
|
||||
&context.http_client,
|
||||
Some(&template.id),
|
||||
)
|
||||
.map_err(|e| {
|
||||
log::error!("Failed to fetch template from OCI repository: {e}");
|
||||
DevContainerError::ResourceFetchFailed
|
||||
})
|
||||
.await?;
|
||||
|
||||
let layer = &manifest.layers.get(0).ok_or_else(|| {
|
||||
log::error!("Given manifest has no layers to query for blob. Aborting");
|
||||
DevContainerError::ResourceFetchFailed
|
||||
})?;
|
||||
|
||||
let timestamp = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_millis())
|
||||
.unwrap_or(0);
|
||||
let extract_dir = std::env::temp_dir()
|
||||
.join(&template.id)
|
||||
.join(format!("extracted-{timestamp}"));
|
||||
|
||||
context.fs.create_dir(&extract_dir).await.map_err(|e| {
|
||||
log::error!("Could not create temporary directory: {e}");
|
||||
DevContainerError::FilesystemError
|
||||
})?;
|
||||
|
||||
download_oci_tarball(
|
||||
&token.token,
|
||||
ghcr_registry(),
|
||||
devcontainer_templates_repository(),
|
||||
&layer.digest,
|
||||
"application/vnd.oci.image.manifest.v1+json",
|
||||
&extract_dir,
|
||||
&context.http_client,
|
||||
&context.fs,
|
||||
Some(&template.id),
|
||||
)
|
||||
.map_err(|e| {
|
||||
log::error!("Error downloading tarball: {:?}", e);
|
||||
DevContainerError::ResourceFetchFailed
|
||||
})
|
||||
.await?;
|
||||
|
||||
let downloaded_devcontainer_folder = &extract_dir.join(".devcontainer/");
|
||||
let mut project_files = Vec::new();
|
||||
for entry in WalkDir::new(downloaded_devcontainer_folder) {
|
||||
let Ok(entry) = entry else {
|
||||
continue;
|
||||
};
|
||||
if !entry.file_type().is_file() {
|
||||
continue;
|
||||
}
|
||||
let relative_path = entry.path().strip_prefix(&extract_dir).map_err(|e| {
|
||||
log::error!("Can't create relative path: {e}");
|
||||
DevContainerError::FilesystemError
|
||||
})?;
|
||||
let rel_path = RelPath::unix(relative_path)
|
||||
.map_err(|e| {
|
||||
log::error!("Can't create relative path: {e}");
|
||||
DevContainerError::FilesystemError
|
||||
})?
|
||||
.into_arc();
|
||||
let content = context.fs.load(entry.path()).await.map_err(|e| {
|
||||
log::error!("Unable to read file: {e}");
|
||||
DevContainerError::FilesystemError
|
||||
})?;
|
||||
|
||||
let mut content = expand_template_options(content, template_options);
|
||||
if let Some("devcontainer.json") = &rel_path.file_name() {
|
||||
content = insert_features_into_devcontainer_json(&content, features_selected)
|
||||
}
|
||||
worktree
|
||||
.update(cx, |worktree, cx| {
|
||||
worktree.create_entry(rel_path.clone(), false, Some(content.into_bytes()), cx)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| {
|
||||
log::error!("Unable to create entry in worktree: {e}");
|
||||
DevContainerError::NotInValidProject
|
||||
})?;
|
||||
project_files.push(rel_path);
|
||||
}
|
||||
|
||||
Ok(DevContainerApply { project_files })
|
||||
}
|
||||
|
||||
fn insert_features_into_devcontainer_json(
|
||||
content: &str,
|
||||
features: &HashSet<DevContainerFeature>,
|
||||
) -> String {
|
||||
if features.is_empty() {
|
||||
return content.to_string();
|
||||
}
|
||||
|
||||
let features_value: serde_json::Value = features
|
||||
.iter()
|
||||
.map(|f| {
|
||||
let key = format!(
|
||||
"{}/{}:{}",
|
||||
f.source_repository.as_deref().unwrap_or(""),
|
||||
f.id,
|
||||
f.major_version()
|
||||
);
|
||||
(key, serde_json::Value::Object(Default::default()))
|
||||
})
|
||||
.collect::<serde_json::Map<String, serde_json::Value>>()
|
||||
.into();
|
||||
|
||||
let tab_size = infer_json_indent_size(content);
|
||||
let (range, replacement) = replace_value_in_json_text(
|
||||
content,
|
||||
&["features"],
|
||||
tab_size,
|
||||
Some(&features_value),
|
||||
None,
|
||||
);
|
||||
|
||||
let mut result = content.to_string();
|
||||
result.replace_range(range, &replacement);
|
||||
result
|
||||
}
|
||||
|
||||
fn expand_template_options(content: String, template_options: &HashMap<String, String>) -> String {
|
||||
let mut replaced_content = content;
|
||||
for (key, val) in template_options {
|
||||
replaced_content = replaced_content.replace(&format!("${{templateOption:{key}}}"), val)
|
||||
}
|
||||
replaced_content
|
||||
}
|
||||
|
||||
fn get_backup_project_name(remote_workspace_folder: &str, container_id: &str) -> String {
|
||||
Path::new(remote_workspace_folder)
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.map(|string| string.to_string())
|
||||
.unwrap_or_else(|| container_id.to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::devcontainer_api::{DevContainerConfig, find_configs_in_snapshot};
|
||||
use fs::FakeFs;
|
||||
use gpui::TestAppContext;
|
||||
use project::Project;
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use util::path;
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_find_configs_root_devcontainer_json(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
path!("/project"),
|
||||
json!({
|
||||
".devcontainer.json": "{}"
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
|
||||
cx.run_until_parked();
|
||||
|
||||
let configs = project.read_with(cx, |project, cx| {
|
||||
let worktree = project
|
||||
.visible_worktrees(cx)
|
||||
.next()
|
||||
.expect("should have a worktree");
|
||||
find_configs_in_snapshot(worktree.read(cx))
|
||||
});
|
||||
|
||||
assert_eq!(configs.len(), 1);
|
||||
assert_eq!(configs[0].name, "root");
|
||||
assert_eq!(configs[0].config_path, PathBuf::from(".devcontainer.json"));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_find_configs_default_devcontainer_dir(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
path!("/project"),
|
||||
json!({
|
||||
".devcontainer": {
|
||||
"devcontainer.json": "{}"
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
|
||||
cx.run_until_parked();
|
||||
|
||||
let configs = project.read_with(cx, |project, cx| {
|
||||
let worktree = project
|
||||
.visible_worktrees(cx)
|
||||
.next()
|
||||
.expect("should have a worktree");
|
||||
find_configs_in_snapshot(worktree.read(cx))
|
||||
});
|
||||
|
||||
assert_eq!(configs.len(), 1);
|
||||
assert_eq!(configs[0], DevContainerConfig::default_config());
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_find_configs_dir_and_root_both_included(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
path!("/project"),
|
||||
json!({
|
||||
".devcontainer.json": "{}",
|
||||
".devcontainer": {
|
||||
"devcontainer.json": "{}"
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
|
||||
cx.run_until_parked();
|
||||
|
||||
let configs = project.read_with(cx, |project, cx| {
|
||||
let worktree = project
|
||||
.visible_worktrees(cx)
|
||||
.next()
|
||||
.expect("should have a worktree");
|
||||
find_configs_in_snapshot(worktree.read(cx))
|
||||
});
|
||||
|
||||
assert_eq!(configs.len(), 2);
|
||||
assert_eq!(configs[0], DevContainerConfig::default_config());
|
||||
assert_eq!(configs[1], DevContainerConfig::root_config());
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_find_configs_subfolder_configs(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
path!("/project"),
|
||||
json!({
|
||||
".devcontainer": {
|
||||
"rust": {
|
||||
"devcontainer.json": "{}"
|
||||
},
|
||||
"python": {
|
||||
"devcontainer.json": "{}"
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
|
||||
cx.run_until_parked();
|
||||
|
||||
let configs = project.read_with(cx, |project, cx| {
|
||||
let worktree = project
|
||||
.visible_worktrees(cx)
|
||||
.next()
|
||||
.expect("should have a worktree");
|
||||
find_configs_in_snapshot(worktree.read(cx))
|
||||
});
|
||||
|
||||
assert_eq!(configs.len(), 2);
|
||||
let names: Vec<&str> = configs.iter().map(|c| c.name.as_str()).collect();
|
||||
assert!(names.contains(&"python"));
|
||||
assert!(names.contains(&"rust"));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_find_configs_default_and_subfolder(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
path!("/project"),
|
||||
json!({
|
||||
".devcontainer": {
|
||||
"devcontainer.json": "{}",
|
||||
"gpu": {
|
||||
"devcontainer.json": "{}"
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
|
||||
cx.run_until_parked();
|
||||
|
||||
let configs = project.read_with(cx, |project, cx| {
|
||||
let worktree = project
|
||||
.visible_worktrees(cx)
|
||||
.next()
|
||||
.expect("should have a worktree");
|
||||
find_configs_in_snapshot(worktree.read(cx))
|
||||
});
|
||||
|
||||
assert_eq!(configs.len(), 2);
|
||||
assert_eq!(configs[0].name, "default");
|
||||
assert_eq!(configs[1].name, "gpu");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_find_configs_no_devcontainer(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
path!("/project"),
|
||||
json!({
|
||||
"src": {
|
||||
"main.rs": "fn main() {}"
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
|
||||
cx.run_until_parked();
|
||||
|
||||
let configs = project.read_with(cx, |project, cx| {
|
||||
let worktree = project
|
||||
.visible_worktrees(cx)
|
||||
.next()
|
||||
.expect("should have a worktree");
|
||||
find_configs_in_snapshot(worktree.read(cx))
|
||||
});
|
||||
|
||||
assert!(configs.is_empty());
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_find_configs_root_json_and_subfolder_configs(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
path!("/project"),
|
||||
json!({
|
||||
".devcontainer.json": "{}",
|
||||
".devcontainer": {
|
||||
"rust": {
|
||||
"devcontainer.json": "{}"
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
|
||||
cx.run_until_parked();
|
||||
|
||||
let configs = project.read_with(cx, |project, cx| {
|
||||
let worktree = project
|
||||
.visible_worktrees(cx)
|
||||
.next()
|
||||
.expect("should have a worktree");
|
||||
find_configs_in_snapshot(worktree.read(cx))
|
||||
});
|
||||
|
||||
assert_eq!(configs.len(), 2);
|
||||
assert_eq!(configs[0].name, "root");
|
||||
assert_eq!(configs[0].config_path, PathBuf::from(".devcontainer.json"));
|
||||
assert_eq!(configs[1].name, "rust");
|
||||
assert_eq!(
|
||||
configs[1].config_path,
|
||||
PathBuf::from(".devcontainer/rust/devcontainer.json")
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_find_configs_empty_devcontainer_dir_falls_back_to_root(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
path!("/project"),
|
||||
json!({
|
||||
".devcontainer.json": "{}",
|
||||
".devcontainer": {}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
|
||||
cx.run_until_parked();
|
||||
|
||||
let configs = project.read_with(cx, |project, cx| {
|
||||
let worktree = project
|
||||
.visible_worktrees(cx)
|
||||
.next()
|
||||
.expect("should have a worktree");
|
||||
find_configs_in_snapshot(worktree.read(cx))
|
||||
});
|
||||
|
||||
assert_eq!(configs.len(), 1);
|
||||
assert_eq!(configs[0], DevContainerConfig::root_config());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user