diff --git a/crates/git_ui/src/clone.rs b/crates/git_ui/src/clone.rs new file mode 100644 index 00000000000000..6b120dad2776ce --- /dev/null +++ b/crates/git_ui/src/clone.rs @@ -0,0 +1,128 @@ +use gpui::{App, Context, Window}; +use notifications::status_toast::{StatusToast, ToastIcon}; +use std::sync::Arc; +use ui::{Color, IconName}; +use util::ResultExt; +use workspace::{self, AppState}; + +pub fn clone_and_open( + repo_url: String, + app_state: Arc, + cx: &mut App, + on_success: Arc< + dyn Fn(&mut workspace::Workspace, &mut Window, &mut Context) + + Send + + Sync + + 'static, + >, +) { + workspace::with_active_or_new_workspace(cx, |_workspace, window, cx| { + let path_prompt = cx.prompt_for_paths(gpui::PathPromptOptions { + files: false, + directories: true, + multiple: false, + prompt: Some("Select directory for cloned repository".into()), + }); + + cx.spawn_in(window, async move |workspace, cx| { + let mut paths = path_prompt.await.ok()?.ok()??; + let mut path = paths.pop()?; + + let repo_name = repo_url + .split('/') + .next_back() + .and_then(|name| name.strip_suffix(".git")) + .unwrap_or("repository") + .to_owned(); + + let fs = workspace + .read_with(cx, |workspace, _| workspace.app_state().fs.clone()) + .ok()?; + + let prompt_answer = match fs.git_clone(&repo_url, path.as_path()).await { + Ok(_) => cx.update(|window, cx| { + window.prompt( + gpui::PromptLevel::Info, + &format!("Git Clone: \"{}\"", repo_name), + None, + &["Add to current project", "Open in new window"], + cx, + ) + }), + Err(e) => { + workspace + .update(cx, |workspace, cx| { + let toast = StatusToast::new(e.to_string(), cx, |this, _| { + this.icon(ToastIcon::new(IconName::XCircle).color(Color::Error)) + .dismiss_button(true) + }); + workspace.toggle_status_toast(toast, cx); + }) + .ok()?; + + return None; + } + } + .ok()?; + + path.push(&repo_name); + + match prompt_answer.await.ok()? { + 0 => { + workspace + .update_in(cx, |workspace, window, cx| { + let worktree_task = workspace.project().update(cx, |project, cx| { + project.create_worktree(path.as_path(), true, cx) + }); + let workspace_weak = cx.weak_entity(); + cx.spawn_in(window, async move |_window, cx| { + if worktree_task.await.log_err().is_some() { + workspace_weak + .update_in(cx, |workspace, window, cx| { + (on_success)(workspace, window, cx); + }) + .ok(); + } + }) + .detach(); + }) + .ok()?; + } + 1 => { + workspace + .update(cx, move |_workspace, cx| { + workspace::open_new( + Default::default(), + app_state, + cx, + move |workspace, window, cx| { + cx.activate(true); + let worktree_task = + workspace.project().update(cx, |project, cx| { + project.create_worktree(&path, true, cx) + }); + let workspace_weak = cx.weak_entity(); + cx.spawn_in(window, async move |_window, cx| { + if worktree_task.await.log_err().is_some() { + workspace_weak + .update_in(cx, |workspace, window, cx| { + (on_success)(workspace, window, cx); + }) + .ok(); + } + }) + .detach(); + }, + ) + .detach(); + }) + .ok(); + } + _ => {} + } + + Some(()) + }) + .detach(); + }); +} diff --git a/crates/git_ui/src/git_ui.rs b/crates/git_ui/src/git_ui.rs index b4e833f7af72cf..5b8b721d8ecf42 100644 --- a/crates/git_ui/src/git_ui.rs +++ b/crates/git_ui/src/git_ui.rs @@ -9,6 +9,7 @@ use ui::{ }; mod blame_ui; +pub mod clone; use git::{ repository::{Branch, Upstream, UpstreamTracking, UpstreamTrackingStatus}, diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 14e718ec2457b7..736ade7dba62fa 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -15,11 +15,13 @@ use extension::ExtensionHostProxy; use fs::{Fs, RealFs}; use futures::{StreamExt, channel::oneshot, future}; use git::GitHostingProviderRegistry; +use git_ui::clone::clone_and_open; use gpui::{App, AppContext, Application, AsyncApp, Focusable as _, QuitMode, UpdateGlobal as _}; use gpui_tokio::Tokio; use language::LanguageRegistry; use onboarding::{FIRST_OPEN, show_onboarding_view}; +use project_panel::ProjectPanel; use prompt_store::PromptBuilder; use remote::RemoteConnectionOptions; use reqwest_client::ReqwestClient; @@ -873,6 +875,17 @@ fn handle_open_request(request: OpenRequest, app_state: Arc, cx: &mut }) .detach_and_log_err(cx); } + OpenRequestKind::GitClone { repo_url } => { + let app_state = app_state.clone(); + clone_and_open( + repo_url, + app_state, + cx, + Arc::new(|workspace: &mut workspace::Workspace, window, cx| { + workspace.focus_panel::(window, cx); + }), + ); + } } return; diff --git a/crates/zed/src/zed/open_listener.rs b/crates/zed/src/zed/open_listener.rs index 13b636731798eb..2a9ca48c09cfb7 100644 --- a/crates/zed/src/zed/open_listener.rs +++ b/crates/zed/src/zed/open_listener.rs @@ -57,6 +57,9 @@ pub enum OpenRequestKind { // None just opens settings without navigating to a specific path setting_path: Option, }, + GitClone { + repo_url: String, + }, } impl OpenRequest { @@ -109,6 +112,12 @@ impl OpenRequest { this.kind = Some(OpenRequestKind::Setting { setting_path: Some(setting_path.to_string()), }); + } else if let Some(repo_url) = url.strip_prefix("zed://git/clone/") { + this.kind = Some(OpenRequestKind::GitClone { + repo_url: urlencoding::decode(repo_url) + .unwrap_or_else(|_| repo_url.into()) + .to_string(), + }); } else if url.starts_with("ssh://") { this.parse_ssh_file_path(&url, cx)? } else if let Some(request_path) = parse_zed_link(&url, cx) { @@ -929,4 +938,53 @@ mod tests { assert!(!errored_reuse); } + + #[gpui::test] + fn test_parse_git_clone_url(cx: &mut TestAppContext) { + let _app_state = init_test(cx); + + let request = cx.update(|cx| { + OpenRequest::parse( + RawOpenRequest { + urls: vec!["zed://git/clone/https://github.com/zed-industries/zed.git".into()], + ..Default::default() + }, + cx, + ) + .unwrap() + }); + + match request.kind { + Some(OpenRequestKind::GitClone { repo_url }) => { + assert_eq!(repo_url, "https://github.com/zed-industries/zed.git"); + } + _ => panic!("Expected GitClone kind"), + } + } + + #[gpui::test] + fn test_parse_git_clone_url_with_encoding(cx: &mut TestAppContext) { + let _app_state = init_test(cx); + + let request = cx.update(|cx| { + OpenRequest::parse( + RawOpenRequest { + urls: vec![ + "zed://git/clone/https%3A%2F%2Fgithub.com%2Fzed-industries%2Fzed.git" + .into(), + ], + ..Default::default() + }, + cx, + ) + .unwrap() + }); + + match request.kind { + Some(OpenRequestKind::GitClone { repo_url }) => { + assert_eq!(repo_url, "https://github.com/zed-industries/zed.git"); + } + _ => panic!("Expected GitClone kind"), + } + } }