diff --git a/gpui/src/lib.rs b/gpui/src/lib.rs index eef55742cb749679a7bbd9469f0d35b2954c6a47..216ed79b327039225be7421ffb850a796bfbf616 100644 --- a/gpui/src/lib.rs +++ b/gpui/src/lib.rs @@ -25,7 +25,7 @@ pub mod json; pub mod keymap; mod platform; pub use gpui_macros::test; -pub use platform::{Event, PathPromptOptions, PromptLevel}; +pub use platform::{Event, PathPromptOptions, Platform, PromptLevel}; pub use presenter::{ AfterLayoutContext, Axis, DebugContext, EventContext, LayoutContext, PaintContext, SizeConstraint, Vector2FExt, diff --git a/zed/src/lib.rs b/zed/src/lib.rs index 56b696fb4b746abd5c4c64362a1f34d18a9e4db7..2e63e882f90697eb958bedb207e53b4ee0a1be34 100644 --- a/zed/src/lib.rs +++ b/zed/src/lib.rs @@ -1,12 +1,3 @@ -use anyhow::{anyhow, Context, Result}; -use gpui::{AsyncAppContext, MutableAppContext, Task}; -use rpc_client::RpcClient; -use std::{convert::TryFrom, time::Duration}; -use tiny_http::{Header, Response, Server}; -use url::Url; -use util::SurfResultExt; -use zed_rpc::{proto, rest::CreateWorktreeResponse}; - pub mod assets; pub mod editor; pub mod file_finder; @@ -29,146 +20,10 @@ pub struct AppState { pub language_registry: std::sync::Arc, } -pub fn init(cx: &mut MutableAppContext) { - cx.add_global_action("app:share_worktree", share_worktree); +pub fn init(cx: &mut gpui::MutableAppContext) { cx.add_global_action("app:quit", quit); } -fn share_worktree(_: &(), cx: &mut MutableAppContext) { - let zed_url = std::env::var("ZED_SERVER_URL").unwrap_or("https://zed.dev".to_string()); - let executor = cx.background_executor().clone(); - - let task = cx.spawn::<_, _, surf::Result<()>>(|cx| async move { - let (user_id, access_token) = login(zed_url.clone(), &cx).await?; - - let mut response = surf::post(format!("{}/api/worktrees", &zed_url)) - .header( - "Authorization", - http_auth_basic::Credentials::new(&user_id, &access_token).as_http_header(), - ) - .await - .context("")?; - - let CreateWorktreeResponse { - worktree_id, - rpc_address, - } = response.body_json().await?; - - eprintln!("got worktree response: {:?} {:?}", worktree_id, rpc_address); - - // TODO - If the `ZED_SERVER_URL` uses https, then wrap this stream in - // a TLS stream using `native-tls`. - let stream = smol::net::TcpStream::connect(rpc_address).await?; - - let rpc_client = RpcClient::new(stream, executor); - - let auth_response = rpc_client - .request(proto::from_client::Auth { - user_id: user_id.parse::()?, - access_token, - }) - .await?; - if !auth_response.credentials_valid { - Err(anyhow!("failed to authenticate with RPC server"))?; - } - - let share_response = rpc_client - .request(proto::from_client::ShareWorktree { - worktree_id: worktree_id as u64, - files: Vec::new(), - }) - .await?; - - log::info!("sharing worktree {:?}", share_response); - - Ok(()) - }); - - cx.spawn(|_| async move { - if let Err(e) = task.await { - log::error!("sharing failed: {}", e); - } - }) - .detach(); -} - -fn login(zed_url: String, cx: &AsyncAppContext) -> Task> { - let platform = cx.platform(); - let executor = cx.background_executor(); - executor.clone().spawn(async move { - if let Some((user_id, access_token)) = platform.read_credentials(&zed_url) { - log::info!("already signed in. user_id: {}", user_id); - return Ok((user_id, String::from_utf8(access_token).unwrap())); - } - - // Generate a pair of asymmetric encryption keys. The public key will be used by the - // zed server to encrypt the user's access token, so that it can'be intercepted by - // any other app running on the user's device. - let (public_key, private_key) = - zed_rpc::auth::keypair().expect("failed to generate keypair for auth"); - let public_key_string = - String::try_from(public_key).expect("failed to serialize public key for auth"); - - // Start an HTTP server to receive the redirect from Zed's sign-in page. - let server = Server::http("127.0.0.1:0").expect("failed to find open port"); - let port = server.server_addr().port(); - - // Open the Zed sign-in page in the user's browser, with query parameters that indicate - // that the user is signing in from a Zed app running on the same device. - platform.open_url(&format!( - "{}/sign_in?native_app_port={}&native_app_public_key={}", - zed_url, port, public_key_string - )); - - // Receive the HTTP request from the user's browser. Retrieve the user id and encrypted - // access token from the query params. - // - // TODO - Avoid ever starting more than one HTTP server. Maybe switch to using a - // custom URL scheme instead of this local HTTP server. - let (user_id, access_token) = executor - .spawn::, _>(async move { - if let Some(req) = server.recv_timeout(Duration::from_secs(10 * 60))? { - let path = req.url(); - let mut user_id = None; - let mut access_token = None; - let url = Url::parse(&format!("http://example.com{}", path)) - .context("failed to parse login notification url")?; - for (key, value) in url.query_pairs() { - if key == "access_token" { - access_token = Some(value.to_string()); - } else if key == "user_id" { - user_id = Some(value.to_string()); - } - } - req.respond( - Response::from_string(LOGIN_RESPONSE) - .with_header(Header::from_bytes("Content-Type", "text/html").unwrap()), - ) - .context("failed to respond to login http request")?; - Ok(user_id.zip(access_token)) - } else { - Ok(None) - } - }) - .await? - .ok_or_else(|| anyhow!(""))?; - - let access_token = private_key - .decrypt_string(&access_token) - .context("failed to decrypt access token")?; - platform.activate(true); - platform.write_credentials(&zed_url, &user_id, access_token.as_bytes()); - Ok((user_id.to_string(), access_token)) - }) -} - -fn quit(_: &(), cx: &mut MutableAppContext) { +fn quit(_: &(), cx: &mut gpui::MutableAppContext) { cx.platform().quit(); } - -const LOGIN_RESPONSE: &'static str = " - - - - -"; diff --git a/zed/src/menus.rs b/zed/src/menus.rs index 6c671b325763e93bfdc2bc0ef4a16cd568027e97..7c67236d4fde01d9e5ed81cb4b0e63d247e37445 100644 --- a/zed/src/menus.rs +++ b/zed/src/menus.rs @@ -17,7 +17,7 @@ pub fn menus(state: AppState) -> Vec> { MenuItem::Action { name: "Share", keystroke: None, - action: "app:share_worktree", + action: "workspace:share_worktree", arg: None, }, MenuItem::Action { diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index 7bf2d7abf67b2ab63ca94bbe490a84dc432e24f4..14810c08b243a209d785b3c56ea38d5be959d467 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -1,13 +1,17 @@ pub mod pane; pub mod pane_group; + use crate::{ editor::{Buffer, Editor}, language::LanguageRegistry, + rpc_client::RpcClient, settings::Settings, time::ReplicaId, + util::SurfResultExt as _, worktree::{FileHandle, Worktree, WorktreeHandle}, AppState, }; +use anyhow::{anyhow, Context as _}; use gpui::{ color::rgbu, elements::*, json::to_string_pretty, keymap::Binding, AnyViewHandle, AppContext, ClipboardItem, Entity, ModelHandle, MutableAppContext, PathPromptOptions, PromptLevel, Task, @@ -20,10 +24,14 @@ use postage::watch; use smol::prelude::*; use std::{ collections::{hash_map::Entry, HashMap, HashSet}, + convert::TryFrom, future::Future, path::{Path, PathBuf}, sync::Arc, + time::Duration, }; +use surf::Url; +use zed_rpc::{proto, rest::CreateWorktreeResponse}; pub fn init(cx: &mut MutableAppContext) { cx.add_global_action("workspace:open", open); @@ -31,6 +39,7 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action("workspace:save", Workspace::save_active_item); cx.add_action("workspace:debug_elements", Workspace::debug_elements); cx.add_action("workspace:new_file", Workspace::open_new_file); + cx.add_action("workspace:share_worktree", Workspace::share_worktree); cx.add_bindings(vec![ Binding::new("cmd-s", "workspace:save", None), Binding::new("cmd-alt-i", "workspace:debug_elements", None), @@ -634,6 +643,65 @@ impl Workspace { }; } + fn share_worktree(&mut self, _: &(), cx: &mut ViewContext) { + let zed_url = std::env::var("ZED_SERVER_URL").unwrap_or("https://zed.dev".to_string()); + let executor = cx.background_executor().clone(); + + let task = cx.spawn::<_, _, surf::Result<()>>(|_this, cx| async move { + let (user_id, access_token) = + login(zed_url.clone(), cx.platform(), cx.background_executor()).await?; + + let mut response = surf::post(format!("{}/api/worktrees", &zed_url)) + .header( + "Authorization", + http_auth_basic::Credentials::new(&user_id, &access_token).as_http_header(), + ) + .await + .context("")?; + + let CreateWorktreeResponse { + worktree_id, + rpc_address, + } = response.body_json().await?; + + eprintln!("got worktree response: {:?} {:?}", worktree_id, rpc_address); + + // TODO - If the `ZED_SERVER_URL` uses https, then wrap this stream in + // a TLS stream using `native-tls`. + let stream = smol::net::TcpStream::connect(rpc_address).await?; + + let rpc_client = RpcClient::new(stream, executor); + + let auth_response = rpc_client + .request(proto::from_client::Auth { + user_id: user_id.parse::()?, + access_token, + }) + .await?; + if !auth_response.credentials_valid { + Err(anyhow!("failed to authenticate with RPC server"))?; + } + + let share_response = rpc_client + .request(proto::from_client::ShareWorktree { + worktree_id: worktree_id as u64, + files: Vec::new(), + }) + .await?; + + log::info!("sharing worktree {:?}", share_response); + + Ok(()) + }); + + cx.spawn(|_, _| async move { + if let Err(e) = task.await { + log::error!("sharing failed: {}", e); + } + }) + .detach(); + } + fn add_pane(&mut self, cx: &mut ViewContext) -> ViewHandle { let pane = cx.add_view(|_| Pane::new(self.settings.clone())); let pane_id = pane.id(); @@ -766,6 +834,86 @@ impl WorkspaceHandle for ViewHandle { } } +fn login( + zed_url: String, + platform: Arc, + executor: Arc, +) -> Task> { + executor.clone().spawn(async move { + if let Some((user_id, access_token)) = platform.read_credentials(&zed_url) { + log::info!("already signed in. user_id: {}", user_id); + return Ok((user_id, String::from_utf8(access_token).unwrap())); + } + + // Generate a pair of asymmetric encryption keys. The public key will be used by the + // zed server to encrypt the user's access token, so that it can'be intercepted by + // any other app running on the user's device. + let (public_key, private_key) = + zed_rpc::auth::keypair().expect("failed to generate keypair for auth"); + let public_key_string = + String::try_from(public_key).expect("failed to serialize public key for auth"); + + // Start an HTTP server to receive the redirect from Zed's sign-in page. + let server = tiny_http::Server::http("127.0.0.1:0").expect("failed to find open port"); + let port = server.server_addr().port(); + + // Open the Zed sign-in page in the user's browser, with query parameters that indicate + // that the user is signing in from a Zed app running on the same device. + platform.open_url(&format!( + "{}/sign_in?native_app_port={}&native_app_public_key={}", + zed_url, port, public_key_string + )); + + // Receive the HTTP request from the user's browser. Retrieve the user id and encrypted + // access token from the query params. + // + // TODO - Avoid ever starting more than one HTTP server. Maybe switch to using a + // custom URL scheme instead of this local HTTP server. + let (user_id, access_token) = executor + .spawn::, _>(async move { + if let Some(req) = server.recv_timeout(Duration::from_secs(10 * 60))? { + let path = req.url(); + let mut user_id = None; + let mut access_token = None; + let url = Url::parse(&format!("http://example.com{}", path)) + .context("failed to parse login notification url")?; + for (key, value) in url.query_pairs() { + if key == "access_token" { + access_token = Some(value.to_string()); + } else if key == "user_id" { + user_id = Some(value.to_string()); + } + } + req.respond( + tiny_http::Response::from_string(LOGIN_RESPONSE).with_header( + tiny_http::Header::from_bytes("Content-Type", "text/html").unwrap(), + ), + ) + .context("failed to respond to login http request")?; + Ok(user_id.zip(access_token)) + } else { + Ok(None) + } + }) + .await? + .ok_or_else(|| anyhow!(""))?; + + let access_token = private_key + .decrypt_string(&access_token) + .context("failed to decrypt access token")?; + platform.activate(true); + platform.write_credentials(&zed_url, &user_id, access_token.as_bytes()); + Ok((user_id.to_string(), access_token)) + }) +} + +const LOGIN_RESPONSE: &'static str = " + + + + +"; + #[cfg(test)] mod tests { use super::*;