diff --git a/crates/cli/src/cli.rs b/crates/cli/src/cli.rs index 1a3ce059b8116ac7438f3eb0330b47660cc863de..d8da78c53210230597dab49ce297d9fa694e62f1 100644 --- a/crates/cli/src/cli.rs +++ b/crates/cli/src/cli.rs @@ -21,6 +21,7 @@ pub enum CliRequest { reuse: bool, env: Option>, user_data_dir: Option, + dev_container: bool, }, } diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index b8af5896285d3080ca3320a5909b3f58f72de643..41f2d14c1908ac18e7ea297eef19d8d9bd1cf8b5 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -118,6 +118,12 @@ struct Args { /// Will attempt to give the correct command to run #[arg(long)] system_specs: bool, + /// Open the project in a dev container. + /// + /// Automatically triggers "Reopen in Dev Container" if a `.devcontainer/` + /// configuration is found in the project directory. + #[arg(long)] + dev_container: bool, /// Pairs of file paths to diff. Can be specified multiple times. /// When directories are provided, recurses into them and shows all changed files in a single multi-diff view. #[arg(long, action = clap::ArgAction::Append, num_args = 2, value_names = ["OLD_PATH", "NEW_PATH"])] @@ -670,6 +676,7 @@ fn main() -> Result<()> { reuse: args.reuse, env, user_data_dir: user_data_dir_for_thread, + dev_container: args.dev_container, })?; while let Ok(response) = rx.recv() { diff --git a/crates/recent_projects/src/dev_container_suggest.rs b/crates/recent_projects/src/dev_container_suggest.rs index b134833688fa081c288e5b90a371bc3c462401f0..759eef2ba32074a964979ee670c0b4ec216f404b 100644 --- a/crates/recent_projects/src/dev_container_suggest.rs +++ b/crates/recent_projects/src/dev_container_suggest.rs @@ -30,17 +30,20 @@ fn project_devcontainer_key(project_path: &str) -> String { } pub fn suggest_on_worktree_updated( + workspace: &mut Workspace, worktree_id: WorktreeId, updated_entries: &UpdatedEntriesSet, project: &gpui::Entity, window: &mut Window, cx: &mut Context, ) { + let cli_auto_open = workspace.open_in_dev_container(); + let devcontainer_updated = updated_entries.iter().any(|(path, _, _)| { path.as_ref() == devcontainer_dir_path() || path.as_ref() == devcontainer_json_path() }); - if !devcontainer_updated { + if !devcontainer_updated && !cli_auto_open { return; } @@ -54,7 +57,35 @@ pub fn suggest_on_worktree_updated( return; } - if find_configs_in_snapshot(worktree).is_empty() { + let has_configs = !find_configs_in_snapshot(worktree).is_empty(); + + if cli_auto_open { + workspace.set_open_in_dev_container(false); + let task = cx.spawn_in(window, async move |workspace, cx| { + let scans_complete = + workspace.update(cx, |workspace, cx| workspace.worktree_scans_complete(cx))?; + scans_complete.await; + + workspace.update_in(cx, |workspace, window, cx| { + let has_configs = workspace + .project() + .read(cx) + .worktrees(cx) + .any(|wt| !find_configs_in_snapshot(wt.read(cx)).is_empty()); + if has_configs { + cx.on_next_frame(window, move |_workspace, window, cx| { + window.dispatch_action(Box::new(zed_actions::OpenDevContainer), cx); + }); + } else { + log::warn!("--dev-container: no devcontainer configuration found in project"); + } + }) + }); + workspace.set_dev_container_task(task); + return; + } + + if !has_configs { return; } diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs index e1a0cb0609a9883bfe73048eda64cc8d1b299c2e..b3f918e204c5600193cd01a0f7569888d333edd9 100644 --- a/crates/recent_projects/src/recent_projects.rs +++ b/crates/recent_projects/src/recent_projects.rs @@ -475,11 +475,12 @@ pub fn init(cx: &mut App) { cx.subscribe_in( workspace.project(), window, - move |_, project, event, window, cx| { + move |workspace, project, event, window, cx| { if let project::Event::WorktreeUpdatedEntries(worktree_id, updated_entries) = event { dev_container_suggest::suggest_on_worktree_updated( + workspace, *worktree_id, updated_entries, project, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index aa692ab39a6084126c9b15b07856549364b13842..ecc03806f7eeffbb62ad1340022e0ea475fe9531 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1344,6 +1344,8 @@ pub struct Workspace { scheduled_tasks: Vec>, last_open_dock_positions: Vec, removing: bool, + open_in_dev_container: bool, + _dev_container_task: Option>>, _panels_task: Option>>, sidebar_focus_handle: Option, multi_workspace: Option>, @@ -1778,6 +1780,8 @@ impl Workspace { removing: false, sidebar_focus_handle: None, multi_workspace, + open_in_dev_container: false, + _dev_container_task: None, } } @@ -2800,6 +2804,18 @@ impl Workspace { self.debugger_provider = Some(Arc::new(provider)); } + pub fn set_open_in_dev_container(&mut self, value: bool) { + self.open_in_dev_container = value; + } + + pub fn open_in_dev_container(&self) -> bool { + self.open_in_dev_container + } + + pub fn set_dev_container_task(&mut self, task: Task>) { + self._dev_container_task = Some(task); + } + pub fn debugger_provider(&self) -> Option> { self.debugger_provider.clone() } @@ -3026,7 +3042,6 @@ impl Workspace { self.project.read(cx).visible_worktrees(cx) } - #[cfg(any(test, feature = "test-support"))] pub fn worktree_scans_complete(&self, cx: &App) -> impl Future + 'static + use<> { let futures = self .worktrees(cx) @@ -9214,6 +9229,7 @@ pub struct OpenOptions { pub requesting_window: Option>, pub open_mode: OpenMode, pub env: Option>, + pub open_in_dev_container: bool, } /// The result of opening a workspace via [`open_paths`], [`Workspace::new_local`], @@ -9393,12 +9409,17 @@ pub fn open_paths( } } + let open_in_dev_container = open_options.open_in_dev_container; + let result = if let Some((existing, target_workspace)) = existing { let open_task = existing .update(cx, |multi_workspace, window, cx| { window.activate_window(); multi_workspace.activate(target_workspace.clone(), window, cx); target_workspace.update(cx, |workspace, cx| { + if open_in_dev_container { + workspace.set_open_in_dev_container(true); + } workspace.open_paths( abs_paths, OpenOptions { @@ -9426,6 +9447,13 @@ pub fn open_paths( Ok(OpenResult { window: existing, workspace: target_workspace, opened_items: open_task }) } else { + let init = if open_in_dev_container { + Some(Box::new(|workspace: &mut Workspace, _window: &mut Window, _cx: &mut Context| { + workspace.set_open_in_dev_container(true); + }) as Box) + Send>) + } else { + None + }; let result = cx .update(move |cx| { Workspace::new_local( @@ -9433,7 +9461,7 @@ pub fn open_paths( app_state.clone(), open_options.requesting_window, open_options.env, - None, + init, open_options.open_mode, cx, ) diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 303f21b8ffa62f9d9f380d9c18beecd77775df20..0e1cbc96ff1521626bfe8bcf62091404324132a0 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -857,6 +857,7 @@ fn main() { diff_paths, wsl, diff_all: diff_all_mode, + dev_container: args.dev_container, }) } @@ -1208,6 +1209,7 @@ fn handle_open_request(request: OpenRequest, app_state: Arc, cx: &mut } let mut task = None; + let dev_container = request.dev_container; if !request.open_paths.is_empty() || !request.diff_paths.is_empty() { let app_state = app_state.clone(); task = Some(cx.spawn(async move |cx| { @@ -1218,7 +1220,10 @@ fn handle_open_request(request: OpenRequest, app_state: Arc, cx: &mut &request.diff_paths, request.diff_all, app_state, - workspace::OpenOptions::default(), + workspace::OpenOptions { + open_in_dev_container: dev_container, + ..Default::default() + }, cx, ) .await?; @@ -1636,6 +1641,13 @@ struct Args { #[arg(long, value_name = "USER@DISTRO")] wsl: Option, + /// Open the project in a dev container. + /// + /// Automatically triggers "Reopen in Dev Container" if a `.devcontainer/` + /// configuration is found in the project directory. + #[arg(long)] + dev_container: bool, + /// Instructs zed to run as a dev server on this machine. (not implemented) #[arg(long)] dev_server_token: Option, diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 75fe04feff794f21ff3fdd0e763084e4887a040b..fbebb37985c2ebd76a63db5b4b807a8a7e0203ce 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -5167,6 +5167,7 @@ mod tests { app_state.languages.add(markdown_lang()); gpui_tokio::init(cx); + AppState::set_global(app_state.clone(), cx); theme_settings::init(theme::LoadThemes::JustBase, cx); audio::init(cx); channel::init(&app_state.client, app_state.user_store.clone(), cx); diff --git a/crates/zed/src/zed/open_listener.rs b/crates/zed/src/zed/open_listener.rs index 7645eae88d69f777f650ac9f86724bfef0f10bc5..0a302291cacc8caa9e0618da00b8d7c6370ccf0e 100644 --- a/crates/zed/src/zed/open_listener.rs +++ b/crates/zed/src/zed/open_listener.rs @@ -37,6 +37,7 @@ pub struct OpenRequest { pub open_paths: Vec, pub diff_paths: Vec<[String; 2]>, pub diff_all: bool, + pub dev_container: bool, pub open_channel_notes: Vec<(u64, Option)>, pub join_channel: Option, pub remote_connection: Option, @@ -78,6 +79,7 @@ impl OpenRequest { this.diff_paths = request.diff_paths; this.diff_all = request.diff_all; + this.dev_container = request.dev_container; if let Some(wsl) = request.wsl { let (user, distro_name) = if let Some((user, distro)) = wsl.split_once('@') { if user.is_empty() { @@ -256,6 +258,7 @@ pub struct RawOpenRequest { pub urls: Vec, pub diff_paths: Vec<[String; 2]>, pub diff_all: bool, + pub dev_container: bool, pub wsl: Option, } @@ -413,6 +416,7 @@ pub async fn handle_cli_connection( reuse, env, user_data_dir: _, + dev_container, } => { if !urls.is_empty() { cx.update(|cx| { @@ -421,6 +425,7 @@ pub async fn handle_cli_connection( urls, diff_paths, diff_all, + dev_container, wsl, }, cx, @@ -450,6 +455,7 @@ pub async fn handle_cli_connection( reuse, &responses, wait, + dev_container, app_state.clone(), env, cx, @@ -471,6 +477,7 @@ async fn open_workspaces( reuse: bool, responses: &IpcSender, wait: bool, + dev_container: bool, app_state: Arc, env: Option>, cx: &mut AsyncApp, @@ -532,6 +539,7 @@ async fn open_workspaces( requesting_window: replace_window, wait, env: env.clone(), + open_in_dev_container: dev_container, ..Default::default() }; @@ -1545,4 +1553,123 @@ mod tests { }) .unwrap(); } + + #[gpui::test] + async fn test_dev_container_flag_opens_modal(cx: &mut TestAppContext) { + let app_state = init_test(cx); + cx.update(|cx| recent_projects::init(cx)); + + app_state + .fs + .as_fake() + .insert_tree( + path!("/project"), + json!({ + ".devcontainer": { + "devcontainer.json": "{}" + }, + "src": { + "main.rs": "fn main() {}" + } + }), + ) + .await; + + let (response_tx, _) = ipc::channel::().unwrap(); + let errored = cx + .spawn({ + let app_state = app_state.clone(); + |mut cx| async move { + open_local_workspace( + vec![path!("/project").to_owned()], + vec![], + false, + workspace::OpenOptions { + open_in_dev_container: true, + ..Default::default() + }, + &response_tx, + &app_state, + &mut cx, + ) + .await + } + }) + .await; + + assert!(!errored); + + let multi_workspace = cx.update(|cx| cx.windows()[0].downcast::().unwrap()); + multi_workspace + .update(cx, |multi_workspace, _, cx| { + let flag = multi_workspace.workspace().read(cx).open_in_dev_container(); + assert!( + !flag, + "open_in_dev_container flag should be consumed by suggest_on_worktree_updated" + ); + }) + .unwrap(); + } + + #[gpui::test] + async fn test_dev_container_flag_cleared_without_config(cx: &mut TestAppContext) { + let app_state = init_test(cx); + cx.update(|cx| recent_projects::init(cx)); + + app_state + .fs + .as_fake() + .insert_tree( + path!("/project"), + json!({ + "src": { + "main.rs": "fn main() {}" + } + }), + ) + .await; + + let (response_tx, _) = ipc::channel::().unwrap(); + let errored = cx + .spawn({ + let app_state = app_state.clone(); + |mut cx| async move { + open_local_workspace( + vec![path!("/project").to_owned()], + vec![], + false, + workspace::OpenOptions { + open_in_dev_container: true, + ..Default::default() + }, + &response_tx, + &app_state, + &mut cx, + ) + .await + } + }) + .await; + + assert!(!errored); + + // Let any pending worktree scan events and updates settle. + cx.run_until_parked(); + + // With no .devcontainer config, the flag should be cleared once the + // worktree scan completes, rather than persisting on the workspace. + let multi_workspace = cx.update(|cx| cx.windows()[0].downcast::().unwrap()); + multi_workspace + .update(cx, |multi_workspace, _, cx| { + let flag = multi_workspace + .workspace() + .read(cx) + .open_in_dev_container(); + assert!( + !flag, + "open_in_dev_container flag should be cleared when no devcontainer config exists" + ); + }) + .unwrap(); + } } diff --git a/crates/zed/src/zed/windows_only_instance.rs b/crates/zed/src/zed/windows_only_instance.rs index 5790715bc13bdcc68d180519d9176873bd81bc50..f22f49e26a982cb8cb68e21645033819e059de36 100644 --- a/crates/zed/src/zed/windows_only_instance.rs +++ b/crates/zed/src/zed/windows_only_instance.rs @@ -162,6 +162,7 @@ fn send_args_to_instance(args: &Args) -> anyhow::Result<()> { reuse: false, env: None, user_data_dir: args.user_data_dir.clone(), + dev_container: args.dev_container, } };